main
1{lib, ...}: {
2 den.aspects.services.mihomo.tproxy = {
3 nixos = {
4 host,
5 pkgs,
6 ...
7 }: let
8 cfg = host.settings.services.mihomo;
9 tproxyMark = "666";
10 # Unqualified policy rule: fib_validate_source() keeps the mark on reverse
11 # lookups (iif lo), which would re-route LAN packets into table 100.
12 selfMark = "667";
13 tproxyRules = pkgs.writeText "mihomo-tproxy.nft" ''
14 table inet mihomo {
15 define MIHOMO_TPROXY_MARK=${tproxyMark}
16 define MIHOMO_SELF_MARK=${selfMark}
17 define MIHOMO_TPROXY_PORT=${toString cfg.ports.tproxy}
18 define MIHOMO_DNS_PORT=${toString cfg.ports.dns}
19 set bypass-ipv4 {
20 type ipv4_addr
21 flags interval
22 elements = {
23 0.0.0.0/8,
24 10.0.0.0/8,
25 100.64.0.0/10,
26 127.0.0.0/8,
27 169.254.0.0/16,
28 172.16.0.0/12,
29 192.168.0.0/16,
30 224.0.0.0/4,
31 240.0.0.0/4
32 }
33 }
34 set bypass-ipv6 {
35 type ipv6_addr
36 flags interval
37 elements = {
38 ::/128,
39 ::1/128,
40 fc00::/7,
41 fe80::/10,
42 ff00::/8
43 }
44 }
45 set bypass-tcp-ports {
46 type inet_service
47 elements = { 53, 67, 68, 123 }
48 }
49 set bypass-udp-ports {
50 type inet_service
51 # 3478: STUN; 40000: peer relay; 41641: WireGuard endpoints of Tailscale peers.
52 elements = { 53, 67, 68, 123, 3478, 40000, 41641 }
53 }
54 set bypass-udp-sports {
55 type inet_service
56 # LAN WireGuard sockets: proxying breaks NAT endpoint
57 # discovery and tailscaled already holds UDP 41641 (EADDRINUSE).
58 elements = { 41641 }
59 }
60 set outbounds {
61 type ifname
62 elements = { ${cfg.interfaces.wan} }
63 }
64 chain tproxy-prerouting {
65 type filter hook prerouting priority mangle; policy accept;
66 meta l4proto { tcp, udp } socket transparent 1 mark set $MIHOMO_TPROXY_MARK return
67 socket transparent 0 socket wildcard 0 return
68 ip daddr @bypass-ipv4 return
69 ip6 daddr @bypass-ipv6 return
70 tcp dport @bypass-tcp-ports return
71 udp dport @bypass-udp-ports return
72 udp sport @bypass-udp-sports return
73 fib daddr type { local, broadcast, anycast, multicast } return
74 meta l4proto { tcp, udp } tproxy to :$MIHOMO_TPROXY_PORT meta mark set $MIHOMO_TPROXY_MARK return
75 }
76 chain tproxy-output {
77 type route hook output priority mangle; policy accept;
78 # Exempt mihomo's own traffic (proxy nodes, DoH, providers).
79 meta skuid mihomo return
80 oifname != @outbounds return
81 ip daddr @bypass-ipv4 return
82 ip6 daddr @bypass-ipv6 return
83 tcp dport @bypass-tcp-ports return
84 udp dport @bypass-udp-ports return
85 udp sport @bypass-udp-sports return
86 fib daddr type { local, broadcast, anycast, multicast } return
87 meta l4proto { tcp, udp } meta mark set $MIHOMO_SELF_MARK return
88 }
89 chain dns-prerouting {
90 type nat hook prerouting priority dstnat; policy accept;
91 tcp dport 53 redirect to :$MIHOMO_DNS_PORT
92 udp dport 53 redirect to :$MIHOMO_DNS_PORT
93 }
94 chain dns-output {
95 type nat hook output priority dstnat; policy accept;
96 meta skuid mihomo return
97 # Keep systemd-resolved loops (127.0.0.53) and Tailscale
98 # MagicDNS (100.100.100.100) intact.
99 ip daddr @bypass-ipv4 return
100 ip6 daddr @bypass-ipv6 return
101 tcp dport 53 redirect to :$MIHOMO_DNS_PORT
102 udp dport 53 redirect to :$MIHOMO_DNS_PORT
103 }
104 }
105 '';
106 in {
107 boot.kernel.sysctl = {
108 "net.ipv4.conf.all.rp_filter" = 0;
109 "net.ipv4.conf.default.rp_filter" = 0;
110 };
111
112 # rpfilter's `fib saddr . mark` hits table 100 on marked packets (oif
113 # never equals iif there), so they must be exempted.
114 networking.firewall.extraReversePathFilterRules = "meta mark { ${tproxyMark}, ${selfMark} } accept";
115
116 services.mihomo = {
117 # tunMode grants CAP_NET_ADMIN (no PrivateUsers) for IP_TRANSPARENT;
118 # the TUN device stays off because nftables intercepts traffic.
119 tunMode = true;
120 config = {
121 tun.enable = lib.mkForce false;
122 tproxy-port = cfg.ports.tproxy;
123 };
124 };
125
126 systemd.services.mihomo = {
127 serviceConfig = let
128 ip = lib.getExe' pkgs.iproute2 "ip";
129 # Qualify by LAN ingress: fib_validate_source() keeps the fwmark, so a
130 # bare rule would also match reverse lookups (iif lo) into table 100.
131 ipRulesAdd =
132 lib.concatMapStrings (ifname: ''
133 ${ip} rule add fwmark ${tproxyMark} iif ${ifname} lookup 100 pref 5000 2>/dev/null || true
134 '')
135 cfg.interfaces.lan;
136 ipRulesDel =
137 lib.concatMapStrings (ifname: ''
138 ${ip} rule del fwmark ${tproxyMark} iif ${ifname} lookup 100 pref 5000 2>/dev/null || true
139 '')
140 cfg.interfaces.lan;
141 in {
142 ExecStartPre = lib.mkAfter [
143 "+${pkgs.writeShellScript "mihomo-tproxy-start" ''
144 ${lib.getExe pkgs.nftables} delete table inet mihomo 2>/dev/null || true
145 ${lib.getExe pkgs.nftables} -f ${tproxyRules}
146 ${ipRulesAdd}
147 ${ip} rule add fwmark ${selfMark} lookup 100 pref 5001 2>/dev/null || true
148 ${ip} route add local 0.0.0.0/0 dev lo table 100 2>/dev/null || true
149 ''}"
150 ];
151 ExecStopPost = lib.mkAfter [
152 "+${pkgs.writeShellScript "mihomo-tproxy-stop" ''
153 ${ip} route del local 0.0.0.0/0 dev lo table 100 2>/dev/null || true
154 ${ip} rule del fwmark ${selfMark} lookup 100 pref 5001 2>/dev/null || true
155 ${ipRulesDel}
156 ${lib.getExe pkgs.nftables} delete table inet mihomo 2>/dev/null || true
157 ''}"
158 ];
159 AmbientCapabilities = lib.mkAfter ["CAP_NET_BIND_SERVICE"];
160 CapabilityBoundingSet = lib.mkAfter ["CAP_NET_BIND_SERVICE"];
161 };
162 };
163 };
164 };
165}