mirror of
https://github.com/slackhq/nebula.git
synced 2026-08-15 21:06:58 +02:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7794e93762 | |||
| 3df60ae195 |
@@ -342,6 +342,14 @@ logging:
|
|||||||
# after receiving the response for lighthouse queries
|
# after receiving the response for lighthouse queries
|
||||||
#trigger_buffer: 64
|
#trigger_buffer: 64
|
||||||
|
|
||||||
|
# max_rate limits the number of new inbound handshakes per second. Once the limit is reached,
|
||||||
|
# new handshakes are dropped until the next second. A value of 0 means unlimited (default).
|
||||||
|
# This is useful for preventing DoS attacks that attempt to exhaust CPU with handshake crypto.
|
||||||
|
# Running `openssl speed ecdhp256` on your hardware can be a good rule of thumb for choosing
|
||||||
|
# a max, as each handshake performs similar DH operations. Note that this benchmarks a single
|
||||||
|
# core, so you may wish to scale the value by the number of `routines` configured.
|
||||||
|
#max_rate: 0
|
||||||
|
|
||||||
# Tunnel manager settings
|
# Tunnel manager settings
|
||||||
#tunnels:
|
#tunnels:
|
||||||
# drop_inactive controls whether inactive tunnels are maintained or dropped after the inactive_timeout period has
|
# drop_inactive controls whether inactive tunnels are maintained or dropped after the inactive_timeout period has
|
||||||
|
|||||||
+57
-8
@@ -23,22 +23,25 @@ const (
|
|||||||
DefaultHandshakeRetries = 10
|
DefaultHandshakeRetries = 10
|
||||||
DefaultHandshakeTriggerBuffer = 64
|
DefaultHandshakeTriggerBuffer = 64
|
||||||
DefaultUseRelays = true
|
DefaultUseRelays = true
|
||||||
|
DefaultMaxHandshakeRate = 0 // 0 means unlimited
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
defaultHandshakeConfig = HandshakeConfig{
|
defaultHandshakeConfig = HandshakeConfig{
|
||||||
tryInterval: DefaultHandshakeTryInterval,
|
tryInterval: DefaultHandshakeTryInterval,
|
||||||
retries: DefaultHandshakeRetries,
|
retries: DefaultHandshakeRetries,
|
||||||
triggerBuffer: DefaultHandshakeTriggerBuffer,
|
triggerBuffer: DefaultHandshakeTriggerBuffer,
|
||||||
useRelays: DefaultUseRelays,
|
useRelays: DefaultUseRelays,
|
||||||
|
maxHandshakeRate: DefaultMaxHandshakeRate,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type HandshakeConfig struct {
|
type HandshakeConfig struct {
|
||||||
tryInterval time.Duration
|
tryInterval time.Duration
|
||||||
retries int64
|
retries int64
|
||||||
triggerBuffer int
|
triggerBuffer int
|
||||||
useRelays bool
|
useRelays bool
|
||||||
|
maxHandshakeRate int
|
||||||
|
|
||||||
messageMetrics *MessageMetrics
|
messageMetrics *MessageMetrics
|
||||||
}
|
}
|
||||||
@@ -58,9 +61,15 @@ type HandshakeManager struct {
|
|||||||
messageMetrics *MessageMetrics
|
messageMetrics *MessageMetrics
|
||||||
metricInitiated metrics.Counter
|
metricInitiated metrics.Counter
|
||||||
metricTimedOut metrics.Counter
|
metricTimedOut metrics.Counter
|
||||||
|
metricRateLimited metrics.Counter
|
||||||
f *Interface
|
f *Interface
|
||||||
l *logrus.Logger
|
l *logrus.Logger
|
||||||
|
|
||||||
|
// Rate limiting for new handshakes (token bucket)
|
||||||
|
rateBucket int // tokens currently available
|
||||||
|
rateMax int // max tokens (== max handshakes per second), 0 means unlimited
|
||||||
|
rateLastTick time.Time
|
||||||
|
|
||||||
// can be used to trigger outbound handshake for the given vpnIp
|
// can be used to trigger outbound handshake for the given vpnIp
|
||||||
trigger chan netip.Addr
|
trigger chan netip.Addr
|
||||||
}
|
}
|
||||||
@@ -116,10 +125,41 @@ func NewHandshakeManager(l *logrus.Logger, mainHostMap *HostMap, lightHouse *Lig
|
|||||||
messageMetrics: config.messageMetrics,
|
messageMetrics: config.messageMetrics,
|
||||||
metricInitiated: metrics.GetOrRegisterCounter("handshake_manager.initiated", nil),
|
metricInitiated: metrics.GetOrRegisterCounter("handshake_manager.initiated", nil),
|
||||||
metricTimedOut: metrics.GetOrRegisterCounter("handshake_manager.timed_out", nil),
|
metricTimedOut: metrics.GetOrRegisterCounter("handshake_manager.timed_out", nil),
|
||||||
|
metricRateLimited: metrics.GetOrRegisterCounter("handshake_manager.rate_limited", nil),
|
||||||
|
rateBucket: config.maxHandshakeRate,
|
||||||
|
rateMax: config.maxHandshakeRate,
|
||||||
|
rateLastTick: time.Now(),
|
||||||
l: l,
|
l: l,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// handshakeRateAllow checks the token bucket rate limiter and returns true if a
|
||||||
|
// new handshake is allowed. Must be called with hm.Lock held.
|
||||||
|
func (hm *HandshakeManager) handshakeRateAllow(now time.Time) bool {
|
||||||
|
if hm.rateMax == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refill tokens based on elapsed time
|
||||||
|
elapsed := now.Sub(hm.rateLastTick)
|
||||||
|
if elapsed >= time.Second {
|
||||||
|
// Add tokens for full seconds elapsed
|
||||||
|
tokens := int(elapsed/time.Second) * hm.rateMax
|
||||||
|
hm.rateBucket += tokens
|
||||||
|
if hm.rateBucket > hm.rateMax {
|
||||||
|
hm.rateBucket = hm.rateMax
|
||||||
|
}
|
||||||
|
hm.rateLastTick = now
|
||||||
|
}
|
||||||
|
|
||||||
|
if hm.rateBucket > 0 {
|
||||||
|
hm.rateBucket--
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (hm *HandshakeManager) Run(ctx context.Context) {
|
func (hm *HandshakeManager) Run(ctx context.Context) {
|
||||||
clockSource := time.NewTicker(hm.config.tryInterval)
|
clockSource := time.NewTicker(hm.config.tryInterval)
|
||||||
defer clockSource.Stop()
|
defer clockSource.Stop()
|
||||||
@@ -149,6 +189,15 @@ func (hm *HandshakeManager) HandleIncoming(via ViaSender, packet []byte, h *head
|
|||||||
case header.HandshakeIXPSK0:
|
case header.HandshakeIXPSK0:
|
||||||
switch h.MessageCounter {
|
switch h.MessageCounter {
|
||||||
case 1:
|
case 1:
|
||||||
|
// Check rate limit for new incoming handshakes
|
||||||
|
hm.Lock()
|
||||||
|
allowed := hm.handshakeRateAllow(time.Now())
|
||||||
|
hm.Unlock()
|
||||||
|
if !allowed {
|
||||||
|
hm.metricRateLimited.Inc(1)
|
||||||
|
hm.l.WithField("from", via).Debug("Handshake rate limit reached, dropping incoming handshake")
|
||||||
|
return
|
||||||
|
}
|
||||||
ixHandshakeStage1(hm.f, via, packet, h)
|
ixHandshakeStage1(hm.f, via, packet, h)
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
|
|||||||
@@ -65,6 +65,68 @@ func Test_NewHandshakeManagerVpnIp(t *testing.T) {
|
|||||||
assert.NotContains(t, blah.vpnIps, ip)
|
assert.NotContains(t, blah.vpnIps, ip)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_HandshakeManagerRateLimit(t *testing.T) {
|
||||||
|
l := test.NewLogger()
|
||||||
|
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
||||||
|
preferredRanges := []netip.Prefix{localrange}
|
||||||
|
mainHM := newHostMap(l)
|
||||||
|
mainHM.preferredRanges.Store(&preferredRanges)
|
||||||
|
|
||||||
|
lh := newTestLighthouse()
|
||||||
|
|
||||||
|
config := defaultHandshakeConfig
|
||||||
|
config.maxHandshakeRate = 2
|
||||||
|
|
||||||
|
hm := NewHandshakeManager(l, mainHM, lh, &udp.NoopConn{}, config)
|
||||||
|
hm.f = &Interface{handshakeManager: hm, pki: &PKI{}, l: l}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
// Should allow up to maxHandshakeRate handshakes
|
||||||
|
hm.Lock()
|
||||||
|
assert.True(t, hm.handshakeRateAllow(now), "first handshake should be allowed")
|
||||||
|
assert.True(t, hm.handshakeRateAllow(now), "second handshake should be allowed")
|
||||||
|
assert.False(t, hm.handshakeRateAllow(now), "third handshake should be rate limited")
|
||||||
|
hm.Unlock()
|
||||||
|
|
||||||
|
// After advancing time by 1 second, tokens should refill
|
||||||
|
hm.Lock()
|
||||||
|
assert.True(t, hm.handshakeRateAllow(now.Add(time.Second)), "handshake should be allowed after token refill")
|
||||||
|
hm.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_HandshakeManagerRateLimitUnlimited(t *testing.T) {
|
||||||
|
l := test.NewLogger()
|
||||||
|
localrange := netip.MustParsePrefix("10.1.1.1/24")
|
||||||
|
preferredRanges := []netip.Prefix{localrange}
|
||||||
|
mainHM := newHostMap(l)
|
||||||
|
mainHM.preferredRanges.Store(&preferredRanges)
|
||||||
|
|
||||||
|
lh := newTestLighthouse()
|
||||||
|
|
||||||
|
cs := &CertState{
|
||||||
|
initiatingVersion: cert.Version1,
|
||||||
|
privateKey: []byte{},
|
||||||
|
v1Cert: &dummyCert{version: cert.Version1},
|
||||||
|
v1HandshakeBytes: []byte{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default config has maxHandshakeRate=0 (unlimited)
|
||||||
|
hm := NewHandshakeManager(l, mainHM, lh, &udp.NoopConn{}, defaultHandshakeConfig)
|
||||||
|
hm.f = &Interface{handshakeManager: hm, pki: &PKI{}, l: l}
|
||||||
|
hm.f.pki.cs.Store(cs)
|
||||||
|
|
||||||
|
// Should allow many handshakes with no limit
|
||||||
|
// Limited to 10 due to test lighthouse query channel buffer
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
ip := netip.MustParseAddr("172.1.1.1").As16()
|
||||||
|
ip[15] = byte(i + 1)
|
||||||
|
addr := netip.AddrFrom16(ip)
|
||||||
|
h := hm.StartHandshake(addr, nil)
|
||||||
|
assert.NotNil(t, h, "handshake %d should be allowed with unlimited rate", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testCountTimerWheelEntries(tw *LockingTimerWheel[netip.Addr]) (c int) {
|
func testCountTimerWheelEntries(tw *LockingTimerWheel[netip.Addr]) (c int) {
|
||||||
for _, i := range tw.t.wheel {
|
for _, i := range tw.t.wheel {
|
||||||
n := i.Head
|
n := i.Head
|
||||||
|
|||||||
@@ -204,10 +204,11 @@ func Main(c *config.C, configTest bool, buildVersion string, logger *logrus.Logg
|
|||||||
useRelays := c.GetBool("relay.use_relays", DefaultUseRelays) && !c.GetBool("relay.am_relay", false)
|
useRelays := c.GetBool("relay.use_relays", DefaultUseRelays) && !c.GetBool("relay.am_relay", false)
|
||||||
|
|
||||||
handshakeConfig := HandshakeConfig{
|
handshakeConfig := HandshakeConfig{
|
||||||
tryInterval: c.GetDuration("handshakes.try_interval", DefaultHandshakeTryInterval),
|
tryInterval: c.GetDuration("handshakes.try_interval", DefaultHandshakeTryInterval),
|
||||||
retries: int64(c.GetInt("handshakes.retries", DefaultHandshakeRetries)),
|
retries: int64(c.GetInt("handshakes.retries", DefaultHandshakeRetries)),
|
||||||
triggerBuffer: c.GetInt("handshakes.trigger_buffer", DefaultHandshakeTriggerBuffer),
|
triggerBuffer: c.GetInt("handshakes.trigger_buffer", DefaultHandshakeTriggerBuffer),
|
||||||
useRelays: useRelays,
|
useRelays: useRelays,
|
||||||
|
maxHandshakeRate: c.GetInt("handshakes.max_rate", DefaultMaxHandshakeRate),
|
||||||
|
|
||||||
messageMetrics: messageMetrics,
|
messageMetrics: messageMetrics,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -632,6 +632,9 @@ func sshCreateTunnel(ifce *Interface, fs any, a []string, w sshd.StringWriter) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
hostInfo = ifce.handshakeManager.StartHandshake(vpnAddr, nil)
|
hostInfo = ifce.handshakeManager.StartHandshake(vpnAddr, nil)
|
||||||
|
if hostInfo == nil {
|
||||||
|
return w.WriteLine("Handshake rate limit reached")
|
||||||
|
}
|
||||||
if addr.IsValid() {
|
if addr.IsValid() {
|
||||||
hostInfo.SetRemote(addr)
|
hostInfo.SetRemote(addr)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user