mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-29 20:11:28 +00:00
platform: Prepare connections list
This commit is contained in:
parent
2f0f6144a6
commit
a35d223dd6
1
box.go
1
box.go
|
@ -111,6 +111,7 @@ func New(options Options) (*Box, error) {
|
||||||
ctx,
|
ctx,
|
||||||
router,
|
router,
|
||||||
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
inboundOptions,
|
inboundOptions,
|
||||||
options.PlatformInterface,
|
options.PlatformInterface,
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,6 +32,12 @@ const (
|
||||||
|
|
||||||
func ProxyDisplayName(proxyType string) string {
|
func ProxyDisplayName(proxyType string) string {
|
||||||
switch proxyType {
|
switch proxyType {
|
||||||
|
case TypeTun:
|
||||||
|
return "TUN"
|
||||||
|
case TypeRedirect:
|
||||||
|
return "Redirect"
|
||||||
|
case TypeTProxy:
|
||||||
|
return "TProxy"
|
||||||
case TypeDirect:
|
case TypeDirect:
|
||||||
return "Direct"
|
return "Direct"
|
||||||
case TypeBlock:
|
case TypeBlock:
|
||||||
|
@ -42,6 +48,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||||
return "SOCKS"
|
return "SOCKS"
|
||||||
case TypeHTTP:
|
case TypeHTTP:
|
||||||
return "HTTP"
|
return "HTTP"
|
||||||
|
case TypeMixed:
|
||||||
|
return "Mixed"
|
||||||
case TypeShadowsocks:
|
case TypeShadowsocks:
|
||||||
return "Shadowsocks"
|
return "Shadowsocks"
|
||||||
case TypeVMess:
|
case TypeVMess:
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
"github.com/go-chi/render"
|
"github.com/go-chi/render"
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
|
func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
|
||||||
|
@ -76,10 +77,10 @@ func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseW
|
||||||
|
|
||||||
func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
func closeConnection(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
id := chi.URLParam(r, "id")
|
id := uuid.FromStringOrNil(chi.URLParam(r, "id"))
|
||||||
snapshot := trafficManager.Snapshot()
|
snapshot := trafficManager.Snapshot()
|
||||||
for _, c := range snapshot.Connections {
|
for _, c := range snapshot.Connections {
|
||||||
if id == c.ID() {
|
if id == c.Metadata().ID {
|
||||||
c.Close()
|
c.Close()
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,6 @@ import (
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
F "github.com/sagernet/sing/common/format"
|
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
|
@ -218,58 +217,15 @@ func (s *Server) TrafficManager() *trafficontrol.Manager {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) {
|
func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) {
|
||||||
tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule)
|
tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
|
||||||
return tracker, tracker
|
return tracker, tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) {
|
func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) (N.PacketConn, adapter.Tracker) {
|
||||||
tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule)
|
tracker := trafficontrol.NewUDPTracker(conn, s.trafficManager, metadata, s.router, matchedRule)
|
||||||
return tracker, tracker
|
return tracker, tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
func castMetadata(metadata adapter.InboundContext) trafficontrol.Metadata {
|
|
||||||
var inbound string
|
|
||||||
if metadata.Inbound != "" {
|
|
||||||
inbound = metadata.InboundType + "/" + metadata.Inbound
|
|
||||||
} else {
|
|
||||||
inbound = metadata.InboundType
|
|
||||||
}
|
|
||||||
var domain string
|
|
||||||
if metadata.Domain != "" {
|
|
||||||
domain = metadata.Domain
|
|
||||||
} else {
|
|
||||||
domain = metadata.Destination.Fqdn
|
|
||||||
}
|
|
||||||
var processPath string
|
|
||||||
if metadata.ProcessInfo != nil {
|
|
||||||
if metadata.ProcessInfo.ProcessPath != "" {
|
|
||||||
processPath = metadata.ProcessInfo.ProcessPath
|
|
||||||
} else if metadata.ProcessInfo.PackageName != "" {
|
|
||||||
processPath = metadata.ProcessInfo.PackageName
|
|
||||||
}
|
|
||||||
if processPath == "" {
|
|
||||||
if metadata.ProcessInfo.UserId != -1 {
|
|
||||||
processPath = F.ToString(metadata.ProcessInfo.UserId)
|
|
||||||
}
|
|
||||||
} else if metadata.ProcessInfo.User != "" {
|
|
||||||
processPath = F.ToString(processPath, " (", metadata.ProcessInfo.User, ")")
|
|
||||||
} else if metadata.ProcessInfo.UserId != -1 {
|
|
||||||
processPath = F.ToString(processPath, " (", metadata.ProcessInfo.UserId, ")")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return trafficontrol.Metadata{
|
|
||||||
NetWork: metadata.Network,
|
|
||||||
Type: inbound,
|
|
||||||
SrcIP: metadata.Source.Addr,
|
|
||||||
DstIP: metadata.Destination.Addr,
|
|
||||||
SrcPort: F.ToString(metadata.Source.Port),
|
|
||||||
DstPort: F.ToString(metadata.Destination.Port),
|
|
||||||
Host: domain,
|
|
||||||
DNSMode: "normal",
|
|
||||||
ProcessPath: processPath,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func authentication(serverSecret string) func(next http.Handler) http.Handler {
|
func authentication(serverSecret string) func(next http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
|
@ -2,10 +2,17 @@ package trafficontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
|
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/atomic"
|
"github.com/sagernet/sing/common/atomic"
|
||||||
|
"github.com/sagernet/sing/common/json"
|
||||||
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Manager struct {
|
type Manager struct {
|
||||||
|
@ -16,7 +23,9 @@ type Manager struct {
|
||||||
uploadTotal atomic.Int64
|
uploadTotal atomic.Int64
|
||||||
downloadTotal atomic.Int64
|
downloadTotal atomic.Int64
|
||||||
|
|
||||||
connections compatible.Map[string, tracker]
|
connections compatible.Map[uuid.UUID, Tracker]
|
||||||
|
closedConnectionsAccess sync.Mutex
|
||||||
|
closedConnections list.List[TrackerMetadata]
|
||||||
ticker *time.Ticker
|
ticker *time.Ticker
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
// process *process.Process
|
// process *process.Process
|
||||||
|
@ -33,12 +42,22 @@ func NewManager() *Manager {
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Join(c tracker) {
|
func (m *Manager) Join(c Tracker) {
|
||||||
m.connections.Store(c.ID(), c)
|
m.connections.Store(c.Metadata().ID, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Leave(c tracker) {
|
func (m *Manager) Leave(c Tracker) {
|
||||||
m.connections.Delete(c.ID())
|
metadata := c.Metadata()
|
||||||
|
_, loaded := m.connections.LoadAndDelete(metadata.ID)
|
||||||
|
if loaded {
|
||||||
|
metadata.ClosedAt = time.Now()
|
||||||
|
m.closedConnectionsAccess.Lock()
|
||||||
|
defer m.closedConnectionsAccess.Unlock()
|
||||||
|
if m.closedConnections.Len() >= 1000 {
|
||||||
|
m.closedConnections.PopFront()
|
||||||
|
}
|
||||||
|
m.closedConnections.PushBack(metadata)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) PushUploaded(size int64) {
|
func (m *Manager) PushUploaded(size int64) {
|
||||||
|
@ -59,14 +78,39 @@ func (m *Manager) Total() (up int64, down int64) {
|
||||||
return m.uploadTotal.Load(), m.downloadTotal.Load()
|
return m.uploadTotal.Load(), m.downloadTotal.Load()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Manager) Connections() int {
|
func (m *Manager) ConnectionsLen() int {
|
||||||
return m.connections.Len()
|
return m.connections.Len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Connections() []TrackerMetadata {
|
||||||
|
var connections []TrackerMetadata
|
||||||
|
m.connections.Range(func(_ uuid.UUID, value Tracker) bool {
|
||||||
|
connections = append(connections, value.Metadata())
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
return connections
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) ClosedConnections() []TrackerMetadata {
|
||||||
|
m.closedConnectionsAccess.Lock()
|
||||||
|
defer m.closedConnectionsAccess.Unlock()
|
||||||
|
return m.closedConnections.Array()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Connection(id uuid.UUID) Tracker {
|
||||||
|
connection, loaded := m.connections.Load(id)
|
||||||
|
if !loaded {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return connection
|
||||||
|
}
|
||||||
|
|
||||||
func (m *Manager) Snapshot() *Snapshot {
|
func (m *Manager) Snapshot() *Snapshot {
|
||||||
var connections []tracker
|
var connections []Tracker
|
||||||
m.connections.Range(func(_ string, value tracker) bool {
|
m.connections.Range(func(_ uuid.UUID, value Tracker) bool {
|
||||||
|
if value.Metadata().OutboundType != C.TypeDNS {
|
||||||
connections = append(connections, value)
|
connections = append(connections, value)
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -75,8 +119,8 @@ func (m *Manager) Snapshot() *Snapshot {
|
||||||
m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased
|
m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased
|
||||||
|
|
||||||
return &Snapshot{
|
return &Snapshot{
|
||||||
UploadTotal: m.uploadTotal.Load(),
|
Upload: m.uploadTotal.Load(),
|
||||||
DownloadTotal: m.downloadTotal.Load(),
|
Download: m.downloadTotal.Load(),
|
||||||
Connections: connections,
|
Connections: connections,
|
||||||
Memory: m.memory,
|
Memory: m.memory,
|
||||||
}
|
}
|
||||||
|
@ -114,8 +158,17 @@ func (m *Manager) Close() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Snapshot struct {
|
type Snapshot struct {
|
||||||
DownloadTotal int64 `json:"downloadTotal"`
|
Download int64
|
||||||
UploadTotal int64 `json:"uploadTotal"`
|
Upload int64
|
||||||
Connections []tracker `json:"connections"`
|
Connections []Tracker
|
||||||
Memory uint64 `json:"memory"`
|
Memory uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Snapshot) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(map[string]any{
|
||||||
|
"downloadTotal": s.Download,
|
||||||
|
"uploadTotal": s.Upload,
|
||||||
|
"connections": common.Map(s.Connections, func(t Tracker) TrackerMetadata { return t.Metadata() }),
|
||||||
|
"memory": s.Memory,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,97 +2,135 @@ package trafficontrol
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/atomic"
|
"github.com/sagernet/sing/common/atomic"
|
||||||
"github.com/sagernet/sing/common/bufio"
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
"github.com/gofrs/uuid/v5"
|
"github.com/gofrs/uuid/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Metadata struct {
|
type TrackerMetadata struct {
|
||||||
NetWork string `json:"network"`
|
ID uuid.UUID
|
||||||
Type string `json:"type"`
|
Metadata adapter.InboundContext
|
||||||
SrcIP netip.Addr `json:"sourceIP"`
|
CreatedAt time.Time
|
||||||
DstIP netip.Addr `json:"destinationIP"`
|
ClosedAt time.Time
|
||||||
SrcPort string `json:"sourcePort"`
|
Upload *atomic.Int64
|
||||||
DstPort string `json:"destinationPort"`
|
Download *atomic.Int64
|
||||||
Host string `json:"host"`
|
Chain []string
|
||||||
DNSMode string `json:"dnsMode"`
|
Rule adapter.Rule
|
||||||
ProcessPath string `json:"processPath"`
|
Outbound string
|
||||||
|
OutboundType string
|
||||||
}
|
}
|
||||||
|
|
||||||
type tracker interface {
|
func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
|
||||||
ID() string
|
var inbound string
|
||||||
Close() error
|
if t.Metadata.Inbound != "" {
|
||||||
Leave()
|
inbound = t.Metadata.InboundType + "/" + t.Metadata.Inbound
|
||||||
|
} else {
|
||||||
|
inbound = t.Metadata.InboundType
|
||||||
}
|
}
|
||||||
|
var domain string
|
||||||
type trackerInfo struct {
|
if t.Metadata.Domain != "" {
|
||||||
UUID uuid.UUID `json:"id"`
|
domain = t.Metadata.Domain
|
||||||
Metadata Metadata `json:"metadata"`
|
} else {
|
||||||
UploadTotal *atomic.Int64 `json:"upload"`
|
domain = t.Metadata.Destination.Fqdn
|
||||||
DownloadTotal *atomic.Int64 `json:"download"`
|
}
|
||||||
Start time.Time `json:"start"`
|
var processPath string
|
||||||
Chain []string `json:"chains"`
|
if t.Metadata.ProcessInfo != nil {
|
||||||
Rule string `json:"rule"`
|
if t.Metadata.ProcessInfo.ProcessPath != "" {
|
||||||
RulePayload string `json:"rulePayload"`
|
processPath = t.Metadata.ProcessInfo.ProcessPath
|
||||||
|
} else if t.Metadata.ProcessInfo.PackageName != "" {
|
||||||
|
processPath = t.Metadata.ProcessInfo.PackageName
|
||||||
|
}
|
||||||
|
if processPath == "" {
|
||||||
|
if t.Metadata.ProcessInfo.UserId != -1 {
|
||||||
|
processPath = F.ToString(t.Metadata.ProcessInfo.UserId)
|
||||||
|
}
|
||||||
|
} else if t.Metadata.ProcessInfo.User != "" {
|
||||||
|
processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.User, ")")
|
||||||
|
} else if t.Metadata.ProcessInfo.UserId != -1 {
|
||||||
|
processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var rule string
|
||||||
|
if t.Rule != nil {
|
||||||
|
rule = F.ToString(t.Rule, " => ", t.Rule.Outbound())
|
||||||
|
} else {
|
||||||
|
rule = "final"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t trackerInfo) MarshalJSON() ([]byte, error) {
|
|
||||||
return json.Marshal(map[string]any{
|
return json.Marshal(map[string]any{
|
||||||
"id": t.UUID.String(),
|
"id": t.ID,
|
||||||
"metadata": t.Metadata,
|
"metadata": map[string]any{
|
||||||
"upload": t.UploadTotal.Load(),
|
"network": t.Metadata.Network,
|
||||||
"download": t.DownloadTotal.Load(),
|
"type": inbound,
|
||||||
"start": t.Start,
|
"sourceIP": t.Metadata.Source.Addr,
|
||||||
|
"destinationIP": t.Metadata.Destination.Addr,
|
||||||
|
"sourcePort": F.ToString(t.Metadata.Source.Port),
|
||||||
|
"destinationPort": F.ToString(t.Metadata.Destination.Port),
|
||||||
|
"host": domain,
|
||||||
|
"dnsMode": "normal",
|
||||||
|
"processPath": processPath,
|
||||||
|
},
|
||||||
|
"upload": t.Upload.Load(),
|
||||||
|
"download": t.Download.Load(),
|
||||||
|
"start": t.CreatedAt,
|
||||||
"chains": t.Chain,
|
"chains": t.Chain,
|
||||||
"rule": t.Rule,
|
"rule": rule,
|
||||||
"rulePayload": t.RulePayload,
|
"rulePayload": "",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
type tcpTracker struct {
|
type Tracker interface {
|
||||||
N.ExtendedConn `json:"-"`
|
adapter.Tracker
|
||||||
*trackerInfo
|
Metadata() TrackerMetadata
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPConn struct {
|
||||||
|
N.ExtendedConn
|
||||||
|
metadata TrackerMetadata
|
||||||
manager *Manager
|
manager *Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tt *tcpTracker) ID() string {
|
func (tt *TCPConn) Metadata() TrackerMetadata {
|
||||||
return tt.UUID.String()
|
return tt.metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tt *tcpTracker) Close() error {
|
func (tt *TCPConn) Close() error {
|
||||||
tt.manager.Leave(tt)
|
tt.manager.Leave(tt)
|
||||||
return tt.ExtendedConn.Close()
|
return tt.ExtendedConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tt *tcpTracker) Leave() {
|
func (tt *TCPConn) Leave() {
|
||||||
tt.manager.Leave(tt)
|
tt.manager.Leave(tt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tt *tcpTracker) Upstream() any {
|
func (tt *TCPConn) Upstream() any {
|
||||||
return tt.ExtendedConn
|
return tt.ExtendedConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tt *tcpTracker) ReaderReplaceable() bool {
|
func (tt *TCPConn) ReaderReplaceable() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tt *tcpTracker) WriterReplaceable() bool {
|
func (tt *TCPConn) WriterReplaceable() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *tcpTracker {
|
func NewTCPTracker(conn net.Conn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *TCPConn {
|
||||||
uuid, _ := uuid.NewV4()
|
id, _ := uuid.NewV4()
|
||||||
|
var (
|
||||||
var chain []string
|
chain []string
|
||||||
var next string
|
next string
|
||||||
|
outbound string
|
||||||
|
outboundType string
|
||||||
|
)
|
||||||
if rule == nil {
|
if rule == nil {
|
||||||
if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
|
if defaultOutbound, err := router.DefaultOutbound(N.NetworkTCP); err == nil {
|
||||||
next = defaultOutbound.Tag()
|
next = defaultOutbound.Tag()
|
||||||
|
@ -106,17 +144,17 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
|
||||||
if !loaded {
|
if !loaded {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
outbound = detour.Tag()
|
||||||
|
outboundType = detour.Type()
|
||||||
group, isGroup := detour.(adapter.OutboundGroup)
|
group, isGroup := detour.(adapter.OutboundGroup)
|
||||||
if !isGroup {
|
if !isGroup {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
next = group.Now()
|
next = group.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
upload := new(atomic.Int64)
|
upload := new(atomic.Int64)
|
||||||
download := new(atomic.Int64)
|
download := new(atomic.Int64)
|
||||||
|
tracker := &TCPConn{
|
||||||
t := &tcpTracker{
|
|
||||||
ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) {
|
ExtendedConn: bufio.NewCounterConn(conn, []N.CountFunc{func(n int64) {
|
||||||
upload.Add(n)
|
upload.Add(n)
|
||||||
manager.PushUploaded(n)
|
manager.PushUploaded(n)
|
||||||
|
@ -124,64 +162,62 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
|
||||||
download.Add(n)
|
download.Add(n)
|
||||||
manager.PushDownloaded(n)
|
manager.PushDownloaded(n)
|
||||||
}}),
|
}}),
|
||||||
manager: manager,
|
metadata: TrackerMetadata{
|
||||||
trackerInfo: &trackerInfo{
|
ID: id,
|
||||||
UUID: uuid,
|
|
||||||
Start: time.Now(),
|
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
Upload: upload,
|
||||||
|
Download: download,
|
||||||
Chain: common.Reverse(chain),
|
Chain: common.Reverse(chain),
|
||||||
Rule: "",
|
Rule: rule,
|
||||||
UploadTotal: upload,
|
Outbound: outbound,
|
||||||
DownloadTotal: download,
|
OutboundType: outboundType,
|
||||||
},
|
},
|
||||||
|
manager: manager,
|
||||||
|
}
|
||||||
|
manager.Join(tracker)
|
||||||
|
return tracker
|
||||||
}
|
}
|
||||||
|
|
||||||
if rule != nil {
|
type UDPConn struct {
|
||||||
t.trackerInfo.Rule = rule.String() + " => " + rule.Outbound()
|
|
||||||
} else {
|
|
||||||
t.trackerInfo.Rule = "final"
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.Join(t)
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
type udpTracker struct {
|
|
||||||
N.PacketConn `json:"-"`
|
N.PacketConn `json:"-"`
|
||||||
*trackerInfo
|
metadata TrackerMetadata
|
||||||
manager *Manager
|
manager *Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ut *udpTracker) ID() string {
|
func (ut *UDPConn) Metadata() TrackerMetadata {
|
||||||
return ut.UUID.String()
|
return ut.metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ut *udpTracker) Close() error {
|
func (ut *UDPConn) Close() error {
|
||||||
ut.manager.Leave(ut)
|
ut.manager.Leave(ut)
|
||||||
return ut.PacketConn.Close()
|
return ut.PacketConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ut *udpTracker) Leave() {
|
func (ut *UDPConn) Leave() {
|
||||||
ut.manager.Leave(ut)
|
ut.manager.Leave(ut)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ut *udpTracker) Upstream() any {
|
func (ut *UDPConn) Upstream() any {
|
||||||
return ut.PacketConn
|
return ut.PacketConn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ut *udpTracker) ReaderReplaceable() bool {
|
func (ut *UDPConn) ReaderReplaceable() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ut *udpTracker) WriterReplaceable() bool {
|
func (ut *UDPConn) WriterReplaceable() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, router adapter.Router, rule adapter.Rule) *udpTracker {
|
func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata adapter.InboundContext, router adapter.Router, rule adapter.Rule) *UDPConn {
|
||||||
uuid, _ := uuid.NewV4()
|
id, _ := uuid.NewV4()
|
||||||
|
var (
|
||||||
var chain []string
|
chain []string
|
||||||
var next string
|
next string
|
||||||
|
outbound string
|
||||||
|
outboundType string
|
||||||
|
)
|
||||||
if rule == nil {
|
if rule == nil {
|
||||||
if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
|
if defaultOutbound, err := router.DefaultOutbound(N.NetworkUDP); err == nil {
|
||||||
next = defaultOutbound.Tag()
|
next = defaultOutbound.Tag()
|
||||||
|
@ -195,17 +231,17 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
|
||||||
if !loaded {
|
if !loaded {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
outbound = detour.Tag()
|
||||||
|
outboundType = detour.Type()
|
||||||
group, isGroup := detour.(adapter.OutboundGroup)
|
group, isGroup := detour.(adapter.OutboundGroup)
|
||||||
if !isGroup {
|
if !isGroup {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
next = group.Now()
|
next = group.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
upload := new(atomic.Int64)
|
upload := new(atomic.Int64)
|
||||||
download := new(atomic.Int64)
|
download := new(atomic.Int64)
|
||||||
|
trackerConn := &UDPConn{
|
||||||
ut := &udpTracker{
|
|
||||||
PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) {
|
PacketConn: bufio.NewCounterPacketConn(conn, []N.CountFunc{func(n int64) {
|
||||||
upload.Add(n)
|
upload.Add(n)
|
||||||
manager.PushUploaded(n)
|
manager.PushUploaded(n)
|
||||||
|
@ -213,24 +249,19 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
|
||||||
download.Add(n)
|
download.Add(n)
|
||||||
manager.PushDownloaded(n)
|
manager.PushDownloaded(n)
|
||||||
}}),
|
}}),
|
||||||
manager: manager,
|
metadata: TrackerMetadata{
|
||||||
trackerInfo: &trackerInfo{
|
ID: id,
|
||||||
UUID: uuid,
|
|
||||||
Start: time.Now(),
|
|
||||||
Metadata: metadata,
|
Metadata: metadata,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
Upload: upload,
|
||||||
|
Download: download,
|
||||||
Chain: common.Reverse(chain),
|
Chain: common.Reverse(chain),
|
||||||
Rule: "",
|
Rule: rule,
|
||||||
UploadTotal: upload,
|
Outbound: outbound,
|
||||||
DownloadTotal: download,
|
OutboundType: outboundType,
|
||||||
},
|
},
|
||||||
|
manager: manager,
|
||||||
}
|
}
|
||||||
|
manager.Join(trackerConn)
|
||||||
if rule != nil {
|
return trackerConn
|
||||||
ut.trackerInfo.Rule = rule.String() + " => " + rule.Outbound()
|
|
||||||
} else {
|
|
||||||
ut.trackerInfo.Rule = "final"
|
|
||||||
}
|
|
||||||
|
|
||||||
manager.Join(ut)
|
|
||||||
return ut
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,4 +14,6 @@ const (
|
||||||
CommandSetClashMode
|
CommandSetClashMode
|
||||||
CommandGetSystemProxyStatus
|
CommandGetSystemProxyStatus
|
||||||
CommandSetSystemProxyEnabled
|
CommandSetSystemProxyEnabled
|
||||||
|
CommandConnections
|
||||||
|
CommandCloseConnection
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,6 +31,7 @@ type CommandClientHandler interface {
|
||||||
WriteGroups(message OutboundGroupIterator)
|
WriteGroups(message OutboundGroupIterator)
|
||||||
InitializeClashMode(modeList StringIterator, currentMode string)
|
InitializeClashMode(modeList StringIterator, currentMode string)
|
||||||
UpdateClashMode(newMode string)
|
UpdateClashMode(newMode string)
|
||||||
|
WriteConnections(message *Connections)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStandaloneCommandClient() *CommandClient {
|
func NewStandaloneCommandClient() *CommandClient {
|
||||||
|
@ -116,6 +117,13 @@ func (c *CommandClient) Connect() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
go c.handleModeConn(conn)
|
go c.handleModeConn(conn)
|
||||||
|
case CommandConnections:
|
||||||
|
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "write interval")
|
||||||
|
}
|
||||||
|
c.handler.Connected()
|
||||||
|
go c.handleConnectionsConn(conn)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
53
experimental/libbox/command_close_connection.go
Normal file
53
experimental/libbox/command_close_connection.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package libbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/experimental/clashapi"
|
||||||
|
"github.com/sagernet/sing/common/binary"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CommandClient) CloseConnection(connId string) error {
|
||||||
|
conn, err := c.directConnect()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
writer := bufio.NewWriter(conn)
|
||||||
|
err = binary.WriteData(writer, binary.BigEndian, connId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = writer.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return readError(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommandServer) handleCloseConnection(conn net.Conn) error {
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
var connId string
|
||||||
|
err := binary.ReadData(reader, binary.BigEndian, &connId)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read connection id")
|
||||||
|
}
|
||||||
|
service := s.service
|
||||||
|
if service == nil {
|
||||||
|
return writeError(conn, E.New("service not ready"))
|
||||||
|
}
|
||||||
|
clashServer := service.instance.Router().ClashServer()
|
||||||
|
if clashServer == nil {
|
||||||
|
return writeError(conn, E.New("Clash API disabled"))
|
||||||
|
}
|
||||||
|
targetConn := clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId))
|
||||||
|
if targetConn == nil {
|
||||||
|
return writeError(conn, E.New("connection already closed"))
|
||||||
|
}
|
||||||
|
targetConn.Close()
|
||||||
|
return writeError(conn, nil)
|
||||||
|
}
|
268
experimental/libbox/command_connections.go
Normal file
268
experimental/libbox/command_connections.go
Normal file
|
@ -0,0 +1,268 @@
|
||||||
|
package libbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"net"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/experimental/clashapi"
|
||||||
|
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
||||||
|
"github.com/sagernet/sing/common/binary"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *CommandClient) handleConnectionsConn(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
reader := bufio.NewReader(conn)
|
||||||
|
var connections Connections
|
||||||
|
for {
|
||||||
|
err := binary.ReadData(reader, binary.BigEndian, &connections.connections)
|
||||||
|
if err != nil {
|
||||||
|
c.handler.Disconnected(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.handler.WriteConnections(&connections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommandServer) handleConnectionsConn(conn net.Conn) error {
|
||||||
|
var interval int64
|
||||||
|
err := binary.Read(conn, binary.BigEndian, &interval)
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "read interval")
|
||||||
|
}
|
||||||
|
ticker := time.NewTicker(time.Duration(interval))
|
||||||
|
defer ticker.Stop()
|
||||||
|
ctx := connKeepAlive(conn)
|
||||||
|
var trafficManager *trafficontrol.Manager
|
||||||
|
for {
|
||||||
|
service := s.service
|
||||||
|
if service != nil {
|
||||||
|
clashServer := service.instance.Router().ClashServer()
|
||||||
|
if clashServer == nil {
|
||||||
|
return E.New("Clash API disabled")
|
||||||
|
}
|
||||||
|
trafficManager = clashServer.(*clashapi.Server).TrafficManager()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-ticker.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
connections = make(map[uuid.UUID]*Connection)
|
||||||
|
outConnections []Connection
|
||||||
|
)
|
||||||
|
writer := bufio.NewWriter(conn)
|
||||||
|
for {
|
||||||
|
outConnections = outConnections[:0]
|
||||||
|
for _, connection := range trafficManager.Connections() {
|
||||||
|
outConnections = append(outConnections, newConnection(connections, connection, false))
|
||||||
|
}
|
||||||
|
for _, connection := range trafficManager.ClosedConnections() {
|
||||||
|
outConnections = append(outConnections, newConnection(connections, connection, true))
|
||||||
|
}
|
||||||
|
err = binary.WriteData(writer, binary.BigEndian, outConnections)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = writer.Flush()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-ticker.C:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnectionStateAll = iota
|
||||||
|
ConnectionStateActive
|
||||||
|
ConnectionStateClosed
|
||||||
|
)
|
||||||
|
|
||||||
|
type Connections struct {
|
||||||
|
connections []Connection
|
||||||
|
filteredConnections []Connection
|
||||||
|
outConnections *[]Connection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connections) FilterState(state int32) {
|
||||||
|
c.filteredConnections = c.filteredConnections[:0]
|
||||||
|
switch state {
|
||||||
|
case ConnectionStateAll:
|
||||||
|
c.filteredConnections = append(c.filteredConnections, c.connections...)
|
||||||
|
case ConnectionStateActive:
|
||||||
|
for _, connection := range c.connections {
|
||||||
|
if connection.ClosedAt == 0 {
|
||||||
|
c.filteredConnections = append(c.filteredConnections, connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ConnectionStateClosed:
|
||||||
|
for _, connection := range c.connections {
|
||||||
|
if connection.ClosedAt != 0 {
|
||||||
|
c.filteredConnections = append(c.filteredConnections, connection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connections) SortByDate() {
|
||||||
|
slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int {
|
||||||
|
if x.CreatedAt < y.CreatedAt {
|
||||||
|
return 1
|
||||||
|
} else if x.CreatedAt > y.CreatedAt {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return strings.Compare(y.ID, x.ID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connections) SortByTraffic() {
|
||||||
|
slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int {
|
||||||
|
xTraffic := x.Uplink + x.Downlink
|
||||||
|
yTraffic := y.Uplink + y.Downlink
|
||||||
|
if xTraffic < yTraffic {
|
||||||
|
return 1
|
||||||
|
} else if xTraffic > yTraffic {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return strings.Compare(y.ID, x.ID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connections) SortByTrafficTotal() {
|
||||||
|
slices.SortStableFunc(c.filteredConnections, func(x, y Connection) int {
|
||||||
|
xTraffic := x.UplinkTotal + x.DownlinkTotal
|
||||||
|
yTraffic := y.UplinkTotal + y.DownlinkTotal
|
||||||
|
if xTraffic < yTraffic {
|
||||||
|
return 1
|
||||||
|
} else if xTraffic > yTraffic {
|
||||||
|
return -1
|
||||||
|
} else {
|
||||||
|
return strings.Compare(y.ID, x.ID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connections) Iterator() ConnectionIterator {
|
||||||
|
return newPtrIterator(c.filteredConnections)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Connection struct {
|
||||||
|
ID string
|
||||||
|
Inbound string
|
||||||
|
InboundType string
|
||||||
|
IPVersion int32
|
||||||
|
Network string
|
||||||
|
Source string
|
||||||
|
Destination string
|
||||||
|
Domain string
|
||||||
|
Protocol string
|
||||||
|
User string
|
||||||
|
FromOutbound string
|
||||||
|
CreatedAt int64
|
||||||
|
ClosedAt int64
|
||||||
|
Uplink int64
|
||||||
|
Downlink int64
|
||||||
|
UplinkTotal int64
|
||||||
|
DownlinkTotal int64
|
||||||
|
Rule string
|
||||||
|
Outbound string
|
||||||
|
OutboundType string
|
||||||
|
ChainList []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) Chain() StringIterator {
|
||||||
|
return newIterator(c.ChainList)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Connection) DisplayDestination() string {
|
||||||
|
destination := M.ParseSocksaddr(c.Destination)
|
||||||
|
if destination.IsIP() && c.Domain != "" {
|
||||||
|
destination = M.Socksaddr{
|
||||||
|
Fqdn: c.Domain,
|
||||||
|
Port: destination.Port,
|
||||||
|
}
|
||||||
|
return destination.String()
|
||||||
|
}
|
||||||
|
return c.Destination
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectionIterator interface {
|
||||||
|
Next() *Connection
|
||||||
|
HasNext() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) Connection {
|
||||||
|
if oldConnection, loaded := connections[metadata.ID]; loaded {
|
||||||
|
if isClosed {
|
||||||
|
if oldConnection.ClosedAt == 0 {
|
||||||
|
oldConnection.Uplink = 0
|
||||||
|
oldConnection.Downlink = 0
|
||||||
|
oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
|
||||||
|
}
|
||||||
|
return *oldConnection
|
||||||
|
}
|
||||||
|
lastUplink := oldConnection.UplinkTotal
|
||||||
|
lastDownlink := oldConnection.DownlinkTotal
|
||||||
|
uplinkTotal := metadata.Upload.Load()
|
||||||
|
downlinkTotal := metadata.Download.Load()
|
||||||
|
oldConnection.Uplink = uplinkTotal - lastUplink
|
||||||
|
oldConnection.Downlink = downlinkTotal - lastDownlink
|
||||||
|
oldConnection.UplinkTotal = uplinkTotal
|
||||||
|
oldConnection.DownlinkTotal = downlinkTotal
|
||||||
|
return *oldConnection
|
||||||
|
}
|
||||||
|
var rule string
|
||||||
|
if metadata.Rule != nil {
|
||||||
|
rule = metadata.Rule.String()
|
||||||
|
}
|
||||||
|
uplinkTotal := metadata.Upload.Load()
|
||||||
|
downlinkTotal := metadata.Download.Load()
|
||||||
|
uplink := uplinkTotal
|
||||||
|
downlink := downlinkTotal
|
||||||
|
var closedAt int64
|
||||||
|
if !metadata.ClosedAt.IsZero() {
|
||||||
|
closedAt = metadata.ClosedAt.UnixMilli()
|
||||||
|
uplink = 0
|
||||||
|
downlink = 0
|
||||||
|
}
|
||||||
|
connection := Connection{
|
||||||
|
ID: metadata.ID.String(),
|
||||||
|
Inbound: metadata.Metadata.Inbound,
|
||||||
|
InboundType: metadata.Metadata.InboundType,
|
||||||
|
IPVersion: int32(metadata.Metadata.IPVersion),
|
||||||
|
Network: metadata.Metadata.Network,
|
||||||
|
Source: metadata.Metadata.Source.String(),
|
||||||
|
Destination: metadata.Metadata.Destination.String(),
|
||||||
|
Domain: metadata.Metadata.Domain,
|
||||||
|
Protocol: metadata.Metadata.Protocol,
|
||||||
|
User: metadata.Metadata.User,
|
||||||
|
FromOutbound: metadata.Metadata.Outbound,
|
||||||
|
CreatedAt: metadata.CreatedAt.UnixMilli(),
|
||||||
|
ClosedAt: closedAt,
|
||||||
|
Uplink: uplink,
|
||||||
|
Downlink: downlink,
|
||||||
|
UplinkTotal: uplinkTotal,
|
||||||
|
DownlinkTotal: downlinkTotal,
|
||||||
|
Rule: rule,
|
||||||
|
Outbound: metadata.Outbound,
|
||||||
|
OutboundType: metadata.OutboundType,
|
||||||
|
ChainList: metadata.Chain,
|
||||||
|
}
|
||||||
|
connections[metadata.ID] = &connection
|
||||||
|
return connection
|
||||||
|
}
|
|
@ -14,36 +14,6 @@ import (
|
||||||
"github.com/sagernet/sing/service"
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OutboundGroup struct {
|
|
||||||
Tag string
|
|
||||||
Type string
|
|
||||||
Selectable bool
|
|
||||||
Selected string
|
|
||||||
IsExpand bool
|
|
||||||
items []*OutboundGroupItem
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
|
|
||||||
return newIterator(g.items)
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutboundGroupIterator interface {
|
|
||||||
Next() *OutboundGroup
|
|
||||||
HasNext() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutboundGroupItem struct {
|
|
||||||
Tag string
|
|
||||||
Type string
|
|
||||||
URLTestTime int64
|
|
||||||
URLTestDelay int32
|
|
||||||
}
|
|
||||||
|
|
||||||
type OutboundGroupItemIterator interface {
|
|
||||||
Next() *OutboundGroupItem
|
|
||||||
HasNext() bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *CommandClient) handleGroupConn(conn net.Conn) {
|
func (c *CommandClient) handleGroupConn(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
|
@ -92,6 +62,36 @@ func (s *CommandServer) handleGroupConn(conn net.Conn) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OutboundGroup struct {
|
||||||
|
Tag string
|
||||||
|
Type string
|
||||||
|
Selectable bool
|
||||||
|
Selected string
|
||||||
|
IsExpand bool
|
||||||
|
items []*OutboundGroupItem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
|
||||||
|
return newIterator(g.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboundGroupIterator interface {
|
||||||
|
Next() *OutboundGroup
|
||||||
|
HasNext() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboundGroupItem struct {
|
||||||
|
Tag string
|
||||||
|
Type string
|
||||||
|
URLTestTime int64
|
||||||
|
URLTestDelay int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type OutboundGroupItemIterator interface {
|
||||||
|
Next() *OutboundGroupItem
|
||||||
|
HasNext() bool
|
||||||
|
}
|
||||||
|
|
||||||
func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
|
func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
|
||||||
var groupLength uint16
|
var groupLength uint16
|
||||||
err := binary.Read(reader, binary.BigEndian, &groupLength)
|
err := binary.Read(reader, binary.BigEndian, &groupLength)
|
||||||
|
|
|
@ -33,6 +33,8 @@ type CommandServer struct {
|
||||||
urlTestUpdate chan struct{}
|
urlTestUpdate chan struct{}
|
||||||
modeUpdate chan struct{}
|
modeUpdate chan struct{}
|
||||||
logReset chan struct{}
|
logReset chan struct{}
|
||||||
|
|
||||||
|
closedConnections []Connection
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandServerHandler interface {
|
type CommandServerHandler interface {
|
||||||
|
@ -176,6 +178,10 @@ func (s *CommandServer) handleConnection(conn net.Conn) error {
|
||||||
return s.handleGetSystemProxyStatus(conn)
|
return s.handleGetSystemProxyStatus(conn)
|
||||||
case CommandSetSystemProxyEnabled:
|
case CommandSetSystemProxyEnabled:
|
||||||
return s.handleSetSystemProxyEnabled(conn)
|
return s.handleSetSystemProxyEnabled(conn)
|
||||||
|
case CommandConnections:
|
||||||
|
return s.handleConnectionsConn(conn)
|
||||||
|
case CommandCloseConnection:
|
||||||
|
return s.handleCloseConnection(conn)
|
||||||
default:
|
default:
|
||||||
return E.New("unknown command: ", command)
|
return E.New("unknown command: ", command)
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,7 @@ func (s *CommandServer) readStatus() StatusMessage {
|
||||||
trafficManager := clashServer.(*clashapi.Server).TrafficManager()
|
trafficManager := clashServer.(*clashapi.Server).TrafficManager()
|
||||||
message.Uplink, message.Downlink = trafficManager.Now()
|
message.Uplink, message.Downlink = trafficManager.Now()
|
||||||
message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
|
message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
|
||||||
message.ConnectionsIn = int32(trafficManager.Connections())
|
message.ConnectionsIn = int32(trafficManager.ConnectionsLen())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,10 @@ func newIterator[T any](values []T) *iterator[T] {
|
||||||
return &iterator[T]{values}
|
return &iterator[T]{values}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newPtrIterator[T any](values []T) *iterator[*T] {
|
||||||
|
return &iterator[*T]{common.Map(values, func(value T) *T { return &value })}
|
||||||
|
}
|
||||||
|
|
||||||
func (i *iterator[T]) Next() T {
|
func (i *iterator[T]) Next() T {
|
||||||
if len(i.values) == 0 {
|
if len(i.values) == 0 {
|
||||||
return common.DefaultValue[T]()
|
return common.DefaultValue[T]()
|
||||||
|
|
|
@ -149,33 +149,6 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions
|
||||||
return tun.New(*options)
|
return tun.New(*options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) {
|
|
||||||
var uid int32
|
|
||||||
if w.useProcFS {
|
|
||||||
uid = procfs.ResolveSocketByProcSearch(network, source, destination)
|
|
||||||
if uid == -1 {
|
|
||||||
return nil, E.New("procfs: not found")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var ipProtocol int32
|
|
||||||
switch N.NetworkName(network) {
|
|
||||||
case N.NetworkTCP:
|
|
||||||
ipProtocol = syscall.IPPROTO_TCP
|
|
||||||
case N.NetworkUDP:
|
|
||||||
ipProtocol = syscall.IPPROTO_UDP
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown network: ", network)
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
packageName, _ := w.iif.PackageNameByUid(uid)
|
|
||||||
return &process.Info{UserId: uid, PackageName: packageName}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
|
func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
|
||||||
return w.iif.UsePlatformDefaultInterfaceMonitor()
|
return w.iif.UsePlatformDefaultInterfaceMonitor()
|
||||||
}
|
}
|
||||||
|
@ -229,6 +202,33 @@ func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
|
||||||
return (adapter.WIFIState)(*wifiState)
|
return (adapter.WIFIState)(*wifiState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) {
|
||||||
|
var uid int32
|
||||||
|
if w.useProcFS {
|
||||||
|
uid = procfs.ResolveSocketByProcSearch(network, source, destination)
|
||||||
|
if uid == -1 {
|
||||||
|
return nil, E.New("procfs: not found")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var ipProtocol int32
|
||||||
|
switch N.NetworkName(network) {
|
||||||
|
case N.NetworkTCP:
|
||||||
|
ipProtocol = syscall.IPPROTO_TCP
|
||||||
|
case N.NetworkUDP:
|
||||||
|
ipProtocol = syscall.IPPROTO_UDP
|
||||||
|
default:
|
||||||
|
return nil, E.New("unknown network: ", network)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packageName, _ := w.iif.PackageNameByUid(uid)
|
||||||
|
return &process.Info{UserId: uid, PackageName: packageName}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (w *platformInterfaceWrapper) DisableColors() bool {
|
func (w *platformInterfaceWrapper) DisableColors() bool {
|
||||||
return runtime.GOOS != "android"
|
return runtime.GOOS != "android"
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,12 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/user"
|
"os/user"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/humanize"
|
"github.com/sagernet/sing-box/common/humanize"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
_ "github.com/sagernet/sing-box/include"
|
_ "github.com/sagernet/sing-box/include"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -59,6 +61,10 @@ func FormatMemoryBytes(length int64) string {
|
||||||
return humanize.MemoryBytes(uint64(length))
|
return humanize.MemoryBytes(uint64(length))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func FormatDuration(duration int64) string {
|
||||||
|
return log.FormatDuration(time.Duration(duration) * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
func ProxyDisplayType(proxyType string) string {
|
func ProxyDisplayType(proxyType string) string {
|
||||||
return C.ProxyDisplayName(proxyType)
|
return C.ProxyDisplayName(proxyType)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,43 +11,43 @@ import (
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) {
|
func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) {
|
||||||
if options.Type == "" {
|
if options.Type == "" {
|
||||||
return nil, E.New("missing inbound type")
|
return nil, E.New("missing inbound type")
|
||||||
}
|
}
|
||||||
switch options.Type {
|
switch options.Type {
|
||||||
case C.TypeTun:
|
case C.TypeTun:
|
||||||
return NewTun(ctx, router, logger, options.Tag, options.TunOptions, platformInterface)
|
return NewTun(ctx, router, logger, tag, options.TunOptions, platformInterface)
|
||||||
case C.TypeRedirect:
|
case C.TypeRedirect:
|
||||||
return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions), nil
|
return NewRedirect(ctx, router, logger, tag, options.RedirectOptions), nil
|
||||||
case C.TypeTProxy:
|
case C.TypeTProxy:
|
||||||
return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil
|
return NewTProxy(ctx, router, logger, tag, options.TProxyOptions), nil
|
||||||
case C.TypeDirect:
|
case C.TypeDirect:
|
||||||
return NewDirect(ctx, router, logger, options.Tag, options.DirectOptions), nil
|
return NewDirect(ctx, router, logger, tag, options.DirectOptions), nil
|
||||||
case C.TypeSOCKS:
|
case C.TypeSOCKS:
|
||||||
return NewSocks(ctx, router, logger, options.Tag, options.SocksOptions), nil
|
return NewSocks(ctx, router, logger, tag, options.SocksOptions), nil
|
||||||
case C.TypeHTTP:
|
case C.TypeHTTP:
|
||||||
return NewHTTP(ctx, router, logger, options.Tag, options.HTTPOptions)
|
return NewHTTP(ctx, router, logger, tag, options.HTTPOptions)
|
||||||
case C.TypeMixed:
|
case C.TypeMixed:
|
||||||
return NewMixed(ctx, router, logger, options.Tag, options.MixedOptions), nil
|
return NewMixed(ctx, router, logger, tag, options.MixedOptions), nil
|
||||||
case C.TypeShadowsocks:
|
case C.TypeShadowsocks:
|
||||||
return NewShadowsocks(ctx, router, logger, options.Tag, options.ShadowsocksOptions)
|
return NewShadowsocks(ctx, router, logger, tag, options.ShadowsocksOptions)
|
||||||
case C.TypeVMess:
|
case C.TypeVMess:
|
||||||
return NewVMess(ctx, router, logger, options.Tag, options.VMessOptions)
|
return NewVMess(ctx, router, logger, tag, options.VMessOptions)
|
||||||
case C.TypeTrojan:
|
case C.TypeTrojan:
|
||||||
return NewTrojan(ctx, router, logger, options.Tag, options.TrojanOptions)
|
return NewTrojan(ctx, router, logger, tag, options.TrojanOptions)
|
||||||
case C.TypeNaive:
|
case C.TypeNaive:
|
||||||
return NewNaive(ctx, router, logger, options.Tag, options.NaiveOptions)
|
return NewNaive(ctx, router, logger, tag, options.NaiveOptions)
|
||||||
case C.TypeHysteria:
|
case C.TypeHysteria:
|
||||||
return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions)
|
return NewHysteria(ctx, router, logger, tag, options.HysteriaOptions)
|
||||||
case C.TypeShadowTLS:
|
case C.TypeShadowTLS:
|
||||||
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
|
return NewShadowTLS(ctx, router, logger, tag, options.ShadowTLSOptions)
|
||||||
case C.TypeVLESS:
|
case C.TypeVLESS:
|
||||||
return NewVLESS(ctx, router, logger, options.Tag, options.VLESSOptions)
|
return NewVLESS(ctx, router, logger, tag, options.VLESSOptions)
|
||||||
case C.TypeTUIC:
|
case C.TypeTUIC:
|
||||||
return NewTUIC(ctx, router, logger, options.Tag, options.TUICOptions)
|
return NewTUIC(ctx, router, logger, tag, options.TUICOptions)
|
||||||
case C.TypeHysteria2:
|
case C.TypeHysteria2:
|
||||||
return NewHysteria2(ctx, router, logger, options.Tag, options.Hysteria2Options)
|
return NewHysteria2(ctx, router, logger, tag, options.Hysteria2Options)
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", options.Type)
|
return nil, E.New("unknown inbound type: ", options.Type)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ func (f Formatter) Format(ctx context.Context, level Level, tag string, message
|
||||||
id, hasId = IDFromContext(ctx)
|
id, hasId = IDFromContext(ctx)
|
||||||
}
|
}
|
||||||
if hasId {
|
if hasId {
|
||||||
activeDuration := formatDuration(time.Since(id.CreatedAt))
|
activeDuration := FormatDuration(time.Since(id.CreatedAt))
|
||||||
if !f.DisableColors {
|
if !f.DisableColors {
|
||||||
var color aurora.Color
|
var color aurora.Color
|
||||||
color = aurora.Color(uint8(id.ID))
|
color = aurora.Color(uint8(id.ID))
|
||||||
|
@ -113,7 +113,7 @@ func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string
|
||||||
id, hasId = IDFromContext(ctx)
|
id, hasId = IDFromContext(ctx)
|
||||||
}
|
}
|
||||||
if hasId {
|
if hasId {
|
||||||
activeDuration := formatDuration(time.Since(id.CreatedAt))
|
activeDuration := FormatDuration(time.Since(id.CreatedAt))
|
||||||
if !f.DisableColors {
|
if !f.DisableColors {
|
||||||
var color aurora.Color
|
var color aurora.Color
|
||||||
color = aurora.Color(uint8(id.ID))
|
color = aurora.Color(uint8(id.ID))
|
||||||
|
@ -163,7 +163,7 @@ func xd(value int, x int) string {
|
||||||
return message
|
return message
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatDuration(duration time.Duration) string {
|
func FormatDuration(duration time.Duration) string {
|
||||||
if duration < time.Second {
|
if duration < time.Second {
|
||||||
return F.ToString(duration.Milliseconds(), "ms")
|
return F.ToString(duration.Milliseconds(), "ms")
|
||||||
} else if duration < time.Minute {
|
} else if duration < time.Minute {
|
||||||
|
|
Loading…
Reference in a new issue