package xboard import ( "net" "sort" "strconv" "time" "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/service/ssmapi" N "github.com/sagernet/sing/common/network" ) const aliveIPRetention = 5 * time.Minute var _ adapter.SSMTracker = (*xboardTracker)(nil) type xboardTracker struct { service *Service traffic *ssmapi.TrafficManager } func newXboardTracker(service *Service) *xboardTracker { return &xboardTracker{ service: service, traffic: ssmapi.NewTrafficManager(), } } func (t *xboardTracker) TrafficManager() *ssmapi.TrafficManager { return t.traffic } func (t *xboardTracker) TrackConnection(conn net.Conn, metadata adapter.InboundContext) net.Conn { t.service.recordAliveIP(metadata) return t.traffic.TrackConnection(conn, metadata) } func (t *xboardTracker) TrackPacketConnection(conn N.PacketConn, metadata adapter.InboundContext) N.PacketConn { t.service.recordAliveIP(metadata) return t.traffic.TrackPacketConnection(conn, metadata) } func (s *Service) recordAliveIP(metadata adapter.InboundContext) { if metadata.User == "" || !metadata.Source.IsValid() || !metadata.Source.Addr.IsValid() { return } clientIP := metadata.Source.Addr.Unmap().String() if clientIP == "" { return } s.access.Lock() defer s.access.Unlock() ipSet, exists := s.aliveUsers[metadata.User] if !exists { ipSet = make(map[string]time.Time) s.aliveUsers[metadata.User] = ipSet } ipSet[clientIP] = time.Now() } func (s *Service) buildAlivePayload() map[string][]string { now := time.Now() s.access.Lock() defer s.access.Unlock() payload := make(map[string][]string) for userName, ipSet := range s.aliveUsers { userMeta, exists := s.localUsers[userName] if !exists || userMeta.ID == 0 { delete(s.aliveUsers, userName) continue } activeIPs := make([]string, 0, len(ipSet)) for ip, lastSeen := range ipSet { if now.Sub(lastSeen) > aliveIPRetention { delete(ipSet, ip) continue } activeIPs = append(activeIPs, ip) } if len(ipSet) == 0 { delete(s.aliveUsers, userName) } if len(activeIPs) == 0 { continue } sort.Strings(activeIPs) payload[strconv.Itoa(userMeta.ID)] = activeIPs } return payload }