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  }