mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-07 04:42:51 +08:00
- 后端 ConnectionConfig 增加代理配置并完成规范化处理 - 普通 TCP 数据源通过本地转发接入代理 - MongoDB 使用 Dialer 支持代理连接(含 SRV) - 前端连接配置新增代理 UI、字段清洗与数据回填 - refs #122
345 lines
8.5 KiB
Go
345 lines
8.5 KiB
Go
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)
|
||
}
|