Add basic clash api

This commit is contained in:
世界
2022-07-19 22:16:49 +08:00
parent c7fabe40ed
commit c5b3e8b042
36 changed files with 1498 additions and 41 deletions

View File

@@ -0,0 +1,23 @@
package clashapi
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func cacheRouter() http.Handler {
r := chi.NewRouter()
r.Post("/fakeip/flush", flushFakeip)
return r
}
func flushFakeip(w http.ResponseWriter, r *http.Request) {
/*if err := cachefile.Cache().FlushFakeip(); err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, newError(err.Error()))
return
}*/
render.NoContent(w, r)
}

View File

@@ -0,0 +1,17 @@
package clashapi
import (
"net/http"
"net/url"
"github.com/go-chi/chi/v5"
)
// When name is composed of a partial escape string, Golang does not unescape it
func getEscapeParam(r *http.Request, paramName string) string {
param := chi.URLParam(r, paramName)
if newParam, err := url.PathUnescape(param); err == nil {
param = newParam
}
return param
}

View File

@@ -0,0 +1,49 @@
package compatible
import "sync"
// Map is a generics sync.Map
type Map[K comparable, V any] struct {
m sync.Map
}
func (m *Map[K, V]) Load(key K) (V, bool) {
v, ok := m.m.Load(key)
if !ok {
return *new(V), false
}
return v.(V), ok
}
func (m *Map[K, V]) Store(key K, value V) {
m.m.Store(key, value)
}
func (m *Map[K, V]) Delete(key K) {
m.m.Delete(key)
}
func (m *Map[K, V]) Range(f func(key K, value V) bool) {
m.m.Range(func(key, value any) bool {
return f(key.(K), value.(V))
})
}
func (m *Map[K, V]) LoadOrStore(key K, value V) (V, bool) {
v, ok := m.m.LoadOrStore(key, value)
return v.(V), ok
}
func (m *Map[K, V]) LoadAndDelete(key K) (V, bool) {
v, ok := m.m.LoadAndDelete(key)
if !ok {
return *new(V), false
}
return v.(V), ok
}
func New[K comparable, V any]() *Map[K, V] {
return &Map[K, V]{m: sync.Map{}}
}

View File

@@ -0,0 +1,49 @@
package clashapi
import (
"net/http"
"github.com/sagernet/sing-box/log"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func configRouter(logFactory log.Factory) http.Handler {
r := chi.NewRouter()
r.Get("/", getConfigs(logFactory))
r.Put("/", updateConfigs)
r.Patch("/", patchConfigs)
return r
}
type configSchema struct {
Port *int `json:"port"`
SocksPort *int `json:"socks-port"`
RedirPort *int `json:"redir-port"`
TProxyPort *int `json:"tproxy-port"`
MixedPort *int `json:"mixed-port"`
AllowLan *bool `json:"allow-lan"`
BindAddress *string `json:"bind-address"`
Mode string `json:"mode"`
LogLevel string `json:"log-level"`
IPv6 *bool `json:"ipv6"`
Tun any `json:"tun"`
}
func getConfigs(logFactory log.Factory) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, &configSchema{
Mode: "Rule",
LogLevel: log.FormatLevel(logFactory.Level()),
})
}
}
func patchConfigs(w http.ResponseWriter, r *http.Request) {
render.NoContent(w, r)
}
func updateConfigs(w http.ResponseWriter, r *http.Request) {
render.NoContent(w, r)
}

View File

@@ -0,0 +1,97 @@
package clashapi
import (
"bytes"
"encoding/json"
"net/http"
"strconv"
"time"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontroll"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
"github.com/gorilla/websocket"
)
func connectionRouter(trafficManager *trafficontroll.Manager) http.Handler {
r := chi.NewRouter()
r.Get("/", getConnections(trafficManager))
r.Delete("/", closeAllConnections(trafficManager))
r.Delete("/{id}", closeConnection(trafficManager))
return r
}
func getConnections(trafficManager *trafficontroll.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if !websocket.IsWebSocketUpgrade(r) {
snapshot := trafficManager.Snapshot()
render.JSON(w, r, snapshot)
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
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()
snapshot := trafficManager.Snapshot()
if err := json.NewEncoder(buf).Encode(snapshot); err != nil {
return err
}
return conn.WriteMessage(websocket.TextMessage, buf.Bytes())
}
if err = sendSnapshot(); err != nil {
return
}
tick := time.NewTicker(time.Millisecond * time.Duration(interval))
defer tick.Stop()
for range tick.C {
if err = sendSnapshot(); err != nil {
break
}
}
}
}
func closeConnection(trafficManager *trafficontroll.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
snapshot := trafficManager.Snapshot()
for _, c := range snapshot.Connections {
if id == c.ID() {
c.Close()
break
}
}
render.NoContent(w, r)
}
}
func closeAllConnections(trafficManager *trafficontroll.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
snapshot := trafficManager.Snapshot()
for _, c := range snapshot.Connections {
c.Close()
}
render.NoContent(w, r)
}
}

