Add ssm api server

This commit is contained in:
世界 2023-02-18 16:26:05 +08:00
parent ec4a0c8497
commit d34091f8fc
No known key found for this signature in database
GPG key ID: CD109927C34A63C4
20 changed files with 938 additions and 26 deletions

View file

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

View file

@ -1,6 +1,6 @@
NAME = sing-box
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
PARAMS = -v -trimpath -tags "$(TAGS)" -ldflags "-s -w -buildid="
MAIN = ./cmd/sing-box

View file

@ -48,3 +48,16 @@ type V2RayStatsService interface {
RoutedConnection(inbound string, outbound string, user string, conn net.Conn) net.Conn
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
}

View file

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

21
box.go
View file

@ -32,6 +32,7 @@ type Box struct {
logFile *os.File
clashServer adapter.ClashServer
v2rayServer adapter.V2RayServer
ssmServer adapter.SSMServer
done chan struct{}
}
@ -41,6 +42,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
var needClashAPI bool
var needV2RayAPI bool
var needSSMAPI bool
if options.Experimental != nil {
if options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
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 != "" {
needV2RayAPI = true
}
if options.Experimental.SSMAPI != nil && options.Experimental.SSMAPI.Listen != "" {
needSSMAPI = true
}
}
var logFactory log.Factory
@ -156,6 +161,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
var clashServer adapter.ClashServer
var v2rayServer adapter.V2RayServer
var ssmServer adapter.SSMServer
if needClashAPI {
clashServer, err = experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
if err != nil {
@ -170,6 +176,13 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
}
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{
router: router,
inbounds: inbounds,
@ -180,6 +193,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
logFile: logFile,
clashServer: clashServer,
v2rayServer: v2rayServer,
ssmServer: ssmServer,
done: make(chan struct{}),
}, nil
}
@ -244,6 +258,12 @@ func (s *Box) start() error {
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)")
return nil
}
@ -266,6 +286,7 @@ func (s *Box) Close() error {
s.logFactory,
s.clashServer,
s.v2rayServer,
s.ssmServer,
common.PtrOrNil(s.logFile),
)
}

24
experimental/ssmapi.go Normal file
View file

@ -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
experimental/ssmapi/api.go Normal file
View file

@ -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,
})
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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()
}

View file

@ -22,7 +22,7 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
if len(options.Users) > 0 && len(options.Destinations) > 0 {
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)
} else if len(options.Destinations) > 0 {
return newShadowsocksRelay(ctx, router, logger, tag, options)
@ -34,6 +34,7 @@ func NewShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
var (
_ adapter.Inbound = (*Shadowsocks)(nil)
_ adapter.InjectableInbound = (*Shadowsocks)(nil)
_ adapter.ManagedShadowsocksServer = (*Shadowsocks)(nil)
)
type Shadowsocks struct {
@ -76,6 +77,18 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
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 {
return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
}

View file

@ -21,6 +21,7 @@ import (
var (
_ adapter.Inbound = (*ShadowsocksMulti)(nil)
_ adapter.InjectableInbound = (*ShadowsocksMulti)(nil)
_ adapter.ManagedShadowsocksServer = (*ShadowsocksMulti)(nil)
)
type ShadowsocksMulti struct {
@ -61,11 +62,13 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
if err != nil {
return nil, err
}
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 {
return nil, err
}
@ -75,6 +78,29 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
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 {
return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
}

View file

@ -20,6 +20,7 @@ import (
var (
_ adapter.Inbound = (*ShadowsocksRelay)(nil)
_ adapter.InjectableInbound = (*ShadowsocksRelay)(nil)
_ adapter.ManagedShadowsocksServer = (*ShadowsocksRelay)(nil)
)
type ShadowsocksRelay struct {
@ -71,6 +72,18 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.
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 {
return h.service.NewConnection(adapter.WithContext(log.ContextWithNewID(ctx), &metadata), conn, adapter.UpstreamMetadata(metadata))
}

5
include/ssmapi.go Normal file
View file

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

17
include/ssmapi_stub.go Normal file
View file

@ -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`)
})
}

View file

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

View file

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

54
option/ssmapi.go Normal file
View file

@ -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"`
}

View file

@ -97,6 +97,7 @@ type Router struct {
processSearcher process.Searcher
clashServer adapter.ClashServer
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) {
@ -380,10 +381,28 @@ func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outb
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 {
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 {
if r.needGeoIPDatabase {
err := r.prepareGeoIPDatabase()
@ -504,19 +523,6 @@ func (r *Router) LoadGeosite(code string) (adapter.Rule, error) {
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 {
if 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)
}
}
if r.ssmServer != nil {
conn = r.ssmServer.RoutedConnection(metadata, conn)
}
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)
}
}
if r.ssmServer != nil {
conn = r.ssmServer.RoutedPacketConnection(metadata, conn)
}
return detour.NewPacketConnection(ctx, conn, metadata)
}
@ -777,6 +789,14 @@ func (r *Router) SetV2RayServer(server adapter.V2RayServer) {
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 {
for _, rule := range rules {
switch rule.Type {