Compare commits

...

158 Commits

Author SHA1 Message Date
世界
7ebbd58b00 Update documentation 2022-12-03 14:38:52 +08:00
世界
d0095fd0f4 Fix close clash cache 2022-12-03 13:29:37 +08:00
世界
66d8d563eb Add WorkingDirectory for systemd service 2022-11-28 18:30:50 +08:00
世界
4bf96c7eb5 Fix quic stub 2022-11-28 16:06:54 +08:00
世界
f687c25fa9 Update documentation 2022-11-28 13:11:07 +08:00
世界
a92412ecac Fix router 2022-11-28 13:11:07 +08:00
世界
8dcafa5b33 Add trojan-go multiplex support for trojan inbound 2022-11-28 12:51:23 +08:00
世界
7a02cb83a7 Revert "Fix listen packet on address"
This reverts commit d1fe17a4db.
2022-11-28 12:51:23 +08:00
世界
51ce672076 Fix crash when input bad method in shadowsocks multi-user inbound 2022-11-28 12:51:23 +08:00
世界
7734afc40c Update dependencies 2022-11-28 12:51:23 +08:00
世界
ee3cd49aa5 Fix tls config for h2 server 2022-11-26 14:55:51 +08:00
世界
bf20ff84b5 Fix lint 2022-11-26 11:21:24 +08:00
世界
c58302554c Fix documentation 2022-11-26 11:06:57 +08:00
世界
05ed88aba8 Update documentation 2022-11-25 22:59:30 +08:00
世界
9f5cc0442b Fix dockerfile 2022-11-25 22:59:30 +08:00
世界
2641a43ad8 Remove test on pull request 2022-11-25 21:12:45 +08:00
世界
4a6ab5e9fd Fix cancel on start 2022-11-25 21:12:45 +08:00
世界
d1fe17a4db Fix listen packet on address 2022-11-25 21:12:45 +08:00
世界
7c910165ef Cleanup code 2022-11-24 12:37:29 +08:00
世界
8c1fddcf8d Remove connect packet conn 2022-11-24 12:01:25 +08:00
Hellojack
01b4769852 Cleanup gun conn code 2022-11-23 14:56:31 +08:00
世界
a401828ed5 Fix shadowtls server detection 2022-11-22 22:15:38 +08:00
世界
ffd54eef6c Update documentation 2022-11-21 21:20:44 +08:00
世界
c16e4316d6 Fix shadowtls server 2022-11-21 21:20:44 +08:00
世界
8b7fe20b7f Include uTLS in release 2022-11-21 15:25:49 +08:00
世界
696c1065b6 Update stable documentation 2022-11-21 14:57:22 +08:00
世界
5d690f4147 Update documentation 2022-11-21 13:18:04 +08:00
世界
f906641a82 Add uTLS to makefile default tags 2022-11-21 13:18:04 +08:00
世界
89913dfa8c Improve shadowtls server 2022-11-21 13:18:04 +08:00
世界
468778f67f Update dependencies 2022-11-21 13:18:04 +08:00
世界
22a22aebe2 Fix default dns transport strategy 2022-11-21 13:18:04 +08:00
世界
a2d2ec9b45 Update documentation 2022-11-15 17:36:42 +08:00
世界
2695b3516e Update issue template 2022-11-13 11:45:24 +08:00
世界
3a9ef8fac0 Remove unused 2022-11-13 11:30:48 +08:00
世界
ebad363201 Fix create TLS config 2022-11-13 11:24:37 +08:00
世界
11076d52cd Fix dns buffer & quic retry 2022-11-13 11:16:10 +08:00
世界
5eb132063e Fix connect packet connection for mux client 2022-11-12 03:53:42 +08:00
世界
13ab5d3348 Remove follow in update script 2022-11-11 22:32:24 +08:00
世界
ce1ddc400f Support x/h2 v0.2.0 deadline 2022-11-11 22:08:20 +08:00
arm64v8a
2c9d25e853 Fix websocket alpn 2022-11-11 20:01:49 +08:00
世界
3d76777760 Fix tor geoip 2022-11-10 22:42:05 +08:00
世界
24f4dfea04 Fix hysteria test 2022-11-10 21:10:18 +08:00
世界
2fc1a0a9dd Update documentation 2022-11-10 16:33:10 +08:00
世界
617aba84e4 Add multi user support for hysteria inbound 2022-11-09 21:00:08 +08:00
世界
5510c474c7 Fix h2c transport 2022-11-09 12:15:14 +08:00
世界
eb2e8a0b40 Add custom tls client support for std grpc 2022-11-09 11:46:29 +08:00
世界
972491c19d Fix default local DNS server behavior 2022-11-09 10:35:16 +08:00
世界
7358ca4a52 Fix vmess request buffer 2022-11-09 10:16:22 +08:00
世界
61c274045a Update install go script 2022-11-08 23:19:53 +08:00
世界
f205140b04 Fix smux keep alive 2022-11-08 16:45:38 +08:00
世界
1db8e03c86 Fix format 2022-11-08 14:54:19 +08:00
世界
2ecf86c2bc Update patched quic-go 2022-11-08 13:54:01 +08:00
世界
999a847e86 Add custom wireguard worker size option 2022-11-08 13:48:14 +08:00
世界
1f63ce5dee Fix reset outbound 2022-11-06 10:36:19 +08:00
世界
0ad1bbea11 Fix wireguard close 2022-11-06 10:20:23 +08:00
世界
b2cd78d279 Move WFP manipulation to strict route 2022-11-06 10:16:07 +08:00
世界
d5bb58a0b4 Update documentation 2022-11-06 10:16:07 +08:00
世界
7f84936050 Split bind_address 2022-11-06 10:16:07 +08:00
Dreamacro
6adfea0a72 Fix macOS Ventura process name match 2022-11-06 10:16:07 +08:00
Hellojack
10f213bf3d Adjust uTLS wrapper 2022-11-06 10:16:07 +08:00
世界
6e8c4f6576 Update documentation 2022-10-31 13:59:52 +08:00
世界
9779dc0154 Fix test 2022-10-31 13:59:52 +08:00
世界
a2abe31298 Fix uTLS config 2022-10-31 13:59:52 +08:00
世界
930d177dd0 Update dependencies 2022-10-31 13:59:52 +08:00
Fei1Yang
f3d1b59173 Update container action 2022-10-29 18:01:32 +08:00
世界
14452f3049 Update documentation 2022-10-29 18:00:05 +08:00
世界
4119c8647b Update dependencies 2022-10-29 17:56:21 +08:00
世界
90a94a8c63 Improve local dns transport 2022-10-29 17:37:11 +08:00
世界
b0c39ac7ff Suppress no network error 2022-10-28 09:54:04 +08:00
世界
8703e1ff98 Fix decrypt xplus packet 2022-10-28 09:53:57 +08:00
世界
35886b88d7 Add option for custom wireguard reserved bytes 2022-10-28 09:53:57 +08:00
永雏塔菲
d583b35717 Add s390x architecture support
* Update debug.yml

Signed-off-by: 永雏塔菲 <108621198+taffychan@users.noreply.github.com>
2022-10-28 09:53:57 +08:00
Hellojack
217ffb2f95 Update uTLS usage
* Update new uTLS fingerprints

