mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
Compare commits
48 Commits
renovate/g
...
fix-acme-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d22636778 | ||
|
|
ab01d7b9d9 | ||
|
|
ebf8a213b6 | ||
|
|
ab323e0eb9 | ||
|
|
2132e68d3a | ||
|
|
47742abe93 | ||
|
|
77e51035bd | ||
|
|
eeb5dead2a | ||
|
|
45339d101b | ||
|
|
04c0490992 | ||
|
|
7ffdc48b49 | ||
|
|
e15bdf11eb | ||
|
|
e3bcb06c3e | ||
|
|
84d2280960 | ||
|
|
4fd2532b0a | ||
|
|
02ccde6c71 | ||
|
|
e98b4ad449 | ||
|
|
d09182614c | ||
|
|
6381de7bab | ||
|
|
b0c6762bc1 | ||
|
|
7425100bac | ||
|
|
d454aa0fdf | ||
|
|
a3623eb41a | ||
|
|
72bc4c1f87 | ||
|
|
9ac1e2ff32 | ||
|
|
0045103d14 | ||
|
|
d2a933784c | ||
|
|
3f05a37f65 | ||
|
|
b8e5a71450 | ||
|
|
c13faa8e3c | ||
|
|
7623bcd19e | ||
|
|
795d1c2892 | ||
|
|
6913b11e0a | ||
|
|
1e57c06295 | ||
|
|
ea464cef8d | ||
|
|
a8e3cd3256 | ||
|
|
686cf1f304 | ||
|
|
9fbfb87723 | ||
|
|
d2fa21d07b | ||
|
|
d3768cca36 | ||
|
|
0889ddd001 | ||
|
|
f46fbf188a | ||
|
|
f2d15139f5 | ||
|
|
041646b728 | ||
|
|
b990de2e12 | ||
|
|
fe585157d2 | ||
|
|
eed6a36e5d | ||
|
|
eb0f38544c |
@@ -4,6 +4,7 @@
|
||||
--license GPL-3.0-or-later
|
||||
--description "The universal proxy platform."
|
||||
--url "https://sing-box.sagernet.org/"
|
||||
--vendor SagerNet
|
||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||
--no-deb-generate-changes
|
||||
|
||||
2
.github/CRONET_GO_VERSION
vendored
2
.github/CRONET_GO_VERSION
vendored
@@ -1 +1 @@
|
||||
2fef65f9dba90ddb89a87d00a6eb6165487c10c1
|
||||
ea7cd33752aed62603775af3df946c1b83f4b0b3
|
||||
|
||||
33
.github/detect_track.sh
vendored
Executable file
33
.github/detect_track.sh
vendored
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
branches=$(git branch -r --contains HEAD)
|
||||
if echo "$branches" | grep -q 'origin/stable'; then
|
||||
track=stable
|
||||
elif echo "$branches" | grep -q 'origin/testing'; then
|
||||
track=testing
|
||||
elif echo "$branches" | grep -q 'origin/oldstable'; then
|
||||
track=oldstable
|
||||
else
|
||||
echo "ERROR: HEAD is not on any known release branch (stable/testing/oldstable)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$track" == "stable" ]]; then
|
||||
tag=$(git describe --tags --exact-match HEAD 2>/dev/null || true)
|
||||
if [[ -n "$tag" && "$tag" == *"-"* ]]; then
|
||||
track=beta
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$track" in
|
||||
stable) name=sing-box; docker_tag=latest ;;
|
||||
beta) name=sing-box-beta; docker_tag=latest-beta ;;
|
||||
testing) name=sing-box-testing; docker_tag=latest-testing ;;
|
||||
oldstable) name=sing-box-oldstable; docker_tag=latest-oldstable ;;
|
||||
esac
|
||||
|
||||
echo "track=${track} name=${name} docker_tag=${docker_tag}" >&2
|
||||
echo "TRACK=${track}" >> "$GITHUB_ENV"
|
||||
echo "NAME=${name}" >> "$GITHUB_ENV"
|
||||
echo "DOCKER_TAG=${docker_tag}" >> "$GITHUB_ENV"
|
||||
19
.github/workflows/docker.yml
vendored
19
.github/workflows/docker.yml
vendored
@@ -19,7 +19,6 @@ env:
|
||||
jobs:
|
||||
build_binary:
|
||||
name: Build binary
|
||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -260,13 +259,13 @@ jobs:
|
||||
fi
|
||||
echo "ref=$ref"
|
||||
echo "ref=$ref" >> $GITHUB_OUTPUT
|
||||
if [[ $ref == *"-"* ]]; then
|
||||
latest=latest-beta
|
||||
else
|
||||
latest=latest
|
||||
fi
|
||||
echo "latest=$latest"
|
||||
echo "latest=$latest" >> $GITHUB_OUTPUT
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
ref: ${{ steps.ref.outputs.ref }}
|
||||
fetch-depth: 0
|
||||
- name: Detect track
|
||||
run: bash .github/detect_track.sh
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
@@ -286,11 +285,11 @@ jobs:
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}" \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}" \
|
||||
-t "${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}" \
|
||||
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
|
||||
- name: Inspect image
|
||||
if: github.event_name != 'push'
|
||||
run: |
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.latest }}
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ env.DOCKER_TAG }}
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.ref.outputs.ref }}
|
||||
|
||||
16
.github/workflows/linux.yml
vendored
16
.github/workflows/linux.yml
vendored
@@ -11,11 +11,6 @@ on:
|
||||
description: "Version name"
|
||||
required: true
|
||||
type: string
|
||||
forceBeta:
|
||||
description: "Force beta"
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
@@ -23,7 +18,6 @@ on:
|
||||
jobs:
|
||||
calculate_version:
|
||||
name: Calculate version
|
||||
if: github.event_name != 'release' || github.event.release.target_commitish != 'oldstable'
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.outputs.outputs.version }}
|
||||
@@ -168,14 +162,8 @@ jobs:
|
||||
- name: Set mtime
|
||||
run: |-
|
||||
TZ=UTC touch -t '197001010000' dist/sing-box
|
||||
- name: Set name
|
||||
if: (! contains(needs.calculate_version.outputs.version, '-')) && !inputs.forceBeta
|
||||
run: |-
|
||||
echo "NAME=sing-box" >> "$GITHUB_ENV"
|
||||
- name: Set beta name
|
||||
if: contains(needs.calculate_version.outputs.version, '-') || inputs.forceBeta
|
||||
run: |-
|
||||
echo "NAME=sing-box-beta" >> "$GITHUB_ENV"
|
||||
- name: Detect track
|
||||
run: bash .github/detect_track.sh
|
||||
- name: Set version
|
||||
run: |-
|
||||
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
|
||||
|
||||
21
adapter/certificate/adapter.go
Normal file
21
adapter/certificate/adapter.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package certificate
|
||||
|
||||
type Adapter struct {
|
||||
providerType string
|
||||
providerTag string
|
||||
}
|
||||
|
||||
func NewAdapter(providerType string, providerTag string) Adapter {
|
||||
return Adapter{
|
||||
providerType: providerType,
|
||||
providerTag: providerTag,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) Type() string {
|
||||
return a.providerType
|
||||
}
|
||||
|
||||
func (a *Adapter) Tag() string {
|
||||
return a.providerTag
|
||||
}
|
||||
158
adapter/certificate/manager.go
Normal file
158
adapter/certificate/manager.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
F "github.com/sagernet/sing/common/format"
|
||||
)
|
||||
|
||||
var _ adapter.CertificateProviderManager = (*Manager)(nil)
|
||||
|
||||
type Manager struct {
|
||||
logger log.ContextLogger
|
||||
registry adapter.CertificateProviderRegistry
|
||||
access sync.Mutex
|
||||
started bool
|
||||
stage adapter.StartStage
|
||||
providers []adapter.CertificateProviderService
|
||||
providerByTag map[string]adapter.CertificateProviderService
|
||||
}
|
||||
|
||||
func NewManager(logger log.ContextLogger, registry adapter.CertificateProviderRegistry) *Manager {
|
||||
return &Manager{
|
||||
logger: logger,
|
||||
registry: registry,
|
||||
providerByTag: make(map[string]adapter.CertificateProviderService),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||
m.access.Lock()
|
||||
if m.started && m.stage >= stage {
|
||||
panic("already started")
|
||||
}
|
||||
m.started = true
|
||||
m.stage = stage
|
||||
providers := m.providers
|
||||
m.access.Unlock()
|
||||
for _, provider := range providers {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
m.logger.Trace(stage, " ", name)
|
||||
startTime := time.Now()
|
||||
err := adapter.LegacyStart(provider, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() error {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if !m.started {
|
||||
return nil
|
||||
}
|
||||
m.started = false
|
||||
providers := m.providers
|
||||
m.providers = nil
|
||||
monitor := taskmonitor.New(m.logger, C.StopTimeout)
|
||||
var err error
|
||||
for _, provider := range providers {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
m.logger.Trace("close ", name)
|
||||
startTime := time.Now()
|
||||
monitor.Start("close ", name)
|
||||
err = E.Append(err, provider.Close(), func(err error) error {
|
||||
return E.Cause(err, "close ", name)
|
||||
})
|
||||
monitor.Finish()
|
||||
m.logger.Trace("close ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Manager) CertificateProviders() []adapter.CertificateProviderService {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
return m.providers
|
||||
}
|
||||
|
||||
func (m *Manager) Get(tag string) (adapter.CertificateProviderService, bool) {
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
m.access.Unlock()
|
||||
return provider, found
|
||||
}
|
||||
|
||||
func (m *Manager) Remove(tag string) error {
|
||||
m.access.Lock()
|
||||
provider, found := m.providerByTag[tag]
|
||||
if !found {
|
||||
m.access.Unlock()
|
||||
return os.ErrInvalid
|
||||
}
|
||||
delete(m.providerByTag, tag)
|
||||
index := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
|
||||
return it == provider
|
||||
})
|
||||
if index == -1 {
|
||||
panic("invalid certificate provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:index], m.providers[index+1:]...)
|
||||
started := m.started
|
||||
m.access.Unlock()
|
||||
if started {
|
||||
return provider.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error {
|
||||
provider, err := m.registry.Create(ctx, logger, tag, providerType, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
if m.started {
|
||||
name := "certificate-provider/" + provider.Type() + "[" + provider.Tag() + "]"
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
m.logger.Trace(stage, " ", name)
|
||||
startTime := time.Now()
|
||||
err = adapter.LegacyStart(provider, stage)
|
||||
if err != nil {
|
||||
return E.Cause(err, stage, " ", name)
|
||||
}
|
||||
m.logger.Trace(stage, " ", name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||
}
|
||||
}
|
||||
if existsProvider, loaded := m.providerByTag[tag]; loaded {
|
||||
if m.started {
|
||||
err = existsProvider.Close()
|
||||
if err != nil {
|
||||
return E.Cause(err, "close certificate-provider/", existsProvider.Type(), "[", existsProvider.Tag(), "]")
|
||||
}
|
||||
}
|
||||
existsIndex := common.Index(m.providers, func(it adapter.CertificateProviderService) bool {
|
||||
return it == existsProvider
|
||||
})
|
||||
if existsIndex == -1 {
|
||||
panic("invalid certificate provider index")
|
||||
}
|
||||
m.providers = append(m.providers[:existsIndex], m.providers[existsIndex+1:]...)
|
||||
}
|
||||
m.providers = append(m.providers, provider)
|
||||
m.providerByTag[tag] = provider
|
||||
return nil
|
||||
}
|
||||
72
adapter/certificate/registry.go
Normal file
72
adapter/certificate/registry.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ConstructorFunc[T any] func(ctx context.Context, logger log.ContextLogger, tag string, options T) (adapter.CertificateProviderService, error)
|
||||
|
||||
func Register[Options any](registry *Registry, providerType string, constructor ConstructorFunc[Options]) {
|
||||
registry.register(providerType, func() any {
|
||||
return new(Options)
|
||||
}, func(ctx context.Context, logger log.ContextLogger, tag string, rawOptions any) (adapter.CertificateProviderService, error) {
|
||||
var options *Options
|
||||
if rawOptions != nil {
|
||||
options = rawOptions.(*Options)
|
||||
}
|
||||
return constructor(ctx, logger, tag, common.PtrValueOrDefault(options))
|
||||
})
|
||||
}
|
||||
|
||||
var _ adapter.CertificateProviderRegistry = (*Registry)(nil)
|
||||
|
||||
type (
|
||||
optionsConstructorFunc func() any
|
||||
constructorFunc func(ctx context.Context, logger log.ContextLogger, tag string, options any) (adapter.CertificateProviderService, error)
|
||||
)
|
||||
|
||||
type Registry struct {
|
||||
access sync.Mutex
|
||||
optionsType map[string]optionsConstructorFunc
|
||||
constructor map[string]constructorFunc
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
optionsType: make(map[string]optionsConstructorFunc),
|
||||
constructor: make(map[string]constructorFunc),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Registry) CreateOptions(providerType string) (any, bool) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
optionsConstructor, loaded := m.optionsType[providerType]
|
||||
if !loaded {
|
||||
return nil, false
|
||||
}
|
||||
return optionsConstructor(), true
|
||||
}
|
||||
|
||||
func (m *Registry) Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (adapter.CertificateProviderService, error) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
constructor, loaded := m.constructor[providerType]
|
||||
if !loaded {
|
||||
return nil, E.New("certificate provider type not found: " + providerType)
|
||||
}
|
||||
return constructor(ctx, logger, tag, options)
|
||||
}
|
||||
|
||||
func (m *Registry) register(providerType string, optionsConstructor optionsConstructorFunc, constructor constructorFunc) {
|
||||
m.access.Lock()
|
||||
defer m.access.Unlock()
|
||||
m.optionsType[providerType] = optionsConstructor
|
||||
m.constructor[providerType] = constructor
|
||||
}
|
||||
38
adapter/certificate_provider.go
Normal file
38
adapter/certificate_provider.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
)
|
||||
|
||||
type CertificateProvider interface {
|
||||
GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
}
|
||||
|
||||
type ACMECertificateProvider interface {
|
||||
CertificateProvider
|
||||
GetACMENextProtos() []string
|
||||
}
|
||||
|
||||
type CertificateProviderService interface {
|
||||
Lifecycle
|
||||
Type() string
|
||||
Tag() string
|
||||
CertificateProvider
|
||||
}
|
||||
|
||||
type CertificateProviderRegistry interface {
|
||||
option.CertificateProviderOptionsRegistry
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) (CertificateProviderService, error)
|
||||
}
|
||||
|
||||
type CertificateProviderManager interface {
|
||||
Lifecycle
|
||||
CertificateProviders() []CertificateProviderService
|
||||
Get(tag string) (CertificateProviderService, bool)
|
||||
Remove(tag string) error
|
||||
Create(ctx context.Context, logger log.ContextLogger, tag string, providerType string, options any) error
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package adapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
@@ -82,6 +83,8 @@ type InboundContext struct {
|
||||
SourceGeoIPCode string
|
||||
GeoIPCode string
|
||||
ProcessInfo *ConnectionOwner
|
||||
SourceMACAddress net.HardwareAddr
|
||||
SourceHostname string
|
||||
QueryType uint16
|
||||
FakeIP bool
|
||||
|
||||
@@ -101,6 +104,10 @@ type InboundContext struct {
|
||||
func (c *InboundContext) ResetRuleCache() {
|
||||
c.IPCIDRMatchSource = false
|
||||
c.IPCIDRAcceptEmpty = false
|
||||
c.ResetRuleMatchCache()
|
||||
}
|
||||
|
||||
func (c *InboundContext) ResetRuleMatchCache() {
|
||||
c.SourceAddressMatch = false
|
||||
c.SourcePortMatch = false
|
||||
c.DestinationAddressMatch = false
|
||||
|
||||
23
adapter/neighbor.go
Normal file
23
adapter/neighbor.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
)
|
||||
|
||||
type NeighborEntry struct {
|
||||
Address netip.Addr
|
||||
MACAddress net.HardwareAddr
|
||||
Hostname string
|
||||
}
|
||||
|
||||
type NeighborResolver interface {
|
||||
LookupMAC(address netip.Addr) (net.HardwareAddr, bool)
|
||||
LookupHostname(address netip.Addr) (string, bool)
|
||||
Start() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type NeighborUpdateListener interface {
|
||||
UpdateNeighborTable(entries []NeighborEntry)
|
||||
}
|
||||
@@ -36,6 +36,10 @@ type PlatformInterface interface {
|
||||
|
||||
UsePlatformNotification() bool
|
||||
SendNotification(notification *Notification) error
|
||||
|
||||
UsePlatformNeighborResolver() bool
|
||||
StartNeighborMonitor(listener NeighborUpdateListener) error
|
||||
CloseNeighborMonitor(listener NeighborUpdateListener) error
|
||||
}
|
||||
|
||||
type FindConnectionOwnerRequest struct {
|
||||
@@ -47,11 +51,11 @@ type FindConnectionOwnerRequest struct {
|
||||
}
|
||||
|
||||
type ConnectionOwner struct {
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageName string
|
||||
ProcessID uint32
|
||||
UserId int32
|
||||
UserName string
|
||||
ProcessPath string
|
||||
AndroidPackageNames []string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
|
||||
@@ -26,6 +26,8 @@ type Router interface {
|
||||
RuleSet(tag string) (RuleSet, bool)
|
||||
Rules() []Rule
|
||||
NeedFindProcess() bool
|
||||
NeedFindNeighbor() bool
|
||||
NeighborResolver() NeighborResolver
|
||||
AppendTracker(tracker ConnectionTracker)
|
||||
ResetNetwork()
|
||||
}
|
||||
|
||||
123
box.go
123
box.go
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
boxCertificate "github.com/sagernet/sing-box/adapter/certificate"
|
||||
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||
"github.com/sagernet/sing-box/adapter/inbound"
|
||||
"github.com/sagernet/sing-box/adapter/outbound"
|
||||
@@ -37,20 +38,21 @@ import (
|
||||
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||
|
||||
type Box struct {
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
service *boxService.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
createdAt time.Time
|
||||
logFactory log.Factory
|
||||
logger log.ContextLogger
|
||||
network *route.NetworkManager
|
||||
endpoint *endpoint.Manager
|
||||
inbound *inbound.Manager
|
||||
outbound *outbound.Manager
|
||||
service *boxService.Manager
|
||||
certificateProvider *boxCertificate.Manager
|
||||
dnsTransport *dns.TransportManager
|
||||
dnsRouter *dns.Router
|
||||
connection *route.ConnectionManager
|
||||
router *route.Router
|
||||
internalService []adapter.LifecycleService
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@@ -66,6 +68,7 @@ func Context(
|
||||
endpointRegistry adapter.EndpointRegistry,
|
||||
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||
serviceRegistry adapter.ServiceRegistry,
|
||||
certificateProviderRegistry adapter.CertificateProviderRegistry,
|
||||
) context.Context {
|
||||
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||
@@ -90,6 +93,10 @@ func Context(
|
||||
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||
}
|
||||
if service.FromContext[adapter.CertificateProviderRegistry](ctx) == nil {
|
||||
ctx = service.ContextWith[option.CertificateProviderOptionsRegistry](ctx, certificateProviderRegistry)
|
||||
ctx = service.ContextWith[adapter.CertificateProviderRegistry](ctx, certificateProviderRegistry)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -106,6 +113,7 @@ func New(options Options) (*Box, error) {
|
||||
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||
certificateProviderRegistry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
|
||||
|
||||
if endpointRegistry == nil {
|
||||
return nil, E.New("missing endpoint registry in context")
|
||||
@@ -122,6 +130,9 @@ func New(options Options) (*Box, error) {
|
||||
if serviceRegistry == nil {
|
||||
return nil, E.New("missing service registry in context")
|
||||
}
|
||||
if certificateProviderRegistry == nil {
|
||||
return nil, E.New("missing certificate provider registry in context")
|
||||
}
|
||||
|
||||
ctx = pause.WithDefaultManager(ctx)
|
||||
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||
@@ -179,11 +190,13 @@ func New(options Options) (*Box, error) {
|
||||
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||
certificateProviderManager := boxCertificate.NewManager(logFactory.NewLogger("certificate-provider"), certificateProviderRegistry)
|
||||
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
||||
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||
@@ -272,6 +285,24 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize inbound[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, serviceOptions := range options.Services {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
tag = serviceOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = serviceManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
serviceOptions.Type,
|
||||
serviceOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, outboundOptions := range options.Outbounds {
|
||||
var tag string
|
||||
if outboundOptions.Tag != "" {
|
||||
@@ -298,22 +329,22 @@ func New(options Options) (*Box, error) {
|
||||
return nil, E.Cause(err, "initialize outbound[", i, "]")
|
||||
}
|
||||
}
|
||||
for i, serviceOptions := range options.Services {
|
||||
for i, certificateProviderOptions := range options.CertificateProviders {
|
||||
var tag string
|
||||
if serviceOptions.Tag != "" {
|
||||
tag = serviceOptions.Tag
|
||||
if certificateProviderOptions.Tag != "" {
|
||||
tag = certificateProviderOptions.Tag
|
||||
} else {
|
||||
tag = F.ToString(i)
|
||||
}
|
||||
err = serviceManager.Create(
|
||||
err = certificateProviderManager.Create(
|
||||
ctx,
|
||||
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||
logFactory.NewLogger(F.ToString("certificate-provider/", certificateProviderOptions.Type, "[", tag, "]")),
|
||||
tag,
|
||||
serviceOptions.Type,
|
||||
serviceOptions.Options,
|
||||
certificateProviderOptions.Type,
|
||||
certificateProviderOptions.Options,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "initialize service[", i, "]")
|
||||
return nil, E.Cause(err, "initialize certificate provider[", i, "]")
|
||||
}
|
||||
}
|
||||
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||
@@ -383,20 +414,21 @@ func New(options Options) (*Box, error) {
|
||||
internalServices = append(internalServices, adapter.NewLifecycleService(ntpService, "ntp service"))
|
||||
}
|
||||
return &Box{
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
network: networkManager,
|
||||
endpoint: endpointManager,
|
||||
inbound: inboundManager,
|
||||
outbound: outboundManager,
|
||||
dnsTransport: dnsTransportManager,
|
||||
service: serviceManager,
|
||||
certificateProvider: certificateProviderManager,
|
||||
dnsRouter: dnsRouter,
|
||||
connection: connectionManager,
|
||||
router: router,
|
||||
createdAt: createdAt,
|
||||
logFactory: logFactory,
|
||||
logger: logFactory.Logger(),
|
||||
internalService: internalServices,
|
||||
done: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -450,7 +482,7 @@ func (s *Box) preStart() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service, s.certificateProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -470,11 +502,19 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.certificateProvider)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.endpoint, s.certificateProvider, s.inbound, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -482,7 +522,7 @@ func (s *Box) start() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.endpoint, s.certificateProvider, s.inbound, s.service)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -506,8 +546,9 @@ func (s *Box) Close() error {
|
||||
service adapter.Lifecycle
|
||||
}{
|
||||
{"service", s.service},
|
||||
{"endpoint", s.endpoint},
|
||||
{"inbound", s.inbound},
|
||||
{"certificate-provider", s.certificateProvider},
|
||||
{"endpoint", s.endpoint},
|
||||
{"outbound", s.outbound},
|
||||
{"router", s.router},
|
||||
{"connection", s.connection},
|
||||
|
||||
Submodule clients/android updated: 7777469b5d...cba1cc3ce0
Submodule clients/apple updated: c19945f65b...ffbf405b52
@@ -239,7 +239,7 @@ func setMarkWrapper(networkManager adapter.NetworkManager, mark uint32, isDefaul
|
||||
func (d *DefaultDialer) DialContext(ctx context.Context, network string, address M.Socksaddr) (net.Conn, error) {
|
||||
if !address.IsValid() {
|
||||
return nil, E.New("invalid address")
|
||||
} else if address.IsFqdn() {
|
||||
} else if address.IsDomain() {
|
||||
return nil, E.New("domain not resolved")
|
||||
}
|
||||
if d.networkStrategy == nil {
|
||||
@@ -329,9 +329,9 @@ 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
|
||||
} else {
|
||||
return d.dialer6.Dialer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ func (d *resolveDialer) DialContext(ctx context.Context, network string, destina
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
if !destination.IsDomain() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
@@ -116,7 +116,7 @@ func (d *resolveDialer) ListenPacket(ctx context.Context, destination M.Socksadd
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
if !destination.IsDomain() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
@@ -144,7 +144,7 @@ func (d *resolveParallelNetworkDialer) DialParallelInterface(ctx context.Context
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
if !destination.IsDomain() {
|
||||
return d.dialer.DialContext(ctx, network, destination)
|
||||
}
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
@@ -167,7 +167,7 @@ func (d *resolveParallelNetworkDialer) ListenSerialInterfacePacket(ctx context.C
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !destination.IsFqdn() {
|
||||
if !destination.IsDomain() {
|
||||
return d.dialer.ListenPacket(ctx, destination)
|
||||
}
|
||||
ctx = log.ContextWithOverrideLevel(ctx, log.LevelDebug)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func (c *Conn) Read(b []byte) (int, error) {
|
||||
@@ -229,7 +230,7 @@ func (c *Conn) readRawRecord() (typ uint8, data []byte, err error) {
|
||||
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))))
|
||||
err = c.rawConn.In.SetErrorLocked(c.sendAlert(*(*uint8)((*[2]unsafe.Pointer)(unsafe.Pointer(&err))[1])))
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
type Searcher interface {
|
||||
FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
var ErrNotFound = E.New("process not found")
|
||||
@@ -28,7 +29,7 @@ func FindProcessInfo(searcher Searcher, ctx context.Context, network string, sou
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if info.UserId != -1 {
|
||||
if info.UserId != -1 && info.UserName == "" {
|
||||
osUser, _ := user.LookupId(F.ToString(info.UserId))
|
||||
if osUser != nil {
|
||||
info.UserName = osUser.Username
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-tun"
|
||||
"github.com/sagernet/sing/common"
|
||||
)
|
||||
|
||||
var _ Searcher = (*androidSearcher)(nil)
|
||||
@@ -18,22 +19,30 @@ func NewSearcher(config Config) (Searcher, error) {
|
||||
return &androidSearcher{config.PackageManager}, nil
|
||||
}
|
||||
|
||||
func (s *androidSearcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *androidSearcher) FindProcessInfo(ctx context.Context, network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
_, uid, err := resolveSocketByNetlink(network, source, destination)
|
||||
family, protocol, err := socketDiagSettings(network, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(uid % 100000); loaded {
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
AndroidPackageName: sharedPackage,
|
||||
}, nil
|
||||
_, uid, err := querySocketDiagOnce(family, protocol, source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if packageName, loaded := s.packageManager.PackageByID(uid % 100000); loaded {
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
AndroidPackageName: packageName,
|
||||
}, nil
|
||||
appID := uid % 100000
|
||||
var packageNames []string
|
||||
if sharedPackage, loaded := s.packageManager.SharedPackageByID(appID); loaded {
|
||||
packageNames = append(packageNames, sharedPackage)
|
||||
}
|
||||
return &adapter.ConnectionOwner{UserId: int32(uid)}, nil
|
||||
if packages, loaded := s.packageManager.PackagesByID(appID); loaded {
|
||||
packageNames = append(packageNames, packages...)
|
||||
}
|
||||
packageNames = common.Uniq(packageNames)
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
AndroidPackageNames: packageNames,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
//go:build darwin
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var _ Searcher = (*darwinSearcher)(nil)
|
||||
@@ -24,12 +20,12 @@ func NewSearcher(_ Config) (Searcher, error) {
|
||||
return &darwinSearcher{}, nil
|
||||
}
|
||||
|
||||
func (d *darwinSearcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 &adapter.ConnectionOwner{ProcessPath: processName, UserId: -1}, nil
|
||||
return FindDarwinConnectionOwner(network, source, destination)
|
||||
}
|
||||
|
||||
var structSize = func() int {
|
||||
@@ -47,107 +43,3 @@ var structSize = func() int {
|
||||
return 384
|
||||
}
|
||||
}()
|
||||
|
||||
func findProcessName(network string, ip netip.Addr, port int) (string, error) {
|
||||
var spath string
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
spath = "net.inet.tcp.pcblist_n"
|
||||
case N.NetworkUDP:
|
||||
spath = "net.inet.udp.pcblist_n"
|
||||
default:
|
||||
return "", os.ErrInvalid
|
||||
}
|
||||
|
||||
isIPv4 := ip.Is4()
|
||||
|
||||
value, err := unix.SysctlRaw(spath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := value
|
||||
|
||||
// from darwin-xnu/bsd/netinet/in_pcblist.c:get_pcblist_n
|
||||
// size/offset are round up (aligned) to 8 bytes in darwin
|
||||
// rup8(sizeof(xinpcb_n)) + rup8(sizeof(xsocket_n)) +
|
||||
// 2 * rup8(sizeof(xsockbuf_n)) + rup8(sizeof(xsockstat_n))
|
||||
itemSize := structSize
|
||||
if network == N.NetworkTCP {
|
||||
// rup8(sizeof(xtcpcb_n))
|
||||
itemSize += 208
|
||||
}
|
||||
|
||||
var fallbackUDPProcess string
|
||||
// skip the first xinpgen(24 bytes) block
|
||||
for i := 24; i+itemSize <= len(buf); i += itemSize {
|
||||
// offset of xinpcb_n and xsocket_n
|
||||
inp, so := i, i+104
|
||||
|
||||
srcPort := binary.BigEndian.Uint16(buf[inp+18 : inp+20])
|
||||
if uint16(port) != srcPort {
|
||||
continue
|
||||
}
|
||||
|
||||
// xinpcb_n.inp_vflag
|
||||
flag := buf[inp+44]
|
||||
|
||||
var srcIP netip.Addr
|
||||
srcIsIPv4 := false
|
||||
switch {
|
||||
case flag&0x1 > 0 && isIPv4:
|
||||
// ipv4
|
||||
srcIP = netip.AddrFrom4([4]byte(buf[inp+76 : inp+80]))
|
||||
srcIsIPv4 = true
|
||||
case flag&0x2 > 0 && !isIPv4:
|
||||
// ipv6
|
||||
srcIP = netip.AddrFrom16([16]byte(buf[inp+64 : inp+80]))
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
if ip == srcIP {
|
||||
// xsocket_n.so_last_pid
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
return getExecPathFromPID(pid)
|
||||
}
|
||||
|
||||
// udp packet connection may be not equal with srcIP
|
||||
if network == N.NetworkUDP && srcIP.IsUnspecified() && isIPv4 == srcIsIPv4 {
|
||||
pid := readNativeUint32(buf[so+68 : so+72])
|
||||
fallbackUDPProcess, _ = getExecPathFromPID(pid)
|
||||
}
|
||||
}
|
||||
|
||||
if network == N.NetworkUDP && len(fallbackUDPProcess) > 0 {
|
||||
return fallbackUDPProcess, nil
|
||||
}
|
||||
|
||||
return "", ErrNotFound
|
||||
}
|
||||
|
||||
func getExecPathFromPID(pid uint32) (string, error) {
|
||||
const (
|
||||
procpidpathinfo = 0xb
|
||||
procpidpathinfosize = 1024
|
||||
proccallnumpidinfo = 0x2
|
||||
)
|
||||
buf := make([]byte, procpidpathinfosize)
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PROC_INFO,
|
||||
proccallnumpidinfo,
|
||||
uintptr(pid),
|
||||
procpidpathinfo,
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
procpidpathinfosize)
|
||||
if errno != 0 {
|
||||
return "", errno
|
||||
}
|
||||
|
||||
return unix.ByteSliceToString(buf), nil
|
||||
}
|
||||
|
||||
func readNativeUint32(b []byte) uint32 {
|
||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||
}
|
||||
|
||||
269
common/process/searcher_darwin_shared.go
Normal file
269
common/process/searcher_darwin_shared.go
Normal file
@@ -0,0 +1,269 @@
|
||||
//go:build darwin
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net/netip"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const (
|
||||
darwinSnapshotTTL = 200 * time.Millisecond
|
||||
|
||||
darwinXinpgenSize = 24
|
||||
darwinXsocketOffset = 104
|
||||
darwinXinpcbForeignPort = 16
|
||||
darwinXinpcbLocalPort = 18
|
||||
darwinXinpcbVFlag = 44
|
||||
darwinXinpcbForeignAddr = 48
|
||||
darwinXinpcbLocalAddr = 64
|
||||
darwinXinpcbIPv4Addr = 12
|
||||
darwinXsocketUID = 64
|
||||
darwinXsocketLastPID = 68
|
||||
darwinTCPExtraStructSize = 208
|
||||
)
|
||||
|
||||
type darwinConnectionEntry struct {
|
||||
localAddr netip.Addr
|
||||
remoteAddr netip.Addr
|
||||
localPort uint16
|
||||
remotePort uint16
|
||||
pid uint32
|
||||
uid int32
|
||||
}
|
||||
|
||||
type darwinConnectionMatchKind uint8
|
||||
|
||||
const (
|
||||
darwinConnectionMatchExact darwinConnectionMatchKind = iota
|
||||
darwinConnectionMatchLocalFallback
|
||||
darwinConnectionMatchWildcardFallback
|
||||
)
|
||||
|
||||
type darwinSnapshot struct {
|
||||
createdAt time.Time
|
||||
entries []darwinConnectionEntry
|
||||
}
|
||||
|
||||
type darwinConnectionFinder struct {
|
||||
access sync.Mutex
|
||||
ttl time.Duration
|
||||
snapshots map[string]darwinSnapshot
|
||||
builder func(string) (darwinSnapshot, error)
|
||||
}
|
||||
|
||||
var sharedDarwinConnectionFinder = newDarwinConnectionFinder(darwinSnapshotTTL)
|
||||
|
||||
func newDarwinConnectionFinder(ttl time.Duration) *darwinConnectionFinder {
|
||||
return &darwinConnectionFinder{
|
||||
ttl: ttl,
|
||||
snapshots: make(map[string]darwinSnapshot),
|
||||
builder: buildDarwinSnapshot,
|
||||
}
|
||||
}
|
||||
|
||||
func FindDarwinConnectionOwner(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
return sharedDarwinConnectionFinder.find(network, source, destination)
|
||||
}
|
||||
|
||||
func (f *darwinConnectionFinder) find(network string, source netip.AddrPort, destination netip.AddrPort) (*adapter.ConnectionOwner, error) {
|
||||
networkName := N.NetworkName(network)
|
||||
source = normalizeDarwinAddrPort(source)
|
||||
destination = normalizeDarwinAddrPort(destination)
|
||||
var lastOwner *adapter.ConnectionOwner
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
snapshot, fromCache, err := f.loadSnapshot(networkName, attempt > 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry, matchKind, err := matchDarwinConnectionEntry(snapshot.entries, networkName, source, destination)
|
||||
if err != nil {
|
||||
if err == ErrNotFound && fromCache {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if fromCache && matchKind != darwinConnectionMatchExact {
|
||||
continue
|
||||
}
|
||||
owner := &adapter.ConnectionOwner{
|
||||
UserId: entry.uid,
|
||||
}
|
||||
lastOwner = owner
|
||||
if entry.pid == 0 {
|
||||
return owner, nil
|
||||
}
|
||||
processPath, err := getExecPathFromPID(entry.pid)
|
||||
if err == nil {
|
||||
owner.ProcessPath = processPath
|
||||
return owner, nil
|
||||
}
|
||||
if fromCache {
|
||||
continue
|
||||
}
|
||||
return owner, nil
|
||||
}
|
||||
if lastOwner != nil {
|
||||
return lastOwner, nil
|
||||
}
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
func (f *darwinConnectionFinder) loadSnapshot(network string, forceRefresh bool) (darwinSnapshot, bool, error) {
|
||||
f.access.Lock()
|
||||
defer f.access.Unlock()
|
||||
if !forceRefresh {
|
||||
if snapshot, loaded := f.snapshots[network]; loaded && time.Since(snapshot.createdAt) < f.ttl {
|
||||
return snapshot, true, nil
|
||||
}
|
||||
}
|
||||
snapshot, err := f.builder(network)
|
||||
if err != nil {
|
||||
return darwinSnapshot{}, false, err
|
||||
}
|
||||
f.snapshots[network] = snapshot
|
||||
return snapshot, false, nil
|
||||
}
|
||||
|
||||
func buildDarwinSnapshot(network string) (darwinSnapshot, error) {
|
||||
spath, itemSize, err := darwinSnapshotSettings(network)
|
||||
if err != nil {
|
||||
return darwinSnapshot{}, err
|
||||
}
|
||||
value, err := unix.SysctlRaw(spath)
|
||||
if err != nil {
|
||||
return darwinSnapshot{}, err
|
||||
}
|
||||
return darwinSnapshot{
|
||||
createdAt: time.Now(),
|
||||
entries: parseDarwinSnapshot(value, itemSize),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func darwinSnapshotSettings(network string) (string, int, error) {
|
||||
itemSize := structSize
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
return "net.inet.tcp.pcblist_n", itemSize + darwinTCPExtraStructSize, nil
|
||||
case N.NetworkUDP:
|
||||
return "net.inet.udp.pcblist_n", itemSize, nil
|
||||
default:
|
||||
return "", 0, os.ErrInvalid
|
||||
}
|
||||
}
|
||||
|
||||
func parseDarwinSnapshot(buf []byte, itemSize int) []darwinConnectionEntry {
|
||||
entries := make([]darwinConnectionEntry, 0, (len(buf)-darwinXinpgenSize)/itemSize)
|
||||
for i := darwinXinpgenSize; i+itemSize <= len(buf); i += itemSize {
|
||||
inp := i
|
||||
so := i + darwinXsocketOffset
|
||||
entry, ok := parseDarwinConnectionEntry(buf[inp:so], buf[so:so+structSize-darwinXsocketOffset])
|
||||
if ok {
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func parseDarwinConnectionEntry(inp []byte, so []byte) (darwinConnectionEntry, bool) {
|
||||
if len(inp) < darwinXsocketOffset || len(so) < structSize-darwinXsocketOffset {
|
||||
return darwinConnectionEntry{}, false
|
||||
}
|
||||
entry := darwinConnectionEntry{
|
||||
remotePort: binary.BigEndian.Uint16(inp[darwinXinpcbForeignPort : darwinXinpcbForeignPort+2]),
|
||||
localPort: binary.BigEndian.Uint16(inp[darwinXinpcbLocalPort : darwinXinpcbLocalPort+2]),
|
||||
pid: binary.NativeEndian.Uint32(so[darwinXsocketLastPID : darwinXsocketLastPID+4]),
|
||||
uid: int32(binary.NativeEndian.Uint32(so[darwinXsocketUID : darwinXsocketUID+4])),
|
||||
}
|
||||
flag := inp[darwinXinpcbVFlag]
|
||||
switch {
|
||||
case flag&0x1 != 0:
|
||||
entry.remoteAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr : darwinXinpcbForeignAddr+darwinXinpcbIPv4Addr+4]))
|
||||
entry.localAddr = netip.AddrFrom4([4]byte(inp[darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr : darwinXinpcbLocalAddr+darwinXinpcbIPv4Addr+4]))
|
||||
return entry, true
|
||||
case flag&0x2 != 0:
|
||||
entry.remoteAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbForeignAddr : darwinXinpcbForeignAddr+16]))
|
||||
entry.localAddr = netip.AddrFrom16([16]byte(inp[darwinXinpcbLocalAddr : darwinXinpcbLocalAddr+16]))
|
||||
return entry, true
|
||||
default:
|
||||
return darwinConnectionEntry{}, false
|
||||
}
|
||||
}
|
||||
|
||||
func matchDarwinConnectionEntry(entries []darwinConnectionEntry, network string, source netip.AddrPort, destination netip.AddrPort) (darwinConnectionEntry, darwinConnectionMatchKind, error) {
|
||||
sourceAddr := source.Addr()
|
||||
if !sourceAddr.IsValid() {
|
||||
return darwinConnectionEntry{}, darwinConnectionMatchExact, os.ErrInvalid
|
||||
}
|
||||
var localFallback darwinConnectionEntry
|
||||
var hasLocalFallback bool
|
||||
var wildcardFallback darwinConnectionEntry
|
||||
var hasWildcardFallback bool
|
||||
for _, entry := range entries {
|
||||
if entry.localPort != source.Port() || sourceAddr.BitLen() != entry.localAddr.BitLen() {
|
||||
continue
|
||||
}
|
||||
if entry.localAddr == sourceAddr && destination.IsValid() && entry.remotePort == destination.Port() && entry.remoteAddr == destination.Addr() {
|
||||
return entry, darwinConnectionMatchExact, nil
|
||||
}
|
||||
if !destination.IsValid() && entry.localAddr == sourceAddr {
|
||||
return entry, darwinConnectionMatchExact, nil
|
||||
}
|
||||
if network != N.NetworkUDP {
|
||||
continue
|
||||
}
|
||||
if !hasLocalFallback && entry.localAddr == sourceAddr {
|
||||
hasLocalFallback = true
|
||||
localFallback = entry
|
||||
}
|
||||
if !hasWildcardFallback && entry.localAddr.IsUnspecified() {
|
||||
hasWildcardFallback = true
|
||||
wildcardFallback = entry
|
||||
}
|
||||
}
|
||||
if hasLocalFallback {
|
||||
return localFallback, darwinConnectionMatchLocalFallback, nil
|
||||
}
|
||||
if hasWildcardFallback {
|
||||
return wildcardFallback, darwinConnectionMatchWildcardFallback, nil
|
||||
}
|
||||
return darwinConnectionEntry{}, darwinConnectionMatchExact, ErrNotFound
|
||||
}
|
||||
|
||||
func normalizeDarwinAddrPort(addrPort netip.AddrPort) netip.AddrPort {
|
||||
if !addrPort.IsValid() {
|
||||
return addrPort
|
||||
}
|
||||
return netip.AddrPortFrom(addrPort.Addr().Unmap(), addrPort.Port())
|
||||
}
|
||||
|
||||
func getExecPathFromPID(pid uint32) (string, error) {
|
||||
const (
|
||||
procpidpathinfo = 0xb
|
||||
procpidpathinfosize = 1024
|
||||
proccallnumpidinfo = 0x2
|
||||
)
|
||||
buf := make([]byte, procpidpathinfosize)
|
||||
_, _, errno := syscall.Syscall6(
|
||||
syscall.SYS_PROC_INFO,
|
||||
proccallnumpidinfo,
|
||||
uintptr(pid),
|
||||
procpidpathinfo,
|
||||
0,
|
||||
uintptr(unsafe.Pointer(&buf[0])),
|
||||
procpidpathinfosize)
|
||||
if errno != 0 {
|
||||
return "", errno
|
||||
}
|
||||
return unix.ByteSliceToString(buf), nil
|
||||
}
|
||||
@@ -4,33 +4,82 @@ package process
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
var _ Searcher = (*linuxSearcher)(nil)
|
||||
|
||||
type linuxSearcher struct {
|
||||
logger log.ContextLogger
|
||||
logger log.ContextLogger
|
||||
diagConns [4]*socketDiagConn
|
||||
processPathCache *uidProcessPathCache
|
||||
}
|
||||
|
||||
func NewSearcher(config Config) (Searcher, error) {
|
||||
return &linuxSearcher{config.Logger}, nil
|
||||
searcher := &linuxSearcher{
|
||||
logger: config.Logger,
|
||||
processPathCache: newUIDProcessPathCache(time.Second),
|
||||
}
|
||||
for _, family := range []uint8{syscall.AF_INET, syscall.AF_INET6} {
|
||||
for _, protocol := range []uint8{syscall.IPPROTO_TCP, syscall.IPPROTO_UDP} {
|
||||
searcher.diagConns[socketDiagConnIndex(family, protocol)] = newSocketDiagConn(family, protocol)
|
||||
}
|
||||
}
|
||||
return searcher, nil
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) Close() error {
|
||||
var errs []error
|
||||
for _, conn := range s.diagConns {
|
||||
if conn == nil {
|
||||
continue
|
||||
}
|
||||
errs = append(errs, conn.Close())
|
||||
}
|
||||
return E.Errors(errs...)
|
||||
}
|
||||
|
||||
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)
|
||||
inode, uid, err := s.resolveSocketByNetlink(network, source, destination)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
processPath, err := resolveProcessNameByProcSearch(inode, uid)
|
||||
processInfo := &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
}
|
||||
processPath, err := s.processPathCache.findProcessPath(inode, uid)
|
||||
if err != nil {
|
||||
s.logger.DebugContext(ctx, "find process path: ", err)
|
||||
} else {
|
||||
processInfo.ProcessPath = processPath
|
||||
}
|
||||
return &adapter.ConnectionOwner{
|
||||
UserId: int32(uid),
|
||||
ProcessPath: processPath,
|
||||
}, nil
|
||||
return processInfo, nil
|
||||
}
|
||||
|
||||
func (s *linuxSearcher) resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
family, protocol, err := socketDiagSettings(network, source)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
conn := s.diagConns[socketDiagConnIndex(family, protocol)]
|
||||
if conn == nil {
|
||||
return 0, 0, E.New("missing socket diag connection for family=", family, " protocol=", protocol)
|
||||
}
|
||||
if destination.IsValid() && source.Addr().BitLen() == destination.Addr().BitLen() {
|
||||
inode, uid, err = conn.query(source, destination)
|
||||
if err == nil {
|
||||
return inode, uid, nil
|
||||
}
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
return 0, 0, err
|
||||
}
|
||||
}
|
||||
return querySocketDiagOnce(family, protocol, source)
|
||||
}
|
||||
|
||||
@@ -3,43 +3,67 @@
|
||||
package process
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"errors"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/sing/contrab/freelru"
|
||||
"github.com/sagernet/sing/contrab/maphash"
|
||||
)
|
||||
|
||||
// from https://github.com/vishvananda/netlink/blob/bca67dfc8220b44ef582c9da4e9172bf1c9ec973/nl/nl_linux.go#L52-L62
|
||||
var nativeEndian = func() binary.ByteOrder {
|
||||
var x uint32 = 0x01020304
|
||||
if *(*byte)(unsafe.Pointer(&x)) == 0x01 {
|
||||
return binary.BigEndian
|
||||
}
|
||||
|
||||
return binary.LittleEndian
|
||||
}()
|
||||
|
||||
const (
|
||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + 8 + 48
|
||||
socketDiagByFamily = 20
|
||||
pathProc = "/proc"
|
||||
sizeOfSocketDiagRequestData = 56
|
||||
sizeOfSocketDiagRequest = syscall.SizeofNlMsghdr + sizeOfSocketDiagRequestData
|
||||
socketDiagResponseMinSize = 72
|
||||
socketDiagByFamily = 20
|
||||
pathProc = "/proc"
|
||||
)
|
||||
|
||||
func resolveSocketByNetlink(network string, source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
var family uint8
|
||||
var protocol uint8
|
||||
type socketDiagConn struct {
|
||||
access sync.Mutex
|
||||
family uint8
|
||||
protocol uint8
|
||||
fd int
|
||||
}
|
||||
|
||||
type uidProcessPathCache struct {
|
||||
cache freelru.Cache[uint32, *uidProcessPaths]
|
||||
}
|
||||
|
||||
type uidProcessPaths struct {
|
||||
entries map[uint32]string
|
||||
}
|
||||
|
||||
func newSocketDiagConn(family, protocol uint8) *socketDiagConn {
|
||||
return &socketDiagConn{
|
||||
family: family,
|
||||
protocol: protocol,
|
||||
fd: -1,
|
||||
}
|
||||
}
|
||||
|
||||
func socketDiagConnIndex(family, protocol uint8) int {
|
||||
index := 0
|
||||
if protocol == syscall.IPPROTO_UDP {
|
||||
index += 2
|
||||
}
|
||||
if family == syscall.AF_INET6 {
|
||||
index++
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
func socketDiagSettings(network string, source netip.AddrPort) (family, protocol uint8, err error) {
|
||||
switch network {
|
||||
case N.NetworkTCP:
|
||||
protocol = syscall.IPPROTO_TCP
|
||||
@@ -48,151 +72,308 @@ func resolveSocketByNetlink(network string, source netip.AddrPort, destination n
|
||||
default:
|
||||
return 0, 0, os.ErrInvalid
|
||||
}
|
||||
|
||||
if source.Addr().Is4() {
|
||||
switch {
|
||||
case source.Addr().Is4():
|
||||
family = syscall.AF_INET
|
||||
} else {
|
||||
case source.Addr().Is6():
|
||||
family = syscall.AF_INET6
|
||||
default:
|
||||
return 0, 0, os.ErrInvalid
|
||||
}
|
||||
|
||||
req := packSocketDiagRequest(family, protocol, source)
|
||||
|
||||
socket, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM, syscall.NETLINK_INET_DIAG)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "dial netlink")
|
||||
}
|
||||
defer syscall.Close(socket)
|
||||
|
||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, &syscall.Timeval{Usec: 100})
|
||||
syscall.SetsockoptTimeval(socket, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, &syscall.Timeval{Usec: 100})
|
||||
|
||||
err = syscall.Connect(socket, &syscall.SockaddrNetlink{
|
||||
Family: syscall.AF_NETLINK,
|
||||
Pad: 0,
|
||||
Pid: 0,
|
||||
Groups: 0,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = syscall.Write(socket, req)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "write netlink request")
|
||||
}
|
||||
|
||||
buffer := buf.New()
|
||||
defer buffer.Release()
|
||||
|
||||
n, err := syscall.Read(socket, buffer.FreeBytes())
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "read netlink response")
|
||||
}
|
||||
|
||||
buffer.Truncate(n)
|
||||
|
||||
messages, err := syscall.ParseNetlinkMessage(buffer.Bytes())
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "parse netlink message")
|
||||
} else if len(messages) == 0 {
|
||||
return 0, 0, E.New("unexcepted netlink response")
|
||||
}
|
||||
|
||||
message := messages[0]
|
||||
if message.Header.Type&syscall.NLMSG_ERROR != 0 {
|
||||
return 0, 0, E.New("netlink message: NLMSG_ERROR")
|
||||
}
|
||||
|
||||
inode, uid = unpackSocketDiagResponse(&messages[0])
|
||||
return
|
||||
return family, protocol, nil
|
||||
}
|
||||
|
||||
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort) []byte {
|
||||
s := make([]byte, 16)
|
||||
copy(s, source.Addr().AsSlice())
|
||||
|
||||
buf := make([]byte, sizeOfSocketDiagRequest)
|
||||
|
||||
nativeEndian.PutUint32(buf[0:4], sizeOfSocketDiagRequest)
|
||||
nativeEndian.PutUint16(buf[4:6], socketDiagByFamily)
|
||||
nativeEndian.PutUint16(buf[6:8], syscall.NLM_F_REQUEST|syscall.NLM_F_DUMP)
|
||||
nativeEndian.PutUint32(buf[8:12], 0)
|
||||
nativeEndian.PutUint32(buf[12:16], 0)
|
||||
|
||||
buf[16] = family
|
||||
buf[17] = protocol
|
||||
buf[18] = 0
|
||||
buf[19] = 0
|
||||
nativeEndian.PutUint32(buf[20:24], 0xFFFFFFFF)
|
||||
|
||||
binary.BigEndian.PutUint16(buf[24:26], source.Port())
|
||||
binary.BigEndian.PutUint16(buf[26:28], 0)
|
||||
|
||||
copy(buf[28:44], s)
|
||||
copy(buf[44:60], net.IPv6zero)
|
||||
|
||||
nativeEndian.PutUint32(buf[60:64], 0)
|
||||
nativeEndian.PutUint64(buf[64:72], 0xFFFFFFFFFFFFFFFF)
|
||||
|
||||
return buf
|
||||
func newUIDProcessPathCache(ttl time.Duration) *uidProcessPathCache {
|
||||
cache := common.Must1(freelru.NewSharded[uint32, *uidProcessPaths](64, maphash.NewHasher[uint32]().Hash32))
|
||||
cache.SetLifetime(ttl)
|
||||
return &uidProcessPathCache{cache: cache}
|
||||
}
|
||||
|
||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||
if len(msg.Data) < 72 {
|
||||
return 0, 0
|
||||
func (c *uidProcessPathCache) findProcessPath(targetInode, uid uint32) (string, error) {
|
||||
if cached, ok := c.cache.Get(uid); ok {
|
||||
if processPath, found := cached.entries[targetInode]; found {
|
||||
return processPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
data := msg.Data
|
||||
|
||||
uid = nativeEndian.Uint32(data[64:68])
|
||||
inode = nativeEndian.Uint32(data[68:72])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func resolveProcessNameByProcSearch(inode, uid uint32) (string, error) {
|
||||
files, err := os.ReadDir(pathProc)
|
||||
processPaths, err := buildProcessPathByUIDCache(uid)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c.cache.Add(uid, &uidProcessPaths{entries: processPaths})
|
||||
processPath, found := processPaths[targetInode]
|
||||
if !found {
|
||||
return "", E.New("process of uid(", uid, "), inode(", targetInode, ") not found")
|
||||
}
|
||||
return processPath, nil
|
||||
}
|
||||
|
||||
func (c *socketDiagConn) Close() error {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
return c.closeLocked()
|
||||
}
|
||||
|
||||
func (c *socketDiagConn) query(source netip.AddrPort, destination netip.AddrPort) (inode, uid uint32, err error) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
request := packSocketDiagRequest(c.family, c.protocol, source, destination, false)
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
err = c.ensureOpenLocked()
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "dial netlink")
|
||||
}
|
||||
inode, uid, err = querySocketDiag(c.fd, request)
|
||||
if err == nil || errors.Is(err, ErrNotFound) {
|
||||
return inode, uid, err
|
||||
}
|
||||
if !shouldRetrySocketDiag(err) {
|
||||
return 0, 0, err
|
||||
}
|
||||
_ = c.closeLocked()
|
||||
}
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
func querySocketDiagOnce(family, protocol uint8, source netip.AddrPort) (inode, uid uint32, err error) {
|
||||
fd, err := openSocketDiag()
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "dial netlink")
|
||||
}
|
||||
defer syscall.Close(fd)
|
||||
return querySocketDiag(fd, packSocketDiagRequest(family, protocol, source, netip.AddrPort{}, true))
|
||||
}
|
||||
|
||||
func (c *socketDiagConn) ensureOpenLocked() error {
|
||||
if c.fd != -1 {
|
||||
return nil
|
||||
}
|
||||
fd, err := openSocketDiag()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.fd = fd
|
||||
return nil
|
||||
}
|
||||
|
||||
func openSocketDiag() (int, error) {
|
||||
fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_DGRAM|syscall.SOCK_CLOEXEC, syscall.NETLINK_INET_DIAG)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
timeout := &syscall.Timeval{Usec: 100}
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_SNDTIMEO, timeout); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
if err = syscall.SetsockoptTimeval(fd, syscall.SOL_SOCKET, syscall.SO_RCVTIMEO, timeout); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
if err = syscall.Connect(fd, &syscall.SockaddrNetlink{
|
||||
Family: syscall.AF_NETLINK,
|
||||
Pid: 0,
|
||||
Groups: 0,
|
||||
}); err != nil {
|
||||
syscall.Close(fd)
|
||||
return -1, err
|
||||
}
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
func (c *socketDiagConn) closeLocked() error {
|
||||
if c.fd == -1 {
|
||||
return nil
|
||||
}
|
||||
err := syscall.Close(c.fd)
|
||||
c.fd = -1
|
||||
return err
|
||||
}
|
||||
|
||||
func packSocketDiagRequest(family, protocol byte, source netip.AddrPort, destination netip.AddrPort, dump bool) []byte {
|
||||
request := make([]byte, sizeOfSocketDiagRequest)
|
||||
|
||||
binary.NativeEndian.PutUint32(request[0:4], sizeOfSocketDiagRequest)
|
||||
binary.NativeEndian.PutUint16(request[4:6], socketDiagByFamily)
|
||||
flags := uint16(syscall.NLM_F_REQUEST)
|
||||
if dump {
|
||||
flags |= syscall.NLM_F_DUMP
|
||||
}
|
||||
binary.NativeEndian.PutUint16(request[6:8], flags)
|
||||
binary.NativeEndian.PutUint32(request[8:12], 0)
|
||||
binary.NativeEndian.PutUint32(request[12:16], 0)
|
||||
|
||||
request[16] = family
|
||||
request[17] = protocol
|
||||
request[18] = 0
|
||||
request[19] = 0
|
||||
if dump {
|
||||
binary.NativeEndian.PutUint32(request[20:24], 0xFFFFFFFF)
|
||||
}
|
||||
requestSource := source
|
||||
requestDestination := destination
|
||||
if protocol == syscall.IPPROTO_UDP && !dump && destination.IsValid() {
|
||||
// udp_dump_one expects the exact-match endpoints reversed for historical reasons.
|
||||
requestSource, requestDestination = destination, source
|
||||
}
|
||||
binary.BigEndian.PutUint16(request[24:26], requestSource.Port())
|
||||
binary.BigEndian.PutUint16(request[26:28], requestDestination.Port())
|
||||
if family == syscall.AF_INET6 {
|
||||
copy(request[28:44], requestSource.Addr().AsSlice())
|
||||
if requestDestination.IsValid() {
|
||||
copy(request[44:60], requestDestination.Addr().AsSlice())
|
||||
}
|
||||
} else {
|
||||
copy(request[28:32], requestSource.Addr().AsSlice())
|
||||
if requestDestination.IsValid() {
|
||||
copy(request[44:48], requestDestination.Addr().AsSlice())
|
||||
}
|
||||
}
|
||||
binary.NativeEndian.PutUint32(request[60:64], 0)
|
||||
binary.NativeEndian.PutUint64(request[64:72], 0xFFFFFFFFFFFFFFFF)
|
||||
return request
|
||||
}
|
||||
|
||||
func querySocketDiag(fd int, request []byte) (inode, uid uint32, err error) {
|
||||
_, err = syscall.Write(fd, request)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "write netlink request")
|
||||
}
|
||||
buffer := make([]byte, 64<<10)
|
||||
n, err := syscall.Read(fd, buffer)
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "read netlink response")
|
||||
}
|
||||
messages, err := syscall.ParseNetlinkMessage(buffer[:n])
|
||||
if err != nil {
|
||||
return 0, 0, E.Cause(err, "parse netlink message")
|
||||
}
|
||||
return unpackSocketDiagMessages(messages)
|
||||
}
|
||||
|
||||
func unpackSocketDiagMessages(messages []syscall.NetlinkMessage) (inode, uid uint32, err error) {
|
||||
for _, message := range messages {
|
||||
switch message.Header.Type {
|
||||
case syscall.NLMSG_DONE:
|
||||
continue
|
||||
case syscall.NLMSG_ERROR:
|
||||
err = unpackSocketDiagError(&message)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
case socketDiagByFamily:
|
||||
inode, uid = unpackSocketDiagResponse(&message)
|
||||
if inode != 0 || uid != 0 {
|
||||
return inode, uid, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0, 0, ErrNotFound
|
||||
}
|
||||
|
||||
func unpackSocketDiagResponse(msg *syscall.NetlinkMessage) (inode, uid uint32) {
|
||||
if len(msg.Data) < socketDiagResponseMinSize {
|
||||
return 0, 0
|
||||
}
|
||||
uid = binary.NativeEndian.Uint32(msg.Data[64:68])
|
||||
inode = binary.NativeEndian.Uint32(msg.Data[68:72])
|
||||
return inode, uid
|
||||
}
|
||||
|
||||
func unpackSocketDiagError(msg *syscall.NetlinkMessage) error {
|
||||
if len(msg.Data) < 4 {
|
||||
return E.New("netlink message: NLMSG_ERROR")
|
||||
}
|
||||
errno := int32(binary.NativeEndian.Uint32(msg.Data[:4]))
|
||||
if errno == 0 {
|
||||
return nil
|
||||
}
|
||||
if errno < 0 {
|
||||
errno = -errno
|
||||
}
|
||||
sysErr := syscall.Errno(errno)
|
||||
switch sysErr {
|
||||
case syscall.ENOENT, syscall.ESRCH:
|
||||
return ErrNotFound
|
||||
default:
|
||||
return E.New("netlink message: ", sysErr)
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRetrySocketDiag(err error) bool {
|
||||
return err != nil && !errors.Is(err, ErrNotFound)
|
||||
}
|
||||
|
||||
func buildProcessPathByUIDCache(uid uint32) (map[uint32]string, error) {
|
||||
files, err := os.ReadDir(pathProc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buffer := make([]byte, syscall.PathMax)
|
||||
socket := []byte(fmt.Sprintf("socket:[%d]", inode))
|
||||
|
||||
for _, f := range files {
|
||||
if !f.IsDir() || !isPid(f.Name()) {
|
||||
processPaths := make(map[uint32]string)
|
||||
for _, file := range files {
|
||||
if !file.IsDir() || !isPid(file.Name()) {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := f.Info()
|
||||
info, err := file.Info()
|
||||
if err != nil {
|
||||
return "", err
|
||||
if isIgnorableProcError(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if info.Sys().(*syscall.Stat_t).Uid != uid {
|
||||
continue
|
||||
}
|
||||
|
||||
processPath := path.Join(pathProc, f.Name())
|
||||
fdPath := path.Join(processPath, "fd")
|
||||
|
||||
processPath := filepath.Join(pathProc, file.Name())
|
||||
fdPath := filepath.Join(processPath, "fd")
|
||||
exePath, err := os.Readlink(filepath.Join(processPath, "exe"))
|
||||
if err != nil {
|
||||
if isIgnorableProcError(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
fds, err := os.ReadDir(fdPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, fd := range fds {
|
||||
n, err := syscall.Readlink(path.Join(fdPath, fd.Name()), buffer)
|
||||
n, err := syscall.Readlink(filepath.Join(fdPath, fd.Name()), buffer)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.Equal(buffer[:n], socket) {
|
||||
return os.Readlink(path.Join(processPath, "exe"))
|
||||
inode, ok := parseSocketInode(buffer[:n])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if _, loaded := processPaths[inode]; !loaded {
|
||||
processPaths[inode] = exePath
|
||||
}
|
||||
}
|
||||
}
|
||||
return processPaths, nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("process of uid(%d),inode(%d) not found", uid, inode)
|
||||
func isIgnorableProcError(err error) bool {
|
||||
return os.IsNotExist(err) || os.IsPermission(err)
|
||||
}
|
||||
|
||||
func parseSocketInode(link []byte) (uint32, bool) {
|
||||
const socketPrefix = "socket:["
|
||||
if len(link) <= len(socketPrefix) || string(link[:len(socketPrefix)]) != socketPrefix || link[len(link)-1] != ']' {
|
||||
return 0, false
|
||||
}
|
||||
var inode uint64
|
||||
for _, char := range link[len(socketPrefix) : len(link)-1] {
|
||||
if char < '0' || char > '9' {
|
||||
return 0, false
|
||||
}
|
||||
inode = inode*10 + uint64(char-'0')
|
||||
if inode > uint64(^uint32(0)) {
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
return uint32(inode), true
|
||||
}
|
||||
|
||||
func isPid(s string) bool {
|
||||
|
||||
60
common/process/searcher_linux_shared_test.go
Normal file
60
common/process/searcher_linux_shared_test.go
Normal file
@@ -0,0 +1,60 @@
|
||||
//go:build linux
|
||||
|
||||
package process
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQuerySocketDiagUDPExact(t *testing.T) {
|
||||
t.Parallel()
|
||||
server, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0})
|
||||
require.NoError(t, err)
|
||||
defer server.Close()
|
||||
|
||||
client, err := net.DialUDP("udp4", nil, server.LocalAddr().(*net.UDPAddr))
|
||||
require.NoError(t, err)
|
||||
defer client.Close()
|
||||
|
||||
err = client.SetDeadline(time.Now().Add(time.Second))
|
||||
require.NoError(t, err)
|
||||
_, err = client.Write([]byte{0})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = server.SetReadDeadline(time.Now().Add(time.Second))
|
||||
require.NoError(t, err)
|
||||
buffer := make([]byte, 1)
|
||||
_, _, err = server.ReadFromUDP(buffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
source := addrPortFromUDPAddr(t, client.LocalAddr())
|
||||
destination := addrPortFromUDPAddr(t, client.RemoteAddr())
|
||||
|
||||
fd, err := openSocketDiag()
|
||||
require.NoError(t, err)
|
||||
defer syscall.Close(fd)
|
||||
|
||||
inode, uid, err := querySocketDiag(fd, packSocketDiagRequest(syscall.AF_INET, syscall.IPPROTO_UDP, source, destination, false))
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, inode)
|
||||
require.EqualValues(t, os.Getuid(), uid)
|
||||
}
|
||||
|
||||
func addrPortFromUDPAddr(t *testing.T, addr net.Addr) netip.AddrPort {
|
||||
t.Helper()
|
||||
|
||||
udpAddr, ok := addr.(*net.UDPAddr)
|
||||
require.True(t, ok)
|
||||
|
||||
ip, ok := netip.AddrFromSlice(udpAddr.IP)
|
||||
require.True(t, ok)
|
||||
|
||||
return netip.AddrPortFrom(ip.Unmap(), uint16(udpAddr.Port))
|
||||
}
|
||||
@@ -28,6 +28,10 @@ func initWin32API() error {
|
||||
return winiphlpapi.LoadExtendedTable()
|
||||
}
|
||||
|
||||
func (s *windowsSearcher) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -38,37 +38,6 @@ func (w *acmeWrapper) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type acmeLogWriter struct {
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
func (w *acmeLogWriter) Write(p []byte) (n int, err error) {
|
||||
logLine := strings.ReplaceAll(string(p), " ", ": ")
|
||||
switch {
|
||||
case strings.HasPrefix(logLine, "error: "):
|
||||
w.logger.Error(logLine[7:])
|
||||
case strings.HasPrefix(logLine, "warn: "):
|
||||
w.logger.Warn(logLine[6:])
|
||||
case strings.HasPrefix(logLine, "info: "):
|
||||
w.logger.Info(logLine[6:])
|
||||
case strings.HasPrefix(logLine, "debug: "):
|
||||
w.logger.Debug(logLine[7:])
|
||||
default:
|
||||
w.logger.Debug(logLine)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *acmeLogWriter) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func encoderConfig() zapcore.EncoderConfig {
|
||||
config := zap.NewProductionEncoderConfig()
|
||||
config.TimeKey = zapcore.OmitKey
|
||||
return config
|
||||
}
|
||||
|
||||
func startACME(ctx context.Context, logger logger.Logger, options option.InboundACMEOptions) (*tls.Config, adapter.SimpleLifecycle, error) {
|
||||
var acmeServer string
|
||||
switch options.Provider {
|
||||
@@ -91,8 +60,8 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
||||
storage = certmagic.Default.Storage
|
||||
}
|
||||
zapLogger := zap.New(zapcore.NewCore(
|
||||
zapcore.NewConsoleEncoder(encoderConfig()),
|
||||
&acmeLogWriter{logger: logger},
|
||||
zapcore.NewConsoleEncoder(ACMEEncoderConfig()),
|
||||
&ACMELogWriter{Logger: logger},
|
||||
zap.DebugLevel,
|
||||
))
|
||||
config := &certmagic.Config{
|
||||
@@ -100,10 +69,21 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
||||
Storage: storage,
|
||||
Logger: zapLogger,
|
||||
}
|
||||
profile := options.Profile
|
||||
if profile == "" && acmeServer == certmagic.LetsEncryptProductionCA {
|
||||
for _, domain := range options.Domain {
|
||||
if certmagic.SubjectIsIP(domain) {
|
||||
profile = "shortlived"
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
acmeConfig := certmagic.ACMEIssuer{
|
||||
CA: acmeServer,
|
||||
Email: options.Email,
|
||||
Agreed: true,
|
||||
Profile: profile,
|
||||
DisableHTTPChallenge: options.DisableHTTPChallenge,
|
||||
DisableTLSALPNChallenge: options.DisableTLSALPNChallenge,
|
||||
AltHTTPPort: int(options.AlternativeHTTPPort),
|
||||
@@ -158,7 +138,7 @@ func startACME(ctx context.Context, logger logger.Logger, options option.Inbound
|
||||
} else {
|
||||
tlsConfig = &tls.Config{
|
||||
GetCertificate: config.GetCertificate,
|
||||
NextProtos: []string{ACMETLS1Protocol},
|
||||
NextProtos: []string{C.ACMETLS1Protocol},
|
||||
}
|
||||
}
|
||||
return tlsConfig, &acmeWrapper{ctx: ctx, cfg: config, cache: cache, domain: options.Domain}, nil
|
||||
|
||||
41
common/tls/acme_logger.go
Normal file
41
common/tls/acme_logger.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type ACMELogWriter struct {
|
||||
Logger logger.Logger
|
||||
}
|
||||
|
||||
func (w *ACMELogWriter) Write(p []byte) (n int, err error) {
|
||||
logLine := strings.ReplaceAll(string(p), " ", ": ")
|
||||
switch {
|
||||
case strings.HasPrefix(logLine, "error: "):
|
||||
w.Logger.Error(logLine[7:])
|
||||
case strings.HasPrefix(logLine, "warn: "):
|
||||
w.Logger.Warn(logLine[6:])
|
||||
case strings.HasPrefix(logLine, "info: "):
|
||||
w.Logger.Info(logLine[6:])
|
||||
case strings.HasPrefix(logLine, "debug: "):
|
||||
w.Logger.Debug(logLine[7:])
|
||||
default:
|
||||
w.Logger.Debug(logLine)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *ACMELogWriter) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ACMEEncoderConfig() zapcore.EncoderConfig {
|
||||
config := zap.NewProductionEncoderConfig()
|
||||
config.TimeKey = zapcore.OmitKey
|
||||
return config
|
||||
}
|
||||
@@ -32,6 +32,10 @@ type RealityServerConfig struct {
|
||||
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
var tlsConfig utls.RealityConfig
|
||||
|
||||
if options.CertificateProvider != nil {
|
||||
return nil, E.New("certificate_provider is unavailable in reality")
|
||||
}
|
||||
//nolint:staticcheck
|
||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||
return nil, E.New("acme is unavailable in reality")
|
||||
}
|
||||
|
||||
@@ -13,19 +13,87 @@ import (
|
||||
"github.com/sagernet/fswatch"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/ntp"
|
||||
"github.com/sagernet/sing/service"
|
||||
)
|
||||
|
||||
var errInsecureUnused = E.New("tls: insecure unused")
|
||||
|
||||
type managedCertificateProvider interface {
|
||||
adapter.CertificateProvider
|
||||
adapter.SimpleLifecycle
|
||||
}
|
||||
|
||||
type sharedCertificateProvider struct {
|
||||
tag string
|
||||
manager adapter.CertificateProviderManager
|
||||
provider adapter.CertificateProviderService
|
||||
}
|
||||
|
||||
func (p *sharedCertificateProvider) Start() error {
|
||||
provider, found := p.manager.Get(p.tag)
|
||||
if !found {
|
||||
return E.New("certificate provider not found: ", p.tag)
|
||||
}
|
||||
p.provider = provider
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *sharedCertificateProvider) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *sharedCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return p.provider.GetCertificate(hello)
|
||||
}
|
||||
|
||||
func (p *sharedCertificateProvider) GetACMENextProtos() []string {
|
||||
return getACMENextProtos(p.provider)
|
||||
}
|
||||
|
||||
type inlineCertificateProvider struct {
|
||||
provider adapter.CertificateProviderService
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) Start() error {
|
||||
for _, stage := range adapter.ListStartStages {
|
||||
err := adapter.LegacyStart(p.provider, stage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) Close() error {
|
||||
return p.provider.Close()
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
return p.provider.GetCertificate(hello)
|
||||
}
|
||||
|
||||
func (p *inlineCertificateProvider) GetACMENextProtos() []string {
|
||||
return getACMENextProtos(p.provider)
|
||||
}
|
||||
|
||||
func getACMENextProtos(provider adapter.CertificateProvider) []string {
|
||||
if acmeProvider, isACME := provider.(adapter.ACMECertificateProvider); isACME {
|
||||
return acmeProvider.GetACMENextProtos()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type STDServerConfig struct {
|
||||
access sync.RWMutex
|
||||
config *tls.Config
|
||||
logger log.Logger
|
||||
certificateProvider managedCertificateProvider
|
||||
acmeService adapter.SimpleLifecycle
|
||||
certificate []byte
|
||||
key []byte
|
||||
@@ -53,18 +121,17 @@ func (c *STDServerConfig) SetServerName(serverName string) {
|
||||
func (c *STDServerConfig) NextProtos() []string {
|
||||
c.access.RLock()
|
||||
defer c.access.RUnlock()
|
||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
|
||||
return c.config.NextProtos[1:]
|
||||
} else {
|
||||
return c.config.NextProtos
|
||||
}
|
||||
return c.config.NextProtos
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
config := c.config.Clone()
|
||||
if c.acmeService != nil && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == ACMETLS1Protocol {
|
||||
if c.hasACMEALPN() && len(c.config.NextProtos) > 1 && c.config.NextProtos[0] == C.ACMETLS1Protocol {
|
||||
config.NextProtos = append(c.config.NextProtos[:1], nextProto...)
|
||||
} else {
|
||||
config.NextProtos = nextProto
|
||||
@@ -72,6 +139,18 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
||||
c.config = config
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) hasACMEALPN() bool {
|
||||
if c.acmeService != nil {
|
||||
return true
|
||||
}
|
||||
if c.certificateProvider != nil {
|
||||
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
|
||||
return len(acmeProvider.GetACMENextProtos()) > 0
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) STDConfig() (*STDConfig, error) {
|
||||
return c.config, nil
|
||||
}
|
||||
@@ -91,15 +170,39 @@ func (c *STDServerConfig) Clone() Config {
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) Start() error {
|
||||
if c.acmeService != nil {
|
||||
return c.acmeService.Start()
|
||||
} else {
|
||||
err := c.startWatcher()
|
||||
if c.certificateProvider != nil {
|
||||
err := c.certificateProvider.Start()
|
||||
if err != nil {
|
||||
c.logger.Warn("create fsnotify watcher: ", err)
|
||||
return err
|
||||
}
|
||||
if acmeProvider, isACME := c.certificateProvider.(adapter.ACMECertificateProvider); isACME {
|
||||
nextProtos := acmeProvider.GetACMENextProtos()
|
||||
if len(nextProtos) > 0 {
|
||||
c.access.Lock()
|
||||
config := c.config.Clone()
|
||||
mergedNextProtos := append([]string{}, nextProtos...)
|
||||
for _, nextProto := range config.NextProtos {
|
||||
if !common.Contains(mergedNextProtos, nextProto) {
|
||||
mergedNextProtos = append(mergedNextProtos, nextProto)
|
||||
}
|
||||
}
|
||||
config.NextProtos = mergedNextProtos
|
||||
c.config = config
|
||||
c.access.Unlock()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if c.acmeService != nil {
|
||||
err := c.acmeService.Start()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := c.startWatcher()
|
||||
if err != nil {
|
||||
c.logger.Warn("create fsnotify watcher: ", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) startWatcher() error {
|
||||
@@ -203,23 +306,34 @@ func (c *STDServerConfig) certificateUpdated(path string) error {
|
||||
}
|
||||
|
||||
func (c *STDServerConfig) Close() error {
|
||||
if c.acmeService != nil {
|
||||
return c.acmeService.Close()
|
||||
}
|
||||
if c.watcher != nil {
|
||||
return c.watcher.Close()
|
||||
}
|
||||
return nil
|
||||
return common.Close(c.certificateProvider, c.acmeService, c.watcher)
|
||||
}
|
||||
|
||||
func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||
if !options.Enabled {
|
||||
return nil, nil
|
||||
}
|
||||
//nolint:staticcheck
|
||||
if options.CertificateProvider != nil && options.ACME != nil {
|
||||
return nil, E.New("certificate_provider and acme are mutually exclusive")
|
||||
}
|
||||
var tlsConfig *tls.Config
|
||||
var certificateProvider managedCertificateProvider
|
||||
var acmeService adapter.SimpleLifecycle
|
||||
var err error
|
||||
if options.ACME != nil && len(options.ACME.Domain) > 0 {
|
||||
if options.CertificateProvider != nil {
|
||||
certificateProvider, err = newCertificateProvider(ctx, logger, options.CertificateProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig = &tls.Config{
|
||||
GetCertificate: certificateProvider.GetCertificate,
|
||||
}
|
||||
if options.Insecure {
|
||||
return nil, errInsecureUnused
|
||||
}
|
||||
} else if options.ACME != nil && len(options.ACME.Domain) > 0 { //nolint:staticcheck
|
||||
deprecated.Report(ctx, deprecated.OptionInlineACME)
|
||||
//nolint:staticcheck
|
||||
tlsConfig, acmeService, err = startACME(ctx, logger, common.PtrValueOrDefault(options.ACME))
|
||||
if err != nil {
|
||||
@@ -272,7 +386,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
certificate []byte
|
||||
key []byte
|
||||
)
|
||||
if acmeService == nil {
|
||||
if certificateProvider == nil && acmeService == nil {
|
||||
if len(options.Certificate) > 0 {
|
||||
certificate = []byte(strings.Join(options.Certificate, "\n"))
|
||||
} else if options.CertificatePath != "" {
|
||||
@@ -360,6 +474,7 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
serverConfig := &STDServerConfig{
|
||||
config: tlsConfig,
|
||||
logger: logger,
|
||||
certificateProvider: certificateProvider,
|
||||
acmeService: acmeService,
|
||||
certificate: certificate,
|
||||
key: key,
|
||||
@@ -369,8 +484,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
echKeyPath: echKeyPath,
|
||||
}
|
||||
serverConfig.config.GetConfigForClient = func(info *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
serverConfig.access.Lock()
|
||||
defer serverConfig.access.Unlock()
|
||||
serverConfig.access.RLock()
|
||||
defer serverConfig.access.RUnlock()
|
||||
return serverConfig.config, nil
|
||||
}
|
||||
var config ServerConfig = serverConfig
|
||||
@@ -387,3 +502,27 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func newCertificateProvider(ctx context.Context, logger log.ContextLogger, options *option.CertificateProviderOptions) (managedCertificateProvider, error) {
|
||||
if options.IsShared() {
|
||||
manager := service.FromContext[adapter.CertificateProviderManager](ctx)
|
||||
if manager == nil {
|
||||
return nil, E.New("missing certificate provider manager in context")
|
||||
}
|
||||
return &sharedCertificateProvider{
|
||||
tag: options.Tag,
|
||||
manager: manager,
|
||||
}, nil
|
||||
}
|
||||
registry := service.FromContext[adapter.CertificateProviderRegistry](ctx)
|
||||
if registry == nil {
|
||||
return nil, E.New("missing certificate provider registry in context")
|
||||
}
|
||||
provider, err := registry.Create(ctx, logger, "", options.Type, options.Options)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create inline certificate provider")
|
||||
}
|
||||
return &inlineCertificateProvider{
|
||||
provider: provider,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,36 +1,38 @@
|
||||
package constant
|
||||
|
||||
const (
|
||||
TypeTun = "tun"
|
||||
TypeRedirect = "redirect"
|
||||
TypeTProxy = "tproxy"
|
||||
TypeDirect = "direct"
|
||||
TypeBlock = "block"
|
||||
TypeDNS = "dns"
|
||||
TypeSOCKS = "socks"
|
||||
TypeHTTP = "http"
|
||||
TypeMixed = "mixed"
|
||||
TypeShadowsocks = "shadowsocks"
|
||||
TypeVMess = "vmess"
|
||||
TypeTrojan = "trojan"
|
||||
TypeNaive = "naive"
|
||||
TypeWireGuard = "wireguard"
|
||||
TypeHysteria = "hysteria"
|
||||
TypeTor = "tor"
|
||||
TypeSSH = "ssh"
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeAnyTLS = "anytls"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeDERP = "derp"
|
||||
TypeResolved = "resolved"
|
||||
TypeSSMAPI = "ssm-api"
|
||||
TypeCCM = "ccm"
|
||||
TypeOCM = "ocm"
|
||||
TypeOOMKiller = "oom-killer"
|
||||
TypeTun = "tun"
|
||||
TypeRedirect = "redirect"
|
||||
TypeTProxy = "tproxy"
|
||||
TypeDirect = "direct"
|
||||
TypeBlock = "block"
|
||||
TypeDNS = "dns"
|
||||
TypeSOCKS = "socks"
|
||||
TypeHTTP = "http"
|
||||
TypeMixed = "mixed"
|
||||
TypeShadowsocks = "shadowsocks"
|
||||
TypeVMess = "vmess"
|
||||
TypeTrojan = "trojan"
|
||||
TypeNaive = "naive"
|
||||
TypeWireGuard = "wireguard"
|
||||
TypeHysteria = "hysteria"
|
||||
TypeTor = "tor"
|
||||
TypeSSH = "ssh"
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeAnyTLS = "anytls"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeDERP = "derp"
|
||||
TypeResolved = "resolved"
|
||||
TypeSSMAPI = "ssm-api"
|
||||
TypeCCM = "ccm"
|
||||
TypeOCM = "ocm"
|
||||
TypeOOMKiller = "oom-killer"
|
||||
TypeACME = "acme"
|
||||
TypeCloudflareOriginCA = "cloudflare-origin-ca"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
package tls
|
||||
package constant
|
||||
|
||||
const ACMETLS1Protocol = "acme-tls/1"
|
||||
@@ -168,7 +168,7 @@ func (s *StartedService) waitForStarted(ctx context.Context) error {
|
||||
func (s *StartedService) StartOrReloadService(profileContent string, options *OverrideOptions) error {
|
||||
s.serviceAccess.Lock()
|
||||
switch s.serviceStatus.Status {
|
||||
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING:
|
||||
case ServiceStatus_IDLE, ServiceStatus_STARTED, ServiceStatus_STARTING, ServiceStatus_FATAL:
|
||||
default:
|
||||
s.serviceAccess.Unlock()
|
||||
return os.ErrInvalid
|
||||
@@ -226,13 +226,14 @@ func (s *StartedService) CloseService() error {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
s.updateStatus(ServiceStatus_STOPPING)
|
||||
if s.instance != nil {
|
||||
err := s.instance.Close()
|
||||
instance := s.instance
|
||||
s.instance = nil
|
||||
if instance != nil {
|
||||
err := instance.Close()
|
||||
if err != nil {
|
||||
return s.updateStatusError(err)
|
||||
}
|
||||
}
|
||||
s.instance = nil
|
||||
s.startedAt = time.Time{}
|
||||
s.updateStatus(ServiceStatus_IDLE)
|
||||
s.serviceAccess.Unlock()
|
||||
@@ -949,11 +950,11 @@ func buildConnectionProto(metadata *trafficontrol.TrackerMetadata) *Connection {
|
||||
var processInfo *ProcessInfo
|
||||
if metadata.Metadata.ProcessInfo != nil {
|
||||
processInfo = &ProcessInfo{
|
||||
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
|
||||
UserId: metadata.Metadata.ProcessInfo.UserId,
|
||||
UserName: metadata.Metadata.ProcessInfo.UserName,
|
||||
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
|
||||
PackageName: metadata.Metadata.ProcessInfo.AndroidPackageName,
|
||||
ProcessId: metadata.Metadata.ProcessInfo.ProcessID,
|
||||
UserId: metadata.Metadata.ProcessInfo.UserId,
|
||||
UserName: metadata.Metadata.ProcessInfo.UserName,
|
||||
ProcessPath: metadata.Metadata.ProcessInfo.ProcessPath,
|
||||
PackageNames: metadata.Metadata.ProcessInfo.AndroidPackageNames,
|
||||
}
|
||||
}
|
||||
return &Connection{
|
||||
|
||||
@@ -1460,7 +1460,7 @@ type ProcessInfo struct {
|
||||
UserId int32 `protobuf:"varint,2,opt,name=userId,proto3" json:"userId,omitempty"`
|
||||
UserName string `protobuf:"bytes,3,opt,name=userName,proto3" json:"userName,omitempty"`
|
||||
ProcessPath string `protobuf:"bytes,4,opt,name=processPath,proto3" json:"processPath,omitempty"`
|
||||
PackageName string `protobuf:"bytes,5,opt,name=packageName,proto3" json:"packageName,omitempty"`
|
||||
PackageNames []string `protobuf:"bytes,5,rep,name=packageNames,proto3" json:"packageNames,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1523,11 +1523,11 @@ func (x *ProcessInfo) GetProcessPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProcessInfo) GetPackageName() string {
|
||||
func (x *ProcessInfo) GetPackageNames() []string {
|
||||
if x != nil {
|
||||
return x.PackageName
|
||||
return x.PackageNames
|
||||
}
|
||||
return ""
|
||||
return nil
|
||||
}
|
||||
|
||||
type CloseConnectionRequest struct {
|
||||
@@ -1884,13 +1884,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
||||
"\boutbound\x18\x13 \x01(\tR\boutbound\x12\"\n" +
|
||||
"\foutboundType\x18\x14 \x01(\tR\foutboundType\x12\x1c\n" +
|
||||
"\tchainList\x18\x15 \x03(\tR\tchainList\x125\n" +
|
||||
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa3\x01\n" +
|
||||
"\vprocessInfo\x18\x16 \x01(\v2\x13.daemon.ProcessInfoR\vprocessInfo\"\xa5\x01\n" +
|
||||
"\vProcessInfo\x12\x1c\n" +
|
||||
"\tprocessId\x18\x01 \x01(\rR\tprocessId\x12\x16\n" +
|
||||
"\x06userId\x18\x02 \x01(\x05R\x06userId\x12\x1a\n" +
|
||||
"\buserName\x18\x03 \x01(\tR\buserName\x12 \n" +
|
||||
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12 \n" +
|
||||
"\vpackageName\x18\x05 \x01(\tR\vpackageName\"(\n" +
|
||||
"\vprocessPath\x18\x04 \x01(\tR\vprocessPath\x12\"\n" +
|
||||
"\fpackageNames\x18\x05 \x03(\tR\fpackageNames\"(\n" +
|
||||
"\x16CloseConnectionRequest\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\"K\n" +
|
||||
"\x12DeprecatedWarnings\x125\n" +
|
||||
|
||||
@@ -195,7 +195,7 @@ message ProcessInfo {
|
||||
int32 userId = 2;
|
||||
string userName = 3;
|
||||
string processPath = 4;
|
||||
string packageName = 5;
|
||||
repeated string packageNames = 5;
|
||||
}
|
||||
|
||||
message CloseConnectionRequest {
|
||||
|
||||
@@ -55,6 +55,12 @@ type contextKeyConnecting struct{}
|
||||
|
||||
var errRecursiveConnectorDial = E.New("recursive connector dial")
|
||||
|
||||
type connectorDialResult[T any] struct {
|
||||
connection T
|
||||
cancel context.CancelFunc
|
||||
err error
|
||||
}
|
||||
|
||||
func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
||||
var zero T
|
||||
for {
|
||||
@@ -100,41 +106,37 @@ func (c *Connector[T]) Get(ctx context.Context) (T, error) {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
c.connecting = make(chan struct{})
|
||||
connecting := make(chan struct{})
|
||||
c.connecting = connecting
|
||||
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
|
||||
dialResult := make(chan connectorDialResult[T], 1)
|
||||
c.access.Unlock()
|
||||
|
||||
dialContext := context.WithValue(ctx, contextKeyConnecting{}, c)
|
||||
connection, cancel, err := c.dialWithCancellation(dialContext)
|
||||
go func() {
|
||||
connection, cancel, err := c.dialWithCancellation(dialContext)
|
||||
dialResult <- connectorDialResult[T]{
|
||||
connection: connection,
|
||||
cancel: cancel,
|
||||
err: err,
|
||||
}
|
||||
}()
|
||||
|
||||
c.access.Lock()
|
||||
close(c.connecting)
|
||||
c.connecting = nil
|
||||
|
||||
if err != nil {
|
||||
c.access.Unlock()
|
||||
return zero, err
|
||||
}
|
||||
|
||||
if c.closed {
|
||||
cancel()
|
||||
c.callbacks.Close(connection)
|
||||
c.access.Unlock()
|
||||
select {
|
||||
case result := <-dialResult:
|
||||
return c.completeDial(ctx, connecting, result)
|
||||
case <-ctx.Done():
|
||||
go func() {
|
||||
result := <-dialResult
|
||||
_, _ = c.completeDial(ctx, connecting, result)
|
||||
}()
|
||||
return zero, ctx.Err()
|
||||
case <-c.closeCtx.Done():
|
||||
go func() {
|
||||
result := <-dialResult
|
||||
_, _ = c.completeDial(ctx, connecting, result)
|
||||
}()
|
||||
return zero, ErrTransportClosed
|
||||
}
|
||||
if err = ctx.Err(); err != nil {
|
||||
cancel()
|
||||
c.callbacks.Close(connection)
|
||||
c.access.Unlock()
|
||||
return zero, err
|
||||
}
|
||||
|
||||
c.connection = connection
|
||||
c.hasConnection = true
|
||||
c.connectionCancel = cancel
|
||||
result := c.connection
|
||||
c.access.Unlock()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,6 +145,38 @@ func isRecursiveConnectorDial[T any](ctx context.Context, connector *Connector[T
|
||||
return loaded && dialConnector == connector
|
||||
}
|
||||
|
||||
func (c *Connector[T]) completeDial(ctx context.Context, connecting chan struct{}, result connectorDialResult[T]) (T, error) {
|
||||
var zero T
|
||||
|
||||
c.access.Lock()
|
||||
defer c.access.Unlock()
|
||||
defer func() {
|
||||
if c.connecting == connecting {
|
||||
c.connecting = nil
|
||||
}
|
||||
close(connecting)
|
||||
}()
|
||||
|
||||
if result.err != nil {
|
||||
return zero, result.err
|
||||
}
|
||||
if c.closed || c.closeCtx.Err() != nil {
|
||||
result.cancel()
|
||||
c.callbacks.Close(result.connection)
|
||||
return zero, ErrTransportClosed
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
result.cancel()
|
||||
c.callbacks.Close(result.connection)
|
||||
return zero, err
|
||||
}
|
||||
|
||||
c.connection = result.connection
|
||||
c.hasConnection = true
|
||||
c.connectionCancel = result.cancel
|
||||
return c.connection, nil
|
||||
}
|
||||
|
||||
func (c *Connector[T]) dialWithCancellation(ctx context.Context) (T, context.CancelFunc, error) {
|
||||
var zero T
|
||||
if err := ctx.Err(); err != nil {
|
||||
|
||||
@@ -188,13 +188,157 @@ func TestConnectorCanceledRequestDoesNotCacheConnection(t *testing.T) {
|
||||
err := <-result
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
require.EqualValues(t, 1, dialCount.Load())
|
||||
require.EqualValues(t, 1, closeCount.Load())
|
||||
require.Eventually(t, func() bool {
|
||||
return closeCount.Load() == 1
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
_, err = connector.Get(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 2, dialCount.Load())
|
||||
}
|
||||
|
||||
func TestConnectorCanceledRequestReturnsBeforeIgnoredDialCompletes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
dialCount atomic.Int32
|
||||
closeCount atomic.Int32
|
||||
)
|
||||
dialStarted := make(chan struct{}, 1)
|
||||
releaseDial := make(chan struct{})
|
||||
|
||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
dialCount.Add(1)
|
||||
select {
|
||||
case dialStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
<-releaseDial
|
||||
return &testConnectorConnection{}, nil
|
||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {
|
||||
closeCount.Add(1)
|
||||
},
|
||||
Reset: func(connection *testConnectorConnection) {},
|
||||
})
|
||||
|
||||
requestContext, cancel := context.WithCancel(context.Background())
|
||||
result := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := connector.Get(requestContext)
|
||||
result <- err
|
||||
}()
|
||||
|
||||
<-dialStarted
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case err := <-result:
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("Get did not return after request cancel")
|
||||
}
|
||||
|
||||
require.EqualValues(t, 1, dialCount.Load())
|
||||
require.EqualValues(t, 0, closeCount.Load())
|
||||
|
||||
close(releaseDial)
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return closeCount.Load() == 1
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
_, err := connector.Get(context.Background())
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 2, dialCount.Load())
|
||||
}
|
||||
|
||||
func TestConnectorWaiterDoesNotStartNewDialBeforeCanceledDialCompletes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
dialCount atomic.Int32
|
||||
closeCount atomic.Int32
|
||||
)
|
||||
firstDialStarted := make(chan struct{}, 1)
|
||||
secondDialStarted := make(chan struct{}, 1)
|
||||
releaseFirstDial := make(chan struct{})
|
||||
|
||||
connector := NewConnector(context.Background(), func(ctx context.Context) (*testConnectorConnection, error) {
|
||||
attempt := dialCount.Add(1)
|
||||
switch attempt {
|
||||
case 1:
|
||||
select {
|
||||
case firstDialStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
<-releaseFirstDial
|
||||
case 2:
|
||||
select {
|
||||
case secondDialStarted <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return &testConnectorConnection{}, nil
|
||||
}, ConnectorCallbacks[*testConnectorConnection]{
|
||||
IsClosed: func(connection *testConnectorConnection) bool {
|
||||
return false
|
||||
},
|
||||
Close: func(connection *testConnectorConnection) {
|
||||
closeCount.Add(1)
|
||||
},
|
||||
Reset: func(connection *testConnectorConnection) {},
|
||||
})
|
||||
|
||||
requestContext, cancel := context.WithCancel(context.Background())
|
||||
firstResult := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := connector.Get(requestContext)
|
||||
firstResult <- err
|
||||
}()
|
||||
|
||||
<-firstDialStarted
|
||||
cancel()
|
||||
|
||||
secondResult := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := connector.Get(context.Background())
|
||||
secondResult <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-secondDialStarted:
|
||||
t.Fatal("second dial started before first dial completed")
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
}
|
||||
|
||||
select {
|
||||
case err := <-firstResult:
|
||||
require.ErrorIs(t, err, context.Canceled)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("first Get did not return after request cancel")
|
||||
}
|
||||
|
||||
close(releaseFirstDial)
|
||||
|
||||
require.Eventually(t, func() bool {
|
||||
return closeCount.Load() == 1
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
|
||||
select {
|
||||
case <-secondDialStarted:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("second dial did not start after first dial completed")
|
||||
}
|
||||
|
||||
err := <-secondResult
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, 2, dialCount.Load())
|
||||
}
|
||||
|
||||
func TestConnectorDialContextNotCanceledByRequestContextAfterDial(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -40,13 +39,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, servers []M.Socksaddr,
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, servers, fqdn, message)
|
||||
if err == nil {
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = dns.RcodeSuccess
|
||||
}
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport"
|
||||
"github.com/sagernet/sing/common/buf"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
@@ -49,13 +48,6 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
|
||||
results := make(chan queryResult)
|
||||
startRacer := func(ctx context.Context, fqdn string) {
|
||||
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
|
||||
if err == nil {
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
err = dns.RcodeError(response.Rcode)
|
||||
} else if len(dns.MessageToAddresses(response)) == 0 {
|
||||
err = E.New(fqdn, ": empty result")
|
||||
}
|
||||
}
|
||||
select {
|
||||
case results <- queryResult{response, err}:
|
||||
case <-returned:
|
||||
|
||||
@@ -2,6 +2,127 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.14.0-alpha.8
|
||||
|
||||
* Add BBR profile and hop interval randomization for Hysteria2 **1**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [Hysteria2 Inbound](/configuration/inbound/hysteria2/#bbr_profile) and [Hysteria2 Outbound](/configuration/outbound/hysteria2/#bbr_profile).
|
||||
|
||||
#### 1.14.0-alpha.8
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.5
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.7
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.13.4
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.4
|
||||
|
||||
* Refactor ACME support to certificate provider system **1**
|
||||
* Add Cloudflare Origin CA certificate provider **2**
|
||||
* Add Tailscale certificate provider **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
See [Certificate Provider](/configuration/shared/certificate-provider/) and [Migration](/migration/#migrate-inline-acme-to-certificate-provider).
|
||||
|
||||
**2**:
|
||||
|
||||
See [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca).
|
||||
|
||||
**3**:
|
||||
|
||||
See [Tailscale](/configuration/shared/certificate-provider/tailscale).
|
||||
|
||||
#### 1.13.3
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
* Backport to macOS 10.13 High Sierra **2**
|
||||
* OCM service: Add WebSocket support for Responses API **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||
|
||||
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||
|
||||
**2**:
|
||||
|
||||
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||
|
||||
**3**:
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
#### 1.12.24
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
#### 1.14.0-alpha.2
|
||||
|
||||
* Add OpenWrt and Alpine APK packages to release **1**
|
||||
* Backport to macOS 10.13 High Sierra **2**
|
||||
* OCM service: Add WebSocket support for Responses API **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Alpine APK files use `linux` in the filename to distinguish from OpenWrt APKs which use the `openwrt` prefix:
|
||||
|
||||
- OpenWrt: `sing-box_{version}_openwrt_{architecture}.apk`
|
||||
- Alpine: `sing-box_{version}_linux_{architecture}.apk`
|
||||
|
||||
**2**:
|
||||
|
||||
Legacy macOS binaries (with `-legacy-macos-10.13` suffix) now support
|
||||
macOS 10.13 High Sierra, built using Go 1.25 with patches
|
||||
from [SagerNet/go](https://github.com/SagerNet/go).
|
||||
|
||||
**3**:
|
||||
|
||||
See [OCM](/configuration/service/ocm).
|
||||
|
||||
#### 1.14.0-alpha.1
|
||||
|
||||
* Add `source_mac_address` and `source_hostname` rule items **1**
|
||||
* Add `include_mac_address` and `exclude_mac_address` TUN options **2**
|
||||
* Update NaiveProxy to 145.0.7632.159 **3**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
New rule items for matching LAN devices by MAC address and hostname via neighbor resolution.
|
||||
Supported on Linux, macOS, or in graphical clients on Android and macOS.
|
||||
|
||||
See [Route Rule](/configuration/route/rule/#source_mac_address), [DNS Rule](/configuration/dns/rule/#source_mac_address) and [Neighbor Resolution](/configuration/shared/neighbor/).
|
||||
|
||||
**2**:
|
||||
|
||||
Limit or exclude devices from TUN routing by MAC address.
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
See [TUN](/configuration/inbound/tun/#include_mac_address).
|
||||
|
||||
**3**:
|
||||
|
||||
This is not an official update from NaiveProxy. Instead, it's a Chromium codebase update maintained by Project S.
|
||||
|
||||
#### 1.13.2
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||
|
||||
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
|
||||
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -149,6 +154,12 @@ icon: material/alert-decagram
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@@ -209,7 +220,7 @@ icon: material/alert-decagram
|
||||
(`source_port` || `source_port_range`) &&
|
||||
`other fields`
|
||||
|
||||
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
|
||||
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
|
||||
|
||||
#### inbound
|
||||
|
||||
@@ -408,6 +419,26 @@ Matches network interface (same values as `network_type`) address.
|
||||
|
||||
Match default interface address.
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device MAC address.
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device hostname from DHCP leases.
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
@@ -546,4 +577,4 @@ Match any IP with query response.
|
||||
|
||||
#### rules
|
||||
|
||||
Included rules.
|
||||
Included rules.
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -149,6 +154,12 @@ icon: material/alert-decagram
|
||||
"default_interface_address": [
|
||||
"2000::/3"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"wifi_ssid": [
|
||||
"My WIFI"
|
||||
],
|
||||
@@ -208,7 +219,7 @@ icon: material/alert-decagram
|
||||
(`source_port` || `source_port_range`) &&
|
||||
`other fields`
|
||||
|
||||
另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。
|
||||
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。
|
||||
|
||||
#### inbound
|
||||
|
||||
@@ -256,7 +267,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||
|
||||
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
||||
GeoSite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
|
||||
|
||||
匹配 Geosite。
|
||||
|
||||
@@ -264,7 +275,7 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||
|
||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
|
||||
|
||||
匹配源 GeoIP。
|
||||
|
||||
@@ -407,6 +418,26 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
匹配默认接口地址。
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备 MAC 地址。
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备从 DHCP 租约获取的主机名。
|
||||
|
||||
#### wifi_ssid
|
||||
|
||||
!!! quote ""
|
||||
@@ -453,7 +484,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||
|
||||
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-outbound-dns-rule-items-to-domain-resolver)。
|
||||
`outbound` 规则项已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-outbound-dns-规则项到域解析选项)。
|
||||
|
||||
匹配出站。
|
||||
|
||||
@@ -505,7 +536,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||
|
||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
|
||||
|
||||
|
||||
与查询响应匹配 GeoIP。
|
||||
@@ -550,4 +581,4 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
==必填==
|
||||
|
||||
包括的规则。
|
||||
包括的规则。
|
||||
|
||||
@@ -64,7 +64,7 @@ DNS 服务器的路径。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ DNS 服务器的路径。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.12.0"
|
||||
|
||||
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/migration/#migrate-to-new-dns-servers)。
|
||||
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||
|
||||
!!! quote "sing-box 1.9.0 中的更改"
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ DNS 服务器的端口。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ DNS 服务器的端口。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
|
||||
将拒绝的 DNS 响应缓存存储在缓存文件中。
|
||||
|
||||
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#_3) 的检查结果将被缓存至过期。
|
||||
[地址筛选 DNS 规则项](/zh/configuration/dns/rule/#地址筛选字段) 的检查结果将被缓存至过期。
|
||||
|
||||
#### rdrc_timeout
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
!!! quote ""
|
||||
|
||||
默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#_5)。
|
||||
默认安装不包含 V2Ray API,参阅 [安装](/zh/installation/build-from-source/#构建标记)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -58,4 +58,4 @@ AnyTLS 填充方案行数组。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
#### users
|
||||
|
||||
|
||||
@@ -104,4 +104,4 @@ base64 编码的认证密码。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [bbr_profile](#bbr_profile)
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
:material-alert: [masquerade](#masquerade)
|
||||
@@ -31,6 +35,7 @@ icon: material/alert-decagram
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
"masquerade": "", // or {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
}
|
||||
```
|
||||
@@ -141,6 +146,14 @@ Fixed response headers.
|
||||
|
||||
Fixed response content.
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`.
|
||||
|
||||
`standard` is used by default.
|
||||
|
||||
#### brutal_debug
|
||||
|
||||
Enable debug information logging for Hysteria Brutal CC.
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [bbr_profile](#bbr_profile)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-alert: [masquerade](#masquerade)
|
||||
@@ -31,6 +35,7 @@ icon: material/alert-decagram
|
||||
"ignore_client_bandwidth": false,
|
||||
"tls": {},
|
||||
"masquerade": "", // 或 {}
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false
|
||||
}
|
||||
```
|
||||
@@ -38,7 +43,7 @@ icon: material/alert-decagram
|
||||
!!! warning "与官方 Hysteria2 的区别"
|
||||
|
||||
官方程序支持一种名为 **userpass** 的验证方式,
|
||||
本质上上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
|
||||
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
|
||||
要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。
|
||||
|
||||
### 监听字段
|
||||
@@ -85,7 +90,7 @@ Hysteria 用户
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
#### masquerade
|
||||
|
||||
@@ -138,6 +143,14 @@ HTTP3 服务器认证失败时的行为 (对象配置)。
|
||||
|
||||
固定响应内容。
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。
|
||||
|
||||
默认使用 `standard`。
|
||||
|
||||
#### brutal_debug
|
||||
|
||||
启用 Hysteria Brutal CC 的调试信息日志记录。
|
||||
|
||||
@@ -60,4 +60,4 @@ QUIC 拥塞控制算法。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
@@ -93,4 +93,4 @@
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
|
||||
|
||||
@@ -43,7 +43,7 @@ Trojan 用户。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
#### fallback
|
||||
|
||||
@@ -61,7 +61,7 @@ TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -75,4 +75,4 @@ QUIC 拥塞控制算法
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
@@ -2,6 +2,15 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [include_mac_address](#include_mac_address)
|
||||
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.3"
|
||||
|
||||
:material-alert: [strict_route](#strict_route)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
|
||||
@@ -125,6 +134,12 @@ icon: material/new-box
|
||||
"exclude_package": [
|
||||
"com.android.captiveportallogin"
|
||||
],
|
||||
"include_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"exclude_mac_address": [
|
||||
"66:77:88:99:aa:bb"
|
||||
],
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": false,
|
||||
@@ -348,6 +363,9 @@ Enforce strict routing rules when `auto_route` is enabled:
|
||||
|
||||
* Let unsupported network unreachable
|
||||
* For legacy reasons, when neither `strict_route` nor `auto_redirect` are enabled, all ICMP traffic will not go through TUN.
|
||||
* When `auto_redirect` is enabled, `strict_route` also affects `SO_BINDTODEVICE` traffic:
|
||||
* Enabled: `SO_BINDTODEVICE` traffic is redirected through sing-box.
|
||||
* Disabled: `SO_BINDTODEVICE` traffic bypasses sing-box.
|
||||
|
||||
*In Windows*:
|
||||
|
||||
@@ -548,6 +566,30 @@ Limit android packages in route.
|
||||
|
||||
Exclude android packages in route.
|
||||
|
||||
#### include_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
Limit MAC addresses in route. Not limited by default.
|
||||
|
||||
Conflict with `exclude_mac_address`.
|
||||
|
||||
#### exclude_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux with `auto_route` and `auto_redirect` enabled.
|
||||
|
||||
Exclude MAC addresses in route.
|
||||
|
||||
Conflict with `include_mac_address`.
|
||||
|
||||
#### platform
|
||||
|
||||
Platform-specific settings, provided by client applications.
|
||||
|
||||
@@ -2,6 +2,15 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [include_mac_address](#include_mac_address)
|
||||
:material-plus: [exclude_mac_address](#exclude_mac_address)
|
||||
|
||||
!!! quote "sing-box 1.13.3 中的更改"
|
||||
|
||||
:material-alert: [strict_route](#strict_route)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [auto_redirect_reset_mark](#auto_redirect_reset_mark)
|
||||
@@ -126,6 +135,12 @@ icon: material/new-box
|
||||
"exclude_package": [
|
||||
"com.android.captiveportallogin"
|
||||
],
|
||||
"include_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"exclude_mac_address": [
|
||||
"66:77:88:99:aa:bb"
|
||||
],
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": false,
|
||||
@@ -347,6 +362,9 @@ tun 接口的 IPv6 前缀。
|
||||
|
||||
* 使不支持的网络不可达。
|
||||
* 出于历史遗留原因,当未启用 `strict_route` 或 `auto_redirect` 时,所有 ICMP 流量将不会通过 TUN。
|
||||
* 当启用 `auto_redirect` 时,`strict_route` 也影响 `SO_BINDTODEVICE` 流量:
|
||||
* 启用:`SO_BINDTODEVICE` 流量被重定向通过 sing-box。
|
||||
* 禁用:`SO_BINDTODEVICE` 流量绕过 sing-box。
|
||||
|
||||
*在 Windows 中*:
|
||||
|
||||
@@ -536,6 +554,30 @@ TCP/IP 栈。
|
||||
|
||||
排除路由的 Android 应用包名。
|
||||
|
||||
#### include_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。
|
||||
|
||||
限制被路由的 MAC 地址。默认不限制。
|
||||
|
||||
与 `exclude_mac_address` 冲突。
|
||||
|
||||
#### exclude_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux,且需要 `auto_route` 和 `auto_redirect` 已启用。
|
||||
|
||||
排除路由的 MAC 地址。
|
||||
|
||||
与 `include_mac_address` 冲突。
|
||||
|
||||
#### platform
|
||||
|
||||
平台特定的设置,由客户端应用提供。
|
||||
|
||||
@@ -48,11 +48,11 @@ VLESS 子协议。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -43,11 +43,11 @@ VMess 用户。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#inbound)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#入站)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Introduction
|
||||
|
||||
sing-box uses JSON for configuration files.
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
@@ -10,6 +9,7 @@ sing-box uses JSON for configuration files.
|
||||
"dns": {},
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -27,6 +27,7 @@ sing-box uses JSON for configuration files.
|
||||
| `dns` | [DNS](./dns/) |
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [Certificate](./certificate/) |
|
||||
| `certificate_providers` | [Certificate Provider](./shared/certificate-provider/) |
|
||||
| `endpoints` | [Endpoint](./endpoint/) |
|
||||
| `inbounds` | [Inbound](./inbound/) |
|
||||
| `outbounds` | [Outbound](./outbound/) |
|
||||
@@ -50,4 +51,4 @@ sing-box format -w -c config.json -D config_directory
|
||||
|
||||
```bash
|
||||
sing-box merge output.json -c config.json -D config_directory
|
||||
```
|
||||
```
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# 引言
|
||||
|
||||
sing-box 使用 JSON 作为配置文件格式。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
@@ -10,6 +9,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
"dns": {},
|
||||
"ntp": {},
|
||||
"certificate": {},
|
||||
"certificate_providers": [],
|
||||
"endpoints": [],
|
||||
"inbounds": [],
|
||||
"outbounds": [],
|
||||
@@ -27,6 +27,7 @@ sing-box 使用 JSON 作为配置文件格式。
|
||||
| `dns` | [DNS](./dns/) |
|
||||
| `ntp` | [NTP](./ntp/) |
|
||||
| `certificate` | [证书](./certificate/) |
|
||||
| `certificate_providers` | [证书提供者](./shared/certificate-provider/) |
|
||||
| `endpoints` | [端点](./endpoint/) |
|
||||
| `inbounds` | [入站](./inbound/) |
|
||||
| `outbounds` | [出站](./outbound/) |
|
||||
@@ -50,4 +51,4 @@ sing-box format -w -c config.json -D config_directory
|
||||
|
||||
```bash
|
||||
sing-box merge output.json -c config.json -D config_directory
|
||||
```
|
||||
```
|
||||
|
||||
@@ -59,7 +59,7 @@ AnyTLS 密码。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-alert-decagram: [override_address](#override_address)
|
||||
:material-alert-decagram: [override_port](#override_port)
|
||||
:material-delete-clock: [override_address](#override_address)
|
||||
:material-delete-clock: [override_port](#override_port)
|
||||
|
||||
`direct` 出站直接发送请求。
|
||||
|
||||
@@ -29,7 +29,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
|
||||
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
|
||||
|
||||
覆盖连接目标地址。
|
||||
|
||||
@@ -37,7 +37,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-destination-override-fields-to-route-options)。
|
||||
目标覆盖字段在 sing-box 1.11.0 中已废弃,并将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-direct-出站中的目标地址覆盖字段到路由字段)。
|
||||
|
||||
覆盖连接目标端口。
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/migration/#migrate-legacy-special-outbounds-to-rule-actions).
|
||||
旧的特殊出站已被弃用,且将在 sing-box 1.13.0 中被移除, 参阅 [迁移指南](/zh/migration/#迁移旧的特殊出站到规则动作).
|
||||
|
||||
`dns` 出站是一个内部 DNS 服务器。
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ HTTP 请求的额外标头。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@ base64 编码的认证密码。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
|
||||
### 拨号字段
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [hop_interval_max](#hop_interval_max)
|
||||
:material-plus: [bbr_profile](#bbr_profile)
|
||||
|
||||
!!! quote "Changes in sing-box 1.11.0"
|
||||
|
||||
:material-plus: [server_ports](#server_ports)
|
||||
@@ -9,13 +14,14 @@
|
||||
{
|
||||
"type": "hysteria2",
|
||||
"tag": "hy2-out",
|
||||
|
||||
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 1080,
|
||||
"server_ports": [
|
||||
"2080:3000"
|
||||
],
|
||||
"hop_interval": "",
|
||||
"hop_interval_max": "",
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100,
|
||||
"obfs": {
|
||||
@@ -25,8 +31,9 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
|
||||
... // Dial Fields
|
||||
}
|
||||
```
|
||||
@@ -75,6 +82,14 @@ Port hopping interval.
|
||||
|
||||
`30s` is used by default.
|
||||
|
||||
#### hop_interval_max
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Maximum port hopping interval, used for randomization.
|
||||
|
||||
If set, the actual hop interval will be randomly chosen between `hop_interval` and `hop_interval_max`.
|
||||
|
||||
#### up_mbps, down_mbps
|
||||
|
||||
Max bandwidth, in Mbps.
|
||||
@@ -109,6 +124,14 @@ Both is enabled by default.
|
||||
|
||||
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
BBR congestion control algorithm profile, one of `conservative` `standard` `aggressive`.
|
||||
|
||||
`standard` is used by default.
|
||||
|
||||
#### brutal_debug
|
||||
|
||||
Enable debug information logging for Hysteria Brutal CC.
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [hop_interval_max](#hop_interval_max)
|
||||
:material-plus: [bbr_profile](#bbr_profile)
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: [server_ports](#server_ports)
|
||||
@@ -16,6 +21,7 @@
|
||||
"2080:3000"
|
||||
],
|
||||
"hop_interval": "",
|
||||
"hop_interval_max": "",
|
||||
"up_mbps": 100,
|
||||
"down_mbps": 100,
|
||||
"obfs": {
|
||||
@@ -25,8 +31,9 @@
|
||||
"password": "goofy_ahh_password",
|
||||
"network": "tcp",
|
||||
"tls": {},
|
||||
"bbr_profile": "",
|
||||
"brutal_debug": false,
|
||||
|
||||
|
||||
... // 拨号字段
|
||||
}
|
||||
```
|
||||
@@ -38,7 +45,7 @@
|
||||
!!! warning "与官方 Hysteria2 的区别"
|
||||
|
||||
官方程序支持一种名为 **userpass** 的验证方式,
|
||||
本质上上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
|
||||
本质上是将用户名与密码的组合 `<username>:<password>` 作为实际上的密码,而 sing-box 不提供此别名。
|
||||
要将 sing-box 与官方程序一起使用, 您需要填写该组合作为实际密码。
|
||||
|
||||
### 字段
|
||||
@@ -73,6 +80,14 @@
|
||||
|
||||
默认使用 `30s`。
|
||||
|
||||
#### hop_interval_max
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
最大端口跳跃间隔,用于随机化。
|
||||
|
||||
如果设置,实际跳跃间隔将在 `hop_interval` 和 `hop_interval_max` 之间随机选择。
|
||||
|
||||
#### up_mbps, down_mbps
|
||||
|
||||
最大带宽。
|
||||
@@ -105,7 +120,15 @@ QUIC 流量混淆器密码.
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
#### bbr_profile
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
BBR 拥塞控制算法配置,可选 `conservative` `standard` `aggressive`。
|
||||
|
||||
默认使用 `standard`。
|
||||
|
||||
#### brutal_debug
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ QUIC 拥塞控制算法。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
只有 `server_name`、`certificate`、`certificate_path` 和 `ech` 是被支持的。
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
!!! quote ""
|
||||
|
||||
选择器目前只能通过 [Clash API](/zh/configuration/experimental#clash-api) 来控制。
|
||||
选择器目前只能通过 [Clash API](/zh/configuration/experimental/clash-api/) 来控制。
|
||||
|
||||
### 字段
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ UDP over TCP 配置。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ ShadowTLS 协议版本。
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
!!! info ""
|
||||
|
||||
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#_5)。
|
||||
默认安装不包含嵌入式 Tor, 参阅 [安装](/zh/installation/build-from-source/#构建标记)。
|
||||
|
||||
### 字段
|
||||
|
||||
|
||||
@@ -47,11 +47,11 @@ Trojan 密码。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ UDP 包中继模式
|
||||
|
||||
==必填==
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
### 拨号字段
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ VLESS 子协议。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
#### packet_encoding
|
||||
|
||||
@@ -71,7 +71,7 @@ UDP 包编码,默认使用 xudp。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ VMess 用户 ID。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#outbound)。
|
||||
TLS 配置, 参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||
|
||||
#### packet_encoding
|
||||
|
||||
@@ -96,7 +96,7 @@ UDP 包编码。
|
||||
|
||||
#### multiplex
|
||||
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#outbound)。
|
||||
参阅 [多路复用](/zh/configuration/shared/multiplex#出站)。
|
||||
|
||||
#### transport
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/delete-clock
|
||||
|
||||
!!! failure "已在 sing-box 1.11.0 废弃"
|
||||
|
||||
WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/migration/#migrate-wireguard-outbound-to-endpoint)。
|
||||
WireGuard 出站已被弃用,且将在 sing-box 1.13.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-wireguard-出站到端点)。
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/note-remove
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||
|
||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
GeoIP 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ icon: material/note-remove
|
||||
|
||||
!!! failure "已在 sing-box 1.12.0 中被移除"
|
||||
|
||||
Geosite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
||||
Geosite 已在 sing-box 1.8.0 废弃且在 sing-box 1.12.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -4,6 +4,11 @@ icon: material/alert-decagram
|
||||
|
||||
# Route
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
!!! quote "Changes in sing-box 1.12.0"
|
||||
|
||||
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
||||
@@ -35,6 +40,9 @@ icon: material/alert-decagram
|
||||
"override_android_vpn": false,
|
||||
"default_interface": "",
|
||||
"default_mark": 0,
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_domain_resolver": "", // or {}
|
||||
"default_network_strategy": "",
|
||||
"default_network_type": [],
|
||||
@@ -107,6 +115,38 @@ Set routing mark by default.
|
||||
|
||||
Takes no effect if `outbound.routing_mark` is set.
|
||||
|
||||
#### find_process
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, Windows, and macOS.
|
||||
|
||||
Enable process search for logging when no `process_name`, `process_path`, `package_name`, `user` or `user_id` rules exist.
|
||||
|
||||
#### find_neighbor
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux and macOS.
|
||||
|
||||
Enable neighbor resolution for logging when no `source_mac_address` or `source_hostname` rules exist.
|
||||
|
||||
See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
#### dhcp_lease_files
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux and macOS.
|
||||
|
||||
Custom DHCP lease file paths for hostname and MAC address resolution.
|
||||
|
||||
Automatically detected from common DHCP servers (dnsmasq, odhcpd, ISC dhcpd, Kea) if empty.
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
@@ -4,6 +4,11 @@ icon: material/alert-decagram
|
||||
|
||||
# 路由
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [find_neighbor](#find_neighbor)
|
||||
:material-plus: [dhcp_lease_files](#dhcp_lease_files)
|
||||
|
||||
!!! quote "sing-box 1.12.0 中的更改"
|
||||
|
||||
:material-plus: [default_domain_resolver](#default_domain_resolver)
|
||||
@@ -12,7 +17,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: [network_strategy](#network_strategy)
|
||||
:material-plus: [default_network_strategy](#default_network_strategy)
|
||||
:material-plus: [default_network_type](#default_network_type)
|
||||
:material-plus: [default_fallback_network_type](#default_fallback_network_type)
|
||||
:material-plus: [default_fallback_delay](#default_fallback_delay)
|
||||
@@ -37,6 +42,9 @@ icon: material/alert-decagram
|
||||
"override_android_vpn": false,
|
||||
"default_interface": "",
|
||||
"default_mark": 0,
|
||||
"find_process": false,
|
||||
"find_neighbor": false,
|
||||
"dhcp_lease_files": [],
|
||||
"default_network_strategy": "",
|
||||
"default_fallback_delay": ""
|
||||
}
|
||||
@@ -106,11 +114,43 @@ icon: material/alert-decagram
|
||||
|
||||
如果设置了 `outbound.routing_mark` 设置,则不生效。
|
||||
|
||||
#### find_process
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、Windows 和 macOS。
|
||||
|
||||
在没有 `process_name`、`process_path`、`package_name`、`user` 或 `user_id` 规则时启用进程搜索以输出日志。
|
||||
|
||||
#### find_neighbor
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux 和 macOS。
|
||||
|
||||
在没有 `source_mac_address` 或 `source_hostname` 规则时启用邻居解析以输出日志。
|
||||
|
||||
参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
#### dhcp_lease_files
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux 和 macOS。
|
||||
|
||||
用于主机名和 MAC 地址解析的自定义 DHCP 租约文件路径。
|
||||
|
||||
为空时自动从常见 DHCP 服务器(dnsmasq、odhcpd、ISC dhcpd、Kea)检测。
|
||||
|
||||
#### default_domain_resolver
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#domain_resolver)。
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#domain_resolver)。
|
||||
|
||||
可以被 `outbound.domain_resolver` 覆盖。
|
||||
|
||||
@@ -118,7 +158,7 @@ icon: material/alert-decagram
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。
|
||||
|
||||
当 `outbound.bind_interface`, `outbound.inet4_bind_address` 或 `outbound.inet6_bind_address` 已设置时不生效。
|
||||
|
||||
@@ -130,16 +170,16 @@ icon: material/alert-decagram
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#default_network_type)。
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_network_type)。
|
||||
|
||||
#### default_fallback_network_type
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#default_fallback_network_type)。
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#default_fallback_network_type)。
|
||||
|
||||
#### default_fallback_delay
|
||||
|
||||
!!! question "自 sing-box 1.11.0 起"
|
||||
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -159,6 +164,12 @@ icon: material/new-box
|
||||
"tailscale",
|
||||
"wireguard"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
@@ -199,7 +210,7 @@ icon: material/new-box
|
||||
(`source_port` || `source_port_range`) &&
|
||||
`other fields`
|
||||
|
||||
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
|
||||
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
|
||||
|
||||
#### inbound
|
||||
|
||||
@@ -449,6 +460,26 @@ Match specified outbounds' preferred routes.
|
||||
| `tailscale` | Match MagicDNS domains and peers' allowed IPs |
|
||||
| `wireguard` | Match peers's allowed IPs |
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device MAC address.
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
Only supported on Linux, macOS, or in graphical clients on Android and macOS. See [Neighbor Resolution](/configuration/shared/neighbor/) for setup.
|
||||
|
||||
Match source device hostname from DHCP leases.
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "Since sing-box 1.8.0"
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [source_mac_address](#source_mac_address)
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
:material-plus: [interface_address](#interface_address)
|
||||
@@ -22,6 +27,7 @@ icon: material/new-box
|
||||
|
||||
:material-plus: [client](#client)
|
||||
:material-delete-clock: [rule_set_ipcidr_match_source](#rule_set_ipcidr_match_source)
|
||||
:material-plus: [rule_set_ip_cidr_match_source](#rule_set_ip_cidr_match_source)
|
||||
:material-plus: [process_path_regex](#process_path_regex)
|
||||
|
||||
!!! quote "sing-box 1.8.0 中的更改"
|
||||
@@ -156,6 +162,12 @@ icon: material/new-box
|
||||
"tailscale",
|
||||
"wireguard"
|
||||
],
|
||||
"source_mac_address": [
|
||||
"00:11:22:33:44:55"
|
||||
],
|
||||
"source_hostname": [
|
||||
"my-device"
|
||||
],
|
||||
"rule_set": [
|
||||
"geoip-cn",
|
||||
"geosite-cn"
|
||||
@@ -196,7 +208,7 @@ icon: material/new-box
|
||||
(`source_port` || `source_port_range`) &&
|
||||
`other fields`
|
||||
|
||||
另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。
|
||||
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。
|
||||
|
||||
#### inbound
|
||||
|
||||
@@ -254,7 +266,7 @@ icon: material/new-box
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geosite)。
|
||||
Geosite 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geosite-到规则集)。
|
||||
|
||||
匹配 Geosite。
|
||||
|
||||
@@ -262,7 +274,7 @@ icon: material/new-box
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
|
||||
|
||||
匹配源 GeoIP。
|
||||
|
||||
@@ -270,7 +282,7 @@ icon: material/new-box
|
||||
|
||||
!!! failure "已在 sing-box 1.8.0 废弃"
|
||||
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#geoip)。
|
||||
GeoIP 已废弃且可能在不久的将来移除,参阅 [迁移指南](/zh/migration/#迁移-geoip-到规则集)。
|
||||
|
||||
匹配 GeoIP。
|
||||
|
||||
@@ -446,6 +458,26 @@ icon: material/new-box
|
||||
| `tailscale` | 匹配 MagicDNS 域名和对端的 allowed IPs |
|
||||
| `wireguard` | 匹配对端的 allowed IPs |
|
||||
|
||||
#### source_mac_address
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备 MAC 地址。
|
||||
|
||||
#### source_hostname
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
!!! quote ""
|
||||
|
||||
仅支持 Linux、macOS,或在 Android 和 macOS 图形客户端中支持。参阅 [邻居解析](/configuration/shared/neighbor/) 了解设置方法。
|
||||
|
||||
匹配源设备从 DHCP 租约获取的主机名。
|
||||
|
||||
#### rule_set
|
||||
|
||||
!!! question "自 sing-box 1.8.0 起"
|
||||
@@ -500,4 +532,4 @@ icon: material/new-box
|
||||
|
||||
==必填==
|
||||
|
||||
包括的规则。
|
||||
包括的规则。
|
||||
|
||||
@@ -66,7 +66,7 @@ icon: material/new-box
|
||||
|
||||
目标出站的标签。
|
||||
|
||||
如果未指定,规则仅在来自 auto redirect 的[预匹配](/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。
|
||||
如果未指定,规则仅在来自 auto redirect 的[预匹配](/zh/configuration/shared/pre-match/)中匹配,在其他场景中将被跳过。
|
||||
|
||||
#### route-options 字段
|
||||
|
||||
@@ -154,22 +154,22 @@ icon: material/new-box
|
||||
|
||||
#### network_strategy
|
||||
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#network_strategy)。
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_strategy)。
|
||||
|
||||
仅当出站为 `direct` 且 `outbound.bind_interface`, `outbound.inet4_bind_address`
|
||||
且 `outbound.inet6_bind_address` 未设置时生效。
|
||||
|
||||
#### network_type
|
||||
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#network_type)。
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#network_type)。
|
||||
|
||||
#### fallback_network_type
|
||||
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_network_type)。
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_network_type)。
|
||||
|
||||
#### fallback_delay
|
||||
|
||||
详情参阅 [拨号字段](/configuration/shared/dial/#fallback_delay)。
|
||||
详情参阅 [拨号字段](/zh/configuration/shared/dial/#fallback_delay)。
|
||||
|
||||
#### udp_disable_domain_unmapping
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ icon: material/new-box
|
||||
!!! quote "sing-box 1.11.0 中的更改"
|
||||
|
||||
:material-plus: [network_type](#network_type)
|
||||
:material-alert: [network_is_expensive](#network_is_expensive)
|
||||
:material-alert: [network_is_constrained](#network_is_constrained)
|
||||
:material-plus: [network_is_expensive](#network_is_expensive)
|
||||
:material-plus: [network_is_constrained](#network_is_constrained)
|
||||
|
||||
### 结构
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Claude Code OAuth 凭据文件的路径。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### 示例
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ DERP 服务是一个 Tailscale DERP 服务器,类似于 [derper](https://pkg.g
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
#### config_path
|
||||
|
||||
@@ -96,7 +96,7 @@ Derper 配置文件路径。
|
||||
- `server`:**必填** DERP 服务器地址。
|
||||
- `server_port`:**必填** DERP 服务器端口。
|
||||
- `host`:自定义 DERP 主机名。
|
||||
- `tls`:[TLS](/zh/configuration/shared/tls/#outbound)
|
||||
- `tls`:[TLS](/zh/configuration/shared/tls/#出站)
|
||||
- `拨号字段`:[拨号字段](/zh/configuration/shared/dial/)
|
||||
|
||||
#### mesh_psk
|
||||
|
||||
@@ -90,7 +90,7 @@ OpenAI OAuth 凭据文件的路径。
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
|
||||
### 示例
|
||||
|
||||
|
||||
@@ -55,4 +55,4 @@ SSM API 服务是一个用于管理 Shadowsocks 服务器的 RESTful API 服务
|
||||
|
||||
#### tls
|
||||
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#inbound)。
|
||||
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#入站)。
|
||||
162
docs/configuration/shared/certificate-provider/acme.md
Normal file
162
docs/configuration/shared/certificate-provider/acme.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
:material-plus: [account_key](#account_key)
|
||||
:material-plus: [key_type](#key_type)
|
||||
:material-plus: [profile](#profile)
|
||||
:material-plus: [detour](#detour)
|
||||
|
||||
# ACME
|
||||
|
||||
!!! quote ""
|
||||
|
||||
`with_acme` build tag required.
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "acme",
|
||||
"tag": "",
|
||||
|
||||
"domain": [],
|
||||
"data_directory": "",
|
||||
"default_server_name": "",
|
||||
"email": "",
|
||||
"provider": "",
|
||||
"account_key": "",
|
||||
"disable_http_challenge": false,
|
||||
"disable_tls_alpn_challenge": false,
|
||||
"alternative_http_port": 0,
|
||||
"alternative_tls_port": 0,
|
||||
"external_account": {
|
||||
"key_id": "",
|
||||
"mac_key": ""
|
||||
},
|
||||
"dns01_challenge": {},
|
||||
"key_type": "",
|
||||
"profile": "",
|
||||
"detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### domain
|
||||
|
||||
==Required==
|
||||
|
||||
List of domains.
|
||||
|
||||
#### data_directory
|
||||
|
||||
The directory to store ACME data.
|
||||
|
||||
`$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic` will be used if empty.
|
||||
|
||||
#### default_server_name
|
||||
|
||||
Server name to use when choosing a certificate if the ClientHello's ServerName field is empty.
|
||||
|
||||
#### email
|
||||
|
||||
The email address to use when creating or selecting an existing ACME server account.
|
||||
|
||||
#### provider
|
||||
|
||||
The ACME CA provider to use.
|
||||
|
||||
| Value | Provider |
|
||||
|-------------------------|---------------|
|
||||
| `letsencrypt (default)` | Let's Encrypt |
|
||||
| `zerossl` | ZeroSSL |
|
||||
| `https://...` | Custom |
|
||||
|
||||
When `provider` is `zerossl`, sing-box will automatically request ZeroSSL EAB credentials if `email` is set and
|
||||
`external_account` is empty.
|
||||
|
||||
When `provider` is `zerossl`, at least one of `external_account`, `email`, or `account_key` is required.
|
||||
|
||||
#### account_key
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
The PEM-encoded private key of an existing ACME account.
|
||||
|
||||
#### disable_http_challenge
|
||||
|
||||
Disable all HTTP challenges.
|
||||
|
||||
#### disable_tls_alpn_challenge
|
||||
|
||||
Disable all TLS-ALPN challenges
|
||||
|
||||
#### alternative_http_port
|
||||
|
||||
The alternate port to use for the ACME HTTP challenge; if non-empty, this port will be used instead of 80 to spin up a
|
||||
listener for the HTTP challenge.
|
||||
|
||||
#### alternative_tls_port
|
||||
|
||||
The alternate port to use for the ACME TLS-ALPN challenge; the system must forward 443 to this port for challenge to
|
||||
succeed.
|
||||
|
||||
#### external_account
|
||||
|
||||
EAB (External Account Binding) contains information necessary to bind or map an ACME account to some other account known
|
||||
by the CA.
|
||||
|
||||
External account bindings are used to associate an ACME account with an existing account in a non-ACME system, such as
|
||||
a CA customer database.
|
||||
|
||||
To enable ACME account binding, the CA operating the ACME server needs to provide the ACME client with a MAC key and a
|
||||
key identifier, using some mechanism outside of ACME. §7.3.4
|
||||
|
||||
#### external_account.key_id
|
||||
|
||||
The key identifier.
|
||||
|
||||
#### external_account.mac_key
|
||||
|
||||
The MAC key.
|
||||
|
||||
#### dns01_challenge
|
||||
|
||||
ACME DNS01 challenge field. If configured, other challenge methods will be disabled.
|
||||
|
||||
See [DNS01 Challenge Fields](/configuration/shared/dns01_challenge/) for details.
|
||||
|
||||
#### key_type
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
The private key type to generate for new certificates.
|
||||
|
||||
| Value | Type |
|
||||
|------------|---------|
|
||||
| `ed25519` | Ed25519 |
|
||||
| `p256` | P-256 |
|
||||
| `p384` | P-384 |
|
||||
| `rsa2048` | RSA |
|
||||
| `rsa4096` | RSA |
|
||||
|
||||
#### profile
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
The ACME profile name to use for certificate orders.
|
||||
|
||||
See [ACME Profiles](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/) for details.
|
||||
|
||||
When using Let's Encrypt with IP address identifiers, the `shortlived` profile is automatically selected if not set.
|
||||
|
||||
#### detour
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
The tag of the upstream outbound.
|
||||
|
||||
All provider HTTP requests will use this outbound.
|
||||
157
docs/configuration/shared/certificate-provider/acme.zh.md
Normal file
157
docs/configuration/shared/certificate-provider/acme.zh.md
Normal file
@@ -0,0 +1,157 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
:material-plus: [account_key](#account_key)
|
||||
:material-plus: [key_type](#key_type)
|
||||
:material-plus: [profile](#profile)
|
||||
:material-plus: [detour](#detour)
|
||||
|
||||
# ACME
|
||||
|
||||
!!! quote ""
|
||||
|
||||
需要 `with_acme` 构建标签。
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "acme",
|
||||
"tag": "",
|
||||
|
||||
"domain": [],
|
||||
"data_directory": "",
|
||||
"default_server_name": "",
|
||||
"email": "",
|
||||
"provider": "",
|
||||
"account_key": "",
|
||||
"disable_http_challenge": false,
|
||||
"disable_tls_alpn_challenge": false,
|
||||
"alternative_http_port": 0,
|
||||
"alternative_tls_port": 0,
|
||||
"external_account": {
|
||||
"key_id": "",
|
||||
"mac_key": ""
|
||||
},
|
||||
"dns01_challenge": {},
|
||||
"key_type": "",
|
||||
"profile": "",
|
||||
"detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### domain
|
||||
|
||||
==必填==
|
||||
|
||||
域名列表。
|
||||
|
||||
#### data_directory
|
||||
|
||||
ACME 数据存储目录。
|
||||
|
||||
如果为空则使用 `$XDG_DATA_HOME/certmagic|$HOME/.local/share/certmagic`。
|
||||
|
||||
#### default_server_name
|
||||
|
||||
如果 ClientHello 的 ServerName 字段为空,则选择证书时要使用的服务器名称。
|
||||
|
||||
#### email
|
||||
|
||||
创建或选择现有 ACME 服务器帐户时使用的电子邮件地址。
|
||||
|
||||
#### provider
|
||||
|
||||
要使用的 ACME CA 提供商。
|
||||
|
||||
| 值 | 提供商 |
|
||||
|--------------------|---------------|
|
||||
| `letsencrypt (默认)` | Let's Encrypt |
|
||||
| `zerossl` | ZeroSSL |
|
||||
| `https://...` | 自定义 |
|
||||
|
||||
当 `provider` 为 `zerossl` 时,如果设置了 `email` 且未设置 `external_account`,
|
||||
sing-box 会自动向 ZeroSSL 请求 EAB 凭据。
|
||||
|
||||
当 `provider` 为 `zerossl` 时,必须至少设置 `external_account`、`email` 或 `account_key` 之一。
|
||||
|
||||
#### account_key
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
现有 ACME 帐户的 PEM 编码私钥。
|
||||
|
||||
#### disable_http_challenge
|
||||
|
||||
禁用所有 HTTP 质询。
|
||||
|
||||
#### disable_tls_alpn_challenge
|
||||
|
||||
禁用所有 TLS-ALPN 质询。
|
||||
|
||||
#### alternative_http_port
|
||||
|
||||
用于 ACME HTTP 质询的备用端口;如果非空,将使用此端口而不是 80 来启动 HTTP 质询的侦听器。
|
||||
|
||||
#### alternative_tls_port
|
||||
|
||||
用于 ACME TLS-ALPN 质询的备用端口; 系统必须将 443 转发到此端口以使质询成功。
|
||||
|
||||
#### external_account
|
||||
|
||||
EAB(外部帐户绑定)包含将 ACME 帐户绑定或映射到 CA 已知的其他帐户所需的信息。
|
||||
|
||||
外部帐户绑定用于将 ACME 帐户与非 ACME 系统中的现有帐户相关联,例如 CA 客户数据库。
|
||||
|
||||
为了启用 ACME 帐户绑定,运行 ACME 服务器的 CA 需要使用 ACME 之外的某种机制向 ACME 客户端提供 MAC 密钥和密钥标识符。§7.3.4
|
||||
|
||||
#### external_account.key_id
|
||||
|
||||
密钥标识符。
|
||||
|
||||
#### external_account.mac_key
|
||||
|
||||
MAC 密钥。
|
||||
|
||||
#### dns01_challenge
|
||||
|
||||
ACME DNS01 质询字段。如果配置,将禁用其他质询方法。
|
||||
|
||||
参阅 [DNS01 质询字段](/zh/configuration/shared/dns01_challenge/)。
|
||||
|
||||
#### key_type
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
为新证书生成的私钥类型。
|
||||
|
||||
| 值 | 类型 |
|
||||
|-----------|----------|
|
||||
| `ed25519` | Ed25519 |
|
||||
| `p256` | P-256 |
|
||||
| `p384` | P-384 |
|
||||
| `rsa2048` | RSA |
|
||||
| `rsa4096` | RSA |
|
||||
|
||||
#### profile
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
用于证书订单的 ACME 配置文件名称。
|
||||
|
||||
参阅 [ACME Profiles](https://datatracker.ietf.org/doc/draft-aaron-acme-profiles/)。
|
||||
|
||||
当使用 Let's Encrypt 且包含 IP 地址标识符时,如果未设置,将自动选择 `shortlived` 配置文件。
|
||||
|
||||
#### detour
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
上游出站的标签。
|
||||
|
||||
所有提供者 HTTP 请求将使用此出站。
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
# Cloudflare Origin CA
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "cloudflare-origin-ca",
|
||||
"tag": "",
|
||||
|
||||
"domain": [],
|
||||
"data_directory": "",
|
||||
"api_token": "",
|
||||
"origin_ca_key": "",
|
||||
"request_type": "",
|
||||
"requested_validity": 0,
|
||||
"detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
#### domain
|
||||
|
||||
==Required==
|
||||
|
||||
List of domain names or wildcard domain names to include in the certificate.
|
||||
|
||||
#### data_directory
|
||||
|
||||
Root directory used to store the issued certificate, private key, and metadata.
|
||||
|
||||
If empty, sing-box uses the same default data directory as the ACME certificate provider:
|
||||
`$XDG_DATA_HOME/certmagic` or `$HOME/.local/share/certmagic`.
|
||||
|
||||
#### api_token
|
||||
|
||||
Cloudflare API token used to create the certificate.
|
||||
|
||||
Get or create one in [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens).
|
||||
|
||||
Requires the `Zone / SSL and Certificates / Edit` permission.
|
||||
|
||||
Conflict with `origin_ca_key`.
|
||||
|
||||
#### origin_ca_key
|
||||
|
||||
Cloudflare Origin CA Key.
|
||||
|
||||
Get it in [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens).
|
||||
|
||||
Conflict with `api_token`.
|
||||
|
||||
#### request_type
|
||||
|
||||
The signature type to request from Cloudflare.
|
||||
|
||||
| Value | Type |
|
||||
|----------------------|-------------|
|
||||
| `origin-rsa` | RSA |
|
||||
| `origin-ecc` | ECDSA P-256 |
|
||||
|
||||
`origin-rsa` is used if empty.
|
||||
|
||||
#### requested_validity
|
||||
|
||||
The requested certificate validity in days.
|
||||
|
||||
Available values: `7`, `30`, `90`, `365`, `730`, `1095`, `5475`.
|
||||
|
||||
`5475` days (15 years) is used if empty.
|
||||
|
||||
#### detour
|
||||
|
||||
The tag of the upstream outbound.
|
||||
|
||||
All provider HTTP requests will use this outbound.
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
# Cloudflare Origin CA
|
||||
|
||||
### 结构
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "cloudflare-origin-ca",
|
||||
"tag": "",
|
||||
|
||||
"domain": [],
|
||||
"data_directory": "",
|
||||
"api_token": "",
|
||||
"origin_ca_key": "",
|
||||
"request_type": "",
|
||||
"requested_validity": 0,
|
||||
"detour": ""
|
||||
}
|
||||
```
|
||||
|
||||
### 字段
|
||||
|
||||
#### domain
|
||||
|
||||
==必填==
|
||||
|
||||
要写入证书的域名或通配符域名列表。
|
||||
|
||||
#### data_directory
|
||||
|
||||
保存签发证书、私钥和元数据的根目录。
|
||||
|
||||
如果为空,sing-box 会使用与 ACME 证书提供者相同的默认数据目录:
|
||||
`$XDG_DATA_HOME/certmagic` 或 `$HOME/.local/share/certmagic`。
|
||||
|
||||
#### api_token
|
||||
|
||||
用于创建证书的 Cloudflare API Token。
|
||||
|
||||
可在 [Cloudflare Dashboard > My Profile > API Tokens](https://dash.cloudflare.com/profile/api-tokens) 获取或创建。
|
||||
|
||||
需要 `Zone / SSL and Certificates / Edit` 权限。
|
||||
|
||||
与 `origin_ca_key` 冲突。
|
||||
|
||||
#### origin_ca_key
|
||||
|
||||
Cloudflare Origin CA Key。
|
||||
|
||||
可在 [Cloudflare Dashboard > My Profile > API Tokens > API Keys > Origin CA Key](https://dash.cloudflare.com/profile/api-tokens) 获取。
|
||||
|
||||
与 `api_token` 冲突。
|
||||
|
||||
#### request_type
|
||||
|
||||
向 Cloudflare 请求的签名类型。
|
||||
|
||||
| 值 | 类型 |
|
||||
|----------------------|-------------|
|
||||
| `origin-rsa` | RSA |
|
||||
| `origin-ecc` | ECDSA P-256 |
|
||||
|
||||
如果为空,使用 `origin-rsa`。
|
||||
|
||||
#### requested_validity
|
||||
|
||||
请求的证书有效期,单位为天。
|
||||
|
||||
可用值:`7`、`30`、`90`、`365`、`730`、`1095`、`5475`。
|
||||
|
||||
如果为空,使用 `5475` 天(15 年)。
|
||||
|
||||
#### detour
|
||||
|
||||
上游出站的标签。
|
||||
|
||||
所有提供者 HTTP 请求将使用此出站。
|
||||
32
docs/configuration/shared/certificate-provider/index.md
Normal file
32
docs/configuration/shared/certificate-provider/index.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
icon: material/new-box
|
||||
---
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
# Certificate Provider
|
||||
|
||||
### Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"certificate_providers": [
|
||||
{
|
||||
"type": "",
|
||||
"tag": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Fields
|
||||
|
||||
| Type | Format |
|
||||
|--------|------------------|
|
||||
| `acme` | [ACME](/configuration/shared/certificate-provider/acme) |
|
||||
| `tailscale` | [Tailscale](/configuration/shared/certificate-provider/tailscale) |
|
||||
| `cloudflare-origin-ca` | [Cloudflare Origin CA](/configuration/shared/certificate-provider/cloudflare-origin-ca) |
|
||||
|
||||
#### tag
|
||||
|
||||
The tag of the certificate provider.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user