Update sing-box core

This commit is contained in:
Shtorm
2026-03-10 04:25:01 +03:00
439 changed files with 30827 additions and 6031 deletions

View File

@@ -46,6 +46,7 @@ type CacheFile struct {
storeWARPConfig bool
rdrcTimeout time.Duration
DB *bbolt.DB
resetAccess sync.Mutex
saveMetadataTimer *time.Timer
saveFakeIPAccess sync.RWMutex
saveDomain map[netip.Addr]string
@@ -171,13 +172,55 @@ func (c *CacheFile) Close() error {
return c.DB.Close()
}
func (c *CacheFile) view(fn func(tx *bbolt.Tx) error) (err error) {
defer func() {
if r := recover(); r != nil {
c.resetDB()
err = E.New("database corrupted: ", r)
}
}()
return c.DB.View(fn)
}
func (c *CacheFile) batch(fn func(tx *bbolt.Tx) error) (err error) {
defer func() {
if r := recover(); r != nil {
c.resetDB()
err = E.New("database corrupted: ", r)
}
}()
return c.DB.Batch(fn)
}
func (c *CacheFile) update(fn func(tx *bbolt.Tx) error) (err error) {
defer func() {
if r := recover(); r != nil {
c.resetDB()
err = E.New("database corrupted: ", r)
}
}()
return c.DB.Update(fn)
}
func (c *CacheFile) resetDB() {
c.resetAccess.Lock()
defer c.resetAccess.Unlock()
c.DB.Close()
os.Remove(c.path)
db, err := bbolt.Open(c.path, 0o666, &bbolt.Options{Timeout: time.Second})
if err == nil {
_ = filemanager.Chown(c.ctx, c.path)
c.DB = db
}
}
func (c *CacheFile) StoreFakeIP() bool {
return c.storeFakeIP
}
func (c *CacheFile) LoadMode() string {
var mode string
c.DB.View(func(t *bbolt.Tx) error {
c.view(func(t *bbolt.Tx) error {
bucket := t.Bucket(bucketMode)
if bucket == nil {
return nil
@@ -195,7 +238,7 @@ func (c *CacheFile) LoadMode() string {
}
func (c *CacheFile) StoreMode(mode string) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
return c.batch(func(t *bbolt.Tx) error {
bucket, err := t.CreateBucketIfNotExists(bucketMode)
if err != nil {
return err
@@ -232,7 +275,7 @@ func (c *CacheFile) createBucket(t *bbolt.Tx, key []byte) (*bbolt.Bucket, error)
func (c *CacheFile) LoadSelected(group string) string {
var selected string
c.DB.View(func(t *bbolt.Tx) error {
c.view(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketSelected)
if bucket == nil {
return nil
@@ -247,7 +290,7 @@ func (c *CacheFile) LoadSelected(group string) string {
}
func (c *CacheFile) StoreSelected(group, selected string) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
return c.batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketSelected)
if err != nil {
return err
@@ -257,7 +300,7 @@ func (c *CacheFile) StoreSelected(group, selected string) error {
}
func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) {
c.DB.View(func(t *bbolt.Tx) error {
c.view(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketExpand)
if bucket == nil {
return nil
@@ -273,7 +316,7 @@ func (c *CacheFile) LoadGroupExpand(group string) (isExpand bool, loaded bool) {
}
func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
return c.batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketExpand)
if err != nil {
return err
@@ -288,7 +331,7 @@ func (c *CacheFile) StoreGroupExpand(group string, isExpand bool) error {
func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary {
var savedSet adapter.SavedBinary
err := c.DB.View(func(t *bbolt.Tx) error {
err := c.view(func(t *bbolt.Tx) error {
bucket := c.bucket(t, bucketRuleSet)
if bucket == nil {
return os.ErrNotExist
@@ -306,7 +349,7 @@ func (c *CacheFile) LoadRuleSet(tag string) *adapter.SavedBinary {
}
func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
return c.DB.Batch(func(t *bbolt.Tx) error {
return c.batch(func(t *bbolt.Tx) error {
bucket, err := c.createBucket(t, bucketRuleSet)
if err != nil {
return err

View File

@@ -23,7 +23,7 @@ var (
func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
var metadata adapter.FakeIPMetadata
err := c.DB.Batch(func(tx *bbolt.Tx) error {
err := c.batch(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP)
if bucket == nil {
return os.ErrNotExist
@@ -45,7 +45,7 @@ func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
}
func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
return c.batch(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
if err != nil {
return err
@@ -69,7 +69,7 @@ func (c *CacheFile) FakeIPSaveMetadataAsync(metadata *adapter.FakeIPMetadata) {
}
func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
return c.batch(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
if err != nil {
return err
@@ -136,7 +136,7 @@ func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) {
return cachedDomain, true
}
var domain string
_ = c.DB.View(func(tx *bbolt.Tx) error {
_ = c.view(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP)
if bucket == nil {
return nil
@@ -163,7 +163,7 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo
return cachedAddress, true
}
var address netip.Addr
_ = c.DB.View(func(tx *bbolt.Tx) error {
_ = c.view(func(tx *bbolt.Tx) error {
var bucket *bbolt.Bucket
if isIPv6 {
bucket = tx.Bucket(bucketFakeIPDomain6)
@@ -180,7 +180,7 @@ func (c *CacheFile) FakeIPLoadDomain(domain string, isIPv6 bool) (netip.Addr, bo
}
func (c *CacheFile) FakeIPReset() error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
return c.batch(func(tx *bbolt.Tx) error {
err := tx.DeleteBucket(bucketFakeIP)
if err != nil {
return err

View File

@@ -31,7 +31,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (
copy(key[2:], qName)
defer buf.Put(key)
var deleteCache bool
err := c.DB.View(func(tx *bbolt.Tx) error {
err := c.view(func(tx *bbolt.Tx) error {
bucket := c.bucket(tx, bucketRDRC)
if bucket == nil {
return nil
@@ -56,7 +56,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (
return
}
if deleteCache {
c.DB.Update(func(tx *bbolt.Tx) error {
c.update(func(tx *bbolt.Tx) error {
bucket := c.bucket(tx, bucketRDRC)
if bucket == nil {
return nil
@@ -72,7 +72,7 @@ func (c *CacheFile) LoadRDRC(transportName string, qName string, qType uint16) (
}
func (c *CacheFile) SaveRDRC(transportName string, qName string, qType uint16) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
return c.batch(func(tx *bbolt.Tx) error {
bucket, err := c.createBucket(tx, bucketRDRC)
if err != nil {
return err

View File

@@ -14,6 +14,7 @@ import (
func cacheRouter(ctx context.Context) http.Handler {
r := chi.NewRouter()
r.Post("/fakeip/flush", flushFakeip(ctx))
r.Post("/dns/flush", flushDNS(ctx))
return r
}
@@ -31,3 +32,13 @@ func flushFakeip(ctx context.Context) func(w http.ResponseWriter, r *http.Reques
render.NoContent(w, r)
}
}
func flushDNS(ctx context.Context) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
if dnsRouter != nil {
dnsRouter.ClearCache()
}
render.NoContent(w, r)
}
}

View File

@@ -2,6 +2,7 @@ package clashapi
import (
"bytes"
"context"
"net/http"
"strconv"
"time"
@@ -17,15 +18,15 @@ import (
"github.com/gofrs/uuid/v5"
)
func connectionRouter(router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
func connectionRouter(ctx context.Context, router adapter.Router, trafficManager *trafficontrol.Manager) http.Handler {
r := chi.NewRouter()
r.Get("/", getConnections(trafficManager))
r.Get("/", getConnections(ctx, trafficManager))
r.Delete("/", closeAllConnections(router, trafficManager))
r.Delete("/{id}", closeConnection(trafficManager))
return r
}
func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
func getConnections(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Upgrade") != "websocket" {
snapshot := trafficManager.Snapshot()
@@ -67,7 +68,12 @@ func getConnections(trafficManager *trafficontrol.Manager) func(w http.ResponseW
tick := time.NewTicker(time.Millisecond * time.Duration(interval))
defer tick.Stop()
for range tick.C {
for {
select {
case <-ctx.Done():
return
case <-tick.C:
}
if err = sendSnapshot(); err != nil {
break
}

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
@@ -115,12 +116,12 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
r.Use(authentication(options.Secret))
r.Get("/", hello(options.ExternalUI != ""))
r.Get("/logs", getLogs(logFactory))
r.Get("/traffic", traffic(trafficManager))
r.Get("/traffic", traffic(s.ctx, trafficManager))
r.Get("/version", version)
r.Mount("/configs", configRouter(s, logFactory))
r.Mount("/proxies", proxyRouter(s, s.router))
r.Mount("/rules", ruleRouter(s.router))
r.Mount("/connections", connectionRouter(s.router, trafficManager))
r.Mount("/connections", connectionRouter(s.ctx, s.router, trafficManager))
r.Mount("/providers/proxies", proxyProviderRouter())
r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter())
@@ -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)
@@ -305,7 +303,7 @@ type Traffic struct {
Down int64 `json:"down"`
}
func traffic(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
func traffic(ctx context.Context, trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var conn net.Conn
if r.Header.Get("Upgrade") == "websocket" {
@@ -326,7 +324,12 @@ func traffic(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter,
defer tick.Stop()
buf := &bytes.Buffer{}
uploadTotal, downloadTotal := trafficManager.Total()
for range tick.C {
for {
select {
case <-ctx.Done():
return
case <-tick.C:
}
buf.Reset()
uploadTotalNew, downloadTotalNew := trafficManager.Total()
err := json.NewEncoder(buf).Encode(Traffic{

View File

@@ -10,11 +10,31 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/json"
"github.com/sagernet/sing/common/observable"
"github.com/sagernet/sing/common/x/list"
"github.com/gofrs/uuid/v5"
)
type ConnectionEventType int
const (
ConnectionEventNew ConnectionEventType = iota
ConnectionEventUpdate
ConnectionEventClosed
)
type ConnectionEvent struct {
Type ConnectionEventType
ID uuid.UUID
Metadata *TrackerMetadata
UplinkDelta int64
DownlinkDelta int64
ClosedAt time.Time
}
const closedConnectionsLimit = 1000
type Manager struct {
uploadTotal atomic.Int64
downloadTotal atomic.Int64
@@ -22,29 +42,52 @@ type Manager struct {
connections compatible.Map[uuid.UUID, Tracker]
closedConnectionsAccess sync.Mutex
closedConnections list.List[TrackerMetadata]
// process *process.Process
memory uint64
memory uint64
eventSubscriber *observable.Subscriber[ConnectionEvent]
}
func NewManager() *Manager {
return &Manager{}
}
func (m *Manager) SetEventHook(subscriber *observable.Subscriber[ConnectionEvent]) {
m.eventSubscriber = subscriber
}
func (m *Manager) Join(c Tracker) {
m.connections.Store(c.Metadata().ID, c)
metadata := c.Metadata()
m.connections.Store(metadata.ID, c)
if m.eventSubscriber != nil {
m.eventSubscriber.Emit(ConnectionEvent{
Type: ConnectionEventNew,
ID: metadata.ID,
Metadata: metadata,
})
}
}
func (m *Manager) Leave(c Tracker) {
metadata := c.Metadata()
_, loaded := m.connections.LoadAndDelete(metadata.ID)
if loaded {
metadata.ClosedAt = time.Now()
closedAt := time.Now()
metadata.ClosedAt = closedAt
metadataCopy := *metadata
m.closedConnectionsAccess.Lock()
defer m.closedConnectionsAccess.Unlock()
if m.closedConnections.Len() >= 1000 {
if m.closedConnections.Len() >= closedConnectionsLimit {
m.closedConnections.PopFront()
}
m.closedConnections.PushBack(metadata)
m.closedConnections.PushBack(metadataCopy)
m.closedConnectionsAccess.Unlock()
if m.eventSubscriber != nil {
m.eventSubscriber.Emit(ConnectionEvent{
Type: ConnectionEventClosed,
ID: metadata.ID,
Metadata: &metadataCopy,
ClosedAt: closedAt,
})
}
}
}
@@ -64,8 +107,8 @@ func (m *Manager) ConnectionsLen() int {
return m.connections.Len()
}
func (m *Manager) Connections() []TrackerMetadata {
var connections []TrackerMetadata
func (m *Manager) Connections() []*TrackerMetadata {
var connections []*TrackerMetadata
m.connections.Range(func(_ uuid.UUID, value Tracker) bool {
connections = append(connections, value.Metadata())
return true
@@ -73,10 +116,18 @@ func (m *Manager) Connections() []TrackerMetadata {
return connections
}
func (m *Manager) ClosedConnections() []TrackerMetadata {
func (m *Manager) ClosedConnections() []*TrackerMetadata {
m.closedConnectionsAccess.Lock()
defer m.closedConnectionsAccess.Unlock()
return m.closedConnections.Array()
values := m.closedConnections.Array()
m.closedConnectionsAccess.Unlock()
if len(values) == 0 {
return nil
}
connections := make([]*TrackerMetadata, len(values))
for i := range values {
connections[i] = &values[i]
}
return connections
}
func (m *Manager) Connection(id uuid.UUID) Tracker {
@@ -124,7 +175,7 @@ func (s *Snapshot) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
"downloadTotal": s.Download,
"uploadTotal": s.Upload,
"connections": common.Map(s.Connections, func(t Tracker) TrackerMetadata { return t.Metadata() }),
"connections": common.Map(s.Connections, func(t Tracker) *TrackerMetadata { return t.Metadata() }),
"memory": s.Memory,
})
}

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, ")")
}
@@ -87,7 +87,7 @@ func (t TrackerMetadata) MarshalJSON() ([]byte, error) {
}
type Tracker interface {
Metadata() TrackerMetadata
Metadata() *TrackerMetadata
Close() error
}
@@ -97,8 +97,8 @@ type TCPConn struct {
manager *Manager
}
func (tt *TCPConn) Metadata() TrackerMetadata {
return tt.metadata
func (tt *TCPConn) Metadata() *TrackerMetadata {
return &tt.metadata
}
func (tt *TCPConn) Close() error {
@@ -178,8 +178,8 @@ type UDPConn struct {
manager *Manager
}
func (ut *UDPConn) Metadata() TrackerMetadata {
return ut.metadata
func (ut *UDPConn) Metadata() *TrackerMetadata {
return &ut.metadata
}
func (ut *UDPConn) Close() error {

View File

@@ -57,96 +57,6 @@ func (n Note) MessageWithLink() string {
}
}
var OptionBadMatchSource = Note{
Name: "bad-match-source",
Description: "legacy match source rule item",
DeprecatedVersion: "1.10.0",
ScheduledVersion: "1.11.0",
EnvName: "BAD_MATCH_SOURCE",
MigrationLink: "https://sing-box.sagernet.org/deprecated/#match-source-rule-items-are-renamed",
}
var OptionGEOIP = Note{
Name: "geoip",
Description: "geoip database",
DeprecatedVersion: "1.8.0",
ScheduledVersion: "1.12.0",
EnvName: "GEOIP",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-geoip-to-rule-sets",
}
var OptionGEOSITE = Note{
Name: "geosite",
Description: "geosite database",
DeprecatedVersion: "1.8.0",
ScheduledVersion: "1.12.0",
EnvName: "GEOSITE",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-geosite-to-rule-sets",
}
var OptionTUNAddressX = Note{
Name: "tun-address-x",
Description: "legacy tun address fields",
DeprecatedVersion: "1.10.0",
ScheduledVersion: "1.12.0",
EnvName: "TUN_ADDRESS_X",
MigrationLink: "https://sing-box.sagernet.org/migration/#tun-address-fields-are-merged",
}
var OptionSpecialOutbounds = Note{
Name: "special-outbounds",
Description: "legacy special outbounds",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.13.0",
EnvName: "SPECIAL_OUTBOUNDS",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions",
}
var OptionInboundOptions = Note{
Name: "inbound-options",
Description: "legacy inbound fields",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.13.0",
EnvName: "INBOUND_OPTIONS",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions",
}
var OptionDestinationOverrideFields = Note{
Name: "destination-override-fields",
Description: "destination override fields in direct outbound",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.13.0",
EnvName: "DESTINATION_OVERRIDE_FIELDS",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-destination-override-fields-to-route-options",
}
var OptionWireGuardOutbound = Note{
Name: "wireguard-outbound",
Description: "legacy wireguard outbound",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.13.0",
EnvName: "WIREGUARD_OUTBOUND",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint",
}
var OptionWireGuardGSO = Note{
Name: "wireguard-gso",
Description: "GSO option in wireguard outbound",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.13.0",
EnvName: "WIREGUARD_GSO",
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint",
}
var OptionTUNGSO = Note{
Name: "tun-gso",
Description: "GSO option in tun",
DeprecatedVersion: "1.11.0",
ScheduledVersion: "1.12.0",
EnvName: "TUN_GSO",
MigrationLink: "https://sing-box.sagernet.org/deprecated/#gso-option-in-tun",
}
var OptionLegacyDNSTransport = Note{
Name: "legacy-dns-transport",
Description: "legacy DNS servers",
@@ -183,15 +93,6 @@ var OptionMissingDomainResolver = Note{
MigrationLink: "https://sing-box.sagernet.org/migration/#migrate-outbound-dns-rule-items-to-domain-resolver",
}
var OptionLegacyECHOptions = Note{
Name: "legacy-ech-options",
Description: "legacy ECH options",
DeprecatedVersion: "1.12.0",
ScheduledVersion: "1.13.0",
EnvName: "LEGACY_ECH_OPTIONS",
MigrationLink: "https://sing-box.sagernet.org/deprecated/#legacy-ech-fields",
}
var OptionLegacyDomainStrategyOptions = Note{
Name: "legacy-domain-strategy-options",
Description: "legacy domain strategy options",
@@ -202,20 +103,9 @@ var OptionLegacyDomainStrategyOptions = Note{
}
var Options = []Note{
OptionBadMatchSource,
OptionGEOIP,
OptionGEOSITE,
OptionTUNAddressX,
OptionSpecialOutbounds,
OptionInboundOptions,
OptionDestinationOverrideFields,
OptionWireGuardOutbound,
OptionWireGuardGSO,
OptionTUNGSO,
OptionLegacyDNSTransport,
OptionLegacyDNSFakeIPOptions,
OptionOutboundDNSRuleItem,
OptionMissingDomainResolver,
OptionLegacyECHOptions,
OptionLegacyDomainStrategyOptions,
}

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,41 +1,80 @@
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
standalone bool
}
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)
UpdateClashMode(newMode string)
WriteConnections(message *Connections)
WriteConnectionEvents(events *ConnectionEvents)
}
type LogEntry struct {
Level int32
Message string
}
type LogIterator interface {
Len() int32
HasNext() bool
Next() *LogEntry
}
type XPCDialer interface {
DialXPC() (int32, error)
}
var sXPCDialer XPCDialer
func SetXPCDialer(dialer XPCDialer) {
sXPCDialer = dialer
}
func NewStandaloneCommandClient() *CommandClient {
return new(CommandClient)
return &CommandClient{standalone: true}
}
func NewCommandClient(handler CommandClientHandler, options *CommandClientOptions) *CommandClient {
@@ -45,106 +84,495 @@ 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) {
var (
conn net.Conn
err error
)
for i := 0; i < 10; i++ {
conn, err = c.directConnect()
if err == nil {
return conn, nil
}
time.Sleep(time.Duration(100+i*50) * time.Millisecond)
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 nil, err
return streamer(ctx, desc, cc, method, opts...)
}
const (
commandClientDialAttempts = 10
commandClientDialBaseDelay = 100 * time.Millisecond
commandClientDialStepDelay = 50 * time.Millisecond
)
func commandClientDialDelay(attempt int) time.Duration {
return commandClientDialBaseDelay + time.Duration(attempt)*commandClientDialStepDelay
}
func dialTarget() (string, func(context.Context, string) (net.Conn, error)) {
if sXPCDialer != nil {
return "passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) {
fileDescriptor, err := sXPCDialer.DialXPC()
if err != nil {
return nil, err
}
return networkConnectionFromFileDescriptor(fileDescriptor)
}
}
if sCommandServerListenPort == 0 {
socketPath := filepath.Join(sBasePath, "command.sock")
return "passthrough:///command-socket", func(ctx context.Context, _ string) (net.Conn, error) {
var networkDialer net.Dialer
return networkDialer.DialContext(ctx, "unix", socketPath)
}
}
return net.JoinHostPort("127.0.0.1", strconv.Itoa(int(sCommandServerListenPort))), nil
}
func networkConnectionFromFileDescriptor(fileDescriptor int32) (net.Conn, error) {
file := os.NewFile(uintptr(fileDescriptor), "xpc-command-socket")
if file == nil {
return nil, E.New("invalid file descriptor")
}
networkConnection, err := net.FileConn(file)
if err != nil {
file.Close()
return nil, E.Cause(err, "create connection from fd")
}
file.Close()
return networkConnection, nil
}
func (c *CommandClient) dialWithRetry(target string, contextDialer func(context.Context, string) (net.Conn, error), retryDial bool) (*grpc.ClientConn, daemon.StartedServiceClient, error) {
var connection *grpc.ClientConn
var client daemon.StartedServiceClient
var lastError error
for attempt := 0; attempt < commandClientDialAttempts; attempt++ {
if connection == nil {
options := []grpc.DialOption{
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(unaryClientAuthInterceptor),
grpc.WithStreamInterceptor(streamClientAuthInterceptor),
}
if contextDialer != nil {
options = append(options, grpc.WithContextDialer(contextDialer))
}
var err error
connection, err = grpc.NewClient(target, options...)
if err != nil {
lastError = err
if !retryDial {
return nil, nil, err
}
time.Sleep(commandClientDialDelay(attempt))
continue
}
client = daemon.NewStartedServiceClient(connection)
}
waitDuration := commandClientDialDelay(attempt)
ctx, cancel := context.WithTimeout(context.Background(), waitDuration)
_, err := client.GetStartedAt(ctx, &emptypb.Empty{}, grpc.WaitForReady(true))
cancel()
if err == nil {
return connection, client, nil
}
lastError = err
}
if connection != nil {
connection.Close()
}
return nil, nil, lastError
}
func (c *CommandClient) Connect() error {
common.Close(c.conn)
conn, err := c.directConnectWithRetry()
c.clientMutex.Lock()
common.Close(common.PtrOrNil(c.grpcConn))
target, contextDialer := dialTarget()
connection, client, err := c.dialWithRetry(target, contextDialer, true)
if err != nil {
c.clientMutex.Unlock()
return err
}
c.conn = conn
err = binary.Write(conn, binary.BigEndian, uint8(c.options.Command))
c.grpcConn = connection
c.grpcClient = client
c.ctx, c.cancel = context.WithCancel(context.Background())
c.clientMutex.Unlock()
c.handler.Connected()
return c.dispatchCommands()
}
func (c *CommandClient) ConnectWithFD(fd int32) error {
c.clientMutex.Lock()
common.Close(common.PtrOrNil(c.grpcConn))
networkConnection, err := networkConnectionFromFileDescriptor(fd)
if err != nil {
c.clientMutex.Unlock()
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")
connection, client, err := c.dialWithRetry("passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) {
return networkConnection, nil
}, false)
if err != nil {
networkConnection.Close()
c.clientMutex.Unlock()
return err
}
c.grpcConn = connection
c.grpcClient = client
c.ctx, c.cancel = context.WithCancel(context.Background())
c.clientMutex.Unlock()
c.handler.Connected()
return c.dispatchCommands()
}
func (c *CommandClient) dispatchCommands() error {
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
}
target, contextDialer := dialTarget()
connection, client, err := c.dialWithRetry(target, contextDialer, true)
if err != nil {
return nil, err
}
c.grpcConn = connection
c.grpcClient = client
if c.ctx == nil {
c.ctx, c.cancel = context.WithCancel(context.Background())
}
return c.grpcClient, nil
}
func (c *CommandClient) closeConnection() {
c.clientMutex.Lock()
defer c.clientMutex.Unlock()
if c.grpcConn != nil {
c.grpcConn.Close()
c.grpcConn = nil
c.grpcClient = nil
}
}
func callWithResult[T any](c *CommandClient, call func(client daemon.StartedServiceClient) (T, error)) (T, error) {
client, err := c.getClientForCall()
if err != nil {
var zero T
return zero, err
}
if c.standalone {
defer c.closeConnection()
}
return call(client)
}
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
}
for {
events, err := stream.Recv()
if err != nil {
c.handler.Disconnected(err.Error())
return
}
libboxEvents := connectionEventsFromGRPC(events)
c.handler.WriteConnectionEvents(libboxEvents)
}
}
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{
GroupTag: groupTag,
OutboundTag: outboundTag,
})
})
return err
}
func (c *CommandClient) URLTest(groupTag string) error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.URLTest(context.Background(), &daemon.URLTestRequest{
OutboundTag: groupTag,
})
})
return err
}
func (c *CommandClient) SetClashMode(newMode string) error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.SetClashMode(context.Background(), &daemon.ClashMode{
Mode: newMode,
})
})
return err
}
func (c *CommandClient) CloseConnection(connId string) error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.CloseConnection(context.Background(), &daemon.CloseConnectionRequest{
Id: connId,
})
})
return err
}
func (c *CommandClient) CloseConnections() error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.CloseAllConnections(context.Background(), &emptypb.Empty{})
})
return err
}
func (c *CommandClient) ServiceReload() error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.ReloadService(context.Background(), &emptypb.Empty{})
})
return err
}
func (c *CommandClient) ServiceClose() error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.StopService(context.Background(), &emptypb.Empty{})
})
return err
}
func (c *CommandClient) ClearLogs() error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.ClearLogs(context.Background(), &emptypb.Empty{})
})
return err
}
func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) {
return callWithResult(c, func(client daemon.StartedServiceClient) (*SystemProxyStatus, error) {
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 {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return client.SetSystemProxyEnabled(context.Background(), &daemon.SetSystemProxyEnabledRequest{
Enabled: isEnabled,
})
})
return err
}
func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) {
return callWithResult(c, func(client daemon.StartedServiceClient) (DeprecatedNoteIterator, error) {
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) GetStartedAt() (int64, error) {
return callWithResult(c, func(client daemon.StartedServiceClient) (int64, error) {
startedAt, err := client.GetStartedAt(context.Background(), &emptypb.Empty{})
if err != nil {
return 0, err
}
return startedAt.StartedAt, nil
})
}
func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
return 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,276 @@
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,
OOMKiller: memoryLimitEnabled,
// 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)
s.StartedService.Close()
}
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().Network().NeedWIFIState()
}
func (s *CommandServer) NeedFindProcess() bool {
instance := s.StartedService.Instance()
if instance == nil || instance.Box() == nil {
return false
}
return instance.Box().Router().NeedFindProcess()
}
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,426 @@
package libbox
import (
"slices"
"strings"
"time"
"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
)
const (
ConnectionEventNew = iota
ConnectionEventUpdate
ConnectionEventClosed
)
const (
closedConnectionMaxAge = int64((5 * time.Minute) / time.Millisecond)
)
type ConnectionEvent struct {
Type int32
ID string
Connection *Connection
UplinkDelta int64
DownlinkDelta int64
ClosedAt int64
}
type ConnectionEvents struct {
Reset bool
events []*ConnectionEvent
}
func (c *ConnectionEvents) Iterator() ConnectionEventIterator {
return newIterator(c.events)
}
type ConnectionEventIterator interface {
Next() *ConnectionEvent
HasNext() bool
}
type Connections struct {
connectionMap map[string]*Connection
input []Connection
filtered []Connection
filterState int32
filterApplied bool
}
func NewConnections() *Connections {
return &Connections{
connectionMap: make(map[string]*Connection),
}
}
func (c *Connections) ApplyEvents(events *ConnectionEvents) {
if events == nil {
return
}
if events.Reset {
c.connectionMap = make(map[string]*Connection)
}
for _, event := range events.events {
switch event.Type {
case ConnectionEventNew:
if event.Connection != nil {
conn := *event.Connection
c.connectionMap[event.ID] = &conn
}
case ConnectionEventUpdate:
if conn, ok := c.connectionMap[event.ID]; ok {
conn.Uplink = event.UplinkDelta
conn.Downlink = event.DownlinkDelta
conn.UplinkTotal += event.UplinkDelta
conn.DownlinkTotal += event.DownlinkDelta
}
case ConnectionEventClosed:
if event.Connection != nil {
conn := *event.Connection
conn.ClosedAt = event.ClosedAt
conn.Uplink = 0
conn.Downlink = 0
c.connectionMap[event.ID] = &conn
continue
}
if conn, ok := c.connectionMap[event.ID]; ok {
conn.ClosedAt = event.ClosedAt
conn.Uplink = 0
conn.Downlink = 0
}
}
}
c.evictClosedConnections(time.Now().UnixMilli())
c.input = c.input[:0]
for _, conn := range c.connectionMap {
c.input = append(c.input, *conn)
}
if c.filterApplied {
c.FilterState(c.filterState)
} else {
c.filtered = c.filtered[:0]
c.filtered = append(c.filtered, c.input...)
}
}
func (c *Connections) evictClosedConnections(nowMilliseconds int64) {
for id, conn := range c.connectionMap {
if conn.ClosedAt == 0 {
continue
}
if nowMilliseconds-conn.ClosedAt > closedConnectionMaxAge {
delete(c.connectionMap, id)
}
}
}
func (c *Connections) FilterState(state int32) {
c.filterApplied = true
c.filterState = state
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 ProcessInfo struct {
ProcessID int64
UserID int32
UserName string
ProcessPath string
PackageName string
}
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
ProcessInfo *ProcessInfo
}
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 {
var processInfo *ProcessInfo
if conn.ProcessInfo != nil {
processInfo = &ProcessInfo{
ProcessID: int64(conn.ProcessInfo.ProcessId),
UserID: conn.ProcessInfo.UserId,
UserName: conn.ProcessInfo.UserName,
ProcessPath: conn.ProcessInfo.ProcessPath,
PackageName: conn.ProcessInfo.PackageName,
}
}
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,
ProcessInfo: processInfo,
}
}
func connectionEventFromGRPC(event *daemon.ConnectionEvent) *ConnectionEvent {
if event == nil {
return nil
}
libboxEvent := &ConnectionEvent{
Type: int32(event.Type),
ID: event.Id,
UplinkDelta: event.UplinkDelta,
DownlinkDelta: event.DownlinkDelta,
ClosedAt: event.ClosedAt,
}
if event.Connection != nil {
conn := connectionFromGRPC(event.Connection)
libboxEvent.Connection = &conn
}
return libboxEvent
}
func connectionEventsFromGRPC(events *daemon.ConnectionEvents) *ConnectionEvents {
if events == nil {
return nil
}
libboxEvents := &ConnectionEvents{
Reset: events.Reset_,
}
for _, event := range events.Events {
if libboxEvent := connectionEventFromGRPC(event); libboxEvent != nil {
libboxEvents.events = append(libboxEvents.events, libboxEvent)
}
}
return libboxEvents
}
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"
@@ -25,7 +22,7 @@ import (
"github.com/sagernet/sing/service/filemanager"
)
func BaseContext(platformInterface PlatformInterface) context.Context {
func baseContext(platformInterface PlatformInterface) context.Context {
dnsRegistry := include.DNSTransportRegistry()
if platformInterface != nil {
if localTransport := platformInterface.LocalDNSTransport(); localTransport != nil {
@@ -48,14 +45,14 @@ func parseConfig(ctx context.Context, configContent string) (option.Options, err
}
func CheckConfig(configContent string) error {
ctx := BaseContext(nil)
ctx := baseContext(nil)
options, err := parseConfig(ctx, configContent)
if err != nil {
return err
}
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,13 +105,21 @@ 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) RequestPermissionForWIFIState() error {
return nil
}
func (s *platformInterfaceStub) UsePlatformWIFIMonitor() bool {
return false
}
func (s *platformInterfaceStub) ReadWIFIState() adapter.WIFIState {
return adapter.WIFIState{}
}
@@ -115,11 +128,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
}
@@ -160,7 +189,7 @@ func (s *interfaceMonitorStub) MyInterface() string {
}
func FormatConfig(configContent string) (*StringBox, error) {
options, err := parseConfig(BaseContext(nil), configContent)
options, err := parseConfig(baseContext(nil), configContent)
if err != nil {
return nil, err
}

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

@@ -46,6 +46,9 @@ func (p *platformTransport) Close() error {
return nil
}
func (p *platformTransport) Reset() {
}
func (p *platformTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
response := &ExchangeContext{
context: ctx,

View File

@@ -0,0 +1,257 @@
{
"version": 1,
"variables": {
"VERSION": "$(go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)",
"WORKSPACE_ROOT": "../../..",
"DEPLOY_ANDROID": "${WORKSPACE_ROOT}/sing-box-for-android/app/libs",
"DEPLOY_APPLE": "${WORKSPACE_ROOT}/sing-box-for-apple",
"DEPLOY_WINDOWS": "${WORKSPACE_ROOT}/sing-box-for-windows/local-packages"
},
"packages": [
{
"id": "libbox",
"path": ".",
"java_package": "io.nekohasekai.libbox",
"csharp_namespace": "SagerNet",
"csharp_entrypoint": "Libbox",
"apple_prefix": "Libbox"
}
],
"builds": [
{
"id": "android-main",
"packages": ["libbox"],
"default": {
"tags": [
"with_gvisor",
"with_quic",
"with_wireguard",
"with_utls",
"with_naive_outbound",
"with_clash_api",
"badlinkname",
"tfogo_checklinkname0",
"with_tailscale",
"ts_omit_logtail",
"ts_omit_ssh",
"ts_omit_drive",
"ts_omit_taildrop",
"ts_omit_webclient",
"ts_omit_doctor",
"ts_omit_capture",
"ts_omit_kube",
"ts_omit_aws",
"ts_omit_synology",
"ts_omit_bird"
],
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0",
"trimpath": true
}
},
{
"id": "android-legacy",
"packages": ["libbox"],
"default": {
"tags": [
"with_gvisor",
"with_quic",
"with_wireguard",
"with_utls",
"with_clash_api",
"badlinkname",
"tfogo_checklinkname0",
"with_tailscale",
"ts_omit_logtail",
"ts_omit_ssh",
"ts_omit_drive",
"ts_omit_taildrop",
"ts_omit_webclient",
"ts_omit_doctor",
"ts_omit_capture",
"ts_omit_kube",
"ts_omit_aws",
"ts_omit_synology",
"ts_omit_bird"
],
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0",
"trimpath": true
}
},
{
"id": "apple",
"packages": ["libbox"],
"default": {
"tags": [
"with_gvisor",
"with_quic",
"with_wireguard",
"with_utls",
"with_naive_outbound",
"with_clash_api",
"badlinkname",
"tfogo_checklinkname0",
"with_dhcp",
"grpcnotrace",
"with_tailscale",
"ts_omit_logtail",
"ts_omit_ssh",
"ts_omit_drive",
"ts_omit_taildrop",
"ts_omit_webclient",
"ts_omit_doctor",
"ts_omit_capture",
"ts_omit_kube",
"ts_omit_aws",
"ts_omit_synology",
"ts_omit_bird"
],
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0",
"trimpath": true
},
"overrides": [
{
"match": { "os": "ios" },
"tags_append": ["with_low_memory"]
},
{
"match": { "os": "tvos" },
"tags_append": ["with_low_memory"]
}
]
},
{
"id": "windows",
"packages": ["libbox"],
"default": {
"tags": [
"with_gvisor",
"with_quic",
"with_wireguard",
"with_utls",
"with_naive_outbound",
"with_purego",
"with_clash_api",
"badlinkname",
"tfogo_checklinkname0",
"with_tailscale",
"ts_omit_logtail",
"ts_omit_ssh",
"ts_omit_drive",
"ts_omit_taildrop",
"ts_omit_webclient",
"ts_omit_doctor",
"ts_omit_capture",
"ts_omit_kube",
"ts_omit_aws",
"ts_omit_synology",
"ts_omit_bird"
],
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0",
"trimpath": true
}
}
],
"platforms": [
{
"type": "android",
"build": "android-main",
"min_sdk": 23,
"ndk_version": "28.0.13004108",
"lib_name": "box",
"languages": [{ "type": "java" }],
"artifacts": [
{
"type": "aar",
"output_path": "libbox.aar",
"execute_after": [
"if [ -d \"${DEPLOY_ANDROID}\" ]; then",
" rm -f \"${DEPLOY_ANDROID}/$$(basename \"${OUTPUT_PATH}\")\"",
" mv \"${OUTPUT_PATH}\" \"${DEPLOY_ANDROID}/\"",
"fi"
]
}
]
},
{
"type": "android",
"build": "android-legacy",
"min_sdk": 21,
"ndk_version": "28.0.13004108",
"lib_name": "box",
"languages": [{ "type": "java" }],
"artifacts": [
{
"type": "aar",
"output_path": "libbox-legacy.aar",
"execute_after": [
"if [ -d \"${DEPLOY_ANDROID}\" ]; then",
" rm -f \"${DEPLOY_ANDROID}/$$(basename \"${OUTPUT_PATH}\")\"",
" mv \"${OUTPUT_PATH}\" \"${DEPLOY_ANDROID}/\"",
"fi"
]
}
]
},
{
"type": "apple",
"build": "apple",
"targets": [
"ios/arm64",
"ios/simulator/arm64",
"ios/simulator/amd64",
"tvos/arm64",
"tvos/simulator/arm64",
"tvos/simulator/amd64",
"macos/arm64",
"macos/amd64"
],
"languages": [{ "type": "objc" }],
"artifacts": [
{
"type": "xcframework",
"module_name": "Libbox",
"execute_after": [
"if [ -d \"${DEPLOY_APPLE}\" ]; then",
" rm -rf \"${DEPLOY_APPLE}/${MODULE_NAME}.xcframework\"",
" mv \"${OUTPUT_PATH}\" \"${DEPLOY_APPLE}/\"",
"fi"
]
}
]
},
{
"type": "csharp",
"build": "windows",
"targets": [
"windows/amd64"
],
"languages": [{ "type": "csharp" }],
"artifacts": [
{
"type": "nuget",
"package_id": "SagerNet.Libbox",
"package_version": "0.0.0-local",
"execute_after": {
"windows": [
"$$deployPath = '${DEPLOY_WINDOWS}'",
"if (Test-Path $$deployPath) {",
" Remove-Item \"$$deployPath\\${PACKAGE_ID}.*.nupkg\" -ErrorAction SilentlyContinue",
" Move-Item -Force '${OUTPUT_PATH}' \"$$deployPath\\\"",
" $$cachePath = if ($$env:NUGET_PACKAGES) { $$env:NUGET_PACKAGES } else { \"$$env:USERPROFILE\\.nuget\\packages\" }",
" Remove-Item -Recurse -Force \"$$cachePath\\sagernet.libbox\\${PACKAGE_VERSION}\" -ErrorAction SilentlyContinue",
"}"
],
"default": [
"if [ -d \"${DEPLOY_WINDOWS}\" ]; then",
" rm -f \"${DEPLOY_WINDOWS}/${PACKAGE_ID}.*.nupkg\"",
" mv \"${OUTPUT_PATH}\" \"${DEPLOY_WINDOWS}/\"",
" cache_path=\"$${NUGET_PACKAGES:-$${HOME}/.nuget/packages}\"",
" rm -rf \"$${cache_path}/sagernet.libbox/${PACKAGE_VERSION}\"",
"fi"
]
}
}
]
}
]
}

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

@@ -5,11 +5,10 @@ package libbox
import (
"os"
"runtime"
"golang.org/x/sys/unix"
"runtime/debug"
)
var stderrFile *os.File
var crashOutputFile *os.File
func RedirectStderr(path string) error {
if stats, err := os.Stat(path); err == nil && stats.Size() > 0 {
@@ -27,12 +26,12 @@ func RedirectStderr(path string) error {
return err
}
}
err = unix.Dup2(int(outputFile.Fd()), int(os.Stderr.Fd()))
err = debug.SetCrashOutput(outputFile, debug.CrashOptions{})
if err != nil {
outputFile.Close()
os.Remove(outputFile.Name())
return err
}
stderrFile = outputFile
crashOutputFile = outputFile
return nil
}

View File

@@ -4,20 +4,23 @@ import (
"math"
runtimeDebug "runtime/debug"
"github.com/sagernet/sing-box/common/conntrack"
C "github.com/sagernet/sing-box/constant"
)
var memoryLimitEnabled bool
func SetMemoryLimit(enabled bool) {
const memoryLimit = 45 * 1024 * 1024
const memoryLimitGo = memoryLimit / 1.5
memoryLimitEnabled = enabled
const memoryLimitGo = 45 * 1024 * 1024
if enabled {
runtimeDebug.SetGCPercent(10)
runtimeDebug.SetMemoryLimit(memoryLimitGo)
conntrack.KillerEnabled = true
conntrack.MemoryLimit = memoryLimit
if C.IsIos {
runtimeDebug.SetMemoryLimit(memoryLimitGo)
}
} else {
runtimeDebug.SetGCPercent(100)
runtimeDebug.SetMemoryLimit(math.MaxInt64)
conntrack.KillerEnabled = false
if C.IsIos {
runtimeDebug.SetMemoryLimit(math.MaxInt64)
}
}
}

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,11 +10,8 @@ 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)
UIDByPackageName(packageName string) (int32, error)
FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error)
StartDefaultInterfaceMonitor(listener InterfaceUpdateListener) error
CloseDefaultInterfaceMonitor(listener InterfaceUpdateListener) error
GetInterfaces() (NetworkInterfaceIterator, error)
@@ -26,9 +23,11 @@ type PlatformInterface interface {
SendNotification(notification *Notification) error
}
type TunInterface interface {
FileDescriptor() int32
Close() error
type ConnectionOwner struct {
UserId int32
UserName string
ProcessPath string
AndroidPackageName string
}
type InterfaceUpdateListener interface {

View File

@@ -1,35 +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()
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

@@ -5,6 +5,7 @@ import (
"bytes"
"compress/gzip"
"encoding/binary"
"io"
E "github.com/sagernet/sing/common/exceptions"
"github.com/sagernet/sing/common/varbin"
@@ -35,7 +36,7 @@ type ErrorMessage struct {
func (e *ErrorMessage) Encode() []byte {
var buffer bytes.Buffer
buffer.WriteByte(MessageTypeError)
varbin.Write(&buffer, binary.BigEndian, e.Message)
writeString(&buffer, e.Message)
return buffer.Bytes()
}
@@ -49,7 +50,7 @@ func DecodeErrorMessage(data []byte) (*ErrorMessage, error) {
return nil, E.New("invalid message")
}
var message ErrorMessage
message.Message, err = varbin.ReadValue[string](reader, binary.BigEndian)
message.Message, err = readString(reader)
if err != nil {
return nil, err
}
@@ -87,7 +88,7 @@ func (e *ProfileEncoder) Encode() []byte {
binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles)))
for _, preview := range e.profiles {
binary.Write(&buffer, binary.BigEndian, preview.ProfileID)
varbin.Write(&buffer, binary.BigEndian, preview.Name)
writeString(&buffer, preview.Name)
binary.Write(&buffer, binary.BigEndian, preview.Type)
}
return buffer.Bytes()
@@ -117,7 +118,7 @@ func (d *ProfileDecoder) Decode(data []byte) error {
if err != nil {
return err
}
profile.Name, err = varbin.ReadValue[string](reader, binary.BigEndian)
profile.Name, err = readString(reader)
if err != nil {
return err
}
@@ -178,11 +179,11 @@ func (c *ProfileContent) Encode() []byte {
buffer.WriteByte(1)
gWriter := gzip.NewWriter(buffer)
writer := bufio.NewWriter(gWriter)
varbin.Write(writer, binary.BigEndian, c.Name)
writeStringBuffered(writer, c.Name)
binary.Write(writer, binary.BigEndian, c.Type)
varbin.Write(writer, binary.BigEndian, c.Config)
writeStringBuffered(writer, c.Config)
if c.Type != ProfileTypeLocal {
varbin.Write(writer, binary.BigEndian, c.RemotePath)
writeStringBuffered(writer, c.RemotePath)
}
if c.Type == ProfileTypeRemote {
binary.Write(writer, binary.BigEndian, c.AutoUpdate)
@@ -214,7 +215,7 @@ func DecodeProfileContent(data []byte) (*ProfileContent, error) {
}
bReader := varbin.StubReader(gReader)
var content ProfileContent
content.Name, err = varbin.ReadValue[string](bReader, binary.BigEndian)
content.Name, err = readString(bReader)
if err != nil {
return nil, err
}
@@ -222,12 +223,12 @@ func DecodeProfileContent(data []byte) (*ProfileContent, error) {
if err != nil {
return nil, err
}
content.Config, err = varbin.ReadValue[string](bReader, binary.BigEndian)
content.Config, err = readString(bReader)
if err != nil {
return nil, err
}
if content.Type != ProfileTypeLocal {
content.RemotePath, err = varbin.ReadValue[string](bReader, binary.BigEndian)
content.RemotePath, err = readString(bReader)
if err != nil {
return nil, err
}
@@ -250,3 +251,28 @@ func DecodeProfileContent(data []byte) (*ProfileContent, error) {
}
return &content, nil
}
func readString(reader io.ByteReader) (string, error) {
length, err := binary.ReadUvarint(reader)
if err != nil {
return "", err
}
buf := make([]byte, length)
for i := range buf {
buf[i], err = reader.ReadByte()
if err != nil {
return "", err
}
}
return string(buf), nil
}
func writeString(buffer *bytes.Buffer, value string) {
varbin.WriteUvarint(buffer, uint64(len(value)))
buffer.WriteString(value)
}
func writeStringBuffered(writer *bufio.Writer, value string) {
varbin.WriteUvarint(writer, uint64(len(value)))
writer.WriteString(value)
}

View File

@@ -0,0 +1,27 @@
package libbox
import (
"strings"
"golang.org/x/mod/semver"
)
func CompareSemver(left string, right string) bool {
normalizedLeft := normalizeSemver(left)
if !semver.IsValid(normalizedLeft) {
return false
}
normalizedRight := normalizeSemver(right)
if !semver.IsValid(normalizedRight) {
return false
}
return semver.Compare(normalizedLeft, normalizedRight) > 0
}
func normalizeSemver(version string) string {
trimmedVersion := strings.TrimSpace(version)
if strings.HasPrefix(trimmedVersion, "v") {
return trimmedVersion
}
return "v" + trimmedVersion
}

View File

@@ -0,0 +1,16 @@
package libbox
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCompareSemver(t *testing.T) {
t.Parallel()
require.False(t, CompareSemver("1.13.0-rc.4", "1.13.0"))
require.True(t, CompareSemver("1.13.1", "1.13.0"))
require.False(t, CompareSemver("v1.13.0", "1.13.0"))
require.False(t, CompareSemver("1.13.0-", "1.13.0"))
}

View File

@@ -1,127 +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")
}
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
}
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.Router().NeedWIFIState()
}
var (
_ platform.Interface = (*platformInterfaceWrapper)(nil)
_ log.PlatformWriter = (*platformInterfaceWrapper)(nil)
)
var _ adapter.PlatformInterface = (*platformInterfaceWrapper)(nil)
type platformInterfaceWrapper struct {
iif PlatformInterface
@@ -147,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")
}
@@ -176,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,
@@ -183,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
@@ -213,6 +126,9 @@ func (w *platformInterfaceWrapper) Interfaces() ([]adapter.NetworkInterface, err
Constrained: isDefault && w.isConstrained,
})
}
interfaces = common.UniqBy(interfaces, func(it adapter.NetworkInterface) string {
return it.Name
})
return interfaces, nil
}
@@ -220,7 +136,7 @@ func (w *platformInterfaceWrapper) UnderNetworkExtension() bool {
return w.iif.UnderNetworkExtension()
}
func (w *platformInterfaceWrapper) IncludeAllNetworks() bool {
func (w *platformInterfaceWrapper) NetworkExtensionIncludeAllNetworks() bool {
return w.iif.IncludeAllNetworks()
}
@@ -228,6 +144,14 @@ func (w *platformInterfaceWrapper) ClearDNSCache() {
w.iif.ClearDNSCache()
}
func (w *platformInterfaceWrapper) RequestPermissionForWIFIState() error {
return nil
}
func (w *platformInterfaceWrapper) UsePlatformWIFIMonitor() bool {
return true
}
func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
wifiState := w.iif.ReadWIFIState()
if wifiState == nil {
@@ -240,41 +164,84 @@ 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) {
var uid int32
func (w *platformInterfaceWrapper) UsePlatformConnectionOwnerFinder() bool {
return true
}
func (w *platformInterfaceWrapper) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) {
if w.useProcFS {
uid = procfs.ResolveSocketByProcSearch(network, source, destination)
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()))
if err != nil {
return nil, err
}
return &adapter.ConnectionOwner{
UserId: uid,
}, nil
}
packageName, _ := w.iif.PackageNameByUid(uid)
return &process.Info{UserId: uid, PackageName: packageName}, nil
result, err := w.iif.FindConnectionOwner(request.IpProtocol, request.SourceAddress, request.SourcePort, request.DestinationAddress, request.DestinationPort)
if err != nil {
return nil, err
}
return &adapter.ConnectionOwner{
UserId: result.UserId,
UserName: result.UserName,
ProcessPath: result.ProcessPath,
AndroidPackageName: result.AndroidPackageName,
}, 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
}
@@ -75,11 +71,11 @@ func Version() string {
}
func FormatBytes(length int64) string {
return byteformats.FormatBytes(uint64(length))
return byteformats.FormatKBytes(uint64(length))
}
func FormatMemoryBytes(length int64) string {
return byteformats.FormatMemoryBytes(uint64(length))
return byteformats.FormatMemoryKBytes(uint64(length))
}
func FormatDuration(duration int64) string {

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"
)

View File

@@ -84,15 +84,15 @@ type StatsServiceServer interface {
type UnimplementedStatsServiceServer struct{}
func (UnimplementedStatsServiceServer) GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented")
return nil, status.Error(codes.Unimplemented, "method GetStats not implemented")
}
func (UnimplementedStatsServiceServer) QueryStats(context.Context, *QueryStatsRequest) (*QueryStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method QueryStats not implemented")
return nil, status.Error(codes.Unimplemented, "method QueryStats not implemented")
}
func (UnimplementedStatsServiceServer) GetSysStats(context.Context, *SysStatsRequest) (*SysStatsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSysStats not implemented")
return nil, status.Error(codes.Unimplemented, "method GetSysStats not implemented")
}
func (UnimplementedStatsServiceServer) mustEmbedUnimplementedStatsServiceServer() {}
func (UnimplementedStatsServiceServer) testEmbeddedByValue() {}
@@ -105,7 +105,7 @@ type UnsafeStatsServiceServer interface {
}
func RegisterStatsServiceServer(s grpc.ServiceRegistrar, srv StatsServiceServer) {
// If the following call pancis, it indicates UnimplementedStatsServiceServer was
// If the following call panics, it indicates UnimplementedStatsServiceServer 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.