Compare commits

..
5 Commits
Author SHA1 Message Date
Fangliding fa58459817 Refactor 2026-10-21 13:20:30 +08:00
Fangliding 8d0deebf86 simple clear 2026-10-18 22:30:54 +08:00
Fangliding 0396b34eff test 2026-10-18 22:10:39 +08:00
Roman Rublev 83147cf3a7 SS2022 inbound: Don't end a UDP session when its first source address expires
The relay session context was derived from the inbound connection of the
source address that created the session. After a client changes its source
address (NAT rebinding), the UDP worker of the old address is cleaned up after
~2 minutes of inactivity and cancels that context, which tears down the
dispatched outbound flow; the next packet then creates a brand-new session and
outbound flow (new local port on the server, reset replay window).

Sessions are routed by client session ID (SIP022 3.2.4), so detach the session
context from the originating connection's cancellation and let the session's
own inactivity timer govern its lifetime.
2026-10-18 16:28:31 +03:00
Roman Rublev 8d0a7cf856 SS2022 inbound: Fix data races and duplicate acceptance when decoding one UDP session concurrently
Packets of one client session can be processed by two inbound workers at
the same time (e.g. the client's old and new source address after a NAT
rebinding). The sliding window was checked and updated in two separate
critical sections, and clientBodyCipher and the session's User were read
without the lock, so the same packet ID could be accepted more than once
and go test -race reported data races.

Re-check the window and publish the cached body cipher in one critical
section after the packet is fully validated, and always initialize the
session's User under the lock.
2026-10-18 16:28:31 +03:00
22 changed files with 511 additions and 352 deletions
+11 -5
View File
@@ -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://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty RUN mkdir -p /tmp/empty
@@ -40,15 +40,21 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
/tmp/var/log/xray/access.log \
/tmp/var/log/xray/error.log
# Build finally image # Build finally image
FROM gcr.io/distroless/static:nonroot FROM gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:65532 --chmod=750 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:65532 --chmod=640 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=65532:65532 --chmod=700 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/
VOLUME /usr/local/etc/xray VOLUME /usr/local/etc/xray
VOLUME /var/log/xray VOLUME /var/log/xray
+11 -5
View File
@@ -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://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geoip/geoip-full.dat /tmp/geodat/geoip.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geoip.dat /tmp/geodat/geoip.dat
ADD https://raw.githubusercontent.com/XTLS/Xray-rules-dat/release/geosite/geosite-expand.dat /tmp/geodat/geosite.dat ADD https://raw.githubusercontent.com/Loyalsoldier/v2ray-rules-dat/release/geosite.dat /tmp/geodat/geosite.dat
RUN mkdir -p /tmp/empty RUN mkdir -p /tmp/empty
@@ -40,6 +40,11 @@ RUN echo '{}' >/tmp/usr/local/etc/xray/11_geodata.json
RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json RUN echo '{}' >/tmp/usr/local/etc/xray/12_env.json
RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json RUN echo '{}' >/tmp/usr/local/etc/xray/99_version.json
# Create log files
RUN mkdir -p /tmp/var/log/xray && touch \
/tmp/var/log/xray/access.log \
/tmp/var/log/xray/error.log
# Build finally image # Build finally image
# Note on Distroless Base Image and Architecture Support: # Note on Distroless Base Image and Architecture Support:
# - The official 'gcr.io/distroless/static' image provided by Google only supports a limited set of architectures for Linux: # - The official 'gcr.io/distroless/static' image provided by Google only supports a limited set of architectures for Linux:
@@ -55,9 +60,10 @@ FROM --platform=linux/amd64 gcr.io/distroless/static:nonroot
COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray COPY --from=build --chown=0:0 --chmod=755 /src/xray /usr/local/bin/xray
COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray COPY --from=build --chown=65532:65532 --chmod=755 /tmp/empty /usr/local/share/xray
COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/ COPY --from=build --chown=65532:65532 --chmod=644 /tmp/geodat/*.dat /usr/local/share/xray/
COPY --from=build --chown=0:65532 --chmod=750 /tmp/empty /usr/local/etc/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /usr/local/etc/xray
COPY --from=build --chown=0:65532 --chmod=640 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/ COPY --from=build --chown=0:0 --chmod=644 /tmp/usr/local/etc/xray/*.json /usr/local/etc/xray/
COPY --from=build --chown=65532:65532 --chmod=700 /tmp/empty /var/log/xray COPY --from=build --chown=0:0 --chmod=755 /tmp/empty /var/log/xray
COPY --from=build --chown=65532:65532 --chmod=600 /tmp/var/log/xray/*.log /var/log/xray/
VOLUME /usr/local/etc/xray VOLUME /usr/local/etc/xray
VOLUME /var/log/xray VOLUME /var/log/xray
@@ -40,7 +40,7 @@ jobs:
max_attempts: 60 max_attempts: 60
command: | command: |
[ -d 'resources' ] || mkdir resources [ -d 'resources' ] || mkdir resources
LIST=('XTLS Xray-rules-dat geoip/geoip-lite geoip' 'XTLS Xray-rules-dat geosite/geosite geosite') LIST=('Loyalsoldier v2ray-rules-dat geoip geoip' 'Loyalsoldier v2ray-rules-dat geosite geosite')
for i in "${LIST[@]}" for i in "${LIST[@]}"
do do
INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3,$4}')) INFO=($(echo $i | awk 'BEGIN{FS=" ";OFS=" "} {print $1,$2,$3,$4}'))
+4 -4
View File
@@ -128,7 +128,7 @@ func TestDNSScriptGeoIPFallback(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
script := ` script := `
local servers = require("xray.dns").Servers local servers = require("xray.dns").Servers
local ru_ips = require("xray.geodata").BuildIPMatcher("geoip:ru") local us_ips = require("xray.geodata").BuildIPMatcher("geoip:us")
local by_id = {} local by_id = {}
for _, server in ipairs(servers) do for _, server in ipairs(servers) do
@@ -138,7 +138,7 @@ assert(by_id.primary and by_id.fallback, "primary and fallback DNS servers are r
function HandleDNSQuery(domain, ipv4, ipv6, fake) function HandleDNSQuery(domain, ipv4, ipv6, fake)
local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake) local ips, ttl, err = by_id.primary:Query(domain, ipv4, ipv6, fake)
if not err and ru_ips:AnyMatch(ips) then if not err and us_ips:AnyMatch(ips) then
return ips, ttl, nil return ips, ttl, nil
end end
return by_id.fallback:Query(domain, ipv4, ipv6, fake) return by_id.fallback:Query(domain, ipv4, ipv6, fake)
@@ -152,7 +152,7 @@ end
primary := &scriptNameServer{ primary := &scriptNameServer{
name: "primary", name: "primary",
answers: map[string]net.IP{ answers: map[string]net.IP{
"ru.example": net.ParseIP("2a02:6b8::feed:0ff"), "us.example": net.ParseIP("2001:4860:4860::8888"),
"other.example": net.ParseIP("127.0.0.1"), "other.example": net.ParseIP("127.0.0.1"),
}, },
ttl: 30, ttl: 30,
@@ -191,7 +191,7 @@ end
ip net.IP ip net.IP
ttl uint32 ttl uint32
}{ }{
{"Ru.Example.", net.ParseIP("2a02:6b8::feed:0ff"), 30}, {"Us.Example.", net.ParseIP("2001:4860:4860::8888"), 30},
{"other.example", net.ParseIP("9.9.9.9"), 60}, {"other.example", net.ParseIP("9.9.9.9"), 60},
} { } {
ips, ttl, err := server.LookupIP(tc.domain, option) ips, ttl, err := server.LookupIP(tc.domain, option)
+1 -1
View File
@@ -415,7 +415,7 @@ func BenchmarkMphDomainMatcher(b *testing.B) {
func BenchmarkMultiGeoIPMatcher(b *testing.B) { func BenchmarkMultiGeoIPMatcher(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:ir", "geoip:ru"}) rules, err := geodata.ParseIPRules([]string{"geoip:cn", "geoip:jp", "geoip:ca", "geoip:us"})
common.Must(err) common.Must(err)
matcher, err := NewIPMatcher(rules, MatcherAsType_Target) matcher, err := NewIPMatcher(rules, MatcherAsType_Target)
+7 -7
View File
@@ -400,13 +400,13 @@ func TestIPMatcher4CN(t *testing.T) {
} }
} }
func TestIPMatcher6RU(t *testing.T) { func TestIPMatcher6US(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:ru") matcher := buildIPMatcher("geoip:us")
if !matcher.Match(xnet.ParseAddress("2a02:6b8::feed:0ff").IP()) { if !matcher.Match(xnet.ParseAddress("2001:4860:4860::8888").IP()) {
t.Error("expect RU geoip contain 2a02:6b8::feed:0ff, but actually not") t.Error("expect US geoip contain 2001:4860:4860::8888, but actually not")
} }
} }
@@ -423,11 +423,11 @@ func BenchmarkIPMatcher4CN(b *testing.B) {
} }
} }
func BenchmarkIPMatcher6RU(b *testing.B) { func BenchmarkIPMatcher6US(b *testing.B) {
b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) b.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
matcher := buildIPMatcher("geoip:ru") matcher := buildIPMatcher("geoip:us")
ip := xnet.ParseAddress("2a02:6b8::feed:0ff").IP() ip := xnet.ParseAddress("2001:4860:4860::8888").IP()
b.ResetTimer() b.ResetTimer()
+3 -3
View File
@@ -11,7 +11,7 @@ func TestParseIPRules(t *testing.T) {
t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources")) t.Setenv("xray.location.asset", filepath.Join("..", "..", "resources"))
rules := []string{ rules := []string{
"geoip:ru", "geoip:us",
"geoip:cn", "geoip:cn",
"!geoip:cn", "!geoip:cn",
"!!geoip:cn", "!!geoip:cn",
@@ -20,9 +20,9 @@ func TestParseIPRules(t *testing.T) {
"!geoip:!cn", "!geoip:!cn",
"ext:geoip.dat:!cn", "ext:geoip.dat:!cn",
"ext:geoip.dat:!!cn", "ext:geoip.dat:!!cn",
"ext:geoip.dat:ir", "ext:geoip.dat:ca",
"ext-ip:geoip.dat:!cn", "ext-ip:geoip.dat:!cn",
"ext-ip:geoip.dat:!ir", "ext-ip:geoip.dat:!ca",
"192.168.0.0/24", "192.168.0.0/24",
"!192.168.0.0/24", "!192.168.0.0/24",
"!!192.168.0.0/24", "!!192.168.0.0/24",
+16 -33
View File
@@ -26,20 +26,20 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
} }
var procFiles []string var procFile string
switch network { switch network {
case "tcp": case "tcp":
if net.ParseIP(srcIP).To4() != nil { if net.ParseIP(srcIP).To4() != nil {
procFiles = []string{"/proc/net/tcp"} procFile = "/proc/net/tcp"
} else { } else {
procFiles = []string{"/proc/net/tcp6"} procFile = "/proc/net/tcp6"
} }
case "udp": case "udp":
if net.ParseIP(srcIP).To4() != nil { if net.ParseIP(srcIP).To4() != nil {
procFiles = []string{"/proc/net/udp", "/proc/net/udp6"} procFile = "/proc/net/udp"
} else { } else {
procFiles = []string{"/proc/net/udp6", "/proc/net/udp"} procFile = "/proc/net/udp6"
} }
default: default:
panic("Unsupported network type for process lookup.") panic("Unsupported network type for process lookup.")
@@ -50,17 +50,12 @@ func FindProcess(network, srcIP string, srcPort uint16, destIP string, destPort
return 0, "", "", errors.New("failed to format address: ", err) return 0, "", "", errors.New("failed to format address: ", err)
} }
portFallback := "" inode, err := findInodeInFile(procFile, targetHexAddr)
if network == "udp" {
portFallback = fmt.Sprintf(":%04X", srcPort)
}
inode, err := findInodeInFiles(procFiles, targetHexAddr, portFallback)
if err != nil { if err != nil {
return 0, "", "", errors.New("could not search in ", strings.Join(procFiles, " and ")).Base(err) return 0, "", "", errors.New("could not search in ", procFile).Base(err)
} }
if inode == "" { if inode == "" {
return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", strings.Join(procFiles, " or ")) return 0, "", "", errors.New("connection for ", srcIP, ":", srcPort, " not found in ", procFile)
} }
pidStr, err := findPidByInode(inode) pidStr, err := findPidByInode(inode)
@@ -107,43 +102,31 @@ func formatLittleEndianString(addr net.IP, port Port) (string, error) {
return fmt.Sprintf("%s:%s", ipHex, portHex), nil return fmt.Sprintf("%s:%s", ipHex, portHex), nil
} }
func findInodeInFiles(filePaths []string, targetHexAddr, portFallback string) (string, error) { func findInodeInFile(filePath, targetHexAddr string) (string, error) {
onlyMatchedPort := ""
for i, filePath := range filePaths {
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
if i > 0 && os.IsNotExist(err) {
continue
}
return "", err return "", err
} }
defer file.Close() defer file.Close()
scanner := bufio.NewScanner(file) scanner := bufio.NewScanner(file)
for scanner.Scan() { for scanner.Scan() {
fields := strings.Fields(scanner.Text()) line := scanner.Text()
fields := strings.Fields(line)
if len(fields) < 10 { if len(fields) < 10 {
continue continue
} }
localAddress := fields[1] localAddress := fields[1]
if localAddress == targetHexAddr { if localAddress == targetHexAddr {
return fields[9], nil inode := fields[9]
} return inode, nil
if portFallback != "" && strings.HasSuffix(localAddress, portFallback) {
wildcardIP := strings.TrimSuffix(localAddress, portFallback)
if (len(wildcardIP) == 8 || len(wildcardIP) == 32) && strings.Trim(wildcardIP, "0") == "" {
onlyMatchedPort = fields[9]
}
}
}
if err := scanner.Err(); err != nil {
return "", err
} }
} }
return onlyMatchedPort, nil return "", scanner.Err()
} }
func findPidByInode(inode string) (string, error) { func findPidByInode(inode string) (string, error) {
+29 -30
View File
@@ -3,7 +3,11 @@ package signal
import ( import (
"context" "context"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/task"
) )
type ActivityUpdater interface { type ActivityUpdater interface {
@@ -13,10 +17,10 @@ type ActivityUpdater interface {
type ActivityTimer struct { type ActivityTimer struct {
mu sync.RWMutex mu sync.RWMutex
updated chan struct{} updated chan struct{}
checker *time.Timer checkTask *task.Periodic
timeout time.Duration
onTimeout func() onTimeout func()
consumed bool consumed atomic.Bool
once sync.Once
} }
func (t *ActivityTimer) Update() { func (t *ActivityTimer) Update() {
@@ -26,29 +30,30 @@ func (t *ActivityTimer) Update() {
} }
} }
func (t *ActivityTimer) check() bool { func (t *ActivityTimer) check() error {
select { select {
case <-t.updated: case <-t.updated:
return true
default: default:
t.finish() t.finish()
return false
} }
return nil
} }
func (t *ActivityTimer) finish() { func (t *ActivityTimer) finish() {
t.once.Do(func() {
t.consumed.Store(true)
t.mu.Lock() t.mu.Lock()
if t.consumed { defer t.mu.Unlock()
t.mu.Unlock()
return common.CloseIfExists(t.checkTask)
}
t.checker.Stop()
t.consumed = true
t.mu.Unlock()
t.onTimeout() t.onTimeout()
})
} }
func (t *ActivityTimer) SetTimeout(timeout time.Duration) { func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
if t.consumed.Load() {
return
}
if timeout == 0 { if timeout == 0 {
t.finish() t.finish()
return return
@@ -56,31 +61,25 @@ func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
if t.consumed { // double check, just in case
if t.consumed.Load() {
return return
} }
t.timeout = timeout newCheckTask := &task.Periodic{
t.checker.Reset(timeout) Interval: timeout,
Execute: t.check,
}
common.CloseIfExists(t.checkTask)
t.checkTask = newCheckTask
t.Update()
common.Must(newCheckTask.Start())
} }
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer { func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
timer := &ActivityTimer{ timer := &ActivityTimer{
updated: make(chan struct{}, 1), updated: make(chan struct{}, 1),
onTimeout: cancel, onTimeout: cancel,
timeout: timeout,
}
timer.checker = time.AfterFunc(timeout, func() {
if timer.check() {
timer.mu.Lock()
defer timer.mu.Unlock()
if timer.consumed {
return
}
timer.checker.Reset(timer.timeout)
}
})
if timeout == 0 {
timer.SetTimeout(0)
} }
timer.SetTimeout(timeout)
return timer return timer
} }
+2 -2
View File
@@ -9,7 +9,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.1 github.com/klauspost/cpuid/v2 v2.4.0
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
@@ -23,7 +23,7 @@ require (
github.com/yuin/gopher-lua v1.1.2 github.com/yuin/gopher-lua v1.1.2
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.58.0
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
golang.org/x/net v0.61.0 golang.org/x/net v0.61.0
golang.org/x/sync v0.24.0 golang.org/x/sync v0.24.0
+4 -4
View File
@@ -33,8 +33,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.1 h1:I9KxadAkvo+QjtcEE2f2tyfEvmIla/T0m+14PrZNl0U= github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw=
github.com/klauspost/cpuid/v2 v2.4.1/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= github.com/klauspost/cpuid/v2 v2.4.0/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=
@@ -97,8 +97,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a h1:V5oicZXS0oqZx30gY/apgSVD7YvNBfrIOdcMnlelJY4= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582 h1:wjDBrGbfLuifgrVLFEWUBJYAfh5Q1wkMc5t0FY0tCbs=
golang.org/x/crypto/x509roots/fallback v0.0.0-20261020153306-9f2b28d4331a/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E= golang.org/x/crypto/x509roots/fallback v0.0.0-20261005185213-c3db4df58582/go.mod h1:HPze8vhfG6fO06AM+VSvxRm4E3+5Yk375mgrJ5M2z1E=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+8 -14
View File
@@ -53,7 +53,12 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
return nil, err return nil, err
} }
udpCodec, err := NewUDPServerCodec(method, psk, 500*time.Second) user := &protocol.MemoryUser{
Email: config.Email,
Level: uint32(config.Level),
}
udpCodec, err := NewUDPServerCodec(method, psk, user, 500*time.Second)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -64,10 +69,7 @@ func NewServer(ctx context.Context, config *ServerConfig) (*Inbound, error) {
method: method, method: method,
psk: psk, psk: psk,
saltFilter: antireplay.NewMapFilter[[32]byte](60), saltFilter: antireplay.NewMapFilter[[32]byte](60),
user: &protocol.MemoryUser{ user: user,
Email: config.Email,
Level: uint32(config.Level),
},
udpCodec: udpCodec, udpCodec: udpCodec,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
}, nil }, nil
@@ -155,20 +157,12 @@ func (i *Inbound) processUDP(ctx context.Context, conn stat.Connection, dispatch
} }
for _, b := range mb { for _, b := range mb {
decoded, err := i.udpCodec.DecodePacket(b.Bytes()) sessionItem, decoded, err := i.udpCodec.DecodePacket(b.Bytes())
b.Release() b.Release()
if err != nil || decoded.HeaderType != HeaderTypeClient { if err != nil || decoded.HeaderType != HeaderTypeClient {
continue continue
} }
sessionItem := i.udpCodec.GetSession(decoded.SessionID)
if sessionItem.User == nil {
sessionItem.Lock()
if sessionItem.User == nil {
sessionItem.User = i.user
}
sessionItem.Unlock()
}
link := sessionItem.link.Load() link := sessionItem.link.Load()
if link != nil { if link != nil {
sessionItem.UpdateConn(conn) sessionItem.UpdateConn(conn)
+12 -62
View File
@@ -2,8 +2,6 @@ package shadowsocks_2022
import ( import (
"context" "context"
"crypto/cipher"
"encoding/binary"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@@ -41,8 +39,7 @@ type MultiUserInbound struct {
usersByEmail *utils.TypedSyncMap[string, *protocol.MemoryUser] usersByEmail *utils.TypedSyncMap[string, *protocol.MemoryUser]
userCount atomic.Int64 userCount atomic.Int64
saltFilter *antireplay.ReplayFilter[[32]byte] saltFilter *antireplay.ReplayFilter[[32]byte]
udpSessions *UDPSessionManager udpCodec *UDPMultiServerCodec
udpMasterCipher cipher.Block
policyManager policy.Manager policyManager policy.Manager
} }
@@ -68,7 +65,7 @@ func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiU
return nil, err return nil, err
} }
masterBlock, err := method.NewBlock(masterPSK) udpCodec, err := NewUDPMultiServerCodec(method, masterPSK, 500*time.Second)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -81,8 +78,7 @@ func NewMultiServer(ctx context.Context, config *MultiUserServerConfig) (*MultiU
usersByHash: utils.NewTypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser](), usersByHash: utils.NewTypedSyncMap[[AESBlockSize]byte, *protocol.MemoryUser](),
usersByEmail: utils.NewTypedSyncMap[string, *protocol.MemoryUser](), usersByEmail: utils.NewTypedSyncMap[string, *protocol.MemoryUser](),
saltFilter: antireplay.NewMapFilter[[32]byte](60), saltFilter: antireplay.NewMapFilter[[32]byte](60),
udpSessions: NewUDPSessionManager(500 * time.Second), udpCodec: udpCodec,
udpMasterCipher: masterBlock,
policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager),
} }
@@ -281,68 +277,26 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
} }
for _, b := range mb { for _, b := range mb {
// In multi-user UDP: sessionItem, decoded, user, err := i.udpCodec.DecodePacket(b.Bytes(), func(hash [AESBlockSize]byte) (*protocol.MemoryUser, []byte, bool) {
// Packet header is 16 bytes: Encrypted(SessionID + PacketID) u, ok := i.usersByHash.Load(hash)
// Followed by 16 bytes EIH
packetBytes := b.Bytes()
if len(packetBytes) < 32+1+8+2 {
b.Release()
continue
}
var rawHeader [16]byte
i.udpMasterCipher.Decrypt(rawHeader[:], packetBytes[:16])
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
sessionItem := i.udpSessions.GetOrCreate(sessionID)
if !sessionItem.CheckPacketID(packetID) {
b.Release()
continue
}
var userPSK []byte
var currentUser *protocol.MemoryUser
sessionItem.Lock()
currentUser = sessionItem.User
userPSK = sessionItem.UserPSK
sessionItem.Unlock()
if currentUser == nil {
// Decrypt EIH
decryptedHash := DecryptUDPEIH(i.udpMasterCipher, rawHeader[:], packetBytes[16:32])
user, ok := i.usersByHash.Load(decryptedHash)
if !ok { if !ok {
return nil, nil, false
}
return u, u.Account.(*MemoryAccount).Key, true
})
b.Release() b.Release()
if err != nil || decoded.HeaderType != HeaderTypeClient {
continue continue
} }
currentUser = user
userPSK = user.Account.(*MemoryAccount).Key
}
decoded, err := sessionItem.DecryptAESPayload(i.method, userPSK, sessionID, packetID, rawHeader[:], packetBytes[32:])
b.Release()
if err != nil {
continue
}
sessionItem.Lock()
if sessionItem.User == nil {
sessionItem.User = currentUser
sessionItem.UserPSK = userPSK
}
sessionItem.Unlock()
link := sessionItem.link.Load() link := sessionItem.link.Load()
if link != nil { if link != nil {
sessionItem.UpdateConn(conn) sessionItem.UpdateConn(conn)
} else { } else {
userPSK := user.Account.(*MemoryAccount).Key
var err error var err error
link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) { link, err = sessionItem.EnsureLink(ctx, conn, decoded.Destination, dispatcher, i.policyManager, func(replyDest net.Destination, payload []byte) ([]byte, error) {
return i.encodeServerUDPPacket(sessionID, userPSK, replyDest, payload) return i.udpCodec.EncodeServerPacket(decoded.SessionID, userPSK, replyDest, payload)
}) })
if err != nil { if err != nil {
continue continue
@@ -356,7 +310,3 @@ func (i *MultiUserInbound) processUDP(ctx context.Context, conn stat.Connection,
} }
} }
} }
func (i *MultiUserInbound) encodeServerUDPPacket(clientSessionID uint64, userPSK []byte, dest net.Destination, payload []byte) ([]byte, error) {
return i.udpSessions.EncodeServerPacket(i.method, userPSK, clientSessionID, dest, payload)
}
-2
View File
@@ -243,7 +243,6 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
dest.Network = net.Network_UDP dest.Network = net.Network_UDP
sessionItem := i.udpSessions.GetOrCreate(sessionID) sessionItem := i.udpSessions.GetOrCreate(sessionID)
if sessionItem.User == nil {
sessionItem.Lock() sessionItem.Lock()
if sessionItem.User == nil { if sessionItem.User == nil {
sessionItem.User = &protocol.MemoryUser{ sessionItem.User = &protocol.MemoryUser{
@@ -252,7 +251,6 @@ func (i *RelayInbound) processUDP(ctx context.Context, conn stat.Connection, dis
} }
} }
sessionItem.Unlock() sessionItem.Unlock()
}
link := sessionItem.link.Load() link := sessionItem.link.Load()
if link != nil { if link != nil {
sessionItem.UpdateConn(conn) sessionItem.UpdateConn(conn)
+3 -3
View File
@@ -30,7 +30,7 @@ type Outbound struct {
method *CipherMethod method *CipherMethod
pskList [][]byte pskList [][]byte
finalPSK []byte finalPSK []byte
udpCodec *UDPPacketCodec udpCodec *UDPClientCodec
policyManager policy.Manager policyManager policy.Manager
} }
@@ -50,9 +50,9 @@ func NewClient(ctx context.Context, config *ClientConfig) (*Outbound, error) {
} }
finalPSK := pskList[len(pskList)-1] finalPSK := pskList[len(pskList)-1]
udpCodec, err := NewUDPPacketCodec(method, pskList) udpCodec, err := NewUDPClientCodec(method, pskList)
if err != nil { if err != nil {
return nil, errors.New("failed to create udp packet codec").Base(err) return nil, errors.New("failed to create udp client codec").Base(err)
} }
v := core.MustFromContext(ctx) v := core.MustFromContext(ctx)
+323 -123
View File
@@ -7,33 +7,78 @@ import (
"io" "io"
"math" "math"
mrand "math/rand/v2" mrand "math/rand/v2"
"sync"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/xtls/xray-core/common/buf" "github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors" "github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net" "github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
) )
type UDPCodec struct { type UDPClientCodec struct {
method *CipherMethod method *CipherMethod
pskList [][]byte pskList [][]byte
psk []byte psk []byte
blockCipher cipher.Block blockCipher cipher.Block
blockCiphers []cipher.Block blockCiphers []cipher.Block
chachaCipher cipher.AEAD chachaCipher cipher.AEAD
}
func NewUDPClientCodec(method *CipherMethod, pskList [][]byte) (*UDPClientCodec, error) {
if len(pskList) == 0 {
return nil, errors.New("empty psk list")
}
if method.IsChaCha && len(pskList) > 1 {
return nil, errors.New("multi-key is not supported for chacha20-poly1305")
}
finalPSK := pskList[len(pskList)-1]
c := &UDPClientCodec{
method: method,
pskList: pskList,
psk: finalPSK,
}
var err error
if method.IsChaCha {
c.chachaCipher, err = method.NewUDPCipher(finalPSK)
} else {
c.blockCipher, err = method.NewBlock(finalPSK)
}
if err != nil {
return nil, err
}
if len(pskList) > 1 {
c.blockCiphers = make([]cipher.Block, len(pskList))
for i, psk := range pskList {
c.blockCiphers[i], err = method.NewBlock(psk)
if err != nil {
return nil, err
}
}
}
return c, nil
}
func NewUDPPacketCodec(method *CipherMethod, pskList [][]byte) (*UDPClientCodec, error) {
return NewUDPClientCodec(method, pskList)
}
type UDPServerCodec struct {
method *CipherMethod
psk []byte
user *protocol.MemoryUser
blockCipher cipher.Block
chachaCipher cipher.AEAD
sessions *UDPSessionManager sessions *UDPSessionManager
} }
type ( func NewUDPServerCodec(method *CipherMethod, psk []byte, user *protocol.MemoryUser, sessionTimeout time.Duration) (*UDPServerCodec, error) {
UDPPacketCodec = UDPCodec c := &UDPServerCodec{
UDPServerCodec = UDPCodec
)
func newUDPCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
c := &UDPCodec{
method: method, method: method,
psk: psk, psk: psk,
user: user,
sessions: NewUDPSessionManager(sessionTimeout),
} }
var err error var err error
if method.IsChaCha { if method.IsChaCha {
@@ -47,46 +92,129 @@ func newUDPCodec(method *CipherMethod, psk []byte) (*UDPCodec, error) {
return c, nil return c, nil
} }
func NewUDPPacketCodec(method *CipherMethod, pskList [][]byte) (*UDPCodec, error) { func (c *UDPServerCodec) Sessions() *UDPSessionManager {
if method.IsChaCha && len(pskList) > 1 {
return nil, errors.New("multi-key is not supported for chacha20-poly1305")
}
finalPSK := pskList[len(pskList)-1]
c, err := newUDPCodec(method, finalPSK)
if err != nil {
return nil, err
}
c.pskList = pskList
if len(pskList) > 1 {
c.blockCiphers = make([]cipher.Block, len(pskList))
for i, psk := range pskList {
c.blockCiphers[i], err = method.NewBlock(psk)
if err != nil {
return nil, err
}
}
}
return c, nil
}
func NewUDPServerCodec(method *CipherMethod, psk []byte, sessionTimeout time.Duration) (*UDPCodec, error) {
c, err := newUDPCodec(method, psk)
if err != nil {
return nil, err
}
c.sessions = NewUDPSessionManager(sessionTimeout)
return c, nil
}
func (c *UDPCodec) Sessions() *UDPSessionManager {
return c.sessions return c.sessions
} }
func (c *UDPCodec) GetSession(sessionID uint64) *ServerUDPSession { func (c *UDPServerCodec) GetSession(sessionID uint64) *ServerUDPSession {
if c.sessions == nil { if c.sessions == nil {
return nil return nil
} }
return c.sessions.GetOrCreate(sessionID) return c.sessions.Get(sessionID)
}
type UDPMultiServerCodec struct {
method *CipherMethod
masterPSK []byte
masterBlock cipher.Block
sessions *UDPSessionManager
}
func NewUDPMultiServerCodec(method *CipherMethod, masterPSK []byte, sessionTimeout time.Duration) (*UDPMultiServerCodec, error) {
if method.IsChaCha {
return nil, errors.New("multi-user is not supported for chacha20-poly1305")
}
masterBlock, err := method.NewBlock(masterPSK)
if err != nil {
return nil, err
}
return &UDPMultiServerCodec{
method: method,
masterPSK: masterPSK,
masterBlock: masterBlock,
sessions: NewUDPSessionManager(sessionTimeout),
}, nil
}
func (c *UDPMultiServerCodec) Sessions() *UDPSessionManager {
return c.sessions
}
func (c *UDPMultiServerCodec) GetSession(sessionID uint64) *ServerUDPSession {
if c.sessions == nil {
return nil
}
return c.sessions.Get(sessionID)
}
type UserLookupFunc func(hash [AESBlockSize]byte) (*protocol.MemoryUser, []byte, bool)
func (c *UDPMultiServerCodec) DecodePacket(data []byte, userLookup UserLookupFunc) (*ServerUDPSession, DecodedUDPPacket, *protocol.MemoryUser, error) {
if len(data) < 2*AESBlockSize+AEADTagSize+RequestHeaderFixedChunkLength {
return nil, DecodedUDPPacket{}, nil, ErrPacketTooShort
}
var rawHeader [16]byte
c.masterBlock.Decrypt(rawHeader[:], data[:16])
sessionID := binary.BigEndian.Uint64(rawHeader[:8])
packetID := binary.BigEndian.Uint64(rawHeader[8:16])
sessionItem := c.sessions.Get(sessionID)
var currentUser *protocol.MemoryUser
var userPSK []byte
var bodyAead cipher.AEAD
if sessionItem != nil {
sessionItem.Lock()
currentUser = sessionItem.User
userPSK = sessionItem.UserPSK
bodyAead = sessionItem.clientBodyCipher
sessionItem.Unlock()
if currentUser != nil {
if !sessionItem.CheckPacketID(packetID) {
return nil, DecodedUDPPacket{}, nil, ErrPacketIdNotUnique
}
}
}
if currentUser == nil {
decryptedHash := DecryptUDPEIH(c.masterBlock, rawHeader[:], data[16:32])
user, psk, ok := userLookup(decryptedHash)
if !ok {
return nil, DecodedUDPPacket{}, nil, errors.New("user not found")
}
currentUser = user
userPSK = psk
}
if bodyAead == nil {
bodyKey := DeriveSessionSubKey(userPSK, rawHeader[:8], c.method.KeySaltLength)
var err error
bodyAead, err = c.method.NewAEAD(bodyKey)
if err != nil {
return nil, DecodedUDPPacket{}, nil, err
}
}
bodyNonce := rawHeader[4:16]
bodyCipher := data[32:]
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
if err != nil {
return nil, DecodedUDPPacket{}, nil, errors.New("failed to decrypt aes udp body").Base(err)
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain)
if err != nil {
return nil, DecodedUDPPacket{}, nil, err
}
if decoded.HeaderType != HeaderTypeClient {
return nil, DecodedUDPPacket{}, nil, ErrBadHeaderType
}
if sessionItem == nil {
sessionItem = c.sessions.GetOrCreate(sessionID)
}
if !sessionItem.Commit(packetID, bodyAead, currentUser, userPSK) {
return nil, DecodedUDPPacket{}, nil, ErrPacketIdNotUnique
}
return sessionItem, decoded, currentUser, nil
}
func (c *UDPMultiServerCodec) EncodeServerPacket(clientSessionID uint64, userPSK []byte, dest net.Destination, payload []byte) ([]byte, error) {
return c.sessions.EncodeServerPacket(c.method, userPSK, clientSessionID, dest, payload)
} }
type DecodedUDPPacket struct { type DecodedUDPPacket struct {
@@ -193,44 +321,42 @@ func parsePlainUDPPacket(sessionID, packetID uint64, bodyPlain []byte) (DecodedU
}, nil }, nil
} }
func (c *UDPCodec) DecodePacket(data []byte) (DecodedUDPPacket, error) { func (c *UDPServerCodec) DecodePacket(data []byte) (*ServerUDPSession, DecodedUDPPacket, error) {
if len(data) < PacketMinimalHeaderSize { if len(data) < PacketMinimalHeaderSize {
return DecodedUDPPacket{}, ErrPacketTooShort return nil, DecodedUDPPacket{}, ErrPacketTooShort
} }
if c.method.IsChaCha { if c.method.IsChaCha {
if len(data) < PacketNonceSize+AEADTagSize { if len(data) < PacketNonceSize+AEADTagSize {
return DecodedUDPPacket{}, ErrPacketTooShort return nil, DecodedUDPPacket{}, ErrPacketTooShort
} }
nonce := data[:PacketNonceSize] nonce := data[:PacketNonceSize]
ciphertext := data[PacketNonceSize:] ciphertext := data[PacketNonceSize:]
plain, err := c.chachaCipher.Open(nil, nonce, ciphertext, nil) plain, err := c.chachaCipher.Open(nil, nonce, ciphertext, nil)
if err != nil { if err != nil {
return DecodedUDPPacket{}, errors.New("failed to decrypt chacha udp packet").Base(err) return nil, DecodedUDPPacket{}, errors.New("failed to decrypt chacha udp packet").Base(err)
} }
if len(plain) < 16+1+8+2 { if len(plain) < 16+1+8+2 {
return DecodedUDPPacket{}, ErrPacketTooShort return nil, DecodedUDPPacket{}, ErrPacketTooShort
} }
sessionID := binary.BigEndian.Uint64(plain[:8]) sessionID := binary.BigEndian.Uint64(plain[:8])
packetID := binary.BigEndian.Uint64(plain[8:16]) packetID := binary.BigEndian.Uint64(plain[8:16])
sessionItem := c.sessions.GetOrCreate(sessionID)
if !sessionItem.CheckPacketID(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:]) decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:])
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return nil, DecodedUDPPacket{}, err
} }
if decoded.HeaderType != HeaderTypeClient { if decoded.HeaderType != HeaderTypeClient {
return DecodedUDPPacket{}, ErrBadHeaderType return nil, DecodedUDPPacket{}, ErrBadHeaderType
} }
sessionItem.AddPacketID(packetID) sessionItem := c.sessions.GetOrCreate(sessionID)
return decoded, nil if !sessionItem.Commit(packetID, nil, c.user, c.psk) {
return nil, DecodedUDPPacket{}, ErrPacketIdNotUnique
}
return sessionItem, decoded, nil
} }
// AES mode // AES mode
@@ -239,49 +365,48 @@ func (c *UDPCodec) DecodePacket(data []byte) (DecodedUDPPacket, error) {
sessionID := binary.BigEndian.Uint64(rawHeader[:8]) sessionID := binary.BigEndian.Uint64(rawHeader[:8])
packetID := binary.BigEndian.Uint64(rawHeader[8:16]) packetID := binary.BigEndian.Uint64(rawHeader[8:16])
sessionItem := c.sessions.GetOrCreate(sessionID) sessionItem := c.sessions.Get(sessionID)
var bodyAead cipher.AEAD
if sessionItem != nil {
if !sessionItem.CheckPacketID(packetID) { if !sessionItem.CheckPacketID(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique return nil, DecodedUDPPacket{}, ErrPacketIdNotUnique
}
sessionItem.Lock()
bodyAead = sessionItem.clientBodyCipher
sessionItem.Unlock()
} }
return sessionItem.DecryptAESPayload(c.method, c.psk, sessionID, packetID, rawHeader[:], data[16:])
}
func (s *ServerUDPSession) DecryptAESPayload(method *CipherMethod, psk []byte, sessionID, packetID uint64, rawHeader, bodyCipher []byte) (DecodedUDPPacket, error) {
bodyAead := s.clientBodyCipher
isNewCipher := false
if bodyAead == nil { if bodyAead == nil {
bodyKey := DeriveSessionSubKey(psk, rawHeader[:8], method.KeySaltLength) bodyKey := DeriveSessionSubKey(c.psk, rawHeader[:8], c.method.KeySaltLength)
var err error var err error
bodyAead, err = method.NewAEAD(bodyKey) bodyAead, err = c.method.NewAEAD(bodyKey)
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return nil, DecodedUDPPacket{}, err
} }
isNewCipher = true
} }
bodyNonce := rawHeader[4:16] bodyNonce := rawHeader[4:16]
bodyCipher := data[16:]
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil) bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
if err != nil { if err != nil {
return DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err) return nil, DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err)
} }
decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain) decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain)
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return nil, DecodedUDPPacket{}, err
} }
if decoded.HeaderType != HeaderTypeClient { if decoded.HeaderType != HeaderTypeClient {
return DecodedUDPPacket{}, ErrBadHeaderType return nil, DecodedUDPPacket{}, ErrBadHeaderType
} }
s.AddPacketID(packetID) if sessionItem == nil {
sessionItem = c.sessions.GetOrCreate(sessionID)
if isNewCipher {
s.clientBodyCipher = bodyAead
} }
if !sessionItem.Commit(packetID, bodyAead, c.user, c.psk) {
return decoded, nil return nil, DecodedUDPPacket{}, ErrPacketIdNotUnique
}
return sessionItem, decoded, nil
} }
func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, psk []byte) error { func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, psk []byte) error {
@@ -303,6 +428,9 @@ func (s *ServerUDPSession) EnsureServerState(method *CipherMethod, psk []byte) e
if method.IsChaCha { if method.IsChaCha {
var err error var err error
s.serverChaCha, err = method.NewUDPCipher(psk) s.serverChaCha, err = method.NewUDPCipher(psk)
if err != nil {
s.ServerSessionID = 0
}
return err return err
} }
@@ -387,11 +515,12 @@ func (s *ServerUDPSession) EncodeServerPacket(method *CipherMethod, clientSessio
return res, nil return res, nil
} }
func (c *UDPCodec) EncodeServerPacket(clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) { func (c *UDPServerCodec) EncodeServerPacket(clientSessionID uint64, dest net.Destination, payload []byte) ([]byte, error) {
return c.sessions.EncodeServerPacket(c.method, c.psk, clientSessionID, dest, payload) return c.sessions.EncodeServerPacket(c.method, c.psk, clientSessionID, dest, payload)
} }
type serverSessionState struct { type serverSessionState struct {
sync.Mutex
sessionID uint64 sessionID uint64
window *SlidingWindow window *SlidingWindow
cipher cipher.AEAD cipher cipher.AEAD
@@ -399,21 +528,20 @@ type serverSessionState struct {
} }
func (st *serverSessionState) check(packetID uint64) bool { func (st *serverSessionState) check(packetID uint64) bool {
if st.window == nil { st.Lock()
st.window = new(SlidingWindow) defer st.Unlock()
}
return st.window.Check(packetID) return st.window.Check(packetID)
} }
func (st *serverSessionState) add(packetID uint64) { func (st *serverSessionState) commit(packetID uint64) bool {
if st.window == nil { st.Lock()
st.window = new(SlidingWindow) defer st.Unlock()
} return st.window.CheckAndAdd(packetID)
st.window.Add(packetID)
} }
type ClientUDPSession struct { type ClientUDPSession struct {
codec *UDPCodec mu sync.Mutex
codec *UDPClientCodec
clientSessionID uint64 clientSessionID uint64
nextPacketID atomic.Uint64 nextPacketID atomic.Uint64
clientBodyCipher cipher.AEAD clientBodyCipher cipher.AEAD
@@ -421,7 +549,7 @@ type ClientUDPSession struct {
old atomic.Pointer[serverSessionState] old atomic.Pointer[serverSessionState]
} }
func (c *UDPCodec) NewClientSession() (*ClientUDPSession, error) { func (c *UDPClientCodec) NewClientSession() (*ClientUDPSession, error) {
var sessID [8]byte var sessID [8]byte
if _, err := io.ReadFull(rand.Reader, sessID[:]); err != nil { if _, err := io.ReadFull(rand.Reader, sessID[:]); err != nil {
return nil, err return nil, err
@@ -446,7 +574,7 @@ func (c *UDPCodec) NewClientSession() (*ClientUDPSession, error) {
}, nil }, nil
} }
func (s *ClientUDPSession) getServerSession(sessionID uint64, now int64) (*serverSessionState, error) { func (s *ClientUDPSession) getOrTransitionServerSession(sessionID uint64, cipher cipher.AEAD, now int64) (*serverSessionState, error) {
cur := s.current.Load() cur := s.current.Load()
if cur != nil && cur.sessionID == sessionID { if cur != nil && cur.sessionID == sessionID {
return cur, nil return cur, nil
@@ -455,39 +583,42 @@ func (s *ClientUDPSession) getServerSession(sessionID uint64, now int64) (*serve
old := s.old.Load() old := s.old.Load()
if old != nil && old.sessionID == sessionID { if old != nil && old.sessionID == sessionID {
if now-old.lastSeen.Load() > 60 { if now-old.lastSeen.Load() > 60 {
s.old.CompareAndSwap(old, nil)
return nil, errors.New("old server session expired") return nil, errors.New("old server session expired")
} }
return old, nil return old, nil
} }
// New server session: s.mu.Lock()
defer s.mu.Unlock()
cur = s.current.Load()
if cur != nil && cur.sessionID == sessionID {
return cur, nil
}
old = s.old.Load()
if old != nil && old.sessionID == sessionID {
if now-old.lastSeen.Load() > 60 {
return nil, errors.New("old server session expired")
}
return old, nil
}
// Spec §3.2.4: reject newer server sessions when the last packet received from the old session is less than 1 minute old. // Spec §3.2.4: reject newer server sessions when the last packet received from the old session is less than 1 minute old.
if old != nil && now-old.lastSeen.Load() < 60 { if old != nil && now-old.lastSeen.Load() < 60 {
return nil, errors.New("newer server session rejected: old session is less than 1 minute old") return nil, errors.New("newer server session rejected: old session is less than 1 minute old")
} }
var bodyAead cipher.AEAD
if !s.codec.method.IsChaCha {
var sessBytes [8]byte
binary.BigEndian.PutUint64(sessBytes[:], sessionID)
bodyKey := DeriveSessionSubKey(s.codec.psk, sessBytes[:], s.codec.method.KeySaltLength)
var err error
bodyAead, err = s.codec.method.NewAEAD(bodyKey)
if err != nil {
return nil, err
}
}
newState := &serverSessionState{ newState := &serverSessionState{
sessionID: sessionID, sessionID: sessionID,
cipher: bodyAead, window: new(SlidingWindow),
cipher: cipher,
} }
newState.lastSeen.Store(now) newState.lastSeen.Store(now)
if cur == nil { if cur == nil {
s.current.CompareAndSwap(nil, newState) s.current.Store(newState)
return s.current.Load(), nil return newState, nil
} }
s.old.Store(cur) s.old.Store(cur)
@@ -637,15 +768,6 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
sessionID := binary.BigEndian.Uint64(plain[:8]) sessionID := binary.BigEndian.Uint64(plain[:8])
packetID := binary.BigEndian.Uint64(plain[8:16]) packetID := binary.BigEndian.Uint64(plain[8:16])
now := time.Now().Unix()
st, err := s.getServerSession(sessionID, now)
if err != nil {
return DecodedUDPPacket{}, err
}
if !st.check(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:]) decoded, err := parsePlainUDPPacket(sessionID, packetID, plain[16:])
if err != nil { if err != nil {
return DecodedUDPPacket{}, err return DecodedUDPPacket{}, err
@@ -658,7 +780,16 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
return DecodedUDPPacket{}, errors.New("client session ID mismatch") return DecodedUDPPacket{}, errors.New("client session ID mismatch")
} }
st.add(packetID) now := time.Now().Unix()
st, err := s.getOrTransitionServerSession(sessionID, nil, now)
if err != nil {
return DecodedUDPPacket{}, err
}
// Atomically commit after all validations succeed
if !st.commit(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
st.lastSeen.Store(now) st.lastSeen.Store(now)
return decoded, nil return decoded, nil
@@ -671,15 +802,30 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
packetID := binary.BigEndian.Uint64(rawHeader[8:16]) packetID := binary.BigEndian.Uint64(rawHeader[8:16])
now := time.Now().Unix() now := time.Now().Unix()
st, err := s.getServerSession(sessionID, now)
if err != nil { // 1. Fast path for known active session
return DecodedUDPPacket{}, err cur := s.current.Load()
var st *serverSessionState
var bodyAead cipher.AEAD
if cur != nil && cur.sessionID == sessionID {
st = cur
bodyAead = cur.cipher
} else {
old := s.old.Load()
if old != nil && old.sessionID == sessionID {
if now-old.lastSeen.Load() > 60 {
return DecodedUDPPacket{}, errors.New("old server session expired")
} }
st = old
bodyAead = old.cipher
}
}
if st != nil {
if !st.check(packetID) { if !st.check(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique return DecodedUDPPacket{}, ErrPacketIdNotUnique
} }
bodyAead := st.cipher
bodyNonce := rawHeader[4:16] bodyNonce := rawHeader[4:16]
bodyCipher := data[16:] bodyCipher := data[16:]
bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil) bodyPlain, err := bodyAead.Open(nil, bodyNonce, bodyCipher, nil)
@@ -699,7 +845,61 @@ func (s *ClientUDPSession) DecodePacket(data []byte) (DecodedUDPPacket, error) {
return DecodedUDPPacket{}, errors.New("client session ID mismatch") return DecodedUDPPacket{}, errors.New("client session ID mismatch")
} }
st.add(packetID) if !st.commit(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
st.lastSeen.Store(now)
return decoded, nil
}
// 2. Unknown session: Trial path without mutating s.current or s.old yet!
// Re-check cur in case another worker concurrently transitioned to this sessionID
cur = s.current.Load()
old := s.old.Load()
if (cur == nil || cur.sessionID != sessionID) && (old == nil || old.sessionID != sessionID) {
// Spec §3.2.4: reject newer server sessions when the last packet received from the old session is less than 1 minute old.
if old != nil && now-old.lastSeen.Load() < 60 {
return DecodedUDPPacket{}, errors.New("newer server session rejected: old session is less than 1 minute old")
}
}
var sessBytes [8]byte
binary.BigEndian.PutUint64(sessBytes[:], sessionID)
bodyKey := DeriveSessionSubKey(s.codec.psk, sessBytes[:], s.codec.method.KeySaltLength)
trialAead, err := s.codec.method.NewAEAD(bodyKey)
if err != nil {
return DecodedUDPPacket{}, err
}
bodyNonce := rawHeader[4:16]
bodyCipher := data[16:]
bodyPlain, err := trialAead.Open(nil, bodyNonce, bodyCipher, nil)
if err != nil {
return DecodedUDPPacket{}, errors.New("failed to decrypt aes udp body").Base(err)
}
decoded, err := parsePlainUDPPacket(sessionID, packetID, bodyPlain)
if err != nil {
return DecodedUDPPacket{}, err
}
if decoded.HeaderType != HeaderTypeServer {
return DecodedUDPPacket{}, ErrBadHeaderType
}
if decoded.ClientSessionID != s.clientSessionID {
return DecodedUDPPacket{}, errors.New("client session ID mismatch")
}
// Payload authenticated! Safely commit and transition session under lock
st, err = s.getOrTransitionServerSession(sessionID, trialAead, now)
if err != nil {
return DecodedUDPPacket{}, err
}
if !st.commit(packetID) {
return DecodedUDPPacket{}, ErrPacketIdNotUnique
}
st.lastSeen.Store(now) st.lastSeen.Store(now)
return decoded, nil return decoded, nil
+4 -3
View File
@@ -191,7 +191,7 @@ func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
// Verify downstream destination can decode both packets // Verify downstream destination can decode both packets
method, err := GetCipherMethod(MethodAES128GCM) method, err := GetCipherMethod(MethodAES128GCM)
common.Must(err) common.Must(err)
destCodec, err := NewUDPServerCodec(method, destKey, 300*time.Second) destCodec, err := NewUDPServerCodec(method, destKey, nil, 300*time.Second)
common.Must(err) common.Must(err)
mu.Lock() mu.Lock()
@@ -202,7 +202,8 @@ func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
t.Fatalf("expected 2 received packets at destination, got %d", len(pkts)) t.Fatalf("expected 2 received packets at destination, got %d", len(pkts))
} }
dec1, err := destCodec.DecodePacket(pkts[0]) dec1, dec2 := DecodedUDPPacket{}, DecodedUDPPacket{}
_, dec1, err = destCodec.DecodePacket(pkts[0])
if err != nil { if err != nil {
t.Fatalf("dest failed to decode packet 1: %v", err) t.Fatalf("dest failed to decode packet 1: %v", err)
} }
@@ -210,7 +211,7 @@ func TestRelayUDPSessionStabilityAndDispatch(t *testing.T) {
t.Fatalf("dec1 mismatch: sess=%x, pktID=%d, payload=%s", dec1.SessionID, dec1.PacketID, string(dec1.Payload)) t.Fatalf("dec1 mismatch: sess=%x, pktID=%d, payload=%s", dec1.SessionID, dec1.PacketID, string(dec1.Payload))
} }
dec2, err := destCodec.DecodePacket(pkts[1]) _, dec2, err = destCodec.DecodePacket(pkts[1])
if err != nil { if err != nil {
t.Fatalf("dest failed to decode packet 2: %v", err) t.Fatalf("dest failed to decode packet 2: %v", err)
} }
+23 -7
View File
@@ -100,19 +100,27 @@ type ServerUDPSession struct {
func (s *ServerUDPSession) CheckPacketID(packetID uint64) bool { func (s *ServerUDPSession) CheckPacketID(packetID uint64) bool {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if s.Window == nil {
s.Window = new(SlidingWindow)
}
return s.Window.Check(packetID) return s.Window.Check(packetID)
} }
func (s *ServerUDPSession) AddPacketID(packetID uint64) { func (s *ServerUDPSession) Commit(packetID uint64, bodyAead cipher.AEAD, user *protocol.MemoryUser, userPSK []byte) bool {
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
if s.Window == nil { if s.User != nil && user != nil && s.User != user {
s.Window = new(SlidingWindow) return false
} }
s.Window.Add(packetID) if !s.Window.CheckAndAdd(packetID) {
return false
}
if s.clientBodyCipher == nil && bodyAead != nil {
s.clientBodyCipher = bodyAead
}
if s.User == nil && user != nil {
s.User = user
s.UserPSK = userPSK
}
s.LastActive.Store(time.Now().Unix())
return true
} }
type UDPSessionManager struct { type UDPSessionManager struct {
@@ -128,6 +136,13 @@ func NewUDPSessionManager(timeout time.Duration) *UDPSessionManager {
} }
} }
func (m *UDPSessionManager) Get(sessionID uint64) *ServerUDPSession {
if s, ok := m.sessions.Load(sessionID); ok {
return s
}
return nil
}
func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession { func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession {
now := time.Now().Unix() now := time.Now().Unix()
if s, ok := m.sessions.Load(sessionID); ok { if s, ok := m.sessions.Load(sessionID); ok {
@@ -137,6 +152,7 @@ func (m *UDPSessionManager) GetOrCreate(sessionID uint64) *ServerUDPSession {
s := &ServerUDPSession{ s := &ServerUDPSession{
SessionID: sessionID, SessionID: sessionID,
Window: new(SlidingWindow),
manager: m, manager: m,
} }
s.LastActive.Store(now) s.LastActive.Store(now)
+9 -3
View File
@@ -63,10 +63,16 @@ func (s *ServerUDPSession) EnsureLink(
s.currentConn.Store(conn) s.currentConn.Store(conn)
sessCtx, cancel := context.WithCancel(ctx) // UDP Session might travel across multiple connections
// don't bind its lifetime to a parent context
sessCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
inbound := session.InboundFromContext(sessCtx) inbound := session.InboundFromContext(sessCtx)
if inbound != nil && s.User != nil { if inbound != nil {
inbound.User = s.User newInbound := *inbound
if s.User != nil {
newInbound.User = s.User
}
sessCtx = session.ContextWithInbound(sessCtx, &newInbound)
} }
var email string var email string
var level uint32 var level uint32
@@ -265,7 +265,7 @@ func TestUDPCodec(t *testing.T) {
clientCodec, err := NewUDPPacketCodec(method, [][]byte{psk}) clientCodec, err := NewUDPPacketCodec(method, [][]byte{psk})
common.Must(err) common.Must(err)
serverCodec, err := NewUDPServerCodec(method, psk, time.Minute) serverCodec, err := NewUDPServerCodec(method, psk, nil, time.Minute)
common.Must(err) common.Must(err)
session, err := clientCodec.NewClientSession() session, err := clientCodec.NewClientSession()
@@ -277,7 +277,7 @@ func TestUDPCodec(t *testing.T) {
rawCopy := make([]byte, pktBuf.Len()) rawCopy := make([]byte, pktBuf.Len())
copy(rawCopy, pktBuf.Bytes()) copy(rawCopy, pktBuf.Bytes())
decoded, err := serverCodec.DecodePacket(pktBuf.Bytes()) _, decoded, err := serverCodec.DecodePacket(pktBuf.Bytes())
common.Must(err) common.Must(err)
if decoded.HeaderType != HeaderTypeClient { if decoded.HeaderType != HeaderTypeClient {
@@ -291,7 +291,7 @@ func TestUDPCodec(t *testing.T) {
} }
// Replay same packet wire bytes should fail with ErrPacketIdNotUnique // Replay same packet wire bytes should fail with ErrPacketIdNotUnique
_, err = serverCodec.DecodePacket(rawCopy) _, _, err = serverCodec.DecodePacket(rawCopy)
if err != ErrPacketIdNotUnique { if err != ErrPacketIdNotUnique {
t.Fatalf("expected ErrPacketIdNotUnique on replay, got: %v", err) t.Fatalf("expected ErrPacketIdNotUnique on replay, got: %v", err)
} }
@@ -427,7 +427,7 @@ func TestClientUDPSessionMultiDestination(t *testing.T) {
clientCodec, err := NewUDPPacketCodec(method, [][]byte{rawKey}) clientCodec, err := NewUDPPacketCodec(method, [][]byte{rawKey})
common.Must(err) common.Must(err)
serverCodec, err := NewUDPServerCodec(method, rawKey, time.Minute) serverCodec, err := NewUDPServerCodec(method, rawKey, nil, time.Minute)
common.Must(err) common.Must(err)
session, err := clientCodec.NewClientSession() session, err := clientCodec.NewClientSession()
@@ -448,9 +448,9 @@ func TestClientUDPSessionMultiDestination(t *testing.T) {
defer pkt2.Release() defer pkt2.Release()
// Server decodes both // Server decodes both
dec1, err := serverCodec.DecodePacket(pkt1.Bytes()) _, dec1, err := serverCodec.DecodePacket(pkt1.Bytes())
common.Must(err) common.Must(err)
dec2, err := serverCodec.DecodePacket(pkt2.Bytes()) _, dec2, err := serverCodec.DecodePacket(pkt2.Bytes())
common.Must(err) common.Must(err)
if dec1.SessionID != session.ClientSessionID() || dec2.SessionID != session.ClientSessionID() { if dec1.SessionID != session.ClientSessionID() || dec2.SessionID != session.ClientSessionID() {
+1 -1
View File
@@ -68,7 +68,7 @@ func KeyLogWriterFromConfig(c *Config) io.Writer {
return nil return nil
} }
writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600) writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o644)
if err != nil { if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log") errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log")
} }
+1 -1
View File
@@ -466,7 +466,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
} }
if len(c.MasterKeyLog) > 0 && c.MasterKeyLog != "none" { if len(c.MasterKeyLog) > 0 && c.MasterKeyLog != "none" {
writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o600) writer, err := os.OpenFile(c.MasterKeyLog, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0o644)
if err != nil { if err != nil {
errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log") errors.LogErrorInner(context.Background(), err, "failed to open ", c.MasterKeyLog, " as master key log")
} else { } else {