Skip to content

[client] Add IPv6 support to usersace bind#5147

Merged
lixmal merged 5 commits intomainfrom
feature/bind-ipv6-v2
Jan 22, 2026
Merged

[client] Add IPv6 support to usersace bind#5147
lixmal merged 5 commits intomainfrom
feature/bind-ipv6-v2

Conversation

@lixmal
Copy link
Copy Markdown
Collaborator

@lixmal lixmal commented Jan 21, 2026

Describe your changes

  • New DualStackPacketConn: Wraps separate IPv4 and IPv6 UDP connections, routing writes to the appropriate socket based on destination address
  • Updated ICEBind: Now creates and manages both IPv4 and IPv6 listeners, passing a dual-stack connection to the UDP mux
  • Successful path logging: Added debug logging to show all successful ICE candidate pair paths when connection is established

Example log output

  DEBG [peer: ZUqe... iceSessionID=abc123] successful ICE path: [udp4 host 192.168.100.166] <-> [udp4 srflx 192.168.100.202] rtt=3.475ms                                                                                            
  DEBG [peer: ZUqe... iceSessionID=abc123] successful ICE path: [udp6 host fd9c:4f71:5bc5::1f8] <-> [udp6 host fd9c:4f71:5bc5::2] rtt=1.515ms                

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features

    • Dual-stack (IPv4/IPv6) UDP support with correct routing, unified receiver flow, and IPv4-preferred local address selection.
    • ICE enhancements: dual-stack mux management, unified receiver creation, improved logging of successful ICE paths and connection details.
  • Tests

    • Added comprehensive unit tests and benchmarks covering dual-stack, mixed IPv4/IPv6 traffic, and performance scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Adds a DualStackPacketConn that multiplexes UDP writes between IPv4 and IPv6, integrates dual-stack tracking and unified receiver creation into ICEBind, and adds comprehensive tests and benchmarks for dual-stack behavior and performance. (47 words)

Changes

Cohort / File(s) Summary
Dual-stack connection wrapper
client/iface/bind/dual_stack_conn.go
New exported DualStackPacketConn and NewDualStackPacketConn; implements WriteTo, ReadFrom (first-call warning), Close, LocalAddr, and deadline methods. Routes writes by address family, aggregates errors with multierror/nberrors, and defines package error vars.
Dual-stack tests & benchmarks
client/iface/bind/dual_stack_conn_test.go, client/iface/bind/dual_stack_conn_bench_test.go
Unit tests covering write routing (IPv4, IPv6, IPv4-mapped, loopback), missing-conn error cases, LocalAddr preference, ReadFrom warning; benchmarks comparing direct UDP vs DualStack under IPv4-only, IPv6-only, and dual-stack mixed traffic.
ICEBind dual-stack integration
client/iface/bind/ice_bind.go
Replaced IPv4-specific receiver path with unified createReceiverFn accepting a generic batch reader, added ipv4Conn/ipv6Conn fields, and createOrUpdateMux to build/refresh the UDP mux before processing. Adjusted STUN handling, receiver creation, and Close to clear new fields.
ICEBind integration tests
client/iface/bind/ice_bind_test.go
New tests for dual-stack receiver creation, IPv4-only/IPv6-only flows, simultaneous IPv4/IPv6 peer delivery, concurrent mixed traffic, and address-family detection; includes test helpers for setup and message pooling.
Minor refactors & addr handling
client/internal/peer/worker_ice.go, sharedsock/sock_linux.go
Use net.JoinHostPort/strconv for address construction and prefer IPv4 LocalAddr when available in sharedsock.

Sequence Diagram(s)

sequenceDiagram
    participant App as Application
    participant DSPC as DualStackPacketConn
    participant IPv4 as IPv4_UDPConn
    participant IPv6 as IPv6_UDPConn

    App->>DSPC: WriteTo(payload, addr)
    alt addr is IPv4
        DSPC->>IPv4: WriteTo(payload, addr)
        IPv4-->>DSPC: (n, err)
    else addr is IPv6
        alt IPv4 conn exists
            DSPC->>IPv4: WriteTo(payload, addr)
            IPv4-->>DSPC: error (type-mismatch)
            DSPC->>IPv6: WriteTo(payload, addr)
            IPv6-->>DSPC: (n, err)
        else
            DSPC->>IPv6: WriteTo(payload, addr)
            IPv6-->>DSPC: (n, err)
        end
    else invalid addr type
        DSPC-->>App: net.OpError (invalid addr type)
    end
    DSPC-->>App: (n, err)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • pappz
  • crn4

Poem

🐇 I hopped through stacks both v4 and v6,

I nudge each packet to its matching fix,
A mux in my pouch keeps families apart,
I close both doors but always do my part,
🥕🌐

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding IPv6 support to the userspace bind layer, which aligns with the primary objective of implementing dual-stack UDP support.
Description check ✅ Passed The pull request description covers key changes (DualStackPacketConn, ICEBind updates, logging), includes example log output, and completes required template fields including checklist items and documentation justification.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@client/iface/bind/ice_bind.go`:
- Around line 279-283: filterOutStunMessages currently reads s.udpMux without
synchronization and can race with createReceiverFn which sets s.udpMux under
s.muUDPMux; fix by acquiring s.muUDPMux when accessing s.udpMux in
filterOutStunMessages (or take a short-lived local copy while holding
s.muUDPMux), then release the lock before calling s.udpMux.HandleSTUNMessage so
the call is done without holding the mutex; reference s.udpMux, s.muUDPMux,
createReceiverFn, and filterOutStunMessages when making the change.
- Around line 237-265: In createOrUpdateMux (method on ICEBind) close the
previous UDP mux instance before overwriting s.udpMux: if s.udpMux != nil call
its Close() (e.g., s.udpMux.Close()) and handle/ignore the returned error as
appropriate, then assign the new udpmux from udpmux.NewUniversalUDPMuxDefault;
do this while holding the existing muUDPMux as the comment requires to ensure
safe replacement and avoid leaking resources from the previous
UniversalUDPMuxDefault.
🧹 Nitpick comments (1)
client/internal/peer/worker_ice.go (1)

332-338: Consider using net.JoinHostPort for consistency.

This function manually wraps IPv6 addresses in brackets before using fmt.Sprintf. Since lines 290-291 now use net.JoinHostPort (which handles bracketing automatically), consider applying the same pattern here for consistency.

♻️ Suggested refactor
 func (w *WorkerICE) punchRemoteWGPort(pair *ice.CandidatePair, remoteWgPort int) {
 	// wait local endpoint configuration
 	time.Sleep(time.Second)
-	addrString := pair.Remote.Address()
-	parsed, err := netip.ParseAddr(addrString)
-	if (err == nil) && (parsed.Is6()) {
-		addrString = fmt.Sprintf("[%s]", addrString)
-		//IPv6 Literals need to be wrapped in brackets for Resolve*Addr()
-	}
-	addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", addrString, remoteWgPort))
+	addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(pair.Remote.Address(), strconv.Itoa(remoteWgPort)))
 	if err != nil {
 		w.log.Warnf("got an error while resolving the udp address, err: %s", err)
 		return
 	}

Comment thread client/iface/bind/ice_bind.go
Comment thread client/iface/bind/ice_bind.go Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@client/iface/bind/ice_bind.go`:
- Around line 60-61: ipv4Conn and ipv6Conn can remain pointing at closed sockets
across Close/Open and later be used by createOrUpdateMux causing write errors;
fix by clearing both ipv4Conn and ipv6Conn (under the muUDPMux lock) when
closing or before re-opening receivers—e.g., set ipv4Conn = nil and ipv6Conn =
nil inside Close() (or at start of Open()) while holding muUDPMux so
createOrUpdateMux never sees stale closed UDPConns.

In `@client/internal/peer/worker_ice.go`:
- Around line 390-418: In WorkerICE.logSuccessfulPaths, avoid reading
w.sessionID without the muxAgent lock: acquire w.muxAgent.RLock(), copy
sessionID to a local variable, release the lock, and use that local variable in
the w.log.WithField call; do the same for the other similar logging block in
this file (the subsequent successful/failed-path log) so all accesses to
w.sessionID are guarded and races are eliminated.
♻️ Duplicate comments (2)
client/iface/bind/ice_bind.go (2)

237-265: Close the previous udpMux before replacing it.

createOrUpdateMux overwrites s.udpMux without closing the prior instance, which can leak goroutines and sockets. (Duplicate of prior review comment.)

🔧 Suggested fix
 func (s *ICEBind) createOrUpdateMux() {
 	var muxConn net.PacketConn
@@
-	s.udpMux = udpmux.NewUniversalUDPMuxDefault(
+	if s.udpMux != nil {
+		_ = s.udpMux.Close()
+	}
+	s.udpMux = udpmux.NewUniversalUDPMuxDefault(
 		udpmux.UniversalUDPMuxParams{
 			UDPConn:   muxConn,
 			Net:       s.transportNet,
 			FilterFn:  s.filterFn,
 			WGAddress: s.address,
 			MTU:       s.mtu,
 		},
 	)
 }

279-283: Synchronize udpMux access in STUN handler.

filterOutStunMessages reads s.udpMux without synchronization while other paths update it under muUDPMux, which can race. (Duplicate of prior review comment.)

🔧 Suggested fix
-	if s.udpMux != nil {
-		if muxErr := s.udpMux.HandleSTUNMessage(msg, addr); muxErr != nil {
+	s.muUDPMux.Lock()
+	mux := s.udpMux
+	s.muUDPMux.Unlock()
+	if mux != nil {
+		if muxErr := mux.HandleSTUNMessage(msg, addr); muxErr != nil {
 			log.Warnf("failed to handle STUN packet: %v", muxErr)
 		}
 	}
🧹 Nitpick comments (1)
client/iface/bind/dual_stack_conn_bench_test.go (1)

14-118: Optional: extract shared benchmark setup helper.

The ListenUDP + DualStackPacketConn setup repeats across benchmarks; a small helper would reduce duplication and keep IPv6 skip logic consistent as benchmarks evolve.

Comment thread client/iface/bind/ice_bind.go
Comment thread client/internal/peer/worker_ice.go
@sonarqubecloud
Copy link
Copy Markdown

@lixmal lixmal merged commit ee54827 into main Jan 22, 2026
38 checks passed
@lixmal lixmal deleted the feature/bind-ipv6-v2 branch January 22, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants