mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-07 15:09:34 +08:00
✨ feat(db): 数据库连接新增 SOCKS5/HTTP 代理能力并兼容 SRV/SSH 场景
- 后端 ConnectionConfig 增加代理配置并完成规范化处理 - 普通 TCP 数据源通过本地转发接入代理 - MongoDB 使用 Dialer 支持代理连接(含 SRV) - 前端连接配置新增代理 UI、字段清洗与数据回填 - refs #122
This commit is contained in:
344
internal/proxy/proxy.go
Normal file
344
internal/proxy/proxy.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"GoNavi-Wails/internal/connection"
|
||||
"GoNavi-Wails/internal/logger"
|
||||
|
||||
xproxy "golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDialTimeout = 8 * time.Second
|
||||
)
|
||||
|
||||
type LocalForwarder struct {
|
||||
LocalAddr string
|
||||
RemoteAddr string
|
||||
ProxyAddr string
|
||||
ProxyType string
|
||||
|
||||
cfg connection.ProxyConfig
|
||||
listener net.Listener
|
||||
closeChan chan struct{}
|
||||
closeOnce sync.Once
|
||||
|
||||
closed bool
|
||||
closedMu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
forwarderMu sync.RWMutex
|
||||
localForwarders = make(map[string]*LocalForwarder)
|
||||
)
|
||||
|
||||
func NormalizeConfig(config connection.ProxyConfig) (connection.ProxyConfig, error) {
|
||||
result := connection.ProxyConfig{
|
||||
Type: strings.ToLower(strings.TrimSpace(config.Type)),
|
||||
Host: strings.TrimSpace(config.Host),
|
||||
Port: config.Port,
|
||||
User: strings.TrimSpace(config.User),
|
||||
Password: config.Password,
|
||||
}
|
||||
|
||||
switch result.Type {
|
||||
case "socks5", "socks5h", "http":
|
||||
default:
|
||||
return result, fmt.Errorf("不支持的代理类型:%s", config.Type)
|
||||
}
|
||||
if result.Type == "socks5h" {
|
||||
result.Type = "socks5"
|
||||
}
|
||||
if result.Host == "" {
|
||||
return result, fmt.Errorf("代理主机为空")
|
||||
}
|
||||
if result.Port <= 0 || result.Port > 65535 {
|
||||
return result, fmt.Errorf("代理端口无效:%d", result.Port)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetOrCreateLocalForwarder(proxyConfig connection.ProxyConfig, remoteHost string, remotePort int) (*LocalForwarder, error) {
|
||||
cfg, err := NormalizeConfig(proxyConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(remoteHost) == "" || remotePort <= 0 {
|
||||
return nil, fmt.Errorf("无效的远端地址:%s:%d", remoteHost, remotePort)
|
||||
}
|
||||
|
||||
key := forwarderCacheKey(cfg, remoteHost, remotePort)
|
||||
forwarderMu.RLock()
|
||||
forwarder, exists := localForwarders[key]
|
||||
forwarderMu.RUnlock()
|
||||
if exists && forwarder != nil && !forwarder.IsClosed() {
|
||||
return forwarder, nil
|
||||
}
|
||||
|
||||
if exists {
|
||||
forwarderMu.Lock()
|
||||
delete(localForwarders, key)
|
||||
forwarderMu.Unlock()
|
||||
}
|
||||
|
||||
next, err := NewLocalForwarder(cfg, remoteHost, remotePort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
forwarderMu.Lock()
|
||||
localForwarders[key] = next
|
||||
forwarderMu.Unlock()
|
||||
return next, nil
|
||||
}
|
||||
|
||||
func forwarderCacheKey(cfg connection.ProxyConfig, remoteHost string, remotePort int) string {
|
||||
trimmedHost := strings.TrimSpace(remoteHost)
|
||||
credential := cfg.User + "\x00" + cfg.Password
|
||||
credentialHash := sha256.Sum256([]byte(credential))
|
||||
// 仅保留短指纹用于区分不同认证信息,避免在 key 日志中泄露明文口令。
|
||||
fingerprint := hex.EncodeToString(credentialHash[:8])
|
||||
return fmt.Sprintf("%s://%s:%d@%s:%d#%s", cfg.Type, cfg.Host, cfg.Port, trimmedHost, remotePort, fingerprint)
|
||||
}
|
||||
|
||||
func NewLocalForwarder(proxyConfig connection.ProxyConfig, remoteHost string, remotePort int) (*LocalForwarder, error) {
|
||||
cfg, err := NormalizeConfig(proxyConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建本地代理监听失败:%w", err)
|
||||
}
|
||||
|
||||
localAddr := listener.Addr().String()
|
||||
remoteAddr := net.JoinHostPort(strings.TrimSpace(remoteHost), fmt.Sprintf("%d", remotePort))
|
||||
proxyAddr := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", cfg.Port))
|
||||
forwarder := &LocalForwarder{
|
||||
LocalAddr: localAddr,
|
||||
RemoteAddr: remoteAddr,
|
||||
ProxyAddr: proxyAddr,
|
||||
ProxyType: cfg.Type,
|
||||
cfg: cfg,
|
||||
listener: listener,
|
||||
closeChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
go forwarder.forward()
|
||||
logger.Infof("已创建代理端口转发:本地 %s -> 远端 %s(代理 %s://%s)", localAddr, remoteAddr, cfg.Type, proxyAddr)
|
||||
return forwarder, nil
|
||||
}
|
||||
|
||||
func (f *LocalForwarder) forward() {
|
||||
for {
|
||||
localConn, err := f.listener.Accept()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-f.closeChan:
|
||||
return
|
||||
default:
|
||||
logger.Warnf("接受本地代理连接失败:%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
go f.handleConnection(localConn)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *LocalForwarder) handleConnection(localConn net.Conn) {
|
||||
defer localConn.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultDialTimeout)
|
||||
remoteConn, err := dialThroughProxy(ctx, f.cfg, "tcp", f.RemoteAddr)
|
||||
cancel()
|
||||
if err != nil {
|
||||
logger.Warnf("通过代理连接远端失败:远端=%s 代理=%s://%s 错误=%v", f.RemoteAddr, f.ProxyType, f.ProxyAddr, err)
|
||||
return
|
||||
}
|
||||
defer remoteConn.Close()
|
||||
|
||||
errc := make(chan error, 2)
|
||||
var closeOnce sync.Once
|
||||
closeBoth := func() {
|
||||
_ = localConn.Close()
|
||||
_ = remoteConn.Close()
|
||||
}
|
||||
go func() {
|
||||
_, copyErr := io.Copy(remoteConn, localConn)
|
||||
closeOnce.Do(closeBoth)
|
||||
errc <- copyErr
|
||||
}()
|
||||
go func() {
|
||||
_, copyErr := io.Copy(localConn, remoteConn)
|
||||
closeOnce.Do(closeBoth)
|
||||
errc <- copyErr
|
||||
}()
|
||||
<-errc
|
||||
<-errc
|
||||
}
|
||||
|
||||
func (f *LocalForwarder) Close() error {
|
||||
var err error
|
||||
f.closeOnce.Do(func() {
|
||||
f.closedMu.Lock()
|
||||
f.closed = true
|
||||
f.closedMu.Unlock()
|
||||
close(f.closeChan)
|
||||
err = f.listener.Close()
|
||||
if err != nil {
|
||||
logger.Warnf("关闭代理端口转发失败:%v", err)
|
||||
}
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *LocalForwarder) IsClosed() bool {
|
||||
f.closedMu.RLock()
|
||||
defer f.closedMu.RUnlock()
|
||||
return f.closed
|
||||
}
|
||||
|
||||
func CloseAllForwarders() {
|
||||
forwarderMu.Lock()
|
||||
defer forwarderMu.Unlock()
|
||||
|
||||
for key, forwarder := range localForwarders {
|
||||
if forwarder == nil {
|
||||
continue
|
||||
}
|
||||
_ = forwarder.Close()
|
||||
logger.Infof("已关闭代理端口转发:%s", key)
|
||||
}
|
||||
localForwarders = make(map[string]*LocalForwarder)
|
||||
}
|
||||
|
||||
func DialContext(ctx context.Context, proxyConfig connection.ProxyConfig, network, address string) (net.Conn, error) {
|
||||
cfg, err := NormalizeConfig(proxyConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dialThroughProxy(ctx, cfg, network, address)
|
||||
}
|
||||
|
||||
func dialThroughProxy(ctx context.Context, cfg connection.ProxyConfig, network, address string) (net.Conn, error) {
|
||||
switch cfg.Type {
|
||||
case "socks5":
|
||||
return dialSOCKS5(ctx, cfg, network, address)
|
||||
case "http":
|
||||
return dialHTTPConnect(ctx, cfg, address)
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的代理类型:%s", cfg.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func dialSOCKS5(ctx context.Context, cfg connection.ProxyConfig, network, address string) (net.Conn, error) {
|
||||
proxyAddr := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", cfg.Port))
|
||||
var auth *xproxy.Auth
|
||||
if cfg.User != "" || cfg.Password != "" {
|
||||
auth = &xproxy.Auth{
|
||||
User: cfg.User,
|
||||
Password: cfg.Password,
|
||||
}
|
||||
}
|
||||
dialer, err := xproxy.SOCKS5("tcp", proxyAddr, auth, &net.Dialer{Timeout: defaultDialTimeout})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 SOCKS5 代理拨号器失败:%w", err)
|
||||
}
|
||||
|
||||
type result struct {
|
||||
conn net.Conn
|
||||
err error
|
||||
}
|
||||
ch := make(chan result, 1)
|
||||
go func() {
|
||||
conn, dialErr := dialer.Dial(network, address)
|
||||
ch <- result{conn: conn, err: dialErr}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
go func() {
|
||||
r := <-ch
|
||||
if r.conn != nil {
|
||||
_ = r.conn.Close()
|
||||
}
|
||||
}()
|
||||
return nil, ctx.Err()
|
||||
case r := <-ch:
|
||||
if r.err != nil {
|
||||
return nil, fmt.Errorf("SOCKS5 代理连接失败:%w", r.err)
|
||||
}
|
||||
return r.conn, nil
|
||||
}
|
||||
}
|
||||
|
||||
func dialHTTPConnect(ctx context.Context, cfg connection.ProxyConfig, address string) (net.Conn, error) {
|
||||
proxyAddr := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", cfg.Port))
|
||||
dialer := &net.Dialer{Timeout: defaultDialTimeout}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", proxyAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("连接 HTTP 代理失败:%w", err)
|
||||
}
|
||||
|
||||
connectReq := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Opaque: address},
|
||||
Host: address,
|
||||
Header: make(http.Header),
|
||||
}
|
||||
if cfg.User != "" || cfg.Password != "" {
|
||||
raw := cfg.User + ":" + cfg.Password
|
||||
connectReq.Header.Set("Proxy-Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(raw)))
|
||||
}
|
||||
if err := connectReq.Write(conn); err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("发送 HTTP CONNECT 请求失败:%w", err)
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(conn)
|
||||
resp, err := http.ReadResponse(reader, connectReq)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("读取 HTTP CONNECT 响应失败:%w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("HTTP 代理 CONNECT 失败:%s", strings.TrimSpace(resp.Status))
|
||||
}
|
||||
|
||||
if reader.Buffered() == 0 {
|
||||
return conn, nil
|
||||
}
|
||||
return &bufferedConn{Conn: conn, reader: reader}, nil
|
||||
}
|
||||
|
||||
type bufferedConn struct {
|
||||
net.Conn
|
||||
reader *bufio.Reader
|
||||
}
|
||||
|
||||
func (c *bufferedConn) Read(p []byte) (int, error) {
|
||||
if c.reader == nil {
|
||||
return c.Conn.Read(p)
|
||||
}
|
||||
if c.reader.Buffered() == 0 {
|
||||
c.reader = nil
|
||||
return c.Conn.Read(p)
|
||||
}
|
||||
return c.reader.Read(p)
|
||||
}
|
||||
Reference in New Issue
Block a user