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

# go-libp2p-udx-transport

## What it is

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.

## Install

```sh
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`.

```go
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`.

## Core workflow

### A host on UDX

```go
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`.

### Echo protocol

```go
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)
```

### Without the libp2p constructor

```go
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

```go
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`.)

## Key concepts / API map

- `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`.

## Gotchas

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.

## Testing

- 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`).

## Red lines

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

## Reference

- Docs: https://cryptopeer.net/udx/go-libp2p-transport/ and https://cryptopeer.net/udx/
- Repo: https://github.com/stephanfeb/go-libp2p-udx-transport
- go-udx: https://cryptopeer.net/udx/go/, https://github.com/stephanfeb/go-udx
- Related skills: udx-go, udx-js, udx-dart
- Known doc gaps (README vs code at v0.1.8): `NewTransport` takes three
  arguments and requires an upgrader; the files are `transport.go`,
  `listener.go`, `stream_conn.go` and `multiaddr.go` (no `conn.go`/`stream.go`);
  the licence file is Apache 2.0 while the README says MIT.
