Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
66 changes: 64 additions & 2 deletions p2p/discovery/mdns/mdns.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import (

logging "github.com/libp2p/go-libp2p/gologshim"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)

const (
Expand Down Expand Up @@ -109,6 +108,68 @@ func (s *mdnsService) getIPs(addrs []ma.Multiaddr) ([]string, error) {
return ips, nil
}

// containsUnsuitableProtocol returns true if the multiaddr includes protocols
// that are not suitable for mDNS advertisement:
// - Circuit relay (requires intermediary, not direct LAN connectivity)
// - Browser transports: WebTransport, WebRTC, WebSocket (browsers don't use mDNS)
func containsUnsuitableProtocol(addr ma.Multiaddr) bool {
found := false
ma.ForEach(addr, func(c ma.Component) bool {
switch c.Protocol().Code {
case ma.P_CIRCUIT,
ma.P_WEBTRANSPORT,
ma.P_WEBRTC,
ma.P_WEBRTC_DIRECT,
ma.P_P2P_WEBRTC_DIRECT,
ma.P_WS,
ma.P_WSS:
found = true
return false
}
return true
})
return found
}

// isSuitableForMDNS returns true for multiaddrs that should be advertised
// via mDNS.
//
// For an address to be suitable:
// 1. It must start with /ip4, /ip6, or a .local DNS name. The .local TLD is
// reserved for mDNS (RFC 6762) and resolved via multicast, not unicast DNS.
// Non-.local DNS names are filtered out as they require external DNS.
// 2. It must not use circuit relay or browser-only transports (WebTransport,
// WebRTC, WebSocket) because these are not useful for direct LAN discovery.
//
// Filtering reduces mDNS packet size, helping stay within the 1500-byte MTU
// limit per RFC 6762. See: https://github.com/libp2p/go-libp2p/issues/3415
Copy link
Collaborator

Choose a reason for hiding this comment

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

A slight correction in this comment. The RFC states the packet "MUST NOT exceed 9000 bytes". Below 1500 bytes is recommended.

Copy link
Member Author

Choose a reason for hiding this comment

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

Addressed in 1aabf13

func isSuitableForMDNS(addr ma.Multiaddr) bool {
if addr == nil {
return false
}

first, _ := ma.SplitFirst(addr)
if first == nil {
return false
}

// Check the addressing scheme
switch first.Protocol().Code {
case ma.P_IP4, ma.P_IP6:
// Direct IP addresses are always suitable for LAN discovery
case ma.P_DNS, ma.P_DNS4, ma.P_DNS6, ma.P_DNSADDR:
Copy link
Collaborator

@MarcoPolo MarcoPolo Dec 2, 2025

Choose a reason for hiding this comment

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

Does DNSADDR make sense here?

Copy link
Member Author

@lidel lidel Dec 2, 2025

Choose a reason for hiding this comment

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

Added it mostly for completeness, but yes, technically someone could have TXT record on _dnsaddr.foo.local and use it for signaling peerid similar to how one can ipfs swarm connect /dnsaddr/bootstrap.libp2p.io without knowing peerid upfront (or connecting to more than one peer)

// DNS names are only suitable if they're in the .local TLD,
// which is resolved via mDNS (RFC 6762), not unicast DNS.
if !strings.HasSuffix(strings.ToLower(first.Value()), ".local") {
return false
}
default:
return false
}

return !containsUnsuitableProtocol(addr)
}

func (s *mdnsService) startServer() error {
interfaceAddrs, err := s.host.Network().InterfaceListenAddresses()
if err != nil {
Expand All @@ -121,9 +182,10 @@ func (s *mdnsService) startServer() error {
if err != nil {
return err
}
// Build TXT records for addresses suitable for mDNS advertisement.
var txts []string
for _, addr := range addrs {
if manet.IsThinWaist(addr) { // don't announce circuit addresses
if isSuitableForMDNS(addr) {
txts = append(txts, dnsaddrPrefix+addr.String())
}
}
Expand Down
52 changes: 52 additions & 0 deletions p2p/discovery/mdns/mdns_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/peer"

ma "github.com/multiformats/go-multiaddr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
Expand Down Expand Up @@ -102,3 +103,54 @@ func TestOtherDiscovery(t *testing.T) {
"expected peers to find each other",
)
}

func TestIsSuitableForMDNS(t *testing.T) {
tests := []struct {
name string
addr string
expected bool
}{
// IP addresses with native transports - suitable for mDNS
{"tcp", "/ip4/192.168.1.1/tcp/4001", true},
{"quic-v1", "/ip4/192.168.1.2/udp/4001/quic-v1", true},
{"tcp-ipv6", "/ip6/fe80::1/tcp/4001", true},
{"quic-v1-ipv6", "/ip6/fe80::2/udp/4001/quic-v1", true},

// Browser transports - NOT suitable for mDNS
// (browsers don't use mDNS for peer discovery)
{"webtransport", "/ip4/192.168.1.1/udp/4001/quic-v1/webtransport", false},
{"webrtc", "/ip4/192.168.1.1/udp/4001/webrtc/certhash/uEiAkH5a4DPGKUuOBjYw0CgwjLa2R_RF71v86aVxlqdKNOQ", false},
{"webrtc-direct", "/ip4/192.168.1.1/udp/4001/webrtc-direct", false},
{"ws", "/ip4/192.168.1.1/tcp/4001/ws", false},
{"wss", "/ip4/192.168.1.1/tcp/443/wss", false},

// .local DNS names - suitable for mDNS
// (.local TLD is resolved via mDNS per RFC 6762)
{"dns-local", "/dns/myhost.local/tcp/4001", true},
{"dns4-local", "/dns4/myhost.local/tcp/4001", true},
{"dns6-local", "/dns6/myhost.local/tcp/4001", true},
{"dnsaddr-local", "/dnsaddr/myhost.local/tcp/4001", true},
{"dns-local-mixed-case", "/dns4/MyHost.LOCAL/tcp/4001", true},

// Non-.local DNS names - NOT suitable for mDNS
// (require unicast DNS resolution, not mDNS)
{"dns4-public", "/dns4/example.com/tcp/4001", false},
{"dns6-public", "/dns6/example.com/tcp/4001", false},
{"dnsaddr-public", "/dnsaddr/example.com/tcp/4001", false},
{"dns-local-suffix-not-tld", "/dns4/notlocal.com/tcp/4001", false},
{"dns-fake-local", "/dns4/local.example.com/tcp/4001", false},
{"libp2p-direct", "/dns4/192-0-2-1.k51qzi5uqu5dgutdk6i1ynyzgkqngpha5xpgia3a5qqp4jsh0u4csozksxel3r.libp2p.direct/tcp/30895/tls/ws", false},

// Circuit relay addresses - NOT suitable for mDNS
// (require relay node, not direct LAN connectivity)
{"circuit-relay", "/ip4/198.51.100.1/tcp/4001/p2p/12D3KooWDpJ7As7BWAwRMfu1VU2WCqNjvq387JEYKDBj4kx6nXTN/p2p-circuit/p2p/12D3KooWGzBXWNvHpLALvz3jhwdCF6kfv9MfhMn9CuS2MBD2GpSy", false},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
addr, err := ma.NewMultiaddr(tc.addr)
require.NoError(t, err)
got := isSuitableForMDNS(addr)
assert.Equal(t, tc.expected, got, "isSuitableForMDNS(%s)", tc.addr)
})
}
}
Loading