First Commmit
This commit is contained in:
234
experimental/libbox/build_info.go
Normal file
234
experimental/libbox/build_info.go
Normal file
@@ -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
|
||||
}
|
||||
10
experimental/libbox/command.go
Normal file
10
experimental/libbox/command.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package libbox
|
||||
|
||||
const (
|
||||
CommandLog int32 = iota
|
||||
CommandStatus
|
||||
CommandGroup
|
||||
CommandClashMode
|
||||
CommandConnections
|
||||
CommandOutbounds
|
||||
)
|
||||
807
experimental/libbox/command_client.go
Normal file
807
experimental/libbox/command_client.go
Normal file
@@ -0,0 +1,807 @@
|
||||
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/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
"google.golang.org/grpc/status"
|
||||
"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)
|
||||
WriteOutbounds(message OutboundGroupItemIterator)
|
||||
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, E.Cause(err, "dial xpc")
|
||||
}
|
||||
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, E.Cause(err, "create command client")
|
||||
}
|
||||
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, E.Cause(lastError, "probe command server")
|
||||
}
|
||||
|
||||
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()
|
||||
case CommandOutbounds:
|
||||
go c.handleOutboundsStream()
|
||||
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, E.Cause(err, "get command client")
|
||||
}
|
||||
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(E.Cause(err, "subscribe log").Error())
|
||||
return
|
||||
}
|
||||
defaultLogLevel, err := client.GetDefaultLogLevel(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "get default log level").Error())
|
||||
return
|
||||
}
|
||||
c.handler.SetDefaultLogLevel(int32(defaultLogLevel.Level))
|
||||
for {
|
||||
logMessage, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "log stream recv").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(E.Cause(err, "subscribe status").Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
status, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "status stream recv").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(E.Cause(err, "subscribe groups").Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
groups, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "groups stream recv").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(E.Cause(err, "get clash mode status").Error())
|
||||
return
|
||||
}
|
||||
|
||||
if sFixAndroidStack {
|
||||
go func() {
|
||||
c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
|
||||
if len(modeStatus.ModeList) == 0 {
|
||||
c.handler.Disconnected(E.Cause(os.ErrInvalid, "empty clash mode list").Error())
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
c.handler.InitializeClashMode(newIterator(modeStatus.ModeList), modeStatus.CurrentMode)
|
||||
if len(modeStatus.ModeList) == 0 {
|
||||
c.handler.Disconnected(E.Cause(os.ErrInvalid, "empty clash mode list").Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(modeStatus.ModeList) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
stream, err := client.SubscribeClashMode(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "subscribe clash mode").Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
mode, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "clash mode stream recv").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(E.Cause(err, "subscribe connections").Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
events, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "connections stream recv").Error())
|
||||
return
|
||||
}
|
||||
libboxEvents := connectionEventsFromGRPC(events)
|
||||
c.handler.WriteConnectionEvents(libboxEvents)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) handleOutboundsStream() {
|
||||
client, ctx := c.getStreamContext()
|
||||
|
||||
stream, err := client.SubscribeOutbounds(ctx, &emptypb.Empty{})
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "subscribe outbounds").Error())
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
list, err := stream.Recv()
|
||||
if err != nil {
|
||||
c.handler.Disconnected(E.Cause(err, "outbounds stream recv").Error())
|
||||
return
|
||||
}
|
||||
c.handler.WriteOutbounds(outboundGroupItemListFromGRPC(list))
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "select outbound")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "url test")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "set clash mode")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "close connection")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) CloseConnections() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.CloseAllConnections(context.Background(), &emptypb.Empty{})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "close all connections")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) ServiceReload() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.ReloadService(context.Background(), &emptypb.Empty{})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "reload service")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) ServiceClose() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.StopService(context.Background(), &emptypb.Empty{})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "stop service")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) ClearLogs() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.ClearLogs(context.Background(), &emptypb.Empty{})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "clear logs")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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, E.Cause(err, "get system proxy status")
|
||||
}
|
||||
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,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "set system proxy enabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) TriggerGoCrash() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.TriggerDebugCrash(context.Background(), &daemon.DebugCrashRequest{
|
||||
Type: daemon.DebugCrashRequest_GO,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "trigger debug crash")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) TriggerNativeCrash() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.TriggerDebugCrash(context.Background(), &daemon.DebugCrashRequest{
|
||||
Type: daemon.DebugCrashRequest_NATIVE,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "trigger native crash")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) TriggerOOMReport() error {
|
||||
_, err := callWithResult(c, func(client daemon.StartedServiceClient) (*emptypb.Empty, error) {
|
||||
return client.TriggerOOMReport(context.Background(), &emptypb.Empty{})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "trigger oom report")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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, E.Cause(err, "get deprecated warnings")
|
||||
}
|
||||
var notes []*DeprecatedNote
|
||||
for _, warning := range warnings.Warnings {
|
||||
notes = append(notes, &DeprecatedNote{
|
||||
Description: warning.Description,
|
||||
DeprecatedVersion: warning.DeprecatedVersion,
|
||||
ScheduledVersion: warning.ScheduledVersion,
|
||||
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, E.Cause(err, "get started at")
|
||||
}
|
||||
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,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "set group expand")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CommandClient) StartNetworkQualityTest(configURL string, outboundTag string, serial bool, maxRuntimeSeconds int32, http3 bool, handler NetworkQualityTestHandler) error {
|
||||
client, err := c.getClientForCall()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start network quality test")
|
||||
}
|
||||
if c.standalone {
|
||||
defer c.closeConnection()
|
||||
}
|
||||
stream, err := client.StartNetworkQualityTest(context.Background(), &daemon.NetworkQualityTestRequest{
|
||||
ConfigURL: configURL,
|
||||
OutboundTag: outboundTag,
|
||||
Serial: serial,
|
||||
MaxRuntimeSeconds: maxRuntimeSeconds,
|
||||
Http3: http3,
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "start network quality test")
|
||||
}
|
||||
for {
|
||||
event, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
recvErr = E.Cause(recvErr, "network quality test recv")
|
||||
handler.OnError(recvErr.Error())
|
||||
return recvErr
|
||||
}
|
||||
if event.IsFinal {
|
||||
if event.Error != "" {
|
||||
handler.OnError(event.Error)
|
||||
} else {
|
||||
handler.OnResult(&NetworkQualityResult{
|
||||
DownloadCapacity: event.DownloadCapacity,
|
||||
UploadCapacity: event.UploadCapacity,
|
||||
DownloadRPM: event.DownloadRPM,
|
||||
UploadRPM: event.UploadRPM,
|
||||
IdleLatencyMs: event.IdleLatencyMs,
|
||||
DownloadCapacityAccuracy: event.DownloadCapacityAccuracy,
|
||||
UploadCapacityAccuracy: event.UploadCapacityAccuracy,
|
||||
DownloadRPMAccuracy: event.DownloadRPMAccuracy,
|
||||
UploadRPMAccuracy: event.UploadRPMAccuracy,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
handler.OnProgress(networkQualityProgressFromGRPC(event))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) StartSTUNTest(server string, outboundTag string, handler STUNTestHandler) error {
|
||||
client, err := c.getClientForCall()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start stun test")
|
||||
}
|
||||
if c.standalone {
|
||||
defer c.closeConnection()
|
||||
}
|
||||
stream, err := client.StartSTUNTest(context.Background(), &daemon.STUNTestRequest{
|
||||
Server: server,
|
||||
OutboundTag: outboundTag,
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "start stun test")
|
||||
}
|
||||
for {
|
||||
event, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
recvErr = E.Cause(recvErr, "stun test recv")
|
||||
handler.OnError(recvErr.Error())
|
||||
return recvErr
|
||||
}
|
||||
if event.IsFinal {
|
||||
if event.Error != "" {
|
||||
handler.OnError(event.Error)
|
||||
} else {
|
||||
handler.OnResult(&STUNTestResult{
|
||||
ExternalAddr: event.ExternalAddr,
|
||||
LatencyMs: event.LatencyMs,
|
||||
NATMapping: event.NatMapping,
|
||||
NATFiltering: event.NatFiltering,
|
||||
NATTypeSupported: event.NatTypeSupported,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
handler.OnProgress(stunTestProgressFromGRPC(event))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) SubscribeTailscaleStatus(handler TailscaleStatusHandler) error {
|
||||
client, err := c.getClientForCall()
|
||||
if err != nil {
|
||||
return E.Cause(err, "subscribe tailscale status")
|
||||
}
|
||||
if c.standalone {
|
||||
defer c.closeConnection()
|
||||
}
|
||||
stream, err := client.SubscribeTailscaleStatus(context.Background(), &emptypb.Empty{})
|
||||
if err != nil {
|
||||
return E.Cause(err, "subscribe tailscale status")
|
||||
}
|
||||
for {
|
||||
event, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
if status.Code(recvErr) == codes.NotFound || status.Code(recvErr) == codes.Unavailable {
|
||||
return nil
|
||||
}
|
||||
recvErr = E.Cause(recvErr, "tailscale status recv")
|
||||
handler.OnError(recvErr.Error())
|
||||
return recvErr
|
||||
}
|
||||
handler.OnStatusUpdate(tailscaleStatusUpdateFromGRPC(event))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CommandClient) StartTailscalePing(endpointTag string, peerIP string, handler TailscalePingHandler) error {
|
||||
client, err := c.getClientForCall()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start tailscale ping")
|
||||
}
|
||||
if c.standalone {
|
||||
defer c.closeConnection()
|
||||
}
|
||||
stream, err := client.StartTailscalePing(context.Background(), &daemon.TailscalePingRequest{
|
||||
EndpointTag: endpointTag,
|
||||
PeerIP: peerIP,
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "start tailscale ping")
|
||||
}
|
||||
for {
|
||||
event, recvErr := stream.Recv()
|
||||
if recvErr != nil {
|
||||
recvErr = E.Cause(recvErr, "tailscale ping recv")
|
||||
handler.OnError(recvErr.Error())
|
||||
return recvErr
|
||||
}
|
||||
handler.OnPingResult(tailscalePingResultFromGRPC(event))
|
||||
}
|
||||
}
|
||||
288
experimental/libbox/command_server.go
Normal file
288
experimental/libbox/command_server.go
Normal file
@@ -0,0 +1,288 @@
|
||||
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
|
||||
TriggerNativeCrash() 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,
|
||||
OOMKillerEnabled: sOOMKillerEnabled,
|
||||
OOMKillerDisabled: sOOMKillerDisabled,
|
||||
OOMMemoryLimit: uint64(sOOMMemoryLimit),
|
||||
// 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 {
|
||||
saveConfigSnapshot(configContent)
|
||||
err := s.StartedService.StartOrReloadService(configContent, &daemon.OverrideOptions{
|
||||
AutoRedirect: options.AutoRedirect,
|
||||
IncludePackage: iteratorToArray(options.IncludePackage),
|
||||
ExcludePackage: iteratorToArray(options.ExcludePackage),
|
||||
})
|
||||
if err != nil {
|
||||
return E.Cause(err, "start or reload service")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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, E.Cause(err, "get system proxy status")
|
||||
}
|
||||
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) TriggerNativeCrash() error {
|
||||
return (*CommandServer)(h).handler.TriggerNativeCrash()
|
||||
}
|
||||
|
||||
func (h *platformHandler) WriteDebugMessage(message string) {
|
||||
(*CommandServer)(h).handler.WriteDebugMessage(message)
|
||||
}
|
||||
446
experimental/libbox/command_types.go
Normal file
446
experimental/libbox/command_types.go
Normal file
@@ -0,0 +1,446 @@
|
||||
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 outboundGroupItemListFromGRPC(list *daemon.OutboundList) OutboundGroupItemIterator {
|
||||
if list == nil || len(list.Outbounds) == 0 {
|
||||
return newIterator([]*OutboundGroupItem{})
|
||||
}
|
||||
var items []*OutboundGroupItem
|
||||
for _, ob := range list.Outbounds {
|
||||
items = append(items, &OutboundGroupItem{
|
||||
Tag: ob.Tag,
|
||||
Type: ob.Type,
|
||||
URLTestTime: ob.UrlTestTime,
|
||||
URLTestDelay: ob.UrlTestDelay,
|
||||
})
|
||||
}
|
||||
return newIterator(items)
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
51
experimental/libbox/command_types_nq.go
Normal file
51
experimental/libbox/command_types_nq.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package libbox
|
||||
|
||||
import "github.com/sagernet/sing-box/daemon"
|
||||
|
||||
type NetworkQualityProgress struct {
|
||||
Phase int32
|
||||
DownloadCapacity int64
|
||||
UploadCapacity int64
|
||||
DownloadRPM int32
|
||||
UploadRPM int32
|
||||
IdleLatencyMs int32
|
||||
ElapsedMs int64
|
||||
DownloadCapacityAccuracy int32
|
||||
UploadCapacityAccuracy int32
|
||||
DownloadRPMAccuracy int32
|
||||
UploadRPMAccuracy int32
|
||||
}
|
||||
|
||||
type NetworkQualityResult struct {
|
||||
DownloadCapacity int64
|
||||
UploadCapacity int64
|
||||
DownloadRPM int32
|
||||
UploadRPM int32
|
||||
IdleLatencyMs int32
|
||||
DownloadCapacityAccuracy int32
|
||||
UploadCapacityAccuracy int32
|
||||
DownloadRPMAccuracy int32
|
||||
UploadRPMAccuracy int32
|
||||
}
|
||||
|
||||
type NetworkQualityTestHandler interface {
|
||||
OnProgress(progress *NetworkQualityProgress)
|
||||
OnResult(result *NetworkQualityResult)
|
||||
OnError(message string)
|
||||
}
|
||||
|
||||
func networkQualityProgressFromGRPC(event *daemon.NetworkQualityTestProgress) *NetworkQualityProgress {
|
||||
return &NetworkQualityProgress{
|
||||
Phase: event.Phase,
|
||||
DownloadCapacity: event.DownloadCapacity,
|
||||
UploadCapacity: event.UploadCapacity,
|
||||
DownloadRPM: event.DownloadRPM,
|
||||
UploadRPM: event.UploadRPM,
|
||||
IdleLatencyMs: event.IdleLatencyMs,
|
||||
ElapsedMs: event.ElapsedMs,
|
||||
DownloadCapacityAccuracy: event.DownloadCapacityAccuracy,
|
||||
UploadCapacityAccuracy: event.UploadCapacityAccuracy,
|
||||
DownloadRPMAccuracy: event.DownloadRPMAccuracy,
|
||||
UploadRPMAccuracy: event.UploadRPMAccuracy,
|
||||
}
|
||||
}
|
||||
35
experimental/libbox/command_types_stun.go
Normal file
35
experimental/libbox/command_types_stun.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package libbox
|
||||
|
||||
import "github.com/sagernet/sing-box/daemon"
|
||||
|
||||
type STUNTestProgress struct {
|
||||
Phase int32
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping int32
|
||||
NATFiltering int32
|
||||
}
|
||||
|
||||
type STUNTestResult struct {
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping int32
|
||||
NATFiltering int32
|
||||
NATTypeSupported bool
|
||||
}
|
||||
|
||||
type STUNTestHandler interface {
|
||||
OnProgress(progress *STUNTestProgress)
|
||||
OnResult(result *STUNTestResult)
|
||||
OnError(message string)
|
||||
}
|
||||
|
||||
func stunTestProgressFromGRPC(event *daemon.STUNTestProgress) *STUNTestProgress {
|
||||
return &STUNTestProgress{
|
||||
Phase: event.Phase,
|
||||
ExternalAddr: event.ExternalAddr,
|
||||
LatencyMs: event.LatencyMs,
|
||||
NATMapping: event.NatMapping,
|
||||
NATFiltering: event.NatFiltering,
|
||||
}
|
||||
}
|
||||
132
experimental/libbox/command_types_tailscale.go
Normal file
132
experimental/libbox/command_types_tailscale.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package libbox
|
||||
|
||||
import "github.com/sagernet/sing-box/daemon"
|
||||
|
||||
type TailscaleStatusUpdate struct {
|
||||
endpoints []*TailscaleEndpointStatus
|
||||
}
|
||||
|
||||
func (u *TailscaleStatusUpdate) Endpoints() TailscaleEndpointStatusIterator {
|
||||
return newIterator(u.endpoints)
|
||||
}
|
||||
|
||||
type TailscaleEndpointStatusIterator interface {
|
||||
Next() *TailscaleEndpointStatus
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type TailscaleEndpointStatus struct {
|
||||
EndpointTag string
|
||||
BackendState string
|
||||
AuthURL string
|
||||
NetworkName string
|
||||
MagicDNSSuffix string
|
||||
Self *TailscalePeer
|
||||
userGroups []*TailscaleUserGroup
|
||||
}
|
||||
|
||||
func (s *TailscaleEndpointStatus) UserGroups() TailscaleUserGroupIterator {
|
||||
return newIterator(s.userGroups)
|
||||
}
|
||||
|
||||
type TailscaleUserGroupIterator interface {
|
||||
Next() *TailscaleUserGroup
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type TailscaleUserGroup struct {
|
||||
UserID int64
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
peers []*TailscalePeer
|
||||
}
|
||||
|
||||
func (g *TailscaleUserGroup) Peers() TailscalePeerIterator {
|
||||
return newIterator(g.peers)
|
||||
}
|
||||
|
||||
type TailscalePeerIterator interface {
|
||||
Next() *TailscalePeer
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type TailscalePeer struct {
|
||||
HostName string
|
||||
DNSName string
|
||||
OS string
|
||||
tailscaleIPs []string
|
||||
Online bool
|
||||
ExitNode bool
|
||||
ExitNodeOption bool
|
||||
Active bool
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
KeyExpiry int64
|
||||
}
|
||||
|
||||
func (p *TailscalePeer) TailscaleIPs() StringIterator {
|
||||
return newIterator(p.tailscaleIPs)
|
||||
}
|
||||
|
||||
type TailscaleStatusHandler interface {
|
||||
OnStatusUpdate(status *TailscaleStatusUpdate)
|
||||
OnError(message string)
|
||||
}
|
||||
|
||||
func tailscaleStatusUpdateFromGRPC(update *daemon.TailscaleStatusUpdate) *TailscaleStatusUpdate {
|
||||
endpoints := make([]*TailscaleEndpointStatus, len(update.Endpoints))
|
||||
for i, endpoint := range update.Endpoints {
|
||||
endpoints[i] = tailscaleEndpointStatusFromGRPC(endpoint)
|
||||
}
|
||||
return &TailscaleStatusUpdate{endpoints: endpoints}
|
||||
}
|
||||
|
||||
func tailscaleEndpointStatusFromGRPC(status *daemon.TailscaleEndpointStatus) *TailscaleEndpointStatus {
|
||||
userGroups := make([]*TailscaleUserGroup, len(status.UserGroups))
|
||||
for i, group := range status.UserGroups {
|
||||
userGroups[i] = tailscaleUserGroupFromGRPC(group)
|
||||
}
|
||||
result := &TailscaleEndpointStatus{
|
||||
EndpointTag: status.EndpointTag,
|
||||
BackendState: status.BackendState,
|
||||
AuthURL: status.AuthURL,
|
||||
NetworkName: status.NetworkName,
|
||||
MagicDNSSuffix: status.MagicDNSSuffix,
|
||||
userGroups: userGroups,
|
||||
}
|
||||
if status.Self != nil {
|
||||
result.Self = tailscalePeerFromGRPC(status.Self)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func tailscaleUserGroupFromGRPC(group *daemon.TailscaleUserGroup) *TailscaleUserGroup {
|
||||
peers := make([]*TailscalePeer, len(group.Peers))
|
||||
for i, peer := range group.Peers {
|
||||
peers[i] = tailscalePeerFromGRPC(peer)
|
||||
}
|
||||
return &TailscaleUserGroup{
|
||||
UserID: group.UserID,
|
||||
LoginName: group.LoginName,
|
||||
DisplayName: group.DisplayName,
|
||||
ProfilePicURL: group.ProfilePicURL,
|
||||
peers: peers,
|
||||
}
|
||||
}
|
||||
|
||||
func tailscalePeerFromGRPC(peer *daemon.TailscalePeer) *TailscalePeer {
|
||||
return &TailscalePeer{
|
||||
HostName: peer.HostName,
|
||||
DNSName: peer.DnsName,
|
||||
OS: peer.Os,
|
||||
tailscaleIPs: peer.TailscaleIPs,
|
||||
Online: peer.Online,
|
||||
ExitNode: peer.ExitNode,
|
||||
ExitNodeOption: peer.ExitNodeOption,
|
||||
Active: peer.Active,
|
||||
RxBytes: peer.RxBytes,
|
||||
TxBytes: peer.TxBytes,
|
||||
KeyExpiry: peer.KeyExpiry,
|
||||
}
|
||||
}
|
||||
28
experimental/libbox/command_types_tailscale_ping.go
Normal file
28
experimental/libbox/command_types_tailscale_ping.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package libbox
|
||||
|
||||
import "github.com/sagernet/sing-box/daemon"
|
||||
|
||||
type TailscalePingResult struct {
|
||||
LatencyMs float64
|
||||
IsDirect bool
|
||||
Endpoint string
|
||||
DERPRegionID int32
|
||||
DERPRegionCode string
|
||||
Error string
|
||||
}
|
||||
|
||||
type TailscalePingHandler interface {
|
||||
OnPingResult(result *TailscalePingResult)
|
||||
OnError(message string)
|
||||
}
|
||||
|
||||
func tailscalePingResultFromGRPC(response *daemon.TailscalePingResponse) *TailscalePingResult {
|
||||
return &TailscalePingResult{
|
||||
LatencyMs: response.LatencyMs,
|
||||
IsDirect: response.IsDirect,
|
||||
Endpoint: response.Endpoint,
|
||||
DERPRegionID: response.DerpRegionID,
|
||||
DERPRegionCode: response.DerpRegionCode,
|
||||
Error: response.Error,
|
||||
}
|
||||
}
|
||||
222
experimental/libbox/config.go
Normal file
222
experimental/libbox/config.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"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"
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
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"
|
||||
)
|
||||
|
||||
var sOOMReporter oomkiller.OOMReporter
|
||||
|
||||
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)
|
||||
if sOOMReporter != nil {
|
||||
ctx = service.ContextWith[oomkiller.OOMReporter](ctx, sOOMReporter)
|
||||
}
|
||||
return box.Context(ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry(), include.CertificateProviderRegistry())
|
||||
}
|
||||
|
||||
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) UsePlatformNeighborResolver() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error {
|
||||
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
|
||||
}
|
||||
57
experimental/libbox/connection_owner_darwin.go
Normal file
57
experimental/libbox/connection_owner_darwin.go
Normal file
@@ -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
|
||||
}
|
||||
12
experimental/libbox/debug.go
Normal file
12
experimental/libbox/debug.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func TriggerGoPanic() {
|
||||
time.AfterFunc(200*time.Millisecond, func() {
|
||||
*(*int)(unsafe.Pointer(uintptr(0))) = 0
|
||||
})
|
||||
}
|
||||
33
experimental/libbox/deprecated.go
Normal file
33
experimental/libbox/deprecated.go
Normal file
@@ -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
|
||||
}
|
||||
150
experimental/libbox/dns.go
Normal file
150
experimental/libbox/dns.go
Normal file
@@ -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)
|
||||
}
|
||||
493
experimental/libbox/fdroid.go
Normal file
493
experimental/libbox/fdroid.go
Normal file
@@ -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)
|
||||
}
|
||||
92
experimental/libbox/fdroid_mirrors.go
Normal file
92
experimental/libbox/fdroid_mirrors.go
Normal file
@@ -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)
|
||||
}
|
||||
257
experimental/libbox/ffi.json
Normal file
257
experimental/libbox/ffi.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
274
experimental/libbox/http.go
Normal file
274
experimental/libbox/http.go
Normal file
@@ -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
|
||||
}
|
||||
390
experimental/libbox/internal/oomprofile/builder.go
Normal file
390
experimental/libbox/internal/oomprofile/builder.go
Normal file
@@ -0,0 +1,390 @@
|
||||
//go:build darwin || linux || windows
|
||||
|
||||
package oomprofile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
tagProfile_SampleType = 1
|
||||
tagProfile_Sample = 2
|
||||
tagProfile_Mapping = 3
|
||||
tagProfile_Location = 4
|
||||
tagProfile_Function = 5
|
||||
tagProfile_StringTable = 6
|
||||
tagProfile_TimeNanos = 9
|
||||
tagProfile_PeriodType = 11
|
||||
tagProfile_Period = 12
|
||||
tagProfile_DefaultSampleType = 14
|
||||
|
||||
tagValueType_Type = 1
|
||||
tagValueType_Unit = 2
|
||||
|
||||
tagSample_Location = 1
|
||||
tagSample_Value = 2
|
||||
tagSample_Label = 3
|
||||
|
||||
tagLabel_Key = 1
|
||||
tagLabel_Str = 2
|
||||
tagLabel_Num = 3
|
||||
|
||||
tagMapping_ID = 1
|
||||
tagMapping_Start = 2
|
||||
tagMapping_Limit = 3
|
||||
tagMapping_Offset = 4
|
||||
tagMapping_Filename = 5
|
||||
tagMapping_BuildID = 6
|
||||
tagMapping_HasFunctions = 7
|
||||
tagMapping_HasFilenames = 8
|
||||
tagMapping_HasLineNumbers = 9
|
||||
tagMapping_HasInlineFrames = 10
|
||||
|
||||
tagLocation_ID = 1
|
||||
tagLocation_MappingID = 2
|
||||
tagLocation_Address = 3
|
||||
tagLocation_Line = 4
|
||||
|
||||
tagLine_FunctionID = 1
|
||||
tagLine_Line = 2
|
||||
|
||||
tagFunction_ID = 1
|
||||
tagFunction_Name = 2
|
||||
tagFunction_SystemName = 3
|
||||
tagFunction_Filename = 4
|
||||
tagFunction_StartLine = 5
|
||||
)
|
||||
|
||||
type memMap struct {
|
||||
start uintptr
|
||||
end uintptr
|
||||
offset uint64
|
||||
file string
|
||||
buildID string
|
||||
funcs symbolizeFlag
|
||||
fake bool
|
||||
}
|
||||
|
||||
type symbolizeFlag uint8
|
||||
|
||||
const (
|
||||
lookupTried symbolizeFlag = 1 << iota
|
||||
lookupFailed
|
||||
)
|
||||
|
||||
func newProfileBuilder(w io.Writer) *profileBuilder {
|
||||
builder := &profileBuilder{
|
||||
start: time.Now(),
|
||||
w: w,
|
||||
strings: []string{""},
|
||||
stringMap: map[string]int{"": 0},
|
||||
locs: map[uintptr]locInfo{},
|
||||
funcs: map[string]int{},
|
||||
}
|
||||
builder.readMapping()
|
||||
return builder
|
||||
}
|
||||
|
||||
func (b *profileBuilder) stringIndex(s string) int64 {
|
||||
id, ok := b.stringMap[s]
|
||||
if !ok {
|
||||
id = len(b.strings)
|
||||
b.strings = append(b.strings, s)
|
||||
b.stringMap[s] = id
|
||||
}
|
||||
return int64(id)
|
||||
}
|
||||
|
||||
func (b *profileBuilder) flush() {
|
||||
const dataFlush = 4096
|
||||
if b.err != nil || b.pb.nest != 0 || len(b.pb.data) <= dataFlush {
|
||||
return
|
||||
}
|
||||
|
||||
_, b.err = b.w.Write(b.pb.data)
|
||||
b.pb.data = b.pb.data[:0]
|
||||
}
|
||||
|
||||
func (b *profileBuilder) pbValueType(tag int, typ string, unit string) {
|
||||
start := b.pb.startMessage()
|
||||
b.pb.int64(tagValueType_Type, b.stringIndex(typ))
|
||||
b.pb.int64(tagValueType_Unit, b.stringIndex(unit))
|
||||
b.pb.endMessage(tag, start)
|
||||
}
|
||||
|
||||
func (b *profileBuilder) pbSample(values []int64, locs []uint64, labels func()) {
|
||||
start := b.pb.startMessage()
|
||||
b.pb.int64s(tagSample_Value, values)
|
||||
b.pb.uint64s(tagSample_Location, locs)
|
||||
if labels != nil {
|
||||
labels()
|
||||
}
|
||||
b.pb.endMessage(tagProfile_Sample, start)
|
||||
b.flush()
|
||||
}
|
||||
|
||||
func (b *profileBuilder) pbLabel(tag int, key string, str string, num int64) {
|
||||
start := b.pb.startMessage()
|
||||
b.pb.int64Opt(tagLabel_Key, b.stringIndex(key))
|
||||
b.pb.int64Opt(tagLabel_Str, b.stringIndex(str))
|
||||
b.pb.int64Opt(tagLabel_Num, num)
|
||||
b.pb.endMessage(tag, start)
|
||||
}
|
||||
|
||||
func (b *profileBuilder) pbLine(tag int, funcID uint64, line int64) {
|
||||
start := b.pb.startMessage()
|
||||
b.pb.uint64Opt(tagLine_FunctionID, funcID)
|
||||
b.pb.int64Opt(tagLine_Line, line)
|
||||
b.pb.endMessage(tag, start)
|
||||
}
|
||||
|
||||
func (b *profileBuilder) pbMapping(tag int, id uint64, base uint64, limit uint64, offset uint64, file string, buildID string, hasFuncs bool) {
|
||||
start := b.pb.startMessage()
|
||||
b.pb.uint64Opt(tagMapping_ID, id)
|
||||
b.pb.uint64Opt(tagMapping_Start, base)
|
||||
b.pb.uint64Opt(tagMapping_Limit, limit)
|
||||
b.pb.uint64Opt(tagMapping_Offset, offset)
|
||||
b.pb.int64Opt(tagMapping_Filename, b.stringIndex(file))
|
||||
b.pb.int64Opt(tagMapping_BuildID, b.stringIndex(buildID))
|
||||
if hasFuncs {
|
||||
b.pb.bool(tagMapping_HasFunctions, true)
|
||||
}
|
||||
b.pb.endMessage(tag, start)
|
||||
}
|
||||
|
||||
func (b *profileBuilder) build() error {
|
||||
if b.err != nil {
|
||||
return b.err
|
||||
}
|
||||
|
||||
b.pb.int64Opt(tagProfile_TimeNanos, b.start.UnixNano())
|
||||
for i, mapping := range b.mem {
|
||||
hasFunctions := mapping.funcs == lookupTried
|
||||
b.pbMapping(tagProfile_Mapping, uint64(i+1), uint64(mapping.start), uint64(mapping.end), mapping.offset, mapping.file, mapping.buildID, hasFunctions)
|
||||
}
|
||||
b.pb.strings(tagProfile_StringTable, b.strings)
|
||||
if b.err != nil {
|
||||
return b.err
|
||||
}
|
||||
_, err := b.w.Write(b.pb.data)
|
||||
return err
|
||||
}
|
||||
|
||||
func allFrames(addr uintptr) ([]runtime.Frame, symbolizeFlag) {
|
||||
frames := runtime.CallersFrames([]uintptr{addr})
|
||||
frame, more := frames.Next()
|
||||
if frame.Function == "runtime.goexit" {
|
||||
return nil, 0
|
||||
}
|
||||
|
||||
result := lookupTried
|
||||
if frame.PC == 0 || frame.Function == "" || frame.File == "" || frame.Line == 0 {
|
||||
result |= lookupFailed
|
||||
}
|
||||
if frame.PC == 0 {
|
||||
frame.PC = addr - 1
|
||||
}
|
||||
|
||||
ret := []runtime.Frame{frame}
|
||||
for frame.Function != "runtime.goexit" && more {
|
||||
frame, more = frames.Next()
|
||||
ret = append(ret, frame)
|
||||
}
|
||||
return ret, result
|
||||
}
|
||||
|
||||
type locInfo struct {
|
||||
id uint64
|
||||
|
||||
pcs []uintptr
|
||||
|
||||
firstPCFrames []runtime.Frame
|
||||
firstPCSymbolizeResult symbolizeFlag
|
||||
}
|
||||
|
||||
func (b *profileBuilder) appendLocsForStack(locs []uint64, stk []uintptr) []uint64 {
|
||||
b.deck.reset()
|
||||
origStk := stk
|
||||
stk = runtimeExpandFinalInlineFrame(stk)
|
||||
|
||||
for len(stk) > 0 {
|
||||
addr := stk[0]
|
||||
if loc, ok := b.locs[addr]; ok {
|
||||
if len(b.deck.pcs) > 0 {
|
||||
if b.deck.tryAdd(addr, loc.firstPCFrames, loc.firstPCSymbolizeResult) {
|
||||
stk = stk[1:]
|
||||
continue
|
||||
}
|
||||
}
|
||||
if id := b.emitLocation(); id > 0 {
|
||||
locs = append(locs, id)
|
||||
}
|
||||
locs = append(locs, loc.id)
|
||||
if len(loc.pcs) > len(stk) {
|
||||
panic(fmt.Sprintf("stack too short to match cached location; stk = %#x, loc.pcs = %#x, original stk = %#x", stk, loc.pcs, origStk))
|
||||
}
|
||||
stk = stk[len(loc.pcs):]
|
||||
continue
|
||||
}
|
||||
|
||||
frames, symbolizeResult := allFrames(addr)
|
||||
if len(frames) == 0 {
|
||||
if id := b.emitLocation(); id > 0 {
|
||||
locs = append(locs, id)
|
||||
}
|
||||
stk = stk[1:]
|
||||
continue
|
||||
}
|
||||
|
||||
if b.deck.tryAdd(addr, frames, symbolizeResult) {
|
||||
stk = stk[1:]
|
||||
continue
|
||||
}
|
||||
if id := b.emitLocation(); id > 0 {
|
||||
locs = append(locs, id)
|
||||
}
|
||||
|
||||
if loc, ok := b.locs[addr]; ok {
|
||||
locs = append(locs, loc.id)
|
||||
stk = stk[len(loc.pcs):]
|
||||
} else {
|
||||
b.deck.tryAdd(addr, frames, symbolizeResult)
|
||||
stk = stk[1:]
|
||||
}
|
||||
}
|
||||
if id := b.emitLocation(); id > 0 {
|
||||
locs = append(locs, id)
|
||||
}
|
||||
return locs
|
||||
}
|
||||
|
||||
type pcDeck struct {
|
||||
pcs []uintptr
|
||||
frames []runtime.Frame
|
||||
symbolizeResult symbolizeFlag
|
||||
|
||||
firstPCFrames int
|
||||
firstPCSymbolizeResult symbolizeFlag
|
||||
}
|
||||
|
||||
func (d *pcDeck) reset() {
|
||||
d.pcs = d.pcs[:0]
|
||||
d.frames = d.frames[:0]
|
||||
d.symbolizeResult = 0
|
||||
d.firstPCFrames = 0
|
||||
d.firstPCSymbolizeResult = 0
|
||||
}
|
||||
|
||||
func (d *pcDeck) tryAdd(pc uintptr, frames []runtime.Frame, symbolizeResult symbolizeFlag) bool {
|
||||
if existing := len(d.frames); existing > 0 {
|
||||
newFrame := frames[0]
|
||||
last := d.frames[existing-1]
|
||||
if last.Func != nil {
|
||||
return false
|
||||
}
|
||||
if last.Entry == 0 || newFrame.Entry == 0 {
|
||||
return false
|
||||
}
|
||||
if last.Entry != newFrame.Entry {
|
||||
return false
|
||||
}
|
||||
if runtimeFrameSymbolName(&last) == runtimeFrameSymbolName(&newFrame) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
d.pcs = append(d.pcs, pc)
|
||||
d.frames = append(d.frames, frames...)
|
||||
d.symbolizeResult |= symbolizeResult
|
||||
if len(d.pcs) == 1 {
|
||||
d.firstPCFrames = len(d.frames)
|
||||
d.firstPCSymbolizeResult = symbolizeResult
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *profileBuilder) emitLocation() uint64 {
|
||||
if len(b.deck.pcs) == 0 {
|
||||
return 0
|
||||
}
|
||||
defer b.deck.reset()
|
||||
|
||||
addr := b.deck.pcs[0]
|
||||
firstFrame := b.deck.frames[0]
|
||||
|
||||
type newFunc struct {
|
||||
id uint64
|
||||
name string
|
||||
file string
|
||||
startLine int64
|
||||
}
|
||||
|
||||
newFuncs := make([]newFunc, 0, 8)
|
||||
id := uint64(len(b.locs)) + 1
|
||||
b.locs[addr] = locInfo{
|
||||
id: id,
|
||||
pcs: append([]uintptr{}, b.deck.pcs...),
|
||||
firstPCFrames: append([]runtime.Frame{}, b.deck.frames[:b.deck.firstPCFrames]...),
|
||||
firstPCSymbolizeResult: b.deck.firstPCSymbolizeResult,
|
||||
}
|
||||
|
||||
start := b.pb.startMessage()
|
||||
b.pb.uint64Opt(tagLocation_ID, id)
|
||||
b.pb.uint64Opt(tagLocation_Address, uint64(firstFrame.PC))
|
||||
for _, frame := range b.deck.frames {
|
||||
funcName := runtimeFrameSymbolName(&frame)
|
||||
funcID := uint64(b.funcs[funcName])
|
||||
if funcID == 0 {
|
||||
funcID = uint64(len(b.funcs)) + 1
|
||||
b.funcs[funcName] = int(funcID)
|
||||
newFuncs = append(newFuncs, newFunc{
|
||||
id: funcID,
|
||||
name: funcName,
|
||||
file: frame.File,
|
||||
startLine: int64(runtimeFrameStartLine(&frame)),
|
||||
})
|
||||
}
|
||||
b.pbLine(tagLocation_Line, funcID, int64(frame.Line))
|
||||
}
|
||||
for i := range b.mem {
|
||||
if (b.mem[i].start <= addr && addr < b.mem[i].end) || b.mem[i].fake {
|
||||
b.pb.uint64Opt(tagLocation_MappingID, uint64(i+1))
|
||||
mapping := b.mem[i]
|
||||
mapping.funcs |= b.deck.symbolizeResult
|
||||
b.mem[i] = mapping
|
||||
break
|
||||
}
|
||||
}
|
||||
b.pb.endMessage(tagProfile_Location, start)
|
||||
|
||||
for _, fn := range newFuncs {
|
||||
start := b.pb.startMessage()
|
||||
b.pb.uint64Opt(tagFunction_ID, fn.id)
|
||||
b.pb.int64Opt(tagFunction_Name, b.stringIndex(fn.name))
|
||||
b.pb.int64Opt(tagFunction_SystemName, b.stringIndex(fn.name))
|
||||
b.pb.int64Opt(tagFunction_Filename, b.stringIndex(fn.file))
|
||||
b.pb.int64Opt(tagFunction_StartLine, fn.startLine)
|
||||
b.pb.endMessage(tagProfile_Function, start)
|
||||
}
|
||||
|
||||
b.flush()
|
||||
return id
|
||||
}
|
||||
|
||||
func (b *profileBuilder) addMapping(lo uint64, hi uint64, offset uint64, file string, buildID string) {
|
||||
b.addMappingEntry(lo, hi, offset, file, buildID, false)
|
||||
}
|
||||
|
||||
func (b *profileBuilder) addMappingEntry(lo uint64, hi uint64, offset uint64, file string, buildID string, fake bool) {
|
||||
b.mem = append(b.mem, memMap{
|
||||
start: uintptr(lo),
|
||||
end: uintptr(hi),
|
||||
offset: offset,
|
||||
file: file,
|
||||
buildID: buildID,
|
||||
fake: fake,
|
||||
})
|
||||
}
|
||||
24
experimental/libbox/internal/oomprofile/defs_darwin_amd64.go
Normal file
24
experimental/libbox/internal/oomprofile/defs_darwin_amd64.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build darwin && amd64
|
||||
|
||||
package oomprofile
|
||||
|
||||
type machVMRegionBasicInfoData struct {
|
||||
Protection int32
|
||||
MaxProtection int32
|
||||
Inheritance uint32
|
||||
Shared uint32
|
||||
Reserved uint32
|
||||
Offset [8]byte
|
||||
Behavior int32
|
||||
UserWiredCount uint16
|
||||
PadCgo1 [2]byte
|
||||
}
|
||||
|
||||
const (
|
||||
_VM_PROT_READ = 0x1
|
||||
_VM_PROT_EXECUTE = 0x4
|
||||
|
||||
_MACH_SEND_INVALID_DEST = 0x10000003
|
||||
|
||||
_MAXPATHLEN = 0x400
|
||||
)
|
||||
24
experimental/libbox/internal/oomprofile/defs_darwin_arm64.go
Normal file
24
experimental/libbox/internal/oomprofile/defs_darwin_arm64.go
Normal file
@@ -0,0 +1,24 @@
|
||||
//go:build darwin && arm64
|
||||
|
||||
package oomprofile
|
||||
|
||||
type machVMRegionBasicInfoData struct {
|
||||
Protection int32
|
||||
MaxProtection int32
|
||||
Inheritance uint32
|
||||
Shared int32
|
||||
Reserved int32
|
||||
Offset [8]byte
|
||||
Behavior int32
|
||||
UserWiredCount uint16
|
||||
PadCgo1 [2]byte
|
||||
}
|
||||
|
||||
const (
|
||||
_VM_PROT_READ = 0x1
|
||||
_VM_PROT_EXECUTE = 0x4
|
||||
|
||||
_MACH_SEND_INVALID_DEST = 0x10000003
|
||||
|
||||
_MAXPATHLEN = 0x400
|
||||
)
|
||||
46
experimental/libbox/internal/oomprofile/linkname.go
Normal file
46
experimental/libbox/internal/oomprofile/linkname.go
Normal file
@@ -0,0 +1,46 @@
|
||||
//go:build darwin || linux || windows
|
||||
|
||||
package oomprofile
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
_ "runtime/pprof"
|
||||
"unsafe"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname runtimeMemProfileInternal runtime.pprof_memProfileInternal
|
||||
func runtimeMemProfileInternal(p []memProfileRecord, inuseZero bool) (n int, ok bool)
|
||||
|
||||
//go:linkname runtimeBlockProfileInternal runtime.pprof_blockProfileInternal
|
||||
func runtimeBlockProfileInternal(p []blockProfileRecord) (n int, ok bool)
|
||||
|
||||
//go:linkname runtimeMutexProfileInternal runtime.pprof_mutexProfileInternal
|
||||
func runtimeMutexProfileInternal(p []blockProfileRecord) (n int, ok bool)
|
||||
|
||||
//go:linkname runtimeThreadCreateInternal runtime.pprof_threadCreateInternal
|
||||
func runtimeThreadCreateInternal(p []stackRecord) (n int, ok bool)
|
||||
|
||||
//go:linkname runtimeGoroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels
|
||||
func runtimeGoroutineProfileWithLabels(p []stackRecord, labels []unsafe.Pointer) (n int, ok bool)
|
||||
|
||||
//go:linkname runtimeCyclesPerSecond runtime/pprof.runtime_cyclesPerSecond
|
||||
func runtimeCyclesPerSecond() int64
|
||||
|
||||
//go:linkname runtimeMakeProfStack runtime.pprof_makeProfStack
|
||||
func runtimeMakeProfStack() []uintptr
|
||||
|
||||
//go:linkname runtimeFrameStartLine runtime/pprof.runtime_FrameStartLine
|
||||
func runtimeFrameStartLine(f *runtime.Frame) int
|
||||
|
||||
//go:linkname runtimeFrameSymbolName runtime/pprof.runtime_FrameSymbolName
|
||||
func runtimeFrameSymbolName(f *runtime.Frame) string
|
||||
|
||||
//go:linkname runtimeExpandFinalInlineFrame runtime/pprof.runtime_expandFinalInlineFrame
|
||||
func runtimeExpandFinalInlineFrame(stk []uintptr) []uintptr
|
||||
|
||||
//go:linkname stdParseProcSelfMaps runtime/pprof.parseProcSelfMaps
|
||||
func stdParseProcSelfMaps(data []byte, addMapping func(lo uint64, hi uint64, offset uint64, file string, buildID string))
|
||||
|
||||
//go:linkname stdELFBuildID runtime/pprof.elfBuildID
|
||||
func stdELFBuildID(file string) (string, error)
|
||||
56
experimental/libbox/internal/oomprofile/mapping_darwin.go
Normal file
56
experimental/libbox/internal/oomprofile/mapping_darwin.go
Normal file
@@ -0,0 +1,56 @@
|
||||
//go:build darwin
|
||||
|
||||
package oomprofile
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"os"
|
||||
"unsafe"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
func isExecutable(protection int32) bool {
|
||||
return (protection&_VM_PROT_EXECUTE) != 0 && (protection&_VM_PROT_READ) != 0
|
||||
}
|
||||
|
||||
func (b *profileBuilder) readMapping() {
|
||||
if !machVMInfo(b.addMapping) {
|
||||
b.addMappingEntry(0, 0, 0, "", "", true)
|
||||
}
|
||||
}
|
||||
|
||||
func machVMInfo(addMapping func(lo uint64, hi uint64, off uint64, file string, buildID string)) bool {
|
||||
added := false
|
||||
addr := uint64(0x1)
|
||||
for {
|
||||
var regionSize uint64
|
||||
var info machVMRegionBasicInfoData
|
||||
kr := machVMRegion(&addr, ®ionSize, unsafe.Pointer(&info))
|
||||
if kr != 0 {
|
||||
if kr == _MACH_SEND_INVALID_DEST {
|
||||
return true
|
||||
}
|
||||
return added
|
||||
}
|
||||
if isExecutable(info.Protection) {
|
||||
addMapping(addr, addr+regionSize, binary.LittleEndian.Uint64(info.Offset[:]), regionFilename(addr), "")
|
||||
added = true
|
||||
}
|
||||
addr += regionSize
|
||||
}
|
||||
}
|
||||
|
||||
func regionFilename(address uint64) string {
|
||||
buf := make([]byte, _MAXPATHLEN)
|
||||
n := procRegionFilename(os.Getpid(), address, unsafe.SliceData(buf), int64(cap(buf)))
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
//go:linkname machVMRegion runtime/pprof.mach_vm_region
|
||||
func machVMRegion(address *uint64, regionSize *uint64, info unsafe.Pointer) int32
|
||||
|
||||
//go:linkname procRegionFilename runtime/pprof.proc_regionfilename
|
||||
func procRegionFilename(pid int, address uint64, buf *byte, buflen int64) int32
|
||||
13
experimental/libbox/internal/oomprofile/mapping_linux.go
Normal file
13
experimental/libbox/internal/oomprofile/mapping_linux.go
Normal file
@@ -0,0 +1,13 @@
|
||||
//go:build linux
|
||||
|
||||
package oomprofile
|
||||
|
||||
import "os"
|
||||
|
||||
func (b *profileBuilder) readMapping() {
|
||||
data, _ := os.ReadFile("/proc/self/maps")
|
||||
stdParseProcSelfMaps(data, b.addMapping)
|
||||
if len(b.mem) == 0 {
|
||||
b.addMappingEntry(0, 0, 0, "", "", true)
|
||||
}
|
||||
}
|
||||
58
experimental/libbox/internal/oomprofile/mapping_windows.go
Normal file
58
experimental/libbox/internal/oomprofile/mapping_windows.go
Normal file
@@ -0,0 +1,58 @@
|
||||
//go:build windows
|
||||
|
||||
package oomprofile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func (b *profileBuilder) readMapping() {
|
||||
snapshot, err := createModuleSnapshot()
|
||||
if err != nil {
|
||||
b.addMappingEntry(0, 0, 0, "", "", true)
|
||||
return
|
||||
}
|
||||
defer windows.CloseHandle(snapshot)
|
||||
|
||||
var module windows.ModuleEntry32
|
||||
module.Size = uint32(windows.SizeofModuleEntry32)
|
||||
err = windows.Module32First(snapshot, &module)
|
||||
if err != nil {
|
||||
b.addMappingEntry(0, 0, 0, "", "", true)
|
||||
return
|
||||
}
|
||||
for err == nil {
|
||||
exe := windows.UTF16ToString(module.ExePath[:])
|
||||
b.addMappingEntry(
|
||||
uint64(module.ModBaseAddr),
|
||||
uint64(module.ModBaseAddr)+uint64(module.ModBaseSize),
|
||||
0,
|
||||
exe,
|
||||
peBuildID(exe),
|
||||
false,
|
||||
)
|
||||
err = windows.Module32Next(snapshot, &module)
|
||||
}
|
||||
}
|
||||
|
||||
func createModuleSnapshot() (windows.Handle, error) {
|
||||
for {
|
||||
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, uint32(windows.GetCurrentProcessId()))
|
||||
var errno windows.Errno
|
||||
if err != nil && errors.As(err, &errno) && errno == windows.ERROR_BAD_LENGTH {
|
||||
continue
|
||||
}
|
||||
return snapshot, err
|
||||
}
|
||||
}
|
||||
|
||||
func peBuildID(file string) string {
|
||||
info, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return file
|
||||
}
|
||||
return file + info.ModTime().String()
|
||||
}
|
||||
383
experimental/libbox/internal/oomprofile/oomprofile.go
Normal file
383
experimental/libbox/internal/oomprofile/oomprofile.go
Normal file
@@ -0,0 +1,383 @@
|
||||
//go:build darwin || linux || windows
|
||||
|
||||
package oomprofile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type stackRecord struct {
|
||||
Stack []uintptr
|
||||
}
|
||||
|
||||
type memProfileRecord struct {
|
||||
AllocBytes, FreeBytes int64
|
||||
AllocObjects, FreeObjects int64
|
||||
Stack []uintptr
|
||||
}
|
||||
|
||||
func (r *memProfileRecord) InUseBytes() int64 {
|
||||
return r.AllocBytes - r.FreeBytes
|
||||
}
|
||||
|
||||
func (r *memProfileRecord) InUseObjects() int64 {
|
||||
return r.AllocObjects - r.FreeObjects
|
||||
}
|
||||
|
||||
type blockProfileRecord struct {
|
||||
Count int64
|
||||
Cycles int64
|
||||
Stack []uintptr
|
||||
}
|
||||
|
||||
type label struct {
|
||||
key string
|
||||
value string
|
||||
}
|
||||
|
||||
type labelSet struct {
|
||||
list []label
|
||||
}
|
||||
|
||||
type labelMap struct {
|
||||
labelSet
|
||||
}
|
||||
|
||||
func WriteFile(destPath string, name string) (string, error) {
|
||||
writer, ok := profileWriters[name]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unsupported profile %q", name)
|
||||
}
|
||||
|
||||
filePath := filepath.Join(destPath, name+".pb")
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
if err := writer(file); err != nil {
|
||||
_ = os.Remove(filePath)
|
||||
return "", err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
_ = os.Remove(filePath)
|
||||
return "", err
|
||||
}
|
||||
return filePath, nil
|
||||
}
|
||||
|
||||
var profileWriters = map[string]func(io.Writer) error{
|
||||
"allocs": writeAlloc,
|
||||
"block": writeBlock,
|
||||
"goroutine": writeGoroutine,
|
||||
"heap": writeHeap,
|
||||
"mutex": writeMutex,
|
||||
"threadcreate": writeThreadCreate,
|
||||
}
|
||||
|
||||
func writeHeap(w io.Writer) error {
|
||||
return writeHeapInternal(w, "")
|
||||
}
|
||||
|
||||
func writeAlloc(w io.Writer) error {
|
||||
return writeHeapInternal(w, "alloc_space")
|
||||
}
|
||||
|
||||
func writeHeapInternal(w io.Writer, defaultSampleType string) error {
|
||||
var profile []memProfileRecord
|
||||
n, _ := runtimeMemProfileInternal(nil, true)
|
||||
var ok bool
|
||||
for {
|
||||
profile = make([]memProfileRecord, n+50)
|
||||
n, ok = runtimeMemProfileInternal(profile, true)
|
||||
if ok {
|
||||
profile = profile[:n]
|
||||
break
|
||||
}
|
||||
}
|
||||
return writeHeapProto(w, profile, int64(runtime.MemProfileRate), defaultSampleType)
|
||||
}
|
||||
|
||||
func writeGoroutine(w io.Writer) error {
|
||||
return writeRuntimeProfile(w, "goroutine", runtimeGoroutineProfileWithLabels)
|
||||
}
|
||||
|
||||
func writeThreadCreate(w io.Writer) error {
|
||||
return writeRuntimeProfile(w, "threadcreate", func(p []stackRecord, _ []unsafe.Pointer) (int, bool) {
|
||||
return runtimeThreadCreateInternal(p)
|
||||
})
|
||||
}
|
||||
|
||||
func writeRuntimeProfile(w io.Writer, name string, fetch func([]stackRecord, []unsafe.Pointer) (int, bool)) error {
|
||||
var profile []stackRecord
|
||||
var labels []unsafe.Pointer
|
||||
|
||||
n, _ := fetch(nil, nil)
|
||||
var ok bool
|
||||
for {
|
||||
profile = make([]stackRecord, n+10)
|
||||
labels = make([]unsafe.Pointer, n+10)
|
||||
n, ok = fetch(profile, labels)
|
||||
if ok {
|
||||
profile = profile[:n]
|
||||
labels = labels[:n]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return writeCountProfile(w, name, &runtimeProfile{profile, labels})
|
||||
}
|
||||
|
||||
func writeBlock(w io.Writer) error {
|
||||
return writeCycleProfile(w, "contentions", "delay", runtimeBlockProfileInternal)
|
||||
}
|
||||
|
||||
func writeMutex(w io.Writer) error {
|
||||
return writeCycleProfile(w, "contentions", "delay", runtimeMutexProfileInternal)
|
||||
}
|
||||
|
||||
func writeCycleProfile(w io.Writer, countName string, cycleName string, fetch func([]blockProfileRecord) (int, bool)) error {
|
||||
var profile []blockProfileRecord
|
||||
n, _ := fetch(nil)
|
||||
var ok bool
|
||||
for {
|
||||
profile = make([]blockProfileRecord, n+50)
|
||||
n, ok = fetch(profile)
|
||||
if ok {
|
||||
profile = profile[:n]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(profile, func(i, j int) bool {
|
||||
return profile[i].Cycles > profile[j].Cycles
|
||||
})
|
||||
|
||||
builder := newProfileBuilder(w)
|
||||
builder.pbValueType(tagProfile_PeriodType, countName, "count")
|
||||
builder.pb.int64Opt(tagProfile_Period, 1)
|
||||
builder.pbValueType(tagProfile_SampleType, countName, "count")
|
||||
builder.pbValueType(tagProfile_SampleType, cycleName, "nanoseconds")
|
||||
|
||||
cpuGHz := float64(runtimeCyclesPerSecond()) / 1e9
|
||||
values := []int64{0, 0}
|
||||
var locs []uint64
|
||||
expandedStack := runtimeMakeProfStack()
|
||||
for _, record := range profile {
|
||||
values[0] = record.Count
|
||||
if cpuGHz > 0 {
|
||||
values[1] = int64(float64(record.Cycles) / cpuGHz)
|
||||
} else {
|
||||
values[1] = 0
|
||||
}
|
||||
n := expandInlinedFrames(expandedStack, record.Stack)
|
||||
locs = builder.appendLocsForStack(locs[:0], expandedStack[:n])
|
||||
builder.pbSample(values, locs, nil)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
type countProfile interface {
|
||||
Len() int
|
||||
Stack(i int) []uintptr
|
||||
Label(i int) *labelMap
|
||||
}
|
||||
|
||||
type runtimeProfile struct {
|
||||
stk []stackRecord
|
||||
labels []unsafe.Pointer
|
||||
}
|
||||
|
||||
func (p *runtimeProfile) Len() int {
|
||||
return len(p.stk)
|
||||
}
|
||||
|
||||
func (p *runtimeProfile) Stack(i int) []uintptr {
|
||||
return p.stk[i].Stack
|
||||
}
|
||||
|
||||
func (p *runtimeProfile) Label(i int) *labelMap {
|
||||
return (*labelMap)(p.labels[i])
|
||||
}
|
||||
|
||||
func writeCountProfile(w io.Writer, name string, profile countProfile) error {
|
||||
var buf strings.Builder
|
||||
key := func(stk []uintptr, labels *labelMap) string {
|
||||
buf.Reset()
|
||||
buf.WriteByte('@')
|
||||
for _, pc := range stk {
|
||||
fmt.Fprintf(&buf, " %#x", pc)
|
||||
}
|
||||
if labels != nil {
|
||||
buf.WriteString("\n# labels:")
|
||||
for _, label := range labels.list {
|
||||
fmt.Fprintf(&buf, " %q:%q", label.key, label.value)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
counts := make(map[string]int)
|
||||
index := make(map[string]int)
|
||||
var keys []string
|
||||
for i := 0; i < profile.Len(); i++ {
|
||||
k := key(profile.Stack(i), profile.Label(i))
|
||||
if counts[k] == 0 {
|
||||
index[k] = i
|
||||
keys = append(keys, k)
|
||||
}
|
||||
counts[k]++
|
||||
}
|
||||
|
||||
sort.Sort(&keysByCount{keys: keys, count: counts})
|
||||
|
||||
builder := newProfileBuilder(w)
|
||||
builder.pbValueType(tagProfile_PeriodType, name, "count")
|
||||
builder.pb.int64Opt(tagProfile_Period, 1)
|
||||
builder.pbValueType(tagProfile_SampleType, name, "count")
|
||||
|
||||
values := []int64{0}
|
||||
var locs []uint64
|
||||
for _, k := range keys {
|
||||
values[0] = int64(counts[k])
|
||||
idx := index[k]
|
||||
locs = builder.appendLocsForStack(locs[:0], profile.Stack(idx))
|
||||
|
||||
var labels func()
|
||||
if profile.Label(idx) != nil {
|
||||
labels = func() {
|
||||
for _, label := range profile.Label(idx).list {
|
||||
builder.pbLabel(tagSample_Label, label.key, label.value, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.pbSample(values, locs, labels)
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
type keysByCount struct {
|
||||
keys []string
|
||||
count map[string]int
|
||||
}
|
||||
|
||||
func (x *keysByCount) Len() int {
|
||||
return len(x.keys)
|
||||
}
|
||||
|
||||
func (x *keysByCount) Swap(i int, j int) {
|
||||
x.keys[i], x.keys[j] = x.keys[j], x.keys[i]
|
||||
}
|
||||
|
||||
func (x *keysByCount) Less(i int, j int) bool {
|
||||
ki, kj := x.keys[i], x.keys[j]
|
||||
ci, cj := x.count[ki], x.count[kj]
|
||||
if ci != cj {
|
||||
return ci > cj
|
||||
}
|
||||
return ki < kj
|
||||
}
|
||||
|
||||
func expandInlinedFrames(dst []uintptr, pcs []uintptr) int {
|
||||
frames := runtime.CallersFrames(pcs)
|
||||
var n int
|
||||
for n < len(dst) {
|
||||
frame, more := frames.Next()
|
||||
dst[n] = frame.PC + 1
|
||||
n++
|
||||
if !more {
|
||||
break
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func writeHeapProto(w io.Writer, profile []memProfileRecord, rate int64, defaultSampleType string) error {
|
||||
builder := newProfileBuilder(w)
|
||||
builder.pbValueType(tagProfile_PeriodType, "space", "bytes")
|
||||
builder.pb.int64Opt(tagProfile_Period, rate)
|
||||
builder.pbValueType(tagProfile_SampleType, "alloc_objects", "count")
|
||||
builder.pbValueType(tagProfile_SampleType, "alloc_space", "bytes")
|
||||
builder.pbValueType(tagProfile_SampleType, "inuse_objects", "count")
|
||||
builder.pbValueType(tagProfile_SampleType, "inuse_space", "bytes")
|
||||
if defaultSampleType != "" {
|
||||
builder.pb.int64Opt(tagProfile_DefaultSampleType, builder.stringIndex(defaultSampleType))
|
||||
}
|
||||
|
||||
values := []int64{0, 0, 0, 0}
|
||||
var locs []uint64
|
||||
for _, record := range profile {
|
||||
hideRuntime := true
|
||||
for tries := 0; tries < 2; tries++ {
|
||||
stk := record.Stack
|
||||
if hideRuntime {
|
||||
for i, addr := range stk {
|
||||
if f := runtime.FuncForPC(addr); f != nil && (strings.HasPrefix(f.Name(), "runtime.") || strings.HasPrefix(f.Name(), "internal/runtime/")) {
|
||||
continue
|
||||
}
|
||||
stk = stk[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
locs = builder.appendLocsForStack(locs[:0], stk)
|
||||
if len(locs) > 0 {
|
||||
break
|
||||
}
|
||||
hideRuntime = false
|
||||
}
|
||||
|
||||
values[0], values[1] = scaleHeapSample(record.AllocObjects, record.AllocBytes, rate)
|
||||
values[2], values[3] = scaleHeapSample(record.InUseObjects(), record.InUseBytes(), rate)
|
||||
|
||||
var blockSize int64
|
||||
if record.AllocObjects > 0 {
|
||||
blockSize = record.AllocBytes / record.AllocObjects
|
||||
}
|
||||
builder.pbSample(values, locs, func() {
|
||||
if blockSize != 0 {
|
||||
builder.pbLabel(tagSample_Label, "bytes", "", blockSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
func scaleHeapSample(count int64, size int64, rate int64) (int64, int64) {
|
||||
if count == 0 || size == 0 {
|
||||
return 0, 0
|
||||
}
|
||||
if rate <= 1 {
|
||||
return count, size
|
||||
}
|
||||
|
||||
avgSize := float64(size) / float64(count)
|
||||
scale := 1 / (1 - math.Exp(-avgSize/float64(rate)))
|
||||
return int64(float64(count) * scale), int64(float64(size) * scale)
|
||||
}
|
||||
|
||||
type profileBuilder struct {
|
||||
start time.Time
|
||||
w io.Writer
|
||||
err error
|
||||
|
||||
pb protobuf
|
||||
strings []string
|
||||
stringMap map[string]int
|
||||
locs map[uintptr]locInfo
|
||||
funcs map[string]int
|
||||
mem []memMap
|
||||
deck pcDeck
|
||||
}
|
||||
120
experimental/libbox/internal/oomprofile/protobuf.go
Normal file
120
experimental/libbox/internal/oomprofile/protobuf.go
Normal file
@@ -0,0 +1,120 @@
|
||||
//go:build darwin || linux || windows
|
||||
|
||||
package oomprofile
|
||||
|
||||
type protobuf struct {
|
||||
data []byte
|
||||
tmp [16]byte
|
||||
nest int
|
||||
}
|
||||
|
||||
func (b *protobuf) varint(x uint64) {
|
||||
for x >= 128 {
|
||||
b.data = append(b.data, byte(x)|0x80)
|
||||
x >>= 7
|
||||
}
|
||||
b.data = append(b.data, byte(x))
|
||||
}
|
||||
|
||||
func (b *protobuf) length(tag int, length int) {
|
||||
b.varint(uint64(tag)<<3 | 2)
|
||||
b.varint(uint64(length))
|
||||
}
|
||||
|
||||
func (b *protobuf) uint64(tag int, x uint64) {
|
||||
b.varint(uint64(tag) << 3)
|
||||
b.varint(x)
|
||||
}
|
||||
|
||||
func (b *protobuf) uint64s(tag int, x []uint64) {
|
||||
if len(x) > 2 {
|
||||
n1 := len(b.data)
|
||||
for _, u := range x {
|
||||
b.varint(u)
|
||||
}
|
||||
n2 := len(b.data)
|
||||
b.length(tag, n2-n1)
|
||||
n3 := len(b.data)
|
||||
copy(b.tmp[:], b.data[n2:n3])
|
||||
copy(b.data[n1+(n3-n2):], b.data[n1:n2])
|
||||
copy(b.data[n1:], b.tmp[:n3-n2])
|
||||
return
|
||||
}
|
||||
for _, u := range x {
|
||||
b.uint64(tag, u)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *protobuf) uint64Opt(tag int, x uint64) {
|
||||
if x == 0 {
|
||||
return
|
||||
}
|
||||
b.uint64(tag, x)
|
||||
}
|
||||
|
||||
func (b *protobuf) int64(tag int, x int64) {
|
||||
b.uint64(tag, uint64(x))
|
||||
}
|
||||
|
||||
func (b *protobuf) int64Opt(tag int, x int64) {
|
||||
if x == 0 {
|
||||
return
|
||||
}
|
||||
b.int64(tag, x)
|
||||
}
|
||||
|
||||
func (b *protobuf) int64s(tag int, x []int64) {
|
||||
if len(x) > 2 {
|
||||
n1 := len(b.data)
|
||||
for _, u := range x {
|
||||
b.varint(uint64(u))
|
||||
}
|
||||
n2 := len(b.data)
|
||||
b.length(tag, n2-n1)
|
||||
n3 := len(b.data)
|
||||
copy(b.tmp[:], b.data[n2:n3])
|
||||
copy(b.data[n1+(n3-n2):], b.data[n1:n2])
|
||||
copy(b.data[n1:], b.tmp[:n3-n2])
|
||||
return
|
||||
}
|
||||
for _, u := range x {
|
||||
b.int64(tag, u)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *protobuf) bool(tag int, x bool) {
|
||||
if x {
|
||||
b.uint64(tag, 1)
|
||||
} else {
|
||||
b.uint64(tag, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *protobuf) string(tag int, x string) {
|
||||
b.length(tag, len(x))
|
||||
b.data = append(b.data, x...)
|
||||
}
|
||||
|
||||
func (b *protobuf) strings(tag int, x []string) {
|
||||
for _, s := range x {
|
||||
b.string(tag, s)
|
||||
}
|
||||
}
|
||||
|
||||
type msgOffset int
|
||||
|
||||
func (b *protobuf) startMessage() msgOffset {
|
||||
b.nest++
|
||||
return msgOffset(len(b.data))
|
||||
}
|
||||
|
||||
func (b *protobuf) endMessage(tag int, start msgOffset) {
|
||||
n1 := int(start)
|
||||
n2 := len(b.data)
|
||||
b.length(tag, n2-n1)
|
||||
n3 := len(b.data)
|
||||
copy(b.tmp[:], b.data[n2:n3])
|
||||
copy(b.data[n1+(n3-n2):], b.data[n1:n2])
|
||||
copy(b.data[n1:], b.tmp[:n3-n2])
|
||||
b.nest--
|
||||
}
|
||||
148
experimental/libbox/internal/procfs/procfs.go
Normal file
148
experimental/libbox/internal/procfs/procfs.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
63
experimental/libbox/iterator.go
Normal file
63
experimental/libbox/iterator.go
Normal file
@@ -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
|
||||
}
|
||||
11
experimental/libbox/link_flags_stub.go
Normal file
11
experimental/libbox/link_flags_stub.go
Normal file
@@ -0,0 +1,11 @@
|
||||
//go:build !unix
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
func linkFlags(rawFlags uint32) net.Flags {
|
||||
panic("stub!")
|
||||
}
|
||||
32
experimental/libbox/link_flags_unix.go
Normal file
32
experimental/libbox/link_flags_unix.go
Normal file
@@ -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
|
||||
}
|
||||
165
experimental/libbox/log.go
Normal file
165
experimental/libbox/log.go
Normal file
@@ -0,0 +1,165 @@
|
||||
//go:build darwin || linux || windows
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"time"
|
||||
)
|
||||
|
||||
type crashReportMetadata struct {
|
||||
reportMetadata
|
||||
CrashedAt string `json:"crashedAt,omitempty"`
|
||||
SignalName string `json:"signalName,omitempty"`
|
||||
SignalCode string `json:"signalCode,omitempty"`
|
||||
ExceptionName string `json:"exceptionName,omitempty"`
|
||||
ExceptionReason string `json:"exceptionReason,omitempty"`
|
||||
}
|
||||
|
||||
func archiveCrashReport(path string, crashReportsDir string) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil || len(content) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
info, _ := os.Stat(path)
|
||||
crashTime := time.Now().UTC()
|
||||
if info != nil {
|
||||
crashTime = info.ModTime().UTC()
|
||||
}
|
||||
|
||||
initReportDir(crashReportsDir)
|
||||
destPath, err := nextAvailableReportPath(crashReportsDir, crashTime)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
initReportDir(destPath)
|
||||
|
||||
writeReportFile(destPath, "go.log", content)
|
||||
metadata := crashReportMetadata{
|
||||
reportMetadata: baseReportMetadata(),
|
||||
CrashedAt: crashTime.Format(time.RFC3339),
|
||||
}
|
||||
writeReportMetadata(destPath, metadata)
|
||||
os.Remove(path)
|
||||
copyConfigSnapshot(destPath)
|
||||
}
|
||||
|
||||
func configSnapshotPath() string {
|
||||
return filepath.Join(sBasePath, "configuration.json")
|
||||
}
|
||||
|
||||
func saveConfigSnapshot(configContent string) {
|
||||
snapshotPath := configSnapshotPath()
|
||||
os.WriteFile(snapshotPath, []byte(configContent), 0o666)
|
||||
chownReport(snapshotPath)
|
||||
}
|
||||
|
||||
func redirectStderr(path string) error {
|
||||
crashReportsDir := filepath.Join(sWorkingPath, "crash_reports")
|
||||
archiveCrashReport(path, crashReportsDir)
|
||||
archiveCrashReport(path+".old", crashReportsDir)
|
||||
|
||||
outputFile, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if runtime.GOOS != "android" && runtime.GOOS != "windows" {
|
||||
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
|
||||
}
|
||||
_ = outputFile.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func CreateZipArchive(sourcePath string, destinationPath string) error {
|
||||
sourceInfo, err := os.Stat(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !sourceInfo.IsDir() {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
destinationFile, err := os.Create(destinationPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = destinationFile.Close()
|
||||
}()
|
||||
|
||||
zipWriter := zip.NewWriter(destinationFile)
|
||||
|
||||
rootName := filepath.Base(sourcePath)
|
||||
err = filepath.WalkDir(sourcePath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relativePath, err := filepath.Rel(sourcePath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if relativePath == "." {
|
||||
return nil
|
||||
}
|
||||
|
||||
archivePath := filepath.ToSlash(filepath.Join(rootName, relativePath))
|
||||
if d.IsDir() {
|
||||
_, err = zipWriter.Create(archivePath + "/")
|
||||
return err
|
||||
}
|
||||
|
||||
fileInfo, err := d.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header, err := zip.FileInfoHeader(fileInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = archivePath
|
||||
header.Method = zip.Deflate
|
||||
|
||||
writer, err := zipWriter.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sourceFile, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(writer, sourceFile)
|
||||
closeErr := sourceFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return closeErr
|
||||
})
|
||||
if err != nil {
|
||||
_ = zipWriter.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return zipWriter.Close()
|
||||
}
|
||||
117
experimental/libbox/monitor.go
Normal file
117
experimental/libbox/monitor.go
Normal file
@@ -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
|
||||
}
|
||||
53
experimental/libbox/neighbor.go
Normal file
53
experimental/libbox/neighbor.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type NeighborEntry struct {
|
||||
Address string
|
||||
MacAddress string
|
||||
Hostname string
|
||||
}
|
||||
|
||||
type NeighborEntryIterator interface {
|
||||
Next() *NeighborEntry
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type NeighborSubscription struct {
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func (s *NeighborSubscription) Close() {
|
||||
close(s.done)
|
||||
}
|
||||
|
||||
func tableToIterator(table map[netip.Addr]net.HardwareAddr) NeighborEntryIterator {
|
||||
entries := make([]*NeighborEntry, 0, len(table))
|
||||
for address, mac := range table {
|
||||
entries = append(entries, &NeighborEntry{
|
||||
Address: address.String(),
|
||||
MacAddress: mac.String(),
|
||||
})
|
||||
}
|
||||
return &neighborEntryIterator{entries}
|
||||
}
|
||||
|
||||
type neighborEntryIterator struct {
|
||||
entries []*NeighborEntry
|
||||
}
|
||||
|
||||
func (i *neighborEntryIterator) HasNext() bool {
|
||||
return len(i.entries) > 0
|
||||
}
|
||||
|
||||
func (i *neighborEntryIterator) Next() *NeighborEntry {
|
||||
if len(i.entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
entry := i.entries[0]
|
||||
i.entries = i.entries[1:]
|
||||
return entry
|
||||
}
|
||||
123
experimental/libbox/neighbor_darwin.go
Normal file
123
experimental/libbox/neighbor_darwin.go
Normal file
@@ -0,0 +1,123 @@
|
||||
//go:build darwin
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/route"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
xroute "golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) {
|
||||
entries, err := route.ReadNeighborEntries()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initial neighbor dump")
|
||||
}
|
||||
table := make(map[netip.Addr]net.HardwareAddr)
|
||||
for _, entry := range entries {
|
||||
table[entry.Address] = entry.MACAddress
|
||||
}
|
||||
listener.UpdateNeighborTable(tableToIterator(table))
|
||||
routeSocket, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "open route socket")
|
||||
}
|
||||
err = unix.SetNonblock(routeSocket, true)
|
||||
if err != nil {
|
||||
unix.Close(routeSocket)
|
||||
return nil, E.Cause(err, "set route socket nonblock")
|
||||
}
|
||||
subscription := &NeighborSubscription{
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go subscription.loop(listener, routeSocket, table)
|
||||
return subscription, nil
|
||||
}
|
||||
|
||||
func (s *NeighborSubscription) loop(listener NeighborUpdateListener, routeSocket int, table map[netip.Addr]net.HardwareAddr) {
|
||||
routeSocketFile := os.NewFile(uintptr(routeSocket), "route")
|
||||
defer routeSocketFile.Close()
|
||||
buffer := buf.NewPacket()
|
||||
defer buffer.Release()
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
tv := unix.NsecToTimeval(int64(3 * time.Second))
|
||||
_ = unix.SetsockoptTimeval(routeSocket, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv)
|
||||
n, err := routeSocketFile.Read(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
continue
|
||||
}
|
||||
messages, err := xroute.ParseRIB(xroute.RIBTypeRoute, buffer.FreeBytes()[:n])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
changed := false
|
||||
for _, message := range messages {
|
||||
routeMessage, isRouteMessage := message.(*xroute.RouteMessage)
|
||||
if !isRouteMessage {
|
||||
continue
|
||||
}
|
||||
if routeMessage.Flags&unix.RTF_LLINFO == 0 {
|
||||
continue
|
||||
}
|
||||
address, mac, isDelete, ok := route.ParseRouteNeighborMessage(routeMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if isDelete {
|
||||
if _, exists := table[address]; exists {
|
||||
delete(table, address)
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
existing, exists := table[address]
|
||||
if !exists || !slices.Equal(existing, mac) {
|
||||
table[address] = mac
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
listener.UpdateNeighborTable(tableToIterator(table))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ReadBootpdLeases() NeighborEntryIterator {
|
||||
leaseIPToMAC, ipToHostname, macToHostname := route.ReloadLeaseFiles([]string{"/var/db/dhcpd_leases"})
|
||||
entries := make([]*NeighborEntry, 0, len(leaseIPToMAC))
|
||||
for address, mac := range leaseIPToMAC {
|
||||
entry := &NeighborEntry{
|
||||
Address: address.String(),
|
||||
MacAddress: mac.String(),
|
||||
}
|
||||
hostname, found := ipToHostname[address]
|
||||
if !found {
|
||||
hostname = macToHostname[mac.String()]
|
||||
}
|
||||
entry.Hostname = hostname
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return &neighborEntryIterator{entries}
|
||||
}
|
||||
88
experimental/libbox/neighbor_linux.go
Normal file
88
experimental/libbox/neighbor_linux.go
Normal file
@@ -0,0 +1,88 @@
|
||||
//go:build linux
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/route"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
"github.com/mdlayher/netlink"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func SubscribeNeighborTable(listener NeighborUpdateListener) (*NeighborSubscription, error) {
|
||||
entries, err := route.ReadNeighborEntries()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initial neighbor dump")
|
||||
}
|
||||
table := make(map[netip.Addr]net.HardwareAddr)
|
||||
for _, entry := range entries {
|
||||
table[entry.Address] = entry.MACAddress
|
||||
}
|
||||
listener.UpdateNeighborTable(tableToIterator(table))
|
||||
connection, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
|
||||
Groups: 1 << (unix.RTNLGRP_NEIGH - 1),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "subscribe neighbor updates")
|
||||
}
|
||||
subscription := &NeighborSubscription{
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go subscription.loop(listener, connection, table)
|
||||
return subscription, nil
|
||||
}
|
||||
|
||||
func (s *NeighborSubscription) loop(listener NeighborUpdateListener, connection *netlink.Conn, table map[netip.Addr]net.HardwareAddr) {
|
||||
defer connection.Close()
|
||||
for {
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
err := connection.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
messages, err := connection.Receive()
|
||||
if err != nil {
|
||||
if nerr, ok := err.(net.Error); ok && nerr.Timeout() {
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case <-s.done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
continue
|
||||
}
|
||||
changed := false
|
||||
for _, message := range messages {
|
||||
address, mac, isDelete, ok := route.ParseNeighborMessage(message)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if isDelete {
|
||||
if _, exists := table[address]; exists {
|
||||
delete(table, address)
|
||||
changed = true
|
||||
}
|
||||
} else {
|
||||
existing, exists := table[address]
|
||||
if !exists || !slices.Equal(existing, mac) {
|
||||
table[address] = mac
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
listener.UpdateNeighborTable(tableToIterator(table))
|
||||
}
|
||||
}
|
||||
}
|
||||
9
experimental/libbox/neighbor_stub.go
Normal file
9
experimental/libbox/neighbor_stub.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !linux && !darwin
|
||||
|
||||
package libbox
|
||||
|
||||
import "os"
|
||||
|
||||
func SubscribeNeighborTable(_ NeighborUpdateListener) (*NeighborSubscription, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
74
experimental/libbox/networkquality.go
Normal file
74
experimental/libbox/networkquality.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/networkquality"
|
||||
)
|
||||
|
||||
type NetworkQualityTest struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewNetworkQualityTest() *NetworkQualityTest {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &NetworkQualityTest{ctx: ctx, cancel: cancel}
|
||||
}
|
||||
|
||||
func (t *NetworkQualityTest) Start(configURL string, serial bool, maxRuntimeSeconds int32, http3 bool, handler NetworkQualityTestHandler) {
|
||||
go func() {
|
||||
httpClient := networkquality.NewHTTPClient(nil)
|
||||
defer httpClient.CloseIdleConnections()
|
||||
|
||||
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(nil, http3)
|
||||
if err != nil {
|
||||
handler.OnError(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
result, err := networkquality.Run(networkquality.Options{
|
||||
ConfigURL: configURL,
|
||||
HTTPClient: httpClient,
|
||||
NewMeasurementClient: measurementClientFactory,
|
||||
Serial: serial,
|
||||
MaxRuntime: time.Duration(maxRuntimeSeconds) * time.Second,
|
||||
Context: t.ctx,
|
||||
OnProgress: func(p networkquality.Progress) {
|
||||
handler.OnProgress(&NetworkQualityProgress{
|
||||
Phase: int32(p.Phase),
|
||||
DownloadCapacity: p.DownloadCapacity,
|
||||
UploadCapacity: p.UploadCapacity,
|
||||
DownloadRPM: p.DownloadRPM,
|
||||
UploadRPM: p.UploadRPM,
|
||||
IdleLatencyMs: p.IdleLatencyMs,
|
||||
ElapsedMs: p.ElapsedMs,
|
||||
DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy),
|
||||
UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy),
|
||||
DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy),
|
||||
UploadRPMAccuracy: int32(p.UploadRPMAccuracy),
|
||||
})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
handler.OnError(err.Error())
|
||||
return
|
||||
}
|
||||
handler.OnResult(&NetworkQualityResult{
|
||||
DownloadCapacity: result.DownloadCapacity,
|
||||
UploadCapacity: result.UploadCapacity,
|
||||
DownloadRPM: result.DownloadRPM,
|
||||
UploadRPM: result.UploadRPM,
|
||||
IdleLatencyMs: result.IdleLatencyMs,
|
||||
DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy),
|
||||
UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy),
|
||||
DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy),
|
||||
UploadRPMAccuracy: int32(result.UploadRPMAccuracy),
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *NetworkQualityTest) Cancel() {
|
||||
t.cancel()
|
||||
}
|
||||
141
experimental/libbox/oom_report.go
Normal file
141
experimental/libbox/oom_report.go
Normal file
@@ -0,0 +1,141 @@
|
||||
//go:build darwin || linux || windows
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/experimental/libbox/internal/oomprofile"
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
)
|
||||
|
||||
func init() {
|
||||
sOOMReporter = &oomReporter{}
|
||||
}
|
||||
|
||||
var oomReportProfiles = []string{
|
||||
"allocs",
|
||||
"block",
|
||||
"goroutine",
|
||||
"heap",
|
||||
"mutex",
|
||||
"threadcreate",
|
||||
}
|
||||
|
||||
type oomReportMetadata struct {
|
||||
reportMetadata
|
||||
RecordedAt string `json:"recordedAt"`
|
||||
MemoryUsage string `json:"memoryUsage"`
|
||||
AvailableMemory string `json:"availableMemory,omitempty"`
|
||||
// Heap
|
||||
HeapAlloc string `json:"heapAlloc,omitempty"`
|
||||
HeapObjects uint64 `json:"heapObjects,omitempty,string"`
|
||||
HeapInuse string `json:"heapInuse,omitempty"`
|
||||
HeapIdle string `json:"heapIdle,omitempty"`
|
||||
HeapReleased string `json:"heapReleased,omitempty"`
|
||||
HeapSys string `json:"heapSys,omitempty"`
|
||||
// Stack
|
||||
StackInuse string `json:"stackInuse,omitempty"`
|
||||
StackSys string `json:"stackSys,omitempty"`
|
||||
// Runtime metadata
|
||||
MSpanInuse string `json:"mSpanInuse,omitempty"`
|
||||
MSpanSys string `json:"mSpanSys,omitempty"`
|
||||
MCacheSys string `json:"mCacheSys,omitempty"`
|
||||
BuckHashSys string `json:"buckHashSys,omitempty"`
|
||||
GCSys string `json:"gcSys,omitempty"`
|
||||
OtherSys string `json:"otherSys,omitempty"`
|
||||
Sys string `json:"sys,omitempty"`
|
||||
// GC & runtime
|
||||
TotalAlloc string `json:"totalAlloc,omitempty"`
|
||||
NumGC uint32 `json:"numGC,omitempty,string"`
|
||||
NumGoroutine int `json:"numGoroutine,omitempty,string"`
|
||||
NextGC string `json:"nextGC,omitempty"`
|
||||
LastGC string `json:"lastGC,omitempty"`
|
||||
}
|
||||
|
||||
type oomReporter struct{}
|
||||
|
||||
var _ oomkiller.OOMReporter = (*oomReporter)(nil)
|
||||
|
||||
func (r *oomReporter) WriteReport(memoryUsage uint64) error {
|
||||
now := time.Now().UTC()
|
||||
reportsDir := filepath.Join(sWorkingPath, "oom_reports")
|
||||
err := os.MkdirAll(reportsDir, 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chownReport(reportsDir)
|
||||
|
||||
destPath, err := nextAvailableReportPath(reportsDir, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(destPath, 0o777)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chownReport(destPath)
|
||||
|
||||
for _, name := range oomReportProfiles {
|
||||
writeOOMProfile(destPath, name)
|
||||
}
|
||||
|
||||
writeReportFile(destPath, "cmdline", []byte(strings.Join(os.Args, "\000")))
|
||||
|
||||
var memStats runtime.MemStats
|
||||
runtime.ReadMemStats(&memStats)
|
||||
|
||||
metadata := oomReportMetadata{
|
||||
reportMetadata: baseReportMetadata(),
|
||||
RecordedAt: now.Format(time.RFC3339),
|
||||
MemoryUsage: byteformats.FormatMemoryBytes(memoryUsage),
|
||||
// Heap
|
||||
HeapAlloc: byteformats.FormatMemoryBytes(memStats.HeapAlloc),
|
||||
HeapObjects: memStats.HeapObjects,
|
||||
HeapInuse: byteformats.FormatMemoryBytes(memStats.HeapInuse),
|
||||
HeapIdle: byteformats.FormatMemoryBytes(memStats.HeapIdle),
|
||||
HeapReleased: byteformats.FormatMemoryBytes(memStats.HeapReleased),
|
||||
HeapSys: byteformats.FormatMemoryBytes(memStats.HeapSys),
|
||||
// Stack
|
||||
StackInuse: byteformats.FormatMemoryBytes(memStats.StackInuse),
|
||||
StackSys: byteformats.FormatMemoryBytes(memStats.StackSys),
|
||||
// Runtime metadata
|
||||
MSpanInuse: byteformats.FormatMemoryBytes(memStats.MSpanInuse),
|
||||
MSpanSys: byteformats.FormatMemoryBytes(memStats.MSpanSys),
|
||||
MCacheSys: byteformats.FormatMemoryBytes(memStats.MCacheSys),
|
||||
BuckHashSys: byteformats.FormatMemoryBytes(memStats.BuckHashSys),
|
||||
GCSys: byteformats.FormatMemoryBytes(memStats.GCSys),
|
||||
OtherSys: byteformats.FormatMemoryBytes(memStats.OtherSys),
|
||||
Sys: byteformats.FormatMemoryBytes(memStats.Sys),
|
||||
// GC & runtime
|
||||
TotalAlloc: byteformats.FormatMemoryBytes(memStats.TotalAlloc),
|
||||
NumGC: memStats.NumGC,
|
||||
NumGoroutine: runtime.NumGoroutine(),
|
||||
NextGC: byteformats.FormatMemoryBytes(memStats.NextGC),
|
||||
}
|
||||
if memStats.LastGC > 0 {
|
||||
metadata.LastGC = time.Unix(0, int64(memStats.LastGC)).UTC().Format(time.RFC3339)
|
||||
}
|
||||
availableMemory := memory.Available()
|
||||
if availableMemory > 0 {
|
||||
metadata.AvailableMemory = byteformats.FormatMemoryBytes(availableMemory)
|
||||
}
|
||||
writeReportMetadata(destPath, metadata)
|
||||
copyConfigSnapshot(destPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeOOMProfile(destPath string, name string) {
|
||||
filePath, err := oomprofile.WriteFile(destPath, name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
chownReport(filePath)
|
||||
}
|
||||
12
experimental/libbox/panic.go
Normal file
12
experimental/libbox/panic.go
Normal file
@@ -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}
|
||||
}
|
||||
19
experimental/libbox/pidfd_android.go
Normal file
19
experimental/libbox/pidfd_android.go
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
141
experimental/libbox/platform.go
Normal file
141
experimental/libbox/platform.go
Normal file
@@ -0,0 +1,141 @@
|
||||
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
|
||||
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||
RegisterMyInterface(name string)
|
||||
}
|
||||
|
||||
type NeighborUpdateListener interface {
|
||||
UpdateNeighborTable(entries NeighborEntryIterator)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
33
experimental/libbox/pprof.go
Normal file
33
experimental/libbox/pprof.go
Normal file
@@ -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()
|
||||
}
|
||||
278
experimental/libbox/profile_import.go
Normal file
278
experimental/libbox/profile_import.go
Normal file
@@ -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)
|
||||
}
|
||||
41
experimental/libbox/remote_profile.go
Normal file
41
experimental/libbox/remote_profile.go
Normal file
@@ -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
|
||||
}
|
||||
97
experimental/libbox/report.go
Normal file
97
experimental/libbox/report.go
Normal file
@@ -0,0 +1,97 @@
|
||||
//go:build darwin || linux || windows
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type reportMetadata struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
BundleIdentifier string `json:"bundleIdentifier,omitempty"`
|
||||
ProcessName string `json:"processName,omitempty"`
|
||||
ProcessPath string `json:"processPath,omitempty"`
|
||||
StartedAt string `json:"startedAt,omitempty"`
|
||||
AppVersion string `json:"appVersion,omitempty"`
|
||||
AppMarketingVersion string `json:"appMarketingVersion,omitempty"`
|
||||
CoreVersion string `json:"coreVersion,omitempty"`
|
||||
GoVersion string `json:"goVersion,omitempty"`
|
||||
}
|
||||
|
||||
func baseReportMetadata() reportMetadata {
|
||||
processPath, _ := os.Executable()
|
||||
processName := filepath.Base(processPath)
|
||||
if processName == "." {
|
||||
processName = ""
|
||||
}
|
||||
return reportMetadata{
|
||||
Source: sCrashReportSource,
|
||||
ProcessName: processName,
|
||||
ProcessPath: processPath,
|
||||
CoreVersion: C.Version,
|
||||
GoVersion: GoVersion(),
|
||||
}
|
||||
}
|
||||
|
||||
func writeReportFile(destPath string, name string, content []byte) {
|
||||
filePath := filepath.Join(destPath, name)
|
||||
os.WriteFile(filePath, content, 0o666)
|
||||
chownReport(filePath)
|
||||
}
|
||||
|
||||
func writeReportMetadata(destPath string, metadata any) {
|
||||
data, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
writeReportFile(destPath, "metadata.json", data)
|
||||
}
|
||||
|
||||
func copyConfigSnapshot(destPath string) {
|
||||
snapshotPath := configSnapshotPath()
|
||||
content, err := os.ReadFile(snapshotPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(bytes.TrimSpace(content)) == 0 {
|
||||
return
|
||||
}
|
||||
writeReportFile(destPath, "configuration.json", content)
|
||||
}
|
||||
|
||||
func initReportDir(path string) {
|
||||
os.MkdirAll(path, 0o777)
|
||||
chownReport(path)
|
||||
}
|
||||
|
||||
func chownReport(path string) {
|
||||
if runtime.GOOS != "android" && runtime.GOOS != "windows" {
|
||||
os.Chown(path, sUserID, sGroupID)
|
||||
}
|
||||
}
|
||||
|
||||
func nextAvailableReportPath(reportsDir string, timestamp time.Time) (string, error) {
|
||||
destName := timestamp.Format("2006-01-02T15-04-05")
|
||||
destPath := filepath.Join(reportsDir, destName)
|
||||
_, err := os.Stat(destPath)
|
||||
if os.IsNotExist(err) {
|
||||
return destPath, nil
|
||||
}
|
||||
for i := 1; i <= 1000; i++ {
|
||||
suffixedPath := filepath.Join(reportsDir, destName+"-"+strconv.Itoa(i))
|
||||
_, err = os.Stat(suffixedPath)
|
||||
if os.IsNotExist(err) {
|
||||
return suffixedPath, nil
|
||||
}
|
||||
}
|
||||
return "", E.New("no available report path for ", destName)
|
||||
}
|
||||
27
experimental/libbox/semver.go
Normal file
27
experimental/libbox/semver.go
Normal file
@@ -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
|
||||
}
|
||||
16
experimental/libbox/semver_test.go
Normal file
16
experimental/libbox/semver_test.go
Normal file
@@ -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"))
|
||||
}
|
||||
288
experimental/libbox/service.go
Normal file
288
experimental/libbox/service.go
Normal file
@@ -0,0 +1,288 @@
|
||||
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
|
||||
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.iif.RegisterMyInterface(options.Name)
|
||||
return tun.New(*options)
|
||||
}
|
||||
|
||||
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) {
|
||||
if netInterface.Name == w.myTunName {
|
||||
continue
|
||||
}
|
||||
w.defaultInterfaceAccess.Lock()
|
||||
// (GOOS=windows) SA4006: this value of `isDefault` is never used
|
||||
// Why not used?
|
||||
//nolint:staticcheck
|
||||
isDefault := 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 (w *platformInterfaceWrapper) UsePlatformNeighborResolver() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) StartNeighborMonitor(listener adapter.NeighborUpdateListener) error {
|
||||
return w.iif.StartNeighborMonitor(&neighborUpdateListenerWrapper{listener: listener})
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) CloseNeighborMonitor(listener adapter.NeighborUpdateListener) error {
|
||||
return w.iif.CloseNeighborMonitor(nil)
|
||||
}
|
||||
|
||||
type neighborUpdateListenerWrapper struct {
|
||||
listener adapter.NeighborUpdateListener
|
||||
}
|
||||
|
||||
func (w *neighborUpdateListenerWrapper) UpdateNeighborTable(entries NeighborEntryIterator) {
|
||||
var result []adapter.NeighborEntry
|
||||
for entries.HasNext() {
|
||||
entry := entries.Next()
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
address, err := netip.ParseAddr(entry.Address)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
macAddress, err := net.ParseMAC(entry.MacAddress)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
result = append(result, adapter.NeighborEntry{
|
||||
Address: address,
|
||||
MACAddress: macAddress,
|
||||
Hostname: entry.Hostname,
|
||||
})
|
||||
}
|
||||
w.listener.UpdateNeighborTable(result)
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
9
experimental/libbox/service_other.go
Normal file
9
experimental/libbox/service_other.go
Normal file
@@ -0,0 +1,9 @@
|
||||
//go:build !windows
|
||||
|
||||
package libbox
|
||||
|
||||
import "syscall"
|
||||
|
||||
func dup(fd int) (nfd int, err error) {
|
||||
return syscall.Dup(fd)
|
||||
}
|
||||
7
experimental/libbox/service_windows.go
Normal file
7
experimental/libbox/service_windows.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package libbox
|
||||
|
||||
import "os"
|
||||
|
||||
func dup(fd int) (nfd int, err error) {
|
||||
return 0, os.ErrInvalid
|
||||
}
|
||||
191
experimental/libbox/setup.go
Normal file
191
experimental/libbox/setup.go
Normal file
@@ -0,0 +1,191 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/networkquality"
|
||||
"github.com/sagernet/sing-box/common/stun"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental/locale"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/service/oomkiller"
|
||||
"github.com/sagernet/sing/common/byteformats"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var (
|
||||
sBasePath string
|
||||
sWorkingPath string
|
||||
sTempPath string
|
||||
sUserID int
|
||||
sGroupID int
|
||||
sFixAndroidStack bool
|
||||
sCommandServerListenPort uint16
|
||||
sCommandServerSecret string
|
||||
sLogMaxLines int
|
||||
sDebug bool
|
||||
sCrashReportSource string
|
||||
sOOMKillerEnabled bool
|
||||
sOOMKillerDisabled bool
|
||||
sOOMMemoryLimit int64
|
||||
)
|
||||
|
||||
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
|
||||
CrashReportSource string
|
||||
OomKillerEnabled bool
|
||||
OomKillerDisabled bool
|
||||
OomMemoryLimit int64
|
||||
}
|
||||
|
||||
func applySetupOptions(options *SetupOptions) {
|
||||
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
|
||||
sCrashReportSource = options.CrashReportSource
|
||||
ReloadSetupOptions(options)
|
||||
}
|
||||
|
||||
func ReloadSetupOptions(options *SetupOptions) {
|
||||
sOOMKillerEnabled = options.OomKillerEnabled
|
||||
sOOMKillerDisabled = options.OomKillerDisabled
|
||||
sOOMMemoryLimit = options.OomMemoryLimit
|
||||
if sOOMKillerEnabled {
|
||||
if sOOMMemoryLimit == 0 && C.IsIos {
|
||||
sOOMMemoryLimit = oomkiller.DefaultAppleNetworkExtensionMemoryLimit
|
||||
}
|
||||
if sOOMMemoryLimit > 0 {
|
||||
debug.SetMemoryLimit(sOOMMemoryLimit * 3 / 4)
|
||||
} else {
|
||||
debug.SetMemoryLimit(math.MaxInt64)
|
||||
}
|
||||
} else {
|
||||
debug.SetMemoryLimit(math.MaxInt64)
|
||||
}
|
||||
}
|
||||
|
||||
func Setup(options *SetupOptions) error {
|
||||
applySetupOptions(options)
|
||||
os.MkdirAll(sWorkingPath, 0o777)
|
||||
os.MkdirAll(sTempPath, 0o777)
|
||||
return redirectStderr(filepath.Join(sWorkingPath, "CrashReport-"+sCrashReportSource+".log"))
|
||||
}
|
||||
|
||||
func SetLocale(localeId string) error {
|
||||
if strings.Contains(localeId, "@") {
|
||||
localeId = strings.Split(localeId, "@")[0]
|
||||
}
|
||||
if !locale.Set(localeId) {
|
||||
return E.New("unsupported locale: ", localeId)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Version() string {
|
||||
return C.Version
|
||||
}
|
||||
|
||||
func GoVersion() string {
|
||||
return runtime.Version() + ", " + runtime.GOOS + "/" + runtime.GOARCH
|
||||
}
|
||||
|
||||
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 FormatBitrate(bps int64) string {
|
||||
return networkquality.FormatBitrate(bps)
|
||||
}
|
||||
|
||||
const NetworkQualityDefaultConfigURL = networkquality.DefaultConfigURL
|
||||
|
||||
const NetworkQualityDefaultMaxRuntimeSeconds = int32(networkquality.DefaultMaxRuntime / time.Second)
|
||||
|
||||
const (
|
||||
NetworkQualityAccuracyLow = int32(networkquality.AccuracyLow)
|
||||
NetworkQualityAccuracyMedium = int32(networkquality.AccuracyMedium)
|
||||
NetworkQualityAccuracyHigh = int32(networkquality.AccuracyHigh)
|
||||
)
|
||||
|
||||
const (
|
||||
NetworkQualityPhaseIdle = int32(networkquality.PhaseIdle)
|
||||
NetworkQualityPhaseDownload = int32(networkquality.PhaseDownload)
|
||||
NetworkQualityPhaseUpload = int32(networkquality.PhaseUpload)
|
||||
NetworkQualityPhaseDone = int32(networkquality.PhaseDone)
|
||||
)
|
||||
|
||||
const STUNDefaultServer = stun.DefaultServer
|
||||
|
||||
const (
|
||||
STUNPhaseBinding = int32(stun.PhaseBinding)
|
||||
STUNPhaseNATMapping = int32(stun.PhaseNATMapping)
|
||||
STUNPhaseNATFiltering = int32(stun.PhaseNATFiltering)
|
||||
STUNPhaseDone = int32(stun.PhaseDone)
|
||||
)
|
||||
|
||||
const (
|
||||
NATMappingEndpointIndependent = int32(stun.NATMappingEndpointIndependent)
|
||||
NATMappingAddressDependent = int32(stun.NATMappingAddressDependent)
|
||||
NATMappingAddressAndPortDependent = int32(stun.NATMappingAddressAndPortDependent)
|
||||
)
|
||||
|
||||
const (
|
||||
NATFilteringEndpointIndependent = int32(stun.NATFilteringEndpointIndependent)
|
||||
NATFilteringAddressDependent = int32(stun.NATFilteringAddressDependent)
|
||||
NATFilteringAddressAndPortDependent = int32(stun.NATFilteringAddressAndPortDependent)
|
||||
)
|
||||
|
||||
func FormatNATMapping(value int32) string {
|
||||
return stun.NATMapping(value).String()
|
||||
}
|
||||
|
||||
func FormatNATFiltering(value int32) string {
|
||||
return stun.NATFiltering(value).String()
|
||||
}
|
||||
|
||||
func FormatFQDN(fqdn string) string {
|
||||
return dns.FqdnToDomain(fqdn)
|
||||
}
|
||||
|
||||
func ProxyDisplayType(proxyType string) string {
|
||||
return C.ProxyDisplayName(proxyType)
|
||||
}
|
||||
146
experimental/libbox/signal_handler_darwin.go
Normal file
146
experimental/libbox/signal_handler_darwin.go
Normal file
@@ -0,0 +1,146 @@
|
||||
//go:build darwin && badlinkname
|
||||
|
||||
package libbox
|
||||
|
||||
/*
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
static struct sigaction _go_sa[32];
|
||||
static struct sigaction _plcrash_sa[32];
|
||||
static int _saved = 0;
|
||||
|
||||
static int _signals[] = {SIGSEGV, SIGBUS, SIGFPE, SIGILL, SIGTRAP};
|
||||
static const int _signal_count = sizeof(_signals) / sizeof(_signals[0]);
|
||||
|
||||
static void _save_go_handlers(void) {
|
||||
if (_saved) return;
|
||||
for (int i = 0; i < _signal_count; i++)
|
||||
sigaction(_signals[i], NULL, &_go_sa[_signals[i]]);
|
||||
_saved = 1;
|
||||
}
|
||||
|
||||
static void _combined_handler(int sig, siginfo_t *info, void *uap) {
|
||||
// Step 1: PLCrashReporter writes .plcrash, resets all handlers to SIG_DFL,
|
||||
// and calls raise(sig) which pends (signal is blocked, no SA_NODEFER).
|
||||
if ((_plcrash_sa[sig].sa_flags & SA_SIGINFO) &&
|
||||
(uintptr_t)_plcrash_sa[sig].sa_sigaction > 1)
|
||||
_plcrash_sa[sig].sa_sigaction(sig, info, uap);
|
||||
|
||||
// SIGTRAP does not rely on sigreturn -> sigpanic. Once Go's trap trampoline
|
||||
// is force-installed, we can chain into it directly after PLCrashReporter.
|
||||
if (sig == SIGTRAP &&
|
||||
(_go_sa[sig].sa_flags & SA_SIGINFO) &&
|
||||
(uintptr_t)_go_sa[sig].sa_sigaction > 1) {
|
||||
_go_sa[sig].sa_sigaction(sig, info, uap);
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 2: Restore Go's handler via sigaction (overwrites PLCrashReporter's SIG_DFL).
|
||||
// Do NOT call Go's handler directly — Go's preparePanic only modifies the
|
||||
// ucontext and returns. The actual crash output is written by sigpanic, which
|
||||
// only runs when the KERNEL restores the modified ucontext via sigreturn.
|
||||
// A direct C function call has no sigreturn, so sigpanic would never execute.
|
||||
sigaction(sig, &_go_sa[sig], NULL);
|
||||
|
||||
// Step 3: Return. The kernel restores the original ucontext and re-executes
|
||||
// the faulting instruction. Two signals are now pending/imminent:
|
||||
// a) PLCrashReporter's raise() (SI_USER) — Go's handler ignores it
|
||||
// (sighandler: sigFromUser() → return).
|
||||
// b) The re-executed fault (SEGV_MAPERR) — Go's handler processes it:
|
||||
// preparePanic → kernel sigreturn → sigpanic → crash output written
|
||||
// via debug.SetCrashOutput.
|
||||
}
|
||||
|
||||
static void _reinstall_handlers(void) {
|
||||
if (!_saved) return;
|
||||
for (int i = 0; i < _signal_count; i++) {
|
||||
int sig = _signals[i];
|
||||
struct sigaction current;
|
||||
sigaction(sig, NULL, ¤t);
|
||||
// Only save the handler if it's not one of ours
|
||||
if (current.sa_sigaction != _combined_handler) {
|
||||
// If current handler is still Go's, PLCrashReporter wasn't installed
|
||||
if ((current.sa_flags & SA_SIGINFO) &&
|
||||
(uintptr_t)current.sa_sigaction > 1 &&
|
||||
current.sa_sigaction == _go_sa[sig].sa_sigaction)
|
||||
memset(&_plcrash_sa[sig], 0, sizeof(_plcrash_sa[sig]));
|
||||
else
|
||||
_plcrash_sa[sig] = current;
|
||||
}
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_sigaction = _combined_handler;
|
||||
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sigaction(sig, &sa, NULL);
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
_sigtrap = 5
|
||||
_nsig = 32
|
||||
)
|
||||
|
||||
//go:linkname runtimeGetsig runtime.getsig
|
||||
func runtimeGetsig(i uint32) uintptr
|
||||
|
||||
//go:linkname runtimeSetsig runtime.setsig
|
||||
func runtimeSetsig(i uint32, fn uintptr)
|
||||
|
||||
//go:linkname runtimeCgoSigtramp runtime.cgoSigtramp
|
||||
func runtimeCgoSigtramp()
|
||||
|
||||
//go:linkname runtimeFwdSig runtime.fwdSig
|
||||
var runtimeFwdSig [_nsig]uintptr
|
||||
|
||||
//go:linkname runtimeHandlingSig runtime.handlingSig
|
||||
var runtimeHandlingSig [_nsig]uint32
|
||||
|
||||
func forceGoSIGTRAPHandler() {
|
||||
runtimeFwdSig[_sigtrap] = runtimeGetsig(_sigtrap)
|
||||
runtimeHandlingSig[_sigtrap] = 1
|
||||
runtimeSetsig(_sigtrap, reflect.ValueOf(runtimeCgoSigtramp).Pointer())
|
||||
}
|
||||
|
||||
// PrepareCrashSignalHandlers captures Go's original synchronous signal handlers.
|
||||
//
|
||||
// In gomobile/c-archive embeddings, package init runs on the first Go entry.
|
||||
// That means a native crash reporter installed before the first Go call would
|
||||
// otherwise be captured as the "Go" handler and break handler restoration on
|
||||
// SIGSEGV. Go skips SIGTRAP in c-archive mode, so install its trap trampoline
|
||||
// before saving handlers. Call this before installing PLCrashReporter.
|
||||
func PrepareCrashSignalHandlers() {
|
||||
forceGoSIGTRAPHandler()
|
||||
C._save_go_handlers()
|
||||
}
|
||||
|
||||
// ReinstallCrashSignalHandlers installs a combined signal handler that chains
|
||||
// PLCrashReporter (native crash report) and Go's runtime handler (Go crash log).
|
||||
//
|
||||
// Call PrepareCrashSignalHandlers before installing PLCrashReporter, then call
|
||||
// this after PLCrashReporter has been installed.
|
||||
//
|
||||
// Flow on SIGSEGV:
|
||||
// 1. Combined handler calls PLCrashReporter's saved handler → .plcrash written
|
||||
// 2. Combined handler restores Go's handler via sigaction
|
||||
// 3. Combined handler returns — kernel re-executes faulting instruction
|
||||
// 4. PLCrashReporter's pending raise() (SI_USER) is ignored by Go's handler
|
||||
// 5. Hardware fault → Go's handler → preparePanic → kernel sigreturn →
|
||||
// sigpanic → crash output via debug.SetCrashOutput
|
||||
//
|
||||
// Flow on SIGTRAP:
|
||||
// 1. PrepareCrashSignalHandlers force-installs Go's cgo trap trampoline
|
||||
// 2. Combined handler calls PLCrashReporter's saved handler → .plcrash written
|
||||
// 3. Combined handler directly calls the saved Go trap trampoline
|
||||
func ReinstallCrashSignalHandlers() {
|
||||
C._reinstall_handlers()
|
||||
}
|
||||
7
experimental/libbox/signal_handler_stub.go
Normal file
7
experimental/libbox/signal_handler_stub.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !darwin || !badlinkname
|
||||
|
||||
package libbox
|
||||
|
||||
func PrepareCrashSignalHandlers() {}
|
||||
|
||||
func ReinstallCrashSignalHandlers() {}
|
||||
50
experimental/libbox/stun.go
Normal file
50
experimental/libbox/stun.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box/common/stun"
|
||||
)
|
||||
|
||||
type STUNTest struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewSTUNTest() *STUNTest {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
return &STUNTest{ctx: ctx, cancel: cancel}
|
||||
}
|
||||
|
||||
func (t *STUNTest) Start(server string, handler STUNTestHandler) {
|
||||
go func() {
|
||||
result, err := stun.Run(stun.Options{
|
||||
Server: server,
|
||||
Context: t.ctx,
|
||||
OnProgress: func(p stun.Progress) {
|
||||
handler.OnProgress(&STUNTestProgress{
|
||||
Phase: int32(p.Phase),
|
||||
ExternalAddr: p.ExternalAddr,
|
||||
LatencyMs: p.LatencyMs,
|
||||
NATMapping: int32(p.NATMapping),
|
||||
NATFiltering: int32(p.NATFiltering),
|
||||
})
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
handler.OnError(err.Error())
|
||||
return
|
||||
}
|
||||
handler.OnResult(&STUNTestResult{
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: int32(result.NATMapping),
|
||||
NATFiltering: int32(result.NATFiltering),
|
||||
NATTypeSupported: result.NATTypeSupported,
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *STUNTest) Cancel() {
|
||||
t.cancel()
|
||||
}
|
||||
168
experimental/libbox/tun.go
Normal file
168
experimental/libbox/tun.go
Normal file
@@ -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)
|
||||
}
|
||||
34
experimental/libbox/tun_darwin.go
Normal file
34
experimental/libbox/tun_darwin.go
Normal file
@@ -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
|
||||
}
|
||||
11
experimental/libbox/tun_name_darwin.go
Normal file
11
experimental/libbox/tun_name_darwin.go
Normal file
@@ -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 */
|
||||
)
|
||||
}
|
||||
26
experimental/libbox/tun_name_linux.go
Normal file
26
experimental/libbox/tun_name_linux.go
Normal file
@@ -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
|
||||
}
|
||||
9
experimental/libbox/tun_name_other.go
Normal file
9
experimental/libbox/tun_name_other.go
Normal file
@@ -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