Linux下的透明代理( systemd & nftables )

  • 最后更新于 2024年 5月14日

参考资料

前提条件

  • Linux主机,已安装 nftables,已安装并使用 systemd 作为启动管理、网络配置管理( systemd-networkd )。
  • 本机已运行支持 Tproxy 的代理软件,监听端口以下以 2500 为例。

配置流程

传统的方式如下,这也是网络上绝大多数文章写烂了的。原文照抄的Hysteria,点上面的链接能看到完整内容。

# IPv4
ip rule add fwmark 0x1 lookup 100
ip route add local default dev lo table 100

# IPv6
ip -6 rule add fwmark 0x1 lookup 100
ip -6 route add local default dev lo table 100

第一句让 fwmark 为 1 的流量,查询路由表 100,第二句是让路由表100的流量都转发到本地lo设备。

define TPROXY_MARK=0x1
define HYSTERIA_USER=hysteria
define HYSTERIA_TPROXY_PORT=2500

# 需要代理的协议类型 
define TPROXY_L4PROTO={ tcp, udp }

# 需要绕过的地址 
define BYPASS_IPV4={
    0.0.0.0/8, 10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16,
    172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/3
}

define BYPASS_IPV6={ ::/128 }

table inet hysteria_tproxy {
  chain prerouting {
    type filter hook prerouting priority mangle; policy accept;
    # 跳过已经由 TProxy 接管的流量 
    meta l4proto $TPROXY_L4PROTO socket transparent 1 counter mark set $TPROXY_MARK
    socket transparent 0 counter return

    # 绕过私有和特殊 IP 地址
    ip daddr $BYPASS_IPV4 counter return
    ip6 daddr $BYPASS_IPV6 counter return

    # 仅对公网 IPv6 地址启用代理
    ip6 daddr != 2000::/3 counter return

    # 重定向流量到 TProxy 端口
    meta l4proto $TPROXY_L4PROTO counter tproxy to :$HYSTERIA_TPROXY_PORT meta mark set $TPROXY_MARK accept
  }
}

# 代理本机流量 
table inet hysteria_tproxy_local {
  chain output {
    type route hook output priority mangle; policy accept;

    # 通过匹配用户来避免环路
    meta skuid $HYSTERIA_USER counter return

    # 绕过私有和特殊 IP 地址
    ip daddr $BYPASS_IPV4 counter return
    ip6 daddr $BYPASS_IPV6 counter return

    # 仅对公网 IPv6 地址启用代理
    ip6 daddr != 2000::/3 counter return

    # 重路由 OUTPUT 链流量到 PREROUTING 链
    meta l4proto $TPROXY_L4PROTO counter meta mark set $TPROXY_MARK
  }
}

以上注释很清晰,需要注意的是:首先此配置仅适用于系统中单独创建了 hysteria 用户来运行透明代理服务,所以才有下面的写法,不过我的代理服务也是用了专门的用户,但是这个貌似没起作用,采用了其他方法来避免环路。

-     # 通过匹配用户来避免环路
     meta skuid $HYSTERIA_USER counter return

systemd-networkd 实现策略路由

这部分的作用跟传统方法中那四条 ip 命令一样,只是因为传统方式的命令重启后会消失,我们需要想办法让它开机自动运行。既然用 systemd-networkd 管理网络,自然要研究它的方式:

在 /etc/systemd/network 新增一个.network的配置文件,名称随意:

$ cat 13-tproxy.network 
[Match]
Name = lo

[RoutingPolicyRule]
FirewallMark = 1
Table = 100
Priority = 100
Family=both   # 支持ipv4、ipv6和both

[Route]
Table = 100 
Destination=0.0.0.0/0  # ipv4
Type = local

[Route]
Table = 100 
Destination=::/0     # ipv6
Type = local

设置好后 systemctl restart systemd-networkd 或重启,按如下方式检验:

