cmd/coordinator: add buildlet TCP proxy handler, for gomote rdp
Updates golang/go#26090
Change-Id: I095f70baceb23cf28fcd70a78fd72df29603370e
Reviewed-on: https://go-review.googlesource.com/c/build/+/207357
Reviewed-by: Bryan C. Mills <bcmills@google.com>
diff --git a/cmd/coordinator/remote.go b/cmd/coordinator/remote.go
index 2fd4697..6058c6d 100644
--- a/cmd/coordinator/remote.go
+++ b/cmd/coordinator/remote.go
@@ -296,6 +296,11 @@
return
}
+ if r.Method == "POST" && r.URL.Path == "/tcpproxy" {
+ proxyBuildletTCP(w, r, rb)
+ return
+ }
+
outReq, err := http.NewRequest(r.Method, rb.buildlet.URL()+r.URL.Path+"?"+r.URL.RawQuery, r.Body)
if err != nil {
log.Printf("bad proxy request: %v", err)
@@ -317,6 +322,87 @@
proxy.ServeHTTP(w, outReq)
}
+// proxyBuildletTCP handles connecting to and proxying between a
+// backend buildlet VM's TCP port and the client. This is called once
+// it's already authenticated by proxyBuildletHTTP.
+func proxyBuildletTCP(w http.ResponseWriter, r *http.Request, rb *remoteBuildlet) {
+ if r.ProtoMajor > 1 {
+ // TODO: deal with HTTP/2 requests if https://farmer.golang.org enables it later.
+ // Currently it does not, as other handlers Hijack too. We'd need to teach clients
+ // when to explicitly disable HTTP/1, or update the protocols to do read/write
+ // bodies instead of 101 Switching Protocols.
+ http.Error(w, "unexpected HTTP/2 request", http.StatusInternalServerError)
+ return
+ }
+ hj, ok := w.(http.Hijacker)
+ if !ok {
+ http.Error(w, "not a Hijacker", http.StatusInternalServerError)
+ return
+ }
+ // The target port is a header instead of a query parameter for no real reason other
+ // than being consistent with the reverse buildlet registration headers.
+ port, err := strconv.Atoi(r.Header.Get("X-Target-Port"))
+ if err != nil {
+ http.Error(w, "invalid or missing X-Target-Port", http.StatusBadRequest)
+ return
+ }
+ hc, ok := dashboard.Hosts[rb.HostType]
+ if !ok || !hc.IsVM() {
+ // TODO: implement support for non-VM types if/when needed.
+ http.Error(w, fmt.Sprintf("unsupported non-VM host type %q", rb.HostType), http.StatusBadRequest)
+ return
+ }
+ ip, _, err := net.SplitHostPort(rb.buildlet.IPPort())
+ if err != nil {
+ http.Error(w, fmt.Sprintf("unexpected backend ip:port %q", rb.buildlet.IPPort()), http.StatusInternalServerError)
+ return
+ }
+
+ c, err := (&net.Dialer{}).DialContext(r.Context(), "tcp", net.JoinHostPort(ip, fmt.Sprint(port)))
+ if err != nil {
+ http.Error(w, fmt.Sprintf("failed to connect to port %v: %v", port, err), http.StatusInternalServerError)
+ return
+ }
+ defer c.Close()
+
+ // Hijack early so we can check for any unexpected buffered
+ // request data without doing a potentially blocking
+ // r.Body.Read. Also it's nice to be able to WriteString the
+ // response header explicitly. But using w.WriteHeader+w.Flush
+ // would probably also work. Somewhat arbitrary to do it early.
+ cc, buf, err := hj.Hijack()
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Hijack: %v", err), http.StatusInternalServerError)
+ return
+ }
+ defer cc.Close()
+
+ if buf.Reader.Buffered() != 0 {
+ io.WriteString(cc, "HTTP/1.0 400 Bad Request\r\n\r\nUnexpected buffered data.\n")
+ return
+ }
+
+ // If we send a 101 response with an Upgrade header and a
+ // "Connection: Upgrade" header, that makes net/http's
+ // *Response.isProtocolSwitch() return true, which gives us a
+ // writable Response.Body on the client side, which simplifies
+ // the gomote code.
+ io.WriteString(cc, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: tcpproxy\r\nConnection: upgrade\r\n\r\n")
+
+ errc := make(chan error, 2)
+ // Copy from HTTP client to backend.
+ go func() {
+ _, err := io.Copy(c, cc)
+ errc <- err
+ }()
+ // And copy from backend to the HTTP client.
+ go func() {
+ _, err := io.Copy(cc, c)
+ errc <- err
+ }()
+ <-errc
+}
+
func requireBuildletProxyAuth(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()