
用Caddy搓个MC监控
最近在维护自己的 Minecraft 服务器,遇到个挺真实的需求 因为服务器是放在宿舍局域网里通过 FRP 穿透出去的,平时想知道有没有朋友上线,只能登到服务器上,我又不想用别人改过的服务端 于是写了一个 Caddy 的 Layer 4 扩展插件 mc_monitor。它能在不对 MC 服务端做任何改动的前提下,在网关层直接“嗅探”出登录的玩家名称,并异步推送到 Telegram 或 Discord。 这个插件工作在 OSI 模型的第四层(传输层)。 它的核心逻辑是在 TCP 连接建立时,利用 io.TeeReader 这种“三通”技术,在不消耗原始数据流的情况下,把玩家发来的握手包(Handshake)和登录包(Login Start)复制一份出来。 通过解析这些二进制包提取出玩家 ID(Username),然后再把流量原封不动地转发给后端的 MC 服务器。最后计算玩家从连接到断开的时长,生成报告。 当然,我这里用到了之前之前大佬写的关于mc服务端相关的库 核心代码实现 这里贴一下这个 Caddy 模块的核心逻辑代码,主要是 Handle 和 parseMCLogin 方法,它们负责了主要的流量劫持和协议解析工作: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 // Handle 处理连接的核心逻辑 // 这里我们利用 io.TeeReader 在建立连接初期“偷听”流量,提取用户名 func (m *MCMonitor) Handle(cx *layer4.Connection, next layer4.Handler) error { startTime := time.Now() // 1. 创建一个 Buffer 用来暂存读取的数据 replayBuf := new(bytes.Buffer) // 2. 创建 TeeReader:从 cx 读取的数据会同时写入 replayBuf teeReader := io.TeeReader(cx, replayBuf) // 设置读取超时,防止非 MC 流量导致卡死 cx.SetReadDeadline(time.Now().Add(3 * time.Second)) // 3. 解析登录包,提取用户名 (同时数据被 TeeReader 存入 replayBuf) username, err := m.parseMCLogin(teeReader) cx.SetReadDeadline(time.Time{}) // 重置超时 // ... (日志记录逻辑) // 4. 连接真实的后端 MC 服务器 remote, err := net.Dial("tcp", m.BackendAddress) if err != nil { return err } defer remote.Close() // 5. 关键步骤:先把刚才“偷听”并存在 replayBuf 里的握手包发给后端 // 否则后端会因为缺少握手包而拒绝连接 if _, err := io.Copy(remote, replayBuf); err != nil { return err } // 6. 发送上线通知 if username != "Unknown" { if m.WebhookUrl != "" { go m.sendWebhook(username, fmt.Sprintf("Player %s connected", username)) } } // 7. 开启双向管道转发后续流量 errc := make(chan error, 1) go func() { _, err := io.Copy(cx, remote) errc <- err }() go func() { _, err := io.Copy(remote, cx) errc <- err }() <-errc // 阻塞等待连接断开 // 8. 计算时长并发送下线通知 duration := time.Since(startTime) if username != "Unknown" && m.WebhookUrl != "" { go m.sendWebhook(username, fmt.Sprintf("Player %s disconnected. Duration: %s", username, duration)) } return nil } // parseMCLogin 极其简化的协议解析 // 依次读取 Handshake 包和 Login Start 包 func (m *MCMonitor) parseMCLogin(r io.Reader) (string, error) { // 使用 go-mc 库帮助解包,避免手动处理 VarInt var p packet.Packet // --- 第一步:读取 Handshake 包 --- if err := p.UnPack(r, -1); err != nil { return "", err } // Handshake 包结构: Protocol Version | Server Address | Server Port | Next State var ( protocolVersion packet.VarInt serverAddr packet.String serverPort packet.UnsignedShort nextState packet.VarInt ) if err := p.Scan(&protocolVersion, &serverAddr, &serverPort, &nextState); err != nil { return "", err } if nextState != 2 { // 2 代表登录状态 return "", fmt.Errorf("not a login handshake") } // --- 第二步:读取 Login Start 包 --- // 紧接着发来的就是包含了用户名的包 if err := p.UnPack(r, -1); err != nil { return "", err } var username packet.String // Login Start 包结构: Username | (Optional UUID) err := p.Scan(&username) return string(username), err } Caddyfile 配置 插件的使用非常简单,直接在 Caddyfile 的 layer4 块里定义即可。 ...



