mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-25 10:01:30 +00:00
Add basic clash api
This commit is contained in:
parent
c7fabe40ed
commit
c5b3e8b042
|
@ -16,6 +16,7 @@ type Inbound interface {
|
|||
|
||||
type InboundContext struct {
|
||||
Inbound string
|
||||
InboundType string
|
||||
Network string
|
||||
Source M.Socksaddr
|
||||
Destination M.Socksaddr
|
||||
|
|
|
@ -34,10 +34,14 @@ type Router interface {
|
|||
AutoDetectInterface() bool
|
||||
AutoDetectInterfaceName() string
|
||||
AutoDetectInterfaceIndex() int
|
||||
|
||||
Rules() []Rule
|
||||
SetTrafficController(controller TrafficController)
|
||||
}
|
||||
|
||||
type Rule interface {
|
||||
Service
|
||||
Type() string
|
||||
UpdateGeosite() error
|
||||
Match(metadata *InboundContext) bool
|
||||
Outbound() string
|
||||
|
|
13
adapter/traffic_controller.go
Normal file
13
adapter/traffic_controller.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type TrafficController interface {
|
||||
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) net.Conn
|
||||
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) N.PacketConn
|
||||
}
|
57
box.go
57
box.go
|
@ -7,6 +7,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/experimental/clashapi"
|
||||
"github.com/sagernet/sing-box/inbound"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
@ -20,20 +21,27 @@ import (
|
|||
var _ adapter.Service = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
router adapter.Router
|
||||
inbounds []adapter.Inbound
|
||||
outbounds []adapter.Outbound
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
logFile *os.File
|
||||
createdAt time.Time
|
||||
router adapter.Router
|
||||
inbounds []adapter.Inbound
|
||||
outbounds []adapter.Outbound
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
logFile *os.File
|
||||
clashServer *clashapi.Server
|
||||
}
|
||||
|
||||
func New(ctx context.Context, options option.Options) (*Box, error) {
|
||||
createdAt := time.Now()
|
||||
logOptions := common.PtrValueOrDefault(options.Log)
|
||||
|
||||
var needClashAPI bool
|
||||
if options.Experimental != nil && options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
|
||||
needClashAPI = true
|
||||
}
|
||||
|
||||
var logFactory log.Factory
|
||||
var observableLogFactory log.ObservableFactory
|
||||
var logFile *os.File
|
||||
if logOptions.Disabled {
|
||||
logFactory = log.NewNOPFactory()
|
||||
|
@ -58,7 +66,12 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
|||
FullTimestamp: logOptions.Timestamp,
|
||||
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
||||
}
|
||||
logFactory = log.NewFactory(logFormatter, logWriter)
|
||||
if needClashAPI {
|
||||
observableLogFactory = log.NewObservableFactory(logFormatter, logWriter)
|
||||
logFactory = observableLogFactory
|
||||
} else {
|
||||
logFactory = log.NewFactory(logFormatter, logWriter)
|
||||
}
|
||||
if logOptions.Level != "" {
|
||||
logLevel, err := log.ParseLevel(logOptions.Level)
|
||||
if err != nil {
|
||||
|
@ -127,14 +140,21 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var clashServer *clashapi.Server
|
||||
if needClashAPI {
|
||||
clashServer = clashapi.NewServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
|
||||
router.SetTrafficController(clashServer)
|
||||
}
|
||||
return &Box{
|
||||
router: router,
|
||||
inbounds: inbounds,
|
||||
outbounds: outbounds,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.NewLogger(""),
|
||||
logFile: logFile,
|
||||
router: router,
|
||||
inbounds: inbounds,
|
||||
outbounds: outbounds,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.NewLogger(""),
|
||||
logFile: logFile,
|
||||
clashServer: clashServer,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -152,6 +172,12 @@ func (s *Box) Start() error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if s.clashServer != nil {
|
||||
err = s.clashServer.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start clash api")
|
||||
}
|
||||
}
|
||||
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||
return nil
|
||||
}
|
||||
|
@ -166,5 +192,6 @@ func (s *Box) Close() error {
|
|||
return common.Close(
|
||||
s.router,
|
||||
common.PtrOrNil(s.logFile),
|
||||
common.PtrOrNil(s.clashServer),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ func run(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
if disableColor {
|
||||
if options.Log == nil {
|
||||
options.Log = &option.LogOption{}
|
||||
options.Log = &option.LogOptions{}
|
||||
}
|
||||
options.Log.DisableColor = true
|
||||
}
|
||||
|
|
23
experimental/clashapi/cache.go
Normal file
23
experimental/clashapi/cache.go
Normal 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)
|
||||
}
|
17
experimental/clashapi/common.go
Normal file
17
experimental/clashapi/common.go
Normal 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
|
||||
}
|
49
experimental/clashapi/compatible/map.go
Normal file
49
experimental/clashapi/compatible/map.go
Normal 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{}}
|
||||
}
|
49
experimental/clashapi/configs.go
Normal file
49
experimental/clashapi/configs.go
Normal 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)
|
||||
}
|
97
experimental/clashapi/connections.go
Normal file
97
experimental/clashapi/connections.go
Normal 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)
|
||||
}
|
||||
}
|
14
experimental/clashapi/ctxkeys.go
Normal file
14
experimental/clashapi/ctxkeys.go
Normal 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)
|
||||
}
|
22
experimental/clashapi/errors.go
Normal file
22
experimental/clashapi/errors.go
Normal 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}
|
||||
}
|
53
experimental/clashapi/profile.go
Normal file
53
experimental/clashapi/profile.go
Normal 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
|
||||
}
|
||||
}*/
|
||||
}
|
74
experimental/clashapi/provider.go
Normal file
74
experimental/clashapi/provider.go
Normal 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))
|
||||
})
|
||||
}
|
122
experimental/clashapi/proxies.go
Normal file
122
experimental/clashapi/proxies.go
Normal 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,
|
||||
})
|
||||
}
|
58
experimental/clashapi/ruleprovider.go
Normal file
58
experimental/clashapi/ruleprovider.go
Normal 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))
|
||||
})
|
||||
}
|
41
experimental/clashapi/rules.go
Normal file
41
experimental/clashapi/rules.go
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
98
experimental/clashapi/script.go
Normal file
98
experimental/clashapi/script.go
Normal 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)
|
||||
}
|
298
experimental/clashapi/server.go
Normal file
298
experimental/clashapi/server.go
Normal 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})
|
||||
}
|
94
experimental/clashapi/trafficontroll/manager.go
Normal file
94
experimental/clashapi/trafficontroll/manager.go
Normal 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"`
|
||||
}
|
162
experimental/clashapi/trafficontroll/tracker.go
Normal file
162
experimental/clashapi/trafficontroll/tracker.go
Normal 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
|
||||
}
|
10
go.mod
10
go.mod
|
@ -4,25 +4,29 @@ go 1.18
|
|||
|
||||
require (
|
||||
github.com/database64128/tfo-go v1.1.0
|
||||
github.com/go-chi/chi/v5 v5.0.7
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/go-chi/render v1.0.1
|
||||
github.com/goccy/go-json v0.9.10
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/oschwald/maxminddb-golang v1.9.0
|
||||
github.com/sagernet/sing v0.0.0-20220718035659-3d74b823ed56
|
||||
github.com/sagernet/sing-dns v0.0.0-20220711062726-c64e938e4619
|
||||
github.com/sagernet/sing-shadowsocks v0.0.0-20220717063942-45a2ad9cd41f
|
||||
github.com/sagernet/sing-tun v0.0.0-20220717030718-f53aabff275f
|
||||
github.com/sagernet/sing-vmess v0.0.0-20220718031323-07c377156e4a
|
||||
github.com/spf13/cobra v1.5.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
go.uber.org/atomic v1.9.0
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
|
||||
golang.org/x/net v0.0.0-20220708220712-1185a9018129
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
|
||||
)
|
||||
|
||||
require github.com/sagernet/sing-vmess v0.0.0-20220718031323-07c377156e4a
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect; indirectg
|
||||
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||
github.com/gofrs/uuid v4.2.0+incompatible
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
||||
|
|
11
go.sum
11
go.sum
|
@ -4,12 +4,20 @@ github.com/database64128/tfo-go v1.1.0/go.mod h1:95pOT8bnV3P2Lmu9upHNWFHz6dYGJ9c
|
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
|
||||
github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
|
@ -43,6 +51,7 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
|||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
|
@ -51,6 +60,8 @@ github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYp
|
|||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg=
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20220708220712-1185a9018129 h1:vucSRfWwTsoXro7P+3Cjlr6flUMtzCwzlvkxEQtHHB0=
|
||||
|
|
|
@ -158,6 +158,7 @@ func (a *myInboundAdapter) loopTCPIn() {
|
|||
ctx := log.ContextWithNewID(a.ctx)
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = a.tag
|
||||
metadata.InboundType = a.protocol
|
||||
metadata.SniffEnabled = a.listenOptions.SniffEnabled
|
||||
metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
|
||||
metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
|
||||
|
@ -191,6 +192,7 @@ func (a *myInboundAdapter) loopUDPIn() {
|
|||
buffer.Truncate(n)
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = a.tag
|
||||
metadata.InboundType = a.protocol
|
||||
metadata.SniffEnabled = a.listenOptions.SniffEnabled
|
||||
metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
|
||||
metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
|
||||
|
@ -222,6 +224,7 @@ func (a *myInboundAdapter) loopUDPOOBIn() {
|
|||
buffer.Truncate(n)
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = a.tag
|
||||
metadata.InboundType = a.protocol
|
||||
metadata.SniffEnabled = a.listenOptions.SniffEnabled
|
||||
metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
|
||||
metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
|
||||
|
@ -247,6 +250,7 @@ func (a *myInboundAdapter) loopUDPInThreadSafe() {
|
|||
buffer.Truncate(n)
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = a.tag
|
||||
metadata.InboundType = a.protocol
|
||||
metadata.SniffEnabled = a.listenOptions.SniffEnabled
|
||||
metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
|
||||
metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
|
||||
|
@ -274,6 +278,7 @@ func (a *myInboundAdapter) loopUDPOOBInThreadSafe() {
|
|||
buffer.Truncate(n)
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = a.tag
|
||||
metadata.InboundType = a.protocol
|
||||
metadata.SniffEnabled = a.listenOptions.SniffEnabled
|
||||
metadata.SniffOverrideDestination = a.listenOptions.SniffOverrideDestination
|
||||
metadata.DomainStrategy = dns.DomainStrategy(a.listenOptions.DomainStrategy)
|
||||
|
|
|
@ -102,6 +102,7 @@ func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata
|
|||
ctx = log.ContextWithNewID(ctx)
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = t.tag
|
||||
metadata.InboundType = C.TypeTun
|
||||
metadata.Network = C.NetworkTCP
|
||||
metadata.Source = upstreamMetadata.Source
|
||||
metadata.Destination = upstreamMetadata.Destination
|
||||
|
@ -122,6 +123,7 @@ func (t *Tun) NewPacketConnection(ctx context.Context, conn N.PacketConn, upstre
|
|||
ctx = log.ContextWithNewID(ctx)
|
||||
var metadata adapter.InboundContext
|
||||
metadata.Inbound = t.tag
|
||||
metadata.InboundType = C.TypeTun
|
||||
metadata.Network = C.NetworkUDP
|
||||
metadata.Source = upstreamMetadata.Source
|
||||
metadata.Destination = upstreamMetadata.Destination
|
||||
|
|
|
@ -78,6 +78,66 @@ func (f Formatter) Format(ctx context.Context, level Level, tag string, message
|
|||
return message
|
||||
}
|
||||
|
||||
func (f Formatter) FormatWithSimple(ctx context.Context, level Level, tag string, message string, timestamp time.Time) (string, string) {
|
||||
levelString := strings.ToUpper(FormatLevel(level))
|
||||
if !f.DisableColors {
|
||||
switch level {
|
||||
case LevelDebug, LevelTrace:
|
||||
levelString = aurora.White(levelString).String()
|
||||
case LevelInfo:
|
||||
levelString = aurora.Cyan(levelString).String()
|
||||
case LevelWarn:
|
||||
levelString = aurora.Yellow(levelString).String()
|
||||
case LevelError, LevelFatal, LevelPanic:
|
||||
levelString = aurora.Red(levelString).String()
|
||||
}
|
||||
}
|
||||
if tag != "" {
|
||||
message = tag + ": " + message
|
||||
}
|
||||
messageSimple := message
|
||||
var id uint32
|
||||
var hasId bool
|
||||
if ctx != nil {
|
||||
id, hasId = IDFromContext(ctx)
|
||||
}
|
||||
if hasId {
|
||||
if !f.DisableColors {
|
||||
var color aurora.Color
|
||||
color = aurora.Color(uint8(id))
|
||||
color %= 215
|
||||
row := uint(color / 36)
|
||||
column := uint(color % 36)
|
||||
|
||||
var r, g, b float32
|
||||
r = float32(row * 51)
|
||||
g = float32(column / 6 * 51)
|
||||
b = float32((column % 6) * 51)
|
||||
luma := 0.2126*r + 0.7152*g + 0.0722*b
|
||||
if luma < 60 {
|
||||
row = 5 - row
|
||||
column = 35 - column
|
||||
color = aurora.Color(row*36 + column)
|
||||
}
|
||||
color += 16
|
||||
color = color << 16
|
||||
color |= 1 << 14
|
||||
message = F.ToString("[", aurora.Colorize(id, color).String(), "] ", message)
|
||||
} else {
|
||||
message = F.ToString("[", id, "] ", message)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case f.DisableTimestamp:
|
||||
message = levelString + " " + message
|
||||
case f.FullTimestamp:
|
||||
message = F.ToString(int(timestamp.Sub(f.BaseTime)/time.Second)) + " " + levelString + " " + message
|
||||
default:
|
||||
message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message
|
||||
}
|
||||
return message, messageSimple
|
||||
}
|
||||
|
||||
func xd(value int, x int) string {
|
||||
message := strconv.Itoa(value)
|
||||
for len(message) < x {
|
||||
|
|
|
@ -66,17 +66,16 @@ func (l *observableLogger) Log(ctx context.Context, level Level, args []any) {
|
|||
if level > l.level {
|
||||
return
|
||||
}
|
||||
message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), time.Now()) + "\n"
|
||||
message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), time.Now())
|
||||
if level == LevelPanic {
|
||||
panic(message)
|
||||
}
|
||||
l.writer.Write([]byte(message))
|
||||
l.writer.Write([]byte{'\n'})
|
||||
if level == LevelFatal {
|
||||
os.Exit(1)
|
||||
}
|
||||
if l.subscriber != nil {
|
||||
l.subscriber.Emit(Entry{level, message})
|
||||
}
|
||||
l.subscriber.Emit(Entry{level, messageSimple})
|
||||
}
|
||||
|
||||
func (l *observableLogger) Trace(args ...any) {
|
||||
|
|
|
@ -11,11 +11,12 @@ import (
|
|||
)
|
||||
|
||||
type _Options struct {
|
||||
Log *LogOption `json:"log,omitempty"`
|
||||
DNS *DNSOptions `json:"dns,omitempty"`
|
||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
Route *RouteOptions `json:"route,omitempty"`
|
||||
Log *LogOptions `json:"log,omitempty"`
|
||||
DNS *DNSOptions `json:"dns,omitempty"`
|
||||
Inbounds []Inbound `json:"inbounds,omitempty"`
|
||||
Outbounds []Outbound `json:"outbounds,omitempty"`
|
||||
Route *RouteOptions `json:"route,omitempty"`
|
||||
Experimental *ExperimentalOptions `json:"experimental,omitempty"`
|
||||
}
|
||||
|
||||
type Options _Options
|
||||
|
@ -41,10 +42,11 @@ func (o Options) Equals(other Options) bool {
|
|||
common.PtrEquals(o.DNS, other.DNS) &&
|
||||
common.SliceEquals(o.Inbounds, other.Inbounds) &&
|
||||
common.ComparableSliceEquals(o.Outbounds, other.Outbounds) &&
|
||||
common.PtrEquals(o.Route, other.Route)
|
||||
common.PtrEquals(o.Route, other.Route) &&
|
||||
common.ComparablePtrEquals(o.Experimental, other.Experimental)
|
||||
}
|
||||
|
||||
type LogOption struct {
|
||||
type LogOptions struct {
|
||||
Disabled bool `json:"disabled,omitempty"`
|
||||
Level string `json:"level,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
|
|
10
option/experimental.go
Normal file
10
option/experimental.go
Normal file
|
@ -0,0 +1,10 @@
|
|||
package option
|
||||
|
||||
type ExperimentalOptions struct {
|
||||
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
|
||||
}
|
||||
|
||||
type ClashAPIOptions struct {
|
||||
ExternalController string `json:"external_controller,omitempty"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
}
|
|
@ -69,6 +69,8 @@ type Router struct {
|
|||
autoDetectInterface bool
|
||||
defaultInterface string
|
||||
interfaceMonitor DefaultInterfaceMonitor
|
||||
|
||||
trafficController adapter.TrafficController
|
||||
}
|
||||
|
||||
func NewRouter(ctx context.Context, logger log.ContextLogger, dnsLogger log.ContextLogger, options option.RouteOptions, dnsOptions option.DNSOptions) (*Router, error) {
|
||||
|
@ -438,11 +440,14 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
|
|||
metadata.DestinationAddresses = addresses
|
||||
r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]")
|
||||
}
|
||||
detour := r.match(ctx, metadata, r.defaultOutboundForConnection)
|
||||
matchedRule, detour := r.match(ctx, metadata, r.defaultOutboundForConnection)
|
||||
if !common.Contains(detour.Network(), C.NetworkTCP) {
|
||||
conn.Close()
|
||||
return E.New("missing supported outbound, closing connection")
|
||||
}
|
||||
if r.trafficController != nil {
|
||||
conn = r.trafficController.RoutedConnection(ctx, conn, metadata, matchedRule)
|
||||
}
|
||||
return detour.NewConnection(ctx, conn, metadata)
|
||||
}
|
||||
|
||||
|
@ -480,11 +485,14 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
|
|||
metadata.DestinationAddresses = addresses
|
||||
r.dnsLogger.DebugContext(ctx, "resolved [", strings.Join(F.MapToString(metadata.DestinationAddresses), " "), "]")
|
||||
}
|
||||
detour := r.match(ctx, metadata, r.defaultOutboundForPacketConnection)
|
||||
matchedRule, detour := r.match(ctx, metadata, r.defaultOutboundForPacketConnection)
|
||||
if !common.Contains(detour.Network(), C.NetworkUDP) {
|
||||
conn.Close()
|
||||
return E.New("missing supported outbound, closing packet connection")
|
||||
}
|
||||
if r.trafficController != nil {
|
||||
conn = r.trafficController.RoutedPacketConnection(ctx, conn, metadata, matchedRule)
|
||||
}
|
||||
return detour.NewPacketConnection(ctx, conn, metadata)
|
||||
}
|
||||
|
||||
|
@ -500,18 +508,18 @@ func (r *Router) LookupDefault(ctx context.Context, domain string) ([]netip.Addr
|
|||
return r.dnsClient.Lookup(ctx, r.matchDNS(ctx), domain, r.defaultDomainStrategy)
|
||||
}
|
||||
|
||||
func (r *Router) match(ctx context.Context, metadata adapter.InboundContext, defaultOutbound adapter.Outbound) adapter.Outbound {
|
||||
func (r *Router) match(ctx context.Context, metadata adapter.InboundContext, defaultOutbound adapter.Outbound) (adapter.Rule, adapter.Outbound) {
|
||||
for i, rule := range r.rules {
|
||||
if rule.Match(&metadata) {
|
||||
detour := rule.Outbound()
|
||||
r.logger.DebugContext(ctx, "match[", i, "] ", rule.String(), " => ", detour)
|
||||
if outbound, loaded := r.Outbound(detour); loaded {
|
||||
return outbound
|
||||
return rule, outbound
|
||||
}
|
||||
r.logger.ErrorContext(ctx, "outbound not found: ", detour)
|
||||
}
|
||||
}
|
||||
return defaultOutbound
|
||||
return nil, defaultOutbound
|
||||
}
|
||||
|
||||
func (r *Router) matchDNS(ctx context.Context) dns.Transport {
|
||||
|
@ -559,6 +567,14 @@ func (r *Router) AutoDetectInterfaceIndex() int {
|
|||
return r.interfaceMonitor.DefaultInterfaceIndex()
|
||||
}
|
||||
|
||||
func (r *Router) Rules() []adapter.Rule {
|
||||
return r.rules
|
||||
}
|
||||
|
||||
func (r *Router) SetTrafficController(controller adapter.TrafficController) {
|
||||
r.trafficController = controller
|
||||
}
|
||||
|
||||
func hasGeoRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
|
||||
for _, rule := range rules {
|
||||
switch rule.Type {
|
||||
|
|
|
@ -48,6 +48,10 @@ type DefaultRule struct {
|
|||
outbound string
|
||||
}
|
||||
|
||||
func (r *DefaultRule) Type() string {
|
||||
return C.RuleTypeDefault
|
||||
}
|
||||
|
||||
type RuleItem interface {
|
||||
Match(metadata *adapter.InboundContext) bool
|
||||
String() string
|
||||
|
@ -238,6 +242,10 @@ type LogicalRule struct {
|
|||
outbound string
|
||||
}
|
||||
|
||||
func (r *LogicalRule) Type() string {
|
||||
return C.RuleTypeLogical
|
||||
}
|
||||
|
||||
func (r *LogicalRule) UpdateGeosite() error {
|
||||
for _, rule := range r.rules {
|
||||
err := rule.UpdateGeosite()
|
||||
|
|
|
@ -47,6 +47,10 @@ type DefaultDNSRule struct {
|
|||
outbound string
|
||||
}
|
||||
|
||||
func (r *DefaultDNSRule) Type() string {
|
||||
return C.RuleTypeDefault
|
||||
}
|
||||
|
||||
func NewDefaultDNSRule(router adapter.Router, logger log.ContextLogger, options option.DefaultDNSRule) (*DefaultDNSRule, error) {
|
||||
rule := &DefaultDNSRule{
|
||||
outbound: options.Server,
|
||||
|
@ -199,6 +203,10 @@ type LogicalDNSRule struct {
|
|||
outbound string
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) Type() string {
|
||||
return C.RuleTypeLogical
|
||||
}
|
||||
|
||||
func (r *LogicalDNSRule) UpdateGeosite() error {
|
||||
for _, rule := range r.rules {
|
||||
err := rule.UpdateGeosite()
|
||||
|
|
|
@ -22,9 +22,13 @@ require (
|
|||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/distribution v2.8.1+incompatible // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.7 // indirect
|
||||
github.com/go-chi/cors v1.2.1 // indirect
|
||||
github.com/go-chi/render v1.0.1 // indirect
|
||||
github.com/goccy/go-json v0.9.10 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.0.12 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
|
@ -42,6 +46,7 @@ require (
|
|||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/vishvananda/netlink v1.1.0 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
|
||||
|
|
11
test/go.sum
11
test/go.sum
|
@ -17,6 +17,12 @@ github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKoh
|
|||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
|
||||
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
|
||||
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
|
||||
github.com/goccy/go-json v0.9.10 h1:hCeNmprSNLB8B8vQKWl6DpuH0t60oEs+TAk9a7CScKc=
|
||||
github.com/goccy/go-json v0.9.10/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
|
||||
|
@ -29,6 +35,8 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
|||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
|
@ -73,6 +81,7 @@ github.com/spyzhov/ajson v0.7.1/go.mod h1:63V+CGM6f1Bu/p4nLIN8885ojBdt88TbLoSFzy
|
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
|
@ -83,6 +92,8 @@ github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695AP
|
|||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
|
|
|
@ -75,7 +75,7 @@ func testShadowsocksInboundWithShadowsocksRust(t *testing.T, method string, pass
|
|||
Cmd: []string{"-s", F.ToString("127.0.0.1:", serverPort), "-b", F.ToString("0.0.0.0:", clientPort), "-m", method, "-k", password, "-U"},
|
||||
})
|
||||
startInstance(t, option.Options{
|
||||
Log: &option.LogOption{
|
||||
Log: &option.LogOptions{
|
||||
Level: "error",
|
||||
},
|
||||
Inbounds: []option.Inbound{
|
||||
|
@ -107,7 +107,7 @@ func testShadowsocksOutboundWithShadowsocksRust(t *testing.T, method string, pas
|
|||
Cmd: []string{"-s", F.ToString("0.0.0.0:", serverPort), "-m", method, "-k", password, "-U"},
|
||||
})
|
||||
startInstance(t, option.Options{
|
||||
Log: &option.LogOption{
|
||||
Log: &option.LogOptions{
|
||||
Level: "error",
|
||||
},
|
||||
Inbounds: []option.Inbound{
|
||||
|
@ -144,7 +144,7 @@ func testShadowsocksSelf(t *testing.T, method string, password string) {
|
|||
clientPort := mkPort(t)
|
||||
testPort := mkPort(t)
|
||||
startInstance(t, option.Options{
|
||||
Log: &option.LogOption{
|
||||
Log: &option.LogOptions{
|
||||
Level: "error",
|
||||
},
|
||||
Inbounds: []option.Inbound{
|
||||
|
|
|
@ -139,7 +139,7 @@ func testVMessInboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, au
|
|||
})
|
||||
|
||||
startInstance(t, option.Options{
|
||||
Log: &option.LogOption{
|
||||
Log: &option.LogOptions{
|
||||
Level: "error",
|
||||
},
|
||||
Inbounds: []option.Inbound{
|
||||
|
@ -193,7 +193,7 @@ func testVMessOutboundWithV2Ray(t *testing.T, security string, uuid uuid.UUID, g
|
|||
})
|
||||
|
||||
startInstance(t, option.Options{
|
||||
Log: &option.LogOption{
|
||||
Log: &option.LogOptions{
|
||||
Level: "error",
|
||||
},
|
||||
Inbounds: []option.Inbound{
|
||||
|
@ -233,7 +233,7 @@ func testVMessSelf(t *testing.T, security string, uuid uuid.UUID, globalPadding
|
|||
clientPort := mkPort(t)
|
||||
testPort := mkPort(t)
|
||||
startInstance(t, option.Options{
|
||||
Log: &option.LogOption{
|
||||
Log: &option.LogOptions{
|
||||
Level: "error",
|
||||
},
|
||||
Inbounds: []option.Inbound{
|
||||
|
|
Loading…
Reference in a new issue