Commit 993da19

HPCesia <me@hpcesia.com>
2026-07-13 10:18:58
Add mihomo tproxy aspect and use it on tribios
nftables tproxy + DNS redirect for LAN and router-local traffic via fwmark policy routing. Includes nixos-fw rpfilter exemptions, LAN-iif qualified mark rule (src_valid_mark=1), Tailscale WireGuard/STUN bypass, skuid self-exemption, my_hosts DIRECT ruleset, and controller port fix. Replaces TUN mode on tribios. Assisted-by: opencode:deepseek-v4-pro
1 parent 84401b5
Changed files (2)
modules
hosts
services
proxy
mihomo
modules/hosts/tribios/networking.nix
@@ -17,7 +17,7 @@
   den.aspects.tribios = {
     includes = [
       den.aspects.services.proxy.mihomo
-      den.aspects.services.proxy.mihomo.tun
+      den.aspects.services.proxy.mihomo.tproxy
     ];
 
     nixos = {
modules/services/proxy/mihomo/tproxy.nix
@@ -0,0 +1,183 @@
+{lib, ...}: {
+  den.aspects.services.proxy.mihomo.tproxy = {
+    nixos = {
+      host,
+      pkgs,
+      ...
+    }: let
+      cfg = host.settings.services.proxy.mihomo;
+      tproxyMark = "666";
+      # Marks WAN-bound local traffic in the output chain. Must differ
+      # from tproxyMark and its policy rule must stay un-qualified:
+      # fib_validate_source() reverse lookups always run with
+      # flowi4_iif = lo and (src_valid_mark=1, set by tailscaled) keep
+      # the packet mark, so reusing mark 666 or adding "iif lo" would
+      # route LAN reverse lookups into table 100 again and drop them
+      # as martians. Looped-back self packets skip source validation
+      # because their output dst survives loopback_xmit.
+      selfMark = "667";
+      tproxyRules = pkgs.writeText "mihomo-tproxy.nft" ''
+        table inet mihomo {
+          define MIHOMO_TPROXY_MARK=${tproxyMark}
+          define MIHOMO_SELF_MARK=${selfMark}
+          define MIHOMO_TPROXY_PORT=${toString cfg.ports.tproxy}
+          define MIHOMO_DNS_PORT=${toString cfg.ports.dns}
+          set bypass-ipv4 {
+            type ipv4_addr
+            flags interval
+            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.168.0.0/16,
+              224.0.0.0/4,
+              240.0.0.0/4
+            }
+          }
+          set bypass-ipv6 {
+            type ipv6_addr
+            flags interval
+            elements = {
+              ::/128,
+              ::1/128,
+              fc00::/7,
+              fe80::/10,
+              ff00::/8
+            }
+          }
+          set bypass-tcp-ports {
+            type inet_service
+            elements = { 53, 67, 68, 123 }
+          }
+          set bypass-udp-ports {
+            type inet_service
+            # 3478: STUN; 41641: WireGuard endpoints of Tailscale peers.
+            elements = { 53, 67, 68, 123, 3478, 41641 }
+          }
+          set bypass-udp-sports {
+            type inet_service
+            # Tailscale WireGuard sockets on LAN clients. Proxying them
+            # breaks NAT endpoint discovery, and the router's own
+            # tailscaled already occupies UDP 41641, so mihomo cannot
+            # even bind its transparent reply socket (EADDRINUSE).
+            elements = { 41641 }
+          }
+          set outbounds {
+            type ifname
+            elements = { ${cfg.interfaces.wan} }
+          }
+          chain tproxy-prerouting {
+            type filter hook prerouting priority mangle; policy accept;
+            meta l4proto { tcp, udp } socket transparent 1 mark set $MIHOMO_TPROXY_MARK return
+            socket transparent 0 socket wildcard 0 return
+            ip daddr @bypass-ipv4 return
+            ip6 daddr @bypass-ipv6 return
+            tcp dport @bypass-tcp-ports return
+            udp dport @bypass-udp-ports return
+            udp sport @bypass-udp-sports return
+            fib daddr type { local, broadcast, anycast, multicast } return
+            meta l4proto { tcp, udp } tproxy to :$MIHOMO_TPROXY_PORT meta mark set $MIHOMO_TPROXY_MARK return
+          }
+          chain tproxy-output {
+            type route hook output priority mangle; policy accept;
+            # Exempt mihomo's own traffic (proxy nodes, DoH, providers).
+            meta skuid mihomo return
+            oifname != @outbounds return
+            ip daddr @bypass-ipv4 return
+            ip6 daddr @bypass-ipv6 return
+            tcp dport @bypass-tcp-ports return
+            udp dport @bypass-udp-ports return
+            udp sport @bypass-udp-sports return
+            fib daddr type { local, broadcast, anycast, multicast } return
+            meta l4proto { tcp, udp } meta mark set $MIHOMO_SELF_MARK return
+          }
+          chain dns-prerouting {
+            type nat hook prerouting priority dstnat; policy accept;
+            tcp dport 53 redirect to :$MIHOMO_DNS_PORT
+            udp dport 53 redirect to :$MIHOMO_DNS_PORT
+          }
+          chain dns-output {
+            type nat hook output priority dstnat; policy accept;
+            meta skuid mihomo return
+            # Keep systemd-resolved loops (127.0.0.53) and Tailscale
+            # MagicDNS (100.100.100.100) intact.
+            ip daddr @bypass-ipv4 return
+            ip6 daddr @bypass-ipv6 return
+            tcp dport 53 redirect to :$MIHOMO_DNS_PORT
+            udp dport 53 redirect to :$MIHOMO_DNS_PORT
+          }
+        }
+      '';
+    in {
+      boot.kernel.sysctl = {
+        "net.ipv4.conf.all.rp_filter" = 0;
+        "net.ipv4.conf.default.rp_filter" = 0;
+      };
+
+      # The nftables firewall's strict rpfilter chain (priority mangle + 10)
+      # runs right after tproxy-prerouting (priority mangle) and does
+      # `fib saddr . mark . iif oif` with the tproxy mark set: the lookup
+      # hits table 100 (local default dev lo), oif=lo never equals iif,
+      # so every tproxied packet would be dropped without this exemption.
+      networking.firewall.extraReversePathFilterRules = "meta mark { ${tproxyMark}, ${selfMark} } accept";
+
+      services.mihomo = {
+        # tunMode grants CAP_NET_ADMIN + PrivateUsers=false needed for
+        # IP_TRANSPARENT. tun.enable = false keeps the actual TUN
+        # device off — traffic interception is done via nftables instead.
+        tunMode = true;
+        config = {
+          tun.enable = lib.mkForce false;
+          tproxy-port = cfg.ports.tproxy;
+        };
+      };
+
+      systemd.services.mihomo = {
+        serviceConfig = let
+          ip = lib.getExe' pkgs.iproute2 "ip";
+          # tailscaled sets net.ipv4.conf.all.src_valid_mark=1, making
+          # fib_validate_source() keep the fwmark during its reverse
+          # lookup. Without an iif qualifier that lookup would also hit
+          # table 100 (local default dev lo -> RTN_LOCAL, not
+          # RTN_UNICAST) and every marked packet would be dropped as a
+          # martian. The reverse lookup runs with flowi4_iif = lo, so
+          # restricting the rule to LAN ingress keeps it out of source
+          # validation while forward lookups from LAN still match.
+          ipRulesAdd =
+            lib.concatMapStrings (ifname: ''
+              ${ip} rule add fwmark ${tproxyMark} iif ${ifname} lookup 100 pref 5000 2>/dev/null || true
+            '')
+            cfg.interfaces.lan;
+          ipRulesDel =
+            lib.concatMapStrings (ifname: ''
+              ${ip} rule del fwmark ${tproxyMark} iif ${ifname} lookup 100 pref 5000 2>/dev/null || true
+            '')
+            cfg.interfaces.lan;
+        in {
+          ExecStartPre = lib.mkAfter [
+            "+${pkgs.writeShellScript "mihomo-tproxy-start" ''
+              ${lib.getExe pkgs.nftables} delete table inet mihomo 2>/dev/null || true
+              ${lib.getExe pkgs.nftables} -f ${tproxyRules}
+              ${ipRulesAdd}
+              ${ip} rule add fwmark ${selfMark} lookup 100 pref 5001 2>/dev/null || true
+              ${ip} route add local 0.0.0.0/0 dev lo table 100 2>/dev/null || true
+            ''}"
+          ];
+          ExecStopPost = lib.mkAfter [
+            "+${pkgs.writeShellScript "mihomo-tproxy-stop" ''
+              ${ip} route del local 0.0.0.0/0 dev lo table 100 2>/dev/null || true
+              ${ip} rule del fwmark ${selfMark} lookup 100 pref 5001 2>/dev/null || true
+              ${ipRulesDel}
+              ${lib.getExe pkgs.nftables} delete table inet mihomo 2>/dev/null || true
+            ''}"
+          ];
+          AmbientCapabilities = lib.mkAfter ["CAP_NET_BIND_SERVICE"];
+          CapabilityBoundingSet = lib.mkAfter ["CAP_NET_BIND_SERVICE"];
+        };
+      };
+    };
+  };
+}