platform: Refactoring libbox to use gRPC-based protocol

This commit is contained in:
世界
2025-10-07 15:40:11 +08:00
parent 743b460e51
commit 5bc0dfa9dd
67 changed files with 6131 additions and 2002 deletions

View File

@@ -24,6 +24,7 @@ import (
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/filemanager"
"github.com/sagernet/ws"
@@ -53,7 +54,7 @@ type Server struct {
mode string
modeList []string
modeUpdateHook chan<- struct{}
modeUpdateHook *observable.Subscriber[struct{}]
externalController bool
externalUI string
@@ -203,7 +204,7 @@ func (s *Server) ModeList() []string {
return s.modeList
}
func (s *Server) SetModeUpdateHook(hook chan<- struct{}) {
func (s *Server) SetModeUpdateHook(hook *observable.Subscriber[struct{}]) {
s.modeUpdateHook = hook
}
@@ -221,10 +222,7 @@ func (s *Server) SetMode(newMode string) {
}
s.mode = newMode
if s.modeUpdateHook != nil {
select {
case s.modeUpdateHook <- struct{}{}:
default:
}
s.modeUpdateHook.Emit(struct{}{})
}
s.dnsRouter.ClearCache()
cacheFile := service.FromContext[adapter.CacheFile](s.ctx)

View File

@@ -45,15 +45,15 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
if t.Metadata.ProcessInfo != nil {
if t.Metadata.ProcessInfo.ProcessPath != "" {
processPath = t.Metadata.ProcessInfo.ProcessPath
} else if t.Metadata.ProcessInfo.PackageName != "" {
processPath = t.Metadata.ProcessInfo.PackageName
} else if t.Metadata.ProcessInfo.AndroidPackageName != "" {
processPath = t.Metadata.ProcessInfo.AndroidPackageName
}
if processPath == "" {
if t.Metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(t.Metadata.ProcessInfo.UserId)
}
} else if t.Metadata.ProcessInfo.User != "" {
processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.User, ")")
} else if t.Metadata.ProcessInfo.UserName != "" {
processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserName, ")")
} else if t.Metadata.ProcessInfo.UserId != -1 {
processPath = F.ToString(processPath, " (", t.Metadata.ProcessInfo.UserId, ")")
}

View File

@@ -3,18 +3,7 @@ package libbox
const (
CommandLog int32 = iota
CommandStatus
CommandServiceReload
CommandServiceClose
CommandCloseConnections
CommandGroup
CommandSelectOutbound
CommandURLTest
CommandGroupExpand
CommandClashMode
CommandSetClashMode
CommandGetSystemProxyStatus
CommandSetSystemProxyEnabled
CommandConnections
CommandCloseConnection
CommandGetDeprecatedNotes
)

View File

@@ -1,124 +0,0 @@
package libbox
import (
"encoding/binary"
"io"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental/clashapi"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
)
func (c *CommandClient) SetClashMode(newMode string) error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandSetClashMode))
if err != nil {
return err
}
err = varbin.Write(conn, binary.BigEndian, newMode)
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleSetClashMode(conn net.Conn) error {
newMode, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
service := s.service
if service == nil {
return writeError(conn, E.New("service not ready"))
}
service.clashServer.(*clashapi.Server).SetMode(newMode)
return writeError(conn, nil)
}
func (c *CommandClient) handleModeConn(conn net.Conn) {
defer conn.Close()
for {
newMode, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.UpdateClashMode(newMode)
}
}
func (s *CommandServer) handleModeConn(conn net.Conn) error {
ctx := connKeepAlive(conn)
for s.service == nil {
select {
case <-time.After(time.Second):
continue
case <-ctx.Done():
return ctx.Err()
}
}
err := writeClashModeList(conn, s.service.clashServer)
if err != nil {
return err
}
for {
select {
case <-s.modeUpdate:
err = varbin.Write(conn, binary.BigEndian, s.service.clashServer.Mode())
if err != nil {
return err
}
case <-ctx.Done():
return ctx.Err()
}
}
}
func readClashModeList(reader io.Reader) (modeList []string, currentMode string, err error) {
var modeListLength uint16
err = binary.Read(reader, binary.BigEndian, &modeListLength)
if err != nil {
return
}
if modeListLength == 0 {
return
}
modeList = make([]string, modeListLength)
for i := 0; i < int(modeListLength); i++ {
modeList[i], err = varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return
}
}
currentMode, err = varbin.ReadValue[string](reader, binary.BigEndian)
return
}
func writeClashModeList(writer io.Writer, clashServer adapter.ClashServer) error {
modeList := clashServer.ModeList()
err := binary.Write(writer, binary.BigEndian, uint16(len(modeList)))
if err != nil {
return err
}
if len(modeList) > 0 {
for _, mode := range modeList {
err = varbin.Write(writer, binary.BigEndian, mode)
if err != nil {
return err
}
}
err = varbin.Write(writer, binary.BigEndian, clashServer.Mode())
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,32 +1,49 @@
package libbox
import (
"encoding/binary"
"context"
"net"
"os"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/types/known/emptypb"
)
type CommandClient struct {
handler CommandClientHandler
conn net.Conn
options CommandClientOptions
handler CommandClientHandler
grpcConn *grpc.ClientConn
grpcClient daemon.StartedServiceClient
options CommandClientOptions
ctx context.Context
cancel context.CancelFunc
clientMutex sync.RWMutex
}
type CommandClientOptions struct {
Command int32
commands []int32
StatusInterval int64
}
func (o *CommandClientOptions) AddCommand(command int32) {
o.commands = append(o.commands, command)
}
type CommandClientHandler interface {
Connected()
Disconnected(message string)
SetDefaultLogLevel(level int32)
ClearLogs()
WriteLogs(messageList StringIterator)
WriteLogs(messageList LogIterator)
WriteStatus(message *StatusMessage)
WriteGroups(message OutboundGroupIterator)
InitializeClashMode(modeList StringIterator, currentMode string)
@@ -34,6 +51,17 @@ type CommandClientHandler interface {
WriteConnections(message *Connections)
}
type LogEntry struct {
Level int32
Message string
}
type LogIterator interface {
Len() int32
HasNext() bool
Next() *LogEntry
}
func NewStandaloneCommandClient() *CommandClient {
return new(CommandClient)
}
@@ -45,24 +73,38 @@ func NewCommandClient(handler CommandClientHandler, options *CommandClientOption
}
}
func (c *CommandClient) directConnect() (net.Conn, error) {
if !sTVOS {
return net.DialUnix("unix", nil, &net.UnixAddr{
Name: filepath.Join(sBasePath, "command.sock"),
Net: "unix",
})
} else {
return net.Dial("tcp", "127.0.0.1:8964")
func unaryClientAuthInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
if sCommandServerSecret != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "x-command-secret", sCommandServerSecret)
}
return invoker(ctx, method, req, reply, cc, opts...)
}
func (c *CommandClient) directConnectWithRetry() (net.Conn, error) {
func streamClientAuthInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
if sCommandServerSecret != "" {
ctx = metadata.AppendToOutgoingContext(ctx, "x-command-secret", sCommandServerSecret)
}
return streamer(ctx, desc, cc, method, opts...)
}
func (c *CommandClient) grpcDial() (*grpc.ClientConn, error) {
var target string
if sCommandServerListenPort == 0 {
target = "unix://" + filepath.Join(sBasePath, "command.sock")
} else {
target = net.JoinHostPort("127.0.0.1", strconv.Itoa(int(sCommandServerListenPort)))
}
var (
conn net.Conn
conn *grpc.ClientConn
err error
)
clientOptions := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(unaryClientAuthInterceptor),
grpc.WithStreamInterceptor(streamClientAuthInterceptor),
}
for i := 0; i < 10; i++ {
conn, err = c.directConnect()
conn, err = grpc.NewClient(target, clientOptions...)
if err == nil {
return conn, nil
}
@@ -72,79 +114,357 @@ func (c *CommandClient) directConnectWithRetry() (net.Conn, error) {
}
func (c *CommandClient) Connect() error {
common.Close(c.conn)
conn, err := c.directConnectWithRetry()
c.clientMutex.Lock()
common.Close(common.PtrOrNil(c.grpcConn))
conn, err := c.grpcDial()
if err != nil {
c.clientMutex.Unlock()
return err
}
c.conn = conn
err = binary.Write(conn, binary.BigEndian, uint8(c.options.Command))
if err != nil {
return err
}
switch c.options.Command {
case CommandLog:
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil {
return E.Cause(err, "write interval")
c.grpcConn = conn
c.grpcClient = daemon.NewStartedServiceClient(conn)
c.ctx, c.cancel = context.WithCancel(context.Background())
c.clientMutex.Unlock()
c.handler.Connected()
for _, command := range c.options.commands {
switch command {
case CommandLog:
go c.handleLogStream()
case CommandStatus:
go c.handleStatusStream()
case CommandGroup:
go c.handleGroupStream()
case CommandClashMode:
go c.handleClashModeStream()
case CommandConnections:
go c.handleConnectionsStream()
default:
return E.New("unknown command: ", command)
}
c.handler.Connected()
go c.handleLogConn(conn)
case CommandStatus:
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil {
return E.Cause(err, "write interval")
}
c.handler.Connected()
go c.handleStatusConn(conn)
case CommandGroup:
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil {
return E.Cause(err, "write interval")
}
c.handler.Connected()
go c.handleGroupConn(conn)
case CommandClashMode:
var (
modeList []string
currentMode string
)
modeList, currentMode, err = readClashModeList(conn)
if err != nil {
return err
}
if sFixAndroidStack {
go func() {
c.handler.Connected()
c.handler.InitializeClashMode(newIterator(modeList), currentMode)
if len(modeList) == 0 {
conn.Close()
c.handler.Disconnected(os.ErrInvalid.Error())
}
}()
} else {
c.handler.Connected()
c.handler.InitializeClashMode(newIterator(modeList), currentMode)
if len(modeList) == 0 {
conn.Close()
c.handler.Disconnected(os.ErrInvalid.Error())
}
}
if len(modeList) == 0 {
return nil
}
go c.handleModeConn(conn)
case CommandConnections:
err = binary.Write(conn, binary.BigEndian, c.options.StatusInterval)
if err != nil {
return E.Cause(err, "write interval")
}
c.handler.Connected()
go c.handleConnectionsConn(conn)
}
return nil
}
func (c *CommandClient) Disconnect() error {
return common.Close(c.conn)
c.clientMutex.Lock()
defer c.clientMutex.Unlock()
if c.cancel != nil {
c.cancel()
}
return common.Close(common.PtrOrNil(c.grpcConn))
}
func (c *CommandClient) getClientForCall() (daemon.StartedServiceClient, error) {
c.clientMutex.RLock()
if c.grpcClient != nil {
defer c.clientMutex.RUnlock()
return c.grpcClient, nil
}
c.clientMutex.RUnlock()
c.clientMutex.Lock()
defer c.clientMutex.Unlock()
if c.grpcClient != nil {
return c.grpcClient, nil
}
conn, err := c.grpcDial()
if err != nil {
return nil, err
}
c.grpcConn = conn
c.grpcClient = daemon.NewStartedServiceClient(conn)
if c.ctx == nil {
c.ctx, c.cancel = context.WithCancel(context.Background())
}
return c.grpcClient, nil
}
func (c *CommandClient) getStreamContext() (daemon.StartedServiceClient, context.Context) {
c.clientMutex.RLock()
defer c.clientMutex.RUnlock()
return c.grpcClient, c.ctx
}
func (c *CommandClient) handleLogStream() {
client, ctx := c.getStreamContext()
stream, err := client.SubscribeLog(ctx, &emptypb.Empty{})
if err != nil {
c.handler.Disconnected(err.Error())
return
}
defaultLogLevel, err := client.GetDefaultLogLevel(ctx, &emptypb.Empty{})
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.SetDefaultLogLevel(int32(defaultLogLevel.Level))
for {
logMessage, err := stream.Recv()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
if logMessage.Reset_ {
c.handler.ClearLogs()
}
var messages []*LogEntry
for _, msg := range logMessage.Messages {
messages = append(messages, &LogEntry{
Level: int32(msg.Level),
Message: msg.Message,
})
}
c.handler.WriteLogs(newIterator(messages))
}
}
func (c *CommandClient) handleStatusStream() {
client, ctx := c.getStreamContext()
interval := c.options.StatusInterval
stream, err := client.SubscribeStatus(ctx, &daemon.SubscribeStatusRequest{
Interval: interval,
})
if err != nil {
c.handler.Disconnected(err.Error())
return
}
for {
status, err := stream.Recv()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteStatus(StatusMessageFromGRPC(status))
}
}
func (c *CommandClient) handleGroupStream() {
client, ctx := c.getStreamContext()
stream, err := client.SubscribeGroups(ctx, &emptypb.Empty{})
if err != nil {
c.handler.Disconnected(err.Error())
return
}
for {
groups, err := stream.Recv()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteGroups(OutboundGroupIteratorFromGRPC(groups))
}
}
func (c *CommandClient) handleClashModeStream() {
client, ctx := c.getStreamContext()
modeStatus, err := client.GetClashModeStatus(ctx, &emptypb.Empty{})
if err != nil {
c.handler.Disconnected(err.Error())
return
}
if sFixAndroidStack {
go func() {
c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
if len(modeStatus.ModeList) == 0 {
c.handler.Disconnected(os.ErrInvalid.Error())
}
}()
} else {
c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
if len(modeStatus.ModeList) == 0 {
c.handler.Disconnected(os.ErrInvalid.Error())
return
}
}
if len(modeStatus.ModeList) == 0 {
return
}
stream, err := client.SubscribeClashMode(ctx, &emptypb.Empty{})
if err != nil {
c.handler.Disconnected(err.Error())
return
}
for {
mode, err := stream.Recv()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.UpdateClashMode(mode.Mode)
}
}
func (c *CommandClient) handleConnectionsStream() {
client, ctx := c.getStreamContext()
interval := c.options.StatusInterval
stream, err := client.SubscribeConnections(ctx, &daemon.SubscribeConnectionsRequest{
Interval: interval,
})
if err != nil {
c.handler.Disconnected(err.Error())
return
}
var connections Connections
for {
conns, err := stream.Recv()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
connections.input = ConnectionsFromGRPC(conns)
c.handler.WriteConnections(&connections)
}
}
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{
GroupTag: groupTag,
OutboundTag: outboundTag,
})
return err
}
func (c *CommandClient) URLTest(groupTag string) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.URLTest(context.Background(), &daemon.URLTestRequest{
OutboundTag: groupTag,
})
return err
}
func (c *CommandClient) SetClashMode(newMode string) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.SetClashMode(context.Background(), &daemon.ClashMode{
Mode: newMode,
})
return err
}
func (c *CommandClient) CloseConnection(connId string) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.CloseConnection(context.Background(), &daemon.CloseConnectionRequest{
Id: connId,
})
return err
}
func (c *CommandClient) CloseConnections() error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.CloseAllConnections(context.Background(), &emptypb.Empty{})
return err
}
func (c *CommandClient) ServiceReload() error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.ReloadService(context.Background(), &emptypb.Empty{})
return err
}
func (c *CommandClient) ServiceClose() error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.StopService(context.Background(), &emptypb.Empty{})
return err
}
func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) {
client, err := c.getClientForCall()
if err != nil {
return nil, err
}
status, err := client.GetSystemProxyStatus(context.Background(), &emptypb.Empty{})
if err != nil {
return nil, err
}
return SystemProxyStatusFromGRPC(status), nil
}
func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.SetSystemProxyEnabled(context.Background(), &daemon.SetSystemProxyEnabledRequest{
Enabled: isEnabled,
})
return err
}
func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) {
client, err := c.getClientForCall()
if err != nil {
return nil, err
}
warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{})
if err != nil {
return nil, err
}
var notes []*DeprecatedNote
for _, warning := range warnings.Warnings {
notes = append(notes, &DeprecatedNote{
Description: warning.Message,
MigrationLink: warning.MigrationLink,
})
}
return newIterator(notes), nil
}
func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
client, err := c.getClientForCall()
if err != nil {
return err
}
_, err = client.SetGroupExpand(context.Background(), &daemon.SetGroupExpandRequest{
GroupTag: groupTag,
IsExpand: isExpand,
})
return err
}

View File

@@ -1,54 +0,0 @@
package libbox
import (
"bufio"
"net"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing/common/binary"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
"github.com/gofrs/uuid/v5"
)
func (c *CommandClient) CloseConnection(connId string) error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandCloseConnection))
if err != nil {
return err
}
writer := bufio.NewWriter(conn)
err = varbin.Write(writer, binary.BigEndian, connId)
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleCloseConnection(conn net.Conn) error {
reader := bufio.NewReader(conn)
var connId string
err := varbin.Read(reader, binary.BigEndian, &connId)
if err != nil {
return E.Cause(err, "read connection id")
}
service := s.service
if service == nil {
return writeError(conn, E.New("service not ready"))
}
targetConn := service.clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(connId))
if targetConn == nil {
return writeError(conn, E.New("connection already closed"))
}
targetConn.Close()
return writeError(conn, nil)
}

View File

@@ -1,269 +0,0 @@
package libbox
import (
"bufio"
"net"
"slices"
"strings"
"time"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
"github.com/sagernet/sing/common/binary"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/varbin"
"github.com/gofrs/uuid/v5"
)
func (c *CommandClient) handleConnectionsConn(conn net.Conn) {
defer conn.Close()
reader := bufio.NewReader(conn)
var (
rawConnections []Connection
connections Connections
)
for {
rawConnections = nil
err := varbin.Read(reader, binary.BigEndian, &rawConnections)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
connections.input = rawConnections
c.handler.WriteConnections(&connections)
}
}
func (s *CommandServer) handleConnectionsConn(conn net.Conn) error {
var interval int64
err := binary.Read(conn, binary.BigEndian, &interval)
if err != nil {
return E.Cause(err, "read interval")
}
ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop()
ctx := connKeepAlive(conn)
var trafficManager *trafficontrol.Manager
for {
service := s.service
if service != nil {
trafficManager = service.clashServer.(*clashapi.Server).TrafficManager()
break
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
var (
connections = make(map[uuid.UUID]*Connection)
outConnections []Connection
)
writer := bufio.NewWriter(conn)
for {
outConnections = outConnections[:0]
for _, connection := range trafficManager.Connections() {
outConnections = append(outConnections, newConnection(connections, connection, false))
}
for _, connection := range trafficManager.ClosedConnections() {
outConnections = append(outConnections, newConnection(connections, connection, true))
}
err = varbin.Write(writer, binary.BigEndian, outConnections)
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
}
}
const (
ConnectionStateAll = iota
ConnectionStateActive
ConnectionStateClosed
)
type Connections struct {
input []Connection
filtered []Connection
}
func (c *Connections) FilterState(state int32) {
c.filtered = c.filtered[:0]
switch state {
case ConnectionStateAll:
c.filtered = append(c.filtered, c.input...)
case ConnectionStateActive:
for _, connection := range c.input {
if connection.ClosedAt == 0 {
c.filtered = append(c.filtered, connection)
}
}
case ConnectionStateClosed:
for _, connection := range c.input {
if connection.ClosedAt != 0 {
c.filtered = append(c.filtered, connection)
}
}
}
}
func (c *Connections) SortByDate() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
if x.CreatedAt < y.CreatedAt {
return 1
} else if x.CreatedAt > y.CreatedAt {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) SortByTraffic() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
xTraffic := x.Uplink + x.Downlink
yTraffic := y.Uplink + y.Downlink
if xTraffic < yTraffic {
return 1
} else if xTraffic > yTraffic {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) SortByTrafficTotal() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
xTraffic := x.UplinkTotal + x.DownlinkTotal
yTraffic := y.UplinkTotal + y.DownlinkTotal
if xTraffic < yTraffic {
return 1
} else if xTraffic > yTraffic {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) Iterator() ConnectionIterator {
return newPtrIterator(c.filtered)
}
type Connection struct {
ID string
Inbound string
InboundType string
IPVersion int32
Network string
Source string
Destination string
Domain string
Protocol string
User string
FromOutbound string
CreatedAt int64
ClosedAt int64
Uplink int64
Downlink int64
UplinkTotal int64
DownlinkTotal int64
Rule string
Outbound string
OutboundType string
ChainList []string
}
func (c *Connection) Chain() StringIterator {
return newIterator(c.ChainList)
}
func (c *Connection) DisplayDestination() string {
destination := M.ParseSocksaddr(c.Destination)
if destination.IsIP() && c.Domain != "" {
destination = M.Socksaddr{
Fqdn: c.Domain,
Port: destination.Port,
}
return destination.String()
}
return c.Destination
}
type ConnectionIterator interface {
Next() *Connection
HasNext() bool
}
func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) Connection {
if oldConnection, loaded := connections[metadata.ID]; loaded {
if isClosed {
if oldConnection.ClosedAt == 0 {
oldConnection.Uplink = 0
oldConnection.Downlink = 0
oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
}
return *oldConnection
}
lastUplink := oldConnection.UplinkTotal
lastDownlink := oldConnection.DownlinkTotal
uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load()
oldConnection.Uplink = uplinkTotal - lastUplink
oldConnection.Downlink = downlinkTotal - lastDownlink
oldConnection.UplinkTotal = uplinkTotal
oldConnection.DownlinkTotal = downlinkTotal
return *oldConnection
}
var rule string
if metadata.Rule != nil {
rule = metadata.Rule.String()
}
uplinkTotal := metadata.Upload.Load()
downlinkTotal := metadata.Download.Load()
uplink := uplinkTotal
downlink := downlinkTotal
var closedAt int64
if !metadata.ClosedAt.IsZero() {
closedAt = metadata.ClosedAt.UnixMilli()
uplink = 0
downlink = 0
}
connection := Connection{
ID: metadata.ID.String(),
Inbound: metadata.Metadata.Inbound,
InboundType: metadata.Metadata.InboundType,
IPVersion: int32(metadata.Metadata.IPVersion),
Network: metadata.Metadata.Network,
Source: metadata.Metadata.Source.String(),
Destination: metadata.Metadata.Destination.String(),
Domain: metadata.Metadata.Domain,
Protocol: metadata.Metadata.Protocol,
User: metadata.Metadata.User,
FromOutbound: metadata.Metadata.Outbound,
CreatedAt: metadata.CreatedAt.UnixMilli(),
ClosedAt: closedAt,
Uplink: uplink,
Downlink: downlink,
UplinkTotal: uplinkTotal,
DownlinkTotal: downlinkTotal,
Rule: rule,
Outbound: metadata.Outbound,
OutboundType: metadata.OutboundType,
ChainList: metadata.Chain,
}
connections[metadata.ID] = &connection
return connection
}

View File

@@ -1,28 +0,0 @@
package libbox
import (
"encoding/binary"
"net"
runtimeDebug "runtime/debug"
"time"
"github.com/sagernet/sing-box/common/conntrack"
)
func (c *CommandClient) CloseConnections() error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
return binary.Write(conn, binary.BigEndian, uint8(CommandCloseConnections))
}
func (s *CommandServer) handleCloseConnections(conn net.Conn) error {
conntrack.Close()
go func() {
time.Sleep(time.Second)
runtimeDebug.FreeOSMemory()
}()
return nil
}

View File

@@ -1,46 +0,0 @@
package libbox
import (
"encoding/binary"
"net"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
"github.com/sagernet/sing/service"
)
func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) {
conn, err := c.directConnect()
if err != nil {
return nil, err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandGetDeprecatedNotes))
if err != nil {
return nil, err
}
err = readError(conn)
if err != nil {
return nil, err
}
var features []deprecated.Note
err = varbin.Read(conn, binary.BigEndian, &features)
if err != nil {
return nil, err
}
return newIterator(common.Map(features, func(it deprecated.Note) *DeprecatedNote { return (*DeprecatedNote)(&it) })), nil
}
func (s *CommandServer) handleGetDeprecatedNotes(conn net.Conn) error {
boxService := s.service
if boxService == nil {
return writeError(conn, E.New("service not ready"))
}
err := writeError(conn, nil)
if err != nil {
return err
}
return varbin.Write(conn, binary.BigEndian, service.FromContext[deprecated.Manager](boxService.ctx).(*deprecatedManager).Get())
}

View File

@@ -1,198 +0,0 @@
package libbox
import (
"bufio"
"encoding/binary"
"io"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/protocol/group"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
"github.com/sagernet/sing/service"
)
func (c *CommandClient) handleGroupConn(conn net.Conn) {
defer conn.Close()
for {
groups, err := readGroups(conn)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteGroups(groups)
}
}
func (s *CommandServer) handleGroupConn(conn net.Conn) error {
var interval int64
err := binary.Read(conn, binary.BigEndian, &interval)
if err != nil {
return E.Cause(err, "read interval")
}
ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop()
ctx := connKeepAlive(conn)
writer := bufio.NewWriter(conn)
for {
service := s.service
if service != nil {
err = writeGroups(writer, service)
if err != nil {
return err
}
} else {
err = binary.Write(writer, binary.BigEndian, uint16(0))
if err != nil {
return err
}
}
err = writer.Flush()
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
select {
case <-ctx.Done():
return ctx.Err()
case <-s.urlTestUpdate:
}
}
}
type OutboundGroup struct {
Tag string
Type string
Selectable bool
Selected string
IsExpand bool
ItemList []*OutboundGroupItem
}
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
return newIterator(g.ItemList)
}
type OutboundGroupIterator interface {
Next() *OutboundGroup
HasNext() bool
}
type OutboundGroupItem struct {
Tag string
Type string
URLTestTime int64
URLTestDelay int32
}
type OutboundGroupItemIterator interface {
Next() *OutboundGroupItem
HasNext() bool
}
func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
groups, err := varbin.ReadValue[[]*OutboundGroup](reader, binary.BigEndian)
if err != nil {
return nil, err
}
return newIterator(groups), nil
}
func writeGroups(writer io.Writer, boxService *BoxService) error {
historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx)
cacheFile := service.FromContext[adapter.CacheFile](boxService.ctx)
outbounds := boxService.instance.Outbound().Outbounds()
var iGroups []adapter.OutboundGroup
for _, it := range outbounds {
if group, isGroup := it.(adapter.OutboundGroup); isGroup {
iGroups = append(iGroups, group)
}
}
var groups []OutboundGroup
for _, iGroup := range iGroups {
var outboundGroup OutboundGroup
outboundGroup.Tag = iGroup.Tag()
outboundGroup.Type = iGroup.Type()
_, outboundGroup.Selectable = iGroup.(*group.Selector)
outboundGroup.Selected = iGroup.Now()
if cacheFile != nil {
if isExpand, loaded := cacheFile.LoadGroupExpand(outboundGroup.Tag); loaded {
outboundGroup.IsExpand = isExpand
}
}
for _, itemTag := range iGroup.All() {
itemOutbound, isLoaded := boxService.instance.Outbound().Outbound(itemTag)
if !isLoaded {
continue
}
var item OutboundGroupItem
item.Tag = itemTag
item.Type = itemOutbound.Type()
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(itemOutbound)); history != nil {
item.URLTestTime = history.Time.Unix()
item.URLTestDelay = int32(history.Delay)
}
outboundGroup.ItemList = append(outboundGroup.ItemList, &item)
}
if len(outboundGroup.ItemList) < 2 {
continue
}
groups = append(groups, outboundGroup)
}
return varbin.Write(writer, binary.BigEndian, groups)
}
func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandGroupExpand))
if err != nil {
return err
}
err = varbin.Write(conn, binary.BigEndian, groupTag)
if err != nil {
return err
}
err = binary.Write(conn, binary.BigEndian, isExpand)
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleSetGroupExpand(conn net.Conn) error {
groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
var isExpand bool
err = binary.Read(conn, binary.BigEndian, &isExpand)
if err != nil {
return err
}
serviceNow := s.service
if serviceNow == nil {
return writeError(conn, E.New("service not ready"))
}
cacheFile := service.FromContext[adapter.CacheFile](serviceNow.ctx)
if cacheFile != nil {
err = cacheFile.StoreGroupExpand(groupTag, isExpand)
if err != nil {
return writeError(conn, err)
}
}
return writeError(conn, nil)
}

View File

@@ -1,160 +0,0 @@
package libbox
import (
"bufio"
"context"
"io"
"net"
"time"
"github.com/sagernet/sing/common/binary"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
)
func (s *CommandServer) ResetLog() {
s.access.Lock()
defer s.access.Unlock()
s.savedLines.Init()
select {
case s.logReset <- struct{}{}:
default:
}
}
func (s *CommandServer) WriteMessage(message string) {
s.subscriber.Emit(message)
s.access.Lock()
s.savedLines.PushBack(message)
if s.savedLines.Len() > s.maxLines {
s.savedLines.Remove(s.savedLines.Front())
}
s.access.Unlock()
}
func (s *CommandServer) handleLogConn(conn net.Conn) error {
var (
interval int64
timer *time.Timer
)
err := binary.Read(conn, binary.BigEndian, &interval)
if err != nil {
return E.Cause(err, "read interval")
}
timer = time.NewTimer(time.Duration(interval))
if !timer.Stop() {
<-timer.C
}
var savedLines []string
s.access.Lock()
savedLines = make([]string, 0, s.savedLines.Len())
for element := s.savedLines.Front(); element != nil; element = element.Next() {
savedLines = append(savedLines, element.Value)
}
s.access.Unlock()
subscription, done, err := s.observer.Subscribe()
if err != nil {
return err
}
defer s.observer.UnSubscribe(subscription)
writer := bufio.NewWriter(conn)
select {
case <-s.logReset:
err = writer.WriteByte(1)
if err != nil {
return err
}
err = writer.Flush()
if err != nil {
return err
}
default:
}
if len(savedLines) > 0 {
err = writer.WriteByte(0)
if err != nil {
return err
}
err = varbin.Write(writer, binary.BigEndian, savedLines)
if err != nil {
return err
}
}
ctx := connKeepAlive(conn)
var logLines []string
for {
err = writer.Flush()
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-s.logReset:
err = writer.WriteByte(1)
if err != nil {
return err
}
case <-done:
return nil
case logLine := <-subscription:
logLines = logLines[:0]
logLines = append(logLines, logLine)
timer.Reset(time.Duration(interval))
loopLogs:
for {
select {
case logLine = <-subscription:
logLines = append(logLines, logLine)
case <-timer.C:
break loopLogs
}
}
err = writer.WriteByte(0)
if err != nil {
return err
}
err = varbin.Write(writer, binary.BigEndian, logLines)
if err != nil {
return err
}
}
}
}
func (c *CommandClient) handleLogConn(conn net.Conn) {
reader := bufio.NewReader(conn)
for {
messageType, err := reader.ReadByte()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
var messages []string
switch messageType {
case 0:
err = varbin.Read(reader, binary.BigEndian, &messages)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteLogs(newIterator(messages))
case 1:
c.handler.ClearLogs()
}
}
}
func connKeepAlive(reader io.Reader) context.Context {
ctx, cancel := context.WithCancelCause(context.Background())
go func() {
for {
_, err := reader.Read(make([]byte, 1))
if err != nil {
cancel(err)
return
}
}
}()
return ctx
}

View File

@@ -1,59 +0,0 @@
package libbox
import (
"encoding/binary"
"net"
"github.com/sagernet/sing/common/varbin"
)
func (c *CommandClient) ServiceReload() error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandServiceReload))
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleServiceReload(conn net.Conn) error {
rErr := s.handler.ServiceReload()
err := binary.Write(conn, binary.BigEndian, rErr != nil)
if err != nil {
return err
}
if rErr != nil {
return varbin.Write(conn, binary.BigEndian, rErr.Error())
}
return nil
}
func (c *CommandClient) ServiceClose() error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandServiceClose))
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleServiceClose(conn net.Conn) error {
rErr := s.service.Close()
s.handler.PostServiceClose()
err := binary.Write(conn, binary.BigEndian, rErr != nil)
if err != nil {
return err
}
if rErr != nil {
return varbin.Write(conn, binary.BigEndian, rErr.Error())
}
return nil
}

View File

@@ -1,58 +0,0 @@
package libbox
import (
"encoding/binary"
"net"
"github.com/sagernet/sing-box/protocol/group"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
)
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandSelectOutbound))
if err != nil {
return err
}
err = varbin.Write(conn, binary.BigEndian, groupTag)
if err != nil {
return err
}
err = varbin.Write(conn, binary.BigEndian, outboundTag)
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleSelectOutbound(conn net.Conn) error {
groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
outboundTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
service := s.service
if service == nil {
return writeError(conn, E.New("service not ready"))
}
outboundGroup, isLoaded := service.instance.Outbound().Outbound(groupTag)
if !isLoaded {
return writeError(conn, E.New("selector not found: ", groupTag))
}
selector, isSelector := outboundGroup.(*group.Selector)
if !isSelector {
return writeError(conn, E.New("outbound is not a selector: ", groupTag))
}
if !selector.SelectOutbound(outboundTag) {
return writeError(conn, E.New("outbound not found in selector: ", outboundTag))
}
return writeError(conn, nil)
}

View File

