menu

Passwall

代码以 4-26 为准:https://github.com/xiaorouji/openwrt-passwall/commit/794f980b5975bdba2b2c2e9cd8c9f668bb40b052

一个 tcp 请求的生命周期:

Domain -> dnsmasq(route#53)-不匹配任何名单-> 本地 dns -> ip <
                           |_> 匹配代理名单 -> tproxy -> 远程 dns -> ip -> 写入相应 ipset nameset <
                           |_> 匹配直连名单 -> 本地 dns -> ip -> 写入相应 ipset nameset <

ip -> iptables
        |_> 匹配代理 nameset -> rediect -> tproxy
        |_> 直接转发

名单

passwall 预配置有多个名单和支持自定义名单,以运行时 ipset 的名单为例(仅ipv4、相同的名单ipv6也有一份):

  • laniplist,写死的常见内网 ip 段
  • vpsiplist,节点 ip
  • shuntlist,分流列表
  • gfwlist
  • chnroute
  • blacklist
  • whitelist
  • blocklist

当中总体上根据行为可分为三种:

  1. 直连名单
  2. 代理名单
  3. 封锁名单

分别对应 Rule List Manager 页面的三个自定义列表,当然还包括未匹配名单的名单,称为 default。

  • 直连:laniplist、vpsiplist、whitelist(同用户自定义列表)
  • 代理:blacklist(同用户自定义列表)
  • 封锁:blocklist

剩下 gfwlist、chnroute、default,根据模式的不同走不同的行为。PSW 提供有五种模式。

  • noproxy:全部直连
  • global:全部代理
  • gfwlist:gfwlist 代理, chnroute、default 直连
  • chnroute(udp 显示为 Game Mode):gfwlist 、default 代理, chnroute 直连
  • returnhome:chnroute 代理,gfwlist 、default 直连

按名单项的类型又可分为

  1. ip
  2. 域名

ip 在 iptables.sh#add_firewall_rule 方法中直接导入 ipset 对应的集合中。 域名,需要通过 dns 查询后,根据域名所处的名单,将返回的 ip 加入相应的 upset nameset 中。这一步需要:本地的 dns 服务且该服务支持 ipset。另外写入结果需要在应当给下游之前发送。才能让接下来的请求匹配到 iptables 的规则。

DNS

dnsmasq 是路由默认的 dns 服务。也是 dhcp 下发的默认 dns 地址。默认情况下 dnsmasq 的上游是 wan 下发的 dns。

dnsmasq 支持:

  • 能根据配置将不同域名查询结果添加到对应的 ipset
  • 也能根据配置为不同域名选择不同的上游 ns 服务器。也就是域名分流

passwall 做的事就是为 dnsmasq 生成这一份配置。

也就是说其他支持这两个功能的 dns 服务都是可以替换 dnsmasq的,比如:

  • adguardhome
  • smartdns

域名分流的服务器也是对应名单的:

  1. 本地 ns(LOCAL_DNS),对应直连名单
  2. 远程 ns(TUN_DNS),对应代理名单

对于对应封锁名单的域名,则直接返回 0.0.0.0

有一个例外,如果是启用 chinang-dns 来代替 dnsmasq 作域名分流。那么 dnsmasq 会直接查询 dchinang-dns 的服务。

为什么要用 china-ng-dns 来代替 dnsmasq 做域名分流。因为dnsmasq 的域名匹配算法对大名单有 performance issue。

本地 NS

用来解直连名单的 dns,默认是 DEFAULT_DNS,是指在 /tmp/resolv.conf.d/resolv.conf.auto/tmp/resolv.conf.auto 配置的 dns,

若未配置则取 119.29.29.29

openwrt-passwall/app.sh at main · xiaorouji/openwrt-passwall

远程 NS

远程 NS 也是 passwall 的主要核心功能之一。支持多种配置

  • fake ip
  • pdnsd
  • dns2socks
  • xray dns
  • udp(requery dns by udp node)
  • custom dns
  • nonuse

除了 custom dns 直接 udp 查询, no filter 直接本地 ns,外。其他选项都是最终在本地(127.0.0.1#7913)启动一个 ns。

pdnsd

默认的选择是 pdnsd Requery DNS By TCP Node。pdnsd 的特点是可以在本地持久化对上游 dns 的查询结果。

这个选项以 TCP only 的方式向远程 DNS 发起请求。By TCP Node 并不是由 pdnsd 控制的,

如果是内置的两个 DNS 服务器 ip 都是存在代理名单里的。使用本机对这些 ip 发起的任何 TCP 请求,都会通过 TCP 节点代理。如果不是,通过 use_tcp_node_resolve_dns 开关,,iptables.sh#_proxy_tcp_access 为 iptables 添加一条规则。向目标服务器的目标端口发出的包都会重定向到 tcp 代理。

生成的 pdnsd 配置在:/var/etc/passwall/pdnsd

TODO ipv6

dns2socks

dns2socks 如名所示,通过 socks 隧道转发 DNS 请求。也就是需要一个 socks 协议的代理服务,只支持 socks 5。一般来说使用 dns2socks 需要顺便开启 psw 的 socks 服务。

搭配的远程 DNS 服务器必须支持 TCP 查询(未验证)。

udp node

与 custom 的区别就在于一个开关 use_udp_node_resolve_dns 控制。在 psw 开启了 udp 代理的情况下,iptables.sh#_proxy_udp_access 回为 iptables 添加一条规则。向目标服务器的目标端口发出的包都会重定向到 udp 代理。

注意内置的 Google DNS 、OpenDNS ip 已经在内置的代理名单里,所以只有有开 udp 代理,向这个两个 ns 的 udp 请求也是会走代理的。

xray doh

提供多个 doh,ip 是预解析的 ip。所以不需要 bootstrap——dns。代码中仍将这个 ip 命名为 bootstrap_dns。

自定义的格式参考下方内置的 doh 服务器格式。ip 是可以支持多个的(,分隔)。

o:value(“https://dns.adguard.com/dns-query,176.103.130.130”, “AdGuard”) o:value(“https://cloudflare-dns.com/dns-query,1.1.1.1”, “Cloudflare”) o:value(“https://security.cloudflare-dns.com/dns-query,1.1.1.2”, “Cloudflare-Security”) o:value(“https://doh.opendns.com/dns-query,208.67.222.222”, “OpenDNS”) o:value(“https://dns.google/dns-query,8.8.8.8”, “Google”) o:value(“https://doh.libredns.gr/dns-query,116.202.176.26”, “LibreDNS”) o:value(“https://doh.libredns.gr/ads,116.202.176.26”, “LibreDNS (No Ads)”) o:value(“https://dns.quad9.net/dns-query,9.9.9.9”, “Quad9-Recommended”)

TCP Node 模式这些 ip 会走 tcp node。

DNS.json

inbounds 是 dokodemo-door,发送到 127.0.0.1:7913 的 udp 流量,会转发到 8.8.8.8:53

{
      "port": 7913,
      "protocol": "dokodemo-door",
      "settings": {
        "port": 53,
        "network": "udp",
        "address": "8.8.8.8"
      },
      "tag": "dns-in",
      "listen": "127.0.0.1"
    }

routing 配置

 {
        "type": "field",
        "inboundTag": [
          "dns-in"
        ],
        "outboundTag": "dns-out"
      }

对应的 outbound 是 DNS

此出站协议会将 IP 查询(即 A 和 AAAA)转发给内置的 DNS 服务器。其它类型的查询流量将被转发至它们原本的目标地址。

dns 配置是走 google 的 doh,这里为 dns.google 预声明了 ip,所以 bootstrap dns。

"dns": {
    "servers": [
      "https:\/\/dns.google\/dns-query"
    ],
    "hosts": {
      "dns.google": "8.8.8.8"
    },
    "tag": "dns-in1"
  }

到 dns.google 的流量,根据路由的配置是走直连

 {
        "type": "field",
        "inboundTag": [
          "dns-in1"
        ],
        "outboundTag": "direct"
      }

注意 direct outbound 在 streamSettings 中设置了 mark:

"streamSettings": {
  "sockopt": {
    "mark": 255
  }
}

而 psw 对 mark 0xff 的处理就是直连。

RETURN     all  --  anywhere             anywhere             mark match 0xff

但是同时 8.8.8.8 是在 blacklist 里。

也就是到 8.8.8.8 的流量是要走代理的。最终会通过位于 NAT 表 PSW_OUTPUT 链的下的这条规则转发到代理。

REDIRECT	tcp	*	*	0.0.0.0/0	0.0.0.0/0	multiport dports 22,25,53,143,465,587,993,995,80,443 match-set blacklist dst redir ports 1041

那么 xray 的 doh 请求是会走直连还是TCP 代理呢?在运行时 iptables 第一条规则是在在第二条起码的。

RETURN     all  --  anywhere             anywhere             mark match 0xff
...
REDIRECT   tcp  --  anywhere             anywhere             multiport dports ssh,smtp,domain,imap2,ssmtp,submission,imaps,pop3s,www,https match-set blacklist dst redir ports 1041

看起来似乎是直连,是 BUG 吗?

实际是走 TCP 代理的,不过和这两条规则无关。还记得 _proxy_tcp_access 吗?它把另外一条规则添加在上面两条规则前面

REDIRECT   tcp  --  anywhere             dns.google           tcp dpt:https redir ports 1041

最后总结一下,DNS.json,就是将 127.0.0.1#7913 的 udp DNS 查询,转发到 8.8.8.8#53,其中的 A/AAAA 查询又转换成 DOH 流量,最终匹配 iptable 的规则,在TCP 代理服务器上实际执行这个 DNS 请求。

Fake Ip

请求一个网站的时候是需要两个请求的,一个域名解析,另外一个才是实际请求。

基于 TUN/TAP 或者 iptables 实现的透明代理对于客户端来说是不可知的。所以不可避免一个请求,需要两次代理请求。

基于 socket 的代理反而有优势,因为 Socks5 协议是支持将整个请求打包给代理服务器。在代理服务器做域名解析。

https://tools.ietf.org/html/rfc1928

Fake IP 的目的就是类似实现类似 socks5 的效果,将域名解析和请求合并为一个代理请求。

具体的原理是,域名解析的时候返回一个 fake ip,这个 ip 相当于 key,客户端向这个 ip 发送请求的时候。代理C端通过 ip 反查出域名。再将域名和请求打包发给代理 S 端处理。

实现这些技术,需要两个角色,特殊的 NS 服务。需要维护一个 hashset。

PSW 怎么做的? https://xtls.github.io/config/base/inbounds/#sniffingobject

PSW 将所有代理名单的域名解析为 11.1.1.1

在 iptables 添加规则

-A PSW_OUTPUT -d 11.1.1.1/32 -p tcp -j REDIRECT --to-ports 1041

所有目的为 11.1.1.1 的 tcp 链接重定向到代理客户端。

chnlist 模式下默认名单走代理怎么实现?

实际上,默认名单就是走代理了。PSW fake ip 的实现,不同于上面描述的 ip 而是利用了 v2ray 的 sniffing 功能。

  ...
  "sniffing": {
    "enabled": true,
    "destOverride": [
      "http",
      "tls"
    ]
  },
  ...

实际上 11.1.1.1 这个 ip 也是没有意义。只是起到标识流量的作用,让 iptables 转发到代理端口而已。流量到达代理客户端后,开启了 sniffing 客户端会嗅探流量,在应用层协议中找出原始域名。再用域名重置请求的目标地址,从而实现在代理服务器端做域名解析。这种实现法只支持 http(headers["Host"]),tls(sni)这两个有暴露域名的应用层协议。比如对 ssh 就无能为力了。

另外 sniffing 是无论有没有开启 fake ip 都存在配置文件的。也就是说其他 DNS 选项会执行两次域名解析、两次代理往返才能完成一个请求。

openwrt-passwall/gen_xray.lua at 794f980b5975bdba2b2c2e9cd8c9f668bb40b052 · xiaorouji/openwrt-passwall

还有 xray/v2ray 已经支持正式的 fake ip 了:FakeDNS :: Project X

pws 这一块还是有优化空间的。

不支持与chinadns-ng 共存

https://xtls.github.io/config/base/fakedns/ https://blog.skk.moe/post/what-happend-to-dns-in-proxy/ https://bleepcoder.com/clash/443041625/fake-ip-mo-shi-zuo-wei-wang-guan-dai-li-de-ji-ge-wen-ti

chinadns-ng

zfl9/chinadns-ng: chinadns next generation, refactoring with epoll and ipset

chinadns-ng 是替换 dnsmasq 的域名分流功能。相对 dnsmasq 来说 chinadns-ng

  1. 大名单有更明显性能优势,匹配的实现算法不同。TODO
  2. 非列表的域名查询逻辑有差异

chinadns-ng 的逻辑更丰富一些:

  • 如果启用了黑名单(gfwlist)且查询的域名命中了黑名单,则将该请求转发给可信 DNS。
  • 如果启用了白名单(chnlist)且查询的域名命中了白名单,则将该请求转发给国内 DNS。
  • 如果未启用黑名单、白名单,或未命中黑名单、白名单,则将请求转发给所有上游 DNS。

  • 如果关联的查询是命中了黑白名单的,则直接将其转发给请求客户端,并释放相关上下文。
  • 如果关联的查询是未命中黑白名单的,则检查国内 DNS 返回的是否为国内 IP(即是否命中 chnroute/chnroute6);如果是,则接收此响应,将其转发给请求客户端,并释放相关上下文;如果不是,则丢弃此响应,然后采用可信 DNS 的解析结果。如果可信 DNS 有一定概率会比国内 DNS 先返回的话,请务必启用”公平模式”(默认是”抢答模式”),也即指定选项 -f/–fair-mode。但也不是说无论何时都要启用公平模式,如果国内 DNS 绝大多数情况下都比可信 DNS 先返回的话,是不需要启用公平模式的,当然你启用公平模式也不会有任何问题以及性能损失。其实按理来说抢答模式是可以丢弃的,但考虑到一些特殊情况,还是打算留着抢答模式。

dnsmasq 是使用遍历链表来匹配域名,性能较差(2.72 版本)。

chinadns-ng 用的是哈希表。

iptables

Nat 表 , Prerouting 链创建一条子链 PSW:

Pkts.	Traffic	Target	Prot.	In	Out	Source	Destination	Options	Comment
16.95 K	1.08 MB	RETURN	all	*	*	0.0.0.0/0	0.0.0.0/0	match-set laniplist dst	-
0 	0 B	RETURN	all	*	*	0.0.0.0/0	0.0.0.0/0	match-set vpsiplist dst	-
474 	33.11 KB	RETURN	all	*	*	0.0.0.0/0	0.0.0.0/0	match-set whitelist dst	-
0 	0 B	REDIRECT	tcp	*	*	0.0.0.0/0	0.0.0.0/0	multiport dports 22,25,53,143,465,587,993,995,80,443 match-set shuntlist dst redir ports 1041	'默认'
427 	26.02 KB	REDIRECT	tcp	*	*	0.0.0.0/0	0.0.0.0/0	multiport dports 22,25,53,143,465,587,993,995,80,443 match-set blacklist dst redir ports 1041	'默认'
18.34 K	1.11 MB	REDIRECT	tcp	*	*	0.0.0.0/0	0.0.0.0/0	multiport dports 22,25,53,143,465,587,993,995,80,443 ! match-set chnroute dst redir ports 1041	'默认'
4.93 K	344.18 KB	RETURN	tcp	*	*	0.0.0.0/0	0.0.0.0/0	-	'默认'

TUN/TAP

Redirect 与 TProxy 的区别

  • REDIRECT 只在 nat PREROUTING、OUTPUT 有效。
  • TPROXY 自在 mangle PREROUTING 有效

REDIRECT 相当于目标地址固定为 localhost 的 DNAT。REDIRECT 不能通过 getsockname 获取原始链接的目标地址。不过 TCP 链接仍然能通过 SO_ORIGINAL_DST 获取到原始目标地址。这也就是 REDIRECT 只支持 TCP 透明代理的原因。

DNAT 与 REDIRECT 是可以获取到真实的源地址(实际发起请求的客户端的地址)。

REDIRECT 的目标地址已经被改为 local,所有是不需要重新路由的,也没必要再打标签。

ip rule add fwmark 1 lookup 100 ip route add local 0.0.0.0/0 dev lo table 100

TPROXY 不会修改传输层 Header。

Original destination was: 192.168.5.32:5301                                                       Connected by <socket.socket fd=4, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0,laddr=('192.168.50.32', 10025), raddr=('192.168.50.211', 50312)>
o

何谓透明

透明意味着不可见,透明代理中这里的主语指的是链接的发起端或接收端。客户端向真实的服务端ip发起请求,服务端看到的也是真实的ip。中间链路的作妖不可见。FIXME

operating system functions as a router, but some (or all) traffic gets redirected for userspace processing.

It is entirely possible to tell Linux 0.0.0.0/0 (‘everything’) is local, but this would leave it unable to connect to any remote address.

ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100

让所有 ip 都经过本地回环,这样才能被本地监听 0.0.0.0:PORT 的应用程序收到包。

tcp        0      0 :::1041                 :::*                    LISTEN      2654/xray

IP_TRANSPARENT

https://man7.org/linux/man-pages/man7/ip.7.html

  1. Binding to addresses that are not (usually) considered local
  2. Receiving connections and packets from iptables TPROXY redirected sessions

因为不论是 TCP 还是 UDP,编程模型都需要 bind 本机地址(或者 0.0.0.0),不是给本机的包,进程不收。但 2.6.24 的内核出了个 IP_TRANSPARENT 的 socket 选项,打开这个选项,就可以接收任意目的地址的包了。

-m socket

文档所说的:

It matches if there is an established or non-zero bound listening socket (possibly with a non-local address).

经验证 tcp 第一个包 syn 是不会触发 -m socket 后续的包就会触发。也就是说后续的包直接走 DIVERT 不会触发 TPROXY 规则了,不过仍然能被正确转发到目标透明代理的进程,因为“链接”已经被建立了?反正绕过了后续的规则最终起到提高效率的作用。面对无链接的 udp 貌似就不起作用了。

本地流量如何走透明代理

TPROXY 不能用于 OUTPUT 链,不可以通过给 OUTPUT 链给流量加标识。

iptables -t mangle -A OUTPUT -p udp -j MARK --set-mark 1
iptables -t mangle -A OUTPUT -p tcp -j MARK --set-mark 1

猜测是根据之前配置的路由规则,流量会被路由至 lo,这时就会走 PREROUTING 链。所以也能应用 TPROXY,经常测试应用 TPROXY 后就不会经过 OUTPUT 链了,所以也就不会有死循环的问题。

测试

其他

haproxy 负载均衡

passwall 生成的负载均衡配置文件:

global
    log         127.0.0.1 local2
    chroot      /usr/bin
    maxconn     60000
    stats socket  /var/etc/passwall/haproxy/haproxy.sock
    user        root
    daemon

defaults
    mode                    tcp
    log                     global
    option                  tcplog
    option                  dontlognull
    option http-server-close
    #option forwardfor       except 127.0.0.0/8
    option                  redispatch
    retries                 2
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 3000

listen 1181
    mode tcp
    bind 0.0.0.0:1181
    server g1.dourok.info:443 g1.dourok.info:443 weight 5 check inter 1500 rise 1 fall 3
    server g2.dourok.info:443 g2.dourok.info:443 weight 5 check inter 1500 rise 1 fall 3
listen console
    bind 0.0.0.0:1188
    mode http
    stats refresh 30s
    stats uri /
    stats admin if TRUE
    stats auth

我以为 backend 是本地的 tcp 端口,比如多个本地 xray 服务监听不同短空,一个 haproxy frontend。 结果是 backend 是 xray 服务端的地址,与域名无关的可以用这种方式(tcp mode),与域名有关的,比如 xless 估计用不了,另外 xray 配置文件也没有涉及 haproxy 对应的端口,不清楚这样做的目的是什么。

Rule list

/usr/bin/lua/luci/model/cbi/passwall/client/rule_list.lua

重点在这个函数

function m.on_apply(self)
luci.sys.call("/etc/init.d/passwall reload > /dev/null 2>&1 &")
end

reload 未实现,所以每次保存列表都会重启服务

报错

Too many open files

ulimit:

Hard limits:

sysctl -Hn 8192

Soft limits:

ulimit -Sn 2048

sysctl

sysctl -w fs.file-max=xxxxx

init 脚本

procd_set_param limits nofile="65535 65535"
keyboard_arrow_up