Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 0e66c05

Browse files
authored
feat: Enable TURN proxying over WebSocket (#384)
* feat: Enable TURN proxying over WebSocket * Clean up API * Add net dependency * Close ws body * Add nop credentials to TURN candidate * Fix body close * Rename conn to dataChannelConn * Wrap websocket * Don't dial bad candidate * Refactor API * Fix deadline exceeding * Add comments * Try listing failed files * Organize imports * Fix test * Cleanup turnProxyConn impl
1 parent a8443d0 commit 0e66c05

File tree

11 files changed

+179
-106
lines changed

11 files changed

+179
-106
lines changed

ci/scripts/files_changed.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ cd "$(git rev-parse --show-toplevel)"
66

77
if [[ $(git ls-files --other --modified --exclude-standard) ]]; then
88
echo "Files have changed:"
9+
git ls-files --other --modified --exclude-standard
910
git -c color.ui=never status
1011
exit 1
1112
fi

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4
2424
github.com/rjeczalik/notify v0.9.2
2525
github.com/spf13/cobra v1.2.1
26+
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
2627
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
2728
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015
2829
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1

internal/cmd/agent.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ coder agent start --coder-url https://my-coder.com --token xxxx-xxxx
7373
}
7474
}
7575

76-
listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token))
76+
listener, err := wsnet.Listen(context.Background(), wsnet.ListenEndpoint(u, token), wsnet.TURNProxyWebSocket(u, token))
7777
if err != nil {
7878
return xerrors.Errorf("listen: %w", err)
7979
}

internal/cmd/tunnel.go

Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cmd
22

