diff --git a/.gitignore b/.gitignore index ff683132..570d676a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ /site/ /bin/ /dist/ -/sing-box \ No newline at end of file +/sing-box +/build/ +/*.jar +/*.aar \ No newline at end of file diff --git a/Makefile b/Makefile index 19a53276..aa3cfc29 100644 --- a/Makefile +++ b/Makefile @@ -71,6 +71,14 @@ test_stdio: go mod tidy && \ go test -v -tags "$(TAGS_TEST),force_stdio" . +lib: + go run ./cmd/internal/build_libbox + +lib_install: + go get -v -d + go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20221130124640-349ebaa752ca + go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20221130124640-349ebaa752ca + clean: rm -rf bin dist sing-box rm -f $(shell go env GOPATH)/sing-box diff --git a/adapter/router.go b/adapter/router.go index 7a07c852..39178f02 100644 --- a/adapter/router.go +++ b/adapter/router.go @@ -34,6 +34,7 @@ type Router interface { InterfaceFinder() control.InterfaceFinder DefaultInterface() string AutoDetectInterface() bool + AutoDetectInterfaceFunc() control.Func DefaultMark() int NetworkMonitor() tun.NetworkUpdateMonitor InterfaceMonitor() tun.DefaultInterfaceMonitor diff --git a/box.go b/box.go index 01bb7701..af683375 100644 --- a/box.go +++ b/box.go @@ -9,6 +9,7 @@ import ( "time" "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" "github.com/sagernet/sing-box/experimental" "github.com/sagernet/sing-box/inbound" "github.com/sagernet/sing-box/log" @@ -53,46 +54,52 @@ func New(ctx context.Context, options option.Options) (*Box, error) { var logFactory log.Factory var observableLogFactory log.ObservableFactory var logFile *os.File + var logWriter io.Writer if logOptions.Disabled { observableLogFactory = log.NewNOPFactory() logFactory = observableLogFactory } else { - var logWriter io.Writer switch logOptions.Output { - case "", "stderr": + case "": + if options.PlatformInterface != nil { + logWriter = io.Discard + } else { + logWriter = os.Stdout + } + case "stderr": logWriter = os.Stderr case "stdout": logWriter = os.Stdout default: var err error - logFile, err = os.OpenFile(logOptions.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + logFile, err = os.OpenFile(C.BasePath(logOptions.Output), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return nil, err } 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) - logFactory = observableLogFactory - } else { - logFactory = log.NewFactory(logFormatter, logWriter) - } - 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) + } + 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) } router, err := route.NewRouter( @@ -101,6 +108,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) { common.PtrValueOrDefault(options.Route), common.PtrValueOrDefault(options.DNS), options.Inbounds, + options.PlatformInterface, ) if err != nil { return nil, E.Cause(err, "parse route options") @@ -120,6 +128,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) { router, logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")), inboundOptions, + options.PlatformInterface, ) if err != nil { return nil, E.Cause(err, "parse inbound[", i, "]") @@ -255,19 +264,43 @@ func (s *Box) Close() error { default: close(s.done) } - for _, in := range s.inbounds { - in.Close() + var errors error + for i, in := range s.inbounds { + errors = E.Append(errors, in.Close(), func(err error) error { + return E.Cause(err, "close inbound/", in.Type(), "[", i, "]") + }) } - for _, out := range s.outbounds { - common.Close(out) + for i, out := range s.outbounds { + errors = E.Append(errors, common.Close(out), func(err error) error { + return E.Cause(err, "close inbound/", out.Type(), "[", i, "]") + }) } - return common.Close( - s.router, - s.logFactory, - s.clashServer, - s.v2rayServer, - common.PtrOrNil(s.logFile), - ) + if err := common.Close(s.router); err != nil { + errors = E.Append(errors, err, func(err error) error { + return E.Cause(err, "close router") + }) + } + if err := common.Close(s.logFactory); err != nil { + errors = E.Append(errors, err, func(err error) error { + return E.Cause(err, "close log factory") + }) + } + if err := common.Close(s.clashServer); err != nil { + errors = E.Append(errors, err, func(err error) error { + return E.Cause(err, "close clash api server") + }) + } + if err := common.Close(s.v2rayServer); err != nil { + errors = E.Append(errors, err, func(err error) error { + return E.Cause(err, "close v2ray api server") + }) + } + if s.logFile != nil { + errors = E.Append(errors, s.logFile.Close(), func(err error) error { + return E.Cause(err, "close log file") + }) + } + return errors } func (s *Box) Router() adapter.Router { diff --git a/cmd/internal/build/main.go b/cmd/internal/build/main.go index b6adaf63..0bd11f98 100644 --- a/cmd/internal/build/main.go +++ b/cmd/internal/build/main.go @@ -4,11 +4,12 @@ import ( "os" "os/exec" + "github.com/sagernet/sing-box/cmd/internal/build_shared" "github.com/sagernet/sing-box/log" ) func main() { - findSDK() + build_shared.FindSDK() command := exec.Command(os.Args[1], os.Args[2:]...) command.Stdout = os.Stdout diff --git a/cmd/internal/build_libbox/main.go b/cmd/internal/build_libbox/main.go new file mode 100644 index 00000000..b471acc5 --- /dev/null +++ b/cmd/internal/build_libbox/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "flag" + "os" + "os/exec" + "path/filepath" + + _ "github.com/sagernet/gomobile/asset" + "github.com/sagernet/sing-box/cmd/internal/build_shared" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing/common/rw" +) + +var debugEnabled bool + +func init() { + flag.BoolVar(&debugEnabled, "debug", false, "enable debug") +} + +func main() { + build_shared.FindSDK() + build_shared.FindMobile() + + args := []string{ + "bind", + "-v", + "-androidapi", "21", + "-javapkg=io.nekohasekai", + "-libname=box", + } + if !debugEnabled { + args = append(args, + "-trimpath", "-ldflags=-s -w -buildid=", + "-tags", "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api,debug", + ) + } else { + args = append(args, "-tags", "with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api") + } + + 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) + } + + const name = "libbox.aar" + copyPath := filepath.Join("..", "sing-box-for-android", "app", "libs") + if rw.FileExists(copyPath) { + copyPath, _ = filepath.Abs(copyPath) + err = rw.CopyFile(name, filepath.Join(copyPath, name)) + if err != nil { + log.Fatal(err) + } + log.Info("copied to ", copyPath) + } +} diff --git a/cmd/internal/build/sdk.go b/cmd/internal/build_shared/sdk.go similarity index 88% rename from cmd/internal/build/sdk.go rename to cmd/internal/build_shared/sdk.go index e5468c98..35607196 100644 --- a/cmd/internal/build/sdk.go +++ b/cmd/internal/build_shared/sdk.go @@ -1,6 +1,7 @@ -package main +package build_shared import ( + "go/build" "os" "path/filepath" "runtime" @@ -18,7 +19,7 @@ var ( androidNDKPath string ) -func findSDK() { +func FindSDK() { searchPath := []string{ "$ANDROID_HOME", "$HOME/Android/Sdk", @@ -79,3 +80,13 @@ func findNDK() bool { } return false } + +var GoBinPath string + +func FindMobile() { + goBin := filepath.Join(build.Default.GOPATH, "bin") + if !rw.FileExists(goBin + "/" + "gobind") { + log.Fatal("missing gomobile installation") + } + GoBinPath = goBin +} diff --git a/common/dialer/default.go b/common/dialer/default.go index be22380b..b128902b 100644 --- a/common/dialer/default.go +++ b/common/dialer/default.go @@ -70,15 +70,7 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } else if router.AutoDetectInterface() { - const useInterfaceName = C.IsLinux - bindFunc := control.BindToInterfaceFunc(router.InterfaceFinder(), func(network string, address string) (interfaceName string, interfaceIndex int) { - remoteAddr := M.ParseSocksaddr(address).Addr - if C.IsLinux { - return router.InterfaceMonitor().DefaultInterfaceName(remoteAddr), -1 - } else { - return "", router.InterfaceMonitor().DefaultInterfaceIndex(remoteAddr) - } - }) + bindFunc := router.AutoDetectInterfaceFunc() dialer.Control = control.Append(dialer.Control, bindFunc) listener.Control = control.Append(listener.Control, bindFunc) } else if router.DefaultInterface() != "" { diff --git a/constant/path.go b/constant/path.go index 98acacdc..7e423312 100644 --- a/constant/path.go +++ b/constant/path.go @@ -3,13 +3,28 @@ package constant import ( "os" "path/filepath" + "strings" "github.com/sagernet/sing/common/rw" ) const dirName = "sing-box" -var resourcePaths []string +var ( + basePath string + resourcePaths []string +) + +func BasePath(name string) string { + if basePath == "" || strings.HasPrefix(name, "/") { + return name + } + return filepath.Join(basePath, name) +} + +func SetBasePath(path string) { + basePath = path +} func FindPath(name string) (string, bool) { name = os.ExpandEnv(name) diff --git a/experimental/clashapi/server.go b/experimental/clashapi/server.go index c2e20df5..c2ac9ca4 100644 --- a/experimental/clashapi/server.go +++ b/experimental/clashapi/server.go @@ -42,7 +42,6 @@ type Server struct { httpServer *http.Server trafficManager *trafficontrol.Manager urlTestHistory *urltest.HistoryStorage - tcpListener net.Listener mode string storeSelected bool cacheFile adapter.ClashCacheFile @@ -71,6 +70,11 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options if cachePath == "" { cachePath = "cache.db" } + if foundPath, loaded := C.FindPath(cachePath); loaded { + cachePath = foundPath + } else { + cachePath = C.BasePath(cachePath) + } cacheFile, err := cachefile.Open(cachePath) if err != nil { return nil, E.Cause(err, "open cache file") @@ -103,7 +107,7 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options }) if options.ExternalUI != "" { chiRouter.Group(func(r chi.Router) { - fs := http.StripPrefix("/ui", http.FileServer(http.Dir(os.ExpandEnv(options.ExternalUI)))) + fs := http.StripPrefix("/ui", http.FileServer(http.Dir(C.BasePath(os.ExpandEnv(options.ExternalUI))))) r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP) r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) { fs.ServeHTTP(w, r) @@ -119,7 +123,6 @@ func (s *Server) Start() error { return E.Cause(err, "external controller listen error") } s.logger.Info("restful api listening at ", listener.Addr()) - s.tcpListener = listener go func() { err = s.httpServer.Serve(listener) if err != nil && !errors.Is(err, http.ErrServerClosed) { @@ -132,7 +135,6 @@ func (s *Server) Start() error { func (s *Server) Close() error { return common.Close( common.PtrOrNil(s.httpServer), - s.tcpListener, s.trafficManager, s.cacheFile, ) diff --git a/experimental/libbox/config.go b/experimental/libbox/config.go new file mode 100644 index 00000000..253f72c6 --- /dev/null +++ b/experimental/libbox/config.go @@ -0,0 +1,15 @@ +package libbox + +import ( + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func parseConfig(configContent string) (option.Options, error) { + var options option.Options + err := options.UnmarshalJSON([]byte(configContent)) + if err != nil { + return option.Options{}, E.Cause(err, "decode config") + } + return options, nil +} diff --git a/experimental/libbox/internal/procfs/procfs.go b/experimental/libbox/internal/procfs/procfs.go new file mode 100644 index 00000000..8c918a79 --- /dev/null +++ b/experimental/libbox/internal/procfs/procfs.go @@ -0,0 +1,148 @@ +package procfs + +import ( + "bufio" + "encoding/binary" + "encoding/hex" + "fmt" + "net" + "net/netip" + "os" + "strconv" + "strings" + "unsafe" + + N "github.com/sagernet/sing/common/network" +) + +var ( + netIndexOfLocal = -1 + netIndexOfUid = -1 + nativeEndian binary.ByteOrder +) + +func init() { + var x uint32 = 0x01020304 + if *(*byte)(unsafe.Pointer(&x)) == 0x01 { + nativeEndian = binary.BigEndian + } else { + nativeEndian = binary.LittleEndian + } +} + +func ResolveSocketByProcSearch(network string, source, _ netip.AddrPort) int32 { + if netIndexOfLocal < 0 || netIndexOfUid < 0 { + return -1 + } + + path := "/proc/net/" + + if network == N.NetworkTCP { + path += "tcp" + } else { + path += "udp" + } + + if source.Addr().Is6() { + path += "6" + } + + sIP := source.Addr().AsSlice() + if len(sIP) == 0 { + return -1 + } + + var bytes [2]byte + binary.BigEndian.PutUint16(bytes[:], source.Port()) + local := fmt.Sprintf("%s:%s", hex.EncodeToString(nativeEndianIP(sIP)), hex.EncodeToString(bytes[:])) + + file, err := os.Open(path) + if err != nil { + return -1 + } + + defer file.Close() + + reader := bufio.NewReader(file) + + for { + row, _, err := reader.ReadLine() + if err != nil { + return -1 + } + + fields := strings.Fields(string(row)) + + if len(fields) <= netIndexOfLocal || len(fields) <= netIndexOfUid { + continue + } + + if strings.EqualFold(local, fields[netIndexOfLocal]) { + uid, err := strconv.Atoi(fields[netIndexOfUid]) + if err != nil { + return -1 + } + + return int32(uid) + } + } +} + +func nativeEndianIP(ip net.IP) []byte { + result := make([]byte, len(ip)) + + for i := 0; i < len(ip); i += 4 { + value := binary.BigEndian.Uint32(ip[i:]) + + nativeEndian.PutUint32(result[i:], value) + } + + return result +} + +func init() { + file, err := os.Open("/proc/net/tcp") + if err != nil { + return + } + + defer file.Close() + + reader := bufio.NewReader(file) + + header, _, err := reader.ReadLine() + if err != nil { + return + } + + columns := strings.Fields(string(header)) + + var txQueue, rxQueue, tr, tmWhen bool + + for idx, col := range columns { + offset := 0 + + if txQueue && rxQueue { + offset-- + } + + if tr && tmWhen { + offset-- + } + + switch col { + case "tx_queue": + txQueue = true + case "rx_queue": + rxQueue = true + case "tr": + tr = true + case "tm->when": + tmWhen = true + case "local_address": + netIndexOfLocal = idx + offset + case "uid": + netIndexOfUid = idx + offset + } + } +} diff --git a/experimental/libbox/iterator.go b/experimental/libbox/iterator.go new file mode 100644 index 00000000..e23e2a7f --- /dev/null +++ b/experimental/libbox/iterator.go @@ -0,0 +1,31 @@ +package libbox + +import "github.com/sagernet/sing/common" + +type StringIterator interface { + Next() string + HasNext() bool +} + +var _ StringIterator = (*iterator[string])(nil) + +type iterator[T any] struct { + values []T +} + +func newIterator[T any](values []T) *iterator[T] { + return &iterator[T]{values} +} + +func (i *iterator[T]) Next() T { + if len(i.values) == 0 { + return common.DefaultValue[T]() + } + nextValue := i.values[0] + i.values = i.values[1:] + return nextValue +} + +func (i *iterator[T]) HasNext() bool { + return len(i.values) > 0 +} diff --git a/experimental/libbox/platform.go b/experimental/libbox/platform.go new file mode 100644 index 00000000..c494c598 --- /dev/null +++ b/experimental/libbox/platform.go @@ -0,0 +1,16 @@ +package libbox + +type PlatformInterface interface { + AutoDetectInterfaceControl(fd int32) error + OpenTun(options TunOptions) (TunInterface, 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) +} + +type TunInterface interface { + FileDescriptor() int32 + Close() error +} diff --git a/experimental/libbox/platform/interface.go b/experimental/libbox/platform/interface.go new file mode 100644 index 00000000..49d131ae --- /dev/null +++ b/experimental/libbox/platform/interface.go @@ -0,0 +1,16 @@ +package platform + +import ( + "io" + + "github.com/sagernet/sing-box/common/process" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" +) + +type Interface interface { + AutoDetectInterfaceControl() control.Func + OpenTun(options tun.Options) (tun.Tun, error) + process.Searcher + io.Writer +} diff --git a/experimental/libbox/pprof.go b/experimental/libbox/pprof.go new file mode 100644 index 00000000..86cf3b52 --- /dev/null +++ b/experimental/libbox/pprof.go @@ -0,0 +1,35 @@ +//go:build debug + +package libbox + +import ( + "net" + "net/http" + _ "net/http/pprof" + "strconv" +) + +type PProfServer struct { + server *http.Server +} + +func NewPProfServer(port int) *PProfServer { + return &PProfServer{ + &http.Server{ + Addr: ":" + strconv.Itoa(port), + }, + } +} + +func (s *PProfServer) Start() error { + ln, err := net.Listen("tcp", s.server.Addr) + if err != nil { + return err + } + go s.server.Serve(ln) + return nil +} + +func (s *PProfServer) Close() error { + return s.server.Close() +} diff --git a/experimental/libbox/pprof_stub.go b/experimental/libbox/pprof_stub.go new file mode 100644 index 00000000..580d1ba9 --- /dev/null +++ b/experimental/libbox/pprof_stub.go @@ -0,0 +1,21 @@ +//go:build !debug + +package libbox + +import ( + "os" +) + +type PProfServer struct{} + +func NewPProfServer(port int) *PProfServer { + return &PProfServer{} +} + +func (s *PProfServer) Start() error { + return os.ErrInvalid +} + +func (s *PProfServer) Close() error { + return os.ErrInvalid +} diff --git a/experimental/libbox/service.go b/experimental/libbox/service.go new file mode 100644 index 00000000..f0f149e5 --- /dev/null +++ b/experimental/libbox/service.go @@ -0,0 +1,120 @@ +package libbox + +import ( + "context" + "net/netip" + "os" + "syscall" + + "github.com/sagernet/sing-box" + "github.com/sagernet/sing-box/common/process" + "github.com/sagernet/sing-box/experimental/libbox/internal/procfs" + "github.com/sagernet/sing-box/experimental/libbox/platform" + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common/control" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" +) + +type BoxService struct { + ctx context.Context + cancel context.CancelFunc + instance *box.Box +} + +func NewService(configContent string, platformInterface PlatformInterface) (*BoxService, error) { + options, err := parseConfig(configContent) + if err != nil { + return nil, err + } + options.PlatformInterface = &platformInterfaceWrapper{platformInterface, platformInterface.UseProcFS()} + ctx, cancel := context.WithCancel(context.Background()) + instance, err := box.New(ctx, options) + if err != nil { + cancel() + return nil, E.Cause(err, "create service") + } + return &BoxService{ + ctx: ctx, + cancel: cancel, + instance: instance, + }, nil +} + +func (s *BoxService) Start() error { + return s.instance.Start() +} + +func (s *BoxService) Close() error { + s.cancel() + return s.instance.Close() +} + +var _ platform.Interface = (*platformInterfaceWrapper)(nil) + +type platformInterfaceWrapper struct { + iif PlatformInterface + useProcFS bool +} + +func (w *platformInterfaceWrapper) AutoDetectInterfaceControl() control.Func { + return func(network, address string, conn syscall.RawConn) error { + return control.Raw(conn, func(fd uintptr) error { + return w.iif.AutoDetectInterfaceControl(int32(fd)) + }) + } +} + +func (w *platformInterfaceWrapper) OpenTun(options tun.Options) (tun.Tun, error) { + if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 { + return nil, E.New("android: unsupported uid options") + } + if len(options.IncludeAndroidUser) > 0 { + return nil, E.New("android: unsupported android_user option") + } + + optionsWrapper := tunOptions(options) + tunInterface, err := w.iif.OpenTun(&optionsWrapper) + if err != nil { + return nil, err + } + tunFd := tunInterface.FileDescriptor() + return &nativeTun{ + tunFd: int(tunFd), + tunFile: os.NewFile(uintptr(tunFd), "tun"), + tunMTU: options.MTU, + closer: tunInterface, + }, nil +} + +func (w *platformInterfaceWrapper) Write(p []byte) (n int, err error) { + w.iif.WriteLog(string(p)) + return len(p), nil +} + +func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*process.Info, error) { + var uid int32 + if w.useProcFS { + 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 + } + } + packageName, _ := w.iif.PackageNameByUid(uid) + return &process.Info{UserId: uid, PackageName: packageName}, nil +} diff --git a/experimental/libbox/setup.go b/experimental/libbox/setup.go new file mode 100644 index 00000000..0d0fb035 --- /dev/null +++ b/experimental/libbox/setup.go @@ -0,0 +1,7 @@ +package libbox + +import C "github.com/sagernet/sing-box/constant" + +func SetBasePath(path string) { + C.SetBasePath(path) +} diff --git a/experimental/libbox/tun.go b/experimental/libbox/tun.go new file mode 100644 index 00000000..27912bb9 --- /dev/null +++ b/experimental/libbox/tun.go @@ -0,0 +1,109 @@ +package libbox + +import ( + "io" + "net/netip" + "os" + + "github.com/sagernet/sing-tun" + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +type TunOptions interface { + GetInet4Address() RoutePrefixIterator + GetInet6Address() RoutePrefixIterator + GetDNSServerAddress() (string, error) + GetMTU() int32 + GetAutoRoute() bool + GetStrictRoute() bool + GetInet4RouteAddress() RoutePrefixIterator + GetInet6RouteAddress() RoutePrefixIterator + GetIncludePackage() StringIterator + GetExcludePackage() StringIterator +} + +type RoutePrefix struct { + Address string + Prefix int32 +} + +type RoutePrefixIterator interface { + Next() *RoutePrefix + HasNext() bool +} + +func mapRoutePrefix(prefixes []netip.Prefix) RoutePrefixIterator { + return newIterator(common.Map(prefixes, func(prefix netip.Prefix) *RoutePrefix { + return &RoutePrefix{ + Address: prefix.Addr().String(), + Prefix: int32(prefix.Bits()), + } + })) +} + +var _ TunOptions = (*tunOptions)(nil) + +type tunOptions tun.Options + +func (o *tunOptions) GetInet4Address() RoutePrefixIterator { + return mapRoutePrefix(o.Inet4Address) +} + +func (o *tunOptions) GetInet6Address() RoutePrefixIterator { + return mapRoutePrefix(o.Inet6Address) +} + +func (o *tunOptions) GetDNSServerAddress() (string, error) { + if len(o.Inet4Address) == 0 || o.Inet4Address[0].Bits() == 32 { + return "", E.New("need one more IPv4 address for DNS hijacking") + } + return o.Inet4Address[0].Addr().Next().String(), nil +} + +func (o *tunOptions) GetMTU() int32 { + return int32(o.MTU) +} + +func (o *tunOptions) GetAutoRoute() bool { + return o.AutoRoute +} + +func (o *tunOptions) GetStrictRoute() bool { + return o.StrictRoute +} + +func (o *tunOptions) GetInet4RouteAddress() RoutePrefixIterator { + return mapRoutePrefix(o.Inet4RouteAddress) +} + +func (o *tunOptions) GetInet6RouteAddress() RoutePrefixIterator { + return mapRoutePrefix(o.Inet6RouteAddress) +} + +func (o *tunOptions) GetIncludePackage() StringIterator { + return newIterator(o.IncludePackage) +} + +func (o *tunOptions) GetExcludePackage() StringIterator { + return newIterator(o.ExcludePackage) +} + +type nativeTun struct { + tunFd int + tunFile *os.File + tunMTU uint32 + closer io.Closer +} + +func (t *nativeTun) Read(p []byte) (n int, err error) { + return t.tunFile.Read(p) +} + +func (t *nativeTun) Write(p []byte) (n int, err error) { + return t.tunFile.Write(p) +} + +func (t *nativeTun) Close() error { + return t.closer.Close() +} diff --git a/experimental/libbox/tun_gvisor.go b/experimental/libbox/tun_gvisor.go new file mode 100644 index 00000000..97b807ee --- /dev/null +++ b/experimental/libbox/tun_gvisor.go @@ -0,0 +1,19 @@ +//go:build with_gvisor && linux + +package libbox + +import ( + "github.com/sagernet/sing-tun" + + "gvisor.dev/gvisor/pkg/tcpip/link/fdbased" + "gvisor.dev/gvisor/pkg/tcpip/stack" +) + +var _ tun.GVisorTun = (*nativeTun)(nil) + +func (t *nativeTun) NewEndpoint() (stack.LinkEndpoint, error) { + return fdbased.New(&fdbased.Options{ + FDs: []int{t.tunFd}, + MTU: t.tunMTU, + }) +} diff --git a/go.mod b/go.mod index e0681c20..ad7e2863 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/pires/go-proxyproto v0.6.2 github.com/refraction-networking/utls v1.2.2 github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 + github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32 github.com/sagernet/sing v0.1.7-0.20230209132010-5f1ef3441c13 github.com/sagernet/sing-dns v0.1.2-0.20230209132355-3c2e2957b455 diff --git a/go.sum b/go.sum index 5840dbd9..c81473ac 100644 --- a/go.sum +++ b/go.sum @@ -119,6 +119,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-20221130124640-349ebaa752ca h1:w56+kf8BeqLqllrRJ1tdwKc3sCdWOn/DuNHpY9fAiqs= +github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE= github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM= github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32 h1:tztuJB+giOWNRKQEBVY2oI3PsheTooMdh+/yxemYQYY= diff --git a/inbound/builder.go b/inbound/builder.go index f9e4b18c..727169f4 100644 --- a/inbound/builder.go +++ b/inbound/builder.go @@ -5,18 +5,19 @@ import ( "github.com/sagernet/sing-box/adapter" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" E "github.com/sagernet/sing/common/exceptions" ) -func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.Inbound) (adapter.Inbound, error) { +func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, options option.Inbound, platformInterface platform.Interface) (adapter.Inbound, error) { if options.Type == "" { return nil, E.New("missing inbound type") } switch options.Type { case C.TypeTun: - return NewTun(ctx, router, logger, options.Tag, options.TunOptions) + return NewTun(ctx, router, logger, options.Tag, options.TunOptions, platformInterface) case C.TypeRedirect: return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions), nil case C.TypeTProxy: diff --git a/inbound/tun.go b/inbound/tun.go index eb6341ce..374c3767 100644 --- a/inbound/tun.go +++ b/inbound/tun.go @@ -10,6 +10,7 @@ import ( "github.com/sagernet/sing-box/adapter" "github.com/sagernet/sing-box/common/canceler" C "github.com/sagernet/sing-box/constant" + "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" @@ -34,9 +35,10 @@ type Tun struct { stack string tunIf tun.Tun tunStack tun.Stack + platformInterface platform.Interface } -func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions) (*Tun, error) { +func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) { tunName := options.InterfaceName if tunName == "" { tunName = tun.CalculateInterfaceName("") @@ -93,6 +95,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger endpointIndependentNat: options.EndpointIndependentNat, udpTimeout: udpTimeout, stack: options.Stack, + platformInterface: platformInterface, }, nil } @@ -137,17 +140,25 @@ func (t *Tun) Tag() string { } func (t *Tun) Start() error { - if C.IsAndroid { + if C.IsAndroid && t.platformInterface == nil { t.tunOptions.BuildAndroidRules(t.router.PackageManager(), t) } - tunIf, err := tun.Open(t.tunOptions) + var ( + tunInterface tun.Tun + err error + ) + if t.platformInterface != nil { + tunInterface, err = t.platformInterface.OpenTun(t.tunOptions) + } else { + tunInterface, err = tun.Open(t.tunOptions) + } if err != nil { return E.Cause(err, "configure tun interface") } - t.tunIf = tunIf + t.tunIf = tunInterface t.tunStack, err = tun.NewStack(t.stack, tun.StackOptions{ Context: t.ctx, - Tun: tunIf, + Tun: tunInterface, MTU: t.tunOptions.MTU, Name: t.tunOptions.Name, Inet4Address: t.tunOptions.Inet4Address, diff --git a/log/default.go b/log/default.go index f3acd9b6..5faeb76a 100644 --- a/log/default.go +++ b/log/default.go @@ -12,16 +12,22 @@ import ( var _ Factory = (*simpleFactory)(nil) type simpleFactory struct { - formatter Formatter - writer io.Writer - level Level + formatter Formatter + platformFormatter Formatter + writer io.Writer + platformWriter io.Writer + level Level } -func NewFactory(formatter Formatter, writer io.Writer) Factory { +func NewFactory(formatter Formatter, writer io.Writer, platformWriter io.Writer) Factory { return &simpleFactory{ formatter: formatter, - writer: writer, - level: LevelTrace, + platformFormatter: Formatter{ + BaseTime: formatter.BaseTime, + }, + writer: writer, + platformWriter: platformWriter, + level: LevelTrace, } } @@ -53,7 +59,8 @@ func (l *simpleLogger) Log(ctx context.Context, level Level, args []any) { if level > l.level { return } - message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), time.Now()) + nowTime := time.Now() + message := l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime) if level == LevelPanic { panic(message) } @@ -61,6 +68,9 @@ func (l *simpleLogger) Log(ctx context.Context, level Level, args []any) { if level == LevelFatal { os.Exit(1) } + if l.platformWriter != nil { + l.platformWriter.Write([]byte(l.platformFormatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime))) + } } func (l *simpleLogger) Trace(args ...any) { diff --git a/log/export.go b/log/export.go index 565ae7a7..690a8a1a 100644 --- a/log/export.go +++ b/log/export.go @@ -9,7 +9,7 @@ import ( var std ContextLogger func init() { - std = NewFactory(Formatter{BaseTime: time.Now()}, os.Stderr).Logger() + std = NewFactory(Formatter{BaseTime: time.Now()}, os.Stderr, nil).Logger() } func StdLogger() ContextLogger { diff --git a/log/observable.go b/log/observable.go index 9c20b628..b2c87412 100644 --- a/log/observable.go +++ b/log/observable.go @@ -14,19 +14,25 @@ import ( var _ Factory = (*observableFactory)(nil) type observableFactory struct { - formatter Formatter - writer io.Writer - level Level - subscriber *observable.Subscriber[Entry] - observer *observable.Observer[Entry] + formatter Formatter + platformFormatter Formatter + writer io.Writer + platformWriter io.Writer + level Level + subscriber *observable.Subscriber[Entry] + observer *observable.Observer[Entry] } -func NewObservableFactory(formatter Formatter, writer io.Writer) ObservableFactory { +func NewObservableFactory(formatter Formatter, writer io.Writer, platformWriter io.Writer) ObservableFactory { factory := &observableFactory{ - formatter: formatter, - writer: writer, - level: LevelTrace, - subscriber: observable.NewSubscriber[Entry](128), + formatter: formatter, + platformFormatter: Formatter{ + BaseTime: formatter.BaseTime, + }, + writer: writer, + platformWriter: platformWriter, + level: LevelTrace, + subscriber: observable.NewSubscriber[Entry](128), } factory.observer = observable.NewObserver[Entry](factory.subscriber, 64) return factory @@ -74,7 +80,8 @@ func (l *observableLogger) Log(ctx context.Context, level Level, args []any) { if level > l.level { return } - message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), time.Now()) + nowTime := time.Now() + message, messageSimple := l.formatter.FormatWithSimple(ctx, level, l.tag, F.ToString(args...), nowTime) if level == LevelPanic { panic(message) } @@ -83,6 +90,9 @@ func (l *observableLogger) Log(ctx context.Context, level Level, args []any) { os.Exit(1) } l.subscriber.Emit(Entry{level, messageSimple}) + if l.platformWriter != nil { + l.platformWriter.Write([]byte(l.formatter.Format(ctx, level, l.tag, F.ToString(args...), nowTime))) + } } func (l *observableLogger) Trace(args ...any) { diff --git a/option/config.go b/option/config.go index 54341079..b148aad4 100644 --- a/option/config.go +++ b/option/config.go @@ -5,16 +5,18 @@ import ( "strings" "github.com/sagernet/sing-box/common/json" + "github.com/sagernet/sing-box/experimental/libbox/platform" E "github.com/sagernet/sing/common/exceptions" ) type _Options struct { - Log *LogOptions `json:"log,omitempty"` - DNS *DNSOptions `json:"dns,omitempty"` - Inbounds []Inbound `json:"inbounds,omitempty"` - Outbounds []Outbound `json:"outbounds,omitempty"` - Route *RouteOptions `json:"route,omitempty"` - Experimental *ExperimentalOptions `json:"experimental,omitempty"` + Log *LogOptions `json:"log,omitempty"` + DNS *DNSOptions `json:"dns,omitempty"` + Inbounds []Inbound `json:"inbounds,omitempty"` + Outbounds []Outbound `json:"outbounds,omitempty"` + Route *RouteOptions `json:"route,omitempty"` + Experimental *ExperimentalOptions `json:"experimental,omitempty"` + PlatformInterface platform.Interface `json:"-"` } type Options _Options diff --git a/route/router.go b/route/router.go index 0944741e..1dfbd9ed 100644 --- a/route/router.go +++ b/route/router.go @@ -22,6 +22,7 @@ import ( "github.com/sagernet/sing-box/common/sniff" "github.com/sagernet/sing-box/common/warning" C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/experimental/libbox/platform" "github.com/sagernet/sing-box/log" "github.com/sagernet/sing-box/option" "github.com/sagernet/sing-dns" @@ -97,9 +98,10 @@ type Router struct { processSearcher process.Searcher clashServer adapter.ClashServer v2rayServer adapter.V2RayServer + platformInterface platform.Interface } -func NewRouter(ctx context.Context, logFactory log.Factory, options option.RouteOptions, dnsOptions option.DNSOptions, inbounds []option.Inbound) (*Router, error) { +func NewRouter(ctx context.Context, logFactory log.Factory, options option.RouteOptions, dnsOptions option.DNSOptions, inbounds []option.Inbound, platformInterface platform.Interface) (*Router, error) { if options.DefaultInterface != "" { warnDefaultInterfaceOnUnsupportedPlatform.Check() } @@ -127,6 +129,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route autoDetectInterface: options.AutoDetectInterface, defaultInterface: options.DefaultInterface, defaultMark: options.DefaultMark, + platformInterface: platformInterface, } router.dnsClient = dns.NewClient(dnsOptions.DNSClientOptions.DisableCache, dnsOptions.DNSClientOptions.DisableExpire, router.dnsLogger) for i, ruleOptions := range options.Rules { @@ -248,9 +251,9 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route router.transportMap = transportMap router.transportDomainStrategy = transportDomainStrategy - needInterfaceMonitor := options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool { + needInterfaceMonitor := platformInterface == nil && (options.AutoDetectInterface || common.Any(inbounds, func(inbound option.Inbound) bool { return inbound.HTTPOptions.SetSystemProxy || inbound.MixedOptions.SetSystemProxy || inbound.TunOptions.AutoRoute - }) + })) if needInterfaceMonitor { networkMonitor, err := tun.NewNetworkUpdateMonitor(router) @@ -272,7 +275,7 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route } needFindProcess := hasRule(options.Rules, isProcessRule) || hasDNSRule(dnsOptions.Rules, isProcessDNSRule) || options.FindProcess - needPackageManager := C.IsAndroid && (needFindProcess || common.Any(inbounds, func(inbound option.Inbound) bool { + needPackageManager := C.IsAndroid && platformInterface == nil && (needFindProcess || common.Any(inbounds, func(inbound option.Inbound) bool { return len(inbound.TunOptions.IncludePackage) > 0 || len(inbound.TunOptions.ExcludePackage) > 0 })) if needPackageManager { @@ -283,16 +286,20 @@ func NewRouter(ctx context.Context, logFactory log.Factory, options option.Route router.packageManager = packageManager } if needFindProcess { - searcher, err := process.NewSearcher(process.Config{ - Logger: logFactory.NewLogger("router/process"), - PackageManager: router.packageManager, - }) - if err != nil { - if err != os.ErrInvalid { - router.logger.Warn(E.Cause(err, "create process searcher")) - } + if platformInterface != nil { + router.processSearcher = platformInterface } else { - router.processSearcher = searcher + searcher, err := process.NewSearcher(process.Config{ + Logger: logFactory.NewLogger("router/process"), + PackageManager: router.packageManager, + }) + if err != nil { + if err != os.ErrInvalid { + router.logger.Warn(E.Cause(err, "create process searcher")) + } + } else { + router.processSearcher = searcher + } } } return router, nil @@ -737,6 +744,21 @@ func (r *Router) AutoDetectInterface() bool { return r.autoDetectInterface } +func (r *Router) AutoDetectInterfaceFunc() control.Func { + if r.platformInterface != nil { + return r.platformInterface.AutoDetectInterfaceControl() + } else { + return control.BindToInterfaceFunc(r.InterfaceFinder(), func(network string, address string) (interfaceName string, interfaceIndex int) { + remoteAddr := M.ParseSocksaddr(address).Addr + if C.IsLinux { + return r.InterfaceMonitor().DefaultInterfaceName(remoteAddr), -1 + } else { + return "", r.InterfaceMonitor().DefaultInterfaceIndex(remoteAddr) + } + }) + } +} + func (r *Router) DefaultInterface() string { return r.defaultInterface } @@ -849,6 +871,8 @@ func (r *Router) prepareGeoIPDatabase() error { geoPath = "geoip.db" if foundPath, loaded := C.FindPath(geoPath); loaded { geoPath = foundPath + } else { + geoPath = C.BasePath(geoPath) } } if !rw.FileExists(geoPath) { @@ -861,7 +885,7 @@ func (r *Router) prepareGeoIPDatabase() error { } r.logger.Error("download geoip database: ", err) os.Remove(geoPath) - time.Sleep(10 * time.Second) + // time.Sleep(10 * time.Second) } if err != nil { return err @@ -884,6 +908,8 @@ func (r *Router) prepareGeositeDatabase() error { geoPath = "geosite.db" if foundPath, loaded := C.FindPath(geoPath); loaded { geoPath = foundPath + } else { + geoPath = C.BasePath(geoPath) } } if !rw.FileExists(geoPath) { @@ -896,7 +922,7 @@ func (r *Router) prepareGeositeDatabase() error { } r.logger.Error("download geosite database: ", err) os.Remove(geoPath) - time.Sleep(10 * time.Second) + // time.Sleep(10 * time.Second) } if err != nil { return err @@ -950,6 +976,7 @@ func (r *Router) downloadGeoIPDatabase(savePath string) error { }, }, } + defer httpClient.CloseIdleConnections() response, err := httpClient.Get(downloadURL) if err != nil { return err @@ -997,6 +1024,7 @@ func (r *Router) downloadGeositeDatabase(savePath string) error { }, }, } + defer httpClient.CloseIdleConnections() response, err := httpClient.Get(downloadURL) if err != nil { return err