Files
TianXuan Developer cd68d7c9af fix: gen_multi2 使用真实公网IP,GeoIP可解析
根因: 服务器IP(203.0.113.x等RFC5737)和代理IP(10.0.x.x私有)无法被GeoIP解析
→ 500条边仅2条两端有坐标 → 图上不显示

修复:
- 服务器IP: 替换为 Akamai/Cloudflare/Google/AWS/Microsoft 公网IP
- 代理IP: 替换为 1.1.1.1/8.8.8.8 等已知DNS公网IP
- 移除硬编码SERVER_GEO,依赖MMDB自动解析

结果: 710/718节点有坐标, 496/500边两端可渲染 → 300条边显示

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-24 20:38:59 +08:00

217 lines
8.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Topology-aware test data generator — exact gen_test_data format.
Strategy:
1. Generate rows using gen_test_data.generate_row() (EXACT same format/coverages)
2. Then remap IPs: when snam/cnam has a value, assign :ipd from a fixed server pool
3. Result: same format as gen_test_data, but with realistic network topology
Usage:
runtime\\python\\python.exe scripts\\gen_multi2.py [--files N] [--rows N] [--seed S]
"""
import sys
import random
import csv
from pathlib import Path
from datetime import datetime, timedelta
from collections import defaultdict
sys.path.insert(0, str(Path(__file__).parent))
from gen_test_data import (
COLUMNS, COVERAGE,
generate_row as _base_row,
TLS_VERSIONS, TLS_WEIGHTS, CIPHER_HEX, CIPHER_NAMES,
NAMED_CURVES_HEX, NAMED_CURVES_TEXT,
SRC_IPS, DST_IPS, PORTS, COUNTRIES, ISPS, ORGS, CITIES,
SERVICES, SRC_NODES, NODE_NAMES,
_rand_ip, _rand_hex, _maybe, _blank_or, _blank_or_plus,
)
# ════════════════════════════════
# Server IP pools per service
# ════════════════════════════════
# Each snam/cnam maps to 1-3 specific server IPs.
# When a row has a given snam/cnam, its :ipd is replaced with one of these IPs.
# This creates realistic topology — servers have high in-degree.
# CDN: 3 edge nodes in different regions (Akamai/Cloudflare ranges)
CDN_IPS = ['23.0.5.10', '104.16.100.10', '151.101.1.10']
API_IPS = ['52.84.100.20', '54.230.100.20'] # Amazon CloudFront
MAIL_IPS = ['74.125.200.26', '66.102.1.26'] # Google mail
AUTH_IPS = ['40.126.32.100'] # Microsoft Azure
SOCIAL_IPS = ['157.240.1.35'] # Facebook
STORAGE_IPS = ['52.216.100.50'] # Amazon S3
STREAM_IPS = ['52.84.200.50', '54.192.200.50'] # Amazon CloudFront
SEARCH_IPS = ['142.250.80.4'] # Google
SNAM_TO_IPS = {
'cdn.example.com': CDN_IPS,
'*.cloudfront.net': CDN_IPS,
'api.example.com': API_IPS,
'mail.example.com': MAIL_IPS,
'auth.example.net': AUTH_IPS,
'login.live.com': AUTH_IPS,
'graph.facebook.com': SOCIAL_IPS,
'*.s3.amazonaws.com': STORAGE_IPS,
'stream.example.org': STREAM_IPS,
'www.google.com': SEARCH_IPS,
}
CNAM_TO_IPS = {
'cdn.example.com': CDN_IPS,
'*.cloudfront.net': CDN_IPS,
'api.example.com': API_IPS,
'mail.example.com': MAIL_IPS,
'auth.example.net': AUTH_IPS,
'login.live.com': AUTH_IPS,
'graph.facebook.com': SOCIAL_IPS,
'*.s3.amazonaws.com': STORAGE_IPS,
'stream.example.org': STREAM_IPS,
'www.google.com': SEARCH_IPS,
}
ALL_SERVER_IPS = list(set(
ip for pool in SNAM_TO_IPS.values() for ip in pool
))
# Proxy IPs — public IPs resolvable by GeoIP MMDB
# MUST be public IPs that GeoIP can resolve (no private ranges!)
PROXY_IPS = [
'1.1.1.1', # Cloudflare DNS — US-West
'8.8.8.8', # Google DNS — US-West
'223.5.5.5', # AliDNS — CN
'208.67.222.222', # OpenDNS — US-East
'77.88.8.8', # Yandex DNS — EU
]
def main():
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('--files', type=int, default=200)
parser.add_argument('--rows', type=int, default=10000)
parser.add_argument('--seed', type=int, default=2026)
parser.add_argument('--output-dir', type=str, default='data/gen_multi2')
args = parser.parse_args()
random.seed(args.seed)
out_dir = Path(args.output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
_keep_cols = {':ips', ':ipd', ':prs', ':prd', 'server-ip', 'client-ip'}
_droppable = [c for c in COLUMNS if c not in _keep_cols]
base_time = datetime(2026, 6, 1, 0, 0, 0)
total_rows = 0
degree_src = defaultdict(int)
degree_dst = defaultdict(int)
snam_ip_count = defaultdict(lambda: defaultdict(int))
cnam_ip_count = defaultdict(lambda: defaultdict(int))
print(f'Generating {args.files} files × {args.rows} rows → {out_dir}')
for fi in range(args.files):
# ── Column dropping (matching gen_multi.py exactly) ──
dropped = set()
for col in _droppable:
cov = COVERAGE.get(col, 1.0)
drop_prob = max(0.10, min(0.90, 1.0 - cov))
if random.random() < drop_prob:
dropped.add(col)
if len(dropped) < 3:
extra = random.sample([c for c in _droppable if c not in dropped],
k=min(3 - len(dropped), len(_droppable) - len(dropped)))
dropped.update(extra)
if len(dropped) == len(_droppable):
spared = set(random.sample(list(dropped), k=min(3, len(dropped))))
dropped -= spared
file_columns = [c for c in COLUMNS if c not in dropped]
# ── Generate rows with base format ──
file_rows = []
file_base = base_time + timedelta(seconds=random.randint(0, 86400 * 30))
for ri in range(args.rows):
row = _base_row(total_rows + ri, file_base)
# ── Fix garbled cnam/snam from gen_test_data cert_cn bug ──
# gen_test_data line 135 generates "*.['example', 'com']" garbage
# via f'*.{random.choice(SERVICES).split(".")[-2:]}' — missing .join()
snam_val = row.get('snam', '').strip()
cnam_val = row.get('cnam', '').strip()
import re
if snam_val and snam_val != '' and re.match(r'^\*\.[\[\(]', snam_val):
row['snam'] = ''
snam_val = ''
if cnam_val and cnam_val != '' and re.match(r'^\*\.[\[\(]', cnam_val):
row['cnam'] = ''
cnam_val = ''
# ── Remap IPs using topology ──
# Determine appropriate :ipd based on snam/cnam
server_pool = None
if snam_val and snam_val in SNAM_TO_IPS:
server_pool = SNAM_TO_IPS[snam_val]
elif cnam_val and cnam_val in CNAM_TO_IPS:
server_pool = CNAM_TO_IPS[cnam_val]
if server_pool:
# Assign a specific server IP for this service
dst_ip = random.choice(server_pool)
row[':ipd'] = dst_ip
row['server-ip'] = dst_ip
snam_ip_count[snam_val][dst_ip] += 1
cnam_ip_count[cnam_val][dst_ip] += 1
# 30% of the time, use a proxy as :ips instead of random client
if random.random() < 0.30:
row[':ips'] = random.choice(PROXY_IPS)
row['client-ip'] = row[':ips']
# GeoIP: use MMDB in upload flow; CSV lat/lon are ignored anyway
file_rows.append(row)
degree_src[row[':ips']] += 1
degree_dst[row[':ipd']] += 1
total_rows += args.rows
# ── Shuffle column order per file ──
shuffled = file_columns[:]
random.shuffle(shuffled)
# ── Write ──
out_path = out_dir / f'{fi}.csv'
with open(out_path, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=shuffled, extrasaction='ignore')
writer.writeheader()
for row in file_rows:
writer.writerow(row)
if (fi + 1) % 50 == 0:
print(f' [{fi + 1}/{args.files}] {total_rows} rows...')
# ── Stats ──
top_src = sorted(degree_src.items(), key=lambda x: -x[1])[:10]
top_dst = sorted(degree_dst.items(), key=lambda x: -x[1])[:10]
print(f'\nDone: {args.files} files, {total_rows} rows → {out_dir.resolve()}')
print(f' Avg src degree: {sum(degree_src.values())/len(degree_src):.1f}')
print(f' Avg dst degree: {sum(degree_dst.values())/len(degree_dst):.1f}')
print(f' Top source (out-degree):')
for ip, deg in top_src:
label = ' [proxy]' if ip in PROXY_IPS else ''
print(f' {ip}: {deg}{label}')
print(f' Top dest (in-degree):')
for ip, deg in top_dst:
is_server = ip in ALL_SERVER_IPS
print(f' {ip}: {deg}{" [server]" if is_server else ""}')
print(f' snam→IP mapping:')
for snam, ips in sorted(snam_ip_count.items()):
top = sorted(ips.items(), key=lambda x: -x[1])
print(f' {snam}: {dict(top)}')
if __name__ == '__main__':
main()