-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
(v6) Fix app access websockets support #6028
Conversation
// | ||
// This goroutine stops when either the SSH channel closes or this | ||
// connection is closed e.g. by a http.Server (see Close below). | ||
go io.Copy(writer, ch) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: not sure if this goroutine races with writer.Close
but I'd ensure that it exits before CancelableChConn.Close
exits at least. On the bonus side, I'd ensure that it exits before the writer
is closed,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm not sure there's necessarily a race - if any part of the connection (writer or ChConn) closes, the copy will just stop which isn't much different from just closing the connection during a read.
My primary concern with this was to avoid a goroutine leak which I think can't happen if any part of the connection closes (which it does). We could add a channel signaling that the copy has exited to be a bit paranoid though.
|
||
// SetDeadline sets the channel connection read/write deadlines. | ||
func (c *CancelableChConn) SetDeadline(t time.Time) error { | ||
return c.reader.SetDeadline(t) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Which makes me wonder whether the original ChConn
should return errors instead of nil
for the APIs it does not implement - WDYT? Would be at least interesting to see whether it breaks anywhere in tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm pretty sure things will break in mysterious ways :) A lot of Go runtime uses deadlines and will trip if we start returning errors. I'm actually more surprised that things seems to work ok without deadlines implemented on ChConn (well, besides this websockets issue).
p.log.Debugf("Handing off connection to a local %q service.", dreq.ConnType) | ||
switch dreq.ConnType { | ||
case types.AppTunnel: | ||
// Use channel connection wrapper that supports deadlines when | ||
// proxying applications. This is required for websockets to | ||
// work over Application Access. | ||
p.server.HandleConnection(utils.NewCancelableChConn(p.sconn, p.channel)) | ||
default: | ||
p.server.HandleConnection(utils.NewChConn(p.sconn, p.channel)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason why you separated these two and created CancelableChConn
instead of just adding deadline support to ChConn
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@russjones No good reason other than out of abundance of caution. I can just update ChConn if you feel strongly about it, but I just felt that this functionality is only required for app access and wanted to avoid the risk of potentially introducing some side-effects to other types of services like ssh (and esp. since it's going to a minor release).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense for 6.1. What do you think updating ChConn
on master
to support deadlines but keeping them separate on branch/v6
? master
is still 6 months away from becoming 7.0 so we would get a fair amount of testing in during that time plus you would get any additional testing for Application Access on 6.1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@russjones Yeah, that was one of my alternative ideas as well. We can do that. I'll rebase this PR off of branch/v6
and open the one to master with just updated ChConn.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright, looks good to me, merge it in after rebase off of branch/v6
.
lib/srv/app/transport.go
Outdated
// Rewrite rewrites the websockets request. | ||
func (r *websocketsRewriter) Rewrite(req *http.Request) { | ||
// Update host to the target app's host to make sure it's forwarded correctly. | ||
req.URL.Host = r.uri.Host | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't go through the round tripper?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, websockets use their own forwarder logic and a separate rewriter as well:
https://github.com/gravitational/oxy/blob/master/forward/fwd.go#L124-L128
9ede6c1
to
9a00854
Compare
Fixes #5264.
What
This pull request fixes websockets proxying through the application access.
I used the Node-RED application to test this according to the @webvictim's example in the ticket linked above. With these changes the websockets connection gets proxied correctly and the output in the inspector is consistent with the "working" screenshot from that ticket.
How
The meat of the fix is in
lib/utils/fakeconn.go
. Websockets forwarding works by hijacking the raw connection from ResponseWriter and passing all traffic back/forth between the client/server:https://github.com/gravitational/oxy/blob/1.0.4/forward/fwd.go#L270
During investigation I found out that Hijack call is where it would hang indefinitely.
After further investigation, I found that before returning raw connection to the user during hijack, Go runtime tries to cancel all pending reads on the connection:
https://github.com/golang/go/blob/go1.15.10/src/net/http/server.go#L315
It does that by setting read deadline far in the past and waiting for pending reads to fail:
https://github.com/golang/go/blob/go1.15.10/src/net/http/server.go#L736
The problem is that all connections coming out of reverse tunnel are wrapped in
utils.ChConn
:https://github.com/gravitational/teleport/blob/master/lib/reversetunnel/transport.go#L301
which doesn't implement deadlines:
https://github.com/gravitational/teleport/blob/master/lib/utils/fakeconn.go#L192-L196
Thus, this PR introduces a
CancelableChConn
that extendsChConn
with read deadlines support, based on thenet.Pipe
.Note, I only used CancelableChConn for App Access servers to limit the scope and potential impact of the change. We could just update regular ChConn so it applies to all reverse tunnel connections but I opted to be on a safe side since this functionality is only required for websockets support through AAP.
There also were a couple of tweaks required here and there to have oxy/forward properly forward websockets.