Files
kevin b81b702433
Release / build-macos (push) Canceled after 0s
Release / build-windows (push) Canceled after 0s
Release / release (push) Canceled after 0s
feat: 添加IP竞速拨号器与IP偏好设置,连接状态显示域名与实际IP
- 新增竞速拨号器(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翻译
2026-07-09 18:46:55 +08:00

315 lines
8.8 KiB
Go

// Package ipc implements the communication protocol between the GUI
// process (user) and the privileged daemon (root) over a unix domain
// socket.
//
// Protocol: newline-delimited JSON. Each message is one JSON object
// followed by '\n'.
//
// GUI → daemon: Request (start, stop, shutdown, stats)
// daemon → GUI: Event (state, stats, error)
package ipc
import (
"bufio"
"encoding/json"
"fmt"
"io"
"net"
"os"
"sync"
"lmvpn/internal/paths"
"lmvpn/internal/route"
"lmvpn/internal/stats"
)
// Command types sent from GUI to daemon.
const (
CmdStart = "start"
CmdStop = "stop"
CmdShutdown = "shutdown"
CmdStats = "stats"
CmdVersion = "version" // query daemon build version
CmdRefreshCIDR = "refresh_cidr" // re-fetch CIDR URL sources and update routes
)
// Event types sent from daemon to GUI.
const (
EvState = "state"
EvStats = "stats"
EvError = "error"
)
// Request is a command from the GUI to the daemon.
type Request struct {
Cmd string `json:"cmd"`
Config *ClientConfig `json:"config,omitempty"`
}
// ClientConfig is the session configuration sent over IPC. It mirrors
// vpn.SessionConfig but is kept separate to avoid importing the vpn
// package (which needs root-only TUN) into the GUI.
type ClientConfig struct {
ServerURL string `json:"server_url"`
SNIHost string `json:"sni_host"` // TLS SNI hostname for CDN
ServerIPs []string `json:"server_ips"` // CDN edge IP list for failover
Username string `json:"username"`
Password string `json:"password"`
Token string `json:"token"`
AuthMode string `json:"auth_mode"`
RoutingMode string `json:"routing_mode"` // "full", "proxy", "bypass"
CIDRV4 []string `json:"cidr_v4"` // static IPv4 CIDRs
CIDRV6 []string `json:"cidr_v6"` // static IPv6 CIDRs
CIDRV4URLs []CIDRURLSource `json:"cidr_v4_urls"` // IPv4 CIDR URL sources
CIDRV6URLs []CIDRURLSource `json:"cidr_v6_urls"` // IPv6 CIDR URL sources
MTUOverride int `json:"mtu_override"`
TLSCACert string `json:"tls_ca_cert"` // inline CA cert PEM (wss only)
TLSCAPath string `json:"tls_ca_path"` // CA cert file path (wss only)
TLSInsecure bool `json:"tls_insecure"` // skip cert verification (wss only)
TLSPinnedHash string `json:"tls_pinned_hash"` // SHA-256 cert pin (wss only)
IPPreference string `json:"ip_preference"` // "auto", "v4", "v6" (hostname mode only)
}
// CIDRURLSource describes a URL that provides a CIDR list. It mirrors
// model.CIDRURLSource but is kept here to avoid importing the model
// package into the IPC wire format.
type CIDRURLSource struct {
URL string `json:"url"`
FetchTiming string `json:"fetch_timing"` // "before" or "after"
}
// Event is a notification from the daemon to the GUI.
type Event struct {
Event string `json:"event"`
State string `json:"state,omitempty"`
ConnectStep string `json:"connect_step,omitempty"` // current connection step (for EvState)
Stats *stats.Snapshot `json:"stats,omitempty"`
Code string `json:"code,omitempty"` // stable auth-error code for EvError
Message string `json:"message,omitempty"`
}
// --- Wire helpers ---
func writeMsg(w io.Writer, v interface{}) error {
data, err := json.Marshal(v)
if err != nil {
return err
}
data = append(data, '\n')
_, err = w.Write(data)
return err
}
func readMsg(r *bufio.Reader, v interface{}) error {
line, err := r.ReadString('\n')
if err != nil {
return err
}
return json.Unmarshal([]byte(line), v)
}
// --- Server (daemon side) ---
// Server listens on the IPC socket and manages connected clients.
type Server struct {
listener net.Listener
mu sync.Mutex
clients map[net.Conn]bool
}
// NewServer creates (but does not start) the IPC server. It removes
// any stale socket file first (unix only).
func NewServer() (*Server, error) {
netType := paths.IPCNetwork()
addr := paths.IPCAddress()
// Guard against a second daemon instance on unix: probe the
// socket before removing it. If the dial succeeds, another
// daemon is alive and owns the socket - refuse to start so we
// don't silently orphan it. Only remove the file when the dial
// fails (stale socket from a crashed process).
if netType == "unix" {
if c, derr := net.Dial(netType, addr); derr == nil {
c.Close()
return nil, fmt.Errorf("daemon already running on %s", addr)
}
_ = os.Remove(addr)
}
l, err := net.Listen(netType, addr)
if err != nil {
return nil, fmt.Errorf("listen ipc: %w", err)
}
// Set socket permissions so group members can connect (unix only).
if netType == "unix" {
_ = os.Chmod(addr, 0o660)
}
return &Server{listener: l, clients: make(map[net.Conn]bool)}, nil
}
// Accept runs the accept loop. Each connection is handled in a
// goroutine; the handler callback receives each Request.
func (s *Server) Accept(handle func(net.Conn, Request)) error {
for {
conn, err := s.listener.Accept()
if err != nil {
return err
}
s.mu.Lock()
s.clients[conn] = true
s.mu.Unlock()
go s.serve(conn, handle)
}
}
func (s *Server) serve(conn net.Conn, handle func(net.Conn, Request)) {
defer func() {
s.mu.Lock()
delete(s.clients, conn)
s.mu.Unlock()
conn.Close()
}()
r := bufio.NewReader(conn)
for {
var req Request
if err := readMsg(r, &req); err != nil {
return
}
handle(conn, req)
}
}
// Broadcast sends an event to all connected clients.
func (s *Server) Broadcast(ev Event) {
data, _ := json.Marshal(ev)
data = append(data, '\n')
s.mu.Lock()
defer s.mu.Unlock()
for c := range s.clients {
_, _ = c.Write(data)
}
}
// Close stops the server and closes all connections.
func (s *Server) Close() {
s.mu.Lock()
defer s.mu.Unlock()
for c := range s.clients {
c.Close()
}
s.clients = nil
if s.listener != nil {
s.listener.Close()
}
}
// --- Client (GUI side) ---
// Client connects to the daemon's IPC socket.
type Client struct {
conn net.Conn
r *bufio.Reader
}
// Dial connects to the daemon.
func Dial() (*Client, error) {
conn, err := net.Dial(paths.IPCNetwork(), paths.IPCAddress())
if err != nil {
return nil, fmt.Errorf("dial daemon: %w", err)
}
return &Client{conn: conn, r: bufio.NewReader(conn)}, nil
}
// Send sends a request to the daemon.
func (c *Client) Send(req Request) error {
return writeMsg(c.conn, &req)
}
// Recv reads the next event from the daemon. Blocks until an event is
// available or the connection breaks.
func (c *Client) Recv() (Event, error) {
var ev Event
if err := readMsg(c.r, &ev); err != nil {
return ev, err
}
return ev, nil
}
// Close closes the connection.
func (c *Client) Close() error { return c.conn.Close() }
// Response is a simple ack/error reply to a command.
type Response struct {
OK bool `json:"ok"`
Error string `json:"error,omitempty"`
Version string `json:"version,omitempty"` // set for CmdVersion replies
}
// WriteOK sends a success response to a connection.
func WriteOK(conn net.Conn) error {
return writeMsg(conn, Response{OK: true})
}
// WriteErr sends an error response to a connection.
func WriteErr(conn net.Conn, msg string) error {
return writeMsg(conn, Response{OK: false, Error: msg})
}
// SendStart is a convenience helper for sending a start command.
func SendStart(c *Client, cfg ClientConfig) error {
return c.Send(Request{Cmd: CmdStart, Config: &cfg})
}
// SendStop is a convenience helper for sending a stop command.
func SendStop(c *Client) error {
return c.Send(Request{Cmd: CmdStop})
}
// SendRefreshCIDR is a convenience helper for sending a refresh CIDR command.
func SendRefreshCIDR(c *Client) error {
return c.Send(Request{Cmd: CmdRefreshCIDR})
}
// SendShutdown is a convenience helper for sending a shutdown command.
func SendShutdown(c *Client) error {
return c.Send(Request{Cmd: CmdShutdown})
}
// WriteVersion sends a version response to a connection.
func WriteVersion(conn net.Conn, ver string) error {
return writeMsg(conn, Response{OK: true, Version: ver})
}
// QueryVersion sends a CmdVersion request and reads the daemon's
// build version. It must be called before any VPN session starts (so
// no Event messages are in flight on the connection).
func (c *Client) QueryVersion() (string, error) {
if err := c.Send(Request{Cmd: CmdVersion}); err != nil {
return "", err
}
var resp Response
if err := readMsg(c.r, &resp); err != nil {
return "", err
}
if !resp.OK {
return "", fmt.Errorf("version query failed: %s", resp.Error)
}
return resp.Version, nil
}
// RoutingModeFromIPC converts an IPC routing mode string to route.Mode.
func RoutingModeFromIPC(s string) route.Mode {
switch s {
case "full":
return route.ModeFull
case "proxy":
return route.ModeProxy
case "bypass":
return route.ModeBypass
default:
return route.ModeFull
}
}