Files
sing-box/experimental/clashapi/connections.go
T
2026-06-25 17:38:53 +08:00

180 lines
5.0 KiB
Go

package clashapi
import (
"bytes"
"context"
"net/http"
"strconv"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/trafficcontrol"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
F "github.com/sagernet/sing/common/format"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/ws"
"github.com/sagernet/ws/wsutil"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/gofrs/uuid/v5"
)
func connectionRouter(ctx context.Context, network adapter.NetworkManager, trafficManager *trafficcontrol.Manager) http.Handler {
r := chi.NewRouter()
r.Get("/", getConnections(ctx, trafficManager))
r.Delete("/", closeAllConnections(network, trafficManager))
r.Delete("/{id}", closeConnection(trafficManager))
return r
}
func connectionsSnapshot(trafficManager *trafficcontrol.Manager) render.M {
uplinkTotal, downlinkTotal := trafficManager.Total()
connections := common.Filter(trafficManager.Connections(), func(metadata *trafficcontrol.TrackerMetadata) bool {
return metadata.OutboundType != C.TypeDNS
})
return render.M{
"downloadTotal": downlinkTotal,
"uploadTotal": uplinkTotal,
"connections": common.Map(connections, func(metadata *trafficcontrol.TrackerMetadata) connectionObject {
return connectionObject(*metadata)
}),
"memory": inuseMemory(),
}
}
type connectionObject trafficcontrol.TrackerMetadata
func (c connectionObject) MarshalJSON() ([]byte, error) {
var inbound string
if c.Metadata.Inbound != "" {
inbound = c.Metadata.InboundType + "/" + c.Metadata.Inbound
} else {
inbound = c.Metadata.InboundType
}
var domain string
if c.Metadata.Domain != "" {
domain = c.Metadata.Domain
} else {
domain = c.Metadata.Destination.Fqdn
}
var processPath string
if c.Metadata.ProcessInfo != nil {
if c.Metadata.ProcessInfo.ProcessPath != "" {
processPath = c.Metadata.ProcessInfo.ProcessPath
} else if len(c.Metadata.ProcessInfo.AndroidPackageNames) > 0 {
processPath = c.Metadata.ProcessInfo.AndroidPackageNames[0]
}
if processPath == "" {
if c.Metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(c.Metadata.ProcessInfo.UserId)
}
} else if c.Metadata.ProcessInfo.UserName != "" {
processPath = F.ToString(processPath, " (", c.Metadata.ProcessInfo.UserName, ")")
} else if c.Metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(processPath, " (", c.Metadata.ProcessInfo.UserId, ")")
}
}
var rule string
if c.Rule != nil {
rule = F.ToString(c.Rule, " => ", c.Rule.Action())
} else {
rule = "final"
}
return json.Marshal(map[string]any{
"id": c.ID,
"metadata": map[string]any{
"network": c.Metadata.Network,
"type": inbound,
"sourceIP": c.Metadata.Source.Addr,
"destinationIP": c.Metadata.Destination.Addr,
"sourcePort": F.ToString(c.Metadata.Source.Port),
"destinationPort": F.ToString(c.Metadata.Destination.Port),
"host": domain,
"dnsMode": "normal",
"processPath": processPath,
},
"upload": c.Upload.Load(),
"download": c.Download.Load(),
"start": c.CreatedAt,
"chains": c.Chain,
"rule": rule,
"rulePayload": "",
})
}
func getConnections(ctx context.Context, trafficManager *trafficcontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Upgrade") != "websocket" {
render.JSON(w, r, connectionsSnapshot(trafficManager))
return
}
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
return
}
defer conn.Close()
intervalStr := r.URL.Query().Get("interval")
interval := 1000
if intervalStr != "" {
t, err := strconv.Atoi(intervalStr)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
interval = t
}
buf := &bytes.Buffer{}
sendSnapshot := func() error {
buf.Reset()
encodeErr := json.NewEncoder(buf).Encode(connectionsSnapshot(trafficManager))
if encodeErr != nil {
return encodeErr
}
return wsutil.WriteServerText(conn, buf.Bytes())
}
if err = sendSnapshot(); err != nil {
return
}
tick := time.NewTicker(time.Millisecond * time.Duration(interval))
defer tick.Stop()
for {
select {
case <-ctx.Done():
return
case <-tick.C:
}
if err = sendSnapshot(); err != nil {
break
}
}
}
}
func closeConnection(trafficManager *trafficcontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
id := uuid.FromStringOrNil(chi.URLParam(r, "id"))
targetConnection := trafficManager.Connection(id)
if targetConnection != nil {
targetConnection.Close()
}
render.NoContent(w, r)
}
}
func closeAllConnections(network adapter.NetworkManager, trafficManager *trafficcontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
trafficManager.CloseAllConnections()
network.ResetNetwork()
render.NoContent(w, r)
}
}