2 Commits

10 changed files with 362 additions and 3 deletions
+1
View File
@@ -133,5 +133,6 @@ http://<host-ip>:8080
| 路由 | [docs/config/routing.md](docs/config/routing.md) |
| DNS | [docs/config/dns.md](docs/config/dns.md) |
| 透明代理 | [docs/config/transparent.md](docs/config/transparent.md) |
| 透明代理 iptables 排查 | [docs/transparent-iptables.md](docs/transparent-iptables.md) |
| 出站和自动更新 | [docs/config/outbounds-auto-update.md](docs/config/outbounds-auto-update.md) |
| Xray 资源下载 | [docs/config/assets.md](docs/config/assets.md) |
+1
View File
@@ -24,6 +24,7 @@ UI 中有些字段隐藏但仍存在于 `settings.toml`。下表的“UI”列
| 路由 | [config/routing.md](config/routing.md) | rule 入站和透明代理流量的分流策略。 |
| DNS | [config/dns.md](config/dns.md) | Xray DNS 模块、本地 DNS 入站、DNS 服务器路由。 |
| 透明代理 | [config/transparent.md](config/transparent.md) | transparent inbound、iptables/nft、resolv、TinyTun。 |
| 透明代理 iptables 排查 | [transparent-iptables.md](transparent-iptables.md) | 当前 redirect 规则、宿主机查看方式、链和计数器含义。 |
| 出站和自动更新 | [config/outbounds-auto-update.md](config/outbounds-auto-update.md) | 出站组预留字段、自动更新预留字段。 |
| 资源下载 | [config/assets.md](config/assets.md) | `xray``geoip.dat``geosite.dat` 下载设置。 |
+302
View File
@@ -0,0 +1,302 @@
# 透明代理 iptables 规则说明
本文说明 `transparent.type = "redirect"` 时,pyxray 当前会生成哪些 `iptables` 规则、这些规则是什么意思,以及如何查看宿主机当前是否生效。
## 当前配置
当前 `data/settings.toml` 中透明代理相关配置类似:
```toml
[transparent]
mode = "pac"
type = "redirect"
port = 52345
docker_transparent = true
docker_transparent_cidrs = "172.16.0.0/12"
output_bypass_rules = "all 117.72.47.28"
```
含义:
| 配置 | 含义 |
| --- | --- |
| `mode = "pac"` | 透明代理流量进入 Xray 后,复用 `[routing].mode` 的分流策略。 |
| `type = "redirect"` | 使用 `iptables nat` 表的 `REDIRECT`,主要处理 TCP 流量。 |
| `port = 52345` | 被拦截的 TCP 流量会转发到本机 Xray transparent inbound 端口。 |
| `docker_transparent = true` | 额外处理来自 Docker 网段的容器流量。 |
| `docker_transparent_cidrs = "172.16.0.0/12"` | 只有源地址在该 CIDR 内的 Docker 容器流量会进入 `PREROUTING` 透明代理规则。 |
| `output_bypass_rules = "all 117.72.47.28"` | 宿主机本机访问 `117.72.47.28` 时直接放行,不进入透明代理。redirect 下实际只生成 TCP 绕过规则。 |
## 生成文件
透明代理规则文件生成在:
```text
data/transparent/
```
常用文件:
| 文件 | 作用 |
| --- | --- |
| `transparent-iptables-setup.sh` | 安装 iptables 规则。 |
| `transparent-iptables-cleanup.sh` | 清理 iptables 规则。 |
| `transparent-nft-setup.sh` | nftables 后端安装脚本。 |
| `transparent-nft-cleanup.sh` | nftables 后端清理脚本。 |
| `v2raya.nft` | nftables 后端规则表。 |
| `ip-forward-apply.sh` | 写 `/proc/sys/net/ipv4/ip_forward` 和 IPv6 forwarding。 |
| `resolv-hijack-setup.sh` | DNS 劫持时改写 `/etc/resolv.conf`。 |
| `resolv-hijack-cleanup.sh` | 停止或回滚时恢复 `/etc/resolv.conf`。 |
直接查看生成的 iptables 脚本:
```bash
sed -n '1,220p' data/transparent/transparent-iptables-setup.sh
sed -n '1,220p' data/transparent/transparent-iptables-cleanup.sh
```
如果在 Docker 宿主机上查看挂载目录:
```bash
sed -n '1,220p' ./data/transparent/transparent-iptables-setup.sh
```
## 当前会生成的 redirect 规则
当前 `redirect` 模式使用 `nat` 表,并创建 3 条自定义链:
| 链 | 作用 |
| --- | --- |
| `TP_OUT` | 处理宿主机本机进程发起的 TCP 流量,也就是 `OUTPUT` 流量。 |
| `TP_PRE` | 处理进入宿主机的 TCP 流量,也就是 `PREROUTING` 流量;主要用于 Docker 容器或其它转发流量。 |
| `TP_RULE` | 统一判断哪些目标直连,哪些目标重定向到 Xray。 |
核心结构:
```bash
iptables -t nat -N TP_OUT
iptables -t nat -N TP_PRE
iptables -t nat -N TP_RULE
iptables -t nat -I OUTPUT -p tcp -j TP_OUT
iptables -t nat -I PREROUTING -p tcp -j TP_PRE
iptables -t nat -A TP_PRE -s 172.16.0.0/12 -j TP_RULE
iptables -t nat -A TP_OUT -j TP_RULE
iptables -t nat -A TP_RULE -p tcp -j REDIRECT --to-ports 52345
```
执行路径:
| 流量来源 | 路径 |
| --- | --- |
| 宿主机本机进程访问外部 TCP | `nat OUTPUT -> TP_OUT -> TP_RULE -> REDIRECT :52345` |
| Docker 容器访问外部 TCP,源地址匹配 `172.16.0.0/12` | `nat PREROUTING -> TP_PRE -> TP_RULE -> REDIRECT :52345` |
| Docker 容器源地址不匹配 `docker_transparent_cidrs` | 进入 `TP_PRE` 后不会跳到 `TP_RULE`,不会被 pyxray redirect。 |
| 访问保留地址、内网地址、本机接口地址、绕过目标 | 在 `TP_RULE``RETURN`,不进入 Xray。 |
## TP_RULE 里的 RETURN 是什么意思
`TP_RULE` 前半段是一批 `RETURN` 规则,用来避免把不该代理的流量送进 Xray。
常见规则:
```bash
iptables -t nat -A TP_RULE -d 10.0.0.0/8 -j RETURN
iptables -t nat -A TP_RULE -d 127.0.0.0/8 -j RETURN
iptables -t nat -A TP_RULE -d 172.16.0.0/12 -j RETURN
iptables -t nat -A TP_RULE -d 192.168.0.0/16 -j RETURN
iptables -t nat -A TP_RULE -m mark --mark 0x80/0x80 -j RETURN
iptables -t nat -A TP_RULE -i wg+ -j RETURN
iptables -t nat -A TP_RULE -i ppp+ -j RETURN
```
含义:
| 规则 | 含义 |
| --- | --- |
| `-d 10.0.0.0/8 -j RETURN` | 访问私有网段时直连。 |
| `-d 127.0.0.0/8 -j RETURN` | 访问本机回环地址时直连,避免回环。 |
| `-d 172.16.0.0/12 -j RETURN` | 访问 Docker 或内网私有地址时直连。 |
| `-d 192.168.0.0/16 -j RETURN` | 访问局域网地址时直连。 |
| `-m mark --mark 0x80/0x80 -j RETURN` | 已打过特定标记的流量跳过,避免重复处理。 |
| `-i wg+ -j RETURN` | 从 WireGuard 之类接口进入的流量跳过。 |
| `-i ppp+ -j RETURN` | 从 PPP 之类接口进入的流量跳过。 |
pyxray 还会把宿主机当前 IPv4 地址所在 CIDR 加入 RETURN
```bash
ip -o -4 addr show | awk '{print $4}'
```
这部分用于避免访问宿主机本机地址或本地接口地址时被透明代理截获。
## output_bypass_rules 生成的规则
当前配置:
```toml
output_bypass_rules = "all 117.72.47.28"
```
redirect 模式只处理 TCP,所以会生成:
```bash
iptables -t nat -A TP_OUT -p tcp -d 117.72.47.28 -j RETURN
```
它的位置在 `TP_OUT -> TP_RULE` 之前,含义是:
```text
宿主机本机进程访问 117.72.47.28 的 TCP 流量直接 RETURN,不进入 TP_RULE,也不会 REDIRECT 到 52345。
```
这个规则只影响宿主机本机 `OUTPUT` 流量,不影响 Docker 容器从 `PREROUTING` 进入的流量。
## 如何查看当前生效状态
宿主机直接查看:
```bash
sudo iptables -t nat -S
sudo iptables -t nat -L -n -v
sudo iptables-save -t nat
```
只看 pyxray 透明代理相关链:
```bash
sudo iptables -t nat -S TP_OUT
sudo iptables -t nat -S TP_PRE
sudo iptables -t nat -S TP_RULE
```
在容器里查看:
```bash
docker exec -it pyxray iptables -t nat -S
docker exec -it pyxray iptables -t nat -S TP_OUT
docker exec -it pyxray iptables -t nat -S TP_PRE
docker exec -it pyxray iptables -t nat -S TP_RULE
```
当前 `compose.yaml` 使用:
```yaml
network_mode: host
privileged: true
```
因此容器里执行的 `iptables` 作用在宿主机网络命名空间。正常情况下,宿主机和容器里看到的是同一套规则。
## 看不到规则时怎么判断原因
先确认 pyxray 是否还在运行:
```bash
docker ps | grep pyxray
docker logs pyxray --tail 80
```
查看 pyxray 自己的透明代理执行日志:
```bash
grep 'pyxray transparent' data/xray.log | tail -80
```
如果看到类似:
```text
setup transparent iptables: /bin/sh data/transparent/transparent-iptables-setup.sh
```
说明启动时使用了 iptables 后端。
如果最后看到的是:
```text
cleanup transparent iptables: /bin/sh data/transparent/transparent-iptables-cleanup.sh
```
说明停止或回滚时已经清理过规则,宿主机上可能就看不到 `TP_OUT``TP_PRE``TP_RULE`
如果容器里能看到、宿主机看不到,检查 `iptables` 前端是否一致:
```bash
sudo iptables --version
docker exec pyxray iptables --version
sudo iptables-nft -t nat -S
sudo iptables-legacy -t nat -S
sudo nft list ruleset
```
有些系统同时存在 `iptables-nft``iptables-legacy`。如果宿主机默认命令和容器里的命令使用不同后端,看到的规则可能不一致。
## 如何判断流量是否命中规则
查看计数器:
```bash
sudo iptables -t nat -L TP_OUT -n -v
sudo iptables -t nat -L TP_PRE -n -v
sudo iptables -t nat -L TP_RULE -n -v
```
关注字段:
| 字段 | 含义 |
| --- | --- |
| `pkts` | 命中该规则的数据包数量。 |
| `bytes` | 命中该规则的字节数。 |
| `REDIRECT tcp -- anywhere anywhere redir ports 52345` | 命中后会被转发到 Xray transparent inbound。 |
| `RETURN` | 命中后从当前自定义链返回,不继续走 pyxray 后续规则。 |
测试前可以清零计数器:
```bash
sudo iptables -t nat -Z TP_OUT
sudo iptables -t nat -Z TP_PRE
sudo iptables -t nat -Z TP_RULE
```
然后从宿主机发起一个 TCP 访问,再查看计数器是否增加。
## 清理规则
正常停止 pyxray 时会执行:
```bash
data/transparent/transparent-iptables-cleanup.sh
```
手动清理:
```bash
sudo sh data/transparent/transparent-iptables-cleanup.sh
```
容器里手动清理:
```bash
docker exec -it pyxray sh /config/transparent/transparent-iptables-cleanup.sh
```
清理脚本会删除:
```bash
nat OUTPUT -> TP_OUT
nat PREROUTING -> TP_PRE
TP_OUT
TP_PRE
TP_RULE
```
## 代码位置
| 路径 | 作用 |
| --- | --- |
| `pyxray/libs/xray_config/transparent_rules.py` | 生成 iptables/nftables 脚本。 |
| `pyxray/libs/xray_transparent_runtime.py` | 启动、停止、回滚时执行脚本。 |
| `data/transparent/transparent-iptables-setup.sh` | 当前配置实际生成出的安装脚本。 |
| `data/transparent/transparent-iptables-cleanup.sh` | 当前配置实际生成出的清理脚本。 |
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "pyxray"
version = "1.0.4"
version = "1.0.5"
description = "A lightweight Linux xray control plane."
readme = "README.md"
requires-python = ">=3.14"
+19 -1
View File
@@ -31,6 +31,8 @@ def generate_xray_config(node: Node, settings: XrayConfigSettings | None = None)
log = _build_log(settings)
if log:
config["log"] = log
if _fakedns_enabled(settings):
config["fakedns"] = [{"ipPool": "198.18.0.0/15", "poolSize": 65535}]
config["routing"]["rules"].extend(_build_dns_routing(settings))
config["routing"]["rules"].append({"type": "field", "inboundTag": _dns_inbound_tags(settings), "outboundTag": "dns-out"})
config["routing"]["rules"].extend(_build_rule_port_routing(settings))
@@ -157,9 +159,12 @@ def _transparent_inbounds(settings: XrayConfigSettings) -> list[dict[str, Any]]:
def _with_sniffing(inbound: dict[str, Any], settings: XrayConfigSettings) -> dict[str, Any]:
if settings.inbounds.inbound_sniffing == "disable":
return inbound
dest_override = settings.inbounds.inbound_sniffing.split(",")
if _fakedns_enabled(settings) and "fakedns" not in dest_override:
dest_override.append("fakedns")
inbound["sniffing"] = {
"enabled": True,
"destOverride": settings.inbounds.inbound_sniffing.split(","),
"destOverride": dest_override,
"domainsExcluded": _split_lines(settings.inbounds.domains_excluded),
"routeOnly": settings.inbounds.route_only,
}
@@ -207,6 +212,9 @@ def _dns_inbound_tags(settings: XrayConfigSettings) -> list[str]:
def _build_dns(settings: XrayConfigSettings, node: Node) -> dict[str, Any]:
servers: list[Any] = []
fakedns_domains = _fakedns_domains(settings)
if fakedns_domains:
servers.append({"address": "fakedns", "domains": fakedns_domains})
routing_domains = _domains_to_lookup(settings, node)
for rule in settings.dns.rules:
domains = _split_lines(rule.domains)
@@ -230,6 +238,16 @@ def _build_dns(settings: XrayConfigSettings, node: Node) -> dict[str, Any]:
return dns
def _fakedns_enabled(settings: XrayConfigSettings) -> bool:
return settings.dns.special_mode == "fakedns"
def _fakedns_domains(settings: XrayConfigSettings) -> list[str]:
if not _fakedns_enabled(settings):
return []
return _split_lines(settings.dns.fakedns_domains) or ["geosite:geolocation-!cn"]
def _dns_server(rule: DnsRuleSettings, domains: list[str]) -> Any:
address, port = _parse_dns_addr(rule.server)
server_address = rule.server if "://" in rule.server else address
+1
View File
@@ -126,6 +126,7 @@ class DnsSettings:
)
antipollution: str = "closed"
special_mode: str = "none"
fakedns_domains: str = "geosite:geolocation-!cn"
@dataclass(slots=True)
+4
View File
@@ -43,6 +43,10 @@
</select>
</label>
</div>
<label class="config-field mt-4">
<span>FakeDNS 域名范围,每行一个 domain/geosite/keyword 规则</span>
<textarea name="dns.fakedns_domains" placeholder="geosite:geolocation-!cn">{{ settings.dns.fakedns_domains }}</textarea>
</label>
<label class="config-field mt-4">
<span>DNS 规则,每行 server|domains|outbound</span>
<textarea name="dns.rules">{% for item in settings.dns.rules %}{{ item.server }}|{{ item.domains }}|{{ item.outbound }}
+1
View File
@@ -203,6 +203,7 @@ def _settings_from_request() -> XrayConfigSettings:
settings.dns.local_dns_listen = _bool("dns.local_dns_listen")
settings.dns.antipollution = form.get("dns.antipollution", settings.dns.antipollution)
settings.dns.special_mode = form.get("dns.special_mode", settings.dns.special_mode)
settings.dns.fakedns_domains = form.get("dns.fakedns_domains", settings.dns.fakedns_domains)
settings.dns.rules = [
DnsRuleSettings(server=parts[0], domains=parts[1], outbound=parts[2])
for parts in _split_table("dns.rules", 3)
+26
View File
@@ -89,6 +89,8 @@ def test_settings_defaults_match_v2raya_core_values() -> None:
assert settings.transparent.docker_transparent_cidrs == "172.16.0.0/12"
assert settings.transparent.output_bypass_rules == ""
assert settings.dns.query_strategy == "UseIPv4"
assert settings.dns.special_mode == "none"
assert settings.dns.fakedns_domains == "geosite:geolocation-!cn"
assert settings.dns.rules == [
DnsRuleSettings(server="localhost", domains="geosite:private", outbound="direct"),
DnsRuleSettings(server="223.5.5.5", domains="geosite:cn", outbound="direct"),
@@ -403,6 +405,30 @@ def test_generate_dns_rules_and_dns_routing() -> None:
assert any(rule.get("domain") == ["dns.google"] and rule.get("outboundTag") == "proxy" for rule in config["routing"]["rules"])
def test_generate_fakedns_adds_dns_server_pool_and_sniffing() -> None:
settings = XrayConfigSettings()
settings.transparent.mode = "gfwlist"
settings.transparent.type = "redirect"
settings.dns.special_mode = "fakedns"
config = generate_xray_config(parse_node_link(_ss_link()), settings)
assert config["fakedns"] == [{"ipPool": "198.18.0.0/15", "poolSize": 65535}]
assert {"address": "fakedns", "domains": ["geosite:geolocation-!cn"]} in config["dns"]["servers"]
assert "fakedns" in _inbound(config, "transparent")["sniffing"]["destOverride"]
assert "fakedns" in _inbound(config, "rule-mixed")["sniffing"]["destOverride"]
def test_generate_fakedns_uses_custom_domain_scope() -> None:
settings = XrayConfigSettings()
settings.dns.special_mode = "fakedns"
settings.dns.fakedns_domains = "geosite:gfw\nkeyword:example"
config = generate_xray_config(parse_node_link(_ss_link()), settings)
assert {"address": "fakedns", "domains": ["geosite:gfw", "keyword:example"]} in config["dns"]["servers"]
def test_validate_settings_rejects_invalid_values() -> None:
settings = XrayConfigSettings()
settings.transparent.type = "bad"
+6 -1
View File
@@ -667,7 +667,8 @@ def test_xray_config_api_saves_settings_from_form_controls(tmp_path: Path) -> No
"dns.query_strategy": "UseIPv4",
"dns.local_dns_listen": "on",
"dns.antipollution": "closed",
"dns.special_mode": "none",
"dns.special_mode": "fakedns",
"dns.fakedns_domains": "geosite:gfw\nkeyword:example",
"dns.rules": "localhost|geosite:private|direct\n8.8.8.8||proxy",
"outbounds.0.tag": "proxy",
"outbounds.0.probe_url": "https://www.gstatic.com/generate_204",
@@ -691,6 +692,8 @@ def test_xray_config_api_saves_settings_from_form_controls(tmp_path: Path) -> No
assert "auth_password = \"secret\"" in saved.get_json()["settings_toml"]
assert "route_only = true" in saved.get_json()["settings_toml"]
assert "output_bypass_rules = \"tcp 117.72.47.28:33010\"" in saved.get_json()["settings_toml"]
assert "special_mode = \"fakedns\"" in saved.get_json()["settings_toml"]
assert "fakedns_domains = \"geosite:gfw\\nkeyword:example\"" in saved.get_json()["settings_toml"]
assert generated.status_code == 200
assert config["log"]["loglevel"] == "error"
assert any(inbound["tag"] == "rule-mixed" and inbound["port"] == 20181 for inbound in config["inbounds"])
@@ -699,6 +702,8 @@ def test_xray_config_api_saves_settings_from_form_controls(tmp_path: Path) -> No
]
assert config["outbounds"][0]["mux"] == {"enabled": True, "concurrency": 16}
assert config["dns"]["queryStrategy"] == "UseIPv4"
assert config["fakedns"] == [{"ipPool": "198.18.0.0/15", "poolSize": 65535}]
assert {"address": "fakedns", "domains": ["geosite:gfw", "keyword:example"]} in config["dns"]["servers"]
def test_xray_config_api_mux_zero_disables_mux(tmp_path: Path) -> None: