浏览代码

Add ssm api server

世界 2 年之前
父节点
当前提交
d34091f8fc

+ 2 - 0
.goreleaser.yaml

@@ -17,6 +17,7 @@ builds:
       - with_wireguard
       - with_wireguard
       - with_utls
       - with_utls
       - with_clash_api
       - with_clash_api
+      - with_ssm_api
     env:
     env:
       - CGO_ENABLED=0
       - CGO_ENABLED=0
     targets:
     targets:
@@ -50,6 +51,7 @@ builds:
       - with_wireguard
       - with_wireguard
       - with_utls
       - with_utls
       - with_clash_api
       - with_clash_api
+      - with_ssm_api
     env:
     env:
       - CGO_ENABLED=1
       - CGO_ENABLED=1
     overrides:
     overrides:

+ 1 - 1
Makefile

@@ -1,6 +1,6 @@
 NAME = sing-box
 NAME = sing-box
 COMMIT = $(shell git rev-parse --short HEAD)
 COMMIT = $(shell git rev-parse --short HEAD)
-TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api
+TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api,with_ssm_api
 TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_shadowsocksr
 TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_shadowsocksr
 PARAMS = -v -trimpath -tags "$(TAGS)" -ldflags "-s -w -buildid="
 PARAMS = -v -trimpath -tags "$(TAGS)" -ldflags "-s -w -buildid="
 MAIN = ./cmd/sing-box
 MAIN = ./cmd/sing-box

+ 13 - 0
adapter/experimental.go

@@ -48,3 +48,16 @@ type V2RayStatsService interface {
 	RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn
 	RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn
 	RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn
 	RoutedPacketConnection(inbound string, outbound string, user string, conn N.PacketConn) N.PacketConn
 }
 }
+
+type SSMServer interface {
+	Service
+	RoutedConnection(metadata InboundContext, conn net.Conn) net.Conn
+	RoutedPacketConnection(metadata InboundContext, conn N.PacketConn) N.PacketConn
+}
+
+type ManagedShadowsocksServer interface {
+	Inbound
+	Method() string
+	Password() string
+	UpdateUsers(users []string, uPSKs []string) error
+}

+ 4 - 0
adapter/router.go

@@ -17,6 +17,7 @@ import (
 type Router interface {
 type Router interface {
 	Service
 	Service
 
 
+	Inbound(tag string) (Inbound, bool)
 	Outbounds() []Outbound
 	Outbounds() []Outbound
 	Outbound(tag string) (Outbound, bool)
 	Outbound(tag string) (Outbound, bool)
 	DefaultOutbound(network string) Outbound
 	DefaultOutbound(network string) Outbound
@@ -45,6 +46,9 @@ type Router interface {
 
 
 	V2RayServer() V2RayServer
 	V2RayServer() V2RayServer
 	SetV2RayServer(server V2RayServer)
 	SetV2RayServer(server V2RayServer)
+
+	SSMServer() SSMServer
+	SetSSMServer(server SSMServer)
 }
 }
 
 
 type routerContextKey struct{}
 type routerContextKey struct{}

+ 21 - 0
box.go

@@ -32,6 +32,7 @@ type Box struct {
 	logFile     *os.File
 	logFile     *os.File
 	clashServer adapter.ClashServer
 	clashServer adapter.ClashServer
 	v2rayServer adapter.V2RayServer
 	v2rayServer adapter.V2RayServer
+	ssmServer   adapter.SSMServer
 	done        chan struct{}
 	done        chan struct{}
 }
 }
 
 
@@ -41,6 +42,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
 
 
 	var needClashAPI bool
 	var needClashAPI bool
 	var needV2RayAPI bool
 	var needV2RayAPI bool
+	var needSSMAPI bool
 	if options.Experimental != nil {
 	if options.Experimental != nil {
 		if options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
 		if options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
 			needClashAPI = true
 			needClashAPI = true
@@ -48,6 +50,9 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
 		if options.Experimental.V2RayAPI != nil && options.Experimental.V2RayAPI.Listen != "" {
 		if options.Experimental.V2RayAPI != nil && options.Experimental.V2RayAPI.Listen != "" {
 			needV2RayAPI = true
 			needV2RayAPI = true
 		}
 		}
+		if options.Experimental.SSMAPI != nil && options.Experimental.SSMAPI.Listen != "" {
+			needSSMAPI = true
+		}
 	}
 	}
 
 
 	var logFactory log.Factory
 	var logFactory log.Factory
@@ -156,6 +161,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
 
 
 	var clashServer adapter.ClashServer
 	var clashServer adapter.ClashServer
 	var v2rayServer adapter.V2RayServer
 	var v2rayServer adapter.V2RayServer
+	var ssmServer adapter.SSMServer
 	if needClashAPI {
 	if needClashAPI {
 		clashServer, err = experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
 		clashServer, err = experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
 		if err != nil {
 		if err != nil {
@@ -170,6 +176,13 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
 		}
 		}
 		router.SetV2RayServer(v2rayServer)
 		router.SetV2RayServer(v2rayServer)
 	}
 	}
+	if needSSMAPI {
+		ssmServer, err = experimental.NewSSMServer(router, logFactory.NewLogger("ssm-api"), common.PtrValueOrDefault(options.Experimental.SSMAPI))
+		if err != nil {
+			return nil, E.Cause(err, "create ssm api server")
+		}
+		router.SetSSMServer(ssmServer)
+	}
 	return &Box{
 	return &Box{
 		router:      router,
 		router:      router,
 		inbounds:    inbounds,
 		inbounds:    inbounds,
@@ -180,6 +193,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
 		logFile:     logFile,
 		logFile:     logFile,
 		clashServer: clashServer,
 		clashServer: clashServer,
 		v2rayServer: v2rayServer,
 		v2rayServer: v2rayServer,
+		ssmServer:   ssmServer,
 		done:        make(chan struct{}),
 		done:        make(chan struct{}),
 	}, nil
 	}, nil
 }
 }
