Restructure repo around sing-box-rr package layout
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
//go:build android
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"debug/buildinfo"
|
||||
"io"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
const (
|
||||
androidVPNCoreTypeOpenVPN = "OpenVPN"
|
||||
androidVPNCoreTypeShadowsocks = "Shadowsocks"
|
||||
androidVPNCoreTypeClash = "Clash"
|
||||
androidVPNCoreTypeV2Ray = "V2Ray"
|
||||
androidVPNCoreTypeWireGuard = "WireGuard"
|
||||
androidVPNCoreTypeSingBox = "sing-box"
|
||||
androidVPNCoreTypeUnknown = "Unknown"
|
||||
)
|
||||
|
||||
type AndroidVPNType struct {
|
||||
CoreType string
|
||||
CorePath string
|
||||
GoVersion string
|
||||
}
|
||||
|
||||
func ReadAndroidVPNType(publicSourceDirList StringIterator) (*AndroidVPNType, error) {
|
||||
apkPathList := iteratorToArray[string](publicSourceDirList)
|
||||
var lastError error
|
||||
for _, apkPath := range apkPathList {
|
||||
androidVPNType, err := readAndroidVPNType(apkPath)
|
||||
if androidVPNType == nil {
|
||||
if err != nil {
|
||||
lastError = err
|
||||
}
|
||||
continue
|
||||
}
|
||||
return androidVPNType, nil
|
||||
}
|
||||
return nil, lastError
|
||||
}
|
||||
|
||||
func readAndroidVPNType(publicSourceDir string) (*AndroidVPNType, error) {
|
||||
reader, err := zip.OpenReader(publicSourceDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer reader.Close()
|
||||
var lastError error
|
||||
for _, file := range reader.File {
|
||||
if !strings.HasPrefix(file.Name, "lib/") {
|
||||
continue
|
||||
}
|
||||
vpnType, err := readAndroidVPNTypeEntry(file)
|
||||
if err != nil {
|
||||
lastError = err
|
||||
continue
|
||||
}
|
||||
return vpnType, nil
|
||||
}
|
||||
for _, file := range reader.File {
|
||||
if !strings.HasPrefix(file.Name, "lib/") {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(file.Name, androidVPNCoreTypeOpenVPN) || strings.Contains(file.Name, "ovpn") {
|
||||
return &AndroidVPNType{CoreType: androidVPNCoreTypeOpenVPN}, nil
|
||||
}
|
||||
if strings.Contains(file.Name, androidVPNCoreTypeShadowsocks) {
|
||||
return &AndroidVPNType{CoreType: androidVPNCoreTypeShadowsocks}, nil
|
||||
}
|
||||
}
|
||||
return nil, lastError
|
||||
}
|
||||
|
||||
func readAndroidVPNTypeEntry(zipFile *zip.File) (*AndroidVPNType, error) {
|
||||
readCloser, err := zipFile.Open()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
libContent := make([]byte, zipFile.UncompressedSize64)
|
||||
_, err = io.ReadFull(readCloser, libContent)
|
||||
readCloser.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buildInfo, err := buildinfo.Read(bytes.NewReader(libContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var vpnType AndroidVPNType
|
||||
vpnType.GoVersion = buildInfo.GoVersion
|
||||
if !strings.HasPrefix(vpnType.GoVersion, "go") {
|
||||
vpnType.GoVersion = "obfuscated"
|
||||
} else {
|
||||
vpnType.GoVersion = vpnType.GoVersion[2:]
|
||||
}
|
||||
vpnType.CoreType = androidVPNCoreTypeUnknown
|
||||
if len(buildInfo.Deps) == 0 {
|
||||
vpnType.CoreType = "obfuscated"
|
||||
return &vpnType, nil
|
||||
}
|
||||
|
||||
dependencies := make(map[string]bool)
|
||||
dependencies[buildInfo.Path] = true
|
||||
for _, module := range buildInfo.Deps {
|
||||
dependencies[module.Path] = true
|
||||
if module.Replace != nil {
|
||||
dependencies[module.Replace.Path] = true
|
||||
}
|
||||
}
|
||||
for dependency := range dependencies {
|
||||
pkgType, loaded := determinePkgType(dependency)
|
||||
if loaded {
|
||||
vpnType.CoreType = pkgType
|
||||
}
|
||||
}
|
||||
if vpnType.CoreType == androidVPNCoreTypeUnknown {
|
||||
for dependency := range dependencies {
|
||||
pkgType, loaded := determinePkgTypeSecondary(dependency)
|
||||
if loaded {
|
||||
vpnType.CoreType = pkgType
|
||||
return &vpnType, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if vpnType.CoreType != androidVPNCoreTypeUnknown {
|
||||
vpnType.CorePath, _ = determineCorePath(buildInfo, vpnType.CoreType)
|
||||
return &vpnType, nil
|
||||
}
|
||||
if dependencies["github.com/golang/protobuf"] && dependencies["github.com/v2fly/ss-bloomring"] {
|
||||
vpnType.CoreType = androidVPNCoreTypeV2Ray
|
||||
return &vpnType, nil
|
||||
}
|
||||
return &vpnType, nil
|
||||
}
|
||||
|
||||
func determinePkgType(pkgName string) (string, bool) {
|
||||
pkgNameLower := strings.ToLower(pkgName)
|
||||
if strings.Contains(pkgNameLower, "clash") {
|
||||
return androidVPNCoreTypeClash, true
|
||||
}
|
||||
if strings.Contains(pkgNameLower, "v2ray") || strings.Contains(pkgNameLower, "xray") {
|
||||
return androidVPNCoreTypeV2Ray, true
|
||||
}
|
||||
|
||||
if strings.Contains(pkgNameLower, "sing-box") {
|
||||
return androidVPNCoreTypeSingBox, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func determinePkgTypeSecondary(pkgName string) (string, bool) {
|
||||
pkgNameLower := strings.ToLower(pkgName)
|
||||
if strings.Contains(pkgNameLower, "wireguard") {
|
||||
return androidVPNCoreTypeWireGuard, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func determineCorePath(pkgInfo *buildinfo.BuildInfo, pkgType string) (string, bool) {
|
||||
switch pkgType {
|
||||
case androidVPNCoreTypeClash:
|
||||
return determineCorePathForPkgs(pkgInfo, []string{"github.com/Dreamacro/clash"}, []string{"clash"})
|
||||
case androidVPNCoreTypeV2Ray:
|
||||
if v2rayVersion, loaded := determineCorePathForPkgs(pkgInfo, []string{
|
||||
"github.com/v2fly/v2ray-core",
|
||||
"github.com/v2fly/v2ray-core/v4",
|
||||
"github.com/v2fly/v2ray-core/v5",
|
||||
}, []string{
|
||||
"v2ray",
|
||||
}); loaded {
|
||||
return v2rayVersion, true
|
||||
}
|
||||
if xrayVersion, loaded := determineCorePathForPkgs(pkgInfo, []string{
|
||||
"github.com/xtls/xray-core",
|
||||
}, []string{
|
||||
"xray",
|
||||
}); loaded {
|
||||
return xrayVersion, true
|
||||
}
|
||||
return "", false
|
||||
case androidVPNCoreTypeSingBox:
|
||||
return determineCorePathForPkgs(pkgInfo, []string{"github.com/sagernet/sing-box"}, []string{"sing-box"})
|
||||
case androidVPNCoreTypeWireGuard:
|
||||
return determineCorePathForPkgs(pkgInfo, []string{"golang.zx2c4.com/wireguard"}, []string{"wireguard"})
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func determineCorePathForPkgs(pkgInfo *buildinfo.BuildInfo, pkgs []string, names []string) (string, bool) {
|
||||
for _, pkg := range pkgs {
|
||||
if pkgInfo.Path == pkg {
|
||||
return pkg, true
|
||||
}
|
||||
strictDependency := common.Find(pkgInfo.Deps, func(module *debug.Module) bool {
|
||||
return module.Path == pkg
|
||||
})
|
||||
if strictDependency != nil {
|
||||
if isValidVersion(strictDependency.Version) {
|
||||
return strictDependency.Path + " " + strictDependency.Version, true
|
||||
} else {
|
||||
return strictDependency.Path, true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, name := range names {
|
||||
if strings.Contains(pkgInfo.Path, name) {
|
||||
return pkgInfo.Path, true
|
||||
}
|
||||
looseDependency := common.Find(pkgInfo.Deps, func(module *debug.Module) bool {
|
||||
return strings.Contains(module.Path, name) || (module.Replace != nil && strings.Contains(module.Replace.Path, name))
|
||||
})
|
||||
if looseDependency != nil {
|
||||
return looseDependency.Path, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func isValidVersion(version string) bool {
|
||||
if version == "(devel)" {
|
||||
return false
|
||||
}
|
||||
if strings.Contains(version, "v0.0.0") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package libbox
|
||||
|
||||
const (
|
||||
CommandLog int32 = iota
|
||||
CommandStatus
|
||||
CommandGroup
|
||||
CommandClashMode
|
||||
CommandConnections
|
||||
)
|
||||
@@ -0,0 +1,578 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
type CommandClient struct {
|
||||
handler CommandClientHandler
|
||||
grpcConn *grpc.ClientConn
|
||||
grpcClient daemon.StartedServiceClient
|
||||
options CommandClientOptions
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
clientMutex sync.RWMutex
|
||||
standalone bool
|
||||
}
|
||||
|
||||
type CommandClientOptions struct {
|
||||
commands []int32
|
||||
StatusInterval int64
|
||||
}
|
||||
|
||||
func (o *CommandClientOptions) AddCommand(command int32) {
|
||||
o.commands = append(o.commands, command)
|
||||
}
|
||||
|
||||
type CommandClientHandler interface {
|
||||
Connected()
|
||||
Disconnected(message string)
|
||||
SetDefaultLogLevel(level int32)
|
||||
ClearLogs()
|
||||
WriteLogs(messageList LogIterator)
|
||||
WriteStatus(message *StatusMessage)
|
||||
WriteGroups(message OutboundGroupIterator)
|
||||
InitializeClashMode(modeList StringIterator, currentMode string)
|
||||
UpdateClashMode(newMode string)
|
||||
WriteConnectionEvents(events *ConnectionEvents)
|
||||
}
|
||||
|
||||
type LogEntry struct {
|
||||
Level int32
|
||||
Message string
|
||||
}
|
||||
|
||||
type LogIterator interface {
|
||||
Len() int32
|
||||
HasNext() bool
|
||||
Next() *LogEntry
|
||||
}
|
||||
|
||||
type XPCDialer interface {
|
||||
DialXPC() (int32, error)
|
||||
}
|
||||
|
||||
var sXPCDialer XPCDialer
|
||||
|
||||
func SetXPCDialer(dialer XPCDialer) {
|
||||
sXPCDialer = dialer
|
||||
}
|
||||
|
||||
func NewStandaloneCommandClient() *CommandClient {
|
||||
return &CommandClient{standalone: true}
|
||||
}
|
||||
|
||||
func NewCommandClient(handler CommandClientHandler, options *CommandClientOptions) *CommandClient {
|
||||
return &CommandClient{
|
||||
handler: handler,
|
||||
options: common.PtrValueOrDefault(options),
|
||||
}
|
||||
}
|
||||
|
||||
func unaryClientAuthInterceptor(ctx context.Context, method string, req, reply any, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
if sCommandServerSecret != "" {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "x-command-secret", sCommandServerSecret)
|
||||
}
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
|
||||
func streamClientAuthInterceptor(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
if sCommandServerSecret != "" {
|
||||
ctx = metadata.AppendToOutgoingContext(ctx, "x-command-secret", sCommandServerSecret)
|
||||
}
|
||||
return streamer(ctx, desc, cc, method, opts...)
|
||||
}
|
||||
|
||||
const (
|
||||
commandClientDialAttempts = 10
|
||||
commandClientDialBaseDelay = 100 * time.Millisecond
|
||||
commandClientDialStepDelay = 50 * time.Millisecond
|
||||
)
|
||||
|
||||
func commandClientDialDelay(attempt int) time.Duration {
|
||||
return commandClientDialBaseDelay + time.Duration(attempt)*commandClientDialStepDelay
|
||||
}
|
||||
|
||||
func dialTarget() (string, func(context.Context, string) (net.Conn, error)) {
|
||||
if sXPCDialer != nil {
|
||||
return "passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
fileDescriptor, err := sXPCDialer.DialXPC()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return networkConnectionFromFileDescriptor(fileDescriptor)
|
||||
}
|
||||
}
|
||||
if sCommandServerListenPort == 0 {
|
||||
socketPath := filepath.Join(sBasePath, "command.sock")
|
||||
return "passthrough:///command-socket", func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
var networkDialer net.Dialer
|
||||
return networkDialer.DialContext(ctx, "unix", socketPath)
|
||||
}
|
||||
}
|
||||
return net.JoinHostPort("127.0.0.1", strconv.Itoa(int(sCommandServerListenPort))), nil
|
||||
}
|
||||
|
||||
func networkConnectionFromFileDescriptor(fileDescriptor int32) (net.Conn, error) {
|
||||
file := os.NewFile(uintptr(fileDescriptor), "xpc-command-socket")
|
||||
if file == nil {
|
||||
return nil, E.New("invalid file descriptor")
|
||||
}
|
||||
networkConnection, err := net.FileConn(file)
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, E.Cause(err, "create connection from fd")
|
||||
}
|
||||
file.Close()
|
||||
return networkConnection, nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) dialWithRetry(target string, contextDialer func(context.Context, string) (net.Conn, error), retryDial bool) (*grpc.ClientConn, daemon.StartedServiceClient, error) {
|
||||
var connection *grpc.ClientConn
|
||||
var client daemon.StartedServiceClient
|
||||
var lastError error
|
||||
|
||||
for attempt := 0; attempt < commandClientDialAttempts; attempt++ {
|
||||
if connection == nil {
|
||||
options := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithUnaryInterceptor(unaryClientAuthInterceptor),
|
||||
grpc.WithStreamInterceptor(streamClientAuthInterceptor),
|
||||
}
|
||||
if contextDialer != nil {
|
||||
options = append(options, grpc.WithContextDialer(contextDialer))
|
||||
}
|
||||
var err error
|
||||
connection, err = grpc.NewClient(target, options...)
|
||||
if err != nil {
|
||||
lastError = err
|
||||
if !retryDial {
|
||||
return nil, nil, err
|
||||
}
|
||||
time.Sleep(commandClientDialDelay(attempt))
|
||||
continue
|
||||
}
|
||||
client = daemon.NewStartedServiceClient(connection)
|
||||
}
|
||||
waitDuration := commandClientDialDelay(attempt)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), waitDuration)
|
||||
_, err := client.GetStartedAt(ctx, &emptypb.Empty{}, grpc.WaitForReady(true))
|
||||
cancel()
|
||||
if err == nil {
|
||||
return connection, client, nil
|
||||
}
|
||||
lastError = err
|
||||
}
|
||||
|
||||
if connection != nil {
|
||||
connection.Close()
|
||||
}
|
||||
return nil, nil, lastError
|
||||
}
|
||||
|
||||
func (c *CommandClient) Connect() error {
|
||||
c.clientMutex.Lock()
|
||||
common.Close(common.PtrOrNil(c.grpcConn))
|
||||
|
||||
target, contextDialer := dialTarget()
|
||||
connection, client, err := c.dialWithRetry(target, contextDialer, true)
|
||||
if err != nil {
|
||||
c.clientMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
c.grpcConn = connection
|
||||
c.grpcClient = client
|
||||
c.ctx, c.cancel = context.WithCancel(context.Background())
|
||||
c.clientMutex.Unlock()
|
||||
|
||||
c.handler.Connected()
|
||||
return c.dispatchCommands()
|
||||
}
|
||||
|
||||
func (c *CommandClient) ConnectWithFD(fd int32) error {
|
||||
c.clientMutex.Lock()
|
||||
common.Close(common.PtrOrNil(c.grpcConn))
|
||||
|
||||
networkConnection, err := networkConnectionFromFileDescriptor(fd)
|
||||
if err != nil {
|
||||
c.clientMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
connection, client, err := c.dialWithRetry("passthrough:///xpc", func(ctx context.Context, _ string) (net.Conn, error) {
|
||||
return networkConnection, nil
|
||||
}, false)
|
||||
if err != nil {
|
||||
networkConnection.Close()
|
||||
c.clientMutex.Unlock()
|
||||
return err
|
||||
}
|
||||
c.grpcConn = connection
|
||||
c.grpcClient = client
|
||||
c.ctx, c.cancel = context.WithCancel(context.Background())
|
||||
c.clientMutex.Unlock()
|
||||
|
||||
c.handler.Connected()
|
||||
return c.dispatchCommands()
|
||||
}
|
||||
|
||||
func (c *CommandClient) dispatchCommands() error {
|
||||
for _, command := range c.options.commands {
|
||||
switch command {
|
||||
case CommandLog:
|
||||
go c.handleLogStream()
|
||||
case CommandStatus:
|
||||
go c.handleStatusStream()
|
||||
case CommandGroup:
|
||||
go c.handleGroupStream()
|
||||
case CommandClashMode:
|
||||
go c.handleClashModeStream()
|
||||
case CommandConnections:
|
||||
go c.handleConnectionsStream()
|
||||
default:
|
||||
return E.New("unknown command: ", command)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) Disconnect() error {
|
||||
c.clientMutex.Lock()
|
||||
defer c.clientMutex.Unlock()
|
||||
if c.cancel != nil {
|
||||
c.cancel()
|
||||
}
|
||||
return common.Close(common.PtrOrNil(c.grpcConn))
|
||||
}
|
||||
|
||||
func (c *CommandClient) getClientForCall() (daemon.StartedServiceClient, error) {
|
||||
c.clientMutex.RLock()
|
||||
if c.grpcClient != nil {
|
||||
defer c.clientMutex.RUnlock()
|
||||
return c.grpcClient, nil
|
||||
}
|
||||
c.clientMutex.RUnlock()
|
||||
|
||||
c.clientMutex.Lock()
|
||||
defer c.clientMutex.Unlock()
|
||||
|
||||
if c.grpcClient != nil {
|
||||
return c.grpcClient, nil
|
||||
}
|
||||
|
||||
target, contextDialer := dialTarget()
|
||||
connection, client, err := c.dialWithRetry(target, contextDialer, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.grpcConn = connection
|
||||
c.grpcClient = client
|
||||
if c.ctx == nil {
|
||||
c.ctx, c.cancel = context.WithCancel(context.Background())
|
||||
}
|
||||
return c.grpcClient, nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) closeConnection() {
|
||||
c.clientMutex.Lock()
|
||||
defer c.clientMutex.Unlock()
|
||||
if c.grpcConn != nil {
|
||||
c.grpcConn.Close()
|
||||
c.grpcConn = nil
|
||||
c.grpcClient = nil
|
||||
}
|
||||
}
|
||||
|
||||
func callWithResult[T any](c *CommandClient, call func(client daemon.StartedServiceClient) (T, error)) (T, error) {
|
||||
client, err := c.getClientForCall()
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, err
|
||||
}
|
||||
if c.standalone {
|
||||
defer c.closeConnection()
|
||||
}
|
||||
return call(client)
|
||||
}
|
||||
|
||||
func (c *CommandClient) getStreamContext() (daemon.StartedServiceClient, context.Context) {
|
||||
c.clientMutex.RLock()
|
||||
defer c.clientMutex.RUnlock()
|
||||
return c.grpcClient, c.ctx
|
||||
}
|
||||
|
||||
func (c *CommandClient) handleLogStream() {
|
||||
client, ctx := c.getStreamContext()
|
||||
stream, err := client.SubscribeLog(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
defaultLogLevel, err := client.GetDefaultLogLevel(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
c.handler.SetDefaultLogLevel(int32(defaultLogLevel.Level))
|
||||
for {
|
||||
logMessage, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
if logMessage.Reset_ {
|
||||
c.handler.ClearLogs()
|
||||
}
|
||||
var messages []*LogEntry
|
||||
for _, msg := range logMessage.Messages {
|
||||
messages = append(messages, &LogEntry{
|
||||
Level: int32(msg.Level),
|
||||
Message: msg.Message,
|
||||
})
|
||||
}
|
||||
c.handler.WriteLogs(newIterator(messages))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) handleStatusStream() {
|
||||
client, ctx := c.getStreamContext()
|
||||
interval := c.options.StatusInterval
|
||||
|
||||
stream, err := client.SubscribeStatus(ctx, &daemon.SubscribeStatusRequest{
|
||||
Interval: interval,
|
||||
})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
status, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
c.handler.WriteStatus(statusMessageFromGRPC(status))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) handleGroupStream() {
|
||||
client, ctx := c.getStreamContext()
|
||||
|
||||
stream, err := client.SubscribeGroups(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
groups, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
c.handler.WriteGroups(outboundGroupIteratorFromGRPC(groups))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) handleClashModeStream() {
|
||||
client, ctx := c.getStreamContext()
|
||||
|
||||
modeStatus, err := client.GetClashModeStatus(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if sFixAndroidStack {
|
||||
go func() {
|
||||
c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
|
||||
if len(modeStatus.ModeList) == 0 {
|
||||
c.handler.Disconnected(os.ErrInvalid.Error())
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
|
||||
if len(modeStatus.ModeList) == 0 {
|
||||
c.handler.Disconnected(os.ErrInvalid.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(modeStatus.ModeList) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
stream, err := client.SubscribeClashMode(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
mode, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
c.handler.UpdateClashMode(mode.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) handleConnectionsStream() {
|
||||
client, ctx := c.getStreamContext()
|
||||
interval := c.options.StatusInterval
|
||||
|
||||
stream, err := client.SubscribeConnections(ctx, &daemon.SubscribeConnectionsRequest{
|
||||
Interval: interval,
|
||||
})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
events, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(err.Error())
|
||||
return
|
||||
}
|
||||
libboxEvents := connectionEventsFromGRPC(events)
|
||||
c.handler.WriteConnectionEvents(libboxEvents)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) SelectOutbound(groupTag string, outboundTag string) error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.SelectOutbound(context.Background(), &daemon.SelectOutboundRequest{
|
||||
GroupTag: groupTag,
|
||||
OutboundTag: outboundTag,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CommandClient) URLTest(groupTag string) error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.URLTest(context.Background(), &daemon.URLTestRequest{
|
||||
OutboundTag: groupTag,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CommandClient) SetClashMode(newMode string) error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.SetClashMode(context.Background(), &daemon.ClashMode{
|
||||
Mode: newMode,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CommandClient) CloseConnection(connId string) error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.CloseConnection(context.Background(), &daemon.CloseConnectionRequest{
|
||||
Id: connId,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CommandClient) CloseConnections() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.CloseAllConnections(context.Background(), &emptypb.Empty{})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CommandClient) ServiceReload() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.ReloadService(context.Background(), &emptypb.Empty{})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CommandClient) ServiceClose() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.StopService(context.Background(), &emptypb.Empty{})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CommandClient) ClearLogs() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.ClearLogs(context.Background(), &emptypb.Empty{})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CommandClient) GetSystemProxyStatus() (*SystemProxyStatus, error) {
|
||||
return callWithResult(c, func(client daemon.StartedServiceClient) (*SystemProxyStatus, error) {
|
||||
status, err := client.GetSystemProxyStatus(context.Background(), &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return systemProxyStatusFromGRPC(status), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CommandClient) SetSystemProxyEnabled(isEnabled bool) error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.SetSystemProxyEnabled(context.Background(), &daemon.SetSystemProxyEnabledRequest{
|
||||
Enabled: isEnabled,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *CommandClient) GetDeprecatedNotes() (DeprecatedNoteIterator, error) {
|
||||
return callWithResult(c, func(client daemon.StartedServiceClient) (DeprecatedNoteIterator, error) {
|
||||
warnings, err := client.GetDeprecatedWarnings(context.Background(), &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var notes []*DeprecatedNote
|
||||
for _, warning := range warnings.Warnings {
|
||||
notes = append(notes, &DeprecatedNote{
|
||||
Description: warning.Message,
|
||||
MigrationLink: warning.MigrationLink,
|
||||
})
|
||||
}
|
||||
return newIterator(notes), nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CommandClient) GetStartedAt() (int64, error) {
|
||||
return callWithResult(c, func(client daemon.StartedServiceClient) (int64, error) {
|
||||
startedAt, err := client.GetStartedAt(context.Background(), &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return startedAt.StartedAt, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *CommandClient) SetGroupExpand(groupTag string, isExpand bool) error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.SetGroupExpand(context.Background(), &daemon.SetGroupExpandRequest{
|
||||
GroupTag: groupTag,
|
||||
IsExpand: isExpand,
|
||||
})
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type CommandServer struct {
|
||||
*daemon.StartedService
|
||||
handler CommandServerHandler
|
||||
platformInterface PlatformInterface
|
||||
platformWrapper *platformInterfaceWrapper
|
||||
grpcServer *grpc.Server
|
||||
listener net.Listener
|
||||
endPauseTimer *time.Timer
|
||||
}
|
||||
|
||||
type CommandServerHandler interface {
|
||||
ServiceStop() error
|
||||
ServiceReload() error
|
||||
GetSystemProxyStatus() (*SystemProxyStatus, error)
|
||||
SetSystemProxyEnabled(enabled bool) error
|
||||
WriteDebugMessage(message string)
|
||||
}
|
||||
|
||||
func NewCommandServer(handler CommandServerHandler, platformInterface PlatformInterface) (*CommandServer, error) {
|
||||
ctx := baseContext(platformInterface)
|
||||
platformWrapper := &platformInterfaceWrapper{
|
||||
iif: platformInterface,
|
||||
useProcFS: platformInterface.UseProcFS(),
|
||||
}
|
||||
service.MustRegister[adapter.PlatformInterface](ctx, platformWrapper)
|
||||
server := &CommandServer{
|
||||
handler: handler,
|
||||
platformInterface: platformInterface,
|
||||
platformWrapper: platformWrapper,
|
||||
}
|
||||
server.StartedService = daemon.NewStartedService(daemon.ServiceOptions{
|
||||
Context: ctx,
|
||||
// Platform: platformWrapper,
|
||||
Handler: (*platformHandler)(server),
|
||||
Debug: sDebug,
|
||||
LogMaxLines: sLogMaxLines,
|
||||
OOMKiller: memoryLimitEnabled,
|
||||
// WorkingDirectory: sWorkingPath,
|
||||
// TempDirectory: sTempPath,
|
||||
// UserID: sUserID,
|
||||
// GroupID: sGroupID,
|
||||
// SystemProxyEnabled: false,
|
||||
})
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func unaryAuthInterceptor(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
|
||||
if sCommandServerSecret == "" {
|
||||
return handler(ctx, req)
|
||||
}
|
||||
md, ok := metadata.FromIncomingContext(ctx)
|
||||
if !ok {
|
||||
return nil, status.Error(codes.Unauthenticated, "missing metadata")
|
||||
}
|
||||
values := md.Get("x-command-secret")
|
||||
if len(values) == 0 {
|
||||
return nil, status.Error(codes.Unauthenticated, "missing authentication secret")
|
||||
}
|
||||
if values[0] != sCommandServerSecret {
|
||||
return nil, status.Error(codes.Unauthenticated, "invalid authentication secret")
|
||||
}
|
||||
return handler(ctx, req)
|
||||
}
|
||||
|
||||
func streamAuthInterceptor(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
|
||||
if sCommandServerSecret == "" {
|
||||
return handler(srv, ss)
|
||||
}
|
||||
md, ok := metadata.FromIncomingContext(ss.Context())
|
||||
if !ok {
|
||||
return status.Error(codes.Unauthenticated, "missing metadata")
|
||||
}
|
||||
values := md.Get("x-command-secret")
|
||||
if len(values) == 0 {
|
||||
return status.Error(codes.Unauthenticated, "missing authentication secret")
|
||||
}
|
||||
if values[0] != sCommandServerSecret {
|
||||
return status.Error(codes.Unauthenticated, "invalid authentication secret")
|
||||
}
|
||||
return handler(srv, ss)
|
||||
}
|
||||
|
||||
func (s *CommandServer) Start() error {
|
||||
var (
|
||||
listener net.Listener
|
||||
err error
|
||||
)
|
||||
if sCommandServerListenPort == 0 {
|
||||
sockPath := filepath.Join(sBasePath, "command.sock")
|
||||
os.Remove(sockPath)
|
||||
for i := 0; i < 30; i++ {
|
||||
listener, err = net.ListenUnix("unix", &net.UnixAddr{
|
||||
Name: sockPath,
|
||||
Net: "unix",
|
||||
})
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if !errors.Is(err, syscall.EROFS) {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
if err != nil {
|
||||
return E.Cause(err, "listen command server")
|
||||
}
|
||||
if sUserID != os.Getuid() {
|
||||
err = os.Chown(sockPath, sUserID, sGroupID)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
os.Remove(sockPath)
|
||||
return E.Cause(err, "chown")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listener, err = net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(sCommandServerListenPort))))
|
||||
if err != nil {
|
||||
return E.Cause(err, "listen command server")
|
||||
}
|
||||
}
|
||||
s.listener = listener
|
||||
serverOptions := []grpc.ServerOption{
|
||||
grpc.UnaryInterceptor(unaryAuthInterceptor),
|
||||
grpc.StreamInterceptor(streamAuthInterceptor),
|
||||
}
|
||||
s.grpcServer = grpc.NewServer(serverOptions...)
|
||||
daemon.RegisterStartedServiceServer(s.grpcServer, s.StartedService)
|
||||
go s.grpcServer.Serve(listener)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *CommandServer) Close() {
|
||||
if s.grpcServer != nil {
|
||||
s.grpcServer.Stop()
|
||||
}
|
||||
common.Close(s.listener)
|
||||
s.StartedService.Close()
|
||||
}
|
||||
|
||||
type OverrideOptions struct {
|
||||
AutoRedirect bool
|
||||
IncludePackage StringIterator
|
||||
ExcludePackage StringIterator
|
||||
}
|
||||
|
||||
func (s *CommandServer) StartOrReloadService(configContent string, options *OverrideOptions) error {
|
||||
return s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{
|
||||
AutoRedirect: options.AutoRedirect,
|
||||
IncludePackage: iteratorToArray(options.IncludePackage),
|
||||
ExcludePackage: iteratorToArray(options.ExcludePackage),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *CommandServer) CloseService() error {
|
||||
return s.StartedService.CloseService()
|
||||
}
|
||||
|
||||
func (s *CommandServer) WriteMessage(level int32, message string) {
|
||||
s.StartedService.WriteMessage(log.Level(level), message)
|
||||
}
|
||||
|
||||
func (s *CommandServer) SetError(message string) {
|
||||
s.StartedService.SetError(E.New(message))
|
||||
}
|
||||
|
||||
func (s *CommandServer) NeedWIFIState() bool {
|
||||
instance := s.StartedService.Instance()
|
||||
if instance == nil || instance.Box() == nil {
|
||||
return false
|
||||
}
|
||||
return instance.Box().Network().NeedWIFIState()
|
||||
}
|
||||
|
||||
func (s *CommandServer) NeedFindProcess() bool {
|
||||
instance := s.StartedService.Instance()
|
||||
if instance == nil || instance.Box() == nil {
|
||||
return false
|
||||
}
|
||||
return instance.Box().Router().NeedFindProcess()
|
||||
}
|
||||
|
||||
func (s *CommandServer) Pause() {
|
||||
instance := s.StartedService.Instance()
|
||||
if instance == nil || instance.PauseManager() == nil {
|
||||
return
|
||||
}
|
||||
instance.PauseManager().DevicePause()
|
||||
if C.IsIos {
|
||||
if s.endPauseTimer == nil {
|
||||
s.endPauseTimer = time.AfterFunc(time.Minute, instance.PauseManager().DeviceWake)
|
||||
} else {
|
||||
s.endPauseTimer.Reset(time.Minute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CommandServer) Wake() {
|
||||
instance := s.StartedService.Instance()
|
||||
if instance == nil || instance.PauseManager() == nil {
|
||||
return
|
||||
}
|
||||
if !C.IsIos {
|
||||
instance.PauseManager().DeviceWake()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *CommandServer) ResetNetwork() {
|
||||
instance := s.StartedService.Instance()
|
||||
if instance == nil || instance.Box() == nil {
|
||||
return
|
||||
}
|
||||
instance.Box().Router().ResetNetwork()
|
||||
}
|
||||
|
||||
func (s *CommandServer) UpdateWIFIState() {
|
||||
instance := s.StartedService.Instance()
|
||||
if instance == nil || instance.Box() == nil {
|
||||
return
|
||||
}
|
||||
instance.Box().Network().UpdateWIFIState()
|
||||
}
|
||||
|
||||
type platformHandler CommandServer
|
||||
|
||||
func (h *platformHandler) ServiceStop() error {
|
||||
return (*CommandServer)(h).handler.ServiceStop()
|
||||
}
|
||||
|
||||
func (h *platformHandler) ServiceReload() error {
|
||||
return (*CommandServer)(h).handler.ServiceReload()
|
||||
}
|
||||
|
||||
func (h *platformHandler) SystemProxyStatus() (*daemon.SystemProxyStatus, error) {
|
||||
status, err := (*CommandServer)(h).handler.GetSystemProxyStatus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &daemon.SystemProxyStatus{
|
||||
Enabled: status.Enabled,
|
||||
Available: status.Available,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *platformHandler) SetSystemProxyEnabled(enabled bool) error {
|
||||
return (*CommandServer)(h).handler.SetSystemProxyEnabled(enabled)
|
||||
}
|
||||
|
||||
func (h *platformHandler) WriteDebugMessage(message string) {
|
||||
(*CommandServer)(h).handler.WriteDebugMessage(message)
|
||||
}
|
||||
@@ -0,0 +1,430 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/daemon"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
)
|
||||
|
||||
type StatusMessage struct {
|
||||
Memory int64
|
||||
Goroutines int32
|
||||
ConnectionsIn int32
|
||||
ConnectionsOut int32
|
||||
TrafficAvailable bool
|
||||
Uplink int64
|
||||
Downlink int64
|
||||
UplinkTotal int64
|
||||
DownlinkTotal int64
|
||||
}
|
||||
|
||||
type SystemProxyStatus struct {
|
||||
Available bool
|
||||
Enabled bool
|
||||
}
|
||||
|
||||
type OutboundGroup struct {
|
||||
Tag string
|
||||
Type string
|
||||
Selectable bool
|
||||
Selected string
|
||||
IsExpand bool
|
||||
itemList []*OutboundGroupItem
|
||||
}
|
||||
|
||||
func (g *OutboundGroup) GetItems() OutboundGroupItemIterator {
|
||||
return newIterator(g.itemList)
|
||||
}
|
||||
|
||||
type OutboundGroupIterator interface {
|
||||
Next() *OutboundGroup
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type OutboundGroupItem struct {
|
||||
Tag string
|
||||
Type string
|
||||
URLTestTime int64
|
||||
URLTestDelay int32
|
||||
}
|
||||
|
||||
type OutboundGroupItemIterator interface {
|
||||
Next() *OutboundGroupItem
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
const (
|
||||
ConnectionStateAll = iota
|
||||
ConnectionStateActive
|
||||
ConnectionStateClosed
|
||||
)
|
||||
|
||||
const (
|
||||
ConnectionEventNew = iota
|
||||
ConnectionEventUpdate
|
||||
ConnectionEventClosed
|
||||
)
|
||||
|
||||
const (
|
||||
closedConnectionMaxAge = int64((5 * time.Minute) / time.Millisecond)
|
||||
)
|
||||
|
||||
type ConnectionEvent struct {
|
||||
Type int32
|
||||
ID string
|
||||
Connection *Connection
|
||||
UplinkDelta int64
|
||||
DownlinkDelta int64
|
||||
ClosedAt int64
|
||||
}
|
||||
|
||||
type ConnectionEvents struct {
|
||||
Reset bool
|
||||
events []*ConnectionEvent
|
||||
}
|
||||
|
||||
func (c *ConnectionEvents) Iterator() ConnectionEventIterator {
|
||||
return newIterator(c.events)
|
||||
}
|
||||
|
||||
type ConnectionEventIterator interface {
|
||||
Next() *ConnectionEvent
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type Connections struct {
|
||||
connectionMap map[string]*Connection
|
||||
input []Connection
|
||||
filtered []Connection
|
||||
filterState int32
|
||||
filterApplied bool
|
||||
}
|
||||
|
||||
func NewConnections() *Connections {
|
||||
return &Connections{
|
||||
connectionMap: make(map[string]*Connection),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connections) ApplyEvents(events *ConnectionEvents) {
|
||||
if events == nil {
|
||||
return
|
||||
}
|
||||
if events.Reset {
|
||||
c.connectionMap = make(map[string]*Connection)
|
||||
}
|
||||
|
||||
for _, event := range events.events {
|
||||
switch event.Type {
|
||||
case ConnectionEventNew:
|
||||
if event.Connection != nil {
|
||||
conn := *event.Connection
|
||||
c.connectionMap[event.ID] = &conn
|
||||
}
|
||||
case ConnectionEventUpdate:
|
||||
if conn, ok := c.connectionMap[event.ID]; ok {
|
||||
conn.Uplink = event.UplinkDelta
|
||||
conn.Downlink = event.DownlinkDelta
|
||||
conn.UplinkTotal += event.UplinkDelta
|
||||
conn.DownlinkTotal += event.DownlinkDelta
|
||||
}
|
||||
case ConnectionEventClosed:
|
||||
if event.Connection != nil {
|
||||
conn := *event.Connection
|
||||
conn.ClosedAt = event.ClosedAt
|
||||
conn.Uplink = 0
|
||||
conn.Downlink = 0
|
||||
c.connectionMap[event.ID] = &conn
|
||||
continue
|
||||
}
|
||||
if conn, ok := c.connectionMap[event.ID]; ok {
|
||||
conn.ClosedAt = event.ClosedAt
|
||||
conn.Uplink = 0
|
||||
conn.Downlink = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.evictClosedConnections(time.Now().UnixMilli())
|
||||
c.input = c.input[:0]
|
||||
for _, conn := range c.connectionMap {
|
||||
c.input = append(c.input, *conn)
|
||||
}
|
||||
if c.filterApplied {
|
||||
c.FilterState(c.filterState)
|
||||
} else {
|
||||
c.filtered = c.filtered[:0]
|
||||
c.filtered = append(c.filtered, c.input...)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connections) evictClosedConnections(nowMilliseconds int64) {
|
||||
for id, conn := range c.connectionMap {
|
||||
if conn.ClosedAt == 0 {
|
||||
continue
|
||||
}
|
||||
if nowMilliseconds-conn.ClosedAt > closedConnectionMaxAge {
|
||||
delete(c.connectionMap, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connections) FilterState(state int32) {
|
||||
c.filterApplied = true
|
||||
c.filterState = state
|
||||
c.filtered = c.filtered[:0]
|
||||
switch state {
|
||||
case ConnectionStateAll:
|
||||
c.filtered = append(c.filtered, c.input...)
|
||||
case ConnectionStateActive:
|
||||
for _, connection := range c.input {
|
||||
if connection.ClosedAt == 0 {
|
||||
c.filtered = append(c.filtered, connection)
|
||||
}
|
||||
}
|
||||
case ConnectionStateClosed:
|
||||
for _, connection := range c.input {
|
||||
if connection.ClosedAt != 0 {
|
||||
c.filtered = append(c.filtered, connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Connections) SortByDate() {
|
||||
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
|
||||
if x.CreatedAt < y.CreatedAt {
|
||||
return 1
|
||||
} else if x.CreatedAt > y.CreatedAt {
|
||||
return -1
|
||||
} else {
|
||||
return strings.Compare(y.ID, x.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Connections) SortByTraffic() {
|
||||
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
|
||||
xTraffic := x.Uplink + x.Downlink
|
||||
yTraffic := y.Uplink + y.Downlink
|
||||
if xTraffic < yTraffic {
|
||||
return 1
|
||||
} else if xTraffic > yTraffic {
|
||||
return -1
|
||||
} else {
|
||||
return strings.Compare(y.ID, x.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Connections) SortByTrafficTotal() {
|
||||
slices.SortStableFunc(c.filtered, func(x, y Connection) int {
|
||||
xTraffic := x.UplinkTotal + x.DownlinkTotal
|
||||
yTraffic := y.UplinkTotal + y.DownlinkTotal
|
||||
if xTraffic < yTraffic {
|
||||
return 1
|
||||
} else if xTraffic > yTraffic {
|
||||
return -1
|
||||
} else {
|
||||
return strings.Compare(y.ID, x.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Connections) Iterator() ConnectionIterator {
|
||||
return newPtrIterator(c.filtered)
|
||||
}
|
||||
|
||||
type ProcessInfo struct {
|
||||
ProcessID int64
|
||||
UserID int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
packageNames []string
|
||||
}
|
||||
|
||||
func (p *ProcessInfo) PackageNames() StringIterator {
|
||||
return newIterator(p.packageNames)
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
ID string
|
||||
Inbound string
|
||||
InboundType string
|
||||
IPVersion int32
|
||||
Network string
|
||||
Source string
|
||||
Destination string
|
||||
Domain string
|
||||
Protocol string
|
||||
User string
|
||||
FromOutbound string
|
||||
CreatedAt int64
|
||||
ClosedAt int64
|
||||
Uplink int64
|
||||
Downlink int64
|
||||
UplinkTotal int64
|
||||
DownlinkTotal int64
|
||||
Rule string
|
||||
Outbound string
|
||||
OutboundType string
|
||||
chainList []string
|
||||
ProcessInfo *ProcessInfo
|
||||
}
|
||||
|
||||
func (c *Connection) Chain() StringIterator {
|
||||
return newIterator(c.chainList)
|
||||
}
|
||||
|
||||
func (c *Connection) DisplayDestination() string {
|
||||
destination := M.ParseSocksaddr(c.Destination)
|
||||
if destination.IsIP() && c.Domain != "" {
|
||||
destination = M.Socksaddr{
|
||||
Fqdn: c.Domain,
|
||||
Port: destination.Port,
|
||||
}
|
||||
return destination.String()
|
||||
}
|
||||
return c.Destination
|
||||
}
|
||||
|
||||
type ConnectionIterator interface {
|
||||
Next() *Connection
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
func statusMessageFromGRPC(status *daemon.Status) *StatusMessage {
|
||||
if status == nil {
|
||||
return nil
|
||||
}
|
||||
return &StatusMessage{
|
||||
Memory: int64(status.Memory),
|
||||
Goroutines: status.Goroutines,
|
||||
ConnectionsIn: status.ConnectionsIn,
|
||||
ConnectionsOut: status.ConnectionsOut,
|
||||
TrafficAvailable: status.TrafficAvailable,
|
||||
Uplink: status.Uplink,
|
||||
Downlink: status.Downlink,
|
||||
UplinkTotal: status.UplinkTotal,
|
||||
DownlinkTotal: status.DownlinkTotal,
|
||||
}
|
||||
}
|
||||
|
||||
func outboundGroupIteratorFromGRPC(groups *daemon.Groups) OutboundGroupIterator {
|
||||
if groups == nil || len(groups.Group) == 0 {
|
||||
return newIterator([]*OutboundGroup{})
|
||||
}
|
||||
var libboxGroups []*OutboundGroup
|
||||
for _, g := range groups.Group {
|
||||
libboxGroup := &OutboundGroup{
|
||||
Tag: g.Tag,
|
||||
Type: g.Type,
|
||||
Selectable: g.Selectable,
|
||||
Selected: g.Selected,
|
||||
IsExpand: g.IsExpand,
|
||||
}
|
||||
for _, item := range g.Items {
|
||||
libboxGroup.itemList = append(libboxGroup.itemList, &OutboundGroupItem{
|
||||
Tag: item.Tag,
|
||||
Type: item.Type,
|
||||
URLTestTime: item.UrlTestTime,
|
||||
URLTestDelay: item.UrlTestDelay,
|
||||
})
|
||||
}
|
||||
libboxGroups = append(libboxGroups, libboxGroup)
|
||||
}
|
||||
return newIterator(libboxGroups)
|
||||
}
|
||||
|
||||
func connectionFromGRPC(conn *daemon.Connection) Connection {
|
||||
var processInfo *ProcessInfo
|
||||
if conn.ProcessInfo != nil {
|
||||
processInfo = &ProcessInfo{
|
||||
ProcessID: int64(conn.ProcessInfo.ProcessId),
|
||||
UserID: conn.ProcessInfo.UserId,
|
||||
UserName: conn.ProcessInfo.UserName,
|
||||
ProcessPath: conn.ProcessInfo.ProcessPath,
|
||||
packageNames: conn.ProcessInfo.PackageNames,
|
||||
}
|
||||
}
|
||||
return Connection{
|
||||
ID: conn.Id,
|
||||
Inbound: conn.Inbound,
|
||||
InboundType: conn.InboundType,
|
||||
IPVersion: conn.IpVersion,
|
||||
Network: conn.Network,
|
||||
Source: conn.Source,
|
||||
Destination: conn.Destination,
|
||||
Domain: conn.Domain,
|
||||
Protocol: conn.Protocol,
|
||||
User: conn.User,
|
||||
FromOutbound: conn.FromOutbound,
|
||||
CreatedAt: conn.CreatedAt,
|
||||
ClosedAt: conn.ClosedAt,
|
||||
Uplink: conn.Uplink,
|
||||
Downlink: conn.Downlink,
|
||||
UplinkTotal: conn.UplinkTotal,
|
||||
DownlinkTotal: conn.DownlinkTotal,
|
||||
Rule: conn.Rule,
|
||||
Outbound: conn.Outbound,
|
||||
OutboundType: conn.OutboundType,
|
||||
chainList: conn.ChainList,
|
||||
ProcessInfo: processInfo,
|
||||
}
|
||||
}
|
||||
|
||||
func connectionEventFromGRPC(event *daemon.ConnectionEvent) *ConnectionEvent {
|
||||
if event == nil {
|
||||
return nil
|
||||
}
|
||||
libboxEvent := &ConnectionEvent{
|
||||
Type: int32(event.Type),
|
||||
ID: event.Id,
|
||||
UplinkDelta: event.UplinkDelta,
|
||||
DownlinkDelta: event.DownlinkDelta,
|
||||
ClosedAt: event.ClosedAt,
|
||||
}
|
||||
if event.Connection != nil {
|
||||
conn := connectionFromGRPC(event.Connection)
|
||||
libboxEvent.Connection = &conn
|
||||
}
|
||||
return libboxEvent
|
||||
}
|
||||
|
||||
func connectionEventsFromGRPC(events *daemon.ConnectionEvents) *ConnectionEvents {
|
||||
if events == nil {
|
||||
return nil
|
||||
}
|
||||
libboxEvents := &ConnectionEvents{
|
||||
Reset: events.Reset_,
|
||||
}
|
||||
for _, event := range events.Events {
|
||||
if libboxEvent := connectionEventFromGRPC(event); libboxEvent != nil {
|
||||
libboxEvents.events = append(libboxEvents.events, libboxEvent)
|
||||
}
|
||||
}
|
||||
return libboxEvents
|
||||
}
|
||||
|
||||
func systemProxyStatusFromGRPC(status *daemon.SystemProxyStatus) *SystemProxyStatus {
|
||||
if status == nil {
|
||||
return nil
|
||||
}
|
||||
return &SystemProxyStatus{
|
||||
Available: status.Available,
|
||||
Enabled: status.Enabled,
|
||||
}
|
||||
}
|
||||
|
||||
func systemProxyStatusToGRPC(status *SystemProxyStatus) *daemon.SystemProxyStatus {
|
||||
if status == nil {
|
||||
return nil
|
||||
}
|
||||
return &daemon.SystemProxyStatus{
|
||||
Available: status.Available,
|
||||
Enabled: status.Enabled,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/netip"
|
||||
"os"
|
||||
|
||||
box "github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/include"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
tun "github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
)
|
||||
|
||||
func baseContext(platformInterface PlatformInterface) context.Context {
|
||||
dnsRegistry := include.DNSTransportRegistry()
|
||||
if platformInterface != nil {
|
||||
if localTransport := platformInterface.LocalDNSTransport(); localTransport != nil {
|
||||
dns.RegisterTransport[option.LocalDNSServerOptions](dnsRegistry, C.DNSTypeLocal, func(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
|
||||
return newPlatformTransport(localTransport, tag, options), nil
|
||||
})
|
||||
}
|
||||
}
|
||||
ctx := context.Background()
|
||||
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
|
||||
return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), include.ProviderRegistry(), dnsRegistry, include.ServiceRegistry())
|
||||
}
|
||||
|
||||
func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
|
||||
options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent))
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "decode config")
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
|
||||
func CheckConfig(configContent string) error {
|
||||
ctx := baseContext(nil)
|
||||
options, err := parseConfig(ctx, configContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
ctx = service.ContextWith[adapter.PlatformInterface](ctx, (*platformInterfaceStub)(nil))
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
})
|
||||
if err == nil {
|
||||
instance.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type platformInterfaceStub struct{}
|
||||
|
||||
func (s *platformInterfaceStub) Initialize(networkManager adapter.NetworkManager) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UsePlatformAutoDetectInterfaceControl() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) AutoDetectInterfaceControl(fd int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UsePlatformInterface() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UsePlatformDefaultInterfaceMonitor() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor {
|
||||
return (*interfaceMonitorStub)(nil)
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UsePlatformNetworkInterfaces() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) NetworkInterfaces() ([]adapter.NetworkInterface, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UnderNetworkExtension() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) NetworkExtensionIncludeAllNetworks() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) ClearDNSCache() {
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) RequestPermissionForWIFIState() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UsePlatformWIFIMonitor() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) ReadWIFIState() adapter.WIFIState {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) SystemCertificates() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UsePlatformConnectionOwnerFinder() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UsePlatformNotification() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) SendNotification(notification *adapter.Notification) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) MyInterfaceAddress() []netip.Addr {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) UsePlatformLocalDNSTransport() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions] {
|
||||
return nil
|
||||
}
|
||||
|
||||
type interfaceMonitorStub struct{}
|
||||
|
||||
func (s *interfaceMonitorStub) Start() error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *interfaceMonitorStub) Close() error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *interfaceMonitorStub) DefaultInterface() *control.Interface {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *interfaceMonitorStub) OverrideAndroidVPN() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *interfaceMonitorStub) AndroidVPNEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *interfaceMonitorStub) RegisterCallback(callback tun.DefaultInterfaceUpdateCallback) *list.Element[tun.DefaultInterfaceUpdateCallback] {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *interfaceMonitorStub) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) {
|
||||
}
|
||||
|
||||
func (s *interfaceMonitorStub) RegisterMyInterface(interfaceName string) {
|
||||
}
|
||||
|
||||
func (s *interfaceMonitorStub) MyInterface() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func FormatConfig(configContent string) (*StringBox, error) {
|
||||
options, err := parseConfig(baseContext(nil), configContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
encoder := json.NewEncoder(&buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wrapString(buffer.String()), nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"os/user"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
func FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error) {
|
||||
source, err := parseConnectionOwnerAddrPort(sourceAddress, sourcePort)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse source")
|
||||
}
|
||||
destination, err := parseConnectionOwnerAddrPort(destinationAddress, destinationPort)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "parse destination")
|
||||
}
|
||||
var network string
|
||||
switch ipProtocol {
|
||||
case syscall.IPPROTO_TCP:
|
||||
network = "tcp"
|
||||
case syscall.IPPROTO_UDP:
|
||||
network = "udp"
|
||||
default:
|
||||
return nil, E.New("unknown protocol: ", ipProtocol)
|
||||
}
|
||||
owner, err := process.FindDarwinConnectionOwner(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &ConnectionOwner{
|
||||
UserId: owner.UserId,
|
||||
ProcessPath: owner.ProcessPath,
|
||||
}
|
||||
if owner.UserId != -1 && owner.UserName == "" {
|
||||
osUser, _ := user.LookupId(F.ToString(owner.UserId))
|
||||
if osUser != nil {
|
||||
result.UserName = osUser.Username
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseConnectionOwnerAddrPort(address string, port int32) (netip.AddrPort, error) {
|
||||
if port < 0 || port > 65535 {
|
||||
return netip.AddrPort{}, E.New("invalid port: ", port)
|
||||
}
|
||||
addr, err := netip.ParseAddr(address)
|
||||
if err != nil {
|
||||
return netip.AddrPort{}, err
|
||||
}
|
||||
return netip.AddrPortFrom(addr.Unmap(), uint16(port)), nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
)
|
||||
|
||||
var _ = deprecated.Note(DeprecatedNote{})
|
||||
|
||||
type DeprecatedNote struct {
|
||||
Name string
|
||||
Description string
|
||||
DeprecatedVersion string
|
||||
ScheduledVersion string
|
||||
EnvName string
|
||||
MigrationLink string
|
||||
}
|
||||
|
||||
func (n DeprecatedNote) Impending() bool {
|
||||
return deprecated.Note(n).Impending()
|
||||
}
|
||||
|
||||
func (n DeprecatedNote) Message() string {
|
||||
return deprecated.Note(n).Message()
|
||||
}
|
||||
|
||||
func (n DeprecatedNote) MessageWithLink() string {
|
||||
return deprecated.Note(n).MessageWithLink()
|
||||
}
|
||||
|
||||
type DeprecatedNoteIterator interface {
|
||||
HasNext() bool
|
||||
Next() *DeprecatedNote
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/task"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type LocalDNSTransport interface {
|
||||
Raw() bool
|
||||
Lookup(ctx *ExchangeContext, network string, domain string) error
|
||||
Exchange(ctx *ExchangeContext, message []byte) error
|
||||
}
|
||||
|
||||
var _ adapter.DNSTransport = (*platformTransport)(nil)
|
||||
|
||||
type platformTransport struct {
|
||||
dns.TransportAdapter
|
||||
iif LocalDNSTransport
|
||||
}
|
||||
|
||||
func newPlatformTransport(iif LocalDNSTransport, tag string, options option.LocalDNSServerOptions) *platformTransport {
|
||||
return &platformTransport{
|
||||
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
||||
iif: iif,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *platformTransport) Start(stage adapter.StartStage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *platformTransport) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *platformTransport) Reset() {
|
||||
}
|
||||
|
||||
func (p *platformTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
response := &ExchangeContext{
|
||||
context: ctx,
|
||||
}
|
||||
if p.iif.Raw() {
|
||||
messageBytes, err := message.Pack()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var responseMessage *mDNS.Msg
|
||||
var group task.Group
|
||||
group.Append0(func(ctx context.Context) error {
|
||||
err = p.iif.Exchange(response, messageBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.error != nil {
|
||||
return response.error
|
||||
}
|
||||
responseMessage = &response.message
|
||||
return nil
|
||||
})
|
||||
err = group.Run(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return responseMessage, nil
|
||||
} else {
|
||||
question := message.Question[0]
|
||||
var network string
|
||||
switch question.Qtype {
|
||||
case mDNS.TypeA:
|
||||
network = "ip4"
|
||||
case mDNS.TypeAAAA:
|
||||
network = "ip6"
|
||||
default:
|
||||
return nil, E.New("only IP queries are supported by current version of Android")
|
||||
}
|
||||
var responseAddrs []netip.Addr
|
||||
var group task.Group
|
||||
group.Append0(func(ctx context.Context) error {
|
||||
err := p.iif.Lookup(response, network, question.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if response.error != nil {
|
||||
return response.error
|
||||
}
|
||||
responseAddrs = response.addresses
|
||||
return nil
|
||||
})
|
||||
err := group.Run(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dns.FixedResponse(message.Id, question, responseAddrs, C.DefaultDNSTTL), nil
|
||||
}
|
||||
}
|
||||
|
||||
type Func interface {
|
||||
Invoke() error
|
||||
}
|
||||
|
||||
type ExchangeContext struct {
|
||||
context context.Context
|
||||
message mDNS.Msg
|
||||
addresses []netip.Addr
|
||||
error error
|
||||
}
|
||||
|
||||
func (c *ExchangeContext) OnCancel(callback Func) {
|
||||
go func() {
|
||||
<-c.context.Done()
|
||||
callback.Invoke()
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *ExchangeContext) Success(result string) {
|
||||
c.addresses = common.Map(common.Filter(strings.Split(result, "\n"), func(it string) bool {
|
||||
return !common.IsEmpty(it)
|
||||
}), func(it string) netip.Addr {
|
||||
return M.ParseSocksaddrHostPort(it, 0).Unwrap().Addr
|
||||
})
|
||||
}
|
||||
|
||||
func (c *ExchangeContext) RawSuccess(result []byte) {
|
||||
err := c.message.Unpack(result)
|
||||
if err != nil {
|
||||
c.error = E.Cause(err, "parse response")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ExchangeContext) ErrorCode(code int32) {
|
||||
c.error = dns.RcodeError(code)
|
||||
}
|
||||
|
||||
func (c *ExchangeContext) ErrnoCode(code int32) {
|
||||
c.error = syscall.Errno(code)
|
||||
}
|
||||
@@ -0,0 +1,493 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
const fdroidUserAgent = "F-Droid 1.21.1"
|
||||
|
||||
type FDroidUpdateInfo struct {
|
||||
VersionCode int32
|
||||
VersionName string
|
||||
DownloadURL string
|
||||
FileSize int64
|
||||
FileSHA256 string
|
||||
}
|
||||
|
||||
type FDroidPingResult struct {
|
||||
URL string
|
||||
LatencyMs int32
|
||||
Error string
|
||||
}
|
||||
|
||||
type FDroidPingResultIterator interface {
|
||||
Len() int32
|
||||
HasNext() bool
|
||||
Next() *FDroidPingResult
|
||||
}
|
||||
|
||||
type fdroidAPIResponse struct {
|
||||
PackageName string `json:"packageName"`
|
||||
SuggestedVersionCode int32 `json:"suggestedVersionCode"`
|
||||
Packages []fdroidAPIPackage `json:"packages"`
|
||||
}
|
||||
|
||||
type fdroidAPIPackage struct {
|
||||
VersionName string `json:"versionName"`
|
||||
VersionCode int32 `json:"versionCode"`
|
||||
}
|
||||
|
||||
type fdroidEntry struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Version int `json:"version"`
|
||||
Index fdroidEntryFile `json:"index"`
|
||||
Diffs map[string]fdroidEntryFile `json:"diffs"`
|
||||
}
|
||||
|
||||
type fdroidEntryFile struct {
|
||||
Name string `json:"name"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
NumPackages int `json:"numPackages"`
|
||||
}
|
||||
|
||||
type fdroidIndexV2 struct {
|
||||
Packages map[string]fdroidV2Package `json:"packages"`
|
||||
}
|
||||
|
||||
type fdroidV2Package struct {
|
||||
Versions map[string]fdroidV2Version `json:"versions"`
|
||||
}
|
||||
|
||||
type fdroidV2Version struct {
|
||||
Manifest fdroidV2Manifest `json:"manifest"`
|
||||
File fdroidV2File `json:"file"`
|
||||
}
|
||||
|
||||
type fdroidV2Manifest struct {
|
||||
VersionCode int32 `json:"versionCode"`
|
||||
VersionName string `json:"versionName"`
|
||||
}
|
||||
|
||||
type fdroidV2File struct {
|
||||
Name string `json:"name"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type fdroidIndexV1 struct {
|
||||
Packages map[string][]fdroidV1Package `json:"packages"`
|
||||
}
|
||||
|
||||
type fdroidV1Package struct {
|
||||
VersionCode int32 `json:"versionCode"`
|
||||
VersionName string `json:"versionName"`
|
||||
ApkName string `json:"apkName"`
|
||||
Size int64 `json:"size"`
|
||||
Hash string `json:"hash"`
|
||||
HashType string `json:"hashType"`
|
||||
}
|
||||
|
||||
type fdroidCache struct {
|
||||
MirrorURL string `json:"mirrorURL"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
ETag string `json:"etag"`
|
||||
IsV1 bool `json:"isV1,omitempty"`
|
||||
}
|
||||
|
||||
func CheckFDroidUpdate(mirrorURL, packageName string, currentVersionCode int32, cachePath string) (*FDroidUpdateInfo, error) {
|
||||
mirrorURL = strings.TrimRight(mirrorURL, "/")
|
||||
if strings.Contains(mirrorURL, "f-droid.org") {
|
||||
return checkFDroidAPI(mirrorURL, packageName, currentVersionCode)
|
||||
}
|
||||
client := newFDroidHTTPClient()
|
||||
defer client.CloseIdleConnections()
|
||||
cache := loadFDroidCache(cachePath, mirrorURL)
|
||||
if cache != nil && cache.IsV1 {
|
||||
return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, cache)
|
||||
}
|
||||
return checkFDroidV2(client, mirrorURL, packageName, currentVersionCode, cachePath, cache)
|
||||
}
|
||||
|
||||
func PingFDroidMirrors(mirrorURLs string) (FDroidPingResultIterator, error) {
|
||||
urls := strings.Split(mirrorURLs, ",")
|
||||
results := make([]*FDroidPingResult, len(urls))
|
||||
var waitGroup sync.WaitGroup
|
||||
for i, rawURL := range urls {
|
||||
waitGroup.Add(1)
|
||||
go func(index int, target string) {
|
||||
defer waitGroup.Done()
|
||||
target = strings.TrimSpace(target)
|
||||
result := &FDroidPingResult{URL: target}
|
||||
latency, err := pingTLS(target)
|
||||
if err != nil {
|
||||
result.LatencyMs = -1
|
||||
result.Error = err.Error()
|
||||
} else {
|
||||
result.LatencyMs = int32(latency.Milliseconds())
|
||||
}
|
||||
results[index] = result
|
||||
}(i, rawURL)
|
||||
}
|
||||
waitGroup.Wait()
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
if results[i].LatencyMs < 0 {
|
||||
return false
|
||||
}
|
||||
if results[j].LatencyMs < 0 {
|
||||
return true
|
||||
}
|
||||
return results[i].LatencyMs < results[j].LatencyMs
|
||||
})
|
||||
return newIterator(results), nil
|
||||
}
|
||||
|
||||
func PingFDroidMirror(mirrorURL string) *FDroidPingResult {
|
||||
mirrorURL = strings.TrimSpace(mirrorURL)
|
||||
result := &FDroidPingResult{URL: mirrorURL}
|
||||
latency, err := pingTLS(mirrorURL)
|
||||
if err != nil {
|
||||
result.LatencyMs = -1
|
||||
result.Error = err.Error()
|
||||
} else {
|
||||
result.LatencyMs = int32(latency.Milliseconds())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func newFDroidHTTPClient() *http.Client {
|
||||
return &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
func newFDroidRequest(requestURL string) (*http.Request, error) {
|
||||
request, err := http.NewRequest("GET", requestURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Header.Set("User-Agent", fdroidUserAgent)
|
||||
return request, nil
|
||||
}
|
||||
|
||||
func checkFDroidAPI(mirrorURL, packageName string, currentVersionCode int32) (*FDroidUpdateInfo, error) {
|
||||
client := newFDroidHTTPClient()
|
||||
defer client.CloseIdleConnections()
|
||||
|
||||
apiURL := "https://f-droid.org/api/v1/packages/" + packageName
|
||||
request, err := newFDroidRequest(apiURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, E.New("HTTP ", response.Status)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var apiResponse fdroidAPIResponse
|
||||
err = json.Unmarshal(body, &apiResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var bestCode int32
|
||||
var bestName string
|
||||
for _, pkg := range apiResponse.Packages {
|
||||
if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode {
|
||||
bestCode = pkg.VersionCode
|
||||
bestName = pkg.VersionName
|
||||
}
|
||||
}
|
||||
|
||||
if bestCode == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &FDroidUpdateInfo{
|
||||
VersionCode: bestCode,
|
||||
VersionName: bestName,
|
||||
DownloadURL: "https://f-droid.org/repo/" + packageName + "_" + strconv.FormatInt(int64(bestCode), 10) + ".apk",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func checkFDroidV2(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) {
|
||||
entryURL := mirrorURL + "/entry.jar"
|
||||
request, err := newFDroidRequest(entryURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cache != nil && cache.ETag != "" {
|
||||
request.Header.Set("If-None-Match", cache.ETag)
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode == http.StatusNotModified {
|
||||
return nil, nil
|
||||
}
|
||||
if response.StatusCode == http.StatusNotFound {
|
||||
writeFDroidCache(cachePath, mirrorURL, 0, "", true)
|
||||
return checkFDroidV1(client, mirrorURL, packageName, currentVersionCode, cachePath, nil)
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, E.New("HTTP ", response.Status, ": ", entryURL)
|
||||
}
|
||||
|
||||
jarData, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
etag := response.Header.Get("ETag")
|
||||
|
||||
var entry fdroidEntry
|
||||
err = readJSONFromJar(jarData, "entry.json", &entry)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read entry.jar")
|
||||
}
|
||||
|
||||
if entry.Timestamp == 0 {
|
||||
return nil, E.New("entry.json not found in entry.jar")
|
||||
}
|
||||
|
||||
if cache != nil && cache.Timestamp == entry.Timestamp {
|
||||
writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var indexURL string
|
||||
if cache != nil {
|
||||
cachedTimestamp := strconv.FormatInt(cache.Timestamp, 10)
|
||||
if diff, ok := entry.Diffs[cachedTimestamp]; ok {
|
||||
indexURL = mirrorURL + "/" + diff.Name
|
||||
}
|
||||
}
|
||||
if indexURL == "" {
|
||||
indexURL = mirrorURL + "/" + entry.Index.Name
|
||||
}
|
||||
|
||||
indexRequest, err := newFDroidRequest(indexURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
indexResponse, err := client.Do(indexRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer indexResponse.Body.Close()
|
||||
|
||||
if indexResponse.StatusCode != http.StatusOK {
|
||||
return nil, E.New("HTTP ", indexResponse.Status, ": ", indexURL)
|
||||
}
|
||||
|
||||
indexData, err := io.ReadAll(indexResponse.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var index fdroidIndexV2
|
||||
err = json.Unmarshal(indexData, &index)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
writeFDroidCache(cachePath, mirrorURL, entry.Timestamp, etag, false)
|
||||
|
||||
pkg, ok := index.Packages[packageName]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var bestCode int32
|
||||
var bestVersion fdroidV2Version
|
||||
for _, version := range pkg.Versions {
|
||||
if version.Manifest.VersionCode > currentVersionCode && version.Manifest.VersionCode > bestCode {
|
||||
bestCode = version.Manifest.VersionCode
|
||||
bestVersion = version
|
||||
}
|
||||
}
|
||||
|
||||
if bestCode == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &FDroidUpdateInfo{
|
||||
VersionCode: bestCode,
|
||||
VersionName: bestVersion.Manifest.VersionName,
|
||||
DownloadURL: mirrorURL + "/" + bestVersion.File.Name,
|
||||
FileSize: bestVersion.File.Size,
|
||||
FileSHA256: bestVersion.File.SHA256,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func checkFDroidV1(client *http.Client, mirrorURL, packageName string, currentVersionCode int32, cachePath string, cache *fdroidCache) (*FDroidUpdateInfo, error) {
|
||||
indexURL := mirrorURL + "/index-v1.jar"
|
||||
|
||||
request, err := newFDroidRequest(indexURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cache != nil && cache.ETag != "" {
|
||||
request.Header.Set("If-None-Match", cache.ETag)
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode == http.StatusNotModified {
|
||||
return nil, nil
|
||||
}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, E.New("HTTP ", response.Status, ": ", indexURL)
|
||||
}
|
||||
|
||||
jarData, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
etag := response.Header.Get("ETag")
|
||||
|
||||
var index fdroidIndexV1
|
||||
err = readJSONFromJar(jarData, "index-v1.json", &index)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read index-v1.jar")
|
||||
}
|
||||
|
||||
writeFDroidCache(cachePath, mirrorURL, 0, etag, true)
|
||||
|
||||
packages, ok := index.Packages[packageName]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var bestCode int32
|
||||
var bestPackage fdroidV1Package
|
||||
for _, pkg := range packages {
|
||||
if pkg.VersionCode > currentVersionCode && pkg.VersionCode > bestCode {
|
||||
bestCode = pkg.VersionCode
|
||||
bestPackage = pkg
|
||||
}
|
||||
}
|
||||
|
||||
if bestCode == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return &FDroidUpdateInfo{
|
||||
VersionCode: bestCode,
|
||||
VersionName: bestPackage.VersionName,
|
||||
DownloadURL: mirrorURL + "/" + bestPackage.ApkName,
|
||||
FileSize: bestPackage.Size,
|
||||
FileSHA256: bestPackage.Hash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readJSONFromJar(jarData []byte, fileName string, destination any) error {
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(jarData), int64(len(jarData)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, file := range zipReader.File {
|
||||
if file.Name != fileName {
|
||||
continue
|
||||
}
|
||||
reader, err := file.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := io.ReadAll(reader)
|
||||
reader.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(data, destination)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func pingTLS(mirrorURL string) (time.Duration, error) {
|
||||
parsed, err := url.Parse(mirrorURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
host := parsed.Host
|
||||
if !strings.Contains(host, ":") {
|
||||
host = host + ":443"
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
start := time.Now()
|
||||
conn, err := tls.DialWithDialer(dialer, "tcp", host, &tls.Config{})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
latency := time.Since(start)
|
||||
conn.Close()
|
||||
return latency, nil
|
||||
}
|
||||
|
||||
func loadFDroidCache(cachePath, mirrorURL string) *fdroidCache {
|
||||
cacheFile := filepath.Join(cachePath, "fdroid_cache.json")
|
||||
data, err := os.ReadFile(cacheFile)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var cache fdroidCache
|
||||
err = json.Unmarshal(data, &cache)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if cache.MirrorURL != mirrorURL {
|
||||
return nil
|
||||
}
|
||||
return &cache
|
||||
}
|
||||
|
||||
func writeFDroidCache(cachePath, mirrorURL string, timestamp int64, etag string, isV1 bool) {
|
||||
cache := fdroidCache{
|
||||
MirrorURL: mirrorURL,
|
||||
Timestamp: timestamp,
|
||||
ETag: etag,
|
||||
IsV1: isV1,
|
||||
}
|
||||
data, err := json.Marshal(cache)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
os.MkdirAll(cachePath, 0o755)
|
||||
os.WriteFile(filepath.Join(cachePath, "fdroid_cache.json"), data, 0o644)
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package libbox
|
||||
|
||||
type FDroidMirror struct {
|
||||
URL string
|
||||
Country string
|
||||
Name string
|
||||
}
|
||||
|
||||
type FDroidMirrorIterator interface {
|
||||
Len() int32
|
||||
HasNext() bool
|
||||
Next() *FDroidMirror
|
||||
}
|
||||
|
||||
var builtinFDroidMirrors = []FDroidMirror{
|
||||
// Official
|
||||
{URL: "https://f-droid.org/repo", Country: "Official", Name: "f-droid.org"},
|
||||
{URL: "https://cloudflare.f-droid.org/repo", Country: "Official", Name: "Cloudflare CDN"},
|
||||
|
||||
// China
|
||||
{URL: "https://mirrors.tuna.tsinghua.edu.cn/fdroid/repo", Country: "China", Name: "Tsinghua TUNA"},
|
||||
{URL: "https://mirrors.nju.edu.cn/fdroid/repo", Country: "China", Name: "Nanjing University"},
|
||||
{URL: "https://mirror.iscas.ac.cn/fdroid/repo", Country: "China", Name: "ISCAS"},
|
||||
{URL: "https://mirror.nyist.edu.cn/fdroid/repo", Country: "China", Name: "NYIST"},
|
||||
{URL: "https://mirrors.cqupt.edu.cn/fdroid/repo", Country: "China", Name: "CQUPT"},
|
||||
{URL: "https://mirrors.shanghaitech.edu.cn/fdroid/repo", Country: "China", Name: "ShanghaiTech"},
|
||||
|
||||
// India
|
||||
{URL: "https://mirror.hyd.albony.in/fdroid/repo", Country: "India", Name: "Albony Hyderabad"},
|
||||
{URL: "https://mirror.del2.albony.in/fdroid/repo", Country: "India", Name: "Albony Delhi"},
|
||||
|
||||
// Taiwan
|
||||
{URL: "https://mirror.ossplanet.net/fdroid/repo", Country: "Taiwan", Name: "OSSPlanet"},
|
||||
|
||||
// France
|
||||
{URL: "https://fdroid.tetaneutral.net/fdroid/repo", Country: "France", Name: "tetaneutral.net"},
|
||||
{URL: "https://mirror.freedif.org/fdroid/repo", Country: "France", Name: "FreeDif"},
|
||||
|
||||
// Germany
|
||||
{URL: "https://ftp.fau.de/fdroid/repo", Country: "Germany", Name: "FAU Erlangen"},
|
||||
{URL: "https://ftp.agdsn.de/fdroid/repo", Country: "Germany", Name: "AGDSN Dresden"},
|
||||
{URL: "https://ftp.gwdg.de/pub/android/fdroid/repo", Country: "Germany", Name: "GWDG"},
|
||||
{URL: "https://mirror.level66.network/fdroid/repo", Country: "Germany", Name: "Level66"},
|
||||
{URL: "https://mirror.mci-1.serverforge.org/fdroid/repo", Country: "Germany", Name: "ServerForge"},
|
||||
|
||||
// Netherlands
|
||||
{URL: "https://ftp.snt.utwente.nl/pub/software/fdroid/repo", Country: "Netherlands", Name: "University of Twente"},
|
||||
|
||||
// Sweden
|
||||
{URL: "https://ftp.lysator.liu.se/pub/fdroid/repo", Country: "Sweden", Name: "Lysator"},
|
||||
|
||||
// Denmark
|
||||
{URL: "https://mirrors.dotsrc.org/fdroid/repo", Country: "Denmark", Name: "dotsrc.org"},
|
||||
|
||||
// Austria
|
||||
{URL: "https://mirror.kumi.systems/fdroid/repo", Country: "Austria", Name: "Kumi Systems"},
|
||||
|
||||
// Switzerland
|
||||
{URL: "https://mirror.init7.net/fdroid/repo", Country: "Switzerland", Name: "Init7"},
|
||||
|
||||
// Romania
|
||||
{URL: "https://mirrors.hostico.ro/fdroid/repo", Country: "Romania", Name: "Hostico"},
|
||||
{URL: "https://mirrors.chroot.ro/fdroid/repo", Country: "Romania", Name: "Chroot"},
|
||||
{URL: "https://ftp.lug.ro/fdroid/repo", Country: "Romania", Name: "LUG Romania"},
|
||||
|
||||
// US
|
||||
{URL: "https://plug-mirror.rcac.purdue.edu/fdroid/repo", Country: "US", Name: "Purdue"},
|
||||
{URL: "https://mirror.fcix.net/fdroid/repo", Country: "US", Name: "FCIX"},
|
||||
{URL: "https://opencolo.mm.fcix.net/fdroid/repo", Country: "US", Name: "OpenColo"},
|
||||
{URL: "https://forksystems.mm.fcix.net/fdroid/repo", Country: "US", Name: "Fork Systems"},
|
||||
{URL: "https://southfront.mm.fcix.net/fdroid/repo", Country: "US", Name: "South Front"},
|
||||
{URL: "https://ziply.mm.fcix.net/fdroid/repo", Country: "US", Name: "Ziply"},
|
||||
|
||||
// Canada
|
||||
{URL: "https://mirror.quantum5.ca/fdroid/repo", Country: "Canada", Name: "Quantum5"},
|
||||
|
||||
// Australia
|
||||
{URL: "https://mirror.aarnet.edu.au/fdroid/repo", Country: "Australia", Name: "AARNet"},
|
||||
|
||||
// Other
|
||||
{URL: "https://mirror.cyberbits.eu/fdroid/repo", Country: "Europe", Name: "Cyberbits EU"},
|
||||
{URL: "https://mirror.eu.ossplanet.net/fdroid/repo", Country: "Europe", Name: "OSSPlanet EU"},
|
||||
{URL: "https://mirror.cyberbits.asia/fdroid/repo", Country: "Asia", Name: "Cyberbits Asia"},
|
||||
{URL: "https://mirrors.jevincanders.net/fdroid/repo", Country: "US", Name: "Jevincanders"},
|
||||
{URL: "https://mirrors.komogoto.com/fdroid/repo", Country: "US", Name: "Komogoto"},
|
||||
{URL: "https://fdroid.rasp.sh/fdroid/repo", Country: "Europe", Name: "rasp.sh"},
|
||||
{URL: "https://mirror.gofoss.xyz/fdroid/repo", Country: "Europe", Name: "GoFOSS"},
|
||||
}
|
||||
|
||||
func GetFDroidMirrors() FDroidMirrorIterator {
|
||||
return newPtrIterator(builtinFDroidMirrors)
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
{
|
||||
"version": 1,
|
||||
"variables": {
|
||||
"VERSION": "$(go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)",
|
||||
"WORKSPACE_ROOT": "../../..",
|
||||
"DEPLOY_ANDROID": "${WORKSPACE_ROOT}/sing-box-for-android/app/libs",
|
||||
"DEPLOY_APPLE": "${WORKSPACE_ROOT}/sing-box-for-apple",
|
||||
"DEPLOY_WINDOWS": "${WORKSPACE_ROOT}/sing-box-for-windows/local-packages"
|
||||
},
|
||||
"packages": [
|
||||
{
|
||||
"id": "libbox",
|
||||
"path": ".",
|
||||
"java_package": "io.nekohasekai.libbox",
|
||||
"csharp_namespace": "SagerNet",
|
||||
"csharp_entrypoint": "Libbox",
|
||||
"apple_prefix": "Libbox"
|
||||
}
|
||||
],
|
||||
"builds": [
|
||||
{
|
||||
"id": "android-main",
|
||||
"packages": ["libbox"],
|
||||
"default": {
|
||||
"tags": [
|
||||
"with_gvisor",
|
||||
"with_quic",
|
||||
"with_wireguard",
|
||||
"with_utls",
|
||||
"with_naive_outbound",
|
||||
"with_clash_api",
|
||||
"badlinkname",
|
||||
"tfogo_checklinkname0",
|
||||
"with_tailscale",
|
||||
"ts_omit_logtail",
|
||||
"ts_omit_ssh",
|
||||
"ts_omit_drive",
|
||||
"ts_omit_taildrop",
|
||||
"ts_omit_webclient",
|
||||
"ts_omit_doctor",
|
||||
"ts_omit_capture",
|
||||
"ts_omit_kube",
|
||||
"ts_omit_aws",
|
||||
"ts_omit_synology",
|
||||
"ts_omit_bird"
|
||||
],
|
||||
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0",
|
||||
"trimpath": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "android-legacy",
|
||||
"packages": ["libbox"],
|
||||
"default": {
|
||||
"tags": [
|
||||
"with_gvisor",
|
||||
"with_quic",
|
||||
"with_wireguard",
|
||||
"with_utls",
|
||||
"with_clash_api",
|
||||
"badlinkname",
|
||||
"tfogo_checklinkname0",
|
||||
"with_tailscale",
|
||||
"ts_omit_logtail",
|
||||
"ts_omit_ssh",
|
||||
"ts_omit_drive",
|
||||
"ts_omit_taildrop",
|
||||
"ts_omit_webclient",
|
||||
"ts_omit_doctor",
|
||||
"ts_omit_capture",
|
||||
"ts_omit_kube",
|
||||
"ts_omit_aws",
|
||||
"ts_omit_synology",
|
||||
"ts_omit_bird"
|
||||
],
|
||||
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0",
|
||||
"trimpath": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "apple",
|
||||
"packages": ["libbox"],
|
||||
"default": {
|
||||
"tags": [
|
||||
"with_gvisor",
|
||||
"with_quic",
|
||||
"with_wireguard",
|
||||
"with_utls",
|
||||
"with_naive_outbound",
|
||||
"with_clash_api",
|
||||
"badlinkname",
|
||||
"tfogo_checklinkname0",
|
||||
"with_dhcp",
|
||||
"grpcnotrace",
|
||||
"with_tailscale",
|
||||
"ts_omit_logtail",
|
||||
"ts_omit_ssh",
|
||||
"ts_omit_drive",
|
||||
"ts_omit_taildrop",
|
||||
"ts_omit_webclient",
|
||||
"ts_omit_doctor",
|
||||
"ts_omit_capture",
|
||||
"ts_omit_kube",
|
||||
"ts_omit_aws",
|
||||
"ts_omit_synology",
|
||||
"ts_omit_bird"
|
||||
],
|
||||
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0",
|
||||
"trimpath": true
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"match": { "os": "ios" },
|
||||
"tags_append": ["with_low_memory"]
|
||||
},
|
||||
{
|
||||
"match": { "os": "tvos" },
|
||||
"tags_append": ["with_low_memory"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "windows",
|
||||
"packages": ["libbox"],
|
||||
"default": {
|
||||
"tags": [
|
||||
"with_gvisor",
|
||||
"with_quic",
|
||||
"with_wireguard",
|
||||
"with_utls",
|
||||
"with_naive_outbound",
|
||||
"with_purego",
|
||||
"with_clash_api",
|
||||
"badlinkname",
|
||||
"tfogo_checklinkname0",
|
||||
"with_tailscale",
|
||||
"ts_omit_logtail",
|
||||
"ts_omit_ssh",
|
||||
"ts_omit_drive",
|
||||
"ts_omit_taildrop",
|
||||
"ts_omit_webclient",
|
||||
"ts_omit_doctor",
|
||||
"ts_omit_capture",
|
||||
"ts_omit_kube",
|
||||
"ts_omit_aws",
|
||||
"ts_omit_synology",
|
||||
"ts_omit_bird"
|
||||
],
|
||||
"ldflags": "-X github.com/sagernet/sing-box/constant.Version=${VERSION} -X internal/godebug.defaultGODEBUG=multipathtcp=0 -s -w -buildid= -checklinkname=0",
|
||||
"trimpath": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"platforms": [
|
||||
{
|
||||
"type": "android",
|
||||
"build": "android-main",
|
||||
"min_sdk": 23,
|
||||
"ndk_version": "28.0.13004108",
|
||||
"lib_name": "box",
|
||||
"languages": [{ "type": "java" }],
|
||||
"artifacts": [
|
||||
{
|
||||
"type": "aar",
|
||||
"output_path": "libbox.aar",
|
||||
"execute_after": [
|
||||
"if [ -d \"${DEPLOY_ANDROID}\" ]; then",
|
||||
" rm -f \"${DEPLOY_ANDROID}/$$(basename \"${OUTPUT_PATH}\")\"",
|
||||
" mv \"${OUTPUT_PATH}\" \"${DEPLOY_ANDROID}/\"",
|
||||
"fi"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "android",
|
||||
"build": "android-legacy",
|
||||
"min_sdk": 21,
|
||||
"ndk_version": "28.0.13004108",
|
||||
"lib_name": "box",
|
||||
"languages": [{ "type": "java" }],
|
||||
"artifacts": [
|
||||
{
|
||||
"type": "aar",
|
||||
"output_path": "libbox-legacy.aar",
|
||||
"execute_after": [
|
||||
"if [ -d \"${DEPLOY_ANDROID}\" ]; then",
|
||||
" rm -f \"${DEPLOY_ANDROID}/$$(basename \"${OUTPUT_PATH}\")\"",
|
||||
" mv \"${OUTPUT_PATH}\" \"${DEPLOY_ANDROID}/\"",
|
||||
"fi"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "apple",
|
||||
"build": "apple",
|
||||
"targets": [
|
||||
"ios/arm64",
|
||||
"ios/simulator/arm64",
|
||||
"ios/simulator/amd64",
|
||||
"tvos/arm64",
|
||||
"tvos/simulator/arm64",
|
||||
"tvos/simulator/amd64",
|
||||
"macos/arm64",
|
||||
"macos/amd64"
|
||||
],
|
||||
"languages": [{ "type": "objc" }],
|
||||
"artifacts": [
|
||||
{
|
||||
"type": "xcframework",
|
||||
"module_name": "Libbox",
|
||||
"execute_after": [
|
||||
"if [ -d \"${DEPLOY_APPLE}\" ]; then",
|
||||
" rm -rf \"${DEPLOY_APPLE}/${MODULE_NAME}.xcframework\"",
|
||||
" mv \"${OUTPUT_PATH}\" \"${DEPLOY_APPLE}/\"",
|
||||
"fi"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "csharp",
|
||||
"build": "windows",
|
||||
"targets": [
|
||||
"windows/amd64"
|
||||
],
|
||||
"languages": [{ "type": "csharp" }],
|
||||
"artifacts": [
|
||||
{
|
||||
"type": "nuget",
|
||||
"package_id": "SagerNet.Libbox",
|
||||
"package_version": "0.0.0-local",
|
||||
"execute_after": {
|
||||
"windows": [
|
||||
"$$deployPath = '${DEPLOY_WINDOWS}'",
|
||||
"if (Test-Path $$deployPath) {",
|
||||
" Remove-Item \"$$deployPath\\${PACKAGE_ID}.*.nupkg\" -ErrorAction SilentlyContinue",
|
||||
" Move-Item -Force '${OUTPUT_PATH}' \"$$deployPath\\\"",
|
||||
" $$cachePath = if ($$env:NUGET_PACKAGES) { $$env:NUGET_PACKAGES } else { \"$$env:USERPROFILE\\.nuget\\packages\" }",
|
||||
" Remove-Item -Recurse -Force \"$$cachePath\\sagernet.libbox\\${PACKAGE_VERSION}\" -ErrorAction SilentlyContinue",
|
||||
"}"
|
||||
],
|
||||
"default": [
|
||||
"if [ -d \"${DEPLOY_WINDOWS}\" ]; then",
|
||||
" rm -f \"${DEPLOY_WINDOWS}/${PACKAGE_ID}.*.nupkg\"",
|
||||
" mv \"${OUTPUT_PATH}\" \"${DEPLOY_WINDOWS}/\"",
|
||||
" cache_path=\"$${NUGET_PACKAGES:-$${HOME}/.nuget/packages}\"",
|
||||
" rm -rf \"$${cache_path}/sagernet.libbox/${PACKAGE_VERSION}\"",
|
||||
"fi"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
"github.com/sagernet/sing/protocol/socks/socks5"
|
||||
)
|
||||
|
||||
type HTTPClient interface {
|
||||
RestrictedTLS()
|
||||
ModernTLS()
|
||||
PinnedTLS12()
|
||||
PinnedSHA256(sumHex string)
|
||||
TrySocks5(port int32)
|
||||
KeepAlive()
|
||||
NewRequest() HTTPRequest
|
||||
Close()
|
||||
}
|
||||
|
||||
type HTTPRequest interface {
|
||||
SetURL(link string) error
|
||||
SetMethod(method string)
|
||||
SetHeader(key string, value string)
|
||||
SetContent(content []byte)
|
||||
SetContentString(content string)
|
||||
RandomUserAgent()
|
||||
SetUserAgent(userAgent string)
|
||||
Execute() (HTTPResponse, error)
|
||||
}
|
||||
|
||||
type HTTPResponse interface {
|
||||
GetContent() (*StringBox, error)
|
||||
WriteTo(path string) error
|
||||
WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error
|
||||
}
|
||||
|
||||
type HTTPResponseWriteToProgressHandler interface {
|
||||
Update(progress int64, total int64)
|
||||
}
|
||||
|
||||
var (
|
||||
_ HTTPClient = (*httpClient)(nil)
|
||||
_ HTTPRequest = (*httpRequest)(nil)
|
||||
_ HTTPResponse = (*httpResponse)(nil)
|
||||
)
|
||||
|
||||
type httpClient struct {
|
||||
tls tls.Config
|
||||
client http.Client
|
||||
transport http.Transport
|
||||
}
|
||||
|
||||
func NewHTTPClient() HTTPClient {
|
||||
client := new(httpClient)
|
||||
client.client.Transport = &client.transport
|
||||
client.transport.ForceAttemptHTTP2 = true
|
||||
client.transport.TLSHandshakeTimeout = C.TCPTimeout
|
||||
client.transport.TLSClientConfig = &client.tls
|
||||
client.transport.DisableKeepAlives = true
|
||||
return client
|
||||
}
|
||||
|
||||
func (c *httpClient) ModernTLS() {
|
||||
c.setTLSVersion(tls.VersionTLS12, 0, func(suite *tls.CipherSuite) bool { return true })
|
||||
}
|
||||
|
||||
func (c *httpClient) RestrictedTLS() {
|
||||
c.setTLSVersion(tls.VersionTLS13, 0, func(suite *tls.CipherSuite) bool {
|
||||
return common.Contains(suite.SupportedVersions, uint16(tls.VersionTLS13))
|
||||
})
|
||||
}
|
||||
|
||||
func (c *httpClient) setTLSVersion(minVersion, maxVersion uint16, filter func(*tls.CipherSuite) bool) {
|
||||
c.tls.MinVersion = minVersion
|
||||
if maxVersion != 0 {
|
||||
c.tls.MaxVersion = maxVersion
|
||||
}
|
||||
c.tls.CipherSuites = common.Map(common.Filter(tls.CipherSuites(), filter), func(it *tls.CipherSuite) uint16 {
|
||||
return it.ID
|
||||
})
|
||||
}
|
||||
|
||||
func (c *httpClient) PinnedTLS12() {
|
||||
c.setTLSVersion(tls.VersionTLS12, tls.VersionTLS12, func(suite *tls.CipherSuite) bool { return true })
|
||||
}
|
||||
|
||||
func (c *httpClient) PinnedSHA256(sumHex string) {
|
||||
c.tls.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
for _, rawCert := range rawCerts {
|
||||
certSum := sha256.Sum256(rawCert)
|
||||
if sumHex == hex.EncodeToString(certSum[:]) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return E.New("pinned sha256 sum mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *httpClient) TrySocks5(port int32) {
|
||||
dialer := new(net.Dialer)
|
||||
c.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
for {
|
||||
socksConn, err := dialer.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(int(port)))
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
_, err = socks.ClientHandshake5(socksConn, socks5.CommandConnect, M.ParseSocksaddr(addr), "", "")
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
//nolint:staticcheck
|
||||
return socksConn, err
|
||||
}
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *httpClient) KeepAlive() {
|
||||
c.transport.DisableKeepAlives = false
|
||||
}
|
||||
|
||||
func (c *httpClient) NewRequest() HTTPRequest {
|
||||
req := &httpRequest{httpClient: c}
|
||||
req.request = http.Request{
|
||||
Method: "GET",
|
||||
Header: http.Header{},
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func (c *httpClient) Close() {
|
||||
c.transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
type httpRequest struct {
|
||||
*httpClient
|
||||
request http.Request
|
||||
}
|
||||
|
||||
func (r *httpRequest) SetURL(link string) (err error) {
|
||||
r.request.URL, err = url.Parse(link)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if r.request.URL.User != nil {
|
||||
user := r.request.URL.User.Username()
|
||||
password, _ := r.request.URL.User.Password()
|
||||
r.request.SetBasicAuth(user, password)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *httpRequest) SetMethod(method string) {
|
||||
r.request.Method = method
|
||||
}
|
||||
|
||||
func (r *httpRequest) SetHeader(key string, value string) {
|
||||
r.request.Header.Set(key, value)
|
||||
}
|
||||
|
||||
func (r *httpRequest) RandomUserAgent() {
|
||||
r.request.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", rand.Int()%54, rand.Int()%2))
|
||||
}
|
||||
|
||||
func (r *httpRequest) SetUserAgent(userAgent string) {
|
||||
r.request.Header.Set("User-Agent", userAgent)
|
||||
}
|
||||
|
||||
func (r *httpRequest) SetContent(content []byte) {
|
||||
r.request.Body = io.NopCloser(bytes.NewReader(content))
|
||||
r.request.ContentLength = int64(len(content))
|
||||
}
|
||||
|
||||
func (r *httpRequest) SetContentString(content string) {
|
||||
r.SetContent([]byte(content))
|
||||
}
|
||||
|
||||
func (r *httpRequest) Execute() (HTTPResponse, error) {
|
||||
response, err := r.client.Do(&r.request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpResp := &httpResponse{Response: response}
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, errors.New(httpResp.errorString())
|
||||
}
|
||||
return httpResp, nil
|
||||
}
|
||||
|
||||
type httpResponse struct {
|
||||
*http.Response
|
||||
|
||||
getContentOnce sync.Once
|
||||
content []byte
|
||||
contentError error
|
||||
}
|
||||
|
||||
func (h *httpResponse) errorString() string {
|
||||
content, err := h.GetContent()
|
||||
if err != nil {
|
||||
return fmt.Sprint("HTTP ", h.Status)
|
||||
}
|
||||
return fmt.Sprint("HTTP ", h.Status, ": ", content)
|
||||
}
|
||||
|
||||
func (h *httpResponse) GetContent() (*StringBox, error) {
|
||||
h.getContentOnce.Do(func() {
|
||||
defer h.Body.Close()
|
||||
h.content, h.contentError = io.ReadAll(h.Body)
|
||||
})
|
||||
if h.contentError != nil {
|
||||
return nil, h.contentError
|
||||
}
|
||||
return wrapString(string(h.content)), nil
|
||||
}
|
||||
|
||||
func (h *httpResponse) WriteTo(path string) error {
|
||||
defer h.Body.Close()
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return common.Error(bufio.Copy(file, h.Body))
|
||||
}
|
||||
|
||||
func (h *httpResponse) WriteToWithProgress(path string, handler HTTPResponseWriteToProgressHandler) error {
|
||||
defer h.Body.Close()
|
||||
file, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
return common.Error(bufio.Copy(&progressWriter{
|
||||
writer: file,
|
||||
handler: handler,
|
||||
total: h.ContentLength,
|
||||
}, h.Body))
|
||||
}
|
||||
|
||||
type progressWriter struct {
|
||||
writer io.Writer
|
||||
handler HTTPResponseWriteToProgressHandler
|
||||
total int64
|
||||
written int64
|
||||
}
|
||||
|
||||
func (w *progressWriter) Write(p []byte) (int, error) {
|
||||
n, err := w.writer.Write(p)
|
||||
w.written += int64(n)
|
||||
w.handler.Update(w.written, w.total)
|
||||
return n, err
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package libbox
|
||||
|
||||
import "github.com/sagernet/sing/common"
|
||||
|
||||
type StringIterator interface {
|
||||
Len() int32
|
||||
HasNext() bool
|
||||
Next() string
|
||||
}
|
||||
|
||||
type Int32Iterator interface {
|
||||
Len() int32
|
||||
HasNext() bool
|
||||
Next() int32
|
||||
}
|
||||
|
||||
var _ StringIterator = (*iterator[string])(nil)
|
||||
|
||||
type iterator[T any] struct {
|
||||
values []T
|
||||
}
|
||||
|
||||
func newIterator[T any](values []T) *iterator[T] {
|
||||
return &iterator[T]{values}
|
||||
}
|
||||
|
||||
//go:noinline
|
||||
func newPtrIterator[T any](values []T) *iterator[*T] {
|
||||
return &iterator[*T]{common.Map(values, func(value T) *T { return &value })}
|
||||
}
|
||||
|
||||
func (i *iterator[T]) Len() int32 {
|
||||
return int32(len(i.values))
|
||||
}
|
||||
|
||||
func (i *iterator[T]) HasNext() bool {
|
||||
return len(i.values) > 0
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
type abstractIterator[T any] interface {
|
||||
Next() T
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
func iteratorToArray[T any](iterator abstractIterator[T]) []T {
|
||||
if iterator == nil {
|
||||
return nil
|
||||
}
|
||||
var values []T
|
||||
for iterator.HasNext() {
|
||||
values = append(values, iterator.Next())
|
||||
}
|
||||
return values
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
//go:build !unix
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func linkFlags(rawFlags uint32) net.Flags {
|
||||
panic("stub!")
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build unix
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// copied from net.linkFlags
|
||||
func linkFlags(rawFlags uint32) net.Flags {
|
||||
var f net.Flags
|
||||
if rawFlags&syscall.IFF_UP != 0 {
|
||||
f |= net.FlagUp
|
||||
}
|
||||
if rawFlags&syscall.IFF_RUNNING != 0 {
|
||||
f |= net.FlagRunning
|
||||
}
|
||||
if rawFlags&syscall.IFF_BROADCAST != 0 {
|
||||
f |= net.FlagBroadcast
|
||||
}
|
||||
if rawFlags&syscall.IFF_LOOPBACK != 0 {
|
||||
f |= net.FlagLoopback
|
||||
}
|
||||
if rawFlags&syscall.IFF_POINTOPOINT != 0 {
|
||||
f |= net.FlagPointToPoint
|
||||
}
|
||||
if rawFlags&syscall.IFF_MULTICAST != 0 {
|
||||
f |= net.FlagMulticast
|
||||
}
|
||||
return f
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//go:build darwin || linux
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
)
|
||||
|
||||
var crashOutputFile *os.File
|
||||
|
||||
func RedirectStderr(path string) error {
|
||||
if stats, err := os.Stat(path); err == nil && stats.Size() > 0 {
|
||||
_ = os.Rename(path, path+".old")
|
||||
}
|
||||
outputFile, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS != "android" {
|
||||
err = outputFile.Chown(sUserID, sGroupID)
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputFile.Name())
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = debug.SetCrashOutput(outputFile, debug.CrashOptions{})
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputFile.Name())
|
||||
return err
|
||||
}
|
||||
crashOutputFile = outputFile
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"math"
|
||||
runtimeDebug "runtime/debug"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
)
|
||||
|
||||
var memoryLimitEnabled bool
|
||||
|
||||
func SetMemoryLimit(enabled bool) {
|
||||
memoryLimitEnabled = enabled
|
||||
const memoryLimitGo = 45 * 1024 * 1024
|
||||
if enabled {
|
||||
runtimeDebug.SetGCPercent(10)
|
||||
if C.IsIos {
|
||||
runtimeDebug.SetMemoryLimit(memoryLimitGo)
|
||||
}
|
||||
} else {
|
||||
runtimeDebug.SetGCPercent(100)
|
||||
if C.IsIos {
|
||||
runtimeDebug.SetMemoryLimit(math.MaxInt64)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
tun "github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
)
|
||||
|
||||
var (
|
||||
_ tun.DefaultInterfaceMonitor = (*platformDefaultInterfaceMonitor)(nil)
|
||||
_ InterfaceUpdateListener = (*platformDefaultInterfaceMonitor)(nil)
|
||||
)
|
||||
|
||||
type platformDefaultInterfaceMonitor struct {
|
||||
*platformInterfaceWrapper
|
||||
logger logger.Logger
|
||||
element *list.Element[tun.NetworkUpdateCallback]
|
||||
callbacks list.List[tun.DefaultInterfaceUpdateCallback]
|
||||
myInterface string
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) Start() error {
|
||||
return m.iif.StartDefaultInterfaceMonitor(m)
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) Close() error {
|
||||
return m.iif.CloseDefaultInterfaceMonitor(m)
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) DefaultInterface() *control.Interface {
|
||||
m.defaultInterfaceAccess.Lock()
|
||||
defer m.defaultInterfaceAccess.Unlock()
|
||||
return m.defaultInterface
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) OverrideAndroidVPN() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) AndroidVPNEnabled() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) RegisterCallback(callback tun.DefaultInterfaceUpdateCallback) *list.Element[tun.DefaultInterfaceUpdateCallback] {
|
||||
m.defaultInterfaceAccess.Lock()
|
||||
defer m.defaultInterfaceAccess.Unlock()
|
||||
return m.callbacks.PushBack(callback)
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) {
|
||||
m.defaultInterfaceAccess.Lock()
|
||||
defer m.defaultInterfaceAccess.Unlock()
|
||||
m.callbacks.Remove(element)
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) {
|
||||
if sFixAndroidStack {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained)
|
||||
close(done)
|
||||
}()
|
||||
<-done
|
||||
} else {
|
||||
m.updateDefaultInterface(interfaceName, interfaceIndex32, isExpensive, isConstrained)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) updateDefaultInterface(interfaceName string, interfaceIndex32 int32, isExpensive bool, isConstrained bool) {
|
||||
m.isExpensive = isExpensive
|
||||
m.isConstrained = isConstrained
|
||||
err := m.networkManager.UpdateInterfaces()
|
||||
if err != nil {
|
||||
m.logger.Error(E.Cause(err, "update interfaces"))
|
||||
}
|
||||
m.defaultInterfaceAccess.Lock()
|
||||
if interfaceIndex32 == -1 {
|
||||
m.defaultInterface = nil
|
||||
callbacks := m.callbacks.Array()
|
||||
m.defaultInterfaceAccess.Unlock()
|
||||
for _, callback := range callbacks {
|
||||
callback(nil, 0)
|
||||
}
|
||||
return
|
||||
}
|
||||
oldInterface := m.defaultInterface
|
||||
newInterface, err := m.networkManager.InterfaceFinder().ByIndex(int(interfaceIndex32))
|
||||
if err != nil {
|
||||
m.defaultInterfaceAccess.Unlock()
|
||||
m.logger.Error(E.Cause(err, "find updated interface: ", interfaceName))
|
||||
return
|
||||
}
|
||||
m.defaultInterface = newInterface
|
||||
if oldInterface != nil && oldInterface.Name == m.defaultInterface.Name && oldInterface.Index == m.defaultInterface.Index {
|
||||
m.defaultInterfaceAccess.Unlock()
|
||||
return
|
||||
}
|
||||
callbacks := m.callbacks.Array()
|
||||
m.defaultInterfaceAccess.Unlock()
|
||||
for _, callback := range callbacks {
|
||||
callback(newInterface, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) RegisterMyInterface(interfaceName string) {
|
||||
m.defaultInterfaceAccess.Lock()
|
||||
defer m.defaultInterfaceAccess.Unlock()
|
||||
m.myInterface = interfaceName
|
||||
}
|
||||
|
||||
func (m *platformDefaultInterfaceMonitor) MyInterface() string {
|
||||
m.defaultInterfaceAccess.Lock()
|
||||
defer m.defaultInterfaceAccess.Unlock()
|
||||
return m.myInterface
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package libbox
|
||||
|
||||
// https://github.com/golang/go/issues/46893
|
||||
// TODO: remove after `bulkBarrierPreWrite: unaligned arguments` fixed
|
||||
|
||||
type StringBox struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func wrapString(value string) *StringBox {
|
||||
return &StringBox{Value: value}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"os"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
// https://github.com/SagerNet/sing-box/issues/3233
|
||||
// https://github.com/golang/go/issues/70508
|
||||
// https://github.com/tailscale/tailscale/issues/13452
|
||||
|
||||
//go:linkname checkPidfdOnce os.checkPidfdOnce
|
||||
var checkPidfdOnce func() error
|
||||
|
||||
func init() {
|
||||
checkPidfdOnce = func() error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type PlatformInterface interface {
|
||||
LocalDNSTransport() LocalDNSTransport
|
||||
UsePlatformAutoDetectInterfaceControl() bool
|
||||
AutoDetectInterfaceControl(fd int32) error
|
||||
OpenTun(options TunOptions) (int32, error)
|
||||
UseProcFS() bool
|
||||
FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (*ConnectionOwner, error)
|
||||
StartDefaultInterfaceMonitor(listener InterfaceUpdateListener) error
|
||||
CloseDefaultInterfaceMonitor(listener InterfaceUpdateListener) error
|
||||
GetInterfaces() (NetworkInterfaceIterator, error)
|
||||
UnderNetworkExtension() bool
|
||||
IncludeAllNetworks() bool
|
||||
ReadWIFIState() *WIFIState
|
||||
SystemCertificates() StringIterator
|
||||
ClearDNSCache()
|
||||
SendNotification(notification *Notification) error
|
||||
}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
androidPackageNames []string
|
||||
}
|
||||
|
||||
func (c *ConnectionOwner) SetAndroidPackageNames(names StringIterator) {
|
||||
c.androidPackageNames = iteratorToArray[string](names)
|
||||
}
|
||||
|
||||
func (c *ConnectionOwner) AndroidPackageNames() StringIterator {
|
||||
return newIterator(c.androidPackageNames)
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
UpdateDefaultInterface(interfaceName string, interfaceIndex int32, isExpensive bool, isConstrained bool)
|
||||
}
|
||||
|
||||
const (
|
||||
InterfaceTypeWIFI = int32(C.InterfaceTypeWIFI)
|
||||
InterfaceTypeCellular = int32(C.InterfaceTypeCellular)
|
||||
InterfaceTypeEthernet = int32(C.InterfaceTypeEthernet)
|
||||
InterfaceTypeOther = int32(C.InterfaceTypeOther)
|
||||
)
|
||||
|
||||
type NetworkInterface struct {
|
||||
Index int32
|
||||
MTU int32
|
||||
Name string
|
||||
Addresses StringIterator
|
||||
Flags int32
|
||||
|
||||
Type int32
|
||||
DNSServer StringIterator
|
||||
Metered bool
|
||||
}
|
||||
|
||||
type WIFIState struct {
|
||||
SSID string
|
||||
BSSID string
|
||||
}
|
||||
|
||||
func NewWIFIState(wifiSSID string, wifiBSSID string) *WIFIState {
|
||||
return &WIFIState{wifiSSID, wifiBSSID}
|
||||
}
|
||||
|
||||
type NetworkInterfaceIterator interface {
|
||||
Next() *NetworkInterface
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
Identifier string
|
||||
TypeName string
|
||||
TypeID int32
|
||||
Title string
|
||||
Subtitle string
|
||||
Body string
|
||||
OpenURL string
|
||||
}
|
||||
|
||||
type OnDemandRule interface {
|
||||
Target() int32
|
||||
DNSSearchDomainMatch() StringIterator
|
||||
DNSServerAddressMatch() StringIterator
|
||||
InterfaceTypeMatch() int32
|
||||
SSIDMatch() StringIterator
|
||||
ProbeURL() string
|
||||
}
|
||||
|
||||
type OnDemandRuleIterator interface {
|
||||
Next() OnDemandRule
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type onDemandRule struct {
|
||||
option.OnDemandRule
|
||||
}
|
||||
|
||||
func (r *onDemandRule) Target() int32 {
|
||||
if r.OnDemandRule.Action == nil {
|
||||
return -1
|
||||
}
|
||||
return int32(*r.OnDemandRule.Action)
|
||||
}
|
||||
|
||||
func (r *onDemandRule) DNSSearchDomainMatch() StringIterator {
|
||||
return newIterator(r.OnDemandRule.DNSSearchDomainMatch)
|
||||
}
|
||||
|
||||
func (r *onDemandRule) DNSServerAddressMatch() StringIterator {
|
||||
return newIterator(r.OnDemandRule.DNSServerAddressMatch)
|
||||
}
|
||||
|
||||
func (r *onDemandRule) InterfaceTypeMatch() int32 {
|
||||
if r.OnDemandRule.InterfaceTypeMatch == nil {
|
||||
return -1
|
||||
}
|
||||
return int32(*r.OnDemandRule.InterfaceTypeMatch)
|
||||
}
|
||||
|
||||
func (r *onDemandRule) SSIDMatch() StringIterator {
|
||||
return newIterator(r.OnDemandRule.SSIDMatch)
|
||||
}
|
||||
|
||||
func (r *onDemandRule) ProbeURL() string {
|
||||
return r.OnDemandRule.ProbeURL
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
)
|
||||
|
||||
func EncodeChunkedMessage(data []byte) []byte {
|
||||
var buffer bytes.Buffer
|
||||
binary.Write(&buffer, binary.BigEndian, uint16(len(data)))
|
||||
buffer.Write(data)
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func DecodeLengthChunk(data []byte) int32 {
|
||||
return int32(binary.BigEndian.Uint16(data))
|
||||
}
|
||||
|
||||
const (
|
||||
MessageTypeError = iota
|
||||
MessageTypeProfileList
|
||||
MessageTypeProfileContentRequest
|
||||
MessageTypeProfileContent
|
||||
)
|
||||
|
||||
type ErrorMessage struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *ErrorMessage) Encode() []byte {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteByte(MessageTypeError)
|
||||
writeString(&buffer, e.Message)
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func DecodeErrorMessage(data []byte) (*ErrorMessage, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
messageType, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if messageType != MessageTypeError {
|
||||
return nil, E.New("invalid message")
|
||||
}
|
||||
var message ErrorMessage
|
||||
message.Message, err = readString(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &message, nil
|
||||
}
|
||||
|
||||
const (
|
||||
ProfileTypeLocal int32 = iota
|
||||
ProfileTypeiCloud
|
||||
ProfileTypeRemote
|
||||
)
|
||||
|
||||
type ProfilePreview struct {
|
||||
ProfileID int64
|
||||
Name string
|
||||
Type int32
|
||||
}
|
||||
|
||||
type ProfilePreviewIterator interface {
|
||||
Next() *ProfilePreview
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type ProfileEncoder struct {
|
||||
profiles []ProfilePreview
|
||||
}
|
||||
|
||||
func (e *ProfileEncoder) Append(profile *ProfilePreview) {
|
||||
e.profiles = append(e.profiles, *profile)
|
||||
}
|
||||
|
||||
func (e *ProfileEncoder) Encode() []byte {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteByte(MessageTypeProfileList)
|
||||
binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles)))
|
||||
for _, preview := range e.profiles {
|
||||
binary.Write(&buffer, binary.BigEndian, preview.ProfileID)
|
||||
writeString(&buffer, preview.Name)
|
||||
binary.Write(&buffer, binary.BigEndian, preview.Type)
|
||||
}
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
type ProfileDecoder struct {
|
||||
profiles []*ProfilePreview
|
||||
}
|
||||
|
||||
func (d *ProfileDecoder) Decode(data []byte) error {
|
||||
reader := bytes.NewReader(data)
|
||||
messageType, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if messageType != MessageTypeProfileList {
|
||||
return E.New("invalid message")
|
||||
}
|
||||
var profileCount uint16
|
||||
err = binary.Read(reader, binary.BigEndian, &profileCount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := 0; i < int(profileCount); i++ {
|
||||
var profile ProfilePreview
|
||||
err = binary.Read(reader, binary.BigEndian, &profile.ProfileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
profile.Name, err = readString(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Read(reader, binary.BigEndian, &profile.Type)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.profiles = append(d.profiles, &profile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *ProfileDecoder) Iterator() ProfilePreviewIterator {
|
||||
return newIterator(d.profiles)
|
||||
}
|
||||
|
||||
type ProfileContentRequest struct {
|
||||
ProfileID int64
|
||||
}
|
||||
|
||||
func (r *ProfileContentRequest) Encode() []byte {
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteByte(MessageTypeProfileContentRequest)
|
||||
binary.Write(&buffer, binary.BigEndian, r.ProfileID)
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func DecodeProfileContentRequest(data []byte) (*ProfileContentRequest, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
messageType, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if messageType != MessageTypeProfileContentRequest {
|
||||
return nil, E.New("invalid message")
|
||||
}
|
||||
var request ProfileContentRequest
|
||||
err = binary.Read(reader, binary.BigEndian, &request.ProfileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &request, nil
|
||||
}
|
||||
|
||||
type ProfileContent struct {
|
||||
Name string
|
||||
Type int32
|
||||
Config string
|
||||
RemotePath string
|
||||
AutoUpdate bool
|
||||
AutoUpdateInterval int32
|
||||
LastUpdated int64
|
||||
}
|
||||
|
||||
func (c *ProfileContent) Encode() []byte {
|
||||
buffer := new(bytes.Buffer)
|
||||
buffer.WriteByte(MessageTypeProfileContent)
|
||||
buffer.WriteByte(1)
|
||||
gWriter := gzip.NewWriter(buffer)
|
||||
writer := bufio.NewWriter(gWriter)
|
||||
writeStringBuffered(writer, c.Name)
|
||||
binary.Write(writer, binary.BigEndian, c.Type)
|
||||
writeStringBuffered(writer, c.Config)
|
||||
if c.Type != ProfileTypeLocal {
|
||||
writeStringBuffered(writer, c.RemotePath)
|
||||
}
|
||||
if c.Type == ProfileTypeRemote {
|
||||
binary.Write(writer, binary.BigEndian, c.AutoUpdate)
|
||||
binary.Write(writer, binary.BigEndian, c.AutoUpdateInterval)
|
||||
binary.Write(writer, binary.BigEndian, c.LastUpdated)
|
||||
}
|
||||
writer.Flush()
|
||||
gWriter.Flush()
|
||||
gWriter.Close()
|
||||
return buffer.Bytes()
|
||||
}
|
||||
|
||||
func DecodeProfileContent(data []byte) (*ProfileContent, error) {
|
||||
reader := bytes.NewReader(data)
|
||||
messageType, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if messageType != MessageTypeProfileContent {
|
||||
return nil, E.New("invalid message")
|
||||
}
|
||||
version, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gReader, err := gzip.NewReader(reader)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unsupported profile")
|
||||
}
|
||||
bReader := varbin.StubReader(gReader)
|
||||
var content ProfileContent
|
||||
content.Name, err = readString(bReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = binary.Read(bReader, binary.BigEndian, &content.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content.Config, err = readString(bReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if content.Type != ProfileTypeLocal {
|
||||
content.RemotePath, err = readString(bReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if content.Type == ProfileTypeRemote || (version == 0 && content.Type != ProfileTypeLocal) {
|
||||
err = binary.Read(bReader, binary.BigEndian, &content.AutoUpdate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if version >= 1 {
|
||||
err = binary.Read(bReader, binary.BigEndian, &content.AutoUpdateInterval)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
err = binary.Read(bReader, binary.BigEndian, &content.LastUpdated)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &content, nil
|
||||
}
|
||||
|
||||
func readString(reader io.ByteReader) (string, error) {
|
||||
length, err := binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf := make([]byte, length)
|
||||
for i := range buf {
|
||||
buf[i], err = reader.ReadByte()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func writeString(buffer *bytes.Buffer, value string) {
|
||||
varbin.WriteUvarint(buffer, uint64(len(value)))
|
||||
buffer.WriteString(value)
|
||||
}
|
||||
|
||||
func writeStringBuffered(writer *bufio.Writer, value string) {
|
||||
varbin.WriteUvarint(writer, uint64(len(value)))
|
||||
writer.WriteString(value)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func GenerateRemoteProfileImportLink(name string, remoteURL string) string {
|
||||
importLink := &url.URL{
|
||||
Scheme: "sing-box",
|
||||
Host: "import-remote-profile",
|
||||
RawQuery: url.Values{"url": []string{remoteURL}}.Encode(),
|
||||
Fragment: name,
|
||||
}
|
||||
return importLink.String()
|
||||
}
|
||||
|
||||
type ImportRemoteProfile struct {
|
||||
Name string
|
||||
URL string
|
||||
Host string
|
||||
}
|
||||
|
||||
func ParseRemoteProfileImportLink(importLink string) (*ImportRemoteProfile, error) {
|
||||
importURL, err := url.Parse(importLink)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteURL, err := url.Parse(importURL.Query().Get("url"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
name := importURL.Fragment
|
||||
if name == "" {
|
||||
name = remoteURL.Host
|
||||
}
|
||||
return &ImportRemoteProfile{
|
||||
Name: name,
|
||||
URL: remoteURL.String(),
|
||||
Host: remoteURL.Host,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
func CompareSemver(left string, right string) bool {
|
||||
normalizedLeft := normalizeSemver(left)
|
||||
if !semver.IsValid(normalizedLeft) {
|
||||
return false
|
||||
}
|
||||
normalizedRight := normalizeSemver(right)
|
||||
if !semver.IsValid(normalizedRight) {
|
||||
return false
|
||||
}
|
||||
return semver.Compare(normalizedLeft, normalizedRight) > 0
|
||||
}
|
||||
|
||||
func normalizeSemver(version string) string {
|
||||
trimmedVersion := strings.TrimSpace(version)
|
||||
if strings.HasPrefix(trimmedVersion, "v") {
|
||||
return trimmedVersion
|
||||
}
|
||||
return "v" + trimmedVersion
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCompareSemver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.False(t, CompareSemver("1.13.0-rc.4", "1.13.0"))
|
||||
require.True(t, CompareSemver("1.13.1", "1.13.0"))
|
||||
require.False(t, CompareSemver("v1.13.0", "1.13.0"))
|
||||
require.False(t, CompareSemver("1.13.0-", "1.13.0"))
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/internal/procfs"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
tun "github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
var _ adapter.PlatformInterface = (*platformInterfaceWrapper)(nil)
|
||||
|
||||
type platformInterfaceWrapper struct {
|
||||
iif PlatformInterface
|
||||
useProcFS bool
|
||||
networkManager adapter.NetworkManager
|
||||
myTunName string
|
||||
myTunAddress []netip.Addr
|
||||
defaultInterfaceAccess sync.Mutex
|
||||
defaultInterface *control.Interface
|
||||
isExpensive bool
|
||||
isConstrained bool
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) Initialize(networkManager adapter.NetworkManager) error {
|
||||
w.networkManager = networkManager
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) UsePlatformAutoDetectInterfaceControl() bool {
|
||||
return w.iif.UsePlatformAutoDetectInterfaceControl()
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) AutoDetectInterfaceControl(fd int) error {
|
||||
return w.iif.AutoDetectInterfaceControl(int32(fd))
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) UsePlatformInterface() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
|
||||
if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 {
|
||||
return nil, E.New("platform: unsupported uid options")
|
||||
}
|
||||
if len(options.IncludeAndroidUser) > 0 {
|
||||
return nil, E.New("platform: unsupported android_user option")
|
||||
}
|
||||
routeRanges, err := options.BuildAutoRouteRanges(true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tunFd, err := w.iif.OpenTun(&tunOptions{options, routeRanges, platformOptions})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options.Name, err = getTunnelName(tunFd)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "query tun name")
|
||||
}
|
||||
options.InterfaceMonitor.RegisterMyInterface(options.Name)
|
||||
dupFd, err := dup(int(tunFd))
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "dup tun file descriptor")
|
||||
}
|
||||
options.FileDescriptor = dupFd
|
||||
w.myTunName = options.Name
|
||||
w.myTunAddress = myTunAddress(options)
|
||||
return tun.New(*options)
|
||||
}
|
||||
|
||||
func myTunAddress(options *tun.Options) []netip.Addr {
|
||||
addresses := make([]netip.Addr, 0, len(options.Inet4Address)+len(options.Inet6Address))
|
||||
for _, prefix := range options.Inet4Address {
|
||||
addresses = append(addresses, prefix.Addr())
|
||||
}
|
||||
for _, prefix := range options.Inet6Address {
|
||||
addresses = append(addresses, prefix.Addr())
|
||||
}
|
||||
return addresses
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) MyInterfaceAddress() []netip.Addr {
|
||||
return w.myTunAddress
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor {
|
||||
return &platformDefaultInterfaceMonitor{
|
||||
platformInterfaceWrapper: w,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) UsePlatformNetworkInterfaces() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) NetworkInterfaces() ([]adapter.NetworkInterface, error) {
|
||||
interfaceIterator, err := w.iif.GetInterfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var interfaces []adapter.NetworkInterface
|
||||
for _, netInterface := range iteratorToArray[*NetworkInterface](interfaceIterator) {
|
||||
w.defaultInterfaceAccess.Lock()
|
||||
// (GOOS=windows) SA4006: this value of `isDefault` is never used
|
||||
// Why not used?
|
||||
//nolint:staticcheck
|
||||
isDefault := netInterface.Name != w.myTunName && w.defaultInterface != nil && int(netInterface.Index) == w.defaultInterface.Index
|
||||
w.defaultInterfaceAccess.Unlock()
|
||||
interfaces = append(interfaces, adapter.NetworkInterface{
|
||||
Interface: control.Interface{
|
||||
Index: int(netInterface.Index),
|
||||
MTU: int(netInterface.MTU),
|
||||
Name: netInterface.Name,
|
||||
Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix),
|
||||
Flags: linkFlags(uint32(netInterface.Flags)),
|
||||
},
|
||||
Type: C.InterfaceType(netInterface.Type),
|
||||
DNSServers: iteratorToArray[string](netInterface.DNSServer),
|
||||
Expensive: netInterface.Metered || isDefault && w.isExpensive,
|
||||
Constrained: isDefault && w.isConstrained,
|
||||
})
|
||||
}
|
||||
interfaces = common.UniqBy(interfaces, func(it adapter.NetworkInterface) string {
|
||||
return it.Name
|
||||
})
|
||||
return interfaces, nil
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) UnderNetworkExtension() bool {
|
||||
return w.iif.UnderNetworkExtension()
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) NetworkExtensionIncludeAllNetworks() bool {
|
||||
return w.iif.IncludeAllNetworks()
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) ClearDNSCache() {
|
||||
w.iif.ClearDNSCache()
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) RequestPermissionForWIFIState() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) UsePlatformWIFIMonitor() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
|
||||
wifiState := w.iif.ReadWIFIState()
|
||||
if wifiState == nil {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
return adapter.WIFIState(*wifiState)
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) SystemCertificates() []string {
|
||||
return iteratorToArray[string](w.iif.SystemCertificates())
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) UsePlatformConnectionOwnerFinder() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) FindConnectionOwner(request *adapter.FindConnectionOwnerRequest) (*adapter.ConnectionOwner, error) {
|
||||
if w.useProcFS {
|
||||
var source netip.AddrPort
|
||||
var destination netip.AddrPort
|
||||
sourceAddr, _ := netip.ParseAddr(request.SourceAddress)
|
||||
source = netip.AddrPortFrom(sourceAddr, uint16(request.SourcePort))
|
||||
destAddr, _ := netip.ParseAddr(request.DestinationAddress)
|
||||
destination = netip.AddrPortFrom(destAddr, uint16(request.DestinationPort))
|
||||
|
||||
var network string
|
||||
switch request.IpProtocol {
|
||||
case int32(syscall.IPPROTO_TCP):
|
||||
network = "tcp"
|
||||
case int32(syscall.IPPROTO_UDP):
|
||||
network = "udp"
|
||||
default:
|
||||
return nil, E.New("unknown protocol: ", request.IpProtocol)
|
||||
}
|
||||
|
||||
uid := procfs.ResolveSocketByProcSearch(network, source, destination)
|
||||
if uid == -1 {
|
||||
return nil, E.New("procfs: not found")
|
||||
}
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: uid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
result, err := w.iif.FindConnectionOwner(request.IpProtocol, request.SourceAddress, request.SourcePort, request.DestinationAddress, request.DestinationPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: result.UserId,
|
||||
UserName: result.UserName,
|
||||
ProcessPath: result.ProcessPath,
|
||||
AndroidPackageNames: result.androidPackageNames,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) DisableColors() bool {
|
||||
return runtime.GOOS != "android"
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) UsePlatformNotification() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) SendNotification(notification *adapter.Notification) error {
|
||||
return w.iif.SendNotification((*Notification)(notification))
|
||||
}
|
||||
|
||||
func AvailablePort(startPort int32) (int32, error) {
|
||||
for port := int(startPort); ; port++ {
|
||||
if port > 65535 {
|
||||
return 0, E.New("no available port found")
|
||||
}
|
||||
listener, err := net.Listen("tcp", net.JoinHostPort("127.0.0.1", strconv.Itoa(int(port))))
|
||||
if err != nil {
|
||||
if errors.Is(err, syscall.EADDRINUSE) {
|
||||
continue
|
||||
}
|
||||
return 0, E.Cause(err, "find available port")
|
||||
}
|
||||
err = listener.Close()
|
||||
if err != nil {
|
||||
return 0, E.Cause(err, "close listener")
|
||||
}
|
||||
return int32(port), nil
|
||||
}
|
||||
}
|
||||
|
||||
func RandomHex(length int32) *StringBox {
|
||||
bytes := make([]byte, length)
|
||||
common.Must1(rand.Read(bytes))
|
||||
return wrapString(hex.EncodeToString(bytes))
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package libbox
|
||||
|
||||
import "syscall"
|
||||
|
||||
func dup(fd int) (nfd int, err error) {
|
||||
return syscall.Dup(fd)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package libbox
|
||||
|
||||
import "os"
|
||||
|
||||
func dup(fd int) (nfd int, err error) {
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/locale"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
)
|
||||
|
||||
var (
|
||||
sBasePath string
|
||||
sWorkingPath string
|
||||
sTempPath string
|
||||
sUserID int
|
||||
sGroupID int
|
||||
sFixAndroidStack bool
|
||||
sCommandServerListenPort uint16
|
||||
sCommandServerSecret string
|
||||
sLogMaxLines int
|
||||
sDebug bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
debug.SetPanicOnFault(true)
|
||||
debug.SetTraceback("all")
|
||||
}
|
||||
|
||||
type SetupOptions struct {
|
||||
BasePath string
|
||||
WorkingPath string
|
||||
TempPath string
|
||||
FixAndroidStack bool
|
||||
CommandServerListenPort int32
|
||||
CommandServerSecret string
|
||||
LogMaxLines int
|
||||
Debug bool
|
||||
}
|
||||
|
||||
func Setup(options *SetupOptions) error {
|
||||
sBasePath = options.BasePath
|
||||
sWorkingPath = options.WorkingPath
|
||||
sTempPath = options.TempPath
|
||||
|
||||
sUserID = os.Getuid()
|
||||
sGroupID = os.Getgid()
|
||||
|
||||
// TODO: remove after fixed
|
||||
// https://github.com/golang/go/issues/68760
|
||||
sFixAndroidStack = options.FixAndroidStack
|
||||
|
||||
sCommandServerListenPort = uint16(options.CommandServerListenPort)
|
||||
sCommandServerSecret = options.CommandServerSecret
|
||||
sLogMaxLines = options.LogMaxLines
|
||||
sDebug = options.Debug
|
||||
|
||||
os.MkdirAll(sWorkingPath, 0o777)
|
||||
os.MkdirAll(sTempPath, 0o777)
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetLocale(localeId string) {
|
||||
locale.Set(localeId)
|
||||
}
|
||||
|
||||
func Version() string {
|
||||
return C.Version
|
||||
}
|
||||
|
||||
func FormatBytes(length int64) string {
|
||||
return byteformats.FormatKBytes(uint64(length))
|
||||
}
|
||||
|
||||
func FormatMemoryBytes(length int64) string {
|
||||
return byteformats.FormatMemoryKBytes(uint64(length))
|
||||
}
|
||||
|
||||
func FormatDuration(duration int64) string {
|
||||
return log.FormatDuration(time.Duration(duration) * time.Millisecond)
|
||||
}
|
||||
|
||||
func ProxyDisplayType(proxyType string) string {
|
||||
return C.ProxyDisplayName(proxyType)
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
tun "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() (*StringBox, error)
|
||||
GetMTU() int32
|
||||
GetAutoRoute() bool
|
||||
GetStrictRoute() bool
|
||||
GetInet4RouteAddress() RoutePrefixIterator
|
||||
GetInet6RouteAddress() RoutePrefixIterator
|
||||
GetInet4RouteExcludeAddress() RoutePrefixIterator
|
||||
GetInet6RouteExcludeAddress() RoutePrefixIterator
|
||||
GetInet4RouteRange() RoutePrefixIterator
|
||||
GetInet6RouteRange() RoutePrefixIterator
|
||||
GetIncludePackage() StringIterator
|
||||
GetExcludePackage() StringIterator
|
||||
IsHTTPProxyEnabled() bool
|
||||
GetHTTPProxyServer() string
|
||||
GetHTTPProxyServerPort() int32
|
||||
GetHTTPProxyBypassDomain() StringIterator
|
||||
GetHTTPProxyMatchDomain() StringIterator
|
||||
}
|
||||
|
||||
type RoutePrefix struct {
|
||||
address netip.Addr
|
||||
prefix int
|
||||
}
|
||||
|
||||
func (p *RoutePrefix) Address() string {
|
||||
return p.address.String()
|
||||
}
|
||||
|
||||
func (p *RoutePrefix) Prefix() int32 {
|
||||
return int32(p.prefix)
|
||||
}
|
||||
|
||||
func (p *RoutePrefix) Mask() string {
|
||||
var bits int
|
||||
if p.address.Is6() {
|
||||
bits = 128
|
||||
} else {
|
||||
bits = 32
|
||||
}
|
||||
return net.IP(net.CIDRMask(p.prefix, bits)).String()
|
||||
}
|
||||
|
||||
func (p *RoutePrefix) String() string {
|
||||
return netip.PrefixFrom(p.address, p.prefix).String()
|
||||
}
|
||||
|
||||
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(),
|
||||
prefix: prefix.Bits(),
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
var _ TunOptions = (*tunOptions)(nil)
|
||||
|
||||
type tunOptions struct {
|
||||
*tun.Options
|
||||
routeRanges []netip.Prefix
|
||||
option.TunPlatformOptions
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetInet4Address() RoutePrefixIterator {
|
||||
return mapRoutePrefix(o.Inet4Address)
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetInet6Address() RoutePrefixIterator {
|
||||
return mapRoutePrefix(o.Inet6Address)
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetDNSServerAddress() (*StringBox, error) {
|
||||
if len(o.Inet4Address) == 0 || o.Inet4Address[0].Bits() == 32 {
|
||||
return nil, E.New("need one more IPv4 address for DNS hijacking")
|
||||
}
|
||||
return wrapString(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) GetInet4RouteExcludeAddress() RoutePrefixIterator {
|
||||
return mapRoutePrefix(o.Inet4RouteExcludeAddress)
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetInet6RouteExcludeAddress() RoutePrefixIterator {
|
||||
return mapRoutePrefix(o.Inet6RouteExcludeAddress)
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetInet4RouteRange() RoutePrefixIterator {
|
||||
return mapRoutePrefix(common.Filter(o.routeRanges, func(it netip.Prefix) bool {
|
||||
return it.Addr().Is4()
|
||||
}))
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetInet6RouteRange() RoutePrefixIterator {
|
||||
return mapRoutePrefix(common.Filter(o.routeRanges, func(it netip.Prefix) bool {
|
||||
return it.Addr().Is6()
|
||||
}))
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetIncludePackage() StringIterator {
|
||||
return newIterator(o.IncludePackage)
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetExcludePackage() StringIterator {
|
||||
return newIterator(o.ExcludePackage)
|
||||
}
|
||||
|
||||
func (o *tunOptions) IsHTTPProxyEnabled() bool {
|
||||
if o.TunPlatformOptions.HTTPProxy == nil {
|
||||
return false
|
||||
}
|
||||
return o.TunPlatformOptions.HTTPProxy.Enabled
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetHTTPProxyServer() string {
|
||||
return o.TunPlatformOptions.HTTPProxy.Server
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetHTTPProxyServerPort() int32 {
|
||||
return int32(o.TunPlatformOptions.HTTPProxy.ServerPort)
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetHTTPProxyBypassDomain() StringIterator {
|
||||
return newIterator(o.TunPlatformOptions.HTTPProxy.BypassDomain)
|
||||
}
|
||||
|
||||
func (o *tunOptions) GetHTTPProxyMatchDomain() StringIterator {
|
||||
return newIterator(o.TunPlatformOptions.HTTPProxy.MatchDomain)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// kanged from wireauard-apple
|
||||
|
||||
const utunControlName = "com.apple.net.utun_control"
|
||||
|
||||
func GetTunnelFileDescriptor() int32 {
|
||||
ctlInfo := &unix.CtlInfo{}
|
||||
copy(ctlInfo.Name[:], utunControlName)
|
||||
for fd := 0; fd < 1024; fd++ {
|
||||
addr, err := unix.Getpeername(fd)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
addrCTL, loaded := addr.(*unix.SockaddrCtl)
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
if ctlInfo.Id == 0 {
|
||||
err = unix.IoctlCtlInfo(fd, ctlInfo)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if addrCTL.ID == ctlInfo.Id {
|
||||
return int32(fd)
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package libbox
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
func getTunnelName(fd int32) (string, error) {
|
||||
return unix.GetsockoptString(
|
||||
int(fd),
|
||||
2, /* #define SYSPROTO_CONTROL 2 */
|
||||
2, /* #define UTUN_OPT_IFNAME 2 */
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const ifReqSize = unix.IFNAMSIZ + 64
|
||||
|
||||
func getTunnelName(fd int32) (string, error) {
|
||||
var ifr [ifReqSize]byte
|
||||
var errno syscall.Errno
|
||||
_, _, errno = unix.Syscall(
|
||||
unix.SYS_IOCTL,
|
||||
uintptr(fd),
|
||||
uintptr(unix.TUNGETIFF),
|
||||
uintptr(unsafe.Pointer(&ifr[0])),
|
||||
)
|
||||
if errno != 0 {
|
||||
return "", fmt.Errorf("failed to get name of TUN device: %w", errno)
|
||||
}
|
||||
return unix.ByteSliceToString(ifr[:]), nil
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !(darwin || linux)
|
||||
|
||||
package libbox
|
||||
|
||||
import "os"
|
||||
|
||||
func getTunnelName(fd int32) (string, error) {
|
||||
return "", os.ErrInvalid
|
||||
}
|
||||
Reference in New Issue
Block a user