commit f0c1c3641834957e8a2e4343445f3ce921d12fdb Author: Jerry Date: Mon Jul 10 15:35:57 2023 +0800 init diff --git a/configs/1.json b/configs/1.json new file mode 100644 index 0000000..79105b8 --- /dev/null +++ b/configs/1.json @@ -0,0 +1,4 @@ +{ + "#note": "plain old shadowsocks config json, mode defaults to ipv4_tcp_udp, see 'MODES' in ss.py", + "server":"1.2.3.4" +} diff --git a/configs/2.json b/configs/2.json new file mode 100644 index 0000000..54c9341 --- /dev/null +++ b/configs/2.json @@ -0,0 +1,12 @@ +{ + "modes": [ + "ipv4_tcp_udp", + "ipv6_tcp_udp" + ], + "common": { + "server":"1.2.3.4", + "server_port":443, + "plugin":"v2ray-plugin", + "plugin_opts":"tls;host=1.2.3.4;path=/;mux=0" + } +} diff --git a/configs/4.json b/configs/4.json new file mode 100644 index 0000000..3b3a53b --- /dev/null +++ b/configs/4.json @@ -0,0 +1,26 @@ +{ + "modes": [ + "ipv4_tcp_only", + "ipv4_udp_only", + "ipv6_tcp_only", + "ipv6_udp_only" + ], + "common": { + "server":"1.2.3.4", + "server_port":443, + "plugin":"v2ray-plugin", + "plugin_opts":"tls;host=1.2.3.4;path=/;mux=0" + }, + "ipv4_tcp_only": { + "server_port":123 + }, + "ipv4_udp_only": { + "server_port":443 + }, + "ipv6_tcp_only": { + "server_port":123 + }, + "ipv6_udp_only": { + "server_port":443 + } +} diff --git a/configs/config.common.inc b/configs/config.common.inc new file mode 100644 index 0000000..7e25562 --- /dev/null +++ b/configs/config.common.inc @@ -0,0 +1,41 @@ +{ + "modes": [ + "ipv4_tcp_udp" + ], + "common": { + "server_port":443, + "local_address":"127.0.0.1", + "local_port":1080, + "password":"hello_kitty", + "timeout":3600, + "method":"aes-256-gcm", + "mode":"tcp_and_udp", + "fast_open":true, + "ipv6_first":false, + "reuse_port":true, + "protocol":"redir", + "plugin_mode":"tcp_only", + "tcp_redir":"redirect", + "#tcp_redir":"tproxy", + "udp_redir":"tproxy" + }, + "ipv4_tcp_udp": {}, + "ipv6_tcp_udp": { + "local_address":"::1" + }, + "ipv4_tcp_only": { + "mode": "tcp_only" + }, + "ipv4_udp_only": { + "mode": "udp_only" + }, + "ipv6_tcp_only": { + "local_address":"::1", + "mode": "tcp_only" + }, + "ipv6_udp_only": { + "local_address":"::1", + "mode": "udp_only" + }, + "ipv4_ipv6_tcp_udp": {} +} diff --git a/ss-autostart.service b/ss-autostart.service new file mode 100644 index 0000000..07067e3 --- /dev/null +++ b/ss-autostart.service @@ -0,0 +1,14 @@ +[Unit] +Description=Configure transparent proxy +After=network-online.target + +[Service] +Type=simple +User=root +RemainAfterExit=true +Environment="SCRIPT=/path/to/ss.py" +ExecStart=/usr/bin/python ${SCRIPT} up +TimeoutStopSec=2 + +[Install] +WantedBy=multi-user.target diff --git a/ss.py b/ss.py new file mode 100755 index 0000000..7a198dc --- /dev/null +++ b/ss.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python3 +import os +import argparse +from pathlib import Path +from functools import reduce +import json +import subprocess +import ipaddress +import socket + +script_path = Path(__file__).parent +local_config_path = script_path / "configs" +include_config = local_config_path / "config.common.inc" +ss_config_paths = [Path("/etc/shadowsocks"), Path("/etc/shadowsocks-rust"), Path("/etc/shadowsocks-libev")] +ss_service_name = "shadowsocks-libev-redir@" # "shadowsocks-rust@" +ss_prefix = "autogen-" +ss_config_path =[p for p in ss_config_paths if p.exists()][0] +ss_config_get = lambda name: ss_config_path / f"{ss_prefix}{name}.json" +ss_service_get = lambda name: f"{ss_service_name}{ss_prefix}{name}.service" +nft_rule_redir = script_path / "transparent-proxy.nft" +nft_rule_v6_redir = script_path / "transparent-proxy-v6.nft" +nft_rule_tproxy = script_path / "transparent-proxy-tproxy.nft" +nft_rule_v6_tproxy = script_path / "transparent-proxy-v6-tproxy.nft" +chnroute = "/etc/dnsmasq.d/chinadns_chnroute.txt" +chnroute6 = "/etc/dnsmasq.d/chinadns_chnroute6.txt" +proxy_interfaces = [] +proxy_interfaces_v6 = [] or proxy_interfaces +extra_bypass = [] + + +def gen_configs(config_name: str) -> dict: + config_inc = json.loads(include_config.read_text()) + RUST_ATTRS = {'mv': ('local_address', 'local_port', 'mode', 'protocol'), 'mvdel': ('tcp_redir', 'udp_redir')} + assert config_inc['common']['tcp_redir'] in ('redirect', 'tproxy') + assert config_inc['common']['udp_redir'] == 'tproxy' + rust_only = config_inc['common']['tcp_redir'] != 'redirect' + config = json.loads((local_config_path / f"{config_name}.json").read_text()) + MODES = ("ipv4_tcp_udp", "ipv6_tcp_udp", "ipv4_tcp_only", "ipv4_udp_only", "ipv6_tcp_only", "ipv6_udp_only", "ipv4_ipv6_tcp_udp") + # handle legacy config + config_common = config if "modes" not in config else config["common"] + config_inc["common"] = dict(sorted({**config_inc["common"], **config_common}.items())) + if "modes" in config: + config_inc["modes"] = config["modes"] + assert all(m in MODES for m in config_inc["modes"]) + assert config_inc["modes"] + def assert_overlap(): + _f = lambda ipx, proto: len([m for m in config["modes"] if ipx in m and proto in m]) > 1 + IPX = ("ipv4", "ipv6") + L4PROTO = ("tcp", "udp") + assert not any(_f(ipx, proto) for ipx in IPX for proto in L4PROTO) # has overlap + ipvx_enabled = lambda ipx: any(ipx in m for m in config_inc['modes']) + # has both tcp and udp + assert all(any(True for m in config["modes"] if ipx in m and proto in m) for ipx in IPX if ipvx_enabled(ipx) for proto in L4PROTO) + assert_overlap() + for m in config_inc["modes"]: + config_inc[m] = dict(sorted({**config_inc["common"], **config_inc.get(m, dict()), **config.get(m, dict())}.items())) + for idx, m in enumerate(config_inc["modes"]): + config_inc[m] = dict(sorted({**config_inc["common"], **config_inc.get(m, dict())}.items())) + if rust_only: + config_inc[m]['locals'] = [{k: config_inc[m][k] for k in reduce(lambda x,y:x+y, RUST_ATTRS.values())}] + for _a in reduce(lambda x,y:x+y, RUST_ATTRS.values()): + config_inc[m][f"#{_a}"] = config_inc[m].pop(_a, None) + else: + for _a in reduce(lambda x,y:x+y, RUST_ATTRS.values()): + config_inc[m][f"#{_a}"] = config_inc[m].get(_a, None) + for _a in RUST_ATTRS['mvdel']: + config_inc[m].pop(_a, None) + if idx == 0: + config_inc[m]['_meta_name'] = config_name + return config_inc + +def print_config_names(do_print=True) -> str: + def get_current_up() -> str: + primary_conf = ss_config_get(0) + try: + if primary_conf.exists(): + current_up = json.loads(primary_conf.read_text())['_meta_name'] + return current_up + except Exception: + return "" + current_up = get_current_up() + if do_print: + for conf in local_config_path.iterdir(): + if conf.name.endswith('.json'): + name = conf.name[:-len('.json')] + _c = gen_configs(name) + c = _c[_c["modes"][0]] + server_info = " %s \t(%s:%d)" % (name, c["server"], c["server_port"]) + if name == current_up: + server_info = ">" + server_info[1:] + print(server_info) + return current_up + +def stop_and_remove(config_name): + service = ss_service_get(config_name) + if not subprocess.run(["systemctl", "is-active", service], check=False, capture_output=True).returncode: + if subprocess.run(["systemctl", "stop", service], check=False).returncode: + print(f"[!] systemctl stop {service} failed") + ss_config_get(config_name).unlink() + +def stop_all_configs(): + for conf in ss_config_path.iterdir(): + if conf.name.endswith(".json") and conf.name.startswith(ss_prefix): + name = conf.name[len(ss_prefix):-len(".json")] + service = ss_service_get(name) + if not subprocess.run(["systemctl", "is-active", service], check=False, capture_output=True).returncode: + if subprocess.run(["systemctl", "stop", service], check=False).returncode: + print(f"[!] systemctl stop {service} failed") + print(f"stopped {service}") + +def write_and_enable_configs(config_dict, dry_run=False) -> bool: + changed = [False, False, False] + def mark_changed(x): + changed[x] = True + idx_to_name = {k: v for k, v in enumerate(config_dict['modes'])} + for conf in ss_config_path.iterdir(): + if conf.name.endswith(".json") and conf.name.startswith(ss_prefix): + name = conf.name[len(ss_prefix):-len(".json")] + try: + idx = int(name) + assert idx in idx_to_name + except Exception: + if dry_run: + print(f"check failed: should stop and remove {conf.name=}") + else: + stop_and_remove(name) + mark_changed(0) + for idx, name in enumerate(config_dict['modes']): + cfgname = str(idx) + cfg = ss_config_get(cfgname) + old = cfg.read_text() if cfg.exists() else "" + new = json.dumps({k:v for k, v in config_dict[name].items() if not k.startswith("#")}) + config_same = new == old + if not config_same: + if dry_run: + print(f"check failed: should write {cfgname} {name}") + else: + cfg.write_text(new) + mark_changed(1) + systemd_ret = subprocess.run(["systemctl", "is-active", ss_service_get(cfgname)], check=False, capture_output=True).returncode + def restart_service(name): + service = ss_service_get(name) + if dry_run: + print(f"check failed: should start {service}") + else: + if subprocess.run(["systemctl", "restart", service], check=False).returncode: + print(f"[!] systemctl start {service} failed") + mark_changed(2) + if systemd_ret: + restart_service(cfgname) + else: + if not config_same: + restart_service(cfgname) + if changed[0]: + print("deleted old config") + if changed[1]: + print("wrote new config") + if changed[2]: + print("restart systemd") + +def invoke_self_with_sudo(): + assert os.getuid() != 0 + import sys + return subprocess.run(["sudo", sys.executable, *sys.argv], check=False).returncode + +def prepare_cgroup_path(): + CGv2_ROOT = Path('/sys/fs/cgroup') + needed_slices = ('ss_bp.slice', 'ss_bp_tcp.slice', 'ss_bp_udp.slice', 'ss_fw.slice', 'ss_fw_tcp.slice', 'ss_fw_udp.slice') + for slice in needed_slices: + (CGv2_ROOT / slice).mkdir(exist_ok=True) + +def process_nft_rule(configs: dict) -> list: + nft_rule, nft_rule_v6 = (nft_rule_redir, nft_rule_v6_redir) \ + if configs['common']['tcp_redir'] == 'redirect' \ + else (nft_rule_tproxy, nft_rule_v6_tproxy) + def get_family_proto_config(family: int, l4proto: str) -> str: + filter_family = [m for m in configs['modes'] if f"ipv{family}" in m] + mode = [m for m in filter_family if l4proto in m][0] + return mode + def process_nft_rule(family: int) -> str: + nft_lines = list(filter(None, (nft_rule_v6 if family == 6 else nft_rule).read_text().split('\n'))) + nft_lines = nft_lines[nft_lines.index('## DO NOT CHANGE THIS LINE'):] + + _tcp = configs[get_family_proto_config(family, 'tcp')] + _udp = configs[get_family_proto_config(family, 'udp')] + def get_server(hostname_or_ip: str): + try: + server = ipaddress.ip_address(hostname_or_ip) + except ValueError: + server = ipaddress.ip_address(socket.getaddrinfo(hostname_or_ip, None, type=socket.SOCK_RAW)[0][4][0]) + return server + _tcp_server = get_server(_tcp['server']) + _udp_server = get_server(_udp['server']) + proxy_ifs_real = proxy_interfaces_v6 if family == 6 else proxy_interfaces + nft_define = { + 'tcp_host': f"@empty_ipv{family}" if _tcp_server.version != family else str(_tcp_server), + 'udp_host': f"@empty_ipv{family}" if _udp_server.version != family else str(_udp_server), + 'tcp_proxy_ifnames': "{ %s }" % ', '.join([f'"{x}"' for x in proxy_ifs_real]) if proxy_ifs_real else '@empty_str', + 'udp_proxy_ifnames': "{ %s }" % ', '.join([f'"{x}"' for x in proxy_ifs_real]) if proxy_ifs_real else '@empty_str', + 'tcp_server_port': _tcp['server_port'], + 'udp_server_port': _udp['server_port'], + 'tcp_local_port': _tcp['#local_port'], + 'udp_local_port': _udp['#local_port'] + } + nft_lines = [f"define {k} = {v}" for k, v in nft_define.items()] + nft_lines + return '\n'.join(nft_lines) + ipvx_enabled = lambda x: any(f"ipv{x}" in m for m in configs['modes']) + return {x: process_nft_rule(x) for x in (4, 6) if ipvx_enabled(x)} + +def flush_nft() -> bool: + nft = '\n'.join(( + 'add table ip transparent_proxy', + 'delete table ip transparent_proxy', + 'add table ip6 transparent_proxy_v6', + 'delete table ip6 transparent_proxy_v6', + 'add table ip6 output_deny', + 'delete table ip6 output_deny', + )).encode('utf-8') + if subprocess.run(["nft", "-f", "-"], input=nft, check=False).returncode: + print("[!] nft flush failed") + return False + return True + +def flush_iproute2() -> None: + ip_batch = '\n'.join(('route flush table 100', 'rule del fwmark 0xdeaf table 100')).encode('utf-8') + subprocess.run(["ip", "-force", "-batch", "-"], input=ip_batch, check=False, stderr=subprocess.DEVNULL) + subprocess.run(["ip", "-6", "-force", "-batch", "-"], input=ip_batch, check=False, stderr=subprocess.DEVNULL) # always run v6 cleanup + +def main(): + parser = argparse.ArgumentParser(description='ss.py') + parser.add_argument('action', type=str, default='info', nargs='?', choices=['info', 'up', 'down'], help='what to do') + parser.add_argument('config', type=str, default=None, nargs='?', help='config name') + parser.add_argument('-s', '--stop-all', action='store_true', help='stop systemd units') + args = parser.parse_args() + if args.action == 'info': + name = print_config_names() + if name: + if (local_config_path / f"{name}.json").exists(): + write_and_enable_configs(gen_configs(name), dry_run=True) + else: + print(f"[!] current config {name}.json is missing") + elif args.action == 'up': + if os.getuid() != 0: + return invoke_self_with_sudo() + prepare_cgroup_path() + if not args.config: + name = print_config_names(do_print=False) + args.config = name + print("autoselected config %s" % name) + assert args.config + configs = gen_configs(args.config) + write_and_enable_configs(configs) + ipvx_enabled = lambda x: any(f"ipv{x}" in m for m in configs['modes']) + nfts = {k: v.encode('utf-8') for k, v in process_nft_rule(configs).items()} + flush_iproute2() + ip_batch = '\n'.join(('route add local default dev lo table 100', 'rule add fwmark 0xdeaf table 100')).encode('utf-8') + for x in (4, 6): + if ipvx_enabled(x): + if subprocess.run(["ip", f"-{x}", "-force", "-batch", "-"], input=ip_batch, check=False).returncode: + print(f"[!] iproute2 ipv{x} failed") + flush_nft() + for x, nft in nfts.items(): + if subprocess.run(["nft", "-f", "-"], input=nft, check=False).returncode: + print(f"[!] nft ipv{x} failed, flushing") + flush_nft() + break + else: + bp = [ipaddress.ip_network(net) for net in extra_bypass] + for x in (4, 6): + if ipvx_enabled(x): + nft_chnroute = list(filter(None, Path(chnroute6 if x==6 else chnroute).read_text().split('\n'))) + nft_chnroute.extend([str(net) for net in bp if net.version == x]) + nft_chnroute_rule = '\n'.join([(f"add element {'ip6' if x==6 else 'ip'} " + f"transparent_proxy{'_v6' if x==6 else ''} chnroute {{ {ipx} }}") for ipx in nft_chnroute]).encode('utf-8') + if subprocess.run(["nft", "-f", "-"], input=nft_chnroute_rule, check=False).returncode: + print("[!] nft chnroute failed") + elif args.action == 'down': + if os.getuid() != 0: + return invoke_self_with_sudo() + flush_iproute2() + flush_nft() + if args.stop_all: + stop_all_configs() + +if __name__ == "__main__": + exit(main() or 0) diff --git a/transparent-proxy-tproxy.nft b/transparent-proxy-tproxy.nft new file mode 100644 index 0000000..583e34c --- /dev/null +++ b/transparent-proxy-tproxy.nft @@ -0,0 +1,98 @@ +define tcp_host = @empty_ipv4 +define udp_host = @empty_ipv4 +define tcp_proxy_ifnames = @empty_str +define udp_proxy_ifnames = @empty_str +define tcp_server_port = 443 +define udp_server_port = 443 +define tcp_local_port = 1080 +define udp_local_port = 1080 + +## DO NOT CHANGE THIS LINE +# need "ss_bp.slice", "ss_bp_tcp.slice", "ss_bp_udp.slice", "ss_fw.slice", "ss_fw_tcp.slice", "ss_fw_udp.slice" + +add table ip transparent_proxy +delete table ip transparent_proxy +table ip transparent_proxy { + set empty_ipv4 { + type ipv4_addr + flags constant + } + set empty_str { + typeof iifname + flags constant + } + set chnroute { + type ipv4_addr + flags interval + auto-merge + + elements = { + 0.0.0.0/8, + 10.0.0.0/8, + 100.64.0.0/10, + 127.0.0.0/8, + 169.254.0.0/16, + 172.16.0.0/12, + 192.0.0.0/24, + 192.0.2.0/24, + 192.88.99.0/24, + 192.168.0.0/16, + 198.18.0.0/15, + 198.51.100.0/24, + 203.0.113.0/24, + 224.0.0.0/4, + 240.0.0.0/4, + 255.255.255.255, + } + } + + chain mangle_prerouting { + type filter hook prerouting priority mangle + policy accept + + ip protocol { tcp, udp } iif lo meta mark 0xdeaf goto tcp_udp_tproxy + ip protocol tcp iifname $tcp_proxy_ifnames ip daddr != @chnroute goto tcp_udp_forward_conditional_tproxy + ip protocol udp iifname $udp_proxy_ifnames ip daddr != @chnroute goto tcp_udp_forward_conditional_tproxy + } + + chain mangle_output { + type route hook output priority mangle + policy accept + + ip protocol tcp ct direction reply accept + ip protocol tcp socket cgroupv2 level 1 { "ss_bp.slice", "ss_bp_tcp.slice" } accept + ip protocol udp socket cgroupv2 level 1 { "ss_bp.slice", "ss_bp_udp.slice" } accept + ip protocol tcp socket cgroupv2 level 1 { "ss_fw.slice", "ss_fw_tcp.slice" } goto tcp_udp_output_mark + ip protocol udp socket cgroupv2 level 1 { "ss_fw.slice", "ss_fw_udp.slice" } goto tcp_udp_output_mark + ip protocol tcp ip daddr $tcp_host tcp dport != $tcp_server_port goto tcp_udp_output_mark + ip protocol udp ip daddr $udp_host udp dport != $udp_server_port goto tcp_udp_output_mark + ip protocol tcp ip daddr $tcp_host accept + ip protocol udp ip daddr $udp_host accept + ip protocol { tcp, udp } ip daddr != @chnroute goto tcp_udp_output_mark + } + chain tcp_udp_output_mark { + ip protocol { tcp, udp } mark set 0xdeaf + } + chain tcp_udp_forward_conditional_tproxy { + ip protocol tcp ip daddr $tcp_host tcp dport != $tcp_server_port meta mark set 0x0000deaf goto tcp_udp_tproxy + ip protocol udp ip daddr $udp_host udp dport != $udp_server_port meta mark set 0x0000deaf goto tcp_udp_tproxy + ip protocol tcp ip daddr $tcp_host accept + ip protocol udp ip daddr $udp_host accept + ip protocol { tcp, udp } ip daddr != @chnroute meta mark set 0x0000deaf goto tcp_udp_tproxy + } + chain tcp_udp_tproxy { + ip protocol tcp tproxy to 127.0.0.1:$tcp_local_port + ip protocol udp tproxy to 127.0.0.1:$udp_local_port + } +} + +add table ip6 output_deny +delete table ip6 output_deny +table ip6 output_deny { + chain output { + type filter hook output priority filter + policy accept + + ip6 daddr != { ::/127, ::ffff:0:0/96, ::ffff:0:0:0/96, 64:ff9b::/96, 64:ff9b:1::/48, 100::/64, 2001:0000::/32, 2001:20::/28, 2001:db8::/32, fc00::/7, fe80::/64, ff00::/8 } reject + } +} diff --git a/transparent-proxy-v6-tproxy.nft b/transparent-proxy-v6-tproxy.nft new file mode 100644 index 0000000..eaad51c --- /dev/null +++ b/transparent-proxy-v6-tproxy.nft @@ -0,0 +1,87 @@ +define tcp_host = @empty_ipv6 +define udp_host = @empty_ipv6 +define tcp_proxy_ifnames = @empty_str +define udp_proxy_ifnames = @empty_str +define tcp_server_port = 443 +define udp_server_port = 443 +define tcp_local_port = 1080 +define udp_local_port = 1080 + +## DO NOT CHANGE THIS LINE +# need "ss_bp.slice", "ss_bp_tcp.slice", "ss_bp_udp.slice", "ss_fw.slice", "ss_fw_tcp.slice", "ss_fw_udp.slice" + +# this works since v4 rule is always loaded first +add table ip6 output_deny +delete table ip6 output_deny + +add table ip6 transparent_proxy_v6 +delete table ip6 transparent_proxy_v6 +table ip6 transparent_proxy_v6 { + set empty_ipv6 { + type ipv6_addr + flags constant + } + set empty_str { + typeof iifname + flags constant + } + set chnroute { + type ipv6_addr + flags interval + auto-merge + + elements = { + ::/127, + ::ffff:0:0/96, + ::ffff:0:0:0/96, + 64:ff9b::/96, + 64:ff9b:1::/48, + 100::/64, + 2001:0000::/32, + 2001:20::/28, + 2001:db8::/32, + fc00::/7, + fe80::/64, + ff00::/8, + } + } + + chain mangle_prerouting { + type filter hook prerouting priority mangle + policy accept + + meta l4proto { tcp, udp } iif lo meta mark 0xdeaf goto tcp_udp_tproxy + meta l4proto tcp iifname $tcp_proxy_ifnames ip6 daddr != @chnroute goto tcp_udp_forward_conditional_tproxy + meta l4proto udp iifname $udp_proxy_ifnames ip6 daddr != @chnroute goto tcp_udp_forward_conditional_tproxy + } + + chain mangle_output { + type route hook output priority mangle + policy accept + + meta l4proto tcp ct direction reply accept + meta l4proto tcp socket cgroupv2 level 1 { "ss_bp.slice", "ss_bp_tcp.slice" } accept + meta l4proto udp socket cgroupv2 level 1 { "ss_bp.slice", "ss_bp_udp.slice" } accept + meta l4proto tcp socket cgroupv2 level 1 { "ss_fw.slice", "ss_fw_tcp.slice" } goto tcp_udp_output_mark + meta l4proto udp socket cgroupv2 level 1 { "ss_fw.slice", "ss_fw_udp.slice" } goto tcp_udp_output_mark + meta l4proto tcp ip6 daddr $tcp_host tcp dport != $tcp_server_port goto tcp_udp_output_mark + meta l4proto udp ip6 daddr $udp_host udp dport != $udp_server_port goto tcp_udp_output_mark + meta l4proto tcp ip6 daddr $tcp_host accept + meta l4proto udp ip6 daddr $udp_host accept + meta l4proto { tcp, udp } ip6 daddr != @chnroute goto tcp_udp_output_mark + } + chain tcp_udp_output_mark { + meta l4proto { tcp, udp } mark set 0xdeaf + } + chain tcp_udp_forward_conditional_tproxy { + meta l4proto tcp ip6 daddr $tcp_host tcp dport != $tcp_server_port meta mark set 0x0000deaf goto tcp_udp_tproxy + meta l4proto udp ip6 daddr $udp_host udp dport != $udp_server_port meta mark set 0x0000deaf goto tcp_udp_tproxy + meta l4proto tcp ip6 daddr $tcp_host accept + meta l4proto udp ip6 daddr $udp_host accept + meta l4proto { tcp, udp } ip6 daddr != @chnroute meta mark set 0x0000deaf goto tcp_udp_tproxy + } + chain tcp_udp_tproxy { + meta l4proto tcp tproxy to [::1]:$tcp_local_port + meta l4proto udp tproxy to [::1]:$udp_local_port + } +} diff --git a/transparent-proxy-v6.nft b/transparent-proxy-v6.nft new file mode 100644 index 0000000..a59ce41 --- /dev/null +++ b/transparent-proxy-v6.nft @@ -0,0 +1,105 @@ +define tcp_host = @empty_ipv6 +define udp_host = @empty_ipv6 +define tcp_proxy_ifnames = @empty_str +define udp_proxy_ifnames = @empty_str +define tcp_server_port = 443 +define udp_server_port = 443 +define tcp_local_port = 1080 +define udp_local_port = 1080 + +## DO NOT CHANGE THIS LINE +# need "ss_bp.slice", "ss_bp_tcp.slice", "ss_bp_udp.slice", "ss_fw.slice", "ss_fw_tcp.slice", "ss_fw_udp.slice" + +# this works since v4 rule is always loaded first +add table ip6 output_deny +delete table ip6 output_deny + +add table ip6 transparent_proxy_v6 +delete table ip6 transparent_proxy_v6 +table ip6 transparent_proxy_v6 { + set empty_ipv6 { + type ipv6_addr + flags constant + } + set empty_str { + typeof iifname + flags constant + } + set chnroute { + type ipv6_addr + flags interval + auto-merge + + elements = { + ::/127, + ::ffff:0:0/96, + ::ffff:0:0:0/96, + 64:ff9b::/96, + 64:ff9b:1::/48, + 100::/64, + 2001:0000::/32, + 2001:20::/28, + 2001:db8::/32, + fc00::/7, + fe80::/64, + ff00::/8, + } + } + + # tcp part + + chain nat_prerouting { + type nat hook prerouting priority dstnat + policy accept + + meta l4proto tcp iifname $tcp_proxy_ifnames jump tcp_pre_redirect + } + chain nat_output { + type nat hook output priority -100 + policy accept + + meta l4proto tcp socket cgroupv2 level 1 { "ss_bp.slice", "ss_bp_tcp.slice" } accept + meta l4proto tcp socket cgroupv2 level 1 { "ss_fw.slice", "ss_fw_tcp.slice" } goto tcp_redirect + meta l4proto tcp jump tcp_pre_redirect + } + chain tcp_pre_redirect { + meta l4proto tcp ip6 daddr $tcp_host tcp dport != $tcp_server_port goto tcp_redirect + meta l4proto tcp ip6 daddr $tcp_host accept + meta l4proto tcp ip6 daddr != @chnroute goto tcp_redirect + } + chain tcp_redirect { + meta l4proto tcp redirect to :$tcp_local_port + } + + # udp part + + chain mangle_prerouting { + type filter hook prerouting priority mangle + policy accept + + meta l4proto udp iif lo meta mark 0xdeaf goto udp_tproxy + meta l4proto udp iifname $udp_proxy_ifnames ip6 daddr != @chnroute goto udp_forward_conditional_tproxy + } + + chain mangle_output { + type route hook output priority mangle + policy accept + + meta l4proto udp socket cgroupv2 level 1 { "ss_bp.slice", "ss_bp_udp.slice" } accept + meta l4proto udp socket cgroupv2 level 1 { "ss_fw.slice", "ss_fw_udp.slice" } goto udp_output_mark + meta l4proto udp ip6 daddr $udp_host udp dport != $udp_server_port goto udp_output_mark + meta l4proto udp ip6 daddr $udp_host accept + meta l4proto udp ip6 daddr != @chnroute goto udp_output_mark + } + chain udp_output_mark { + meta l4proto udp mark set 0xdeaf + } + chain udp_forward_conditional_tproxy { + meta l4proto udp ip6 daddr $udp_host udp dport != $udp_server_port meta mark set 0x0000deaf goto udp_tproxy + meta l4proto udp ip6 daddr $udp_host accept + meta l4proto udp ip6 daddr != @chnroute meta mark set 0x0000deaf goto udp_tproxy + } + chain udp_tproxy { + meta l4proto udp tproxy to [::1]:$udp_local_port + } +} diff --git a/transparent-proxy.nft b/transparent-proxy.nft new file mode 100644 index 0000000..9636f3a --- /dev/null +++ b/transparent-proxy.nft @@ -0,0 +1,116 @@ +define tcp_host = @empty_ipv4 +define udp_host = @empty_ipv4 +define tcp_proxy_ifnames = @empty_str +define udp_proxy_ifnames = @empty_str +define tcp_server_port = 443 +define udp_server_port = 443 +define tcp_local_port = 1080 +define udp_local_port = 1080 + +## DO NOT CHANGE THIS LINE +# need "ss_bp.slice", "ss_bp_tcp.slice", "ss_bp_udp.slice", "ss_fw.slice", "ss_fw_tcp.slice", "ss_fw_udp.slice" + +add table ip transparent_proxy +delete table ip transparent_proxy +table ip transparent_proxy { + set empty_ipv4 { + type ipv4_addr + flags constant + } + set empty_str { + typeof iifname + flags constant + } + set chnroute { + type ipv4_addr + flags interval + auto-merge + + elements = { + 0.0.0.0/8, + 10.0.0.0/8, + 100.64.0.0/10, + 127.0.0.0/8, + 169.254.0.0/16, + 172.16.0.0/12, + 192.0.0.0/24, + 192.0.2.0/24, + 192.88.99.0/24, + 192.168.0.0/16, + 198.18.0.0/15, + 198.51.100.0/24, + 203.0.113.0/24, + 224.0.0.0/4, + 240.0.0.0/4, + 255.255.255.255, + } + } + + # tcp part + + chain nat_prerouting { + type nat hook prerouting priority dstnat + policy accept + + ip protocol tcp iifname $tcp_proxy_ifnames jump tcp_pre_redirect + } + chain nat_output { + type nat hook output priority -100 + policy accept + + ip protocol tcp socket cgroupv2 level 1 { "ss_bp.slice", "ss_bp_tcp.slice" } accept + ip protocol tcp socket cgroupv2 level 1 { "ss_fw.slice", "ss_fw_tcp.slice" } goto tcp_redirect + ip protocol tcp jump tcp_pre_redirect + } + chain tcp_pre_redirect { + ip protocol tcp ip daddr $tcp_host tcp dport != $tcp_server_port goto tcp_redirect + ip protocol tcp ip daddr $tcp_host accept + ip protocol tcp ip daddr != @chnroute goto tcp_redirect + } + chain tcp_redirect { + ip protocol tcp redirect to :$tcp_local_port + } + + # udp part + + chain mangle_prerouting { + type filter hook prerouting priority mangle + policy accept + + ip protocol udp iif lo meta mark 0xdeaf goto udp_tproxy + ip protocol udp iifname $udp_proxy_ifnames ip daddr != @chnroute goto udp_forward_conditional_tproxy + } + + chain mangle_output { + type route hook output priority mangle + policy accept + + ip protocol udp socket cgroupv2 level 1 { "ss_bp.slice", "ss_bp_udp.slice" } accept + ip protocol udp socket cgroupv2 level 1 { "ss_fw.slice", "ss_fw_udp.slice" } goto udp_output_mark + ip protocol udp ip daddr $udp_host udp dport != $udp_server_port goto udp_output_mark + ip protocol udp ip daddr $udp_host accept + ip protocol udp ip daddr != @chnroute goto udp_output_mark + } + chain udp_output_mark { + ip protocol udp mark set 0xdeaf + } + chain udp_forward_conditional_tproxy { + ip protocol udp ip daddr $udp_host udp dport != $udp_server_port meta mark set 0x0000deaf goto udp_tproxy + ip protocol udp ip daddr $udp_host accept + ip protocol udp ip daddr != @chnroute meta mark set 0x0000deaf goto udp_tproxy + } + chain udp_tproxy { + ip protocol udp tproxy to 127.0.0.1:$udp_local_port + } +} + +add table ip6 output_deny +delete table ip6 output_deny +table ip6 output_deny { + chain output { + type filter hook output priority filter + policy accept + + ip6 daddr != { ::/127, ::ffff:0:0/96, ::ffff:0:0:0/96, 64:ff9b::/96, 64:ff9b:1::/48, 100::/64, 2001:0000::/32, 2001:20::/28, 2001:db8::/32, fc00::/7, fe80::/64, ff00::/8 } reject + } +} diff --git a/update_list.sh b/update_list.sh new file mode 100755 index 0000000..c255c81 --- /dev/null +++ b/update_list.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -e -o pipefail + +tdir=$(mktemp -d -p /tmp update_list.XXXX) +echo $tdir |grep -Eq '^/tmp/update_list' || exit 1 +cd $tdir + +CHNROUTE=/etc/dnsmasq.d/chinadns_chnroute.txt +CHNROUTE6=/etc/dnsmasq.d/chinadns_chnroute6.txt +DNSMASQ=/etc/dnsmasq.d/dnsmasq_gfwlist.conf +DNS_IP=127.0.0.1 +DNS_PORT=5453 +CURL_OPT='--user-agent curl/8.1.2' + +#CHNROUTE +FILE_CHNROUTE=$(basename $CHNROUTE) +FILE_CHNROUTE6=$(basename $CHNROUTE6) +curl $CURL_OPT https://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest -o delegated-apnic-latest.txt +cat delegated-apnic-latest.txt | grep ipv4 | grep CN | awk -F\| '{printf("%s/%d\n", $4, 32-log($5)/log(2))}' >> $FILE_CHNROUTE +cat delegated-apnic-latest.txt | grep ipv6 | grep CN | awk -F\| '{printf("%s/%d\n", $4, $5)}' >> $FILE_CHNROUTE6 +rm delegated-apnic-latest.txt + +#GFWLIST +FILE_DNSMASQ=$(basename $DNSMASQ) +git clone https://github.com/felixonmars/dnsmasq-china-list.git --depth=1 +pushd dnsmasq-china-list +make SERVER='#' dnsmasq +cat accelerated-domains.china.dnsmasq.conf google.china.dnsmasq.conf apple.china.dnsmasq.conf > $FILE_DNSMASQ + +curl $CURL_OPT https://publicsuffix.org/list/public_suffix_list.dat |python3 -c " +r=[l.split('/')[1] for l in open('${FILE_DNSMASQ}').read().split('\n') if l.strip()] +d=sorted(list({l.strip().split('.')[-1].encode('idna').decode('utf-8') for l in open(0) if l.strip() and not l.startswith('//')})) +d=['server=/'+i+'/${DNS_IP}#${DNS_PORT}' for i in d if i not in r] +print('\n') +print('\n'.join(d)) +" >> $FILE_DNSMASQ + +mv -f ../$FILE_CHNROUTE $CHNROUTE +mv -f ../$FILE_CHNROUTE6 $CHNROUTE6 +mv -f ./$FILE_DNSMASQ $DNSMASQ +popd + +cd / +rm -rf $tdir