Add new admin panel, failover, dns fallback, providers, limiters. Update XHTTP

This commit is contained in:
Sergei Maklagin
2026-05-11 00:59:35 +03:00
parent 652e0baf57
commit 3bd162ed6f
241 changed files with 36409 additions and 4086 deletions

View File

@@ -0,0 +1,345 @@
package client
import (
"context"
"net"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/dialer"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
CM "github.com/sagernet/sing-box/service/manager/constant"
pb "github.com/sagernet/sing-box/service/node_manager_api/manager"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"google.golang.org/grpc"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
type APIClient struct {
boxService.Adapter
ctx context.Context
logger log.ContextLogger
dialer N.Dialer
creds credentials.TransportCredentials
options option.NodeManagerAPIClientOptions
conn *grpc.ClientConn
mtx sync.Mutex
}
func NewAPIClient(ctx context.Context, logger log.ContextLogger, tag string, options option.NodeManagerAPIClientOptions) (*APIClient, error) {
if options.APIKey == "" {
return nil, E.New("missing api key")
}
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
if err != nil {
return nil, err
}
creds := insecure.NewCredentials()
if options.TLS != nil {
tlsConfig, err := tls.NewClient(ctx, logger, options.Server, common.PtrValueOrDefault(options.TLS))
if err != nil {
return nil, err
}
creds = &tlsCreds{tlsConfig}
}
return &APIClient{
Adapter: boxService.NewAdapter(C.TypeManager, tag),
ctx: metadata.AppendToOutgoingContext(ctx, "authorization", options.APIKey),
logger: logger,
dialer: outboundDialer,
creds: creds,
options: options,
}, nil
}
func (s *APIClient) AddNode(uuid string, node CM.ConnectedNode) error {
go func() {
isRetry := false
for {
if !isRetry {
select {
case <-s.ctx.Done():
return
default:
isRetry = true
}
} else {
select {
case <-time.After(5 * time.Second):
break
case <-s.ctx.Done():
return
}
}
conn, err := s.getConn()
if err != nil {
s.logger.Error(err)
continue
}
client := pb.NewManagerClient(conn)
stream, err := client.AddNode(s.ctx, &pb.Node{Uuid: uuid})
if err != nil {
s.logger.Error(err)
continue
}
err = s.handler(node, stream)
if err != nil {
s.logger.Error(err)
continue
}
}
}()
return nil
}
func (s *APIClient) AcquireLock(limiterId int, id string) (string, error) {
conn, err := s.getConn()
if err != nil {
return "", err
}
client := pb.NewManagerClient(conn)
lockReply, err := client.AcquireLock(s.ctx, &pb.AcquireLockRequest{LimiterId: int32(limiterId), Id: id})
if err != nil {
return "", err
}
return lockReply.HandleId, err
}
func (s *APIClient) RefreshLock(limiterId int, id string, handleId string) error {
conn, err := s.getConn()
if err != nil {
return err
}
client := pb.NewManagerClient(conn)
_, err = client.RefreshLock(s.ctx, &pb.LockData{LimiterId: int32(limiterId), Id: id, HandleId: handleId})
return err
}
func (s *APIClient) ReleaseLock(limiterId int, id string, handleId string) error {
conn, err := s.getConn()
if err != nil {
return err
}
client := pb.NewManagerClient(conn)
_, err = client.ReleaseLock(s.ctx, &pb.LockData{LimiterId: int32(limiterId), Id: id, HandleId: handleId})
return err
}
func (s *APIClient) AddTrafficUsage(limiterId int, n uint64) (uint64, error) {
conn, err := s.getConn()
if err != nil {
return 0, err
}
client := pb.NewManagerClient(conn)
reply, err := client.AddTrafficUsage(s.ctx, &pb.TrafficUsageRequest{LimiterId: int32(limiterId), N: n})
if err != nil {
return 0, err
}
return reply.Remaining, nil
}
func (s *APIClient) Start(stage adapter.StartStage) error {
return nil
}
func (s *APIClient) Close() error {
return nil
}
func (s *APIClient) getConn() (*grpc.ClientConn, error) {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.conn != nil {
state := s.conn.GetState()
if state != connectivity.Shutdown && state != connectivity.TransientFailure {
return s.conn, nil
}
}
for {
conn, err := s.createConn()
if err != nil {
return nil, err
}
s.conn = conn
return conn, nil
}
}
func (s *APIClient) createConn() (*grpc.ClientConn, error) {
conn, err := grpc.NewClient(
s.options.ServerOptions.Build().String(),
grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
return s.dialer.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(addr))
}),
grpc.WithTransportCredentials(s.creds),
)
if err != nil {
return nil, err
}
return conn, nil
}
func (s *APIClient) handler(node CM.ConnectedNode, stream grpc.ServerStreamingClient[pb.NodeData]) error {
for {
data, err := stream.Recv()
if err != nil {
return err
}
switch data.Op {
case pb.OpType_updateUser:
s.logger.DebugContext(s.ctx, "update user")
node.UpdateUser(s.convertUser(data.Data.(*pb.NodeData_User).User))
case pb.OpType_updateUsers:
s.logger.DebugContext(s.ctx, "update users")
users := data.Data.(*pb.NodeData_Users).Users.Values
convertedUsers := make([]CM.User, len(users))
for i, user := range users {
convertedUsers[i] = s.convertUser(user)
}
node.UpdateUsers(convertedUsers)
case pb.OpType_deleteUser:
s.logger.DebugContext(s.ctx, "delete user")
node.DeleteUser(s.convertUser(data.Data.(*pb.NodeData_User).User))
case pb.OpType_updateConnectionLimiter:
s.logger.DebugContext(s.ctx, "update connection limiter")
node.UpdateConnectionLimiter(s.convertConnectionLimiter(data.Data.(*pb.NodeData_ConnectionLimiter).ConnectionLimiter))
case pb.OpType_updateConnectionLimiters:
s.logger.DebugContext(s.ctx, "update connection limiters")
limiters := data.Data.(*pb.NodeData_ConnectionLimiters).ConnectionLimiters.Values
convertedLimiters := make([]CM.ConnectionLimiter, len(limiters))
for i, limiter := range limiters {
convertedLimiters[i] = s.convertConnectionLimiter(limiter)
}
node.UpdateConnectionLimiters(convertedLimiters)
case pb.OpType_deleteConnectionLimiter:
s.logger.DebugContext(s.ctx, "delete connection limiter")
node.DeleteConnectionLimiter(s.convertConnectionLimiter(data.Data.(*pb.NodeData_ConnectionLimiter).ConnectionLimiter))
case pb.OpType_updateBandwidthLimiter:
s.logger.DebugContext(s.ctx, "update bandwidth limiter")
node.UpdateBandwidthLimiter(s.convertBandwidthLimiter(data.Data.(*pb.NodeData_BandwidthLimiter).BandwidthLimiter))
case pb.OpType_updateBandwidthLimiters:
s.logger.DebugContext(s.ctx, "update bandwidth limiters")
limiters := data.Data.(*pb.NodeData_BandwidthLimiters).BandwidthLimiters.Values
convertedLimiters := make([]CM.BandwidthLimiter, len(limiters))
for i, limiter := range limiters {
convertedLimiters[i] = s.convertBandwidthLimiter(limiter)
}
node.UpdateBandwidthLimiters(convertedLimiters)
case pb.OpType_deleteBandwidthLimiter:
s.logger.DebugContext(s.ctx, "delete bandwidth limiter")
node.DeleteBandwidthLimiter(s.convertBandwidthLimiter(data.Data.(*pb.NodeData_BandwidthLimiter).BandwidthLimiter))
case pb.OpType_updateTrafficLimiter:
s.logger.DebugContext(s.ctx, "update traffic limiter")
node.UpdateTrafficLimiter(s.convertTrafficLimiter(data.Data.(*pb.NodeData_TrafficLimiter).TrafficLimiter))
case pb.OpType_updateTrafficLimiters:
s.logger.DebugContext(s.ctx, "update traffic limiters")
limiters := data.Data.(*pb.NodeData_TrafficLimiters).TrafficLimiters.Values
convertedLimiters := make([]CM.TrafficLimiter, len(limiters))
for i, limiter := range limiters {
convertedLimiters[i] = s.convertTrafficLimiter(limiter)
}
node.UpdateTrafficLimiters(convertedLimiters)
case pb.OpType_deleteTrafficLimiter:
s.logger.DebugContext(s.ctx, "delete traffic limiter")
node.DeleteTrafficLimiter(s.convertTrafficLimiter(data.Data.(*pb.NodeData_TrafficLimiter).TrafficLimiter))
case pb.OpType_updateRateLimiter:
s.logger.DebugContext(s.ctx, "update rate limiter")
node.UpdateRateLimiter(s.convertRateLimiter(data.Data.(*pb.NodeData_RateLimiter).RateLimiter))
case pb.OpType_updateRateLimiters:
s.logger.DebugContext(s.ctx, "update rate limiters")
limiters := data.Data.(*pb.NodeData_RateLimiters).RateLimiters.Values
convertedLimiters := make([]CM.RateLimiter, len(limiters))
for i, limiter := range limiters {
convertedLimiters[i] = s.convertRateLimiter(limiter)
}
node.UpdateRateLimiters(convertedLimiters)
case pb.OpType_deleteRateLimiter:
s.logger.DebugContext(s.ctx, "delete rate limiter")
node.DeleteRateLimiter(s.convertRateLimiter(data.Data.(*pb.NodeData_RateLimiter).RateLimiter))
}
}
}
func (s *APIClient) convertUser(user *pb.User) CM.User {
return CM.User{
ID: int(user.Id),
Username: user.Username,
Inbound: user.Inbound,
Type: user.Type,
UUID: user.Uuid,
Password: user.Password,
Secret: user.Secret,
Flow: user.Flow,
AlterID: int(user.AlterId),
}
}
func (s *APIClient) convertBandwidthLimiter(limiter *pb.BandwidthLimiter) CM.BandwidthLimiter {
return CM.BandwidthLimiter{
ID: int(limiter.Id),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
ConnectionType: limiter.ConnectionType,
Mode: limiter.Mode,
FlowKeys: limiter.FlowKeys,
Speed: limiter.Speed,
RawSpeed: limiter.RawSpeed,
}
}
func (s *APIClient) convertConnectionLimiter(limiter *pb.ConnectionLimiter) CM.ConnectionLimiter {
return CM.ConnectionLimiter{
ID: int(limiter.Id),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
ConnectionType: limiter.ConnectionType,
LockType: limiter.LockType,
Count: limiter.Count,
}
}
func (s *APIClient) convertTrafficLimiter(limiter *pb.TrafficLimiter) CM.TrafficLimiter {
return CM.TrafficLimiter{
ID: int(limiter.Id),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
Mode: limiter.Mode,
RawUsed: limiter.RawUsed,
Quota: limiter.Quota,
RawQuota: limiter.RawQuota,
}
}
func (s *APIClient) convertRateLimiter(limiter *pb.RateLimiter) CM.RateLimiter {
return CM.RateLimiter{
ID: int(limiter.Id),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
ConnectionType: limiter.ConnectionType,
Count: limiter.Count,
Interval: limiter.Interval,
}
}