@@ -244,6 +258,12 @@ func (s *Box) start() error {
 			return E.Cause(err, "start v2ray api server")
 			return E.Cause(err, "start v2ray api server")
 		}
 		}
 	}
 	}
+	if s.ssmServer != nil {
+		err = s.ssmServer.Start()
+		if err != nil {
+			return E.Cause(err, "start ssm api server")
+		}
+	}
 	s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
 	s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
 	return nil
 	return nil
 }
 }
@@ -266,6 +286,7 @@ func (s *Box) Close() error {
 		s.logFactory,
 		s.logFactory,
 		s.clashServer,
 		s.clashServer,
 		s.v2rayServer,
 		s.v2rayServer,
+		s.ssmServer,
 		common.PtrOrNil(s.logFile),
 		common.PtrOrNil(s.logFile),
 	)
 	)
 }
 }

+ 24 - 0
experimental/ssmapi.go

@@ -0,0 +1,24 @@
+package experimental
+
+import (
+	"os"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+)
+
+type SSMServerConstructor = func(router adapter.Router, logger log.Logger, options option.SSMAPIOptions) (adapter.SSMServer, error)
+
+var ssmServerConstructor SSMServerConstructor
+
+func RegisterSSMServerConstructor(constructor SSMServerConstructor) {
+	ssmServerConstructor = constructor
+}
+
+func NewSSMServer(router adapter.Router, logger log.Logger, options option.SSMAPIOptions) (adapter.SSMServer, error) {
+	if ssmServerConstructor == nil {
+		return nil, os.ErrInvalid
+	}
+	return ssmServerConstructor(router, logger, options)
+}

+ 214 - 0
experimental/ssmapi/api.go

@@ -0,0 +1,214 @@
+package ssmapi
+
+import (
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing/common"
+	"net/http"
+
+	"github.com/go-chi/chi/v5"
+	"github.com/go-chi/render"
+)
+
+func (s *Server) setupRoutes(r chi.Router) {
+	r.Group(func(r chi.Router) {
+		r.Get("/", s.getServerInfo)
+
+		r.Get("/nodes", s.getNodes)
+		r.Post("/nodes", s.addNode)
+		r.Get("/nodes/{id}", s.getNode)
+		r.Put("/nodes/{id}", s.updateNode)
+		r.Delete("/nodes/{id}", s.deleteNode)
+
+		r.Get("/users", s.listUser)
+		r.Post("/users", s.addUser)
+		r.Get("/users/{username}", s.getUser)
+		r.Put("/users/{username}", s.updateUser)
+		r.Delete("/users/{username}", s.deleteUser)
+
+		r.Get("/stats", s.getStats)
+	})
+}
+
+func (s *Server) getServerInfo(writer http.ResponseWriter, request *http.Request) {
+	render.JSON(writer, request, render.M{
+		"server":            "sing-box",
+		"apiVersion":        "v1",
+		"_sing_box_version": C.Version,
+	})
+}
+
+func (s *Server) getNodes(writer http.ResponseWriter, request *http.Request) {
+	var response struct {
+		Protocols   []string                `json:"protocols"`
+		Shadowsocks []ShadowsocksNodeObject `json:"shadowsocks,omitempty"`
+	}
+	for _, node := range s.nodes {
+		if !common.Contains(response.Protocols, node.Protocol()) {
+			response.Protocols = append(response.Protocols, node.Protocol())
+		}
+		switch node.Protocol() {
+		case C.TypeShadowsocks:
+			response.Shadowsocks = append(response.Shadowsocks, node.Shadowsocks())
+		}
+	}
+	render.JSON(writer, request, &response)
+}
+
+func (s *Server) addNode(writer http.ResponseWriter, request *http.Request) {
+	writer.WriteHeader(http.StatusNotImplemented)
+}
+
+func (s *Server) getNode(writer http.ResponseWriter, request *http.Request) {
+	nodeID := chi.URLParam(request, "id")
+	if nodeID == "" {
+		writer.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	for _, node := range s.nodes {
+		if nodeID == node.ID() {
+			render.JSON(writer, request, render.M{
+				node.Protocol(): node.Object(),
+			})
+			return
+		}
+	}
+}
+
+func (s *Server) updateNode(writer http.ResponseWriter, request *http.Request) {
+	writer.WriteHeader(http.StatusNotImplemented)
+}
+
+func (s *Server) deleteNode(writer http.ResponseWriter, request *http.Request) {
+	writer.WriteHeader(http.StatusNotImplemented)
+}
+
+type SSMUserObject struct {
+	UserName      string `json:"username"`
+	Password      string `json:"uPSK,omitempty"`
+	DownlinkBytes int64  `json:"downlinkBytes"`
+	UplinkBytes   int64  `json:"uplinkBytes"`
+
+	DownlinkPackets int64 `json:"downlinkPackets"`
+	UplinkPackets   int64 `json:"uplinkPackets"`
+	TCPSessions     int64 `json:"tcpSessions"`
+	UDPSessions     int64 `json:"udpSessions"`
+}
+
+func (s *Server) listUser(writer http.ResponseWriter, request *http.Request) {
+	render.JSON(writer, request, render.M{
+		"users": s.userManager.List(),
+	})
+}
+
+func (s *Server) addUser(writer http.ResponseWriter, request *http.Request) {
+	var addRequest struct {
+		UserName string `json:"username"`
+		Password string `json:"uPSK"`
+	}
+	err := render.DecodeJSON(request.Body, &addRequest)
+	if err != nil {
+		render.Status(request, http.StatusBadRequest)
+		render.PlainText(writer, request, err.Error())
+		return
+	}
+	err = s.userManager.Add(addRequest.UserName, addRequest.Password)
+	if err != nil {
+		render.Status(request, http.StatusBadRequest)
+		render.PlainText(writer, request, err.Error())
+		return
+	}
+	writer.WriteHeader(http.StatusCreated)
+}
+
+func (s *Server) getUser(writer http.ResponseWriter, request *http.Request) {
+	userName := chi.URLParam(request, "username")
+	if userName == "" {
+		writer.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	uPSK, loaded := s.userManager.Get(userName)
+	if !loaded {
+		writer.WriteHeader(http.StatusNotFound)
+		return
+	}
+	user := SSMUserObject{
+		UserName: userName,
+		Password: uPSK,
+	}
+	s.trafficManager.ReadUser(&user)
+	render.JSON(writer, request, user)
+}
+
+func (s *Server) updateUser(writer http.ResponseWriter, request *http.Request) {
+	userName := chi.URLParam(request, "username")
+	if userName == "" {
+		writer.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	var updateRequest struct {
+		Password string `json:"uPSK"`
+	}
+	err := render.DecodeJSON(request.Body, &updateRequest)
+	if err != nil {
+		render.Status(request, http.StatusBadRequest)
+		render.PlainText(writer, request, err.Error())
+		return
+	}
+	_, loaded := s.userManager.Get(userName)
+	if !loaded {
+		writer.WriteHeader(http.StatusNotFound)
+		return
+	}
+	err = s.userManager.Update(userName, updateRequest.Password)
+	if err != nil {
+		render.Status(request, http.StatusBadRequest)
+		render.PlainText(writer, request, err.Error())
+		return
+	}
+	writer.WriteHeader(http.StatusNoContent)
+}
+
+func (s *Server) deleteUser(writer http.ResponseWriter, request *http.Request) {
+	userName := chi.URLParam(request, "username")
+	if userName == "" {
+		writer.WriteHeader(http.StatusBadRequest)
+		return
+	}
+	_, loaded := s.userManager.Get(userName)
+	if !loaded {
+		writer.WriteHeader(http.StatusNotFound)
+		return
+	}
+	err := s.userManager.Delete(userName)
+	if err != nil {
+		render.Status(request, http.StatusBadRequest)
+		render.PlainText(writer, request, err.Error())
+		return
+	}
+	writer.WriteHeader(http.StatusNoContent)
+}
+
+func (s *Server) getStats(writer http.ResponseWriter, request *http.Request) {
+	requireClear := chi.URLParam(request, "clear") == "true"
+
+	users := s.userManager.List()
+	s.trafficManager.ReadUsers(users)
+	for i := range users {
+		users[i].Password = ""
+	}
+	uplinkBytes, downlinkBytes, uplinkPackets, downlinkPackets, tcpSessions, udpSessions := s.trafficManager.ReadGlobal()
+
+	if requireClear {
+		s.trafficManager.Clear()
+	}
+
+	render.JSON(writer, request, render.M{
+		"uplinkBytes":     uplinkBytes,
+		"downlinkBytes":   downlinkBytes,
+		"uplinkPackets":   uplinkPackets,
+		"downlinkPackets": downlinkPackets,
+		"tcpSessions":     tcpSessions,
+		"udpSessions":     udpSessions,
+		"users":           users,
+	})
+}

+ 117 - 0
experimental/ssmapi/server.go

@@ -0,0 +1,117 @@
+package ssmapi
+
+import (
+	"errors"
+	"net"
+	"net/http"
+	"strings"
+
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/experimental"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	"github.com/sagernet/sing/common"
+	E "github.com/sagernet/sing/common/exceptions"
+	N "github.com/sagernet/sing/common/network"
+
+	"github.com/go-chi/chi/v5"
+)
+
+func init() {
+	experimental.RegisterSSMServerConstructor(NewServer)
+}
+
+var _ adapter.SSMServer = (*Server)(nil)
+
+type Server struct {
+	router      adapter.Router
+	logger      log.Logger
+	httpServer  *http.Server
+	tcpListener net.Listener
+
+	nodes          []Node
+	userManager    *UserManager
+	trafficManager *TrafficManager
+}
+
+type Node interface {
+	Protocol() string
+	ID() string
+	Shadowsocks() ShadowsocksNodeObject
+	Object() any
+	Tag() string
+	UpdateUsers(users []string, uPSKs []string) error
+}
+
+func NewServer(router adapter.Router, logger log.Logger, options option.SSMAPIOptions) (adapter.SSMServer, error) {
+	chiRouter := chi.NewRouter()
+	server := &Server{
+		router: router,
+		logger: logger,
+		httpServer: &http.Server{
+			Addr:    options.Listen,
+			Handler: chiRouter,
+		},
+		nodes: make([]Node, 0, len(options.Nodes)),
+	}
+	for i, nodeOptions := range options.Nodes {
+		switch nodeOptions.Type {
+		case C.TypeShadowsocks:
+			ssOptions := nodeOptions.ShadowsocksOptions
+			inbound, loaded := router.Inbound(ssOptions.Inbound)
+			if !loaded {
+				return nil, E.New("parse SSM node[", i, "]: inbound", ssOptions.Inbound, "not found")
+			}
+			ssInbound, isSS := inbound.(adapter.ManagedShadowsocksServer)
+			if !isSS {
+				return nil, E.New("parse SSM node[", i, "]: inbound", ssOptions.Inbound, "is not a shadowsocks inbound")
+			}
+			node := &ShadowsocksNode{
+				ssOptions,
+				ssInbound,
+			}
+			server.nodes = append(server.nodes, node)
+		}
+	}
+	server.trafficManager = NewTrafficManager(server.nodes)
+	server.userManager = NewUserManager(server.nodes, server.trafficManager)
+	listenPrefix := options.ListenPrefix
+	if !strings.HasPrefix(listenPrefix, "/") {
+		listenPrefix = "/" + listenPrefix
+	}
+	chiRouter.Route(listenPrefix+"server/v1", server.setupRoutes)
+	return server, nil
+}
+
+func (s *Server) Start() error {
+	listener, err := net.Listen("tcp", s.httpServer.Addr)
+	if err != nil {
+		return err
+	}
+	s.logger.Info("ssm-api started at ", listener.Addr())
+	s.tcpListener = listener
+	go func() {
+		err = s.httpServer.Serve(listener)
+		if err != nil && !errors.Is(err, http.ErrServerClosed) {
+			s.logger.Error("ssm-api serve error: ", err)
+		}
+	}()
+	return nil
+}
+
+func (s *Server) Close() error {
+	return common.Close(
+		common.PtrOrNil(s.httpServer),
+		s.tcpListener,
+		s.trafficManager,
+	)
+}
+
+func (s *Server) RoutedConnection(metadata adapter.InboundContext, conn net.Conn) net.Conn {
+	return s.trafficManager.RoutedConnection(metadata, conn)
+}
+
+func (s *Server) RoutedPacketConnection(metadata adapter.InboundContext, conn N.PacketConn) N.PacketConn {
+	return s.trafficManager.RoutedPacketConnection(metadata, conn)
+}

+ 54 - 0
experimental/ssmapi/shadowsocks.go

@@ -0,0 +1,54 @@
+package ssmapi
+
+import (
+	"github.com/sagernet/sing-box/adapter"
+	C "github.com/sagernet/sing-box/constant"
+	"github.com/sagernet/sing-box/option"
+)
+
+var _ Node = (*ShadowsocksNode)(nil)
+
+type ShadowsocksNode struct {
+	node    option.SSMShadowsocksNode
+	inbound adapter.ManagedShadowsocksServer
+}
+
+type ShadowsocksNodeObject struct {
+	ID        string   `json:"id,omitempty"`
+	Name      string   `json:"name,omitempty"`
+	Endpoint  string   `json:"endpoint,omitempty"`
+	Method    string   `json:"method,omitempty"`
+	Passwords []string `json:"iPSKs,omitempty"`
+	Tags      []string `json:"tags,omitempty"`
+}
+
+func (n *ShadowsocksNode) Protocol() string {
+	return C.TypeShadowsocks
+}
+
+func (n *ShadowsocksNode) ID() string {
+	return n.node.ID
+}
+
+func (n *ShadowsocksNode) Shadowsocks() ShadowsocksNodeObject {
+	return ShadowsocksNodeObject{
+		ID:        n.node.ID,
+		Name:      n.node.Name,
+		Endpoint:  n.node.Address,
+		Method:    n.inbound.Method(),
+		Passwords: []string{n.inbound.Password()},
+		Tags:      n.node.Tags,
+	}
+}
+
+func (n *ShadowsocksNode) Object() any {
+	return n.Shadowsocks()
+}
+
+func (n *ShadowsocksNode) Tag() string {
+	return n.inbound.Tag()
+}
+
+func (n *ShadowsocksNode) UpdateUsers(users []string, uPSKs []string) error {
+	return n.inbound.UpdateUsers(users, uPSKs)
+}

+ 227 - 0
experimental/ssmapi/traffic.go

@@ -0,0 +1,227 @@
+package ssmapi
+
+import (
+	"net"
+	"sync"
+
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/experimental/trackerconn"
+	N "github.com/sagernet/sing/common/network"
+
+	"go.uber.org/atomic"
+)
+
+type TrafficManager struct {
+	nodeTags              map[string]bool
+	nodeUsers             map[string]bool
+	globalUplink          *atomic.Int64
+	globalDownlink        *atomic.Int64
+	globalUplinkPackets   *atomic.Int64
+	globalDownlinkPackets *atomic.Int64
+	globalTCPSessions     *atomic.Int64
+	globalUDPSessions     *atomic.Int64
+	userAccess            sync.Mutex
+	userUplink            map[string]*atomic.Int64
+	userDownlink          map[string]*atomic.Int64
+	userUplinkPackets     map[string]*atomic.Int64
+	userDownlinkPackets   map[string]*atomic.Int64
+	userTCPSessions       map[string]*atomic.Int64
+	userUDPSessions       map[string]*atomic.Int64
+}
+
+func NewTrafficManager(nodes []Node) *TrafficManager {
+	manager := &TrafficManager{
+		nodeTags:              make(map[string]bool),
+		globalUplink:          atomic.NewInt64(0),
+		globalDownlink:        atomic.NewInt64(0),
+		globalUplinkPackets:   atomic.NewInt64(0),
+		globalDownlinkPackets: atomic.NewInt64(0),
+		globalTCPSessions:     atomic.NewInt64(0),
+		globalUDPSessions:     atomic.NewInt64(0),
+		userUplink:            make(map[string]*atomic.Int64),
+		userDownlink:          make(map[string]*atomic.Int64),
+		userUplinkPackets:     make(map[string]*atomic.Int64),
+		userDownlinkPackets:   make(map[string]*atomic.Int64),
+		userTCPSessions:       make(map[string]*atomic.Int64),
+		userUDPSessions:       make(map[string]*atomic.Int64),
+	}
+	for _, node := range nodes {
+		manager.nodeTags[node.Tag()] = true
+	}
+	return manager
+}
+
+func (s *TrafficManager) UpdateUsers(users []string) {
+	nodeUsers := make(map[string]bool)
+	for _, user := range users {
+		nodeUsers[user] = true
+	}
+	s.nodeUsers = nodeUsers
+}
+
+func (s *TrafficManager) userCounter(user string) (*atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64, *atomic.Int64) {
+	s.userAccess.Lock()
+	defer s.userAccess.Unlock()
+	upCounter, loaded := s.userUplink[user]
+	if !loaded {
+		upCounter = atomic.NewInt64(0)
+		s.userUplink[user] = upCounter
+	}
+	downCounter, loaded := s.userDownlink[user]
+	if !loaded {
+		downCounter = atomic.NewInt64(0)
+		s.userDownlink[user] = downCounter
+	}
+	upPacketsCounter, loaded := s.userUplinkPackets[user]
+	if !loaded {
+		upPacketsCounter = atomic.NewInt64(0)
+		s.userUplinkPackets[user] = upPacketsCounter
+	}
+	downPacketsCounter, loaded := s.userDownlinkPackets[user]
+	if !loaded {
+		downPacketsCounter = atomic.NewInt64(0)
+		s.userDownlinkPackets[user] = downPacketsCounter
+	}
+	tcpSessionsCounter, loaded := s.userTCPSessions[user]
+	if !loaded {
+		tcpSessionsCounter = atomic.NewInt64(0)
+		s.userTCPSessions[user] = tcpSessionsCounter
+	}
+	udpSessionsCounter, loaded := s.userUDPSessions[user]
+	if !loaded {
+		udpSessionsCounter = atomic.NewInt64(0)
+		s.userUDPSessions[user] = udpSessionsCounter
+	}
+	return upCounter, downCounter, upPacketsCounter, downPacketsCounter, tcpSessionsCounter, udpSessionsCounter
+}
+
+func createCounter(counterList []*atomic.Int64, packetCounterList []*atomic.Int64) func(n int64) {
+	return func(n int64) {
+		for _, counter := range counterList {
+			counter.Add(n)
+		}
+		for _, counter := range packetCounterList {
+			counter.Inc()
+		}
+	}
+}
+
+func (s *TrafficManager) RoutedConnection(metadata adapter.InboundContext, conn net.Conn) net.Conn {
+	s.globalTCPSessions.Inc()
+
+	var readCounter []*atomic.Int64
+	var writeCounter []*atomic.Int64
+
+	if s.nodeTags[metadata.Inbound] {
+		readCounter = append(readCounter, s.globalUplink)
+		writeCounter = append(writeCounter, s.globalDownlink)
+	}
+	if s.nodeUsers[metadata.User] {
+		upCounter, downCounter, _, _, tcpSessionCounter, _ := s.userCounter(metadata.User)
+		readCounter = append(readCounter, upCounter)
+		writeCounter = append(writeCounter, downCounter)
+		tcpSessionCounter.Inc()
+	}
+	if len(readCounter) > 0 {
+		return trackerconn.New(conn, readCounter, writeCounter)
+	}
+	return conn
+}
+
+func (s *TrafficManager) RoutedPacketConnection(metadata adapter.InboundContext, conn N.PacketConn) N.PacketConn {
+	s.globalUDPSessions.Inc()
+
+	var readCounter []*atomic.Int64
+	var readPacketCounter []*atomic.Int64
+	var writeCounter []*atomic.Int64
+	var writePacketCounter []*atomic.Int64
+
+	if s.nodeTags[metadata.Inbound] {
+		readCounter = append(readCounter, s.globalUplink)
+		writeCounter = append(writeCounter, s.globalDownlink)
+		readPacketCounter = append(readPacketCounter, s.globalUplinkPackets)
+		writePacketCounter = append(writePacketCounter, s.globalDownlinkPackets)
+	}
+	if s.nodeUsers[metadata.User] {
+		upCounter, downCounter, upPacketsCounter, downPacketsCounter, _, udpSessionCounter := s.userCounter(metadata.User)
+		readCounter = append(readCounter, upCounter)
+		writeCounter = append(writeCounter, downCounter)
+		readPacketCounter = append(readPacketCounter, upPacketsCounter)
+		writePacketCounter = append(writePacketCounter, downPacketsCounter)
+		udpSessionCounter.Inc()
+	}
+	if len(readCounter) > 0 {
+		return trackerconn.NewHookPacket(conn, createCounter(readCounter, readPacketCounter), createCounter(writeCounter, writePacketCounter))
+	}
+	return conn
+}
+
+func (s *TrafficManager) ReadUser(user *SSMUserObject) {
+	s.userAccess.Lock()
+	defer s.userAccess.Unlock()
+
+	s.readUser(user)
+}
+
+func (s *TrafficManager) readUser(user *SSMUserObject) {
+	if counter, loaded := s.userUplink[user.UserName]; loaded {
+		user.UplinkBytes = counter.Load()
+	}
+	if counter, loaded := s.userDownlink[user.UserName]; loaded {
+		user.DownlinkBytes = counter.Load()
+	}
+	if counter, loaded := s.userUplinkPackets[user.UserName]; loaded {
+		user.UplinkPackets = counter.Load()
+	}
+	if counter, loaded := s.userDownlinkPackets[user.UserName]; loaded {
+		user.DownlinkPackets = counter.Load()
+	}
+	if counter, loaded := s.userTCPSessions[user.UserName]; loaded {
+		user.TCPSessions = counter.Load()
+	}
+	if counter, loaded := s.userUDPSessions[user.UserName]; loaded {
+		user.UDPSessions = counter.Load()
+	}
+}
+
+func (s *TrafficManager) ReadUsers(users []*SSMUserObject) {
+	s.userAccess.Lock()
+	defer s.userAccess.Unlock()
+	for _, user := range users {
+		s.readUser(user)
+	}
+	return
+}
+
+func (s *TrafficManager) ReadGlobal() (
+	uplinkBytes int64,
+	downlinkBytes int64,
+	uplinkPackets int64,
+	downlinkPackets int64,
+	tcpSessions int64,
+	udpSessions int64,
+) {
+	return s.globalUplink.Load(),
+		s.globalDownlink.Load(),
+		s.globalUplinkPackets.Load(),
+		s.globalDownlinkPackets.Load(),
+		s.globalTCPSessions.Load(),
+		s.globalUDPSessions.Load()
+}
+
+func (s *TrafficManager) Clear() {
+	s.globalUplink.Store(0)
+	s.globalDownlink.Store(0)
+	s.globalUplinkPackets.Store(0)
+	s.globalDownlinkPackets.Store(0)
+	s.globalTCPSessions.Store(0)
+	s.globalUDPSessions.Store(0)
+	s.userAccess.Lock()
+	defer s.userAccess.Unlock()
+	s.userUplink = make(map[string]*atomic.Int64)
+	s.userDownlink = make(map[string]*atomic.Int64)
+	s.userUplinkPackets = make(map[string]*atomic.Int64)
+	s.userDownlinkPackets = make(map[string]*atomic.Int64)
+	s.userTCPSessions = make(map[string]*atomic.Int64)
+	s.userUDPSessions = make(map[string]*atomic.Int64)
+}

+ 86 - 0
experimental/ssmapi/user.go

@@ -0,0 +1,86 @@
+package ssmapi
+
+import (
+	"sync"
+
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+type UserManager struct {
+	access         sync.Mutex
+	usersMap       map[string]string
+	nodes          []Node
+	trafficManager *TrafficManager
+}
+
+func NewUserManager(nodes []Node, trafficManager *TrafficManager) *UserManager {
+	return &UserManager{
+		usersMap:       make(map[string]string),
+		nodes:          nodes,
+		trafficManager: trafficManager,
+	}
+}
+
+func (m *UserManager) postUpdate() error {
+	users := make([]string, 0, len(m.usersMap))
+	uPSKs := make([]string, 0, len(m.usersMap))
+	for username, password := range m.usersMap {
+		users = append(users, username)
+		uPSKs = append(uPSKs, password)
+	}
+	for _, node := range m.nodes {
+		err := node.UpdateUsers(users, uPSKs)
+		if err != nil {
+			return err
+		}
+	}
+	m.trafficManager.UpdateUsers(users)
+	return nil
+}
+
+func (m *UserManager) List() []*SSMUserObject {
+	m.access.Lock()
+	defer m.access.Unlock()
+
+	users := make([]*SSMUserObject, 0, len(m.usersMap))
+	for username, password := range m.usersMap {
+		users = append(users, &SSMUserObject{
+			UserName: username,
+			Password: password,
+		})
+	}
+	return users
+}
+
+func (m *UserManager) Add(username string, password string) error {
+	m.access.Lock()
+	defer m.access.Unlock()
+	if _, found := m.usersMap[username]; found {
+		return E.New("user", username, "already exists")
+	}
+	m.usersMap[username] = password
+	return m.postUpdate()
+}
+
+func (m *UserManager) Get(username string) (string, bool) {
+	m.access.Lock()
+	defer m.access.Unlock()
+	if password, found := m.usersMap[username]; found {
+		return password, true
+	}
+	return "", false
+}
+
+func (m *UserManager) Update(username string, password string) error {
+	m.access.Lock()
+	defer m.access.Unlock()
+	m.usersMap[username] = password
+	return m.postUpdate()
+}
+
+func (m *UserManager) Delete(username string) error {
+	m.access.Lock()
+	defer m.access.Unlock()
+	delete(m.usersMap, username)
+	return m.postUpdate()
+}

+ 16 - 3
inbound/shadowsocks.go

@@ -22,7 +22,7 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 	if len(options.Users) > 0 && len(options.Destinations) > 0 {
 	if len(options.Users) > 0 && len(options.Destinations) > 0 {
 		return nil, E.New("users and destinations options must not be combined")
 		return nil, E.New("users and destinations options must not be combined")
 	}
 	}
-	if len(options.Users) > 0 {
+	if len(options.Users) > 0 || options.Managed {
 		return newShadowsocksMulti(ctx, router, logger, tag, options)
 		return newShadowsocksMulti(ctx, router, logger, tag, options)
 	} else if len(options.Destinations) > 0 {
 	} else if len(options.Destinations) > 0 {
 		return newShadowsocksRelay(ctx, router, logger, tag, options)
 		return newShadowsocksRelay(ctx, router, logger, tag, options)
@@ -32,8 +32,9 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 }
 }
 
 
 var (
 var (
-	_ adapter.Inbound           = (*Shadowsocks)(nil)
-	_ adapter.InjectableInbound = (*Shadowsocks)(nil)
+	_ adapter.Inbound                  = (*Shadowsocks)(nil)
+	_ adapter.InjectableInbound        = (*Shadowsocks)(nil)
+	_ adapter.ManagedShadowsocksServer = (*Shadowsocks)(nil)
 )
 )
 
 
 type Shadowsocks struct {
 type Shadowsocks struct {
@@ -76,6 +77,18 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
 	return inbound, err
 	return inbound, err
 }
 }
 
 
+func (h *Shadowsocks) Method() string {
+	return h.service.Name()
+}
+
+func (h *Shadowsocks) Password() string {
+	return h.service.Password()
+}
+
+func (h *Shadowsocks) UpdateUsers(names []string, uPSKs []string) error {
+	return os.ErrInvalid
+}
+
 func (h *Shadowsocks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 func (h *Shadowsocks) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
 	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
 }
 }

+ 33 - 7
inbound/shadowsocks_multi.go

@@ -19,8 +19,9 @@ import (
 )
 )
 
 
 var (
 var (
-	_ adapter.Inbound           = (*ShadowsocksMulti)(nil)
-	_ adapter.InjectableInbound = (*ShadowsocksMulti)(nil)
+	_ adapter.Inbound                  = (*ShadowsocksMulti)(nil)
+	_ adapter.InjectableInbound        = (*ShadowsocksMulti)(nil)
+	_ adapter.ManagedShadowsocksServer = (*ShadowsocksMulti)(nil)
 )
 )
 
 
 type ShadowsocksMulti struct {
 type ShadowsocksMulti struct {
@@ -61,11 +62,13 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int {
-		return index
-	}), common.Map(options.Users, func(user option.ShadowsocksUser) string {
-		return user.Password
-	}))
+	if len(options.Users) > 0 {
+		err = service.UpdateUsersWithPasswords(common.MapIndexed(options.Users, func(index int, user option.ShadowsocksUser) int {
+			return index
+		}), common.Map(options.Users, func(user option.ShadowsocksUser) string {
+			return user.Password
+		}))
+	}
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
@@ -75,6 +78,29 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
 	return inbound, err
 	return inbound, err
 }
 }
 
 
