agent-bypass 2.16.0 → 2.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ # fake-IP / TUN 环境下的扫描与溯源(iSH / iOS / 桌面代理通用)
2
+
3
+ > 症状:iSH 或桌面开了 TUN(Clash / mihomo / sing-box / Stash / Surfboard 等)后,
4
+ > 端口扫描全超时、nmap 报 "host seems down"、curl 能通但扫不到端口、溯源拿到一堆假 IP。
5
+
6
+ ## 1. 判定:是不是被 fake-IP 劫持了
7
+
8
+ ```sh
9
+ # 解析任意已知域名,看是否落在 fake-IP 段
10
+ nslookup example.com # iSH/BusyBox 自带;或 getent ahostsv4 example.com
11
+ python3 scripts/doh_resolve.sh example.com # DoH 真 IP(正常应返回真实公网 IP)
12
+ python3 scripts/netdns.py example.com # 对照:系统解析 vs DoH
13
+ ```
14
+
15
+ fake-IP 常见段(命中任一即不可信):
16
+ - `198.18.0.0/15`(198.18.x / 198.19.x)——Clash/mihomo/sing-box 默认 fake-ip-range
17
+ - `198.51.100.0/24`、`203.0.113.0/24`(TEST-NET)与 `240.0.0.0/4`(保留段)——部分代理实现
18
+
19
+ 判据:**系统解析落在上述段、DoH 返回真实公网 IP = TUN 劫持了 UDP:53**。
20
+
21
+ ## 2. 为什么扫不动
22
+
23
+ - 端口扫描是 raw socket / connect:连接假地址(198.18.x)不可路由 → 全超时/误判全关。
24
+ - 系统解析结果会被缓存:代理切换后旧 fake-IP 仍可能残留。
25
+ - 溯源/测绘同理:子域、历史 DNS、CT 目标如果都走系统解析,拿到的都是假 IP。
26
+
27
+ ## 3. 处理优先级(从便宜到重)
28
+
29
+ 1. **DoH 拿真 IP(首选,无需动代理配置)**:
30
+ ```sh
31
+ sh scripts/doh_resolve.sh target.com # A(--aaaa 同时查 IPv6)
32
+ python3 scripts/netdns.py target.com --doh
33
+ python3 scripts/fastrecon.py dns target.com --doh # 批量 + 缓存
34
+ ```
35
+ DoH 走 HTTPS/443,不经过被劫持的 UDP:53。
36
+ 2. **端口扫描直接用真 IP,禁止再解析**:
37
+ ```sh
38
+ python3 scripts/portscan.py <真实IP> --ports top100
39
+ nmap -n -Pn -sT -p- <真实IP> # -n 不解析;-Pn 跳过主机发现
40
+ masscan <真实IP> -p1-65535 --rate=5000
41
+ ```
42
+ 本仓库 `portscan.py` 已默认检测 fake-IP:`--resolve auto` 会自动换 DoH;`--resolve doh` 强制 DoH;只剩 fake 时直接拒绝扫描(exit 3)。
43
+ 3. **HTTP/HTTPS 探测用 `--resolve` 保持 SNI**:
44
+ ```sh
45
+ curl -sk --resolve target.com:443:<真实IP> https://target.com/
46
+ ```
47
+ 4. **代理侧绕开(需要可用配置时)**:
48
+ - 规则里把目标域名/网段设为 `DIRECT`(或代理组走直连);
49
+ - `fake-ip-filter` 加入目标域名(Clash/mihomo:`fake-ip-filter: ['+.target.com']`);
50
+ - DNS 模式改 `redir-host`(真实解析)而不是 `fake-ip`;
51
+ - 关闭「TUN 劫持 DNS」/「DNS 增强」开关;
52
+ - 扫描期间别开「全局 TUN」。
53
+ 5. **验证是不是真源站/真端口**:`curl -sk --resolve` 对比标题/favicon/响应头;扫到的端口再用协议 banner 复核(如 `nc -v ip 22`、`curl -k https://ip:8443`)。
54
+
55
+ ## 4. 端口扫描 SOP(fake-IP 环境)
56
+
57
+ 1. 用 DoH 拿真 IP(域名 → 真实 IP 列表);
58
+ 2. 只对真实 IP 扫:先 `top100` 快扫,再全端口;iSH 并发 ≤200、超时 ≥3s;
59
+ 3. 扫到的开放端口用协议回连验证(不是只有 open TCP 就算);
60
+ 4. 需要域名语义时:`curl --resolve` / `Host:` / SNI 手动指定;
61
+ 5. 报告里写清「解析方式(DoH/系统)+ 扫描目标 IP + 时间」,避免把 fake-IP 结果当证据。
62
+
63
+ ## 5. 爆破注意(TUN 会改变出口与限速语义)
64
+
65
+ - **限速/锁定按什么算**:账号 vs IP vs 指纹。TUN 下出口 IP 可能是代理节点 IP(甚至多个),
66
+ 锁定策略会跟着代理池变化——先小量探测锁定律(几次锁/锁多久/锁 IP 还是账号)。
67
+ - **出口稳定优先**:爆破建议走**固定直连出口**(或固定代理节点)保持 IP 会话一致;
68
+ 频繁轮换出口会触发风控/让锁定判断失真。绕限速顺序:换头 → 换账号 → 窗口对齐 → 出口轮换(最后)。
69
+ - **别把扫描与爆破混在同一出口**:扫描流量特征明显,容易把同一出口的爆破流量一起喂给 WAF。
70
+ - **iSH 约束**:hydra 走 apk 安装;legba 在 iSH 上不可用/不稳,改用 hydra 或 python paramiko;
71
+ 并发降低(1–4),长任务 `nohup ... &` + 日志断点续跑。
72
+ - 其它细节见 `references/bruteforce-playbook.md`。
73
+
74
+ ## 6. 溯源注意(源站 / 历史解析)
75
+
76
+ - 历史 DNS、子域、CT 查询统一走 **DoH**(`scripts/origin_trace.py` 已内置),不要信系统解析;
77
+ - CT/被动 DNS 用多源并发兜底:crt.sh / CertSpotter / OTX / HackerTarget / Anubis / RapidDNS(`--ct-sources all` 默认全开;部分源 502/限速会自动跳过,`--no-ct` 可完全关闭);
78
+ - 测绘平台(FOFA/Hunter/Shodan)返回的 IP 先排除 CDN 段/回源特征,再用 `curl --resolve` 直连验证;
79
+ - 直连验证看:标题一致、Server 头、无 CDN 头(cf-ray/x-cache/via 等)、证书 SAN 匹配;
80
+ - TUN 环境下验证请求也要 `--resolve` 到候选 IP,否则 curl 又被 fake-IP 带偏;
81
+ - 结论三档:直连一致 = 确认;仅历史解析 = 线索;只有 CDN 响应 = 未穿透。
82
+
83
+ ## 7. 卡住排查(iSH 重点)
84
+
85
+ iSH 常见"一直卡住"的三个来源:DNS 解析挂起、TUN 拦截的 TCP 连接无响应、CT/HTTP 请求慢。
86
+
87
+ ```sh
88
+ # ① DNS 卡住:用 DoH(硬超时),别等系统解析
89
+ sh scripts/doh_resolve.sh example.com # curl --max-time 4
90
+ python3 scripts/netdns.py example.com --doh # 守护线程 + DoH 总时限
91
+
92
+ # ② 端口扫描卡住:加总时限,到点返回部分结果
93
+ python3 scripts/portscan.py <真实IP> --ports top100 --deadline 180
94
+
95
+ # ③ 溯源卡住:跳过 CT、缩小候选、限总预算
96
+ python3 scripts/origin_trace.py example.com --no-ct --max-candidates 8 --budget 60
97
+
98
+ # ④ 长任务别前台干等(iSH 无 GNU timeout)
99
+ nohup python3 scripts/portscan.py <ip> --ports 1-65535 --deadline 300 > scan.log 2>&1 &
100
+ # 看进度/停止
101
+ tail -f scan.log ; kill %1
102
+ ```
103
+
104
+ - iSH 上并发/超时已自动降档(portscan:并发 ≤200、超时 ≥3s)。
105
+ - 所有脚本都有内部硬超时,不允许无限等待;卡住时先降载重跑,不要同一命令反复硬等。
106
+ - 代理侧卡住:把目标加 DIRECT/fake-ip-filter,或临时关 TUN DNS 劫持再试。
107
+
108
+ ## 8. 自检清单
109
+
110
+ - [ ] `netdns.py`/`doh_resolve.sh` 能拿到非 fake 的真实 IP
111
+ - [ ] 扫描目标是真实 IP(不是 198.18.x)
112
+ - [ ] 开放端口有协议级验证,不只是 TCP connect 成功
113
+ - [ ] HTTP 验证用 `--resolve` 保持正确 SNI/Host
114
+ - [ ] 报告记录了「解析方式 + 目标 IP + 时间」
@@ -1,6 +1,14 @@
1
1
  # 工具选型与原包脚本对照(只读能力发现)
