Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b852ce1
Add IPv6 overlay address support to client interface and engine
lixmal Mar 24, 2026
1a7e835
Fix CodeRabbit findings: hasIPv6Changed restart loop, empty peerIPs p…
lixmal Mar 24, 2026
878dc45
Fix govet non-constant format string in log.Warnf
lixmal Mar 24, 2026
71962f8
Add IPv6 reverse DNS and host configurator support
lixmal Mar 24, 2026
d81cd5d
Add IPv6 support to SSH server, client config, and netflow logger
lixmal Mar 24, 2026
3be5a5f
Fix CodeRabbit findings: hasIPv6Changed restart loop, empty peerIPs p…
lixmal Mar 24, 2026
5fcea07
Merge branch 'client-ipv6-iface' into client-ipv6-dns
lixmal Mar 25, 2026
5a29fa8
Merge branch 'client-ipv6-dns' into client-ipv6-ssh-netflow
lixmal Mar 25, 2026
baf2c03
Fix CodeRabbit findings: hasIPv6Changed restart loop, empty peerIPs p…
lixmal Mar 24, 2026
641e386
Merge branch 'client-ipv6-iface' into client-ipv6-dns
lixmal Mar 25, 2026
0c2fbd5
Merge branch 'client-ipv6-dns' into client-ipv6-ssh-netflow
lixmal Mar 25, 2026
50c0bc5
Fix connect.go lint: use SetIPv6FromCompact instead of if-else chain
lixmal Mar 25, 2026
bc6ed1a
Merge branch 'client-ipv6-dns' into client-ipv6-ssh-netflow
lixmal Mar 25, 2026
6acc6a1
Merge branch 'proto-ipv6-overlay' into client-ipv6-iface
lixmal Mar 26, 2026
1a6cf9d
Merge branch 'client-ipv6-iface' into client-ipv6-dns
lixmal Mar 26, 2026
1cc19e7
Merge branch 'client-ipv6-dns' into client-ipv6-ssh-netflow
lixmal Mar 26, 2026
daeb90c
Merge pull request #5687 from netbirdio/client-ipv6-ssh-netflow
lixmal Apr 7, 2026
ed646f5
Merge pull request #5686 from netbirdio/client-ipv6-dns
lixmal Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/wasm-build-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ jobs:

echo "Size: ${SIZE} bytes (${SIZE_MB} MB)"

if [ ${SIZE} -gt 57671680 ]; then
echo "Wasm binary size (${SIZE_MB}MB) exceeds 55MB limit!"
if [ ${SIZE} -gt 58720256 ]; then
echo "Wasm binary size (${SIZE_MB}MB) exceeds 56MB limit!"
exit 1
fi

9 changes: 5 additions & 4 deletions client/android/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,11 @@ func (c *Client) PeersList() *PeerInfoArray {
peerInfos := make([]PeerInfo, len(fullStatus.Peers))
for n, p := range fullStatus.Peers {
pi := PeerInfo{
p.IP,
p.FQDN,
p.ConnStatus.String(),
PeerRoutes{routes: maps.Keys(p.GetRoutes())},
IP: p.IP,
IPv6: p.IPv6,
FQDN: p.FQDN,
ConnStatus: p.ConnStatus.String(),
Routes: PeerRoutes{routes: maps.Keys(p.GetRoutes())},
}
peerInfos[n] = pi
}
Expand Down
1 change: 1 addition & 0 deletions client/android/peer_notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package android
// PeerInfo describe information about the peers. It designed for the UI usage
type PeerInfo struct {
IP string
IPv6 string
FQDN string
ConnStatus string // Todo replace to enum
Routes PeerRoutes
Expand Down
18 changes: 18 additions & 0 deletions client/android/preferences.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,24 @@ func (p *Preferences) SetBlockInbound(block bool) {
p.configInput.BlockInbound = &block
}

// GetDisableIPv6 reads disable IPv6 setting from config file
func (p *Preferences) GetDisableIPv6() (bool, error) {
if p.configInput.DisableIPv6 != nil {
return *p.configInput.DisableIPv6, nil
}

cfg, err := profilemanager.ReadConfig(p.configInput.ConfigPath)
if err != nil {
return false, err
}
return cfg.DisableIPv6, err
}

// SetDisableIPv6 stores the given value and waits for commit
func (p *Preferences) SetDisableIPv6(disable bool) {
p.configInput.DisableIPv6 = &disable
}

// Commit writes out the changes to the config file
func (p *Preferences) Commit() error {
_, err := profilemanager.UpdateOrCreateConfig(p.configInput)
Expand Down
14 changes: 12 additions & 2 deletions client/cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
var (
detailFlag bool
ipv4Flag bool
ipv6Flag bool
jsonFlag bool
yamlFlag bool
ipsFilter []string
Expand All @@ -45,8 +46,9 @@ func init() {
statusCmd.PersistentFlags().BoolVar(&jsonFlag, "json", false, "display detailed status information in json format")
statusCmd.PersistentFlags().BoolVar(&yamlFlag, "yaml", false, "display detailed status information in yaml format")
statusCmd.PersistentFlags().BoolVar(&ipv4Flag, "ipv4", false, "display only NetBird IPv4 of this peer, e.g., --ipv4 will output 100.64.0.33")
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4")
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs, e.g., --filter-by-ips 100.64.0.100,100.64.0.200")
statusCmd.PersistentFlags().BoolVar(&ipv6Flag, "ipv6", false, "display only NetBird IPv6 of this peer")
statusCmd.MarkFlagsMutuallyExclusive("detail", "json", "yaml", "ipv4", "ipv6")
statusCmd.PersistentFlags().StringSliceVar(&ipsFilter, "filter-by-ips", []string{}, "filters the detailed output by a list of one or more IPs (v4 or v6), e.g., --filter-by-ips 100.64.0.100,fd00::1")
statusCmd.PersistentFlags().StringSliceVar(&prefixNamesFilter, "filter-by-names", []string{}, "filters the detailed output by a list of one or more peer FQDN or hostnames, e.g., --filter-by-names peer-a,peer-b.netbird.cloud")
statusCmd.PersistentFlags().StringVar(&statusFilter, "filter-by-status", "", "filters the detailed output by connection status(idle|connecting|connected), e.g., --filter-by-status connected")
statusCmd.PersistentFlags().StringVar(&connectionTypeFilter, "filter-by-connection-type", "", "filters the detailed output by connection type (P2P|Relayed), e.g., --filter-by-connection-type P2P")
Expand Down Expand Up @@ -101,6 +103,14 @@ func statusFunc(cmd *cobra.Command, args []string) error {
return nil
}

if ipv6Flag {
ipv6 := resp.GetFullStatus().GetLocalPeerState().GetIpv6()
if ipv6 != "" {
cmd.Print(parseInterfaceIP(ipv6))
}
return nil
}

pm := profilemanager.NewProfileManager()
var profName string
if activeProf, err := pm.GetActiveProfile(); err == nil {
Expand Down
5 changes: 5 additions & 0 deletions client/cmd/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const (
disableFirewallFlag = "disable-firewall"
blockLANAccessFlag = "block-lan-access"
blockInboundFlag = "block-inbound"
disableIPv6Flag = "disable-ipv6"
)

var (
Expand All @@ -17,6 +18,7 @@ var (
disableFirewall bool
blockLANAccess bool
blockInbound bool
disableIPv6 bool
)

func init() {
Expand All @@ -39,4 +41,7 @@ func init() {
upCmd.PersistentFlags().BoolVar(&blockInbound, blockInboundFlag, false,
"Block inbound connections. If enabled, the client will not allow any inbound connections to the local machine nor routed networks.\n"+
"This overrides any policies received from the management service.")

upCmd.PersistentFlags().BoolVar(&disableIPv6, disableIPv6Flag, false,
"Disable IPv6 overlay. If enabled, the client won't request or use an IPv6 overlay address.")
}
12 changes: 12 additions & 0 deletions client/cmd/up.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,10 @@ func setupSetConfigReq(customDNSAddressConverted []byte, cmd *cobra.Command, pro
req.BlockInbound = &blockInbound
}

if cmd.Flag(disableIPv6Flag).Changed {
req.DisableIpv6 = &disableIPv6
}

if cmd.Flag(enableLazyConnectionFlag).Changed {
req.LazyConnectionEnabled = &lazyConnEnabled
}
Expand Down Expand Up @@ -547,6 +551,10 @@ func setupConfig(customDNSAddressConverted []byte, cmd *cobra.Command, configFil
ic.BlockInbound = &blockInbound
}

if cmd.Flag(disableIPv6Flag).Changed {
ic.DisableIPv6 = &disableIPv6
}

if cmd.Flag(enableLazyConnectionFlag).Changed {
ic.LazyConnectionEnabled = &lazyConnEnabled
}
Expand Down Expand Up @@ -661,6 +669,10 @@ func setupLoginRequest(providedSetupKey string, customDNSAddressConverted []byte
loginRequest.BlockInbound = &blockInbound
}

if cmd.Flag(disableIPv6Flag).Changed {
loginRequest.DisableIpv6 = &disableIPv6
}

if cmd.Flag(enableLazyConnectionFlag).Changed {
loginRequest.LazyConnectionEnabled = &lazyConnEnabled
}
Expand Down
3 changes: 3 additions & 0 deletions client/embed/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ type Options struct {
StatePath string
// DisableClientRoutes disables the client routes
DisableClientRoutes bool
// DisableIPv6 disables IPv6 overlay addressing
DisableIPv6 bool
// BlockInbound blocks all inbound connections from peers
BlockInbound bool
// WireguardPort is the port for the WireGuard interface. Use 0 for a random port.
Expand Down Expand Up @@ -170,6 +172,7 @@ func New(opts Options) (*Client, error) {
PreSharedKey: &opts.PreSharedKey,
DisableServerRoutes: &t,
DisableClientRoutes: &opts.DisableClientRoutes,
DisableIPv6: &opts.DisableIPv6,
BlockInbound: &opts.BlockInbound,
WireguardPort: opts.WireguardPort,
MTU: opts.MTU,
Expand Down
33 changes: 21 additions & 12 deletions client/iface/device/device_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,23 +131,32 @@ func (t *TunDevice) Device() *device.Device {

// assignAddr Adds IP address to the tunnel interface and network route based on the range provided
func (t *TunDevice) assignAddr() error {
cmd := exec.Command("ifconfig", t.name, "inet", t.address.IP.String(), t.address.IP.String())
if out, err := cmd.CombinedOutput(); err != nil {
log.Errorf("adding address command '%v' failed with output: %s", cmd.String(), out)
return err
if out, err := exec.Command("ifconfig", t.name, "inet", t.address.IP.String(), t.address.IP.String()).CombinedOutput(); err != nil {
return fmt.Errorf("add v4 address: %s: %w", string(out), err)
}

// dummy ipv6 so routing works
cmd = exec.Command("ifconfig", t.name, "inet6", "fe80::/64")
if out, err := cmd.CombinedOutput(); err != nil {
log.Debugf("adding address command '%v' failed with output: %s", cmd.String(), out)
// Assign a dummy link-local so macOS enables IPv6 on the tun device.
// When a real overlay v6 is present, use that instead.
v6Addr := "fe80::/64"
if t.address.HasIPv6() {
v6Addr = t.address.IPv6String()
}
if out, err := exec.Command("ifconfig", t.name, "inet6", v6Addr).CombinedOutput(); err != nil {
log.Warnf("failed to assign IPv6 address %s, continuing v4-only: %s: %v", v6Addr, string(out), err)
t.address.ClearIPv6()
}

if out, err := exec.Command("route", "add", "-net", t.address.Network.String(), "-interface", t.name).CombinedOutput(); err != nil {
return fmt.Errorf("add route %s via %s: %s: %w", t.address.Network, t.name, string(out), err)
}

routeCmd := exec.Command("route", "add", "-net", t.address.Network.String(), "-interface", t.name)
if out, err := routeCmd.CombinedOutput(); err != nil {
log.Errorf("adding route command '%v' failed with output: %s", routeCmd.String(), out)
return err
if t.address.HasIPv6() {
if out, err := exec.Command("route", "add", "-inet6", "-net", t.address.IPv6Net.String(), "-interface", t.name).CombinedOutput(); err != nil {
log.Warnf("failed to add route %s via %s, continuing v4-only: %s: %v", t.address.IPv6Net, t.name, string(out), err)
t.address.ClearIPv6()
}
}

return nil
}

Expand Down
7 changes: 5 additions & 2 deletions client/iface/device/device_ios.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,11 @@ func (t *TunDevice) MTU() uint16 {
return t.mtu
}

func (t *TunDevice) UpdateAddr(_ wgaddr.Address) error {
// todo implement
// UpdateAddr updates the device address. On iOS the tunnel is managed by the
// NetworkExtension, so we only store the new value. The extension picks up the
// change on the next tunnel reconfiguration.
func (t *TunDevice) UpdateAddr(addr wgaddr.Address) error {
t.address = addr
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion client/iface/device/device_kernel_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (t *TunKernelDevice) FilteredDevice() *FilteredDevice {

// assignAddr Adds IP address to the tunnel interface
func (t *TunKernelDevice) assignAddr() error {
return t.link.assignAddr(t.address)
return t.link.assignAddr(&t.address)
}

func (t *TunKernelDevice) GetNet() *netstack.Net {
Expand Down
9 changes: 7 additions & 2 deletions client/iface/device/device_netstack.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package device
import (
"errors"
"fmt"
"net/netip"

log "github.com/sirupsen/logrus"
"golang.zx2c4.com/wireguard/conn"
Expand Down Expand Up @@ -63,8 +64,12 @@ func (t *TunNetstackDevice) create() (WGConfigurer, error) {
return nil, fmt.Errorf("last ip: %w", err)
}

log.Debugf("netstack using address: %s", t.address.IP)
t.nsTun = nbnetstack.NewNetStackTun(t.listenAddress, t.address.IP, dnsAddr, int(t.mtu))
addresses := []netip.Addr{t.address.IP}
if t.address.HasIPv6() {
addresses = append(addresses, t.address.IPv6)
}
log.Debugf("netstack using addresses: %v", addresses)
t.nsTun = nbnetstack.NewNetStackTun(t.listenAddress, addresses, dnsAddr, int(t.mtu))
log.Debugf("netstack using dns address: %s", dnsAddr)
tunIface, net, err := t.nsTun.Create()
if err != nil {
Expand Down
32 changes: 16 additions & 16 deletions client/iface/device/device_usp_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import (
"github.com/netbirdio/netbird/client/iface/wgaddr"
)

type USPDevice struct {
type TunDevice struct {
name string
address wgaddr.Address
port int
Expand All @@ -30,10 +30,10 @@ type USPDevice struct {
configurer WGConfigurer
}

func NewUSPDevice(name string, address wgaddr.Address, port int, key string, mtu uint16, iceBind *bind.ICEBind) *USPDevice {
func NewTunDevice(name string, address wgaddr.Address, port int, key string, mtu uint16, iceBind *bind.ICEBind) *TunDevice {
log.Infof("using userspace bind mode")

return &USPDevice{
return &TunDevice{
name: name,
address: address,
port: port,
Expand All @@ -43,7 +43,7 @@ func NewUSPDevice(name string, address wgaddr.Address, port int, key string, mtu
}
}

func (t *USPDevice) Create() (WGConfigurer, error) {
func (t *TunDevice) Create() (WGConfigurer, error) {
log.Info("create tun interface")
tunIface, err := tun.CreateTUN(t.name, int(t.mtu))
if err != nil {
Expand Down Expand Up @@ -75,7 +75,7 @@ func (t *USPDevice) Create() (WGConfigurer, error) {
return t.configurer, nil
}

func (t *USPDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
func (t *TunDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
if t.device == nil {
return nil, fmt.Errorf("device is not ready yet")
}
Expand All @@ -95,12 +95,12 @@ func (t *USPDevice) Up() (*udpmux.UniversalUDPMuxDefault, error) {
return udpMux, nil
}

func (t *USPDevice) UpdateAddr(address wgaddr.Address) error {
func (t *TunDevice) UpdateAddr(address wgaddr.Address) error {
t.address = address
return t.assignAddr()
}

func (t *USPDevice) Close() error {
func (t *TunDevice) Close() error {
if t.configurer != nil {
t.configurer.Close()
}
Expand All @@ -115,39 +115,39 @@ func (t *USPDevice) Close() error {
return nil
}

func (t *USPDevice) WgAddress() wgaddr.Address {
func (t *TunDevice) WgAddress() wgaddr.Address {
return t.address
}

func (t *USPDevice) MTU() uint16 {
func (t *TunDevice) MTU() uint16 {
return t.mtu
}

func (t *USPDevice) DeviceName() string {
func (t *TunDevice) DeviceName() string {
return t.name
}

func (t *USPDevice) FilteredDevice() *FilteredDevice {
func (t *TunDevice) FilteredDevice() *FilteredDevice {
return t.filteredDevice
}

// Device returns the wireguard device
func (t *USPDevice) Device() *device.Device {
func (t *TunDevice) Device() *device.Device {
return t.device
}

// assignAddr Adds IP address to the tunnel interface
func (t *USPDevice) assignAddr() error {
func (t *TunDevice) assignAddr() error {
link := newWGLink(t.name)

return link.assignAddr(t.address)
return link.assignAddr(&t.address)
}

func (t *USPDevice) GetNet() *netstack.Net {
func (t *TunDevice) GetNet() *netstack.Net {
return nil
}

// GetICEBind returns the ICEBind instance
func (t *USPDevice) GetICEBind() EndpointManager {
func (t *TunDevice) GetICEBind() EndpointManager {
return t.iceBind
}
Loading
Loading