Skip to content

[client] Handle IPv6 candidates in userspace bind #4123

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
153 changes: 153 additions & 0 deletions client/iface/bind/dual_stack_conn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package bind

import (
"errors"
"net"
"time"

"github.com/hashicorp/go-multierror"

nberrors "github.com/netbirdio/netbird/client/errors"
)

var (
errNoIPv4Conn = errors.New("no IPv4 connection available")
errNoIPv6Conn = errors.New("no IPv6 connection available")
errInvalidAddr = errors.New("invalid address type")
)

// DualStackPacketConn is a composite PacketConn that can handle both IPv4 and IPv6
type DualStackPacketConn struct {
ipv4Conn net.PacketConn
ipv6Conn net.PacketConn
}

// NewDualStackPacketConn creates a new dual-stack packet connection
func NewDualStackPacketConn(ipv4Conn, ipv6Conn net.PacketConn) *DualStackPacketConn {
return &DualStackPacketConn{
ipv4Conn: ipv4Conn,
ipv6Conn: ipv6Conn,
}
}

// ReadFrom reads from both IPv4 and IPv6 connections
func (d *DualStackPacketConn) ReadFrom(b []byte) (n int, addr net.Addr, err error) {
// Prefer IPv4 if available
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we read from both?

if d.ipv4Conn != nil {
return d.ipv4Conn.ReadFrom(b)
}
if d.ipv6Conn != nil {
return d.ipv6Conn.ReadFrom(b)
}
return 0, nil, net.ErrClosed
}

// WriteTo writes to the appropriate connection based on the address type
func (d *DualStackPacketConn) WriteTo(b []byte, addr net.Addr) (n int, err error) {
udpAddr, ok := addr.(*net.UDPAddr)
if !ok {
return 0, &net.OpError{
Op: "write",
Net: "udp",
Addr: addr,
Err: errInvalidAddr,
}
}

if udpAddr.IP.To4() == nil {
if d.ipv6Conn != nil {
return d.ipv6Conn.WriteTo(b, addr)
}
return 0, &net.OpError{
Op: "write",
Net: "udp6",
Addr: addr,
Err: errNoIPv6Conn,
}
}

if d.ipv4Conn != nil {
return d.ipv4Conn.WriteTo(b, addr)
}
return 0, &net.OpError{
Op: "write",
Net: "udp4",
Addr: addr,
Err: errNoIPv4Conn,
}
}

// Close closes both connections
func (d *DualStackPacketConn) Close() error {
var result *multierror.Error
if d.ipv4Conn != nil {
if err := d.ipv4Conn.Close(); err != nil {
result = multierror.Append(result, err)
}
}
if d.ipv6Conn != nil {
if err := d.ipv6Conn.Close(); err != nil {
result = multierror.Append(result, err)
}
}
return nberrors.FormatErrorOrNil(result)
}

// LocalAddr returns the local address of the IPv4 connection (for compatibility)
func (d *DualStackPacketConn) LocalAddr() net.Addr {
if d.ipv4Conn != nil {
return d.ipv4Conn.LocalAddr()
}
if d.ipv6Conn != nil {
return d.ipv6Conn.LocalAddr()
}
return nil
}

// SetDeadline sets the deadline for both connections
func (d *DualStackPacketConn) SetDeadline(t time.Time) error {
var result *multierror.Error
if d.ipv4Conn != nil {
if err := d.ipv4Conn.SetDeadline(t); err != nil {
result = multierror.Append(result, err)
}
}
if d.ipv6Conn != nil {
if err := d.ipv6Conn.SetDeadline(t); err != nil {
result = multierror.Append(result, err)
}
}
return nberrors.FormatErrorOrNil(result)
}

// SetReadDeadline sets the read deadline for both connections
func (d *DualStackPacketConn) SetReadDeadline(t time.Time) error {
var result *multierror.Error
if d.ipv4Conn != nil {
if err := d.ipv4Conn.SetReadDeadline(t); err != nil {
result = multierror.Append(result, err)
}
}
if d.ipv6Conn != nil {
if err := d.ipv6Conn.SetReadDeadline(t); err != nil {
result = multierror.Append(result, err)
}
}
return nberrors.FormatErrorOrNil(result)
}

// SetWriteDeadline sets the write deadline for both connections
func (d *DualStackPacketConn) SetWriteDeadline(t time.Time) error {
var result *multierror.Error
if d.ipv4Conn != nil {
if err := d.ipv4Conn.SetWriteDeadline(t); err != nil {
result = multierror.Append(result, err)
}
}
if d.ipv6Conn != nil {
if err := d.ipv6Conn.SetWriteDeadline(t); err != nil {
result = multierror.Append(result, err)
}
}
return nberrors.FormatErrorOrNil(result)
}
77 changes: 56 additions & 21 deletions client/iface/bind/ice_bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package bind

import (
"encoding/binary"
"fmt"
"errors"
"net"
"net/netip"
"runtime"
Expand All @@ -11,7 +11,6 @@ import (
"github.com/pion/stun/v2"
"github.com/pion/transport/v3"
log "github.com/sirupsen/logrus"
"golang.org/x/net/ipv4"
"golang.org/x/net/ipv6"
wgConn "golang.zx2c4.com/wireguard/conn"

Expand All @@ -27,8 +26,8 @@ type receiverCreator struct {
iceBind *ICEBind
}

func (rc receiverCreator) CreateIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, rxOffload bool, msgPool *sync.Pool) wgConn.ReceiveFunc {
return rc.iceBind.createIPv4ReceiverFn(pc, conn, rxOffload, msgPool)
func (rc receiverCreator) CreateReceiverFn(pc wgConn.BatchReader, conn *net.UDPConn, rxOffload bool, msgPool *sync.Pool) wgConn.ReceiveFunc {
return rc.iceBind.createReceiverFn(pc, conn, rxOffload, msgPool)
}

// ICEBind is a bind implementation with two main features:
Expand All @@ -54,6 +53,8 @@ type ICEBind struct {

muUDPMux sync.Mutex
udpMux *UniversalUDPMuxDefault
ipv4Conn *net.UDPConn
ipv6Conn *net.UDPConn
address wgaddr.Address
activityRecorder *ActivityRecorder
}
Expand Down Expand Up @@ -111,11 +112,11 @@ func (s *ICEBind) ActivityRecorder() *ActivityRecorder {
func (s *ICEBind) GetICEMux() (*UniversalUDPMuxDefault, error) {
s.muUDPMux.Lock()
defer s.muUDPMux.Unlock()
if s.udpMux == nil {
return nil, fmt.Errorf("ICEBind has not been initialized yet")
}

return s.udpMux, nil
if s.udpMux != nil {
return s.udpMux, nil
}
return nil, errors.New("ICEBind has not been initialized yet")
}

func (b *ICEBind) SetEndpoint(fakeIP netip.Addr, conn net.Conn) {
Expand Down Expand Up @@ -147,18 +148,46 @@ func (b *ICEBind) Send(bufs [][]byte, ep wgConn.Endpoint) error {
return nil
}

func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, rxOffload bool, msgsPool *sync.Pool) wgConn.ReceiveFunc {
func (s *ICEBind) createReceiverFn(pc wgConn.BatchReader, conn *net.UDPConn, rxOffload bool, msgsPool *sync.Pool) wgConn.ReceiveFunc {
s.muUDPMux.Lock()
defer s.muUDPMux.Unlock()

s.udpMux = NewUniversalUDPMuxDefault(
UniversalUDPMuxParams{
UDPConn: conn,
Net: s.transportNet,
FilterFn: s.filterFn,
WGAddress: s.address,
},
)
localAddr, ok := conn.LocalAddr().(*net.UDPAddr)
if !ok {
log.Errorf("ICEBind: unexpected address type: %T", conn.LocalAddr())
return nil
}
isIPv6 := localAddr.IP.To4() == nil

if isIPv6 {
s.ipv6Conn = conn
} else {
s.ipv4Conn = conn
}

needsNewMux := s.udpMux == nil && (s.ipv4Conn != nil || s.ipv6Conn != nil)
needsUpgrade := s.udpMux != nil && s.ipv4Conn != nil && s.ipv6Conn != nil

if needsNewMux || needsUpgrade {
var iceMuxConn net.PacketConn
switch {
case s.ipv4Conn != nil && s.ipv6Conn != nil:
iceMuxConn = NewDualStackPacketConn(s.ipv4Conn, s.ipv6Conn)
case s.ipv4Conn != nil:
iceMuxConn = s.ipv4Conn
default:
iceMuxConn = s.ipv6Conn
}

s.udpMux = NewUniversalUDPMuxDefault(
UniversalUDPMuxParams{
UDPConn: iceMuxConn,
Net: s.transportNet,
FilterFn: s.filterFn,
WGAddress: s.address,
},
)
}
return func(bufs [][]byte, sizes []int, eps []wgConn.Endpoint) (n int, err error) {
msgs := getMessages(msgsPool)
for i := range bufs {
Expand Down Expand Up @@ -205,7 +234,12 @@ func (s *ICEBind) createIPv4ReceiverFn(pc *ipv4.PacketConn, conn *net.UDPConn, r
if sizes[i] == 0 {
continue
}
addrPort := msg.Addr.(*net.UDPAddr).AddrPort()
udpAddr, ok := msg.Addr.(*net.UDPAddr)
if !ok {
log.Errorf("ICEBind: unexpected address type: %T", msg.Addr)
continue
}
addrPort := udpAddr.AddrPort()

if isTransportPkg(msg.Buffers, msg.N) {
s.activityRecorder.record(addrPort)
Expand All @@ -231,9 +265,10 @@ func (s *ICEBind) filterOutStunMessages(buffers [][]byte, n int, addr net.Addr)
return true, err
}

muxErr := s.udpMux.HandleSTUNMessage(msg, addr)
if muxErr != nil {
log.Warnf("failed to handle STUN packet")
if s.udpMux != nil {
if err := s.udpMux.HandleSTUNMessage(msg, addr); err != nil {
log.Warnf("failed to handle STUN packet: %v", err)
}
}

buffers[i] = []byte{}
Expand Down
3 changes: 3 additions & 0 deletions client/iface/bind/udp_mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,9 @@ func (m *UDPMuxDefault) Close() error {
}

func (m *UDPMuxDefault) writeTo(buf []byte, rAddr net.Addr) (n int, err error) {
if dualStackConn, ok := m.params.UDPConn.(*DualStackPacketConn); ok {
return dualStackConn.WriteTo(buf, rAddr)
}
return m.params.UDPConn.WriteTo(buf, rAddr)
}

Expand Down
15 changes: 15 additions & 0 deletions client/iface/bind/udp_mux_universal.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@ type udpConn struct {
}

func (u *udpConn) WriteTo(b []byte, addr net.Addr) (int, error) {
// Check if this is a dual-stack connection and handle IPv6 addresses properly
if dualStackConn, ok := u.PacketConn.(*DualStackPacketConn); ok {
return dualStackConn.WriteTo(b, addr)
}

Comment on lines 128 to +133
Copy link
Preview

Copilot AI Jul 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The dual-stack type assertion and WriteTo logic is duplicated in WriteTo, handleCachedAddress, and handleUncachedAddress. Consider extracting a helper to reduce duplication.

Suggested change
func (u *udpConn) WriteTo(b []byte, addr net.Addr) (int, error) {
// Check if this is a dual-stack connection and handle IPv6 addresses properly
if dualStackConn, ok := u.PacketConn.(*DualStackPacketConn); ok {
return dualStackConn.WriteTo(b, addr)
}
func (u *udpConn) writeToDualStack(b []byte, addr net.Addr) (int, error) {
if dualStackConn, ok := u.PacketConn.(*DualStackPacketConn); ok {
return dualStackConn.WriteTo(b, addr)
}
return 0, nil
}
func (u *udpConn) WriteTo(b []byte, addr net.Addr) (int, error) {
// Use helper to handle dual-stack connections
if n, err := u.writeToDualStack(b, addr); err != nil || n > 0 {
return n, err
}

Copilot uses AI. Check for mistakes.

if u.filterFn == nil {
return u.PacketConn.WriteTo(b, addr)
}
Expand All @@ -141,13 +146,23 @@ func (u *udpConn) handleCachedAddress(isRouted bool, b []byte, addr net.Addr) (i
if isRouted {
return 0, fmt.Errorf("address %s is part of a routed network, refusing to write", addr)
}

if dualStackConn, ok := u.PacketConn.(*DualStackPacketConn); ok {
return dualStackConn.WriteTo(b, addr)
}

return u.PacketConn.WriteTo(b, addr)
}

func (u *udpConn) handleUncachedAddress(b []byte, addr net.Addr) (int, error) {
if err := u.performFilterCheck(addr); err != nil {
return 0, err
}

if dualStackConn, ok := u.PacketConn.(*DualStackPacketConn); ok {
return dualStackConn.WriteTo(b, addr)
}

return u.PacketConn.WriteTo(b, addr)
}

Expand Down
13 changes: 11 additions & 2 deletions client/internal/peer/worker_ice.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ func (w *WorkerICE) OnNewOffer(remoteOfferAnswer *OfferAnswer) {
RosenpassAddr: remoteOfferAnswer.RosenpassAddr,
LocalIceCandidateType: pair.Local.Type().String(),
RemoteIceCandidateType: pair.Remote.Type().String(),
LocalIceCandidateEndpoint: fmt.Sprintf("%s:%d", pair.Local.Address(), pair.Local.Port()),
RemoteIceCandidateEndpoint: fmt.Sprintf("%s:%d", pair.Remote.Address(), pair.Remote.Port()),
LocalIceCandidateEndpoint: formatEndpoint(pair.Local.Address(), pair.Local.Port()),
RemoteIceCandidateEndpoint: formatEndpoint(pair.Remote.Address(), pair.Remote.Port()),
Relayed: isRelayed(pair),
RelayedOnLocal: isRelayCandidate(pair.Local),
}
Expand Down Expand Up @@ -405,3 +405,12 @@ func selectedPriority(pair *ice.CandidatePair) conntype.ConnPriority {
return conntype.ICEP2P
}
}

// formatEndpoint formats an IP address and port for display, adding brackets around IPv6 addresses
func formatEndpoint(addr string, port int) string {
parsed, err := netip.ParseAddr(addr)
if err == nil && parsed.Is6() {
return fmt.Sprintf("[%s]:%d", addr, port)
}
return fmt.Sprintf("%s:%d", addr, port)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-2024

replace github.com/getlantern/systray => github.com/netbirdio/systray v0.0.0-20231030152038-ef1ed2a27949

replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6
replace golang.zx2c4.com/wireguard => github.com/netbirdio/wireguard-go v0.0.0-20250709101833-3247b6066880

replace github.com/cloudflare/circl => github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -509,8 +509,8 @@ github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9ax
github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb h1:Cr6age+ePALqlSvtp7wc6lYY97XN7rkD1K4XEDmY+TU=
github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250514131221-a464fd5f30cb/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ=
github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6 h1:X5h5QgP7uHAv78FWgHV8+WYLjHxK9v3ilkVXT1cpCrQ=
github.com/netbirdio/wireguard-go v0.0.0-20241230120307-6a676aebaaf6/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
github.com/netbirdio/wireguard-go v0.0.0-20250709101833-3247b6066880 h1:s4y7B+jGbOSCnBAyh+APS8QSe8Liq+akU4enLHs8Efo=
github.com/netbirdio/wireguard-go v0.0.0-20250709101833-3247b6066880/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
Expand Down
Loading
Loading