Skip to content

net: ListenMulticastUDP doesn't limit data to packets from the declared group port on Linux #73484

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

Open
mrinny opened this issue Apr 23, 2025 · 9 comments
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.

Comments

@mrinny
Copy link

mrinny commented Apr 23, 2025

What version of Go are you using (go version)?

$ go version
go version go1.23.7 linux/amd64

Does this issue reproduce with the latest release?

yes, also tested on 1.24.2 linux/amd64
tested on
debian trixie - kernel 6.12.22-amd64
ubuntu 24.04 - kernel 6.8.0-51-generic

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/vclaes/.cache/go-build'
GOENV='/home/vclaes/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/vclaes/go/pkg/mod'
GONOPROXY='git.pulsar-lab.be/*'
GONOSUMDB='git.pulsar-lab.be/*'
GOOS='linux'
GOPATH='/home/vclaes/go'
GOPRIVATE='git.pulsar-lab.be/*'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/vclaes/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.7.linux-amd64'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/vclaes/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.7.linux-amd64/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.23.7'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/vclaes/.config/go/telemetry'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/vclaes/Documents/git/pulsarlab/playground/tadaa/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build1225361755=/tmp/go-build -gno-record-gcc-switches'
uname -sr: Linux 6.12.22-amd64
Distributor ID:	Debian
Description:	Debian GNU/Linux trixie/sid
Release:	n/a
Codename:	trixie
/lib/x86_64-linux-gnu/libc.so.6: GNU C Library (Debian GLIBC 2.41-7) stable release version 2.41.
gdb --version: GNU gdb (Debian 16.2-8) 16.2

What did you do?

join a multicast group using net.ListenMulticastUDP.

example / repoduction scenario

A sender process sends multicast packets every second on group A (239.0.10.10).
A proxy process listens for multicast packets on group A if a packet is received it sends a packet with different content on group B (239.0.10.11)
A receiver process listens for multicast packets on group B

mc_sender process: https://gist.github.com/mrinny/3c948eb86fc9cfa151cdf4b089980cbc
mc_proxy process: https://gist.github.com/mrinny/bbe1884dea72a4d08ad4101301f9b258
mc_receiver process: https://gist.github.com/mrinny/2e38dede9d32ebea222b28176537b55f

What did you expect to see?

reading from the connection should only return data send to multicast group which was decared as the remote address (group address and port).

the proxy process prints out "hello world" every second
the receiver process prints out "hello proxy" every second

What did you see instead?

Data from any linux kernel joined group is being returned by calling ReadFromUDP.

The proxy process prints out "hello world" followed by lots of "hello proxy"
the proxy process prints out "hello proxy"

Further Information

On OSX the behavior seems to be as expected.

The example reproduction uses three go programs to demonstrate the issue. Though it can also be observed with other programs.
For example:

  1. run the go proxy process
  2. start ffmpeg as a dummy multicast source "ffmpeg -re -f lavfi -i testsrc=size=960x540:rate=1 -r 1 -c:v libx264 -preset fast -tune zerolatency -crf 28 -g 10 -f rtp rtp://239.0.2.1:3000?pkt_size=1280"
  3. start iperf to make the linux ip stack join the multicast group. "iperf -u -s -B 239.0.2.1"
    The moment the linux ip stack joins the multicast group. I start seeing the proxy process printout the binary video data. While the poxy process never actually joined the group being used for the rtp multicast.

Instead of the call ListenMulticastUDP I've also tried the net.ListenPacket => ipv4.PacketConn => JoinGroup approach. Though this gives the same result.

@JunyangShao
Copy link
Contributor

@neild

@JunyangShao JunyangShao added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Apr 24, 2025
@mrinny
Copy link
Author

mrinny commented Apr 27, 2025

Hi,

