Compare commits

..

29 Commits

Author SHA1 Message Date
世界
fbb01555c8 documentation: Update changelog 2023-04-19 22:05:54 +08:00
世界
3c217fea63 Improve VLESS request 2023-04-19 22:00:05 +08:00
世界
a02c676d33 Update badtls 2023-04-19 22:00:05 +08:00
世界
c2ae261c8d Add deadline interface 2023-04-19 22:00:05 +08:00
世界
1fe69cc08c shadowsocks: Multi-user support for legacy AEAD inbound
Signed-off-by: wwqgtxx <wwqgtxx@gmail.com>
2023-04-19 22:00:05 +08:00
世界
dacb885930 Add headers option for HTTP outbound 2023-04-19 22:00:05 +08:00
世界
31682c313b URLTest improvements 2023-04-19 21:59:59 +08:00
世界
6fd73131cb Fix wireguard reconnect 2023-04-19 21:59:58 +08:00
世界
f3ff76abcc Use HTTPS URLTest source 2023-04-19 21:59:58 +08:00
H1JK
d9757bfaff Add BaseContext to http servers 2023-04-19 21:59:58 +08:00
H1JK
9d97ed6220 Fix gRPC service name escape 2023-04-19 21:59:58 +08:00
Hellojack
054211d091 Remove TLS requirement for gRPC client 2023-04-19 21:59:51 +08:00
世界
69b92810d6 clash-api: Add Clash.Meta APIs 2023-04-19 21:59:51 +08:00
世界
ea23267ced clash api: download clash-dashboard if external-ui directory is empty 2023-04-19 21:59:51 +08:00
世界
bbe2b1ab09 Add multi-peer support for wireguard outbound 2023-04-19 21:59:51 +08:00
世界
d3530a54fb Add fakeip support 2023-04-19 21:59:47 +08:00
世界
c238df9309 Add L3 routing support 2023-04-19 21:59:47 +08:00
世界
a223262678 Add dns reverse mapping 2023-04-19 21:59:47 +08:00
armv9
ec13965fd0 Fix HTTP sniffer 2023-04-19 21:58:58 +08:00
世界
ddf747006e Update to uuid v5 2023-04-19 21:58:58 +08:00
世界
4382093868 Prepare deadline interface 2023-04-19 21:58:58 +08:00
世界
a5322850b3 documentation: Update client notes 2023-04-19 21:58:58 +08:00
世界
407b08975c Remove legacy warnings 2023-04-19 21:58:58 +08:00
世界
c7067ff5e8 Fix default interface monitor for darwin 2023-04-19 21:58:58 +08:00
世界
9b2384b296 Add udpnat test 2023-04-19 21:43:13 +08:00
世界
b498a22972 Fix interface monitor for android 2023-04-19 21:42:40 +08:00
世界
20e9da5c67 Fix udp timeout 2023-04-19 21:42:10 +08:00
世界
ec8974673b Fix platform interface monitor & Fix system tun stack for ios 2023-04-19 21:40:03 +08:00
世界
5e6e7923e4 Fix shadowsocksr build 2023-04-17 18:06:43 +08:00
176 changed files with 4942 additions and 1717 deletions

View File

