mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-15 02:23:59 +00:00
Add api command
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user