Skip to content

NewSTUNConn() doesn't work with UDP net.Conn #523

Open

Description

@cacggghp

Your environment.

v4.1.4

What did you do?

conn := net.Dial("udp", turnaddr)
turnConn := turn.NewSTUNConn(conn)

What did you expect?

Connection being able to send and recieve STUN packets

What happened?

Recieving packets doesn't work

Activity

  1. JoTurk commented on Jan 10, 2026

    @JoTurk
    Member

    @cacggghp hello 👋 can you share more details? test environment? how did you test it?

  2. cacggghp commented on Jan 10, 2026

    @cacggghp
    Author

    @cacggghp hello 👋 can you share more details? test environment? how did you test it?

    Like here: https://github.com/pion/turn/blob/master/examples/turn-client/udp/main.go but using net.Dial and turn.NewSTUNConn instead of net.ListenPacket

  3. JoTurk commented on Jan 10, 2026

    @JoTurk
    Member

    @cacggghp oh, it makes sense, NewSTUNConn expects a stream transport (TCP/TLS/DTLS). I patched the example a bit to show how you can use it with custom dial (DialUDP because we need a packetConn).

    What is your use case anyway?

    udp-client-dial.patch

  4. cacggghp commented on Jan 10, 2026

    @cacggghp
    Author

    @JoTurk I just have a flag to choose between tcp and udp, and I thought net.Dial would work for both. You probably should write in description of NewSTUNConn that it expects a stream. By the way, STUN is a datagram protocol, why would the parsing function break on udp? Aren't STUN messages independent of each other?

    I'm making a dtls encrypted TURN vpn.

  5. JoTurk commented on Jan 10, 2026

    @JoTurk
    Member

    NewSTUNConn takes a net.Conn and re-expose it as a PakcetConn, by framing STUN messages with a length header, this is used for transports with no datagram boundaries. it's unnecessary for UDP and it will cause many problems with handling of address-ports.
    And it's described here https://pkg.go.dev/github.com/pion/turn/v4#STUNConn

    We're doing v5 soon. Maybe we should make this more explicit, clear?

    CC: @Sean-Der @rg0now

  6. iibaranov-IG commented on Oct 12, 2026

    @iibaranov-IG

    Agreed — this belongs in the v5 API rather than as another clarification on the v4 surface.

    The trap is that net.Dial("udp", …) satisfies net.Conn, even though the transport still preserves datagram boundaries. That makes it easy to pass UDP into the stream framer, where length-based reassembly is both unnecessary and wrong for address/port handling.

    For v5, an API boundary that makes the stream-only case explicit (for example a stream-named constructor), with UDP callers using net.PacketConn directly, would make the valid path much harder to misuse. A small regression pair could then cover: a fragmented stream STUN message is reassembled; a UDP datagram is left untouched.

    If the v5 naming/direction is decided, I would be happy to prepare that focused implementation and the characterization tests.

  7. rg0now commented on Oct 12, 2026

    @rg0now
    Contributor

    On a semi-related note: we're working on a refactor to translate pion/turn fully to net.Conns. Once we're ready, there'd be no net.PacketConns at all in the server. TURN is a connection-oriented protocol after all (really!), so it makes little sense to run it over a net.PacketConn. Plus, the server runs a separate readloop per net.Conn (so one goroutine per TCP client) but only a single readloop per net.PacketConn (shared between all clients), so currently we're multi-threading much more efficiently for TCP and TLS than for UDP.

    The idea is to expect the user to bootstrap TURN/UDP servers with a net.Listener that implements a "UDP server" analogous to a TCP server: every UDP packet from a new source IP:port spawns a new connected net.Conn (pion/transport/udp offers a candidate implementation, and here is a multi-threaded alternative). Since all the other transports are already net.Listeners (even DTLS), we can drop the ServerConfig.PacketConnConfigs and we get UDP multi-threading for free. On the relay address generator side we would implement UDP relays over the same API as TCP: AllocateListener creates a net.ListenPacket with reuse-port as the shared relay address and each new peer relay connection gets a separate net.Conn with AllocateConn. Then we drop AllocatePacketConn.

    We're halfway there: server config API ported but internally the server still uses net.PacketConns and the relay generator has not been rewritten yet. This already bore some fruit: net.Conns can be individually closed when the surrounding allocation is closed (which is not the case with the shared net.PacketConn), plus we get proper protocol tags inside the server (last time I checked we defaulted everything to UDP).

    I will post a PR when we're finished. Apart from the obvious gains the API may make very very little sense to most users (people default UDP to PacketConns), so I would not fight for it being accepted, but it's at least an interesting experiment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions