mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-18 08:16:39 +00:00
[client] Add IPv6 support to ACL manager, USP filter, and forwarder (#5688)
This commit is contained in:
@@ -5,7 +5,6 @@ import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -19,6 +18,7 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/acl/id"
|
||||
"github.com/netbirdio/netbird/shared/management/domain"
|
||||
mgmProto "github.com/netbirdio/netbird/shared/management/proto"
|
||||
"github.com/netbirdio/netbird/shared/netiputil"
|
||||
)
|
||||
|
||||
var ErrSourceRangesEmpty = errors.New("sources range is empty")
|
||||
@@ -105,6 +105,10 @@ func (d *DefaultManager) applyPeerACLs(networkMap *mgmProto.NetworkMap) {
|
||||
newRulePairs := make(map[id.RuleID][]firewall.Rule)
|
||||
ipsetByRuleSelectors := make(map[string]string)
|
||||
|
||||
// TODO: deny rules should be fatal: if a deny rule fails to apply, we must
|
||||
// roll back all allow rules to avoid a fail-open where allowed traffic bypasses
|
||||
// the missing deny. Currently we accumulate errors and continue.
|
||||
var merr *multierror.Error
|
||||
for _, r := range rules {
|
||||
// if this rule is member of rule selection with more than DefaultIPsCountForSet
|
||||
// it's IP address can be used in the ipset for firewall manager which supports it
|
||||
@@ -117,9 +121,8 @@ func (d *DefaultManager) applyPeerACLs(networkMap *mgmProto.NetworkMap) {
|
||||
}
|
||||
pairID, rulePair, err := d.protoRuleToFirewallRule(r, ipsetName)
|
||||
if err != nil {
|
||||
log.Errorf("failed to apply firewall rule: %+v, %v", r, err)
|
||||
d.rollBack(newRulePairs)
|
||||
break
|
||||
merr = multierror.Append(merr, fmt.Errorf("apply firewall rule: %w", err))
|
||||
continue
|
||||
}
|
||||
if len(rulePair) > 0 {
|
||||
d.peerRulesPairs[pairID] = rulePair
|
||||
@@ -127,6 +130,10 @@ func (d *DefaultManager) applyPeerACLs(networkMap *mgmProto.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
if merr != nil {
|
||||
log.Errorf("failed to apply %d peer ACL rule(s): %v", merr.Len(), nberrors.FormatErrorOrNil(merr))
|
||||
}
|
||||
|
||||
for pairID, rules := range d.peerRulesPairs {
|
||||
if _, ok := newRulePairs[pairID]; !ok {
|
||||
for _, rule := range rules {
|
||||
@@ -216,10 +223,9 @@ func (d *DefaultManager) protoRuleToFirewallRule(
|
||||
r *mgmProto.FirewallRule,
|
||||
ipsetName string,
|
||||
) (id.RuleID, []firewall.Rule, error) {
|
||||
//nolint:staticcheck // PeerIP used for backward compatibility with old management
|
||||
ip := net.ParseIP(r.PeerIP)
|
||||
if ip == nil {
|
||||
return "", nil, fmt.Errorf("invalid IP address, skipping firewall rule")
|
||||
ip, err := extractRuleIP(r)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
protocol, err := convertToFirewallProtocol(r.Protocol)
|
||||
@@ -290,13 +296,13 @@ func portInfoEmpty(portInfo *mgmProto.PortInfo) bool {
|
||||
|
||||
func (d *DefaultManager) addInRules(
|
||||
id []byte,
|
||||
ip net.IP,
|
||||
ip netip.Addr,
|
||||
protocol firewall.Protocol,
|
||||
port *firewall.Port,
|
||||
action firewall.Action,
|
||||
ipsetName string,
|
||||
) ([]firewall.Rule, error) {
|
||||
rule, err := d.firewall.AddPeerFiltering(id, ip, protocol, nil, port, action, ipsetName)
|
||||
rule, err := d.firewall.AddPeerFiltering(id, ip.AsSlice(), protocol, nil, port, action, ipsetName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add firewall rule: %w", err)
|
||||
}
|
||||
@@ -306,7 +312,7 @@ func (d *DefaultManager) addInRules(
|
||||
|
||||
func (d *DefaultManager) addOutRules(
|
||||
id []byte,
|
||||
ip net.IP,
|
||||
ip netip.Addr,
|
||||
protocol firewall.Protocol,
|
||||
port *firewall.Port,
|
||||
action firewall.Action,
|
||||
@@ -316,7 +322,7 @@ func (d *DefaultManager) addOutRules(
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rule, err := d.firewall.AddPeerFiltering(id, ip, protocol, port, nil, action, ipsetName)
|
||||
rule, err := d.firewall.AddPeerFiltering(id, ip.AsSlice(), protocol, port, nil, action, ipsetName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add firewall rule: %w", err)
|
||||
}
|
||||
@@ -324,9 +330,9 @@ func (d *DefaultManager) addOutRules(
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
// getPeerRuleID() returns unique ID for the rule based on its parameters.
|
||||
// getPeerRuleID returns unique ID for the rule based on its parameters.
|
||||
func (d *DefaultManager) getPeerRuleID(
|
||||
ip net.IP,
|
||||
ip netip.Addr,
|
||||
proto firewall.Protocol,
|
||||
direction int,
|
||||
port *firewall.Port,
|
||||
@@ -345,15 +351,25 @@ func (d *DefaultManager) getRuleGroupingSelector(rule *mgmProto.FirewallRule) st
|
||||
return fmt.Sprintf("%v:%v:%v:%s:%v", strconv.Itoa(int(rule.Direction)), rule.Action, rule.Protocol, rule.Port, rule.PortInfo)
|
||||
}
|
||||
|
||||
func (d *DefaultManager) rollBack(newRulePairs map[id.RuleID][]firewall.Rule) {
|
||||
log.Debugf("rollback ACL to previous state")
|
||||
for _, rules := range newRulePairs {
|
||||
for _, rule := range rules {
|
||||
if err := d.firewall.DeletePeerRule(rule); err != nil {
|
||||
log.Errorf("failed to delete new firewall rule (id: %v) during rollback: %v", rule.ID(), err)
|
||||
}
|
||||
|
||||
// extractRuleIP extracts the peer IP from a firewall rule.
|
||||
// If sourcePrefixes is populated (new management), decode the first entry and use its address.
|
||||
// Otherwise fall back to the deprecated PeerIP string field (old management).
|
||||
func extractRuleIP(r *mgmProto.FirewallRule) (netip.Addr, error) {
|
||||
if len(r.SourcePrefixes) > 0 {
|
||||
addr, err := netiputil.DecodeAddr(r.SourcePrefixes[0])
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("decode source prefix: %w", err)
|
||||
}
|
||||
return addr.Unmap(), nil
|
||||
}
|
||||
|
||||
//nolint:staticcheck // PeerIP used for backward compatibility with old management
|
||||
addr, err := netip.ParseAddr(r.PeerIP)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("invalid IP address, skipping firewall rule")
|
||||
}
|
||||
return addr.Unmap(), nil
|
||||
}
|
||||
|
||||
func convertToFirewallProtocol(protocol mgmProto.RuleProtocol) (firewall.Protocol, error) {
|
||||
|
||||
@@ -430,8 +430,6 @@ func isInCGNATRange(ip net.IP) bool {
|
||||
}
|
||||
|
||||
func TestAnonymizeFirewallRules(t *testing.T) {
|
||||
// TODO: Add ipv6
|
||||
|
||||
// Example iptables-save output
|
||||
iptablesSave := `# Generated by iptables-save v1.8.7 on Thu Dec 19 10:00:00 2024
|
||||
*filter
|
||||
@@ -467,17 +465,31 @@ Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
|
||||
Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
|
||||
pkts bytes target prot opt in out source destination`
|
||||
|
||||
// Example nftables output
|
||||
// Example ip6tables-save output
|
||||
ip6tablesSave := `# Generated by ip6tables-save v1.8.7 on Thu Dec 19 10:00:00 2024
|
||||
*filter
|
||||
:INPUT ACCEPT [0:0]
|
||||
:FORWARD ACCEPT [0:0]
|
||||
:OUTPUT ACCEPT [0:0]
|
||||
-A INPUT -s fd00:1234::1/128 -j ACCEPT
|
||||
-A INPUT -s 2607:f8b0:4005::1/128 -j DROP
|
||||
-A FORWARD -s 2001:db8::/32 -d 2607:f8b0:4005::200e/128 -j ACCEPT
|
||||
COMMIT`
|
||||
|
||||
// Example nftables output with IPv6
|
||||
nftablesRules := `table inet filter {
|
||||
chain input {
|
||||
type filter hook input priority filter; policy accept;
|
||||
ip saddr 192.168.1.1 accept
|
||||
ip saddr 44.192.140.1 drop
|
||||
ip6 saddr 2607:f8b0:4005::1 drop
|
||||
ip6 saddr fd00:1234::1 accept
|
||||
}
|
||||
chain forward {
|
||||
type filter hook forward priority filter; policy accept;
|
||||
ip saddr 10.0.0.0/8 drop
|
||||
ip saddr 44.192.140.0/24 ip daddr 52.84.12.34/24 accept
|
||||
ip6 saddr 2001:db8::/32 ip6 daddr 2607:f8b0:4005::200e/128 accept
|
||||
}
|
||||
}`
|
||||
|
||||
@@ -540,4 +552,35 @@ Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
|
||||
assert.Contains(t, anonNftables, "table inet filter {")
|
||||
assert.Contains(t, anonNftables, "chain input {")
|
||||
assert.Contains(t, anonNftables, "type filter hook input priority filter; policy accept;")
|
||||
|
||||
// IPv6 public addresses in nftables should be anonymized
|
||||
assert.NotContains(t, anonNftables, "2607:f8b0:4005::1")
|
||||
assert.NotContains(t, anonNftables, "2607:f8b0:4005::200e")
|
||||
assert.NotContains(t, anonNftables, "2001:db8::")
|
||||
assert.Contains(t, anonNftables, "2001:db8:ffff::") // Default anonymous v6 range
|
||||
|
||||
// ULA addresses in nftables should remain unchanged (private)
|
||||
assert.Contains(t, anonNftables, "fd00:1234::1")
|
||||
|
||||
// IPv6 nftables structure preserved
|
||||
assert.Contains(t, anonNftables, "ip6 saddr")
|
||||
assert.Contains(t, anonNftables, "ip6 daddr")
|
||||
|
||||
// Test ip6tables-save anonymization
|
||||
anonIp6tablesSave := anonymizer.AnonymizeString(ip6tablesSave)
|
||||
|
||||
// ULA (private) IPv6 should remain unchanged
|
||||
assert.Contains(t, anonIp6tablesSave, "fd00:1234::1/128")
|
||||
|
||||
// Public IPv6 addresses should be anonymized
|
||||
assert.NotContains(t, anonIp6tablesSave, "2607:f8b0:4005::1")
|
||||
assert.NotContains(t, anonIp6tablesSave, "2607:f8b0:4005::200e")
|
||||
assert.NotContains(t, anonIp6tablesSave, "2001:db8::")
|
||||
assert.Contains(t, anonIp6tablesSave, "2001:db8:ffff::") // Default anonymous v6 range
|
||||
|
||||
// Structure should be preserved
|
||||
assert.Contains(t, anonIp6tablesSave, "*filter")
|
||||
assert.Contains(t, anonIp6tablesSave, "COMMIT")
|
||||
assert.Contains(t, anonIp6tablesSave, "-j DROP")
|
||||
assert.Contains(t, anonIp6tablesSave, "-j ACCEPT")
|
||||
}
|
||||
|
||||
@@ -189,10 +189,10 @@ func (s *serviceViaListener) RuntimeIP() netip.Addr {
|
||||
}
|
||||
|
||||
|
||||
// evalListenAddress figure out the listen address for the DNS server
|
||||
// first check the 53 port availability on WG interface or lo, if not success
|
||||
// pick a random port on WG interface for eBPF, if not success
|
||||
// check the 5053 port availability on WG interface or lo without eBPF usage,
|
||||
// evalListenAddress figures out the listen address for the DNS server.
|
||||
// IPv4-only: all peers have a v4 overlay address, and DNS config points to v4.
|
||||
// First checks port 53 on WG interface or lo, then tries eBPF on a random port,
|
||||
// then falls back to port 5053.
|
||||
func (s *serviceViaListener) evalListenAddress() (netip.Addr, uint16, error) {
|
||||
if s.customAddr != nil {
|
||||
return s.customAddr.Addr(), s.customAddr.Port(), nil
|
||||
@@ -278,7 +278,7 @@ func (s *serviceViaListener) tryToUseeBPF() (ebpfMgr.Manager, uint16, bool) {
|
||||
}
|
||||
|
||||
ebpfSrv := ebpf.GetEbpfManagerInstance()
|
||||
err = ebpfSrv.LoadDNSFwd(s.wgInterface.Address().IP.String(), int(port))
|
||||
err = ebpfSrv.LoadDNSFwd(s.wgInterface.Address().IP, int(port))
|
||||
if err != nil {
|
||||
log.Warnf("failed to load DNS forwarder eBPF program, error: %s", err)
|
||||
return nil, 0, false
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"golang.zx2c4.com/wireguard/tun/netstack"
|
||||
|
||||
"github.com/netbirdio/netbird/client/iface"
|
||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||
"github.com/netbirdio/netbird/client/internal/dns/resutil"
|
||||
"github.com/netbirdio/netbird/client/internal/dns/types"
|
||||
"github.com/netbirdio/netbird/client/internal/peer"
|
||||
@@ -29,6 +30,12 @@ import (
|
||||
|
||||
var currentMTU uint16 = iface.DefaultMTU
|
||||
|
||||
// privateClientIface is the subset of the WireGuard interface needed by GetClientPrivate.
|
||||
type privateClientIface interface {
|
||||
Name() string
|
||||
Address() wgaddr.Address
|
||||
}
|
||||
|
||||
func SetCurrentMTU(mtu uint16) {
|
||||
currentMTU = mtu
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ func (u *upstreamResolver) isLocalResolver(upstream string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) {
|
||||
func GetClientPrivate(_ privateClientIface, _ netip.Addr, dialTimeout time.Duration) (*dns.Client, error) {
|
||||
return &dns.Client{
|
||||
Timeout: dialTimeout,
|
||||
Net: "udp",
|
||||
|
||||
@@ -52,7 +52,7 @@ func (u *upstreamResolver) exchange(ctx context.Context, upstream string, r *dns
|
||||
return ExchangeWithFallback(ctx, client, r, upstream)
|
||||
}
|
||||
|
||||
func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) {
|
||||
func GetClientPrivate(_ privateClientIface, _ netip.Addr, dialTimeout time.Duration) (*dns.Client, error) {
|
||||
return &dns.Client{
|
||||
Timeout: dialTimeout,
|
||||
Net: "udp",
|
||||
|
||||
@@ -19,11 +19,7 @@ import (
|
||||
|
||||
type upstreamResolverIOS struct {
|
||||
*upstreamResolverBase
|
||||
lIP netip.Addr
|
||||
lNet netip.Prefix
|
||||
lIPv6 netip.Addr
|
||||
lNetV6 netip.Prefix
|
||||
interfaceName string
|
||||
wgIface WGIface
|
||||
}
|
||||
|
||||
func newUpstreamResolver(
|
||||
@@ -37,11 +33,7 @@ func newUpstreamResolver(
|
||||
|
||||
ios := &upstreamResolverIOS{
|
||||
upstreamResolverBase: upstreamResolverBase,
|
||||
lIP: wgIface.Address().IP,
|
||||
lNet: wgIface.Address().Network,
|
||||
lIPv6: wgIface.Address().IPv6,
|
||||
lNetV6: wgIface.Address().IPv6Net,
|
||||
interfaceName: wgIface.Name(),
|
||||
wgIface: wgIface,
|
||||
}
|
||||
ios.upstreamClient = ios
|
||||
|
||||
@@ -69,24 +61,15 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *
|
||||
} else {
|
||||
upstreamIP = upstreamIP.Unmap()
|
||||
}
|
||||
needsPrivate := u.lNet.Contains(upstreamIP) ||
|
||||
u.lNetV6.Contains(upstreamIP) ||
|
||||
addr := u.wgIface.Address()
|
||||
needsPrivate := addr.Network.Contains(upstreamIP) ||
|
||||
addr.IPv6Net.Contains(upstreamIP) ||
|
||||
(u.routeMatch != nil && u.routeMatch(upstreamIP))
|
||||
if needsPrivate {
|
||||
var bindIP netip.Addr
|
||||
switch {
|
||||
case upstreamIP.Is6() && u.lIPv6.IsValid():
|
||||
bindIP = u.lIPv6
|
||||
case upstreamIP.Is4() && u.lIP.IsValid():
|
||||
bindIP = u.lIP
|
||||
}
|
||||
|
||||
if bindIP.IsValid() {
|
||||
log.Debugf("using private client to query %s via upstream %s", r.Question[0].Name, upstream)
|
||||
client, err = GetClientPrivate(bindIP, u.interfaceName, timeout)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("create private client: %s", err)
|
||||
}
|
||||
log.Debugf("using private client to query %s via upstream %s", r.Question[0].Name, upstream)
|
||||
client, err = GetClientPrivate(u.wgIface, upstreamIP, timeout)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("create private client: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,23 +77,29 @@ func (u *upstreamResolverIOS) exchange(ctx context.Context, upstream string, r *
|
||||
return ExchangeWithFallback(nil, client, r, upstream)
|
||||
}
|
||||
|
||||
// GetClientPrivate returns a new DNS client bound to the local IP address of the Netbird interface
|
||||
// This method is needed for iOS
|
||||
func GetClientPrivate(ip netip.Addr, interfaceName string, dialTimeout time.Duration) (*dns.Client, error) {
|
||||
index, err := getInterfaceIndex(interfaceName)
|
||||
// GetClientPrivate returns a new DNS client bound to the local IP of the Netbird interface.
|
||||
// It selects the v6 bind address when the upstream is IPv6 and the interface has one, otherwise v4.
|
||||
func GetClientPrivate(iface privateClientIface, upstreamIP netip.Addr, dialTimeout time.Duration) (*dns.Client, error) {
|
||||
index, err := getInterfaceIndex(iface.Name())
|
||||
if err != nil {
|
||||
log.Debugf("unable to get interface index for %s: %s", interfaceName, err)
|
||||
log.Debugf("unable to get interface index for %s: %s", iface.Name(), err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr := iface.Address()
|
||||
bindIP := addr.IP
|
||||
if upstreamIP.Is6() && addr.HasIPv6() {
|
||||
bindIP = addr.IPv6
|
||||
}
|
||||
|
||||
proto, opt := unix.IPPROTO_IP, unix.IP_BOUND_IF
|
||||
if ip.Is6() {
|
||||
if bindIP.Is6() {
|
||||
proto, opt = unix.IPPROTO_IPV6, unix.IPV6_BOUND_IF
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
LocalAddr: net.UDPAddrFromAddrPort(netip.AddrPortFrom(ip, 0)),
|
||||
Timeout: dialTimeout,
|
||||
LocalAddr: net.UDPAddrFromAddrPort(netip.AddrPortFrom(bindIP, 0)),
|
||||
Timeout: dialTimeout,
|
||||
Control: func(network, address string, c syscall.RawConn) error {
|
||||
var operr error
|
||||
fn := func(s uintptr) {
|
||||
|
||||
@@ -80,6 +80,7 @@ func (m *Manager) Start(fwdEntries []*ForwarderEntry) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// IPv4-only: peers reach the forwarder via its v4 overlay address.
|
||||
localAddr := m.wgIface.Address().IP
|
||||
|
||||
if localAddr.IsValid() && m.firewall != nil {
|
||||
|
||||
@@ -2,7 +2,8 @@ package ebpf
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"net"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -12,7 +13,7 @@ const (
|
||||
mapKeyDNSPort uint32 = 1
|
||||
)
|
||||
|
||||
func (tf *GeneralManager) LoadDNSFwd(ip string, dnsPort int) error {
|
||||
func (tf *GeneralManager) LoadDNSFwd(ip netip.Addr, dnsPort int) error {
|
||||
log.Debugf("load eBPF DNS forwarder, watching addr: %s:53, redirect to port: %d", ip, dnsPort)
|
||||
tf.lock.Lock()
|
||||
defer tf.lock.Unlock()
|
||||
@@ -22,7 +23,11 @@ func (tf *GeneralManager) LoadDNSFwd(ip string, dnsPort int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tf.bpfObjs.NbMapDnsIp.Put(mapKeyDNSIP, ip2int(ip))
|
||||
if !ip.Is4() {
|
||||
return fmt.Errorf("eBPF DNS forwarder only supports IPv4, got %s", ip)
|
||||
}
|
||||
ip4 := ip.As4()
|
||||
err = tf.bpfObjs.NbMapDnsIp.Put(mapKeyDNSIP, binary.BigEndian.Uint32(ip4[:]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -45,7 +50,3 @@ func (tf *GeneralManager) FreeDNSFwd() error {
|
||||
return tf.unsetFeatureFlag(featureFlagDnsForwarder)
|
||||
}
|
||||
|
||||
func ip2int(ipString string) uint32 {
|
||||
ip := net.ParseIP(ipString)
|
||||
return binary.BigEndian.Uint32(ip.To4())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package manager
|
||||
|
||||
import "net/netip"
|
||||
|
||||
// Manager is used to load multiple eBPF programs. E.g., current DNS programs and WireGuard proxy
|
||||
type Manager interface {
|
||||
LoadDNSFwd(ip string, dnsPort int) error
|
||||
LoadDNSFwd(ip netip.Addr, dnsPort int) error
|
||||
FreeDNSFwd() error
|
||||
LoadWgProxy(proxyPort, wgPort int) error
|
||||
FreeWGProxy() error
|
||||
|
||||
@@ -630,7 +630,7 @@ func (e *Engine) initFirewall() error {
|
||||
rosenpassPort := e.rpManager.GetAddress().Port
|
||||
port := firewallManager.Port{Values: []uint16{uint16(rosenpassPort)}}
|
||||
|
||||
// this rule is static and will be torn down on engine down by the firewall manager
|
||||
// IPv4-only: rosenpass peers connect via AllowedIps[0] which is always v4.
|
||||
if _, err := e.firewall.AddPeerFiltering(
|
||||
nil,
|
||||
net.IP{0, 0, 0, 0},
|
||||
@@ -682,10 +682,15 @@ func (e *Engine) blockLanAccess() {
|
||||
|
||||
log.Infof("blocking route LAN access for networks: %v", toBlock)
|
||||
v4 := netip.PrefixFrom(netip.IPv4Unspecified(), 0)
|
||||
v6 := netip.PrefixFrom(netip.IPv6Unspecified(), 0)
|
||||
for _, network := range toBlock {
|
||||
source := v4
|
||||
if network.Addr().Is6() {
|
||||
source = v6
|
||||
}
|
||||
if _, err := e.firewall.AddRouteFiltering(
|
||||
nil,
|
||||
[]netip.Prefix{v4},
|
||||
[]netip.Prefix{source},
|
||||
firewallManager.Network{Prefix: network},
|
||||
firewallManager.ProtocolALL,
|
||||
nil,
|
||||
@@ -1494,10 +1499,10 @@ func (e *Engine) updateOfflinePeers(offlinePeers []*mgmProto.RemotePeerConfig) {
|
||||
replacement := make([]peer.State, len(offlinePeers))
|
||||
for i, offlinePeer := range offlinePeers {
|
||||
log.Debugf("added offline peer %s", offlinePeer.Fqdn)
|
||||
v4, v6 := splitAllowedIPs(offlinePeer.GetAllowedIps(), e.wgInterface.Address().IPv6Net)
|
||||
v4, v6 := overlayAddrsFromAllowedIPs(offlinePeer.GetAllowedIps(), e.wgInterface.Address().IPv6Net)
|
||||
replacement[i] = peer.State{
|
||||
IP: v4,
|
||||
IPv6: v6,
|
||||
IP: addrToString(v4),
|
||||
IPv6: addrToString(v6),
|
||||
PubKey: offlinePeer.GetWgPubKey(),
|
||||
FQDN: offlinePeer.GetFqdn(),
|
||||
ConnStatus: peer.StatusIdle,
|
||||
@@ -1508,30 +1513,37 @@ func (e *Engine) updateOfflinePeers(offlinePeers []*mgmProto.RemotePeerConfig) {
|
||||
e.statusRecorder.ReplaceOfflinePeers(replacement)
|
||||
}
|
||||
|
||||
// splitAllowedIPs separates the peer's overlay v4 (/32) and v6 (/128) addresses
|
||||
// from a list of AllowedIPs CIDRs. The v6 address is only matched if it falls
|
||||
// within ourV6Net (the local overlay v6 subnet), to avoid confusing routed /128
|
||||
// prefixes with the peer's overlay address.
|
||||
func splitAllowedIPs(allowedIPs []string, ourV6Net netip.Prefix) (v4, v6 string) {
|
||||
// overlayAddrsFromAllowedIPs extracts the peer's v4 and v6 overlay addresses
|
||||
// from AllowedIPs strings. Only host routes (/32, /128) are considered; v6 must
|
||||
// fall within ourV6Net to distinguish overlay addresses from routed prefixes.
|
||||
func overlayAddrsFromAllowedIPs(allowedIPs []string, ourV6Net netip.Prefix) (v4, v6 netip.Addr) {
|
||||
for _, cidr := range allowedIPs {
|
||||
prefix, err := netip.ParsePrefix(cidr)
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse AllowedIP %q: %v", cidr, err)
|
||||
continue
|
||||
}
|
||||
addr := prefix.Addr().Unmap()
|
||||
switch {
|
||||
case prefix.Addr().Is4() && prefix.Bits() == 32 && v4 == "":
|
||||
v4 = prefix.Addr().String()
|
||||
case prefix.Addr().Is6() && prefix.Bits() == 128 && ourV6Net.Contains(prefix.Addr()) && v6 == "":
|
||||
v6 = prefix.Addr().String()
|
||||
case addr.Is4() && prefix.Bits() == 32 && !v4.IsValid():
|
||||
v4 = addr
|
||||
case addr.Is6() && prefix.Bits() == 128 && ourV6Net.Contains(addr) && !v6.IsValid():
|
||||
v6 = addr
|
||||
}
|
||||
if v4 != "" && v6 != "" {
|
||||
if v4.IsValid() && v6.IsValid() {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func addrToString(addr netip.Addr) string {
|
||||
if !addr.IsValid() {
|
||||
return ""
|
||||
}
|
||||
return addr.String()
|
||||
}
|
||||
|
||||
// addNewPeers adds peers that were not know before but arrived from the Management service with the update
|
||||
func (e *Engine) addNewPeers(peersUpdate []*mgmProto.RemotePeerConfig) error {
|
||||
for _, p := range peersUpdate {
|
||||
@@ -1572,8 +1584,8 @@ func (e *Engine) addNewPeer(peerConfig *mgmProto.RemotePeerConfig) error {
|
||||
return fmt.Errorf("create peer connection: %w", err)
|
||||
}
|
||||
|
||||
peerV4, peerV6 := splitAllowedIPs(peerConfig.GetAllowedIps(), e.wgInterface.Address().IPv6Net)
|
||||
err = e.statusRecorder.AddPeer(peerKey, peerConfig.Fqdn, peerV4, peerV6)
|
||||
peerV4, peerV6 := overlayAddrsFromAllowedIPs(peerConfig.GetAllowedIps(), e.wgInterface.Address().IPv6Net)
|
||||
err = e.statusRecorder.AddPeer(peerKey, peerConfig.Fqdn, addrToString(peerV4), addrToString(peerV6))
|
||||
if err != nil {
|
||||
log.Warnf("error adding peer %s to status recorder, got error: %v", peerKey, err)
|
||||
}
|
||||
@@ -2355,8 +2367,7 @@ func getInterfacePrefixes() ([]netip.Prefix, error) {
|
||||
prefix := netip.PrefixFrom(addr.Unmap(), ones).Masked()
|
||||
ip := prefix.Addr()
|
||||
|
||||
// TODO: add IPv6
|
||||
if !ip.Is4() || ip.IsLoopback() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||
if ip.IsLoopback() || ip.IsMulticast() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -145,13 +145,13 @@ func (e *Engine) extractPeerSSHInfo(remotePeers []*mgmProto.RemotePeerConfig) []
|
||||
continue
|
||||
}
|
||||
|
||||
peerIP, peerIPv6 := e.extractPeerIPs(peerConfig)
|
||||
peerV4, peerV6 := overlayAddrsFromAllowedIPs(peerConfig.GetAllowedIps(), e.wgInterface.Address().IPv6Net)
|
||||
hostname := e.extractHostname(peerConfig)
|
||||
|
||||
peerInfo = append(peerInfo, sshconfig.PeerSSHInfo{
|
||||
Hostname: hostname,
|
||||
IP: peerIP,
|
||||
IPv6: peerIPv6,
|
||||
IP: peerV4,
|
||||
IPv6: peerV6,
|
||||
FQDN: peerConfig.GetFqdn(),
|
||||
})
|
||||
}
|
||||
@@ -159,28 +159,6 @@ func (e *Engine) extractPeerSSHInfo(remotePeers []*mgmProto.RemotePeerConfig) []
|
||||
return peerInfo
|
||||
}
|
||||
|
||||
// extractPeerIPs extracts IPv4 and IPv6 overlay addresses from peer's allowed IPs.
|
||||
// Only considers host routes (/32, /128) within the overlay networks to avoid
|
||||
// picking up routed prefixes or static routes like 2620:fe::fe/128.
|
||||
func (e *Engine) extractPeerIPs(peerConfig *mgmProto.RemotePeerConfig) (v4, v6 netip.Addr) {
|
||||
wgAddr := e.wgInterface.Address()
|
||||
for _, allowedIP := range peerConfig.GetAllowedIps() {
|
||||
prefix, err := netip.ParsePrefix(allowedIP)
|
||||
if err != nil {
|
||||
log.Warnf("failed to parse AllowedIP %q: %v", allowedIP, err)
|
||||
continue
|
||||
}
|
||||
addr := prefix.Addr().Unmap()
|
||||
switch {
|
||||
case addr.Is4() && prefix.Bits() == 32 && wgAddr.Network.Contains(addr) && !v4.IsValid():
|
||||
v4 = addr
|
||||
case addr.Is6() && prefix.Bits() == 128 && wgAddr.IPv6Net.IsValid() && wgAddr.IPv6Net.Contains(addr) && !v6.IsValid():
|
||||
v6 = addr
|
||||
}
|
||||
}
|
||||
return v4, v6
|
||||
}
|
||||
|
||||
// extractHostname extracts short hostname from FQDN
|
||||
func (e *Engine) extractHostname(peerConfig *mgmProto.RemotePeerConfig) string {
|
||||
fqdn := peerConfig.GetFqdn()
|
||||
|
||||
@@ -1837,7 +1837,7 @@ func TestFilterAllowedIPs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitAllowedIPs(t *testing.T) {
|
||||
func TestOverlayAddrsFromAllowedIPs(t *testing.T) {
|
||||
ourV6Net := netip.MustParsePrefix("fd00:1234:5678:abcd::/64")
|
||||
|
||||
tests := []struct {
|
||||
@@ -1900,9 +1900,17 @@ func TestSplitAllowedIPs(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
v4, v6 := splitAllowedIPs(tt.allowedIPs, tt.ourV6Net)
|
||||
assert.Equal(t, tt.wantV4, v4, "v4")
|
||||
assert.Equal(t, tt.wantV6, v6, "v6")
|
||||
v4, v6 := overlayAddrsFromAllowedIPs(tt.allowedIPs, tt.ourV6Net)
|
||||
if tt.wantV4 == "" {
|
||||
assert.False(t, v4.IsValid(), "expected no v4")
|
||||
} else {
|
||||
assert.Equal(t, tt.wantV4, v4.String(), "v4")
|
||||
}
|
||||
if tt.wantV6 == "" {
|
||||
assert.False(t, v6.IsValid(), "expected no v6")
|
||||
} else {
|
||||
assert.Equal(t, tt.wantV6, v6.String(), "v6")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ func NewBindListener(wgIface WgInterface, bind device.EndpointManager, cfg lazyc
|
||||
// deriveFakeIP creates a deterministic fake IP for bind mode based on peer's NetBird IP.
|
||||
// Maps peer IP 100.64.x.y to fake IP 127.2.x.y (similar to relay proxy using 127.1.x.y).
|
||||
// It finds the peer's actual NetBird IP by checking which allowedIP is in the same subnet as our WG interface.
|
||||
// For IPv6-only peers, the last two bytes of the v6 address are used.
|
||||
func deriveFakeIP(wgIface WgInterface, allowedIPs []netip.Prefix) (netip.Addr, error) {
|
||||
if len(allowedIPs) == 0 {
|
||||
return netip.Addr{}, fmt.Errorf("no allowed IPs for peer")
|
||||
@@ -64,6 +65,7 @@ func deriveFakeIP(wgIface WgInterface, allowedIPs []netip.Prefix) (netip.Addr, e
|
||||
|
||||
ourNetwork := wgIface.Address().Network
|
||||
|
||||
// Try v4 first (preferred: deterministic from overlay IP)
|
||||
var peerIP netip.Addr
|
||||
for _, allowedIP := range allowedIPs {
|
||||
ip := allowedIP.Addr()
|
||||
@@ -76,13 +78,24 @@ func deriveFakeIP(wgIface WgInterface, allowedIPs []netip.Prefix) (netip.Addr, e
|
||||
}
|
||||
}
|
||||
|
||||
if !peerIP.IsValid() {
|
||||
return netip.Addr{}, fmt.Errorf("no peer NetBird IP found in allowed IPs")
|
||||
if peerIP.IsValid() {
|
||||
octets := peerIP.As4()
|
||||
return netip.AddrFrom4([4]byte{127, 2, octets[2], octets[3]}), nil
|
||||
}
|
||||
|
||||
octets := peerIP.As4()
|
||||
fakeIP := netip.AddrFrom4([4]byte{127, 2, octets[2], octets[3]})
|
||||
return fakeIP, nil
|
||||
// Fallback: use last two bytes of first v6 overlay IP
|
||||
addr := wgIface.Address()
|
||||
if addr.IPv6Net.IsValid() {
|
||||
for _, allowedIP := range allowedIPs {
|
||||
ip := allowedIP.Addr()
|
||||
if ip.Is6() && addr.IPv6Net.Contains(ip) {
|
||||
raw := ip.As16()
|
||||
return netip.AddrFrom4([4]byte{127, 2, raw[14], raw[15]}), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return netip.Addr{}, fmt.Errorf("no peer NetBird IP found in allowed IPs")
|
||||
}
|
||||
|
||||
func (d *BindListener) setupLazyConn() error {
|
||||
|
||||
@@ -1055,7 +1055,11 @@ func (d *Status) notifyPeerListChanged() {
|
||||
}
|
||||
|
||||
func (d *Status) notifyAddressChanged() {
|
||||
d.notifier.localAddressChanged(d.localPeer.FQDN, d.localPeer.IP)
|
||||
addr := d.localPeer.IP
|
||||
if d.localPeer.IPv6 != "" {
|
||||
addr = addr + "\n" + d.localPeer.IPv6
|
||||
}
|
||||
d.notifier.localAddressChanged(d.localPeer.FQDN, addr)
|
||||
}
|
||||
|
||||
func (d *Status) numOfPeers() int {
|
||||
|
||||
@@ -3,9 +3,8 @@ package client
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
@@ -566,7 +565,7 @@ func HandlerFromRoute(params common.HandlerParams) RouteHandler {
|
||||
return dnsinterceptor.New(params)
|
||||
case handlerTypeDynamic:
|
||||
dns := nbdns.NewServiceViaMemory(params.WgInterface)
|
||||
dnsAddr := net.JoinHostPort(dns.RuntimeIP().String(), strconv.Itoa(dns.RuntimePort()))
|
||||
dnsAddr := netip.AddrPortFrom(dns.RuntimeIP(), uint16(dns.RuntimePort()))
|
||||
return dynamic.NewRoute(params, dnsAddr)
|
||||
default:
|
||||
return static.NewRoute(params)
|
||||
|
||||
@@ -582,7 +582,7 @@ func (d *DnsInterceptor) queryUpstreamDNS(ctx context.Context, w dns.ResponseWri
|
||||
if nsNet != nil {
|
||||
reply, err = nbdns.ExchangeWithNetstack(ctx, nsNet, r, upstream)
|
||||
} else {
|
||||
client, clientErr := nbdns.GetClientPrivate(d.wgInterface.Address().IP, d.wgInterface.Name(), dnsTimeout)
|
||||
client, clientErr := nbdns.GetClientPrivate(d.wgInterface, upstreamIP, dnsTimeout)
|
||||
if clientErr != nil {
|
||||
d.writeDNSError(w, r, logger, fmt.Sprintf("create DNS client: %v", clientErr))
|
||||
return nil
|
||||
|
||||
@@ -50,10 +50,10 @@ type Route struct {
|
||||
cancel context.CancelFunc
|
||||
statusRecorder *peer.Status
|
||||
wgInterface iface.WGIface
|
||||
resolverAddr string
|
||||
resolverAddr netip.AddrPort
|
||||
}
|
||||
|
||||
func NewRoute(params common.HandlerParams, resolverAddr string) *Route {
|
||||
func NewRoute(params common.HandlerParams, resolverAddr netip.AddrPort) *Route {
|
||||
return &Route{
|
||||
route: params.Route,
|
||||
routeRefCounter: params.RouteRefCounter,
|
||||
|
||||
@@ -17,37 +17,47 @@ import (
|
||||
const dialTimeout = 10 * time.Second
|
||||
|
||||
func (r *Route) getIPsFromResolver(domain domain.Domain) ([]net.IP, error) {
|
||||
privateClient, err := nbdns.GetClientPrivate(r.wgInterface.Address().IP, r.wgInterface.Name(), dialTimeout)
|
||||
privateClient, err := nbdns.GetClientPrivate(r.wgInterface, r.resolverAddr.Addr(), dialTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error while creating private client: %s", err)
|
||||
}
|
||||
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(dns.Fqdn(domain.PunycodeString()), dns.TypeA)
|
||||
|
||||
fqdn := dns.Fqdn(domain.PunycodeString())
|
||||
startTime := time.Now()
|
||||
|
||||
response, _, err := nbdns.ExchangeWithFallback(nil, privateClient, msg, r.resolverAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("DNS query for %s failed after %s: %s ", domain.SafeString(), time.Since(startTime), err)
|
||||
}
|
||||
var ips []net.IP
|
||||
var queryErr error
|
||||
|
||||
if response.Rcode != dns.RcodeSuccess {
|
||||
return nil, fmt.Errorf("dns response code: %s", dns.RcodeToString[response.Rcode])
|
||||
}
|
||||
for _, qtype := range []uint16{dns.TypeA, dns.TypeAAAA} {
|
||||
msg := new(dns.Msg)
|
||||
msg.SetQuestion(fqdn, qtype)
|
||||
|
||||
ips := make([]net.IP, 0)
|
||||
|
||||
for _, answ := range response.Answer {
|
||||
if aRecord, ok := answ.(*dns.A); ok {
|
||||
ips = append(ips, aRecord.A)
|
||||
response, _, err := nbdns.ExchangeWithFallback(nil, privateClient, msg, r.resolverAddr.String())
|
||||
if err != nil {
|
||||
if queryErr == nil {
|
||||
queryErr = fmt.Errorf("DNS query for %s (type %d) after %s: %w", domain.SafeString(), qtype, time.Since(startTime), err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if aaaaRecord, ok := answ.(*dns.AAAA); ok {
|
||||
ips = append(ips, aaaaRecord.AAAA)
|
||||
|
||||
if response.Rcode != dns.RcodeSuccess {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, answ := range response.Answer {
|
||||
if aRecord, ok := answ.(*dns.A); ok {
|
||||
ips = append(ips, aRecord.A)
|
||||
}
|
||||
if aaaaRecord, ok := answ.(*dns.AAAA); ok {
|
||||
ips = append(ips, aaaaRecord.AAAA)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(ips) == 0 {
|
||||
if queryErr != nil {
|
||||
return nil, queryErr
|
||||
}
|
||||
return nil, fmt.Errorf("no A or AAAA records found for %s", domain.SafeString())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,93 +1,145 @@
|
||||
package fakeip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Manager manages allocation of fake IPs from the 240.0.0.0/8 block
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
nextIP netip.Addr // Next IP to allocate
|
||||
var (
|
||||
// 240.0.0.1 - 240.255.255.254, block 240.0.0.0/8 (reserved, RFC 1112)
|
||||
v4Base = netip.AddrFrom4([4]byte{240, 0, 0, 1})
|
||||
v4Max = netip.AddrFrom4([4]byte{240, 255, 255, 254})
|
||||
v4Block = netip.PrefixFrom(netip.AddrFrom4([4]byte{240, 0, 0, 0}), 8)
|
||||
|
||||
// 0100::1 - 0100::ffff:ffff:ffff:fffe, block 0100::/64 (discard, RFC 6666)
|
||||
v6Base = netip.AddrFrom16([16]byte{0x01, 0x00, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01})
|
||||
v6Max = netip.AddrFrom16([16]byte{0x01, 0x00, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe})
|
||||
v6Block = netip.PrefixFrom(netip.AddrFrom16([16]byte{0x01, 0x00}), 64)
|
||||
)
|
||||
|
||||
// fakeIPPool holds the allocation state for a single address family.
|
||||
type fakeIPPool struct {
|
||||
nextIP netip.Addr
|
||||
baseIP netip.Addr
|
||||
maxIP netip.Addr
|
||||
block netip.Prefix
|
||||
allocated map[netip.Addr]netip.Addr // real IP -> fake IP
|
||||
fakeToReal map[netip.Addr]netip.Addr // fake IP -> real IP
|
||||
baseIP netip.Addr // First usable IP: 240.0.0.1
|
||||
maxIP netip.Addr // Last usable IP: 240.255.255.254
|
||||
}
|
||||
|
||||
// NewManager creates a new fake IP manager using 240.0.0.0/8 block
|
||||
func NewManager() *Manager {
|
||||
baseIP := netip.AddrFrom4([4]byte{240, 0, 0, 1})
|
||||
maxIP := netip.AddrFrom4([4]byte{240, 255, 255, 254})
|
||||
|
||||
return &Manager{
|
||||
nextIP: baseIP,
|
||||
func newPool(base, maxAddr netip.Addr, block netip.Prefix) *fakeIPPool {
|
||||
return &fakeIPPool{
|
||||
nextIP: base,
|
||||
baseIP: base,
|
||||
maxIP: maxAddr,
|
||||
block: block,
|
||||
allocated: make(map[netip.Addr]netip.Addr),
|
||||
fakeToReal: make(map[netip.Addr]netip.Addr),
|
||||
baseIP: baseIP,
|
||||
maxIP: maxIP,
|
||||
}
|
||||
}
|
||||
|
||||
// AllocateFakeIP allocates a fake IP for the given real IP
|
||||
// Returns the fake IP, or existing fake IP if already allocated
|
||||
func (m *Manager) AllocateFakeIP(realIP netip.Addr) (netip.Addr, error) {
|
||||
if !realIP.Is4() {
|
||||
return netip.Addr{}, fmt.Errorf("only IPv4 addresses supported")
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if fakeIP, exists := m.allocated[realIP]; exists {
|
||||
// allocate allocates a fake IP for the given real IP.
|
||||
// Returns the existing fake IP if already allocated.
|
||||
func (p *fakeIPPool) allocate(realIP netip.Addr) (netip.Addr, error) {
|
||||
if fakeIP, exists := p.allocated[realIP]; exists {
|
||||
return fakeIP, nil
|
||||
}
|
||||
|
||||
startIP := m.nextIP
|
||||
startIP := p.nextIP
|
||||
for {
|
||||
currentIP := m.nextIP
|
||||
currentIP := p.nextIP
|
||||
|
||||
// Advance to next IP, wrapping at boundary
|
||||
if m.nextIP.Compare(m.maxIP) >= 0 {
|
||||
m.nextIP = m.baseIP
|
||||
if p.nextIP.Compare(p.maxIP) >= 0 {
|
||||
p.nextIP = p.baseIP
|
||||
} else {
|
||||
m.nextIP = m.nextIP.Next()
|
||||
p.nextIP = p.nextIP.Next()
|
||||
}
|
||||
|
||||
// Check if current IP is available
|
||||
if _, inUse := m.fakeToReal[currentIP]; !inUse {
|
||||
m.allocated[realIP] = currentIP
|
||||
m.fakeToReal[currentIP] = realIP
|
||||
if _, inUse := p.fakeToReal[currentIP]; !inUse {
|
||||
p.allocated[realIP] = currentIP
|
||||
p.fakeToReal[currentIP] = realIP
|
||||
return currentIP, nil
|
||||
}
|
||||
|
||||
// Prevent infinite loop if all IPs exhausted
|
||||
if m.nextIP.Compare(startIP) == 0 {
|
||||
return netip.Addr{}, fmt.Errorf("no more fake IPs available in 240.0.0.0/8 block")
|
||||
if p.nextIP.Compare(startIP) == 0 {
|
||||
return netip.Addr{}, fmt.Errorf("no more fake IPs available in %s block", p.block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetFakeIP returns the fake IP for a real IP if it exists
|
||||
// Manager manages allocation of fake IPs for dynamic DNS routes.
|
||||
// IPv4 uses 240.0.0.0/8 (reserved), IPv6 uses 0100::/64 (discard, RFC 6666).
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
v4 *fakeIPPool
|
||||
v6 *fakeIPPool
|
||||
}
|
||||
|
||||
// NewManager creates a new fake IP manager.
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
v4: newPool(v4Base, v4Max, v4Block),
|
||||
v6: newPool(v6Base, v6Max, v6Block),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) pool(ip netip.Addr) *fakeIPPool {
|
||||
if ip.Is6() {
|
||||
return m.v6
|
||||
}
|
||||
return m.v4
|
||||
}
|
||||
|
||||
// AllocateFakeIP allocates a fake IP for the given real IP.
|
||||
func (m *Manager) AllocateFakeIP(realIP netip.Addr) (netip.Addr, error) {
|
||||
realIP = realIP.Unmap()
|
||||
if !realIP.IsValid() {
|
||||
return netip.Addr{}, errors.New("invalid IP address")
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
return m.pool(realIP).allocate(realIP)
|
||||
}
|
||||
|
||||
// GetFakeIP returns the fake IP for a real IP if it exists.
|
||||
func (m *Manager) GetFakeIP(realIP netip.Addr) (netip.Addr, bool) {
|
||||
realIP = realIP.Unmap()
|
||||
if !realIP.IsValid() {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
fakeIP, exists := m.allocated[realIP]
|
||||
return fakeIP, exists
|
||||
fakeIP, ok := m.pool(realIP).allocated[realIP]
|
||||
return fakeIP, ok
|
||||
}
|
||||
|
||||
// GetRealIP returns the real IP for a fake IP if it exists, otherwise false
|
||||
// GetRealIP returns the real IP for a fake IP if it exists.
|
||||
func (m *Manager) GetRealIP(fakeIP netip.Addr) (netip.Addr, bool) {
|
||||
fakeIP = fakeIP.Unmap()
|
||||
if !fakeIP.IsValid() {
|
||||
return netip.Addr{}, false
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
realIP, exists := m.fakeToReal[fakeIP]
|
||||
return realIP, exists
|
||||
realIP, ok := m.pool(fakeIP).fakeToReal[fakeIP]
|
||||
return realIP, ok
|
||||
}
|
||||
|
||||
// GetFakeIPBlock returns the fake IP block used by this manager
|
||||
// GetFakeIPBlock returns the v4 fake IP block used by this manager.
|
||||
func (m *Manager) GetFakeIPBlock() netip.Prefix {
|
||||
return netip.MustParsePrefix("240.0.0.0/8")
|
||||
return m.v4.block
|
||||
}
|
||||
|
||||
// GetFakeIPv6Block returns the v6 fake IP block used by this manager.
|
||||
func (m *Manager) GetFakeIPv6Block() netip.Prefix {
|
||||
return m.v6.block
|
||||
}
|
||||
|
||||
@@ -9,16 +9,16 @@ import (
|
||||
func TestNewManager(t *testing.T) {
|
||||
manager := NewManager()
|
||||
|
||||
if manager.baseIP.String() != "240.0.0.1" {
|
||||
t.Errorf("Expected base IP 240.0.0.1, got %s", manager.baseIP.String())
|
||||
if manager.v4.baseIP.String() != "240.0.0.1" {
|
||||
t.Errorf("Expected v4 base IP 240.0.0.1, got %s", manager.v4.baseIP.String())
|
||||
}
|
||||
|
||||
if manager.maxIP.String() != "240.255.255.254" {
|
||||
t.Errorf("Expected max IP 240.255.255.254, got %s", manager.maxIP.String())
|
||||
if manager.v4.maxIP.String() != "240.255.255.254" {
|
||||
t.Errorf("Expected v4 max IP 240.255.255.254, got %s", manager.v4.maxIP.String())
|
||||
}
|
||||
|
||||
if manager.nextIP.Compare(manager.baseIP) != 0 {
|
||||
t.Errorf("Expected nextIP to start at baseIP")
|
||||
if manager.v6.baseIP.String() != "100::1" {
|
||||
t.Errorf("Expected v6 base IP 100::1, got %s", manager.v6.baseIP.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ func TestAllocateFakeIP(t *testing.T) {
|
||||
t.Error("Fake IP should be IPv4")
|
||||
}
|
||||
|
||||
// Check it's in the correct range
|
||||
if fakeIP.As4()[0] != 240 {
|
||||
t.Errorf("Fake IP should be in 240.0.0.0/8 range, got %s", fakeIP.String())
|
||||
}
|
||||
@@ -51,13 +50,31 @@ func TestAllocateFakeIP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllocateFakeIPIPv6Rejection(t *testing.T) {
|
||||
func TestAllocateFakeIPv6(t *testing.T) {
|
||||
manager := NewManager()
|
||||
realIPv6 := netip.MustParseAddr("2001:db8::1")
|
||||
realIP := netip.MustParseAddr("2001:db8::1")
|
||||
|
||||
_, err := manager.AllocateFakeIP(realIPv6)
|
||||
if err == nil {
|
||||
t.Error("Expected error for IPv6 address")
|
||||
fakeIP, err := manager.AllocateFakeIP(realIP)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate fake IPv6: %v", err)
|
||||
}
|
||||
|
||||
if !fakeIP.Is6() {
|
||||
t.Error("Fake IP should be IPv6")
|
||||
}
|
||||
|
||||
if !netip.MustParsePrefix("100::/64").Contains(fakeIP) {
|
||||
t.Errorf("Fake IP should be in 100::/64 range, got %s", fakeIP.String())
|
||||
}
|
||||
|
||||
// Should return same fake IP for same real IP
|
||||
fakeIP2, err := manager.AllocateFakeIP(realIP)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get existing fake IPv6: %v", err)
|
||||
}
|
||||
|
||||
if fakeIP.Compare(fakeIP2) != 0 {
|
||||
t.Errorf("Expected same fake IP, got %s and %s", fakeIP.String(), fakeIP2.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +82,11 @@ func TestGetFakeIP(t *testing.T) {
|
||||
manager := NewManager()
|
||||
realIP := netip.MustParseAddr("1.1.1.1")
|
||||
|
||||
// Should not exist initially
|
||||
_, exists := manager.GetFakeIP(realIP)
|
||||
if exists {
|
||||
t.Error("Fake IP should not exist before allocation")
|
||||
}
|
||||
|
||||
// Allocate and check
|
||||
expectedFakeIP, err := manager.AllocateFakeIP(realIP)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate: %v", err)
|
||||
@@ -87,12 +102,30 @@ func TestGetFakeIP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRealIPv6(t *testing.T) {
|
||||
manager := NewManager()
|
||||
realIP := netip.MustParseAddr("2001:db8::1")
|
||||
|
||||
fakeIP, err := manager.AllocateFakeIP(realIP)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate: %v", err)
|
||||
}
|
||||
|
||||
gotReal, exists := manager.GetRealIP(fakeIP)
|
||||
if !exists {
|
||||
t.Error("Real IP should exist for allocated fake IP")
|
||||
}
|
||||
|
||||
if gotReal.Compare(realIP) != 0 {
|
||||
t.Errorf("Expected real IP %s, got %s", realIP, gotReal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMultipleAllocations(t *testing.T) {
|
||||
manager := NewManager()
|
||||
|
||||
allocations := make(map[netip.Addr]netip.Addr)
|
||||
|
||||
// Allocate multiple IPs
|
||||
for i := 1; i <= 100; i++ {
|
||||
realIP := netip.AddrFrom4([4]byte{10, 0, byte(i / 256), byte(i % 256)})
|
||||
fakeIP, err := manager.AllocateFakeIP(realIP)
|
||||
@@ -100,7 +133,6 @@ func TestMultipleAllocations(t *testing.T) {
|
||||
t.Fatalf("Failed to allocate fake IP for %s: %v", realIP.String(), err)
|
||||
}
|
||||
|
||||
// Check for duplicates
|
||||
for _, existingFake := range allocations {
|
||||
if fakeIP.Compare(existingFake) == 0 {
|
||||
t.Errorf("Duplicate fake IP allocated: %s", fakeIP.String())
|
||||
@@ -110,7 +142,6 @@ func TestMultipleAllocations(t *testing.T) {
|
||||
allocations[realIP] = fakeIP
|
||||
}
|
||||
|
||||
// Verify all allocations can be retrieved
|
||||
for realIP, expectedFake := range allocations {
|
||||
actualFake, exists := manager.GetFakeIP(realIP)
|
||||
if !exists {
|
||||
@@ -124,11 +155,13 @@ func TestMultipleAllocations(t *testing.T) {
|
||||
|
||||
func TestGetFakeIPBlock(t *testing.T) {
|
||||
manager := NewManager()
|
||||
block := manager.GetFakeIPBlock()
|
||||
|
||||
expected := "240.0.0.0/8"
|
||||
if block.String() != expected {
|
||||
t.Errorf("Expected %s, got %s", expected, block.String())
|
||||
if block := manager.GetFakeIPBlock(); block.String() != "240.0.0.0/8" {
|
||||
t.Errorf("Expected 240.0.0.0/8, got %s", block.String())
|
||||
}
|
||||
|
||||
if block := manager.GetFakeIPv6Block(); block.String() != "100::/64" {
|
||||
t.Errorf("Expected 100::/64, got %s", block.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,7 +174,6 @@ func TestConcurrentAccess(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
results := make(chan netip.Addr, numGoroutines*allocationsPerGoroutine)
|
||||
|
||||
// Concurrent allocations
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(goroutineID int) {
|
||||
@@ -161,7 +193,6 @@ func TestConcurrentAccess(t *testing.T) {
|
||||
wg.Wait()
|
||||
close(results)
|
||||
|
||||
// Check for duplicates
|
||||
seen := make(map[netip.Addr]bool)
|
||||
count := 0
|
||||
for fakeIP := range results {
|
||||
@@ -178,47 +209,61 @@ func TestConcurrentAccess(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIPExhaustion(t *testing.T) {
|
||||
// Create a manager with limited range for testing
|
||||
manager := &Manager{
|
||||
nextIP: netip.AddrFrom4([4]byte{240, 0, 0, 1}),
|
||||
allocated: make(map[netip.Addr]netip.Addr),
|
||||
fakeToReal: make(map[netip.Addr]netip.Addr),
|
||||
baseIP: netip.AddrFrom4([4]byte{240, 0, 0, 1}),
|
||||
maxIP: netip.AddrFrom4([4]byte{240, 0, 0, 3}), // Only 3 IPs available
|
||||
v4: newPool(
|
||||
netip.AddrFrom4([4]byte{240, 0, 0, 1}),
|
||||
netip.AddrFrom4([4]byte{240, 0, 0, 3}),
|
||||
netip.MustParsePrefix("240.0.0.0/8"),
|
||||
),
|
||||
v6: newPool(
|
||||
netip.MustParseAddr("100::1"),
|
||||
netip.MustParseAddr("100::3"),
|
||||
netip.MustParsePrefix("100::/64"),
|
||||
),
|
||||
}
|
||||
|
||||
// Allocate all available IPs
|
||||
realIPs := []netip.Addr{
|
||||
netip.MustParseAddr("1.0.0.1"),
|
||||
netip.MustParseAddr("1.0.0.2"),
|
||||
netip.MustParseAddr("1.0.0.3"),
|
||||
}
|
||||
|
||||
for _, realIP := range realIPs {
|
||||
_, err := manager.AllocateFakeIP(realIP)
|
||||
for _, realIP := range []string{"1.0.0.1", "1.0.0.2", "1.0.0.3"} {
|
||||
_, err := manager.AllocateFakeIP(netip.MustParseAddr(realIP))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate fake IP: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Try to allocate one more - should fail
|
||||
_, err := manager.AllocateFakeIP(netip.MustParseAddr("1.0.0.4"))
|
||||
if err == nil {
|
||||
t.Error("Expected exhaustion error")
|
||||
t.Error("Expected v4 exhaustion error")
|
||||
}
|
||||
|
||||
// Same for v6
|
||||
for _, realIP := range []string{"2001:db8::1", "2001:db8::2", "2001:db8::3"} {
|
||||
_, err := manager.AllocateFakeIP(netip.MustParseAddr(realIP))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate fake IPv6: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = manager.AllocateFakeIP(netip.MustParseAddr("2001:db8::4"))
|
||||
if err == nil {
|
||||
t.Error("Expected v6 exhaustion error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrapAround(t *testing.T) {
|
||||
// Create manager starting near the end of range
|
||||
manager := &Manager{
|
||||
nextIP: netip.AddrFrom4([4]byte{240, 0, 0, 254}),
|
||||
allocated: make(map[netip.Addr]netip.Addr),
|
||||
fakeToReal: make(map[netip.Addr]netip.Addr),
|
||||
baseIP: netip.AddrFrom4([4]byte{240, 0, 0, 1}),
|
||||
maxIP: netip.AddrFrom4([4]byte{240, 0, 0, 254}),
|
||||
v4: newPool(
|
||||
netip.AddrFrom4([4]byte{240, 0, 0, 1}),
|
||||
netip.AddrFrom4([4]byte{240, 0, 0, 254}),
|
||||
netip.MustParsePrefix("240.0.0.0/8"),
|
||||
),
|
||||
v6: newPool(
|
||||
netip.MustParseAddr("100::1"),
|
||||
netip.MustParseAddr("100::ffff:ffff:ffff:fffe"),
|
||||
netip.MustParsePrefix("100::/64"),
|
||||
),
|
||||
}
|
||||
// Start near the end
|
||||
manager.v4.nextIP = netip.AddrFrom4([4]byte{240, 0, 0, 254})
|
||||
|
||||
// Allocate the last IP
|
||||
fakeIP1, err := manager.AllocateFakeIP(netip.MustParseAddr("1.0.0.1"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate first IP: %v", err)
|
||||
@@ -228,7 +273,6 @@ func TestWrapAround(t *testing.T) {
|
||||
t.Errorf("Expected 240.0.0.254, got %s", fakeIP1.String())
|
||||
}
|
||||
|
||||
// Next allocation should wrap around to the beginning
|
||||
fakeIP2, err := manager.AllocateFakeIP(netip.MustParseAddr("1.0.0.2"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate second IP: %v", err)
|
||||
@@ -238,3 +282,32 @@ func TestWrapAround(t *testing.T) {
|
||||
t.Errorf("Expected 240.0.0.1 after wrap, got %s", fakeIP2.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMixedV4V6(t *testing.T) {
|
||||
manager := NewManager()
|
||||
|
||||
v4Fake, err := manager.AllocateFakeIP(netip.MustParseAddr("8.8.8.8"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate v4: %v", err)
|
||||
}
|
||||
|
||||
v6Fake, err := manager.AllocateFakeIP(netip.MustParseAddr("2001:db8::1"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to allocate v6: %v", err)
|
||||
}
|
||||
|
||||
if !v4Fake.Is4() || !v6Fake.Is6() {
|
||||
t.Errorf("Wrong families: v4=%s v6=%s", v4Fake, v6Fake)
|
||||
}
|
||||
|
||||
// Reverse lookups should work for both
|
||||
gotV4, ok := manager.GetRealIP(v4Fake)
|
||||
if !ok || gotV4.String() != "8.8.8.8" {
|
||||
t.Errorf("v4 reverse lookup failed: got %s, ok=%v", gotV4, ok)
|
||||
}
|
||||
|
||||
gotV6, ok := manager.GetRealIP(v6Fake)
|
||||
if !ok || gotV6.String() != "2001:db8::1" {
|
||||
t.Errorf("v6 reverse lookup failed: got %s, ok=%v", gotV6, ok)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,11 @@ import (
|
||||
)
|
||||
|
||||
// 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: 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.
|
||||
type IPForwardingState struct {
|
||||
enabledCounter int
|
||||
}
|
||||
|
||||
@@ -159,15 +159,23 @@ func (m *DefaultManager) setupAndroidRoutes(config ManagerConfig) {
|
||||
if config.DNSFeatureFlag {
|
||||
m.fakeIPManager = fakeip.NewManager()
|
||||
|
||||
id := uuid.NewString()
|
||||
fakeIPRoute := &route.Route{
|
||||
ID: route.ID(id),
|
||||
v4ID := uuid.NewString()
|
||||
cr = append(cr, &route.Route{
|
||||
ID: route.ID(v4ID),
|
||||
Network: m.fakeIPManager.GetFakeIPBlock(),
|
||||
NetID: route.NetID(id),
|
||||
NetID: route.NetID(v4ID),
|
||||
Peer: m.pubKey,
|
||||
NetworkType: route.IPv4Network,
|
||||
}
|
||||
cr = append(cr, fakeIPRoute)
|
||||
})
|
||||
|
||||
v6ID := uuid.NewString()
|
||||
cr = append(cr, &route.Route{
|
||||
ID: route.ID(v6ID),
|
||||
Network: m.fakeIPManager.GetFakeIPv6Block(),
|
||||
NetID: route.NetID(v6ID),
|
||||
Peer: m.pubKey,
|
||||
NetworkType: route.IPv6Network,
|
||||
})
|
||||
}
|
||||
|
||||
m.notifier.SetInitialClientRoutes(cr, routesForComparison)
|
||||
|
||||
@@ -146,8 +146,7 @@ func routeToRouterPair(route *route.Route, useNewDNSRoute bool) firewall.RouterP
|
||||
if useNewDNSRoute {
|
||||
destination.Set = firewall.NewDomainSet(route.Domains)
|
||||
} else {
|
||||
// TODO: add ipv6 additionally
|
||||
destination = getDefaultPrefix(destination.Prefix)
|
||||
destination = getDefaultPrefix(route.Network)
|
||||
}
|
||||
} else {
|
||||
destination.Prefix = route.Network.Masked()
|
||||
|
||||
@@ -107,8 +107,13 @@ func (r *SysOps) validateRoute(prefix netip.Prefix) error {
|
||||
addr.IsInterfaceLocalMulticast(),
|
||||
addr.IsMulticast(),
|
||||
addr.IsUnspecified() && prefix.Bits() != 0,
|
||||
r.wgInterface.Address().Network.Contains(addr):
|
||||
r.isOwnAddress(addr):
|
||||
return vars.ErrRouteNotAllowed
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SysOps) isOwnAddress(addr netip.Addr) bool {
|
||||
wgAddr := r.wgInterface.Address()
|
||||
return wgAddr.Network.Contains(addr) || (wgAddr.IPv6Net.IsValid() && wgAddr.IPv6Net.Contains(addr))
|
||||
}
|
||||
|
||||
@@ -222,30 +222,20 @@ func (r *SysOps) genericAddVPNRoute(prefix netip.Prefix, intf *net.Interface) er
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: remove once IPv6 is supported on the interface
|
||||
if err := r.addToRouteTable(splitDefaultv6_1, nextHop); err != nil {
|
||||
return fmt.Errorf("add unreachable route split 1: %w", err)
|
||||
}
|
||||
if err := r.addToRouteTable(splitDefaultv6_2, nextHop); err != nil {
|
||||
if err2 := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err2 != nil {
|
||||
log.Warnf("Failed to rollback route addition: %s", err2)
|
||||
// When the interface has no v6, add v6 split-default as blackhole so
|
||||
// unroutable v6 goes to WG (dropped, no AllowedIPs) instead of leaking
|
||||
// to the system default route. When v6 is active, management sends ::/0
|
||||
// as a separate route that the dedicated handler adds.
|
||||
// Soft-fail: v6 blackhole is best-effort, don't abort v4 routing on failure.
|
||||
if !r.wgInterface.Address().HasIPv6() {
|
||||
if err := r.addV6SplitDefault(nextHop); err != nil {
|
||||
log.Warnf("failed to add v6 split-default blackhole: %s", err)
|
||||
}
|
||||
return fmt.Errorf("add unreachable route split 2: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
case vars.Defaultv6:
|
||||
if err := r.addToRouteTable(splitDefaultv6_1, nextHop); err != nil {
|
||||
return fmt.Errorf("add unreachable route split 1: %w", err)
|
||||
}
|
||||
if err := r.addToRouteTable(splitDefaultv6_2, nextHop); err != nil {
|
||||
if err2 := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err2 != nil {
|
||||
log.Warnf("Failed to rollback route addition: %s", err2)
|
||||
}
|
||||
return fmt.Errorf("add unreachable route split 2: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return r.addV6SplitDefault(nextHop)
|
||||
}
|
||||
|
||||
return r.addToRouteTable(prefix, nextHop)
|
||||
@@ -266,30 +256,42 @@ func (r *SysOps) genericRemoveVPNRoute(prefix netip.Prefix, intf *net.Interface)
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
|
||||
// TODO: remove once IPv6 is supported on the interface
|
||||
if err := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
if err := r.removeFromRouteTable(splitDefaultv6_2, nextHop); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
if !r.wgInterface.Address().HasIPv6() {
|
||||
result = multierror.Append(result, r.removeV6SplitDefault(nextHop))
|
||||
}
|
||||
|
||||
return nberrors.FormatErrorOrNil(result)
|
||||
case vars.Defaultv6:
|
||||
var result *multierror.Error
|
||||
if err := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
if err := r.removeFromRouteTable(splitDefaultv6_2, nextHop); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
|
||||
return nberrors.FormatErrorOrNil(result)
|
||||
return nberrors.FormatErrorOrNil(r.removeV6SplitDefault(nextHop))
|
||||
default:
|
||||
return r.removeFromRouteTable(prefix, nextHop)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *SysOps) addV6SplitDefault(nextHop Nexthop) error {
|
||||
if err := r.addToRouteTable(splitDefaultv6_1, nextHop); err != nil {
|
||||
return fmt.Errorf("add split 1: %w", err)
|
||||
}
|
||||
if err := r.addToRouteTable(splitDefaultv6_2, nextHop); err != nil {
|
||||
if err2 := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err2 != nil {
|
||||
log.Warnf("Failed to rollback v6 split-default: %s", err2)
|
||||
}
|
||||
return fmt.Errorf("add split 2: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *SysOps) removeV6SplitDefault(nextHop Nexthop) *multierror.Error {
|
||||
var result *multierror.Error
|
||||
if err := r.removeFromRouteTable(splitDefaultv6_1, nextHop); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
if err := r.removeFromRouteTable(splitDefaultv6_2, nextHop); err != nil {
|
||||
result = multierror.Append(result, err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *SysOps) setupHooks(initAddresses []net.IP, stateManager *statemanager.Manager) error {
|
||||
beforeHook := func(connID hooks.ConnectionID, prefix netip.Prefix) error {
|
||||
if _, err := r.refCounter.IncrementWithID(string(connID), prefix, struct{}{}); err != nil {
|
||||
|
||||
@@ -53,6 +53,8 @@ const (
|
||||
|
||||
// ipv4ForwardingPath is the path to the file containing the IP forwarding setting.
|
||||
ipv4ForwardingPath = "net.ipv4.ip_forward"
|
||||
// ipv6ForwardingPath is the path to the file containing the IPv6 forwarding setting.
|
||||
ipv6ForwardingPath = "net.ipv6.conf.all.forwarding"
|
||||
)
|
||||
|
||||
var ErrTableIDExists = errors.New("ID exists with different name")
|
||||
@@ -185,10 +187,11 @@ func (r *SysOps) AddVPNRoute(prefix netip.Prefix, intf *net.Interface) error {
|
||||
|
||||
// No need to check if routes exist as main table takes precedence over the VPN table via Rule 1
|
||||
|
||||
// TODO remove this once we have ipv6 support
|
||||
if prefix == vars.Defaultv4 {
|
||||
// When the peer has no IPv6, blackhole v6 to prevent leaking.
|
||||
// When IPv6 is enabled, management sends ::/0 as a separate route.
|
||||
if prefix == vars.Defaultv4 && (r.wgInterface == nil || !r.wgInterface.Address().HasIPv6()) {
|
||||
if err := addUnreachableRoute(vars.Defaultv6, NetbirdVPNTableID); err != nil {
|
||||
return fmt.Errorf("add blackhole: %w", err)
|
||||
return fmt.Errorf("add v6 blackhole: %w", err)
|
||||
}
|
||||
}
|
||||
if err := addRoute(prefix, Nexthop{netip.Addr{}, intf}, NetbirdVPNTableID); err != nil {
|
||||
@@ -206,10 +209,9 @@ func (r *SysOps) RemoveVPNRoute(prefix netip.Prefix, intf *net.Interface) error
|
||||
return r.genericRemoveVPNRoute(prefix, intf)
|
||||
}
|
||||
|
||||
// TODO remove this once we have ipv6 support
|
||||
if prefix == vars.Defaultv4 {
|
||||
if prefix == vars.Defaultv4 && (r.wgInterface == nil || !r.wgInterface.Address().HasIPv6()) {
|
||||
if err := removeUnreachableRoute(vars.Defaultv6, NetbirdVPNTableID); err != nil {
|
||||
return fmt.Errorf("remove unreachable route: %w", err)
|
||||
log.Debugf("remove v6 blackhole: %v", err)
|
||||
}
|
||||
}
|
||||
if err := removeRoute(prefix, Nexthop{netip.Addr{}, intf}, NetbirdVPNTableID); err != nil {
|
||||
@@ -762,8 +764,13 @@ func flushRoutes(tableID, family int) error {
|
||||
}
|
||||
|
||||
func EnableIPForwarding() error {
|
||||
_, err := sysctl.Set(ipv4ForwardingPath, 1, false)
|
||||
return err
|
||||
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
|
||||
}
|
||||
|
||||
// entryExists checks if the specified ID or name already exists in the rt_tables file
|
||||
|
||||
Reference in New Issue
Block a user