diff --git a/.gitignore b/.gitignore index 570d676a..df2cf0dc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ /sing-box /build/ /*.jar -/*.aar \ No newline at end of file +/*.aar +/*.xcframework/ diff --git a/box.go b/box.go index 2924cc7b..2ae960f4 100644 --- a/box.go +++ b/box.go @@ -78,28 +78,28 @@ func New(ctx context.Context, options option.Options) (*Box, error) { } logWriter = logFile } - } - logFormatter := log.Formatter{ - BaseTime: createdAt, - DisableColors: logOptions.DisableColor || logFile != nil, - DisableTimestamp: !logOptions.Timestamp && logFile != nil, - FullTimestamp: logOptions.Timestamp, - TimestampFormat: "-0700 2006-01-02 15:04:05", - } - if needClashAPI { - observableLogFactory = log.NewObservableFactory(logFormatter, logWriter, options.PlatformInterface) - logFactory = observableLogFactory - } else { - logFactory = log.NewFactory(logFormatter, logWriter, options.PlatformInterface) - } - if logOptions.Level != "" { - logLevel, err := log.ParseLevel(logOptions.Level) - if err != nil { - return nil, E.Cause(err, "parse log level") + logFormatter := log.Formatter{ + BaseTime: createdAt, + DisableColors: logOptions.DisableColor || logFile != nil, + DisableTimestamp: !logOptions.Timestamp && logFile != nil, + FullTimestamp: logOptions.Timestamp, + TimestampFormat: "-0700 2006-01-02 15:04:05", + } + if needClashAPI { + observableLogFactory = log.NewObservableFactory(logFormatter, logWriter, options.PlatformInterface) + logFactory = observableLogFactory + } else { + logFactory = log.NewFactory(logFormatter, logWriter, options.PlatformInterface) + } + if logOptions.Level != "" { + logLevel, err := log.ParseLevel(logOptions.Level) + if err != nil { + return nil, E.Cause(err, "parse log level") + } + logFactory.SetLevel(logLevel) + } else { + logFactory.SetLevel(log.LevelTrace) } - logFactory.SetLevel(logLevel) - } else { - logFactory.SetLevel(log.LevelTrace) } router, err := route.NewRouter( diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go index b471acc5..7bc76cd2 100644 --- a/cmd/internal/build_libbox/main.go +++ b/cmd/internal/build_libbox/main.go @@ -12,16 +12,32 @@ import ( "github.com/sagernet/sing/common/rw" ) -var debugEnabled bool +var ( + debugEnabled bool + target string +) func init() { flag.BoolVar(&debugEnabled, "debug", false, "enable debug") + flag.StringVar(&target, "target", "android", "target platform") } func main() { - build_shared.FindSDK() + flag.Parse() + build_shared.FindMobile() + switch target { + case "android": + buildAndroid() + case "ios": + buildiOS() + } +} + +func buildAndroid() { + build_shared.FindSDK() + args := []string{ "bind", "-v", @@ -32,10 +48,10 @@ func main() { if !debugEnabled { args = append(args, "-trimpath", "-ldflags=-s -w -buildid=", - "-tags", "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api,debug", + "-tags", "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api", ) } else { - args = append(args, "-tags", "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api") + args = append(args, "-tags", "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api,debug") } args = append(args, "./experimental/libbox") @@ -59,3 +75,38 @@ func main() { log.Info("copied to ", copyPath) } } + +func buildiOS() { + args := []string{ + "bind", + "-v", + "-target", "ios,iossimulator,macos", + "-libname=box", + } + if !debugEnabled { + args = append(args, + "-trimpath", "-ldflags=-s -w -buildid=", + ) + } else { + args = append(args, "-tags", "debug") + } + + args = append(args, "./experimental/libbox") + + command := exec.Command(build_shared.GoBinPath+"/gomobile", args...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + err := command.Run() + if err != nil { + log.Fatal(err) + } + + copyPath := filepath.Join("..", "sfi") + if rw.FileExists(copyPath) { + targetDir := filepath.Join(copyPath, "Libbox.xcframework") + targetDir, _ = filepath.Abs(targetDir) + os.RemoveAll(targetDir) + os.Rename("Libbox.xcframework", targetDir) + log.Info("copied to ", targetDir) + } +} diff --git a/experimental/libbox/log.go b/experimental/libbox/log.go new file mode 100644 index 00000000..07190773 --- /dev/null +++ b/experimental/libbox/log.go @@ -0,0 +1,54 @@ +package libbox + +import ( + "bufio" + "log" + "os" +) + +type StandardOutput interface { + WriteOutput(message string) + WriteErrorOutput(message string) +} + +func SetOutput(output StandardOutput) { + log.SetOutput(logWriter{output}) + pipeIn, pipeOut, err := os.Pipe() + if err != nil { + panic(err) + } + os.Stdout = os.NewFile(pipeOut.Fd(), "stdout") + go lineLog(pipeIn, output.WriteOutput) + + pipeIn, pipeOut, err = os.Pipe() + if err != nil { + panic(err) + } + os.Stderr = os.NewFile(pipeOut.Fd(), "srderr") + go lineLog(pipeIn, output.WriteErrorOutput) +} + +type logWriter struct { + output StandardOutput +} + +func (w logWriter) Write(p []byte) (n int, err error) { + w.output.WriteOutput(string(p)) + return len(p), nil +} + +func lineLog(f *os.File, output func(string)) { + const logSize = 1024 // matches android/log.h. + r := bufio.NewReaderSize(f, logSize) + for { + line, _, err := r.ReadLine() + str := string(line) + if err != nil { + str += " " + err.Error() + } + output(str) + if err != nil { + break + } + } +} diff --git a/experimental/libbox/log_client.go b/experimental/libbox/log_client.go new file mode 100644 index 00000000..f503e57c --- /dev/null +++ b/experimental/libbox/log_client.go @@ -0,0 +1,59 @@ +//go:build ios + +package libbox + +import ( + "net" + "path/filepath" + + "github.com/sagernet/sing/common" +) + +type LogClient struct { + sockPath string + handler LogClientHandler + conn net.Conn +} + +type LogClientHandler interface { + Connected() + Disconnected() + WriteLog(message string) +} + +func NewLogClient(sharedDirectory string, handler LogClientHandler) *LogClient { + return &LogClient{ + sockPath: filepath.Join(sharedDirectory, "log.sock"), + handler: handler, + } +} + +func (c *LogClient) Connect() error { + conn, err := net.DialUnix("unix", nil, &net.UnixAddr{ + Name: c.sockPath, + Net: "unix", + }) + if err != nil { + return err + } + c.conn = conn + go c.loopConnection(&messageConn{conn}) + return nil +} + +func (c *LogClient) Disconnect() error { + return common.Close(c.conn) +} + +func (c *LogClient) loopConnection(conn *messageConn) { + c.handler.Connected() + defer c.handler.Disconnected() + for { + message, err := conn.Read() + if err != nil { + c.handler.WriteLog("(log client error) " + err.Error()) + return + } + c.handler.WriteLog(string(message)) + } +} diff --git a/experimental/libbox/log_server.go b/experimental/libbox/log_server.go new file mode 100644 index 00000000..af80a635 --- /dev/null +++ b/experimental/libbox/log_server.go @@ -0,0 +1,139 @@ +//go:build ios + +package libbox + +import ( + "encoding/binary" + "io" + "net" + "os" + "path/filepath" + "sync" + + "github.com/sagernet/sing-box/log" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/observable" + "github.com/sagernet/sing/common/x/list" +) + +type LogServer struct { + sockPath string + listener net.Listener + + access sync.Mutex + savedLines *list.List[string] + subscriber *observable.Subscriber[string] + observer *observable.Observer[string] +} + +func NewLogServer(sharedDirectory string) *LogServer { + server := &LogServer{ + sockPath: filepath.Join(sharedDirectory, "log.sock"), + savedLines: new(list.List[string]), + subscriber: observable.NewSubscriber[string](128), + } + server.observer = observable.NewObserver[string](server.subscriber, 64) + return server +} + +func (s *LogServer) Start() error { + os.Remove(s.sockPath) + listener, err := net.ListenUnix("unix", &net.UnixAddr{ + Name: s.sockPath, + Net: "unix", + }) + if err != nil { + return err + } + go s.loopConnection(listener) + return nil +} + +func (s *LogServer) Close() error { + return s.listener.Close() +} + +func (s *LogServer) WriteMessage(message string) { + s.subscriber.Emit(message) + s.access.Lock() + s.savedLines.PushBack(message) + if s.savedLines.Len() > 100 { + s.savedLines.Remove(s.savedLines.Front()) + } + s.access.Unlock() +} + +func (s *LogServer) loopConnection(listener net.Listener) { + for { + conn, err := listener.Accept() + if err != nil { + return + } + go func() { + hErr := s.handleConnection(&messageConn{conn}) + if hErr != nil && !E.IsClosed(err) { + log.Warn("log-server: process connection: ", hErr) + } + }() + } +} + +func (s *LogServer) handleConnection(conn *messageConn) error { + 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) + for _, line := range savedLines { + err = conn.Write([]byte(line)) + if err != nil { + return err + } + } + for { + select { + case message := <-subscription: + err = conn.Write([]byte(message)) + if err != nil { + return err + } + case <-done: + conn.Close() + return nil + } + } +} + +type messageConn struct { + net.Conn +} + +func (c *messageConn) Read() ([]byte, error) { + var messageLength uint16 + err := binary.Read(c.Conn, binary.BigEndian, &messageLength) + if err != nil { + return nil, err + } + data := make([]byte, messageLength) + _, err = io.ReadFull(c.Conn, data) + if err != nil { + return nil, err + } + return data, nil +} + +func (c *messageConn) Write(message []byte) error { + err := binary.Write(c.Conn, binary.BigEndian, uint16(len(message))) + if err != nil { + return err + } + _, err = c.Conn.Write(message) + return err +} diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go index f0f149e5..a08e9b7d 100644 --- a/experimental/libbox/service.go +++ b/experimental/libbox/service.go @@ -4,6 +4,7 @@ import ( "context" "net/netip" "os" + "runtime" "syscall" "github.com/sagernet/sing-box" @@ -27,6 +28,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box if err != nil { return nil, err } + platformInterface.WriteLog("Hello " + runtime.GOOS + "/" + runtime.GOARCH) options.PlatformInterface = &platformInterfaceWrapper{platformInterface, platformInterface.UseProcFS()} ctx, cancel := context.WithCancel(context.Background()) instance, err := box.New(ctx, options) diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go index 0d0fb035..be38d3ab 100644 --- a/experimental/libbox/setup.go +++ b/experimental/libbox/setup.go @@ -5,3 +5,7 @@ import C "github.com/sagernet/sing-box/constant" func SetBasePath(path string) { C.SetBasePath(path) } + +func Version() string { + return C.Version +} diff --git a/log/default.go b/log/default.go index 5faeb76a..e8db489a 100644 --- a/log/default.go +++ b/log/default.go @@ -6,6 +6,7 @@ import ( "os" "time" + C "github.com/sagernet/sing-box/constant" F "github.com/sagernet/sing/common/format" ) @@ -23,7 +24,8 @@ func NewFactory(formatter Formatter, writer io.Writer, platformWriter io.Writer) return &simpleFactory{ formatter: formatter, platformFormatter: Formatter{ - BaseTime: formatter.BaseTime, + BaseTime: formatter.BaseTime, + DisableColors: C.IsIos, }, writer: writer, platformWriter: platformWriter,