mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-15 21:00:27 +00:00
202 lines
6.3 KiB
Go
202 lines
6.3 KiB
Go
package main
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/daemon"
|
|
"github.com/sagernet/sing/common"
|
|
"github.com/sagernet/sing/common/byteformats"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var commandAPIConnectionListFlagColumns []string
|
|
|
|
type connectionRate struct {
|
|
uplink int64
|
|
downlink int64
|
|
}
|
|
|
|
type connectionColumn struct {
|
|
header string
|
|
value func(connection *daemon.Connection, rates map[string]connectionRate) string
|
|
}
|
|
|
|
var connectionColumnNames = []string{
|
|
"id", "network", "source", "destination", "inbound", "outbound",
|
|
"chain", "rule", "protocol", "user", "process", "created", "rate", "total",
|
|
}
|
|
|
|
var connectionColumns = map[string]connectionColumn{
|
|
"id": {"ID", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return connection.GetId()
|
|
}},
|
|
"network": {"NETWORK", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return connection.GetNetwork()
|
|
}},
|
|
"source": {"SOURCE", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return connection.GetSource()
|
|
}},
|
|
"destination": {"DESTINATION", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return connectionDestination(connection)
|
|
}},
|
|
"inbound": {"INBOUND", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
if connection.GetInbound() == "" {
|
|
return connection.GetInboundType()
|
|
}
|
|
return connection.GetInboundType() + "/" + connection.GetInbound()
|
|
}},
|
|
"outbound": {"OUTBOUND", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return connection.GetOutbound()
|
|
}},
|
|
"chain": {"CHAIN", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
chain := slices.Clone(connection.GetChainList())
|
|
slices.Reverse(chain)
|
|
return strings.Join(chain, "/")
|
|
}},
|
|
"rule": {"RULE", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return connection.GetRule()
|
|
}},
|
|
"protocol": {"PROTOCOL", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return connection.GetProtocol()
|
|
}},
|
|
"user": {"USER", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return connection.GetUser()
|
|
}},
|
|
"process": {"PROCESS", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
processInfo := connection.GetProcessInfo()
|
|
if processInfo.GetProcessPath() != "" {
|
|
return processInfo.GetProcessPath()
|
|
}
|
|
if len(processInfo.GetPackageNames()) > 0 {
|
|
return processInfo.GetPackageNames()[0]
|
|
}
|
|
return ""
|
|
}},
|
|
"created": {"CREATED", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return formatConnectionTime(connection.GetCreatedAt())
|
|
}},
|
|
"rate": {"RATE", func(connection *daemon.Connection, rates map[string]connectionRate) string {
|
|
rate, found := rates[connection.GetId()]
|
|
if !found || (rate.uplink == 0 && rate.downlink == 0) {
|
|
return ""
|
|
}
|
|
return "↑" + byteformats.FormatBytes(uint64(rate.uplink)) + "/s ↓" + byteformats.FormatBytes(uint64(rate.downlink)) + "/s"
|
|
}},
|
|
"total": {"TOTAL", func(connection *daemon.Connection, _ map[string]connectionRate) string {
|
|
return "↑" + byteformats.FormatBytes(uint64(connection.GetUplinkTotal())) + " ↓" + byteformats.FormatBytes(uint64(connection.GetDownlinkTotal()))
|
|
}},
|
|
}
|
|
|
|
var commandAPIConnectionList = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List open connections",
|
|
Args: cobra.NoArgs,
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
return runAPIConnectionList()
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
commandAPIConnectionList.Flags().StringSliceVar(
|
|
&commandAPIConnectionListFlagColumns,
|
|
"columns",
|
|
[]string{"id", "network", "destination", "inbound", "outbound", "total"},
|
|
"Columns to display (available: "+strings.Join(connectionColumnNames, ", ")+")",
|
|
)
|
|
commandAPIConnection.AddCommand(commandAPIConnectionList)
|
|
}
|
|
|
|
func runAPIConnectionList() error {
|
|
columns := make([]connectionColumn, 0, len(commandAPIConnectionListFlagColumns))
|
|
sampleRates := false
|
|
for _, name := range commandAPIConnectionListFlagColumns {
|
|
column, found := connectionColumns[name]
|
|
if !found {
|
|
return E.New("unknown column: ", name, ", available: ", strings.Join(connectionColumnNames, ", "))
|
|
}
|
|
if name == "rate" {
|
|
sampleRates = true
|
|
}
|
|
columns = append(columns, column)
|
|
}
|
|
clientConn, client, err := createAPIClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer clientConn.Close()
|
|
var (
|
|
connections []*daemon.Connection
|
|
rates map[string]connectionRate
|
|
)
|
|
if sampleRates {
|
|
connections, rates, err = fetchConnectionsAndRates(client)
|
|
} else {
|
|
connections, err = fetchConnections(client)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
table := tableWriter{
|
|
header: common.Map(columns, func(it connectionColumn) string {
|
|
return it.header
|
|
}),
|
|
emptyMessage: "no connections",
|
|
}
|
|
for _, connection := range connections {
|
|
if connection.GetClosedAt() != 0 {
|
|
continue
|
|
}
|
|
table.addRow(common.Map(columns, func(it connectionColumn) string {
|
|
return it.value(connection, rates)
|
|
})...)
|
|
}
|
|
table.flush()
|
|
return nil
|
|
}
|
|
|
|
func connectionDestination(connection *daemon.Connection) string {
|
|
destination := connection.GetDestination()
|
|
domain := connection.GetDomain()
|
|
if domain == "" {
|
|
return destination
|
|
}
|
|
portIndex := strings.LastIndex(destination, ":")
|
|
if portIndex == -1 {
|
|
return domain
|
|
}
|
|
return domain + destination[portIndex:]
|
|
}
|
|
|
|
func fetchConnectionsAndRates(client daemon.StartedServiceClient) ([]*daemon.Connection, map[string]connectionRate, error) {
|
|
stream, err := client.SubscribeConnections(globalCtx, &daemon.SubscribeConnectionsRequest{Interval: int64(time.Second)})
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
initialEvents, err := stream.Recv()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
connections := common.FilterNotNil(common.Map(initialEvents.GetEvents(), func(it *daemon.ConnectionEvent) *daemon.Connection {
|
|
return it.GetConnection()
|
|
}))
|
|
common.SortBy(connections, func(it *daemon.Connection) int64 {
|
|
return it.GetCreatedAt()
|
|
})
|
|
updateEvents, err := stream.Recv()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
rates := make(map[string]connectionRate, len(updateEvents.GetEvents()))
|
|
for _, event := range updateEvents.GetEvents() {
|
|
rates[event.GetId()] = connectionRate{
|
|
uplink: event.GetUplinkDelta(),
|
|
downlink: event.GetDownlinkDelta(),
|
|
}
|
|
}
|
|
return connections, rates, nil
|
|
}
|