View File

@@ -0,0 +1,14 @@
package clashapi
var (
CtxKeyProxyName = contextKey("proxy name")
CtxKeyProviderName = contextKey("provider name")
CtxKeyProxy = contextKey("proxy")
CtxKeyProvider = contextKey("provider")
)
type contextKey string
func (c contextKey) String() string {
return "clash context key " + string(c)
}

View File

@@ -0,0 +1,22 @@
package clashapi
var (
ErrUnauthorized = newError("Unauthorized")
ErrBadRequest = newError("Body invalid")
ErrForbidden = newError("Forbidden")
ErrNotFound = newError("Resource not found")
ErrRequestTimeout = newError("Timeout")
)
// HTTPError is custom HTTP error for API
type HTTPError struct {
Message string `json:"message"`
}
func (e *HTTPError) Error() string {
return e.Message
}
func newError(msg string) *HTTPError {
return &HTTPError{Message: msg}
}

View File

@@ -0,0 +1,53 @@
package clashapi
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func profileRouter() http.Handler {
r := chi.NewRouter()
r.Get("/tracing", subscribeTracing)
return r
}
func subscribeTracing(w http.ResponseWriter, r *http.Request) {
// if !profile.Tracing.Load() {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
//return
//}
/*wsConn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
ch := make(chan map[string]any, 1024)
sub := event.Subscribe()
defer event.UnSubscribe(sub)
buf := &bytes.Buffer{}
go func() {
for elm := range sub {
select {
case ch <- elm:
default:
}
}
close(ch)
}()
for elm := range ch {
buf.Reset()
if err := json.NewEncoder(buf).Encode(elm); err != nil {
break
}
if err := wsConn.WriteMessage(websocket.TextMessage, buf.Bytes()); err != nil {
break
}
}*/
}

View File

