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}