@@ -1,182 +1,266 @@
package libbox
import (
"encoding/binary"
"context"
"errors"
"net"
"os"
"path/filepath"
"sync"
"strconv"
"syscall"
"time"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/daemon"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/debug"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/x/list"
"github.com/sagernet/sing/service"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
type CommandServer struct {
listener net.Listener
handler CommandServerHandler
access sync.Mutex
savedLines list.List[string]
maxLines int
subscriber *observable.Subscriber[string]
observer *observable.Observer[string]
service *BoxService
// These channels only work with a single client. if multi-client support is needed, replace with Subscriber/Observer
urlTestUpdate chan struct{}
modeUpdate chan struct{}
logReset chan struct{}
closedConnections []Connection
*daemon.StartedService
handler CommandServerHandler
platformInterface PlatformInterface
platformWrapper *platformInterfaceWrapper
grpcServer *grpc.Server
listener net.Listener
endPauseTimer *time.Timer
}
type CommandServerHandler interface {
ServiceStop() error
ServiceReload() error
PostServiceClose()
GetSystemProxyStatus() *SystemProxyStatus
SetSystemProxyEnabled(isEnabled bool) error
GetSystemProxyStatus() (*SystemProxyStatus, error)
SetSystemProxyEnabled(enabled bool) error
WriteDebugMessage(message string)
}
func NewCommandServer(handler CommandServerHandler, maxLines int32) *CommandServer {
func NewCommandServer(handler CommandServerHandler, platformInterface PlatformInterface) (*CommandServer, error) {
ctx := BaseContext(platformInterface)
platformWrapper := &platformInterfaceWrapper{
iif: platformInterface,
useProcFS: platformInterface.UseProcFS(),
}
service.MustRegister[adapter.PlatformInterface](ctx, platformWrapper)
server := &CommandServer{
handler: handler,
maxLines: int(maxLines),
subscriber: observable.NewSubscriber[string](128),
urlTestUpdate: make(chan struct{}, 1),
modeUpdate: make(chan struct{}, 1),
logReset: make(chan struct{}, 1),
handler: handler,
platformInterface: platformInterface,
platformWrapper: platformWrapper,
}
server.observer = observable.NewObserver[string](server.subscriber, 64)
return server
server.StartedService = daemon.NewStartedService(daemon.ServiceOptions{
Context: ctx,
// Platform: platformWrapper,
Handler: (*platformHandler)(server),
Debug: sDebug,
LogMaxLines: sLogMaxLines,
// WorkingDirectory: sWorkingPath,
// TempDirectory: sTempPath,
// UserID: sUserID,
// GroupID: sGroupID,
// SystemProxyEnabled: false,
})
return server, nil
}
func (s *CommandServer) SetService(newService *BoxService) {
if newService != nil {
service.PtrFromContext[urltest.HistoryStorage](newService.ctx).SetHook(s.urlTestUpdate)
newService.clashServer.(*clashapi.Server).SetModeUpdateHook(s.modeUpdate)
func unaryAuthInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
if sCommandServerSecret == "" {
return handler(ctx, req)
}
s.service = newService
s.notifyURLTestUpdate()
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "missing metadata")
}
values := md.Get("x-command-secret")
if len(values) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing authentication secret")
}
if values[0] != sCommandServerSecret {
return nil, status.Error(codes.Unauthenticated, "invalid authentication secret")
}
return handler(ctx, req)
}
func (s *CommandServer) notifyURLTestUpdate() {
select {
case s.urlTestUpdate <- struct{}{}:
default:
func streamAuthInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if sCommandServerSecret == "" {
return handler(srv, ss)
}
md, ok := metadata.FromIncomingContext(ss.Context())
if !ok {
return status.Error(codes.Unauthenticated, "missing metadata")
}
values := md.Get("x-command-secret")
if len(values) == 0 {
return status.Error(codes.Unauthenticated, "missing authentication secret")
}
if values[0] != sCommandServerSecret {
return status.Error(codes.Unauthenticated, "invalid authentication secret")
}
return handler(srv, ss)
}
func (s *CommandServer) Start() error {
if !sTVOS {
return s.listenUNIX()
} else {
return s.listenTCP()
}
}
func (s *CommandServer) listenUNIX() error {
sockPath := filepath.Join(sBasePath, "command.sock")
os.Remove(sockPath)
listener, err := net.ListenUnix("unix", &net.UnixAddr{
Name: sockPath,
Net: "unix",
})
if err != nil {
return E.Cause(err, "listen ", sockPath)
}
err = os.Chown(sockPath, sUserID, sGroupID)
if err != nil {
listener.Close()
os.Remove(sockPath)
return E.Cause(err, "chown")
}
s.listener = listener
go s.loopConnection(listener)
return nil
}
func (s *CommandServer) listenTCP() error {
listener, err := net.Listen("tcp", "127.0.0.1:8964")
if err != nil {
return E.Cause(err, "listen")
}
s.listener = listener
go s.loopConnection(listener)
return nil
}
func (s *CommandServer) Close() error {
return common.Close(
s.listener,
s.observer,
var (
listener net.Listener
err error
)
}
func (s *CommandServer) loopConnection(listener net.Listener) {
for {
conn, err := listener.Accept()
if err != nil {
return
}
go func() {
hErr := s.handleConnection(conn)
if hErr != nil && !E.IsClosed(err) {
if debug.Enabled {
log.Warn("log-server: process connection: ", hErr)
}
if sCommandServerListenPort == 0 {
sockPath := filepath.Join(sBasePath, "command.sock")
os.Remove(sockPath)
for i := 0; i < 30; i++ {
listener, err = net.ListenUnix("unix", &net.UnixAddr{
Name: sockPath,
Net: "unix",
})
if err == nil {
break
}
}()
if !errors.Is(err, syscall.EROFS) {
break
}
time.Sleep(time.Second)
}
if err != nil {
return E.Cause(err, "listen command server")
}
if sUserID != os.Getuid() {
err = os.Chown(sockPath, sUserID, sGroupID)
if err != nil {
listener.Close()
os.Remove(sockPath)
return E.Cause(err, "chown")
}
}
} else {
listener, err = net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(sCommandServerListenPort))))
if err != nil {
return E.Cause(err, "listen command server")
}
}
s.listener = listener
serverOptions := []grpc.ServerOption{
grpc.UnaryInterceptor(unaryAuthInterceptor),
grpc.StreamInterceptor(streamAuthInterceptor),
}
s.grpcServer = grpc.NewServer(serverOptions...)
daemon.RegisterStartedServiceServer(s.grpcServer, s.StartedService)
go s.grpcServer.Serve(listener)
return nil
}
func (s *CommandServer) Close() {
if s.grpcServer != nil {
s.grpcServer.Stop()
}
common.Close(s.listener)
}
type OverrideOptions struct {
AutoRedirect bool
IncludePackage StringIterator
ExcludePackage StringIterator
}
func (s *CommandServer) StartOrReloadService(configContent string, options *OverrideOptions) error {
return s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{
AutoRedirect: options.AutoRedirect,
IncludePackage: iteratorToArray(options.IncludePackage),
ExcludePackage: iteratorToArray(options.ExcludePackage),
})
}
func (s *CommandServer) CloseService() error {
return s.StartedService.CloseService()
}
func (s *CommandServer) WriteMessage(level int32, message string) {
s.StartedService.WriteMessage(log.Level(level), message)
}
func (s *CommandServer) SetError(message string) {
s.StartedService.SetError(E.New(message))
}
func (s *CommandServer) NeedWIFIState() bool {
instance := s.StartedService.Instance()
if instance == nil || instance.Box() == nil {
return false
}
return instance.Box().Router().NeedWIFIState()
}
func (s *CommandServer) Pause() {
instance := s.StartedService.Instance()
if instance == nil || instance.PauseManager() == nil {
return
}
instance.PauseManager().DevicePause()
if C.IsIos {
if s.endPauseTimer == nil {
s.endPauseTimer = time.AfterFunc(time.Minute, instance.PauseManager().DeviceWake)
} else {
s.endPauseTimer.Reset(time.Minute)
}
}
}
func (s *CommandServer) handleConnection(conn net.Conn) error {
defer conn.Close()
var command uint8
err := binary.Read(conn, binary.BigEndian, &command)
if err != nil {
return E.Cause(err, "read command")
func (s *CommandServer) Wake() {
instance := s.StartedService.Instance()
if instance == nil || instance.PauseManager() == nil {
return
}
switch int32(command) {
case CommandLog:
return s.handleLogConn(conn)
case CommandStatus:
return s.handleStatusConn(conn)
case CommandServiceReload:
return s.handleServiceReload(conn)
case CommandServiceClose:
return s.handleServiceClose(conn)
case CommandCloseConnections:
return s.handleCloseConnections(conn)
case CommandGroup:
return s.handleGroupConn(conn)
case CommandSelectOutbound:
return s.handleSelectOutbound(conn)
case CommandURLTest:
return s.handleURLTest(conn)
case CommandGroupExpand:
return s.handleSetGroupExpand(conn)
case CommandClashMode:
return s.handleModeConn(conn)
case CommandSetClashMode:
return s.handleSetClashMode(conn)
case CommandGetSystemProxyStatus:
return s.handleGetSystemProxyStatus(conn)
case CommandSetSystemProxyEnabled:
return s.handleSetSystemProxyEnabled(conn)
case CommandConnections:
return s.handleConnectionsConn(conn)
case CommandCloseConnection:
return s.handleCloseConnection(conn)
case CommandGetDeprecatedNotes:
return s.handleGetDeprecatedNotes(conn)
default:
return E.New("unknown command: ", command)
if !C.IsIos {
instance.PauseManager().DeviceWake()
}
}
func (s *CommandServer) ResetNetwork() {
instance := s.StartedService.Instance()
if instance == nil || instance.Box() == nil {
return
}
instance.Box().Router().ResetNetwork()
}
func (s *CommandServer) UpdateWIFIState() {
instance := s.StartedService.Instance()
if instance == nil || instance.Box() == nil {
return
}
instance.Box().Network().UpdateWIFIState()
}
type platformHandler CommandServer
func (h *platformHandler) ServiceStop() error {
return (*CommandServer)(h).handler.ServiceStop()
}
func (h *platformHandler) ServiceReload() error {
return (*CommandServer)(h).handler.ServiceReload()
}
func (h *platformHandler) SystemProxyStatus() (*daemon.SystemProxyStatus, error) {
status, err := (*CommandServer)(h).handler.GetSystemProxyStatus()
if err != nil {
return nil, err
}
return &daemon.SystemProxyStatus{
Enabled: status.Enabled,
Available: status.Available,
}, nil
}
func (h *platformHandler) SetSystemProxyEnabled(enabled bool) error {
return (*CommandServer)(h).handler.SetSystemProxyEnabled(enabled)
}
func (h *platformHandler) WriteDebugMessage(message string) {
(*CommandServer)(h).handler.WriteDebugMessage(message)
}

View File

@@ -1,39 +0,0 @@
package libbox
import (
"encoding/binary"
"io"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
)
func readError(reader io.Reader) error {
var hasError bool
err := binary.Read(reader, binary.BigEndian, &hasError)
if err != nil {
return err
}
if hasError {
errorMessage, err := varbin.ReadValue[string](reader, binary.BigEndian)
if err != nil {
return err
}
return E.New(errorMessage)
}
return nil
}
func writeError(writer io.Writer, wErr error) error {
err := binary.Write(writer, binary.BigEndian, wErr != nil)
if err != nil {
return err
}
if wErr != nil {
err = varbin.Write(writer, binary.BigEndian, wErr.Error())
if err != nil {
return err
}
}
return nil
}

View File

@@ -1,85 +0,0 @@
package libbox
import (
"encoding/binary"
"net"
"runtime"
"time"
"github.com/sagernet/sing-box/common/conntrack"
"github.com/sagernet/sing-box/experimental/clashapi"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/memory"
)
type StatusMessage struct {
Memory int64
Goroutines int32
ConnectionsIn int32
ConnectionsOut int32
TrafficAvailable bool
Uplink int64
Downlink int64
UplinkTotal int64
DownlinkTotal int64
}
func (s *CommandServer) readStatus() StatusMessage {
var message StatusMessage
message.Memory = int64(memory.Inuse())
message.Goroutines = int32(runtime.NumGoroutine())
message.ConnectionsOut = int32(conntrack.Count())
if s.service != nil {
message.TrafficAvailable = true
trafficManager := s.service.clashServer.(*clashapi.Server).TrafficManager()
message.UplinkTotal, message.DownlinkTotal = trafficManager.Total()
message.ConnectionsIn = int32(trafficManager.ConnectionsLen())
}
return message
}
func (s *CommandServer) handleStatusConn(conn net.Conn) error {
var interval int64
err := binary.Read(conn, binary.BigEndian, &interval)
if err != nil {
return E.Cause(err, "read interval")
}
ticker := time.NewTicker(time.Duration(interval))
defer ticker.Stop()
ctx := connKeepAlive(conn)
status := s.readStatus()
uploadTotal := status.UplinkTotal
downloadTotal := status.DownlinkTotal
for {
err = binary.Write(conn, binary.BigEndian, status)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
}
status = s.readStatus()
upload := status.UplinkTotal - uploadTotal
download := status.DownlinkTotal - downloadTotal
uploadTotal = status.UplinkTotal
downloadTotal = status.DownlinkTotal
status.Uplink = upload
status.Downlink = download
}
}
func (c *CommandClient) handleStatusConn(conn net.Conn) {
for {
var message StatusMessage
err := binary.Read(conn, binary.BigEndian, &message)
if err != nil {
c.handler.Disconnected(err.Error())
return
}
c.handler.WriteStatus(&message)
}
}

View File

@@ -1,80 +0,0 @@
package libbox
import (
"encoding/binary"
"net"
)
type SystemProxyStatus struct {
Available bool
Enabled bool
}
func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) {
conn, err := c.directConnectWithRetry()
if err != nil {
return nil, err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandGetSystemProxyStatus))
if err != nil {
return nil, err
}
var status SystemProxyStatus
err = binary.Read(conn, binary.BigEndian, &status.Available)
if err != nil {
return nil, err
}
if status.Available {
err = binary.Read(conn, binary.BigEndian, &status.Enabled)
if err != nil {
return nil, err
}
}
return &status, nil
}
func (s *CommandServer) handleGetSystemProxyStatus(conn net.Conn) error {
status := s.handler.GetSystemProxyStatus()
err := binary.Write(conn, binary.BigEndian, status.Available)
if err != nil {
return err
}
if status.Available {
err = binary.Write(conn, binary.BigEndian, status.Enabled)
if err != nil {
return err
}
}
return nil
}
func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandSetSystemProxyEnabled))
if err != nil {
return err
}
err = binary.Write(conn, binary.BigEndian, isEnabled)
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleSetSystemProxyEnabled(conn net.Conn) error {
var isEnabled bool
err := binary.Read(conn, binary.BigEndian, &isEnabled)
if err != nil {
return err
}
err = s.handler.SetSystemProxyEnabled(isEnabled)
if err != nil {
return writeError(conn, err)
}
return writeError(conn, nil)
}