View File

@@ -0,0 +1,44 @@
package client
import (
"context"
"net"
"github.com/sagernet/sing-box/common/tls"
E "github.com/sagernet/sing/common/exceptions"
"google.golang.org/grpc/credentials"
)
type tlsCreds struct {
// TLS configuration
config tls.Config
}
func (c tlsCreds) Info() credentials.ProtocolInfo {
return credentials.ProtocolInfo{
SecurityProtocol: "tls",
SecurityVersion: "1.2",
ServerName: c.config.ServerName(),
}
}
func (c *tlsCreds) ClientHandshake(ctx context.Context, authority string, rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
conn, err := tls.ClientHandshake(ctx, rawConn, c.config)
if err != nil {
return nil, nil, err
}
return conn, credentials.TLSInfo{State: conn.ConnectionState()}, err
}
func (c *tlsCreds) ServerHandshake(rawConn net.Conn) (net.Conn, credentials.AuthInfo, error) {
return nil, nil, E.New("not implemented")
}
func (c *tlsCreds) Clone() credentials.TransportCredentials {
return &tlsCreds{config: c.config.Clone()}
}
func (c *tlsCreds) OverrideServerName(serverNameOverride string) error {
c.config.SetServerName(serverNameOverride)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,155 @@
syntax = "proto3";
option go_package = "github.com/sagernet/sing-box/service/node_manager_api/manager";
package node_manager_api.v1;
service Manager {
rpc AddNode (Node) returns (stream NodeData);
rpc AcquireLock(AcquireLockRequest) returns (LockData);
rpc RefreshLock(LockData) returns (Empty);
rpc ReleaseLock(LockData) returns (Empty);
rpc AddTrafficUsage(TrafficUsageRequest) returns (TrafficUsageReply);
}
message Node {
string uuid = 1;
}
enum OpType {
updateUsers = 0;
updateUser = 1;
deleteUser = 2;
updateBandwidthLimiters = 3;
updateBandwidthLimiter = 4;
deleteBandwidthLimiter = 5;
updateConnectionLimiters = 6;
updateConnectionLimiter = 7;
deleteConnectionLimiter = 8;
updateTrafficLimiters = 9;
updateTrafficLimiter = 10;
deleteTrafficLimiter = 11;
updateRateLimiters = 12;
updateRateLimiter = 13;
deleteRateLimiter = 14;
}
message User {
int32 id = 1;
string username = 2;
string inbound = 3;
string type = 4;
string uuid = 5;
string password = 6;
string secret = 7;
string flow = 8;
int32 alter_id = 9;
}
message UserList {
repeated User values = 1;
}
message BandwidthLimiter {
int32 id = 1;
string username = 2;
string outbound = 3;
string strategy = 4;
string connection_type = 5;
string mode = 6;
repeated string flow_keys = 7;
string speed = 8;
uint64 raw_speed = 9;
}
message BandwidthLimiterList {
repeated BandwidthLimiter values = 1;
}
message ConnectionLimiter {
int32 id = 1;
string username = 2;
string outbound = 3;
string strategy = 4;
string connection_type = 5;
string lock_type = 6;
uint32 count = 7;
}
message ConnectionLimiterList {
repeated ConnectionLimiter values = 1;
}
message TrafficLimiter {
int32 id = 1;
string username = 2;
string outbound = 3;
string strategy = 4;
string mode = 5;
uint64 raw_used = 6;
string quota = 7;
uint64 raw_quota = 8;
}
message TrafficLimiterList {
repeated TrafficLimiter values = 1;
}
message RateLimiter {
int32 id = 1;
string username = 2;
string outbound = 3;
string strategy = 4;
string connection_type = 5;
uint32 count = 6;
string interval = 7;
}
message RateLimiterList {
repeated RateLimiter values = 1;
}
message NodeData {
OpType op = 1;
oneof data {
UserList users = 2;
User user = 3;
BandwidthLimiterList bandwidth_limiters = 4;
BandwidthLimiter bandwidth_limiter = 5;
ConnectionLimiterList connection_limiters = 6;
ConnectionLimiter connection_limiter = 7;
TrafficLimiterList traffic_limiters = 8;
TrafficLimiter traffic_limiter = 9;
RateLimiterList rate_limiters = 10;
RateLimiter rate_limiter = 11;
}
}
message AcquireLockRequest {
int32 limiter_id = 1;
string id = 2;
}
message LockData {
int32 limiter_id = 1;
string id = 2;
string handleId = 3;
}
message TrafficUsageRequest {
int32 limiter_id = 1;
uint64 n = 2;
}
message TrafficUsageReply {
uint64 remaining = 1;
}
message Empty {}

View File

@@ -0,0 +1,277 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.6.1
// - protoc v6.31.1
// source: service/node_manager_api/manager/manager.proto
package manager
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
Manager_AddNode_FullMethodName = "/node_manager_api.v1.Manager/AddNode"
Manager_AcquireLock_FullMethodName = "/node_manager_api.v1.Manager/AcquireLock"
Manager_RefreshLock_FullMethodName = "/node_manager_api.v1.Manager/RefreshLock"
Manager_ReleaseLock_FullMethodName = "/node_manager_api.v1.Manager/ReleaseLock"
Manager_AddTrafficUsage_FullMethodName = "/node_manager_api.v1.Manager/AddTrafficUsage"
)
// ManagerClient is the client API for Manager service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type ManagerClient interface {
AddNode(ctx context.Context, in *Node, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NodeData], error)
AcquireLock(ctx context.Context, in *AcquireLockRequest, opts ...grpc.CallOption) (*LockData, error)
RefreshLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error)
ReleaseLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error)
AddTrafficUsage(ctx context.Context, in *TrafficUsageRequest, opts ...grpc.CallOption) (*TrafficUsageReply, error)
}
type managerClient struct {
cc grpc.ClientConnInterface
}
func NewManagerClient(cc grpc.ClientConnInterface) ManagerClient {
return &managerClient{cc}
}
func (c *managerClient) AddNode(ctx context.Context, in *Node, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NodeData], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &Manager_ServiceDesc.Streams[0], Manager_AddNode_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[Node, NodeData]{ClientStream: stream}
if err := x.ClientStream.SendMsg(in); err != nil {
return nil, err
}
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Manager_AddNodeClient = grpc.ServerStreamingClient[NodeData]
func (c *managerClient) AcquireLock(ctx context.Context, in *AcquireLockRequest, opts ...grpc.CallOption) (*LockData, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(LockData)
err := c.cc.Invoke(ctx, Manager_AcquireLock_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managerClient) RefreshLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Empty)
err := c.cc.Invoke(ctx, Manager_RefreshLock_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managerClient) ReleaseLock(ctx context.Context, in *LockData, opts ...grpc.CallOption) (*Empty, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Empty)
err := c.cc.Invoke(ctx, Manager_ReleaseLock_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *managerClient) AddTrafficUsage(ctx context.Context, in *TrafficUsageRequest, opts ...grpc.CallOption) (*TrafficUsageReply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(TrafficUsageReply)
err := c.cc.Invoke(ctx, Manager_AddTrafficUsage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// ManagerServer is the server API for Manager service.
// All implementations must embed UnimplementedManagerServer
// for forward compatibility.
type ManagerServer interface {
AddNode(*Node, grpc.ServerStreamingServer[NodeData]) error
AcquireLock(context.Context, *AcquireLockRequest) (*LockData, error)
RefreshLock(context.Context, *LockData) (*Empty, error)
ReleaseLock(context.Context, *LockData) (*Empty, error)
AddTrafficUsage(context.Context, *TrafficUsageRequest) (*TrafficUsageReply, error)
mustEmbedUnimplementedManagerServer()
}
// UnimplementedManagerServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedManagerServer struct{}
func (UnimplementedManagerServer) AddNode(*Node, grpc.ServerStreamingServer[NodeData]) error {
return status.Error(codes.Unimplemented, "method AddNode not implemented")
}
func (UnimplementedManagerServer) AcquireLock(context.Context, *AcquireLockRequest) (*LockData, error) {
return nil, status.Error(codes.Unimplemented, "method AcquireLock not implemented")
}
func (UnimplementedManagerServer) RefreshLock(context.Context, *LockData) (*Empty, error) {
return nil, status.Error(codes.Unimplemented, "method RefreshLock not implemented")
}
func (UnimplementedManagerServer) ReleaseLock(context.Context, *LockData) (*Empty, error) {
return nil, status.Error(codes.Unimplemented, "method ReleaseLock not implemented")
}
func (UnimplementedManagerServer) AddTrafficUsage(context.Context, *TrafficUsageRequest) (*TrafficUsageReply, error) {
return nil, status.Error(codes.Unimplemented, "method AddTrafficUsage not implemented")
}
func (UnimplementedManagerServer) mustEmbedUnimplementedManagerServer() {}
func (UnimplementedManagerServer) testEmbeddedByValue() {}
// UnsafeManagerServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ManagerServer will
// result in compilation errors.
type UnsafeManagerServer interface {
mustEmbedUnimplementedManagerServer()
}
func RegisterManagerServer(s grpc.ServiceRegistrar, srv ManagerServer) {
// If the following call panics, it indicates UnimplementedManagerServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&Manager_ServiceDesc, srv)
}
func _Manager_AddNode_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(Node)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(ManagerServer).AddNode(m, &grpc.GenericServerStream[Node, NodeData]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type Manager_AddNodeServer = grpc.ServerStreamingServer[NodeData]
func _Manager_AcquireLock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AcquireLockRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagerServer).AcquireLock(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Manager_AcquireLock_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagerServer).AcquireLock(ctx, req.(*AcquireLockRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Manager_RefreshLock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LockData)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagerServer).RefreshLock(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Manager_RefreshLock_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagerServer).RefreshLock(ctx, req.(*LockData))
}
return interceptor(ctx, in, info, handler)
}
func _Manager_ReleaseLock_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(LockData)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagerServer).ReleaseLock(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Manager_ReleaseLock_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagerServer).ReleaseLock(ctx, req.(*LockData))
}
return interceptor(ctx, in, info, handler)
}
func _Manager_AddTrafficUsage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TrafficUsageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ManagerServer).AddTrafficUsage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Manager_AddTrafficUsage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ManagerServer).AddTrafficUsage(ctx, req.(*TrafficUsageRequest))
}
return interceptor(ctx, in, info, handler)
}
// Manager_ServiceDesc is the grpc.ServiceDesc for Manager service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Manager_ServiceDesc = grpc.ServiceDesc{
ServiceName: "node_manager_api.v1.Manager",
HandlerType: (*ManagerServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "AcquireLock",
Handler: _Manager_AcquireLock_Handler,
},
{
MethodName: "RefreshLock",
Handler: _Manager_RefreshLock_Handler,
},
{
MethodName: "ReleaseLock",
Handler: _Manager_ReleaseLock_Handler,
},
{
MethodName: "AddTrafficUsage",
Handler: _Manager_AddTrafficUsage_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "AddNode",
Handler: _Manager_AddNode_Handler,
ServerStreams: true,
},
},
Metadata: "service/node_manager_api/manager/manager.proto",
}