33
import (
44
"context"
5-
"errors"
65
"fmt"
76
"io"
87
"net"
@@ -12,7 +11,6 @@ import (
1211

1312
"cdr.dev/slog"
1413
"cdr.dev/slog/sloggers/sloghuman"
15-
"github.com/pion/webrtc/v3"
1614
"github.com/spf13/cobra"
1715
"golang.org/x/xerrors"
1816

@@ -104,30 +102,14 @@ type tunnneler struct {
104102
}
105103

106104
func (c *tunnneler) start(ctx context.Context) error {
107-
username, password, err := wsnet.TURNCredentials(c.token)
108-
if err != nil {
109-
return xerrors.Errorf("failed to parse credentials from token")
110-
}
111-
server := webrtc.ICEServer{
112-
URLs: []string{wsnet.TURNEndpoint(c.brokerAddr)},
113-
Username: username,
114-
Credential: password,
115-
CredentialType: webrtc.ICECredentialTypePassword,
116-
}
117-
118-
err = wsnet.DialICE(server, nil)
119-
if errors.Is(err, wsnet.ErrInvalidCredentials) {
120-
return xerrors.Errorf("failed to authenticate your user for this workspace")
121-
}
122-
if errors.Is(err, wsnet.ErrMismatchedProtocol) {
123-
return xerrors.Errorf("your TURN server is configured incorrectly. check TLS settings")
124-
}
125-
if err != nil {
126-
return xerrors.Errorf("dial ice: %w", err)
127-
}
128-
129105
c.log.Debug(ctx, "Connecting to workspace...")
130-
wd, err := wsnet.DialWebsocket(ctx, wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token), []webrtc.ICEServer{server})
106+
wd, err := wsnet.DialWebsocket(
107+
ctx,
108+
wsnet.ConnectEndpoint(c.brokerAddr, c.workspaceID, c.token),
109+
&wsnet.DialOptions{
110+
TURNProxy: wsnet.TURNProxyWebSocket(c.brokerAddr, c.token),
111+
},
112+
)
131113
if err != nil {
132114
return xerrors.Errorf("creating workspace dialer: %w", err)
133115
}

wsnet/auth.go

Lines changed: 0 additions & 22 deletions
This file was deleted.

wsnet/conn.go

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
package wsnet
22

33
import (
4+
"context"
45
"fmt"
56
"net"
7+
"net/http"
68
"net/url"
79
"sync"
810
"time"
911

1012
"github.com/pion/datachannel"
1113
"github.com/pion/webrtc/v3"
14+
"golang.org/x/net/proxy"
15+
"nhooyr.io/websocket"
16+
17+
"cdr.dev/coder-cli/coder-sdk"
1218
)
1319

1420
const (
@@ -22,16 +28,6 @@ const (
2228
maxMessageLength = 32 * 1024 // 32 KB
2329
)
2430

25-
// TURNEndpoint returns the TURN address for a Coder baseURL.
26-
func TURNEndpoint(baseURL *url.URL) string {
27-
turnScheme := "turns"
28-
if baseURL.Scheme == httpScheme {
29-
turnScheme = "turn"
30-
}
31-
32-
return fmt.Sprintf("%s:%s:5349?transport=tcp", turnScheme, baseURL.Hostname())
33-
}
34-
3531
// ListenEndpoint returns the Coder endpoint to listen for workspace connections.
3632
func ListenEndpoint(baseURL *url.URL, token string) string {
3733
wsScheme := "wss"
@@ -50,7 +46,80 @@ func ConnectEndpoint(baseURL *url.URL, workspace, token string) string {
5046
return fmt.Sprintf("%s://%s%s%s%s%s", wsScheme, baseURL.Host, "/api/private/envagent/", workspace, "/connect?session_token=", token)
5147
}
5248

53-
type conn struct {
49+
// TURNWebSocketICECandidate returns a valid relay ICEServer that can be used to
50+
// trigger a TURNWebSocketDialer.
51+
func TURNProxyICECandidate() webrtc.ICEServer {
52+
return webrtc.ICEServer{
53+
URLs: []string{"turn:127.0.0.1:3478?transport=tcp"},
54+
Username: "~magicalusername~",
55+
Credential: "~magicalpassword~",
56+
CredentialType: webrtc.ICECredentialTypePassword,
57+
}
58+
}
59+
60+
// TURNWebSocketDialer proxies all TURN traffic through a WebSocket.
61+
func TURNProxyWebSocket(baseURL *url.URL, token string) proxy.Dialer {
62+
return &turnProxyDialer{
63+
baseURL: baseURL,
64+
token: token,
65+
}
66+
}
67+
68+
// Proxies all TURN ICEServer traffic through this dialer.
69+
// References Coder APIs with a specific token.
70+
type turnProxyDialer struct {
71+
baseURL *url.URL
72+
token string
73+
}
74+
75+
func (t *turnProxyDialer) Dial(network, addr string) (c net.Conn, err error) {
76+
headers := http.Header{}
77+
headers.Set("Session-Token", t.token)
78+
79+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*15)
80+
defer cancel()
81+
82+
// Copy the baseURL so we can adjust path.
83+
url := *t.baseURL
84+
url.Scheme = "wss"
85+
if url.Scheme == httpScheme {
86+
url.Scheme = "ws"
87+
}
88+
url.Path = "/api/private/turn"
89+
conn, resp, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{
90+
HTTPHeader: headers,
91+
})
92+
if err != nil {
93+
if resp != nil {
94+
defer resp.Body.Close()
95+
return nil, coder.NewHTTPError(resp)
96+
}
97+
return nil, fmt.Errorf("dial: %w", err)
98+
}
99+
100+
return &turnProxyConn{
101+
websocket.NetConn(context.Background(), conn, websocket.MessageBinary),
102+
}, nil
103+
}
104+
105+
// turnProxyConn is a net.Conn wrapper that returns a TCPAddr for the
106+
// LocalAddr function. pion/ice unsafely checks the types. See:
107+
// https://github.com/pion/ice/blob/e78f26fb435987420546c70369ade5d713beca39/gather.go#L448
108+
type turnProxyConn struct {
109+
net.Conn
110+
}
111+
112+
// The LocalAddr specified here doesn't really matter,
113+
// it just has to be of type "TCPAddr".
114+
func (*turnProxyConn) LocalAddr() net.Addr {
115+
return &net.TCPAddr{
116+
IP: net.IPv4(127, 0, 0, 1),
117+
Port: 0,
118+
}
119+
}
120+
121+
// Properly buffers data for data channel connections.
122+
type dataChannelConn struct {
54123
addr *net.UnixAddr
55124
dc *webrtc.DataChannel
56125
rw datachannel.ReadWriteCloser
@@ -62,7 +131,7 @@ type conn struct {
62131
writeMutex sync.Mutex
63132
}
64133

65-
func (c *conn) init() {
134+
func (c *dataChannelConn) init() {
66135
c.sendMore = make(chan struct{}, 1)
67136
c.dc.SetBufferedAmountLowThreshold(bufferedAmountLowThreshold)
68137
c.dc.OnBufferedAmountLow(func() {
@@ -78,11 +147,11 @@ func (c *conn) init() {
78147
})
79148
}
80149

81-
func (c *conn) Read(b []byte) (n int, err error) {
150+
func (c *dataChannelConn) Read(b []byte) (n int, err error) {
82151
return c.rw.Read(b)
83152
}
84153

85-
func (c *conn) Write(b []byte) (n int, err error) {
154+
func (c *dataChannelConn) Write(b []byte) (n int, err error) {
86155
c.writeMutex.Lock()
87156
defer c.writeMutex.Unlock()
88157
if len(b) > maxMessageLength {
@@ -101,7 +170,7 @@ func (c *conn) Write(b []byte) (n int, err error) {
101170
return c.rw.Write(b)
102171
}
103172

104-
func (c *conn) Close() error {
173+
func (c *dataChannelConn) Close() error {
105174
c.closedMutex.Lock()
106175
defer c.closedMutex.Unlock()
107176
if !c.closed {
@@ -111,22 +180,22 @@ func (c *conn) Close() error {
111180
return c.dc.Close()
112181
}
113182

114-
func (c *conn) LocalAddr() net.Addr {
183+
func (c *dataChannelConn) LocalAddr() net.Addr {
115184
return c.addr
116185
}
117186

118-
func (c *conn) RemoteAddr() net.Addr {
187+
func (c *dataChannelConn) RemoteAddr() net.Addr {
119188
return c.addr
120189
}
121190

122-
func (c *conn) SetDeadline(t time.Time) error {
191+
func (c *dataChannelConn) SetDeadline(t time.Time) error {
123192
return nil
124193
}
125194

126-
func (c *conn) SetReadDeadline(t time.Time) error {
195+
func (c *dataChannelConn) SetReadDeadline(t time.Time) error {
127196
return nil
128197
}
129198

130-
func (c *conn) SetWriteDeadline(t time.Time) error {
199+
func (c *dataChannelConn) SetWriteDeadline(t time.Time) error {
131200
return nil
132201
}

wsnet/dial.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,26 @@ import (
1212

1313
"github.com/pion/datachannel"
1414
"github.com/pion/webrtc/v3"
15+
"golang.org/x/net/proxy"
1516
"nhooyr.io/websocket"
1617

1718
"cdr.dev/coder-cli/coder-sdk"
1819
)
1920

21+
// DialOptions are configurable options for a wsnet connection.
22+
type DialOptions struct {
23+
// ICEServers is an array of STUN or TURN servers to use for negotiation purposes.
24+
// See: https://developer.mozilla.org/en-US/docs/Web/API/RTCConfiguration/iceServers
25+
ICEServers []webrtc.ICEServer
26+
27+
// TURNProxy is a function used to proxy all TURN traffic.
28+
// If specified without ICEServers, `TURNProxyICECandidate`
29+
// will be used.
30+
TURNProxy proxy.Dialer
31+
}
32+
2033
// DialWebsocket dials the broker with a WebSocket and negotiates a connection.
21-
func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICEServer) (*Dialer, error) {
34+
func DialWebsocket(ctx context.Context, broker string, options *DialOptions) (*Dialer, error) {
2235
conn, resp, err := websocket.Dial(ctx, broker, nil)
2336
if err != nil {
2437
if resp != nil {
@@ -35,16 +48,24 @@ func DialWebsocket(ctx context.Context, broker string, iceServers []webrtc.ICESe
3548
// We should close the socket intentionally.
3649
_ = conn.Close(websocket.StatusInternalError, "an error occurred")
3750
}()
38-
return Dial(nconn, iceServers)
51+
return Dial(nconn, options)
3952
}
4053

4154
// Dial negotiates a connection to a listener.
42-
func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) {
43-
if iceServers == nil {
44-
iceServers = []webrtc.ICEServer{}
55+
func Dial(conn net.Conn, options *DialOptions) (*Dialer, error) {
56+
if options == nil {
57+
options = &DialOptions{}
58+
}
59+
if options.ICEServers == nil {
60+
options.ICEServers = []webrtc.ICEServer{}
61+
}
62+
// If the TURNProxy is specified and ICEServers aren't,
63+
// it's safe to assume we can inject the default proxy candidate.
64+
if len(options.ICEServers) == 0 && options.TURNProxy != nil {
65+
options.ICEServers = []webrtc.ICEServer{TURNProxyICECandidate()}
4566
}
4667

47-
rtc, err := newPeerConnection(iceServers)
68+
rtc, err := newPeerConnection(options.ICEServers, options.TURNProxy)
4869
if err != nil {
4970
return nil, fmt.Errorf("create peer connection: %w", err)
5071
}
@@ -70,7 +91,7 @@ func Dial(conn net.Conn, iceServers []webrtc.ICEServer) (*Dialer, error) {
7091

7192
offerMessage, err := json.Marshal(&BrokerMessage{
7293
Offer: &offer,
73-
Servers: iceServers,
94+
Servers: options.ICEServers,
7495
})
7596
if err != nil {
7697
return nil, fmt.Errorf("marshal offer message: %w", err)
@@ -287,7 +308,7 @@ func (d *Dialer) DialContext(ctx context.Context, network, address string) (net.
287308
return nil, ctx.Err()
288309
}
289310

290-
c := &conn{
311+
c := &dataChannelConn{
291312
addr: &net.UnixAddr{
292313
Name: address,
293314
Net: network,

0 commit comments

Comments
 (0)