diff --git a/pyxray/libs/xray_config/generator.py b/pyxray/libs/xray_config/generator.py index d581ba4..87afbe2 100644 --- a/pyxray/libs/xray_config/generator.py +++ b/pyxray/libs/xray_config/generator.py @@ -106,10 +106,11 @@ def _transparent_inbounds(settings: XrayConfigSettings) -> list[dict[str, Any]]: return [] if settings.transparent.type in {"redirect", "tproxy"}: tproxy = settings.transparent.type + listen = "0.0.0.0" if settings.transparent.docker_transparent and settings.transparent.type == "redirect" else "127.0.0.1" return [ _with_sniffing( { - "listen": "127.0.0.1", + "listen": listen, "port": settings.transparent.port, "protocol": "dokodemo-door", "settings": {"network": "tcp,udp", "followRedirect": True}, diff --git a/pyxray/libs/xray_config/settings.py b/pyxray/libs/xray_config/settings.py index 0b0458e..3cb4fbb 100644 --- a/pyxray/libs/xray_config/settings.py +++ b/pyxray/libs/xray_config/settings.py @@ -81,6 +81,7 @@ class TransparentSettings: socks_port: int = 52306 ipforward: bool = False docker_transparent: bool = True + docker_transparent_cidrs: str = "172.16.0.0/12" tproxy_excluded_interfaces: str = "docker*,veth*,wg*,ppp*,br-*" tproxy_white_country_codes: list[str] = field(default_factory=list) tproxy_white_custom_ips: list[str] = field(default_factory=list) diff --git a/pyxray/libs/xray_config/transparent_rules.py b/pyxray/libs/xray_config/transparent_rules.py index 665c327..d886b7b 100644 --- a/pyxray/libs/xray_config/transparent_rules.py +++ b/pyxray/libs/xray_config/transparent_rules.py @@ -1,5 +1,6 @@ from __future__ import annotations +import ipaddress from dataclasses import dataclass from pathlib import Path @@ -171,13 +172,13 @@ def _redirect_rules(settings: XrayConfigSettings, *, backend: str, ipv6: bool, n "iptables -w 2 -t nat -N TP_OUT", "iptables -w 2 -t nat -N TP_PRE", "iptables -w 2 -t nat -N TP_RULE", - *[f"iptables -w 2 -t nat -A TP_RULE -d {cidr} -j RETURN" for cidr in IPV4_RESERVED_CIDRS], + *[f"iptables -w 2 -t nat -A TP_RULE -d {cidr} -j RETURN" for cidr in _ipv4_reserved_cidrs(settings)], "iptables -w 2 -t nat -A TP_RULE -m mark --mark 0x80/0x80 -j RETURN", *[f"iptables -w 2 -t nat -A TP_RULE -i {_iptables_interface(value)} -j RETURN" for value in _excluded_interfaces(settings)], f"iptables -w 2 -t nat -A TP_RULE -p tcp -j REDIRECT --to-ports {settings.transparent.port}", "iptables -w 2 -t nat -I PREROUTING -p tcp -j TP_PRE", "iptables -w 2 -t nat -I OUTPUT -p tcp -j TP_OUT", - "iptables -w 2 -t nat -A TP_PRE -j TP_RULE", + *_redirect_prerouting_jumps(settings), "iptables -w 2 -t nat -A TP_OUT -j TP_RULE", ] cleanup = [ @@ -227,8 +228,7 @@ def _tproxy_rules(settings: XrayConfigSettings, *, backend: str, ipv6: bool, nft "iptables -w 2 -t mangle -A TP_OUT -p tcp -m addrtype --src-type LOCAL ! --dst-type LOCAL -j TP_RULE", "iptables -w 2 -t mangle -A TP_OUT -p udp -m addrtype --src-type LOCAL ! --dst-type LOCAL -j TP_RULE", "iptables -w 2 -t mangle -A TP_PRE -i lo -m mark ! --mark 0x40/0xc0 -j RETURN", - "iptables -w 2 -t mangle -A TP_PRE -p tcp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TP_RULE", - "iptables -w 2 -t mangle -A TP_PRE -p udp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TP_RULE", + *_tproxy_prerouting_jumps(settings), f"iptables -w 2 -t mangle -A TP_PRE -p tcp -m mark --mark 0x40/0xc0 -j TPROXY --on-port {settings.transparent.port} --on-ip 127.0.0.1", f"iptables -w 2 -t mangle -A TP_PRE -p udp -m mark --mark 0x40/0xc0 -j TPROXY --on-port {settings.transparent.port} --on-ip 127.0.0.1", "iptables -w 2 -t mangle -A TP_RULE -j CONNMARK --restore-mark", @@ -283,7 +283,7 @@ def _ip_forward_script(settings: XrayConfigSettings) -> str: 这里保持相同语义,只把操作写成可审计脚本,不主动执行。 """ - value = "1" if settings.transparent.ipforward else "0" + value = "1" if settings.transparent.ipforward or settings.transparent.docker_transparent else "0" return "\n".join([ f"printf '%s' {value} > /proc/sys/net/ipv4/ip_forward", f"printf '%s' {value} > /proc/sys/net/ipv6/conf/all/forwarding 2>/dev/null || true", @@ -322,7 +322,7 @@ def _redirect_nft_table(settings: XrayConfigSettings, *, ipv6: bool) -> str: interface_returns = "\n".join(f' iifname "{value}" return' for value in _excluded_interfaces(settings)) whitelist6 = _nft_set("whitelist6", "ipv6_addr", IPV6_RESERVED_CIDRS) if ipv6 else "" return f"""table inet v2raya {{ -{_nft_set("whitelist", "ipv4_addr", IPV4_RESERVED_CIDRS)} +{_nft_set("whitelist", "ipv4_addr", _ipv4_reserved_cidrs(settings))} {whitelist6} set interface {{ type ipv4_addr @@ -348,7 +348,7 @@ def _redirect_nft_table(settings: XrayConfigSettings, *, ipv6: bool) -> str: chain tp_pre {{ type nat hook prerouting priority dstnat - 5 - {nfproto} meta l4proto tcp jump tp_rule +{_redirect_nft_prerouting_jumps(settings, nfproto)} }} chain tp_out {{ @@ -382,7 +382,7 @@ def _tproxy_nft_table(settings: XrayConfigSettings, *, ipv6: bool) -> str: chain tp_pre {{ iifname "lo" mark & 0xc0 != 0x40 return - meta l4proto {{ tcp, udp }} fib saddr type != local fib daddr type != local jump tp_rule +{_tproxy_nft_prerouting_jumps(settings, nfproto)} meta l4proto {{ tcp, udp }} mark & 0xc0 == 0x40 tproxy ip to 127.0.0.1:{settings.transparent.port} {'meta l4proto { tcp, udp } mark & 0xc0 == 0x40 tproxy ip6 to [::1]:' + str(settings.transparent.port) if ipv6 else ''} }} @@ -489,6 +489,66 @@ def _excluded_interfaces(settings: XrayConfigSettings) -> list[str]: return [item for item in interfaces if not _is_docker_interface_pattern(item)] +def _ipv4_reserved_cidrs(settings: XrayConfigSettings) -> list[str]: + return IPV4_RESERVED_CIDRS.copy() + + +def _docker_transparent_cidrs(settings: XrayConfigSettings) -> list[ipaddress.IPv4Network]: + cidrs: list[ipaddress.IPv4Network] = [] + for raw in settings.transparent.docker_transparent_cidrs.split(";"): + value = raw.strip() + if not value: + continue + network = ipaddress.ip_network(value, strict=False) + if isinstance(network, ipaddress.IPv4Network): + cidrs.append(network) + return cidrs + + +def _docker_transparent_cidr_strings(settings: XrayConfigSettings) -> list[str]: + return [str(cidr) for cidr in _docker_transparent_cidrs(settings)] + + +def _redirect_prerouting_jumps(settings: XrayConfigSettings) -> list[str]: + cidrs = _docker_transparent_cidr_strings(settings) + if not settings.transparent.docker_transparent or not cidrs: + return ["iptables -w 2 -t nat -A TP_PRE -j TP_RULE"] + return [f"iptables -w 2 -t nat -A TP_PRE -s {cidr} -j TP_RULE" for cidr in cidrs] + + +def _tproxy_prerouting_jumps(settings: XrayConfigSettings) -> list[str]: + cidrs = _docker_transparent_cidr_strings(settings) + if not settings.transparent.docker_transparent or not cidrs: + return [ + "iptables -w 2 -t mangle -A TP_PRE -p tcp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TP_RULE", + "iptables -w 2 -t mangle -A TP_PRE -p udp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TP_RULE", + ] + return [ + f"iptables -w 2 -t mangle -A TP_PRE -s {cidr} -p tcp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TP_RULE" + for cidr in cidrs + ] + [ + f"iptables -w 2 -t mangle -A TP_PRE -s {cidr} -p udp -m addrtype ! --src-type LOCAL ! --dst-type LOCAL -j TP_RULE" + for cidr in cidrs + ] + + +def _redirect_nft_prerouting_jumps(settings: XrayConfigSettings, nfproto: str) -> str: + cidrs = _docker_transparent_cidr_strings(settings) + if not settings.transparent.docker_transparent or not cidrs: + return f" {nfproto} meta l4proto tcp jump tp_rule" + return "\n".join(f" {nfproto} ip saddr {cidr} meta l4proto tcp jump tp_rule" for cidr in cidrs) + + +def _tproxy_nft_prerouting_jumps(settings: XrayConfigSettings, nfproto: str) -> str: + cidrs = _docker_transparent_cidr_strings(settings) + if not settings.transparent.docker_transparent or not cidrs: + return f" {nfproto} meta l4proto {{ tcp, udp }} fib saddr type != local fib daddr type != local jump tp_rule" + return "\n".join( + f" {nfproto} ip saddr {cidr} meta l4proto {{ tcp, udp }} fib saddr type != local fib daddr type != local jump tp_rule" + for cidr in cidrs + ) + + def _is_docker_interface_pattern(value: str) -> bool: normalized = value.strip().lower().replace("+", "*") return normalized.startswith(("docker", "veth", "br-")) diff --git a/pyxray/web/static/js/app.js b/pyxray/web/static/js/app.js index 6497142..27071e4 100644 --- a/pyxray/web/static/js/app.js +++ b/pyxray/web/static/js/app.js @@ -201,17 +201,22 @@ function initTransparentSettings() { const typeSelect = document.querySelector("#transparent-type"); const excludedInput = document.querySelector("#tproxy-excluded-interfaces"); const dockerTransparent = document.querySelector("#docker-transparent"); - if (!typeSelect || !excludedInput || !dockerTransparent) return; + const dockerCidrs = document.querySelector("#docker-transparent-cidrs"); + if (!typeSelect || !excludedInput || !dockerTransparent || !dockerCidrs) return; const update = () => { const disabled = !["redirect", "tproxy"].includes(typeSelect.value); + const cidrsDisabled = disabled || dockerTransparent.value !== "on"; excludedInput.disabled = disabled; dockerTransparent.disabled = disabled; + dockerCidrs.disabled = cidrsDisabled; excludedInput.classList.toggle("opacity-50", disabled); dockerTransparent.classList.toggle("opacity-50", disabled); + dockerCidrs.classList.toggle("opacity-50", cidrsDisabled); }; typeSelect.addEventListener("change", update); + dockerTransparent.addEventListener("change", update); update(); } diff --git a/pyxray/web/templates/configs/transparent.html b/pyxray/web/templates/configs/transparent.html index 1dcfe45..da0e6d2 100644 --- a/pyxray/web/templates/configs/transparent.html +++ b/pyxray/web/templates/configs/transparent.html @@ -57,4 +57,8 @@ + diff --git a/pyxray/web/xray_config.py b/pyxray/web/xray_config.py index f0837c0..7348ff7 100644 --- a/pyxray/web/xray_config.py +++ b/pyxray/web/xray_config.py @@ -163,6 +163,10 @@ def _settings_from_request() -> XrayConfigSettings: "transparent.docker_transparent", settings.transparent.docker_transparent, ) + settings.transparent.docker_transparent_cidrs = form.get( + "transparent.docker_transparent_cidrs", + settings.transparent.docker_transparent_cidrs, + ) settings.transparent.tproxy_excluded_interfaces = form.get( "transparent.tproxy_excluded_interfaces", settings.transparent.tproxy_excluded_interfaces, diff --git a/tests/libs/test_transparent_rules.py b/tests/libs/test_transparent_rules.py index bd8d2e0..7b2c0bb 100644 --- a/tests/libs/test_transparent_rules.py +++ b/tests/libs/test_transparent_rules.py @@ -79,6 +79,53 @@ def test_docker_transparent_removes_docker_interface_exclusions() -> None: assert "iifname \"wg*\" return" in nft_rules.nftables +def test_docker_transparent_keeps_configured_docker_cidrs_as_destination_returns() -> None: + settings = XrayConfigSettings() + settings.transparent.mode = "proxy" + settings.transparent.type = "redirect" + settings.transparent.docker_transparent = True + settings.transparent.docker_transparent_cidrs = "172.16.0.0/12" + + iptables_rules = generate_transparent_rules(settings, backend="iptables") + nft_rules = generate_transparent_rules(settings, backend="nft") + + assert "iptables -w 2 -t nat -A TP_RULE -d 172.16.0.0/12 -j RETURN" in iptables_rules.setup + assert "172.16.0.0/12" in nft_rules.nftables + + +def test_docker_transparent_limits_prerouting_to_configured_source_cidrs() -> None: + settings = XrayConfigSettings() + settings.transparent.mode = "proxy" + settings.transparent.type = "redirect" + settings.transparent.docker_transparent = True + settings.transparent.docker_transparent_cidrs = "172.16.0.0/12;172.30.250.0/24" + + iptables_rules = generate_transparent_rules(settings, backend="iptables") + nft_rules = generate_transparent_rules(settings, backend="nft") + + assert "iptables -w 2 -t nat -A TP_PRE -s 172.16.0.0/12 -j TP_RULE" in iptables_rules.setup + assert "iptables -w 2 -t nat -A TP_PRE -s 172.30.250.0/24 -j TP_RULE" in iptables_rules.setup + assert "iptables -w 2 -t nat -A TP_PRE -j TP_RULE" not in iptables_rules.setup + assert "ip saddr 172.16.0.0/12 meta l4proto tcp jump tp_rule" in nft_rules.nftables + assert "ip saddr 172.30.250.0/24 meta l4proto tcp jump tp_rule" in nft_rules.nftables + + +def test_docker_transparent_limits_tproxy_prerouting_to_configured_source_cidrs() -> None: + settings = XrayConfigSettings() + settings.transparent.mode = "proxy" + settings.transparent.type = "tproxy" + settings.transparent.docker_transparent = True + settings.transparent.docker_transparent_cidrs = "172.16.0.0/12" + + iptables_rules = generate_transparent_rules(settings, backend="iptables") + nft_rules = generate_transparent_rules(settings, backend="nft") + + assert "iptables -w 2 -t mangle -A TP_PRE -s 172.16.0.0/12 -p tcp" in iptables_rules.setup + assert "iptables -w 2 -t mangle -A TP_PRE -s 172.16.0.0/12 -p udp" in iptables_rules.setup + assert "iptables -w 2 -t mangle -A TP_PRE -p tcp -m addrtype" not in iptables_rules.setup + assert "ip saddr 172.16.0.0/12 meta l4proto { tcp, udp }" in nft_rules.nftables + + def test_close_mode_has_no_system_rules() -> None: settings = XrayConfigSettings() settings.transparent.mode = "close" @@ -93,6 +140,7 @@ def test_write_transparent_rule_files_outputs_auditable_scripts(tmp_path) -> Non settings = XrayConfigSettings() settings.transparent.mode = "proxy" settings.transparent.type = "redirect" + settings.transparent.docker_transparent = False files = write_transparent_rule_files(settings, tmp_path) @@ -116,6 +164,17 @@ def test_ip_forward_script_follows_setting_like_v2raya(tmp_path) -> None: assert "printf '%s' 1 > /proc/sys/net/ipv6/conf/all/forwarding" in content +def test_docker_transparent_forces_ip_forward_script(tmp_path) -> None: + settings = XrayConfigSettings() + settings.transparent.ipforward = False + settings.transparent.docker_transparent = True + + files = write_transparent_rule_files(settings, tmp_path) + content = files.ip_forward.read_text(encoding="utf-8") + + assert "printf '%s' 1 > /proc/sys/net/ipv4/ip_forward" in content + + def test_resolv_hijack_scripts_follow_v2raya_redirect_dns_behavior(tmp_path) -> None: settings = XrayConfigSettings() settings.transparent.mode = "proxy" diff --git a/tests/libs/test_xray_config.py b/tests/libs/test_xray_config.py index c4cafa9..9a8416e 100644 --- a/tests/libs/test_xray_config.py +++ b/tests/libs/test_xray_config.py @@ -84,6 +84,7 @@ def test_settings_defaults_match_v2raya_core_values() -> None: assert settings.transparent.type == "redirect" assert settings.transparent.port == 52345 assert settings.transparent.docker_transparent is True + assert settings.transparent.docker_transparent_cidrs == "172.16.0.0/12" assert settings.dns.query_strategy == "UseIPv4" assert settings.dns.rules == [ DnsRuleSettings(server="localhost", domains="geosite:private", outbound="direct"), @@ -131,11 +132,23 @@ def test_generate_redirect_dns_inbound_when_transparent_redirect_enabled() -> No config = generate_xray_config(parse_node_link(_ss_link()), settings) + assert _inbound(config, "transparent")["listen"] == "0.0.0.0" assert _inbound(config, "dns-in")["listen"] == "127.2.0.17" assert {"type": "field", "inboundTag": ["dns-in"], "outboundTag": "dns-out"} in config["routing"]["rules"] assert _outbound(config, "proxy")["streamSettings"]["sockopt"]["mark"] == 128 +def test_generate_redirect_transparent_inbound_stays_local_when_docker_transparent_disabled() -> None: + settings = XrayConfigSettings() + settings.transparent.mode = "proxy" + settings.transparent.type = "redirect" + settings.transparent.docker_transparent = False + + config = generate_xray_config(parse_node_link(_ss_link()), settings) + + assert _inbound(config, "transparent")["listen"] == "127.0.0.1" + + def test_generate_lan_dns_inbound_when_redirect_and_port_sharing_enabled() -> None: settings = XrayConfigSettings() settings.inbounds.port_sharing = True diff --git a/tests/web/test_xray_assets_web.py b/tests/web/test_xray_assets_web.py index 70f16a2..3df0d85 100644 --- a/tests/web/test_xray_assets_web.py +++ b/tests/web/test_xray_assets_web.py @@ -646,6 +646,7 @@ def test_xray_config_api_generates_transparent_rule_files(tmp_path: Path) -> Non "transparent.socks_port": "52306", "transparent.ipforward": "off", "transparent.docker_transparent": "off", + "transparent.docker_transparent_cidrs": "172.16.0.0/12;172.18.0.0/16", "transparent.tproxy_excluded_interfaces": "docker*,veth*", "transparent.tun_auto_route": "on", "dns.disable_fallback": "off",