世界 3 rokov pred
rodič
commit
222196b182

+ 4 - 1
.gitignore

@@ -5,4 +5,7 @@
 /site/
 /bin/
 /dist/
-/sing-box
+/sing-box
+/build/
+/*.jar
+/*.aar

+ 8 - 0
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/[email protected]
+	go install -v github.com/sagernet/gomobile/cmd/[email protected]
+
 clean:
 	rm -rf bin dist sing-box
 	rm -f $(shell go env GOPATH)/sing-box

+ 1 - 0
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

+ 68 - 35
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 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, "]")
+		})
 	}
-	for _, out := range s.outbounds {
-		common.Close(out)
+	if err := common.Close(s.router); err != nil {
+		errors = E.Append(errors, err, func(err error) error {
+			return E.Cause(err, "close router")
+		})
 	}
-	return common.Close(
-		s.router,
-		s.logFactory,
-		s.clashServer,
-		s.v2rayServer,
-		common.PtrOrNil(s.logFile),
-	)
+	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 {

+ 2 - 1
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

+ 61 - 0
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)
+	}
+}

+ 13 - 2
cmd/internal/build/sdk.go → 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
+}

+ 1 - 9
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() != "" {

+ 16 - 1
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)

+ 6 - 4
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,
 	)

+ 15 - 0
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
+}

+ 148 - 0
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
+		}
+	}
+}

+ 31 - 0
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
+}

+ 16 - 0
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
+}

+ 16 - 0
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
+}

+ 35 - 0
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()
+}

+ 21 - 0
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
+}

+ 120 - 0
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
+}

+ 7 - 0
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)
+}

+ 109 - 0
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()
+}

+ 19 - 0
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,
+	})
+}

+ 1 - 0
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

+ 2 - 0
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=

+ 3 - 2
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:

+ 16 - 5
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,

+ 17 - 7
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) {

+ 1 - 1
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 {

+ 21 - 11
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) {

+ 8 - 6
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

+ 43 - 15
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