mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Compare commits
36 Commits
dev-next
...
v1.3-beta5
Author | SHA1 | Date | |
---|---|---|---|
![]() |
32875e7cbc | ||
![]() |
3de56620ce | ||
![]() |
3651605d3b | ||
![]() |
7d1174e545 | ||
![]() |
bd9e6e5cd9 | ||
![]() |
ed37cb858b | ||
![]() |
62bcf22c26 | ||
![]() |
84bd997742 | ||
![]() |
a548e45ad7 | ||
![]() |
5c1de2bb06 | ||
![]() |
e5f0add1ab | ||
![]() |
70e47df295 | ||
![]() |
f20642d6fd | ||
![]() |
73fa926b48 | ||
![]() |
5d9dce8078 | ||
![]() |
e20e2d57c9 | ||
![]() |
25f31890ed | ||
![]() |
194b36b987 | ||
![]() |
1e39196bc9 | ||
![]() |
da82a41697 | ||
![]() |
aceb82a75e | ||
![]() |
f2749bc29d | ||
![]() |
55afaa87da | ||
![]() |
d77940ab39 | ||
![]() |
1eea446e45 | ||
![]() |
19c6241e10 | ||
![]() |
b290d0ed32 | ||
![]() |
2afe662646 | ||
![]() |
107a9a3b51 | ||
![]() |
3d0c64f523 | ||
![]() |
422ca34ac2 | ||
![]() |
6d63f9255f | ||
![]() |
6f2cc9761d | ||
![]() |
b484d9bca6 | ||
![]() |
58c4fd745a | ||
![]() |
7d1e6affb3 |
12
.github/workflows/debug.yml
vendored
12
.github/workflows/debug.yml
vendored
@ -31,12 +31,6 @@ jobs:
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- name: Add cache to Go proxy
|
||||
run: |
|
||||
version=`git rev-parse HEAD`
|
||||
@ -196,12 +190,6 @@ jobs:
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- name: Build
|
||||
id: build
|
||||
run: make
|
||||
|
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@ -31,12 +31,6 @@ jobs:
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ${{ steps.version.outputs.go_version }}
|
||||
- name: Cache go module
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/go/pkg/mod
|
||||
key: go-${{ hashFiles('**/go.sum') }}
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
|
13
Makefile
13
Makefile
@ -77,13 +77,20 @@ test_stdio:
|
||||
go mod tidy && \
|
||||
go test -v -tags "$(TAGS_TEST),force_stdio" .
|
||||
|
||||
android:
|
||||
go run ./cmd/internal/build_libbox -target android
|
||||
|
||||
ios:
|
||||
go run ./cmd/internal/build_libbox -target ios
|
||||
|
||||
lib:
|
||||
go run ./cmd/internal/build_libbox
|
||||
go run ./cmd/internal/build_libbox -target android
|
||||
go run ./cmd/internal/build_libbox -target ios
|
||||
|
||||
lib_install:
|
||||
go get -v -d
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20221130124640-349ebaa752ca
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20221130124640-349ebaa752ca
|
||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230413023804-244d7ff07035
|
||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230413023804-244d7ff07035
|
||||
|
||||
clean:
|
||||
rm -rf bin dist sing-box
|
||||
|
@ -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
23
adapter/fakeip.go
Normal 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
|
||||
}
|
50
adapter/fakeip_metadata.go
Normal file
50
adapter/fakeip_metadata.go
Normal 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
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
@ -39,7 +44,9 @@ type Router interface {
|
||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||
PackageManager() tun.PackageManager
|
||||
|
||||
Rules() []Rule
|
||||
IPRules() []IPRule
|
||||
|
||||
TimeService
|
||||
|
||||
@ -76,6 +83,12 @@ type Rule interface {
|
||||
type DNSRule interface {
|
||||
Rule
|
||||
DisableCache() bool
|
||||
RewriteTTL() *uint32
|
||||
}
|
||||
|
||||
type IPRule interface {
|
||||
Rule
|
||||
Action() tun.ActionType
|
||||
}
|
||||
|
||||
type InterfaceUpdateListener interface {
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
114
common/badversion/version.go
Normal file
114
common/badversion/version.go
Normal 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
|
||||
}
|
17
common/badversion/version_json.go
Normal file
17
common/badversion/version_json.go
Normal 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
|
||||
}
|
18
common/badversion/version_test.go
Normal file
18
common/badversion/version_test.go
Normal 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")))
|
||||
}
|
@ -12,7 +12,7 @@ type Conn struct {
|
||||
element *list.Element[io.Closer]
|
||||
}
|
||||
|
||||
func NewConn(conn net.Conn) (*Conn, error) {
|
||||
func NewConn(conn net.Conn) (net.Conn, error) {
|
||||
connAccess.Lock()
|
||||
element := openConnection.PushBack(conn)
|
||||
connAccess.Unlock()
|
||||
|
@ -12,7 +12,7 @@ type PacketConn struct {
|
||||
element *list.Element[io.Closer]
|
||||
}
|
||||
|
||||
func NewPacketConn(conn net.PacketConn) (*PacketConn, error) {
|
||||
func NewPacketConn(conn net.PacketConn) (net.PacketConn, error) {
|
||||
connAccess.Lock()
|
||||
element := openConnection.PushBack(conn)
|
||||
connAccess.Unlock()
|
||||
|
@ -73,7 +73,7 @@ func (d *ResolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), destination, M.SocksaddrFrom(destinationAddress, destination.Port)), nil
|
||||
return bufio.NewNATPacketConn(bufio.NewPacketConn(conn), M.SocksaddrFrom(destinationAddress, destination.Port), destination), nil
|
||||
}
|
||||
|
||||
func (d *ResolveDialer) Upstream() any {
|
||||
|
@ -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,
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@ -68,7 +69,7 @@ func (c *Client) DialContext(ctx context.Context, network string, destination M.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bufio.NewUnbindPacketConn(&ClientPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination}), nil
|
||||
return bufio.NewBindPacketConn(deadline.NewPacketConn(bufio.NewNetPacketConn(&ClientPacketConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination})), destination), nil
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
@ -79,7 +80,7 @@ func (c *Client) ListenPacket(ctx context.Context, destination M.Socksaddr) (net
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ClientPacketAddrConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination}, nil
|
||||
return deadline.NewPacketConn(&ClientPacketAddrConn{ExtendedConn: bufio.NewExtendedConn(stream), destination: destination}), nil
|
||||
}
|
||||
|
||||
func (c *Client) openStream() (net.Conn, error) {
|
||||
@ -413,7 +414,11 @@ func (c *ClientPacketAddrConn) ReadFrom(p []byte) (n int, addr net.Addr, err err
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if destination.IsFqdn() {
|
||||
addr = destination
|
||||
} else {
|
||||
addr = destination.UDPAddr()
|
||||
}
|
||||
var length uint16
|
||||
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
|
||||
if err != nil {
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@ -67,7 +68,7 @@ func newConnection(ctx context.Context, router adapter.Router, errorHandler E.Ha
|
||||
logger.InfoContext(ctx, "inbound multiplex packet connection")
|
||||
packetConn = &ServerPacketAddrConn{ExtendedConn: bufio.NewExtendedConn(stream)}
|
||||
}
|
||||
hErr := router.RoutePacketConnection(ctx, packetConn, metadata)
|
||||
hErr := router.RoutePacketConnection(ctx, deadline.NewPacketConn(bufio.NewNetPacketConn(packetConn)), metadata)
|
||||
stream.Close()
|
||||
if hErr != nil {
|
||||
errorHandler.NewError(ctx, hErr)
|
||||
|
@ -3,10 +3,12 @@ package process
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"os/user"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-tun"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
type Searcher interface {
|
||||
@ -28,5 +30,15 @@ type Info struct {
|
||||
}
|
||||
|
||||
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
return findProcessInfo(searcher, ctx, network, source, destination)
|
||||
info, err := searcher.FindProcessInfo(ctx, network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.UserId != -1 {
|
||||
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
||||
if osUser != nil {
|
||||
info.User = osUser.Username
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
@ -1,25 +0,0 @@
|
||||
//go:build linux && !android
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"os/user"
|
||||
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
func findProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
info, err := searcher.FindProcessInfo(ctx, network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.UserId != -1 {
|
||||
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
||||
if osUser != nil {
|
||||
info.User = osUser.Username
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
//go:build !linux || android
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
func findProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
return searcher.FindProcessInfo(ctx, network, source, destination)
|
||||
}
|
@ -24,12 +24,12 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout
|
||||
}
|
||||
err := conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, E.Cause(err, "set read deadline")
|
||||
}
|
||||
_, err = buffer.ReadOnceFrom(conn)
|
||||
err = E.Errors(err, conn.SetReadDeadline(time.Time{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, E.Cause(err, "read payload")
|
||||
}
|
||||
var metadata *adapter.InboundContext
|
||||
var errors []error
|
||||
|
@ -180,7 +180,7 @@ func NewSTDServer(ctx context.Context, router adapter.Router, logger log.Logger,
|
||||
tlsConfig.ServerName = options.ServerName
|
||||
}
|
||||
if len(options.ALPN) > 0 {
|
||||
tlsConfig.NextProtos = append(tlsConfig.NextProtos, options.ALPN...)
|
||||
tlsConfig.NextProtos = append(options.ALPN, tlsConfig.NextProtos...)
|
||||
}
|
||||
if options.MinVersion != "" {
|
||||
minVersion, err := ParseTLSVersion(options.MinVersion)
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -1,3 +1,38 @@
|
||||
#### 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)
|
||||
|
25
docs/configuration/dns/fakeip.md
Normal file
25
docs/configuration/dns/fakeip.md
Normal 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.
|
25
docs/configuration/dns/fakeip.zh.md
Normal file
25
docs/configuration/dns/fakeip.zh.md
Normal 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 地址范围。
|
@ -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
|
||||
|
||||
@ -44,3 +47,14 @@ Disable dns cache.
|
||||
#### disable_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.
|
||||
|
@ -10,7 +10,9 @@
|
||||
"final": "",
|
||||
"strategy": "",
|
||||
"disable_cache": false,
|
||||
"disable_expire": false
|
||||
"disable_expire": false,
|
||||
"reverse_mapping": false,
|
||||
"fakeip": {}
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,3 +46,13 @@
|
||||
#### disable_expire
|
||||
|
||||
禁用 DNS 缓存过期。
|
||||
|
||||
#### reverse_mapping
|
||||
|
||||
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
|
||||
|
||||
由于此过程依赖于应用程序在发出请求之前解析域名的行为,因此在 macOS 等 DNS 由系统代理和缓存的环境中可能会出现问题。
|
||||
|
||||
#### fakeip
|
||||
|
||||
[FakeIP](./fakeip) 设置。
|
||||
|
@ -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
|
||||
|
@ -243,6 +243,10 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
在此查询中禁用缓存。
|
||||
|
||||
#### rewrite_ttl
|
||||
|
||||
重写 DNS 回应中的 TTL。
|
||||
|
||||
### 逻辑字段
|
||||
|
||||
#### type
|
||||
|
@ -31,7 +31,7 @@ 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` |
|
||||
@ -41,6 +41,7 @@ The address of the dns server.
|
||||
| `HTTP3` | `h3://8.8.8.8/dns-query` |
|
||||
| `RCode` | `rcode://refused` |
|
||||
| `DHCP` | `dhcp://auto` or `dhcp://en0` |
|
||||
| [FakeIP](./fakeip) | `fakeip` |
|
||||
|
||||
!!! warning ""
|
||||
|
||||
|
@ -31,7 +31,7 @@ DNS 服务器的标签。
|
||||
DNS 服务器的地址。
|
||||
|
||||
| 协议 | 格式 |
|
||||
|----------|------------------------------|
|
||||
|--------------------|------------------------------|
|
||||
| `System` | `local` |
|
||||
| `TCP` | `tcp://1.0.0.1` |
|
||||
| `UDP` | `8.8.8.8` `udp://8.8.4.4` |
|
||||
@ -41,6 +41,7 @@ DNS 服务器的地址。
|
||||
| `HTTP3` | `h3://8.8.8.8/dns-query` |
|
||||
| `RCode` | `rcode://refused` |
|
||||
| `DHCP` | `dhcp://auto` 或 `dhcp://en0` |
|
||||
| [FakeIP](./fakeip) | `fakeip` |
|
||||
|
||||
!!! warning ""
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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 的密钥(可选)
|
||||
|
@ -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).
|
||||
|
@ -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)。
|
||||
|
@ -38,3 +38,9 @@
|
||||
#### tag
|
||||
|
||||
The tag of the outbound.
|
||||
|
||||
### Features
|
||||
|
||||
#### Outbounds that support IP connection
|
||||
|
||||
* `WireGuard`
|
||||
|
@ -37,3 +37,9 @@
|
||||
#### tag
|
||||
|
||||
出站的标签。
|
||||
|
||||
### 特性
|
||||
|
||||
#### 支持 IP 连接的出站
|
||||
|
||||
* `WireGuard`
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
"route": {
|
||||
"geoip": {},
|
||||
"geosite": {},
|
||||
"ip_rules": [],
|
||||
"rules": [],
|
||||
"final": "",
|
||||
"auto_detect_interface": false,
|
||||
@ -20,9 +21,10 @@
|
||||
### Fields
|
||||
|
||||
| Key | Format |
|
||||
|-----------|------------------------------|
|
||||
|------------|------------------------------------|
|
||||
| `geoip` | [GeoIP](./geoip) |
|
||||
| `geosite` | [Geosite](./geosite) |
|
||||
| `ip_rules` | List of [IP Route Rule](./ip-rule) |
|
||||
| `rules` | List of [Route Rule](./rule) |
|
||||
|
||||
#### final
|
||||
|
@ -7,6 +7,7 @@
|
||||
"route": {
|
||||
"geoip": {},
|
||||
"geosite": {},
|
||||
"ip_rules": [],
|
||||
"rules": [],
|
||||
"final": "",
|
||||
"auto_detect_interface": false,
|
||||
@ -20,9 +21,10 @@
|
||||
### 字段
|
||||
|
||||
| 键 | 格式 |
|
||||
|-----------|----------------------|
|
||||
|------------|-------------------------|
|
||||
| `geoip` | [GeoIP](./geoip) |
|
||||
| `geosite` | [GeoSite](./geosite) |
|
||||
| `ip_rules` | 一组 [IP 路由规则](./ip-rule) |
|
||||
| `rules` | 一组 [路由规则](./rule) |
|
||||
|
||||
#### final
|
||||
|
205
docs/configuration/route/ip-rule.md
Normal file
205
docs/configuration/route/ip-rule.md
Normal 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.
|
204
docs/configuration/route/ip-rule.zh.md
Normal file
204
docs/configuration/route/ip-rule.zh.md
Normal 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
|
||||
|
||||
==必填==
|
||||
|
||||
包括的默认规则。
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
==必填==
|
||||
|
||||
目标出站的标签。
|
||||
包括的默认规则。
|
@ -8,3 +8,4 @@ Configuration examples for sing-box.
|
||||
* [Shadowsocks](./shadowsocks)
|
||||
* [ShadowTLS](./shadowtls)
|
||||
* [Clash API](./clash-api)
|
||||
* [WireGuard Direct](./wireguard-direct)
|
||||
|
@ -8,3 +8,4 @@ sing-box 的配置示例。
|
||||
* [Shadowsocks](./shadowsocks)
|
||||
* [ShadowTLS](./shadowtls)
|
||||
* [Clash API](./clash-api)
|
||||
* [WireGuard Direct](./wireguard-direct)
|
||||
|
90
docs/examples/wireguard-direct.md
Normal file
90
docs/examples/wireguard-direct.md
Normal 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
20
docs/faq/fakeip.md
Normal 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
19
docs/faq/fakeip.zh.md
Normal file
@ -0,0 +1,19 @@
|
||||
# FakeIP
|
||||
|
||||
FakeIP 是指同时劫持 DNS 和连接请求的程序中的一种行为。它通过虚拟结果响应 DNS 请求,在接受连接时恢复映射。
|
||||
|
||||
#### 优点
|
||||
|
||||
* 在像 L3 路由这样无法进行流量探测的地方检索所请求的域名,以协助路由。
|
||||
* 减少对一个域的第一个 TCP 请求的 RTT(这是最常见的原因)。
|
||||
|
||||
#### 限制
|
||||
|
||||
* 它的机制会破坏依赖于返回正确远程地址的应用程序。
|
||||
* 仅支持 A 和 AAAA(IP)请求,这可能会破坏依赖于其他请求的应用程序。
|
||||
|
||||
#### 建议
|
||||
|
||||
* 如果不需要 L3 路由,请勿使用。
|
||||
* 如果使用 tun,请确保 tun 路由中包含 FakeIP 地址范围。
|
||||
* 启用 `experimental.clash_api.store_fakeip` 以持久化 FakeIP 记录,或者使用 `dns.rules.rewrite_ttl` 避免程序重启后在 DNS 被缓存的环境中丢失记录。
|
@ -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.
|
||||
|
@ -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 的网络栈,仅实现它的传输协议是舍本逐末的。
|
||||
|
78
experimental/clashapi/api_meta.go
Normal file
78
experimental/clashapi/api_meta.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
136
experimental/clashapi/api_meta_group.go
Normal file
136
experimental/clashapi/api_meta_group.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
77
experimental/clashapi/cachefile/fakeip.go
Normal file
77
experimental/clashapi/cachefile/fakeip.go
Normal 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)
|
||||
})
|
||||
}
|
@ -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)
|
||||
|
@ -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) {
|
||||
@ -61,12 +66,15 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
|
||||
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})
|
||||
}
|
||||
|
164
experimental/clashapi/server_resources.go
Normal file
164
experimental/clashapi/server_resources.go
Normal 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
|
||||
}
|
@ -1,35 +1,33 @@
|
||||
package trafficontrol
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/experimental/clashapi/compatible"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
uploadTemp atomic.Int64
|
||||
downloadTemp atomic.Int64
|
||||
uploadBlip atomic.Int64
|
||||
downloadBlip atomic.Int64
|
||||
uploadTotal atomic.Int64
|
||||
downloadTotal atomic.Int64
|
||||
|
||||
connections compatible.Map[string, tracker]
|
||||
uploadTemp *atomic.Int64
|
||||
downloadTemp *atomic.Int64
|
||||
uploadBlip *atomic.Int64
|
||||
downloadBlip *atomic.Int64
|
||||
uploadTotal *atomic.Int64
|
||||
downloadTotal *atomic.Int64
|
||||
ticker *time.Ticker
|
||||
done chan struct{}
|
||||
// process *process.Process
|
||||
memory uint64
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
manager := &Manager{
|
||||
uploadTemp: atomic.NewInt64(0),
|
||||
downloadTemp: atomic.NewInt64(0),
|
||||
uploadBlip: atomic.NewInt64(0),
|
||||
downloadBlip: atomic.NewInt64(0),
|
||||
uploadTotal: atomic.NewInt64(0),
|
||||
downloadTotal: atomic.NewInt64(0),
|
||||
ticker: time.NewTicker(time.Second),
|
||||
done: make(chan struct{}),
|
||||
// process: &process.Process{Pid: int32(os.Getpid())},
|
||||
}
|
||||
go manager.handle()
|
||||
return manager
|
||||
@ -64,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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,4 +112,5 @@ type Snapshot struct {
|
||||
DownloadTotal int64 `json:"downloadTotal"`
|
||||
UploadTotal int64 `json:"uploadTotal"`
|
||||
Connections []tracker `json:"connections"`
|
||||
Memory uint64 `json:"memory"`
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package trafficontrol
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
@ -8,10 +9,10 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/experimental/trackerconn"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"go.uber.org/atomic"
|
||||
"github.com/gofrs/uuid/v5"
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
@ -43,6 +44,19 @@ type trackerInfo struct {
|
||||
RulePayload string `json:"rulePayload"`
|
||||
}
|
||||
|
||||
func (t trackerInfo) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"id": t.UUID.String(),
|
||||
"metadata": t.Metadata,
|
||||
"upload": t.UploadTotal.Load(),
|
||||
"download": t.DownloadTotal.Load(),
|
||||
"start": t.Start,
|
||||
"chains": t.Chain,
|
||||
"rule": t.Rule,
|
||||
"rulePayload": t.RulePayload,
|
||||
})
|
||||
}
|
||||
|
||||
type tcpTracker struct {
|
||||
N.ExtendedConn `json:"-"`
|
||||
*trackerInfo
|
||||
@ -97,8 +111,8 @@ func NewTCPTracker(conn net.Conn, manager *Manager, metadata Metadata, router ad
|
||||
next = group.Now()
|
||||
}
|
||||
|
||||
upload := atomic.NewInt64(0)
|
||||
download := atomic.NewInt64(0)
|
||||
upload := new(atomic.Int64)
|
||||
download := new(atomic.Int64)
|
||||
|
||||
t := &tcpTracker{
|
||||
ExtendedConn: trackerconn.NewHook(conn, func(n int64) {
|
||||
@ -184,8 +198,8 @@ func NewUDPTracker(conn N.PacketConn, manager *Manager, metadata Metadata, route
|
||||
next = group.Now()
|
||||
}
|
||||
|
||||
upload := atomic.NewInt64(0)
|
||||
download := atomic.NewInt64(0)
|
||||
upload := new(atomic.Int64)
|
||||
download := new(atomic.Int64)
|
||||
|
||||
ut := &udpTracker{
|
||||
PacketConn: trackerconn.NewHookPacket(conn, func(n int64) {
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/debug"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
@ -71,8 +72,10 @@ func (s *CommandServer) loopConnection(listener net.Listener) {
|
||||
go func() {
|
||||
hErr := s.handleConnection(conn)
|
||||
if hErr != nil && !E.IsClosed(err) {
|
||||
if debug.Enabled {
|
||||
log.Warn("log-server: process connection: ", hErr)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ func (s *CommandServer) handleStatusConn(conn net.Conn) error {
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
|
29
experimental/libbox/log.go
Normal file
29
experimental/libbox/log.go
Normal file
@ -0,0 +1,29 @@
|
||||
//go:build darwin || linux
|
||||
|
||||
package libbox
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var stderrFile *os.File
|
||||
|
||||
func RedirectStderr(path string) error {
|
||||
if stats, err := os.Stat(path); err == nil && stats.Size() > 0 {
|
||||
_ = os.Rename(path, path+".old")
|
||||
}
|
||||
outputFile, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = unix.Dup2(int(outputFile.Fd()), int(os.Stderr.Fd()))
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputFile.Name())
|
||||
return err
|
||||
}
|
||||
stderrFile = outputFile
|
||||
return nil
|
||||
}
|
@ -10,6 +10,10 @@ func SetBasePath(path string) {
|
||||
C.SetBasePath(path)
|
||||
}
|
||||
|
||||
func SetTempPath(path string) {
|
||||
C.SetTempPath(path)
|
||||
}
|
||||
|
||||
func Version() string {
|
||||
return C.Version
|
||||
}
|
||||
|
@ -3,11 +3,10 @@ package trackerconn
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common/bufio"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
func New(conn net.Conn, readCounter []*atomic.Int64, writeCounter []*atomic.Int64) *Conn {
|
||||
|
@ -1,11 +1,10 @@
|
||||
package trackerconn
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
func NewPacket(conn N.PacketConn, readCounter []*atomic.Int64, writeCounter []*atomic.Int64) *PacketConn {
|
||||
|
@ -12,10 +12,9 @@ import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/experimental/trackerconn"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -211,7 +210,7 @@ func (s *StatsService) loadOrCreateCounter(name string) *atomic.Int64 {
|
||||
if loaded {
|
||||
return counter
|
||||
}
|
||||
counter = atomic.NewInt64(0)
|
||||
counter = &atomic.Int64{}
|
||||
s.counters[name] = counter
|
||||
return counter
|
||||
}
|
||||
|
18
go.mod
18
go.mod
@ -12,9 +12,9 @@ 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-20230327135226-74ae03f2425e
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/mholt/acmez v1.1.0
|
||||
github.com/miekg/dns v1.1.53
|
||||
@ -22,15 +22,15 @@ require (
|
||||
github.com/oschwald/maxminddb-golang v1.10.0
|
||||
github.com/pires/go-proxyproto v0.7.0
|
||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0
|
||||
github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca
|
||||
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.2-0.20230407053809-308e421e33c2
|
||||
github.com/sagernet/sing v0.2.3-0.20230413023204-48b019b13e78
|
||||
github.com/sagernet/sing-dns v0.1.5-0.20230408004833-5adaf486d440
|
||||
github.com/sagernet/sing-shadowsocks v0.2.0
|
||||
github.com/sagernet/sing-shadowtls v0.1.0
|
||||
github.com/sagernet/sing-shadowsocks v0.2.1-0.20230412123110-1a7c32b4e2e7
|
||||
github.com/sagernet/sing-shadowtls v0.1.1-0.20230409094821-9abef019436f
|
||||
github.com/sagernet/sing-tun v0.1.4-0.20230326080954-8848c0e4cbab
|
||||
github.com/sagernet/sing-vmess v0.1.3
|
||||
github.com/sagernet/sing-vmess v0.1.4-0.20230412122845-9470e68f5e45
|
||||
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
|
||||
@ -39,10 +39,9 @@ require (
|
||||
github.com/spf13/cobra v1.7.0
|
||||
github.com/stretchr/testify v1.8.2
|
||||
go.etcd.io/bbolt v1.3.7
|
||||
go.uber.org/atomic v1.10.0
|
||||
go.uber.org/zap v1.24.0
|
||||
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35
|
||||
golang.org/x/crypto v0.7.0
|
||||
golang.org/x/crypto v0.8.0
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
|
||||
golang.org/x/net v0.9.0
|
||||
golang.org/x/sys v0.7.0
|
||||
@ -82,6 +81,7 @@ require (
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/text v0.9.0 // indirect
|
||||
|
34
go.sum
34
go.sum
@ -33,8 +33,8 @@ 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=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@ -51,8 +51,8 @@ github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbg
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230327135226-74ae03f2425e h1:8ChxkWKTVYg7LKBvYNLNRnlobgbPrzzossZUoST2T7o=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230327135226-74ae03f2425e/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI=
|
||||
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
|
||||
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
@ -101,8 +101,10 @@ github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt
|
||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
|
||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
|
||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
|
||||
github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca h1:w56+kf8BeqLqllrRJ1tdwKc3sCdWOn/DuNHpY9fAiqs=
|
||||
github.com/sagernet/gomobile v0.0.0-20221130124640-349ebaa752ca/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU=
|
||||
github.com/sagernet/gomobile v0.0.0-20230413023437-ec061884b992 h1:WkcHhOX3ce9ElLKDUQKJrAt7SjpKNnASsPbMfqfZEPc=
|
||||
github.com/sagernet/gomobile v0.0.0-20230413023437-ec061884b992/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU=
|
||||
github.com/sagernet/gomobile v0.0.0-20230413023804-244d7ff07035 h1:KttYh6bBhIw8Y6/Ljn7CGwC3CKZn788rzMJmeAKjY+8=
|
||||
github.com/sagernet/gomobile v0.0.0-20230413023804-244d7ff07035/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU=
|
||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||
github.com/sagernet/quic-go v0.0.0-20230202071646-a8c8afb18b32 h1:tztuJB+giOWNRKQEBVY2oI3PsheTooMdh+/yxemYQYY=
|
||||
@ -111,18 +113,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.2-0.20230407053809-308e421e33c2 h1:VjeHDxEgpB2fqK5G16yBvtLacibvg3h2MsIjal0UXH0=
|
||||
github.com/sagernet/sing v0.2.2-0.20230407053809-308e421e33c2/go.mod h1:9uHswk2hITw8leDbiLS/xn0t9nzBcbePxzm9PJhwdlw=
|
||||
github.com/sagernet/sing v0.2.3-0.20230413023204-48b019b13e78 h1:bTE9RgURmiFiTjXaYN6q9BYPTBPKIzlFYNy2p+WOubI=
|
||||
github.com/sagernet/sing v0.2.3-0.20230413023204-48b019b13e78/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w=
|
||||
github.com/sagernet/sing-dns v0.1.5-0.20230408004833-5adaf486d440 h1:VH8/BcOVuApHtS+vKP+khxlGRcXH7KKhgkTDtNynqSQ=
|
||||
github.com/sagernet/sing-dns v0.1.5-0.20230408004833-5adaf486d440/go.mod h1:69PNSHyEmXdjf6C+bXBOdr2GQnPeEyWjIzo/MV8gmz8=
|
||||
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-shadowsocks v0.2.1-0.20230412123110-1a7c32b4e2e7 h1:3WDMIF1aE/twc5gJ+9PF2ZJqUxwZ80MPtNBKE3yBevU=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.1-0.20230412123110-1a7c32b4e2e7/go.mod h1:WoVjGUvRqsx5yhYeDAB5CijCHpNDi0LUPHl3cf7u8Lc=
|
||||
github.com/sagernet/sing-shadowtls v0.1.1-0.20230409094821-9abef019436f h1:qzQvpcDm60zPW8UlZa8UEaBoFORFeGAnhDncPc3VWT4=
|
||||
github.com/sagernet/sing-shadowtls v0.1.1-0.20230409094821-9abef019436f/go.mod h1:MxB+Q9H0pAHcrlvNmwSs1crljRwHFFVhtXyOMBy44Nw=
|
||||
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-vmess v0.1.4-0.20230412122845-9470e68f5e45 h1:QqYhWah3u+o2tvLRuTfEu3BwsGpf/wNnVK/VNQV2YBM=
|
||||
github.com/sagernet/sing-vmess v0.1.4-0.20230412122845-9470e68f5e45/go.mod h1:eULig3LgaeNiWSquSlzXF42Joypsj3fO1W+Qy93o6hk=
|
||||
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=
|
||||
@ -168,8 +170,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29 h1:ooxPy7fPvB4kwsA2h+iBNHkAbp/4JxTSwCmvdjEYmug=
|
||||
golang.org/x/exp v0.0.0-20230321023759-10a507213a29/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
|
@ -10,11 +10,10 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/atomic"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
var _ adapter.Inbound = (*myInboundAdapter)(nil)
|
||||
|
@ -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
|
||||
|
@ -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,12 +140,17 @@ 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)
|
||||
} else {
|
||||
@ -155,7 +159,12 @@ func (t *Tun) Start() error {
|
||||
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
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"github.com/sagernet/sing-vmess/packetaddr"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
@ -171,7 +172,7 @@ func (h *VLESS) newPacketConnection(ctx context.Context, conn N.PacketConn, meta
|
||||
}
|
||||
if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress {
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
conn = packetaddr.NewConn(conn.(vmess.PacketConn), metadata.Destination)
|
||||
conn = deadline.NewPacketConn(packetaddr.NewConn(conn.(vmess.PacketConn), metadata.Destination))
|
||||
h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection")
|
||||
} else {
|
||||
h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"github.com/sagernet/sing-vmess/packetaddr"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/auth"
|
||||
"github.com/sagernet/sing/common/bufio/deadline"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
@ -179,7 +180,7 @@ func (h *VMess) newPacketConnection(ctx context.Context, conn N.PacketConn, meta
|
||||
}
|
||||
if metadata.Destination.Fqdn == packetaddr.SeqPacketMagicAddress {
|
||||
metadata.Destination = M.Socksaddr{}
|
||||
conn = packetaddr.NewConn(conn.(vmess.PacketConn), metadata.Destination)
|
||||
conn = deadline.NewPacketConn(packetaddr.NewConn(conn.(vmess.PacketConn), metadata.Destination))
|
||||
h.logger.InfoContext(ctx, "[", user, "] inbound packet addr connection")
|
||||
} else {
|
||||
h.logger.InfoContext(ctx, "[", user, "] inbound packet connection to ", metadata.Destination)
|
||||
|
@ -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: 协议探测
|
||||
|
||||
|
@ -2,6 +2,7 @@ package ntp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@ -9,6 +10,7 @@ import (
|
||||
"github.com/sagernet/sing-box/common/settings"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
@ -20,7 +22,7 @@ var _ adapter.TimeService = (*Service)(nil)
|
||||
|
||||
type Service struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
cancel common.ContextCancelCauseFunc
|
||||
server M.Socksaddr
|
||||
writeToSystem bool
|
||||
dialer N.Dialer
|
||||
@ -30,7 +32,7 @@ type Service struct {
|
||||
}
|
||||
|
||||
func NewService(ctx context.Context, router adapter.Router, logger logger.Logger, options option.NTPOptions) *Service {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
ctx, cancel := common.ContextWithCancelCause(ctx)
|
||||
server := options.ServerOptions.Build()
|
||||
if server.Port == 0 {
|
||||
server.Port = 123
|
||||
@ -64,7 +66,7 @@ func (s *Service) Start() error {
|
||||
|
||||
func (s *Service) Close() error {
|
||||
s.ticker.Stop()
|
||||
s.cancel()
|
||||
s.cancel(os.ErrClosed)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -3,9 +3,12 @@ package option
|
||||
type ClashAPIOptions struct {
|
||||
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"`
|
||||
}
|
||||
|
||||
|
113
option/dns.go
113
option/dns.go
@ -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"`
|
||||
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 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"`
|
||||
type DNSClientOptions struct {
|
||||
Strategy DomainStrategy `json:"strategy,omitempty"`
|
||||
DisableCache bool `json:"disable_cache,omitempty"`
|
||||
DisableExpire bool `json:"disable_expire,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"`
|
||||
}
|
||||
|
101
option/route.go
101
option/route.go
@ -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
101
option/rule.go
Normal 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
107
option/rule_dns.go
Normal 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
120
option/rule_ip.go
Normal 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)
|
||||
}
|
@ -30,4 +30,6 @@ type HTTPOutboundOptions struct {
|
||||
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"`
|
||||
}
|
||||
|
@ -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"`
|
||||
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"`
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -102,11 +102,10 @@ func (d *DNS) handleConnection(ctx context.Context, conn net.Conn, metadata adap
|
||||
|
||||
func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||
ctx = adapter.WithContext(ctx, &metadata)
|
||||
fastClose, cancel := context.WithCancel(ctx)
|
||||
fastClose, cancel := common.ContextWithCancelCause(ctx)
|
||||
timeout := canceler.New(fastClose, cancel, C.DNSTimeout)
|
||||
var group task.Group
|
||||
group.Append0(func(ctx context.Context) error {
|
||||
defer cancel()
|
||||
_buffer := buf.StackNewSize(dns.FixedPacketSize)
|
||||
defer common.KeepAlive(_buffer)
|
||||
buffer := common.Dup(_buffer)
|
||||
@ -115,11 +114,13 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada
|
||||
buffer.FullReset()
|
||||
destination, err := conn.ReadPacket(buffer)
|
||||
if err != nil {
|
||||
cancel(err)
|
||||
return err
|
||||
}
|
||||
var message mDNS.Msg
|
||||
err = message.Unpack(buffer.Bytes())
|
||||
if err != nil {
|
||||
cancel(err)
|
||||
return err
|
||||
}
|
||||
timeout.Update()
|
||||
@ -127,17 +128,22 @@ func (d *DNS) NewPacketConnection(ctx context.Context, conn N.PacketConn, metada
|
||||
go func() error {
|
||||
response, err := d.router.Exchange(adapter.WithContext(ctx, &metadataInQuery), &message)
|
||||
if err != nil {
|
||||
cancel(err)
|
||||
return err
|
||||
}
|
||||
timeout.Update()
|
||||
responseBuffer := buf.NewPacket()
|
||||
n, err := response.PackBuffer(responseBuffer.FreeBytes())
|
||||
if err != nil {
|
||||
cancel(err)
|
||||
responseBuffer.Release()
|
||||
return err
|
||||
}
|
||||
responseBuffer.Truncate(len(n))
|
||||
err = conn.WritePacket(responseBuffer, destination)
|
||||
if err != nil {
|
||||
cancel(err)
|
||||
}
|
||||
return err
|
||||
}()
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package outbound
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@ -14,14 +15,14 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/protocol/http"
|
||||
sHTTP "github.com/sagernet/sing/protocol/http"
|
||||
)
|
||||
|
||||
var _ adapter.Outbound = (*HTTP)(nil)
|
||||
|
||||
type HTTP struct {
|
||||
myOutboundAdapter
|
||||
client *http.Client
|
||||
client *sHTTP.Client
|
||||
}
|
||||
|
||||
func NewHTTP(router adapter.Router, logger log.ContextLogger, tag string, options option.HTTPOutboundOptions) (*HTTP, error) {
|
||||
@ -29,6 +30,13 @@ func NewHTTP(router adapter.Router, logger log.ContextLogger, tag string, option
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var headers http.Header
|
||||
if options.Headers != nil {
|
||||
headers = make(http.Header)
|
||||
for key, values := range options.Headers {
|
||||
headers[key] = values
|
||||
}
|
||||
}
|
||||
return &HTTP{
|
||||
myOutboundAdapter{
|
||||
protocol: C.TypeHTTP,
|
||||
@ -37,7 +45,14 @@ func NewHTTP(router adapter.Router, logger log.ContextLogger, tag string, option
|
||||
logger: logger,
|
||||
tag: tag,
|
||||
},
|
||||
http.NewClient(detour, options.ServerOptions.Build(), options.Username, options.Password, nil),
|
||||
sHTTP.NewClient(sHTTP.Options{
|
||||
Dialer: detour,
|
||||
Server: options.ServerOptions.Build(),
|
||||
Username: options.Username,
|
||||
Password: options.Password,
|
||||
Path: options.Path,
|
||||
Headers: headers,
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -157,7 +157,7 @@ func (h *shadowsocksDialer) DialContext(ctx context.Context, network string, des
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &bufio.BindPacketConn{PacketConn: h.method.DialPacketConn(outConn), Addr: destination}, nil
|
||||
return bufio.NewBindPacketConn(h.method.DialPacketConn(outConn), destination), nil
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
|
@ -127,7 +127,7 @@ func (h *ShadowsocksR) DialContext(ctx context.Context, network string, destinat
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &bufio.BindPacketConn{PacketConn: conn, Addr: destination}, nil
|
||||
return bufio.NewBindPacketConn(conn, destination), nil
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ func (h *trojanDialer) DialContext(ctx context.Context, network string, destinat
|
||||
case N.NetworkTCP:
|
||||
return trojan.NewClientConn(conn, h.key, destination), nil
|
||||
case N.NetworkUDP:
|
||||
return &bufio.BindPacketConn{PacketConn: trojan.NewClientPacketConn(conn, h.key), Addr: destination}, nil
|
||||
return bufio.NewBindPacketConn(trojan.NewClientPacketConn(conn, h.key), destination), nil
|
||||
default:
|
||||
return nil, E.Extend(N.ErrUnknownNetwork, network)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
@ -25,6 +26,7 @@ var (
|
||||
|
||||
type URLTest struct {
|
||||
myOutboundAdapter
|
||||
ctx context.Context
|
||||
tags []string
|
||||
link string
|
||||
interval time.Duration
|
||||
@ -32,7 +34,7 @@ type URLTest struct {
|
||||
group *URLTestGroup
|
||||
}
|
||||
|
||||
func NewURLTest(router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) {
|
||||
func NewURLTest(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.URLTestOutboundOptions) (*URLTest, error) {
|
||||
outbound := &URLTest{
|
||||
myOutboundAdapter: myOutboundAdapter{
|
||||
protocol: C.TypeURLTest,
|
||||
@ -40,6 +42,7 @@ func NewURLTest(router adapter.Router, logger log.ContextLogger, tag string, opt
|
||||
logger: logger,
|
||||
tag: tag,
|
||||
},
|
||||
ctx: ctx,
|
||||
tags: options.Outbounds,
|
||||
link: options.URL,
|
||||
interval: time.Duration(options.Interval),
|
||||
@ -67,11 +70,11 @@ func (s *URLTest) Start() error {
|
||||
}
|
||||
outbounds = append(outbounds, detour)
|
||||
}
|
||||
s.group = NewURLTestGroup(s.router, s.logger, outbounds, s.link, s.interval, s.tolerance)
|
||||
s.group = NewURLTestGroup(s.ctx, s.router, s.logger, outbounds, s.link, s.interval, s.tolerance)
|
||||
return s.group.Start()
|
||||
}
|
||||
|
||||
func (s URLTest) Close() error {
|
||||
func (s *URLTest) Close() error {
|
||||
return common.Close(
|
||||
common.PtrOrNil(s.group),
|
||||
)
|
||||
@ -85,6 +88,10 @@ func (s *URLTest) All() []string {
|
||||
return s.tags
|
||||
}
|
||||
|
||||
func (s *URLTest) URLTest(ctx context.Context, link string) (map[string]uint16, error) {
|
||||
return s.group.URLTest(ctx, link)
|
||||
}
|
||||
|
||||
func (s *URLTest) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
outbound := s.group.Select(network)
|
||||
conn, err := outbound.DialContext(ctx, network, destination)
|
||||
@ -92,14 +99,7 @@ func (s *URLTest) DialContext(ctx context.Context, network string, destination M
|
||||
return conn, nil
|
||||
}
|
||||
s.logger.ErrorContext(ctx, err)
|
||||
go s.group.checkOutbounds()
|
||||
outbounds := s.group.Fallback(outbound)
|
||||
for _, fallback := range outbounds {
|
||||
conn, err = fallback.DialContext(ctx, network, destination)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
s.group.history.DeleteURLTestHistory(outbound.Tag())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -110,14 +110,7 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne
|
||||
return conn, nil
|
||||
}
|
||||
s.logger.ErrorContext(ctx, err)
|
||||
go s.group.checkOutbounds()
|
||||
outbounds := s.group.Fallback(outbound)
|
||||
for _, fallback := range outbounds {
|
||||
conn, err = fallback.ListenPacket(ctx, destination)
|
||||
if err == nil {
|
||||
return conn, nil
|
||||
}
|
||||
}
|
||||
s.group.history.DeleteURLTestHistory(outbound.Tag())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -130,6 +123,7 @@ func (s *URLTest) NewPacketConnection(ctx context.Context, conn N.PacketConn, me
|
||||
}
|
||||
|
||||
type URLTestGroup struct {
|
||||
ctx context.Context
|
||||
router adapter.Router
|
||||
logger log.Logger
|
||||
outbounds []adapter.Outbound
|
||||
@ -142,11 +136,7 @@ type URLTestGroup struct {
|
||||
close chan struct{}
|
||||
}
|
||||
|
||||
func NewURLTestGroup(router adapter.Router, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16) *URLTestGroup {
|
||||
if link == "" {
|
||||
//goland:noinspection HttpUrlsUsage
|
||||
link = "http://www.gstatic.com/generate_204"
|
||||
}
|
||||
func NewURLTestGroup(ctx context.Context, router adapter.Router, logger log.Logger, outbounds []adapter.Outbound, link string, interval time.Duration, tolerance uint16) *URLTestGroup {
|
||||
if interval == 0 {
|
||||
interval = C.DefaultURLTestInterval
|
||||
}
|
||||
@ -160,6 +150,7 @@ func NewURLTestGroup(router adapter.Router, logger log.Logger, outbounds []adapt
|
||||
history = urltest.NewHistoryStorage()
|
||||
}
|
||||
return &URLTestGroup{
|
||||
ctx: ctx,
|
||||
router: router,
|
||||
logger: logger,
|
||||
outbounds: outbounds,
|
||||
@ -249,8 +240,14 @@ func (g *URLTestGroup) loopCheck() {
|
||||
}
|
||||
|
||||
func (g *URLTestGroup) checkOutbounds() {
|
||||
b, _ := batch.New(context.Background(), batch.WithConcurrencyNum[any](10))
|
||||
_, _ = g.URLTest(g.ctx, g.link)
|
||||
}
|
||||
|
||||
func (g *URLTestGroup) URLTest(ctx context.Context, link string) (map[string]uint16, error) {
|
||||
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 g.outbounds {
|
||||
tag := detour.Tag()
|
||||
realTag := RealTag(detour)
|
||||
@ -269,7 +266,7 @@ func (g *URLTestGroup) checkOutbounds() {
|
||||
b.Go(realTag, func() (any, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), C.TCPTimeout)
|
||||
defer cancel()
|
||||
t, err := urltest.URLTest(ctx, g.link, p)
|
||||
t, err := urltest.URLTest(ctx, link, p)
|
||||
if err != nil {
|
||||
g.logger.Debug("outbound ", tag, " unavailable: ", err)
|
||||
g.history.DeleteURLTestHistory(realTag)
|
||||
@ -279,9 +276,13 @@ func (g *URLTestGroup) checkOutbounds() {
|
||||
Time: time.Now(),
|
||||
Delay: t,
|
||||
})
|
||||
resultAccess.Lock()
|
||||
result[tag] = t
|
||||
resultAccess.Unlock()
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
b.Wait()
|
||||
return result, nil
|
||||
}
|
||||
|
@ -111,7 +111,7 @@ func (h *VLESS) DialContext(ctx context.Context, network string, destination M.S
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &bufio.BindPacketConn{PacketConn: packetaddr.NewConn(packetConn, destination), Addr: destination}, nil
|
||||
return bufio.NewBindPacketConn(packetaddr.NewConn(packetConn, destination), destination), nil
|
||||
} else {
|
||||
return h.client.DialEarlyPacketConn(conn, destination)
|
||||
}
|
||||
|
@ -8,7 +8,9 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
@ -26,7 +28,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
_ adapter.Outbound = (*WireGuard)(nil)
|
||||
_ adapter.IPOutbound = (*WireGuard)(nil)
|
||||
_ adapter.InterfaceUpdateListener = (*WireGuard)(nil)
|
||||
)
|
||||
|
||||
@ -34,6 +36,7 @@ type WireGuard struct {
|
||||
myOutboundAdapter
|
||||
bind *wireguard.ClientBind
|
||||
device *device.Device
|
||||
natDevice wireguard.NatDevice
|
||||
tunDevice wireguard.Device
|
||||
}
|
||||
|
||||
@ -54,13 +57,22 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
|
||||
}
|
||||
copy(reserved[:], options.Reserved)
|
||||
}
|
||||
peerAddr := options.ServerOptions.Build()
|
||||
outbound.bind = wireguard.NewClientBind(ctx, dialer.New(router, options.DialerOptions), peerAddr, reserved)
|
||||
var isConnect bool
|
||||
var connectAddr M.Socksaddr
|
||||
if len(options.Peers) < 2 {
|
||||
isConnect = true
|
||||
if len(options.Peers) == 1 {
|
||||
connectAddr = options.Peers[0].ServerOptions.Build()
|
||||
} else {
|
||||
connectAddr = options.ServerOptions.Build()
|
||||
}
|
||||
}
|
||||
outbound.bind = wireguard.NewClientBind(ctx, dialer.New(router, options.DialerOptions), isConnect, connectAddr, reserved)
|
||||
localPrefixes := common.Map(options.LocalAddress, option.ListenPrefix.Build)
|
||||
if len(localPrefixes) == 0 {
|
||||
return nil, E.New("missing local address")
|
||||
}
|
||||
var privateKey, peerPublicKey, preSharedKey string
|
||||
var privateKey string
|
||||
{
|
||||
bytes, err := base64.StdEncoding.DecodeString(options.PrivateKey)
|
||||
if err != nil {
|
||||
@ -68,6 +80,46 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
|
||||
}
|
||||
privateKey = hex.EncodeToString(bytes)
|
||||
}
|
||||
ipcConf := "private_key=" + privateKey
|
||||
if len(options.Peers) > 0 {
|
||||
for i, peer := range options.Peers {
|
||||
var peerPublicKey, preSharedKey string
|
||||
{
|
||||
bytes, err := base64.StdEncoding.DecodeString(peer.PublicKey)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode public key for peer ", i)
|
||||
}
|
||||
peerPublicKey = hex.EncodeToString(bytes)
|
||||
}
|
||||
if peer.PreSharedKey != "" {
|
||||
bytes, err := base64.StdEncoding.DecodeString(peer.PreSharedKey)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "decode pre shared key for peer ", i)
|
||||
}
|
||||
preSharedKey = hex.EncodeToString(bytes)
|
||||
}
|
||||
destination := peer.ServerOptions.Build()
|
||||
ipcConf += "\npublic_key=" + peerPublicKey
|
||||
ipcConf += "\nendpoint=" + destination.String()
|
||||
if preSharedKey != "" {
|
||||
ipcConf += "\npreshared_key=" + preSharedKey
|
||||
}
|
||||
if len(peer.AllowedIPs) == 0 {
|
||||
return nil, E.New("missing allowed_ips for peer ", i)
|
||||
}
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
ipcConf += "\nallowed_ip=" + allowedIP
|
||||
}
|
||||
if len(peer.Reserved) > 0 {
|
||||
if len(peer.Reserved) != 3 {
|
||||
return nil, E.New("invalid reserved value for peer ", i, ", required 3 bytes, got ", len(peer.Reserved))
|
||||
}
|
||||
copy(reserved[:], options.Reserved)
|
||||
outbound.bind.SetReservedForEndpoint(destination, reserved)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var peerPublicKey, preSharedKey string
|
||||
{
|
||||
bytes, err := base64.StdEncoding.DecodeString(options.PeerPublicKey)
|
||||
if err != nil {
|
||||
@ -82,9 +134,8 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
|
||||
}
|
||||
preSharedKey = hex.EncodeToString(bytes)
|
||||
}
|
||||
ipcConf := "private_key=" + privateKey
|
||||
ipcConf += "\npublic_key=" + peerPublicKey
|
||||
ipcConf += "\nendpoint=" + peerAddr.String()
|
||||
ipcConf += "\nendpoint=" + options.ServerOptions.Build().String()
|
||||
if preSharedKey != "" {
|
||||
ipcConf += "\npreshared_key=" + preSharedKey
|
||||
}
|
||||
@ -102,21 +153,30 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
|
||||
if has6 {
|
||||
ipcConf += "\nallowed_ip=::/0"
|
||||
}
|
||||
}
|
||||
mtu := options.MTU
|
||||
if mtu == 0 {
|
||||
mtu = 1408
|
||||
}
|
||||
var wireTunDevice wireguard.Device
|
||||
var tunDevice wireguard.Device
|
||||
var err error
|
||||
if !options.SystemInterface && tun.WithGVisor {
|
||||
wireTunDevice, err = wireguard.NewStackDevice(localPrefixes, mtu)
|
||||
tunDevice, err = wireguard.NewStackDevice(localPrefixes, mtu, options.IPRewrite)
|
||||
} else {
|
||||
wireTunDevice, err = wireguard.NewSystemDevice(router, options.InterfaceName, localPrefixes, mtu)
|
||||
tunDevice, err = wireguard.NewSystemDevice(router, options.InterfaceName, localPrefixes, mtu)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create WireGuard device")
|
||||
}
|
||||
wgDevice := device.NewDevice(wireTunDevice, outbound.bind, &device.Logger{
|
||||
natDevice, isNatDevice := tunDevice.(wireguard.NatDevice)
|
||||
if !isNatDevice && router.NatRequired(tag) {
|
||||
natDevice = wireguard.NewNATDevice(tunDevice, options.IPRewrite)
|
||||
}
|
||||
deviceInput := tunDevice
|
||||
if natDevice != nil {
|
||||
deviceInput = natDevice
|
||||
}
|
||||
wgDevice := device.NewDevice(deviceInput, outbound.bind, &device.Logger{
|
||||
Verbosef: func(format string, args ...interface{}) {
|
||||
logger.Debug(fmt.Sprintf(strings.ToLower(format), args...))
|
||||
},
|
||||
@ -132,7 +192,8 @@ func NewWireGuard(ctx context.Context, router adapter.Router, logger log.Context
|
||||
return nil, E.Cause(err, "setup wireguard")
|
||||
}
|
||||
outbound.device = wgDevice
|
||||
outbound.tunDevice = wireTunDevice
|
||||
outbound.natDevice = natDevice
|
||||
outbound.tunDevice = tunDevice
|
||||
return outbound, nil
|
||||
}
|
||||
|
||||
@ -171,6 +232,27 @@ func (w *WireGuard) NewPacketConnection(ctx context.Context, conn N.PacketConn,
|
||||
return NewPacketConnection(ctx, w, conn, metadata)
|
||||
}
|
||||
|
||||
func (w *WireGuard) NewIPConnection(ctx context.Context, conn tun.RouteContext, metadata adapter.InboundContext) (tun.DirectDestination, error) {
|
||||
if w.natDevice == nil {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
session := tun.RouteSession{
|
||||
IPVersion: metadata.IPVersion,
|
||||
Network: tun.NetworkFromName(metadata.Network),
|
||||
Source: metadata.Source.AddrPort(),
|
||||
Destination: metadata.Destination.AddrPort(),
|
||||
}
|
||||
switch session.Network {
|
||||
case syscall.IPPROTO_TCP:
|
||||
w.logger.InfoContext(ctx, "linked connection to ", metadata.Destination)
|
||||
case syscall.IPPROTO_UDP:
|
||||
w.logger.InfoContext(ctx, "linked packet connection to ", metadata.Destination)
|
||||
default:
|
||||
w.logger.InfoContext(ctx, "linked ", metadata.Network, " connection to ", metadata.Destination.AddrString())
|
||||
}
|
||||
return w.natDevice.CreateDestination(session, conn), nil
|
||||
}
|
||||
|
||||
func (w *WireGuard) Start() error {
|
||||
return w.tunDevice.Start()
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user