mirror of
https://github.com/netbirdio/netbird.git
synced 2026-06-02 14:09:56 +00:00
Compare commits
1 Commits
main
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3d3523317 |
39
.github/workflows/proto-version-check.yml
vendored
39
.github/workflows/proto-version-check.yml
vendored
@@ -20,30 +20,15 @@ jobs:
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
// Cover renamed .pb.go files in addition to plain edits.
|
||||
// Renamed entries land under the new path with previous_filename
|
||||
// pointing at the base-side name, so we read the base content
|
||||
// from the old path when present.
|
||||
const changedPbFiles = files
|
||||
.filter(f => (f.status === 'modified' || f.status === 'renamed')
|
||||
&& f.filename.endsWith('.pb.go'))
|
||||
.map(f => ({
|
||||
headPath: f.filename,
|
||||
basePath: f.previous_filename || f.filename,
|
||||
}));
|
||||
if (changedPbFiles.length === 0) {
|
||||
console.log('No modified or renamed .pb.go files to check');
|
||||
const modifiedPbFiles = files.filter(
|
||||
f => f.filename.endsWith('.pb.go') && f.status === 'modified'
|
||||
);
|
||||
if (modifiedPbFiles.length === 0) {
|
||||
console.log('No modified .pb.go files to check');
|
||||
return;
|
||||
}
|
||||
|
||||
// Matches the generator version headers protoc writes at the top
|
||||
// of generated files:
|
||||
// // protoc v3.21.12
|
||||
// // protoc-gen-go v1.26.0
|
||||
// // - protoc-gen-go-grpc v1.6.1 (grpc files prefix with "- ")
|
||||
// The optional "- " prefix and the optional -gen-go / -gen-go-grpc
|
||||
// suffixes keep the *_grpc.pb.go headers in scope.
|
||||
const versionPattern = /^\s*\/\/\s+(?:-\s+)?protoc(?:-gen-go(?:-grpc)?)?\s+v[\d.]+/;
|
||||
const versionPattern = /^\s*\/\/\s+protoc(?:-gen-go)?\s+v[\d.]+/;
|
||||
const baseSha = context.payload.pull_request.base.sha;
|
||||
const headSha = context.payload.pull_request.head.sha;
|
||||
|
||||
@@ -70,22 +55,20 @@ jobs:
|
||||
}
|
||||
|
||||
const violations = [];
|
||||
for (const file of changedPbFiles) {
|
||||
for (const file of modifiedPbFiles) {
|
||||
const [base, head] = await Promise.all([
|
||||
getVersionHeader(file.basePath, baseSha),
|
||||
getVersionHeader(file.headPath, headSha),
|
||||
getVersionHeader(file.filename, baseSha),
|
||||
getVersionHeader(file.filename, headSha),
|
||||
]);
|
||||
if (!base.ok || !head.ok) {
|
||||
core.warning(
|
||||
`Skipping ${file.headPath}: base=${base.ok ? 'ok' : base.reason}, head=${head.ok ? 'ok' : head.reason}`
|
||||
`Skipping ${file.filename}: base=${base.ok ? 'ok' : base.reason}, head=${head.ok ? 'ok' : head.reason}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (base.lines.join('\n') !== head.lines.join('\n')) {
|
||||
violations.push({
|
||||
file: file.basePath === file.headPath
|
||||
? file.headPath
|
||||
: `${file.basePath} → ${file.headPath}`,
|
||||
file: file.filename,
|
||||
base: base.lines,
|
||||
head: head.lines,
|
||||
});
|
||||
|
||||
@@ -362,10 +362,6 @@ func (f *Forwarder) injectICMPv6Reply(id stack.TransportEndpointID, icmpPayload
|
||||
return 0
|
||||
}
|
||||
|
||||
if pc := f.endpoint.capture.Load(); pc != nil {
|
||||
(*pc).Offer(fullPacket, true)
|
||||
}
|
||||
|
||||
return len(fullPacket)
|
||||
}
|
||||
|
||||
|
||||
@@ -310,12 +310,8 @@ func (d *Status) PeerByIP(ip string) (string, bool) {
|
||||
|
||||
// PeerStateByIP returns the full peer State for the given tunnel IP.
|
||||
// Matches against either the IPv4 (State.IP) or IPv6 (State.IPv6) tunnel
|
||||
// address so dual-stack peers are reachable on either family. Searches
|
||||
// both d.peers and d.offlinePeers — peers that have been moved into
|
||||
// the offline slice by ReplaceOfflinePeers are still part of the
|
||||
// account's roster and callers (DNS filter, embed.Client.IdentityForIP)
|
||||
// need to recognise them rather than treating them as unknown. Returns
|
||||
// the zero State and false when no peer matches or the input is empty.
|
||||
// address so dual-stack peers are reachable on either family. Returns the
|
||||
// zero State and false when no peer matches or the input is empty.
|
||||
func (d *Status) PeerStateByIP(ip string) (State, bool) {
|
||||
if ip == "" {
|
||||
return State{}, false
|
||||
@@ -328,11 +324,6 @@ func (d *Status) PeerStateByIP(ip string) (State, bool) {
|
||||
return state, true
|
||||
}
|
||||
}
|
||||
for _, state := range d.offlinePeers {
|
||||
if (state.IP != "" && state.IP == ip) || (state.IPv6 != "" && state.IPv6 == ip) {
|
||||
return state, true
|
||||
}
|
||||
}
|
||||
return State{}, false
|
||||
}
|
||||
|
||||
|
||||
@@ -90,28 +90,6 @@ func TestStatus_PeerStateByIP_MatchesIPv6(t *testing.T) {
|
||||
req.Equal("pk-1", state.PubKey, "matching state must carry the right pub key")
|
||||
}
|
||||
|
||||
// TestStatus_PeerStateByIP_MatchesOfflinePeers covers peers that have
|
||||
// been moved into the offline slice via ReplaceOfflinePeers. Callers
|
||||
// (DNS filter, embed.Client.IdentityForIP) need to treat them as known
|
||||
// rather than unknown — otherwise authentication / DNS filtering treats
|
||||
// known-but-offline peers as foreign IPs.
|
||||
func TestStatus_PeerStateByIP_MatchesOfflinePeers(t *testing.T) {
|
||||
status := NewRecorder("https://mgm")
|
||||
req := require.New(t)
|
||||
|
||||
status.ReplaceOfflinePeers([]State{
|
||||
{PubKey: "pk-offline", FQDN: "offline.netbird", IP: "100.64.0.20", IPv6: "fd00::20"},
|
||||
})
|
||||
|
||||
state, ok := status.PeerStateByIP("100.64.0.20")
|
||||
req.True(ok, "offline peer must resolve by IPv4 tunnel address")
|
||||
req.Equal("pk-offline", state.PubKey, "matching state must carry the offline peer's pub key")
|
||||
|
||||
state, ok = status.PeerStateByIP("fd00::20")
|
||||
req.True(ok, "offline peer must resolve by IPv6 tunnel address")
|
||||
req.Equal("pk-offline", state.PubKey, "IPv6 match must carry the offline peer's pub key")
|
||||
}
|
||||
|
||||
func TestStatus_UpdatePeerFQDN(t *testing.T) {
|
||||
key := "abc"
|
||||
fqdn := "peer-a.netbird.local"
|
||||
|
||||
36
go.mod
36
go.mod
@@ -32,10 +32,10 @@ require (
|
||||
fyne.io/systray v1.12.1-0.20260116214250-81f8e1a496f9
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
|
||||
github.com/awnumar/memguard v0.23.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.9
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.20
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2
|
||||
github.com/c-robinson/iplib v1.0.3
|
||||
github.com/caddyserver/certmagic v0.21.3
|
||||
github.com/cilium/ebpf v0.19.0
|
||||
@@ -151,21 +151,21 @@ require (
|
||||
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/awnumar/memcall v0.4.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect
|
||||
github.com/aws/smithy-go v1.26.0 // indirect
|
||||
github.com/beevik/etree v1.6.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
|
||||
|
||||
72
go.sum
72
go.sum
@@ -50,44 +50,44 @@ github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g
|
||||
github.com/awnumar/memcall v0.4.0/go.mod h1:8xOx1YbfyuCg3Fy6TO8DK0kZUua3V42/goA5Ru47E8w=
|
||||
github.com/awnumar/memguard v0.23.0 h1:sJ3a1/SWlcuKIQ7MV+R9p0Pvo9CWsMbGZvcZQtmc68A=
|
||||
github.com/awnumar/memguard v0.23.0/go.mod h1:olVofBrsPdITtJ2HgxQKrEYEMyIBAIciVG4wNnZhW9M=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3 h1:B6cV4oxnMs45fql4yRH+/Po/YU+597zgWqvDpYMturk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.38.3/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1 h1:i8p8P4diljCr60PpJp6qZXNlgX4m2yQFpYk+9ZT+J4E=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.1/go.mod h1:ddqbooRZYNoJ2dsTwOty16rM+/Aqmk/GOXrK8cg7V00=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6 h1:a1t8fXY4GT4xjyJExz4knbuoxSCacB5hT/WgtfPyLjo=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.31.6/go.mod h1:5ByscNi7R+ztvOGzeUaIu49vkMk2soq5NaH5PYe33MQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10 h1:xdJnXCouCx8Y0NncgoptztUocIYLKeQxrCgN6x9sdhg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.18.10/go.mod h1:7tQk08ntj914F/5i9jC4+2HQTAuJirq7m1vZVIhEkWs=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6 h1:wbjnrrMnKew78/juW7I2BtKQwa1qlf6EjQgS69uYY14=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.6/go.mod h1:AtiqqNrDioJXuUgz3+3T0mBWN7Hro2n9wll2zRUc0ww=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6 h1:uF68eJA6+S9iVr9WgX1NaRGyQ/6MdIyc4JNUo6TN1FA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.6/go.mod h1:qlPeVZCGPiobx8wb1ft0GHT5l+dc6ldnwInDFaMvC7Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6 h1:pa1DEC6JoI0zduhZePp3zmhWvk/xxm4NB8Hy/Tlsgos=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.6/go.mod h1:gxEjPebnhWGJoaDdtDkA0JX46VRg1wcTHYe63OfX5pE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6 h1:R0tNFJqfjHL3900cqhXuwQ+1K4G0xc9Yf8EDbFXCKEw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.6/go.mod h1:y/7sDdu+aJvPtGXr4xYosdpq9a6T9Z0jkXfugmti0rI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 h1:oegbebPEMA/1Jny7kvwejowCaHz1FWZAQ94WXFNCyTM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1/go.mod h1:kemo5Myr9ac0U9JfSjMo9yHLtw+pECEHsFtJ9tqCEI8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6 h1:hncKj/4gR+TPauZgTAsxOxNcvBayhUlYZ6LO/BYiQ30=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.8.6/go.mod h1:OiIh45tp6HdJDDJGnja0mw8ihQGz3VGrUflLqSL0SmM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6 h1:LHS1YAIJXJ4K9zS+1d/xa9JAA9sL2QyXIQCQFQW/X08=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.6/go.mod h1:c9PCiTEuh0wQID5/KqA32J+HAgZxN9tOGXKCiYJjTZI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6 h1:nEXUSAwyUfLTgnc9cxlDWy637qsq4UWwp3sNAfl0Z3Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.6/go.mod h1:HGzIULx4Ge3Do2V0FaiYKcyKzOqwrhUZgCI77NisswQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11/go.mod h1:dnakxebH6UwFvcvujL0LVggYQ8nEvBGjU4G/V79Nv94=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.20 h1:8VMDnWc/kEzxsI/1ngGM9mG81a8IGmIHD8KLcYGwagc=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.20/go.mod h1:PuwEpciweIXGULWeOeSTXtSbH4CW9mWdWrhdCKQI1sM=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19 h1:yuFzSV1U0aRNYCQGVaTY2zW2M/L93pYHnXnrJUphYhU=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.19/go.mod h1:7y63L1kGzeoDlJaQ3Z578KrnmfBut96JjvJUzGwR+YE=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25 h1:0w6dCiO8iez+YKwRhRBlL1CH/E3GTfdkuzrwj1by8vo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.25/go.mod h1:9FDWUothyr5RCRAHc45XOiVCzUR8n/IhCYX+uVqw6vk=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25 h1:Uii3frf9ztec/ABM2/FSH9/z7PLzxfpG8h4RpkUFflQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.25/go.mod h1:G6kntsA2GorAxDPbap6xgB2F+amSLUF8GJTi7PUoX44=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25 h1:r1+/l6m+WaUJF9HISEsNOLHSNj5EXYQxK8VX6Cz9NlA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.25/go.mod h1:cKf+D+NMDK1LndD7BowHbBZPgR9V0/5HubH0PFWvA+c=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26 h1:A1PmWU2zfkIm9EyFlJncFXL4W4phML+h8KjltUsCvNQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.26/go.mod h1:dY4MRzXEizrD4hqtpKvWVGPX7QleSGGVY+EBolo1RmM=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10 h1:d5/908OJ4bXg8lyjeMPvXetEKqoDoLi5Owy1zNue3yg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.10/go.mod h1:a57l7Hwh+FWI+we50g5NPJHYUKeJKfXbc4w8SyXu8Ig=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18 h1:W/EyPFl9A5rXrtoilfwHYEvzHER+K4SpBPtMXi24Mos=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.18/go.mod h1:UG50K+pvd/uy6xExbobg0rjqFBFZe6I3l75EPDZw4tg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25 h1:dD3dhHNglpd98gs72my22Ndqi1hqQGllFFg1F+twfxg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.25/go.mod h1:0yAbjPfd64gG7mj85RW+fMEYdfBgCRZw8g/oWcL1pjc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25 h1:2pQEbwf+/6EDbiit/GcBE2K4IUpMZymaA0kOz3xK978=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.25/go.mod h1:KvT6NCcQ0EZ+ZkVRrlBMt04Po3ok23YELEp7WimhLhM=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3 h1:MmLCRqP4U4Cw9gJ4bNrCG0mWqEtBlmAVleyelcHARMU=
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.42.3/go.mod h1:AMPjK2YnRh0YgOID3PqhJA1BRNfXDfGOnSsKHtAe8yA=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3 h1:ETkfWcXP2KNPLecaDa++5bsQhCRa5M5sLUJa5DWYIIg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.87.3/go.mod h1:+/3ZTqoYb3Ur7DObD00tarKMLMuKg8iqz5CHEanqTnw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1 h1:8OLZnVJPvjnrxEwHFg9hVUof/P4sibH+Ea4KKuqAGSg=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.29.1/go.mod h1:27M3BpVi0C02UiQh1w9nsBEit6pLhlaH3NHna6WUbDE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2 h1:gKWSTnqudpo8dAxqBqZnDoDWCiEh/40FziUjr/mo6uA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.2/go.mod h1:x7+rkNmRoEN1U13A6JE2fXne9EWyJy54o3n6d4mGaXQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2 h1:YZPjhyaGzhDQEvsffDEcpycq49nl7fiGcfJTIo8BszI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.38.2/go.mod h1:2dIN8qhQfv37BdUYGgEC8Q3tteM3zFxTI1MLO2O3J3c=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2 h1:ie4ElCmUKS26pzrZcIk/lmt4yWjAqLLcawstyQCh298=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.102.2/go.mod h1:zjsomFeX5duj+4PlMB+o4JoWTIx+G0XMyzjYrUbQkN0=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1 h1:1VwbP3qMNfxUDEXWki4rCE5iA+44VA1lokTz9HasGzw=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.1.1/go.mod h1:vUtyoSj0OPji3kjIVSc/GlKuWEiL33f/WFxl6dmpy/A=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19 h1:N6pIsdFOW1Kd9S4KyFKXdGRBojPPxkP32+uHFWLv4Hc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.19/go.mod h1:3gt5WJArFooNmyLONS+h/R4J+o86II8du38IgCwj9dE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 h1:hc+lBYiiTr8Zk4MTzIsQ92MeDWCIDvWGmzKUWOaBcOg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2/go.mod h1:hU6fqB3OJA6/ePheD47LQnxvjYk6br6PtQxs+Q9ojvk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3SJXQNEgksORW3Js=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8=
|
||||
github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s=
|
||||
github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
|
||||
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
|
||||
@@ -932,11 +932,7 @@ func (s *Service) validateL4Target(target *Target) error {
|
||||
if target.TargetId == "" {
|
||||
return errors.New("target_id is required for L4 services")
|
||||
}
|
||||
// Cluster targets resolve their upstream host:port from the target's
|
||||
// own Host/Port fields just like the other L4 types — buildPathMappings
|
||||
// emits net.JoinHostPort(target.Host, target.Port) for every L4
|
||||
// target, so allowing port=0 here would let ":0" reach the proxy.
|
||||
if target.Port == 0 {
|
||||
if target.TargetType != TargetTypeCluster && target.Port == 0 {
|
||||
return errors.New("target port is required for L4 services")
|
||||
}
|
||||
switch target.TargetType {
|
||||
|
||||
@@ -1176,12 +1176,7 @@ func TestValidate_HTTPClusterTarget_RequiresDirectUpstream(t *testing.T) {
|
||||
assert.ErrorContains(t, rp.Validate(), "direct upstream disabled", "cluster target must reject direct_upstream=false")
|
||||
}
|
||||
|
||||
// TestValidate_L4ClusterTarget_RequiresPort confirms that an L4 cluster
|
||||
// target without an explicit port is rejected. buildPathMappings emits
|
||||
// net.JoinHostPort(target.Host, target.Port) for every L4 target — so
|
||||
// allowing port=0 would let the proxy ship ":0" upstreams. The port
|
||||
// requirement is the same as every other L4 target type.
|
||||
func TestValidate_L4ClusterTarget_RequiresPort(t *testing.T) {
|
||||
func TestValidate_L4ClusterTarget(t *testing.T) {
|
||||
rp := validProxy()
|
||||
rp.Mode = ModeTCP
|
||||
rp.ListenPort = 9000
|
||||
@@ -1191,12 +1186,7 @@ func TestValidate_L4ClusterTarget_RequiresPort(t *testing.T) {
|
||||
Protocol: "tcp",
|
||||
Enabled: true,
|
||||
}}
|
||||
assert.ErrorContains(t, rp.Validate(), "port is required",
|
||||
"L4 cluster target must require an explicit port like other L4 target types")
|
||||
|
||||
rp.Targets[0].Port = 5432
|
||||
rp.Targets[0].Host = "db.lan"
|
||||
require.NoError(t, rp.Validate(), "L4 cluster target with host:port must validate")
|
||||
require.NoError(t, rp.Validate(), "L4 cluster target must validate without an explicit port")
|
||||
}
|
||||
|
||||
func TestService_Copy_RoundtripsPrivate(t *testing.T) {
|
||||
|
||||
@@ -102,7 +102,7 @@ func generateSessionKeyPair(t *testing.T) (string, string) {
|
||||
|
||||
func createSessionToken(t *testing.T, privKeyB64, userID, domain string) string {
|
||||
t.Helper()
|
||||
token, err := sessionkey.SignToken(privKeyB64, userID, "", domain, auth.MethodOIDC, nil, nil, time.Hour)
|
||||
token, err := sessionkey.SignToken(privKeyB64, userID, domain, auth.MethodOIDC, nil, time.Hour)
|
||||
require.NoError(t, err)
|
||||
return token
|
||||
}
|
||||
@@ -394,10 +394,6 @@ func (m *testValidateSessionProxyManager) ClusterSupportsCrowdSec(_ context.Cont
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *testValidateSessionProxyManager) ClusterSupportsPrivate(_ context.Context, _ string) *bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
type testValidateSessionUsersManager struct {
|
||||
store store.Store
|
||||
}
|
||||
@@ -405,24 +401,3 @@ type testValidateSessionUsersManager struct {
|
||||
func (m *testValidateSessionUsersManager) GetUser(ctx context.Context, userID string) (*types.User, error) {
|
||||
return m.store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
|
||||
}
|
||||
|
||||
func (m *testValidateSessionUsersManager) GetUserWithGroups(ctx context.Context, userID string) (*types.User, []*types.Group, error) {
|
||||
user, err := m.store.GetUserByUserID(ctx, store.LockingStrengthNone, userID)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(user.AutoGroups) == 0 {
|
||||
return user, nil, nil
|
||||
}
|
||||
groupsMap, err := m.store.GetGroupsByIDs(ctx, store.LockingStrengthNone, user.AccountID, user.AutoGroups)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
groups := make([]*types.Group, 0, len(user.AutoGroups))
|
||||
for _, id := range user.AutoGroups {
|
||||
if g, ok := groupsMap[id]; ok && g != nil {
|
||||
groups = append(groups, g)
|
||||
}
|
||||
}
|
||||
return user, groups, nil
|
||||
}
|
||||
|
||||
@@ -4734,13 +4734,7 @@ func (s *SqlStore) GetPeerByIP(ctx context.Context, lockStrength LockingStrength
|
||||
result := tx.
|
||||
Take(&peer, fmt.Sprintf("account_id = ? AND %s = ?", column), accountID, jsonValue)
|
||||
if result.Error != nil {
|
||||
// A tunnel-IP miss is an expected outcome (e.g. the proxy's
|
||||
// ValidateTunnelPeer probing an address that isn't in the
|
||||
// account roster); surface it as NotFound so callers can tell
|
||||
// it apart from a real store failure.
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
return nil, status.Errorf(status.NotFound, "peer with ip %s not found", ip.String())
|
||||
}
|
||||
// no logging here
|
||||
return nil, status.Errorf(status.Internal, "failed to get peer from store")
|
||||
}
|
||||
|
||||
@@ -5968,7 +5962,6 @@ func (s *SqlStore) getClusterCapability(ctx context.Context, clusterAddr, column
|
||||
}
|
||||
|
||||
err := s.db.
|
||||
WithContext(ctx).
|
||||
Model(&proxy.Proxy{}).
|
||||
Select("COUNT(CASE WHEN "+column+" IS NOT NULL THEN 1 END) > 0 AS has_capability, "+
|
||||
"COALESCE(MAX(CASE WHEN "+column+" = true THEN 1 ELSE 0 END), 0) = 1 AS any_true").
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestSqlStore_GetAccount_PrivateServiceRoundtrip(t *testing.T) {
|
||||
if os.Getenv("CI") == "true" && (runtime.GOOS == "darwin" || runtime.GOOS == "windows") {
|
||||
if (os.Getenv("CI") == "true" && runtime.GOOS == "darwin") || runtime.GOOS == "windows" {
|
||||
t.Skip("skip CI tests on darwin and windows")
|
||||
}
|
||||
|
||||
|
||||
@@ -491,27 +491,6 @@ func Test_GetAccount(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestSqlStore_GetPeerByIP_NotFound pins the not-found semantics the
|
||||
// proxy's ValidateTunnelPeer relies on: a tunnel-IP that isn't in the
|
||||
// account roster must surface as a NotFound error (not a generic
|
||||
// Internal) so callers can distinguish an expected miss from a real
|
||||
// store failure. A known IP still resolves.
|
||||
func TestSqlStore_GetPeerByIP_NotFound(t *testing.T) {
|
||||
runTestForAllEngines(t, "../testdata/store.sql", func(t *testing.T, store Store) {
|
||||
const accountID = "bf1c8084-ba50-4ce7-9439-34653001fc3b"
|
||||
|
||||
peer, err := store.GetPeerByIP(context.Background(), LockingStrengthNone, accountID, net.ParseIP("192.168.0.0"))
|
||||
require.NoError(t, err, "known tunnel IP must resolve")
|
||||
require.NotNil(t, peer)
|
||||
|
||||
_, err = store.GetPeerByIP(context.Background(), LockingStrengthNone, accountID, net.ParseIP("100.65.0.99"))
|
||||
require.Error(t, err, "unknown tunnel IP must error")
|
||||
parsedErr, ok := status.FromError(err)
|
||||
require.True(t, ok, "error must be a status error")
|
||||
require.Equal(t, status.NotFound, parsedErr.Type(), "tunnel-IP miss must be NotFound, not Internal")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSqlStore_SavePeer(t *testing.T) {
|
||||
store, cleanUp, err := NewTestStoreFromSQL(context.Background(), "../testdata/store.sql", t.TempDir())
|
||||
t.Cleanup(cleanUp)
|
||||
|
||||
@@ -214,10 +214,7 @@ func runServer(cmd *cobra.Command, args []string) error {
|
||||
return fmt.Errorf("invalid --trusted-proxies: %w", err)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||
defer stop()
|
||||
|
||||
srv := proxy.New(ctx, proxy.Config{
|
||||
srv := proxy.New(proxy.Config{
|
||||
ListenAddr: addr,
|
||||
Logger: logger,
|
||||
Version: Version,
|
||||
@@ -254,6 +251,9 @@ func runServer(cmd *cobra.Command, args []string) error {
|
||||
CrowdSecAPIKey: crowdsecAPIKey,
|
||||
})
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
||||
defer stop()
|
||||
|
||||
return srv.ListenAndServe(ctx, addr)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -43,7 +42,7 @@ const privateInboundPortHTTPS = 443
|
||||
const privateInboundPortHTTP = 80
|
||||
|
||||
// inboundManager wires per-account inbound listeners into the proxy
|
||||
// pipeline when --private is enabled. When disabled the manager
|
||||
// pipeline when --private-inbound is enabled. When disabled the manager
|
||||
// is nil and every method on *Server that touches it short-circuits.
|
||||
type inboundManager struct {
|
||||
logger *log.Logger
|
||||
@@ -56,18 +55,15 @@ type inboundManager struct {
|
||||
}
|
||||
|
||||
// inboundEntry owns the listeners, router and HTTP servers for a single
|
||||
// account's embedded netstack. errorLogWriters retain the logrus pipe
|
||||
// writers backing each http.Server's ErrorLog so tearDown can close
|
||||
// them — otherwise the pipe + its scanner goroutine leak per account.
|
||||
// account's embedded netstack.
|
||||
type inboundEntry struct {
|
||||
router *nbtcp.Router
|
||||
tlsListener net.Listener
|
||||
plainListener net.Listener
|
||||
httpsServer *http.Server
|
||||
httpServer *http.Server
|
||||
errorLogWriters []*io.PipeWriter
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
router *nbtcp.Router
|
||||
tlsListener net.Listener
|
||||
plainListener net.Listener
|
||||
httpsServer *http.Server
|
||||
httpServer *http.Server
|
||||
cancel context.CancelFunc
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
// pendingInboundRoute holds a route that arrived before the account's
|
||||
@@ -151,34 +147,30 @@ func (m *inboundManager) bringUp(ctx context.Context, accountID types.AccountID,
|
||||
return types.WithOverlayOrigin(ctx)
|
||||
}
|
||||
|
||||
httpsErrLog, httpsErrW := newInboundErrorLog(m.logger, "https", accountID)
|
||||
httpErrLog, httpErrW := newInboundErrorLog(m.logger, "http", accountID)
|
||||
|
||||
httpsServer := &http.Server{
|
||||
Handler: scopedHandler,
|
||||
TLSConfig: m.tlsConfig,
|
||||
ReadHeaderTimeout: httpInboundReadHeaderTimeout,
|
||||
IdleTimeout: httpInboundIdleTimeout,
|
||||
ErrorLog: httpsErrLog,
|
||||
ErrorLog: newInboundErrorLog(m.logger, "https", accountID),
|
||||
ConnContext: markOverlayOrigin,
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: scopedHandler,
|
||||
ReadHeaderTimeout: httpInboundReadHeaderTimeout,
|
||||
IdleTimeout: httpInboundIdleTimeout,
|
||||
ErrorLog: httpErrLog,
|
||||
ErrorLog: newInboundErrorLog(m.logger, "http", accountID),
|
||||
ConnContext: markOverlayOrigin,
|
||||
}
|
||||
|
||||
runCtx, cancel := context.WithCancel(ctx)
|
||||
entry := &inboundEntry{
|
||||
router: router,
|
||||
tlsListener: tlsListener,
|
||||
plainListener: plainListener,
|
||||
httpsServer: httpsServer,
|
||||
httpServer: httpServer,
|
||||
errorLogWriters: []*io.PipeWriter{httpsErrW, httpErrW},
|
||||
cancel: cancel,
|
||||
router: router,
|
||||
tlsListener: tlsListener,
|
||||
plainListener: plainListener,
|
||||
httpsServer: httpsServer,
|
||||
httpServer: httpServer,
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
entry.wg.Add(1)
|
||||
@@ -245,14 +237,6 @@ func (m *inboundManager) tearDown(accountID types.AccountID, entry *inboundEntry
|
||||
m.logger.Debugf("close per-account plain listener: %v", err)
|
||||
}
|
||||
entry.wg.Wait()
|
||||
// Close the ErrorLog pipes only after the http.Servers have fully
|
||||
// stopped so any straggling stdlib write doesn't race with the
|
||||
// close. Each writer also tears down the logrus scanner goroutine.
|
||||
for _, w := range entry.errorLogWriters {
|
||||
if err := w.Close(); err != nil {
|
||||
m.logger.Debugf("close per-account inbound error log writer: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AddRoute records an SNI/host route on the account's per-account router.
|
||||
@@ -390,7 +374,7 @@ func (m *inboundManager) ListenerInfo(accountID types.AccountID) (InboundListene
|
||||
}
|
||||
|
||||
// Snapshot returns the inbound listener state for every account that has
|
||||
// a live listener at call time. Empty when --private is off or
|
||||
// a live listener at call time. Empty when --private-inbound is off or
|
||||
// no accounts have come up yet.
|
||||
func (m *inboundManager) Snapshot() map[types.AccountID]InboundListenerInfo {
|
||||
if m == nil {
|
||||
@@ -513,7 +497,7 @@ func accountTunnelLookup(client *embed.Client) auth.TunnelLookupFunc {
|
||||
// peerstore lookup to every request's context before delegating to next.
|
||||
// Calling on the host-level listener is a no-op because that path never
|
||||
// installs this wrapper, so the existing behaviour stays byte-for-byte
|
||||
// identical when --private is off or the request didn't arrive
|
||||
// identical when --private-inbound is off or the request didn't arrive
|
||||
// on a per-account listener.
|
||||
func withTunnelLookup(next http.Handler, lookup auth.TunnelLookupFunc) http.Handler {
|
||||
if lookup == nil {
|
||||
@@ -554,14 +538,10 @@ func (a inboundDebugAdapter) InboundListeners() map[types.AccountID]debug.Inboun
|
||||
}
|
||||
|
||||
// newInboundErrorLog routes a per-account http.Server's stdlib error
|
||||
// stream through logrus at warn level. The returned PipeWriter must be
|
||||
// closed by the caller (tearDown) once the http.Server has shut down —
|
||||
// otherwise the pipe and its scanner goroutine leak per account, see
|
||||
// logrus.Entry.WriterLevel.
|
||||
func newInboundErrorLog(logger *log.Logger, scheme string, accountID types.AccountID) (*stdlog.Logger, *io.PipeWriter) {
|
||||
w := logger.WithFields(log.Fields{
|
||||
// stream through logrus at warn level.
|
||||
func newInboundErrorLog(logger *log.Logger, scheme string, accountID types.AccountID) *stdlog.Logger {
|
||||
return stdlog.New(logger.WithFields(log.Fields{
|
||||
"inbound-http": scheme,
|
||||
"account_id": accountID,
|
||||
}).WriterLevel(log.WarnLevel)
|
||||
return stdlog.New(w, "", 0), w
|
||||
}).WriterLevel(log.WarnLevel), "", 0)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -111,7 +110,7 @@ func TestServer_PrivateInbound_Enabled_WiresLifecycle(t *testing.T) {
|
||||
// Construct a NetBird transport. We can't actually start the embedded
|
||||
// client here (that needs a real management server), but we can
|
||||
// confirm that the lifecycle callbacks are registered.
|
||||
s.netbird = roundtrip.NewNetBird(t.Context(), "test", "test", roundtrip.ClientConfig{
|
||||
s.netbird = roundtrip.NewNetBird("test", "test", roundtrip.ClientConfig{
|
||||
MgmtAddr: "http://invalid.test",
|
||||
}, quietLogger(), nil, fakeMgmtClient{})
|
||||
|
||||
@@ -140,7 +139,7 @@ func TestInboundManager_AddRouteAfterReady_RegistersDirectly(t *testing.T) {
|
||||
|
||||
// TestPrivateCapability_DerivedFromPrivateOnly tests that the capability
|
||||
// bit reported upstream tracks --private exclusively. The previous
|
||||
// --private flag has been folded into --private.
|
||||
// --private-inbound flag has been folded into --private.
|
||||
func TestPrivateCapability_DerivedFromPrivateOnly(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -319,7 +318,7 @@ func TestInboundManager_ListenerInfo(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestInboundManager_NilManagerSafe ensures the observability accessors
|
||||
// are safe to call when --private is off (nil manager).
|
||||
// are safe to call when --private-inbound is off (nil manager).
|
||||
func TestInboundManager_NilManagerSafe(t *testing.T) {
|
||||
var mgr *inboundManager
|
||||
_, ok := mgr.ListenerInfo("anything")
|
||||
@@ -483,38 +482,6 @@ func selfSignedTLSConfig(t *testing.T) *tls.Config {
|
||||
return &tls.Config{Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12} //nolint:gosec
|
||||
}
|
||||
|
||||
// TestNewInboundErrorLog_WriterIsCloseable guards the close path on the
|
||||
// logrus PipeWriter that backs each per-account http.Server's ErrorLog.
|
||||
// logrus.Entry.WriterLevel returns an *io.PipeWriter that owns a pipe +
|
||||
// scanner goroutine; the caller must Close() it on teardown or the
|
||||
// resources leak per account. The contract is verified two ways:
|
||||
//
|
||||
// - the constructor returns a non-nil writer the caller can keep,
|
||||
// - writing to the writer after Close() fails with io.ErrClosedPipe,
|
||||
// which is the only externally observable sign that Close was wired.
|
||||
//
|
||||
// A leaking refactor (forgetting to thread the writer to tearDown, or
|
||||
// dropping the Close call) would still pass this test individually but
|
||||
// fail an integration goleak check; this unit test is the cheap first
|
||||
// line of defence.
|
||||
func TestNewInboundErrorLog_WriterIsCloseable(t *testing.T) {
|
||||
logger := quietLogger()
|
||||
stdLog, writer := newInboundErrorLog(logger, "https", types.AccountID("acct-1"))
|
||||
|
||||
require.NotNil(t, stdLog, "newInboundErrorLog must return a non-nil *log.Logger")
|
||||
require.NotNil(t, writer, "newInboundErrorLog must return the underlying PipeWriter so tearDown can Close it")
|
||||
|
||||
// First Close succeeds.
|
||||
require.NoError(t, writer.Close(), "PipeWriter.Close should succeed the first time")
|
||||
|
||||
// After Close, the writer must refuse new writes — that's the only
|
||||
// behavioural signal that the pipe (and its scanner goroutine) has
|
||||
// shut down.
|
||||
_, err := writer.Write([]byte("post-close write\n"))
|
||||
require.ErrorIs(t, err, io.ErrClosedPipe,
|
||||
"writes after Close must surface io.ErrClosedPipe so callers know the writer is gone")
|
||||
}
|
||||
|
||||
// testCertPEM / testKeyPEM are a minimal RSA self-signed cert for
|
||||
// 127.0.0.1 — only used by tests that need a working TLS handshake.
|
||||
var testCertPEM = []byte(`-----BEGIN CERTIFICATE-----
|
||||
|
||||
@@ -346,15 +346,13 @@ func (mw *Middleware) forwardWithSessionCookie(w http.ResponseWriter, r *http.Re
|
||||
// management unreachable, peer unknown, user not in group) returns false so
|
||||
// the caller falls back to the existing OIDC scheme dispatch.
|
||||
//
|
||||
// The fast-path is gated on TunnelLookupFromContext(r.Context()) being
|
||||
// present — that context value is attached only by the per-account
|
||||
// inbound (overlay) listener. The host listener never sets it, so a
|
||||
// public client whose source IP happens to fall inside an RFC1918 / ULA
|
||||
// / CGNAT range can't impersonate a mesh peer by colliding with a
|
||||
// tunnel-IP. Once we know the request arrived over WireGuard the
|
||||
// per-account peerstore lookup is consulted: a miss denies fast (no
|
||||
// management round-trip), a hit gates the cached ValidateTunnelPeer RPC
|
||||
// that mints the session JWT.
|
||||
// Phase 3 adds a local-first short-circuit: when the request arrived on a
|
||||
// per-account inbound listener the context carries a peerstore lookup
|
||||
// (TunnelLookupFromContext). If the lookup says the IP isn't in the account's
|
||||
// roster the proxy denies fast without calling management. If the lookup
|
||||
// confirms a known peer the RPC still runs for the user-identity tail
|
||||
// (UserID + group access), but its result is cached for tunnelCacheTTL so
|
||||
// repeat requests skip management entirely.
|
||||
func (mw *Middleware) forwardWithTunnelPeer(w http.ResponseWriter, r *http.Request, host string, config DomainConfig, next http.Handler) bool {
|
||||
if mw.sessionValidator == nil {
|
||||
return false
|
||||
@@ -363,24 +361,18 @@ func (mw *Middleware) forwardWithTunnelPeer(w http.ResponseWriter, r *http.Reque
|
||||
if !clientIP.IsValid() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Anti-spoof: only honour the tunnel-peer fast-path on requests that
|
||||
// were stamped by an overlay listener. Without that marker an
|
||||
// attacker could send a request from a colliding RFC1918 / CGNAT
|
||||
// source on the public listener and bypass operator auth.
|
||||
lookup := TunnelLookupFromContext(r.Context())
|
||||
if lookup == nil {
|
||||
return false
|
||||
}
|
||||
if !isTunnelSourceIP(clientIP) {
|
||||
return false
|
||||
}
|
||||
if _, ok := lookup(clientIP); !ok {
|
||||
mw.logger.WithFields(log.Fields{
|
||||
"host": host,
|
||||
"remote": clientIP,
|
||||
}).Debug("local peerstore: tunnel IP not in account roster; denying without RPC")
|
||||
return false
|
||||
|
||||
if lookup := TunnelLookupFromContext(r.Context()); lookup != nil {
|
||||
if _, ok := lookup(clientIP); !ok {
|
||||
mw.logger.WithFields(log.Fields{
|
||||
"host": host,
|
||||
"remote": clientIP,
|
||||
}).Debug("local peerstore: tunnel IP not in account roster; denying without RPC")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
resp, _, err := mw.tunnelCache.fetch(r.Context(), tunnelCacheKey{
|
||||
|
||||
@@ -1227,93 +1227,3 @@ func TestProtect_NonOIDCSchemes_PlainHTTP_NotBlocked(t *testing.T) {
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, rec.Code, "PIN-only domain should serve the login page on plain HTTP")
|
||||
}
|
||||
|
||||
// stubTunnelValidator records ValidateTunnelPeer calls so a test can
|
||||
// assert whether the fast-path reached management.
|
||||
type stubTunnelValidator struct {
|
||||
called bool
|
||||
resp *proto.ValidateTunnelPeerResponse
|
||||
}
|
||||
|
||||
func (s *stubTunnelValidator) ValidateSession(context.Context, *proto.ValidateSessionRequest, ...grpc.CallOption) (*proto.ValidateSessionResponse, error) {
|
||||
return nil, errors.New("not used in this test")
|
||||
}
|
||||
|
||||
func (s *stubTunnelValidator) ValidateTunnelPeer(context.Context, *proto.ValidateTunnelPeerRequest, ...grpc.CallOption) (*proto.ValidateTunnelPeerResponse, error) {
|
||||
s.called = true
|
||||
return s.resp, nil
|
||||
}
|
||||
|
||||
// TestProtect_TunnelPeerFastPath_RequiresInboundMarker guards the
|
||||
// anti-spoof gate: a request with an RFC1918 source IP arriving on the
|
||||
// public listener (no TunnelLookupFromContext attached) must not be
|
||||
// allowed to take the tunnel-peer fast-path. Without this gate a public
|
||||
// client whose source IP happens to fall inside an RFC1918 range could
|
||||
// bypass the configured auth scheme by colliding with a known tunnel
|
||||
// IP.
|
||||
func TestProtect_TunnelPeerFastPath_RequiresInboundMarker(t *testing.T) {
|
||||
validator := &stubTunnelValidator{
|
||||
resp: &proto.ValidateTunnelPeerResponse{
|
||||
Valid: true,
|
||||
SessionToken: "should-not-be-used",
|
||||
UserId: "user-1",
|
||||
},
|
||||
}
|
||||
mw := NewMiddleware(log.StandardLogger(), validator, nil)
|
||||
kp := generateTestKeyPair(t)
|
||||
|
||||
scheme := &stubScheme{method: auth.MethodPIN, promptID: "pin"}
|
||||
require.NoError(t, mw.AddDomain("example.com", []Scheme{scheme}, kp.PublicKey, time.Hour, "", "", nil, false))
|
||||
|
||||
handler := mw.Protect(newPassthroughHandler())
|
||||
|
||||
// Request from an RFC1918 source IP on the public listener — no
|
||||
// TunnelLookupFromContext attached. The fast-path must reject this
|
||||
// and fall through to the PIN scheme (which renders 401 on plain
|
||||
// HTTP for a non-authenticated request).
|
||||
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
req.RemoteAddr = "100.64.0.5:5000"
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.False(t, validator.called,
|
||||
"ValidateTunnelPeer must not be invoked when the request lacks the inbound TunnelLookup marker")
|
||||
assert.Equal(t, http.StatusUnauthorized, rec.Code,
|
||||
"without the inbound marker the request must fall through to the operator auth scheme")
|
||||
}
|
||||
|
||||
// TestProtect_TunnelPeerFastPath_TakesPathWithInboundMarker verifies
|
||||
// the positive side: a request marked as overlay-origin (carrying the
|
||||
// TunnelLookup context value) and matching a tunnel-IP range does take
|
||||
// the fast-path and reach management.
|
||||
func TestProtect_TunnelPeerFastPath_TakesPathWithInboundMarker(t *testing.T) {
|
||||
validator := &stubTunnelValidator{
|
||||
resp: &proto.ValidateTunnelPeerResponse{
|
||||
Valid: true,
|
||||
SessionToken: "tunnel-session-token",
|
||||
UserId: "user-1",
|
||||
},
|
||||
}
|
||||
mw := NewMiddleware(log.StandardLogger(), validator, nil)
|
||||
kp := generateTestKeyPair(t)
|
||||
|
||||
scheme := &stubScheme{method: auth.MethodPIN, promptID: "pin"}
|
||||
require.NoError(t, mw.AddDomain("example.com", []Scheme{scheme}, kp.PublicKey, time.Hour, "", "", nil, false))
|
||||
|
||||
handler := mw.Protect(newPassthroughHandler())
|
||||
|
||||
lookup := TunnelLookupFunc(func(_ netip.Addr) (PeerIdentity, bool) {
|
||||
return PeerIdentity{}, true
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
|
||||
req.RemoteAddr = "100.64.0.5:5000"
|
||||
req = req.WithContext(WithTunnelLookup(req.Context(), lookup))
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
assert.True(t, validator.called,
|
||||
"ValidateTunnelPeer must run when the request carries the inbound TunnelLookup marker")
|
||||
assert.Equal(t, http.StatusOK, rec.Code,
|
||||
"a successful tunnel-peer validation must forward to the next handler")
|
||||
}
|
||||
|
||||
@@ -101,10 +101,7 @@ func TestForwardWithTunnelPeer_GroupsPropagateToCapturedData(t *testing.T) {
|
||||
|
||||
w, r := newTunnelRequest("100.64.0.10:55555")
|
||||
cd := proxy.NewCapturedData("")
|
||||
lookup := TunnelLookupFunc(func(_ netip.Addr) (PeerIdentity, bool) {
|
||||
return PeerIdentity{}, true
|
||||
})
|
||||
r = r.WithContext(proxy.WithCapturedData(WithTunnelLookup(r.Context(), lookup), cd))
|
||||
r = r.WithContext(proxy.WithCapturedData(r.Context(), cd))
|
||||
|
||||
called := false
|
||||
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })
|
||||
@@ -151,13 +148,9 @@ func TestForwardWithTunnelPeer_LocalLookupKnownPeerStillRPCs(t *testing.T) {
|
||||
assert.Equal(t, int32(1), validator.tunnelCalls.Load(), "RPC must run for the user-identity tail when local lookup confirms the peer")
|
||||
}
|
||||
|
||||
// TestForwardWithTunnelPeer_NoLookupRefusesFastPath guards the
|
||||
// anti-spoof gate: requests that didn't arrive on the per-account
|
||||
// inbound listener (no TunnelLookup attached) must never reach
|
||||
// management's ValidateTunnelPeer, even when the source IP looks like
|
||||
// a tunnel address. A colliding RFC1918 / CGNAT source on the public
|
||||
// listener would otherwise impersonate a mesh peer.
|
||||
func TestForwardWithTunnelPeer_NoLookupRefusesFastPath(t *testing.T) {
|
||||
// TestForwardWithTunnelPeer_NoLookupKeepsLegacyPath ensures the existing
|
||||
// behaviour stays intact on the host-level listener (no lookup attached).
|
||||
func TestForwardWithTunnelPeer_NoLookupKeepsLegacyPath(t *testing.T) {
|
||||
validator := &stubSessionValidator{
|
||||
respFn: func(_ *proto.ValidateTunnelPeerRequest) *proto.ValidateTunnelPeerResponse {
|
||||
return &proto.ValidateTunnelPeerResponse{Valid: true, SessionToken: "tok", UserId: "user-1"}
|
||||
@@ -172,9 +165,9 @@ func TestForwardWithTunnelPeer_NoLookupRefusesFastPath(t *testing.T) {
|
||||
config, _ := mw.getDomainConfig("svc.example")
|
||||
handled := mw.forwardWithTunnelPeer(w, r, "svc.example", config, next)
|
||||
|
||||
assert.False(t, handled, "fast-path must refuse without the inbound marker")
|
||||
assert.False(t, called, "next handler must not run")
|
||||
assert.Equal(t, int32(0), validator.tunnelCalls.Load(), "ValidateTunnelPeer must not be invoked without the inbound marker")
|
||||
assert.True(t, handled, "host-level path forwards on positive RPC result")
|
||||
assert.True(t, called, "next handler runs on host-level success")
|
||||
assert.Equal(t, int32(1), validator.tunnelCalls.Load(), "host-level path always RPCs (Phase 3 unchanged)")
|
||||
}
|
||||
|
||||
// TestForwardWithTunnelPeer_RPCErrorFallsThrough validates that an RPC
|
||||
@@ -208,13 +201,8 @@ func TestForwardWithTunnelPeer_CacheReusesPositiveResponse(t *testing.T) {
|
||||
}
|
||||
mw := newTunnelMiddleware(t, validator)
|
||||
|
||||
lookup := TunnelLookupFunc(func(_ netip.Addr) (PeerIdentity, bool) {
|
||||
return PeerIdentity{}, true
|
||||
})
|
||||
|
||||
for i := 0; i < 4; i++ {
|
||||
w, r := newTunnelRequest("100.64.0.10:55555")
|
||||
r = r.WithContext(WithTunnelLookup(r.Context(), lookup))
|
||||
next := http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})
|
||||
config, _ := mw.getDomainConfig("svc.example")
|
||||
handled := mw.forwardWithTunnelPeer(w, r, "svc.example", config, next)
|
||||
@@ -238,21 +226,11 @@ func TestForwardWithTunnelPeer_RoutesAccountIDIntoCacheKey(t *testing.T) {
|
||||
require.NoError(t, mw.AddDomain("svc-a.example", nil, "", 0, "acct-a", "svc-a", nil, false))
|
||||
require.NoError(t, mw.AddDomain("svc-b.example", nil, "", 0, "acct-b", "svc-b", nil, false))
|
||||
|
||||
// The fast-path requires the inbound-listener marker on the context.
|
||||
// The peerstore lookup itself is account-agnostic at this level
|
||||
// (one TunnelLookupFunc per account is attached by inbound.go); a
|
||||
// trivial "always hit" lookup is enough to exercise the cache-key
|
||||
// branch this test covers.
|
||||
lookup := TunnelLookupFunc(func(_ netip.Addr) (PeerIdentity, bool) {
|
||||
return PeerIdentity{}, true
|
||||
})
|
||||
|
||||
for _, host := range []string{"svc-a.example", "svc-b.example"} {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest(http.MethodGet, "https://"+host+"/", nil)
|
||||
r.Host = host
|
||||
r.RemoteAddr = "100.64.0.10:55555"
|
||||
r = r.WithContext(WithTunnelLookup(r.Context(), lookup))
|
||||
config, _ := mw.getDomainConfig(host)
|
||||
handled := mw.forwardWithTunnelPeer(w, r, host, config, http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
|
||||
require.True(t, handled, "host %s should forward", host)
|
||||
@@ -336,17 +314,9 @@ func TestPrivateService_ForwardsOnTunnelPeerSuccess(t *testing.T) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
// Per-account inbound listener attaches WithTunnelLookup; without it
|
||||
// forwardWithTunnelPeer refuses to take the fast-path. Mirror the
|
||||
// real flow so this test exercises the post-gating success branch.
|
||||
lookup := TunnelLookupFunc(func(_ netip.Addr) (PeerIdentity, bool) {
|
||||
return PeerIdentity{}, true
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://private.svc/", nil)
|
||||
req.Host = "private.svc"
|
||||
req.RemoteAddr = "100.64.0.10:55555"
|
||||
req = req.WithContext(WithTunnelLookup(req.Context(), lookup))
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
|
||||
@@ -131,7 +131,7 @@ func (h *Handler) SetCertStatus(cs certStatus) {
|
||||
|
||||
// SetInboundProvider wires per-account inbound listener observability.
|
||||
// Pass nil (or skip the call) to keep the inbound section out of debug
|
||||
// responses on proxies that don't run --private.
|
||||
// responses on proxies that don't run --private-inbound.
|
||||
func (h *Handler) SetInboundProvider(p InboundProvider) {
|
||||
h.inbound = p
|
||||
}
|
||||
|
||||
@@ -66,22 +66,6 @@ func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Loop guard for private services: a peer that hosts the target
|
||||
// dialing its own service URL would round-trip its own traffic
|
||||
// through the proxy and back over WG to itself. Refuse the request
|
||||
// with 421 (Misdirected Request) so the caller sees an explicit
|
||||
// error instead of silently doubling tunnel traffic.
|
||||
if p.isSelfTargetLoop(r, result.target.URL) {
|
||||
if cd := CapturedDataFromContext(r.Context()); cd != nil {
|
||||
cd.SetOrigin(OriginNoRoute)
|
||||
}
|
||||
requestID := getRequestID(r)
|
||||
web.ServeErrorPage(w, r, http.StatusMisdirectedRequest, "Loop Detected",
|
||||
"This peer is the target of the requested service. Reach the backend directly instead of dialing the public service URL from the same machine.",
|
||||
requestID, web.ErrorStatus{Proxy: true, Destination: false})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
// Set the account ID in the context for the roundtripper to use.
|
||||
ctx = roundtrip.WithAccountID(ctx, result.accountID)
|
||||
@@ -123,32 +107,6 @@ func (p *ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
rp.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
|
||||
// isSelfTargetLoop reports whether an overlay-origin request is about to
|
||||
// be forwarded back to the very peer that initiated it. The detection
|
||||
// is intentionally narrow: it only fires when the request arrived on
|
||||
// the per-account inbound (overlay) listener (so we're confident the
|
||||
// source address is the caller's tunnel IP), and only when the resolved
|
||||
// target host matches that tunnel IP. Catching this here returns 421 to
|
||||
// the caller instead of letting the proxy round-trip its own traffic
|
||||
// over WG twice.
|
||||
func (p *ReverseProxy) isSelfTargetLoop(r *http.Request, target *url.URL) bool {
|
||||
if target == nil {
|
||||
return false
|
||||
}
|
||||
if !types.IsOverlayOrigin(r.Context()) {
|
||||
return false
|
||||
}
|
||||
srcIP := extractHostIP(r.RemoteAddr)
|
||||
if !srcIP.IsValid() {
|
||||
return false
|
||||
}
|
||||
targetIP, err := netip.ParseAddr(target.Hostname())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return srcIP.Unmap() == targetIP.Unmap()
|
||||
}
|
||||
|
||||
// rewriteFunc returns a Rewrite function for httputil.ReverseProxy that rewrites
|
||||
// inbound requests to target the backend service while setting security-relevant
|
||||
// forwarding headers and stripping proxy authentication credentials.
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
|
||||
"github.com/netbirdio/netbird/proxy/auth"
|
||||
"github.com/netbirdio/netbird/proxy/internal/roundtrip"
|
||||
"github.com/netbirdio/netbird/proxy/internal/types"
|
||||
"github.com/netbirdio/netbird/proxy/web"
|
||||
)
|
||||
|
||||
@@ -1286,103 +1285,6 @@ func TestStampNetBirdIdentity_OmitsGroupsHeaderWhenAllInvalid(t *testing.T) {
|
||||
"X-NetBird-Groups must not be set when every group label is rejected")
|
||||
}
|
||||
|
||||
// nopOKTransport returns 200 for every request without dialing — used
|
||||
// by the self-target-loop tests so the non-loop cases don't pay a real
|
||||
// TCP-dial timeout.
|
||||
type nopOKTransport struct{}
|
||||
|
||||
func (nopOKTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{StatusCode: http.StatusOK, Body: http.NoBody, Header: http.Header{}}, nil
|
||||
}
|
||||
|
||||
// TestServeHTTP_SelfTargetLoopReturns421 covers the loop guard for
|
||||
// private services: when a peer dials a service whose only target is
|
||||
// the peer itself, the proxy must refuse with 421 (Misdirected
|
||||
// Request) rather than round-tripping the request back over WG to
|
||||
// the same peer.
|
||||
func TestServeHTTP_SelfTargetLoopReturns421(t *testing.T) {
|
||||
rp := NewReverseProxy(nopOKTransport{}, "auto", nil, nil)
|
||||
rp.AddMapping(Mapping{
|
||||
ID: "svc-1",
|
||||
AccountID: "acct-1",
|
||||
Host: "private.svc",
|
||||
Paths: map[string]*PathTarget{
|
||||
"/": {
|
||||
URL: &url.URL{Scheme: "http", Host: "100.64.0.5:8080"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://private.svc/", nil)
|
||||
req.Host = "private.svc"
|
||||
req.RemoteAddr = "100.64.0.5:55555"
|
||||
req = req.WithContext(types.WithOverlayOrigin(req.Context()))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
rp.ServeHTTP(rec, req)
|
||||
|
||||
assert.Equal(t, http.StatusMisdirectedRequest, rec.Code,
|
||||
"a peer dialing a service whose target is itself must get 421")
|
||||
}
|
||||
|
||||
// TestServeHTTP_SelfTargetLoop_NonOverlayRequestPassesThrough verifies
|
||||
// the guard is scoped to overlay-origin requests. A public-listener
|
||||
// request that happens to share a source IP with the target host must
|
||||
// not be misinterpreted as a loop — the gating relies on the inbound
|
||||
// marker being attached only by the per-account overlay listener.
|
||||
func TestServeHTTP_SelfTargetLoop_NonOverlayRequestPassesThrough(t *testing.T) {
|
||||
rp := NewReverseProxy(nopOKTransport{}, "auto", nil, nil)
|
||||
rp.AddMapping(Mapping{
|
||||
ID: "svc-1",
|
||||
AccountID: "acct-1",
|
||||
Host: "public.svc",
|
||||
Paths: map[string]*PathTarget{
|
||||
"/": {
|
||||
URL: &url.URL{Scheme: "http", Host: "100.64.0.5:8080"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://public.svc/", nil)
|
||||
req.Host = "public.svc"
|
||||
req.RemoteAddr = "100.64.0.5:55555"
|
||||
// No WithOverlayOrigin → the guard must not fire.
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
rp.ServeHTTP(rec, req)
|
||||
|
||||
assert.NotEqual(t, http.StatusMisdirectedRequest, rec.Code,
|
||||
"a non-overlay request with a colliding source IP must not be flagged as a loop")
|
||||
}
|
||||
|
||||
// TestServeHTTP_SelfTargetLoop_OverlayDifferentIPPassesThrough confirms
|
||||
// that overlay-origin requests with a source IP that does *not* match
|
||||
// the target host are forwarded normally.
|
||||
func TestServeHTTP_SelfTargetLoop_OverlayDifferentIPPassesThrough(t *testing.T) {
|
||||
rp := NewReverseProxy(nopOKTransport{}, "auto", nil, nil)
|
||||
rp.AddMapping(Mapping{
|
||||
ID: "svc-1",
|
||||
AccountID: "acct-1",
|
||||
Host: "private.svc",
|
||||
Paths: map[string]*PathTarget{
|
||||
"/": {
|
||||
URL: &url.URL{Scheme: "http", Host: "100.64.0.5:8080"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "http://private.svc/", nil)
|
||||
req.Host = "private.svc"
|
||||
req.RemoteAddr = "100.64.0.99:55555" // different from the target
|
||||
req = req.WithContext(types.WithOverlayOrigin(req.Context()))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
rp.ServeHTTP(rec, req)
|
||||
|
||||
assert.NotEqual(t, http.StatusMisdirectedRequest, rec.Code,
|
||||
"overlay request with a non-matching source IP must not be flagged as a loop")
|
||||
}
|
||||
|
||||
// TestStampNetBirdIdentity_CapturedDataPresentButEmpty covers requests
|
||||
// that carry CapturedData with no identity fields populated (e.g. the
|
||||
// auth middleware ran but the request didn't authenticate). Both
|
||||
|
||||
@@ -152,7 +152,6 @@ type managementClient interface {
|
||||
// backed by underlying NetBird connections.
|
||||
// Clients are keyed by AccountID, allowing multiple services to share the same connection.
|
||||
type NetBird struct {
|
||||
ctx context.Context
|
||||
proxyID string
|
||||
proxyAddr string
|
||||
clientCfg ClientConfig
|
||||
@@ -214,11 +213,7 @@ func (n *NetBird) AddPeer(ctx context.Context, accountID types.AccountID, key Se
|
||||
}).Debug("registered service with existing client")
|
||||
|
||||
if started && n.statusNotifier != nil {
|
||||
// Use a background context, not the caller's: the management
|
||||
// connection notification must land even if the request /
|
||||
// stream that triggered this registration is cancelled.
|
||||
// Mirrors the async runClientStartup path.
|
||||
if err := n.statusNotifier.NotifyStatus(context.Background(), accountID, serviceID, true); err != nil {
|
||||
if err := n.statusNotifier.NotifyStatus(ctx, accountID, serviceID, true); err != nil {
|
||||
n.logger.WithFields(log.Fields{
|
||||
"account_id": accountID,
|
||||
"service_key": key,
|
||||
@@ -247,10 +242,8 @@ func (n *NetBird) AddPeer(ctx context.Context, accountID types.AccountID, key Se
|
||||
}).Info("created new client for account")
|
||||
|
||||
// Attempt to start the client in the background; if this fails we will
|
||||
// retry on the first request via RoundTrip. runClientStartup uses its
|
||||
// own background context so the caller's request-scoped ctx can't
|
||||
// cancel the inbound bring-up.
|
||||
go n.runClientStartup(accountID, entry.client)
|
||||
// retry on the first request via RoundTrip.
|
||||
go n.runClientStartup(ctx, accountID, entry.client)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -314,7 +307,7 @@ func (n *NetBird) createClientEntry(ctx context.Context, accountID types.Account
|
||||
ManagementURL: n.clientCfg.MgmtAddr,
|
||||
PrivateKey: privateKey.String(),
|
||||
LogLevel: log.WarnLevel.String(),
|
||||
BlockInbound: n.clientCfg.BlockInbound,
|
||||
BlockInbound: n.clientCfg.BlockInbound,
|
||||
// The embedded proxy peer must never be a stepping stone into
|
||||
// the proxy host's LAN: it only exists to reach NetBird mesh
|
||||
// targets or, when direct_upstream is set, the host network
|
||||
@@ -362,14 +355,8 @@ func (n *NetBird) createClientEntry(ctx context.Context, accountID types.Account
|
||||
}, nil
|
||||
}
|
||||
|
||||
// runClientStartup starts the client and notifies registered services on
|
||||
// success. This function runs in a goroutine launched from AddPeer, so it
|
||||
// must never inherit the caller's request-scoped context — a canceled
|
||||
// request must not abort the inbound listener bring-up or the management
|
||||
// status notification. The embedded client.Start gets its own bounded
|
||||
// startCtx; once Start succeeds, notifyClientReady takes over with a
|
||||
// fresh context.Background() (see that function for the contract).
|
||||
func (n *NetBird) runClientStartup(accountID types.AccountID, client *embed.Client) {
|
||||
// runClientStartup starts the client and notifies registered services on success.
|
||||
func (n *NetBird) runClientStartup(ctx context.Context, accountID types.AccountID, client *embed.Client) {
|
||||
startCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -382,17 +369,7 @@ func (n *NetBird) runClientStartup(accountID types.AccountID, client *embed.Clie
|
||||
return
|
||||
}
|
||||
|
||||
n.notifyClientReady(accountID, client)
|
||||
}
|
||||
|
||||
// notifyClientReady marks the account's client as started, fires the
|
||||
// readyHandler hook, and notifies management of the new tunnel
|
||||
// connection for every registered service. It is split out of
|
||||
// runClientStartup so a regression test can drive the post-Start tail
|
||||
// without needing a live embedded client. The contract that the
|
||||
// hooks/notifier see context.Background() — never the AddPeer caller's
|
||||
// ctx — lives here.
|
||||
func (n *NetBird) notifyClientReady(accountID types.AccountID, client *embed.Client) {
|
||||
// Mark client as started and collect services to notify outside the lock.
|
||||
n.clientsMux.Lock()
|
||||
entry, exists := n.clients[accountID]
|
||||
if exists {
|
||||
@@ -408,7 +385,7 @@ func (n *NetBird) notifyClientReady(accountID types.AccountID, client *embed.Cli
|
||||
n.clientsMux.Unlock()
|
||||
|
||||
if readyHandler != nil {
|
||||
state := readyHandler(n.ctx, accountID, client)
|
||||
state := readyHandler(ctx, accountID, client)
|
||||
n.clientsMux.Lock()
|
||||
if e, ok := n.clients[accountID]; ok {
|
||||
e.inbound = state
|
||||
@@ -427,7 +404,7 @@ func (n *NetBird) notifyClientReady(accountID types.AccountID, client *embed.Cli
|
||||
return
|
||||
}
|
||||
for _, sn := range toNotify {
|
||||
if err := n.statusNotifier.NotifyStatus(n.ctx, accountID, sn.serviceID, true); err != nil {
|
||||
if err := n.statusNotifier.NotifyStatus(ctx, accountID, sn.serviceID, true); err != nil {
|
||||
n.logger.WithFields(log.Fields{
|
||||
"account_id": accountID,
|
||||
"service_key": sn.key,
|
||||
@@ -689,12 +666,11 @@ func (n *NetBird) ListClientsForStartup() map[types.AccountID]*embed.Client {
|
||||
// NewNetBird creates a new NetBird transport. Set clientCfg.WGPort to 0 for a random
|
||||
// OS-assigned port. A fixed port only works with single-account deployments;
|
||||
// multiple accounts will fail to bind the same port.
|
||||
func NewNetBird(ctx context.Context, proxyID, proxyAddr string, clientCfg ClientConfig, logger *log.Logger, notifier statusNotifier, mgmtClient managementClient) *NetBird {
|
||||
func NewNetBird(proxyID, proxyAddr string, clientCfg ClientConfig, logger *log.Logger, notifier statusNotifier, mgmtClient managementClient) *NetBird {
|
||||
if logger == nil {
|
||||
logger = log.StandardLogger()
|
||||
}
|
||||
return &NetBird{
|
||||
ctx: ctx,
|
||||
proxyID: proxyID,
|
||||
proxyAddr: proxyAddr,
|
||||
clientCfg: clientCfg,
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
|
||||
"github.com/netbirdio/netbird/client/embed"
|
||||
"github.com/netbirdio/netbird/proxy/internal/types"
|
||||
"github.com/netbirdio/netbird/shared/management/proto"
|
||||
)
|
||||
@@ -31,15 +30,12 @@ type statusCall struct {
|
||||
accountID types.AccountID
|
||||
serviceID types.ServiceID
|
||||
connected bool
|
||||
// ctx is captured so tests can assert the notifier received a
|
||||
// fresh background context rather than an inherited request ctx.
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (m *mockStatusNotifier) NotifyStatus(ctx context.Context, accountID types.AccountID, serviceID types.ServiceID, connected bool) error {
|
||||
func (m *mockStatusNotifier) NotifyStatus(_ context.Context, accountID types.AccountID, serviceID types.ServiceID, connected bool) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.statuses = append(m.statuses, statusCall{accountID, serviceID, connected, ctx})
|
||||
m.statuses = append(m.statuses, statusCall{accountID, serviceID, connected})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -52,7 +48,7 @@ func (m *mockStatusNotifier) calls() []statusCall {
|
||||
// mockNetBird creates a NetBird instance for testing without actually connecting.
|
||||
// It uses an invalid management URL to prevent real connections.
|
||||
func mockNetBird() *NetBird {
|
||||
return NewNetBird(context.Background(), "test-proxy", "invalid.test", ClientConfig{
|
||||
return NewNetBird("test-proxy", "invalid.test", ClientConfig{
|
||||
MgmtAddr: "http://invalid.test:9999",
|
||||
WGPort: 0,
|
||||
PreSharedKey: "",
|
||||
@@ -283,7 +279,7 @@ func TestNetBird_RoundTrip_RequiresExistingClient(t *testing.T) {
|
||||
|
||||
func TestNetBird_AddPeer_ExistingStartedClient_NotifiesStatus(t *testing.T) {
|
||||
notifier := &mockStatusNotifier{}
|
||||
nb := NewNetBird(context.Background(), "test-proxy", "invalid.test", ClientConfig{
|
||||
nb := NewNetBird("test-proxy", "invalid.test", ClientConfig{
|
||||
MgmtAddr: "http://invalid.test:9999",
|
||||
WGPort: 0,
|
||||
PreSharedKey: "",
|
||||
@@ -299,12 +295,8 @@ func TestNetBird_AddPeer_ExistingStartedClient_NotifiesStatus(t *testing.T) {
|
||||
nb.clients[accountID].started = true
|
||||
nb.clientsMux.Unlock()
|
||||
|
||||
// Add second service with an already-cancelled caller context —
|
||||
// should notify immediately (client is started) AND the notification
|
||||
// must not inherit the cancelled ctx.
|
||||
cancelledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
err = nb.AddPeer(cancelledCtx, accountID, "domain2.test", "key-1", types.ServiceID("svc-2"))
|
||||
// Add second service — should notify immediately since client is already started.
|
||||
err = nb.AddPeer(context.Background(), accountID, "domain2.test", "key-1", types.ServiceID("svc-2"))
|
||||
require.NoError(t, err)
|
||||
|
||||
calls := notifier.calls()
|
||||
@@ -312,9 +304,6 @@ func TestNetBird_AddPeer_ExistingStartedClient_NotifiesStatus(t *testing.T) {
|
||||
assert.Equal(t, accountID, calls[0].accountID)
|
||||
assert.Equal(t, types.ServiceID("svc-2"), calls[0].serviceID)
|
||||
assert.True(t, calls[0].connected)
|
||||
require.NotNil(t, calls[0].ctx, "NotifyStatus must receive a context")
|
||||
require.NoError(t, calls[0].ctx.Err(),
|
||||
"already-started NotifyStatus must use a background ctx, not the cancelled caller ctx")
|
||||
}
|
||||
|
||||
// TestNetBird_IdentityForIP_UnknownAccountReturnsFalse confirms that the
|
||||
@@ -349,7 +338,7 @@ func TestClientEntry_IdentityForIP_InvalidIPReturnsFalse(t *testing.T) {
|
||||
|
||||
func TestNetBird_RemovePeer_NotifiesDisconnection(t *testing.T) {
|
||||
notifier := &mockStatusNotifier{}
|
||||
nb := NewNetBird(context.Background(), "test-proxy", "invalid.test", ClientConfig{
|
||||
nb := NewNetBird("test-proxy", "invalid.test", ClientConfig{
|
||||
MgmtAddr: "http://invalid.test:9999",
|
||||
WGPort: 0,
|
||||
PreSharedKey: "",
|
||||
@@ -371,53 +360,3 @@ func TestNetBird_RemovePeer_NotifiesDisconnection(t *testing.T) {
|
||||
assert.Equal(t, types.ServiceID("svc-1"), calls[0].serviceID)
|
||||
assert.False(t, calls[0].connected)
|
||||
}
|
||||
|
||||
// TestNotifyClientReady_UsesBackgroundCtx pins the contract that the
|
||||
// post-Start hooks (readyHandler + statusNotifier.NotifyStatus) run on
|
||||
// a fresh context.Background() rather than inheriting the AddPeer
|
||||
// caller's request- or stream-scoped ctx. Without this, a cancelled
|
||||
// caller ctx could abort the inbound listener bring-up or cause the
|
||||
// management status notification to fail spuriously and leave the
|
||||
// account in a half-connected state.
|
||||
func TestNotifyClientReady_UsesBackgroundCtx(t *testing.T) {
|
||||
notifier := &mockStatusNotifier{}
|
||||
nb := NewNetBird(context.Background(), "test-proxy", "invalid.test", ClientConfig{
|
||||
MgmtAddr: "http://invalid.test:9999",
|
||||
}, nil, notifier, &mockMgmtClient{})
|
||||
|
||||
accountID := types.AccountID("acct-async")
|
||||
// Pre-populate a client entry so notifyClientReady has something
|
||||
// to mark started + something to enumerate for NotifyStatus.
|
||||
nb.clientsMux.Lock()
|
||||
nb.clients[accountID] = &clientEntry{
|
||||
services: map[ServiceKey]serviceInfo{
|
||||
DomainServiceKey("svc.example"): {serviceID: types.ServiceID("svc-1")},
|
||||
},
|
||||
}
|
||||
nb.clientsMux.Unlock()
|
||||
|
||||
var capturedReadyCtx context.Context
|
||||
nb.SetClientLifecycle(
|
||||
func(ctx context.Context, _ types.AccountID, _ *embed.Client) any {
|
||||
capturedReadyCtx = ctx
|
||||
return nil
|
||||
},
|
||||
nil,
|
||||
)
|
||||
|
||||
// Drive the post-Start path directly; a real client.Start would
|
||||
// need a working management URL.
|
||||
nb.notifyClientReady(accountID, nil)
|
||||
|
||||
require.NotNil(t, capturedReadyCtx, "readyHandler must have been invoked")
|
||||
require.NoError(t, capturedReadyCtx.Err(),
|
||||
"readyHandler must receive a background context, not an inherited cancelled one")
|
||||
deadline, ok := capturedReadyCtx.Deadline()
|
||||
assert.False(t, ok, "readyHandler ctx must have no deadline (background); got %v", deadline)
|
||||
|
||||
calls := notifier.calls()
|
||||
require.Len(t, calls, 1, "NotifyStatus must be invoked once per registered service")
|
||||
require.NotNil(t, calls[0].ctx, "NotifyStatus must receive a context")
|
||||
require.NoError(t, calls[0].ctx.Err(),
|
||||
"NotifyStatus must receive a background context, not an inherited cancelled one")
|
||||
}
|
||||
|
||||
@@ -1781,14 +1781,11 @@ func TestRouter_PlainHTTP_RoutesToPlainChannel(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
tlsListener, ok := router.HTTPListener().(*chanListener)
|
||||
require.True(t, ok, "router.HTTPListener() must be the test's chanListener; the test relies on observing its channel directly")
|
||||
|
||||
select {
|
||||
case conn := <-acceptDone:
|
||||
require.NotNil(t, conn)
|
||||
_ = conn.Close()
|
||||
case <-tlsListener.ch:
|
||||
case <-router.HTTPListener().(*chanListener).ch:
|
||||
t.Fatal("plain HTTP request leaked into TLS channel")
|
||||
case <-time.After(3 * time.Second):
|
||||
t.Fatal("plain HTTP connection never reached plain channel")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
@@ -21,17 +20,14 @@ import (
|
||||
type Config struct {
|
||||
// ListenAddr is the TCP address the main listener binds. Required.
|
||||
ListenAddr string
|
||||
// ID identifies this proxy instance to management. Empty values are
|
||||
// replaced with a timestamped default at Server.Start time (see
|
||||
// initDefaults), not in New.
|
||||
// ID identifies this proxy instance to management. Empty value lets
|
||||
// New generate a timestamped default.
|
||||
ID string
|
||||
// Logger is the logrus logger used everywhere. Empty values fall
|
||||
// back to log.StandardLogger() at Server.Start time (see
|
||||
// initDefaults), not in New.
|
||||
// Logger is the logrus logger used everywhere. Empty value falls back
|
||||
// to log.StandardLogger().
|
||||
Logger *log.Logger
|
||||
// Version is the build version string reported to management. Empty
|
||||
// values are replaced with "dev" at Server.Start time (see
|
||||
// initDefaults), not in New.
|
||||
// becomes "dev".
|
||||
Version string
|
||||
// ProxyURL is the public address operators use to reach this proxy.
|
||||
ProxyURL string
|
||||
@@ -129,9 +125,8 @@ type Config struct {
|
||||
// bound — call Start to bring the proxy up. Returning a fully-formed
|
||||
// Server keeps the standalone code path (which still constructs Server
|
||||
// directly) byte-for-byte equivalent.
|
||||
func New(ctx context.Context, cfg Config) *Server {
|
||||
func New(cfg Config) *Server {
|
||||
return &Server{
|
||||
ctx: ctx,
|
||||
ListenAddr: cfg.ListenAddr,
|
||||
ID: cfg.ID,
|
||||
Logger: cfg.Logger,
|
||||
|
||||
@@ -73,7 +73,7 @@ func benchServerWithLatency(b *testing.B, createPeerDelay, statusDelay time.Dura
|
||||
statusUpdateDelay: statusDelay,
|
||||
}
|
||||
|
||||
nb := roundtrip.NewNetBird(b.Context(), "bench-proxy", "bench.test",
|
||||
nb := roundtrip.NewNetBird("bench-proxy", "bench.test",
|
||||
roundtrip.ClientConfig{MgmtAddr: "http://bench.test:9999"},
|
||||
logger, nil, mgmtClient)
|
||||
|
||||
|
||||
@@ -75,7 +75,6 @@ type portRouter struct {
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
ctx context.Context
|
||||
mgmtClient proto.ProxyServiceClient
|
||||
proxy *proxy.ReverseProxy
|
||||
netbird *roundtrip.NetBird
|
||||
@@ -282,7 +281,7 @@ func (s *Server) NotifyCertificateIssued(ctx context.Context, accountID types.Ac
|
||||
}
|
||||
|
||||
// inboundListenerProto resolves the per-account inbound listener state for
|
||||
// the SendStatusUpdate payload. Returns nil when --private is off
|
||||
// the SendStatusUpdate payload. Returns nil when --private-inbound is off
|
||||
// or the account has no live listener so management treats the field as
|
||||
// absent.
|
||||
func (s *Server) inboundListenerProto(accountID types.AccountID) *proto.ProxyInboundListener {
|
||||
@@ -529,10 +528,10 @@ func (s *Server) initManagementClient() error {
|
||||
}
|
||||
|
||||
// initNetBirdClient builds the multi-tenant embedded NetBird client used
|
||||
// for outbound RoundTripping and (when --private is on) per-account
|
||||
// for outbound RoundTripping and (when --private-inbound is on) per-account
|
||||
// inbound listeners.
|
||||
func (s *Server) initNetBirdClient() {
|
||||
s.netbird = roundtrip.NewNetBird(s.ctx, s.ID, s.ProxyURL, roundtrip.ClientConfig{
|
||||
s.netbird = roundtrip.NewNetBird(s.ID, s.ProxyURL, roundtrip.ClientConfig{
|
||||
MgmtAddr: s.ManagementAddress,
|
||||
WGPort: s.WireguardPort,
|
||||
PreSharedKey: s.PreSharedKey,
|
||||
|
||||
@@ -64,7 +64,7 @@ func quietLifecycleLogger() *log.Logger {
|
||||
}
|
||||
|
||||
func TestStopBeforeStartIsNoOp(t *testing.T) {
|
||||
srv := New(t.Context(), Config{Logger: quietLifecycleLogger()})
|
||||
srv := New(Config{Logger: quietLifecycleLogger()})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
@@ -77,7 +77,7 @@ func TestStopBeforeStartIsNoOp(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStartFailsWithoutManagement(t *testing.T) {
|
||||
srv := New(t.Context(), Config{
|
||||
srv := New(Config{
|
||||
Logger: quietLifecycleLogger(),
|
||||
ListenAddr: "127.0.0.1:0",
|
||||
ManagementAddress: "://broken-url",
|
||||
@@ -137,7 +137,7 @@ func TestRecordRunErrPreservesFirstFailure(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStopSkipsShutdownWhenNeverStarted(t *testing.T) {
|
||||
srv := New(t.Context(), Config{Logger: quietLifecycleLogger()})
|
||||
srv := New(Config{Logger: quietLifecycleLogger()})
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
@@ -2,7 +2,6 @@ package rest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
@@ -34,12 +33,6 @@ func (a *ReverseProxyClustersAPI) List(ctx context.Context) ([]api.ProxyCluster,
|
||||
// NetBird cannot be deleted via this endpoint; the server returns 404 / 400
|
||||
// for cluster addresses the account does not own.
|
||||
func (a *ReverseProxyClustersAPI) Delete(ctx context.Context, clusterAddress string) error {
|
||||
// Guard against the empty input: url.PathEscape("") returns "" which
|
||||
// would collapse the request URL onto the collection endpoint and
|
||||
// silently delete nothing (or 405 depending on routing).
|
||||
if clusterAddress == "" {
|
||||
return errors.New("clusterAddress is required")
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/reverse-proxies/clusters/"+url.PathEscape(clusterAddress), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -88,17 +88,3 @@ func TestReverseProxyClusters_Delete_Err(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReverseProxyClusters_Delete_EmptyAddress guards against an empty
|
||||
// clusterAddress reaching the wire — that would collapse the URL onto
|
||||
// the collection endpoint instead of a specific cluster. The client
|
||||
// must short-circuit with a typed error before any request is issued.
|
||||
func TestReverseProxyClusters_Delete_EmptyAddress(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/clusters/", func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("empty clusterAddress must be rejected client-side; no request should reach the server")
|
||||
})
|
||||
err := c.ReverseProxyClusters.Delete(context.Background(), "")
|
||||
assert.Error(t, err, "empty clusterAddress must surface as an error")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
|
||||
"github.com/netbirdio/netbird/shared/management/http/api"
|
||||
@@ -62,12 +61,6 @@ func (a *ReverseProxyTokensAPI) Create(ctx context.Context, request api.ProxyTok
|
||||
// credentials existed; the plain secret can no longer authenticate any
|
||||
// new proxy registration.
|
||||
func (a *ReverseProxyTokensAPI) Delete(ctx context.Context, tokenID string) error {
|
||||
// Guard against the empty input: url.PathEscape("") returns "" which
|
||||
// would collapse the request URL onto the collection endpoint and
|
||||
// silently delete nothing (or 405 depending on routing).
|
||||
if tokenID == "" {
|
||||
return errors.New("tokenID is required")
|
||||
}
|
||||
resp, err := a.c.NewRequest(ctx, "DELETE", "/api/reverse-proxies/proxy-tokens/"+url.PathEscape(tokenID), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -129,16 +129,3 @@ func TestReverseProxyTokens_Delete_Err(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
// TestReverseProxyTokens_Delete_EmptyID guards against an empty tokenID
|
||||
// reaching the wire — url.PathEscape("") would collapse the URL onto
|
||||
// the collection endpoint.
|
||||
func TestReverseProxyTokens_Delete_EmptyID(t *testing.T) {
|
||||
withMockClient(func(c *rest.Client, mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/reverse-proxies/proxy-tokens/", func(http.ResponseWriter, *http.Request) {
|
||||
t.Fatal("empty tokenID must be rejected client-side; no request should reach the server")
|
||||
})
|
||||
err := c.ReverseProxyTokens.Delete(context.Background(), "")
|
||||
assert.Error(t, err, "empty tokenID must surface as an error")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3086,24 +3086,6 @@ components:
|
||||
- enabled
|
||||
- auth
|
||||
- meta
|
||||
allOf:
|
||||
# When private=true, access_groups must be present and non-empty,
|
||||
# and the service mode must be "http". The bearer-auth mutex is
|
||||
# enforced at the service-validation layer
|
||||
# (validatePrivateRequirements) because it sits in a nested
|
||||
# ServiceAuthConfig and isn't cleanly expressible here.
|
||||
- if:
|
||||
required: [private]
|
||||
properties:
|
||||
private:
|
||||
const: true
|
||||
then:
|
||||
required: [access_groups]
|
||||
properties:
|
||||
access_groups:
|
||||
minItems: 1
|
||||
mode:
|
||||
const: http
|
||||
ServiceMeta:
|
||||
type: object
|
||||
properties:
|
||||
@@ -3191,23 +3173,6 @@ components:
|
||||
- name
|
||||
- domain
|
||||
- enabled
|
||||
allOf:
|
||||
# Mirror of the Service conditional: when private=true the
|
||||
# request must carry a non-empty access_groups list and the
|
||||
# mode must be "http". The bearer-auth mutex is enforced at the
|
||||
# service-validation layer (validatePrivateRequirements).
|
||||
- if:
|
||||
required: [private]
|
||||
properties:
|
||||
private:
|
||||
const: true
|
||||
then:
|
||||
required: [access_groups]
|
||||
properties:
|
||||
access_groups:
|
||||
minItems: 1
|
||||
mode:
|
||||
const: http
|
||||
ServiceTargetOptions:
|
||||
type: object
|
||||
properties:
|
||||
|
||||
@@ -237,7 +237,7 @@ message SendStatusUpdateRequest {
|
||||
bool certificate_issued = 4;
|
||||
optional string error_message = 5;
|
||||
// Per-account inbound listener state for the account that owns
|
||||
// service_id. Populated only when --private is enabled and the
|
||||
// service_id. Populated only when --private-inbound is enabled and the
|
||||
// embedded client for the account is up. Field numbers >=50 reserved
|
||||
// for observability extensions.
|
||||
optional ProxyInboundListener inbound_listener = 50;
|
||||
|
||||
Reference in New Issue
Block a user