mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 13:46:59 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8a4090831 | |||
| f4c8dfdfd2 | |||
| 4c4faa3fac | |||
| f03f753baa | |||
| 28d2b47164 | |||
| 51308b845b | |||
| 422fc2ad1e | |||
| e8bb874e14 | |||
| 353ad1f271 | |||
| f573e8a266 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,5 +1,4 @@
|
|||||||
//go:build boringcrypto
|
//go:build boringcrypto
|
||||||
// +build boringcrypto
|
|
||||||
|
|
||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build e2e_testing
|
//go:build e2e_testing
|
||||||
// +build e2e_testing
|
|
||||||
|
|
||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -382,8 +382,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
|
||||||
|
|||||||
+69
-49
@@ -249,20 +249,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
|
||||||
@@ -280,6 +266,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
|
||||||
@@ -287,6 +279,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,24 +355,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)
|
||||||
@@ -467,7 +480,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
|
||||||
@@ -660,6 +673,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 {
|
||||||
@@ -804,11 +824,9 @@ 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" {
|
||||||
return true
|
return true
|
||||||
@@ -1018,54 +1036,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" {
|
||||||
|
return firewall.PortFragment, firewall.PortFragment, nil
|
||||||
|
}
|
||||||
|
if !strings.Contains(s, `-`) {
|
||||||
|
rPort, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return notAPort, notAPort, fmt.Errorf("was not a number; `%s`", s)
|
||||||
|
}
|
||||||
|
return int32(rPort), int32(rPort), nil
|
||||||
|
}
|
||||||
|
|
||||||
} else if s == "fragment" {
|
|
||||||
startPort = firewall.PortFragment
|
|
||||||
endPort = firewall.PortFragment
|
|
||||||
|
|
||||||
} else if strings.Contains(s, `-`) {
|
|
||||||
sPorts := strings.SplitN(s, `-`, 2)
|
sPorts := strings.SplitN(s, `-`, 2)
|
||||||
sPorts[0] = strings.Trim(sPorts[0], " ")
|
for i := range sPorts {
|
||||||
sPorts[1] = strings.Trim(sPorts[1], " ")
|
sPorts[i] = strings.Trim(sPorts[i], " ")
|
||||||
|
}
|
||||||
if len(sPorts) != 2 || sPorts[0] == "" || 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)
|
return notAPort, notAPort, fmt.Errorf("appears to be a range but could not be parsed; `%s`", s)
|
||||||
}
|
}
|
||||||
|
|
||||||
rStartPort, err := strconv.Atoi(sPorts[0])
|
rStartPort, err := strconv.Atoi(sPorts[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("beginning range was not a number; `%s`", sPorts[0])
|
return notAPort, notAPort, fmt.Errorf("beginning range was not a number; `%s`", sPorts[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
rEndPort, err := strconv.Atoi(sPorts[1])
|
rEndPort, err := strconv.Atoi(sPorts[1])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("ending range was not a number; `%s`", sPorts[1])
|
return notAPort, notAPort, fmt.Errorf("ending range was not a number; `%s`", sPorts[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
startPort = int32(rStartPort)
|
startPort := int32(rStartPort)
|
||||||
endPort = int32(rEndPort)
|
endPort := int32(rEndPort)
|
||||||
|
|
||||||
if startPort == firewall.PortAny {
|
if startPort == firewall.PortAny {
|
||||||
endPort = firewall.PortAny
|
endPort = firewall.PortAny
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
return startPort, endPort, nil
|
||||||
rPort, err := strconv.Atoi(s)
|
|
||||||
if err != nil {
|
|
||||||
return 0, 0, fmt.Errorf("was not a number; `%s`", s)
|
|
||||||
}
|
|
||||||
startPort = int32(rPort)
|
|
||||||
endPort = startPort
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ const (
|
|||||||
type Packet struct {
|
type Packet struct {
|
||||||
LocalAddr netip.Addr
|
LocalAddr netip.Addr
|
||||||
RemoteAddr netip.Addr
|
RemoteAddr netip.Addr
|
||||||
|
// LocalPort is the destination port for incoming traffic, or the source port for outgoing. Zero for ICMP.
|
||||||
LocalPort uint16
|
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
|
||||||
@@ -46,6 +49,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:
|
||||||
|
|||||||
+158
-6
@@ -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(), "", "", ""))
|
||||||
@@ -734,6 +735,150 @@ func TestFirewall_DropConntrackReload(t *testing.T) {
|
|||||||
assert.Equal(t, fw.Drop(p, false, &h, cp, nil), ErrNoMatchingRule)
|
assert.Equal(t, fw.Drop(p, 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, false, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
// Allow inbound
|
||||||
|
resetConntrack(fw)
|
||||||
|
require.NoError(t, fw.Drop(*p, true, &h, cp, nil))
|
||||||
|
//now also allow outbound
|
||||||
|
require.NoError(t, fw.Drop(*p, 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, false, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
// Allow inbound
|
||||||
|
resetConntrack(fw)
|
||||||
|
require.NoError(t, fw.Drop(*p, true, &h, cp, nil))
|
||||||
|
//now also allow outbound
|
||||||
|
require.NoError(t, fw.Drop(*p, 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, false, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
// Allow inbound
|
||||||
|
resetConntrack(fw)
|
||||||
|
assert.Equal(t, fw.Drop(*p, true, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
//now also allow outbound
|
||||||
|
assert.Equal(t, fw.Drop(*p, 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, false, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
// Allow inbound
|
||||||
|
resetConntrack(fw)
|
||||||
|
assert.Equal(t, fw.Drop(*p, true, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
//now also allow outbound
|
||||||
|
assert.Equal(t, fw.Drop(*p, 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, false, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
// Allow inbound
|
||||||
|
resetConntrack(fw)
|
||||||
|
assert.Equal(t, fw.Drop(*p, true, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
//now also allow outbound
|
||||||
|
assert.Equal(t, fw.Drop(*p, 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, false, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
// Allow inbound
|
||||||
|
resetConntrack(fw)
|
||||||
|
require.NoError(t, fw.Drop(*p, true, &h, cp, nil))
|
||||||
|
//now also allow outbound
|
||||||
|
require.NoError(t, fw.Drop(*p, 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, false, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
// Allow inbound
|
||||||
|
resetConntrack(fw)
|
||||||
|
require.NoError(t, fw.Drop(*p, true, &h, cp, nil))
|
||||||
|
//now also allow outbound
|
||||||
|
require.NoError(t, fw.Drop(*p, false, &h, cp, nil))
|
||||||
|
//different ID is blocked
|
||||||
|
p.RemotePort++
|
||||||
|
require.Equal(t, fw.Drop(*p, false, &h, cp, nil), ErrNoMatchingRule)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestFirewall_DropIPSpoofing(t *testing.T) {
|
func TestFirewall_DropIPSpoofing(t *testing.T) {
|
||||||
l := test.NewLogger()
|
l := test.NewLogger()
|
||||||
ob := &bytes.Buffer{}
|
ob := &bytes.Buffer{}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build e2e_testing
|
//go:build e2e_testing
|
||||||
// +build e2e_testing
|
|
||||||
|
|
||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
|
|||||||
@@ -9,75 +9,8 @@ import (
|
|||||||
"github.com/slackhq/nebula/iputil"
|
"github.com/slackhq/nebula/iputil"
|
||||||
"github.com/slackhq/nebula/noiseutil"
|
"github.com/slackhq/nebula/noiseutil"
|
||||||
"github.com/slackhq/nebula/routing"
|
"github.com/slackhq/nebula/routing"
|
||||||
"github.com/slackhq/nebula/udp"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// consumeInsidePacketBatched is a variant of consumeInsidePacket that queues
|
|
||||||
// outgoing packets into pendingPackets instead of sending them immediately.
|
|
||||||
// The caller is responsible for flushing pendingPackets with WriteBatch.
|
|
||||||
func (f *Interface) consumeInsidePacketBatched(packet []byte, fwPacket *firewall.Packet, nb, out []byte, q int, localCache firewall.ConntrackCache, pendingPackets *[]udp.BatchPacket) {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore local broadcast packets
|
|
||||||
if f.dropLocalBroadcast {
|
|
||||||
if f.myBroadcastAddrsTable.Contains(fwPacket.RemoteAddr) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if f.myVpnAddrsTable.Contains(fwPacket.RemoteAddr) {
|
|
||||||
if immediatelyForwardToSelf {
|
|
||||||
_, err := f.readers[q].Write(packet)
|
|
||||||
if err != nil {
|
|
||||||
f.l.WithError(err).Error("Failed to forward to tun")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore multicast packets
|
|
||||||
if f.dropMulticast && fwPacket.RemoteAddr.IsMulticast() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ready {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
dropReason := f.firewall.Drop(*fwPacket, false, hostinfo, f.pki.GetCAPool(), localCache)
|
|
||||||
if dropReason == nil {
|
|
||||||
f.sendNoMetricsBatched(header.Message, 0, hostinfo.ConnectionState, hostinfo, netip.AddrPort{}, packet, nb, out, q, pendingPackets)
|
|
||||||
} else {
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -136,6 +69,7 @@ func (f *Interface) consumeInsidePacket(packet []byte, fwPacket *firewall.Packet
|
|||||||
dropReason := f.firewall.Drop(*fwPacket, false, hostinfo, f.pki.GetCAPool(), localCache)
|
dropReason := f.firewall.Drop(*fwPacket, 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 {
|
||||||
@@ -476,75 +410,3 @@ func (f *Interface) sendNoMetrics(t header.MessageType, st header.MessageSubType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendNoMetricsBatched is like sendNoMetrics but queues the packet for batched sending
|
|
||||||
// instead of sending immediately. The caller must flush pendingPackets with WriteBatch.
|
|
||||||
func (f *Interface) sendNoMetricsBatched(t header.MessageType, st header.MessageSubType, ci *ConnectionState, hostinfo *HostInfo, remote netip.AddrPort, p, nb, out []byte, q int, pendingPackets *[]udp.BatchPacket) {
|
|
||||||
if ci.eKey == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
useRelay := !remote.IsValid() && !hostinfo.remote.IsValid()
|
|
||||||
fullOut := out
|
|
||||||
|
|
||||||
if useRelay {
|
|
||||||
if len(out) < header.Len {
|
|
||||||
out = out[:header.Len]
|
|
||||||
}
|
|
||||||
out = out[header.Len:]
|
|
||||||
}
|
|
||||||
|
|
||||||
if noiseutil.EncryptLockNeeded {
|
|
||||||
ci.writeLock.Lock()
|
|
||||||
}
|
|
||||||
c := ci.messageCounter.Add(1)
|
|
||||||
|
|
||||||
out = header.Encode(out, header.Version, t, st, hostinfo.remoteIndexId, c)
|
|
||||||
f.connectionManager.Out(hostinfo)
|
|
||||||
|
|
||||||
if t != header.CloseTunnel && 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, p, c, nb)
|
|
||||||
if noiseutil.EncryptLockNeeded {
|
|
||||||
ci.writeLock.Unlock()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
hostinfo.logger(f.l).WithError(err).
|
|
||||||
WithField("udpAddr", remote).WithField("counter", c).
|
|
||||||
WithField("attemptedCounter", c).
|
|
||||||
Error("Failed to encrypt outgoing packet")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Queue the packet for batched sending
|
|
||||||
var addr netip.AddrPort
|
|
||||||
if remote.IsValid() {
|
|
||||||
addr = remote
|
|
||||||
} else if hostinfo.remote.IsValid() {
|
|
||||||
addr = hostinfo.remote
|
|
||||||
} else {
|
|
||||||
// Relay path - send immediately, not batched
|
|
||||||
for _, relayIP := range hostinfo.relayState.CopyRelayIps() {
|
|
||||||
relayHostInfo, relay, err := f.hostMap.QueryVpnAddrsRelayFor(hostinfo.vpnAddrs, relayIP)
|
|
||||||
if err != nil {
|
|
||||||
hostinfo.relayState.DeleteRelay(relayIP)
|
|
||||||
hostinfo.logger(f.l).WithField("relay", relayIP).WithError(err).Info("sendNoMetricsBatched failed to find HostInfo")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
f.SendVia(relayHostInfo, relay, out, nb, fullOut[:header.Len+len(out)], true)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the payload since the buffer will be reused
|
|
||||||
payload := make([]byte, len(out))
|
|
||||||
copy(payload, out)
|
|
||||||
*pendingPackets = append(*pendingPackets, udp.BatchPacket{Payload: payload, Addr: addr})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|
||||||
|
|||||||
+2
-73
@@ -48,8 +48,6 @@ type InterfaceConfig struct {
|
|||||||
|
|
||||||
ConntrackCacheTimeout time.Duration
|
ConntrackCacheTimeout time.Duration
|
||||||
l *logrus.Logger
|
l *logrus.Logger
|
||||||
|
|
||||||
tunBatchSize int // batch size for TUN read/write batching, 0 to disable
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Interface struct {
|
type Interface struct {
|
||||||
@@ -90,7 +88,6 @@ type Interface struct {
|
|||||||
|
|
||||||
writers []udp.Conn
|
writers []udp.Conn
|
||||||
readers []io.ReadWriteCloser
|
readers []io.ReadWriteCloser
|
||||||
tunBatchSize int // batch size for TUN read/write batching
|
|
||||||
|
|
||||||
metricHandshakes metrics.Histogram
|
metricHandshakes metrics.Histogram
|
||||||
messageMetrics *MessageMetrics
|
messageMetrics *MessageMetrics
|
||||||
@@ -190,7 +187,6 @@ func NewInterface(ctx context.Context, c *InterfaceConfig) (*Interface, error) {
|
|||||||
relayManager: c.relayManager,
|
relayManager: c.relayManager,
|
||||||
connectionManager: c.connectionManager,
|
connectionManager: c.connectionManager,
|
||||||
conntrackCacheTimeout: c.ConntrackCacheTimeout,
|
conntrackCacheTimeout: c.ConntrackCacheTimeout,
|
||||||
tunBatchSize: c.tunBatchSize,
|
|
||||||
|
|
||||||
metricHandshakes: metrics.GetOrRegisterHistogram("handshakes", nil, metrics.NewExpDecaySample(1028, 0.015)),
|
metricHandshakes: metrics.GetOrRegisterHistogram("handshakes", nil, metrics.NewExpDecaySample(1028, 0.015)),
|
||||||
messageMetrics: c.MessageMetrics,
|
messageMetrics: c.MessageMetrics,
|
||||||
@@ -248,15 +244,6 @@ func (f *Interface) activate() {
|
|||||||
f.readers[i] = reader
|
f.readers[i] = reader
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enable batch reading on all readers if batch size > 1
|
|
||||||
if f.tunBatchSize > 1 {
|
|
||||||
for i := 0; i < f.routines; i++ {
|
|
||||||
if err := overlay.EnableBatchReading(f.readers[i]); err != nil {
|
|
||||||
f.l.WithError(err).WithField("routine", i).Warn("Failed to enable batch reading, falling back to single reads")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := f.inside.Activate(); err != nil {
|
if err := f.inside.Activate(); err != nil {
|
||||||
f.inside.Close()
|
f.inside.Close()
|
||||||
f.l.Fatal(err)
|
f.l.Fatal(err)
|
||||||
@@ -276,8 +263,6 @@ func (f *Interface) run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Interface) listenOut(i int) {
|
func (f *Interface) listenOut(i int) {
|
||||||
runtime.LockOSThread()
|
|
||||||
|
|
||||||
var li udp.Conn
|
var li udp.Conn
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
li = f.writers[i]
|
li = f.writers[i]
|
||||||
@@ -300,21 +285,13 @@ func (f *Interface) listenOut(i int) {
|
|||||||
func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
|
func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
|
||||||
runtime.LockOSThread()
|
runtime.LockOSThread()
|
||||||
|
|
||||||
conntrackCache := firewall.NewConntrackCacheTicker(f.conntrackCacheTimeout)
|
|
||||||
|
|
||||||
// Check if batch reading is available and enabled
|
|
||||||
batchReader := overlay.AsBatchReader(reader)
|
|
||||||
if batchReader != nil && f.tunBatchSize > 1 {
|
|
||||||
f.listenInBatched(reader, batchReader, i, conntrackCache)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to single-packet reading
|
|
||||||
packet := make([]byte, mtu)
|
packet := make([]byte, mtu)
|
||||||
out := make([]byte, mtu)
|
out := make([]byte, mtu)
|
||||||
fwPacket := &firewall.Packet{}
|
fwPacket := &firewall.Packet{}
|
||||||
nb := make([]byte, 12, 12)
|
nb := make([]byte, 12, 12)
|
||||||
|
|
||||||
|
conntrackCache := firewall.NewConntrackCacheTicker(f.conntrackCacheTimeout)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
n, err := reader.Read(packet)
|
n, err := reader.Read(packet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -331,54 +308,6 @@ func (f *Interface) listenIn(reader io.ReadWriteCloser, i int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Interface) listenInBatched(reader io.ReadWriteCloser, batchReader overlay.BatchReader, i int, conntrackCache *firewall.ConntrackCacheTicker) {
|
|
||||||
batchSize := f.tunBatchSize
|
|
||||||
|
|
||||||
// Pre-allocate buffers for batch reading
|
|
||||||
packets := make([][]byte, batchSize)
|
|
||||||
for j := range packets {
|
|
||||||
packets[j] = make([]byte, mtu)
|
|
||||||
}
|
|
||||||
sizes := make([]int, batchSize)
|
|
||||||
|
|
||||||
// Pre-allocate buffers for packet processing
|
|
||||||
out := make([]byte, mtu)
|
|
||||||
fwPacket := &firewall.Packet{}
|
|
||||||
nb := make([]byte, 12, 12)
|
|
||||||
|
|
||||||
// Pre-allocate buffer for batched UDP writes
|
|
||||||
pendingPackets := make([]udp.BatchPacket, 0, batchSize)
|
|
||||||
|
|
||||||
for {
|
|
||||||
// Read a batch of packets from TUN
|
|
||||||
n, err := batchReader.ReadBatch(packets, sizes)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, os.ErrClosed) && f.closed.Load() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f.l.WithError(err).Error("Error while reading outbound packets")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process all packets in the batch
|
|
||||||
cache := conntrackCache.Get(f.l)
|
|
||||||
for j := 0; j < n; j++ {
|
|
||||||
f.consumeInsidePacketBatched(packets[j][:sizes[j]], fwPacket, nb, out, i, cache, &pendingPackets)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flush all pending UDP writes
|
|
||||||
if len(pendingPackets) > 0 {
|
|
||||||
f.writers[i].WriteBatch(pendingPackets)
|
|
||||||
pendingPackets = pendingPackets[:0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *Interface) RegisterConfigChangeCallbacks(c *config.C) {
|
func (f *Interface) RegisterConfigChangeCallbacks(c *config.C) {
|
||||||
c.RegisterReloadCallback(f.reloadFirewall)
|
c.RegisterReloadCallback(f.reloadFirewall)
|
||||||
c.RegisterReloadCallback(f.reloadSendRecvError)
|
c.RegisterReloadCallback(f.reloadSendRecvError)
|
||||||
|
|||||||
+2
-9
@@ -713,23 +713,16 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
@@ -250,7 +246,6 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
|
|||||||
punchy: punchy,
|
punchy: punchy,
|
||||||
ConntrackCacheTimeout: conntrackCacheTimeout,
|
ConntrackCacheTimeout: conntrackCacheTimeout,
|
||||||
l: l,
|
l: l,
|
||||||
tunBatchSize: c.GetInt("listen.batch", 64),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var ifce *Interface
|
var ifce *Interface
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
//go:build !boringcrypto
|
//go:build !boringcrypto
|
||||||
// +build !boringcrypto
|
|
||||||
|
|
||||||
package nebula
|
package nebula
|
||||||
|
|
||||||
|
|||||||
+35
-15
@@ -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 {
|
||||||
|
if fp.Protocol == firewall.ProtoICMP {
|
||||||
|
minLen += minFwPacketLen + 2
|
||||||
|
} else {
|
||||||
minLen += minFwPacketLen
|
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 {
|
}
|
||||||
|
|
||||||
|
if fp.Fragment {
|
||||||
fp.RemotePort = 0
|
fp.RemotePort = 0
|
||||||
fp.LocalPort = 0
|
fp.LocalPort = 0
|
||||||
|
} 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 {
|
} else {
|
||||||
fp.LocalPort = binary.BigEndian.Uint16(data[ihl : ihl+2])
|
fp.LocalPort = binary.BigEndian.Uint16(data[ihl : ihl+2]) //src port
|
||||||
fp.RemotePort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4])
|
fp.RemotePort = binary.BigEndian.Uint16(data[ihl+2 : ihl+4]) //dst port
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
+17
-10
@@ -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...)
|
||||||
|
|||||||
@@ -16,38 +16,3 @@ type Device interface {
|
|||||||
SupportsMultiqueue() bool
|
SupportsMultiqueue() bool
|
||||||
NewMultiQueueReader() (io.ReadWriteCloser, error)
|
NewMultiQueueReader() (io.ReadWriteCloser, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BatchReader is an optional interface that devices can implement
|
|
||||||
// to support reading multiple packets in a single batch operation.
|
|
||||||
// This can significantly reduce syscall overhead under high load.
|
|
||||||
type BatchReader interface {
|
|
||||||
// ReadBatch reads up to len(packets) packets into the provided buffers.
|
|
||||||
// Each packet is read into packets[i] and its length is stored in sizes[i].
|
|
||||||
// Returns the number of packets read, or an error.
|
|
||||||
// A return of (0, nil) indicates no packets were available (non-blocking).
|
|
||||||
ReadBatch(packets [][]byte, sizes []int) (int, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AsBatchReader returns a BatchReader if the reader supports batch operations,
|
|
||||||
// otherwise returns nil.
|
|
||||||
func AsBatchReader(r io.ReadWriteCloser) BatchReader {
|
|
||||||
if br, ok := r.(BatchReader); ok {
|
|
||||||
return br
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchEnabler is an optional interface for devices that need explicit
|
|
||||||
// enabling of batch read support (e.g., setting non-blocking mode).
|
|
||||||
type BatchEnabler interface {
|
|
||||||
EnableBatchReading() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnableBatchReading enables batch reading on the device if supported.
|
|
||||||
// Returns nil if the device doesn't support or need explicit enabling.
|
|
||||||
func EnableBatchReading(d interface{}) error {
|
|
||||||
if be, ok := d.(BatchEnabler); ok {
|
|
||||||
return be.EnableBatchReading()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
+3
-396
@@ -24,11 +24,6 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// virtioNetHdrLen is the length of virtio_net_hdr (without mergeable buffers)
|
|
||||||
virtioNetHdrLen = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
type tun struct {
|
type tun struct {
|
||||||
io.ReadWriteCloser
|
io.ReadWriteCloser
|
||||||
fd int
|
fd int
|
||||||
@@ -39,13 +34,6 @@ type tun struct {
|
|||||||
TXQueueLen int
|
TXQueueLen int
|
||||||
deviceIndex int
|
deviceIndex int
|
||||||
ioctlFd uintptr
|
ioctlFd uintptr
|
||||||
nonBlocking bool // true if fd is in non-blocking mode
|
|
||||||
vnetHdr bool // true if IFF_VNET_HDR is enabled on the TUN device
|
|
||||||
|
|
||||||
// readBuf is used when vnetHdr is enabled to read the full packet+header
|
|
||||||
// before stripping the header. This is needed because caller-provided
|
|
||||||
// buffers are sized for MTU but kernel writes MTU+10 with virtio header.
|
|
||||||
readBuf []byte
|
|
||||||
|
|
||||||
Routes atomic.Pointer[[]Route]
|
Routes atomic.Pointer[[]Route]
|
||||||
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
|
routeTree atomic.Pointer[bart.Table[routing.Gateways]]
|
||||||
@@ -65,23 +53,6 @@ func (t *tun) Networks() []netip.Prefix {
|
|||||||
return t.vpnNetworks
|
return t.vpnNetworks
|
||||||
}
|
}
|
||||||
|
|
||||||
// tunVnetHdrSupported checks if the kernel supports IFF_VNET_HDR on TUN devices
|
|
||||||
func tunVnetHdrSupported() bool {
|
|
||||||
fd, err := unix.Open("/dev/net/tun", unix.O_RDONLY, 0)
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
defer unix.Close(fd)
|
|
||||||
|
|
||||||
var features uint32
|
|
||||||
err = ioctl(uintptr(fd), uintptr(unix.TUNGETFEATURES), uintptr(unsafe.Pointer(&features)))
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return features&unix.IFF_VNET_HDR != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
type ifReq struct {
|
type ifReq struct {
|
||||||
Name [16]byte
|
Name [16]byte
|
||||||
Flags uint16
|
Flags uint16
|
||||||
@@ -136,18 +107,11 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, multiqueu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if VNET_HDR is supported before trying to use it
|
|
||||||
useVnetHdr := tunVnetHdrSupported()
|
|
||||||
|
|
||||||
var req ifReq
|
var req ifReq
|
||||||
req.Flags = uint16(unix.IFF_TUN | unix.IFF_NO_PI)
|
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
|
||||||
}
|
}
|
||||||
if useVnetHdr {
|
|
||||||
req.Flags |= unix.IFF_VNET_HDR
|
|
||||||
}
|
|
||||||
|
|
||||||
nameStr := c.GetString("tun.dev", "")
|
nameStr := c.GetString("tun.dev", "")
|
||||||
copy(req.Name[:], nameStr)
|
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 {
|
||||||
@@ -158,13 +122,6 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, multiqueu
|
|||||||
}
|
}
|
||||||
name := strings.Trim(string(req.Name[:]), "\x00")
|
name := strings.Trim(string(req.Name[:]), "\x00")
|
||||||
|
|
||||||
// Track if VNET_HDR is in use
|
|
||||||
// Note: We don't call TUNSETOFFLOAD - just handle the headers manually
|
|
||||||
vnetHdrEnabled := useVnetHdr
|
|
||||||
if vnetHdrEnabled {
|
|
||||||
l.Info("TUN VNET_HDR enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
file := os.NewFile(uintptr(fd), "/dev/net/tun")
|
file := os.NewFile(uintptr(fd), "/dev/net/tun")
|
||||||
t, err := newTunGeneric(c, l, file, vpnNetworks)
|
t, err := newTunGeneric(c, l, file, vpnNetworks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -172,13 +129,6 @@ func newTun(c *config.C, l *logrus.Logger, vpnNetworks []netip.Prefix, multiqueu
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Device = name
|
t.Device = name
|
||||||
t.vnetHdr = vnetHdrEnabled
|
|
||||||
|
|
||||||
// Allocate read buffer for virtio header handling
|
|
||||||
// Buffer needs to be large enough for virtio header + max packet
|
|
||||||
if t.vnetHdr {
|
|
||||||
t.readBuf = make([]byte, t.MaxMTU+virtioNetHdrLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, nil
|
return t, nil
|
||||||
}
|
}
|
||||||
@@ -289,172 +239,21 @@ func (t *tun) SupportsMultiqueue() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
func (t *tun) NewMultiQueueReader() (io.ReadWriteCloser, error) {
|
||||||
fd, err := unix.Open("/dev/net/tun", os.O_RDWR|unix.O_NONBLOCK, 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
|
||||||
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_MULTI_QUEUE)
|
||||||
if t.vnetHdr {
|
|
||||||
req.Flags |= unix.IFF_VNET_HDR
|
|
||||||
}
|
|
||||||
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 {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := &tunBatchReader{fd: fd, device: t.Device, vnetHdr: t.vnetHdr}
|
file := os.NewFile(uintptr(fd), "/dev/net/tun")
|
||||||
if t.vnetHdr {
|
|
||||||
reader.readBuf = make([]byte, t.MaxMTU+virtioNetHdrLen)
|
|
||||||
}
|
|
||||||
return reader, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// tunBatchReader implements BatchReader for efficient batch packet reading
|
return file, nil
|
||||||
type tunBatchReader struct {
|
|
||||||
fd int
|
|
||||||
device string
|
|
||||||
vnetHdr bool
|
|
||||||
readBuf []byte // internal buffer for virtio header handling
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *tunBatchReader) Read(b []byte) (int, error) {
|
|
||||||
// Choose buffer: use internal buffer for vnetHdr, caller's buffer otherwise
|
|
||||||
readBuf := b
|
|
||||||
if r.vnetHdr {
|
|
||||||
readBuf = r.readBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use poll to wait for data, then read
|
|
||||||
for {
|
|
||||||
n, err := unix.Read(r.fd, readBuf)
|
|
||||||
if err == nil {
|
|
||||||
if r.vnetHdr && n > virtioNetHdrLen {
|
|
||||||
packetLen := n - virtioNetHdrLen
|
|
||||||
copy(b, readBuf[virtioNetHdrLen:n])
|
|
||||||
return packetLen, nil
|
|
||||||
}
|
|
||||||
if r.vnetHdr {
|
|
||||||
return 0, nil // No packet data
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
if err == unix.EAGAIN || err == unix.EWOULDBLOCK {
|
|
||||||
// Wait for data
|
|
||||||
pfds := []unix.PollFd{{Fd: int32(r.fd), Events: unix.POLLIN}}
|
|
||||||
_, err = unix.Poll(pfds, -1)
|
|
||||||
if err != nil {
|
|
||||||
if err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *tunBatchReader) Write(b []byte) (int, error) {
|
|
||||||
if !r.vnetHdr {
|
|
||||||
return unix.Write(r.fd, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use writev to prepend virtio header without copying the packet data
|
|
||||||
// Header is all zeros = no GSO, no checksum offload
|
|
||||||
var hdr [virtioNetHdrLen]byte
|
|
||||||
bufs := [][]byte{hdr[:], b}
|
|
||||||
|
|
||||||
n, err := unix.Writev(r.fd, bufs)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
// Return only the packet bytes written (exclude header)
|
|
||||||
if n > virtioNetHdrLen {
|
|
||||||
return n - virtioNetHdrLen, nil
|
|
||||||
}
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *tunBatchReader) Close() error {
|
|
||||||
return unix.Close(r.fd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBatch reads up to len(packets) packets from the TUN device.
|
|
||||||
// It drains all available packets without blocking, using poll() only
|
|
||||||
// when no packets have been read yet.
|
|
||||||
func (r *tunBatchReader) ReadBatch(packets [][]byte, sizes []int) (int, error) {
|
|
||||||
count := 0
|
|
||||||
maxPackets := len(packets)
|
|
||||||
if len(sizes) < maxPackets {
|
|
||||||
maxPackets = len(sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose read buffer based on vnetHdr
|
|
||||||
readBuf := packets[0] // Will be updated in loop for non-vnetHdr
|
|
||||||
if r.vnetHdr {
|
|
||||||
readBuf = r.readBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
for count < maxPackets {
|
|
||||||
if !r.vnetHdr {
|
|
||||||
readBuf = packets[count]
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := unix.Read(r.fd, readBuf)
|
|
||||||
if err == nil && n > 0 {
|
|
||||||
if r.vnetHdr {
|
|
||||||
if n > virtioNetHdrLen {
|
|
||||||
packetLen := n - virtioNetHdrLen
|
|
||||||
copy(packets[count], readBuf[virtioNetHdrLen:n])
|
|
||||||
sizes[count] = packetLen
|
|
||||||
} else {
|
|
||||||
// Malformed packet (no data after header), skip
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sizes[count] = n
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == unix.EAGAIN || err == unix.EWOULDBLOCK {
|
|
||||||
// No more packets available
|
|
||||||
if count > 0 {
|
|
||||||
// We have some packets, return them
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
// No packets yet, wait for at least one
|
|
||||||
pfds := []unix.PollFd{{Fd: int32(r.fd), Events: unix.POLLIN}}
|
|
||||||
_, err = unix.Poll(pfds, -1)
|
|
||||||
if err != nil {
|
|
||||||
if err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if count > 0 {
|
|
||||||
// Return what we have
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if n == 0 {
|
|
||||||
if count > 0 {
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
|
func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
|
||||||
@@ -463,27 +262,6 @@ func (t *tun) RoutesFor(ip netip.Addr) routing.Gateways {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *tun) Write(b []byte) (int, error) {
|
func (t *tun) Write(b []byte) (int, error) {
|
||||||
if !t.vnetHdr {
|
|
||||||
return t.writeSimple(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use writev to prepend virtio header without copying the packet data
|
|
||||||
// Header is all zeros = no GSO, no checksum offload
|
|
||||||
var hdr [virtioNetHdrLen]byte
|
|
||||||
bufs := [][]byte{hdr[:], b}
|
|
||||||
|
|
||||||
n, err := unix.Writev(t.fd, bufs)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
// Return only the packet bytes written (exclude header)
|
|
||||||
if n > virtioNetHdrLen {
|
|
||||||
return n - virtioNetHdrLen, nil
|
|
||||||
}
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tun) writeSimple(b []byte) (int, error) {
|
|
||||||
var nn int
|
var nn int
|
||||||
maximum := len(b)
|
maximum := len(b)
|
||||||
|
|
||||||
@@ -506,177 +284,6 @@ func (t *tun) writeSimple(b []byte) (int, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnableBatchReading sets the TUN fd to non-blocking mode to enable batch reading.
|
|
||||||
// This should be called before using ReadBatch.
|
|
||||||
func (t *tun) EnableBatchReading() error {
|
|
||||||
if t.nonBlocking {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := unix.SetNonblock(t.fd, true)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
t.nonBlocking = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read overrides the default Read to handle non-blocking mode and virtio headers
|
|
||||||
func (t *tun) Read(b []byte) (int, error) {
|
|
||||||
if !t.vnetHdr {
|
|
||||||
return t.readSimple(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
// With VNET_HDR, read into internal buffer (which has space for header)
|
|
||||||
// then copy packet data to caller's buffer
|
|
||||||
if !t.nonBlocking {
|
|
||||||
n, err := t.ReadWriteCloser.Read(t.readBuf)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
if n <= virtioNetHdrLen {
|
|
||||||
return 0, nil // No packet data
|
|
||||||
}
|
|
||||||
packetLen := n - virtioNetHdrLen
|
|
||||||
copy(b, t.readBuf[virtioNetHdrLen:n])
|
|
||||||
return packetLen, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-blocking read with poll
|
|
||||||
for {
|
|
||||||
n, err := unix.Read(t.fd, t.readBuf)
|
|
||||||
if err == nil {
|
|
||||||
if n <= virtioNetHdrLen {
|
|
||||||
return 0, nil // No packet data
|
|
||||||
}
|
|
||||||
packetLen := n - virtioNetHdrLen
|
|
||||||
copy(b, t.readBuf[virtioNetHdrLen:n])
|
|
||||||
return packetLen, nil
|
|
||||||
}
|
|
||||||
if err == unix.EAGAIN || err == unix.EWOULDBLOCK {
|
|
||||||
pfds := []unix.PollFd{{Fd: int32(t.fd), Events: unix.POLLIN}}
|
|
||||||
_, err = unix.Poll(pfds, -1)
|
|
||||||
if err != nil {
|
|
||||||
if err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *tun) readSimple(b []byte) (int, error) {
|
|
||||||
if !t.nonBlocking {
|
|
||||||
return t.ReadWriteCloser.Read(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
n, err := unix.Read(t.fd, b)
|
|
||||||
if err == nil {
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
if err == unix.EAGAIN || err == unix.EWOULDBLOCK {
|
|
||||||
pfds := []unix.PollFd{{Fd: int32(t.fd), Events: unix.POLLIN}}
|
|
||||||
_, err = unix.Poll(pfds, -1)
|
|
||||||
if err != nil {
|
|
||||||
if err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadBatch reads up to len(packets) packets from the TUN device.
|
|
||||||
// EnableBatchReading must be called first.
|
|
||||||
func (t *tun) ReadBatch(packets [][]byte, sizes []int) (int, error) {
|
|
||||||
if !t.nonBlocking {
|
|
||||||
// Fallback to single read if non-blocking not enabled
|
|
||||||
n, err := t.Read(packets[0])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
sizes[0] = n
|
|
||||||
return 1, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
count := 0
|
|
||||||
maxPackets := len(packets)
|
|
||||||
if len(sizes) < maxPackets {
|
|
||||||
maxPackets = len(sizes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Choose read buffer based on vnetHdr
|
|
||||||
// With vnetHdr, we need to read into internal buffer (has space for header)
|
|
||||||
// then copy packet data to caller's buffer
|
|
||||||
readBuf := packets[0] // Will be updated in the loop
|
|
||||||
if t.vnetHdr {
|
|
||||||
readBuf = t.readBuf
|
|
||||||
}
|
|
||||||
|
|
||||||
for count < maxPackets {
|
|
||||||
if !t.vnetHdr {
|
|
||||||
readBuf = packets[count]
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := unix.Read(t.fd, readBuf)
|
|
||||||
if err == nil && n > 0 {
|
|
||||||
if t.vnetHdr {
|
|
||||||
if n > virtioNetHdrLen {
|
|
||||||
packetLen := n - virtioNetHdrLen
|
|
||||||
copy(packets[count], readBuf[virtioNetHdrLen:n])
|
|
||||||
sizes[count] = packetLen
|
|
||||||
} else {
|
|
||||||
// Malformed packet (no data after header), skip
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sizes[count] = n
|
|
||||||
}
|
|
||||||
count++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == unix.EAGAIN || err == unix.EWOULDBLOCK {
|
|
||||||
// No more packets available
|
|
||||||
if count > 0 {
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
// No packets yet, wait for at least one
|
|
||||||
pfds := []unix.PollFd{{Fd: int32(t.fd), Events: unix.POLLIN}}
|
|
||||||
_, err = unix.Poll(pfds, -1)
|
|
||||||
if err != nil {
|
|
||||||
if err == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if count > 0 {
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if n == 0 {
|
|
||||||
if count > 0 {
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
return 0, io.EOF
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count, 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)
|
||||||
|
|||||||
+1
-1
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+2
-2
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
-18
@@ -13,22 +13,13 @@ type EncReader func(
|
|||||||
payload []byte,
|
payload []byte,
|
||||||
)
|
)
|
||||||
|
|
||||||
// BatchPacket represents a single packet in a batch write operation
|
|
||||||
type BatchPacket struct {
|
|
||||||
Payload []byte
|
|
||||||
Addr netip.AddrPort
|
|
||||||
}
|
|
||||||
|
|
||||||
type Conn interface {
|
type Conn interface {
|
||||||
Rebind() error
|
Rebind() error
|
||||||
LocalAddr() (netip.AddrPort, error)
|
LocalAddr() (netip.AddrPort, error)
|
||||||
ListenOut(r EncReader)
|
ListenOut(r EncReader)
|
||||||
WriteTo(b []byte, addr netip.AddrPort) error
|
WriteTo(b []byte, addr netip.AddrPort) error
|
||||||
WriteBatch(pkts []BatchPacket) (int, error)
|
|
||||||
ReloadConfig(c *config.C)
|
ReloadConfig(c *config.C)
|
||||||
SupportsMultipleReaders() bool
|
SupportsMultipleReaders() bool
|
||||||
SupportsGSO() bool
|
|
||||||
SupportsGRO() bool
|
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,18 +37,9 @@ func (NoopConn) ListenOut(_ EncReader) {
|
|||||||
func (NoopConn) SupportsMultipleReaders() bool {
|
func (NoopConn) SupportsMultipleReaders() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
func (NoopConn) SupportsGSO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
func (NoopConn) SupportsGRO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
func (NoopConn) WriteTo(_ []byte, _ netip.AddrPort) error {
|
func (NoopConn) WriteTo(_ []byte, _ netip.AddrPort) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (NoopConn) WriteBatch(pkts []BatchPacket) (int, error) {
|
|
||||||
return len(pkts), nil
|
|
||||||
}
|
|
||||||
func (NoopConn) ReloadConfig(_ *config.C) {
|
func (NoopConn) ReloadConfig(_ *config.C) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,14 +188,6 @@ func (u *StdConn) SupportsMultipleReaders() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) SupportsGSO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *StdConn) SupportsGRO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *StdConn) Rebind() error {
|
func (u *StdConn) Rebind() error {
|
||||||
var err error
|
var err error
|
||||||
if u.isV4 {
|
if u.isV4 {
|
||||||
@@ -210,12 +202,3 @@ func (u *StdConn) Rebind() error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) WriteBatch(pkts []BatchPacket) (int, error) {
|
|
||||||
for i := range pkts {
|
|
||||||
if err := u.WriteTo(pkts[i].Payload, pkts[i].Addr); err != nil {
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(pkts), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -101,20 +101,3 @@ func (u *GenericConn) ListenOut(r EncReader) {
|
|||||||
func (u *GenericConn) SupportsMultipleReaders() bool {
|
func (u *GenericConn) SupportsMultipleReaders() bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *GenericConn) SupportsGSO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *GenericConn) SupportsGRO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *GenericConn) WriteBatch(pkts []BatchPacket) (int, error) {
|
|
||||||
for i := range pkts {
|
|
||||||
if err := u.WriteTo(pkts[i].Payload, pkts[i].Addr); err != nil {
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(pkts), nil
|
|
||||||
}
|
|
||||||
|
|||||||
+171
-466
@@ -4,8 +4,8 @@
|
|||||||
package udp
|
package udp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
@@ -19,305 +19,217 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StdConn struct {
|
type StdConn struct {
|
||||||
sysFd int
|
udpConn *net.UDPConn
|
||||||
|
rawConn syscall.RawConn
|
||||||
|
sockFd int
|
||||||
isV4 bool
|
isV4 bool
|
||||||
l *logrus.Logger
|
l *logrus.Logger
|
||||||
batch int
|
batch int
|
||||||
gsoSupported bool
|
|
||||||
groSupported bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func maybeIPV4(ip net.IP) (net.IP, bool) {
|
func setReusePort(network, address string, c syscall.RawConn) error {
|
||||||
ip4 := ip.To4()
|
var opErr error
|
||||||
if ip4 != nil {
|
err := c.Control(func(fd uintptr) {
|
||||||
return ip4, true
|
opErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
|
||||||
|
//CloseOnExec already set by the runtime
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return ip, false
|
return opErr
|
||||||
}
|
|
||||||
|
|
||||||
// supportsUDPOffload checks if the kernel supports UDP GSO (Generic Segmentation Offload)
|
|
||||||
// by attempting to get the UDP_SEGMENT socket option.
|
|
||||||
func supportsUDPOffload(fd int) bool {
|
|
||||||
_, err := unix.GetsockoptInt(fd, unix.IPPROTO_UDP, unix.UDP_SEGMENT)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// supportsUDPGRO checks if the kernel supports UDP GRO (Generic Receive Offload)
|
|
||||||
// and attempts to enable it on the socket.
|
|
||||||
func supportsUDPGRO(fd int) bool {
|
|
||||||
// Try to enable UDP_GRO
|
|
||||||
err := unix.SetsockoptInt(fd, unix.IPPROTO_UDP, unix.UDP_GRO, 1)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Maximum number of datagrams that can be coalesced with GSO/GRO
|
|
||||||
udpSegmentMaxDatagrams = 64
|
|
||||||
|
|
||||||
// Maximum size of a GRO coalesced packet (64KB is the practical limit)
|
|
||||||
// This is udpSegmentMaxDatagrams * MTU but capped at 65535
|
|
||||||
groMaxPacketSize = 65535
|
|
||||||
)
|
|
||||||
|
|
||||||
// setGSOSize writes a UDP_SEGMENT control message to the provided buffer
|
|
||||||
// with the given segment size. Returns the actual control message length.
|
|
||||||
func setGSOSize(control []byte, gsoSize uint16) int {
|
|
||||||
// Build the cmsghdr structure
|
|
||||||
cmsgLen := unix.CmsgLen(2) // 2 bytes for uint16 segment size
|
|
||||||
cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&control[0]))
|
|
||||||
cmsg.Level = unix.IPPROTO_UDP
|
|
||||||
cmsg.Type = unix.UDP_SEGMENT
|
|
||||||
cmsg.SetLen(cmsgLen)
|
|
||||||
|
|
||||||
// Write the segment size after the header (after cmsghdr)
|
|
||||||
binary.NativeEndian.PutUint16(control[unix.SizeofCmsghdr:], gsoSize)
|
|
||||||
|
|
||||||
return unix.CmsgSpace(2) // aligned size
|
|
||||||
}
|
|
||||||
|
|
||||||
// getGROSize parses a control message buffer to extract the UDP_GRO segment size.
|
|
||||||
// Returns 0 if no GRO control message is present (meaning the packet is not coalesced).
|
|
||||||
func getGROSize(control []byte, controlLen int) uint16 {
|
|
||||||
if controlLen < unix.SizeofCmsghdr {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse control messages
|
|
||||||
for offset := 0; offset < controlLen; {
|
|
||||||
if offset+unix.SizeofCmsghdr > controlLen {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
cmsg := (*unix.Cmsghdr)(unsafe.Pointer(&control[offset]))
|
|
||||||
cmsgDataLen := int(cmsg.Len) - unix.SizeofCmsghdr
|
|
||||||
if cmsgDataLen < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmsg.Level == unix.IPPROTO_UDP && cmsg.Type == unix.UDP_GRO {
|
|
||||||
if cmsgDataLen >= 2 {
|
|
||||||
return binary.NativeEndian.Uint16(control[offset+unix.SizeofCmsghdr:])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to next control message (aligned)
|
|
||||||
offset += unix.CmsgSpace(cmsgDataLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewListener(l *logrus.Logger, ip netip.Addr, port int, multi bool, batch int) (Conn, error) {
|
func NewListener(l *logrus.Logger, ip netip.Addr, port int, multi bool, batch int) (Conn, error) {
|
||||||
af := unix.AF_INET6
|
listen := netip.AddrPortFrom(ip, uint16(port))
|
||||||
if ip.Is4() {
|
lc := net.ListenConfig{}
|
||||||
af = unix.AF_INET
|
if multi {
|
||||||
|
lc.Control = setReusePort
|
||||||
}
|
}
|
||||||
syscall.ForkLock.RLock()
|
network := "udp"
|
||||||
fd, err := unix.Socket(af, unix.SOCK_DGRAM, unix.IPPROTO_UDP)
|
if ip.Is4() { //todo I am not a fan of this, would prefer a default where we always bind dual-stack unless given a non-wildcard v4 addr
|
||||||
if err == nil {
|
network = "udp4"
|
||||||
unix.CloseOnExec(fd)
|
|
||||||
}
|
}
|
||||||
syscall.ForkLock.RUnlock()
|
//this context is only used during the bind operation, you can't cancel it to kill the socket
|
||||||
|
pc, err := lc.ListenPacket(context.Background(), network, listen.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
unix.Close(fd)
|
|
||||||
return nil, fmt.Errorf("unable to open socket: %s", err)
|
return nil, fmt.Errorf("unable to open socket: %s", err)
|
||||||
}
|
}
|
||||||
|
udpConn := pc.(*net.UDPConn)
|
||||||
if multi {
|
rawConn, err := udpConn.SyscallConn()
|
||||||
if err = unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, 1); err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to set SO_REUSEPORT: %s", err)
|
_ = udpConn.Close()
|
||||||
}
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var sa unix.Sockaddr
|
//steal the socket's fd for sending
|
||||||
if ip.Is4() {
|
var sockFd uintptr
|
||||||
sa4 := &unix.SockaddrInet4{Port: port}
|
err = rawConn.Control(func(fd uintptr) {
|
||||||
sa4.Addr = ip.As4()
|
sockFd = fd
|
||||||
sa = sa4
|
})
|
||||||
} else {
|
if err != nil {
|
||||||
sa6 := &unix.SockaddrInet6{Port: port}
|
_ = udpConn.Close()
|
||||||
sa6.Addr = ip.As16()
|
return nil, err
|
||||||
sa = sa6
|
|
||||||
}
|
|
||||||
if err = unix.Bind(fd, sa); err != nil {
|
|
||||||
return nil, fmt.Errorf("unable to bind to socket: %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gsoSupported := supportsUDPOffload(fd)
|
return &StdConn{
|
||||||
if gsoSupported {
|
udpConn: udpConn,
|
||||||
l.Info("UDP GSO offload is supported")
|
rawConn: rawConn,
|
||||||
}
|
sockFd: int(sockFd),
|
||||||
|
isV4: ip.Is4(),
|
||||||
groSupported := supportsUDPGRO(fd)
|
l: l,
|
||||||
if groSupported {
|
batch: batch,
|
||||||
l.Info("UDP GRO offload is supported and enabled")
|
}, err
|
||||||
}
|
|
||||||
|
|
||||||
return &StdConn{sysFd: fd, isV4: ip.Is4(), l: l, batch: batch, gsoSupported: gsoSupported, groSupported: groSupported}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) SupportsMultipleReaders() bool {
|
func (u *StdConn) SupportsMultipleReaders() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) SupportsGSO() bool {
|
|
||||||
return u.gsoSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *StdConn) SupportsGRO() bool {
|
|
||||||
return u.groSupported
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *StdConn) Rebind() error {
|
func (u *StdConn) Rebind() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) getSockOptInt(opt int) (int, error) {
|
||||||
|
if u.rawConn == nil {
|
||||||
|
return 0, fmt.Errorf("no UDP connection")
|
||||||
|
}
|
||||||
|
var out int
|
||||||
|
var opErr error
|
||||||
|
err := u.rawConn.Control(func(fd uintptr) {
|
||||||
|
out, opErr = unix.GetsockoptInt(int(fd), unix.SOL_SOCKET, opt)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return out, opErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) setSockOptInt(opt int, n int) error {
|
||||||
|
if u.rawConn == nil {
|
||||||
|
return fmt.Errorf("no UDP connection")
|
||||||
|
}
|
||||||
|
var opErr error
|
||||||
|
err := u.rawConn.Control(func(fd uintptr) {
|
||||||
|
opErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, opt, n)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return opErr
|
||||||
|
}
|
||||||
|
|
||||||
func (u *StdConn) SetRecvBuffer(n int) error {
|
func (u *StdConn) SetRecvBuffer(n int) error {
|
||||||
return unix.SetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_RCVBUFFORCE, n)
|
return u.setSockOptInt(unix.SO_RCVBUFFORCE, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) SetSendBuffer(n int) error {
|
func (u *StdConn) SetSendBuffer(n int) error {
|
||||||
return unix.SetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_SNDBUFFORCE, n)
|
return u.setSockOptInt(unix.SO_SNDBUFFORCE, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) SetSoMark(mark int) error {
|
func (u *StdConn) SetSoMark(mark int) error {
|
||||||
return unix.SetsockoptInt(u.sysFd, unix.SOL_SOCKET, unix.SO_MARK, mark)
|
return u.setSockOptInt(unix.SO_MARK, mark)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) GetRecvBuffer() (int, error) {
|
func (u *StdConn) GetRecvBuffer() (int, error) {
|
||||||
return unix.GetsockoptInt(int(u.sysFd), unix.SOL_SOCKET, unix.SO_RCVBUF)
|
return u.getSockOptInt(unix.SO_RCVBUF)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) GetSendBuffer() (int, error) {
|
func (u *StdConn) GetSendBuffer() (int, error) {
|
||||||
return unix.GetsockoptInt(int(u.sysFd), unix.SOL_SOCKET, unix.SO_SNDBUF)
|
return u.getSockOptInt(unix.SO_SNDBUF)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) GetSoMark() (int, error) {
|
func (u *StdConn) GetSoMark() (int, error) {
|
||||||
return unix.GetsockoptInt(int(u.sysFd), unix.SOL_SOCKET, unix.SO_MARK)
|
return u.getSockOptInt(unix.SO_MARK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) LocalAddr() (netip.AddrPort, error) {
|
func (u *StdConn) LocalAddr() (netip.AddrPort, error) {
|
||||||
sa, err := unix.Getsockname(u.sysFd)
|
addr := u.udpConn.LocalAddr()
|
||||||
if err != nil {
|
return netip.ParseAddrPort(addr.String())
|
||||||
return netip.AddrPort{}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch sa := sa.(type) {
|
func recvmmsg(fd uintptr, msgs []rawMessage) (int, bool, error) {
|
||||||
case *unix.SockaddrInet4:
|
var errno syscall.Errno
|
||||||
return netip.AddrPortFrom(netip.AddrFrom4(sa.Addr), uint16(sa.Port)), nil
|
n, _, errno := unix.Syscall6(
|
||||||
|
|
||||||
case *unix.SockaddrInet6:
|
|
||||||
return netip.AddrPortFrom(netip.AddrFrom16(sa.Addr), uint16(sa.Port)), nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return netip.AddrPort{}, fmt.Errorf("unsupported sock type: %T", sa)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *StdConn) ListenOut(r EncReader) {
|
|
||||||
var ip netip.Addr
|
|
||||||
|
|
||||||
msgs, buffers, names, controls := u.PrepareRawMessages(u.batch)
|
|
||||||
read := u.ReadMulti
|
|
||||||
if u.batch == 1 {
|
|
||||||
read = u.ReadSingle
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the original control buffer size for resetting after each read
|
|
||||||
controlLen := 0
|
|
||||||
if u.groSupported && len(controls) > 0 && len(controls[0]) > 0 {
|
|
||||||
controlLen = len(controls[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
// Reset Controllen before each read - the kernel updates this field
|
|
||||||
// after recvmsg to indicate actual received control data length
|
|
||||||
if controlLen > 0 {
|
|
||||||
for i := range msgs {
|
|
||||||
setMsghdrControllen(&msgs[i].Hdr, controlLen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
n, err := read(msgs)
|
|
||||||
if err != nil {
|
|
||||||
u.l.WithError(err).Debug("udp socket is closed, exiting read loop")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < n; i++ {
|
|
||||||
// Extract source address
|
|
||||||
if u.isV4 {
|
|
||||||
ip, _ = netip.AddrFromSlice(names[i][4:8])
|
|
||||||
} else {
|
|
||||||
ip, _ = netip.AddrFromSlice(names[i][8:24])
|
|
||||||
}
|
|
||||||
srcAddr := netip.AddrPortFrom(ip.Unmap(), binary.BigEndian.Uint16(names[i][2:4]))
|
|
||||||
|
|
||||||
// Check for GRO coalesced packet
|
|
||||||
totalLen := int(msgs[i].Len)
|
|
||||||
segmentSize := uint16(0)
|
|
||||||
if controlLen > 0 {
|
|
||||||
segmentSize = getGROSize(controls[i], getMsghdrControllen(&msgs[i].Hdr))
|
|
||||||
}
|
|
||||||
|
|
||||||
if segmentSize > 0 && totalLen > int(segmentSize) {
|
|
||||||
// This is a GRO coalesced packet - split it into individual datagrams
|
|
||||||
for offset := 0; offset < totalLen; {
|
|
||||||
packetLen := int(segmentSize)
|
|
||||||
if offset+packetLen > totalLen {
|
|
||||||
// Last packet may be smaller
|
|
||||||
packetLen = totalLen - offset
|
|
||||||
}
|
|
||||||
r(srcAddr, buffers[i][offset:offset+packetLen])
|
|
||||||
offset += packetLen
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Single packet, no coalescing
|
|
||||||
r(srcAddr, buffers[i][:totalLen])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *StdConn) ReadSingle(msgs []rawMessage) (int, error) {
|
|
||||||
for {
|
|
||||||
n, _, err := unix.Syscall6(
|
|
||||||
unix.SYS_RECVMSG,
|
|
||||||
uintptr(u.sysFd),
|
|
||||||
uintptr(unsafe.Pointer(&(msgs[0].Hdr))),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != 0 {
|
|
||||||
return 0, &net.OpError{Op: "recvmsg", Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
msgs[0].Len = uint32(n)
|
|
||||||
return 1, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *StdConn) ReadMulti(msgs []rawMessage) (int, error) {
|
|
||||||
for {
|
|
||||||
n, _, err := unix.Syscall6(
|
|
||||||
unix.SYS_RECVMMSG,
|
unix.SYS_RECVMMSG,
|
||||||
uintptr(u.sysFd),
|
fd,
|
||||||
uintptr(unsafe.Pointer(&msgs[0])),
|
uintptr(unsafe.Pointer(&msgs[0])),
|
||||||
uintptr(len(msgs)),
|
uintptr(len(msgs)),
|
||||||
unix.MSG_WAITFORONE,
|
unix.MSG_WAITFORONE,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
if errno == syscall.EAGAIN || errno == syscall.EWOULDBLOCK {
|
||||||
if err != 0 {
|
// No data available, block for I/O and try again.
|
||||||
return 0, &net.OpError{Op: "recvmmsg", Err: err}
|
return int(n), false, nil
|
||||||
|
}
|
||||||
|
if errno != 0 {
|
||||||
|
return int(n), true, &net.OpError{Op: "recvmmsg", Err: errno}
|
||||||
|
}
|
||||||
|
return int(n), true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return int(n), nil
|
func (u *StdConn) listenOutSingle(r EncReader) {
|
||||||
|
var err error
|
||||||
|
var n int
|
||||||
|
var from netip.AddrPort
|
||||||
|
buffer := make([]byte, MTU)
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, from, err = u.udpConn.ReadFromUDPAddrPort(buffer)
|
||||||
|
if err != nil {
|
||||||
|
u.l.WithError(err).Debug("udp socket is closed, exiting read loop")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
from = netip.AddrPortFrom(from.Addr().Unmap(), from.Port())
|
||||||
|
r(from, buffer[:n])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) listenOutBatch(r EncReader) {
|
||||||
|
var ip netip.Addr
|
||||||
|
var n int
|
||||||
|
var operr error
|
||||||
|
|
||||||
|
msgs, buffers, names := u.PrepareRawMessages(u.batch)
|
||||||
|
|
||||||
|
//reader needs to capture variables from this function, since it's used as a lambda with rawConn.Read
|
||||||
|
//defining it outside the loop so it gets re-used
|
||||||
|
reader := func(fd uintptr) (done bool) {
|
||||||
|
n, done, operr = recvmmsg(fd, msgs)
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := u.rawConn.Read(reader)
|
||||||
|
if err != nil {
|
||||||
|
u.l.WithError(err).Debug("udp socket is closed, exiting read loop")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if operr != nil {
|
||||||
|
u.l.WithError(operr).Debug("operr: udp socket is closed, exiting read loop")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
ip, _ = netip.AddrFromSlice(names[i][4:8])
|
||||||
|
} else {
|
||||||
|
ip, _ = netip.AddrFromSlice(names[i][8:24])
|
||||||
|
}
|
||||||
|
r(netip.AddrPortFrom(ip.Unmap(), binary.BigEndian.Uint16(names[i][2:4])), buffers[i][:msgs[i].Len])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *StdConn) ListenOut(r EncReader) {
|
||||||
|
if u.batch == 1 {
|
||||||
|
//save some ram by not calling PrepareRawMessages for fields we won't use
|
||||||
|
//we could also make this path more common by calling recvmmsg with msgs[:1],
|
||||||
|
//but that's still the recvmmsg syscall, which would be a change
|
||||||
|
u.listenOutSingle(r)
|
||||||
|
} else {
|
||||||
|
u.listenOutBatch(r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +249,7 @@ func (u *StdConn) writeTo6(b []byte, ip netip.AddrPort) error {
|
|||||||
for {
|
for {
|
||||||
_, _, err := unix.Syscall6(
|
_, _, err := unix.Syscall6(
|
||||||
unix.SYS_SENDTO,
|
unix.SYS_SENDTO,
|
||||||
uintptr(u.sysFd),
|
uintptr(u.sockFd),
|
||||||
uintptr(unsafe.Pointer(&b[0])),
|
uintptr(unsafe.Pointer(&b[0])),
|
||||||
uintptr(len(b)),
|
uintptr(len(b)),
|
||||||
uintptr(0),
|
uintptr(0),
|
||||||
@@ -366,7 +278,7 @@ func (u *StdConn) writeTo4(b []byte, ip netip.AddrPort) error {
|
|||||||
for {
|
for {
|
||||||
_, _, err := unix.Syscall6(
|
_, _, err := unix.Syscall6(
|
||||||
unix.SYS_SENDTO,
|
unix.SYS_SENDTO,
|
||||||
uintptr(u.sysFd),
|
uintptr(u.sockFd),
|
||||||
uintptr(unsafe.Pointer(&b[0])),
|
uintptr(unsafe.Pointer(&b[0])),
|
||||||
uintptr(len(b)),
|
uintptr(len(b)),
|
||||||
uintptr(0),
|
uintptr(0),
|
||||||
@@ -432,236 +344,29 @@ func (u *StdConn) ReloadConfig(c *config.C) {
|
|||||||
|
|
||||||
func (u *StdConn) getMemInfo(meminfo *[unix.SK_MEMINFO_VARS]uint32) error {
|
func (u *StdConn) getMemInfo(meminfo *[unix.SK_MEMINFO_VARS]uint32) error {
|
||||||
var vallen uint32 = 4 * unix.SK_MEMINFO_VARS
|
var vallen uint32 = 4 * unix.SK_MEMINFO_VARS
|
||||||
_, _, err := unix.Syscall6(unix.SYS_GETSOCKOPT, uintptr(u.sysFd), uintptr(unix.SOL_SOCKET), uintptr(unix.SO_MEMINFO), uintptr(unsafe.Pointer(meminfo)), uintptr(unsafe.Pointer(&vallen)), 0)
|
|
||||||
if err != 0 {
|
if u.rawConn == nil {
|
||||||
|
return fmt.Errorf("no UDP connection")
|
||||||
|
}
|
||||||
|
var opErr error
|
||||||
|
err := u.rawConn.Control(func(fd uintptr) {
|
||||||
|
_, _, syserr := unix.Syscall6(unix.SYS_GETSOCKOPT, fd, uintptr(unix.SOL_SOCKET), uintptr(unix.SO_MEMINFO), uintptr(unsafe.Pointer(meminfo)), uintptr(unsafe.Pointer(&vallen)), 0)
|
||||||
|
if syserr != 0 {
|
||||||
|
opErr = syserr
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return opErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) Close() error {
|
func (u *StdConn) Close() error {
|
||||||
return syscall.Close(u.sysFd)
|
if u.udpConn != nil {
|
||||||
|
return u.udpConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) WriteBatch(pkts []BatchPacket) (int, error) {
|
|
||||||
if len(pkts) == 0 {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If GSO is supported, try to coalesce packets to the same destination
|
|
||||||
if u.gsoSupported {
|
|
||||||
return u.writeBatchGSO(pkts)
|
|
||||||
}
|
|
||||||
|
|
||||||
return u.writeBatchSendmmsg(pkts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeBatchSendmmsg sends packets using sendmmsg without GSO coalescing
|
|
||||||
func (u *StdConn) writeBatchSendmmsg(pkts []BatchPacket) (int, error) {
|
|
||||||
msgs := make([]rawMessage, len(pkts))
|
|
||||||
iovecs := make([]iovec, len(pkts))
|
|
||||||
var names4 []unix.RawSockaddrInet4
|
|
||||||
var names6 []unix.RawSockaddrInet6
|
|
||||||
|
|
||||||
if u.isV4 {
|
|
||||||
names4 = make([]unix.RawSockaddrInet4, len(pkts))
|
|
||||||
} else {
|
|
||||||
names6 = make([]unix.RawSockaddrInet6, len(pkts))
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range pkts {
|
|
||||||
setIovecBase(&iovecs[i], &pkts[i].Payload[0])
|
|
||||||
setIovecLen(&iovecs[i], len(pkts[i].Payload))
|
|
||||||
msgs[i].Hdr.Iov = &iovecs[i]
|
|
||||||
setMsghdrIovlen(&msgs[i].Hdr, 1)
|
|
||||||
|
|
||||||
if u.isV4 {
|
|
||||||
names4[i].Family = unix.AF_INET
|
|
||||||
names4[i].Addr = pkts[i].Addr.Addr().As4()
|
|
||||||
binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&names4[i].Port))[:], pkts[i].Addr.Port())
|
|
||||||
msgs[i].Hdr.Name = (*byte)(unsafe.Pointer(&names4[i]))
|
|
||||||
msgs[i].Hdr.Namelen = unix.SizeofSockaddrInet4
|
|
||||||
} else {
|
|
||||||
names6[i].Family = unix.AF_INET6
|
|
||||||
names6[i].Addr = pkts[i].Addr.Addr().As16()
|
|
||||||
binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&names6[i].Port))[:], pkts[i].Addr.Port())
|
|
||||||
msgs[i].Hdr.Name = (*byte)(unsafe.Pointer(&names6[i]))
|
|
||||||
msgs[i].Hdr.Namelen = unix.SizeofSockaddrInet6
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sent int
|
|
||||||
for sent < len(msgs) {
|
|
||||||
n, _, errno := unix.Syscall6(
|
|
||||||
unix.SYS_SENDMMSG,
|
|
||||||
uintptr(u.sysFd),
|
|
||||||
uintptr(unsafe.Pointer(&msgs[sent])),
|
|
||||||
uintptr(len(msgs)-sent),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
|
|
||||||
if errno == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if errno != 0 {
|
|
||||||
return sent, &net.OpError{Op: "sendmmsg", Err: errno}
|
|
||||||
}
|
|
||||||
|
|
||||||
sent += int(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sent, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeBatchGSO sends packets using GSO coalescing when possible.
|
|
||||||
// Packets to the same destination with the same size are coalesced into a single
|
|
||||||
// GSO message. Mixed destinations or sizes fall back to individual sendmmsg calls.
|
|
||||||
func (u *StdConn) writeBatchGSO(pkts []BatchPacket) (int, error) {
|
|
||||||
// Group packets by destination and try to coalesce
|
|
||||||
totalSent := 0
|
|
||||||
i := 0
|
|
||||||
|
|
||||||
for i < len(pkts) {
|
|
||||||
// Find a run of packets to the same destination with compatible sizes
|
|
||||||
startIdx := i
|
|
||||||
dst := pkts[i].Addr
|
|
||||||
segmentSize := len(pkts[i].Payload)
|
|
||||||
|
|
||||||
// Count how many packets we can coalesce (same destination, same size except possibly last)
|
|
||||||
coalescedCount := 1
|
|
||||||
totalSize := segmentSize
|
|
||||||
for i+coalescedCount < len(pkts) && coalescedCount < udpSegmentMaxDatagrams {
|
|
||||||
next := pkts[i+coalescedCount]
|
|
||||||
if next.Addr != dst {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
nextSize := len(next.Payload)
|
|
||||||
// For GSO, all packets except the last must have the same size
|
|
||||||
// The last packet can be smaller (but not larger)
|
|
||||||
if nextSize != segmentSize {
|
|
||||||
// Check if this could be the last packet (smaller is ok)
|
|
||||||
if nextSize < segmentSize && i+coalescedCount == len(pkts)-1 {
|
|
||||||
coalescedCount++
|
|
||||||
totalSize += nextSize
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
coalescedCount++
|
|
||||||
totalSize += nextSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have multiple packets to coalesce, use GSO
|
|
||||||
if coalescedCount > 1 {
|
|
||||||
err := u.sendGSO(pkts[startIdx:startIdx+coalescedCount], dst, segmentSize, totalSize)
|
|
||||||
if err != nil {
|
|
||||||
// If GSO fails (e.g., EIO due to NIC not supporting checksum offload),
|
|
||||||
// disable GSO and fall back to sendmmsg for the rest
|
|
||||||
if isGSOError(err) {
|
|
||||||
u.l.WithError(err).Warn("GSO send failed, disabling GSO for this connection")
|
|
||||||
u.gsoSupported = false
|
|
||||||
// Send remaining packets with sendmmsg
|
|
||||||
remaining, rerr := u.writeBatchSendmmsg(pkts[startIdx:])
|
|
||||||
return totalSent + remaining, rerr
|
|
||||||
}
|
|
||||||
return totalSent, err
|
|
||||||
}
|
|
||||||
totalSent += coalescedCount
|
|
||||||
i += coalescedCount
|
|
||||||
} else {
|
|
||||||
// Single packet, send without GSO overhead
|
|
||||||
err := u.WriteTo(pkts[i].Payload, pkts[i].Addr)
|
|
||||||
if err != nil {
|
|
||||||
return totalSent, err
|
|
||||||
}
|
|
||||||
totalSent++
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return totalSent, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sendGSO sends coalesced packets using UDP GSO
|
|
||||||
func (u *StdConn) sendGSO(pkts []BatchPacket, dst netip.AddrPort, segmentSize, totalSize int) error {
|
|
||||||
// Allocate a buffer large enough for all packet payloads
|
|
||||||
coalescedBuf := make([]byte, totalSize)
|
|
||||||
offset := 0
|
|
||||||
for _, pkt := range pkts {
|
|
||||||
copy(coalescedBuf[offset:], pkt.Payload)
|
|
||||||
offset += len(pkt.Payload)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare control message with GSO segment size
|
|
||||||
control := make([]byte, unix.CmsgSpace(2))
|
|
||||||
controlLen := setGSOSize(control, uint16(segmentSize))
|
|
||||||
|
|
||||||
// Prepare the iovec
|
|
||||||
iov := iovec{}
|
|
||||||
setIovecBase(&iov, &coalescedBuf[0])
|
|
||||||
setIovecLen(&iov, totalSize)
|
|
||||||
|
|
||||||
// Prepare the msghdr
|
|
||||||
var hdr msghdr
|
|
||||||
hdr.Iov = &iov
|
|
||||||
setMsghdrIovlen(&hdr, 1)
|
|
||||||
hdr.Control = &control[0]
|
|
||||||
setMsghdrControllen(&hdr, controlLen)
|
|
||||||
|
|
||||||
// Declare sockaddr at function scope so it remains valid for the syscall
|
|
||||||
// (must not go out of scope before the syscall is made)
|
|
||||||
var rsa4 unix.RawSockaddrInet4
|
|
||||||
var rsa6 unix.RawSockaddrInet6
|
|
||||||
|
|
||||||
// Set destination address
|
|
||||||
if u.isV4 {
|
|
||||||
rsa4.Family = unix.AF_INET
|
|
||||||
rsa4.Addr = dst.Addr().As4()
|
|
||||||
binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&rsa4.Port))[:], dst.Port())
|
|
||||||
hdr.Name = (*byte)(unsafe.Pointer(&rsa4))
|
|
||||||
hdr.Namelen = unix.SizeofSockaddrInet4
|
|
||||||
} else {
|
|
||||||
rsa6.Family = unix.AF_INET6
|
|
||||||
rsa6.Addr = dst.Addr().As16()
|
|
||||||
binary.BigEndian.PutUint16((*[2]byte)(unsafe.Pointer(&rsa6.Port))[:], dst.Port())
|
|
||||||
hdr.Name = (*byte)(unsafe.Pointer(&rsa6))
|
|
||||||
hdr.Namelen = unix.SizeofSockaddrInet6
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
_, _, errno := unix.Syscall6(
|
|
||||||
unix.SYS_SENDMSG,
|
|
||||||
uintptr(u.sysFd),
|
|
||||||
uintptr(unsafe.Pointer(&hdr)),
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
)
|
|
||||||
|
|
||||||
if errno == unix.EINTR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if errno != 0 {
|
|
||||||
return &net.OpError{Op: "sendmsg", Err: errno}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// isGSOError returns true if the error indicates GSO is not supported by the NIC
|
|
||||||
func isGSOError(err error) bool {
|
|
||||||
var opErr *net.OpError
|
|
||||||
if !errors.As(err, &opErr) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
// EIO typically means the NIC doesn't support checksum offload required for GSO
|
|
||||||
return errors.Is(opErr.Err, unix.EIO)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUDPStatsEmitter(udpConns []Conn) func() {
|
func NewUDPStatsEmitter(udpConns []Conn) func() {
|
||||||
// Check if our kernel supports SO_MEMINFO before registering the gauges
|
// Check if our kernel supports SO_MEMINFO before registering the gauges
|
||||||
|
|||||||
+3
-43
@@ -30,26 +30,13 @@ type rawMessage struct {
|
|||||||
Len uint32
|
Len uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte, [][]byte) {
|
func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte) {
|
||||||
msgs := make([]rawMessage, n)
|
msgs := make([]rawMessage, n)
|
||||||
buffers := make([][]byte, n)
|
buffers := make([][]byte, n)
|
||||||
names := make([][]byte, n)
|
names := make([][]byte, n)
|
||||||
controls := make([][]byte, n)
|
|
||||||
|
|
||||||
// Use larger buffers if GRO is enabled to hold coalesced packets
|
|
||||||
bufSize := MTU
|
|
||||||
if u.groSupported {
|
|
||||||
bufSize = groMaxPacketSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// Control buffer size for receiving UDP_GRO segment size
|
|
||||||
controlSize := 0
|
|
||||||
if u.groSupported {
|
|
||||||
controlSize = unix.CmsgSpace(2) // space for uint16 segment size
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range msgs {
|
for i := range msgs {
|
||||||
buffers[i] = make([]byte, bufSize)
|
buffers[i] = make([]byte, MTU)
|
||||||
names[i] = make([]byte, unix.SizeofSockaddrInet6)
|
names[i] = make([]byte, unix.SizeofSockaddrInet6)
|
||||||
|
|
||||||
vs := []iovec{
|
vs := []iovec{
|
||||||
@@ -61,34 +48,7 @@ func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte, [
|
|||||||
|
|
||||||
msgs[i].Hdr.Name = &names[i][0]
|
msgs[i].Hdr.Name = &names[i][0]
|
||||||
msgs[i].Hdr.Namelen = uint32(len(names[i]))
|
msgs[i].Hdr.Namelen = uint32(len(names[i]))
|
||||||
|
|
||||||
// Set up control message buffer for GRO
|
|
||||||
if controlSize > 0 {
|
|
||||||
controls[i] = make([]byte, controlSize)
|
|
||||||
msgs[i].Hdr.Control = &controls[i][0]
|
|
||||||
msgs[i].Hdr.Controllen = uint32(controlSize)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return msgs, buffers, names, controls
|
return msgs, buffers, names
|
||||||
}
|
|
||||||
|
|
||||||
func setIovecBase(iov *iovec, base *byte) {
|
|
||||||
iov.Base = base
|
|
||||||
}
|
|
||||||
|
|
||||||
func setIovecLen(iov *iovec, l int) {
|
|
||||||
iov.Len = uint32(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setMsghdrIovlen(hdr *msghdr, l int) {
|
|
||||||
hdr.Iovlen = uint32(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setMsghdrControllen(hdr *msghdr, l int) {
|
|
||||||
hdr.Controllen = uint32(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMsghdrControllen(hdr *msghdr) int {
|
|
||||||
return int(hdr.Controllen)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-43
@@ -33,26 +33,13 @@ type rawMessage struct {
|
|||||||
Pad0 [4]byte
|
Pad0 [4]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte, [][]byte) {
|
func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte) {
|
||||||
msgs := make([]rawMessage, n)
|
msgs := make([]rawMessage, n)
|
||||||
buffers := make([][]byte, n)
|
buffers := make([][]byte, n)
|
||||||
names := make([][]byte, n)
|
names := make([][]byte, n)
|
||||||
controls := make([][]byte, n)
|
|
||||||
|
|
||||||
// Use larger buffers if GRO is enabled to hold coalesced packets
|
|
||||||
bufSize := MTU
|
|
||||||
if u.groSupported {
|
|
||||||
bufSize = groMaxPacketSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// Control buffer size for receiving UDP_GRO segment size
|
|
||||||
controlSize := 0
|
|
||||||
if u.groSupported {
|
|
||||||
controlSize = unix.CmsgSpace(2) // space for uint16 segment size
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range msgs {
|
for i := range msgs {
|
||||||
buffers[i] = make([]byte, bufSize)
|
buffers[i] = make([]byte, MTU)
|
||||||
names[i] = make([]byte, unix.SizeofSockaddrInet6)
|
names[i] = make([]byte, unix.SizeofSockaddrInet6)
|
||||||
|
|
||||||
vs := []iovec{
|
vs := []iovec{
|
||||||
@@ -64,34 +51,7 @@ func (u *StdConn) PrepareRawMessages(n int) ([]rawMessage, [][]byte, [][]byte, [
|
|||||||
|
|
||||||
msgs[i].Hdr.Name = &names[i][0]
|
msgs[i].Hdr.Name = &names[i][0]
|
||||||
msgs[i].Hdr.Namelen = uint32(len(names[i]))
|
msgs[i].Hdr.Namelen = uint32(len(names[i]))
|
||||||
|
|
||||||
// Set up control message buffer for GRO
|
|
||||||
if controlSize > 0 {
|
|
||||||
controls[i] = make([]byte, controlSize)
|
|
||||||
msgs[i].Hdr.Control = &controls[i][0]
|
|
||||||
msgs[i].Hdr.Controllen = uint64(controlSize)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return msgs, buffers, names, controls
|
return msgs, buffers, names
|
||||||
}
|
|
||||||
|
|
||||||
func setIovecBase(iov *iovec, base *byte) {
|
|
||||||
iov.Base = base
|
|
||||||
}
|
|
||||||
|
|
||||||
func setIovecLen(iov *iovec, l int) {
|
|
||||||
iov.Len = uint64(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setMsghdrIovlen(hdr *msghdr, l int) {
|
|
||||||
hdr.Iovlen = uint64(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setMsghdrControllen(hdr *msghdr, l int) {
|
|
||||||
hdr.Controllen = uint64(l)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMsghdrControllen(hdr *msghdr) int {
|
|
||||||
return int(hdr.Controllen)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,29 +332,12 @@ func (u *RIOConn) SupportsMultipleReaders() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RIOConn) SupportsGSO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *RIOConn) SupportsGRO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *RIOConn) Rebind() error {
|
func (u *RIOConn) Rebind() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *RIOConn) ReloadConfig(*config.C) {}
|
func (u *RIOConn) ReloadConfig(*config.C) {}
|
||||||
|
|
||||||
func (u *RIOConn) WriteBatch(pkts []BatchPacket) (int, error) {
|
|
||||||
for i := range pkts {
|
|
||||||
if err := u.WriteTo(pkts[i].Payload, pkts[i].Addr); err != nil {
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(pkts), 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
|
||||||
|
|||||||
@@ -131,14 +131,6 @@ func (u *TesterConn) SupportsMultipleReaders() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TesterConn) SupportsGSO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *TesterConn) SupportsGRO() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *TesterConn) Rebind() error {
|
func (u *TesterConn) Rebind() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -150,12 +142,3 @@ func (u *TesterConn) Close() error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *TesterConn) WriteBatch(pkts []BatchPacket) (int, error) {
|
|
||||||
for i := range pkts {
|
|
||||||
if err := u.WriteTo(pkts[i].Payload, pkts[i].Addr); err != nil {
|
|
||||||
return i, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return len(pkts), nil
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user