Compare commits

..

37 Commits

Author SHA1 Message Date
JackDoan ef6ada3a2b document new config options 2026-03-09 12:55:30 -05:00
JackDoan 3e3bd9cead add some context for the next guy 2026-03-04 13:38:58 -06:00
JackDoan a2c2235b9b rename 2026-03-04 13:11:23 -06:00
JackDoan 2e50518066 new error 2026-03-04 13:08:15 -06:00
JackDoan 1580175b2e remove silly panic 2026-03-04 12:36:23 -06:00
JackDoan 36bbc515d2 log if UnsafeNetworks assignment changes across reload 2026-03-04 12:33:24 -06:00
JackDoan 09fe406dba log if V1 and V2 certs have mismatched UnsafeNetworks 2026-03-04 12:33:24 -06:00
Nate Brown d21baede1f Nits and fix tests 2026-02-27 18:09:52 -06:00
Nate Brown 037459ef73 Review nits 2026-02-27 17:49:31 -06:00
Nate Brown 7655a10108 Remove thing 2026-02-27 16:51:40 -06:00
JackDoan 5cbccdc0fd remove dead comment 2026-02-26 11:48:53 -06:00
JackDoan 629700fbb6 feedback 2026-02-26 10:58:10 -06:00
JackDoan e4897b07c9 leftover cruft from merging 2026-02-26 10:49:05 -06:00
JackDoan f7dd3c0ce4 moar test 2026-02-26 10:46:06 -06:00
JackDoan 009a4698a0 thanks clod! 2026-02-26 10:31:18 -06:00
JackDoan 34e817742b thanks clod! 2026-02-26 10:26:16 -06:00
JackDoan a881e4fdf8 fix test2 2026-02-20 12:10:14 -06:00
JackDoan e77f49abb8 fix test 2026-02-20 12:00:21 -06:00
JackDoan 2319eb9492 remove notes 2026-02-20 11:57:43 -06:00
JackDoan ae1b501468 oops 2026-02-20 11:29:46 -06:00
JackDoan 879b77d076 oops 2026-02-19 15:57:02 -06:00
JackDoan dd786cddf1 appease CI 2026-02-19 14:59:32 -06:00
JackDoan 8f1d384eb8 think really hard 2026-02-19 14:55:49 -06:00
JackDoan 064153f0c2 split the client-snat-addr and the router-snat-addr to decrease confusion hopefully 2026-02-19 14:18:09 -06:00
JackDoan 25610225bb crappy AI tests 2026-02-19 10:23:35 -06:00
JackDoan 92ee45ed13 tun tester more useful 2026-02-19 10:23:35 -06:00
JackDoan 37abdd7f96 it works again but linux is pickier than I thought, I need to refactor even more 2026-02-19 10:23:11 -06:00
JackDoan 7498c6846d checkpt 2026-02-19 10:23:11 -06:00
JackDoan 27d764ba57 auto-assign snataddr on Mac+Windows 2026-02-19 10:23:11 -06:00
JackDoan 1cc257f997 bolt more stuff onto tun to help auto-assign snat addresses 2026-02-19 10:23:11 -06:00
JackDoan 83744a106d checkpt 2026-02-19 10:23:11 -06:00
JackDoan 70399ea533 use in-Nebula SNAT to send IPv4 UnsafeNetworks traffic over an IPv6 overlay 2026-02-19 10:23:11 -06:00
Jack Doan 51308b845b connection-track ICMP traffic (#1602)
gofmt / Run gofmt (push) Failing after 2s
smoke-extra / Run extra smoke tests (push) Failing after 3s
smoke / Run multi node smoke test (push) Failing after 2s
Build and test / Build all and test on ubuntu-linux (push) Failing after 2s
Build and test / Build and test on linux with boringcrypto (push) Failing after 2s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
* connection-track ICMP and ICMPv6 traffic

* icmpv6 only has identifier on echo
2026-02-18 23:19:37 -06:00
Wade Simmons 422fc2ad1e go fix (#1608)
gofmt / Run gofmt (push) Failing after 3s
smoke-extra / Run extra smoke tests (push) Failing after 2s
smoke / Run multi node smoke test (push) Failing after 2s
Build and test / Build all and test on ubuntu-linux (push) Failing after 2s
Build and test / Build and test on linux with boringcrypto (push) Failing after 3s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
2026-02-17 11:42:14 -05:00
Wade Simmons e8bb874e14 smoke-extra: try AMD-V workaround (#1610)
gofmt / Run gofmt (push) Failing after 3s
smoke-extra / Run extra smoke tests (push) Failing after 2s
smoke / Run multi node smoke test (push) Failing after 2s
Build and test / Build all and test on ubuntu-linux (push) Failing after 3s
Build and test / Build and test on linux with boringcrypto (push) Failing after 2s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
* smoke-extra: try AMD-V workaround

- https://github.com/slackhq/nebula/actions/runs/21995850645/job/63555492676?pr=1602
- https://github.com/actions/runner-images/issues/13202
- https://github.com/cri-o/packaging/pull/306/changes
2026-02-13 12:55:19 -06:00
Jack Doan 353ad1f271 firewall: icmp no longer requires a port spec (#1609) 2026-02-13 11:10:40 -06:00
Jack Doan f573e8a266 Merge commit from fork
gofmt / Run gofmt (push) Failing after 3s
smoke-extra / Run extra smoke tests (push) Failing after 2s
smoke / Run multi node smoke test (push) Failing after 3s
Build and test / Build all and test on ubuntu-linux (push) Failing after 2s
Build and test / Build and test on linux with boringcrypto (push) Failing after 3s
Build and test / Build and test on linux with pkcs11 (push) Failing after 2s
Build and test / Build and test on macos-latest (push) Has been cancelled
Build and test / Build and test on windows-latest (push) Has been cancelled
Newly signed P256 based certificates will have their signature clamped to the low-s form.

Update CHANGELOG.md
2026-02-06 14:26:51 -05:00
67 changed files with 3683 additions and 1695 deletions
+3
View File
@@ -30,6 +30,9 @@ jobs:
- name: add hashicorp source - name: add hashicorp source
run: wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list run: wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg && echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
- name: workaround AMD-V issue # https://github.com/cri-o/packaging/pull/306
run: sudo rmmod kvm_amd
- name: install vagrant - name: install vagrant
run: sudo apt-get update && sudo apt-get install -y vagrant virtualbox run: sudo apt-get update && sudo apt-get install -y vagrant virtualbox
+10 -9
View File
@@ -37,17 +37,18 @@ docker run --name host4 --device /dev/net/tun:/dev/net/tun --cap-add NET_ADMIN -
sleep 1 sleep 1
# grab tcpdump pcaps for debugging # grab tcpdump pcaps for debugging
docker exec lighthouse1 tcpdump -i nebula1 -q -w - -U 2>logs/lighthouse1.inside.log >logs/lighthouse1.inside.pcap & docker exec lighthouse1 tcpdump -i tun0 -q -w - -U 2>logs/lighthouse1.inside.log >logs/lighthouse1.inside.pcap &
docker exec lighthouse1 tcpdump -i eth0 -q -w - -U 2>logs/lighthouse1.outside.log >logs/lighthouse1.outside.pcap & docker exec lighthouse1 tcpdump -i eth0 -q -w - -U 2>logs/lighthouse1.outside.log >logs/lighthouse1.outside.pcap &
docker exec host2 tcpdump -i nebula1 -q -w - -U 2>logs/host2.inside.log >logs/host2.inside.pcap & docker exec host2 tcpdump -i tun0 -q -w - -U 2>logs/host2.inside.log >logs/host2.inside.pcap &
docker exec host2 tcpdump -i eth0 -q -w - -U 2>logs/host2.outside.log >logs/host2.outside.pcap & docker exec host2 tcpdump -i eth0 -q -w - -U 2>logs/host2.outside.log >logs/host2.outside.pcap &
docker exec host3 tcpdump -i nebula1 -q -w - -U 2>logs/host3.inside.log >logs/host3.inside.pcap & docker exec host3 tcpdump -i tun0 -q -w - -U 2>logs/host3.inside.log >logs/host3.inside.pcap &
docker exec host3 tcpdump -i eth0 -q -w - -U 2>logs/host3.outside.log >logs/host3.outside.pcap & docker exec host3 tcpdump -i eth0 -q -w - -U 2>logs/host3.outside.log >logs/host3.outside.pcap &
docker exec host4 tcpdump -i nebula1 -q -w - -U 2>logs/host4.inside.log >logs/host4.inside.pcap & docker exec host4 tcpdump -i tun0 -q -w - -U 2>logs/host4.inside.log >logs/host4.inside.pcap &
docker exec host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host4.outside.pcap & docker exec host4 tcpdump -i eth0 -q -w - -U 2>logs/host4.outside.log >logs/host4.outside.pcap &
docker exec host2 ncat -nklv 0.0.0.0 2000 & docker exec host2 ncat -nklv 0.0.0.0 2000 &
docker exec host3 ncat -nklv 0.0.0.0 2000 & docker exec host3 ncat -nklv 0.0.0.0 2000 &
docker exec host4 ncat -nkluv 0.0.0.0 4000 &
docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 & docker exec host2 ncat -e '/usr/bin/echo host2' -nkluv 0.0.0.0 3000 &
docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000 & docker exec host3 ncat -e '/usr/bin/echo host3' -nkluv 0.0.0.0 3000 &
@@ -119,11 +120,11 @@ echo
echo " *** Testing conntrack" echo " *** Testing conntrack"
echo echo
set -x set -x
# host2 can ping host3 now that host3 pinged it first
docker exec host2 ping -c1 192.168.100.3 # host2 speaking to host4 on UDP 4000 should allow it to reply, when firewall rules would normally not permit this
# host4 can ping host2 once conntrack established docker exec host2 sh -c "/usr/bin/echo host2 | ncat -nuv 192.168.100.4 4000"
docker exec host2 ping -c1 192.168.100.4 docker exec host2 ncat -e '/usr/bin/echo helloagainfromhost2' -nkluv 0.0.0.0 4000 &
docker exec host4 ping -c1 192.168.100.2 docker exec host4 sh -c "/usr/bin/echo host4 | ncat -nuv 192.168.100.2 4000"
docker exec host4 sh -c 'kill 1' docker exec host4 sh -c 'kill 1'
docker exec host3 sh -c 'kill 1' docker exec host3 sh -c 'kill 1'
+15 -1
View File
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [1.10.3] - 2026-02-06
### Security
- Fix an issue where blocklist bypass is possible when using curve P256 since the signature can have 2 valid representations.
Both fingerprint representations will be tested against the blocklist.
Any newly issued P256 based certificates will have their signature clamped to the low-s form.
Nebula will assert the low-s signature form when validating certificates in a future version. [GHSA-69x3-g4r3-p962](https://github.com/slackhq/nebula/security/advisories/GHSA-69x3-g4r3-p962)
### Changed
- Improve error reporting if nebula fails to start due to a tun device naming issue. (#1588)
## [1.10.2] - 2026-01-21 ## [1.10.2] - 2026-01-21
### Fixed ### Fixed
@@ -775,7 +788,8 @@ created.)
- Initial public release. - Initial public release.
[Unreleased]: https://github.com/slackhq/nebula/compare/v1.10.2...HEAD [Unreleased]: https://github.com/slackhq/nebula/compare/v1.10.3...HEAD
[1.10.3]: https://github.com/slackhq/nebula/releases/tag/v1.10.3
[1.10.2]: https://github.com/slackhq/nebula/releases/tag/v1.10.2 [1.10.2]: https://github.com/slackhq/nebula/releases/tag/v1.10.2
[1.10.1]: https://github.com/slackhq/nebula/releases/tag/v1.10.1 [1.10.1]: https://github.com/slackhq/nebula/releases/tag/v1.10.1
[1.10.0]: https://github.com/slackhq/nebula/releases/tag/v1.10.0 [1.10.0]: https://github.com/slackhq/nebula/releases/tag/v1.10.0
-1
View File
@@ -1,5 +1,4 @@
//go:build boringcrypto //go:build boringcrypto
// +build boringcrypto
package nebula package nebula
+18
View File
@@ -141,10 +141,23 @@ func (ncp *CAPool) VerifyCertificate(now time.Time, c Certificate) (*CachedCerti
return nil, err return nil, err
} }
// Pre nebula v1.10.3 could generate signatures in either high or low s form and validation
// of signatures allowed for either. Nebula v1.10.3 and beyond clamps signature generation to low-s form
// but validation still allows for either. Since a change in the signature bytes affects the fingerprint, we
// need to test both forms until such a time comes that we enforce low-s form on signature validation.
fp2, err := CalculateAlternateFingerprint(c)
if err != nil {
return nil, fmt.Errorf("could not calculate alternate fingerprint to verify: %w", err)
}
if fp2 != "" && ncp.IsBlocklisted(fp2) {
return nil, ErrBlockListed
}
cc := CachedCertificate{ cc := CachedCertificate{
Certificate: c, Certificate: c,
InvertedGroups: make(map[string]struct{}), InvertedGroups: make(map[string]struct{}),
Fingerprint: fp, Fingerprint: fp,
fingerprint2: fp2,
signerFingerprint: signer.Fingerprint, signerFingerprint: signer.Fingerprint,
} }
@@ -158,6 +171,11 @@ func (ncp *CAPool) VerifyCertificate(now time.Time, c Certificate) (*CachedCerti
// VerifyCachedCertificate is the same as VerifyCertificate other than it operates on a pre-verified structure and // VerifyCachedCertificate is the same as VerifyCertificate other than it operates on a pre-verified structure and
// is a cheaper operation to perform as a result. // is a cheaper operation to perform as a result.
func (ncp *CAPool) VerifyCachedCertificate(now time.Time, c *CachedCertificate) error { func (ncp *CAPool) VerifyCachedCertificate(now time.Time, c *CachedCertificate) error {
// Check any available alternate fingerprint forms for this certificate, re P256 high-s/low-s
if c.fingerprint2 != "" && ncp.IsBlocklisted(c.fingerprint2) {
return ErrBlockListed
}
_, err := ncp.verify(c.Certificate, now, c.Fingerprint, c.signerFingerprint) _, err := ncp.verify(c.Certificate, now, c.Fingerprint, c.signerFingerprint)
return err return err
} }
+43 -4
View File
@@ -5,6 +5,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/slackhq/nebula/cert/p256"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -170,6 +171,15 @@ func TestCertificateV1_VerifyP256(t *testing.T) {
_, err = caPool.VerifyCertificate(time.Now(), c) _, err = caPool.VerifyCertificate(time.Now(), c)
require.EqualError(t, err, "certificate is in the block list") require.EqualError(t, err, "certificate is in the block list")
// Create a copy of the cert and swap to the alternate form for the signature
nc := c.Copy()
b, err := p256.Swap(c.Signature())
require.NoError(t, err)
require.NoError(t, nc.(*certificateV1).setSignature(b))
_, err = caPool.VerifyCertificate(time.Now(), nc)
require.EqualError(t, err, "certificate is in the block list")
caPool.ResetCertBlocklist() caPool.ResetCertBlocklist()
_, err = caPool.VerifyCertificate(time.Now(), c) _, err = caPool.VerifyCertificate(time.Now(), c)
require.NoError(t, err) require.NoError(t, err)
@@ -187,7 +197,7 @@ func TestCertificateV1_VerifyP256(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
caPool = NewCAPool() caPool = NewCAPool()
b, err := caPool.AddCAFromPEM(caPem) b, err = caPool.AddCAFromPEM(caPem)
require.NoError(t, err) require.NoError(t, err)
assert.Empty(t, b) assert.Empty(t, b)
@@ -196,7 +206,17 @@ func TestCertificateV1_VerifyP256(t *testing.T) {
}) })
c, _, _, _ = NewTestCert(Version1, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"}) c, _, _, _ = NewTestCert(Version1, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"})
_, err = caPool.VerifyCertificate(time.Now(), c) cc, err := caPool.VerifyCertificate(time.Now(), c)
require.NoError(t, err)
// Reset the blocklist and block the alternate form fingerprint
caPool.ResetCertBlocklist()
caPool.BlocklistFingerprint(cc.fingerprint2)
err = caPool.VerifyCachedCertificate(time.Now(), cc)
require.EqualError(t, err, "certificate is in the block list")
caPool.ResetCertBlocklist()
err = caPool.VerifyCachedCertificate(time.Now(), cc)
require.NoError(t, err) require.NoError(t, err)
} }
@@ -394,6 +414,15 @@ func TestCertificateV2_VerifyP256(t *testing.T) {
_, err = caPool.VerifyCertificate(time.Now(), c) _, err = caPool.VerifyCertificate(time.Now(), c)
require.EqualError(t, err, "certificate is in the block list") require.EqualError(t, err, "certificate is in the block list")
// Create a copy of the cert and swap to the alternate form for the signature
nc := c.Copy()
b, err := p256.Swap(c.Signature())
require.NoError(t, err)
require.NoError(t, nc.(*certificateV2).setSignature(b))
_, err = caPool.VerifyCertificate(time.Now(), nc)
require.EqualError(t, err, "certificate is in the block list")
caPool.ResetCertBlocklist() caPool.ResetCertBlocklist()
_, err = caPool.VerifyCertificate(time.Now(), c) _, err = caPool.VerifyCertificate(time.Now(), c)
require.NoError(t, err) require.NoError(t, err)
@@ -411,7 +440,7 @@ func TestCertificateV2_VerifyP256(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
caPool = NewCAPool() caPool = NewCAPool()
b, err := caPool.AddCAFromPEM(caPem) b, err = caPool.AddCAFromPEM(caPem)
require.NoError(t, err) require.NoError(t, err)
assert.Empty(t, b) assert.Empty(t, b)
@@ -420,7 +449,17 @@ func TestCertificateV2_VerifyP256(t *testing.T) {
}) })
c, _, _, _ = NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"}) c, _, _, _ = NewTestCert(Version2, Curve_P256, ca, caKey, "test", time.Now(), time.Now().Add(5*time.Minute), nil, nil, []string{"test1"})
_, err = caPool.VerifyCertificate(time.Now(), c) cc, err := caPool.VerifyCertificate(time.Now(), c)
require.NoError(t, err)
// Reset the blocklist and block the alternate form fingerprint
caPool.ResetCertBlocklist()
caPool.BlocklistFingerprint(cc.fingerprint2)
err = caPool.VerifyCachedCertificate(time.Now(), cc)
require.EqualError(t, err, "certificate is in the block list")
caPool.ResetCertBlocklist()
err = caPool.VerifyCachedCertificate(time.Now(), cc)
require.NoError(t, err) require.NoError(t, err)
} }
+33
View File
@@ -4,6 +4,8 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"time" "time"
"github.com/slackhq/nebula/cert/p256"
) )
type Version uint8 type Version uint8
@@ -110,6 +112,9 @@ type CachedCertificate struct {
InvertedGroups map[string]struct{} InvertedGroups map[string]struct{}
Fingerprint string Fingerprint string
signerFingerprint string signerFingerprint string
// A place to store a 2nd fingerprint if the certificate could have one, such as with P256
fingerprint2 string
} }
func (cc *CachedCertificate) String() string { func (cc *CachedCertificate) String() string {
@@ -152,3 +157,31 @@ func Recombine(v Version, rawCertBytes, publicKey []byte, curve Curve) (Certific
return c, nil return c, nil
} }
// CalculateAlternateFingerprint calculates a 2nd fingerprint representation for P256 certificates
// CAPool blocklist testing through `VerifyCertificate` and `VerifyCachedCertificate` automatically performs this step.
func CalculateAlternateFingerprint(c Certificate) (string, error) {
if c.Curve() != Curve_P256 {
return "", nil
}
nc := c.Copy()
b, err := p256.Swap(nc.Signature())
if err != nil {
return "", err
}
switch v := nc.(type) {
case *certificateV1:
err = v.setSignature(b)
case *certificateV2:
err = v.setSignature(b)
default:
return "", ErrUnknownVersion
}
if err != nil {
return "", err
}
return nc.Fingerprint()
}
+1 -5
View File
@@ -439,11 +439,7 @@ func (c *certificateV2) validate() error {
if !hasV6Networks { if !hasV6Networks {
return NewErrInvalidCertificateProperties("IPv6 unsafe networks require an IPv6 address assignment: %s", network) return NewErrInvalidCertificateProperties("IPv6 unsafe networks require an IPv6 address assignment: %s", network)
} }
} else if network.Addr().Is4() { } // as long as we have any IP address, IPv4 UnsafeNetworks are allowed
if !hasV4Networks {
return NewErrInvalidCertificateProperties("IPv4 unsafe networks require an IPv4 address assignment: %s", network)
}
}
} }
} }
+122
View File
@@ -0,0 +1,122 @@
package p256
import (
"crypto/elliptic"
"errors"
"math/big"
"filippo.io/bigmod"
"golang.org/x/crypto/cryptobyte"
"golang.org/x/crypto/cryptobyte/asn1"
)
var halfN = new(big.Int).Rsh(elliptic.P256().Params().N, 1)
var nMod *bigmod.Modulus
func init() {
n, err := bigmod.NewModulus(elliptic.P256().Params().N.Bytes())
if err != nil {
panic(err)
}
nMod = n
}
func IsNormalized(sig []byte) (bool, error) {
r, s, err := parseSignature(sig)
if err != nil {
return false, err
}
return checkLowS(r, s), nil
}
func checkLowS(_, s []byte) bool {
bigS := new(big.Int).SetBytes(s)
// Check if S <= (N/2), because we want to include the midpoint in the set of low-s
return bigS.Cmp(halfN) <= 0
}
func swap(r, s []byte) ([]byte, []byte, error) {
var err error
bigS, err := bigmod.NewNat().SetBytes(s, nMod)
if err != nil {
return nil, nil, err
}
sNormalized := nMod.Nat().Sub(bigS, nMod)
return r, sNormalized.Bytes(nMod), nil
}
func Normalize(sig []byte) ([]byte, error) {
r, s, err := parseSignature(sig)
if err != nil {
return nil, err
}
if checkLowS(r, s) {
return sig, nil
}
newR, newS, err := swap(r, s)
if err != nil {
return nil, err
}
return encodeSignature(newR, newS)
}
// Swap will change sig between its current form to the opposite high or low form.
func Swap(sig []byte) ([]byte, error) {
r, s, err := parseSignature(sig)
if err != nil {
return nil, err
}
newR, newS, err := swap(r, s)
if err != nil {
return nil, err
}
return encodeSignature(newR, newS)
}
// parseSignature taken exactly from crypto/ecdsa/ecdsa.go
func parseSignature(sig []byte) (r, s []byte, err error) {
var inner cryptobyte.String
input := cryptobyte.String(sig)
if !input.ReadASN1(&inner, asn1.SEQUENCE) ||
!input.Empty() ||
!inner.ReadASN1Integer(&r) ||
!inner.ReadASN1Integer(&s) ||
!inner.Empty() {
return nil, nil, errors.New("invalid ASN.1")
}
return r, s, nil
}
func encodeSignature(r, s []byte) ([]byte, error) {
var b cryptobyte.Builder
b.AddASN1(asn1.SEQUENCE, func(b *cryptobyte.Builder) {
addASN1IntBytes(b, r)
addASN1IntBytes(b, s)
})
return b.Bytes()
}
// addASN1IntBytes encodes in ASN.1 a positive integer represented as
// a big-endian byte slice with zero or more leading zeroes.
func addASN1IntBytes(b *cryptobyte.Builder, bytes []byte) {
for len(bytes) > 0 && bytes[0] == 0 {
bytes = bytes[1:]
}
if len(bytes) == 0 {
b.SetError(errors.New("invalid integer"))
return
}
b.AddASN1(asn1.INTEGER, func(c *cryptobyte.Builder) {
if bytes[0]&0x80 != 0 {
c.AddUint8(0)
}
c.AddBytes(bytes)
})
}
+28
View File
@@ -0,0 +1,28 @@
package p256
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"testing"
"github.com/stretchr/testify/require"
)
func TestFlipping(t *testing.T) {
priv, err1 := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err1)
out, err := ecdsa.SignASN1(rand.Reader, priv, []byte("big chungus"))
require.NoError(t, err)
r, s, err := parseSignature(out)
require.NoError(t, err)
r, s1, err := swap(r, s)
require.NoError(t, err)
r, s2, err := swap(r, s1)
require.NoError(t, err)
require.Equal(t, s, s2)
require.NotEqual(t, s, s1)
}
+9
View File
@@ -9,6 +9,8 @@ import (
"fmt" "fmt"
"net/netip" "net/netip"
"time" "time"
"github.com/slackhq/nebula/cert/p256"
) )
// TBSCertificate represents a certificate intended to be signed. // TBSCertificate represents a certificate intended to be signed.
@@ -126,6 +128,13 @@ func (t *TBSCertificate) SignWith(signer Certificate, curve Curve, sp SignerLamb
return nil, err return nil, err
} }
if curve == Curve_P256 {
sig, err = p256.Normalize(sig)
if err != nil {
return nil, err
}
}
err = c.setSignature(sig) err = c.setSignature(sig)
if err != nil { if err != nil {
return nil, err return nil, err
+46
View File
@@ -9,6 +9,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/slackhq/nebula/cert/p256"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -89,3 +90,48 @@ func TestCertificateV1_SignP256(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
assert.NotNil(t, uc) assert.NotNil(t, uc)
} }
func TestCertificate_SignP256_AlwaysNormalized(t *testing.T) {
before := time.Now().Add(time.Second * -60).Round(time.Second)
after := time.Now().Add(time.Second * 60).Round(time.Second)
pubKey := []byte("01234567890abcedfghij1234567890ab1234567890abcedfghij1234567890ab")
tbs := TBSCertificate{
Version: Version1,
Name: "testing",
Networks: []netip.Prefix{
mustParsePrefixUnmapped("10.1.1.1/24"),
mustParsePrefixUnmapped("10.1.1.2/16"),
},
UnsafeNetworks: []netip.Prefix{
mustParsePrefixUnmapped("9.1.1.2/24"),
mustParsePrefixUnmapped("9.1.1.3/16"),
},
Groups: []string{"test-group1", "test-group2", "test-group3"},
NotBefore: before,
NotAfter: after,
PublicKey: pubKey,
IsCA: true,
Curve: Curve_P256,
}
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
require.NoError(t, err)
pub := elliptic.Marshal(elliptic.P256(), priv.PublicKey.X, priv.PublicKey.Y)
rawPriv := priv.D.FillBytes(make([]byte, 32))
for i := 0; i < 1000; i++ {
if i&1 == 1 {
tbs.Version = Version1
} else {
tbs.Version = Version2
}
c, err := tbs.Sign(nil, Curve_P256, rawPriv)
require.NoError(t, err)
assert.NotNil(t, c)
assert.True(t, c.CheckSignature(pub))
normie, err := p256.IsNormalized(c.Signature())
require.NoError(t, err)
assert.True(t, normie)
}
}
-4
View File
@@ -1,4 +0,0 @@
package main
const NoSuchFileError = "no such file or directory"
const NoSuchDirError = "no such file or directory"
-4
View File
@@ -1,4 +0,0 @@
package main
const NoSuchFileError = "no such file or directory"
const NoSuchDirError = "no such file or directory"
+4 -5
View File
@@ -11,7 +11,6 @@ import (
"github.com/slackhq/nebula/cert" "github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/config" "github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/test" "github.com/slackhq/nebula/test"
"github.com/slackhq/nebula/test/device"
"github.com/slackhq/nebula/udp" "github.com/slackhq/nebula/udp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -53,7 +52,7 @@ func Test_NewConnectionManagerTest(t *testing.T) {
lh := newTestLighthouse() lh := newTestLighthouse()
ifce := &Interface{ ifce := &Interface{
hostMap: hostMap, hostMap: hostMap,
inside: &device.NoopTun{}, inside: &test.NoopTun{},
outside: &udp.NoopConn{}, outside: &udp.NoopConn{},
firewall: &Firewall{}, firewall: &Firewall{},
lightHouse: lh, lightHouse: lh,
@@ -136,7 +135,7 @@ func Test_NewConnectionManagerTest2(t *testing.T) {
lh := newTestLighthouse() lh := newTestLighthouse()
ifce := &Interface{ ifce := &Interface{
hostMap: hostMap, hostMap: hostMap,
inside: &device.NoopTun{}, inside: &test.NoopTun{},
outside: &udp.NoopConn{}, outside: &udp.NoopConn{},
firewall: &Firewall{}, firewall: &Firewall{},
lightHouse: lh, lightHouse: lh,
@@ -221,7 +220,7 @@ func Test_NewConnectionManager_DisconnectInactive(t *testing.T) {
lh := newTestLighthouse() lh := newTestLighthouse()
ifce := &Interface{ ifce := &Interface{
hostMap: hostMap, hostMap: hostMap,
inside: &device.NoopTun{}, inside: &test.NoopTun{},
outside: &udp.NoopConn{}, outside: &udp.NoopConn{},
firewall: &Firewall{}, firewall: &Firewall{},
lightHouse: lh, lightHouse: lh,
@@ -348,7 +347,7 @@ func Test_NewConnectionManagerTest_DisconnectInvalid(t *testing.T) {
lh := newTestLighthouse() lh := newTestLighthouse()
ifce := &Interface{ ifce := &Interface{
hostMap: hostMap, hostMap: hostMap,
inside: &device.NoopTun{}, inside: &test.NoopTun{},
outside: &udp.NoopConn{}, outside: &udp.NoopConn{},
firewall: &Firewall{}, firewall: &Firewall{},
lightHouse: lh, lightHouse: lh,
-1
View File
@@ -1,5 +1,4 @@
//go:build e2e_testing //go:build e2e_testing
// +build e2e_testing
package nebula package nebula
+400
View File
@@ -0,0 +1,400 @@
//go:build e2e_testing
// +build e2e_testing
package e2e
import (
"encoding/binary"
"net/netip"
"testing"
"time"
"github.com/slackhq/nebula/cert"
"github.com/slackhq/nebula/cert_test"
"github.com/slackhq/nebula/e2e/router"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// parseIPv4UDPPacket extracts source/dest IPs, ports, and payload from an IPv4 UDP packet.
func parseIPv4UDPPacket(t testing.TB, pkt []byte) (srcIP, dstIP netip.Addr, srcPort, dstPort uint16, payload []byte) {
t.Helper()
require.True(t, len(pkt) >= 28, "packet too short for IPv4+UDP header")
require.Equal(t, byte(0x45), pkt[0]&0xF0|pkt[0]&0x0F, "not a simple IPv4 packet (IHL!=5)")
srcIP, _ = netip.AddrFromSlice(pkt[12:16])
dstIP, _ = netip.AddrFromSlice(pkt[16:20])
ihl := int(pkt[0]&0x0F) * 4
require.True(t, len(pkt) >= ihl+8, "packet too short for UDP header")
srcPort = binary.BigEndian.Uint16(pkt[ihl : ihl+2])
dstPort = binary.BigEndian.Uint16(pkt[ihl+2 : ihl+4])
udpLen := binary.BigEndian.Uint16(pkt[ihl+4 : ihl+6])
payload = pkt[ihl+8 : ihl+int(udpLen)]
return
}
func TestSNAT_IPv6OnlyPeer_IPv4UnsafeTraffic(t *testing.T) {
// Scenario: Two IPv6-only VPN nodes. The "router" node has unsafe networks
// (192.168.0.0/16) in its cert and a configured SNAT address. The "sender"
// node has an unsafe route for 192.168.0.0/16 via the router.
//
// When sender injects an IPv4 packet destined for the unsafe network, it
// gets tunneled to the router. The router's firewall detects this is IPv4
// from an IPv6-only peer and applies SNAT, rewriting the source IP to the
// SNAT address before delivering it to TUN.
//
// When a reply comes back from TUN addressed to the SNAT address, the
// router un-SNATs it (restoring the original destination) and tunnels it
// back to the sender.
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
unsafePrefix := "192.168.0.0/16"
snatAddr := netip.MustParseAddr("169.254.42.42")
// Router: IPv6-only with unsafe networks and a manual SNAT address.
// Override inbound firewall with local_cidr: "any" so both IPv4 (unsafe)
// and IPv6 (VPN) traffic is accepted.
routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
cert.Version2, ca, caKey, "router", "ff::1/64",
netip.MustParseAddrPort("[beef::1]:4242"),
unsafePrefix,
m{
"firewall": m{
"inbound": []m{{
"proto": "any",
"port": "any",
"host": "any",
"local_cidr": "any",
}},
},
"tun": m{
"snat_address_for_4over6": snatAddr.String(),
},
},
)
// Sender: IPv6-only with an unsafe route via the router
senderControl, _, _, _ := newSimpleServerWithUdp(
cert.Version2, ca, caKey, "sender", "ff::2/64",
netip.MustParseAddrPort("[beef::2]:4242"),
m{
"tun": m{
"unsafe_routes": []m{
{"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
},
},
},
)
// Tell sender where the router lives
senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
// Build the router and start both nodes
r := router.NewR(t, routerControl, senderControl)
defer r.RenderFlow()
routerControl.Start()
senderControl.Start()
// --- Outbound: sender -> IPv4 unsafe dest (via router with SNAT) ---
origSrcIP := netip.MustParseAddr("10.0.0.1")
unsafeDest := netip.MustParseAddr("192.168.1.1")
var origSrcPort uint16 = 12345
var dstPort uint16 = 80
t.Log("Sender injects an IPv4 packet to the unsafe network")
senderControl.InjectTunUDPPacket(unsafeDest, dstPort, origSrcIP, origSrcPort, []byte("snat me"))
t.Log("Route packets (handshake + data) until the router gets the packet on TUN")
snatPkt := r.RouteForAllUntilTxTun(routerControl)
t.Log("Verify the packet was SNATted")
gotSrcIP, gotDstIP, gotSrcPort, gotDstPort, gotPayload := parseIPv4UDPPacket(t, snatPkt)
assert.Equal(t, snatAddr, gotSrcIP, "source IP should be rewritten to the SNAT address")
assert.Equal(t, unsafeDest, gotDstIP, "destination IP should be unchanged")
assert.Equal(t, dstPort, gotDstPort, "destination port should be unchanged")
assert.Equal(t, []byte("snat me"), gotPayload, "payload should be unchanged")
// Capture the SNAT port (may differ from original if port was remapped)
snatPort := gotSrcPort
t.Logf("SNAT port: %d (original: %d)", snatPort, origSrcPort)
// --- Return: reply from unsafe dest -> un-SNATted back to sender ---
t.Log("Router injects a reply packet from the unsafe dest to the SNAT address")
routerControl.InjectTunUDPPacket(snatAddr, snatPort, unsafeDest, dstPort, []byte("reply from unsafe"))
t.Log("Route until sender gets the reply on TUN")
replyPkt := r.RouteForAllUntilTxTun(senderControl)
t.Log("Verify the reply was un-SNATted")
replySrcIP, replyDstIP, replySrcPort, replyDstPort, replyPayload := parseIPv4UDPPacket(t, replyPkt)
assert.Equal(t, unsafeDest, replySrcIP, "reply source should be the unsafe dest")
assert.Equal(t, origSrcIP, replyDstIP, "reply dest should be the original source IP (un-SNATted)")
assert.Equal(t, dstPort, replySrcPort, "reply source port should be the unsafe dest port")
assert.Equal(t, origSrcPort, replyDstPort, "reply dest port should be the original source port (un-SNATted)")
assert.Equal(t, []byte("reply from unsafe"), replyPayload, "payload should be unchanged")
r.RenderHostmaps("Final hostmaps", routerControl, senderControl)
// Also verify normal IPv6 VPN traffic still works between the nodes
t.Log("Verify normal IPv6 VPN tunnel still works")
assertTunnel(t, routerVpnIpNet[0].Addr(), senderControl.GetVpnAddrs()[0], routerControl, senderControl, r)
routerControl.Stop()
senderControl.Stop()
}
func TestSNAT_MultipleFlows(t *testing.T) {
// Test that multiple distinct IPv4 flows from the same IPv6-only peer
// are tracked independently through SNAT.
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
unsafePrefix := "192.168.0.0/16"
snatAddr := netip.MustParseAddr("169.254.42.42")
routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
cert.Version2, ca, caKey, "router", "ff::1/64",
netip.MustParseAddrPort("[beef::1]:4242"),
unsafePrefix,
m{
"firewall": m{
"inbound": []m{{
"proto": "any",
"port": "any",
"host": "any",
"local_cidr": "any",
}},
},
"tun": m{
"snat_address_for_4over6": snatAddr.String(),
},
},
)
senderControl, _, _, _ := newSimpleServerWithUdp(
cert.Version2, ca, caKey, "sender", "ff::2/64",
netip.MustParseAddrPort("[beef::2]:4242"),
m{
"tun": m{
"unsafe_routes": []m{
{"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
},
},
},
)
senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
r := router.NewR(t, routerControl, senderControl)
defer r.RenderFlow()
r.CancelFlowLogs()
routerControl.Start()
senderControl.Start()
unsafeDest := netip.MustParseAddr("192.168.1.1")
// Send first flow
senderControl.InjectTunUDPPacket(unsafeDest, 80, netip.MustParseAddr("10.0.0.1"), 1111, []byte("flow1"))
pkt1 := r.RouteForAllUntilTxTun(routerControl)
srcIP1, _, srcPort1, _, payload1 := parseIPv4UDPPacket(t, pkt1)
assert.Equal(t, snatAddr, srcIP1)
assert.Equal(t, []byte("flow1"), payload1)
// Send second flow (different source port)
senderControl.InjectTunUDPPacket(unsafeDest, 80, netip.MustParseAddr("10.0.0.1"), 2222, []byte("flow2"))
pkt2 := r.RouteForAllUntilTxTun(routerControl)
srcIP2, _, srcPort2, _, payload2 := parseIPv4UDPPacket(t, pkt2)
assert.Equal(t, snatAddr, srcIP2)
assert.Equal(t, []byte("flow2"), payload2)
// The two flows should have different SNAT ports (since they're different conntracks)
t.Logf("Flow 1 SNAT port: %d, Flow 2 SNAT port: %d", srcPort1, srcPort2)
// Reply to flow 2 first (out of order)
routerControl.InjectTunUDPPacket(snatAddr, srcPort2, unsafeDest, 80, []byte("reply2"))
reply2 := r.RouteForAllUntilTxTun(senderControl)
_, replyDst2, _, replyDstPort2, replyPayload2 := parseIPv4UDPPacket(t, reply2)
assert.Equal(t, netip.MustParseAddr("10.0.0.1"), replyDst2)
assert.Equal(t, uint16(2222), replyDstPort2, "reply to flow 2 should restore original port 2222")
assert.Equal(t, []byte("reply2"), replyPayload2)
// Reply to flow 1
routerControl.InjectTunUDPPacket(snatAddr, srcPort1, unsafeDest, 80, []byte("reply1"))
reply1 := r.RouteForAllUntilTxTun(senderControl)
_, replyDst1, _, replyDstPort1, replyPayload1 := parseIPv4UDPPacket(t, reply1)
assert.Equal(t, netip.MustParseAddr("10.0.0.1"), replyDst1)
assert.Equal(t, uint16(1111), replyDstPort1, "reply to flow 1 should restore original port 1111")
assert.Equal(t, []byte("reply1"), replyPayload1)
routerControl.Stop()
senderControl.Stop()
}
// --- Adversarial SNAT E2E Tests ---
func TestSNAT_UnsolicitedReplyDropped(t *testing.T) {
// Without any outbound SNAT traffic, inject a packet from the router's TUN
// addressed to the SNAT address. The sender must never receive it because
// there's no conntrack entry to un-SNAT through.
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
unsafePrefix := "192.168.0.0/16"
snatAddr := netip.MustParseAddr("169.254.42.42")
routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
cert.Version2, ca, caKey, "router", "ff::1/64",
netip.MustParseAddrPort("[beef::1]:4242"),
unsafePrefix,
m{
"firewall": m{
"inbound": []m{{
"proto": "any",
"port": "any",
"host": "any",
"local_cidr": "any",
}},
},
"tun": m{
"snat_address_for_4over6": snatAddr.String(),
},
},
)
senderControl, _, _, _ := newSimpleServerWithUdp(
cert.Version2, ca, caKey, "sender", "ff::2/64",
netip.MustParseAddrPort("[beef::2]:4242"),
m{
"tun": m{
"unsafe_routes": []m{
{"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
},
},
},
)
senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
r := router.NewR(t, routerControl, senderControl)
defer r.RenderFlow()
r.CancelFlowLogs()
routerControl.Start()
senderControl.Start()
// First establish the tunnel with normal IPv6 traffic so handshake completes
assertTunnel(t, routerVpnIpNet[0].Addr(), senderControl.GetVpnAddrs()[0], routerControl, senderControl, r)
// Inject the unsolicited reply from router's TUN to the SNAT address.
// There is NO prior outbound SNAT flow, so no conntrack entry exists.
// The router should silently drop this because unSnat finds no matching conntrack.
routerControl.InjectTunUDPPacket(snatAddr, 55555, netip.MustParseAddr("192.168.1.1"), 80, []byte("unsolicited"))
// Send a canary IPv6 VPN packet after the bad one. Since the router processes
// TUN packets sequentially, the canary arriving proves the bad packet was processed first.
senderVpnAddr := senderControl.GetVpnAddrs()[0]
routerControl.InjectTunUDPPacket(senderVpnAddr, 90, routerVpnIpNet[0].Addr(), 80, []byte("canary"))
canaryPkt := r.RouteForAllUntilTxTun(senderControl)
assertUdpPacket(t, []byte("canary"), canaryPkt, routerVpnIpNet[0].Addr(), senderVpnAddr, 80, 90)
// The unsolicited packet should have been dropped — nothing else on sender's TUN
got := senderControl.GetFromTun(false)
assert.Nil(t, got, "sender should not receive unsolicited packet to SNAT address with no conntrack entry")
routerControl.Stop()
senderControl.Stop()
}
func TestSNAT_NonUnsafeDestDropped(t *testing.T) {
// An IPv6-only sender sends IPv4 traffic to a destination outside the router's
// unsafe networks (172.16.0.1 when unsafe is 192.168.0.0/16). The router should
// reject this because the local address is not routable. This verifies that
// willingToHandleLocalAddr enforces boundaries on what SNAT traffic can reach.
ca, _, caKey, _ := cert_test.NewTestCaCert(cert.Version2, cert.Curve_CURVE25519, time.Now(), time.Now().Add(10*time.Minute), nil, nil, []string{})
unsafePrefix := "192.168.0.0/16"
snatAddr := netip.MustParseAddr("169.254.42.42")
routerControl, routerVpnIpNet, routerUdpAddr, _ := newSimpleServerWithUdpAndUnsafeNetworks(
cert.Version2, ca, caKey, "router", "ff::1/64",
netip.MustParseAddrPort("[beef::1]:4242"),
unsafePrefix,
m{
"firewall": m{
"inbound": []m{{
"proto": "any",
"port": "any",
"host": "any",
"local_cidr": "any",
}},
},
"tun": m{
"snat_address_for_4over6": snatAddr.String(),
},
},
)
// Sender has unsafe routes for BOTH 192.168.0.0/16 AND 172.16.0.0/12 via router.
// This means the sender will route 172.16.0.1 through the tunnel to the router.
// But the router should reject it because 172.16.0.0/12 is NOT in its unsafe networks.
senderControl, _, _, _ := newSimpleServerWithUdp(
cert.Version2, ca, caKey, "sender", "ff::2/64",
netip.MustParseAddrPort("[beef::2]:4242"),
m{
"tun": m{
"unsafe_routes": []m{
{"route": unsafePrefix, "via": routerVpnIpNet[0].Addr().String()},
{"route": "172.16.0.0/12", "via": routerVpnIpNet[0].Addr().String()},
},
},
},
)
senderControl.InjectLightHouseAddr(routerVpnIpNet[0].Addr(), routerUdpAddr)
r := router.NewR(t, routerControl, senderControl)
defer r.RenderFlow()
r.CancelFlowLogs()
routerControl.Start()
senderControl.Start()
// Establish the tunnel first
assertTunnel(t, routerVpnIpNet[0].Addr(), senderControl.GetVpnAddrs()[0], routerControl, senderControl, r)
// Send to 172.16.0.1 (NOT in router's unsafe networks 192.168.0.0/16).
// The router should reject this at willingToHandleLocalAddr.
senderControl.InjectTunUDPPacket(
netip.MustParseAddr("172.16.0.1"), 80,
netip.MustParseAddr("10.0.0.1"), 12345,
[]byte("wrong dest"),
)
// Send a canary to a valid unsafe destination to prove the bad packet was processed
senderControl.InjectTunUDPPacket(
netip.MustParseAddr("192.168.1.1"), 80,
netip.MustParseAddr("10.0.0.1"), 33333,
[]byte("canary"),
)
// Route until the canary arrives — the 172.16.0.1 packet should have been
// processed and dropped before the canary gets through
canaryPkt := r.RouteForAllUntilTxTun(routerControl)
_, canaryDst, _, _, canaryPayload := parseIPv4UDPPacket(t, canaryPkt)
assert.Equal(t, netip.MustParseAddr("192.168.1.1"), canaryDst, "canary should arrive at the valid unsafe dest")
assert.Equal(t, []byte("canary"), canaryPayload)
// No more packets — the 172.16.0.1 packet was dropped
got := routerControl.GetFromTun(false)
assert.Nil(t, got, "packet to non-unsafe destination 172.16.0.1 should be dropped by the router")
routerControl.Stop()
senderControl.Stop()
}
+10 -1
View File
@@ -283,6 +283,15 @@ tun:
# If using massive routes updates, for example BGP, you may need to increase this value to avoid packet loss. # If using massive routes updates, for example BGP, you may need to increase this value to avoid packet loss.
# SO_RCVBUFFORCE is used to avoid having to raise the system wide max # SO_RCVBUFFORCE is used to avoid having to raise the system wide max
#use_system_route_table_buffer_size: 0 #use_system_route_table_buffer_size: 0
#
# When using the feature to carry IPv4 unsafe_routes through a IPv6-only overlay network (4over6),
# the system still needs to use IPv4 addresses to pass that traffic.
# By default, they are randomly chosen out of 169.254.0.0/16 at startup, but if you need them to be static, you can set them here.
# Setting these options is not required for use of the 4over6 feature.
# this is the address that a client will use as a source IP for packets sent to the router
#unsafe_origin_address_for_4over6: 169.254.55.96
# this is the IP the router will use internally to hairpin-NAT 4over6 traffic before it hits the operating system.
#snat_address_for_4over6: 169.254.12.34
# Configure logging level # Configure logging level
logging: logging:
@@ -382,8 +391,8 @@ firewall:
# Rules are comprised of a protocol, port, and one or more of host, group, or CIDR # Rules are comprised of a protocol, port, and one or more of host, group, or CIDR
# Logical evaluation is roughly: port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND (local cidr) # Logical evaluation is roughly: port AND proto AND (ca_sha OR ca_name) AND (host OR group OR groups OR cidr) AND (local cidr)
# - port: Takes `0` or `any` as any, a single number `80`, a range `200-901`, or `fragment` to match second and further fragments of fragmented packets (since there is no port available). # - port: Takes `0` or `any` as any, a single number `80`, a range `200-901`, or `fragment` to match second and further fragments of fragmented packets (since there is no port available).
# code: same as port but makes more sense when talking about ICMP, TODO: this is not currently implemented in a way that works, use `any`
# proto: `any`, `tcp`, `udp`, or `icmp` # proto: `any`, `tcp`, `udp`, or `icmp`
# a port specification is ignored if proto is `icmp`
# host: `any` or a literal hostname, ie `test-host` # host: `any` or a literal hostname, ie `test-host`
# group: `any` or a literal group name, ie `default-group` # group: `any` or a literal group name, ie `default-group`
# groups: Same as group but accepts a list of values. Multiple values are AND'd together and a certificate would have to contain all groups to pass # groups: Same as group but accepts a list of values. Multiple values are AND'd together and a certificate would have to contain all groups to pass
+350 -126
View File
@@ -2,6 +2,7 @@ package nebula
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/binary"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
@@ -22,10 +23,35 @@ import (
"github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall"
) )
var ErrCannotSNAT = errors.New("cannot SNAT this packet")
var ErrSNATIdentityMismatch = errors.New("refusing to SNAT for mismatched host")
var ErrSNATAddressCollision = errors.New("refusing to accept an incoming packet with my SNAT address")
const ipv4SourcePosition = 12
const ipv4DestinationPosition = 16
const sourcePortOffset = 0
const destinationPortOffset = 2
type FirewallInterface interface { type FirewallInterface interface {
AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr string, caName string, caSha string) error AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr string, caName string, caSha string) error
} }
type snatInfo struct {
//Src is the source IP+port to write into unsafe-route-bound packet
Src netip.AddrPort
//SrcVpnIp is the overlay IP associated with this flow. It's needed to associate reply traffic so we can get it back to the right host.
SrcVpnIp netip.Addr
//SnatPort is the port to rewrite into an overlay-bound packet
SnatPort uint16
}
func (s *snatInfo) Valid() bool {
if s == nil {
return false
}
return s.Src.IsValid()
}
type conn struct { type conn struct {
Expires time.Time // Time when this conntrack entry will expire Expires time.Time // Time when this conntrack entry will expire
@@ -34,6 +60,9 @@ type conn struct {
// fields pack for free after the uint32 above // fields pack for free after the uint32 above
incoming bool incoming bool
rulesVersion uint16 rulesVersion uint16
//for SNAT support
snat *snatInfo
} }
// TODO: need conntrack max tracked connections handling // TODO: need conntrack max tracked connections handling
@@ -66,6 +95,8 @@ type Firewall struct {
defaultLocalCIDRAny bool defaultLocalCIDRAny bool
incomingMetrics firewallMetrics incomingMetrics firewallMetrics
outgoingMetrics firewallMetrics outgoingMetrics firewallMetrics
unsafeIPv4Origin netip.Addr
snatAddr netip.Addr
l *logrus.Logger l *logrus.Logger
} }
@@ -81,14 +112,8 @@ type FirewallConntrack struct {
Conns map[firewall.Packet]*conn Conns map[firewall.Packet]*conn
TimerWheel *TimerWheel[firewall.Packet] TimerWheel *TimerWheel[firewall.Packet]
// purgeCounter tracks lookups to trigger periodic purge instead of every lookup
purgeCounter uint32
} }
// purgeInterval defines how many lookups between purge attempts
const conntrackPurgeInterval = 1024
// FirewallTable is the entry point for a rule, the evaluation order is: // FirewallTable is the entry point for a rule, the evaluation order is:
// Proto AND port AND (CA SHA or CA name) AND local CIDR AND (group OR groups OR name OR remote CIDR) // Proto AND port AND (CA SHA or CA name) AND local CIDR AND (group OR groups OR name OR remote CIDR)
type FirewallTable struct { type FirewallTable struct {
@@ -199,12 +224,12 @@ func NewFirewall(l *logrus.Logger, tcpTimeout, UDPTimeout, defaultTimeout time.D
func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firewall, error) { func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firewall, error) {
certificate := cs.getCertificate(cert.Version2) certificate := cs.getCertificate(cert.Version2)
if certificate == nil { if certificate == nil { //todo if config.initiating_version is set to 1, and unsafe_networks differ, things will suck
certificate = cs.getCertificate(cert.Version1) certificate = cs.getCertificate(cert.Version1)
} }
if certificate == nil { if certificate == nil {
panic("No certificate available to reconfigure the firewall") return nil, errors.New("no certificate available to reconfigure the firewall")
} }
fw := NewFirewall( fw := NewFirewall(
@@ -213,7 +238,6 @@ func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firew
c.GetDuration("firewall.conntrack.udp_timeout", time.Minute*3), c.GetDuration("firewall.conntrack.udp_timeout", time.Minute*3),
c.GetDuration("firewall.conntrack.default_timeout", time.Minute*10), c.GetDuration("firewall.conntrack.default_timeout", time.Minute*10),
certificate, certificate,
//TODO: max_connections
) )
fw.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", false) fw.defaultLocalCIDRAny = c.GetBool("firewall.default_local_cidr_any", false)
@@ -255,20 +279,6 @@ func NewFirewallFromConfig(l *logrus.Logger, cs *CertState, c *config.C) (*Firew
// AddRule properly creates the in memory rule structure for a firewall table. // AddRule properly creates the in memory rule structure for a firewall table.
func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error { func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort int32, groups []string, host string, cidr, localCidr, caName string, caSha string) error {
// We need this rule string because we generate a hash. Removing this will break firewall reload.
ruleString := fmt.Sprintf(
"incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s",
incoming, proto, startPort, endPort, groups, host, cidr, localCidr, caName, caSha,
)
f.rules += ruleString + "\n"
direction := "incoming"
if !incoming {
direction = "outgoing"
}
f.l.WithField("firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha}).
Info("Firewall rule added")
var ( var (
ft *FirewallTable ft *FirewallTable
fp firewallPort fp firewallPort
@@ -286,6 +296,12 @@ func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort
case firewall.ProtoUDP: case firewall.ProtoUDP:
fp = ft.UDP fp = ft.UDP
case firewall.ProtoICMP, firewall.ProtoICMPv6: case firewall.ProtoICMP, firewall.ProtoICMPv6:
//ICMP traffic doesn't have ports, so we always coerce to "any", even if a value is provided
if startPort != firewall.PortAny {
f.l.WithField("startPort", startPort).Warn("ignoring port specification for ICMP firewall rule")
}
startPort = firewall.PortAny
endPort = firewall.PortAny
fp = ft.ICMP fp = ft.ICMP
case firewall.ProtoAny: case firewall.ProtoAny:
fp = ft.AnyProto fp = ft.AnyProto
@@ -293,6 +309,20 @@ func (f *Firewall) AddRule(incoming bool, proto uint8, startPort int32, endPort
return fmt.Errorf("unknown protocol %v", proto) return fmt.Errorf("unknown protocol %v", proto)
} }
// We need this rule string because we generate a hash. Removing this will break firewall reload.
ruleString := fmt.Sprintf(
"incoming: %v, proto: %v, startPort: %v, endPort: %v, groups: %v, host: %v, ip: %v, localIp: %v, caName: %v, caSha: %s",
incoming, proto, startPort, endPort, groups, host, cidr, localCidr, caName, caSha,
)
f.rules += ruleString + "\n"
direction := "incoming"
if !incoming {
direction = "outgoing"
}
f.l.WithField("firewallRule", m{"direction": direction, "proto": proto, "startPort": startPort, "endPort": endPort, "groups": groups, "host": host, "cidr": cidr, "localCidr": localCidr, "caName": caName, "caSha": caSha}).
Info("Firewall rule added")
return fp.addRule(f, startPort, endPort, groups, host, cidr, localCidr, caName, caSha) return fp.addRule(f, startPort, endPort, groups, host, cidr, localCidr, caName, caSha)
} }
@@ -314,6 +344,18 @@ func (f *Firewall) GetRuleHashes() string {
return "SHA:" + f.GetRuleHash() + ",FNV:" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10) return "SHA:" + f.GetRuleHash() + ",FNV:" + strconv.FormatUint(uint64(f.GetRuleHashFNV()), 10)
} }
func (f *Firewall) SetSNATAddressFromInterface(i *Interface) {
//address-mutation-avoidance is done inside Interface, the firewall doesn't need to care
//todo should snatted conntracks get expired out? Probably not needed until if/when we allow reload
f.snatAddr = i.inside.SNATAddress().Addr()
f.unsafeIPv4Origin = i.inside.UnsafeIPv4OriginAddress().Addr()
}
func (f *Firewall) ShouldUnSNAT(fp *firewall.Packet) bool {
// f.snatAddr is only valid if we're a snat-capable router
return f.snatAddr.IsValid() && fp.RemoteAddr == f.snatAddr
}
func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw FirewallInterface) error { func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw FirewallInterface) error {
var table string var table string
if inbound { if inbound {
@@ -355,24 +397,31 @@ func AddFirewallRulesFromConfig(l *logrus.Logger, inbound bool, c *config.C, fw
sPort = r.Port sPort = r.Port
} }
startPort, endPort, err := parsePort(sPort)
if err != nil {
return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
}
var proto uint8 var proto uint8
var startPort, endPort int32
switch r.Proto { switch r.Proto {
case "any": case "any":
proto = firewall.ProtoAny proto = firewall.ProtoAny
startPort, endPort, err = parsePort(sPort)
case "tcp": case "tcp":
proto = firewall.ProtoTCP proto = firewall.ProtoTCP
startPort, endPort, err = parsePort(sPort)
case "udp": case "udp":
proto = firewall.ProtoUDP proto = firewall.ProtoUDP
startPort, endPort, err = parsePort(sPort)
case "icmp": case "icmp":
proto = firewall.ProtoICMP proto = firewall.ProtoICMP
startPort = firewall.PortAny
endPort = firewall.PortAny
if sPort != "" {
l.WithField("port", sPort).Warn("ignoring port specification for ICMP firewall rule")
}
default: default:
return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto) return fmt.Errorf("%s rule #%v; proto was not understood; `%s`", table, i, r.Proto)
} }
if err != nil {
return fmt.Errorf("%s rule #%v; %s %s", table, i, errPort, err)
}
if r.Cidr != "" && r.Cidr != "any" { if r.Cidr != "" && r.Cidr != "any" {
_, err = netip.ParsePrefix(r.Cidr) _, err = netip.ParsePrefix(r.Cidr)
@@ -407,50 +456,207 @@ var ErrInvalidRemoteIP = errors.New("remote address is not in remote certificate
var ErrInvalidLocalIP = errors.New("local address is not in list of handled local addresses") var ErrInvalidLocalIP = errors.New("local address is not in list of handled local addresses")
var ErrNoMatchingRule = errors.New("no matching rule in firewall table") var ErrNoMatchingRule = errors.New("no matching rule in firewall table")
func (f *Firewall) unSnat(data []byte, fp *firewall.Packet) netip.Addr {
c := f.peek(*fp) //unfortunately this needs to lock. Surely there's a better way.
if c == nil {
return netip.Addr{}
}
if !c.snat.Valid() {
return netip.Addr{}
}
oldIP := netip.AddrPortFrom(f.snatAddr, fp.RemotePort)
rewritePacket(data, fp, oldIP, c.snat.Src, ipv4DestinationPosition, destinationPortOffset)
return c.snat.SrcVpnIp
}
func rewritePacket(data []byte, fp *firewall.Packet, oldIP netip.AddrPort, newIP netip.AddrPort, ipOffset int, portOffset int) {
//change address
copy(data[ipOffset:], newIP.Addr().AsSlice())
recalcIPv4Checksum(data, oldIP.Addr(), newIP.Addr())
ipHeaderLen := int(data[0]&0x0F) * 4
switch fp.Protocol {
case firewall.ProtoICMP:
binary.BigEndian.PutUint16(data[ipHeaderLen+4:ipHeaderLen+6], newIP.Port()) //we use the ID field as a "port" for ICMP
icmpCode := uint16(data[ipHeaderLen+1]) //todo not snatting on code yet (but Linux would)
recalcICMPv4Checksum(data, icmpCode, icmpCode, oldIP.Port(), newIP.Port())
case firewall.ProtoUDP:
dstport := ipHeaderLen + portOffset
binary.BigEndian.PutUint16(data[dstport:dstport+2], newIP.Port())
recalcUDPv4Checksum(data, oldIP, newIP)
case firewall.ProtoTCP:
dstport := ipHeaderLen + portOffset
binary.BigEndian.PutUint16(data[dstport:dstport+2], newIP.Port())
recalcTCPv4Checksum(data, oldIP, newIP)
}
}
func (f *Firewall) findUsableSNATPort(fp *firewall.Packet, c *conn) error {
const halfThePorts = 0x7fff
oldPort := fp.RemotePort
conntrack := f.Conntrack
conntrack.Lock()
defer conntrack.Unlock()
for numPortsChecked := 0; numPortsChecked < halfThePorts; numPortsChecked++ {
_, ok := conntrack.Conns[*fp]
if !ok {
//yay, we can use this port
//track the snatted flow with the same expiration as the unsnatted version
c.snat.SnatPort = fp.RemotePort
conntrack.Conns[*fp] = c
return nil
}
//increment and retry. There's probably better strategies out there
fp.RemotePort++
if fp.RemotePort < halfThePorts {
fp.RemotePort += halfThePorts // keep it ephemeral for now
}
}
//if we made it here, we failed
fp.RemotePort = oldPort
return ErrCannotSNAT
}
func (f *Firewall) applySnat(data []byte, fp *firewall.Packet, c *conn, hostinfo *HostInfo) error {
if !f.snatAddr.IsValid() {
return ErrCannotSNAT
}
if f.snatAddr == fp.LocalAddr { //a packet that came from UDP (incoming) should never ever have our snat address on it
return ErrSNATAddressCollision
}
if c.snat.Valid() {
//old flow: make sure it came from the right place
if !slices.Contains(hostinfo.vpnAddrs, c.snat.SrcVpnIp) {
return ErrSNATIdentityMismatch
}
fp.RemoteAddr = f.snatAddr
fp.RemotePort = c.snat.SnatPort
} else if hostinfo.vpnAddrs[0].Is6() {
//we got a new flow
c.snat = &snatInfo{
Src: netip.AddrPortFrom(fp.RemoteAddr, fp.RemotePort),
SrcVpnIp: hostinfo.vpnAddrs[0],
}
fp.RemoteAddr = f.snatAddr
//find a new port to use, if needed
err := f.findUsableSNATPort(fp, c)
if err != nil {
c.snat = nil
return err
}
} else {
return ErrCannotSNAT
}
newIP := netip.AddrPortFrom(f.snatAddr, c.snat.SnatPort)
rewritePacket(data, fp, c.snat.Src, newIP, ipv4SourcePosition, sourcePortOffset)
return nil
}
func (f *Firewall) identifyRemoteNetworkType(h *HostInfo, fp firewall.Packet) NetworkType {
if h.networks == nil {
// Simple case: Certificate has one address and no unsafe networks
if h.vpnAddrs[0] == fp.RemoteAddr {
return NetworkTypeVPN
} //else, fallthrough
} else if nwType, ok := h.networks.Lookup(fp.RemoteAddr); ok {
return nwType //will return NetworkTypeVPN or NetworkTypeUnsafe
}
//RemoteAddr not in our networks table
if f.snatAddr.IsValid() && fp.IsIPv4() && h.HasOnlyV6Addresses() {
return NetworkTypeUnverifiedSNATPeer
} else {
return NetworkTypeInvalidPeer
}
}
func (f *Firewall) allowRemoteNetworkType(nwType NetworkType, fp firewall.Packet) error {
switch nwType {
case NetworkTypeVPN:
return nil
case NetworkTypeInvalidPeer:
return ErrInvalidRemoteIP
case NetworkTypeVPNPeer:
//one day we might need a specialSnatMode case in here to handle routers with v4 addresses when we don't also have a v4 address?
return ErrPeerRejected // reject for now, one day this may have different FW rules
case NetworkTypeUnsafe:
return nil // nothing special, one day this may have different FW rules
case NetworkTypeUnverifiedSNATPeer:
if f.unsafeIPv4Origin.IsValid() && fp.LocalAddr == f.unsafeIPv4Origin {
return nil //the client case
}
if f.snatAddr.IsValid() {
if fp.RemoteAddr == f.snatAddr {
return ErrInvalidRemoteIP //we should never get a packet with our SNAT addr as the destination, or "from" our SNAT addr
}
return nil
} else {
return ErrInvalidRemoteIP
}
default:
return ErrUnknownNetworkType //should never happen
}
}
func (f *Firewall) willingToHandleLocalAddr(incoming bool, fp firewall.Packet, remoteNwType NetworkType) error {
if f.routableNetworks.Contains(fp.LocalAddr) {
return nil //easy, this should handle NetworkTypeVPN in all cases, and NetworkTypeUnsafe on the router side
}
if incoming { //at least for now, reject all traffic other than what we've already decided is locally routable
return ErrInvalidLocalIP
}
//below this line, all traffic is outgoing. Outgoing traffic to NetworkTypeUnsafe is not required to be considered inbound-routable
if remoteNwType == NetworkTypeUnsafe {
return nil
}
return ErrInvalidLocalIP
}
// Drop returns an error if the packet should be dropped, explaining why. It // Drop returns an error if the packet should be dropped, explaining why. It
// returns nil if the packet should not be dropped. // returns nil if the packet should not be dropped.
func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) error { func (f *Firewall) Drop(fp firewall.Packet, pkt []byte, incoming bool, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) error {
table := f.OutRules
if incoming {
table = f.InRules
}
snatmode := fp.IsIPv4() && h.HasOnlyV6Addresses() && f.snatAddr.IsValid()
if snatmode {
//if this is an IPv4 packet from a V6 only host, and we're configured to snat that kind of traffic, it must be snatted,
//so it can never be in the localcache, which lacks SNAT data
//nil out the pointer to avoid ever using it
localCache = nil
}
// Check if we spoke to this tuple, if we did then allow this packet // Check if we spoke to this tuple, if we did then allow this packet
if f.inConns(fp, h, caPool, localCache) { if localCache != nil {
if _, ok := localCache[fp]; ok {
return nil //packet matched the cache, we're not snatting, we can return early
}
}
c := f.inConns(fp, h, caPool, localCache)
if c != nil {
if incoming && snatmode {
return f.applySnat(pkt, &fp, c, h)
}
return nil return nil
} }
// Make sure remote address matches nebula certificate, and determine how to treat it // Make sure remote address matches nebula certificate, and determine how to treat it
if h.networks == nil { remoteNetworkType := f.identifyRemoteNetworkType(h, fp)
// Simple case: Certificate has one address and no unsafe networks if err := f.allowRemoteNetworkType(remoteNetworkType, fp); err != nil {
if h.vpnAddrs[0] != fp.RemoteAddr { f.metrics(incoming).droppedRemoteAddr.Inc(1)
f.metrics(incoming).droppedRemoteAddr.Inc(1) return err
return ErrInvalidRemoteIP
}
} else {
nwType, ok := h.networks.Lookup(fp.RemoteAddr)
if !ok {
f.metrics(incoming).droppedRemoteAddr.Inc(1)
return ErrInvalidRemoteIP
}
switch nwType {
case NetworkTypeVPN:
break // nothing special
case NetworkTypeVPNPeer:
f.metrics(incoming).droppedRemoteAddr.Inc(1)
return ErrPeerRejected // reject for now, one day this may have different FW rules
case NetworkTypeUnsafe:
break // nothing special, one day this may have different FW rules
default:
f.metrics(incoming).droppedRemoteAddr.Inc(1)
return ErrUnknownNetworkType //should never happen
}
} }
// Make sure we are supposed to be handling this local ip address // Make sure we are supposed to be handling this local ip address
if !f.routableNetworks.Contains(fp.LocalAddr) { if err := f.willingToHandleLocalAddr(incoming, fp, remoteNetworkType); err != nil {
f.metrics(incoming).droppedLocalAddr.Inc(1) f.metrics(incoming).droppedLocalAddr.Inc(1)
return ErrInvalidLocalIP return err
}
table := f.OutRules
if incoming {
table = f.InRules
} }
// We now know which firewall table to check against // We now know which firewall table to check against
@@ -460,9 +666,14 @@ func (f *Firewall) Drop(fp firewall.Packet, incoming bool, h *HostInfo, caPool *
} }
// We always want to conntrack since it is a faster operation // We always want to conntrack since it is a faster operation
f.addConn(fp, incoming) c = f.addConn(fp, incoming)
return nil if incoming && remoteNetworkType == NetworkTypeUnverifiedSNATPeer {
return f.applySnat(pkt, &fp, c, h)
} else {
//outgoing snat is handled before this function is called
return nil
}
} }
func (f *Firewall) metrics(incoming bool) firewallMetrics { func (f *Firewall) metrics(incoming bool) firewallMetrics {
@@ -473,7 +684,7 @@ func (f *Firewall) metrics(incoming bool) firewallMetrics {
} }
} }
// Destroy cleans up any known cyclical references so the object can be free'd my GC. This should be called if a new // Destroy cleans up any known cyclical references so the object can be freed by GC. This should be called if a new
// firewall object is created // firewall object is created
func (f *Firewall) Destroy() { func (f *Firewall) Destroy() {
//TODO: clean references if/when needed //TODO: clean references if/when needed
@@ -489,29 +700,28 @@ func (f *Firewall) EmitStats() {
metrics.GetOrRegisterGauge("firewall.rules.hash", nil).Update(int64(f.GetRuleHashFNV())) metrics.GetOrRegisterGauge("firewall.rules.hash", nil).Update(int64(f.GetRuleHashFNV()))
} }
func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) bool { func (f *Firewall) peek(fp firewall.Packet) *conn {
if localCache != nil { f.Conntrack.Lock()
if _, ok := localCache[fp]; ok { c := f.Conntrack.Conns[fp]
return true f.Conntrack.Unlock()
} return c
} }
func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool, localCache firewall.ConntrackCache) *conn {
conntrack := f.Conntrack conntrack := f.Conntrack
conntrack.Lock() conntrack.Lock()
// Periodic purge instead of every lookup (major CPU savings) // Purge every time we test
conntrack.purgeCounter++ ep, has := conntrack.TimerWheel.Purge()
if conntrack.purgeCounter >= conntrackPurgeInterval { if has {
conntrack.purgeCounter = 0 f.evict(ep)
ep, has := conntrack.TimerWheel.Purge()
if has {
f.evict(ep)
}
} }
c, ok := conntrack.Conns[fp] c, ok := conntrack.Conns[fp]
if !ok { if !ok {
conntrack.Unlock() conntrack.Unlock()
return false return nil
} }
if c.rulesVersion != f.rulesVersion { if c.rulesVersion != f.rulesVersion {
@@ -534,7 +744,7 @@ func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool,
} }
delete(conntrack.Conns, fp) delete(conntrack.Conns, fp)
conntrack.Unlock() conntrack.Unlock()
return false return nil
} }
if f.l.Level >= logrus.DebugLevel { if f.l.Level >= logrus.DebugLevel {
@@ -564,12 +774,11 @@ func (f *Firewall) inConns(fp firewall.Packet, h *HostInfo, caPool *cert.CAPool,
localCache[fp] = struct{}{} localCache[fp] = struct{}{}
} }
return true return c
} }
func (f *Firewall) addConn(fp firewall.Packet, incoming bool) { func (f *Firewall) packetTimeout(fp firewall.Packet) time.Duration {
var timeout time.Duration var timeout time.Duration
c := &conn{}
switch fp.Protocol { switch fp.Protocol {
case firewall.ProtoTCP: case firewall.ProtoTCP:
@@ -579,7 +788,13 @@ func (f *Firewall) addConn(fp firewall.Packet, incoming bool) {
default: default:
timeout = f.DefaultTimeout timeout = f.DefaultTimeout
} }
return timeout
}
func (f *Firewall) addConn(fp firewall.Packet, incoming bool) *conn {
c := &conn{}
timeout := f.packetTimeout(fp)
conntrack := f.Conntrack conntrack := f.Conntrack
conntrack.Lock() conntrack.Lock()
if _, ok := conntrack.Conns[fp]; !ok { if _, ok := conntrack.Conns[fp]; !ok {
@@ -593,7 +808,9 @@ func (f *Firewall) addConn(fp firewall.Packet, incoming bool) {
c.rulesVersion = f.rulesVersion c.rulesVersion = f.rulesVersion
c.Expires = time.Now().Add(timeout) c.Expires = time.Now().Add(timeout)
conntrack.Conns[fp] = c conntrack.Conns[fp] = c
conntrack.Unlock() conntrack.Unlock()
return c
} }
// Evict checks if a conntrack entry has expired, if so it is removed, if not it is re-added to the wheel // Evict checks if a conntrack entry has expired, if so it is removed, if not it is re-added to the wheel
@@ -669,6 +886,13 @@ func (fp firewallPort) match(p firewall.Packet, incoming bool, c *cert.CachedCer
return false return false
} }
// this branch is here to catch traffic from FirewallTable.Any.match and FirewallTable.ICMP.match
if p.Protocol == firewall.ProtoICMP || p.Protocol == firewall.ProtoICMPv6 {
// port numbers are re-used for connection tracking of ICMP,
// but we don't want to actually filter on them.
return fp[firewall.PortAny].match(p, c, caPool)
}
var port int32 var port int32
if p.Fragment { if p.Fragment {
@@ -813,10 +1037,8 @@ func (fr *FirewallRule) isAny(groups []string, host string, cidr string) bool {
return true return true
} }
for _, group := range groups { if slices.Contains(groups, "any") {
if group == "any" { return true
return true
}
} }
if host == "any" { if host == "any" {
@@ -1027,54 +1249,56 @@ func (r *rule) sanity() error {
} }
} }
if r.Code != "" {
return fmt.Errorf("code specified as [%s]. Support for 'code' will be dropped in a future release, as it has never been functional", r.Code)
}
//todo alert on cidr-any //todo alert on cidr-any
return nil return nil
} }
func parsePort(s string) (startPort, endPort int32, err error) { func parsePort(s string) (int32, int32, error) {
var err error
const notAPort int32 = -2
if s == "any" { if s == "any" {
startPort = firewall.PortAny return firewall.PortAny, firewall.PortAny, nil
endPort = firewall.PortAny }
if s == "fragment" {
} else if s == "fragment" { return firewall.PortFragment, firewall.PortFragment, nil
startPort = firewall.PortFragment }
endPort = firewall.PortFragment if !strings.Contains(s, `-`) {
} else if strings.Contains(s, `-`) {
sPorts := strings.SplitN(s, `-`, 2)
sPorts[0] = strings.Trim(sPorts[0], " ")
sPorts[1] = strings.Trim(sPorts[1], " ")
if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
return 0, 0, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
}
rStartPort, err := strconv.Atoi(sPorts[0])
if err != nil {
return 0, 0, fmt.Errorf("beginning range was not a number; `%s`", sPorts[0])
}
rEndPort, err := strconv.Atoi(sPorts[1])
if err != nil {
return 0, 0, fmt.Errorf("ending range was not a number; `%s`", sPorts[1])
}
startPort = int32(rStartPort)
endPort = int32(rEndPort)
if startPort == firewall.PortAny {
endPort = firewall.PortAny
}
} else {
rPort, err := strconv.Atoi(s) rPort, err := strconv.Atoi(s)
if err != nil { if err != nil {
return 0, 0, fmt.Errorf("was not a number; `%s`", s) return notAPort, notAPort, fmt.Errorf("was not a number; `%s`", s)
} }
startPort = int32(rPort) return int32(rPort), int32(rPort), nil
endPort = startPort
} }
return sPorts := strings.SplitN(s, `-`, 2)
for i := range sPorts {
sPorts[i] = strings.Trim(sPorts[i], " ")
}
if len(sPorts) != 2 || sPorts[0] == "" || sPorts[1] == "" {
return notAPort, notAPort, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
}
rStartPort, err := strconv.Atoi(sPorts[0])
if err != nil {
return notAPort, notAPort, fmt.Errorf("beginning range was not a number; `%s`", sPorts[0])
}
rEndPort, err := strconv.Atoi(sPorts[1])
if err != nil {
return notAPort, notAPort, fmt.Errorf("ending range was not a number; `%s`", sPorts[1])
}
startPort := int32(rStartPort)
endPort := int32(rEndPort)
if startPort == firewall.PortAny {
endPort = firewall.PortAny
}
return startPort, endPort, nil
} }
+10 -1
View File
@@ -22,12 +22,19 @@ const (
type Packet struct { type Packet struct {
LocalAddr netip.Addr LocalAddr netip.Addr
RemoteAddr netip.Addr RemoteAddr netip.Addr
LocalPort uint16 // LocalPort is the destination port for incoming traffic, or the source port for outgoing. Zero for ICMP.
LocalPort uint16
// RemotePort is the source port for incoming traffic, or the destination port for outgoing.
// For ICMP, it's the "identifier". This is only used for connection tracking, actual firewall rules will not filter on ICMP identifier
RemotePort uint16 RemotePort uint16
Protocol uint8 Protocol uint8
Fragment bool Fragment bool
} }
func (fp *Packet) IsIPv4() bool {
return fp.LocalAddr.Is4() && fp.RemoteAddr.Is4()
}
func (fp *Packet) Copy() *Packet { func (fp *Packet) Copy() *Packet {
return &Packet{ return &Packet{
LocalAddr: fp.LocalAddr, LocalAddr: fp.LocalAddr,
@@ -46,6 +53,8 @@ func (fp Packet) MarshalJSON() ([]byte, error) {
proto = "tcp" proto = "tcp"
case ProtoICMP: case ProtoICMP:
proto = "icmp" proto = "icmp"
case ProtoICMPv6:
proto = "icmpv6"
case ProtoUDP: case ProtoUDP:
proto = "udp" proto = "udp"
default: default:
+266 -40
View File
@@ -87,9 +87,10 @@ func TestFirewall_AddRule(t *testing.T) {
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
require.NoError(t, fw.AddRule(true, firewall.ProtoICMP, 1, 1, []string{}, "h1", "", "", "", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoICMP, 1, 1, []string{}, "h1", "", "", "", ""))
assert.Nil(t, fw.InRules.ICMP[1].Any.Any) //no matter what port is given for icmp, it should end up as "any"
assert.Empty(t, fw.InRules.ICMP[1].Any.Groups) assert.Nil(t, fw.InRules.ICMP[firewall.PortAny].Any.Any)
assert.Contains(t, fw.InRules.ICMP[1].Any.Hosts, "h1") assert.Empty(t, fw.InRules.ICMP[firewall.PortAny].Any.Groups)
assert.Contains(t, fw.InRules.ICMP[firewall.PortAny].Any.Hosts, "h1")
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c)
require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", ti.String(), "", "", "")) require.NoError(t, fw.AddRule(false, firewall.ProtoAny, 1, 1, []string{}, "", ti.String(), "", "", ""))
@@ -212,44 +213,44 @@ func TestFirewall_Drop(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// Drop outbound // Drop outbound
assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, false, &h, cp, nil)) assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, nil, false, &h, cp, nil))
// Allow inbound // Allow inbound
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil))
// Allow outbound because conntrack // Allow outbound because conntrack
require.NoError(t, fw.Drop(p, false, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, false, &h, cp, nil))
// test remote mismatch // test remote mismatch
oldRemote := p.RemoteAddr oldRemote := p.RemoteAddr
p.RemoteAddr = netip.MustParseAddr("1.2.3.10") p.RemoteAddr = netip.MustParseAddr("1.2.3.10")
assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrInvalidRemoteIP) assert.Equal(t, fw.Drop(p, nil, false, &h, cp, nil), ErrInvalidRemoteIP)
p.RemoteAddr = oldRemote p.RemoteAddr = oldRemote
// ensure signer doesn't get in the way of group checks // ensure signer doesn't get in the way of group checks
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum"))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad"))
assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, nil, true, &h, cp, nil), ErrNoMatchingRule)
// test caSha doesn't drop on match // test caSha doesn't drop on match
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad"))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum"))
require.NoError(t, fw.Drop(p, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil))
// ensure ca name doesn't get in the way of group checks // ensure ca name doesn't get in the way of group checks
cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}} cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", ""))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", ""))
assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, nil, true, &h, cp, nil), ErrNoMatchingRule)
// test caName doesn't drop on match // test caName doesn't drop on match
cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}} cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", ""))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", ""))
require.NoError(t, fw.Drop(p, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil))
} }
func TestFirewall_DropV6(t *testing.T) { func TestFirewall_DropV6(t *testing.T) {
@@ -291,44 +292,44 @@ func TestFirewall_DropV6(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// Drop outbound // Drop outbound
assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, false, &h, cp, nil)) assert.Equal(t, ErrNoMatchingRule, fw.Drop(p, nil, false, &h, cp, nil))
// Allow inbound // Allow inbound
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil))
// Allow outbound because conntrack // Allow outbound because conntrack
require.NoError(t, fw.Drop(p, false, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, false, &h, cp, nil))
// test remote mismatch // test remote mismatch
oldRemote := p.RemoteAddr oldRemote := p.RemoteAddr
p.RemoteAddr = netip.MustParseAddr("fd12::56") p.RemoteAddr = netip.MustParseAddr("fd12::56")
assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrInvalidRemoteIP) assert.Equal(t, fw.Drop(p, nil, false, &h, cp, nil), ErrInvalidRemoteIP)
p.RemoteAddr = oldRemote p.RemoteAddr = oldRemote
// ensure signer doesn't get in the way of group checks // ensure signer doesn't get in the way of group checks
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum"))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum-bad"))
assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, nil, true, &h, cp, nil), ErrNoMatchingRule)
// test caSha doesn't drop on match // test caSha doesn't drop on match
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "", "signer-shasum-bad"))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "", "signer-shasum"))
require.NoError(t, fw.Drop(p, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil))
// ensure ca name doesn't get in the way of group checks // ensure ca name doesn't get in the way of group checks
cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}} cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good", ""))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good-bad", ""))
assert.Equal(t, fw.Drop(p, true, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, nil, true, &h, cp, nil), ErrNoMatchingRule)
// test caName doesn't drop on match // test caName doesn't drop on match
cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}} cp.CAs["signer-shasum"] = &cert.CachedCertificate{Certificate: &dummyCert{name: "ca-good"}}
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, &c)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"nope"}, "", "", "", "ca-good-bad", ""))
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"default-group"}, "", "", "", "ca-good", ""))
require.NoError(t, fw.Drop(p, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil))
} }
func BenchmarkFirewallTable_match(b *testing.B) { func BenchmarkFirewallTable_match(b *testing.B) {
@@ -536,10 +537,10 @@ func TestFirewall_Drop2(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// h1/c1 lacks the proper groups // h1/c1 lacks the proper groups
require.ErrorIs(t, fw.Drop(p, true, &h1, cp, nil), ErrNoMatchingRule) require.ErrorIs(t, fw.Drop(p, nil, true, &h1, cp, nil), ErrNoMatchingRule)
// c has the proper groups // c has the proper groups
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil))
} }
func TestFirewall_Drop3(t *testing.T) { func TestFirewall_Drop3(t *testing.T) {
@@ -617,18 +618,18 @@ func TestFirewall_Drop3(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// c1 should pass because host match // c1 should pass because host match
require.NoError(t, fw.Drop(p, true, &h1, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h1, cp, nil))
// c2 should pass because ca sha match // c2 should pass because ca sha match
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, true, &h2, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h2, cp, nil))
// c3 should fail because no match // c3 should fail because no match
resetConntrack(fw) resetConntrack(fw)
assert.Equal(t, fw.Drop(p, true, &h3, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, nil, true, &h3, cp, nil), ErrNoMatchingRule)
// Test a remote address match // Test a remote address match
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "1.2.3.4/24", "", "", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "1.2.3.4/24", "", "", ""))
require.NoError(t, fw.Drop(p, true, &h1, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h1, cp, nil))
} }
func TestFirewall_Drop3V6(t *testing.T) { func TestFirewall_Drop3V6(t *testing.T) {
@@ -666,7 +667,7 @@ func TestFirewall_Drop3V6(t *testing.T) {
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
cp := cert.NewCAPool() cp := cert.NewCAPool()
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "fd12::34/120", "", "", "")) require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 1, 1, []string{}, "", "fd12::34/120", "", "", ""))
require.NoError(t, fw.Drop(p, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil))
} }
func TestFirewall_DropConntrackReload(t *testing.T) { func TestFirewall_DropConntrackReload(t *testing.T) {
@@ -708,12 +709,12 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
cp := cert.NewCAPool() cp := cert.NewCAPool()
// Drop outbound // Drop outbound
assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound // Allow inbound
resetConntrack(fw) resetConntrack(fw)
require.NoError(t, fw.Drop(p, true, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, true, &h, cp, nil))
// Allow outbound because conntrack // Allow outbound because conntrack
require.NoError(t, fw.Drop(p, false, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, false, &h, cp, nil))
oldFw := fw oldFw := fw
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
@@ -722,7 +723,7 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
fw.rulesVersion = oldFw.rulesVersion + 1 fw.rulesVersion = oldFw.rulesVersion + 1
// Allow outbound because conntrack and new rules allow port 10 // Allow outbound because conntrack and new rules allow port 10
require.NoError(t, fw.Drop(p, false, &h, cp, nil)) require.NoError(t, fw.Drop(p, nil, false, &h, cp, nil))
oldFw = fw oldFw = fw
fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate) fw = NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
@@ -731,7 +732,151 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
fw.rulesVersion = oldFw.rulesVersion + 1 fw.rulesVersion = oldFw.rulesVersion + 1
// Drop outbound because conntrack doesn't match new ruleset // Drop outbound because conntrack doesn't match new ruleset
assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrNoMatchingRule) assert.Equal(t, fw.Drop(p, nil, false, &h, cp, nil), ErrNoMatchingRule)
}
func TestFirewall_ICMPPortBehavior(t *testing.T) {
l := test.NewLogger()
ob := &bytes.Buffer{}
l.SetOutput(ob)
myVpnNetworksTable := new(bart.Lite)
myVpnNetworksTable.Insert(netip.MustParsePrefix("1.1.1.1/8"))
network := netip.MustParsePrefix("1.2.3.4/24")
c := cert.CachedCertificate{
Certificate: &dummyCert{
name: "host1",
networks: []netip.Prefix{network},
groups: []string{"default-group"},
issuer: "signer-shasum",
},
InvertedGroups: map[string]struct{}{"default-group": {}},
}
h := HostInfo{
ConnectionState: &ConnectionState{
peerCert: &c,
},
vpnAddrs: []netip.Addr{network.Addr()},
}
h.buildNetworks(myVpnNetworksTable, c.Certificate)
cp := cert.NewCAPool()
templ := firewall.Packet{
LocalAddr: netip.MustParseAddr("1.2.3.4"),
RemoteAddr: netip.MustParseAddr("1.2.3.4"),
Protocol: firewall.ProtoICMP,
Fragment: false,
}
t.Run("ICMP allowed", func(t *testing.T) {
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
require.NoError(t, fw.AddRule(true, firewall.ProtoICMP, 0, 0, []string{"any"}, "", "", "", "", ""))
t.Run("zero ports", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 0
p.RemotePort = 0
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
require.NoError(t, fw.Drop(*p, nil, true, &h, cp, nil))
//now also allow outbound
require.NoError(t, fw.Drop(*p, nil, false, &h, cp, nil))
})
t.Run("nonzero ports", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 0xabcd
p.RemotePort = 0x1234
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
require.NoError(t, fw.Drop(*p, nil, true, &h, cp, nil))
//now also allow outbound
require.NoError(t, fw.Drop(*p, nil, false, &h, cp, nil))
})
})
t.Run("Any proto, some ports allowed", func(t *testing.T) {
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 80, 444, []string{"any"}, "", "", "", "", ""))
t.Run("zero ports, still blocked", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 0
p.RemotePort = 0
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
assert.Equal(t, fw.Drop(*p, nil, true, &h, cp, nil), ErrNoMatchingRule)
//now also allow outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
})
t.Run("nonzero ports, still blocked", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 0xabcd
p.RemotePort = 0x1234
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
assert.Equal(t, fw.Drop(*p, nil, true, &h, cp, nil), ErrNoMatchingRule)
//now also allow outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
})
t.Run("nonzero, matching ports, still blocked", func(t *testing.T) {
p := templ.Copy()
p.LocalPort = 80
p.RemotePort = 80
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
assert.Equal(t, fw.Drop(*p, nil, true, &h, cp, nil), ErrNoMatchingRule)
//now also allow outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
})
})
t.Run("Any proto, any port", func(t *testing.T) {
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, c.Certificate)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
t.Run("zero ports, allowed", func(t *testing.T) {
resetConntrack(fw)
p := templ.Copy()
p.LocalPort = 0
p.RemotePort = 0
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
require.NoError(t, fw.Drop(*p, nil, true, &h, cp, nil))
//now also allow outbound
require.NoError(t, fw.Drop(*p, nil, false, &h, cp, nil))
})
t.Run("nonzero ports, allowed", func(t *testing.T) {
resetConntrack(fw)
p := templ.Copy()
p.LocalPort = 0xabcd
p.RemotePort = 0x1234
// Drop outbound
assert.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
// Allow inbound
resetConntrack(fw)
require.NoError(t, fw.Drop(*p, nil, true, &h, cp, nil))
//now also allow outbound
require.NoError(t, fw.Drop(*p, nil, false, &h, cp, nil))
//different ID is blocked
p.RemotePort++
require.Equal(t, fw.Drop(*p, nil, false, &h, cp, nil), ErrNoMatchingRule)
})
})
} }
func TestFirewall_DropIPSpoofing(t *testing.T) { func TestFirewall_DropIPSpoofing(t *testing.T) {
@@ -777,7 +922,7 @@ func TestFirewall_DropIPSpoofing(t *testing.T) {
Protocol: firewall.ProtoUDP, Protocol: firewall.ProtoUDP,
Fragment: false, Fragment: false,
} }
assert.Equal(t, fw.Drop(p, true, &h1, cp, nil), ErrInvalidRemoteIP) assert.Equal(t, fw.Drop(p, nil, true, &h1, cp, nil), ErrInvalidRemoteIP)
} }
func BenchmarkLookup(b *testing.B) { func BenchmarkLookup(b *testing.B) {
@@ -897,7 +1042,7 @@ func TestNewFirewallFromConfig(t *testing.T) {
l := test.NewLogger() l := test.NewLogger()
// Test a bad rule definition // Test a bad rule definition
c := &dummyCert{} c := &dummyCert{}
cs, err := newCertState(cert.Version2, nil, c, false, cert.Curve_CURVE25519, nil) cs, err := newCertState(l, cert.Version2, nil, c, false, cert.Curve_CURVE25519, nil)
require.NoError(t, err) require.NoError(t, err)
conf := config.NewC(l) conf := config.NewC(l)
@@ -919,11 +1064,11 @@ func TestNewFirewallFromConfig(t *testing.T) {
// Test code/port error // Test code/port error
conf = config.NewC(l) conf = config.NewC(l)
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "a", "host": "testh"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"code": "a", "host": "testh", "proto": "any"}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; code was not a number; `a`") require.EqualError(t, err, "firewall.outbound rule #0; code was not a number; `a`")
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "a", "host": "testh"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "a", "host": "testh", "proto": "any"}}}
_, err = NewFirewallFromConfig(l, cs, conf) _, err = NewFirewallFromConfig(l, cs, conf)
require.EqualError(t, err, "firewall.outbound rule #0; port was not a number; `a`") require.EqualError(t, err, "firewall.outbound rule #0; port was not a number; `a`")
@@ -973,7 +1118,14 @@ func TestAddFirewallRulesFromConfig(t *testing.T) {
mf = &mockFirewall{} mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "icmp", "host": "a"}}} conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"port": "1", "proto": "icmp", "host": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf)) require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: 1, endPort: 1, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall) assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
// Test adding icmp rule no port
conf = config.NewC(l)
mf = &mockFirewall{}
conf.Settings["firewall"] = map[string]any{"outbound": []any{map[string]any{"proto": "icmp", "host": "a"}}}
require.NoError(t, AddFirewallRulesFromConfig(l, false, conf, mf))
assert.Equal(t, addRuleCall{incoming: false, proto: firewall.ProtoICMP, startPort: firewall.PortAny, endPort: firewall.PortAny, groups: nil, host: "a", ip: "", localIp: ""}, mf.lastCall)
// Test adding any rule // Test adding any rule
conf = config.NewC(l) conf = config.NewC(l)
@@ -1184,7 +1336,7 @@ func (c *testcase) Test(t *testing.T, fw *Firewall) {
t.Helper() t.Helper()
cp := cert.NewCAPool() cp := cert.NewCAPool()
resetConntrack(fw) resetConntrack(fw)
err := fw.Drop(c.p, true, c.h, cp, nil) err := fw.Drop(c.p, nil, true, c.h, cp, nil)
if c.err == nil { if c.err == nil {
require.NoError(t, err, "failed to not drop remote address %s", c.p.RemoteAddr) require.NoError(t, err, "failed to not drop remote address %s", c.p.RemoteAddr)
} else { } else {
@@ -1192,7 +1344,7 @@ func (c *testcase) Test(t *testing.T, fw *Firewall) {
} }
} }
func buildTestCase(setup testsetup, err error, theirPrefixes ...netip.Prefix) testcase { func buildHostinfo(setup testsetup, theirPrefixes ...netip.Prefix) *HostInfo {
c1 := dummyCert{ c1 := dummyCert{
name: "host1", name: "host1",
networks: theirPrefixes, networks: theirPrefixes,
@@ -1212,6 +1364,11 @@ func buildTestCase(setup testsetup, err error, theirPrefixes ...netip.Prefix) te
h.vpnAddrs[i] = theirPrefixes[i].Addr() h.vpnAddrs[i] = theirPrefixes[i].Addr()
} }
h.buildNetworks(setup.myVpnNetworksTable, &c1) h.buildNetworks(setup.myVpnNetworksTable, &c1)
return &h
}
func buildTestCase(setup testsetup, err error, theirPrefixes ...netip.Prefix) testcase {
h := buildHostinfo(setup, theirPrefixes...)
p := firewall.Packet{ p := firewall.Packet{
LocalAddr: setup.c.Networks()[0].Addr(), //todo? LocalAddr: setup.c.Networks()[0].Addr(), //todo?
RemoteAddr: theirPrefixes[0].Addr(), RemoteAddr: theirPrefixes[0].Addr(),
@@ -1221,9 +1378,9 @@ func buildTestCase(setup testsetup, err error, theirPrefixes ...netip.Prefix) te
Fragment: false, Fragment: false,
} }
return testcase{ return testcase{
h: &h, h: h,
p: p, p: p,
c: &c1, c: h.ConnectionState.peerCert.Certificate,
err: err, err: err,
} }
} }
@@ -1245,6 +1402,19 @@ func newSetup(t *testing.T, l *logrus.Logger, myPrefixes ...netip.Prefix) testse
return newSetupFromCert(t, l, c) return newSetupFromCert(t, l, c)
} }
func newSnatSetup(t *testing.T, l *logrus.Logger, myPrefix netip.Prefix, snatAddr netip.Addr) testsetup {
c := dummyCert{
name: "me",
networks: []netip.Prefix{myPrefix},
groups: []string{"default-group"},
issuer: "signer-shasum",
}
out := newSetupFromCert(t, l, c)
out.fw.snatAddr = snatAddr
return out
}
func newSetupFromCert(t *testing.T, l *logrus.Logger, c dummyCert) testsetup { func newSetupFromCert(t *testing.T, l *logrus.Logger, c dummyCert) testsetup {
myVpnNetworksTable := new(bart.Lite) myVpnNetworksTable := new(bart.Lite)
for _, prefix := range c.Networks() { for _, prefix := range c.Networks() {
@@ -1380,3 +1550,59 @@ func resetConntrack(fw *Firewall) {
fw.Conntrack.Conns = map[firewall.Packet]*conn{} fw.Conntrack.Conns = map[firewall.Packet]*conn{}
fw.Conntrack.Unlock() fw.Conntrack.Unlock()
} }
func TestFirewall_SNAT(t *testing.T) {
t.Parallel()
l := test.NewLogger()
ob := &bytes.Buffer{}
l.SetOutput(ob)
cp := cert.NewCAPool()
myPrefix := netip.MustParsePrefix("1.1.1.1/8")
MyCert := dummyCert{
name: "me",
networks: []netip.Prefix{myPrefix},
groups: []string{"default-group"},
issuer: "signer-shasum",
}
theirPrefix := netip.MustParsePrefix("1.2.2.2/8")
snatAddr := netip.MustParseAddr("169.254.55.96")
t.Run("allow inbound all matching", func(t *testing.T) {
t.Parallel()
myCert := MyCert.Copy()
setup := newSnatSetup(t, l, myPrefix, snatAddr)
fw := NewFirewall(l, time.Second, time.Minute, time.Hour, myCert)
require.NoError(t, fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", "", "", ""))
resetConntrack(setup.fw)
h := buildHostinfo(setup, theirPrefix)
p := firewall.Packet{
LocalAddr: setup.c.Networks()[0].Addr(), //todo?
RemoteAddr: h.vpnAddrs[0],
LocalPort: 10,
RemotePort: 90,
Protocol: firewall.ProtoUDP,
Fragment: false,
}
require.NoError(t, setup.fw.Drop(p, nil, true, h, cp, nil))
})
//t.Run("allow inbound unsafe route", func(t *testing.T) {
// t.Parallel()
// unsafePrefix := netip.MustParsePrefix("192.168.0.0/24")
// c := dummyCert{
// name: "me",
// networks: []netip.Prefix{myPrefix},
// unsafeNetworks: []netip.Prefix{unsafePrefix},
// groups: []string{"default-group"},
// issuer: "signer-shasum",
// }
// unsafeSetup := newSetupFromCert(t, l, c)
// tc := buildTestCase(unsafeSetup, nil, twoPrefixes...)
// tc.p.LocalAddr = netip.MustParseAddr("192.168.0.3")
// tc.err = ErrNoMatchingRule
// tc.Test(t, unsafeSetup.fw) //should hit firewall and bounce off
// require.NoError(t, unsafeSetup.fw.AddRule(true, firewall.ProtoAny, 0, 0, []string{"any"}, "", "", unsafePrefix.String(), "", ""))
// tc.err = nil
// tc.Test(t, unsafeSetup.fw) //should pass
//})
}
+4 -3
View File
@@ -4,6 +4,7 @@ go 1.25
require ( require (
dario.cat/mergo v1.0.2 dario.cat/mergo v1.0.2
filippo.io/bigmod v0.1.0
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/armon/go-radix v1.0.0 github.com/armon/go-radix v1.0.0
github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432 github.com/cyberdelia/go-metrics-graphite v0.0.0-20161219230853-39f87cc3b432
@@ -30,11 +31,11 @@ require (
golang.org/x/sys v0.40.0 golang.org/x/sys v0.40.0
golang.org/x/term v0.39.0 golang.org/x/term v0.39.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b
golang.zx2c4.com/wireguard/windows v0.5.3 golang.zx2c4.com/wireguard/windows v0.5.3
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe
) )
require ( require (
@@ -50,6 +51,6 @@ require (
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/mod v0.31.0 // indirect golang.org/x/mod v0.31.0 // indirect
golang.org/x/time v0.7.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.40.0 // indirect golang.org/x/tools v0.40.0 // indirect
) )
+8 -6
View File
@@ -1,6 +1,8 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/bigmod v0.1.0 h1:UNzDk7y9ADKST+axd9skUpBQeW7fG2KrTZyOE4uGQy8=
filippo.io/bigmod v0.1.0/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -216,8 +218,8 @@ golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
@@ -231,8 +233,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b h1:J1CaxgLerRR5lgx3wnr6L04cJFbWoceSK9JWBdglINo=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard v0.0.0-20230325221338-052af4a8072b/go.mod h1:tqur9LnfstdR9ep2LaJT4lFUl0EjlHtge+gAjmsHUG4=
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE= golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI= golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -258,5 +260,5 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI= gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe h1:fre4i6mv4iBuz5lCMOzHD1rH1ljqHWSICFmZRbbgp3g=
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g= gvisor.dev/gvisor v0.0.0-20240423190808-9d7a357edefe/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU=
+1 -1
View File
@@ -590,7 +590,7 @@ func (hm *HandshakeManager) allocateIndex(hh *HandshakeHostInfo) error {
hm.Lock() hm.Lock()
defer hm.Unlock() defer hm.Unlock()
for i := 0; i < 32; i++ { for range 32 {
index, err := generateIndex(hm.l) index, err := generateIndex(hm.l)
if err != nil { if err != nil {
return err return err
+12
View File
@@ -224,6 +224,9 @@ const (
NetworkTypeVPNPeer NetworkTypeVPNPeer
// NetworkTypeUnsafe is a network from Certificate.UnsafeNetworks() // NetworkTypeUnsafe is a network from Certificate.UnsafeNetworks()
NetworkTypeUnsafe NetworkTypeUnsafe
// NetworkTypeUnverifiedSNATPeer is used to indicate traffic we're willing to route, but never deliver to a NetworkTypeVPN
NetworkTypeUnverifiedSNATPeer
NetworkTypeInvalidPeer
) )
type HostInfo struct { type HostInfo struct {
@@ -277,6 +280,15 @@ type HostInfo struct {
lastUsed time.Time lastUsed time.Time
} }
func (i *HostInfo) HasOnlyV6Addresses() bool {
for _, vpnIp := range i.vpnAddrs {
if !vpnIp.Is6() {
return false
}
}
return true
}
type ViaSender struct { type ViaSender struct {
UdpAddr netip.AddrPort UdpAddr netip.AddrPort
relayHI *HostInfo // relayHI is the host info object of the relay relayHI *HostInfo // relayHI is the host info object of the relay
-1
View File
@@ -1,5 +1,4 @@
//go:build e2e_testing //go:build e2e_testing
// +build e2e_testing
package nebula package nebula
+23 -260
View File
@@ -2,8 +2,6 @@ package nebula
import ( import (
"net/netip" "net/netip"
"sync"
"time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/slackhq/nebula/firewall" "github.com/slackhq/nebula/firewall"
@@ -13,258 +11,6 @@ import (
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
// preEncryptionPacket holds packet data before batch encryption
type preEncryptionPacket struct {
hostinfo *HostInfo
ci *ConnectionState
packet []byte
out []byte
}
// Pool for preEncryptionBatch slices to reduce allocations
var preEncryptionBatchPool = sync.Pool{
New: func() any {
// Pre-allocate with reasonable capacity
batch := make([]preEncryptionPacket, 0, 128)
return &batch
},
}
// consumeInsidePackets processes multiple packets in a batch for improved performance
// packets: slice of packet buffers to process
// sizes: slice of packet sizes
// count: number of packets to process
// outs: slice of output buffers (one per packet) with virtio headroom
// q: queue index
// localCache: firewall conntrack cache
// batchPackets: pre-allocated slice for accumulating encrypted packets
// batchAddrs: pre-allocated slice for accumulating destination addresses
func (f *Interface) consumeInsidePackets(packets [][]byte, sizes []int, count int, outs [][]byte, nb []byte, q int, localCache firewall.ConntrackCache, batchPackets *[][]byte, batchAddrs *[]netip.AddrPort) {
// Reusable per-packet state
fwPacket := &firewall.Packet{}
// Reset batch accumulation slices (reuse capacity)
*batchPackets = (*batchPackets)[:0]
*batchAddrs = (*batchAddrs)[:0]
// Collect packets for batched encryption
preEncryptionBatch := make([]preEncryptionPacket, 0, count)
// Process each packet in the batch
for i := 0; i < count; i++ {
packet := packets[i][:sizes[i]]
out := outs[i]
// Inline the consumeInsidePacket logic for better performance
err := newPacket(packet, false, fwPacket)
if err != nil {
if f.l.Level >= logrus.DebugLevel {
f.l.WithField("packet", packet).Debugf("Error while validating outbound packet: %s", err)
}
continue
}
// Ignore local broadcast packets
if f.dropLocalBroadcast {
if f.myBroadcastAddrsTable.Contains(fwPacket.RemoteAddr) {
continue
}
}
if f.myVpnAddrsTable.Contains(fwPacket.RemoteAddr) {
// Immediately forward packets from self to self.
if immediatelyForwardToSelf {
_, err := f.readers[q].Write(packet)
if err != nil {
f.l.WithError(err).Error("Failed to forward to tun")
}
}
continue
}
// Ignore multicast packets
if f.dropMulticast && fwPacket.RemoteAddr.IsMulticast() {
continue
}
hostinfo, ready := f.getOrHandshakeConsiderRouting(fwPacket, func(hh *HandshakeHostInfo) {
hh.cachePacket(f.l, header.Message, 0, packet, f.sendMessageNow, f.cachedPacketMetrics)
})
if hostinfo == nil {
f.rejectInside(packet, out, q)
if f.l.Level >= logrus.DebugLevel {
f.l.WithField("vpnAddr", fwPacket.RemoteAddr).
WithField("fwPacket", fwPacket).
Debugln("dropping outbound packet, vpnAddr not in our vpn networks or in unsafe networks")
}
continue
}
if !ready {
continue
}
dropReason := f.firewall.Drop(*fwPacket, false, hostinfo, f.pki.GetCAPool(), localCache)
if dropReason != nil {
f.rejectInside(packet, out, q)
if f.l.Level >= logrus.DebugLevel {
hostinfo.logger(f.l).
WithField("fwPacket", fwPacket).
WithField("reason", dropReason).
Debugln("dropping outbound packet")
}
continue
}
// Prepare packet for batch encryption
ci := hostinfo.ConnectionState
if ci.eKey == nil {
continue
}
// Check if this needs relay - if so, send immediately and skip batching
useRelay := !hostinfo.remote.IsValid()
if useRelay {
// Handle relay sends individually (less common path)
f.sendNoMetrics(header.Message, 0, ci, hostinfo, netip.AddrPort{}, packet, nb, out, q)
continue
}
// Collect for batched encryption
preEncryptionBatch = append(preEncryptionBatch, preEncryptionPacket{
hostinfo: hostinfo,
ci: ci,
packet: packet,
out: out,
})
}
// BATCH ENCRYPTION: Process all collected packets
if len(preEncryptionBatch) > 0 {
f.encryptBatch(preEncryptionBatch, nb, batchPackets, batchAddrs)
}
// Send all accumulated packets in one batch
if len(*batchPackets) > 0 {
batchSize := len(*batchPackets)
n, err := f.writers[q].WriteMulti(*batchPackets, *batchAddrs)
if err != nil {
f.l.WithError(err).WithField("sent", n).WithField("total", batchSize).Error("Failed to send batch")
}
}
}
// encryptBatch processes multiple packets, grouping by ConnectionState to reduce lock acquisitions
func (f *Interface) encryptBatch(batch []preEncryptionPacket, nb []byte, batchPackets *[][]byte, batchAddrs *[]netip.AddrPort) {
lockStart := time.Now()
lockAcquisitions := int64(0)
if noiseutil.EncryptLockNeeded {
// Group packets by ConnectionState to minimize lock acquisitions
// Process packets in order but batch lock acquisitions per CI
var currentCI *ConnectionState
var lockHeld bool
for i := range batch {
ci := batch[i].ci
hostinfo := batch[i].hostinfo
// Validate packet data to prevent nil pointer dereference
if ci == nil || hostinfo == nil {
continue
}
// Switch locks if we're moving to a different ConnectionState
if ci != currentCI {
if lockHeld {
currentCI.writeLock.Unlock()
}
ci.writeLock.Lock()
lockAcquisitions++
currentCI = ci
lockHeld = true
}
c := ci.messageCounter.Add(1)
out := header.Encode(batch[i].out, header.Version, header.Message, 0, hostinfo.remoteIndexId, c)
f.connectionManager.Out(hostinfo)
// Query lighthouse if needed
if hostinfo.lastRebindCount != f.rebindCount {
f.lightHouse.QueryServer(hostinfo.vpnAddrs[0])
hostinfo.lastRebindCount = f.rebindCount
if f.l.Level >= logrus.DebugLevel {
f.l.WithField("vpnAddrs", hostinfo.vpnAddrs).Debug("Lighthouse update triggered for punch due to rebind counter")
}
}
var err error
out, err = ci.eKey.EncryptDanger(out, out, batch[i].packet, c, nb)
if err != nil {
hostinfo.logger(f.l).WithError(err).
WithField("counter", c).
Error("Failed to encrypt outgoing packet")
continue
}
// Add to output batches
*batchPackets = append(*batchPackets, out)
*batchAddrs = append(*batchAddrs, hostinfo.remote)
}
// Release final lock
if lockHeld {
currentCI.writeLock.Unlock()
}
} else {
// No locks needed - process directly
for i := range batch {
ci := batch[i].ci
hostinfo := batch[i].hostinfo
// Validate packet data to prevent nil pointer dereference
if ci == nil || hostinfo == nil {
continue
}
c := ci.messageCounter.Add(1)
out := header.Encode(batch[i].out, header.Version, header.Message, 0, hostinfo.remoteIndexId, c)
f.connectionManager.Out(hostinfo)
// Query lighthouse if needed
if hostinfo.lastRebindCount != f.rebindCount {
f.lightHouse.QueryServer(hostinfo.vpnAddrs[0])
hostinfo.lastRebindCount = f.rebindCount
if f.l.Level >= logrus.DebugLevel {
f.l.WithField("vpnAddrs", hostinfo.vpnAddrs).Debug("Lighthouse update triggered for punch due to rebind counter")
}
}
var err error
out, err = ci.eKey.EncryptDanger(out, out, batch[i].packet, c, nb)
if err != nil {
hostinfo.logger(f.l).WithError(err).
WithField("counter", c).
Error("Failed to encrypt outgoing packet")
continue
}
// Add to output batches
*batchPackets = append(*batchPackets, out)
*batchAddrs = append(*batchAddrs, hostinfo.remote)
}
}
// Record metrics
encryptionTime := time.Since(lockStart)
if noiseutil.EncryptLockNeeded {
f.batchMetrics.lockAcquisitions.Inc(lockAcquisitions)
}
f.batchMetrics.encryptionTime.Update(encryptionTime.Nanoseconds())
f.batchMetrics.batchSize.Update(int64(len(batch)))
}
func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet, nb, out []byte, q int, localCache firewall.ConntrackCache) { func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet, nb, out []byte, q int, localCache firewall.ConntrackCache) {
err := newPacket(packet, false, fwPacket) err := newPacket(packet, false, fwPacket)
if err != nil { if err != nil {
@@ -302,9 +48,7 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
return return
} }
hostinfo, ready := f.getOrHandshakeConsiderRouting(fwPacket, func(hh *HandshakeHostInfo) { hostinfo, ready := f.getHostinfo(packet, fwPacket)
hh.cachePacket(f.l, header.Message, 0, packet, f.sendMessageNow, f.cachedPacketMetrics)
})
if hostinfo == nil { if hostinfo == nil {
f.rejectInside(packet, out, q) f.rejectInside(packet, out, q)
@@ -320,10 +64,9 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
return return
} }
dropReason := f.firewall.Drop(*fwPacket, false, hostinfo, f.pki.GetCAPool(), localCache) dropReason := f.firewall.Drop(*fwPacket, packet, false, hostinfo, f.pki.GetCAPool(), localCache)
if dropReason == nil { if dropReason == nil {
f.sendNoMetrics(header.Message, 0, hostinfo.ConnectionState, hostinfo, netip.AddrPort{}, packet, nb, out, q) f.sendNoMetrics(header.Message, 0, hostinfo.ConnectionState, hostinfo, netip.AddrPort{}, packet, nb, out, q)
} else { } else {
f.rejectInside(packet, out, q) f.rejectInside(packet, out, q)
if f.l.Level >= logrus.DebugLevel { if f.l.Level >= logrus.DebugLevel {
@@ -335,6 +78,26 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
} }
} }
func (f *Interface) getHostinfo(packet []byte, fwPacket *firewall.Packet) (*HostInfo, bool) {
if f.firewall.ShouldUnSNAT(fwPacket) {
//unsnat packet re-writing also happens here, would be nice to not,
//but we need to do the unsnat lookup to find the hostinfo so we can run the firewall checks
destVpnAddr := f.firewall.unSnat(packet, fwPacket)
if destVpnAddr.IsValid() {
//because this was a snatted packet, we know it has an on-overlay destination, so no routing should be required.
return f.getOrHandshakeNoRouting(destVpnAddr, func(hh *HandshakeHostInfo) {
hh.cachePacket(f.l, header.Message, 0, packet, f.sendMessageNow, f.cachedPacketMetrics)
})
} else {
return nil, false
}
} else { //if we didn't need to unsnat
return f.getOrHandshakeConsiderRouting(fwPacket, func(hh *HandshakeHostInfo) {
hh.cachePacket(f.l, header.Message, 0, packet, f.sendMessageNow, f.cachedPacketMetrics)
})
}
}
func (f *Interface) rejectInside(packet []byte, out []byte, q int) { func (f *Interface) rejectInside(packet []byte, out []byte, q int) {
if !f.firewall.InSendReject { if !f.firewall.InSendReject {
return return
@@ -472,7 +235,7 @@ func (f *Interface) sendMessageNow(t header.MessageType, st header.MessageSubTyp
} }
// check if packet is in outbound fw rules // check if packet is in outbound fw rules
dropReason := f.firewall.Drop(*fp, false, hostinfo, f.pki.GetCAPool(), nil) dropReason := f.firewall.Drop(*fp, p, false, hostinfo, f.pki.GetCAPool(), nil)
if dropReason != nil { if dropReason != nil {
if f.l.Level >= logrus.DebugLevel { if f.l.Level >= logrus.DebugLevel {
f.l.WithField("fwPacket", fp). f.l.WithField("fwPacket", fp).
-1
View File
@@ -1,5 +1,4 @@
//go:build darwin || dragonfly || freebsd || netbsd || openbsd //go:build darwin || dragonfly || freebsd || netbsd || openbsd
// +build darwin dragonfly freebsd netbsd openbsd
package nebula package nebula
-1
View File
@@ -1,5 +1,4 @@
//go:build !darwin && !dragonfly && !freebsd && !netbsd && !openbsd //go:build !darwin && !dragonfly && !freebsd && !netbsd && !openbsd
// +build !darwin,!dragonfly,!freebsd,!netbsd,!openbsd
package nebula package nebula
+18 -59
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"net/netip" "net/netip"
"os" "os"
"runtime" "runtime"
@@ -21,7 +22,6 @@ import (
) )
const mtu = 9001 const mtu = 9001
const virtioNetHdrLen = overlay.VirtioNetHdrLen
type InterfaceConfig struct { type InterfaceConfig struct {
HostMap *HostMap HostMap *HostMap
@@ -50,13 +50,6 @@ type InterfaceConfig struct {
l *logrus.Logger l *logrus.Logger
} }
type batchMetrics struct {
udpReadSize metrics.Histogram
encryptionTime metrics.Histogram // Time spent in encryption (including lock waits)
batchSize metrics.Histogram // Dynamic batch sizes being used
lockAcquisitions metrics.Counter // Number of lock acquisitions (should be minimal)
}
type Interface struct { type Interface struct {
hostMap *HostMap hostMap *HostMap
outside udp.Conn outside udp.Conn
@@ -94,12 +87,11 @@ type Interface struct {
conntrackCacheTimeout time.Duration conntrackCacheTimeout time.Duration
writers []udp.Conn writers []udp.Conn
readers []overlay.BatchReadWriter readers []io.ReadWriteCloser
metricHandshakes metrics.Histogram metricHandshakes metrics.Histogram
messageMetrics *MessageMetrics messageMetrics *MessageMetrics
cachedPacketMetrics *cachedPacketMetrics cachedPacketMetrics *cachedPacketMetrics
batchMetrics *batchMetrics
l *logrus.Logger l *logrus.Logger
} }
@@ -186,7 +178,7 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
routines: c.routines, routines: c.routines,
version: c.version, version: c.version,
writers: make([]udp.Conn, c.routines), writers: make([]udp.Conn, c.routines),
readers: make([]overlay.BatchReadWriter, c.routines), readers: make([]io.ReadWriteCloser, c.routines),
myVpnNetworks: cs.myVpnNetworks, myVpnNetworks: cs.myVpnNetworks,
myVpnNetworksTable: cs.myVpnNetworksTable, myVpnNetworksTable: cs.myVpnNetworksTable,
myVpnAddrs: cs.myVpnAddrs, myVpnAddrs: cs.myVpnAddrs,
@@ -202,12 +194,6 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
sent: metrics.GetOrRegisterCounter("hostinfo.cached_packets.sent", nil), sent: metrics.GetOrRegisterCounter("hostinfo.cached_packets.sent", nil),
dropped: metrics.GetOrRegisterCounter("hostinfo.cached_packets.dropped", nil), dropped: metrics.GetOrRegisterCounter("hostinfo.cached_packets.dropped", nil),
}, },
batchMetrics: &batchMetrics{
udpReadSize: metrics.GetOrRegisterHistogram("batch.udp_read_size", nil, metrics.NewUniformSample(1024)),
encryptionTime: metrics.GetOrRegisterHistogram("batch.encryption_time_ns", nil, metrics.NewUniformSample(1024)),
batchSize: metrics.GetOrRegisterHistogram("batch.size", nil, metrics.NewUniformSample(1024)),
lockAcquisitions: metrics.GetOrRegisterCounter("batch.lock_acquisitions", nil),
},
l: c.l, l: c.l,
} }
@@ -247,7 +233,7 @@ func (f *Interface) activate() {
metrics.GetOrRegisterGauge("routines", nil).Update(int64(f.routines)) metrics.GetOrRegisterGauge("routines", nil).Update(int64(f.routines))
// Prepare n tun queues // Prepare n tun queues
var reader overlay.BatchReadWriter = f.inside var reader io.ReadWriteCloser = f.inside
for i := 0; i < f.routines; i++ { for i := 0; i < f.routines; i++ {
if i > 0 { if i > 0 {
reader, err = f.inside.NewMultiQueueReader() reader, err = f.inside.NewMultiQueueReader()
@@ -262,6 +248,7 @@ func (f *Interface) activate() {
f.inside.Close() f.inside.Close()
f.l.Fatal(err) f.l.Fatal(err)
} }
f.firewall.SetSNATAddressFromInterface(f)
} }
func (f *Interface) run() { func (f *Interface) run() {
@@ -288,68 +275,39 @@ func (f *Interface) listenOut(i int) {
ctCache := firewall.NewConntrackCacheTicker(f.conntrackCacheTimeout) ctCache := firewall.NewConntrackCacheTicker(f.conntrackCacheTimeout)
lhh := f.lightHouse.NewRequestHandler() lhh := f.lightHouse.NewRequestHandler()
plaintext := make([]byte, udp.MTU)
// Pre-allocate output buffers for batch processing
batchSize := li.BatchSize()
outs := make([][]byte, batchSize)
for idx := range outs {
// Allocate full buffer with virtio header space
outs[idx] = make([]byte, virtioNetHdrLen, virtioNetHdrLen+udp.MTU)
}
h := &header.H{} h := &header.H{}
fwPacket := &firewall.Packet{} fwPacket := &firewall.Packet{}
nb := make([]byte, 12) nb := make([]byte, 12, 12)
li.ListenOutBatch(func(addrs []netip.AddrPort, payloads [][]byte, count int) {
f.readOutsidePacketsBatch(addrs, payloads, count, outs[:count], nb, i, h, fwPacket, lhh, ctCache.Get(f.l))
li.ListenOut(func(fromUdpAddr netip.AddrPort, payload []byte) {
f.readOutsidePackets(ViaSender{UdpAddr: fromUdpAddr}, plaintext[:0], payload, h, fwPacket, lhh, nb, i, ctCache.Get(f.l))
}) })
} }
func (f *Interface) listenIn(reader overlay.BatchReadWriter, i int) { func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
runtime.LockOSThread() runtime.LockOSThread()
batchSize := reader.BatchSize() packet := make([]byte, mtu)
out := make([]byte, mtu)
// Allocate buffers for batch reading fwPacket := &firewall.Packet{}
bufs := make([][]byte, batchSize) nb := make([]byte, 12, 12)
for idx := range bufs {
bufs[idx] = make([]byte, mtu)
}
sizes := make([]int, batchSize)
// Allocate output buffers for batch processing (one per packet)
// Each has virtio header headroom to avoid copies on write
outs := make([][]byte, batchSize)
for idx := range outs {
outBuf := make([]byte, virtioNetHdrLen+mtu)
outs[idx] = outBuf[virtioNetHdrLen:] // Slice starting after headroom
}
// Pre-allocate batch accumulation buffers for sending
batchPackets := make([][]byte, 0, batchSize)
batchAddrs := make([]netip.AddrPort, 0, batchSize)
// Pre-allocate nonce buffer (reused for all encryptions)
nb := make([]byte, 12)
conntrackCache := firewall.NewConntrackCacheTicker(f.conntrackCacheTimeout) conntrackCache := firewall.NewConntrackCacheTicker(f.conntrackCacheTimeout)
for { for {
n, err := reader.BatchRead(bufs, sizes) n, err := reader.Read(packet)
if err != nil { if err != nil {
if errors.Is(err, os.ErrClosed) && f.closed.Load() { if errors.Is(err, os.ErrClosed) && f.closed.Load() {
return return
} }
f.l.WithError(err).Error("Error while batch reading outbound packets") f.l.WithError(err).Error("Error while reading outbound packet")
// This only seems to happen when something fatal happens to the fd, so exit. // This only seems to happen when something fatal happens to the fd, so exit.
os.Exit(2) os.Exit(2)
} }
// Process all packets in the batch at once f.consumeInsidePacket(packet[:n], fwPacket, nb, out, i, conntrackCache.Get(f.l))
f.consumeInsidePackets(bufs, sizes, n, outs, nb, i, conntrackCache.Get(f.l), &batchPackets, &batchAddrs)
} }
} }
@@ -387,6 +345,7 @@ func (f *Interface) reloadFirewall(c *config.C) {
f.l.WithError(err).Error("Error while creating firewall during reload") f.l.WithError(err).Error("Error while creating firewall during reload")
return return
} }
fw.SetSNATAddressFromInterface(f)
oldFw := f.firewall oldFw := f.firewall
conntrack := oldFw.Conntrack conntrack := oldFw.Conntrack
+3 -10
View File
@@ -713,21 +713,14 @@ func (lh *LightHouse) unlockedShouldAddV6(vpnAddr netip.Addr, to *V6AddrPort) bo
func (lh *LightHouse) IsLighthouseAddr(vpnAddr netip.Addr) bool { func (lh *LightHouse) IsLighthouseAddr(vpnAddr netip.Addr) bool {
l := lh.GetLighthouses() l := lh.GetLighthouses()
for i := range l { return slices.Contains(l, vpnAddr)
if l[i] == vpnAddr {
return true
}
}
return false
} }
func (lh *LightHouse) IsAnyLighthouseAddr(vpnAddrs []netip.Addr) bool { func (lh *LightHouse) IsAnyLighthouseAddr(vpnAddrs []netip.Addr) bool {
l := lh.GetLighthouses() l := lh.GetLighthouses()
for i := range vpnAddrs { for i := range vpnAddrs {
for j := range l { if slices.Contains(l, vpnAddrs[i]) {
if l[j] == vpnAddrs[i] { return true
return true
}
} }
} }
return false return false
+4 -7
View File
@@ -105,11 +105,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
// deprecated and undocumented // deprecated and undocumented
tunQueues := c.GetInt("tun.routines", 1) tunQueues := c.GetInt("tun.routines", 1)
udpQueues := c.GetInt("listen.routines", 1) udpQueues := c.GetInt("listen.routines", 1)
if tunQueues > udpQueues { routines = max(tunQueues, udpQueues)
routines = tunQueues
} else {
routines = udpQueues
}
if routines != 1 { if routines != 1 {
l.WithField("routines", routines).Warn("Setting tun.routines and listen.routines is deprecated. Use `routines` instead") l.WithField("routines", routines).Warn("Setting tun.routines and listen.routines is deprecated. Use `routines` instead")
} }
@@ -135,7 +131,8 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
deviceFactory = overlay.NewDeviceFromConfig deviceFactory = overlay.NewDeviceFromConfig
} }
tun, err = deviceFactory(c, l, pki.getCertState().myVpnNetworks, routines) cs := pki.getCertState()
tun, err = deviceFactory(c, l, cs.myVpnNetworks, cs.GetDefaultCertificate().UnsafeNetworks(), routines)
if err != nil { if err != nil {
return nil, util.ContextualizeIfNeeded("Failed to get a tun/tap device", err) return nil, util.ContextualizeIfNeeded("Failed to get a tun/tap device", err)
} }
@@ -171,7 +168,7 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
for i := 0; i < routines; i++ { for i := 0; i < routines; i++ {
l.Infof("listening on %v", netip.AddrPortFrom(listenHost, uint16(port))) l.Infof("listening on %v", netip.AddrPortFrom(listenHost, uint16(port)))
udpServer, err := udp.NewListener(l, listenHost, port, routines > 1, c.GetInt("listen.batch", 128)) udpServer, err := udp.NewListener(l, listenHost, port, routines > 1, c.GetInt("listen.batch", 64))
if err != nil { if err != nil {
return nil, util.NewContextualError("Failed to open udp listener", m{"queue": i}, err) return nil, util.NewContextualError("Failed to open udp listener", m{"queue": i}, err)
} }
-1
View File
@@ -1,5 +1,4 @@
//go:build !boringcrypto //go:build !boringcrypto
// +build !boringcrypto
package nebula package nebula
+47 -134
View File
@@ -102,7 +102,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
relay: relay, relay: relay,
IsRelayed: true, IsRelayed: true,
} }
f.readOutsidePackets(via, out[:virtioNetHdrLen], signedPayload, h, fwPacket, lhf, nb, q, localCache) f.readOutsidePackets(via, out[:0], signedPayload, h, fwPacket, lhf, nb, q, localCache)
return return
case ForwardingType: case ForwardingType:
// Find the target HostInfo relay object // Find the target HostInfo relay object
@@ -145,7 +145,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
} }
//TODO: assert via is not relayed //TODO: assert via is not relayed
lhf.HandleRequest(via.UdpAddr, hostinfo.vpnAddrs, d[virtioNetHdrLen:], f) lhf.HandleRequest(via.UdpAddr, hostinfo.vpnAddrs, d, f)
// Fallthrough to the bottom to record incoming traffic // Fallthrough to the bottom to record incoming traffic
@@ -167,7 +167,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
// This testRequest might be from TryPromoteBest, so we should roam // This testRequest might be from TryPromoteBest, so we should roam
// to the new IP address before responding // to the new IP address before responding
f.handleHostRoaming(hostinfo, via) f.handleHostRoaming(hostinfo, via)
f.send(header.Test, header.TestReply, ci, hostinfo, d[virtioNetHdrLen:], nb, out) f.send(header.Test, header.TestReply, ci, hostinfo, d, nb, out)
} }
// Fallthrough to the bottom to record incoming traffic // Fallthrough to the bottom to record incoming traffic
@@ -210,7 +210,7 @@ func (f *Interface) readOutsidePackets(via ViaSender, out []byte, packet []byte,
return return
} }
f.relayManager.HandleControlMsg(hostinfo, d[virtioNetHdrLen:], f) f.relayManager.HandleControlMsg(hostinfo, d, f)
default: default:
f.messageMetrics.Rx(h.Type, h.Subtype, 1) f.messageMetrics.Rx(h.Type, h.Subtype, 1)
@@ -327,13 +327,29 @@ func parseV6(data []byte, incoming bool, fp *firewall.Packet) error {
proto := layers.IPProtocol(data[protoAt]) proto := layers.IPProtocol(data[protoAt])
switch proto { switch proto {
case layers.IPProtocolICMPv6, layers.IPProtocolESP, layers.IPProtocolNoNextHeader: case layers.IPProtocolESP, layers.IPProtocolNoNextHeader:
fp.Protocol = uint8(proto) fp.Protocol = uint8(proto)
fp.RemotePort = 0 fp.RemotePort = 0
fp.LocalPort = 0 fp.LocalPort = 0
fp.Fragment = false fp.Fragment = false
return nil return nil
case layers.IPProtocolICMPv6:
if dataLen < offset+6 {
return ErrIPv6PacketTooShort
}
fp.Protocol = uint8(proto)
fp.LocalPort = 0 //incoming vs outgoing doesn't matter for icmpv6
icmptype := data[offset+1]
switch icmptype {
case layers.ICMPv6TypeEchoRequest, layers.ICMPv6TypeEchoReply:
fp.RemotePort = binary.BigEndian.Uint16(data[offset+4 : offset+6]) //identifier
default:
fp.RemotePort = 0
}
fp.Fragment = false
return nil
case layers.IPProtocolTCP, layers.IPProtocolUDP: case layers.IPProtocolTCP, layers.IPProtocolUDP:
if dataLen < offset+4 { if dataLen < offset+4 {
return ErrIPv6PacketTooShort return ErrIPv6PacketTooShort
@@ -423,34 +439,38 @@ func parseV4(data []byte, incoming bool, fp *firewall.Packet) error {
// Accounting for a variable header length, do we have enough data for our src/dst tuples? // Accounting for a variable header length, do we have enough data for our src/dst tuples?
minLen := ihl minLen := ihl
if !fp.Fragment && fp.Protocol != firewall.ProtoICMP { if !fp.Fragment {
minLen += minFwPacketLen if fp.Protocol == firewall.ProtoICMP {
minLen += minFwPacketLen + 2
} else {
minLen += minFwPacketLen
}
} }
if len(data) < minLen { if len(data) < minLen {
return ErrIPv4InvalidHeaderLength return ErrIPv4InvalidHeaderLength
} }
// Firewall packets are locally oriented if incoming { // Firewall packets are locally oriented
if incoming {
fp.RemoteAddr, _ = netip.AddrFromSlice(data[12:16]) fp.RemoteAddr, _ = netip.AddrFromSlice(data[12:16])
fp.LocalAddr, _ = netip.AddrFromSlice(data[16:20]) fp.LocalAddr, _ = netip.AddrFromSlice(data[16:20])
if fp.Fragment || fp.Protocol == firewall.ProtoICMP {
fp.RemotePort = 0
fp.LocalPort = 0
} else {
fp.RemotePort = binary.BigEndian.Uint16(data[ihl : ihl+2])
fp.LocalPort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4])
}
} else { } else {
fp.LocalAddr, _ = netip.AddrFromSlice(data[12:16]) fp.LocalAddr, _ = netip.AddrFromSlice(data[12:16])
fp.RemoteAddr, _ = netip.AddrFromSlice(data[16:20]) fp.RemoteAddr, _ = netip.AddrFromSlice(data[16:20])
if fp.Fragment || fp.Protocol == firewall.ProtoICMP { }
fp.RemotePort = 0
fp.LocalPort = 0 if fp.Fragment {
} else { fp.RemotePort = 0
fp.LocalPort = binary.BigEndian.Uint16(data[ihl : ihl+2]) fp.LocalPort = 0
fp.RemotePort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4]) } else if fp.Protocol == firewall.ProtoICMP { //note that orientation doesn't matter on ICMP
} fp.RemotePort = binary.BigEndian.Uint16(data[ihl+4 : ihl+6]) //identifier
fp.LocalPort = 0 //code would be uint16(data[ihl+1])
} else if incoming {
fp.RemotePort = binary.BigEndian.Uint16(data[ihl : ihl+2]) //src port
fp.LocalPort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4]) //dst port
} else {
fp.LocalPort = binary.BigEndian.Uint16(data[ihl : ihl+2]) //src port
fp.RemotePort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4]) //dst port
} }
return nil return nil
@@ -481,11 +501,9 @@ func (f *Interface) decryptToTun(hostinfo *HostInfo, messageCounter uint64, out
return false return false
} }
packetData := out[virtioNetHdrLen:] err = newPacket(out, true, fwPacket)
err = newPacket(packetData, true, fwPacket)
if err != nil { if err != nil {
hostinfo.logger(f.l).WithError(err).WithField("packet", packetData). hostinfo.logger(f.l).WithError(err).WithField("packet", out).
Warnf("Error while validating inbound packet") Warnf("Error while validating inbound packet")
return false return false
} }
@@ -496,11 +514,11 @@ func (f *Interface) decryptToTun(hostinfo *HostInfo, messageCounter uint64, out
return false return false
} }
dropReason := f.firewall.Drop(*fwPacket, true, hostinfo, f.pki.GetCAPool(), localCache) dropReason := f.firewall.Drop(*fwPacket, out, true, hostinfo, f.pki.GetCAPool(), localCache)
if dropReason != nil { if dropReason != nil {
// NOTE: We give `packet` as the `out` here since we already decrypted from it and we don't need it anymore // NOTE: We give `packet` as the `out` here since we already decrypted from it and we don't need it anymore
// This gives us a buffer to build the reject packet in // This gives us a buffer to build the reject packet in
f.rejectOutside(packetData, hostinfo.ConnectionState, hostinfo, nb, packet, q) f.rejectOutside(out, hostinfo.ConnectionState, hostinfo, nb, packet, q)
if f.l.Level >= logrus.DebugLevel { if f.l.Level >= logrus.DebugLevel {
hostinfo.logger(f.l).WithField("fwPacket", fwPacket). hostinfo.logger(f.l).WithField("fwPacket", fwPacket).
WithField("reason", dropReason). WithField("reason", dropReason).
@@ -564,108 +582,3 @@ func (f *Interface) handleRecvError(addr netip.AddrPort, h *header.H) {
// We also delete it from pending hostmap to allow for fast reconnect. // We also delete it from pending hostmap to allow for fast reconnect.
f.handshakeManager.DeleteHostInfo(hostinfo) f.handshakeManager.DeleteHostInfo(hostinfo)
} }
// readOutsidePacketsBatch processes multiple packets received from UDP in a batch
// and writes all successfully decrypted packets to TUN in a single operation
func (f *Interface) readOutsidePacketsBatch(addrs []netip.AddrPort, payloads [][]byte, count int, outs [][]byte, nb []byte, q int, h *header.H, fwPacket *firewall.Packet, lhf *LightHouseHandler, localCache firewall.ConntrackCache) {
// Pre-allocate slice for accumulating successful decryptions
tunPackets := make([][]byte, 0, count)
for i := 0; i < count; i++ {
payload := payloads[i]
addr := addrs[i]
out := outs[i]
// Parse header
err := h.Parse(payload)
if err != nil {
if len(payload) > 1 {
f.l.WithField("packet", payload).Infof("Error while parsing inbound packet from %s: %s", addr, err)
}
continue
}
if addr.IsValid() {
if f.myVpnNetworksTable.Contains(addr.Addr()) {
if f.l.Level >= logrus.DebugLevel {
f.l.WithField("udpAddr", addr).Debug("Refusing to process double encrypted packet")
}
continue
}
}
var hostinfo *HostInfo
if h.Type == header.Message && h.Subtype == header.MessageRelay {
hostinfo = f.hostMap.QueryRelayIndex(h.RemoteIndex)
} else {
hostinfo = f.hostMap.QueryIndex(h.RemoteIndex)
}
var ci *ConnectionState
if hostinfo != nil {
ci = hostinfo.ConnectionState
}
switch h.Type {
case header.Message:
if !f.handleEncrypted(ci, ViaSender{UdpAddr: addr}, h) {
continue
}
switch h.Subtype {
case header.MessageNone:
// Decrypt packet
out, err = hostinfo.ConnectionState.dKey.DecryptDanger(out, payload[:header.Len], payload[header.Len:], h.MessageCounter, nb)
if err != nil {
hostinfo.logger(f.l).WithError(err).Error("Failed to decrypt packet")
continue
}
packetData := out[virtioNetHdrLen:]
err = newPacket(packetData, true, fwPacket)
if err != nil {
hostinfo.logger(f.l).WithError(err).WithField("packet", packetData).Warnf("Error while validating inbound packet")
continue
}
if !hostinfo.ConnectionState.window.Update(f.l, h.MessageCounter) {
hostinfo.logger(f.l).WithField("fwPacket", fwPacket).Debugln("dropping out of window packet")
continue
}
dropReason := f.firewall.Drop(*fwPacket, true, hostinfo, f.pki.GetCAPool(), localCache)
if dropReason != nil {
f.rejectOutside(packetData, hostinfo.ConnectionState, hostinfo, nb, payload, q)
if f.l.Level >= logrus.DebugLevel {
hostinfo.logger(f.l).WithField("fwPacket", fwPacket).WithField("reason", dropReason).Debugln("dropping inbound packet")
}
continue
}
f.connectionManager.In(hostinfo)
// Add to batch for TUN write
tunPackets = append(tunPackets, out)
case header.MessageRelay:
// Skip relay packets in batch mode for now (less common path)
f.readOutsidePackets(ViaSender{UdpAddr: addr}, out[:virtioNetHdrLen], payload, h, fwPacket, lhf, nb, q, localCache)
default:
hostinfo.logger(f.l).Debugf("unexpected message subtype %d", h.Subtype)
}
default:
// Handle non-Message types using single-packet path
f.readOutsidePackets(ViaSender{UdpAddr: addr}, out[:virtioNetHdrLen], payload, h, fwPacket, lhf, nb, q, localCache)
}
}
if len(tunPackets) > 0 {
n, err := f.readers[q].WriteBatch(tunPackets, virtioNetHdrLen)
if err != nil {
f.l.WithError(err).WithField("sent", n).WithField("total", len(tunPackets)).Error("Failed to batch write to tun")
}
}
}
+17 -10
View File
@@ -155,6 +155,7 @@ func Test_newPacket_v6(t *testing.T) {
// next layer, missing length byte // next layer, missing length byte
err = newPacket(buffer.Bytes()[:49], true, p) err = newPacket(buffer.Bytes()[:49], true, p)
require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload) require.ErrorIs(t, err, ErrIPv6CouldNotFindPayload)
err = nil
// A good ICMP packet // A good ICMP packet
ip = layers.IPv6{ ip = layers.IPv6{
@@ -165,20 +166,26 @@ func Test_newPacket_v6(t *testing.T) {
DstIP: net.IPv6linklocalallnodes, DstIP: net.IPv6linklocalallnodes,
} }
icmp := layers.ICMPv6{} icmp := layers.ICMPv6{
TypeCode: layers.ICMPv6TypeEchoRequest,
buffer.Clear() Checksum: 0x1234,
err = gopacket.SerializeLayers(buffer, opt, &ip, &icmp)
if err != nil {
panic(err)
} }
err = newPacket(buffer.Bytes(), true, p) buffer.Clear()
require.NoError(t, err) require.NoError(t, gopacket.SerializeLayers(buffer, opt, &ip, &icmp))
require.Error(t, newPacket(buffer.Bytes(), true, p))
buffer.Clear()
echo := layers.ICMPv6Echo{
Identifier: 0xabcd,
SeqNumber: 1234,
}
require.NoError(t, gopacket.SerializeLayers(buffer, opt, &ip, &icmp, &echo))
require.NoError(t, newPacket(buffer.Bytes(), true, p))
assert.Equal(t, uint8(layers.IPProtocolICMPv6), p.Protocol) assert.Equal(t, uint8(layers.IPProtocolICMPv6), p.Protocol)
assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr) assert.Equal(t, netip.MustParseAddr("ff02::2"), p.RemoteAddr)
assert.Equal(t, netip.MustParseAddr("ff02::1"), p.LocalAddr) assert.Equal(t, netip.MustParseAddr("ff02::1"), p.LocalAddr)
assert.Equal(t, uint16(0), p.RemotePort) assert.Equal(t, uint16(0xabcd), p.RemotePort)
assert.Equal(t, uint16(0), p.LocalPort) assert.Equal(t, uint16(0), p.LocalPort)
assert.False(t, p.Fragment) assert.False(t, p.Fragment)
@@ -574,7 +581,7 @@ func BenchmarkParseV6(b *testing.B) {
} }
evilBytes := buffer.Bytes() evilBytes := buffer.Bytes()
for i := 0; i < 200; i++ { for range 200 {
evilBytes = append(evilBytes, hopHeader...) evilBytes = append(evilBytes, hopHeader...)
} }
evilBytes = append(evilBytes, lastHopHeader...) evilBytes = append(evilBytes, lastHopHeader...)
+5 -16
View File
@@ -7,26 +7,15 @@ import (
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
// BatchReadWriter extends io.ReadWriteCloser with batch I/O operations
type BatchReadWriter interface {
io.ReadWriteCloser
// BatchRead reads multiple packets at once
BatchRead(bufs [][]byte, sizes []int) (int, error)
// WriteBatch writes multiple packets at once
WriteBatch(bufs [][]byte, offset int) (int, error)
// BatchSize returns the optimal batch size for this device
BatchSize() int
}
type Device interface { type Device interface {
BatchReadWriter io.ReadWriteCloser
Activate() error Activate() error
Networks() []netip.Prefix Networks() []netip.Prefix
UnsafeNetworks() []netip.Prefix
UnsafeIPv4OriginAddress() netip.Prefix
SNATAddress() netip.Prefix
Name() string Name() string
RoutesFor(netip.Addr) routing.Gateways RoutesFor(netip.Addr) routing.Gateways
SupportsMultiqueue() bool SupportsMultiqueue() bool
NewMultiQueueReader() (BatchReadWriter, error) NewMultiQueueReader() (io.ReadWriteCloser, error)
} }
+89 -6
View File
@@ -1,7 +1,9 @@
package overlay package overlay
import ( import (
"crypto/rand"
"fmt" "fmt"
"io"
"net" "net"
"net/netip" "net/netip"
@@ -11,7 +13,6 @@ import (
) )
const DefaultMTU = 1300 const DefaultMTU = 1300
const VirtioNetHdrLen = 10 // Size of virtio_net_hdr structure
type NameError struct { type NameError struct {
Name string Name string
@@ -23,22 +24,22 @@ func (e *NameError) Error() string {
} }
// TODO: We may be able to remove routines // TODO: We may be able to remove routines
type DeviceFactory func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) type DeviceFactory func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, routines int) (Device, error)
func NewDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) { func NewDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, routines int) (Device, error) {
switch { switch {
case c.GetBool("tun.disabled", false): case c.GetBool("tun.disabled", false):
tun := newDisabledTun(vpnNetworks, c.GetInt("tun.tx_queue", 500), c.GetBool("stats.message_metrics", false), l) tun := newDisabledTun(vpnNetworks, c.GetInt("tun.tx_queue", 500), c.GetBool("stats.message_metrics", false), l)
return tun, nil return tun, nil
default: default:
return newTun(c, l, vpnNetworks, routines > 1) return newTun(c, l, vpnNetworks, unsafeNetworks, routines > 1)
} }
} }
func NewFdDeviceFromConfig(fd *int) DeviceFactory { func NewFdDeviceFromConfig(fd *int) DeviceFactory {
return func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) { return func(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, routines int) (Device, error) {
return newTunFromFd(c, l, *fd, vpnNetworks) return newTunFromFd(c, l, *fd, vpnNetworks, unsafeNetworks)
} }
} }
@@ -130,3 +131,85 @@ func selectGateway(dest netip.Prefix, gateways []netip.Prefix) (netip.Prefix, er
return netip.Prefix{}, fmt.Errorf("no gateway found for %v in the list of vpn networks", dest) return netip.Prefix{}, fmt.Errorf("no gateway found for %v in the list of vpn networks", dest)
} }
// genLinkLocal generates a random IPv4 link-local address.
// If randomizer is nil, it uses rand.Reader to find two random bytes
func genLinkLocal(randomizer io.Reader) netip.Prefix {
if randomizer == nil {
randomizer = rand.Reader
}
octets := []byte{169, 254, 0, 0}
_, _ = randomizer.Read(octets[2:4])
return coerceLinkLocal(octets)
}
func coerceLinkLocal(octets []byte) netip.Prefix {
if octets[3] == 0 {
octets[3] = 1 //please no .0 addresses
} else if octets[2] == 255 && octets[3] == 255 {
octets[3] = 254 //please no broadcast addresses
}
out, _ := netip.AddrFromSlice(octets)
return netip.PrefixFrom(out, 32)
}
// prepareUnsafeOriginAddr provides the IPv4 address used on IPv6-only clients that need to access IPv4 unsafe routes
func prepareUnsafeOriginAddr(d Device, l *logrus.Logger, c *config.C, routes []Route) netip.Prefix {
if !d.Networks()[0].Addr().Is6() {
return netip.Prefix{} //if we have an IPv4 assignment within the overlay, we don't need an unsafe origin address
}
needed := false
for _, route := range routes { //or if we have a route defined into an IPv4 range
if route.Cidr.Addr().Is4() {
needed = true //todo should this only apply to unsafe routes? almost certainly
break
}
}
if !needed {
return netip.Prefix{}
}
//todo better config name for sure
if a := c.GetString("tun.unsafe_origin_address_for_4over6", ""); a != "" {
out, err := netip.ParseAddr(a)
if err != nil {
l.WithField("value", a).WithError(err).Warn("failed to parse tun.unsafe_origin_address_for_4over6, will use a random value")
} else if !out.Is4() || !out.IsLinkLocalUnicast() {
l.WithField("value", out).Warn("tun.unsafe_origin_address_for_4over6 must be an IPv4 address")
} else if out.IsValid() {
return netip.PrefixFrom(out, 32)
}
}
return genLinkLocal(nil)
}
// prepareSnatAddr provides the address that an IPv6-only unsafe router should use to SNAT traffic before handing it to the operating system
func prepareSnatAddr(d Device, l *logrus.Logger, c *config.C) netip.Prefix {
if !d.Networks()[0].Addr().Is6() {
return netip.Prefix{} //if we have an IPv4 assignment within the overlay, we don't need a snat address
}
needed := false
for _, un := range d.UnsafeNetworks() { //if we are an unsafe router for an IPv4 range
if un.Addr().Is4() {
needed = true
break
}
}
if !needed {
return netip.Prefix{}
}
if a := c.GetString("tun.snat_address_for_4over6", ""); a != "" {
out, err := netip.ParseAddr(a)
if err != nil {
l.WithField("value", a).WithError(err).Warn("failed to parse tun.snat_address_for_4over6, will use a random value")
} else if !out.Is4() || !out.IsLinkLocalUnicast() {
l.WithField("value", out).Warn("tun.snat_address_for_4over6 must be an IPv4 address")
} else if out.IsValid() {
return netip.PrefixFrom(out, 32)
}
}
return genLinkLocal(nil)
}
+25 -31
View File
@@ -19,14 +19,16 @@ import (
type tun struct { type tun struct {
io.ReadWriteCloser io.ReadWriteCloser
fd int fd int
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
Routes atomic.Pointer[[]Route] unsafeNetworks []netip.Prefix
routeTree atomic.Pointer[bart.Table[routing.Gateways]] unsafeIPv4Origin netip.Prefix
l *logrus.Logger Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *logrus.Logger
} }
func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) { func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix) (*tun, error) {
// XXX Android returns an fd in non-blocking mode which is necessary for shutdown to work properly. // XXX Android returns an fd in non-blocking mode which is necessary for shutdown to work properly.
// Be sure not to call file.Fd() as it will set the fd to blocking mode. // Be sure not to call file.Fd() as it will set the fd to blocking mode.
file := os.NewFile(uintptr(deviceFd), "/dev/net/tun") file := os.NewFile(uintptr(deviceFd), "/dev/net/tun")
@@ -35,6 +37,7 @@ func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []net
ReadWriteCloser: file, ReadWriteCloser: file,
fd: deviceFd, fd: deviceFd,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks,
l: l, l: l,
} }
@@ -53,7 +56,7 @@ func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []net
return t, nil return t, nil
} }
func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ bool) (*tun, error) { func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ []netip.Prefix, _ bool) (*tun, error) {
return nil, fmt.Errorf("newTun not supported in Android") return nil, fmt.Errorf("newTun not supported in Android")
} }
@@ -76,6 +79,8 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -91,6 +96,18 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return "android" return "android"
} }
@@ -99,29 +116,6 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for android") return nil, fmt.Errorf("TODO: multiqueue not implemented for android")
} }
func (t *tun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
n, err := t.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
func (t *tun) WriteBatch(bufs [][]byte, offset int) (int, error) {
for i, buf := range bufs {
_, err := t.Write(buf[offset:])
if err != nil {
return i, err
}
}
return len(bufs), nil
}
func (t *tun) BatchSize() int {
return 1
}
+34 -36
View File
@@ -24,13 +24,15 @@ import (
type tun struct { type tun struct {
io.ReadWriteCloser io.ReadWriteCloser
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
DefaultMTU int unsafeNetworks []netip.Prefix
Routes atomic.Pointer[[]Route] unsafeIPv4Origin netip.Prefix
routeTree atomic.Pointer[bart.Table[routing.Gateways]] DefaultMTU int
linkAddr *netroute.LinkAddr Routes atomic.Pointer[[]Route]
l *logrus.Logger routeTree atomic.Pointer[bart.Table[routing.Gateways]]
linkAddr *netroute.LinkAddr
l *logrus.Logger
// cache out buffer since we need to prepend 4 bytes for tun metadata // cache out buffer since we need to prepend 4 bytes for tun metadata
out []byte out []byte
@@ -79,7 +81,7 @@ type ifreqAlias6 struct {
Lifetime addrLifetime Lifetime addrLifetime
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*tun, error) {
name := c.GetString("tun.dev", "") name := c.GetString("tun.dev", "")
ifIndex := -1 ifIndex := -1
if name != "" && name != "utun" { if name != "" && name != "utun" {
@@ -127,6 +129,7 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
ReadWriteCloser: os.NewFile(uintptr(fd), ""), ReadWriteCloser: os.NewFile(uintptr(fd), ""),
Device: name, Device: name,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks,
DefaultMTU: c.GetInt("tun.mtu", DefaultMTU), DefaultMTU: c.GetInt("tun.mtu", DefaultMTU),
l: l, l: l,
} }
@@ -153,7 +156,7 @@ func (t *tun) deviceBytes() (o [16]byte) {
return return
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in Darwin") return nil, fmt.Errorf("newTunFromFd not supported in Darwin")
} }
@@ -213,6 +216,11 @@ func (t *tun) Activate() error {
} }
} }
} }
if t.unsafeIPv4Origin.IsValid() && t.unsafeIPv4Origin.Addr().Is4() {
if err = t.activate4(t.unsafeIPv4Origin); err != nil {
return err
}
}
// Run the interface // Run the interface
ifrf.Flags = ifrf.Flags | unix.IFF_UP | unix.IFF_RUNNING ifrf.Flags = ifrf.Flags | unix.IFF_UP | unix.IFF_RUNNING
@@ -314,6 +322,10 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -545,6 +557,18 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return t.Device return t.Device
} }
@@ -553,32 +577,6 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for darwin") return nil, fmt.Errorf("TODO: multiqueue not implemented for darwin")
} }
// BatchRead reads a single packet (batch size 1 for non-Linux platforms)
func (t *tun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
n, err := t.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
// WriteBatch writes packets individually (no batching for non-Linux platforms)
func (t *tun) WriteBatch(bufs [][]byte, offset int) (int, error) {
for i, buf := range bufs {
_, err := t.Write(buf[offset:])
if err != nil {
return i, err
}
}
return len(bufs), nil
}
// BatchSize returns 1 for non-Linux platforms (no batching)
func (t *tun) BatchSize() int {
return 1
}
+12 -27
View File
@@ -52,6 +52,17 @@ func (t *disabledTun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (*disabledTun) UnsafeNetworks() []netip.Prefix {
return nil
}
func (*disabledTun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (*disabledTun) UnsafeIPv4OriginAddress() netip.Prefix {
return netip.Prefix{}
}
func (*disabledTun) Name() string { func (*disabledTun) Name() string {
return "disabled" return "disabled"
} }
@@ -109,36 +120,10 @@ func (t *disabledTun) SupportsMultiqueue() bool {
return true return true
} }
func (t *disabledTun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *disabledTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return t, nil return t, nil
} }
// BatchRead reads a single packet (batch size 1 for disabled tun)
func (t *disabledTun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
n, err := t.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
// WriteBatch writes packets individually (no batching for disabled tun)
func (t *disabledTun) WriteBatch(bufs [][]byte, offset int) (int, error) {
for i, buf := range bufs {
_, err := t.Write(buf[offset:])
if err != nil {
return i, err
}
}
return len(bufs), nil
}
// BatchSize returns 1 for disabled tun (no batching)
func (t *disabledTun) BatchSize() int {
return 1
}
func (t *disabledTun) Close() error { func (t *disabledTun) Close() error {
if t.read != nil { if t.read != nil {
close(t.read) close(t.read)
+36 -42
View File
@@ -7,6 +7,7 @@ import (
"bytes" "bytes"
"errors" "errors"
"fmt" "fmt"
"io"
"io/fs" "io/fs"
"net/netip" "net/netip"
"sync/atomic" "sync/atomic"
@@ -85,14 +86,16 @@ type ifreqAlias6 struct {
} }
type tun struct { type tun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
MTU int unsafeNetworks []netip.Prefix
Routes atomic.Pointer[[]Route] unsafeIPv4Origin netip.Prefix
routeTree atomic.Pointer[bart.Table[routing.Gateways]] MTU int
linkAddr *netroute.LinkAddr Routes atomic.Pointer[[]Route]
l *logrus.Logger routeTree atomic.Pointer[bart.Table[routing.Gateways]]
devFd int linkAddr *netroute.LinkAddr
l *logrus.Logger
devFd int
} }
func (t *tun) Read(to []byte) (int, error) { func (t *tun) Read(to []byte) (int, error) {
@@ -198,11 +201,11 @@ func (t *tun) Close() error {
return nil return nil
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in FreeBSD") return nil, fmt.Errorf("newTunFromFd not supported in FreeBSD")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*tun, error) {
// Try to open existing tun device // Try to open existing tun device
var fd int var fd int
var err error var err error
@@ -269,11 +272,12 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
} }
t := &tun{ t := &tun{
Device: deviceName, Device: deviceName,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
MTU: c.GetInt("tun.mtu", DefaultMTU), unsafeNetworks: unsafeNetworks,
l: l, MTU: c.GetInt("tun.mtu", DefaultMTU),
devFd: fd, l: l,
devFd: fd,
} }
err = t.reload(c, true) err = t.reload(c, true)
@@ -409,6 +413,10 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -445,6 +453,18 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return t.Device return t.Device
} }
@@ -453,36 +473,10 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for freebsd") return nil, fmt.Errorf("TODO: multiqueue not implemented for freebsd")
} }
// BatchRead reads a single packet (batch size 1 for FreeBSD)
func (t *tun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
n, err := t.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
// WriteBatch writes packets individually (no batching for FreeBSD)
func (t *tun) WriteBatch(bufs [][]byte, offset int) (int, error) {
for i, buf := range bufs {
_, err := t.Write(buf[offset:])
if err != nil {
return i, err
}
}
return len(bufs), nil
}
// BatchSize returns 1 for FreeBSD (no batching)
func (t *tun) BatchSize() int {
return 1
}
func (t *tun) addRoutes(logErrors bool) error { func (t *tun) addRoutes(logErrors bool) error {
routes := *t.Routes.Load() routes := *t.Routes.Load()
for _, r := range routes { for _, r := range routes {
+24 -30
View File
@@ -22,20 +22,23 @@ import (
type tun struct { type tun struct {
io.ReadWriteCloser io.ReadWriteCloser
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
Routes atomic.Pointer[[]Route] unsafeNetworks []netip.Prefix
routeTree atomic.Pointer[bart.Table[routing.Gateways]] unsafeIPv4Origin netip.Prefix
l *logrus.Logger Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *logrus.Logger
} }
func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ bool) (*tun, error) { func newTun(_ *config.C, _ *logrus.Logger, _ []netip.Prefix, _ []netip.Prefix, _ bool) (*tun, error) {
return nil, fmt.Errorf("newTun not supported in iOS") return nil, fmt.Errorf("newTun not supported in iOS")
} }
func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) { func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix) (*tun, error) {
file := os.NewFile(uintptr(deviceFd), "/dev/tun") file := os.NewFile(uintptr(deviceFd), "/dev/tun")
t := &tun{ t := &tun{
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks,
ReadWriteCloser: &tunReadCloser{f: file}, ReadWriteCloser: &tunReadCloser{f: file},
l: l, l: l,
} }
@@ -69,6 +72,8 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -147,6 +152,18 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return "iOS" return "iOS"
} }
@@ -155,29 +172,6 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for ios") return nil, fmt.Errorf("TODO: multiqueue not implemented for ios")
} }
func (t *tun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
n, err := t.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
func (t *tun) WriteBatch(bufs [][]byte, offset int) (int, error) {
for i, buf := range bufs {
_, err := t.Write(buf[offset:])
if err != nil {
return i, err
}
}
return len(bufs), nil
}
func (t *tun) BatchSize() int {
return 1
}
+109 -241
View File
@@ -9,6 +9,7 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@@ -21,20 +22,19 @@ import (
"github.com/slackhq/nebula/util" "github.com/slackhq/nebula/util"
"github.com/vishvananda/netlink" "github.com/vishvananda/netlink"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
wgtun "golang.zx2c4.com/wireguard/tun"
) )
type tun struct { type tun struct {
io.ReadWriteCloser io.ReadWriteCloser
wgDevice wgtun.Device fd int
fd int Device string
Device string vpnNetworks []netip.Prefix
vpnNetworks []netip.Prefix unsafeNetworks []netip.Prefix
MaxMTU int MaxMTU int
DefaultMTU int DefaultMTU int
TXQueueLen int TXQueueLen int
deviceIndex int deviceIndex int
ioctlFd uintptr ioctlFd uintptr
Routes atomic.Pointer[[]Route] Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]] routeTree atomic.Pointer[bart.Table[routing.Gateways]]
@@ -47,6 +47,9 @@ type tun struct {
routesFromSystem map[netip.Prefix]routing.Gateways routesFromSystem map[netip.Prefix]routing.Gateways
routesFromSystemLock sync.Mutex routesFromSystemLock sync.Mutex
snatAddr netip.Prefix
unsafeIPv4Origin netip.Prefix
l *logrus.Logger l *logrus.Logger
} }
@@ -54,6 +57,18 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return t.snatAddr
}
type ifReq struct { type ifReq struct {
Name [16]byte Name [16]byte
Flags uint16 Flags uint16
@@ -72,164 +87,74 @@ type ifreqQLEN struct {
pad [8]byte pad [8]byte
} }
// wgDeviceWrapper wraps a wireguard Device to implement io.ReadWriteCloser func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix) (*tun, error) {
// This allows multiqueue readers to use the same wireguard Device batching as the main device file := os.NewFile(uintptr(deviceFd), "/dev/net/tun")
type wgDeviceWrapper struct {
dev wgtun.Device
buf []byte // Reusable buffer for single packet reads
}
func (w *wgDeviceWrapper) Read(b []byte) (int, error) { t, err := newTunGeneric(c, l, file, vpnNetworks, unsafeNetworks)
// Use wireguard Device's batch API for single packet
bufs := [][]byte{b}
sizes := make([]int, 1)
n, err := w.dev.Read(bufs, sizes, 0)
if err != nil { if err != nil {
return 0, err
}
if n == 0 {
return 0, io.EOF
}
return sizes[0], nil
}
func (w *wgDeviceWrapper) Write(b []byte) (int, error) {
// Buffer b should have virtio header space (10 bytes) at the beginning
// The decrypted packet data starts at offset 10
// Pass the full buffer to WireGuard with offset=virtioNetHdrLen
bufs := [][]byte{b}
n, err := w.dev.Write(bufs, VirtioNetHdrLen)
if err != nil {
return 0, err
}
if n == 0 {
return 0, io.ErrShortWrite
}
return len(b), nil
}
func (w *wgDeviceWrapper) WriteBatch(bufs [][]byte, offset int) (int, error) {
// Pass all buffers to WireGuard's batch write
return w.dev.Write(bufs, offset)
}
func (w *wgDeviceWrapper) Close() error {
return w.dev.Close()
}
// BatchRead implements batching for multiqueue readers
func (w *wgDeviceWrapper) BatchRead(bufs [][]byte, sizes []int) (int, error) {
// The zero here is offset.
return w.dev.Read(bufs, sizes, 0)
}
// BatchSize returns the optimal batch size
func (w *wgDeviceWrapper) BatchSize() int {
return w.dev.BatchSize()
}
func newTunFromFd(c *config.C, l *logrus.Logger, deviceFd int, vpnNetworks []netip.Prefix) (*tun, error) {
wgDev, name, err := wgtun.CreateUnmonitoredTUNFromFD(deviceFd)
if err != nil {
return nil, fmt.Errorf("failed to create TUN from FD: %w", err)
}
file := wgDev.File()
t, err := newTunGeneric(c, l, file, vpnNetworks)
if err != nil {
_ = wgDev.Close()
return nil, err return nil, err
} }
t.wgDevice = wgDev t.Device = "tun0"
t.Device = name
return t, nil return t, nil
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, multiqueue bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, multiqueue bool) (*tun, error) {
// Check if /dev/net/tun exists, create if needed (for docker containers)
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
if err := os.MkdirAll("/dev/net", 0755); err != nil {
return nil, fmt.Errorf("/dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %w", err)
}
if err := unix.Mknod("/dev/net/tun", unix.S_IFCHR|0600, int(unix.Mkdev(10, 200))); err != nil {
return nil, fmt.Errorf("failed to create /dev/net/tun: %w", err)
}
}
devName := c.GetString("tun.dev", "")
mtu := c.GetInt("tun.mtu", DefaultMTU)
// Create TUN device manually to support multiqueue
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0) fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
if err != nil { if err != nil {
return nil, err // If /dev/net/tun doesn't exist, try to create it (will happen in docker)
if os.IsNotExist(err) {
err = os.MkdirAll("/dev/net", 0755)
if err != nil {
return nil, fmt.Errorf("/dev/net/tun doesn't exist, failed to mkdir -p /dev/net: %w", err)
}
err = unix.Mknod("/dev/net/tun", unix.S_IFCHR|0600, int(unix.Mkdev(10, 200)))
if err != nil {
return nil, fmt.Errorf("failed to create /dev/net/tun: %w", err)
}
fd, err = unix.Open("/dev/net/tun", os.O_RDWR, 0)
if err != nil {
return nil, fmt.Errorf("created /dev/net/tun, but still failed: %w", err)
}
} else {
return nil, err
}
} }
var req ifReq var req ifReq
req.Flags = uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_VNET_HDR) req.Flags = uint16(unix.IFF_TUN | unix.IFF_NO_PI)
if multiqueue { if multiqueue {
req.Flags |= unix.IFF_MULTI_QUEUE req.Flags |= unix.IFF_MULTI_QUEUE
} }
copy(req.Name[:], devName) nameStr := c.GetString("tun.dev", "")
copy(req.Name[:], nameStr)
if err = ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil { if err = ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
unix.Close(fd) return nil, &NameError{
return nil, err Name: nameStr,
} Underlying: err,
}
// Set nonblocking
if err = unix.SetNonblock(fd, true); err != nil {
unix.Close(fd)
return nil, err
}
// Enable TCP and UDP offload (TSO/GRO) for performance
// This allows the kernel to handle segmentation/coalescing
const (
tunTCPOffloads = unix.TUN_F_CSUM | unix.TUN_F_TSO4 | unix.TUN_F_TSO6
tunUDPOffloads = unix.TUN_F_USO4 | unix.TUN_F_USO6
)
offloads := tunTCPOffloads | tunUDPOffloads
if err = unix.IoctlSetInt(fd, unix.TUNSETOFFLOAD, offloads); err != nil {
// Log warning but don't fail - offload is optional
l.WithError(err).Warn("Failed to enable TUN offload (TSO/GRO), performance may be reduced")
} }
name := strings.Trim(string(req.Name[:]), "\x00")
file := os.NewFile(uintptr(fd), "/dev/net/tun") file := os.NewFile(uintptr(fd), "/dev/net/tun")
t, err := newTunGeneric(c, l, file, vpnNetworks, unsafeNetworks)
// Create wireguard device from file descriptor
wgDev, err := wgtun.CreateTUNFromFile(file, mtu)
if err != nil { if err != nil {
file.Close()
return nil, fmt.Errorf("failed to create TUN from file: %w", err)
}
name, err := wgDev.Name()
if err != nil {
_ = wgDev.Close()
return nil, fmt.Errorf("failed to get TUN device name: %w", err)
}
// file is now owned by wgDev, get a new reference
file = wgDev.File()
t, err := newTunGeneric(c, l, file, vpnNetworks)
if err != nil {
_ = wgDev.Close()
return nil, err return nil, err
} }
t.wgDevice = wgDev
t.Device = name t.Device = name
return t, nil return t, nil
} }
func newTunGeneric(c *config.C, l *logrus.Logger, file *os.File, vpnNetworks []netip.Prefix) (*tun, error) { func newTunGeneric(c *config.C, l *logrus.Logger, file *os.File, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix) (*tun, error) {
t := &tun{ t := &tun{
ReadWriteCloser: file, ReadWriteCloser: file,
fd: int(file.Fd()), fd: int(file.Fd()),
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
unsafeNetworks: unsafeNetworks,
TXQueueLen: c.GetInt("tun.tx_queue", 500), TXQueueLen: c.GetInt("tun.tx_queue", 500),
useSystemRoutes: c.GetBool("tun.use_system_route_table", false), useSystemRoutes: c.GetBool("tun.use_system_route_table", false),
useSystemRoutesBufferSize: c.GetInt("tun.use_system_route_table_buffer_size", 0), useSystemRoutesBufferSize: c.GetInt("tun.use_system_route_table_buffer_size", 0),
@@ -262,6 +187,11 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes) //todo MUST be different from t.snatAddr!
t.snatAddr = prepareSnatAddr(t, t.l, c)
}
routeTree, err := makeRouteTree(t.l, routes, true) routeTree, err := makeRouteTree(t.l, routes, true)
if err != nil { if err != nil {
return err return err
@@ -330,44 +260,22 @@ func (t *tun) SupportsMultiqueue() bool {
return true return true
} }
func (t *tun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0) fd, err := unix.Open("/dev/net/tun", os.O_RDWR, 0)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var req ifReq var req ifReq
// MUST match the flags used in newTun - includes IFF_VNET_HDR req.Flags = uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_MULTI_QUEUE)
req.Flags = uint16(unix.IFF_TUN | unix.IFF_NO_PI | unix.IFF_VNET_HDR | unix.IFF_MULTI_QUEUE)
copy(req.Name[:], t.Device) copy(req.Name[:], t.Device)
if err = ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil { if err = ioctl(uintptr(fd), uintptr(unix.TUNSETIFF), uintptr(unsafe.Pointer(&req))); err != nil {
unix.Close(fd)
return nil, err return nil, err
} }
// Set nonblocking mode - CRITICAL for proper netpoller integration
if err = unix.SetNonblock(fd, true); err != nil {
unix.Close(fd)
return nil, err
}
// Get MTU from main device
mtu := t.MaxMTU
if mtu == 0 {
mtu = DefaultMTU
}
file := os.NewFile(uintptr(fd), "/dev/net/tun") file := os.NewFile(uintptr(fd), "/dev/net/tun")
// Create wireguard Device from the file descriptor (just like the main device) return file, nil
wgDev, err := wgtun.CreateTUNFromFile(file, mtu)
if err != nil {
file.Close()
return nil, fmt.Errorf("failed to create multiqueue TUN device: %w", err)
}
// Return a wrapper that uses the wireguard Device for all I/O
return &wgDeviceWrapper{dev: wgDev}, nil
} }
func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways { func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
@@ -375,68 +283,7 @@ func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
return r return r
} }
func (t *tun) Read(b []byte) (int, error) {
if t.wgDevice != nil {
// Use wireguard device which handles virtio headers internally
bufs := [][]byte{b}
sizes := make([]int, 1)
n, err := t.wgDevice.Read(bufs, sizes, 0)
if err != nil {
return 0, err
}
if n == 0 {
return 0, io.EOF
}
return sizes[0], nil
}
// Fallback: direct read from file (shouldn't happen in normal operation)
return t.ReadWriteCloser.Read(b)
}
// BatchRead reads multiple packets at once for improved performance
// bufs: slice of buffers to read into
// sizes: slice that will be filled with packet sizes
// Returns number of packets read
func (t *tun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
if t.wgDevice != nil {
return t.wgDevice.Read(bufs, sizes, 0)
}
// Fallback: single packet read
n, err := t.ReadWriteCloser.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
// BatchSize returns the optimal number of packets to read/write in a batch
func (t *tun) BatchSize() int {
if t.wgDevice != nil {
return t.wgDevice.BatchSize()
}
return 1
}
func (t *tun) Write(b []byte) (int, error) { func (t *tun) Write(b []byte) (int, error) {
if t.wgDevice != nil {
// Buffer b should have virtio header space (10 bytes) at the beginning
// The decrypted packet data starts at offset 10
// Pass the full buffer to WireGuard with offset=virtioNetHdrLen
bufs := [][]byte{b}
n, err := t.wgDevice.Write(bufs, VirtioNetHdrLen)
if err != nil {
return 0, err
}
if n == 0 {
return 0, io.ErrShortWrite
}
return len(b), nil
}
// Fallback: direct write (shouldn't happen in normal operation)
var nn int var nn int
maximum := len(b) maximum := len(b)
@@ -459,22 +306,6 @@ func (t *tun) Write(b []byte) (int, error) {
} }
} }
// WriteBatch writes multiple packets to the TUN device in a single syscall
func (t *tun) WriteBatch(bufs [][]byte, offset int) (int, error) {
if t.wgDevice != nil {
return t.wgDevice.Write(bufs, offset)
}
// Fallback: write individually (shouldn't happen in normal operation)
for i, buf := range bufs {
_, err := t.Write(buf)
if err != nil {
return i, err
}
}
return len(bufs), nil
}
func (t *tun) deviceBytes() (o [16]byte) { func (t *tun) deviceBytes() (o [16]byte) {
for i, c := range t.Device { for i, c := range t.Device {
o[i] = byte(c) o[i] = byte(c)
@@ -504,6 +335,17 @@ func (t *tun) addIPs(link netlink.Link) error {
} }
} }
if t.unsafeIPv4Origin.IsValid() {
newAddrs = append(newAddrs, &netlink.Addr{
IPNet: &net.IPNet{
IP: t.unsafeIPv4Origin.Addr().AsSlice(),
Mask: net.CIDRMask(t.unsafeIPv4Origin.Bits(), t.unsafeIPv4Origin.Addr().BitLen()),
},
Label: t.unsafeIPv4Origin.Addr().Zone(),
})
t.l.WithField("address", t.unsafeIPv4Origin).Info("Adding origin address for IPv4 unsafe_routes")
}
//add all new addresses //add all new addresses
for i := range newAddrs { for i := range newAddrs {
//AddrReplace still adds new IPs, but if their properties change it will change them as well //AddrReplace still adds new IPs, but if their properties change it will change them as well
@@ -591,7 +433,13 @@ func (t *tun) Activate() error {
//set route MTU //set route MTU
for i := range t.vpnNetworks { for i := range t.vpnNetworks {
if err = t.setDefaultRoute(t.vpnNetworks[i]); err != nil { if err = t.setDefaultRoute(t.vpnNetworks[i]); err != nil {
return fmt.Errorf("failed to set default route MTU: %w", err) return fmt.Errorf("failed to set default route MTU for %s: %w", t.vpnNetworks[i], err)
}
}
if t.unsafeIPv4Origin.IsValid() {
if err = t.setDefaultRoute(t.unsafeIPv4Origin); err != nil {
return fmt.Errorf("failed to set default route MTU for %s: %w", t.unsafeIPv4Origin, err)
} }
} }
@@ -618,6 +466,23 @@ func (t *tun) setMTU() {
} }
} }
func (t *tun) setSnatRoute() error {
dr := &net.IPNet{
IP: t.snatAddr.Masked().Addr().AsSlice(),
Mask: net.CIDRMask(t.snatAddr.Bits(), t.snatAddr.Addr().BitLen()),
}
nr := netlink.Route{
LinkIndex: t.deviceIndex,
Dst: dr,
Scope: unix.RT_SCOPE_LINK,
//Protocol: unix.RTPROT_KERNEL,
Table: unix.RT_TABLE_MAIN,
Type: unix.RTN_UNICAST,
}
return netlink.RouteReplace(&nr)
}
func (t *tun) setDefaultRoute(cidr netip.Prefix) error { func (t *tun) setDefaultRoute(cidr netip.Prefix) error {
dr := &net.IPNet{ dr := &net.IPNet{
IP: cidr.Masked().Addr().AsSlice(), IP: cidr.Masked().Addr().AsSlice(),
@@ -694,6 +559,13 @@ func (t *tun) addRoutes(logErrors bool) error {
} }
} }
if t.snatAddr.IsValid() {
//at least for Linux, we need to set a return route for the SNATted traffic in order to satisfy the reverse-path filter,
//and to help the kernel deliver our reply traffic to the tun device.
//however, it is important that we do not actually /assign/ the SNAT address,
//since link-local addresses will not be routed between interfaces without significant trickery.
return t.setSnatRoute()
}
return nil return nil
} }
@@ -902,10 +774,6 @@ func (t *tun) Close() error {
close(t.routeChan) close(t.routeChan)
} }
if t.wgDevice != nil {
_ = t.wgDevice.Close()
}
if t.ReadWriteCloser != nil { if t.ReadWriteCloser != nil {
_ = t.ReadWriteCloser.Close() _ = t.ReadWriteCloser.Close()
} }
+30 -34
View File
@@ -6,6 +6,7 @@ package overlay
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"net/netip" "net/netip"
"os" "os"
"regexp" "regexp"
@@ -57,23 +58,25 @@ type addrLifetime struct {
} }
type tun struct { type tun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
MTU int unsafeNetworks []netip.Prefix
Routes atomic.Pointer[[]Route] unsafeIPv4Origin netip.Prefix
routeTree atomic.Pointer[bart.Table[routing.Gateways]] MTU int
l *logrus.Logger Routes atomic.Pointer[[]Route]
f *os.File routeTree atomic.Pointer[bart.Table[routing.Gateways]]
fd int l *logrus.Logger
f *os.File
fd int
} }
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`) var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in NetBSD") return nil, fmt.Errorf("newTunFromFd not supported in NetBSD")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*tun, error) {
// Try to open tun device // Try to open tun device
var err error var err error
deviceName := c.GetString("tun.dev", "") deviceName := c.GetString("tun.dev", "")
@@ -349,6 +352,10 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -385,6 +392,18 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return t.Device return t.Device
} }
@@ -393,33 +412,10 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for netbsd") return nil, fmt.Errorf("TODO: multiqueue not implemented for netbsd")
} }
func (t *tun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
n, err := t.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
func (t *tun) WriteBatch(bufs [][]byte, offset int) (int, error) {
for i, buf := range bufs {
_, err := t.Write(buf[offset:])
if err != nil {
return i, err
}
}
return len(bufs), nil
}
func (t *tun) BatchSize() int {
return 1
}
func (t *tun) addRoutes(logErrors bool) error { func (t *tun) addRoutes(logErrors bool) error {
routes := *t.Routes.Load() routes := *t.Routes.Load()
+37 -40
View File
@@ -6,6 +6,7 @@ package overlay
import ( import (
"errors" "errors"
"fmt" "fmt"
"io"
"net/netip" "net/netip"
"os" "os"
"regexp" "regexp"
@@ -48,25 +49,27 @@ type ifreq struct {
} }
type tun struct { type tun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
MTU int unsafeNetworks []netip.Prefix
Routes atomic.Pointer[[]Route] unsafeIPv4Origin netip.Prefix
routeTree atomic.Pointer[bart.Table[routing.Gateways]] MTU int
l *logrus.Logger Routes atomic.Pointer[[]Route]
f *os.File routeTree atomic.Pointer[bart.Table[routing.Gateways]]
fd int l *logrus.Logger
f *os.File
fd int
// cache out buffer since we need to prepend 4 bytes for tun metadata // cache out buffer since we need to prepend 4 bytes for tun metadata
out []byte out []byte
} }
var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`) var deviceNameRE = regexp.MustCompile(`^tun[0-9]+$`)
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*tun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*tun, error) {
return nil, fmt.Errorf("newTunFromFd not supported in openbsd") return nil, fmt.Errorf("newTunFromFd not supported in openbsd")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*tun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*tun, error) {
// Try to open tun device // Try to open tun device
var err error var err error
deviceName := c.GetString("tun.dev", "") deviceName := c.GetString("tun.dev", "")
@@ -88,12 +91,13 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
} }
t := &tun{ t := &tun{
f: os.NewFile(uintptr(fd), ""), f: os.NewFile(uintptr(fd), ""),
fd: fd, fd: fd,
Device: deviceName, Device: deviceName,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
MTU: c.GetInt("tun.mtu", DefaultMTU), unsafeNetworks: unsafeNetworks,
l: l, MTU: c.GetInt("tun.mtu", DefaultMTU),
l: l,
} }
err = t.reload(c, true) err = t.reload(c, true)
@@ -269,6 +273,10 @@ func (t *tun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -305,6 +313,18 @@ func (t *tun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *tun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *tun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *tun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *tun) Name() string { func (t *tun) Name() string {
return t.Device return t.Device
} }
@@ -313,33 +333,10 @@ func (t *tun) SupportsMultiqueue() bool {
return false return false
} }
func (t *tun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for openbsd") return nil, fmt.Errorf("TODO: multiqueue not implemented for openbsd")
} }
func (t *tun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
n, err := t.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
func (t *tun) WriteBatch(bufs [][]byte, offset int) (int, error) {
for i, buf := range bufs {
_, err := t.Write(buf[offset:])
if err != nil {
return i, err
}
}
return len(bufs), nil
}
func (t *tun) BatchSize() int {
return 1
}
func (t *tun) addRoutes(logErrors bool) error { func (t *tun) addRoutes(logErrors bool) error {
routes := *t.Routes.Load() routes := *t.Routes.Load()
+179
View File
@@ -0,0 +1,179 @@
package overlay
import (
"io"
"net/netip"
"testing"
"github.com/sirupsen/logrus"
"github.com/slackhq/nebula/config"
"github.com/slackhq/nebula/routing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockDevice is a minimal Device implementation for testing prepareUnsafeOriginAddr.
type mockDevice struct {
networks []netip.Prefix
unsafeNetworks []netip.Prefix
snatAddr netip.Prefix
unsafeSnatAddr netip.Prefix
}
func (d *mockDevice) Read([]byte) (int, error) { return 0, nil }
func (d *mockDevice) Write([]byte) (int, error) { return 0, nil }
func (d *mockDevice) Close() error { return nil }
func (d *mockDevice) Activate() error { return nil }
func (d *mockDevice) Networks() []netip.Prefix { return d.networks }
func (d *mockDevice) UnsafeNetworks() []netip.Prefix { return d.unsafeNetworks }
func (d *mockDevice) SNATAddress() netip.Prefix { return d.snatAddr }
func (d *mockDevice) UnsafeIPv4OriginAddress() netip.Prefix { return d.unsafeSnatAddr }
func (d *mockDevice) Name() string { return "mock" }
func (d *mockDevice) RoutesFor(netip.Addr) routing.Gateways { return routing.Gateways{} }
func (d *mockDevice) SupportsMultiqueue() bool { return false }
func (d *mockDevice) NewMultiQueueReader() (io.ReadWriteCloser, error) { return nil, nil }
func TestPrepareSnatAddr_V4Primary_NoSnat(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// If the device has an IPv4 primary address, no SNAT needed
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("10.0.0.1/24")},
}
result := prepareUnsafeOriginAddr(d, l, c, nil)
assert.Equal(t, netip.Prefix{}, result, "should not assign SNAT addr when device has IPv4 primary")
}
func TestPrepareSnatAddr_V6Primary_NoUnsafeOrRoutes(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// IPv6 primary but no unsafe networks or IPv4 routes
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
}
result := prepareUnsafeOriginAddr(d, l, c, nil)
assert.Equal(t, netip.Prefix{}, result, "should not assign SNAT addr without IPv4 unsafe networks or routes")
}
func TestPrepareSnatAddr_V6Primary_WithV4Unsafe(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// IPv6 primary with IPv4 unsafe network -> should get SNAT addr
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
}
result := prepareSnatAddr(d, l, c)
require.True(t, result.IsValid(), "should assign SNAT addr")
assert.True(t, result.Addr().Is4(), "SNAT addr should be IPv4")
assert.True(t, result.Addr().IsLinkLocalUnicast(), "SNAT addr should be link-local")
assert.Equal(t, 32, result.Bits(), "SNAT addr should be /32")
result = prepareUnsafeOriginAddr(d, l, c, nil)
require.False(t, result.IsValid(), "no routes = no origin addr needed")
}
func TestPrepareUnsafeOriginAddr_V6Primary_WithV4Route(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// IPv6 primary with IPv4 route -> should get SNAT addr
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
}
routes := []Route{
{Cidr: netip.MustParsePrefix("10.0.0.0/8")},
}
result := prepareUnsafeOriginAddr(d, l, c, routes)
require.True(t, result.IsValid(), "should assign SNAT addr when IPv4 route exists")
assert.True(t, result.Addr().Is4())
assert.True(t, result.Addr().IsLinkLocalUnicast())
result = prepareSnatAddr(d, l, c)
require.False(t, result.IsValid(), "no UnsafeNetworks = no snat addr needed")
}
func TestPrepareSnatAddr_V6Primary_V6UnsafeOnly(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
// IPv6 primary with only IPv6 unsafe network -> no SNAT needed
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("fd01::/64")},
}
result := prepareUnsafeOriginAddr(d, l, c, nil)
assert.Equal(t, netip.Prefix{}, result, "should not assign SNAT addr for IPv6-only unsafe networks")
}
func TestPrepareSnatAddr_ManualAddress(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
c.Settings["tun"] = map[string]any{
"snat_address_for_4over6": "169.254.42.42",
}
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
}
result := prepareSnatAddr(d, l, c)
require.True(t, result.IsValid())
assert.Equal(t, netip.MustParseAddr("169.254.42.42"), result.Addr())
assert.Equal(t, 32, result.Bits())
}
func TestPrepareSnatAddr_InvalidManualAddress_Fallback(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
c.Settings["tun"] = map[string]any{
"snat_address_for_4over6": "not-an-ip",
}
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
}
result := prepareSnatAddr(d, l, c)
// Should fall back to auto-assignment
require.True(t, result.IsValid(), "should fall back to auto-assigned address")
assert.True(t, result.Addr().Is4())
assert.True(t, result.Addr().IsLinkLocalUnicast())
}
func TestPrepareSnatAddr_AutoGenerated_Range(t *testing.T) {
l := logrus.New()
l.SetLevel(logrus.PanicLevel)
c := config.NewC(l)
d := &mockDevice{
networks: []netip.Prefix{netip.MustParsePrefix("fd00::1/128")},
unsafeNetworks: []netip.Prefix{netip.MustParsePrefix("192.168.0.0/16")},
}
// Generate several addresses and verify they're all in the expected range
for i := 0; i < 100; i++ {
result := prepareSnatAddr(d, l, c)
require.True(t, result.IsValid())
addr := result.Addr()
octets := addr.As4()
assert.Equal(t, byte(169), octets[0], "first octet should be 169")
assert.Equal(t, byte(254), octets[1], "second octet should be 254")
// Should not have .0 in the last octet
assert.NotEqual(t, byte(0), octets[3], "last octet should not be 0")
// Should not be 169.254.255.255 (broadcast)
if octets[2] == 255 {
assert.NotEqual(t, byte(255), octets[3], "should not be broadcast address")
}
}
}
+37
View File
@@ -0,0 +1,37 @@
package overlay
import (
"bytes"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
func TestLinkLocal(t *testing.T) {
r := bytes.NewReader([]byte{42, 99})
result := genLinkLocal(r)
assert.Equal(t, netip.MustParsePrefix("169.254.42.99/32"), result, "genLinkLocal with a deterministic randomizer")
result = genLinkLocal(nil)
assert.True(t, result.IsValid(), "genLinkLocal with nil randomizer should be valid")
assert.True(t, result.Addr().IsLinkLocalUnicast(), "genLinkLocal with nil randomizer should be link-local")
result = coerceLinkLocal([]byte{169, 254, 100, 50})
assert.Equal(t, netip.MustParsePrefix("169.254.100.50/32"), result, "coerceLinkLocal should pass through normal values")
result = coerceLinkLocal([]byte{169, 254, 0, 0})
assert.Equal(t, netip.MustParsePrefix("169.254.0.1/32"), result, "coerceLinkLocal should bump .0 last octet to .1")
result = coerceLinkLocal([]byte{169, 254, 255, 255})
assert.Equal(t, netip.MustParsePrefix("169.254.255.254/32"), result, "coerceLinkLocal should bump broadcast 255.255 to 255.254")
result = coerceLinkLocal([]byte{169, 254, 0, 1})
assert.Equal(t, netip.MustParsePrefix("169.254.0.1/32"), result, "coerceLinkLocal should leave .1 last octet unchanged")
result = coerceLinkLocal([]byte{169, 254, 255, 254})
assert.Equal(t, netip.MustParsePrefix("169.254.255.254/32"), result, "coerceLinkLocal should leave 255.254 unchanged")
result = coerceLinkLocal([]byte{169, 254, 255, 100})
assert.Equal(t, netip.MustParsePrefix("169.254.255.100/32"), result, "coerceLinkLocal should leave 255.100 unchanged")
}
+32 -45
View File
@@ -17,18 +17,21 @@ import (
) )
type TestTun struct { type TestTun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
Routes []Route unsafeNetworks []netip.Prefix
routeTree *bart.Table[routing.Gateways] snatAddr netip.Prefix
l *logrus.Logger unsafeIPv4Origin netip.Prefix
Routes []Route
routeTree *bart.Table[routing.Gateways]
l *logrus.Logger
closed atomic.Bool closed atomic.Bool
rxPackets chan []byte // Packets to receive into nebula rxPackets chan []byte // Packets to receive into nebula
TxPackets chan []byte // Packets transmitted outside by nebula TxPackets chan []byte // Packets transmitted outside by nebula
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*TestTun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*TestTun, error) {
_, routes, err := getAllRoutesFromConfig(c, vpnNetworks, true) _, routes, err := getAllRoutesFromConfig(c, vpnNetworks, true)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -38,18 +41,22 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
return nil, err return nil, err
} }
return &TestTun{ tt := &TestTun{
Device: c.GetString("tun.dev", ""), Device: c.GetString("tun.dev", ""),
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
Routes: routes, unsafeNetworks: unsafeNetworks,
routeTree: routeTree, Routes: routes,
l: l, routeTree: routeTree,
rxPackets: make(chan []byte, 10), l: l,
TxPackets: make(chan []byte, 10), rxPackets: make(chan []byte, 10),
}, nil TxPackets: make(chan []byte, 10),
}
tt.unsafeIPv4Origin = prepareUnsafeOriginAddr(tt, l, c, routes)
tt.snatAddr = prepareSnatAddr(tt, tt.l, c)
return tt, nil
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (*TestTun, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (*TestTun, error) {
return nil, fmt.Errorf("newTunFromFd not supported") return nil, fmt.Errorf("newTunFromFd not supported")
} }
@@ -109,12 +116,8 @@ func (t *TestTun) Write(b []byte) (n int, err error) {
return 0, io.ErrClosedPipe return 0, io.ErrClosedPipe
} }
// Skip virtio header (consistent with production Linux tun) packet := make([]byte, len(b), len(b))
// The buffer b has VirtioNetHdrLen bytes of header followed by the actual packet copy(packet, b)
data := b[VirtioNetHdrLen:]
packet := make([]byte, len(data))
copy(packet, data)
t.TxPackets <- packet t.TxPackets <- packet
return len(b), nil return len(b), nil
} }
@@ -140,34 +143,18 @@ func (t *TestTun) SupportsMultiqueue() bool {
return false return false
} }
func (t *TestTun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *TestTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented") return nil, fmt.Errorf("TODO: multiqueue not implemented")
} }
func (t *TestTun) BatchRead(bufs [][]byte, sizes []int) (int, error) { func (t *TestTun) UnsafeNetworks() []netip.Prefix {
n, err := t.Read(bufs[0]) return t.unsafeNetworks
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
} }
func (t *TestTun) WriteBatch(bufs [][]byte, offset int) (int, error) { func (t *TestTun) UnsafeIPv4OriginAddress() netip.Prefix {
if t.closed.Load() { return t.unsafeIPv4Origin
return 0, io.ErrClosedPipe
}
for _, buf := range bufs {
// Strip the header at offset and send directly to channel
data := buf[offset:]
packet := make([]byte, len(data))
copy(packet, data)
t.TxPackets <- packet
}
return len(bufs), nil
} }
func (t *TestTun) BatchSize() int { func (t *TestTun) SNATAddress() netip.Prefix {
return 1 return t.snatAddr
} }
+39 -40
View File
@@ -6,6 +6,7 @@ package overlay
import ( import (
"crypto" "crypto"
"fmt" "fmt"
"io"
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
@@ -27,21 +28,23 @@ import (
const tunGUIDLabel = "Fixed Nebula Windows GUID v1" const tunGUIDLabel = "Fixed Nebula Windows GUID v1"
type winTun struct { type winTun struct {
Device string Device string
vpnNetworks []netip.Prefix vpnNetworks []netip.Prefix
MTU int unsafeNetworks []netip.Prefix
Routes atomic.Pointer[[]Route] unsafeIPv4Origin netip.Prefix
routeTree atomic.Pointer[bart.Table[routing.Gateways]] MTU int
l *logrus.Logger Routes atomic.Pointer[[]Route]
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
l *logrus.Logger
tun *wintun.NativeTun tun *wintun.NativeTun
} }
func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix) (Device, error) { func newTunFromFd(_ *config.C, _ *logrus.Logger, _ int, _ []netip.Prefix, _ []netip.Prefix) (Device, error) {
return nil, fmt.Errorf("newTunFromFd not supported in Windows") return nil, fmt.Errorf("newTunFromFd not supported in Windows")
} }
func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (*winTun, error) { func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, _ bool) (*winTun, error) {
err := checkWinTunExists() err := checkWinTunExists()
if err != nil { if err != nil {
return nil, fmt.Errorf("can not load the wintun driver: %w", err) return nil, fmt.Errorf("can not load the wintun driver: %w", err)
@@ -54,10 +57,11 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, _ bool) (
} }
t := &winTun{ t := &winTun{
Device: deviceName, Device: deviceName,
vpnNetworks: vpnNetworks, vpnNetworks: vpnNetworks,
MTU: c.GetInt("tun.mtu", DefaultMTU), unsafeNetworks: unsafeNetworks,
l: l, MTU: c.GetInt("tun.mtu", DefaultMTU),
l: l,
} }
err = t.reload(c, true) err = t.reload(c, true)
@@ -101,6 +105,10 @@ func (t *winTun) reload(c *config.C, initial bool) error {
return nil return nil
} }
if initial {
t.unsafeIPv4Origin = prepareUnsafeOriginAddr(t, t.l, c, routes)
}
routeTree, err := makeRouteTree(t.l, routes, false) routeTree, err := makeRouteTree(t.l, routes, false)
if err != nil { if err != nil {
return err return err
@@ -131,7 +139,12 @@ func (t *winTun) reload(c *config.C, initial bool) error {
func (t *winTun) Activate() error { func (t *winTun) Activate() error {
luid := winipcfg.LUID(t.tun.LUID()) luid := winipcfg.LUID(t.tun.LUID())
err := luid.SetIPAddresses(t.vpnNetworks) prefixes := t.vpnNetworks
if t.unsafeIPv4Origin.IsValid() {
prefixes = append(prefixes, t.unsafeIPv4Origin)
}
err := luid.SetIPAddresses(prefixes)
if err != nil { if err != nil {
return fmt.Errorf("failed to set address: %w", err) return fmt.Errorf("failed to set address: %w", err)
} }
@@ -224,6 +237,18 @@ func (t *winTun) Networks() []netip.Prefix {
return t.vpnNetworks return t.vpnNetworks
} }
func (t *winTun) UnsafeNetworks() []netip.Prefix {
return t.unsafeNetworks
}
func (t *winTun) UnsafeIPv4OriginAddress() netip.Prefix {
return t.unsafeIPv4Origin
}
func (t *winTun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (t *winTun) Name() string { func (t *winTun) Name() string {
return t.Device return t.Device
} }
@@ -240,36 +265,10 @@ func (t *winTun) SupportsMultiqueue() bool {
return false return false
} }
func (t *winTun) NewMultiQueueReader() (BatchReadWriter, error) { func (t *winTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, fmt.Errorf("TODO: multiqueue not implemented for windows") return nil, fmt.Errorf("TODO: multiqueue not implemented for windows")
} }
// BatchRead reads a single packet (batch size 1 for Windows)
func (t *winTun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
n, err := t.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
// WriteBatch writes packets individually (no batching for Windows)
func (t *winTun) WriteBatch(bufs [][]byte, offset int) (int, error) {
for i, buf := range bufs {
_, err := t.Write(buf[offset:])
if err != nil {
return i, err
}
}
return len(bufs), nil
}
// BatchSize returns 1 for Windows (no batching)
func (t *winTun) BatchSize() int {
return 1
}
func (t *winTun) Close() error { func (t *winTun) Close() error {
// It seems that the Windows networking stack doesn't like it when we destroy interfaces that have active routes, // It seems that the Windows networking stack doesn't like it when we destroy interfaces that have active routes,
// so to be certain, just remove everything before destroying. // so to be certain, just remove everything before destroying.
+13 -28
View File
@@ -9,7 +9,7 @@ import (
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
func NewUserDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, routines int) (Device, error) { func NewUserDeviceFromConfig(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, unsafeNetworks []netip.Prefix, routines int) (Device, error) {
return NewUserDevice(vpnNetworks) return NewUserDevice(vpnNetworks)
} }
@@ -36,6 +36,17 @@ type UserDevice struct {
inboundWriter *io.PipeWriter inboundWriter *io.PipeWriter
} }
func (d *UserDevice) UnsafeNetworks() []netip.Prefix {
return nil
}
func (d *UserDevice) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (d *UserDevice) UnsafeIPv4OriginAddress() netip.Prefix {
return netip.Prefix{}
}
func (d *UserDevice) Activate() error { func (d *UserDevice) Activate() error {
return nil return nil
} }
@@ -50,36 +61,10 @@ func (d *UserDevice) SupportsMultiqueue() bool {
return true return true
} }
func (d *UserDevice) NewMultiQueueReader() (BatchReadWriter, error) { func (d *UserDevice) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return d, nil return d, nil
} }
// BatchRead reads a single packet (batch size 1 for UserDevice)
func (d *UserDevice) BatchRead(bufs [][]byte, sizes []int) (int, error) {
n, err := d.Read(bufs[0])
if err != nil {
return 0, err
}
sizes[0] = n
return 1, nil
}
// WriteBatch writes packets individually (no batching for UserDevice)
func (d *UserDevice) WriteBatch(bufs [][]byte, offset int) (int, error) {
for i, buf := range bufs {
_, err := d.Write(buf[offset:])
if err != nil {
return i, err
}
}
return len(bufs), nil
}
// BatchSize returns 1 for UserDevice (no batching)
func (d *UserDevice) BatchSize() int {
return 1
}
func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) { func (d *UserDevice) Pipe() (*io.PipeReader, *io.PipeWriter) {
return d.inboundReader, d.outboundWriter return d.inboundReader, d.outboundWriter
} }
+42 -5
View File
@@ -91,7 +91,7 @@ func (p *PKI) reload(c *config.C, initial bool) error {
} }
func (p *PKI) reloadCerts(c *config.C, initial bool) *util.ContextualError { func (p *PKI) reloadCerts(c *config.C, initial bool) *util.ContextualError {
newState, err := newCertStateFromConfig(c) newState, err := newCertStateFromConfig(c, p.l)
if err != nil { if err != nil {
return util.NewContextualError("Could not load client cert", nil, err) return util.NewContextualError("Could not load client cert", nil, err)
} }
@@ -102,7 +102,7 @@ func (p *PKI) reloadCerts(c *config.C, initial bool) *util.ContextualError {
if currentState.v1Cert == nil { if currentState.v1Cert == nil {
//adding certs is fine, actually. Networks-in-common confirmed in newCertState(). //adding certs is fine, actually. Networks-in-common confirmed in newCertState().
} else { } else {
// did IP in cert change? if so, don't set // did IP in cert change? if so, don't set. If we ever allow this, need to set p.firewallReloadNeeded
if !slices.Equal(currentState.v1Cert.Networks(), newState.v1Cert.Networks()) { if !slices.Equal(currentState.v1Cert.Networks(), newState.v1Cert.Networks()) {
return util.NewContextualError( return util.NewContextualError(
"Networks in new cert was different from old", "Networks in new cert was different from old",
@@ -158,6 +158,14 @@ func (p *PKI) reloadCerts(c *config.C, initial bool) *util.ContextualError {
} }
} }
newUN := newState.GetDefaultCertificate().UnsafeNetworks()
oldUN := currentState.GetDefaultCertificate().UnsafeNetworks()
if !slices.Equal(newUN, oldUN) {
//todo I don't love this, because other clients will see the new assignments and act on them, but we will not be able to.
//I think we need to wire this into the firewall reload.
p.l.WithFields(m{"previous": oldUN, "new": newUN}).Warning("UnsafeNetworks assignments differ. A restart is required in order for this to take effect.")
}
// Cipher cant be hot swapped so just leave it at what it was before // Cipher cant be hot swapped so just leave it at what it was before
newState.cipher = currentState.cipher newState.cipher = currentState.cipher
@@ -260,7 +268,7 @@ func (cs *CertState) MarshalJSON() ([]byte, error) {
return json.Marshal(msg) return json.Marshal(msg)
} }
func newCertStateFromConfig(c *config.C) (*CertState, error) { func newCertStateFromConfig(c *config.C, l *logrus.Logger) (*CertState, error) {
var err error var err error
privPathOrPEM := c.GetString("pki.key", "") privPathOrPEM := c.GetString("pki.key", "")
@@ -344,10 +352,33 @@ func newCertStateFromConfig(c *config.C) (*CertState, error) {
return nil, fmt.Errorf("unknown pki.initiating_version: %v", rawInitiatingVersion) return nil, fmt.Errorf("unknown pki.initiating_version: %v", rawInitiatingVersion)
} }
return newCertState(initiatingVersion, v1, v2, isPkcs11, curve, rawKey) return newCertState(l, initiatingVersion, v1, v2, isPkcs11, curve, rawKey)
} }
func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, privateKeyCurve cert.Curve, privateKey []byte) (*CertState, error) { func compareUnsafeNetworksAcrossCertVersions(v1, v2 cert.Certificate) error {
if v1 == nil || v2 == nil {
return nil //can't be a problem if we don't have one of the kinds of cert
}
v4UnsafeNets := 0
for _, n := range v2.UnsafeNetworks() {
if n.Addr().Is6() {
continue // V1 certs can't have IPv6 unsafe networks
} else {
v4UnsafeNets++
}
if !slices.Contains(v1.UnsafeNetworks(), n) {
return errors.New("UnsafeNetworks mismatch")
}
}
if len(v1.UnsafeNetworks()) != v4UnsafeNets {
return errors.New("UnsafeNetworks mismatch")
}
return nil
}
func newCertState(l *logrus.Logger, dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, privateKeyCurve cert.Curve, privateKey []byte) (*CertState, error) {
cs := CertState{ cs := CertState{
privateKey: privateKey, privateKey: privateKey,
pkcs11Backed: pkcs11backed, pkcs11Backed: pkcs11backed,
@@ -370,6 +401,12 @@ func newCertState(dv cert.Version, v1, v2 cert.Certificate, pkcs11backed bool, p
} }
cs.initiatingVersion = dv cs.initiatingVersion = dv
warn := compareUnsafeNetworksAcrossCertVersions(v1, v2)
if warn != nil {
l.WithFields(m{"UnsafeNetworksV1": v1.UnsafeNetworks(), "UnsafeNetworksV2": v2.UnsafeNetworks()}).
Warning("the IPv4 UnsafeNetworks assigned in the V1 certificate do not match the ones in V2")
}
} }
if v1 != nil { if v1 != nil {
+1 -1
View File
@@ -55,7 +55,7 @@ func (rm *relayManager) setAmRelay(v bool) {
func AddRelay(l *logrus.Logger, relayHostInfo *HostInfo, hm *HostMap, vpnIp netip.Addr, remoteIdx *uint32, relayType int, state int) (uint32, error) { func AddRelay(l *logrus.Logger, relayHostInfo *HostInfo, hm *HostMap, vpnIp netip.Addr, remoteIdx *uint32, relayType int, state int) (uint32, error) {
hm.Lock() hm.Lock()
defer hm.Unlock() defer hm.Unlock()
for i := 0; i < 32; i++ { for range 32 {
index, err := generateIndex(l) index, err := generateIndex(l)
if err != nil { if err != nil {
return 0, err return 0, err
+1 -6
View File
@@ -404,12 +404,7 @@ func (r *RemoteList) Rebuild(preferredRanges []netip.Prefix) {
// unlockedIsBad assumes you have the write lock and checks if the remote matches any entry in the blocked address list // unlockedIsBad assumes you have the write lock and checks if the remote matches any entry in the blocked address list
func (r *RemoteList) unlockedIsBad(remote netip.AddrPort) bool { func (r *RemoteList) unlockedIsBad(remote netip.AddrPort) bool {
for _, v := range r.badRemotes { return slices.Contains(r.badRemotes, remote)
if v == remote {
return true
}
}
return false
} }
// unlockedSetLearnedV4 assumes you have the write lock and sets the current learned address for this owner and marks the // unlockedSetLearnedV4 assumes you have the write lock and sets the current learned address for this owner and marks the
+91
View File
@@ -0,0 +1,91 @@
package nebula
import (
"encoding/binary"
"net/netip"
)
func recalcIPv4Checksum(data []byte, oldSrcIP netip.Addr, newSrcIP netip.Addr) {
oldChecksum := binary.BigEndian.Uint16(data[10:12])
//because of how checksums work, we can re-use this function
checksum := calcNewTransportChecksum(oldChecksum, oldSrcIP, 0, newSrcIP, 0)
binary.BigEndian.PutUint16(data[10:12], checksum)
}
func calcNewTransportChecksum(oldChecksum uint16, oldSrcIP netip.Addr, oldSrcPort uint16, newSrcIP netip.Addr, newSrcPort uint16) uint16 {
oldIP := binary.BigEndian.Uint32(oldSrcIP.AsSlice())
newIP := binary.BigEndian.Uint32(newSrcIP.AsSlice())
// Start with inverted checksum
checksum := uint32(^oldChecksum)
// Subtract old IP (as two 16-bit words)
checksum += uint32(^uint16(oldIP >> 16))
checksum += uint32(^uint16(oldIP & 0xFFFF))
// Subtract old port
checksum += uint32(^oldSrcPort)
// Add new IP (as two 16-bit words)
checksum += uint32(newIP >> 16)
checksum += uint32(newIP & 0xFFFF)
// Add new port
checksum += uint32(newSrcPort)
// Fold carries
for checksum > 0xFFFF {
checksum = (checksum & 0xFFFF) + (checksum >> 16)
}
// Return ones' complement
return ^uint16(checksum)
}
func recalcV4TransportChecksum(offsetInsideHeader int, data []byte, oldSrcIP netip.AddrPort, newSrcIP netip.AddrPort) {
ipHeaderOffset := int(data[0]&0x0F) * 4
offset := ipHeaderOffset + offsetInsideHeader
oldcsum := binary.BigEndian.Uint16(data[offset : offset+2])
checksum := calcNewTransportChecksum(oldcsum, oldSrcIP.Addr(), oldSrcIP.Port(), newSrcIP.Addr(), newSrcIP.Port())
binary.BigEndian.PutUint16(data[offset:offset+2], checksum)
}
func recalcUDPv4Checksum(data []byte, oldSrcIP netip.AddrPort, newSrcIP netip.AddrPort) {
const offsetInsideHeader = 6
recalcV4TransportChecksum(offsetInsideHeader, data, oldSrcIP, newSrcIP)
}
func recalcTCPv4Checksum(data []byte, oldSrcIP netip.AddrPort, newSrcIP netip.AddrPort) {
const offsetInsideHeader = 16
recalcV4TransportChecksum(offsetInsideHeader, data, oldSrcIP, newSrcIP)
}
func calcNewICMPChecksum(oldChecksum uint16, oldCode uint16, newCode uint16, oldID uint16, newID uint16) uint16 {
// Start with inverted checksum
checksum := uint32(^oldChecksum)
// Subtract old stuff
checksum += uint32(^oldCode)
checksum += uint32(^oldID)
// Add new stuff
checksum += uint32(newCode)
checksum += uint32(newID)
// Fold carries
for checksum > 0xFFFF {
checksum = (checksum & 0xFFFF) + (checksum >> 16)
}
// Return ones' complement
return ^uint16(checksum)
}
func recalcICMPv4Checksum(data []byte, oldCode uint16, newCode uint16, oldID uint16, newID uint16) {
const offsetInsideHeader = 2
ipHeaderOffset := int(data[0]&0x0F) * 4
offset := ipHeaderOffset + offsetInsideHeader
oldChecksum := binary.BigEndian.Uint16(data[offset : offset+2])
checksum := calcNewICMPChecksum(oldChecksum, oldCode, newCode, oldID, newID)
binary.BigEndian.PutUint16(data[offset:offset+2], checksum)
}
+1310
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -6,6 +6,7 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"maps"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@@ -831,9 +832,7 @@ func sshPrintRelays(ifce *Interface, fs any, a []string, w sshd.StringWriter) er
relays := map[uint32]*HostInfo{} relays := map[uint32]*HostInfo{}
ifce.hostMap.Lock() ifce.hostMap.Lock()
for k, v := range ifce.hostMap.Relays { maps.Copy(relays, ifce.hostMap.Relays)
relays[k] = v
}
ifce.hostMap.Unlock() ifce.hostMap.Unlock()
type RelayFor struct { type RelayFor struct {
-1
View File
@@ -6,7 +6,6 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
_ "net/http/pprof"
"runtime" "runtime"
"strconv" "strconv"
"time" "time"
+14 -18
View File
@@ -1,16 +1,27 @@
package device package test
import ( import (
"errors" "errors"
"io" "io"
"net/netip" "net/netip"
"github.com/slackhq/nebula/overlay"
"github.com/slackhq/nebula/routing" "github.com/slackhq/nebula/routing"
) )
type NoopTun struct{} type NoopTun struct{}
func (NoopTun) UnsafeNetworks() []netip.Prefix {
return nil
}
func (NoopTun) SNATAddress() netip.Prefix {
return netip.Prefix{}
}
func (NoopTun) UnsafeIPv4OriginAddress() netip.Prefix {
return netip.Prefix{}
}
func (NoopTun) RoutesFor(addr netip.Addr) routing.Gateways { func (NoopTun) RoutesFor(addr netip.Addr) routing.Gateways {
return routing.Gateways{} return routing.Gateways{}
} }
@@ -39,25 +50,10 @@ func (NoopTun) SupportsMultiqueue() bool {
return false return false
} }
func (NoopTun) NewMultiQueueReader() (overlay.BatchReadWriter, error) { func (NoopTun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
return nil, errors.New("unsupported") return nil, errors.New("unsupported")
} }
func (NoopTun) Close() error { func (NoopTun) Close() error {
return nil return nil
} }
// BatchRead implements BatchReadWriter interface
func (NoopTun) BatchRead(bufs [][]byte, sizes []int) (int, error) {
return 0, io.EOF
}
// WriteBatch implements BatchReadWriter interface
func (NoopTun) WriteBatch(bufs [][]byte, offset int) (int, error) {
return len(bufs), nil
}
// BatchSize implements BatchReadWriter interface
func (NoopTun) BatchSize() int {
return 1
}
+2 -2
View File
@@ -134,7 +134,7 @@ func TestTimerWheel_Purge(t *testing.T) {
assert.True(t, tw.lastTick.After(lastTick)) assert.True(t, tw.lastTick.After(lastTick))
// Make sure we get all 4 packets back // Make sure we get all 4 packets back
for i := 0; i < 4; i++ { for i := range 4 {
p, has := tw.Purge() p, has := tw.Purge()
assert.True(t, has) assert.True(t, has)
assert.Equal(t, fps[i], p) assert.Equal(t, fps[i], p)
@@ -149,7 +149,7 @@ func TestTimerWheel_Purge(t *testing.T) {
// Make sure we cached the free'd items // Make sure we cached the free'd items
assert.Equal(t, 4, tw.itemsCached) assert.Equal(t, 4, tw.itemsCached)
ci := tw.itemCache ci := tw.itemCache
for i := 0; i < 4; i++ { for range 4 {
assert.NotNil(t, ci) assert.NotNil(t, ci)
ci = ci.Next ci = ci.Next
} }
+6 -22
View File
@@ -13,21 +13,13 @@ type EncReader func(
payload []byte, payload []byte,
) )
type EncBatchReader func(
addrs []netip.AddrPort,
payloads [][]byte,
count int,
)
type Conn interface { type Conn interface {
Rebind() error Rebind() error
LocalAddr() (netip.AddrPort, error) LocalAddr() (netip.AddrPort, error)
ListenOutBatch(r EncBatchReader) ListenOut(r EncReader)
WriteTo(b []byte, addr netip.AddrPort) error WriteTo(b []byte, addr netip.AddrPort) error
WriteMulti(packets [][]byte, addrs []netip.AddrPort) (int, error)
ReloadConfig(c *config.C) ReloadConfig(c *config.C)
SupportsMultipleReaders() bool SupportsMultipleReaders() bool
BatchSize() int
Close() error Close() error
} }
@@ -39,25 +31,17 @@ func (NoopConn) Rebind() error {
func (NoopConn) LocalAddr() (netip.AddrPort, error) { func (NoopConn) LocalAddr() (netip.AddrPort, error) {
return netip.AddrPort{}, nil return netip.AddrPort{}, nil
} }
func (NoopConn) ListenOut(_ EncReader) {
func (NoopConn) ListenOut(_ EncReader) {} return
}
func (NoopConn) SupportsMultipleReaders() bool { func (NoopConn) SupportsMultipleReaders() bool {
return false return false
} }
func (NoopConn) ListenOutBatch(_ EncBatchReader) {}
func (NoopConn) WriteTo(_ []byte, _ netip.AddrPort) error { func (NoopConn) WriteTo(_ []byte, _ netip.AddrPort) error {
return nil return nil
} }
func (NoopConn) WriteMulti(_ [][]byte, _ []netip.AddrPort) (int, error) { func (NoopConn) ReloadConfig(_ *config.C) {
return 0, nil return
}
func (NoopConn) ReloadConfig(_ *config.C) {}
func (NoopConn) BatchSize() int {
return 1
} }
func (NoopConn) Close() error { func (NoopConn) Close() error {
return nil return nil
-39
View File
@@ -140,17 +140,6 @@ func (u *StdConn) WriteTo(b []byte, ap netip.AddrPort) error {
} }
} }
// WriteMulti sends multiple packets - fallback implementation without sendmmsg
func (u *StdConn) WriteMulti(packets [][]byte, addrs []netip.AddrPort) (int, error) {
for i := range packets {
err := u.WriteTo(packets[i], addrs[i])
if err != nil {
return i, err
}
}
return len(packets), nil
}
func (u *StdConn) LocalAddr() (netip.AddrPort, error) { func (u *StdConn) LocalAddr() (netip.AddrPort, error) {
a := u.UDPConn.LocalAddr() a := u.UDPConn.LocalAddr()
@@ -199,34 +188,6 @@ func (u *StdConn) SupportsMultipleReaders() bool {
return false return false
} }
// ListenOutBatch - fallback to single-packet reads for Darwin
func (u *StdConn) ListenOutBatch(r EncBatchReader) {
buffer := make([]byte, MTU)
addrs := make([]netip.AddrPort, 1)
payloads := make([][]byte, 1)
for {
// Just read one packet at a time and call batch callback with count=1
n, rua, err := u.ReadFromUDPAddrPort(buffer)
if err != nil {
if errors.Is(err, net.ErrClosed) {
u.l.WithError(err).Debug("udp socket is closed, exiting read loop")
return
}
u.l.WithError(err).Error("unexpected udp socket receive error")
}
addrs[0] = netip.AddrPortFrom(rua.Addr().Unmap(), rua.Port())
payloads[0] = buffer[:n]
r(addrs, payloads, 1)
}
}
func (u *StdConn) BatchSize() int {
return 1
}
func (u *StdConn) Rebind() error { func (u *StdConn) Rebind() error {
var err error var err error
if u.isV4 { if u.isV4 {
-35
View File
@@ -101,38 +101,3 @@ func (u *GenericConn) ListenOut(r EncReader) {
func (u *GenericConn) SupportsMultipleReaders() bool { func (u *GenericConn) SupportsMultipleReaders() bool {
return false return false
} }
// ListenOutBatch - fallback to single-packet reads for generic platforms
func (u *GenericConn) ListenOutBatch(r EncBatchReader) {
buffer := make([]byte, MTU)
addrs := make([]netip.AddrPort, 1)
payloads := make([][]byte, 1)
for {
// Just read one packet at a time and call batch callback with count=1
n, rua, err := u.ReadFromUDPAddrPort(buffer)
if err != nil {
u.l.WithError(err).Debug("udp socket is closed, exiting read loop")
return
}
addrs[0] = netip.AddrPortFrom(rua.Addr().Unmap(), rua.Port())
payloads[0] = buffer[:n]
r(addrs, payloads, 1)
}
}
// WriteMulti sends multiple packets - fallback implementation
func (u *GenericConn) WriteMulti(packets [][]byte, addrs []netip.AddrPort) (int, error) {
for i := range packets {
err := u.WriteTo(packets[i], addrs[i])
if err != nil {
return i, err
}
}
return len(packets), nil
}
func (u *GenericConn) BatchSize() int {
return 1
}
+4 -174
View File
@@ -22,11 +22,6 @@ type StdConn struct {
isV4 bool isV4 bool
l *logrus.Logger l *logrus.Logger
batch int batch int
// Pre-allocated buffers for batch writes (sized for IPv6, works for both)
writeMsgs []rawMessage
writeIovecs []iovec
writeNames [][]byte
} }
func maybeIPV4(ip net.IP) (net.IP, bool) { func maybeIPV4(ip net.IP) (net.IP, bool) {
@@ -74,26 +69,7 @@ func NewListener(l *logrus.Logger, ip netip.Addr, port int, multi bool, batch in
return nil, fmt.Errorf("unable to bind to socket: %s", err) return nil, fmt.Errorf("unable to bind to socket: %s", err)
} }
c := &StdConn{sysFd: fd, isV4: ip.Is4(), l: l, batch: batch} return &StdConn{sysFd: fd, isV4: ip.Is4(), l: l, batch: batch}, err
// Pre-allocate write message structures for batching (sized for IPv6, works for both)
c.writeMsgs = make([]rawMessage, batch)
c.writeIovecs = make([]iovec, batch)
c.writeNames = make([][]byte, batch)
for i := range c.writeMsgs {
// Allocate for IPv6 size (larger than IPv4, works for both)
c.writeNames[i] = make([]byte, unix.SizeofSockaddrInet6)
// Point to the iovec in the slice
c.writeMsgs[i].Hdr.Iov = &c.writeIovecs[i]
c.writeMsgs[i].Hdr.Iovlen = 1
c.writeMsgs[i].Hdr.Name = &c.writeNames[i][0]
// Namelen will be set appropriately in writeMulti4/writeMulti6
}
return c, err
} }
func (u *StdConn) SupportsMultipleReaders() bool { func (u *StdConn) SupportsMultipleReaders() bool {
@@ -146,7 +122,7 @@ func (u *StdConn) LocalAddr() (netip.AddrPort, error) {
} }
} }
func (u *StdConn) ListenOutBatch(r EncBatchReader) { func (u *StdConn) ListenOut(r EncReader) {
var ip netip.Addr var ip netip.Addr
msgs, buffers, names := u.PrepareRawMessages(u.batch) msgs, buffers, names := u.PrepareRawMessages(u.batch)
@@ -155,12 +131,6 @@ func (u *StdConn) ListenOutBatch(r EncBatchReader) {
read = u.ReadSingle read = u.ReadSingle
} }
udpBatchHist := metrics.GetOrRegisterHistogram("batch.udp_read_size", nil, metrics.NewUniformSample(1024))
// Pre-allocate slices for batch callback
addrs := make([]netip.AddrPort, u.batch)
payloads := make([][]byte, u.batch)
for { for {
n, err := read(msgs) n, err := read(msgs)
if err != nil { if err != nil {
@@ -168,21 +138,15 @@ func (u *StdConn) ListenOutBatch(r EncBatchReader) {
return return
} }
udpBatchHist.Update(int64(n))
// Prepare batch data
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
// Its ok to skip the ok check here, the slicing is the only error that can occur and it will panic
if u.isV4 { if u.isV4 {
ip, _ = netip.AddrFromSlice(names[i][4:8]) ip, _ = netip.AddrFromSlice(names[i][4:8])
} else { } else {
ip, _ = netip.AddrFromSlice(names[i][8:24]) ip, _ = netip.AddrFromSlice(names[i][8:24])
} }
addrs[i] = netip.AddrPortFrom(ip.Unmap(), binary.BigEndian.Uint16(names[i][2:4])) r(netip.AddrPortFrom(ip.Unmap(), binary.BigEndian.Uint16(names[i][2:4])), buffers[i][:msgs[i].Len])
payloads[i] = buffers[i][:msgs[i].Len]
} }
// Call batch callback with all packets
r(addrs, payloads, n)
} }
} }
@@ -234,19 +198,6 @@ func (u *StdConn) WriteTo(b []byte, ip netip.AddrPort) error {
return u.writeTo6(b, ip) return u.writeTo6(b, ip)
} }
func (u *StdConn) WriteMulti(packets [][]byte, addrs []netip.AddrPort) (int, error) {
if len(packets) != len(addrs) {
return 0, fmt.Errorf("packets and addrs length mismatch")
}
if len(packets) == 0 {
return 0, nil
}
if u.isV4 {
return u.writeMulti4(packets, addrs)
}
return u.writeMulti6(packets, addrs)
}
func (u *StdConn) writeTo6(b []byte, ip netip.AddrPort) error { func (u *StdConn) writeTo6(b []byte, ip netip.AddrPort) error {
var rsa unix.RawSockaddrInet6 var rsa unix.RawSockaddrInet6
rsa.Family = unix.AF_INET6 rsa.Family = unix.AF_INET6
@@ -301,123 +252,6 @@ func (u *StdConn) writeTo4(b []byte, ip netip.AddrPort) error {
} }
} }
func (u *StdConn) writeMulti4(packets [][]byte, addrs []netip.AddrPort) (int, error) {
sent := 0
for sent < len(packets) {
// Determine batch size based on remaining packets and buffer capacity
batchSize := len(packets) - sent
if batchSize > len(u.writeMsgs) {
batchSize = len(u.writeMsgs)
}
// Use pre-allocated buffers
msgs := u.writeMsgs[:batchSize]
iovecs := u.writeIovecs[:batchSize]
names := u.writeNames[:batchSize]
// Setup message structures for this batch
for i := 0; i < batchSize; i++ {
pktIdx := sent + i
if !addrs[pktIdx].Addr().Is4() {
return sent + i, ErrInvalidIPv6RemoteForSocket
}
// Setup the packet buffer
iovecs[i].Base = &packets[pktIdx][0]
iovecs[i].Len = uint(len(packets[pktIdx]))
// Setup the destination address
rsa := (*unix.RawSockaddrInet4)(unsafe.Pointer(&names[i][0]))
rsa.Family = unix.AF_INET
rsa.Addr = addrs[pktIdx].Addr().As4()
binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&rsa.Port))[:], addrs[pktIdx].Port())
// Set the appropriate address length for IPv4
msgs[i].Hdr.Namelen = unix.SizeofSockaddrInet4
}
// Send this batch
nsent, _, err := unix.Syscall6(
unix.SYS_SENDMMSG,
uintptr(u.sysFd),
uintptr(unsafe.Pointer(&msgs[0])),
uintptr(batchSize),
0,
0,
0,
)
if err != 0 {
return sent + int(nsent), &net.OpError{Op: "sendmmsg", Err: err}
}
sent += int(nsent)
if int(nsent) < batchSize {
// Couldn't send all packets in batch, return what we sent
return sent, nil
}
}
return sent, nil
}
func (u *StdConn) writeMulti6(packets [][]byte, addrs []netip.AddrPort) (int, error) {
sent := 0
for sent < len(packets) {
// Determine batch size based on remaining packets and buffer capacity
batchSize := len(packets) - sent
if batchSize > len(u.writeMsgs) {
batchSize = len(u.writeMsgs)
}
// Use pre-allocated buffers
msgs := u.writeMsgs[:batchSize]
iovecs := u.writeIovecs[:batchSize]
names := u.writeNames[:batchSize]
// Setup message structures for this batch
for i := 0; i < batchSize; i++ {
pktIdx := sent + i
// Setup the packet buffer
iovecs[i].Base = &packets[pktIdx][0]
iovecs[i].Len = uint(len(packets[pktIdx]))
// Setup the destination address
rsa := (*unix.RawSockaddrInet6)(unsafe.Pointer(&names[i][0]))
rsa.Family = unix.AF_INET6
rsa.Addr = addrs[pktIdx].Addr().As16()
binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&rsa.Port))[:], addrs[pktIdx].Port())
// Set the appropriate address length for IPv6
msgs[i].Hdr.Namelen = unix.SizeofSockaddrInet6
}
// Send this batch
nsent, _, err := unix.Syscall6(
unix.SYS_SENDMMSG,
uintptr(u.sysFd),
uintptr(unsafe.Pointer(&msgs[0])),
uintptr(batchSize),
0,
0,
0,
)
if err != 0 {
return sent + int(nsent), &net.OpError{Op: "sendmmsg", Err: err}
}
sent += int(nsent)
if int(nsent) < batchSize {
// Couldn't send all packets in batch, return what we sent
return sent, nil
}
}
return sent, nil
}
func (u *StdConn) ReloadConfig(c *config.C) { func (u *StdConn) ReloadConfig(c *config.C) {
b := c.GetInt("listen.read_buffer", 0) b := c.GetInt("listen.read_buffer", 0)
if b > 0 { if b > 0 {
@@ -475,10 +309,6 @@ func (u *StdConn) getMemInfo(meminfo *[unix.SK_MEMINFO_VARS]uint32) error {
return nil return nil
} }
func (u *StdConn) BatchSize() int {
return u.batch
}
func (u *StdConn) Close() error { func (u *StdConn) Close() error {
return syscall.Close(u.sysFd) return syscall.Close(u.sysFd)
} }
+2 -2
View File
@@ -12,7 +12,7 @@ import (
type iovec struct { type iovec struct {
Base *byte Base *byte
Len uint Len uint32
} }
type msghdr struct { type msghdr struct {
@@ -40,7 +40,7 @@ func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte) {
names[i] = make([]byte, unix.SizeofSockaddrInet6) names[i] = make([]byte, unix.SizeofSockaddrInet6)
vs := []iovec{ vs := []iovec{
{Base: &buffers[i][0], Len: uint(len(buffers[i]))}, {Base: &buffers[i][0], Len: uint32(len(buffers[i]))},
} }
msgs[i].Hdr.Iov = &vs[0] msgs[i].Hdr.Iov = &vs[0]
+2 -2
View File
@@ -12,7 +12,7 @@ import (
type iovec struct { type iovec struct {
Base *byte Base *byte
Len uint Len uint64
} }
type msghdr struct { type msghdr struct {
@@ -43,7 +43,7 @@ func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte) {
names[i] = make([]byte, unix.SizeofSockaddrInet6) names[i] = make([]byte, unix.SizeofSockaddrInet6)
vs := []iovec{ vs := []iovec{
{Base: &buffers[i][0], Len: uint(len(buffers[i]))}, {Base: &buffers[i][0], Len: uint64(len(buffers[i]))},
} }
msgs[i].Hdr.Iov = &vs[0] msgs[i].Hdr.Iov = &vs[0]
-44
View File
@@ -338,50 +338,6 @@ func (u *RIOConn) Rebind() error {
func (u *RIOConn) ReloadConfig(*config.C) {} func (u *RIOConn) ReloadConfig(*config.C) {}
// BatchSize returns 1 since RIO reads packets one at a time
func (u *RIOConn) BatchSize() int {
return 1
}
// ListenOutBatch - fallback to single-packet reads for RIO
func (u *RIOConn) ListenOutBatch(r EncBatchReader) {
buffer := make([]byte, MTU)
addrs := make([]netip.AddrPort, 1)
payloads := make([][]byte, 1)
var lastRecvErr time.Time
for {
n, rua, err := u.receive(buffer)
if err != nil {
if errors.Is(err, net.ErrClosed) {
u.l.WithError(err).Debug("udp socket is closed, exiting read loop")
return
}
if lastRecvErr.IsZero() || time.Since(lastRecvErr) > time.Minute {
lastRecvErr = time.Now()
u.l.WithError(err).Warn("unexpected udp socket receive error")
}
continue
}
addrs[0] = netip.AddrPortFrom(netip.AddrFrom16(rua.Addr).Unmap(), (rua.Port>>8)|((rua.Port&0xff)<<8))
payloads[0] = buffer[:n]
r(addrs, payloads, 1)
}
}
// WriteMulti sends multiple packets - fallback implementation
func (u *RIOConn) WriteMulti(packets [][]byte, addrs []netip.AddrPort) (int, error) {
for i := range packets {
err := u.WriteTo(packets[i], addrs[i])
if err != nil {
return i, err
}
}
return len(packets), nil
}
func (u *RIOConn) Close() error { func (u *RIOConn) Close() error {
if !u.isOpen.CompareAndSwap(true, false) { if !u.isOpen.CompareAndSwap(true, false) {
return nil return nil
-29
View File
@@ -116,31 +116,6 @@ func (u *TesterConn) ListenOut(r EncReader) {
} }
} }
func (u *TesterConn) ListenOutBatch(r EncBatchReader) {
addrs := make([]netip.AddrPort, 1)
payloads := make([][]byte, 1)
for {
p, ok := <-u.RxPackets
if !ok {
return
}
addrs[0] = p.From
payloads[0] = p.Data
r(addrs, payloads, 1)
}
}
func (u *TesterConn) WriteMulti(packets [][]byte, addrs []netip.AddrPort) (int, error) {
for i := range packets {
err := u.WriteTo(packets[i], addrs[i])
if err != nil {
return i, err
}
}
return len(packets), nil
}
func (u *TesterConn) ReloadConfig(*config.C) {} func (u *TesterConn) ReloadConfig(*config.C) {}
func NewUDPStatsEmitter(_ []Conn) func() { func NewUDPStatsEmitter(_ []Conn) func() {
@@ -156,10 +131,6 @@ func (u *TesterConn) SupportsMultipleReaders() bool {
return false return false
} }
func (u *TesterConn) BatchSize() int {
return 1
}
func (u *TesterConn) Rebind() error { func (u *TesterConn) Rebind() error {
return nil return nil
} }