fix: implement CloseByUUID and move v1 token from URL to POST body
CI / Test (push) Has been cancelled
CI / Build (amd64, linux) (push) Has been cancelled
CI / Build (arm64, linux) (push) Has been cancelled
CI / Build & Push Docker Image (push) Has been cancelled
CI / Release (push) Has been cancelled

CloseByUUID: add UUID→connIDs reverse index so ban-user actually
tears down existing TCP connections immediately instead of no-op.

v1 token: change config/user fetches from GET (token in URL, leaks
to logs) to POST (token in JSON body). Panel routes updated to any().
This commit is contained in:
will
2026-08-14 10:59:05 +09:00
parent 0e1f97ee03
commit 4bab53f20f
2 changed files with 45 additions and 10 deletions
+38 -7
View File
@@ -126,6 +126,7 @@ type ConnTracker struct {
users map[int]*userStats // userID → stats users map[int]*userStats // userID → stats
uuidMap map[string]int // UUID → userID (for lookup in RoutedConnection) uuidMap map[string]int // UUID → userID (for lookup in RoutedConnection)
connMap map[string]net.Conn // connID → conn (only for force-close support) connMap map[string]net.Conn // connID → conn (only for force-close support)
uuidConns map[string]map[string]struct{} // UUID → set of connIDs (reverse index for CloseByUUID)
idCounter atomic.Int64 idCounter atomic.Int64
@@ -147,6 +148,7 @@ func NewConnTracker(_ int) *ConnTracker {
users: make(map[int]*userStats), users: make(map[int]*userStats),
uuidMap: make(map[string]int), uuidMap: make(map[string]int),
connMap: make(map[string]net.Conn), connMap: make(map[string]net.Conn),
uuidConns: make(map[string]map[string]struct{}),
globalDevices: make(map[int]map[string]bool), globalDevices: make(map[int]map[string]bool),
} }
} }
@@ -239,6 +241,10 @@ func (t *ConnTracker) RoutedConnection(
// Store conn reference for force-close support // Store conn reference for force-close support
t.usersMu.Lock() t.usersMu.Lock()
t.connMap[connID] = conn t.connMap[connID] = conn
if _, ok := t.uuidConns[uuid]; !ok {
t.uuidConns[uuid] = make(map[string]struct{})
}
t.uuidConns[uuid][connID] = struct{}{}
t.usersMu.Unlock() t.usersMu.Unlock()
var lim *rate.Limiter var lim *rate.Limiter
@@ -252,6 +258,7 @@ func (t *ConnTracker) RoutedConnection(
us: us, us: us,
userID: uid, userID: uid,
connID: connID, connID: connID,
uuid: uuid,
sourceIP: sourceIP, sourceIP: sourceIP,
limiter: lim, limiter: lim,
ctx: ctx, ctx: ctx,
@@ -304,6 +311,7 @@ func (t *ConnTracker) RoutedPacketConnection(
us: us, us: us,
userID: uid, userID: uid,
connID: connID, connID: connID,
uuid: uuid,
sourceIP: sourceIP, sourceIP: sourceIP,
limiter: lim, limiter: lim,
ctx: ctx, ctx: ctx,
@@ -456,11 +464,25 @@ func (t *ConnTracker) CloseByID(id string) bool {
// CloseByUUID force-closes ALL connections for a given user UUID. // CloseByUUID force-closes ALL connections for a given user UUID.
func (t *ConnTracker) CloseByUUID(uuid string) int { func (t *ConnTracker) CloseByUUID(uuid string) int {
// This is a no-op for now — sing-box doesn't expose per-user connection t.usersMu.RLock()
// kill easily. The kernel's RemoveUsers removes the inbound user which ids, ok := t.uuidConns[uuid]
// prevents new connections, and existing connections will fail on next I/O. if !ok || len(ids) == 0 {
t.usersMu.RUnlock()
return 0 return 0
} }
conns := make([]net.Conn, 0, len(ids))
for id := range ids {
if c, ok := t.connMap[id]; ok {
conns = append(conns, c)
}
}
t.usersMu.RUnlock()
for _, c := range conns {
c.Close()
}
return len(conns)
}
// ActiveCount returns the total number of active connections. // ActiveCount returns the total number of active connections.
func (t *ConnTracker) ActiveCount() int { func (t *ConnTracker) ActiveCount() int {
@@ -475,10 +497,16 @@ func (t *ConnTracker) ActiveCount() int {
return total return total
} }
// removeConnRef removes the connection reference from connMap on close. // removeConnRef removes the connection reference from connMap and uuidConns on close.
func (t *ConnTracker) removeConnRef(connID string) { func (t *ConnTracker) removeConnRef(connID, uuid string) {
t.usersMu.Lock() t.usersMu.Lock()
delete(t.connMap, connID) delete(t.connMap, connID)
if ids, ok := t.uuidConns[uuid]; ok {
delete(ids, connID)
if len(ids) == 0 {
delete(t.uuidConns, uuid)
}
}
t.usersMu.Unlock() t.usersMu.Unlock()
} }
@@ -563,6 +591,7 @@ type trackedConn struct {
us *userStats // per-user stats (upload/download atomics + IP tracking) us *userStats // per-user stats (upload/download atomics + IP tracking)
userID int // user ID for device tracking userID int // user ID for device tracking
connID string connID string
uuid string
sourceIP string sourceIP string
limiter *rate.Limiter limiter *rate.Limiter
ctx context.Context ctx context.Context
@@ -636,7 +665,7 @@ func (c *trackedConn) Close() error {
if c.us != nil { if c.us != nil {
c.us.removeConn(c.sourceIP) c.us.removeConn(c.sourceIP)
} }
c.tracker.removeConnRef(c.connID) c.tracker.removeConnRef(c.connID, c.uuid)
} }
return c.Conn.Close() return c.Conn.Close()
} }
@@ -692,6 +721,7 @@ type trackedPacketConn struct {
us *userStats us *userStats
userID int userID int
connID string connID string
uuid string
sourceIP string sourceIP string
limiter *rate.Limiter limiter *rate.Limiter
ctx context.Context ctx context.Context
@@ -759,7 +789,7 @@ func (c *trackedPacketConn) Close() error {
if c.us != nil { if c.us != nil {
c.us.removeConn(c.sourceIP) c.us.removeConn(c.sourceIP)
} }
c.tracker.removeConnRef(c.connID) c.tracker.removeConnRef(c.connID, c.uuid)
} }
return c.PacketConn.Close() return c.PacketConn.Close()
} }
@@ -804,3 +834,4 @@ func (c *trackedPacketConn) UnwrapPacketWriter() (N.PacketWriter, []N.CountFunc)
func (c *trackedPacketConn) Upstream() any { return c.PacketConn } func (c *trackedPacketConn) Upstream() any { return c.PacketConn }
func (c *trackedPacketConn) ReaderReplaceable() bool { return true } func (c *trackedPacketConn) ReaderReplaceable() bool { return true }
func (c *trackedPacketConn) WriterReplaceable() bool { return true } func (c *trackedPacketConn) WriterReplaceable() bool { return true }
+6 -2
View File
@@ -200,6 +200,10 @@ func decodeWeakRaw(input map[string]interface{}, output interface{}) error {
// configPath returns the API path for config fetching. // configPath returns the API path for config fetching.
// Machine mode uses the v2 endpoint; legacy mode uses the v1 UniProxy endpoint. // Machine mode uses the v2 endpoint; legacy mode uses the v1 UniProxy endpoint.
// fetchMethod returns POST for all API calls (token stays in body, not URL).
// v2 already used POST; v1 legacy endpoints need panel route update to match.
func (c *Client) fetchMethod() string { return "POST" }
func (c *Client) configPath() string { func (c *Client) configPath() string {
if c.machineID > 0 { if c.machineID > 0 {
return "/api/v2/server/config" return "/api/v2/server/config"
@@ -217,7 +221,7 @@ func (c *Client) userPath() string {
// GetConfig fetches node configuration. Returns nil if not modified (304). // GetConfig fetches node configuration. Returns nil if not modified (304).
func (c *Client) GetConfig() (*NodeConfig, error) { func (c *Client) GetConfig() (*NodeConfig, error) {
resp, err := c.doRequest("GET", c.configPath(), nil, c.configETag) resp, err := c.doRequest(c.fetchMethod(), c.configPath(), nil, c.configETag)
if err != nil { if err != nil {
return nil, fmt.Errorf("get config: %w", err) return nil, fmt.Errorf("get config: %w", err)
} }
@@ -255,7 +259,7 @@ func (c *Client) GetConfig() (*NodeConfig, error) {
// GetUsers fetches available users. Returns nil if not modified (304). // GetUsers fetches available users. Returns nil if not modified (304).
func (c *Client) GetUsers() ([]User, error) { func (c *Client) GetUsers() ([]User, error) {
resp, err := c.doRequest("GET", c.userPath(), nil, c.userETag) resp, err := c.doRequest(c.fetchMethod(), c.userPath(), nil, c.userETag)
if err != nil { if err != nil {
return nil, fmt.Errorf("get users: %w", err) return nil, fmt.Errorf("get users: %w", err)
} }