From b2fb3fc46d4d5c807d56e91d3f1f4846fef5395a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 13 Jul 2021 20:21:34 +0000 Subject: [PATCH 01/16] feat: Enable TURN proxying over WebSocket --- wsnet/conn.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ wsnet/dial.go | 9 +++++---- wsnet/dial_test.go | 34 +++++++++++++++++----------------- wsnet/listen.go | 9 ++++++--- wsnet/listen_test.go | 2 +- wsnet/rtc.go | 9 ++++++++- 6 files changed, 81 insertions(+), 26 deletions(-) diff --git a/wsnet/conn.go b/wsnet/conn.go index b5dea0a5..2b95190c 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -1,14 +1,19 @@ package wsnet import ( + "context" "fmt" "net" + "net/http" "net/url" "sync" "time" + "cdr.dev/coder-cli/coder-sdk" "github.com/pion/datachannel" "github.com/pion/webrtc/v3" + "golang.org/x/net/proxy" + "nhooyr.io/websocket" ) const ( @@ -50,6 +55,45 @@ func ConnectEndpoint(baseURL *url.URL, workspace, token string) string { return fmt.Sprintf("%s://%s%s%s%s%s", wsScheme, baseURL.Host, "/api/private/envagent/", workspace, "/connect?session_token=", token) } +// TURNProxyDialer proxies all TURN traffic through a WebSocket for the workspace. +func TURNProxyDialer(baseURL *url.URL, token string) proxy.Dialer { + return &turnProxyDialer{ + baseURL: baseURL, + token: token, + } +} + +type turnProxyDialer struct { + baseURL *url.URL + token string +} + +func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { + headers := http.Header{} + headers.Set("Session-Token", t.token) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + + // Copy the baseURL so we can adjust path. + url := *t.baseURL + url.Scheme = "wss" + if url.Scheme == httpScheme { + url.Scheme = "ws" + } + url.Path = "/api/private/turn" + conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{ + HTTPHeader: headers, + }) + if err != nil { + if resp != nil { + return nil, coder.NewHTTPError(resp) + } + return nil, fmt.Errorf("dial: %w", err) + } + return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil +} + type conn struct { addr *net.UnixAddr dc *webrtc.DataChannel diff --git a/wsnet/dial.go b/wsnet/dial.go index c72dc513..67123dff 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -12,13 +12,14 @@ import ( "github.com/pion/datachannel" "github.com/pion/webrtc/v3" + "golang.org/x/net/proxy" "nhooyr.io/websocket" "cdr.dev/coder-cli/coder-sdk" ) // DialWebsocket dials the broker with a WebSocket and negotiates a connection. -func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICEServer) (*Dialer, error) { +func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICEServer, tcpProxy proxy.Dialer) (*Dialer, error) { conn, resp, err := websocket.Dial(ctx, broker, nil) if err != nil { if resp != nil { @@ -35,16 +36,16 @@ func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICESe // We should close the socket intentionally. _ = conn.Close(websocket.StatusInternalError, "an error occurred") }() - return Dial(nconn, iceServers) + return Dial(nconn, iceServers, tcpProxy) } // Dial negotiates a connection to a listener. -func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) { +func Dial(conn net.Conn, iceServers []webrtc.ICEServer, tcpProxy proxy.Dialer) (*Dialer, error) { if iceServers == nil { iceServers = []webrtc.ICEServer{} } - rtc, err := newPeerConnection(iceServers) + rtc, err := newPeerConnection(iceServers, tcpProxy) if err != nil { return nil, fmt.Errorf("create peer connection: %w", err) } diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 9b412a3e..e12eeed6 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -35,7 +35,7 @@ func ExampleDial_basic() { } } - dialer, err := DialWebsocket(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", servers) + dialer, err := DialWebsocket(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", servers, nil) if err != nil { // Do something... } @@ -51,12 +51,12 @@ func ExampleDial_basic() { func TestDial(t *testing.T) { t.Run("Ping", func(t *testing.T) { connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) if err != nil { t.Error(err) return @@ -69,12 +69,12 @@ func TestDial(t *testing.T) { t.Run("OPError", func(t *testing.T) { connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) if err != nil { t.Error(err) } @@ -106,12 +106,12 @@ func TestDial(t *testing.T) { }() connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), listenAddr) + _, err = Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) if err != nil { t.Error(err) return @@ -143,12 +143,12 @@ func TestDial(t *testing.T) { _, _ = listener.Accept() }() connectAddr, listenAddr := createDumbBroker(t) - srv, err := Listen(context.Background(), listenAddr) + srv, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) if err != nil { t.Error(err) } @@ -168,12 +168,12 @@ func TestDial(t *testing.T) { t.Run("Disconnect", func(t *testing.T) { connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) if err != nil { t.Error(err) return @@ -200,7 +200,7 @@ func TestDial(t *testing.T) { }() connectAddr, listenAddr := createDumbBroker(t) - _, err = Listen(context.Background(), listenAddr) + _, err = Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return @@ -211,7 +211,7 @@ func TestDial(t *testing.T) { Username: "example", Credential: testPass, CredentialType: webrtc.ICECredentialTypePassword, - }}) + }}, nil) if err != nil { t.Error(err) return @@ -233,12 +233,12 @@ func TestDial(t *testing.T) { t.Run("Closed", func(t *testing.T) { connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) if err != nil { t.Error(err) return @@ -284,13 +284,13 @@ func BenchmarkThroughput(b *testing.B) { } }() connectAddr, listenAddr := createDumbBroker(b) - _, err = Listen(context.Background(), listenAddr) + _, err = Listen(context.Background(), listenAddr, nil) if err != nil { b.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) if err != nil { b.Error(err) return diff --git a/wsnet/listen.go b/wsnet/listen.go index 3c7c3b3e..c2805994 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/yamux" "github.com/pion/webrtc/v3" + "golang.org/x/net/proxy" "nhooyr.io/websocket" "cdr.dev/coder-cli/coder-sdk" @@ -39,10 +40,11 @@ type DialChannelResponse struct { // Listen connects to the broker proxies connections to the local net. // Close will end all RTC connections. -func Listen(ctx context.Context, broker string) (io.Closer, error) { +func Listen(ctx context.Context, broker string, tcpProxy proxy.Dialer) (io.Closer, error) { l := &listener{ broker: broker, connClosers: make([]io.Closer, 0), + tcpProxy: tcpProxy, } // We do a one-off dial outside of the loop to ensure the initial // connection is successful. If not, there's likely an error the @@ -83,7 +85,8 @@ func Listen(ctx context.Context, broker string) (io.Closer, error) { } type listener struct { - broker string + broker string + tcpProxy proxy.Dialer acceptError error ws *websocket.Conn @@ -192,7 +195,7 @@ func (l *listener) negotiate(conn net.Conn) { return } } - rtc, err = newPeerConnection(msg.Servers) + rtc, err = newPeerConnection(msg.Servers, l.tcpProxy) if err != nil { closeError(err) return diff --git a/wsnet/listen_test.go b/wsnet/listen_test.go index 45519b92..47b856c3 100644 --- a/wsnet/listen_test.go +++ b/wsnet/listen_test.go @@ -45,7 +45,7 @@ func TestListen(t *testing.T) { addr := listener.Addr() broker := fmt.Sprintf("http://%s/", addr.String()) - _, err = Listen(context.Background(), broker) + _, err = Listen(context.Background(), broker, nil) if err != nil { t.Error(err) return diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 4d454311..aa95216f 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -17,6 +17,7 @@ import ( "github.com/pion/logging" "github.com/pion/turn/v2" "github.com/pion/webrtc/v3" + "golang.org/x/net/proxy" ) var ( @@ -154,7 +155,7 @@ func dialICEURL(server webrtc.ICEServer, rawURL string, options *DialICEOptions) } // Generalizes creating a new peer connection with consistent options. -func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, error) { +func newPeerConnection(servers []webrtc.ICEServer, dialer proxy.Dialer) (*webrtc.PeerConnection, error) { se := webrtc.SettingEngine{} se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeUDP4}) se.SetSrflxAcceptanceMinWait(0) @@ -164,6 +165,12 @@ func newPeerConnection(servers []webrtc.ICEServer) (*webrtc.PeerConnection, erro lf.DefaultLogLevel = logging.LogLevelDisabled se.LoggerFactory = lf + // Enables tunneling of TURN traffic through an arbitrary proxy. + // We proxy TURN over a WebSocket to reduce deployment complexity. + if dialer != nil { + se.SetICEProxyDialer(dialer) + } + transportPolicy := webrtc.ICETransportPolicyAll // If one server is provided and we know it's TURN, we can set the From 14dcb9aa642c9eae41fd2fbdfc7f5cc570cb84c9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 13 Jul 2021 20:36:35 +0000 Subject: [PATCH 02/16] Clean up API --- internal/cmd/agent.go | 6 +++++- internal/cmd/tunnel.go | 30 ++++++------------------------ wsnet/auth.go | 22 ---------------------- wsnet/conn.go | 22 ++++++++++------------ wsnet/rtc.go | 4 ++-- 5 files changed, 23 insertions(+), 61 deletions(-) delete mode 100644 wsnet/auth.go diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index 38853dd1..b69c746a 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -73,7 +73,11 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx } } - listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token)) + listener, err := wsnet.Listen( + context.Background(), + wsnet.ListenEndpoint(u, token), + wsnet.TURNWebSocketDialer(u, token), + ) if err != nil { return xerrors.Errorf("listen: %w", err) } diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index ae7fe14c..15ea5885 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -2,7 +2,6 @@ package cmd import ( "context" - "errors" "fmt" "io" "net" @@ -104,30 +103,13 @@ type tunnneler struct { } func (c *tunnneler) start(ctx context.Context) error { - username, password, err := wsnet.TURNCredentials(c.token) - if err != nil { - return xerrors.Errorf("failed to parse credentials from token") - } - server := webrtc.ICEServer{ - URLs: []string{wsnet.TURNEndpoint(c.brokerAddr)}, - Username: username, - Credential: password, - CredentialType: webrtc.ICECredentialTypePassword, - } - - err = wsnet.DialICE(server, nil) - if errors.Is(err, wsnet.ErrInvalidCredentials) { - return xerrors.Errorf("failed to authenticate your user for this workspace") - } - if errors.Is(err, wsnet.ErrMismatchedProtocol) { - return xerrors.Errorf("your TURN server is configured incorrectly. check TLS settings") - } - if err != nil { - return xerrors.Errorf("dial ice: %w", err) - } - c.log.Debug(ctx, "Connecting to workspace...") - wd, err := wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), []webrtc.ICEServer{server}) + wd, err := wsnet.DialWebsocket( + ctx, + wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), + []webrtc.ICEServer{wsnet.TURNWebSocketICECandidate()}, + wsnet.TURNWebSocketDialer(c.brokerAddr, c.token), + ) if err != nil { return xerrors.Errorf("creating workspace dialer: %w", err) } diff --git a/wsnet/auth.go b/wsnet/auth.go deleted file mode 100644 index a5daf45e..00000000 --- a/wsnet/auth.go +++ /dev/null @@ -1,22 +0,0 @@ -package wsnet - -import ( - "crypto/sha256" - "encoding/base64" - "errors" - "strings" -) - -// TURNCredentials returns a username and password pair -// for a Coder token. -func TURNCredentials(token string) (username, password string, err error) { - str := strings.SplitN(token, "-", 2) - if len(str) != 2 { - err = errors.New("invalid token format") - return - } - username = str[0] - hash := sha256.Sum256([]byte(str[1])) - password = base64.StdEncoding.EncodeToString(hash[:]) - return -} diff --git a/wsnet/conn.go b/wsnet/conn.go index 2b95190c..37a7f30d 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -27,16 +27,6 @@ const ( maxMessageLength = 32 * 1024 // 32 KB ) -// TURNEndpoint returns the TURN address for a Coder baseURL. -func TURNEndpoint(baseURL *url.URL) string { - turnScheme := "turns" - if baseURL.Scheme == httpScheme { - turnScheme = "turn" - } - - return fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, baseURL.Hostname()) -} - // ListenEndpoint returns the Coder endpoint to listen for workspace connections. func ListenEndpoint(baseURL *url.URL, token string) string { wsScheme := "wss" @@ -55,8 +45,16 @@ func ConnectEndpoint(baseURL *url.URL, workspace, token string) string { return fmt.Sprintf("%s://%s%s%s%s%s", wsScheme, baseURL.Host, "/api/private/envagent/", workspace, "/connect?session_token=", token) } -// TURNProxyDialer proxies all TURN traffic through a WebSocket for the workspace. -func TURNProxyDialer(baseURL *url.URL, token string) proxy.Dialer { +// TURNWebSocketICECandidate returns a valid relay ICEServer that can be used to +// trigger a TURNWebSocketDialer. +func TURNWebSocketICECandidate() webrtc.ICEServer { + return webrtc.ICEServer{ + URLs: []string{"turn:127.0.0.1:3478?transport=tcp"}, + } +} + +// TURNWebSocketDialer proxies all TURN traffic through a WebSocket for the workspace. +func TURNWebSocketDialer(baseURL *url.URL, token string) proxy.Dialer { return &turnProxyDialer{ baseURL: baseURL, token: token, diff --git a/wsnet/rtc.go b/wsnet/rtc.go index aa95216f..6e477988 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -177,9 +177,9 @@ func newPeerConnection(servers []webrtc.ICEServer, dialer proxy.Dialer) (*webrtc // relay acceptable so the connection starts immediately. if len(servers) == 1 { server := servers[0] - if server.Credential != nil && len(server.URLs) == 1 { + if len(server.URLs) == 1 { url, err := ice.ParseURL(server.URLs[0]) - if err == nil && url.Proto == ice.ProtoTypeTCP { + if err == nil && server.Credential != nil && url.Proto == ice.ProtoTypeTCP { se.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6}) se.SetRelayAcceptanceMinWait(0) } From bdc599b19f1f3c4f680ba6a3a756ccd43aa7adf2 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 13 Jul 2021 20:37:57 +0000 Subject: [PATCH 03/16] Add net dependency --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index 960091bb..46a9d7ce 100644 --- a/go.mod +++ b/go.mod @@ -23,6 +23,7 @@ require ( github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/rjeczalik/notify v0.9.2 github.com/spf13/cobra v1.2.1 + golang.org/x/net v0.0.0-20210614182718-04defd469f4e golang.org/x/sync v0.0.0-20210220032951-036812b2e83c golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 From b0894e4829cc2f4528d2b71d28d38abd00f16f7a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Tue, 13 Jul 2021 20:45:49 +0000 Subject: [PATCH 04/16] Close ws body --- wsnet/conn.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/wsnet/conn.go b/wsnet/conn.go index 37a7f30d..563f6028 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -89,6 +89,8 @@ func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { } return nil, fmt.Errorf("dial: %w", err) } + _ = resp.Body.Close() + return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil } From 3f845033d965dda41b6c93cdd87edf918950acb9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 00:04:51 +0000 Subject: [PATCH 05/16] Add nop credentials to TURN candidate --- wsnet/conn.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wsnet/conn.go b/wsnet/conn.go index 563f6028..1aaeafed 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -49,7 +49,10 @@ func ConnectEndpoint(baseURL *url.URL, workspace, token string) string { // trigger a TURNWebSocketDialer. func TURNWebSocketICECandidate() webrtc.ICEServer { return webrtc.ICEServer{ - URLs: []string{"turn:127.0.0.1:3478?transport=tcp"}, + URLs: []string{"turn:127.0.0.1:3478?transport=tcp"}, + Username: "nop", + Credential: "nop", + CredentialType: webrtc.ICECredentialTypePassword, } } From 0c06143806644500b10d4e395683c9540ae42f9a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 00:10:22 +0000 Subject: [PATCH 06/16] Fix body close --- wsnet/conn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsnet/conn.go b/wsnet/conn.go index 1aaeafed..9dd0e1b2 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -88,11 +88,11 @@ func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { }) if err != nil { if resp != nil { + defer resp.Body.Close() return nil, coder.NewHTTPError(resp) } return nil, fmt.Errorf("dial: %w", err) } - _ = resp.Body.Close() return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil } From 0d32c1be7b5b18eeb8f7745e5645cd36b30dc39e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 00:16:08 +0000 Subject: [PATCH 07/16] Rename conn to dataChannelConn --- wsnet/conn.go | 20 ++++++++++---------- wsnet/dial.go | 2 +- wsnet/listen.go | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/wsnet/conn.go b/wsnet/conn.go index 9dd0e1b2..ce974111 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -97,7 +97,7 @@ func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil } -type conn struct { +type dataChannelConn struct { addr *net.UnixAddr dc *webrtc.DataChannel rw datachannel.ReadWriteCloser @@ -109,7 +109,7 @@ type conn struct { writeMutex sync.Mutex } -func (c *conn) init() { +func (c *dataChannelConn) init() { c.sendMore = make(chan struct{}, 1) c.dc.SetBufferedAmountLowThreshold(bufferedAmountLowThreshold) c.dc.OnBufferedAmountLow(func() { @@ -125,11 +125,11 @@ func (c *conn) init() { }) } -func (c *conn) Read(b []byte) (n int, err error) { +func (c *dataChannelConn) Read(b []byte) (n int, err error) { return c.rw.Read(b) } -func (c *conn) Write(b []byte) (n int, err error) { +func (c *dataChannelConn) Write(b []byte) (n int, err error) { c.writeMutex.Lock() defer c.writeMutex.Unlock() if len(b) > maxMessageLength { @@ -148,7 +148,7 @@ func (c *conn) Write(b []byte) (n int, err error) { return c.rw.Write(b) } -func (c *conn) Close() error { +func (c *dataChannelConn) Close() error { c.closedMutex.Lock() defer c.closedMutex.Unlock() if !c.closed { @@ -158,22 +158,22 @@ func (c *conn) Close() error { return c.dc.Close() } -func (c *conn) LocalAddr() net.Addr { +func (c *dataChannelConn) LocalAddr() net.Addr { return c.addr } -func (c *conn) RemoteAddr() net.Addr { +func (c *dataChannelConn) RemoteAddr() net.Addr { return c.addr } -func (c *conn) SetDeadline(t time.Time) error { +func (c *dataChannelConn) SetDeadline(t time.Time) error { return nil } -func (c *conn) SetReadDeadline(t time.Time) error { +func (c *dataChannelConn) SetReadDeadline(t time.Time) error { return nil } -func (c *conn) SetWriteDeadline(t time.Time) error { +func (c *dataChannelConn) SetWriteDeadline(t time.Time) error { return nil } diff --git a/wsnet/dial.go b/wsnet/dial.go index 67123dff..fae628fb 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -288,7 +288,7 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net. return nil, ctx.Err() } - c := &conn{ + c := &dataChannelConn{ addr: &net.UnixAddr{ Name: address, Net: network, diff --git a/wsnet/listen.go b/wsnet/listen.go index c2805994..777efb21 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -329,7 +329,7 @@ func (l *listener) handle(msg BrokerMessage) func(dc *webrtc.DataChannel) { } // Must wrap the data channel inside this connection // for buffering from the dialed endpoint to the client. - co := &conn{ + co := &dataChannelConn{ addr: nil, dc: dc, rw: rw, From 397477f9a61fdd89ffed4eceb831efee3982a85d Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 00:21:41 +0000 Subject: [PATCH 08/16] Wrap websocket --- wsnet/conn.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/wsnet/conn.go b/wsnet/conn.go index ce974111..f3f7921c 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -94,7 +94,54 @@ func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { return nil, fmt.Errorf("dial: %w", err) } - return websocket.NetConn(ctx, conn, websocket.MessageBinary), nil + return &turnProxyConn{ + conn: websocket.NetConn(ctx, conn, websocket.MessageBinary), + }, nil +} + +// turnProxyConn is a net.Conn wrapper that returns a TCPAddr for the +// LocalAddr and RemoteAddr functions. pion/ice unsafely checks the +// types. See: https://github.com/pion/ice/blob/e78f26fb435987420546c70369ade5d713beca39/gather.go#L448 +type turnProxyConn struct { + conn net.Conn +} + +func (t *turnProxyConn) Read(b []byte) (n int, err error) { + return t.conn.Read(b) +} + +func (t *turnProxyConn) Write(b []byte) (n int, err error) { + return t.conn.Write(b) +} + +func (t *turnProxyConn) Close() error { + return t.conn.Close() +} + +func (t *turnProxyConn) LocalAddr() net.Addr { + return &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 0, + } +} + +func (t *turnProxyConn) RemoteAddr() net.Addr { + return &net.TCPAddr{ + IP: net.IPv4(127, 0, 0, 1), + Port: 0, + } +} + +func (t *turnProxyConn) SetDeadline(time time.Time) error { + return t.SetDeadline(time) +} + +func (t *turnProxyConn) SetReadDeadline(time time.Time) error { + return t.SetReadDeadline(time) +} + +func (t *turnProxyConn) SetWriteDeadline(time time.Time) error { + return t.SetWriteDeadline(time) } type dataChannelConn struct { From 31fed4dee7f917c98d23e19662ecf5a0173e19e6 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 00:36:30 +0000 Subject: [PATCH 09/16] Don't dial bad candidate --- wsnet/rtc.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 6e477988..8697b2a3 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -46,6 +46,10 @@ func DialICE(server webrtc.ICEServer, options *DialICEOptions) error { if options == nil { options = &DialICEOptions{} } + if server.Username == TURNWebSocketICECandidate().Username { + // Don't validate if the proxy candidate can dial. + return nil + } for _, rawURL := range server.URLs { err := dialICEURL(server, rawURL, options) From f92bb430b443fc7c0b17250fd369d7caaa4a6b23 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 01:29:59 +0000 Subject: [PATCH 10/16] Refactor API --- internal/cmd/agent.go | 2 +- internal/cmd/tunnel.go | 6 +++--- wsnet/conn.go | 16 ++++++++-------- wsnet/dial.go | 32 +++++++++++++++++++++++++------- wsnet/dial_test.go | 32 ++++++++++++++++++-------------- wsnet/listen.go | 5 +++++ wsnet/rtc.go | 4 ---- 7 files changed, 60 insertions(+), 37 deletions(-) diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index b69c746a..605a5d79 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -76,7 +76,7 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx listener, err := wsnet.Listen( context.Background(), wsnet.ListenEndpoint(u, token), - wsnet.TURNWebSocketDialer(u, token), + wsnet.TURNProxyWebSocket(u, token), ) if err != nil { return xerrors.Errorf("listen: %w", err) diff --git a/internal/cmd/tunnel.go b/internal/cmd/tunnel.go index 15ea5885..7b14cf33 100644 --- a/internal/cmd/tunnel.go +++ b/internal/cmd/tunnel.go @@ -11,7 +11,6 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" - "github.com/pion/webrtc/v3" "github.com/spf13/cobra" "golang.org/x/xerrors" @@ -107,8 +106,9 @@ func (c *tunnneler) start(ctx context.Context) error { wd, err := wsnet.DialWebsocket( ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), - []webrtc.ICEServer{wsnet.TURNWebSocketICECandidate()}, - wsnet.TURNWebSocketDialer(c.brokerAddr, c.token), + &wsnet.DialOptions{ + TURNProxy: wsnet.TURNProxyWebSocket(c.brokerAddr, c.token), + }, ) if err != nil { return xerrors.Errorf("creating workspace dialer: %w", err) diff --git a/wsnet/conn.go b/wsnet/conn.go index f3f7921c..6b697313 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -47,17 +47,17 @@ func ConnectEndpoint(baseURL *url.URL, workspace, token string) string { // TURNWebSocketICECandidate returns a valid relay ICEServer that can be used to // trigger a TURNWebSocketDialer. -func TURNWebSocketICECandidate() webrtc.ICEServer { +func TURNProxyICECandidate() webrtc.ICEServer { return webrtc.ICEServer{ URLs: []string{"turn:127.0.0.1:3478?transport=tcp"}, - Username: "nop", - Credential: "nop", + Username: "~magicalusername~", + Credential: "~magicalpassword~", CredentialType: webrtc.ICECredentialTypePassword, } } -// TURNWebSocketDialer proxies all TURN traffic through a WebSocket for the workspace. -func TURNWebSocketDialer(baseURL *url.URL, token string) proxy.Dialer { +// TURNWebSocketDialer proxies all TURN traffic through a WebSocket. +func TURNProxyWebSocket(baseURL *url.URL, token string) proxy.Dialer { return &turnProxyDialer{ baseURL: baseURL, token: token, @@ -133,15 +133,15 @@ func (t *turnProxyConn) RemoteAddr() net.Addr { } func (t *turnProxyConn) SetDeadline(time time.Time) error { - return t.SetDeadline(time) + return t.conn.SetDeadline(time) } func (t *turnProxyConn) SetReadDeadline(time time.Time) error { - return t.SetReadDeadline(time) + return t.conn.SetReadDeadline(time) } func (t *turnProxyConn) SetWriteDeadline(time time.Time) error { - return t.SetWriteDeadline(time) + return t.conn.SetWriteDeadline(time) } type dataChannelConn struct { diff --git a/wsnet/dial.go b/wsnet/dial.go index fae628fb..abc9330e 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -18,8 +18,20 @@ import ( "cdr.dev/coder-cli/coder-sdk" ) +// DialOptions are configurable options for a wsnet connection. +type DialOptions struct { + // ICEServers is an array of STUN or TURN servers to use for negotiation purposes. + // See: https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration/iceServers + ICEServers []webrtc.ICEServer + + // TURNProxy is a function used to proxy all TURN traffic. + // If specified without ICEServers, `TURNProxyICECandidate` + // will be used. + TURNProxy proxy.Dialer +} + // DialWebsocket dials the broker with a WebSocket and negotiates a connection. -func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICEServer, tcpProxy proxy.Dialer) (*Dialer, error) { +func DialWebsocket(ctx context.Context, broker string, options *DialOptions) (*Dialer, error) { conn, resp, err := websocket.Dial(ctx, broker, nil) if err != nil { if resp != nil { @@ -36,16 +48,22 @@ func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICESe // We should close the socket intentionally. _ = conn.Close(websocket.StatusInternalError, "an error occurred") }() - return Dial(nconn, iceServers, tcpProxy) + return Dial(nconn, options) } // Dial negotiates a connection to a listener. -func Dial(conn net.Conn, iceServers []webrtc.ICEServer, tcpProxy proxy.Dialer) (*Dialer, error) { - if iceServers == nil { - iceServers = []webrtc.ICEServer{} +func Dial(conn net.Conn, options *DialOptions) (*Dialer, error) { + if options == nil { + options = &DialOptions{} + } + if options.ICEServers == nil { + options.ICEServers = []webrtc.ICEServer{} + } + if len(options.ICEServers) == 0 && options.TURNProxy != nil { + options.ICEServers = []webrtc.ICEServer{TURNProxyICECandidate()} } - rtc, err := newPeerConnection(iceServers, tcpProxy) + rtc, err := newPeerConnection(options.ICEServers, options.TURNProxy) if err != nil { return nil, fmt.Errorf("create peer connection: %w", err) } @@ -71,7 +89,7 @@ func Dial(conn net.Conn, iceServers []webrtc.ICEServer, tcpProxy proxy.Dialer) ( offerMessage, err := json.Marshal(&BrokerMessage{ Offer: &offer, - Servers: iceServers, + Servers: options.ICEServers, }) if err != nil { return nil, fmt.Errorf("marshal offer message: %w", err) diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index e12eeed6..b7f4a92d 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -35,7 +35,9 @@ func ExampleDial_basic() { } } - dialer, err := DialWebsocket(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", servers, nil) + dialer, err := DialWebsocket(context.Background(), "wss://master.cdr.dev/agent/workspace/connect", &DialOptions{ + ICEServers: servers, + }) if err != nil { // Do something... } @@ -56,7 +58,7 @@ func TestDial(t *testing.T) { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) return @@ -74,7 +76,7 @@ func TestDial(t *testing.T) { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) } @@ -111,7 +113,7 @@ func TestDial(t *testing.T) { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) return @@ -148,7 +150,7 @@ func TestDial(t *testing.T) { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) } @@ -173,7 +175,7 @@ func TestDial(t *testing.T) { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) return @@ -206,12 +208,14 @@ func TestDial(t *testing.T) { return } turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) - dialer, err := DialWebsocket(context.Background(), connectAddr, []webrtc.ICEServer{{ - URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, - Username: "example", - Credential: testPass, - CredentialType: webrtc.ICECredentialTypePassword, - }}, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + ICEServers: []webrtc.ICEServer{{ + URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, + Username: "example", + Credential: testPass, + CredentialType: webrtc.ICECredentialTypePassword, + }}, + }) if err != nil { t.Error(err) return @@ -238,7 +242,7 @@ func TestDial(t *testing.T) { t.Error(err) return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { t.Error(err) return @@ -290,7 +294,7 @@ func BenchmarkThroughput(b *testing.B) { return } - dialer, err := DialWebsocket(context.Background(), connectAddr, nil, nil) + dialer, err := DialWebsocket(context.Background(), connectAddr, nil) if err != nil { b.Error(err) return diff --git a/wsnet/listen.go b/wsnet/listen.go index 777efb21..b29bbdb3 100644 --- a/wsnet/listen.go +++ b/wsnet/listen.go @@ -189,6 +189,11 @@ func (l *listener) negotiate(conn net.Conn) { return } for _, server := range msg.Servers { + if server.Username == TURNProxyICECandidate().Username { + // This candidate is only used when proxying, + // so it will not validate. + continue + } err = DialICE(server, nil) if err != nil { closeError(fmt.Errorf("dial server %+v: %w", server.URLs, err)) diff --git a/wsnet/rtc.go b/wsnet/rtc.go index 8697b2a3..6e477988 100644 --- a/wsnet/rtc.go +++ b/wsnet/rtc.go @@ -46,10 +46,6 @@ func DialICE(server webrtc.ICEServer, options *DialICEOptions) error { if options == nil { options = &DialICEOptions{} } - if server.Username == TURNWebSocketICECandidate().Username { - // Don't validate if the proxy candidate can dial. - return nil - } for _, rawURL := range server.URLs { err := dialICEURL(server, rawURL, options) From 57cab76b9de363482ebfd36627fbbdcf03fc225e Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 02:35:41 +0000 Subject: [PATCH 11/16] Fix deadline exceeding --- wsnet/conn.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/wsnet/conn.go b/wsnet/conn.go index 6b697313..e6a29a4d 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -95,7 +95,7 @@ func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { } return &turnProxyConn{ - conn: websocket.NetConn(ctx, conn, websocket.MessageBinary), + conn: websocket.NetConn(context.Background(), conn, websocket.MessageBinary), }, nil } @@ -126,10 +126,7 @@ func (t *turnProxyConn) LocalAddr() net.Addr { } func (t *turnProxyConn) RemoteAddr() net.Addr { - return &net.TCPAddr{ - IP: net.IPv4(127, 0, 0, 1), - Port: 0, - } + return t.conn.RemoteAddr() } func (t *turnProxyConn) SetDeadline(time time.Time) error { From 4cb7187b2673997b413c1984bcced37fe7fb0ca3 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 02:58:05 +0000 Subject: [PATCH 12/16] Add comments --- internal/cmd/agent.go | 6 +----- wsnet/conn.go | 5 +++++ wsnet/dial.go | 2 ++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/cmd/agent.go b/internal/cmd/agent.go index 605a5d79..c19bdfee 100644 --- a/internal/cmd/agent.go +++ b/internal/cmd/agent.go @@ -73,11 +73,7 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx } } - listener, err := wsnet.Listen( - context.Background(), - wsnet.ListenEndpoint(u, token), - wsnet.TURNProxyWebSocket(u, token), - ) + listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token), wsnet.TURNProxyWebSocket(u, token)) if err != nil { return xerrors.Errorf("listen: %w", err) } diff --git a/wsnet/conn.go b/wsnet/conn.go index e6a29a4d..a4e59696 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -64,6 +64,8 @@ func TURNProxyWebSocket(baseURL *url.URL, token string) proxy.Dialer { } } +// Proxies all TURN ICEServer traffic through this dialer. +// References Coder APIs with a specific token. type turnProxyDialer struct { baseURL *url.URL token string @@ -118,6 +120,8 @@ func (t *turnProxyConn) Close() error { return t.conn.Close() } +// The LocalAddr specified here doesn't really matter, +// it just has to be of type "TCPAddr". func (t *turnProxyConn) LocalAddr() net.Addr { return &net.TCPAddr{ IP: net.IPv4(127, 0, 0, 1), @@ -141,6 +145,7 @@ func (t *turnProxyConn) SetWriteDeadline(time time.Time) error { return t.conn.SetWriteDeadline(time) } +// Properly buffers data for data channel connections. type dataChannelConn struct { addr *net.UnixAddr dc *webrtc.DataChannel diff --git a/wsnet/dial.go b/wsnet/dial.go index abc9330e..e4bbd69c 100644 --- a/wsnet/dial.go +++ b/wsnet/dial.go @@ -59,6 +59,8 @@ func Dial(conn net.Conn, options *DialOptions) (*Dialer, error) { if options.ICEServers == nil { options.ICEServers = []webrtc.ICEServer{} } + // If the TURNProxy is specified and ICEServers aren't, + // it's safe to assume we can inject the default proxy candidate. if len(options.ICEServers) == 0 && options.TURNProxy != nil { options.ICEServers = []webrtc.ICEServer{TURNProxyICECandidate()} } From 70f78c8b67fd0a4efea04f2f419be53ccc1ba97a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 03:05:09 +0000 Subject: [PATCH 13/16] Try listing failed files --- ci/scripts/files_changed.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/scripts/files_changed.sh b/ci/scripts/files_changed.sh index 759c68d3..490cb5ad 100755 --- a/ci/scripts/files_changed.sh +++ b/ci/scripts/files_changed.sh @@ -6,6 +6,7 @@ cd "$(git rev-parse --show-toplevel)" if [[ $(git ls-files --other --modified --exclude-standard) ]]; then echo "Files have changed:" + git ls-files --other --modified --exclude-standard git -c color.ui=never status exit 1 fi From 86028e0a0cda3a3edd88bef61b6286fe70ac1737 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 03:06:37 +0000 Subject: [PATCH 14/16] Organize imports --- wsnet/conn.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wsnet/conn.go b/wsnet/conn.go index a4e59696..584226af 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -9,11 +9,12 @@ import ( "sync" "time" - "cdr.dev/coder-cli/coder-sdk" "github.com/pion/datachannel" "github.com/pion/webrtc/v3" "golang.org/x/net/proxy" "nhooyr.io/websocket" + + "cdr.dev/coder-cli/coder-sdk" ) const ( From 403ed8418cf8edc1ce991df964b928fc7c75bfa9 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 03:22:06 +0000 Subject: [PATCH 15/16] Fix test --- wsnet/dial_test.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/wsnet/dial_test.go b/wsnet/dial_test.go index 4994399c..6ad27866 100644 --- a/wsnet/dial_test.go +++ b/wsnet/dial_test.go @@ -75,18 +75,20 @@ func TestDial(t *testing.T) { t.Parallel() connectAddr, listenAddr := createDumbBroker(t) - _, err := Listen(context.Background(), listenAddr) + _, err := Listen(context.Background(), listenAddr, nil) if err != nil { t.Error(err) return } turnAddr, closeTurn := createTURNServer(t, ice.SchemeTypeTURN) - dialer, err := DialWebsocket(context.Background(), connectAddr, []webrtc.ICEServer{{ - URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, - Username: "example", - Credential: testPass, - CredentialType: webrtc.ICECredentialTypePassword, - }}) + dialer, err := DialWebsocket(context.Background(), connectAddr, &DialOptions{ + ICEServers: []webrtc.ICEServer{{ + URLs: []string{fmt.Sprintf("turn:%s", turnAddr)}, + Username: "example", + Credential: testPass, + CredentialType: webrtc.ICECredentialTypePassword, + }}, + }) if err != nil { t.Error(err) return From d75849db108066fdf7e82985d8f3bcf5c7c87548 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 14 Jul 2021 04:21:15 +0000 Subject: [PATCH 16/16] Cleanup turnProxyConn impl --- wsnet/conn.go | 38 +++++--------------------------------- 1 file changed, 5 insertions(+), 33 deletions(-) diff --git a/wsnet/conn.go b/wsnet/conn.go index 584226af..608c5c70 100644 --- a/wsnet/conn.go +++ b/wsnet/conn.go @@ -98,54 +98,26 @@ func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) { } return &turnProxyConn{ - conn: websocket.NetConn(context.Background(), conn, websocket.MessageBinary), + websocket.NetConn(context.Background(), conn, websocket.MessageBinary), }, nil } // turnProxyConn is a net.Conn wrapper that returns a TCPAddr for the -// LocalAddr and RemoteAddr functions. pion/ice unsafely checks the -// types. See: https://github.com/pion/ice/blob/e78f26fb435987420546c70369ade5d713beca39/gather.go#L448 +// LocalAddr function. pion/ice unsafely checks the types. See: +// https://github.com/pion/ice/blob/e78f26fb435987420546c70369ade5d713beca39/gather.go#L448 type turnProxyConn struct { - conn net.Conn -} - -func (t *turnProxyConn) Read(b []byte) (n int, err error) { - return t.conn.Read(b) -} - -func (t *turnProxyConn) Write(b []byte) (n int, err error) { - return t.conn.Write(b) -} - -func (t *turnProxyConn) Close() error { - return t.conn.Close() + net.Conn } // The LocalAddr specified here doesn't really matter, // it just has to be of type "TCPAddr". -func (t *turnProxyConn) LocalAddr() net.Addr { +func (*turnProxyConn) LocalAddr() net.Addr { return &net.TCPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 0, } } -func (t *turnProxyConn) RemoteAddr() net.Addr { - return t.conn.RemoteAddr() -} - -func (t *turnProxyConn) SetDeadline(time time.Time) error { - return t.conn.SetDeadline(time) -} - -func (t *turnProxyConn) SetReadDeadline(time time.Time) error { - return t.conn.SetReadDeadline(time) -} - -func (t *turnProxyConn) SetWriteDeadline(time time.Time) error { - return t.conn.SetWriteDeadline(time) -} - // Properly buffers data for data channel connections. type dataChannelConn struct { addr *net.UnixAddr