coder / websocket

Minimal and idiomatic WebSocket library for Go
ISC License
3.86k stars 289 forks source link

How do I use websocket to tunnel gRPC through it? #251

Closed jprukner closed 4 years ago

jprukner commented 4 years ago

Hey, I've been trying to implement tunnel using this websocket lib. gRPC is supposed to go through the tunnel. I first tried to use https://github.com/dennwc/dom/tree/master/examples/grpc-over-ws. But that example does not compile to WASM with the latest go (1.14) due to changes in syscall/js.

The error I get in the browser with this library:

panic: rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing failed to WebSocket dial \"ws://localhost:8443\": context deadline exceeded"

It seems to hang here - https://github.com/nhooyr/websocket/blob/master/frame.go#L54. But no error is being printed.

Anyway, here is my implementation heavily inspired by the example from @dennwc:

Server:

package main

import (
    "flag"
    "fmt"
    "log"

    "xxx/pkg/api"
)

var port int

func init() {
    flag.IntVar(&port, "port", 8443, "Port of gRPC server.")
}
func main() {
    flag.Parse()
    // gRPC server
    server := api.GetServer()

    addr := fmt.Sprintf("localhost:%d", port)

    lis, err := api.Listen(addr)
    if err != nil {
        panic(err)
    }

    log.Println("listening on ", addr)

    err = server.Serve(lis)
    if err != nil {
        panic(err)
    }
}
package api

import (
    "xxx/pkg/generated"

    "google.golang.org/grpc"
)

// GetServer set's up grpc server and returns it
func GetServer() *grpc.Server {
    grpcServer := grpc.NewServer()

    generated.RegisterPublicServiceServer(grpcServer, &publicServer{})

    return grpcServer
}
package api

import (
    "fmt"
    "log"
    "net"
    "net/http"

    "nhooyr.io/websocket"
)

type wsListener struct {
    stop chan struct{}
    errc chan error
    conn chan net.Conn
    h    *http.Server
}

func Listen(addr string) (net.Listener, error) {
    srv := wsListener{
        stop: make(chan struct{}),
        errc: make(chan error, 1),
        conn: make(chan net.Conn),
    }
    // TODO: support HTTPS
    srv.h = &http.Server{
        Handler: srv,
    }

    lis, err := net.Listen("tcp", addr)
    if err != nil {
        return nil, err
    }
    go func() {
        defer close(srv.errc)
        srv.errc <- srv.h.Serve(lis)
    }()
    return srv, nil
}

func (w wsListener) ServeHTTP(wr http.ResponseWriter, r *http.Request) {
    c, err := websocket.Accept(wr, r, &websocket.AcceptOptions{
        InsecureSkipVerify: true,
    })

    if err != nil {
        log.Println(err)
        return
    }
    defer c.Close(websocket.StatusInternalError, "fail")

    ctx := r.Context()
    select {
    case <-w.stop:
        return
    default:
        w.conn <- websocket.NetConn(ctx, c, websocket.MessageBinary)
        select {
        // wait until wsListener is closed or when request is over
        case <-w.stop:
        case <-r.Context().Done():
        }
    }
    c.Close(websocket.StatusNormalClosure, "ok")
}

func (w wsListener) Accept() (net.Conn, error) {
    select {
    case <-w.stop:
        return nil, fmt.Errorf("server stopped")
    case err := <-w.errc:
        _ = w.Close()
        return nil, err
    case c := <-w.conn:
        return c, nil
    }
}

func (w wsListener) Close() error {
    select {
    case <-w.stop:
    default:
        close(w.stop)
    }
    if w.h != nil {
        return w.h.Close()
    }

    return nil
}

func (w wsListener) Addr() net.Addr {
    return net.Addr(nil)
}

Client

import (
    "time"
    "xxx/pkg/generated"
    "google.golang.org/grpc"
    "net"
    "time"
    "nhooyr.io/websocket"
    "context"
    "log"
    "sync"
)

type Root struct {
    ShowWasm bool `vugu:"data"`
    ShowGo bool   `vugu:"data"`
    ShowVugu bool `vugu:"data"`
    Msg string    `vugu:"data"`
}

func dialer(url string, dt time.Duration) (net.Conn, error) {
    ctx, cancel := context.WithTimeout(context.Background(), dt)
    defer cancel()
    c, _, err := websocket.Dial(ctx, url, nil)
    if err != nil {
        return nil, err
    }
    return websocket.NetConn(ctx, c, websocket.MessageBinary), nil
}

const host = "ws://localhost:8443"

func (c *Root) Hello(){
    wg := new(sync.WaitGroup)
    wg.Add(1)
    go func(){  
        conn, err := grpc.Dial(host, grpc.WithDialer(dialer), grpc.WithInsecure())
        if err != nil {
            panic(err)
        }
        defer conn.Close()
        cli := generated.NewPublicServiceClient(conn)

        user := generated.User{}
        status, err := cli.Login(context.TODO(), &user)
        if err != nil {
            panic(err)
        }
        c.Msg = status.Msg
        wg.Done()
    }()
    wg.Wait()

}
nhooyr commented 4 years ago

Try setting a timeout and see what happens.

Looks like there is no data being passed through so it's just stuck trying to read a frame.

nhooyr commented 4 years ago

Oh sorry I misread. I'll give your example a shot and let you know.

jprukner commented 4 years ago

I tried to run client via go run instead of using WASM in a browser. Noticed that websocket.NetConn takes context that determines a lifetime of the connection. I pass context that is canceled as soon as the dialer func exits in the example above. I fixed that and the client works when run this way - go run ....

It did not solve the issue with the WASM build as it blocks at websocket.Dial call.

jprukner commented 4 years ago

I figured something out. When I use this lib with Vugu, it does not work. When I compile to wasm using GOOS=js GOARCH=wasm go build -o main.wasm ... it works. I will share this with guys from Vugu.

nhooyr commented 4 years ago

Nice!

Glad there was no bug here 😅

joeblew99 commented 4 years ago

@jprukner are you using the improbable backend with grpcweb.WithWebsockets(true) ?

The server code in your example does not show it so I can't reproduce

jprukner commented 4 years ago

@joeblew99 It was not a bug in this library. It was an user error. I tried to use it together with Vugu and used it incorrectly. Please see https://github.com/vugu/vugu/issues/142.