Skip to content

go-libp2p-udx

Skill go-libp2p-udx. Use when adding UDX to a go-libp2p host with github.com/stephanfeb/go-libp2p-udx-transport (package udxtransport): libp2p.Transport(udxtransport.NewTransport), /ip4//udp//udx listen and dial addresses, Noise and Yamux over UDX, hole punching, and interop with dart-libp2p and js-libp2p nodes.

SKILL.md (Claude Code, Codex, Cursor) · go-libp2p-udx.md (Boubou) · install: curl -fsSL https://cryptopeer.net/skills/install.sh | sh -s -- go-libp2p-udx

A go-libp2p transport.Transport that carries libp2p connections over UDX (go-udx). Each libp2p connection is one UDX connection; its first UDX stream is handed to go-libp2p’s upgrader as a raw net.Conn, which runs multistream-select, then Noise, then Yamux on it, as for TCP. This is the same layering dart-libp2p’s UDX transport and js-libp2p’s @go-ricochet/libp2p-udx use, which is what makes the three interoperate. Use it to reach dart-libp2p (Flutter) and js-libp2p nodes over UDP, or to run the Ricochet server stack. For plain UDX without libp2p, use go-udx (skill udx-go); for a standard UDP transport between go-libp2p nodes only, QUIC is the usual choice.

Terminal window
go get github.com/stephanfeb/go-libp2p-udx-transport@v0.1.8

v0.1.8 requires github.com/libp2p/go-libp2p v0.49.0, github.com/multiformats/go-multiaddr v0.16.1, github.com/stephanfeb/go-udx v0.1.3, and declares go 1.25.7.

import (
"github.com/libp2p/go-libp2p"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/p2p/muxer/yamux"
"github.com/libp2p/go-libp2p/p2p/security/noise"
udxtransport "github.com/stephanfeb/go-libp2p-udx-transport"
)

Importing the package registers the udx multiaddr protocol (code 0x0300, no value) in its init.

h, err := libp2p.New(
libp2p.NoTransports, // or keep the defaults and add UDX
libp2p.Transport(udxtransport.NewTransport),
libp2p.Security(noise.ID, noise.New),
libp2p.Muxer(yamux.ID, yamux.DefaultTransport),
libp2p.ListenAddrStrings("/ip4/0.0.0.0/udp/4001/udx", "/ip6/::/udp/4001/udx"),
)
if err != nil { return err }
defer h.Close()

NewTransport(key ic.PrivKey, u transport.Upgrader, rcmgr network.ResourceManager) is an fx constructor: libp2p supplies all three. A nil resource manager becomes network.NullResourceManager.

const proto = "/echo/1.0.0"
server.SetStreamHandler(proto, func(s network.Stream) {
defer s.Close()
io.Copy(s, s)
})
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := client.Connect(ctx, peer.AddrInfo{ID: server.ID(), Addrs: server.Addrs()}); err != nil {
return err
}
s, err := client.NewStream(ctx, server.ID(), proto)
if err != nil { return err }
s.Write([]byte("hello over UDX"))
s.CloseWrite()
reply, _ := io.ReadAll(s)
tr, err := udxtransport.NewTransport(priv, upg, nil) // upg from go-libp2p's p2p/net/upgrader
if err != nil { return err }
defer tr.Close() // closes the shared outbound sockets
laddr, _ := ma.NewMultiaddr("/ip4/127.0.0.1/udp/0/udx")
ln, err := tr.Listen(laddr) // ln.Multiaddr() has the real port
conn, err := tr.Dial(ctx, ln.Multiaddr(), serverPeerID) // a secured, muxed CapableConn

The README’s two-argument NewTransport(priv, nil) and its claim that no upgrader is needed are out of date; see the gaps list in the Reference section.

Yamux keep-alive matched to UDX’s idle timeout

Section titled “Yamux keep-alive matched to UDX’s idle timeout”
ymx := *yamux.DefaultTransport
ymx.KeepAliveInterval = 15 * time.Second // under UDX's 30 s idle timeout
ymx.ConnectionWriteTimeout = 10 * time.Second
libp2p.Muxer(yamux.ID, &ymx)