@@ -0,0 +1,74 @@
package clashapi
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func proxyProviderRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProviders)
r.Route("/{name}", func(r chi.Router) {
r.Use(parseProviderName, findProviderByName)
r.Get("/", getProvider)
r.Put("/", updateProvider)
r.Get("/healthcheck", healthCheckProvider)
})
return r
}
func getProviders(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{
"providers": []string{},
})
}
func getProvider(w http.ResponseWriter, r *http.Request) {
/*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
render.JSON(w, r, provider)*/
render.NoContent(w, r)
}
func updateProvider(w http.ResponseWriter, r *http.Request) {
/*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
if err := provider.Update(); err != nil {
render.Status(r, http.StatusServiceUnavailable)
render.JSON(w, r, newError(err.Error()))
return
}*/
render.NoContent(w, r)
}
func healthCheckProvider(w http.ResponseWriter, r *http.Request) {
/*provider := r.Context().Value(CtxKeyProvider).(provider.ProxyProvider)
provider.HealthCheck()*/
render.NoContent(w, r)
}
func parseProviderName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := getEscapeParam(r, "name")
ctx := context.WithValue(r.Context(), CtxKeyProviderName, name)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func findProviderByName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
/*name := r.Context().Value(CtxKeyProviderName).(string)
providers := tunnel.ProxyProviders()
provider, exist := providers[name]
if !exist {*/
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
//return
//}
// ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
// next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -0,0 +1,122 @@
package clashapi
import (
"context"
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func proxyRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getProxies)
r.Route("/{name}", func(r chi.Router) {
r.Use(parseProxyName, findProxyByName)
r.Get("/", getProxy)
r.Get("/delay", getProxyDelay)
r.Put("/", updateProxy)
})
return r
}
func parseProxyName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := getEscapeParam(r, "name")
ctx := context.WithValue(r.Context(), CtxKeyProxyName, name)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func findProxyByName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
/*name := r.Context().Value(CtxKeyProxyName).(string)
proxies := tunnel.Proxies()
proxy, exist := proxies[name]
if !exist {*/
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
return
//}
// ctx := context.WithValue(r.Context(), CtxKeyProxy, proxy)
// next.ServeHTTP(w, r.WithContext(ctx))
})
}
func getProxies(w http.ResponseWriter, r *http.Request) {
// proxies := tunnel.Proxies()
render.JSON(w, r, render.M{
"proxies": []string{},
})
}
func getProxy(w http.ResponseWriter, r *http.Request) {
/* proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
render.JSON(w, r, proxy)*/
render.Status(r, http.StatusServiceUnavailable)
}
type UpdateProxyRequest struct {
Name string `json:"name"`
}
func updateProxy(w http.ResponseWriter, r *http.Request) {
/* req := UpdateProxyRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
proxy := r.Context().Value(CtxKeyProxy).(*adapter.Proxy)
selector, ok := proxy.ProxyAdapter.(*outboundgroup.Selector)
if !ok {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("Must be a Selector"))
return
}
if err := selector.Set(req.Name); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(fmt.Sprintf("Selector update error: %s", err.Error())))
return
}
cachefile.Cache().SetSelected(proxy.Name(), req.Name)*/
render.NoContent(w, r)
}
func getProxyDelay(w http.ResponseWriter, r *http.Request) {
/* query := r.URL.Query()
url := query.Get("url")
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
proxy := r.Context().Value(CtxKeyProxy).(C.Proxy)
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout))
defer cancel()
delay, err := proxy.URLTest(ctx, url)
if ctx.Err() != nil {
render.Status(r, http.StatusGatewayTimeout)
render.JSON(w, r, ErrRequestTimeout)
return
}
if err != nil || delay == 0 {
render.Status(r, http.StatusServiceUnavailable)
render.JSON(w, r, newError("An error occurred in the delay test"))
return
}
*/
render.JSON(w, r, render.M{
"delay": 114514,
})
}

View File

@@ -0,0 +1,58 @@
package clashapi
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func ruleProviderRouter() http.Handler {
r := chi.NewRouter()
r.Get("/", getRuleProviders)
r.Route("/{name}", func(r chi.Router) {
r.Use(parseProviderName, findRuleProviderByName)
r.Get("/", getRuleProvider)
r.Put("/", updateRuleProvider)
})
return r
}
func getRuleProviders(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{
"providers": []string{},
})
}
func getRuleProvider(w http.ResponseWriter, r *http.Request) {
// provider := r.Context().Value(CtxKeyProvider).(provider.RuleProvider)
// render.JSON(w, r, provider)
render.NoContent(w, r)
}
func updateRuleProvider(w http.ResponseWriter, r *http.Request) {
/*provider := r.Context().Value(CtxKeyProvider).(provider.RuleProvider)
if err := provider.Update(); err != nil {
render.Status(r, http.StatusServiceUnavailable)
render.JSON(w, r, newError(err.Error()))
return
}*/
render.NoContent(w, r)
}
func findRuleProviderByName(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
/*name := r.Context().Value(CtxKeyProviderName).(string)
providers := tunnel.RuleProviders()
provider, exist := providers[name]
if !exist {*/
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
//return
//}
// ctx := context.WithValue(r.Context(), CtxKeyProvider, provider)
// next.ServeHTTP(w, r.WithContext(ctx))
})
}

View File

@@ -0,0 +1,41 @@
package clashapi
import (
"net/http"
"github.com/sagernet/sing-box/adapter"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func ruleRouter(router adapter.Router) http.Handler {
r := chi.NewRouter()
r.Get("/", getRules(router))
return r
}
type Rule struct {
Type string `json:"type"`
Payload string `json:"payload"`
Proxy string `json:"proxy"`
}
func getRules(router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
rawRules := router.Rules()
var rules []Rule
for _, rule := range rawRules {
rules = append(rules, Rule{
Type: rule.Type(),
Payload: rule.String(),
Proxy: rule.Outbound(),
})
}
render.JSON(w, r, render.M{
"rules": rules,
})
}
}

View File

