feat: 添加IP竞速拨号器与IP偏好设置,连接状态显示域名与实际IP
Release / build-macos (push) Canceled after 0s
Release / build-windows (push) Canceled after 0s
Release / release (push) Canceled after 0s

- 新增竞速拨号器(racedial.go):域名连接时并行解析所有A/AAAA记录,
  同时对所有IP发起TCP连接,第一个成功的胜出,确保选择延迟最低的地址
- ServerProfile新增IPPreference字段(auto/v4/v6),支持用户指定IP版本偏好
- 填写服务器IP时跳过域名解析直接使用IP,顺序failover
- 连接成功后状态栏显示「已连接 (域名 -> 实际IP)」
- DB迁移v6:server_profiles表添加ip_preference列
- WebSocket拨号与HTTP登录均使用竞速拨号器
- 补充中英文i18n翻译
This commit is contained in:
2026-07-09 18:46:55 +08:00
parent 7febed50ac
commit b81b702433
22 changed files with 398 additions and 146 deletions
+24 -7
View File
@@ -18,6 +18,7 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"sync"
@@ -30,13 +31,14 @@ import (
// HandshakeConfig configures a single connection attempt.
type HandshakeConfig struct {
ServerURL string // e.g. wss://vpn.example.com/ws
SNIHost string // TLS SNI hostname for CDN edge connections
Token string // JWT; if non-empty, used via ?token= (method A)
Username string // for password auth (method B), or fallback
Password string // for password auth (method B), or fallback
OnInit func(protocol.InitMessage) error // configure TUN; nil = auto-ready
TLSConfig *tls.Config // pre-built TLS config (nil = derive from SNIHost)
ServerURL string // e.g. wss://vpn.example.com/ws
SNIHost string // TLS SNI hostname for CDN edge connections
Token string // JWT; if non-empty, used via ?token= (method A)
Username string // for password auth (method B), or fallback
Password string // for password auth (method B), or fallback
OnInit func(protocol.InitMessage) error // configure TUN; nil = auto-ready
TLSConfig *tls.Config // pre-built TLS config (nil = derive from SNIHost)
IPPreference string // "auto" (default), "v4", "v6" - hostname mode only
}
// Conn is an established VPN tunnel connection.
@@ -53,6 +55,7 @@ type Conn struct {
// error occurs.
func Connect(ctx context.Context, cfg HandshakeConfig) (*Conn, error) {
dialer := websocket.Dialer{
NetDialContext: NewRaceDialer(cfg.IPPreference),
HandshakeTimeout: 15 * time.Second,
ReadBufferSize: 4096, // match server (handler.go:17)
WriteBufferSize: 4096, // match server (handler.go:18)
@@ -269,6 +272,20 @@ func (c *Conn) AssignedIP() string { return c.init.IP }
// AssignedIP6 returns the IPv6 assigned by the server (empty if none).
func (c *Conn) AssignedIP6() string { return c.init.IP6 }
// RemoteIP returns the remote IP address of the underlying TCP
// connection (the actual server/CDN IP the WebSocket is connected to).
// Returns an empty string if the connection is not established.
func (c *Conn) RemoteIP() string {
if c.ws == nil {
return ""
}
addr := c.ws.RemoteAddr()
if tcpAddr, ok := addr.(*net.TCPAddr); ok {
return tcpAddr.IP.String()
}
return addr.String()
}
// Close terminates the connection.
func (c *Conn) Close() error {
c.mu.Lock()