基于nhooyr.io/websocket的代理拦截yamux多路复用WebSocket数据时出现部分乱码,如何解析完整可读内容?
基于nhooyr.io/websocket的代理拦截yamux多路复用WebSocket数据时出现部分乱码,如何解析完整可读内容?
问题背景
我原本有一个直接连接WebSocket服务器的客户端:
WS_Server <--> WS_Client
现在需要一个正向代理来拦截客户端和服务器之间的数据,架构变成:
WS_Server <--> Proxy <--> WS_Client
我用nhooyr.io/websocket实现了一个简单的HTTP服务器作为代理:客户端通过dial连接代理,代理accept连接后再dial目标WS服务器,之后在两个连接之间转发数据。其中客户端到代理的连接未加密,代理到服务器的连接是TLS加密的。
目前代理能正常工作,也能拦截到数据(就是代码里的msg变量),但只有极少部分数据是可读的,大部分都是乱码。我用Wireshark抓包后,解包看到的内容和日志里的一致。另外需要说明的是,这个WebSocket连接之上还使用了yamux做多路复用。
我的代理代码
... import ( ... "nhooyr.io/websocket" ) func main() { http.HandleFunc("/rest/v1/client/proxy", handleWebSocketConnection) err := http.ListenAndServe(":8081", nil) if err != nil { log.Fatalf("Failed to start proxy server: %v", err) } } func handleWebSocketConnection(w http.ResponseWriter, r *http.Request) { // Accept WebSocket conn from Client connA, err := websocket.Accept(w, r, nil) if err != nil { log.Printf("Failed to accept WebSocket connection: %v", err) return } defer connA.Close(200, "Closing connectionA") // Connect to Dest Server caCert, err := os.ReadFile(caCertPath) caPool := x509.NewCertPool() if !caPool.AppendCertsFromPEM(caCert) { log.Fatalf("Failed to append CA certificate") } tlsConfig := &tls.Config{ RootCAs: caPool, } httpClient := &http.Client{ Transport: &http.Transport{ TLSClientConfig: tlsConfig, }, } // Dial to dest connB, _, err := websocket.Dial(r.Context(), DestWsAddr, &websocket.DialOptions{ HTTPClient: httpClient, }) if err != nil { log.Printf("Failed to connect to Dest: %v", err) return } defer connB.Close(200, "Closing connectionB") // Start goroutines to forward messages between client and agg go forwardMessages(connA, connB) go forwardMessages(connB, connA) // Keep the connection open indefinitely select {} } func forwardMessages(src, dst *websocket.Conn) { ctx := context.Background() for { // Read messages from the source connection msgType, msg, err := src.Read(ctx) log.Printf(" : [%v]", string(msg)) if err != nil { log.Printf("Failed to read message: %v", err) return } // Write the message to the destination connection err = dst.Write(ctx, msgType, msg) if err != nil { log.Printf("Failed to write message: %v", err) return } } }
日志片段
... ... 2024/11/13 06:13:56 : [HTTP/1.1 200 OK ] 2024/11/13 06:13:56 : [] 2024/11/13 06:13:56 : [Content-Length: 0 ] 2024/11/13 06:13:56 : [] 2024/11/13 06:13:56 : [ ] 2024/11/13 06:13:56 : [] 2024/11/13 06:13:56 : [��p� V�Cd^너+�Cn2A�/�RS�x�e8 �W��R����c;E�.�ى�+D2td>�K�,*'internalServer.server.abc.xyz.com �+3&$ ����,�-)��b�a(�%/�s�L����9] 2024/11/13 06:13:56 : [�] 0��M&�� �3 06:13:56 : [zv�-ee�j�N�=���&�m8T[���=��P �W��R����c;E�.�ى�+D2td>�K.+3$ ��{6� `�N���NAw��^X��y,R5`��i_���>K4�6Y��aɂ�w;vSѰ] �n��[���`���ab�+J��%�=���bu���O�p�iUSV�/_���9�U������[Z�~cn-�l�v��i>X���}U�I�ds�Z�v�?&�r������<�����Y�T��λ���T܌
问题分析与解决思路
别急,我来帮你梳理下问题所在:
1. 乱码的核心原因:yamux的二进制帧格式
你看到的可读内容应该是yamux建立连接时的握手HTTP报文,而后面的乱码是yamux的二进制帧数据。yamux是基于字节流的多路复用协议,它会把上层数据封装成自己的二进制帧格式(包含长度、类型、流ID等头部信息),这些帧不是纯文本,直接转成string自然会显示乱码。
2. 如何解析yamux数据?
要读取到可读的上层数据,你需要在代理中把yamux的帧解码出来,而不是直接转发原始的WebSocket消息。具体步骤如下:
- 在代理中接入yamux的会话:当代理和客户端、代理和服务器的WebSocket连接建立后,分别把这两个WebSocket连接转换成yamux的
Session。因为WebSocket本身是双向字节流,你可以用websocket.NetConn把websocket.Conn转换成标准的net.Conn,这样yamux就能直接使用。 - 监听yamux的流:yamux会话会创建多个子流,你需要在代理中监听这些子流,然后读取子流里的实际数据(这些数据才是你要的可读内容),同时还要负责转发子流的数据到另一端的yamux会话。
修改后的核心代码大概是这样:
func handleWebSocketConnection(w http.ResponseWriter, r *http.Request) { // 省略原有的连接建立代码... // 把WebSocket连接转换成net.Conn,供yamux使用 clientConn := websocket.NetConn(r.Context(), connA, websocket.MessageBinary) defer clientConn.Close() serverConn := websocket.NetConn(r.Context(), connB, websocket.MessageBinary) defer serverConn.Close() // 初始化yamux会话:客户端侧作为客户端会话,服务器侧作为服务器会话(或者根据实际情况调整) clientSession, err := yamux.Client(clientConn, nil) if err != nil { log.Printf("Failed to create client yamux session: %v", err) return } defer clientSession.Close() serverSession, err := yamux.Server(serverConn, nil) if err != nil { log.Printf("Failed to create server yamux session: %v", err) return } defer serverSession.Close() // 启动goroutine处理两边的yamux流 go handleYamuxSessions(clientSession, serverSession) go handleYamuxSessions(serverSession, clientSession) select {} } func handleYamuxSessions(srcSession, dstSession *yamux.Session) { for { // 接受来自源会话的子流 srcStream, err := srcSession.Accept() if err != nil { log.Printf("Failed to accept yamux stream: %v", err) return } // 创建对应的目标子流 dstStream, err := dstSession.Open() if err != nil { log.Printf("Failed to open yamux stream: %v", err) srcStream.Close() continue } // 转发两个流之间的数据,同时可以在这里拦截解析数据 go forwardStream(srcStream, dstStream) } } func forwardStream(src, dst io.ReadWriteCloser) { defer src.Close() defer dst.Close() // 这里可以用io.Copy同时转发,或者自己读取数据后再写入 buffer := make([]byte, 4096) for { n, err := src.Read(buffer) if err != nil { if err != io.EOF { log.Printf("Failed to read from stream: %v", err) } return } data := buffer[:n] // 这里可以解析data,因为这是yamux子流里的实际上层数据,如果是文本的话转string就可读了 log.Printf("Intercepted data: %s", string(data)) _, err = dst.Write(data) if err != nil { log.Printf("Failed to write to stream: %v", err) return } } }
3. 额外注意事项
- WebSocket消息类型:yamux使用的是二进制消息类型,所以在调用
websocket.NetConn时指定websocket.MessageBinary,确保数据传输的正确性。 - 流的生命周期管理:每个yamux子流都需要正确关闭,避免资源泄漏。
- 数据编码格式:即使解析了yamux流,如果上层数据本身是二进制格式(比如Protobuf),你还是需要对应的解码器来转换成可读内容,这时候可以根据业务情况添加对应的解码逻辑。
总结
你现在看到的乱码是yamux的原始帧数据,不是最终的业务数据。只要在代理中加入yamux的会话处理,解码出子流里的内容,就能得到可读的实际数据啦。
备注:内容来源于stack exchange,提问作者ray an
相关产品推荐
相关产品推荐