View File

@@ -0,0 +1,276 @@
package libbox
import (
"slices"
"strings"
"github.com/sagernet/sing-box/daemon"
M "github.com/sagernet/sing/common/metadata"
)
type StatusMessage struct {
Memory int64
Goroutines int32
ConnectionsIn int32
ConnectionsOut int32
TrafficAvailable bool
Uplink int64
Downlink int64
UplinkTotal int64
DownlinkTotal int64
}
type SystemProxyStatus struct {
Available bool
Enabled bool
}
type OutboundGroup struct {
Tag string
Type string
Selectable bool
Selected string
IsExpand bool
ItemList []*OutboundGroupItem
}
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
return newIterator(g.ItemList)
}
type OutboundGroupIterator interface {
Next() *OutboundGroup
HasNext() bool
}
type OutboundGroupItem struct {
Tag string
Type string
URLTestTime int64
URLTestDelay int32
}
type OutboundGroupItemIterator interface {
Next() *OutboundGroupItem
HasNext() bool
}
const (
ConnectionStateAll = iota
ConnectionStateActive
ConnectionStateClosed
)
type Connections struct {
input []Connection
filtered []Connection
}
func (c *Connections) FilterState(state int32) {
c.filtered = c.filtered[:0]
switch state {
case ConnectionStateAll:
c.filtered = append(c.filtered, c.input...)
case ConnectionStateActive:
for _, connection := range c.input {
if connection.ClosedAt == 0 {
c.filtered = append(c.filtered, connection)
}
}
case ConnectionStateClosed:
for _, connection := range c.input {
if connection.ClosedAt != 0 {
c.filtered = append(c.filtered, connection)
}
}
}
}
func (c *Connections) SortByDate() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
if x.CreatedAt < y.CreatedAt {
return 1
} else if x.CreatedAt > y.CreatedAt {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) SortByTraffic() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
xTraffic := x.Uplink + x.Downlink
yTraffic := y.Uplink + y.Downlink
if xTraffic < yTraffic {
return 1
} else if xTraffic > yTraffic {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) SortByTrafficTotal() {
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
xTraffic := x.UplinkTotal + x.DownlinkTotal
yTraffic := y.UplinkTotal + y.DownlinkTotal
if xTraffic < yTraffic {
return 1
} else if xTraffic > yTraffic {
return -1
} else {
return strings.Compare(y.ID, x.ID)
}
})
}
func (c *Connections) Iterator() ConnectionIterator {
return newPtrIterator(c.filtered)
}
type Connection struct {
ID string
Inbound string
InboundType string
IPVersion int32
Network string
Source string
Destination string
Domain string
Protocol string
User string
FromOutbound string
CreatedAt int64
ClosedAt int64
Uplink int64
Downlink int64
UplinkTotal int64
DownlinkTotal int64
Rule string
Outbound string
OutboundType string
ChainList []string
}
func (c *Connection) Chain() StringIterator {
return newIterator(c.ChainList)
}
func (c *Connection) DisplayDestination() string {
destination := M.ParseSocksaddr(c.Destination)
if destination.IsIP() && c.Domain != "" {
destination = M.Socksaddr{
Fqdn: c.Domain,
Port: destination.Port,
}
return destination.String()
}
return c.Destination
}
type ConnectionIterator interface {
Next() *Connection
HasNext() bool
}
func StatusMessageFromGRPC(status *daemon.Status) *StatusMessage {
if status == nil {
return nil
}
return &StatusMessage{
Memory: int64(status.Memory),
Goroutines: status.Goroutines,
ConnectionsIn: status.ConnectionsIn,
ConnectionsOut: status.ConnectionsOut,
TrafficAvailable: status.TrafficAvailable,
Uplink: status.Uplink,
Downlink: status.Downlink,
UplinkTotal: status.UplinkTotal,
DownlinkTotal: status.DownlinkTotal,
}
}
func OutboundGroupIteratorFromGRPC(groups *daemon.Groups) OutboundGroupIterator {
if groups == nil || len(groups.Group) == 0 {
return newIterator([]*OutboundGroup{})
}
var libboxGroups []*OutboundGroup
for _, g := range groups.Group {
libboxGroup := &OutboundGroup{
Tag: g.Tag,
Type: g.Type,
Selectable: g.Selectable,
Selected: g.Selected,
IsExpand: g.IsExpand,
}
for _, item := range g.Items {
libboxGroup.ItemList = append(libboxGroup.ItemList, &OutboundGroupItem{
Tag: item.Tag,
Type: item.Type,
URLTestTime: item.UrlTestTime,
URLTestDelay: item.UrlTestDelay,
})
}
libboxGroups = append(libboxGroups, libboxGroup)
}
return newIterator(libboxGroups)
}
func ConnectionFromGRPC(conn *daemon.Connection) Connection {
return Connection{
ID: conn.Id,
Inbound: conn.Inbound,
InboundType: conn.InboundType,
IPVersion: conn.IpVersion,
Network: conn.Network,
Source: conn.Source,
Destination: conn.Destination,
Domain: conn.Domain,
Protocol: conn.Protocol,
User: conn.User,
FromOutbound: conn.FromOutbound,
CreatedAt: conn.CreatedAt,
ClosedAt: conn.ClosedAt,
Uplink: conn.Uplink,
Downlink: conn.Downlink,
UplinkTotal: conn.UplinkTotal,
DownlinkTotal: conn.DownlinkTotal,
Rule: conn.Rule,
Outbound: conn.Outbound,
OutboundType: conn.OutboundType,
ChainList: conn.ChainList,
}
}
func ConnectionsFromGRPC(connections *daemon.Connections) []Connection {
if connections == nil || len(connections.Connections) == 0 {
return nil
}
var libboxConnections []Connection
for _, conn := range connections.Connections {
libboxConnections = append(libboxConnections, ConnectionFromGRPC(conn))
}
return libboxConnections
}
func SystemProxyStatusFromGRPC(status *daemon.SystemProxyStatus) *SystemProxyStatus {
if status == nil {
return nil
}
return &SystemProxyStatus{
Available: status.Available,
Enabled: status.Enabled,
}
}
func SystemProxyStatusToGRPC(status *SystemProxyStatus) *daemon.SystemProxyStatus {
if status == nil {
return nil
}
return &daemon.SystemProxyStatus{
Available: status.Available,
Enabled: status.Enabled,
}
}

View File

@@ -1,86 +0,0 @@
package libbox
import (
"encoding/binary"
"net"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
"github.com/sagernet/sing/service"
)
func (c *CommandClient) URLTest(groupTag string) error {
conn, err := c.directConnect()
if err != nil {
return err
}
defer conn.Close()
err = binary.Write(conn, binary.BigEndian, uint8(CommandURLTest))
if err != nil {
return err
}
err = varbin.Write(conn, binary.BigEndian, groupTag)
if err != nil {
return err
}
return readError(conn)
}
func (s *CommandServer) handleURLTest(conn net.Conn) error {
groupTag, err := varbin.ReadValue[string](conn, binary.BigEndian)
if err != nil {
return err
}
serviceNow := s.service
if serviceNow == nil {
return nil
}
abstractOutboundGroup, isLoaded := serviceNow.instance.Outbound().Outbound(groupTag)
if !isLoaded {
return writeError(conn, E.New("outbound group not found: ", groupTag))
}
outboundGroup, isOutboundGroup := abstractOutboundGroup.(adapter.OutboundGroup)
if !isOutboundGroup {
return writeError(conn, E.New("outbound is not a group: ", groupTag))
}
urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest)
if isURLTest {
go urlTest.CheckOutbounds()
} else {
historyStorage := service.PtrFromContext[urltest.HistoryStorage](serviceNow.ctx)
outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
itOutbound, _ := serviceNow.instance.Outbound().Outbound(it)
return itOutbound
}), func(it adapter.Outbound) bool {
if it == nil {
return false
}
_, isGroup := it.(adapter.OutboundGroup)
return !isGroup
})
b, _ := batch.New(serviceNow.ctx, batch.WithConcurrencyNum[any](10))
for _, detour := range outbounds {
outboundToTest := detour
outboundTag := outboundToTest.Tag()
b.Go(outboundTag, func() (any, error) {
t, err := urltest.URLTest(serviceNow.ctx, "", outboundToTest)
if err != nil {
historyStorage.DeleteURLTestHistory(outboundTag)
} else {
historyStorage.StoreURLTestHistory(outboundTag, &adapter.URLTestHistory{
Time: time.Now(),
Delay: t,
})
}
return nil, nil
})
}
}
return writeError(conn, nil)
}

