This commit is contained in:
JerryXiao 2023-07-10 15:35:57 +08:00
commit f0c1c36418
Signed by: Jerry
GPG key ID: 22618F758B5BE2E5
11 changed files with 833 additions and 0 deletions

4
configs/1.json Normal file
View file

@ -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"
}

12
configs/2.json Normal file
View file

@ -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"
}
}

26
configs/4.json Normal file
View file

@ -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
}
}

41
configs/config.common.inc Normal file
View file

@ -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": {}
}

14
ss-autostart.service Normal file
View file

@ -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

286
ss.py Executable file
View file

@ -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)

View file

@ -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
}
}

View file

@ -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
}
}

105
transparent-proxy-v6.nft Normal file
View file

@ -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
}
}

116
transparent-proxy.nft Normal file
View file

@ -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
}
}

44
update_list.sh Executable file
View file

@ -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