2
2
 
3
- 本表依据原始 `scripts/`、`module 13–33` 和参考项目的能力划分整理。它不是命令执行清单。`scripts/tool_inventory.py` 用于发现工具,**不会**下载依赖、扫描目标或调用下列渗透脚本。
3
+ 本表依据原始 `scripts/`、`module 00–34` 和参考项目的能力划分整理。它不是命令执行清单。`scripts/tool_inventory.py` 用于发现工具,**不会**下载依赖、扫描目标或调用下列渗透脚本。
4
+
5
+ ## 默认工具策略(先看这个)
6
+
7
+ - **文本检索 / 日志过滤 / 代码搜索 / 查配置:默认一律 `rg`(ripgrep)**;`grep` 仅在 rg 不可用且装不上时兜底。
8
+ - 安装:macOS `brew install ripgrep`;Debian/Ubuntu `apt install ripgrep`;Alpine/iSH `apk add ripgrep`;Termux `pkg install ripgrep`;Windows `winget install BurntSushi.ripgrep.MSVC`(或 scoop/choco)。
9
+ - 常用:`rg -n 关键词 路径`(行号)· `rg -l`(只列文件)· `rg -i`(忽略大小写)· `rg -S`(智能大小写)· `rg --hidden -g '!node_modules'` · `rg -A/-B/-C`(上下文)· `rg -f patterns.txt`(模式文件)。
10
+ - **fake-IP / TUN 环境**:`scripts/netdns.py`(DoH 真 IP)、`scripts/doh_resolve.sh`、`scripts/origin_trace.py`(源站溯源);细节见 `references/fake-ip-tun.md`。
11
+ - **爆破**:字典/锁定/验证细节见 `references/bruteforce-playbook.md`。
4
12
 