I've had a deeper look into the socket setup from the golang executable using strace. When the socket is being setup it is not bound to the actual group address. Instead it is being bound to 0.0.0.0 . This causes all packets received on a interface destined to the provided port to be delivered to the program userspace. This doesn't only create some unexpected data to be received in user program, but also performance impact as unneeded data is being copied from kernel space to userspace (though I'm no expert here).

I guess a modification is required in net/sock_posix.go netFD.dial to correctly bind to the group address. Though I might have made a wrong turn following the function calls.

The fourth line in the strace output is the line of interest.
The Call in the second line of the strace also seems unexpected for me as multicast doesn't have anything to do with broadcast.

main() {
con, err := net.ListenMulticastUDP("udp", nil, &net.UDPAddr{
Port: 3000,
IP: net.IPv4(239, 0, 20, 21),
})
if err != nil {
slog.Error(err.Error())
os.Exit(1)
}
con.Close()
}

strace output from where the socket setup starts

socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
setsockopt(3, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(3000), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
epoll_create1(EPOLL_CLOEXEC) = 4
eventfd2(0, EFD_CLOEXEC|EFD_NONBLOCK) = 5
epoll_ctl(4, EPOLL_CTL_ADD, 5, {events=EPOLLIN, data=0x62d938}) = 0
epoll_ctl(4, EPOLL_CTL_ADD, 3, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data=0x7f620dc73eb00001}) = 0
getsockname(3, {sa_family=AF_INET, sin_port=htons(3000), sin_addr=inet_addr("0.0.0.0")}, [112 => 16]) = 0
setsockopt(3, SOL_IP, IP_MULTICAST_LOOP, [0], 4) = 0
setsockopt(3, SOL_IP, IP_ADD_MEMBERSHIP, {imr_multiaddr=inet_addr("239.0.20.21"), imr_interface=inet_addr("0.0.0.0")}, 8) = 0
epoll_ctl(4, EPOLL_CTL_DEL, 3, 0xc000069dcc) = 0
close(3) = 0
exit_group(0) = ?
+++ exited with 0 +++

@rittneje
Copy link
Contributor

As per the code, it intentionally binds to 0.0.0.0 instead of the original address.

go/src/net/sock_posix.go

Lines 184 to 203 in f9ce1dd

// We provide a socket that listens to a wildcard
// address with reusable UDP port when the given laddr
// is an appropriate UDP multicast address prefix.
// This makes it possible for a single UDP listener to
// join multiple different group addresses, for
// multiple UDP listeners that listen on the same UDP
// port to join the same group address.
if addr.IP != nil && addr.IP.IsMulticast() {
if err := setDefaultMulticastSockopts(fd.pfd.Sysfd); err != nil {
return err
}
addr := *addr
switch fd.family {
case syscall.AF_INET:
addr.IP = IPv4zero
case syscall.AF_INET6:
addr.IP = IPv6unspecified
}
laddr = &addr
}

@mrinny
Copy link
Author

mrinny commented Apr 27, 2025

I wonder if such a policy decision should be made so deep/close to the kernel. I would think it would be rather simple to enforce such a policy in some higher level function where one could actually make a conscious decision if they want a wildcard socket or not.
Imagine a system running a 100 processes where each process is responsible for recording a video multicast stream. This wildcard socket policy would easily cripple a system as each process would get the data for each of the subscribed multicast streams.

@rittneje
Copy link
Contributor

I'm definitely not the expert here, and I don't understand the comment in the code. It was introduced in 0ae8078.

It seems to me the that the standard library should not be binding to 0.0.0.0 like this. Clearly net.ListenMulticastUDP is going to join exactly one group address. So what use case exactly is referred to by the comment? Anyone that wants to join two groups must already be doing that themselves somehow. They ought to be explicit and bind to 0.0.0.0 in the first place.

This would also fix the current surprising behavior where c.LocalAddr() gives 0.0.0.0 instead of the multicast address.

@mrinny
Copy link
Author

mrinny commented Apr 28, 2025

I've looked at the commit message, From that message I assume that fuctionallity was pushed down the stack which should have been solved at a higher level. namely the ipv4 package in this case.
I'm not sure yet on how this can be rectified without impacting backwards compatibility.

@rittneje
Copy link
Contributor

@mrinny do you have an example of how an application could be relying on the current behavior?

@mrinny
Copy link
Author

mrinny commented Apr 28, 2025

I off course don't have knowledge about all usecases. Though I think maybe in a discovery system where one could receive announces from multiples groups and consolidate them in a single socket. I've been using multicast just as multimedia transport. which has other contraints that using it as a messaging transport I would imagine.
I'm going to reach out to @cixtor. Maybe he can shed some light on this.

@mrinny
Copy link
Author

mrinny commented Apr 28, 2025

just some small feedback.
I've commented out the switch in the standard lib of my go1.24.2 install. Recompiling my binaries result in the behavior I would have expected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

3 participants