🐛 fix(oceanbase): 修复预探测漏判导致 Oracle 路径走 TNS 死路

- 预探测放宽 payload 上限 1024→65536 字节并删除 protocol_version 严格检查,避免 OB 4.x 携带能力位扩展的 handshake 被误判为非 MySQL wire
- Connect Oracle 路由:预探测失败时不再单走 TNS(在 OB MySQL wire 端口上必然失败),改为串行尝试 OBClient capability 路径 → TNS 路径,两路都失败时合并错误信息
- 移除 annotateOceanBaseOracleConnectError 里"GoNavi 暂未实现 OBClient 协议"过时文案,改为说明 OBClient 路径已实现且路由层会优先尝试
- 删除随 probe 放宽而失去意义的 IgnoresNonMySQLProtocol 测试,新增 AcceptsLargerPayload 锁定 64KB 上限内识别 OB 的能力
- 刷新 oceanbase driver-agent revision
This commit is contained in:
Syngnat
2026-05-15 15:18:22 +08:00
parent 067cbd5ab2
commit 8b90c0b3f0
3 changed files with 55 additions and 30 deletions

View File

@@ -5,7 +5,7 @@ package db
func init() {
optionalDriverAgentRevisions = map[string]string{
"mariadb": "src-4e1ec648c70c87ea",
"oceanbase": "src-f08c1fb112767bbf",
"oceanbase": "src-8e445fc4899d850f",
"diros": "src-74927b3809258666",
"sphinx": "src-269bd60a34df47d3",
"sqlserver": "src-84553484c72e7253",

View File

@@ -458,7 +458,7 @@ func annotateOceanBaseOracleConnectError(err error) error {
strings.Contains(lower, "unexpected packet"),
strings.Contains(lower, "got packets out of order"),
strings.Contains(lower, "use of closed network connection"):
return fmt.Errorf("%wOceanBase Oracle 协议握手失败:当前端口可能是 OBServer 的 MySQL 协议端口OBClient 协议而非 OBProxy 的 Oracle 协议端口;GoNavi 暂未实现 OBClient 协议,请将连接端口改为 OBProxy 暴露的 Oracle 协议端口", err)
return fmt.Errorf("%wOceanBase Oracle TNS 路径握手失败:当前端口可能是 OBServer 的 MySQL wire 协议端口而非 OBProxy 的 Oracle listener。GoNavi 实现 OBClient capability 注入路径,路由层会优先尝试该路径;如这里仍报此错说明 OBClient 路径也未成功,详见随后的 OBClient 错误诊断", err)
case strings.Contains(lower, "ora-"):
return fmt.Errorf("%wOceanBase Oracle 租户认证或服务名失败请确认服务名Service Name、用户名如 SYS@oracle_tenant#cluster_name与权限配置", err)
}
@@ -468,18 +468,20 @@ func annotateOceanBaseOracleConnectError(err error) error {
// probeOceanBaseMySQLWireHandshake 通过读取目标端口的 MySQL initial handshake packet
// 判断该端口背后是否是 OceanBase 的 MySQL wire 协议端口。
//
// 在 Oracle 路径连接前主动探测,是为了避免用户在 mysql wire 协议OB Error 1235
// Oracle TNS 协议use of closed network connection之间反复方向摇摆。
//
// 探测过程:
// 1. TCP 建连(带 timeout
// 2. 读 4 字节 packet header3 字节 payload length + 1 字节 sequence id
// 3. 读 payloadpayload[0] 为 protocol versionMySQL 历史上 9 或 10
// 3. 读 payloadpayload[0] 为 protocol version
// 4. server_version 是从 payload[1] 开始的 null-terminated 字符串
// 5. server_version 中包含 "oceanbase" / "ob" 关键字时判定为 OB MySQL wire
//
// 返回值:(isOBMySQLWire, probeSucceeded)。probeSucceeded=false 表示建连/读包失败,
// 此时让上层正常走 go-ora 路径(不要因为探测失败就阻止真正的尝试)。
// 返回值:(isOBMySQLWire, probeSucceeded)。probeSucceeded=false 表示建连/读包失败,
// 上层应该兜底执行真实连接尝试OBClient 优先于 TNS)。
//
// 容忍度设计:
// - protocol_version 不严限OB 自定义版本号也接受)
// - payload 上限 64KBOB 4.x 的 handshake 可能携带额外的能力位信息)
// - 短超时2s探测只为方向选择主流程的真实超时由 Connect 控制
func probeOceanBaseMySQLWireHandshake(host string, port int, timeout time.Duration) (bool, bool) {
if timeout <= 0 {
timeout = 2 * time.Second
@@ -498,8 +500,8 @@ func probeOceanBaseMySQLWireHandshake(host string, port int, timeout time.Durati
return false, false
}
payloadLen := int(header[0]) | int(header[1])<<8 | int(header[2])<<16
// 合理的 MySQL initial handshake payload 长度在几十~几百字节之间,超出范围视为非 MySQL 协议
if payloadLen < 1 || payloadLen > 1024 {
// 放宽上限OB 4.x handshake 可能携带额外 capability info。仍要约束以避免读取异常长度
if payloadLen < 1 || payloadLen > 65536 {
return false, true
}
payload := make([]byte, payloadLen)
@@ -507,15 +509,10 @@ func probeOceanBaseMySQLWireHandshake(host string, port int, timeout time.Durati
return false, false
}
protocolVersion := payload[0]
if protocolVersion != 10 && protocolVersion != 9 {
// 不是 MySQL initial handshake 格式(可能是 TNS 或其他协议)
return false, true
}
// 不再严格检查 protocol_version。OB 自定义版本号也认作 MySQL wire 候选——
// 只要 server_version 字符串含 OceanBase/OBProxy 关键字就足以做方向选择。
nullIdx := bytes.IndexByte(payload[1:], 0)
if nullIdx < 0 {
// 没有 server_version 终止符,格式不符
return false, true
}
serverVersion := strings.ToLower(string(payload[1 : 1+nullIdx]))
@@ -525,8 +522,6 @@ func probeOceanBaseMySQLWireHandshake(host string, port int, timeout time.Durati
if strings.Contains(serverVersion, "oceanbase") || strings.Contains(serverVersion, "obproxy") {
return true, true
}
// MySQL server_version 通常形如 "5.7.25-OceanBase-v4.x" 或 "5.7.25-OB"
// 用 "-ob" 后缀做兜底匹配(社区版有些版本只在 server_version 里加 -OB 后缀)
if strings.Contains(serverVersion, "-ob") {
return true, true
}
@@ -653,15 +648,28 @@ func (o *OceanBaseDB) Connect(config connection.ConnectionConfig) error {
isOBMySQLWire, probed := probeOceanBaseMySQLWireHandshake(runConfig.Host, runConfig.Port, probeTimeout)
switch {
case probed && isOBMySQLWire:
// 明确识别为 OB MySQL wire 端口:直接走 OBClient capability 路径
logger.Infof("OceanBase 协议=Oracle 预探测:%s:%d 是 OB MySQL wire 端口,走 OBClient capability 注入路径连接 Oracle 租户", runConfig.Host, runConfig.Port)
return o.connectOracleViaOBClient(runConfig)
case probed:
// 探测成功但 server_version 不含 OceanBase 标识:可能是真正的 Oracle TNS 端口
logger.Infof("OceanBase 协议=Oracle 预探测:%s:%d 不是 OB MySQL wire走标准 Oracle TNS 协议OBProxy Oracle listener", runConfig.Host, runConfig.Port)
return o.connectOracleViaTNS(runConfig)
default:
// 探测失败(端口不通 / 网络问题)—— 让 go-ora 走一遍把真实错误暴露出来
logger.Warnf("OceanBase 协议=Oracle 预探测失败(端口不通或无响应),回退到 Oracle TNS 路径让 go-ora 报告真实错误:%s:%d", runConfig.Host, runConfig.Port)
return o.connectOracleViaTNS(runConfig)
// 探测失败(建连或读 handshake 失败):可能是网络不通、防火墙阻断、或某些 OB 版本不主动发 handshake。
// 不能盲选 TNS——用户填 60014/2881 这类端口大概率仍是 OB MySQL wire。
// 串行尝试两条真实路径:先 OBClient命中概率更高失败再 TNS合并错误信息。
logger.Warnf("OceanBase 协议=Oracle 预探测失败:%s:%d串行尝试 OBClient capability 与 TNS 两条路径", runConfig.Host, runConfig.Port)
obclientErr := o.connectOracleViaOBClient(runConfig)
if obclientErr == nil {
return nil
}
logger.Warnf("OceanBase Oracle OBClient 路径失败,继续尝试 TNS 路径:%v", obclientErr)
tnsErr := o.connectOracleViaTNS(runConfig)
if tnsErr == nil {
return nil
}
return fmt.Errorf("OceanBase Oracle 两条连接路径均失败OBClient 路径错误:%vTNS 路径错误:%w", obclientErr, tnsErr)
}
}

View File

@@ -202,7 +202,7 @@ func TestAnnotateOceanBaseOracleConnectErrorClassifies(t *testing.T) {
{
name: "non-oracle protocol on port (e.g. mysql wire)",
raw: errors.New("TNS: protocol error - got unexpected packet from server"),
want: "MySQL 协议端口",
want: "MySQL wire 协议端口",
},
{
name: "ora authentication error",
@@ -394,19 +394,36 @@ func TestProbeOceanBaseMySQLWireHandshakeReturnsFalseOnUnreachable(t *testing.T)
}
}
func TestProbeOceanBaseMySQLWireHandshakeIgnoresNonMySQLProtocol(t *testing.T) {
// probe 放宽 protocol_version 检查后,普通 MySQL/MariaDBserver_version 不含 OB 关键字)
// 应仍判定为非 OB MySQL wire由 regular_mysql_is_not_flagged / mariadb_is_not_flagged 子用例
// 覆盖)。原 IgnoresNonMySQLProtocol 测试因 probe 不再严格区分 mysql vs 非 mysql 而失效,已删除。
// probe 在 payload_length 落在新放宽的 65536 上限内仍能正确读取并提取 server_version。
// 模拟 OB 4.x 可能携带额外能力位、payload 略大于历史 MySQL handshake 的情况。
func TestProbeOceanBaseMySQLWireHandshakeAcceptsLargerPayload(t *testing.T) {
t.Parallel()
// 模拟一个 Oracle TNS 端口:返回非 MySQL 协议格式的字节,探测应判定为非 OB MySQL wire
host, port, cleanup := startMockHandshakeServer(t, []byte{0x00, 0x20, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00})
base := buildMySQLHandshakePacket("5.7.25-OceanBase-v4.2.1.0")
// 在 packet 末尾追加 4096 字节伪能力位扩展,重写 header 的 payload_length 字段
extra := make([]byte, 4096)
for i := range extra {
extra[i] = 0x42
}
originalPayload := base[4:]
enlargedPayload := append(append([]byte{}, originalPayload...), extra...)
payloadLen := len(enlargedPayload)
header := []byte{byte(payloadLen), byte(payloadLen >> 8), byte(payloadLen >> 16), 0}
packet := append(header, enlargedPayload...)
host, port, cleanup := startMockHandshakeServer(t, packet)
defer cleanup()
gotIsOB, probed := probeOceanBaseMySQLWireHandshake(host, port, time.Second)
if gotIsOB {
t.Fatal("expected non-MySQL packet not flagged as OB")
}
if !probed {
t.Fatal("expected probe to complete the read")
t.Fatal("expected probe to read full packet within new 64KB limit")
}
if !gotIsOB {
t.Fatal("expected enlarged OceanBase handshake to be flagged as OB MySQL wire")
}
}