From 5ea16e63a1574ca2e4e07c52d167350241a6ff1c Mon Sep 17 00:00:00 2001 From: Vasyl Gello Date: Tue, 23 Jul 2024 21:58:11 +0000 Subject: [PATCH] Implement websocket (ws:// and wss://) links (#1152) ws:// can be listened and dialed wss:// is a convenience link for ws:// that supports dialing to ws:// peer. --------- Signed-off-by: Vasyl Gello Co-authored-by: Neil Alexander --- go.mod | 1 + go.sum | 2 + src/core/link.go | 12 +++++ src/core/link_ws.go | 123 +++++++++++++++++++++++++++++++++++++++++++ src/core/link_wss.go | 43 +++++++++++++++ 5 files changed, 181 insertions(+) create mode 100644 src/core/link_ws.go create mode 100644 src/core/link_wss.go diff --git a/go.mod b/go.mod index d32d1143..5ff4479a 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( golang.org/x/text v0.16.0 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard/windows v0.5.3 + nhooyr.io/websocket v1.8.11 ) require ( diff --git a/go.sum b/go.sum index be42c112..1cb8f5b8 100644 --- a/go.sum +++ b/go.sum @@ -154,3 +154,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= +nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/src/core/link.go b/src/core/link.go index b646605c..f45c2cee 100644 --- a/src/core/link.go +++ b/src/core/link.go @@ -37,6 +37,8 @@ type links struct { unix *linkUNIX // UNIX interface support socks *linkSOCKS // SOCKS interface support quic *linkQUIC // QUIC interface support + ws *linkWS // WS interface support + wss *linkWSS // WSS interface support // _links can only be modified safely from within the links actor _links map[linkInfo]*link // *link is nil if connection in progress } @@ -97,6 +99,8 @@ func (l *links) init(c *Core) error { l.unix = l.newLinkUNIX() l.socks = l.newLinkSOCKS() l.quic = l.newLinkQUIC() + l.ws = l.newLinkWS() + l.wss = l.newLinkWSS() l._links = make(map[linkInfo]*link) var listeners []ListenAddress @@ -417,6 +421,10 @@ func (l *links) listen(u *url.URL, sintf string) (*Listener, error) { protocol = l.unix case "quic": protocol = l.quic + case "ws": + protocol = l.ws + case "wss": + protocol = l.wss default: cancel() return nil, ErrLinkUnrecognisedSchema @@ -545,6 +553,10 @@ func (l *links) connect(ctx context.Context, u *url.URL, info linkInfo, options dialer = l.unix case "quic": dialer = l.quic + case "ws": + dialer = l.ws + case "wss": + dialer = l.wss default: return nil, ErrLinkUnrecognisedSchema } diff --git a/src/core/link_ws.go b/src/core/link_ws.go new file mode 100644 index 00000000..f323b025 --- /dev/null +++ b/src/core/link_ws.go @@ -0,0 +1,123 @@ +package core + +import ( + "context" + "net" + "net/http" + "net/url" + "time" + + "github.com/Arceliar/phony" + "nhooyr.io/websocket" +) + +type linkWS struct { + phony.Inbox + *links +} + +type linkWSConn struct { + net.Conn +} + +type linkWSListener struct { + ch chan *linkWSConn + ctx context.Context + httpServer *http.Server + listener net.Listener +} + +type wsServer struct { + ch chan *linkWSConn + ctx context.Context +} + +func (l *linkWSListener) Accept() (net.Conn, error) { + qs := <-l.ch + if qs == nil { + return nil, context.Canceled + } + return qs, nil +} + +func (l *linkWSListener) Addr() net.Addr { + return l.listener.Addr() +} + +func (l *linkWSListener) Close() error { + if err := l.httpServer.Shutdown(l.ctx); err != nil { + return err + } + return l.listener.Close() +} + +func (s *wsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/health" || r.URL.Path == "/healthz" { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + return + } + + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"ygg-ws"}, + }) + if err != nil { + return + } + + if c.Subprotocol() != "ygg-ws" { + c.Close(websocket.StatusPolicyViolation, "client must speak the ygg-ws subprotocol") + return + } + + s.ch <- &linkWSConn{ + Conn: websocket.NetConn(s.ctx, c, websocket.MessageBinary), + } +} + +func (l *links) newLinkWS() *linkWS { + lt := &linkWS{ + links: l, + } + return lt +} + +func (l *linkWS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + wsconn, _, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{ + Subprotocols: []string{"ygg-ws"}, + }) + if err != nil { + return nil, err + } + return &linkWSConn{ + Conn: websocket.NetConn(ctx, wsconn, websocket.MessageBinary), + }, nil +} + +func (l *linkWS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) { + nl, err := net.Listen("tcp", url.Host) + if err != nil { + return nil, err + } + + ch := make(chan *linkWSConn) + + httpServer := &http.Server{ + Handler: &wsServer{ + ch: ch, + ctx: ctx, + }, + BaseContext: func(_ net.Listener) context.Context { return ctx }, + ReadTimeout: time.Second * 10, + WriteTimeout: time.Second * 10, + } + + lwl := &linkWSListener{ + ch: ch, + ctx: ctx, + httpServer: httpServer, + listener: nl, + } + go lwl.httpServer.Serve(nl) // nolint:errcheck + return lwl, nil +} diff --git a/src/core/link_wss.go b/src/core/link_wss.go new file mode 100644 index 00000000..a9a8df24 --- /dev/null +++ b/src/core/link_wss.go @@ -0,0 +1,43 @@ +package core + +import ( + "context" + "fmt" + "net" + "net/url" + + "github.com/Arceliar/phony" + "nhooyr.io/websocket" +) + +type linkWSS struct { + phony.Inbox + *links +} + +type linkWSSConn struct { + net.Conn +} + +func (l *links) newLinkWSS() *linkWSS { + lwss := &linkWSS{ + links: l, + } + return lwss +} + +func (l *linkWSS) dial(ctx context.Context, url *url.URL, info linkInfo, options linkOptions) (net.Conn, error) { + wsconn, _, err := websocket.Dial(ctx, url.String(), &websocket.DialOptions{ + Subprotocols: []string{"ygg-ws"}, + }) + if err != nil { + return nil, err + } + return &linkWSSConn{ + Conn: websocket.NetConn(ctx, wsconn, websocket.MessageBinary), + }, nil +} + +func (l *linkWSS) listen(ctx context.Context, url *url.URL, _ string) (net.Listener, error) { + return nil, fmt.Errorf("WSS listener not supported, use WS listener behind reverse proxy instead") +}