diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000000..642bb7ff70951f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,122 @@ +name: Build toolchain + +permissions: + contents: write + +on: + push: + branches: + - tailscale + - 'tailscale.go1.21' + pull_request: + branches: + - '*' + workflow_dispatch: + inputs: + ref: + description: Branch, commit or tag to build from + required: true + default: 'tailscale.go1.21' + +jobs: + test: + runs-on: ubuntu-20.04 + steps: + - name: checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref || github.ref }} + - name: test + run: cd src && ./all.bash + + build_release: + strategy: + matrix: + GOOS: ["linux", "darwin"] + GOARCH: ["amd64", "arm64"] + runs-on: ubuntu-20.04 + if: contains(fromJSON('["push", "workflow_dispatch"]'), github.event_name) + steps: + - name: checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref || github.ref }} + - name: build + run: cd src && ./make.bash + env: + GOOS: "${{ matrix.GOOS }}" + GOARCH: "${{ matrix.GOARCH }}" + CGO_ENABLED: "0" + - name: trim unnecessary bits + run: | + rm -rf pkg/*_* + mv pkg/tool/${{ matrix.GOOS }}_${{ matrix.GOARCH }} pkg + rm -rf pkg/tool/*_* + mv -f bin/${{ matrix.GOOS }}_${{ matrix.GOARCH }}/* bin/ || true + rm -rf bin/${{ matrix.GOOS }}_${{ matrix.GOARCH }} + mv pkg/${{ matrix.GOOS }}_${{ matrix.GOARCH }} pkg/tool + find . -type d -name 'testdata' -print0 | xargs -0 rm -rf + find . -name '*_test.go' -delete + - name: archive + run: cd .. && tar --exclude-vcs -zcf ${{ matrix.GOOS }}-${{ matrix.GOARCH }}.tar.gz go + - name: save + uses: actions/upload-artifact@v1 + with: + name: ${{ matrix.GOOS }}-${{ matrix.GOARCH }} + path: ../${{ matrix.GOOS }}-${{ matrix.GOARCH }}.tar.gz + + create_release: + runs-on: ubuntu-20.04 + if: contains(fromJSON('["push", "workflow_dispatch"]'), github.event_name) + needs: [test, build_release] + outputs: + url: ${{ steps.create_release.outputs.upload_url }} + steps: + - name: create release + id: create_release + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + # Release name can't be the same as tag name, sigh + tag_name: build-${{ inputs.ref || github.sha }} + release_name: ${{ inputs.ref || github.sha }} + draft: false + prerelease: true + + upload_release: + strategy: + matrix: + GOOS: ["linux", "darwin"] + GOARCH: ["amd64", "arm64"] + runs-on: ubuntu-20.04 + if: contains(fromJSON('["push", "workflow_dispatch"]'), github.event_name) + needs: [create_release] + steps: + - name: download artifact + uses: actions/download-artifact@v1 + with: + name: ${{ matrix.GOOS }}-${{ matrix.GOARCH }} + - name: upload artifact + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.create_release.outputs.url }} + asset_path: ${{ matrix.GOOS }}-${{ matrix.GOARCH }}/${{ matrix.GOOS }}-${{ matrix.GOARCH }}.tar.gz + asset_name: ${{ matrix.GOOS }}-${{ matrix.GOARCH }}.tar.gz + asset_content_type: application/gzip + + clean_old: + runs-on: ubuntu-20.04 + # Do not clean up old builds on workflow_dispatch to allow temporarily + # re-creating old releases for backports. + if: github.event_name == 'push' + needs: [upload_release] + steps: + - name: checkout + uses: actions/checkout@v3 + with: + ref: ${{ inputs.ref || github.ref }} + - name: Delete older builds + run: ./.github/workflows/prune_old_builds.sh "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/prune_old_builds.sh b/.github/workflows/prune_old_builds.sh new file mode 100755 index 00000000000000..e7dc68cfba12d6 --- /dev/null +++ b/.github/workflows/prune_old_builds.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -euo pipefail + +KEEP=10 +GITHUB_TOKEN=$1 + +delete_release() { + release_id=$1 + tag_name=$2 + set -x + curl -X DELETE --header "Authorization: Bearer $GITHUB_TOKEN" "https://api.github.com/repos/tailscale/go/releases/$release_id" + curl -X DELETE --header "Authorization: Bearer $GITHUB_TOKEN" "https://api.github.com/repos/tailscale/go/git/refs/tags/$tag_name" + set +x +} + +curl https://api.github.com/repos/tailscale/go/releases 2>/dev/null |\ + jq -r '.[] | "\(.published_at) \(.id) \(.tag_name)"' |\ + egrep '[^ ]+ [^ ]+ build-[0-9a-f]{40}' |\ + sort |\ + head --lines=-${KEEP}|\ + while read date release_id tag_name; do + delete_release "$release_id" "$tag_name" + done diff --git a/api/go1.99999.txt b/api/go1.99999.txt new file mode 100644 index 00000000000000..3c59e814c5ca5a --- /dev/null +++ b/api/go1.99999.txt @@ -0,0 +1,11 @@ +pkg net, func SetDialEnforcer(func(context.Context, []Addr) error) #55 +pkg net, func SetResolveEnforcer(func(context.Context, string, string, string, Addr) error) #55 +pkg net/http, func SetRoundTripEnforcer(func(*Request) error) #55 +pkg net, func WithSockTrace(context.Context, *SockTrace) context.Context #58 +pkg net, func ContextSockTrace(context.Context) *SockTrace #58 +pkg net, type SockTrace struct #58 +pkg net, type SockTrace struct, DidRead func(int) #58 +pkg net, type SockTrace struct, DidWrite func(int) #58 +pkg net, type SockTrace struct, WillOverwrite func(*SockTrace) #58 +pkg net, type SockTrace struct, DidCreateTCPConn func(syscall.RawConn) #58 +pkg net, type SockTrace struct, WillCloseTCPConn func(syscall.RawConn) #58 diff --git a/src/cmd/dist/build.go b/src/cmd/dist/build.go index 32e59b446a5d9b..a687709300a102 100644 --- a/src/cmd/dist/build.go +++ b/src/cmd/dist/build.go @@ -393,6 +393,12 @@ func findgoversion() string { // its content if available, which is empty at this point. // Only use the VERSION file if it is non-empty. if b != "" { + if rev := os.Getenv("TAILSCALE_TOOLCHAIN_REV"); rev != "" { + if len(rev) > 10 { + rev = rev[:10] + } + b += "-ts" + chomp(rev) + } return b } } diff --git a/src/cmd/dist/buildgo.go b/src/cmd/dist/buildgo.go index 884e9d729a6a35..5c1daecd4c3d98 100644 --- a/src/cmd/dist/buildgo.go +++ b/src/cmd/dist/buildgo.go @@ -7,7 +7,6 @@ package main import ( "fmt" "io" - "os" "path/filepath" "sort" "strings" @@ -119,7 +118,7 @@ func mkzcgo(dir, file string) { writeHeader(&buf) fmt.Fprintf(&buf, "package build\n") fmt.Fprintln(&buf) - fmt.Fprintf(&buf, "const defaultCGO_ENABLED = %s\n", quote(os.Getenv("CGO_ENABLED"))) + fmt.Fprintf(&buf, "const defaultCGO_ENABLED = %q\n", "") writefile(buf.String(), file, writeSkipSame) } diff --git a/src/cmd/go/internal/cache/default.go b/src/cmd/go/internal/cache/default.go index b5650eac669b46..63aa26f81e357a 100644 --- a/src/cmd/go/internal/cache/default.go +++ b/src/cmd/go/internal/cache/default.go @@ -59,7 +59,9 @@ func initDefaultCache() { base.Fatalf("failed to initialize build cache at %s: %s\n", dir, err) } - if v := cfg.Getenv("GOCACHEPROG"); v != "" && goexperiment.CacheProg { + // We don't require the GOEXPERIMENT in Tailscale's Go tree. + const isTailscaleGoTree = true + if v := cfg.Getenv("GOCACHEPROG"); v != "" && (isTailscaleGoTree || goexperiment.CacheProg) { defaultCache = startCacheProg(v, diskCache) } else { defaultCache = diskCache diff --git a/src/net/dial.go b/src/net/dial.go index a6565c3ce5d13b..2e8239da0831f0 100644 --- a/src/net/dial.go +++ b/src/net/dial.go @@ -258,6 +258,24 @@ func parseNetwork(ctx context.Context, network string, needsProto bool) (afnet s return "", 0, UnknownNetworkError(network) } +// SetResolveEnforcer set a program-global resolver enforcer that can cause resolvers to +// fail based on the context and/or other arguments. +// +// f must be non-nil, it can only be called once, and must not be called +// concurrent with any dial/resolve. +func SetResolveEnforcer(f func(ctx context.Context, op, network, addr string, hint Addr) error) { + if f == nil { + panic("nil func") + } + if resolveEnforcer != nil { + panic("already called") + } + resolveEnforcer = f +} + +// resolveEnforcer, if non-nil, is the installed hook from SetResolveEnforcer. +var resolveEnforcer func(ctx context.Context, op, network, addr string, hint Addr) error + // resolveAddrList resolves addr using hint and returns a list of // addresses. The result contains at least one address when error is // nil. @@ -280,6 +298,13 @@ func (r *Resolver) resolveAddrList(ctx context.Context, op, network, addr string } return addrList{addr}, nil } + + if resolveEnforcer != nil { + if err := resolveEnforcer(ctx, op, network, addr, hint); err != nil { + return nil, err + } + } + addrs, err := r.internetAddrList(ctx, afnet, addr) if err != nil || op != "dial" || hint == nil { return addrs, err @@ -584,9 +609,32 @@ func (sd *sysDialer) dialParallel(ctx context.Context, primaries, fallbacks addr } } +// SetDialEnforcer set a program-global dial enforcer that can cause dials to +// fail based on the context and/or Addr(s). +// +// f must be non-nil, it can only be called once, and must not be called +// concurrent with any dial. +func SetDialEnforcer(f func(context.Context, []Addr) error) { + if f == nil { + panic("nil func") + } + if dialEnforcer != nil { + panic("already called") + } + dialEnforcer = f +} + +// dialEnforce, if non-nil, is any installed hook from SetDialEnforcer. +var dialEnforcer func(context.Context, []Addr) error + // dialSerial connects to a list of addresses in sequence, returning // either the first successful connection, or the first error. func (sd *sysDialer) dialSerial(ctx context.Context, ras addrList) (Conn, error) { + if dialEnforcer != nil { + if err := dialEnforcer(ctx, ras); err != nil { + return nil, err + } + } var firstErr error // The error from the first address is most relevant. for i, ra := range ras { diff --git a/src/net/fd_posix.go b/src/net/fd_posix.go index ffb9bcf8b9e8ca..5c88b50cdae49c 100644 --- a/src/net/fd_posix.go +++ b/src/net/fd_posix.go @@ -24,6 +24,12 @@ type netFD struct { net string laddr Addr raddr Addr + + // hooks (if provided) are called after successful reads or writes with the + // number of bytes transferred. + readHook func(int) + writeHook func(int) + closeHook func() } func (fd *netFD) setAddr(laddr, raddr Addr) { @@ -34,6 +40,9 @@ func (fd *netFD) setAddr(laddr, raddr Addr) { func (fd *netFD) Close() error { runtime.SetFinalizer(fd, nil) + if fd.closeHook != nil { + fd.closeHook() + } return fd.pfd.Close() } @@ -44,92 +53,140 @@ func (fd *netFD) shutdown(how int) error { } func (fd *netFD) closeRead() error { + if fd.closeHook != nil { + fd.closeHook() + } return fd.shutdown(syscall.SHUT_RD) } func (fd *netFD) closeWrite() error { + if fd.closeHook != nil { + fd.closeHook() + } return fd.shutdown(syscall.SHUT_WR) } func (fd *netFD) Read(p []byte) (n int, err error) { n, err = fd.pfd.Read(p) + if fd.readHook != nil && err == nil { + fd.readHook(n) + } runtime.KeepAlive(fd) return n, wrapSyscallError(readSyscallName, err) } func (fd *netFD) readFrom(p []byte) (n int, sa syscall.Sockaddr, err error) { n, sa, err = fd.pfd.ReadFrom(p) + if fd.readHook != nil && err == nil { + fd.readHook(n) + } runtime.KeepAlive(fd) return n, sa, wrapSyscallError(readFromSyscallName, err) } func (fd *netFD) readFromInet4(p []byte, from *syscall.SockaddrInet4) (n int, err error) { n, err = fd.pfd.ReadFromInet4(p, from) + if fd.readHook != nil && err == nil { + fd.readHook(n) + } runtime.KeepAlive(fd) return n, wrapSyscallError(readFromSyscallName, err) } func (fd *netFD) readFromInet6(p []byte, from *syscall.SockaddrInet6) (n int, err error) { n, err = fd.pfd.ReadFromInet6(p, from) + if fd.readHook != nil && err == nil { + fd.readHook(n) + } runtime.KeepAlive(fd) return n, wrapSyscallError(readFromSyscallName, err) } func (fd *netFD) readMsg(p []byte, oob []byte, flags int) (n, oobn, retflags int, sa syscall.Sockaddr, err error) { n, oobn, retflags, sa, err = fd.pfd.ReadMsg(p, oob, flags) + if fd.readHook != nil && err == nil { + fd.readHook(n) + } runtime.KeepAlive(fd) return n, oobn, retflags, sa, wrapSyscallError(readMsgSyscallName, err) } func (fd *netFD) readMsgInet4(p []byte, oob []byte, flags int, sa *syscall.SockaddrInet4) (n, oobn, retflags int, err error) { n, oobn, retflags, err = fd.pfd.ReadMsgInet4(p, oob, flags, sa) + if fd.readHook != nil && err == nil { + fd.readHook(n) + } runtime.KeepAlive(fd) return n, oobn, retflags, wrapSyscallError(readMsgSyscallName, err) } func (fd *netFD) readMsgInet6(p []byte, oob []byte, flags int, sa *syscall.SockaddrInet6) (n, oobn, retflags int, err error) { n, oobn, retflags, err = fd.pfd.ReadMsgInet6(p, oob, flags, sa) + if fd.readHook != nil && err == nil { + fd.readHook(n) + } runtime.KeepAlive(fd) return n, oobn, retflags, wrapSyscallError(readMsgSyscallName, err) } func (fd *netFD) Write(p []byte) (nn int, err error) { nn, err = fd.pfd.Write(p) + if fd.writeHook != nil && err == nil { + fd.writeHook(nn) + } runtime.KeepAlive(fd) return nn, wrapSyscallError(writeSyscallName, err) } func (fd *netFD) writeTo(p []byte, sa syscall.Sockaddr) (n int, err error) { n, err = fd.pfd.WriteTo(p, sa) + if fd.writeHook != nil && err == nil { + fd.writeHook(n) + } runtime.KeepAlive(fd) return n, wrapSyscallError(writeToSyscallName, err) } func (fd *netFD) writeToInet4(p []byte, sa *syscall.SockaddrInet4) (n int, err error) { n, err = fd.pfd.WriteToInet4(p, sa) + if fd.writeHook != nil && err == nil { + fd.writeHook(n) + } runtime.KeepAlive(fd) return n, wrapSyscallError(writeToSyscallName, err) } func (fd *netFD) writeToInet6(p []byte, sa *syscall.SockaddrInet6) (n int, err error) { n, err = fd.pfd.WriteToInet6(p, sa) + if fd.writeHook != nil && err == nil { + fd.writeHook(n) + } runtime.KeepAlive(fd) return n, wrapSyscallError(writeToSyscallName, err) } func (fd *netFD) writeMsg(p []byte, oob []byte, sa syscall.Sockaddr) (n int, oobn int, err error) { n, oobn, err = fd.pfd.WriteMsg(p, oob, sa) + if fd.writeHook != nil && err == nil { + fd.writeHook(n) + } runtime.KeepAlive(fd) return n, oobn, wrapSyscallError(writeMsgSyscallName, err) } func (fd *netFD) writeMsgInet4(p []byte, oob []byte, sa *syscall.SockaddrInet4) (n int, oobn int, err error) { n, oobn, err = fd.pfd.WriteMsgInet4(p, oob, sa) + if fd.writeHook != nil && err == nil { + fd.writeHook(n) + } runtime.KeepAlive(fd) return n, oobn, wrapSyscallError(writeMsgSyscallName, err) } func (fd *netFD) writeMsgInet6(p []byte, oob []byte, sa *syscall.SockaddrInet6) (n int, oobn int, err error) { n, oobn, err = fd.pfd.WriteMsgInet6(p, oob, sa) + if fd.writeHook != nil && err == nil { + fd.writeHook(n) + } runtime.KeepAlive(fd) return n, oobn, wrapSyscallError(writeMsgSyscallName, err) } diff --git a/src/net/http/tailscale.go b/src/net/http/tailscale.go new file mode 100644 index 00000000000000..b2a13893d3eaaa --- /dev/null +++ b/src/net/http/tailscale.go @@ -0,0 +1,21 @@ +package http + +var roundTripEnforcer func(*Request) error + +// SetRoundTripEnforcer set a program-global resolver enforcer that can cause +// RoundTrip calls to fail based on the request and its context. +// +// f must be non-nil. +// +// SetRoundTripEnforcer can only be called once, and must not be called +// concurrent with any RoundTrip call; it's expected to be registered during +// init. +func SetRoundTripEnforcer(f func(*Request) error) { + if f == nil { + panic("nil func") + } + if roundTripEnforcer != nil { + panic("already called") + } + roundTripEnforcer = f +} diff --git a/src/net/http/transport.go b/src/net/http/transport.go index 57c70e72f9522c..e7185ed805cc17 100644 --- a/src/net/http/transport.go +++ b/src/net/http/transport.go @@ -515,6 +515,11 @@ func (t *Transport) alternateRoundTripper(req *Request) RoundTripper { // roundTrip implements a RoundTripper over HTTP. func (t *Transport) roundTrip(req *Request) (*Response, error) { + if roundTripEnforcer != nil { + if err := roundTripEnforcer(req); err != nil { + return nil, err + } + } t.nextProtoOnce.Do(t.onceSetNextProtoDefaults) ctx := req.Context() trace := httptrace.ContextClientTrace(ctx) diff --git a/src/net/sock_posix.go b/src/net/sock_posix.go index d04c26e7ef449c..349e095b248cd1 100644 --- a/src/net/sock_posix.go +++ b/src/net/sock_posix.go @@ -28,6 +28,21 @@ func socket(ctx context.Context, net string, family, sotype, proto int, ipv6only poll.CloseFunc(s) return nil, err } + if trace := ContextSockTrace(ctx); trace != nil { + fd.readHook = trace.DidRead + fd.writeHook = trace.DidWrite + if (trace.DidCreateTCPConn != nil || trace.WillCloseTCPConn != nil) && len(net) >= 3 && net[0:3] == "tcp" { + c := newRawConn(fd) + if trace.DidCreateTCPConn != nil { + trace.DidCreateTCPConn(c) + } + if trace.WillCloseTCPConn != nil { + fd.closeHook = func() { + trace.WillCloseTCPConn(c) + } + } + } + } // This function makes a network file descriptor for the // following applications: diff --git a/src/net/socktrace.go b/src/net/socktrace.go new file mode 100644 index 00000000000000..b02a8d12484d4e --- /dev/null +++ b/src/net/socktrace.go @@ -0,0 +1,53 @@ +// Copyright 2023 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 net + +import ( + "context" + "syscall" +) + +// SockTrace is a set of hooks to run at various operations on a network socket. +// Any particular hook may be nil. Functions may be called concurrently from +// different goroutines. +type SockTrace struct { + // DidOpenTCPConn is called when a TCP socket was created. The + // underlying raw network connection that was created is provided. + DidCreateTCPConn func(c syscall.RawConn) + // DidRead is called after a successful read from the socket, where n bytes + // were read. + DidRead func(n int) + // DidWrite is called after a successful write to the socket, where n bytes + // were written. + DidWrite func(n int) + // WillOverwrite is called when the registered trace is overwritten by a + // subsequent call to WithSockTrace. The provided trace is the new trace + // that will be used. + WillOverwrite func(trace *SockTrace) + // WillCloseTCPConn is called when a TCP socket is about to be closed. The + // underlying raw network connection that is being closed is provided. + WillCloseTCPConn func(c syscall.RawConn) +} + +// WithSockTrace returns a new context based on the provided parent +// ctx. Socket reads and writes made with the returned context will use +// the provided trace hooks. Any previous hooks registered with ctx are +// ovewritten (their WillOverwrite hook will be called). +func WithSockTrace(ctx context.Context, trace *SockTrace) context.Context { + if previous := ContextSockTrace(ctx); previous != nil && previous.WillOverwrite != nil { + previous.WillOverwrite(trace) + } + return context.WithValue(ctx, sockTraceKey{}, trace) +} + +// ContextSockTrace returns the SockTrace associated with the +// provided context. If none, it returns nil. +func ContextSockTrace(ctx context.Context) *SockTrace { + trace, _ := ctx.Value(sockTraceKey{}).(*SockTrace) + return trace +} + +// unique type to prevent assignment. +type sockTraceKey struct{} diff --git a/src/net/tcpsock_posix.go b/src/net/tcpsock_posix.go index 01b5ec9ed05642..5f1cafd17e143d 100644 --- a/src/net/tcpsock_posix.go +++ b/src/net/tcpsock_posix.go @@ -175,11 +175,33 @@ func (ln *TCPListener) file() (*os.File, error) { return f, nil } +// Tailscale addition: if TS_PANIC_ON_TEST_LISTEN_UNSPEC is set, panic +// if a listen tries to listen on all interfaces (for debugging Mac +// firewall dialogs in tests). +func panicOnUnspecListen(ip IP) bool { + if ip != nil && !ip.IsUnspecified() { + return false + } + v := os.Getenv("TS_PANIC_ON_TEST_LISTEN_UNSPEC") + if v == "" { + return false + } + switch v[0] { + case 't', 'T', '1': + return true + } + return false +} + func (sl *sysListener) listenTCP(ctx context.Context, laddr *TCPAddr) (*TCPListener, error) { return sl.listenTCPProto(ctx, laddr, 0) } func (sl *sysListener) listenTCPProto(ctx context.Context, laddr *TCPAddr, proto int) (*TCPListener, error) { + if panicOnUnspecListen(laddr.IP) { + panic("tailscale: can't listen on unspecified address in test") + } + var ctrlCtxFn func(cxt context.Context, network, address string, c syscall.RawConn) error if sl.ListenConfig.Control != nil { ctrlCtxFn = func(cxt context.Context, network, address string, c syscall.RawConn) error { diff --git a/src/net/udpsock_posix.go b/src/net/udpsock_posix.go index 50350598317eb9..21d92df4ca2a51 100644 --- a/src/net/udpsock_posix.go +++ b/src/net/udpsock_posix.go @@ -217,6 +217,10 @@ func (sd *sysDialer) dialUDP(ctx context.Context, laddr, raddr *UDPAddr) (*UDPCo } func (sl *sysListener) listenUDP(ctx context.Context, laddr *UDPAddr) (*UDPConn, error) { + if panicOnUnspecListen(laddr.IP) { + panic("tailscale: can't listen on unspecified address in test") + } + var ctrlCtxFn func(cxt context.Context, network, address string, c syscall.RawConn) error if sl.ListenConfig.Control != nil { ctrlCtxFn = func(cxt context.Context, network, address string, c syscall.RawConn) error {