Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC6062: Implement client side for TCP allocations #311

Merged
merged 17 commits into from
May 19, 2023
Merged

Conversation

AndyLc
Copy link
Contributor

@AndyLc AndyLc commented Mar 14, 2023

Description

Builds off of #148

Implements client side support for TCP allocations.
I'm also planning on adding support to server side

For the TCP allocation implementation, I tried follow the pattern of creating a client, calling allocate, and then using that allocation for communication. I thought that instead of calling WriteTo() and ReadFrom() on a TCP allocation, it would be better to Accept and Dial, and return a net.Conn for the end user to use, so I ended up having to do some refactoring.

Any feedback is welcome!

Usage:

  1. Start coturn or some other turn server
    ./bin/turnserver --lt-cred-mech -u andy:secret -r pion.ly --no-cli ./examples/ca/turn_server_cert.pem --pkey ./examples/ca/turn_server_pkey.pem --allow-loopback-peers

  2. Run tcp client, also starting up a simple tcp server to communicate relay IPs.
    ./tcp_alloc --host 127.0.0.1 --user andy=secret -server

  3. Run another tcp client, which gets the relay IP from the other client and sends its own.
    ./tcp_alloc --host 127.0.0.1 --user andy=secret

Both clients should read and write a single message.

Reference issue

#143, #118

@rg0now
Copy link
Contributor

rg0now commented Mar 14, 2023

Thanks for the PR, I'm loving it! I also wanted to take up #148 myself, I'm very happy that you came forward to do that.

Quick question: what is the purpose of the new Protocol field in the ClientConfig? My initial guess was that this was to fix the client exclusively to RFC6062-TCP allocations, but then why do you introduce an AllocateTCP function for a seemingly similar purpose? I mean, what's the difference between calling Allocate on a client with client.Protocol="tcp" and calling AllocateTCP on a client with client.Protocol="udp"? Then it occurred to me that most probably you introduce this in order to drive the client in resolving the TURN/STUN server addresses, but this is not needed: RFC6062-TCP allocations get you a connection to a TCP peer and not a TCP STUN/TURN server.

My suggestion would be to remove ClientConfig.Protocol and let the user specify the peer protocol by calling either Allocate or AllocateTCP. Also, you won't need a separate UDP/TCP resolving round for stunServ and turnServ, just use ResolveUDPAddr, this is what we have used so far anyway.

I'll try to do a formal review later too. And thanks again for picking this PR up, it is really useful!

@AndyLc
Copy link
Contributor Author

AndyLc commented Mar 15, 2023

Thanks for the PR, I'm loving it! I also wanted to take up #148 myself, I'm very happy that you came forward to do that.

Quick question: what is the purpose of the new Protocol field in the ClientConfig? My initial guess was that this was to fix the client exclusively to RFC6062-TCP allocations, but then why do you introduce an AllocateTCP function for a seemingly similar purpose? I mean, what's the difference between calling Allocate on a client with client.Protocol="tcp" and calling AllocateTCP on a client with client.Protocol="udp"? Then it occurred to me that most probably you introduce this in order to drive the client in resolving the TURN/STUN server addresses, but this is not needed: RFC6062-TCP allocations get you a connection to a TCP peer and not a TCP STUN/TURN server.

My suggestion would be to remove ClientConfig.Protocol and let the user specify the peer protocol by calling either Allocate or AllocateTCP. Also, you won't need a separate UDP/TCP resolving round for stunServ and turnServ, just use ResolveUDPAddr, this is what we have used so far anyway.

I'll try to do a formal review later too. And thanks again for picking this PR up, it is really useful!

Thank you for looking at the PR! I agree, specifying Protocol in the ClientConfig and having both AllocateTCP and AllocateUDP is redundant.

I'll update the PR with those changes.

@rg0now
Copy link
Contributor

rg0now commented Mar 17, 2023

it would be better to Accept and Dial, and return a net.Conn for the end user to use, so I ended up having to do some refactoring.

I'm thinking about the semantics of the thingie that is supposed to be returned from AllocateTCP:

  • on the one hand it should be a proper net.Dialer so that the client can call Dial or DialContext to create a new connection to a peer
  • on the other hand it also must implement the net.Listener interface so that we can call Accept on it to handle the cases when a peer connects to our transport relay connection on the server.

Wdyt?

@AndyLc
Copy link
Contributor Author

AndyLc commented Mar 17, 2023

I think that makes a lot of sense. What I return right now supports just Dial() and Accept()

Thinking about making it implement net.Listener, since net.Listener's Accept() returns (Conn, error), if I wanted to know the address of the peer that is trying to connect to me, calling RemoteAddr() will give me the address of the TURN server, not the peer. We would need to create a new struct which implements net.Conn that returns the correct RemoteAddr().

As for net.Dialer, what are you thinking for making it a net.Dialer but also implementing net.Listener?

@rg0now
Copy link
Contributor

rg0now commented Mar 20, 2023

Thinking about making it implement net.Listener, since net.Listener's Accept() returns (Conn, error), if I wanted to know the address of the peer that is trying to connect to me, calling RemoteAddr() will give me the address of the TURN server, not the peer. We would need to create a new struct which implements net.Conn that returns the correct RemoteAddr().

This is very similar to how the LocalAddr function of the UDPConn returned by client.Allocate actually returns the relay transport address instead of the local address. So indeed, this is a good idea.

As for net.Dialer, what are you thinking for making it a net.Dialer but also implementing net.Listener?

The client needs to be able to both (1) accept new connections from different peers and (2) make new connections to different peers.

This is simple for UDP that is unconnected: we have a single Allocate returning a "universal" UDPConn that you can use to send to any peer by specifying the peer address in the WriteTo, and to receive packets from any peer by using ReadFrom. Problem is: this does not exist with TCP since there is no WriteTo and ReadFrom, only Write and Read, as TCP is connected. So every time the client wants to make a new connection to a peer it has to create a new TCP connection via the TURN server, which is essentially the same as implementing net.Dialer on the thing returned by AllocateTCP, and every time it wants to accept new connections from a peer it has to make a new connection again, which maps nicely to net.Listener.

So what I'm saying is that, for a client to accept new connections from a peer it should call Accept on the thing returned by AllocateTCP:

stuff, err := client.AllocateTCP()

conn, err := stuff.Accept()

While for making a new connection to a peer it has to call Dial:

stuff, err := client.AllocateTCP()

conn, err := stuff.Dial("tcp", peerAddr)

I'm thinking about a good name for stuff: transport or net are the closest I can come up with right now, as per pion/transport...

@AndyLc
Copy link
Contributor Author

AndyLc commented Mar 22, 2023

Makes sense, I totally agree

I'm thinking about a good name for stuff: transport or net are the closest I can come up with right now, as per pion/transport...

I'm open to either, maybe socket? It feels like we're using a socket on the allocation we made to accept to connections from peers, or dial to peers.

@AndyLc AndyLc marked this pull request as ready for review March 22, 2023 04:33
@rg0now
Copy link
Contributor

rg0now commented Mar 22, 2023

I'm open to either, maybe socket? It feels like we're using a socket on the allocation we made to accept to connections from peers, or dial to peers.

Unfortunately socket is too much overloaded these days. What about TCPRelay?

Another problem tht I spotted is the hardcoded use of net.Dial inside the TCPConn implementation. In my view one of the greatest virtues of pion/turn is that it doesn't enforce the use of Go's standard net functions to handle networking. Instead, clients and servers assume only interfaces, like net.Listener or net.PacketConn, and allow you to pass in your own implementation. There are a lot of users for this functionality, e.g., we use it for wrapping all our network connections in a pseudo-connection that reports all passing bytes and packets to Prometheus.

It would be nice to keep this generality in your implementation, but it seems there are quite a lot of places where you assume net.Dial and company in your code, e.g., in TCPConn:

conn, err := net.Dial("tcp", c.obs.TURNServerAddr().String())

My suggestion would be the following:

  • create a new turn.Dialer interface that generalizes net.Dialer (I can't for the love of god understand why net.Dialer is not an interface but anyway), which is essentially a factory to make new connections (whatever type of connections, doesn't have to be TCP):
    type Dialer interface {
        Dial(network, address string) (Conn, error)
        DialContext(ctx context.Context, network, address string) (Conn, error) // I'm not sure we need this...
    }
    
  • make a default implementation that implements turn.Dialer using net
  • allow the user to pass in their own implementation either in the NewClient or the AllocateTCP call (I'd prefer the first) but make this optional; if no turn.Dialer is specified then we set the default implementation that uses net
  • convert TCPConn to use the abstract Dialer

I guess the server side will be OK since ListenerConfig already uses the abstract net.Listener interface. Wdyt?

@AndyLc
Copy link
Contributor Author

AndyLc commented Mar 23, 2023

  • create a new turn.Dialer interface that generalizes net.Dialer (I can't for the love of god understand why net.Dialer is not an interface but anyway), which is essentially a factory to make new connections (whatever type of connections, doesn't have to be TCP):
    type Dialer interface {
        Dial(network, address string) (Conn, error)
        DialContext(ctx context.Context, network, address string) (Conn, error) // I'm not sure we need this...
    }
    
  • make a default implementation that implements turn.Dialer using net
  • allow the user to pass in their own implementation either in the NewClient or the AllocateTCP call (I'd prefer the first) but make this optional; if no turn.Dialer is specified then we set the default implementation that uses net
  • convert TCPConn to use the abstract Dialer

Sounds great, I will add this. One issue, currently the turn package imports client. TCPConn lives in package client, and if I were to give it a user-defined dialer, it would create create circular dependency. Shall we move turn.Dialer to a new package? Maybe utils or something?

Also for TCPRelay, you're thinking renaming TCPConn->TCPRelay? Or creating another interface, TCPRelay, which has both Dial and Accept?

@rg0now
Copy link
Contributor

rg0now commented Mar 25, 2023

Sounds great, I will add this. One issue, currently the turn package imports client. TCPConn lives in package client, and if I were to give it a user-defined dialer, it would create create circular dependency. Shall we move turn.Dialer to a new package? Maybe utils or something?

Hmm, good catch. My gut feeling is that it should go into pion/transport but I really don't know. Let's put it into internals/ipnet for now but this is not optimal, I hope @stv0g will jump in and suggest something better.

Also for TCPRelay, you're thinking renaming TCPConn->TCPRelay? Or creating another interface, TCPRelay, which has both Dial and Accept?

I'd call the new interface that is both a net.Dialer and a net.Listener as TCPRelay, I would put this into internals/ipnet or a new package internals/net or internals/util or internals/transport, and I would make TCPConn an implementation of this interface. Does this sound stupid? API-design-wise this PR ended up pretty unique: I 've never seen a case when a TCP endpoint behaved both as a net.Dialer and a net.Listener.

Update: maybe TCPRelay is not really a good name for the interface because to connotes that the protocol must be TCP. What about calling it simply Relay or NetRelay? The best option would be to move it to pion/transport and then the name could be transport.Relay, that would be perfect.

Update2: Here is my final conclusion: let's put the Relay interface into a new package called internal/transport and then the users of it would refer to it as transport.Relay and that would nicely connote the meaning that this interface works in two directions, both as a Dialer and a Listener.

@rg0now
Copy link
Contributor

rg0now commented Mar 25, 2023

As per why net.Dialer is not an interface, see this: golang/go#9360. I don't agree with the points made there, but anyway.

@AndyLc
Copy link
Contributor Author

AndyLc commented Mar 28, 2023

let's put the Relay interface into a new package called internal/transport and then the users of it would refer to it as transport.Relay and that would nicely connote the meaning that this interface works in two directions, both as a Dialer and a Listener.

So I tried creating this interface

import (
	t "github.com/pion/transport/v2"
	"net"
)

type Relay interface {
	net.Listener
	t.Dialer
}

but net.Listener's Accept() only returns a Conn and error. From net.Conn, RemoteAddr() will return the address of the turn server, not the peer. Do you have any ideas on the best way to convey from which peer we are accepting a connection?

Originally I had Accept also return another address, but maybe there's a better way?

@rg0now
Copy link
Contributor

rg0now commented Mar 28, 2023

but net.Listener's Accept() only returns a Conn and error. From net.Conn, RemoteAddr() will return the address of the turn server, not the peer. Do you have any ideas on the best way to convey from which peer we are accepting a connection?

Bad news: you'll have to implement your own net.TCPListener and net.Conn for this. Good news, this seems simple thanks to Go's embedding. So here is my take.

Basically we need to create our own implementation of net.Listener called, say, TCPRelay, that will provide us our own net.Conn implementation, say, TCPRelayConn, on accept.

type TCPRelay struct {
    net.TCPListener
    // put something here that will provide the custom remote addr for the connections created
    // on this TCPRelay, say, a turn.Allocation
}

type TCPRelayConn struct {
    net.Conn
    RemoteAddr net.Addr
}

// Implement all of net.Listener for TCPRelay, here is the Accept() for a sample
func (l *TCPRelay) Accept() (TCPRelayConn, error){
    c, err := TCPListener.Accept()
    a := <obtain the remote addr for this allocation>
    return &TCPRelayConn{c, a}, err
}

// Implement all of net.Conn for TCPRelayConn, here is RemoteAddr
func (c *TCPRelayConn) RemoteAddr() net.Addr {
    return c.RemoteAddr
}

// Implement the whole of `transport.Dialer` for TCPRelay
func (l *TCPRelay) Dial(network, address string) (TCPRelayConn, error) {
    // use DialTCP or similar and then return the TCPRelayConn that wraps the net.Conn obtained
}

At the end we should have a default implementation for the Relay interface that people can use if
they don't want to mess with overriding anything, but since your code internally only ever
interacts with the Relay interface it is generic enough to support people providing their own
implementation.

Looking at this right now, I'm not sure we have to take all this complexity at the first step. We
could go with your the hardcoded-TCP implementation for now, let the PRs get merged, and only after
that generalize? What do you think is the best way forward?

@AndyLc
Copy link
Contributor Author

AndyLc commented Mar 29, 2023

We could go with your the hardcoded-TCP implementation for now, let the PRs get merged, and only after
that generalize? What do you think is the best way forward?

Agreed, I think getting the PRs in and then generalizing will make the code changes simpler and more iterative.

I'll re-push my current changes based off your suggestions now!

@stv0g
Copy link
Member

stv0g commented Mar 31, 2023

Hi all, I am joining the party a bit late. So I will need some more time to review the PR. Thanks @AndyLc for starting to work on it :) And @rg0now for the first review :)