5
13
  ## A. 按文件类型和任务选工具
6
14
 
@@ -1,7 +1,112 @@
1
1
  #!/bin/sh
2
- # Resolve via DoH, bypassing the sandbox's fake DNS
3
- for d in "$@"; do
4
- ans=$(curl -s --noproxy '*' "https://1.1.1.1/dns-query?name=$d&type=A" -H 'accept: application/dns-json' \
5
- | tr ',' '\n' | grep '"data"' | sed 's/.*"data":"//;s/".*//' | grep -E '^[0-9]+\.' | tr '\n' ' ')
6
- echo "$d => $ans"
2
+ # DoH 解析:绕过 TUN / fake-IP(Clash/mihomo 默认 198.18.0.0/15),拿真实 IP。
3
+ #
4
+ # 用法:
5
+ # sh doh_resolve.sh example.com # A 记录
6
+ # sh doh_resolve.sh example.com --aaaa # 同时查 AAAA
7
+ # sh doh_resolve.sh a.com b.com --json # JSON 输出
8
+ # sh doh_resolve.sh example.com -q # 只输出 IP(便于管道)
9
+ # DOH_PROVIDERS="https://1.1.1.1/dns-query https://dns.google/resolve" sh doh_resolve.sh example.com
10
+ #
11
+ # 说明:走 HTTPS/443 的 DoH,不经过被 TUN 劫持的 UDP:53;
12
+ # fake-IP 段(198.18/15、198.51.100/24、203.0.113/24、240/4)会标注告警。
13
+ set -u
14
+
15
+ AAAA=0; JSON=0; QUIET=0; DOMAINS=""
16
+ for a in "$@"; do
17
+ case "$a" in
18
+ --aaaa) AAAA=1 ;;
19
+ --json) JSON=1 ;;
20
+ -q|--quiet) QUIET=1 ;;
21
+ -h|--help)
22
+ sed -n '2,14p' "$0" | sed 's/^# \{0,1\}//'
23
+ exit 0 ;;
24
+ -*) echo "未知参数: $a" >&2; exit 2 ;;
25
+ *) DOMAINS="$DOMAINS $a" ;;
26
+ esac
7
27
  done
28
+ [ -z "${DOMAINS# }" ] && { echo "用法: sh doh_resolve.sh <domain>... [--aaaa] [--json] [-q]" >&2; exit 2; }
29
+
30
+ PROVIDERS="${DOH_PROVIDERS:-https://1.1.1.1/dns-query https://dns.google/resolve https://223.5.5.5/resolve}"
31
+ DOH_TIMEOUT="${DOH_TIMEOUT:-4}"
32
+
33
+ command -v curl >/dev/null 2>&1 || { echo "[错误] 缺 curl(iSH: apk add curl)" >&2; exit 2; }
34
+
35
+ is_fake() {
36
+ case "$1" in
37
+ 198.18.*|198.19.*|240.*|198.51.100.*|203.0.113.*) return 0 ;;
38
+ esac
39
+ return 1
40
+ }
41
+
42
+ doh_raw() { # name qtype provider
43
+ curl -s --max-time "$DOH_TIMEOUT" -H 'accept: application/dns-json' "$3?name=$1&type=$2" 2>/dev/null
44
+ }
45
+
46
+ extract_ips() { # json qtype
47
+ if command -v python3 >/dev/null 2>&1; then
48
+ printf '%s' "$1" | python3 -c '
49
+ import json,sys
50
+ try:
51
+ d=json.load(sys.stdin)
52
+ except Exception:
53
+ sys.exit(0)
54
+ want=1 if sys.argv[1]=="A" else 28
55
+ seen=[]
56
+ for a in (d.get("Answer") or []):
57
+ if a.get("type")==want:
58
+ v=str(a.get("data") or "").strip()
59
+ if v and v not in seen:
60
+ seen.append(v)
61
+ print(" ".join(seen))
62
+ ' "$2"
63
+ else
64
+ printf '%s' "$1" | tr ',' '\n' | sed -n 's/.*"data":"\([0-9a-fA-F.:]*\)".*/\1/p' | tr '\n' ' '
65
+ fi
66
+ }
67
+
68
+ lookup() { # name qtype -> ips (空格分隔)
69
+ _n=$1; _t=$2; _out=""
70
+ for _p in $PROVIDERS; do
71
+ _j=$(doh_raw "$_n" "$_t" "$_p")
72
+ [ -z "$_j" ] && continue
73
+ _out=$(extract_ips "$_j" "$_t")
74
+ [ -n "$_out" ] && break
75
+ done
76
+ echo "$_out"
77
+ }
78
+
79
+ json_rows=""
80
+ for d in $DOMAINS; do
81
+ a_ips=$(lookup "$d" A)
82
+ a4=""; fake=""
83
+ for ip in $a_ips; do
84
+ if is_fake "$ip"; then fake="$fake $ip"; else a4="$a4 $ip"; fi
85
+ done
86
+ a4=$(printf '%s' "$a4" | tr ' ' '\n' | sed '/^$/d' | tr '\n' ' ' | sed 's/ $//')
87
+ fake=$(printf '%s' "$fake" | sed 's/^ //')
88
+ aaaa=""
89
+ if [ "$AAAA" = "1" ]; then
90
+ aaaa=$(lookup "$d" AAAA | sed 's/ *$//')
91
+ fi
92
+
93
+ if [ "$JSON" = "1" ]; then
94
+ esc_a=$(printf '%s' "$a4" | tr ' ' ',')
95
+ esc_aaaa=$(printf '%s' "$aaaa" | tr ' ' ',')
96
+ esc_fake=$(printf '%s' "$fake" | tr ' ' ',')
97
+ json_rows="$json_rows{\"domain\":\"$d\",\"a\":\"$esc_a\",\"aaaa\":\"$esc_aaaa\",\"fake\":\"$esc_fake\"},"
98
+ else
99
+ if [ "$QUIET" = "1" ]; then
100
+ [ -n "$a4" ] && echo "$a4"
101
+ [ "$AAAA" = "1" ] && [ -n "$aaaa" ] && echo "$aaaa"
102
+ else
103
+ echo "$d => ${a4:--}${aaaa:+ | AAAA: $aaaa}"
104
+ fi
105
+ [ -n "$fake" ] && echo " [告警] 结果含 fake-IP($fake):TUN 劫持了 DNS,该地址不可信" >&2
106
+ fi
107
+ [ -z "$a4" ] && [ -z "$aaaa" ] && echo " [失败] $d DoH 无结果(检查出网/DoH 端点)" >&2
108
+ done
109
+
110
+ if [ "$JSON" = "1" ]; then
111
+ printf '[%s]\n' "$(printf '%s' "$json_rows" | sed 's/,$//')"
112
+ fi
@@ -10,6 +10,7 @@
10
10
  用法:
11
11
  fastrecon.py dns example.com github.com # 批量解析(UDP 并发 + 缓存)
12
12
  fastrecon.py dns subs.txt -w 50 # 文件批量(每行一个域名)
13
+ fastrecon.py dns example.com --doh # 强制 DoH(绕 TUN/fake-IP 真 IP)
13
14
  fastrecon.py scan 192.168.1.1 -p top100 # 端口扫描(selectors 并发)
14
15
  fastrecon.py scan 10.0.0.0/24 -p 22,80,443 -t 0.6
15
16
  fastrecon.py http urls.txt -b 48 # HTTP 批量探测(curl -Z 并行)
@@ -36,6 +37,11 @@ import argsmini as argparse
36
37
  import ipaddress, os, random, re, selectors, socket, struct, sys, time
37
38
  import platf
38
39
 
40
+ try: # 共享 DNS 工具(同目录 netdns.py):fake-IP 判定 + DoH 真 IP
41
+ import netdns
42
+ except Exception:
43
+ netdns = None
44
+
39
45
  # 懒加载:iSH 上 concurrent.futures/subprocess import 各耗 2-3s,仅首次使用时载入
40
46
  _cf_mod = None
41
47
  def _get_cf():
@@ -197,8 +203,23 @@ def load_resolvers(override=None):
197
203
 
198
204
 
199
205
  def _is_fakeip(ip):
200
- """fake-ip 段判定:RFC 2544 198.18.0.0/15(Clash/mihomo 默认)与 240.0.0.0/4"""
201
- return ip.startswith('198.18.') or ip.startswith('198.19.') or ip.startswith('240.')
206
+ """fake-IP / 伪造段判定(优先共享 netdns;单文件场景退化)。"""
207
+ if netdns is not None:
208
+ try:
209
+ return netdns.is_fake_ip(ip)
210
+ except Exception:
211
+ pass
212
+ return str(ip).startswith(('198.18.', '198.19.', '240.', '198.51.100.', '203.0.113.'))
213
+
214
+
215
+ def _doh_lookup(name, timeout=6.0):
216
+ """DoH 拿真实 IP(绕 TUN/fake-IP);无共享模块或失败返回 []。"""
217
+ if netdns is None:
218
+ return []
219
+ try:
220
+ return netdns.doh_query(name, 'A', timeout)
221
+ except Exception:
222
+ return []
202
223
 
203
224
 
204
225
  _PROBE_NAME = 'www.apple.com'
@@ -645,7 +666,14 @@ def cmd_dns(args):
645
666
 
646
667
  def one(n):
647
668
  try:
648
- return n, dns_query(n, servers, timeout=timeout, via=args.via)
669
+ if getattr(args, 'doh', False): # 强制 DoH(TUN/fake-IP 环境)
670
+ return n, _doh_lookup(n, timeout)
671
+ ips = dns_query(n, servers, timeout=timeout, via=args.via)
672
+ if not ips or all(_is_fakeip(x) for x in ips):
673
+ d = _doh_lookup(n, timeout) # 系统 DNS 被劫持 → 走 DoH 拿真 IP
674
+ if d:
675
+ return n, d
676
+ return n, ips
649
677
  except Exception as e: # 单点失败不影响整批
650
678
  print(f'[警告] {n} 解析异常: {e}', file=sys.stderr)
651
679
  return n, []
@@ -661,6 +689,15 @@ def cmd_dns(args):
661
689
  for n, ips in _run_pool(failed, _getent_one, _clamp_int(workers, 1, 20, 20)):
662
690
  if ips:
663
691
  results[n] = ips
692
+ # DoH 兜底:系统解析为空或全 fake-IP(TUN 劫持 UDP:53)→ HTTPS DoH 拿真 IP
693
+ doh_todo = [n for n in todo
694
+ if not [x for x in (results.get(n) or []) if not _is_fakeip(x)]]
695
+ if doh_todo:
696
+ print(f'[DoH] {len(doh_todo)} 个域名系统结果为空/fake-IP,改走 DoH 真 IP', file=sys.stderr)
697
+ for n, ips in _run_pool(doh_todo, lambda x: (x, _doh_lookup(x, timeout)),
698
+ _clamp_int(workers, 1, 10, 10)):
699
+ if ips:
700
+ results[n] = ips
664
701
  new_cache, written = [], set()
665
702
  for n in todo:
666
703
  ips = results.get(n)
@@ -923,10 +960,20 @@ def cmd_scan(args):
923
960
  except ValueError:
924
961
  try:
925
962
  ip = socket.gethostbyname(h)
963
+ if _is_fakeip(ip) and netdns is not None:
964
+ real, src = netdns.resolve_real(h)
965
+ if [x for x in real if not _is_fakeip(x)]:
966
+ _warn(f'{h} 系统解析为 fake-IP({ip}),已用 DoH 真 IP: {real[0]}')
967
+ ip = real[0]
926
968
  except (OSError, UnicodeError):
927
- print(f'{h}\tDNS 解析失败或非法地址')
928
- rc = rc or 1
929
- continue
969
+ real = _doh_lookup(h) if netdns is not None else []
970
+ if real:
971
+ _warn(f'{h} 系统解析失败,已用 DoH 真 IP: {real[0]}')
972
+ ip = real[0]
973
+ else:
974
+ print(f'{h}\tDNS 解析失败或非法地址(TUN/fake-IP?试 dns {h} --doh)')
975
+ rc = rc or 1
976
+ continue
930
977
  t0 = time.time()
931
978
  try:
932
979
  found, banners, skipped = scan_host(ip, ports, timeout, workers, args.banner)
@@ -1383,6 +1430,8 @@ def main():
1383
1430
  help='解析通道: udp(默认) / dot(DoT 853) / auto(UDP 失败自动 DoT)')
1384
1431
  d.add_argument('-t', '--timeout', type=float, default=4)
1385
1432
  d.add_argument('--no-cache', action='store_true')
1433
+ d.add_argument('--doh', action='store_true',
1434
+ help='强制 DoH 解析(绕 TUN/fake-IP;iSH/TUN 环境下推荐)')
1386
1435
 
1387
1436
  s = sub.add_parser('scan', help='端口扫描(selectors 并发)')