@@ -13,6 +13,7 @@ type ClashServer interface {
PreStarter
Mode() string
StoreSelected() bool
StoreFakeIP() bool
CacheFile() ClashCacheFile
HistoryStorage() *urltest.HistoryStorage
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
@@ -22,6 +23,7 @@ type ClashServer interface {
type ClashCacheFile interface {
LoadSelected(group string) string
StoreSelected(group string, selected string) error
FakeIPStorage
}
type Tracker interface {
@@ -33,6 +35,11 @@ type OutboundGroup interface {
All() []string
}
type URLTestGroup interface {
OutboundGroup
URLTest(ctx context.Context, url string) (map[string]uint16, error)
}
func OutboundTag(detour Outbound) string {
if group, isGroup := detour.(OutboundGroup); isGroup {
return group.Now()

23
adapter/fakeip.go Normal file
View File

@@ -0,0 +1,23 @@
package adapter
import (
"net/netip"
"github.com/sagernet/sing-dns"
)
type FakeIPStore interface {
Service
Contains(address netip.Addr) bool
Create(domain string, strategy dns.DomainStrategy) (netip.Addr, error)
Lookup(address netip.Addr) (string, bool)
Reset() error
}
type FakeIPStorage interface {
FakeIPMetadata() *FakeIPMetadata
FakeIPSaveMetadata(metadata *FakeIPMetadata) error
FakeIPStore(address netip.Addr, domain string) error
FakeIPLoad(address netip.Addr) (string, bool)
FakeIPReset() error
}

View File

@@ -0,0 +1,50 @@
package adapter
import (
"bytes"
"encoding"
"encoding/binary"
"io"
"net/netip"
"github.com/sagernet/sing/common"
)
type FakeIPMetadata struct {
Inet4Range netip.Prefix
Inet6Range netip.Prefix
Inet4Current netip.Addr
Inet6Current netip.Addr
}
func (m *FakeIPMetadata) MarshalBinary() (data []byte, err error) {
var buffer bytes.Buffer
for _, marshaler := range []encoding.BinaryMarshaler{m.Inet4Range, m.Inet6Range, m.Inet4Current, m.Inet6Current} {
data, err = marshaler.MarshalBinary()
if err != nil {
return
}
common.Must(binary.Write(&buffer, binary.BigEndian, uint16(len(data))))
buffer.Write(data)
}
data = buffer.Bytes()
return
}
func (m *FakeIPMetadata) UnmarshalBinary(data []byte) error {
reader := bytes.NewReader(data)
for _, unmarshaler := range []encoding.BinaryUnmarshaler{&m.Inet4Range, &m.Inet6Range, &m.Inet4Current, &m.Inet6Current} {
var length uint16
common.Must(binary.Read(reader, binary.BigEndian, &length))
element := make([]byte, length)
_, err := io.ReadFull(reader, element)
if err != nil {
return err
}
err = unmarshaler.UnmarshalBinary(element)
if err != nil {
return err
}
}
return nil
}

View File

@@ -27,7 +27,7 @@ type InjectableInbound interface {
type InboundContext struct {
Inbound string
InboundType string
IPVersion int
IPVersion uint8
Network string
Source M.Socksaddr
Destination M.Socksaddr

View File

@@ -4,6 +4,7 @@ import (
"context"
"net"
"github.com/sagernet/sing-tun"
N "github.com/sagernet/sing/common/network"
)
@@ -17,3 +18,8 @@ type Outbound interface {
NewConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
}
type IPOutbound interface {
Outbound
NewIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) (tun.DirectDestination, error)
}

View File

@@ -21,8 +21,13 @@ type Router interface {
Outbound(tag string) (Outbound, bool)
DefaultOutbound(network string) Outbound
FakeIPStore() FakeIPStore
RouteConnection(ctx context.Context, conn net.Conn, metadata InboundContext) error
RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext) error
RouteIPConnection(ctx context.Context, conn tun.RouteContext, metadata InboundContext) tun.RouteAction
NatRequired(outbound string) bool
GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error)
@@ -32,6 +37,7 @@ type Router interface {
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
InterfaceFinder() control.InterfaceFinder
UpdateInterfaces() error
DefaultInterface() string
AutoDetectInterface() bool
AutoDetectInterfaceFunc() control.Func
@@ -39,7 +45,9 @@ type Router interface {
NetworkMonitor() tun.NetworkUpdateMonitor
InterfaceMonitor() tun.DefaultInterfaceMonitor
PackageManager() tun.PackageManager
Rules() []Rule
IPRules() []IPRule
TimeService
@@ -76,6 +84,12 @@ type Rule interface {
type DNSRule interface {
Rule
DisableCache() bool
RewriteTTL() *uint32
}
type IPRule interface {
Rule
Action() tun.ActionType
}
type InterfaceUpdateListener interface {

6
box.go
View File

@@ -133,6 +133,12 @@ func New(options Options) (*Box, error) {
if err != nil {
return nil, err
}
if options.PlatformInterface != nil {
err = options.PlatformInterface.Initialize(ctx, router)
if err != nil {
return nil, E.Cause(err, "initialize platform interface")
}
}
preServices := make(map[string]adapter.Service)
postServices := make(map[string]adapter.Service)
if needClashAPI {

View File

@@ -9,7 +9,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/gofrs/uuid"
"github.com/gofrs/uuid/v5"
"github.com/spf13/cobra"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)

View File

@@ -21,7 +21,7 @@ func TestMergeJSON(t *testing.T) {
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
Network: N.NetworkTCP,
Network: []string{N.NetworkTCP},
Outbound: "direct",
},
},
@@ -42,7 +42,7 @@ func TestMergeJSON(t *testing.T) {
{
Type: C.RuleTypeDefault,
DefaultOptions: option.DefaultRule{
Network: N.NetworkUDP,
Network: []string{N.NetworkUDP},
Outbound: "direct",
},
},

View File

@@ -1,4 +1,4 @@
//go:build go1.19 && !go1.20
//go:build go1.20 && !go1.21
package badtls
@@ -14,39 +14,60 @@ import (
"sync/atomic"
"unsafe"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
aTLS "github.com/sagernet/sing/common/tls"
)
type Conn struct {
*tls.Conn
writer N.ExtendedWriter
activeCall *int32
closeNotifySent *bool
version *uint16
rand io.Reader
halfAccess *sync.Mutex
halfError *error
cipher cipher.AEAD
explicitNonceLen int
halfPtr uintptr
halfSeq []byte
halfScratchBuf []byte
writer N.ExtendedWriter
isHandshakeComplete *atomic.Bool
activeCall *atomic.Int32
closeNotifySent *bool
version *uint16
rand io.Reader
halfAccess *sync.Mutex
halfError *error
cipher cipher.AEAD
explicitNonceLen int
halfPtr uintptr
halfSeq []byte
halfScratchBuf []byte
}
func Create(conn *tls.Conn) (TLSConn, error) {
if !handshakeComplete(conn) {
func TryCreate(conn aTLS.Conn) aTLS.Conn {
tlsConn, ok := conn.(*tls.Conn)
if !ok {
return conn
}
badConn, err := Create(tlsConn)
if err != nil {
log.Warn("initialize badtls: ", err)
return conn
}
return badConn
}
func Create(conn *tls.Conn) (aTLS.Conn, error) {
rawConn := reflect.Indirect(reflect.ValueOf(conn))
rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete")
if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid isHandshakeComplete")
}
isHandshakeComplete := (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr()))
if !isHandshakeComplete.Load() {
return nil, E.New("handshake not finished")
}
rawConn := reflect.Indirect(reflect.ValueOf(conn))
rawActiveCall := rawConn.FieldByName("activeCall")
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Int32 {
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid active call")
}
activeCall := (*int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
activeCall := (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
rawHalfConn := rawConn.FieldByName("out")
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half conn")
@@ -108,19 +129,20 @@ func Create(conn *tls.Conn) (TLSConn, error) {
}
halfScratchBuf := rawHalfScratchBuf.Bytes()
return &Conn{
Conn: conn,
writer: bufio.NewExtendedWriter(conn.NetConn()),
activeCall: activeCall,
closeNotifySent: closeNotifySent,
version: version,
halfAccess: halfAccess,
halfError: halfError,
cipher: aeadCipher,
explicitNonceLen: explicitNonceLen,
rand: randReader,
halfPtr: rawHalfConn.UnsafeAddr(),
halfSeq: halfSeq,
halfScratchBuf: halfScratchBuf,
Conn: conn,
writer: bufio.NewExtendedWriter(conn.NetConn()),
isHandshakeComplete: isHandshakeComplete,
activeCall: activeCall,
closeNotifySent: closeNotifySent,
version: version,
halfAccess: halfAccess,
halfError: halfError,
cipher: aeadCipher,
explicitNonceLen: explicitNonceLen,
rand: randReader,
halfPtr: rawHalfConn.UnsafeAddr(),
halfSeq: halfSeq,
halfScratchBuf: halfScratchBuf,
}, nil
}
@@ -130,15 +152,15 @@ func (c *Conn) WriteBuffer(buffer *buf.Buffer) error {
return common.Error(c.Write(buffer.Bytes()))
}
for {
x := atomic.LoadInt32(c.activeCall)
x := c.activeCall.Load()
if x&1 != 0 {
return net.ErrClosed
}
if atomic.CompareAndSwapInt32(c.activeCall, x, x+2) {
if c.activeCall.CompareAndSwap(x, x+2) {
break
}
}
defer atomic.AddInt32(c.activeCall, -2)
defer c.activeCall.Add(-2)
c.halfAccess.Lock()
defer c.halfAccess.Unlock()
if err := *c.halfError; err != nil {
@@ -186,6 +208,7 @@ func (c *Conn) WriteBuffer(buffer *buf.Buffer) error {
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+c.explicitNonceLen+c.cipher.Overhead()))
}
incSeq(c.halfPtr)
log.Trace("badtls write ", buffer.Len())
return c.writer.WriteBuffer(buffer)
}

View File

@@ -1,4 +1,4 @@
//go:build !go1.19 || go1.20
//go:build !go1.19 || go1.21
package badtls

View File

@@ -1,13 +0,0 @@
package badtls
import (
"context"
"crypto/tls"
"net"
)
type TLSConn interface {
net.Conn
HandshakeContext(ctx context.Context) error
ConnectionState() tls.ConnectionState
}

View File

@@ -1,9 +1,8 @@
//go:build go1.19 && !go.1.20
//go:build go1.20 && !go.1.21
package badtls
import (
"crypto/tls"
"reflect"
_ "unsafe"
)
@@ -16,9 +15,6 @@ const (
//go:linkname errShutdown crypto/tls.errShutdown
var errShutdown error
//go:linkname handshakeComplete crypto/tls.(*Conn).handshakeComplete
func handshakeComplete(conn *tls.Conn) bool
//go:linkname incSeq crypto/tls.(*halfConn).incSeq
func incSeq(conn uintptr)

View File

@@ -0,0 +1,114 @@
package badversion
import (
"strconv"
"strings"
F "github.com/sagernet/sing/common/format"
)
type Version struct {
Major int
Minor int
Patch int
PreReleaseIdentifier string
PreReleaseVersion int
}
func (v Version) After(anotherVersion Version) bool {
if v.Major > anotherVersion.Major {
return true
} else if v.Major < anotherVersion.Major {
return false
}
if v.Minor > anotherVersion.Minor {
return true
} else if v.Minor < anotherVersion.Minor {
return false
}
if v.Patch > anotherVersion.Patch {
return true
} else if v.Patch < anotherVersion.Patch {
return false
}
if v.PreReleaseIdentifier == "" && anotherVersion.PreReleaseIdentifier != "" {
return true
} else if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier == "" {
return false
}
if v.PreReleaseIdentifier != "" && anotherVersion.PreReleaseIdentifier != "" {
if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
return true
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
return false
}
if v.PreReleaseVersion > anotherVersion.PreReleaseVersion {
return true
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
return false
}
}
return false
}
func (v Version) String() string {
version := F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
if v.PreReleaseIdentifier != "" {
version = F.ToString(version, "-", v.PreReleaseIdentifier, ".", v.PreReleaseVersion)
}
return version
}
func (v Version) BadString() string {
version := F.ToString(v.Major, ".", v.Minor)
if v.Patch > 0 {
version = F.ToString(version, ".", v.Patch)
}
if v.PreReleaseIdentifier != "" {
version = F.ToString(version, "-", v.PreReleaseIdentifier)
if v.PreReleaseVersion > 0 {
version = F.ToString(version, v.PreReleaseVersion)
}
}
return version
}
func Parse(versionName string) (version Version) {
if strings.HasPrefix(versionName, "v") {
versionName = versionName[1:]
}
if strings.Contains(versionName, "-") {
parts := strings.Split(versionName, "-")
versionName = parts[0]
identifier := parts[1]
if strings.Contains(identifier, ".") {
identifierParts := strings.Split(identifier, ".")
version.PreReleaseIdentifier = identifierParts[0]
if len(identifierParts) >= 2 {
version.PreReleaseVersion, _ = strconv.Atoi(identifierParts[1])
}
} else {
if strings.HasPrefix(identifier, "alpha") {
version.PreReleaseIdentifier = "alpha"
version.PreReleaseVersion, _ = strconv.Atoi(identifier[5:])
} else if strings.HasPrefix(identifier, "beta") {
version.PreReleaseIdentifier = "beta"
version.PreReleaseVersion, _ = strconv.Atoi(identifier[4:])
} else {
version.PreReleaseIdentifier = identifier
}
}
}
versionElements := strings.Split(versionName, ".")
versionLen := len(versionElements)
if versionLen >= 1 {
version.Major, _ = strconv.Atoi(versionElements[0])
}
if versionLen >= 2 {
version.Minor, _ = strconv.Atoi(versionElements[1])
}
if versionLen >= 3 {
version.Patch, _ = strconv.Atoi(versionElements[2])
}
return
}

View File

@@ -0,0 +1,17 @@
package badversion
import "github.com/sagernet/sing-box/common/json"
func (v Version) MarshalJSON() ([]byte, error) {
return json.Marshal(v.String())
}
func (v *Version) UnmarshalJSON(data []byte) error {
var version string
err := json.Unmarshal(data, &version)
if err != nil {
return err
}
*v = Parse(version)
return nil
}

View File

@@ -0,0 +1,18 @@
package badversion
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestCompareVersion(t *testing.T) {
t.Parallel()
require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String())
require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString())
require.True(t, Parse("1.3.0").After(Parse("1.3-beta1")))
require.True(t, Parse("1.3.0").After(Parse("1.3.0-beta1")))
require.True(t, Parse("1.3.0-beta1").After(Parse("1.3.0-alpha1")))
require.True(t, Parse("1.3.1").After(Parse("1.3.0")))
require.True(t, Parse("1.4").After(Parse("1.3")))
}

View File

@@ -7,7 +7,6 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/dialer/conntrack"
"github.com/sagernet/sing-box/common/warning"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/control"
@@ -17,41 +16,6 @@ import (
"github.com/sagernet/tfo-go"
)
var warnBindInterfaceOnUnsupportedPlatform = warning.New(
func() bool {
return !(C.IsLinux || C.IsWindows || C.IsDarwin)
},
"outbound option `bind_interface` is only supported on Linux and Windows",
)
var warnRoutingMarkOnUnsupportedPlatform = warning.New(
func() bool {
return !C.IsLinux
},
"outbound option `routing_mark` is only supported on Linux",
)
var warnReuseAdderOnUnsupportedPlatform = warning.New(
func() bool {
return !(C.IsDarwin || C.IsDragonfly || C.IsFreebsd || C.IsLinux || C.IsNetbsd || C.IsOpenbsd || C.IsSolaris || C.IsWindows)
},
"outbound option `reuse_addr` is unsupported on current platform",
)
var warnProtectPathOnNonAndroid = warning.New(
func() bool {
return !C.IsAndroid
},
"outbound option `protect_path` is only supported on Android",
)
var warnTFOOnUnsupportedPlatform = warning.New(
func() bool {
return !(C.IsDarwin || C.IsFreebsd || C.IsLinux || C.IsWindows)
},
"outbound option `tcp_fast_open` is unsupported on current platform",
)
type DefaultDialer struct {
dialer4 tfo.Dialer
dialer6 tfo.Dialer
@@ -66,7 +30,6 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
var dialer net.Dialer
var listener net.ListenConfig
if options.BindInterface != "" {
warnBindInterfaceOnUnsupportedPlatform.Check()
bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
@@ -80,7 +43,6 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
listener.Control = control.Append(listener.Control, bindFunc)
}
if options.RoutingMark != 0 {
warnRoutingMarkOnUnsupportedPlatform.Check()
dialer.Control = control.Append(dialer.Control, control.RoutingMark(options.RoutingMark))
listener.Control = control.Append(listener.Control, control.RoutingMark(options.RoutingMark))
} else if router.DefaultMark() != 0 {
@@ -88,11 +50,9 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
listener.Control = control.Append(listener.Control, control.RoutingMark(router.DefaultMark()))
}
if options.ReuseAddr {
warnReuseAdderOnUnsupportedPlatform.Check()
listener.Control = control.Append(listener.Control, control.ReuseAddr())
}
if options.ProtectPath != "" {
warnProtectPathOnNonAndroid.Check()
dialer.Control = control.Append(dialer.Control, control.ProtectPath(options.ProtectPath))
listener.Control = control.Append(listener.Control, control.ProtectPath(options.ProtectPath))
}
@@ -101,9 +61,6 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
} else {
dialer.Timeout = C.TCPTimeout
}
if options.TCPFastOpen {
warnTFOOnUnsupportedPlatform.Check()
}
var udpFragment bool
if options.UDPFragment != nil {
udpFragment = *options.UDPFragment

View File

@@ -27,7 +27,12 @@ type slowOpenConn struct {
func DialSlowContext(dialer *tfo.Dialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
if dialer.DisableTFO || N.NetworkName(network) != N.NetworkTCP {
return dialer.DialContext(ctx, network, destination.String(), nil)
switch N.NetworkName(network) {
case N.NetworkTCP, N.NetworkUDP:
return dialer.Dialer.DialContext(ctx, network, destination.String())
default:
return dialer.Dialer.DialContext(ctx, network, destination.AddrString())
}
}
return &slowOpenConn{
dialer: dialer,

View File

@@ -249,6 +249,10 @@ func (c *ClientConn) WriterReplaceable() bool {
return c.requestWrite
}
func (c *ClientConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ClientConn) Upstream() any {
return c.Conn
}
@@ -377,6 +381,10 @@ func (c *ClientPacketConn) RemoteAddr() net.Addr {
return c.destination.UDPAddr()
}
func (c *ClientPacketConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ClientPacketConn) Upstream() any {
return c.ExtendedConn
}
@@ -518,6 +526,10 @@ func (c *ClientPacketAddrConn) FrontHeadroom() int {
return 2 + M.MaxSocksaddrLength
}
func (c *ClientPacketAddrConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ClientPacketAddrConn) Upstream() any {
return c.ExtendedConn
}

View File

@@ -131,6 +131,10 @@ func (c *ServerConn) FrontHeadroom() int {
return 0
}
func (c *ServerConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ServerConn) Upstream() any {
return c.ExtendedConn
}
@@ -183,6 +187,10 @@ func (c *ServerPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksad
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerPacketConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ServerPacketConn) Upstream() any {
return c.ExtendedConn
}
@@ -245,6 +253,10 @@ func (c *ServerPacketAddrConn) WritePacket(buffer *buf.Buffer, destination M.Soc
return c.ExtendedConn.WriteBuffer(buffer)
}
func (c *ServerPacketAddrConn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *ServerPacketAddrConn) Upstream() any {
return c.ExtendedConn
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/protocol/http"
)
@@ -15,5 +16,5 @@ func HTTPHost(ctx context.Context, reader io.Reader) (*adapter.InboundContext, e
if err != nil {
return nil, err
}
return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: request.Host}, nil
return &adapter.InboundContext{Protocol: C.ProtocolHTTP, Domain: M.ParseSocksaddr(request.Host).AddrString()}, nil
}

27
common/sniff/http_test.go Normal file
View File

@@ -0,0 +1,27 @@
package sniff_test
import (
"context"
"strings"
"testing"
"github.com/sagernet/sing-box/common/sniff"
"github.com/stretchr/testify/require"
)
func TestSniffHTTP1(t *testing.T) {
t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.google.com\r\nAccept: */*\r\n\r\n"
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.google.com")
}
func TestSniffHTTP1WithPort(t *testing.T) {
t.Parallel()
pkt := "GET / HTTP/1.1\r\nHost: www.gov.cn:8080\r\nAccept: */*\r\n\r\n"
metadata, err := sniff.HTTPHost(context.Background(), strings.NewReader(pkt))
require.NoError(t, err)
require.Equal(t, metadata.Domain, "www.gov.cn")
}

View File

@@ -125,7 +125,7 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
hello.SessionId[0] = 1
hello.SessionId[1] = 8
hello.SessionId[2] = 0
hello.SessionId[2] = 1
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
copy(hello.SessionId[8:], e.shortID[:])

View File

@@ -50,6 +50,9 @@ func (s *HistoryStorage) StoreURLTestHistory(tag string, history *History) {
}
func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err error) {
if link == "" {
link = "https://www.gstatic.com/generate_204"
}
linkURL, err := url.Parse(link)
if err != nil {
return

View File

@@ -1,31 +0,0 @@
package warning
import (
"sync"
"github.com/sagernet/sing-box/log"
)
type Warning struct {
logger log.Logger
check CheckFunc
message string
checkOnce sync.Once
}
type CheckFunc = func() bool
func New(checkFunc CheckFunc, message string) Warning {
return Warning{
check: checkFunc,
message: message,
}
}
func (w *Warning) Check() {
w.checkOnce.Do(func() {
if w.check() {
log.Warn(w.message)
}
})
}

View File

@@ -12,6 +12,7 @@ const dirName = "sing-box"
var (
basePath string
tempPath string
resourcePaths []string
)
@@ -22,10 +23,21 @@ func BasePath(name string) string {
return filepath.Join(basePath, name)
}
func CreateTemp(pattern string) (*os.File, error) {
if tempPath == "" {
tempPath = os.TempDir()
}
return os.CreateTemp(tempPath, pattern)
}
func SetBasePath(path string) {
basePath = path
}
func SetTempPath(path string) {
tempPath = path
}
func FindPath(name string) (string, bool) {
name = os.ExpandEnv(name)
if rw.FileExists(name) {

View File

@@ -1,7 +1,65 @@
#### 1.3-beta8
* Fix `system` tun stack for ios
* Fix network monitor for android/ios
* Update VLESS and XUDP protocol **1**
* Fixes and improvements
*1:
As in Xray, this is an incompatible update for XUDP in VLESS if vision flow is enabled.
#### 1.3-beta7
* Add `path` and `headers` options for HTTP outbound
* Add multi-user support for Shadowsocks legacy AEAD inbound
* Fixes and improvements
#### 1.2.4
* Fixes and improvements
#### 1.3-beta6
* Fix WireGuard reconnect
* Perform URLTest recheck after network changes
* Fix bugs and update dependencies
#### 1.3-beta5
* Add Clash.Meta API compatibility for Clash API
* Download Yacd-meta by default if the specified Clash `external_ui` directory is empty
* Add path and headers option for HTTP outbound
* Fixes and improvements
#### 1.3-beta4
* Fix bugs
#### 1.3-beta2
* Download clash-dashboard if the specified Clash `external_ui` directory is empty
* Fix bugs and update dependencies
#### 1.3-beta1
* Add [DNS reverse mapping](/configuration/dns#reverse_mapping) support
* Add [L3 routing](/configuration/route/ip-rule) support **1**
* Add `rewrite_ttl` DNS rule action
* Add [FakeIP](/configuration/dns/fakeip) support **2**
* Add `store_fakeip` Clash API option
* Add multi-peer support for [WireGuard](/configuration/outbound/wireguard#peers) outbound
* Add loopback detect
*1*:
It can currently be used to [route connections directly to WireGuard](/examples/wireguard-direct) or block connections
at the IP layer.
*2*:
See [FAQ](/faq/fakeip) for more information.
#### 1.2.3
* Introducing our [new Android client application](/installation/clients/sfa)

View File

@@ -0,0 +1,25 @@
# FakeIP
### Structure
```json
{
"enabled": true,
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
```
### Fields
#### enabled
Enable FakeIP service.
#### inet4_range
IPv4 address range for FakeIP.
#### inet6_address
IPv6 address range for FakeIP.

View File

@@ -0,0 +1,25 @@
# FakeIP
### 结构
```json
{
"enabled": true,
"inet4_range": "198.18.0.0/15",
"inet6_range": "fc00::/18"
}
```
### 字段
#### enabled
启用 FakeIP 服务。
#### inet4_range
用于 FakeIP 的 IPv4 地址范围。
#### inet6_range
用于 FakeIP 的 IPv6 地址范围。

View File

@@ -10,7 +10,9 @@
"final": "",
"strategy": "",
"disable_cache": false,
"disable_expire": false
"disable_expire": false,
"reverse_mapping": false,
"fakeip": {}
}
}
@@ -22,6 +24,7 @@
|----------|--------------------------------|
| `server` | List of [DNS Server](./server) |
| `rules` | List of [DNS Rule](./rule) |
| `fakeip` | [FakeIP](./fakeip) |
#### final
@@ -43,4 +46,15 @@ Disable dns cache.
#### disable_expire
Disable dns cache expire.
Disable dns cache expire.
#### reverse_mapping
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
Since this process relies on the act of resolving domain names by an application before making a request, it can be
problematic in environments such as macOS, where DNS is proxied and cached by the system.
#### fakeip
[FakeIP](./fakeip) settings.

View File

@@ -10,7 +10,9 @@
"final": "",
"strategy": "",
"disable_cache": false,
"disable_expire": false
"disable_expire": false,
"reverse_mapping": false,
"fakeip": {}
}
}
@@ -43,4 +45,14 @@
#### disable_expire
禁用 DNS 缓存过期。
禁用 DNS 缓存过期。
#### reverse_mapping
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
#### fakeip
[FakeIP](./fakeip) 设置。

View File

@@ -84,14 +84,16 @@
"direct"
],
"server": "local",
"disable_cache": false
"disable_cache": false,
"rewrite_ttl": 100
},
{
"type": "logical",
"mode": "and",
"rules": [],
"server": "local",
"disable_cache": false
"disable_cache": false,
"rewrite_ttl": 100
}
]
}
@@ -244,6 +246,10 @@ Tag of the target dns server.
Disable cache and save cache in this query.
#### rewrite_ttl
Rewrite TTL in DNS responses.
### Logical Fields
#### type

View File

@@ -243,6 +243,10 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
在此查询中禁用缓存。
#### rewrite_ttl
重写 DNS 回应中的 TTL。
### 逻辑字段
#### type

View File

@@ -30,17 +30,18 @@ The tag of the dns server.
The address of the dns server.
| Protocol | Format |
|----------|-------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto` or `dhcp://en0` |
| Protocol | Format |
|---------------------|-------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto` or `dhcp://en0` |
| [FakeIP](./fakeip) | `fakeip` |
!!! warning ""

View File

@@ -30,17 +30,18 @@ DNS 服务器的标签。
DNS 服务器的地址。
| 协议 | 格式 |
|----------|------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto``dhcp://en0` |
| 协议 | 格式 |
|--------------------|------------------------------|
| `System` | `local` |
| `TCP` | `tcp://1.0.0.1` |
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
| `TLS` | `tls://dns.google` |
| `HTTPS` | `https://1.1.1.1/dns-query` |
| `QUIC` | `quic://dns.adguard.com` |
| `HTTP3` | `h3://8.8.8.8/dns-query` |
| `RCode` | `rcode://refused` |
| `DHCP` | `dhcp://auto``dhcp://en0` |
| [FakeIP](./fakeip) | `fakeip` |
!!! warning ""

View File

@@ -8,6 +8,8 @@
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "folder",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "",
"default_mode": "rule",
"store_selected": false,
@@ -53,6 +55,18 @@ A relative path to the configuration directory or an absolute path to a
directory in which you put some static web resource. sing-box will then
serve it at `http://{{external-controller}}/ui`.
#### external_ui_download_url
ZIP download URL for the external UI, will be used if the specified `external_ui` directory is empty.
`https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip` will be used if empty.
#### external_ui_download_detour
The tag of the outbound to download the external UI.
Default outbound will be used if empty.
#### secret
Secret for the RESTful API (optional)

View File

@@ -8,6 +8,8 @@
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "folder",
"external_ui_download_url": "",
"external_ui_download_detour": "",
"secret": "",
"default_mode": "rule",
"store_selected": false,
@@ -51,6 +53,18 @@ RESTful web API 监听地址。如果为空,则禁用 Clash API。
到静态网页资源目录的相对路径或绝对路径。sing-box 会在 `http://{{external-controller}}/ui` 下提供它。
#### external_ui_download_url
静态网页资源的 ZIP 下载 URL如果指定的 `external_ui` 目录为空,将使用。
默认使用 `https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip`
#### external_ui_download_detour
用于下载静态网页资源的出站的标签。
如果为空,将使用默认出站。
#### secret
RESTful API 的密钥(可选)

View File

@@ -11,6 +11,8 @@
"server_port": 1080,
"username": "sekai",
"password": "admin",
"path": "",
"headers": {},
"tls": {},
... // Dial Fields
@@ -39,6 +41,14 @@ Basic authorization username.
Basic authorization password.
#### path
Path of HTTP request.
#### headers
Extra headers of HTTP request.
#### tls
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).

View File

@@ -11,6 +11,8 @@
"server_port": 1080,
"username": "sekai",
"password": "admin",
"path": "",
"headers": {},
"tls": {},
... // 拨号字段
@@ -39,6 +41,14 @@ Basic 认证用户名。
Basic 认证密码。
#### path
HTTP 请求路径。
#### headers
HTTP 请求的额外标头。
#### tls
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。

View File

@@ -37,4 +37,10 @@
#### tag
The tag of the outbound.
The tag of the outbound.
### Features
#### Outbounds that support IP connection
* `WireGuard`

View File

@@ -36,4 +36,10 @@
#### tag
出站的标签。
出站的标签。
### 特性
#### 支持 IP 连接的出站
* `WireGuard`

View File

@@ -10,7 +10,7 @@
"proxy-b",
"proxy-c"
],
"url": "http://www.gstatic.com/generate_204",
"url": "https://www.gstatic.com/generate_204",
"interval": "1m",
"tolerance": 50
}
@@ -26,7 +26,7 @@ List of outbound tags to test.
#### url
The URL to test. `http://www.gstatic.com/generate_204` will be used if empty.
The URL to test. `https://www.gstatic.com/generate_204` will be used if empty.
#### interval

View File

@@ -10,7 +10,7 @@
"proxy-b",
"proxy-c"
],
"url": "http://www.gstatic.com/generate_204",
"url": "https://www.gstatic.com/generate_204",
"interval": "1m",
"tolerance": 50
}
@@ -26,7 +26,7 @@
#### url
用于测试的链接。默认使用 `http://www.gstatic.com/generate_204`
用于测试的链接。默认使用 `https://www.gstatic.com/generate_204`
#### interval

View File

@@ -13,6 +13,18 @@
"10.0.0.2/32"
],
"private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=",
"peers": [
{
"server": "127.0.0.1",
"server_port": 1080,
"public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
"allowed_ips": [
"0.0.0.0/0"
],
"reserved": [0, 0, 0]
}
],
"peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
"reserved": [0, 0, 0],
@@ -36,13 +48,13 @@
#### server
==Required==
==Required if multi-peer disabled==
The server address.
#### server_port
==Required==
==Required if multi-peer disabled==
The server port.
@@ -75,9 +87,25 @@ wg genkey
echo "private key" || wg pubkey
```
#### peers
Multi-peer support.
If enabled, `server, server_port, peer_public_key, pre_shared_key` will be ignored.
#### peers.allowed_ips
WireGuard allowed IPs.
#### peers.reserved
WireGuard reserved field bytes.
`$outbound.reserved` will be used if empty.
#### peer_public_key
==Required==
==Required if multi-peer disabled==
WireGuard peer public key.

View File

@@ -7,6 +7,7 @@
"route": {
"geoip": {},
"geosite": {},
"ip_rules": [],
"rules": [],
"final": "",
"auto_detect_interface": false,
@@ -19,11 +20,12 @@
### Fields
| Key | Format |
|-----------|------------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [Geosite](./geosite) |
| `rules` | List of [Route Rule](./rule) |
| Key | Format |
|------------|------------------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [Geosite](./geosite) |
| `ip_rules` | List of [IP Route Rule](./ip-rule) |
| `rules` | List of [Route Rule](./rule) |
#### final

View File

@@ -7,6 +7,7 @@
"route": {
"geoip": {},
"geosite": {},
"ip_rules": [],
"rules": [],
"final": "",
"auto_detect_interface": false,
@@ -19,11 +20,12 @@
### 字段
| 键 | 格式 |
|-----------|----------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [GeoSite](./geosite) |
| `rules` | 一组 [路由规则](./rule) |
| 键 | 格式 |
|------------|-------------------------|
| `geoip` | [GeoIP](./geoip) |
| `geosite` | [GeoSite](./geosite) |
| `ip_rules` | 一组 [IP 路由规则](./ip-rule) |
| `rules` | 一组 [路由规则](./rule) |
#### final
@@ -65,4 +67,4 @@
默认为出站连接设置路由标记。
如果设置了 `outbound.routing_mark` 设置,则不生效。
如果设置了 `outbound.routing_mark` 设置,则不生效。

View File

@@ -0,0 +1,205 @@
### Structure
```json
{
"route": {
"ip_rules": [
{
"inbound": [
"mixed-in"
],
"ip_version": 6,
"network": [
"tcp"
],
"domain": [
"test.com"
],
"domain_suffix": [
".cn"
],
"domain_keyword": [
"test"
],
"domain_regex": [
"^stun\\..+"
],
"geosite": [
"cn"
],
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_port": [
12345
],
"source_port_range": [
"1000:2000",
":3000",
"4000:"
],
"port": [
80,
443
],
"port_range": [
"1000:2000",
":3000",
"4000:"
],
"invert": false,
"action": "direct",
"outbound": "wireguard"
},
{
"type": "logical",
"mode": "and",
"rules": [],
"invert": false,
"action": "direct",
"outbound": "wireguard"
}
]
}
}
```
!!! note ""
You can ignore the JSON Array [] tag when the content is only one item
### Default Fields
!!! note ""
The default rule uses the following matching logic:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr`) &&
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound
Tags of [Inbound](/configuration/inbound).
#### ip_version
4 or 6.
Not limited if empty.
#### network
Match network protocol.
Available values:
* `tcp`
* `udp`
* `icmpv4`
* `icmpv6`
#### domain
Match full domain.
#### domain_suffix
Match domain suffix.
#### domain_keyword
Match domain using keyword.
#### domain_regex
Match domain using regular expression.
#### geosite
Match geosite.
#### source_geoip
Match source geoip.
#### geoip
Match geoip.
#### source_ip_cidr
Match source ip cidr.
#### ip_cidr
Match ip cidr.
#### source_port
Match source port.
#### source_port_range
Match source port range.
#### port
Match port.
#### port_range
Match port range.
#### invert
Invert match result.
#### action
==Required==
| Action | Description |
|--------|--------------------------------------------------------------------|
| return | Stop IP routing and assemble the connection to the transport layer |
| block | Block the connection |
| direct | Directly forward the connection |
#### outbound
==Required if action is direct==
Tag of the target outbound.
Only outbound which supports IP connection can be used, see [Outbounds that support IP connection](/configuration/outbound/#outbounds-that-support-ip-connection).
### Logical Fields
#### type
`logical`
#### mode
==Required==
`and` or `or`
#### rules
==Required==
Included default rules.

View File

@@ -0,0 +1,204 @@
### 结构
```json
{
"route": {
"ip_rules": [
{
"inbound": [
"mixed-in"
],
"ip_version": 6,
"network": [
"tcp"
],
"domain": [
"test.com"
],
"domain_suffix": [
".cn"
],
"domain_keyword": [
"test"
],
"domain_regex": [
"^stun\\..+"
],
"geosite": [
"cn"
],
"source_geoip": [
"private"
],
"geoip": [
"cn"
],
"source_ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"ip_cidr": [
"10.0.0.0/24",
"192.168.0.1"
],
"source_port": [
12345
],
"source_port_range": [
"1000:2000",
":3000",
"4000:"
],
"port": [
80,
443
],
"port_range": [
"1000:2000",
":3000",
"4000:"
],
"invert": false,
"action": "direct",
"outbound": "wireguard"
},
{
"type": "logical",
"mode": "and",
"rules": [],
"invert": false,
"action": "direct",
"outbound": "wireguard"
}
]
}
}
```
!!! note ""
当内容只有一项时,可以忽略 JSON 数组 [] 标签。
### Default Fields
!!! note ""
默认规则使用以下匹配逻辑:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr`) &&
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound
[入站](/zh/configuration/inbound) 标签。
#### ip_version
4 或 6。
默认不限制。
#### network
匹配网络协议。
可用值:
* `tcp`
* `udp`
* `icmpv4`
* `icmpv6`
#### domain
匹配完整域名。
#### domain_suffix
匹配域名后缀。
#### domain_keyword
匹配域名关键字。
#### domain_regex
匹配域名正则表达式。
#### geosite
匹配 GeoSite。
#### source_geoip
匹配源 GeoIP。
#### geoip
匹配 GeoIP。
#### source_ip_cidr
匹配源 IP CIDR。
#### ip_cidr
匹配 IP CIDR。
#### source_port
匹配源端口。
#### source_port_range
匹配源端口范围。
#### port
匹配端口。
#### port_range
匹配端口范围。
#### invert
反选匹配结果。
#### action
==必填==
| Action | 描述 |
|--------|---------------------|
| return | 停止 IP 路由并将该连接组装到传输层 |
| block | 屏蔽该连接 |
| direct | 直接转发该连接 |
#### outbound
==action 为 direct 则必填==
目标出站的标签。
### 逻辑字段
#### type
`logical`
#### mode
==必填==
`and``or`
#### rules
==必填==
包括的默认规则。

View File

@@ -9,7 +9,9 @@
"mixed-in"
],
"ip_version": 6,
"network": "tcp",
"network": [
"tcp"
],
"auth_user": [
"usera",
"userb"
@@ -244,18 +246,12 @@ Tag of the target outbound.
#### mode
==Required==
`and` or `or`
#### rules
Included default rules.
#### invert
Invert match result.
#### outbound
==Required==
Tag of the target outbound.
Included default rules.

View File

@@ -9,7 +9,9 @@
"mixed-in"
],
"ip_version": 6,
"network": "tcp",
"network": [
"tcp"
],
"auth_user": [
"usera",
"userb"
@@ -242,18 +244,12 @@
#### mode
==必填==
`and``or`
#### rules
包括的默认规则。
#### invert
反选匹配结果。
#### outbound
==必填==
目标出站的标签
包括的默认规则

View File

@@ -8,3 +8,4 @@ Configuration examples for sing-box.
* [Shadowsocks](./shadowsocks)
* [ShadowTLS](./shadowtls)
* [Clash API](./clash-api)
* [WireGuard Direct](./wireguard-direct)

View File

@@ -8,3 +8,4 @@ sing-box 的配置示例。
* [Shadowsocks](./shadowsocks)
* [ShadowTLS](./shadowtls)
* [Clash API](./clash-api)
* [WireGuard Direct](./wireguard-direct)

View File

@@ -0,0 +1,90 @@
# WireGuard Direct
```json
{
"dns": {
"servers": [
{
"tag": "google",
"address": "tls://8.8.8.8"
},
{
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
}
],
"rules": [
{
"geoip": "cn",
"server": "direct"
}
],
"reverse_mapping": true
},
"inbounds": [
{
"type": "tun",
"tag": "tun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"sniff": true,
"stack": "system"
}
],
"outbounds": [
{
"type": "wireguard",
"tag": "wg",
"server": "127.0.0.1",
"server_port": 2345,
"local_address": [
"172.19.0.1/128"
],
"private_key": "KLTnpPY03pig/WC3zR8U7VWmpANHPFh2/4pwICGJ5Fk=",
"peer_public_key": "uvNabcamf6Rs0vzmcw99jsjTJbxo6eWGOykSY66zsUk="
},
{
"type": "dns",
"tag": "dns"
},
{
"type": "direct",
"tag": "direct"
},
{
"type": "block",
"tag": "block"
}
],
"route": {
"ip_rules": [
{
"port": 53,
"action": "return"
},
{
"geoip": "cn",
"geosite": "cn",
"action": "return"
},
{
"action": "direct",
"outbound": "wg"
}
],
"rules": [
{
"protocol": "dns",
"outbound": "dns"
},
{
"geoip": "cn",
"geosite": "cn",
"outbound": "direct"
}
],
"auto_detect_interface": true
}
}
```

20
docs/faq/fakeip.md Normal file
View File

@@ -0,0 +1,20 @@
# FakeIP
FakeIP refers to a type of behavior in a program that simultaneously hijacks both DNS and connection requests. It
responds to DNS requests with virtual results and restores mapping when accepting connections.
#### Advantage
* Retrieve the requested domain in places like IP routing (L3) where traffic detection is not possible to assist with routing.
* Decrease an RTT on the first TCP request to a domain (the most common reason).
#### Limitation
* Its mechanism breaks applications that depend on returning correct remote addresses.
* Only A and AAAA (IP) requests are supported, which may break applications that rely on other requests.
#### Recommendation
* Do not use if you do not need L3 routing.
* If using tun, make sure FakeIP ranges is included in the tun's routes.
* Enable `experimental.clash_api.store_fakeip` to persist FakeIP records, or use `dns.rules.rewrite_ttl` to avoid losing records after program restart in DNS cached environments.

19
docs/faq/fakeip.zh.md Normal file
View File

@@ -0,0 +1,19 @@
# FakeIP
FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它通过虚拟结果响应 DNS 请求,在接受连接时恢复映射。
#### 优点
* 在像 L3 路由这样无法进行流量探测的地方检索所请求的域名,以协助路由。
* 减少对一个域的第一个 TCP 请求的 RTT这是最常见的原因
#### 限制
* 它的机制会破坏依赖于返回正确远程地址的应用程序。
* 仅支持 A 和 AAAAIP请求这可能会破坏依赖于其他请求的应用程序。
#### 建议
* 如果不需要 L3 路由,请勿使用。
* 如果使用 tun请确保 tun 路由中包含 FakeIP 地址范围。
* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。

View File

@@ -11,12 +11,6 @@ it doesn't fit, because it compromises performance or design clarity, or because
If it bothers you that sing-box is missing feature X, please forgive us and investigate the features that sing-box does
have. You might find that they compensate in interesting ways for the lack of X.
#### Fake IP
Fake IP (also called Fake DNS) is an invasive and imperfect DNS solution that breaks expected behavior, causes DNS leaks
and makes many software unusable. It is recommended by some software that lacks DNS processing and caching, but sing-box
does not need this.
#### Naive outbound
NaïveProxy's main function is chromium's network stack, and it makes no sense to implement only its transport protocol.

View File

@@ -9,11 +9,6 @@
如果 sing-box 缺少功能 X 让您感到困扰,请原谅我们并调查 sing-box 确实有的功能。 您可能会发现它们以有趣的方式弥补了 X 的缺失。
#### Fake IP
Fake IP也称 Fake DNS是一种侵入性和不完善的 DNS 解决方案,它打破了预期的行为,导致 DNS 泄漏并使许多软件无法使用。
一些缺乏 DNS 处理和缓存的软件推荐使用它,但 sing-box 不需要。
#### Naive 出站
NaïveProxy 的主要功能是 chromium 的网络栈,仅实现它的传输协议是舍本逐末的。

View File

@@ -12,5 +12,6 @@ Experimental Android client for sing-box.
#### Note
* Working directory is at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory)
* User Agent is `SFA/$version ($version_code; sing-box $sing_box_version)` in the remote profile request
* User Agent in remote profile request is `SFA/$version ($version_code; sing-box $sing_box_version)`
* The working directory is located at `/sdcard/Android/data/io.nekohasekai.sfa/files` (External files directory)
* Crash logs is located in `$working_directory/stderr.log`

View File

@@ -12,5 +12,6 @@
#### 注意事项
* 工作目录位于 `/sdcard/Android/data/io.nekohasekai.sfa/files` (外部文件目录)
* 远程配置文件请求中的 User Agent 为 `SFA/$version ($version_code; sing-box $sing_box_version)`
* 工作目录位于 `/sdcard/Android/data/io.nekohasekai.sfa/files` (外部文件目录)
* 崩溃日志位于 `$working_directory/stderr.log`

View File

@@ -13,8 +13,8 @@ Experimental iOS client for sing-box.
#### Note
* `system` tun stack not working on iOS
* User Agent is `SFI/$version ($version_code; sing-box $sing_box_version)` in the remote profile request
* User Agent in remote profile request is `SFA/$version ($version_code; sing-box $sing_box_version)`
* Crash logs is located in `Settings` -> `View Service Log`
#### Privacy policy

View File

@@ -13,8 +13,8 @@
#### 注意事项
* `system` tun stack 在 iOS 不工作
* 远程配置文件请求中的 User Agent 为 `SFI/$version ($version_code; sing-box $sing_box_version)`
* 崩溃日志位于 `设置` -> `查看服务日志`
#### 隐私政策

View File

@@ -0,0 +1,78 @@
package clashapi
import (
"bytes"
"net/http"
"time"
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
"github.com/sagernet/websocket"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
// API created by Clash.Meta
func (s *Server) setupMetaAPI(r chi.Router) {
r.Get("/memory", memory(s.trafficManager))
r.Mount("/group", groupRouter(s))
}
type Memory struct {
Inuse uint64 `json:"inuse"`
OSLimit uint64 `json:"oslimit"` // maybe we need it in the future
}
func memory(trafficManager *trafficontrol.Manager) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var wsConn *websocket.Conn
if websocket.IsWebSocketUpgrade(r) {
var err error
wsConn, err = upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
}
if wsConn == nil {
w.Header().Set("Content-Type", "application/json")
render.Status(r, http.StatusOK)
}
tick := time.NewTicker(time.Second)
defer tick.Stop()
buf := &bytes.Buffer{}
var err error
first := true
for range tick.C {
buf.Reset()
inuse := trafficManager.Snapshot().Memory
// make chat.js begin with zero
// this is shit var,but we need output 0 for first time
if first {
first = false
inuse = 0
}
if err := json.NewEncoder(buf).Encode(Memory{
Inuse: inuse,
OSLimit: 0,
}); err != nil {
break
}
if wsConn == nil {
_, err = w.Write(buf.Bytes())
w.(http.Flusher).Flush()
} else {
err = wsConn.WriteMessage(websocket.TextMessage, buf.Bytes())
}
if err != nil {
break
}
}
}
}

View File

@@ -0,0 +1,136 @@
package clashapi
import (
"context"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/badjson"
"github.com/sagernet/sing-box/common/urltest"
"github.com/sagernet/sing-box/outbound"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/batch"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func groupRouter(server *Server) http.Handler {
r := chi.NewRouter()
r.Get("/", getGroups(server))
r.Route("/{name}", func(r chi.Router) {
r.Use(parseProxyName, findProxyByName(server.router))
r.Get("/", getGroup(server))
r.Get("/delay", getGroupDelay(server))
})
return r
}
func getGroups(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
groups := common.Map(common.Filter(server.router.Outbounds(), func(it adapter.Outbound) bool {
_, isGroup := it.(adapter.OutboundGroup)
return isGroup
}), func(it adapter.Outbound) *badjson.JSONObject {
return proxyInfo(server, it)
})
render.JSON(w, r, render.M{
"proxies": groups,
})
}
}
func getGroup(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
if _, ok := proxy.(adapter.OutboundGroup); ok {
render.JSON(w, r, proxyInfo(server, proxy))
return
}
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
}
}
func getGroupDelay(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
proxy := r.Context().Value(CtxKeyProxy).(adapter.Outbound)
group, ok := proxy.(adapter.OutboundGroup)
if !ok {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, ErrNotFound)
return
}
query := r.URL.Query()
url := query.Get("url")
if strings.HasPrefix(url, "http://") {
url = ""
}
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 32)
if err != nil {
render.Status(r, http.StatusBadRequest)
render.JSON(w, r, ErrBadRequest)
return
}
ctx, cancel := context.WithTimeout(r.Context(), time.Millisecond*time.Duration(timeout))
defer cancel()
var result map[string]uint16
if urlTestGroup, isURLTestGroup := group.(adapter.URLTestGroup); isURLTestGroup {
result, err = urlTestGroup.URLTest(ctx, url)
} else {
outbounds := common.FilterNotNil(common.Map(group.All(), func(it string) adapter.Outbound {
itOutbound, _ := server.router.Outbound(it)
return itOutbound
}))
b, _ := batch.New(ctx, batch.WithConcurrencyNum[any](10))
checked := make(map[string]bool)
result = make(map[string]uint16)
var resultAccess sync.Mutex
for _, detour := range outbounds {
tag := detour.Tag()
realTag := outbound.RealTag(detour)
if checked[realTag] {
continue
}
checked[realTag] = true
p, loaded := server.router.Outbound(realTag)
if !loaded {
continue
}
b.Go(realTag, func() (any, error) {
t, err := urltest.URLTest(ctx, url, p)
if err != nil {
server.logger.Debug("outbound ", tag, " unavailable: ", err)
server.urlTestHistory.DeleteURLTestHistory(realTag)
} else {
server.logger.Debug("outbound ", tag, " available: ", t, "ms")
server.urlTestHistory.StoreURLTestHistory(realTag, &urltest.History{
Time: time.Now(),
Delay: t,
})
resultAccess.Lock()
result[tag] = t
resultAccess.Unlock()
}
return nil, nil
})
}
b.Wait()
}
if err != nil {
render.Status(r, http.StatusGatewayTimeout)
render.JSON(w, r, newError(err.Error()))
return
}
render.JSON(w, r, result)
}
}

View File

@@ -3,21 +3,28 @@ package clashapi
import (
"net/http"
"github.com/sagernet/sing-box/adapter"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func cacheRouter() http.Handler {
func cacheRouter(router adapter.Router) http.Handler {
r := chi.NewRouter()
r.Post("/fakeip/flush", flushFakeip)
r.Post("/fakeip/flush", flushFakeip(router))
return r
}
func flushFakeip(w http.ResponseWriter, r *http.Request) {
/*if err := cachefile.Cache().FlushFakeip(); err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, newError(err.Error()))
return
}*/
render.NoContent(w, r)
func flushFakeip(router adapter.Router) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if cacheFile := router.ClashServer().CacheFile(); cacheFile != nil {
err := cacheFile.FakeIPReset()
if err != nil {
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, newError(err.Error()))
return
}
}
render.NoContent(w, r)
}
}

View File

@@ -0,0 +1,77 @@
package cachefile
import (
"net/netip"
"os"
"github.com/sagernet/sing-box/adapter"
"go.etcd.io/bbolt"
)
var (
bucketFakeIP = []byte("fakeip")
keyMetadata = []byte("metadata")
)
func (c *CacheFile) FakeIPMetadata() *adapter.FakeIPMetadata {
var metadata adapter.FakeIPMetadata
err := c.DB.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP)
if bucket == nil {
return nil
}
metadataBinary := bucket.Get(keyMetadata)
if len(metadataBinary) == 0 {
return os.ErrInvalid
}
return metadata.UnmarshalBinary(metadataBinary)
})
if err != nil {
return nil
}
return &metadata
}
func (c *CacheFile) FakeIPSaveMetadata(metadata *adapter.FakeIPMetadata) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
if err != nil {
return err
}
metadataBinary, err := metadata.MarshalBinary()
if err != nil {
return err
}
return bucket.Put(keyMetadata, metadataBinary)
})
}
func (c *CacheFile) FakeIPStore(address netip.Addr, domain string) error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
bucket, err := tx.CreateBucketIfNotExists(bucketFakeIP)
if err != nil {
return err
}
return bucket.Put(address.AsSlice(), []byte(domain))
})
}
func (c *CacheFile) FakeIPLoad(address netip.Addr) (string, bool) {
var domain string
_ = c.DB.View(func(tx *bbolt.Tx) error {
bucket := tx.Bucket(bucketFakeIP)
if bucket == nil {
return nil
}
domain = string(bucket.Get(address.AsSlice()))
return nil
})
return domain, domain != ""
}
func (c *CacheFile) FakeIPReset() error {
return c.DB.Batch(func(tx *bbolt.Tx) error {
return tx.DeleteBucket(bucketFakeIP)
})
}

View File

@@ -6,6 +6,7 @@ import (
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -214,6 +215,9 @@ func getProxyDelay(server *Server) func(w http.ResponseWriter, r *http.Request)
return func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
url := query.Get("url")
if strings.HasPrefix(url, "http://") {
url = ""
}
timeout, err := strconv.ParseInt(query.Get("timeout"), 10, 16)
if err != nil {
render.Status(r, http.StatusBadRequest)

View File

@@ -44,8 +44,13 @@ type Server struct {
urlTestHistory *urltest.HistoryStorage
mode string
storeSelected bool
storeFakeIP bool
cacheFilePath string
cacheFile adapter.ClashCacheFile
externalUI string
externalUIDownloadURL string
externalUIDownloadDetour string
}
func NewServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
@@ -58,15 +63,18 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
Addr: options.ExternalController,
Handler: chiRouter,
},
trafficManager: trafficManager,
urlTestHistory: urltest.NewHistoryStorage(),
mode: strings.ToLower(options.DefaultMode),
trafficManager: trafficManager,
urlTestHistory: urltest.NewHistoryStorage(),
mode: strings.ToLower(options.DefaultMode),
storeSelected: options.StoreSelected,
storeFakeIP: options.StoreFakeIP,
externalUIDownloadURL: options.ExternalUIDownloadURL,
externalUIDownloadDetour: options.ExternalUIDownloadDetour,
}
if server.mode == "" {
server.mode = "rule"
}
if options.StoreSelected {
server.storeSelected = true
if options.StoreSelected || options.StoreFakeIP {
cachePath := os.ExpandEnv(options.CacheFile)
if cachePath == "" {
cachePath = "cache.db"
@@ -99,12 +107,15 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
r.Mount("/providers/rules", ruleProviderRouter())
r.Mount("/script", scriptRouter())
r.Mount("/profile", profileRouter())
r.Mount("/cache", cacheRouter())
r.Mount("/cache", cacheRouter(router))
r.Mount("/dns", dnsRouter(router))
server.setupMetaAPI(r)
})
if options.ExternalUI != "" {
server.externalUI = C.BasePath(os.ExpandEnv(options.ExternalUI))
chiRouter.Group(func(r chi.Router) {
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(C.BasePath(os.ExpandEnv(options.ExternalUI)))))
fs := http.StripPrefix("/ui", http.FileServer(http.Dir(server.externalUI)))
r.Get("/ui", http.RedirectHandler("/ui/", http.StatusTemporaryRedirect).ServeHTTP)
r.Get("/ui/*", func(w http.ResponseWriter, r *http.Request) {
fs.ServeHTTP(w, r)
@@ -126,6 +137,7 @@ func (s *Server) PreStart() error {
}
func (s *Server) Start() error {
s.checkAndDownloadExternalUI()
listener, err := net.Listen("tcp", s.httpServer.Addr)
if err != nil {
return E.Cause(err, "external controller listen error")
@@ -156,6 +168,10 @@ func (s *Server) StoreSelected() bool {
return s.storeSelected
}
func (s *Server) StoreFakeIP() bool {
return s.storeFakeIP
}
func (s *Server) CacheFile() adapter.ClashCacheFile {
return s.cacheFile
}
@@ -392,5 +408,5 @@ func getLogs(logFactory log.ObservableFactory) func(w http.ResponseWriter, r *ht
}
func version(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true})
render.JSON(w, r, render.M{"version": "sing-box " + C.Version, "premium": true, "meta": true})
}

View File

@@ -0,0 +1,164 @@
package clashapi
import (
"archive/zip"
"context"
"io"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/sagernet/sing-box/adapter"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
func (s *Server) checkAndDownloadExternalUI() {
if s.externalUI == "" {
return
}
entries, err := os.ReadDir(s.externalUI)
if err != nil {
os.MkdirAll(s.externalUI, 0o755)
}
if len(entries) == 0 {
err = s.downloadExternalUI()
if err != nil {
s.logger.Error("download external ui error: ", err)
}
}
}
func (s *Server) downloadExternalUI() error {
var downloadURL string
if s.externalUIDownloadURL != "" {
downloadURL = s.externalUIDownloadURL
} else {
downloadURL = "https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip"
}
s.logger.Info("downloading external ui")
var detour adapter.Outbound
if s.externalUIDownloadDetour != "" {
outbound, loaded := s.router.Outbound(s.externalUIDownloadDetour)
if !loaded {
return E.New("detour outbound not found: ", s.externalUIDownloadDetour)
}
detour = outbound
} else {
detour = s.router.DefaultOutbound(N.NetworkTCP)
}
httpClient := &http.Client{
Transport: &http.Transport{
ForceAttemptHTTP2: true,
TLSHandshakeTimeout: 5 * time.Second,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
},
},
}
defer httpClient.CloseIdleConnections()
response, err := httpClient.Get(downloadURL)
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return E.New("download external ui failed: ", response.Status)
}
err = s.downloadZIP(filepath.Base(downloadURL), response.Body, s.externalUI)
if err != nil {
removeAllInDirectory(s.externalUI)
}
return err
}
func (s *Server) downloadZIP(name string, body io.Reader, output string) error {
tempFile, err := C.CreateTemp(name)
if err != nil {
return err
}
defer os.Remove(tempFile.Name())
_, err = io.Copy(tempFile, body)
tempFile.Close()
if err != nil {
return err
}
reader, err := zip.OpenReader(tempFile.Name())
if err != nil {
return err
}
defer reader.Close()
trimDir := zipIsInSingleDirectory(reader.File)
for _, file := range reader.File {
if file.FileInfo().IsDir() {
continue
}
pathElements := strings.Split(file.Name, "/")
if trimDir {
pathElements = pathElements[1:]
}
saveDirectory := output
if len(pathElements) > 1 {
saveDirectory = filepath.Join(saveDirectory, filepath.Join(pathElements[:len(pathElements)-1]...))
}
err = os.MkdirAll(saveDirectory, 0o755)
if err != nil {
return err
}
savePath := filepath.Join(saveDirectory, pathElements[len(pathElements)-1])
err = downloadZIPEntry(file, savePath)
if err != nil {
return err
}
}
return nil
}
func downloadZIPEntry(zipFile *zip.File, savePath string) error {
saveFile, err := os.Create(savePath)
if err != nil {
return err
}
defer saveFile.Close()
reader, err := zipFile.Open()
if err != nil {
return err
}
defer reader.Close()
return common.Error(io.Copy(saveFile, reader))
}
func removeAllInDirectory(directory string) {
dirEntries, err := os.ReadDir(directory)
if err != nil {
return
}
for _, dirEntry := range dirEntries {
os.RemoveAll(filepath.Join(directory, dirEntry.Name()))
}
}
func zipIsInSingleDirectory(files []*zip.File) bool {
var singleDirectory string
for _, file := range files {
if file.FileInfo().IsDir() {
continue
}
pathElements := strings.Split(file.Name, "/")
if len(pathElements) == 0 {
return false
}
if singleDirectory == "" {
singleDirectory = pathElements[0]
} else if singleDirectory != pathElements[0] {
return false
}
}
return true
}

View File

@@ -1,6 +1,7 @@
package trafficontrol
import (
"runtime"
"time"
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
@@ -18,12 +19,15 @@ type Manager struct {
connections compatible.Map[string, tracker]
ticker *time.Ticker
done chan struct{}
// process *process.Process
memory uint64
}
func NewManager() *Manager {
manager := &Manager{
ticker: time.NewTicker(time.Second),
done: make(chan struct{}),
// process: &process.Process{Pid: int32(os.Getpid())},
}
go manager.handle()
return manager
@@ -58,10 +62,18 @@ func (m *Manager) Snapshot() *Snapshot {
return true
})
//if memoryInfo, err := m.process.MemoryInfo(); err == nil {
// m.memory = memoryInfo.RSS
//} else {
var memStats runtime.MemStats
runtime.ReadMemStats(&memStats)
m.memory = memStats.StackInuse + memStats.HeapInuse + memStats.HeapIdle - memStats.HeapReleased
return &Snapshot{
UploadTotal: m.uploadTotal.Load(),
DownloadTotal: m.downloadTotal.Load(),
Connections: connections,
Memory: m.memory,
}
}
@@ -100,4 +112,5 @@ type Snapshot struct {
DownloadTotal int64 `json:"downloadTotal"`
UploadTotal int64 `json:"uploadTotal"`
Connections []tracker `json:"connections"`
Memory uint64 `json:"memory"`
}

View File

@@ -29,3 +29,19 @@ func (i *iterator[T]) Next() T {
func (i *iterator[T]) HasNext() bool {
return len(i.values) > 0
}
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
}

View File

@@ -0,0 +1,183 @@
package libbox
import (
"context"
"net"
"net/netip"
"sync"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/x/list"
)
var (
_ tun.DefaultInterfaceMonitor = (*platformDefaultInterfaceMonitor)(nil)
_ InterfaceUpdateListener = (*platformDefaultInterfaceMonitor)(nil)
)
type platformDefaultInterfaceMonitor struct {
*platformInterfaceWrapper
errorHandler E.Handler
networkAddresses []networkAddress
defaultInterfaceName string
defaultInterfaceIndex int
element *list.Element[tun.NetworkUpdateCallback]
access sync.Mutex
callbacks list.List[tun.DefaultInterfaceUpdateCallback]
}
type networkAddress struct {
interfaceName string
interfaceIndex int
addresses []netip.Prefix
}
func (m *platformDefaultInterfaceMonitor) Start() error {
return m.iif.StartDefaultInterfaceMonitor(m)
}
func (m *platformDefaultInterfaceMonitor) Close() error {
return m.iif.CloseDefaultInterfaceMonitor(m)
}
func (m *platformDefaultInterfaceMonitor) DefaultInterfaceName(destination netip.Addr) string {
for _, address := range m.networkAddresses {
for _, prefix := range address.addresses {
if prefix.Contains(destination) {
return address.interfaceName
}
}
}
return m.defaultInterfaceName
}
func (m *platformDefaultInterfaceMonitor) DefaultInterfaceIndex(destination netip.Addr) int {
for _, address := range m.networkAddresses {
for _, prefix := range address.addresses {
if prefix.Contains(destination) {
return address.interfaceIndex
}
}
}
return m.defaultInterfaceIndex
}
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.access.Lock()
defer m.access.Unlock()
return m.callbacks.PushBack(callback)
}
func (m *platformDefaultInterfaceMonitor) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) {
m.access.Lock()
defer m.access.Unlock()
m.callbacks.Remove(element)
}
func (m *platformDefaultInterfaceMonitor) UpdateDefaultInterface(interfaceName string, interfaceIndex32 int32) {
var err error
if m.iif.UsePlatformInterfaceGetter() {
err = m.updateInterfacesPlatform()
} else {
err = m.updateInterfaces()
}
if err == nil {
err = m.router.UpdateInterfaces()
}
if err != nil {
m.errorHandler.NewError(context.Background(), E.Cause(err, "update interfaces"))
}
interfaceIndex := int(interfaceIndex32)
if interfaceName == "" {
for _, netIf := range m.networkAddresses {
if netIf.interfaceIndex == interfaceIndex {
interfaceName = netIf.interfaceName
break
}
}
} else if interfaceIndex == -1 {
for _, netIf := range m.networkAddresses {
if netIf.interfaceName == interfaceName {
interfaceIndex = netIf.interfaceIndex
break
}
}
}
if interfaceName == "" {
m.errorHandler.NewError(context.Background(), E.New("invalid interface name for ", interfaceIndex))
return
} else if interfaceIndex == -1 {
m.errorHandler.NewError(context.Background(), E.New("invalid interface index for ", interfaceName))
return
}
if m.defaultInterfaceName == interfaceName && m.defaultInterfaceIndex == interfaceIndex {
return
}
m.defaultInterfaceName = interfaceName
m.defaultInterfaceIndex = interfaceIndex
m.access.Lock()
callbacks := m.callbacks.Array()
m.access.Unlock()
for _, callback := range callbacks {
err = callback(tun.EventInterfaceUpdate)
if err != nil {
m.errorHandler.NewError(context.Background(), err)
}
}
}
func (m *platformDefaultInterfaceMonitor) updateInterfaces() error {
interfaces, err := net.Interfaces()
if err != nil {
return err
}
var addresses []networkAddress
for _, iif := range interfaces {
var netAddresses []net.Addr
netAddresses, err = iif.Addrs()
if err != nil {
return err
}
var address networkAddress
address.interfaceName = iif.Name
address.interfaceIndex = iif.Index
address.addresses = common.Map(common.FilterIsInstance(netAddresses, func(it net.Addr) (*net.IPNet, bool) {
value, loaded := it.(*net.IPNet)
return value, loaded
}), func(it *net.IPNet) netip.Prefix {
bits, _ := it.Mask.Size()
return netip.PrefixFrom(M.AddrFromIP(it.IP), bits)
})
addresses = append(addresses, address)
}
m.networkAddresses = addresses
return nil
}
func (m *platformDefaultInterfaceMonitor) updateInterfacesPlatform() error {
interfaces, err := m.Interfaces()
if err != nil {
return err
}
var addresses []networkAddress
for _, iif := range interfaces {
var address networkAddress
address.interfaceName = iif.Name
address.interfaceIndex = iif.Index
// address.addresses = common.Map(iif.Addresses, netip.MustParsePrefix)
addresses = append(addresses, address)
}
m.networkAddresses = addresses
return nil
}

View File

@@ -1,6 +1,8 @@
package libbox
import "github.com/sagernet/sing-box/option"
import (
"github.com/sagernet/sing-box/option"
)
type PlatformInterface interface {
AutoDetectInterfaceControl(fd int32) error
@@ -10,6 +12,11 @@ type PlatformInterface interface {
FindConnectionOwner(ipProtocol int32, sourceAddress string, sourcePort int32, destinationAddress string, destinationPort int32) (int32, error)
PackageNameByUid(uid int32) (string, error)
UIDByPackageName(packageName string) (int32, error)
UsePlatformDefaultInterfaceMonitor() bool
StartDefaultInterfaceMonitor(listener InterfaceUpdateListener) error
CloseDefaultInterfaceMonitor(listener InterfaceUpdateListener) error
UsePlatformInterfaceGetter() bool
GetInterfaces() (NetworkInterfaceIterator, error)
}
type TunInterface interface {
@@ -17,8 +24,19 @@ type TunInterface interface {
Close() error
}
type OnDemandRuleIterator interface {
Next() OnDemandRule
type InterfaceUpdateListener interface {
UpdateDefaultInterface(interfaceName string, interfaceIndex int32)
}
type NetworkInterface struct {
Index int32
MTU int32
Name string
Addresses StringIterator
}
type NetworkInterfaceIterator interface {
Next() *NetworkInterface
HasNext() bool
}
@@ -31,6 +49,11 @@ type OnDemandRule interface {
ProbeURL() string
}
type OnDemandRuleIterator interface {
Next() OnDemandRule
HasNext() bool
}
type onDemandRule struct {
option.OnDemandRule
}

View File

@@ -1,17 +1,33 @@
package platform
import (
"context"
"io"
"net/netip"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/process"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
)
type Interface interface {
Initialize(ctx context.Context, router adapter.Router) error
AutoDetectInterfaceControl() control.Func
OpenTun(options tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
UsePlatformDefaultInterfaceMonitor() bool
CreateDefaultInterfaceMonitor(errorHandler E.Handler) tun.DefaultInterfaceMonitor
UsePlatformInterfaceGetter() bool
Interfaces() ([]NetworkInterface, error)
process.Searcher
io.Writer
}
type NetworkInterface struct {
Index int
MTU int
Name string
Addresses []netip.Prefix
}

View File

@@ -6,11 +6,13 @@ import (
"syscall"
"github.com/sagernet/sing-box"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/process"
"github.com/sagernet/sing-box/experimental/libbox/internal/procfs"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
@@ -31,7 +33,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
instance, err := box.New(box.Options{
Context: ctx,
Options: options,
PlatformInterface: &platformInterfaceWrapper{platformInterface, platformInterface.UseProcFS()},
PlatformInterface: &platformInterfaceWrapper{iif: platformInterface, useProcFS: platformInterface.UseProcFS()},
})
if err != nil {
cancel()
@@ -58,6 +60,12 @@ var _ platform.Interface = (*platformInterfaceWrapper)(nil)
type platformInterfaceWrapper struct {
iif PlatformInterface
useProcFS bool
router adapter.Router
}
func (w *platformInterfaceWrapper) Initialize(ctx context.Context, router adapter.Router) error {
w.router = router
return nil
}
func (w *platformInterfaceWrapper) AutoDetectInterfaceControl() control.Func {
@@ -68,7 +76,7 @@ func (w *platformInterfaceWrapper) AutoDetectInterfaceControl() control.Func {
}
}
func (w *platformInterfaceWrapper) OpenTun(options tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
func (w *platformInterfaceWrapper) OpenTun(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error) {
if len(options.IncludeUID) > 0 || len(options.ExcludeUID) > 0 {
return nil, E.New("android: unsupported uid options")
}
@@ -79,12 +87,16 @@ func (w *platformInterfaceWrapper) OpenTun(options tun.Options, platformOptions
if err != nil {
return nil, err
}
options.Name, err = getTunnelName(tunFd)
if err != nil {
return nil, E.Cause(err, "query tun name")
}
dupFd, err := dup(int(tunFd))
if err != nil {
return nil, E.Cause(err, "dup tun file descriptor")
}
options.FileDescriptor = dupFd
return tun.New(options)
return tun.New(*options)
}
func (w *platformInterfaceWrapper) Write(p []byte) (n int, err error) {
@@ -118,3 +130,36 @@ func (w *platformInterfaceWrapper) FindProcessInfo(ctx context.Context, network
packageName, _ := w.iif.PackageNameByUid(uid)
return &process.Info{UserId: uid, PackageName: packageName}, nil
}
func (w *platformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool {
return w.iif.UsePlatformDefaultInterfaceMonitor()
}
func (w *platformInterfaceWrapper) CreateDefaultInterfaceMonitor(errorHandler E.Handler) tun.DefaultInterfaceMonitor {
return &platformDefaultInterfaceMonitor{
platformInterfaceWrapper: w,
errorHandler: errorHandler,
defaultInterfaceIndex: -1,
}
}
func (w *platformInterfaceWrapper) UsePlatformInterfaceGetter() bool {
return w.iif.UsePlatformInterfaceGetter()
}
func (w *platformInterfaceWrapper) Interfaces() ([]platform.NetworkInterface, error) {
interfaceIterator, err := w.iif.GetInterfaces()
if err != nil {
return nil, err
}
var interfaces []platform.NetworkInterface
for _, netInterface := range iteratorToArray[*NetworkInterface](interfaceIterator) {
interfaces = append(interfaces, platform.NetworkInterface{
Index: int(netInterface.Index),
MTU: int(netInterface.MTU),
Name: netInterface.Name,
Addresses: common.Map(iteratorToArray[string](netInterface.Addresses), netip.MustParsePrefix),
})
}
return interfaces, nil
}

View File

@@ -10,6 +10,10 @@ func SetBasePath(path string) {
C.SetBasePath(path)
}
func SetTempPath(path string) {
C.SetTempPath(path)
}
func Version() string {
return C.Version
}

View File

@@ -59,7 +59,7 @@ func mapRoutePrefix(prefixes []netip.Prefix) RoutePrefixIterator {
var _ TunOptions = (*tunOptions)(nil)
type tunOptions struct {
tun.Options
*tun.Options
option.TunPlatformOptions
}

View 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 */
)
}

View 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
}

View File

@@ -0,0 +1,9 @@
//go:build !(darwin || linux)
package libbox
import "os"
func getTunnelName(fd int32) (string, error) {
return "", os.ErrInvalid
}

13
go.mod
View File

@@ -12,7 +12,6 @@ require (
github.com/go-chi/chi/v5 v5.0.8
github.com/go-chi/cors v1.2.1
github.com/go-chi/render v1.0.2
github.com/gofrs/uuid v4.4.0+incompatible
github.com/gofrs/uuid/v5 v5.0.0
github.com/hashicorp/yamux v0.1.1
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16
@@ -26,12 +25,12 @@ require (
github.com/sagernet/gomobile v0.0.0-20230413023804-244d7ff07035
github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.2.3
github.com/sagernet/sing-dns v0.1.5-0.20230415085626-111ecf799dfc
github.com/sagernet/sing-shadowsocks v0.2.0
github.com/sagernet/sing-shadowtls v0.1.0
github.com/sagernet/sing-tun v0.1.4-0.20230326080954-8848c0e4cbab
github.com/sagernet/sing-vmess v0.1.3
github.com/sagernet/sing v0.2.4-0.20230418095640-3b5e6c1812d3
github.com/sagernet/sing-dns v0.1.5-0.20230418025317-8a132998b322
github.com/sagernet/sing-shadowsocks v0.2.2-0.20230418025154-6114beeeba6d
github.com/sagernet/sing-shadowtls v0.1.2-0.20230417103049-4f682e05f19b
github.com/sagernet/sing-tun v0.1.4-0.20230419061614-d744d03d9302
github.com/sagernet/sing-vmess v0.1.5-0.20230417103030-8c3070ae3fb3
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37
github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9
github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2

26
go.sum
View File

@@ -33,8 +33,6 @@ github.com/go-chi/render v1.0.2 h1:4ER/udB0+fMWB2Jlf15RV3F4A2FDuYi/9f+lFttR/Lg=
github.com/go-chi/render v1.0.2/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
@@ -113,18 +111,18 @@ github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byL
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
github.com/sagernet/sing v0.2.3 h1:V50MvZ4c3Iij2lYFWPlzL1PyipwSzjGeN9x+Ox89vpk=
github.com/sagernet/sing v0.2.3/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w=
github.com/sagernet/sing-dns v0.1.5-0.20230415085626-111ecf799dfc h1:hmbuqKv48SAjiKPoqtJGvS5pEHVPZjTHq9CPwQY2cZ4=
github.com/sagernet/sing-dns v0.1.5-0.20230415085626-111ecf799dfc/go.mod h1:ZKuuqgsHRxDahYrzgSgy4vIAGGuKPlIf4hLcNzYzLkY=
github.com/sagernet/sing-shadowsocks v0.2.0 h1:ILDWL7pwWfkPLEbviE/MyCgfjaBmJY/JVVY+5jhSb58=
github.com/sagernet/sing-shadowsocks v0.2.0/go.mod h1:ysYzszRLpNzJSorvlWRMuzU6Vchsp7sd52q+JNY4axw=
github.com/sagernet/sing-shadowtls v0.1.0 h1:05MYce8aR5xfKIn+y7xRFsdKhKt44QZTSEQW+lG5IWQ=
github.com/sagernet/sing-shadowtls v0.1.0/go.mod h1:Kn1VUIprdkwCgkS6SXYaLmIpKzQbqBIKJBMY+RvBhYc=
github.com/sagernet/sing-tun v0.1.4-0.20230326080954-8848c0e4cbab h1:a9oeWuPBuIZ70qMhIIH6RrYhp886xN9jJIwsuu4ZFUo=
github.com/sagernet/sing-tun v0.1.4-0.20230326080954-8848c0e4cbab/go.mod h1:4YxIDEkkCjGXDOTMPw1SXpLmCQUFAWuaQN250oo+928=
github.com/sagernet/sing-vmess v0.1.3 h1:q/+tsF46dvvapL6CpQBgPHJ6nQrDUZqEtLHCbsjO7iM=
github.com/sagernet/sing-vmess v0.1.3/go.mod h1:GVXqAHwe9U21uS+Voh4YBIrADQyE4F9v0ayGSixSQAE=
github.com/sagernet/sing v0.2.4-0.20230418095640-3b5e6c1812d3 h1:dkH6SEs3yZlUjSXUAn64LUlFAfAgJqiThaWiBKWJ0Q0=
github.com/sagernet/sing v0.2.4-0.20230418095640-3b5e6c1812d3/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w=
github.com/sagernet/sing-dns v0.1.5-0.20230418025317-8a132998b322 h1:UDSeJZ2xB3dj1lySnM5LpF48dGlphGstw2BqtkJwcZI=
github.com/sagernet/sing-dns v0.1.5-0.20230418025317-8a132998b322/go.mod h1:2wjxSr1Gbecq9A0ESA9cnR399tQTcpCZEOGytekb+qI=
github.com/sagernet/sing-shadowsocks v0.2.2-0.20230418025154-6114beeeba6d h1:UUxtLujzp5jmtOXqXpSOGvHwHSZcBveKVDzRJ4GlnFU=
github.com/sagernet/sing-shadowsocks v0.2.2-0.20230418025154-6114beeeba6d/go.mod h1:Co3PJXcaZoLwHGBfT0rbSnn9C7ywc41zVYWtDeoeI/Q=
github.com/sagernet/sing-shadowtls v0.1.2-0.20230417103049-4f682e05f19b h1:ouW/6IDCrxkBe19YSbdCd7buHix7b+UZ6BM4Zz74XF4=
github.com/sagernet/sing-shadowtls v0.1.2-0.20230417103049-4f682e05f19b/go.mod h1:oG8bPerYI6cZ74KquY3DvA7ynECyrILPBnce6wtBqeI=
github.com/sagernet/sing-tun v0.1.4-0.20230419061614-d744d03d9302 h1:aPb0T2HQRTG2t7fEwLvFLZSXmhmnBh+SMs2NufhmrsI=
github.com/sagernet/sing-tun v0.1.4-0.20230419061614-d744d03d9302/go.mod h1:bvcVzlf9q9dgxt8qKluW+zOXCFoN1+SpBG3sHTq8/9Q=
github.com/sagernet/sing-vmess v0.1.5-0.20230417103030-8c3070ae3fb3 h1:BHOnxrbC929JonuKqFdJ7ZbDp7zs4oTlH5KFvKtWu9U=
github.com/sagernet/sing-vmess v0.1.5-0.20230417103030-8c3070ae3fb3/go.mod h1:yKrAr+dqZd64DxBXCHWrYicp+n4qbqO73mtwv3dck8U=
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as=
github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37/go.mod h1:3skNSftZDJWTGVtVaM2jfbce8qHnmH/AGDRe62iNOg0=
github.com/sagernet/tfo-go v0.0.0-20230303015439-ffcfd8c41cf9 h1:2ItpW1nMNkPzmBTxV0/eClCklHrFSQMnUGcpUmJxVeE=

View File

@@ -53,7 +53,7 @@ func NewDirect(ctx context.Context, router adapter.Router, logger log.ContextLog
} else {
udpTimeout = int64(C.UDPTimeout.Seconds())
}
inbound.udpNat = udpnat.New[netip.AddrPort](udpTimeout, adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
inbound.udpNat = udpnat.New[netip.AddrPort](ctx, udpTimeout, adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
inbound.connHandler = inbound
inbound.packetHandler = inbound
inbound.packetUpstream = inbound.udpNat

View File

@@ -90,6 +90,9 @@ func (n *Naive) Start() error {
n.httpServer = &http.Server{
Handler: n,
TLSConfig: tlsConfig,
BaseContext: func(listener net.Listener) context.Context {
return n.ctx
},
}
go func() {
var sErr error
@@ -606,6 +609,10 @@ func (c *naiveH2Conn) SetWriteDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *naiveH2Conn) NeedAdditionalReadDeadline() bool {
return true
}
func (c *naiveH2Conn) UpstreamReader() any {
return c.reader
}

View File

@@ -64,11 +64,11 @@ func newShadowsocks(ctx context.Context, router adapter.Router, logger log.Conte
var err error
switch {
case options.Method == shadowsocks.MethodNone:
inbound.service = shadowsocks.NewNoneService(options.UDPTimeout, inbound.upstreamContextHandler())
inbound.service = shadowsocks.NewNoneService(ctx, options.UDPTimeout, inbound.upstreamContextHandler())
case common.Contains(shadowaead.List, options.Method):
inbound.service, err = shadowaead.NewService(options.Method, nil, options.Password, udpTimeout, inbound.upstreamContextHandler())
inbound.service, err = shadowaead.NewService(ctx, options.Method, nil, options.Password, udpTimeout, inbound.upstreamContextHandler())
case common.Contains(shadowaead_2022.List, options.Method):
inbound.service, err = shadowaead_2022.NewServiceWithPassword(options.Method, options.Password, udpTimeout, inbound.upstreamContextHandler(), router.TimeFunc())
inbound.service, err = shadowaead_2022.NewServiceWithPassword(ctx, options.Method, options.Password, udpTimeout, inbound.upstreamContextHandler(), router.TimeFunc())
default:
err = E.New("unsupported method: ", options.Method)
}

View File

@@ -9,6 +9,8 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-shadowsocks"
"github.com/sagernet/sing-shadowsocks/shadowaead"
"github.com/sagernet/sing-shadowsocks/shadowaead_2022"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/auth"
@@ -25,7 +27,7 @@ var (
type ShadowsocksMulti struct {
myInboundAdapter
service *shadowaead_2022.MultiService[int]
service shadowsocks.MultiService[int]
users []option.ShadowsocksUser
}
@@ -49,16 +51,28 @@ func newShadowsocksMulti(ctx context.Context, router adapter.Router, logger log.
} else {
udpTimeout = int64(C.UDPTimeout.Seconds())
}
if !common.Contains(shadowaead_2022.List, options.Method) {
var (
service shadowsocks.MultiService[int]
err error
)
if common.Contains(shadowaead_2022.List, options.Method) {
service, err = shadowaead_2022.NewMultiServiceWithPassword[int](
ctx,
options.Method,
options.Password,
udpTimeout,
adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound),
router.TimeFunc(),
)
} else if common.Contains(shadowaead.List, options.Method) {
service, err = shadowaead.NewMultiService[int](
ctx,
options.Method,
udpTimeout,
adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound))
} else {
return nil, E.New("unsupported method: " + options.Method)
}
service, err := shadowaead_2022.NewMultiServiceWithPassword[int](
options.Method,
options.Password,
udpTimeout,
adapter.NewUpstreamContextHandler(inbound.newConnection, inbound.newPacketConnection, inbound),
router.TimeFunc(),
)
if err != nil {
return nil, err
}

View File

@@ -50,6 +50,7 @@ func newShadowsocksRelay(ctx context.Context, router adapter.Router, logger log.
udpTimeout = int64(C.UDPTimeout.Seconds())
}
service, err := shadowaead_2022.NewRelayServiceWithPassword[int](
ctx,
options.Method,
options.Password,
udpTimeout,

View File

@@ -45,7 +45,7 @@ func NewTProxy(ctx context.Context, router adapter.Router, logger log.ContextLog
}
tproxy.connHandler = tproxy
tproxy.oobPacketHandler = tproxy
tproxy.udpNat = udpnat.New[netip.AddrPort](udpTimeout, tproxy.upstreamContextHandler())
tproxy.udpNat = udpnat.New[netip.AddrPort](ctx, udpTimeout, tproxy.upstreamContextHandler())
tproxy.packetUpstream = tproxy.udpNat
return tproxy
}

View File

@@ -19,7 +19,10 @@ import (
"github.com/sagernet/sing/common/ranges"
)
var _ adapter.Inbound = (*Tun)(nil)
var (
_ adapter.Inbound = (*Tun)(nil)
_ tun.Router = (*Tun)(nil)
)
type Tun struct {
tag string
@@ -38,10 +41,6 @@ type Tun struct {
}
func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunInboundOptions, platformInterface platform.Interface) (*Tun, error) {
tunName := options.InterfaceName
if tunName == "" {
tunName = tun.CalculateInterfaceName("")
}
tunMTU := options.MTU
if tunMTU == 0 {
tunMTU = 9000
@@ -75,7 +74,7 @@ func NewTun(ctx context.Context, router adapter.Router, logger log.ContextLogger
logger: logger,
inboundOptions: options.InboundOptions,
tunOptions: tun.Options{
Name: tunName,
Name: options.InterfaceName,
MTU: tunMTU,
Inet4Address: common.Map(options.Inet4Address, option.ListenPrefix.Build),
Inet6Address: common.Map(options.Inet6Address, option.ListenPrefix.Build),
@@ -141,21 +140,31 @@ func (t *Tun) Tag() string {
func (t *Tun) Start() error {
if C.IsAndroid && t.platformInterface == nil {
t.logger.Trace("building android rules")
t.tunOptions.BuildAndroidRules(t.router.PackageManager(), t)
}
if t.tunOptions.Name == "" {
t.tunOptions.Name = tun.CalculateInterfaceName("")
}
var (
tunInterface tun.Tun
err error
)
t.logger.Trace("opening interface")
if t.platformInterface != nil {
tunInterface, err = t.platformInterface.OpenTun(t.tunOptions, t.platformOptions)
tunInterface, err = t.platformInterface.OpenTun(&t.tunOptions, t.platformOptions)
} else {
tunInterface, err = tun.New(t.tunOptions)
}
if err != nil {
return E.Cause(err, "configure tun interface")
}
t.logger.Trace("creating stack")
t.tunIf = tunInterface
var tunRouter tun.Router
if len(t.router.IPRules()) > 0 {
tunRouter = t
}
t.tunStack, err = tun.NewStack(t.stack, tun.StackOptions{
Context: t.ctx,
Tun: tunInterface,
@@ -165,6 +174,7 @@ func (t *Tun) Start() error {
Inet6Address: t.tunOptions.Inet6Address,
EndpointIndependentNat: t.endpointIndependentNat,
UDPTimeout: t.udpTimeout,
Router: tunRouter,
Handler: t,
Logger: t.logger,
UnderPlatform: t.platformInterface != nil,
@@ -172,6 +182,7 @@ func (t *Tun) Start() error {
if err != nil {
return err
}
t.logger.Trace("starting stack")
err = t.tunStack.Start()
if err != nil {
return err
@@ -187,6 +198,21 @@ func (t *Tun) Close() error {
)
}
func (t *Tun) RouteConnection(session tun.RouteSession, conn tun.RouteContext) tun.RouteAction {
ctx := log.ContextWithNewID(t.ctx)
var metadata adapter.InboundContext
metadata.Inbound = t.tag
metadata.InboundType = C.TypeTun
metadata.IPVersion = session.IPVersion
metadata.Network = tun.NetworkName(session.Network)
metadata.Source = M.SocksaddrFromNetIP(session.Source)
metadata.Destination = M.SocksaddrFromNetIP(session.Destination)
metadata.InboundOptions = t.inboundOptions
t.logger.DebugContext(ctx, "incoming connection from ", metadata.Source)
t.logger.DebugContext(ctx, "incoming connection to ", metadata.Destination)
return t.router.RouteIPConnection(ctx, conn, metadata)
}
func (t *Tun) NewConnection(ctx context.Context, conn net.Conn, upstreamMetadata M.Metadata) error {
ctx = log.ContextWithNewID(ctx)
var metadata adapter.InboundContext

View File

@@ -48,12 +48,14 @@ nav:
- configuration/dns/index.md
- DNS Server: configuration/dns/server.md
- DNS Rule: configuration/dns/rule.md
- FakeIP: configuration/dns/fakeip.md
- NTP:
- configuration/ntp/index.md
- Route:
- configuration/route/index.md
- GeoIP: configuration/route/geoip.md
- Geosite: configuration/route/geosite.md
- IP Route Rule: configuration/route/ip-rule.md
- Route Rule: configuration/route/rule.md
- Protocol Sniff: configuration/route/sniff.md
- Experimental:
@@ -102,6 +104,7 @@ nav:
- URLTest: configuration/outbound/urltest.md
- FAQ:
- faq/index.md
- FakeIP: faq/fakeip.md
- Known Issues: faq/known-issues.md
- Examples:
- examples/index.md
@@ -111,6 +114,7 @@ nav:
- Shadowsocks: examples/shadowsocks.md
- ShadowTLS: examples/shadowtls.md
- Clash API: examples/clash-api.md
- WireGuard Direct: examples/wireguard-direct.md
- Contributing:
- contributing/index.md
- Developing:
@@ -169,6 +173,7 @@ plugins:
DNS Rule: DNS 规则
Route: 路由
IP Route Rule: IP 路由规则
Route Rule: 路由规则
Protocol Sniff: 协议探测

View File

@@ -1,12 +1,15 @@
package option
type ClashAPIOptions struct {
ExternalController string `json:"external_controller,omitempty"`
ExternalUI string `json:"external_ui,omitempty"`
Secret string `json:"secret,omitempty"`
DefaultMode string `json:"default_mode,omitempty"`
StoreSelected bool `json:"store_selected,omitempty"`
CacheFile string `json:"cache_file,omitempty"`
ExternalController string `json:"external_controller,omitempty"`
ExternalUI string `json:"external_ui,omitempty"`
ExternalUIDownloadURL string `json:"external_ui_download_url,omitempty"`
ExternalUIDownloadDetour string `json:"external_ui_download_detour,omitempty"`
Secret string `json:"secret,omitempty"`
DefaultMode string `json:"default_mode,omitempty"`
StoreSelected bool `json:"store_selected,omitempty"`
StoreFakeIP bool `json:"store_fakeip,omitempty"`
CacheFile string `json:"cache_file,omitempty"`
}
type SelectorOutboundOptions struct {

View File

@@ -1,27 +1,14 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type DNSOptions struct {
Servers []DNSServerOptions `json:"servers,omitempty"`
Rules []DNSRule `json:"rules,omitempty"`
Final string `json:"final,omitempty"`
Servers []DNSServerOptions `json:"servers,omitempty"`
Rules []DNSRule `json:"rules,omitempty"`
Final string `json:"final,omitempty"`
ReverseMapping bool `json:"reverse_mapping,omitempty"`
FakeIP *DNSFakeIPOptions `json:"fakeip,omitempty"`
DNSClientOptions
}
type DNSClientOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableExpire bool `json:"disable_expire,omitempty"`
}
type DNSServerOptions struct {
Tag string `json:"tag,omitempty"`
Address string `json:"address"`
@@ -32,96 +19,14 @@ type DNSServerOptions struct {
Detour string `json:"detour,omitempty"`
}
type _DNSRule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultDNSRule `json:"-"`
LogicalOptions LogicalDNSRule `json:"-"`
type DNSClientOptions struct {
Strategy DomainStrategy `json:"strategy,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
DisableExpire bool `json:"disable_expire,omitempty"`
}
type DNSRule _DNSRule
func (r DNSRule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_DNSRule)(r), v)
}
func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_DNSRule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_DNSRule)(r), v)
if err != nil {
return E.Cause(err, "dns route rule")
}
return nil
}
type DefaultDNSRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
QueryType Listable[DNSQueryType] `json:"query_type,omitempty"`
Network string `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
Outbound Listable[string] `json:"outbound,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
}
func (r DefaultDNSRule) IsValid() bool {
var defaultValue DefaultDNSRule
defaultValue.Invert = r.Invert
defaultValue.Server = r.Server
defaultValue.DisableCache = r.DisableCache
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalDNSRule struct {
Mode string `json:"mode"`
Rules []DefaultDNSRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
}
func (r LogicalDNSRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid)
type DNSFakeIPOptions struct {
Enabled bool `json:"enabled,omitempty"`
Inet4Range *ListenPrefix `json:"inet4_range,omitempty"`
Inet6Range *ListenPrefix `json:"inet6_range,omitempty"`
}

View File

@@ -1,17 +1,9 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type RouteOptions struct {
GeoIP *GeoIPOptions `json:"geoip,omitempty"`
Geosite *GeositeOptions `json:"geosite,omitempty"`
IPRules []IPRule `json:"ip_rules,omitempty"`
Rules []Rule `json:"rules,omitempty"`
Final string `json:"final,omitempty"`
FindProcess bool `json:"find_process,omitempty"`
@@ -32,94 +24,3 @@ type GeositeOptions struct {
DownloadURL string `json:"download_url,omitempty"`
DownloadDetour string `json:"download_detour,omitempty"`
}
type _Rule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultRule `json:"-"`
LogicalOptions LogicalRule `json:"-"`
}
type Rule _Rule
func (r Rule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_Rule)(r), v)
}
func (r *Rule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_Rule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_Rule)(r), v)
if err != nil {
return E.Cause(err, "route rule")
}
return nil
}
type DefaultRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Network string `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
GeoIP Listable[string] `json:"geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r DefaultRule) IsValid() bool {
var defaultValue DefaultRule
defaultValue.Invert = r.Invert
defaultValue.Outbound = r.Outbound
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalRule struct {
Mode string `json:"mode"`
Rules []DefaultRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r LogicalRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid)
}