@@ -0,0 +1,98 @@
package clashapi
import (
"net/http"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func scriptRouter() http.Handler {
r := chi.NewRouter()
r.Post("/", testScript)
r.Patch("/", patchScript)
return r
}
/*type TestScriptRequest struct {
Script *string `json:"script"`
Metadata C.Metadata `json:"metadata"`
}*/
func testScript(w http.ResponseWriter, r *http.Request) {
/* req := TestScriptRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
fn := tunnel.ScriptFn()
if req.Script == nil && fn == nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("should send `script`"))
return
}
if !req.Metadata.Valid() {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("metadata not valid"))
return
}
if req.Script != nil {
var err error
fn, err = script.ParseScript(*req.Script)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(err.Error()))
return
}
}
ctx, _ := script.MakeContext(tunnel.ProxyProviders(), tunnel.RuleProviders())
thread := &starlark.Thread{}
ret, err := starlark.Call(thread, fn, starlark.Tuple{ctx, script.MakeMetadata(&req.Metadata)}, nil)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(err.Error()))
return
}
elm, ok := ret.(starlark.String)
if !ok {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, "script fn must return a string")
return
}
render.JSON(w, r, render.M{
"result": string(elm),
})*/
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError("not implemented"))
}
type PatchScriptRequest struct {
Script string `json:"script"`
}
func patchScript(w http.ResponseWriter, r *http.Request) {
/*req := PatchScriptRequest{}
if err := render.DecodeJSON(r.Body, &req); err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
fn, err := script.ParseScript(req.Script)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, newError(err.Error()))
return
}
tunnel.UpdateScript(fn)*/
render.NoContent(w, r)
}

View File

@@ -0,0 +1,298 @@
package clashapi
import (
"bytes"
"context"
"net"
"net/http"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontroll"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
N "github.com/sagernet/sing/common/network"
"github.com/go-chi/chi/v5"
"github.com/go-chi/cors"
"github.com/go-chi/render"
"github.com/goccy/go-json"
"github.com/gorilla/websocket"
)
var (
_ adapter.Service = (*Server)(nil)
_ adapter.TrafficController = (*Server)(nil)
)
type Server struct {
logger log.Logger
httpServer *http.Server
trafficManager *trafficontroll.Manager
}
func NewServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) *Server {
trafficManager := trafficontroll.NewManager()
chiRouter := chi.NewRouter()
cors := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
MaxAge: 300,
})
chiRouter.Use(cors.Handler)
chiRouter.Group(func(r chi.Router) {
r.Use(authentication(options.Secret))
r.Get("/", hello)
r.Get("/logs", getLogs(logFactory))
r.Get("/traffic", traffic(trafficManager))
r.Get("/version", version)
r.Mount("/configs", configRouter(logFactory))
r.Mount("/proxies", proxyRouter())
r.Mount("/rules", ruleRouter(router))
r.Mount("/connections", connectionRouter(trafficManager))
r.Mount("/providers/proxies", proxyProviderRouter())
r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter())
r.Mount("/profile", profileRouter())
r.Mount("/cache", cacheRouter())
})
return &Server{
logFactory.NewLogger("clash-api"),
&http.Server{
Addr: options.ExternalController,
Handler: chiRouter,
},
trafficManager,
}
}
func (s *Server) Start() error {
listener, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return E.Cause(err, "external controller listen error")
}
s.logger.Info("restful api listening at ", listener.Addr())
go func() {
err = s.httpServer.Serve(listener)
if err != nil && !E.IsClosed(err) {
log.Error("external controller serve error: ", err)
}
}()
return nil
}
func (s *Server) Close() error {
return s.httpServer.Close()
}
func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) net.Conn {
return trafficontroll.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), matchedRule)
}
func (s *Server) RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule) N.PacketConn {
return trafficontroll.NewUDPTracker(conn, s.trafficManager, castMetadata(metadata), matchedRule)
}
func castMetadata(metadata adapter.InboundContext) trafficontroll.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
}
return trafficontroll.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",
}
}
func authentication(serverSecret string) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if serverSecret == "" {
next.ServeHTTP(w, r)
return
}
// Browser websocket not support custom header
if websocket.IsWebSocketUpgrade(r) && r.URL.Query().Get("token") != "" {
token := r.URL.Query().Get("token")
if token != serverSecret {
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ErrUnauthorized)
return
}
next.ServeHTTP(w, r)
return
}
header := r.Header.Get("Authorization")
bearer, token, found := strings.Cut(header, " ")
hasInvalidHeader := bearer != "Bearer"
hasInvalidSecret := !found || token != serverSecret
if hasInvalidHeader || hasInvalidSecret {
render.Status(r, http.StatusUnauthorized)
render.JSON(w, r, ErrUnauthorized)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}
func hello(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{"hello": "clash"})
}
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
type Traffic struct {
Up int64 `json:"up"`
Down int64 `json:"down"`
}
func traffic(trafficManager *trafficontroll.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var wsConn *websocket.Conn
if websocket.IsWebSocketUpgrade(r) {
var err error
wsConn, err = upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
}
if wsConn == nil {
w.Header().Set("Content-Type", "application/json")
render.Status(r, http.StatusOK)
}
tick := time.NewTicker(time.Second)
defer tick.Stop()
buf := &bytes.Buffer{}
var err error
for range tick.C {
buf.Reset()
up, down := trafficManager.Now()
if err := json.NewEncoder(buf).Encode(Traffic{
Up: up,
Down: down,
}); err != nil {
break
}
if wsConn == nil {
_, err = w.Write(buf.Bytes())
w.(http.Flusher).Flush()
} else {
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
}
if err != nil {
break
}
}
}
}
type Log struct {
Type string `json:"type"`
Payload string `json:"payload"`
}
func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
levelText := r.URL.Query().Get("level")
if levelText == "" {
levelText = "info"
}
level, ok := log.ParseLevel(levelText)
if ok != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
var wsConn *websocket.Conn
if websocket.IsWebSocketUpgrade(r) {
var err error
wsConn, err = upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
}
if wsConn == nil {
w.Header().Set("Content-Type", "application/json")
render.Status(r, http.StatusOK)
}
subscription, done, err := logFactory.Subscribe()
if err != nil {
log.Warn(err)
render.Status(r, http.StatusInternalServerError)
return
}
defer logFactory.UnSubscribe(subscription)
buf := &bytes.Buffer{}
var logEntry log.Entry
for {
select {
case <-done:
return
case logEntry = <-subscription:
}
if logEntry.Level > level {
continue
}
buf.Reset()
err = json.NewEncoder(buf).Encode(Log{
Type: log.FormatLevel(logEntry.Level),
Payload: logEntry.Message,
})
if err != nil {
break
}
if wsConn == nil {
_, err = w.Write(buf.Bytes())
w.(http.Flusher).Flush()
} else {
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
}
if err != nil {
break
}
}
}
}
func version(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": false})
}

