Golang实现HTTP/HTTPS代理时遭遇SSL_ERROR_BAD_MAC_READ错误求助
HTTPS代理出现SSL_ERROR_BAD_MAC_READ错误的解决方法
我参考教程用Golang实现了HTTP/HTTPS代理,核心逻辑为:接收浏览器发送的指令,若为CONNECT请求则建立TCP套接字,让浏览器与目标服务器直接通信并双向转发数据;若为GET请求则处理HTTP转发。目前HTTP请求可正常工作,但在Firefox中访问HTTPS网站时,出现以下错误:
Secure Connection Failed
An error occurred during a connection to www.google.com. SSL received a record with an incorrect Message Authentication Code.
Error code: SSL_ERROR_BAD_MAC_READ
问题代码
package main import ( "bufio" "fmt" "net" "strings" ) func main() { fmt.Println("Start server...") ln, _ := net.Listen("tcp", ":8000") conn, _ := ln.Accept() handleSocket(conn) } func handleSocket(client_to_proxy net.Conn) { message, e := bufio.NewReader(client_to_proxy).ReadString('\n') message = strings.ReplaceAll(message, "\r\n", "") if e != nil { fmt.Println("ERROR1 ", e) return } splited := strings.Split(message, " ") host := strings.Split(splited[1], ":") if splited[0] == "CONNECT" { proxy_to_server, e := net.Dial("tcp", splited[1]) if e != nil { fmt.Println("ERROR2 ", e) return } lenn, e := client_to_proxy.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) if e != nil { fmt.Println("ERROR8 ", e) return } fmt.Println(lenn) readAll(client_to_proxy, proxy_to_server) } else if splited[0] == "GET" { remote_conn, e := net.Dial("tcp", strings.Replace(splited[1][:len(splited[1])-1], "http://", "", 2)+":80") if e != nil { fmt.Println("ERROR7 ", e) return } _, e = remote_conn.Write([]byte("GET / " + splited[2] + "\r\n" + "Host: " + host[0] + "\r\n\r\n")) if e != nil { fmt.Println("ERROR6 ", e) return } writeAll(client_to_proxy, remote_conn) } } func writeAll(client_to_proxy net.Conn, proxy_to_server net.Conn) { buffer := make([]byte, 32*1024) for { readLeng, err := proxy_to_server.Read(buffer) if err != nil { fmt.Println("ERROR9 ", err) return } if readLeng > 0 { _, err := client_to_proxy.Write(buffer) if err != nil { fmt.Println("ERR4 ", err) return } } } } func readAll(client_to_proxy net.Conn, proxy_to_server net.Conn) { buffer := make([]byte, 32*1024) go writeAll(client_to_proxy, proxy_to_server) for { read, err := client_to_proxy.Read(buffer) if err != nil { return } if read > 0 { _, err := proxy_to_server.Write(buffer) if err != nil { fmt.Println("ERR5 ", err) return } } } }
问题分析
- 转发数据长度错误:在
writeAll和readAll函数中,读取到数据后直接写入整个buffer,而非实际读取的字节长度。SSL是二进制协议,多余的未初始化字节会被当作有效数据转发,破坏SSL记录的MAC校验,导致SSL_ERROR_BAD_MAC_READ。 - 仅处理单个连接:main函数中只调用一次
ln.Accept(),处理完第一个连接后程序就会退出,无法处理后续请求。 - 未读完CONNECT请求的完整头部:浏览器发送CONNECT请求时会附带完整的HTTP头部(直到
\r\n\r\n),当前代码只读取了第一行,剩余头部数据会留在缓冲区中,后续被当作SSL数据转发,干扰正常加密通信。
修复后的代码
package main import ( "bufio" "fmt" "io" "net" "strings" ) func main() { fmt.Println("Start server...") ln, err := net.Listen("tcp", ":8000") if err != nil { fmt.Println("Listen error:", err) return } defer ln.Close() // 循环接受连接,用goroutine处理每个请求 for { conn, err := ln.Accept() if err != nil { fmt.Println("Accept error:", err) continue } go handleSocket(conn) } } func handleSocket(clientToProxy net.Conn) { defer clientToProxy.Close() reader := bufio.NewReader(clientToProxy) // 读取请求行 requestLine, err := reader.ReadString('\n') if err != nil { fmt.Println("Read request line error:", err) return } requestLine = strings.TrimSpace(requestLine) splited := strings.Split(requestLine, " ") if len(splited) < 3 { fmt.Println("Invalid request line:", requestLine) return } if splited[0] == "CONNECT" { // 读完剩余的HTTP头部,直到\r\n\r\n for { line, err := reader.ReadString('\n') if err != nil { fmt.Println("Read header error:", err) return } if strings.TrimSpace(line) == "" { break } } // 建立到目标服务器的连接 proxyToServer, err := net.Dial("tcp", splited[1]) if err != nil { fmt.Println("Dial to server error:", err) // 返回503错误 clientToProxy.Write([]byte("HTTP/1.1 503 Service Unavailable\r\n\r\n")) return } defer proxyToServer.Close() // 回复浏览器连接建立成功 _, err = clientToProxy.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")) if err != nil { fmt.Println("Write 200 OK error:", err) return } // 双向转发数据 go io.Copy(proxyToServer, clientToProxy) io.Copy(clientToProxy, proxyToServer) } else if splited[0] == "GET" { // 处理HTTP GET请求 hostPort := strings.TrimPrefix(splited[1], "http://") // 去掉路径部分,只保留主机和端口 hostPort = strings.Split(hostPort, "/")[0] if !strings.Contains(hostPort, ":") { hostPort += ":80" } remoteConn, err := net.Dial("tcp", hostPort) if err != nil { fmt.Println("Dial to HTTP server error:", err) clientToProxy.Write([]byte("HTTP/1.1 503 Service Unavailable\r\n\r\n")) return } defer remoteConn.Close() // 构造转发的GET请求 forwardRequest := fmt.Sprintf("GET %s %s\r\nHost: %s\r\n\r\n", splited[1], splited[2], strings.Split(hostPort, ":")[0]) _, err = remoteConn.Write([]byte(forwardRequest)) if err != nil { fmt.Println("Write to HTTP server error:", err) return } // 转发响应 io.Copy(clientToProxy, remoteConn) } }
修复说明
- 修正数据转发长度:直接使用标准库
io.Copy函数处理双向转发,它会自动处理读取和写入的字节长度,避免手动buffer操作的错误。 - 循环处理连接:main函数中通过
for循环持续接受新连接,并使用goroutine异步处理每个连接,支持多客户端请求。 - 完整读取CONNECT请求头部:在处理CONNECT请求时,读取所有头部直到
\r\n\r\n,确保缓冲区中没有残留的HTTP头部数据,避免干扰后续的SSL通信。 - 完善错误处理:添加了更多错误场景的处理,比如无效请求、连接失败时返回合适的HTTP错误码。
内容的提问来源于stack exchange,提问作者user13954958
相关产品推荐
相关产品推荐