View File

@@ -3,19 +3,16 @@ package libbox
import (
"bytes"
"context"
"net/netip"
"os"
"github.com/sagernet/sing-box"
box "github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/process"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/dns"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/json"
@@ -55,7 +52,7 @@ func CheckConfig(configContent string) error {
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = service.ContextWith[platform.Interface](ctx, (*platformInterfaceStub)(nil))
ctx = service.ContextWith[adapter.PlatformInterface](ctx, (*platformInterfaceStub)(nil))
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
@@ -80,7 +77,11 @@ func (s *platformInterfaceStub) AutoDetectInterfaceControl(fd int) error {
return nil
}
func (s *platformInterfaceStub) OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
func (s *platformInterfaceStub) UsePlatformInterface() bool {
return false
}
func (s *platformInterfaceStub) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
return nil, os.ErrInvalid
}
@@ -92,7 +93,11 @@ func (s *platformInterfaceStub) CreateDefaultInterfaceMonitor(logger logger.Logg
return (*interfaceMonitorStub)(nil)
}
func (s *platformInterfaceStub) Interfaces() ([]adapter.NetworkInterface, error) {
func (s *platformInterfaceStub) UsePlatformNetworkInterfaces() bool {
return false
}
func (s *platformInterfaceStub) NetworkInterfaces() ([]adapter.NetworkInterface, error) {
return nil, os.ErrInvalid
}
@@ -100,15 +105,15 @@ func (s *platformInterfaceStub) UnderNetworkExtension() bool {
return false
}
func (s *platformInterfaceStub) IncludeAllNetworks() bool {
func (s *platformInterfaceStub) NetworkExtensionIncludeAllNetworks() bool {
return false
}
func (s *platformInterfaceStub) ClearDNSCache() {
}
func (s *platformInterfaceStub) UsePlatformWIFIMonitor() bool {
return false
func (s *platformInterfaceStub) RequestPermissionForWIFIState() error {
return nil
}
func (s *platformInterfaceStub) ReadWIFIState() adapter.WIFIState {
@@ -119,11 +124,27 @@ func (s *platformInterfaceStub) SystemCertificates() []string {
return nil
}
func (s *platformInterfaceStub) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) {
func (s *platformInterfaceStub) UsePlatformConnectionOwnerFinder() bool {
return false
}
func (s *platformInterfaceStub) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) {
return nil, os.ErrInvalid
}
func (s *platformInterfaceStub) SendNotification(notification *platform.Notification) error {
func (s *platformInterfaceStub) UsePlatformNotification() bool {
return false
}
func (s *platformInterfaceStub) SendNotification(notification *adapter.Notification) error {
return nil
}
func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool {
return false
}
func (s *platformInterfaceStub) LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions] {
return nil
}

View File

@@ -1,33 +1,9 @@
package libbox
import (
"sync"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing/common"
)
var _ deprecated.Manager = (*deprecatedManager)(nil)
type deprecatedManager struct {
access sync.Mutex
notes []deprecated.Note
}
func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) {
m.access.Lock()
defer m.access.Unlock()
m.notes = common.Uniq(append(m.notes, feature))
}
func (m *deprecatedManager) Get() []deprecated.Note {
m.access.Lock()
defer m.access.Unlock()
notes := m.notes
m.notes = nil
return notes
}
var _ = deprecated.Note(DeprecatedNote{})
type DeprecatedNote struct {

View File

@@ -77,22 +77,27 @@ func NewHTTPClient() HTTPClient {
}
func (c *httpClient) ModernTLS() {
c.tls.MinVersion = tls.VersionTLS12
c.tls.CipherSuites = common.Map(tls.CipherSuites(), func(it *tls.CipherSuite) uint16 { return it.ID })
c.setTLSVersion(tls.VersionTLS12, 0, func(suite *tls.CipherSuite) bool { return true })
}
func (c *httpClient) RestrictedTLS() {
c.tls.MinVersion = tls.VersionTLS13
c.tls.CipherSuites = common.Map(common.Filter(tls.CipherSuites(), func(it *tls.CipherSuite) bool {
return common.Contains(it.SupportedVersions, uint16(tls.VersionTLS13))
}), func(it *tls.CipherSuite) uint16 {
c.setTLSVersion(tls.VersionTLS13, 0, func(suite *tls.CipherSuite) bool {
return common.Contains(suite.SupportedVersions, uint16(tls.VersionTLS13))
})
}
func (c *httpClient) setTLSVersion(minVersion, maxVersion uint16, filter func(*tls.CipherSuite) bool) {
c.tls.MinVersion = minVersion
if maxVersion != 0 {
c.tls.MaxVersion = maxVersion
}
c.tls.CipherSuites = common.Map(common.Filter(tls.CipherSuites(), filter), func(it *tls.CipherSuite) uint16 {
return it.ID
})
}
func (c *httpClient) PinnedTLS12() {
c.tls.MinVersion = tls.VersionTLS12
c.tls.MaxVersion = tls.VersionTLS12
c.setTLSVersion(tls.VersionTLS12, tls.VersionTLS12, func(suite *tls.CipherSuite) bool { return true })
}
func (c *httpClient) PinnedSHA256(sumHex string) {
@@ -178,9 +183,7 @@ func (r *httpRequest) SetUserAgent(userAgent string) {
}
func (r *httpRequest) SetContent(content []byte) {
buffer := bytes.Buffer{}
buffer.Write(content)
r.request.Body = io.NopCloser(bytes.NewReader(buffer.Bytes()))
r.request.Body = io.NopCloser(bytes.NewReader(content))
r.request.ContentLength = int64(len(content))
}

View File

@@ -8,6 +8,12 @@ type StringIterator interface {
Next() string
}
type Int32Iterator interface {
Len() int32
HasNext() bool
Next() int32
}
var _ StringIterator = (*iterator[string])(nil)
type iterator[T any] struct {

View File

@@ -1,7 +1,7 @@
package libbox
import (
"github.com/sagernet/sing-tun"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"

View File

@@ -10,7 +10,6 @@ type PlatformInterface interface {
UsePlatformAutoDetectInterfaceControl() bool
AutoDetectInterfaceControl(fd int32) error
OpenTun(options TunOptions) (int32, error)
WriteLog(message string)
UseProcFS() bool
FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (int32, error)
PackageNameByUid(uid int32) (string, error)
@@ -26,11 +25,6 @@ type PlatformInterface interface {
SendNotification(notification *Notification) error
}
type TunInterface interface {
FileDescriptor() int32
Close() error
}
type InterfaceUpdateListener interface {
UpdateDefaultInterface(interfaceName string, interfaceIndex int32, isExpensive bool, isConstrained bool)
}

View File

@@ -1,36 +0,0 @@
package platform
import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/process"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/logger"
)
type Interface interface {
Initialize(networkManager adapter.NetworkManager) error
UsePlatformAutoDetectInterfaceControl() bool
AutoDetectInterfaceControl(fd int) error
OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
Interfaces() ([]adapter.NetworkInterface, error)
UnderNetworkExtension() bool
IncludeAllNetworks() bool
ClearDNSCache()
UsePlatformWIFIMonitor() bool
ReadWIFIState() adapter.WIFIState
SystemCertificates() []string
process.Searcher
SendNotification(notification *Notification) error
}
type Notification struct {
Identifier string
TypeName string
TypeID int32
Title string
Subtitle string
Body string
OpenURL string
}

View File

@@ -1,123 +1,28 @@
package libbox
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"net"
"net/netip"
"os"
"runtime"
runtimeDebug "runtime/debug"
"strconv"
"sync"
"syscall"
"time"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/process"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental/deprecated"
"github.com/sagernet/sing-box/experimental/libbox/internal/procfs"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/logger"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/service"
"github.com/sagernet/sing/service/pause"
)
type BoxService struct {
ctx context.Context
cancel context.CancelFunc
urlTestHistoryStorage adapter.URLTestHistoryStorage
instance *box.Box
clashServer adapter.ClashServer
pauseManager pause.Manager
iOSPauseFields
}
func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) {
ctx := BaseContext(platformInterface)
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
options, err := parseConfig(ctx, configContent)
if err != nil {
return nil, err
}
runtimeDebug.FreeOSMemory()
ctx, cancel := context.WithCancel(ctx)
urlTestHistoryStorage := urltest.NewHistoryStorage()
ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
platformWrapper := &platformInterfaceWrapper{
iif: platformInterface,
useProcFS: platformInterface.UseProcFS(),
}
service.MustRegister[platform.Interface](ctx, platformWrapper)
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
PlatformLogWriter: platformWrapper,
})
if err != nil {
cancel()
return nil, E.Cause(err, "create service")
}
runtimeDebug.FreeOSMemory()
return &BoxService{
ctx: ctx,
cancel: cancel,
instance: instance,
urlTestHistoryStorage: urlTestHistoryStorage,
pauseManager: service.FromContext[pause.Manager](ctx),
clashServer: service.FromContext[adapter.ClashServer](ctx),
}, nil
}
func (s *BoxService) Start() error {
if sFixAndroidStack {
var err error
done := make(chan struct{})
go func() {
err = s.instance.Start()
close(done)
}()
<-done
return err
} else {
return s.instance.Start()
}
}
func (s *BoxService) Close() error {
s.cancel()
s.urlTestHistoryStorage.Close()
var err error
done := make(chan struct{})
go func() {
err = s.instance.Close()
close(done)
}()
select {
case <-done:
return err
case <-time.After(C.FatalStopTimeout):
os.Exit(1)
return nil
}
}
func (s *BoxService) NeedWIFIState() bool {
return s.instance.Network().NeedWIFIState()
}
var (
_ platform.Interface = (*platformInterfaceWrapper)(nil)
_ log.PlatformWriter = (*platformInterfaceWrapper)(nil)
)
var _ adapter.PlatformInterface = (*platformInterfaceWrapper)(nil)
type platformInterfaceWrapper struct {
iif PlatformInterface
@@ -143,7 +48,11 @@ func (w *platformInterfaceWrapper) AutoDetectInterfaceControl(fd int) error {
return w.iif.AutoDetectInterfaceControl(int32(fd))
}
func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
func (w *platformInterfaceWrapper) UsePlatformInterface() bool {
return true
}
func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 {
return nil, E.New("platform: unsupported uid options")
}
@@ -172,6 +81,10 @@ func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions
return tun.New(*options)
}
func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
return true
}
func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor {
return &platformDefaultInterfaceMonitor{
platformInterfaceWrapper: w,
@@ -179,7 +92,11 @@ func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.L
}
}
func (w *platformInterfaceWrapper) Interfaces() ([]adapter.NetworkInterface, error) {
func (w *platformInterfaceWrapper) UsePlatformNetworkInterfaces() bool {
return true
}
func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterface, error) {
interfaceIterator, err := w.iif.GetInterfaces()
if err != nil {
return nil, err
@@ -216,7 +133,7 @@ func (w *platformInterfaceWrapper) UnderNetworkExtension() bool {
return w.iif.UnderNetworkExtension()
}
func (w *platformInterfaceWrapper) IncludeAllNetworks() bool {
func (w *platformInterfaceWrapper) NetworkExtensionIncludeAllNetworks() bool {
return w.iif.IncludeAllNetworks()
}
@@ -224,8 +141,8 @@ func (w *platformInterfaceWrapper) ClearDNSCache() {
w.iif.ClearDNSCache()
}
func (w *platformInterfaceWrapper) UsePlatformWIFIMonitor() bool {
return true
func (w *platformInterfaceWrapper) RequestPermissionForWIFIState() error {
return nil
}
func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
@@ -240,41 +157,82 @@ func (w *platformInterfaceWrapper) SystemCertificates() []string {
return iteratorToArray[string](w.iif.SystemCertificates())
}
func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) {
func (w *platformInterfaceWrapper) UsePlatformConnectionOwnerFinder() bool {
return true
}
func (w *platformInterfaceWrapper) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) {
var uid int32
if w.useProcFS {
var source netip.AddrPort
var destination netip.AddrPort
sourceAddr, _ := netip.ParseAddr(request.SourceAddress)
source = netip.AddrPortFrom(sourceAddr, uint16(request.SourcePort))
destAddr, _ := netip.ParseAddr(request.DestinationAddress)
destination = netip.AddrPortFrom(destAddr, uint16(request.DestinationPort))
var network string
switch request.IpProtocol {
case int32(syscall.IPPROTO_TCP):
network = "tcp"
case int32(syscall.IPPROTO_UDP):
network = "udp"
default:
return nil, E.New("unknown protocol: ", request.IpProtocol)
}
uid = procfs.ResolveSocketByProcSearch(network, source, destination)
if uid == -1 {
return nil, E.New("procfs: not found")
}
} else {
var ipProtocol int32
switch N.NetworkName(network) {
case N.NetworkTCP:
ipProtocol = syscall.IPPROTO_TCP
case N.NetworkUDP:
ipProtocol = syscall.IPPROTO_UDP
default:
return nil, E.New("unknown network: ", network)
}
var err error
uid, err = w.iif.FindConnectionOwner(ipProtocol, source.Addr().String(), int32(source.Port()), destination.Addr().String(), int32(destination.Port()))
uid, err = w.iif.FindConnectionOwner(request.IpProtocol, request.SourceAddress, request.SourcePort, request.DestinationAddress, request.DestinationPort)
if err != nil {
return nil, err
}
}
packageName, _ := w.iif.PackageNameByUid(uid)
return &process.Info{UserId: uid, PackageName: packageName}, nil
return &adapter.ConnectionOwner{
UserId: uid,
AndroidPackageName: packageName,
}, nil
}
func (w *platformInterfaceWrapper) DisableColors() bool {
return runtime.GOOS != "android"
}
func (w *platformInterfaceWrapper) WriteMessage(level log.Level, message string) {
w.iif.WriteLog(message)
func (w *platformInterfaceWrapper) UsePlatformNotification() bool {
return true
}
func (w *platformInterfaceWrapper) SendNotification(notification *platform.Notification) error {
func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notification) error {
return w.iif.SendNotification((*Notification)(notification))
}
func AvailablePort(startPort int32) (int32, error) {
for port := int(startPort); ; port++ {
if port > 65535 {
return 0, E.New("no available port found")
}
listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port))))
if err != nil {
if errors.Is(err, syscall.EADDRINUSE) {
continue
}
return 0, E.Cause(err, "find available port")
}
err = listener.Close()
if err != nil {
return 0, E.Cause(err, "close listener")
}
return int32(port), nil
}
}
func RandomHex(length int32) *StringBox {
bytes := make([]byte, length)
common.Must1(rand.Read(bytes))
return wrapString(hex.EncodeToString(bytes))
}

View File

@@ -1,32 +0,0 @@
package libbox
import (
"os"
"path/filepath"
)
func serviceErrorPath() string {
return filepath.Join(sWorkingPath, "network_extension_error")
}
func ClearServiceError() {
os.Remove(serviceErrorPath())
}
func ReadServiceError() (*StringBox, error) {
data, err := os.ReadFile(serviceErrorPath())
if err == nil {
os.Remove(serviceErrorPath())
}
return wrapString(string(data)), err
}
func WriteServiceError(message string) error {
errorFile, err := os.Create(serviceErrorPath())
if err != nil {
return err
}
errorFile.WriteString(message)
errorFile.Chown(sUserID, sGroupID)
return errorFile.Close()
}

View File

@@ -1,36 +0,0 @@
package libbox
import (
"time"
C "github.com/sagernet/sing-box/constant"
)
type iOSPauseFields struct {
endPauseTimer *time.Timer
}
func (s *BoxService) Pause() {
s.pauseManager.DevicePause()
if C.IsIos {
if s.endPauseTimer == nil {
s.endPauseTimer = time.AfterFunc(time.Minute, s.pauseManager.DeviceWake)
} else {
s.endPauseTimer.Reset(time.Minute)
}
}
}
func (s *BoxService) Wake() {
if !C.IsIos {
s.pauseManager.DeviceWake()
}
}
func (s *BoxService) ResetNetwork() {
s.instance.Router().ResetNetwork()
}
func (s *BoxService) UpdateWIFIState() {
s.instance.Network().UpdateWIFIState()
}

View File

@@ -2,9 +2,7 @@ package libbox
import (
"os"
"os/user"
"runtime/debug"
"strconv"
"time"
C "github.com/sagernet/sing-box/constant"
@@ -14,55 +12,53 @@ import (
)
var (
sBasePath string
sWorkingPath string
sTempPath string
sUserID int
sGroupID int
sTVOS bool
sFixAndroidStack bool
sBasePath string
sWorkingPath string
sTempPath string
sUserID int
sGroupID int
sFixAndroidStack bool
sCommandServerListenPort uint16
sCommandServerSecret string
sLogMaxLines int
sDebug bool
)
func init() {
debug.SetPanicOnFault(true)
debug.SetTraceback("all")
}
type SetupOptions struct {
BasePath string
WorkingPath string
TempPath string
Username string
IsTVOS bool
FixAndroidStack bool
BasePath string
WorkingPath string
TempPath string
FixAndroidStack bool
CommandServerListenPort int32
CommandServerSecret string
LogMaxLines int
Debug bool
}
func Setup(options *SetupOptions) error {
sBasePath = options.BasePath
sWorkingPath = options.WorkingPath
sTempPath = options.TempPath
if options.Username != "" {
sUser, err := user.Lookup(options.Username)
if err != nil {
return err
}
sUserID, _ = strconv.Atoi(sUser.Uid)
sGroupID, _ = strconv.Atoi(sUser.Gid)
} else {
sUserID = os.Getuid()
sGroupID = os.Getgid()
}
sTVOS = options.IsTVOS
sUserID = os.Getuid()
sGroupID = os.Getgid()
// TODO: remove after fixed
// https://github.com/golang/go/issues/68760
sFixAndroidStack = options.FixAndroidStack
sCommandServerListenPort = uint16(options.CommandServerListenPort)
sCommandServerSecret = options.CommandServerSecret
sLogMaxLines = options.LogMaxLines
sDebug = options.Debug
os.MkdirAll(sWorkingPath, 0o777)
os.MkdirAll(sTempPath, 0o777)
if options.Username != "" {
os.Chown(sWorkingPath, sUserID, sGroupID)
os.Chown(sTempPath, sUserID, sGroupID)
}
return nil
}

View File

@@ -5,7 +5,7 @@ import (
"net/netip"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)