mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-16 13:44:41 +10:00
Compare commits
5 Commits
v1.9.2
...
v1.10.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae47deab58 | ||
|
|
c0341118ec | ||
|
|
116492cb46 | ||
|
|
2d8d9a1959 | ||
|
|
5735227174 |
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
EOF
|
||||
echo "HOME=$HOME" >> "$GITHUB_ENV"
|
||||
- name: Publish release
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
uses: goreleaser/goreleaser-action@v5
|
||||
with:
|
||||
distribution: goreleaser-pro
|
||||
version: latest
|
||||
|
||||
@@ -93,6 +93,7 @@ type DNSRule interface {
|
||||
|
||||
type RuleSet interface {
|
||||
StartContext(ctx context.Context, startContext RuleSetStartContext) error
|
||||
PostStart() error
|
||||
Metadata() RuleSetMetadata
|
||||
Close() error
|
||||
HeadlessRule
|
||||
|
||||
Submodule clients/android updated: 8622e6a5bc...e20fa632f6
@@ -16,40 +16,30 @@ import (
|
||||
)
|
||||
|
||||
type LinuxSystemProxy struct {
|
||||
hasGSettings bool
|
||||
kWriteConfigCmd string
|
||||
sudoUser string
|
||||
serverAddr M.Socksaddr
|
||||
supportSOCKS bool
|
||||
isEnabled bool
|
||||
hasGSettings bool
|
||||
hasKWriteConfig5 bool
|
||||
sudoUser string
|
||||
serverAddr M.Socksaddr
|
||||
supportSOCKS bool
|
||||
isEnabled bool
|
||||
}
|
||||
|
||||
func NewSystemProxy(ctx context.Context, serverAddr M.Socksaddr, supportSOCKS bool) (*LinuxSystemProxy, error) {
|
||||
hasGSettings := common.Error(exec.LookPath("gsettings")) == nil
|
||||
kWriteConfigCmds := []string{
|
||||
"kwriteconfig5",
|
||||
"kwriteconfig6",
|
||||
}
|
||||
var kWriteConfigCmd string
|
||||
for _, cmd := range kWriteConfigCmds {
|
||||
if common.Error(exec.LookPath(cmd)) == nil {
|
||||
kWriteConfigCmd = cmd
|
||||
break
|
||||
}
|
||||
}
|
||||
hasKWriteConfig5 := common.Error(exec.LookPath("kwriteconfig5")) == nil
|
||||
var sudoUser string
|
||||
if os.Getuid() == 0 {
|
||||
sudoUser = os.Getenv("SUDO_USER")
|
||||
}
|
||||
if !hasGSettings && kWriteConfigCmd == "" {
|
||||
if !hasGSettings && !hasKWriteConfig5 {
|
||||
return nil, E.New("unsupported desktop environment")
|
||||
}
|
||||
return &LinuxSystemProxy{
|
||||
hasGSettings: hasGSettings,
|
||||
kWriteConfigCmd: kWriteConfigCmd,
|
||||
sudoUser: sudoUser,
|
||||
serverAddr: serverAddr,
|
||||
supportSOCKS: supportSOCKS,
|
||||
hasGSettings: hasGSettings,
|
||||
hasKWriteConfig5: hasKWriteConfig5,
|
||||
sudoUser: sudoUser,
|
||||
serverAddr: serverAddr,
|
||||
supportSOCKS: supportSOCKS,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -80,8 +70,8 @@ func (p *LinuxSystemProxy) Enable() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if p.kWriteConfigCmd != "" {
|
||||
err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1")
|
||||
if p.hasKWriteConfig5 {
|
||||
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "1")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -93,7 +83,7 @@ func (p *LinuxSystemProxy) Enable() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0")
|
||||
err = p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "Authmode", "0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,8 +103,8 @@ func (p *LinuxSystemProxy) Disable() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if p.kWriteConfigCmd != "" {
|
||||
err := p.runAsUser(p.kWriteConfigCmd, "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0")
|
||||
if p.hasKWriteConfig5 {
|
||||
err := p.runAsUser("kwriteconfig5", "--file", "kioslaverc", "--group", "Proxy Settings", "--key", "ProxyType", "0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -160,7 +150,7 @@ func (p *LinuxSystemProxy) setKDEProxy(proxyTypes ...string) error {
|
||||
proxyUrl = "http://" + p.serverAddr.String()
|
||||
}
|
||||
err := p.runAsUser(
|
||||
p.kWriteConfigCmd,
|
||||
"kwriteconfig5",
|
||||
"--file",
|
||||
"kioslaverc",
|
||||
"--group",
|
||||
|
||||
113
common/sniff/bittorrent.go
Normal file
113
common/sniff/bittorrent.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package sniff
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
)
|
||||
|
||||
const (
|
||||
trackerConnectFlag = iota
|
||||
trackerAnnounceFlag
|
||||
trackerScrapeFlag
|
||||
|
||||
trackerProtocolID = 0x41727101980
|
||||
|
||||
trackerConnectMinSize = 16
|
||||
trackerAnnounceMinSize = 20
|
||||
trackerScrapeMinSize = 8
|
||||
)
|
||||
|
||||
// BitTorrent detects if the stream is a BitTorrent connection.
|
||||
// For the BitTorrent protocol specification, see https://www.bittorrent.org/beps/bep_0003.html
|
||||
func BitTorrent(_ context.Context, reader io.Reader) (*adapter.InboundContext, error) {
|
||||
var first byte
|
||||
err := binary.Read(reader, binary.BigEndian, &first)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if first != 19 {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
var protocol [19]byte
|
||||
_, err = reader.Read(protocol[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if string(protocol[:]) != "BitTorrent protocol" {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
return &adapter.InboundContext{
|
||||
Protocol: C.ProtocolBitTorrent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UTP detects if the packet is a uTP connection packet.
|
||||
// For the uTP protocol specification, see
|
||||
// 1. https://www.bittorrent.org/beps/bep_0029.html
|
||||
// 2. https://github.com/bittorrent/libutp/blob/2b364cbb0650bdab64a5de2abb4518f9f228ec44/utp_internal.cpp#L112
|
||||
func UTP(_ context.Context, packet []byte) (*adapter.InboundContext, error) {
|
||||
// A valid uTP packet must be at least 20 bytes long.
|
||||
if len(packet) < 20 {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
version := packet[0] & 0x0F
|
||||
ty := packet[0] >> 4
|
||||
if version != 1 || ty > 4 {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
// Validate the extensions
|
||||
extension := packet[1]
|
||||
reader := bytes.NewReader(packet[20:])
|
||||
for extension != 0 {
|
||||
err := binary.Read(reader, binary.BigEndian, &extension)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var length byte
|
||||
err = binary.Read(reader, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err = reader.Seek(int64(length), io.SeekCurrent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &adapter.InboundContext{
|
||||
Protocol: C.ProtocolBitTorrent,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UDPTracker detects if the packet is a UDP Tracker Protocol packet.
|
||||
// For the UDP Tracker Protocol specification, see https://www.bittorrent.org/beps/bep_0015.html
|
||||
func UDPTracker(_ context.Context, packet []byte) (*adapter.InboundContext, error) {
|
||||
switch {
|
||||
case len(packet) >= trackerConnectMinSize &&
|
||||
binary.BigEndian.Uint64(packet[:8]) == trackerProtocolID &&
|
||||
binary.BigEndian.Uint32(packet[8:12]) == trackerConnectFlag:
|
||||
fallthrough
|
||||
case len(packet) >= trackerAnnounceMinSize &&
|
||||
binary.BigEndian.Uint32(packet[8:12]) == trackerAnnounceFlag:
|
||||
fallthrough
|
||||
case len(packet) >= trackerScrapeMinSize &&
|
||||
binary.BigEndian.Uint32(packet[8:12]) == trackerScrapeFlag:
|
||||
return &adapter.InboundContext{
|
||||
Protocol: C.ProtocolBitTorrent,
|
||||
}, nil
|
||||
default:
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
}
|
||||
81
common/sniff/bittorrent_test.go
Normal file
81
common/sniff/bittorrent_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package sniff_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/sagernet/sing-box/common/sniff"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSniffBittorrent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
packets := []string{
|
||||
"13426974546f7272656e742070726f746f636f6c0000000000100000e21ea9569b69bab33c97851d0298bdfa89bc90922d5554313631302dea812fcd6a3563e3be40c1d1",
|
||||
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452333030302d653369733079647675763638",
|
||||
"13426974546f7272656e742070726f746f636f6c00000000001000052aa4f5a7e209e54b32803d43670971c4c8caaa052d5452343035302d6f7a316c6e79377931716130",
|
||||
}
|
||||
|
||||
for _, pkt := range packets {
|
||||
pkt, err := hex.DecodeString(pkt)
|
||||
require.NoError(t, err)
|
||||
metadata, err := sniff.BitTorrent(context.TODO(), bytes.NewReader(pkt))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSniffUTP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
packets := []string{
|
||||
"010041a282d7ee7b583afb160004000006d8318da776968f92d666f7963f32dae23ba0d2c810d8b8209cc4939f54fde9eeaa521c2c20c9ba7f43f4fb0375f28de06643b5e3ca4685ab7ac76adca99783be72ef05ed59ef4234f5712b75b4c7c0d7bee8fe2ca20ad626ba5bb0ffcc16bf06790896f888048cf72716419a07db1a3dca4550fbcea75b53e97235168a221cf3e553dfbb723961bd719fab038d86e0ecb74747f5a2cd669de1c4b9ad375f3a492d09d98cdfad745435625401315bbba98d35d32086299801377b93495a63a9efddb8d05f5b37a5c5b1c0a25e917f12007bb5e05013ada8aff544fab8cadf61d80ddb0b60f12741e44515a109d144fd53ef845acb4b5ccf0d6fc302d7003d76df3fc3423bb0237301c9e88f900c2d392a8e0fdb36d143cf7527a93fd0a2638b746e72f6699fffcd4fd15348fce780d4caa04382fd9faf1ca0ae377ca805da7536662b84f5ee18dd3ae38fcb095a7543e55f9069ae92c8cf54ae44e97b558d35e2545c66601ed2149cbc32bd6df199a2be7cf0da8b2ff137e0d23e776bc87248425013876d3a3cc31a83b424b752bd0346437f24b532978005d8f5b1b0be1a37a2489c32a18a9ad3118e3f9d30eb299bffae18e1f0677c2a5c185e62519093fe6bc2b7339299ea50a587989f726ca6443a75dd5bb936f6367c6355d80fae53ff529d740b2e5576e3eefdf1fdbfc69c3c8d8ac750512635de63e054bee1d3b689bc1b2bc3d2601e42a00b5c89066d173d4ae7ffedfd2274e5cf6d868fbe640aedb69b8246142f00b32d459974287537ddd5373460dcbc92f5cfdd7a3ed6020822ae922d947893752ca1983d0d32977374c384ac8f5ab566859019b7351526b9f13e932037a55bb052d9deb3b3c23317e0784fdc51a64f2159bfea3b069cf5caf02ee2c3c1a6b6b427bb16165713e8802d95b5c8ed77953690e994bd38c9ae113fedaf6ee7fc2b96c032ceafc2a530ad0422e84546b9c6ad8ef6ea02fa508abddd1805c38a7b42e9b7c971b1b636865ebec06ed754bb404cd6b4e6cc8cb77bd4a0c43410d5cd5ef8fe853a66d49b3b9e06cb141236cdbfdd5761601dc54d1250b86c660e0f898fe62526fdd9acf0eab60a3bbbb2151970461f28f10b31689594bea646c4b03ee197d63bdef4e5a7c22716b3bb9494a83b78ecd81b338b80ac6c09c43485b1b09ba41c74343832c78f0520c1d659ac9eb1502094141e82fb9e5e620970ebc0655514c43c294a7714cbf9a499d277daf089f556398a01589a77494bec8bfb60a108f3813b55368672b88c1af40f6b3c8b513f7c70c3e0efce85228b8b9ec67ba0393f9f7305024d8e2da6a26cf85613d14f249170ce1000089df4c9c260df7f8292aa2ecb5d5bac97656d59aa248caedea2d198e51ce87baece338716d114b458de02d65c9ff808ca5b5b73723b4d1e962d9ac2d98176544dc9984cf8554d07820ef3dd0861cfe57b478328046380de589adad94ee44743ffac73bb7361feca5d56f07cf8ce75080e261282ae30350d7882679b15cab9e7e53ddf93310b33f7390ae5d318bb53f387e6af5d0ef4f947fc9cb8e7e38b52c7f8d772ece6156b38d88796ea19df02c53723b44df7c76315a0de9462f27287e682d2b4cda1a68fe00d7e48c51ee981be44e1ca940fb5190c12655edb4a83c3a4f33e48a015692df4f0b3d61656e362aca657b5ae8c12db5a0db3db1e45135ee918b66918f40e53c4f83e9da0cddfe63f736ae751ab3837a30ae3220d8e8e311487093a7b90c7e7e40dd54ca750e19452f9193aa892aa6a6229ab493dadae988b1724f7898ee69c36d3eb7364c4adbeca811cfe2065873e78c2b6dfdf1595f7a7831c07e03cda82e4f86f76438dfb2b07c13638ce7b509cfa71b88b5102b39a203b423202088e1c2103319cb32c13c1e546ff8612fa194c95a7808ab767c265a1bd5fa0efed5c8ec1701876a00ec8",
|
||||
"01001ecb68176f215d04326300100000dbcf30292d14b54e9ee2d115ee5b8ebc7fad3e882d4fcdd0c14c6b917c11cb4c6a9f410b52a33ae97c2ac77c7a2b122b8955e09af3c5c595f1b2e79ca57cfe44c44e069610773b9bc9ba223d7f6b383e3adddd03fb88a8476028e30979c2ef321ffc97c5c132bcf9ac5b410bbb5ec6cefca3c7209202a14c5ae922b6b157b0a80249d13ffe5b996af0bc8e54ba576d148372494303e7ead0602b05b9c8fc97d48508a028a04d63a1fd28b0edfcd5c51715f63188b53eefede98a76912dca98518551a8856567307a56a702cbfcc115ea0c755b418bc2c7b57721239b82f09fb24328a4b0ce0f109bcb2a64e04b8aadb1f8487585425acdf8fc4ec8ea93cfcec5ac098bb29d42ddef6e46b03f34a5de28316726699b7cb5195c33e5c48abe87d591d63f9991c84c30819d186d6e0e95fd83c8dff07aa669c4430989bcaccfeacb9bcadbdb4d8f1964dbeb9687745656edd30b21c66cc0a1d742a78717d134a19a7f02d285a4973b1a198c00cfdff4676608dc4f3e817e3463c3b4e2c80d3e8d4fbac541a58a2fb7ad6939f607f8144eff6c8b0adc28ee5609ea158987519892fb",
|
||||
"21001ecb6817f2805d044fd700100000dbd03029",
|
||||
"410277ef0b1fb1f60000000000040000c233000000080000000000000000",
|
||||
}
|
||||
|
||||
for _, pkt := range packets {
|
||||
pkt, err := hex.DecodeString(pkt)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata, err := sniff.UTP(context.TODO(), pkt)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSniffUDPTracker(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
connectPackets := []string{
|
||||
// connect packets
|
||||
"00000417271019800000000078e90560",
|
||||
"00000417271019800000000022c5d64d",
|
||||
"000004172710198000000000b3863541",
|
||||
|
||||
// announce packets
|
||||
"3d7592ead4b8c9e300000001b871a3820000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365",
|
||||
"3d7592ead4b8c9e30000000188deed1c0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365",
|
||||
"3d7592ead4b8c9e300000001ceb948ad0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a3362cdb7020ff920e5aa642c3d4066950dd1f01f4d00000000000000000000000000000000000000000000000000000000000000000000000000000000000002092f616e6e6f756e6365",
|
||||
|
||||
// scrape packets
|
||||
"3d7592ead4b8c9e300000002d2f4bba5a94a8fe5ccb19ba61c4c0873d391e987982fbbd3",
|
||||
"3d7592ead4b8c9e300000002441243292aae6c35c94fcfb415dbe95f408b9ce91ee846ed",
|
||||
"3d7592ead4b8c9e300000002b2aa461b1ad1fa9661cf3fe45fb2504ad52ec6c67758e294",
|
||||
}
|
||||
|
||||
for _, pkt := range connectPackets {
|
||||
pkt, err := hex.DecodeString(pkt)
|
||||
require.NoError(t, err)
|
||||
|
||||
metadata, err := sniff.UDPTracker(context.TODO(), pkt)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, C.ProtocolBitTorrent, metadata.Protocol)
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
ProtocolTLS = "tls"
|
||||
ProtocolHTTP = "http"
|
||||
ProtocolQUIC = "quic"
|
||||
ProtocolDNS = "dns"
|
||||
ProtocolSTUN = "stun"
|
||||
ProtocolTLS = "tls"
|
||||
ProtocolHTTP = "http"
|
||||
ProtocolQUIC = "quic"
|
||||
ProtocolDNS = "dns"
|
||||
ProtocolSTUN = "stun"
|
||||
ProtocolBitTorrent = "bittorrent"
|
||||
)
|
||||
|
||||
@@ -2,13 +2,25 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
### 1.9.2
|
||||
#### 1.10.0-alpha.1
|
||||
|
||||
* Fixes and improvements
|
||||
* Add tailing comma support in JSON configuration
|
||||
* Add simple auto redirect for Android **1**
|
||||
* Add BitTorrent sniffer **2**
|
||||
|
||||
### 1.9.1
|
||||
**1**:
|
||||
|
||||
* Fixes and improvements
|
||||
It allows you to use redirect inbound in the sing-box Android client
|
||||
and automatically configures IPv4 TCP redirection via su.
|
||||
|
||||
This may alleviate the symptoms of some OCD patients who think that
|
||||
redirect can effectively save power compared to the system HTTP Proxy.
|
||||
|
||||
See [Redirect](/configuration/inbound/redirect/).
|
||||
|
||||
**2**:
|
||||
|
||||
See [Protocol Sniff](/configuration/route/sniff/).
|
||||
|
||||
### 1.9.0
|
||||
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.9.0"
|
||||
|
||||
:material-plus: [auto_redirect](#auto_redirect)
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux and macOS.
|
||||
@@ -9,6 +17,11 @@
|
||||
"type": "redirect",
|
||||
"tag": "redirect-in",
|
||||
|
||||
"auto_redirect": {
|
||||
"enabled": false,
|
||||
"continue_on_no_permission": false
|
||||
},
|
||||
|
||||
... // Listen Fields
|
||||
}
|
||||
```
|
||||
@@ -16,3 +29,23 @@
|
||||
### Listen Fields
|
||||
|
||||
See [Listen Fields](/configuration/shared/listen/) for details.
|
||||
|
||||
### Fields
|
||||
|
||||
#### `auto_redirect`
|
||||
|
||||
!!! question "Since sing-box 1.10.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Android.
|
||||
|
||||
Automatically add iptables nat rules to hijack **IPv4 TCP** connections.
|
||||
|
||||
It is expected to run with the Android graphical client (it will attempt to su at runtime).
|
||||
|
||||
#### `auto_redirect.continue_on_no_permission`
|
||||
|
||||
!!! question "Since sing-box 1.10.0"
|
||||
|
||||
Ignore errors when the Android device is not rooted or is denied root access.
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.10.0 中的更改"
|
||||
|
||||
:material-plus: [auto_redirect](#auto_redirect)
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux 和 macOS。
|
||||
@@ -9,9 +17,35 @@
|
||||
"type": "redirect",
|
||||
"tag": "redirect-in",
|
||||
|
||||
"auto_redirect": {
|
||||
"enabled": false,
|
||||
"continue_on_no_permission": false
|
||||
},
|
||||
|
||||
... // 监听字段
|
||||
}
|
||||
```
|
||||
|
||||
### 监听字段
|
||||
|
||||
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||
|
||||
### 字段
|
||||
|
||||
#### `auto_redirect`
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Android。
|
||||
|
||||
自动添加 iptables nat 规则以劫持 **IPv4 TCP** 连接。
|
||||
|
||||
它预计与 Android 图形客户端一起运行(将在运行时尝试 su)。
|
||||
|
||||
#### `auto_redirect.continue_on_no_permission`
|
||||
|
||||
!!! question "自 sing-box 1.10.0 起"
|
||||
|
||||
当 Android 设备未获得 root 权限或 root 访问权限被拒绝时,忽略错误。
|
||||
|
||||
@@ -2,10 +2,11 @@ If enabled in the inbound, the protocol and domain name (if present) of by the c
|
||||
|
||||
#### Supported Protocols
|
||||
|
||||
| Network | Protocol | Domain Name |
|
||||
|:-------:|:--------:|:-----------:|
|
||||
| TCP | HTTP | Host |
|
||||
| TCP | TLS | Server Name |
|
||||
| UDP | QUIC | Server Name |
|
||||
| UDP | STUN | / |
|
||||
| TCP/UDP | DNS | / |
|
||||
| Network | Protocol | Domain Name |
|
||||
|:-------:|:-----------:|:-----------:|
|
||||
| TCP | HTTP | Host |
|
||||
| TCP | TLS | Server Name |
|
||||
| UDP | QUIC | Server Name |
|
||||
| UDP | STUN | / |
|
||||
| TCP/UDP | DNS | / |
|
||||
| TCP/UDP | BitTorrent | / |
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
#### 支持的协议
|
||||
|
||||
| 网络 | 协议 | 域名 |
|
||||
|:-------:|:----:|:-----------:|
|
||||
| TCP | HTTP | Host |
|
||||
| TCP | TLS | Server Name |
|
||||
| UDP | QUIC | Server Name |
|
||||
| UDP | STUN | / |
|
||||
| TCP/UDP | DNS | / |
|
||||
| 网络 | 协议 | 域名 |
|
||||
|:-------:|:-----------:|:-----------:|
|
||||
| TCP | HTTP | Host |
|
||||
| TCP | TLS | Server Name |
|
||||
| UDP | QUIC | Server Name |
|
||||
| UDP | STUN | / |
|
||||
| TCP/UDP | DNS | / |
|
||||
| TCP/UDP | BitTorrent | / |
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"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/platform"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
@@ -97,6 +98,14 @@ func (s *platformInterfaceStub) FindProcessInfo(ctx context.Context, network str
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) PerAppProxyList() ([]uint32, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *platformInterfaceStub) PerAppProxyMode() int32 {
|
||||
return platform.PerAppProxyModeDisabled
|
||||
}
|
||||
|
||||
type interfaceMonitorStub struct{}
|
||||
|
||||
func (s *interfaceMonitorStub) Start() error {
|
||||
|
||||
@@ -22,6 +22,8 @@ type PlatformInterface interface {
|
||||
IncludeAllNetworks() bool
|
||||
ReadWIFIState() *WIFIState
|
||||
ClearDNSCache()
|
||||
PerAppProxyList() (IntegerIterator, error)
|
||||
PerAppProxyMode() int32
|
||||
}
|
||||
|
||||
type TunInterface interface {
|
||||
@@ -54,6 +56,11 @@ type NetworkInterfaceIterator interface {
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type IntegerIterator interface {
|
||||
Next() int32
|
||||
HasNext() bool
|
||||
}
|
||||
|
||||
type OnDemandRule interface {
|
||||
Target() int32
|
||||
DNSSearchDomainMatch() StringIterator
|
||||
|
||||
@@ -11,6 +11,12 @@ import (
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
PerAppProxyModeDisabled int32 = iota
|
||||
PerAppProxyModeExclude
|
||||
PerAppProxyModeInclude
|
||||
)
|
||||
|
||||
type Interface interface {
|
||||
Initialize(ctx context.Context, router adapter.Router) error
|
||||
UsePlatformAutoDetectInterfaceControl() bool
|
||||
@@ -24,5 +30,7 @@ type Interface interface {
|
||||
IncludeAllNetworks() bool
|
||||
ClearDNSCache()
|
||||
ReadWIFIState() adapter.WIFIState
|
||||
PerAppProxyList() ([]uint32, error)
|
||||
PerAppProxyMode() int32
|
||||
process.Searcher
|
||||
}
|
||||
|
||||
@@ -229,6 +229,18 @@ func (w *platformInterfaceWrapper) ReadWIFIState() adapter.WIFIState {
|
||||
return (adapter.WIFIState)(*wifiState)
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) PerAppProxyList() ([]uint32, error) {
|
||||
uidIterator, err := w.iif.PerAppProxyList()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return common.Map(iteratorToArray[int32](uidIterator), func(it int32) uint32 { return uint32(it) }), nil
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) PerAppProxyMode() int32 {
|
||||
return w.iif.PerAppProxyMode()
|
||||
}
|
||||
|
||||
func (w *platformInterfaceWrapper) DisableColors() bool {
|
||||
return runtime.GOOS != "android"
|
||||
}
|
||||
|
||||
@@ -16,18 +16,25 @@ func (s *BoxService) Pause() {
|
||||
if s.pauseTimer != nil {
|
||||
s.pauseTimer.Stop()
|
||||
}
|
||||
s.pauseTimer = time.AfterFunc(3*time.Second, s.ResetNetwork)
|
||||
s.pauseTimer = time.AfterFunc(time.Minute, s.pause)
|
||||
}
|
||||
|
||||
func (s *BoxService) pause() {
|
||||
s.pauseAccess.Lock()
|
||||
defer s.pauseAccess.Unlock()
|
||||
s.pauseManager.DevicePause()
|
||||
_ = s.instance.Router().ResetNetwork()
|
||||
s.pauseTimer = nil
|
||||
}
|
||||
|
||||
func (s *BoxService) Wake() {
|
||||
_ = s.instance.Router().ResetNetwork()
|
||||
s.pauseAccess.Lock()
|
||||
defer s.pauseAccess.Unlock()
|
||||
if s.pauseTimer != nil {
|
||||
s.pauseTimer.Stop()
|
||||
s.pauseTimer = nil
|
||||
return
|
||||
}
|
||||
s.pauseTimer = time.AfterFunc(3*time.Minute, s.ResetNetwork)
|
||||
}
|
||||
|
||||
func (s *BoxService) ResetNetwork() {
|
||||
_ = s.instance.Router().ResetNetwork()
|
||||
s.pauseManager.DeviceWake()
|
||||
}
|
||||
|
||||
8
go.mod
8
go.mod
@@ -26,14 +26,14 @@ require (
|
||||
github.com/sagernet/gvisor v0.0.0-20240428053021-e691de28565f
|
||||
github.com/sagernet/quic-go v0.43.1-beta.1
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||
github.com/sagernet/sing v0.4.1
|
||||
github.com/sagernet/sing-dns v0.2.0
|
||||
github.com/sagernet/sing v0.5.0-alpha.4
|
||||
github.com/sagernet/sing-dns v0.2.0-beta.18
|
||||
github.com/sagernet/sing-mux v0.2.0
|
||||
github.com/sagernet/sing-quic v0.2.0-beta.5
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.0
|
||||
github.com/sagernet/sing-shadowtls v0.1.4
|
||||
github.com/sagernet/sing-tun v0.3.2
|
||||
github.com/sagernet/sing-tun v0.3.0-beta.6
|
||||
github.com/sagernet/sing-vmess v0.1.8
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
|
||||
github.com/sagernet/tfo-go v0.0.0-20231209031829-7b5343ac1dc6
|
||||
@@ -46,7 +46,7 @@ require (
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||
golang.org/x/crypto v0.23.0
|
||||
golang.org/x/net v0.25.0
|
||||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/sys v0.20.0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
google.golang.org/grpc v1.63.2
|
||||
google.golang.org/protobuf v1.33.0
|
||||
|
||||
16
go.sum
16
go.sum
@@ -106,10 +106,10 @@ github.com/sagernet/quic-go v0.43.1-beta.1/go.mod h1:BkrQYeop7Jx3hN3TW8/76CXcdhY
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
|
||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing v0.4.1 h1:zVlpE+7k7AFoC2pv6ReqLf0PIHjihL/jsBl5k05PQFk=
|
||||
github.com/sagernet/sing v0.4.1/go.mod h1:ieZHA/+Y9YZfXs2I3WtuwgyCZ6GPsIR7HdKb1SdEnls=
|
||||
github.com/sagernet/sing-dns v0.2.0 h1:dka3weRX6+CrYO3v+hrTy2z68rCOCZXNBiNXpLZ6JNs=
|
||||
github.com/sagernet/sing-dns v0.2.0/go.mod h1:BJpJv6XLnrUbSyIntOT6DG9FW0f4fETmPAHvNjOprLg=
|
||||
github.com/sagernet/sing v0.5.0-alpha.4 h1:8ljxrEq1BMcDuGe8FxoeUGasPUXQsIywpUusGrlGkQ0=
|
||||
github.com/sagernet/sing v0.5.0-alpha.4/go.mod h1:Xh4KO9nGdvm4K/LVg9Xn9jSxJdqe9KcXbAzNC1S2qfw=
|
||||
github.com/sagernet/sing-dns v0.2.0-beta.18 h1:6vzXZThRdA7YUzBOpSbUT48XRumtl/KIpIHFSOP0za8=
|
||||
github.com/sagernet/sing-dns v0.2.0-beta.18/go.mod h1:k/dmFcQpg6+m08gC1yQBy+13+QkuLqpKr4bIreq4U24=
|
||||
github.com/sagernet/sing-mux v0.2.0 h1:4C+vd8HztJCWNYfufvgL49xaOoOHXty2+EAjnzN3IYo=
|
||||
github.com/sagernet/sing-mux v0.2.0/go.mod h1:khzr9AOPocLa+g53dBplwNDz4gdsyx/YM3swtAhlkHQ=
|
||||
github.com/sagernet/sing-quic v0.2.0-beta.5 h1:ceKFLd1iS5AtM+pScKmcDp5k7R6WgYIe8vl6nB0aVsE=
|
||||
@@ -120,8 +120,8 @@ github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wK
|
||||
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
|
||||
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
|
||||
github.com/sagernet/sing-tun v0.3.2 h1:z0bLUT/YXH9RrJS9DsIpB0Bb9afl2hVJOmHd0zA3HJY=
|
||||
github.com/sagernet/sing-tun v0.3.2/go.mod h1:DxLIyhjWU/HwGYoX0vNGg2c5QgTQIakphU1MuERR5tQ=
|
||||
github.com/sagernet/sing-tun v0.3.0-beta.6 h1:L11kMrM7UfUW0pzQiU66Fffh4o86KZc1SFGbkYi8Ma8=
|
||||
github.com/sagernet/sing-tun v0.3.0-beta.6/go.mod h1:DxLIyhjWU/HwGYoX0vNGg2c5QgTQIakphU1MuERR5tQ=
|
||||
github.com/sagernet/sing-vmess v0.1.8 h1:XVWad1RpTy9b5tPxdm5MCU8cGfrTGdR8qCq6HV2aCNc=
|
||||
github.com/sagernet/sing-vmess v0.1.8/go.mod h1:vhx32UNzTDUkNwOyIjcZQohre1CaytquC5mPplId8uA=
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
|
||||
@@ -184,8 +184,8 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
||||
@@ -19,7 +19,7 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o
|
||||
case C.TypeTun:
|
||||
return NewTun(ctx, router, logger, options.Tag, options.TunOptions, platformInterface)
|
||||
case C.TypeRedirect:
|
||||
return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions), nil
|
||||
return NewRedirect(ctx, router, logger, options.Tag, options.RedirectOptions, platformInterface)
|
||||
case C.TypeTProxy:
|
||||
return NewTProxy(ctx, router, logger, options.Tag, options.TProxyOptions), nil
|
||||
case C.TypeDirect:
|
||||
|
||||
@@ -2,25 +2,39 @@ package inbound
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/redir"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
type Redirect struct {
|
||||
myInboundAdapter
|
||||
platformInterface platform.Interface
|
||||
autoRedirect option.AutoRedirectOptions
|
||||
needSu bool
|
||||
suPath string
|
||||
}
|
||||
|
||||
func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions) *Redirect {
|
||||
func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.RedirectInboundOptions, platformInterface platform.Interface) (*Redirect, error) {
|
||||
redirect := &Redirect{
|
||||
myInboundAdapter{
|
||||
myInboundAdapter: myInboundAdapter{
|
||||
protocol: C.TypeRedirect,
|
||||
network: []string{N.NetworkTCP},
|
||||
ctx: ctx,
|
||||
@@ -29,9 +43,28 @@ func NewRedirect(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
tag: tag,
|
||||
listenOptions: options.ListenOptions,
|
||||
},
|
||||
platformInterface: platformInterface,
|
||||
autoRedirect: common.PtrValueOrDefault(options.AutoRedirect),
|
||||
}
|
||||
if redirect.autoRedirect.Enabled {
|
||||
if !C.IsAndroid {
|
||||
return nil, E.New("auto redirect is only supported on Android")
|
||||
}
|
||||
userId := os.Getuid()
|
||||
if userId != 0 {
|
||||
suPath, err := exec.LookPath("/bin/su")
|
||||
if err == nil {
|
||||
redirect.needSu = true
|
||||
redirect.suPath = suPath
|
||||
} else if redirect.autoRedirect.ContinueOnNoPermission {
|
||||
redirect.autoRedirect.Enabled = false
|
||||
} else {
|
||||
return nil, E.Extend(E.Cause(err, "root permission is required for auto redirect"), os.Getenv("PATH"))
|
||||
}
|
||||
}
|
||||
}
|
||||
redirect.connHandler = redirect
|
||||
return redirect
|
||||
return redirect, nil
|
||||
}
|
||||
|
||||
func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||
@@ -42,3 +75,124 @@ func (r *Redirect) NewConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||
metadata.Destination = M.SocksaddrFromNetIP(destination)
|
||||
return r.newConnection(ctx, conn, metadata)
|
||||
}
|
||||
|
||||
func (r *Redirect) Start() error {
|
||||
err := r.myInboundAdapter.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if r.autoRedirect.Enabled {
|
||||
r.cleanupRedirect()
|
||||
err = r.setupRedirect()
|
||||
if err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if errors.As(err, &exitError) && exitError.ExitCode() == 13 && r.autoRedirect.ContinueOnNoPermission {
|
||||
r.logger.Error(E.Cause(err, "setup auto redirect"))
|
||||
return nil
|
||||
}
|
||||
r.cleanupRedirect()
|
||||
return E.Cause(err, "setup auto redirect")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Redirect) Close() error {
|
||||
if r.autoRedirect.Enabled {
|
||||
r.cleanupRedirect()
|
||||
}
|
||||
return r.myInboundAdapter.Close()
|
||||
}
|
||||
|
||||
func (r *Redirect) setupRedirect() error {
|
||||
tableName := "sing-box"
|
||||
rules := `
|
||||
set -e -o pipefail
|
||||
iptables -t nat -N sing-box
|
||||
`
|
||||
rules += strings.Join(common.FlatMap(r.router.(adapter.Router).InterfaceFinder().Interfaces(), func(it control.Interface) []string {
|
||||
return common.Map(common.Filter(it.Addresses, func(it netip.Prefix) bool { return it.Addr().Is4() }), func(it netip.Prefix) string {
|
||||
return "iptables -t nat -A " + tableName + " -p tcp -j RETURN -d " + it.String()
|
||||
})
|
||||
}), "\n")
|
||||
var (
|
||||
myUid = uint32(os.Getuid())
|
||||
perAppProxyList []uint32
|
||||
perAppProxyMap = make(map[uint32]bool)
|
||||
perAppProxyMode int32
|
||||
err error
|
||||
)
|
||||
if r.platformInterface != nil {
|
||||
perAppProxyMode = r.platformInterface.PerAppProxyMode()
|
||||
if perAppProxyMode != platform.PerAppProxyModeDisabled {
|
||||
perAppProxyList, err = r.platformInterface.PerAppProxyList()
|
||||
if err != nil {
|
||||
return E.Cause(err, "read per app proxy configuration")
|
||||
}
|
||||
}
|
||||
for _, proxyUID := range perAppProxyList {
|
||||
perAppProxyMap[proxyUID] = true
|
||||
}
|
||||
}
|
||||
excludeUser := func(userID uint32) {
|
||||
if perAppProxyMode != platform.PerAppProxyModeInclude {
|
||||
perAppProxyMap[userID] = false
|
||||
} else {
|
||||
delete(perAppProxyMap, userID)
|
||||
}
|
||||
}
|
||||
excludeUser(myUid)
|
||||
if myUid != 0 && myUid != 2000 {
|
||||
excludeUser(0)
|
||||
}
|
||||
perAppProxyList = perAppProxyList[:0]
|
||||
for uid := range perAppProxyMap {
|
||||
perAppProxyList = append(perAppProxyList, uid)
|
||||
}
|
||||
sort.SliceStable(perAppProxyList, func(i, j int) bool {
|
||||
return perAppProxyList[i] < perAppProxyList[j]
|
||||
})
|
||||
redirectPortStr := F.ToString(M.AddrPortFromNet(r.tcpListener.Addr()).Port())
|
||||
if perAppProxyMode != platform.PerAppProxyModeInclude {
|
||||
rules += "\n" + strings.Join(common.Map(perAppProxyList, func(it uint32) string {
|
||||
return "iptables -t nat -A " + tableName + " -j RETURN -m owner --uid-owner " + F.ToString(it)
|
||||
}), "\n")
|
||||
rules += "\niptables -t nat -A " + tableName + " -p tcp -j REDIRECT --to-ports " + redirectPortStr
|
||||
} else {
|
||||
rules += "\n" + strings.Join(common.Map(perAppProxyList, func(it uint32) string {
|
||||
return "iptables -t nat -A " + tableName + " -p tcp -j REDIRECT --to-ports " + redirectPortStr + " -m owner --uid-owner " + F.ToString(it)
|
||||
}), "\n")
|
||||
}
|
||||
rules += "\niptables -t nat -A OUTPUT -p tcp -j " + tableName
|
||||
for _, ruleLine := range strings.Split(rules, "\n") {
|
||||
ruleLine = strings.TrimSpace(ruleLine)
|
||||
if ruleLine == "" {
|
||||
continue
|
||||
}
|
||||
r.logger.Debug("# ", ruleLine)
|
||||
}
|
||||
return r.runAndroidShell(rules)
|
||||
}
|
||||
|
||||
func (r *Redirect) cleanupRedirect() {
|
||||
_ = r.runAndroidShell(`
|
||||
iptables -t nat -D OUTPUT -p tcp -j sing-box
|
||||
iptables -t nat -F sing-box
|
||||
iptables -t nat -X sing-box
|
||||
`)
|
||||
}
|
||||
|
||||
func (r *Redirect) runAndroidShell(content string) error {
|
||||
var command *exec.Cmd
|
||||
if r.needSu {
|
||||
command = exec.Command(r.suPath, "-c", "sh")
|
||||
} else {
|
||||
command = exec.Command("sh")
|
||||
}
|
||||
command.Stdin = strings.NewReader(content)
|
||||
combinedOutput, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
return E.Extend(err, string(combinedOutput))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ package option
|
||||
|
||||
type RedirectInboundOptions struct {
|
||||
ListenOptions
|
||||
AutoRedirect *AutoRedirectOptions `json:"auto_redirect,omitempty"`
|
||||
}
|
||||
|
||||
type AutoRedirectOptions struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
ContinueOnNoPermission bool `json:"continue_on_no_permission,omitempty"`
|
||||
}
|
||||
|
||||
type TProxyInboundOptions struct {
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/sagernet/sing-box/transport/wireguard"
|
||||
"github.com/sagernet/sing-dns"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"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"
|
||||
@@ -112,25 +111,6 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
|
||||
}
|
||||
|
||||
func (w *WireGuard) Start() error {
|
||||
if common.Any(w.peers, func(peer wireguard.PeerConfig) bool {
|
||||
return !peer.Endpoint.IsValid()
|
||||
}) {
|
||||
// wait for all outbounds to be started and continue in PortStart
|
||||
return nil
|
||||
}
|
||||
return w.start()
|
||||
}
|
||||
|
||||
func (w *WireGuard) PostStart() error {
|
||||
if common.All(w.peers, func(peer wireguard.PeerConfig) bool {
|
||||
return peer.Endpoint.IsValid()
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
return w.start()
|
||||
}
|
||||
|
||||
func (w *WireGuard) start() error {
|
||||
err := wireguard.ResolvePeers(w.ctx, w.router, w.peers)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
236
route/router.go
236
route/router.go
@@ -221,7 +221,7 @@ func NewRouter(
|
||||
if serverAddress == "" {
|
||||
serverAddress = server.Address
|
||||
}
|
||||
notIpAddress := !M.ParseSocksaddr(serverAddress).Addr.IsValid()
|
||||
_, notIpAddress := netip.ParseAddr(serverAddress)
|
||||
if server.AddressResolver != "" {
|
||||
if !transportTagMap[server.AddressResolver] {
|
||||
return nil, E.New("parse dns server[", tag, "]: address resolver not found: ", server.AddressResolver)
|
||||
@@ -231,7 +231,7 @@ func NewRouter(
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
} else if notIpAddress && strings.Contains(server.Address, ".") {
|
||||
} else if notIpAddress != nil && strings.Contains(server.Address, ".") {
|
||||
return nil, E.New("parse dns server[", tag, "]: missing address_resolver")
|
||||
}
|
||||
}
|
||||
@@ -352,6 +352,14 @@ func NewRouter(
|
||||
router.interfaceMonitor = interfaceMonitor
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
powerListener, err := winpowrprof.NewEventListener(router.notifyWindowsPowerEvent)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize power listener")
|
||||
}
|
||||
router.powerListener = powerListener
|
||||
}
|
||||
|
||||
if ntpOptions.Enabled {
|
||||
ntpDialer, err := dialer.New(router, ntpOptions.DialerOptions)
|
||||
if err != nil {
|
||||
@@ -394,17 +402,20 @@ func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outb
|
||||
defaultOutboundForPacketConnection = detour
|
||||
}
|
||||
}
|
||||
var index, packetIndex int
|
||||
if defaultOutboundForConnection == nil {
|
||||
for _, detour := range outbounds {
|
||||
for i, detour := range outbounds {
|
||||
if common.Contains(detour.Network(), N.NetworkTCP) {
|
||||
index = i
|
||||
defaultOutboundForConnection = detour
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if defaultOutboundForPacketConnection == nil {
|
||||
for _, detour := range outbounds {
|
||||
for i, detour := range outbounds {
|
||||
if common.Contains(detour.Network(), N.NetworkUDP) {
|
||||
packetIndex = i
|
||||
defaultOutboundForPacketConnection = detour
|
||||
break
|
||||
}
|
||||
@@ -421,6 +432,22 @@ func (r *Router) Initialize(inbounds []adapter.Inbound, outbounds []adapter.Outb
|
||||
outbounds = append(outbounds, detour)
|
||||
outboundByTag[detour.Tag()] = detour
|
||||
}
|
||||
if defaultOutboundForConnection != defaultOutboundForPacketConnection {
|
||||
var description string
|
||||
if defaultOutboundForConnection.Tag() != "" {
|
||||
description = defaultOutboundForConnection.Tag()
|
||||
} else {
|
||||
description = F.ToString(index)
|
||||
}
|
||||
var packetDescription string
|
||||
if defaultOutboundForPacketConnection.Tag() != "" {
|
||||
packetDescription = defaultOutboundForPacketConnection.Tag()
|
||||
} else {
|
||||
packetDescription = F.ToString(packetIndex)
|
||||
}
|
||||
r.logger.Info("using ", defaultOutboundForConnection.Type(), "[", description, "] as default outbound for connection")
|
||||
r.logger.Info("using ", defaultOutboundForPacketConnection.Type(), "[", packetDescription, "] as default outbound for packet connection")
|
||||
}
|
||||
r.inboundByTag = inboundByTag
|
||||
r.outbounds = outbounds
|
||||
r.defaultOutboundForConnection = defaultOutboundForConnection
|
||||
@@ -509,12 +536,75 @@ func (r *Router) Start() error {
|
||||
r.geositeReader = nil
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
powerListener, err := winpowrprof.NewEventListener(r.notifyWindowsPowerEvent)
|
||||
if err == nil {
|
||||
r.powerListener = powerListener
|
||||
if len(r.ruleSets) > 0 {
|
||||
monitor.Start("initialize rule-set")
|
||||
ruleSetStartContext := NewRuleSetStartContext()
|
||||
var ruleSetStartGroup task.Group
|
||||
for i, ruleSet := range r.ruleSets {
|
||||
ruleSetInPlace := ruleSet
|
||||
ruleSetStartGroup.Append0(func(ctx context.Context) error {
|
||||
err := ruleSetInPlace.StartContext(ctx, ruleSetStartContext)
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize rule-set[", i, "]")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ruleSetStartGroup.Concurrency(5)
|
||||
ruleSetStartGroup.FastFail()
|
||||
err := ruleSetStartGroup.Run(r.ctx)
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ruleSetStartContext.Close()
|
||||
}
|
||||
var (
|
||||
needProcessFromRuleSet bool
|
||||
needWIFIStateFromRuleSet bool
|
||||
)
|
||||
for _, ruleSet := range r.ruleSets {
|
||||
metadata := ruleSet.Metadata()
|
||||
if metadata.ContainsProcessRule {
|
||||
needProcessFromRuleSet = true
|
||||
}
|
||||
if metadata.ContainsWIFIRule {
|
||||
needWIFIStateFromRuleSet = true
|
||||
}
|
||||
}
|
||||
if needProcessFromRuleSet || r.needFindProcess || r.needPackageManager {
|
||||
if C.IsAndroid && r.platformInterface == nil {
|
||||
monitor.Start("initialize package manager")
|
||||
packageManager, err := tun.NewPackageManager(r)
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create package manager")
|
||||
}
|
||||
monitor.Start("start package manager")
|
||||
err = packageManager.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start package manager")
|
||||
}
|
||||
r.packageManager = packageManager
|
||||
}
|
||||
|
||||
if r.platformInterface != nil {
|
||||
r.processSearcher = r.platformInterface
|
||||
} else {
|
||||
r.logger.Warn("initialize power listener: ", err)
|
||||
monitor.Start("initialize process searcher")
|
||||
searcher, err := process.NewSearcher(process.Config{
|
||||
Logger: r.logger,
|
||||
PackageManager: r.packageManager,
|
||||
})
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
if err != os.ErrInvalid {
|
||||
r.logger.Warn(E.Cause(err, "create process searcher"))
|
||||
}
|
||||
} else {
|
||||
r.processSearcher = searcher
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +617,25 @@ func (r *Router) Start() error {
|
||||
}
|
||||
}
|
||||
|
||||
if (needWIFIStateFromRuleSet || r.needWIFIState) && r.platformInterface != nil {
|
||||
monitor.Start("initialize WIFI state")
|
||||
r.needWIFIState = true
|
||||
r.interfaceMonitor.RegisterCallback(func(_ int) {
|
||||
r.updateWIFIState()
|
||||
})
|
||||
r.updateWIFIState()
|
||||
monitor.Finish()
|
||||
}
|
||||
|
||||
for i, rule := range r.rules {
|
||||
monitor.Start("initialize rule[", i, "]")
|
||||
err := rule.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize rule[", i, "]")
|
||||
}
|
||||
}
|
||||
|
||||
monitor.Start("initialize DNS client")
|
||||
r.dnsClient.Start()
|
||||
monitor.Finish()
|
||||
@@ -635,93 +744,12 @@ func (r *Router) Close() error {
|
||||
}
|
||||
|
||||
func (r *Router) PostStart() error {
|
||||
monitor := taskmonitor.New(r.logger, C.StopTimeout)
|
||||
if len(r.ruleSets) > 0 {
|
||||
monitor.Start("initialize rule-set")
|
||||
ruleSetStartContext := NewRuleSetStartContext()
|
||||
var ruleSetStartGroup task.Group
|
||||
for i, ruleSet := range r.ruleSets {
|
||||
ruleSetInPlace := ruleSet
|
||||
ruleSetStartGroup.Append0(func(ctx context.Context) error {
|
||||
err := ruleSetInPlace.StartContext(ctx, ruleSetStartContext)
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize rule-set[", i, "]")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
ruleSetStartGroup.Concurrency(5)
|
||||
ruleSetStartGroup.FastFail()
|
||||
err := ruleSetStartGroup.Run(r.ctx)
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ruleSetStartContext.Close()
|
||||
}
|
||||
var (
|
||||
needProcessFromRuleSet bool
|
||||
needWIFIStateFromRuleSet bool
|
||||
)
|
||||
for _, ruleSet := range r.ruleSets {
|
||||
metadata := ruleSet.Metadata()
|
||||
if metadata.ContainsProcessRule {
|
||||
needProcessFromRuleSet = true
|
||||
}
|
||||
if metadata.ContainsWIFIRule {
|
||||
needWIFIStateFromRuleSet = true
|
||||
}
|
||||
}
|
||||
if needProcessFromRuleSet || r.needFindProcess || r.needPackageManager {
|
||||
if C.IsAndroid && r.platformInterface == nil {
|
||||
monitor.Start("initialize package manager")
|
||||
packageManager, err := tun.NewPackageManager(r)
|
||||
monitor.Finish()
|
||||
err := ruleSet.PostStart()
|
||||
if err != nil {
|
||||
return E.Cause(err, "create package manager")
|
||||
return E.Cause(err, "post start rule-set[", i, "]")
|
||||
}
|
||||
monitor.Start("start package manager")
|
||||
err = packageManager.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "start package manager")
|
||||
}
|
||||
r.packageManager = packageManager
|
||||
}
|
||||
|
||||
if r.platformInterface != nil {
|
||||
r.processSearcher = r.platformInterface
|
||||
} else {
|
||||
monitor.Start("initialize process searcher")
|
||||
searcher, err := process.NewSearcher(process.Config{
|
||||
Logger: r.logger,
|
||||
PackageManager: r.packageManager,
|
||||
})
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
if err != os.ErrInvalid {
|
||||
r.logger.Warn(E.Cause(err, "create process searcher"))
|
||||
}
|
||||
} else {
|
||||
r.processSearcher = searcher
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needWIFIStateFromRuleSet || r.needWIFIState) && r.platformInterface != nil {
|
||||
monitor.Start("initialize WIFI state")
|
||||
r.needWIFIState = true
|
||||
r.interfaceMonitor.RegisterCallback(func(_ int) {
|
||||
r.updateWIFIState()
|
||||
})
|
||||
r.updateWIFIState()
|
||||
monitor.Finish()
|
||||
}
|
||||
for i, rule := range r.rules {
|
||||
monitor.Start("initialize rule[", i, "]")
|
||||
err := rule.Start()
|
||||
monitor.Finish()
|
||||
if err != nil {
|
||||
return E.Cause(err, "initialize rule[", i, "]")
|
||||
}
|
||||
}
|
||||
r.started = true
|
||||
@@ -822,7 +850,16 @@ func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata ad
|
||||
|
||||
if metadata.InboundOptions.SniffEnabled {
|
||||
buffer := buf.NewPacket()
|
||||
sniffMetadata, err := sniff.PeekStream(ctx, conn, buffer, time.Duration(metadata.InboundOptions.SniffTimeout), sniff.StreamDomainNameQuery, sniff.TLSClientHello, sniff.HTTPHost)
|
||||
sniffMetadata, err := sniff.PeekStream(
|
||||
ctx,
|
||||
conn,
|
||||
buffer,
|
||||
time.Duration(metadata.InboundOptions.SniffTimeout),
|
||||
sniff.StreamDomainNameQuery,
|
||||
sniff.TLSClientHello,
|
||||
sniff.HTTPHost,
|
||||
sniff.BitTorrent,
|
||||
)
|
||||
if sniffMetadata != nil {
|
||||
metadata.Protocol = sniffMetadata.Protocol
|
||||
metadata.Domain = sniffMetadata.Domain
|
||||
@@ -949,7 +986,15 @@ func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, m
|
||||
metadata.Destination = destination
|
||||
}
|
||||
if metadata.InboundOptions.SniffEnabled {
|
||||
sniffMetadata, _ := sniff.PeekPacket(ctx, buffer.Bytes(), sniff.DomainNameQuery, sniff.QUICClientHello, sniff.STUNMessage)
|
||||
sniffMetadata, _ := sniff.PeekPacket(
|
||||
ctx,
|
||||
buffer.Bytes(),
|
||||
sniff.DomainNameQuery,
|
||||
sniff.QUICClientHello,
|
||||
sniff.STUNMessage,
|
||||
sniff.UTP,
|
||||
sniff.UDPTracker,
|
||||
)
|
||||
if sniffMetadata != nil {
|
||||
metadata.Protocol = sniffMetadata.Protocol
|
||||
metadata.Domain = sniffMetadata.Domain
|
||||
@@ -1093,9 +1138,6 @@ func (r *Router) AutoDetectInterfaceFunc() control.Func {
|
||||
if r.platformInterface != nil && r.platformInterface.UsePlatformAutoDetectInterfaceControl() {
|
||||
return r.platformInterface.AutoDetectInterfaceControl()
|
||||
} else {
|
||||
if r.interfaceMonitor == nil {
|
||||
return nil
|
||||
}
|
||||
return control.BindToInterfaceFunc(r.InterfaceFinder(), func(network string, address string) (interfaceName string, interfaceIndex int, err error) {
|
||||
remoteAddr := M.ParseSocksaddr(address).Addr
|
||||
if C.IsLinux {
|
||||
|
||||
@@ -78,6 +78,10 @@ func (s *LocalRuleSet) StartContext(ctx context.Context, startContext adapter.Ru
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) PostStart() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *LocalRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
@@ -112,6 +112,16 @@ func (s *RemoteRuleSet) StartContext(ctx context.Context, startContext adapter.R
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) PostStart() error {
|
||||
if s.lastUpdated.IsZero() {
|
||||
err := s.fetchOnce(s.ctx, nil)
|
||||
if err != nil {
|
||||
s.logger.Error("fetch rule-set ", s.options.Tag, ": ", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *RemoteRuleSet) Metadata() adapter.RuleSetMetadata {
|
||||
return s.metadata
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import (
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
"github.com/sagernet/wireguard-go/conn"
|
||||
)
|
||||
|
||||
@@ -21,9 +19,6 @@ var _ conn.Bind = (*ClientBind)(nil)
|
||||
|
||||
type ClientBind struct {
|
||||
ctx context.Context
|
||||
pauseManager pause.Manager
|
||||
bindCtx context.Context
|
||||
bindDone context.CancelFunc
|
||||
errorHandler E.Handler
|
||||
dialer N.Dialer
|
||||
reservedForEndpoint map[netip.AddrPort][3]uint8
|
||||
@@ -38,7 +33,6 @@ type ClientBind struct {
|
||||
func NewClientBind(ctx context.Context, errorHandler E.Handler, dialer N.Dialer, isConnect bool, connectAddr netip.AddrPort, reserved [3]uint8) *ClientBind {
|
||||
return &ClientBind{
|
||||
ctx: ctx,
|
||||
pauseManager: service.FromContext[pause.Manager](ctx),
|
||||
errorHandler: errorHandler,
|
||||
dialer: dialer,
|
||||
reservedForEndpoint: make(map[netip.AddrPort][3]uint8),
|
||||
@@ -61,11 +55,6 @@ func (c *ClientBind) connect() (*wireConn, error) {
|
||||
}
|
||||
c.connAccess.Lock()
|
||||
defer c.connAccess.Unlock()
|
||||
select {
|
||||
case <-c.done:
|
||||
return nil, net.ErrClosed
|
||||
default:
|
||||
}
|
||||
serverConn = c.conn
|
||||
if serverConn != nil {
|
||||
select {
|
||||
@@ -76,7 +65,7 @@ func (c *ClientBind) connect() (*wireConn, error) {
|
||||
}
|
||||
}
|
||||
if c.isConnect {
|
||||
udpConn, err := c.dialer.DialContext(c.bindCtx, N.NetworkUDP, M.SocksaddrFromNetIP(c.connectAddr))
|
||||
udpConn, err := c.dialer.DialContext(c.ctx, N.NetworkUDP, M.SocksaddrFromNetIP(c.connectAddr))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -85,7 +74,7 @@ func (c *ClientBind) connect() (*wireConn, error) {
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
} else {
|
||||
udpConn, err := c.dialer.ListenPacket(c.bindCtx, M.Socksaddr{Addr: netip.IPv4Unspecified()})
|
||||
udpConn, err := c.dialer.ListenPacket(c.ctx, M.Socksaddr{Addr: netip.IPv4Unspecified()})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -103,7 +92,6 @@ func (c *ClientBind) Open(port uint16) (fns []conn.ReceiveFunc, actualPort uint1
|
||||
c.done = make(chan struct{})
|
||||
default:
|
||||
}
|
||||
c.bindCtx, c.bindDone = context.WithCancel(c.ctx)
|
||||
return []conn.ReceiveFunc{c.receive}, 0, nil
|
||||
}
|
||||
|
||||
@@ -117,7 +105,6 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint)
|
||||
}
|
||||
c.errorHandler.NewError(context.Background(), E.Cause(err, "connect to server"))
|
||||
err = nil
|
||||
c.pauseManager.WaitActive()
|
||||
time.Sleep(time.Second)
|
||||
return
|
||||
}
|
||||
@@ -143,17 +130,12 @@ func (c *ClientBind) receive(packets [][]byte, sizes []int, eps []conn.Endpoint)
|
||||
}
|
||||
|
||||
func (c *ClientBind) Close() error {
|
||||
common.Close(common.PtrOrNil(c.conn))
|
||||
select {
|
||||
case <-c.done:
|
||||
default:
|
||||
close(c.done)
|
||||
}
|
||||
if c.bindDone != nil {
|
||||
c.bindDone()
|
||||
}
|
||||
c.connAccess.Lock()
|
||||
defer c.connAccess.Unlock()
|
||||
common.Close(common.PtrOrNil(c.conn))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -164,8 +146,6 @@ func (c *ClientBind) SetMark(mark uint32) error {
|
||||
func (c *ClientBind) Send(bufs [][]byte, ep conn.Endpoint) error {
|
||||
udpConn, err := c.connect()
|
||||
if err != nil {
|
||||
c.pauseManager.WaitActive()
|
||||
time.Sleep(time.Second)
|
||||
return err
|
||||
}
|
||||
destination := netip.AddrPort(ep.(Endpoint))
|
||||
|
||||
Reference in New Issue
Block a user