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
|
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,10 +464,24 @@ 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 {
|
||||||
return 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.
|
// ActiveCount returns the total number of active connections.
|
||||||
@@ -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 }
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user