Caddy2 的 ListenerWrapper
本文所有源码分析基于 Caddy2 v2.4.2 版本进行,未来版本可能源码会有变化,阅读本文时请自行将源码切换到 v2.4.2 版本。
一、这玩意是什么?
Caddy2 对配置文件中的 listener_wrappers
配置有以下描述:
Allows configuring listener wrappers, which can modify the behaviour of the base listener. They are applied in the given order.
同时对于 tls
这个 listener_wrappers
还做了一下说明:
There is a special no-op tls listener wrapper provided as a standard module which marks where TLS should be handled in the chain of listener wrappers. It should only be used if another listener wrapper must be placed in front of the TLS handshake.
综上所述,简单的理解就是 listener_wrappers
在 Caddy2 中用于改变链接行为,这个行为可以理解为我们可以自定义接管链接,这些 “接管” 更偏向于底层,比如在 TLS 握手之前做点事情或者在 TLS 握手之后做点事情,这样我们就可以实现一些魔法操作。
二、加载与初始化
在 Caddy2 启动时首先会进行配置文件解析,例如解析 Caddyfile、json 等格式的配置文件,listener_wrappers
在配置文件中被定义为一个 ServerOption
:
caddyconfig/httpcaddyfile/serveroptions.go:47
该配置最终会被注入到 Server 的 listenerWrappers 属性中(先解析为 ListenerWrappersRaw
然后再实例化)
modules/caddyhttp/server.go:132
最后在 App 的启动过程中遍历 listenerWrappers 并逐个应用,在应用 listenerWrappers
时有个比较重要的顺序处理:
首先 Caddy2 会尝试在 net.Listener
上应用一部分 listenerWrappers
,当触及到 tls
这个 token 的 listenerWrappers
之后终止应用;终止前已被应用的这部分 listenerWrappers
被认为是 TLS 握手之前的自定义处理,然后在 TLS 握手之后再次应用剩下的 listenerWrappers
,后面这部分被认为是 TLS 握手之后的自定义处理。
最终对 ListenerWrapper 加载流程分析如下:
- 首先解析配置文件,并将配置转化为 Server 的
ListenerWrappersRaw []json.RawMessage
- 然后通过
ctx.LoadModule(srv, "ListenerWrappersRaw")
实例化 ListenerWrapper - 在
ctx.LoadModule
时,如果发现了tls
指令则按照配置文件顺序排序 ListenerWrapper 切片,否则将tls
这个特殊的 ListenerWrapper 放在首位;这意味着在配置中不写tls
时,所有 ListenerWrapper 永远处于 TLS 握手之后 - 最后在 App 启动时按照切片顺序应用 ListenerWrapper,需要注意的是 ListenerWrapper 接口针对的是
net.Listener
的处理,其底层是net.Conn
;这意味着 ListenerWrapper 不会对 UDP(net.PacketConn
) 做处理,代码中也可以看到 ListenerWrapper 并未对 HTTP3 处理
三、具体实际应用
说了半天,也分析了源码,那么最终回到问题原点: ListenerWrapper 能干什么?答案就是自定义协议,例如神奇的 caddy-trojan 插件。
caddy-trojan 插件实现了 ListenerWrapper,在 App 启动时通过源码可以看到,TLS 握手完成后原始的 TCP 链接将交由这个 ListenerWrapper 处理:
1 |
|
该插件对 WrapListener
方法的实现如下:
1 |
|
所以这个 wrapper 核心处理在 loop()
中:
1 |
|
可以看到,当新链接进入时,首先对包头做检测 if ok := up.Validate(ByteSliceToString(b[:HeaderLen]))
;如果检测通过那么这个链接就完全插件自己处理后续逻辑了;如果不通过则将此链接返回给 Caddy2,让 Caddy2 继续处理。
这里面涉及到一个一开始让我不解的问题: “链接不可重复读”,后来看源码才明白作者处理方式很简单: 包装一个 rawConn
,在验证部分由于已经读了一点数据,如果验证不通过就把它存起来,然后让下一个读操作先读这个 buffer,从而实现原始数据组装。
1 |
|
四、思考和总结
ListenerWrapper 是 Caddy2 一个强大的扩展能力,在 ListenerWrapper 基础上我们可以实现对 TCP 链接自定义处理,我们因此可以创造一些奇奇怪怪的协议。同时我们通过让链接重新交由 Caddy2 处理又能做到完美的伪装: 当你去尝试访问时,如果密码学验证不通过,那么后续行为就与标准 Caddy2 表现一致,主动探测基本无效。对任何自己创造的 ListenerWrapper 来说,如果开启了类似 AEAD 这种加密,探测行为本身就会被转接到对抗密码学原理上。