mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-12 17:59:06 +02:00
185 lines
4.6 KiB
Go
185 lines
4.6 KiB
Go
package ipfwdstate
|
|
|
|
import (
|
|
"fmt"
|
|
"sync"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
|
)
|
|
|
|
// IPForwardingState tracks v4 and v6 IP-forwarding sysctl enables with
|
|
// independent refcounts so a v4-only routing setup doesn't flip v6 sysctls.
|
|
type IPForwardingState struct {
|
|
mu sync.Mutex
|
|
|
|
v4Count int
|
|
v6Count int
|
|
|
|
// routingV4/routingV6 track whether the routing path currently holds a
|
|
// reference, so repeated EnableRouting calls (one per network-map update)
|
|
// hold at most one reference per family and an unpaired DisableRouting
|
|
// can't release references held by DNAT rules.
|
|
routingV4 bool
|
|
routingV6 bool
|
|
|
|
wgIfaceName string
|
|
v6Saved map[string]int
|
|
}
|
|
|
|
// NewIPForwardingState returns a state tracker for the IP-forwarding sysctls.
|
|
// wgIfaceName is excluded from the per-interface accept_ra handling.
|
|
func NewIPForwardingState(wgIfaceName string) *IPForwardingState {
|
|
return &IPForwardingState{wgIfaceName: wgIfaceName}
|
|
}
|
|
|
|
// Counts returns the current v4 and v6 refcounts. Intended for diagnostics
|
|
// and tests.
|
|
func (f *IPForwardingState) Counts() (v4, v6 int) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.v4Count, f.v6Count
|
|
}
|
|
|
|
// RequestRouting takes the forwarding references for the routing path. It is
|
|
// idempotent: while routing already holds a reference, further calls don't
|
|
// increment the refcounts, and a v4-only request releases a previously held v6
|
|
// reference. A v6 sysctl failure is logged and not returned so it can't take
|
|
// down v4 routing (the sysctl may be unwritable, e.g. read-only /proc/sys or
|
|
// IPv6 disabled on the kernel command line); v6 is retried on the next call.
|
|
func (f *IPForwardingState) RequestRouting(v6 bool) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
if !f.routingV4 {
|
|
if err := f.requestV4(); err != nil {
|
|
return err
|
|
}
|
|
f.routingV4 = true
|
|
}
|
|
|
|
if !v6 {
|
|
if !f.routingV6 {
|
|
return nil
|
|
}
|
|
f.routingV6 = false
|
|
return f.releaseV6()
|
|
}
|
|
|
|
if f.routingV6 {
|
|
return nil
|
|
}
|
|
if err := f.requestV6(); err != nil {
|
|
log.Warnf("enable IPv6 forwarding for routing: %v", err)
|
|
return nil
|
|
}
|
|
f.routingV6 = true
|
|
return nil
|
|
}
|
|
|
|
// ReleaseRouting releases the references RequestRouting holds. Calls without a
|
|
// held reference are no-ops.
|
|
func (f *IPForwardingState) ReleaseRouting() error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
if f.routingV4 {
|
|
f.routingV4 = false
|
|
f.releaseV4()
|
|
}
|
|
if f.routingV6 {
|
|
f.routingV6 = false
|
|
return f.releaseV6()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RequestForwarding enables the family's forwarding sysctl on first request.
|
|
func (f *IPForwardingState) RequestForwarding(v6 bool) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
if v6 {
|
|
return f.requestV6()
|
|
}
|
|
return f.requestV4()
|
|
}
|
|
|
|
// ReleaseForwarding decrements the family counter. The last v6 release restores
|
|
// what enable captured. v4 stays on: net.ipv4.ip_forward is co-owned by other
|
|
// tooling (docker, k8s, libvirt).
|
|
func (f *IPForwardingState) ReleaseForwarding(v6 bool) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
|
|
if v6 {
|
|
return f.releaseV6()
|
|
}
|
|
f.releaseV4()
|
|
return nil
|
|
}
|
|
|
|
func (f *IPForwardingState) requestV4() error {
|
|
if f.v4Count == 0 {
|
|
if err := systemops.EnableV4IPForwarding(); err != nil {
|
|
return fmt.Errorf("enable IPv4 forwarding: %w", err)
|
|
}
|
|
log.Info("IPv4 forwarding enabled")
|
|
}
|
|
f.v4Count++
|
|
return nil
|
|
}
|
|
|
|
func (f *IPForwardingState) releaseV4() {
|
|
if f.v4Count > 0 {
|
|
f.v4Count--
|
|
}
|
|
}
|
|
|
|
func (f *IPForwardingState) requestV6() error {
|
|
if f.v6Count == 0 {
|
|
saved, err := systemops.EnableV6IPForwarding(f.wgIfaceName)
|
|
if err != nil {
|
|
if rerr := systemops.DisableV6IPForwarding(saved); rerr != nil {
|
|
log.Warnf("rollback partial v6 sysctls: %v", rerr)
|
|
}
|
|
return fmt.Errorf("enable IPv6 forwarding: %w", err)
|
|
}
|
|
// A failed restore on a previous release keeps its saved values; those
|
|
// are the true originals, so keep them over what this enable captured.
|
|
if f.v6Saved == nil {
|
|
f.v6Saved = saved
|
|
} else {
|
|
for k, v := range saved {
|
|
if _, ok := f.v6Saved[k]; !ok {
|
|
f.v6Saved[k] = v
|
|
}
|
|
}
|
|
}
|
|
log.Info("IPv6 forwarding enabled")
|
|
}
|
|
f.v6Count++
|
|
return nil
|
|
}
|
|
|
|
func (f *IPForwardingState) releaseV6() error {
|
|
if f.v6Count == 0 {
|
|
return nil
|
|
}
|
|
f.v6Count--
|
|
if f.v6Count > 0 {
|
|
return nil
|
|
}
|
|
|
|
// Keep the saved values on failure so a later release or enable/release
|
|
// cycle can still restore them; re-restoring an already-restored key is a
|
|
// no-op since the sysctl already holds the desired value.
|
|
if err := systemops.DisableV6IPForwarding(f.v6Saved); err != nil {
|
|
return fmt.Errorf("disable IPv6 forwarding: %w", err)
|
|
}
|
|
f.v6Saved = nil
|
|
log.Info("IPv6 forwarding disabled")
|
|
return nil
|
|
}
|