+func (h *ShadowsocksMulti) Method() string {
+	return h.service.Name()
+}
+
+func (h *ShadowsocksMulti) Password() string {
+	return h.service.Password()
+}
+
+func (h *ShadowsocksMulti) UpdateUsers(users []string, uPSKs []string) error {
+	err := h.service.UpdateUsersWithPasswords(common.MapIndexed(users, func(index int, user string) int {
+		return index
+	}), uPSKs)
+	if err != nil {
+		return err
+	}
+	h.users = common.Map(users, func(user string) option.ShadowsocksUser {
+		return option.ShadowsocksUser{
+			Name: user,
+		}
+	})
+	return nil
+}
+
 func (h *ShadowsocksMulti) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 func (h *ShadowsocksMulti) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
 	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
 }
 }

+ 15 - 2
inbound/shadowsocks_relay.go

@@ -18,8 +18,9 @@ import (
 )
 )
 
 
 var (
 var (
-	_ adapter.Inbound           = (*ShadowsocksRelay)(nil)
-	_ adapter.InjectableInbound = (*ShadowsocksRelay)(nil)
+	_ adapter.Inbound                  = (*ShadowsocksRelay)(nil)
+	_ adapter.InjectableInbound        = (*ShadowsocksRelay)(nil)
+	_ adapter.ManagedShadowsocksServer = (*ShadowsocksRelay)(nil)
 )
 )
 
 
 type ShadowsocksRelay struct {
 type ShadowsocksRelay struct {
@@ -71,6 +72,18 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.
 	return inbound, err
 	return inbound, err
 }
 }
 
 
