diff --git a/.github/workflows/cross-illumos.yaml b/.github/workflows/cross-illumos.yaml new file mode 100644 index 0000000000000..a328084cf0cfa --- /dev/null +++ b/.github/workflows/cross-illumos.yaml @@ -0,0 +1,32 @@ +name: illumos-Cross + +on: + push: + branches: + - main + - 'illumos-*' + pull_request: + branches: + - '*' + +jobs: + build: + runs-on: ubuntu-latest + + if: "!contains(github.event.head_commit.message, '[ci skip]')" + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + check-latest: true + id: go + + - name: SunOS build script + run: bash -x build.sh diff --git a/.github/workflows/nshalman-sunos-releases.yml b/.github/workflows/nshalman-sunos-releases.yml new file mode 100644 index 0000000000000..15a942e6faeec --- /dev/null +++ b/.github/workflows/nshalman-sunos-releases.yml @@ -0,0 +1,43 @@ +--- +name: "tagged-release" + +on: + push: + tags: + - "v*-sunos" + +jobs: + tagged-release: + name: "SunOS Tagged Release" + runs-on: "ubuntu-latest" + + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + check-latest: true + id: go + + - name: SunOS build script + run: bash -x build.sh + + - name: Create Release + uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + prerelease: false + files: | + cmd/tailscaled/tailscale.xml + sha256sums + tailscale-illumos + tailscale-solaris + tailscaled-illumos + tailscaled-solaris + tailscaled-plain-illumos + tailscaled-plain-solaris diff --git a/AUTHORS b/AUTHORS index 03d5932c04746..b00ac7cd8e5bc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,3 +15,4 @@ # company that owns the rights to your contribution. Tailscale Inc. +Nahum Shalman diff --git a/build.sh b/build.sh new file mode 100755 index 0000000000000..b051890d85d9f --- /dev/null +++ b/build.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +set -o xtrace +set -o errexit + +export TS_USE_TOOLCHAIN=true +# This prevents illumos libc from leaking into Solaris binaries when built on illumos +export CGO_ENABLED=0 + +fix_osabi () { + if [[ $(uname -s) == SunOS ]]; then + /usr/bin/elfedit \ + -e "ehdr:ei_osabi ELFOSABI_SOLARIS" \ + -e "ehdr:ei_abiversion EAV_SUNW_CURRENT" \ + "${1?}" + else + elfedit --output-osabi "Solaris" --output-abiversion "1" "${1?}" + fi +} + +for GOOS in illumos solaris; do + export GOOS + # Build "box" binary that can be both daemon and client + # Continuing to use the same name as before + bash -x ./build_dist.sh --extra-small --box ./cmd/tailscaled + fix_osabi tailscaled + mv tailscaled{,-${GOOS}} + # Build plain daemon binary + bash -x ./build_dist.sh ./cmd/tailscaled + fix_osabi tailscaled + mv tailscaled{,-plain-${GOOS}} + # Build plain client binary + bash -x ./build_dist.sh ./cmd/tailscale + fix_osabi tailscale + mv tailscale{,-${GOOS}} +done + +ln cmd/tailscaled/tailscale.xml . +shasum -a 256 tailscaled-* tailscale-* tailscale.xml >sha256sums +rm ./tailscale.xml diff --git a/cmd/tailscaled/tailscale-smartos-gz.xml b/cmd/tailscaled/tailscale-smartos-gz.xml new file mode 100644 index 0000000000000..5c76c81ea11db --- /dev/null +++ b/cmd/tailscaled/tailscale-smartos-gz.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/tailscaled/tailscale.xml b/cmd/tailscaled/tailscale.xml new file mode 100644 index 0000000000000..2977a3abf60f6 --- /dev/null +++ b/cmd/tailscaled/tailscale.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/cmd/tailscaled/tailscaled.go b/cmd/tailscaled/tailscaled.go index 1c52361235ad7..b095839d11fc8 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -74,7 +74,7 @@ import ( // defaultTunName returns the default tun device name for the platform. func defaultTunName() string { switch runtime.GOOS { - case "openbsd": + case "openbsd", "illumos", "solaris": return "tun" case "windows": return "Tailscale" @@ -84,7 +84,7 @@ func defaultTunName() string { return "utun" case "plan9": return "auto" - case "aix", "solaris", "illumos": + case "aix": return "userspace-networking" case "linux": switch distro.Get() { diff --git a/go.mod b/go.mod index 0c1224cf11e1e..ef74dddfa02b8 100644 --- a/go.mod +++ b/go.mod @@ -410,3 +410,5 @@ require ( sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect ) + +replace github.com/tailscale/wireguard-go => github.com/nshalman/wireguard-go v0.0.20200321-0.20250114140547-94bec3171972 diff --git a/go.sum b/go.sum index 8c8da8d148db5..664a0996af44d 100644 --- a/go.sum +++ b/go.sum @@ -748,6 +748,8 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= +github.com/nshalman/wireguard-go v0.0.20200321-0.20250114140547-94bec3171972 h1:BzBBQHKXmdv6L2qivoRY2cz06R0REDlkc/JrPDXsHC4= +github.com/nshalman/wireguard-go v0.0.20200321-0.20250114140547-94bec3171972/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/nunnatsa/ginkgolinter v0.16.1 h1:uDIPSxgVHZ7PgbJElRDGzymkXH+JaF7mjew+Thjnt6Q= github.com/nunnatsa/ginkgolinter v0.16.1/go.mod h1:4tWRinDN1FeJgU+iJANW/kz7xKN5nYRAOfJDQUS9dOQ= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -963,8 +965,6 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= -github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 h1:h/41LFTrwMxB9Xvvug0kRdQCU5TlV1+pAMQw0ZtDE3U= -github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= diff --git a/net/tstun/tstun_stub.go b/net/tstun/tstun_stub.go index d21eda6b07a57..09debbbf3a4f7 100644 --- a/net/tstun/tstun_stub.go +++ b/net/tstun/tstun_stub.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build aix || solaris || illumos +//go:build aix package tstun diff --git a/net/tstun/tun.go b/net/tstun/tun.go index 88679daa24b6c..fe4ed515d2314 100644 --- a/net/tstun/tun.go +++ b/net/tstun/tun.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build !wasm && !tamago && !aix && !solaris && !illumos +//go:build !wasm && !tamago && !aix // Package tun creates a tuntap device, working around OS-specific // quirks if necessary. diff --git a/tool/go b/tool/go index 1c53683d52f95..f4e76864974d2 100755 --- a/tool/go +++ b/tool/go @@ -4,4 +4,10 @@ # currently-desired version from https://github.com/tailscale/go, # downloading it first if necessary. +case $(uname -s) in + SunOS) + exec go "$@" + ;; +esac + exec "$(dirname "$0")/../tool/gocross/gocross-wrapper.sh" "$@" diff --git a/wgengine/router/router_solaris.go b/wgengine/router/router_solaris.go new file mode 100644 index 0000000000000..98e243d9402ff --- /dev/null +++ b/wgengine/router/router_solaris.go @@ -0,0 +1,46 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package router + +import ( + "strings" + + "github.com/tailscale/wireguard-go/tun" + "tailscale.com/health" + "tailscale.com/net/netmon" + "tailscale.com/types/logger" +) + +// For now this router only supports the userspace WireGuard implementations. + +func newUserspaceRouter(logf logger.Logf, tundev tun.Device, linkMon *netmon.Monitor, health *health.Tracker) (Router, error) { + return newUserspaceSunosRouter(logf, tundev, linkMon, health) +} + +func cleanUp(logf logger.Logf, interfaceName string) { + ipadm := []string{"ipadm", "show-addr", "-p", "-o", "addrobj"} + out, err := cmd(ipadm...).Output() + if err != nil { + logf("ipadm show-addr: %v\n%s", err, out) + } + for _, a := range strings.Fields(string(out)) { + s := strings.Split(a, "/") + if len(s) > 1 && strings.Contains(s[1], "tailscale") { + ipadm = []string{"ipadm", "down-addr", "-t", a} + cmdVerbose(logf, ipadm) + ipadm = []string{"ipadm", "delete-addr", a} + cmdVerbose(logf, ipadm) + ipadm = []string{"ipadm", "delete-if", s[0]} + cmdVerbose(logf, ipadm) + } + } + ifcfg := []string{"ifconfig", interfaceName, "unplumb"} + if out, err := cmd(ifcfg...).CombinedOutput(); err != nil { + logf("ifconfig unplumb: %v\n%s", err, out) + } + ifcfg = []string{"ifconfig", interfaceName, "inet6", "unplumb"} + if out, err := cmd(ifcfg...).CombinedOutput(); err != nil { + logf("ifconfig inet6 unplumb: %v\n%s", err, out) + } +} diff --git a/wgengine/router/router_userspace_solaris.go b/wgengine/router/router_userspace_solaris.go new file mode 100644 index 0000000000000..51ca435cb936f --- /dev/null +++ b/wgengine/router/router_userspace_solaris.go @@ -0,0 +1,233 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +//go:build illumos || solaris +// +build illumos solaris + +package router + +import ( + "fmt" + "log" + "net/netip" + "os/exec" + + "github.com/tailscale/wireguard-go/tun" + "go4.org/netipx" + "tailscale.com/health" + "tailscale.com/net/netmon" + "tailscale.com/types/logger" +) + +type userspaceSunosRouter struct { + logf logger.Logf + linkMon *netmon.Monitor + health *health.Tracker + tunname string + local []netip.Prefix + routes map[netip.Prefix]struct{} +} + +func newUserspaceSunosRouter(logf logger.Logf, tundev tun.Device, linkMon *netmon.Monitor, health *health.Tracker) (Router, error) { + tunname, err := tundev.Name() + if err != nil { + return nil, err + } + + return &userspaceSunosRouter{ + logf: logf, + linkMon: linkMon, + health: health, + tunname: tunname, + }, nil +} + +func (r *userspaceSunosRouter) addrsToRemove(newLocalAddrs []netip.Prefix) (remove []netip.Prefix) { + for _, cur := range r.local { + found := false + for _, v := range newLocalAddrs { + found = (v == cur) + if found { + break + } + } + if !found { + remove = append(remove, cur) + } + } + return +} + +func (r *userspaceSunosRouter) addrsToAdd(newLocalAddrs []netip.Prefix) (add []netip.Prefix) { + for _, cur := range newLocalAddrs { + found := false + for _, v := range r.local { + found = (v == cur) + if found { + break + } + } + if !found { + add = append(add, cur) + } + } + return +} + +func cmd(args ...string) *exec.Cmd { + if len(args) == 0 { + log.Fatalf("exec.Cmd(%#v) invalid; need argv[0]", args) + } + return exec.Command(args[0], args[1:]...) +} + +func cmdVerbose(logf logger.Logf, args []string) (string, error) { + o, err := cmd(args...).CombinedOutput() + out := string(o) + if err != nil { + logf("cmd %v failed: %v\n%s", args, err, string(out)) + } + return out, err +} + +func (r *userspaceSunosRouter) Up() error { + ifup := []string{"ifconfig", r.tunname, "up"} + if out, err := cmd(ifup...).CombinedOutput(); err != nil { + r.logf("running ifconfig failed: %v\n%s", err, out) + // this seems to fail harmlessly on illumos + //return err + } + return nil +} + +func inet(p netip.Prefix) string { + if p.Addr().Is6() { + return "inet6" + } + return "inet" +} + +func (r *userspaceSunosRouter) Set(cfg *Config) (reterr error) { + if cfg == nil { + cfg = &shutdownConfig + } + + var errq error + setErr := func(err error) { + if errq == nil { + errq = err + } + } + + // illumos requires routes to have a nexthop. For routes such as + // ours where the nexthop is meaningless, you're supposed to use + // one of the local IP addresses of the interface. Find an IPv4 + // and IPv6 address we can use for this purpose. + var firstGateway4 string + var firstGateway6 string + for _, addr := range cfg.LocalAddrs { + if addr.Addr().Is4() && firstGateway4 == "" { + firstGateway4 = addr.Addr().String() + } else if addr.Addr().Is6() && firstGateway6 == "" { + firstGateway6 = addr.Addr().String() + } + } + + // Update the addresses. TODO(nshalman) + for _, addr := range r.addrsToRemove(cfg.LocalAddrs) { + arg := []string{"ifconfig", r.tunname, inet(addr), addr.String(), "-alias"} + out, err := cmd(arg...).CombinedOutput() + if err != nil { + r.logf("addr del failed: %v => %v\n%s", arg, err, out) + setErr(err) + } + } + for _, addr := range r.addrsToAdd(cfg.LocalAddrs) { + addrString := fmt.Sprintf("local=%s,remote=%s", addr.String(), addr.Addr().String()) + addrObj := r.tunname + "/tailscale" + inet(addr) + // TODO(2024-05-18) fix will be a year old. remove workaround + // This is a mitigation to odd behaviour first noticed in 1.44, but that may have existed even before that... + // It is *probably* https://www.illumos.org/issues/13316 based on the system where it was seen + // I will leave this in place for a while until most distros have pulled in the fix which landed + // in upstream illumos on Thu, 18 May 2023 01:24:32 +0000 + var arg0 = []string{"ipadm", "delete-addr", addrObj} + _, err := cmd(arg0...).CombinedOutput() + // Under normal circumstances this should fail. If it didn't we have tripped the bug and should log it. + if err == nil { + r.logf("BUG: unexpected delete-addr success for addrobj: %s", addrObj) + } + var arg = []string{"ipadm", "create-addr", "-t", "-T", "static", "-a", addrString, addrObj} + out, err := cmd(arg...).CombinedOutput() + if err != nil { + r.logf("addr add failed: %v => %v\n%s", arg, err, out) + setErr(err) + } + var arg2 = []string{"ifconfig"} + out, err = cmd(arg2...).CombinedOutput() + r.logf("%v => %v\n%s", arg, err, out) + } + + newRoutes := make(map[netip.Prefix]struct{}) + for _, route := range cfg.Routes { + newRoutes[route] = struct{}{} + } + // Delete any pre-existing routes. + for route := range r.routes { + if _, keep := newRoutes[route]; !keep { + net := netipx.PrefixIPNet(route) + nip := net.IP.Mask(net.Mask) + nstr := fmt.Sprintf("%v/%d", nip, route.Bits()) + del := "delete" + routedel := []string{"route", "-q", "-n", + del, "-" + inet(route), nstr, + "-iface", r.tunname} + out, err := cmd(routedel...).CombinedOutput() + if err != nil { + r.logf("route delete failed: %v: %v\n%s", routedel, err, out) + setErr(err) + } + } + } + for route := range newRoutes { + if _, exists := r.routes[route]; !exists { + net := netipx.PrefixIPNet(route) + nip := net.IP.Mask(net.Mask) + nstr := fmt.Sprintf("%v/%d", nip, route.Bits()) + var gateway string + if route.Addr().Is4() && firstGateway4 != "" { + gateway = firstGateway4 + } + if route.Addr().Is6() && firstGateway6 != "" { + gateway = firstGateway6 + } + routeadd := []string{"route", "-q", "-n", + "add", "-" + inet(route), nstr, + "-ifp", r.tunname, gateway, "-iface"} + out, err := cmd(routeadd...).CombinedOutput() + if err != nil { + r.logf("addr add failed: %v: %v\n%s", routeadd, err, out) + setErr(err) + } + } + } + + // Store the interface and routes so we know what to change on an update. + if errq == nil { + r.local = append([]netip.Prefix{}, cfg.LocalAddrs...) + } + r.routes = newRoutes + + return errq +} + +func (r *userspaceSunosRouter) Close() error { + cleanUp(r.logf, r.tunname) + return nil +} + +// UpdateMagicsockPort implements the Router interface. This implementation +// does nothing and returns nil because this router does not currently need +// to know what the magicsock UDP port is. +func (r *userspaceSunosRouter) UpdateMagicsockPort(_ uint16, _ string) error { + return nil +}