fix: implement CloseByUUID and move v1 token from URL to POST body
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:
@@ -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 }
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user