+func (h *ShadowsocksRelay) Method() string {
+	return h.service.Name()
+}
+
+func (h *ShadowsocksRelay) Password() string {
+	return h.service.Password()
+}
+
+func (h *ShadowsocksRelay) UpdateUsers(users []string, uPSKs []string) error {
+	return os.ErrInvalid
+}
+
 func (h *ShadowsocksRelay) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 func (h *ShadowsocksRelay) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
 	return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
 }
 }

+ 5 - 0
include/ssmapi.go

@@ -0,0 +1,5 @@
+//go:build with_ssm_api
+
+package include
+
+import _ "github.com/sagernet/sing-box/experimental/ssmapi"

+ 17 - 0
include/ssmapi_stub.go

@@ -0,0 +1,17 @@
+//go:build !with_ssm_api
+
+package include
+
+import (
+	"github.com/sagernet/sing-box/adapter"
+	"github.com/sagernet/sing-box/experimental"
+	"github.com/sagernet/sing-box/log"
+	"github.com/sagernet/sing-box/option"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+func init() {
+	experimental.RegisterSSMServerConstructor(func(router adapter.Router, logger log.Logger, options option.SSMAPIOptions) (adapter.SSMServer, error) {
+		return nil, E.New(`SSM api is not included in this build, rebuild with -tags with_ssm_api`)
+	})
+}

+ 1 - 0
option/experimental.go

@@ -3,4 +3,5 @@ package option
 type ExperimentalOptions struct {
 type ExperimentalOptions struct {
 	ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
 	ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
 	V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
 	V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
+	SSMAPI   *SSMAPIOptions   `json:"ssm_api,omitempty"`
 }
 }

+ 1 - 0
option/shadowsocks.go

@@ -7,6 +7,7 @@ type ShadowsocksInboundOptions struct {
 	Password     string                   `json:"password"`
 	Password     string                   `json:"password"`
 	Users        []ShadowsocksUser        `json:"users,omitempty"`
 	Users        []ShadowsocksUser        `json:"users,omitempty"`
 	Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
 	Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
+	Managed      bool                     `json:"managed,omitempty"`
 }
 }
 
 
 type ShadowsocksUser struct {
 type ShadowsocksUser struct {

+ 54 - 0
option/ssmapi.go

@@ -0,0 +1,54 @@
+package option
+
+import (
+	"github.com/sagernet/sing-box/common/json"
+	C "github.com/sagernet/sing-box/constant"
+	E "github.com/sagernet/sing/common/exceptions"
+)
+
+type SSMAPIOptions struct {
+	Listen       string    `json:"listen,omitempty"`
+	ListenPrefix string    `json:"listen_prefix,omitempty"`
+	Nodes        []SSMNode `json:"nodes,omitempty"`
+}
+
+type _SSMNode struct {
+	Type               string             `json:"type,omitempty"`
+	ShadowsocksOptions SSMShadowsocksNode `json:"-"`
+}
+
+type SSMNode _SSMNode
+
+func (h SSMNode) MarshalJSON() ([]byte, error) {
+	var v any
+	switch h.Type {
+	case C.TypeShadowsocks:
+		v = h.ShadowsocksOptions
+	default:
+		return nil, E.New("unknown ssm node type: ", h.Type)
+	}
+	return MarshallObjects((_SSMNode)(h), v)
+}
+
+func (h *SSMNode) UnmarshalJSON(data []byte) error {
+	err := json.Unmarshal(data, (*_SSMNode)(h))
+	if err != nil {
+		return err
+	}
+	var v any
+	switch h.Type {
+	case C.TypeShadowsocks:
+		v = &h.ShadowsocksOptions
+	default:
+		return E.New("unknown ssm node type: ", h.Type)
+	}
+	return UnmarshallExcluded(data, (*_SSMNode)(h), v)
+}
+
+type SSMShadowsocksNode struct {
+	ID      string   `json:"id"`
+	Name    string   `json:"name"`
+	Address string   `json:"address"`
+	Tags    []string `json:"tags"`
+	Inbound string   `json:"inbound"`
+}

+ 33 - 13
route/router.go

@@ -97,6 +97,7 @@ type Router struct {
 	processSearcher                    process.Searcher
 	processSearcher                    process.Searcher
 	clashServer                        adapter.ClashServer
 	clashServer                        adapter.ClashServer
 	v2rayServer                        adapter.V2RayServer
 	v2rayServer                        adapter.V2RayServer
+	ssmServer                          adapter.SSMServer
 }
 }
 
 
 func NewRouter(ctx context.Context, logFactory log.Factory, options option.RouteOptions, dnsOptions option.DNSOptions, inbounds []option.Inbound) (*Router, error) {
 func NewRouter(ctx context.Context, logFactory log.Factory, options option.RouteOptions, dnsOptions option.DNSOptions, inbounds []option.Inbound) (*Router, error) {
@@ -380,10 +381,28 @@ func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outb
 	return nil
 	return nil
 }
 }
 
 
+func (r *Router) Inbound(tag string) (adapter.Inbound, bool) {
+	inbound, loaded := r.inboundByTag[tag]
+	return inbound, loaded
+}
+
 func (r *Router) Outbounds() []adapter.Outbound {
 func (r *Router) Outbounds() []adapter.Outbound {
 	return r.outbounds
 	return r.outbounds
 }
 }
 
 
+func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
+	outbound, loaded := r.outboundByTag[tag]
+	return outbound, loaded
+}
+
+func (r *Router) DefaultOutbound(network string) adapter.Outbound {
+	if network == N.NetworkTCP {
+		return r.defaultOutboundForConnection
+	} else {
+		return r.defaultOutboundForPacketConnection
+	}
+}
+
 func (r *Router) Start() error {
 func (r *Router) Start() error {
 	if r.needGeoIPDatabase {
 	if r.needGeoIPDatabase {
 		err := r.prepareGeoIPDatabase()
 		err := r.prepareGeoIPDatabase()
@@ -504,19 +523,6 @@ func (r *Router) LoadGeosite(code string) (adapter.Rule, error) {
 	return rule, nil
 	return rule, nil
 }
 }
 
 
-func (r *Router) Outbound(tag string) (adapter.Outbound, bool) {
-	outbound, loaded := r.outboundByTag[tag]
-	return outbound, loaded
-}
-
-func (r *Router) DefaultOutbound(network string) adapter.Outbound {
-	if network == N.NetworkTCP {
-		return r.defaultOutboundForConnection
-	} else {
-		return r.defaultOutboundForPacketConnection
-	}
-}
-
 func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
 	if metadata.InboundDetour != "" {
 	if metadata.InboundDetour != "" {
 		if metadata.LastInbound == metadata.InboundDetour {
 		if metadata.LastInbound == metadata.InboundDetour {
@@ -603,6 +609,9 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
 			conn = statsService.RoutedConnection(metadata.Inbound, detour.Tag(), metadata.User, conn)
 			conn = statsService.RoutedConnection(metadata.Inbound, detour.Tag(), metadata.User, conn)
 		}
 		}
 	}
 	}
+	if r.ssmServer != nil {
+		conn = r.ssmServer.RoutedConnection(metadata, conn)
+	}
 	return detour.NewConnection(ctx, conn, metadata)
 	return detour.NewConnection(ctx, conn, metadata)
 }
 }
 
 
@@ -681,6 +690,9 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
 			conn = statsService.RoutedPacketConnection(metadata.Inbound, detour.Tag(), metadata.User, conn)
 			conn = statsService.RoutedPacketConnection(metadata.Inbound, detour.Tag(), metadata.User, conn)
 		}
 		}
 	}
 	}
+	if r.ssmServer != nil {
+		conn = r.ssmServer.RoutedPacketConnection(metadata, conn)
+	}
 	return detour.NewPacketConnection(ctx, conn, metadata)
 	return detour.NewPacketConnection(ctx, conn, metadata)
 }
 }
 
 
@@ -777,6 +789,14 @@ func (r *Router) SetV2RayServer(server adapter.V2RayServer) {
 	r.v2rayServer = server
 	r.v2rayServer = server
 }
 }
 
 
+func (r *Router) SSMServer() adapter.SSMServer {
+	return r.ssmServer
+}
+
+func (r *Router) SetSSMServer(server adapter.SSMServer) {
+	r.ssmServer = server
+}
+
 func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
 func hasRule(rules []option.Rule, cond func(rule option.DefaultRule) bool) bool {
 	for _, rule := range rules {
 	for _, rule := range rules {
 		switch rule.Type {
 		switch rule.Type {