Skip to content

Conversation

@rg0now
Copy link
Contributor

@rg0now rg0now commented Jan 10, 2026

With the upcoming addition of TURN/TCP allocations, the mental complexity of working with the client API is becoming unwieldy. As pion/turn gains adoption in areas beyond WebRTC (VPNs, P2P networks, tunneling), this complexity may become a barrier to entry. This PR proposes a simplified client API that mimics the familiar Dialer and Listener interfaces, even at the cost of hiding some TURN complexity. For simple use cases this API should be easier to pick up, while the current client API remains available for users that want full control.

This is just a humble PR to raise some discussion, definitely not production ready.

The Problem

With TCP allocations, we now have two fundamentally different workflows for connecting to peers:

  • UDP: Call client.Allocate() -> returns net.PacketConn -> use WriteTo/ReadFrom
  • TCP: Call client.AllocateTCP() -> returns *client.TCPAllocation -> call DialTCP() -> returns net.Conn

Likewise for "accepting" peer connections:

  • UDP: ReadFrom on the net.PacketConn returned by client.Allocate()
  • TCP: Accept on the *client.TCPAllocation returned from client.AllocateTCP()

Plus there's the complexity of managing peer permissions and client.Listen() that users often forget to call.

Proposal

A unified Allocation interface that works like a combined Dialer + Listener:

type Allocation interface {
    Dial(network, address string) (net.Conn, error)
    Accept() (net.Conn, error)
    Addr() net.Addr
    Close() error
    CreatePermission(addr net.Addr) error
}

Key simplifications:

  • Single constructor (NewAllocation) for both UDP and TCP
  • Dial() handles permission creation internally
  • Accept() works uniformly across both transport types
  • Returns standard net.Conn instead of protocol-specific types

Usage

Create UDP allocation:

alloc, _ := turn.NewAllocation("udp", &turn.ClientConfig{
    Conn:           turnConn,
    TURNServerAddr: "turn.example.com:3478",
    Username:       "user",
    Password:       "pass",
    Realm:          "example.com",
})
defer alloc.Close()

For TCP, just set the network type to "tcp" and pass in a TCP TURN socket.

Dial a peer

Dial creates permission automatically and returns a net.Conn bound to the peer both for UDP and TCP.

conn, _ := alloc.Dial("udp", "192.0.2.1:5000")
conn.Write([]byte("hello"))

Accept incoming connections from permitted peers

Note that Dial automatically calls CreatePermission, but for listeners CreatePermission must be called explicitly.

_ = alloc.CreatePermission(peer)
incoming, _ := alloc.Accept()
// handle incoming...

@codecov
Copy link

codecov bot commented Jan 10, 2026

Codecov Report

❌ Patch coverage is 69.76744% with 52 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.16%. Comparing base (27f733c) to head (e924e07).

Files with missing lines Patch % Lines
allocation.go 69.41% 42 Missing and 10 partials ⚠️

❌ Your patch check has failed because the patch coverage (69.76%) is below the target coverage (70.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #526      +/-   ##
==========================================
- Coverage   81.72%   81.16%   -0.56%     
==========================================
  Files          46       47       +1     
  Lines        3157     3329     +172     
==========================================
+ Hits         2580     2702     +122     
- Misses        375      416      +41     
- Partials      202      211       +9     
Flag Coverage Δ
go 81.16% <69.76%> (-0.56%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@rg0now rg0now force-pushed the client-api-overhaul branch from 650d7ab to 89d7c2b Compare January 10, 2026 21:57
@Sean-Der
Copy link
Member

I am all for it @rg0now as long as we drop the previous API!

  • Could we be making a mistake doing UDP traffic over a net.Conn? If each Read/Write is one datagram seems ok?
  • Does CreatePermission need to exist? Could Accept(addr net.Addr) be a thing? Would I ever need to a create a Permission if I didn't intend to accept traffic upon it? If a user wants to Accept anything pass a nil address?

@jech @JoTurk what's your take?

@rg0now
Copy link
Contributor Author

rg0now commented Jan 11, 2026

I am all for it @rg0now as long as we drop the previous API!

Huhh, that's a tough call. I'm afraid this would be a massive rewrite burden on people using pion/turn. Plus the this PR merely calls into the existing API so we cannot remove it right now anyway. And then there's the issue that this API hides some features (like PerformTransaction, HandleInbound, Connect and BindConnection, etc.) that people may rely on. Plus the code should really really get some testing...

Could we be making a mistake doing UDP traffic over a net.Conn? If each Read/Write is one datagram seems ok?

The net.Conn that Dial("udp", peer) returns is merely a wrapper on top of the existing UDPConn, just with binding automatically calling WriteTo/ReadFrom with peer as the destination address. So everything that applies to UDPConn applies to our net.Conn too.

Does CreatePermission need to exist? Could Accept(addr net.Addr) be a thing? Would I ever need to a create a Permission if I didn't intend to accept traffic upon it? If a user wants to Accept anything pass a nil address?

I was hesitating whether to do this but eventually decided to go with CreatePermission/Accept. The problem is that there can be more than one peer that may request a connection over the allocation at any instance of time and it would be difficult to orchestrate multiple parallel blocking Accept(peer) calls.

Note that you only need to call CreatePermission if you call Accept before/without a Dial (Dial automatically manages permissions), but I think this is the rare case for TURN. The more I think about it the more I feel this is the right thing to do not just for TURN but maybe also for plain old UDP: it is a natural thing to want to decide who can send on a connectionless channel.

Copy link
Member

@JoTurk JoTurk left a comment

Choose a reason for hiding this comment

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

I agree with the permissions API comment, maybe we just need a higher-level API?

Thank you, this is a really cool API.

allocation.go Outdated
}

// NewAllocation creates a TURN allocation that can dial peers and accept incoming connections.
func NewAllocation(network string, conf *ClientConfig) (*turnAllocation, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Am i missing something or why we don't return Allocation seems more straightforward to keep the details private?

Comment on lines 118 to 124
if a.closed {
return nil, ErrAllocationClosed
}
Copy link
Member

Choose a reason for hiding this comment

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

Should we lock before checking closed?

allocation.go Outdated
case d.acceptCh <- conn:
// Sent to accept queue.
default:
// Accept queue full - conn is still registered, just not queued.
Copy link
Member

Choose a reason for hiding this comment

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

How do we handle this? do we enqueue it somewhere else? i couldn't find it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea is that when the accept queue is full we just drop the connection. E.g., TCP does some 3way handshake magic in such cases that we cannot reproduce here. However I think we'd better unregister the dropped call first. We should probably also emit a log at some point...


// Dial establishes a connection to the address through TURN.
// Network must be "udp" or "tcp".
func (a *turnAllocation) Dial(network, address string) (net.Conn, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Maybe if we accept network we should take udp4/udp6/tcp4/tcp6 too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very true. We'll get back to this once #528 is merged

@rg0now rg0now force-pushed the client-api-overhaul branch from 89d7c2b to 509a050 Compare January 12, 2026 19:34
@rg0now rg0now force-pushed the client-api-overhaul branch from 5b6e0bc to e924e07 Compare January 15, 2026 20:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants