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

[question] How to implement io.ReadWriter on websocket.Conn? #588

Closed
bdwyertech opened this issue Apr 16, 2020 · 11 comments
Closed

[question] How to implement io.ReadWriter on websocket.Conn? #588

bdwyertech opened this issue Apr 16, 2020 · 11 comments
Labels

Comments

@bdwyertech
Copy link

bdwyertech commented Apr 16, 2020

Hello!

I have written an NTLM Proxy Forwarder (https://github.com/bdwyertech/gontlm-proxy) and I am trying to implement WebSocket support.

I lean on elazarl/goproxy heavily. I am attempting to add NTLM proxy support to the websocket implementation.
https://github.com/elazarl/goproxy/blob/master/websocket.go#L28-L47

It leverages a basic io.Copy(dst, src) between the client and target connections. I am wondering how I can implement an io.ReadWriter interface on a websocket.Conn.

Thank you kindly for any insight you could provide!

Brian

        *websocket.Conn does not implement io.ReadWriter (missing Read method)
                have websocket.read(int) ([]byte, error)
                want Read([]byte) (int, error)
@bdwyertech bdwyertech changed the title [question] How to implement io.ReadWriter? [question] How to implement io.ReadWriter on websocket.Conn? Apr 16, 2020
@ghost
Copy link

ghost commented Apr 16, 2020

The basic io.Copy(dst, src) does not in general make sense to use with WebSocket connections because the operation will discard message boundaries.

An NTLM proxy should operate at the net.Conn layer. Use Dialer.NetDialContext or the HTTP network listener to plumb an NTLM connection below the websocket layer.

The websocket protocol can be proxied with little or no knowledge of the WebSocket protocol. That's the approach taken by the elazarl/goproxy package. The package facilitates the WebSocket handshake and then pumps bytes back and forth at the net.Conn level. If your proxy does not need to inspect the message stream, then I recommend following the approach used in the elazarl/goproxy package.

@bdwyertech
Copy link
Author

Thanks @srybacki -- the issue in elazarl/goproxy websocket implementation is that it attempts to dial directly to the requested URL -- no consideration is given to dialcontext, etc.

https://github.com/elazarl/goproxy/blob/1f3cb6622dad84fad2c5416d502fe4c2c2dce48b/websocket.go#L32

So essentially what I am trying to do is create a net.Conn with the NTLM handshake done (ntlm auth is connection based).

I was able to use gorilla/websocket to create a Conn to the target, but attempting to bridge the client (net.Conn) and target (websocket.Conn) is where I have the issue. Thinking of injecting a net.conn into gorilla/websocket similar to your solution in #573

Is there a better approach?

@elithrar
Copy link
Contributor

elithrar commented Apr 16, 2020 via email

@bdwyertech
Copy link
Author

I think so, this is what I've scribbled together.

bdwyertech/goproxy@1d2074f#diff-8990d2c9c7f1690e60641d425c8932f7

@ghost
Copy link

ghost commented Apr 16, 2020

Does this work:

  d := *websocket.DefaultDialer // copy dialer for local modification
  d.NetDialContext = ntlm_proxy.WrapDialContext((&net.Dialer{}).DialContext, proxyAddr)
  c, resp, err := d.Dial(url, header)

@bdwyertech
Copy link
Author

bdwyertech commented Apr 16, 2020

I will try to get something like this functional after lunch -- it seems to get through the NTLM part at least bdwyertech/goproxy@1d2074f#diff-8990d2c9c7f1690e60641d425c8932f7

Hopefully this gives an idea of what I'm trying to do... Basically pass this function a net.Conn and let it do its NTLM dance before I present it to the elazarl/goproxy websocket logic.

func dialWithConn(c *net.Conn, url url.URL, dialContext proxyplease.DialContext) (err error) {
	d := websocket.Dialer{
		ReadBufferSize:   1024,
		WriteBufferSize:  1024,
		HandshakeTimeout: 45 * time.Second,
		NetDial:          func(network, addr string) (net.Conn, error) { return *c, nil },
		NetDialContext:   dialContext,
	}

	_, _, err = d.Dial(url.String(), nil)

	return
}

@ghost
Copy link

ghost commented Apr 16, 2020

I am now confused about what you are trying to do and I may have been confused from the get go.

Is your goal to create an HTTP proxy server that authenticates connections to a remote server using NTLM? Further, is the expected use case that the client specifies the proxy server using the HTTP_PROXY, HTTPS_PROXY environment variables or a proxy function?

@bdwyertech
Copy link
Author

bdwyertech commented Apr 16, 2020

Yes. My proxy provides a non-authenticated local proxy which forwards to upstream corporate proxy. It is supposed to make developers lives easier behind corporate NTLM proxies.

Everything works great except websockets because the elazarl websocket code assumes it can speak directly to the request URL.

Basically a developer runs my proxy as a daemon on their machine and set http_proxy, etc. to point to localhost. This alleviates the need to have user/password set as environment variables.

Some other projects are available which achieve similar e.g. CNTLM and PX. PX works well but it is a bit of a resource hog and hard to distribute being Python based.

I hope this explains the desire for io.copy. if I was to convert the client connection to a websocket.conn, how could I copy raw stream from one to the other? Maybe this would be easier.

@ghost
Copy link

ghost commented Apr 16, 2020

I recommend that you follow the approach in github.com/elazarl/goproxy or net/http/httputil ReverseProxy. Both of these proxies copy bytes back and forth with no understanding of the WebSocket protocol outside of the handshake headers.

For the elazarl/goproxy proxy, replace the calls to dial a net.Conn in ProxyHttpServer.serveWebsocketTLS and ProxyHttpServer.serveWebsocket with a function that dials an NTLM authenticated connection. All other websocket related code can remain as is.

For the httputil Proxy, provide a transport that's configured to dial NTLM authenticated connections.

A proxy can be written above the *websocket.Conn layer. This approach requires getting several details right at the WebSocket layer and it will not provide a benefit over pumping bytes back and forth. If you do go down this path, then work through carefully how PING, PONG and CLOSE messages are passed back and forth.

@bdwyertech
Copy link
Author

bdwyertech commented Apr 16, 2020

I seem to be getting a little further... This is what I've built up.

	dialer := proxyplease.NewDialContext(proxyplease.Proxy{URL: proxyUrl, TLSConfig: tlsConfig})
	cctx, _ := context.WithCancel(context.Background())
	targetConn, err := dialer(cctx, "tcp", targetURL.Host)

Full context:
https://github.com/bdwyertech/goproxy/blob/1221dada879ba457a3808120adad1684d10fd1cf/websocket.go

Seem to be getting stuff about malformed response, will poke around more tomorrow.

@bdwyertech
Copy link
Author

@srybacki I managed to get all of this to work -- I just needed to upgrade the connection to TLS and establish the intial connection via NTLM dialer. Thanks for pointing me back on the right track!

bdwyertech/goproxy@7aaf118

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants