mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-13 20:28:32 +10:00
Compare commits
8 Commits
testing
...
v1.14.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc6017418 | ||
|
|
b4abffeb73 | ||
|
|
bf2aa55e63 | ||
|
|
bf202367cd | ||
|
|
2f876770ee | ||
|
|
c243637d07 | ||
|
|
e2e39f16db | ||
|
|
9b35890324 |
@@ -4,7 +4,6 @@
|
|||||||
--license GPL-3.0-or-later
|
--license GPL-3.0-or-later
|
||||||
--description "The universal proxy platform."
|
--description "The universal proxy platform."
|
||||||
--url "https://sing-box.sagernet.org/"
|
--url "https://sing-box.sagernet.org/"
|
||||||
--vendor SagerNet
|
|
||||||
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
--maintainer "nekohasekai <contact-git@sekai.icu>"
|
||||||
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
--deb-field "Bug: https://github.com/SagerNet/sing-box/issues"
|
||||||
--no-deb-generate-changes
|
--no-deb-generate-changes
|
||||||
|
|||||||
2
.github/CRONET_GO_VERSION
vendored
2
.github/CRONET_GO_VERSION
vendored
@@ -1 +1 @@
|
|||||||
335e5bef5d88fc4474c9a70b865561f45a67de83
|
ea7cd33752aed62603775af3df946c1b83f4b0b3
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package adapter
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"time"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -26,19 +25,18 @@ type DNSRouter interface {
|
|||||||
|
|
||||||
type DNSClient interface {
|
type DNSClient interface {
|
||||||
Start()
|
Start()
|
||||||
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error)
|
Exchange(ctx context.Context, transport DNSTransport, message *dns.Msg, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error)
|
||||||
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error)
|
Lookup(ctx context.Context, transport DNSTransport, domain string, options DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error)
|
||||||
ClearCache()
|
ClearCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSQueryOptions struct {
|
type DNSQueryOptions struct {
|
||||||
Transport DNSTransport
|
Transport DNSTransport
|
||||||
Strategy C.DomainStrategy
|
Strategy C.DomainStrategy
|
||||||
LookupStrategy C.DomainStrategy
|
LookupStrategy C.DomainStrategy
|
||||||
DisableCache bool
|
DisableCache bool
|
||||||
DisableOptimisticCache bool
|
RewriteTTL *uint32
|
||||||
RewriteTTL *uint32
|
ClientSubnet netip.Prefix
|
||||||
ClientSubnet netip.Prefix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptions) (*DNSQueryOptions, error) {
|
||||||
@@ -51,12 +49,11 @@ func DNSQueryOptionsFrom(ctx context.Context, options *option.DomainResolveOptio
|
|||||||
return nil, E.New("domain resolver not found: " + options.Server)
|
return nil, E.New("domain resolver not found: " + options.Server)
|
||||||
}
|
}
|
||||||
return &DNSQueryOptions{
|
return &DNSQueryOptions{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Strategy: C.DomainStrategy(options.Strategy),
|
Strategy: C.DomainStrategy(options.Strategy),
|
||||||
DisableCache: options.DisableCache,
|
DisableCache: options.DisableCache,
|
||||||
DisableOptimisticCache: options.DisableOptimisticCache,
|
RewriteTTL: options.RewriteTTL,
|
||||||
RewriteTTL: options.RewriteTTL,
|
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
||||||
ClientSubnet: options.ClientSubnet.Build(netip.Prefix{}),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,13 +63,6 @@ type RDRCStore interface {
|
|||||||
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
SaveRDRCAsync(transportName string, qName string, qType uint16, logger logger.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
type DNSCacheStore interface {
|
|
||||||
LoadDNSCache(transportName string, qName string, qType uint16) (rawMessage []byte, expireAt time.Time, loaded bool)
|
|
||||||
SaveDNSCache(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time) error
|
|
||||||
SaveDNSCacheAsync(transportName string, qName string, qType uint16, rawMessage []byte, expireAt time.Time, logger logger.Logger)
|
|
||||||
ClearDNSCache() error
|
|
||||||
}
|
|
||||||
|
|
||||||
type DNSTransport interface {
|
type DNSTransport interface {
|
||||||
Lifecycle
|
Lifecycle
|
||||||
Type() string
|
Type() string
|
||||||
@@ -82,6 +72,11 @@ type DNSTransport interface {
|
|||||||
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
Exchange(ctx context.Context, message *dns.Msg) (*dns.Msg, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LegacyDNSTransport interface {
|
||||||
|
LegacyStrategy() C.DomainStrategy
|
||||||
|
LegacyClientSubnet() netip.Prefix
|
||||||
|
}
|
||||||
|
|
||||||
type DNSTransportRegistry interface {
|
type DNSTransportRegistry interface {
|
||||||
option.DNSTransportOptionsRegistry
|
option.DNSTransportOptionsRegistry
|
||||||
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
CreateDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, transportType string, options any) (DNSTransport, error)
|
||||||
|
|||||||
@@ -47,12 +47,6 @@ type CacheFile interface {
|
|||||||
StoreRDRC() bool
|
StoreRDRC() bool
|
||||||
RDRCStore
|
RDRCStore
|
||||||
|
|
||||||
StoreDNS() bool
|
|
||||||
DNSCacheStore
|
|
||||||
|
|
||||||
SetDisableExpire(disableExpire bool)
|
|
||||||
SetOptimisticTimeout(timeout time.Duration)
|
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
LoadSelected(group string) string
|
LoadSelected(group string) string
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
type HTTPClientManager interface {
|
|
||||||
ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error)
|
|
||||||
DefaultTransport() http.RoundTripper
|
|
||||||
}
|
|
||||||
@@ -10,8 +10,6 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Inbound interface {
|
type Inbound interface {
|
||||||
@@ -81,16 +79,14 @@ type InboundContext struct {
|
|||||||
FallbackNetworkType []C.InterfaceType
|
FallbackNetworkType []C.InterfaceType
|
||||||
FallbackDelay time.Duration
|
FallbackDelay time.Duration
|
||||||
|
|
||||||
DestinationAddresses []netip.Addr
|
DestinationAddresses []netip.Addr
|
||||||
DNSResponse *dns.Msg
|
SourceGeoIPCode string
|
||||||
DestinationAddressMatchFromResponse bool
|
GeoIPCode string
|
||||||
SourceGeoIPCode string
|
ProcessInfo *ConnectionOwner
|
||||||
GeoIPCode string
|
SourceMACAddress net.HardwareAddr
|
||||||
ProcessInfo *ConnectionOwner
|
SourceHostname string
|
||||||
SourceMACAddress net.HardwareAddr
|
QueryType uint16
|
||||||
SourceHostname string
|
FakeIP bool
|
||||||
QueryType uint16
|
|
||||||
FakeIP bool
|
|
||||||
|
|
||||||
// rule cache
|
// rule cache
|
||||||
|
|
||||||
@@ -119,51 +115,6 @@ func (c *InboundContext) ResetRuleMatchCache() {
|
|||||||
c.DidMatch = false
|
c.DidMatch = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *InboundContext) DNSResponseAddressesForMatch() []netip.Addr {
|
|
||||||
return DNSResponseAddresses(c.DNSResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DNSResponseAddresses(response *dns.Msg) []netip.Addr {
|
|
||||||
if response == nil || response.Rcode != dns.RcodeSuccess {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
addresses := make([]netip.Addr, 0, len(response.Answer))
|
|
||||||
for _, rawRecord := range response.Answer {
|
|
||||||
switch record := rawRecord.(type) {
|
|
||||||
case *dns.A:
|
|
||||||
addr := M.AddrFromIP(record.A)
|
|
||||||
if addr.IsValid() {
|
|
||||||
addresses = append(addresses, addr)
|
|
||||||
}
|
|
||||||
case *dns.AAAA:
|
|
||||||
addr := M.AddrFromIP(record.AAAA)
|
|
||||||
if addr.IsValid() {
|
|
||||||
addresses = append(addresses, addr)
|
|
||||||
}
|
|
||||||
case *dns.HTTPS:
|
|
||||||
for _, value := range record.SVCB.Value {
|
|
||||||
switch hint := value.(type) {
|
|
||||||
case *dns.SVCBIPv4Hint:
|
|
||||||
for _, ip := range hint.Hint {
|
|
||||||
addr := M.AddrFromIP(ip).Unmap()
|
|
||||||
if addr.IsValid() {
|
|
||||||
addresses = append(addresses, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case *dns.SVCBIPv6Hint:
|
|
||||||
for _, ip := range hint.Hint {
|
|
||||||
addr := M.AddrFromIP(ip)
|
|
||||||
if addr.IsValid() {
|
|
||||||
addresses = append(addresses, addr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return addresses
|
|
||||||
}
|
|
||||||
|
|
||||||
type inboundContextKey struct{}
|
type inboundContextKey struct{}
|
||||||
|
|
||||||
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
func WithContext(ctx context.Context, inboundContext *InboundContext) context.Context {
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDNSResponseAddressesUnmapsHTTPSIPv4Hints(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ipv4Hint := net.ParseIP("1.1.1.1")
|
|
||||||
require.NotNil(t, ipv4Hint)
|
|
||||||
|
|
||||||
response := &dns.Msg{
|
|
||||||
MsgHdr: dns.MsgHdr{
|
|
||||||
Response: true,
|
|
||||||
Rcode: dns.RcodeSuccess,
|
|
||||||
},
|
|
||||||
Answer: []dns.RR{
|
|
||||||
&dns.HTTPS{
|
|
||||||
SVCB: dns.SVCB{
|
|
||||||
Hdr: dns.RR_Header{
|
|
||||||
Name: dns.Fqdn("example.com"),
|
|
||||||
Rrtype: dns.TypeHTTPS,
|
|
||||||
Class: dns.ClassINET,
|
|
||||||
Ttl: 60,
|
|
||||||
},
|
|
||||||
Priority: 1,
|
|
||||||
Target: ".",
|
|
||||||
Value: []dns.SVCBKeyValue{
|
|
||||||
&dns.SVCBIPv4Hint{Hint: []net.IP{ipv4Hint}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
addresses := DNSResponseAddresses(response)
|
|
||||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("1.1.1.1")}, addresses)
|
|
||||||
require.True(t, addresses[0].Is4())
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,17 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-tun"
|
"github.com/sagernet/sing-tun"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
|
|
||||||
"go4.org/netipx"
|
"go4.org/netipx"
|
||||||
@@ -45,7 +51,7 @@ type ConnectionRouterEx interface {
|
|||||||
|
|
||||||
type RuleSet interface {
|
type RuleSet interface {
|
||||||
Name() string
|
Name() string
|
||||||
StartContext(ctx context.Context) error
|
StartContext(ctx context.Context, startContext *HTTPStartContext) error
|
||||||
PostStart() error
|
PostStart() error
|
||||||
Metadata() RuleSetMetadata
|
Metadata() RuleSetMetadata
|
||||||
ExtractIPSet() []*netipx.IPSet
|
ExtractIPSet() []*netipx.IPSet
|
||||||
@@ -60,14 +66,51 @@ type RuleSet interface {
|
|||||||
|
|
||||||
type RuleSetUpdateCallback func(it RuleSet)
|
type RuleSetUpdateCallback func(it RuleSet)
|
||||||
|
|
||||||
type DNSRuleSetUpdateValidator interface {
|
type RuleSetMetadata struct {
|
||||||
ValidateRuleSetMetadataUpdate(tag string, metadata RuleSetMetadata) error
|
ContainsProcessRule bool
|
||||||
|
ContainsWIFIRule bool
|
||||||
|
ContainsIPCIDRRule bool
|
||||||
|
}
|
||||||
|
type HTTPStartContext struct {
|
||||||
|
ctx context.Context
|
||||||
|
access sync.Mutex
|
||||||
|
httpClientCache map[string]*http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
// ip_version is not a headless-rule item, so ContainsIPVersionRule is intentionally absent.
|
func NewHTTPStartContext(ctx context.Context) *HTTPStartContext {
|
||||||
type RuleSetMetadata struct {
|
return &HTTPStartContext{
|
||||||
ContainsProcessRule bool
|
ctx: ctx,
|
||||||
ContainsWIFIRule bool
|
httpClientCache: make(map[string]*http.Client),
|
||||||
ContainsIPCIDRRule bool
|
}
|
||||||
ContainsDNSQueryTypeRule bool
|
}
|
||||||
|
|
||||||
|
func (c *HTTPStartContext) HTTPClient(detour string, dialer N.Dialer) *http.Client {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
if httpClient, loaded := c.httpClientCache[detour]; loaded {
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
ForceAttemptHTTP2: true,
|
||||||
|
TLSHandshakeTimeout: C.TCPTimeout,
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
|
},
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
Time: ntp.TimeFuncFromContext(c.ctx),
|
||||||
|
RootCAs: RootPoolFromContext(c.ctx),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
c.httpClientCache[detour] = httpClient
|
||||||
|
return httpClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HTTPStartContext) Close() {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
for _, client := range c.httpClientCache {
|
||||||
|
client.CloseIdleConnections()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package adapter
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
|
||||||
"github.com/miekg/dns"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type HeadlessRule interface {
|
type HeadlessRule interface {
|
||||||
@@ -20,9 +18,8 @@ type Rule interface {
|
|||||||
|
|
||||||
type DNSRule interface {
|
type DNSRule interface {
|
||||||
Rule
|
Rule
|
||||||
LegacyPreMatch(metadata *InboundContext) bool
|
|
||||||
WithAddressLimit() bool
|
WithAddressLimit() bool
|
||||||
MatchAddressLimit(metadata *InboundContext, response *dns.Msg) bool
|
MatchAddressLimit(metadata *InboundContext) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleAction interface {
|
type RuleAction interface {
|
||||||
@@ -32,7 +29,7 @@ type RuleAction interface {
|
|||||||
|
|
||||||
func IsFinalAction(action RuleAction) bool {
|
func IsFinalAction(action RuleAction) bool {
|
||||||
switch action.Type() {
|
switch action.Type() {
|
||||||
case C.RuleActionTypeSniff, C.RuleActionTypeResolve, C.RuleActionTypeEvaluate:
|
case C.RuleActionTypeSniff, C.RuleActionTypeResolve:
|
||||||
return false
|
return false
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package adapter
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type TailscaleEndpoint interface {
|
|
||||||
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
|
|
||||||
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type TailscalePingResult struct {
|
|
||||||
LatencyMs float64
|
|
||||||
IsDirect bool
|
|
||||||
Endpoint string
|
|
||||||
DERPRegionID int32
|
|
||||||
DERPRegionCode string
|
|
||||||
Error string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TailscaleEndpointStatus struct {
|
|
||||||
BackendState string
|
|
||||||
AuthURL string
|
|
||||||
NetworkName string
|
|
||||||
MagicDNSSuffix string
|
|
||||||
Self *TailscalePeer
|
|
||||||
UserGroups []*TailscaleUserGroup
|
|
||||||
}
|
|
||||||
|
|
||||||
type TailscaleUserGroup struct {
|
|
||||||
UserID int64
|
|
||||||
LoginName string
|
|
||||||
DisplayName string
|
|
||||||
ProfilePicURL string
|
|
||||||
Peers []*TailscalePeer
|
|
||||||
}
|
|
||||||
|
|
||||||
type TailscalePeer struct {
|
|
||||||
HostName string
|
|
||||||
DNSName string
|
|
||||||
OS string
|
|
||||||
TailscaleIPs []string
|
|
||||||
Online bool
|
|
||||||
ExitNode bool
|
|
||||||
ExitNodeOption bool
|
|
||||||
Active bool
|
|
||||||
RxBytes int64
|
|
||||||
TxBytes int64
|
|
||||||
UserID int64
|
|
||||||
KeyExpiry int64
|
|
||||||
}
|
|
||||||
52
box.go
52
box.go
@@ -16,14 +16,13 @@ import (
|
|||||||
boxService "github.com/sagernet/sing-box/adapter/service"
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||||
"github.com/sagernet/sing-box/common/certificate"
|
"github.com/sagernet/sing-box/common/certificate"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
"github.com/sagernet/sing-box/common/httpclient"
|
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/local"
|
||||||
"github.com/sagernet/sing-box/experimental"
|
"github.com/sagernet/sing-box/experimental"
|
||||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing-box/protocol/direct"
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
@@ -52,7 +51,6 @@ type Box struct {
|
|||||||
dnsRouter *dns.Router
|
dnsRouter *dns.Router
|
||||||
connection *route.ConnectionManager
|
connection *route.ConnectionManager
|
||||||
router *route.Router
|
router *route.Router
|
||||||
httpClientService adapter.LifecycleService
|
|
||||||
internalService []adapter.LifecycleService
|
internalService []adapter.LifecycleService
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
}
|
||||||
@@ -172,10 +170,6 @@ func New(options Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var internalServices []adapter.LifecycleService
|
var internalServices []adapter.LifecycleService
|
||||||
routeOptions := common.PtrValueOrDefault(options.Route)
|
|
||||||
httpClientManager := httpclient.NewManager(ctx, logFactory.NewLogger("httpclient"), options.HTTPClients, routeOptions.DefaultHTTPClient)
|
|
||||||
service.MustRegister[adapter.HTTPClientManager](ctx, httpClientManager)
|
|
||||||
httpClientService := adapter.LifecycleService(httpClientManager)
|
|
||||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||||
len(certificateOptions.Certificate) > 0 ||
|
len(certificateOptions.Certificate) > 0 ||
|
||||||
@@ -188,6 +182,8 @@ func New(options Options) (*Box, error) {
|
|||||||
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
internalServices = append(internalServices, certificateStore)
|
internalServices = append(internalServices, certificateStore)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
routeOptions := common.PtrValueOrDefault(options.Route)
|
||||||
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
dnsOptions := common.PtrValueOrDefault(options.DNS)
|
||||||
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||||
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||||
@@ -201,12 +197,8 @@ func New(options Options) (*Box, error) {
|
|||||||
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||||
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||||
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
service.MustRegister[adapter.CertificateProviderManager](ctx, certificateProviderManager)
|
||||||
dnsRouter, err := dns.NewRouter(ctx, logFactory, dnsOptions)
|
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "initialize DNS router")
|
|
||||||
}
|
|
||||||
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||||
service.MustRegister[adapter.DNSRuleSetUpdateValidator](ctx, dnsRouter)
|
|
||||||
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, E.Cause(err, "initialize network manager")
|
return nil, E.Cause(err, "initialize network manager")
|
||||||
@@ -365,20 +357,13 @@ func New(options Options) (*Box, error) {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||||
return dnsTransportRegistry.CreateDNSTransport(
|
return local.NewTransport(
|
||||||
ctx,
|
ctx,
|
||||||
logFactory.NewLogger("dns/local"),
|
logFactory.NewLogger("dns/local"),
|
||||||
"local",
|
"local",
|
||||||
C.DNSTypeLocal,
|
option.LocalDNSServerOptions{},
|
||||||
&option.LocalDNSServerOptions{},
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
httpClientManager.Initialize(func() (*httpclient.Client, error) {
|
|
||||||
deprecated.Report(ctx, deprecated.OptionImplicitDefaultHTTPClient)
|
|
||||||
var httpClientOptions option.HTTPClientOptions
|
|
||||||
httpClientOptions.DefaultOutbound = true
|
|
||||||
return httpclient.NewClient(ctx, logFactory.NewLogger("httpclient"), "", httpClientOptions)
|
|
||||||
})
|
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
err = platformInterface.Initialize(networkManager)
|
err = platformInterface.Initialize(networkManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -386,7 +371,7 @@ func New(options Options) (*Box, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if needCacheFile {
|
if needCacheFile {
|
||||||
cacheFile := cachefile.New(ctx, logFactory.NewLogger("cache-file"), common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
cacheFile := cachefile.New(ctx, common.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||||
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||||
internalServices = append(internalServices, cacheFile)
|
internalServices = append(internalServices, cacheFile)
|
||||||
}
|
}
|
||||||
@@ -439,7 +424,6 @@ func New(options Options) (*Box, error) {
|
|||||||
dnsRouter: dnsRouter,
|
dnsRouter: dnsRouter,
|
||||||
connection: connectionManager,
|
connection: connectionManager,
|
||||||
router: router,
|
router: router,
|
||||||
httpClientService: httpClientService,
|
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
logFactory: logFactory,
|
logFactory: logFactory,
|
||||||
logger: logFactory.Logger(),
|
logger: logFactory.Logger(),
|
||||||
@@ -502,15 +486,7 @@ func (s *Box) preStart() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.network, s.connection)
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.StartNamed(s.logger, adapter.StartStateStart, []adapter.LifecycleService{s.httpClientService})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.Start(s.logger, adapter.StartStateStart, s.router, s.dnsRouter)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -587,14 +563,6 @@ func (s *Box) Close() error {
|
|||||||
})
|
})
|
||||||
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||||
}
|
}
|
||||||
if s.httpClientService != nil {
|
|
||||||
s.logger.Trace("close ", s.httpClientService.Name())
|
|
||||||
startTime := time.Now()
|
|
||||||
err = E.Append(err, s.httpClientService.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close ", s.httpClientService.Name())
|
|
||||||
})
|
|
||||||
s.logger.Trace("close ", s.httpClientService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
|
||||||
}
|
|
||||||
for _, lifecycleService := range s.internalService {
|
for _, lifecycleService := range s.internalService {
|
||||||
s.logger.Trace("close ", lifecycleService.Name())
|
s.logger.Trace("close ", lifecycleService.Name())
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
@@ -628,10 +596,6 @@ func (s *Box) Outbound() adapter.OutboundManager {
|
|||||||
return s.outbound
|
return s.outbound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Box) Endpoint() adapter.EndpointManager {
|
|
||||||
return s.endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Box) LogFactory() log.Factory {
|
func (s *Box) LogFactory() log.Factory {
|
||||||
return s.logFactory
|
return s.logFactory
|
||||||
}
|
}
|
||||||
|
|||||||
Submodule clients/android updated: fea0f3a7ba...7cf3a1b71b
Submodule clients/apple updated: ffbf405b52...bff1a5395f
@@ -5,7 +5,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
@@ -36,9 +35,21 @@ func updateMozillaIncludedRootCAs() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
geoIndex := slices.Index(header, "Geographic Focus")
|
geoIndex := slices.Index(header, "Geographic Focus")
|
||||||
|
nameIndex := slices.Index(header, "Common Name or Certificate Name")
|
||||||
certIndex := slices.Index(header, "PEM Info")
|
certIndex := slices.Index(header, "PEM Info")
|
||||||
|
|
||||||
pemBundle := strings.Builder{}
|
generated := strings.Builder{}
|
||||||
|
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||||
|
|
||||||
|
package certificate
|
||||||
|
|
||||||
|
import "crypto/x509"
|
||||||
|
|
||||||
|
var mozillaIncluded *x509.CertPool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
mozillaIncluded = x509.NewCertPool()
|
||||||
|
`)
|
||||||
for {
|
for {
|
||||||
record, err := reader.Read()
|
record, err := reader.Read()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@@ -49,12 +60,18 @@ func updateMozillaIncludedRootCAs() error {
|
|||||||
if record[geoIndex] == "China" {
|
if record[geoIndex] == "China" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
generated.WriteString("\n // ")
|
||||||
|
generated.WriteString(record[nameIndex])
|
||||||
|
generated.WriteString("\n")
|
||||||
|
generated.WriteString(" mozillaIncluded.AppendCertsFromPEM([]byte(`")
|
||||||
cert := record[certIndex]
|
cert := record[certIndex]
|
||||||
|
// Remove single quotes
|
||||||
cert = cert[1 : len(cert)-1]
|
cert = cert[1 : len(cert)-1]
|
||||||
pemBundle.WriteString(cert)
|
generated.WriteString(cert)
|
||||||
pemBundle.WriteString("\n")
|
generated.WriteString("`))\n")
|
||||||
}
|
}
|
||||||
return writeGeneratedCertificateBundle("mozilla", "mozillaIncluded", pemBundle.String())
|
generated.WriteString("}\n")
|
||||||
|
return os.WriteFile("common/certificate/mozilla.go", []byte(generated.String()), 0o644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchChinaFingerprints() (map[string]bool, error) {
|
func fetchChinaFingerprints() (map[string]bool, error) {
|
||||||
@@ -102,11 +119,23 @@ func updateChromeIncludedRootCAs() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
subjectIndex := slices.Index(header, "Subject")
|
||||||
statusIndex := slices.Index(header, "Google Chrome Status")
|
statusIndex := slices.Index(header, "Google Chrome Status")
|
||||||
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
|
certIndex := slices.Index(header, "X.509 Certificate (PEM)")
|
||||||
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
|
fingerprintIndex := slices.Index(header, "SHA-256 Fingerprint")
|
||||||
|
|
||||||
pemBundle := strings.Builder{}
|
generated := strings.Builder{}
|
||||||
|
generated.WriteString(`// Code generated by 'make update_certificates'. DO NOT EDIT.
|
||||||
|
|
||||||
|
package certificate
|
||||||
|
|
||||||
|
import "crypto/x509"
|
||||||
|
|
||||||
|
var chromeIncluded *x509.CertPool
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
chromeIncluded = x509.NewCertPool()
|
||||||
|
`)
|
||||||
for {
|
for {
|
||||||
record, err := reader.Read()
|
record, err := reader.Read()
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
@@ -120,39 +149,18 @@ func updateChromeIncludedRootCAs() error {
|
|||||||
if chinaFingerprints[record[fingerprintIndex]] {
|
if chinaFingerprints[record[fingerprintIndex]] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
generated.WriteString("\n // ")
|
||||||
|
generated.WriteString(record[subjectIndex])
|
||||||
|
generated.WriteString("\n")
|
||||||
|
generated.WriteString(" chromeIncluded.AppendCertsFromPEM([]byte(`")
|
||||||
cert := record[certIndex]
|
cert := record[certIndex]
|
||||||
|
// Remove single quotes if present
|
||||||
if len(cert) > 0 && cert[0] == '\'' {
|
if len(cert) > 0 && cert[0] == '\'' {
|
||||||
cert = cert[1 : len(cert)-1]
|
cert = cert[1 : len(cert)-1]
|
||||||
}
|
}
|
||||||
pemBundle.WriteString(cert)
|
generated.WriteString(cert)
|
||||||
pemBundle.WriteString("\n")
|
generated.WriteString("`))\n")
|
||||||
}
|
}
|
||||||
return writeGeneratedCertificateBundle("chrome", "chromeIncluded", pemBundle.String())
|
generated.WriteString("}\n")
|
||||||
}
|
return os.WriteFile("common/certificate/chrome.go", []byte(generated.String()), 0o644)
|
||||||
|
|
||||||
func writeGeneratedCertificateBundle(name string, variableName string, pemBundle string) error {
|
|
||||||
goSource := `// Code generated by 'make update_certificates'. DO NOT EDIT.
|
|
||||||
|
|
||||||
package certificate
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/x509"
|
|
||||||
_ "embed"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed ` + name + `.pem
|
|
||||||
var ` + variableName + `PEM string
|
|
||||||
|
|
||||||
var ` + variableName + ` *x509.CertPool
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
` + variableName + ` = x509.NewCertPool()
|
|
||||||
` + variableName + `.AppendCertsFromPEM([]byte(` + variableName + `PEM))
|
|
||||||
}
|
|
||||||
`
|
|
||||||
err := os.WriteFile(filepath.Join("common/certificate", name+".pem"), []byte(pemBundle), 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return os.WriteFile(filepath.Join("common/certificate", name+".go"), []byte(goSource), 0o644)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,11 +82,6 @@ func compileRuleSet(sourcePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
||||||
if version == C.RuleSetVersion5 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
|
||||||
return len(rule.PackageNameRegex) > 0
|
|
||||||
}) {
|
|
||||||
version = C.RuleSetVersion4
|
|
||||||
}
|
|
||||||
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||||
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
||||||
len(rule.DefaultInterfaceAddress) > 0
|
len(rule.DefaultInterfaceAddress) > 0
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/networkquality"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
commandNetworkQualityFlagConfigURL string
|
|
||||||
commandNetworkQualityFlagSerial bool
|
|
||||||
commandNetworkQualityFlagMaxRuntime int
|
|
||||||
commandNetworkQualityFlagHTTP3 bool
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandNetworkQuality = &cobra.Command{
|
|
||||||
Use: "networkquality",
|
|
||||||
Short: "Run a network quality test",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := runNetworkQuality()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandNetworkQuality.Flags().StringVar(
|
|
||||||
&commandNetworkQualityFlagConfigURL,
|
|
||||||
"config-url", "",
|
|
||||||
"Network quality test config URL (default: Apple mensura)",
|
|
||||||
)
|
|
||||||
commandNetworkQuality.Flags().BoolVar(
|
|
||||||
&commandNetworkQualityFlagSerial,
|
|
||||||
"serial", false,
|
|
||||||
"Run download and upload tests sequentially instead of in parallel",
|
|
||||||
)
|
|
||||||
commandNetworkQuality.Flags().IntVar(
|
|
||||||
&commandNetworkQualityFlagMaxRuntime,
|
|
||||||
"max-runtime", int(networkquality.DefaultMaxRuntime/time.Second),
|
|
||||||
"Network quality maximum runtime in seconds",
|
|
||||||
)
|
|
||||||
commandNetworkQuality.Flags().BoolVar(
|
|
||||||
&commandNetworkQualityFlagHTTP3,
|
|
||||||
"http3", false,
|
|
||||||
"Use HTTP/3 (QUIC) for measurement traffic",
|
|
||||||
)
|
|
||||||
commandTools.AddCommand(commandNetworkQuality)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runNetworkQuality() error {
|
|
||||||
instance, err := createPreStartedClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer instance.Close()
|
|
||||||
|
|
||||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := networkquality.NewHTTPClient(dialer)
|
|
||||||
defer httpClient.CloseIdleConnections()
|
|
||||||
|
|
||||||
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(dialer, commandNetworkQualityFlagHTTP3)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr, "==== NETWORK QUALITY TEST ====")
|
|
||||||
|
|
||||||
result, err := networkquality.Run(networkquality.Options{
|
|
||||||
ConfigURL: commandNetworkQualityFlagConfigURL,
|
|
||||||
HTTPClient: httpClient,
|
|
||||||
NewMeasurementClient: measurementClientFactory,
|
|
||||||
Serial: commandNetworkQualityFlagSerial,
|
|
||||||
MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second,
|
|
||||||
Context: globalCtx,
|
|
||||||
OnProgress: func(p networkquality.Progress) {
|
|
||||||
if !commandNetworkQualityFlagSerial && p.Phase != networkquality.PhaseIdle {
|
|
||||||
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d Upload: %s RPM: %d",
|
|
||||||
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM,
|
|
||||||
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch networkquality.Phase(p.Phase) {
|
|
||||||
case networkquality.PhaseIdle:
|
|
||||||
if p.IdleLatencyMs > 0 {
|
|
||||||
fmt.Fprintf(os.Stderr, "\rIdle Latency: %d ms", p.IdleLatencyMs)
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(os.Stderr, "\rMeasuring idle latency...")
|
|
||||||
}
|
|
||||||
case networkquality.PhaseDownload:
|
|
||||||
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d",
|
|
||||||
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM)
|
|
||||||
case networkquality.PhaseUpload:
|
|
||||||
fmt.Fprintf(os.Stderr, "\rUpload: %s RPM: %d",
|
|
||||||
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr)
|
|
||||||
fmt.Fprintln(os.Stderr, strings.Repeat("-", 40))
|
|
||||||
fmt.Fprintf(os.Stderr, "Idle Latency: %d ms\n", result.IdleLatencyMs)
|
|
||||||
fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy)
|
|
||||||
fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy)
|
|
||||||
fmt.Fprintf(os.Stderr, "Download Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.DownloadRPM), result.DownloadRPMAccuracy)
|
|
||||||
fmt.Fprintf(os.Stderr, "Upload Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.UploadRPM), result.UploadRPMAccuracy)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/stun"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
|
||||||
|
|
||||||
var commandSTUNFlagServer string
|
|
||||||
|
|
||||||
var commandSTUN = &cobra.Command{
|
|
||||||
Use: "stun",
|
|
||||||
Short: "Run a STUN test",
|
|
||||||
Args: cobra.NoArgs,
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
err := runSTUN()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
commandSTUN.Flags().StringVarP(&commandSTUNFlagServer, "server", "s", stun.DefaultServer, "STUN server address")
|
|
||||||
commandTools.AddCommand(commandSTUN)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSTUN() error {
|
|
||||||
instance, err := createPreStartedClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer instance.Close()
|
|
||||||
|
|
||||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr, "==== STUN TEST ====")
|
|
||||||
|
|
||||||
result, err := stun.Run(stun.Options{
|
|
||||||
Server: commandSTUNFlagServer,
|
|
||||||
Dialer: dialer,
|
|
||||||
Context: globalCtx,
|
|
||||||
OnProgress: func(p stun.Progress) {
|
|
||||||
switch p.Phase {
|
|
||||||
case stun.PhaseBinding:
|
|
||||||
if p.ExternalAddr != "" {
|
|
||||||
fmt.Fprintf(os.Stderr, "\rExternal Address: %s (%d ms)", p.ExternalAddr, p.LatencyMs)
|
|
||||||
} else {
|
|
||||||
fmt.Fprint(os.Stderr, "\rSending binding request...")
|
|
||||||
}
|
|
||||||
case stun.PhaseNATMapping:
|
|
||||||
fmt.Fprint(os.Stderr, "\rDetecting NAT mapping behavior...")
|
|
||||||
case stun.PhaseNATFiltering:
|
|
||||||
fmt.Fprint(os.Stderr, "\rDetecting NAT filtering behavior...")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr)
|
|
||||||
fmt.Fprintf(os.Stderr, "External Address: %s\n", result.ExternalAddr)
|
|
||||||
fmt.Fprintf(os.Stderr, "Latency: %d ms\n", result.LatencyMs)
|
|
||||||
if result.NATTypeSupported {
|
|
||||||
fmt.Fprintf(os.Stderr, "NAT Mapping: %s\n", result.NATMapping)
|
|
||||||
fmt.Fprintf(os.Stderr, "NAT Filtering: %s\n", result.NATFiltering)
|
|
||||||
} else {
|
|
||||||
fmt.Fprintln(os.Stderr, "NAT Type Detection: not supported by server")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,8 @@ var _ adapter.CertificateStore = (*Store)(nil)
|
|||||||
|
|
||||||
type Store struct {
|
type Store struct {
|
||||||
access sync.RWMutex
|
access sync.RWMutex
|
||||||
store string
|
|
||||||
systemPool *x509.CertPool
|
systemPool *x509.CertPool
|
||||||
currentPool *x509.CertPool
|
currentPool *x509.CertPool
|
||||||
currentPEM []string
|
|
||||||
certificate string
|
certificate string
|
||||||
certificatePaths []string
|
certificatePaths []string
|
||||||
certificateDirectoryPaths []string
|
certificateDirectoryPaths []string
|
||||||
@@ -63,7 +61,6 @@ func NewStore(ctx context.Context, logger logger.Logger, options option.Certific
|
|||||||
return nil, E.New("unknown certificate store: ", options.Store)
|
return nil, E.New("unknown certificate store: ", options.Store)
|
||||||
}
|
}
|
||||||
store := &Store{
|
store := &Store{
|
||||||
store: options.Store,
|
|
||||||
systemPool: systemPool,
|
systemPool: systemPool,
|
||||||
certificate: strings.Join(options.Certificate, "\n"),
|
certificate: strings.Join(options.Certificate, "\n"),
|
||||||
certificatePaths: options.CertificatePath,
|
certificatePaths: options.CertificatePath,
|
||||||
@@ -126,37 +123,19 @@ func (s *Store) Pool() *x509.CertPool {
|
|||||||
return s.currentPool
|
return s.currentPool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) StoreKind() string {
|
|
||||||
return s.store
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) CurrentPEM() []string {
|
|
||||||
s.access.RLock()
|
|
||||||
defer s.access.RUnlock()
|
|
||||||
return append([]string(nil), s.currentPEM...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Store) update() error {
|
func (s *Store) update() error {
|
||||||
s.access.Lock()
|
s.access.Lock()
|
||||||
defer s.access.Unlock()
|
defer s.access.Unlock()
|
||||||
var currentPool *x509.CertPool
|
var currentPool *x509.CertPool
|
||||||
var currentPEM []string
|
|
||||||
if s.systemPool == nil {
|
if s.systemPool == nil {
|
||||||
currentPool = x509.NewCertPool()
|
currentPool = x509.NewCertPool()
|
||||||
} else {
|
} else {
|
||||||
currentPool = s.systemPool.Clone()
|
currentPool = s.systemPool.Clone()
|
||||||
}
|
}
|
||||||
switch s.store {
|
|
||||||
case C.CertificateStoreMozilla:
|
|
||||||
currentPEM = append(currentPEM, mozillaIncludedPEM)
|
|
||||||
case C.CertificateStoreChrome:
|
|
||||||
currentPEM = append(currentPEM, chromeIncludedPEM)
|
|
||||||
}
|
|
||||||
if s.certificate != "" {
|
if s.certificate != "" {
|
||||||
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
if !currentPool.AppendCertsFromPEM([]byte(s.certificate)) {
|
||||||
return E.New("invalid certificate PEM strings")
|
return E.New("invalid certificate PEM strings")
|
||||||
}
|
}
|
||||||
currentPEM = append(currentPEM, s.certificate)
|
|
||||||
}
|
}
|
||||||
for _, path := range s.certificatePaths {
|
for _, path := range s.certificatePaths {
|
||||||
pemContent, err := os.ReadFile(path)
|
pemContent, err := os.ReadFile(path)
|
||||||
@@ -166,7 +145,6 @@ func (s *Store) update() error {
|
|||||||
if !currentPool.AppendCertsFromPEM(pemContent) {
|
if !currentPool.AppendCertsFromPEM(pemContent) {
|
||||||
return E.New("invalid certificate PEM file: ", path)
|
return E.New("invalid certificate PEM file: ", path)
|
||||||
}
|
}
|
||||||
currentPEM = append(currentPEM, string(pemContent))
|
|
||||||
}
|
}
|
||||||
var firstErr error
|
var firstErr error
|
||||||
for _, directoryPath := range s.certificateDirectoryPaths {
|
for _, directoryPath := range s.certificateDirectoryPaths {
|
||||||
@@ -179,8 +157,8 @@ func (s *Store) update() error {
|
|||||||
}
|
}
|
||||||
for _, directoryEntry := range directoryEntries {
|
for _, directoryEntry := range directoryEntries {
|
||||||
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
pemContent, err := os.ReadFile(filepath.Join(directoryPath, directoryEntry.Name()))
|
||||||
if err == nil && currentPool.AppendCertsFromPEM(pemContent) {
|
if err == nil {
|
||||||
currentPEM = append(currentPEM, string(pemContent))
|
currentPool.AppendCertsFromPEM(pemContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -188,7 +166,6 @@ func (s *Store) update() error {
|
|||||||
return firstErr
|
return firstErr
|
||||||
}
|
}
|
||||||
s.currentPool = currentPool
|
s.currentPool = currentPool
|
||||||
s.currentPEM = currentPEM
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,10 +149,7 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
|||||||
} else {
|
} else {
|
||||||
dialer.Timeout = C.TCPConnectTimeout
|
dialer.Timeout = C.TCPConnectTimeout
|
||||||
}
|
}
|
||||||
if options.DisableTCPKeepAlive {
|
if !options.DisableTCPKeepAlive {
|
||||||
dialer.KeepAlive = -1
|
|
||||||
dialer.KeepAliveConfig.Enable = false
|
|
||||||
} else {
|
|
||||||
keepIdle := time.Duration(options.TCPKeepAlive)
|
keepIdle := time.Duration(options.TCPKeepAlive)
|
||||||
if keepIdle == 0 {
|
if keepIdle == 0 {
|
||||||
keepIdle = C.TCPKeepAliveInitial
|
keepIdle = C.TCPKeepAliveInitial
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ type DirectDialer interface {
|
|||||||
type DetourDialer struct {
|
type DetourDialer struct {
|
||||||
outboundManager adapter.OutboundManager
|
outboundManager adapter.OutboundManager
|
||||||
detour string
|
detour string
|
||||||
defaultOutbound bool
|
|
||||||
legacyDNSDialer bool
|
legacyDNSDialer bool
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
initOnce sync.Once
|
initOnce sync.Once
|
||||||
@@ -34,13 +33,6 @@ func NewDetour(outboundManager adapter.OutboundManager, detour string, legacyDNS
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultOutboundDetour(outboundManager adapter.OutboundManager) N.Dialer {
|
|
||||||
return &DetourDialer{
|
|
||||||
outboundManager: outboundManager,
|
|
||||||
defaultOutbound: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func InitializeDetour(dialer N.Dialer) error {
|
func InitializeDetour(dialer N.Dialer) error {
|
||||||
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
detourDialer, isDetour := common.Cast[*DetourDialer](dialer)
|
||||||
if !isDetour {
|
if !isDetour {
|
||||||
@@ -55,18 +47,12 @@ func (d *DetourDialer) Dialer() (N.Dialer, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *DetourDialer) init() {
|
func (d *DetourDialer) init() {
|
||||||
var dialer adapter.Outbound
|
dialer, loaded := d.outboundManager.Outbound(d.detour)
|
||||||
if d.detour != "" {
|
if !loaded {
|
||||||
var loaded bool
|
d.initErr = E.New("outbound detour not found: ", d.detour)
|
||||||
dialer, loaded = d.outboundManager.Outbound(d.detour)
|
return
|
||||||
if !loaded {
|
|
||||||
d.initErr = E.New("outbound detour not found: ", d.detour)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
dialer = d.outboundManager.Default()
|
|
||||||
}
|
}
|
||||||
if !d.defaultOutbound && !d.legacyDNSDialer {
|
if !d.legacyDNSDialer {
|
||||||
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
if directDialer, isDirect := dialer.(DirectDialer); isDirect {
|
||||||
if directDialer.IsEmpty() {
|
if directDialer.IsEmpty() {
|
||||||
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
d.initErr = E.New("detour to an empty direct outbound makes no sense")
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ type Options struct {
|
|||||||
NewDialer bool
|
NewDialer bool
|
||||||
LegacyDNSDialer bool
|
LegacyDNSDialer bool
|
||||||
DirectOutbound bool
|
DirectOutbound bool
|
||||||
DefaultOutbound bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: merge with NewWithOptions
|
// TODO: merge with NewWithOptions
|
||||||
@@ -43,26 +42,19 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
|||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
hasDetour := dialOptions.Detour != "" || options.DefaultOutbound
|
|
||||||
if dialOptions.Detour != "" {
|
if dialOptions.Detour != "" {
|
||||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
||||||
if outboundManager == nil {
|
if outboundManager == nil {
|
||||||
return nil, E.New("missing outbound manager")
|
return nil, E.New("missing outbound manager")
|
||||||
}
|
}
|
||||||
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
dialer = NewDetour(outboundManager, dialOptions.Detour, options.LegacyDNSDialer)
|
||||||
} else if options.DefaultOutbound {
|
|
||||||
outboundManager := service.FromContext[adapter.OutboundManager](options.Context)
|
|
||||||
if outboundManager == nil {
|
|
||||||
return nil, E.New("missing outbound manager")
|
|
||||||
}
|
|
||||||
dialer = NewDefaultOutboundDetour(outboundManager)
|
|
||||||
} else {
|
} else {
|
||||||
dialer, err = NewDefault(options.Context, dialOptions)
|
dialer, err = NewDefault(options.Context, dialOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if options.RemoteIsDomain && (!hasDetour || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
if options.RemoteIsDomain && (dialOptions.Detour == "" || options.ResolverOnDetour || dialOptions.DomainResolver != nil && dialOptions.DomainResolver.Server != "") {
|
||||||
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
networkManager := service.FromContext[adapter.NetworkManager](options.Context)
|
||||||
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
dnsTransport := service.FromContext[adapter.DNSTransportManager](options.Context)
|
||||||
var defaultOptions adapter.NetworkOptions
|
var defaultOptions adapter.NetworkOptions
|
||||||
@@ -95,12 +87,11 @@ func NewWithOptions(options Options) (N.Dialer, error) {
|
|||||||
}
|
}
|
||||||
server = dialOptions.DomainResolver.Server
|
server = dialOptions.DomainResolver.Server
|
||||||
dnsQueryOptions = adapter.DNSQueryOptions{
|
dnsQueryOptions = adapter.DNSQueryOptions{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
Strategy: strategy,
|
Strategy: strategy,
|
||||||
DisableCache: dialOptions.DomainResolver.DisableCache,
|
DisableCache: dialOptions.DomainResolver.DisableCache,
|
||||||
DisableOptimisticCache: dialOptions.DomainResolver.DisableOptimisticCache,
|
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
||||||
RewriteTTL: dialOptions.DomainResolver.RewriteTTL,
|
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
||||||
ClientSubnet: dialOptions.DomainResolver.ClientSubnet.Build(netip.Prefix{}),
|
|
||||||
}
|
}
|
||||||
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
resolveFallbackDelay = time.Duration(dialOptions.FallbackDelay)
|
||||||
} else if options.DirectResolver {
|
} else if options.DirectResolver {
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type httpTransport interface {
|
|
||||||
http.RoundTripper
|
|
||||||
CloseIdleConnections()
|
|
||||||
Clone() httpTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client struct {
|
|
||||||
transport httpTransport
|
|
||||||
headers http.Header
|
|
||||||
host string
|
|
||||||
tag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClient(ctx context.Context, logger logger.ContextLogger, tag string, options option.HTTPClientOptions) (*Client, error) {
|
|
||||||
rawDialer, err := dialer.NewWithOptions(dialer.Options{
|
|
||||||
Context: ctx,
|
|
||||||
Options: options.DialerOptions,
|
|
||||||
RemoteIsDomain: true,
|
|
||||||
ResolverOnDetour: options.ResolveOnDetour,
|
|
||||||
NewDialer: options.ResolveOnDetour,
|
|
||||||
DefaultOutbound: options.DefaultOutbound,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsOptions := common.PtrValueOrDefault(options.TLS)
|
|
||||||
tlsOptions.Enabled = true
|
|
||||||
baseTLSConfig, err := tls.NewClientWithOptions(tls.ClientOptions{
|
|
||||||
Context: ctx,
|
|
||||||
Logger: logger,
|
|
||||||
Options: tlsOptions,
|
|
||||||
AllowEmptyServerName: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return NewClientWithDialer(rawDialer, baseTLSConfig, tag, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClientWithDialer(rawDialer N.Dialer, baseTLSConfig tls.Config, tag string, options option.HTTPClientOptions) (*Client, error) {
|
|
||||||
headers := options.Headers.Build()
|
|
||||||
host := headers.Get("Host")
|
|
||||||
headers.Del("Host")
|
|
||||||
transport, err := newTransport(rawDialer, baseTLSConfig, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Client{
|
|
||||||
transport: transport,
|
|
||||||
headers: headers,
|
|
||||||
host: host,
|
|
||||||
tag: tag,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTPClientOptions) (httpTransport, error) {
|
|
||||||
version := options.Version
|
|
||||||
if version == 0 {
|
|
||||||
version = 2
|
|
||||||
}
|
|
||||||
fallbackDelay := time.Duration(options.DialerOptions.FallbackDelay)
|
|
||||||
if fallbackDelay == 0 {
|
|
||||||
fallbackDelay = 300 * time.Millisecond
|
|
||||||
}
|
|
||||||
var transport httpTransport
|
|
||||||
var err error
|
|
||||||
switch version {
|
|
||||||
case 1:
|
|
||||||
transport = newHTTP1Transport(rawDialer, baseTLSConfig)
|
|
||||||
case 2:
|
|
||||||
if options.DisableVersionFallback {
|
|
||||||
transport, err = newHTTP2Transport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
|
||||||
} else {
|
|
||||||
transport, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
|
||||||
}
|
|
||||||
case 3:
|
|
||||||
if baseTLSConfig != nil {
|
|
||||||
_, err = baseTLSConfig.STDConfig()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if options.DisableVersionFallback {
|
|
||||||
transport, err = newHTTP3Transport(rawDialer, baseTLSConfig, options.HTTP3Options)
|
|
||||||
} else {
|
|
||||||
var h2Fallback httpTransport
|
|
||||||
h2Fallback, err = newHTTP2FallbackTransport(rawDialer, baseTLSConfig, options.HTTP2Options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
transport, err = newHTTP3FallbackTransport(rawDialer, baseTLSConfig, h2Fallback, options.HTTP3Options, fallbackDelay)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown HTTP version: ", version)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return transport, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if c.tag == "" && len(c.headers) == 0 && c.host == "" {
|
|
||||||
return c.transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
if c.tag != "" {
|
|
||||||
if transportTag, loaded := transportTagFromContext(request.Context()); loaded && transportTag == c.tag {
|
|
||||||
return nil, E.New("HTTP request loopback in transport[", c.tag, "]")
|
|
||||||
}
|
|
||||||
request = request.Clone(contextWithTransportTag(request.Context(), c.tag))
|
|
||||||
} else {
|
|
||||||
request = request.Clone(request.Context())
|
|
||||||
}
|
|
||||||
applyHeaders(request, c.headers, c.host)
|
|
||||||
return c.transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) CloseIdleConnections() {
|
|
||||||
c.transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Clone() *Client {
|
|
||||||
return &Client{
|
|
||||||
transport: c.transport.Clone(),
|
|
||||||
headers: c.headers.Clone(),
|
|
||||||
host: c.host,
|
|
||||||
tag: c.tag,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
|
||||||
c.CloseIdleConnections()
|
|
||||||
if closer, isCloser := c.transport.(io.Closer); isCloser {
|
|
||||||
return closer.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import "context"
|
|
||||||
|
|
||||||
type transportKey struct{}
|
|
||||||
|
|
||||||
func contextWithTransportTag(ctx context.Context, transportTag string) context.Context {
|
|
||||||
return context.WithValue(ctx, transportKey{}, transportTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func transportTagFromContext(ctx context.Context) (string, bool) {
|
|
||||||
value, loaded := ctx.Value(transportKey{}).(string)
|
|
||||||
return value, loaded
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func dialTLS(ctx context.Context, rawDialer N.Dialer, baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string, expectProto string) (net.Conn, error) {
|
|
||||||
if baseTLSConfig == nil {
|
|
||||||
return nil, E.New("TLS transport unavailable")
|
|
||||||
}
|
|
||||||
tlsConfig := baseTLSConfig.Clone()
|
|
||||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
|
||||||
tlsConfig.SetServerName(destination.AddrString())
|
|
||||||
}
|
|
||||||
tlsConfig.SetNextProtos(nextProtos)
|
|
||||||
conn, err := rawDialer.DialContext(ctx, N.NetworkTCP, destination)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tlsConn, err := tls.ClientHandshake(ctx, conn, tlsConfig)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if expectProto != "" && tlsConn.ConnectionState().NegotiatedProtocol != expectProto {
|
|
||||||
tlsConn.Close()
|
|
||||||
return nil, errHTTP2Fallback
|
|
||||||
}
|
|
||||||
return tlsConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyHeaders(request *http.Request, headers http.Header, host string) {
|
|
||||||
for header, values := range headers {
|
|
||||||
request.Header[header] = append([]string(nil), values...)
|
|
||||||
}
|
|
||||||
if host != "" {
|
|
||||||
request.Host = host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestRequiresHTTP1(request *http.Request) bool {
|
|
||||||
return strings.Contains(strings.ToLower(request.Header.Get("Connection")), "upgrade") &&
|
|
||||||
strings.EqualFold(request.Header.Get("Upgrade"), "websocket")
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestReplayable(request *http.Request) bool {
|
|
||||||
return request.Body == nil || request.Body == http.NoBody || request.GetBody != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func cloneRequestForRetry(request *http.Request) *http.Request {
|
|
||||||
cloned := request.Clone(request.Context())
|
|
||||||
if request.Body != nil && request.Body != http.NoBody && request.GetBody != nil {
|
|
||||||
cloned.Body = mustGetBody(request)
|
|
||||||
}
|
|
||||||
return cloned
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustGetBody(request *http.Request) io.ReadCloser {
|
|
||||||
body, err := request.GetBody()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return body
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSTDTLSConfig(baseTLSConfig tls.Config, destination M.Socksaddr, nextProtos []string) (*stdTLS.Config, error) {
|
|
||||||
if baseTLSConfig == nil {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
tlsConfig := baseTLSConfig.Clone()
|
|
||||||
if tlsConfig.ServerName() == "" && destination.IsValid() {
|
|
||||||
tlsConfig.SetServerName(destination.AddrString())
|
|
||||||
}
|
|
||||||
tlsConfig.SetNextProtos(nextProtos)
|
|
||||||
return tlsConfig.STDConfig()
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type http1Transport struct {
|
|
||||||
transport *http.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP1Transport(rawDialer N.Dialer, baseTLSConfig tls.Config) *http1Transport {
|
|
||||||
transport := &http.Transport{
|
|
||||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return rawDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if baseTLSConfig != nil {
|
|
||||||
transport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
||||||
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{"http/1.1"}, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &http1Transport{transport: transport}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http1Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
return t.transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http1Transport) CloseIdleConnections() {
|
|
||||||
t.transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http1Transport) Clone() httpTransport {
|
|
||||||
return &http1Transport{transport: t.transport.Clone()}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CloneHTTP2Transport(transport *http2.Transport) *http2.Transport {
|
|
||||||
return &http2.Transport{
|
|
||||||
ReadIdleTimeout: transport.ReadIdleTimeout,
|
|
||||||
PingTimeout: transport.PingTimeout,
|
|
||||||
DialTLSContext: transport.DialTLSContext,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConfigureHTTP2Transport(options option.HTTP2Options) (*http2.Transport, error) {
|
|
||||||
stdTransport := &http.Transport{
|
|
||||||
TLSClientConfig: &stdTLS.Config{},
|
|
||||||
HTTP2: &http.HTTP2Config{
|
|
||||||
MaxReceiveBufferPerStream: int(options.StreamReceiveWindow.Value()),
|
|
||||||
MaxReceiveBufferPerConnection: int(options.ConnectionReceiveWindow.Value()),
|
|
||||||
MaxConcurrentStreams: options.MaxConcurrentStreams,
|
|
||||||
SendPingTimeout: time.Duration(options.KeepAlivePeriod),
|
|
||||||
PingTimeout: time.Duration(options.IdleTimeout),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
h2Transport, err := http2.ConfigureTransports(stdTransport)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "configure HTTP/2 transport")
|
|
||||||
}
|
|
||||||
// ConfigureTransports binds ConnPool to the throwaway http.Transport; sever it so DialTLSContext is used directly.
|
|
||||||
h2Transport.ConnPool = nil
|
|
||||||
h2Transport.ReadIdleTimeout = time.Duration(options.KeepAlivePeriod)
|
|
||||||
h2Transport.PingTimeout = time.Duration(options.IdleTimeout)
|
|
||||||
return h2Transport, nil
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"sync/atomic"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var errHTTP2Fallback = E.New("fallback to HTTP/1.1")
|
|
||||||
|
|
||||||
type http2FallbackTransport struct {
|
|
||||||
h2Transport *http2.Transport
|
|
||||||
h1Transport *http1Transport
|
|
||||||
h2Fallback *atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP2FallbackTransport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2FallbackTransport, error) {
|
|
||||||
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
|
|
||||||
var fallback atomic.Bool
|
|
||||||
h2Transport, err := ConfigureHTTP2Transport(options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
|
|
||||||
conn, dialErr := dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS, "http/1.1"}, http2.NextProtoTLS)
|
|
||||||
if dialErr != nil {
|
|
||||||
if errors.Is(dialErr, errHTTP2Fallback) {
|
|
||||||
fallback.Store(true)
|
|
||||||
}
|
|
||||||
return nil, dialErr
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
return &http2FallbackTransport{
|
|
||||||
h2Transport: h2Transport,
|
|
||||||
h1Transport: h1,
|
|
||||||
h2Fallback: &fallback,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
return t.roundTrip(request, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2FallbackTransport) roundTrip(request *http.Request, allowHTTP1Fallback bool) (*http.Response, error) {
|
|
||||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
|
||||||
return t.h1Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
if t.h2Fallback.Load() {
|
|
||||||
if !allowHTTP1Fallback {
|
|
||||||
return nil, errHTTP2Fallback
|
|
||||||
}
|
|
||||||
return t.h1Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
response, err := t.h2Transport.RoundTrip(request)
|
|
||||||
if err == nil {
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
if !errors.Is(err, errHTTP2Fallback) || !allowHTTP1Fallback {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t.h1Transport.RoundTrip(cloneRequestForRetry(request))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2FallbackTransport) CloseIdleConnections() {
|
|
||||||
t.h1Transport.CloseIdleConnections()
|
|
||||||
t.h2Transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2FallbackTransport) Clone() httpTransport {
|
|
||||||
return &http2FallbackTransport{
|
|
||||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
|
||||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
|
||||||
h2Fallback: t.h2Fallback,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
|
|
||||||
"golang.org/x/net/http2"
|
|
||||||
)
|
|
||||||
|
|
||||||
type http2Transport struct {
|
|
||||||
h2Transport *http2.Transport
|
|
||||||
h1Transport *http1Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP2Transport(rawDialer N.Dialer, baseTLSConfig tls.Config, options option.HTTP2Options) (*http2Transport, error) {
|
|
||||||
h1 := newHTTP1Transport(rawDialer, baseTLSConfig)
|
|
||||||
h2Transport, err := ConfigureHTTP2Transport(options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
h2Transport.DialTLSContext = func(ctx context.Context, network, addr string, _ *stdTLS.Config) (net.Conn, error) {
|
|
||||||
return dialTLS(ctx, rawDialer, baseTLSConfig, M.ParseSocksaddr(addr), []string{http2.NextProtoTLS}, http2.NextProtoTLS)
|
|
||||||
}
|
|
||||||
return &http2Transport{
|
|
||||||
h2Transport: h2Transport,
|
|
||||||
h1Transport: h1,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
|
||||||
return t.h1Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
return t.h2Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2Transport) CloseIdleConnections() {
|
|
||||||
t.h1Transport.CloseIdleConnections()
|
|
||||||
t.h2Transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http2Transport) Clone() httpTransport {
|
|
||||||
return &http2Transport{
|
|
||||||
h2Transport: CloneHTTP2Transport(t.h2Transport),
|
|
||||||
h1Transport: t.h1Transport.Clone().(*http1Transport),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,311 +0,0 @@
|
|||||||
//go:build with_quic
|
|
||||||
|
|
||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdTLS "crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
|
||||||
"github.com/sagernet/quic-go/http3"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
type http3Transport struct {
|
|
||||||
h3Transport *http3.Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
type http3FallbackTransport struct {
|
|
||||||
h3Transport *http3.Transport
|
|
||||||
h2Fallback httpTransport
|
|
||||||
fallbackDelay time.Duration
|
|
||||||
brokenAccess sync.Mutex
|
|
||||||
brokenUntil time.Time
|
|
||||||
brokenBackoff time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP3RoundTripper(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
options option.QUICOptions,
|
|
||||||
) *http3.Transport {
|
|
||||||
var handshakeTimeout time.Duration
|
|
||||||
if baseTLSConfig != nil {
|
|
||||||
handshakeTimeout = baseTLSConfig.HandshakeTimeout()
|
|
||||||
}
|
|
||||||
quicConfig := &quic.Config{
|
|
||||||
InitialStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
|
||||||
MaxStreamReceiveWindow: options.StreamReceiveWindow.Value(),
|
|
||||||
InitialConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
|
||||||
MaxConnectionReceiveWindow: options.ConnectionReceiveWindow.Value(),
|
|
||||||
KeepAlivePeriod: time.Duration(options.KeepAlivePeriod),
|
|
||||||
MaxIdleTimeout: time.Duration(options.IdleTimeout),
|
|
||||||
DisablePathMTUDiscovery: options.DisablePathMTUDiscovery,
|
|
||||||
}
|
|
||||||
if options.InitialPacketSize > 0 {
|
|
||||||
quicConfig.InitialPacketSize = uint16(options.InitialPacketSize)
|
|
||||||
}
|
|
||||||
if options.MaxConcurrentStreams > 0 {
|
|
||||||
quicConfig.MaxIncomingStreams = int64(options.MaxConcurrentStreams)
|
|
||||||
}
|
|
||||||
if handshakeTimeout > 0 {
|
|
||||||
quicConfig.HandshakeIdleTimeout = handshakeTimeout
|
|
||||||
}
|
|
||||||
h3Transport := &http3.Transport{
|
|
||||||
TLSClientConfig: &stdTLS.Config{},
|
|
||||||
QUICConfig: quicConfig,
|
|
||||||
Dial: func(ctx context.Context, addr string, tlsConfig *stdTLS.Config, quicConfig *quic.Config) (*quic.Conn, error) {
|
|
||||||
if handshakeTimeout > 0 && quicConfig.HandshakeIdleTimeout == 0 {
|
|
||||||
quicConfig = quicConfig.Clone()
|
|
||||||
quicConfig.HandshakeIdleTimeout = handshakeTimeout
|
|
||||||
}
|
|
||||||
if baseTLSConfig != nil {
|
|
||||||
var err error
|
|
||||||
tlsConfig, err = buildSTDTLSConfig(baseTLSConfig, M.ParseSocksaddr(addr), []string{http3.NextProtoH3})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tlsConfig = tlsConfig.Clone()
|
|
||||||
tlsConfig.NextProtos = []string{http3.NextProtoH3}
|
|
||||||
}
|
|
||||||
conn, err := rawDialer.DialContext(ctx, N.NetworkUDP, M.ParseSocksaddr(addr))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
quicConn, err := quic.DialEarly(ctx, bufio.NewUnbindPacketConn(conn), conn.RemoteAddr(), tlsConfig, quicConfig)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return quicConn, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return h3Transport
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP3Transport(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
options option.QUICOptions,
|
|
||||||
) (httpTransport, error) {
|
|
||||||
return &http3Transport{
|
|
||||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP3FallbackTransport(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
h2Fallback httpTransport,
|
|
||||||
options option.QUICOptions,
|
|
||||||
fallbackDelay time.Duration,
|
|
||||||
) (httpTransport, error) {
|
|
||||||
return &http3FallbackTransport{
|
|
||||||
h3Transport: newHTTP3RoundTripper(rawDialer, baseTLSConfig, options),
|
|
||||||
h2Fallback: h2Fallback,
|
|
||||||
fallbackDelay: fallbackDelay,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
return t.h3Transport.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3Transport) CloseIdleConnections() {
|
|
||||||
t.h3Transport.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3Transport) Close() error {
|
|
||||||
t.CloseIdleConnections()
|
|
||||||
return t.h3Transport.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3Transport) Clone() httpTransport {
|
|
||||||
return &http3Transport{
|
|
||||||
h3Transport: t.h3Transport,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if request.URL.Scheme != "https" || requestRequiresHTTP1(request) {
|
|
||||||
return t.h2Fallback.RoundTrip(request)
|
|
||||||
}
|
|
||||||
return t.roundTripHTTP3(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) roundTripHTTP3(request *http.Request) (*http.Response, error) {
|
|
||||||
if t.h3Broken() {
|
|
||||||
return t.h2FallbackRoundTrip(request)
|
|
||||||
}
|
|
||||||
response, err := t.h3Transport.RoundTripOpt(request, http3.RoundTripOpt{OnlyCachedConn: true})
|
|
||||||
if err == nil {
|
|
||||||
t.clearH3Broken()
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
if !errors.Is(err, http3.ErrNoCachedConn) {
|
|
||||||
t.markH3Broken()
|
|
||||||
return t.h2FallbackRoundTrip(cloneRequestForRetry(request))
|
|
||||||
}
|
|
||||||
if !requestReplayable(request) {
|
|
||||||
response, err = t.h3Transport.RoundTrip(request)
|
|
||||||
if err == nil {
|
|
||||||
t.clearH3Broken()
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
t.markH3Broken()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return t.roundTripHTTP3Race(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) roundTripHTTP3Race(request *http.Request) (*http.Response, error) {
|
|
||||||
ctx, cancel := context.WithCancel(request.Context())
|
|
||||||
defer cancel()
|
|
||||||
type result struct {
|
|
||||||
response *http.Response
|
|
||||||
err error
|
|
||||||
h3 bool
|
|
||||||
}
|
|
||||||
results := make(chan result, 2)
|
|
||||||
startRoundTrip := func(request *http.Request, useH3 bool) {
|
|
||||||
request = request.WithContext(ctx)
|
|
||||||
var (
|
|
||||||
response *http.Response
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
if useH3 {
|
|
||||||
response, err = t.h3Transport.RoundTrip(request)
|
|
||||||
} else {
|
|
||||||
response, err = t.h2FallbackRoundTrip(request)
|
|
||||||
}
|
|
||||||
results <- result{response: response, err: err, h3: useH3}
|
|
||||||
}
|
|
||||||
goroutines := 1
|
|
||||||
received := 0
|
|
||||||
drainRemaining := func() {
|
|
||||||
cancel()
|
|
||||||
for range goroutines - received {
|
|
||||||
go func() {
|
|
||||||
loser := <-results
|
|
||||||
if loser.response != nil && loser.response.Body != nil {
|
|
||||||
loser.response.Body.Close()
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go startRoundTrip(cloneRequestForRetry(request), true)
|
|
||||||
timer := time.NewTimer(t.fallbackDelay)
|
|
||||||
defer timer.Stop()
|
|
||||||
var (
|
|
||||||
h3Err error
|
|
||||||
fallbackErr error
|
|
||||||
)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
if goroutines == 1 {
|
|
||||||
goroutines++
|
|
||||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
|
||||||
}
|
|
||||||
case raceResult := <-results:
|
|
||||||
received++
|
|
||||||
if raceResult.err == nil {
|
|
||||||
if raceResult.h3 {
|
|
||||||
t.clearH3Broken()
|
|
||||||
}
|
|
||||||
drainRemaining()
|
|
||||||
return raceResult.response, nil
|
|
||||||
}
|
|
||||||
if raceResult.h3 {
|
|
||||||
t.markH3Broken()
|
|
||||||
h3Err = raceResult.err
|
|
||||||
if goroutines == 1 {
|
|
||||||
goroutines++
|
|
||||||
if !timer.Stop() {
|
|
||||||
select {
|
|
||||||
case <-timer.C:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
go startRoundTrip(cloneRequestForRetry(request), false)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fallbackErr = raceResult.err
|
|
||||||
}
|
|
||||||
if received < goroutines {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
drainRemaining()
|
|
||||||
switch {
|
|
||||||
case h3Err != nil && fallbackErr != nil:
|
|
||||||
return nil, E.Errors(h3Err, fallbackErr)
|
|
||||||
case fallbackErr != nil:
|
|
||||||
return nil, fallbackErr
|
|
||||||
default:
|
|
||||||
return nil, h3Err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) h2FallbackRoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
if fallback, isFallback := t.h2Fallback.(*http2FallbackTransport); isFallback {
|
|
||||||
return fallback.roundTrip(request, true)
|
|
||||||
}
|
|
||||||
return t.h2Fallback.RoundTrip(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) CloseIdleConnections() {
|
|
||||||
t.h3Transport.CloseIdleConnections()
|
|
||||||
t.h2Fallback.CloseIdleConnections()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) Close() error {
|
|
||||||
t.CloseIdleConnections()
|
|
||||||
return t.h3Transport.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) Clone() httpTransport {
|
|
||||||
return &http3FallbackTransport{
|
|
||||||
h3Transport: t.h3Transport,
|
|
||||||
h2Fallback: t.h2Fallback.Clone(),
|
|
||||||
fallbackDelay: t.fallbackDelay,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) h3Broken() bool {
|
|
||||||
t.brokenAccess.Lock()
|
|
||||||
defer t.brokenAccess.Unlock()
|
|
||||||
return !t.brokenUntil.IsZero() && time.Now().Before(t.brokenUntil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) clearH3Broken() {
|
|
||||||
t.brokenAccess.Lock()
|
|
||||||
t.brokenUntil = time.Time{}
|
|
||||||
t.brokenBackoff = 0
|
|
||||||
t.brokenAccess.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *http3FallbackTransport) markH3Broken() {
|
|
||||||
t.brokenAccess.Lock()
|
|
||||||
defer t.brokenAccess.Unlock()
|
|
||||||
if t.brokenBackoff == 0 {
|
|
||||||
t.brokenBackoff = 5 * time.Minute
|
|
||||||
} else {
|
|
||||||
t.brokenBackoff *= 2
|
|
||||||
if t.brokenBackoff > 48*time.Hour {
|
|
||||||
t.brokenBackoff = 48 * time.Hour
|
|
||||||
}
|
|
||||||
}
|
|
||||||
t.brokenUntil = time.Now().Add(t.brokenBackoff)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
//go:build !with_quic
|
|
||||||
|
|
||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newHTTP3FallbackTransport(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
h2Fallback httpTransport,
|
|
||||||
options option.QUICOptions,
|
|
||||||
fallbackDelay time.Duration,
|
|
||||||
) (httpTransport, error) {
|
|
||||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
|
||||||
}
|
|
||||||
|
|
||||||
func newHTTP3Transport(
|
|
||||||
rawDialer N.Dialer,
|
|
||||||
baseTLSConfig tls.Config,
|
|
||||||
options option.QUICOptions,
|
|
||||||
) (httpTransport, error) {
|
|
||||||
return nil, E.New("HTTP/3 requires building with the with_quic tag")
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
package httpclient
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
"github.com/sagernet/sing-box/log"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ adapter.HTTPClientManager = (*Manager)(nil)
|
|
||||||
_ adapter.LifecycleService = (*Manager)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
type Manager struct {
|
|
||||||
ctx context.Context
|
|
||||||
logger log.ContextLogger
|
|
||||||
access sync.Mutex
|
|
||||||
defines map[string]option.HTTPClient
|
|
||||||
clients map[string]*Client
|
|
||||||
defaultTag string
|
|
||||||
defaultTransport http.RoundTripper
|
|
||||||
defaultTransportFallback func() (*Client, error)
|
|
||||||
fallbackClient *Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewManager(ctx context.Context, logger log.ContextLogger, clients []option.HTTPClient, defaultHTTPClient string) *Manager {
|
|
||||||
defines := make(map[string]option.HTTPClient, len(clients))
|
|
||||||
for _, client := range clients {
|
|
||||||
defines[client.Tag] = client
|
|
||||||
}
|
|
||||||
defaultTag := defaultHTTPClient
|
|
||||||
if defaultTag == "" && len(clients) > 0 {
|
|
||||||
defaultTag = clients[0].Tag
|
|
||||||
}
|
|
||||||
return &Manager{
|
|
||||||
ctx: ctx,
|
|
||||||
logger: logger,
|
|
||||||
defines: defines,
|
|
||||||
clients: make(map[string]*Client),
|
|
||||||
defaultTag: defaultTag,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Initialize(defaultTransportFallback func() (*Client, error)) {
|
|
||||||
m.defaultTransportFallback = defaultTransportFallback
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Name() string {
|
|
||||||
return "http-client"
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
|
||||||
if stage != adapter.StartStateStart {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if m.defaultTag != "" {
|
|
||||||
transport, err := m.resolveShared(m.defaultTag)
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "resolve default http client")
|
|
||||||
}
|
|
||||||
m.defaultTransport = transport
|
|
||||||
} else if m.defaultTransportFallback != nil {
|
|
||||||
client, err := m.defaultTransportFallback()
|
|
||||||
if err != nil {
|
|
||||||
return E.Cause(err, "create default http client")
|
|
||||||
}
|
|
||||||
m.defaultTransport = client
|
|
||||||
m.fallbackClient = client
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) DefaultTransport() http.RoundTripper {
|
|
||||||
return m.defaultTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) ResolveTransport(logger logger.ContextLogger, options option.HTTPClientOptions) (http.RoundTripper, error) {
|
|
||||||
if options.Tag != "" {
|
|
||||||
if options.ResolveOnDetour {
|
|
||||||
define, loaded := m.defines[options.Tag]
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("http_client not found: ", options.Tag)
|
|
||||||
}
|
|
||||||
resolvedOptions := define.Options()
|
|
||||||
resolvedOptions.ResolveOnDetour = true
|
|
||||||
return NewClient(m.ctx, logger, options.Tag, resolvedOptions)
|
|
||||||
}
|
|
||||||
return m.resolveShared(options.Tag)
|
|
||||||
}
|
|
||||||
return NewClient(m.ctx, logger, "", options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) resolveShared(tag string) (http.RoundTripper, error) {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
if client, loaded := m.clients[tag]; loaded {
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
define, loaded := m.defines[tag]
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("http_client not found: ", tag)
|
|
||||||
}
|
|
||||||
client, err := NewClient(m.ctx, m.logger, tag, define.Options())
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create shared http_client[", tag, "]")
|
|
||||||
}
|
|
||||||
m.clients[tag] = client
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Manager) Close() error {
|
|
||||||
m.access.Lock()
|
|
||||||
defer m.access.Unlock()
|
|
||||||
if m.clients == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
for _, client := range m.clients {
|
|
||||||
err = E.Append(err, client.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close http client")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if m.fallbackClient != nil {
|
|
||||||
err = E.Append(err, m.fallbackClient.Close(), func(err error) error {
|
|
||||||
return E.Cause(err, "close default http client")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
m.clients = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -37,10 +37,7 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
|||||||
if l.listenOptions.ReuseAddr {
|
if l.listenOptions.ReuseAddr {
|
||||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||||
}
|
}
|
||||||
if l.listenOptions.DisableTCPKeepAlive {
|
if !l.listenOptions.DisableTCPKeepAlive {
|
||||||
listenConfig.KeepAlive = -1
|
|
||||||
listenConfig.KeepAliveConfig.Enable = false
|
|
||||||
} else {
|
|
||||||
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
||||||
if keepIdle == 0 {
|
if keepIdle == 0 {
|
||||||
keepIdle = C.TCPKeepAliveInitial
|
keepIdle = C.TCPKeepAliveInitial
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
package networkquality
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
sBufio "github.com/sagernet/sing/common/bufio"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FormatBitrate(bps int64) string {
|
|
||||||
switch {
|
|
||||||
case bps >= 1_000_000_000:
|
|
||||||
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
|
|
||||||
case bps >= 1_000_000:
|
|
||||||
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
|
|
||||||
case bps >= 1_000:
|
|
||||||
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%d bps", bps)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTPClient(dialer N.Dialer) *http.Client {
|
|
||||||
transport := &http.Transport{
|
|
||||||
ForceAttemptHTTP2: true,
|
|
||||||
TLSHandshakeTimeout: C.TCPTimeout,
|
|
||||||
}
|
|
||||||
if dialer != nil {
|
|
||||||
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
|
||||||
return dialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &http.Client{Transport: transport}
|
|
||||||
}
|
|
||||||
|
|
||||||
func baseTransportFromClient(client *http.Client) (*http.Transport, error) {
|
|
||||||
if client == nil {
|
|
||||||
return nil, E.New("http client is nil")
|
|
||||||
}
|
|
||||||
if client.Transport == nil {
|
|
||||||
return http.DefaultTransport.(*http.Transport).Clone(), nil
|
|
||||||
}
|
|
||||||
transport, ok := client.Transport.(*http.Transport)
|
|
||||||
if !ok {
|
|
||||||
return nil, E.New("http client transport must be *http.Transport")
|
|
||||||
}
|
|
||||||
return transport.Clone(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMeasurementClient(
|
|
||||||
baseClient *http.Client,
|
|
||||||
connectEndpoint string,
|
|
||||||
singleConnection bool,
|
|
||||||
disableKeepAlives bool,
|
|
||||||
readCounters []N.CountFunc,
|
|
||||||
writeCounters []N.CountFunc,
|
|
||||||
) (*http.Client, error) {
|
|
||||||
transport, err := baseTransportFromClient(baseClient)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
transport.DisableCompression = true
|
|
||||||
transport.DisableKeepAlives = disableKeepAlives
|
|
||||||
if singleConnection {
|
|
||||||
transport.MaxConnsPerHost = 1
|
|
||||||
transport.MaxIdleConnsPerHost = 1
|
|
||||||
transport.MaxIdleConns = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
baseDialContext := transport.DialContext
|
|
||||||
if baseDialContext == nil {
|
|
||||||
dialer := &net.Dialer{}
|
|
||||||
baseDialContext = dialer.DialContext
|
|
||||||
}
|
|
||||||
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
|
||||||
dialAddr := addr
|
|
||||||
if connectEndpoint != "" {
|
|
||||||
dialAddr = rewriteDialAddress(addr, connectEndpoint)
|
|
||||||
}
|
|
||||||
conn, dialErr := baseDialContext(ctx, network, dialAddr)
|
|
||||||
if dialErr != nil {
|
|
||||||
return nil, dialErr
|
|
||||||
}
|
|
||||||
if len(readCounters) > 0 || len(writeCounters) > 0 {
|
|
||||||
return sBufio.NewCounterConn(conn, readCounters, writeCounters), nil
|
|
||||||
}
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
CheckRedirect: baseClient.CheckRedirect,
|
|
||||||
Jar: baseClient.Jar,
|
|
||||||
Timeout: baseClient.Timeout,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type MeasurementClientFactory func(
|
|
||||||
connectEndpoint string,
|
|
||||||
singleConnection bool,
|
|
||||||
disableKeepAlives bool,
|
|
||||||
readCounters []N.CountFunc,
|
|
||||||
writeCounters []N.CountFunc,
|
|
||||||
) (*http.Client, error)
|
|
||||||
|
|
||||||
func defaultMeasurementClientFactory(baseClient *http.Client) MeasurementClientFactory {
|
|
||||||
return func(connectEndpoint string, singleConnection, disableKeepAlives bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
|
||||||
return newMeasurementClient(baseClient, connectEndpoint, singleConnection, disableKeepAlives, readCounters, writeCounters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOptionalHTTP3Factory(dialer N.Dialer, useHTTP3 bool) (MeasurementClientFactory, error) {
|
|
||||||
if !useHTTP3 {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return NewHTTP3MeasurementClientFactory(dialer)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rewriteDialAddress(addr string, connectEndpoint string) string {
|
|
||||||
connectEndpoint = strings.TrimSpace(connectEndpoint)
|
|
||||||
host, port, err := net.SplitHostPort(addr)
|
|
||||||
if err != nil {
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
endpointHost, endpointPort, err := net.SplitHostPort(connectEndpoint)
|
|
||||||
if err == nil {
|
|
||||||
host = endpointHost
|
|
||||||
if endpointPort != "" {
|
|
||||||
port = endpointPort
|
|
||||||
}
|
|
||||||
} else if connectEndpoint != "" {
|
|
||||||
host = connectEndpoint
|
|
||||||
}
|
|
||||||
return net.JoinHostPort(host, port)
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
//go:build with_quic
|
|
||||||
|
|
||||||
package networkquality
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
|
||||||
"github.com/sagernet/quic-go/http3"
|
|
||||||
sBufio "github.com/sagernet/sing/common/bufio"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
|
||||||
// singleConnection and disableKeepAlives are not applied:
|
|
||||||
// HTTP/3 multiplexes streams over a single QUIC connection by default.
|
|
||||||
return func(connectEndpoint string, _, _ bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
|
||||||
transport := &http3.Transport{
|
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
|
||||||
dialAddr := addr
|
|
||||||
if connectEndpoint != "" {
|
|
||||||
dialAddr = rewriteDialAddress(addr, connectEndpoint)
|
|
||||||
}
|
|
||||||
destination := M.ParseSocksaddr(dialAddr)
|
|
||||||
var udpConn net.Conn
|
|
||||||
var dialErr error
|
|
||||||
if dialer != nil {
|
|
||||||
udpConn, dialErr = dialer.DialContext(ctx, N.NetworkUDP, destination)
|
|
||||||
} else {
|
|
||||||
var netDialer net.Dialer
|
|
||||||
udpConn, dialErr = netDialer.DialContext(ctx, N.NetworkUDP, destination.String())
|
|
||||||
}
|
|
||||||
if dialErr != nil {
|
|
||||||
return nil, dialErr
|
|
||||||
}
|
|
||||||
wrappedConn := udpConn
|
|
||||||
if len(readCounters) > 0 || len(writeCounters) > 0 {
|
|
||||||
wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters)
|
|
||||||
}
|
|
||||||
packetConn := sBufio.NewUnbindPacketConn(wrappedConn)
|
|
||||||
quicConn, dialErr := quic.DialEarly(ctx, packetConn, udpConn.RemoteAddr(), tlsCfg, cfg)
|
|
||||||
if dialErr != nil {
|
|
||||||
udpConn.Close()
|
|
||||||
return nil, dialErr
|
|
||||||
}
|
|
||||||
return quicConn, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return &http.Client{Transport: transport}, nil
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build !with_quic
|
|
||||||
|
|
||||||
package networkquality
|
|
||||||
|
|
||||||
import (
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
|
||||||
return nil, C.ErrQUICNotIncluded
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -46,7 +46,6 @@ const (
|
|||||||
ruleItemNetworkIsConstrained
|
ruleItemNetworkIsConstrained
|
||||||
ruleItemNetworkInterfaceAddress
|
ruleItemNetworkInterfaceAddress
|
||||||
ruleItemDefaultInterfaceAddress
|
ruleItemDefaultInterfaceAddress
|
||||||
ruleItemPackageNameRegex
|
|
||||||
ruleItemFinal uint8 = 0xFF
|
ruleItemFinal uint8 = 0xFF
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -216,8 +215,6 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
|
|||||||
rule.ProcessPathRegex, err = readRuleItemString(reader)
|
rule.ProcessPathRegex, err = readRuleItemString(reader)
|
||||||
case ruleItemPackageName:
|
case ruleItemPackageName:
|
||||||
rule.PackageName, err = readRuleItemString(reader)
|
rule.PackageName, err = readRuleItemString(reader)
|
||||||
case ruleItemPackageNameRegex:
|
|
||||||
rule.PackageNameRegex, err = readRuleItemString(reader)
|
|
||||||
case ruleItemWIFISSID:
|
case ruleItemWIFISSID:
|
||||||
rule.WIFISSID, err = readRuleItemString(reader)
|
rule.WIFISSID, err = readRuleItemString(reader)
|
||||||
case ruleItemWIFIBSSID:
|
case ruleItemWIFIBSSID:
|
||||||
@@ -397,15 +394,6 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(rule.PackageNameRegex) > 0 {
|
|
||||||
if generateVersion < C.RuleSetVersion5 {
|
|
||||||
return E.New("`package_name_regex` rule item is only supported in version 5 or later")
|
|
||||||
}
|
|
||||||
err = writeRuleItemString(writer, ruleItemPackageNameRegex, rule.PackageNameRegex)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(rule.NetworkType) > 0 {
|
if len(rule.NetworkType) > 0 {
|
||||||
if generateVersion < C.RuleSetVersion3 {
|
if generateVersion < C.RuleSetVersion3 {
|
||||||
return E.New("`network_type` rule item is only supported in version 3 or later")
|
return E.New("`network_type` rule item is only supported in version 3 or later")
|
||||||
|
|||||||
@@ -1,612 +0,0 @@
|
|||||||
package stun
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/netip"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing/common/bufio"
|
|
||||||
"github.com/sagernet/sing/common/bufio/deadline"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
|
||||||
N "github.com/sagernet/sing/common/network"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DefaultServer = "stun.voipgate.com:3478"
|
|
||||||
|
|
||||||
magicCookie = 0x2112A442
|
|
||||||
headerSize = 20
|
|
||||||
|
|
||||||
bindingRequest = 0x0001
|
|
||||||
bindingSuccessResponse = 0x0101
|
|
||||||
bindingErrorResponse = 0x0111
|
|
||||||
|
|
||||||
attrMappedAddress = 0x0001
|
|
||||||
attrChangeRequest = 0x0003
|
|
||||||
attrErrorCode = 0x0009
|
|
||||||
attrXORMappedAddress = 0x0020
|
|
||||||
attrOtherAddress = 0x802c
|
|
||||||
|
|
||||||
familyIPv4 = 0x01
|
|
||||||
familyIPv6 = 0x02
|
|
||||||
|
|
||||||
changeIP = 0x04
|
|
||||||
changePort = 0x02
|
|
||||||
|
|
||||||
defaultRTO = 500 * time.Millisecond
|
|
||||||
minRTO = 250 * time.Millisecond
|
|
||||||
maxRetransmit = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
type Phase int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
PhaseBinding Phase = iota
|
|
||||||
PhaseNATMapping
|
|
||||||
PhaseNATFiltering
|
|
||||||
PhaseDone
|
|
||||||
)
|
|
||||||
|
|
||||||
type NATMapping int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
NATMappingUnknown NATMapping = iota
|
|
||||||
_ // reserved
|
|
||||||
NATMappingEndpointIndependent
|
|
||||||
NATMappingAddressDependent
|
|
||||||
NATMappingAddressAndPortDependent
|
|
||||||
)
|
|
||||||
|
|
||||||
func (m NATMapping) String() string {
|
|
||||||
switch m {
|
|
||||||
case NATMappingEndpointIndependent:
|
|
||||||
return "Endpoint Independent"
|
|
||||||
case NATMappingAddressDependent:
|
|
||||||
return "Address Dependent"
|
|
||||||
case NATMappingAddressAndPortDependent:
|
|
||||||
return "Address and Port Dependent"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type NATFiltering int32
|
|
||||||
|
|
||||||
const (
|
|
||||||
NATFilteringUnknown NATFiltering = iota
|
|
||||||
NATFilteringEndpointIndependent
|
|
||||||
NATFilteringAddressDependent
|
|
||||||
NATFilteringAddressAndPortDependent
|
|
||||||
)
|
|
||||||
|
|
||||||
func (f NATFiltering) String() string {
|
|
||||||
switch f {
|
|
||||||
case NATFilteringEndpointIndependent:
|
|
||||||
return "Endpoint Independent"
|
|
||||||
case NATFilteringAddressDependent:
|
|
||||||
return "Address Dependent"
|
|
||||||
case NATFilteringAddressAndPortDependent:
|
|
||||||
return "Address and Port Dependent"
|
|
||||||
default:
|
|
||||||
return "Unknown"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TransactionID [12]byte
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
Server string
|
|
||||||
Dialer N.Dialer
|
|
||||||
Context context.Context
|
|
||||||
OnProgress func(Progress)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Progress struct {
|
|
||||||
Phase Phase
|
|
||||||
ExternalAddr string
|
|
||||||
LatencyMs int32
|
|
||||||
NATMapping NATMapping
|
|
||||||
NATFiltering NATFiltering
|
|
||||||
}
|
|
||||||
|
|
||||||
type Result struct {
|
|
||||||
ExternalAddr string
|
|
||||||
LatencyMs int32
|
|
||||||
NATMapping NATMapping
|
|
||||||
NATFiltering NATFiltering
|
|
||||||
NATTypeSupported bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type parsedResponse struct {
|
|
||||||
xorMappedAddr netip.AddrPort
|
|
||||||
mappedAddr netip.AddrPort
|
|
||||||
otherAddr netip.AddrPort
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) {
|
|
||||||
if r.xorMappedAddr.IsValid() {
|
|
||||||
return r.xorMappedAddr, true
|
|
||||||
}
|
|
||||||
if r.mappedAddr.IsValid() {
|
|
||||||
return r.mappedAddr, true
|
|
||||||
}
|
|
||||||
return netip.AddrPort{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
type stunAttribute struct {
|
|
||||||
typ uint16
|
|
||||||
value []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTransactionID() TransactionID {
|
|
||||||
var id TransactionID
|
|
||||||
_, _ = rand.Read(id[:])
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte {
|
|
||||||
attrLen := 0
|
|
||||||
for _, attr := range attrs {
|
|
||||||
attrLen += 4 + len(attr.value) + paddingLen(len(attr.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, headerSize+attrLen)
|
|
||||||
binary.BigEndian.PutUint16(buf[0:2], bindingRequest)
|
|
||||||
binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen))
|
|
||||||
binary.BigEndian.PutUint32(buf[4:8], magicCookie)
|
|
||||||
copy(buf[8:20], txID[:])
|
|
||||||
|
|
||||||
offset := headerSize
|
|
||||||
for _, attr := range attrs {
|
|
||||||
binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ)
|
|
||||||
binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value)))
|
|
||||||
copy(buf[offset+4:offset+4+len(attr.value)], attr.value)
|
|
||||||
offset += 4 + len(attr.value) + paddingLen(len(attr.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
return buf
|
|
||||||
}
|
|
||||||
|
|
||||||
func changeRequestAttr(flags byte) stunAttribute {
|
|
||||||
return stunAttribute{
|
|
||||||
typ: attrChangeRequest,
|
|
||||||
value: []byte{0, 0, 0, flags},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) {
|
|
||||||
if len(data) < headerSize {
|
|
||||||
return nil, E.New("response too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
msgType := binary.BigEndian.Uint16(data[0:2])
|
|
||||||
if msgType&0xC000 != 0 {
|
|
||||||
return nil, E.New("invalid STUN message: top 2 bits not zero")
|
|
||||||
}
|
|
||||||
|
|
||||||
cookie := binary.BigEndian.Uint32(data[4:8])
|
|
||||||
if cookie != magicCookie {
|
|
||||||
return nil, E.New("invalid magic cookie")
|
|
||||||
}
|
|
||||||
|
|
||||||
var txID TransactionID
|
|
||||||
copy(txID[:], data[8:20])
|
|
||||||
if txID != expectedTxID {
|
|
||||||
return nil, E.New("transaction ID mismatch")
|
|
||||||
}
|
|
||||||
|
|
||||||
msgLen := int(binary.BigEndian.Uint16(data[2:4]))
|
|
||||||
if msgLen > len(data)-headerSize {
|
|
||||||
return nil, E.New("message length exceeds data")
|
|
||||||
}
|
|
||||||
|
|
||||||
attrData := data[headerSize : headerSize+msgLen]
|
|
||||||
|
|
||||||
if msgType == bindingErrorResponse {
|
|
||||||
return nil, parseErrorResponse(attrData)
|
|
||||||
}
|
|
||||||
if msgType != bindingSuccessResponse {
|
|
||||||
return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType))
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := &parsedResponse{}
|
|
||||||
offset := 0
|
|
||||||
for offset+4 <= len(attrData) {
|
|
||||||
attrType := binary.BigEndian.Uint16(attrData[offset : offset+2])
|
|
||||||
attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4]))
|
|
||||||
if offset+4+attrLen > len(attrData) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
attrValue := attrData[offset+4 : offset+4+attrLen]
|
|
||||||
|
|
||||||
switch attrType {
|
|
||||||
case attrXORMappedAddress:
|
|
||||||
addr, err := parseXORMappedAddress(attrValue, txID)
|
|
||||||
if err == nil {
|
|
||||||
resp.xorMappedAddr = addr
|
|
||||||
}
|
|
||||||
case attrMappedAddress:
|
|
||||||
addr, err := parseMappedAddress(attrValue)
|
|
||||||
if err == nil {
|
|
||||||
resp.mappedAddr = addr
|
|
||||||
}
|
|
||||||
case attrOtherAddress:
|
|
||||||
addr, err := parseMappedAddress(attrValue)
|
|
||||||
if err == nil {
|
|
||||||
resp.otherAddr = addr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
offset += 4 + attrLen + paddingLen(attrLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseErrorResponse(data []byte) error {
|
|
||||||
offset := 0
|
|
||||||
for offset+4 <= len(data) {
|
|
||||||
attrType := binary.BigEndian.Uint16(data[offset : offset+2])
|
|
||||||
attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4]))
|
|
||||||
if offset+4+attrLen > len(data) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if attrType == attrErrorCode && attrLen >= 4 {
|
|
||||||
attrValue := data[offset+4 : offset+4+attrLen]
|
|
||||||
class := int(attrValue[2] & 0x07)
|
|
||||||
number := int(attrValue[3])
|
|
||||||
code := class*100 + number
|
|
||||||
if attrLen > 4 {
|
|
||||||
return E.New("STUN error ", code, ": ", string(attrValue[4:]))
|
|
||||||
}
|
|
||||||
return E.New("STUN error ", code)
|
|
||||||
}
|
|
||||||
offset += 4 + attrLen + paddingLen(attrLen)
|
|
||||||
}
|
|
||||||
return E.New("STUN error response")
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) {
|
|
||||||
if len(data) < 4 {
|
|
||||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
family := data[1]
|
|
||||||
xPort := binary.BigEndian.Uint16(data[2:4])
|
|
||||||
port := xPort ^ uint16(magicCookie>>16)
|
|
||||||
|
|
||||||
switch family {
|
|
||||||
case familyIPv4:
|
|
||||||
if len(data) < 8 {
|
|
||||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short")
|
|
||||||
}
|
|
||||||
var ip [4]byte
|
|
||||||
binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie)
|
|
||||||
return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil
|
|
||||||
case familyIPv6:
|
|
||||||
if len(data) < 20 {
|
|
||||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short")
|
|
||||||
}
|
|
||||||
var ip [16]byte
|
|
||||||
var xorKey [16]byte
|
|
||||||
binary.BigEndian.PutUint32(xorKey[0:4], magicCookie)
|
|
||||||
copy(xorKey[4:16], txID[:])
|
|
||||||
for i := range 16 {
|
|
||||||
ip[i] = data[4+i] ^ xorKey[i]
|
|
||||||
}
|
|
||||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
|
||||||
default:
|
|
||||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseMappedAddress(data []byte) (netip.AddrPort, error) {
|
|
||||||
if len(data) < 4 {
|
|
||||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
family := data[1]
|
|
||||||
port := binary.BigEndian.Uint16(data[2:4])
|
|
||||||
|
|
||||||
switch family {
|
|
||||||
case familyIPv4:
|
|
||||||
if len(data) < 8 {
|
|
||||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short")
|
|
||||||
}
|
|
||||||
return netip.AddrPortFrom(
|
|
||||||
netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port,
|
|
||||||
), nil
|
|
||||||
case familyIPv6:
|
|
||||||
if len(data) < 20 {
|
|
||||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short")
|
|
||||||
}
|
|
||||||
var ip [16]byte
|
|
||||||
copy(ip[:], data[4:20])
|
|
||||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
|
||||||
default:
|
|
||||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) {
|
|
||||||
request := buildBindingRequest(txID, attrs...)
|
|
||||||
currentRTO := rto
|
|
||||||
retransmitCount := 0
|
|
||||||
|
|
||||||
sendTime := time.Now()
|
|
||||||
_, err := conn.WriteTo(request, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, E.Cause(err, "send STUN request")
|
|
||||||
}
|
|
||||||
|
|
||||||
buf := make([]byte, 1024)
|
|
||||||
for {
|
|
||||||
err = conn.SetReadDeadline(sendTime.Add(currentRTO))
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, E.Cause(err, "set read deadline")
|
|
||||||
}
|
|
||||||
|
|
||||||
n, _, readErr := conn.ReadFrom(buf)
|
|
||||||
if readErr != nil {
|
|
||||||
if E.IsTimeout(readErr) && retransmitCount < maxRetransmit {
|
|
||||||
retransmitCount++
|
|
||||||
currentRTO *= 2
|
|
||||||
sendTime = time.Now()
|
|
||||||
_, err = conn.WriteTo(request, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, E.Cause(err, "retransmit STUN request")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, 0, E.Cause(readErr, "read STUN response")
|
|
||||||
}
|
|
||||||
|
|
||||||
if n < headerSize || buf[0]&0xC0 != 0 ||
|
|
||||||
binary.BigEndian.Uint32(buf[4:8]) != magicCookie {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var receivedTxID TransactionID
|
|
||||||
copy(receivedTxID[:], buf[8:20])
|
|
||||||
if receivedTxID != txID {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
latency := time.Since(sendTime)
|
|
||||||
|
|
||||||
resp, parseErr := parseResponse(buf[:n], txID)
|
|
||||||
if parseErr != nil {
|
|
||||||
return nil, 0, parseErr
|
|
||||||
}
|
|
||||||
|
|
||||||
return resp, latency, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Run(options Options) (*Result, error) {
|
|
||||||
ctx := options.Context
|
|
||||||
if ctx == nil {
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
|
|
||||||
server := options.Server
|
|
||||||
if server == "" {
|
|
||||||
server = DefaultServer
|
|
||||||
}
|
|
||||||
serverSocksaddr := M.ParseSocksaddr(server)
|
|
||||||
if serverSocksaddr.Port == 0 {
|
|
||||||
serverSocksaddr.Port = 3478
|
|
||||||
}
|
|
||||||
|
|
||||||
reportProgress := options.OnProgress
|
|
||||||
if reportProgress == nil {
|
|
||||||
reportProgress = func(Progress) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
packetConn net.PacketConn
|
|
||||||
serverAddr net.Addr
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if options.Dialer != nil {
|
|
||||||
packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create UDP socket")
|
|
||||||
}
|
|
||||||
serverAddr = serverSocksaddr
|
|
||||||
} else {
|
|
||||||
serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String())
|
|
||||||
if resolveErr != nil {
|
|
||||||
return nil, E.Cause(resolveErr, "resolve STUN server")
|
|
||||||
}
|
|
||||||
packetConn, err = net.ListenPacket("udp", "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "create UDP socket")
|
|
||||||
}
|
|
||||||
serverAddr = serverUDPAddr
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
_ = packetConn.Close()
|
|
||||||
}()
|
|
||||||
if deadline.NeedAdditionalReadDeadline(packetConn) {
|
|
||||||
packetConn = deadline.NewPacketConn(bufio.NewPacketConn(packetConn))
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
rto := defaultRTO
|
|
||||||
|
|
||||||
// Phase 1: Binding
|
|
||||||
reportProgress(Progress{Phase: PhaseBinding})
|
|
||||||
|
|
||||||
txID := newTransactionID()
|
|
||||||
resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "binding request")
|
|
||||||
}
|
|
||||||
|
|
||||||
rto = max(minRTO, 3*latency)
|
|
||||||
|
|
||||||
externalAddr, ok := resp.externalAddr()
|
|
||||||
if !ok {
|
|
||||||
return nil, E.New("no mapped address in response")
|
|
||||||
}
|
|
||||||
|
|
||||||
result := &Result{
|
|
||||||
ExternalAddr: externalAddr.String(),
|
|
||||||
LatencyMs: int32(latency.Milliseconds()),
|
|
||||||
}
|
|
||||||
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseBinding,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
})
|
|
||||||
|
|
||||||
otherAddr := resp.otherAddr
|
|
||||||
if !otherAddr.IsValid() {
|
|
||||||
result.NATTypeSupported = false
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseDone,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
})
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
result.NATTypeSupported = true
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return result, nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3)
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseNATMapping,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
})
|
|
||||||
|
|
||||||
result.NATMapping = detectNATMapping(
|
|
||||||
packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto,
|
|
||||||
)
|
|
||||||
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseNATMapping,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
NATMapping: result.NATMapping,
|
|
||||||
})
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return result, nil
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4)
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseNATFiltering,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
NATMapping: result.NATMapping,
|
|
||||||
})
|
|
||||||
|
|
||||||
result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto)
|
|
||||||
|
|
||||||
reportProgress(Progress{
|
|
||||||
Phase: PhaseDone,
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
NATMapping: result.NATMapping,
|
|
||||||
NATFiltering: result.NATFiltering,
|
|
||||||
})
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectNATMapping(
|
|
||||||
conn net.PacketConn,
|
|
||||||
serverPort uint16,
|
|
||||||
externalAddr netip.AddrPort,
|
|
||||||
otherAddr netip.AddrPort,
|
|
||||||
rto time.Duration,
|
|
||||||
) NATMapping {
|
|
||||||
// Mapping Test II: Send to other_ip:server_port
|
|
||||||
testIIAddr := net.UDPAddrFromAddrPort(
|
|
||||||
netip.AddrPortFrom(otherAddr.Addr(), serverPort),
|
|
||||||
)
|
|
||||||
txID2 := newTransactionID()
|
|
||||||
resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto)
|
|
||||||
if err != nil {
|
|
||||||
return NATMappingUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
externalAddr2, ok := resp2.externalAddr()
|
|
||||||
if !ok {
|
|
||||||
return NATMappingUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
if externalAddr == externalAddr2 {
|
|
||||||
return NATMappingEndpointIndependent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mapping Test III: Send to other_ip:other_port
|
|
||||||
testIIIAddr := net.UDPAddrFromAddrPort(otherAddr)
|
|
||||||
txID3 := newTransactionID()
|
|
||||||
resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto)
|
|
||||||
if err != nil {
|
|
||||||
return NATMappingUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
externalAddr3, ok := resp3.externalAddr()
|
|
||||||
if !ok {
|
|
||||||
return NATMappingUnknown
|
|
||||||
}
|
|
||||||
|
|
||||||
if externalAddr2 == externalAddr3 {
|
|
||||||
return NATMappingAddressDependent
|
|
||||||
}
|
|
||||||
return NATMappingAddressAndPortDependent
|
|
||||||
}
|
|
||||||
|
|
||||||
func detectNATFiltering(
|
|
||||||
conn net.PacketConn,
|
|
||||||
serverAddr net.Addr,
|
|
||||||
rto time.Duration,
|
|
||||||
) NATFiltering {
|
|
||||||
// Filtering Test II: Request response from different IP and port
|
|
||||||
txID := newTransactionID()
|
|
||||||
_, _, err := roundTrip(conn, serverAddr, txID,
|
|
||||||
[]stunAttribute{changeRequestAttr(changeIP | changePort)}, rto)
|
|
||||||
if err == nil {
|
|
||||||
return NATFilteringEndpointIndependent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filtering Test III: Request response from different port only
|
|
||||||
txID = newTransactionID()
|
|
||||||
_, _, err = roundTrip(conn, serverAddr, txID,
|
|
||||||
[]stunAttribute{changeRequestAttr(changePort)}, rto)
|
|
||||||
if err == nil {
|
|
||||||
return NATFilteringAddressDependent
|
|
||||||
}
|
|
||||||
|
|
||||||
return NATFilteringAddressAndPortDependent
|
|
||||||
}
|
|
||||||
|
|
||||||
func paddingLen(n int) int {
|
|
||||||
if n%4 == 0 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 4 - n%4
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,205 +0,0 @@
|
|||||||
//go:build darwin && cgo
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
stdtls "crypto/tls"
|
|
||||||
"net"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
const appleTLSTestTimeout = 5 * time.Second
|
|
||||||
|
|
||||||
type appleTLSServerResult struct {
|
|
||||||
state stdtls.ConnectionState
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppleClientHandshakeAppliesALPNAndVersion(t *testing.T) {
|
|
||||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
|
||||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
|
||||||
Certificates: []stdtls.Certificate{serverCertificate},
|
|
||||||
MinVersion: stdtls.VersionTLS12,
|
|
||||||
MaxVersion: stdtls.VersionTLS12,
|
|
||||||
NextProtos: []string{"h2"},
|
|
||||||
})
|
|
||||||
|
|
||||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
|
||||||
Enabled: true,
|
|
||||||
Engine: "apple",
|
|
||||||
ServerName: "localhost",
|
|
||||||
MinVersion: "1.2",
|
|
||||||
MaxVersion: "1.2",
|
|
||||||
ALPN: badoption.Listable[string]{"h2"},
|
|
||||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer clientConn.Close()
|
|
||||||
|
|
||||||
clientState := clientConn.ConnectionState()
|
|
||||||
if clientState.Version != stdtls.VersionTLS12 {
|
|
||||||
t.Fatalf("unexpected negotiated version: %x", clientState.Version)
|
|
||||||
}
|
|
||||||
if clientState.NegotiatedProtocol != "h2" {
|
|
||||||
t.Fatalf("unexpected negotiated protocol: %q", clientState.NegotiatedProtocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
result := <-serverResult
|
|
||||||
if result.err != nil {
|
|
||||||
t.Fatal(result.err)
|
|
||||||
}
|
|
||||||
if result.state.Version != stdtls.VersionTLS12 {
|
|
||||||
t.Fatalf("server negotiated unexpected version: %x", result.state.Version)
|
|
||||||
}
|
|
||||||
if result.state.NegotiatedProtocol != "h2" {
|
|
||||||
t.Fatalf("server negotiated unexpected protocol: %q", result.state.NegotiatedProtocol)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppleClientHandshakeRejectsVersionMismatch(t *testing.T) {
|
|
||||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
|
||||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
|
||||||
Certificates: []stdtls.Certificate{serverCertificate},
|
|
||||||
MinVersion: stdtls.VersionTLS13,
|
|
||||||
MaxVersion: stdtls.VersionTLS13,
|
|
||||||
})
|
|
||||||
|
|
||||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
|
||||||
Enabled: true,
|
|
||||||
Engine: "apple",
|
|
||||||
ServerName: "localhost",
|
|
||||||
MaxVersion: "1.2",
|
|
||||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
clientConn.Close()
|
|
||||||
t.Fatal("expected version mismatch handshake to fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
if result := <-serverResult; result.err == nil {
|
|
||||||
t.Fatal("expected server handshake to fail on version mismatch")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppleClientHandshakeRejectsServerNameMismatch(t *testing.T) {
|
|
||||||
serverCertificate, serverCertificatePEM := newAppleTestCertificate(t, "localhost")
|
|
||||||
serverResult, serverAddress := startAppleTLSTestServer(t, &stdtls.Config{
|
|
||||||
Certificates: []stdtls.Certificate{serverCertificate},
|
|
||||||
})
|
|
||||||
|
|
||||||
clientConn, err := newAppleTestClientConn(t, serverAddress, option.OutboundTLSOptions{
|
|
||||||
Enabled: true,
|
|
||||||
Engine: "apple",
|
|
||||||
ServerName: "example.com",
|
|
||||||
Certificate: badoption.Listable[string]{serverCertificatePEM},
|
|
||||||
})
|
|
||||||
if err == nil {
|
|
||||||
clientConn.Close()
|
|
||||||
t.Fatal("expected server name mismatch handshake to fail")
|
|
||||||
}
|
|
||||||
|
|
||||||
if result := <-serverResult; result.err == nil {
|
|
||||||
t.Fatal("expected server handshake to fail on server name mismatch")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAppleTestCertificate(t *testing.T, serverName string) (stdtls.Certificate, string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
privateKeyPEM, certificatePEM, err := GenerateCertificate(nil, nil, time.Now, serverName, time.Now().Add(time.Hour))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
certificate, err := stdtls.X509KeyPair(certificatePEM, privateKeyPEM)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return certificate, string(certificatePEM)
|
|
||||||
}
|
|
||||||
|
|
||||||
func startAppleTLSTestServer(t *testing.T, tlsConfig *stdtls.Config) (<-chan appleTLSServerResult, string) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
listener.Close()
|
|
||||||
})
|
|
||||||
|
|
||||||
if tcpListener, isTCP := listener.(*net.TCPListener); isTCP {
|
|
||||||
err = tcpListener.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(chan appleTLSServerResult, 1)
|
|
||||||
go func() {
|
|
||||||
defer close(result)
|
|
||||||
|
|
||||||
conn, err := listener.Accept()
|
|
||||||
if err != nil {
|
|
||||||
result <- appleTLSServerResult{err: err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
err = conn.SetDeadline(time.Now().Add(appleTLSTestTimeout))
|
|
||||||
if err != nil {
|
|
||||||
result <- appleTLSServerResult{err: err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConn := stdtls.Server(conn, tlsConfig)
|
|
||||||
defer tlsConn.Close()
|
|
||||||
|
|
||||||
err = tlsConn.Handshake()
|
|
||||||
if err != nil {
|
|
||||||
result <- appleTLSServerResult{err: err}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
result <- appleTLSServerResult{state: tlsConn.ConnectionState()}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return result, listener.Addr().String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAppleTestClientConn(t *testing.T, serverAddress string, options option.OutboundTLSOptions) (Conn, error) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), appleTLSTestTimeout)
|
|
||||||
t.Cleanup(cancel)
|
|
||||||
|
|
||||||
clientConfig, err := NewClientWithOptions(ClientOptions{
|
|
||||||
Context: ctx,
|
|
||||||
Logger: logger.NOP(),
|
|
||||||
ServerAddress: "",
|
|
||||||
Options: options,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.DialTimeout("tcp", serverAddress, appleTLSTestTimeout)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConn, err := ClientHandshake(ctx, conn, clientConfig)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tlsConn, nil
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
//go:build !darwin || !cgo
|
|
||||||
|
|
||||||
package tls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newAppleClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
|
||||||
return nil, E.New("Apple TLS engine is not available on non-Apple platforms")
|
|
||||||
}
|
|
||||||
@@ -8,16 +8,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/common/badtls"
|
"github.com/sagernet/sing-box/common/badtls"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
aTLS "github.com/sagernet/sing/common/tls"
|
aTLS "github.com/sagernet/sing/common/tls"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errMissingServerName = E.New("missing server_name or insecure=true")
|
|
||||||
|
|
||||||
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
func NewDialerFromOptions(ctx context.Context, logger logger.ContextLogger, dialer N.Dialer, serverAddress string, options option.OutboundTLSOptions) (N.Dialer, error) {
|
||||||
if !options.Enabled {
|
if !options.Enabled {
|
||||||
return dialer, nil
|
return dialer, nil
|
||||||
@@ -44,12 +42,11 @@ func NewClient(ctx context.Context, logger logger.ContextLogger, serverAddress s
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ClientOptions struct {
|
type ClientOptions struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
Logger logger.ContextLogger
|
Logger logger.ContextLogger
|
||||||
ServerAddress string
|
ServerAddress string
|
||||||
Options option.OutboundTLSOptions
|
Options option.OutboundTLSOptions
|
||||||
AllowEmptyServerName bool
|
KTLSCompatible bool
|
||||||
KTLSCompatible bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClientWithOptions(options ClientOptions) (Config, error) {
|
func NewClientWithOptions(options ClientOptions) (Config, error) {
|
||||||
@@ -64,22 +61,17 @@ func NewClientWithOptions(options ClientOptions) (Config, error) {
|
|||||||
if options.Options.KernelRx {
|
if options.Options.KernelRx {
|
||||||
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
|
options.Logger.Warn("enabling kTLS RX will definitely reduce performance, please checkout https://sing-box.sagernet.org/configuration/shared/tls/#kernel_rx")
|
||||||
}
|
}
|
||||||
switch options.Options.Engine {
|
|
||||||
case "", "go":
|
|
||||||
case "apple":
|
|
||||||
return newAppleClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
|
||||||
default:
|
|
||||||
return nil, E.New("unknown tls engine: ", options.Options.Engine)
|
|
||||||
}
|
|
||||||
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
if options.Options.Reality != nil && options.Options.Reality.Enabled {
|
||||||
return newRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
return NewRealityClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||||
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
} else if options.Options.UTLS != nil && options.Options.UTLS.Enabled {
|
||||||
return newUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
return NewUTLSClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||||
}
|
}
|
||||||
return newSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options, options.AllowEmptyServerName)
|
return NewSTDClient(options.Context, options.Logger, options.ServerAddress, options.Options)
|
||||||
}
|
}
|
||||||
|
|
||||||
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
func ClientHandshake(ctx context.Context, conn net.Conn, config Config) (Conn, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||||
|
defer cancel()
|
||||||
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
|
tlsConn, err := aTLS.ClientHandshake(ctx, conn, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -52,15 +52,11 @@ type RealityClientConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
|
||||||
if options.UTLS == nil || !options.UTLS.Enabled {
|
if options.UTLS == nil || !options.UTLS.Enabled {
|
||||||
return nil, E.New("uTLS is required by reality client")
|
return nil, E.New("uTLS is required by reality client")
|
||||||
}
|
}
|
||||||
|
|
||||||
uClient, err := newUTLSClient(ctx, logger, serverAddress, options, allowEmptyServerName)
|
uClient, err := NewUTLSClient(ctx, logger, serverAddress, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -112,14 +108,6 @@ func (e *RealityClientConfig) SetNextProtos(nextProto []string) {
|
|||||||
e.uClient.SetNextProtos(nextProto)
|
e.uClient.SetNextProtos(nextProto)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *RealityClientConfig) HandshakeTimeout() time.Duration {
|
|
||||||
return e.uClient.HandshakeTimeout()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *RealityClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
|
||||||
e.uClient.SetHandshakeTimeout(timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) {
|
func (e *RealityClientConfig) STDConfig() (*STDConfig, error) {
|
||||||
return nil, E.New("unsupported usage for reality")
|
return nil, E.New("unsupported usage for reality")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ import (
|
|||||||
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
|
var _ ServerConfigCompat = (*RealityServerConfig)(nil)
|
||||||
|
|
||||||
type RealityServerConfig struct {
|
type RealityServerConfig struct {
|
||||||
config *utls.RealityConfig
|
config *utls.RealityConfig
|
||||||
handshakeTimeout time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
func NewRealityServer(ctx context.Context, logger log.ContextLogger, options option.InboundTLSOptions) (ServerConfig, error) {
|
||||||
@@ -131,16 +130,7 @@ func NewRealityServer(ctx context.Context, logger log.ContextLogger, options opt
|
|||||||
if options.ECH != nil && options.ECH.Enabled {
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
return nil, E.New("Reality is conflict with ECH")
|
return nil, E.New("Reality is conflict with ECH")
|
||||||
}
|
}
|
||||||
var handshakeTimeout time.Duration
|
var config ServerConfig = &RealityServerConfig{&tlsConfig}
|
||||||
if options.HandshakeTimeout > 0 {
|
|
||||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
|
||||||
} else {
|
|
||||||
handshakeTimeout = C.TCPTimeout
|
|
||||||
}
|
|
||||||
var config ServerConfig = &RealityServerConfig{
|
|
||||||
config: &tlsConfig,
|
|
||||||
handshakeTimeout: handshakeTimeout,
|
|
||||||
}
|
|
||||||
if options.KernelTx || options.KernelRx {
|
if options.KernelTx || options.KernelRx {
|
||||||
if !C.IsLinux {
|
if !C.IsLinux {
|
||||||
return nil, E.New("kTLS is only supported on Linux")
|
return nil, E.New("kTLS is only supported on Linux")
|
||||||
@@ -171,14 +161,6 @@ func (c *RealityServerConfig) SetNextProtos(nextProto []string) {
|
|||||||
c.config.NextProtos = nextProto
|
c.config.NextProtos = nextProto
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *RealityServerConfig) HandshakeTimeout() time.Duration {
|
|
||||||
return c.handshakeTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RealityServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
|
||||||
c.handshakeTimeout = timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) {
|
func (c *RealityServerConfig) STDConfig() (*tls.Config, error) {
|
||||||
return nil, E.New("unsupported usage for reality")
|
return nil, E.New("unsupported usage for reality")
|
||||||
}
|
}
|
||||||
@@ -209,8 +191,7 @@ func (c *RealityServerConfig) ServerHandshake(ctx context.Context, conn net.Conn
|
|||||||
|
|
||||||
func (c *RealityServerConfig) Clone() Config {
|
func (c *RealityServerConfig) Clone() Config {
|
||||||
return &RealityServerConfig{
|
return &RealityServerConfig{
|
||||||
config: c.config.Clone(),
|
config: c.config.Clone(),
|
||||||
handshakeTimeout: c.handshakeTimeout,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,11 +46,8 @@ func NewServerWithOptions(options ServerOptions) (ServerConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
func ServerHandshake(ctx context.Context, conn net.Conn, config ServerConfig) (Conn, error) {
|
||||||
if config.HandshakeTimeout() == 0 {
|
ctx, cancel := context.WithTimeout(ctx, C.TCPTimeout)
|
||||||
var cancel context.CancelFunc
|
defer cancel()
|
||||||
ctx, cancel = context.WithTimeout(ctx, C.TCPTimeout)
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
|
tlsConn, err := aTLS.ServerHandshake(ctx, conn, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -24,30 +24,16 @@ import (
|
|||||||
type STDClientConfig struct {
|
type STDClientConfig struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
config *tls.Config
|
config *tls.Config
|
||||||
serverName string
|
|
||||||
disableSNI bool
|
|
||||||
verifyServerName bool
|
|
||||||
handshakeTimeout time.Duration
|
|
||||||
fragment bool
|
fragment bool
|
||||||
fragmentFallbackDelay time.Duration
|
fragmentFallbackDelay time.Duration
|
||||||
recordFragment bool
|
recordFragment bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDClientConfig) ServerName() string {
|
func (c *STDClientConfig) ServerName() string {
|
||||||
return c.serverName
|
return c.config.ServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDClientConfig) SetServerName(serverName string) {
|
func (c *STDClientConfig) SetServerName(serverName string) {
|
||||||
c.serverName = serverName
|
|
||||||
if c.disableSNI {
|
|
||||||
c.config.ServerName = ""
|
|
||||||
if c.verifyServerName {
|
|
||||||
c.config.VerifyConnection = verifyConnection(c.config.RootCAs, c.config.Time, serverName)
|
|
||||||
} else {
|
|
||||||
c.config.VerifyConnection = nil
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.config.ServerName = serverName
|
c.config.ServerName = serverName
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,14 +45,6 @@ func (c *STDClientConfig) SetNextProtos(nextProto []string) {
|
|||||||
c.config.NextProtos = nextProto
|
c.config.NextProtos = nextProto
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDClientConfig) HandshakeTimeout() time.Duration {
|
|
||||||
return c.handshakeTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
|
||||||
c.handshakeTimeout = timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDClientConfig) STDConfig() (*STDConfig, error) {
|
func (c *STDClientConfig) STDConfig() (*STDConfig, error) {
|
||||||
return c.config, nil
|
return c.config, nil
|
||||||
}
|
}
|
||||||
@@ -79,19 +57,13 @@ func (c *STDClientConfig) Client(conn net.Conn) (Conn, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDClientConfig) Clone() Config {
|
func (c *STDClientConfig) Clone() Config {
|
||||||
cloned := &STDClientConfig{
|
return &STDClientConfig{
|
||||||
ctx: c.ctx,
|
ctx: c.ctx,
|
||||||
config: c.config.Clone(),
|
config: c.config.Clone(),
|
||||||
serverName: c.serverName,
|
|
||||||
disableSNI: c.disableSNI,
|
|
||||||
verifyServerName: c.verifyServerName,
|
|
||||||
handshakeTimeout: c.handshakeTimeout,
|
|
||||||
fragment: c.fragment,
|
fragment: c.fragment,
|
||||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
||||||
recordFragment: c.recordFragment,
|
recordFragment: c.recordFragment,
|
||||||
}
|
}
|
||||||
cloned.SetServerName(cloned.serverName)
|
|
||||||
return cloned
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDClientConfig) ECHConfigList() []byte {
|
func (c *STDClientConfig) ECHConfigList() []byte {
|
||||||
@@ -103,27 +75,41 @@ func (c *STDClientConfig) SetECHConfigList(EncryptedClientHelloConfigList []byte
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
return newSTDClient(ctx, logger, serverAddress, options, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
|
||||||
var serverName string
|
var serverName string
|
||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
serverName = options.ServerName
|
serverName = options.ServerName
|
||||||
} else if serverAddress != "" {
|
} else if serverAddress != "" {
|
||||||
serverName = serverAddress
|
serverName = serverAddress
|
||||||
}
|
}
|
||||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
if serverName == "" && !options.Insecure {
|
||||||
return nil, errMissingServerName
|
return nil, E.New("missing server_name or insecure=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsConfig tls.Config
|
var tlsConfig tls.Config
|
||||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||||
|
if !options.DisableSNI {
|
||||||
|
tlsConfig.ServerName = serverName
|
||||||
|
}
|
||||||
if options.Insecure {
|
if options.Insecure {
|
||||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||||
} else if options.DisableSNI {
|
} else if options.DisableSNI {
|
||||||
tlsConfig.InsecureSkipVerify = true
|
tlsConfig.InsecureSkipVerify = true
|
||||||
|
tlsConfig.VerifyConnection = func(state tls.ConnectionState) error {
|
||||||
|
verifyOptions := x509.VerifyOptions{
|
||||||
|
Roots: tlsConfig.RootCAs,
|
||||||
|
DNSName: serverName,
|
||||||
|
Intermediates: x509.NewCertPool(),
|
||||||
|
}
|
||||||
|
for _, cert := range state.PeerCertificates[1:] {
|
||||||
|
verifyOptions.Intermediates.AddCert(cert)
|
||||||
|
}
|
||||||
|
if tlsConfig.Time != nil {
|
||||||
|
verifyOptions.CurrentTime = tlsConfig.Time()
|
||||||
|
}
|
||||||
|
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||||
@@ -212,24 +198,7 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
|
|||||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||||
return nil, E.New("client certificate and client key must be provided together")
|
return nil, E.New("client certificate and client key must be provided together")
|
||||||
}
|
}
|
||||||
var handshakeTimeout time.Duration
|
var config Config = &STDClientConfig{ctx, &tlsConfig, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||||
if options.HandshakeTimeout > 0 {
|
|
||||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
|
||||||
} else {
|
|
||||||
handshakeTimeout = C.TCPTimeout
|
|
||||||
}
|
|
||||||
var config Config = &STDClientConfig{
|
|
||||||
ctx: ctx,
|
|
||||||
config: &tlsConfig,
|
|
||||||
serverName: serverName,
|
|
||||||
disableSNI: options.DisableSNI,
|
|
||||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
|
||||||
handshakeTimeout: handshakeTimeout,
|
|
||||||
fragment: options.Fragment,
|
|
||||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
|
||||||
recordFragment: options.RecordFragment,
|
|
||||||
}
|
|
||||||
config.SetServerName(serverName)
|
|
||||||
if options.ECH != nil && options.ECH.Enabled {
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
var err error
|
var err error
|
||||||
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
|
config, err = parseECHClientConfig(ctx, config.(ECHCapableConfig), options)
|
||||||
@@ -251,27 +220,6 @@ func newSTDClient(ctx context.Context, logger logger.ContextLogger, serverAddres
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyConnection(rootCAs *x509.CertPool, timeFunc func() time.Time, serverName string) func(state tls.ConnectionState) error {
|
|
||||||
return func(state tls.ConnectionState) error {
|
|
||||||
if serverName == "" {
|
|
||||||
return errMissingServerName
|
|
||||||
}
|
|
||||||
verifyOptions := x509.VerifyOptions{
|
|
||||||
Roots: rootCAs,
|
|
||||||
DNSName: serverName,
|
|
||||||
Intermediates: x509.NewCertPool(),
|
|
||||||
}
|
|
||||||
for _, cert := range state.PeerCertificates[1:] {
|
|
||||||
verifyOptions.Intermediates.AddCert(cert)
|
|
||||||
}
|
|
||||||
if timeFunc != nil {
|
|
||||||
verifyOptions.CurrentTime = timeFunc()
|
|
||||||
}
|
|
||||||
_, err := state.PeerCertificates[0].Verify(verifyOptions)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
|
func verifyPublicKeySHA256(knownHashValues [][]byte, rawCerts [][]byte, timeFunc func() time.Time) error {
|
||||||
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
|
leafCertificate, err := x509.ParseCertificate(rawCerts[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ func getACMENextProtos(provider adapter.CertificateProvider) []string {
|
|||||||
type STDServerConfig struct {
|
type STDServerConfig struct {
|
||||||
access sync.RWMutex
|
access sync.RWMutex
|
||||||
config *tls.Config
|
config *tls.Config
|
||||||
handshakeTimeout time.Duration
|
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
certificateProvider managedCertificateProvider
|
certificateProvider managedCertificateProvider
|
||||||
acmeService adapter.SimpleLifecycle
|
acmeService adapter.SimpleLifecycle
|
||||||
@@ -140,18 +139,6 @@ func (c *STDServerConfig) SetNextProtos(nextProto []string) {
|
|||||||
c.config = config
|
c.config = config
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STDServerConfig) HandshakeTimeout() time.Duration {
|
|
||||||
c.access.RLock()
|
|
||||||
defer c.access.RUnlock()
|
|
||||||
return c.handshakeTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) SetHandshakeTimeout(timeout time.Duration) {
|
|
||||||
c.access.Lock()
|
|
||||||
defer c.access.Unlock()
|
|
||||||
c.handshakeTimeout = timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *STDServerConfig) hasACMEALPN() bool {
|
func (c *STDServerConfig) hasACMEALPN() bool {
|
||||||
if c.acmeService != nil {
|
if c.acmeService != nil {
|
||||||
return true
|
return true
|
||||||
@@ -178,8 +165,7 @@ func (c *STDServerConfig) Server(conn net.Conn) (Conn, error) {
|
|||||||
|
|
||||||
func (c *STDServerConfig) Clone() Config {
|
func (c *STDServerConfig) Clone() Config {
|
||||||
return &STDServerConfig{
|
return &STDServerConfig{
|
||||||
config: c.config.Clone(),
|
config: c.config.Clone(),
|
||||||
handshakeTimeout: c.handshakeTimeout,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -485,15 +471,8 @@ func NewSTDServer(ctx context.Context, logger log.ContextLogger, options option.
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var handshakeTimeout time.Duration
|
|
||||||
if options.HandshakeTimeout > 0 {
|
|
||||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
|
||||||
} else {
|
|
||||||
handshakeTimeout = C.TCPTimeout
|
|
||||||
}
|
|
||||||
serverConfig := &STDServerConfig{
|
serverConfig := &STDServerConfig{
|
||||||
config: tlsConfig,
|
config: tlsConfig,
|
||||||
handshakeTimeout: handshakeTimeout,
|
|
||||||
logger: logger,
|
logger: logger,
|
||||||
certificateProvider: certificateProvider,
|
certificateProvider: certificateProvider,
|
||||||
acmeService: acmeService,
|
acmeService: acmeService,
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ import (
|
|||||||
type UTLSClientConfig struct {
|
type UTLSClientConfig struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
config *utls.Config
|
config *utls.Config
|
||||||
serverName string
|
|
||||||
disableSNI bool
|
|
||||||
verifyServerName bool
|
|
||||||
handshakeTimeout time.Duration
|
|
||||||
id utls.ClientHelloID
|
id utls.ClientHelloID
|
||||||
fragment bool
|
fragment bool
|
||||||
fragmentFallbackDelay time.Duration
|
fragmentFallbackDelay time.Duration
|
||||||
@@ -39,20 +35,10 @@ type UTLSClientConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *UTLSClientConfig) ServerName() string {
|
func (c *UTLSClientConfig) ServerName() string {
|
||||||
return c.serverName
|
return c.config.ServerName
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UTLSClientConfig) SetServerName(serverName string) {
|
func (c *UTLSClientConfig) SetServerName(serverName string) {
|
||||||
c.serverName = serverName
|
|
||||||
if c.disableSNI {
|
|
||||||
c.config.ServerName = ""
|
|
||||||
if c.verifyServerName {
|
|
||||||
c.config.InsecureServerNameToVerify = serverName
|
|
||||||
} else {
|
|
||||||
c.config.InsecureServerNameToVerify = ""
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.config.ServerName = serverName
|
c.config.ServerName = serverName
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,14 +53,6 @@ func (c *UTLSClientConfig) SetNextProtos(nextProto []string) {
|
|||||||
c.config.NextProtos = nextProto
|
c.config.NextProtos = nextProto
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UTLSClientConfig) HandshakeTimeout() time.Duration {
|
|
||||||
return c.handshakeTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UTLSClientConfig) SetHandshakeTimeout(timeout time.Duration) {
|
|
||||||
c.handshakeTimeout = timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) {
|
func (c *UTLSClientConfig) STDConfig() (*STDConfig, error) {
|
||||||
return nil, E.New("unsupported usage for uTLS")
|
return nil, E.New("unsupported usage for uTLS")
|
||||||
}
|
}
|
||||||
@@ -91,20 +69,9 @@ func (c *UTLSClientConfig) SetSessionIDGenerator(generator func(clientHello []by
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *UTLSClientConfig) Clone() Config {
|
func (c *UTLSClientConfig) Clone() Config {
|
||||||
cloned := &UTLSClientConfig{
|
return &UTLSClientConfig{
|
||||||
ctx: c.ctx,
|
c.ctx, c.config.Clone(), c.id, c.fragment, c.fragmentFallbackDelay, c.recordFragment,
|
||||||
config: c.config.Clone(),
|
|
||||||
serverName: c.serverName,
|
|
||||||
disableSNI: c.disableSNI,
|
|
||||||
verifyServerName: c.verifyServerName,
|
|
||||||
handshakeTimeout: c.handshakeTimeout,
|
|
||||||
id: c.id,
|
|
||||||
fragment: c.fragment,
|
|
||||||
fragmentFallbackDelay: c.fragmentFallbackDelay,
|
|
||||||
recordFragment: c.recordFragment,
|
|
||||||
}
|
}
|
||||||
cloned.SetServerName(cloned.serverName)
|
|
||||||
return cloned
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UTLSClientConfig) ECHConfigList() []byte {
|
func (c *UTLSClientConfig) ECHConfigList() []byte {
|
||||||
@@ -176,29 +143,29 @@ func (c *utlsALPNWrapper) HandshakeContext(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
|
||||||
var serverName string
|
var serverName string
|
||||||
if options.ServerName != "" {
|
if options.ServerName != "" {
|
||||||
serverName = options.ServerName
|
serverName = options.ServerName
|
||||||
} else if serverAddress != "" {
|
} else if serverAddress != "" {
|
||||||
serverName = serverAddress
|
serverName = serverAddress
|
||||||
}
|
}
|
||||||
if serverName == "" && !options.Insecure && !allowEmptyServerName {
|
if serverName == "" && !options.Insecure {
|
||||||
return nil, errMissingServerName
|
return nil, E.New("missing server_name or insecure=true")
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsConfig utls.Config
|
var tlsConfig utls.Config
|
||||||
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
tlsConfig.Time = ntp.TimeFuncFromContext(ctx)
|
||||||
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
tlsConfig.RootCAs = adapter.RootPoolFromContext(ctx)
|
||||||
|
if !options.DisableSNI {
|
||||||
|
tlsConfig.ServerName = serverName
|
||||||
|
}
|
||||||
if options.Insecure {
|
if options.Insecure {
|
||||||
tlsConfig.InsecureSkipVerify = options.Insecure
|
tlsConfig.InsecureSkipVerify = options.Insecure
|
||||||
} else if options.DisableSNI {
|
} else if options.DisableSNI {
|
||||||
if options.Reality != nil && options.Reality.Enabled {
|
if options.Reality != nil && options.Reality.Enabled {
|
||||||
return nil, E.New("disable_sni is unsupported in reality")
|
return nil, E.New("disable_sni is unsupported in reality")
|
||||||
}
|
}
|
||||||
|
tlsConfig.InsecureServerNameToVerify = serverName
|
||||||
}
|
}
|
||||||
if len(options.CertificatePublicKeySHA256) > 0 {
|
if len(options.CertificatePublicKeySHA256) > 0 {
|
||||||
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
if len(options.Certificate) > 0 || options.CertificatePath != "" {
|
||||||
@@ -284,29 +251,11 @@ func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddre
|
|||||||
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
} else if len(clientCertificate) > 0 || len(clientKey) > 0 {
|
||||||
return nil, E.New("client certificate and client key must be provided together")
|
return nil, E.New("client certificate and client key must be provided together")
|
||||||
}
|
}
|
||||||
var handshakeTimeout time.Duration
|
|
||||||
if options.HandshakeTimeout > 0 {
|
|
||||||
handshakeTimeout = options.HandshakeTimeout.Build()
|
|
||||||
} else {
|
|
||||||
handshakeTimeout = C.TCPTimeout
|
|
||||||
}
|
|
||||||
id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
|
id, err := uTLSClientHelloID(options.UTLS.Fingerprint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var config Config = &UTLSClientConfig{
|
var config Config = &UTLSClientConfig{ctx, &tlsConfig, id, options.Fragment, time.Duration(options.FragmentFallbackDelay), options.RecordFragment}
|
||||||
ctx: ctx,
|
|
||||||
config: &tlsConfig,
|
|
||||||
serverName: serverName,
|
|
||||||
disableSNI: options.DisableSNI,
|
|
||||||
verifyServerName: options.DisableSNI && !options.Insecure,
|
|
||||||
handshakeTimeout: handshakeTimeout,
|
|
||||||
id: id,
|
|
||||||
fragment: options.Fragment,
|
|
||||||
fragmentFallbackDelay: time.Duration(options.FragmentFallbackDelay),
|
|
||||||
recordFragment: options.RecordFragment,
|
|
||||||
}
|
|
||||||
config.SetServerName(serverName)
|
|
||||||
if options.ECH != nil && options.ECH.Enabled {
|
if options.ECH != nil && options.ECH.Enabled {
|
||||||
if options.Reality != nil && options.Reality.Enabled {
|
if options.Reality != nil && options.Reality.Enabled {
|
||||||
return nil, E.New("Reality is conflict with ECH")
|
return nil, E.New("Reality is conflict with ECH")
|
||||||
|
|||||||
@@ -12,18 +12,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
return newUTLSClient(ctx, logger, serverAddress, options, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUTLSClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
|
||||||
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
return nil, E.New(`uTLS is not included in this build, rebuild with -tags with_utls`)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
|
||||||
return newRealityClient(ctx, logger, serverAddress, options, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions, allowEmptyServerName bool) (Config, error) {
|
|
||||||
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
|
return nil, E.New(`uTLS, which is required by reality is not included in this build, rebuild with -tags with_utls`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,18 +15,19 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DNSTypeLegacy = "legacy"
|
DNSTypeLegacy = "legacy"
|
||||||
DNSTypeUDP = "udp"
|
DNSTypeLegacyRcode = "legacy_rcode"
|
||||||
DNSTypeTCP = "tcp"
|
DNSTypeUDP = "udp"
|
||||||
DNSTypeTLS = "tls"
|
DNSTypeTCP = "tcp"
|
||||||
DNSTypeHTTPS = "https"
|
DNSTypeTLS = "tls"
|
||||||
DNSTypeQUIC = "quic"
|
DNSTypeHTTPS = "https"
|
||||||
DNSTypeHTTP3 = "h3"
|
DNSTypeQUIC = "quic"
|
||||||
DNSTypeLocal = "local"
|
DNSTypeHTTP3 = "h3"
|
||||||
DNSTypeHosts = "hosts"
|
DNSTypeLocal = "local"
|
||||||
DNSTypeFakeIP = "fakeip"
|
DNSTypeHosts = "hosts"
|
||||||
DNSTypeDHCP = "dhcp"
|
DNSTypeFakeIP = "fakeip"
|
||||||
DNSTypeTailscale = "tailscale"
|
DNSTypeDHCP = "dhcp"
|
||||||
|
DNSTypeTailscale = "tailscale"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ const (
|
|||||||
TypeTUIC = "tuic"
|
TypeTUIC = "tuic"
|
||||||
TypeHysteria2 = "hysteria2"
|
TypeHysteria2 = "hysteria2"
|
||||||
TypeTailscale = "tailscale"
|
TypeTailscale = "tailscale"
|
||||||
TypeCloudflared = "cloudflared"
|
|
||||||
TypeDERP = "derp"
|
TypeDERP = "derp"
|
||||||
TypeResolved = "resolved"
|
TypeResolved = "resolved"
|
||||||
TypeSSMAPI = "ssm-api"
|
TypeSSMAPI = "ssm-api"
|
||||||
@@ -91,8 +90,6 @@ func ProxyDisplayName(proxyType string) string {
|
|||||||
return "AnyTLS"
|
return "AnyTLS"
|
||||||
case TypeTailscale:
|
case TypeTailscale:
|
||||||
return "Tailscale"
|
return "Tailscale"
|
||||||
case TypeCloudflared:
|
|
||||||
return "Cloudflared"
|
|
||||||
case TypeSelector:
|
case TypeSelector:
|
||||||
return "Selector"
|
return "Selector"
|
||||||
case TypeURLTest:
|
case TypeURLTest:
|
||||||
|
|||||||
@@ -23,15 +23,12 @@ const (
|
|||||||
RuleSetVersion2
|
RuleSetVersion2
|
||||||
RuleSetVersion3
|
RuleSetVersion3
|
||||||
RuleSetVersion4
|
RuleSetVersion4
|
||||||
RuleSetVersion5
|
RuleSetVersionCurrent = RuleSetVersion4
|
||||||
RuleSetVersionCurrent = RuleSetVersion5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RuleActionTypeRoute = "route"
|
RuleActionTypeRoute = "route"
|
||||||
RuleActionTypeRouteOptions = "route-options"
|
RuleActionTypeRouteOptions = "route-options"
|
||||||
RuleActionTypeEvaluate = "evaluate"
|
|
||||||
RuleActionTypeRespond = "respond"
|
|
||||||
RuleActionTypeDirect = "direct"
|
RuleActionTypeDirect = "direct"
|
||||||
RuleActionTypeBypass = "bypass"
|
RuleActionTypeBypass = "bypass"
|
||||||
RuleActionTypeReject = "reject"
|
RuleActionTypeReject = "reject"
|
||||||
|
|||||||
@@ -87,17 +87,12 @@ func (s *StartedService) newInstance(profileContent string, overrideOptions *Ove
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if s.oomKillerEnabled {
|
if s.oomKiller && C.IsIos {
|
||||||
if !common.Any(options.Services, func(it option.Service) bool {
|
if !common.Any(options.Services, func(it option.Service) bool {
|
||||||
return it.Type == C.TypeOOMKiller
|
return it.Type == C.TypeOOMKiller
|
||||||
}) {
|
}) {
|
||||||
oomOptions := &option.OOMKillerServiceOptions{
|
|
||||||
KillerDisabled: s.oomKillerDisabled,
|
|
||||||
MemoryLimitOverride: s.oomMemoryLimit,
|
|
||||||
}
|
|
||||||
options.Services = append(options.Services, option.Service{
|
options.Services = append(options.Services, option.Service{
|
||||||
Type: C.TypeOOMKiller,
|
Type: C.TypeOOMKiller,
|
||||||
Options: oomOptions,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,5 @@ type PlatformHandler interface {
|
|||||||
ServiceReload() error
|
ServiceReload() error
|
||||||
SystemProxyStatus() (*SystemProxyStatus, error)
|
SystemProxyStatus() (*SystemProxyStatus, error)
|
||||||
SetSystemProxyEnabled(enabled bool) error
|
SetSystemProxyEnabled(enabled bool) error
|
||||||
TriggerNativeCrash() error
|
|
||||||
WriteDebugMessage(message string)
|
WriteDebugMessage(message string)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,14 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
|
||||||
"github.com/sagernet/sing-box/common/networkquality"
|
|
||||||
"github.com/sagernet/sing-box/common/stun"
|
|
||||||
"github.com/sagernet/sing-box/common/urltest"
|
"github.com/sagernet/sing-box/common/urltest"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/experimental/clashapi"
|
"github.com/sagernet/sing-box/experimental/clashapi"
|
||||||
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
||||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/protocol/group"
|
"github.com/sagernet/sing-box/protocol/group"
|
||||||
"github.com/sagernet/sing-box/service/oomkiller"
|
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
"github.com/sagernet/sing/common/batch"
|
"github.com/sagernet/sing/common/batch"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
@@ -30,8 +24,6 @@ import (
|
|||||||
|
|
||||||
"github.com/gofrs/uuid/v5"
|
"github.com/gofrs/uuid/v5"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/codes"
|
|
||||||
"google.golang.org/grpc/status"
|
|
||||||
"google.golang.org/protobuf/types/known/emptypb"
|
"google.golang.org/protobuf/types/known/emptypb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,12 +32,10 @@ var _ StartedServiceServer = (*StartedService)(nil)
|
|||||||
type StartedService struct {
|
type StartedService struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
// platform adapter.PlatformInterface
|
// platform adapter.PlatformInterface
|
||||||
handler PlatformHandler
|
handler PlatformHandler
|
||||||
debug bool
|
debug bool
|
||||||
logMaxLines int
|
logMaxLines int
|
||||||
oomKillerEnabled bool
|
oomKiller bool
|
||||||
oomKillerDisabled bool
|
|
||||||
oomMemoryLimit uint64
|
|
||||||
// workingDirectory string
|
// workingDirectory string
|
||||||
// tempDirectory string
|
// tempDirectory string
|
||||||
// userID int
|
// userID int
|
||||||
@@ -74,12 +64,10 @@ type StartedService struct {
|
|||||||
type ServiceOptions struct {
|
type ServiceOptions struct {
|
||||||
Context context.Context
|
Context context.Context
|
||||||
// Platform adapter.PlatformInterface
|
// Platform adapter.PlatformInterface
|
||||||
Handler PlatformHandler
|
Handler PlatformHandler
|
||||||
Debug bool
|
Debug bool
|
||||||
LogMaxLines int
|
LogMaxLines int
|
||||||
OOMKillerEnabled bool
|
OOMKiller bool
|
||||||
OOMKillerDisabled bool
|
|
||||||
OOMMemoryLimit uint64
|
|
||||||
// WorkingDirectory string
|
// WorkingDirectory string
|
||||||
// TempDirectory string
|
// TempDirectory string
|
||||||
// UserID int
|
// UserID int
|
||||||
@@ -91,12 +79,10 @@ func NewStartedService(options ServiceOptions) *StartedService {
|
|||||||
s := &StartedService{
|
s := &StartedService{
|
||||||
ctx: options.Context,
|
ctx: options.Context,
|
||||||
// platform: options.Platform,
|
// platform: options.Platform,
|
||||||
handler: options.Handler,
|
handler: options.Handler,
|
||||||
debug: options.Debug,
|
debug: options.Debug,
|
||||||
logMaxLines: options.LogMaxLines,
|
logMaxLines: options.LogMaxLines,
|
||||||
oomKillerEnabled: options.OOMKillerEnabled,
|
oomKiller: options.OOMKiller,
|
||||||
oomKillerDisabled: options.OOMKillerDisabled,
|
|
||||||
oomMemoryLimit: options.OOMMemoryLimit,
|
|
||||||
// workingDirectory: options.WorkingDirectory,
|
// workingDirectory: options.WorkingDirectory,
|
||||||
// tempDirectory: options.TempDirectory,
|
// tempDirectory: options.TempDirectory,
|
||||||
// userID: options.UserID,
|
// userID: options.UserID,
|
||||||
@@ -696,42 +682,7 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &emptypb.Empty{}, nil
|
return nil, err
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) {
|
|
||||||
if !s.debug {
|
|
||||||
return nil, status.Error(codes.PermissionDenied, "debug crash trigger unavailable")
|
|
||||||
}
|
|
||||||
if request == nil {
|
|
||||||
return nil, status.Error(codes.InvalidArgument, "missing debug crash request")
|
|
||||||
}
|
|
||||||
switch request.Type {
|
|
||||||
case DebugCrashRequest_GO:
|
|
||||||
time.AfterFunc(200*time.Millisecond, func() {
|
|
||||||
*(*int)(unsafe.Pointer(uintptr(0))) = 0
|
|
||||||
})
|
|
||||||
case DebugCrashRequest_NATIVE:
|
|
||||||
err := s.handler.TriggerNativeCrash()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, status.Error(codes.InvalidArgument, "unknown debug crash type")
|
|
||||||
}
|
|
||||||
return &emptypb.Empty{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StartedService) TriggerOOMReport(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) {
|
|
||||||
instance := s.Instance()
|
|
||||||
if instance == nil {
|
|
||||||
return nil, status.Error(codes.FailedPrecondition, "service not started")
|
|
||||||
}
|
|
||||||
reporter := service.FromContext[oomkiller.OOMReporter](instance.ctx)
|
|
||||||
if reporter == nil {
|
|
||||||
return nil, status.Error(codes.Unavailable, "OOM reporter not available")
|
|
||||||
}
|
|
||||||
return &emptypb.Empty{}, reporter.WriteReport(memory.Total())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
|
func (s *StartedService) SubscribeConnections(request *SubscribeConnectionsRequest, server grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||||
@@ -1068,12 +1019,9 @@ func (s *StartedService) GetDeprecatedWarnings(ctx context.Context, empty *empty
|
|||||||
return &DeprecatedWarnings{
|
return &DeprecatedWarnings{
|
||||||
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
|
Warnings: common.Map(notes, func(it deprecated.Note) *DeprecatedWarning {
|
||||||
return &DeprecatedWarning{
|
return &DeprecatedWarning{
|
||||||
Message: it.Message(),
|
Message: it.Message(),
|
||||||
Impending: it.Impending(),
|
Impending: it.Impending(),
|
||||||
MigrationLink: it.MigrationLink,
|
MigrationLink: it.MigrationLink,
|
||||||
Description: it.Description,
|
|
||||||
DeprecatedVersion: it.DeprecatedVersion,
|
|
||||||
ScheduledVersion: it.ScheduledVersion,
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}, nil
|
}, nil
|
||||||
@@ -1085,386 +1033,6 @@ func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty)
|
|||||||
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
|
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error {
|
|
||||||
err := s.waitForStarted(server.Context())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
subscription, done, err := s.urlTestObserver.Subscribe()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer s.urlTestObserver.UnSubscribe(subscription)
|
|
||||||
for {
|
|
||||||
s.serviceAccess.RLock()
|
|
||||||
if s.serviceStatus.Status != ServiceStatus_STARTED {
|
|
||||||
s.serviceAccess.RUnlock()
|
|
||||||
return os.ErrInvalid
|
|
||||||
}
|
|
||||||
boxService := s.instance
|
|
||||||
s.serviceAccess.RUnlock()
|
|
||||||
historyStorage := boxService.urlTestHistoryStorage
|
|
||||||
var list OutboundList
|
|
||||||
for _, ob := range boxService.instance.Outbound().Outbounds() {
|
|
||||||
item := &GroupItem{
|
|
||||||
Tag: ob.Tag(),
|
|
||||||
Type: ob.Type(),
|
|
||||||
}
|
|
||||||
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
|
|
||||||
item.UrlTestTime = history.Time.Unix()
|
|
||||||
item.UrlTestDelay = int32(history.Delay)
|
|
||||||
}
|
|
||||||
list.Outbounds = append(list.Outbounds, item)
|
|
||||||
}
|
|
||||||
for _, ep := range boxService.instance.Endpoint().Endpoints() {
|
|
||||||
item := &GroupItem{
|
|
||||||
Tag: ep.Tag(),
|
|
||||||
Type: ep.Type(),
|
|
||||||
}
|
|
||||||
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil {
|
|
||||||
item.UrlTestTime = history.Time.Unix()
|
|
||||||
item.UrlTestDelay = int32(history.Delay)
|
|
||||||
}
|
|
||||||
list.Outbounds = append(list.Outbounds, item)
|
|
||||||
}
|
|
||||||
err = server.Send(&list)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-subscription:
|
|
||||||
case <-s.ctx.Done():
|
|
||||||
return s.ctx.Err()
|
|
||||||
case <-server.Context().Done():
|
|
||||||
return server.Context().Err()
|
|
||||||
case <-done:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveOutbound(instance *Instance, tag string) (adapter.Outbound, error) {
|
|
||||||
if tag == "" {
|
|
||||||
return instance.instance.Outbound().Default(), nil
|
|
||||||
}
|
|
||||||
outbound, loaded := instance.instance.Outbound().Outbound(tag)
|
|
||||||
if !loaded {
|
|
||||||
return nil, E.New("outbound not found: ", tag)
|
|
||||||
}
|
|
||||||
return outbound, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StartedService) StartNetworkQualityTest(
|
|
||||||
request *NetworkQualityTestRequest,
|
|
||||||
server grpc.ServerStreamingServer[NetworkQualityTestProgress],
|
|
||||||
) error {
|
|
||||||
err := s.waitForStarted(server.Context())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.serviceAccess.RLock()
|
|
||||||
boxService := s.instance
|
|
||||||
s.serviceAccess.RUnlock()
|
|
||||||
|
|
||||||
outbound, err := resolveOutbound(boxService, request.OutboundTag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
|
|
||||||
httpClient := networkquality.NewHTTPClient(resolvedDialer)
|
|
||||||
defer httpClient.CloseIdleConnections()
|
|
||||||
|
|
||||||
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(resolvedDialer, request.Http3)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
result, nqErr := networkquality.Run(networkquality.Options{
|
|
||||||
ConfigURL: request.ConfigURL,
|
|
||||||
HTTPClient: httpClient,
|
|
||||||
NewMeasurementClient: measurementClientFactory,
|
|
||||||
Serial: request.Serial,
|
|
||||||
MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second,
|
|
||||||
Context: server.Context(),
|
|
||||||
OnProgress: func(p networkquality.Progress) {
|
|
||||||
_ = server.Send(&NetworkQualityTestProgress{
|
|
||||||
Phase: int32(p.Phase),
|
|
||||||
DownloadCapacity: p.DownloadCapacity,
|
|
||||||
UploadCapacity: p.UploadCapacity,
|
|
||||||
DownloadRPM: p.DownloadRPM,
|
|
||||||
UploadRPM: p.UploadRPM,
|
|
||||||
IdleLatencyMs: p.IdleLatencyMs,
|
|
||||||
ElapsedMs: p.ElapsedMs,
|
|
||||||
DownloadCapacityAccuracy: int32(p.DownloadCapacityAccuracy),
|
|
||||||
UploadCapacityAccuracy: int32(p.UploadCapacityAccuracy),
|
|
||||||
DownloadRPMAccuracy: int32(p.DownloadRPMAccuracy),
|
|
||||||
UploadRPMAccuracy: int32(p.UploadRPMAccuracy),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if nqErr != nil {
|
|
||||||
return server.Send(&NetworkQualityTestProgress{
|
|
||||||
IsFinal: true,
|
|
||||||
Error: nqErr.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return server.Send(&NetworkQualityTestProgress{
|
|
||||||
Phase: int32(networkquality.PhaseDone),
|
|
||||||
DownloadCapacity: result.DownloadCapacity,
|
|
||||||
UploadCapacity: result.UploadCapacity,
|
|
||||||
DownloadRPM: result.DownloadRPM,
|
|
||||||
UploadRPM: result.UploadRPM,
|
|
||||||
IdleLatencyMs: result.IdleLatencyMs,
|
|
||||||
IsFinal: true,
|
|
||||||
DownloadCapacityAccuracy: int32(result.DownloadCapacityAccuracy),
|
|
||||||
UploadCapacityAccuracy: int32(result.UploadCapacityAccuracy),
|
|
||||||
DownloadRPMAccuracy: int32(result.DownloadRPMAccuracy),
|
|
||||||
UploadRPMAccuracy: int32(result.UploadRPMAccuracy),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StartedService) StartSTUNTest(
|
|
||||||
request *STUNTestRequest,
|
|
||||||
server grpc.ServerStreamingServer[STUNTestProgress],
|
|
||||||
) error {
|
|
||||||
err := s.waitForStarted(server.Context())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.serviceAccess.RLock()
|
|
||||||
boxService := s.instance
|
|
||||||
s.serviceAccess.RUnlock()
|
|
||||||
|
|
||||||
outbound, err := resolveOutbound(boxService, request.OutboundTag)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
|
|
||||||
|
|
||||||
result, stunErr := stun.Run(stun.Options{
|
|
||||||
Server: request.Server,
|
|
||||||
Dialer: resolvedDialer,
|
|
||||||
Context: server.Context(),
|
|
||||||
OnProgress: func(p stun.Progress) {
|
|
||||||
_ = server.Send(&STUNTestProgress{
|
|
||||||
Phase: int32(p.Phase),
|
|
||||||
ExternalAddr: p.ExternalAddr,
|
|
||||||
LatencyMs: p.LatencyMs,
|
|
||||||
NatMapping: int32(p.NATMapping),
|
|
||||||
NatFiltering: int32(p.NATFiltering),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if stunErr != nil {
|
|
||||||
return server.Send(&STUNTestProgress{
|
|
||||||
IsFinal: true,
|
|
||||||
Error: stunErr.Error(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return server.Send(&STUNTestProgress{
|
|
||||||
Phase: int32(stun.PhaseDone),
|
|
||||||
ExternalAddr: result.ExternalAddr,
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
NatMapping: int32(result.NATMapping),
|
|
||||||
NatFiltering: int32(result.NATFiltering),
|
|
||||||
IsFinal: true,
|
|
||||||
NatTypeSupported: result.NATTypeSupported,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StartedService) SubscribeTailscaleStatus(
|
|
||||||
_ *emptypb.Empty,
|
|
||||||
server grpc.ServerStreamingServer[TailscaleStatusUpdate],
|
|
||||||
) error {
|
|
||||||
err := s.waitForStarted(server.Context())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.serviceAccess.RLock()
|
|
||||||
boxService := s.instance
|
|
||||||
s.serviceAccess.RUnlock()
|
|
||||||
|
|
||||||
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
|
|
||||||
if endpointManager == nil {
|
|
||||||
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
type tailscaleEndpoint struct {
|
|
||||||
tag string
|
|
||||||
provider adapter.TailscaleEndpoint
|
|
||||||
}
|
|
||||||
var endpoints []tailscaleEndpoint
|
|
||||||
for _, endpoint := range endpointManager.Endpoints() {
|
|
||||||
if endpoint.Type() != C.TypeTailscale {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
|
||||||
if !loaded {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
endpoints = append(endpoints, tailscaleEndpoint{
|
|
||||||
tag: endpoint.Tag(),
|
|
||||||
provider: provider,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if len(endpoints) == 0 {
|
|
||||||
return status.Error(codes.NotFound, "no Tailscale endpoint found")
|
|
||||||
}
|
|
||||||
|
|
||||||
type taggedStatus struct {
|
|
||||||
tag string
|
|
||||||
status *adapter.TailscaleEndpointStatus
|
|
||||||
}
|
|
||||||
updates := make(chan taggedStatus, len(endpoints))
|
|
||||||
ctx, cancel := context.WithCancel(server.Context())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
var waitGroup sync.WaitGroup
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
waitGroup.Add(1)
|
|
||||||
go func(tag string, provider adapter.TailscaleEndpoint) {
|
|
||||||
defer waitGroup.Done()
|
|
||||||
_ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) {
|
|
||||||
select {
|
|
||||||
case updates <- taggedStatus{tag: tag, status: endpointStatus}:
|
|
||||||
case <-ctx.Done():
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}(endpoint.tag, endpoint.provider)
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
waitGroup.Wait()
|
|
||||||
close(updates)
|
|
||||||
}()
|
|
||||||
|
|
||||||
var tags []string
|
|
||||||
statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints))
|
|
||||||
for update := range updates {
|
|
||||||
if _, exists := statuses[update.tag]; !exists {
|
|
||||||
tags = append(tags, update.tag)
|
|
||||||
}
|
|
||||||
statuses[update.tag] = update.status
|
|
||||||
protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses))
|
|
||||||
for _, tag := range tags {
|
|
||||||
protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag]))
|
|
||||||
}
|
|
||||||
sendErr := server.Send(&TailscaleStatusUpdate{
|
|
||||||
Endpoints: protoEndpoints,
|
|
||||||
})
|
|
||||||
if sendErr != nil {
|
|
||||||
return sendErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus {
|
|
||||||
userGroups := make([]*TailscaleUserGroup, len(s.UserGroups))
|
|
||||||
for i, group := range s.UserGroups {
|
|
||||||
peers := make([]*TailscalePeer, len(group.Peers))
|
|
||||||
for j, peer := range group.Peers {
|
|
||||||
peers[j] = tailscalePeerToProto(peer)
|
|
||||||
}
|
|
||||||
userGroups[i] = &TailscaleUserGroup{
|
|
||||||
UserID: group.UserID,
|
|
||||||
LoginName: group.LoginName,
|
|
||||||
DisplayName: group.DisplayName,
|
|
||||||
ProfilePicURL: group.ProfilePicURL,
|
|
||||||
Peers: peers,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result := &TailscaleEndpointStatus{
|
|
||||||
EndpointTag: tag,
|
|
||||||
BackendState: s.BackendState,
|
|
||||||
AuthURL: s.AuthURL,
|
|
||||||
NetworkName: s.NetworkName,
|
|
||||||
MagicDNSSuffix: s.MagicDNSSuffix,
|
|
||||||
UserGroups: userGroups,
|
|
||||||
}
|
|
||||||
if s.Self != nil {
|
|
||||||
result.Self = tailscalePeerToProto(s.Self)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer {
|
|
||||||
return &TailscalePeer{
|
|
||||||
HostName: peer.HostName,
|
|
||||||
DnsName: peer.DNSName,
|
|
||||||
Os: peer.OS,
|
|
||||||
TailscaleIPs: peer.TailscaleIPs,
|
|
||||||
Online: peer.Online,
|
|
||||||
ExitNode: peer.ExitNode,
|
|
||||||
ExitNodeOption: peer.ExitNodeOption,
|
|
||||||
Active: peer.Active,
|
|
||||||
RxBytes: peer.RxBytes,
|
|
||||||
TxBytes: peer.TxBytes,
|
|
||||||
KeyExpiry: peer.KeyExpiry,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StartedService) StartTailscalePing(
|
|
||||||
request *TailscalePingRequest,
|
|
||||||
server grpc.ServerStreamingServer[TailscalePingResponse],
|
|
||||||
) error {
|
|
||||||
err := s.waitForStarted(server.Context())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
s.serviceAccess.RLock()
|
|
||||||
boxService := s.instance
|
|
||||||
s.serviceAccess.RUnlock()
|
|
||||||
|
|
||||||
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
|
|
||||||
if endpointManager == nil {
|
|
||||||
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
var provider adapter.TailscaleEndpoint
|
|
||||||
if request.EndpointTag != "" {
|
|
||||||
endpoint, loaded := endpointManager.Get(request.EndpointTag)
|
|
||||||
if !loaded {
|
|
||||||
return status.Error(codes.NotFound, "endpoint not found: "+request.EndpointTag)
|
|
||||||
}
|
|
||||||
if endpoint.Type() != C.TypeTailscale {
|
|
||||||
return status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+request.EndpointTag)
|
|
||||||
}
|
|
||||||
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
|
||||||
if !loaded {
|
|
||||||
return status.Error(codes.FailedPrecondition, "endpoint does not support ping")
|
|
||||||
}
|
|
||||||
provider = pingProvider
|
|
||||||
} else {
|
|
||||||
for _, endpoint := range endpointManager.Endpoints() {
|
|
||||||
if endpoint.Type() != C.TypeTailscale {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
|
||||||
if loaded {
|
|
||||||
provider = pingProvider
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if provider == nil {
|
|
||||||
return status.Error(codes.NotFound, "no Tailscale endpoint found")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) {
|
|
||||||
_ = server.Send(&TailscalePingResponse{
|
|
||||||
LatencyMs: result.LatencyMs,
|
|
||||||
IsDirect: result.IsDirect,
|
|
||||||
Endpoint: result.Endpoint,
|
|
||||||
DerpRegionID: result.DERPRegionID,
|
|
||||||
DerpRegionCode: result.DERPRegionCode,
|
|
||||||
Error: result.Error,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
|
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,20 +26,12 @@ service StartedService {
|
|||||||
|
|
||||||
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
|
rpc GetSystemProxyStatus(google.protobuf.Empty) returns(SystemProxyStatus) {}
|
||||||
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
|
rpc SetSystemProxyEnabled(SetSystemProxyEnabledRequest) returns(google.protobuf.Empty) {}
|
||||||
rpc TriggerDebugCrash(DebugCrashRequest) returns(google.protobuf.Empty) {}
|
|
||||||
rpc TriggerOOMReport(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
|
||||||
|
|
||||||
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
|
rpc SubscribeConnections(SubscribeConnectionsRequest) returns(stream ConnectionEvents) {}
|
||||||
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
|
rpc CloseConnection(CloseConnectionRequest) returns(google.protobuf.Empty) {}
|
||||||
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
rpc CloseAllConnections(google.protobuf.Empty) returns(google.protobuf.Empty) {}
|
||||||
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
|
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
|
||||||
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
|
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
|
||||||
|
|
||||||
rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {}
|
|
||||||
rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {}
|
|
||||||
rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {}
|
|
||||||
rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {}
|
|
||||||
rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message ServiceStatus {
|
message ServiceStatus {
|
||||||
@@ -149,15 +141,6 @@ message SetSystemProxyEnabledRequest {
|
|||||||
bool enabled = 1;
|
bool enabled = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message DebugCrashRequest {
|
|
||||||
enum Type {
|
|
||||||
GO = 0;
|
|
||||||
NATIVE = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
Type type = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message SubscribeConnectionsRequest {
|
message SubscribeConnectionsRequest {
|
||||||
int64 interval = 1;
|
int64 interval = 1;
|
||||||
}
|
}
|
||||||
@@ -227,105 +210,8 @@ message DeprecatedWarning {
|
|||||||
string message = 1;
|
string message = 1;
|
||||||
bool impending = 2;
|
bool impending = 2;
|
||||||
string migrationLink = 3;
|
string migrationLink = 3;
|
||||||
string description = 4;
|
|
||||||
string deprecatedVersion = 5;
|
|
||||||
string scheduledVersion = 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message StartedAt {
|
message StartedAt {
|
||||||
int64 startedAt = 1;
|
int64 startedAt = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message OutboundList {
|
|
||||||
repeated GroupItem outbounds = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message NetworkQualityTestRequest {
|
|
||||||
string configURL = 1;
|
|
||||||
string outboundTag = 2;
|
|
||||||
bool serial = 3;
|
|
||||||
int32 maxRuntimeSeconds = 4;
|
|
||||||
bool http3 = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message NetworkQualityTestProgress {
|
|
||||||
int32 phase = 1;
|
|
||||||
int64 downloadCapacity = 2;
|
|
||||||
int64 uploadCapacity = 3;
|
|
||||||
int32 downloadRPM = 4;
|
|
||||||
int32 uploadRPM = 5;
|
|
||||||
int32 idleLatencyMs = 6;
|
|
||||||
int64 elapsedMs = 7;
|
|
||||||
bool isFinal = 8;
|
|
||||||
string error = 9;
|
|
||||||
int32 downloadCapacityAccuracy = 10;
|
|
||||||
int32 uploadCapacityAccuracy = 11;
|
|
||||||
int32 downloadRPMAccuracy = 12;
|
|
||||||
int32 uploadRPMAccuracy = 13;
|
|
||||||
}
|
|
||||||
|
|
||||||
message STUNTestRequest {
|
|
||||||
string server = 1;
|
|
||||||
string outboundTag = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message STUNTestProgress {
|
|
||||||
int32 phase = 1;
|
|
||||||
string externalAddr = 2;
|
|
||||||
int32 latencyMs = 3;
|
|
||||||
int32 natMapping = 4;
|
|
||||||
int32 natFiltering = 5;
|
|
||||||
bool isFinal = 6;
|
|
||||||
string error = 7;
|
|
||||||
bool natTypeSupported = 8;
|
|
||||||
}
|
|
||||||
|
|
||||||
message TailscaleStatusUpdate {
|
|
||||||
repeated TailscaleEndpointStatus endpoints = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
message TailscaleEndpointStatus {
|
|
||||||
string endpointTag = 1;
|
|
||||||
string backendState = 2;
|
|
||||||
string authURL = 3;
|
|
||||||
string networkName = 4;
|
|
||||||
string magicDNSSuffix = 5;
|
|
||||||
TailscalePeer self = 6;
|
|
||||||
repeated TailscaleUserGroup userGroups = 7;
|
|
||||||
}
|
|
||||||
|
|
||||||
message TailscaleUserGroup {
|
|
||||||
int64 userID = 1;
|
|
||||||
string loginName = 2;
|
|
||||||
string displayName = 3;
|
|
||||||
string profilePicURL = 4;
|
|
||||||
repeated TailscalePeer peers = 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
message TailscalePeer {
|
|
||||||
string hostName = 1;
|
|
||||||
string dnsName = 2;
|
|
||||||
string os = 3;
|
|
||||||
repeated string tailscaleIPs = 4;
|
|
||||||
bool online = 5;
|
|
||||||
bool exitNode = 6;
|
|
||||||
bool exitNodeOption = 7;
|
|
||||||
bool active = 8;
|
|
||||||
int64 rxBytes = 9;
|
|
||||||
int64 txBytes = 10;
|
|
||||||
int64 keyExpiry = 11;
|
|
||||||
}
|
|
||||||
|
|
||||||
message TailscalePingRequest {
|
|
||||||
string endpointTag = 1;
|
|
||||||
string peerIP = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
message TailscalePingResponse {
|
|
||||||
double latencyMs = 1;
|
|
||||||
bool isDirect = 2;
|
|
||||||
string endpoint = 3;
|
|
||||||
int32 derpRegionID = 4;
|
|
||||||
string derpRegionCode = 5;
|
|
||||||
string error = 6;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,34 +15,27 @@ import (
|
|||||||
const _ = grpc.SupportPackageIsVersion9
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
|
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
|
||||||
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
|
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
|
||||||
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
||||||
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
||||||
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
||||||
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
||||||
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
||||||
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
||||||
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
||||||
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
||||||
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
||||||
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
||||||
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
||||||
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
||||||
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
|
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
|
||||||
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
|
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
|
||||||
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
|
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
||||||
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
|
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
||||||
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
|
||||||
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
|
||||||
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
|
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
|
||||||
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
|
|
||||||
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
|
|
||||||
StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds"
|
|
||||||
StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest"
|
|
||||||
StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest"
|
|
||||||
StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus"
|
|
||||||
StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// StartedServiceClient is the client API for StartedService service.
|
// StartedServiceClient is the client API for StartedService service.
|
||||||
@@ -65,18 +58,11 @@ type StartedServiceClient interface {
|
|||||||
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
SetGroupExpand(ctx context.Context, in *SetGroupExpandRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
|
GetSystemProxyStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*SystemProxyStatus, error)
|
||||||
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
SetSystemProxyEnabled(ctx context.Context, in *SetSystemProxyEnabledRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
|
||||||
TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
|
||||||
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
|
SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error)
|
||||||
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
CloseConnection(ctx context.Context, in *CloseConnectionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||||
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
|
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
|
||||||
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
|
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
|
||||||
SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error)
|
|
||||||
StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error)
|
|
||||||
StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error)
|
|
||||||
SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error)
|
|
||||||
StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type startedServiceClient struct {
|
type startedServiceClient struct {
|
||||||
@@ -292,26 +278,6 @@ func (c *startedServiceClient) SetSystemProxyEnabled(ctx context.Context, in *Se
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *startedServiceClient) TriggerDebugCrash(ctx context.Context, in *DebugCrashRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(emptypb.Empty)
|
|
||||||
err := c.cc.Invoke(ctx, StartedService_TriggerDebugCrash_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *startedServiceClient) TriggerOOMReport(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
out := new(emptypb.Empty)
|
|
||||||
err := c.cc.Invoke(ctx, StartedService_TriggerOOMReport_FullMethodName, in, out, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
|
func (c *startedServiceClient) SubscribeConnections(ctx context.Context, in *SubscribeConnectionsRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[ConnectionEvents], error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
|
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[5], StartedService_SubscribeConnections_FullMethodName, cOpts...)
|
||||||
@@ -371,101 +337,6 @@ func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Emp
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *startedServiceClient) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeOutbounds_FullMethodName, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
x := &grpc.GenericClientStream[emptypb.Empty, OutboundList]{ClientStream: stream}
|
|
||||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := x.ClientStream.CloseSend(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return x, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_SubscribeOutboundsClient = grpc.ServerStreamingClient[OutboundList]
|
|
||||||
|
|
||||||
func (c *startedServiceClient) StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[7], StartedService_StartNetworkQualityTest_FullMethodName, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
x := &grpc.GenericClientStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ClientStream: stream}
|
|
||||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := x.ClientStream.CloseSend(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return x, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_StartNetworkQualityTestClient = grpc.ServerStreamingClient[NetworkQualityTestProgress]
|
|
||||||
|
|
||||||
func (c *startedServiceClient) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[8], StartedService_StartSTUNTest_FullMethodName, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
x := &grpc.GenericClientStream[STUNTestRequest, STUNTestProgress]{ClientStream: stream}
|
|
||||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := x.ClientStream.CloseSend(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return x, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_StartSTUNTestClient = grpc.ServerStreamingClient[STUNTestProgress]
|
|
||||||
|
|
||||||
func (c *startedServiceClient) SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[9], StartedService_SubscribeTailscaleStatus_FullMethodName, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
x := &grpc.GenericClientStream[emptypb.Empty, TailscaleStatusUpdate]{ClientStream: stream}
|
|
||||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := x.ClientStream.CloseSend(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return x, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_SubscribeTailscaleStatusClient = grpc.ServerStreamingClient[TailscaleStatusUpdate]
|
|
||||||
|
|
||||||
func (c *startedServiceClient) StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) {
|
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
|
||||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[10], StartedService_StartTailscalePing_FullMethodName, cOpts...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
x := &grpc.GenericClientStream[TailscalePingRequest, TailscalePingResponse]{ClientStream: stream}
|
|
||||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := x.ClientStream.CloseSend(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return x, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_StartTailscalePingClient = grpc.ServerStreamingClient[TailscalePingResponse]
|
|
||||||
|
|
||||||
// StartedServiceServer is the server API for StartedService service.
|
// StartedServiceServer is the server API for StartedService service.
|
||||||
// All implementations must embed UnimplementedStartedServiceServer
|
// All implementations must embed UnimplementedStartedServiceServer
|
||||||
// for forward compatibility.
|
// for forward compatibility.
|
||||||
@@ -486,18 +357,11 @@ type StartedServiceServer interface {
|
|||||||
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
|
SetGroupExpand(context.Context, *SetGroupExpandRequest) (*emptypb.Empty, error)
|
||||||
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
|
GetSystemProxyStatus(context.Context, *emptypb.Empty) (*SystemProxyStatus, error)
|
||||||
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
|
SetSystemProxyEnabled(context.Context, *SetSystemProxyEnabledRequest) (*emptypb.Empty, error)
|
||||||
TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error)
|
|
||||||
TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
|
||||||
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
|
SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error
|
||||||
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
|
CloseConnection(context.Context, *CloseConnectionRequest) (*emptypb.Empty, error)
|
||||||
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||||
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
|
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
|
||||||
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
|
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
|
||||||
SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error
|
|
||||||
StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error
|
|
||||||
StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error
|
|
||||||
SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error
|
|
||||||
StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error
|
|
||||||
mustEmbedUnimplementedStartedServiceServer()
|
mustEmbedUnimplementedStartedServiceServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,14 +436,6 @@ func (UnimplementedStartedServiceServer) SetSystemProxyEnabled(context.Context,
|
|||||||
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
|
return nil, status.Error(codes.Unimplemented, "method SetSystemProxyEnabled not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (UnimplementedStartedServiceServer) TriggerDebugCrash(context.Context, *DebugCrashRequest) (*emptypb.Empty, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method TriggerDebugCrash not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (UnimplementedStartedServiceServer) TriggerOOMReport(context.Context, *emptypb.Empty) (*emptypb.Empty, error) {
|
|
||||||
return nil, status.Error(codes.Unimplemented, "method TriggerOOMReport not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
|
func (UnimplementedStartedServiceServer) SubscribeConnections(*SubscribeConnectionsRequest, grpc.ServerStreamingServer[ConnectionEvents]) error {
|
||||||
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
|
return status.Error(codes.Unimplemented, "method SubscribeConnections not implemented")
|
||||||
}
|
}
|
||||||
@@ -599,26 +455,6 @@ func (UnimplementedStartedServiceServer) GetDeprecatedWarnings(context.Context,
|
|||||||
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
|
func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error) {
|
||||||
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
|
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error {
|
|
||||||
return status.Error(codes.Unimplemented, "method SubscribeOutbounds not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error {
|
|
||||||
return status.Error(codes.Unimplemented, "method StartNetworkQualityTest not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error {
|
|
||||||
return status.Error(codes.Unimplemented, "method StartSTUNTest not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error {
|
|
||||||
return status.Error(codes.Unimplemented, "method SubscribeTailscaleStatus not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (UnimplementedStartedServiceServer) StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error {
|
|
||||||
return status.Error(codes.Unimplemented, "method StartTailscalePing not implemented")
|
|
||||||
}
|
|
||||||
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
|
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
|
||||||
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
|
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
@@ -893,42 +729,6 @@ func _StartedService_SetSystemProxyEnabled_Handler(srv interface{}, ctx context.
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _StartedService_TriggerDebugCrash_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(DebugCrashRequest)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: StartedService_TriggerDebugCrash_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(StartedServiceServer).TriggerDebugCrash(ctx, req.(*DebugCrashRequest))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _StartedService_TriggerOOMReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
|
||||||
in := new(emptypb.Empty)
|
|
||||||
if err := dec(in); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if interceptor == nil {
|
|
||||||
return srv.(StartedServiceServer).TriggerOOMReport(ctx, in)
|
|
||||||
}
|
|
||||||
info := &grpc.UnaryServerInfo{
|
|
||||||
Server: srv,
|
|
||||||
FullMethod: StartedService_TriggerOOMReport_FullMethodName,
|
|
||||||
}
|
|
||||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
|
||||||
return srv.(StartedServiceServer).TriggerOOMReport(ctx, req.(*emptypb.Empty))
|
|
||||||
}
|
|
||||||
return interceptor(ctx, in, info, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
|
func _StartedService_SubscribeConnections_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
m := new(SubscribeConnectionsRequest)
|
m := new(SubscribeConnectionsRequest)
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
if err := stream.RecvMsg(m); err != nil {
|
||||||
@@ -1012,61 +812,6 @@ func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context,
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
func _StartedService_SubscribeOutbounds_Handler(srv interface{}, stream grpc.ServerStream) error {
|
|
||||||
m := new(emptypb.Empty)
|
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return srv.(StartedServiceServer).SubscribeOutbounds(m, &grpc.GenericServerStream[emptypb.Empty, OutboundList]{ServerStream: stream})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_SubscribeOutboundsServer = grpc.ServerStreamingServer[OutboundList]
|
|
||||||
|
|
||||||
func _StartedService_StartNetworkQualityTest_Handler(srv interface{}, stream grpc.ServerStream) error {
|
|
||||||
m := new(NetworkQualityTestRequest)
|
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return srv.(StartedServiceServer).StartNetworkQualityTest(m, &grpc.GenericServerStream[NetworkQualityTestRequest, NetworkQualityTestProgress]{ServerStream: stream})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_StartNetworkQualityTestServer = grpc.ServerStreamingServer[NetworkQualityTestProgress]
|
|
||||||
|
|
||||||
func _StartedService_StartSTUNTest_Handler(srv interface{}, stream grpc.ServerStream) error {
|
|
||||||
m := new(STUNTestRequest)
|
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return srv.(StartedServiceServer).StartSTUNTest(m, &grpc.GenericServerStream[STUNTestRequest, STUNTestProgress]{ServerStream: stream})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_StartSTUNTestServer = grpc.ServerStreamingServer[STUNTestProgress]
|
|
||||||
|
|
||||||
func _StartedService_SubscribeTailscaleStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
|
|
||||||
m := new(emptypb.Empty)
|
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return srv.(StartedServiceServer).SubscribeTailscaleStatus(m, &grpc.GenericServerStream[emptypb.Empty, TailscaleStatusUpdate]{ServerStream: stream})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_SubscribeTailscaleStatusServer = grpc.ServerStreamingServer[TailscaleStatusUpdate]
|
|
||||||
|
|
||||||
func _StartedService_StartTailscalePing_Handler(srv interface{}, stream grpc.ServerStream) error {
|
|
||||||
m := new(TailscalePingRequest)
|
|
||||||
if err := stream.RecvMsg(m); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return srv.(StartedServiceServer).StartTailscalePing(m, &grpc.GenericServerStream[TailscalePingRequest, TailscalePingResponse]{ServerStream: stream})
|
|
||||||
}
|
|
||||||
|
|
||||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
|
||||||
type StartedService_StartTailscalePingServer = grpc.ServerStreamingServer[TailscalePingResponse]
|
|
||||||
|
|
||||||
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
|
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
|
||||||
// It's only intended for direct use with grpc.RegisterService,
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
// and not to be introspected or modified (even as a copy)
|
// and not to be introspected or modified (even as a copy)
|
||||||
@@ -1118,14 +863,6 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "SetSystemProxyEnabled",
|
MethodName: "SetSystemProxyEnabled",
|
||||||
Handler: _StartedService_SetSystemProxyEnabled_Handler,
|
Handler: _StartedService_SetSystemProxyEnabled_Handler,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
MethodName: "TriggerDebugCrash",
|
|
||||||
Handler: _StartedService_TriggerDebugCrash_Handler,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
MethodName: "TriggerOOMReport",
|
|
||||||
Handler: _StartedService_TriggerOOMReport_Handler,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
MethodName: "CloseConnection",
|
MethodName: "CloseConnection",
|
||||||
Handler: _StartedService_CloseConnection_Handler,
|
Handler: _StartedService_CloseConnection_Handler,
|
||||||
@@ -1174,31 +911,6 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
Handler: _StartedService_SubscribeConnections_Handler,
|
Handler: _StartedService_SubscribeConnections_Handler,
|
||||||
ServerStreams: true,
|
ServerStreams: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
StreamName: "SubscribeOutbounds",
|
|
||||||
Handler: _StartedService_SubscribeOutbounds_Handler,
|
|
||||||
ServerStreams: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
StreamName: "StartNetworkQualityTest",
|
|
||||||
Handler: _StartedService_StartNetworkQualityTest_Handler,
|
|
||||||
ServerStreams: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
StreamName: "StartSTUNTest",
|
|
||||||
Handler: _StartedService_StartSTUNTest_Handler,
|
|
||||||
ServerStreams: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
StreamName: "SubscribeTailscaleStatus",
|
|
||||||
Handler: _StartedService_SubscribeTailscaleStatus_Handler,
|
|
||||||
ServerStreams: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
StreamName: "StartTailscalePing",
|
|
||||||
Handler: _StartedService_StartTailscalePing_Handler,
|
|
||||||
ServerStreams: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Metadata: "daemon/started_service.proto",
|
Metadata: "daemon/started_service.proto",
|
||||||
}
|
}
|
||||||
|
|||||||
580
dns/client.go
580
dns/client.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
"github.com/sagernet/sing/common/task"
|
"github.com/sagernet/sing/common/task"
|
||||||
"github.com/sagernet/sing/contrab/freelru"
|
"github.com/sagernet/sing/contrab/freelru"
|
||||||
"github.com/sagernet/sing/contrab/maphash"
|
"github.com/sagernet/sing/contrab/maphash"
|
||||||
@@ -30,63 +32,59 @@ var (
|
|||||||
var _ adapter.DNSClient = (*Client)(nil)
|
var _ adapter.DNSClient = (*Client)(nil)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ctx context.Context
|
timeout time.Duration
|
||||||
timeout time.Duration
|
disableCache bool
|
||||||
disableCache bool
|
disableExpire bool
|
||||||
disableExpire bool
|
independentCache bool
|
||||||
optimisticTimeout time.Duration
|
clientSubnet netip.Prefix
|
||||||
cacheCapacity uint32
|
rdrc adapter.RDRCStore
|
||||||
clientSubnet netip.Prefix
|
initRDRCFunc func() adapter.RDRCStore
|
||||||
rdrc adapter.RDRCStore
|
logger logger.ContextLogger
|
||||||
initRDRCFunc func() adapter.RDRCStore
|
cache freelru.Cache[dns.Question, *dns.Msg]
|
||||||
dnsCache adapter.DNSCacheStore
|
cacheLock compatible.Map[dns.Question, chan struct{}]
|
||||||
initDNSCacheFunc func() adapter.DNSCacheStore
|
transportCache freelru.Cache[transportCacheKey, *dns.Msg]
|
||||||
logger logger.ContextLogger
|
transportCacheLock compatible.Map[dns.Question, chan struct{}]
|
||||||
cache freelru.Cache[dnsCacheKey, *dns.Msg]
|
|
||||||
cacheLock compatible.Map[dnsCacheKey, chan struct{}]
|
|
||||||
backgroundRefresh compatible.Map[dnsCacheKey, struct{}]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientOptions struct {
|
type ClientOptions struct {
|
||||||
Context context.Context
|
Timeout time.Duration
|
||||||
Timeout time.Duration
|
DisableCache bool
|
||||||
DisableCache bool
|
DisableExpire bool
|
||||||
DisableExpire bool
|
IndependentCache bool
|
||||||
OptimisticTimeout time.Duration
|
CacheCapacity uint32
|
||||||
CacheCapacity uint32
|
ClientSubnet netip.Prefix
|
||||||
ClientSubnet netip.Prefix
|
RDRC func() adapter.RDRCStore
|
||||||
RDRC func() adapter.RDRCStore
|
Logger logger.ContextLogger
|
||||||
DNSCache func() adapter.DNSCacheStore
|
|
||||||
Logger logger.ContextLogger
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(options ClientOptions) *Client {
|
func NewClient(options ClientOptions) *Client {
|
||||||
cacheCapacity := options.CacheCapacity
|
|
||||||
if cacheCapacity < 1024 {
|
|
||||||
cacheCapacity = 1024
|
|
||||||
}
|
|
||||||
client := &Client{
|
client := &Client{
|
||||||
ctx: options.Context,
|
timeout: options.Timeout,
|
||||||
timeout: options.Timeout,
|
disableCache: options.DisableCache,
|
||||||
disableCache: options.DisableCache,
|
disableExpire: options.DisableExpire,
|
||||||
disableExpire: options.DisableExpire,
|
independentCache: options.IndependentCache,
|
||||||
optimisticTimeout: options.OptimisticTimeout,
|
clientSubnet: options.ClientSubnet,
|
||||||
cacheCapacity: cacheCapacity,
|
initRDRCFunc: options.RDRC,
|
||||||
clientSubnet: options.ClientSubnet,
|
logger: options.Logger,
|
||||||
initRDRCFunc: options.RDRC,
|
|
||||||
initDNSCacheFunc: options.DNSCache,
|
|
||||||
logger: options.Logger,
|
|
||||||
}
|
}
|
||||||
if client.timeout == 0 {
|
if client.timeout == 0 {
|
||||||
client.timeout = C.DNSTimeout
|
client.timeout = C.DNSTimeout
|
||||||
}
|
}
|
||||||
if !client.disableCache && client.initDNSCacheFunc == nil {
|
cacheCapacity := options.CacheCapacity
|
||||||
client.initializeMemoryCache()
|
if cacheCapacity < 1024 {
|
||||||
|
cacheCapacity = 1024
|
||||||
|
}
|
||||||
|
if !client.disableCache {
|
||||||
|
if !client.independentCache {
|
||||||
|
client.cache = common.Must1(freelru.NewSharded[dns.Question, *dns.Msg](cacheCapacity, maphash.NewHasher[dns.Question]().Hash32))
|
||||||
|
} else {
|
||||||
|
client.transportCache = common.Must1(freelru.NewSharded[transportCacheKey, *dns.Msg](cacheCapacity, maphash.NewHasher[transportCacheKey]().Hash32))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return client
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
type dnsCacheKey struct {
|
type transportCacheKey struct {
|
||||||
dns.Question
|
dns.Question
|
||||||
transportTag string
|
transportTag string
|
||||||
}
|
}
|
||||||
@@ -95,19 +93,6 @@ func (c *Client) Start() {
|
|||||||
if c.initRDRCFunc != nil {
|
if c.initRDRCFunc != nil {
|
||||||
c.rdrc = c.initRDRCFunc()
|
c.rdrc = c.initRDRCFunc()
|
||||||
}
|
}
|
||||||
if c.initDNSCacheFunc != nil {
|
|
||||||
c.dnsCache = c.initDNSCacheFunc()
|
|
||||||
}
|
|
||||||
if c.dnsCache == nil {
|
|
||||||
c.initializeMemoryCache()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) initializeMemoryCache() {
|
|
||||||
if c.disableCache || c.cache != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.cache = common.Must1(freelru.NewSharded[dnsCacheKey, *dns.Msg](c.cacheCapacity, maphash.NewHasher[dnsCacheKey]().Hash32))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
||||||
@@ -124,38 +109,7 @@ func extractNegativeTTL(response *dns.Msg) (uint32, bool) {
|
|||||||
return 0, false
|
return 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeTimeToLive(response *dns.Msg) uint32 {
|
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) (*dns.Msg, error) {
|
||||||
var timeToLive uint32
|
|
||||||
if len(response.Answer) == 0 {
|
|
||||||
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
|
||||||
return soaTTL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
if record.Header().Rrtype == dns.TypeOPT {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
|
||||||
timeToLive = record.Header().Ttl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return timeToLive
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeTTL(response *dns.Msg, timeToLive uint32) {
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
if record.Header().Rrtype == dns.TypeOPT {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
record.Header().Ttl = timeToLive
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) (*dns.Msg, error) {
|
|
||||||
if len(message.Question) == 0 {
|
if len(message.Question) == 0 {
|
||||||
if c.logger != nil {
|
if c.logger != nil {
|
||||||
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
c.logger.WarnContext(ctx, "bad question size: ", len(message.Question))
|
||||||
@@ -169,7 +123,13 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
}
|
}
|
||||||
return FixedResponseStatus(message, dns.RcodeSuccess), nil
|
return FixedResponseStatus(message, dns.RcodeSuccess), nil
|
||||||
}
|
}
|
||||||
message = c.prepareExchangeMessage(message, options)
|
clientSubnet := options.ClientSubnet
|
||||||
|
if !clientSubnet.IsValid() {
|
||||||
|
clientSubnet = c.clientSubnet
|
||||||
|
}
|
||||||
|
if clientSubnet.IsValid() {
|
||||||
|
message = SetClientSubnet(message, clientSubnet)
|
||||||
|
}
|
||||||
|
|
||||||
isSimpleRequest := len(message.Question) == 1 &&
|
isSimpleRequest := len(message.Question) == 1 &&
|
||||||
len(message.Ns) == 0 &&
|
len(message.Ns) == 0 &&
|
||||||
@@ -181,32 +141,40 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
!options.ClientSubnet.IsValid()
|
!options.ClientSubnet.IsValid()
|
||||||
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
disableCache := !isSimpleRequest || c.disableCache || options.DisableCache
|
||||||
if !disableCache {
|
if !disableCache {
|
||||||
cacheKey := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
if c.cache != nil {
|
||||||
cond, loaded := c.cacheLock.LoadOrStore(cacheKey, make(chan struct{}))
|
cond, loaded := c.cacheLock.LoadOrStore(question, make(chan struct{}))
|
||||||
if loaded {
|
if loaded {
|
||||||
select {
|
select {
|
||||||
case <-cond:
|
case <-cond:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defer func() {
|
||||||
|
c.cacheLock.Delete(question)
|
||||||
|
close(cond)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
} else if c.transportCache != nil {
|
||||||
|
cond, loaded := c.transportCacheLock.LoadOrStore(question, make(chan struct{}))
|
||||||
|
if loaded {
|
||||||
|
select {
|
||||||
|
case <-cond:
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defer func() {
|
||||||
|
c.transportCacheLock.Delete(question)
|
||||||
|
close(cond)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
defer func() {
|
|
||||||
c.cacheLock.Delete(cacheKey)
|
|
||||||
close(cond)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
response, ttl, isStale := c.loadResponse(question, transport)
|
response, ttl := c.loadResponse(question, transport)
|
||||||
if response != nil {
|
if response != nil {
|
||||||
if isStale && !options.DisableOptimisticCache {
|
logCachedResponse(c.logger, ctx, response, ttl)
|
||||||
c.backgroundRefreshDNS(transport, question, message.Copy(), options, responseChecker)
|
response.Id = message.Id
|
||||||
logOptimisticResponse(c.logger, ctx, response)
|
return response, nil
|
||||||
response.Id = message.Id
|
|
||||||
return response, nil
|
|
||||||
} else if !isStale {
|
|
||||||
logCachedResponse(c.logger, ctx, response, ttl)
|
|
||||||
response.Id = message.Id
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,17 +190,62 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
return nil, ErrResponseRejectedCached
|
return nil, ErrResponseRejectedCached
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
response, err := c.exchangeToTransport(ctx, transport, message)
|
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||||
|
response, err := transport.Exchange(ctx, message)
|
||||||
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
var rcodeError RcodeError
|
||||||
|
if errors.As(err, &rcodeError) {
|
||||||
|
response = FixedResponseStatus(message, int(rcodeError))
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
/*if question.Qtype == dns.TypeA || question.Qtype == dns.TypeAAAA {
|
||||||
|
validResponse := response
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
var (
|
||||||
|
addresses int
|
||||||
|
queryCNAME string
|
||||||
|
)
|
||||||
|
for _, rawRR := range validResponse.Answer {
|
||||||
|
switch rr := rawRR.(type) {
|
||||||
|
case *dns.A:
|
||||||
|
break loop
|
||||||
|
case *dns.AAAA:
|
||||||
|
break loop
|
||||||
|
case *dns.CNAME:
|
||||||
|
queryCNAME = rr.Target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if queryCNAME == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
exMessage := *message
|
||||||
|
exMessage.Question = []dns.Question{{
|
||||||
|
Name: queryCNAME,
|
||||||
|
Qtype: question.Qtype,
|
||||||
|
}}
|
||||||
|
validResponse, err = c.Exchange(ctx, transport, &exMessage, options, responseChecker)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if validResponse != response {
|
||||||
|
response.Answer = append(response.Answer, validResponse.Answer...)
|
||||||
|
}
|
||||||
|
}*/
|
||||||
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
|
disableCache = disableCache || (response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError)
|
||||||
if responseChecker != nil {
|
if responseChecker != nil {
|
||||||
var rejected bool
|
var rejected bool
|
||||||
|
// TODO: add accept_any rule and support to check response instead of addresses
|
||||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
||||||
rejected = true
|
rejected = true
|
||||||
|
} else if len(response.Answer) == 0 {
|
||||||
|
rejected = !responseChecker(nil)
|
||||||
} else {
|
} else {
|
||||||
rejected = !responseChecker(response)
|
rejected = !responseChecker(MessageToAddresses(response))
|
||||||
}
|
}
|
||||||
if rejected {
|
if rejected {
|
||||||
if !disableCache && c.rdrc != nil {
|
if !disableCache && c.rdrc != nil {
|
||||||
@@ -242,7 +255,48 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
return response, ErrResponseRejected
|
return response, ErrResponseRejected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timeToLive := applyResponseOptions(question, response, options)
|
if question.Qtype == dns.TypeHTTPS {
|
||||||
|
if options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only {
|
||||||
|
for _, rr := range response.Answer {
|
||||||
|
https, isHTTPS := rr.(*dns.HTTPS)
|
||||||
|
if !isHTTPS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content := https.SVCB
|
||||||
|
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
||||||
|
if options.Strategy == C.DomainStrategyIPv4Only {
|
||||||
|
return it.Key() != dns.SVCB_IPV6HINT
|
||||||
|
} else {
|
||||||
|
return it.Key() != dns.SVCB_IPV4HINT
|
||||||
|
}
|
||||||
|
})
|
||||||
|
https.SVCB = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var timeToLive uint32
|
||||||
|
if len(response.Answer) == 0 {
|
||||||
|
if soaTTL, hasSOA := extractNegativeTTL(response); hasSOA {
|
||||||
|
timeToLive = soaTTL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if timeToLive == 0 {
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||||
|
timeToLive = record.Header().Ttl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.RewriteTTL != nil {
|
||||||
|
timeToLive = *options.RewriteTTL
|
||||||
|
}
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
record.Header().Ttl = timeToLive
|
||||||
|
}
|
||||||
|
}
|
||||||
if !disableCache {
|
if !disableCache {
|
||||||
c.storeCache(transport, question, response, timeToLive)
|
c.storeCache(transport, question, response, timeToLive)
|
||||||
}
|
}
|
||||||
@@ -261,7 +315,7 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
|||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, domain string, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||||
domain = FqdnToDomain(domain)
|
domain = FqdnToDomain(domain)
|
||||||
dnsName := dns.Fqdn(domain)
|
dnsName := dns.Fqdn(domain)
|
||||||
var strategy C.DomainStrategy
|
var strategy C.DomainStrategy
|
||||||
@@ -308,12 +362,8 @@ func (c *Client) Lookup(ctx context.Context, transport adapter.DNSTransport, dom
|
|||||||
func (c *Client) ClearCache() {
|
func (c *Client) ClearCache() {
|
||||||
if c.cache != nil {
|
if c.cache != nil {
|
||||||
c.cache.Purge()
|
c.cache.Purge()
|
||||||
}
|
} else if c.transportCache != nil {
|
||||||
if c.dnsCache != nil {
|
c.transportCache.Purge()
|
||||||
err := c.dnsCache.ClearDNSCache()
|
|
||||||
if err != nil && c.logger != nil {
|
|
||||||
c.logger.Warn("clear DNS cache: ", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,44 +379,46 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
|||||||
if timeToLive == 0 {
|
if timeToLive == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.dnsCache != nil {
|
|
||||||
packed, err := message.Pack()
|
|
||||||
if err == nil {
|
|
||||||
expireAt := time.Now().Add(time.Second * time.Duration(timeToLive))
|
|
||||||
c.dnsCache.SaveDNSCacheAsync(transport.Tag(), question.Name, question.Qtype, packed, expireAt, c.logger)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if c.cache == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
|
||||||
if c.disableExpire {
|
if c.disableExpire {
|
||||||
c.cache.Add(key, message.Copy())
|
if !c.independentCache {
|
||||||
|
c.cache.Add(question, message)
|
||||||
|
} else {
|
||||||
|
c.transportCache.Add(transportCacheKey{
|
||||||
|
Question: question,
|
||||||
|
transportTag: transport.Tag(),
|
||||||
|
}, message)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
c.cache.AddWithLifetime(key, message.Copy(), time.Second*time.Duration(timeToLive))
|
if !c.independentCache {
|
||||||
|
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
||||||
|
} else {
|
||||||
|
c.transportCache.AddWithLifetime(transportCacheKey{
|
||||||
|
Question: question,
|
||||||
|
transportTag: transport.Tag(),
|
||||||
|
}, message, time.Second*time.Duration(timeToLive))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTransport, name string, qType uint16, options adapter.DNSQueryOptions, responseChecker func(responseAddrs []netip.Addr) bool) ([]netip.Addr, error) {
|
||||||
question := dns.Question{
|
question := dns.Question{
|
||||||
Name: name,
|
Name: name,
|
||||||
Qtype: qType,
|
Qtype: qType,
|
||||||
Qclass: dns.ClassINET,
|
Qclass: dns.ClassINET,
|
||||||
}
|
}
|
||||||
|
disableCache := c.disableCache || options.DisableCache
|
||||||
|
if !disableCache {
|
||||||
|
cachedAddresses, err := c.questionCache(question, transport)
|
||||||
|
if err != ErrNotCached {
|
||||||
|
return cachedAddresses, err
|
||||||
|
}
|
||||||
|
}
|
||||||
message := dns.Msg{
|
message := dns.Msg{
|
||||||
MsgHdr: dns.MsgHdr{
|
MsgHdr: dns.MsgHdr{
|
||||||
RecursionDesired: true,
|
RecursionDesired: true,
|
||||||
},
|
},
|
||||||
Question: []dns.Question{question},
|
Question: []dns.Question{question},
|
||||||
}
|
}
|
||||||
disableCache := c.disableCache || options.DisableCache
|
|
||||||
if !disableCache {
|
|
||||||
cachedAddresses, err := c.questionCache(ctx, transport, &message, options, responseChecker)
|
|
||||||
if err != ErrNotCached {
|
|
||||||
return cachedAddresses, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
|
response, err := c.Exchange(ctx, transport, &message, options, responseChecker)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -377,181 +429,111 @@ func (c *Client) lookupToExchange(ctx context.Context, transport adapter.DNSTran
|
|||||||
return MessageToAddresses(response), nil
|
return MessageToAddresses(response), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) questionCache(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) ([]netip.Addr, error) {
|
func (c *Client) questionCache(question dns.Question, transport adapter.DNSTransport) ([]netip.Addr, error) {
|
||||||
question := message.Question[0]
|
response, _ := c.loadResponse(question, transport)
|
||||||
response, _, isStale := c.loadResponse(question, transport)
|
|
||||||
if response == nil {
|
if response == nil {
|
||||||
return nil, ErrNotCached
|
return nil, ErrNotCached
|
||||||
}
|
}
|
||||||
if isStale {
|
|
||||||
if options.DisableOptimisticCache {
|
|
||||||
return nil, ErrNotCached
|
|
||||||
}
|
|
||||||
c.backgroundRefreshDNS(transport, question, c.prepareExchangeMessage(message.Copy(), options), options, responseChecker)
|
|
||||||
logOptimisticResponse(c.logger, ctx, response)
|
|
||||||
}
|
|
||||||
if response.Rcode != dns.RcodeSuccess {
|
if response.Rcode != dns.RcodeSuccess {
|
||||||
return nil, RcodeError(response.Rcode)
|
return nil, RcodeError(response.Rcode)
|
||||||
}
|
}
|
||||||
return MessageToAddresses(response), nil
|
return MessageToAddresses(response), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
|
func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int) {
|
||||||
if c.dnsCache != nil {
|
var (
|
||||||
return c.loadPersistentResponse(question, transport)
|
response *dns.Msg
|
||||||
}
|
loaded bool
|
||||||
if c.cache == nil {
|
)
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
||||||
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
|
||||||
if c.disableExpire {
|
if c.disableExpire {
|
||||||
response, loaded := c.cache.Get(key)
|
if !c.independentCache {
|
||||||
if !loaded {
|
response, loaded = c.cache.Get(question)
|
||||||
return nil, 0, false
|
} else {
|
||||||
}
|
response, loaded = c.transportCache.Get(transportCacheKey{
|
||||||
return response.Copy(), 0, false
|
Question: question,
|
||||||
}
|
transportTag: transport.Tag(),
|
||||||
response, expireAt, loaded := c.cache.GetWithLifetimeNoExpire(key)
|
|
||||||
if !loaded {
|
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
||||||
timeNow := time.Now()
|
|
||||||
if timeNow.After(expireAt) {
|
|
||||||
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
|
|
||||||
response = response.Copy()
|
|
||||||
normalizeTTL(response, 1)
|
|
||||||
return response, 0, true
|
|
||||||
}
|
|
||||||
c.cache.Remove(key)
|
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
||||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
|
||||||
if nowTTL < 0 {
|
|
||||||
nowTTL = 0
|
|
||||||
}
|
|
||||||
response = response.Copy()
|
|
||||||
normalizeTTL(response, uint32(nowTTL))
|
|
||||||
return response, nowTTL, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) loadPersistentResponse(question dns.Question, transport adapter.DNSTransport) (*dns.Msg, int, bool) {
|
|
||||||
rawMessage, expireAt, loaded := c.dnsCache.LoadDNSCache(transport.Tag(), question.Name, question.Qtype)
|
|
||||||
if !loaded {
|
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
||||||
response := new(dns.Msg)
|
|
||||||
err := response.Unpack(rawMessage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
||||||
if c.disableExpire {
|
|
||||||
return response, 0, false
|
|
||||||
}
|
|
||||||
timeNow := time.Now()
|
|
||||||
if timeNow.After(expireAt) {
|
|
||||||
if c.optimisticTimeout > 0 && timeNow.Before(expireAt.Add(c.optimisticTimeout)) {
|
|
||||||
normalizeTTL(response, 1)
|
|
||||||
return response, 0, true
|
|
||||||
}
|
|
||||||
return nil, 0, false
|
|
||||||
}
|
|
||||||
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
|
||||||
if nowTTL < 0 {
|
|
||||||
nowTTL = 0
|
|
||||||
}
|
|
||||||
normalizeTTL(response, uint32(nowTTL))
|
|
||||||
return response, nowTTL, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func applyResponseOptions(question dns.Question, response *dns.Msg, options adapter.DNSQueryOptions) uint32 {
|
|
||||||
if question.Qtype == dns.TypeHTTPS && (options.Strategy == C.DomainStrategyIPv4Only || options.Strategy == C.DomainStrategyIPv6Only) {
|
|
||||||
for _, rr := range response.Answer {
|
|
||||||
https, isHTTPS := rr.(*dns.HTTPS)
|
|
||||||
if !isHTTPS {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
content := https.SVCB
|
|
||||||
content.Value = common.Filter(content.Value, func(it dns.SVCBKeyValue) bool {
|
|
||||||
if options.Strategy == C.DomainStrategyIPv4Only {
|
|
||||||
return it.Key() != dns.SVCB_IPV6HINT
|
|
||||||
}
|
|
||||||
return it.Key() != dns.SVCB_IPV4HINT
|
|
||||||
})
|
})
|
||||||
https.SVCB = content
|
|
||||||
}
|
}
|
||||||
}
|
if !loaded {
|
||||||
timeToLive := computeTimeToLive(response)
|
return nil, 0
|
||||||
if options.RewriteTTL != nil {
|
|
||||||
timeToLive = *options.RewriteTTL
|
|
||||||
}
|
|
||||||
normalizeTTL(response, timeToLive)
|
|
||||||
return timeToLive
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) backgroundRefreshDNS(transport adapter.DNSTransport, question dns.Question, message *dns.Msg, options adapter.DNSQueryOptions, responseChecker func(response *dns.Msg) bool) {
|
|
||||||
key := dnsCacheKey{Question: question, transportTag: transport.Tag()}
|
|
||||||
_, loaded := c.backgroundRefresh.LoadOrStore(key, struct{}{})
|
|
||||||
if loaded {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
defer c.backgroundRefresh.Delete(key)
|
|
||||||
ctx := contextWithTransportTag(c.ctx, transport.Tag())
|
|
||||||
response, err := c.exchangeToTransport(ctx, transport, message)
|
|
||||||
if err != nil {
|
|
||||||
if c.logger != nil {
|
|
||||||
c.logger.Debug("optimistic refresh failed for ", FqdnToDomain(question.Name), ": ", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if responseChecker != nil {
|
return response.Copy(), 0
|
||||||
var rejected bool
|
} else {
|
||||||
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
var expireAt time.Time
|
||||||
rejected = true
|
if !c.independentCache {
|
||||||
|
response, expireAt, loaded = c.cache.GetWithLifetime(question)
|
||||||
|
} else {
|
||||||
|
response, expireAt, loaded = c.transportCache.GetWithLifetime(transportCacheKey{
|
||||||
|
Question: question,
|
||||||
|
transportTag: transport.Tag(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if !loaded {
|
||||||
|
return nil, 0
|
||||||
|
}
|
||||||
|
timeNow := time.Now()
|
||||||
|
if timeNow.After(expireAt) {
|
||||||
|
if !c.independentCache {
|
||||||
|
c.cache.Remove(question)
|
||||||
} else {
|
} else {
|
||||||
rejected = !responseChecker(response)
|
c.transportCache.Remove(transportCacheKey{
|
||||||
|
Question: question,
|
||||||
|
transportTag: transport.Tag(),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
if rejected {
|
return nil, 0
|
||||||
if c.rdrc != nil {
|
|
||||||
c.rdrc.SaveRDRCAsync(transport.Tag(), question.Name, question.Qtype, c.logger)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
timeToLive := applyResponseOptions(question, response, options)
|
var originTTL int
|
||||||
c.storeCache(transport, question, response, timeToLive)
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
}()
|
for _, record := range recordList {
|
||||||
}
|
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
||||||
|
originTTL = int(record.Header().Ttl)
|
||||||
func (c *Client) prepareExchangeMessage(message *dns.Msg, options adapter.DNSQueryOptions) *dns.Msg {
|
}
|
||||||
clientSubnet := options.ClientSubnet
|
}
|
||||||
if !clientSubnet.IsValid() {
|
}
|
||||||
clientSubnet = c.clientSubnet
|
nowTTL := int(expireAt.Sub(timeNow).Seconds())
|
||||||
|
if nowTTL < 0 {
|
||||||
|
nowTTL = 0
|
||||||
|
}
|
||||||
|
response = response.Copy()
|
||||||
|
if originTTL > 0 {
|
||||||
|
duration := uint32(originTTL - nowTTL)
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
record.Header().Ttl = record.Header().Ttl - duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||||
|
for _, record := range recordList {
|
||||||
|
record.Header().Ttl = uint32(nowTTL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response, nowTTL
|
||||||
}
|
}
|
||||||
if clientSubnet.IsValid() {
|
|
||||||
message = SetClientSubnet(message, clientSubnet)
|
|
||||||
}
|
|
||||||
return message
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) exchangeToTransport(ctx context.Context, transport adapter.DNSTransport, message *dns.Msg) (*dns.Msg, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, c.timeout)
|
|
||||||
defer cancel()
|
|
||||||
response, err := transport.Exchange(ctx, message)
|
|
||||||
if err == nil {
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
var rcodeError RcodeError
|
|
||||||
if errors.As(err, &rcodeError) {
|
|
||||||
return FixedResponseStatus(message, int(rcodeError)), nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
func MessageToAddresses(response *dns.Msg) []netip.Addr {
|
||||||
return adapter.DNSResponseAddresses(response)
|
if response == nil || response.Rcode != dns.RcodeSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
addresses := make([]netip.Addr, 0, len(response.Answer))
|
||||||
|
for _, rawAnswer := range response.Answer {
|
||||||
|
switch answer := rawAnswer.(type) {
|
||||||
|
case *dns.A:
|
||||||
|
addresses = append(addresses, M.AddrFromIP(answer.A))
|
||||||
|
case *dns.AAAA:
|
||||||
|
addresses = append(addresses, M.AddrFromIP(answer.AAAA))
|
||||||
|
case *dns.HTTPS:
|
||||||
|
for _, value := range answer.SVCB.Value {
|
||||||
|
if value.Key() == dns.SVCB_IPV4HINT || value.Key() == dns.SVCB_IPV6HINT {
|
||||||
|
addresses = append(addresses, common.Map(strings.Split(value.String(), ","), M.ParseAddr)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addresses
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrapError(err error) error {
|
func wrapError(err error) error {
|
||||||
|
|||||||
@@ -22,19 +22,6 @@ func logCachedResponse(logger logger.ContextLogger, ctx context.Context, respons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logOptimisticResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg) {
|
|
||||||
if logger == nil || len(response.Question) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
domain := FqdnToDomain(response.Question[0].Name)
|
|
||||||
logger.DebugContext(ctx, "optimistic ", domain, " ", dns.RcodeToString[response.Rcode])
|
|
||||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
|
||||||
for _, record := range recordList {
|
|
||||||
logger.InfoContext(ctx, "optimistic ", dns.Type(record.Header().Rrtype).String(), " ", FormatQuestion(record.String()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
|
func logExchangedResponse(logger logger.ContextLogger, ctx context.Context, response *dns.Msg, ttl uint32) {
|
||||||
if logger == nil || len(response.Question) == 0 {
|
if logger == nil || len(response.Question) == 0 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,11 +5,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
||||||
RcodeServerFailure RcodeError = mDNS.RcodeServerFailure
|
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type RcodeError int
|
type RcodeError int
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
package dns
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/netip"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
|
||||||
C "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/option"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
"github.com/sagernet/sing/common/json/badoption"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestReproLookupWithRulesUsesRequestStrategy(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
defaultTransport := &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP}
|
|
||||||
var qTypes []uint16
|
|
||||||
router := newTestRouter(t, nil, &fakeDNSTransportManager{
|
|
||||||
defaultTransport: defaultTransport,
|
|
||||||
transports: map[string]adapter.DNSTransport{
|
|
||||||
"default": defaultTransport,
|
|
||||||
},
|
|
||||||
}, &fakeDNSClient{
|
|
||||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
qTypes = append(qTypes, message.Question[0].Qtype)
|
|
||||||
if message.Question[0].Qtype == mDNS.TypeA {
|
|
||||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2.2.2.2")}, 60), nil
|
|
||||||
}
|
|
||||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("2001:db8::1")}, 60), nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
addresses, err := router.Lookup(context.Background(), "example.com", adapter.DNSQueryOptions{
|
|
||||||
Strategy: C.DomainStrategyIPv4Only,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, []uint16{mDNS.TypeA}, qTypes)
|
|
||||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("2.2.2.2")}, addresses)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReproLogicalMatchResponseIPCIDR(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
transportManager := &fakeDNSTransportManager{
|
|
||||||
defaultTransport: &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
|
||||||
transports: map[string]adapter.DNSTransport{
|
|
||||||
"upstream": &fakeDNSTransport{tag: "upstream", transportType: C.DNSTypeUDP},
|
|
||||||
"selected": &fakeDNSTransport{tag: "selected", transportType: C.DNSTypeUDP},
|
|
||||||
"default": &fakeDNSTransport{tag: "default", transportType: C.DNSTypeUDP},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client := &fakeDNSClient{
|
|
||||||
exchange: func(transport adapter.DNSTransport, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
switch transport.Tag() {
|
|
||||||
case "upstream":
|
|
||||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("1.1.1.1")}, 60), nil
|
|
||||||
case "selected":
|
|
||||||
return FixedResponse(0, message.Question[0], []netip.Addr{netip.MustParseAddr("8.8.8.8")}, 60), nil
|
|
||||||
default:
|
|
||||||
return nil, E.New("unexpected transport")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
rules := []option.DNSRule{
|
|
||||||
{
|
|
||||||
Type: C.RuleTypeDefault,
|
|
||||||
DefaultOptions: option.DefaultDNSRule{
|
|
||||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
|
||||||
Domain: badoption.Listable[string]{"example.com"},
|
|
||||||
},
|
|
||||||
DNSRuleAction: option.DNSRuleAction{
|
|
||||||
Action: C.RuleActionTypeEvaluate,
|
|
||||||
RouteOptions: option.DNSRouteActionOptions{Server: "upstream"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Type: C.RuleTypeLogical,
|
|
||||||
LogicalOptions: option.LogicalDNSRule{
|
|
||||||
RawLogicalDNSRule: option.RawLogicalDNSRule{
|
|
||||||
Mode: C.LogicalTypeOr,
|
|
||||||
Rules: []option.DNSRule{{
|
|
||||||
Type: C.RuleTypeDefault,
|
|
||||||
DefaultOptions: option.DefaultDNSRule{
|
|
||||||
RawDefaultDNSRule: option.RawDefaultDNSRule{
|
|
||||||
MatchResponse: true,
|
|
||||||
IPCIDR: badoption.Listable[string]{"1.1.1.0/24"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}},
|
|
||||||
},
|
|
||||||
DNSRuleAction: option.DNSRuleAction{
|
|
||||||
Action: C.RuleActionTypeRoute,
|
|
||||||
RouteOptions: option.DNSRouteActionOptions{Server: "selected"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
router := newTestRouter(t, rules, transportManager, client)
|
|
||||||
|
|
||||||
response, err := router.Exchange(context.Background(), &mDNS.Msg{
|
|
||||||
Question: []mDNS.Question{fixedQuestion("example.com", mDNS.TypeA)},
|
|
||||||
}, adapter.DNSQueryOptions{})
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, []netip.Addr{netip.MustParseAddr("8.8.8.8")}, MessageToAddresses(response))
|
|
||||||
}
|
|
||||||
851
dns/router.go
851
dns/router.go
File diff suppressed because it is too large
Load Diff
2547
dns/router_test.go
2547
dns/router_test.go
File diff suppressed because it is too large
Load Diff
@@ -3,18 +3,17 @@ package transport
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
"github.com/sagernet/sing-box/common/httpclient"
|
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"github.com/sagernet/sing-box/dns"
|
||||||
@@ -45,20 +44,14 @@ type HTTPSTransport struct {
|
|||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
destination *url.URL
|
destination *url.URL
|
||||||
method string
|
headers http.Header
|
||||||
host string
|
|
||||||
queryHeaders http.Header
|
|
||||||
transportAccess sync.Mutex
|
transportAccess sync.Mutex
|
||||||
transport *httpclient.Client
|
transport *HTTPSTransportWrapper
|
||||||
transportResetAt time.Time
|
transportResetAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
remoteOptions := option.RemoteDNSServerOptions{
|
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
|
||||||
}
|
|
||||||
remoteOptions.DialerOptions = options.DialerOptions
|
|
||||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -69,21 +62,28 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if len(tlsConfig.NextProtos()) == 0 {
|
if len(tlsConfig.NextProtos()) == 0 {
|
||||||
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS})
|
tlsConfig.SetNextProtos([]string{http2.NextProtoTLS, "http/1.1"})
|
||||||
} else if !common.Contains(tlsConfig.NextProtos(), http2.NextProtoTLS) {
|
|
||||||
tlsConfig.SetNextProtos(append([]string{http2.NextProtoTLS}, tlsConfig.NextProtos()...))
|
|
||||||
}
|
}
|
||||||
headers := options.Headers.Build()
|
headers := options.Headers.Build()
|
||||||
serverAddr := options.DNSServerAddressOptions.Build()
|
host := headers.Get("Host")
|
||||||
if serverAddr.Port == 0 {
|
if host != "" {
|
||||||
serverAddr.Port = 443
|
headers.Del("Host")
|
||||||
}
|
} else {
|
||||||
if !serverAddr.IsValid() {
|
if tlsConfig.ServerName() != "" {
|
||||||
return nil, E.New("invalid server address: ", serverAddr)
|
host = tlsConfig.ServerName()
|
||||||
|
} else {
|
||||||
|
host = options.Server
|
||||||
|
}
|
||||||
}
|
}
|
||||||
destinationURL := url.URL{
|
destinationURL := url.URL{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
Host: doHURLHost(serverAddr, 443),
|
Host: host,
|
||||||
|
}
|
||||||
|
if destinationURL.Host == "" {
|
||||||
|
destinationURL.Host = options.Server
|
||||||
|
}
|
||||||
|
if options.ServerPort != 0 && options.ServerPort != 443 {
|
||||||
|
destinationURL.Host = net.JoinHostPort(destinationURL.Host, strconv.Itoa(int(options.ServerPort)))
|
||||||
}
|
}
|
||||||
path := options.Path
|
path := options.Path
|
||||||
if path == "" {
|
if path == "" {
|
||||||
@@ -93,67 +93,41 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
method := strings.ToUpper(options.Method)
|
serverAddr := options.DNSServerAddressOptions.Build()
|
||||||
if method == "" {
|
if serverAddr.Port == 0 {
|
||||||
method = http.MethodPost
|
serverAddr.Port = 443
|
||||||
}
|
}
|
||||||
switch method {
|
if !serverAddr.IsValid() {
|
||||||
case http.MethodGet, http.MethodPost:
|
return nil, E.New("invalid server address: ", serverAddr)
|
||||||
default:
|
|
||||||
return nil, E.New("unsupported HTTPS DNS method: ", options.Method)
|
|
||||||
}
|
}
|
||||||
httpClientOptions := options.HTTPClientOptions
|
return NewHTTPSRaw(
|
||||||
return NewHTTPRaw(
|
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
|
||||||
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, remoteOptions),
|
|
||||||
logger,
|
logger,
|
||||||
transportDialer,
|
transportDialer,
|
||||||
&destinationURL,
|
&destinationURL,
|
||||||
headers,
|
headers,
|
||||||
|
serverAddr,
|
||||||
tlsConfig,
|
tlsConfig,
|
||||||
httpClientOptions,
|
), nil
|
||||||
method,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPRaw(
|
func NewHTTPSRaw(
|
||||||
adapter dns.TransportAdapter,
|
adapter dns.TransportAdapter,
|
||||||
logger logger.ContextLogger,
|
logger log.ContextLogger,
|
||||||
dialer N.Dialer,
|
dialer N.Dialer,
|
||||||
destination *url.URL,
|
destination *url.URL,
|
||||||
headers http.Header,
|
headers http.Header,
|
||||||
|
serverAddr M.Socksaddr,
|
||||||
tlsConfig tls.Config,
|
tlsConfig tls.Config,
|
||||||
httpClientOptions option.HTTPClientOptions,
|
) *HTTPSTransport {
|
||||||
method string,
|
|
||||||
) (*HTTPSTransport, error) {
|
|
||||||
if destination.Scheme == "https" && tlsConfig == nil {
|
|
||||||
return nil, E.New("TLS transport unavailable")
|
|
||||||
}
|
|
||||||
queryHeaders := headers.Clone()
|
|
||||||
if queryHeaders == nil {
|
|
||||||
queryHeaders = make(http.Header)
|
|
||||||
}
|
|
||||||
host := queryHeaders.Get("Host")
|
|
||||||
queryHeaders.Del("Host")
|
|
||||||
queryHeaders.Set("Accept", MimeType)
|
|
||||||
if method == http.MethodPost {
|
|
||||||
queryHeaders.Set("Content-Type", MimeType)
|
|
||||||
}
|
|
||||||
httpClientOptions.Tag = ""
|
|
||||||
httpClientOptions.Headers = nil
|
|
||||||
currentTransport, err := httpclient.NewClientWithDialer(dialer, tlsConfig, "", httpClientOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &HTTPSTransport{
|
return &HTTPSTransport{
|
||||||
TransportAdapter: adapter,
|
TransportAdapter: adapter,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
dialer: dialer,
|
dialer: dialer,
|
||||||
destination: destination,
|
destination: destination,
|
||||||
method: method,
|
headers: headers,
|
||||||
host: host,
|
transport: NewHTTPSTransportWrapper(tls.NewDialer(dialer, tlsConfig), serverAddr),
|
||||||
queryHeaders: queryHeaders,
|
}
|
||||||
transport: currentTransport,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
func (t *HTTPSTransport) Start(stage adapter.StartStage) error {
|
||||||
@@ -207,25 +181,14 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
|||||||
requestBuffer.Release()
|
requestBuffer.Release()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
requestURL := *t.destination
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, t.destination.String(), bytes.NewReader(rawMessage))
|
||||||
var request *http.Request
|
|
||||||
switch t.method {
|
|
||||||
case http.MethodGet:
|
|
||||||
query := requestURL.Query()
|
|
||||||
query.Set("dns", base64.RawURLEncoding.EncodeToString(rawMessage))
|
|
||||||
requestURL.RawQuery = query.Encode()
|
|
||||||
request, err = http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), nil)
|
|
||||||
default:
|
|
||||||
request, err = http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(rawMessage))
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
requestBuffer.Release()
|
requestBuffer.Release()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
request.Header = t.queryHeaders.Clone()
|
request.Header = t.headers.Clone()
|
||||||
if t.host != "" {
|
request.Header.Set("Content-Type", MimeType)
|
||||||
request.Host = t.host
|
request.Header.Set("Accept", MimeType)
|
||||||
}
|
|
||||||
t.transportAccess.Lock()
|
t.transportAccess.Lock()
|
||||||
currentTransport := t.transport
|
currentTransport := t.transport
|
||||||
t.transportAccess.Unlock()
|
t.transportAccess.Unlock()
|
||||||
@@ -259,13 +222,3 @@ func (t *HTTPSTransport) exchange(ctx context.Context, message *mDNS.Msg) (*mDNS
|
|||||||
}
|
}
|
||||||
return &responseMessage, nil
|
return &responseMessage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func doHURLHost(serverAddr M.Socksaddr, defaultPort uint16) string {
|
|
||||||
if serverAddr.Port != defaultPort {
|
|
||||||
return serverAddr.String()
|
|
||||||
}
|
|
||||||
if serverAddr.IsIPv6() {
|
|
||||||
return "[" + serverAddr.AddrString() + "]"
|
|
||||||
}
|
|
||||||
return serverAddr.AddrString()
|
|
||||||
}
|
|
||||||
|
|||||||
80
dns/transport/https_transport.go
Normal file
80
dns/transport/https_transport.go
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
)
|
||||||
|
|
||||||
|
var errFallback = E.New("fallback to HTTP/1.1")
|
||||||
|
|
||||||
|
type HTTPSTransportWrapper struct {
|
||||||
|
http2Transport *http2.Transport
|
||||||
|
httpTransport *http.Transport
|
||||||
|
fallback *atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHTTPSTransportWrapper(dialer tls.Dialer, serverAddr M.Socksaddr) *HTTPSTransportWrapper {
|
||||||
|
var fallback atomic.Bool
|
||||||
|
return &HTTPSTransportWrapper{
|
||||||
|
http2Transport: &http2.Transport{
|
||||||
|
DialTLSContext: func(ctx context.Context, _, _ string, _ *tls.STDConfig) (net.Conn, error) {
|
||||||
|
tlsConn, err := dialer.DialTLSContext(ctx, serverAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
state := tlsConn.ConnectionState()
|
||||||
|
if state.NegotiatedProtocol == http2.NextProtoTLS {
|
||||||
|
return tlsConn, nil
|
||||||
|
}
|
||||||
|
tlsConn.Close()
|
||||||
|
fallback.Store(true)
|
||||||
|
return nil, errFallback
|
||||||
|
},
|
||||||
|
},
|
||||||
|
httpTransport: &http.Transport{
|
||||||
|
DialTLSContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
|
||||||
|
return dialer.DialTLSContext(ctx, serverAddr)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fallback: &fallback,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPSTransportWrapper) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
if h.fallback.Load() {
|
||||||
|
return h.httpTransport.RoundTrip(request)
|
||||||
|
} else {
|
||||||
|
response, err := h.http2Transport.RoundTrip(request)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, errFallback) {
|
||||||
|
return h.httpTransport.RoundTrip(request)
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPSTransportWrapper) CloseIdleConnections() {
|
||||||
|
h.http2Transport.CloseIdleConnections()
|
||||||
|
h.httpTransport.CloseIdleConnections()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *HTTPSTransportWrapper) Clone() *HTTPSTransportWrapper {
|
||||||
|
return &HTTPSTransportWrapper{
|
||||||
|
httpTransport: h.httpTransport,
|
||||||
|
http2Transport: &http2.Transport{
|
||||||
|
DialTLSContext: h.http2Transport.DialTLSContext,
|
||||||
|
},
|
||||||
|
fallback: h.fallback,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ package local
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
|
||||||
"github.com/sagernet/sing-box/adapter"
|
"github.com/sagernet/sing-box/adapter"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
@@ -12,6 +14,7 @@ import (
|
|||||||
"github.com/sagernet/sing-box/log"
|
"github.com/sagernet/sing-box/log"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
"github.com/sagernet/sing/common"
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/logger"
|
"github.com/sagernet/sing/common/logger"
|
||||||
M "github.com/sagernet/sing/common/metadata"
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
@@ -32,8 +35,10 @@ type Transport struct {
|
|||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
hosts *hosts.File
|
hosts *hosts.File
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
|
preferGo bool
|
||||||
fallback bool
|
fallback bool
|
||||||
dhcpTransport dhcpTransport
|
dhcpTransport dhcpTransport
|
||||||
|
resolver net.Resolver
|
||||||
}
|
}
|
||||||
|
|
||||||
type dhcpTransport interface {
|
type dhcpTransport interface {
|
||||||
@@ -47,12 +52,14 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options)
|
||||||
return &Transport{
|
return &Transport{
|
||||||
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
TransportAdapter: transportAdapter,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
hosts: hosts.NewFile(hosts.DefaultPath),
|
hosts: hosts.NewFile(hosts.DefaultPath),
|
||||||
dialer: transportDialer,
|
dialer: transportDialer,
|
||||||
|
preferGo: options.PreferGo,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,3 +97,44 @@ func (t *Transport) Reset() {
|
|||||||
t.dhcpTransport.Reset()
|
t.dhcpTransport.Reset()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
question := message.Question[0]
|
||||||
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
|
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
|
||||||
|
if len(addresses) > 0 {
|
||||||
|
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !t.fallback {
|
||||||
|
return t.exchange(ctx, message, question.Name)
|
||||||
|
}
|
||||||
|
if t.dhcpTransport != nil {
|
||||||
|
dhcpTransports := t.dhcpTransport.Fetch()
|
||||||
|
if len(dhcpTransports) > 0 {
|
||||||
|
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t.preferGo {
|
||||||
|
// Assuming the user knows what they are doing, we still execute the query which will fail.
|
||||||
|
return t.exchange(ctx, message, question.Name)
|
||||||
|
}
|
||||||
|
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||||
|
var network string
|
||||||
|
if question.Qtype == mDNS.TypeA {
|
||||||
|
network = "ip4"
|
||||||
|
} else {
|
||||||
|
network = "ip6"
|
||||||
|
}
|
||||||
|
addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name)
|
||||||
|
if err != nil {
|
||||||
|
var dnsError *net.DNSError
|
||||||
|
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||||
|
return nil, dns.RcodeRefused
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||||
|
}
|
||||||
|
return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.")
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,249 +0,0 @@
|
|||||||
//go:build darwin
|
|
||||||
|
|
||||||
package local
|
|
||||||
|
|
||||||
/*
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <dns.h>
|
|
||||||
#include <resolv.h>
|
|
||||||
|
|
||||||
static void *cgo_dns_open_super() {
|
|
||||||
return (void *)dns_open(NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void cgo_dns_close(void *opaque) {
|
|
||||||
if (opaque != NULL) dns_free((dns_handle_t)opaque);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int cgo_dns_search(void *opaque, const char *name, int class, int type,
|
|
||||||
unsigned char *answer, int anslen) {
|
|
||||||
dns_handle_t handle = (dns_handle_t)opaque;
|
|
||||||
struct sockaddr_storage from;
|
|
||||||
uint32_t fromlen = sizeof(from);
|
|
||||||
return dns_search(handle, name, class, type, (char *)answer, anslen, (struct sockaddr *)&from, &fromlen);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void *cgo_res_init() {
|
|
||||||
res_state state = calloc(1, sizeof(struct __res_state));
|
|
||||||
if (state == NULL) return NULL;
|
|
||||||
if (res_ninit(state) != 0) {
|
|
||||||
free(state);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void cgo_res_destroy(void *opaque) {
|
|
||||||
res_state state = (res_state)opaque;
|
|
||||||
res_ndestroy(state);
|
|
||||||
free(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type,
|
|
||||||
unsigned char *answer, int anslen,
|
|
||||||
int timeout_seconds,
|
|
||||||
int *out_h_errno) {
|
|
||||||
res_state state = (res_state)opaque;
|
|
||||||
state->retrans = timeout_seconds;
|
|
||||||
state->retry = 1;
|
|
||||||
int n = res_nsearch(state, dname, class, type, answer, anslen);
|
|
||||||
if (n < 0) {
|
|
||||||
*out_h_errno = state->res_h_errno;
|
|
||||||
}
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
import "C"
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
boxC "github.com/sagernet/sing-box/constant"
|
|
||||||
"github.com/sagernet/sing-box/dns"
|
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
|
||||||
|
|
||||||
mDNS "github.com/miekg/dns"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
darwinResolverHostNotFound = 1
|
|
||||||
darwinResolverTryAgain = 2
|
|
||||||
darwinResolverNoRecovery = 3
|
|
||||||
darwinResolverNoData = 4
|
|
||||||
|
|
||||||
darwinResolverMaxPacketSize = 65535
|
|
||||||
)
|
|
||||||
|
|
||||||
var errDarwinNeedLargerBuffer = errors.New("darwin resolver response truncated")
|
|
||||||
|
|
||||||
func darwinLookupSystemDNS(name string, class, qtype, timeoutSeconds int) (*mDNS.Msg, error) {
|
|
||||||
response, err := darwinSearchWithSystemRouting(name, class, qtype)
|
|
||||||
if err == nil {
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
fallbackResponse, fallbackErr := darwinSearchWithResolv(name, class, qtype, timeoutSeconds)
|
|
||||||
if fallbackErr == nil || fallbackResponse != nil {
|
|
||||||
return fallbackResponse, fallbackErr
|
|
||||||
}
|
|
||||||
return nil, E.Errors(
|
|
||||||
E.Cause(err, "dns_search"),
|
|
||||||
E.Cause(fallbackErr, "res_nsearch"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func darwinSearchWithSystemRouting(name string, class, qtype int) (*mDNS.Msg, error) {
|
|
||||||
handle := C.cgo_dns_open_super()
|
|
||||||
if handle == nil {
|
|
||||||
return nil, E.New("dns_open failed")
|
|
||||||
}
|
|
||||||
defer C.cgo_dns_close(handle)
|
|
||||||
|
|
||||||
cName := C.CString(name)
|
|
||||||
defer C.free(unsafe.Pointer(cName))
|
|
||||||
|
|
||||||
bufSize := 1232
|
|
||||||
for {
|
|
||||||
answer := make([]byte, bufSize)
|
|
||||||
n := C.cgo_dns_search(handle, cName, C.int(class), C.int(qtype),
|
|
||||||
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)))
|
|
||||||
if n <= 0 {
|
|
||||||
return nil, E.New("dns_search failed for ", name)
|
|
||||||
}
|
|
||||||
if int(n) > bufSize {
|
|
||||||
bufSize = int(n)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return unpackDarwinResolverMessage(answer[:int(n)], "dns_search")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func darwinSearchWithResolv(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
|
|
||||||
state := C.cgo_res_init()
|
|
||||||
if state == nil {
|
|
||||||
return nil, E.New("res_ninit failed")
|
|
||||||
}
|
|
||||||
defer C.cgo_res_destroy(state)
|
|
||||||
|
|
||||||
cName := C.CString(name)
|
|
||||||
defer C.free(unsafe.Pointer(cName))
|
|
||||||
|
|
||||||
bufSize := 1232
|
|
||||||
for {
|
|
||||||
answer := make([]byte, bufSize)
|
|
||||||
var hErrno C.int
|
|
||||||
n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype),
|
|
||||||
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)),
|
|
||||||
C.int(timeoutSeconds),
|
|
||||||
&hErrno)
|
|
||||||
if n >= 0 {
|
|
||||||
if int(n) > bufSize {
|
|
||||||
bufSize = int(n)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return unpackDarwinResolverMessage(answer[:int(n)], "res_nsearch")
|
|
||||||
}
|
|
||||||
response, err := handleDarwinResolvFailure(name, answer, int(hErrno))
|
|
||||||
if err == nil {
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
if errors.Is(err, errDarwinNeedLargerBuffer) && bufSize < darwinResolverMaxPacketSize {
|
|
||||||
bufSize *= 2
|
|
||||||
if bufSize > darwinResolverMaxPacketSize {
|
|
||||||
bufSize = darwinResolverMaxPacketSize
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func unpackDarwinResolverMessage(packet []byte, source string) (*mDNS.Msg, error) {
|
|
||||||
var response mDNS.Msg
|
|
||||||
err := response.Unpack(packet)
|
|
||||||
if err != nil {
|
|
||||||
return nil, E.Cause(err, "unpack ", source, " response")
|
|
||||||
}
|
|
||||||
return &response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func handleDarwinResolvFailure(name string, answer []byte, hErrno int) (*mDNS.Msg, error) {
|
|
||||||
response, err := unpackDarwinResolverMessage(answer, "res_nsearch failure")
|
|
||||||
if err == nil && response.Response {
|
|
||||||
if response.Truncated && len(answer) < darwinResolverMaxPacketSize {
|
|
||||||
return nil, errDarwinNeedLargerBuffer
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
return nil, darwinResolverHErrno(name, hErrno)
|
|
||||||
}
|
|
||||||
|
|
||||||
func darwinResolverHErrno(name string, hErrno int) error {
|
|
||||||
switch hErrno {
|
|
||||||
case darwinResolverHostNotFound:
|
|
||||||
return dns.RcodeNameError
|
|
||||||
case darwinResolverTryAgain:
|
|
||||||
return dns.RcodeServerFailure
|
|
||||||
case darwinResolverNoRecovery:
|
|
||||||
return dns.RcodeServerFailure
|
|
||||||
case darwinResolverNoData:
|
|
||||||
return dns.RcodeSuccess
|
|
||||||
default:
|
|
||||||
return E.New("res_nsearch: unknown error ", hErrno, " for ", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
|
||||||
question := message.Question[0]
|
|
||||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
|
||||||
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
|
|
||||||
if len(addresses) > 0 {
|
|
||||||
return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if t.fallback && t.dhcpTransport != nil {
|
|
||||||
dhcpServers := t.dhcpTransport.Fetch()
|
|
||||||
if len(dhcpServers) > 0 {
|
|
||||||
return t.dhcpTransport.Exchange0(ctx, message, dhcpServers)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
name := question.Name
|
|
||||||
timeoutSeconds := int(boxC.DNSTimeout / time.Second)
|
|
||||||
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
|
|
||||||
remaining := time.Until(deadline)
|
|
||||||
if remaining <= 0 {
|
|
||||||
return nil, context.DeadlineExceeded
|
|
||||||
}
|
|
||||||
seconds := int(remaining.Seconds())
|
|
||||||
if seconds < 1 {
|
|
||||||
seconds = 1
|
|
||||||
}
|
|
||||||
timeoutSeconds = seconds
|
|
||||||
}
|
|
||||||
type resolvResult struct {
|
|
||||||
response *mDNS.Msg
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
resultCh := make(chan resolvResult, 1)
|
|
||||||
go func() {
|
|
||||||
response, err := darwinLookupSystemDNS(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
|
|
||||||
resultCh <- resolvResult{response, err}
|
|
||||||
}()
|
|
||||||
var result resolvResult
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
case result = <-resultCh:
|
|
||||||
}
|
|
||||||
if result.err != nil {
|
|
||||||
var rcodeError dns.RcodeError
|
|
||||||
if errors.As(result.err, &rcodeError) {
|
|
||||||
return dns.FixedResponseStatus(message, int(rcodeError)), nil
|
|
||||||
}
|
|
||||||
return nil, result.err
|
|
||||||
}
|
|
||||||
result.response.Id = message.Id
|
|
||||||
return result.response, nil
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
//go:build !darwin
|
|
||||||
|
|
||||||
package local
|
package local
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sagernet/quic-go"
|
"github.com/sagernet/quic-go"
|
||||||
"github.com/sagernet/quic-go/http3"
|
"github.com/sagernet/quic-go/http3"
|
||||||
@@ -41,23 +40,18 @@ func RegisterHTTP3Transport(registry *dns.TransportRegistry) {
|
|||||||
|
|
||||||
type HTTP3Transport struct {
|
type HTTP3Transport struct {
|
||||||
dns.TransportAdapter
|
dns.TransportAdapter
|
||||||
logger logger.ContextLogger
|
logger logger.ContextLogger
|
||||||
dialer N.Dialer
|
dialer N.Dialer
|
||||||
destination *url.URL
|
destination *url.URL
|
||||||
headers http.Header
|
headers http.Header
|
||||||
handshakeTimeout time.Duration
|
serverAddr M.Socksaddr
|
||||||
serverAddr M.Socksaddr
|
tlsConfig *tls.STDConfig
|
||||||
tlsConfig *tls.STDConfig
|
transportAccess sync.Mutex
|
||||||
transportAccess sync.Mutex
|
transport *http3.Transport
|
||||||
transport *http3.Transport
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options option.RemoteHTTPSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
remoteOptions := option.RemoteDNSServerOptions{
|
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||||
DNSServerAddressOptions: options.DNSServerAddressOptions,
|
|
||||||
}
|
|
||||||
remoteOptions.DialerOptions = options.DialerOptions
|
|
||||||
transportDialer, err := dns.NewRemoteDialer(ctx, remoteOptions)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -67,7 +61,6 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
handshakeTimeout := tlsConfig.HandshakeTimeout()
|
|
||||||
stdConfig, err := tlsConfig.STDConfig()
|
stdConfig, err := tlsConfig.STDConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -109,12 +102,11 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
|||||||
return nil, E.New("invalid server address: ", serverAddr)
|
return nil, E.New("invalid server address: ", serverAddr)
|
||||||
}
|
}
|
||||||
t := &HTTP3Transport{
|
t := &HTTP3Transport{
|
||||||
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, remoteOptions),
|
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
dialer: transportDialer,
|
dialer: transportDialer,
|
||||||
destination: &destinationURL,
|
destination: &destinationURL,
|
||||||
headers: headers,
|
headers: headers,
|
||||||
handshakeTimeout: handshakeTimeout,
|
|
||||||
serverAddr: serverAddr,
|
serverAddr: serverAddr,
|
||||||
tlsConfig: stdConfig,
|
tlsConfig: stdConfig,
|
||||||
}
|
}
|
||||||
@@ -123,17 +115,8 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *HTTP3Transport) newTransport() *http3.Transport {
|
func (t *HTTP3Transport) newTransport() *http3.Transport {
|
||||||
quicConfig := &quic.Config{}
|
|
||||||
if t.handshakeTimeout > 0 {
|
|
||||||
quicConfig.HandshakeIdleTimeout = t.handshakeTimeout
|
|
||||||
}
|
|
||||||
return &http3.Transport{
|
return &http3.Transport{
|
||||||
QUICConfig: quicConfig,
|
|
||||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
|
Dial: func(ctx context.Context, addr string, tlsCfg *tls.STDConfig, cfg *quic.Config) (*quic.Conn, error) {
|
||||||
if t.handshakeTimeout > 0 && cfg.HandshakeIdleTimeout == 0 {
|
|
||||||
cfg = cfg.Clone()
|
|
||||||
cfg.HandshakeIdleTimeout = t.handshakeTimeout
|
|
||||||
}
|
|
||||||
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
conn, dialErr := t.dialer.DialContext(ctx, N.NetworkUDP, t.serverAddr)
|
||||||
if dialErr != nil {
|
if dialErr != nil {
|
||||||
return nil, dialErr
|
return nil, dialErr
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
package dns
|
package dns
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ adapter.LegacyDNSTransport = (*TransportAdapter)(nil)
|
||||||
|
|
||||||
type TransportAdapter struct {
|
type TransportAdapter struct {
|
||||||
transportType string
|
transportType string
|
||||||
transportTag string
|
transportTag string
|
||||||
dependencies []string
|
dependencies []string
|
||||||
|
strategy C.DomainStrategy
|
||||||
|
clientSubnet netip.Prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
|
func NewTransportAdapter(transportType string, transportTag string, dependencies []string) TransportAdapter {
|
||||||
@@ -27,6 +35,8 @@ func NewTransportAdapterWithLocalOptions(transportType string, transportTag stri
|
|||||||
transportType: transportType,
|
transportType: transportType,
|
||||||
transportTag: transportTag,
|
transportTag: transportTag,
|
||||||
dependencies: dependencies,
|
dependencies: dependencies,
|
||||||
|
strategy: C.DomainStrategy(localOptions.LegacyStrategy),
|
||||||
|
clientSubnet: localOptions.LegacyClientSubnet,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,10 +45,15 @@ func NewTransportAdapterWithRemoteOptions(transportType string, transportTag str
|
|||||||
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
|
if remoteOptions.DomainResolver != nil && remoteOptions.DomainResolver.Server != "" {
|
||||||
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
|
dependencies = append(dependencies, remoteOptions.DomainResolver.Server)
|
||||||
}
|
}
|
||||||
|
if remoteOptions.LegacyAddressResolver != "" {
|
||||||
|
dependencies = append(dependencies, remoteOptions.LegacyAddressResolver)
|
||||||
|
}
|
||||||
return TransportAdapter{
|
return TransportAdapter{
|
||||||
transportType: transportType,
|
transportType: transportType,
|
||||||
transportTag: transportTag,
|
transportTag: transportTag,
|
||||||
dependencies: dependencies,
|
dependencies: dependencies,
|
||||||
|
strategy: C.DomainStrategy(remoteOptions.LegacyStrategy),
|
||||||
|
clientSubnet: remoteOptions.LegacyClientSubnet,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,3 +68,11 @@ func (a *TransportAdapter) Tag() string {
|
|||||||
func (a *TransportAdapter) Dependencies() []string {
|
func (a *TransportAdapter) Dependencies() []string {
|
||||||
return a.dependencies
|
return a.dependencies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *TransportAdapter) LegacyStrategy() C.DomainStrategy {
|
||||||
|
return a.strategy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *TransportAdapter) LegacyClientSubnet() netip.Prefix {
|
||||||
|
return a.clientSubnet
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,25 +2,104 @@ package dns
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/option"
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
N "github.com/sagernet/sing/common/network"
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
|
func NewLocalDialer(ctx context.Context, options option.LocalDNSServerOptions) (N.Dialer, error) {
|
||||||
return dialer.NewWithOptions(dialer.Options{
|
if options.LegacyDefaultDialer {
|
||||||
Context: ctx,
|
return dialer.NewDefaultOutbound(ctx), nil
|
||||||
Options: options.DialerOptions,
|
} else {
|
||||||
DirectResolver: true,
|
return dialer.NewWithOptions(dialer.Options{
|
||||||
})
|
Context: ctx,
|
||||||
|
Options: options.DialerOptions,
|
||||||
|
DirectResolver: true,
|
||||||
|
LegacyDNSDialer: options.Legacy,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
|
func NewRemoteDialer(ctx context.Context, options option.RemoteDNSServerOptions) (N.Dialer, error) {
|
||||||
return dialer.NewWithOptions(dialer.Options{
|
if options.LegacyDefaultDialer {
|
||||||
Context: ctx,
|
transportDialer := dialer.NewDefaultOutbound(ctx)
|
||||||
Options: options.DialerOptions,
|
if options.LegacyAddressResolver != "" {
|
||||||
RemoteIsDomain: options.ServerIsDomain(),
|
transport := service.FromContext[adapter.DNSTransportManager](ctx)
|
||||||
DirectResolver: true,
|
resolverTransport, loaded := transport.Transport(options.LegacyAddressResolver)
|
||||||
})
|
if !loaded {
|
||||||
|
return nil, E.New("address resolver not found: ", options.LegacyAddressResolver)
|
||||||
|
}
|
||||||
|
transportDialer = newTransportDialer(transportDialer, service.FromContext[adapter.DNSRouter](ctx), resolverTransport, C.DomainStrategy(options.LegacyAddressStrategy), time.Duration(options.LegacyAddressFallbackDelay))
|
||||||
|
} else if options.ServerIsDomain() {
|
||||||
|
return nil, E.New("missing address resolver for server: ", options.Server)
|
||||||
|
}
|
||||||
|
return transportDialer, nil
|
||||||
|
} else {
|
||||||
|
return dialer.NewWithOptions(dialer.Options{
|
||||||
|
Context: ctx,
|
||||||
|
Options: options.DialerOptions,
|
||||||
|
RemoteIsDomain: options.ServerIsDomain(),
|
||||||
|
DirectResolver: true,
|
||||||
|
LegacyDNSDialer: options.Legacy,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type legacyTransportDialer struct {
|
||||||
|
dialer N.Dialer
|
||||||
|
dnsRouter adapter.DNSRouter
|
||||||
|
transport adapter.DNSTransport
|
||||||
|
strategy C.DomainStrategy
|
||||||
|
fallbackDelay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTransportDialer(dialer N.Dialer, dnsRouter adapter.DNSRouter, transport adapter.DNSTransport, strategy C.DomainStrategy, fallbackDelay time.Duration) *legacyTransportDialer {
|
||||||
|
return &legacyTransportDialer{
|
||||||
|
dialer,
|
||||||
|
dnsRouter,
|
||||||
|
transport,
|
||||||
|
strategy,
|
||||||
|
fallbackDelay,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *legacyTransportDialer) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
if destination.IsIP() {
|
||||||
|
return d.dialer.DialContext(ctx, network, destination)
|
||||||
|
}
|
||||||
|
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
||||||
|
Transport: d.transport,
|
||||||
|
Strategy: d.strategy,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return N.DialParallel(ctx, d.dialer, network, destination, addresses, d.strategy == C.DomainStrategyPreferIPv6, d.fallbackDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *legacyTransportDialer) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
if destination.IsIP() {
|
||||||
|
return d.dialer.ListenPacket(ctx, destination)
|
||||||
|
}
|
||||||
|
addresses, err := d.dnsRouter.Lookup(ctx, destination.Fqdn, adapter.DNSQueryOptions{
|
||||||
|
Transport: d.transport,
|
||||||
|
Strategy: d.strategy,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
conn, _, err := N.ListenSerial(ctx, d.dialer, destination, addresses)
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *legacyTransportDialer) Upstream() any {
|
||||||
|
return d.dialer
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,110 +2,11 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 1.14.0-alpha.11
|
#### 1.14.0-alpha.5
|
||||||
|
|
||||||
* Add optimistic DNS cache **1**
|
|
||||||
* Update NaiveProxy to 147.0.7727.49
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
Optimistic DNS cache returns an expired cached response immediately while
|
|
||||||
refreshing it in the background, reducing tail latency for repeated
|
|
||||||
queries. Enabled via [`optimistic`](/configuration/dns/#optimistic)
|
|
||||||
in DNS options, and can be persisted across restarts with the new
|
|
||||||
[`store_dns`](/configuration/experimental/cache-file/#store_dns) cache
|
|
||||||
file option. A per-query
|
|
||||||
[`disable_optimistic_cache`](/configuration/dns/rule_action/#disable_optimistic_cache)
|
|
||||||
field is also available on DNS rule actions and the `resolve` route rule
|
|
||||||
action.
|
|
||||||
|
|
||||||
This deprecates the `independent_cache` DNS option (the DNS cache now
|
|
||||||
always keys by transport) and the `store_rdrc` cache file option
|
|
||||||
(replaced by `store_dns`); both will be removed in sing-box 1.16.0.
|
|
||||||
See [Migration](/migration/#migrate-independent-dns-cache).
|
|
||||||
|
|
||||||
#### 1.14.0-alpha.10
|
|
||||||
|
|
||||||
* Add `evaluate` DNS rule action and Response Match Fields **1**
|
|
||||||
* `ip_version` and `query_type` now also take effect on internal DNS lookups **2**
|
|
||||||
* Add `package_name_regex` route, DNS and headless rule item **3**
|
|
||||||
* Add cloudflared inbound **4**
|
|
||||||
* Fixes and improvements
|
|
||||||
|
|
||||||
**1**:
|
|
||||||
|
|
||||||
Response Match Fields
|
|
||||||
([`response_rcode`](/configuration/dns/rule/#response_rcode),
|
|
||||||
[`response_answer`](/configuration/dns/rule/#response_answer),
|
|
||||||
[`response_ns`](/configuration/dns/rule/#response_ns),
|
|
||||||
and [`response_extra`](/configuration/dns/rule/#response_extra))
|
|
||||||
match the evaluated DNS response. They are gated by the new
|
|
||||||
[`match_response`](/configuration/dns/rule/#match_response) field and
|
|
||||||
populated by a preceding
|
|
||||||
[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action;
|
|
||||||
the evaluated response can also be returned directly by a
|
|
||||||
[`respond`](/configuration/dns/rule_action/#respond) action.
|
|
||||||
|
|
||||||
This deprecates the Legacy Address Filter Fields (`ip_cidr`,
|
|
||||||
`ip_is_private` without `match_response`) in DNS rules, the Legacy
|
|
||||||
`strategy` DNS rule action option, and the Legacy
|
|
||||||
`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed
|
|
||||||
in sing-box 1.16.0.
|
|
||||||
See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
|
||||||
|
|
||||||
**2**:
|
|
||||||
|
|
||||||
`ip_version` and `query_type` in DNS rules, together with `query_type` in
|
|
||||||
referenced rule-sets, now take effect on every DNS rule evaluation,
|
|
||||||
including matches from internal domain resolutions that do not target a
|
|
||||||
specific DNS server (for example a `resolve` route rule action without
|
|
||||||
`server` set). In earlier versions they were silently ignored in that
|
|
||||||
path. Combining these fields with any of the legacy DNS fields deprecated
|
|
||||||
in **1** in the same DNS configuration is no longer supported and is
|
|
||||||
rejected at startup.
|
|
||||||
See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
|
|
||||||
|
|
||||||
**3**:
|
|
||||||
|
|
||||||
See [Route Rule](/configuration/route/rule/#package_name_regex),
|
|
||||||
[DNS Rule](/configuration/dns/rule/#package_name_regex) and
|
|
||||||
[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex).
|
|
||||||
|
|
||||||
**4**:
|
|
||||||
|
|
||||||
See [Cloudflared](/configuration/inbound/cloudflared/).
|
|
||||||
|
|
||||||
#### 1.13.7
|
|
||||||
|
|
||||||
* Fixes and improvement
|
|
||||||
|
|
||||||
#### 1.13.6
|
|
||||||
|
|
||||||
* Fixes and improvements
|
* Fixes and improvements
|
||||||
|
|
||||||
#### 1.14.0-alpha.8
|
#### 1.13.4-beta.2
|
||||||
|
|
||||||
* 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
|
* Fixes and improvements
|
||||||
|
|
||||||
@@ -826,7 +727,7 @@ DNS servers are refactored for better performance and scalability.
|
|||||||
|
|
||||||
See [DNS server](/configuration/dns/server/).
|
See [DNS server](/configuration/dns/server/).
|
||||||
|
|
||||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
|
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
|
||||||
|
|
||||||
Compatibility for old formats will be removed in sing-box 1.14.0.
|
Compatibility for old formats will be removed in sing-box 1.14.0.
|
||||||
|
|
||||||
@@ -1296,7 +1197,7 @@ DNS servers are refactored for better performance and scalability.
|
|||||||
|
|
||||||
See [DNS server](/configuration/dns/server/).
|
See [DNS server](/configuration/dns/server/).
|
||||||
|
|
||||||
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-server-formats).
|
For migration, see [Migrate to new DNS server formats](/migration/#migrate-to-new-dns-servers).
|
||||||
|
|
||||||
Compatibility for old formats will be removed in sing-box 1.14.0.
|
Compatibility for old formats will be removed in sing-box 1.14.0.
|
||||||
|
|
||||||
@@ -2132,7 +2033,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
|
|||||||
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
||||||
if using this method.
|
if using this method.
|
||||||
|
|
||||||
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
|
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
|
||||||
|
|
||||||
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||||
|
|
||||||
@@ -2146,7 +2047,7 @@ the [Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users
|
|||||||
**5**:
|
**5**:
|
||||||
|
|
||||||
The new feature allows you to cache the check results of
|
The new feature allows you to cache the check results of
|
||||||
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
|
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
|
||||||
|
|
||||||
**6**:
|
**6**:
|
||||||
|
|
||||||
@@ -2327,7 +2228,7 @@ See [TUN](/configuration/inbound/tun) inbound.
|
|||||||
**1**:
|
**1**:
|
||||||
|
|
||||||
The new feature allows you to cache the check results of
|
The new feature allows you to cache the check results of
|
||||||
[Legacy Address Filter Fields](/configuration/dns/rule/#legacy-address-filter-fields) until expiration.
|
[Address filter DNS rule items](/configuration/dns/rule/#address-filter-fields) until expiration.
|
||||||
|
|
||||||
#### 1.9.0-alpha.7
|
#### 1.9.0-alpha.7
|
||||||
|
|
||||||
@@ -2374,7 +2275,7 @@ See [Migration](/migration/#process_path-format-update-on-windows).
|
|||||||
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
The new DNS feature allows you to more precisely bypass Chinese websites via **DNS leaks**. Do not use plain local DNS
|
||||||
if using this method.
|
if using this method.
|
||||||
|
|
||||||
See [Legacy Address Filter Fields](/configuration/dns/rule#legacy-address-filter-fields).
|
See [Address Filter Fields](/configuration/dns/rule#address-filter-fields).
|
||||||
|
|
||||||
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
[Client example](/manual/proxy/client#traffic-bypass-usage-for-chinese-users) updated.
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
|
|||||||
| `process_path` | :material-close: | No permission |
|
| `process_path` | :material-close: | No permission |
|
||||||
| `process_path_regex` | :material-close: | No permission |
|
| `process_path_regex` | :material-close: | No permission |
|
||||||
| `package_name` | :material-check: | / |
|
| `package_name` | :material-check: | / |
|
||||||
| `package_name_regex` | :material-check: | / |
|
|
||||||
| `user` | :material-close: | Use `package_name` instead |
|
| `user` | :material-close: | Use `package_name` instead |
|
||||||
| `user_id` | :material-close: | Use `package_name` instead |
|
| `user_id` | :material-close: | Use `package_name` instead |
|
||||||
| `wifi_ssid` | :material-check: | Fine location permission required |
|
| `wifi_ssid` | :material-check: | Fine location permission required |
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension
|
|||||||
| `process_path` | :material-close: | No permission |
|
| `process_path` | :material-close: | No permission |
|
||||||
| `process_path_regex` | :material-close: | No permission |
|
| `process_path_regex` | :material-close: | No permission |
|
||||||
| `package_name` | :material-close: | / |
|
| `package_name` | :material-close: | / |
|
||||||
| `package_name_regex` | :material-close: | / |
|
|
||||||
| `user` | :material-close: | No permission |
|
| `user` | :material-close: | No permission |
|
||||||
| `user_id` | :material-close: | No permission |
|
| `user_id` | :material-close: | No permission |
|
||||||
| `wifi_ssid` | :material-alert: | Only supported on iOS |
|
| `wifi_ssid` | :material-alert: | Only supported on iOS |
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
icon: material/note-remove
|
icon: material/delete-clock
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! failure "Removed in sing-box 1.14.0"
|
!!! failure "Deprecated in sing-box 1.12.0"
|
||||||
|
|
||||||
Legacy fake-ip configuration is deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats).
|
Legacy fake-ip configuration is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
|
||||||
|
|
||||||
### Structure
|
### Structure
|
||||||
|
|
||||||
@@ -26,6 +26,6 @@ Enable FakeIP service.
|
|||||||
|
|
||||||
IPv4 address range for FakeIP.
|
IPv4 address range for FakeIP.
|
||||||
|
|
||||||
#### inet6_range
|
#### inet6_address
|
||||||
|
|
||||||
IPv6 address range for FakeIP.
|
IPv6 address range for FakeIP.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
icon: material/note-remove
|
icon: material/delete-clock
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.14.0 移除"
|
!!! failure "已在 sing-box 1.12.0 废弃"
|
||||||
|
|
||||||
旧的 fake-ip 配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
旧的 fake-ip 配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||||
|
|
||||||
### 结构
|
### 结构
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.14.0"
|
|
||||||
|
|
||||||
:material-delete-clock: [independent_cache](#independent_cache)
|
|
||||||
:material-plus: [optimistic](#optimistic)
|
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-decagram: [servers](#servers)
|
:material-decagram: [servers](#servers)
|
||||||
@@ -30,7 +25,6 @@ icon: material/alert-decagram
|
|||||||
"disable_expire": false,
|
"disable_expire": false,
|
||||||
"independent_cache": false,
|
"independent_cache": false,
|
||||||
"cache_capacity": 0,
|
"cache_capacity": 0,
|
||||||
"optimistic": false, // or {}
|
|
||||||
"reverse_mapping": false,
|
"reverse_mapping": false,
|
||||||
"client_subnet": "",
|
"client_subnet": "",
|
||||||
"fakeip": {}
|
"fakeip": {}
|
||||||
@@ -45,7 +39,7 @@ icon: material/alert-decagram
|
|||||||
|----------|---------------------------------|
|
|----------|---------------------------------|
|
||||||
| `server` | List of [DNS Server](./server/) |
|
| `server` | List of [DNS Server](./server/) |
|
||||||
| `rules` | List of [DNS Rule](./rule/) |
|
| `rules` | List of [DNS Rule](./rule/) |
|
||||||
| `fakeip` | :material-note-remove: [FakeIP](./fakeip/) |
|
| `fakeip` | [FakeIP](./fakeip/) |
|
||||||
|
|
||||||
#### final
|
#### final
|
||||||
|
|
||||||
@@ -63,20 +57,12 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
|||||||
|
|
||||||
Disable dns cache.
|
Disable dns cache.
|
||||||
|
|
||||||
Conflict with `optimistic`.
|
|
||||||
|
|
||||||
#### disable_expire
|
#### disable_expire
|
||||||
|
|
||||||
Disable dns cache expire.
|
Disable dns cache expire.
|
||||||
|
|
||||||
Conflict with `optimistic`.
|
|
||||||
|
|
||||||
#### independent_cache
|
#### independent_cache
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.14.0"
|
|
||||||
|
|
||||||
`independent_cache` is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-independent-dns-cache).
|
|
||||||
|
|
||||||
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
|
Make each DNS server's cache independent for special purposes. If enabled, will slightly degrade performance.
|
||||||
|
|
||||||
#### cache_capacity
|
#### cache_capacity
|
||||||
@@ -87,34 +73,6 @@ LRU cache capacity.
|
|||||||
|
|
||||||
Value less than 1024 will be ignored.
|
Value less than 1024 will be ignored.
|
||||||
|
|
||||||
#### optimistic
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.14.0"
|
|
||||||
|
|
||||||
Enable optimistic DNS caching. When a cached DNS entry has expired but is still within the timeout window,
|
|
||||||
the stale response is returned immediately while a background refresh is triggered.
|
|
||||||
|
|
||||||
Conflict with `disable_cache` and `disable_expire`.
|
|
||||||
|
|
||||||
Accepts a boolean or an object. When set to `true`, the default timeout of `3d` is used.
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"timeout": "3d"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### enabled
|
|
||||||
|
|
||||||
Enable optimistic DNS caching.
|
|
||||||
|
|
||||||
##### timeout
|
|
||||||
|
|
||||||
The maximum time an expired cache entry can be served optimistically.
|
|
||||||
|
|
||||||
`3d` is used by default.
|
|
||||||
|
|
||||||
#### reverse_mapping
|
#### reverse_mapping
|
||||||
|
|
||||||
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
|
Stores a reverse mapping of IP addresses after responding to a DNS query in order to provide domain names when routing.
|
||||||
@@ -130,4 +88,4 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
|||||||
|
|
||||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||||
|
|
||||||
Can be overridden by `servers.[].client_subnet` or `rules.[].client_subnet`.
|
Can be overrides by `servers.[].client_subnet` or `rules.[].client_subnet`.
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
icon: material/alert-decagram
|
icon: material/alert-decagram
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "sing-box 1.14.0 中的更改"
|
|
||||||
|
|
||||||
:material-delete-clock: [independent_cache](#independent_cache)
|
|
||||||
:material-plus: [optimistic](#optimistic)
|
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.0 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-decagram: [servers](#servers)
|
:material-decagram: [servers](#servers)
|
||||||
@@ -30,7 +25,6 @@ icon: material/alert-decagram
|
|||||||
"disable_expire": false,
|
"disable_expire": false,
|
||||||
"independent_cache": false,
|
"independent_cache": false,
|
||||||
"cache_capacity": 0,
|
"cache_capacity": 0,
|
||||||
"optimistic": false, // or {}
|
|
||||||
"reverse_mapping": false,
|
"reverse_mapping": false,
|
||||||
"client_subnet": "",
|
"client_subnet": "",
|
||||||
"fakeip": {}
|
"fakeip": {}
|
||||||
@@ -62,20 +56,12 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
禁用 DNS 缓存。
|
禁用 DNS 缓存。
|
||||||
|
|
||||||
与 `optimistic` 冲突。
|
|
||||||
|
|
||||||
#### disable_expire
|
#### disable_expire
|
||||||
|
|
||||||
禁用 DNS 缓存过期。
|
禁用 DNS 缓存过期。
|
||||||
|
|
||||||
与 `optimistic` 冲突。
|
|
||||||
|
|
||||||
#### independent_cache
|
#### independent_cache
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
|
||||||
|
|
||||||
`independent_cache` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除,参阅[迁移指南](/zh/migration/#迁移-independent-dns-cache)。
|
|
||||||
|
|
||||||
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
|
使每个 DNS 服务器的缓存独立,以满足特殊目的。如果启用,将轻微降低性能。
|
||||||
|
|
||||||
#### cache_capacity
|
#### cache_capacity
|
||||||
@@ -86,34 +72,6 @@ LRU 缓存容量。
|
|||||||
|
|
||||||
小于 1024 的值将被忽略。
|
小于 1024 的值将被忽略。
|
||||||
|
|
||||||
#### optimistic
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.14.0 起"
|
|
||||||
|
|
||||||
启用乐观 DNS 缓存。当缓存的 DNS 条目已过期但仍在超时窗口内时,
|
|
||||||
立即返回过期的响应,同时在后台触发刷新。
|
|
||||||
|
|
||||||
与 `disable_cache` 和 `disable_expire` 冲突。
|
|
||||||
|
|
||||||
接受布尔值或对象。当设置为 `true` 时,使用默认超时 `3d`。
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"enabled": true,
|
|
||||||
"timeout": "3d"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### enabled
|
|
||||||
|
|
||||||
启用乐观 DNS 缓存。
|
|
||||||
|
|
||||||
##### timeout
|
|
||||||
|
|
||||||
过期缓存条目可被乐观提供的最长时间。
|
|
||||||
|
|
||||||
默认使用 `3d`。
|
|
||||||
|
|
||||||
#### reverse_mapping
|
#### reverse_mapping
|
||||||
|
|
||||||
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
|
在响应 DNS 查询后存储 IP 地址的反向映射以为路由目的提供域名。
|
||||||
@@ -130,6 +88,6 @@ LRU 缓存容量。
|
|||||||
|
|
||||||
可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。
|
可以被 `servers.[].client_subnet` 或 `rules.[].client_subnet` 覆盖。
|
||||||
|
|
||||||
#### fakeip :material-note-remove:
|
#### fakeip
|
||||||
|
|
||||||
[FakeIP](./fakeip/) 设置。
|
[FakeIP](./fakeip/) 设置。
|
||||||
|
|||||||
@@ -6,15 +6,6 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
:material-plus: [source_mac_address](#source_mac_address)
|
:material-plus: [source_mac_address](#source_mac_address)
|
||||||
:material-plus: [source_hostname](#source_hostname)
|
:material-plus: [source_hostname](#source_hostname)
|
||||||
:material-plus: [match_response](#match_response)
|
|
||||||
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
|
|
||||||
:material-plus: [response_rcode](#response_rcode)
|
|
||||||
:material-plus: [response_answer](#response_answer)
|
|
||||||
:material-plus: [response_ns](#response_ns)
|
|
||||||
:material-plus: [response_extra](#response_extra)
|
|
||||||
:material-plus: [package_name_regex](#package_name_regex)
|
|
||||||
:material-alert: [ip_version](#ip_version)
|
|
||||||
:material-alert: [query_type](#query_type)
|
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.13.0"
|
!!! quote "Changes in sing-box 1.13.0"
|
||||||
|
|
||||||
@@ -103,6 +94,12 @@ icon: material/alert-decagram
|
|||||||
"192.168.0.1"
|
"192.168.0.1"
|
||||||
],
|
],
|
||||||
"source_ip_is_private": false,
|
"source_ip_is_private": false,
|
||||||
|
"ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"ip_is_private": false,
|
||||||
|
"ip_accept_any": false,
|
||||||
"source_port": [
|
"source_port": [
|
||||||
12345
|
12345
|
||||||
],
|
],
|
||||||
@@ -132,9 +129,6 @@ icon: material/alert-decagram
|
|||||||
"package_name": [
|
"package_name": [
|
||||||
"com.termux"
|
"com.termux"
|
||||||
],
|
],
|
||||||
"package_name_regex": [
|
|
||||||
"^com\\.termux.*"
|
|
||||||
],
|
|
||||||
"user": [
|
"user": [
|
||||||
"sekai"
|
"sekai"
|
||||||
],
|
],
|
||||||
@@ -177,17 +171,7 @@ icon: material/alert-decagram
|
|||||||
"geosite-cn"
|
"geosite-cn"
|
||||||
],
|
],
|
||||||
"rule_set_ip_cidr_match_source": false,
|
"rule_set_ip_cidr_match_source": false,
|
||||||
"match_response": false,
|
"rule_set_ip_cidr_accept_empty": false,
|
||||||
"ip_cidr": [
|
|
||||||
"10.0.0.0/24",
|
|
||||||
"192.168.0.1"
|
|
||||||
],
|
|
||||||
"ip_is_private": false,
|
|
||||||
"ip_accept_any": false,
|
|
||||||
"response_rcode": "",
|
|
||||||
"response_answer": [],
|
|
||||||
"response_ns": [],
|
|
||||||
"response_extra": [],
|
|
||||||
"invert": false,
|
"invert": false,
|
||||||
"outbound": [
|
"outbound": [
|
||||||
"direct"
|
"direct"
|
||||||
@@ -197,7 +181,6 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
// Deprecated
|
// Deprecated
|
||||||
|
|
||||||
"rule_set_ip_cidr_accept_empty": false,
|
|
||||||
"rule_set_ipcidr_match_source": false,
|
"rule_set_ipcidr_match_source": false,
|
||||||
"geosite": [
|
"geosite": [
|
||||||
"cn"
|
"cn"
|
||||||
@@ -237,7 +220,7 @@ icon: material/alert-decagram
|
|||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`other fields`
|
||||||
|
|
||||||
Additionally, each branch inside an included rule-set can be considered merged into the outer rule, while different branches keep OR semantics.
|
Additionally, included rule-sets can be considered merged rather than as a single rule sub-item.
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
@@ -245,46 +228,12 @@ Tags of [Inbound](/configuration/inbound/).
|
|||||||
|
|
||||||
#### ip_version
|
#### ip_version
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.14.0"
|
|
||||||
|
|
||||||
This field now also applies when a DNS rule is matched from an internal
|
|
||||||
domain resolution that does not target a specific DNS server, such as a
|
|
||||||
[`resolve`](../../route/rule_action/#resolve) route rule action without a
|
|
||||||
`server` set. In earlier versions, only DNS queries received from a
|
|
||||||
client evaluated this field. See
|
|
||||||
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
|
|
||||||
for the full list.
|
|
||||||
|
|
||||||
Setting this field makes the DNS rule incompatible in the same DNS
|
|
||||||
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
|
|
||||||
`strategy` DNS rule action option, and the Legacy
|
|
||||||
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
|
|
||||||
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
|
|
||||||
action and [`match_response`](#match_response).
|
|
||||||
|
|
||||||
4 (A DNS query) or 6 (AAAA DNS query).
|
4 (A DNS query) or 6 (AAAA DNS query).
|
||||||
|
|
||||||
Not limited if empty.
|
Not limited if empty.
|
||||||
|
|
||||||
#### query_type
|
#### query_type
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.14.0"
|
|
||||||
|
|
||||||
This field now also applies when a DNS rule is matched from an internal
|
|
||||||
domain resolution that does not target a specific DNS server, such as a
|
|
||||||
[`resolve`](../../route/rule_action/#resolve) route rule action without a
|
|
||||||
`server` set. In earlier versions, only DNS queries received from a
|
|
||||||
client evaluated this field. See
|
|
||||||
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
|
|
||||||
for the full list.
|
|
||||||
|
|
||||||
Setting this field makes the DNS rule incompatible in the same DNS
|
|
||||||
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
|
|
||||||
`strategy` DNS rule action option, and the Legacy
|
|
||||||
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
|
|
||||||
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
|
|
||||||
action and [`match_response`](#match_response).
|
|
||||||
|
|
||||||
DNS query type. Values can be integers or type name strings.
|
DNS query type. Values can be integers or type name strings.
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
@@ -387,12 +336,6 @@ Match process path using regular expression.
|
|||||||
|
|
||||||
Match android package name.
|
Match android package name.
|
||||||
|
|
||||||
#### package_name_regex
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.14.0"
|
|
||||||
|
|
||||||
Match android package name using regular expression.
|
|
||||||
|
|
||||||
#### user
|
#### user
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
@@ -534,25 +477,6 @@ Make `ip_cidr` rule items in rule-sets match the source IP.
|
|||||||
|
|
||||||
Make `ip_cidr` rule items in rule-sets match the source IP.
|
Make `ip_cidr` rule items in rule-sets match the source IP.
|
||||||
|
|
||||||
#### match_response
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.14.0"
|
|
||||||
|
|
||||||
Enable response-based matching. When enabled, this rule matches against the evaluated response
|
|
||||||
(set by a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action)
|
|
||||||
instead of only matching the original query.
|
|
||||||
|
|
||||||
The evaluated response can also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
|
|
||||||
|
|
||||||
Required for Response Match Fields (`response_rcode`, `response_answer`, `response_ns`, `response_extra`).
|
|
||||||
Also required for `ip_cidr`, `ip_is_private`, and `ip_accept_any` when used with `evaluate` or Response Match Fields.
|
|
||||||
|
|
||||||
#### ip_accept_any
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
|
||||||
|
|
||||||
Match when the DNS query response contains at least one address.
|
|
||||||
|
|
||||||
#### invert
|
#### invert
|
||||||
|
|
||||||
Invert match result.
|
Invert match result.
|
||||||
@@ -597,12 +521,7 @@ See [DNS Rule Actions](../rule_action/) for details.
|
|||||||
|
|
||||||
Moved to [DNS Rule Action](../rule_action#route).
|
Moved to [DNS Rule Action](../rule_action#route).
|
||||||
|
|
||||||
### Legacy Address Filter Fields
|
### Address Filter Fields
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.14.0"
|
|
||||||
|
|
||||||
Legacy Address Filter Fields are deprecated and will be removed in sing-box 1.16.0,
|
|
||||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
|
||||||
|
|
||||||
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
|
Only takes effect for address requests (A/AAAA/HTTPS). When the query results do not match the address filtering rule items, the current rule will be skipped.
|
||||||
|
|
||||||
@@ -628,61 +547,23 @@ Match GeoIP with query response.
|
|||||||
|
|
||||||
Match IP CIDR with query response.
|
Match IP CIDR with query response.
|
||||||
|
|
||||||
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
|
|
||||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
|
||||||
|
|
||||||
#### ip_is_private
|
#### ip_is_private
|
||||||
|
|
||||||
!!! question "Since sing-box 1.9.0"
|
!!! question "Since sing-box 1.9.0"
|
||||||
|
|
||||||
Match private IP with query response.
|
Match private IP with query response.
|
||||||
|
|
||||||
As a Legacy Address Filter Field, deprecated. Use with `match_response` instead,
|
|
||||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
|
||||||
|
|
||||||
#### rule_set_ip_cidr_accept_empty
|
#### rule_set_ip_cidr_accept_empty
|
||||||
|
|
||||||
!!! question "Since sing-box 1.10.0"
|
!!! question "Since sing-box 1.10.0"
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.14.0"
|
|
||||||
|
|
||||||
`rule_set_ip_cidr_accept_empty` is deprecated and will be removed in sing-box 1.16.0,
|
|
||||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
|
||||||
|
|
||||||
Make `ip_cidr` rules in rule-sets accept empty query response.
|
Make `ip_cidr` rules in rule-sets accept empty query response.
|
||||||
|
|
||||||
### Response Match Fields
|
#### ip_accept_any
|
||||||
|
|
||||||
!!! question "Since sing-box 1.14.0"
|
!!! question "Since sing-box 1.12.0"
|
||||||
|
|
||||||
Match fields for the evaluated response. Require `match_response` to be set to `true`
|
Match any IP with query response.
|
||||||
and a preceding rule with [`evaluate`](/configuration/dns/rule_action/#evaluate) action to populate the response.
|
|
||||||
|
|
||||||
That evaluated response may also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
|
|
||||||
|
|
||||||
#### response_rcode
|
|
||||||
|
|
||||||
Match DNS response code.
|
|
||||||
|
|
||||||
Accepted values are the same as in the [predefined action rcode](/configuration/dns/rule_action/#rcode).
|
|
||||||
|
|
||||||
#### response_answer
|
|
||||||
|
|
||||||
Match DNS answer records.
|
|
||||||
|
|
||||||
Record format is the same as in [predefined action answer](/configuration/dns/rule_action/#answer).
|
|
||||||
|
|
||||||
#### response_ns
|
|
||||||
|
|
||||||
Match DNS name server records.
|
|
||||||
|
|
||||||
Record format is the same as in [predefined action ns](/configuration/dns/rule_action/#ns).
|
|
||||||
|
|
||||||
#### response_extra
|
|
||||||
|
|
||||||
Match DNS extra records.
|
|
||||||
|
|
||||||
Record format is the same as in [predefined action extra](/configuration/dns/rule_action/#extra).
|
|
||||||
|
|
||||||
### Logical Fields
|
### Logical Fields
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,6 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
:material-plus: [source_mac_address](#source_mac_address)
|
:material-plus: [source_mac_address](#source_mac_address)
|
||||||
:material-plus: [source_hostname](#source_hostname)
|
:material-plus: [source_hostname](#source_hostname)
|
||||||
:material-plus: [match_response](#match_response)
|
|
||||||
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
|
|
||||||
:material-plus: [response_rcode](#response_rcode)
|
|
||||||
:material-plus: [response_answer](#response_answer)
|
|
||||||
:material-plus: [response_ns](#response_ns)
|
|
||||||
:material-plus: [response_extra](#response_extra)
|
|
||||||
:material-plus: [package_name_regex](#package_name_regex)
|
|
||||||
:material-alert: [ip_version](#ip_version)
|
|
||||||
:material-alert: [query_type](#query_type)
|
|
||||||
|
|
||||||
!!! quote "sing-box 1.13.0 中的更改"
|
!!! quote "sing-box 1.13.0 中的更改"
|
||||||
|
|
||||||
@@ -103,6 +94,12 @@ icon: material/alert-decagram
|
|||||||
"192.168.0.1"
|
"192.168.0.1"
|
||||||
],
|
],
|
||||||
"source_ip_is_private": false,
|
"source_ip_is_private": false,
|
||||||
|
"ip_cidr": [
|
||||||
|
"10.0.0.0/24",
|
||||||
|
"192.168.0.1"
|
||||||
|
],
|
||||||
|
"ip_is_private": false,
|
||||||
|
"ip_accept_any": false,
|
||||||
"source_port": [
|
"source_port": [
|
||||||
12345
|
12345
|
||||||
],
|
],
|
||||||
@@ -132,9 +129,6 @@ icon: material/alert-decagram
|
|||||||
"package_name": [
|
"package_name": [
|
||||||
"com.termux"
|
"com.termux"
|
||||||
],
|
],
|
||||||
"package_name_regex": [
|
|
||||||
"^com\\.termux.*"
|
|
||||||
],
|
|
||||||
"user": [
|
"user": [
|
||||||
"sekai"
|
"sekai"
|
||||||
],
|
],
|
||||||
@@ -177,17 +171,7 @@ icon: material/alert-decagram
|
|||||||
"geosite-cn"
|
"geosite-cn"
|
||||||
],
|
],
|
||||||
"rule_set_ip_cidr_match_source": false,
|
"rule_set_ip_cidr_match_source": false,
|
||||||
"match_response": false,
|
"rule_set_ip_cidr_accept_empty": false,
|
||||||
"ip_cidr": [
|
|
||||||
"10.0.0.0/24",
|
|
||||||
"192.168.0.1"
|
|
||||||
],
|
|
||||||
"ip_is_private": false,
|
|
||||||
"ip_accept_any": false,
|
|
||||||
"response_rcode": "",
|
|
||||||
"response_answer": [],
|
|
||||||
"response_ns": [],
|
|
||||||
"response_extra": [],
|
|
||||||
"invert": false,
|
"invert": false,
|
||||||
"outbound": [
|
"outbound": [
|
||||||
"direct"
|
"direct"
|
||||||
@@ -196,8 +180,6 @@ icon: material/alert-decagram
|
|||||||
"server": "local",
|
"server": "local",
|
||||||
|
|
||||||
// 已弃用
|
// 已弃用
|
||||||
|
|
||||||
"rule_set_ip_cidr_accept_empty": false,
|
|
||||||
"rule_set_ipcidr_match_source": false,
|
"rule_set_ipcidr_match_source": false,
|
||||||
"geosite": [
|
"geosite": [
|
||||||
"cn"
|
"cn"
|
||||||
@@ -237,7 +219,7 @@ icon: material/alert-decagram
|
|||||||
(`source_port` || `source_port_range`) &&
|
(`source_port` || `source_port_range`) &&
|
||||||
`other fields`
|
`other fields`
|
||||||
|
|
||||||
另外,引用规则集中的每个分支都可视为与外层规则合并,不同分支之间仍保持 OR 语义。
|
另外,引用的规则集可视为被合并,而不是作为一个单独的规则子项。
|
||||||
|
|
||||||
#### inbound
|
#### inbound
|
||||||
|
|
||||||
@@ -245,38 +227,12 @@ icon: material/alert-decagram
|
|||||||
|
|
||||||
#### ip_version
|
#### ip_version
|
||||||
|
|
||||||
!!! quote "sing-box 1.14.0 中的更改"
|
|
||||||
|
|
||||||
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
|
|
||||||
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
|
|
||||||
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
|
|
||||||
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
|
|
||||||
|
|
||||||
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
|
|
||||||
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
|
|
||||||
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
|
|
||||||
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
|
|
||||||
[`match_response`](#match_response)。
|
|
||||||
|
|
||||||
4 (A DNS 查询) 或 6 (AAAA DNS 查询)。
|
4 (A DNS 查询) 或 6 (AAAA DNS 查询)。
|
||||||
|
|
||||||
默认不限制。
|
默认不限制。
|
||||||
|
|
||||||
#### query_type
|
#### query_type
|
||||||
|
|
||||||
!!! quote "sing-box 1.14.0 中的更改"
|
|
||||||
|
|
||||||
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
|
|
||||||
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
|
|
||||||
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
|
|
||||||
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
|
|
||||||
|
|
||||||
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
|
|
||||||
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
|
|
||||||
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
|
|
||||||
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
|
|
||||||
[`match_response`](#match_response)。
|
|
||||||
|
|
||||||
DNS 查询类型。值可以为整数或者类型名称字符串。
|
DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||||
|
|
||||||
#### network
|
#### network
|
||||||
@@ -379,12 +335,6 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
|||||||
|
|
||||||
匹配 Android 应用包名。
|
匹配 Android 应用包名。
|
||||||
|
|
||||||
#### package_name_regex
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.14.0 起"
|
|
||||||
|
|
||||||
使用正则表达式匹配 Android 应用包名。
|
|
||||||
|
|
||||||
#### user
|
#### user
|
||||||
|
|
||||||
!!! quote ""
|
!!! quote ""
|
||||||
@@ -526,23 +476,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
使规则集中的 `ip_cidr` 规则匹配源 IP。
|
使规则集中的 `ip_cidr` 规则匹配源 IP。
|
||||||
|
|
||||||
#### match_response
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.14.0 起"
|
|
||||||
|
|
||||||
启用响应匹配。启用后,此规则将匹配已评估的响应(由前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作设置),而不仅是匹配原始查询。
|
|
||||||
|
|
||||||
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
|
|
||||||
|
|
||||||
响应匹配字段(`response_rcode`、`response_answer`、`response_ns`、`response_extra`)需要此选项。
|
|
||||||
当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr`、`ip_is_private` 和 `ip_accept_any` 也需要此选项。
|
|
||||||
|
|
||||||
#### ip_accept_any
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
|
||||||
|
|
||||||
当 DNS 查询响应包含至少一个地址时匹配。
|
|
||||||
|
|
||||||
#### invert
|
#### invert
|
||||||
|
|
||||||
反选匹配结果。
|
反选匹配结果。
|
||||||
@@ -587,12 +520,7 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
已移动到 [DNS 规则动作](../rule_action#route).
|
已移动到 [DNS 规则动作](../rule_action#route).
|
||||||
|
|
||||||
### 旧版地址筛选字段
|
### 地址筛选字段
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
|
||||||
|
|
||||||
旧版地址筛选字段已废弃,且将在 sing-box 1.16.0 中被移除,
|
|
||||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
|
||||||
|
|
||||||
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
|
仅对地址请求 (A/AAAA/HTTPS) 生效。 当查询结果与地址筛选规则项不匹配时,将跳过当前规则。
|
||||||
|
|
||||||
@@ -619,62 +547,24 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
|||||||
|
|
||||||
与查询响应匹配 IP CIDR。
|
与查询响应匹配 IP CIDR。
|
||||||
|
|
||||||
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
|
|
||||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
|
||||||
|
|
||||||
#### ip_is_private
|
#### ip_is_private
|
||||||
|
|
||||||
!!! question "自 sing-box 1.9.0 起"
|
!!! question "自 sing-box 1.9.0 起"
|
||||||
|
|
||||||
与查询响应匹配非公开 IP。
|
与查询响应匹配非公开 IP。
|
||||||
|
|
||||||
作为旧版地址筛选字段已废弃。请改为配合 `match_response` 使用,
|
#### ip_accept_any
|
||||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
|
||||||
|
!!! question "自 sing-box 1.12.0 起"
|
||||||
|
|
||||||
|
匹配任意 IP。
|
||||||
|
|
||||||
#### rule_set_ip_cidr_accept_empty
|
#### rule_set_ip_cidr_accept_empty
|
||||||
|
|
||||||
!!! question "自 sing-box 1.10.0 起"
|
!!! question "自 sing-box 1.10.0 起"
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
|
||||||
|
|
||||||
`rule_set_ip_cidr_accept_empty` 已废弃且将在 sing-box 1.16.0 中被移除,
|
|
||||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
|
||||||
|
|
||||||
使规则集中的 `ip_cidr` 规则接受空查询响应。
|
使规则集中的 `ip_cidr` 规则接受空查询响应。
|
||||||
|
|
||||||
### 响应匹配字段
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.14.0 起"
|
|
||||||
|
|
||||||
已评估的响应的匹配字段。需要将 `match_response` 设为 `true`,
|
|
||||||
且需要前序规则使用 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作来填充响应。
|
|
||||||
|
|
||||||
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
|
|
||||||
|
|
||||||
#### response_rcode
|
|
||||||
|
|
||||||
匹配 DNS 响应码。
|
|
||||||
|
|
||||||
接受的值与 [predefined 动作 rcode](/zh/configuration/dns/rule_action/#rcode) 中相同。
|
|
||||||
|
|
||||||
#### response_answer
|
|
||||||
|
|
||||||
匹配 DNS 应答记录。
|
|
||||||
|
|
||||||
记录格式与 [predefined 动作 answer](/zh/configuration/dns/rule_action/#answer) 中相同。
|
|
||||||
|
|
||||||
#### response_ns
|
|
||||||
|
|
||||||
匹配 DNS 名称服务器记录。
|
|
||||||
|
|
||||||
记录格式与 [predefined 动作 ns](/zh/configuration/dns/rule_action/#ns) 中相同。
|
|
||||||
|
|
||||||
#### response_extra
|
|
||||||
|
|
||||||
匹配 DNS 额外记录。
|
|
||||||
|
|
||||||
记录格式与 [predefined 动作 extra](/zh/configuration/dns/rule_action/#extra) 中相同。
|
|
||||||
|
|
||||||
### 逻辑字段
|
### 逻辑字段
|
||||||
|
|
||||||
#### type
|
#### type
|
||||||
|
|||||||
@@ -2,13 +2,6 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.14.0"
|
|
||||||
|
|
||||||
:material-delete-clock: [strategy](#strategy)
|
|
||||||
:material-plus: [evaluate](#evaluate)
|
|
||||||
:material-plus: [respond](#respond)
|
|
||||||
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.12.0"
|
!!! quote "Changes in sing-box 1.12.0"
|
||||||
|
|
||||||
:material-plus: [strategy](#strategy)
|
:material-plus: [strategy](#strategy)
|
||||||
@@ -24,7 +17,6 @@ icon: material/new-box
|
|||||||
"server": "",
|
"server": "",
|
||||||
"strategy": "",
|
"strategy": "",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
"disable_optimistic_cache": false,
|
|
||||||
"rewrite_ttl": null,
|
"rewrite_ttl": null,
|
||||||
"client_subnet": null
|
"client_subnet": null
|
||||||
}
|
}
|
||||||
@@ -42,10 +34,6 @@ Tag of target server.
|
|||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
!!! question "Since sing-box 1.12.0"
|
||||||
|
|
||||||
!!! failure "Deprecated in sing-box 1.14.0"
|
|
||||||
|
|
||||||
`strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0.
|
|
||||||
|
|
||||||
Set domain strategy for this query.
|
Set domain strategy for this query.
|
||||||
|
|
||||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||||
@@ -54,12 +42,6 @@ One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
|||||||
|
|
||||||
Disable cache and save cache in this query.
|
Disable cache and save cache in this query.
|
||||||
|
|
||||||
#### disable_optimistic_cache
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.14.0"
|
|
||||||
|
|
||||||
Disable optimistic DNS caching in this query.
|
|
||||||
|
|
||||||
#### rewrite_ttl
|
#### rewrite_ttl
|
||||||
|
|
||||||
Rewrite TTL in DNS responses.
|
Rewrite TTL in DNS responses.
|
||||||
@@ -70,75 +52,7 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
|||||||
|
|
||||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||||
|
|
||||||
Will override `dns.client_subnet`.
|
Will overrides `dns.client_subnet`.
|
||||||
|
|
||||||
### evaluate
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.14.0"
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"action": "evaluate",
|
|
||||||
"server": "",
|
|
||||||
"disable_cache": false,
|
|
||||||
"disable_optimistic_cache": false,
|
|
||||||
"rewrite_ttl": null,
|
|
||||||
"client_subnet": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`evaluate` sends a DNS query to the specified server and saves the evaluated response for subsequent rules
|
|
||||||
to match against using [`match_response`](/configuration/dns/rule/#match_response) and response fields.
|
|
||||||
Unlike `route`, it does **not** terminate rule evaluation.
|
|
||||||
|
|
||||||
Only allowed on top-level DNS rules (not inside logical sub-rules).
|
|
||||||
Rules that use [`match_response`](/configuration/dns/rule/#match_response) or Response Match Fields
|
|
||||||
require a preceding top-level rule with `evaluate` action. A rule's own `evaluate` action
|
|
||||||
does not satisfy this requirement, because matching happens before the action runs.
|
|
||||||
|
|
||||||
#### server
|
|
||||||
|
|
||||||
==Required==
|
|
||||||
|
|
||||||
Tag of target server.
|
|
||||||
|
|
||||||
#### disable_cache
|
|
||||||
|
|
||||||
Disable cache and save cache in this query.
|
|
||||||
|
|
||||||
#### disable_optimistic_cache
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.14.0"
|
|
||||||
|
|
||||||
Disable optimistic DNS caching in this query.
|
|
||||||
|
|
||||||
#### rewrite_ttl
|
|
||||||
|
|
||||||
Rewrite TTL in DNS responses.
|
|
||||||
|
|
||||||
#### client_subnet
|
|
||||||
|
|
||||||
Append a `edns0-subnet` OPT extra record with the specified IP prefix to every query by default.
|
|
||||||
|
|
||||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
|
||||||
|
|
||||||
Will override `dns.client_subnet`.
|
|
||||||
|
|
||||||
### respond
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.14.0"
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"action": "respond"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`respond` terminates rule evaluation and returns the evaluated response from a preceding [`evaluate`](/configuration/dns/rule_action/#evaluate) action.
|
|
||||||
|
|
||||||
This action does not send a new DNS query and has no extra options.
|
|
||||||
|
|
||||||
Only allowed after a preceding top-level `evaluate` rule. If the action is reached without an evaluated response at runtime, the request fails with an error instead of falling through to later rules.
|
|
||||||
|
|
||||||
### route-options
|
### route-options
|
||||||
|
|
||||||
@@ -146,7 +60,6 @@ Only allowed after a preceding top-level `evaluate` rule. If the action is reach
|
|||||||
{
|
{
|
||||||
"action": "route-options",
|
"action": "route-options",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
"disable_optimistic_cache": false,
|
|
||||||
"rewrite_ttl": null,
|
"rewrite_ttl": null,
|
||||||
"client_subnet": null
|
"client_subnet": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,6 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "sing-box 1.14.0 中的更改"
|
|
||||||
|
|
||||||
:material-delete-clock: [strategy](#strategy)
|
|
||||||
:material-plus: [evaluate](#evaluate)
|
|
||||||
:material-plus: [respond](#respond)
|
|
||||||
:material-plus: [disable_optimistic_cache](#disable_optimistic_cache)
|
|
||||||
|
|
||||||
!!! quote "sing-box 1.12.0 中的更改"
|
!!! quote "sing-box 1.12.0 中的更改"
|
||||||
|
|
||||||
:material-plus: [strategy](#strategy)
|
:material-plus: [strategy](#strategy)
|
||||||
@@ -24,7 +17,6 @@ icon: material/new-box
|
|||||||
"server": "",
|
"server": "",
|
||||||
"strategy": "",
|
"strategy": "",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
"disable_optimistic_cache": false,
|
|
||||||
"rewrite_ttl": null,
|
"rewrite_ttl": null,
|
||||||
"client_subnet": null
|
"client_subnet": null
|
||||||
}
|
}
|
||||||
@@ -42,10 +34,6 @@ icon: material/new-box
|
|||||||
|
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
!!! question "自 sing-box 1.12.0 起"
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
|
||||||
|
|
||||||
`strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。
|
|
||||||
|
|
||||||
为此查询设置域名策略。
|
为此查询设置域名策略。
|
||||||
|
|
||||||
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
||||||
@@ -54,12 +42,6 @@ icon: material/new-box
|
|||||||
|
|
||||||
在此查询中禁用缓存。
|
在此查询中禁用缓存。
|
||||||
|
|
||||||
#### disable_optimistic_cache
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.14.0 起"
|
|
||||||
|
|
||||||
在此查询中禁用乐观 DNS 缓存。
|
|
||||||
|
|
||||||
#### rewrite_ttl
|
#### rewrite_ttl
|
||||||
|
|
||||||
重写 DNS 回应中的 TTL。
|
重写 DNS 回应中的 TTL。
|
||||||
@@ -72,79 +54,12 @@ icon: material/new-box
|
|||||||
|
|
||||||
将覆盖 `dns.client_subnet`.
|
将覆盖 `dns.client_subnet`.
|
||||||
|
|
||||||
### evaluate
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.14.0 起"
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"action": "evaluate",
|
|
||||||
"server": "",
|
|
||||||
"disable_cache": false,
|
|
||||||
"disable_optimistic_cache": false,
|
|
||||||
"rewrite_ttl": null,
|
|
||||||
"client_subnet": null
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`evaluate` 向指定服务器发送 DNS 查询并保存已评估的响应,供后续规则通过 [`match_response`](/zh/configuration/dns/rule/#match_response) 和响应字段进行匹配。与 `route` 不同,它**不会**终止规则评估。
|
|
||||||
|
|
||||||
仅允许在顶层 DNS 规则中使用(不可在逻辑子规则内部使用)。
|
|
||||||
使用 [`match_response`](/zh/configuration/dns/rule/#match_response) 或响应匹配字段的规则,
|
|
||||||
需要位于更早的顶层 `evaluate` 规则之后。规则自身的 `evaluate` 动作不能满足这个条件,
|
|
||||||
因为匹配发生在动作执行之前。
|
|
||||||
|
|
||||||
#### server
|
|
||||||
|
|
||||||
==必填==
|
|
||||||
|
|
||||||
目标 DNS 服务器的标签。
|
|
||||||
|
|
||||||
#### disable_cache
|
|
||||||
|
|
||||||
在此查询中禁用缓存。
|
|
||||||
|
|
||||||
#### disable_optimistic_cache
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.14.0 起"
|
|
||||||
|
|
||||||
在此查询中禁用乐观 DNS 缓存。
|
|
||||||
|
|
||||||
#### rewrite_ttl
|
|
||||||
|
|
||||||
重写 DNS 回应中的 TTL。
|
|
||||||
|
|
||||||
#### client_subnet
|
|
||||||
|
|
||||||
默认情况下,将带有指定 IP 前缀的 `edns0-subnet` OPT 附加记录附加到每个查询。
|
|
||||||
|
|
||||||
如果值是 IP 地址而不是前缀,则会自动附加 `/32` 或 `/128`。
|
|
||||||
|
|
||||||
将覆盖 `dns.client_subnet`.
|
|
||||||
|
|
||||||
### respond
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.14.0 起"
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"action": "respond"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`respond` 会终止规则评估,并直接返回前序 [`evaluate`](/zh/configuration/dns/rule_action/#evaluate) 动作保存的已评估的响应。
|
|
||||||
|
|
||||||
此动作不会发起新的 DNS 查询,也没有额外选项。
|
|
||||||
|
|
||||||
只能用于前面已有顶层 `evaluate` 规则的场景。如果运行时命中该动作时没有已评估的响应,则请求会直接返回错误,而不是继续匹配后续规则。
|
|
||||||
|
|
||||||
### route-options
|
### route-options
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"action": "route-options",
|
"action": "route-options",
|
||||||
"disable_cache": false,
|
"disable_cache": false,
|
||||||
"disable_optimistic_cache": false,
|
|
||||||
"rewrite_ttl": null,
|
"rewrite_ttl": null,
|
||||||
"client_subnet": null
|
"client_subnet": null
|
||||||
}
|
}
|
||||||
@@ -169,7 +84,7 @@ icon: material/new-box
|
|||||||
- `default`: 返回 REFUSED。
|
- `default`: 返回 REFUSED。
|
||||||
- `drop`: 丢弃请求。
|
- `drop`: 丢弃请求。
|
||||||
|
|
||||||
默认使用 `default`。
|
默认使用 `defualt`。
|
||||||
|
|
||||||
#### no_drop
|
#### no_drop
|
||||||
|
|
||||||
|
|||||||
@@ -73,55 +73,24 @@ Example:
|
|||||||
|
|
||||||
=== "Use hosts if available"
|
=== "Use hosts if available"
|
||||||
|
|
||||||
=== ":material-card-multiple: sing-box 1.14.0"
|
```json
|
||||||
|
{
|
||||||
```json
|
"dns": {
|
||||||
{
|
"servers": [
|
||||||
"dns": {
|
{
|
||||||
"servers": [
|
...
|
||||||
{
|
},
|
||||||
...
|
{
|
||||||
},
|
"type": "hosts",
|
||||||
{
|
"tag": "hosts"
|
||||||
"type": "hosts",
|
|
||||||
"tag": "hosts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"action": "evaluate",
|
|
||||||
"server": "hosts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_response": true,
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"action": "respond"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
```
|
"rules": [
|
||||||
|
{
|
||||||
=== ":material-card-remove: sing-box < 1.14.0"
|
"ip_accept_any": true,
|
||||||
|
"server": "hosts"
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dns": {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
...
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "hosts",
|
|
||||||
"tag": "hosts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"server": "hosts"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
```
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -73,55 +73,24 @@ hosts 文件路径列表。
|
|||||||
|
|
||||||
=== "如果可用则使用 hosts"
|
=== "如果可用则使用 hosts"
|
||||||
|
|
||||||
=== ":material-card-multiple: sing-box 1.14.0"
|
```json
|
||||||
|
{
|
||||||
```json
|
"dns": {
|
||||||
{
|
"servers": [
|
||||||
"dns": {
|
{
|
||||||
"servers": [
|
...
|
||||||
{
|
},
|
||||||
...
|
{
|
||||||
},
|
"type": "hosts",
|
||||||
{
|
"tag": "hosts"
|
||||||
"type": "hosts",
|
|
||||||
"tag": "hosts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"action": "evaluate",
|
|
||||||
"server": "hosts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_response": true,
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"action": "respond"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
```
|
"rules": [
|
||||||
|
{
|
||||||
=== ":material-card-remove: sing-box < 1.14.0"
|
"ip_accept_any": true,
|
||||||
|
"server": "hosts"
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dns": {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
...
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "hosts",
|
|
||||||
"tag": "hosts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"server": "hosts"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
```
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
@@ -2,10 +2,6 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.14.0"
|
|
||||||
|
|
||||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
!!! question "Since sing-box 1.12.0"
|
||||||
|
|
||||||
# DNS over HTTP3 (DoH3)
|
# DNS over HTTP3 (DoH3)
|
||||||
@@ -21,18 +17,25 @@ icon: material/new-box
|
|||||||
"tag": "",
|
"tag": "",
|
||||||
|
|
||||||
"server": "",
|
"server": "",
|
||||||
"server_port": 0,
|
"server_port": 443,
|
||||||
|
|
||||||
"path": "",
|
"path": "",
|
||||||
"method": "",
|
"headers": {},
|
||||||
|
|
||||||
... // HTTP Client Fields
|
"tls": {},
|
||||||
|
|
||||||
|
// Dial Fields
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! info "Difference from legacy H3 server"
|
||||||
|
|
||||||
|
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||||
|
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
#### server
|
#### server
|
||||||
@@ -55,14 +58,14 @@ The path of the DNS server.
|
|||||||
|
|
||||||
`/dns-query` will be used by default.
|
`/dns-query` will be used by default.
|
||||||
|
|
||||||
#### method
|
#### headers
|
||||||
|
|
||||||
HTTP request method.
|
Additional headers to be sent to the DNS server.
|
||||||
|
|
||||||
Available values: `GET`, `POST`.
|
#### tls
|
||||||
|
|
||||||
`POST` will be used by default.
|
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||||
|
|
||||||
### HTTP Client Fields
|
### Dial Fields
|
||||||
|
|
||||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "sing-box 1.14.0 中的更改"
|
|
||||||
|
|
||||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
!!! question "自 sing-box 1.12.0 起"
|
||||||
|
|
||||||
# DNS over HTTP3 (DoH3)
|
# DNS over HTTP3 (DoH3)
|
||||||
@@ -21,18 +17,25 @@ icon: material/new-box
|
|||||||
"tag": "",
|
"tag": "",
|
||||||
|
|
||||||
"server": "",
|
"server": "",
|
||||||
"server_port": 0,
|
"server_port": 443,
|
||||||
|
|
||||||
"path": "",
|
"path": "",
|
||||||
"method": "",
|
"headers": {},
|
||||||
|
|
||||||
... // HTTP 客户端字段
|
"tls": {},
|
||||||
|
|
||||||
|
// 拨号字段
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! info "与旧版 H3 服务器的区别"
|
||||||
|
|
||||||
|
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||||
|
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||||
|
|
||||||
### 字段
|
### 字段
|
||||||
|
|
||||||
#### server
|
#### server
|
||||||
@@ -55,14 +58,14 @@ DNS 服务器的路径。
|
|||||||
|
|
||||||
默认使用 `/dns-query`。
|
默认使用 `/dns-query`。
|
||||||
|
|
||||||
#### method
|
#### headers
|
||||||
|
|
||||||
HTTP 请求方法。
|
发送到 DNS 服务器的额外标头。
|
||||||
|
|
||||||
可用值:`GET`、`POST`。
|
#### tls
|
||||||
|
|
||||||
默认使用 `POST`。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### HTTP 客户端字段
|
### 拨号字段
|
||||||
|
|
||||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||||
@@ -2,10 +2,6 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.14.0"
|
|
||||||
|
|
||||||
:material-alert: `headers`, `tls`, Dial Fields moved to [HTTP Client Fields](#http-client-fields)
|
|
||||||
|
|
||||||
!!! question "Since sing-box 1.12.0"
|
!!! question "Since sing-box 1.12.0"
|
||||||
|
|
||||||
# DNS over HTTPS (DoH)
|
# DNS over HTTPS (DoH)
|
||||||
@@ -21,18 +17,25 @@ icon: material/new-box
|
|||||||
"tag": "",
|
"tag": "",
|
||||||
|
|
||||||
"server": "",
|
"server": "",
|
||||||
"server_port": 0,
|
"server_port": 443,
|
||||||
|
|
||||||
"path": "",
|
"path": "",
|
||||||
"method": "",
|
"headers": {},
|
||||||
|
|
||||||
... // HTTP Client Fields
|
"tls": {},
|
||||||
|
|
||||||
|
// Dial Fields
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! info "Difference from legacy HTTPS server"
|
||||||
|
|
||||||
|
* The old server uses default outbound by default unless detour is specified; the new one uses dialer just like outbound, which is equivalent to using an empty direct outbound by default.
|
||||||
|
* The old server uses `address_resolver` and `address_strategy` to resolve the domain name in the server; the new one uses `domain_resolver` and `domain_strategy` in [Dial Fields](/configuration/shared/dial/) instead.
|
||||||
|
|
||||||
### Fields
|
### Fields
|
||||||
|
|
||||||
#### server
|
#### server
|
||||||
@@ -55,14 +58,14 @@ The path of the DNS server.
|
|||||||
|
|
||||||
`/dns-query` will be used by default.
|
`/dns-query` will be used by default.
|
||||||
|
|
||||||
#### method
|
#### headers
|
||||||
|
|
||||||
HTTP request method.
|
Additional headers to be sent to the DNS server.
|
||||||
|
|
||||||
Available values: `GET`, `POST`.
|
#### tls
|
||||||
|
|
||||||
`POST` will be used by default.
|
TLS configuration, see [TLS](/configuration/shared/tls/#outbound).
|
||||||
|
|
||||||
### HTTP Client Fields
|
### Dial Fields
|
||||||
|
|
||||||
See [HTTP Client Fields](/configuration/shared/http-client/) for details.
|
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||||
|
|||||||
@@ -2,10 +2,6 @@
|
|||||||
icon: material/new-box
|
icon: material/new-box
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! quote "sing-box 1.14.0 中的更改"
|
|
||||||
|
|
||||||
:material-alert: `headers`、`tls`、拨号字段已移至 [HTTP 客户端字段](#http-客户端字段)
|
|
||||||
|
|
||||||
!!! question "自 sing-box 1.12.0 起"
|
!!! question "自 sing-box 1.12.0 起"
|
||||||
|
|
||||||
# DNS over HTTPS (DoH)
|
# DNS over HTTPS (DoH)
|
||||||
@@ -21,18 +17,25 @@ icon: material/new-box
|
|||||||
"tag": "",
|
"tag": "",
|
||||||
|
|
||||||
"server": "",
|
"server": "",
|
||||||
"server_port": 0,
|
"server_port": 443,
|
||||||
|
|
||||||
"path": "",
|
"path": "",
|
||||||
"method": "",
|
"headers": {},
|
||||||
|
|
||||||
... // HTTP 客户端字段
|
"tls": {},
|
||||||
|
|
||||||
|
// 拨号字段
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! info "与旧版 HTTPS 服务器的区别"
|
||||||
|
|
||||||
|
* 旧服务器默认使用默认出站,除非指定了绕行;新服务器像出站一样使用拨号器,相当于默认使用空的直连出站。
|
||||||
|
* 旧服务器使用 `address_resolver` 和 `address_strategy` 来解析服务器中的域名;新服务器改用 [拨号字段](/zh/configuration/shared/dial/) 中的 `domain_resolver` 和 `domain_strategy`。
|
||||||
|
|
||||||
### 字段
|
### 字段
|
||||||
|
|
||||||
#### server
|
#### server
|
||||||
@@ -55,14 +58,14 @@ DNS 服务器的路径。
|
|||||||
|
|
||||||
默认使用 `/dns-query`。
|
默认使用 `/dns-query`。
|
||||||
|
|
||||||
#### method
|
#### headers
|
||||||
|
|
||||||
HTTP 请求方法。
|
发送到 DNS 服务器的额外标头。
|
||||||
|
|
||||||
可用值:`GET`、`POST`。
|
#### tls
|
||||||
|
|
||||||
默认使用 `POST`。
|
TLS 配置,参阅 [TLS](/zh/configuration/shared/tls/#出站)。
|
||||||
|
|
||||||
### HTTP 客户端字段
|
### 拨号字段
|
||||||
|
|
||||||
参阅 [HTTP 客户端字段](/zh/configuration/shared/http-client/) 了解详情。
|
参阅 [拨号字段](/zh/configuration/shared/dial/) 了解详情。
|
||||||
@@ -29,7 +29,7 @@ The type of the DNS server.
|
|||||||
|
|
||||||
| Type | Format |
|
| Type | Format |
|
||||||
|-----------------|---------------------------|
|
|-----------------|---------------------------|
|
||||||
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
|
| empty (default) | [Legacy](./legacy/) |
|
||||||
| `local` | [Local](./local/) |
|
| `local` | [Local](./local/) |
|
||||||
| `hosts` | [Hosts](./hosts/) |
|
| `hosts` | [Hosts](./hosts/) |
|
||||||
| `tcp` | [TCP](./tcp/) |
|
| `tcp` | [TCP](./tcp/) |
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ DNS 服务器的类型。
|
|||||||
|
|
||||||
| 类型 | 格式 |
|
| 类型 | 格式 |
|
||||||
|-----------------|---------------------------|
|
|-----------------|---------------------------|
|
||||||
| empty (default) | :material-note-remove: [Legacy](./legacy/) |
|
| empty (default) | [Legacy](./legacy/) |
|
||||||
| `local` | [Local](./local/) |
|
| `local` | [Local](./local/) |
|
||||||
| `hosts` | [Hosts](./hosts/) |
|
| `hosts` | [Hosts](./hosts/) |
|
||||||
| `tcp` | [TCP](./tcp/) |
|
| `tcp` | [TCP](./tcp/) |
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
icon: material/note-remove
|
icon: material/delete-clock
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! failure "Removed in sing-box 1.14.0"
|
!!! failure "Deprecated in sing-box 1.12.0"
|
||||||
|
|
||||||
Legacy DNS servers are deprecated in sing-box 1.12.0 and removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-server-formats).
|
Legacy DNS servers is deprecated and will be removed in sing-box 1.14.0, check [Migration](/migration/#migrate-to-new-dns-servers).
|
||||||
|
|
||||||
!!! quote "Changes in sing-box 1.9.0"
|
!!! quote "Changes in sing-box 1.9.0"
|
||||||
|
|
||||||
@@ -108,6 +108,6 @@ Append a `edns0-subnet` OPT extra record with the specified IP prefix to every q
|
|||||||
|
|
||||||
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
If value is an IP address instead of prefix, `/32` or `/128` will be appended automatically.
|
||||||
|
|
||||||
Can be overridden by `rules.[].client_subnet`.
|
Can be overrides by `rules.[].client_subnet`.
|
||||||
|
|
||||||
Will override `dns.client_subnet`.
|
Will overrides `dns.client_subnet`.
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
icon: material/note-remove
|
icon: material/delete-clock
|
||||||
---
|
---
|
||||||
|
|
||||||
!!! failure "已在 sing-box 1.14.0 移除"
|
!!! failure "Deprecated in sing-box 1.12.0"
|
||||||
|
|
||||||
旧的 DNS 服务器配置已在 sing-box 1.12.0 废弃且已在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
旧的 DNS 服务器配置已废弃且将在 sing-box 1.14.0 中被移除,参阅 [迁移指南](/zh/migration/#迁移到新的-dns-服务器格式)。
|
||||||
|
|
||||||
!!! quote "sing-box 1.9.0 中的更改"
|
!!! quote "sing-box 1.9.0 中的更改"
|
||||||
|
|
||||||
|
|||||||
@@ -43,62 +43,29 @@ If not enabled, `NXDOMAIN` will be returned for requests that do not match searc
|
|||||||
|
|
||||||
=== "Split DNS only"
|
=== "Split DNS only"
|
||||||
|
|
||||||
=== ":material-card-multiple: sing-box 1.14.0"
|
```json
|
||||||
|
{
|
||||||
```json
|
"dns": {
|
||||||
{
|
"servers": [
|
||||||
"dns": {
|
{
|
||||||
"servers": [
|
"type": "local",
|
||||||
{
|
"tag": "local"
|
||||||
"type": "local",
|
},
|
||||||
"tag": "local"
|
{
|
||||||
},
|
"type": "resolved",
|
||||||
{
|
"tag": "resolved",
|
||||||
"type": "resolved",
|
"service": "resolved"
|
||||||
"tag": "resolved",
|
|
||||||
"service": "resolved"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"action": "evaluate",
|
|
||||||
"server": "resolved"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_response": true,
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"action": "respond"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
```
|
"rules": [
|
||||||
|
{
|
||||||
=== ":material-card-remove: sing-box < 1.14.0"
|
"ip_accept_any": true,
|
||||||
|
"server": "resolved"
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dns": {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"type": "local",
|
|
||||||
"tag": "local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "resolved",
|
|
||||||
"tag": "resolved",
|
|
||||||
"service": "resolved"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"server": "resolved"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
```
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
=== "Use as global DNS"
|
=== "Use as global DNS"
|
||||||
|
|
||||||
|
|||||||
@@ -42,62 +42,29 @@ icon: material/new-box
|
|||||||
|
|
||||||
=== "仅分割 DNS"
|
=== "仅分割 DNS"
|
||||||
|
|
||||||
=== ":material-card-multiple: sing-box 1.14.0"
|
```json
|
||||||
|
{
|
||||||
```json
|
"dns": {
|
||||||
{
|
"servers": [
|
||||||
"dns": {
|
{
|
||||||
"servers": [
|
"type": "local",
|
||||||
{
|
"tag": "local"
|
||||||
"type": "local",
|
},
|
||||||
"tag": "local"
|
{
|
||||||
},
|
"type": "resolved",
|
||||||
{
|
"tag": "resolved",
|
||||||
"type": "resolved",
|
"service": "resolved"
|
||||||
"tag": "resolved",
|
|
||||||
"service": "resolved"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"action": "evaluate",
|
|
||||||
"server": "resolved"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_response": true,
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"action": "respond"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
```
|
"rules": [
|
||||||
|
{
|
||||||
=== ":material-card-remove: sing-box < 1.14.0"
|
"ip_accept_any": true,
|
||||||
|
"server": "resolved"
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dns": {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"type": "local",
|
|
||||||
"tag": "local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "resolved",
|
|
||||||
"tag": "resolved",
|
|
||||||
"service": "resolved"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"server": "resolved"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
```
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
=== "用作全局 DNS"
|
=== "用作全局 DNS"
|
||||||
|
|
||||||
|
|||||||
@@ -42,62 +42,29 @@ if not enabled, `NXDOMAIN` will be returned for non-Tailscale domain queries.
|
|||||||
|
|
||||||
=== "MagicDNS only"
|
=== "MagicDNS only"
|
||||||
|
|
||||||
=== ":material-card-multiple: sing-box 1.14.0"
|
```json
|
||||||
|
{
|
||||||
```json
|
"dns": {
|
||||||
{
|
"servers": [
|
||||||
"dns": {
|
{
|
||||||
"servers": [
|
"type": "local",
|
||||||
{
|
"tag": "local"
|
||||||
"type": "local",
|
},
|
||||||
"tag": "local"
|
{
|
||||||
},
|
"type": "tailscale",
|
||||||
{
|
"tag": "ts",
|
||||||
"type": "tailscale",
|
"endpoint": "ts-ep"
|
||||||
"tag": "ts",
|
|
||||||
"endpoint": "ts-ep"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"action": "evaluate",
|
|
||||||
"server": "ts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"match_response": true,
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"action": "respond"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
],
|
||||||
```
|
"rules": [
|
||||||
|
{
|
||||||
=== ":material-card-remove: sing-box < 1.14.0"
|
"ip_accept_any": true,
|
||||||
|
"server": "ts"
|
||||||
```json
|
|
||||||
{
|
|
||||||
"dns": {
|
|
||||||
"servers": [
|
|
||||||
{
|
|
||||||
"type": "local",
|
|
||||||
"tag": "local"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "tailscale",
|
|
||||||
"tag": "ts",
|
|
||||||
"endpoint": "ts-ep"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"ip_accept_any": true,
|
|
||||||
"server": "ts"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
]
|
||||||
```
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
=== "Use as global DNS"
|
=== "Use as global DNS"
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user