mirror of
https://github.com/XTLS/Xray-core.git
synced 2026-11-06 10:12:40 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43a735dd12 | ||
|
|
bcce5ff100 | ||
|
|
f03c20db42 | ||
|
|
2dd075d0e2 |
@@ -9,8 +9,8 @@ ARG TARGETARCH
|
|||||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
|
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
|
||||||
|
|
||||||
# Download geodat into a staging directory
|
# Download geodat into a staging directory
|
||||||
ADD https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geoip-full.dat /tmp/geodat/geoip.dat
|
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat
|
||||||
ADD https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geosite-expand.dat /tmp/geodat/geosite.dat
|
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat
|
||||||
|
|
||||||
RUN mkdir -p /tmp/empty
|
RUN mkdir -p /tmp/empty
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ RUN cat <<EOF >/tmp/usr/local/etc/xray/00_log.json
|
|||||||
{
|
{
|
||||||
"log": {
|
"log": {
|
||||||
"error": "/var/log/xray/error.log",
|
"error": "/var/log/xray/error.log",
|
||||||
"logLevel": "warning",
|
"loglevel": "warning",
|
||||||
"access": "none",
|
"access": "none",
|
||||||
"dnsLog": false
|
"dnsLog": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ ARG TARGETARCH
|
|||||||
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
|
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 go build -o xray -trimpath -buildvcs=false -gcflags="all=-l=4" -ldflags "-s -w -buildid=" ./main
|
||||||
|
|
||||||
# Download geodat into a staging directory
|
# Download geodat into a staging directory
|
||||||
ADD https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geoip-full.dat /tmp/geodat/geoip.dat
|
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat
|
||||||
ADD https://github.com/XTLS/Xray-rules-dat/releases/latest/download/geosite-expand.dat /tmp/geodat/geosite.dat
|
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat
|
||||||
|
|
||||||
RUN mkdir -p /tmp/empty
|
RUN mkdir -p /tmp/empty
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ RUN cat <<EOF >/tmp/usr/local/etc/xray/00_log.json
|
|||||||
{
|
{
|
||||||
"log": {
|
"log": {
|
||||||
"error": "/var/log/xray/error.log",
|
"error": "/var/log/xray/error.log",
|
||||||
"logLevel": "warning",
|
"loglevel": "warning",
|
||||||
"access": "none",
|
"access": "none",
|
||||||
"dnsLog": false
|
"dnsLog": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,8 +122,8 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
|
|||||||
if config.Concurrency < 0 {
|
if config.Concurrency < 0 {
|
||||||
h.mux = &mux.ClientManager{Enabled: false}
|
h.mux = &mux.ClientManager{Enabled: false}
|
||||||
}
|
}
|
||||||
if config.Concurrency >= 0 && config.Concurrency <= 128 {
|
if config.Concurrency == 0 {
|
||||||
config.Concurrency = 128
|
config.Concurrency = 8 // same as before
|
||||||
}
|
}
|
||||||
if config.Concurrency > 0 {
|
if config.Concurrency > 0 {
|
||||||
h.mux = &mux.ClientManager{
|
h.mux = &mux.ClientManager{
|
||||||
@@ -134,7 +134,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
|
|||||||
Dialer: h,
|
Dialer: h,
|
||||||
Strategy: mux.ClientStrategy{
|
Strategy: mux.ClientStrategy{
|
||||||
MaxConcurrency: uint32(config.Concurrency),
|
MaxConcurrency: uint32(config.Concurrency),
|
||||||
MaxConnection: 1024,
|
MaxConnection: 128,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -155,7 +155,7 @@ func NewHandler(ctx context.Context, config *core.OutboundHandlerConfig) (outbou
|
|||||||
Dialer: h,
|
Dialer: h,
|
||||||
Strategy: mux.ClientStrategy{
|
Strategy: mux.ClientStrategy{
|
||||||
MaxConcurrency: uint32(config.XudpConcurrency),
|
MaxConcurrency: uint32(config.XudpConcurrency),
|
||||||
MaxConnection: 1024,
|
MaxConnection: 128,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -73,7 +73,7 @@ func (r *TimeoutWrapperReader) ReadMultiBufferTimeout(duration time.Duration) (M
|
|||||||
}
|
}
|
||||||
return r.mb, r.err
|
return r.mb, r.err
|
||||||
case <-timeout:
|
case <-timeout:
|
||||||
return nil, ErrReadTimeout
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ func TestMarshalConfigJson(t *testing.T) {
|
|||||||
func getConfig() string {
|
func getConfig() string {
|
||||||
return `{
|
return `{
|
||||||
"log": {
|
"log": {
|
||||||
"logLevel": "debug"
|
"loglevel": "debug"
|
||||||
},
|
},
|
||||||
"stats": {},
|
"stats": {},
|
||||||
"policy": {
|
"policy": {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ require (
|
|||||||
github.com/google/go-cmp v0.7.0
|
github.com/google/go-cmp v0.7.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/klauspost/cpuid/v2 v2.4.2
|
github.com/klauspost/cpuid/v2 v2.4.1
|
||||||
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
|
github.com/libp2p/go-nat v1.0.1-0.20250821073202-01afc089f138
|
||||||
github.com/miekg/dns v1.1.73
|
github.com/miekg/dns v1.1.73
|
||||||
github.com/pelletier/go-toml v1.9.5
|
github.com/pelletier/go-toml v1.9.5
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
|||||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||||
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
|
||||||
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
|
||||||
github.com/klauspost/cpuid/v2 v2.4.2 h1:xRw7IZEsAgVCp6trS/DgWbTAqpixhmywgVgBjYGg1bQ=
|
github.com/klauspost/cpuid/v2 v2.4.1 h1:I9KxadAkvo+QjtcEE2f2tyfEvmIla/T0m+14PrZNl0U=
|
||||||
github.com/klauspost/cpuid/v2 v2.4.2/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
github.com/klauspost/cpuid/v2 v2.4.1/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU=
|
||||||
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
||||||
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
|
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
|||||||
+3
-33
@@ -9,7 +9,6 @@ import (
|
|||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/geodata"
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/log"
|
|
||||||
xnet "github.com/xtls/xray-core/common/net"
|
xnet "github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
"github.com/xtls/xray-core/proxy/freedom"
|
"github.com/xtls/xray-core/proxy/freedom"
|
||||||
@@ -49,8 +48,6 @@ type FreedomFinalRuleConfig struct {
|
|||||||
Network *NetworkList `json:"network"`
|
Network *NetworkList `json:"network"`
|
||||||
Port *PortList `json:"port"`
|
Port *PortList `json:"port"`
|
||||||
IP *StringList `json:"ip"`
|
IP *StringList `json:"ip"`
|
||||||
Delay *Int32Range `json:"delay"`
|
|
||||||
LogLevel string `json:"logLevel"`
|
|
||||||
BlockDelay *Int32Range `json:"blockDelay"`
|
BlockDelay *Int32Range `json:"blockDelay"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,37 +280,10 @@ func (c *FreedomFinalRuleConfig) Build() (*freedom.FinalRuleConfig, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if c.BlockDelay != nil {
|
if c.BlockDelay != nil {
|
||||||
errors.PrintDeprecatedFeatureWarning("finalRules.blockDelay", "finalRules.delay")
|
rule.BlockDelay = &freedom.Range{
|
||||||
c.Delay = c.BlockDelay
|
Min: uint64(c.BlockDelay.From),
|
||||||
|
Max: uint64(c.BlockDelay.To),
|
||||||
}
|
}
|
||||||
|
|
||||||
if rule.Action != freedom.RuleAction_Block {
|
|
||||||
if c.Delay != nil || c.LogLevel != "" {
|
|
||||||
return nil, errors.New("delay and logLevel are only supported for block finalRules")
|
|
||||||
}
|
|
||||||
return rule, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.Delay != nil {
|
|
||||||
rule.Delay = &freedom.Range{
|
|
||||||
Min: uint64(c.Delay.From),
|
|
||||||
Max: uint64(c.Delay.To),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch strings.ToLower(c.LogLevel) {
|
|
||||||
case "", "info":
|
|
||||||
rule.LogLevel = log.Severity_Info.Enum()
|
|
||||||
case "none":
|
|
||||||
rule.LogLevel = log.Severity_Unknown.Enum()
|
|
||||||
case "debug":
|
|
||||||
rule.LogLevel = log.Severity_Debug.Enum()
|
|
||||||
case "warning":
|
|
||||||
rule.LogLevel = log.Severity_Warning.Enum()
|
|
||||||
case "error":
|
|
||||||
rule.LogLevel = log.Severity_Error.Enum()
|
|
||||||
default:
|
|
||||||
return nil, errors.New("unknown finalRules.logLevel: ", c.LogLevel)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return rule, nil
|
return rule, nil
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/geodata"
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/log"
|
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/protocol"
|
"github.com/xtls/xray-core/common/protocol"
|
||||||
. "github.com/xtls/xray-core/infra/conf"
|
. "github.com/xtls/xray-core/infra/conf"
|
||||||
@@ -47,8 +46,7 @@ func TestFreedomConfig(t *testing.T) {
|
|||||||
"network": "tcp,udp",
|
"network": "tcp,udp",
|
||||||
"port": "53,443",
|
"port": "53,443",
|
||||||
"ip": ["10.0.0.0/8", "2001:db8::/32"],
|
"ip": ["10.0.0.0/8", "2001:db8::/32"],
|
||||||
"delay": "30-60",
|
"blockDelay": "30-60"
|
||||||
"logLevel": "debug"
|
|
||||||
}, {
|
}, {
|
||||||
"action": "allow",
|
"action": "allow",
|
||||||
"network": ["udp"]
|
"network": ["udp"]
|
||||||
@@ -88,8 +86,10 @@ func TestFreedomConfig(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Delay: &freedom.Range{Min: 30, Max: 60},
|
BlockDelay: &freedom.Range{
|
||||||
LogLevel: log.Severity_Debug.Enum(),
|
Min: 30,
|
||||||
|
Max: 60,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Action: freedom.RuleAction_Allow,
|
Action: freedom.RuleAction_Allow,
|
||||||
|
|||||||
+1
-11
@@ -2,7 +2,6 @@ package conf
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"github.com/xtls/xray-core/app/geodata"
|
"github.com/xtls/xray-core/app/geodata"
|
||||||
@@ -20,20 +19,11 @@ func (c *GeodataAssetConfig) Build() (*geodata.Asset, error) {
|
|||||||
return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err)
|
return nil, errors.New("invalid geodata asset url: ", c.URL).Base(err)
|
||||||
}
|
}
|
||||||
return &geodata.Asset{
|
return &geodata.Asset{
|
||||||
Url: migrateGeodataURL(c.URL),
|
Url: c.URL,
|
||||||
File: c.File,
|
File: c.File,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func migrateGeodataURL(assetURL string) string {
|
|
||||||
if strings.HasSuffix(strings.ToLower(assetURL), ".dat") && (strings.HasPrefix(strings.ToLower(assetURL), "https://github.com/xtls/xray-rules-dat/") ||
|
|
||||||
strings.HasPrefix(strings.ToLower(assetURL), "https://raw.githubusercontent.com/xtls/xray-rules-dat/")) {
|
|
||||||
errors.PrintDeprecatedFeatureWarning(assetURL, assetURL+".gz")
|
|
||||||
return assetURL + ".gz"
|
|
||||||
}
|
|
||||||
return assetURL
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateHTTPS(s string) error {
|
func validateHTTPS(s string) error {
|
||||||
u, err := url.ParseRequestURI(s)
|
u, err := url.ParseRequestURI(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+1
-1
@@ -18,7 +18,7 @@ func DefaultLogConfig() *log.Config {
|
|||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
AccessLog string `json:"access"`
|
AccessLog string `json:"access"`
|
||||||
ErrorLog string `json:"error"`
|
ErrorLog string `json:"error"`
|
||||||
LogLevel string `json:"logLevel"`
|
LogLevel string `json:"loglevel"`
|
||||||
DNSLog bool `json:"dnsLog"`
|
DNSLog bool `json:"dnsLog"`
|
||||||
MaskAddress string `json:"maskAddress"`
|
MaskAddress string `json:"maskAddress"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func TestLoaderError(t *testing.T) {
|
|||||||
"log": {
|
"log": {
|
||||||
// abcd
|
// abcd
|
||||||
0,
|
0,
|
||||||
"logLevel": "info"
|
"loglevel": "info"
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
Output: "line 4 char 6",
|
Output: "line 4 char 6",
|
||||||
@@ -27,7 +27,7 @@ func TestLoaderError(t *testing.T) {
|
|||||||
Input: `{
|
Input: `{
|
||||||
"log": {
|
"log": {
|
||||||
// abcd
|
// abcd
|
||||||
"logLevel": "info",
|
"loglevel": "info",
|
||||||
}
|
}
|
||||||
}`,
|
}`,
|
||||||
Output: "line 5 char 5",
|
Output: "line 5 char 5",
|
||||||
|
|||||||
+1
-1
@@ -52,7 +52,7 @@ func (v *TunConfig) Build() (proto.Message, error) {
|
|||||||
return nil, errors.New("autoSystemWfpBlockLeak needs autoSystemRoutingTable to be set")
|
return nil, errors.New("autoSystemWfpBlockLeak needs autoSystemRoutingTable to be set")
|
||||||
}
|
}
|
||||||
if tun.BlocksSystemDNS(leaks) && len(v.DNS) == 0 {
|
if tun.BlocksSystemDNS(leaks) && len(v.DNS) == 0 {
|
||||||
return nil, errors.New(`autoSystemWfpBlockLeak "all", "allexceptlan", "dns", "windnsclient", or TCP or UDP port 53, needs dns to be set`)
|
return nil, errors.New(`autoSystemWfpBlockLeak "dns", "udp/53", "tcp/53" or "windnsclient" needs dns to be set`)
|
||||||
}
|
}
|
||||||
case "linux", "freebsd":
|
case "linux", "freebsd":
|
||||||
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
|
if v.AutoSystemDnsToGateway && len(v.Gateway) == 0 {
|
||||||
|
|||||||
+5
-49
@@ -29,38 +29,18 @@ func TestTunConfigAutoSystem(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns", "misconfigtun"]}`,
|
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns", "misconfigtun"]}`,
|
||||||
Parser: loadJSON(creator),
|
Parser: loadJSON(creator),
|
||||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"53", "853", "windnsclient", "misconfigtun"}},
|
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853", "misconfigtun"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["DNS"]}`,
|
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["DNS"]}`,
|
||||||
Parser: loadJSON(creator),
|
Parser: loadJSON(creator),
|
||||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"53", "853", "windnsclient"}},
|
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Input: `{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["TCP/853", "udp/853", "tcp/0853"]}`,
|
Input: `{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["TCP/853", "udp/853", "tcp/0853"]}`,
|
||||||
Parser: loadJSON(creator),
|
Parser: loadJSON(creator),
|
||||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"tcp/853", "udp/853"}},
|
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"tcp/853", "udp/853"}},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["All", "UDP", "tcp/8000-9000", "icmp"]}`,
|
|
||||||
Parser: loadJSON(creator),
|
|
||||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"all", "udp", "tcp/8000-9000", "icmp"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["AllExceptLAN"]}`,
|
|
||||||
Parser: loadJSON(creator),
|
|
||||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"allexceptlan", "53", "853", "windnsclient"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Input: `{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["1.1.1.1/24:TCP/853", "[2001:DB8::/32]:udp/10-20", "[::/0]:ICMP", "853"]}`,
|
|
||||||
Parser: loadJSON(creator),
|
|
||||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"1.1.1.0/24:tcp/853", "[2001:db8::/32]:udp/10-20", "[::/0]:icmp", "853"}},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Input: `{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["0.0.0.0/0:dns", "[::/0]:All", "53"]}`,
|
|
||||||
Parser: loadJSON(creator),
|
|
||||||
Output: &tun.Config{Name: "xray0", Desc: "Wintun", MTU: 1500, DNS: []string{"1.1.1.1"}, AutoSystemRoutingTable: []string{"0.0.0.0/0"}, AutoOutboundsInterface: "auto", AutoSystemWfpBlockLeak: []string{"0.0.0.0/0:53", "0.0.0.0/0:853", "0.0.0.0/0:windnsclient", "[::/0]:all", "53"}},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,21 +57,6 @@ func TestTunConfigAutoSystemNeeds(t *testing.T) {
|
|||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["udp/53"]}`, []string{"windows"}},
|
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["udp/53"]}`, []string{"windows"}},
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["windnsclient"]}`, []string{"windows"}},
|
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["windnsclient"]}`, []string{"windows"}},
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["tcp/853", "udp/853"]}`, nil},
|
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["tcp/853", "udp/853"]}`, nil},
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["all"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["udp"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["tcp/50-60"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["tcp/54-60", "icmp"]}`, nil},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["53"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["50-60"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["[::/0]:udp/53"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["1.1.1.1/32:dns"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["[2001:db8::/32]:all"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["0.0.0.0/0:windnsclient"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["0.0.0.0/0:tcp/853", "1.1.1.0/24:icmp", "[::/0]:misconfigtun", "54-60"]}`, nil},
|
|
||||||
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["[::/0]:misconfigtun"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemWfpBlockLeak": ["all"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["allexceptlan"]}`, []string{"windows"}},
|
|
||||||
{`{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["allexceptlan"]}`, nil},
|
|
||||||
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, []string{"linux", "freebsd"}},
|
{`{"name": "xray0", "autoSystemDnsToGateway": true}`, []string{"linux", "freebsd"}},
|
||||||
} {
|
} {
|
||||||
config := new(TunConfig)
|
config := new(TunConfig)
|
||||||
@@ -105,21 +70,12 @@ func TestTunConfigAutoSystemNeeds(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTunConfigAutoSystemWfpBlockLeakUnknown(t *testing.T) {
|
func TestTunConfigAutoSystemWfpBlockLeakUnknown(t *testing.T) {
|
||||||
// With the settings the values need on Windows, so that only an unknown
|
for _, value := range []string{"ip", "tcp/0", "icmp/1"} {
|
||||||
// one fails.
|
|
||||||
build := func(value string) error {
|
|
||||||
config := new(TunConfig)
|
config := new(TunConfig)
|
||||||
if err := json.Unmarshal([]byte(`{"name": "xray0", "dns": ["1.1.1.1"], "autoSystemRoutingTable": ["0.0.0.0/0"], "autoSystemWfpBlockLeak": ["dns", "`+value+`"]}`), config); err != nil {
|
if err := json.Unmarshal([]byte(`{"name": "xray0", "autoSystemWfpBlockLeak": ["dns", "`+value+`"]}`), config); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, err := config.Build()
|
if _, err := config.Build(); err == nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := build("1.1.1.0/24:53"); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, value := range []string{"ip", "tcp/0", "tcp/0-10", "icmp/1", "udp/20-10", "0", "20-10", "53/tcp", "tcpv4", "ipv4:udp", "1.1.1.1:53", "2001:db8::/32:53", "[1.1.1.0/24]:53", "[::ffff:1.1.1.0/120]:53", "1.1.1.0/24"} {
|
|
||||||
if build(value) == nil {
|
|
||||||
t.Errorf("autoSystemWfpBlockLeak value %q was accepted", value)
|
t.Errorf("autoSystemWfpBlockLeak value %q was accepted", value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func TestXrayConfig(t *testing.T) {
|
|||||||
Input: `{
|
Input: `{
|
||||||
"log": {
|
"log": {
|
||||||
"access": "/var/log/xray/access.log",
|
"access": "/var/log/xray/access.log",
|
||||||
"logLevel": "error",
|
"loglevel": "error",
|
||||||
"error": "/var/log/xray/error.log"
|
"error": "/var/log/xray/error.log"
|
||||||
},
|
},
|
||||||
"inbounds": [{
|
"inbounds": [{
|
||||||
|
|||||||
+20
-34
@@ -8,7 +8,6 @@ package freedom
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
geodata "github.com/xtls/xray-core/common/geodata"
|
geodata "github.com/xtls/xray-core/common/geodata"
|
||||||
log "github.com/xtls/xray-core/common/log"
|
|
||||||
net "github.com/xtls/xray-core/common/net"
|
net "github.com/xtls/xray-core/common/net"
|
||||||
protocol "github.com/xtls/xray-core/common/protocol"
|
protocol "github.com/xtls/xray-core/common/protocol"
|
||||||
internet "github.com/xtls/xray-core/transport/internet"
|
internet "github.com/xtls/xray-core/transport/internet"
|
||||||
@@ -358,8 +357,7 @@ type FinalRuleConfig struct {
|
|||||||
Networks []net.Network `protobuf:"varint,2,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"`
|
Networks []net.Network `protobuf:"varint,2,rep,packed,name=networks,proto3,enum=xray.common.net.Network" json:"networks,omitempty"`
|
||||||
PortList *net.PortList `protobuf:"bytes,3,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"`
|
PortList *net.PortList `protobuf:"bytes,3,opt,name=port_list,json=portList,proto3" json:"port_list,omitempty"`
|
||||||
Ip []*geodata.IPRule `protobuf:"bytes,4,rep,name=ip,proto3" json:"ip,omitempty"`
|
Ip []*geodata.IPRule `protobuf:"bytes,4,rep,name=ip,proto3" json:"ip,omitempty"`
|
||||||
Delay *Range `protobuf:"bytes,5,opt,name=delay,proto3" json:"delay,omitempty"`
|
BlockDelay *Range `protobuf:"bytes,5,opt,name=block_delay,json=blockDelay,proto3" json:"block_delay,omitempty"`
|
||||||
LogLevel *log.Severity `protobuf:"varint,6,opt,name=log_level,json=logLevel,proto3,enum=xray.common.log.Severity,oneof" json:"log_level,omitempty"`
|
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
@@ -422,20 +420,13 @@ func (x *FinalRuleConfig) GetIp() []*geodata.IPRule {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *FinalRuleConfig) GetDelay() *Range {
|
func (x *FinalRuleConfig) GetBlockDelay() *Range {
|
||||||
if x != nil {
|
if x != nil {
|
||||||
return x.Delay
|
return x.BlockDelay
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *FinalRuleConfig) GetLogLevel() log.Severity {
|
|
||||||
if x != nil && x.LogLevel != nil {
|
|
||||||
return *x.LogLevel
|
|
||||||
}
|
|
||||||
return log.Severity(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
DomainStrategy internet.DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"`
|
DomainStrategy internet.DomainStrategy `protobuf:"varint,1,opt,name=domain_strategy,json=domainStrategy,proto3,enum=xray.transport.internet.DomainStrategy" json:"domain_strategy,omitempty"`
|
||||||
@@ -532,7 +523,7 @@ var File_proxy_freedom_config_proto protoreflect.FileDescriptor
|
|||||||
|
|
||||||
const file_proxy_freedom_config_proto_rawDesc = "" +
|
const file_proxy_freedom_config_proto_rawDesc = "" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\x1a\x14common/log/log.proto\"S\n" +
|
"\x1aproxy/freedom/config.proto\x12\x12xray.proxy.freedom\x1a!common/protocol/server_spec.proto\x1a\x1ftransport/internet/config.proto\x1a\x15common/net/port.proto\x1a\x18common/net/network.proto\x1a\x1bcommon/geodata/geodat.proto\"S\n" +
|
||||||
"\x13DestinationOverride\x12<\n" +
|
"\x13DestinationOverride\x12<\n" +
|
||||||
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"\x98\x02\n" +
|
"\x06server\x18\x01 \x01(\v2$.xray.common.protocol.ServerEndpointR\x06server\"\x98\x02\n" +
|
||||||
"\bFragment\x12!\n" +
|
"\bFragment\x12!\n" +
|
||||||
@@ -558,16 +549,14 @@ const file_proxy_freedom_config_proto_rawDesc = "" +
|
|||||||
"\bapply_to\x18\x06 \x01(\tR\aapplyTo\"+\n" +
|
"\bapply_to\x18\x06 \x01(\tR\aapplyTo\"+\n" +
|
||||||
"\x05Range\x12\x10\n" +
|
"\x05Range\x12\x10\n" +
|
||||||
"\x03min\x18\x01 \x01(\x04R\x03min\x12\x10\n" +
|
"\x03min\x18\x01 \x01(\x04R\x03min\x12\x10\n" +
|
||||||
"\x03max\x18\x02 \x01(\x04R\x03max\"\xe0\x02\n" +
|
"\x03max\x18\x02 \x01(\x04R\x03max\"\xa0\x02\n" +
|
||||||
"\x0fFinalRuleConfig\x126\n" +
|
"\x0fFinalRuleConfig\x126\n" +
|
||||||
"\x06action\x18\x01 \x01(\x0e2\x1e.xray.proxy.freedom.RuleActionR\x06action\x124\n" +
|
"\x06action\x18\x01 \x01(\x0e2\x1e.xray.proxy.freedom.RuleActionR\x06action\x124\n" +
|
||||||
"\bnetworks\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x126\n" +
|
"\bnetworks\x18\x02 \x03(\x0e2\x18.xray.common.net.NetworkR\bnetworks\x126\n" +
|
||||||
"\tport_list\x18\x03 \x01(\v2\x19.xray.common.net.PortListR\bportList\x12+\n" +
|
"\tport_list\x18\x03 \x01(\v2\x19.xray.common.net.PortListR\bportList\x12+\n" +
|
||||||
"\x02ip\x18\x04 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x02ip\x12/\n" +
|
"\x02ip\x18\x04 \x03(\v2\x1b.xray.common.geodata.IPRuleR\x02ip\x12:\n" +
|
||||||
"\x05delay\x18\x05 \x01(\v2\x19.xray.proxy.freedom.RangeR\x05delay\x12;\n" +
|
"\vblock_delay\x18\x05 \x01(\v2\x19.xray.proxy.freedom.RangeR\n" +
|
||||||
"\tlog_level\x18\x06 \x01(\x0e2\x19.xray.common.log.SeverityH\x00R\blogLevel\x88\x01\x01B\f\n" +
|
"blockDelay\"\xaf\x03\n" +
|
||||||
"\n" +
|
|
||||||
"_log_level\"\xaf\x03\n" +
|
|
||||||
"\x06Config\x12P\n" +
|
"\x06Config\x12P\n" +
|
||||||
"\x0fdomain_strategy\x18\x01 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12Z\n" +
|
"\x0fdomain_strategy\x18\x01 \x01(\x0e2'.xray.transport.internet.DomainStrategyR\x0edomainStrategy\x12Z\n" +
|
||||||
"\x14destination_override\x18\x03 \x01(\v2'.xray.proxy.freedom.DestinationOverrideR\x13destinationOverride\x12\x1d\n" +
|
"\x14destination_override\x18\x03 \x01(\v2'.xray.proxy.freedom.DestinationOverrideR\x13destinationOverride\x12\x1d\n" +
|
||||||
@@ -610,8 +599,7 @@ var file_proxy_freedom_config_proto_goTypes = []any{
|
|||||||
(net.Network)(0), // 8: xray.common.net.Network
|
(net.Network)(0), // 8: xray.common.net.Network
|
||||||
(*net.PortList)(nil), // 9: xray.common.net.PortList
|
(*net.PortList)(nil), // 9: xray.common.net.PortList
|
||||||
(*geodata.IPRule)(nil), // 10: xray.common.geodata.IPRule
|
(*geodata.IPRule)(nil), // 10: xray.common.geodata.IPRule
|
||||||
(log.Severity)(0), // 11: xray.common.log.Severity
|
(internet.DomainStrategy)(0), // 11: xray.transport.internet.DomainStrategy
|
||||||
(internet.DomainStrategy)(0), // 12: xray.transport.internet.DomainStrategy
|
|
||||||
}
|
}
|
||||||
var file_proxy_freedom_config_proto_depIdxs = []int32{
|
var file_proxy_freedom_config_proto_depIdxs = []int32{
|
||||||
7, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint
|
7, // 0: xray.proxy.freedom.DestinationOverride.server:type_name -> xray.common.protocol.ServerEndpoint
|
||||||
@@ -619,18 +607,17 @@ var file_proxy_freedom_config_proto_depIdxs = []int32{
|
|||||||
8, // 2: xray.proxy.freedom.FinalRuleConfig.networks:type_name -> xray.common.net.Network
|
8, // 2: xray.proxy.freedom.FinalRuleConfig.networks:type_name -> xray.common.net.Network
|
||||||
9, // 3: xray.proxy.freedom.FinalRuleConfig.port_list:type_name -> xray.common.net.PortList
|
9, // 3: xray.proxy.freedom.FinalRuleConfig.port_list:type_name -> xray.common.net.PortList
|
||||||
10, // 4: xray.proxy.freedom.FinalRuleConfig.ip:type_name -> xray.common.geodata.IPRule
|
10, // 4: xray.proxy.freedom.FinalRuleConfig.ip:type_name -> xray.common.geodata.IPRule
|
||||||
4, // 5: xray.proxy.freedom.FinalRuleConfig.delay:type_name -> xray.proxy.freedom.Range
|
4, // 5: xray.proxy.freedom.FinalRuleConfig.block_delay:type_name -> xray.proxy.freedom.Range
|
||||||
11, // 6: xray.proxy.freedom.FinalRuleConfig.log_level:type_name -> xray.common.log.Severity
|
11, // 6: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
|
||||||
12, // 7: xray.proxy.freedom.Config.domain_strategy:type_name -> xray.transport.internet.DomainStrategy
|
1, // 7: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride
|
||||||
1, // 8: xray.proxy.freedom.Config.destination_override:type_name -> xray.proxy.freedom.DestinationOverride
|
2, // 8: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment
|
||||||
2, // 9: xray.proxy.freedom.Config.fragment:type_name -> xray.proxy.freedom.Fragment
|
3, // 9: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise
|
||||||
3, // 10: xray.proxy.freedom.Config.noises:type_name -> xray.proxy.freedom.Noise
|
5, // 10: xray.proxy.freedom.Config.final_rules:type_name -> xray.proxy.freedom.FinalRuleConfig
|
||||||
5, // 11: xray.proxy.freedom.Config.final_rules:type_name -> xray.proxy.freedom.FinalRuleConfig
|
11, // [11:11] is the sub-list for method output_type
|
||||||
12, // [12:12] is the sub-list for method output_type
|
11, // [11:11] is the sub-list for method input_type
|
||||||
12, // [12:12] is the sub-list for method input_type
|
11, // [11:11] is the sub-list for extension type_name
|
||||||
12, // [12:12] is the sub-list for extension type_name
|
11, // [11:11] is the sub-list for extension extendee
|
||||||
12, // [12:12] is the sub-list for extension extendee
|
0, // [0:11] is the sub-list for field type_name
|
||||||
0, // [0:12] is the sub-list for field type_name
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() { file_proxy_freedom_config_proto_init() }
|
func init() { file_proxy_freedom_config_proto_init() }
|
||||||
@@ -638,7 +625,6 @@ func file_proxy_freedom_config_proto_init() {
|
|||||||
if File_proxy_freedom_config_proto != nil {
|
if File_proxy_freedom_config_proto != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
file_proxy_freedom_config_proto_msgTypes[4].OneofWrappers = []any{}
|
|
||||||
type x struct{}
|
type x struct{}
|
||||||
out := protoimpl.TypeBuilder{
|
out := protoimpl.TypeBuilder{
|
||||||
File: protoimpl.DescBuilder{
|
File: protoimpl.DescBuilder{
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import "transport/internet/config.proto";
|
|||||||
import "common/net/port.proto";
|
import "common/net/port.proto";
|
||||||
import "common/net/network.proto";
|
import "common/net/network.proto";
|
||||||
import "common/geodata/geodat.proto";
|
import "common/geodata/geodat.proto";
|
||||||
import "common/log/log.proto";
|
|
||||||
|
|
||||||
message DestinationOverride {
|
message DestinationOverride {
|
||||||
xray.common.protocol.ServerEndpoint server = 1;
|
xray.common.protocol.ServerEndpoint server = 1;
|
||||||
@@ -52,8 +51,7 @@ message FinalRuleConfig {
|
|||||||
repeated xray.common.net.Network networks = 2;
|
repeated xray.common.net.Network networks = 2;
|
||||||
xray.common.net.PortList port_list = 3;
|
xray.common.net.PortList port_list = 3;
|
||||||
repeated xray.common.geodata.IPRule ip = 4;
|
repeated xray.common.geodata.IPRule ip = 4;
|
||||||
Range delay = 5;
|
Range block_delay = 5;
|
||||||
optional xray.common.log.Severity log_level = 6;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
message Config {
|
message Config {
|
||||||
|
|||||||
+10
-32
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/dice"
|
"github.com/xtls/xray-core/common/dice"
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
"github.com/xtls/xray-core/common/geodata"
|
"github.com/xtls/xray-core/common/geodata"
|
||||||
"github.com/xtls/xray-core/common/log"
|
|
||||||
"github.com/xtls/xray-core/common/net"
|
"github.com/xtls/xray-core/common/net"
|
||||||
"github.com/xtls/xray-core/common/platform"
|
"github.com/xtls/xray-core/common/platform"
|
||||||
"github.com/xtls/xray-core/common/retry"
|
"github.com/xtls/xray-core/common/retry"
|
||||||
@@ -76,13 +75,11 @@ func init() {
|
|||||||
action: RuleAction_Block,
|
action: RuleAction_Block,
|
||||||
network: allNetworks,
|
network: allNetworks,
|
||||||
ip: geodata.GetPrivateIPMatcher(),
|
ip: geodata.GetPrivateIPMatcher(),
|
||||||
logLevel: log.Severity_Warning.Enum(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultBlockAllRule = &FinalRule{
|
defaultBlockAllRule = &FinalRule{
|
||||||
action: RuleAction_Block,
|
action: RuleAction_Block,
|
||||||
network: allNetworks,
|
network: allNetworks,
|
||||||
logLevel: log.Severity_Warning.Enum(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +88,7 @@ type FinalRule struct {
|
|||||||
network [8]bool
|
network [8]bool
|
||||||
port net.MemoryPortList
|
port net.MemoryPortList
|
||||||
ip geodata.IPMatcher
|
ip geodata.IPMatcher
|
||||||
delay *Range
|
blockDelay *Range
|
||||||
logLevel *log.Severity
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handler handles Freedom connections.
|
// Handler handles Freedom connections.
|
||||||
@@ -107,8 +103,7 @@ type Handler struct {
|
|||||||
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
|
func buildFinalRule(config *FinalRuleConfig) (*FinalRule, error) {
|
||||||
rule := &FinalRule{
|
rule := &FinalRule{
|
||||||
action: config.GetAction(),
|
action: config.GetAction(),
|
||||||
delay: config.GetDelay(),
|
blockDelay: config.GetBlockDelay(),
|
||||||
logLevel: config.LogLevel,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(config.Networks) == 0 {
|
if len(config.Networks) == 0 {
|
||||||
@@ -169,7 +164,7 @@ func getDefaultFinalRule(inbound *session.Inbound) *FinalRule {
|
|||||||
switch inbound.Name {
|
switch inbound.Name {
|
||||||
case "vless-reverse":
|
case "vless-reverse":
|
||||||
return defaultBlockAllRule
|
return defaultBlockAllRule
|
||||||
case "vless", "vmess", "trojan", "hysteria", "wireguard", "masque":
|
case "vless", "vmess", "trojan", "hysteria", "wireguard":
|
||||||
return defaultBlockPrivateRule
|
return defaultBlockPrivateRule
|
||||||
default:
|
default:
|
||||||
if strings.HasPrefix(inbound.Name, "shadowsocks") {
|
if strings.HasPrefix(inbound.Name, "shadowsocks") {
|
||||||
@@ -217,12 +212,12 @@ func (h *Handler) policy() policy.Session {
|
|||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FinalRule) blockDuration() time.Duration {
|
func (h *Handler) blockDelay(rule *FinalRule) time.Duration {
|
||||||
min := uint64(30)
|
min := uint64(30)
|
||||||
max := uint64(90)
|
max := uint64(90)
|
||||||
if r.delay != nil {
|
if rule.blockDelay != nil {
|
||||||
min = r.delay.Min
|
min = rule.blockDelay.Min
|
||||||
max = r.delay.Max
|
max = rule.blockDelay.Max
|
||||||
}
|
}
|
||||||
span := max - min
|
span := max - min
|
||||||
if max < min {
|
if max < min {
|
||||||
@@ -231,26 +226,9 @@ func (r *FinalRule) blockDuration() time.Duration {
|
|||||||
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second
|
return time.Duration(min+uint64(dice.Roll(int(span+1)))) * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *FinalRule) log(ctx context.Context, msg ...interface{}) {
|
|
||||||
level := log.Severity_Info
|
|
||||||
if r.logLevel != nil {
|
|
||||||
level = *r.logLevel
|
|
||||||
}
|
|
||||||
switch level {
|
|
||||||
case log.Severity_Debug:
|
|
||||||
errors.LogDebug(ctx, msg...)
|
|
||||||
case log.Severity_Info:
|
|
||||||
errors.LogInfo(ctx, msg...)
|
|
||||||
case log.Severity_Warning:
|
|
||||||
errors.LogWarning(ctx, msg...)
|
|
||||||
case log.Severity_Error:
|
|
||||||
errors.LogError(ctx, msg...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error {
|
func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Writer, rule *FinalRule, dest *net.Destination) error {
|
||||||
delay := rule.blockDuration()
|
delay := h.blockDelay(rule)
|
||||||
rule.log(ctx, "target blocked by a finalrule: ", *dest, ", blackholing connection for ", delay)
|
errors.LogInfo(ctx, "blocked target: ", *dest, ", blackholing connection for ", delay)
|
||||||
defer common.Close(output)
|
defer common.Close(output)
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() { // a DispatchLink inbound's reader ignores Interrupt, so don't wait for this copy
|
go func() { // a DispatchLink inbound's reader ignores Interrupt, so don't wait for this copy
|
||||||
@@ -262,7 +240,7 @@ func (h *Handler) blackhole(ctx context.Context, input buf.Reader, output buf.Wr
|
|||||||
case <-time.After(delay):
|
case <-time.After(delay):
|
||||||
common.Interrupt(input)
|
common.Interrupt(input)
|
||||||
common.Interrupt(output)
|
common.Interrupt(output)
|
||||||
rule.log(ctx, "closed blackholed connection to blocked target: ", *dest)
|
errors.LogInfo(ctx, "closed blackholed connection to blocked target: ", *dest)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-17
@@ -4,7 +4,7 @@ TUN interface support bridges the gap between network layer 3 and layer 7, intro
|
|||||||
|
|
||||||
This functionality is targeted to assist applications/end devices that don't have proxy support, or can't run external applications (like Smart TV's). Making it possible to run Xray proxy right on network edge devices (routers) with support to route raw network traffic. \
|
This functionality is targeted to assist applications/end devices that don't have proxy support, or can't run external applications (like Smart TV's). Making it possible to run Xray proxy right on network edge devices (routers) with support to route raw network traffic. \
|
||||||
Primary targets are Linux based router devices. Like most popular OpenWRT option. \
|
Primary targets are Linux based router devices. Like most popular OpenWRT option. \
|
||||||
Support for Windows, macOS, FreeBSD, Android and iOS is also implemented (see below).
|
Support for Windows, macOS, Android and iOS is also implemented (see below).
|
||||||
|
|
||||||
## PLEASE READ FOLLOWING CAREFULLY
|
## PLEASE READ FOLLOWING CAREFULLY
|
||||||
|
|
||||||
@@ -25,8 +25,6 @@ Examples of how to achieve this on a simple Linux system (Ubuntu with systemd-ne
|
|||||||
|
|
||||||
Packets are handled by Xray-core's own lightweight TCP/IP stack, which replaced gVisor (XTLS/Xray-core#7130).
|
Packets are handled by Xray-core's own lightweight TCP/IP stack, which replaced gVisor (XTLS/Xray-core#7130).
|
||||||
|
|
||||||
On Linux, as on Android, the TUN can also be a file descriptor handed over through the environment variable `xray.tun.fd` (or `XRAY_TUN_FD`), e.g. for lightweight or unprivileged setups.
|
|
||||||
|
|
||||||
With `autoSystemRoutingTable`, Xray's own connections are kept out of the TUN through `autoOutboundsInterface` (then `"auto"` by default): each of their sockets is bound to another interface, the one named there, or with `"auto"` the one of the default route (Android needs the name). Linux and Android bind with `SO_BINDTODEVICE`, Windows with `IP_UNICAST_IF` and `IPV6_UNICAST_IF`, macOS and iOS with `IP_BOUND_IF` and `IPV6_BOUND_IF`, and FreeBSD switches them to another routing table (see [FreeBSD SUPPORT](#freebsd-support)). Connections to loopback addresses, TCP and UDP, stay unbound. If a socket cannot be bound, the connection fails rather than leave unbound, where it could loop back into the TUN (XTLS/Xray-core#7173).
|
With `autoSystemRoutingTable`, Xray's own connections are kept out of the TUN through `autoOutboundsInterface` (then `"auto"` by default): each of their sockets is bound to another interface, the one named there, or with `"auto"` the one of the default route (Android needs the name). Linux and Android bind with `SO_BINDTODEVICE`, Windows with `IP_UNICAST_IF` and `IPV6_UNICAST_IF`, macOS and iOS with `IP_BOUND_IF` and `IPV6_BOUND_IF`, and FreeBSD switches them to another routing table (see [FreeBSD SUPPORT](#freebsd-support)). Connections to loopback addresses, TCP and UDP, stay unbound. If a socket cannot be bound, the connection fails rather than leave unbound, where it could loop back into the TUN (XTLS/Xray-core#7173).
|
||||||
|
|
||||||
### SYSTEM DNS ON LINUX AND FREEBSD (`autoSystemDnsToGateway`)
|
### SYSTEM DNS ON LINUX AND FREEBSD (`autoSystemDnsToGateway`)
|
||||||
@@ -223,26 +221,23 @@ To make it start, wintun.dll specific for your Windows/arch must be present next
|
|||||||
|
|
||||||
After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running. A Wintun adapter that already has that name is reused.
|
After the start network adapter with the name you chose in the config will be created in the system, and exist while Xray is running. A Wintun adapter that already has that name is reused.
|
||||||
|
|
||||||
With IPv6 disabled system-wide (the `DisabledComponents` registry value), the adapter has no IPv6, and the TUN starts with IPv4 only, unless `gateway`, `dns` or `autoSystemRoutingTable` has IPv6 entries, which then keep it from starting.
|
With IPv6 disabled system-wide (the `DisabledComponents` registry value), the adapter has no IPv6, and the TUN starts with IPv4 only, unless `gateway` or `autoSystemRoutingTable` has IPv6 entries, which then keep it from starting.
|
||||||
|
|
||||||
When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops. Xray's own lookups through the system's DNS servers, like those of the `localhost` DNS server with `autoOutboundsInterface`, skip them, unless another interface uses them as well, as they would lead back into the TUN.
|
When `dns` is set, those servers are applied to the adapter. Windows is kept from registering the TUN's addresses in DNS, and its DNS cache is flushed when the TUN starts and stops.
|
||||||
|
|
||||||
Setting `autoSystemWfpBlockLeak` to at least `["dns"]` is recommended, to block DNS leaks. It needs `autoSystemRoutingTable` (the config is rejected otherwise), and adds Windows Filtering Platform filters that let traffic of every program but Xray itself leave only through the TUN, each kind chosen by a value:
|
With `autoSystemWfpBlockLeak`, which needs `autoSystemRoutingTable` (the config is rejected otherwise), Xray also adds Windows Filtering Platform filters that keep traffic of every program but Xray itself from leaving outside the TUN, each kind chosen by a value in the list, e.g. `"autoSystemWfpBlockLeak": ["dns", "misconfigtun"]`:
|
||||||
- `"dns"`: stands for `"53"`, `"853"` (DNS over TLS and QUIC) and `"windnsclient"`. Windows also queries the DNS servers of the other interfaces, through those interfaces whatever the routes say, and other programs reach a resolver on the local network (e.g. `192.168.1.1` from DHCP) through its more specific route.
|
- `"tcp/<port>"`, `"udp/<port>"`: connections to that TCP or UDP port only go through the TUN, e.g. `"tcp/853"`.
|
||||||
- `"windnsclient"`: Windows' DNS Client service, except for name resolution on the local network (LLMNR, mDNS). Since Windows 11 and Server 2022, it may query over HTTPS or TLS, to any port; earlier versions only query port 53, so there it does nothing.
|
- `"windnsclient"`: Windows' DNS Client service cannot connect outside the TUN at all, except for name resolution on the local network (LLMNR, mDNS). On Windows 11 and Server 2022 and later, it may send name queries over HTTPS or TLS, to any port. Earlier versions only query port 53, so there it does nothing.
|
||||||
- `"tcp"`, `"udp"`: that protocol, to all ports, or with `/<port>` or `/<low>-<high>` (1 to 65535) to some, e.g. `"tcp/853"` or `"udp/10-20"`. The ports alone stand for both, e.g. `"53"` or `"10-20"`.
|
- `"dns"`: stands for `"udp/53"`, `"tcp/53"`, `"windnsclient"`, `"tcp/853"` (DNS over TLS) and `"udp/853"` (DNS over QUIC). Windows keeps sending name queries to the DNS servers of the other interfaces as well, out through those interfaces whatever the routes say, and other programs reach a resolver on the local network (e.g. `192.168.1.1` handed out by DHCP) through its more specific LAN route instead of the TUN. Keeping the system's DNS inside the TUN like this, with `"dns"`, `"udp/53"`, `"tcp/53"` or `"windnsclient"`, needs `dns` (the config is rejected otherwise). The `dns` servers then have to lie within `gateway` or `autoSystemRoutingTable` (a warning is logged otherwise), and DNS servers that should be reached directly belong in Xray's own `dns` settings.
|
||||||
- `"icmp"`: ICMP and ICMPv6, e.g. `ping`.
|
- `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, IPv4 or IPv6, is blocked entirely, in both directions, as it would bypass the TUN. Only loopback and what Windows itself needs on the local link (DHCP, and for IPv6 neighbor and multicast listener discovery) remain allowed. An address of that version in `gateway` is not needed: without one, Windows gives the TUN link-local addresses itself, an IPv6 one at once and an IPv4 one from `169.254.0.0/16` after some seconds (until then, IPv4 routed to the TUN is unreachable), and what is routed to the TUN goes through it with those.
|
||||||
- `"misconfigtun"`: an IP version without routes in `autoSystemRoutingTable`, which would bypass the TUN.
|
|
||||||
- `"all"`: everything, in both directions. Access to the local network then breaks unless it is routed into the TUN as well, and so do incoming connections to other programs, other VPNs, and virtual machines whose NAT runs on the host.
|
|
||||||
- `"allexceptlan"`: like `"all"`, but traffic to and from the local network stays allowed, e.g. for printers or casting to a TV: private, link-local, multicast and broadcast addresses (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, `169.254.0.0/16`, `224.0.0.0/4`, `255.255.255.255`, `fc00::/7`, `fe80::/10`, `ff00::/8`), not global IPv6 ones. It includes `"dns"`, and the other values apply there as well. `"all"` overrides it.
|
|
||||||
|
|
||||||
A CIDR in front limits a value to its addresses, an IPv6 one in brackets, e.g. `"8.8.8.8/32:all"`, `"1.1.1.0/24:53"` or `"[::/0]:all"` (IPv6 only). It limits what the value stands for or includes as well: `"0.0.0.0/0:allexceptlan"`, for instance, leaves DNS over IPv6 open.
|
With the filters in place, Xray's own connections out also get past Windows Firewall's block rules (other firewalls may still block them), while connections to Xray's inbounds stay subject to them.
|
||||||
|
|
||||||
Loopback, DHCP, DHCPv6 and IPv6 neighbor discovery always stay allowed. Xray's own connections out also get past Windows Firewall's block rules, while connections to its inbounds stay subject to them. If the filters cannot be added, Xray does not start; they are removed when it exits. `"dns"` leaves name resolution on the local network (LLMNR, mDNS, NetBIOS) alone; `"all"` or `"udp"`, for instance, block it as well.
|
Names that Xray resolves through the system resolver, such as an outbound's server address given as a domain with the default `AsIs` domain strategy, would be looked up by Windows on Xray's behalf, and those queries would then go into the TUN too. While DNS is restricted this way and `autoOutboundsInterface` is in use (the default with `autoSystemRoutingTable`), Xray therefore resolves them itself, with its own queries to the DNS servers of the other interfaces. That bypasses Windows' DNS cache, and its name resolution on the local network (LLMNR, mDNS): a server address given as a domain is looked up again for every connection, and a DNS server that does not answer delays each lookup. Having Xray's own `dns` resolve it, through the outbound's `sockopt.domainStrategy`, avoids that. The `localhost` DNS server queries the same servers whenever `autoOutboundsInterface` is in use. Both skip the TUN's own DNS servers, unless another interface uses them as well: queried from Xray itself, they would lead back into it, or nowhere.
|
||||||
|
|
||||||
Keeping the system's DNS inside the TUN, with `"all"`, `"allexceptlan"`, `"dns"`, `"windnsclient"`, or anything covering TCP or UDP port 53, even to some addresses only, needs `dns` (the config is rejected otherwise), whose servers have to lie within `gateway` or `autoSystemRoutingTable`, or be loopback addresses like `127.0.0.1` (a warning is logged otherwise); DNS servers to be reached directly belong in Xray's own `dns` settings. With `autoOutboundsInterface`, names that Xray would look up through the system, e.g. an outbound's server address with the default `AsIs` domain strategy, are then resolved by Xray itself, from the DNS servers of the other interfaces, as Windows would send those queries into the TUN. That bypasses Windows' DNS cache, so such a name is looked up for every connection; resolving it with Xray's own `dns`, through the outbound's `sockopt.domainStrategy`, avoids that.
|
If the filters cannot be added, Xray does not start. They are removed when Xray exits. Not covered is name resolution on the local network (LLMNR, mDNS, NetBIOS), except over an IP version that is blocked.
|
||||||
|
|
||||||
`autoSystemWfpBlockLeak` is empty by default, as the filters break some setups: with `"dns"`, a local DNS resolver that queries through another interface, the DNS of another VPN, virtual machines whose NAT resolves names on the host, and captive portals; with `"misconfigtun"`, IPv4 or IPv6 on the local network while not routed into the TUN.
|
`autoSystemWfpBlockLeak` (Windows only) is empty by default, as the filters break some setups: with `"dns"`, a local DNS resolver other programs use (e.g. on `127.0.0.1:53`), the DNS of another VPN on its own interface, virtual machines whose NAT resolves names on the host, or signing in to a captive portal; with `"misconfigtun"`, IPv4 or IPv6 on the local network while no route of that version leads to the TUN. Without the filters, DNS may leak as described above. To keep an IP version out of the TUN on purpose while still blocking DNS leaks, use only `["dns"]`.
|
||||||
|
|
||||||
`autoOutboundsInterface` (the default with `autoSystemRoutingTable`) keeps Xray's own connections out of the TUN by binding them to another interface, which Windows only honors while that interface has weak host send and forwarding off for the IP versions routed to the TUN. Otherwise, Windows sends them into the TUN, from that interface's address, and they stall. While the TUN runs, Xray therefore turns weak host send off on that interface, and on again when it stops or another interface takes over. Forwarding cannot be turned off this way, as Mobile Hotspot and Internet Connection Sharing need it, so a warning is logged while it is on. Having the hotspot share the TUN instead of that interface (Settings, Mobile hotspot, Share my internet connection from) moves forwarding to the TUN, where it does no harm, and sends the hotspot's devices through Xray as well.
|
`autoOutboundsInterface` (the default with `autoSystemRoutingTable`) keeps Xray's own connections out of the TUN by binding them to another interface, which Windows only honors while that interface has weak host send and forwarding off for the IP versions routed to the TUN. Otherwise, Windows sends them into the TUN, from that interface's address, and they stall. While the TUN runs, Xray therefore turns weak host send off on that interface, and on again when it stops or another interface takes over. Forwarding cannot be turned off this way, as Mobile Hotspot and Internet Connection Sharing need it, so a warning is logged while it is on. Having the hotspot share the TUN instead of that interface (Settings, Mobile hotspot, Share my internet connection from) moves forwarding to the TUN, where it does no harm, and sends the hotspot's devices through Xray as well.
|
||||||
|
|
||||||
|
|||||||
+13
-22
@@ -115,7 +115,6 @@ func (t *WindowsTun) Start() (err error) {
|
|||||||
for _, ip := range t.options.DNS {
|
for _, ip := range t.options.DNS {
|
||||||
dns = append(dns, netip.MustParseAddr(ip))
|
dns = append(dns, netip.MustParseAddr(ip))
|
||||||
}
|
}
|
||||||
dns6 := slices.ContainsFunc(dns, netip.Addr.Is6)
|
|
||||||
|
|
||||||
var route4, route6 bool
|
var route4, route6 bool
|
||||||
routesMap := make(map[winipcfg.RouteData]struct{})
|
routesMap := make(map[winipcfg.RouteData]struct{})
|
||||||
@@ -176,7 +175,7 @@ startOver:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
// With IPv6 disabled system-wide (DisabledComponents), the adapter has no
|
// With IPv6 disabled system-wide (DisabledComponents), the adapter has no
|
||||||
// IPv6 interface at all. Skip the family unless the config asks for it.
|
// IPv6 interface at all. Skip the family unless the config asks for it.
|
||||||
if err == windows.ERROR_NOT_FOUND && family == windows.AF_INET6 && !address6 && !route6 && !dns6 {
|
if err == windows.ERROR_NOT_FOUND && family == windows.AF_INET6 && !address6 && !route6 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@@ -202,12 +201,6 @@ startOver:
|
|||||||
}
|
}
|
||||||
err = t.luid.SetDNS(family, dns, nil)
|
err = t.luid.SetDNS(family, dns, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Skip IPv6 the config does not ask for also when setting its DNS
|
|
||||||
// servers fails, as reported on Windows 11 24H2 with IPv6 disabled
|
|
||||||
// (Access is denied).
|
|
||||||
if family == windows.AF_INET6 && !address6 && !route6 && !dns6 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
firstErr = errors.New("unable to set DNS").Base(err)
|
firstErr = errors.New("unable to set DNS").Base(err)
|
||||||
if err == windows.ERROR_NOT_FOUND {
|
if err == windows.ERROR_NOT_FOUND {
|
||||||
goto startOver
|
goto startOver
|
||||||
@@ -254,22 +247,18 @@ startOver:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// With autoSystemWfpBlockLeak, once the system routes lead to the TUN,
|
// With autoSystemWfpBlockLeak, once the system routes lead to the TUN,
|
||||||
// keep the protocols and ports given ("dns" stands for those of DNS),
|
// keep connections to the ports given ("dns" stands for those of DNS),
|
||||||
// Windows' DNS Client service, an IP version no route of which leads to
|
// Windows' DNS Client service, and an IP version no route of which leads
|
||||||
// the TUN ("misconfigtun"), or everything ("all", or "allexceptlan" but
|
// to the TUN ("misconfigtun"), from leaving through the other interfaces.
|
||||||
// for the local network), to the addresses of a CIDR or to all, from
|
// Addresses do not matter: without one of a version in gateway, Windows
|
||||||
// leaving through the other interfaces. The TUN's own addresses do not
|
// gives the TUN a link-local one.
|
||||||
// matter: without one of a version in gateway, Windows gives it a
|
|
||||||
// link-local one.
|
|
||||||
leaks, err := ExpandWfpBlockLeak(t.options.AutoSystemWfpBlockLeak)
|
leaks, err := ExpandWfpBlockLeak(t.options.AutoSystemWfpBlockLeak)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
block := newLeakFilters(leaks, route4, route6)
|
block := newLeakFilters(leaks, route4, route6)
|
||||||
if route4 || route6 {
|
if route4 || route6 {
|
||||||
dnsClient := func(l leak) bool { return l.name == "windnsclient" }
|
if slices.Contains(leaks, "windnsclient") && !block.dnsClient {
|
||||||
if slices.ContainsFunc(block, dnsClient) && !dnsClientUsesAnyPort() {
|
|
||||||
block = slices.DeleteFunc(block, dnsClient)
|
|
||||||
errors.LogInfo(context.Background(), "[tun] windnsclient skipped: before Windows 11 and Server 2022, the DNS Client service only queries port 53")
|
errors.LogInfo(context.Background(), "[tun] windnsclient skipped: before Windows 11 and Server 2022, the DNS Client service only queries port 53")
|
||||||
}
|
}
|
||||||
if block.systemDNS() && len(dns) == 0 {
|
if block.systemDNS() && len(dns) == 0 {
|
||||||
@@ -282,19 +271,21 @@ startOver:
|
|||||||
return errors.New("unable to block ", blocked, " outside the TUN (remove autoSystemWfpBlockLeak to run without)").Base(err)
|
return errors.New("unable to block ", blocked, " outside the TUN (remove autoSystemWfpBlockLeak to run without)").Base(err)
|
||||||
}
|
}
|
||||||
errors.LogInfo(context.Background(), "[tun] blocked outside the TUN: ", blocked)
|
errors.LogInfo(context.Background(), "[tun] blocked outside the TUN: ", blocked)
|
||||||
|
if block.systemDNS() {
|
||||||
covered := slices.Clone(addresses)
|
covered := slices.Clone(addresses)
|
||||||
for _, route := range routesData {
|
for _, route := range routesData {
|
||||||
covered = append(covered, route.Destination)
|
covered = append(covered, route.Destination)
|
||||||
}
|
}
|
||||||
for _, server := range dnsOutsideTUN(dns, covered, block) {
|
for _, server := range dnsOutsideTUN(dns, covered) {
|
||||||
errors.LogWarning(context.Background(), "[tun] DNS server ", server, " is in neither gateway nor autoSystemRoutingTable, so queries to it cannot go through the TUN and are blocked")
|
errors.LogWarning(context.Background(), "[tun] DNS server ", server, " is in neither gateway nor autoSystemRoutingTable, so queries to it cannot go through the TUN and are blocked")
|
||||||
}
|
}
|
||||||
// With updater, the dialer controllers bind Xray's own sockets to the
|
// With updater, the dialer controllers bind Xray's own sockets
|
||||||
// physical interface.
|
// to the physical interface.
|
||||||
if block.systemDNS() && updater != nil {
|
if updater != nil {
|
||||||
t.resolver = resolveOnOwn()
|
t.resolver = resolveOnOwn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if len(dns) > 0 || route4 || route6 {
|
if len(dns) > 0 || route4 || route6 {
|
||||||
if err := flushDNSCache(); err != nil {
|
if err := flushDNSCache(); err != nil {
|
||||||
errors.LogInfoInner(context.Background(), err, "[tun] unable to flush DNS cache")
|
errors.LogInfoInner(context.Background(), err, "[tun] unable to flush DNS cache")
|
||||||
|
|||||||
+150
-317
@@ -3,7 +3,6 @@
|
|||||||
package tun
|
package tun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -45,12 +44,8 @@ const (
|
|||||||
fwpByteArray16Type = 11 // FWP_BYTE_ARRAY16_TYPE
|
fwpByteArray16Type = 11 // FWP_BYTE_ARRAY16_TYPE
|
||||||
fwpByteBlobType = 12 // FWP_BYTE_BLOB_TYPE
|
fwpByteBlobType = 12 // FWP_BYTE_BLOB_TYPE
|
||||||
fwpSecurityDescriptorType = 14 // FWP_SECURITY_DESCRIPTOR_TYPE
|
fwpSecurityDescriptorType = 14 // FWP_SECURITY_DESCRIPTOR_TYPE
|
||||||
fwpV4AddrMask = 0x100 // FWP_V4_ADDR_MASK
|
|
||||||
fwpV6AddrMask = 0x101 // FWP_V6_ADDR_MASK
|
|
||||||
fwpRangeType = 0x102 // FWP_RANGE_TYPE
|
|
||||||
|
|
||||||
fwpMatchEqual = 0 // FWP_MATCH_EQUAL
|
fwpMatchEqual = 0 // FWP_MATCH_EQUAL
|
||||||
fwpMatchRange = 5 // FWP_MATCH_RANGE
|
|
||||||
fwpMatchFlagsAllSet = 6 // FWP_MATCH_FLAGS_ALL_SET
|
fwpMatchFlagsAllSet = 6 // FWP_MATCH_FLAGS_ALL_SET
|
||||||
|
|
||||||
fwpConditionFlagIsLoopback = 1 // FWP_CONDITION_FLAG_IS_LOOPBACK
|
fwpConditionFlagIsLoopback = 1 // FWP_CONDITION_FLAG_IS_LOOPBACK
|
||||||
@@ -99,37 +94,6 @@ type fwpValue0 struct {
|
|||||||
value uintptr
|
value uintptr
|
||||||
}
|
}
|
||||||
|
|
||||||
// fwpRange0 is FWP_RANGE0, which an FWP_RANGE_TYPE condition points to.
|
|
||||||
type fwpRange0 struct {
|
|
||||||
low, high fwpValue0
|
|
||||||
}
|
|
||||||
|
|
||||||
// fwpV4AddrAndMask is FWP_V4_ADDR_AND_MASK, in host byte order.
|
|
||||||
type fwpV4AddrAndMask struct {
|
|
||||||
addr, mask uint32
|
|
||||||
}
|
|
||||||
|
|
||||||
// fwpV6AddrAndMask is FWP_V6_ADDR_AND_MASK.
|
|
||||||
type fwpV6AddrAndMask struct {
|
|
||||||
addr [16]byte
|
|
||||||
prefixLength uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
// lanPrefixes are what "allexceptlan" leaves to the local network: private
|
|
||||||
// and link-local addresses, and multicast and broadcast, which discovery on
|
|
||||||
// it uses.
|
|
||||||
var lanPrefixes = []netip.Prefix{
|
|
||||||
netip.MustParsePrefix("10.0.0.0/8"),
|
|
||||||
netip.MustParsePrefix("172.16.0.0/12"),
|
|
||||||
netip.MustParsePrefix("192.168.0.0/16"),
|
|
||||||
netip.MustParsePrefix("169.254.0.0/16"),
|
|
||||||
netip.MustParsePrefix("224.0.0.0/4"),
|
|
||||||
netip.MustParsePrefix("255.255.255.255/32"),
|
|
||||||
netip.MustParsePrefix("fc00::/7"),
|
|
||||||
netip.MustParsePrefix("fe80::/10"),
|
|
||||||
netip.MustParsePrefix("ff00::/8"),
|
|
||||||
}
|
|
||||||
|
|
||||||
type fwpmDisplayData0 struct {
|
type fwpmDisplayData0 struct {
|
||||||
name *uint16
|
name *uint16
|
||||||
description *uint16
|
description *uint16
|
||||||
@@ -207,36 +171,27 @@ func condition(field *windows.GUID, typ uint32, value uintptr) fwpmFilterConditi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// leakFilters are the values of autoSystemWfpBlockLeak that blockLeaks keeps
|
// leakFilters is what blockLeaks keeps from leaving outside the TUN.
|
||||||
// from leaving outside the TUN.
|
type leakFilters struct {
|
||||||
type leakFilters []leak
|
ports []leakPort // connections to these ports
|
||||||
|
dnsClient bool // Windows' DNS Client service
|
||||||
|
ipv4, ipv6 bool // an IP version altogether
|
||||||
|
}
|
||||||
|
|
||||||
// newLeakFilters turns expanded autoSystemWfpBlockLeak values into the filters
|
// newLeakFilters turns expanded autoSystemWfpBlockLeak values into the filters
|
||||||
// for a TUN that the routes of route4 (IPv4) and route6 (IPv6) lead to, which
|
// for a TUN that the routes of route4 (IPv4) and route6 (IPv6) lead to.
|
||||||
// leave "misconfigtun" over the IP versions without such routes only.
|
|
||||||
func newLeakFilters(leaks []string, route4, route6 bool) leakFilters {
|
func newLeakFilters(leaks []string, route4, route6 bool) leakFilters {
|
||||||
var block leakFilters
|
var block leakFilters
|
||||||
for _, value := range leaks {
|
for _, leak := range leaks {
|
||||||
l, ok := parseLeak(value)
|
switch leak {
|
||||||
if !ok {
|
case "windnsclient":
|
||||||
continue
|
block.dnsClient = dnsClientUsesAnyPort()
|
||||||
|
case "misconfigtun":
|
||||||
|
block.ipv4, block.ipv6 = !route4, !route6
|
||||||
|
default:
|
||||||
|
if port, ok := parseLeakPort(leak); ok {
|
||||||
|
block.ports = append(block.ports, port)
|
||||||
}
|
}
|
||||||
if l.name == "misconfigtun" {
|
|
||||||
unrouted4, unrouted6 := !route4, !route6
|
|
||||||
if l.prefix.IsValid() {
|
|
||||||
unrouted4, unrouted6 = unrouted4 && l.prefix.Addr().Is4(), unrouted6 && l.prefix.Addr().Is6()
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case !unrouted4 && !unrouted6:
|
|
||||||
continue
|
|
||||||
case !l.prefix.IsValid() && !unrouted4:
|
|
||||||
l.prefix = allAddresses(true)
|
|
||||||
case !l.prefix.IsValid() && !unrouted6:
|
|
||||||
l.prefix = allAddresses(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !slices.Contains(block, l) {
|
|
||||||
block = append(block, l)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return block
|
return block
|
||||||
@@ -244,69 +199,24 @@ func newLeakFilters(leaks []string, route4, route6 bool) leakFilters {
|
|||||||
|
|
||||||
// String lists what block keeps inside the TUN, or is empty for nothing.
|
// String lists what block keeps inside the TUN, or is empty for nothing.
|
||||||
func (block leakFilters) String() string {
|
func (block leakFilters) String() string {
|
||||||
values := make([]string, len(block))
|
var blocked []string
|
||||||
for i, l := range block {
|
for _, port := range block.ports {
|
||||||
values[i] = l.String()
|
blocked = append(blocked, port.String())
|
||||||
}
|
}
|
||||||
return strings.Join(values, ", ")
|
for _, b := range []struct {
|
||||||
|
on bool
|
||||||
|
what string
|
||||||
|
}{{block.dnsClient, "windnsclient"}, {block.ipv4, "IPv4"}, {block.ipv6, "IPv6"}} {
|
||||||
|
if b.on {
|
||||||
|
blocked = append(blocked, b.what)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(blocked, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// systemDNS tells whether block keeps the system's DNS inside the TUN, to at
|
// systemDNS tells whether block keeps the system's DNS inside the TUN.
|
||||||
// least some addresses: port 53, the DNS Client service, or everything ("all"),
|
|
||||||
// though not everything but the local network ("allexceptlan"), where a
|
|
||||||
// resolver like the router's may be.
|
|
||||||
func (block leakFilters) systemDNS() bool {
|
func (block leakFilters) systemDNS() bool {
|
||||||
return slices.ContainsFunc(block, leak.blocksDNS)
|
return block.dnsClient || slices.ContainsFunc(block.ports, func(port leakPort) bool { return port.port == 53 })
|
||||||
}
|
|
||||||
|
|
||||||
// prefixes returns the CIDRs of the values of block named name over an IP
|
|
||||||
// version.
|
|
||||||
func (block leakFilters) prefixes(name string, ipv6 bool) []netip.Prefix {
|
|
||||||
var prefixes []netip.Prefix
|
|
||||||
for _, l := range block {
|
|
||||||
if prefix, ok := l.over(ipv6); ok && l.name == name {
|
|
||||||
prefixes = append(prefixes, prefix)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return prefixes
|
|
||||||
}
|
|
||||||
|
|
||||||
// over returns the CIDR of l over an IP version, all its addresses for l
|
|
||||||
// without one, or false for l over the other IP version only.
|
|
||||||
func (l leak) over(ipv6 bool) (netip.Prefix, bool) {
|
|
||||||
if !l.prefix.IsValid() {
|
|
||||||
return allAddresses(ipv6), true
|
|
||||||
}
|
|
||||||
return l.prefix, l.prefix.Addr().Is6() == ipv6
|
|
||||||
}
|
|
||||||
|
|
||||||
// allAddresses is the CIDR of all addresses of an IP version.
|
|
||||||
func allAddresses(ipv6 bool) netip.Prefix {
|
|
||||||
if ipv6 {
|
|
||||||
return netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
|
||||||
}
|
|
||||||
return netip.PrefixFrom(netip.IPv4Unspecified(), 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// lanWithin returns the parts of lanPrefixes that lie in any of prefixes: two
|
|
||||||
// prefixes either share no address, or one holds the other.
|
|
||||||
func lanWithin(prefixes ...netip.Prefix) []netip.Prefix {
|
|
||||||
var parts []netip.Prefix
|
|
||||||
for _, lan := range lanPrefixes {
|
|
||||||
for _, prefix := range prefixes {
|
|
||||||
if !prefix.Overlaps(lan) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
part := lan
|
|
||||||
if prefix.Bits() > lan.Bits() {
|
|
||||||
part = prefix
|
|
||||||
}
|
|
||||||
if !slices.Contains(parts, part) {
|
|
||||||
parts = append(parts, part)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parts
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dnsClientUsesAnyPort tells whether Windows' DNS Client service may send its
|
// dnsClientUsesAnyPort tells whether Windows' DNS Client service may send its
|
||||||
@@ -319,49 +229,31 @@ func dnsClientUsesAnyPort() bool {
|
|||||||
return build >= 20348
|
return build >= 20348
|
||||||
}
|
}
|
||||||
|
|
||||||
// The weights of the filters of blockLeaks, the heaviest matching one of which
|
|
||||||
// decides.
|
|
||||||
const (
|
|
||||||
weightAllExceptLAN uint8 = iota // block "allexceptlan"
|
|
||||||
weightLAN // permit its local network
|
|
||||||
weightDNSClient // block "windnsclient"
|
|
||||||
weightLocalLink // permit its mDNS and LLMNR on the local network
|
|
||||||
weightBlock // block the other values
|
|
||||||
weightPermit // permit what always stays allowed, and the TUN
|
|
||||||
weightXray // permit Xray
|
|
||||||
)
|
|
||||||
|
|
||||||
// blockLeaks keeps traffic from leaving through interfaces other than tun,
|
// blockLeaks keeps traffic from leaving through interfaces other than tun,
|
||||||
// for every program but Xray itself, whose outbounds (DNS included) use the
|
// for every program but Xray itself, whose outbounds (DNS included) use the
|
||||||
// other interfaces on purpose, as the values of block say, each to the
|
// other interfaces on purpose:
|
||||||
// addresses of its CIDR, or to all:
|
|
||||||
//
|
//
|
||||||
// - Protocols and ports: connections of these, TCP or UDP to some or all
|
// - ports: connections to these TCP or UDP ports may only go through the
|
||||||
// ports, or ICMP, may only go through the TUN, those of DNS (53), DNS
|
// TUN, those of DNS (53), DNS over TLS (TCP 853) and DNS over QUIC (UDP
|
||||||
// over TLS (TCP 853) and DNS over QUIC (UDP 853) with "dns". Windows
|
// 853) with "dns". Windows sends a name query to the DNS servers of all
|
||||||
// sends a name query to the DNS servers of all interfaces, not only to
|
// interfaces, not only to those of the TUN: to the first server of each
|
||||||
// those of the TUN: to the first server of each interface, then to all
|
// interface, then to all of them when no answer arrives within a second or
|
||||||
// of them when no answer arrives within a second or two. It sends the
|
// two. It sends the queries for the servers of an interface out through
|
||||||
// queries for the servers of an interface out through that interface,
|
// that interface, whatever the routes say, and other programs reach an
|
||||||
// whatever the routes say, and other programs reach an on-link resolver,
|
// on-link resolver, like 192.168.1.1 from DHCP, through its LAN route,
|
||||||
// like 192.168.1.1 from DHCP, through its LAN route, which is more
|
// which is more specific than the TUN's default route.
|
||||||
// specific than the TUN's default route.
|
// - dnsClient: since Windows 11 and Server 2022, Windows may also send its
|
||||||
// - "windnsclient": since Windows 11 and Server 2022, Windows may also send
|
// queries over HTTPS or TLS, to any port, so its DNS Client service may not
|
||||||
// its queries over HTTPS or TLS, to any port, so its DNS Client service
|
// connect outside the TUN at all, except for name resolution on the local
|
||||||
// may not connect outside the TUN at all, except for name resolution on
|
// link (mDNS, LLMNR).
|
||||||
// the local network (mDNS, LLMNR).
|
// - ipv4, ipv6: no IPv4, or no IPv6, at all, in either direction, for a TUN
|
||||||
// - "all", and "misconfigtun" over an IP version no route of which leads to
|
// that no route of it leads to, except loopback and what Windows itself
|
||||||
// the TUN: nothing at all outside the TUN, in either direction. The TUN
|
// needs on the local link (DHCP, and for IPv6 neighbor and multicast
|
||||||
// carries what is routed to it even without an address of that IP
|
// listener discovery), none of which can leave it. The TUN carries what
|
||||||
// version in gateway: Windows gives it link-local ones itself, an IPv6
|
// is routed to it even without an address of that IP version in gateway:
|
||||||
// one at once, an IPv4 one from 169.254.0.0/16 after some seconds (until
|
// Windows gives it link-local ones itself, an IPv6 one at once, an IPv4
|
||||||
// then, IPv4 routed to the TUN is unreachable).
|
// one from 169.254.0.0/16 after some seconds (until then, IPv4 routed to
|
||||||
// - "allexceptlan": the same, but to and from the local network.
|
// the TUN is unreachable).
|
||||||
//
|
|
||||||
// The other values still apply to what "allexceptlan" and "windnsclient" leave
|
|
||||||
// open on the local network. Whatever is blocked, loopback, which cannot leave
|
|
||||||
// the system, and what Windows itself needs on the local link stay allowed:
|
|
||||||
// DHCP, and for IPv6 neighbor and multicast listener discovery.
|
|
||||||
//
|
//
|
||||||
// The filters live in a dynamic WFP session: closing the returned engine handle
|
// The filters live in a dynamic WFP session: closing the returned engine handle
|
||||||
// with closeWFPEngine deletes them, and so does Windows when the process dies.
|
// with closeWFPEngine deletes them, and so does Windows when the process dies.
|
||||||
@@ -462,149 +354,33 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
|||||||
onTUN := func(field *windows.GUID) fwpmFilterCondition0 {
|
onTUN := func(field *windows.GUID) fwpmFilterCondition0 {
|
||||||
return condition(field, fwpUint64, uintptr(unsafe.Pointer(tunLUID)))
|
return condition(field, fwpUint64, uintptr(unsafe.Pointer(tunLUID)))
|
||||||
}
|
}
|
||||||
// remote matches the remote addresses in any of prefixes, all of one IP
|
|
||||||
// version: conditions on the same field match when any of them does. With
|
|
||||||
// all addresses among them, no condition is needed.
|
|
||||||
remote := func(prefixes ...netip.Prefix) []fwpmFilterCondition0 {
|
|
||||||
if slices.ContainsFunc(prefixes, func(prefix netip.Prefix) bool { return prefix.Bits() == 0 }) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var conditions []fwpmFilterCondition0
|
|
||||||
for _, prefix := range prefixes {
|
|
||||||
var value uintptr
|
|
||||||
typ := uint32(fwpV4AddrMask)
|
|
||||||
if prefix.Addr().Is6() {
|
|
||||||
addr := &fwpV6AddrAndMask{addr: prefix.Addr().As16(), prefixLength: uint8(prefix.Bits())}
|
|
||||||
pinner.Pin(addr) // the condition only holds it as uintptr
|
|
||||||
value, typ = uintptr(unsafe.Pointer(addr)), fwpV6AddrMask
|
|
||||||
} else {
|
|
||||||
a := prefix.Addr().As4()
|
|
||||||
addr := &fwpV4AddrAndMask{addr: binary.BigEndian.Uint32(a[:]), mask: ^uint32(0) << (32 - prefix.Bits())}
|
|
||||||
pinner.Pin(addr) // the condition only holds it as uintptr
|
|
||||||
value = uintptr(unsafe.Pointer(addr))
|
|
||||||
}
|
|
||||||
conditions = append(conditions, condition(&fwpmConditionIPRemoteAddress, typ, value))
|
|
||||||
}
|
|
||||||
return conditions
|
|
||||||
}
|
|
||||||
// toPorts matches the ports of traffic, if not all of them.
|
|
||||||
toPorts := func(traffic leak) []fwpmFilterCondition0 {
|
|
||||||
switch {
|
|
||||||
case traffic.low == 0:
|
|
||||||
return nil
|
|
||||||
case traffic.low == traffic.high:
|
|
||||||
return []fwpmFilterCondition0{condition(&fwpmConditionIPRemotePort, fwpUint16, uintptr(traffic.low))}
|
|
||||||
}
|
|
||||||
ports := &fwpRange0{
|
|
||||||
low: fwpValue0{typ: fwpUint16, value: uintptr(traffic.low)},
|
|
||||||
high: fwpValue0{typ: fwpUint16, value: uintptr(traffic.high)},
|
|
||||||
}
|
|
||||||
pinner.Pin(ports) // the condition only holds it as uintptr
|
|
||||||
return []fwpmFilterCondition0{{
|
|
||||||
fieldKey: fwpmConditionIPRemotePort,
|
|
||||||
matchType: fwpMatchRange,
|
|
||||||
conditionValue: fwpValue0{typ: fwpRangeType, value: uintptr(unsafe.Pointer(ports))},
|
|
||||||
}}
|
|
||||||
}
|
|
||||||
|
|
||||||
// What stays allowed whatever is blocked, in both directions: replies to
|
|
||||||
// a connection accepted from outside would leave through the physical
|
|
||||||
// link as well.
|
|
||||||
type permit struct {
|
|
||||||
name string
|
|
||||||
conditions []fwpmFilterCondition0
|
|
||||||
}
|
|
||||||
loopback := permit{"loopback", []fwpmFilterCondition0{{
|
|
||||||
fieldKey: fwpmConditionFlags,
|
|
||||||
matchType: fwpMatchFlagsAllSet,
|
|
||||||
conditionValue: fwpValue0{typ: fwpUint32, value: fwpConditionFlagIsLoopback},
|
|
||||||
}}}
|
|
||||||
// DHCP keeps the addresses of the other interfaces, which Xray's own
|
|
||||||
// connections use.
|
|
||||||
dhcp := permit{"DHCP", []fwpmFilterCondition0{
|
|
||||||
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
|
||||||
condition(&fwpmConditionIPLocalPort, fwpUint16, 68),
|
|
||||||
condition(&fwpmConditionIPRemotePort, fwpUint16, 67),
|
|
||||||
}}
|
|
||||||
// Neighbor and multicast listener discovery, ICMPv6 130-137 and 143,
|
|
||||||
// whose type and code sit where the local and remote port are.
|
|
||||||
discovery := permit{"IPv6 neighbor and multicast listener discovery", []fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_ICMPV6)}}
|
|
||||||
for _, typ := range []uintptr{130, 131, 132, 133, 134, 135, 136, 137, 143} {
|
|
||||||
discovery.conditions = append(discovery.conditions, condition(&fwpmConditionIPLocalPort, fwpUint16, typ))
|
|
||||||
}
|
|
||||||
discovery.conditions = append(discovery.conditions, condition(&fwpmConditionIPRemotePort, fwpUint16, 0))
|
|
||||||
dhcpv6 := []fwpmFilterCondition0{
|
|
||||||
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
|
||||||
condition(&fwpmConditionIPLocalPort, fwpUint16, 546),
|
|
||||||
condition(&fwpmConditionIPRemotePort, fwpUint16, 547),
|
|
||||||
}
|
|
||||||
// The DHCPv6 client sends to the servers' multicast address, and they
|
|
||||||
// answer from their own.
|
|
||||||
dhcpv6Out := permit{"DHCPv6", slices.Concat(dhcpv6, []fwpmFilterCondition0{condition(&fwpmConditionIPRemoteAddress, fwpByteArray16Type, uintptr(unsafe.Pointer(&ipv6AllDHCPv6Servers)))})}
|
|
||||||
dhcpv6In := permit{"DHCPv6", dhcpv6}
|
|
||||||
|
|
||||||
for _, layer := range []struct {
|
for _, layer := range []struct {
|
||||||
key *windows.GUID
|
key *windows.GUID
|
||||||
selfFlags uint32
|
selfFlags uint32
|
||||||
via *windows.GUID // the interface that has to be the TUN as well
|
via *windows.GUID // the interface that has to be the TUN as well
|
||||||
ipv6 bool
|
|
||||||
permits []permit
|
|
||||||
}{
|
}{
|
||||||
{&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface, false, []permit{loopback, dhcp}},
|
{&fwpmLayerALEAuthConnectV4, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface},
|
||||||
{&fwpmLayerALEAuthRecvAcceptV4, 0, &fwpmConditionIPArrivalInterface, false, []permit{loopback, dhcp}},
|
{&fwpmLayerALEAuthRecvAcceptV4, 0, &fwpmConditionIPArrivalInterface},
|
||||||
{&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface, true, []permit{loopback, discovery, dhcpv6Out}},
|
{&fwpmLayerALEAuthConnectV6, fwpmFilterFlagClearActionRight, &fwpmConditionIPNexthopInterface},
|
||||||
{&fwpmLayerALEAuthRecvAcceptV6, 0, &fwpmConditionIPArrivalInterface, true, []permit{loopback, discovery, dhcpv6In}},
|
{&fwpmLayerALEAuthRecvAcceptV6, 0, &fwpmConditionIPArrivalInterface},
|
||||||
} {
|
} {
|
||||||
throughTUN := permit{"through the TUN", []fwpmFilterCondition0{onTUN(&fwpmConditionIPLocalInterface), onTUN(layer.via)}}
|
if err := add(layer.key, "permit Xray", layer.selfFlags, fwpActionPermit, 4, self); err != nil {
|
||||||
if err := add(layer.key, "permit Xray", layer.selfFlags, fwpActionPermit, weightXray, self); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, p := range append(layer.permits, throughTUN) {
|
for _, port := range block.ports {
|
||||||
if err := add(layer.key, "permit "+p.name, 0, fwpActionPermit, weightPermit, p.conditions...); err != nil {
|
protocol := uintptr(windows.IPPROTO_UDP)
|
||||||
|
if port.tcp {
|
||||||
|
protocol = windows.IPPROTO_TCP
|
||||||
|
}
|
||||||
|
toPort := []fwpmFilterCondition0{
|
||||||
|
condition(&fwpmConditionIPProtocol, fwpUint8, protocol),
|
||||||
|
condition(&fwpmConditionIPRemotePort, fwpUint16, uintptr(port.port)),
|
||||||
|
}
|
||||||
|
throughTUN := slices.Concat(toPort, []fwpmFilterCondition0{onTUN(&fwpmConditionIPLocalInterface), onTUN(layer.via)})
|
||||||
|
if err := add(layer.key, "permit "+port.String()+" through the TUN", 0, fwpActionPermit, 3, throughTUN...); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
if err := add(layer.key, "block "+port.String(), 0, fwpActionBlock, 2, toPort...); err != nil {
|
||||||
for _, l := range block {
|
|
||||||
prefix, ok := l.over(layer.ipv6)
|
|
||||||
if !ok {
|
|
||||||
continue // for the other IP version only
|
|
||||||
}
|
|
||||||
var protocols []uintptr
|
|
||||||
switch l.name {
|
|
||||||
case "tcp":
|
|
||||||
protocols = []uintptr{windows.IPPROTO_TCP}
|
|
||||||
case "udp":
|
|
||||||
protocols = []uintptr{windows.IPPROTO_UDP}
|
|
||||||
case "":
|
|
||||||
protocols = []uintptr{windows.IPPROTO_TCP, windows.IPPROTO_UDP}
|
|
||||||
case "icmp":
|
|
||||||
protocols = []uintptr{windows.IPPROTO_ICMP}
|
|
||||||
if layer.ipv6 {
|
|
||||||
protocols = []uintptr{windows.IPPROTO_ICMPV6}
|
|
||||||
}
|
|
||||||
case "all", "misconfigtun":
|
|
||||||
// every protocol
|
|
||||||
default:
|
|
||||||
continue // "allexceptlan" and "windnsclient" have filters of their own
|
|
||||||
}
|
|
||||||
var matches []fwpmFilterCondition0
|
|
||||||
for _, protocol := range protocols {
|
|
||||||
matches = append(matches, condition(&fwpmConditionIPProtocol, fwpUint8, protocol))
|
|
||||||
}
|
|
||||||
matches = slices.Concat(matches, toPorts(l), remote(prefix))
|
|
||||||
if err := add(layer.key, "block "+l.String(), 0, fwpActionBlock, weightBlock, matches...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allExceptLAN := block.prefixes("allexceptlan", layer.ipv6); len(allExceptLAN) > 0 {
|
|
||||||
// Lighter than the other values, which so still apply on the
|
|
||||||
// local network, DNS with "allexceptlan" among them.
|
|
||||||
if lan := lanWithin(allExceptLAN...); len(lan) > 0 {
|
|
||||||
if err := add(layer.key, "permit the local network", 0, fwpActionPermit, weightLAN, remote(lan...)...); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := add(layer.key, "block allexceptlan", 0, fwpActionBlock, weightAllExceptLAN, remote(allExceptLAN...)...); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -613,11 +389,12 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
|||||||
// Since Windows 11 and Server 2022, the DNS Client service may also send
|
// Since Windows 11 and Server 2022, the DNS Client service may also send
|
||||||
// the queries for an interface's servers over HTTPS or TLS, out through
|
// the queries for an interface's servers over HTTPS or TLS, out through
|
||||||
// that interface and to any port. So there it may only connect through
|
// that interface and to any port. So there it may only connect through
|
||||||
// the TUN, except for mDNS and LLMNR on the local network. Like Windows
|
// the TUN, except for mDNS and LLMNR, which stay on the local link (over
|
||||||
|
// an IP version only while it is not blocked altogether). Like Windows
|
||||||
// Firewall's rules for it, the filters recognize the service by its SID,
|
// Firewall's rules for it, the filters recognize the service by its SID,
|
||||||
// which Windows puts in the token of its process: the security descriptor
|
// which Windows puts in the token of its process: the security descriptor
|
||||||
// grants that SID the right to match (FWP_ACTRL_MATCH_FILTER, CC in SDDL).
|
// grants that SID the right to match (FWP_ACTRL_MATCH_FILTER, CC in SDDL).
|
||||||
if slices.ContainsFunc(block, func(l leak) bool { return l.name == "windnsclient" }) {
|
if block.dnsClient {
|
||||||
sd, err := windows.SecurityDescriptorFromString("O:SYG:SYD:(A;;CCRC;;;" + dnsClientSID + ")")
|
sd, err := windows.SecurityDescriptorFromString("O:SYG:SYD:(A;;CCRC;;;" + dnsClientSID + ")")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -629,19 +406,84 @@ func addLeakFilters(engine windows.Handle, tun winipcfg.LUID, block leakFilters)
|
|||||||
mdnsLLMNR := []fwpmFilterCondition0{dnsClient, condition(&fwpmConditionIPRemotePort, fwpUint16, 5353), condition(&fwpmConditionIPRemotePort, fwpUint16, 5355)}
|
mdnsLLMNR := []fwpmFilterCondition0{dnsClient, condition(&fwpmConditionIPRemotePort, fwpUint16, 5353), condition(&fwpmConditionIPRemotePort, fwpUint16, 5355)}
|
||||||
for _, layer := range []struct {
|
for _, layer := range []struct {
|
||||||
key *windows.GUID
|
key *windows.GUID
|
||||||
ipv6 bool
|
localLink bool
|
||||||
}{
|
}{
|
||||||
{&fwpmLayerALEAuthConnectV4, false},
|
{&fwpmLayerALEAuthConnectV4, !block.ipv4},
|
||||||
{&fwpmLayerALEAuthConnectV6, true},
|
{&fwpmLayerALEAuthConnectV6, !block.ipv6},
|
||||||
} {
|
} {
|
||||||
prefixes := block.prefixes("windnsclient", layer.ipv6)
|
if err := add(layer.key, "permit the DNS Client service through the TUN", 0, fwpActionPermit, 3, dnsClient, onTUN(&fwpmConditionIPLocalInterface), onTUN(&fwpmConditionIPNexthopInterface)); err != nil {
|
||||||
if len(prefixes) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := add(layer.key, "permit the DNS Client service's mDNS and LLMNR", 0, fwpActionPermit, weightLocalLink, slices.Concat(mdnsLLMNR, remote(lanWithin(allAddresses(layer.ipv6))...))...); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := add(layer.key, "block the DNS Client service", 0, fwpActionBlock, weightDNSClient, slices.Concat([]fwpmFilterCondition0{dnsClient}, remote(prefixes...))...); err != nil {
|
if layer.localLink {
|
||||||
|
if err := add(layer.key, "permit the DNS Client service's mDNS and LLMNR", 0, fwpActionPermit, 3, mdnsLLMNR...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := add(layer.key, "block the DNS Client service", 0, fwpActionBlock, 2, dnsClient); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both directions: replies to a connection accepted from outside would
|
||||||
|
// leave through the physical link as well.
|
||||||
|
loopback := fwpmFilterCondition0{
|
||||||
|
fieldKey: fwpmConditionFlags,
|
||||||
|
matchType: fwpMatchFlagsAllSet,
|
||||||
|
conditionValue: fwpValue0{typ: fwpUint32, value: fwpConditionFlagIsLoopback},
|
||||||
|
}
|
||||||
|
if block.ipv4 {
|
||||||
|
// DHCP keeps the addresses of the other interfaces, which Xray's own
|
||||||
|
// connections use.
|
||||||
|
dhcp := []fwpmFilterCondition0{
|
||||||
|
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
||||||
|
condition(&fwpmConditionIPLocalPort, fwpUint16, 68),
|
||||||
|
condition(&fwpmConditionIPRemotePort, fwpUint16, 67),
|
||||||
|
}
|
||||||
|
for _, layer := range []*windows.GUID{&fwpmLayerALEAuthConnectV4, &fwpmLayerALEAuthRecvAcceptV4} {
|
||||||
|
if err := add(layer, "permit IPv4 loopback", 0, fwpActionPermit, 1, loopback); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := add(layer, "permit DHCP", 0, fwpActionPermit, 1, dhcp...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := add(layer, "block IPv4", 0, fwpActionBlock, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if block.ipv6 {
|
||||||
|
// Neighbor and multicast listener discovery, ICMPv6 130-137 and 143,
|
||||||
|
// whose type and code sit where the local and remote port are.
|
||||||
|
discovery := []fwpmFilterCondition0{condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_ICMPV6)}
|
||||||
|
for _, typ := range []uintptr{130, 131, 132, 133, 134, 135, 136, 137, 143} {
|
||||||
|
discovery = append(discovery, condition(&fwpmConditionIPLocalPort, fwpUint16, typ))
|
||||||
|
}
|
||||||
|
discovery = append(discovery, condition(&fwpmConditionIPRemotePort, fwpUint16, 0))
|
||||||
|
dhcpv6 := []fwpmFilterCondition0{
|
||||||
|
condition(&fwpmConditionIPProtocol, fwpUint8, windows.IPPROTO_UDP),
|
||||||
|
condition(&fwpmConditionIPLocalPort, fwpUint16, 546),
|
||||||
|
condition(&fwpmConditionIPRemotePort, fwpUint16, 547),
|
||||||
|
}
|
||||||
|
for _, direction := range []struct {
|
||||||
|
layer *windows.GUID
|
||||||
|
dhcpv6 []fwpmFilterCondition0
|
||||||
|
}{
|
||||||
|
// The client sends to the servers' multicast address, and they
|
||||||
|
// answer from their own.
|
||||||
|
{&fwpmLayerALEAuthConnectV6, slices.Concat(dhcpv6, []fwpmFilterCondition0{condition(&fwpmConditionIPRemoteAddress, fwpByteArray16Type, uintptr(unsafe.Pointer(&ipv6AllDHCPv6Servers)))})},
|
||||||
|
{&fwpmLayerALEAuthRecvAcceptV6, dhcpv6},
|
||||||
|
} {
|
||||||
|
if err := add(direction.layer, "permit IPv6 loopback", 0, fwpActionPermit, 1, loopback); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := add(direction.layer, "permit IPv6 neighbor and multicast listener discovery", 0, fwpActionPermit, 1, discovery...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := add(direction.layer, "permit DHCPv6", 0, fwpActionPermit, 1, direction.dhcpv6...); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := add(direction.layer, "block IPv6", 0, fwpActionBlock, 0); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -668,22 +510,13 @@ func addFilter(engine windows.Handle, sublayer, layer *windows.GUID, name string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// dnsOutsideTUN returns the servers whose queries block blocks: those outside
|
// dnsOutsideTUN returns the servers outside all of prefixes, the TUN's own
|
||||||
// all of prefixes, the TUN's own subnets and routes, as queries to them cannot
|
// subnets and routes: queries to them cannot go through the TUN.
|
||||||
// go through the TUN, and inside the CIDR of a value that keeps DNS inside the
|
func dnsOutsideTUN(servers []netip.Addr, prefixes []netip.Prefix) []netip.Addr {
|
||||||
// TUN, or of "misconfigtun". Loopback ones, local resolvers, are not among
|
|
||||||
// them, as loopback stays allowed.
|
|
||||||
func dnsOutsideTUN(servers []netip.Addr, prefixes []netip.Prefix, block leakFilters) []netip.Addr {
|
|
||||||
var outside []netip.Addr
|
var outside []netip.Addr
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
server = server.Unmap()
|
server = server.Unmap()
|
||||||
if server.IsLoopback() || slices.ContainsFunc(prefixes, func(p netip.Prefix) bool { return p.Contains(server) }) {
|
if !slices.ContainsFunc(prefixes, func(p netip.Prefix) bool { return p.Contains(server) }) {
|
||||||
continue
|
|
||||||
}
|
|
||||||
if slices.ContainsFunc(block, func(l leak) bool {
|
|
||||||
prefix, ok := l.over(server.Is6())
|
|
||||||
return ok && prefix.Contains(server) && (l.blocksDNS() || l.name == "misconfigtun")
|
|
||||||
}) {
|
|
||||||
outside = append(outside, server)
|
outside = append(outside, server)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,21 +40,6 @@ func TestWFPStructLayout(t *testing.T) {
|
|||||||
[]uintptr{unsafe.Sizeof(value), unsafe.Offsetof(value.value)},
|
[]uintptr{unsafe.Sizeof(value), unsafe.Offsetof(value.value)},
|
||||||
[]uintptr{16, 8}, []uintptr{8, 4})
|
[]uintptr{16, 8}, []uintptr{8, 4})
|
||||||
|
|
||||||
var ports fwpRange0
|
|
||||||
check("FWP_RANGE0",
|
|
||||||
[]uintptr{unsafe.Sizeof(ports), unsafe.Offsetof(ports.high)},
|
|
||||||
[]uintptr{32, 16}, []uintptr{16, 8})
|
|
||||||
|
|
||||||
var v4 fwpV4AddrAndMask
|
|
||||||
check("FWP_V4_ADDR_AND_MASK",
|
|
||||||
[]uintptr{unsafe.Sizeof(v4), unsafe.Offsetof(v4.mask)},
|
|
||||||
[]uintptr{8, 4}, []uintptr{8, 4})
|
|
||||||
|
|
||||||
var v6 fwpV6AddrAndMask
|
|
||||||
check("FWP_V6_ADDR_AND_MASK",
|
|
||||||
[]uintptr{unsafe.Sizeof(v6), unsafe.Offsetof(v6.prefixLength)},
|
|
||||||
[]uintptr{17, 16}, []uintptr{17, 16})
|
|
||||||
|
|
||||||
var display fwpmDisplayData0
|
var display fwpmDisplayData0
|
||||||
check("FWPM_DISPLAY_DATA0",
|
check("FWPM_DISPLAY_DATA0",
|
||||||
[]uintptr{unsafe.Sizeof(display), unsafe.Offsetof(display.description)},
|
[]uintptr{unsafe.Sizeof(display), unsafe.Offsetof(display.description)},
|
||||||
@@ -129,101 +114,35 @@ func TestLeakFiltersAccepted(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
// Each with a sublayer of its own.
|
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun", "tcp/443"})
|
||||||
for _, c := range []struct {
|
|
||||||
values []string
|
|
||||||
route4, route6 bool
|
|
||||||
}{
|
|
||||||
{[]string{"dns", "misconfigtun", "all", "tcp/443", "udp/1000-2000", "tcp", "icmp", "10-20", "1-65535"}, false, false},
|
|
||||||
// CIDRs, and the local network's addresses.
|
|
||||||
{[]string{"1.1.1.0/24:tcp/80", "[2001:db8::/32]:udp/5000-6000", "[::/0]:icmp", "8.8.8.8/32:all", "[2000::/3]:misconfigtun", "9.9.9.9/32:dns"}, true, false},
|
|
||||||
{[]string{"allexceptlan", "udp/1000-2000", "icmp", "192.168.1.5/32:all"}, true, false},
|
|
||||||
{[]string{"0.0.0.0/0:allexceptlan", "[::/0]:all", "[::/0]:windnsclient", "1.0.0.0/8:windnsclient"}, true, true},
|
|
||||||
{[]string{"[::/0]:allexceptlan", "0.0.0.0/0:misconfigtun", "10.0.0.0/8:allexceptlan", "1.0.0.0/8:allexceptlan"}, false, true},
|
|
||||||
} {
|
|
||||||
leaks, err := ExpandWfpBlockLeak(c.values)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := addLeakFilters(engine, loopback, newLeakFilters(leaks, c.route4, c.route6)); err != nil {
|
block := newLeakFilters(leaks, false, false)
|
||||||
|
block.dnsClient = true // whatever the Windows version
|
||||||
|
if err := addLeakFilters(engine, loopback, block); err != nil {
|
||||||
skipUnlessElevated(err)
|
skipUnlessElevated(err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewLeakFilters(t *testing.T) {
|
func TestNewLeakFilters(t *testing.T) {
|
||||||
for _, c := range []struct {
|
leaks, err := ExpandWfpBlockLeak([]string{"dns", "misconfigtun"})
|
||||||
values []string
|
|
||||||
route4, route6 bool
|
|
||||||
blocked string
|
|
||||||
systemDNS bool
|
|
||||||
}{
|
|
||||||
{[]string{"dns", "misconfigtun"}, true, false, "53, 853, windnsclient, [::/0]:misconfigtun", true},
|
|
||||||
{[]string{"tcp/853", "udp/853", "misconfigtun"}, true, true, "tcp/853, udp/853", false},
|
|
||||||
{[]string{"all"}, true, true, "all", true},
|
|
||||||
{[]string{"all", "misconfigtun"}, true, true, "all", true},
|
|
||||||
{[]string{"misconfigtun"}, true, false, "[::/0]:misconfigtun", false},
|
|
||||||
{[]string{"misconfigtun"}, false, true, "0.0.0.0/0:misconfigtun", false},
|
|
||||||
{[]string{"misconfigtun"}, false, false, "misconfigtun", false},
|
|
||||||
{[]string{"tcp/10-20", "udp", "icmp"}, true, true, "tcp/10-20, udp, icmp", true},
|
|
||||||
{[]string{"tcp/54-60", "icmp", "54-60"}, true, true, "tcp/54-60, icmp, 54-60", false},
|
|
||||||
{[]string{"allexceptlan"}, true, true, "allexceptlan, 53, 853, windnsclient", true},
|
|
||||||
{[]string{"allexceptlan", "all"}, true, true, "allexceptlan, 53, 853, windnsclient, all", true},
|
|
||||||
{[]string{"misconfigtun", "allexceptlan"}, true, false, "[::/0]:misconfigtun, allexceptlan, 53, 853, windnsclient", true},
|
|
||||||
// Limited to the addresses of a CIDR.
|
|
||||||
{[]string{"0.0.0.0/0:TCP/10-20", "[::/0]:udp", "0.0.0.0/0:icmp"}, true, true, "0.0.0.0/0:tcp/10-20, [::/0]:udp, 0.0.0.0/0:icmp", true},
|
|
||||||
{[]string{"[::/0]:tcp/54-60", "[::/0]:icmp"}, true, true, "[::/0]:tcp/54-60, [::/0]:icmp", false},
|
|
||||||
{[]string{"0.0.0.0/0:windnsclient"}, true, true, "0.0.0.0/0:windnsclient", true},
|
|
||||||
{[]string{"1.1.1.0/24:53"}, true, true, "1.1.1.0/24:53", true},
|
|
||||||
{[]string{"[::/0]:all"}, true, false, "[::/0]:all", true},
|
|
||||||
{[]string{"0.0.0.0/0:allexceptlan", "[::/0]:all"}, true, true, "0.0.0.0/0:allexceptlan, 0.0.0.0/0:53, 0.0.0.0/0:853, 0.0.0.0/0:windnsclient, [::/0]:all", true},
|
|
||||||
{[]string{"0.0.0.0/0:misconfigtun"}, true, false, "", false},
|
|
||||||
{[]string{"[::/0]:misconfigtun"}, true, false, "[::/0]:misconfigtun", false},
|
|
||||||
{[]string{"[2000::/3]:misconfigtun", "1.0.0.0/8:misconfigtun"}, true, false, "[2000::/3]:misconfigtun", false},
|
|
||||||
{[]string{"misconfigtun", "[::/0]:misconfigtun"}, true, false, "[::/0]:misconfigtun", false},
|
|
||||||
{[]string{"0.0.0.0/0:misconfigtun", "[::/0]:allexceptlan"}, false, true, "0.0.0.0/0:misconfigtun, [::/0]:allexceptlan, [::/0]:53, [::/0]:853, [::/0]:windnsclient", true},
|
|
||||||
} {
|
|
||||||
leaks, err := ExpandWfpBlockLeak(c.values)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
block := newLeakFilters(leaks, c.route4, c.route6)
|
block := newLeakFilters(leaks, true, false)
|
||||||
if got := block.String(); got != c.blocked {
|
want := "udp/53, tcp/53, tcp/853, udp/853, IPv6"
|
||||||
t.Errorf("%v: String() = %q, want %q", c.values, got, c.blocked)
|
if dnsClientUsesAnyPort() {
|
||||||
|
want = "udp/53, tcp/53, tcp/853, udp/853, windnsclient, IPv6"
|
||||||
}
|
}
|
||||||
if got := block.systemDNS(); got != c.systemDNS {
|
if got := block.String(); got != want {
|
||||||
t.Errorf("%v: systemDNS() = %v", c.values, got)
|
t.Errorf("String() = %q, want %q", got, want)
|
||||||
}
|
}
|
||||||
|
if !block.systemDNS() {
|
||||||
|
t.Error("systemDNS() = false with dns")
|
||||||
}
|
}
|
||||||
// Without the port 53 values "allexceptlan" brings along, a resolver on
|
if block := newLeakFilters([]string{"tcp/853", "udp/853", "misconfigtun"}, true, true); block.String() != "tcp/853, udp/853" || block.systemDNS() {
|
||||||
// the local network, like the router's, stays reachable.
|
t.Errorf("tcp/853, udp/853: %q, systemDNS() = %v", block.String(), block.systemDNS())
|
||||||
if (leakFilters{{name: "allexceptlan"}}).systemDNS() {
|
|
||||||
t.Error("systemDNS() = true with the local network open")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLanWithin(t *testing.T) {
|
|
||||||
prefixes := func(cidrs ...string) []netip.Prefix {
|
|
||||||
var prefixes []netip.Prefix
|
|
||||||
for _, cidr := range cidrs {
|
|
||||||
prefixes = append(prefixes, netip.MustParsePrefix(cidr))
|
|
||||||
}
|
|
||||||
return prefixes
|
|
||||||
}
|
|
||||||
for _, c := range []struct {
|
|
||||||
within, lan []netip.Prefix
|
|
||||||
}{
|
|
||||||
{nil, nil},
|
|
||||||
{prefixes("0.0.0.0/0"), prefixes("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "169.254.0.0/16", "224.0.0.0/4", "255.255.255.255/32")},
|
|
||||||
{prefixes("::/0"), prefixes("fc00::/7", "fe80::/10", "ff00::/8")},
|
|
||||||
{prefixes("1.0.0.0/8", "10.1.0.0/16"), prefixes("10.1.0.0/16")},
|
|
||||||
{prefixes("192.0.0.0/2"), prefixes("192.168.0.0/16", "224.0.0.0/4", "255.255.255.255/32")},
|
|
||||||
{prefixes("2000::/3", "fe80::/64", "fe80::/10"), prefixes("fe80::/64", "fe80::/10")},
|
|
||||||
{prefixes("8.8.8.8/32", "2001:db8::/32"), nil},
|
|
||||||
} {
|
|
||||||
if got := lanWithin(c.within...); !slices.Equal(got, c.lan) {
|
|
||||||
t.Errorf("%v: got %v, want %v", c.within, got, c.lan)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,38 +167,10 @@ func TestDNSOutsideTUN(t *testing.T) {
|
|||||||
netip.MustParseAddr("::ffff:203.0.113.54"),
|
netip.MustParseAddr("::ffff:203.0.113.54"),
|
||||||
netip.MustParseAddr("8.8.8.8"),
|
netip.MustParseAddr("8.8.8.8"),
|
||||||
netip.MustParseAddr("2001:db8::53"),
|
netip.MustParseAddr("2001:db8::53"),
|
||||||
// Local resolvers, reachable over loopback whatever is blocked.
|
|
||||||
netip.MustParseAddr("127.0.0.1"),
|
|
||||||
netip.MustParseAddr("::ffff:127.0.0.53"),
|
|
||||||
netip.MustParseAddr("::1"),
|
|
||||||
}
|
|
||||||
// Only to the addresses of their CIDRs, if any, and "misconfigtun" over
|
|
||||||
// an IP version without routes to the TUN blocks DNS as well.
|
|
||||||
for _, c := range []struct {
|
|
||||||
values []string
|
|
||||||
route4, route6 bool
|
|
||||||
want []string
|
|
||||||
}{
|
|
||||||
{[]string{"dns"}, true, true, []string{"8.8.8.8", "2001:db8::53"}},
|
|
||||||
{[]string{"0.0.0.0/0:53"}, true, true, []string{"8.8.8.8"}},
|
|
||||||
{[]string{"8.8.8.0/24:windnsclient", "[2001:db8::/32]:tcp/853"}, true, true, []string{"8.8.8.8"}},
|
|
||||||
{[]string{"[2001:db8::53/128]:all", "9.9.9.9/32:53"}, true, true, []string{"2001:db8::53"}},
|
|
||||||
{[]string{"misconfigtun"}, true, false, []string{"2001:db8::53"}},
|
|
||||||
{[]string{"misconfigtun"}, true, true, nil},
|
|
||||||
{[]string{"tcp/853", "udp/5353", "icmp", "10.0.0.0/8:all"}, true, true, nil},
|
|
||||||
{[]string{"[::/0]:allexceptlan"}, true, true, []string{"2001:db8::53"}},
|
|
||||||
} {
|
|
||||||
leaks, err := ExpandWfpBlockLeak(c.values)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
var want []netip.Addr
|
|
||||||
for _, server := range c.want {
|
|
||||||
want = append(want, netip.MustParseAddr(server))
|
|
||||||
}
|
|
||||||
if got := dnsOutsideTUN(servers, prefixes, newLeakFilters(leaks, c.route4, c.route6)); !slices.Equal(got, want) {
|
|
||||||
t.Errorf("%v: got %v, want %v", c.values, got, want)
|
|
||||||
}
|
}
|
||||||
|
want := []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("2001:db8::53")}
|
||||||
|
if got := dnsOutsideTUN(servers, prefixes); !slices.Equal(got, want) {
|
||||||
|
t.Errorf("got %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+43
-124
@@ -1,7 +1,6 @@
|
|||||||
package tun
|
package tun
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -9,152 +8,72 @@ import (
|
|||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// dnsLeaks is what "dns" in autoSystemWfpBlockLeak stands for: DNS over TCP
|
// dnsLeaks is what "dns" in autoSystemWfpBlockLeak stands for: DNS over UDP and
|
||||||
// and UDP, DNS over TLS and QUIC, and Windows' DNS Client service, which may
|
// TCP, Windows' DNS Client service, which may also use other protocols and
|
||||||
// also use other protocols and ports.
|
// ports, DNS over TLS, and DNS over QUIC.
|
||||||
var dnsLeaks = []string{"53", "853", "windnsclient"}
|
var dnsLeaks = []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}
|
||||||
|
|
||||||
// leak is a value of autoSystemWfpBlockLeak: "dns", "windnsclient",
|
// leakPort is a "tcp/<port>" or "udp/<port>" value of autoSystemWfpBlockLeak:
|
||||||
// "misconfigtun", "all", "allexceptlan", "icmp", or "tcp" or "udp", the latter
|
// connections to that port may only go through the TUN.
|
||||||
// two with "/<port>" or "/<low>-<high>" for some ports only, such as "tcp/853"
|
type leakPort struct {
|
||||||
// or "udp/10-20", or such ports alone for TCP and UDP, such as "53". A CIDR in
|
tcp bool
|
||||||
// front limits any of them to its addresses, an IPv6 one in brackets, such as
|
port uint16
|
||||||
// "1.1.1.0/24:53" or "[::/0]:all".
|
|
||||||
type leak struct {
|
|
||||||
prefix netip.Prefix // all addresses of both IP versions if not valid
|
|
||||||
name string // empty for TCP and UDP to some ports
|
|
||||||
low, high uint16 // the ports of TCP and UDP, both 0 for all
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l leak) String() string {
|
func (p leakPort) String() string {
|
||||||
s := l.name
|
if p.tcp {
|
||||||
if l.low != 0 {
|
return "tcp/" + strconv.Itoa(int(p.port))
|
||||||
if s != "" {
|
|
||||||
s += "/"
|
|
||||||
}
|
|
||||||
s += strconv.Itoa(int(l.low))
|
|
||||||
if l.high != l.low {
|
|
||||||
s += "-" + strconv.Itoa(int(l.high))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch {
|
|
||||||
case !l.prefix.IsValid():
|
|
||||||
return s
|
|
||||||
case l.prefix.Addr().Is6():
|
|
||||||
return "[" + l.prefix.String() + "]:" + s
|
|
||||||
default:
|
|
||||||
return l.prefix.String() + ":" + s
|
|
||||||
}
|
}
|
||||||
|
return "udp/" + strconv.Itoa(int(p.port))
|
||||||
}
|
}
|
||||||
|
|
||||||
// covers tells whether l includes connections of protocol, "tcp" or "udp", to
|
// parseLeakPort parses a lower case "tcp/<port>" or "udp/<port>" value.
|
||||||
// port, to at least some addresses.
|
func parseLeakPort(value string) (leakPort, bool) {
|
||||||
func (l leak) covers(protocol string, port uint16) bool {
|
protocol, port, ok := strings.Cut(value, "/")
|
||||||
switch l.name {
|
if !ok || protocol != "tcp" && protocol != "udp" {
|
||||||
case "all":
|
return leakPort{}, false
|
||||||
return true
|
|
||||||
case "", protocol:
|
|
||||||
return l.low == 0 || l.low <= port && port <= l.high
|
|
||||||
}
|
}
|
||||||
return false
|
n, err := strconv.ParseUint(port, 10, 16)
|
||||||
}
|
if err != nil || n == 0 {
|
||||||
|
return leakPort{}, false
|
||||||
// blocksDNS tells whether l keeps the system's DNS, TCP or UDP port 53 or
|
|
||||||
// Windows' DNS Client service, inside the TUN, to at least some addresses.
|
|
||||||
func (l leak) blocksDNS() bool {
|
|
||||||
return l.name == "windnsclient" || l.covers("udp", 53) || l.covers("tcp", 53)
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseLeak parses a lower case value of autoSystemWfpBlockLeak. Ports go from
|
|
||||||
// 1 to 65535, low not above high, and "tcp/1-65535" stands for all of them, as
|
|
||||||
// does "tcp".
|
|
||||||
func parseLeak(value string) (leak, bool) {
|
|
||||||
var l leak
|
|
||||||
cidr, rest, hasCIDR := strings.Cut(value, ":")
|
|
||||||
ipv6 := strings.HasPrefix(value, "[")
|
|
||||||
if ipv6 {
|
|
||||||
cidr, rest, hasCIDR = strings.Cut(value[1:], "]:")
|
|
||||||
}
|
}
|
||||||
if hasCIDR {
|
return leakPort{tcp: protocol == "tcp", port: uint16(n)}, true
|
||||||
// IPv4-mapped IPv6 ones would not match: Windows checks such
|
|
||||||
// connections as IPv4.
|
|
||||||
prefix, err := netip.ParsePrefix(cidr)
|
|
||||||
if err != nil || prefix.Addr().Is6() != ipv6 || prefix.Addr().Is4In6() {
|
|
||||||
return leak{}, false
|
|
||||||
}
|
|
||||||
l.prefix, value = prefix.Masked(), rest
|
|
||||||
}
|
|
||||||
name, ports, hasPorts := strings.Cut(value, "/")
|
|
||||||
switch name {
|
|
||||||
case "tcp", "udp":
|
|
||||||
l.name = name
|
|
||||||
if !hasPorts {
|
|
||||||
return l, true
|
|
||||||
}
|
|
||||||
case "dns", "windnsclient", "misconfigtun", "all", "allexceptlan", "icmp":
|
|
||||||
l.name = name
|
|
||||||
return l, !hasPorts
|
|
||||||
default:
|
|
||||||
if hasPorts {
|
|
||||||
return leak{}, false
|
|
||||||
}
|
|
||||||
ports = name // for TCP and UDP
|
|
||||||
}
|
|
||||||
lowText, highText, isRange := strings.Cut(ports, "-")
|
|
||||||
low, err := strconv.ParseUint(lowText, 10, 16)
|
|
||||||
if err != nil || low == 0 {
|
|
||||||
return leak{}, false
|
|
||||||
}
|
|
||||||
high := low
|
|
||||||
if isRange {
|
|
||||||
if high, err = strconv.ParseUint(highText, 10, 16); err != nil || high < low {
|
|
||||||
return leak{}, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if l.name == "" || low != 1 || high != 65535 {
|
|
||||||
l.low, l.high = uint16(low), uint16(high)
|
|
||||||
}
|
|
||||||
return l, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExpandWfpBlockLeak checks the values of autoSystemWfpBlockLeak and returns
|
// ExpandWfpBlockLeak checks the values of autoSystemWfpBlockLeak and returns
|
||||||
// them in lower case, in order and each once, with "dns" replaced by what it
|
// them in lower case, in order and without duplicates, with "dns" replaced by
|
||||||
// stands for, "allexceptlan" followed by that, CIDRs masked, and ports written
|
// what it stands for.
|
||||||
// the same way, "tcp/1-65535" as "tcp" say.
|
|
||||||
func ExpandWfpBlockLeak(values []string) ([]string, error) {
|
func ExpandWfpBlockLeak(values []string) ([]string, error) {
|
||||||
var expanded []string
|
var leaks []string
|
||||||
add := func(l leak) {
|
add := func(leak string) {
|
||||||
if value := l.String(); !slices.Contains(expanded, value) {
|
if !slices.Contains(leaks, leak) {
|
||||||
expanded = append(expanded, value)
|
leaks = append(leaks, leak)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
l, ok := parseLeak(strings.ToLower(value))
|
switch value = strings.ToLower(value); value {
|
||||||
|
case "dns":
|
||||||
|
for _, leak := range dnsLeaks {
|
||||||
|
add(leak)
|
||||||
|
}
|
||||||
|
case "windnsclient", "misconfigtun":
|
||||||
|
add(value)
|
||||||
|
default:
|
||||||
|
port, ok := parseLeakPort(value)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", value)
|
return nil, errors.New("unknown autoSystemWfpBlockLeak value: ", value)
|
||||||
}
|
}
|
||||||
if l.name != "dns" {
|
add(port.String())
|
||||||
add(l)
|
|
||||||
}
|
|
||||||
// The local network is no way out for DNS with "allexceptlan", as a
|
|
||||||
// resolver there, like the router, would forward outside the TUN.
|
|
||||||
if l.name == "dns" || l.name == "allexceptlan" {
|
|
||||||
for _, dnsLeak := range dnsLeaks {
|
|
||||||
dns, _ := parseLeak(dnsLeak)
|
|
||||||
dns.prefix = l.prefix
|
|
||||||
add(dns)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return leaks, nil
|
||||||
return expanded, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlocksSystemDNS tells whether expanded autoSystemWfpBlockLeak values keep the
|
// BlocksSystemDNS tells whether expanded autoSystemWfpBlockLeak values keep the
|
||||||
// system's DNS (TCP or UDP port 53, or Windows' DNS Client service) inside the
|
// system's DNS (port 53, or Windows' DNS Client service) inside the TUN, which
|
||||||
// TUN, to at least some addresses, which then needs DNS servers of its own.
|
// then needs DNS servers of its own.
|
||||||
func BlocksSystemDNS(leaks []string) bool {
|
func BlocksSystemDNS(leaks []string) bool {
|
||||||
return slices.ContainsFunc(leaks, func(value string) bool {
|
return slices.ContainsFunc(leaks, func(leak string) bool {
|
||||||
l, _ := parseLeak(value)
|
return leak == "udp/53" || leak == "tcp/53" || leak == "windnsclient"
|
||||||
return l.blocksDNS()
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,39 +12,13 @@ func TestExpandWfpBlockLeak(t *testing.T) {
|
|||||||
systemDNS bool
|
systemDNS bool
|
||||||
}{
|
}{
|
||||||
{nil, nil, false},
|
{nil, nil, false},
|
||||||
{[]string{"dns"}, []string{"53", "853", "windnsclient"}, true},
|
{[]string{"dns"}, []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853"}, true},
|
||||||
{[]string{"DNS", "MisconfigTUN"}, []string{"53", "853", "windnsclient", "misconfigtun"}, true},
|
{[]string{"DNS", "MisconfigTUN"}, []string{"udp/53", "tcp/53", "windnsclient", "tcp/853", "udp/853", "misconfigtun"}, true},
|
||||||
{[]string{"tcp/853", "dns", "TCP/0853"}, []string{"tcp/853", "53", "853", "windnsclient"}, true},
|
{[]string{"tcp/853", "dns", "TCP/0853"}, []string{"tcp/853", "udp/53", "tcp/53", "windnsclient", "udp/853"}, true},
|
||||||
{[]string{"tcp/853", "udp/853", "misconfigtun"}, []string{"tcp/853", "udp/853", "misconfigtun"}, false},
|
{[]string{"tcp/853", "udp/853", "misconfigtun"}, []string{"tcp/853", "udp/853", "misconfigtun"}, false},
|
||||||
{[]string{"udp/65535", "tcp/1"}, []string{"udp/65535", "tcp/1"}, false},
|
{[]string{"udp/65535", "tcp/1"}, []string{"udp/65535", "tcp/1"}, false},
|
||||||
{[]string{"tcp/53"}, []string{"tcp/53"}, true},
|
{[]string{"tcp/53"}, []string{"tcp/53"}, true},
|
||||||
{[]string{"windnsclient"}, []string{"windnsclient"}, true},
|
{[]string{"windnsclient"}, []string{"windnsclient"}, true},
|
||||||
{[]string{"ALL"}, []string{"all"}, true},
|
|
||||||
{[]string{"tcp", "TCP", "UDP"}, []string{"tcp", "udp"}, true},
|
|
||||||
{[]string{"tcp/1-65535", "udp/1-65535"}, []string{"tcp", "udp"}, true},
|
|
||||||
{[]string{"udp/10-20", "udp/53-53", "tcp/50-60"}, []string{"udp/10-20", "udp/53", "tcp/50-60"}, true},
|
|
||||||
{[]string{"tcp/54-60", "udp/1-52"}, []string{"tcp/54-60", "udp/1-52"}, false},
|
|
||||||
{[]string{"ICMP", "icmp"}, []string{"icmp"}, false},
|
|
||||||
{[]string{"AllExceptLAN"}, []string{"allexceptlan", "53", "853", "windnsclient"}, true},
|
|
||||||
{[]string{"dns", "allexceptlan", "all"}, []string{"53", "853", "windnsclient", "allexceptlan", "all"}, true},
|
|
||||||
// Ports alone, for TCP and UDP.
|
|
||||||
{[]string{"53"}, []string{"53"}, true},
|
|
||||||
{[]string{"10-20", "0053", "853-853", "53"}, []string{"10-20", "53", "853"}, true},
|
|
||||||
{[]string{"54-60", "1-52", "65535"}, []string{"54-60", "1-52", "65535"}, false},
|
|
||||||
{[]string{"1-65535"}, []string{"1-65535"}, true},
|
|
||||||
{[]string{"853", "tcp/853"}, []string{"853", "tcp/853"}, false},
|
|
||||||
// Limited to the addresses of a CIDR.
|
|
||||||
{[]string{"1.1.1.0/24:53"}, []string{"1.1.1.0/24:53"}, true},
|
|
||||||
{[]string{"1.1.1.1/24:TCP/853", "[2001:DB8::1/32]:udp/10-20", "[::/0]:ICMP"}, []string{"1.1.1.0/24:tcp/853", "[2001:db8::/32]:udp/10-20", "[::/0]:icmp"}, false},
|
|
||||||
{[]string{"0.0.0.0/0:dns"}, []string{"0.0.0.0/0:53", "0.0.0.0/0:853", "0.0.0.0/0:windnsclient"}, true},
|
|
||||||
{[]string{"[::/0]:all"}, []string{"[::/0]:all"}, true},
|
|
||||||
{[]string{"10.0.0.1/32:windnsclient"}, []string{"10.0.0.1/32:windnsclient"}, true},
|
|
||||||
{[]string{"[2000::/3]:misconfigtun"}, []string{"[2000::/3]:misconfigtun"}, false},
|
|
||||||
{[]string{"1.1.1.0/24:tcp/1-65535", "1.1.1.0/24:udp/53-53"}, []string{"1.1.1.0/24:tcp", "1.1.1.0/24:udp/53"}, true},
|
|
||||||
{[]string{"1.1.1.0/24:20-30", "[::/0]:10-10"}, []string{"1.1.1.0/24:20-30", "[::/0]:10"}, false},
|
|
||||||
{[]string{"0.0.0.0/0:allexceptlan", "[::/0]:all"}, []string{"0.0.0.0/0:allexceptlan", "0.0.0.0/0:53", "0.0.0.0/0:853", "0.0.0.0/0:windnsclient", "[::/0]:all"}, true},
|
|
||||||
{[]string{"1.1.1.1/32:53", "1.1.1.1/32:0053", "1.1.1.1/24:53", "1.1.1.0/24:53", "53"}, []string{"1.1.1.1/32:53", "1.1.1.0/24:53", "53"}, true},
|
|
||||||
{[]string{"1.2.3.4/0:icmp", "[::1/0]:icmp", "icmp"}, []string{"0.0.0.0/0:icmp", "[::/0]:icmp", "icmp"}, false},
|
|
||||||
} {
|
} {
|
||||||
leaks, err := ExpandWfpBlockLeak(c.values)
|
leaks, err := ExpandWfpBlockLeak(c.values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -58,65 +32,9 @@ func TestExpandWfpBlockLeak(t *testing.T) {
|
|||||||
t.Errorf("%v: BlocksSystemDNS() = %v", c.values, got)
|
t.Errorf("%v: BlocksSystemDNS() = %v", c.values, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, value := range []string{
|
for _, value := range []string{"", "ip", "tcp", "tcp/", "tcp/0", "udp/65536", "tcp/-1", "tcp/+53", "tcp/53/1", "udp/abc", "icmp/1", "tcp:53", " dns"} {
|
||||||
"", "ip", "tcp/", "udp/65536", "tcp/-1", "tcp/+53", "tcp/53/1", "udp/abc", "icmp/0", "icmp/8", "tcp:53", " dns",
|
|
||||||
"tcp/0-10", "tcp/20-10", "tcp/5-", "tcp/-5", "tcp/1-2-3", "tcp/1-65536", "udp/ 53", "all/0", "dns/53", "windnsclient/53",
|
|
||||||
"tcp/0", "udp/0", "tcp/00", "udp/0-0", "udp/65535-1",
|
|
||||||
// Ports alone.
|
|
||||||
"0", "00", "65536", "0-10", "20-10", "5-", "-5", "-", "1-2-3", "1-65536", "+53", " 53", "53 ", "53/1", "53/tcp", "0x35",
|
|
||||||
// IP versions are CIDRs now.
|
|
||||||
"ipv4", "ipv6", "ipv4:udp", "ipv6:all", "ipv4:dns", "tcpv4", "udpv6", "icmpv4", "tcp4", "dnsv4",
|
|
||||||
// CIDRs, IPv6 ones in brackets, but neither single addresses nor
|
|
||||||
// CIDRs on their own.
|
|
||||||
"1.1.1.0/24", "[2001:db8::/32]", "1.1.1.0/24:", "[::/0]:", ":udp", "[]:udp", "[:udp", "[::/0]udp",
|
|
||||||
"1.1.1.1:53", "[::1]:53", "[2001:db8::1]:tcp/53", "2001:db8::/32:53", "::/0:all", "[1.1.1.0/24]:53",
|
|
||||||
"[2001:db8::]/32:53", "1.1.1.0/33:53", "[::/129]:53", "1.1.1.0/24:1.1.1.0/24:53", "[::/0]:[::/0]:53",
|
|
||||||
"1.1.1.0/24 :53", "1.1.1.0/24: 53", " 1.1.1.0/24:53", "1.1.1.0/24::53", "[fe80::%1/64]:53", "geoip:cn:53",
|
|
||||||
"1.1.1.0/24:udp/0", "[::/0]:icmp/8", "1.1.1.0/24:dns/53", "[::/0]:20-10", "1.1.1.0/24:ipv4:53", "1.1.1.0/24:geoip",
|
|
||||||
"[::ffff:1.1.1.0/120]:53", "[::ffff:0:0/96]:all",
|
|
||||||
} {
|
|
||||||
if _, err := ExpandWfpBlockLeak([]string{value}); err == nil {
|
if _, err := ExpandWfpBlockLeak([]string{value}); err == nil {
|
||||||
t.Errorf("%q was accepted", value)
|
t.Errorf("%q was accepted", value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLeakCovers(t *testing.T) {
|
|
||||||
for _, c := range []struct {
|
|
||||||
value string
|
|
||||||
protocol string
|
|
||||||
port uint16
|
|
||||||
covers bool
|
|
||||||
}{
|
|
||||||
{"tcp", "tcp", 1, true},
|
|
||||||
{"tcp", "udp", 53, false},
|
|
||||||
{"udp/53", "udp", 53, true},
|
|
||||||
{"udp/53", "udp", 54, false},
|
|
||||||
{"tcp/10-20", "tcp", 10, true},
|
|
||||||
{"tcp/10-20", "tcp", 20, true},
|
|
||||||
{"tcp/10-20", "tcp", 21, false},
|
|
||||||
{"tcp/10-20", "tcp", 9, false},
|
|
||||||
{"53", "udp", 53, true},
|
|
||||||
{"53", "tcp", 53, true},
|
|
||||||
{"10-20", "tcp", 15, true},
|
|
||||||
{"10-20", "udp", 21, false},
|
|
||||||
{"1.1.1.0/24:udp/53", "udp", 53, true},
|
|
||||||
{"[::/0]:tcp", "tcp", 443, true},
|
|
||||||
{"[2001:db8::/32]:53", "tcp", 53, true},
|
|
||||||
{"0.0.0.0/0:icmp", "udp", 53, false},
|
|
||||||
{"all", "udp", 53, true},
|
|
||||||
{"1.1.1.1/32:all", "tcp", 1, true},
|
|
||||||
{"allexceptlan", "udp", 53, false},
|
|
||||||
{"misconfigtun", "udp", 53, false},
|
|
||||||
{"windnsclient", "udp", 53, false},
|
|
||||||
} {
|
|
||||||
l, ok := parseLeak(c.value)
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("%q was not parsed", c.value)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if got := l.covers(c.protocol, c.port); got != c.covers {
|
|
||||||
t.Errorf("%q covers %s/%d = %v", c.value, c.protocol, c.port, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
|
|
||||||
type bind struct {
|
type bind struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
resolveFunc func(ctx context.Context, host string) (net.IP, error)
|
resolveFunc func(host string) (net.IP, error)
|
||||||
listenFunc func(ctx context.Context) (net.PacketConn, error)
|
listenFunc func(ctx context.Context) (net.PacketConn, error)
|
||||||
downFunc func() error
|
downFunc func() error
|
||||||
reserved []byte
|
reserved []byte
|
||||||
@@ -143,11 +143,7 @@ func (b *bind) ParseEndpoint(s string) (conn.Endpoint, error) {
|
|||||||
if port < 0 || port > 65535 {
|
if port < 0 || port > 65535 {
|
||||||
return nil, errors.New("invalid port " + sport)
|
return nil, errors.New("invalid port " + sport)
|
||||||
}
|
}
|
||||||
// up sets the connection's context right before it hands over the endpoint
|
ip, err := b.resolveFunc(host)
|
||||||
b.mu.Lock()
|
|
||||||
ctx := b.ctx
|
|
||||||
b.mu.Unlock()
|
|
||||||
ip, err := b.resolveFunc(ctx, host)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,12 +323,12 @@ func (h *Handler) up(ctx context.Context) error {
|
|||||||
return h.dev.Up()
|
return h.dev.Up()
|
||||||
}
|
}
|
||||||
|
|
||||||
func MakeResolveLocal(d dns.Client, streamSettings *internet.MemoryStreamConfig) func(ctx context.Context, host string) (net.IP, error) {
|
func MakeResolveLocal(d dns.Client, streamSettings *internet.MemoryStreamConfig) func(host string) (net.IP, error) {
|
||||||
return func(ctx context.Context, host string) (net.IP, error) {
|
return func(host string) (net.IP, error) {
|
||||||
if ip := net.ParseIP(host); ip != nil {
|
if ip := net.ParseIP(host); ip != nil {
|
||||||
return ip, nil
|
return ip, nil
|
||||||
}
|
}
|
||||||
ips, err := lookupIPContext(ctx, d, host)
|
ips, _, err := d.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -368,27 +368,6 @@ func MakeResolveLocal(d dns.Client, streamSettings *internet.MemoryStreamConfig)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupIPContext(ctx context.Context, d dns.Client, host string) ([]net.IP, error) {
|
|
||||||
if err := ctx.Err(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
type result struct {
|
|
||||||
ips []net.IP
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
done := make(chan result, 1)
|
|
||||||
go func() {
|
|
||||||
ips, _, err := d.LookupIP(host, dns.IPOption{IPv4Enable: true, IPv6Enable: true})
|
|
||||||
done <- result{ips, err}
|
|
||||||
}()
|
|
||||||
select {
|
|
||||||
case r := <-done:
|
|
||||||
return r.ips, r.err
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type UDPConnClient struct {
|
type UDPConnClient struct {
|
||||||
net.PacketConn
|
net.PacketConn
|
||||||
Dest *net.UDPAddr
|
Dest *net.UDPAddr
|
||||||
|
|||||||
@@ -661,7 +661,7 @@ func TestVlessRealityFingerprints(t *testing.T) {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// A blocked connection must close after finalRules.delay even while the client keeps it open and sends nothing.
|
// A blocked connection must be closed after blockDelay even while the client keeps it open and sends nothing.
|
||||||
func TestVlessBlockDelay(t *testing.T) {
|
func TestVlessBlockDelay(t *testing.T) {
|
||||||
tcpServer := tcp.Server{
|
tcpServer := tcp.Server{
|
||||||
MsgProcessor: xor,
|
MsgProcessor: xor,
|
||||||
@@ -671,7 +671,7 @@ func TestVlessBlockDelay(t *testing.T) {
|
|||||||
defer tcpServer.Close()
|
defer tcpServer.Close()
|
||||||
|
|
||||||
for _, delay := range []uint64{0, 1} {
|
for _, delay := range []uint64{0, 1} {
|
||||||
t.Run(fmt.Sprint("delay=", delay), func(t *testing.T) {
|
t.Run(fmt.Sprint("blockDelay=", delay), func(t *testing.T) {
|
||||||
userID := protocol.NewID(uuid.New())
|
userID := protocol.NewID(uuid.New())
|
||||||
serverPort := tcp.PickPort()
|
serverPort := tcp.PickPort()
|
||||||
serverConfig := &core.Config{
|
serverConfig := &core.Config{
|
||||||
@@ -698,7 +698,7 @@ func TestVlessBlockDelay(t *testing.T) {
|
|||||||
FinalRules: []*freedom.FinalRuleConfig{
|
FinalRules: []*freedom.FinalRuleConfig{
|
||||||
{
|
{
|
||||||
Action: freedom.RuleAction_Block,
|
Action: freedom.RuleAction_Block,
|
||||||
Delay: &freedom.Range{Min: delay, Max: delay},
|
BlockDelay: &freedom.Range{Min: delay, Max: delay},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -726,15 +726,15 @@ func TestVlessBlockDelay(t *testing.T) {
|
|||||||
Port: dest.Port,
|
Port: dest.Port,
|
||||||
}, &encoding.Addons{}))
|
}, &encoding.Addons{}))
|
||||||
|
|
||||||
blockDuration := time.Duration(delay) * time.Second
|
blockDelay := time.Duration(delay) * time.Second
|
||||||
common.Must(conn.SetReadDeadline(time.Now().Add(blockDuration + 5*time.Second)))
|
common.Must(conn.SetReadDeadline(time.Now().Add(blockDelay + 5*time.Second)))
|
||||||
_, err = io.Copy(io.Discard, conn)
|
_, err = io.Copy(io.Discard, conn)
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
if err, ok := err.(net.Error); ok && err.Timeout() {
|
if err, ok := err.(net.Error); ok && err.Timeout() {
|
||||||
t.Fatalf("connection is still open after %v", elapsed)
|
t.Fatalf("connection is still open after %v", elapsed)
|
||||||
}
|
}
|
||||||
if elapsed < blockDuration {
|
if elapsed < blockDelay {
|
||||||
t.Fatalf("connection is closed after %v, before finalRules.delay", elapsed)
|
t.Fatalf("connection is closed after %v, before blockDelay", elapsed)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
goerrors "errors"
|
goerrors "errors"
|
||||||
"io"
|
"io"
|
||||||
mrand "math/rand"
|
mrand "math/rand"
|
||||||
stdnet "net"
|
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -165,7 +164,7 @@ func (c *udpHopConn) recv(conn net.PacketConn) {
|
|||||||
n, addr, err := conn.ReadFrom(p)
|
n, addr, err := conn.ReadFrom(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pool.Put(p[:cap(p)])
|
pool.Put(p[:cap(p)])
|
||||||
if c.closed() || goerrors.Is(err, stdnet.ErrClosed) {
|
if c.closed() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var netErr net.Error
|
var netErr net.Error
|
||||||
|
|||||||
@@ -441,8 +441,6 @@ type sessionTransport struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
client *splithttp.XmuxClient
|
client *splithttp.XmuxClient
|
||||||
closed bool
|
closed bool
|
||||||
slowAt, retiredAt time.Time
|
|
||||||
movedAt time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||||
@@ -452,7 +450,7 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|||||||
return nil, errSessionClosed
|
return nil, errSessionClosed
|
||||||
}
|
}
|
||||||
client := t.client
|
client := t.client
|
||||||
if client == nil || client.XmuxConn.IsClosed() || client.LeftRequests.Add(-1) < 0 || !client.UnreusableAt.IsZero() && time.Now().After(client.UnreusableAt) {
|
if client == nil || client.LeftRequests.Add(-1) < 0 || !client.UnreusableAt.IsZero() && time.Now().After(client.UnreusableAt) {
|
||||||
t.pool.mu.Lock()
|
t.pool.mu.Lock()
|
||||||
t.client = t.pool.xmux.GetXmuxClient(r.Context())
|
t.client = t.pool.xmux.GetXmuxClient(r.Context())
|
||||||
t.pool.mu.Unlock()
|
t.pool.mu.Unlock()
|
||||||
@@ -460,7 +458,6 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|||||||
t.client.LeftRequests.Add(-1)
|
t.client.LeftRequests.Add(-1)
|
||||||
if client != nil {
|
if client != nil {
|
||||||
client.DoneRunning()
|
client.DoneRunning()
|
||||||
t.movedAt = time.Now()
|
|
||||||
}
|
}
|
||||||
client = t.client
|
client = t.client
|
||||||
}
|
}
|
||||||
@@ -474,22 +471,6 @@ func (t *sessionTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|||||||
return conn.roundTrip(r)
|
return conn.roundTrip(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *sessionTransport) stalled(began time.Time) {
|
|
||||||
t.mu.Lock()
|
|
||||||
defer t.mu.Unlock()
|
|
||||||
if began.Before(t.movedAt) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
slow := now.Sub(t.slowAt) < 3*time.Second
|
|
||||||
t.slowAt = now
|
|
||||||
if !slow || t.client == nil || now.Sub(t.retiredAt) < 2*time.Second {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
t.retiredAt = now
|
|
||||||
t.client.XmuxConn.(*poolConn).Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *sessionTransport) Close() {
|
func (t *sessionTransport) Close() {
|
||||||
t.mu.Lock()
|
t.mu.Lock()
|
||||||
defer t.mu.Unlock()
|
defer t.mu.Unlock()
|
||||||
|
|||||||
@@ -987,103 +987,3 @@ func TestDriveXmuxOverH3(t *testing.T) {
|
|||||||
}
|
}
|
||||||
waitFor(t, "the first connection of XMUX to close", func() bool { return server.closed.Load() == 1 })
|
waitFor(t, "the first connection of XMUX to close", func() bool { return server.closed.Load() == 1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
type stallConn struct{}
|
|
||||||
|
|
||||||
func (stallConn) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
||||||
<-r.Context().Done()
|
|
||||||
return nil, r.Context().Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
type okConn struct{}
|
|
||||||
|
|
||||||
func (okConn) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
||||||
return &http.Response{
|
|
||||||
StatusCode: http.StatusOK,
|
|
||||||
Header: make(http.Header),
|
|
||||||
Body: io.NopCloser(strings.NewReader(`{"id":"file-1"}`)),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSessionRetiresStalledConn(t *testing.T) {
|
|
||||||
made := atomic.Int32{}
|
|
||||||
transport := &sessionTransport{pool: newConnPool(nil, func(connCtx context.Context) *http.Client {
|
|
||||||
if made.Add(1) == 1 {
|
|
||||||
return &http.Client{Transport: stallConn{}}
|
|
||||||
}
|
|
||||||
return &http.Client{Transport: okConn{}}
|
|
||||||
})}
|
|
||||||
storage := &driveStorage{
|
|
||||||
driveShared: &driveShared{
|
|
||||||
token: "token",
|
|
||||||
tokenExpiry: time.Now().Add(time.Hour),
|
|
||||||
backoff: time.Millisecond,
|
|
||||||
},
|
|
||||||
client: &http.Client{Transport: transport},
|
|
||||||
inflight: make(chan struct{}, 1),
|
|
||||||
reserved: make(chan struct{}, 1),
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.WithValue(context.Background(), attemptKey{}, 100*time.Millisecond)
|
|
||||||
code, body, err := storage.do(ctx, http.MethodPost, "http://drive.invalid/files", "", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("the first request over a stalled connection: %v", err)
|
|
||||||
}
|
|
||||||
if code != http.StatusOK || string(body) != `{"id":"file-1"}` {
|
|
||||||
t.Fatalf("the stalled connection gave %d %q", code, body)
|
|
||||||
}
|
|
||||||
if n := made.Load(); n != 2 {
|
|
||||||
t.Fatalf("a stalled connection took %d connections, want it retired and one made after it", n)
|
|
||||||
}
|
|
||||||
if _, _, err := storage.do(ctx, http.MethodPost, "http://drive.invalid/files", "", nil); err != nil {
|
|
||||||
t.Fatalf("the request after the retirement: %v", err)
|
|
||||||
}
|
|
||||||
if n := made.Load(); n != 2 {
|
|
||||||
t.Fatalf("%d connections were made, want the healthy one kept", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSessionIgnoresStallOfRetiredConn(t *testing.T) {
|
|
||||||
var made atomic.Int32
|
|
||||||
transport := &sessionTransport{pool: newConnPool(nil, func(connCtx context.Context) *http.Client {
|
|
||||||
made.Add(1)
|
|
||||||
return &http.Client{Transport: okConn{}}
|
|
||||||
})}
|
|
||||||
send := func() {
|
|
||||||
req, err := http.NewRequest(http.MethodGet, "http://drive.invalid/files", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
resp, err := transport.RoundTrip(req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
send()
|
|
||||||
old := time.Now()
|
|
||||||
time.Sleep(2 * time.Millisecond)
|
|
||||||
transport.stalled(old)
|
|
||||||
transport.stalled(old)
|
|
||||||
send()
|
|
||||||
if n := made.Load(); n != 2 {
|
|
||||||
t.Fatalf("a stalled connection took %d connections, want it retired and one made after it", n)
|
|
||||||
}
|
|
||||||
transport.mu.Lock()
|
|
||||||
transport.slowAt, transport.retiredAt = time.Time{}, time.Time{}
|
|
||||||
transport.mu.Unlock()
|
|
||||||
transport.stalled(old)
|
|
||||||
transport.stalled(old)
|
|
||||||
send()
|
|
||||||
if n := made.Load(); n != 2 {
|
|
||||||
t.Fatalf("the stall of a retired connection took the healthy one too, %d connections", n)
|
|
||||||
}
|
|
||||||
time.Sleep(2 * time.Millisecond)
|
|
||||||
fresh := time.Now()
|
|
||||||
transport.stalled(fresh)
|
|
||||||
transport.stalled(fresh)
|
|
||||||
send()
|
|
||||||
if n := made.Load(); n != 3 {
|
|
||||||
t.Fatalf("a stall of the current connection made %d connections, want it retired", n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -24,8 +24,7 @@ const (
|
|||||||
driveBoundary = "xdrive-boundary"
|
driveBoundary = "xdrive-boundary"
|
||||||
drivePageSize = 1000
|
drivePageSize = 1000
|
||||||
driveMaxAttempts = 8
|
driveMaxAttempts = 8
|
||||||
driveMaxInflight = 64
|
driveMaxInflight = 32
|
||||||
driveReservedInflight = 8
|
|
||||||
driveInlineLimit = 12000
|
driveInlineLimit = 12000
|
||||||
// driveTimeout ends a request that makes no progress for that long
|
// driveTimeout ends a request that makes no progress for that long
|
||||||
driveTimeout = 60 * time.Second
|
driveTimeout = 60 * time.Second
|
||||||
@@ -37,21 +36,13 @@ var (
|
|||||||
driveFilesURL = "https://www.googleapis.com/drive/v3/files"
|
driveFilesURL = "https://www.googleapis.com/drive/v3/files"
|
||||||
driveUploadURL = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name"
|
driveUploadURL = "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name"
|
||||||
driveInitialBackoff = 200 * time.Millisecond
|
driveInitialBackoff = 200 * time.Millisecond
|
||||||
driveCooldown = 2 * time.Second
|
|
||||||
driveSlotRead = 3 * time.Second
|
|
||||||
driveSlotWrite = 5 * time.Second
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type laneKey struct{}
|
|
||||||
|
|
||||||
type attemptKey struct{}
|
|
||||||
|
|
||||||
type driveStorage struct {
|
type driveStorage struct {
|
||||||
*driveShared
|
*driveShared
|
||||||
// a session has a client and inflight of its own, see session
|
// a session has a client and inflight of its own, see session
|
||||||
client *http.Client
|
client *http.Client
|
||||||
inflight chan struct{}
|
inflight chan struct{}
|
||||||
reserved chan struct{}
|
|
||||||
release func()
|
release func()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,9 +65,6 @@ type driveShared struct {
|
|||||||
|
|
||||||
idMu sync.Mutex
|
idMu sync.Mutex
|
||||||
ids map[string]string
|
ids map[string]string
|
||||||
|
|
||||||
coolMu sync.Mutex
|
|
||||||
coolUntil time.Time
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config) (*driveStorage, error) {
|
func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config) (*driveStorage, error) {
|
||||||
@@ -112,7 +100,6 @@ func newDriveStorage(streamSettings *internet.MemoryStreamConfig, config *Config
|
|||||||
},
|
},
|
||||||
client: client,
|
client: client,
|
||||||
inflight: make(chan struct{}, driveMaxInflight),
|
inflight: make(chan struct{}, driveMaxInflight),
|
||||||
reserved: make(chan struct{}, driveReservedInflight),
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +111,6 @@ func (s *driveStorage) session() Storage {
|
|||||||
driveShared: s.driveShared,
|
driveShared: s.driveShared,
|
||||||
client: &http.Client{Transport: transport},
|
client: &http.Client{Transport: transport},
|
||||||
inflight: make(chan struct{}, driveMaxInflight),
|
inflight: make(chan struct{}, driveMaxInflight),
|
||||||
reserved: make(chan struct{}, driveReservedInflight),
|
|
||||||
release: transport.Close,
|
release: transport.Close,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -233,17 +219,6 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
|||||||
backoff := s.backoff
|
backoff := s.backoff
|
||||||
var lastErr error
|
var lastErr error
|
||||||
|
|
||||||
s.coolMu.Lock()
|
|
||||||
until := s.coolUntil
|
|
||||||
s.coolMu.Unlock()
|
|
||||||
if d := time.Until(until); d > 0 {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return 0, nil, ctx.Err()
|
|
||||||
case <-time.After(d):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for attempt := 0; attempt < driveMaxAttempts; attempt++ {
|
for attempt := 0; attempt < driveMaxAttempts; attempt++ {
|
||||||
if attempt > 0 {
|
if attempt > 0 {
|
||||||
select {
|
select {
|
||||||
@@ -263,12 +238,8 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pool := s.inflight
|
|
||||||
if ctx.Value(laneKey{}) != nil {
|
|
||||||
pool = s.reserved
|
|
||||||
}
|
|
||||||
select {
|
select {
|
||||||
case pool <- struct{}{}:
|
case s.inflight <- struct{}{}:
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return 0, nil, ctx.Err()
|
return 0, nil, ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -277,14 +248,8 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
|||||||
if body != nil {
|
if body != nil {
|
||||||
reader = bytes.NewReader(body)
|
reader = bytes.NewReader(body)
|
||||||
}
|
}
|
||||||
attemptCtx, cancel := ctx, context.CancelFunc(func() {})
|
req, err := http.NewRequestWithContext(ctx, method, target, reader)
|
||||||
if d, ok := ctx.Value(attemptKey{}).(time.Duration); ok {
|
|
||||||
attemptCtx, cancel = context.WithTimeout(ctx, d)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(attemptCtx, method, target, reader)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
|
||||||
<-pool
|
|
||||||
return 0, nil, errors.New("failed to build a Drive request").Base(err)
|
return 0, nil, errors.New("failed to build a Drive request").Base(err)
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", "Bearer "+token)
|
req.Header.Set("Authorization", "Bearer "+token)
|
||||||
@@ -292,17 +257,9 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
|||||||
req.Header.Set("Content-Type", contentType)
|
req.Header.Set("Content-Type", contentType)
|
||||||
}
|
}
|
||||||
|
|
||||||
began := time.Now()
|
|
||||||
resp, err := s.client.Do(req)
|
resp, err := s.client.Do(req)
|
||||||
<-pool
|
<-s.inflight
|
||||||
if err != nil {
|
if err != nil {
|
||||||
timedOut := attemptCtx.Err() == context.DeadlineExceeded && ctx.Err() == nil
|
|
||||||
cancel()
|
|
||||||
if timedOut {
|
|
||||||
if t, ok := s.client.Transport.(*sessionTransport); ok {
|
|
||||||
t.stalled(began)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return 0, nil, ctx.Err()
|
return 0, nil, ctx.Err()
|
||||||
}
|
}
|
||||||
@@ -313,7 +270,6 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
|||||||
}
|
}
|
||||||
payload, err := io.ReadAll(resp.Body)
|
payload, err := io.ReadAll(resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
cancel()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return 0, nil, ctx.Err()
|
return 0, nil, ctx.Err()
|
||||||
@@ -330,19 +286,13 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
|||||||
if resp.StatusCode == http.StatusForbidden && rateLimited(payload) {
|
if resp.StatusCode == http.StatusForbidden && rateLimited(payload) {
|
||||||
lastErr = errors.New("Drive is rate limiting: ", string(payload))
|
lastErr = errors.New("Drive is rate limiting: ", string(payload))
|
||||||
errors.LogWarning(ctx, "rate limited by Drive, attempt ",
|
errors.LogWarning(ctx, "rate limited by Drive, attempt ",
|
||||||
attempt+1, " of ", driveMaxAttempts, ", ", method, " ", len(body),
|
attempt+1, " of ", driveMaxAttempts)
|
||||||
" bytes, body read ", strings.Contains(target, "alt=media"),
|
|
||||||
", lane ", ctx.Value(laneKey{}) != nil, ": ", string(payload[:min(len(payload), 300)]))
|
|
||||||
s.cool()
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if retryableStatus(resp.StatusCode) {
|
if retryableStatus(resp.StatusCode) {
|
||||||
lastErr = errors.New("Drive answered ", resp.StatusCode, ": ", string(payload))
|
lastErr = errors.New("Drive answered ", resp.StatusCode, ": ", string(payload))
|
||||||
errors.LogWarning(ctx, "retrying after Drive answered ", resp.StatusCode,
|
errors.LogWarning(ctx, "retrying after Drive answered ", resp.StatusCode,
|
||||||
", attempt ", attempt+1, " of ", driveMaxAttempts)
|
", attempt ", attempt+1, " of ", driveMaxAttempts)
|
||||||
if resp.StatusCode == http.StatusTooManyRequests {
|
|
||||||
s.cool()
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return resp.StatusCode, payload, nil
|
return resp.StatusCode, payload, nil
|
||||||
@@ -351,12 +301,6 @@ func (s *driveStorage) do(ctx context.Context, method, target, contentType strin
|
|||||||
return 0, nil, lastErr
|
return 0, nil, lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *driveStorage) cool() {
|
|
||||||
s.coolMu.Lock()
|
|
||||||
s.coolUntil = time.Now().Add(jitter(driveCooldown))
|
|
||||||
s.coolMu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *driveStorage) invalidateToken() {
|
func (s *driveStorage) invalidateToken() {
|
||||||
s.tokenMu.Lock()
|
s.tokenMu.Lock()
|
||||||
s.token = ""
|
s.token = ""
|
||||||
@@ -598,27 +542,12 @@ func (s *driveStorage) Delete(ctx context.Context, name string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
sem := make(chan struct{}, 8)
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
var mu sync.Mutex
|
|
||||||
var first error
|
|
||||||
for childName, file := range children {
|
for childName, file := range children {
|
||||||
wg.Add(1)
|
if err := s.deleteID(ctx, childName, file.id); err != nil {
|
||||||
sem <- struct{}{}
|
return err
|
||||||
go func(name, id string) {
|
|
||||||
defer wg.Done()
|
|
||||||
defer func() { <-sem }()
|
|
||||||
if err := s.deleteID(ctx, name, id); err != nil {
|
|
||||||
mu.Lock()
|
|
||||||
if first == nil {
|
|
||||||
first = err
|
|
||||||
}
|
}
|
||||||
mu.Unlock()
|
|
||||||
}
|
}
|
||||||
}(childName, file.id)
|
return nil
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return first
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *driveStorage) List(ctx context.Context, prefix string) ([]Entry, error) {
|
func (s *driveStorage) List(ctx context.Context, prefix string) ([]Entry, error) {
|
||||||
@@ -675,9 +604,6 @@ func (s *driveStorage) makeSlot(ctx context.Context, name string) (string, error
|
|||||||
|
|
||||||
// setSlot implements slotStore, a description and a body go in one request
|
// setSlot implements slotStore, a description and a body go in one request
|
||||||
func (s *driveStorage) setSlot(ctx context.Context, id, description string, body []byte) error {
|
func (s *driveStorage) setSlot(ctx context.Context, id, description string, body []byte) error {
|
||||||
if body == nil {
|
|
||||||
ctx = context.WithValue(ctx, attemptKey{}, driveSlotWrite)
|
|
||||||
}
|
|
||||||
metadata, err := json.Marshal(map[string]string{"description": description})
|
metadata, err := json.Marshal(map[string]string{"description": description})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("failed to build the slot metadata").Base(err)
|
return errors.New("failed to build the slot metadata").Base(err)
|
||||||
@@ -705,7 +631,6 @@ func (s *driveStorage) setSlot(ctx context.Context, id, description string, body
|
|||||||
|
|
||||||
// readDescription implements slotStore
|
// readDescription implements slotStore
|
||||||
func (s *driveStorage) readDescription(ctx context.Context, id string) (string, error) {
|
func (s *driveStorage) readDescription(ctx context.Context, id string) (string, error) {
|
||||||
ctx = context.WithValue(ctx, attemptKey{}, driveSlotRead)
|
|
||||||
status, payload, err := s.do(ctx, http.MethodGet,
|
status, payload, err := s.do(ctx, http.MethodGet,
|
||||||
s.filesURL+"/"+url.PathEscape(id)+"?fields=description&supportsAllDrives=true", "", nil)
|
s.filesURL+"/"+url.PathEscape(id)+"?fields=description&supportsAllDrives=true", "", nil)
|
||||||
switch {
|
switch {
|
||||||
|
|||||||
@@ -7,13 +7,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
|
||||||
"net/http/httptrace"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"sort"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -348,130 +343,3 @@ func TestLiveDriveListLag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Logf("worst listing lag: %v", worst.Round(time.Millisecond))
|
t.Logf("worst listing lag: %v", worst.Round(time.Millisecond))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLiveDriveLatencyFloor(t *testing.T) {
|
|
||||||
config := liveDriveConfig(t)
|
|
||||||
root, err := newDriveStorage(&internet.MemoryStreamConfig{}, config)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer root.Close()
|
|
||||||
store := root.session().(*driveStorage)
|
|
||||||
defer store.Close()
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
const probe = "latency-floor/probe"
|
|
||||||
id, err := store.makeSlot(ctx, probe)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer store.deleteID(ctx, flatten(probe), id)
|
|
||||||
|
|
||||||
small := newSlotFrame(1, slotInline, randomPayload(700)).encode()
|
|
||||||
large := newSlotFrame(1, slotInline, randomPayload(slotInlineLimit)).encode()
|
|
||||||
file := store.filesURL + "/" + url.PathEscape(id)
|
|
||||||
|
|
||||||
type sample struct {
|
|
||||||
total, ttfb time.Duration
|
|
||||||
reused bool
|
|
||||||
}
|
|
||||||
measure := func(label string, op func(context.Context) error) []sample {
|
|
||||||
var out []sample
|
|
||||||
for i := 0; i < 28; i++ {
|
|
||||||
var wrote, first time.Time
|
|
||||||
var reused bool
|
|
||||||
trace := &httptrace.ClientTrace{
|
|
||||||
GotConn: func(info httptrace.GotConnInfo) { reused = info.Reused },
|
|
||||||
WroteRequest: func(httptrace.WroteRequestInfo) { wrote = time.Now() },
|
|
||||||
GotFirstResponseByte: func() { first = time.Now() },
|
|
||||||
}
|
|
||||||
start := time.Now()
|
|
||||||
err := op(httptrace.WithClientTrace(ctx, trace))
|
|
||||||
total := time.Since(start)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("latency-floor: %s sample failed: %v", label, err)
|
|
||||||
} else if i >= 3 {
|
|
||||||
var ttfb time.Duration
|
|
||||||
if !wrote.IsZero() && !first.IsZero() {
|
|
||||||
ttfb = first.Sub(wrote)
|
|
||||||
}
|
|
||||||
out = append(out, sample{total, ttfb, reused})
|
|
||||||
}
|
|
||||||
time.Sleep(250 * time.Millisecond)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
report := func(label string, s []sample) time.Duration {
|
|
||||||
if len(s) == 0 {
|
|
||||||
t.Logf("latency-floor: %-30s no samples", label)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
totals := make([]time.Duration, len(s))
|
|
||||||
ttfbs := make([]time.Duration, len(s))
|
|
||||||
reused := 0
|
|
||||||
for i, x := range s {
|
|
||||||
totals[i], ttfbs[i] = x.total, x.ttfb
|
|
||||||
if x.reused {
|
|
||||||
reused++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Slice(totals, func(a, b int) bool { return totals[a] < totals[b] })
|
|
||||||
sort.Slice(ttfbs, func(a, b int) bool { return ttfbs[a] < ttfbs[b] })
|
|
||||||
q := func(v []time.Duration, p float64) time.Duration { return v[min(int(p*float64(len(v))), len(v)-1)] }
|
|
||||||
t.Logf("latency-floor: %-30s n=%d min=%v p50=%v p95=%v ttfb_p50=%v reused=%d/%d",
|
|
||||||
label, len(s), totals[0], q(totals, 0.5), q(totals, 0.95), q(ttfbs, 0.5), reused, len(s))
|
|
||||||
return q(ttfbs, 0.5)
|
|
||||||
}
|
|
||||||
patch := func(target string, doc map[string]any) func(context.Context) error {
|
|
||||||
body, _ := json.Marshal(doc)
|
|
||||||
return func(ctx context.Context) error {
|
|
||||||
status, reply, err := store.do(ctx, http.MethodPatch, target, "application/json; charset=UTF-8", body)
|
|
||||||
if err == nil && status != http.StatusOK {
|
|
||||||
err = fmt.Errorf("status %d: %s", status, reply)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get := func(target string) func(context.Context) error {
|
|
||||||
return func(ctx context.Context) error {
|
|
||||||
status, reply, err := store.do(ctx, http.MethodGet, target, "", nil)
|
|
||||||
if err == nil && status != http.StatusOK {
|
|
||||||
err = fmt.Errorf("status %d: %s", status, reply)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
proto := ""
|
|
||||||
netFloor := report("net floor (401, no auth)", measure("net", func(ctx context.Context) error {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.googleapis.com/drive/v3/about?fields=user", nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp, err := store.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
proto = resp.Proto
|
|
||||||
io.Copy(io.Discard, resp.Body)
|
|
||||||
return resp.Body.Close()
|
|
||||||
}))
|
|
||||||
t.Logf("latency-floor: negotiated protocol %s", proto)
|
|
||||||
readDesc := report("GET description (prod)", measure("get", func(ctx context.Context) error {
|
|
||||||
_, err := store.readDescription(ctx, id)
|
|
||||||
return err
|
|
||||||
}))
|
|
||||||
report("GET fields=id", measure("getid", get(file+"?fields=id&supportsAllDrives=true")))
|
|
||||||
report("GET description, no allDrives", measure("getnoall", get(file+"?fields=description")))
|
|
||||||
write1k := report("PATCH inline 1 KB (prod)", measure("p1k", func(ctx context.Context) error {
|
|
||||||
return store.setSlot(ctx, id, small, nil)
|
|
||||||
}))
|
|
||||||
write14k := report("PATCH inline 14 KB (prod)", measure("p14k", func(ctx context.Context) error {
|
|
||||||
return store.setSlot(ctx, id, large, nil)
|
|
||||||
}))
|
|
||||||
report("PATCH 1 KB, no allDrives", measure("pnoall", patch(file+"?fields=id", map[string]any{"description": small})))
|
|
||||||
report("PATCH name 200 B", measure("pname", patch(file+"?fields=id&supportsAllDrives=true", map[string]any{"name": strings.Repeat("n", 200)})))
|
|
||||||
report("PATCH appProperties 100 B", measure("pprop", patch(file+"?fields=id&supportsAllDrives=true", map[string]any{"appProperties": map[string]string{"k": strings.Repeat("a", 100)}})))
|
|
||||||
t.Logf("latency-floor: derived: write backend ~ %v, read backend ~ %v, 14 KB payload cost ~ %v",
|
|
||||||
write1k-netFloor, readDesc-netFloor, write14k-write1k)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func (s *localStorage) Get(ctx context.Context, name string) ([]byte, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
data, err := readFile(ctx, full)
|
data, err := os.ReadFile(full)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return nil, errNotFound
|
return nil, errNotFound
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
//go:build !windows
|
|
||||||
|
|
||||||
package xdrive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func readFile(_ context.Context, name string) ([]byte, error) {
|
|
||||||
return os.ReadFile(name)
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package xdrive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
// readFile retries os.ReadFile for up to 2 seconds on sharing violations:
|
|
||||||
// os.Rename (MoveFileEx) in Put holds the file with DELETE access, which
|
|
||||||
// os.ReadFile does not share, until a moment after its new name appears in List
|
|
||||||
func readFile(ctx context.Context, name string) ([]byte, error) {
|
|
||||||
deadline := time.Now().Add(2 * time.Second)
|
|
||||||
delay := time.Millisecond
|
|
||||||
for {
|
|
||||||
data, err := os.ReadFile(name)
|
|
||||||
if !errors.Is(err, windows.ERROR_SHARING_VIOLATION) || time.Now().Add(delay).After(deadline) {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, err
|
|
||||||
case <-time.After(delay):
|
|
||||||
}
|
|
||||||
delay = min(2*delay, 100*time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package xdrive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLocalSharingViolation(t *testing.T) {
|
|
||||||
storage, err := newLocalStorage(t.TempDir())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("newLocalStorage: %v", err)
|
|
||||||
}
|
|
||||||
const name = "streams/session/c2s/000000000.seg"
|
|
||||||
if err := storage.Put(context.Background(), name, []byte("segment")); err != nil {
|
|
||||||
t.Fatalf("Put: %v", err)
|
|
||||||
}
|
|
||||||
full, err := storage.resolve(name)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("resolve: %v", err)
|
|
||||||
}
|
|
||||||
path, err := windows.UTF16PtrFromString(full)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("UTF16PtrFromString: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// os.Rename holds the file like this until a moment after its new name
|
|
||||||
// appears
|
|
||||||
handle, err := windows.CreateFile(path, windows.DELETE,
|
|
||||||
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
|
|
||||||
nil, windows.OPEN_EXISTING, 0, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("CreateFile: %v", err)
|
|
||||||
}
|
|
||||||
var once sync.Once
|
|
||||||
release := func() { once.Do(func() { windows.CloseHandle(handle) }) }
|
|
||||||
defer release()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
||||||
defer cancel()
|
|
||||||
if _, err := storage.Get(ctx, name); !errors.Is(err, windows.ERROR_SHARING_VIOLATION) {
|
|
||||||
t.Fatalf("Get returned %v while the file is held, want a sharing violation", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.AfterFunc(100*time.Millisecond, release)
|
|
||||||
data, err := storage.Get(context.Background(), name)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("Get: %v", err)
|
|
||||||
}
|
|
||||||
if string(data) != "segment" {
|
|
||||||
t.Fatalf("Get returned %q, want %q", data, "segment")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ const (
|
|||||||
defaultConcurrency = 8
|
defaultConcurrency = 8
|
||||||
|
|
||||||
maxSegmentBytes = 16 * 1024 * 1024
|
maxSegmentBytes = 16 * 1024 * 1024
|
||||||
maxConcurrency = 256
|
maxConcurrency = 64
|
||||||
)
|
)
|
||||||
|
|
||||||
type params struct {
|
type params struct {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common/errors"
|
"github.com/xtls/xray-core/common/errors"
|
||||||
@@ -23,11 +22,12 @@ import (
|
|||||||
// reuses a slot only once that says what was in it before was taken
|
// reuses a slot only once that says what was in it before was taken
|
||||||
|
|
||||||
const (
|
const (
|
||||||
slotMagic = "XDRVSL01"
|
slotMagic = "XDRVSL02"
|
||||||
slotHeaderLen = len(slotMagic) + 8 + 1 + 4 + 4
|
slotHeaderLen = len(slotMagic) + 8 + 1 + 4 + 4
|
||||||
// slotInlineLimit is how large a segment is at most to go in the description
|
// slotInlineLimit is how large a segment is at most to go in the description
|
||||||
// of its slot, which takes one request less than a body
|
// of its slot, which takes one request less than a body
|
||||||
slotInlineLimit = 14 << 10
|
slotInlineLimit = 14 << 10
|
||||||
|
slotDepthLimit = 16
|
||||||
// inlineRampSegments segments of one size in a row double it, from
|
// inlineRampSegments segments of one size in a row double it, from
|
||||||
// slotInlineLimit up to segmentBytes
|
// slotInlineLimit up to segmentBytes
|
||||||
inlineRampSegments = 8
|
inlineRampSegments = 8
|
||||||
@@ -41,26 +41,26 @@ const (
|
|||||||
var (
|
var (
|
||||||
// the head is asked for between slotHuntFloor and slotHuntCeiling, a segment
|
// the head is asked for between slotHuntFloor and slotHuntCeiling, a segment
|
||||||
// behind it at about the pace that segments come
|
// behind it at about the pace that segments come
|
||||||
slotHuntFloor = 10 * time.Millisecond
|
slotHuntFloor = 50 * time.Millisecond
|
||||||
slotHuntCeiling = 25 * time.Millisecond
|
slotHuntCeiling = 100 * time.Millisecond
|
||||||
slotTailMin = 150 * time.Millisecond
|
slotTailMin = 150 * time.Millisecond
|
||||||
slotTailMax = 800 * time.Millisecond
|
slotTailMax = 800 * time.Millisecond
|
||||||
slotFetchAhead = 12
|
slotFetchAhead = 10
|
||||||
|
|
||||||
slotAckRetry = 30 * time.Millisecond
|
slotAckRetry = 30 * time.Millisecond
|
||||||
slotWatermarkTTL = 100 * time.Millisecond
|
slotWatermarkTTL = 100 * time.Millisecond
|
||||||
slotStallTimeout = 60 * time.Second
|
slotStallTimeout = 60 * time.Second
|
||||||
slotCloseGrace = 10 * time.Second
|
slotCloseGrace = 10 * time.Second
|
||||||
slotGoneTimeout = 60 * time.Second
|
slotGoneTimeout = 60 * time.Second
|
||||||
slotBodyHedge = 2 * time.Second
|
|
||||||
|
|
||||||
slotReadAhead int64 = 3
|
// a failed write of a segment is tried slotWriteAttempts times
|
||||||
slotReadAheadPoll = 40 * time.Millisecond
|
slotWriteAttempts = 3
|
||||||
slotReadAheadBurst = 500 * time.Millisecond
|
// a read of the head that has not answered for slotHedgeTick is asked a
|
||||||
|
// second time, for slotHedgeBudget at most
|
||||||
|
slotHedgeTick = 250 * time.Millisecond
|
||||||
|
slotHedgeBudget = time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var slotWriteInflight = 12
|
|
||||||
|
|
||||||
// slotStore is a storage of objects that are written and read again by id,
|
// slotStore is a storage of objects that are written and read again by id,
|
||||||
// each with a description and a body
|
// each with a description and a body
|
||||||
type slotStore interface {
|
type slotStore interface {
|
||||||
@@ -150,7 +150,7 @@ func decodeRing(b []byte) (ring, bool) {
|
|||||||
|
|
||||||
// makeRing makes the slots and watermarks of a session, all at once
|
// makeRing makes the slots and watermarks of a session, all at once
|
||||||
func makeRing(ctx context.Context, store slotStore, session string, p params) (ring, error) {
|
func makeRing(ctx context.Context, store slotStore, session string, p params) (ring, error) {
|
||||||
depth := max(p.concurrency, 2)
|
depth := min(max(p.concurrency, 2), slotDepthLimit)
|
||||||
r := ring{
|
r := ring{
|
||||||
Up: slotTable{Slots: make([]string, depth)},
|
Up: slotTable{Slots: make([]string, depth)},
|
||||||
Down: slotTable{Slots: make([]string, depth)},
|
Down: slotTable{Slots: make([]string, depth)},
|
||||||
@@ -184,7 +184,7 @@ func makeRing(ctx context.Context, store slotStore, session string, p params) (r
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readWatermark(ctx context.Context, store slotStore, id string) (int64, error) {
|
func readWatermark(ctx context.Context, store slotStore, id string) (int64, error) {
|
||||||
d, err := store.readDescription(context.WithValue(ctx, laneKey{}, true), id)
|
d, err := store.readDescription(ctx, id)
|
||||||
if err != nil || d == "" {
|
if err != nil || d == "" {
|
||||||
return -1, err
|
return -1, err
|
||||||
}
|
}
|
||||||
@@ -223,7 +223,7 @@ func newSlotWriter(ctx context.Context, store slotStore, table slotTable, p para
|
|||||||
store: store,
|
store: store,
|
||||||
table: table,
|
table: table,
|
||||||
params: p,
|
params: p,
|
||||||
slots: make(chan struct{}, min(len(table.Slots), slotWriteInflight)),
|
slots: make(chan struct{}, len(table.Slots)),
|
||||||
size: min(slotInlineLimit, p.segmentBytes),
|
size: min(slotInlineLimit, p.segmentBytes),
|
||||||
acked: -1,
|
acked: -1,
|
||||||
progress: time.Now(),
|
progress: time.Now(),
|
||||||
@@ -278,6 +278,7 @@ func (w *slotWriter) readyToFlush() bool {
|
|||||||
grew := len(w.buf) > w.lastSize
|
grew := len(w.buf) > w.lastSize
|
||||||
w.lastSize = len(w.buf)
|
w.lastSize = len(w.buf)
|
||||||
|
|
||||||
|
// slots are busy and the buffer small, so a bigger segment is worth the wait
|
||||||
if len(w.slots) > 0 && w.held < maxCoalescedTicks && len(w.buf) < w.size/2 {
|
if len(w.slots) > 0 && w.held < maxCoalescedTicks && len(w.buf) < w.size/2 {
|
||||||
w.held++
|
w.held++
|
||||||
return false
|
return false
|
||||||
@@ -307,6 +308,7 @@ func (w *slotWriter) flushLocked() error {
|
|||||||
return w.err
|
return w.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a ring three quarters busy is saturated by small segments, so widen at once
|
||||||
if w.size < w.segmentBytes && len(w.buf) >= w.size && len(w.slots)*4 > cap(w.slots)*3 {
|
if w.size < w.segmentBytes && len(w.buf) >= w.size && len(w.slots)*4 > cap(w.slots)*3 {
|
||||||
w.size = w.segmentBytes
|
w.size = w.segmentBytes
|
||||||
w.full = 0
|
w.full = 0
|
||||||
@@ -317,9 +319,6 @@ func (w *slotWriter) flushLocked() error {
|
|||||||
// a burst goes at the full size at once
|
// a burst goes at the full size at once
|
||||||
n = min(len(w.buf), w.segmentBytes)
|
n = min(len(w.buf), w.segmentBytes)
|
||||||
}
|
}
|
||||||
if n > slotInlineLimit && len(w.buf) <= 2*cap(w.slots)*slotInlineLimit {
|
|
||||||
n = slotInlineLimit
|
|
||||||
}
|
|
||||||
if n == w.size && w.size < w.segmentBytes {
|
if n == w.size && w.size < w.segmentBytes {
|
||||||
if w.full++; w.full == inlineRampSegments {
|
if w.full++; w.full == inlineRampSegments {
|
||||||
w.full = 0
|
w.full = 0
|
||||||
@@ -351,21 +350,35 @@ func (w *slotWriter) upload(seq int64, chunk []byte) {
|
|||||||
if len(chunk) <= slotInlineLimit {
|
if len(chunk) <= slotInlineLimit {
|
||||||
mode = slotInline
|
mode = slotInline
|
||||||
}
|
}
|
||||||
began := time.Now()
|
|
||||||
if err := w.await(seq); err != nil {
|
if err := w.await(seq); err != nil {
|
||||||
w.fail(errors.New("failed to store segment ", seq).Base(err))
|
w.fail(errors.New("failed to store segment ", seq).Base(err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
awaited := time.Since(began)
|
var lastErr error
|
||||||
putAt := time.Now()
|
for attempt := range slotWriteAttempts {
|
||||||
if err := w.write(seq, mode, chunk); err != nil {
|
if attempt > 0 {
|
||||||
w.fail(errors.New("failed to store segment ", seq).Base(err))
|
select {
|
||||||
// the slot is free, the reader learns there what happened
|
case <-w.ctx.Done():
|
||||||
w.write(seq, slotErr, nil)
|
return
|
||||||
|
case <-time.After(time.Duration(attempt) * 200 * time.Millisecond):
|
||||||
|
}
|
||||||
|
// the failed write may have gone through
|
||||||
|
if d, err := w.store.readDescription(w.ctx, w.slotID(seq)); err == nil {
|
||||||
|
if f, ok := decodeSlot(d); ok && f.seq >= seq {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
errors.LogDebug(w.ctx, "xdrive write ", slotTag(w.table.Watermark), " seq=", seq, " bytes=", len(chunk),
|
}
|
||||||
" mode=", mode, " await=", awaited, " put=", time.Since(putAt))
|
}
|
||||||
|
if lastErr = w.write(seq, mode, chunk); lastErr == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if w.ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.fail(errors.New("failed to store segment ", seq).Base(lastErr))
|
||||||
|
// the slot is free, the reader learns there what happened
|
||||||
|
w.write(seq, slotErr, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *slotWriter) write(seq int64, mode byte, data []byte) error {
|
func (w *slotWriter) write(seq int64, mode byte, data []byte) error {
|
||||||
@@ -486,7 +499,6 @@ type slotReader struct {
|
|||||||
ch chan []byte
|
ch chan []byte
|
||||||
wake chan struct{}
|
wake chan struct{}
|
||||||
acks chan struct{}
|
acks chan struct{}
|
||||||
grow chan struct{}
|
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
head int64
|
head int64
|
||||||
@@ -498,11 +510,7 @@ type slotReader struct {
|
|||||||
gap time.Duration
|
gap time.Duration
|
||||||
activeAt time.Time
|
activeAt time.Time
|
||||||
active chan struct{}
|
active chan struct{}
|
||||||
frontier int64
|
|
||||||
err error
|
err error
|
||||||
reads atomic.Int64
|
|
||||||
tailMin time.Duration
|
|
||||||
tailMax time.Duration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newSlotReader(ctx context.Context, store slotStore, table slotTable, p params) *slotReader {
|
func newSlotReader(ctx context.Context, store slotStore, table slotTable, p params) *slotReader {
|
||||||
@@ -514,12 +522,9 @@ func newSlotReader(ctx context.Context, store slotStore, table slotTable, p para
|
|||||||
ch: make(chan []byte, p.concurrency),
|
ch: make(chan []byte, p.concurrency),
|
||||||
wake: make(chan struct{}, 1),
|
wake: make(chan struct{}, 1),
|
||||||
acks: make(chan struct{}, 1),
|
acks: make(chan struct{}, 1),
|
||||||
grow: make(chan struct{}, 1),
|
|
||||||
hitAt: time.Now(),
|
hitAt: time.Now(),
|
||||||
activeAt: time.Now(),
|
activeAt: time.Now(),
|
||||||
active: make(chan struct{}),
|
active: make(chan struct{}),
|
||||||
tailMin: slotTailMin,
|
|
||||||
tailMax: slotTailMax,
|
|
||||||
}
|
}
|
||||||
go r.run()
|
go r.run()
|
||||||
go r.ackLoop()
|
go r.ackLoop()
|
||||||
@@ -549,20 +554,9 @@ type slotFetch struct {
|
|||||||
ready chan struct{}
|
ready chan struct{}
|
||||||
// promoted is closed when the segment becomes the head
|
// promoted is closed when the segment becomes the head
|
||||||
promoted chan struct{}
|
promoted chan struct{}
|
||||||
near chan struct{}
|
|
||||||
body []byte
|
body []byte
|
||||||
end bool
|
end bool
|
||||||
err error
|
err error
|
||||||
born time.Time
|
|
||||||
readyAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *slotFetch) enter() {
|
|
||||||
select {
|
|
||||||
case <-f.near:
|
|
||||||
default:
|
|
||||||
close(f.near)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *slotReader) run() {
|
func (r *slotReader) run() {
|
||||||
@@ -573,20 +567,17 @@ func (r *slotReader) run() {
|
|||||||
fetches := make(map[int64]*slotFetch)
|
fetches := make(map[int64]*slotFetch)
|
||||||
for {
|
for {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
head, started, frontier := r.head, r.started, r.frontier
|
head, started := r.head, r.started
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
|
|
||||||
window := 1
|
window := 1
|
||||||
if started {
|
if started {
|
||||||
window = min(max(int(frontier-head)+slotFetchAhead, slotFetchAhead), len(r.table.Slots))
|
window = min(slotFetchAhead, len(r.table.Slots))
|
||||||
}
|
}
|
||||||
for i := range window {
|
for i := range window {
|
||||||
seq := head + int64(i)
|
seq := head + int64(i)
|
||||||
if fetches[seq] == nil {
|
if fetches[seq] == nil {
|
||||||
fetches[seq] = &slotFetch{ready: make(chan struct{}), promoted: make(chan struct{}), near: make(chan struct{}), born: time.Now()}
|
fetches[seq] = &slotFetch{ready: make(chan struct{}), promoted: make(chan struct{})}
|
||||||
if int64(i) <= slotReadAhead {
|
|
||||||
fetches[seq].enter()
|
|
||||||
}
|
|
||||||
go r.fetch(ctx, seq, i, fetches[seq])
|
go r.fetch(ctx, seq, i, fetches[seq])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -594,8 +585,6 @@ func (r *slotReader) run() {
|
|||||||
f := fetches[head]
|
f := fetches[head]
|
||||||
select {
|
select {
|
||||||
case <-f.ready:
|
case <-f.ready:
|
||||||
case <-r.grow:
|
|
||||||
continue
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -613,14 +602,8 @@ func (r *slotReader) run() {
|
|||||||
r.head++
|
r.head++
|
||||||
r.started = true
|
r.started = true
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
errors.LogDebug(r.ctx, "xdrive read ", slotTag(r.table.Watermark), " seq=", head, " fetch=",
|
|
||||||
f.readyAt.Sub(f.born), " blocked=", time.Since(f.readyAt), " bytes=", len(f.body),
|
|
||||||
" reads=", r.reads.Load())
|
|
||||||
if next := fetches[head+1]; next != nil {
|
if next := fetches[head+1]; next != nil {
|
||||||
close(next.promoted)
|
close(next.promoted)
|
||||||
if nf := fetches[head+1+slotReadAhead]; nf != nil {
|
|
||||||
nf.enter()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
case r.acks <- struct{}{}:
|
case r.acks <- struct{}{}:
|
||||||
@@ -641,35 +624,11 @@ func (r *slotReader) isHead(seq int64) bool {
|
|||||||
return r.head == seq
|
return r.head == seq
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *slotReader) bursting() bool {
|
|
||||||
r.mu.Lock()
|
|
||||||
defer r.mu.Unlock()
|
|
||||||
return time.Since(r.hitAt) <= slotReadAheadBurst
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *slotReader) reach(seq int64) {
|
|
||||||
r.mu.Lock()
|
|
||||||
grown := seq > r.frontier
|
|
||||||
if grown {
|
|
||||||
r.frontier = seq
|
|
||||||
}
|
|
||||||
r.mu.Unlock()
|
|
||||||
if grown {
|
|
||||||
select {
|
|
||||||
case r.grow <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch asks for the slot of seq until it holds seq: the head often, a segment
|
// fetch asks for the slot of seq until it holds seq: the head often, a segment
|
||||||
// behind it soon after it was asked for, then at the pace that segments come
|
// behind it at the pace that segments come, as it would wait for the head anyway
|
||||||
func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFetch) {
|
func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFetch) {
|
||||||
defer func() {
|
defer close(f.ready)
|
||||||
f.readyAt = time.Now()
|
if offset > 0 && !r.hold(ctx, time.Duration(offset)*r.pace(), f.promoted, nil) {
|
||||||
close(f.ready)
|
|
||||||
}()
|
|
||||||
if offset > 0 && !r.hold(ctx, r.tailMin, f.promoted, f.near) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,13 +638,9 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
|
|||||||
for {
|
for {
|
||||||
var description string
|
var description string
|
||||||
var err error
|
var err error
|
||||||
r.mu.Lock()
|
if r.isHead(seq) {
|
||||||
hot := time.Since(r.activeAt) <= r.eagerWindow
|
description, err = r.readHedged(ctx, id)
|
||||||
r.mu.Unlock()
|
|
||||||
if hot && r.isHead(seq) {
|
|
||||||
description, err = r.readHead(ctx, id, seq)
|
|
||||||
} else {
|
} else {
|
||||||
r.reads.Add(1)
|
|
||||||
description, err = r.store.readDescription(ctx, id)
|
description, err = r.store.readDescription(ctx, id)
|
||||||
}
|
}
|
||||||
frame, ok := decodeSlot(description)
|
frame, ok := decodeSlot(description)
|
||||||
@@ -695,7 +650,6 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
r.touch()
|
r.touch()
|
||||||
r.mu.Unlock()
|
r.mu.Unlock()
|
||||||
r.reach(seq)
|
|
||||||
switch frame.mode {
|
switch frame.mode {
|
||||||
case slotEnd:
|
case slotEnd:
|
||||||
f.end = true
|
f.end = true
|
||||||
@@ -708,7 +662,7 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
|
|||||||
r.hit()
|
r.hit()
|
||||||
return
|
return
|
||||||
case slotMedia:
|
case slotMedia:
|
||||||
if body, err := r.readBodyHedged(ctx, id); err == nil && frame.holds(body) {
|
if body, err := r.store.readBody(ctx, id); err == nil && frame.holds(body) {
|
||||||
f.body = body
|
f.body = body
|
||||||
r.hit()
|
r.hit()
|
||||||
return
|
return
|
||||||
@@ -744,15 +698,7 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
|
|||||||
// only the head is asked for until something comes
|
// only the head is asked for until something comes
|
||||||
wait, a, b, backoff = 0, f.promoted, active, slotHuntFloor
|
wait, a, b, backoff = 0, f.promoted, active, slotHuntFloor
|
||||||
default:
|
default:
|
||||||
wait, a, b, backoff = min(max(r.pace(), r.tailMin), r.tailMax), f.promoted, f.near, slotHuntFloor
|
wait, a, backoff = min(max(r.pace(), slotTailMin), slotTailMax), f.promoted, slotHuntFloor
|
||||||
select {
|
|
||||||
case <-f.near:
|
|
||||||
b = nil
|
|
||||||
if r.bursting() {
|
|
||||||
wait = slotReadAheadPoll
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !r.hold(ctx, wait, a, b) {
|
if !r.hold(ctx, wait, a, b) {
|
||||||
return
|
return
|
||||||
@@ -760,102 +706,51 @@ func (r *slotReader) fetch(ctx context.Context, seq int64, offset int, f *slotFe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *slotReader) readHead(ctx context.Context, id string, seq int64) (string, error) {
|
// readHedged reads the description of the head, and asks a second time if the
|
||||||
|
// first request is slow to answer, as one that is stuck costs more than one
|
||||||
|
// too many. The first good answer is taken
|
||||||
|
func (r *slotReader) readHedged(ctx context.Context, id string) (string, error) {
|
||||||
|
if slotHedgeTick <= 0 {
|
||||||
|
return r.store.readDescription(ctx, id)
|
||||||
|
}
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
ctx = context.WithValue(ctx, laneKey{}, true)
|
|
||||||
|
|
||||||
type result struct {
|
type result struct {
|
||||||
description string
|
description string
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
results := make(chan result, 4)
|
|
||||||
outstanding := 0
|
|
||||||
launch := func() {
|
|
||||||
outstanding++
|
|
||||||
go func() {
|
|
||||||
r.reads.Add(1)
|
|
||||||
description, err := r.store.readDescription(ctx, id)
|
|
||||||
results <- result{description, err}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
launch()
|
|
||||||
ticker := time.NewTicker(slotHuntCeiling)
|
|
||||||
defer ticker.Stop()
|
|
||||||
var last result
|
|
||||||
seen := false
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case res := <-results:
|
|
||||||
outstanding--
|
|
||||||
if res.err != nil {
|
|
||||||
return res.description, res.err
|
|
||||||
}
|
|
||||||
if frame, ok := decodeSlot(res.description); ok && frame.seq == seq {
|
|
||||||
return res.description, nil
|
|
||||||
}
|
|
||||||
last, seen = res, true
|
|
||||||
case <-ticker.C:
|
|
||||||
r.mu.Lock()
|
|
||||||
idle := time.Since(r.activeAt) > r.eagerWindow
|
|
||||||
r.mu.Unlock()
|
|
||||||
switch {
|
|
||||||
case idle && seen:
|
|
||||||
return last.description, nil
|
|
||||||
case !idle && outstanding < 3:
|
|
||||||
launch()
|
|
||||||
}
|
|
||||||
case <-r.wake:
|
|
||||||
if outstanding < 3 {
|
|
||||||
launch()
|
|
||||||
}
|
|
||||||
case <-ctx.Done():
|
|
||||||
return "", ctx.Err()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *slotReader) readBodyHedged(ctx context.Context, id string) ([]byte, error) {
|
|
||||||
if slotBodyHedge <= 0 {
|
|
||||||
return r.store.readBody(ctx, id)
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
type result struct {
|
|
||||||
body []byte
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
results := make(chan result, 2)
|
results := make(chan result, 2)
|
||||||
launch := func() {
|
launch := func() {
|
||||||
go func() {
|
go func() {
|
||||||
body, err := r.store.readBody(ctx, id)
|
d, err := r.store.readDescription(ctx, id)
|
||||||
results <- result{body, err}
|
results <- result{d, err}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
launch()
|
launch()
|
||||||
outstanding := 1
|
outstanding := 1
|
||||||
timer := time.NewTimer(slotBodyHedge)
|
deadline := time.Now().Add(slotHedgeBudget)
|
||||||
defer timer.Stop()
|
ticker := time.NewTicker(slotHedgeTick)
|
||||||
|
defer ticker.Stop()
|
||||||
var last result
|
var last result
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case res := <-results:
|
case res := <-results:
|
||||||
outstanding--
|
outstanding--
|
||||||
if res.err == nil {
|
if res.err == nil {
|
||||||
return res.body, nil
|
return res.description, nil
|
||||||
}
|
}
|
||||||
last = res
|
last = res
|
||||||
if outstanding == 0 {
|
if outstanding == 0 {
|
||||||
return nil, last.err
|
return last.description, last.err
|
||||||
}
|
}
|
||||||
case <-timer.C:
|
case <-ticker.C:
|
||||||
if outstanding == 1 {
|
if outstanding < 2 && time.Now().Before(deadline) {
|
||||||
launch()
|
launch()
|
||||||
outstanding++
|
outstanding++
|
||||||
}
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return nil, ctx.Err()
|
return "", ctx.Err()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -924,7 +819,7 @@ func (r *slotReader) ackLoop() {
|
|||||||
if want <= acked {
|
if want <= acked {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err := r.store.setSlot(context.WithValue(r.ctx, laneKey{}, true), r.table.Watermark, strconv.FormatInt(want, 10), nil); err == nil {
|
if err := r.store.setSlot(r.ctx, r.table.Watermark, strconv.FormatInt(want, 10), nil); err == nil {
|
||||||
acked, wait = want, slotAckRetry
|
acked, wait = want, slotAckRetry
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -954,10 +849,3 @@ func (r *slotReader) Err() error {
|
|||||||
}
|
}
|
||||||
return io.EOF
|
return io.EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
func slotTag(id string) string {
|
|
||||||
if len(id) > 6 {
|
|
||||||
return id[len(id)-6:]
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -305,373 +304,6 @@ func TestSlotStaleBody(t *testing.T) {
|
|||||||
client, server := slotPair(t, &staleSlots{memSlots: newMemSlots(), served: make(map[string]bool)}, 2, 64<<10)
|
client, server := slotPair(t, &staleSlots{memSlots: newMemSlots(), served: make(map[string]bool)}, 2, 64<<10)
|
||||||
payload := randomPayload(200 << 10)
|
payload := randomPayload(200 << 10)
|
||||||
errs := make(chan error, 1)
|
errs := make(chan error, 1)
|
||||||
go func() {
|
|
||||||
_, err := client.Write(payload)
|
|
||||||
if err == nil {
|
|
||||||
err = client.Close()
|
|
||||||
}
|
|
||||||
errs <- err
|
|
||||||
}()
|
|
||||||
readAll(t, server, payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotBurstSplitsInline(t *testing.T) {
|
|
||||||
store := newMemSlots()
|
|
||||||
client, server := slotPair(t, store, 8, 512<<10)
|
|
||||||
payload := randomPayload(90 << 10)
|
|
||||||
errs := make(chan error, 1)
|
|
||||||
writeAll(client, payload, true, errs)
|
writeAll(client, payload, true, errs)
|
||||||
readAll(t, server, payload)
|
readAll(t, server, payload)
|
||||||
store.mu.Lock()
|
|
||||||
bodies := len(store.bodies)
|
|
||||||
store.mu.Unlock()
|
|
||||||
if bodies != 0 {
|
|
||||||
t.Fatalf("a burst of 90 KiB went in %d bodies, want none", bodies)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type stuckBodySlots struct {
|
|
||||||
*memSlots
|
|
||||||
calls atomic.Int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stuckBodySlots) readBody(ctx context.Context, id string) ([]byte, error) {
|
|
||||||
if s.calls.Add(1) == 1 {
|
|
||||||
<-ctx.Done()
|
|
||||||
return nil, ctx.Err()
|
|
||||||
}
|
|
||||||
return s.memSlots.readBody(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotBodyHedge(t *testing.T) {
|
|
||||||
old := slotBodyHedge
|
|
||||||
slotBodyHedge = 50 * time.Millisecond
|
|
||||||
defer func() { slotBodyHedge = old }()
|
|
||||||
|
|
||||||
store := &stuckBodySlots{memSlots: newMemSlots()}
|
|
||||||
client, server := slotPair(t, store, 2, 64<<10)
|
|
||||||
payload := randomPayload(200 << 10)
|
|
||||||
errs := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
_, err := client.Write(payload)
|
|
||||||
if err == nil {
|
|
||||||
err = client.Close()
|
|
||||||
}
|
|
||||||
errs <- err
|
|
||||||
}()
|
|
||||||
readAll(t, server, payload)
|
|
||||||
if store.calls.Load() < 2 {
|
|
||||||
t.Fatalf("the stuck body read was not hedged, %d reads", store.calls.Load())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTakeSpareSkipsStale(t *testing.T) {
|
|
||||||
cfg := &Config{}
|
|
||||||
p := params{sessionTTL: 5 * time.Minute}
|
|
||||||
st1, err := newLocalStorage(t.TempDir())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
st2, err := newLocalStorage(t.TempDir())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
box := spareBoxFor(cfg)
|
|
||||||
box.ch <- spareRing{storage: st1, session: "old", at: time.Now().Add(-time.Hour)}
|
|
||||||
box.ch <- spareRing{storage: st2, session: "new", at: time.Now()}
|
|
||||||
got, ok := takeSpare(cfg, p)
|
|
||||||
if !ok || got.session != "new" {
|
|
||||||
t.Fatalf("takeSpare gave %q, %v, want the fresh one", got.session, ok)
|
|
||||||
}
|
|
||||||
if _, ok := takeSpare(cfg, p); ok {
|
|
||||||
t.Fatal("takeSpare returned a second spare")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDropSpareDeletesSession(t *testing.T) {
|
|
||||||
st, err := newLocalStorage(t.TempDir())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
name := sessionPrefix("gone") + "/" + uplinkDir + "/s0"
|
|
||||||
if err := st.Put(context.Background(), name, []byte("x")); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
dropSpare(spareRing{storage: st, session: "gone"})
|
|
||||||
if got, _ := st.List(context.Background(), sessionPrefix("gone")); len(got) != 0 {
|
|
||||||
t.Fatalf("%d entries left of a dropped spare", len(got))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type stuckHeadSlots struct {
|
|
||||||
*memSlots
|
|
||||||
calls atomic.Int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stuckHeadSlots) readDescription(ctx context.Context, id string) (string, error) {
|
|
||||||
if s.calls.Add(1) == 1 {
|
|
||||||
<-ctx.Done()
|
|
||||||
return "", ctx.Err()
|
|
||||||
}
|
|
||||||
return s.memSlots.readDescription(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotHeadReadOverlaps(t *testing.T) {
|
|
||||||
client, server := slotPair(t, &stuckHeadSlots{memSlots: newMemSlots()}, 4, 64<<10)
|
|
||||||
for _, c := range [][2]*Conn{{client, server}, {server, client}} {
|
|
||||||
c[0].Write([]byte("ping"))
|
|
||||||
if _, err := io.ReadFull(c[1], make([]byte, 4)); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotBurstReadsTogether(t *testing.T) {
|
|
||||||
client, server := slotPair(t, newMemSlots(), 16, 512<<10)
|
|
||||||
payload := randomPayload(8 * slotInlineLimit)
|
|
||||||
go client.Write(payload)
|
|
||||||
start := time.Now()
|
|
||||||
got := make([]byte, len(payload))
|
|
||||||
if _, err := io.ReadFull(server, got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, payload) {
|
|
||||||
t.Fatal("the burst came back changed")
|
|
||||||
}
|
|
||||||
if took := time.Since(start); took > time.Second {
|
|
||||||
t.Fatalf("a burst of 8 segments took %v to read", took)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type lagSlots struct{ *memSlots }
|
|
||||||
|
|
||||||
func (s lagSlots) readDescription(ctx context.Context, id string) (string, error) {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return "", ctx.Err()
|
|
||||||
case <-time.After(200 * time.Millisecond):
|
|
||||||
}
|
|
||||||
return s.memSlots.readDescription(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s lagSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
|
|
||||||
if _, ok := decodeSlot(description); ok {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-time.After(600 * time.Millisecond):
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return s.memSlots.setSlot(ctx, id, description, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotReadAheadBurst(t *testing.T) {
|
|
||||||
oldMin, oldMax := slotTailMin, slotTailMax
|
|
||||||
slotTailMin, slotTailMax = 150*time.Millisecond, 800*time.Millisecond
|
|
||||||
t.Cleanup(func() { slotTailMin, slotTailMax = oldMin, oldMax })
|
|
||||||
|
|
||||||
client, server := slotPair(t, lagSlots{newMemSlots()}, 16, 512<<10)
|
|
||||||
payload := randomPayload(8 * slotInlineLimit)
|
|
||||||
go client.Write(payload)
|
|
||||||
start := time.Now()
|
|
||||||
got := make([]byte, len(payload))
|
|
||||||
if _, err := io.ReadFull(server, got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, payload) {
|
|
||||||
t.Fatal("the burst came back changed")
|
|
||||||
}
|
|
||||||
if took := time.Since(start); took > 1700*time.Millisecond {
|
|
||||||
t.Fatalf("a burst of 8 segments took %v to read", took)
|
|
||||||
}
|
|
||||||
client.Close()
|
|
||||||
server.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotLargeBurstInline(t *testing.T) {
|
|
||||||
store := newMemSlots()
|
|
||||||
client, server := slotPair(t, store, 16, 512<<10)
|
|
||||||
payload := randomPayload(10 * slotInlineLimit)
|
|
||||||
go client.Write(payload)
|
|
||||||
got := make([]byte, len(payload))
|
|
||||||
if _, err := io.ReadFull(server, got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, payload) {
|
|
||||||
t.Fatal("the burst came back changed")
|
|
||||||
}
|
|
||||||
store.mu.Lock()
|
|
||||||
bodies := len(store.bodies)
|
|
||||||
store.mu.Unlock()
|
|
||||||
if bodies != 0 {
|
|
||||||
t.Fatalf("a burst of 140 KiB went in %d bodies, want none", bodies)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotDepthFollowsConcurrency(t *testing.T) {
|
|
||||||
for _, c := range []struct{ set, want int }{{100, 100}, {maxConcurrency, maxConcurrency}, {1 << 20, maxConcurrency}} {
|
|
||||||
p := paramsFromConfig(&Config{Concurrency: uint32(c.set)})
|
|
||||||
r, err := makeRing(context.Background(), newMemSlots(), "session", p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(r.Up.Slots) != c.want || len(r.Down.Slots) != c.want {
|
|
||||||
t.Fatalf("concurrency %d made %d and %d slots, want %d", c.set, len(r.Up.Slots), len(r.Down.Slots), c.want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotDeepRingRoundTrip(t *testing.T) {
|
|
||||||
client, server := slotPair(t, newMemSlots(), 64, 64<<10)
|
|
||||||
payload := randomPayload(2 << 20)
|
|
||||||
errs := make(chan error, 1)
|
|
||||||
writeAll(client, payload, true, errs)
|
|
||||||
readAll(t, server, payload)
|
|
||||||
if err := <-errs; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type pollSlots struct {
|
|
||||||
*memSlots
|
|
||||||
pmu sync.Mutex
|
|
||||||
polled map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *pollSlots) readDescription(ctx context.Context, id string) (string, error) {
|
|
||||||
s.pmu.Lock()
|
|
||||||
s.polled[id] = true
|
|
||||||
s.pmu.Unlock()
|
|
||||||
return s.memSlots.readDescription(ctx, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotReaderReachesPastWindow(t *testing.T) {
|
|
||||||
store := &pollSlots{memSlots: newMemSlots(), polled: make(map[string]bool)}
|
|
||||||
p := paramsFromConfig(&Config{FlushIntervalMs: 1, PollIntervalMs: 1, MaxPollIntervalMs: 20, EagerWindowMs: 50, Concurrency: 64})
|
|
||||||
ring, err := makeRing(context.Background(), store, "session", p)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
put := func(seq int64) {
|
|
||||||
slot := ring.Down.Slots[seq%int64(len(ring.Down.Slots))]
|
|
||||||
if err := store.setSlot(ctx, slot, newSlotFrame(seq, slotInline, []byte{byte(seq)}).encode(), nil); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
reader := newSlotReader(ctx, store, ring.Down, p)
|
|
||||||
next := func() []byte {
|
|
||||||
select {
|
|
||||||
case chunk := <-reader.chunks():
|
|
||||||
return chunk
|
|
||||||
case <-time.After(10 * time.Second):
|
|
||||||
t.Fatal("no segment came")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
put(0)
|
|
||||||
if got := next(); !bytes.Equal(got, []byte{0}) {
|
|
||||||
t.Fatalf("segment 0 came as %v", got)
|
|
||||||
}
|
|
||||||
for seq := int64(2); seq <= 40; seq++ {
|
|
||||||
put(seq)
|
|
||||||
}
|
|
||||||
last := ring.Down.Slots[40]
|
|
||||||
deadline := time.Now().Add(10 * time.Second)
|
|
||||||
for {
|
|
||||||
reader.Wake()
|
|
||||||
store.pmu.Lock()
|
|
||||||
polled := store.polled[last]
|
|
||||||
store.pmu.Unlock()
|
|
||||||
if polled {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if time.Now().After(deadline) {
|
|
||||||
t.Fatal("segment 40 was never asked for while segment 1 was missing")
|
|
||||||
}
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
put(1)
|
|
||||||
for seq := int64(1); seq <= 40; seq++ {
|
|
||||||
if got := next(); !bytes.Equal(got, []byte{byte(seq)}) {
|
|
||||||
t.Fatalf("segment %d came as %v", seq, got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type countSlots struct {
|
|
||||||
*memSlots
|
|
||||||
cur atomic.Int32
|
|
||||||
peak atomic.Int32
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *countSlots) setSlot(ctx context.Context, id, description string, body []byte) error {
|
|
||||||
if _, ok := decodeSlot(description); !ok {
|
|
||||||
return s.memSlots.setSlot(ctx, id, description, body)
|
|
||||||
}
|
|
||||||
n := s.cur.Add(1)
|
|
||||||
for {
|
|
||||||
p := s.peak.Load()
|
|
||||||
if n <= p || s.peak.CompareAndSwap(p, n) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
time.Sleep(20 * time.Millisecond)
|
|
||||||
s.cur.Add(-1)
|
|
||||||
return s.memSlots.setSlot(ctx, id, description, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotWriteInflightCap(t *testing.T) {
|
|
||||||
store := &countSlots{memSlots: newMemSlots()}
|
|
||||||
client, server := slotPair(t, store, 48, 512<<10)
|
|
||||||
payload := randomPayload(60 * slotInlineLimit)
|
|
||||||
errs := make(chan error, 1)
|
|
||||||
go func() {
|
|
||||||
for off := 0; off < len(payload); off += slotInlineLimit {
|
|
||||||
if _, err := client.Write(payload[off : off+slotInlineLimit]); err != nil {
|
|
||||||
errs <- err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
errs <- nil
|
|
||||||
}()
|
|
||||||
got := make([]byte, len(payload))
|
|
||||||
if _, err := io.ReadFull(server, got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := <-errs; err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, payload) {
|
|
||||||
t.Fatal("the payload came back changed")
|
|
||||||
}
|
|
||||||
if p := int(store.peak.Load()); p > slotWriteInflight {
|
|
||||||
t.Fatalf("%d segments were written at once, want at most %d", p, slotWriteInflight)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSlotBurstInlineWaves(t *testing.T) {
|
|
||||||
for _, c := range []struct {
|
|
||||||
pieces int
|
|
||||||
media bool
|
|
||||||
}{{20, false}, {30, true}} {
|
|
||||||
store := newMemSlots()
|
|
||||||
client, server := slotPair(t, store, 16, 512<<10)
|
|
||||||
payload := randomPayload(c.pieces * slotInlineLimit)
|
|
||||||
go client.Write(payload)
|
|
||||||
got := make([]byte, len(payload))
|
|
||||||
if _, err := io.ReadFull(server, got); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got, payload) {
|
|
||||||
t.Fatal("the burst came back changed")
|
|
||||||
}
|
|
||||||
store.mu.Lock()
|
|
||||||
bodies := len(store.bodies)
|
|
||||||
store.mu.Unlock()
|
|
||||||
if (bodies > 0) != c.media {
|
|
||||||
t.Fatalf("a burst of %d pieces went in %d bodies, media wanted %v", c.pieces, bodies, c.media)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ type walWriter struct {
|
|||||||
buf []byte
|
buf []byte
|
||||||
seq int64
|
seq int64
|
||||||
lastSize int
|
lastSize int
|
||||||
|
segBytes int
|
||||||
|
ramped int
|
||||||
held int
|
held int
|
||||||
closed bool
|
closed bool
|
||||||
err error
|
err error
|
||||||
@@ -66,6 +68,10 @@ func newWALWriter(ctx context.Context, storage Storage, prefix string, p params)
|
|||||||
params: p,
|
params: p,
|
||||||
sem: make(chan struct{}, p.concurrency),
|
sem: make(chan struct{}, p.concurrency),
|
||||||
}
|
}
|
||||||
|
// A session being set up is latency bound: small segments fit in a
|
||||||
|
// single inline create, so start there and double up to the configured
|
||||||
|
// size once full segments keep coming.
|
||||||
|
w.segBytes = min(p.segmentBytes, driveInlineLimit)
|
||||||
go w.flushLoop()
|
go w.flushLoop()
|
||||||
return w
|
return w
|
||||||
}
|
}
|
||||||
@@ -82,7 +88,7 @@ func (w *walWriter) Write(p []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.buf = append(w.buf, p...)
|
w.buf = append(w.buf, p...)
|
||||||
for len(w.buf) >= w.segmentBytes {
|
for len(w.buf) >= w.segBytes {
|
||||||
if err := w.flushLocked(); err != nil {
|
if err := w.flushLocked(); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
@@ -133,8 +139,15 @@ func (w *walWriter) flushLocked() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
n := len(w.buf)
|
n := len(w.buf)
|
||||||
if n > w.segmentBytes {
|
if n > w.segBytes {
|
||||||
n = w.segmentBytes
|
n = w.segBytes
|
||||||
|
}
|
||||||
|
if n == w.segBytes && w.segBytes < w.segmentBytes {
|
||||||
|
w.ramped++
|
||||||
|
if w.ramped >= inlineRampSegments {
|
||||||
|
w.ramped = 0
|
||||||
|
w.segBytes = min(w.segBytes*2, w.segmentBytes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chunk := make([]byte, n)
|
chunk := make([]byte, n)
|
||||||
@@ -324,83 +337,73 @@ func (r *walReader) poll() (advanced, eof bool, err error) {
|
|||||||
r.holeSince = time.Time{}
|
r.holeSince = time.Time{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type result struct {
|
||||||
|
chunk []byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
outstanding := make(map[int64]chan result)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
if entry, ok := pending[r.seq]; ok && strings.HasSuffix(entry.Name, errSuffix) {
|
if entry, ok := pending[r.seq]; ok && strings.HasSuffix(entry.Name, errSuffix) {
|
||||||
r.discard(r.prefix + "/" + entry.Name)
|
r.discard(r.prefix + "/" + entry.Name)
|
||||||
return advanced, false, errors.New("the peer could not store segment ", r.seq)
|
return advanced, false, errors.New("the peer could not store segment ", r.seq)
|
||||||
}
|
}
|
||||||
|
if entry, ok := pending[r.seq]; ok && strings.HasSuffix(entry.Name, endSuffix) && len(outstanding) == 0 {
|
||||||
batch, done := r.nextBatch(pending)
|
r.discard(r.prefix + "/" + entry.Name)
|
||||||
if done {
|
|
||||||
return advanced, true, nil
|
return advanced, true, nil
|
||||||
}
|
}
|
||||||
if len(batch) == 0 {
|
|
||||||
return advanced, false, nil
|
// keep the window of fetches full, so the next segments download
|
||||||
|
// while the head one is handed over
|
||||||
|
for i := int64(0); len(outstanding) < r.concurrency; i++ {
|
||||||
|
seq := r.seq + i
|
||||||
|
entry, ok := pending[seq]
|
||||||
|
if !ok || !strings.HasSuffix(entry.Name, segSuffix) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if _, ok := outstanding[seq]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ch := make(chan result, 1)
|
||||||
|
outstanding[seq] = ch
|
||||||
|
if entry.Inline != nil {
|
||||||
|
ch <- result{chunk: entry.Inline}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
go func(name string) {
|
||||||
|
chunk, err := r.storage.Get(r.ctx, r.prefix+"/"+name)
|
||||||
|
ch <- result{chunk, err}
|
||||||
|
}(entry.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks, err := r.fetch(batch)
|
head, ok := outstanding[r.seq]
|
||||||
if err != nil {
|
if !ok {
|
||||||
if err == errNotFound {
|
|
||||||
return advanced, false, nil
|
return advanced, false, nil
|
||||||
}
|
}
|
||||||
return advanced, false, err
|
var res result
|
||||||
}
|
|
||||||
|
|
||||||
for i, chunk := range chunks {
|
|
||||||
select {
|
select {
|
||||||
case r.ch <- chunk:
|
case res = <-head:
|
||||||
case <-r.ctx.Done():
|
case <-r.ctx.Done():
|
||||||
return advanced, true, nil
|
return advanced, true, nil
|
||||||
}
|
}
|
||||||
|
delete(outstanding, r.seq)
|
||||||
|
if res.err != nil {
|
||||||
|
if res.err == errNotFound {
|
||||||
|
return advanced, false, nil
|
||||||
|
}
|
||||||
|
return advanced, false, res.err
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case r.ch <- res.chunk:
|
||||||
|
case <-r.ctx.Done():
|
||||||
|
return advanced, true, nil
|
||||||
|
}
|
||||||
|
name := pending[r.seq].Name
|
||||||
|
delete(pending, r.seq)
|
||||||
r.seq++
|
r.seq++
|
||||||
advanced = true
|
advanced = true
|
||||||
r.discard(r.prefix + "/" + batch[i].Name)
|
r.discard(r.prefix + "/" + name)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *walReader) nextBatch(pending map[int64]Entry) (batch []Entry, done bool) {
|
|
||||||
for i := 0; i < r.concurrency; i++ {
|
|
||||||
entry, ok := pending[r.seq+int64(i)]
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(entry.Name, segSuffix) {
|
|
||||||
if i == 0 && strings.HasSuffix(entry.Name, endSuffix) {
|
|
||||||
r.discard(r.prefix + "/" + entry.Name)
|
|
||||||
return nil, true
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
batch = append(batch, entry)
|
|
||||||
}
|
|
||||||
return batch, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *walReader) fetch(batch []Entry) ([][]byte, error) {
|
|
||||||
chunks := make([][]byte, len(batch))
|
|
||||||
failures := make([]error, len(batch))
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
for i, entry := range batch {
|
|
||||||
if entry.Inline != nil {
|
|
||||||
chunks[i] = entry.Inline
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
wg.Add(1)
|
|
||||||
go func(i int, name string) {
|
|
||||||
defer wg.Done()
|
|
||||||
chunks[i], failures[i] = r.storage.Get(r.ctx, r.prefix+"/"+name)
|
|
||||||
}(i, entry.Name)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
for i := range batch {
|
|
||||||
if failures[i] != nil {
|
|
||||||
return nil, failures[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return chunks, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *walReader) discard(name string) {
|
func (r *walReader) discard(name string) {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/common"
|
"github.com/xtls/xray-core/common"
|
||||||
@@ -27,11 +26,6 @@ const (
|
|||||||
downlinkDir = "s2c"
|
downlinkDir = "s2c"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
spareCount = 2
|
|
||||||
spareIdleStop = 10 * time.Minute
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
|
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
|
||||||
return new(Config)
|
return new(Config)
|
||||||
@@ -84,152 +78,30 @@ func streamConfig(streamSettings *internet.MemoryStreamConfig) (*Config, error)
|
|||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type spareRing struct {
|
|
||||||
storage Storage
|
|
||||||
session string
|
|
||||||
ring ring
|
|
||||||
at time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type spareBox struct {
|
|
||||||
ch chan spareRing
|
|
||||||
busy atomic.Bool
|
|
||||||
last atomic.Int64
|
|
||||||
stop atomic.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var spareBoxes sync.Map
|
|
||||||
|
|
||||||
func spareBoxFor(config *Config) *spareBox {
|
|
||||||
if b, ok := spareBoxes.Load(config); ok {
|
|
||||||
return b.(*spareBox)
|
|
||||||
}
|
|
||||||
b, _ := spareBoxes.LoadOrStore(config, &spareBox{ch: make(chan spareRing, spareCount)})
|
|
||||||
return b.(*spareBox)
|
|
||||||
}
|
|
||||||
|
|
||||||
func dropSpare(s spareRing) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
s.storage.Delete(ctx, sessionPrefix(s.session))
|
|
||||||
s.storage.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func takeSpare(config *Config, p params) (spareRing, bool) {
|
|
||||||
box := spareBoxFor(config)
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case s := <-box.ch:
|
|
||||||
if time.Since(s.at) < p.sessionTTL*2/3 {
|
|
||||||
return s, true
|
|
||||||
}
|
|
||||||
go dropSpare(s)
|
|
||||||
default:
|
|
||||||
return spareRing{}, false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func warmSpares(streamSettings *internet.MemoryStreamConfig, config *Config, p params) {
|
|
||||||
spareBoxFor(config).last.Store(time.Now().UnixNano())
|
|
||||||
fillSpares(streamSettings, config, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fillSpares(streamSettings *internet.MemoryStreamConfig, config *Config, p params) {
|
|
||||||
box := spareBoxFor(config)
|
|
||||||
if !box.busy.CompareAndSwap(false, true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
defer box.busy.Store(false)
|
|
||||||
for n := len(box.ch); n > 0; n-- {
|
|
||||||
select {
|
|
||||||
case s := <-box.ch:
|
|
||||||
if time.Since(s.at) < p.sessionTTL/3 {
|
|
||||||
box.ch <- s
|
|
||||||
} else {
|
|
||||||
dropSpare(s)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for len(box.ch) < cap(box.ch) {
|
|
||||||
for range 40 {
|
|
||||||
if box.stop.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
time.Sleep(50 * time.Millisecond)
|
|
||||||
}
|
|
||||||
if box.stop.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
storage, err := newStorage(streamSettings)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
storage = sessionStorage(storage)
|
|
||||||
slots, ok := storage.(slotStore)
|
|
||||||
if !ok {
|
|
||||||
storage.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
session, err := newSessionID()
|
|
||||||
if err != nil {
|
|
||||||
storage.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
||||||
r, err := makeRing(ctx, slots, session, p)
|
|
||||||
cancel()
|
|
||||||
if err != nil {
|
|
||||||
dropSpare(spareRing{storage: storage, session: session})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case box.ch <- spareRing{storage: storage, session: session, ring: r, at: time.Now()}:
|
|
||||||
default:
|
|
||||||
storage.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
time.AfterFunc(p.sessionTTL/4, func() {
|
|
||||||
if !box.stop.Load() && time.Since(time.Unix(0, box.last.Load())) < spareIdleStop {
|
|
||||||
fillSpares(streamSettings, config, p)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
|
func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
|
||||||
config, err := streamConfig(streamSettings)
|
config, err := streamConfig(streamSettings)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
p := paramsFromConfig(config)
|
storage, err := newStorage(streamSettings)
|
||||||
|
|
||||||
spare, warm := takeSpare(config, p)
|
|
||||||
storage, session, r := spare.storage, spare.session, spare.ring
|
|
||||||
if !warm {
|
|
||||||
storage, err = newStorage(streamSettings)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
storage = sessionStorage(storage)
|
storage = sessionStorage(storage)
|
||||||
|
|
||||||
session, err = newSessionID()
|
session, err := newSessionID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
storage.Close()
|
storage.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
p := paramsFromConfig(config)
|
||||||
var announce []byte
|
var announce []byte
|
||||||
|
var r ring
|
||||||
slots, ok := storage.(slotStore)
|
slots, ok := storage.(slotStore)
|
||||||
if ok {
|
if ok {
|
||||||
if warm {
|
if r, err = makeRing(ctx, slots, session, p); err == nil {
|
||||||
announce, _ = json.Marshal(r)
|
|
||||||
} else if r, err = makeRing(ctx, slots, session, p); err == nil {
|
|
||||||
announce, _ = json.Marshal(r)
|
announce, _ = json.Marshal(r)
|
||||||
} else {
|
} else {
|
||||||
ok = false
|
ok = false
|
||||||
@@ -243,7 +115,6 @@ func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.Me
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ok {
|
if ok {
|
||||||
warmSpares(streamSettings, config, p)
|
|
||||||
errors.LogInfo(ctx, "opened session ", session, " in ", len(r.Up.Slots), " slots")
|
errors.LogInfo(ctx, "opened session ", session, " in ", len(r.Up.Slots), " slots")
|
||||||
return newSlotConn(context.Background(), slots, r.Up, r.Down, p, func() {
|
return newSlotConn(context.Background(), slots, r.Up, r.Down, p, func() {
|
||||||
storage.Close()
|
storage.Close()
|
||||||
@@ -449,13 +320,12 @@ func (l *Listener) collect() error {
|
|||||||
}
|
}
|
||||||
l.mu.Unlock()
|
l.mu.Unlock()
|
||||||
|
|
||||||
var first error
|
|
||||||
for _, session := range expired {
|
for _, session := range expired {
|
||||||
if err := l.storage.Delete(l.ctx, sessionPrefix(session)); err != nil && first == nil {
|
if err := l.storage.Delete(l.ctx, sessionPrefix(session)); err != nil {
|
||||||
first = err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return first
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Listener) Addr() net.Addr {
|
func (l *Listener) Addr() net.Addr {
|
||||||
|
|||||||
@@ -69,13 +69,6 @@ func pairWith(t *testing.T, streamSettings *internet.MemoryStreamConfig) (client
|
|||||||
}
|
}
|
||||||
|
|
||||||
return client, server, func() {
|
return client, server, func() {
|
||||||
if config, err := streamConfig(streamSettings); err == nil {
|
|
||||||
box := spareBoxFor(config)
|
|
||||||
box.stop.Store(true)
|
|
||||||
for box.busy.Load() {
|
|
||||||
time.Sleep(10 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
client.Close()
|
client.Close()
|
||||||
server.Close()
|
server.Close()
|
||||||
listener.Close()
|
listener.Close()
|
||||||
|
|||||||
Reference in New Issue
Block a user