Commit 993da19
Changed files (2)
modules
hosts
tribios
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"];
+ };
+ };
+ };
+ };
+}