diff --git a/.github/workflows/cross-illumos.yaml b/.github/workflows/cross-illumos.yaml new file mode 100644 index 0000000000000..fea55f566c367 --- /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@v3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + 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..6dfd070dbf8b2 --- /dev/null +++ b/.github/workflows/nshalman-sunos-releases.yml @@ -0,0 +1,39 @@ +--- +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@v3 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v4 + 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 + tailscaled-illumos + tailscaled-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..763eede65a338 --- /dev/null +++ b/build.sh @@ -0,0 +1,30 @@ +#!/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 + bash -x ./build_dist.sh --box ./cmd/tailscaled + fix_osabi tailscaled + mv tailscaled{,-${GOOS}} +done + +ln cmd/tailscaled/tailscale.xml . +shasum -a 256 tailscaled-* 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 29daf7113abbc..2b3e2165ea3cb 100644 --- a/cmd/tailscaled/tailscaled.go +++ b/cmd/tailscaled/tailscaled.go @@ -68,6 +68,8 @@ import ( // defaultTunName returns the default tun device name for the platform. func defaultTunName() string { switch runtime.GOOS { + case "illumos", "solaris": + return "tun" case "openbsd": return "tun" case "windows": diff --git a/derper b/derper new file mode 100755 index 0000000000000..06ac2e3559624 Binary files /dev/null and b/derper differ diff --git a/go.mod b/go.mod index 34b5bd7c5196b..17bdc8f4fb011 100644 --- a/go.mod +++ b/go.mod @@ -365,3 +365,5 @@ require ( sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.3.0 // indirect ) + +replace github.com/tailscale/wireguard-go => github.com/nshalman/wireguard-go v0.0.20200321-0.20230423150539-55df1b6c04ac diff --git a/go.sum b/go.sum index 562315a02e2c3..3572bf590e3b1 100644 --- a/go.sum +++ b/go.sum @@ -696,6 +696,8 @@ github.com/nishanths/exhaustive v0.10.0 h1:BMznKAcVa9WOoLq/kTGp4NJOJSMwEpcpjFNAV github.com/nishanths/exhaustive v0.10.0/go.mod h1:IbwrGdVMizvDcIxPYGVdQn5BqWJaOwpCvg4RGb8r/TA= 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.20230423150539-55df1b6c04ac h1:wEG2xJ+7q062tBEU8gkJvN2hC5RpZWhEIWAug5LUC0k= +github.com/nshalman/wireguard-go v0.0.20200321-0.20230423150539-55df1b6c04ac/go.mod h1:J62uaH3KrbhKtrKXQJ6/Z+kcs0r4CdA7qd5AXQtl0/s= github.com/nunnatsa/ginkgolinter v0.11.2 h1:xzQpAsEyZe5F1RMy2Z5kn8UFCGiWfKqJOUd2ZzBXA4M= github.com/nunnatsa/ginkgolinter v0.11.2/go.mod h1:dJIGXYXbkBswqa/pIzG0QlVTTDSBMxDoCFwhsl4Uras= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= @@ -884,8 +886,6 @@ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tailscale/web-client-prebuilt v0.0.0-20230919211114-7bcd7bca7bc5 h1:wKUtQPRpjhZZvAuwYRMcjMZnpWSUEJWIbNJmLtDbR0k= github.com/tailscale/web-client-prebuilt v0.0.0-20230919211114-7bcd7bca7bc5/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= -github.com/tailscale/wireguard-go v0.0.0-20230929223258-2f6748dc88e7 h1:P1od5W+cX/LZZyvbKrNUXuuzxensnKEywLhxhPOeHuY= -github.com/tailscale/wireguard-go v0.0.0-20230929223258-2f6748dc88e7/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 8338c7f6b93df..e71af4329e2eb 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -3141,7 +3141,7 @@ func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) { }) } switch runtime.GOOS { - case "linux", "freebsd", "openbsd", "illumos", "darwin", "windows", "android", "ios": + case "linux", "freebsd", "openbsd", "illumos", "solaris", "darwin", "windows", "android", "ios": // These are the platforms currently supported by // net/dns/resolver/tsdns.go:Resolver.HandleExitNodeDNSQuery. ret = append(ret, tailcfg.Service{ diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 07fcd08d1e8b1..96bf4372cda6c 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -617,6 +617,8 @@ func osEmoji(os string) string { return "🐡" case "illumos": return "☀️" + case "solaris": + return "🌤️" } return "👽" } diff --git a/net/dns/manager_default.go b/net/dns/manager_default.go index 41828ef822fac..5180806e4e51a 100644 --- a/net/dns/manager_default.go +++ b/net/dns/manager_default.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build !linux && !freebsd && !openbsd && !windows && !darwin +//go:build !linux && !freebsd && !openbsd && !windows && !darwin && !illumos && !solaris package dns diff --git a/net/dns/manager_solaris.go b/net/dns/manager_solaris.go new file mode 100644 index 0000000000000..6253182304022 --- /dev/null +++ b/net/dns/manager_solaris.go @@ -0,0 +1,12 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package dns + +import ( + "tailscale.com/types/logger" +) + +func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) { + return newDirectManager(logf), nil +} diff --git a/net/dns/resolver/tsdns.go b/net/dns/resolver/tsdns.go index ddb7b6cdba713..fd86a5564d311 100644 --- a/net/dns/resolver/tsdns.go +++ b/net/dns/resolver/tsdns.go @@ -350,7 +350,7 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne // but for now that's probably good enough. Later we'll // want to blend in everything from scutil --dns. fallthrough - case "linux", "freebsd", "openbsd", "illumos", "ios": + case "linux", "freebsd", "openbsd", "illumos", "solaris", "ios": nameserver, err := stubResolverForOS() if err != nil { r.logf("stubResolverForOS: %v", err) diff --git a/net/netutil/ip_forward.go b/net/netutil/ip_forward.go index afcea4e5adab5..1d5e4b0052346 100644 --- a/net/netutil/ip_forward.go +++ b/net/netutil/ip_forward.go @@ -9,6 +9,7 @@ import ( "fmt" "net/netip" "os" + "os/exec" "path/filepath" "runtime" "strconv" @@ -62,6 +63,11 @@ func CheckIPForwarding(routes []netip.Prefix, state *interfaces.State) (warn, er switch runtime.GOOS { case "dragonfly", "freebsd", "netbsd", "openbsd": return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS), nil + case "illumos", "solaris": + _, err := ipForwardingEnabledSunOS(ipv4, "") + if err != nil { + return nil, fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, "") + } } return nil, nil } @@ -221,3 +227,24 @@ func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) { on := val == 1 || val == 2 return on, nil } + +func ipForwardingEnabledSunOS(p protocol, iface string) (bool, error) { + var proto string + if p == ipv4 { + proto = "ipv4" + } else if p == ipv6 { + proto = "ipv6" + } else { + return false, fmt.Errorf("unknown protocol") + } + + ipadmCmd := "\"ipadm show-prop " + proto + " -p forwarding -o CURRENT -c\"" + bs, err := exec.Command("ipadm", "show-prop", proto, "-p", "forwarding", "-o", "CURRENT", "-c").Output() + if err != nil { + return false, fmt.Errorf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", ipadmCmd, err) + } + if string(bs) != "on\n" { + return false, fmt.Errorf("IP forwarding is set to off. Subnet routes won't work. Try 'routeadm -u -e " + proto + "-forwarding'") + } + return true, nil +} diff --git a/paths/paths_unix.go b/paths/paths_unix.go index fb081cdf434a2..9543dceb5df2d 100644 --- a/paths/paths_unix.go +++ b/paths/paths_unix.go @@ -22,7 +22,7 @@ func init() { func statePath() string { switch runtime.GOOS { - case "linux": + case "linux", "illumos", "solaris": return "/var/lib/tailscale/tailscaled.state" case "freebsd", "openbsd": return "/var/db/tailscale/tailscaled.state" 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_default.go b/wgengine/router/router_default.go index 9fee5d8f2a304..d812ce9690b19 100644 --- a/wgengine/router/router_default.go +++ b/wgengine/router/router_default.go @@ -1,7 +1,7 @@ // Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause -//go:build !windows && !linux && !darwin && !openbsd && !freebsd +//go:build !windows && !linux && !darwin && !openbsd && !freebsd && !illumos && !solaris package router diff --git a/wgengine/router/router_solaris.go b/wgengine/router/router_solaris.go new file mode 100644 index 0000000000000..5c932edf0d989 --- /dev/null +++ b/wgengine/router/router_solaris.go @@ -0,0 +1,45 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package router + +import ( + "strings" + + "github.com/tailscale/wireguard-go/tun" + "tailscale.com/types/logger" + "tailscale.com/net/netmon" +) + +// For now this router only supports the userspace WireGuard implementations. + +func newUserspaceRouter(logf logger.Logf, tundev tun.Device, linkMon *netmon.Monitor) (Router, error) { + return newUserspaceSunosRouter(logf, tundev, linkMon) +} + +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..57254eb533322 --- /dev/null +++ b/wgengine/router/router_userspace_solaris.go @@ -0,0 +1,223 @@ +// 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/types/logger" + "tailscale.com/net/netmon" +) + +type userspaceSunosRouter struct { + logf logger.Logf + linkMon *netmon.Monitor + tunname string + local []netip.Prefix + routes map[netip.Prefix]struct{} +} + +func newUserspaceSunosRouter(logf logger.Logf, tundev tun.Device, linkMon *netmon.Monitor) (Router, error) { + tunname, err := tundev.Name() + if err != nil { + return nil, err + } + + return &userspaceSunosRouter{ + logf: logf, + linkMon: linkMon, + 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 +}