101
option/rule.go Normal file
View File

@@ -0,0 +1,101 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type _Rule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultRule `json:"-"`
LogicalOptions LogicalRule `json:"-"`
}
type Rule _Rule
func (r Rule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_Rule)(r), v)
}
func (r *Rule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_Rule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_Rule)(r), v)
if err != nil {
return E.Cause(err, "route rule")
}
return nil
}
type DefaultRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Network Listable[string] `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
GeoIP Listable[string] `json:"geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r DefaultRule) IsValid() bool {
var defaultValue DefaultRule
defaultValue.Invert = r.Invert
defaultValue.Outbound = r.Outbound
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalRule struct {
Mode string `json:"mode"`
Rules []DefaultRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r LogicalRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultRule.IsValid)
}

107
option/rule_dns.go Normal file
View File

@@ -0,0 +1,107 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type _DNSRule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultDNSRule `json:"-"`
LogicalOptions LogicalDNSRule `json:"-"`
}
type DNSRule _DNSRule
func (r DNSRule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_DNSRule)(r), v)
}
func (r *DNSRule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_DNSRule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_DNSRule)(r), v)
if err != nil {
return E.Cause(err, "dns route rule")
}
return nil
}
type DefaultDNSRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
QueryType Listable[DNSQueryType] `json:"query_type,omitempty"`
Network Listable[string] `json:"network,omitempty"`
AuthUser Listable[string] `json:"auth_user,omitempty"`
Protocol Listable[string] `json:"protocol,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
ProcessName Listable[string] `json:"process_name,omitempty"`
ProcessPath Listable[string] `json:"process_path,omitempty"`
PackageName Listable[string] `json:"package_name,omitempty"`
User Listable[string] `json:"user,omitempty"`
UserID Listable[int32] `json:"user_id,omitempty"`
Outbound Listable[string] `json:"outbound,omitempty"`
ClashMode string `json:"clash_mode,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
}
func (r DefaultDNSRule) IsValid() bool {
var defaultValue DefaultDNSRule
defaultValue.Invert = r.Invert
defaultValue.Server = r.Server
defaultValue.DisableCache = r.DisableCache
defaultValue.RewriteTTL = r.RewriteTTL
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalDNSRule struct {
Mode string `json:"mode"`
Rules []DefaultDNSRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Server string `json:"server,omitempty"`
DisableCache bool `json:"disable_cache,omitempty"`
RewriteTTL *uint32 `json:"rewrite_ttl,omitempty"`
}
func (r LogicalDNSRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultDNSRule.IsValid)
}

