Skip to content

Conversation

@ltlobaro
Copy link
Contributor

@ltlobaro ltlobaro commented Jan 20, 2026

This PR resolves sending packets to IPv4 adresses on an IPv6 dual-stack socket. It resolves #640 and is probably related to #581.

Summary by CodeRabbit

  • Bug Fixes

    • Improved UDP connection handling for IPv4/IPv6 compatibility, enabling better message transmission between different protocol versions.
  • Tests

    • Added test coverage for sending IPv4 traffic from IPv6 sockets to ensure cross-protocol compatibility.

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

This change fixes a UDP send failure on macOS by replacing conditional IPv4/IPv6 wrapper routing with a direct WriteMsgUDP call. Control messages are constructed conditionally—using ipv6.ControlMessage for IPv6 destinations and ipv4.ControlMessage for IPv4, or nil if no control message is provided.

Changes

Cohort / File(s) Change Summary
UDP Send Logic
net/connUDP.go
Replaced IPv4/IPv6 conditional routing logic with direct WriteMsgUDP(buffer, cmb, raddr) call; control message blob now built only when non-nil, selecting appropriate protocol version based on destination IP family
UDP Write Tests
net/connUDP_internal_test.go
Added test case "send to v4 from v6 socket" to verify UDP write from IPv6 socket to IPv4 address succeeds

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • PR #582: Addresses similar UDP send address-family issues in net/connUDP.go but uses an ipv4 packetConn wrapper approach instead of direct WriteMsgUDP call.

Suggested labels

bug

Suggested reviewers

  • jlevesy
  • Danielius1922

Poem

🐰 A socket sends, no more "invalid arg,"
From IPv6 down to IPv4 it'll march,
macOS rejoices, control messages flow,
Cross-family writes now steal the show! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: fixing the ability to send UDP packets to IPv4 addresses from an IPv6 dual-stack socket, matching the core objective addressed by the PR.
Linked Issues check ✅ Passed The PR changes directly address issue #640 by modifying the UDP WriteMsgUDP logic to properly handle control messages based on destination IP type, enabling IPv4 responses from IPv6 dual-stack sockets.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue #640: modifications to UDP connection writing logic and corresponding test additions are within scope.

✏️ 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.

@jkralik
Copy link
Member

jkralik commented Jan 21, 2026

Hi @ltlobaro . Thank you for your contributing. Pls there is an issue in the tests, discovered by CI on Ubuntu:

=== RUN   TestUDPConnWriteWithContext
    connUDP_internal_test.go:364: interface name:lo, flags: up|loopback|running
    connUDP_internal_test.go:364: interface name:eth0, flags: up|broadcast|multicast|running
=== RUN   TestUDPConnWriteWithContext/valid_-_udp4_network
=== RUN   TestUDPConnWriteWithContext/cancelled
=== RUN   TestUDPConnWriteWithContext/send_to_v4_from_v6_socket
    connUDP_internal_test.go:128: 
        	Error Trace:	/home/runner/work/go-coap/go-coap/net/connUDP_internal_test.go:128
        	Error:      	Received unexpected error:
        	            	write udp [::]:56872->10.1.0.251:2154: sendmsg: invalid argument
        	Test:       	TestUDPConnWriteWithContext/send_to_v4_from_v6_socket
=== RUN   TestUDPConnWriteWithContext/valid_-_udp_network
    connUDP_internal_test.go:128: 
        	Error Trace:	/home/runner/work/go-coap/go-coap/net/connUDP_internal_test.go:128
        	Error:      	Received unexpected error:
        	            	write udp [::]:38524->10.1.0.251:2154: sendmsg: invalid argument
        	Test:       	TestUDPConnWriteWithContext/valid_-_udp_network
--- FAIL: TestUDPConnWriteWithContext (0.00s)
    --- PASS: TestUDPConnWriteWithContext/valid_-_udp4_network (0.00s)
    --- PASS: TestUDPConnWriteWithContext/cancelled (0.00s)
    --- FAIL: TestUDPConnWriteWithContext/send_to_v4_from_v6_socket (0.00s)
    --- FAIL: TestUDPConnWriteWithContext/valid_-_udp_network (0.00s)

Pls could you look at it :). Thx

This commit ensures that the unit tests are actually using IPv6
when testing IPv6 functionality. However, this may add adtional
runtime requirements to the unit tests:

- The runtime needs a network interface which is not loopback,
  is up, and has multicast enabled
