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 握手之后的自定义处理。

modules/caddyhttp/app.go:318

最终对 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 处理:

// finish wrapping listener where we left off before TLS
for i := lnWrapperIdx; i < len(srv.listenerWrappers); i++ {
	ln = srv.listenerWrappers[i].WrapListener(ln)
}

该插件对 WrapListener 方法的实现如下:

// WrapListener implements caddy.ListenWrapper
func (m *ListenerWrapper) WrapListener(l net.Listener) net.Listener {
	ln := NewListener(l, m.upstream, m.logger)
	// 异步后台捕获新链接
	go ln.loop()
	return ln
}

所以这个 wrapper 核心处理在 loop() 中:

// loop is ...
func (l *Listener) loop() {
	for {
		conn, err := l.Listener.Accept()
		if err != nil {
			select {
			case <-l.closed:
				return
			default:
				l.logger.Error(fmt.Sprintf("accept net.Conn error: %v", err))
			}
			continue
		}

		go func(c net.Conn, lg *zap.Logger, up *Upstream) {
			b := make([]byte, HeaderLen+2)
			if _, err := io.ReadFull(c, b); err != nil {
				if errors.Is(err, io.EOF) {
					lg.Error(fmt.Sprintf("read prefix error: read tcp %v -> %v: read: %v", c.RemoteAddr(), c.LocalAddr(), err))
				} else {
					lg.Error(fmt.Sprintf("read prefix error: %v", err))
				}
				c.Close()
				return
			}

			// check the net.Conn
			if ok := up.Validate(ByteSliceToString(b[:HeaderLen])); !ok {
				select {
				case <-l.closed:
					c.Close()
				default:
					l.conns <- &rawConn{Conn: c, r: bytes.NewReader(b)}
				}
				return
			}
			defer c.Close()
			lg.Info(fmt.Sprintf("handle trojan net.Conn from %v", c.RemoteAddr()))

			nr, nw, err := Handle(io.Reader(c), io.Writer(c))
			if err != nil {
				lg.Error(fmt.Sprintf("handle net.Conn error: %v", err))
			}
			up.Consume(ByteSliceToString(b[:HeaderLen]), nr, nw)
		}(conn, l.logger, l.upstream)
	}
}

可以看到,当新链接进入时,首先对包头做检测 if ok := up.Validate(ByteSliceToString(b[:HeaderLen]));如果检测通过那么这个链接就完全插件自己处理后续逻辑了;如果不通过则将此链接返回给 Caddy2,让 Caddy2 继续处理。

这里面涉及到一个一开始让我不解的问题: “链接不可重复读”,后来看源码才明白作者处理方式很简单: 包装一个 rawConn,在验证部分由于已经读了一点数据,如果验证不通过就把它存起来,然后让下一个读操作先读这个 buffer,从而实现原始数据组装。

// rawConn is ...
type rawConn struct {
	net.Conn
	r *bytes.Reader
}

// Read is ...
func (c *rawConn) Read(b []byte) (int, error) {
	if c.r == nil {
		return c.Conn.Read(b)
	}
	n, err := c.r.Read(b)
	if errors.Is(err, io.EOF) {
		c.r = nil
		return n, nil
	}
	return n, err
}

四、思考和总结

ListenerWrapper 是 Caddy2 一个强大的扩展能力,在 ListenerWrapper 基础上我们可以实现对 TCP 链接自定义处理,我们因此可以创造一些奇奇怪怪的协议。同时我们通过让链接重新交由 Caddy2 处理又能做到完美的伪装: 当你去尝试访问时,如果密码学验证不通过,那么后续行为就与标准 Caddy2 表现一致,主动探测基本无效。对任何自己创造的 ListenerWrapper 来说,如果开启了类似 AEAD 这种加密,探测行为本身就会被转接到对抗密码学原理上。


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 国际许可协议进行许可,转载请注明出处。