$ sudo ip rule list
0:	from all lookup local
100:	from all fwmark 0x1 lookup 100 proto static   # 这就是ip rule add fwmark 0x1 lookup 100
32766:	from all lookup main
32767:	from all lookup default

$ sudo ip -6 rule list
0:	from all lookup local
100:	from all fwmark 0x1 lookup 100 proto static   # ip -6 rule add fwmark 0x1 lookup 100
32766:	from all lookup main

k$ sudo ip -d r s t all |grep table\ 100
local default dev lo table 100 proto static scope host   # ip route add local default dev lo table 100

说明设置正确。 再来说我的避免环路的方法,贴一下我的 nftable Tproxy部分:

#! /sbin/nft -f

define TPROXY_MARK=0x1
# 运行代理程序的用户uid
define PROXY_USER=999
define TPROXY_PORT=2500
# 需要代理的协议类型
define TPROXY_L4PROTO={ tcp, udp }

# 需要绕过的地址
define BYPASS_IPV4={
  100.64.0.0/10,        # 运营商内部的NAT
  0.0.0.0/8,            # 当前网络(只在本机可用)
  10.0.0.0/8,           # A类
  127.0.0.0/8,          # 环回地址
  169.254.0.0/16,       # DHCP失败时,会自动分配此范围的IP
  172.16.0.0/12,        # B类
  192.168.0.0/16,       # C类
  224.0.0.0/4,          # 多播
  240.0.0.0/4,          # 保留为将来使用,很多设备不支持
  255.255.255.255/32    # 广播地址,最后这三行合并就是224.0.0.0/3, hysteria的配置
}
# define BYPASS_IPV6={ ::/128 }
define BYPASS_IPV6={ 
  FE80::/10,        # 链路本地地址
  FC00::/7,         # 唯一本地地址
  ::1/128,          # 未指定的地址(等同于IPv4的0.0.0.0)
  2001:db8::/32     # 用于文档和示例
}

table inet tproxy-lan {
  set bypass_ipv4 {
    type ipv4_addr
    flags interval
    auto-merge
    elements = { $BYPASS_IPV4 }
  }
  set bypass_ipv6 {
    type ipv6_addr
    flags interval
    elements = { $BYPASS_IPV6 }
  }
  chain prerouting {
    type filter hook prerouting priority mangle; policy accept;

    # 跳过已经由 TProxy 接管的流量
    meta l4proto $TPROXY_L4PROTO socket transparent 1  mark set $TPROXY_MARK comment "重定向到透明代理"
    socket transparent 0 counter return

    # 绕过私有和特殊 IP 地址
    ip daddr @bypass_ipv4 counter return
    ip6 daddr @bypass_ipv6 counter return

    # 仅对公网 IPv6 地址启用代理
    ip6 daddr != 2000::/3 counter return

    # 重定向流量到 TProxy 端口
    meta l4proto $TPROXY_L4PROTO tproxy to :$TPROXY_PORT meta mark set $TPROXY_MARK accept
  }

  # 代理本机流量
  chain output {
    type route hook output priority mangle; policy accept;

    # 绕过私有和特殊 IP 地址
    ip daddr @bypass_ipv4 counter return
    ip6 daddr @bypass_ipv6 counter return

    # 通过匹配用户来避免环路,这个不起作用……
    meta skuid $PROXY_USER counter return comment "代理本身流量不走代理"

    ip daddr 172.5.6.0/24 udp dport !=53 counter return comment "本地流量不走代理,dns请求除外"
    ip daddr 172.5.6.0/24 tcp dport !=53 counter return comment "本地流量不走代理,dns请求除外"
    

    # 仅对公网 IPv6 地址启用代理
    ip6 daddr != 2000::/3 counter return

    # 重路由 OUTPUT 链流量到 PREROUTING 链
    meta l4proto $TPROXY_L4PROTO counter meta mark set $TPROXY_MARK
  }
}