[...] you'll have to implement your own net.TCPListener

We already have an interface for this defined here: https://github.com/pion/transport/blob/22a2f3b9fe3381cb4a2735420e05b64813ca3e4f/net.go#L378

Did you maybe already have a look on how we can plug this into pion/ice?
I think its important to keep this in mind before we merge an API which would be hard/impossible to be used by pion/ice

@rg0now
Copy link
Contributor

rg0now commented Mar 31, 2023

You mean Agent.gatherCandidatesRelay, no?

The definite answer is in RFC 6544 (TCP Candidates with Interactive Connectivity Establishment (ICE) but I don't have time to read it. I think the main idea is that we create two allocations on any TCP TURN server: one conventional UDP and another RFC 6062 style TCP. Then, we send two relay candidates over to the peer, one with protocol UDP and another with TCP.

If we are at RFCs: here is what RFC 8835 (Transports for WebRTC) has to say on this:

TURN TCP candidates, where the connection from the WebRTC endpoint's TURN server to the peer is a TCP connection, [RFC6062] MAY be supported.

However, such candidates are not seen as providing any significant benefit, for the following reasons.

First, use of TURN TCP candidates would only be relevant in cases where both peers are required to use TCP to establish a connection.

Second, that use case is supported in a different way by both sides establishing UDP relay candidates using TURN over TCP to connect to their respective relay servers.

Third, using TCP between the WebRTC endpoint's TURN server and the peer may result in more performance problems than using UDP, e.g., due to head of line blocking.

@stv0g stv0g mentioned this pull request Apr 1, 2023
@AndyLc
Copy link
Contributor Author

AndyLc commented Apr 5, 2023

Thanks @stv0g for taking a look!

I think the main idea is that we create two allocations on any TCP TURN server: one conventional UDP and another RFC 6062 style TCP. Then, we send two relay candidates over to the peer, one with protocol UDP and another with TCP.

If we go with this, I believe it should be pretty straightforward. In addition to calling Allocate(), we also call AllocateTCP() when url.Proto == ProtoTypeTCP. Looks like network type, which is always UDP needs to be changed too. I can take a more detailed look later.

One concern I had with this method is, I feel we should only gather TCP relay candidates under certain conditions. For example, if we ever created an offer specifying m=audio 45664 TCP/RTP/AVP 0, we should only generate TCP candidates right? In that case, we might need to rework GatherCandidates() to take in the target protocol, otherwise allow all.

@stv0g
Copy link
Member

stv0g commented Apr 5, 2023

I feel we should only gather TCP relay candidates under certain conditions.

The ICE agent config already allows you to specify which types of candidates should be gathered via its NetworkTypes and CandidateTypes settings.

@rg0now
Copy link
Contributor

rg0now commented Apr 5, 2023

I took a look at the client API proposed in #143. Turns out there are some important differences.

  1. In order for a client to obtain a new TCP connection to a peer, in this proposal the client uses the "thing" returned from AllocateTCP as a Dialer:

    relayConn, _ := client.AllocateTCP()
    conn, _ = relayConn.Dial("tcp", peerAddr)
    

    In contrast, in RFC6062 implementation #143 we first make a TCP allocation on the server, obtain a new connection id, we open another TCP connection, and finally bind the new connection to the connection id we have just obtained in a separate step:

    // not sure what to do with relayConn
    relayConn, _ := client.AllocateTCP()
    
    // attempt connection, get connection id
    cid, _ := client.Connect(peerAddr)
    
    // open new data connection to turn server
    dconn, _ := net.Dial("tcp", turnServerAddr)
    
    // associate connection with connection id
    _ = client.ConnectionBind(dconn, cid)
    

    I personally prefer the latter, since it allows to bind any net.Conn to the connection id, not just net.TCPConn. This would be fairly difficult with the API proposed here, where the net.(TCP)Conn is created and returned by the Dialer (see the discussion above).

  2. To support the opposite case, when it is the peer that initiates the new connection, the client has to be able to call Accept on the thing returned by AllocateTCP. I don't see support for this in RFC6062 implementation #143 (observe the comment "not sure what to do with relayConn" in the above snipppet, I took it literally from the PR). This PR nicely handles this by returning a thing that is also a proper net.Listener.

In summary, I prefer the API of #143 for making client->peer connections since it does not require us to make the thing returned from AllocateTCP a Dialer, and the API proposed here for making peer->client connections as it is a proper net.Listener. Wdyt?

@AndyLc: if we adopt this new client API then my advice to implement t.Relay is void. I hope it was at least a fun experience, sorry for that...:-(

@AndyLc
Copy link
Contributor Author

AndyLc commented Apr 10, 2023

  1. I personally prefer the latter, since it allows to bind any net.Conn to the connection id, not just net.TCPConn. This would be fairly difficult with the API proposed here, where the net.(TCP)Conn is created and returned by the Dialer (see the discussion above).

@rg0now Hm, is the benefit you're getting at that it would be easier to create your own implementation of net.Conn rather than transport.Dialer?

I'm also utilizing a user chosen transport.Dialer in HandleConnectionAttempt, where I need to create a new connection to the relay to return when the user calls Accept(). So, if I were to remove Dial from the t.Relay, the user would still need an implementation of Dialer if they want custom functionality.

@codecov
Copy link

codecov bot commented Apr 12, 2023

Codecov Report

Patch coverage: 59.13% and project coverage change: +1.26 🎉

Comparison is base (a251248) 68.16% compared to head (50522c3) 69.42%.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #311      +/-   ##
==========================================
+ Coverage   68.16%   69.42%   +1.26%     
==========================================
  Files          39       42       +3     
  Lines        2475     2823     +348     
==========================================
+ Hits         1687     1960     +273     
- Misses        657      696      +39     
- Partials      131      167      +36     
Flag Coverage Δ
go 69.42% <59.13%> (+1.26%) ⬆️
wasm 48.62% <42.95%> (+3.82%) ⬆️

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

Impacted Files Coverage Δ
internal/client/errors.go 75.00% <ø> (+75.00%) ⬆️
internal/client/tcp_conn.go 0.00% <0.00%> (ø)
internal/server/turn.go 59.25% <0.00%> (ø)
internal/client/allocation.go 35.45% <35.45%> (ø)
internal/client/tcp_alloc.go 60.23% <60.23%> (ø)
client.go 71.58% <64.00%> (-1.96%) ⬇️
internal/ipnet/util.go 70.37% <85.71%> (+25.37%) ⬆️
internal/allocation/allocation.go 83.92% <100.00%> (+1.07%) ⬆️
internal/client/permission.go 100.00% <100.00%> (ø)
internal/client/udp_conn.go 74.13% <100.00%> (ø)
... and 1 more

... and 4 files with indirect coverage changes

☔ View full report in Codecov by Sentry.
📢 Do you have feedback about the report comment? Let us know in this issue.

Copy link
Member

@stv0g stv0g left a comment

Choose a reason for hiding this comment

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

Hi @AndyLc

Thanks a lot for your work. It looks great so far.
I mainly have a few nitpick comments.

This is a first round of review. I will probably have a few more comments later.

One thing, we should not forget is to add some info the README.md as well.

client.go Outdated
@@ -15,6 +15,7 @@ import (
"github.com/pion/transport/v2/vnet"
"github.com/pion/turn/v2/internal/client"
"github.com/pion/turn/v2/internal/proto"
t "github.com/pion/turn/v2/internal/transport"
Copy link
Member

Choose a reason for hiding this comment

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

Please use transportx as an alias for package name collissions which have been defined within the module.

client.go Outdated
@@ -83,6 +86,10 @@ func NewClient(config *ClientConfig) (*Client, error) {
return nil, errNilConn
}

if config.Dialer == nil {
Copy link
Member

Choose a reason for hiding this comment

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

Please avoid modifying config as the caller of NewClient might not expect this.

client_test.go Outdated
@@ -187,3 +187,53 @@ func TestClientNonceExpiration(t *testing.T) {
assert.NoError(t, conn.Close())
assert.NoError(t, server.Close())
}

// Create a tcp-based allocation and verify allocation can be created
Copy link
Member

Choose a reason for hiding this comment

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

Please capitalize TCP like "TestTCPClient creates a TCP-based allocation [...]".

Also note that function comments should always have the form "FunctionName is/does XYZ".

client_test.go Outdated
// Create a tcp-based allocation and verify allocation can be created
func TestTCPClient(t *testing.T) {
// Setup server
tcpListener, err := net.Listen("tcp4", "0.0.0.0:3478")
Copy link
Member

Choose a reason for hiding this comment

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

Can we use some non-standard ports for this test here? As we can not currently perform the TCP tests using a virtual network (pion/transport/vnet) we should make sure that we are can cope with any other STUN/TURN server running in the background.

client_test.go Outdated
func TestTCPClient(t *testing.T) {
// Setup server
tcpListener, err := net.Listen("tcp4", "0.0.0.0:3478")
assert.NoError(t, err)
Copy link
Member

Choose a reason for hiding this comment

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

You probably want to use require.NoError() here and all other occurences of assert.

internal/server/turn.go Show resolved Hide resolved
},
}

c.log.Debugf("initial lifetime: %d seconds", int(c.lifetime().Seconds()))
Copy link
Member

Choose a reason for hiding this comment

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

Please start log outputs with a uppercase letter.

Copy link
Member

Choose a reason for hiding this comment

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

Please also check other places where you go logging.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm in this repo there are many instances of c.log.Debug that use lower case, and I was following that. Should we change the standard to first letter uppercase?

return err
}

// read exactly one STUN message,
Copy link
Member

Choose a reason for hiding this comment

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

Comments should start with uppercase letter

@@ -8,6 +8,7 @@ var (
errFake = errors.New("fake error")
errTryAgain = errors.New("try again")
errClosed = errors.New("use of closed network connection")
errTCPAddrCast = errors.New("addr is not a net.TCPAddr")
Copy link
Member

Choose a reason for hiding this comment

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

Can you change the errors descriptions here to something more human friendly?

E.g. not a UDP/TCP address?

I dont think its a nice practice to have actualy Go code/types in error/log messages.

CreatePermissions(addrs ...net.Addr) error
}

type RelayConnContext struct {
Copy link
Member

Choose a reason for hiding this comment

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

I am a bit confused by the different types here Relay, RelayConn & RelayConnContext.
Can you document them? Also, shoudnt we name RelayConnContext more like RelayConnImpl?

Copy link
Member

Choose a reason for hiding this comment

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

Or in general: why RelayX? Isnt this related to TCP connections?

Copy link
Contributor Author

@AndyLc AndyLc Apr 21, 2023

Choose a reason for hiding this comment

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

RelayX are used for both TCP and UDP connections. Both UDPConn and TCPConn use the fields in RelayConnContext. In this case, do you prefer the naming of RelayConnImpl?

In client.go, RelayConn was used to generalize between UDPConn and TCPConn. I think we can get rid of this and each client will have a UDPConn and TCPAllocation.

Copy link
Member

Choose a reason for hiding this comment

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

I think the confusion stems from the fact that we do not have a client.UDPAllocation type.
If we would have it, client.RelayConnContext would be more appropriately named client.Allocation.

But in fact, the fields of client.RelayConnContextare basically just the clients per-allocation state.

Copy link
Member

Choose a reason for hiding this comment

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

I am just confused why we have a per-allocation _nonce and and integrity fields here. Also the naming of ConnObserver is confusing.

I think this could need some refactoring as well.

  • Rename RelayConnContext to Allocation
  • Rename ConnObserver to Client (Just an interface within internal/client representing the public Client type in order to break cyclic dependencies)
  • Move integrity and nonce fields from RelayConnContext to the new Client interface
  • Rename conn.go to udp_conn.go
  • Move common code from conn.go to allocation.go

@rg0now
Copy link
Contributor

rg0now commented Apr 19, 2023

I am a bit confused by the different types here Relay, RelayConn & RelayConnContext.

This one is on me, see: #311 (comment).

Also, can you please comment on the API? It'd be nice to get your opinion on #311 (comment).

@stv0g
Copy link
Member

stv0g commented Apr 19, 2023

Hi @rg0now & @AndyLc,

Also, can you please comment on the API? It'd be nice to get your opinion on #311 (comment).

Sure. I had to spend some time to think about it.
Currently, I am leaning towards the API proposed by @AndyLc in this PR in favor of #143.
Mainly, because I find it hard to find a use case for binding an existing TCP connection to the connection ID.
It just makes the API harder to use as the user must establish the TCP data connection to the same TURN server address as the control connection.

But I am not against supporting also the ConnectionBind use case.

Here is my proposal which basically combines the two approaches by moving the client. ConnectionBind() method to allocation.BindConnection():

package turn

type Client struct { ... }

func (c *Client) AllocateTCP() (*TCPAllocation, error) { ... }

type TCPAllocation struct {
	// addr is the relayed transport address
	addr *net.TCPAddr
	client *Client

	...
}

func (a *TCPAllocation) DialTCP(network string, laddr, raddr *net.TCPAddr) (*TCPConn, error) {
	conn, err := net.DialTCP(network, client.TURNServerAddr)
	if err != nil {
		return nil, fmt.Errorf("failed to establish connection to TURN server: %w", err)
	}

	return a.DialTCPWithConn(conn, network, laddr, raddr)
}

func (a *TCPAllocation) DialTCPWithConn(conn net.Conn, network string, laddr, raddr *net.TCPAddr) (*TCPConn, error) {
	cid := ...
	
	conn := &TCPConn{
		TCPConn: conn,
		ConnectionID: cid
		allocation: a,
	}

	if err := a.BindConnection(conn); err != nil {
		return nil, fmt.Errorf("failed to bind connection: %w", err)
	}

	return conn, nil
}

// AcceptTCP accepts the next incoming call and returns the new connection.
func (a *TCPAllocation) AcceptTCP() (*TCPConn, error) {
	conn, err := net.DialTCP(network, client.TURNServerAddr)
	if err != nil {
		return nil, fmt.Errorf("failed to establish connection to TURN server: %w", err)
	}

	return a.AcceptTCPWithConn(conn)
}

func (a *TCPAllocation) AcceptTCPWithConn(conn net.Conn) (*TCPConn, error) {
	cid := ...
	
	conn := &TCPConn{
		TCPConn: conn,
		ConnectionID: cid
		allocation: a,
	}

	if err := a.BindConnection(conn); err != nil {
		return nil, fmt.Errorf("failed to bind connection: %w", err)
	}

	return conn, nil
}


// Addr returns the allocations relayed address.
func (a *TCPAllocation) Addr() net.Addr { ... }

// Close releases the allocation
// Any blocked Accept operations will be unblocked and return errors.
// Any opened connection via Dial/Accept will be closed.
func (a *TCPAllocation) Close() error { ... }

// SetDeadline sets the deadline associated with the listener.
// A zero time value disables the deadline.
func (a *TCPAllocation) SetDeadline(t time.Time) error { ... }

// TCPConn wraps a net.TCPConn and returns the allocations relayed
// transport address in response to TCPConn.LocalAddress()
type TCPConn struct {
	*net.TCPConn

	// ConnectionID uniquely identifies a peer data connection.
	ConnectionID uint32

	allocation *TCPAllocation
}

func (c *TCPConn) LocalAddress() net.Addr {
	return c.allocation.Addr()
}

var _ transport.TCPListener = (*TCPAllocation)(nil) // Includes type check for net.Listener
var _ transport.TCPConn = (*TCPConn)(nil) // Includes type check for net.Conn
var _ transport.Dialer = (*TCPAllocation)(nil)
// TODO: We might want to add a transport.TCPDialer interface
// var _ transport.TCPDialer = (*TCPAllocation)(nil)

@stv0g
Copy link
Member

stv0g commented May 10, 2023

@AndyLc thanks for the repo access. I've addressed my remaining comments myself and also rebased your dev to the upstream master.

Please reset your local branch as it gets messed up otherwise due to the rebase.

Lets, see what the CI says..

@AndyLc
Copy link
Contributor Author

AndyLc commented May 18, 2023

@stv0g Thanks for making edits! I saw that some tests were still failing, so I pushed another commit to address those, mainly fixing golangci-lint issues and adding more unit tests.

@stv0g
Copy link
Member

stv0g commented May 18, 2023

Hi @AndyLc :)

Great! I think we are getting close merging this into master 🥳

I currently, see only one thing missing: We should add support for pion/transport.Net to the Client as use net.Dial for creating the TCP data-connection. Right now we are limited to the standard net.Dial. By using the interface from pion/transport we will gain the ability to test the feature with pion/transport/vnet as soon as it gains TCP support.

@AndyLc
Copy link
Contributor Author

AndyLc commented May 18, 2023

@stv0g Sounds good, I attempted to add vnet, let me know what you think!

@stv0g
Copy link
Member

stv0g commented May 19, 2023

Hi @AndyLc,

cool :) I've found that we had two implementations of a client mock (dummyConnObverver and dummyClient). I refactored this into mockClient.

I also simplified this mock client by moving more of the static configuration attributes to AllocationConfig.
I've done so to keep the changes to the API surface of turn.Client to a minimum.

Copy link
Member

@stv0g stv0g left a comment

Choose a reason for hiding this comment

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

Hi @AndyLc,

I approved the PR now. Please feel free to pull the trigger for merging it if you are okay with my latest changes.

Copy link
Member

@stv0g stv0g left a comment

Choose a reason for hiding this comment

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

Hi @AndyLc,

I approved the PR now. Please feel free to pull the trigger for merging it if you are okay with my latest changes.

@AndyLc
Copy link
Contributor Author

AndyLc commented May 19, 2023

@stv0g Looks like I don't have access to merge the PR Only those with [write access](https://docs.github.com/articles/what-are-the-different-access-permissions) to this repository can merge pull requests. Your changes look good to me though!

@stv0g stv0g merged commit 521e5ad into pion:master May 19, 2023
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