mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-12 01:57:18 +10:00
Compare commits
39 Commits
v1.12.24
...
dev-wifi-l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
766972ce52 | ||
|
|
f1e48a1cad | ||
|
|
ba496ae300 | ||
|
|
4488148322 | ||
|
|
f086454f81 | ||
|
|
f9b6a068ee | ||
|
|
0ad2a441d9 | ||
|
|
c714b59c87 | ||
|
|
bd6b125707 | ||
|
|
d1109cee90 | ||
|
|
b48002b4db | ||
|
|
67cedfd927 | ||
|
|
853b576d12 | ||
|
|
05cd7f6192 | ||
|
|
e4cf55a86d | ||
|
|
2bf605fad4 | ||
|
|
b913899d43 | ||
|
|
e1a22c0cc2 | ||
|
|
d890c8e8d7 | ||
|
|
1b4ffff67c | ||
|
|
6d012c04cd | ||
|
|
c1309b63c9 | ||
|
|
6674b252bf | ||
|
|
8c7b2e4ac3 | ||
|
|
f400c927c4 | ||
|
|
587d330b58 | ||
|
|
719a28920a | ||
|
|
2e0d344a3d | ||
|
|
387084b7c7 | ||
|
|
e15c02ee33 | ||
|
|
2648b80c6b | ||
|
|
391f7a73d2 | ||
|
|
c1565e2bb0 | ||
|
|
605d8b41f9 | ||
|
|
03d8243d41 | ||
|
|
30f1beb071 | ||
|
|
3841d9cb5a | ||
|
|
dbb7345ec9 | ||
|
|
88f000412b |
23
.fpm_pacman
23
.fpm_pacman
@@ -1,23 +0,0 @@
|
||||
-s dir
|
||||
--name sing-box
|
||||
--category net
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--config-files etc/sing-box/config.json
|
||||
--after-install release/config/sing-box.postinst
|
||||
|
||||
release/config/config.json=/etc/sing-box/config.json
|
||||
|
||||
release/config/sing-box.service=/usr/lib/systemd/system/sing-box.service
|
||||
release/config/sing-box@.service=/usr/lib/systemd/system/sing-box@.service
|
||||
release/config/sing-box.sysusers=/usr/lib/sysusers.d/sing-box.conf
|
||||
release/config/sing-box.rules=usr/share/polkit-1/rules.d/sing-box.rules
|
||||
release/config/sing-box-split-dns.xml=/usr/share/dbus-1/system.d/sing-box-split-dns.conf
|
||||
|
||||
release/completions/sing-box.bash=/usr/share/bash-completion/completions/sing-box.bash
|
||||
release/completions/sing-box.fish=/usr/share/fish/vendor_completions.d/sing-box.fish
|
||||
release/completions/sing-box.zsh=/usr/share/zsh/site-functions/_sing-box
|
||||
|
||||
LICENSE=/usr/share/licenses/sing-box/LICENSE
|
||||
2
.github/setup_go_for_windows7.sh
vendored
2
.github/setup_go_for_windows7.sh
vendored
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
VERSION="1.25.7"
|
||||
VERSION="1.25.1"
|
||||
|
||||
mkdir -p $HOME/go
|
||||
cd $HOME/go
|
||||
|
||||
45
.github/workflows/build.yml
vendored
45
.github/workflows/build.yml
vendored
@@ -25,7 +25,8 @@ on:
|
||||
- publish-android
|
||||
push:
|
||||
branches:
|
||||
- oldstable
|
||||
- main-next
|
||||
- dev-next
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}-${{ inputs.build }}
|
||||
@@ -45,7 +46,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.7
|
||||
go-version: ^1.25.1
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -106,15 +107,15 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Go
|
||||
if: ${{ ! (matrix.legacy_win7 || matrix.legacy_go124) }}
|
||||
if: ${{ ! (matrix.legacy_go123 || matrix.legacy_go124) }}
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.7
|
||||
go-version: ^1.25.1
|
||||
- name: Setup Go 1.24
|
||||
if: matrix.legacy_go124
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.24.10
|
||||
go-version: ~1.24.6
|
||||
- name: Cache Go for Windows 7
|
||||
if: matrix.legacy_win7
|
||||
id: cache-go-for-windows7
|
||||
@@ -122,7 +123,7 @@ jobs:
|
||||
with:
|
||||
path: |
|
||||
~/go/go_win7
|
||||
key: go_win7_1255
|
||||
key: go_win7_1251
|
||||
- name: Setup Go for Windows 7
|
||||
if: matrix.legacy_win7 && steps.cache-go-for-windows7.outputs.cache-hit != 'true'
|
||||
run: |-
|
||||
@@ -145,7 +146,7 @@ jobs:
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0'
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
if: matrix.os != 'android'
|
||||
@@ -153,7 +154,7 @@ jobs:
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
@@ -173,7 +174,7 @@ jobs:
|
||||
export CXX="${CC}++"
|
||||
mkdir -p dist
|
||||
GOOS=$BUILD_GOOS GOARCH=$BUILD_GOARCH build go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "1"
|
||||
@@ -242,7 +243,7 @@ jobs:
|
||||
sudo gem install fpm
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libarchive-tools
|
||||
cp .fpm_pacman .fpm
|
||||
cp .fpm_systemd .fpm
|
||||
fpm -t pacman \
|
||||
-v "$PKG_VERSION" \
|
||||
-p "dist/sing-box_${{ needs.calculate_version.outputs.version }}_${{ matrix.os }}_${{ matrix.pacman }}.pkg.tar.zst" \
|
||||
@@ -286,7 +287,7 @@ jobs:
|
||||
path: "dist"
|
||||
build_android:
|
||||
name: Build Android
|
||||
if: (github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android') && github.ref != 'refs/heads/oldstable'
|
||||
if: github.event_name != 'workflow_dispatch' || inputs.build == 'All' || inputs.build == 'Android'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
@@ -299,7 +300,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.7
|
||||
go-version: ^1.25.1
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -322,12 +323,12 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
- name: Checkout main branch
|
||||
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
||||
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout main
|
||||
- name: Checkout dev branch
|
||||
if: github.ref == 'refs/heads/testing'
|
||||
if: github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout dev
|
||||
@@ -366,7 +367,7 @@ jobs:
|
||||
path: 'dist'
|
||||
publish_android:
|
||||
name: Publish Android
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android' && github.ref != 'refs/heads/oldstable'
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.build == 'publish-android'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- calculate_version
|
||||
@@ -379,7 +380,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.7
|
||||
go-version: ^1.25.1
|
||||
- name: Setup Android NDK
|
||||
id: setup-ndk
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -402,12 +403,12 @@ jobs:
|
||||
JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64
|
||||
ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }}
|
||||
- name: Checkout main branch
|
||||
if: github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
||||
if: github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout main
|
||||
- name: Checkout dev branch
|
||||
if: github.ref == 'refs/heads/testing'
|
||||
if: github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
cd clients/android
|
||||
git checkout dev
|
||||
@@ -478,7 +479,7 @@ jobs:
|
||||
if: matrix.if
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.7
|
||||
go-version: ^1.25.1
|
||||
- name: Set tag
|
||||
if: matrix.if
|
||||
run: |-
|
||||
@@ -486,12 +487,12 @@ jobs:
|
||||
git tag v${{ needs.calculate_version.outputs.version }} -f
|
||||
echo "VERSION=${{ needs.calculate_version.outputs.version }}" >> "$GITHUB_ENV"
|
||||
- name: Checkout main branch
|
||||
if: matrix.if && github.ref == 'refs/heads/stable' && github.event_name != 'workflow_dispatch'
|
||||
if: matrix.if && github.ref == 'refs/heads/main-next' && github.event_name != 'workflow_dispatch'
|
||||
run: |-
|
||||
cd clients/apple
|
||||
git checkout main
|
||||
- name: Checkout dev branch
|
||||
if: matrix.if && github.ref == 'refs/heads/testing'
|
||||
if: matrix.if && github.ref == 'refs/heads/dev-next'
|
||||
run: |-
|
||||
cd clients/apple
|
||||
git checkout dev
|
||||
@@ -577,7 +578,7 @@ jobs:
|
||||
-authenticationKeyID $ASC_KEY_ID \
|
||||
-authenticationKeyIssuerID $ASC_KEY_ISSUER_ID
|
||||
- name: Publish to TestFlight
|
||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/testing'
|
||||
if: matrix.if && matrix.name != 'macOS-standalone' && github.event_name == 'workflow_dispatch' && github.ref =='refs/heads/dev-next'
|
||||
run: |-
|
||||
go run -v ./cmd/internal/app_store_connect publish_testflight ${{ matrix.platform }}
|
||||
- name: Build image
|
||||
|
||||
18
.github/workflows/lint.yml
vendored
18
.github/workflows/lint.yml
vendored
@@ -3,20 +3,18 @@ name: Lint
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
- '.github/**'
|
||||
- '!.github/workflows/lint.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- oldstable
|
||||
- stable
|
||||
- testing
|
||||
- unstable
|
||||
- stable-next
|
||||
- main-next
|
||||
- dev-next
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -30,11 +28,11 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.24.10
|
||||
go-version: ^1.25
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
with:
|
||||
version: latest
|
||||
version: v2.4.0
|
||||
args: --timeout=30m
|
||||
install-mode: binary
|
||||
verify: false
|
||||
|
||||
8
.github/workflows/linux.yml
vendored
8
.github/workflows/linux.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.7
|
||||
go-version: ^1.25.1
|
||||
- name: Check input version
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |-
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ~1.25.7
|
||||
go-version: ^1.25.1
|
||||
- name: Setup Android NDK
|
||||
if: matrix.os == 'android'
|
||||
uses: nttld/setup-ndk@v1
|
||||
@@ -85,14 +85,14 @@ jobs:
|
||||
- name: Set build tags
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale'
|
||||
TAGS='with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0'
|
||||
echo "BUILD_TAGS=${TAGS}" >> "${GITHUB_ENV}"
|
||||
- name: Build
|
||||
run: |
|
||||
set -xeuo pipefail
|
||||
mkdir -p dist
|
||||
go build -v -trimpath -o dist/sing-box -tags "${BUILD_TAGS}" \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }}' \
|
||||
-ldflags '-s -buildid= -X github.com/sagernet/sing-box/constant.Version=${{ needs.calculate_version.outputs.version }} -checklinkname=0' \
|
||||
./cmd/sing-box
|
||||
env:
|
||||
CGO_ENABLED: "0"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
version: "2"
|
||||
run:
|
||||
go: "1.24"
|
||||
go: "1.25"
|
||||
build-tags:
|
||||
- with_gvisor
|
||||
- with_quic
|
||||
|
||||
@@ -13,13 +13,15 @@ RUN set -ex \
|
||||
&& export COMMIT=$(git rev-parse --short HEAD) \
|
||||
&& export VERSION=$(go run ./cmd/internal/read_tag) \
|
||||
&& go build -v -trimpath -tags \
|
||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale" \
|
||||
"with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0" \
|
||||
-o /go/bin/sing-box \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid=" \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$VERSION\" -s -w -buildid= -checklinkname=0" \
|
||||
./cmd/sing-box
|
||||
FROM --platform=$TARGETPLATFORM alpine AS dist
|
||||
LABEL maintainer="nekohasekai <contact-git@sekai.icu>"
|
||||
RUN set -ex \
|
||||
&& apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||
&& apk upgrade \
|
||||
&& apk add bash tzdata ca-certificates nftables \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
COPY --from=builder /go/bin/sing-box /usr/local/bin/sing-box
|
||||
ENTRYPOINT ["sing-box"]
|
||||
|
||||
8
Makefile
8
Makefile
@@ -1,12 +1,12 @@
|
||||
NAME = sing-box
|
||||
COMMIT = $(shell git rev-parse --short HEAD)
|
||||
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale
|
||||
TAGS ?= with_gvisor,with_quic,with_dhcp,with_wireguard,with_utls,with_acme,with_clash_api,with_tailscale,badlinkname,tfogo_checklinkname0
|
||||
|
||||
GOHOSTOS = $(shell go env GOHOSTOS)
|
||||
GOHOSTARCH = $(shell go env GOHOSTARCH)
|
||||
VERSION=$(shell CGO_ENABLED=0 GOOS=$(GOHOSTOS) GOARCH=$(GOHOSTARCH) go run github.com/sagernet/sing-box/cmd/internal/read_tag@latest)
|
||||
|
||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid="
|
||||
PARAMS = -v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=$(VERSION)' -s -w -buildid= -checklinkname=0"
|
||||
MAIN_PARAMS = $(PARAMS) -tags "$(TAGS)"
|
||||
MAIN = ./cmd/sing-box
|
||||
PREFIX ?= $(shell go env GOPATH)
|
||||
@@ -38,7 +38,7 @@ fmt:
|
||||
@gci write --custom-order -s standard -s "prefix(github.com/sagernet/)" -s "default" .
|
||||
|
||||
fmt_install:
|
||||
go install -v mvdan.cc/gofumpt@latest
|
||||
go install -v mvdan.cc/gofumpt@v0.8.0
|
||||
go install -v github.com/daixiang0/gci@latest
|
||||
|
||||
lint:
|
||||
@@ -49,7 +49,7 @@ lint:
|
||||
GOOS=freebsd golangci-lint run ./...
|
||||
|
||||
lint_install:
|
||||
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
|
||||
go install -v github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.4.0
|
||||
|
||||
proto:
|
||||
@go run ./cmd/internal/protogen
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
> Sponsored by [Warp](https://go.warp.dev/sing-box), built for coding with multiple AI agents
|
||||
|
||||
<a href="https://go.warp.dev/sing-box">
|
||||
<img alt="Warp sponsorship" width="400" src="https://github.com/warpdotdev/brand-assets/raw/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
# sing-box
|
||||
|
||||
The universal proxy platform.
|
||||
|
||||
@@ -27,6 +27,8 @@ type DNSClient interface {
|
||||
Start()
|
||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||
LookupCache(domain string, strategy C.DomainStrategy) ([]netip.Addr, bool)
|
||||
ExchangeCache(ctx context.Context, message *dns.Msg) (*dns.Msg, bool)
|
||||
ClearCache()
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/process"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
@@ -85,7 +84,7 @@ type InboundContext struct {
|
||||
DestinationAddresses []netip.Addr
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *process.Info
|
||||
ProcessInfo *ConnectionOwner
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
type NetworkManager interface {
|
||||
Lifecycle
|
||||
Initialize(ruleSets []RuleSet)
|
||||
InterfaceFinder() control.InterfaceFinder
|
||||
UpdateInterfaces() error
|
||||
DefaultNetworkInterface() *NetworkInterface
|
||||
@@ -24,9 +25,10 @@ type NetworkManager interface {
|
||||
NetworkMonitor() tun.NetworkUpdateMonitor
|
||||
InterfaceMonitor() tun.DefaultInterfaceMonitor
|
||||
PackageManager() tun.PackageManager
|
||||
NeedWIFIState() bool
|
||||
WIFIState() WIFIState
|
||||
ResetNetwork()
|
||||
UpdateWIFIState()
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
type NetworkOptions struct {
|
||||
|
||||
@@ -2,9 +2,12 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
@@ -18,6 +21,17 @@ type Outbound interface {
|
||||
N.Dialer
|
||||
}
|
||||
|
||||
type OutboundWithPreferredRoutes interface {
|
||||
Outbound
|
||||
PreferredDomain(domain string) bool
|
||||
PreferredAddress(address netip.Addr) bool
|
||||
}
|
||||
|
||||
type DirectRouteOutbound interface {
|
||||
Outbound
|
||||
NewDirectRouteConnection(metadata InboundContext, routeContext tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
|
||||
}
|
||||
|
||||
type OutboundRegistry interface {
|
||||
option.OutboundOptionsRegistry
|
||||
CreateOutbound(ctx context.Context, router Router, logger log.ContextLogger, tag string, outboundType string, options any) (Outbound, error)
|
||||
|
||||
70
adapter/platform.go
Normal file
70
adapter/platform.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
type PlatformInterface interface {
|
||||
Initialize(networkManager NetworkManager) error
|
||||
|
||||
UsePlatformAutoDetectInterfaceControl() bool
|
||||
AutoDetectInterfaceControl(fd int) error
|
||||
|
||||
UsePlatformInterface() bool
|
||||
OpenInterface(options *tun.Options, platformOptions option.TunPlatformOptions) (tun.Tun, error)
|
||||
|
||||
UsePlatformDefaultInterfaceMonitor() bool
|
||||
CreateDefaultInterfaceMonitor(logger logger.Logger) tun.DefaultInterfaceMonitor
|
||||
|
||||
UsePlatformNetworkInterfaces() bool
|
||||
NetworkInterfaces() ([]NetworkInterface, error)
|
||||
|
||||
UnderNetworkExtension() bool
|
||||
NetworkExtensionIncludeAllNetworks() bool
|
||||
|
||||
ClearDNSCache()
|
||||
RequestPermissionForWIFIState() error
|
||||
ReadWIFIState() WIFIState
|
||||
SystemCertificates() []string
|
||||
|
||||
UsePlatformConnectionOwnerFinder() bool
|
||||
FindConnectionOwner(request *FindConnectionOwnerRequest) (*ConnectionOwner, error)
|
||||
|
||||
UsePlatformWIFIMonitor() bool
|
||||
|
||||
UsePlatformNotification() bool
|
||||
SendNotification(notification *Notification) error
|
||||
}
|
||||
|
||||
type FindConnectionOwnerRequest struct {
|
||||
IpProtocol int32
|
||||
SourceAddress string
|
||||
SourcePort int32
|
||||
DestinationAddress string
|
||||
DestinationPort int32
|
||||
}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageName string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
Identifier string
|
||||
TypeName string
|
||||
TypeID int32
|
||||
Title string
|
||||
Subtitle string
|
||||
Body string
|
||||
OpenURL string
|
||||
}
|
||||
|
||||
type SystemProxyStatus struct {
|
||||
Available bool
|
||||
Enabled bool
|
||||
}
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-tun"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
@@ -19,10 +21,9 @@ import (
|
||||
type Router interface {
|
||||
Lifecycle
|
||||
ConnectionRouter
|
||||
PreMatch(metadata InboundContext) error
|
||||
PreMatch(metadata InboundContext, context tun.DirectRouteContext, timeout time.Duration) (tun.DirectRouteDestination, error)
|
||||
ConnectionRouterEx
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
NeedWIFIState() bool
|
||||
Rules() []Rule
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
|
||||
@@ -73,7 +73,7 @@ func NewUpstreamContextHandlerEx(
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, myMetadata := ExtendContext(ctx)
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
@@ -84,7 +84,7 @@ func (w *myUpstreamContextHandlerWrapperEx) NewConnectionEx(ctx context.Context,
|
||||
}
|
||||
|
||||
func (w *myUpstreamContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, myMetadata := ExtendContext(ctx)
|
||||
myMetadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
myMetadata.Source = source
|
||||
}
|
||||
@@ -146,7 +146,7 @@ type routeContextHandlerWrapperEx struct {
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn net.Conn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := ExtendContext(ctx)
|
||||
metadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
}
|
||||
@@ -157,7 +157,7 @@ func (r *routeContextHandlerWrapperEx) NewConnectionEx(ctx context.Context, conn
|
||||
}
|
||||
|
||||
func (r *routeContextHandlerWrapperEx) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, source M.Socksaddr, destination M.Socksaddr, onClose N.CloseHandlerFunc) {
|
||||
_, metadata := ExtendContext(ctx)
|
||||
metadata := ContextFrom(ctx)
|
||||
if source.IsValid() {
|
||||
metadata.Source = source
|
||||
}
|
||||
|
||||
16
box.go
16
box.go
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/sagernet/sing-box/dns/transport/local"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/protocol/direct"
|
||||
@@ -139,7 +138,7 @@ func New(options Options) (*Box, error) {
|
||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||
needV2RayAPI = true
|
||||
}
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||
var defaultLogWriter io.Writer
|
||||
if platformInterface != nil {
|
||||
defaultLogWriter = io.Discard
|
||||
@@ -184,7 +183,7 @@ func New(options Options) (*Box, error) {
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize network manager")
|
||||
}
|
||||
@@ -323,13 +322,14 @@ func New(options Options) (*Box, error) {
|
||||
option.DirectOutboundOptions{},
|
||||
)
|
||||
})
|
||||
dnsTransportManager.Initialize(common.Must1(
|
||||
local.NewTransport(
|
||||
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||
return local.NewTransport(
|
||||
ctx,
|
||||
logFactory.NewLogger("dns/local"),
|
||||
"local",
|
||||
option.LocalDNSServerOptions{},
|
||||
)))
|
||||
)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
err = platformInterface.Initialize(networkManager)
|
||||
if err != nil {
|
||||
@@ -526,3 +526,7 @@ func (s *Box) Inbound() adapter.InboundManager {
|
||||
func (s *Box) Outbound() adapter.OutboundManager {
|
||||
return s.outbound
|
||||
}
|
||||
|
||||
func (s *Box) LogFactory() log.Factory {
|
||||
return s.logFactory
|
||||
}
|
||||
|
||||
Submodule clients/android updated: 134402995e...e08fbfcfea
Submodule clients/apple updated: 97402ba8b6...84d8cf1757
@@ -46,7 +46,7 @@ var (
|
||||
sharedFlags []string
|
||||
debugFlags []string
|
||||
sharedTags []string
|
||||
darwinTags []string
|
||||
macOSTags []string
|
||||
memcTags []string
|
||||
notMemcTags []string
|
||||
debugTags []string
|
||||
@@ -59,11 +59,11 @@ func init() {
|
||||
if err != nil {
|
||||
currentTag = "unknown"
|
||||
}
|
||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid=")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag)
|
||||
sharedFlags = append(sharedFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -s -w -buildid= -checklinkname=0")
|
||||
debugFlags = append(debugFlags, "-ldflags", "-X github.com/sagernet/sing-box/constant.Version="+currentTag+" -checklinkname=0")
|
||||
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack")
|
||||
darwinTags = append(darwinTags, "with_dhcp")
|
||||
sharedTags = append(sharedTags, "with_gvisor", "with_quic", "with_wireguard", "with_utls", "with_clash_api", "with_conntrack", "badlinkname", "tfogo_checklinkname0")
|
||||
macOSTags = append(macOSTags, "with_dhcp")
|
||||
memcTags = append(memcTags, "with_tailscale")
|
||||
notMemcTags = append(notMemcTags, "with_low_memory")
|
||||
debugTags = append(debugTags, "debug")
|
||||
@@ -107,10 +107,8 @@ func buildAndroid() {
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
sharedFlags[3] = sharedFlags[3] + " -checklinkname=0"
|
||||
args = append(args, sharedFlags...)
|
||||
} else {
|
||||
debugFlags[1] = debugFlags[1] + " -checklinkname=0"
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
@@ -160,7 +158,9 @@ func buildApple() {
|
||||
"-tags-not-macos=with_low_memory",
|
||||
}
|
||||
if !withTailscale {
|
||||
args = append(args, "-tags-macos="+strings.Join(memcTags, ","))
|
||||
args = append(args, "-tags-macos="+strings.Join(append(macOSTags, memcTags...), ","))
|
||||
} else {
|
||||
args = append(args, "-tags-macos="+strings.Join(macOSTags, ","))
|
||||
}
|
||||
|
||||
if !debugEnabled {
|
||||
@@ -169,7 +169,7 @@ func buildApple() {
|
||||
args = append(args, debugFlags...)
|
||||
}
|
||||
|
||||
tags := append(sharedTags, darwinTags...)
|
||||
tags := sharedTags
|
||||
if withTailscale {
|
||||
tags = append(tags, memcTags...)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,10 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing-box/common/srs"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing-box/route/rule"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
@@ -69,7 +71,7 @@ func compileRuleSet(sourcePath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = srs.Write(outputFile, plainRuleSet.Options, plainRuleSet.Version)
|
||||
err = srs.Write(outputFile, plainRuleSet.Options, downgradeRuleSetVersion(plainRuleSet.Version, plainRuleSet.Options))
|
||||
if err != nil {
|
||||
outputFile.Close()
|
||||
os.Remove(outputPath)
|
||||
@@ -78,3 +80,18 @@ func compileRuleSet(sourcePath string) error {
|
||||
outputFile.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
||||
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
||||
len(rule.DefaultInterfaceAddress) > 0
|
||||
}) {
|
||||
version = C.RuleSetVersion3
|
||||
}
|
||||
if version == C.RuleSetVersion3 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||
return len(rule.NetworkType) > 0 || rule.NetworkIsExpensive || rule.NetworkIsConstrained
|
||||
}) {
|
||||
version = C.RuleSetVersion2
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func initializeHTTP3Client(instance *box.Box) error {
|
||||
}
|
||||
http3Client = &http.Client{
|
||||
Transport: &http3.Transport{
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (quic.EarlyConnection, error) {
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
destination := M.ParseSocksaddr(addr)
|
||||
udpConn, dErr := dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||
if dErr != nil {
|
||||
|
||||
176
common/badtls/raw_conn.go
Normal file
176
common/badtls/raw_conn.go
Normal file
@@ -0,0 +1,176 @@
|
||||
//go:build go1.25 && badlinkname
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
"unsafe"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
type RawConn struct {
|
||||
pointer unsafe.Pointer
|
||||
methods *Methods
|
||||
|
||||
IsClient *bool
|
||||
IsHandshakeComplete *atomic.Bool
|
||||
Vers *uint16
|
||||
CipherSuite *uint16
|
||||
|
||||
RawInput *bytes.Buffer
|
||||
Input *bytes.Reader
|
||||
Hand *bytes.Buffer
|
||||
|
||||
CloseNotifySent *bool
|
||||
CloseNotifyErr *error
|
||||
|
||||
In *RawHalfConn
|
||||
Out *RawHalfConn
|
||||
|
||||
BytesSent *int64
|
||||
PacketsSent *int64
|
||||
|
||||
ActiveCall *atomic.Int32
|
||||
Tmp *[16]byte
|
||||
}
|
||||
|
||||
func NewRawConn(rawTLSConn tls.Conn) (*RawConn, error) {
|
||||
var (
|
||||
pointer unsafe.Pointer
|
||||
methods *Methods
|
||||
loaded bool
|
||||
)
|
||||
for _, tlsCreator := range methodRegistry {
|
||||
pointer, methods, loaded = tlsCreator(rawTLSConn)
|
||||
if loaded {
|
||||
break
|
||||
}
|
||||
}
|
||||
if !loaded {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
conn := &RawConn{
|
||||
pointer: pointer,
|
||||
methods: methods,
|
||||
}
|
||||
|
||||
rawConn := reflect.Indirect(reflect.ValueOf(rawTLSConn))
|
||||
|
||||
rawIsClient := rawConn.FieldByName("isClient")
|
||||
if !rawIsClient.IsValid() || rawIsClient.Kind() != reflect.Bool {
|
||||
return nil, E.New("invalid Conn.isClient")
|
||||
}
|
||||
conn.IsClient = (*bool)(unsafe.Pointer(rawIsClient.UnsafeAddr()))
|
||||
|
||||
rawIsHandshakeComplete := rawConn.FieldByName("isHandshakeComplete")
|
||||
if !rawIsHandshakeComplete.IsValid() || rawIsHandshakeComplete.Kind() != reflect.Struct {
|
||||
return nil, E.New("invalid Conn.isHandshakeComplete")
|
||||
}
|
||||
conn.IsHandshakeComplete = (*atomic.Bool)(unsafe.Pointer(rawIsHandshakeComplete.UnsafeAddr()))
|
||||
|
||||
rawVers := rawConn.FieldByName("vers")
|
||||
if !rawVers.IsValid() || rawVers.Kind() != reflect.Uint16 {
|
||||
return nil, E.New("invalid Conn.vers")
|
||||
}
|
||||
conn.Vers = (*uint16)(unsafe.Pointer(rawVers.UnsafeAddr()))
|
||||
|
||||
rawCipherSuite := rawConn.FieldByName("cipherSuite")
|
||||
if !rawCipherSuite.IsValid() || rawCipherSuite.Kind() != reflect.Uint16 {
|
||||
return nil, E.New("invalid Conn.cipherSuite")
|
||||
}
|
||||
conn.CipherSuite = (*uint16)(unsafe.Pointer(rawCipherSuite.UnsafeAddr()))
|
||||
|
||||
rawRawInput := rawConn.FieldByName("rawInput")
|
||||
if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct {
|
||||
return nil, E.New("invalid Conn.rawInput")
|
||||
}
|
||||
conn.RawInput = (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr()))
|
||||
|
||||
rawInput := rawConn.FieldByName("input")
|
||||
if !rawInput.IsValid() || rawInput.Kind() != reflect.Struct {
|
||||
return nil, E.New("invalid Conn.input")
|
||||
}
|
||||
conn.Input = (*bytes.Reader)(unsafe.Pointer(rawInput.UnsafeAddr()))
|
||||
|
||||
rawHand := rawConn.FieldByName("hand")
|
||||
if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct {
|
||||
return nil, E.New("invalid Conn.hand")
|
||||
}
|
||||
conn.Hand = (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
|
||||
|
||||
rawCloseNotifySent := rawConn.FieldByName("closeNotifySent")
|
||||
if !rawCloseNotifySent.IsValid() || rawCloseNotifySent.Kind() != reflect.Bool {
|
||||
return nil, E.New("invalid Conn.closeNotifySent")
|
||||
}
|
||||
conn.CloseNotifySent = (*bool)(unsafe.Pointer(rawCloseNotifySent.UnsafeAddr()))
|
||||
|
||||
rawCloseNotifyErr := rawConn.FieldByName("closeNotifyErr")
|
||||
if !rawCloseNotifyErr.IsValid() || rawCloseNotifyErr.Kind() != reflect.Interface {
|
||||
return nil, E.New("invalid Conn.closeNotifyErr")
|
||||
}
|
||||
conn.CloseNotifyErr = (*error)(unsafe.Pointer(rawCloseNotifyErr.UnsafeAddr()))
|
||||
|
||||
rawIn := rawConn.FieldByName("in")
|
||||
if !rawIn.IsValid() || rawIn.Kind() != reflect.Struct {
|
||||
return nil, E.New("invalid Conn.in")
|
||||
}
|
||||
halfIn, err := NewRawHalfConn(rawIn, methods)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "invalid Conn.in")
|
||||
}
|
||||
conn.In = halfIn
|
||||
|
||||
rawOut := rawConn.FieldByName("out")
|
||||
if !rawOut.IsValid() || rawOut.Kind() != reflect.Struct {
|
||||
return nil, E.New("invalid Conn.out")
|
||||
}
|
||||
halfOut, err := NewRawHalfConn(rawOut, methods)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "invalid Conn.out")
|
||||
}
|
||||
conn.Out = halfOut
|
||||
|
||||
rawBytesSent := rawConn.FieldByName("bytesSent")
|
||||
if !rawBytesSent.IsValid() || rawBytesSent.Kind() != reflect.Int64 {
|
||||
return nil, E.New("invalid Conn.bytesSent")
|
||||
}
|
||||
conn.BytesSent = (*int64)(unsafe.Pointer(rawBytesSent.UnsafeAddr()))
|
||||
|
||||
rawPacketsSent := rawConn.FieldByName("packetsSent")
|
||||
if !rawPacketsSent.IsValid() || rawPacketsSent.Kind() != reflect.Int64 {
|
||||
return nil, E.New("invalid Conn.packetsSent")
|
||||
}
|
||||
conn.PacketsSent = (*int64)(unsafe.Pointer(rawPacketsSent.UnsafeAddr()))
|
||||
|
||||
rawActiveCall := rawConn.FieldByName("activeCall")
|
||||
if !rawActiveCall.IsValid() || rawActiveCall.Kind() != reflect.Struct {
|
||||
return nil, E.New("invalid Conn.activeCall")
|
||||
}
|
||||
conn.ActiveCall = (*atomic.Int32)(unsafe.Pointer(rawActiveCall.UnsafeAddr()))
|
||||
|
||||
rawTmp := rawConn.FieldByName("tmp")
|
||||
if !rawTmp.IsValid() || rawTmp.Kind() != reflect.Array || rawTmp.Len() != 16 || rawTmp.Type().Elem().Kind() != reflect.Uint8 {
|
||||
return nil, E.New("invalid Conn.tmp")
|
||||
}
|
||||
conn.Tmp = (*[16]byte)(unsafe.Pointer(rawTmp.UnsafeAddr()))
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func (c *RawConn) ReadRecord() error {
|
||||
return c.methods.readRecord(c.pointer)
|
||||
}
|
||||
|
||||
func (c *RawConn) HandlePostHandshakeMessage() error {
|
||||
return c.methods.handlePostHandshakeMessage(c.pointer)
|
||||
}
|
||||
|
||||
func (c *RawConn) WriteRecordLocked(typ uint16, data []byte) (int, error) {
|
||||
return c.methods.writeRecordLocked(c.pointer, typ, data)
|
||||
}
|
||||
121
common/badtls/raw_half_conn.go
Normal file
121
common/badtls/raw_half_conn.go
Normal file
@@ -0,0 +1,121 @@
|
||||
//go:build go1.25 && badlinkname
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"hash"
|
||||
"reflect"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type RawHalfConn struct {
|
||||
pointer unsafe.Pointer
|
||||
methods *Methods
|
||||
*sync.Mutex
|
||||
Err *error
|
||||
Version *uint16
|
||||
Cipher *any
|
||||
Seq *[8]byte
|
||||
ScratchBuf *[13]byte
|
||||
TrafficSecret *[]byte
|
||||
Mac *hash.Hash
|
||||
RawKey *[]byte
|
||||
RawIV *[]byte
|
||||
RawMac *[]byte
|
||||
}
|
||||
|
||||
func NewRawHalfConn(rawHalfConn reflect.Value, methods *Methods) (*RawHalfConn, error) {
|
||||
halfConn := &RawHalfConn{
|
||||
pointer: (unsafe.Pointer)(rawHalfConn.UnsafeAddr()),
|
||||
methods: methods,
|
||||
}
|
||||
|
||||
rawMutex := rawHalfConn.FieldByName("Mutex")
|
||||
if !rawMutex.IsValid() || rawMutex.Kind() != reflect.Struct {
|
||||
return nil, E.New("badtls: invalid halfConn.Mutex")
|
||||
}
|
||||
halfConn.Mutex = (*sync.Mutex)(unsafe.Pointer(rawMutex.UnsafeAddr()))
|
||||
|
||||
rawErr := rawHalfConn.FieldByName("err")
|
||||
if !rawErr.IsValid() || rawErr.Kind() != reflect.Interface {
|
||||
return nil, E.New("badtls: invalid halfConn.err")
|
||||
}
|
||||
halfConn.Err = (*error)(unsafe.Pointer(rawErr.UnsafeAddr()))
|
||||
|
||||
rawVersion := rawHalfConn.FieldByName("version")
|
||||
if !rawVersion.IsValid() || rawVersion.Kind() != reflect.Uint16 {
|
||||
return nil, E.New("badtls: invalid halfConn.version")
|
||||
}
|
||||
halfConn.Version = (*uint16)(unsafe.Pointer(rawVersion.UnsafeAddr()))
|
||||
|
||||
rawCipher := rawHalfConn.FieldByName("cipher")
|
||||
if !rawCipher.IsValid() || rawCipher.Kind() != reflect.Interface {
|
||||
return nil, E.New("badtls: invalid halfConn.cipher")
|
||||
}
|
||||
halfConn.Cipher = (*any)(unsafe.Pointer(rawCipher.UnsafeAddr()))
|
||||
|
||||
rawSeq := rawHalfConn.FieldByName("seq")
|
||||
if !rawSeq.IsValid() || rawSeq.Kind() != reflect.Array || rawSeq.Len() != 8 || rawSeq.Type().Elem().Kind() != reflect.Uint8 {
|
||||
return nil, E.New("badtls: invalid halfConn.seq")
|
||||
}
|
||||
halfConn.Seq = (*[8]byte)(unsafe.Pointer(rawSeq.UnsafeAddr()))
|
||||
|
||||
rawScratchBuf := rawHalfConn.FieldByName("scratchBuf")
|
||||
if !rawScratchBuf.IsValid() || rawScratchBuf.Kind() != reflect.Array || rawScratchBuf.Len() != 13 || rawScratchBuf.Type().Elem().Kind() != reflect.Uint8 {
|
||||
return nil, E.New("badtls: invalid halfConn.scratchBuf")
|
||||
}
|
||||
halfConn.ScratchBuf = (*[13]byte)(unsafe.Pointer(rawScratchBuf.UnsafeAddr()))
|
||||
|
||||
rawTrafficSecret := rawHalfConn.FieldByName("trafficSecret")
|
||||
if !rawTrafficSecret.IsValid() || rawTrafficSecret.Kind() != reflect.Slice || rawTrafficSecret.Type().Elem().Kind() != reflect.Uint8 {
|
||||
return nil, E.New("badtls: invalid halfConn.trafficSecret")
|
||||
}
|
||||
halfConn.TrafficSecret = (*[]byte)(unsafe.Pointer(rawTrafficSecret.UnsafeAddr()))
|
||||
|
||||
rawMac := rawHalfConn.FieldByName("mac")
|
||||
if !rawMac.IsValid() || rawMac.Kind() != reflect.Interface {
|
||||
return nil, E.New("badtls: invalid halfConn.mac")
|
||||
}
|
||||
halfConn.Mac = (*hash.Hash)(unsafe.Pointer(rawMac.UnsafeAddr()))
|
||||
|
||||
rawKey := rawHalfConn.FieldByName("rawKey")
|
||||
if rawKey.IsValid() {
|
||||
if /*!rawKey.IsValid() || */ rawKey.Kind() != reflect.Slice || rawKey.Type().Elem().Kind() != reflect.Uint8 {
|
||||
return nil, E.New("badtls: invalid halfConn.rawKey")
|
||||
}
|
||||
halfConn.RawKey = (*[]byte)(unsafe.Pointer(rawKey.UnsafeAddr()))
|
||||
|
||||
rawIV := rawHalfConn.FieldByName("rawIV")
|
||||
if !rawIV.IsValid() || rawIV.Kind() != reflect.Slice || rawIV.Type().Elem().Kind() != reflect.Uint8 {
|
||||
return nil, E.New("badtls: invalid halfConn.rawIV")
|
||||
}
|
||||
halfConn.RawIV = (*[]byte)(unsafe.Pointer(rawIV.UnsafeAddr()))
|
||||
|
||||
rawMAC := rawHalfConn.FieldByName("rawMac")
|
||||
if !rawMAC.IsValid() || rawMAC.Kind() != reflect.Slice || rawMAC.Type().Elem().Kind() != reflect.Uint8 {
|
||||
return nil, E.New("badtls: invalid halfConn.rawMac")
|
||||
}
|
||||
halfConn.RawMac = (*[]byte)(unsafe.Pointer(rawMAC.UnsafeAddr()))
|
||||
}
|
||||
|
||||
return halfConn, nil
|
||||
}
|
||||
|
||||
func (hc *RawHalfConn) Decrypt(record []byte) ([]byte, uint8, error) {
|
||||
return hc.methods.decrypt(hc.pointer, record)
|
||||
}
|
||||
|
||||
func (hc *RawHalfConn) SetErrorLocked(err error) error {
|
||||
return hc.methods.setErrorLocked(hc.pointer, err)
|
||||
}
|
||||
|
||||
func (hc *RawHalfConn) SetTrafficSecret(suite unsafe.Pointer, level int, secret []byte) {
|
||||
hc.methods.setTrafficSecret(hc.pointer, suite, level, secret)
|
||||
}
|
||||
|
||||
func (hc *RawHalfConn) ExplicitNonceLen() int {
|
||||
return hc.methods.explicitNonceLen(hc.pointer)
|
||||
}
|
||||
@@ -1,18 +1,9 @@
|
||||
//go:build go1.21 && !without_badtls
|
||||
//go:build go1.25 && badlinkname
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
@@ -21,63 +12,21 @@ var _ N.ReadWaiter = (*ReadWaitConn)(nil)
|
||||
|
||||
type ReadWaitConn struct {
|
||||
tls.Conn
|
||||
halfAccess *sync.Mutex
|
||||
rawInput *bytes.Buffer
|
||||
input *bytes.Reader
|
||||
hand *bytes.Buffer
|
||||
readWaitOptions N.ReadWaitOptions
|
||||
tlsReadRecord func() error
|
||||
tlsHandlePostHandshakeMessage func() error
|
||||
rawConn *RawConn
|
||||
readWaitOptions N.ReadWaitOptions
|
||||
}
|
||||
|
||||
func NewReadWaitConn(conn tls.Conn) (tls.Conn, error) {
|
||||
var (
|
||||
loaded bool
|
||||
tlsReadRecord func() error
|
||||
tlsHandlePostHandshakeMessage func() error
|
||||
)
|
||||
for _, tlsCreator := range tlsRegistry {
|
||||
loaded, tlsReadRecord, tlsHandlePostHandshakeMessage = tlsCreator(conn)
|
||||
if loaded {
|
||||
break
|
||||
}
|
||||
if _, isReadWaitConn := conn.(N.ReadWaiter); isReadWaitConn {
|
||||
return conn, nil
|
||||
}
|
||||
if !loaded {
|
||||
return nil, os.ErrInvalid
|
||||
rawConn, err := NewRawConn(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawConn := reflect.Indirect(reflect.ValueOf(conn))
|
||||
rawHalfConn := rawConn.FieldByName("in")
|
||||
if !rawHalfConn.IsValid() || rawHalfConn.Kind() != reflect.Struct {
|
||||
return nil, E.New("badtls: invalid half conn")
|
||||
}
|
||||
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()))
|
||||
rawRawInput := rawConn.FieldByName("rawInput")
|
||||
if !rawRawInput.IsValid() || rawRawInput.Kind() != reflect.Struct {
|
||||
return nil, E.New("badtls: invalid raw input")
|
||||
}
|
||||
rawInput := (*bytes.Buffer)(unsafe.Pointer(rawRawInput.UnsafeAddr()))
|
||||
rawInput0 := rawConn.FieldByName("input")
|
||||
if !rawInput0.IsValid() || rawInput0.Kind() != reflect.Struct {
|
||||
return nil, E.New("badtls: invalid input")
|
||||
}
|
||||
input := (*bytes.Reader)(unsafe.Pointer(rawInput0.UnsafeAddr()))
|
||||
rawHand := rawConn.FieldByName("hand")
|
||||
if !rawHand.IsValid() || rawHand.Kind() != reflect.Struct {
|
||||
return nil, E.New("badtls: invalid hand")
|
||||
}
|
||||
hand := (*bytes.Buffer)(unsafe.Pointer(rawHand.UnsafeAddr()))
|
||||
return &ReadWaitConn{
|
||||
Conn: conn,
|
||||
halfAccess: halfAccess,
|
||||
rawInput: rawInput,
|
||||
input: input,
|
||||
hand: hand,
|
||||
tlsReadRecord: tlsReadRecord,
|
||||
tlsHandlePostHandshakeMessage: tlsHandlePostHandshakeMessage,
|
||||
Conn: conn,
|
||||
rawConn: rawConn,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -87,36 +36,36 @@ func (c *ReadWaitConn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy
|
||||
}
|
||||
|
||||
func (c *ReadWaitConn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
|
||||
err = c.HandshakeContext(context.Background())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
c.halfAccess.Lock()
|
||||
defer c.halfAccess.Unlock()
|
||||
for c.input.Len() == 0 {
|
||||
err = c.tlsReadRecord()
|
||||
//err = c.HandshakeContext(context.Background())
|
||||
//if err != nil {
|
||||
// return
|
||||
//}
|
||||
c.rawConn.In.Lock()
|
||||
defer c.rawConn.In.Unlock()
|
||||
for c.rawConn.Input.Len() == 0 {
|
||||
err = c.rawConn.ReadRecord()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for c.hand.Len() > 0 {
|
||||
err = c.tlsHandlePostHandshakeMessage()
|
||||
for c.rawConn.Hand.Len() > 0 {
|
||||
err = c.rawConn.HandlePostHandshakeMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
buffer = c.readWaitOptions.NewBuffer()
|
||||
n, err := c.input.Read(buffer.FreeBytes())
|
||||
n, err := c.rawConn.Input.Read(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
return
|
||||
}
|
||||
buffer.Truncate(n)
|
||||
|
||||
if n != 0 && c.input.Len() == 0 && c.rawInput.Len() > 0 &&
|
||||
// recordType(c.rawInput.Bytes()[0]) == recordTypeAlert {
|
||||
c.rawInput.Bytes()[0] == 21 {
|
||||
_ = c.tlsReadRecord()
|
||||
if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 &&
|
||||
// recordType(c.RawInput.Bytes()[0]) == recordTypeAlert {
|
||||
c.rawConn.RawInput.Bytes()[0] == 21 {
|
||||
_ = c.rawConn.ReadRecord()
|
||||
// return n, err // will be io.EOF on closeNotify
|
||||
}
|
||||
|
||||
@@ -131,25 +80,3 @@ func (c *ReadWaitConn) Upstream() any {
|
||||
func (c *ReadWaitConn) ReaderReplaceable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var tlsRegistry []func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error)
|
||||
|
||||
func init() {
|
||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
||||
tlsConn, loaded := conn.(*tls.STDConn)
|
||||
if !loaded {
|
||||
return
|
||||
}
|
||||
return true, func() error {
|
||||
return stdTLSReadRecord(tlsConn)
|
||||
}, func() error {
|
||||
return stdTLSHandlePostHandshakeMessage(tlsConn)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
|
||||
func stdTLSReadRecord(c *tls.STDConn) error
|
||||
|
||||
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
|
||||
func stdTLSHandlePostHandshakeMessage(c *tls.STDConn) error
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//go:build !go1.21 || without_badtls
|
||||
//go:build !go1.25 || !badlinkname
|
||||
|
||||
package badtls
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
//go:build go1.21 && !without_badtls && with_utls
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"net"
|
||||
_ "unsafe"
|
||||
|
||||
"github.com/metacubex/utls"
|
||||
)
|
||||
|
||||
func init() {
|
||||
tlsRegistry = append(tlsRegistry, func(conn net.Conn) (loaded bool, tlsReadRecord func() error, tlsHandlePostHandshakeMessage func() error) {
|
||||
switch tlsConn := conn.(type) {
|
||||
case *tls.UConn:
|
||||
return true, func() error {
|
||||
return utlsReadRecord(tlsConn.Conn)
|
||||
}, func() error {
|
||||
return utlsHandlePostHandshakeMessage(tlsConn.Conn)
|
||||
}
|
||||
case *tls.Conn:
|
||||
return true, func() error {
|
||||
return utlsReadRecord(tlsConn)
|
||||
}, func() error {
|
||||
return utlsHandlePostHandshakeMessage(tlsConn)
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord
|
||||
func utlsReadRecord(c *tls.Conn) error
|
||||
|
||||
//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage
|
||||
func utlsHandlePostHandshakeMessage(c *tls.Conn) error
|
||||
62
common/badtls/registry.go
Normal file
62
common/badtls/registry.go
Normal file
@@ -0,0 +1,62 @@
|
||||
//go:build go1.25 && badlinkname
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Methods struct {
|
||||
readRecord func(c unsafe.Pointer) error
|
||||
handlePostHandshakeMessage func(c unsafe.Pointer) error
|
||||
writeRecordLocked func(c unsafe.Pointer, typ uint16, data []byte) (int, error)
|
||||
|
||||
setErrorLocked func(hc unsafe.Pointer, err error) error
|
||||
decrypt func(hc unsafe.Pointer, record []byte) ([]byte, uint8, error)
|
||||
setTrafficSecret func(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte)
|
||||
explicitNonceLen func(hc unsafe.Pointer) int
|
||||
}
|
||||
|
||||
var methodRegistry []func(conn net.Conn) (unsafe.Pointer, *Methods, bool)
|
||||
|
||||
func init() {
|
||||
methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) {
|
||||
tlsConn, loaded := conn.(*tls.Conn)
|
||||
if !loaded {
|
||||
return nil, nil, false
|
||||
}
|
||||
return unsafe.Pointer(tlsConn), &Methods{
|
||||
readRecord: stdTLSReadRecord,
|
||||
handlePostHandshakeMessage: stdTLSHandlePostHandshakeMessage,
|
||||
writeRecordLocked: stdWriteRecordLocked,
|
||||
|
||||
setErrorLocked: stdSetErrorLocked,
|
||||
decrypt: stdDecrypt,
|
||||
setTrafficSecret: stdSetTrafficSecret,
|
||||
explicitNonceLen: stdExplicitNonceLen,
|
||||
}, true
|
||||
})
|
||||
}
|
||||
|
||||
//go:linkname stdTLSReadRecord crypto/tls.(*Conn).readRecord
|
||||
func stdTLSReadRecord(c unsafe.Pointer) error
|
||||
|
||||
//go:linkname stdTLSHandlePostHandshakeMessage crypto/tls.(*Conn).handlePostHandshakeMessage
|
||||
func stdTLSHandlePostHandshakeMessage(c unsafe.Pointer) error
|
||||
|
||||
//go:linkname stdWriteRecordLocked crypto/tls.(*Conn).writeRecordLocked
|
||||
func stdWriteRecordLocked(c unsafe.Pointer, typ uint16, data []byte) (int, error)
|
||||
|
||||
//go:linkname stdSetErrorLocked crypto/tls.(*halfConn).setErrorLocked
|
||||
func stdSetErrorLocked(hc unsafe.Pointer, err error) error
|
||||
|
||||
//go:linkname stdDecrypt crypto/tls.(*halfConn).decrypt
|
||||
func stdDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error)
|
||||
|
||||
//go:linkname stdSetTrafficSecret crypto/tls.(*halfConn).setTrafficSecret
|
||||
func stdSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte)
|
||||
|
||||
//go:linkname stdExplicitNonceLen crypto/tls.(*halfConn).explicitNonceLen
|
||||
func stdExplicitNonceLen(hc unsafe.Pointer) int
|
||||
56
common/badtls/registry_utls.go
Normal file
56
common/badtls/registry_utls.go
Normal file
@@ -0,0 +1,56 @@
|
||||
//go:build go1.25 && badlinkname
|
||||
|
||||
package badtls
|
||||
|
||||
import (
|
||||
"net"
|
||||
"unsafe"
|
||||
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/metacubex/utls"
|
||||
)
|
||||
|
||||
func init() {
|
||||
methodRegistry = append(methodRegistry, func(conn net.Conn) (unsafe.Pointer, *Methods, bool) {
|
||||
var pointer unsafe.Pointer
|
||||
if uConn, loaded := N.CastReader[*tls.Conn](conn); loaded {
|
||||
pointer = unsafe.Pointer(uConn)
|
||||
} else if uConn, loaded := N.CastReader[*tls.UConn](conn); loaded {
|
||||
pointer = unsafe.Pointer(uConn.Conn)
|
||||
} else {
|
||||
return nil, nil, false
|
||||
}
|
||||
return pointer, &Methods{
|
||||
readRecord: utlsReadRecord,
|
||||
handlePostHandshakeMessage: utlsHandlePostHandshakeMessage,
|
||||
writeRecordLocked: utlsWriteRecordLocked,
|
||||
|
||||
setErrorLocked: utlsSetErrorLocked,
|
||||
decrypt: utlsDecrypt,
|
||||
setTrafficSecret: utlsSetTrafficSecret,
|
||||
explicitNonceLen: utlsExplicitNonceLen,
|
||||
}, true
|
||||
})
|
||||
}
|
||||
|
||||
//go:linkname utlsReadRecord github.com/metacubex/utls.(*Conn).readRecord
|
||||
func utlsReadRecord(c unsafe.Pointer) error
|
||||
|
||||
//go:linkname utlsHandlePostHandshakeMessage github.com/metacubex/utls.(*Conn).handlePostHandshakeMessage
|
||||
func utlsHandlePostHandshakeMessage(c unsafe.Pointer) error
|
||||
|
||||
//go:linkname utlsWriteRecordLocked github.com/metacubex/utls.(*Conn).writeRecordLocked
|
||||
func utlsWriteRecordLocked(hc unsafe.Pointer, typ uint16, data []byte) (int, error)
|
||||
|
||||
//go:linkname utlsSetErrorLocked github.com/metacubex/utls.(*halfConn).setErrorLocked
|
||||
func utlsSetErrorLocked(hc unsafe.Pointer, err error) error
|
||||
|
||||
//go:linkname utlsDecrypt github.com/metacubex/utls.(*halfConn).decrypt
|
||||
func utlsDecrypt(hc unsafe.Pointer, record []byte) ([]byte, uint8, error)
|
||||
|
||||
//go:linkname utlsSetTrafficSecret github.com/metacubex/utls.(*halfConn).setTrafficSecret
|
||||
func utlsSetTrafficSecret(hc unsafe.Pointer, suite unsafe.Pointer, level int, secret []byte)
|
||||
|
||||
//go:linkname utlsExplicitNonceLen github.com/metacubex/utls.(*halfConn).explicitNonceLen
|
||||
func utlsExplicitNonceLen(hc unsafe.Pointer) int
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"strings"
|
||||
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
@@ -16,7 +18,19 @@ type Version struct {
|
||||
PreReleaseVersion int
|
||||
}
|
||||
|
||||
func (v Version) After(anotherVersion Version) bool {
|
||||
func (v Version) LessThan(anotherVersion Version) bool {
|
||||
return !v.GreaterThanOrEqual(anotherVersion)
|
||||
}
|
||||
|
||||
func (v Version) LessThanOrEqual(anotherVersion Version) bool {
|
||||
return v == anotherVersion || anotherVersion.GreaterThan(v)
|
||||
}
|
||||
|
||||
func (v Version) GreaterThanOrEqual(anotherVersion Version) bool {
|
||||
return v == anotherVersion || v.GreaterThan(anotherVersion)
|
||||
}
|
||||
|
||||
func (v Version) GreaterThan(anotherVersion Version) bool {
|
||||
if v.Major > anotherVersion.Major {
|
||||
return true
|
||||
} else if v.Major < anotherVersion.Major {
|
||||
@@ -44,19 +58,29 @@ func (v Version) After(anotherVersion Version) bool {
|
||||
} else if v.PreReleaseVersion < anotherVersion.PreReleaseVersion {
|
||||
return false
|
||||
}
|
||||
} else if v.PreReleaseIdentifier == "rc" && anotherVersion.PreReleaseIdentifier == "beta" {
|
||||
}
|
||||
preReleaseIdentifier := parsePreReleaseIdentifier(v.PreReleaseIdentifier)
|
||||
anotherPreReleaseIdentifier := parsePreReleaseIdentifier(anotherVersion.PreReleaseIdentifier)
|
||||
if preReleaseIdentifier < anotherPreReleaseIdentifier {
|
||||
return true
|
||||
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "rc" {
|
||||
return false
|
||||
} else if v.PreReleaseIdentifier == "beta" && anotherVersion.PreReleaseIdentifier == "alpha" {
|
||||
return true
|
||||
} else if v.PreReleaseIdentifier == "alpha" && anotherVersion.PreReleaseIdentifier == "beta" {
|
||||
} else if preReleaseIdentifier > anotherPreReleaseIdentifier {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parsePreReleaseIdentifier(identifier string) int {
|
||||
if strings.HasPrefix(identifier, "rc") {
|
||||
return 1
|
||||
} else if strings.HasPrefix(identifier, "beta") {
|
||||
return 2
|
||||
} else if strings.HasPrefix(identifier, "alpha") {
|
||||
return 3
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (v Version) VersionString() string {
|
||||
return F.ToString(v.Major, ".", v.Minor, ".", v.Patch)
|
||||
}
|
||||
@@ -83,6 +107,10 @@ func (v Version) BadString() string {
|
||||
return version
|
||||
}
|
||||
|
||||
func IsValid(versionName string) bool {
|
||||
return semver.IsValid("v" + versionName)
|
||||
}
|
||||
|
||||
func Parse(versionName string) (version Version) {
|
||||
if strings.HasPrefix(versionName, "v") {
|
||||
versionName = versionName[1:]
|
||||
|
||||
@@ -10,9 +10,9 @@ func TestCompareVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, "1.3.0-beta.1", Parse("v1.3.0-beta1").String())
|
||||
require.Equal(t, "1.3-beta1", Parse("v1.3.0-beta.1").BadString())
|
||||
require.True(t, Parse("1.3.0").After(Parse("1.3-beta1")))
|
||||
require.True(t, Parse("1.3.0").After(Parse("1.3.0-beta1")))
|
||||
require.True(t, Parse("1.3.0-beta1").After(Parse("1.3.0-alpha1")))
|
||||
require.True(t, Parse("1.3.1").After(Parse("1.3.0")))
|
||||
require.True(t, Parse("1.4").After(Parse("1.3")))
|
||||
require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3-beta1")))
|
||||
require.True(t, Parse("1.3.0").GreaterThan(Parse("1.3.0-beta1")))
|
||||
require.True(t, Parse("1.3.0-beta1").GreaterThan(Parse("1.3.0-alpha1")))
|
||||
require.True(t, Parse("1.3.1").GreaterThan(Parse("1.3.0")))
|
||||
require.True(t, Parse("1.4").GreaterThan(Parse("1.3")))
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
@@ -36,7 +35,7 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
||||
switch options.Store {
|
||||
case C.CertificateStoreSystem, "":
|
||||
systemPool = x509.NewCertPool()
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||
var systemValid bool
|
||||
if platformInterface != nil {
|
||||
for _, cert := range platformInterface.SystemCertificates() {
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"github.com/sagernet/sing-box/common/conntrack"
|
||||
"github.com/sagernet/sing-box/common/listener"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/libbox/platform"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
@@ -20,6 +19,8 @@ import (
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/database64128/tfo-go/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -28,8 +29,8 @@ var (
|
||||
)
|
||||
|
||||
type DefaultDialer struct {
|
||||
dialer4 tcpDialer
|
||||
dialer6 tcpDialer
|
||||
dialer4 tfo.Dialer
|
||||
dialer6 tfo.Dialer
|
||||
udpDialer4 net.Dialer
|
||||
udpDialer6 net.Dialer
|
||||
udpListener net.ListenConfig
|
||||
@@ -47,7 +48,7 @@ type DefaultDialer struct {
|
||||
|
||||
func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDialer, error) {
|
||||
networkManager := service.FromContext[adapter.NetworkManager](ctx)
|
||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||
|
||||
var (
|
||||
dialer net.Dialer
|
||||
@@ -88,7 +89,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
|
||||
if networkManager != nil {
|
||||
defaultOptions := networkManager.DefaultOptions()
|
||||
if defaultOptions.BindInterface != "" && !disableDefaultBind {
|
||||
if defaultOptions.BindInterface != "" {
|
||||
bindFunc := control.BindToInterface(networkManager.InterfaceFinder(), defaultOptions.BindInterface, -1)
|
||||
dialer.Control = control.Append(dialer.Control, bindFunc)
|
||||
listener.Control = control.Append(listener.Control, bindFunc)
|
||||
@@ -142,7 +143,8 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
dialer.Timeout = C.TCPConnectTimeout
|
||||
}
|
||||
// TODO: Add an option to customize the keep alive period
|
||||
setKeepAliveConfig(&dialer, C.TCPKeepAliveInitial, C.TCPKeepAliveInterval)
|
||||
dialer.KeepAlive = C.TCPKeepAliveInitial
|
||||
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(C.TCPKeepAliveInitial, C.TCPKeepAliveInterval))
|
||||
var udpFragment bool
|
||||
if options.UDPFragment != nil {
|
||||
udpFragment = *options.UDPFragment
|
||||
@@ -176,19 +178,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
udpAddr6 = M.SocksaddrFrom(bindAddr, 0).String()
|
||||
}
|
||||
if options.TCPMultiPath {
|
||||
if !go121Available {
|
||||
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
|
||||
}
|
||||
setMultiPathTCP(&dialer4)
|
||||
}
|
||||
tcpDialer4, err := newTCPDialer(dialer4, options.TCPFastOpen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpDialer6, err := newTCPDialer(dialer6, options.TCPFastOpen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
dialer4.SetMultipathTCP(true)
|
||||
}
|
||||
tcpDialer4 := tfo.Dialer{Dialer: dialer4, DisableTFO: !options.TCPFastOpen}
|
||||
tcpDialer6 := tfo.Dialer{Dialer: dialer6, DisableTFO: !options.TCPFastOpen}
|
||||
return &DefaultDialer{
|
||||
dialer4: tcpDialer4,
|
||||
dialer6: tcpDialer6,
|
||||
@@ -268,7 +261,7 @@ func (d *DefaultDialer) DialParallelInterface(ctx context.Context, network strin
|
||||
}
|
||||
var dialer net.Dialer
|
||||
if N.NetworkName(network) == N.NetworkTCP {
|
||||
dialer = dialerFromTCPDialer(d.dialer4)
|
||||
dialer = d.dialer4.Dialer
|
||||
} else {
|
||||
dialer = d.udpDialer4
|
||||
}
|
||||
@@ -314,6 +307,14 @@ func (d *DefaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) DialerForICMPDestination(destination netip.Addr) net.Dialer {
|
||||
if !destination.Is6() {
|
||||
return d.dialer6.Dialer
|
||||
} else {
|
||||
return d.dialer4.Dialer
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destination M.Socksaddr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, error) {
|
||||
if strategy == nil {
|
||||
strategy = d.networkStrategy
|
||||
@@ -347,18 +348,8 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
|
||||
return trackPacketConn(packetConn, nil)
|
||||
}
|
||||
|
||||
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
|
||||
udpListener := d.udpListener
|
||||
udpListener.Control = control.Append(udpListener.Control, func(network, address string, conn syscall.RawConn) error {
|
||||
for _, wgControlFn := range WgControlFns {
|
||||
err := wgControlFn(network, address, conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return udpListener.ListenPacket(context.Background(), network, address)
|
||||
func (d *DefaultDialer) WireGuardControl() control.Func {
|
||||
return d.udpListener.Control
|
||||
}
|
||||
|
||||
func trackConn(conn net.Conn, err error) (net.Conn, error) {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
//go:build go1.20
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/metacubex/tfo-go"
|
||||
)
|
||||
|
||||
type tcpDialer = tfo.Dialer
|
||||
|
||||
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
||||
return tfo.Dialer{Dialer: dialer, DisableTFO: !tfoEnabled}, nil
|
||||
}
|
||||
|
||||
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
|
||||
return dialer.Dialer
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build go1.21
|
||||
|
||||
package dialer
|
||||
|
||||
import "net"
|
||||
|
||||
const go121Available = true
|
||||
|
||||
func setMultiPathTCP(dialer *net.Dialer) {
|
||||
dialer.SetMultipathTCP(true)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//go:build go1.23
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setKeepAliveConfig(dialer *net.Dialer, idle time.Duration, interval time.Duration) {
|
||||
dialer.KeepAliveConfig = net.KeepAliveConfig{
|
||||
Enable: true,
|
||||
Idle: idle,
|
||||
Interval: interval,
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
//go:build !go1.20
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type tcpDialer = net.Dialer
|
||||
|
||||
func newTCPDialer(dialer net.Dialer, tfoEnabled bool) (tcpDialer, error) {
|
||||
if tfoEnabled {
|
||||
return dialer, E.New("TCP Fast Open requires go1.20, please recompile your binary.")
|
||||
}
|
||||
return dialer, nil
|
||||
}
|
||||
|
||||
func dialerFromTCPDialer(dialer tcpDialer) net.Dialer {
|
||||
return dialer
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
)
|
||||
|
||||
const go121Available = false
|
||||
|
||||
func setMultiPathTCP(dialer *net.Dialer) {
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//go:build !go1.23
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
func setKeepAliveConfig(dialer *net.Dialer, idle time.Duration, interval time.Duration) {
|
||||
dialer.KeepAlive = idle
|
||||
dialer.Control = control.Append(dialer.Control, control.SetKeepAlivePeriod(idle, interval))
|
||||
}
|
||||
@@ -145,7 +145,3 @@ type ParallelNetworkDialer interface {
|
||||
DialParallelNetwork(ctx context.Context, network string, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.Conn, error)
|
||||
ListenSerialNetworkPacket(ctx context.Context, destination M.Socksaddr, destinationAddresses []netip.Addr, strategy *C.NetworkStrategy, interfaceType []C.InterfaceType, fallbackInterfaceType []C.InterfaceType, fallbackDelay time.Duration) (net.PacketConn, netip.Addr, error)
|
||||
}
|
||||
|
||||
type PacketDialerWithDestination interface {
|
||||
ListenPacketWithDestination(ctx context.Context, destination M.Socksaddr) (net.PacketConn, netip.Addr, error)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
//go:build go1.20
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
@@ -16,7 +14,7 @@ import (
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"github.com/metacubex/tfo-go"
|
||||
"github.com/database64128/tfo-go/v2"
|
||||
)
|
||||
|
||||
type slowOpenConn struct {
|
||||
@@ -32,7 +30,7 @@ type slowOpenConn struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, 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 {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP, N.NetworkUDP:
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
//go:build !go1.20
|
||||
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func DialSlowContext(dialer *tcpDialer, ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||
switch N.NetworkName(network) {
|
||||
case N.NetworkTCP, N.NetworkUDP:
|
||||
return dialer.DialContext(ctx, network, destination.String())
|
||||
default:
|
||||
return dialer.DialContext(ctx, network, destination.AddrString())
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
package dialer
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
type WireGuardListener interface {
|
||||
ListenPacketCompat(network, address string) (net.PacketConn, error)
|
||||
WireGuardControl() control.Func
|
||||
}
|
||||
|
||||
var WgControlFns []control.Func
|
||||
|
||||
133
common/ktls/ktls.go
Normal file
133
common/ktls/ktls.go
Normal file
@@ -0,0 +1,133 @@
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badtls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
type Conn struct {
|
||||
aTLS.Conn
|
||||
ctx context.Context
|
||||
logger logger.ContextLogger
|
||||
conn net.Conn
|
||||
rawConn *badtls.RawConn
|
||||
syscallConn syscall.Conn
|
||||
rawSyscallConn syscall.RawConn
|
||||
readWaitOptions N.ReadWaitOptions
|
||||
kernelTx bool
|
||||
kernelRx bool
|
||||
pendingRxSplice bool
|
||||
}
|
||||
|
||||
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
|
||||
err := Load()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
syscallConn, isSyscallConn := N.CastReader[interface {
|
||||
io.Reader
|
||||
syscall.Conn
|
||||
}](conn.NetConn())
|
||||
if !isSyscallConn {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
rawSyscallConn, err := syscallConn.SyscallConn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawConn, err := badtls.NewRawConn(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *rawConn.Vers != tls.VersionTLS13 {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
for rawConn.RawInput.Len() > 0 {
|
||||
err = rawConn.ReadRecord()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for rawConn.Hand.Len() > 0 {
|
||||
err = rawConn.HandlePostHandshakeMessage()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "handle post-handshake messages")
|
||||
}
|
||||
}
|
||||
}
|
||||
kConn := &Conn{
|
||||
Conn: conn,
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
conn: conn.NetConn(),
|
||||
rawConn: rawConn,
|
||||
syscallConn: syscallConn,
|
||||
rawSyscallConn: rawSyscallConn,
|
||||
}
|
||||
err = kConn.setupKernel(txOffload, rxOffload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return kConn, nil
|
||||
}
|
||||
|
||||
func (c *Conn) Upstream() any {
|
||||
return c.Conn
|
||||
}
|
||||
|
||||
func (c *Conn) SyscallConnForRead() syscall.RawConn {
|
||||
if !c.kernelRx {
|
||||
return nil
|
||||
}
|
||||
if !*c.rawConn.IsClient {
|
||||
c.logger.WarnContext(c.ctx, "ktls: RX splice is unavailable on the server size, since it will cause an unknown failure")
|
||||
return nil
|
||||
}
|
||||
c.logger.DebugContext(c.ctx, "ktls: RX splice requested")
|
||||
return c.rawSyscallConn
|
||||
}
|
||||
|
||||
func (c *Conn) HandleSyscallReadError(inputErr error) ([]byte, error) {
|
||||
if errors.Is(inputErr, unix.EINVAL) {
|
||||
c.pendingRxSplice = true
|
||||
err := c.readRecord()
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "ktls: handle non-application-data record")
|
||||
}
|
||||
var input bytes.Buffer
|
||||
if c.rawConn.Input.Len() > 0 {
|
||||
_, err = c.rawConn.Input.WriteTo(&input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return input.Bytes(), nil
|
||||
} else if errors.Is(inputErr, unix.EBADMSG) {
|
||||
return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertBadRecordMAC))
|
||||
} else {
|
||||
return nil, E.Cause(inputErr, "ktls: unexpected errno")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Conn) SyscallConnForWrite() syscall.RawConn {
|
||||
if !c.kernelTx {
|
||||
return nil
|
||||
}
|
||||
c.logger.DebugContext(c.ctx, "ktls: TX splice requested")
|
||||
return c.rawSyscallConn
|
||||
}
|
||||
80
common/ktls/ktls_alert.go
Normal file
80
common/ktls/ktls_alert.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
)
|
||||
|
||||
const (
|
||||
// alert level
|
||||
alertLevelWarning = 1
|
||||
alertLevelError = 2
|
||||
)
|
||||
|
||||
const (
|
||||
alertCloseNotify = 0
|
||||
alertUnexpectedMessage = 10
|
||||
alertBadRecordMAC = 20
|
||||
alertDecryptionFailed = 21
|
||||
alertRecordOverflow = 22
|
||||
alertDecompressionFailure = 30
|
||||
alertHandshakeFailure = 40
|
||||
alertBadCertificate = 42
|
||||
alertUnsupportedCertificate = 43
|
||||
alertCertificateRevoked = 44
|
||||
alertCertificateExpired = 45
|
||||
alertCertificateUnknown = 46
|
||||
alertIllegalParameter = 47
|
||||
alertUnknownCA = 48
|
||||
alertAccessDenied = 49
|
||||
alertDecodeError = 50
|
||||
alertDecryptError = 51
|
||||
alertExportRestriction = 60
|
||||
alertProtocolVersion = 70
|
||||
alertInsufficientSecurity = 71
|
||||
alertInternalError = 80
|
||||
alertInappropriateFallback = 86
|
||||
alertUserCanceled = 90
|
||||
alertNoRenegotiation = 100
|
||||
alertMissingExtension = 109
|
||||
alertUnsupportedExtension = 110
|
||||
alertCertificateUnobtainable = 111
|
||||
alertUnrecognizedName = 112
|
||||
alertBadCertificateStatusResponse = 113
|
||||
alertBadCertificateHashValue = 114
|
||||
alertUnknownPSKIdentity = 115
|
||||
alertCertificateRequired = 116
|
||||
alertNoApplicationProtocol = 120
|
||||
alertECHRequired = 121
|
||||
)
|
||||
|
||||
func (c *Conn) sendAlertLocked(err uint8) error {
|
||||
switch err {
|
||||
case alertNoRenegotiation, alertCloseNotify:
|
||||
c.rawConn.Tmp[0] = alertLevelWarning
|
||||
default:
|
||||
c.rawConn.Tmp[0] = alertLevelError
|
||||
}
|
||||
c.rawConn.Tmp[1] = byte(err)
|
||||
|
||||
_, writeErr := c.writeRecordLocked(recordTypeAlert, c.rawConn.Tmp[0:2])
|
||||
if err == alertCloseNotify {
|
||||
// closeNotify is a special case in that it isn't an error.
|
||||
return writeErr
|
||||
}
|
||||
|
||||
return c.rawConn.Out.SetErrorLocked(&net.OpError{Op: "local error", Err: tls.AlertError(err)})
|
||||
}
|
||||
|
||||
// sendAlert sends a TLS alert message.
|
||||
func (c *Conn) sendAlert(err uint8) error {
|
||||
c.rawConn.Out.Lock()
|
||||
defer c.rawConn.Out.Unlock()
|
||||
return c.sendAlertLocked(err)
|
||||
}
|
||||
326
common/ktls/ktls_cipher_suites_linux.go
Normal file
326
common/ktls/ktls_cipher_suites_linux.go
Normal file
@@ -0,0 +1,326 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badtls"
|
||||
)
|
||||
|
||||
type kernelCryptoCipherType uint16
|
||||
|
||||
const (
|
||||
TLS_CIPHER_AES_GCM_128 kernelCryptoCipherType = 51
|
||||
TLS_CIPHER_AES_GCM_128_IV_SIZE kernelCryptoCipherType = 8
|
||||
TLS_CIPHER_AES_GCM_128_KEY_SIZE kernelCryptoCipherType = 16
|
||||
TLS_CIPHER_AES_GCM_128_SALT_SIZE kernelCryptoCipherType = 4
|
||||
TLS_CIPHER_AES_GCM_128_TAG_SIZE kernelCryptoCipherType = 16
|
||||
TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||
|
||||
TLS_CIPHER_AES_GCM_256 kernelCryptoCipherType = 52
|
||||
TLS_CIPHER_AES_GCM_256_IV_SIZE kernelCryptoCipherType = 8
|
||||
TLS_CIPHER_AES_GCM_256_KEY_SIZE kernelCryptoCipherType = 32
|
||||
TLS_CIPHER_AES_GCM_256_SALT_SIZE kernelCryptoCipherType = 4
|
||||
TLS_CIPHER_AES_GCM_256_TAG_SIZE kernelCryptoCipherType = 16
|
||||
TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||
|
||||
TLS_CIPHER_AES_CCM_128 kernelCryptoCipherType = 53
|
||||
TLS_CIPHER_AES_CCM_128_IV_SIZE kernelCryptoCipherType = 8
|
||||
TLS_CIPHER_AES_CCM_128_KEY_SIZE kernelCryptoCipherType = 16
|
||||
TLS_CIPHER_AES_CCM_128_SALT_SIZE kernelCryptoCipherType = 4
|
||||
TLS_CIPHER_AES_CCM_128_TAG_SIZE kernelCryptoCipherType = 16
|
||||
TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||
|
||||
TLS_CIPHER_CHACHA20_POLY1305 kernelCryptoCipherType = 54
|
||||
TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE kernelCryptoCipherType = 12
|
||||
TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE kernelCryptoCipherType = 32
|
||||
TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE kernelCryptoCipherType = 0
|
||||
TLS_CIPHER_CHACHA20_POLY1305_TAG_SIZE kernelCryptoCipherType = 16
|
||||
TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||
|
||||
// TLS_CIPHER_SM4_GCM kernelCryptoCipherType = 55
|
||||
// TLS_CIPHER_SM4_GCM_IV_SIZE kernelCryptoCipherType = 8
|
||||
// TLS_CIPHER_SM4_GCM_KEY_SIZE kernelCryptoCipherType = 16
|
||||
// TLS_CIPHER_SM4_GCM_SALT_SIZE kernelCryptoCipherType = 4
|
||||
// TLS_CIPHER_SM4_GCM_TAG_SIZE kernelCryptoCipherType = 16
|
||||
// TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||
|
||||
// TLS_CIPHER_SM4_CCM kernelCryptoCipherType = 56
|
||||
// TLS_CIPHER_SM4_CCM_IV_SIZE kernelCryptoCipherType = 8
|
||||
// TLS_CIPHER_SM4_CCM_KEY_SIZE kernelCryptoCipherType = 16
|
||||
// TLS_CIPHER_SM4_CCM_SALT_SIZE kernelCryptoCipherType = 4
|
||||
// TLS_CIPHER_SM4_CCM_TAG_SIZE kernelCryptoCipherType = 16
|
||||
// TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||
|
||||
TLS_CIPHER_ARIA_GCM_128 kernelCryptoCipherType = 57
|
||||
TLS_CIPHER_ARIA_GCM_128_IV_SIZE kernelCryptoCipherType = 8
|
||||
TLS_CIPHER_ARIA_GCM_128_KEY_SIZE kernelCryptoCipherType = 16
|
||||
TLS_CIPHER_ARIA_GCM_128_SALT_SIZE kernelCryptoCipherType = 4
|
||||
TLS_CIPHER_ARIA_GCM_128_TAG_SIZE kernelCryptoCipherType = 16
|
||||
TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||
|
||||
TLS_CIPHER_ARIA_GCM_256 kernelCryptoCipherType = 58
|
||||
TLS_CIPHER_ARIA_GCM_256_IV_SIZE kernelCryptoCipherType = 8
|
||||
TLS_CIPHER_ARIA_GCM_256_KEY_SIZE kernelCryptoCipherType = 32
|
||||
TLS_CIPHER_ARIA_GCM_256_SALT_SIZE kernelCryptoCipherType = 4
|
||||
TLS_CIPHER_ARIA_GCM_256_TAG_SIZE kernelCryptoCipherType = 16
|
||||
TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE kernelCryptoCipherType = 8
|
||||
)
|
||||
|
||||
type kernelCrypto interface {
|
||||
String() string
|
||||
}
|
||||
|
||||
type kernelCryptoInfo struct {
|
||||
version uint16
|
||||
cipher_type kernelCryptoCipherType
|
||||
}
|
||||
|
||||
var _ kernelCrypto = &kernelCryptoAES128GCM{}
|
||||
|
||||
type kernelCryptoAES128GCM struct {
|
||||
kernelCryptoInfo
|
||||
iv [TLS_CIPHER_AES_GCM_128_IV_SIZE]byte
|
||||
key [TLS_CIPHER_AES_GCM_128_KEY_SIZE]byte
|
||||
salt [TLS_CIPHER_AES_GCM_128_SALT_SIZE]byte
|
||||
rec_seq [TLS_CIPHER_AES_GCM_128_REC_SEQ_SIZE]byte
|
||||
}
|
||||
|
||||
func (crypto *kernelCryptoAES128GCM) String() string {
|
||||
crypto.cipher_type = TLS_CIPHER_AES_GCM_128
|
||||
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||
}
|
||||
|
||||
var _ kernelCrypto = &kernelCryptoAES256GCM{}
|
||||
|
||||
type kernelCryptoAES256GCM struct {
|
||||
kernelCryptoInfo
|
||||
iv [TLS_CIPHER_AES_GCM_256_IV_SIZE]byte
|
||||
key [TLS_CIPHER_AES_GCM_256_KEY_SIZE]byte
|
||||
salt [TLS_CIPHER_AES_GCM_256_SALT_SIZE]byte
|
||||
rec_seq [TLS_CIPHER_AES_GCM_256_REC_SEQ_SIZE]byte
|
||||
}
|
||||
|
||||
func (crypto *kernelCryptoAES256GCM) String() string {
|
||||
crypto.cipher_type = TLS_CIPHER_AES_GCM_256
|
||||
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||
}
|
||||
|
||||
var _ kernelCrypto = &kernelCryptoAES128CCM{}
|
||||
|
||||
type kernelCryptoAES128CCM struct {
|
||||
kernelCryptoInfo
|
||||
iv [TLS_CIPHER_AES_CCM_128_IV_SIZE]byte
|
||||
key [TLS_CIPHER_AES_CCM_128_KEY_SIZE]byte
|
||||
salt [TLS_CIPHER_AES_CCM_128_SALT_SIZE]byte
|
||||
rec_seq [TLS_CIPHER_AES_CCM_128_REC_SEQ_SIZE]byte
|
||||
}
|
||||
|
||||
func (crypto *kernelCryptoAES128CCM) String() string {
|
||||
crypto.cipher_type = TLS_CIPHER_AES_CCM_128
|
||||
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||
}
|
||||
|
||||
var _ kernelCrypto = &kernelCryptoChacha20Poly1035{}
|
||||
|
||||
type kernelCryptoChacha20Poly1035 struct {
|
||||
kernelCryptoInfo
|
||||
iv [TLS_CIPHER_CHACHA20_POLY1305_IV_SIZE]byte
|
||||
key [TLS_CIPHER_CHACHA20_POLY1305_KEY_SIZE]byte
|
||||
salt [TLS_CIPHER_CHACHA20_POLY1305_SALT_SIZE]byte
|
||||
rec_seq [TLS_CIPHER_CHACHA20_POLY1305_REC_SEQ_SIZE]byte
|
||||
}
|
||||
|
||||
func (crypto *kernelCryptoChacha20Poly1035) String() string {
|
||||
crypto.cipher_type = TLS_CIPHER_CHACHA20_POLY1305
|
||||
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||
}
|
||||
|
||||
// var _ kernelCrypto = &kernelCryptoSM4GCM{}
|
||||
|
||||
// type kernelCryptoSM4GCM struct {
|
||||
// kernelCryptoInfo
|
||||
// iv [TLS_CIPHER_SM4_GCM_IV_SIZE]byte
|
||||
// key [TLS_CIPHER_SM4_GCM_KEY_SIZE]byte
|
||||
// salt [TLS_CIPHER_SM4_GCM_SALT_SIZE]byte
|
||||
// rec_seq [TLS_CIPHER_SM4_GCM_REC_SEQ_SIZE]byte
|
||||
// }
|
||||
|
||||
// func (crypto *kernelCryptoSM4GCM) String() string {
|
||||
// crypto.cipher_type = TLS_CIPHER_SM4_GCM
|
||||
// return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||
// }
|
||||
|
||||
// var _ kernelCrypto = &kernelCryptoSM4CCM{}
|
||||
|
||||
// type kernelCryptoSM4CCM struct {
|
||||
// kernelCryptoInfo
|
||||
// iv [TLS_CIPHER_SM4_CCM_IV_SIZE]byte
|
||||
// key [TLS_CIPHER_SM4_CCM_KEY_SIZE]byte
|
||||
// salt [TLS_CIPHER_SM4_CCM_SALT_SIZE]byte
|
||||
// rec_seq [TLS_CIPHER_SM4_CCM_REC_SEQ_SIZE]byte
|
||||
// }
|
||||
|
||||
// func (crypto *kernelCryptoSM4CCM) String() string {
|
||||
// crypto.cipher_type = TLS_CIPHER_SM4_CCM
|
||||
// return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||
// }
|
||||
|
||||
var _ kernelCrypto = &kernelCryptoARIA128GCM{}
|
||||
|
||||
type kernelCryptoARIA128GCM struct {
|
||||
kernelCryptoInfo
|
||||
iv [TLS_CIPHER_ARIA_GCM_128_IV_SIZE]byte
|
||||
key [TLS_CIPHER_ARIA_GCM_128_KEY_SIZE]byte
|
||||
salt [TLS_CIPHER_ARIA_GCM_128_SALT_SIZE]byte
|
||||
rec_seq [TLS_CIPHER_ARIA_GCM_128_REC_SEQ_SIZE]byte
|
||||
}
|
||||
|
||||
func (crypto *kernelCryptoARIA128GCM) String() string {
|
||||
crypto.cipher_type = TLS_CIPHER_ARIA_GCM_128
|
||||
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||
}
|
||||
|
||||
var _ kernelCrypto = &kernelCryptoARIA256GCM{}
|
||||
|
||||
type kernelCryptoARIA256GCM struct {
|
||||
kernelCryptoInfo
|
||||
iv [TLS_CIPHER_ARIA_GCM_256_IV_SIZE]byte
|
||||
key [TLS_CIPHER_ARIA_GCM_256_KEY_SIZE]byte
|
||||
salt [TLS_CIPHER_ARIA_GCM_256_SALT_SIZE]byte
|
||||
rec_seq [TLS_CIPHER_ARIA_GCM_256_REC_SEQ_SIZE]byte
|
||||
}
|
||||
|
||||
func (crypto *kernelCryptoARIA256GCM) String() string {
|
||||
crypto.cipher_type = TLS_CIPHER_ARIA_GCM_256
|
||||
return string((*[unsafe.Sizeof(*crypto)]byte)(unsafe.Pointer(crypto))[:])
|
||||
}
|
||||
|
||||
func kernelCipher(kernel *Support, hc *badtls.RawHalfConn, cipherSuite uint16, isRX bool) kernelCrypto {
|
||||
if !kernel.TLS {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch *hc.Version {
|
||||
case tls.VersionTLS12:
|
||||
if isRX && !kernel.TLS_Version13_RX {
|
||||
return nil
|
||||
}
|
||||
|
||||
case tls.VersionTLS13:
|
||||
if !kernel.TLS_Version13 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if isRX && !kernel.TLS_Version13_RX {
|
||||
return nil
|
||||
}
|
||||
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
var key, iv []byte
|
||||
if *hc.Version == tls.VersionTLS13 {
|
||||
key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), *hc.TrafficSecret)
|
||||
/*if isRX {
|
||||
key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.RemoteTrafficSecret)
|
||||
} else {
|
||||
key, iv = trafficKey(cipherSuiteTLS13ByID(cipherSuite), keyLog.TrafficSecret)
|
||||
}*/
|
||||
} else {
|
||||
// csPtr := cipherSuiteByID(cipherSuite)
|
||||
// keysFromMasterSecret(*hc.Version, csPtr, keyLog.Secret, keyLog.Random)
|
||||
return nil
|
||||
}
|
||||
|
||||
switch cipherSuite {
|
||||
case tls.TLS_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
|
||||
crypto := new(kernelCryptoAES128GCM)
|
||||
|
||||
crypto.version = *hc.Version
|
||||
copy(crypto.key[:], key)
|
||||
copy(crypto.iv[:], iv[4:])
|
||||
copy(crypto.salt[:], iv[:4])
|
||||
crypto.rec_seq = *hc.Seq
|
||||
|
||||
return crypto
|
||||
case tls.TLS_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
|
||||
if !kernel.TLS_AES_256_GCM {
|
||||
return nil
|
||||
}
|
||||
|
||||
crypto := new(kernelCryptoAES256GCM)
|
||||
|
||||
crypto.version = *hc.Version
|
||||
copy(crypto.key[:], key)
|
||||
copy(crypto.iv[:], iv[4:])
|
||||
copy(crypto.salt[:], iv[:4])
|
||||
crypto.rec_seq = *hc.Seq
|
||||
|
||||
return crypto
|
||||
//case tls.TLS_AES_128_CCM_SHA256, tls.TLS_RSA_WITH_AES_128_CCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_SHA256:
|
||||
// if !kernel.TLS_AES_128_CCM {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// crypto := new(kernelCryptoAES128CCM)
|
||||
//
|
||||
// crypto.version = *hc.Version
|
||||
// copy(crypto.key[:], key)
|
||||
// copy(crypto.iv[:], iv[4:])
|
||||
// copy(crypto.salt[:], iv[:4])
|
||||
// crypto.rec_seq = *hc.Seq
|
||||
//
|
||||
// return crypto
|
||||
case tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
|
||||
if !kernel.TLS_CHACHA20_POLY1305 {
|
||||
return nil
|
||||
}
|
||||
|
||||
crypto := new(kernelCryptoChacha20Poly1035)
|
||||
|
||||
crypto.version = *hc.Version
|
||||
copy(crypto.key[:], key)
|
||||
copy(crypto.iv[:], iv)
|
||||
crypto.rec_seq = *hc.Seq
|
||||
|
||||
return crypto
|
||||
//case tls.TLS_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_ARIA_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_ARIA_128_GCM_SHA256:
|
||||
// if !kernel.TLS_ARIA_GCM {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// crypto := new(kernelCryptoARIA128GCM)
|
||||
//
|
||||
// crypto.version = *hc.Version
|
||||
// copy(crypto.key[:], key)
|
||||
// copy(crypto.iv[:], iv[4:])
|
||||
// copy(crypto.salt[:], iv[:4])
|
||||
// crypto.rec_seq = *hc.Seq
|
||||
//
|
||||
// return crypto
|
||||
//case tls.TLS_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_ARIA_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_ARIA_256_GCM_SHA384:
|
||||
// if !kernel.TLS_ARIA_GCM {
|
||||
// return nil
|
||||
// }
|
||||
//
|
||||
// crypto := new(kernelCryptoARIA256GCM)
|
||||
//
|
||||
// crypto.version = *hc.Version
|
||||
// copy(crypto.key[:], key)
|
||||
// copy(crypto.iv[:], iv[4:])
|
||||
// copy(crypto.salt[:], iv[:4])
|
||||
// crypto.rec_seq = *hc.Seq
|
||||
//
|
||||
// return crypto
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
67
common/ktls/ktls_close.go
Normal file
67
common/ktls/ktls_close.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (c *Conn) Close() error {
|
||||
if !c.kernelTx {
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
// Interlock with Conn.Write above.
|
||||
var x int32
|
||||
for {
|
||||
x = c.rawConn.ActiveCall.Load()
|
||||
if x&1 != 0 {
|
||||
return net.ErrClosed
|
||||
}
|
||||
if c.rawConn.ActiveCall.CompareAndSwap(x, x|1) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if x != 0 {
|
||||
// io.Writer and io.Closer should not be used concurrently.
|
||||
// If Close is called while a Write is currently in-flight,
|
||||
// interpret that as a sign that this Close is really just
|
||||
// being used to break the Write and/or clean up resources and
|
||||
// avoid sending the alertCloseNotify, which may block
|
||||
// waiting on handshakeMutex or the c.out mutex.
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
var alertErr error
|
||||
if c.rawConn.IsHandshakeComplete.Load() {
|
||||
if err := c.closeNotify(); err != nil {
|
||||
alertErr = fmt.Errorf("tls: failed to send closeNotify alert (but connection was closed anyway): %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.conn.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return alertErr
|
||||
}
|
||||
|
||||
func (c *Conn) closeNotify() error {
|
||||
c.rawConn.Out.Lock()
|
||||
defer c.rawConn.Out.Unlock()
|
||||
|
||||
if !*c.rawConn.CloseNotifySent {
|
||||
// Set a Write Deadline to prevent possibly blocking forever.
|
||||
c.SetWriteDeadline(time.Now().Add(time.Second * 5))
|
||||
*c.rawConn.CloseNotifyErr = c.sendAlertLocked(alertCloseNotify)
|
||||
*c.rawConn.CloseNotifySent = true
|
||||
// Any subsequent writes will fail.
|
||||
c.SetWriteDeadline(time.Now())
|
||||
}
|
||||
return *c.rawConn.CloseNotifyErr
|
||||
}
|
||||
24
common/ktls/ktls_const.go
Normal file
24
common/ktls/ktls_const.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
const (
|
||||
maxPlaintext = 16384 // maximum plaintext payload length
|
||||
maxCiphertext = 16384 + 2048 // maximum ciphertext payload length
|
||||
maxCiphertextTLS13 = 16384 + 256 // maximum ciphertext length in TLS 1.3
|
||||
recordHeaderLen = 5 // record header length
|
||||
maxHandshake = 65536 // maximum handshake we support (protocol max is 16 MB)
|
||||
maxHandshakeCertificateMsg = 262144 // maximum certificate message size (256 KiB)
|
||||
maxUselessRecords = 16 // maximum number of consecutive non-advancing records
|
||||
)
|
||||
|
||||
const (
|
||||
recordTypeChangeCipherSpec = 20
|
||||
recordTypeAlert = 21
|
||||
recordTypeHandshake = 22
|
||||
recordTypeApplicationData = 23
|
||||
)
|
||||
238
common/ktls/ktls_handshake_messages.go
Normal file
238
common/ktls/ktls_handshake_messages.go
Normal file
@@ -0,0 +1,238 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
)
|
||||
|
||||
// The marshalingFunction type is an adapter to allow the use of ordinary
|
||||
// functions as cryptobyte.MarshalingValue.
|
||||
type marshalingFunction func(b *cryptobyte.Builder) error
|
||||
|
||||
func (f marshalingFunction) Marshal(b *cryptobyte.Builder) error {
|
||||
return f(b)
|
||||
}
|
||||
|
||||
// addBytesWithLength appends a sequence of bytes to the cryptobyte.Builder. If
|
||||
// the length of the sequence is not the value specified, it produces an error.
|
||||
func addBytesWithLength(b *cryptobyte.Builder, v []byte, n int) {
|
||||
b.AddValue(marshalingFunction(func(b *cryptobyte.Builder) error {
|
||||
if len(v) != n {
|
||||
return fmt.Errorf("invalid value length: expected %d, got %d", n, len(v))
|
||||
}
|
||||
b.AddBytes(v)
|
||||
return nil
|
||||
}))
|
||||
}
|
||||
|
||||
// addUint64 appends a big-endian, 64-bit value to the cryptobyte.Builder.
|
||||
func addUint64(b *cryptobyte.Builder, v uint64) {
|
||||
b.AddUint32(uint32(v >> 32))
|
||||
b.AddUint32(uint32(v))
|
||||
}
|
||||
|
||||
// readUint64 decodes a big-endian, 64-bit value into out and advances over it.
|
||||
// It reports whether the read was successful.
|
||||
func readUint64(s *cryptobyte.String, out *uint64) bool {
|
||||
var hi, lo uint32
|
||||
if !s.ReadUint32(&hi) || !s.ReadUint32(&lo) {
|
||||
return false
|
||||
}
|
||||
*out = uint64(hi)<<32 | uint64(lo)
|
||||
return true
|
||||
}
|
||||
|
||||
// readUint8LengthPrefixed acts like s.ReadUint8LengthPrefixed, but targets a
|
||||
// []byte instead of a cryptobyte.String.
|
||||
func readUint8LengthPrefixed(s *cryptobyte.String, out *[]byte) bool {
|
||||
return s.ReadUint8LengthPrefixed((*cryptobyte.String)(out))
|
||||
}
|
||||
|
||||
// readUint16LengthPrefixed acts like s.ReadUint16LengthPrefixed, but targets a
|
||||
// []byte instead of a cryptobyte.String.
|
||||
func readUint16LengthPrefixed(s *cryptobyte.String, out *[]byte) bool {
|
||||
return s.ReadUint16LengthPrefixed((*cryptobyte.String)(out))
|
||||
}
|
||||
|
||||
// readUint24LengthPrefixed acts like s.ReadUint24LengthPrefixed, but targets a
|
||||
// []byte instead of a cryptobyte.String.
|
||||
func readUint24LengthPrefixed(s *cryptobyte.String, out *[]byte) bool {
|
||||
return s.ReadUint24LengthPrefixed((*cryptobyte.String)(out))
|
||||
}
|
||||
|
||||
type keyUpdateMsg struct {
|
||||
updateRequested bool
|
||||
}
|
||||
|
||||
func (m *keyUpdateMsg) marshal() ([]byte, error) {
|
||||
var b cryptobyte.Builder
|
||||
b.AddUint8(typeKeyUpdate)
|
||||
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
if m.updateRequested {
|
||||
b.AddUint8(1)
|
||||
} else {
|
||||
b.AddUint8(0)
|
||||
}
|
||||
})
|
||||
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func (m *keyUpdateMsg) unmarshal(data []byte) bool {
|
||||
s := cryptobyte.String(data)
|
||||
|
||||
var updateRequested uint8
|
||||
if !s.Skip(4) || // message type and uint24 length field
|
||||
!s.ReadUint8(&updateRequested) || !s.Empty() {
|
||||
return false
|
||||
}
|
||||
switch updateRequested {
|
||||
case 0:
|
||||
m.updateRequested = false
|
||||
case 1:
|
||||
m.updateRequested = true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// TLS handshake message types.
|
||||
const (
|
||||
typeHelloRequest uint8 = 0
|
||||
typeClientHello uint8 = 1
|
||||
typeServerHello uint8 = 2
|
||||
typeNewSessionTicket uint8 = 4
|
||||
typeEndOfEarlyData uint8 = 5
|
||||
typeEncryptedExtensions uint8 = 8
|
||||
typeCertificate uint8 = 11
|
||||
typeServerKeyExchange uint8 = 12
|
||||
typeCertificateRequest uint8 = 13
|
||||
typeServerHelloDone uint8 = 14
|
||||
typeCertificateVerify uint8 = 15
|
||||
typeClientKeyExchange uint8 = 16
|
||||
typeFinished uint8 = 20
|
||||
typeCertificateStatus uint8 = 22
|
||||
typeKeyUpdate uint8 = 24
|
||||
typeCompressedCertificate uint8 = 25
|
||||
typeMessageHash uint8 = 254 // synthetic message
|
||||
)
|
||||
|
||||
// TLS compression types.
|
||||
const (
|
||||
compressionNone uint8 = 0
|
||||
)
|
||||
|
||||
// TLS extension numbers
|
||||
const (
|
||||
extensionServerName uint16 = 0
|
||||
extensionStatusRequest uint16 = 5
|
||||
extensionSupportedCurves uint16 = 10 // supported_groups in TLS 1.3, see RFC 8446, Section 4.2.7
|
||||
extensionSupportedPoints uint16 = 11
|
||||
extensionSignatureAlgorithms uint16 = 13
|
||||
extensionALPN uint16 = 16
|
||||
extensionSCT uint16 = 18
|
||||
extensionPadding uint16 = 21
|
||||
extensionExtendedMasterSecret uint16 = 23
|
||||
extensionCompressCertificate uint16 = 27 // compress_certificate in TLS 1.3
|
||||
extensionSessionTicket uint16 = 35
|
||||
extensionPreSharedKey uint16 = 41
|
||||
extensionEarlyData uint16 = 42
|
||||
extensionSupportedVersions uint16 = 43
|
||||
extensionCookie uint16 = 44
|
||||
extensionPSKModes uint16 = 45
|
||||
extensionCertificateAuthorities uint16 = 47
|
||||
extensionSignatureAlgorithmsCert uint16 = 50
|
||||
extensionKeyShare uint16 = 51
|
||||
extensionQUICTransportParameters uint16 = 57
|
||||
extensionALPS uint16 = 17513
|
||||
extensionRenegotiationInfo uint16 = 0xff01
|
||||
extensionECHOuterExtensions uint16 = 0xfd00
|
||||
extensionEncryptedClientHello uint16 = 0xfe0d
|
||||
)
|
||||
|
||||
type handshakeMessage interface {
|
||||
marshal() ([]byte, error)
|
||||
unmarshal([]byte) bool
|
||||
}
|
||||
type newSessionTicketMsgTLS13 struct {
|
||||
lifetime uint32
|
||||
ageAdd uint32
|
||||
nonce []byte
|
||||
label []byte
|
||||
maxEarlyData uint32
|
||||
}
|
||||
|
||||
func (m *newSessionTicketMsgTLS13) marshal() ([]byte, error) {
|
||||
var b cryptobyte.Builder
|
||||
b.AddUint8(typeNewSessionTicket)
|
||||
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddUint32(m.lifetime)
|
||||
b.AddUint32(m.ageAdd)
|
||||
b.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes(m.nonce)
|
||||
})
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes(m.label)
|
||||
})
|
||||
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
if m.maxEarlyData > 0 {
|
||||
b.AddUint16(extensionEarlyData)
|
||||
b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddUint32(m.maxEarlyData)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
func (m *newSessionTicketMsgTLS13) unmarshal(data []byte) bool {
|
||||
*m = newSessionTicketMsgTLS13{}
|
||||
s := cryptobyte.String(data)
|
||||
|
||||
var extensions cryptobyte.String
|
||||
if !s.Skip(4) || // message type and uint24 length field
|
||||
!s.ReadUint32(&m.lifetime) ||
|
||||
!s.ReadUint32(&m.ageAdd) ||
|
||||
!readUint8LengthPrefixed(&s, &m.nonce) ||
|
||||
!readUint16LengthPrefixed(&s, &m.label) ||
|
||||
!s.ReadUint16LengthPrefixed(&extensions) ||
|
||||
!s.Empty() {
|
||||
return false
|
||||
}
|
||||
|
||||
for !extensions.Empty() {
|
||||
var extension uint16
|
||||
var extData cryptobyte.String
|
||||
if !extensions.ReadUint16(&extension) ||
|
||||
!extensions.ReadUint16LengthPrefixed(&extData) {
|
||||
return false
|
||||
}
|
||||
|
||||
switch extension {
|
||||
case extensionEarlyData:
|
||||
if !extData.ReadUint32(&m.maxEarlyData) {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
// Ignore unknown extensions.
|
||||
continue
|
||||
}
|
||||
|
||||
if !extData.Empty() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
173
common/ktls/ktls_key_update.go
Normal file
173
common/ktls/ktls_key_update.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// handlePostHandshakeMessage processes a handshake message arrived after the
|
||||
// handshake is complete. Up to TLS 1.2, it indicates the start of a renegotiation.
|
||||
func (c *Conn) handlePostHandshakeMessage() error {
|
||||
if *c.rawConn.Vers != tls.VersionTLS13 {
|
||||
return errors.New("ktls: kernel does not support TLS 1.2 renegotiation")
|
||||
}
|
||||
|
||||
msg, err := c.readHandshake(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//c.retryCount++
|
||||
//if c.retryCount > maxUselessRecords {
|
||||
// c.sendAlert(alertUnexpectedMessage)
|
||||
// return c.in.setErrorLocked(errors.New("tls: too many non-advancing records"))
|
||||
//}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case *newSessionTicketMsgTLS13:
|
||||
// return errors.New("ktls: received new session ticket")
|
||||
return nil
|
||||
case *keyUpdateMsg:
|
||||
return c.handleKeyUpdate(msg)
|
||||
}
|
||||
// The QUIC layer is supposed to treat an unexpected post-handshake CertificateRequest
|
||||
// as a QUIC-level PROTOCOL_VIOLATION error (RFC 9001, Section 4.4). Returning an
|
||||
// unexpected_message alert here doesn't provide it with enough information to distinguish
|
||||
// this condition from other unexpected messages. This is probably fine.
|
||||
c.sendAlert(alertUnexpectedMessage)
|
||||
return fmt.Errorf("tls: received unexpected handshake message of type %T", msg)
|
||||
}
|
||||
|
||||
func (c *Conn) handleKeyUpdate(keyUpdate *keyUpdateMsg) error {
|
||||
//if c.quic != nil {
|
||||
// c.sendAlert(alertUnexpectedMessage)
|
||||
// return c.in.setErrorLocked(errors.New("tls: received unexpected key update message"))
|
||||
//}
|
||||
|
||||
cipherSuite := cipherSuiteTLS13ByID(*c.rawConn.CipherSuite)
|
||||
if cipherSuite == nil {
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertInternalError))
|
||||
}
|
||||
|
||||
newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.In.TrafficSecret)
|
||||
c.rawConn.In.SetTrafficSecret(cipherSuite, 0 /*tls.QUICEncryptionLevelInitial*/, newSecret)
|
||||
|
||||
err := c.resetupRX()
|
||||
if err != nil {
|
||||
c.sendAlert(alertInternalError)
|
||||
return c.rawConn.In.SetErrorLocked(fmt.Errorf("ktls: resetupRX failed: %w", err))
|
||||
}
|
||||
|
||||
if keyUpdate.updateRequested {
|
||||
c.rawConn.Out.Lock()
|
||||
defer c.rawConn.Out.Unlock()
|
||||
|
||||
resetup, err := c.resetupTX()
|
||||
if err != nil {
|
||||
c.sendAlertLocked(alertInternalError)
|
||||
return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err))
|
||||
}
|
||||
|
||||
msg := &keyUpdateMsg{}
|
||||
msgBytes, err := msg.marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.writeRecordLocked(recordTypeHandshake, msgBytes)
|
||||
if err != nil {
|
||||
// Surface the error at the next write.
|
||||
c.rawConn.Out.SetErrorLocked(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
newSecret := nextTrafficSecret(cipherSuite, *c.rawConn.Out.TrafficSecret)
|
||||
c.rawConn.Out.SetTrafficSecret(cipherSuite, 0 /*QUICEncryptionLevelInitial*/, newSecret)
|
||||
|
||||
err = resetup()
|
||||
if err != nil {
|
||||
return c.rawConn.Out.SetErrorLocked(fmt.Errorf("ktls: resetupTX failed: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) readHandshakeBytes(n int) error {
|
||||
//if c.quic != nil {
|
||||
// return c.quicReadHandshakeBytes(n)
|
||||
//}
|
||||
for c.rawConn.Hand.Len() < n {
|
||||
if err := c.readRecord(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) readHandshake(transcript io.Writer) (any, error) {
|
||||
if err := c.readHandshakeBytes(4); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := c.rawConn.Hand.Bytes()
|
||||
|
||||
maxHandshakeSize := maxHandshake
|
||||
// hasVers indicates we're past the first message, forcing someone trying to
|
||||
// make us just allocate a large buffer to at least do the initial part of
|
||||
// the handshake first.
|
||||
//if c.haveVers && data[0] == typeCertificate {
|
||||
// Since certificate messages are likely to be the only messages that
|
||||
// can be larger than maxHandshake, we use a special limit for just
|
||||
// those messages.
|
||||
//maxHandshakeSize = maxHandshakeCertificateMsg
|
||||
//}
|
||||
|
||||
n := int(data[1])<<16 | int(data[2])<<8 | int(data[3])
|
||||
if n > maxHandshakeSize {
|
||||
c.sendAlertLocked(alertInternalError)
|
||||
return nil, c.rawConn.In.SetErrorLocked(fmt.Errorf("tls: handshake message of length %d bytes exceeds maximum of %d bytes", n, maxHandshakeSize))
|
||||
}
|
||||
if err := c.readHandshakeBytes(4 + n); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data = c.rawConn.Hand.Next(4 + n)
|
||||
return c.unmarshalHandshakeMessage(data, transcript)
|
||||
}
|
||||
|
||||
func (c *Conn) unmarshalHandshakeMessage(data []byte, transcript io.Writer) (any, error) {
|
||||
var m handshakeMessage
|
||||
switch data[0] {
|
||||
case typeNewSessionTicket:
|
||||
if *c.rawConn.Vers == tls.VersionTLS13 {
|
||||
m = new(newSessionTicketMsgTLS13)
|
||||
} else {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
case typeKeyUpdate:
|
||||
m = new(keyUpdateMsg)
|
||||
default:
|
||||
return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
}
|
||||
|
||||
// The handshake message unmarshalers
|
||||
// expect to be able to keep references to data,
|
||||
// so pass in a fresh copy that won't be overwritten.
|
||||
data = append([]byte(nil), data...)
|
||||
|
||||
if !m.unmarshal(data) {
|
||||
return nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError))
|
||||
}
|
||||
|
||||
if transcript != nil {
|
||||
transcript.Write(data)
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
329
common/ktls/ktls_linux.go
Normal file
329
common/ktls/ktls_linux.go
Normal file
@@ -0,0 +1,329 @@
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/common/badversion"
|
||||
"github.com/sagernet/sing/common/control"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/shell"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// mod from https://gitlab.com/go-extension/tls
|
||||
|
||||
const (
|
||||
TLS_TX = 1
|
||||
TLS_RX = 2
|
||||
TLS_TX_ZEROCOPY_RO = 3 // TX zerocopy (only sendfile now)
|
||||
TLS_RX_EXPECT_NO_PAD = 4 // Attempt opportunistic zero-copy, TLS 1.3 only
|
||||
|
||||
TLS_SET_RECORD_TYPE = 1
|
||||
TLS_GET_RECORD_TYPE = 2
|
||||
)
|
||||
|
||||
type Support struct {
|
||||
TLS, TLS_RX bool
|
||||
TLS_Version13, TLS_Version13_RX bool
|
||||
|
||||
TLS_TX_ZEROCOPY bool
|
||||
TLS_RX_NOPADDING bool
|
||||
|
||||
TLS_AES_256_GCM bool
|
||||
TLS_AES_128_CCM bool
|
||||
TLS_CHACHA20_POLY1305 bool
|
||||
TLS_SM4 bool
|
||||
TLS_ARIA_GCM bool
|
||||
|
||||
TLS_Version13_KeyUpdate bool
|
||||
}
|
||||
|
||||
var KernelSupport = sync.OnceValues(func() (*Support, error) {
|
||||
var uname unix.Utsname
|
||||
err := unix.Uname(&uname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
kernelVersion := badversion.Parse(strings.Trim(string(uname.Release[:]), "\x00"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var support Support
|
||||
switch {
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 14}):
|
||||
support.TLS_Version13_KeyUpdate = true
|
||||
fallthrough
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6, Minor: 1}):
|
||||
support.TLS_ARIA_GCM = true
|
||||
fallthrough
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 6}):
|
||||
support.TLS_Version13_RX = true
|
||||
support.TLS_RX_NOPADDING = true
|
||||
fallthrough
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 19}):
|
||||
support.TLS_TX_ZEROCOPY = true
|
||||
fallthrough
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 16}):
|
||||
support.TLS_SM4 = true
|
||||
fallthrough
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 11}):
|
||||
support.TLS_CHACHA20_POLY1305 = true
|
||||
fallthrough
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 2}):
|
||||
support.TLS_AES_128_CCM = true
|
||||
fallthrough
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 5, Minor: 1}):
|
||||
support.TLS_AES_256_GCM = true
|
||||
support.TLS_Version13 = true
|
||||
fallthrough
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 17}):
|
||||
support.TLS_RX = true
|
||||
fallthrough
|
||||
case kernelVersion.GreaterThanOrEqual(badversion.Version{Major: 4, Minor: 13}):
|
||||
support.TLS = true
|
||||
}
|
||||
|
||||
if support.TLS && support.TLS_Version13 {
|
||||
_, err := os.Stat("/sys/module/tls")
|
||||
if err != nil {
|
||||
if os.Getuid() == 0 {
|
||||
output, err := shell.Exec("modprobe", "tls").Read()
|
||||
if err != nil {
|
||||
return nil, E.Extend(E.Cause(err, "modprobe tls"), output)
|
||||
}
|
||||
} else {
|
||||
return nil, E.New("ktls: kernel TLS module not loaded")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &support, nil
|
||||
})
|
||||
|
||||
func Load() error {
|
||||
support, err := KernelSupport()
|
||||
if err != nil {
|
||||
return E.Cause(err, "ktls: check availability")
|
||||
}
|
||||
if !support.TLS || !support.TLS_Version13 {
|
||||
return E.New("ktls: kernel does not support TLS 1.3")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) setupKernel(txOffload, rxOffload bool) error {
|
||||
if !txOffload && !rxOffload {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
support, err := KernelSupport()
|
||||
if err != nil {
|
||||
return E.Cause(err, "check availability")
|
||||
}
|
||||
if !support.TLS || !support.TLS_Version13 {
|
||||
return E.New("kernel does not support TLS 1.3")
|
||||
}
|
||||
c.rawConn.Out.Lock()
|
||||
defer c.rawConn.Out.Unlock()
|
||||
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||
return syscall.SetsockoptString(int(fd), unix.SOL_TCP, unix.TCP_ULP, "tls")
|
||||
})
|
||||
if err != nil {
|
||||
return os.NewSyscallError("setsockopt", err)
|
||||
}
|
||||
|
||||
if txOffload {
|
||||
txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false)
|
||||
if txCrypto == nil {
|
||||
return E.New("unsupported cipher suite")
|
||||
}
|
||||
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String())
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if support.TLS_TX_ZEROCOPY {
|
||||
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||
return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_TX_ZEROCOPY_RO, 1)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.kernelTx = true
|
||||
c.logger.DebugContext(c.ctx, "ktls: kernel TLS TX enabled")
|
||||
}
|
||||
|
||||
if rxOffload {
|
||||
rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true)
|
||||
if rxCrypto == nil {
|
||||
return E.New("unsupported cipher suite")
|
||||
}
|
||||
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String())
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *c.rawConn.Vers >= tls.VersionTLS13 && support.TLS_RX_NOPADDING {
|
||||
err = control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||
return syscall.SetsockoptInt(int(fd), unix.SOL_TLS, TLS_RX_EXPECT_NO_PAD, 1)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.kernelRx = true
|
||||
c.logger.DebugContext(c.ctx, "ktls: kernel TLS RX enabled")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) resetupTX() (func() error, error) {
|
||||
if !c.kernelTx {
|
||||
return nil, nil
|
||||
}
|
||||
support, err := KernelSupport()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !support.TLS_Version13_KeyUpdate {
|
||||
return nil, errors.New("ktls: kernel does not support rekey")
|
||||
}
|
||||
txCrypto := kernelCipher(support, c.rawConn.Out, *c.rawConn.CipherSuite, false)
|
||||
if txCrypto == nil {
|
||||
return nil, errors.New("ktls: set kernelCipher on unsupported tls session")
|
||||
}
|
||||
return func() error {
|
||||
return control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_TX, txCrypto.String())
|
||||
})
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Conn) resetupRX() error {
|
||||
if !c.kernelRx {
|
||||
return nil
|
||||
}
|
||||
support, err := KernelSupport()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !support.TLS_Version13_KeyUpdate {
|
||||
return errors.New("ktls: kernel does not support rekey")
|
||||
}
|
||||
rxCrypto := kernelCipher(support, c.rawConn.In, *c.rawConn.CipherSuite, true)
|
||||
if rxCrypto == nil {
|
||||
return errors.New("ktls: set kernelCipher on unsupported tls session")
|
||||
}
|
||||
return control.Raw(c.rawSyscallConn, func(fd uintptr) error {
|
||||
return syscall.SetsockoptString(int(fd), unix.SOL_TLS, TLS_RX, rxCrypto.String())
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Conn) readKernelRecord() (uint8, []byte, error) {
|
||||
if c.rawConn.RawInput.Len() < maxPlaintext {
|
||||
c.rawConn.RawInput.Grow(maxPlaintext - c.rawConn.RawInput.Len())
|
||||
}
|
||||
|
||||
data := c.rawConn.RawInput.Bytes()[:maxPlaintext]
|
||||
|
||||
// cmsg for record type
|
||||
buffer := make([]byte, unix.CmsgSpace(1))
|
||||
cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0]))
|
||||
cmsg.SetLen(unix.CmsgLen(1))
|
||||
|
||||
var iov unix.Iovec
|
||||
iov.Base = &data[0]
|
||||
iov.SetLen(len(data))
|
||||
|
||||
var msg unix.Msghdr
|
||||
msg.Control = &buffer[0]
|
||||
msg.Controllen = cmsg.Len
|
||||
msg.Iov = &iov
|
||||
msg.Iovlen = 1
|
||||
|
||||
var n int
|
||||
var err error
|
||||
er := c.rawSyscallConn.Read(func(fd uintptr) bool {
|
||||
n, err = recvmsg(int(fd), &msg, 0)
|
||||
return err != unix.EAGAIN || c.pendingRxSplice
|
||||
})
|
||||
if er != nil {
|
||||
return 0, nil, er
|
||||
}
|
||||
switch err {
|
||||
case nil:
|
||||
case syscall.EINVAL, syscall.EAGAIN:
|
||||
return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertProtocolVersion))
|
||||
case syscall.EMSGSIZE:
|
||||
return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow))
|
||||
case syscall.EBADMSG:
|
||||
return 0, nil, c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecryptError))
|
||||
default:
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if n <= 0 {
|
||||
return 0, nil, c.rawConn.In.SetErrorLocked(io.EOF)
|
||||
}
|
||||
|
||||
if cmsg.Level == unix.SOL_TLS && cmsg.Type == TLS_GET_RECORD_TYPE {
|
||||
typ := buffer[unix.CmsgLen(0)]
|
||||
return typ, data[:n], nil
|
||||
}
|
||||
|
||||
return recordTypeApplicationData, data[:n], nil
|
||||
}
|
||||
|
||||
func (c *Conn) writeKernelRecord(typ uint16, data []byte) (int, error) {
|
||||
if typ == recordTypeApplicationData {
|
||||
return c.conn.Write(data)
|
||||
}
|
||||
|
||||
// cmsg for record type
|
||||
buffer := make([]byte, unix.CmsgSpace(1))
|
||||
cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&buffer[0]))
|
||||
cmsg.SetLen(unix.CmsgLen(1))
|
||||
buffer[unix.CmsgLen(0)] = byte(typ)
|
||||
cmsg.Level = unix.SOL_TLS
|
||||
cmsg.Type = TLS_SET_RECORD_TYPE
|
||||
|
||||
var iov unix.Iovec
|
||||
iov.Base = &data[0]
|
||||
iov.SetLen(len(data))
|
||||
|
||||
var msg unix.Msghdr
|
||||
msg.Control = &buffer[0]
|
||||
msg.Controllen = cmsg.Len
|
||||
msg.Iov = &iov
|
||||
msg.Iovlen = 1
|
||||
|
||||
var n int
|
||||
var err error
|
||||
ew := c.rawSyscallConn.Write(func(fd uintptr) bool {
|
||||
n, err = sendmsg(int(fd), &msg, 0)
|
||||
return err != unix.EAGAIN
|
||||
})
|
||||
if ew != nil {
|
||||
return 0, ew
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
//go:linkname recvmsg golang.org/x/sys/unix.recvmsg
|
||||
func recvmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error)
|
||||
|
||||
//go:linkname sendmsg golang.org/x/sys/unix.sendmsg
|
||||
func sendmsg(fd int, msg *unix.Msghdr, flags int) (n int, err error)
|
||||
24
common/ktls/ktls_prf.go
Normal file
24
common/ktls/ktls_prf.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import "unsafe"
|
||||
|
||||
//go:linkname cipherSuiteByID github.com/metacubex/utls.cipherSuiteByID
|
||||
func cipherSuiteByID(id uint16) unsafe.Pointer
|
||||
|
||||
//go:linkname keysFromMasterSecret github.com/metacubex/utls.keysFromMasterSecret
|
||||
func keysFromMasterSecret(version uint16, suite unsafe.Pointer, masterSecret, clientRandom, serverRandom []byte, macLen, keyLen, ivLen int) (clientMAC, serverMAC, clientKey, serverKey, clientIV, serverIV []byte)
|
||||
|
||||
//go:linkname cipherSuiteTLS13ByID github.com/metacubex/utls.cipherSuiteTLS13ByID
|
||||
func cipherSuiteTLS13ByID(id uint16) unsafe.Pointer
|
||||
|
||||
//go:linkname nextTrafficSecret github.com/metacubex/utls.(*cipherSuiteTLS13).nextTrafficSecret
|
||||
func nextTrafficSecret(cs unsafe.Pointer, trafficSecret []byte) []byte
|
||||
|
||||
//go:linkname trafficKey github.com/metacubex/utls.(*cipherSuiteTLS13).trafficKey
|
||||
func trafficKey(cs unsafe.Pointer, trafficSecret []byte) (key, iv []byte)
|
||||
292
common/ktls/ktls_read.go
Normal file
292
common/ktls/ktls_read.go
Normal file
@@ -0,0 +1,292 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Conn) Read(b []byte) (int, error) {
|
||||
if !c.kernelRx {
|
||||
return c.Conn.Read(b)
|
||||
}
|
||||
|
||||
if len(b) == 0 {
|
||||
// Put this after Handshake, in case people were calling
|
||||
// Read(nil) for the side effect of the Handshake.
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
c.rawConn.In.Lock()
|
||||
defer c.rawConn.In.Unlock()
|
||||
|
||||
for c.rawConn.Input.Len() == 0 {
|
||||
if err := c.readRecord(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
for c.rawConn.Hand.Len() > 0 {
|
||||
if err := c.handlePostHandshakeMessage(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
n, _ := c.rawConn.Input.Read(b)
|
||||
|
||||
// If a close-notify alert is waiting, read it so that we can return (n,
|
||||
// EOF) instead of (n, nil), to signal to the HTTP response reading
|
||||
// goroutine that the connection is now closed. This eliminates a race
|
||||
// where the HTTP response reading goroutine would otherwise not observe
|
||||
// the EOF until its next read, by which time a client goroutine might
|
||||
// have already tried to reuse the HTTP connection for a new request.
|
||||
// See https://golang.org/cl/76400046 and https://golang.org/issue/3514
|
||||
if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.RawInput.Len() > 0 &&
|
||||
c.rawConn.RawInput.Bytes()[0] == recordTypeAlert {
|
||||
if err := c.readRecord(); err != nil {
|
||||
return n, err // will be io.EOF on closeNotify
|
||||
}
|
||||
}
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *Conn) readRecord() error {
|
||||
if *c.rawConn.In.Err != nil {
|
||||
return *c.rawConn.In.Err
|
||||
}
|
||||
|
||||
typ, data, err := c.readRawRecord()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(data) > maxPlaintext {
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertRecordOverflow))
|
||||
}
|
||||
|
||||
// Application Data messages are always protected.
|
||||
if c.rawConn.In.Cipher == nil && typ == recordTypeApplicationData {
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
}
|
||||
|
||||
//if typ != recordTypeAlert && typ != recordTypeChangeCipherSpec && len(data) > 0 {
|
||||
// This is a state-advancing message: reset the retry count.
|
||||
// c.retryCount = 0
|
||||
//}
|
||||
|
||||
// Handshake messages MUST NOT be interleaved with other record types in TLS 1.3.
|
||||
if *c.rawConn.Vers == tls.VersionTLS13 && typ != recordTypeHandshake && c.rawConn.Hand.Len() > 0 {
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
}
|
||||
|
||||
switch typ {
|
||||
default:
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
case recordTypeAlert:
|
||||
//if c.quic != nil {
|
||||
// return c.rawConn.In.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
//}
|
||||
if len(data) != 2 {
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
}
|
||||
if data[1] == alertCloseNotify {
|
||||
return c.rawConn.In.SetErrorLocked(io.EOF)
|
||||
}
|
||||
if *c.rawConn.Vers == tls.VersionTLS13 {
|
||||
// TLS 1.3 removed warning-level alerts except for alertUserCanceled
|
||||
// (RFC 8446, § 6.1). Since at least one major implementation
|
||||
// (https://bugs.openjdk.org/browse/JDK-8323517) misuses this alert,
|
||||
// many TLS stacks now ignore it outright when seen in a TLS 1.3
|
||||
// handshake (e.g. BoringSSL, NSS, Rustls).
|
||||
if data[1] == alertUserCanceled {
|
||||
// Like TLS 1.2 alertLevelWarning alerts, we drop the record and retry.
|
||||
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
|
||||
}
|
||||
return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])})
|
||||
}
|
||||
switch data[0] {
|
||||
case alertLevelWarning:
|
||||
// Drop the record on the floor and retry.
|
||||
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
|
||||
case alertLevelError:
|
||||
return c.rawConn.In.SetErrorLocked(&net.OpError{Op: "remote error", Err: tls.AlertError(data[1])})
|
||||
default:
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
}
|
||||
|
||||
case recordTypeChangeCipherSpec:
|
||||
if len(data) != 1 || data[0] != 1 {
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertDecodeError))
|
||||
}
|
||||
// Handshake messages are not allowed to fragment across the CCS.
|
||||
if c.rawConn.Hand.Len() > 0 {
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
}
|
||||
// In TLS 1.3, change_cipher_spec records are ignored until the
|
||||
// Finished. See RFC 8446, Appendix D.4. Note that according to Section
|
||||
// 5, a server can send a ChangeCipherSpec before its ServerHello, when
|
||||
// c.vers is still unset. That's not useful though and suspicious if the
|
||||
// server then selects a lower protocol version, so don't allow that.
|
||||
if *c.rawConn.Vers == tls.VersionTLS13 {
|
||||
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
|
||||
}
|
||||
// if !expectChangeCipherSpec {
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
//}
|
||||
//if err := c.rawConn.In.changeCipherSpec(); err != nil {
|
||||
// return c.rawConn.In.setErrorLocked(c.sendAlert(err.(alert)))
|
||||
//}
|
||||
|
||||
case recordTypeApplicationData:
|
||||
// Some OpenSSL servers send empty records in order to randomize the
|
||||
// CBC RawIV. Ignore a limited number of empty records.
|
||||
if len(data) == 0 {
|
||||
return c.retryReadRecord( /*expectChangeCipherSpec*/ )
|
||||
}
|
||||
// Note that data is owned by c.rawInput, following the Next call above,
|
||||
// to avoid copying the plaintext. This is safe because c.rawInput is
|
||||
// not read from or written to until c.input is drained.
|
||||
c.rawConn.Input.Reset(data)
|
||||
case recordTypeHandshake:
|
||||
if len(data) == 0 {
|
||||
return c.rawConn.In.SetErrorLocked(c.sendAlert(alertUnexpectedMessage))
|
||||
}
|
||||
c.rawConn.Hand.Write(data)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//nolint:staticcheck
|
||||
func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) {
|
||||
// Read from kernel.
|
||||
if c.kernelRx {
|
||||
return c.readKernelRecord()
|
||||
}
|
||||
|
||||
// Read header, payload.
|
||||
if err = c.readFromUntil(c.conn, recordHeaderLen); err != nil {
|
||||
// RFC 8446, Section 6.1 suggests that EOF without an alertCloseNotify
|
||||
// is an error, but popular web sites seem to do this, so we accept it
|
||||
// if and only if at the record boundary.
|
||||
if err == io.ErrUnexpectedEOF && c.rawConn.RawInput.Len() == 0 {
|
||||
err = io.EOF
|
||||
}
|
||||
if e, ok := err.(net.Error); !ok || !e.Temporary() {
|
||||
c.rawConn.In.SetErrorLocked(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
hdr := c.rawConn.RawInput.Bytes()[:recordHeaderLen]
|
||||
typ = hdr[0]
|
||||
|
||||
vers := uint16(hdr[1])<<8 | uint16(hdr[2])
|
||||
expectedVers := *c.rawConn.Vers
|
||||
if expectedVers == tls.VersionTLS13 {
|
||||
// All TLS 1.3 records are expected to have 0x0303 (1.2) after
|
||||
// the initial hello (RFC 8446 Section 5.1).
|
||||
expectedVers = tls.VersionTLS12
|
||||
}
|
||||
n := int(hdr[3])<<8 | int(hdr[4])
|
||||
if /*c.haveVers && */ vers != expectedVers {
|
||||
c.sendAlert(alertProtocolVersion)
|
||||
msg := fmt.Sprintf("received record with version %x when expecting version %x", vers, expectedVers)
|
||||
err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg))
|
||||
return
|
||||
}
|
||||
//if !c.haveVers {
|
||||
// // First message, be extra suspicious: this might not be a TLS
|
||||
// // client. Bail out before reading a full 'body', if possible.
|
||||
// // The current max version is 3.3 so if the version is >= 16.0,
|
||||
// // it's probably not real.
|
||||
// if (typ != recordTypeAlert && typ != recordTypeHandshake) || vers >= 0x1000 {
|
||||
// err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(c.conn, "first record does not look like a TLS handshake"))
|
||||
// return
|
||||
// }
|
||||
//}
|
||||
if *c.rawConn.Vers == tls.VersionTLS13 && n > maxCiphertextTLS13 || n > maxCiphertext {
|
||||
c.sendAlert(alertRecordOverflow)
|
||||
msg := fmt.Sprintf("oversized record received with length %d", n)
|
||||
err = c.rawConn.In.SetErrorLocked(c.newRecordHeaderError(nil, msg))
|
||||
return
|
||||
}
|
||||
if err = c.readFromUntil(c.conn, recordHeaderLen+n); err != nil {
|
||||
if e, ok := err.(net.Error); !ok || !e.Temporary() {
|
||||
c.rawConn.In.SetErrorLocked(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Process message.
|
||||
record := c.rawConn.RawInput.Next(recordHeaderLen + n)
|
||||
data, typ, err = c.rawConn.In.Decrypt(record)
|
||||
if err != nil {
|
||||
err = c.rawConn.In.SetErrorLocked(c.sendAlert(uint8(err.(tls.AlertError))))
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// retryReadRecord recurs into readRecordOrCCS to drop a non-advancing record, like
|
||||
// a warning alert, empty application_data, or a change_cipher_spec in TLS 1.3.
|
||||
func (c *Conn) retryReadRecord( /*expectChangeCipherSpec bool*/ ) error {
|
||||
//c.retryCount++
|
||||
//if c.retryCount > maxUselessRecords {
|
||||
// c.sendAlert(alertUnexpectedMessage)
|
||||
// return c.in.setErrorLocked(errors.New("tls: too many ignored records"))
|
||||
//}
|
||||
return c.readRecord( /*expectChangeCipherSpec*/ )
|
||||
}
|
||||
|
||||
// atLeastReader reads from R, stopping with EOF once at least N bytes have been
|
||||
// read. It is different from an io.LimitedReader in that it doesn't cut short
|
||||
// the last Read call, and in that it considers an early EOF an error.
|
||||
type atLeastReader struct {
|
||||
R io.Reader
|
||||
N int64
|
||||
}
|
||||
|
||||
func (r *atLeastReader) Read(p []byte) (int, error) {
|
||||
if r.N <= 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
n, err := r.R.Read(p)
|
||||
r.N -= int64(n) // won't underflow unless len(p) >= n > 9223372036854775809
|
||||
if r.N > 0 && err == io.EOF {
|
||||
return n, io.ErrUnexpectedEOF
|
||||
}
|
||||
if r.N <= 0 && err == nil {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// readFromUntil reads from r into c.rawConn.RawInput until c.rawConn.RawInput contains
|
||||
// at least n bytes or else returns an error.
|
||||
func (c *Conn) readFromUntil(r io.Reader, n int) error {
|
||||
if c.rawConn.RawInput.Len() >= n {
|
||||
return nil
|
||||
}
|
||||
needs := n - c.rawConn.RawInput.Len()
|
||||
// There might be extra input waiting on the wire. Make a best effort
|
||||
// attempt to fetch it so that it can be used in (*Conn).Read to
|
||||
// "predict" closeNotify alerts.
|
||||
c.rawConn.RawInput.Grow(needs + bytes.MinRead)
|
||||
_, err := c.rawConn.RawInput.ReadFrom(&atLeastReader{r, int64(needs)})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Conn) newRecordHeaderError(conn net.Conn, msg string) (err tls.RecordHeaderError) {
|
||||
err.Msg = msg
|
||||
err.Conn = conn
|
||||
copy(err.RecordHeader[:], c.rawConn.RawInput.Bytes())
|
||||
return err
|
||||
}
|
||||
41
common/ktls/ktls_read_wait.go
Normal file
41
common/ktls/ktls_read_wait.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func (c *Conn) InitializeReadWaiter(options N.ReadWaitOptions) (needCopy bool) {
|
||||
c.readWaitOptions = options
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Conn) WaitReadBuffer() (buffer *buf.Buffer, err error) {
|
||||
c.rawConn.In.Lock()
|
||||
defer c.rawConn.In.Unlock()
|
||||
for c.rawConn.Input.Len() == 0 {
|
||||
err = c.readRecord()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
buffer = c.readWaitOptions.NewBuffer()
|
||||
n, err := c.rawConn.Input.Read(buffer.FreeBytes())
|
||||
if err != nil {
|
||||
buffer.Release()
|
||||
return
|
||||
}
|
||||
buffer.Truncate(n)
|
||||
if n != 0 && c.rawConn.Input.Len() == 0 && c.rawConn.Input.Len() > 0 &&
|
||||
c.rawConn.RawInput.Bytes()[0] == recordTypeAlert {
|
||||
_ = c.rawConn.ReadRecord()
|
||||
}
|
||||
c.readWaitOptions.PostReturn(buffer)
|
||||
return
|
||||
}
|
||||
15
common/ktls/ktls_stub_nolinkname.go
Normal file
15
common/ktls/ktls_stub_nolinkname.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build linux && go1.25 && !badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
|
||||
return nil, E.New("kTLS requires build flags `badlinkname` and `-ldflags=-checklinkname=0`, please recompile your binary")
|
||||
}
|
||||
15
common/ktls/ktls_stub_nonlinux.go
Normal file
15
common/ktls/ktls_stub_nonlinux.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build !linux
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
}
|
||||
15
common/ktls/ktls_stub_oldgo.go
Normal file
15
common/ktls/ktls_stub_oldgo.go
Normal file
@@ -0,0 +1,15 @@
|
||||
//go:build linux && !go1.25
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
func NewConn(ctx context.Context, logger logger.ContextLogger, conn aTLS.Conn, txOffload, rxOffload bool) (aTLS.Conn, error) {
|
||||
return nil, E.New("kTLS requires Go 1.25 or later, please recompile your binary")
|
||||
}
|
||||
154
common/ktls/ktls_write.go
Normal file
154
common/ktls/ktls_write.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && go1.25 && badlinkname
|
||||
|
||||
package ktls
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"net"
|
||||
)
|
||||
|
||||
func (c *Conn) Write(b []byte) (int, error) {
|
||||
if !c.kernelTx {
|
||||
return c.Conn.Write(b)
|
||||
}
|
||||
// interlock with Close below
|
||||
for {
|
||||
x := c.rawConn.ActiveCall.Load()
|
||||
if x&1 != 0 {
|
||||
return 0, net.ErrClosed
|
||||
}
|
||||
if c.rawConn.ActiveCall.CompareAndSwap(x, x+2) {
|
||||
break
|
||||
}
|
||||
}
|
||||
defer c.rawConn.ActiveCall.Add(-2)
|
||||
|
||||
//if err := c.Conn.HandshakeContext(context.Background()); err != nil {
|
||||
// return 0, err
|
||||
//}
|
||||
|
||||
c.rawConn.Out.Lock()
|
||||
defer c.rawConn.Out.Unlock()
|
||||
|
||||
if err := *c.rawConn.Out.Err; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if !c.rawConn.IsHandshakeComplete.Load() {
|
||||
return 0, tls.AlertError(alertInternalError)
|
||||
}
|
||||
|
||||
if *c.rawConn.CloseNotifySent {
|
||||
// return 0, errShutdown
|
||||
return 0, errors.New("tls: protocol is shutdown")
|
||||
}
|
||||
|
||||
// TLS 1.0 is susceptible to a chosen-plaintext
|
||||
// attack when using block mode ciphers due to predictable IVs.
|
||||
// This can be prevented by splitting each Application Data
|
||||
// record into two records, effectively randomizing the RawIV.
|
||||
//
|
||||
// https://www.openssl.org/~bodo/tls-cbc.txt
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=665814
|
||||
// https://www.imperialviolet.org/2012/01/15/beastfollowup.html
|
||||
|
||||
var m int
|
||||
if len(b) > 1 && *c.rawConn.Vers == tls.VersionTLS10 {
|
||||
if _, ok := (*c.rawConn.Out.Cipher).(cipher.BlockMode); ok {
|
||||
n, err := c.writeRecordLocked(recordTypeApplicationData, b[:1])
|
||||
if err != nil {
|
||||
return n, c.rawConn.Out.SetErrorLocked(err)
|
||||
}
|
||||
m, b = 1, b[1:]
|
||||
}
|
||||
}
|
||||
|
||||
n, err := c.writeRecordLocked(recordTypeApplicationData, b)
|
||||
return n + m, c.rawConn.Out.SetErrorLocked(err)
|
||||
}
|
||||
|
||||
func (c *Conn) writeRecordLocked(typ uint16, data []byte) (n int, err error) {
|
||||
if !c.kernelTx {
|
||||
return c.rawConn.WriteRecordLocked(typ, data)
|
||||
}
|
||||
/*for len(data) > 0 {
|
||||
m := len(data)
|
||||
if maxPayload := c.maxPayloadSizeForWrite(typ); m > maxPayload {
|
||||
m = maxPayload
|
||||
}
|
||||
_, err = c.writeKernelRecord(typ, data[:m])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
n += m
|
||||
data = data[m:]
|
||||
}*/
|
||||
return c.writeKernelRecord(typ, data)
|
||||
}
|
||||
|
||||
const (
|
||||
// tcpMSSEstimate is a conservative estimate of the TCP maximum segment
|
||||
// size (MSS). A constant is used, rather than querying the kernel for
|
||||
// the actual MSS, to avoid complexity. The value here is the IPv6
|
||||
// minimum MTU (1280 bytes) minus the overhead of an IPv6 header (40
|
||||
// bytes) and a TCP header with timestamps (32 bytes).
|
||||
tcpMSSEstimate = 1208
|
||||
|
||||
// recordSizeBoostThreshold is the number of bytes of application data
|
||||
// sent after which the TLS record size will be increased to the
|
||||
// maximum.
|
||||
recordSizeBoostThreshold = 128 * 1024
|
||||
)
|
||||
|
||||
func (c *Conn) maxPayloadSizeForWrite(typ uint16) int {
|
||||
if /*c.config.DynamicRecordSizingDisabled ||*/ typ != recordTypeApplicationData {
|
||||
return maxPlaintext
|
||||
}
|
||||
|
||||
if *c.rawConn.PacketsSent >= recordSizeBoostThreshold {
|
||||
return maxPlaintext
|
||||
}
|
||||
|
||||
// Subtract TLS overheads to get the maximum payload size.
|
||||
payloadBytes := tcpMSSEstimate - recordHeaderLen - c.rawConn.Out.ExplicitNonceLen()
|
||||
if rawCipher := *c.rawConn.Out.Cipher; rawCipher != nil {
|
||||
switch ciph := rawCipher.(type) {
|
||||
case cipher.Stream:
|
||||
payloadBytes -= (*c.rawConn.Out.Mac).Size()
|
||||
case cipher.AEAD:
|
||||
payloadBytes -= ciph.Overhead()
|
||||
/*case cbcMode:
|
||||
blockSize := ciph.BlockSize()
|
||||
// The payload must fit in a multiple of blockSize, with
|
||||
// room for at least one padding byte.
|
||||
payloadBytes = (payloadBytes & ^(blockSize - 1)) - 1
|
||||
// The RawMac is appended before padding so affects the
|
||||
// payload size directly.
|
||||
payloadBytes -= c.out.mac.Size()*/
|
||||
default:
|
||||
panic("unknown cipher type")
|
||||
}
|
||||
}
|
||||
if *c.rawConn.Vers == tls.VersionTLS13 {
|
||||
payloadBytes-- // encrypted ContentType
|
||||
}
|
||||
|
||||
// Allow packet growth in arithmetic progression up to max.
|
||||
pkt := *c.rawConn.PacketsSent
|
||||
*c.rawConn.PacketsSent++
|
||||
if pkt > 1000 {
|
||||
return maxPlaintext // avoid overflow in multiply below
|
||||
}
|
||||
|
||||
n := payloadBytes * int(pkt+1)
|
||||
if n > maxPlaintext {
|
||||
n = maxPlaintext
|
||||
}
|
||||
return n
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//go:build go1.21
|
||||
|
||||
package listener
|
||||
|
||||
import "net"
|
||||
|
||||
const go121Available = true
|
||||
|
||||
func setMultiPathTCP(listenConfig *net.ListenConfig) {
|
||||
listenConfig.SetMultipathTCP(true)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
//go:build go1.23
|
||||
|
||||
package listener
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
|
||||
listener.KeepAliveConfig = net.KeepAliveConfig{
|
||||
Enable: true,
|
||||
Idle: idle,
|
||||
Interval: interval,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package listener
|
||||
|
||||
import "net"
|
||||
|
||||
const go121Available = false
|
||||
|
||||
func setMultiPathTCP(listenConfig *net.ListenConfig) {
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
//go:build !go1.23
|
||||
|
||||
package listener
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing/common/control"
|
||||
)
|
||||
|
||||
func setKeepAliveConfig(listener *net.ListenConfig, idle time.Duration, interval time.Duration) {
|
||||
listener.KeepAlive = idle
|
||||
listener.Control = control.Append(listener.Control, control.SetKeepAlivePeriod(idle, interval))
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/metacubex/tfo-go"
|
||||
"github.com/database64128/tfo-go/v2"
|
||||
)
|
||||
|
||||
func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
@@ -46,13 +46,14 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
if keepInterval == 0 {
|
||||
keepInterval = C.TCPKeepAliveInterval
|
||||
}
|
||||
setKeepAliveConfig(&listenConfig, keepIdle, keepInterval)
|
||||
listenConfig.KeepAliveConfig = net.KeepAliveConfig{
|
||||
Enable: true,
|
||||
Idle: keepIdle,
|
||||
Interval: keepInterval,
|
||||
}
|
||||
}
|
||||
if l.listenOptions.TCPMultiPath {
|
||||
if !go121Available {
|
||||
return nil, E.New("MultiPath TCP requires go1.21, please recompile your binary.")
|
||||
}
|
||||
setMultiPathTCP(&listenConfig)
|
||||
listenConfig.SetMultipathTCP(true)
|
||||
}
|
||||
if l.tproxy {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, func(network, address string, conn syscall.RawConn) error {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/netip"
|
||||
"os/user"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-tun"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -12,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
type Searcher interface {
|
||||
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error)
|
||||
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
|
||||
}
|
||||
|
||||
var ErrNotFound = E.New("process not found")
|
||||
@@ -22,15 +23,7 @@ type Config struct {
|
||||
PackageManager tun.PackageManager
|
||||
}
|
||||
|
||||
type Info struct {
|
||||
ProcessID uint32
|
||||
ProcessPath string
|
||||
PackageName string
|
||||
User string
|
||||
UserId int32
|
||||
}
|
||||
|
||||
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
func FindProcessInfo(searcher Searcher, ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
info, err := searcher.FindProcessInfo(ctx, network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -38,7 +31,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
|
||||
if info.UserId != -1 {
|
||||
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
||||
if osUser != nil {
|
||||
info.User = osUser.Username
|
||||
info.UserName = osUser.Username
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-tun"
|
||||
)
|
||||
|
||||
@@ -17,22 +18,22 @@ func NewSearcher(config Config) (Searcher, error) {
|
||||
return &androidSearcher{config.PackageManager}, nil
|
||||
}
|
||||
|
||||
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
_, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
|
||||
return &Info{
|
||||
UserId: int32(uid),
|
||||
PackageName: sharedPackage,
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
AndroidPackageName: sharedPackage,
|
||||
}, nil
|
||||
}
|
||||
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
|
||||
return &Info{
|
||||
UserId: int32(uid),
|
||||
PackageName: packageName,
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
AndroidPackageName: packageName,
|
||||
}, nil
|
||||
}
|
||||
return &Info{UserId: int32(uid)}, nil
|
||||
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -23,12 +24,12 @@ func NewSearcher(_ Config) (Searcher, error) {
|
||||
return &darwinSearcher{}, nil
|
||||
}
|
||||
|
||||
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
func (d *darwinSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
processName, err := findProcessName(network, source.Addr(), int(source.Port()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Info{ProcessPath: processName, UserId: -1}, nil
|
||||
return &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
|
||||
}
|
||||
|
||||
var structSize = func() int {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"net/netip"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
)
|
||||
|
||||
@@ -19,7 +20,7 @@ func NewSearcher(config Config) (Searcher, error) {
|
||||
return &linuxSearcher{config.Logger}, nil
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
inode, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -28,7 +29,7 @@ func (s *linuxSearcher) FindProcessInfo(ctx context.Context, network string, sou
|
||||
if err != nil {
|
||||
s.logger.DebugContext(ctx, "find process path: ", err)
|
||||
}
|
||||
return &Info{
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
ProcessPath: processPath,
|
||||
}, nil
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"net/netip"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/winiphlpapi"
|
||||
|
||||
@@ -27,16 +28,16 @@ func initWin32API() error {
|
||||
return winiphlpapi.LoadExtendedTable()
|
||||
}
|
||||
|
||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*Info, error) {
|
||||
func (s *windowsSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
pid, err := winiphlpapi.FindPid(network, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
path, err := getProcessPath(pid)
|
||||
if err != nil {
|
||||
return &Info{ProcessID: pid, UserId: -1}, err
|
||||
return &adapter.ConnectionOwner{ProcessID: pid, UserId: -1}, err
|
||||
}
|
||||
return &Info{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
|
||||
return &adapter.ConnectionOwner{ProcessID: pid, ProcessPath: path, UserId: -1}, nil
|
||||
}
|
||||
|
||||
func getProcessPath(pid uint32) (string, error) {
|
||||
|
||||
9
common/settings/wifi.go
Normal file
9
common/settings/wifi.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package settings
|
||||
|
||||
import "github.com/sagernet/sing-box/adapter"
|
||||
|
||||
type WIFIMonitor interface {
|
||||
ReadWIFIState() adapter.WIFIState
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
46
common/settings/wifi_linux.go
Normal file
46
common/settings/wifi_linux.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type LinuxWIFIMonitor struct {
|
||||
monitor WIFIMonitor
|
||||
}
|
||||
|
||||
func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
|
||||
monitors := []func(func(adapter.WIFIState)) (WIFIMonitor, error){
|
||||
newNetworkManagerMonitor,
|
||||
newIWDMonitor,
|
||||
newWpaSupplicantMonitor,
|
||||
newConnManMonitor,
|
||||
}
|
||||
var errors []error
|
||||
for _, factory := range monitors {
|
||||
monitor, err := factory(callback)
|
||||
if err == nil {
|
||||
return &LinuxWIFIMonitor{monitor: monitor}, nil
|
||||
}
|
||||
errors = append(errors, err)
|
||||
}
|
||||
return nil, E.Cause(E.Errors(errors...), "no supported WIFI manager found")
|
||||
}
|
||||
|
||||
func (m *LinuxWIFIMonitor) ReadWIFIState() adapter.WIFIState {
|
||||
return m.monitor.ReadWIFIState()
|
||||
}
|
||||
|
||||
func (m *LinuxWIFIMonitor) Start() error {
|
||||
if m.monitor != nil {
|
||||
return m.monitor.Start()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *LinuxWIFIMonitor) Close() error {
|
||||
if m.monitor != nil {
|
||||
return m.monitor.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
160
common/settings/wifi_linux_connman.go
Normal file
160
common/settings/wifi_linux_connman.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type connmanMonitor struct {
|
||||
conn *dbus.Conn
|
||||
callback func(adapter.WIFIState)
|
||||
cancel context.CancelFunc
|
||||
signalChan chan *dbus.Signal
|
||||
}
|
||||
|
||||
func newConnManMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
|
||||
conn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmObj := conn.Object("net.connman", "/")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
call := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0)
|
||||
if call.Err != nil {
|
||||
conn.Close()
|
||||
return nil, call.Err
|
||||
}
|
||||
return &connmanMonitor{conn: conn, callback: callback}, nil
|
||||
}
|
||||
|
||||
func (m *connmanMonitor) ReadWIFIState() adapter.WIFIState {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cmObj := m.conn.Object("net.connman", "/")
|
||||
var services []interface{}
|
||||
err := cmObj.CallWithContext(ctx, "net.connman.Manager.GetServices", 0).Store(&services)
|
||||
if err != nil {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
servicePair, ok := service.([]interface{})
|
||||
if !ok || len(servicePair) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
serviceProps, ok := servicePair[1].(map[string]dbus.Variant)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
typeVariant, hasType := serviceProps["Type"]
|
||||
if !hasType {
|
||||
continue
|
||||
}
|
||||
serviceType, ok := typeVariant.Value().(string)
|
||||
if !ok || serviceType != "wifi" {
|
||||
continue
|
||||
}
|
||||
|
||||
stateVariant, hasState := serviceProps["State"]
|
||||
if !hasState {
|
||||
continue
|
||||
}
|
||||
state, ok := stateVariant.Value().(string)
|
||||
if !ok || (state != "online" && state != "ready") {
|
||||
continue
|
||||
}
|
||||
|
||||
nameVariant, hasName := serviceProps["Name"]
|
||||
if !hasName {
|
||||
continue
|
||||
}
|
||||
ssid, ok := nameVariant.Value().(string)
|
||||
if !ok || ssid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
bssidVariant, hasBSSID := serviceProps["BSSID"]
|
||||
if !hasBSSID {
|
||||
return adapter.WIFIState{SSID: ssid}
|
||||
}
|
||||
bssid, ok := bssidVariant.Value().(string)
|
||||
if !ok {
|
||||
return adapter.WIFIState{SSID: ssid}
|
||||
}
|
||||
|
||||
return adapter.WIFIState{
|
||||
SSID: ssid,
|
||||
BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")),
|
||||
}
|
||||
}
|
||||
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
func (m *connmanMonitor) Start() error {
|
||||
if m.callback == nil {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.cancel = cancel
|
||||
|
||||
m.signalChan = make(chan *dbus.Signal, 10)
|
||||
m.conn.Signal(m.signalChan)
|
||||
|
||||
err := m.conn.AddMatchSignal(
|
||||
dbus.WithMatchInterface("net.connman.Service"),
|
||||
dbus.WithMatchSender("net.connman"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state := m.ReadWIFIState()
|
||||
go m.monitorSignals(ctx, m.signalChan, state)
|
||||
m.callback(state)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *connmanMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case signal, ok := <-signalChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if signal.Name == "PropertyChanged" {
|
||||
state := m.ReadWIFIState()
|
||||
if state != lastState {
|
||||
lastState = state
|
||||
m.callback(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *connmanMonitor) Close() error {
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
if m.signalChan != nil {
|
||||
m.conn.RemoveSignal(m.signalChan)
|
||||
close(m.signalChan)
|
||||
}
|
||||
if m.conn != nil {
|
||||
return m.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
184
common/settings/wifi_linux_iwd.go
Normal file
184
common/settings/wifi_linux_iwd.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type iwdMonitor struct {
|
||||
conn *dbus.Conn
|
||||
callback func(adapter.WIFIState)
|
||||
cancel context.CancelFunc
|
||||
signalChan chan *dbus.Signal
|
||||
}
|
||||
|
||||
func newIWDMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
|
||||
conn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iwdObj := conn.Object("net.connman.iwd", "/")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
call := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0)
|
||||
if call.Err != nil {
|
||||
conn.Close()
|
||||
return nil, call.Err
|
||||
}
|
||||
return &iwdMonitor{conn: conn, callback: callback}, nil
|
||||
}
|
||||
|
||||
func (m *iwdMonitor) ReadWIFIState() adapter.WIFIState {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
iwdObj := m.conn.Object("net.connman.iwd", "/")
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
err := iwdObj.CallWithContext(ctx, "org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects)
|
||||
if err != nil {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
for _, interfaces := range objects {
|
||||
stationProps, hasStation := interfaces["net.connman.iwd.Station"]
|
||||
if !hasStation {
|
||||
continue
|
||||
}
|
||||
|
||||
stateVariant, hasState := stationProps["State"]
|
||||
if !hasState {
|
||||
continue
|
||||
}
|
||||
state, ok := stateVariant.Value().(string)
|
||||
if !ok || state != "connected" {
|
||||
continue
|
||||
}
|
||||
|
||||
connectedNetworkVariant, hasNetwork := stationProps["ConnectedNetwork"]
|
||||
if !hasNetwork {
|
||||
continue
|
||||
}
|
||||
networkPath, ok := connectedNetworkVariant.Value().(dbus.ObjectPath)
|
||||
if !ok || networkPath == "/" {
|
||||
continue
|
||||
}
|
||||
|
||||
networkInterfaces, hasNetworkPath := objects[networkPath]
|
||||
if !hasNetworkPath {
|
||||
continue
|
||||
}
|
||||
|
||||
networkProps, hasNetworkInterface := networkInterfaces["net.connman.iwd.Network"]
|
||||
if !hasNetworkInterface {
|
||||
continue
|
||||
}
|
||||
|
||||
nameVariant, hasName := networkProps["Name"]
|
||||
if !hasName {
|
||||
continue
|
||||
}
|
||||
ssid, ok := nameVariant.Value().(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
connectedBSSVariant, hasBSS := stationProps["ConnectedAccessPoint"]
|
||||
if !hasBSS {
|
||||
return adapter.WIFIState{SSID: ssid}
|
||||
}
|
||||
bssPath, ok := connectedBSSVariant.Value().(dbus.ObjectPath)
|
||||
if !ok || bssPath == "/" {
|
||||
return adapter.WIFIState{SSID: ssid}
|
||||
}
|
||||
|
||||
bssInterfaces, hasBSSPath := objects[bssPath]
|
||||
if !hasBSSPath {
|
||||
return adapter.WIFIState{SSID: ssid}
|
||||
}
|
||||
|
||||
bssProps, hasBSSInterface := bssInterfaces["net.connman.iwd.BasicServiceSet"]
|
||||
if !hasBSSInterface {
|
||||
return adapter.WIFIState{SSID: ssid}
|
||||
}
|
||||
|
||||
addressVariant, hasAddress := bssProps["Address"]
|
||||
if !hasAddress {
|
||||
return adapter.WIFIState{SSID: ssid}
|
||||
}
|
||||
bssid, ok := addressVariant.Value().(string)
|
||||
if !ok {
|
||||
return adapter.WIFIState{SSID: ssid}
|
||||
}
|
||||
|
||||
return adapter.WIFIState{
|
||||
SSID: ssid,
|
||||
BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")),
|
||||
}
|
||||
}
|
||||
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
func (m *iwdMonitor) Start() error {
|
||||
if m.callback == nil {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.cancel = cancel
|
||||
|
||||
m.signalChan = make(chan *dbus.Signal, 10)
|
||||
m.conn.Signal(m.signalChan)
|
||||
|
||||
err := m.conn.AddMatchSignal(
|
||||
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
|
||||
dbus.WithMatchSender("net.connman.iwd"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state := m.ReadWIFIState()
|
||||
go m.monitorSignals(ctx, m.signalChan, state)
|
||||
m.callback(state)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *iwdMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case signal, ok := <-signalChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
|
||||
state := m.ReadWIFIState()
|
||||
if state != lastState {
|
||||
lastState = state
|
||||
m.callback(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *iwdMonitor) Close() error {
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
if m.signalChan != nil {
|
||||
m.conn.RemoveSignal(m.signalChan)
|
||||
close(m.signalChan)
|
||||
}
|
||||
if m.conn != nil {
|
||||
return m.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
157
common/settings/wifi_linux_nm.go
Normal file
157
common/settings/wifi_linux_nm.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
type networkManagerMonitor struct {
|
||||
conn *dbus.Conn
|
||||
callback func(adapter.WIFIState)
|
||||
cancel context.CancelFunc
|
||||
signalChan chan *dbus.Signal
|
||||
}
|
||||
|
||||
func newNetworkManagerMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
|
||||
conn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nmObj := conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
defer cancel()
|
||||
var state uint32
|
||||
err = nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "State").Store(&state)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return &networkManagerMonitor{conn: conn, callback: callback}, nil
|
||||
}
|
||||
|
||||
func (m *networkManagerMonitor) ReadWIFIState() adapter.WIFIState {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
nmObj := m.conn.Object("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager")
|
||||
|
||||
var primaryConnectionPath dbus.ObjectPath
|
||||
err := nmObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager", "PrimaryConnection").Store(&primaryConnectionPath)
|
||||
if err != nil || primaryConnectionPath == "/" {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
connObj := m.conn.Object("org.freedesktop.NetworkManager", primaryConnectionPath)
|
||||
|
||||
var devicePaths []dbus.ObjectPath
|
||||
err = connObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Connection.Active", "Devices").Store(&devicePaths)
|
||||
if err != nil || len(devicePaths) == 0 {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
for _, devicePath := range devicePaths {
|
||||
deviceObj := m.conn.Object("org.freedesktop.NetworkManager", devicePath)
|
||||
|
||||
var deviceType uint32
|
||||
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device", "DeviceType").Store(&deviceType)
|
||||
if err != nil || deviceType != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
var accessPointPath dbus.ObjectPath
|
||||
err = deviceObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint").Store(&accessPointPath)
|
||||
if err != nil || accessPointPath == "/" {
|
||||
continue
|
||||
}
|
||||
|
||||
apObj := m.conn.Object("org.freedesktop.NetworkManager", accessPointPath)
|
||||
|
||||
var ssidBytes []byte
|
||||
err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "Ssid").Store(&ssidBytes)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var hwAddress string
|
||||
err = apObj.CallWithContext(ctx, "org.freedesktop.DBus.Properties.Get", 0, "org.freedesktop.NetworkManager.AccessPoint", "HwAddress").Store(&hwAddress)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ssid := strings.TrimSpace(string(ssidBytes))
|
||||
if ssid == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
return adapter.WIFIState{
|
||||
SSID: ssid,
|
||||
BSSID: strings.ToUpper(strings.ReplaceAll(hwAddress, ":", "")),
|
||||
}
|
||||
}
|
||||
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
func (m *networkManagerMonitor) Start() error {
|
||||
if m.callback == nil {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.cancel = cancel
|
||||
|
||||
m.signalChan = make(chan *dbus.Signal, 10)
|
||||
m.conn.Signal(m.signalChan)
|
||||
|
||||
err := m.conn.AddMatchSignal(
|
||||
dbus.WithMatchSender("org.freedesktop.NetworkManager"),
|
||||
dbus.WithMatchInterface("org.freedesktop.DBus.Properties"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
state := m.ReadWIFIState()
|
||||
go m.monitorSignals(ctx, m.signalChan, state)
|
||||
m.callback(state)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *networkManagerMonitor) monitorSignals(ctx context.Context, signalChan chan *dbus.Signal, lastState adapter.WIFIState) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case signal, ok := <-signalChan:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if signal.Name == "org.freedesktop.DBus.Properties.PropertiesChanged" {
|
||||
state := m.ReadWIFIState()
|
||||
if state != lastState {
|
||||
lastState = state
|
||||
m.callback(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *networkManagerMonitor) Close() error {
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
if m.signalChan != nil {
|
||||
m.conn.RemoveSignal(m.signalChan)
|
||||
close(m.signalChan)
|
||||
}
|
||||
if m.conn != nil {
|
||||
return m.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
179
common/settings/wifi_linux_wpa.go
Normal file
179
common/settings/wifi_linux_wpa.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package settings
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
)
|
||||
|
||||
type wpaSupplicantMonitor struct {
|
||||
socketPath string
|
||||
callback func(adapter.WIFIState)
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func newWpaSupplicantMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
|
||||
socketDirs := []string{"/var/run/wpa_supplicant", "/run/wpa_supplicant"}
|
||||
for _, socketDir := range socketDirs {
|
||||
entries, err := os.ReadDir(socketDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || entry.Name() == "." || entry.Name() == ".." {
|
||||
continue
|
||||
}
|
||||
socketPath := filepath.Join(socketDir, entry.Name())
|
||||
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"}
|
||||
remoteAddr := &net.UnixAddr{Name: socketPath, Net: "unixgram"}
|
||||
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
conn.Close()
|
||||
return &wpaSupplicantMonitor{socketPath: socketPath, callback: callback}, nil
|
||||
}
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (m *wpaSupplicantMonitor) ReadWIFIState() adapter.WIFIState {
|
||||
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-%d", os.Getpid()), Net: "unixgram"}
|
||||
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
|
||||
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
|
||||
if err != nil {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetDeadline(time.Now().Add(3 * time.Second))
|
||||
|
||||
status, err := m.sendCommand(conn, "STATUS")
|
||||
if err != nil {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
var ssid, bssid string
|
||||
var connected bool
|
||||
scanner := bufio.NewScanner(strings.NewReader(status))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.HasPrefix(line, "wpa_state=") {
|
||||
state := strings.TrimPrefix(line, "wpa_state=")
|
||||
connected = state == "COMPLETED"
|
||||
} else if strings.HasPrefix(line, "ssid=") {
|
||||
ssid = strings.TrimPrefix(line, "ssid=")
|
||||
} else if strings.HasPrefix(line, "bssid=") {
|
||||
bssid = strings.TrimPrefix(line, "bssid=")
|
||||
}
|
||||
}
|
||||
|
||||
if !connected || ssid == "" {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
return adapter.WIFIState{
|
||||
SSID: ssid,
|
||||
BSSID: strings.ToUpper(strings.ReplaceAll(bssid, ":", "")),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *wpaSupplicantMonitor) sendCommand(conn *net.UnixConn, command string) (string, error) {
|
||||
_, err := conn.Write([]byte(command + "\n"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
response := string(buf[:n])
|
||||
if strings.HasPrefix(response, "FAIL") {
|
||||
return "", os.ErrInvalid
|
||||
}
|
||||
|
||||
return strings.TrimSpace(response), nil
|
||||
}
|
||||
|
||||
func (m *wpaSupplicantMonitor) Start() error {
|
||||
if m.callback == nil {
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
m.cancel = cancel
|
||||
|
||||
state := m.ReadWIFIState()
|
||||
go m.monitorEvents(ctx, state)
|
||||
m.callback(state)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *wpaSupplicantMonitor) monitorEvents(ctx context.Context, lastState adapter.WIFIState) {
|
||||
var consecutiveErrors int
|
||||
|
||||
localAddr := &net.UnixAddr{Name: fmt.Sprintf("@sing-box-wpa-mon-%d", os.Getpid()), Net: "unixgram"}
|
||||
remoteAddr := &net.UnixAddr{Name: m.socketPath, Net: "unixgram"}
|
||||
conn, err := net.DialUnix("unixgram", localAddr, remoteAddr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
_, err = conn.Write([]byte("ATTACH\n"))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf := make([]byte, 4096)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil || !strings.HasPrefix(string(buf[:n]), "OK") {
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
consecutiveErrors++
|
||||
if consecutiveErrors > 10 {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
consecutiveErrors = 0
|
||||
|
||||
msg := string(buf[:n])
|
||||
if strings.Contains(msg, "CTRL-EVENT-CONNECTED") || strings.Contains(msg, "CTRL-EVENT-DISCONNECTED") {
|
||||
state := m.ReadWIFIState()
|
||||
if state != lastState {
|
||||
lastState = state
|
||||
m.callback(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *wpaSupplicantMonitor) Close() error {
|
||||
if m.cancel != nil {
|
||||
m.cancel()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
27
common/settings/wifi_stub.go
Normal file
27
common/settings/wifi_stub.go
Normal file
@@ -0,0 +1,27 @@
|
||||
//go:build !linux
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
)
|
||||
|
||||
type stubWIFIMonitor struct{}
|
||||
|
||||
func NewWIFIMonitor(callback func(adapter.WIFIState)) (WIFIMonitor, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (m *stubWIFIMonitor) ReadWIFIState() adapter.WIFIState {
|
||||
return adapter.WIFIState{}
|
||||
}
|
||||
|
||||
func (m *stubWIFIMonitor) Start() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *stubWIFIMonitor) Close() error {
|
||||
return nil
|
||||
}
|
||||
@@ -303,6 +303,8 @@ find:
|
||||
metadata.Protocol = C.ProtocolQUIC
|
||||
fingerprint, err := ja3.Compute(buffer.Bytes())
|
||||
if err != nil {
|
||||
metadata.Protocol = C.ProtocolQUIC
|
||||
metadata.Client = C.ClientChromium
|
||||
metadata.SniffContext = fragments
|
||||
return E.Cause1(ErrNeedMoreData, err)
|
||||
}
|
||||
@@ -332,7 +334,7 @@ find:
|
||||
}
|
||||
|
||||
if count(frameTypeList, frameTypeCrypto) > 1 || count(frameTypeList, frameTypePing) > 0 {
|
||||
if isQUICGo(fingerprint) {
|
||||
if maybeUQUIC(fingerprint) {
|
||||
metadata.Client = C.ClientQUICGo
|
||||
} else {
|
||||
metadata.Client = C.ClientChromium
|
||||
|
||||
@@ -1,29 +1,21 @@
|
||||
package sniff
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/sagernet/sing-box/common/ja3"
|
||||
)
|
||||
|
||||
const (
|
||||
// X25519Kyber768Draft00 - post-quantum curve used by Go crypto/tls
|
||||
x25519Kyber768Draft00 uint16 = 0x11EC // 4588
|
||||
// renegotiation_info extension used by Go crypto/tls
|
||||
extensionRenegotiationInfo uint16 = 0xFF01 // 65281
|
||||
)
|
||||
|
||||
// isQUICGo detects native quic-go by checking for Go crypto/tls specific features.
|
||||
// Note: uQUIC with Chromium mimicry cannot be reliably distinguished from real Chromium
|
||||
// since it uses the same TLS fingerprint, so it will be identified as Chromium.
|
||||
func isQUICGo(fingerprint *ja3.ClientHello) bool {
|
||||
for _, curve := range fingerprint.EllipticCurves {
|
||||
if curve == x25519Kyber768Draft00 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, ext := range fingerprint.Extensions {
|
||||
if ext == extensionRenegotiationInfo {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// Chromium sends separate client hello packets, but UQUIC has not yet implemented this behavior
|
||||
// The cronet without this behavior does not have version 115
|
||||
var uQUICChrome115 = &ja3.ClientHello{
|
||||
Version: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{4865, 4866, 4867},
|
||||
Extensions: []uint16{0, 10, 13, 16, 27, 43, 45, 51, 57, 17513},
|
||||
EllipticCurves: []uint16{29, 23, 24},
|
||||
SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},
|
||||
}
|
||||
|
||||
func maybeUQUIC(fingerprint *ja3.ClientHello) bool {
|
||||
return !uQUICChrome115.Equals(fingerprint, true)
|
||||
}
|
||||
|
||||
@@ -1,188 +0,0 @@
|
||||
package sniff_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/sniff"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSniffQUICQuicGoFingerprint(t *testing.T) {
|
||||
t.Parallel()
|
||||
const testSNI = "test.example.com"
|
||||
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer udpConn.Close()
|
||||
|
||||
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
||||
packetsChan := make(chan [][]byte, 1)
|
||||
|
||||
go func() {
|
||||
var packets [][]byte
|
||||
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
for i := 0; i < 10; i++ {
|
||||
buf := make([]byte, 2048)
|
||||
n, _, err := udpConn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
packets = append(packets, buf[:n])
|
||||
}
|
||||
packetsChan <- packets
|
||||
}()
|
||||
|
||||
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer clientConn.Close()
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: testSNI,
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h3"},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
||||
|
||||
select {
|
||||
case packets := <-packetsChan:
|
||||
t.Logf("Captured %d packets", len(packets))
|
||||
|
||||
var metadata adapter.InboundContext
|
||||
for i, pkt := range packets {
|
||||
err := sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
t.Logf("Packet %d: err=%v, domain=%s, client=%s", i, err, metadata.Domain, metadata.Client)
|
||||
if metadata.Domain != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("\n=== quic-go TLS Fingerprint Analysis ===")
|
||||
t.Logf("Domain: %s", metadata.Domain)
|
||||
t.Logf("Client: %s", metadata.Client)
|
||||
t.Logf("Protocol: %s", metadata.Protocol)
|
||||
|
||||
// The client should be identified as quic-go, not chromium
|
||||
// Current issue: it's being identified as chromium
|
||||
if metadata.Client == "chromium" {
|
||||
t.Log("WARNING: quic-go is being misidentified as chromium!")
|
||||
}
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSniffQUICInitialFromQuicGo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const testSNI = "test.example.com"
|
||||
|
||||
// Create UDP listener to capture ALL initial packets
|
||||
udpConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer udpConn.Close()
|
||||
|
||||
serverAddr := udpConn.LocalAddr().(*net.UDPAddr)
|
||||
|
||||
// Channel to receive captured packets
|
||||
packetsChan := make(chan [][]byte, 1)
|
||||
|
||||
// Start goroutine to capture packets
|
||||
go func() {
|
||||
var packets [][]byte
|
||||
udpConn.SetReadDeadline(time.Now().Add(3 * time.Second))
|
||||
for i := 0; i < 5; i++ { // Capture up to 5 packets
|
||||
buf := make([]byte, 2048)
|
||||
n, _, err := udpConn.ReadFromUDP(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
packets = append(packets, buf[:n])
|
||||
}
|
||||
packetsChan <- packets
|
||||
}()
|
||||
|
||||
// Create QUIC client connection (will fail but we capture the initial packet)
|
||||
clientConn, err := net.ListenUDP("udp", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer clientConn.Close()
|
||||
|
||||
tlsConfig := &tls.Config{
|
||||
ServerName: testSNI,
|
||||
InsecureSkipVerify: true,
|
||||
NextProtos: []string{"h3"},
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// This will fail (no server) but sends initial packet
|
||||
_, _ = quic.Dial(ctx, clientConn, serverAddr, tlsConfig, &quic.Config{})
|
||||
|
||||
// Wait for captured packets
|
||||
select {
|
||||
case packets := <-packetsChan:
|
||||
t.Logf("Captured %d QUIC packets", len(packets))
|
||||
|
||||
for i, packet := range packets {
|
||||
t.Logf("Packet %d: length=%d, first 30 bytes: %x", i, len(packet), packet[:min(30, len(packet))])
|
||||
}
|
||||
|
||||
// Test sniffer with first packet
|
||||
if len(packets) > 0 {
|
||||
var metadata adapter.InboundContext
|
||||
err := sniff.QUICClientHello(context.Background(), &metadata, packets[0])
|
||||
|
||||
t.Logf("First packet sniff error: %v", err)
|
||||
t.Logf("Protocol: %s", metadata.Protocol)
|
||||
t.Logf("Domain: %s", metadata.Domain)
|
||||
t.Logf("Client: %s", metadata.Client)
|
||||
|
||||
// If first packet needs more data, try with subsequent packets
|
||||
// IMPORTANT: reuse metadata to accumulate CRYPTO fragments via SniffContext
|
||||
if errors.Is(err, sniff.ErrNeedMoreData) && len(packets) > 1 {
|
||||
t.Log("First packet needs more data, trying subsequent packets with shared context...")
|
||||
for i := 1; i < len(packets); i++ {
|
||||
// Reuse same metadata to accumulate fragments
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, packets[i])
|
||||
t.Logf("Packet %d sniff result: err=%v, domain=%s, sniffCtx=%v", i, err, metadata.Domain, metadata.SniffContext != nil)
|
||||
if metadata.Domain != "" || (err != nil && !errors.Is(err, sniff.ErrNeedMoreData)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Print hex dump for debugging
|
||||
t.Logf("First packet hex:\n%s", hex.Dump(packets[0][:min(256, len(packets[0]))]))
|
||||
|
||||
// Log final results
|
||||
t.Logf("Final: Protocol=%s, Domain=%s, Client=%s", metadata.Protocol, metadata.Domain, metadata.Client)
|
||||
|
||||
// Verify SNI extraction
|
||||
if metadata.Domain == "" {
|
||||
t.Errorf("Failed to extract SNI, expected: %s", testSNI)
|
||||
} else {
|
||||
require.Equal(t, testSNI, metadata.Domain, "SNI should match")
|
||||
}
|
||||
|
||||
// Check client identification - quic-go should be identified as quic-go, not chromium
|
||||
t.Logf("Client identified as: %s (expected: quic-go)", metadata.Client)
|
||||
}
|
||||
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("Timeout waiting for QUIC packets")
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func TestSniffQUICChromeNew(t *testing.T) {
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||
require.Empty(t, metadata.Client)
|
||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
pkt, err = hex.DecodeString("cc0000000108e241a0c601413b4f004046006d8f15dae9999edf39d58df6762822b9a2ab996d7f6a10044338af3b51b1814bc4ac0fa5a87c34c6ae604af8cabc5957c5240174deefc8e378719ffdab2ae4e15bf4514bea44894b626c685cd5d5c965f7e97b3a1bdc520b75813e747f37a3ae83ad38b9ca2acb0de4fc9424839a50c8fb815a62b498609fbbc59145698860e0509cc08a04d1b119daef844ba2f09c16e2665e5cc0b47624b71f7b950c54fd56b4a1fbb826cba44eeeee3949ced8f5de60d4c81b19ee59f75aa1abb33f22c6b13c27095eb1e99cff01fdc93e6e88da2622ee18c08a79f508befd7e33e99bca60e64bef9a47b764384bd93823daeeb6fcb4d7cfbc4ab53eff59b3636f6dcaaf229b5a94941b5712807166b9bd5e82cb4a9708a71451c4cd6f6e33fb2fe40c8c70dd51a30b37ff9c5e35783debde0093fde19ce074b4887b3c90980b107b9c0f32cf61a66f37c251b789abc4d27fc421207966846c8cc7faa42d9af6ad355a6bc94cb78223b612be8b3e2a4df61fee83a674a0ceb8b7c3a29b97102cda22fecdf6a4628e5b612bc17eab64d6f75feedd0b106c0419e484e66725759964cb5935ac5125e5ae920cd280bd40df57c1d7ae1845700bd4eb7b7ab12bc0850950bfe6e69edd6ac1daa5db2c2b07484327196e561c513462d72872dc6771c39f6b60d46a1f2c92343b7338450a0ef8e39f97fa70652b3a12cd04043698951627aaaa82cc95e76df92021d30e8014c984f12eea0143de8b17e5e4a36ec07bf4814251b391f168a59ef75afcd2319249aaba930f06bb7a11b9491e6f71b3d5774a6503a965e94edd0a67737282fc9cb0271779ff14151b7aa9267bb8f7d643185512515aeea513c0c98bfae782381a3317064195d8825cf8b25c17cdab5fced02612a3f2870e40df57e6ca3f08228a2b04e8de1425eb4b970118f9bbdc212223ff86a5d6b648cdf2366722f21de4b14a1014879eadb69215cdb1aa2a9f4f310ecfe3116214fe3ab0a23f4775a0a54b48d7dfd8f7283ed687b3ac7e1a7e42a0bdc3478aba8651c03e1e9cc9df17d106b8130afe854269b0103b7a696f452721887b19d8181830073c9f10684c65f96d3a6c6efbae044eec03d6399e001fa44d54635dc72f9b8ea6b87d0f452cad1e1e32273e2b47c40f2730235adcae8523b8282f86b8cf1ab63ae54aaa06130df3bbf6ecac7d7d1d43d2a87aea837267ff8ccfaa4b7e47b7ded909e6603d0b928a304f8915c839153598adc4178eb48bc0e98ad7793d7980275e1e491ba4847a4a04ae30fe7f5cc7d4b6f4f63a525e9964d72245860ca76a668a4654adb6619f16e9db79131e5675b93cafb96c92f1da8464d4fef2a22e7f9db695965fe2cc27ea30974629c8fe17cfa2f860179e1eb9faaa88a91ec9ce6da28c1a2894c3b932b5e1c807146718cc77ca13c61eaae00c7c99e019f599772064b198c5c2c5e863336367673630b417ac845ddb7c93b0856317e5d64bab208c5730abc2c63536784fbeaaec139dffc917e775715f1e42164ddef5138d4d163609ab3fbdcab968f8738385c0e7e34ff3cf7771a1dc5ba25a8850fdf96dabafa21f9065f307457ce9af4b7a73450c9d20a3b46fa8d3a1163d22bd01a7d17f0ec274181bf9640fa941427694bfeb1346089f7a851efe0fbb7a2041fa6bb6541ccbad77dd3e1a97999fc05f1fef070e7b5c4b385b8b2a8cc32483fdeba6a373970de2fa4139ba18e5916f949aab0aab2894")
|
||||
require.NoError(t, err)
|
||||
@@ -39,7 +39,7 @@ func TestSniffQUICChromium(t *testing.T) {
|
||||
var metadata adapter.InboundContext
|
||||
err = sniff.QUICClientHello(context.Background(), &metadata, pkt)
|
||||
require.Equal(t, metadata.Protocol, C.ProtocolQUIC)
|
||||
require.Empty(t, metadata.Client)
|
||||
require.Equal(t, metadata.Client, C.ClientChromium)
|
||||
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
|
||||
pkt, err = hex.DecodeString("c90000000108f40d654cc09b27f5000044d073eb38807026d4088455e650e7ccf750d01a72f15f9bfc8ff40d223499db1a485cff14dbd45b9be118172834dc35dca3cf62f61a1266f40b92faf3d28d67a466cfdca678ddced15cd606d31959cf441828467857b226d1a241847c82c57312cefe68ba5042d929919bcd4403b39e5699fe87dda05df1b3801e048edee792458e9b1a9b1d4039df05847bcee3be567494b5876e3bd4c3220fe9dfdb2c07d77410f907f744251ef15536cc03b267d3668d5b75bc1ad2fe735cd3bb73519dd9f1625a49e17ad27bdeccf706c83b5ea339a0a05dd0072f4a8f162bd29926b4997f05613c6e4b0270b0c02805ca0543f27c1ff8505a5750bdd33529ee73c491050a10c6903f53c1121dbe0380e84c007c8df74a1b02443ed80ba7766aef5549e618d4fd249844ee28565142005369869299e8c3035ecef3d799f6cada8549e75b4ce4cbf4c85ef071fd7ff067b1ca9b5968dc41d13d011f6d7843823bac97acb1eb8ee45883f0f254b5f9bd4c763b67e2d8c70a7618a0ef0de304cf597a485126e09f8b2fd795b394c0b4bc4cd2634c2057970da2c798c5e8af7aed4f76f5e25d04e3f8c9c5a5b150d17e0d4c74229898c69b8dc7b8bcc9d359eb441de75c68fbdebec62fb669dcccfb1aad03e3fa073adb2ccf7bb14cbaf99e307d2c903ee71a8f028102eb510caee7e7397512086a78d1f95635c7d06845b5a708652dc4e5cd61245aae5b3c05b84815d84d367bce9b9e3f6d6b90701ac3679233c14d5ce2a1eff26469c966266dc6284bdb95c9c6158934c413a872ce22101e4163e3293d236b301592ca4ccacc1fd4c37066e79c2d9857c8a2560dcf0b33b19163c4240c471b19907476e7e25c65f7eb37276594a0f6b4c33c340cc3284178f17ac5e34dbe7509db890e4ddfd0540fbf9deb32a0101d24fe58b26c5f81c627db9d6ae59d7a111a3d5d1f6109f4eec0d0234e6d73c73a44f50999462724b51ce0fd8283535d70d9e83872c79c59897407a0736741011ae5c64862eb0712f9e7b07aa1d5418ca3fde8626257c6fe418f3c5479055bb2b0ab4c25f649923fc2a41c79aaa7d0f3af6d8b8cf06f61f0230d09bbb60bb49b9e49cc5973748a6cf7ffdee7804d424f9423c63e7ff22f4bd24e4867636ef9fe8dd37f59941a8a47c27765caa8e875a30b62834f17c569227e5e6ed15d58e05d36e76332befad065a2cd4079e66d5af189b0337624c89b1560c3b1b0befd5c1f20e6de8e3d664b3ac06b3d154b488983e14aa93266f5f8b621d2a9bb7ccce509eb26e025c9c45f7cccc09ce85b3103af0c93ce9822f82ecb168ca3177829afb2ea0da2c380e7b1728add55a5d42632e2290363d4cbe432b67e13691648e1acfab22cf0d551eee857709b428bb78e27a45aff6eca301c02e4d13cf36cc2494fdd1aef8dede6e18febd79dca4c6964d09b91c25a08f0947c76ab5104de9404459c2edf5f4adb9dfd771be83656f77fbbafb1ad3281717066010be8778952495383c9f2cf0a38527228c662a35171c5981731f1af09bab842fe6c3162ad4152a4221f560eb6f9bea66b294ffbd3643da2fe34096da13c246505452540177a2a0a1a69106e5cfc279a4890fc3be2952f26be245f930e6c2d9e7e26ee960481e72b99594a1185b46b94b6436d00ba6c70ffe135d43907c92c6f1c09fb9453f103730714f5700fa4347f9715c774cb04a7218dacc66d9c2fade18b14e684aa7fc9ebda0a28")
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/domain"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json/badjson"
|
||||
"github.com/sagernet/sing/common/json/badoption"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
|
||||
"go4.org/netipx"
|
||||
@@ -41,6 +43,8 @@ const (
|
||||
ruleItemNetworkType
|
||||
ruleItemNetworkIsExpensive
|
||||
ruleItemNetworkIsConstrained
|
||||
ruleItemNetworkInterfaceAddress
|
||||
ruleItemDefaultInterfaceAddress
|
||||
ruleItemFinal uint8 = 0xFF
|
||||
)
|
||||
|
||||
@@ -230,6 +234,51 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
|
||||
rule.NetworkIsExpensive = true
|
||||
case ruleItemNetworkIsConstrained:
|
||||
rule.NetworkIsConstrained = true
|
||||
case ruleItemNetworkInterfaceAddress:
|
||||
rule.NetworkInterfaceAddress = new(badjson.TypedMap[option.InterfaceType, badoption.Listable[*badoption.Prefixable]])
|
||||
var size uint64
|
||||
size, err = binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for i := uint64(0); i < size; i++ {
|
||||
var key uint8
|
||||
err = binary.Read(reader, binary.BigEndian, &key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var value []*badoption.Prefixable
|
||||
var prefixCount uint64
|
||||
prefixCount, err = binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for j := uint64(0); j < prefixCount; j++ {
|
||||
var prefix netip.Prefix
|
||||
prefix, err = readPrefix(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
|
||||
}
|
||||
rule.NetworkInterfaceAddress.Put(option.InterfaceType(key), value)
|
||||
}
|
||||
case ruleItemDefaultInterfaceAddress:
|
||||
var value []*badoption.Prefixable
|
||||
var prefixCount uint64
|
||||
prefixCount, err = binary.ReadUvarint(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for j := uint64(0); j < prefixCount; j++ {
|
||||
var prefix netip.Prefix
|
||||
prefix, err = readPrefix(reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
value = append(value, common.Ptr(badoption.Prefixable(prefix)))
|
||||
}
|
||||
rule.DefaultInterfaceAddress = value
|
||||
case ruleItemFinal:
|
||||
err = binary.Read(reader, binary.BigEndian, &rule.Invert)
|
||||
return
|
||||
@@ -346,7 +395,7 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
||||
}
|
||||
if len(rule.NetworkType) > 0 {
|
||||
if generateVersion < C.RuleSetVersion3 {
|
||||
return E.New("network_type rule item is only supported in version 3 or later")
|
||||
return E.New("`network_type` rule item is only supported in version 3 or later")
|
||||
}
|
||||
err = writeRuleItemUint8(writer, ruleItemNetworkType, rule.NetworkType)
|
||||
if err != nil {
|
||||
@@ -354,17 +403,71 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
||||
}
|
||||
}
|
||||
if rule.NetworkIsExpensive {
|
||||
if generateVersion < C.RuleSetVersion3 {
|
||||
return E.New("`network_is_expensive` rule item is only supported in version 3 or later")
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsExpensive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if rule.NetworkIsConstrained {
|
||||
if generateVersion < C.RuleSetVersion3 {
|
||||
return E.New("`network_is_constrained` rule item is only supported in version 3 or later")
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, ruleItemNetworkIsConstrained)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 {
|
||||
if generateVersion < C.RuleSetVersion4 {
|
||||
return E.New("`network_interface_address` rule item is only supported in version 4 or later")
|
||||
}
|
||||
err = writer.WriteByte(ruleItemNetworkInterfaceAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(writer, uint64(rule.NetworkInterfaceAddress.Size()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range rule.NetworkInterfaceAddress.Entries() {
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(entry.Key.Build()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(writer, uint64(len(entry.Value)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rawPrefix := range entry.Value {
|
||||
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rule.DefaultInterfaceAddress) > 0 {
|
||||
if generateVersion < C.RuleSetVersion4 {
|
||||
return E.New("`default_interface_address` rule item is only supported in version 4 or later")
|
||||
}
|
||||
err = writer.WriteByte(ruleItemDefaultInterfaceAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = varbin.WriteUvarint(writer, uint64(len(rule.DefaultInterfaceAddress)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, rawPrefix := range rule.DefaultInterfaceAddress {
|
||||
err = writePrefix(writer, rawPrefix.Build(netip.Prefix{}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rule.WIFISSID) > 0 {
|
||||
err = writeRuleItemString(writer, ruleItemWIFISSID, rule.WIFISSID)
|
||||
if err != nil {
|
||||
|
||||
33
common/srs/ip_cidr.go
Normal file
33
common/srs/ip_cidr.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package srs
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/common/varbin"
|
||||
)
|
||||
|
||||
func readPrefix(reader varbin.Reader) (netip.Prefix, error) {
|
||||
addrSlice, err := varbin.ReadValue[[]byte](reader, binary.BigEndian)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
prefixBits, err := varbin.ReadValue[uint8](reader, binary.BigEndian)
|
||||
if err != nil {
|
||||
return netip.Prefix{}, err
|
||||
}
|
||||
return netip.PrefixFrom(M.AddrFromIP(addrSlice), int(prefixBits)), nil
|
||||
}
|
||||
|
||||
func writePrefix(writer varbin.Writer, prefix netip.Prefix) error {
|
||||
err := varbin.Write(writer, binary.BigEndian, prefix.Addr().AsSlice())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = binary.Write(writer, binary.BigEndian, uint8(prefix.Bits()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,39 +2,71 @@ package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"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"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
func NewDialerFromOptions(ctx context.Context, router adapter.Router, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||
if !options.Enabled {
|
||||
return dialer, nil
|
||||
}
|
||||
config, err := NewClient(ctx, serverAddress, options)
|
||||
config, err := NewClientWithOptions(ClientOptions{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
ServerAddress: serverAddress,
|
||||
Options: options,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewDialer(dialer, config), nil
|
||||
}
|
||||
|
||||
func NewClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
if !options.Enabled {
|
||||
func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return NewClientWithOptions(ClientOptions{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
ServerAddress: serverAddress,
|
||||
Options: options,
|
||||
})
|
||||
}
|
||||
|
||||
type ClientOptions struct {
|
||||
Context context.Context
|
||||
Logger logger.ContextLogger
|
||||
ServerAddress string
|
||||
Options option.OutboundTLSOptions
|
||||
KTLSCompatible bool
|
||||
}
|
||||
|
||||
func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||
if !options.Options.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return NewRealityClient(ctx, serverAddress, options)
|
||||
} else if options.UTLS != nil && options.UTLS.Enabled {
|
||||
return NewUTLSClient(ctx, serverAddress, options)
|
||||
if !options.KTLSCompatible {
|
||||
if options.Options.KernelTx {
|
||||
options.Logger.Warn("enabling kTLS TX in current scenarios will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_tx")
|
||||
}
|
||||
}
|
||||
return NewSTDClient(ctx, serverAddress, options)
|
||||
if options.Options.KernelRx {
|
||||
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
|
||||
}
|
||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||
return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
||||
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
}
|
||||
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||
}
|
||||
|
||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||
@@ -79,10 +111,10 @@ func (d *defaultDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
||||
}
|
||||
|
||||
func (d *defaultDialer) DialTLSContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
|
||||
return d.dialContext(ctx, destination)
|
||||
return d.dialContext(ctx, destination, true)
|
||||
}
|
||||
|
||||
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr) (Conn, error) {
|
||||
func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr, echRetry bool) (Conn, error) {
|
||||
conn, err := d.dialer.DialContext(ctx, N.NetworkTCP, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -90,6 +122,13 @@ func (d *defaultDialer) dialContext(ctx context.Context, destination M.Socksaddr
|
||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, d.config)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
var echErr *tls.ECHRejectionError
|
||||
if echRetry && errors.As(err, &echErr) && len(echErr.RetryConfigList) > 0 {
|
||||
if echConfig, isECH := d.config.(ECHCapableConfig); isECH {
|
||||
echConfig.SetECHConfigList(echErr.RetryConfigList)
|
||||
return d.dialContext(ctx, destination, false)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
//go:build !go1.24
|
||||
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
func parseECHClientConfig(ctx context.Context, clientConfig ECHCapableConfig, options option.OutboundTLSOptions) (Config, error) {
|
||||
return nil, E.New("ECH requires go1.24, please recompile your binary.")
|
||||
}
|
||||
|
||||
func parseECHServerConfig(ctx context.Context, options option.InboundTLSOptions, tlsConfig *tls.Config, echKeyPath *string) error {
|
||||
return E.New("ECH requires go1.24, please recompile your binary.")
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) setECHServerConfig(echKey []byte) error {
|
||||
panic("unreachable")
|
||||
}
|
||||
67
common/tls/ktls.go
Normal file
67
common/tls/ktls.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/common/ktls"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
type KTLSClientConfig struct {
|
||||
Config
|
||||
logger logger.ContextLogger
|
||||
kernelTx, kernelRx bool
|
||||
}
|
||||
|
||||
func (w *KTLSClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, w.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kConn, err := ktls.NewConn(ctx, w.logger, tlsConn, w.kernelTx, w.kernelRx)
|
||||
if err != nil {
|
||||
tlsConn.Close()
|
||||
return nil, E.Cause(err, "initialize kernel TLS")
|
||||
}
|
||||
return kConn, nil
|
||||
}
|
||||
|
||||
func (w *KTLSClientConfig) Clone() Config {
|
||||
return &KTLSClientConfig{
|
||||
w.Config.Clone(),
|
||||
w.logger,
|
||||
w.kernelTx,
|
||||
w.kernelRx,
|
||||
}
|
||||
}
|
||||
|
||||
type KTlSServerConfig struct {
|
||||
ServerConfig
|
||||
logger logger.ContextLogger
|
||||
kernelTx, kernelRx bool
|
||||
}
|
||||
|
||||
func (w *KTlSServerConfig) ServerHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
|
||||
tlsConn, err := aTLS.ServerHandshake(ctx, conn, w.ServerConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
kConn, err := ktls.NewConn(ctx, w.logger, tlsConn, w.kernelTx, w.kernelRx)
|
||||
if err != nil {
|
||||
tlsConn.Close()
|
||||
return nil, E.Cause(err, "initialize kernel TLS")
|
||||
}
|
||||
return kConn, nil
|
||||
}
|
||||
|
||||
func (w *KTlSServerConfig) Clone() Config {
|
||||
return &KTlSServerConfig{
|
||||
w.ServerConfig.Clone().(ServerConfig),
|
||||
w.logger,
|
||||
w.kernelTx,
|
||||
w.kernelRx,
|
||||
}
|
||||
}
|
||||
@@ -28,10 +28,12 @@ import (
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/debug"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
|
||||
@@ -49,12 +51,12 @@ type RealityClientConfig struct {
|
||||
shortID [8]byte
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (*RealityClientConfig, error) {
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
if options.UTLS == nil || !options.UTLS.Enabled {
|
||||
return nil, E.New("uTLS is required by reality client")
|
||||
}
|
||||
|
||||
uClient, err := NewUTLSClient(ctx, serverAddress, options)
|
||||
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -74,7 +76,20 @@ func NewRealityClient(ctx context.Context, serverAddress string, options option.
|
||||
if decodedLen > 8 {
|
||||
return nil, E.New("invalid short_id")
|
||||
}
|
||||
return &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}, nil
|
||||
|
||||
var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}
|
||||
if options.KernelRx || options.KernelTx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
}
|
||||
config = &KTLSClientConfig{
|
||||
Config: config,
|
||||
logger: logger,
|
||||
kernelTx: options.KernelTx,
|
||||
kernelRx: options.KernelRx,
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) ServerName() string {
|
||||
@@ -93,7 +108,7 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
|
||||
e.uClient.SetNextProtos(nextProto)
|
||||
}
|
||||
|
||||
func (e *RealityClientConfig) Config() (*STDConfig, error) {
|
||||
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -28,7 +29,7 @@ type RealityServerConfig struct {
|
||||
config *utls.RealityConfig
|
||||
}
|
||||
|
||||
func NewRealityServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (*RealityServerConfig, error) {
|
||||
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
var tlsConfig utls.RealityConfig
|
||||
|
||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||
@@ -67,7 +68,10 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
|
||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||
}
|
||||
}
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
if len(options.CurvePreferences) > 0 {
|
||||
return nil, E.New("curve preferences is unavailable in reality")
|
||||
}
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" || len(options.ClientCertificatePublicKeySHA256) > 0 {
|
||||
return nil, E.New("certificate is unavailable in reality")
|
||||
}
|
||||
if len(options.Key) > 0 || options.KeyPath != "" {
|
||||
@@ -119,7 +123,22 @@ func NewRealityServer(ctx context.Context, logger log.Logger, options option.Inb
|
||||
return handshakeDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||
}
|
||||
|
||||
return &RealityServerConfig{&tlsConfig}, nil
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
}
|
||||
var config ServerConfig = &RealityServerConfig{&tlsConfig}
|
||||
if options.KernelTx || options.KernelRx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
}
|
||||
config = &KTlSServerConfig{
|
||||
ServerConfig: config,
|
||||
logger: logger,
|
||||
kernelTx: options.KernelTx,
|
||||
kernelRx: options.KernelRx,
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) ServerName() string {
|
||||
@@ -138,7 +157,7 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *RealityServerConfig) Config() (*tls.Config, error) {
|
||||
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) {
|
||||
return nil, E.New("unsupported usage for reality")
|
||||
}
|
||||
|
||||
|
||||
@@ -12,14 +12,37 @@ import (
|
||||
aTLS "github.com/sagernet/sing/common/tls"
|
||||
)
|
||||
|
||||
func NewServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
if !options.Enabled {
|
||||
type ServerOptions struct {
|
||||
Context context.Context
|
||||
Logger log.ContextLogger
|
||||
Options option.InboundTLSOptions
|
||||
KTLSCompatible bool
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
return NewServerWithOptions(ServerOptions{
|
||||
Context: ctx,
|
||||
Logger: logger,
|
||||
Options: options,
|
||||
})
|
||||
}
|
||||
|
||||
func NewServerWithOptions(options ServerOptions) (ServerConfig, error) {
|
||||
if !options.Options.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return NewRealityServer(ctx, logger, options)
|
||||
if !options.KTLSCompatible {
|
||||
if options.Options.KernelTx {
|
||||
options.Logger.Warn("enabling kTLS TX in current scenarios will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_tx")
|
||||
}
|
||||
}
|
||||
return NewSTDServer(ctx, logger, options)
|
||||
if options.Options.KernelRx {
|
||||
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
|
||||
}
|
||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||
return NewRealityServer(options.Context, options.Logger, options.Options)
|
||||
}
|
||||
return NewSTDServer(options.Context, options.Logger, options.Options)
|
||||
}
|
||||
|
||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -11,8 +14,10 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tlsfragment"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
)
|
||||
|
||||
@@ -40,7 +45,7 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) Config() (*STDConfig, error) {
|
||||
func (c *STDClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
|
||||
@@ -52,7 +57,13 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) Clone() Config {
|
||||
return &STDClientConfig{c.ctx, c.config.Clone(), c.fragment, c.fragmentFallbackDelay, c.recordFragment}
|
||||
return &STDClientConfig{
|
||||
ctx: c.ctx,
|
||||
config: c.config.Clone(),
|
||||
fragment: c.fragment,
|
||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||
recordFragment: c.recordFragment,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *STDClientConfig) ECHConfigList() []byte {
|
||||
@@ -63,7 +74,7 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte
|
||||
c.config.EncryptedClientHelloConfigList = EncryptedClientHelloConfigList
|
||||
}
|
||||
|
||||
func NewSTDClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
@@ -100,6 +111,15 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path")
|
||||
}
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
|
||||
}
|
||||
}
|
||||
if len(options.ALPN) > 0 {
|
||||
tlsConfig.NextProtos = options.ALPN
|
||||
}
|
||||
@@ -129,6 +149,9 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||
}
|
||||
}
|
||||
for _, curve := range options.CurvePreferences {
|
||||
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curve))
|
||||
}
|
||||
var certificate []byte
|
||||
if len(options.Certificate) > 0 {
|
||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||
@@ -146,10 +169,43 @@ func NewSTDClient(ctx context.Context, serverAddress string, options option.Outb
|
||||
}
|
||||
tlsConfig.RootCAs = certPool
|
||||
}
|
||||
stdConfig := &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
return parseECHClientConfig(ctx, stdConfig, options)
|
||||
} else {
|
||||
return stdConfig, nil
|
||||
var err error
|
||||
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if options.KernelRx || options.KernelTx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
}
|
||||
config = &KTLSClientConfig{
|
||||
Config: config,
|
||||
logger: logger,
|
||||
kernelTx: options.KernelTx,
|
||||
kernelRx: options.KernelRx,
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
|
||||
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
|
||||
if err != nil {
|
||||
return E.Cause(err, "failed to parse leaf certificate")
|
||||
}
|
||||
|
||||
pubKeyBytes, err := x509.MarshalPKIXPublicKey(leafCertificate.PublicKey)
|
||||
if err != nil {
|
||||
return E.Cause(err, "failed to marshal public key")
|
||||
}
|
||||
hashValue := sha256.Sum256(pubKeyBytes)
|
||||
for _, value := range knownHashValues {
|
||||
if bytes.Equal(value, hashValue[:]) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return E.New("unrecognized remote public key: ", base64.StdEncoding.EncodeToString(hashValue[:]))
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package tls
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
@@ -21,16 +23,17 @@ import (
|
||||
var errInsecureUnused = E.New("tls: insecure unused")
|
||||
|
||||
type STDServerConfig struct {
|
||||
access sync.RWMutex
|
||||
config *tls.Config
|
||||
logger log.Logger
|
||||
acmeService adapter.SimpleLifecycle
|
||||
certificate []byte
|
||||
key []byte
|
||||
certificatePath string
|
||||
keyPath string
|
||||
echKeyPath string
|
||||
watcher *fswatch.Watcher
|
||||
access sync.RWMutex
|
||||
config *tls.Config
|
||||
logger log.Logger
|
||||
acmeService adapter.SimpleLifecycle
|
||||
certificate []byte
|
||||
key []byte
|
||||
certificatePath string
|
||||
keyPath string
|
||||
clientCertificatePath []string
|
||||
echKeyPath string
|
||||
watcher *fswatch.Watcher
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) ServerName() string {
|
||||
@@ -69,7 +72,7 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) Config() (*STDConfig, error) {
|
||||
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
|
||||
@@ -110,6 +113,9 @@ func (c *STDServerConfig) startWatcher() error {
|
||||
if c.echKeyPath != "" {
|
||||
watchPath = append(watchPath, c.echKeyPath)
|
||||
}
|
||||
if len(c.clientCertificatePath) > 0 {
|
||||
watchPath = append(watchPath, c.clientCertificatePath...)
|
||||
}
|
||||
if len(watchPath) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -158,6 +164,30 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
c.logger.Info("reloaded TLS certificate")
|
||||
} else if common.Contains(c.clientCertificatePath, path) {
|
||||
clientCertificateCA := x509.NewCertPool()
|
||||
var reloaded bool
|
||||
for _, certPath := range c.clientCertificatePath {
|
||||
content, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
c.logger.Error(E.Cause(err, "reload certificate from ", c.clientCertificatePath))
|
||||
continue
|
||||
}
|
||||
if !clientCertificateCA.AppendCertsFromPEM(content) {
|
||||
c.logger.Error(E.New("invalid client certificate file: ", certPath))
|
||||
continue
|
||||
}
|
||||
reloaded = true
|
||||
}
|
||||
if !reloaded {
|
||||
return E.New("client certificates is empty")
|
||||
}
|
||||
c.access.Lock()
|
||||
config := c.config.Clone()
|
||||
config.ClientCAs = clientCertificateCA
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
c.logger.Info("reloaded client certificates")
|
||||
} else if path == c.echKeyPath {
|
||||
echKey, err := os.ReadFile(c.echKeyPath)
|
||||
if err != nil {
|
||||
@@ -182,7 +212,7 @@ func (c *STDServerConfig) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewSTDServer(ctx context.Context, logger log.Logger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
if !options.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -234,8 +264,14 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
return nil, E.New("unknown cipher_suite: ", cipherSuite)
|
||||
}
|
||||
}
|
||||
var certificate []byte
|
||||
var key []byte
|
||||
for _, curveID := range options.CurvePreferences {
|
||||
tlsConfig.CurvePreferences = append(tlsConfig.CurvePreferences, tls.CurveID(curveID))
|
||||
}
|
||||
tlsConfig.ClientAuth = tls.ClientAuthType(options.ClientAuthentication)
|
||||
var (
|
||||
certificate []byte
|
||||
key []byte
|
||||
)
|
||||
if acmeService == nil {
|
||||
if len(options.Certificate) > 0 {
|
||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||
@@ -277,6 +313,43 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
tlsConfig.Certificates = []tls.Certificate{keyPair}
|
||||
}
|
||||
}
|
||||
if len(options.ClientCertificate) > 0 || len(options.ClientCertificatePath) > 0 {
|
||||
if tlsConfig.ClientAuth == tls.NoClientCert {
|
||||
tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
||||
}
|
||||
}
|
||||
if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven || tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
|
||||
if len(options.ClientCertificate) > 0 {
|
||||
clientCertificateCA := x509.NewCertPool()
|
||||
if !clientCertificateCA.AppendCertsFromPEM([]byte(strings.Join(options.ClientCertificate, "\n"))) {
|
||||
return nil, E.New("invalid client certificate strings")
|
||||
}
|
||||
tlsConfig.ClientCAs = clientCertificateCA
|
||||
} else if len(options.ClientCertificatePath) > 0 {
|
||||
clientCertificateCA := x509.NewCertPool()
|
||||
for _, path := range options.ClientCertificatePath {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "read client certificate from ", path)
|
||||
}
|
||||
if !clientCertificateCA.AppendCertsFromPEM(content) {
|
||||
return nil, E.New("invalid client certificate file: ", path)
|
||||
}
|
||||
}
|
||||
tlsConfig.ClientCAs = clientCertificateCA
|
||||
} else if len(options.ClientCertificatePublicKeySHA256) > 0 {
|
||||
if tlsConfig.ClientAuth == tls.RequireAndVerifyClientCert {
|
||||
tlsConfig.ClientAuth = tls.RequireAnyClientCert
|
||||
} else if tlsConfig.ClientAuth == tls.VerifyClientCertIfGiven {
|
||||
tlsConfig.ClientAuth = tls.RequestClientCert
|
||||
}
|
||||
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
return verifyPublicKeySHA256(options.ClientCertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
|
||||
}
|
||||
} else {
|
||||
return nil, E.New("missing client_certificate, client_certificate_path or client_certificate_public_key_sha256 for client authentication")
|
||||
}
|
||||
}
|
||||
var echKeyPath string
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
err = parseECHServerConfig(ctx, options, tlsConfig, &echKeyPath)
|
||||
@@ -285,19 +358,32 @@ func NewSTDServer(ctx context.Context, logger log.Logger, options option.Inbound
|
||||
}
|
||||
}
|
||||
serverConfig := &STDServerConfig{
|
||||
config: tlsConfig,
|
||||
logger: logger,
|
||||
acmeService: acmeService,
|
||||
certificate: certificate,
|
||||
key: key,
|
||||
certificatePath: options.CertificatePath,
|
||||
keyPath: options.KeyPath,
|
||||
echKeyPath: echKeyPath,
|
||||
config: tlsConfig,
|
||||
logger: logger,
|
||||
acmeService: acmeService,
|
||||
certificate: certificate,
|
||||
key: key,
|
||||
certificatePath: options.CertificatePath,
|
||||
clientCertificatePath: options.ClientCertificatePath,
|
||||
keyPath: options.KeyPath,
|
||||
echKeyPath: echKeyPath,
|
||||
}
|
||||
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
serverConfig.access.Lock()
|
||||
defer serverConfig.access.Unlock()
|
||||
return serverConfig.config, nil
|
||||
}
|
||||
return serverConfig, nil
|
||||
var config ServerConfig = serverConfig
|
||||
if options.KernelTx || options.KernelRx {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
}
|
||||
config = &KTlSServerConfig{
|
||||
ServerConfig: config,
|
||||
logger: logger,
|
||||
kernelTx: options.KernelTx,
|
||||
kernelRx: options.KernelRx,
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
@@ -14,8 +14,11 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/tlsfragment"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
|
||||
utls "github.com/metacubex/utls"
|
||||
@@ -50,7 +53,7 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
||||
c.config.NextProtos = nextProto
|
||||
}
|
||||
|
||||
func (c *UTLSClientConfig) Config() (*STDConfig, error) {
|
||||
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) {
|
||||
return nil, E.New("unsupported usage for uTLS")
|
||||
}
|
||||
|
||||
@@ -139,7 +142,7 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
|
||||
return c.UConn.HandshakeContext(ctx)
|
||||
}
|
||||
|
||||
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
var serverName string
|
||||
if options.ServerName != "" {
|
||||
serverName = options.ServerName
|
||||
@@ -164,6 +167,15 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
||||
}
|
||||
tlsConfig.InsecureServerNameToVerify = serverName
|
||||
}
|
||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||
return nil, E.New("certificate_public_key_sha256 is conflict with certificate or certificate_path")
|
||||
}
|
||||
tlsConfig.InsecureSkipVerify = true
|
||||
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
|
||||
return verifyPublicKeySHA256(options.CertificatePublicKeySHA256, rawCerts, tlsConfig.Time)
|
||||
}
|
||||
}
|
||||
if len(options.ALPN) > 0 {
|
||||
tlsConfig.NextProtos = options.ALPN
|
||||
}
|
||||
@@ -214,15 +226,28 @@ func NewUTLSClient(ctx context.Context, serverAddress string, options option.Out
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uConfig := &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||
if options.ECH != nil && options.ECH.Enabled {
|
||||
if options.Reality != nil && options.Reality.Enabled {
|
||||
return nil, E.New("Reality is conflict with ECH")
|
||||
}
|
||||
return parseECHClientConfig(ctx, uConfig, options)
|
||||
} else {
|
||||
return uConfig, nil
|
||||
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if (options.KernelRx || options.KernelTx) && !common.PtrValueOrDefault(options.Reality).Enabled {
|
||||
if !C.IsLinux {
|
||||
return nil, E.New("kTLS is only supported on Linux")
|
||||
}
|
||||
config = &KTLSClientConfig{
|
||||
Config: config,
|
||||
logger: logger,
|
||||
kernelTx: options.KernelTx,
|
||||
kernelRx: options.KernelRx,
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -8,13 +8,14 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
)
|
||||
|
||||
func NewUTLSClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
func NewRealityClient(ctx context.Context, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing/common"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
@@ -100,7 +99,7 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
|
||||
return
|
||||
}
|
||||
defer instance.Close()
|
||||
if earlyConn, isEarlyConn := common.Cast[N.EarlyConn](instance); isEarlyConn && earlyConn.NeedHandshake() {
|
||||
if N.NeedHandshakeForWrite(instance) {
|
||||
start = time.Now()
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodHead, link, nil)
|
||||
|
||||
@@ -22,7 +22,8 @@ const (
|
||||
RuleSetVersion1 = 1 + iota
|
||||
RuleSetVersion2
|
||||
RuleSetVersion3
|
||||
RuleSetVersionCurrent = RuleSetVersion3
|
||||
RuleSetVersion4
|
||||
RuleSetVersionCurrent = RuleSetVersion4
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -39,4 +40,5 @@ const (
|
||||
const (
|
||||
RuleActionRejectMethodDefault = "default"
|
||||
RuleActionRejectMethodDrop = "drop"
|
||||
RuleActionRejectMethodReply = "reply"
|
||||
)
|
||||
|
||||
29
daemon/deprecated.go
Normal file
29
daemon/deprecated.go
Normal file
@@ -0,0 +1,29 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
var _ deprecated.Manager = (*deprecatedManager)(nil)
|
||||
|
||||
type deprecatedManager struct {
|
||||
access sync.Mutex
|
||||
notes []deprecated.Note
|
||||
}
|
||||
|
||||
func (m *deprecatedManager) ReportDeprecated(feature deprecated.Note) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.notes = common.Uniq(append(m.notes, feature))
|
||||
}
|
||||
|
||||
func (m *deprecatedManager) Get() []deprecated.Note {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
notes := m.notes
|
||||
m.notes = nil
|
||||
return notes
|
||||
}
|
||||
702
daemon/helper.pb.go
Normal file
702
daemon/helper.pb.go
Normal file
@@ -0,0 +1,702 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
emptypb "google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type SubscribeHelperRequestRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
AcceptGetWIFIStateRequests bool `protobuf:"varint,1,opt,name=acceptGetWIFIStateRequests,proto3" json:"acceptGetWIFIStateRequests,omitempty"`
|
||||
AcceptFindConnectionOwnerRequests bool `protobuf:"varint,2,opt,name=acceptFindConnectionOwnerRequests,proto3" json:"acceptFindConnectionOwnerRequests,omitempty"`
|
||||
AcceptSendNotificationRequests bool `protobuf:"varint,3,opt,name=acceptSendNotificationRequests,proto3" json:"acceptSendNotificationRequests,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SubscribeHelperRequestRequest) Reset() {
|
||||
*x = SubscribeHelperRequestRequest{}
|
||||
mi := &file_daemon_helper_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SubscribeHelperRequestRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SubscribeHelperRequestRequest) ProtoMessage() {}
|
||||
|
||||
func (x *SubscribeHelperRequestRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_helper_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SubscribeHelperRequestRequest.ProtoReflect.Descriptor instead.
|
||||
func (*SubscribeHelperRequestRequest) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_helper_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *SubscribeHelperRequestRequest) GetAcceptGetWIFIStateRequests() bool {
|
||||
if x != nil {
|
||||
return x.AcceptGetWIFIStateRequests
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *SubscribeHelperRequestRequest) GetAcceptFindConnectionOwnerRequests() bool {
|
||||
if x != nil {
|
||||
return x.AcceptFindConnectionOwnerRequests
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *SubscribeHelperRequestRequest) GetAcceptSendNotificationRequests() bool {
|
||||
if x != nil {
|
||||
return x.AcceptSendNotificationRequests
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type HelperRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
// Types that are valid to be assigned to Request:
|
||||
//
|
||||
// *HelperRequest_GetWIFIState
|
||||
// *HelperRequest_FindConnectionOwner
|
||||
// *HelperRequest_SendNotification
|
||||
Request isHelperRequest_Request `protobuf_oneof:"request"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HelperRequest) Reset() {
|
||||
*x = HelperRequest{}
|
||||
mi := &file_daemon_helper_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HelperRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HelperRequest) ProtoMessage() {}
|
||||
|
||||
func (x *HelperRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_helper_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HelperRequest.ProtoReflect.Descriptor instead.
|
||||
func (*HelperRequest) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_helper_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *HelperRequest) GetId() int64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *HelperRequest) GetRequest() isHelperRequest_Request {
|
||||
if x != nil {
|
||||
return x.Request
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HelperRequest) GetGetWIFIState() *emptypb.Empty {
|
||||
if x != nil {
|
||||
if x, ok := x.Request.(*HelperRequest_GetWIFIState); ok {
|
||||
return x.GetWIFIState
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HelperRequest) GetFindConnectionOwner() *FindConnectionOwnerRequest {
|
||||
if x != nil {
|
||||
if x, ok := x.Request.(*HelperRequest_FindConnectionOwner); ok {
|
||||
return x.FindConnectionOwner
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HelperRequest) GetSendNotification() *Notification {
|
||||
if x != nil {
|
||||
if x, ok := x.Request.(*HelperRequest_SendNotification); ok {
|
||||
return x.SendNotification
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isHelperRequest_Request interface {
|
||||
isHelperRequest_Request()
|
||||
}
|
||||
|
||||
type HelperRequest_GetWIFIState struct {
|
||||
GetWIFIState *emptypb.Empty `protobuf:"bytes,2,opt,name=getWIFIState,proto3,oneof"`
|
||||
}
|
||||
|
||||
type HelperRequest_FindConnectionOwner struct {
|
||||
FindConnectionOwner *FindConnectionOwnerRequest `protobuf:"bytes,3,opt,name=findConnectionOwner,proto3,oneof"`
|
||||
}
|
||||
|
||||
type HelperRequest_SendNotification struct {
|
||||
SendNotification *Notification `protobuf:"bytes,4,opt,name=sendNotification,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*HelperRequest_GetWIFIState) isHelperRequest_Request() {}
|
||||
|
||||
func (*HelperRequest_FindConnectionOwner) isHelperRequest_Request() {}
|
||||
|
||||
func (*HelperRequest_SendNotification) isHelperRequest_Request() {}
|
||||
|
||||
type FindConnectionOwnerRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
IpProtocol int32 `protobuf:"varint,1,opt,name=ipProtocol,proto3" json:"ipProtocol,omitempty"`
|
||||
SourceAddress string `protobuf:"bytes,2,opt,name=sourceAddress,proto3" json:"sourceAddress,omitempty"`
|
||||
SourcePort int32 `protobuf:"varint,3,opt,name=sourcePort,proto3" json:"sourcePort,omitempty"`
|
||||
DestinationAddress string `protobuf:"bytes,4,opt,name=destinationAddress,proto3" json:"destinationAddress,omitempty"`
|
||||
DestinationPort int32 `protobuf:"varint,5,opt,name=destinationPort,proto3" json:"destinationPort,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *FindConnectionOwnerRequest) Reset() {
|
||||
*x = FindConnectionOwnerRequest{}
|
||||
mi := &file_daemon_helper_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *FindConnectionOwnerRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*FindConnectionOwnerRequest) ProtoMessage() {}
|
||||
|
||||
func (x *FindConnectionOwnerRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_helper_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use FindConnectionOwnerRequest.ProtoReflect.Descriptor instead.
|
||||
func (*FindConnectionOwnerRequest) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_helper_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *FindConnectionOwnerRequest) GetIpProtocol() int32 {
|
||||
if x != nil {
|
||||
return x.IpProtocol
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *FindConnectionOwnerRequest) GetSourceAddress() string {
|
||||
if x != nil {
|
||||
return x.SourceAddress
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *FindConnectionOwnerRequest) GetSourcePort() int32 {
|
||||
if x != nil {
|
||||
return x.SourcePort
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *FindConnectionOwnerRequest) GetDestinationAddress() string {
|
||||
if x != nil {
|
||||
return x.DestinationAddress
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *FindConnectionOwnerRequest) GetDestinationPort() int32 {
|
||||
if x != nil {
|
||||
return x.DestinationPort
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Identifier string `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"`
|
||||
TypeName string `protobuf:"bytes,2,opt,name=typeName,proto3" json:"typeName,omitempty"`
|
||||
TypeId int32 `protobuf:"varint,3,opt,name=typeId,proto3" json:"typeId,omitempty"`
|
||||
Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"`
|
||||
Subtitle string `protobuf:"bytes,5,opt,name=subtitle,proto3" json:"subtitle,omitempty"`
|
||||
Body string `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
|
||||
OpenURL string `protobuf:"bytes,7,opt,name=openURL,proto3" json:"openURL,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *Notification) Reset() {
|
||||
*x = Notification{}
|
||||
mi := &file_daemon_helper_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *Notification) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Notification) ProtoMessage() {}
|
||||
|
||||
func (x *Notification) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_helper_proto_msgTypes[3]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use Notification.ProtoReflect.Descriptor instead.
|
||||
func (*Notification) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_helper_proto_rawDescGZIP(), []int{3}
|
||||
}
|
||||
|
||||
func (x *Notification) GetIdentifier() string {
|
||||
if x != nil {
|
||||
return x.Identifier
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Notification) GetTypeName() string {
|
||||
if x != nil {
|
||||
return x.TypeName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Notification) GetTypeId() int32 {
|
||||
if x != nil {
|
||||
return x.TypeId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *Notification) GetTitle() string {
|
||||
if x != nil {
|
||||
return x.Title
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Notification) GetSubtitle() string {
|
||||
if x != nil {
|
||||
return x.Subtitle
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Notification) GetBody() string {
|
||||
if x != nil {
|
||||
return x.Body
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Notification) GetOpenURL() string {
|
||||
if x != nil {
|
||||
return x.OpenURL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type HelperResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
// Types that are valid to be assigned to Response:
|
||||
//
|
||||
// *HelperResponse_WifiState
|
||||
// *HelperResponse_Error
|
||||
// *HelperResponse_ConnectionOwner
|
||||
Response isHelperResponse_Response `protobuf_oneof:"response"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *HelperResponse) Reset() {
|
||||
*x = HelperResponse{}
|
||||
mi := &file_daemon_helper_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *HelperResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*HelperResponse) ProtoMessage() {}
|
||||
|
||||
func (x *HelperResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_helper_proto_msgTypes[4]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use HelperResponse.ProtoReflect.Descriptor instead.
|
||||
func (*HelperResponse) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_helper_proto_rawDescGZIP(), []int{4}
|
||||
}
|
||||
|
||||
func (x *HelperResponse) GetId() int64 {
|
||||
if x != nil {
|
||||
return x.Id
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *HelperResponse) GetResponse() isHelperResponse_Response {
|
||||
if x != nil {
|
||||
return x.Response
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HelperResponse) GetWifiState() *WIFIState {
|
||||
if x != nil {
|
||||
if x, ok := x.Response.(*HelperResponse_WifiState); ok {
|
||||
return x.WifiState
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *HelperResponse) GetError() string {
|
||||
if x != nil {
|
||||
if x, ok := x.Response.(*HelperResponse_Error); ok {
|
||||
return x.Error
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *HelperResponse) GetConnectionOwner() *ConnectionOwner {
|
||||
if x != nil {
|
||||
if x, ok := x.Response.(*HelperResponse_ConnectionOwner); ok {
|
||||
return x.ConnectionOwner
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isHelperResponse_Response interface {
|
||||
isHelperResponse_Response()
|
||||
}
|
||||
|
||||
type HelperResponse_WifiState struct {
|
||||
WifiState *WIFIState `protobuf:"bytes,2,opt,name=wifiState,proto3,oneof"`
|
||||
}
|
||||
|
||||
type HelperResponse_Error struct {
|
||||
Error string `protobuf:"bytes,3,opt,name=error,proto3,oneof"`
|
||||
}
|
||||
|
||||
type HelperResponse_ConnectionOwner struct {
|
||||
ConnectionOwner *ConnectionOwner `protobuf:"bytes,4,opt,name=connectionOwner,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*HelperResponse_WifiState) isHelperResponse_Response() {}
|
||||
|
||||
func (*HelperResponse_Error) isHelperResponse_Response() {}
|
||||
|
||||
func (*HelperResponse_ConnectionOwner) isHelperResponse_Response() {}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserId int32 `protobuf:"varint,1,opt,name=userId,proto3" json:"userId,omitempty"`
|
||||
UserName string `protobuf:"bytes,2,opt,name=userName,proto3" json:"userName,omitempty"`
|
||||
ProcessPath string `protobuf:"bytes,3,opt,name=processPath,proto3" json:"processPath,omitempty"`
|
||||
AndroidPackageName string `protobuf:"bytes,4,opt,name=androidPackageName,proto3" json:"androidPackageName,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ConnectionOwner) Reset() {
|
||||
*x = ConnectionOwner{}
|
||||
mi := &file_daemon_helper_proto_msgTypes[5]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ConnectionOwner) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ConnectionOwner) ProtoMessage() {}
|
||||
|
||||
func (x *ConnectionOwner) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_helper_proto_msgTypes[5]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ConnectionOwner.ProtoReflect.Descriptor instead.
|
||||
func (*ConnectionOwner) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_helper_proto_rawDescGZIP(), []int{5}
|
||||
}
|
||||
|
||||
func (x *ConnectionOwner) GetUserId() int32 {
|
||||
if x != nil {
|
||||
return x.UserId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *ConnectionOwner) GetUserName() string {
|
||||
if x != nil {
|
||||
return x.UserName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ConnectionOwner) GetProcessPath() string {
|
||||
if x != nil {
|
||||
return x.ProcessPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ConnectionOwner) GetAndroidPackageName() string {
|
||||
if x != nil {
|
||||
return x.AndroidPackageName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type WIFIState struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Ssid string `protobuf:"bytes,1,opt,name=ssid,proto3" json:"ssid,omitempty"`
|
||||
Bssid string `protobuf:"bytes,2,opt,name=bssid,proto3" json:"bssid,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *WIFIState) Reset() {
|
||||
*x = WIFIState{}
|
||||
mi := &file_daemon_helper_proto_msgTypes[6]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *WIFIState) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*WIFIState) ProtoMessage() {}
|
||||
|
||||
func (x *WIFIState) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_helper_proto_msgTypes[6]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use WIFIState.ProtoReflect.Descriptor instead.
|
||||
func (*WIFIState) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_helper_proto_rawDescGZIP(), []int{6}
|
||||
}
|
||||
|
||||
func (x *WIFIState) GetSsid() string {
|
||||
if x != nil {
|
||||
return x.Ssid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *WIFIState) GetBssid() string {
|
||||
if x != nil {
|
||||
return x.Bssid
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_daemon_helper_proto protoreflect.FileDescriptor
|
||||
|
||||
const file_daemon_helper_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x13daemon/helper.proto\x12\x06daemon\x1a\x1bgoogle/protobuf/empty.proto\"\xf5\x01\n" +
|
||||
"\x1dSubscribeHelperRequestRequest\x12>\n" +
|
||||
"\x1aacceptGetWIFIStateRequests\x18\x01 \x01(\bR\x1aacceptGetWIFIStateRequests\x12L\n" +
|
||||
"!acceptFindConnectionOwnerRequests\x18\x02 \x01(\bR!acceptFindConnectionOwnerRequests\x12F\n" +
|
||||
"\x1eacceptSendNotificationRequests\x18\x03 \x01(\bR\x1eacceptSendNotificationRequests\"\x84\x02\n" +
|
||||
"\rHelperRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x03R\x02id\x12<\n" +
|
||||
"\fgetWIFIState\x18\x02 \x01(\v2\x16.google.protobuf.EmptyH\x00R\fgetWIFIState\x12V\n" +
|
||||
"\x13findConnectionOwner\x18\x03 \x01(\v2\".daemon.FindConnectionOwnerRequestH\x00R\x13findConnectionOwner\x12B\n" +
|
||||
"\x10sendNotification\x18\x04 \x01(\v2\x14.daemon.NotificationH\x00R\x10sendNotificationB\t\n" +
|
||||
"\arequest\"\xdc\x01\n" +
|
||||
"\x1aFindConnectionOwnerRequest\x12\x1e\n" +
|
||||
"\n" +
|
||||
"ipProtocol\x18\x01 \x01(\x05R\n" +
|
||||
"ipProtocol\x12$\n" +
|
||||
"\rsourceAddress\x18\x02 \x01(\tR\rsourceAddress\x12\x1e\n" +
|
||||
"\n" +
|
||||
"sourcePort\x18\x03 \x01(\x05R\n" +
|
||||
"sourcePort\x12.\n" +
|
||||
"\x12destinationAddress\x18\x04 \x01(\tR\x12destinationAddress\x12(\n" +
|
||||
"\x0fdestinationPort\x18\x05 \x01(\x05R\x0fdestinationPort\"\xc2\x01\n" +
|
||||
"\fNotification\x12\x1e\n" +
|
||||
"\n" +
|
||||
"identifier\x18\x01 \x01(\tR\n" +
|
||||
"identifier\x12\x1a\n" +
|
||||
"\btypeName\x18\x02 \x01(\tR\btypeName\x12\x16\n" +
|
||||
"\x06typeId\x18\x03 \x01(\x05R\x06typeId\x12\x14\n" +
|
||||
"\x05title\x18\x04 \x01(\tR\x05title\x12\x1a\n" +
|
||||
"\bsubtitle\x18\x05 \x01(\tR\bsubtitle\x12\x12\n" +
|
||||
"\x04body\x18\x06 \x01(\tR\x04body\x12\x18\n" +
|
||||
"\aopenURL\x18\a \x01(\tR\aopenURL\"\xbc\x01\n" +
|
||||
"\x0eHelperResponse\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x03R\x02id\x121\n" +
|
||||
"\twifiState\x18\x02 \x01(\v2\x11.daemon.WIFIStateH\x00R\twifiState\x12\x16\n" +
|
||||
"\x05error\x18\x03 \x01(\tH\x00R\x05error\x12C\n" +
|
||||
"\x0fconnectionOwner\x18\x04 \x01(\v2\x17.daemon.ConnectionOwnerH\x00R\x0fconnectionOwnerB\n" +
|
||||
"\n" +
|
||||
"\bresponse\"\x97\x01\n" +
|
||||
"\x0fConnectionOwner\x12\x16\n" +
|
||||
"\x06userId\x18\x01 \x01(\x05R\x06userId\x12\x1a\n" +
|
||||
"\buserName\x18\x02 \x01(\tR\buserName\x12 \n" +
|
||||
"\vprocessPath\x18\x03 \x01(\tR\vprocessPath\x12.\n" +
|
||||
"\x12androidPackageName\x18\x04 \x01(\tR\x12androidPackageName\"5\n" +
|
||||
"\tWIFIState\x12\x12\n" +
|
||||
"\x04ssid\x18\x01 \x01(\tR\x04ssid\x12\x14\n" +
|
||||
"\x05bssid\x18\x02 \x01(\tR\x05bssidB%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
|
||||
|
||||
var (
|
||||
file_daemon_helper_proto_rawDescOnce sync.Once
|
||||
file_daemon_helper_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_daemon_helper_proto_rawDescGZIP() []byte {
|
||||
file_daemon_helper_proto_rawDescOnce.Do(func() {
|
||||
file_daemon_helper_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_daemon_helper_proto_rawDesc), len(file_daemon_helper_proto_rawDesc)))
|
||||
})
|
||||
return file_daemon_helper_proto_rawDescData
|
||||
}
|
||||
|
||||
var (
|
||||
file_daemon_helper_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
||||
file_daemon_helper_proto_goTypes = []any{
|
||||
(*SubscribeHelperRequestRequest)(nil), // 0: daemon.SubscribeHelperRequestRequest
|
||||
(*HelperRequest)(nil), // 1: daemon.HelperRequest
|
||||
(*FindConnectionOwnerRequest)(nil), // 2: daemon.FindConnectionOwnerRequest
|
||||
(*Notification)(nil), // 3: daemon.Notification
|
||||
(*HelperResponse)(nil), // 4: daemon.HelperResponse
|
||||
(*ConnectionOwner)(nil), // 5: daemon.ConnectionOwner
|
||||
(*WIFIState)(nil), // 6: daemon.WIFIState
|
||||
(*emptypb.Empty)(nil), // 7: google.protobuf.Empty
|
||||
}
|
||||
)
|
||||
|
||||
var file_daemon_helper_proto_depIdxs = []int32{
|
||||
7, // 0: daemon.HelperRequest.getWIFIState:type_name -> google.protobuf.Empty
|
||||
2, // 1: daemon.HelperRequest.findConnectionOwner:type_name -> daemon.FindConnectionOwnerRequest
|
||||
3, // 2: daemon.HelperRequest.sendNotification:type_name -> daemon.Notification
|
||||
6, // 3: daemon.HelperResponse.wifiState:type_name -> daemon.WIFIState
|
||||
5, // 4: daemon.HelperResponse.connectionOwner:type_name -> daemon.ConnectionOwner
|
||||
5, // [5:5] is the sub-list for method output_type
|
||||
5, // [5:5] is the sub-list for method input_type
|
||||
5, // [5:5] is the sub-list for extension type_name
|
||||
5, // [5:5] is the sub-list for extension extendee
|
||||
0, // [0:5] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_daemon_helper_proto_init() }
|
||||
func file_daemon_helper_proto_init() {
|
||||
if File_daemon_helper_proto != nil {
|
||||
return
|
||||
}
|
||||
file_daemon_helper_proto_msgTypes[1].OneofWrappers = []any{
|
||||
(*HelperRequest_GetWIFIState)(nil),
|
||||
(*HelperRequest_FindConnectionOwner)(nil),
|
||||
(*HelperRequest_SendNotification)(nil),
|
||||
}
|
||||
file_daemon_helper_proto_msgTypes[4].OneofWrappers = []any{
|
||||
(*HelperResponse_WifiState)(nil),
|
||||
(*HelperResponse_Error)(nil),
|
||||
(*HelperResponse_ConnectionOwner)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_helper_proto_rawDesc), len(file_daemon_helper_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 7,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_daemon_helper_proto_goTypes,
|
||||
DependencyIndexes: file_daemon_helper_proto_depIdxs,
|
||||
MessageInfos: file_daemon_helper_proto_msgTypes,
|
||||
}.Build()
|
||||
File_daemon_helper_proto = out.File
|
||||
file_daemon_helper_proto_goTypes = nil
|
||||
file_daemon_helper_proto_depIdxs = nil
|
||||
}
|
||||
61
daemon/helper.proto
Normal file
61
daemon/helper.proto
Normal file
@@ -0,0 +1,61 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package daemon;
|
||||
option go_package = "github.com/sagernet/sing-box/daemon";
|
||||
|
||||
import "google/protobuf/empty.proto";
|
||||
|
||||
message SubscribeHelperRequestRequest {
|
||||
bool acceptGetWIFIStateRequests = 1;
|
||||
bool acceptFindConnectionOwnerRequests = 2;
|
||||
bool acceptSendNotificationRequests = 3;
|
||||
}
|
||||
|
||||
message HelperRequest {
|
||||
int64 id = 1;
|
||||
oneof request {
|
||||
google.protobuf.Empty getWIFIState = 2;
|
||||
FindConnectionOwnerRequest findConnectionOwner = 3;
|
||||
Notification sendNotification = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message FindConnectionOwnerRequest {
|
||||
int32 ipProtocol = 1;
|
||||
string sourceAddress = 2;
|
||||
int32 sourcePort = 3;
|
||||
string destinationAddress = 4;
|
||||
int32 destinationPort = 5;
|
||||
}
|
||||
|
||||
message Notification {
|
||||
string identifier = 1;
|
||||
string typeName = 2;
|
||||
int32 typeId = 3;
|
||||
string title = 4;
|
||||
string subtitle = 5;
|
||||
string body = 6;
|
||||
string openURL = 7;
|
||||
}
|
||||
|
||||
message HelperResponse {
|
||||
int64 id = 1;
|
||||
oneof response {
|
||||
WIFIState wifiState = 2;
|
||||
string error = 3;
|
||||
ConnectionOwner connectionOwner = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message ConnectionOwner {
|
||||
int32 userId = 1;
|
||||
string userName = 2;
|
||||
string processPath = 3;
|
||||
string androidPackageName = 4;
|
||||
}
|
||||
|
||||
message WIFIState {
|
||||
string ssid = 1;
|
||||
string bssid = 2;
|
||||
}
|
||||
|
||||
147
daemon/instance.go
Normal file
147
daemon/instance.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
|
||||
"github.com/sagernet/sing-box"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/include"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/json"
|
||||
"github.com/sagernet/sing/service"
|
||||
"github.com/sagernet/sing/service/filemanager"
|
||||
"github.com/sagernet/sing/service/pause"
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
instance *box.Box
|
||||
clashServer adapter.ClashServer
|
||||
cacheFile adapter.CacheFile
|
||||
pauseManager pause.Manager
|
||||
urlTestHistoryStorage *urltest.HistoryStorage
|
||||
}
|
||||
|
||||
func (s *StartedService) baseContext() context.Context {
|
||||
dnsRegistry := include.DNSTransportRegistry()
|
||||
if s.platform != nil && s.platform.UsePlatformLocalDNSTransport() {
|
||||
dns.RegisterTransport[option.LocalDNSServerOptions](dnsRegistry, C.DNSTypeLocal, s.platform.LocalDNSTransport())
|
||||
}
|
||||
ctx := box.Context(s.ctx, include.InboundRegistry(), include.OutboundRegistry(), include.EndpointRegistry(), dnsRegistry, include.ServiceRegistry())
|
||||
ctx = filemanager.WithDefault(ctx, s.workingDirectory, s.tempDirectory, s.userID, s.groupID)
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (s *StartedService) CheckConfig(configContent string) error {
|
||||
ctx := s.baseContext()
|
||||
options, err := parseConfig(ctx, configContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
instance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
})
|
||||
if err == nil {
|
||||
instance.Close()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *StartedService) FormatConfig(configContent string) (string, error) {
|
||||
options, err := parseConfig(s.baseContext(), configContent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
encoder := json.NewEncoder(&buffer)
|
||||
encoder.SetIndent("", " ")
|
||||
err = encoder.Encode(options)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buffer.String(), nil
|
||||
}
|
||||
|
||||
type OverrideOptions struct {
|
||||
AutoRedirect bool
|
||||
IncludePackage []string
|
||||
ExcludePackage []string
|
||||
}
|
||||
|
||||
func (s *StartedService) newInstance(profileContent string, overrideOptions *OverrideOptions) (*Instance, error) {
|
||||
ctx := s.baseContext()
|
||||
service.MustRegister[deprecated.Manager](ctx, new(deprecatedManager))
|
||||
ctx, cancel := context.WithCancel(include.Context(ctx))
|
||||
options, err := parseConfig(ctx, profileContent)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
if overrideOptions != nil {
|
||||
for _, inbound := range options.Inbounds {
|
||||
if tunInboundOptions, isTUN := inbound.Options.(*option.TunInboundOptions); isTUN {
|
||||
tunInboundOptions.AutoRedirect = overrideOptions.AutoRedirect
|
||||
tunInboundOptions.IncludePackage = append(tunInboundOptions.IncludePackage, overrideOptions.IncludePackage...)
|
||||
tunInboundOptions.ExcludePackage = append(tunInboundOptions.ExcludePackage, overrideOptions.ExcludePackage...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
urlTestHistoryStorage := urltest.NewHistoryStorage()
|
||||
ctx = service.ContextWithPtr(ctx, urlTestHistoryStorage)
|
||||
i := &Instance{
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
urlTestHistoryStorage: urlTestHistoryStorage,
|
||||
}
|
||||
boxInstance, err := box.New(box.Options{
|
||||
Context: ctx,
|
||||
Options: options,
|
||||
PlatformLogWriter: s,
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, err
|
||||
}
|
||||
i.instance = boxInstance
|
||||
i.clashServer = service.FromContext[adapter.ClashServer](ctx)
|
||||
i.pauseManager = service.FromContext[pause.Manager](ctx)
|
||||
i.cacheFile = service.FromContext[adapter.CacheFile](ctx)
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *Instance) Start() error {
|
||||
return i.instance.Start()
|
||||
}
|
||||
|
||||
func (i *Instance) Close() error {
|
||||
i.cancel()
|
||||
i.urlTestHistoryStorage.Close()
|
||||
return i.instance.Close()
|
||||
}
|
||||
|
||||
func (i *Instance) Box() *box.Box {
|
||||
return i.instance
|
||||
}
|
||||
|
||||
func (i *Instance) PauseManager() pause.Manager {
|
||||
return i.pauseManager
|
||||
}
|
||||
|
||||
func parseConfig(ctx context.Context, configContent string) (option.Options, error) {
|
||||
options, err := json.UnmarshalExtendedContext[option.Options](ctx, []byte(configContent))
|
||||
if err != nil {
|
||||
return option.Options{}, E.Cause(err, "decode config")
|
||||
}
|
||||
return options, nil
|
||||
}
|
||||
22
daemon/platform.go
Normal file
22
daemon/platform.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type PlatformHandler interface {
|
||||
ServiceStop() error
|
||||
ServiceReload() error
|
||||
SystemProxyStatus() (*SystemProxyStatus, error)
|
||||
SetSystemProxyEnabled(enabled bool) error
|
||||
WriteDebugMessage(message string)
|
||||
}
|
||||
|
||||
type PlatformInterface interface {
|
||||
adapter.PlatformInterface
|
||||
|
||||
UsePlatformLocalDNSTransport() bool
|
||||
LocalDNSTransport() dns.TransportConstructorFunc[option.LocalDNSServerOptions]
|
||||
}
|
||||
774
daemon/started_service.go
Normal file
774
daemon/started_service.go
Normal file
@@ -0,0 +1,774 @@
|
||||
package daemon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/conntrack"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
"github.com/sagernet/sing-box/experimental/clashapi"
|
||||
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/protocol/group"
|
||||
"github.com/sagernet/sing/common"
|
||||
"github.com/sagernet/sing/common/batch"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/memory"
|
||||
"github.com/sagernet/sing/common/observable"
|
||||
"github.com/sagernet/sing/common/x/list"
|
||||
"github.com/sagernet/sing/service"
|
||||
|
||||
"github.com/gofrs/uuid/v5"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/protobuf/types/known/emptypb"
|
||||
)
|
||||
|
||||
var _ StartedServiceServer = (*StartedService)(nil)
|
||||
|
||||
type StartedService struct {
|
||||
ctx context.Context
|
||||
platform PlatformInterface
|
||||
platformHandler PlatformHandler
|
||||
debug bool
|
||||
logMaxLines int
|
||||
workingDirectory string
|
||||
tempDirectory string
|
||||
userID int
|
||||
groupID int
|
||||
systemProxyEnabled bool
|
||||
serviceAccess sync.RWMutex
|
||||
serviceStatus *ServiceStatus
|
||||
serviceStatusSubscriber *observable.Subscriber[*ServiceStatus]
|
||||
serviceStatusObserver *observable.Observer[*ServiceStatus]
|
||||
logAccess sync.RWMutex
|
||||
logLines list.List[*log.Entry]
|
||||
logSubscriber *observable.Subscriber[*log.Entry]
|
||||
logObserver *observable.Observer[*log.Entry]
|
||||
instance *Instance
|
||||
urlTestSubscriber *observable.Subscriber[struct{}]
|
||||
urlTestObserver *observable.Observer[struct{}]
|
||||
urlTestHistoryStorage *urltest.HistoryStorage
|
||||
clashModeSubscriber *observable.Subscriber[struct{}]
|
||||
clashModeObserver *observable.Observer[struct{}]
|
||||
}
|
||||
|
||||
type ServiceOptions struct {
|
||||
Context context.Context
|
||||
Platform PlatformInterface
|
||||
PlatformHandler PlatformHandler
|
||||
Debug bool
|
||||
LogMaxLines int
|
||||
WorkingDirectory string
|
||||
TempDirectory string
|
||||
UserID int
|
||||
GroupID int
|
||||
SystemProxyEnabled bool
|
||||
}
|
||||
|
||||
func NewStartedService(options ServiceOptions) *StartedService {
|
||||
s := &StartedService{
|
||||
ctx: options.Context,
|
||||
platform: options.Platform,
|
||||
platformHandler: options.PlatformHandler,
|
||||
debug: options.Debug,
|
||||
logMaxLines: options.LogMaxLines,
|
||||
workingDirectory: options.WorkingDirectory,
|
||||
tempDirectory: options.TempDirectory,
|
||||
userID: options.UserID,
|
||||
groupID: options.GroupID,
|
||||
systemProxyEnabled: options.SystemProxyEnabled,
|
||||
serviceStatus: &ServiceStatus{Status: ServiceStatus_IDLE},
|
||||
serviceStatusSubscriber: observable.NewSubscriber[*ServiceStatus](4),
|
||||
logSubscriber: observable.NewSubscriber[*log.Entry](128),
|
||||
urlTestSubscriber: observable.NewSubscriber[struct{}](1),
|
||||
urlTestHistoryStorage: urltest.NewHistoryStorage(),
|
||||
clashModeSubscriber: observable.NewSubscriber[struct{}](1),
|
||||
}
|
||||
s.serviceStatusObserver = observable.NewObserver(s.serviceStatusSubscriber, 2)
|
||||
s.logObserver = observable.NewObserver(s.logSubscriber, 64)
|
||||
s.urlTestObserver = observable.NewObserver(s.urlTestSubscriber, 1)
|
||||
s.clashModeObserver = observable.NewObserver(s.clashModeSubscriber, 1)
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StartedService) resetLogs() {
|
||||
s.logAccess.Lock()
|
||||
s.logLines = list.List[*log.Entry]{}
|
||||
s.logAccess.Unlock()
|
||||
s.logSubscriber.Emit(nil)
|
||||
}
|
||||
|
||||
func (s *StartedService) updateStatus(newStatus ServiceStatus_Type) {
|
||||
statusObject := &ServiceStatus{Status: newStatus}
|
||||
s.serviceStatusSubscriber.Emit(statusObject)
|
||||
s.serviceStatus = statusObject
|
||||
}
|
||||
|
||||
func (s *StartedService) updateStatusError(err error) error {
|
||||
statusObject := &ServiceStatus{Status: ServiceStatus_FATAL, ErrorMessage: err.Error()}
|
||||
s.serviceStatusSubscriber.Emit(statusObject)
|
||||
s.serviceStatus = statusObject
|
||||
s.serviceAccess.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
|
||||
s.serviceAccess.Lock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
|
||||
default:
|
||||
s.serviceAccess.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
s.updateStatus(ServiceStatus_STARTING)
|
||||
s.resetLogs()
|
||||
instance, err := s.newInstance(profileContent, options)
|
||||
if err != nil {
|
||||
return s.updateStatusError(err)
|
||||
}
|
||||
s.instance = instance
|
||||
s.serviceAccess.Unlock()
|
||||
err = instance.Start()
|
||||
s.serviceAccess.Lock()
|
||||
if s.serviceStatus.Status != ServiceStatus_STARTING {
|
||||
s.serviceAccess.Unlock()
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return s.updateStatusError(err)
|
||||
}
|
||||
s.updateStatus(ServiceStatus_STARTED)
|
||||
s.serviceAccess.Unlock()
|
||||
runtime.GC()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StartedService) CloseService() error {
|
||||
s.serviceAccess.Lock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_STARTING, ServiceStatus_STARTED:
|
||||
default:
|
||||
s.serviceAccess.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
s.updateStatus(ServiceStatus_STOPPING)
|
||||
if s.instance != nil {
|
||||
err := s.instance.Close()
|
||||
if err != nil {
|
||||
return s.updateStatusError(err)
|
||||
}
|
||||
}
|
||||
s.instance = nil
|
||||
s.updateStatus(ServiceStatus_IDLE)
|
||||
s.serviceAccess.Unlock()
|
||||
runtime.GC()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SetError(err error) {
|
||||
s.serviceAccess.Lock()
|
||||
s.updateStatusError(err)
|
||||
s.serviceAccess.Unlock()
|
||||
s.WriteMessage(log.LevelError, err.Error())
|
||||
}
|
||||
|
||||
func (s *StartedService) StopService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
err := s.platformHandler.ServiceStop()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) ReloadService(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
err := s.platformHandler.ServiceReload()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeServiceStatus(empty *emptypb.Empty, server grpc.ServerStreamingServer[ServiceStatus]) error {
|
||||
subscription, done, err := s.serviceStatusObserver.Subscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.serviceStatusObserver.UnSubscribe(subscription)
|
||||
err = server.Send(s.serviceStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case <-server.Context().Done():
|
||||
return server.Context().Err()
|
||||
case newStatus := <-subscription:
|
||||
err = server.Send(newStatus)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeLog(empty *emptypb.Empty, server grpc.ServerStreamingServer[Log]) error {
|
||||
var savedLines []*log.Entry
|
||||
s.logAccess.Lock()
|
||||
savedLines = make([]*log.Entry, 0, s.logLines.Len())
|
||||
for element := s.logLines.Front(); element != nil; element = element.Next() {
|
||||
savedLines = append(savedLines, element.Value)
|
||||
}
|
||||
s.logAccess.Unlock()
|
||||
subscription, done, err := s.logObserver.Subscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.logObserver.UnSubscribe(subscription)
|
||||
err = server.Send(&Log{
|
||||
Messages: common.Map(savedLines, func(it *log.Entry) *Log_Message {
|
||||
return &Log_Message{
|
||||
Level: LogLevel(it.Level),
|
||||
Message: it.Message,
|
||||
}
|
||||
}),
|
||||
Reset_: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case <-server.Context().Done():
|
||||
return server.Context().Err()
|
||||
case message := <-subscription:
|
||||
if message == nil {
|
||||
err = server.Send(&Log{Reset_: true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
messages := []*Log_Message{{
|
||||
Level: LogLevel(message.Level),
|
||||
Message: message.Message,
|
||||
}}
|
||||
fetch:
|
||||
for {
|
||||
select {
|
||||
case message = <-subscription:
|
||||
messages = append(messages, &Log_Message{
|
||||
Level: LogLevel(message.Level),
|
||||
Message: message.Message,
|
||||
})
|
||||
default:
|
||||
break fetch
|
||||
}
|
||||
}
|
||||
err = server.Send(&Log{Messages: messages})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case <-done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) GetDefaultLogLevel(ctx context.Context, empty *emptypb.Empty) (*DefaultLogLevel, error) {
|
||||
s.serviceAccess.RLock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_STARTING, ServiceStatus_STARTED:
|
||||
default:
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
logLevel := s.instance.instance.LogFactory().Level()
|
||||
s.serviceAccess.RUnlock()
|
||||
return &DefaultLogLevel{Level: LogLevel(logLevel)}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeStatus(request *SubscribeStatusRequest, server grpc.ServerStreamingServer[Status]) error {
|
||||
interval := time.Duration(request.Interval)
|
||||
if interval <= 0 {
|
||||
interval = time.Second // Default to 1 second
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
status := s.readStatus()
|
||||
uploadTotal := status.UplinkTotal
|
||||
downloadTotal := status.DownlinkTotal
|
||||
for {
|
||||
err := server.Send(status)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case <-server.Context().Done():
|
||||
return server.Context().Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
status = s.readStatus()
|
||||
upload := status.UplinkTotal - uploadTotal
|
||||
download := status.DownlinkTotal - downloadTotal
|
||||
uploadTotal = status.UplinkTotal
|
||||
downloadTotal = status.DownlinkTotal
|
||||
status.Uplink = upload
|
||||
status.Downlink = download
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) readStatus() *Status {
|
||||
var status Status
|
||||
status.Memory = memory.Inuse()
|
||||
status.Goroutines = int32(runtime.NumGoroutine())
|
||||
status.ConnectionsOut = int32(conntrack.Count())
|
||||
nowService := s.instance
|
||||
if nowService != nil {
|
||||
if clashServer := nowService.clashServer; clashServer != nil {
|
||||
status.TrafficAvailable = true
|
||||
trafficManager := clashServer.(*clashapi.Server).TrafficManager()
|
||||
status.UplinkTotal, status.DownlinkTotal = trafficManager.Total()
|
||||
status.ConnectionsIn = int32(trafficManager.ConnectionsLen())
|
||||
}
|
||||
}
|
||||
return &status
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeGroups(empty *emptypb.Empty, server grpc.ServerStreamingServer[Groups]) error {
|
||||
subscription, done, err := s.urlTestObserver.Subscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.urlTestObserver.UnSubscribe(subscription)
|
||||
for {
|
||||
s.serviceAccess.RLock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_STARTING, ServiceStatus_STARTED:
|
||||
groups := s.readGroups()
|
||||
s.serviceAccess.RUnlock()
|
||||
err = server.Send(groups)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
s.serviceAccess.RUnlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
select {
|
||||
case <-subscription:
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case <-server.Context().Done():
|
||||
return server.Context().Err()
|
||||
case <-done:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) readGroups() *Groups {
|
||||
historyStorage := s.instance.urlTestHistoryStorage
|
||||
boxService := s.instance
|
||||
outbounds := boxService.instance.Outbound().Outbounds()
|
||||
var iGroups []adapter.OutboundGroup
|
||||
for _, it := range outbounds {
|
||||
if group, isGroup := it.(adapter.OutboundGroup); isGroup {
|
||||
iGroups = append(iGroups, group)
|
||||
}
|
||||
}
|
||||
var gs Groups
|
||||
for _, iGroup := range iGroups {
|
||||
var g Group
|
||||
g.Tag = iGroup.Tag()
|
||||
g.Type = iGroup.Type()
|
||||
_, g.Selectable = iGroup.(*group.Selector)
|
||||
g.Selected = iGroup.Now()
|
||||
if boxService.cacheFile != nil {
|
||||
if isExpand, loaded := boxService.cacheFile.LoadGroupExpand(g.Tag); loaded {
|
||||
g.IsExpand = isExpand
|
||||
}
|
||||
}
|
||||
|
||||
for _, itemTag := range iGroup.All() {
|
||||
itemOutbound, isLoaded := boxService.instance.Outbound().Outbound(itemTag)
|
||||
if !isLoaded {
|
||||
continue
|
||||
}
|
||||
|
||||
var item GroupItem
|
||||
item.Tag = itemTag
|
||||
item.Type = itemOutbound.Type()
|
||||
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(itemOutbound)); history != nil {
|
||||
item.UrlTestTime = history.Time.Unix()
|
||||
item.UrlTestDelay = int32(history.Delay)
|
||||
}
|
||||
g.Items = append(g.Items, &item)
|
||||
}
|
||||
if len(g.Items) < 2 {
|
||||
continue
|
||||
}
|
||||
gs.Group = append(gs.Group, &g)
|
||||
}
|
||||
return &gs
|
||||
}
|
||||
|
||||
func (s *StartedService) GetClashModeStatus(ctx context.Context, empty *emptypb.Empty) (*ClashModeStatus, error) {
|
||||
s.serviceAccess.RLock()
|
||||
if s.serviceStatus.Status != ServiceStatus_STARTED {
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
clashServer := s.instance.clashServer
|
||||
s.serviceAccess.RUnlock()
|
||||
if clashServer == nil {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
return &ClashModeStatus{
|
||||
ModeList: clashServer.ModeList(),
|
||||
CurrentMode: clashServer.Mode(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeClashMode(empty *emptypb.Empty, server grpc.ServerStreamingServer[ClashMode]) error {
|
||||
subscription, done, err := s.clashModeObserver.Subscribe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.clashModeObserver.UnSubscribe(subscription)
|
||||
for {
|
||||
select {
|
||||
case <-subscription:
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case <-server.Context().Done():
|
||||
return server.Context().Err()
|
||||
case <-done:
|
||||
return nil
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
if s.serviceStatus.Status != ServiceStatus_STARTED {
|
||||
return nil
|
||||
}
|
||||
message := &ClashMode{Mode: s.instance.clashServer.Mode()}
|
||||
s.serviceAccess.RUnlock()
|
||||
err = server.Send(message)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) SetClashMode(ctx context.Context, request *ClashMode) (*emptypb.Empty, error) {
|
||||
s.serviceAccess.RLock()
|
||||
if s.serviceStatus.Status != ServiceStatus_STARTED {
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
clashServer := s.instance.clashServer
|
||||
s.serviceAccess.RUnlock()
|
||||
clashServer.(*clashapi.Server).SetMode(request.Mode)
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) URLTest(ctx context.Context, request *URLTestRequest) (*emptypb.Empty, error) {
|
||||
s.serviceAccess.RLock()
|
||||
if s.serviceStatus.Status != ServiceStatus_STARTED {
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
groupTag := request.OutboundTag
|
||||
abstractOutboundGroup, isLoaded := boxService.instance.Outbound().Outbound(groupTag)
|
||||
if !isLoaded {
|
||||
return nil, E.New("outbound group not found: ", groupTag)
|
||||
}
|
||||
outboundGroup, isOutboundGroup := abstractOutboundGroup.(adapter.OutboundGroup)
|
||||
if !isOutboundGroup {
|
||||
return nil, E.New("outbound is not a group: ", groupTag)
|
||||
}
|
||||
urlTest, isURLTest := abstractOutboundGroup.(*group.URLTest)
|
||||
if isURLTest {
|
||||
go urlTest.CheckOutbounds()
|
||||
} else {
|
||||
var historyStorage adapter.URLTestHistoryStorage
|
||||
if s.instance.clashServer != nil {
|
||||
historyStorage = s.instance.clashServer.HistoryStorage()
|
||||
} else {
|
||||
return nil, E.New("Clash API is required for URLTest on non-URLTest group")
|
||||
}
|
||||
|
||||
outbounds := common.Filter(common.Map(outboundGroup.All(), func(it string) adapter.Outbound {
|
||||
itOutbound, _ := boxService.instance.Outbound().Outbound(it)
|
||||
return itOutbound
|
||||
}), func(it adapter.Outbound) bool {
|
||||
if it == nil {
|
||||
return false
|
||||
}
|
||||
_, isGroup := it.(adapter.OutboundGroup)
|
||||
if isGroup {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
b, _ := batch.New(boxService.ctx, batch.WithConcurrencyNum[any](10))
|
||||
for _, detour := range outbounds {
|
||||
outboundToTest := detour
|
||||
outboundTag := outboundToTest.Tag()
|
||||
b.Go(outboundTag, func() (any, error) {
|
||||
t, err := urltest.URLTest(boxService.ctx, "", outboundToTest)
|
||||
if err != nil {
|
||||
historyStorage.DeleteURLTestHistory(outboundTag)
|
||||
} else {
|
||||
historyStorage.StoreURLTestHistory(outboundTag, &adapter.URLTestHistory{
|
||||
Time: time.Now(),
|
||||
Delay: t,
|
||||
})
|
||||
}
|
||||
return nil, nil
|
||||
})
|
||||
}
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SelectOutbound(ctx context.Context, request *SelectOutboundRequest) (*emptypb.Empty, error) {
|
||||
s.serviceAccess.RLock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_STARTING, ServiceStatus_STARTED:
|
||||
default:
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
boxService := s.instance.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
outboundGroup, isLoaded := boxService.Outbound().Outbound(request.GroupTag)
|
||||
if !isLoaded {
|
||||
return nil, E.New("selector not found: ", request.GroupTag)
|
||||
}
|
||||
selector, isSelector := outboundGroup.(*group.Selector)
|
||||
if !isSelector {
|
||||
return nil, E.New("outbound is not a selector: ", request.GroupTag)
|
||||
}
|
||||
if !selector.SelectOutbound(request.OutboundTag) {
|
||||
return nil, E.New("outbound not found in selector: ", request.OutboundTag)
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SetGroupExpand(ctx context.Context, request *SetGroupExpandRequest) (*emptypb.Empty, error) {
|
||||
s.serviceAccess.RLock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_STARTING, ServiceStatus_STARTED:
|
||||
default:
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
if boxService.cacheFile != nil {
|
||||
err := boxService.cacheFile.StoreGroupExpand(request.GroupTag, request.IsExpand)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) GetSystemProxyStatus(ctx context.Context, empty *emptypb.Empty) (*SystemProxyStatus, error) {
|
||||
return s.platformHandler.SystemProxyStatus()
|
||||
}
|
||||
|
||||
func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *SetSystemProxyEnabledRequest) (*emptypb.Empty, error) {
|
||||
err := s.platformHandler.SetSystemProxyEnabled(request.Enabled)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[Connections]) error {
|
||||
s.serviceAccess.RLock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_STARTING, ServiceStatus_STARTED:
|
||||
default:
|
||||
s.serviceAccess.RUnlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
ticker := time.NewTicker(time.Duration(request.Interval))
|
||||
defer ticker.Stop()
|
||||
trafficManager := boxService.clashServer.(*clashapi.Server).TrafficManager()
|
||||
var (
|
||||
connections = make(map[uuid.UUID]*Connection)
|
||||
outConnections []*Connection
|
||||
)
|
||||
for {
|
||||
outConnections = outConnections[:0]
|
||||
for _, connection := range trafficManager.Connections() {
|
||||
outConnections = append(outConnections, newConnection(connections, connection, false))
|
||||
}
|
||||
for _, connection := range trafficManager.ClosedConnections() {
|
||||
outConnections = append(outConnections, newConnection(connections, connection, true))
|
||||
}
|
||||
err := server.Send(&Connections{Connections: outConnections})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-s.ctx.Done():
|
||||
return s.ctx.Err()
|
||||
case <-server.Context().Done():
|
||||
return server.Context().Err()
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newConnection(connections map[uuid.UUID]*Connection, metadata trafficontrol.TrackerMetadata, isClosed bool) *Connection {
|
||||
if oldConnection, loaded := connections[metadata.ID]; loaded {
|
||||
if isClosed {
|
||||
if oldConnection.ClosedAt == 0 {
|
||||
oldConnection.Uplink = 0
|
||||
oldConnection.Downlink = 0
|
||||
oldConnection.ClosedAt = metadata.ClosedAt.UnixMilli()
|
||||
}
|
||||
return oldConnection
|
||||
}
|
||||
lastUplink := oldConnection.UplinkTotal
|
||||
lastDownlink := oldConnection.DownlinkTotal
|
||||
uplinkTotal := metadata.Upload.Load()
|
||||
downlinkTotal := metadata.Download.Load()
|
||||
oldConnection.Uplink = uplinkTotal - lastUplink
|
||||
oldConnection.Downlink = downlinkTotal - lastDownlink
|
||||
oldConnection.UplinkTotal = uplinkTotal
|
||||
oldConnection.DownlinkTotal = downlinkTotal
|
||||
return oldConnection
|
||||
}
|
||||
var rule string
|
||||
if metadata.Rule != nil {
|
||||
rule = metadata.Rule.String()
|
||||
}
|
||||
uplinkTotal := metadata.Upload.Load()
|
||||
downlinkTotal := metadata.Download.Load()
|
||||
uplink := uplinkTotal
|
||||
downlink := downlinkTotal
|
||||
var closedAt int64
|
||||
if !metadata.ClosedAt.IsZero() {
|
||||
closedAt = metadata.ClosedAt.UnixMilli()
|
||||
uplink = 0
|
||||
downlink = 0
|
||||
}
|
||||
connection := &Connection{
|
||||
Id: metadata.ID.String(),
|
||||
Inbound: metadata.Metadata.Inbound,
|
||||
InboundType: metadata.Metadata.InboundType,
|
||||
IpVersion: int32(metadata.Metadata.IPVersion),
|
||||
Network: metadata.Metadata.Network,
|
||||
Source: metadata.Metadata.Source.String(),
|
||||
Destination: metadata.Metadata.Destination.String(),
|
||||
Domain: metadata.Metadata.Domain,
|
||||
Protocol: metadata.Metadata.Protocol,
|
||||
User: metadata.Metadata.User,
|
||||
FromOutbound: metadata.Metadata.Outbound,
|
||||
CreatedAt: metadata.CreatedAt.UnixMilli(),
|
||||
ClosedAt: closedAt,
|
||||
Uplink: uplink,
|
||||
Downlink: downlink,
|
||||
UplinkTotal: uplinkTotal,
|
||||
DownlinkTotal: downlinkTotal,
|
||||
Rule: rule,
|
||||
Outbound: metadata.Outbound,
|
||||
OutboundType: metadata.OutboundType,
|
||||
ChainList: metadata.Chain,
|
||||
}
|
||||
connections[metadata.ID] = connection
|
||||
return connection
|
||||
}
|
||||
|
||||
func (s *StartedService) CloseConnection(ctx context.Context, request *CloseConnectionRequest) (*emptypb.Empty, error) {
|
||||
s.serviceAccess.RLock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_STARTING, ServiceStatus_STARTED:
|
||||
default:
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
targetConn := boxService.clashServer.(*clashapi.Server).TrafficManager().Connection(uuid.FromStringOrNil(request.Id))
|
||||
if targetConn != nil {
|
||||
targetConn.Close()
|
||||
}
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) CloseAllConnections(ctx context.Context, empty *emptypb.Empty) (*emptypb.Empty, error) {
|
||||
conntrack.Close()
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *emptypb.Empty) (*DeprecatedWarnings, error) {
|
||||
s.serviceAccess.RLock()
|
||||
if s.serviceStatus.Status != ServiceStatus_STARTED {
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
notes := service.FromContext[deprecated.Manager](boxService.ctx).(*deprecatedManager).Get()
|
||||
return &DeprecatedWarnings{
|
||||
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
|
||||
return &DeprecatedWarning{
|
||||
Message: it.Message(),
|
||||
Impending: it.Impending(),
|
||||
MigrationLink: it.MigrationLink,
|
||||
}
|
||||
}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeHelperEvents(empty *emptypb.Empty, server grpc.ServerStreamingServer[HelperRequest]) error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *StartedService) SendHelperResponse(ctx context.Context, response *HelperResponse) (*emptypb.Empty, error) {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
|
||||
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
|
||||
}
|
||||
|
||||
func (s *StartedService) WriteMessage(level log.Level, message string) {
|
||||
item := &log.Entry{Level: level, Message: message}
|
||||
s.logSubscriber.Emit(item)
|
||||
s.logAccess.Lock()
|
||||
s.logLines.PushBack(item)
|
||||
if s.logLines.Len() > s.logMaxLines {
|
||||
s.logLines.Remove(s.logLines.Front())
|
||||
}
|
||||
s.logAccess.Unlock()
|
||||
if s.debug {
|
||||
s.platformHandler.WriteDebugMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) Instance() *Instance {
|
||||
s.serviceAccess.RLock()
|
||||
defer s.serviceAccess.RUnlock()
|
||||
return s.instance
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user