* Update documentation
2022-10-28 09:53:57 +08:00
世界
22f06f582b Fix v2ray api 2022-10-26 20:06:13 +08:00
世界
f2b5098fa0 Update documentation 2022-10-25 21:26:28 +08:00
世界
0ca3290364 Add go1.18 debug build 2022-10-25 21:25:55 +08:00
世界
43d5b8598b Fix shadowtls conn 2022-10-25 21:25:42 +08:00
世界
f3e1d1defc Fix h3 dns transport 2022-10-20 11:04:03 +08:00
世界
95c03c9373 Fix copy pipe 2022-10-20 10:57:57 +08:00
世界
7e0958b4ac Update documentation 2022-10-19 10:55:06 +08:00
Skyxim
6a26737508 Check destination before udp connect 2022-10-19 10:22:46 +08:00
世界
92a92f39c5 Fix naive overflow 2022-10-18 17:52:52 +08:00
世界
fc533cd38d Fix DF for hysteria 2022-10-18 17:27:50 +08:00
世界
68e286499d Update dependencies 2022-10-18 17:27:50 +08:00
世界
f5c1900aad Add message for tfo error 2022-10-18 17:27:50 +08:00
世界
6591dd58ca Remove strict route on windows
replaced by custom route
2022-10-12 16:24:45 +08:00
XYenon
54af113363 Add custom route support (#147) 2022-10-12 16:20:17 +08:00
世界
3f1fe814ef Fix sniff fragmented quic client hello 2022-10-12 16:11:42 +08:00
世界
5a2cebebd1 Remove unused 2022-10-10 14:23:34 +08:00
世界
b8009d61b2 Fix tfo headroom 2022-10-10 13:33:48 +08:00
世界
a61a64bf9e Add shadowtls inbound test 2022-10-10 11:31:03 +08:00
世界
7d17c52fea Add more messages to darwin route error 2022-10-09 21:22:07 +08:00
世界
f5b15b392b Fix ssh outbound 2022-10-09 20:43:01 +08:00
世界
8a53846efd Fix uTLS handshake 2022-10-08 20:31:01 +08:00
世界
badc454452 Fix test 2022-10-08 20:30:52 +08:00
世界
a01bb569d1 Fix websocket headroom 2022-10-08 20:09:36 +08:00
世界
89ff9f8368 Fix interface monitor 2022-10-08 20:09:36 +08:00
世界
7f816a2ebc Add sniff_timeout 2022-10-08 20:09:36 +08:00
世界
39c141651a Update documentation 2022-10-06 23:33:57 +08:00
世界
b0ad9bb6f1 Add shadowtls v2 support 2022-10-06 22:47:11 +08:00
世界
d135d0f287 Update tfo-go usage 2022-10-06 21:58:50 +08:00
世界
b183ccf23d Fix wfp filter weight 2022-10-05 20:24:27 +08:00
世界
c2969bc186 Update documentation 2022-10-03 04:36:54 +08:00
世界
bd86bfcd22 Fix check system stack packet 2022-10-03 04:36:54 +08:00
世界
8aec64b855 Add v2ray mux support for all connections 2022-10-03 04:34:59 +08:00
世界
1445bdba37 Fix trojan fallback 2022-10-01 11:41:15 +08:00
世界
29d08e63b5 Fix clash tracker 2022-10-01 11:29:46 +08:00
世界
1173fdea64 Improve tls writer 2022-10-01 11:29:46 +08:00
世界
968430c338 Minor fixes 2022-09-30 21:08:07 +08:00
世界
3e5bee6faf Fix windows route 2022-09-30 00:36:42 +08:00
世界
aa613cba73 Fix dns close 2022-09-29 09:12:13 +08:00
世界
1e510511ae Fix random seed 2022-09-29 08:49:34 +08:00
世界
1b44faed17 Add v2ray stats api 2022-09-29 08:49:34 +08:00
世界
c7a485815c Add binary to .gitignore 2022-09-26 19:36:51 +08:00
世界
7f9c870bba Add direct io option for clash api 2022-09-26 15:31:02 +08:00
世界
b5564ef3d3 Fix bind control 2022-09-26 13:50:54 +08:00
世界
8ce244dd04 Fix documentation
Signed-off-by: 世界 <i@sekai.icu>
Signed-off-by: unknowndevQwQ <unknowndevQwQ@pm.me>
2022-09-26 12:25:18 +08:00
世界
0f57b93925 Update documentation 2022-09-25 22:29:18 +08:00
世界
c90a77a185 Refine 4in6 processing 2022-09-25 22:29:18 +08:00
世界
c6586f19fa Fix read source address from grpc-go 2022-09-25 22:29:18 +08:00
世界
cbab86ae38 Refine tproxy write back 2022-09-25 22:29:18 +08:00
世界
17b5f031f1 Fix shadowsocks plugins 2022-09-25 16:43:12 +08:00
世界
b00b6b9e25 Fix fqdn socks5 outbound connection 2022-09-25 14:42:39 +08:00
世界
fb6b3b0401 Fix missing source address from transport connection 2022-09-23 18:55:28 +08:00
世界
22ea878fe9 Improve websocket writer 2022-09-23 18:55:07 +08:00
世界
abe3dc6039 Add self sign cert support 2022-09-23 17:13:18 +08:00
世界
852829b9dc Add VMess benchmark result 2022-09-23 16:13:29 +08:00
世界
407509c985 Fix leaks and add test 2022-09-23 13:14:31 +08:00
世界
9856b73cb5 Update documentation 2022-09-23 10:30:07 +08:00
世界
f42356fbcb Fix system stack ipv4 overflow 2022-09-23 10:29:15 +08:00
世界
d0b467671a Merge VLESS to library 2022-09-23 10:28:51 +08:00
世界
c18c545798 Add stdio test 2022-09-23 10:28:24 +08:00
世界
693ef293ac Update buffer usage 2022-09-23 10:27:48 +08:00
世界
a006627795 Disable DF on direct outbound by default 2022-09-23 10:27:46 +08:00
世界
0738b184e4 Fix url test interval 2022-09-23 10:27:42 +08:00
世界
42524ba04e Fix dns sniffer 2022-09-17 16:59:28 +08:00
世界
63fc95b96d Add mux server and XUDP client for VMess 2022-09-17 11:54:04 +08:00
世界
ab436fc137 Update documentation 2022-09-16 15:48:31 +08:00
世界
1546770bfd Skip bind on local addr 2022-09-16 15:35:29 +08:00
世界
f4b2099488 Fix tun log 2022-09-16 15:32:50 +08:00
世界
a2c4d68031 Fix create UDP transport 2022-09-15 16:46:53 +08:00
世界
cfe14f2817 Suppress bad http2 error 2022-09-15 15:34:52 +08:00
世界
a5402ffb69 Add back urltest outbound 2022-09-15 15:22:08 +08:00
世界
4d24cf5ec4 Update documentation 2022-09-15 13:25:51 +08:00
世界
668d354771 Make gVisor optional 2022-09-15 12:24:08 +08:00
世界
ad14719b14 Fix clash api proxy type 2022-09-14 23:02:11 +08:00
世界
d9aa0a67d6 Fix port rule match logic 2022-09-14 22:03:26 +08:00
世界
92bf784f4f Move shadowsocksr implementation to clash 2022-09-14 21:57:40 +08:00
世界
395b13103a Fix test 2022-09-14 18:02:51 +08:00
世界
628cf56d3c Fix close grpc conn 2022-09-14 18:02:37 +08:00
世界
ac5582537f Add back test workflow 2022-09-14 18:02:37 +08:00
世界
9aa7a20d96 Print tags in version command 2022-09-14 18:02:37 +08:00
世界
189f02c802 Refactor bind control 2022-09-14 18:02:37 +08:00
世界
2373281c41 Fix clash store-selected 2022-09-13 17:34:37 +08:00
世界
e8f4c2d36f Redirect clash hello to external ui 2022-09-13 17:29:57 +08:00
世界
07b6db23c1 Update install go script 2022-09-13 16:24:44 +08:00
世界
9a3360e5d0 Fix build on go1.18 2022-09-13 16:23:20 +08:00
世界
007a278ac8 Refactor to miekg/dns 2022-09-13 16:18:39 +08:00
278 changed files with 9238 additions and 17223 deletions

View File

@@ -12,7 +12,7 @@ body:
required: true
- label: Yes, I've searched similar issues on GitHub and didn't find any.
required: true
- label: Yes, I've included all information below (version, config, log, etc).
- label: Yes, I've included all information below (version, **FULL** config, **FULL** log, etc).
required: true
- type: textarea

View File

@@ -55,6 +55,27 @@ jobs:
- name: Run Test
run: |
go test -v ./...
build_go118:
name: Debug build (Go 1.18)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v2
with:
go-version: 1.18.7
- name: Cache go module
uses: actions/cache@v2
with:
path: |
~/go/pkg/mod
key: go118-${{ hashFiles('**/go.sum') }}
- name: Run Test
run: |
go test -v ./...
cross:
strategy:
matrix:
@@ -128,6 +149,9 @@ jobs:
- name: linux-mips64el
goos: linux
goarch: mips64le
- name: linux-s390x
goos: linux
goarch: s390x
# darwin
- name: darwin-amd64
goos: darwin
@@ -192,4 +216,4 @@ jobs:
uses: actions/upload-artifact@v2
with:
name: sing-box-${{ matrix.name }}
path: sing-box*
path: sing-box*

View File

@@ -15,6 +15,8 @@ jobs:
uses: actions/checkout@v2
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v1
- name: Setup QEMU for Docker Buildx
uses: docker/setup-qemu-action@v2
- name: Login to GitHub Container Registry
uses: docker/login-action@v1
with:
@@ -29,15 +31,18 @@ jobs:
- name: Get tag to build
id: tag
run: |
echo "latest=ghcr.io/sagernet/sing-box:latest" >> $GITHUB_OUTPUT
if [[ -z "${{ github.event.inputs.tag }}" ]]; then
echo ::set-output name=tag::ghcr.io/sagernet/sing-box:${{ github.ref_name }}
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.ref_name }}" >> $GITHUB_OUTPUT
else
echo ::set-output name=tag::ghcr.io/sagernet/sing-box:${{ github.event.inputs.tag }}
echo "versioned=ghcr.io/sagernet/sing-box:${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
fi
- name: Build and release Docker images
uses: docker/build-push-action@v2
with:
platforms: linux/386,linux/amd64
platforms: linux/386,linux/amd64,linux/arm64,linux/s390x
target: dist
tags: ${{ steps.tag.outputs.tag }}
push: true
tags: |
${{ steps.tag.outputs.latest }}
${{ steps.tag.outputs.versioned }}
push: true

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@
/*.db
/site/
/bin/
/dist/
/dist/
/sing-box

View File

@@ -9,10 +9,12 @@ builds:
gcflags:
- all=-trimpath={{.Env.GOPATH}}
ldflags:
- -X github.com/sagernet/sing-box/constant.Commit={{ .ShortCommit }} -s -w -buildid=
- -s -w -buildid=
tags:
- with_gvisor
- with_quic
- with_wireguard
- with_utls
- with_clash_api
env:
- CGO_ENABLED=0
@@ -24,6 +26,7 @@ builds:
- linux_amd64_v3
- linux_arm64
- linux_arm_7
- linux_s390x
- windows_amd64_v1
- windows_amd64_v3
- windows_386

View File

@@ -8,13 +8,12 @@ ENV CGO_ENABLED=0
RUN set -ex \
&& apk add git build-base \
&& export COMMIT=$(git rev-parse --short HEAD) \
&& go build -v -trimpath -tags 'no_gvisor,with_quic,with_wireguard,with_acme' \
&& go build -v -trimpath -tags with_quic,with_wireguard,with_acme \
-o /go/bin/sing-box \
-ldflags "-X github.com/sagernet/sing-box/constant.Commit=${COMMIT} -w -s -buildid=" \
-ldflags "-s -w -buildid=" \
./cmd/sing-box
FROM alpine AS dist
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
RUN [ ! -e /etc/nsswitch.conf ] && echo 'hosts: files dns' > /etc/nsswitch.conf
RUN set -ex \
&& apk upgrade \
&& apk add bash tzdata ca-certificates \

View File

@@ -1,9 +1,8 @@
NAME = sing-box
COMMIT = $(shell git rev-parse --short HEAD)
TAGS ?= with_quic,with_wireguard,with_clash_api
PARAMS = -v -trimpath -tags '$(TAGS)' -ldflags \
'-X "github.com/sagernet/sing-box/constant.Commit=$(COMMIT)" \
-w -s -buildid='
TAGS ?= with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api
TAGS_TEST ?= with_gvisor,with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_shadowsocksr
PARAMS = -v -trimpath -tags "$(TAGS)" -ldflags "-s -w -buildid="
MAIN = ./cmd/sing-box
.PHONY: test release
@@ -61,14 +60,19 @@ release_install:
go install -v github.com/tcnksm/ghr@latest
test:
@go test -v . && \
pushd test && \
@go test -v ./... && \
cd test && \
go mod tidy && \
go test -v -tags with_quic,with_wireguard,with_grpc,with_ech,with_utls,with_shadowsocksr . && \
popd
go test -v -tags "$(TAGS_TEST)" .
test_stdio:
@go test -v ./... && \
cd test && \
go mod tidy && \
go test -v -tags "$(TAGS_TEST),force_stdio" .
clean:
rm -rf bin dist
rm -rf bin dist sing-box
rm -f $(shell go env GOPATH)/sing-box
update:

View File

@@ -4,6 +4,7 @@ import (
"context"
"net"
"github.com/sagernet/sing-box/common/urltest"
N "github.com/sagernet/sing/common/network"
)
@@ -12,6 +13,7 @@ type ClashServer interface {
Mode() string
StoreSelected() bool
CacheFile() ClashCacheFile
HistoryStorage() *urltest.HistoryStorage
RoutedConnection(ctx context.Context, conn net.Conn, metadata InboundContext, matchedRule Rule) (net.Conn, Tracker)
RoutedPacketConnection(ctx context.Context, conn N.PacketConn, metadata InboundContext, matchedRule Rule) (N.PacketConn, Tracker)
}
@@ -36,3 +38,13 @@ func OutboundTag(detour Outbound) string {
}
return detour.Tag()
}
type V2RayServer interface {
Service
StatsService() V2RayStatsService
}
type V2RayStatsService interface {
RoutedConnection(inbound string, outbound string, conn net.Conn) net.Conn
RoutedPacketConnection(inbound string, outbound string, conn N.PacketConn) N.PacketConn
}

View File

@@ -6,7 +6,7 @@ import (
"net/netip"
"github.com/sagernet/sing-box/common/process"
"github.com/sagernet/sing-dns"
"github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
@@ -38,16 +38,14 @@ type InboundContext struct {
// cache
InboundDetour string
LastInbound string
OriginDestination M.Socksaddr
DomainStrategy dns.DomainStrategy
SniffEnabled bool
SniffOverrideDestination bool
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *process.Info
InboundDetour string
LastInbound string
OriginDestination M.Socksaddr
InboundOptions option.InboundOptions
DestinationAddresses []netip.Addr
SourceGeoIPCode string
GeoIPCode string
ProcessInfo *process.Info
}
type inboundContextKey struct{}

View File

@@ -11,7 +11,7 @@ import (
"github.com/sagernet/sing/common/control"
N "github.com/sagernet/sing/common/network"
"golang.org/x/net/dns/dnsmessage"
mdns "github.com/miekg/dns"
)
type Router interface {
@@ -27,11 +27,11 @@ type Router interface {
GeoIPReader() *geoip.Reader
LoadGeosite(code string) (Rule, error)
Exchange(ctx context.Context, message *dnsmessage.Message) (*dnsmessage.Message, error)
Exchange(ctx context.Context, message *mdns.Msg) (*mdns.Msg, error)
Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error)
LookupDefault(ctx context.Context, domain string) ([]netip.Addr, error)
InterfaceBindManager() control.BindManager
InterfaceFinder() control.InterfaceFinder
DefaultInterface() string
AutoDetectInterface() bool
DefaultMark() int
@@ -41,7 +41,10 @@ type Router interface {
Rules() []Rule
ClashServer() ClashServer
SetClashServer(controller ClashServer)
SetClashServer(server ClashServer)
V2RayServer() V2RayServer
SetV2RayServer(server V2RayServer)
}
type Rule interface {
@@ -57,3 +60,7 @@ type DNSRule interface {
Rule
DisableCache() bool
}
type InterfaceUpdateListener interface {
InterfaceUpdated() error
}

View File

@@ -38,13 +38,25 @@ type myUpstreamHandlerWrapper struct {
}
func (w *myUpstreamHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
w.metadata.Destination = metadata.Destination
return w.connectionHandler(ctx, conn, w.metadata)
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.connectionHandler(ctx, conn, myMetadata)
}
func (w *myUpstreamHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
w.metadata.Destination = metadata.Destination
return w.packetHandler(ctx, conn, w.metadata)
myMetadata := w.metadata
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.packetHandler(ctx, conn, myMetadata)
}
func (w *myUpstreamHandlerWrapper) NewError(ctx context.Context, err error) {
@@ -78,13 +90,23 @@ func NewUpstreamContextHandler(
func (w *myUpstreamContextHandlerWrapper) NewConnection(ctx context.Context, conn net.Conn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
myMetadata.Destination = metadata.Destination
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.connectionHandler(ctx, conn, *myMetadata)
}
func (w *myUpstreamContextHandlerWrapper) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata M.Metadata) error {
myMetadata := ContextFrom(ctx)
myMetadata.Destination = metadata.Destination
if metadata.Source.IsValid() {
myMetadata.Source = metadata.Source
}
if metadata.Destination.IsValid() {
myMetadata.Destination = metadata.Destination
}
return w.packetHandler(ctx, conn, *myMetadata)
}

36
box.go
View File

@@ -31,6 +31,7 @@ type Box struct {
logger log.ContextLogger
logFile *os.File
clashServer adapter.ClashServer
v2rayServer adapter.V2RayServer
done chan struct{}
}
@@ -39,8 +40,14 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
logOptions := common.PtrValueOrDefault(options.Log)
var needClashAPI bool
if options.Experimental != nil && options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
needClashAPI = true
var needV2RayAPI bool
if options.Experimental != nil {
if options.Experimental.ClashAPI != nil && options.Experimental.ClashAPI.ExternalController != "" {
needClashAPI = true
}
if options.Experimental.V2RayAPI != nil && options.Experimental.V2RayAPI.Listen != "" {
needV2RayAPI = true
}
}
var logFactory log.Factory
@@ -90,8 +97,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
router, err := route.NewRouter(
ctx,
logFactory.NewLogger("router"),
logFactory.NewLogger("dns"),
logFactory,
common.PtrValueOrDefault(options.Route),
common.PtrValueOrDefault(options.DNS),
options.Inbounds,
@@ -149,6 +155,7 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
}
var clashServer adapter.ClashServer
var v2rayServer adapter.V2RayServer
if needClashAPI {
clashServer, err = experimental.NewClashServer(router, observableLogFactory, common.PtrValueOrDefault(options.Experimental.ClashAPI))
if err != nil {
@@ -156,15 +163,23 @@ func New(ctx context.Context, options option.Options) (*Box, error) {
}
router.SetClashServer(clashServer)
}
if needV2RayAPI {
v2rayServer, err = experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), common.PtrValueOrDefault(options.Experimental.V2RayAPI))
if err != nil {
return nil, E.Cause(err, "create v2ray api server")
}
router.SetV2RayServer(v2rayServer)
}
return &Box{
router: router,
inbounds: inbounds,
outbounds: outbounds,
createdAt: createdAt,
logFactory: logFactory,
logger: logFactory.NewLogger(""),
logger: logFactory.Logger(),
logFile: logFile,
clashServer: clashServer,
v2rayServer: v2rayServer,
done: make(chan struct{}),
}, nil
}
@@ -223,6 +238,12 @@ func (s *Box) start() error {
return E.Cause(err, "start clash api server")
}
}
if s.v2rayServer != nil {
err = s.v2rayServer.Start()
if err != nil {
return E.Cause(err, "start v2ray api server")
}
}
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
return nil
}
@@ -244,6 +265,11 @@ func (s *Box) Close() error {
s.router,
s.logFactory,
s.clashServer,
s.v2rayServer,
common.PtrOrNil(s.logFile),
)
}
func (s *Box) Router() adapter.Router {
return s.router
}

View File

@@ -69,6 +69,20 @@ func create() (*box.Box, context.CancelFunc, error) {
cancel()
return nil, nil, E.Cause(err, "create service")
}
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
defer func() {
signal.Stop(osSignals)
close(osSignals)
}()
go func() {
_, loaded := <-osSignals
if loaded {
cancel()
}
}()
err = instance.Start()
if err != nil {
cancel()
@@ -80,6 +94,7 @@ func create() (*box.Box, context.CancelFunc, error) {
func run() error {
osSignals := make(chan os.Signal, 1)
signal.Notify(osSignals, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
defer signal.Stop(osSignals)
for {
instance, cancel, err := create()
if err != nil {

View File

@@ -3,9 +3,9 @@ package main
import (
"os"
"runtime"
"runtime/debug"
C "github.com/sagernet/sing-box/constant"
F "github.com/sagernet/sing/common/format"
"github.com/spf13/cobra"
)
@@ -25,30 +25,40 @@ func init() {
}
func printVersion(cmd *cobra.Command, args []string) {
var version string
if !nameOnly {
version = "sing-box "
if nameOnly {
os.Stdout.WriteString(C.Version + "\n")
return
}
version += F.ToString(C.Version)
if C.Commit != "" {
version += "." + C.Commit
}
if !nameOnly {
version += " ("
version += runtime.Version()
version += ", "
version += runtime.GOOS
version += "/"
version += runtime.GOARCH
version += ", "
version += "CGO "
if C.CGO_ENABLED {
version += "enabled"
} else {
version += "disabled"
version := "sing-box version " + C.Version + "\n\n"
version += "Environment: " + runtime.Version() + " " + runtime.GOOS + "/" + runtime.GOARCH + "\n"
var tags string
var revision string
debugInfo, loaded := debug.ReadBuildInfo()
if loaded {
for _, setting := range debugInfo.Settings {
switch setting.Key {
case "-tags":
tags = setting.Value
case "vcs.revision":
revision = setting.Value
}
}
version += ")"
}
version += "\n"
if tags != "" {
version += "Tags: " + tags + "\n"
}
if revision != "" {
version += "Revision: " + revision + "\n"
}
if C.CGO_ENABLED {
version += "CGO: enabled\n"
} else {
version += "CGO: disabled\n"
}
os.Stdout.WriteString(version)
}

View File

@@ -3,6 +3,7 @@ package main
import (
"os"
_ "github.com/sagernet/sing-box/include"
"github.com/sagernet/sing-box/log"
"github.com/spf13/cobra"

View File

@@ -26,7 +26,7 @@ func WrapH2(err error) error {
if err == io.ErrUnexpectedEOF {
return io.EOF
}
if Contains(err, "client disconnected", "body closed by handler") {
if Contains(err, "client disconnected", "body closed by handler", "response body closed", "; CANCEL") {
return net.ErrClosed
}
return err

210
common/badtls/badtls.go Normal file
View File

@@ -0,0 +1,210 @@
//go:build go1.19 && !go1.20
package badtls
import (
"crypto/cipher"
"crypto/rand"
"crypto/tls"
"encoding/binary"
"io"
"net"
"reflect"
"sync"
"sync/atomic"
"unsafe"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
N "github.com/sagernet/sing/common/network"
)
type Conn struct {
*tls.Conn
writer N.ExtendedWriter
activeCall *int32
closeNotifySent *bool
version *uint16
rand io.Reader
halfAccess *sync.Mutex
halfError *error
cipher cipher.AEAD
explicitNonceLen int
halfPtr uintptr
halfSeq []byte
halfScratchBuf []byte
}
func Create(conn *tls.Conn) (TLSConn, error) {
if !handshakeComplete(conn) {
return nil, E.New("handshake not finished")
}
rawConn := reflect.Indirect(reflect.ValueOf(conn))
rawActiveCall := rawConn.FieldByName("activeCall")
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Int32 {
return nil, E.New("badtls: invalid active call")
}
activeCall := (*int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
rawHalfConn := rawConn.FieldByName("out")
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half conn")
}
rawVersion := rawConn.FieldByName("vers")
if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 {
return nil, E.New("badtls: invalid version")
}
version := (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr()))
rawCloseNotifySent := rawConn.FieldByName("closeNotifySent")
if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool {
return nil, E.New("badtls: invalid notify")
}
closeNotifySent := (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr()))
rawConfig := reflect.Indirect(rawConn.FieldByName("config"))
if !rawConfig.IsValid() || rawConfig.Kind() != reflect.Struct {
return nil, E.New("badtls: bad config")
}
config := (*tls.Config)(unsafe.Pointer(rawConfig.UnsafeAddr()))
randReader := config.Rand
if randReader == nil {
randReader = rand.Reader
}
rawHalfMutex := rawHalfConn.FieldByName("Mutex")
if !rawHalfMutex.IsValid() || rawHalfMutex.Kind() != reflect.Struct {
return nil, E.New("badtls: invalid half mutex")
}
halfAccess := (*sync.Mutex)(unsafe.Pointer(rawHalfMutex.UnsafeAddr()))
rawHalfError := rawHalfConn.FieldByName("err")
if !rawHalfError.IsValid() || rawHalfError.Kind() != reflect.Interface {
return nil, E.New("badtls: invalid half error")
}
halfError := (*error)(unsafe.Pointer(rawHalfError.UnsafeAddr()))
rawHalfCipherInterface := rawHalfConn.FieldByName("cipher")
if !rawHalfCipherInterface.IsValid() || rawHalfCipherInterface.Kind() != reflect.Interface {
return nil, E.New("badtls: invalid cipher interface")
}
rawHalfCipher := rawHalfCipherInterface.Elem()
aeadCipher, loaded := valueInterface(rawHalfCipher, false).(cipher.AEAD)
if !loaded {
return nil, E.New("badtls: invalid AEAD cipher")
}
var explicitNonceLen int
switch cipherName := reflect.Indirect(rawHalfCipher).Type().String(); cipherName {
case "tls.prefixNonceAEAD":
explicitNonceLen = aeadCipher.NonceSize()
case "tls.xorNonceAEAD":
default:
return nil, E.New("badtls: unknown cipher type: ", cipherName)
}
rawHalfSeq := rawHalfConn.FieldByName("seq")
if !rawHalfSeq.IsValid() || rawHalfSeq.Kind() != reflect.Array {
return nil, E.New("badtls: invalid seq")
}
halfSeq := rawHalfSeq.Bytes()
rawHalfScratchBuf := rawHalfConn.FieldByName("scratchBuf")
if !rawHalfScratchBuf.IsValid() || rawHalfScratchBuf.Kind() != reflect.Array {
return nil, E.New("badtls: invalid scratchBuf")
}
halfScratchBuf := rawHalfScratchBuf.Bytes()
return &Conn{
Conn: conn,
writer: bufio.NewExtendedWriter(conn.NetConn()),
activeCall: activeCall,
closeNotifySent: closeNotifySent,
version: version,
halfAccess: halfAccess,
halfError: halfError,
cipher: aeadCipher,
explicitNonceLen: explicitNonceLen,
rand: randReader,
halfPtr: rawHalfConn.UnsafeAddr(),
halfSeq: halfSeq,
halfScratchBuf: halfScratchBuf,
}, nil
}
func (c *Conn) WriteBuffer(buffer *buf.Buffer) error {
if buffer.Len() > maxPlaintext {
defer buffer.Release()
return common.Error(c.Write(buffer.Bytes()))
}
for {
x := atomic.LoadInt32(c.activeCall)
if x&1 != 0 {
return net.ErrClosed
}
if atomic.CompareAndSwapInt32(c.activeCall, x, x+2) {
break
}
}
defer atomic.AddInt32(c.activeCall, -2)
c.halfAccess.Lock()
defer c.halfAccess.Unlock()
if err := *c.halfError; err != nil {
return err
}
if *c.closeNotifySent {
return errShutdown
}
dataLen := buffer.Len()
dataBytes := buffer.Bytes()
outBuf := buffer.ExtendHeader(recordHeaderLen + c.explicitNonceLen)
outBuf[0] = 23
version := *c.version
if version == 0 {
version = tls.VersionTLS10
} else if version == tls.VersionTLS13 {
version = tls.VersionTLS12
}
binary.BigEndian.PutUint16(outBuf[1:], version)
var nonce []byte
if c.explicitNonceLen > 0 {
nonce = outBuf[5 : 5+c.explicitNonceLen]
if c.explicitNonceLen < 16 {
copy(nonce, c.halfSeq)
} else {
if _, err := io.ReadFull(c.rand, nonce); err != nil {
return err
}
}
}
if len(nonce) == 0 {
nonce = c.halfSeq
}
if *c.version == tls.VersionTLS13 {
buffer.FreeBytes()[0] = 23
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+1+c.cipher.Overhead()))
c.cipher.Seal(outBuf, nonce, outBuf[recordHeaderLen:recordHeaderLen+c.explicitNonceLen+dataLen+1], outBuf[:recordHeaderLen])
buffer.Extend(1 + c.cipher.Overhead())
} else {
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen))
additionalData := append(c.halfScratchBuf[:0], c.halfSeq...)
additionalData = append(additionalData, outBuf[:recordHeaderLen]...)
c.cipher.Seal(outBuf, nonce, dataBytes, additionalData)
buffer.Extend(c.cipher.Overhead())
binary.BigEndian.PutUint16(outBuf[3:], uint16(dataLen+c.explicitNonceLen+c.cipher.Overhead()))
}
incSeq(c.halfPtr)
return c.writer.WriteBuffer(buffer)
}
func (c *Conn) FrontHeadroom() int {
return recordHeaderLen + c.explicitNonceLen
}
func (c *Conn) RearHeadroom() int {
return 1 + c.cipher.Overhead()
}
func (c *Conn) WriterMTU() int {
return maxPlaintext
}
func (c *Conn) Upstream() any {
return c.Conn
}
func (c *Conn) UpstreamWriter() any {
return c.NetConn()
}

View File

@@ -0,0 +1,12 @@
//go:build !go1.19 || go1.20
package badtls
import (
"crypto/tls"
"os"
)
func Create(conn *tls.Conn) (TLSConn, error) {
return nil, os.ErrInvalid
}

13
common/badtls/conn.go Normal file
View File

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

26
common/badtls/link.go Normal file
View File

@@ -0,0 +1,26 @@
//go:build go1.19 && !go.1.20
package badtls
import (
"crypto/tls"
"reflect"
_ "unsafe"
)
const (
maxPlaintext = 16384 // maximum plaintext payload length
recordHeaderLen = 5 // record header length
)
//go:linkname errShutdown crypto/tls.errShutdown
var errShutdown error
//go:linkname handshakeComplete crypto/tls.(*Conn).handshakeComplete
func handshakeComplete(conn *tls.Conn) bool
//go:linkname incSeq crypto/tls.(*halfConn).incSeq
func incSeq(conn uintptr)
//go:linkname valueInterface reflect.valueInterface
func valueInterface(v reflect.Value, safe bool) any

View File

@@ -3,7 +3,6 @@ package dialer
import (
"context"
"net"
"net/netip"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -15,7 +14,7 @@ import (
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/database64128/tfo-go"
"github.com/database64128/tfo-go/v2"
)
var warnBindInterfaceOnUnsupportedPlatform = warning.New(
@@ -54,10 +53,13 @@ var warnTFOOnUnsupportedPlatform = warning.New(
)
type DefaultDialer struct {
dialer tfo.Dialer
udpDialer net.Dialer
dialer4 tfo.Dialer
dialer6 tfo.Dialer
udpDialer4 net.Dialer
udpDialer6 net.Dialer
udpListener net.ListenConfig
bindUDPAddr string
udpAddr4 string
udpAddr6 string
}
func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDialer {
@@ -65,25 +67,23 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
var listener net.ListenConfig
if options.BindInterface != "" {
warnBindInterfaceOnUnsupportedPlatform.Check()
bindFunc := control.BindToInterface(router.InterfaceBindManager(), options.BindInterface)
bindFunc := control.BindToInterface(router.InterfaceFinder(), options.BindInterface, -1)
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else if router.AutoDetectInterface() {
if C.IsWindows {
bindFunc := control.BindToInterfaceIndexFunc(func(network, address string) int {
return router.InterfaceMonitor().DefaultInterfaceIndex(M.ParseSocksaddr(address).Addr)
})
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else {
bindFunc := control.BindToInterfaceFunc(router.InterfaceBindManager(), func(network, address string) string {
return router.InterfaceMonitor().DefaultInterfaceName(M.ParseSocksaddr(address).Addr)
})
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
}
const useInterfaceName = C.IsLinux
bindFunc := control.BindToInterfaceFunc(router.InterfaceFinder(), func(network string, address string) (interfaceName string, interfaceIndex int) {
remoteAddr := M.ParseSocksaddr(address).Addr
if C.IsLinux {
return router.InterfaceMonitor().DefaultInterfaceName(remoteAddr), -1
} else {
return "", router.InterfaceMonitor().DefaultInterfaceIndex(remoteAddr)
}
})
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
} else if router.DefaultInterface() != "" {
bindFunc := control.BindToInterface(router.InterfaceBindManager(), router.DefaultInterface())
bindFunc := control.BindToInterface(router.InterfaceFinder(), router.DefaultInterface(), -1)
dialer.Control = control.Append(dialer.Control, bindFunc)
listener.Control = control.Append(listener.Control, bindFunc)
}
@@ -112,26 +112,47 @@ func NewDefault(router adapter.Router, options option.DialerOptions) *DefaultDia
if options.TCPFastOpen {
warnTFOOnUnsupportedPlatform.Check()
}
if !options.UDPFragment {
var udpFragment bool
if options.UDPFragment != nil {
udpFragment = *options.UDPFragment
} else {
udpFragment = options.UDPFragmentDefault
}
if !udpFragment {
dialer.Control = control.Append(dialer.Control, control.DisableUDPFragment())
listener.Control = control.Append(listener.Control, control.DisableUDPFragment())
}
var bindUDPAddr string
udpDialer := dialer
var bindAddress netip.Addr
if options.BindAddress != nil {
bindAddress = options.BindAddress.Build()
var (
dialer4 = dialer
udpDialer4 = dialer
udpAddr4 string
)
if options.Inet4BindAddress != nil {
bindAddr := options.Inet4BindAddress.Build()
dialer4.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
udpDialer4.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
udpAddr4 = M.SocksaddrFrom(bindAddr, 0).String()
}
if bindAddress.IsValid() {
dialer.LocalAddr = &net.TCPAddr{
IP: bindAddress.AsSlice(),
}
udpDialer.LocalAddr = &net.UDPAddr{
IP: bindAddress.AsSlice(),
}
bindUDPAddr = M.SocksaddrFrom(bindAddress, 0).String()
var (
dialer6 = dialer
udpDialer6 = dialer
udpAddr6 string
)
if options.Inet6BindAddress != nil {
bindAddr := options.Inet6BindAddress.Build()
dialer6.LocalAddr = &net.TCPAddr{IP: bindAddr.AsSlice()}
udpDialer6.LocalAddr = &net.UDPAddr{IP: bindAddr.AsSlice()}
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
}
return &DefaultDialer{
tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen},
tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen},
udpDialer4,
udpDialer6,
listener,
udpAddr4,
udpAddr6,
}
return &DefaultDialer{tfo.Dialer{Dialer: dialer, DisableTFO: !options.TCPFastOpen}, udpDialer, listener, bindUDPAddr}
}
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
@@ -140,11 +161,23 @@ func (d *DefaultDialer) DialContext(ctx context.Context, network string, address
}
switch N.NetworkName(network) {
case N.NetworkUDP:
return d.udpDialer.DialContext(ctx, network, address.String())
if !address.IsIPv6() {
return d.udpDialer4.DialContext(ctx, network, address.String())
} else {
return d.udpDialer6.DialContext(ctx, network, address.String())
}
}
if !address.IsIPv6() {
return DialSlowContext(&d.dialer4, ctx, network, address)
} else {
return DialSlowContext(&d.dialer6, ctx, network, address)
}
return d.dialer.DialContext(ctx, network, address.Unwrap().String())
}
func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.bindUDPAddr)
if !destination.IsIPv6() {
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr4)
} else {
return d.udpListener.ListenPacket(ctx, N.NetworkUDP, d.udpAddr6)
}
}

142
common/dialer/tfo.go Normal file
View File

@@ -0,0 +1,142 @@
package dialer
import (
"context"
"io"
"net"
"os"
"time"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/database64128/tfo-go/v2"
)
type slowOpenConn struct {
dialer *tfo.Dialer
ctx context.Context
network string
destination M.Socksaddr
conn net.Conn
create chan struct{}
err error
}
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)
}
return &slowOpenConn{
dialer: dialer,
ctx: ctx,
network: network,
destination: destination,
create: make(chan struct{}),
}, nil
}
func (c *slowOpenConn) Read(b []byte) (n int, err error) {
if c.conn == nil {
select {
case <-c.create:
if c.err != nil {
return 0, c.err
}
case <-c.ctx.Done():
return 0, c.ctx.Err()
}
}
return c.conn.Read(b)
}
func (c *slowOpenConn) Write(b []byte) (n int, err error) {
if c.conn == nil {
c.conn, err = c.dialer.DialContext(c.ctx, c.network, c.destination.String(), b)
if err != nil {
c.err = E.Cause(err, "dial tcp fast open")
}
close(c.create)
return
}
return c.conn.Write(b)
}
func (c *slowOpenConn) Close() error {
return common.Close(c.conn)
}
func (c *slowOpenConn) LocalAddr() net.Addr {
if c.conn == nil {
return M.Socksaddr{}
}
return c.conn.LocalAddr()
}
func (c *slowOpenConn) RemoteAddr() net.Addr {
if c.conn == nil {
return M.Socksaddr{}
}
return c.conn.RemoteAddr()
}
func (c *slowOpenConn) SetDeadline(t time.Time) error {
if c.conn == nil {
return os.ErrInvalid
}
return c.conn.SetDeadline(t)
}
func (c *slowOpenConn) SetReadDeadline(t time.Time) error {
if c.conn == nil {
return os.ErrInvalid
}
return c.conn.SetReadDeadline(t)
}
func (c *slowOpenConn) SetWriteDeadline(t time.Time) error {
if c.conn == nil {
return os.ErrInvalid
}
return c.conn.SetWriteDeadline(t)
}
func (c *slowOpenConn) Upstream() any {
return c.conn
}
func (c *slowOpenConn) ReaderReplaceable() bool {
return c.conn != nil
}
func (c *slowOpenConn) WriterReplaceable() bool {
return c.conn != nil
}
func (c *slowOpenConn) LazyHeadroom() bool {
return c.conn == nil
}
func (c *slowOpenConn) ReadFrom(r io.Reader) (n int64, err error) {
if c.conn != nil {
return bufio.Copy(c.conn, r)
}
return bufio.ReadFrom0(c, r)
}
func (c *slowOpenConn) WriteTo(w io.Writer) (n int64, err error) {
if c.conn == nil {
select {
case <-c.create:
if c.err != nil {
return 0, c.err
}
case <-c.ctx.Done():
return 0, c.ctx.Err()
}
}
return bufio.Copy(w, c.conn)
}

View File

@@ -329,6 +329,23 @@ func (c *ClientPacketConn) Write(b []byte) (n int, err error) {
return c.ExtendedConn.Write(b)
}
func (c *ClientPacketConn) ReadBuffer(buffer *buf.Buffer) (err error) {
if !c.responseRead {
err = c.readResponse()
if err != nil {
return
}
c.responseRead = true
}
var length uint16
err = binary.Read(c.ExtendedConn, binary.BigEndian, &length)
if err != nil {
return
}
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
return
}
func (c *ClientPacketConn) WriteBuffer(buffer *buf.Buffer) error {
if !c.requestWrite {
defer buffer.Release()
@@ -343,6 +360,11 @@ func (c *ClientPacketConn) FrontHeadroom() int {
return 2
}
func (c *ClientPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
err = c.ReadBuffer(buffer)
return
}
func (c *ClientPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
return c.WriteBuffer(buffer)
}
@@ -466,10 +488,7 @@ func (c *ClientPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Soc
if err != nil {
return
}
if buffer.FreeLen() < int(length) {
return destination, io.ErrShortBuffer
}
_, err = io.ReadFull(c.ExtendedConn, buffer.Extend(int(length)))
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
return
}

View File

@@ -43,7 +43,7 @@ func ParseProtocol(name string) (Protocol, error) {
func (p Protocol) newServer(conn net.Conn) (abstractSession, error) {
switch p {
case ProtocolSMux:
session, err := smux.Server(conn, nil)
session, err := smux.Server(conn, smuxConfig())
if err != nil {
return nil, err
}
@@ -58,7 +58,7 @@ func (p Protocol) newServer(conn net.Conn) (abstractSession, error) {
func (p Protocol) newClient(conn net.Conn) (abstractSession, error) {
switch p {
case ProtocolSMux:
session, err := smux.Client(conn, nil)
session, err := smux.Client(conn, smuxConfig())
if err != nil {
return nil, err
}
@@ -70,6 +70,12 @@ func (p Protocol) newClient(conn net.Conn) (abstractSession, error) {
}
}
func smuxConfig() *smux.Config {
config := smux.DefaultConfig()
config.KeepAliveDisabled = true
return config
}
func yaMuxConfig() *yamux.Config {
config := yamux.DefaultConfig()
config.LogOutput = io.Discard

View File

@@ -3,7 +3,6 @@ package mux
import (
"context"
"encoding/binary"
"io"
"net"
"github.com/sagernet/sing-box/adapter"
@@ -15,6 +14,7 @@ import (
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
"github.com/sagernet/sing/common/rw"
"github.com/sagernet/sing/common/task"
)
func NewConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, conn net.Conn, metadata adapter.InboundContext) error {
@@ -26,14 +26,21 @@ func NewConnection(ctx context.Context, router adapter.Router, errorHandler E.Ha
if err != nil {
return err
}
var stream net.Conn
for {
stream, err = session.Accept()
if err != nil {
return err
var group task.Group
group.Append0(func(ctx context.Context) error {
var stream net.Conn
for {
stream, err = session.Accept()
if err != nil {
return err
}
go newConnection(ctx, router, errorHandler, logger, stream, metadata)
}
go newConnection(ctx, router, errorHandler, logger, stream, metadata)
}
})
group.Cleanup(func() {
session.Close()
})
return group.Run(ctx)
}
func newConnection(ctx context.Context, router adapter.Router, errorHandler E.Handler, logger log.ContextLogger, stream net.Conn, metadata adapter.InboundContext) {
@@ -158,9 +165,6 @@ func (c *ServerPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksad
if err != nil {
return
}
if buffer.FreeLen() < int(length) {
return destination, io.ErrShortBuffer
}
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
if err != nil {
return
@@ -223,9 +227,6 @@ func (c *ServerPacketAddrConn) ReadPacket(buffer *buf.Buffer) (destination M.Soc
if err != nil {
return
}
if buffer.FreeLen() < int(length) {
return destination, io.ErrShortBuffer
}
_, err = buffer.ReadFullFrom(c.ExtendedConn, int(length))
if err != nil {
return

View File

@@ -28,11 +28,5 @@ type Info struct {
}
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
info, err := findProcessInfo(searcher, ctx, network, source, destination)
if err != nil {
if source.Addr().Is4In6() {
info, err = findProcessInfo(searcher, ctx, network, netip.AddrPortFrom(netip.AddrFrom4(source.Addr().As4()), source.Port()), destination)
}
}
return info, err
return findProcessInfo(searcher, ctx, network, source, destination)
}

View File

@@ -5,6 +5,8 @@ import (
"encoding/binary"
"net/netip"
"os"
"strconv"
"strings"
"syscall"
"unsafe"
@@ -29,6 +31,22 @@ func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, so
return &Info{ProcessPath: processName, UserId: -1}, nil
}
var structSize = func() int {
value, _ := syscall.Sysctl("kern.osrelease")
major, _, _ := strings.Cut(value, ".")
n, _ := strconv.ParseInt(major, 10, 64)
switch true {
case n >= 22:
return 408
default:
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
// size/offset are round up (aligned) to 8 bytes in darwin
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
return 384
}
}()
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
var spath string
switch network {
@@ -53,7 +71,7 @@ func findProcessName(network string, ip netip.Addr, port int) (string, error) {
// size/offset are round up (aligned) to 8 bytes in darwin
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
itemSize := 384
itemSize := structSize
if network == N.NetworkTCP {
// rup8(sizeof(xtcpcb_n))
itemSize += 208

View File

@@ -36,8 +36,8 @@ func (l *Listener) Accept() (net.Conn, error) {
}
if header != nil {
return &bufio.AddrConn{Conn: conn, Metadata: M.Metadata{
Source: M.SocksaddrFromNet(header.SourceAddr),
Destination: M.SocksaddrFromNet(header.DestinationAddr),
Source: M.SocksaddrFromNet(header.SourceAddr).Unwrap(),
Destination: M.SocksaddrFromNet(header.DestinationAddr).Unwrap(),
}}, nil
}
return conn, nil

View File

@@ -2,14 +2,11 @@ package redir
import (
"encoding/binary"
"net"
"net/netip"
"os"
"strconv"
"syscall"
"github.com/sagernet/sing/common/control"
E "github.com/sagernet/sing/common/exceptions"
F "github.com/sagernet/sing/common/format"
M "github.com/sagernet/sing/common/metadata"
"golang.org/x/sys/unix"
@@ -32,6 +29,18 @@ func TProxy(fd uintptr, isIPv6 bool) error {
return err
}
func TProxyWriteBack() control.Func {
return func(network, address string, conn syscall.RawConn) error {
return control.Raw(conn, func(fd uintptr) error {
if M.ParseSocksaddr(address).Addr.Is6() {
return syscall.SetsockoptInt(int(fd), syscall.SOL_IPV6, unix.IPV6_TRANSPARENT, 1)
} else {
return syscall.SetsockoptInt(int(fd), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1)
}
})
}
}
func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
controlMessages, err := unix.ParseSocketControlMessage(oob)
if err != nil {
@@ -46,79 +55,3 @@ func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
}
return netip.AddrPort{}, E.New("not found")
}
func DialUDP(lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPConn, error) {
rSockAddr, err := udpAddrToSockAddr(rAddr)
if err != nil {
return nil, err
}
lSockAddr, err := udpAddrToSockAddr(lAddr)
if err != nil {
return nil, err
}
fd, err := syscall.Socket(udpAddrFamily(lAddr, rAddr), syscall.SOCK_DGRAM, 0)
if err != nil {
return nil, err
}
if err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
syscall.Close(fd)
return nil, err
}
if err = syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil {
syscall.Close(fd)
return nil, err
}
if err = syscall.Bind(fd, lSockAddr); err != nil {
syscall.Close(fd)
return nil, err
}
if err = syscall.Connect(fd, rSockAddr); err != nil {
syscall.Close(fd)
return nil, err
}
fdFile := os.NewFile(uintptr(fd), F.ToString("net-udp-dial-", rAddr))
defer fdFile.Close()
c, err := net.FileConn(fdFile)
if err != nil {
syscall.Close(fd)
return nil, err
}
return c.(*net.UDPConn), nil
}
func udpAddrToSockAddr(addr *net.UDPAddr) (syscall.Sockaddr, error) {
switch {
case addr.IP.To4() != nil:
ip := [4]byte{}
copy(ip[:], addr.IP.To4())
return &syscall.SockaddrInet4{Addr: ip, Port: addr.Port}, nil
default:
ip := [16]byte{}
copy(ip[:], addr.IP.To16())
zoneID, err := strconv.ParseUint(addr.Zone, 10, 32)
if err != nil {
zoneID = 0
}
return &syscall.SockaddrInet6{Addr: ip, Port: addr.Port, ZoneId: uint32(zoneID)}, nil
}
}
func udpAddrFamily(lAddr, rAddr *net.UDPAddr) int {
if (lAddr == nil || lAddr.IP.To4() != nil) && (rAddr == nil || lAddr.IP.To4() != nil) {
return syscall.AF_INET
}
return syscall.AF_INET6
}

View File

@@ -3,19 +3,20 @@
package redir
import (
"net"
"net/netip"
"os"
"github.com/sagernet/sing/common/control"
)
func TProxy(fd uintptr, isIPv6 bool) error {
return os.ErrInvalid
}
func TProxyWriteBack() control.Func {
return nil
}
func GetOriginalDestinationFromOOB(oob []byte) (netip.AddrPort, error) {
return netip.AddrPort{}, os.ErrInvalid
}
func DialUDP(lAddr *net.UDPAddr, rAddr *net.UDPAddr) (*net.UDPConn, error) {
return nil, os.ErrInvalid
}

View File

@@ -11,9 +11,10 @@ import (
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
M "github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/common/task"
"golang.org/x/net/dns/dnsmessage"
mDNS "github.com/miekg/dns"
)
func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.InboundContext, error) {
@@ -44,18 +45,13 @@ func StreamDomainNameQuery(readCtx context.Context, reader io.Reader) (*adapter.
}
func DomainNameQuery(ctx context.Context, packet []byte) (*adapter.InboundContext, error) {
var parser dnsmessage.Parser
_, err := parser.Start(packet)
var msg mDNS.Msg
err := msg.Unpack(packet)
if err != nil {
return nil, err
}
question, err := parser.Question()
if err != nil {
if len(msg.Question) == 0 || msg.Question[0].Qclass != mDNS.ClassINET || !M.IsDomainName(msg.Question[0].Name) {
return nil, os.ErrInvalid
}
domain := question.Name.String()
if question.Class == dnsmessage.ClassINET && IsDomainName(domain) {
return &adapter.InboundContext{Protocol: C.ProtocolDNS /*, Domain: domain*/}, nil
}
return nil, os.ErrInvalid
return &adapter.InboundContext{Protocol: C.ProtocolDNS}, nil
}

