diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000000000..7a8432ed7e7ee7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,108 @@ +name: Build toolchain + +permissions: + contents: write + +on: + push: + branches: + - tailscale + - 'tailscale.go1.21' + pull_request: + branches: + - '*' + +jobs: + test: + runs-on: ubuntu-20.04 + steps: + - name: checkout + uses: actions/checkout@v3 + - name: test + run: cd src && ./all.bash + + build_release: + strategy: + matrix: + GOOS: ["linux", "darwin"] + GOARCH: ["amd64", "arm64"] + runs-on: ubuntu-20.04 + if: github.event_name == 'push' + steps: + - name: checkout + uses: actions/checkout@v3 + - 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: github.event_name == 'push' + 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-${{ github.sha }} + release_name: ${{ github.sha }} + draft: false + prerelease: true + + upload_release: + strategy: + matrix: + GOOS: ["linux", "darwin"] + GOARCH: ["amd64", "arm64"] + runs-on: ubuntu-20.04 + if: github.event_name == 'push' + 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 + if: github.event_name == 'push' + needs: [upload_release] + steps: + - name: checkout + uses: actions/checkout@v3 + - 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..a4b85913919892 --- /dev/null +++ b/api/go1.99999.txt @@ -0,0 +1,10 @@ +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, 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 4b77ed36f720e6..dc586afd6ac8e9 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/net/dial.go b/src/net/dial.go index fd1da1ebef0700..593cc220f3999d 100644 --- a/src/net/dial.go +++ b/src/net/dial.go @@ -247,6 +247,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. @@ -269,6 +287,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 @@ -572,9 +597,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/sock_posix.go b/src/net/sock_posix.go index b3e1806ba9f48c..759abe872b15cc 100644 --- a/src/net/sock_posix.go +++ b/src/net/sock_posix.go @@ -28,6 +28,25 @@ 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" { + // Ignore newRawConn errors (they're not possible in the current + // implementation, but even if they were, we don't want to + // affect socket operations for a trace hook invocation). + if c, err := newRawConn(fd); err == nil { + 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 e6f425b1cd0d44..cc3d983a381ccf 100644 --- a/src/net/tcpsock_posix.go +++ b/src/net/tcpsock_posix.go @@ -168,11 +168,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 f3dbcfec00b771..af7f1390a38792 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 {