blob: 91f0a5eebd1c0ad96dec4222a6cf152b61c3e689 [file] [log] [blame]
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bufio"
"context"
"crypto/hmac"
"crypto/md5"
"crypto/tls"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
"go.chromium.org/luci/auth"
"golang.org/x/build/internal/rendezvous"
"golang.org/x/build/revdial/v2"
)
// mode is either a BuildConfig or HostConfig name (map key in x/build/dashboard/builders.go)
func keyForMode(mode string) (string, error) {
if isDevReverseMode() {
return devBuilderKey(mode), nil
}
keyPath := filepath.Join(homedir(), ".gobuildkey-"+mode)
if v := os.Getenv("GO_BUILD_KEY_PATH"); v != "" {
keyPath = v
}
key, err := os.ReadFile(keyPath)
if ok, _ := strconv.ParseBool(os.Getenv("GO_BUILD_KEY_DELETE_AFTER_READ")); ok {
os.Remove(keyPath)
}
if err != nil {
if len(key) == 0 || err != nil {
return "", fmt.Errorf("cannot read key file %q: %v", keyPath, err)
}
}
return strings.TrimSpace(string(key)), nil
}
func isDevReverseMode() bool {
return !strings.HasPrefix(*coordinator, "farmer.golang.org")
}
// dialCoordinator dials the coordinator to establish a revdial connection
// where the returned net.Listener can be used to accept connections from the
// coordinator.
func dialCoordinator() (net.Listener, error) {
devMode := isDevReverseMode()
if *hostname == "" {
*hostname = os.Getenv("HOSTNAME")
if *hostname == "" {
*hostname, _ = os.Hostname()
}
if *hostname == "" {
*hostname = "buildlet"
}
}
key, err := keyForMode(*reverseType)
if err != nil {
log.Fatalf("failed to find key for %s: %v", *reverseType, err)
}
addr := *coordinator
if addr == "farmer.golang.org" {
addr = "farmer.golang.org:443"
}
dial := func(ctx context.Context) (net.Conn, error) {
log.Printf("Dialing coordinator %s ...", addr)
t0 := time.Now()
tcpConn, err := dialServerTCP(ctx, addr)
if err != nil {
log.Printf("buildlet: reverse dial coordinator (%q) error after %v: %v", addr, time.Since(t0).Round(time.Second/100), err)
return nil, err
}
log.Printf("Dialed coordinator %s.", addr)
serverName := strings.TrimSuffix(addr, ":443")
log.Printf("Doing TLS handshake with coordinator (verifying hostname %q)...", serverName)
tcpConn.SetDeadline(time.Now().Add(30 * time.Second))
config := &tls.Config{
ServerName: serverName,
InsecureSkipVerify: devMode,
}
conn := tls.Client(tcpConn, config)
if err := conn.Handshake(); err != nil {
return nil, fmt.Errorf("failed to handshake with coordinator: %v", err)
}
tcpConn.SetDeadline(time.Time{})
return conn, nil
}
conn, err := dial(context.Background())
if err != nil {
return nil, err
}
bufr := bufio.NewReader(conn)
bufw := bufio.NewWriter(conn)
log.Printf("Registering reverse mode with coordinator...")
success := false
location := "/reverse"
const maxRedirects = 2
for i := 0; i < maxRedirects; i++ {
req, err := http.NewRequest("GET", location, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set("X-Go-Host-Type", *reverseType)
req.Header.Set("X-Go-Builder-Key", key)
req.Header.Set("X-Go-Builder-Hostname", *hostname)
req.Header.Set("X-Go-Builder-Version", strconv.Itoa(buildletVersion))
req.Header.Set("X-Revdial-Version", "2")
if err := req.Write(bufw); err != nil {
return nil, fmt.Errorf("coordinator /reverse request failed: %v", err)
}
if err := bufw.Flush(); err != nil {
return nil, fmt.Errorf("coordinator /reverse request flush failed: %v", err)
}
location, err = revdial.ReadProtoSwitchOrRedirect(bufr, req)
if err != nil {
return nil, fmt.Errorf("coordinator registration failed: %v", err)
}
if location == "" {
success = true
break
}
}
if !success {
return nil, errors.New("coordinator /reverse: too many redirects")
}
log.Printf("Connected to coordinator; reverse dialing active")
ln := revdial.NewListener(conn, dial)
return ln, nil
}
// dialGomoteServer dials the gomote server to establish a revdial connection
// where the returned net.Listener can be used to accept connections from the
// gomote server.
func dialGomoteServer() (net.Listener, error) {
devMode := isDevReverseMode()
if *hostname == "" {
*hostname = os.Getenv("HOSTNAME")
if *hostname == "" {
*hostname, _ = os.Hostname()
}
if *hostname == "" {
*hostname = "buildlet"
}
}
addr := *gomoteServerAddr
dial := func(ctx context.Context) (net.Conn, error) {
log.Printf("Dialing gomote server %s ...", addr)
t0 := time.Now()
tcpConn, err := dialServerTCP(ctx, addr)
if err != nil {
log.Printf("buildlet: reverse dial the gomote server (%q) error after %v: %v", addr, time.Since(t0).Round(time.Second/100), err)
return nil, err
}
log.Printf("Dialed coordinator %s.", addr)
serverName := strings.TrimSuffix(addr, ":443")
log.Printf("Doing TLS handshake with the gomote server (verifying hostname %q)...", serverName)
tcpConn.SetDeadline(time.Now().Add(30 * time.Second))
config := &tls.Config{
ServerName: serverName,
InsecureSkipVerify: devMode,
}
conn := tls.Client(tcpConn, config)
if err := conn.Handshake(); err != nil {
return nil, fmt.Errorf("failed to handshake with the gomote server: %v", err)
}
tcpConn.SetDeadline(time.Time{})
return conn, nil
}
conn, err := dial(context.Background())
if err != nil {
return nil, err
}
bufr := bufio.NewReader(conn)
bufw := bufio.NewWriter(conn)
log.Printf("Registering reverse mode with the gomote server...")
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
success := false
location := "/reverse"
const maxRedirects = 2
for i := 0; i < maxRedirects; i++ {
req, err := http.NewRequest("GET", location, nil)
if err != nil {
log.Fatal(err)
}
req.Header.Set(rendezvous.HeaderID, os.Getenv("GOMOTEID"))
req.Header.Set(rendezvous.HeaderToken, mustSwarmingAuthToken(ctx))
req.Header.Set(rendezvous.HeaderHostname, *hostname)
if err := req.Write(bufw); err != nil {
return nil, fmt.Errorf("gomote server /reverse request failed: %v", err)
}
if err := bufw.Flush(); err != nil {
return nil, fmt.Errorf("gomote server /reverse request flush failed: %v", err)
}
location, err = revdial.ReadProtoSwitchOrRedirect(bufr, req)
if err != nil {
return nil, fmt.Errorf("gomote server registration failed: %v", err)
}
if location == "" {
success = true
break
}
}
if !success {
return nil, errors.New("gomote server /reverse: too many redirects")
}
log.Printf("Connected to gomote server; reverse dialing active")
ln := revdial.NewListener(conn, dial)
return ln, nil
}
var coordDialer = &net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: 15 * time.Second,
}
// dialServerTCP returns a TCP connection to the server, making
// a CONNECT request to a proxy as a fallback.
func dialServerTCP(ctx context.Context, addr string) (net.Conn, error) {
tcpConn, err := coordDialer.DialContext(ctx, "tcp", addr)
if err != nil {
// If we had problems connecting to the TCP addr
// directly, perhaps there's a proxy in the way. See
// if they have an HTTPS_PROXY environment variable
// defined and try to do a CONNECT request to it.
req, _ := http.NewRequest("GET", "https://"+addr, nil)
proxyURL, _ := http.ProxyFromEnvironment(req)
if proxyURL != nil {
return dialServerViaCONNECT(ctx, addr, proxyURL)
}
return nil, err
}
return tcpConn, nil
}
func dialServerViaCONNECT(ctx context.Context, addr string, proxy *url.URL) (net.Conn, error) {
proxyAddr := proxy.Host
if proxy.Port() == "" {
proxyAddr = net.JoinHostPort(proxyAddr, "80")
}
log.Printf("dialing proxy %q ...", proxyAddr)
var d net.Dialer
c, err := d.DialContext(ctx, "tcp", proxyAddr)
if err != nil {
return nil, fmt.Errorf("dialing proxy %q failed: %v", proxyAddr, err)
}
fmt.Fprintf(c, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", addr, proxy.Hostname())
br := bufio.NewReader(c)
res, err := http.ReadResponse(br, nil)
if err != nil {
return nil, fmt.Errorf("reading HTTP response from CONNECT to %s via proxy %s failed: %v",
addr, proxyAddr, err)
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("proxy error from %s while dialing %s: %v", proxyAddr, addr, res.Status)
}
// It's safe to discard the bufio.Reader here and return the
// original TCP conn directly because we only use this for
// TLS, and in TLS the client speaks first, so we know there's
// no unbuffered data. But we can double-check.
if br.Buffered() > 0 {
return nil, fmt.Errorf("unexpected %d bytes of buffered data from CONNECT proxy %q",
br.Buffered(), proxyAddr)
}
return c, nil
}
const devMasterKey = "gophers rule"
func devBuilderKey(builder string) string {
h := hmac.New(md5.New, []byte(devMasterKey))
io.WriteString(h, builder)
return fmt.Sprintf("%x", h.Sum(nil))
}
func homedir() string {
switch runtime.GOOS {
case "windows":
return os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
case "plan9":
return os.Getenv("home")
}
home := os.Getenv("HOME")
if home != "" {
return home
}
if os.Getuid() == 0 {
return "/root"
}
return "/"
}
func mustSwarmingAuthToken(ctx context.Context) string {
tok := os.Getenv("GO_BUILDLET_TOKEN")
if tok != "" {
return tok
}
a := auth.NewAuthenticator(ctx, auth.SilentLogin, auth.Options{
Audience: "https://gomote.golang.org",
Method: auth.LUCIContextMethod,
UseIDTokens: true,
})
token, err := a.GetAccessToken(15 * time.Second)
if err != nil {
log.Fatalf("unable to retrieve swarming access token: %s", err)
}
return token.AccessToken
}