View File

@@ -1,6 +0,0 @@
package sniff
import _ "unsafe" // for linkname
//go:linkname IsDomainName net.isDomainName
func IsDomainName(domain string) bool

View File

@@ -24,8 +24,7 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
if err != nil {
return nil, err
}
if typeByte&0x80 == 0 || typeByte&0x40 == 0 {
if typeByte&0x40 == 0 {
return nil, E.New("bad type byte")
}
var versionNumber uint32
@@ -145,9 +144,6 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
default:
return nil, E.New("bad packet number length")
}
if packetNumber != 0 {
return nil, E.New("bad packet number: ", packetNumber)
}
extHdrLen := hdrLen + int(packetNumberLength)
copy(newPacket[extHdrLen:hdrLen+4], packet[extHdrLen:])
data := newPacket[extHdrLen : int(packetLen)+hdrLen]
@@ -172,37 +168,76 @@ func QUICClientHello(ctx context.Context, packet []byte) (*adapter.InboundContex
if err != nil {
return nil, err
}
var frameType byte
var frameLen uint64
var fragments []struct {
offset uint64
length uint64
payload []byte
}
decryptedReader := bytes.NewReader(decrypted)
frameType, err := decryptedReader.ReadByte()
if err != nil {
return nil, err
}
for frameType == 0x0 {
// skip padding
for {
frameType, err = decryptedReader.ReadByte()
if err != nil {
return nil, err
if err == io.EOF {
break
}
switch frameType {
case 0x0:
continue
case 0x1:
continue
case 0x6:
var offset uint64
offset, err = qtls.ReadUvarint(decryptedReader)
if err != nil {
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
}
var length uint64
length, err = qtls.ReadUvarint(decryptedReader)
if err != nil {
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
}
index := len(decrypted) - decryptedReader.Len()
fragments = append(fragments, struct {
offset uint64
length uint64
payload []byte
}{offset, length, decrypted[index : index+int(length)]})
frameLen += length
_, err = decryptedReader.Seek(int64(length), io.SeekCurrent)
if err != nil {
return nil, err
}
default:
// ignore unknown frame type
}
}
if frameType != 0x6 {
// not crypto frame
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, nil
}
_, err = qtls.ReadUvarint(decryptedReader)
if err != nil {
return nil, err
}
_, err = qtls.ReadUvarint(decryptedReader)
if err != nil {
return nil, err
}
tlsHdr := make([]byte, 5)
tlsHdr[0] = 0x16
binary.BigEndian.PutUint16(tlsHdr[1:], uint16(0x0303))
binary.BigEndian.PutUint16(tlsHdr[3:], uint16(decryptedReader.Len()))
metadata, err := TLSClientHello(ctx, io.MultiReader(bytes.NewReader(tlsHdr), decryptedReader))
binary.BigEndian.PutUint16(tlsHdr[3:], uint16(frameLen))
var index uint64
var length int
var readers []io.Reader
readers = append(readers, bytes.NewReader(tlsHdr))
find:
for {
for _, fragment := range fragments {
if fragment.offset == index {
readers = append(readers, bytes.NewReader(fragment.payload))
index = fragment.offset + fragment.length
length++
continue find
}
}
if length == len(fragments) {
break
}
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, E.New("bad fragments")
}
metadata, err := TLSClientHello(ctx, io.MultiReader(readers...))
if err != nil {
return nil, err
return &adapter.InboundContext{Protocol: C.ProtocolQUIC}, err
}
metadata.Protocol = C.ProtocolQUIC
return metadata, nil

View File

@@ -19,6 +19,15 @@ func TestSniffQUICv1(t *testing.T) {
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
}
func TestSniffQUICFragment(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("cc00000001082e3d5d1b64040c55000044d0ccea69e773f6631c1d18b04ae9ee75fcfc34ef74fa62533c93534338a86f101a05d70e0697fb483063fa85db1c59ccfbda5c35234931d8524d8aac37eaaad649470a67794cd754b23c98695238b8363452333bc8c4858376b4166e001da2006e35cf98a91e11a56419b2786775284942d0f7163982f7c248867d12dd374957481dbc564013ff785e1916195eef671f725908f761099d992d69231336ba81d9e25fe2fa3a6eff4318a6ccf10176fc841a1b315f7b35c5b292266fc869d76ca533e7d14e86d82db2e22eacd350977e47d2e012d8a5891c5aaf2a0f4c2b2dae897c161e5b68cbb4dee952472bdc1e21504b8f02534ec4366ce3f8bf86efc78e0232778fbd554457567112abdcafcf6d4d8fcf35083c25d9495679614aba21696e338c62b585046cc55ba8c09c844361d889a47c3ea703b4e23545a9ab2c0bb369693a9ddfb5daffa85cf80fdd6ad66738664e5b0a551729b4955cff7255afcb04dee88c2f072c9de7400947a1bd9327ac5d012a33000ada021d4c03d249fb017d6ac9200b2f9436beab8183ddfbe2d8aee31ffb7df9e1cc181c1af80c39a89965d18ed12da8e3ebe2ae1fbe4b348f83ba19e3e3d1c9b22bcf03ab6ad9b30fe180623faa291ebad83bcd71d7b57f2f5e2f3b8e81d24fb70b2f2159239e8f21ffafef2747aba47d97ab4081e603c018b10678cf99cab1fb42156a14486fa435153979d7279fd22cd40af7088bfc7eff41af2f4b3c0c8864d0040d74dff427f7bffdb8c278474ea00311326cf4925471a8cf596cb92119f19e0f789490ba9cb77b98015a987d93e0324cf1a38b55109f00c3e6ddc5180fb107bf468323afec9bb49fd6a86418569789d66cafe3b8253c2aebb3af3782c1c54dd560487d031d28e6a6e23e159581bb1d47efc4da3fe1d169f9ffb0ca9ba61af0a38a92fde5bc5e6ec026e8378a6315a7b95abf1d2da790a391306ce74d0baf8e2ce648ca74c487f2c0a76a28a80cdf5bd34316eb607684fe7e6d9e83824a00e07660d0b90e3cddd61ebf10748263474afa88c300549e64ce2e90560bb1a12dee7e9484f729a8a4ee7c5651adb5194b3b3ae38e501567c7dbf36e7bb37a2c20b74655f47f2d9af18e52e9d4c9c9eee8e63745779b8f0b06f3a09d846ba62eb978ad77c85de1ee2fee3fbb4c2d283c73e1ccba56a4658e48a2665d200f7f9342f8e84c2ba490094a4f94feec89e42d2f654f564c2beb2997bafa1fc2c68ad8e160b63587d49abc31b834878d52acfb05fb73d0e059b206162e3c90b40c4bc08407ffcb3c08431895b691a3fea923f1f3b48db75d3e6b91fd319ffe4d486e0e14bd5c6affc838dee63d9e0b80f169b5e6c02c7321dcb20deb2b8e707b60e345a308d505bbf26a93d8f18b39d62632e9a77cbe48b3b32eb8819d6311a49820d40f5acbf0273c91c36b2269a03e72ee64df3dfb10ddefe73c64ef60870b2b77bd99dea655f5fe791b538a929a14d99f6d69685d72431ea5f0f4b27a044f2f575ab474fcc3857895934de1ca2581798eaef2c17fe5aaf2e6add97fa32997c7026f15c1b1ad0e6043ae506027a7c0242546fdc851cca39a204e56879f2cef838be8ec66e0f2292f8c862e06f810eb9b80c7a467ce6e90155206352c7f82b1173ba3b98d35bb72c259a60db20dd1a43fe6d7aef0265e6eaa5caafd9b64b448ff745a2046acbdb65cf2a5007809808a4828dc99097feedc734c236260c584")
require.NoError(t, err)
metadata, err := sniff.QUICClientHello(context.Background(), pkt)
require.NoError(t, err)
require.Equal(t, metadata.Domain, "cloudflare-quic.com")
}
func FuzzSniffQUIC(f *testing.F) {
f.Fuzz(func(t *testing.T, data []byte) {
sniff.QUICClientHello(context.Background(), data)

View File

@@ -5,7 +5,6 @@ import (
"context"
"io"
"net"
"os"
"time"
"github.com/sagernet/sing-box/adapter"
@@ -19,8 +18,11 @@ type (
PacketSniffer = func(ctx context.Context, packet []byte) (*adapter.InboundContext, error)
)
func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
err := conn.SetReadDeadline(time.Now().Add(C.ReadPayloadTimeout))
func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, timeout time.Duration, sniffers ...StreamSniffer) (*adapter.InboundContext, error) {
if timeout == 0 {
timeout = C.ReadPayloadTimeout
}
err := conn.SetReadDeadline(time.Now().Add(timeout))
if err != nil {
return nil, err
}
@@ -30,23 +32,25 @@ func PeekStream(ctx context.Context, conn net.Conn, buffer *buf.Buffer, sniffers
return nil, err
}
var metadata *adapter.InboundContext
var errors []error
for _, sniffer := range sniffers {
metadata, err = sniffer(ctx, bytes.NewReader(buffer.Bytes()))
if err != nil {
continue
if metadata != nil {
return metadata, nil
}
return metadata, nil
errors = append(errors, err)
}
return nil, os.ErrInvalid
return nil, E.Errors(errors...)
}
func PeekPacket(ctx context.Context, packet []byte, sniffers ...PacketSniffer) (*adapter.InboundContext, error) {
var errors []error
for _, sniffer := range sniffers {
sniffMetadata, err := sniffer(ctx, packet)
if err != nil {
continue
metadata, err := sniffer(ctx, packet)
if metadata != nil {
return metadata, nil
}
return sniffMetadata, nil
errors = append(errors, err)
}
return nil, os.ErrInvalid
return nil, E.Errors(errors...)
}

View File

@@ -7,11 +7,11 @@ import (
"crypto/tls"
"strings"
"github.com/sagernet/certmagic"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
E "github.com/sagernet/sing/common/exceptions"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/acme"
)

View File

@@ -2,10 +2,12 @@ package tls
import (
"context"
"crypto/tls"
"net"
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/common/badtls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/option"
M "github.com/sagernet/sing/common/metadata"
@@ -13,6 +15,9 @@ import (
)
func NewDialerFromOptions(router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
if !options.Enabled {
return dialer, nil
}
config, err := NewClient(router, serverAddress, options)
if err != nil {
return nil, err
@@ -21,12 +26,15 @@ func NewDialerFromOptions(router adapter.Router, dialer N.Dialer, serverAddress
}
func NewClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
if !options.Enabled {
return nil, nil
}
if options.ECH != nil && options.ECH.Enabled {
return newECHClient(router, serverAddress, options)
return NewECHClient(router, serverAddress, options)
} else if options.UTLS != nil && options.UTLS.Enabled {
return newUTLSClient(router, serverAddress, options)
return NewUTLSClient(router, serverAddress, options)
} else {
return newStdClient(serverAddress, options)
return NewSTDClient(serverAddress, options)
}
}
@@ -35,7 +43,17 @@ func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, e
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
defer cancel()
err := tlsConn.HandshakeContext(ctx)
return tlsConn, err
if err != nil {
return nil, err
}
if stdConn, isSTD := tlsConn.(*tls.Conn); isSTD {
var badConn badtls.TLSConn
badConn, err = badtls.Create(stdConn)
if err == nil {
return badConn, nil
}
}
return tlsConn, nil
}
type Dialer struct {

View File

@@ -15,10 +15,13 @@ type (
)
type Config interface {
ServerName() string
SetServerName(serverName string)
NextProtos() []string
SetNextProtos(nextProto []string)
Config() (*STDConfig, error)
Client(conn net.Conn) Conn
Clone() Config
}
type ServerConfig interface {

View File

@@ -11,36 +11,49 @@ import (
"net/netip"
"os"
cftls "github.com/sagernet/cloudflare-tls"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/option"
cftls "github.com/sagernet/sing-box/transport/cloudflaretls"
"github.com/sagernet/sing-dns"
E "github.com/sagernet/sing/common/exceptions"
mDNS "github.com/miekg/dns"
"golang.org/x/net/dns/dnsmessage"
)
type echClientConfig struct {
type ECHClientConfig struct {
config *cftls.Config
}
func (e *echClientConfig) NextProtos() []string {
func (e *ECHClientConfig) ServerName() string {
return e.config.ServerName
}
func (e *ECHClientConfig) SetServerName(serverName string) {
e.config.ServerName = serverName
}
func (e *ECHClientConfig) NextProtos() []string {
return e.config.NextProtos
}
func (e *echClientConfig) SetNextProtos(nextProto []string) {
func (e *ECHClientConfig) SetNextProtos(nextProto []string) {
e.config.NextProtos = nextProto
}
func (e *echClientConfig) Config() (*STDConfig, error) {
func (e *ECHClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for ECH")
}
func (e *echClientConfig) Client(conn net.Conn) Conn {
func (e *ECHClientConfig) Client(conn net.Conn) Conn {
return &echConnWrapper{cftls.Client(conn, e.config)}
}
func (e *ECHClientConfig) Clone() Config {
return &ECHClientConfig{
config: e.config.Clone(),
}
}
type echConnWrapper struct {
*cftls.Conn
}
@@ -63,7 +76,7 @@ func (c *echConnWrapper) ConnectionState() tls.ConnectionState {
}
}
func newECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func NewECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
@@ -163,22 +176,20 @@ func newECHClient(router adapter.Router, serverAddress string, options option.Ou
} else {
tlsConfig.GetClientECHConfigs = fetchECHClientConfig(router)
}
return &echClientConfig{&tlsConfig}, nil
return &ECHClientConfig{&tlsConfig}, nil
}
const typeHTTPS = 65
func fetchECHClientConfig(router adapter.Router) func(ctx context.Context, serverName string) ([]cftls.ECHConfig, error) {
return func(ctx context.Context, serverName string) ([]cftls.ECHConfig, error) {
message := &dnsmessage.Message{
Header: dnsmessage.Header{
message := &mDNS.Msg{
MsgHdr: mDNS.MsgHdr{
RecursionDesired: true,
},
Questions: []dnsmessage.Question{
Question: []mDNS.Question{
{
Name: dnsmessage.MustNewName(serverName + "."),
Type: typeHTTPS,
Class: dnsmessage.ClassINET,
Name: serverName + ".",
Qtype: mDNS.TypeHTTPS,
Qclass: mDNS.ClassINET,
},
},
}
@@ -186,19 +197,10 @@ func fetchECHClientConfig(router adapter.Router) func(ctx context.Context, serve
if err != nil {
return nil, err
}
if response.RCode != dnsmessage.RCodeSuccess {
return nil, dns.RCodeError(response.RCode)
if response.Rcode != mDNS.RcodeSuccess {
return nil, dns.RCodeError(response.Rcode)
}
content, err := response.Pack()
if err != nil {
return nil, err
}
var mMsg mDNS.Msg
err = mMsg.Unpack(content)
if err != nil {
return nil, err
}
for _, rr := range mMsg.Answer {
for _, rr := range response.Answer {
switch resource := rr.(type) {
case *mDNS.HTTPS:
for _, value := range resource.Value {

View File

@@ -8,6 +8,6 @@ import (
E "github.com/sagernet/sing/common/exceptions"
)
func newECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func NewECHClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`ECH is not included in this build, rebuild with -tags with_ech`)
}

50
common/tls/mkcert.go Normal file
View File

@@ -0,0 +1,50 @@
package tls
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"time"
)
func GenerateKeyPair(serverName string) (*tls.Certificate, error) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return nil, err
}
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, err
}
template := &x509.Certificate{
SerialNumber: serialNumber,
NotBefore: time.Now().Add(time.Hour * -1),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
Subject: pkix.Name{
CommonName: serverName,
},
DNSNames: []string{serverName},
}
publicDer, err := x509.CreateCertificate(rand.Reader, template, template, key.Public(), key)
if err != nil {
return nil, err
}
privateDer, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return nil, err
}
publicPem := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: publicDer})
privPem := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privateDer})
keyPair, err := tls.X509KeyPair(publicPem, privPem)
if err != nil {
return nil, err
}
return &keyPair, err
}

View File

@@ -2,11 +2,36 @@ package tls
import (
"context"
"crypto/tls"
"net"
"github.com/sagernet/sing-box/common/badtls"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
return newSTDServer(ctx, logger, options)
if !options.Enabled {
return nil, nil
}
return NewSTDServer(ctx, logger, options)
}
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
tlsConn := config.Server(conn)
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
defer cancel()
err := tlsConn.HandshakeContext(ctx)
if err != nil {
return nil, err
}
if stdConn, isSTD := tlsConn.(*tls.Conn); isSTD {
var badConn badtls.TLSConn
badConn, err = badtls.Create(stdConn)
if err == nil {
return badConn, nil
}
}
return tlsConn, nil
}

View File

@@ -11,11 +11,39 @@ import (
E "github.com/sagernet/sing/common/exceptions"
)
type stdClientConfig struct {
type STDClientConfig struct {
config *tls.Config
}
func newStdClient(serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func (s *STDClientConfig) ServerName() string {
return s.config.ServerName
}
func (s *STDClientConfig) SetServerName(serverName string) {
s.config.ServerName = serverName
}
func (s *STDClientConfig) NextProtos() []string {
return s.config.NextProtos
}
func (s *STDClientConfig) SetNextProtos(nextProto []string) {
s.config.NextProtos = nextProto
}
func (s *STDClientConfig) Config() (*STDConfig, error) {
return s.config, nil
}
func (s *STDClientConfig) Client(conn net.Conn) Conn {
return tls.Client(conn, s.config)
}
func (s *STDClientConfig) Clone() Config {
return &STDClientConfig{s.config.Clone()}
}
func NewSTDClient(serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
@@ -96,21 +124,5 @@ func newStdClient(serverAddress string, options option.OutboundTLSOptions) (Conf
}
tlsConfig.RootCAs = certPool
}
return &stdClientConfig{&tlsConfig}, nil
}
func (s *stdClientConfig) NextProtos() []string {
return s.config.NextProtos
}
func (s *stdClientConfig) SetNextProtos(nextProto []string) {
s.config.NextProtos = nextProto
}
func (s *stdClientConfig) Config() (*STDConfig, error) {
return s.config, nil
}
func (s *stdClientConfig) Client(conn net.Conn) Conn {
return tls.Client(conn, s.config)
return &STDClientConfig{&tlsConfig}, nil
}

View File

@@ -15,6 +15,8 @@ import (
"github.com/fsnotify/fsnotify"
)
var errInsecureUnused = E.New("tls: insecure unused")
type STDServerConfig struct {
config *tls.Config
logger log.Logger
@@ -26,6 +28,14 @@ type STDServerConfig struct {
watcher *fsnotify.Watcher
}
func (c *STDServerConfig) ServerName() string {
return c.config.ServerName
}
func (c *STDServerConfig) SetServerName(serverName string) {
c.config.ServerName = serverName
}
func (c *STDServerConfig) NextProtos() []string {
return c.config.NextProtos
}
@@ -34,97 +44,6 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
c.config.NextProtos = nextProto
}
func newSTDServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled {
return nil, nil
}
var tlsConfig *tls.Config
var acmeService adapter.Service
var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 {
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
if err != nil {
return nil, err
}
} else {
tlsConfig = &tls.Config{}
}
if options.ServerName != "" {
tlsConfig.ServerName = options.ServerName
}
if len(options.ALPN) > 0 {
tlsConfig.NextProtos = append(tlsConfig.NextProtos, options.ALPN...)
}
if options.MinVersion != "" {
minVersion, err := ParseTLSVersion(options.MinVersion)
if err != nil {
return nil, E.Cause(err, "parse min_version")
}
tlsConfig.MinVersion = minVersion
}
if options.MaxVersion != "" {
maxVersion, err := ParseTLSVersion(options.MaxVersion)
if err != nil {
return nil, E.Cause(err, "parse max_version")
}
tlsConfig.MaxVersion = maxVersion
}
if options.CipherSuites != nil {
find:
for _, cipherSuite := range options.CipherSuites {
for _, tlsCipherSuite := range tls.CipherSuites() {
if cipherSuite == tlsCipherSuite.Name {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
continue find
}
}
return nil, E.New("unknown cipher_suite: ", cipherSuite)
}
}
var certificate []byte
var key []byte
if acmeService == nil {
if options.Certificate != "" {
certificate = []byte(options.Certificate)
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
return nil, E.Cause(err, "read certificate")
}
certificate = content
}
if options.Key != "" {
key = []byte(options.Key)
} else if options.KeyPath != "" {
content, err := os.ReadFile(options.KeyPath)
if err != nil {
return nil, E.Cause(err, "read key")
}
key = content
}
if certificate == nil {
return nil, E.New("missing certificate")
}
if key == nil {
return nil, E.New("missing key")
}
keyPair, err := tls.X509KeyPair(certificate, key)
if err != nil {
return nil, E.Cause(err, "parse x509 key pair")
}
tlsConfig.Certificates = []tls.Certificate{keyPair}
}
return &STDServerConfig{
config: tlsConfig,
logger: logger,
acmeService: acmeService,
certificate: certificate,
key: key,
certificatePath: options.CertificatePath,
keyPath: options.KeyPath,
}, nil
}
func (c *STDServerConfig) Config() (*STDConfig, error) {
return c.config, nil
}
@@ -137,6 +56,12 @@ func (c *STDServerConfig) Server(conn net.Conn) Conn {
return tls.Server(conn, c.config)
}
func (c *STDServerConfig) Clone() Config {
return &STDServerConfig{
config: c.config.Clone(),
}
}
func (c *STDServerConfig) Start() error {
if c.acmeService != nil {
return c.acmeService.Start()
@@ -230,3 +155,104 @@ func (c *STDServerConfig) Close() error {
}
return nil
}
func NewSTDServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
if !options.Enabled {
return nil, nil
}
var tlsConfig *tls.Config
var acmeService adapter.Service
var err error
if options.ACME != nil && len(options.ACME.Domain) > 0 {
tlsConfig, acmeService, err = startACME(ctx, common.PtrValueOrDefault(options.ACME))
//nolint:staticcheck
if err != nil {
return nil, err
}
if options.Insecure {
return nil, errInsecureUnused
}
} else {
tlsConfig = &tls.Config{}
}
if options.ServerName != "" {
tlsConfig.ServerName = options.ServerName
}
if len(options.ALPN) > 0 {
tlsConfig.NextProtos = append(tlsConfig.NextProtos, options.ALPN...)
}
if options.MinVersion != "" {
minVersion, err := ParseTLSVersion(options.MinVersion)
if err != nil {
return nil, E.Cause(err, "parse min_version")
}
tlsConfig.MinVersion = minVersion
}
if options.MaxVersion != "" {
maxVersion, err := ParseTLSVersion(options.MaxVersion)
if err != nil {
return nil, E.Cause(err, "parse max_version")
}
tlsConfig.MaxVersion = maxVersion
}
if options.CipherSuites != nil {
find:
for _, cipherSuite := range options.CipherSuites {
for _, tlsCipherSuite := range tls.CipherSuites() {
if cipherSuite == tlsCipherSuite.Name {
tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, tlsCipherSuite.ID)
continue find
}
}
return nil, E.New("unknown cipher_suite: ", cipherSuite)
}
}
var certificate []byte
var key []byte
if acmeService == nil {
if options.Certificate != "" {
certificate = []byte(options.Certificate)
} else if options.CertificatePath != "" {
content, err := os.ReadFile(options.CertificatePath)
if err != nil {
return nil, E.Cause(err, "read certificate")
}
certificate = content
}
if options.Key != "" {
key = []byte(options.Key)
} else if options.KeyPath != "" {
content, err := os.ReadFile(options.KeyPath)
if err != nil {
return nil, E.Cause(err, "read key")
}
key = content
}
if certificate == nil && key == nil && options.Insecure {
tlsConfig.GetCertificate = func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
return GenerateKeyPair(info.ServerName)
}
} else {
if certificate == nil {
return nil, E.New("missing certificate")
} else if key == nil {
return nil, E.New("missing key")
}
keyPair, err := tls.X509KeyPair(certificate, key)
if err != nil {
return nil, E.Cause(err, "parse x509 key pair")
}
tlsConfig.Certificates = []tls.Certificate{keyPair}
}
}
return &STDServerConfig{
config: tlsConfig,
logger: logger,
acmeService: acmeService,
certificate: certificate,
key: key,
certificatePath: options.CertificatePath,
keyPath: options.KeyPath,
}, nil
}

View File

@@ -3,7 +3,6 @@
package tls
import (
"context"
"crypto/tls"
"crypto/x509"
"net"
@@ -17,35 +16,39 @@ import (
utls "github.com/refraction-networking/utls"
)
type utlsClientConfig struct {
type UTLSClientConfig struct {
config *utls.Config
id utls.ClientHelloID
}
func (e *utlsClientConfig) NextProtos() []string {
func (e *UTLSClientConfig) ServerName() string {
return e.config.ServerName
}
func (e *UTLSClientConfig) SetServerName(serverName string) {
e.config.ServerName = serverName
}
func (e *UTLSClientConfig) NextProtos() []string {
return e.config.NextProtos
}
func (e *utlsClientConfig) SetNextProtos(nextProto []string) {
func (e *UTLSClientConfig) SetNextProtos(nextProto []string) {
e.config.NextProtos = nextProto
}
func (e *utlsClientConfig) Config() (*STDConfig, error) {
func (e *UTLSClientConfig) Config() (*STDConfig, error) {
return nil, E.New("unsupported usage for uTLS")
}
func (e *utlsClientConfig) Client(conn net.Conn) Conn {
return &utlsConnWrapper{utls.UClient(conn, e.config, e.id)}
func (e *UTLSClientConfig) Client(conn net.Conn) Conn {
return &utlsConnWrapper{utls.UClient(conn, e.config.Clone(), e.id)}
}
type utlsConnWrapper struct {
*utls.UConn
}
func (c *utlsConnWrapper) HandshakeContext(ctx context.Context) error {
return c.Conn.Handshake()
}
func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState {
state := c.Conn.ConnectionState()
return tls.ConnectionState{
@@ -64,7 +67,14 @@ func (c *utlsConnWrapper) ConnectionState() tls.ConnectionState {
}
}
func newUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func (e *UTLSClientConfig) Clone() Config {
return &UTLSClientConfig{
config: e.config.Clone(),
id: e.id,
}
}
func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
var serverName string
if options.ServerName != "" {
serverName = options.ServerName
@@ -140,12 +150,22 @@ func newUTLSClient(router adapter.Router, serverAddress string, options option.O
id = utls.HelloChrome_Auto
case "firefox":
id = utls.HelloFirefox_Auto
case "edge":
id = utls.HelloEdge_Auto
case "safari":
id = utls.HelloSafari_Auto
case "360":
id = utls.Hello360_Auto
case "qq":
id = utls.HelloQQ_Auto
case "ios":
id = utls.HelloIOS_Auto
case "android":
id = utls.HelloAndroid_11_OkHttp
case "random":
id = utls.HelloRandomized
default:
return nil, E.New("unknown uTLS fingerprint: ", options.UTLS.Fingerprint)
}
return &utlsClientConfig{&tlsConfig, id}, nil
return &UTLSClientConfig{&tlsConfig, id}, nil
}

View File

@@ -8,6 +8,6 @@ import (
E "github.com/sagernet/sing/common/exceptions"
)
func newUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
func NewUTLSClient(router adapter.Router, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
}

View File

@@ -1,145 +0,0 @@
package trafficcontrol
import (
"io"
"net"
"sync"
"sync/atomic"
"github.com/sagernet/sing/common/buf"
"github.com/sagernet/sing/common/bufio"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
)
type Manager[U comparable] struct {
access sync.Mutex
users map[U]*Traffic
}
type Traffic struct {
Upload uint64
Download uint64
}
func NewManager[U comparable]() *Manager[U] {
return &Manager[U]{
users: make(map[U]*Traffic),
}
}
func (m *Manager[U]) Reset() {
m.users = make(map[U]*Traffic)
}
func (m *Manager[U]) TrackConnection(user U, conn net.Conn) net.Conn {
m.access.Lock()
defer m.access.Unlock()
var traffic *Traffic
if t, loaded := m.users[user]; loaded {
traffic = t
} else {
traffic = new(Traffic)
m.users[user] = traffic
}
return &TrackConn{conn, traffic}
}
func (m *Manager[U]) TrackPacketConnection(user U, conn N.PacketConn) N.PacketConn {
m.access.Lock()
defer m.access.Unlock()
var traffic *Traffic
if t, loaded := m.users[user]; loaded {
traffic = t
} else {
traffic = new(Traffic)
m.users[user] = traffic
}
return &TrackPacketConn{conn, traffic}
}
func (m *Manager[U]) ReadTraffics() map[U]Traffic {
m.access.Lock()
defer m.access.Unlock()
trafficMap := make(map[U]Traffic)
for user, traffic := range m.users {
upload := atomic.SwapUint64(&traffic.Upload, 0)
download := atomic.SwapUint64(&traffic.Download, 0)
if upload == 0 && download == 0 {
continue
}
trafficMap[user] = Traffic{
Upload: upload,
Download: download,
}
}
return trafficMap
}
type TrackConn struct {
net.Conn
*Traffic
}
func (c *TrackConn) Read(p []byte) (n int, err error) {
n, err = c.Conn.Read(p)
if n > 0 {
atomic.AddUint64(&c.Upload, uint64(n))
}
return
}
func (c *TrackConn) Write(p []byte) (n int, err error) {
n, err = c.Conn.Write(p)
if n > 0 {
atomic.AddUint64(&c.Download, uint64(n))
}
return
}
func (c *TrackConn) WriteTo(w io.Writer) (n int64, err error) {
n, err = bufio.Copy(w, c.Conn)
if n > 0 {
atomic.AddUint64(&c.Upload, uint64(n))
}
return
}
func (c *TrackConn) ReadFrom(r io.Reader) (n int64, err error) {
n, err = bufio.Copy(c.Conn, r)
if n > 0 {
atomic.AddUint64(&c.Download, uint64(n))
}
return
}
func (c *TrackConn) Upstream() any {
return c.Conn
}
type TrackPacketConn struct {
N.PacketConn
*Traffic
}
func (c *TrackPacketConn) ReadPacket(buffer *buf.Buffer) (M.Socksaddr, error) {
destination, err := c.PacketConn.ReadPacket(buffer)
if err == nil {
atomic.AddUint64(&c.Upload, uint64(buffer.Len()))
}
return destination, err
}
func (c *TrackPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
n := buffer.Len()
err := c.PacketConn.WritePacket(buffer, destination)
if err == nil {
atomic.AddUint64(&c.Download, uint64(n))
}
return err
}
func (c *TrackPacketConn) Upstream() any {
return c.PacketConn
}

View File

@@ -3,3 +3,5 @@ package constant
import E "github.com/sagernet/sing/common/exceptions"
var ErrTLSRequired = E.New("TLS required")
var ErrQUICNotIncluded = E.New(`QUIC is not included in this build, rebuild with -tags with_quic`)

View File

@@ -25,4 +25,5 @@ const (
const (
TypeSelector = "selector"
TypeURLTest = "urltest"
)

View File

@@ -1,5 +0,0 @@
//go:build with_quic
package constant
const QUIC_AVAILABLE = true

View File

@@ -1,9 +0,0 @@
//go:build !with_quic
package constant
import E "github.com/sagernet/sing/common/exceptions"
const QUIC_AVAILABLE = false
var ErrQUICNotIncluded = E.New(`QUIC is not included in this build, rebuild with -tags with_quic`)

View File

@@ -3,10 +3,11 @@ package constant
import "time"
const (
TCPTimeout = 5 * time.Second
ReadPayloadTimeout = 300 * time.Millisecond
DNSTimeout = 10 * time.Second
QUICTimeout = 30 * time.Second
STUNTimeout = 15 * time.Second
UDPTimeout = 5 * time.Minute
TCPTimeout = 5 * time.Second
ReadPayloadTimeout = 300 * time.Millisecond
DNSTimeout = 10 * time.Second
QUICTimeout = 30 * time.Second
STUNTimeout = 15 * time.Second
UDPTimeout = 5 * time.Minute
DefaultURLTestInterval = 1 * time.Minute
)

View File

@@ -1,6 +1,3 @@
package constant
var (
Version = "1.1-beta4"
Commit = ""
)
var Version = "1.1"

View File

@@ -1,3 +1,225 @@
#### 1.1
* Fix close clash cache
Important changes since 1.0:
* Add support for use with android VPNService
* Add tun support for WireGuard outbound
* Add system tun stack
* Add comment filter for config
* Add option for allow optional proxy protocol header
* Add Clash mode and persistence support
* Add TLS ECH and uTLS support for outbound TLS options
* Add internal simple-obfs and v2ray-plugin
* Add ShadowsocksR outbound
* Add VLESS outbound and XUDP
* Skip wait for hysteria tcp handshake response
* Add v2ray mux support for all inbound
* Add XUDP support for VMess
* Improve websocket writer
* Refine tproxy write back
* Fix DNS leak caused by
Windows' ordinary multihomed DNS resolution behavior
* Add sniff_timeout listen option
* Add custom route support for tun
* Add option for custom wireguard reserved bytes
* Split bind_address into ipv4 and ipv6
* Add ShadowTLS v1 and v2 support
#### 1.1-rc1
* Fix TLS config for h2 server
* Fix crash when input bad method in shadowsocks multi-user inbound
* Fix listen UDP
* Fix check invalid packet on macOS
#### 1.1-beta18
* Enhance defense against active probe for shadowtls server **1**
**1**:
The `fallback_after` option has been removed.
#### 1.1-beta17
* Fix shadowtls server **1**
*1*:
Added [fallback_after](/configuration/inbound/shadowtls#fallback_after) option.
#### 1.0.7
* Add support for new x/h2 deadline
* Fix copy pipe
* Fix decrypt xplus packet
* Fix macOS Ventura process name match
* Fix smux keepalive
* Fix vmess request buffer
* Fix h2c transport
* Fix tor geoip
* Fix udp connect for mux client
* Fix default dns transport strategy
#### 1.1-beta16
* Improve shadowtls server
* Fix default dns transport strategy
* Update uTLS to v1.2.0
#### 1.1-beta15
* Add support for new x/h2 deadline
* Fix udp connect for mux client
* Fix dns buffer
* Fix quic dns retry
* Fix create TLS config
* Fix websocket alpn
* Fix tor geoip
#### 1.1-beta14
* Add multi-user support for hysteria inbound **1**
* Add custom tls client support for std grpc
* Fix smux keep alive
* Fix vmess request buffer
* Fix default local DNS server behavior
* Fix h2c transport
*1*:
The `auth` and `auth_str` fields have been replaced by the `users` field.
#### 1.1-beta13
* Add custom worker count option for WireGuard outbound
* Split bind_address into ipv4 and ipv6
* Move WFP manipulation to strict route
* Fix WireGuard outbound panic when close
* Fix macOS Ventura process name match
* Fix QUIC connection migration by @HyNetwork
* Fix handling QUIC client SNI by @HyNetwork
#### 1.1-beta12
* Fix uTLS config
* Update quic-go to v0.30.0
* Update cloudflare-tls to go1.18.7
#### 1.1-beta11
* Add option for custom wireguard reserved bytes
* Fix shadowtls v2
* Fix h3 dns transport
* Fix copy pipe
* Fix decrypt xplus packet
* Fix v2ray api
* Suppress no network error
* Improve local dns transport
#### 1.1-beta10
* Add [sniff_timeout](/configuration/shared/listen#sniff_timeout) listen option
* Add [custom route](/configuration/inbound/tun#inet4_route_address) support for tun **1**
* Fix interface monitor
* Fix websocket headroom
* Fix uTLS handshake
* Fix ssh outbound
* Fix sniff fragmented quic client hello
* Fix DF for hysteria
* Fix naive overflow
* Check destination before udp connect
* Update uTLS to v1.1.5
* Update tfo-go to v2.0.2
* Update fsnotify to v1.6.0
* Update grpc to v1.50.1
*1*:
The `strict_route` on windows is removed.
#### 1.0.6
* Fix ssh outbound
* Fix sniff fragmented quic client hello
* Fix naive overflow
* Check destination before udp connect
#### 1.1-beta9
* Fix windows route **1**
* Add [v2ray statistics api](/configuration/experimental#v2ray-api-fields)
* Add ShadowTLS v2 support **2**
* Fixes and improvements
**1**:
* Fix DNS leak caused by
Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29)
* Flush Windows DNS cache when start/close
**2**:
See [ShadowTLS inbound](/configuration/inbound/shadowtls#version)
and [ShadowTLS outbound](/configuration/outbound/shadowtls#version)
#### 1.1-beta8
* Fix leaks on close
* Improve websocket writer
* Refine tproxy write back
* Refine 4in6 processing
* Fix shadowsocks plugins
* Fix missing source address from transport connection
* Fix fqdn socks5 outbound connection
* Fix read source address from grpc-go
#### 1.0.5
* Fix missing source address from transport connection
* Fix fqdn socks5 outbound connection
* Fix read source address from grpc-go
#### 1.1-beta7
* Add v2ray mux and XUDP support for VMess inbound
* Add XUDP support for VMess outbound
* Disable DF on direct outbound by default
* Fix bugs in 1.1-beta6
#### 1.1-beta6
* Add [URLTest outbound](/configuration/outbound/urltest)
* Fix bugs in 1.1-beta5
#### 1.1-beta5
* Print tags in version command
* Redirect clash hello to external ui
* Move shadowsocksr implementation to clash
* Make gVisor optional **1**
* Refactor to miekg/dns
* Refactor bind control
* Fix build on go1.18
* Fix clash store-selected
* Fix close grpc conn
* Fix port rule match logic
* Fix clash api proxy type
*1*:
The build tag `no_gvisor` is replaced by `with_gvisor`.
The default tun stack is changed to system.
#### 1.0.4
* Fix close grpc conn
* Fix port rule match logic
* Fix clash api proxy type
#### 1.1-beta4
* Add internal simple-obfs and v2ray-plugin [Shadowsocks plugins](/configuration/outbound/shadowsocks#plugin)

View File

@@ -104,8 +104,10 @@
The default rule uses the following matching logic:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr`) &&
`other fields`
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound

View File

@@ -103,8 +103,10 @@
默认规则使用以下匹配逻辑:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite`) &&
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr`) &&
`other fields`
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound

View File

@@ -9,24 +9,39 @@
"external_controller": "127.0.0.1:9090",
"external_ui": "folder",
"secret": "",
"direct_io": false,
"default_mode": "rule",
"store_selected": false,
"cache_file": "cache.db"
},
"v2ray_api": {
"listen": "127.0.0.1:8080",
"stats": {
"enabled": true,
"direct_io": false,
"inbounds": [
"socks-in"
],
"outbounds": [
"proxy",
"direct"
]
}
}
}
}
```
!!! note ""
Traffic statistics and connection management can degrade performance.
### Clash API Fields
!!! error ""
Clash API is not included by default, see [Installation](/#installation).
!!! note ""
Traffic statistics and connection management will disable TCP splice in linux and reduce performance, use at your own risk.
#### external_controller
RESTful web API listening address. Clash API will be disabled if empty.
@@ -43,6 +58,10 @@ Secret for the RESTful API (optional)
Authenticate by spedifying HTTP header `Authorization: Bearer ${secret}`
ALWAYS set a secret if RESTful API is listening on 0.0.0.0
#### direct_io
Allows lossless relays like splice without real-time traffic reporting.
#### default_mode
Default mode in clash, `rule` will be used if empty.
@@ -59,4 +78,34 @@ Store selected outbound for the `Selector` outbound in cache file.
#### cache_file
Cache file path, `cache.db` will be used if empty.
Cache file path, `cache.db` will be used if empty.
### V2Ray API Fields
!!! error ""
V2Ray API is not included by default, see [Installation](/#installation).
#### listen
gRPC API listening address. V2Ray API will be disabled if empty.
#### stats
Traffic statistics service settings.
#### stats.enabled
Enable statistics service.
#### stats.direct_io
Allows lossless relays like splice without real-time traffic reporting.
#### stats.inbounds
Inbound list to count traffic.
#### stats.outbounds
Outbound list to count traffic.

View File

@@ -9,24 +9,39 @@
"external_controller": "127.0.0.1:9090",
"external_ui": "folder",
"secret": "",
"direct_io": false,
"default_mode": "rule",
"store_selected": false,
"cache_file": "cache.db"
},
"v2ray_api": {
"listen": "127.0.0.1:8080",
"stats": {
"enabled": true,
"direct_io": false,
"inbounds": [
"socks-in"
],
"outbounds": [
"proxy",
"direct"
]
}
}
}
}
```
!!! note ""
流量统计和连接管理会降低性能。
### Clash API 字段
!!! error ""
默认安装不包含 Clash API参阅 [安装](/zh/#_2)。
!!! note ""
流量统计和连接管理将禁用 Linux 中的 TCP splice 并降低性能,使用风险自负。
#### external_controller
RESTful web API 监听地址。如果为空,则禁用 Clash API。
@@ -41,6 +56,10 @@ RESTful API 的密钥(可选)
通过指定 HTTP 标头 `Authorization: Bearer ${secret}` 进行身份验证
如果 RESTful API 正在监听 0.0.0.0,请始终设置一个密钥。
#### direct_io
允许像 splice 这样的没有实时流量报告的无损中继。
#### default_mode
Clash 中的默认模式,默认使用 `rule`
@@ -57,4 +76,34 @@ Clash 中的默认模式,默认使用 `rule`。
#### cache_file
缓存文件路径,默认使用`cache.db`
缓存文件路径,默认使用`cache.db`
### V2Ray API 字段
!!! error ""
默认安装不包含 V2Ray API参阅 [安装](/zh/#_2)。
#### listen
gRPC API 监听地址。如果为空,则禁用 V2Ray API。
#### stats
流量统计服务设置。
#### stats.enabled
启用统计服务。
#### stats.direct_io
允许像 splice 这样的没有实时流量报告的无损中继。
#### stats.inbounds
统计流量的入站列表。
#### stats.outbounds
统计流量的出站列表。

View File

@@ -12,8 +12,15 @@
"down": "100 Mbps",
"down_mbps": 100,
"obfs": "fuck me till the daylight",
"auth": "",
"auth_str": "password",
"users": [
{
"name": "sekai",
"auth": "",
"auth_str": "password"
}
],
"recv_window_conn": 0,
"recv_window_client": 0,
"max_conn_client": 0,
@@ -61,11 +68,19 @@ Supported units (case sensitive, b = bits, B = bytes, 8b=1B):
Obfuscated password.
#### auth
#### users
Hysteria users
#### users.auth
==Required if `auth_str` is empty==
Authentication password, in base64.
#### auth_str
#### users.auth_str
==Required if `auth` is empty==
Authentication password.

View File

@@ -12,8 +12,15 @@
"down": "100 Mbps",
"down_mbps": 100,
"obfs": "fuck me till the daylight",
"auth": "",
"auth_str": "password",
"users": [
{
"name": "sekai",
"auth": "",
"auth_str": "password"
}
],
"recv_window_conn": 0,
"recv_window_client": 0,
"max_conn_client": 0,
@@ -61,11 +68,19 @@
混淆密码。
#### auth
#### users
Hysteria 用户
#### users.auth
==与 auth_str 必填一个==
base64 编码的认证密码。
#### auth_str
#### users.auth_str
==与 auth 必填一个==
认证密码。

View File

@@ -7,6 +7,8 @@
... // Listen Fields
"version": 2,
"password": "fuck me till the daylight",
"handshake": {
"server": "google.com",
"server_port": 443,
@@ -20,12 +22,25 @@
See [Listen Fields](/configuration/shared/listen) for details.
### Fields
#### version
ShadowTLS protocol version.
| Value | Protocol Version |
|---------------|-----------------------------------------------------------------------------------------|
| `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) |
| `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) |
#### password
Set password.
Only available in the ShadowTLS v2 protocol.
#### handshake
==Required==
Handshake server address and [dial options](/configuration/shared/dial).
Handshake server address and [Dial options](/configuration/shared/dial).

View File

@@ -7,6 +7,8 @@
... // 监听字段
"version": 2,
"password": "fuck me till the daylight",
"handshake": {
"server": "google.com",
"server_port": 443,
@@ -22,6 +24,21 @@
### 字段
#### version
ShadowTLS 协议版本。
| 值 | 协议版本 |
|---------------|-----------------------------------------------------------------------------------------|
| `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) |
| `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) |
#### password
设置密码。
仅在 ShadowTLS v2 协议中可用。
#### handshake
==必填==

View File

@@ -8,15 +8,22 @@
{
"type": "tun",
"tag": "tun-in",
"interface_name": "tun0",
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"mtu": 9000,
"auto_route": true,
"strict_route": true,
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
"inet6_route_address": [
"::/1",
"8000::/1"
],
"endpoint_independent_nat": false,
"stack": "gvisor",
"stack": "system",
"include_uid": [
0
],
@@ -39,8 +46,8 @@
"exclude_package": [
"com.android.captiveportallogin"
],
... // Listen Fields
...
// Listen Fields
}
```
@@ -86,7 +93,9 @@ Set the default route to the Tun.
#### strict_route
Enforce strict routing rules in Linux when `auto_route` is enabled:
Enforce strict routing rules when `auto_route` is enabled:
*In Linux*:
* Let unsupported network unreachable
* Route all connections to tun
@@ -94,6 +103,21 @@ Enforce strict routing rules in Linux when `auto_route` is enabled:
It prevents address leaks and makes DNS hijacking work on Android and Linux with systemd-resolved, but your device will
not be accessible by others.
*In Windows*:
* Add firewall rules to prevent DNS leak caused by
Windows' [ordinary multihomed DNS resolution behavior](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29)
It may prevent some applications (such as VirtualBox) from working properly in certain situations.
#### inet4_route_address
Use custom routes instead of default when `auto_route` is enabled.
#### inet6_route_address
Use custom routes instead of default when `auto_route` is enabled.
#### endpoint_independent_nat
!!! info ""
@@ -112,15 +136,15 @@ UDP NAT expiration time in seconds, default is 300 (5 minutes).
TCP/IP stack.
| Stack | Description | Status |
|------------------|--------------------------------------------------------------------------------|-------------------|
| gVisor (default) | Based on [google/gvisor](https://github.com/google/gvisor) | recommended |
| system | Less compatibility and sometimes better performance. | recommended |
| LWIP | Based on [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | upstream archived |
| Stack | Description | Status |
|------------------|----------------------------------------------------------------------------------|-------------------|
| system (default) | Sometimes better performance | recommended |
| gVisor | Better compatibility, based on [google/gvisor](https://github.com/google/gvisor) | recommended |
| LWIP | Based on [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | upstream archived |
!!! warning ""
The LWIP stack is not included by default, see [Installation](/#installation).
gVisor and LWIP stacks is not included by default, see [Installation](/#installation).
#### include_uid

View File

@@ -8,15 +8,22 @@
{
"type": "tun",
"tag": "tun-in",
"interface_name": "tun0",
"inet4_address": "172.19.0.1/30",
"inet6_address": "fdfe:dcba:9876::1/126",
"mtu": 9000,
"auto_route": true,
"strict_route": true,
"inet4_route_address": [
"0.0.0.0/1",
"128.0.0.0/1"
],
"inet6_route_address": [
"::/1",
"8000::/1"
],
"endpoint_independent_nat": false,
"stack": "gvisor",
"stack": "system",
"include_uid": [
0
],
@@ -39,8 +46,8 @@
"exclude_package": [
"com.android.captiveportallogin"
],
... // 监听字段
...
// 监听字段
}
```
@@ -86,13 +93,31 @@ tun 接口的 IPv6 前缀。
#### strict_route
在 Linux 中启用 `auto_route` 时执行严格的路由规则。
启用 `auto_route` 时执行严格的路由规则。
*在 Linux 中*:
* 让不支持的网络无法到达
* 将所有连接路由到 tun
它可以防止地址泄漏,并使 DNS 劫持在 Android 和使用 systemd-resolved 的 Linux 上工作,但你的设备将无法其他设备被访问。
*在 Windows 中*:
* 添加防火墙规则以阻止 Windows
的 [普通多宿主 DNS 解析行为](https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/dd197552%28v%3Dws.10%29)
造成的 DNS 泄露
它可能会使某些应用程序(如 VirtualBox在某些情况下无法正常工作。
#### inet4_route_address
启用 `auto_route` 时使用自定义路由而不是默认路由。
#### inet6_route_address
启用 `auto_route` 时使用自定义路由而不是默认路由。
#### endpoint_independent_nat
启用独立于端点的 NAT。
@@ -107,15 +132,15 @@ UDP NAT 过期时间,以秒为单位,默认为 3005 分钟)。
TCP/IP 栈。
| 栈 | 描述 | 状态 |
|------------------|--------------------------------------------------------------------------|-------|
| gVisor (default) | 基于 [google/gvisor](https://github.com/google/gvisor) | 推荐 |
| system | 兼容性较差,有时性能更好。 | 推荐 |
| LWIP | 基于 [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | 上游已存档 |
| 栈 | 描述 | 状态 |
|-------------|--------------------------------------------------------------------------|-------|
| system (默认) | 有时性能更好 | 推荐 |
| gVisor | 兼容性较好,基于 [google/gvisor](https://github.com/google/gvisor) | 推荐 |
| LWIP | 基于 [eycorsican/go-tun2socks](https://github.com/eycorsican/go-tun2socks) | 上游已存档 |
!!! warning ""
默认安装不包含 LWIP 栈,请参阅 [安装](/zh/#_2)。
默认安装不包含 gVisor 和 LWIP 栈,请参阅 [安装](/zh/#_2)。
#### include_uid
@@ -145,10 +170,10 @@ TCP/IP 栈。
限制被路由的 Android 用户。
| 常用用户 | ID |
| 常用用户 | ID |
|--|-----|
| 您 | 0 |
| 工作资料 | 10 |
| 您 | 0 |
| 工作资料 | 10 |
#### include_package
@@ -160,4 +185,4 @@ TCP/IP 栈。
### 监听字段
参阅 [监听字段](/zh/configuration/shared/listen/)。
参阅 [监听字段](/zh/configuration/shared/listen/)。

View File

@@ -15,21 +15,24 @@
### Fields
| Type | Format |
|---------------|------------------------------|
| `direct` | [Direct](./direct) |
| `block` | [Block](./block) |
| `socks` | [SOCKS](./socks) |
| `http` | [HTTP](./http) |
| `shadowsocks` | [Shadowsocks](./shadowsocks) |
| `vmess` | [VMess](./vmess) |
| `trojan` | [Trojan](./trojan) |
| `wireguard` | [Wireguard](./wireguard) |
| `hysteria` | [Hysteria](./hysteria) |
| `tor` | [Tor](./tor) |
| `ssh` | [SSH](./ssh) |
| `dns` | [DNS](./dns) |
| `selector` | [Selector](./selector) |
| Type | Format |
|----------------|--------------------------------|
| `direct` | [Direct](./direct) |
| `block` | [Block](./block) |
| `socks` | [SOCKS](./socks) |
| `http` | [HTTP](./http) |
| `shadowsocks` | [Shadowsocks](./shadowsocks) |
| `vmess` | [VMess](./vmess) |
| `trojan` | [Trojan](./trojan) |
| `wireguard` | [Wireguard](./wireguard) |
| `hysteria` | [Hysteria](./hysteria) |
| `shadowsocksr` | [ShadowsocksR](./shadowsocksr) |
| `vless` | [VLESS](./vless) |
| `tor` | [Tor](./tor) |
| `ssh` | [SSH](./ssh) |
| `dns` | [DNS](./dns) |
| `selector` | [Selector](./selector) |
| `urltest` | [URLTest](./urltest) |
#### tag

View File

@@ -15,21 +15,24 @@
### 字段
| 类型 | 格式 |
|---------------|------------------------------|
| `direct` | [Direct](./direct) |
| `block` | [Block](./block) |
| `socks` | [SOCKS](./socks) |
| `http` | [HTTP](./http) |
| `shadowsocks` | [Shadowsocks](./shadowsocks) |
| `vmess` | [VMess](./vmess) |
| `trojan` | [Trojan](./trojan) |
| `wireguard` | [Wireguard](./wireguard) |
| `hysteria` | [Hysteria](./hysteria) |
| `tor` | [Tor](./tor) |
| `ssh` | [SSH](./ssh) |
| `dns` | [DNS](./dns) |
| `selector` | [Selector](./selector) |
| 类型 | 格式 |
|----------------|--------------------------------|
| `direct` | [Direct](./direct) |
| `block` | [Block](./block) |
| `socks` | [SOCKS](./socks) |
| `http` | [HTTP](./http) |
| `shadowsocks` | [Shadowsocks](./shadowsocks) |
| `vmess` | [VMess](./vmess) |
| `trojan` | [Trojan](./trojan) |
| `wireguard` | [Wireguard](./wireguard) |
| `hysteria` | [Hysteria](./hysteria) |
| `shadowsocksr` | [ShadowsocksR](./shadowsocksr) |
| `vless` | [VLESS](./vless) |
| `tor` | [Tor](./tor) |
| `ssh` | [SSH](./ssh) |
| `dns` | [DNS](./dns) |
| `selector` | [Selector](./selector) |
| `urltest` | [URLTest](./urltest) |
#### tag

View File

@@ -7,6 +7,8 @@
"server": "127.0.0.1",
"server_port": 1080,
"version": 2,
"password": "fuck me till the daylight",
"tls": {},
... // Dial Fields
@@ -27,6 +29,21 @@ The server address.
The server port.
#### version
ShadowTLS protocol version.
| Value | Protocol Version |
|---------------|-----------------------------------------------------------------------------------------|
| `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) |
| `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) |
#### password
Set password.
Only available in the ShadowTLS v2 protocol.
#### tls
==Required==

View File

@@ -7,6 +7,8 @@
"server": "127.0.0.1",
"server_port": 1080,
"version": 2,
"password": "fuck me till the daylight",
"tls": {},
... // 拨号字段
@@ -27,6 +29,21 @@
服务器端口。
#### version
ShadowTLS 协议版本。
| 值 | 协议版本 |
|---------------|-----------------------------------------------------------------------------------------|
| `1` (default) | [ShadowTLS v1](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v1) |
| `2` | [ShadowTLS v2](https://github.com/ihciah/shadow-tls/blob/master/docs/protocol-en.md#v2) |
#### password
设置密码。
仅在 ShadowTLS v2 协议中可用。
#### tls
==必填==

View File

@@ -0,0 +1,37 @@
### Structure
```json
{
"type": "urltest",
"tag": "auto",
"outbounds": [
"proxy-a",
"proxy-b",
"proxy-c"
],
"url": "http://www.gstatic.com/generate_204",
"interval": "1m",
"tolerance": 50
}
```
### Fields
#### outbounds
==Required==
List of outbound tags to test.
#### url
The URL to test. `http://www.gstatic.com/generate_204` will be used if empty.
#### interval
The test interval. `1m` will be used if empty.
#### tolerance
The test tolerance in milliseconds. `50` will be used if empty.

View File

@@ -0,0 +1,37 @@
### 结构
```json
{
"type": "urltest",
"tag": "auto",
"outbounds": [
"proxy-a",
"proxy-b",
"proxy-c"
],
"url": "http://www.gstatic.com/generate_204",
"interval": "1m",
"tolerance": 50
}
```
### 字段
#### outbounds
==必填==
用于测试的出站标签列表。
#### url
用于测试的链接。默认使用 `http://www.gstatic.com/generate_204`
#### interval
测试间隔。 默认使用 `1m`
#### tolerance
以毫秒为单位的测试容差。 默认使用 `50`

View File

@@ -14,7 +14,7 @@
"authenticated_length": true,
"network": "tcp",
"tls": {},
"packet_addr": false,
"packet_encoding": "",
"multiplex": {},
"transport": {},
@@ -84,9 +84,13 @@ Both is enabled by default.
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
#### packet_addr
#### packet_encoding
Enable packetaddr support.
| Encoding | Description |
|------------|-----------------------|
| (none) | Disabled |
| packetaddr | Supported by v2ray 5+ |
| xudp | Supported by xray |
#### multiplex

View File

@@ -14,7 +14,7 @@
"authenticated_length": true,
"network": "tcp",
"tls": {},
"packet_addr": false,
"packet_encoding": "",
"multiplex": {},
"transport": {},
@@ -84,9 +84,13 @@ VMess 用户 ID。
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
#### packet_addr
#### packet_encoding
启用 packetaddr 支持。
| 编码 | 描述 |
|------------|---------------|
| (空) | 禁用 |
| packetaddr | 由 v2ray 5+ 支持 |
| xudp | 由 xray 支持 |
#### multiplex

View File

@@ -15,6 +15,8 @@
"private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=",
"peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
"reserved": [0, 0, 0],
"workers": 4,
"mtu": 1408,
"network": "tcp",
@@ -26,6 +28,10 @@
WireGuard is not included by default, see [Installation](/#installation).
!!! warning ""
gVisor, which is required by the unprivileged WireGuard is not included by default, see [Installation](/#installation).
### Fields
#### server
@@ -44,7 +50,9 @@ The server port.
Use system tun support.
Requires privileges and cannot conflict with system interfaces.
Requires privilege and cannot conflict with system interfaces.
Forced if gVisor not included in the build.
#### interface_name
@@ -77,9 +85,21 @@ WireGuard peer public key.
WireGuard pre-shared key.
#### reserved
WireGuard reserved field bytes.
#### workers
WireGuard worker count.
CPU count is used by default.
#### mtu
WireGuard MTU. 1408 will be used if empty.
WireGuard MTU.
1408 will be used if empty.
#### network

View File

@@ -15,6 +15,8 @@
"private_key": "YNXtAzepDqRv9H52osJVDQnznT5AM11eCK3ESpwSt04=",
"peer_public_key": "Z1XXLsKYkYxuiYjJIkRvtIKFepCYHTgON+GwPq7SOV4=",
"pre_shared_key": "31aIhAPwktDGpH4JDhA8GNvjFXEf/a6+UaQRyOAiyfM=",
"reserved": [0, 0, 0],
"workers": 4,
"mtu": 1408,
"network": "tcp",
@@ -26,6 +28,10 @@
默认安装不包含 WireGuard, 参阅 [安装](/zh/#_2)。
!!! warning ""
默认安装不包含被非特权 WireGuard 需要的 gVisor, 参阅 [安装](/zh/#_2)。
### 字段
#### server
@@ -46,6 +52,8 @@
需要特权且不能与系统接口冲突。
如果 gVisor 未包含在构建中,则强制执行。
#### interface_name
启用 `system_interface` 时的自定义设备名称。
@@ -79,9 +87,21 @@ WireGuard 对等公钥。
WireGuard 预共享密钥。
#### reserved
WireGuard 保留字段字节。
#### workers
WireGuard worker 数量。
默认使用 CPU 数量。
#### mtu
WireGuard MTU。 默认1408。
WireGuard MTU。
默认使用 1408。
#### network

View File

@@ -107,8 +107,10 @@
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`) &&
`other fields`
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound

View File

@@ -105,8 +105,10 @@
默认规则使用以下匹配逻辑:
(`domain` || `domain_suffix` || `domain_keyword` || `domain_regex` || `geosite` || `geoip` || `ip_cidr`) &&
(`port` || `port_range`) &&
(`source_geoip` || `source_ip_cidr`) &&
`other fields`
(`source_port` || `source_port_range`) &&
`other fields`
#### inbound

View File

@@ -4,7 +4,8 @@
{
"detour": "upstream-out",
"bind_interface": "en0",
"bind_address": "0.0.0.0",
"inet4_bind_address": "0.0.0.0",
"inet6_bind_address": "::",
"routing_mark": 1234,
"reuse_addr": false,
"connect_timeout": "5s",
@@ -17,9 +18,9 @@
### Fields
| Field | Available Context |
|---------------------------------------------------------------------------------------------------------------------|-------------------|
| `bind_interface` /`bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` not set |
| Field | Available Context |
|----------------------------------------------------------------------------------------------------------------------|-------------------|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` not set |
#### detour
@@ -29,9 +30,13 @@ The tag of the upstream outbound.
The network interface to bind to.
#### bind_address
#### inet4_bind_address
The address to bind to.
The IPv4 address to bind to.
#### inet6_bind_address
The IPv6 address to bind to.
#### routing_mark

View File

@@ -4,7 +4,8 @@
{
"detour": "upstream-out",
"bind_interface": "en0",
"bind_address": "0.0.0.0",
"inet4_bind_address": "0.0.0.0",
"inet6_bind_address": "::",
"routing_mark": 1234,
"reuse_addr": false,
"connect_timeout": "5s",
@@ -17,9 +18,9 @@
### 字段
| 字段 | 可用上下文 |
|---------------------------------------------------------------------------------------------------------------------|--------------|
| `bind_interface` /`bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` 未设置 |
| 字段 | 可用上下文 |
|----------------------------------------------------------------------------------------------------------------------|--------------|
| `bind_interface` /`*bind_address` /`routing_mark` /`reuse_addr` / `tcp_fast_open`/ `udp_fragment` /`connect_timeout` | `detour` 未设置 |
#### detour
@@ -32,9 +33,13 @@
要绑定到的网络接口。
#### bind_address
#### inet4_bind_address
要绑定的地址。
要绑定的 IPv4 地址。
#### inet6_bind_address
要绑定的 IPv6 地址。
#### routing_mark

View File

@@ -8,6 +8,7 @@
"udp_fragment": false,
"sniff": false,
"sniff_override_destination": false,
"sniff_timeout": "300ms",
"domain_strategy": "prefer_ipv6",
"udp_timeout": 300,
"proxy_protocol": false,
@@ -57,6 +58,12 @@ Override the connection destination address with the sniffed domain.
If the domain name is invalid (like tor), this will not work.
#### sniff_timeout
Timeout for sniffing.
300ms is used by default.
#### domain_strategy
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.

View File

@@ -8,6 +8,7 @@
"udp_fragment": false,
"sniff": false,
"sniff_override_destination": false,
"sniff_timeout": "300ms",
"domain_strategy": "prefer_ipv6",
"udp_timeout": 300,
"proxy_protocol": false,
@@ -58,6 +59,12 @@
如果域名无效(如 Tor将不生效。
#### sniff_timeout
探测超时时间。
默认使用 300ms。
#### domain_strategy
可选值: `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`

View File

@@ -192,10 +192,15 @@ Available fingerprint values:
* chrome
* firefox
* edge
* safari
* 360
* qq
* ios
* android
* random
Chrome fingerprint will be used if empty.
### ACME Fields

View File

@@ -192,10 +192,16 @@ uTLS 是 "crypto/tls" 的一个分支,它提供了 ClientHello 指纹识别阻
* chrome
* firefox
* edge
* safari
* 360
* qq
* ios
* android
* random
默认使用 chrome 指纹。
### ACME 字段
!!! warning ""

View File

@@ -53,17 +53,6 @@ we need to do this.
The library needs to be updated with the upstream.
#### certmagic
Link: [GitHub repository](https://github.com/SagerNet/certmagic)
Fork of `caddyserver/certmagic`
Since upstream uses `miekg/dns` and we use `x/net/dnsmessage`, we need to replace its DNS part with our own
implementation.
The library needs to be updated with the upstream.
#### smux
Link: [GitHub repository](https://github.com/SagerNet/smux)

View File

@@ -0,0 +1,52 @@
```json
{
"dns": {
"rules": [
{
"domain": [
"clash.razord.top",
"yacd.haishan.me"
],
"server": "local"
},
{
"clash_mode": "direct",
"server": "local"
}
]
},
"outbounds": [
{
"type": "selector",
"tag": "default",
"outbounds": [
"proxy-a",
"proxy-b"
]
}
],
"route": {
"rules": [
{
"clash_mode": "direct",
"outbound": "direct"
},
{
"domain": [
"clash.razord.top",
"yacd.haishan.me"
],
"outbound": "direct"
}
],
"final": "default"
},
"experimental": {
"clash_api": {
"external_controller": "127.0.0.1:9090",
"store_selected": true
}
}
}
```

View File

@@ -3,7 +3,8 @@
Configuration examples for sing-box.
* [Linux Server Installation](./linux-server-installation)
* [Shadowsocks Server](./ss-server)
* [Shadowsocks Client](./ss-client)
* [Shadowsocks Tun](./ss-tun)
* [DNS Hijack](./dns-hijack.md)
* [Tun](./tun)
* [DNS Hijack](./dns-hijack.md)
* [Shadowsocks](./shadowsocks)
* [ShadowTLS](./shadowtls)
* [Clash API](./clash-api)

View File

@@ -3,7 +3,8 @@
sing-box 的配置示例。
* [Linux 服务器安装](./linux-server-installation)
* [Shadowsocks 服务器](./ss-server)
* [Shadowsocks 客户端](./ss-client)
* [Shadowsocks Tun](./ss-tun)
* [DNS 劫持](./dns-hijack.md)
* [Tun](./tun)
* [DNS 劫持](./dns-hijack.md)
* [Shadowsocks](./shadowsocks)
* [ShadowTLS](./shadowtls)
* [Clash API](./clash-api)

View File

@@ -0,0 +1,157 @@
# Shadowsocks
## Single User
#### Server
```json
{
"inbounds": [
{
"type": "shadowsocks",
"listen": "::",
"listen_port": 8080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg=="
}
]
}
```
#### Client
```json
{
"inbounds": [
{
"type": "mixed",
"listen": "::",
"listen_port": 2080
}
],
"outbounds": [
{
"type": "shadowsocks",
"server": "127.0.0.1",
"server_port": 8080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg=="
}
]
}
```
## Multiple Users
#### Server
```json
{
"inbounds": [
{
"type": "shadowsocks",
"listen": "::",
"listen_port": 8080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg==",
"users": [
{
"name": "sekai",
"password": "BXYxVUXJ9NgF7c7KPLQjkg=="
}
]
}
]
}
```
#### Client
```json
{
"inbounds": [
{
"type": "mixed",
"listen": "::",
"listen_port": 2080
}
],
"outbounds": [
{
"type": "shadowsocks",
"server": "127.0.0.1",
"server_port": 8080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg==:BXYxVUXJ9NgF7c7KPLQjkg=="
}
]
}
```
## Relay
#### Server
```json
{
"inbounds": [
{
"type": "shadowsocks",
"listen": "::",
"listen_port": 8080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg=="
}
]
}
```
#### Relay
```json
{
"inbounds": [
{
"type": "shadowsocks",
"listen": "::",
"listen_port": 8081,
"method": "2022-blake3-aes-128-gcm",
"password": "BXYxVUXJ9NgF7c7KPLQjkg==",
"destinations": [
{
"name": "my_server",
"password": "8JCsPssfgS8tiRwiMlhARg==",
"server": "127.0.0.1",
"server_port": 8080
}
]
}
]
}
```
#### Client
```json
{
"inbounds": [
{
"type": "mixed",
"listen": "::",
"listen_port": 2080
}
],
"outbounds": [
{
"type": "shadowsocks",
"server": "127.0.0.1",
"server_port": 8081,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg==:BXYxVUXJ9NgF7c7KPLQjkg=="
}
]
}
```

View File

@@ -7,6 +7,8 @@
"type": "shadowtls",
"listen": "::",
"listen_port": 4443,
"version": 2,
"password": "fuck me till the daylight",
"handshake": {
"server": "google.com",
"server_port": 443
@@ -45,6 +47,8 @@
"tag": "shadowtls-out",
"server": "127.0.0.1",
"server_port": 4443,
"version": 2,
"password": "fuck me till the daylight",
"tls": {
"enabled": true,
"server_name": "google.com"

View File

@@ -1,21 +0,0 @@
```json
{
"inbounds": [
{
"type": "mixed",
"listen": "::",
"listen_port": 2080
}
],
"outbounds": [
{
"type": "shadowsocks",
"server": "::",
"server_port": 8080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg=="
}
]
}
```

View File

@@ -1,13 +0,0 @@
```json
{
"inbounds": [
{
"type": "shadowsocks",
"listen": "::",
"listen_port": 8080,
"method": "2022-blake3-aes-128-gcm",
"password": "8JCsPssfgS8tiRwiMlhARg=="
}
]
}
```

View File

@@ -10,9 +10,18 @@
"tag": "local",
"address": "223.5.5.5",
"detour": "direct"
},
{
"tag": "block",
"address": "rcode://success"
}
],
"rules": [
{
"geosite": "category-ads-all",
"server": "block",
"disable_cache": true
},
{
"domain": "mydomain.com",
"geosite": "cn",
@@ -26,6 +35,7 @@
"type": "tun",
"inet4_address": "172.19.0.1/30",
"auto_route": true,
"strict_route": false,
"sniff": true
}
],
@@ -58,13 +68,16 @@
"outbound": "dns-out"
},
{
"geosite": "category-ads-all",
"outbound": "block"
"geosite": "cn",
"geoip": [
"private",
"cn"
],
"outbound": "direct"
},
{
"geosite": "cn",
"geoip": "cn",
"outbound": "direct"
"geosite": "category-ads-all",
"outbound": "block"
}
],
"auto_detect_interface": true

View File

@@ -7,11 +7,11 @@ the public internet.
##### on Android
`auto-route` cannot automatically hijack DNS requests when Android's `Private DNS` is enabled.
`auto-route` cannot automatically hijack DNS requests when Android's `Private DNS` enabled or `strict_route` disabled.
##### on Linux
`auto-route` cannot automatically hijack DNS requests with `systemd-resolved` enabled, you can switch to NetworkManager.
`auto-route` cannot automatically hijack DNS requests with `systemd-resolved` enabled and `strict_route` disabled.
#### System proxy

View File

@@ -6,11 +6,11 @@
##### Android
`auto-route` 无法自动劫持 DNS 请求如果 `私人 DNS` 开启.
`auto-route` 无法自动劫持 DNS 请求如果 `私人 DNS` 开启`strict_route` 禁用。
##### Linux
`auto-route` 无法自动劫持 DNS 请求如果 `systemd-resolved` 开启, 您可以切换到 NetworkManager.
`auto-route` 无法自动劫持 DNS 请求如果 `systemd-resolved` 开启`strict_route` 禁用。
#### 系统代理

View File

@@ -95,7 +95,9 @@
| cn | 17.8M | 140.3M |
| cn (Loyalsoldier) | 74.3M | 246.7M |
#### Shadowsocks benchmark
#### Benchmark
##### Shadowsocks
| / | none | aes-128-gcm | 2022-blake3-aes-128-gcm |
|------------------------------------|:---------:|:-----------:|:-----------------------:|
@@ -103,6 +105,13 @@
| shadowsocks-rust (v1.15.0-alpha.5) | 10.7 Gbps | / | 9.36 Gbps |
| sing-box | 29.0 Gbps | / | 11.8 Gbps |
##### VMess
| / | TCP | HTTP | H2 TLS | WebSocket TLS | gRPC TLS |
|--------------------|:---------:|:---------:|:---------:|:-------------:|:---------:|
| v2ray-core (5.1.0) | 7.86 GBps | 2.86 Gbps | 1.83 Gbps | 2.36 Gbps | 2.43 Gbps |
| sing-box | 7.96 Gbps | 8.09 Gbps | 6.11 Gbps | 8.02 Gbps | 6.35 Gbps |
#### License
| / | License |

View File

@@ -32,7 +32,8 @@ go install -v -tags with_clash_api github.com/sagernet/sing-box/cmd/sing-box@lat
| `with_utls` | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](./configuration/shared/tls#utls). |
| `with_acme` | Build with ACME TLS certificate issuer support, see [TLS](./configuration/shared/tls). |
| `with_clash_api` | Build with Clash API support, see [Experimental](./configuration/experimental#clash-api-fields). |
| `no_gvisor` | Build without gVisor Tun stack support, see [Tun inbound](./configuration/inbound/tun#stack). |
| `with_v2ray_api` | Build with V2Ray API support, see [Experimental](./configuration/experimental#v2ray-api-fields). |
| `with_gvisor` | Build with gVisor support, see [Tun inbound](./configuration/inbound/tun#stack) and [WireGuard outbound](./configuration/outbound/wireguard#system_interface). |
| `with_embedded_tor` (CGO required) | Build with embedded Tor support, see [Tor outbound](./configuration/outbound/tor). |
| `with_lwip` (CGO required) | Build with LWIP Tun stack support, see [Tun inbound](./configuration/inbound/tun#stack). |
@@ -45,10 +46,6 @@ sing-box version
It is also recommended to use systemd to manage sing-box service,
see [Linux server installation example](./examples/linux-server-installation).
## Contributors
[![](https://opencollective.com/sagernet/contributors.svg?width=740&button=false)](https://github.com/sagernet/sing-box/graphs/contributors)
## License
```

View File

@@ -29,10 +29,11 @@ go install -v -tags with_clash_api github.com/sagernet/sing-box/cmd/sing-box@lat
| `with_wireguard` | 启用 WireGuard 支持,参阅 [WireGuard 出站](./configuration/outbound/wireguard)。 |
| `with_shadowsocksr` | 启用 ShadowsocksR 支持,参阅 [ShadowsocksR 出站](./configuration/outbound/shadowsocksr)。 |
| `with_ech` | 启用 TLS ECH 扩展支持,参阅 [TLS](./configuration/shared/tls#ech)。 |
| `with_utls` | 启用 [uTLS](https://github.com/refraction-networking/utls) 支持, 参阅 [TLS](./configuration/shared/tls#utls)。 |
| `with_utls` | 启用 uTLS 支持,参阅 [实验性](./configuration/experimental#clash-api-fields)。 |
| `with_acme` | 启用 ACME TLS 证书签发支持,参阅 [TLS](./configuration/shared/tls)。 |
| `with_clash_api` | 启用 Clash api 支持,参阅 [实验性](./configuration/experimental#clash-api-fields)。 |
| `no_gvisor` | gVisor Tun 栈支持,参阅 [Tun 入站](./configuration/inbound/tun#stack)。 |
| `with_clash_api` | 启用 Clash API 支持,参阅 [实验性](./configuration/experimental#clash-api-fields)。 |
| `with_v2ray_api` | V2Rat API 支持,参阅 [实验性](./configuration/experimental#v2ray-api-fields)。 |
| `with_gvisor` | 启用 gVisor 支持,参阅 [Tun 入站](./configuration/inbound/tun#stack) 和 [WireGuard 出站](./configuration/outbound/wireguard#system_interface)。 |
| `with_embedded_tor` (需要 CGO) | 启用 嵌入式 Tor 支持,参阅 [Tor 出站](./configuration/outbound/tor)。 |
| `with_lwip` (需要 CGO) | 启用 LWIP Tun 栈支持,参阅 [Tun 入站](./configuration/inbound/tun#stack)。 |
@@ -42,13 +43,9 @@ go install -v -tags with_clash_api github.com/sagernet/sing-box/cmd/sing-box@lat
sing-box version
```
同时推荐使用 Systemd 来管理 sing-box 服务器实例。
同时推荐使用 systemd 来管理 sing-box 服务器实例。
参阅 [Linux 服务器安装示例](./examples/linux-server-installation)。
## 贡献者
[![](https://opencollective.com/sagernet/contributors.svg?width=740&button=false)](https://github.com/sagernet/sing-box/graphs/contributors)
## 授权
```

View File

@@ -1,14 +1,24 @@
//go:build with_clash_api
package experimental
import (
"os"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/experimental/clashapi"
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
)
func NewClashServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
return clashapi.NewServer(router, logFactory, options)
type ClashServerConstructor = func(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error)
var clashServerConstructor ClashServerConstructor
func RegisterClashServerConstructor(constructor ClashServerConstructor) {
clashServerConstructor = constructor
}
func NewClashServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
if clashServerConstructor == nil {
return nil, os.ErrInvalid
}
return clashServerConstructor(router, logFactory, options)
}

View File

@@ -59,3 +59,7 @@ func (c *CacheFile) StoreSelected(group, selected string) error {
return bucket.Put([]byte(group), []byte(selected))
})
}
func (c *CacheFile) Close() error {
return c.DB.Close()
}

View File

@@ -61,7 +61,6 @@ func findProxyByName(router adapter.Router) func(next http.Handler) http.Handler
func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
var info badjson.JSONObject
var clashType string
var isGroup bool
switch detour.Type() {
case C.TypeDirect:
clashType = "Direct"
@@ -70,18 +69,31 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
case C.TypeSocks:
clashType = "Socks"
case C.TypeHTTP:
clashType = "Http"
clashType = "HTTP"
case C.TypeShadowsocks:
clashType = "Shadowsocks"
case C.TypeVMess:
clashType = "Vmess"
clashType = "VMess"
case C.TypeTrojan:
clashType = "Trojan"
case C.TypeHysteria:
clashType = "Hysteria"
case C.TypeWireGuard:
clashType = "WireGuard"
case C.TypeShadowsocksR:
clashType = "ShadowsocksR"
case C.TypeVLESS:
clashType = "VLESS"
case C.TypeTor:
clashType = "Tor"
case C.TypeSSH:
clashType = "SSH"
case C.TypeSelector:
clashType = "Selector"
isGroup = true
case C.TypeURLTest:
clashType = "URLTest"
default:
clashType = "Socks"
clashType = "Direct"
}
info.Put("type", clashType)
info.Put("name", detour.Tag())
@@ -92,10 +104,9 @@ func proxyInfo(server *Server, detour adapter.Outbound) *badjson.JSONObject {
} else {
info.Put("history", []*urltest.History{})
}
if isGroup {
selector := detour.(adapter.OutboundGroup)
info.Put("now", selector.Now())
info.Put("all", selector.All())
if group, isGroup := detour.(adapter.OutboundGroup); isGroup {
info.Put("now", group.Now())
info.Put("all", group.All())
}
return &info
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/sagernet/sing-box/common/json"
"github.com/sagernet/sing-box/common/urltest"
C "github.com/sagernet/sing-box/constant"
"github.com/sagernet/sing-box/experimental"
"github.com/sagernet/sing-box/experimental/clashapi/cachefile"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
"github.com/sagernet/sing-box/log"
@@ -29,6 +30,10 @@ import (
"github.com/go-chi/render"
)
func init() {
experimental.RegisterClashServerConstructor(NewServer)
}
var _ adapter.ClashServer = (*Server)(nil)
type Server struct {
@@ -43,7 +48,7 @@ type Server struct {
cacheFile adapter.ClashCacheFile
}
func NewServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (*Server, error) {
func NewServer(router adapter.Router, logFactory log.ObservableFactory, options option.ClashAPIOptions) (adapter.ClashServer, error) {
trafficManager := trafficontrol.NewManager()
chiRouter := chi.NewRouter()
server := &Server{
@@ -61,6 +66,7 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
server.mode = "rule"
}
if options.StoreSelected {
server.storeSelected = true
cachePath := os.ExpandEnv(options.CacheFile)
if cachePath == "" {
cachePath = "cache.db"
@@ -80,7 +86,7 @@ func NewServer(router adapter.Router, logFactory log.ObservableFactory, options
chiRouter.Use(cors.Handler)
chiRouter.Group(func(r chi.Router) {
r.Use(authentication(options.Secret))
r.Get("/", hello)
r.Get("/", hello(options.ExternalUI != ""))
r.Get("/logs", getLogs(logFactory))
r.Get("/traffic", traffic(trafficManager))
r.Get("/version", version)
@@ -143,6 +149,10 @@ func (s *Server) CacheFile() adapter.ClashCacheFile {
return s.cacheFile
}
func (s *Server) HistoryStorage() *urltest.HistoryStorage {
return s.urlTestHistory
}
func (s *Server) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule) (net.Conn, adapter.Tracker) {
tracker := trafficontrol.NewTCPTracker(conn, s.trafficManager, castMetadata(metadata), s.router, matchedRule)
return tracker, tracker
@@ -232,8 +242,14 @@ func authentication(serverSecret string) func(next http.Handler) http.Handler {
}
}
func hello(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, render.M{"hello": "clash"})
func hello(redirect bool) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if redirect {
http.Redirect(w, r, "/ui/", http.StatusTemporaryRedirect)
} else {
render.JSON(w, r, render.M{"hello": "clash"})
}
}
}
var upgrader = websocket.Upgrader{

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