- The interface needs to have at least one routable, unicast IP
  address, either link-local or global.
@ltlobaro
Copy link
Contributor Author

Hi @jkralik, thank you for your time!

It turns out the tests and implementation are highly dependent on the underlying platform, i.e. linux or darwin.

I fixed a bug in the writeTo method: The control message is now marshalled depending on the destination IP address type, which no longer triggers the error.

Also, I changed the unit tests to actually use IPv6 when testing IPv6 functionality. Before, the getIfaceAddr method was happily returning an IPv4 address, even when the ipv4 argument was false. However, the unit tests now require a network interface with an IPv6 address to pass successfully.

Copy link

@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 `@net/connUDP_internal_test.go`:
- Around line 355-357: The filter in the test excludes link-local addresses but
the commit says link-local or global unicast should be allowed; update the
condition that currently checks ip.IsPrivate() and ip.IsGlobalUnicast() to also
accept link-local by including ip.IsLinkLocalUnicast() (i.e. only continue when
!ip.IsPrivate() && !ip.IsGlobalUnicast() && !ip.IsLinkLocalUnicast()), or
alternatively update the commit/test description to remove the "link-local"
claim—locate the check using ip.IsPrivate() and ip.IsGlobalUnicast() in
net/connUDP_internal_test.go and adjust it or the commit message accordingly.
- Around line 66-85: The dual-stack send logic was removed and must be restored
so IPv4 destinations use an IPv4 packet connection when the local socket is
IPv6; in the send path (where WriteMsgUDP is now invoked) detect when
!IsIPv6(raddr.IP) && c.packetConn.IsIPv6(), construct an ipv4 packet wrapper
(packetConnIPv4{packetConn: ipv4.NewPacketConn(c.connection)}) and call its
WriteTo(buffer, cm, raddr) instead of the direct WriteMsgUDP call to ensure the
correct IPv4 control message format is used.
🧹 Nitpick comments (1)
net/connUDP_internal_test.go (1)

30-32: Consider handling missing IPv6 addresses gracefully.

If the selected interface doesn't have an IPv6 address that passes the filter (private or global unicast), getIfaceAddr(t, iface, false) will fail. The subsequent tests relying on v6 would then also fail.

Consider skipping IPv6-specific tests when no suitable IPv6 address is available:

♻️ Suggested improvement
-	ifaceIpv6 := getIfaceAddr(t, iface, false)
-	v6, err := net.ResolveUDPAddr(udpNetwork, "["+ifaceIpv6.String()+"]:2154")
-	require.NoError(t, err)
+	ifaceIpv6, hasIPv6 := tryGetIfaceAddr(t, iface, false)
+	var v6 *net.UDPAddr
+	if hasIPv6 {
+		v6, err = net.ResolveUDPAddr(udpNetwork, "["+ifaceIpv6.String()+"]:2154")
+		require.NoError(t, err)
+	}

Then conditionally skip the IPv6 test cases when v6 == nil.

