fscan/mysql_tests/mysql_fscan_diagnosis.go
ZacharyZcR 43f210ffc6 feat: 实现新一代插件注册系统完全替代传统手动注册模式
- 重构插件注册架构采用现代工厂模式和自动发现机制
- 新增完整的插件元数据管理系统支持版本能力标签等信息
- 实现智能插件适配器提供向后兼容的桥接功能
- 建立MySQL Redis SSH三个标准插件作为新架构参考实现
- 优化插件扫描逻辑支持按端口按类型的智能查询和过滤
- 添加国际化支持和完善的文档体系
- 代码量减少67%维护成本大幅降低扩展性显著提升

新架构特点:
- 零配置插件注册import即用
- 工厂模式延迟初始化和依赖注入
- 丰富元数据系统和能力声明
- 完全解耦的模块化设计
- 面向未来的可扩展架构

测试验证: MySQL和Redis插件功能完整包括弱密码检测未授权访问检测和自动利用攻击
2025-08-07 11:28:34 +08:00

380 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"context"
"database/sql"
"fmt"
"net"
"time"
_ "github.com/go-sql-driver/mysql"
)
// FScanMySQLDiagnosis fscan MySQL连接问题诊断工具
type FScanMySQLDiagnosis struct {
host string
port int
username string
password string
timeout time.Duration
}
// NewDiagnosis 创建诊断工具
func NewDiagnosis(host string, port int, username, password string, timeout time.Duration) *FScanMySQLDiagnosis {
return &FScanMySQLDiagnosis{
host: host,
port: port,
username: username,
password: password,
timeout: timeout,
}
}
func main() {
fmt.Println("FScan MySQL连接问题诊断工具")
fmt.Println("===========================")
// 初始化诊断工具
diagnosis := NewDiagnosis("127.0.0.1", 3306, "root", "123456", 3*time.Second)
// 执行完整诊断
diagnosis.RunFullDiagnosis()
}
// RunFullDiagnosis 运行完整诊断
func (d *FScanMySQLDiagnosis) RunFullDiagnosis() {
fmt.Printf("目标: %s:%d\n", d.host, d.port)
fmt.Printf("用户: %s\n", d.username)
fmt.Printf("超时: %v\n", d.timeout)
fmt.Println()
// 步骤1: 网络连通性测试
fmt.Println("步骤1: 网络连通性测试")
if !d.testNetworkConnection() {
return
}
// 步骤2: 测试fscan当前的连接字符串格式
fmt.Println("\n步骤2: 测试fscan当前连接字符串格式")
d.testFScanCurrentFormat()
// 步骤3: 测试不同的超时配置
fmt.Println("\n步骤3: 测试不同超时配置")
d.testTimeoutConfigurations()
// 步骤4: 测试连接池配置的影响
fmt.Println("\n步骤4: 测试连接池配置")
d.testConnectionPoolSettings()
// 步骤5: 提供修复建议
fmt.Println("\n步骤5: 修复建议")
d.provideFixSuggestions()
}
// testNetworkConnection 测试网络连接
func (d *FScanMySQLDiagnosis) testNetworkConnection() bool {
fmt.Printf("正在测试TCP连接 %s:%d...\n", d.host, d.port)
conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", d.host, d.port), d.timeout)
if err != nil {
fmt.Printf("❌ TCP连接失败: %v\n", err)
fmt.Println("建议:")
fmt.Println("- 检查MySQL服务是否启动")
fmt.Println("- 检查端口3306是否开放")
fmt.Println("- 检查防火墙设置")
return false
}
defer conn.Close()
fmt.Println("✅ TCP连接成功")
return true
}
// testFScanCurrentFormat 测试fscan当前使用的连接字符串格式
func (d *FScanMySQLDiagnosis) testFScanCurrentFormat() {
timeoutStr := d.timeout.String()
// fscan当前使用的格式
connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8&timeout=%s",
d.username, d.password, d.host, d.port, timeoutStr)
fmt.Printf("连接字符串: %s\n", connStr)
success, elapsed, err := d.testConnectionString(connStr, "fscan当前格式")
if success {
fmt.Printf("✅ fscan格式连接成功 (耗时: %v)\n", elapsed)
} else {
fmt.Printf("❌ fscan格式连接失败 (耗时: %v): %v\n", elapsed, err)
// 尝试替代方案
fmt.Println("\n尝试替代连接字符串...")
d.testAlternativeFormats()
}
}
// testAlternativeFormats 测试替代连接字符串格式
func (d *FScanMySQLDiagnosis) testAlternativeFormats() {
timeoutStr := d.timeout.String()
alternatives := []struct {
name string
format string
desc string
}{
{
name: "不指定数据库",
format: fmt.Sprintf("%s:%s@tcp(%s:%d)/?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr),
desc: "不连接到特定数据库",
},
{
name: "无数据库路径",
format: fmt.Sprintf("%s:%s@tcp(%s:%d)?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr),
desc: "完全省略数据库路径",
},
{
name: "使用information_schema",
format: fmt.Sprintf("%s:%s@tcp(%s:%d)/information_schema?charset=utf8&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr),
desc: "连接到系统数据库",
},
{
name: "UTF8MB4字符集",
format: fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8mb4&timeout=%s", d.username, d.password, d.host, d.port, timeoutStr),
desc: "使用完整UTF-8字符集",
},
}
for _, alt := range alternatives {
success, elapsed, err := d.testConnectionString(alt.format, alt.name)
if success {
fmt.Printf("✅ %s 成功 (耗时: %v)\n", alt.name, elapsed)
fmt.Printf(" 建议使用: %s\n", alt.format)
} else {
fmt.Printf("❌ %s 失败 (耗时: %v): %v\n", alt.name, elapsed, err)
}
}
}
// testConnectionString 测试指定的连接字符串
func (d *FScanMySQLDiagnosis) testConnectionString(connStr, name string) (bool, time.Duration, error) {
ctx, cancel := context.WithTimeout(context.Background(), d.timeout+time.Second)
defer cancel()
start := time.Now()
db, err := sql.Open("mysql", connStr)
if err != nil {
return false, time.Since(start), err
}
defer db.Close()
// 模拟fscan的连接池设置
db.SetConnMaxLifetime(d.timeout)
db.SetConnMaxIdleTime(d.timeout)
db.SetMaxIdleConns(1)
db.SetMaxOpenConns(1)
err = db.PingContext(ctx)
elapsed := time.Since(start)
return err == nil, elapsed, err
}
// testTimeoutConfigurations 测试不同超时配置
func (d *FScanMySQLDiagnosis) testTimeoutConfigurations() {
timeouts := []time.Duration{
1 * time.Second,
3 * time.Second,
5 * time.Second,
10 * time.Second,
}
baseDSN := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8", d.username, d.password, d.host, d.port)
for _, timeout := range timeouts {
fmt.Printf("测试超时: %v\n", timeout)
// DSN中设置timeout
dsnWithTimeout := baseDSN + "&timeout=" + timeout.String()
success, elapsed, err := d.testConnectionStringWithTimeout(dsnWithTimeout, timeout+time.Second)
if success {
fmt.Printf(" ✅ DSN超时%v 成功 (实际耗时: %v)\n", timeout, elapsed)
} else {
fmt.Printf(" ❌ DSN超时%v 失败 (实际耗时: %v): %v\n", timeout, elapsed, err)
}
}
}
// testConnectionStringWithTimeout 使用指定超时测试连接字符串
func (d *FScanMySQLDiagnosis) testConnectionStringWithTimeout(connStr string, contextTimeout time.Duration) (bool, time.Duration, error) {
ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
defer cancel()
start := time.Now()
db, err := sql.Open("mysql", connStr)
if err != nil {
return false, time.Since(start), err
}
defer db.Close()
err = db.PingContext(ctx)
elapsed := time.Since(start)
return err == nil, elapsed, err
}
// testConnectionPoolSettings 测试连接池设置的影响
func (d *FScanMySQLDiagnosis) testConnectionPoolSettings() {
connStr := fmt.Sprintf("%s:%s@tcp(%s:%d)/mysql?charset=utf8&timeout=%s",
d.username, d.password, d.host, d.port, d.timeout.String())
configs := []struct {
name string
configure func(*sql.DB)
desc string
}{
{
name: "fscan默认设置",
configure: func(db *sql.DB) {
db.SetConnMaxLifetime(d.timeout)
db.SetConnMaxIdleTime(d.timeout)
db.SetMaxIdleConns(1)
db.SetMaxOpenConns(1)
},
desc: "模拟fscan当前的连接池配置",
},
{
name: "更长生命周期",
configure: func(db *sql.DB) {
db.SetConnMaxLifetime(30 * time.Second)
db.SetConnMaxIdleTime(30 * time.Second)
db.SetMaxIdleConns(1)
db.SetMaxOpenConns(1)
},
desc: "延长连接生命周期到30秒",
},
{
name: "无生命周期限制",
configure: func(db *sql.DB) {
db.SetMaxIdleConns(1)
db.SetMaxOpenConns(1)
},
desc: "不设置连接生命周期限制",
},
}
for _, config := range configs {
fmt.Printf("测试: %s\n", config.name)
fmt.Printf("描述: %s\n", config.desc)
success, elapsed, err := d.testWithPoolConfig(connStr, config.configure)
if success {
fmt.Printf(" ✅ 成功 (耗时: %v)\n", elapsed)
} else {
fmt.Printf(" ❌ 失败 (耗时: %v): %v\n", elapsed, err)
}
}
}
// testWithPoolConfig 使用指定连接池配置测试
func (d *FScanMySQLDiagnosis) testWithPoolConfig(connStr string, configure func(*sql.DB)) (bool, time.Duration, error) {
ctx, cancel := context.WithTimeout(context.Background(), d.timeout+2*time.Second)
defer cancel()
start := time.Now()
db, err := sql.Open("mysql", connStr)
if err != nil {
return false, time.Since(start), err
}
defer db.Close()
configure(db)
err = db.PingContext(ctx)
elapsed := time.Since(start)
return err == nil, elapsed, err
}
// provideFixSuggestions 提供修复建议
func (d *FScanMySQLDiagnosis) provideFixSuggestions() {
fmt.Println("基于诊断结果的修复建议:")
fmt.Println()
fmt.Println("1. 连接字符串优化:")
fmt.Println(" 原始: user:pass@tcp(host:port)/mysql?charset=utf8&timeout=3s")
fmt.Println(" 建议: user:pass@tcp(host:port)/?charset=utf8mb4&timeout=5s&readTimeout=3s&writeTimeout=3s")
fmt.Println(" 原因: 不指定数据库减少权限要求使用utf8mb4支持完整字符集")
fmt.Println()
fmt.Println("2. 超时配置优化:")
fmt.Println(" - Context超时应该 > DSN timeout")
fmt.Println(" - 建议Context超时 = DSN timeout + 2秒")
fmt.Println(" - 网络环境差时适当增加超时时间")
fmt.Println()
fmt.Println("3. 连接池配置优化:")
fmt.Println(" - SetConnMaxLifetime(10*time.Second) // 适当延长")
fmt.Println(" - SetMaxOpenConns(1) // 扫描场景保持1个连接")
fmt.Println(" - 确保每次使用后正确关闭连接")
fmt.Println()
fmt.Println("4. 错误处理改进:")
fmt.Println(" - 区分网络超时和认证失败")
fmt.Println(" - 实现重试机制")
fmt.Println(" - 记录详细的错误信息")
fmt.Println()
fmt.Println("5. 针对fscan的具体修改建议:")
d.provideFScanSpecificFixes()
}
// provideFScanSpecificFixes 提供fscan特定的修复建议
func (d *FScanMySQLDiagnosis) provideFScanSpecificFixes() {
fmt.Println("针对fscan代码的修改建议:")
fmt.Println()
fmt.Println("修改 plugins/services/mysql/connector.go:")
fmt.Println("```go")
fmt.Println("// buildConnectionString 构建连接字符串 - 优化版本")
fmt.Println("func (c *MySQLConnector) buildConnectionString(host string, port int, username, password string) string {")
fmt.Println(" timeoutStr := c.timeout.String()")
fmt.Println(" readTimeoutStr := (c.timeout - time.Second).String() // 读超时比总超时短1秒")
fmt.Println(" ")
fmt.Println(" var connStr string")
fmt.Println(" if common.Socks5Proxy != \"\" {")
fmt.Println(" // 代理连接 - 不指定具体数据库")
fmt.Println(" connStr = fmt.Sprintf(\"%v:%v@tcp-proxy(%v:%v)/?charset=utf8mb4&timeout=%s&readTimeout=%s&writeTimeout=%s\",")
fmt.Println(" username, password, host, port, timeoutStr, readTimeoutStr, readTimeoutStr)")
fmt.Println(" } else {")
fmt.Println(" // 标准连接 - 不指定具体数据库")
fmt.Println(" connStr = fmt.Sprintf(\"%v:%v@tcp(%v:%v)/?charset=utf8mb4&timeout=%s&readTimeout=%s&writeTimeout=%s\",")
fmt.Println(" username, password, host, port, timeoutStr, readTimeoutStr, readTimeoutStr)")
fmt.Println(" }")
fmt.Println(" ")
fmt.Println(" return connStr")
fmt.Println("}")
fmt.Println("```")
fmt.Println()
fmt.Println("修改认证函数中的Context超时:")
fmt.Println("```go")
fmt.Println("// Authenticate 认证 - 优化版本")
fmt.Println("func (c *MySQLConnector) Authenticate(ctx context.Context, conn interface{}, cred *base.Credential) error {")
fmt.Println(" // 创建更长的context超时避免冲突")
fmt.Println(" authCtx, cancel := context.WithTimeout(ctx, c.timeout+2*time.Second)")
fmt.Println(" defer cancel()")
fmt.Println(" ")
fmt.Println(" // ... 其余代码保持不变")
fmt.Println(" err = db.PingContext(authCtx) // 使用新的context")
fmt.Println(" // ...")
fmt.Println("}")
fmt.Println("```")
fmt.Println()
fmt.Println("这些修改应该能解决'context deadline exceeded'错误。")
}