1388
1437
  s.add_argument('target', help='主机 / CIDR / 文件')
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """共享 DNS 工具:fake-IP 判定 + DoH 真 IP 解析 + 系统解析兜底(纯标准库)。
4
+
5
+ 为什么需要:iSH / iOS 或桌面开了 TUN(Clash / mihomo / sing-box 等)时,
6
+ DNS 常被劫持成 fake-IP(默认 198.18.0.0/15)。此时:
7
+ · 端口扫描(nmap/masscan/本脚本 connect 扫)拿到的是不可路由假地址 → 全部超时/误判;
8
+ · 源站溯源 / 爆破 / 证书探测同样拿不到真 IP。
9
+ 解法:用 DoH(DNS over HTTPS,走 443,不经过被劫持的 UDP:53)拿真实 A/AAAA。
10
+
11
+ 用法(供 portscan / fastrecon / origin_trace 复用):
12
+ import netdns
13
+ ips, source = netdns.resolve_real('example.com') # auto: 系统 → DoH
14
+ ips, source = netdns.resolve_real('example.com', prefer='doh')
15
+ netdns.is_fake_ip('198.18.0.5') # True
16
+
17
+ CLI 自检:
18
+ python3 netdns.py example.com [--doh] [--json]
19
+ """
20
+ import ipaddress
21
+ import json
22
+ import socket
23
+ import sys
24
+ import threading
25
+ import time
26
+ import urllib.parse
27
+ import urllib.request
28
+
29
+ # fake-IP 常见伪造段(Clash/mihomo/sing-box 默认 198.18.0.0/15;其它代理软件另有实现)
30
+ FAKE_NETS = tuple(ipaddress.ip_network(n) for n in (
31
+ "198.18.0.0/15", # RFC 2544 基准测试段:Clash / mihomo / sing-box 默认 fake-ip
32
+ "198.51.100.0/24", # TEST-NET-2:部分代理软件伪造段
33
+ "203.0.113.0/24", # TEST-NET-3
34
+ "240.0.0.0/4", # 保留段:部分 fake-ip 实现
35
+ ))
36
+
37
+ # DoH JSON 端点(走 HTTPS/443,TUN 劫持不到 UDP:53)
38
+ DOH_PROVIDERS = (
39
+ "https://1.1.1.1/dns-query", # Cloudflare(需 accept: application/dns-json)
40
+ "https://dns.google/resolve", # Google
41
+ "https://223.5.5.5/resolve", # AliDNS
42
+ )
43
+ UA = "agent-bypass-netdns/1.0"
44
+ DEFAULT_TIMEOUT = 6.0
45
+
46
+
47
+ def is_ipv4(s):
48
+ try:
49
+ ipaddress.IPv4Address(str(s).strip())
50
+ return True
51
+ except (ValueError, TypeError):
52
+ return False
53
+
54
+
55
+ def is_fake_ip(ip):
56
+ """fake-IP / 不可路由伪造段判定。"""
57
+ if not is_ipv4(ip):
58
+ return False
59
+ a = ipaddress.IPv4Address(str(ip).strip())
60
+ return any(a in n for n in FAKE_NETS)
61
+
62
+
63
+ def _system_resolve_impl(name):
64
+ """实际解析;可能被 TUN 劫持成 fake-IP。"""
65
+ infos = socket.getaddrinfo(name, None, socket.AF_INET, socket.SOCK_STREAM)
66
+ out = []
67
+ for info in infos:
68
+ ip = info[4][0]
69
+ if is_ipv4(ip) and ip not in out:
70
+ out.append(ip)
71
+ return out
72
+
73
+
74
+ def system_resolve(name, timeout=5.0):
75
+ """系统解析 + 硬超时:getaddrinfo 在部分环境(iSH/TUN)会长时间卡住,
76
+ 用守护线程 + join(timeout) 兜底,确保调用方最多等 timeout 秒。"""
77
+ box = {"ips": []}
78
+
79
+ def work():
80
+ try:
81
+ box["ips"] = _system_resolve_impl(name)
82
+ except Exception:
83
+ box["ips"] = []
84
+
85
+ th = threading.Thread(target=work, daemon=True)
86
+ th.start()
87
+ th.join(timeout)
88
+ return list(box["ips"])
89
+
90
+
91
+ def _parse_dns_json(text, qtype):
92
+ try:
93
+ data = json.loads(text)
94
+ except (ValueError, TypeError):
95
+ return []
96
+ want = 1 if qtype == "A" else 28
97
+ ips = []
98
+ for ans in (data.get("Answer") or []):
99
+ if ans.get("type") != want:
100
+ continue
101
+ d = str(ans.get("data") or "").strip()
102
+ if qtype == "A" and is_ipv4(d) and d not in ips:
103
+ ips.append(d)
104
+ elif qtype == "AAAA" and ":" in d and d not in ips:
105
+ ips.append(d)
106
+ return ips
107
+
108
+
109
+ def doh_query(name, qtype="A", timeout=DEFAULT_TIMEOUT, providers=None, fake_ok=False,
110
+ deadline=None):
111
+ """DoH 查询:按顺序尝试多个端点;默认过滤 fake-IP 结果。
112
+ deadline(time.monotonic 秒)用于总时限,避免多端点叠加导致长时间卡住。"""
113
+ deadline = deadline if deadline is not None else time.monotonic() + max(timeout, 1.0) * 3
114
+ for base in (providers or DOH_PROVIDERS):
115
+ remain = deadline - time.monotonic()
116
+ if remain <= 0.2:
117
+ break
118
+ sep = "&" if "?" in base else "?"
119
+ url = "%s%sname=%s&type=%s" % (
120
+ base, sep, urllib.parse.quote(str(name), safe=""), qtype)
121
+ try:
122
+ req = urllib.request.Request(url, headers={
123
+ "accept": "application/dns-json",
124
+ "user-agent": UA,
125
+ })
126
+ with urllib.request.urlopen(req, timeout=min(timeout, remain)) as r:
127
+ ips = _parse_dns_json(r.read(1 << 20).decode("utf-8", "ignore"), qtype)
128
+ except Exception:
129
+ continue
130
+ if not fake_ok:
131
+ ips = [x for x in ips if not is_fake_ip(x)]
132
+ if ips:
133
+ return ips
134
+ return []
135
+
136
+
137
+ def resolve_real(name, prefer="auto", timeout=DEFAULT_TIMEOUT):
138
+ """解析真实 IP。prefer: auto(系统→DoH)/ system / doh。
139
+ 返回 (ips, source):source ∈ ip / system / doh / system-fake / fail。
140
+ """
141
+ name = str(name or "").strip().rstrip(".")
142
+ if is_ipv4(name):
143
+ return [name], "ip"
144
+ if prefer == "system":
145
+ ips = system_resolve(name, timeout)
146
+ return ips, ("system" if ips else "fail")
147
+ t0 = time.monotonic()
148
+ sys_ips = system_resolve(name, min(timeout, 5.0)) if prefer == "auto" else []
149
+ real = [x for x in sys_ips if not is_fake_ip(x)]
150
+ if real:
151
+ return real, "system"
152
+ budget = max(2.0, timeout - (time.monotonic() - t0))
153
+ doh_ips = doh_query(name, "A", timeout=min(4.0, budget),
154
+ deadline=time.monotonic() + budget + 1.0)
155
+ if doh_ips:
156
+ return doh_ips, "doh"
157
+ if sys_ips:
158
+ return sys_ips, "system-fake"
159
+ return [], "fail"
160
+
161
+
162
+ def fmt_ips(ips):
163
+ return ",".join(ips) if ips else "-"
164
+
165
+
166
+ def _main(argv):
167
+ args = [a for a in argv[1:] if not a.startswith("-")]
168
+ flags = [a for a in argv[1:] if a.startswith("-")]
169
+ if not args:
170
+ print("用法: python3 netdns.py <domain>... [--doh] [--json]", file=sys.stderr)
171
+ return 2
172
+ prefer = "doh" if "--doh" in flags else "auto"
173
+ rc = 0
174
+ out = {}
175
+ for name in args:
176
+ ips, source = resolve_real(name, prefer=prefer)
177
+ out[name] = {"ips": ips, "source": source}
178
+ if not ips:
179
+ rc = 1
180
+ print("%s\t-\t[解析失败:TUN/fake-IP?换 DoH 或检查出网]" % name, file=sys.stderr)
181
+ elif source == "system-fake":
182
+ rc = 1
183
+ print("%s\t%s\t[fake-IP:不可信,换 DoH]" % (name, fmt_ips(ips)), file=sys.stderr)
184
+ else:
185
+ print("%s\t%s\t[%s]" % (name, fmt_ips(ips), source))
186
+ if "--json" in flags:
187
+ print(json.dumps(out, ensure_ascii=False))
188
+ return rc
189
+
190
+
191
+ if __name__ == "__main__":
192
+ sys.exit(_main(sys.argv))