View File

@@ -0,0 +1,94 @@
package trafficontroll
import (
"time"
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
"go.uber.org/atomic"
)
type Manager struct {
connections compatible.Map[string, tracker]
uploadTemp *atomic.Int64
downloadTemp *atomic.Int64
uploadBlip *atomic.Int64
downloadBlip *atomic.Int64
uploadTotal *atomic.Int64
downloadTotal *atomic.Int64
}
func NewManager() *Manager {
manager := &Manager{
uploadTemp: atomic.NewInt64(0),
downloadTemp: atomic.NewInt64(0),
uploadBlip: atomic.NewInt64(0),
downloadBlip: atomic.NewInt64(0),
uploadTotal: atomic.NewInt64(0),
downloadTotal: atomic.NewInt64(0),
}
go manager.handle()
return manager
}
func (m *Manager) Join(c tracker) {
m.connections.Store(c.ID(), c)
}
func (m *Manager) Leave(c tracker) {
m.connections.Delete(c.ID())
}
func (m *Manager) PushUploaded(size int64) {
m.uploadTemp.Add(size)
m.uploadTotal.Add(size)
}
func (m *Manager) PushDownloaded(size int64) {
m.downloadTemp.Add(size)
m.downloadTotal.Add(size)
}
func (m *Manager) Now() (up int64, down int64) {
return m.uploadBlip.Load(), m.downloadBlip.Load()
}
func (m *Manager) Snapshot() *Snapshot {
connections := []tracker{}
m.connections.Range(func(_ string, value tracker) bool {
connections = append(connections, value)
return true
})
return &Snapshot{
UploadTotal: m.uploadTotal.Load(),
DownloadTotal: m.downloadTotal.Load(),
Connections: connections,
}
}
func (m *Manager) ResetStatistic() {
m.uploadTemp.Store(0)
m.uploadBlip.Store(0)
m.uploadTotal.Store(0)
m.downloadTemp.Store(0)
m.downloadBlip.Store(0)
m.downloadTotal.Store(0)
}
func (m *Manager) handle() {
ticker := time.NewTicker(time.Second)
for range ticker.C {
m.uploadBlip.Store(m.uploadTemp.Load())
m.uploadTemp.Store(0)
m.downloadBlip.Store(m.downloadTemp.Load())
m.downloadTemp.Store(0)
}
}
type Snapshot struct {
DownloadTotal int64 `json:"downloadTotal"`
UploadTotal int64 `json:"uploadTotal"`
Connections []tracker `json:"connections"`
}

View File

@@ -0,0 +1,162 @@
package trafficontroll
import (
"net"
"net/netip"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/gofrs/uuid"
"go.uber.org/atomic"
)
type Metadata struct {
NetWork string `json:"network"`
Type string `json:"type"`
SrcIP netip.Addr `json:"sourceIP"`
DstIP netip.Addr `json:"destinationIP"`
SrcPort string `json:"sourcePort"`
DstPort string `json:"destinationPort"`
Host string `json:"host"`
DNSMode string `json:"dnsMode"`
ProcessPath string `json:"processPath"`
}
type tracker interface {
ID() string
Close() error
}
type trackerInfo struct {
UUID uuid.UUID `json:"id"`
Metadata Metadata `json:"metadata"`
UploadTotal *atomic.Int64 `json:"upload"`
DownloadTotal *atomic.Int64 `json:"download"`
Start time.Time `json:"start"`
Chain []string `json:"chains"`
Rule string `json:"rule"`
RulePayload string `json:"rulePayload"`
}
type tcpTracker struct {
net.Conn `json:"-"`
*trackerInfo
manager *Manager
}
func (tt *tcpTracker) ID() string {
return tt.UUID.String()
}
func (tt *tcpTracker) Read(b []byte) (int, error) {
n, err := tt.Conn.Read(b)
download := int64(n)
tt.manager.PushDownloaded(download)
tt.DownloadTotal.Add(download)
return n, err
}
func (tt *tcpTracker) Write(b []byte) (int, error) {
n, err := tt.Conn.Write(b)
upload := int64(n)
tt.manager.PushUploaded(upload)
tt.UploadTotal.Add(upload)
return n, err
}
func (tt *tcpTracker) Close() error {
tt.manager.Leave(tt)
return tt.Conn.Close()
}
func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, rule adapter.Rule) *tcpTracker {
uuid, _ := uuid.NewV4()
t := &tcpTracker{
Conn: conn,
manager: manager,
trackerInfo: &trackerInfo{
UUID: uuid,
Start: time.Now(),
Metadata: metadata,
Chain: []string{},
Rule: "",
UploadTotal: atomic.NewInt64(0),
DownloadTotal: atomic.NewInt64(0),
},
}
if rule != nil {
t.trackerInfo.Rule = rule.Outbound()
t.trackerInfo.RulePayload = rule.String()
}
manager.Join(t)
return t
}
type udpTracker struct {
N.PacketConn `json:"-"`
*trackerInfo
manager *Manager
}
func (ut *udpTracker) ID() string {
return ut.UUID.String()
}
func (ut *udpTracker) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
destination, err = ut.PacketConn.ReadPacket(buffer)
if err == nil {
download := int64(buffer.Len())
ut.manager.PushDownloaded(download)
ut.DownloadTotal.Add(download)
}
return
}
func (ut *udpTracker) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
upload := int64(buffer.Len())
err := ut.PacketConn.WritePacket(buffer, destination)
if err != nil {
return err
}
ut.manager.PushUploaded(upload)
ut.UploadTotal.Add(upload)
return nil
}
func (ut *udpTracker) Close() error {
ut.manager.Leave(ut)
return ut.PacketConn.Close()
}
func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, rule adapter.Rule) *udpTracker {
uuid, _ := uuid.NewV4()
ut := &udpTracker{
PacketConn: conn,
manager: manager,
trackerInfo: &trackerInfo{
UUID: uuid,
Start: time.Now(),
Metadata: metadata,
Chain: []string{},
Rule: "",
UploadTotal: atomic.NewInt64(0),
DownloadTotal: atomic.NewInt64(0),
},
}
if rule != nil {
ut.trackerInfo.Rule = rule.Outbound()
ut.trackerInfo.RulePayload = rule.String()
}
manager.Join(ut)
return ut
}