You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go语言MySQL代理服务器无法在转发前修改请求求助

解决MySQL代理中修改请求的问题

要替换io.Copy实现请求修改,核心是按MySQL协议逐包解析、修改后转发,而不是直接复制字节流。因为MySQL是基于数据包的协议,每个请求/响应都有固定格式(前3字节为长度,第4字节为序列号),必须先识别包边界、解析内容后再处理。

针对你需要代理自动添加MySQL认证凭证的需求,具体修改方案如下:

关键修改逻辑

在proxy包的Connection.Handle方法中,替换原有的io.Copy逻辑,改为双向数据包处理循环:

  • 客户端→服务器方向:拦截客户端发送的认证请求包,替换其中的用户名、密码为代理预设的凭证,再转发给MySQL服务器;其他类型数据包直接转发。
  • 服务器→客户端方向:直接转发服务器响应包给客户端,无需修改。

修改后的代码

以下是更新后的Connection.Handle方法:

func (r *Connection) Handle() error {
    address := fmt.Sprintf("%s%s", r.host, r.port)
    mysqlConn, err := net.Dial("tcp", address)
    if err != nil {
        log.Printf("Failed to connect to MySQL: [%d] %s", r.id, err.Error())
        return err
    }
    defer mysqlConn.Close()
    defer r.conn.Close()

    // 转发服务器初始握手包给客户端
    handshakePacket := &protocol.InitialHandshakePacket{}
    err = handshakePacket.Decode(mysqlConn)
    if err != nil {
        log.Printf("Failed to decode handshake initial packet: [%d] %s", r.id, err.Error())
        return err
    }

    handshakeBytes, err := handshakePacket.Encode()
    if err != nil {
        log.Printf("Failed to encode handshake packet: [%d] %s", r.id, err.Error())
        return err
    }
    _, err = r.conn.Write(handshakeBytes)
    if err != nil {
        log.Printf("Failed to send handshake to client: [%d] %s", r.id, err.Error())
        return err
    }

    // 启动双向数据处理协程
    errChan := make(chan error, 2)

    // 客户端→代理→MySQL服务器
    go func() {
        seq := uint8(1) // MySQL包序列号,握手包为0,后续从1开始
        for {
            // 读取客户端完整数据包
            pkg, err := protocol.ReadPacket(r.conn, seq)
            if err != nil {
                errChan <- err
                return
            }

            switch pkg[0] {
            case 0x01: // 识别认证请求包
                // 解析原始认证包
                authPkg := &protocol.AuthPacket{}
                if err := authPkg.Decode(pkg); err != nil {
                    errChan <- fmt.Errorf("decode auth packet failed: [%d] %w", r.id, err)
                    return
                }

                // 替换为代理预设的MySQL凭证
                authPkg.User = "proxy_user" // 你的MySQL用户名
                // 若使用mysql_native_password,需用握手包的salt计算密码哈希
                authPkg.Passwd = protocol.CalcPassword(handshakePacket.ScrambleBytes, []byte("proxy_pass"))

                // 重新编码修改后的认证包
                modifiedPkg, err := authPkg.Encode()
                if err != nil {
                    errChan <- fmt.Errorf("encode modified auth packet failed: [%d] %w", r.id, err)
                    return
                }

                // 发送修改后的包到MySQL服务器
                if _, err := mysqlConn.Write(modifiedPkg); err != nil {
                    errChan <- fmt.Errorf("send modified auth packet failed: [%d] %w", r.id, err)
                    return
                }
            default:
                // 其他类型数据包直接转发
                if _, err := mysqlConn.Write(pkg); err != nil {
                    errChan <- fmt.Errorf("forward packet to mysql failed: [%d] %w", r.id, err)
                    return
                }
            }

            seq++
        }
    }()

    // MySQL服务器→代理→客户端
    go func() {
        seq := uint8(1)
        for {
            pkg, err := protocol.ReadPacket(mysqlConn, seq)
            if err != nil {
                errChan <- err
                return
            }

            // 直接转发响应包给客户端
            if _, err := r.conn.Write(pkg); err != nil {
                errChan <- fmt.Errorf("forward packet to client failed: [%d] %w", r.id, err)
                return
            }

            seq++
        }
    }()

    // 等待任一方向的连接关闭或错误
    err = <-errChan
    log.Printf("Connection closed: [%d], error: %v", r.id, err)
    return err
}

补充实现

需要在protocol包中添加ReadPacket函数,用于读取完整的MySQL数据包:

// protocol/read_packet.go
package protocol

import (
    "io"
    "net"
)

func ReadPacket(conn net.Conn, expectedSeq uint8) ([]byte, error) {
    // 读取4字节包头部(3字节长度+1字节序列号)
    header := make([]byte, 4)
    if _, err := io.ReadFull(conn, header); err != nil {
        return nil, err
    }

    pkgLen := int(uint32(header[0]) | uint32(header[1])<<8 | uint32(header[2])<<16)
    seq := header[3]
    if seq != expectedSeq {
        return nil, io.ErrUnexpectedEOF
    }

    // 读取包内容
    pkgContent := make([]byte, pkgLen)
    if _, err := io.ReadFull(conn, pkgContent); err != nil {
        return nil, err
    }

    // 返回完整数据包(头部+内容)
    return append(header, pkgContent...), nil
}

关键说明

  1. 密码哈希处理:如果MySQL使用mysql_native_password认证方式,不能直接设置明文密码,必须用握手包中的ScrambleBytes对密码进行哈希计算,protocol.CalcPassword是MySQL协议标准的哈希实现。
  2. 协议包解析:确保你的protocol包已正确实现AuthPacket的Decode和Encode方法,能解析和生成MySQL认证数据包。
  3. 为什么不能用io.Copy:io.Copy是字节流直接复制,无法识别MySQL的数据包边界,更无法解析包内的认证信息,只有按协议逐包处理才能实现请求修改。

内容的提问来源于stack exchange,提问作者Prashant Pratap Singh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.21 05:27:22