diff --git a/frontend/src/locales/en.ts b/frontend/src/locales/en.ts index f935dc8..5f578cc 100644 --- a/frontend/src/locales/en.ts +++ b/frontend/src/locales/en.ts @@ -154,6 +154,7 @@ export default { user: 'User', ipv4: 'IPv4', ipv6: 'IPv6', + realIp: 'Real IP', connectTime: 'Connected At', noOnlineClients: 'No online clients', staticIpReservation: 'Static IP Reservation', diff --git a/frontend/src/locales/zh.ts b/frontend/src/locales/zh.ts index 226381f..18ba380 100644 --- a/frontend/src/locales/zh.ts +++ b/frontend/src/locales/zh.ts @@ -153,6 +153,7 @@ export default { user: '用户', ipv4: 'IPv4', ipv6: 'IPv6', + realIp: '真实 IP', connectTime: '连接时间', noOnlineClients: '暂无在线客户端', staticIpReservation: '静态 IP 预留', diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 456ddef..fcac6d5 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -25,6 +25,7 @@ interface ClientInfo { username: string ip: string ip6?: string + real_ip: string connected_at: string rx_bytes: number tx_bytes: number @@ -215,6 +216,7 @@ function handleStatClick(route: string) { {{ t('vpn.user') }} + {{ t('vpn.realIp') }} {{ t('vpn.ipv4') }} {{ t('vpn.ipv6') }} {{ t('vpn.connectTime') }} @@ -225,10 +227,11 @@ function handleStatClick(route: string) { - {{ t('vpn.noOnlineClients') }} + {{ t('vpn.noOnlineClients') }} {{ c.username }} + {{ c.real_ip || '-' }} {{ c.ip }} {{ c.ip6 || '-' }} {{ c.connected_at }} diff --git a/frontend/src/views/ProfileView.vue b/frontend/src/views/ProfileView.vue index ec75245..8c39474 100644 --- a/frontend/src/views/ProfileView.vue +++ b/frontend/src/views/ProfileView.vue @@ -12,6 +12,7 @@ const { t } = useI18n() interface VpnConnection { ip: string ip6?: string + real_ip: string connected_at: string } const vpnConnections = ref([]) @@ -155,6 +156,7 @@ async function handleChangePassword() { + @@ -162,9 +164,10 @@ async function handleChangePassword() { - + + diff --git a/frontend/src/views/VpnView.vue b/frontend/src/views/VpnView.vue index e80bcc1..392a568 100644 --- a/frontend/src/views/VpnView.vue +++ b/frontend/src/views/VpnView.vue @@ -23,6 +23,7 @@ interface ClientInfo { username: string ip: string ip6?: string + real_ip: string connected_at: string } interface Status { diff --git a/internal/config/config.go b/internal/config/config.go index 707248d..b0a7f09 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -10,12 +10,14 @@ import ( ) type WebConfig struct { - Port int `yaml:"port"` - Sock string `yaml:"sock"` - SockMode string `yaml:"sock_mode"` - SockGroup string `yaml:"sock_group"` - SockDirMode string `yaml:"sock_dir_mode"` - JWTSecret string `yaml:"jwt_secret"` + Port int `yaml:"port"` + Sock string `yaml:"sock"` + SockMode string `yaml:"sock_mode"` + SockGroup string `yaml:"sock_group"` + SockDirMode string `yaml:"sock_dir_mode"` + JWTSecret string `yaml:"jwt_secret"` + RealIPHeaders []string `yaml:"real_ip_headers"` + TrustedProxies []string `yaml:"trusted_proxies"` } type DatabaseConfig struct { @@ -36,6 +38,7 @@ func defaultConfig() *Config { Sock: "web.sock", SockMode: "0666", SockDirMode: "0755", + RealIPHeaders: []string{"CF-Connecting-IP", "X-Real-IP", "X-Forwarded-For"}, }, Database: DatabaseConfig{ Type: "sqlite", diff --git a/internal/handler/auth.go b/internal/handler/auth.go index ca46312..58a69ac 100644 --- a/internal/handler/auth.go +++ b/internal/handler/auth.go @@ -80,7 +80,7 @@ func Login(c *gin.Context) { session := model.Session{ SessionID: sessionID, UserID: user.ID, - IP: c.ClientIP(), + IP: middleware.GetRealIP(c), UserAgent: c.GetHeader("User-Agent"), ExpiresAt: time.Now().Add(24 * time.Hour), } diff --git a/internal/handler/vpn.go b/internal/handler/vpn.go index 04976dc..af75b16 100644 --- a/internal/handler/vpn.go +++ b/internal/handler/vpn.go @@ -211,6 +211,7 @@ func UpdateVpnSettings(c *gin.Context) { type myVpnConnection struct { IP string `json:"ip"` IP6 string `json:"ip6,omitempty"` + RealIP string `json:"real_ip"` ConnectedAt string `json:"connected_at"` } @@ -232,6 +233,7 @@ func GetMyVpnConnections(c *gin.Context) { connections = append(connections, myVpnConnection{ IP: ci.IP, IP6: ci.IP6, + RealIP: ci.RealIP, ConnectedAt: ci.ConnectedAt, }) } diff --git a/internal/middleware/realip.go b/internal/middleware/realip.go new file mode 100644 index 0000000..81a1ae1 --- /dev/null +++ b/internal/middleware/realip.go @@ -0,0 +1,28 @@ +package middleware + +import ( + "net" + "strings" + + "github.com/gin-gonic/gin" +) + +var realIPHeaders []string + +func SetRealIPHeaders(headers []string) { + realIPHeaders = headers +} + +func GetRealIP(c *gin.Context) string { + for _, header := range realIPHeaders { + val := c.GetHeader(header) + if val == "" { + continue + } + ip := strings.TrimSpace(strings.Split(val, ",")[0]) + if ip != "" && net.ParseIP(ip) != nil { + return ip + } + } + return c.ClientIP() +} diff --git a/internal/vpn/handler.go b/internal/vpn/handler.go index 56fbb86..38f62d6 100644 --- a/internal/vpn/handler.go +++ b/internal/vpn/handler.go @@ -31,6 +31,7 @@ var upgrader = websocket.Upgrader{ func HandleWS(c *gin.Context) { tokenStr := c.Query("token") + realIP := middleware.GetRealIP(c) conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { @@ -51,11 +52,11 @@ func HandleWS(c *gin.Context) { conn.Close() return } - runTunnel(conn, &u) + runTunnel(conn, &u, realIP) return } - user, err := authenticate(conn, db.DB, c.ClientIP()) + user, err := authenticate(conn, db.DB, realIP) if err != nil { log.Printf("认证读取失败: %v", err) conn.Close() @@ -65,5 +66,5 @@ func HandleWS(c *gin.Context) { return } - runTunnel(conn, user) + runTunnel(conn, user, realIP) } diff --git a/internal/vpn/service.go b/internal/vpn/service.go index 6fca95b..e79d74b 100644 --- a/internal/vpn/service.go +++ b/internal/vpn/service.go @@ -435,6 +435,7 @@ type ClientInfo struct { Username string `json:"username"` IP string `json:"ip"` IP6 string `json:"ip6,omitempty"` + RealIP string `json:"real_ip"` ConnectedAt string `json:"connected_at"` RxBytes int64 `json:"rx_bytes"` TxBytes int64 `json:"tx_bytes"` diff --git a/internal/vpn/tunnel.go b/internal/vpn/tunnel.go index 4174c1c..3406e85 100644 --- a/internal/vpn/tunnel.go +++ b/internal/vpn/tunnel.go @@ -35,6 +35,7 @@ type tunnelConn struct { svc *VpnService assignedIP net.IP assignedIP6 net.IP + realIP string connectedAt time.Time writeMu sync.Mutex ready atomic.Bool @@ -91,6 +92,7 @@ func (c *tunnelConn) info() ClientInfo { UserID: c.user.ID, Username: c.user.Username, IP: c.assignedIP.String(), + RealIP: c.realIP, ConnectedAt: c.connectedAt.Format("2006-01-02 15:04:05"), RxBytes: c.rxBytes.Load(), TxBytes: c.txBytes.Load(), @@ -101,7 +103,7 @@ func (c *tunnelConn) info() ClientInfo { return ci } -func runTunnel(conn *websocket.Conn, user *model.User) { +func runTunnel(conn *websocket.Conn, user *model.User, realIP string) { defer conn.Close() if VPN == nil || !VPN.Running() { @@ -143,6 +145,7 @@ func runTunnel(conn *websocket.Conn, user *model.User) { svc: VPN, assignedIP: ip4, assignedIP6: ip6, + realIP: realIP, connectedAt: time.Now(), } diff --git a/main.go b/main.go index c2d3b9a..2992a05 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ func main() { } middleware.SetJWTSecret(cfg.Web.JWTSecret) + middleware.SetRealIPHeaders(cfg.Web.RealIPHeaders) if err := db.Init(&cfg.Database); err != nil { log.Fatalf("数据库初始化失败: %v", err) @@ -38,6 +39,12 @@ func main() { r := gin.Default() + if len(cfg.Web.TrustedProxies) > 0 { + _ = r.SetTrustedProxies(cfg.Web.TrustedProxies) + } else { + _ = r.SetTrustedProxies(nil) + } + router.Setup(r) if cfg.Web.Port == 0 && cfg.Web.Sock == "" {
{{ t('vpn.realIp') }} {{ t('vpn.ipv4') }} {{ t('vpn.ipv6') }} {{ t('vpn.connectTime') }}
{{ t('profile.noConnections') }}{{ t('profile.noConnections') }}
{{ c.real_ip || '-' }} {{ c.ip }} {{ c.ip6 || '-' }} {{ c.connected_at }}