mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-20 13:49:07 +02:00
Merge remote-tracking branch 'origin/main' into revert/component-types
Signed-off-by: Dmitri Dolguikh <dmitri.external@netbird.io>
This commit is contained in:
@@ -844,6 +844,10 @@ func collectSysctls() string {
|
||||
[]string{"net.ipv4.conf.all.src_valid_mark", "net.ipv4.conf.default.src_valid_mark"},
|
||||
listInterfaceSysctls("ipv4", "src_valid_mark")...,
|
||||
))
|
||||
writeSysctlGroup(&builder, "accept_ra", append(
|
||||
[]string{"net.ipv6.conf.all.accept_ra", "net.ipv6.conf.default.accept_ra"},
|
||||
listInterfaceSysctls("ipv6", "accept_ra")...,
|
||||
))
|
||||
writeSysctlGroup(&builder, "conntrack", []string{
|
||||
"net.netfilter.nf_conntrack_acct",
|
||||
"net.netfilter.nf_conntrack_tcp_loose",
|
||||
|
||||
@@ -267,18 +267,38 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
||||
return SystemDNSSettings{}, fmt.Errorf("sending the command: %w", err)
|
||||
}
|
||||
|
||||
var dnsSettings SystemDNSSettings
|
||||
dnsSettings, serverAddresses, err := parseSystemDNSSettings(b)
|
||||
if err != nil {
|
||||
return dnsSettings, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.origNameservers = serverAddresses
|
||||
s.mu.Unlock()
|
||||
|
||||
return dnsSettings, nil
|
||||
}
|
||||
|
||||
// parseSystemDNSSettings parses the output of `scutil show State:/Network/Service/<id>/DNS`.
|
||||
// Lines that don't match the expected "index : value" shape are skipped: hosts with unusual
|
||||
// network services (e.g. orphaned hardware ports) can produce entries without a value.
|
||||
func parseSystemDNSSettings(out []byte) (SystemDNSSettings, []netip.Addr, error) {
|
||||
// port is not exposed by scutil, default to 53
|
||||
dnsSettings := SystemDNSSettings{ServerPort: DefaultPort}
|
||||
var serverAddresses []netip.Addr
|
||||
inSearchDomainsArray := false
|
||||
inServerAddressesArray := false
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||
scanner := bufio.NewScanner(bytes.NewReader(out))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
switch {
|
||||
case strings.HasPrefix(line, "DomainName :"):
|
||||
domainName := strings.TrimSpace(strings.Split(line, ":")[1])
|
||||
dnsSettings.Domains = append(dnsSettings.Domains, domainName)
|
||||
domainName := strings.TrimSpace(strings.TrimPrefix(line, "DomainName :"))
|
||||
if domainName != "" {
|
||||
dnsSettings.Domains = append(dnsSettings.Domains, domainName)
|
||||
}
|
||||
continue
|
||||
case line == "SearchDomains : <array> {":
|
||||
inSearchDomainsArray = true
|
||||
continue
|
||||
@@ -288,36 +308,45 @@ func (s *systemConfigurator) getSystemDNSSettings() (SystemDNSSettings, error) {
|
||||
case line == "}":
|
||||
inSearchDomainsArray = false
|
||||
inServerAddressesArray = false
|
||||
continue
|
||||
}
|
||||
|
||||
if !inSearchDomainsArray && !inServerAddressesArray {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, " : ", 2)
|
||||
if len(parts) != 2 {
|
||||
log.Debugf("skipping unexpected scutil DNS line %q", line)
|
||||
continue
|
||||
}
|
||||
value := strings.TrimSpace(parts[1])
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
if inSearchDomainsArray {
|
||||
searchDomain := strings.Split(line, " : ")[1]
|
||||
dnsSettings.Domains = append(dnsSettings.Domains, searchDomain)
|
||||
} else if inServerAddressesArray {
|
||||
address := strings.Split(line, " : ")[1]
|
||||
if ip, err := netip.ParseAddr(address); err == nil && !ip.IsUnspecified() {
|
||||
ip = ip.Unmap()
|
||||
serverAddresses = append(serverAddresses, ip)
|
||||
// Prefer the first IPv4 server as ServerIP since our DNS listener is IPv4.
|
||||
if !dnsSettings.ServerIP.IsValid() && ip.Is4() {
|
||||
dnsSettings.ServerIP = ip
|
||||
}
|
||||
}
|
||||
dnsSettings.Domains = append(dnsSettings.Domains, value)
|
||||
continue
|
||||
}
|
||||
|
||||
ip, err := netip.ParseAddr(value)
|
||||
if err != nil || ip.IsUnspecified() {
|
||||
continue
|
||||
}
|
||||
ip = ip.Unmap()
|
||||
serverAddresses = append(serverAddresses, ip)
|
||||
// Prefer the first IPv4 server as ServerIP since our DNS listener is IPv4.
|
||||
if !dnsSettings.ServerIP.IsValid() && ip.Is4() {
|
||||
dnsSettings.ServerIP = ip
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return dnsSettings, err
|
||||
return dnsSettings, serverAddresses, err
|
||||
}
|
||||
|
||||
// default to 53 port
|
||||
dnsSettings.ServerPort = DefaultPort
|
||||
|
||||
s.mu.Lock()
|
||||
s.origNameservers = serverAddresses
|
||||
s.mu.Unlock()
|
||||
|
||||
return dnsSettings, nil
|
||||
return dnsSettings, serverAddresses, nil
|
||||
}
|
||||
|
||||
func (s *systemConfigurator) getOriginalNameservers() []netip.Addr {
|
||||
@@ -435,11 +464,15 @@ func (s *systemConfigurator) getPrimaryService() (string, string, error) {
|
||||
router := ""
|
||||
for scanner.Scan() {
|
||||
text := scanner.Text()
|
||||
parts := strings.SplitN(text, ":", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(text, "PrimaryService") {
|
||||
primaryService = strings.TrimSpace(strings.Split(text, ":")[1])
|
||||
primaryService = strings.TrimSpace(parts[1])
|
||||
}
|
||||
if strings.Contains(text, "Router") {
|
||||
router = strings.TrimSpace(strings.Split(text, ":")[1])
|
||||
router = strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil && err != io.EOF {
|
||||
|
||||
@@ -328,6 +328,120 @@ func removeTestDNSKey(key string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func TestParseSystemDNSSettings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
output string
|
||||
expectedDomains []string
|
||||
expectedServers []netip.Addr
|
||||
expectedIP netip.Addr
|
||||
}{
|
||||
{
|
||||
name: "well_formed",
|
||||
output: `<dictionary> {
|
||||
DomainName : example.com
|
||||
SearchDomains : <array> {
|
||||
0 : example.com
|
||||
1 : corp.example.com
|
||||
}
|
||||
ServerAddresses : <array> {
|
||||
0 : 192.168.1.1
|
||||
1 : fd00::53
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectedDomains: []string{"example.com", "example.com", "corp.example.com"},
|
||||
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1"), netip.MustParseAddr("fd00::53")},
|
||||
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||
},
|
||||
{
|
||||
// entries without a value after the separator used to panic with
|
||||
// "index out of range [1] with length 1"
|
||||
name: "malformed_array_entries_skipped",
|
||||
output: `<dictionary> {
|
||||
SearchDomains : <array> {
|
||||
0 :
|
||||
(null)
|
||||
|
||||
1 : corp.example.com
|
||||
}
|
||||
ServerAddresses : <array> {
|
||||
0 :
|
||||
1 : 192.168.1.1
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectedDomains: []string{"corp.example.com"},
|
||||
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1")},
|
||||
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||
},
|
||||
{
|
||||
name: "domain_name_without_value_skipped",
|
||||
output: `<dictionary> {
|
||||
DomainName :
|
||||
ServerAddresses : <array> {
|
||||
0 : 192.168.1.1
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1")},
|
||||
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||
},
|
||||
{
|
||||
name: "ipv6_first_prefers_ipv4_server_ip",
|
||||
output: `<dictionary> {
|
||||
ServerAddresses : <array> {
|
||||
0 : fd00::53
|
||||
1 : 192.168.1.1
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectedServers: []netip.Addr{netip.MustParseAddr("fd00::53"), netip.MustParseAddr("192.168.1.1")},
|
||||
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||
},
|
||||
{
|
||||
name: "invalid_and_unspecified_addresses_skipped",
|
||||
output: `<dictionary> {
|
||||
ServerAddresses : <array> {
|
||||
0 : (null)
|
||||
1 : 0.0.0.0
|
||||
2 : 192.168.1.1
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1")},
|
||||
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||
},
|
||||
{
|
||||
name: "v4_mapped_address_unmapped",
|
||||
output: `<dictionary> {
|
||||
ServerAddresses : <array> {
|
||||
0 : ::ffff:192.168.1.1
|
||||
}
|
||||
}
|
||||
`,
|
||||
expectedServers: []netip.Addr{netip.MustParseAddr("192.168.1.1")},
|
||||
expectedIP: netip.MustParseAddr("192.168.1.1"),
|
||||
},
|
||||
{
|
||||
name: "empty_output",
|
||||
output: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
settings, servers, err := parseSystemDNSSettings([]byte(tc.output))
|
||||
require.NoError(t, err, "parsing should not fail")
|
||||
|
||||
assert.Equal(t, tc.expectedDomains, settings.Domains, "domains should match")
|
||||
assert.Equal(t, tc.expectedServers, servers, "server addresses should match")
|
||||
assert.Equal(t, tc.expectedIP, settings.ServerIP, "server IP should match")
|
||||
assert.Equal(t, DefaultPort, settings.ServerPort, "server port should default to 53")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOriginalNameservers(t *testing.T) {
|
||||
configurator := &systemConfigurator{
|
||||
createdKeys: make(map[string]struct{}),
|
||||
|
||||
@@ -2,54 +2,183 @@ package ipfwdstate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager/systemops"
|
||||
)
|
||||
|
||||
// IPForwardingState is a struct that keeps track of the IP forwarding state.
|
||||
// todo: read initial state of the IP forwarding from the system and reset the state based on it.
|
||||
// todo: separate v4/v6 forwarding state, since the sysctls are independent
|
||||
// (net.ipv4.ip_forward vs net.ipv6.conf.all.forwarding). Currently the nftables
|
||||
// manager shares one instance between both routers, which works only because
|
||||
// EnableIPForwarding enables both sysctls in a single call.
|
||||
// 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 {
|
||||
enabledCounter int
|
||||
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
|
||||
}
|
||||
|
||||
func NewIPForwardingState() *IPForwardingState {
|
||||
return &IPForwardingState{}
|
||||
// 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}
|
||||
}
|
||||
|
||||
func (f *IPForwardingState) RequestForwarding() error {
|
||||
if f.enabledCounter != 0 {
|
||||
f.enabledCounter++
|
||||
// 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 := systemops.EnableIPForwarding(); err != nil {
|
||||
return fmt.Errorf("failed to enable IP forwarding with sysctl: %w", err)
|
||||
if err := f.requestV6(); err != nil {
|
||||
log.Warnf("enable IPv6 forwarding for routing: %v", err)
|
||||
return nil
|
||||
}
|
||||
f.enabledCounter = 1
|
||||
log.Info("IP forwarding enabled")
|
||||
|
||||
f.routingV6 = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *IPForwardingState) ReleaseForwarding() error {
|
||||
if f.enabledCounter == 0 {
|
||||
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.enabledCounter > 1 {
|
||||
f.enabledCounter--
|
||||
return nil
|
||||
if f.routingV6 {
|
||||
f.routingV6 = false
|
||||
return f.releaseV6()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// if failed to disable IP forwarding we anyway decrement the counter
|
||||
f.enabledCounter = 0
|
||||
// RequestForwarding enables the family's forwarding sysctl on first request.
|
||||
func (f *IPForwardingState) RequestForwarding(v6 bool) error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
// todo call systemops.DisableIPForwarding()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
//go:build privileged
|
||||
|
||||
package ipfwdstate
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestRequestRoutingV6ToV4Transition verifies that a v4-only routing request
|
||||
// releases a previously held routing-owned v6 reference without touching
|
||||
// references held by DNAT rules.
|
||||
func TestRequestRoutingV6ToV4Transition(t *testing.T) {
|
||||
f := NewIPForwardingState("wt-fwd-test")
|
||||
|
||||
require.NoError(t, f.RequestRouting(true), "request routing with v6")
|
||||
v4, v6 := f.Counts()
|
||||
assert.Equal(t, 1, v4, "v4 reference held")
|
||||
assert.Equal(t, 1, v6, "v6 reference held")
|
||||
|
||||
require.NoError(t, f.RequestRouting(false), "request routing v4-only")
|
||||
v4, v6 = f.Counts()
|
||||
assert.Equal(t, 1, v4, "v4 reference kept")
|
||||
assert.Equal(t, 0, v6, "routing-owned v6 reference released")
|
||||
|
||||
// A DNAT-held reference survives a v4-only routing request.
|
||||
require.NoError(t, f.RequestForwarding(true), "dnat v6 reference")
|
||||
require.NoError(t, f.RequestRouting(false), "repeat v4-only request")
|
||||
_, v6 = f.Counts()
|
||||
assert.Equal(t, 1, v6, "dnat-held v6 reference survives")
|
||||
require.NoError(t, f.ReleaseForwarding(true), "release dnat v6 reference")
|
||||
|
||||
require.NoError(t, f.ReleaseRouting(), "release routing")
|
||||
v4, v6 = f.Counts()
|
||||
assert.Equal(t, 0, v4, "all v4 references released")
|
||||
assert.Equal(t, 0, v6, "all v6 references released")
|
||||
}
|
||||
@@ -58,11 +58,7 @@ func Setup(wgIface iface) (map[string]int, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Escape '%' and '.' so they survive the dot-to-slash conversion in Set()
|
||||
safeName := strings.ReplaceAll(intf.Name, "%", percentEscape)
|
||||
safeName = strings.ReplaceAll(safeName, ".", dotEscape)
|
||||
|
||||
i := fmt.Sprintf(rpFilterInterfacePath, safeName)
|
||||
i := fmt.Sprintf(rpFilterInterfacePath, EscapeInterfaceName(intf.Name))
|
||||
oldVal, err := Set(i, 2, true)
|
||||
if err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
@@ -74,6 +70,13 @@ func Setup(wgIface iface) (map[string]int, error) {
|
||||
return keys, nberrors.FormatErrorOrNil(result)
|
||||
}
|
||||
|
||||
// EscapeInterfaceName escapes '%' and '.' in an interface name (e.g. VLANs
|
||||
// like eth0.100) so the name survives the dot-to-slash conversion in Set.
|
||||
func EscapeInterfaceName(name string) string {
|
||||
safe := strings.ReplaceAll(name, "%", percentEscape)
|
||||
return strings.ReplaceAll(safe, ".", dotEscape)
|
||||
}
|
||||
|
||||
// Set sets a sysctl configuration, if onlyIfOne is true it will only set the new value if it's set to 1
|
||||
func Set(key string, desiredValue int, onlyIfOne bool) (int, error) {
|
||||
path := strings.ReplaceAll(key, ".", "/")
|
||||
|
||||
@@ -32,8 +32,17 @@ func (r *SysOps) removeFromRouteTable(netip.Prefix, Nexthop) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnableIPForwarding() error {
|
||||
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
|
||||
func EnableV4IPForwarding() error {
|
||||
log.Infof("Enable IPv4 forwarding is not implemented on %s", runtime.GOOS)
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnableV6IPForwarding(string) (map[string]int, error) {
|
||||
log.Infof("Enable IPv6 forwarding is not implemented on %s", runtime.GOOS)
|
||||
return map[string]int{}, nil
|
||||
}
|
||||
|
||||
func DisableV6IPForwarding(map[string]int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -58,8 +58,17 @@ func (r *SysOps) removeFromRouteTable(netip.Prefix, Nexthop) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnableIPForwarding() error {
|
||||
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
|
||||
func EnableV4IPForwarding() error {
|
||||
log.Infof("Enable IPv4 forwarding is not implemented on %s", runtime.GOOS)
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnableV6IPForwarding(string) (map[string]int, error) {
|
||||
log.Infof("Enable IPv6 forwarding is not implemented on %s", runtime.GOOS)
|
||||
return map[string]int{}, nil
|
||||
}
|
||||
|
||||
func DisableV6IPForwarding(map[string]int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -763,13 +763,10 @@ func flushRoutes(tableID, family int) error {
|
||||
return nberrors.FormatErrorOrNil(result)
|
||||
}
|
||||
|
||||
func EnableIPForwarding() error {
|
||||
func EnableV4IPForwarding() error {
|
||||
if _, err := sysctl.Set(ipv4ForwardingPath, 1, false); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := sysctl.Set(ipv6ForwardingPath, 1, false); err != nil {
|
||||
log.Warnf("failed to enable IPv6 forwarding: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -43,8 +43,17 @@ func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error
|
||||
return r.genericRemoveVPNRoute(prefix, intf)
|
||||
}
|
||||
|
||||
func EnableIPForwarding() error {
|
||||
log.Infof("Enable IP forwarding is not implemented on %s", runtime.GOOS)
|
||||
func EnableV4IPForwarding() error {
|
||||
log.Infof("Enable IPv4 forwarding is not implemented on %s", runtime.GOOS)
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnableV6IPForwarding(string) (map[string]int, error) {
|
||||
log.Infof("Enable IPv6 forwarding is not implemented on %s", runtime.GOOS)
|
||||
return map[string]int{}, nil
|
||||
}
|
||||
|
||||
func DisableV6IPForwarding(map[string]int) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
//go:build !android
|
||||
|
||||
package systemops
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
nberrors "github.com/netbirdio/netbird/client/errors"
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager/sysctl"
|
||||
)
|
||||
|
||||
const (
|
||||
// 1 (default) accepts RAs only while forwarding is off; 2 keeps RA
|
||||
// acceptance on regardless, so RA-installed host defaults survive our
|
||||
// v6 forwarding flip.
|
||||
acceptRAInterfacePath = "net.ipv6.conf.%s.accept_ra"
|
||||
acceptRADefaultPath = "net.ipv6.conf.default.accept_ra"
|
||||
acceptRAProcPathFormat = "/proc/sys/net/ipv6/conf/%s/accept_ra"
|
||||
)
|
||||
|
||||
// EnableV6IPForwarding bumps accept_ra=2 on host v6 interfaces before flipping
|
||||
// forwarding=1, so RA-installed host defaults survive. Returns the prior values
|
||||
// of sysctls we actually changed; entries already at the target are omitted.
|
||||
func EnableV6IPForwarding(wgIfaceName string) (map[string]int, error) {
|
||||
saved := map[string]int{}
|
||||
bumpAcceptRA(saved, wgIfaceName)
|
||||
|
||||
oldVal, err := sysctl.Set(ipv6ForwardingPath, 1, false)
|
||||
if err != nil {
|
||||
return saved, err
|
||||
}
|
||||
if oldVal != 1 {
|
||||
saved[ipv6ForwardingPath] = oldVal
|
||||
}
|
||||
return saved, nil
|
||||
}
|
||||
|
||||
// DisableV6IPForwarding restores what EnableV6IPForwarding captured.
|
||||
func DisableV6IPForwarding(saved map[string]int) error {
|
||||
var result *multierror.Error
|
||||
for key, value := range saved {
|
||||
if _, err := sysctl.Set(key, value, false); err != nil {
|
||||
result = multierror.Append(result, fmt.Errorf("restore %s: %w", key, err))
|
||||
}
|
||||
}
|
||||
return nberrors.FormatErrorOrNil(result)
|
||||
}
|
||||
|
||||
func bumpAcceptRA(saved map[string]int, wgIfaceName string) {
|
||||
// Also bump conf.default so interfaces created while forwarding is on
|
||||
// (hotplug, new Wi-Fi/dock) inherit accept_ra=2 and keep accepting RAs.
|
||||
bumpAcceptRAKey(saved, acceptRADefaultPath)
|
||||
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
log.Warnf("list interfaces for accept_ra: %v", err)
|
||||
return
|
||||
}
|
||||
for _, intf := range interfaces {
|
||||
if intf.Name == "lo" || intf.Name == wgIfaceName {
|
||||
continue
|
||||
}
|
||||
bumpAcceptRAForInterface(saved, intf.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func bumpAcceptRAForInterface(saved map[string]int, name string) {
|
||||
// Build procfs path from name, not the dotted key: VLAN names like eth0.100.
|
||||
if _, err := os.Stat(fmt.Sprintf(acceptRAProcPathFormat, name)); err != nil {
|
||||
return
|
||||
}
|
||||
bumpAcceptRAKey(saved, fmt.Sprintf(acceptRAInterfacePath, sysctl.EscapeInterfaceName(name)))
|
||||
}
|
||||
|
||||
func bumpAcceptRAKey(saved map[string]int, key string) {
|
||||
// onlyIfOne=true: leave admin overrides (0, 2) alone.
|
||||
oldVal, err := sysctl.Set(key, 2, true)
|
||||
if err != nil {
|
||||
log.Warnf("bump %s: %v", key, err)
|
||||
return
|
||||
}
|
||||
// With onlyIfOne, a write only happened when the old value was 1; values
|
||||
// left untouched (0, 2) must not be recorded for restore.
|
||||
if oldVal == 1 {
|
||||
saved[key] = oldVal
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user