(The settings go-ricochet uses, from interop_test.go.)

  • NewTransport(key, upgrader, rcmgr) (*Transport, error): the transport.
  • (*Transport).Dial(ctx, raddr, peer.ID): dials, opens UDX stream 0, upgrades with Noise and Yamux.
  • (*Transport).Listen(laddr): binds udp4 or udp6 explicitly, returns an upgraded listener.
  • (*Transport).CanDial(addr): true when the address has a /udx component.
  • (*Transport).Protocols(): []int{P_UDX}; Proxy(): false; Close(): closes outbound multiplexers.
  • P_UDX = 0x0300: the multiaddr code, shared with dart-libp2p and js-libp2p.
  • Address form: /ip4/<ip>/udp/<port>/udx or /ip6/<ip>/udp/<port>/udx, optionally /p2p/<id>.
  • Internals worth knowing: rawListener wraps a udx.Multiplexer; streamConn wraps stream 0 as a manet.Conn.
  1. This is not a native-muxing transport. Despite the README and the site overview, UDX streams are not libp2p streams here: the transport opens exactly one UDX stream per connection and Yamux multiplexes above it. You must configure a security transport (Noise) and a muxer (Yamux); other stacks reach you only with Noise and Yamux.
  2. Dials leave from the listen port. When a listener on the same address family can reach the target, dials share its socket (as QUIC does with reuseport), so peers observe the NAT mapping of the listen port and identify reports a reachable address, and hole punches go out from the port the peer punches towards. Exceptions use a shared ephemeral socket per family: go-libp2p’s uncoordinated force-direct pre-dial, a dial to the listener’s own address, and a non-loopback dial when the only listener is on loopback (a loopback socket cannot send elsewhere; macOS fails with EADDRNOTAVAIL). Listen on 0.0.0.0 / :: if you want NAT traversal.
  3. Hole punching with dart-libp2p needs dart_udx 4.0.0 or later. Crossing dials are two UDX connections, one each way; dart_udx 3.x folded them into one connection that answered neither, so punches between Go and older Dart peers fail.
  4. Idle connections die after 30 s. go-udx closes a connection that has received nothing for max(30 s, 3 x PTO), silently. go-udx sends no keep-alives, so rely on Yamux keep-alive (go-yamux’s default interval is 30 s, the same as the idle timeout; use 15 s as above) or libp2p ping. js-libp2p peers ping every 10 s by default.
  5. The pinned go-udx is v0.1.3. The 1200-byte datagram cap arrived in go-udx v0.1.4, and PMTUD plus RFC 9002 loss recovery in v0.3.0. With v0.1.3 a host sends stream data in datagrams of up to 1472 bytes, which mobile carrier tunnels (464XLAT, GTP) drop. To get the newer behaviour, require a newer go-udx in your own go.mod (go get github.com/stephanfeb/go-udx@v0.3.0) and run this repo’s tests against it; the go-udx API the transport uses has not changed.
  6. IP addresses only. fromUDXMultiaddr reads /ip4 or /ip6 and /udp; /dns4, /dns6 and /dnsaddr are not resolved by the transport.
  7. A closed listener reports net.ErrClosed (v0.1.6+), which the swarm treats as a normal close. Wrap it the same way if you write a listener.
  8. Accept errors per connection are swallowed. If the first stream never arrives or the resource manager refuses, the raw listener closes that UDX connection and keeps accepting. A dialer that never writes (and so never sends its stream SYN) is never accepted; multistream-select always writes first, so this matters only for custom code driving streamConn directly.
  9. Large transfers are the real test. go-udx once stalled at about 256 KB per stream and every small-message test passed. bulk_canary_test.go pushes megabytes through Noise and Yamux over one UDX stream for that reason; keep such a test in downstream repos too.
  10. Wire version 3 on every peer. A v2 peer is dropped silently and looks unreachable. Interop minimums from js-udx’s matrix: dart-libp2p 3.0.0+ (earlier ones miss a Yamux FIN carried on a window update), js-libp2p via @go-ricochet/libp2p-udx 0.1.x, and this transport v0.1.4+.
  11. Browsers. A js-libp2p web app that gets a relay reservation listing your /udx address must register the codec (0x0300), or js-libp2p rejects the reservation with Protocol 768 was unknown. That is fixed on the browser side (@go-ricochet/libp2p-udx/multiaddr), not here.
  • Two hosts on loopback with libp2p.NoTransports, libp2p.Transport(NewTransport), Noise, Yamux, libp2p.ListenAddrStrings("/ip4/127.0.0.1/udp/0/udx") and libp2p.ResourceManager(&network.NullResourceManager{}) (host_test.go). Use server.Addrs() after start; port 0 is resolved.
  • Put a context.WithTimeout on Connect and NewStream, and SetReadDeadline on streams: a lost path otherwise waits for the 30 s idle timeout.
  • Transport-level tests build an upgrader with upgrader.New and noise.New(noise.ID, key, muxers) (transport_test.go, createUpgrader).
  • Hole-punch paths: network.WithSimultaneousConnect and network.WithForceDirectDial on the dial context (punch_test.go).
  • go test -race ./.... interop_test.go runs a Dart echo server from ../dart-libp2p (dart run bin/interop_echo_server.dart) and needs Dart and that checkout.
  • js-udx’s packages/libp2p-udx/test/interop.test.ts drives this transport from js-libp2p in both directions (tools/go-libp2p-peer).
  • Never change P_UDX (0x0300) or the address form; dart-libp2p and js-libp2p use the same code.
  • Never skip the upgrader or hand out stream 0 unsecured: UDX itself has no encryption or peer authentication; Noise provides both.
  • Never open additional UDX streams on a transport connection expecting the peer to treat them as libp2p streams; peers only read stream 0.
  • Call Transport.Close() (libp2p does on host.Close()) so the shared outbound sockets and their goroutines go away.
  • Do not bump go-udx across a wire version without bumping every peer.

Built by Stephan Februarygithub.com/stephanfebLinkedIn