old
1{lib, ...}: {
2 den.aspects.services.proxy.mihomo.tproxy = {
3 nixos = {
4 host,
5 pkgs,
6 ...
7 }: let
8 cfg = host.settings.services.proxy.mihomo;
9 tproxyMark = "666";
10 # Marks WAN-bound local traffic in the output chain. Must differ
11 # from tproxyMark and its policy rule must stay un-qualified:
12 # fib_validate_source() reverse lookups always run with
13 # flowi4_iif = lo and (src_valid_mark=1, set by tailscaled) keep
14 # the packet mark, so reusing mark 666 or adding "iif lo" would
15 # route LAN reverse lookups into table 100 again and drop them
16 # as martians. Looped-back self packets skip source validation
17 # because their output dst survives loopback_xmit.
18 selfMark = "667";
19 tproxyRules = pkgs.writeText "mihomo-tproxy.nft" ''
20 table inet mihomo {
21 define MIHOMO_TPROXY_MARK=${tproxyMark}
22 define MIHOMO_SELF_MARK=${selfMark}
23 define MIHOMO_TPROXY_PORT=${toString cfg.ports.tproxy}
24 define MIHOMO_DNS_PORT=${toString cfg.ports.dns}
25 set bypass-ipv4 {
26 type ipv4_addr
27 flags interval
28 elements = {
29 0.0.0.0/8,
30 10.0.0.0/8,
31 100.64.0.0/10,
32 127.0.0.0/8,
33 169.254.0.0/16,
34 172.16.0.0/12,
35 192.168.0.0/16,
36 224.0.0.0/4,
37 240.0.0.0/4
38 }
39 }
40 set bypass-ipv6 {
41 type ipv6_addr
42 flags interval
43 elements = {
44 ::/128,
45 ::1/128,
46 fc00::/7,
47 fe80::/10,
48 ff00::/8
49 }
50 }
51 set bypass-tcp-ports {
52 type inet_service
53 elements = { 53, 67, 68, 123 }
54 }
55 set bypass-udp-ports {
56 type inet_service
57 # 3478: STUN; 41641: WireGuard endpoints of Tailscale peers.
58 elements = { 53, 67, 68, 123, 3478, 41641 }
59 }
60 set bypass-udp-sports {
61 type inet_service
62 # Tailscale WireGuard sockets on LAN clients. Proxying them
63 # breaks NAT endpoint discovery, and the router's own
64 # tailscaled already occupies UDP 41641, so mihomo cannot
65 # even bind its transparent reply socket (EADDRINUSE).
66 elements = { 41641 }
67 }
68 set outbounds {
69 type ifname
70 elements = { ${cfg.interfaces.wan} }
71 }
72 chain tproxy-prerouting {
73 type filter hook prerouting priority mangle; policy accept;
74 meta l4proto { tcp, udp } socket transparent 1 mark set $MIHOMO_TPROXY_MARK return
75 socket transparent 0 socket wildcard 0 return
76 ip daddr @bypass-ipv4 return
77 ip6 daddr @bypass-ipv6 return
78 tcp dport @bypass-tcp-ports return
79 udp dport @bypass-udp-ports return
80 udp sport @bypass-udp-sports return
81 fib daddr type { local, broadcast, anycast, multicast } return
82 meta l4proto { tcp, udp } tproxy to :$MIHOMO_TPROXY_PORT meta mark set $MIHOMO_TPROXY_MARK return
83 }
84 chain tproxy-output {
85 type route hook output priority mangle; policy accept;
86 # Exempt mihomo's own traffic (proxy nodes, DoH, providers).
87 meta skuid mihomo return
88 oifname != @outbounds return
89 ip daddr @bypass-ipv4 return
90 ip6 daddr @bypass-ipv6 return
91 tcp dport @bypass-tcp-ports return
92 udp dport @bypass-udp-ports return
93 udp sport @bypass-udp-sports return
94 fib daddr type { local, broadcast, anycast, multicast } return
95 meta l4proto { tcp, udp } meta mark set $MIHOMO_SELF_MARK return
96 }
97 chain dns-prerouting {
98 type nat hook prerouting priority dstnat; policy accept;
99 tcp dport 53 redirect to :$MIHOMO_DNS_PORT
100 udp dport 53 redirect to :$MIHOMO_DNS_PORT
101 }
102 chain dns-output {
103 type nat hook output priority dstnat; policy accept;
104 meta skuid mihomo return
105 # Keep systemd-resolved loops (127.0.0.53) and Tailscale
106 # MagicDNS (100.100.100.100) intact.
107 ip daddr @bypass-ipv4 return
108 ip6 daddr @bypass-ipv6 return
109 tcp dport 53 redirect to :$MIHOMO_DNS_PORT
110 udp dport 53 redirect to :$MIHOMO_DNS_PORT
111 }
112 }
113 '';
114 in {
115 boot.kernel.sysctl = {
116 "net.ipv4.conf.all.rp_filter" = 0;
117 "net.ipv4.conf.default.rp_filter" = 0;
118 };
119
120 # The nftables firewall's strict rpfilter chain (priority mangle + 10)
121 # runs right after tproxy-prerouting (priority mangle) and does
122 # `fib saddr . mark . iif oif` with the tproxy mark set: the lookup
123 # hits table 100 (local default dev lo), oif=lo never equals iif,
124 # so every tproxied packet would be dropped without this exemption.
125 networking.firewall.extraReversePathFilterRules = "meta mark { ${tproxyMark}, ${selfMark} } accept";
126
127 services.mihomo = {
128 # tunMode grants CAP_NET_ADMIN + PrivateUsers=false needed for
129 # IP_TRANSPARENT. tun.enable = false keeps the actual TUN
130 # device off — traffic interception is done via nftables instead.
131 tunMode = true;
132 config = {
133 tun.enable = lib.mkForce false;
134 tproxy-port = cfg.ports.tproxy;
135 };
136 };
137
138 systemd.services.mihomo = {
139 serviceConfig = let
140 ip = lib.getExe' pkgs.iproute2 "ip";
141 # tailscaled sets net.ipv4.conf.all.src_valid_mark=1, making
142 # fib_validate_source() keep the fwmark during its reverse
143 # lookup. Without an iif qualifier that lookup would also hit
144 # table 100 (local default dev lo -> RTN_LOCAL, not
145 # RTN_UNICAST) and every marked packet would be dropped as a
146 # martian. The reverse lookup runs with flowi4_iif = lo, so
147 # restricting the rule to LAN ingress keeps it out of source
148 # validation while forward lookups from LAN still match.
149 ipRulesAdd =
150 lib.concatMapStrings (ifname: ''
151 ${ip} rule add fwmark ${tproxyMark} iif ${ifname} lookup 100 pref 5000 2>/dev/null || true
152 '')
153 cfg.interfaces.lan;
154 ipRulesDel =
155 lib.concatMapStrings (ifname: ''
156 ${ip} rule del fwmark ${tproxyMark} iif ${ifname} lookup 100 pref 5000 2>/dev/null || true
157 '')
158 cfg.interfaces.lan;
159 in {
160 ExecStartPre = lib.mkAfter [
161 "+${pkgs.writeShellScript "mihomo-tproxy-start" ''
162 ${lib.getExe pkgs.nftables} delete table inet mihomo 2>/dev/null || true
163 ${lib.getExe pkgs.nftables} -f ${tproxyRules}
164 ${ipRulesAdd}
165 ${ip} rule add fwmark ${selfMark} lookup 100 pref 5001 2>/dev/null || true
166 ${ip} route add local 0.0.0.0/0 dev lo table 100 2>/dev/null || true
167 ''}"
168 ];
169 ExecStopPost = lib.mkAfter [
170 "+${pkgs.writeShellScript "mihomo-tproxy-stop" ''
171 ${ip} route del local 0.0.0.0/0 dev lo table 100 2>/dev/null || true
172 ${ip} rule del fwmark ${selfMark} lookup 100 pref 5001 2>/dev/null || true
173 ${ipRulesDel}
174 ${lib.getExe pkgs.nftables} delete table inet mihomo 2>/dev/null || true
175 ''}"
176 ];
177 AmbientCapabilities = lib.mkAfter ["CAP_NET_BIND_SERVICE"];
178 CapabilityBoundingSet = lib.mkAfter ["CAP_NET_BIND_SERVICE"];
179 };
180 };
181 };
182 };
183}