golang.org/x/build@v0.0.0-20240506185731-218518f32b70/cmd/buildlet/reverse.go (about) 1 // Copyright 2015 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "bufio" 9 "context" 10 "crypto/hmac" 11 "crypto/md5" 12 "crypto/tls" 13 "errors" 14 "fmt" 15 "io" 16 "log" 17 "net" 18 "net/http" 19 "net/url" 20 "os" 21 "path/filepath" 22 "runtime" 23 "strconv" 24 "strings" 25 "time" 26 27 "go.chromium.org/luci/auth" 28 "golang.org/x/build/internal/rendezvous" 29 "golang.org/x/build/revdial/v2" 30 ) 31 32 // mode is either a BuildConfig or HostConfig name (map key in x/build/dashboard/builders.go) 33 func keyForMode(mode string) (string, error) { 34 if isDevReverseMode() { 35 return devBuilderKey(mode), nil 36 } 37 keyPath := filepath.Join(homedir(), ".gobuildkey-"+mode) 38 if v := os.Getenv("GO_BUILD_KEY_PATH"); v != "" { 39 keyPath = v 40 } 41 key, err := os.ReadFile(keyPath) 42 if ok, _ := strconv.ParseBool(os.Getenv("GO_BUILD_KEY_DELETE_AFTER_READ")); ok { 43 os.Remove(keyPath) 44 } 45 if err != nil { 46 if len(key) == 0 || err != nil { 47 return "", fmt.Errorf("cannot read key file %q: %v", keyPath, err) 48 } 49 } 50 return strings.TrimSpace(string(key)), nil 51 } 52 53 func isDevReverseMode() bool { 54 return !strings.HasPrefix(*coordinator, "farmer.golang.org") 55 } 56 57 // dialCoordinator dials the coordinator to establish a revdial connection 58 // where the returned net.Listener can be used to accept connections from the 59 // coordinator. 60 func dialCoordinator() (net.Listener, error) { 61 devMode := isDevReverseMode() 62 63 if *hostname == "" { 64 *hostname = os.Getenv("HOSTNAME") 65 if *hostname == "" { 66 *hostname, _ = os.Hostname() 67 } 68 if *hostname == "" { 69 *hostname = "buildlet" 70 } 71 } 72 73 key, err := keyForMode(*reverseType) 74 if err != nil { 75 log.Fatalf("failed to find key for %s: %v", *reverseType, err) 76 } 77 78 addr := *coordinator 79 if addr == "farmer.golang.org" { 80 addr = "farmer.golang.org:443" 81 } 82 83 dial := func(ctx context.Context) (net.Conn, error) { 84 log.Printf("Dialing coordinator %s ...", addr) 85 t0 := time.Now() 86 tcpConn, err := dialServerTCP(ctx, addr) 87 if err != nil { 88 log.Printf("buildlet: reverse dial coordinator (%q) error after %v: %v", addr, time.Since(t0).Round(time.Second/100), err) 89 return nil, err 90 } 91 log.Printf("Dialed coordinator %s.", addr) 92 serverName := strings.TrimSuffix(addr, ":443") 93 log.Printf("Doing TLS handshake with coordinator (verifying hostname %q)...", serverName) 94 tcpConn.SetDeadline(time.Now().Add(30 * time.Second)) 95 config := &tls.Config{ 96 ServerName: serverName, 97 InsecureSkipVerify: devMode, 98 } 99 conn := tls.Client(tcpConn, config) 100 if err := conn.Handshake(); err != nil { 101 return nil, fmt.Errorf("failed to handshake with coordinator: %v", err) 102 } 103 tcpConn.SetDeadline(time.Time{}) 104 return conn, nil 105 } 106 conn, err := dial(context.Background()) 107 if err != nil { 108 return nil, err 109 } 110 111 bufr := bufio.NewReader(conn) 112 bufw := bufio.NewWriter(conn) 113 114 log.Printf("Registering reverse mode with coordinator...") 115 116 success := false 117 location := "/reverse" 118 const maxRedirects = 2 119 for i := 0; i < maxRedirects; i++ { 120 req, err := http.NewRequest("GET", location, nil) 121 if err != nil { 122 log.Fatal(err) 123 } 124 req.Header.Set("X-Go-Host-Type", *reverseType) 125 req.Header.Set("X-Go-Builder-Key", key) 126 req.Header.Set("X-Go-Builder-Hostname", *hostname) 127 req.Header.Set("X-Go-Builder-Version", strconv.Itoa(buildletVersion)) 128 req.Header.Set("X-Revdial-Version", "2") 129 if err := req.Write(bufw); err != nil { 130 return nil, fmt.Errorf("coordinator /reverse request failed: %v", err) 131 } 132 if err := bufw.Flush(); err != nil { 133 return nil, fmt.Errorf("coordinator /reverse request flush failed: %v", err) 134 } 135 location, err = revdial.ReadProtoSwitchOrRedirect(bufr, req) 136 if err != nil { 137 return nil, fmt.Errorf("coordinator registration failed: %v", err) 138 } 139 if location == "" { 140 success = true 141 break 142 } 143 } 144 if !success { 145 return nil, errors.New("coordinator /reverse: too many redirects") 146 } 147 148 log.Printf("Connected to coordinator; reverse dialing active") 149 ln := revdial.NewListener(conn, dial) 150 return ln, nil 151 } 152 153 // dialGomoteServer dials the gomote server to establish a revdial connection 154 // where the returned net.Listener can be used to accept connections from the 155 // gomote server. 156 func dialGomoteServer() (net.Listener, error) { 157 devMode := isDevReverseMode() 158 159 if *hostname == "" { 160 *hostname = os.Getenv("HOSTNAME") 161 if *hostname == "" { 162 *hostname, _ = os.Hostname() 163 } 164 if *hostname == "" { 165 *hostname = "buildlet" 166 } 167 } 168 169 addr := *gomoteServerAddr 170 dial := func(ctx context.Context) (net.Conn, error) { 171 log.Printf("Dialing gomote server %s ...", addr) 172 t0 := time.Now() 173 tcpConn, err := dialServerTCP(ctx, addr) 174 if err != nil { 175 log.Printf("buildlet: reverse dial the gomote server (%q) error after %v: %v", addr, time.Since(t0).Round(time.Second/100), err) 176 return nil, err 177 } 178 log.Printf("Dialed coordinator %s.", addr) 179 serverName := strings.TrimSuffix(addr, ":443") 180 log.Printf("Doing TLS handshake with the gomote server (verifying hostname %q)...", serverName) 181 tcpConn.SetDeadline(time.Now().Add(30 * time.Second)) 182 config := &tls.Config{ 183 ServerName: serverName, 184 InsecureSkipVerify: devMode, 185 } 186 conn := tls.Client(tcpConn, config) 187 if err := conn.Handshake(); err != nil { 188 return nil, fmt.Errorf("failed to handshake with the gomote server: %v", err) 189 } 190 tcpConn.SetDeadline(time.Time{}) 191 return conn, nil 192 } 193 conn, err := dial(context.Background()) 194 if err != nil { 195 return nil, err 196 } 197 198 bufr := bufio.NewReader(conn) 199 bufw := bufio.NewWriter(conn) 200 201 log.Printf("Registering reverse mode with the gomote server...") 202 203 ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) 204 defer cancel() 205 206 success := false 207 location := "/reverse" 208 const maxRedirects = 2 209 for i := 0; i < maxRedirects; i++ { 210 req, err := http.NewRequest("GET", location, nil) 211 if err != nil { 212 log.Fatal(err) 213 } 214 req.Header.Set(rendezvous.HeaderID, os.Getenv("GOMOTEID")) 215 req.Header.Set(rendezvous.HeaderToken, mustSwarmingAuthToken(ctx)) 216 req.Header.Set(rendezvous.HeaderHostname, *hostname) 217 if err := req.Write(bufw); err != nil { 218 return nil, fmt.Errorf("gomote server /reverse request failed: %v", err) 219 } 220 if err := bufw.Flush(); err != nil { 221 return nil, fmt.Errorf("gomote server /reverse request flush failed: %v", err) 222 } 223 location, err = revdial.ReadProtoSwitchOrRedirect(bufr, req) 224 if err != nil { 225 return nil, fmt.Errorf("gomote server registration failed: %v", err) 226 } 227 if location == "" { 228 success = true 229 break 230 } 231 } 232 if !success { 233 return nil, errors.New("gomote server /reverse: too many redirects") 234 } 235 236 log.Printf("Connected to gomote server; reverse dialing active") 237 ln := revdial.NewListener(conn, dial) 238 return ln, nil 239 } 240 241 var coordDialer = &net.Dialer{ 242 Timeout: 10 * time.Second, 243 KeepAlive: 15 * time.Second, 244 } 245 246 // dialServerTCP returns a TCP connection to the server, making 247 // a CONNECT request to a proxy as a fallback. 248 func dialServerTCP(ctx context.Context, addr string) (net.Conn, error) { 249 tcpConn, err := coordDialer.DialContext(ctx, "tcp", addr) 250 if err != nil { 251 // If we had problems connecting to the TCP addr 252 // directly, perhaps there's a proxy in the way. See 253 // if they have an HTTPS_PROXY environment variable 254 // defined and try to do a CONNECT request to it. 255 req, _ := http.NewRequest("GET", "https://"+addr, nil) 256 proxyURL, _ := http.ProxyFromEnvironment(req) 257 if proxyURL != nil { 258 return dialServerViaCONNECT(ctx, addr, proxyURL) 259 } 260 return nil, err 261 } 262 return tcpConn, nil 263 } 264 265 func dialServerViaCONNECT(ctx context.Context, addr string, proxy *url.URL) (net.Conn, error) { 266 proxyAddr := proxy.Host 267 if proxy.Port() == "" { 268 proxyAddr = net.JoinHostPort(proxyAddr, "80") 269 } 270 log.Printf("dialing proxy %q ...", proxyAddr) 271 var d net.Dialer 272 c, err := d.DialContext(ctx, "tcp", proxyAddr) 273 if err != nil { 274 return nil, fmt.Errorf("dialing proxy %q failed: %v", proxyAddr, err) 275 } 276 fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", addr, proxy.Hostname()) 277 br := bufio.NewReader(c) 278 res, err := http.ReadResponse(br, nil) 279 if err != nil { 280 return nil, fmt.Errorf("reading HTTP response from CONNECT to %s via proxy %s failed: %v", 281 addr, proxyAddr, err) 282 } 283 if res.StatusCode != 200 { 284 return nil, fmt.Errorf("proxy error from %s while dialing %s: %v", proxyAddr, addr, res.Status) 285 } 286 287 // It's safe to discard the bufio.Reader here and return the 288 // original TCP conn directly because we only use this for 289 // TLS, and in TLS the client speaks first, so we know there's 290 // no unbuffered data. But we can double-check. 291 if br.Buffered() > 0 { 292 return nil, fmt.Errorf("unexpected %d bytes of buffered data from CONNECT proxy %q", 293 br.Buffered(), proxyAddr) 294 } 295 return c, nil 296 } 297 298 const devMasterKey = "gophers rule" 299 300 func devBuilderKey(builder string) string { 301 h := hmac.New(md5.New, []byte(devMasterKey)) 302 io.WriteString(h, builder) 303 return fmt.Sprintf("%x", h.Sum(nil)) 304 } 305 306 func homedir() string { 307 switch runtime.GOOS { 308 case "windows": 309 return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") 310 case "plan9": 311 return os.Getenv("home") 312 } 313 home := os.Getenv("HOME") 314 if home != "" { 315 return home 316 } 317 if os.Getuid() == 0 { 318 return "/root" 319 } 320 return "/" 321 } 322 323 func mustSwarmingAuthToken(ctx context.Context) string { 324 tok := os.Getenv("GO_BUILDLET_TOKEN") 325 if tok != "" { 326 return tok 327 } 328 a := auth.NewAuthenticator(ctx, auth.SilentLogin, auth.Options{ 329 Audience: "https://gomote.golang.org", 330 Method: auth.LUCIContextMethod, 331 UseIDTokens: true, 332 }) 333 token, err := a.GetAccessToken(15 * time.Second) 334 if err != nil { 335 log.Fatalf("unable to retrieve swarming access token: %s", err) 336 } 337 return token.AccessToken 338 }