mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-22 00:21:30 +00:00
platform: Add group interface
This commit is contained in:
parent
5ad0ea2b5a
commit
9c8565cf21
4
Makefile
4
Makefile
|
@ -89,8 +89,8 @@ lib:
|
|||
|
||||
lib_install:
|
||||
go get -v -d
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230413023804-244d7ff07035
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230413023804-244d7ff07035
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230701084532-493ee2e45182
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230701084532-493ee2e45182
|
||||
|
||||
clean:
|
||||
rm -rf bin dist sing-box
|
||||
|
|
|
@ -31,6 +31,7 @@ type Tracker interface {
|
|||
}
|
||||
|
||||
type OutboundGroup interface {
|
||||
Outbound
|
||||
Now() string
|
||||
All() []string
|
||||
}
|
||||
|
|
|
@ -4,12 +4,6 @@ type PreStarter interface {
|
|||
PreStart() error
|
||||
}
|
||||
|
||||
func PreStart(starter any) error {
|
||||
if preService, ok := starter.(PreStarter); ok {
|
||||
err := preService.PreStart()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
type PostStarter interface {
|
||||
PostStart() error
|
||||
}
|
||||
|
|
25
box.go
25
box.go
|
@ -211,10 +211,12 @@ func (s *Box) Start() error {
|
|||
|
||||
func (s *Box) preStart() error {
|
||||
for serviceName, service := range s.preServices {
|
||||
s.logger.Trace("pre-start ", serviceName)
|
||||
err := adapter.PreStart(service)
|
||||
if err != nil {
|
||||
return E.Cause(err, "pre-starting ", serviceName)
|
||||
if preService, isPreService := service.(adapter.PreStarter); isPreService {
|
||||
s.logger.Trace("pre-start ", serviceName)
|
||||
err := preService.PreStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "pre-starting ", serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
err := s.startOutbounds()
|
||||
|
@ -249,13 +251,26 @@ func (s *Box) start() error {
|
|||
return E.Cause(err, "initialize inbound/", in.Type(), "[", tag, "]")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Box) postStart() error {
|
||||
for serviceName, service := range s.postServices {
|
||||
s.logger.Trace("starting ", service)
|
||||
err = service.Start()
|
||||
err := service.Start()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start ", serviceName)
|
||||
}
|
||||
}
|
||||
for serviceName, service := range s.outbounds {
|
||||
if lateService, isLateService := service.(adapter.PostStarter); isLateService {
|
||||
s.logger.Trace("post-starting ", service)
|
||||
err := lateService.PostStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "post-start ", serviceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -133,7 +133,7 @@ func buildiOS() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
copyPath := filepath.Join("..", "sing-box-for-ios")
|
||||
copyPath := filepath.Join("..", "sing-box-for-apple")
|
||||
if rw.FileExists(copyPath) {
|
||||
targetDir := filepath.Join(copyPath, "Libbox.xcframework")
|
||||
targetDir, _ = filepath.Abs(targetDir)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
type History struct {
|
||||
|
@ -20,6 +21,7 @@ type History struct {
|
|||
type HistoryStorage struct {
|
||||
access sync.RWMutex
|
||||
delayHistory map[string]*History
|
||||
callbacks list.List[func()]
|
||||
}
|
||||
|
||||
func NewHistoryStorage() *HistoryStorage {
|
||||
|
@ -28,6 +30,18 @@ func NewHistoryStorage() *HistoryStorage {
|
|||
}
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) AddListener(listener func()) *list.Element[func()] {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
return s.callbacks.PushBack(listener)
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) RemoveListener(element *list.Element[func()]) {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
s.callbacks.Remove(element)
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
|
||||
if s == nil {
|
||||
return nil
|
||||
|
@ -39,14 +53,24 @@ func (s *HistoryStorage) LoadURLTestHistory(tag string) *History {
|
|||
|
||||
func (s *HistoryStorage) DeleteURLTestHistory(tag string) {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
delete(s.delayHistory, tag)
|
||||
s.access.Unlock()
|
||||
s.notifyUpdated()
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
|
||||
s.access.Lock()
|
||||
defer s.access.Unlock()
|
||||
s.delayHistory[tag] = history
|
||||
s.access.Unlock()
|
||||
s.notifyUpdated()
|
||||
}
|
||||
|
||||
func (s *HistoryStorage) notifyUpdated() {
|
||||
s.access.RLock()
|
||||
defer s.access.RUnlock()
|
||||
for element := s.callbacks.Front(); element != nil; element = element.Next() {
|
||||
element.Value()
|
||||
}
|
||||
}
|
||||
|
||||
func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
"github.com/sagernet/websocket"
|
||||
|
||||
|
@ -68,13 +69,16 @@ func NewServer(ctx context.Context, router adapter.Router, logFactory log.Observ
|
|||
Handler: chiRouter,
|
||||
},
|
||||
trafficManager: trafficManager,
|
||||
urlTestHistory: urltest.NewHistoryStorage(),
|
||||
mode: strings.ToLower(options.DefaultMode),
|
||||
storeSelected: options.StoreSelected,
|
||||
storeFakeIP: options.StoreFakeIP,
|
||||
externalUIDownloadURL: options.ExternalUIDownloadURL,
|
||||
externalUIDownloadDetour: options.ExternalUIDownloadDetour,
|
||||
}
|
||||
server.urlTestHistory = service.PtrFromContext[urltest.HistoryStorage](ctx)
|
||||
if server.urlTestHistory == nil {
|
||||
server.urlTestHistory = urltest.NewHistoryStorage()
|
||||
}
|
||||
if server.mode == "" {
|
||||
server.mode = "rule"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@ package libbox
|
|||
const (
|
||||
CommandLog int32 = iota
|
||||
CommandStatus
|
||||
CommandServiceStop
|
||||
CommandServiceReload
|
||||
CommandCloseConnections
|
||||
CommandGroup
|
||||
CommandSelectOutbound
|
||||
CommandURLTest
|
||||
)
|
||||
|
|
|
@ -26,6 +26,13 @@ type CommandClientHandler interface {
|
|||
Disconnected(message string)
|
||||
WriteLog(message string)
|
||||
WriteStatus(message *StatusMessage)
|
||||
WriteGroups(message OutboundGroupIterator)
|
||||
}
|
||||
|
||||
func NewStandaloneCommandClient(sharedDirectory string) *CommandClient {
|
||||
return &CommandClient{
|
||||
sharedDirectory: sharedDirectory,
|
||||
}
|
||||
}
|
||||
|
||||
func NewCommandClient(sharedDirectory string, handler CommandClientHandler, options *CommandClientOptions) *CommandClient {
|
||||
|
@ -36,16 +43,16 @@ func NewCommandClient(sharedDirectory string, handler CommandClientHandler, opti
|
|||
}
|
||||
}
|
||||
|
||||
func clientConnect(sharedDirectory string) (net.Conn, error) {
|
||||
func (c *CommandClient) directConnect() (net.Conn, error) {
|
||||
return net.DialUnix("unix", nil, &net.UnixAddr{
|
||||
Name: filepath.Join(sharedDirectory, "command.sock"),
|
||||
Name: filepath.Join(c.sharedDirectory, "command.sock"),
|
||||
Net: "unix",
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CommandClient) Connect() error {
|
||||
common.Close(c.conn)
|
||||
conn, err := clientConnect(c.sharedDirectory)
|
||||
conn, err := c.directConnect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -65,6 +72,13 @@ func (c *CommandClient) Connect() error {
|
|||
}
|
||||
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)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -9,8 +9,8 @@ import (
|
|||
"github.com/sagernet/sing-box/common/dialer/conntrack"
|
||||
)
|
||||
|
||||
func ClientCloseConnections(sharedDirectory string) error {
|
||||
conn, err := clientConnect(sharedDirectory)
|
||||
func (c *CommandClient) CloseConnections() error {
|
||||
conn, err := c.directConnect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
228
experimental/libbox/command_group.go
Normal file
228
experimental/libbox/command_group.go
Normal file
|
@ -0,0 +1,228 @@
|
|||
package libbox
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
"github.com/sagernet/sing-box/outbound"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
type OutboundGroup struct {
|
||||
Tag string
|
||||
Type string
|
||||
Selectable bool
|
||||
Selected string
|
||||
items []*OutboundGroupItem
|
||||
}
|
||||
|
||||
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
|
||||
return newIterator(g.items)
|
||||
}
|
||||
|
||||
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 (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 {
|
||||
defer conn.Close()
|
||||
ctx := connKeepAlive(conn)
|
||||
for {
|
||||
service := s.service
|
||||
if service != nil {
|
||||
err := writeGroups(conn, service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := binary.Write(conn, binary.BigEndian, uint16(0))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-s.urlTestUpdate:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readGroups(reader io.Reader) (OutboundGroupIterator, error) {
|
||||
var groupLength uint16
|
||||
err := binary.Read(reader, binary.BigEndian, &groupLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups := make([]*OutboundGroup, 0, groupLength)
|
||||
for i := 0; i < int(groupLength); i++ {
|
||||
var group OutboundGroup
|
||||
group.Tag, err = rw.ReadVString(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group.Type, err = rw.ReadVString(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Read(reader, binary.BigEndian, &group.Selectable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group.Selected, err = rw.ReadVString(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var itemLength uint16
|
||||
err = binary.Read(reader, binary.BigEndian, &itemLength)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group.items = make([]*OutboundGroupItem, itemLength)
|
||||
for j := 0; j < int(itemLength); j++ {
|
||||
var item OutboundGroupItem
|
||||
item.Tag, err = rw.ReadVString(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
item.Type, err = rw.ReadVString(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Read(reader, binary.BigEndian, &item.URLTestTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Read(reader, binary.BigEndian, &item.URLTestDelay)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
group.items[j] = &item
|
||||
}
|
||||
groups = append(groups, &group)
|
||||
}
|
||||
return newIterator(groups), nil
|
||||
}
|
||||
|
||||
func writeGroups(writer io.Writer, boxService *BoxService) error {
|
||||
historyStorage := service.PtrFromContext[urltest.HistoryStorage](boxService.ctx)
|
||||
|
||||
outbounds := boxService.instance.Router().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 group OutboundGroup
|
||||
group.Tag = iGroup.Tag()
|
||||
group.Type = iGroup.Type()
|
||||
_, group.Selectable = iGroup.(*outbound.Selector)
|
||||
group.Selected = iGroup.Now()
|
||||
|
||||
for _, itemTag := range iGroup.All() {
|
||||
itemOutbound, isLoaded := boxService.instance.Router().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)
|
||||
}
|
||||
group.items = append(group.items, &item)
|
||||
}
|
||||
groups = append(groups, group)
|
||||
}
|
||||
|
||||
err := binary.Write(writer, binary.BigEndian, uint16(len(groups)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, group := range groups {
|
||||
err = rw.WriteVString(writer, group.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteVString(writer, group.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, group.Selectable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteVString(writer, group.Selected)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, uint16(len(group.items)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range group.items {
|
||||
err = rw.WriteVString(writer, item.Tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteVString(writer, item.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, item.URLTestTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, item.URLTestDelay)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -11,7 +11,7 @@ func (s *CommandServer) WriteMessage(message string) {
|
|||
s.subscriber.Emit(message)
|
||||
s.access.Lock()
|
||||
s.savedLines.PushBack(message)
|
||||
if s.savedLines.Len() > 100 {
|
||||
if s.savedLines.Len() > s.maxLines {
|
||||
s.savedLines.Remove(s.savedLines.Front())
|
||||
}
|
||||
s.access.Unlock()
|
||||
|
|
|
@ -8,8 +8,8 @@ import (
|
|||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
func ClientServiceReload(sharedDirectory string) error {
|
||||
conn, err := clientConnect(sharedDirectory)
|
||||
func (c *CommandClient) ServiceReload() error {
|
||||
conn, err := c.directConnect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
59
experimental/libbox/command_select.go
Normal file
59
experimental/libbox/command_select.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package libbox
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/outbound"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
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 = rw.WriteVString(conn, groupTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = rw.WriteVString(conn, outboundTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return readError(conn)
|
||||
}
|
||||
|
||||
func (s *CommandServer) handleSelectOutbound(conn net.Conn) error {
|
||||
defer conn.Close()
|
||||
groupTag, err := rw.ReadVString(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outboundTag, err := rw.ReadVString(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := s.service
|
||||
if service == nil {
|
||||
return writeError(conn, E.New("service not ready"))
|
||||
}
|
||||
outboundGroup, isLoaded := service.instance.Router().Outbound(groupTag)
|
||||
if !isLoaded {
|
||||
return writeError(conn, E.New("selector not found: ", groupTag))
|
||||
}
|
||||
selector, isSelector := outboundGroup.(*outbound.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)
|
||||
}
|
|
@ -7,12 +7,14 @@ import (
|
|||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
"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"
|
||||
)
|
||||
|
||||
type CommandServer struct {
|
||||
|
@ -22,26 +24,51 @@ type CommandServer struct {
|
|||
|
||||
access sync.Mutex
|
||||
savedLines *list.List[string]
|
||||
maxLines int
|
||||
subscriber *observable.Subscriber[string]
|
||||
observer *observable.Observer[string]
|
||||
service *BoxService
|
||||
|
||||
urlTestListener *list.Element[func()]
|
||||
urlTestUpdate chan struct{}
|
||||
}
|
||||
|
||||
type CommandServerHandler interface {
|
||||
ServiceStop() error
|
||||
ServiceReload() error
|
||||
}
|
||||
|
||||
func NewCommandServer(sharedDirectory string, handler CommandServerHandler) *CommandServer {
|
||||
func NewCommandServer(sharedDirectory string, handler CommandServerHandler, maxLines int32) *CommandServer {
|
||||
server := &CommandServer{
|
||||
sockPath: filepath.Join(sharedDirectory, "command.sock"),
|
||||
handler: handler,
|
||||
savedLines: new(list.List[string]),
|
||||
subscriber: observable.NewSubscriber[string](128),
|
||||
sockPath: filepath.Join(sharedDirectory, "command.sock"),
|
||||
handler: handler,
|
||||
savedLines: new(list.List[string]),
|
||||
maxLines: int(maxLines),
|
||||
subscriber: observable.NewSubscriber[string](128),
|
||||
urlTestUpdate: make(chan struct{}, 1),
|
||||
}
|
||||
server.observer = observable.NewObserver[string](server.subscriber, 64)
|
||||
return server
|
||||
}
|
||||
|
||||
func (s *CommandServer) SetService(newService *BoxService) {
|
||||
if s.service != nil && s.listener != nil {
|
||||
service.PtrFromContext[urltest.HistoryStorage](s.service.ctx).RemoveListener(s.urlTestListener)
|
||||
s.urlTestListener = nil
|
||||
}
|
||||
s.service = newService
|
||||
if newService != nil {
|
||||
s.urlTestListener = service.PtrFromContext[urltest.HistoryStorage](newService.ctx).AddListener(s.notifyURLTestUpdate)
|
||||
}
|
||||
s.notifyURLTestUpdate()
|
||||
}
|
||||
|
||||
func (s *CommandServer) notifyURLTestUpdate() {
|
||||
select {
|
||||
case s.urlTestUpdate <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CommandServer) Start() error {
|
||||
os.Remove(s.sockPath)
|
||||
listener, err := net.ListenUnix("unix", &net.UnixAddr{
|
||||
|
@ -92,12 +119,16 @@ func (s *CommandServer) handleConnection(conn net.Conn) error {
|
|||
return s.handleLogConn(conn)
|
||||
case CommandStatus:
|
||||
return s.handleStatusConn(conn)
|
||||
case CommandServiceStop:
|
||||
return s.handleServiceStop(conn)
|
||||
case CommandServiceReload:
|
||||
return s.handleServiceReload(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)
|
||||
default:
|
||||
return E.New("unknown command: ", command)
|
||||
}
|
||||
|
|
39
experimental/libbox/command_shared.go
Normal file
39
experimental/libbox/command_shared.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package libbox
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
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 := rw.ReadVString(reader)
|
||||
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 = rw.WriteVString(writer, wErr.Error())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package libbox
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"runtime/debug"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
func ClientServiceStop(sharedDirectory string) error {
|
||||
conn, err := clientConnect(sharedDirectory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
err = binary.Write(conn, binary.BigEndian, uint8(CommandServiceStop))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var hasError bool
|
||||
err = binary.Read(conn, binary.BigEndian, &hasError)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hasError {
|
||||
errorMessage, err := rw.ReadVString(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return E.New(errorMessage)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CommandServer) handleServiceStop(conn net.Conn) error {
|
||||
rErr := s.handler.ServiceStop()
|
||||
err := binary.Write(conn, binary.BigEndian, rErr != nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rErr != nil {
|
||||
return rw.WriteVString(conn, rErr.Error())
|
||||
}
|
||||
debug.FreeOSMemory()
|
||||
return nil
|
||||
}
|
95
experimental/libbox/command_urltest.go
Normal file
95
experimental/libbox/command_urltest.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
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/outbound"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/batch"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/rw"
|
||||
)
|
||||
|
||||
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 = rw.WriteVString(conn, groupTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return readError(conn)
|
||||
}
|
||||
|
||||
func (s *CommandServer) handleURLTest(conn net.Conn) error {
|
||||
defer conn.Close()
|
||||
groupTag, err := rw.ReadVString(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
service := s.service
|
||||
if service == nil {
|
||||
return nil
|
||||
}
|
||||
abstractOutboundGroup, isLoaded := service.instance.Router().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.(*outbound.URLTest)
|
||||
if isURLTest {
|
||||
go urlTest.CheckOutbounds()
|
||||
} else {
|
||||
var historyStorage *urltest.HistoryStorage
|
||||
if clashServer := service.instance.Router().ClashServer(); clashServer != nil {
|
||||
historyStorage = clashServer.HistoryStorage()
|
||||
} else {
|
||||
return writeError(conn, E.New("Clash API is required for URLTest on non-URLTest group"))
|
||||
}
|
||||
|
||||
outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
|
||||
itOutbound, _ := service.instance.Router().Outbound(it)
|
||||
return itOutbound
|
||||
}), func(it adapter.Outbound) bool {
|
||||
if it == nil {
|
||||
return false
|
||||
}
|
||||
_, isGroup := it.(adapter.OutboundGroup)
|
||||
if isGroup {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
b, _ := batch.New(service.ctx, batch.WithConcurrencyNum[any](10))
|
||||
for _, detour := range outbounds {
|
||||
outboundToTest := detour
|
||||
outboundTag := outboundToTest.Tag()
|
||||
b.Go(outboundTag, func() (any, error) {
|
||||
t, err := urltest.URLTest(service.ctx, "", outboundToTest)
|
||||
if err != nil {
|
||||
historyStorage.DeleteURLTestHistory(outboundTag)
|
||||
} else {
|
||||
historyStorage.StoreURLTestHistory(outboundTag, &urltest.History{
|
||||
Time: time.Now(),
|
||||
Delay: t,
|
||||
})
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
return writeError(conn, nil)
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"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"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/internal/procfs"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
|
@ -16,6 +17,7 @@ import (
|
|||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
)
|
||||
|
||||
|
@ -32,6 +34,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
|
|||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
ctx = filemanager.WithDefault(ctx, sBasePath, sTempPath, sUserID, sGroupID)
|
||||
ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage())
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
|
|
2
go.mod
2
go.mod
|
@ -21,7 +21,7 @@ require (
|
|||
github.com/oschwald/maxminddb-golang v1.11.0
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0
|
||||
github.com/sagernet/gomobile v0.0.0-20230413023804-244d7ff07035
|
||||
github.com/sagernet/gomobile v0.0.0-20230701084532-493ee2e45182
|
||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2
|
||||
github.com/sagernet/quic-go v0.0.0-20230615020047-10f05c797c02
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||
|
|
5
go.sum
5
go.sum
|
@ -104,8 +104,8 @@ github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt
|
|||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
|
||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
|
||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
|
||||
github.com/sagernet/gomobile v0.0.0-20230413023804-244d7ff07035 h1:KttYh6bBhIw8Y6/Ljn7CGwC3CKZn788rzMJmeAKjY+8=
|
||||
github.com/sagernet/gomobile v0.0.0-20230413023804-244d7ff07035/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU=
|
||||
github.com/sagernet/gomobile v0.0.0-20230701084532-493ee2e45182 h1:sD5g92IO15RAX2DvA4Cq3Uc7tcgqNWVi8K3VTCI6sEo=
|
||||
github.com/sagernet/gomobile v0.0.0-20230701084532-493ee2e45182/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU=
|
||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTSWt6hdPrARORfoYvuUczynvRLrueo=
|
||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
|
||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
||||
|
@ -149,6 +149,7 @@ github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRM
|
|||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
|
|
|
@ -24,8 +24,9 @@ func NewFactory(formatter Formatter, writer io.Writer, platformWriter io.Writer)
|
|||
return &simpleFactory{
|
||||
formatter: formatter,
|
||||
platformFormatter: Formatter{
|
||||
BaseTime: formatter.BaseTime,
|
||||
DisableColors: C.IsDarwin || C.IsIos,
|
||||
BaseTime: formatter.BaseTime,
|
||||
DisableColors: C.IsDarwin || C.IsIos,
|
||||
DisableLineBreak: true,
|
||||
},
|
||||
writer: writer,
|
||||
platformWriter: platformWriter,
|
||||
|
|
|
@ -17,6 +17,7 @@ type Formatter struct {
|
|||
DisableTimestamp bool
|
||||
FullTimestamp bool
|
||||
TimestampFormat string
|
||||
DisableLineBreak bool
|
||||
}
|
||||
|
||||
func (f Formatter) Format(ctx context.Context, level Level, tag string, message string, timestamp time.Time) string {
|
||||
|
@ -76,8 +77,14 @@ func (f Formatter) Format(ctx context.Context, level Level, tag string, message
|
|||
default:
|
||||
message = levelString + "[" + xd(int(timestamp.Sub(f.BaseTime)/time.Second), 4) + "] " + message
|
||||
}
|
||||
if message[len(message)-1] != '\n' {
|
||||
message += "\n"
|
||||
if f.DisableLineBreak {
|
||||
if message[len(message)-1] != '\n' {
|
||||
message = message[:len(message)-1]
|
||||
}
|
||||
} else {
|
||||
if message[len(message)-1] != '\n' {
|
||||
message += "\n"
|
||||
}
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
|
|
@ -28,8 +28,9 @@ func NewObservableFactory(formatter Formatter, writer io.Writer, platformWriter
|
|||
factory := &observableFactory{
|
||||
formatter: formatter,
|
||||
platformFormatter: Formatter{
|
||||
BaseTime: formatter.BaseTime,
|
||||
DisableColors: C.IsDarwin || C.IsIos,
|
||||
BaseTime: formatter.BaseTime,
|
||||
DisableColors: C.IsDarwin || C.IsIos,
|
||||
DisableLineBreak: true,
|
||||
},
|
||||
writer: writer,
|
||||
platformWriter: platformWriter,
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -74,7 +75,11 @@ func (s *URLTest) Start() error {
|
|||
outbounds = append(outbounds, detour)
|
||||
}
|
||||
s.group = NewURLTestGroup(s.ctx, s.router, s.logger, outbounds, s.link, s.interval, s.tolerance)
|
||||
go s.group.CheckOutbounds(false)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *URLTest) PostStart() error {
|
||||
go s.CheckOutbounds()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -96,6 +101,10 @@ func (s *URLTest) URLTest(ctx context.Context, link string) (map[string]uint16,
|
|||
return s.group.URLTest(ctx, link)
|
||||
}
|
||||
|
||||
func (s *URLTest) CheckOutbounds() {
|
||||
s.group.CheckOutbounds(true)
|
||||
}
|
||||
|
||||
func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
s.group.Start()
|
||||
outbound := s.group.Select(network)
|
||||
|
@ -157,7 +166,8 @@ func NewURLTestGroup(ctx context.Context, router adapter.Router, logger log.Logg
|
|||
tolerance = 50
|
||||
}
|
||||
var history *urltest.HistoryStorage
|
||||
if clashServer := router.ClashServer(); clashServer != nil {
|
||||
if history = service.PtrFromContext[urltest.HistoryStorage](ctx); history != nil {
|
||||
} else if clashServer := router.ClashServer(); clashServer != nil {
|
||||
history = clashServer.HistoryStorage()
|
||||
} else {
|
||||
history = urltest.NewHistoryStorage()
|
||||
|
|
Loading…
Reference in a new issue