mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-03 15:46:38 +00:00
[client] Add IPv6 support to ACL manager, USP filter, and forwarder (#5688)
This commit is contained in:
@@ -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