feat: 新增 IPv6 双栈支持
- 协议: InitMessage 增加 ip6/prefix6/server_ip6 字段(omitempty,向后兼容) - TUN: Device 接口新增 ConfigureIPv6, macOS 用 ifconfig inet6, Linux 用 ip -6 addr add - 路由: full 模式自动添加 ::/1+8000::/1, v6 服务器旁路 /128, resolveHosts 双栈解析 - 会话: setupTUN 解析 v6 字段并配置 TUN+路由, connectOnce 传 v6 给 stats/日志 - 统计: SetConnected(ip,ip6), Snapshot.AssignedIP6 - DB: connection_logs 增加 assigned_ip6 列 + migrateV3 幂等迁移 - UI: 状态卡片新增独立 IPv6 行, 始终可见(未获取显示 IPv6: —) - 文档: 同步 client-development.md 至服务端 IPv6 版本
This commit is contained in:
+49
-10
@@ -45,7 +45,10 @@ func (s *Store) migrate() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.migrateV2()
|
||||
if err := s.migrateV2(); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.migrateV3()
|
||||
}
|
||||
|
||||
func (s *Store) migrateV2() error {
|
||||
@@ -165,6 +168,41 @@ func nullStr(s sql.NullString) sql.NullString {
|
||||
return s
|
||||
}
|
||||
|
||||
// migrateV3 adds the assigned_ip6 column to connection_logs for IPv6
|
||||
// dual-stack support. Idempotent: skips if the column already exists.
|
||||
func (s *Store) migrateV3() error {
|
||||
if columnExists(s.db, "connection_logs", "assigned_ip6") {
|
||||
return nil
|
||||
}
|
||||
_, err := s.db.Exec(`ALTER TABLE connection_logs ADD COLUMN assigned_ip6 TEXT NOT NULL DEFAULT ''`)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate v3 add assigned_ip6: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// columnExists reports whether a column exists on a table.
|
||||
func columnExists(db *sql.DB, table, column string) bool {
|
||||
rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var cid int
|
||||
var name, ctype string
|
||||
var notnull, pk int
|
||||
var dflt sql.NullString
|
||||
if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil {
|
||||
return false
|
||||
}
|
||||
if name == column {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const schemaV2 = `
|
||||
CREATE TABLE IF NOT EXISTS server_profiles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -185,15 +223,16 @@ CREATE TABLE IF NOT EXISTS server_profiles (
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS connection_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
profile_id INTEGER NOT NULL,
|
||||
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at DATETIME,
|
||||
assigned_ip TEXT NOT NULL DEFAULT '',
|
||||
rx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||
tx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'connected',
|
||||
error_msg TEXT NOT NULL DEFAULT '',
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
profile_id INTEGER NOT NULL,
|
||||
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
ended_at DATETIME,
|
||||
assigned_ip TEXT NOT NULL DEFAULT '',
|
||||
assigned_ip6 TEXT NOT NULL DEFAULT '',
|
||||
rx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||
tx_bytes INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'connected',
|
||||
error_msg TEXT NOT NULL DEFAULT '',
|
||||
FOREIGN KEY (profile_id) REFERENCES server_profiles(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
+5
-5
@@ -21,13 +21,13 @@ func (s *Store) StartLog(profileID int64) (int64, error) {
|
||||
}
|
||||
|
||||
// FinishLog finalises a connection log entry with final stats.
|
||||
func (s *Store) FinishLog(id int64, status model.ConnectionStatus, assignedIP string, rxBytes, txBytes int64, errMsg string) error {
|
||||
func (s *Store) FinishLog(id int64, status model.ConnectionStatus, assignedIP, assignedIP6 string, rxBytes, txBytes int64, errMsg string) error {
|
||||
_, err := s.db.Exec(
|
||||
`UPDATE connection_logs SET
|
||||
ended_at = ?, assigned_ip = ?, rx_bytes = ?, tx_bytes = ?,
|
||||
ended_at = ?, assigned_ip = ?, assigned_ip6 = ?, rx_bytes = ?, tx_bytes = ?,
|
||||
status = ?, error_msg = ?
|
||||
WHERE id = ?`,
|
||||
time.Now(), assignedIP, rxBytes, txBytes, status, errMsg, id)
|
||||
time.Now(), assignedIP, assignedIP6, rxBytes, txBytes, status, errMsg, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("finish log %d: %w", id, err)
|
||||
}
|
||||
@@ -37,7 +37,7 @@ func (s *Store) FinishLog(id int64, status model.ConnectionStatus, assignedIP st
|
||||
// RecentLogs returns the most recent N connection logs for a profile.
|
||||
func (s *Store) RecentLogs(profileID int64, limit int) ([]model.ConnectionLog, error) {
|
||||
rows, err := s.db.Query(
|
||||
`SELECT id, profile_id, started_at, ended_at, assigned_ip,
|
||||
`SELECT id, profile_id, started_at, ended_at, assigned_ip, assigned_ip6,
|
||||
rx_bytes, tx_bytes, status, error_msg
|
||||
FROM connection_logs WHERE profile_id = ?
|
||||
ORDER BY started_at DESC LIMIT ?`,
|
||||
@@ -52,7 +52,7 @@ func (s *Store) RecentLogs(profileID int64, limit int) ([]model.ConnectionLog, e
|
||||
var l model.ConnectionLog
|
||||
var ended interface{}
|
||||
if err := rows.Scan(&l.ID, &l.ProfileID, &l.StartedAt, &ended,
|
||||
&l.AssignedIP, &l.RxBytes, &l.TxBytes, &l.Status, &l.ErrorMsg); err != nil {
|
||||
&l.AssignedIP, &l.AssignedIP6, &l.RxBytes, &l.TxBytes, &l.Status, &l.ErrorMsg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if t, ok := ended.(time.Time); ok {
|
||||
|
||||
Reference in New Issue
Block a user