View File

@@ -0,0 +1,303 @@
package server
import (
"context"
"sync"
"github.com/sagernet/sing-box/log"
CS "github.com/sagernet/sing-box/service/manager/constant"
pb "github.com/sagernet/sing-box/service/node_manager_api/manager"
E "github.com/sagernet/sing/common/exceptions"
"google.golang.org/grpc"
)
type RemoteNode struct {
ctx context.Context
logger log.ContextLogger
stream grpc.ServerStreamingServer[pb.NodeData]
err error
errChan chan error
mtx sync.Mutex
}
func NewRemoteNode(ctx context.Context, logger log.ContextLogger, stream grpc.ServerStreamingServer[pb.NodeData]) (*RemoteNode, chan error) {
errChan := make(chan error)
return &RemoteNode{
ctx: ctx,
logger: logger,
stream: stream,
errChan: errChan,
}, errChan
}
func (s *RemoteNode) UpdateUser(user CS.User) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_updateUser,
Data: &pb.NodeData_User{User: s.convertUser(user)},
})
}
func (s *RemoteNode) UpdateUsers(users []CS.User) {
s.mtx.Lock()
defer s.mtx.Unlock()
pbUsers := make([]*pb.User, len(users))
for i, user := range users {
pbUsers[i] = s.convertUser(user)
}
s.send(&pb.NodeData{
Op: pb.OpType_updateUsers,
Data: &pb.NodeData_Users{Users: &pb.UserList{Values: pbUsers}},
})
}
func (s *RemoteNode) DeleteUser(user CS.User) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_deleteUser,
Data: &pb.NodeData_User{User: s.convertUser(user)},
})
}
func (s *RemoteNode) UpdateConnectionLimiter(limiter CS.ConnectionLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_updateConnectionLimiter,
Data: &pb.NodeData_ConnectionLimiter{ConnectionLimiter: s.convertConnectionLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateConnectionLimiters(limiters []CS.ConnectionLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
pbLimiters := make([]*pb.ConnectionLimiter, len(limiters))
for i, limiters := range limiters {
pbLimiters[i] = s.convertConnectionLimiter(limiters)
}
s.send(&pb.NodeData{
Op: pb.OpType_updateConnectionLimiters,
Data: &pb.NodeData_ConnectionLimiters{ConnectionLimiters: &pb.ConnectionLimiterList{Values: pbLimiters}},
})
}
func (s *RemoteNode) DeleteConnectionLimiter(limiter CS.ConnectionLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_deleteConnectionLimiter,
Data: &pb.NodeData_ConnectionLimiter{ConnectionLimiter: s.convertConnectionLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateBandwidthLimiter(limiter CS.BandwidthLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_updateBandwidthLimiter,
Data: &pb.NodeData_BandwidthLimiter{BandwidthLimiter: s.convertBandwidthLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateBandwidthLimiters(limiters []CS.BandwidthLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
pbLimiters := make([]*pb.BandwidthLimiter, len(limiters))
for i, limiters := range limiters {
pbLimiters[i] = s.convertBandwidthLimiter(limiters)
}
s.send(&pb.NodeData{
Op: pb.OpType_updateBandwidthLimiters,
Data: &pb.NodeData_BandwidthLimiters{BandwidthLimiters: &pb.BandwidthLimiterList{Values: pbLimiters}},
})
}
func (s *RemoteNode) DeleteBandwidthLimiter(limiter CS.BandwidthLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_deleteBandwidthLimiter,
Data: &pb.NodeData_BandwidthLimiter{BandwidthLimiter: s.convertBandwidthLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateTrafficLimiter(limiter CS.TrafficLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_updateTrafficLimiter,
Data: &pb.NodeData_TrafficLimiter{TrafficLimiter: s.convertTrafficLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateTrafficLimiters(limiters []CS.TrafficLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
pbLimiters := make([]*pb.TrafficLimiter, len(limiters))
for i, limiters := range limiters {
pbLimiters[i] = s.convertTrafficLimiter(limiters)
}
s.send(&pb.NodeData{
Op: pb.OpType_updateTrafficLimiters,
Data: &pb.NodeData_TrafficLimiters{TrafficLimiters: &pb.TrafficLimiterList{Values: pbLimiters}},
})
}
func (s *RemoteNode) DeleteTrafficLimiter(limiter CS.TrafficLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_deleteTrafficLimiter,
Data: &pb.NodeData_TrafficLimiter{TrafficLimiter: s.convertTrafficLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateRateLimiter(limiter CS.RateLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_updateRateLimiter,
Data: &pb.NodeData_RateLimiter{RateLimiter: s.convertRateLimiter(limiter)},
})
}
func (s *RemoteNode) UpdateRateLimiters(limiters []CS.RateLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
pbLimiters := make([]*pb.RateLimiter, len(limiters))
for i, limiters := range limiters {
pbLimiters[i] = s.convertRateLimiter(limiters)
}
s.send(&pb.NodeData{
Op: pb.OpType_updateRateLimiters,
Data: &pb.NodeData_RateLimiters{RateLimiters: &pb.RateLimiterList{Values: pbLimiters}},
})
}
func (s *RemoteNode) DeleteRateLimiter(limiter CS.RateLimiter) {
s.mtx.Lock()
defer s.mtx.Unlock()
s.send(&pb.NodeData{
Op: pb.OpType_deleteRateLimiter,
Data: &pb.NodeData_RateLimiter{RateLimiter: s.convertRateLimiter(limiter)},
})
}
func (s *RemoteNode) IsLocal() bool {
return false
}
func (s *RemoteNode) IsOnline() bool {
s.mtx.Lock()
defer s.mtx.Unlock()
select {
case <-s.stream.Context().Done():
return false
default:
return true
}
}
func (s *RemoteNode) Close() error {
s.mtx.Lock()
defer s.mtx.Unlock()
s.close(E.New("server connection is closed"))
return nil
}
func (s *RemoteNode) send(data *pb.NodeData) {
if s.err != nil {
return
}
select {
case <-s.ctx.Done():
s.close(E.New("server connection is closed"))
return
case <-s.stream.Context().Done():
s.close(E.New("client connection is closed"))
return
default:
}
err := s.stream.Send(data)
if err != nil {
s.close(err)
}
}
func (s *RemoteNode) close(err error) {
if err != nil {
return
}
s.err = err
s.errChan <- err
close(s.errChan)
}
func (s *RemoteNode) convertUser(user CS.User) *pb.User {
return &pb.User{
Id: int32(user.ID),
Username: user.Username,
Inbound: user.Inbound,
Type: user.Type,
Uuid: user.UUID,
Password: user.Password,
Secret: user.Secret,
Flow: user.Flow,
AlterId: int32(user.AlterID),
}
}
func (s *RemoteNode) convertConnectionLimiter(limiter CS.ConnectionLimiter) *pb.ConnectionLimiter {
return &pb.ConnectionLimiter{
Id: int32(limiter.ID),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
ConnectionType: limiter.ConnectionType,
LockType: limiter.LockType,
Count: limiter.Count,
}
}
func (s *RemoteNode) convertBandwidthLimiter(limiter CS.BandwidthLimiter) *pb.BandwidthLimiter {
return &pb.BandwidthLimiter{
Id: int32(limiter.ID),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
ConnectionType: limiter.ConnectionType,
Mode: limiter.Mode,
FlowKeys: limiter.FlowKeys,
Speed: limiter.Speed,
RawSpeed: limiter.RawSpeed,
}
}
func (s *RemoteNode) convertTrafficLimiter(limiter CS.TrafficLimiter) *pb.TrafficLimiter {
return &pb.TrafficLimiter{
Id: int32(limiter.ID),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
Mode: limiter.Mode,
RawUsed: limiter.RawUsed,
Quota: limiter.Quota,
RawQuota: limiter.RawQuota,
}
}
func (s *RemoteNode) convertRateLimiter(limiter CS.RateLimiter) *pb.RateLimiter {
return &pb.RateLimiter{
Id: int32(limiter.ID),
Username: limiter.Username,
Outbound: limiter.Outbound,
Strategy: limiter.Strategy,
ConnectionType: limiter.ConnectionType,
Count: limiter.Count,
Interval: limiter.Interval,
}
}

View File

@@ -0,0 +1,182 @@
package server
import (
"context"
"crypto/subtle"
"errors"
"sync"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
"github.com/sagernet/sing-box/common/listener"
"github.com/sagernet/sing-box/common/tls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
CM "github.com/sagernet/sing-box/service/manager/constant"
pb "github.com/sagernet/sing-box/service/node_manager_api/manager"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
"github.com/sagernet/sing/service"
"golang.org/x/net/http2"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type APIServer struct {
pb.UnimplementedManagerServer
boxService.Adapter
ctx context.Context
logger log.ContextLogger
listener *listener.Listener
tlsConfig tls.ServerConfig
grpcServer *grpc.Server
manager CM.NodeManager
options option.NodeManagerAPIServerOptions
mtx sync.Mutex
}
func NewAPIServer(ctx context.Context, logger log.ContextLogger, tag string, options option.NodeManagerAPIServerOptions) (*APIServer, error) {
if options.APIKey == "" {
return nil, E.New("missing api key")
}
return &APIServer{
Adapter: boxService.NewAdapter(C.TypeManager, tag),
ctx: ctx,
logger: logger,
listener: listener.New(listener.Options{
Context: ctx,
Logger: logger,
Network: []string{N.NetworkTCP},
Listen: options.ListenOptions,
}),
options: options,
}, nil
}
func (s *APIServer) AddNode(node *pb.Node, stream grpc.ServerStreamingServer[pb.NodeData]) error {
remoteNode, errChan := NewRemoteNode(s.ctx, s.logger, stream)
err := s.manager.AddNode(node.Uuid, remoteNode)
if err != nil {
if err == CM.ErrNotFound {
return err
} else {
s.logger.Error(err)
return E.New("internal error")
}
}
return <-errChan
}
func (s *APIServer) AcquireLock(ctx context.Context, request *pb.AcquireLockRequest) (*pb.LockData, error) {
handleId, err := s.manager.AcquireLock(int(request.LimiterId), request.Id)
if err != nil {
return nil, err
}
return &pb.LockData{HandleId: handleId}, nil
}
func (s *APIServer) RefreshLock(ctx context.Context, data *pb.LockData) (*pb.Empty, error) {
return nil, s.manager.RefreshLock(int(data.LimiterId), data.Id, data.HandleId)
}
func (s *APIServer) ReleaseLock(ctx context.Context, data *pb.LockData) (*pb.Empty, error) {
return nil, s.manager.ReleaseLock(int(data.LimiterId), data.Id, data.HandleId)
}
func (s *APIServer) AddTrafficUsage(ctx context.Context, request *pb.TrafficUsageRequest) (*pb.TrafficUsageReply, error) {
remaining, err := s.manager.AddTrafficUsage(int(request.LimiterId), request.N)
if err != nil {
return nil, err
}
return &pb.TrafficUsageReply{Remaining: remaining}, nil
}
func (s *APIServer) Start(stage adapter.StartStage) error {
if stage != adapter.StartStateStart {
return nil
}
boxManager := service.FromContext[adapter.ServiceManager](s.ctx)
service, ok := boxManager.Get(s.options.Manager)
if !ok {
return E.New("manager ", s.options.Manager, " not found")
}
s.manager, ok = service.(CM.NodeManager)
if !ok {
return E.New("invalid", s.options.Manager, " manager")
}
if s.options.TLS != nil {
tlsConfig, err := tls.NewServer(s.ctx, s.logger, common.PtrValueOrDefault(s.options.TLS))
if err != nil {
return err
}
s.tlsConfig = tlsConfig
}
if s.tlsConfig != nil {
err := s.tlsConfig.Start()
if err != nil {
return E.Cause(err, "create TLS config")
}
}
tcpListener, err := s.listener.ListenTCP()
if err != nil {
return err
}
if s.tlsConfig != nil {
if !common.Contains(s.tlsConfig.NextProtos(), http2.NextProtoTLS) {
s.tlsConfig.SetNextProtos(append([]string{"h2"}, s.tlsConfig.NextProtos()...))
}
tcpListener = aTLS.NewListener(tcpListener, s.tlsConfig)
}
s.grpcServer = grpc.NewServer(
grpc.ChainUnaryInterceptor(s.unaryAuthInterceptor),
grpc.StreamInterceptor(s.streamAuthInterceptor),
)
pb.RegisterManagerServer(s.grpcServer, s)
go func() {
err = s.grpcServer.Serve(tcpListener)
if err != nil && !errors.Is(err, grpc.ErrServerStopped) {
s.logger.Error("serve error: ", err)
}
}()
return nil
}
func (s *APIServer) Close() error {
return nil
}
func (s *APIServer) unaryAuthInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
if err := s.authorize(ctx); err != nil {
return nil, err
}
return handler(ctx, req)
}
func (s *APIServer) streamAuthInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if err := s.authorize(ss.Context()); err != nil {
return err
}
return handler(srv, ss)
}
func (s *APIServer) authorize(ctx context.Context) error {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return status.Error(codes.Unauthenticated, "missing api key")
}
values := md.Get("authorization")
if len(values) == 0 {
return status.Error(codes.Unauthenticated, "missing api key")
}
if subtle.ConstantTimeCompare([]byte(values[0]), []byte(s.options.APIKey)) == 0 {
return status.Error(codes.Unauthenticated, "invalid api key")
}
return nil
}

View File

@@ -0,0 +1,31 @@
package node_manager_api
import (
"context"
"github.com/sagernet/sing-box/adapter"
boxService "github.com/sagernet/sing-box/adapter/service"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/service/node_manager_api/client"
"github.com/sagernet/sing-box/service/node_manager_api/server"
E "github.com/sagernet/sing/common/exceptions"
)
func RegisterService(registry *boxService.Registry) {
boxService.Register[option.NodeManagerAPIOptions](registry, C.TypeNodeManagerAPI, NewService)
}
func NewService(ctx context.Context, logger log.ContextLogger, tag string, options option.NodeManagerAPIOptions) (adapter.Service, error) {
switch options.APIType {
case C.NodeManagerAPIServer:
return server.NewAPIServer(ctx, logger, tag, options.ServerOptions)
case C.NodeManagerAPIClient:
return client.NewAPIClient(ctx, logger, tag, options.ClientOptions)
case "":
return nil, E.New("missing api type")
default:
return nil, E.New("unknown api type: ", options.APIType)
}
}