cd68d7c9af
根因: 服务器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>
217 lines
8.3 KiB
Python
217 lines
8.3 KiB
Python
"""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()
|