Comment on lines 66 to 85
{
name: "send to v4 from v6 socket",
args: args{
ctx: context.Background(),
listenNetwork: udpNetwork,
udpAddr: b,
buffer: []byte("hello world"),
},
wantErr: false,
},
{
name: "send to v6 from v6 socket",
args: args{
ctx: context.Background(),
listenNetwork: udpNetwork,
udpAddr: v6,
buffer: []byte("hello world"),
},
wantErr: false,
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the test file
fd -name "connUDP_internal_test.go" -type f

Repository: plgd-dev/go-coap

Length of output: 176


🏁 Script executed:

# Check for ListenUDP usage and dual-stack/V6ONLY handling in the repository
rg -n "ListenUDP|V6ONLY|IPV6_V6ONLY|dualstack" --type=go -B 2 -A 2

Repository: plgd-dev/go-coap

Length of output: 13570


🏁 Script executed:

# Let's also check the full context of the test file to understand the setup
fd -name "connUDP_internal_test.go" -type f -exec wc -l {} \;

Repository: plgd-dev/go-coap

Length of output: 290


🏁 Script executed:

# Find the test file
find . -name "connUDP_internal_test.go" -type f

Repository: plgd-dev/go-coap

Length of output: 90


🏁 Script executed:

# Search for V6ONLY and dualstack references using rg
rg "V6ONLY|IPV6_V6ONLY|dualstack" --type=go -B 2 -A 2

Repository: plgd-dev/go-coap

Length of output: 42


🏁 Script executed:

# Get the file and check its size
find . -name "connUDP_internal_test.go" -type f -exec wc -l {} \;

Repository: plgd-dev/go-coap

Length of output: 94


🏁 Script executed:

# Read the test file to see the test setup and lines 66-85
cat -n ./net/connUDP_internal_test.go | head -150

Repository: plgd-dev/go-coap

Length of output: 4423


🏁 Script executed:

# Search for udpNetwork variable definition
rg "udpNetwork\s*:?=" ./net/connUDP_internal_test.go -B 2 -A 2

Repository: plgd-dev/go-coap

Length of output: 133


🏁 Script executed:

# Also check for any socket option handling related to the tests
rg "SetIPv6Only|IPV6_V6ONLY|Control" ./net/connUDP_internal_test.go -B 2 -A 2

Repository: plgd-dev/go-coap

Length of output: 841


🏁 Script executed:

# Look at the connUDP.go implementation to see how the socket is set up
cat -n ./net/connUDP.go | head -250

Repository: plgd-dev/go-coap

Length of output: 8319


🏁 Script executed:

# Check what NewUDPConn does with the socket
rg -A 30 "func NewUDPConn" ./net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 921


🏁 Script executed:

# Check if there's any socket option setting for dual-stack
rg "SetIPv6Only|SO_REUSEADDR|Control|SyscallConn" ./net/connUDP.go -B 2 -A 5

Repository: plgd-dev/go-coap

Length of output: 6402


🏁 Script executed:

# Read more of connUDP.go to see how newPacketConn determines IPv4 vs IPv6
sed -n '245,300p' ./net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 1553


🏁 Script executed:

# Check if there's any handling of the "udp" network type specifically
rg "network.*udp|newPacketConn" ./net/connUDP.go -B 3 -A 10

Repository: plgd-dev/go-coap

Length of output: 3015


🏁 Script executed:

# Check if there's any socket option handling in the writeTo method
sed -n '380,450p' ./net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 2141


🏁 Script executed:

# Look for any SendMsgs or syscall-level handling
rg "sendmsg|SendMsg|sendto|Sendto|syscall" ./net/connUDP.go -B 3 -A 3

Repository: plgd-dev/go-coap

Length of output: 42


🏁 Script executed:

# Check the test to understand what error message we'd expect on failure
rg "sendmsg.*invalid" ./net/connUDP_internal_test.go

Repository: plgd-dev/go-coap

Length of output: 42


🏁 Script executed:

# Check for any recent fixes or workarounds related to dual-stack in git history
git log --oneline --all -20 | head -20

Repository: plgd-dev/go-coap

Length of output: 1218


🏁 Script executed:

# Look at the writeTo method more completely to see control message handling
sed -n '312,375p' ./net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 1644


🏁 Script executed:

# Check if there are any platform-specific files
find ./net -name "*linux*" -o -name "*darwin*" -o -name "*windows*"

Repository: plgd-dev/go-coap

Length of output: 82


🏁 Script executed:

# Check the recent commit that fixes control message passing
git show 4dd5869 --stat

Repository: plgd-dev/go-coap

Length of output: 11241


🏁 Script executed:

# Also check the Linux IPv4/IPv6 binding fix
git show 91a04ea --stat

Repository: plgd-dev/go-coap

Length of output: 838


🏁 Script executed:

# Look at the writeTo method specifically to see if control messages are correctly passed
sed -n '455,530p' ./net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 2263


🏁 Script executed:

# Look at the actual changes made in the recent fix commits
git show 91a04ea -- net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 1461


🏁 Script executed:

# Check the test file changes from that commit
git show 91a04ea -- net/connUDP_internal_test.go

Repository: plgd-dev/go-coap

Length of output: 4600


🏁 Script executed:

# Check the most recent commit that touched the tests
git show 4dd5869 -- net/connUDP_internal_test.go | head -200

Repository: plgd-dev/go-coap

Length of output: 5014


🏁 Script executed:

# Verify current state: are the dual-stack tests still present?
sed -n '48,105p' ./net/connUDP_internal_test.go

Repository: plgd-dev/go-coap

Length of output: 1326


🏁 Script executed:

# Check if the fix from commit 91a04ea is currently in the code
sed -n '514,530p' ./net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 497


🏁 Script executed:

# Look at the writeTo implementation to confirm the fix is present
rg -A 15 "func \(c \*UDPConn\) writeTo" ./net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 999


🏁 Script executed:

# Get the complete writeTo function
sed -n '520,570p' ./net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 1008


🏁 Script executed:

# Search for the specific fix that checks IsIPv6(raddr.IP) && c.packetConn.IsIPv6()
rg "IsIPv6.*raddr.*packetConn" ./net/connUDP.go -B 3 -A 10

Repository: plgd-dev/go-coap

Length of output: 42


🏁 Script executed:

# Let's check the exact current implementation to see if the fix is there or not
sed -n '513,545p' ./net/connUDP.go

Repository: plgd-dev/go-coap

Length of output: 844


🏁 Script executed:

# Check git log to see if the fix was reverted
git log --oneline --all -- net/connUDP.go | head -20

Repository: plgd-dev/go-coap

Length of output: 1068


🏁 Script executed:

# Check what the most recent commit (4dd5869) changed to writeTo
git show 4dd5869 -- net/connUDP.go | grep -A 50 "func (c \*UDPConn) writeTo"

Repository: plgd-dev/go-coap

Length of output: 2647


🏁 Script executed:

# Also check if the previous fix was reverted
git show 4dd5869:net/connUDP.go | sed -n '513,545p'

Repository: plgd-dev/go-coap

Length of output: 844


🏁 Script executed:

# Check if commit 91a04ea's fix is in the current HEAD
git show HEAD:net/connUDP.go | sed -n '513,540p'

Repository: plgd-dev/go-coap

Length of output: 748


🏁 Script executed:

# Compare the two implementations side by side - check if the IPv4 wrapping logic exists
git show 91a04ea:net/connUDP.go | sed -n '513,540p'

Repository: plgd-dev/go-coap

Length of output: 1115


🏁 Script executed:

# Check the differences between 91a04ea and 4dd5869 for writeTo
git diff 91a04ea 4dd5869 -- net/connUDP.go | grep -A 20 -B 5 "writeTo"

Repository: plgd-dev/go-coap

Length of output: 1010


🏁 Script executed:

# Check what changed in the entire writeTo function between these commits
git show 91a04ea:net/connUDP.go | sed -n '513,530p' > /tmp/old_writeTo.txt
git show 4dd5869:net/connUDP.go | sed -n '513,530p' > /tmp/new_writeTo.txt
diff -u /tmp/old_writeTo.txt /tmp/new_writeTo.txt

Repository: plgd-dev/go-coap

Length of output: 1068


These dual-stack test cases will fail on Linux and require protocol wrapping logic to be restored.

The tests attempt to send IPv4 and IPv6 packets from a dual-stack socket ("udp") with control messages. On Linux, when net.ListenUDP("udp", addr) creates a socket bound to [::], the code determines the protocol handler from the local address (IPv6), but then tries to send IPv4 packets with IPv6 control messages, causing sendmsg: invalid argument errors.

A fix for this was implemented in commit 91a04ea that wraps IPv4 destinations with an IPv4 packet connection when the socket is IPv6-based. However, this logic was removed in the most recent commit (4dd5869), which replaced it with direct WriteMsgUDP calls. The wrapping logic needs to be restored:

if !IsIPv6(raddr.IP) && c.packetConn.IsIPv6() {
    pc := packetConnIPv4{packetConn: ipv4.NewPacketConn(c.connection)}
    return pc.WriteTo(buffer, cm, raddr)
}

This ensures that IPv4 traffic sent from a dual-stack socket uses the appropriate IPv4 control message format rather than attempting IPv6 control messages for IPv4 destinations.

🤖 Prompt for AI Agents
In `@net/connUDP_internal_test.go` around lines 66 - 85, The dual-stack send logic
was removed and must be restored so IPv4 destinations use an IPv4 packet
connection when the local socket is IPv6; in the send path (where WriteMsgUDP is
now invoked) detect when !IsIPv6(raddr.IP) && c.packetConn.IsIPv6(), construct
an ipv4 packet wrapper (packetConnIPv4{packetConn:
ipv4.NewPacketConn(c.connection)}) and call its WriteTo(buffer, cm, raddr)
instead of the direct WriteMsgUDP call to ensure the correct IPv4 control
message format is used.

@ltlobaro
Copy link
Contributor Author

Ok I reverted my changes to the unit tests to let them pass.

Btw how can I stop the slop? :(

@jkralik jkralik merged commit 555ec48 into plgd-dev:master Jan 21, 2026
12 checks passed
@jkralik
Copy link
Member

jkralik commented Jan 21, 2026

Thx for contributing. Good job 👍

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.

sendmsg: invalid argument on macOS via IPv4

2 participants