120
option/rule_ip.go Normal file
View File

@@ -0,0 +1,120 @@
package option
import (
"reflect"
"github.com/sagernet/sing-box/common/json"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
)
type _IPRule struct {
Type string `json:"type,omitempty"`
DefaultOptions DefaultIPRule `json:"-"`
LogicalOptions LogicalIPRule `json:"-"`
}
type IPRule _IPRule
func (r IPRule) MarshalJSON() ([]byte, error) {
var v any
switch r.Type {
case C.RuleTypeDefault:
r.Type = ""
v = r.DefaultOptions
case C.RuleTypeLogical:
v = r.LogicalOptions
default:
return nil, E.New("unknown rule type: " + r.Type)
}
return MarshallObjects((_IPRule)(r), v)
}
func (r *IPRule) UnmarshalJSON(bytes []byte) error {
err := json.Unmarshal(bytes, (*_IPRule)(r))
if err != nil {
return err
}
var v any
switch r.Type {
case "", C.RuleTypeDefault:
r.Type = C.RuleTypeDefault
v = &r.DefaultOptions
case C.RuleTypeLogical:
v = &r.LogicalOptions
default:
return E.New("unknown rule type: " + r.Type)
}
err = UnmarshallExcluded(bytes, (*_IPRule)(r), v)
if err != nil {
return E.Cause(err, "ip route rule")
}
return nil
}
type DefaultIPRule struct {
Inbound Listable[string] `json:"inbound,omitempty"`
IPVersion int `json:"ip_version,omitempty"`
Network Listable[string] `json:"network,omitempty"`
Domain Listable[string] `json:"domain,omitempty"`
DomainSuffix Listable[string] `json:"domain_suffix,omitempty"`
DomainKeyword Listable[string] `json:"domain_keyword,omitempty"`
DomainRegex Listable[string] `json:"domain_regex,omitempty"`
Geosite Listable[string] `json:"geosite,omitempty"`
SourceGeoIP Listable[string] `json:"source_geoip,omitempty"`
GeoIP Listable[string] `json:"geoip,omitempty"`
SourceIPCIDR Listable[string] `json:"source_ip_cidr,omitempty"`
IPCIDR Listable[string] `json:"ip_cidr,omitempty"`
SourcePort Listable[uint16] `json:"source_port,omitempty"`
SourcePortRange Listable[string] `json:"source_port_range,omitempty"`
Port Listable[uint16] `json:"port,omitempty"`
PortRange Listable[string] `json:"port_range,omitempty"`
Invert bool `json:"invert,omitempty"`
Action RouteAction `json:"action,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
type RouteAction tun.ActionType
func (a RouteAction) MarshalJSON() ([]byte, error) {
typeName, err := tun.ActionTypeName(tun.ActionType(a))
if err != nil {
return nil, err
}
return json.Marshal(typeName)
}
func (a *RouteAction) UnmarshalJSON(bytes []byte) error {
var value string
err := json.Unmarshal(bytes, &value)
if err != nil {
return err
}
actionType, err := tun.ParseActionType(value)
if err != nil {
return err
}
*a = RouteAction(actionType)
return nil
}
func (r DefaultIPRule) IsValid() bool {
var defaultValue DefaultIPRule
defaultValue.Invert = r.Invert
defaultValue.Outbound = r.Outbound
return !reflect.DeepEqual(r, defaultValue)
}
type LogicalIPRule struct {
Mode string `json:"mode"`
Rules []DefaultIPRule `json:"rules,omitempty"`
Invert bool `json:"invert,omitempty"`
Action RouteAction `json:"action,omitempty"`
Outbound string `json:"outbound,omitempty"`
}
func (r LogicalIPRule) IsValid() bool {
return len(r.Rules) > 0 && common.All(r.Rules, DefaultIPRule.IsValid)
}

View File

@@ -4,7 +4,7 @@ type ShadowsocksInboundOptions struct {
ListenOptions
Network NetworkList `json:"network,omitempty"`
Method string `json:"method"`
Password string `json:"password"`
Password string `json:"password,omitempty"`
Users []ShadowsocksUser `json:"users,omitempty"`
Destinations []ShadowsocksDestination `json:"destinations,omitempty"`
}

View File

@@ -27,7 +27,9 @@ type SocksOutboundOptions struct {
type HTTPOutboundOptions struct {
DialerOptions
ServerOptions
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
TLS *OutboundTLSOptions `json:"tls,omitempty"`
Username string `json:"username,omitempty"`
Password string `json:"password,omitempty"`
TLS *OutboundTLSOptions `json:"tls,omitempty"`
Path string `json:"path,omitempty"`
Headers map[string]Listable[string] `json:"headers,omitempty"`
}

View File

@@ -2,15 +2,25 @@ package option
type WireGuardOutboundOptions struct {
DialerOptions
ServerOptions
SystemInterface bool `json:"system_interface,omitempty"`
InterfaceName string `json:"interface_name,omitempty"`
LocalAddress Listable[ListenPrefix] `json:"local_address"`
PrivateKey string `json:"private_key"`
PeerPublicKey string `json:"peer_public_key"`
PreSharedKey string `json:"pre_shared_key,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
Workers int `json:"workers,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
Network NetworkList `json:"network,omitempty"`
Peers []WireGuardPeer `json:"peers,omitempty"`
ServerOptions
PeerPublicKey string `json:"peer_public_key"`
PreSharedKey string `json:"pre_shared_key,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
Workers int `json:"workers,omitempty"`
MTU uint32 `json:"mtu,omitempty"`
Network NetworkList `json:"network,omitempty"`
IPRewrite bool `json:"ip_rewrite,omitempty"`
}
type WireGuardPeer struct {
ServerOptions
PublicKey string `json:"public_key,omitempty"`
PreSharedKey string `json:"pre_shared_key,omitempty"`
AllowedIPs Listable[string] `json:"allowed_ips,omitempty"`
Reserved []uint8 `json:"reserved,omitempty"`
}

View File

@@ -54,7 +54,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, t
case C.TypeSelector:
return NewSelector(router, logger, tag, options.SelectorOptions)
case C.TypeURLTest:
return NewURLTest(router, logger, tag, options.URLTestOptions)
return NewURLTest(ctx, router, logger, tag, options.URLTestOptions)
default:
return nil, E.New("unknown outbound type: ", options.Type)
}

View File

@@ -39,6 +39,10 @@ func (a *myOutboundAdapter) Network() []string {
return a.network
}
func (a *myOutboundAdapter) NewError(ctx context.Context, err error) {
NewError(a.logger, ctx, err)
}
func NewConnection(ctx context.Context, this N.Dialer, conn net.Conn, metadata adapter.InboundContext) error {
ctx = adapter.WithContext(ctx, &metadata)
var outConn net.Conn
@@ -121,3 +125,12 @@ func CopyEarlyConn(ctx context.Context, conn net.Conn, serverConn net.Conn) erro
}
return bufio.CopyConn(ctx, conn, serverConn)
}
func NewError(logger log.ContextLogger, ctx context.Context, err error) {
common.Close(err)
if E.IsClosedOrCanceled(err) {
logger.DebugContext(ctx, "connection closed: ", err)
return
}
logger.ErrorContext(ctx, err)
}

Some files were not shown because too many files have changed in this diff Show More