platform: Prepare connections list

This commit is contained in:
世界
2024-06-11 21:16:33 +08:00
parent 19f6c6abfd
commit 85aaa112c4
18 changed files with 651 additions and 254 deletions

View File

@@ -14,4 +14,6 @@ const (
CommandSetClashMode
CommandGetSystemProxyStatus
CommandSetSystemProxyEnabled
CommandConnections
CommandCloseConnection
)

View File

@@ -31,6 +31,7 @@ type CommandClientHandler interface {
WriteGroups(message OutboundGroupIterator)
InitializeClashMode(modeList StringIterator, currentMode string)
UpdateClashMode(newMode string)
WriteConnections(message *Connections)
}
func NewStandaloneCommandClient() *CommandClient {
@@ -116,6 +117,13 @@ func (c *CommandClient) Connect() error {
return nil
}
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
}

View 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)
}

View 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
}

View File

@@ -14,36 +14,6 @@ import (
"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) {
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) {
var groupLength uint16
err := binary.Read(reader, binary.BigEndian, &groupLength)

View File

@@ -33,6 +33,8 @@ type CommandServer struct {
urlTestUpdate chan struct{}
modeUpdate chan struct{}
logReset chan struct{}
closedConnections []Connection
}
type CommandServerHandler interface {
@@ -176,6 +178,10 @@ func (s *CommandServer) handleConnection(conn net.Conn) error {
return s.handleGetSystemProxyStatus(conn)
case CommandSetSystemProxyEnabled:
return s.handleSetSystemProxyEnabled(conn)
case CommandConnections:
return s.handleConnectionsConn(conn)
case CommandCloseConnection:
return s.handleCloseConnection(conn)
default:
return E.New("unknown command: ", command)
}

View File

@@ -36,7 +36,7 @@ func (s *CommandServer) readStatus() StatusMessage {
trafficManager := clashServer.(*clashapi.Server).TrafficManager()
message.Uplink, message.Downlink = trafficManager.Now()
message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
message.ConnectionsIn = int32(trafficManager.Connections())
message.ConnectionsIn = int32(trafficManager.ConnectionsLen())
}
}

View File

@@ -17,6 +17,10 @@ func newIterator[T any](values []T) *iterator[T] {
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 {
if len(i.values) == 0 {
return common.DefaultValue[T]()

View File

@@ -149,33 +149,6 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions
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 {
return w.iif.UsePlatformDefaultInterfaceMonitor()
}
@@ -229,6 +202,33 @@ func (w *platformInterfaceWrapper) ReadWIFIState() adapter.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 {
return runtime.GOOS != "android"
}

View File

@@ -4,10 +4,12 @@ import (
"os"
"os/user"
"strconv"
"time"
"github.com/sagernet/sing-box/common/humanize"
C "github.com/sagernet/sing-box/constant"
_ "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
)
var (
@@ -59,6 +61,10 @@ func FormatMemoryBytes(length int64) string {
return humanize.MemoryBytes(uint64(length))
}
func FormatDuration(duration int64) string {
return log.FormatDuration(time.Duration(duration) * time.Millisecond)
}
func ProxyDisplayType(proxyType string) string {
return C.ProxyDisplayName(proxyType)
}