diff --git a/internal/kernel/singbox/conntracker.go b/internal/kernel/singbox/conntracker.go index c7f9e9d..76fd79b 100644 --- a/internal/kernel/singbox/conntracker.go +++ b/internal/kernel/singbox/conntracker.go @@ -126,6 +126,7 @@ type ConnTracker struct { users map[int]*userStats // userID → stats uuidMap map[string]int // UUID → userID (for lookup in RoutedConnection) 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 @@ -147,6 +148,7 @@ func NewConnTracker(_ int) *ConnTracker { users: make(map[int]*userStats), uuidMap: make(map[string]int), connMap: make(map[string]net.Conn), + uuidConns: make(map[string]map[string]struct{}), globalDevices: make(map[int]map[string]bool), } } @@ -239,6 +241,10 @@ func (t *ConnTracker) RoutedConnection( // Store conn reference for force-close support t.usersMu.Lock() 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() var lim *rate.Limiter @@ -252,6 +258,7 @@ func (t *ConnTracker) RoutedConnection( us: us, userID: uid, connID: connID, + uuid: uuid, sourceIP: sourceIP, limiter: lim, ctx: ctx, @@ -304,6 +311,7 @@ func (t *ConnTracker) RoutedPacketConnection( us: us, userID: uid, connID: connID, + uuid: uuid, sourceIP: sourceIP, limiter: lim, ctx: ctx, @@ -456,10 +464,24 @@ func (t *ConnTracker) CloseByID(id string) bool { // CloseByUUID force-closes ALL connections for a given user UUID. func (t *ConnTracker) CloseByUUID(uuid string) int { - // This is a no-op for now — sing-box doesn't expose per-user connection - // kill easily. The kernel's RemoveUsers removes the inbound user which - // prevents new connections, and existing connections will fail on next I/O. - return 0 + t.usersMu.RLock() + ids, ok := t.uuidConns[uuid] + if !ok || len(ids) == 0 { + t.usersMu.RUnlock() + 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. @@ -475,10 +497,16 @@ func (t *ConnTracker) ActiveCount() int { return total } -// removeConnRef removes the connection reference from connMap on close. -func (t *ConnTracker) removeConnRef(connID string) { +// removeConnRef removes the connection reference from connMap and uuidConns on close. +func (t *ConnTracker) removeConnRef(connID, uuid string) { t.usersMu.Lock() 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() } @@ -563,6 +591,7 @@ type trackedConn struct { us *userStats // per-user stats (upload/download atomics + IP tracking) userID int // user ID for device tracking connID string + uuid string sourceIP string limiter *rate.Limiter ctx context.Context @@ -636,7 +665,7 @@ func (c *trackedConn) Close() error { if c.us != nil { c.us.removeConn(c.sourceIP) } - c.tracker.removeConnRef(c.connID) + c.tracker.removeConnRef(c.connID, c.uuid) } return c.Conn.Close() } @@ -692,6 +721,7 @@ type trackedPacketConn struct { us *userStats userID int connID string + uuid string sourceIP string limiter *rate.Limiter ctx context.Context @@ -759,7 +789,7 @@ func (c *trackedPacketConn) Close() error { if c.us != nil { c.us.removeConn(c.sourceIP) } - c.tracker.removeConnRef(c.connID) + c.tracker.removeConnRef(c.connID, c.uuid) } 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) ReaderReplaceable() bool { return true } func (c *trackedPacketConn) WriterReplaceable() bool { return true } + diff --git a/internal/panel/client.go b/internal/panel/client.go index a725afc..9ed0a83 100644 --- a/internal/panel/client.go +++ b/internal/panel/client.go @@ -200,6 +200,10 @@ func decodeWeakRaw(input map[string]interface{}, output interface{}) error { // configPath returns the API path for config fetching. // 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 { if c.machineID > 0 { return "/api/v2/server/config" @@ -217,7 +221,7 @@ func (c *Client) userPath() string { // GetConfig fetches node configuration. Returns nil if not modified (304). 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 { 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). 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 { return nil, fmt.Errorf("get users: %w", err) }