mirror of
https://github.com/netbirdio/netbird.git
synced 2026-09-07 15:31:30 +02:00
[client] Assign the Android TUN address as a host prefix (#7414)
Android 16+ local network protection derives the blocked prefixes from the interface address prefix. A /16 address turns the whole overlay into a local network, so apps without ACCESS_LOCAL_NETWORK cannot reach any peer. Pass the address as /32 and /128 and add the overlay networks to the route list that the Android side turns into VPN routes, on both the initial create and the renew path.
This commit is contained in:
@@ -63,7 +63,12 @@ func (t *WGTunDevice) Create(routes []string, dns string, searchDomains []string
|
||||
searchDomainsToString = ""
|
||||
}
|
||||
|
||||
fd, err := t.tunAdapter.ConfigureInterface(t.address.String(), t.address.IPv6String(), int(t.mtu), dns, searchDomainsToString, routesString)
|
||||
ipv6Host := ""
|
||||
if t.address.HasIPv6() {
|
||||
ipv6Host = t.address.IPv6HostPrefix().String()
|
||||
}
|
||||
|
||||
fd, err := t.tunAdapter.ConfigureInterface(t.address.HostPrefix().String(), ipv6Host, int(t.mtu), dns, searchDomainsToString, routesString)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create Android interface: %s", err)
|
||||
return nil, err
|
||||
|
||||
@@ -59,6 +59,19 @@ func (addr Address) IPv6Prefix() netip.Prefix {
|
||||
return netip.PrefixFrom(addr.IPv6, addr.IPv6Net.Bits())
|
||||
}
|
||||
|
||||
// HostPrefix returns the v4 address as a single-host prefix.
|
||||
func (addr Address) HostPrefix() netip.Prefix {
|
||||
return netip.PrefixFrom(addr.IP, addr.IP.BitLen())
|
||||
}
|
||||
|
||||
// IPv6HostPrefix returns the v6 address as a single-host prefix, or an invalid prefix when no v6 overlay address is assigned.
|
||||
func (addr Address) IPv6HostPrefix() netip.Prefix {
|
||||
if !addr.HasIPv6() {
|
||||
return netip.Prefix{}
|
||||
}
|
||||
return netip.PrefixFrom(addr.IPv6, addr.IPv6.BitLen())
|
||||
}
|
||||
|
||||
// SetIPv6FromCompact decodes a compact prefix (5 or 17 bytes) and sets the IPv6 fields.
|
||||
// Returns an error if the bytes are invalid. A nil or empty input is a no-op.
|
||||
//
|
||||
|
||||
24
client/iface/wgaddr/address_test.go
Normal file
24
client/iface/wgaddr/address_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package wgaddr
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAddress_HostPrefix(t *testing.T) {
|
||||
addr := MustParseWGAddress("100.91.96.107/16")
|
||||
|
||||
assert.Equal(t, netip.MustParsePrefix("100.91.96.107/32"), addr.HostPrefix(), "v4 host prefix must be a single host")
|
||||
assert.Equal(t, netip.MustParsePrefix("100.91.0.0/16"), addr.Network, "network must keep the overlay prefix length")
|
||||
assert.False(t, addr.IPv6HostPrefix().IsValid(), "no v6 overlay means no v6 host prefix")
|
||||
}
|
||||
|
||||
func TestAddress_IPv6HostPrefix(t *testing.T) {
|
||||
addr := MustParseWGAddress("100.91.96.107/16")
|
||||
addr.IPv6 = netip.MustParseAddr("fd00:1234::1")
|
||||
addr.IPv6Net = netip.MustParsePrefix("fd00:1234::/64")
|
||||
|
||||
assert.Equal(t, netip.MustParsePrefix("fd00:1234::1/128"), addr.IPv6HostPrefix(), "v6 host prefix must be a single host")
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -472,27 +473,13 @@ func (m *DefaultManager) CurrentRouteRange() []string {
|
||||
m.mux.Lock()
|
||||
defer m.mux.Unlock()
|
||||
|
||||
if m.disableClientRoutes {
|
||||
return nil
|
||||
}
|
||||
|
||||
filtered := m.routeSelector.FilterSelectedExitNodes(m.clientRoutes)
|
||||
var nets []string
|
||||
for _, routes := range filtered {
|
||||
for _, r := range routes {
|
||||
if r.IsDynamic() {
|
||||
continue
|
||||
}
|
||||
nets = append(nets, r.NetString())
|
||||
}
|
||||
}
|
||||
|
||||
if m.fakeIPManager != nil {
|
||||
nets = append(nets, m.fakeIPManager.GetFakeIPBlock().String(), m.fakeIPManager.GetFakeIPv6Block().String())
|
||||
nets := m.overlayNetworks()
|
||||
if !m.disableClientRoutes {
|
||||
nets = append(nets, m.clientRouteRange()...)
|
||||
}
|
||||
|
||||
sort.Strings(nets)
|
||||
return nets
|
||||
return slices.Compact(nets)
|
||||
}
|
||||
|
||||
// GetRouteSelector returns the route selector
|
||||
@@ -856,6 +843,42 @@ func (m *DefaultManager) logExitNodeUpdate(info exitNodeInfo, preferred route.Ne
|
||||
len(info.allIDs), preferred, len(info.userSelected), len(info.userDeselected), len(info.selectedByManagement))
|
||||
}
|
||||
|
||||
// overlayNetworks returns the v4 and v6 overlay networks of the WireGuard interface, each only when it is set.
|
||||
func (m *DefaultManager) overlayNetworks() []string {
|
||||
if m.wgInterface == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
addr := m.wgInterface.Address()
|
||||
var nets []string
|
||||
if addr.Network.IsValid() {
|
||||
nets = append(nets, addr.Network.String())
|
||||
}
|
||||
if addr.IPv6Net.IsValid() {
|
||||
nets = append(nets, addr.IPv6Net.String())
|
||||
}
|
||||
return nets
|
||||
}
|
||||
|
||||
// clientRouteRange returns the static client route networks of the selected exit nodes together with the fake IP blocks.
|
||||
func (m *DefaultManager) clientRouteRange() []string {
|
||||
filtered := m.routeSelector.FilterSelectedExitNodes(m.clientRoutes)
|
||||
var nets []string
|
||||
for _, routes := range filtered {
|
||||
for _, r := range routes {
|
||||
if r.IsDynamic() {
|
||||
continue
|
||||
}
|
||||
nets = append(nets, r.NetString())
|
||||
}
|
||||
}
|
||||
|
||||
if m.fakeIPManager != nil {
|
||||
nets = append(nets, m.fakeIPManager.GetFakeIPBlock().String(), m.fakeIPManager.GetFakeIPv6Block().String())
|
||||
}
|
||||
return nets
|
||||
}
|
||||
|
||||
// minNetID returns the lexicographically smallest NetID, for a deterministic
|
||||
// default pick that stays stable across restarts.
|
||||
func minNetID(ids []route.NetID) route.NetID {
|
||||
|
||||
@@ -17,11 +17,12 @@ import (
|
||||
"github.com/netbirdio/netbird/client/internal/routemanager/refcounter"
|
||||
)
|
||||
|
||||
// reconcileWGMock is a minimal iface.WGIface that only records AddAllowedIP calls; every other
|
||||
// method is an inert stub because ReconcilePeerAllowedIPs exercises none of them.
|
||||
// reconcileWGMock is a minimal iface.WGIface that records AddAllowedIP calls and reports the
|
||||
// configured address; every other method is an inert stub because the tests exercise none of them.
|
||||
type reconcileWGMock struct {
|
||||
mu sync.Mutex
|
||||
adds map[string][]netip.Prefix
|
||||
addr wgaddr.Address
|
||||
}
|
||||
|
||||
func (m *reconcileWGMock) AddAllowedIP(peerKey string, allowedIP netip.Prefix) error {
|
||||
@@ -42,7 +43,7 @@ func (m *reconcileWGMock) added(peerKey string) []netip.Prefix {
|
||||
|
||||
func (m *reconcileWGMock) RemoveAllowedIP(string, netip.Prefix) error { return nil }
|
||||
func (m *reconcileWGMock) Name() string { return "utun-test" }
|
||||
func (m *reconcileWGMock) Address() wgaddr.Address { return wgaddr.Address{} }
|
||||
func (m *reconcileWGMock) Address() wgaddr.Address { return m.addr }
|
||||
func (m *reconcileWGMock) ToInterface() *net.Interface { return nil }
|
||||
func (m *reconcileWGMock) IsUserspaceBind() bool { return false }
|
||||
func (m *reconcileWGMock) GetFilter() device.PacketFilter { return nil }
|
||||
|
||||
95
client/internal/routemanager/route_range_test.go
Normal file
95
client/internal/routemanager/route_range_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
//go:build !windows
|
||||
|
||||
package routemanager
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/netbirdio/netbird/client/iface/wgaddr"
|
||||
"github.com/netbirdio/netbird/client/internal/routeselector"
|
||||
"github.com/netbirdio/netbird/route"
|
||||
)
|
||||
|
||||
func TestCurrentRouteRange_OverlayNetworkWithClientRoutesDisabled(t *testing.T) {
|
||||
m := &DefaultManager{
|
||||
wgInterface: &reconcileWGMock{addr: wgaddr.MustParseWGAddress("100.91.96.107/16")},
|
||||
disableClientRoutes: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"100.91.0.0/16"}, m.CurrentRouteRange(), "overlay network must be routed even when client routes are disabled")
|
||||
}
|
||||
|
||||
func TestCurrentRouteRange_OverlayNetworksAndClientRoutes(t *testing.T) {
|
||||
addr := wgaddr.MustParseWGAddress("100.91.96.107/16")
|
||||
addr.IPv6 = netip.MustParseAddr("fd00:1234::1")
|
||||
addr.IPv6Net = netip.MustParsePrefix("fd00:1234::/64")
|
||||
|
||||
static := &route.Route{ID: "static", NetID: "lan", Network: netip.MustParsePrefix("192.168.50.0/24"), NetworkType: route.IPv4Network}
|
||||
dynamic := &route.Route{ID: "dynamic", NetID: "dyn", NetworkType: route.DomainNetwork}
|
||||
|
||||
m := &DefaultManager{
|
||||
wgInterface: &reconcileWGMock{addr: addr},
|
||||
routeSelector: routeselector.NewRouteSelector(),
|
||||
clientRoutes: route.HAMap{
|
||||
static.GetHAUniqueID(): {static},
|
||||
dynamic.GetHAUniqueID(): {dynamic},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"100.91.0.0/16", "192.168.50.0/24", "fd00:1234::/64"}, m.CurrentRouteRange(), "overlay networks and static client routes must be listed, dynamic routes skipped")
|
||||
}
|
||||
|
||||
func TestCurrentRouteRange_NoInterfaceAddress(t *testing.T) {
|
||||
m := &DefaultManager{
|
||||
wgInterface: &reconcileWGMock{},
|
||||
disableClientRoutes: true,
|
||||
}
|
||||
|
||||
assert.Empty(t, m.CurrentRouteRange(), "an unset interface address must not produce a route entry")
|
||||
}
|
||||
|
||||
func TestCurrentRouteRange_IPv6WithoutIPv4Network(t *testing.T) {
|
||||
addr := wgaddr.Address{
|
||||
IPv6: netip.MustParseAddr("fd00:1234::1"),
|
||||
IPv6Net: netip.MustParsePrefix("fd00:1234::/64"),
|
||||
}
|
||||
m := &DefaultManager{
|
||||
wgInterface: &reconcileWGMock{addr: addr},
|
||||
disableClientRoutes: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"fd00:1234::/64"}, m.CurrentRouteRange(), "a v6 overlay network must not depend on a v4 network being set")
|
||||
}
|
||||
|
||||
func TestCurrentRouteRange_IPv6AddressWithoutNetwork(t *testing.T) {
|
||||
addr := wgaddr.MustParseWGAddress("100.91.96.107/16")
|
||||
addr.IPv6 = netip.MustParseAddr("fd00:1234::1")
|
||||
|
||||
m := &DefaultManager{
|
||||
wgInterface: &reconcileWGMock{addr: addr},
|
||||
disableClientRoutes: true,
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"100.91.0.0/16"}, m.CurrentRouteRange(), "a v6 address without a network must not produce a route entry")
|
||||
}
|
||||
|
||||
func TestCurrentRouteRange_DeduplicatesPrefixes(t *testing.T) {
|
||||
// Two HA peers serve the same prefix, and a client route announces the overlay network itself.
|
||||
haPeerA := &route.Route{ID: "ha-a", NetID: "lan", Peer: "peer-a", Network: netip.MustParsePrefix("192.168.50.0/24"), NetworkType: route.IPv4Network}
|
||||
haPeerB := &route.Route{ID: "ha-b", NetID: "lan", Peer: "peer-b", Network: netip.MustParsePrefix("192.168.50.0/24"), NetworkType: route.IPv4Network}
|
||||
overlay := &route.Route{ID: "overlay", NetID: "overlay", Network: netip.MustParsePrefix("100.91.0.0/16"), NetworkType: route.IPv4Network}
|
||||
|
||||
m := &DefaultManager{
|
||||
wgInterface: &reconcileWGMock{addr: wgaddr.MustParseWGAddress("100.91.96.107/16")},
|
||||
routeSelector: routeselector.NewRouteSelector(),
|
||||
clientRoutes: route.HAMap{
|
||||
haPeerA.GetHAUniqueID(): {haPeerA, haPeerB},
|
||||
overlay.GetHAUniqueID(): {overlay},
|
||||
},
|
||||
}
|
||||
|
||||
assert.Equal(t, []string{"100.91.0.0/16", "192.168.50.0/24"}, m.CurrentRouteRange(), "every prefix must be listed once regardless of how many routes carry it")
|
||||
}
|
||||
Reference in New Issue
Block a user