diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 589d8c58..d92e9e6e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,22 +14,38 @@ jobs: steps: - uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + files: | + go.* + **/*.go + integration_test/ + config-example.yaml - name: Setup Go + if: steps.changed-files.outputs.any_changed == 'true' uses: actions/setup-go@v2 with: go-version: "1.17" - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' run: | go version sudo apt update sudo apt install -y make - name: Run build + if: steps.changed-files.outputs.any_changed == 'true' run: make build - uses: actions/upload-artifact@v2 + if: steps.changed-files.outputs.any_changed == 'true' with: name: headscale-linux path: headscale diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6fb985f7..26a24ae7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,8 +8,21 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + files: | + go.* + **/*.go + integration_test/ + config-example.yaml - name: golangci-lint + if: steps.changed-files.outputs.any_changed == 'true' uses: golangci/golangci-lint-action@v2 with: version: latest @@ -24,8 +37,26 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + files: | + **/*.md + **/*.yml + **/*.yaml + **/*.ts + **/*.js + **/*.sass + **/*.css + **/*.scss + **/*.html - name: Prettify code + if: steps.changed-files.outputs.any_changed == 'true' uses: creyD/prettier_action@v4.0 with: prettier_options: >- diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index d4179ea2..9f526f97 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -3,21 +3,30 @@ name: CI on: [pull_request] jobs: - # The "build" workflow integration-test: - # The type of runner that the job will run on runs-on: ubuntu-latest - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + files: | + go.* + **/*.go + integration_test/ + config-example.yaml - # Setup Go - name: Setup Go + if: steps.changed-files.outputs.any_changed == 'true' uses: actions/setup-go@v2 with: go-version: "1.17" - name: Run Integration tests + if: steps.changed-files.outputs.any_changed == 'true' run: go test -tags integration -timeout 30m diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 651063fc..9ce8a779 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,31 +3,41 @@ name: CI on: [push, pull_request] jobs: - # The "build" workflow test: - # The type of runner that the job will run on runs-on: ubuntu-latest - # Steps represent a sequence of tasks that will be executed as part of the job steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v14.1 + with: + files: | + go.* + **/*.go + integration_test/ + config-example.yaml - # Setup Go - name: Setup Go + if: steps.changed-files.outputs.any_changed == 'true' uses: actions/setup-go@v2 with: - go-version: "1.17" # The Go version to download (if necessary) and use. + go-version: "1.17" - # Install all the dependencies - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' run: | go version sudo apt update sudo apt install -y make - name: Run tests + if: steps.changed-files.outputs.any_changed == 'true' run: make test - name: Run build + if: steps.changed-files.outputs.any_changed == 'true' run: make diff --git a/.goreleaser.yml b/.goreleaser.yml index 5073b57c..d1dec26f 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,8 +1,7 @@ -# This is an example .goreleaser.yml file with some sane defaults. -# Make sure to check the documentation at http://goreleaser.com +--- before: hooks: - - go mod tidy + - go mod tidy -compat=1.17 release: prerelease: auto @@ -33,7 +32,7 @@ builds: goarch: - arm goarm: - - 7 + - "7" env: - CC=arm-linux-gnueabihf-gcc - CXX=arm-linux-gnueabihf-g++ diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ba96eab..6c74144e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ **TBD (TBD):** +**0.13.0 (2022-xx-xx):** + +**Features**: + +- Add IPv6 support to the prefix assigned to namespaces + +**Changes**: + +- `ip_prefix` is now superseded by `ip_prefixes` in the configuration [#208](https://github.com/juanfont/headscale/pull/208) + +**0.12.4 (2022-01-29):** + **Changes**: - Make gRPC Unix Socket permissions configurable [#292](https://github.com/juanfont/headscale/pull/292) diff --git a/Dockerfile b/Dockerfile index d5fe3421..050439b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Builder image -FROM golang:1.17.6-bullseye AS build +FROM docker.io/golang:1.17.1-bullseye AS build ENV GOPATH /go WORKDIR /go/src/headscale diff --git a/Dockerfile.alpine b/Dockerfile.alpine index 8185dbe4..a75bbfea 100644 --- a/Dockerfile.alpine +++ b/Dockerfile.alpine @@ -1,5 +1,5 @@ # Builder image -FROM golang:1.17.6-alpine AS build +FROM docker.io/golang:1.17.1-alpine AS build ENV GOPATH /go WORKDIR /go/src/headscale @@ -14,7 +14,7 @@ RUN strip /go/bin/headscale RUN test -e /go/bin/headscale # Production image -FROM alpine:latest +FROM docker.io/alpine:latest COPY --from=build /go/bin/headscale /bin/headscale ENV TZ UTC diff --git a/Dockerfile.debug b/Dockerfile.debug index 320b5c93..b81ee67e 100644 --- a/Dockerfile.debug +++ b/Dockerfile.debug @@ -1,5 +1,5 @@ # Builder image -FROM golang:1.17.1-bullseye AS build +FROM docker.io/golang:1.17.1-bullseye AS build ENV GOPATH /go WORKDIR /go/src/headscale diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale index df8cdcb1..f96c6b9c 100644 --- a/Dockerfile.tailscale +++ b/Dockerfile.tailscale @@ -7,5 +7,5 @@ RUN apt-get update \ && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.gpg | apt-key add - \ && curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/focal.list | tee /etc/apt/sources.list.d/tailscale.list \ && apt-get update \ - && apt-get install -y tailscale=${TAILSCALE_VERSION} \ + && apt-get install -y tailscale=${TAILSCALE_VERSION} dnsutils \ && rm -rf /var/lib/apt/lists/* diff --git a/Makefile b/Makefile index 6b4c02ff..69935bc0 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ test: @go test -coverprofile=coverage.out ./... test_integration: - go test -tags integration -timeout 30m ./... + go test -tags integration -timeout 30m -count=1 ./... test_integration_cli: go test -tags integration -v integration_cli_test.go integration_common_test.go diff --git a/acls.go b/acls.go index 1d54b26d..326625b9 100644 --- a/acls.go +++ b/acls.go @@ -188,7 +188,7 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) { return nil, errInvalidNamespace } for _, node := range nodes { - ips = append(ips, node.IPAddress) + ips = append(ips, node.IPAddresses.ToStringSlice()...) } } @@ -222,7 +222,7 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) { // FIXME: Check TagOwners allows this for _, t := range hostinfo.RequestTags { if alias[4:] == t { - ips = append(ips, machine.IPAddress) + ips = append(ips, machine.IPAddresses.ToStringSlice()...) break } @@ -241,7 +241,7 @@ func (h *Headscale) expandAlias(alias string) ([]string, error) { } ips := []string{} for _, n := range nodes { - ips = append(ips, n.IPAddress) + ips = append(ips, n.IPAddresses.ToStringSlice()...) } return ips, nil diff --git a/acls_test.go b/acls_test.go index 629ce1da..c35f4f84 100644 --- a/acls_test.go +++ b/acls_test.go @@ -61,9 +61,9 @@ func (s *Suite) TestPortRange(c *check.C) { c.Assert(rules, check.NotNil) c.Assert(rules, check.HasLen, 1) - c.Assert((rules)[0].DstPorts, check.HasLen, 1) - c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(5400)) - c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500)) + c.Assert(rules[0].DstPorts, check.HasLen, 1) + c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(5400)) + c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(5500)) } func (s *Suite) TestPortWildcard(c *check.C) { @@ -75,11 +75,11 @@ func (s *Suite) TestPortWildcard(c *check.C) { c.Assert(rules, check.NotNil) c.Assert(rules, check.HasLen, 1) - c.Assert((rules)[0].DstPorts, check.HasLen, 1) - c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) - c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) - c.Assert((rules)[0].SrcIPs, check.HasLen, 1) - c.Assert((rules)[0].SrcIPs[0], check.Equals, "*") + c.Assert(rules[0].DstPorts, check.HasLen, 1) + c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) + c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) + c.Assert(rules[0].SrcIPs, check.HasLen, 1) + c.Assert(rules[0].SrcIPs[0], check.Equals, "*") } func (s *Suite) TestPortNamespace(c *check.C) { @@ -91,7 +91,7 @@ func (s *Suite) TestPortNamespace(c *check.C) { _, err = app.GetMachine("testnamespace", "testmachine") c.Assert(err, check.NotNil) - ip, _ := app.getAvailableIP() + ips, _ := app.getAvailableIPs() machine := Machine{ ID: 0, MachineKey: "foo", @@ -101,7 +101,7 @@ func (s *Suite) TestPortNamespace(c *check.C) { NamespaceID: namespace.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: ip.String(), + IPAddresses: ips, AuthKeyID: uint(pak.ID), } app.db.Save(&machine) @@ -116,12 +116,13 @@ func (s *Suite) TestPortNamespace(c *check.C) { c.Assert(rules, check.NotNil) c.Assert(rules, check.HasLen, 1) - c.Assert((rules)[0].DstPorts, check.HasLen, 1) - c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) - c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) - c.Assert((rules)[0].SrcIPs, check.HasLen, 1) - c.Assert((rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip") - c.Assert((rules)[0].SrcIPs[0], check.Equals, ip.String()) + c.Assert(rules[0].DstPorts, check.HasLen, 1) + c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) + c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) + c.Assert(rules[0].SrcIPs, check.HasLen, 1) + c.Assert(rules[0].SrcIPs[0], check.Not(check.Equals), "not an ip") + c.Assert(len(ips), check.Equals, 1) + c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String()) } func (s *Suite) TestPortGroup(c *check.C) { @@ -133,7 +134,7 @@ func (s *Suite) TestPortGroup(c *check.C) { _, err = app.GetMachine("testnamespace", "testmachine") c.Assert(err, check.NotNil) - ip, _ := app.getAvailableIP() + ips, _ := app.getAvailableIPs() machine := Machine{ ID: 0, MachineKey: "foo", @@ -143,7 +144,7 @@ func (s *Suite) TestPortGroup(c *check.C) { NamespaceID: namespace.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: ip.String(), + IPAddresses: ips, AuthKeyID: uint(pak.ID), } app.db.Save(&machine) @@ -156,10 +157,11 @@ func (s *Suite) TestPortGroup(c *check.C) { c.Assert(rules, check.NotNil) c.Assert(rules, check.HasLen, 1) - c.Assert((rules)[0].DstPorts, check.HasLen, 1) - c.Assert((rules)[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) - c.Assert((rules)[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) - c.Assert((rules)[0].SrcIPs, check.HasLen, 1) - c.Assert((rules)[0].SrcIPs[0], check.Not(check.Equals), "not an ip") - c.Assert((rules)[0].SrcIPs[0], check.Equals, ip.String()) + c.Assert(rules[0].DstPorts, check.HasLen, 1) + c.Assert(rules[0].DstPorts[0].Ports.First, check.Equals, uint16(0)) + c.Assert(rules[0].DstPorts[0].Ports.Last, check.Equals, uint16(65535)) + c.Assert(rules[0].SrcIPs, check.HasLen, 1) + c.Assert(rules[0].SrcIPs[0], check.Not(check.Equals), "not an ip") + c.Assert(len(ips), check.Equals, 1) + c.Assert(rules[0].SrcIPs[0], check.Equals, ips[0].String()) } diff --git a/api.go b/api.go index d3bd5726..020ded01 100644 --- a/api.go +++ b/api.go @@ -497,6 +497,7 @@ func (h *Headscale) handleMachineRegistrationNew( ctx.Data(http.StatusOK, "application/json; charset=utf-8", respBody) } +// TODO: check if any locks are needed around IP allocation. func (h *Headscale) handleAuthKey( ctx *gin.Context, machineKey key.MachinePublic, @@ -554,14 +555,14 @@ func (h *Headscale) handleAuthKey( log.Debug(). Str("func", "handleAuthKey"). Str("machine", machine.Name). - Msg("Authentication key was valid, proceeding to acquire an IP address") - ip, err := h.getAvailableIP() + Msg("Authentication key was valid, proceeding to acquire IP addresses") + ips, err := h.getAvailableIPs() if err != nil { log.Error(). Caller(). Str("func", "handleAuthKey"). Str("machine", machine.Name). - Msg("Failed to find an available IP") + Msg("Failed to find an available IP address") machineRegistrations.WithLabelValues("new", "authkey", "error", machine.Namespace.Name). Inc() @@ -570,12 +571,12 @@ func (h *Headscale) handleAuthKey( log.Info(). Str("func", "handleAuthKey"). Str("machine", machine.Name). - Str("ip", ip.String()). - Msgf("Assigning %s to %s", ip, machine.Name) + Str("ips", strings.Join(ips.ToStringSlice(), ",")). + Msgf("Assigning %s to %s", strings.Join(ips.ToStringSlice(), ","), machine.Name) machine.Expiry = ®isterRequest.Expiry machine.AuthKeyID = uint(pak.ID) - machine.IPAddress = ip.String() + machine.IPAddresses = ips machine.NamespaceID = pak.NamespaceID machine.NodeKey = NodePublicKeyStripPrefix(registerRequest.NodeKey) @@ -610,6 +611,6 @@ func (h *Headscale) handleAuthKey( log.Info(). Str("func", "handleAuthKey"). Str("machine", machine.Name). - Str("ip", machine.IPAddress). + Str("ips", strings.Join(machine.IPAddresses.ToStringSlice(), ", ")). Msg("Successfully authenticated via AuthKey") } diff --git a/app.go b/app.go index 62d284b6..a9ed749b 100644 --- a/app.go +++ b/app.go @@ -73,7 +73,7 @@ type Config struct { ServerURL string Addr string EphemeralNodeInactivityTimeout time.Duration - IPPrefix netaddr.IPPrefix + IPPrefixes []netaddr.IPPrefix PrivateKeyPath string BaseDomain string @@ -204,9 +204,7 @@ func NewHeadscale(cfg Config) (*Headscale, error) { } if app.cfg.DNSConfig != nil && app.cfg.DNSConfig.Proxied { // if MagicDNS - magicDNSDomains := generateMagicDNSRootDomains( - app.cfg.IPPrefix, - ) + magicDNSDomains := generateMagicDNSRootDomains(app.cfg.IPPrefixes) // we might have routes already from Split DNS if app.cfg.DNSConfig.Routes == nil { app.cfg.DNSConfig.Routes = make(map[string][]dnstype.Resolver) diff --git a/app_test.go b/app_test.go index a53a8802..31c5c363 100644 --- a/app_test.go +++ b/app_test.go @@ -41,7 +41,9 @@ func (s *Suite) ResetDB(c *check.C) { c.Fatal(err) } cfg := Config{ - IPPrefix: netaddr.MustParseIPPrefix("10.27.0.0/23"), + IPPrefixes: []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("10.27.0.0/23"), + }, } app = Headscale{ diff --git a/cli_test.go b/cli_test.go index ef7e2993..71f2bea5 100644 --- a/cli_test.go +++ b/cli_test.go @@ -4,6 +4,7 @@ import ( "time" "gopkg.in/check.v1" + "inet.af/netaddr" ) func (s *Suite) TestRegisterMachine(c *check.C) { @@ -19,16 +20,17 @@ func (s *Suite) TestRegisterMachine(c *check.C) { DiscoKey: "faa", Name: "testmachine", NamespaceID: namespace.ID, - IPAddress: "10.0.0.1", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("10.0.0.1")}, Expiry: &now, } - app.db.Save(&machine) + err = app.db.Save(&machine).Error + c.Assert(err, check.IsNil) - _, err = app.GetMachine("test", "testmachine") + _, err = app.GetMachine(namespace.Name, machine.Name) c.Assert(err, check.IsNil) machineAfterRegistering, err := app.RegisterMachine( - "8ce002a935f8c394e55e78fbbb410576575ff8ec5cfa2e627e4b807f1be15b0e", + machine.MachineKey, namespace.Name, ) c.Assert(err, check.IsNil) diff --git a/cmd/headscale/cli/nodes.go b/cmd/headscale/cli/nodes.go index 26ead6dc..d6b86ee1 100644 --- a/cmd/headscale/cli/nodes.go +++ b/cmd/headscale/cli/nodes.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "strconv" + "strings" "time" survey "github.com/AlecAivazis/survey/v2" @@ -459,7 +460,7 @@ func nodesToPtables( "Name", "NodeKey", "Namespace", - "IP address", + "IP addresses", "Ephemeral", "Last seen", "Online", @@ -523,7 +524,7 @@ func nodesToPtables( machine.Name, nodeKey.ShortString(), namespace, - machine.IpAddress, + strings.Join(machine.IpAddresses, ", "), strconv.FormatBool(ephemeral), lastSeenTime, online, diff --git a/cmd/headscale/cli/utils.go b/cmd/headscale/cli/utils.go index ebbe1419..b6282988 100644 --- a/cmd/headscale/cli/utils.go +++ b/cmd/headscale/cli/utils.go @@ -48,8 +48,6 @@ func LoadConfig(path string) error { viper.SetDefault("tls_letsencrypt_challenge_type", "HTTP-01") viper.SetDefault("tls_client_auth_mode", "relaxed") - viper.SetDefault("ip_prefix", "100.64.0.0/10") - viper.SetDefault("log_level", "info") viper.SetDefault("dns_config", nil) @@ -235,10 +233,57 @@ func getHeadscaleConfig() headscale.Config { dnsConfig, baseDomain := GetDNSConfig() derpConfig := GetDERPConfig() + configuredPrefixes := viper.GetStringSlice("ip_prefixes") + parsedPrefixes := make([]netaddr.IPPrefix, 0, len(configuredPrefixes)+1) + + legacyPrefixField := viper.GetString("ip_prefix") + if len(legacyPrefixField) > 0 { + log. + Warn(). + Msgf( + "%s, %s", + "use of 'ip_prefix' for configuration is deprecated", + "please see 'ip_prefixes' in the shipped example.", + ) + legacyPrefix, err := netaddr.ParseIPPrefix(legacyPrefixField) + if err != nil { + panic(fmt.Errorf("failed to parse ip_prefix: %w", err)) + } + parsedPrefixes = append(parsedPrefixes, legacyPrefix) + } + + for i, prefixInConfig := range configuredPrefixes { + prefix, err := netaddr.ParseIPPrefix(prefixInConfig) + if err != nil { + panic(fmt.Errorf("failed to parse ip_prefixes[%d]: %w", i, err)) + } + parsedPrefixes = append(parsedPrefixes, prefix) + } + + prefixes := make([]netaddr.IPPrefix, 0, len(parsedPrefixes)) + { + // dedup + normalizedPrefixes := make(map[string]int, len(parsedPrefixes)) + for i, p := range parsedPrefixes { + normalized, _ := p.Range().Prefix() + normalizedPrefixes[normalized.String()] = i + } + + // convert back to list + for _, i := range normalizedPrefixes { + prefixes = append(prefixes, parsedPrefixes[i]) + } + } + + if len(prefixes) < 1 { + prefixes = append(prefixes, netaddr.MustParseIPPrefix("100.64.0.0/10")) + log.Warn().Msgf("'ip_prefixes' not configured, falling back to default: %v", prefixes) + } + return headscale.Config{ ServerURL: viper.GetString("server_url"), Addr: viper.GetString("listen_addr"), - IPPrefix: netaddr.MustParseIPPrefix(viper.GetString("ip_prefix")), + IPPrefixes: prefixes, PrivateKeyPath: absPath(viper.GetString("private_key_path")), BaseDomain: baseDomain, diff --git a/config-example.yaml b/config-example.yaml index b5fc9686..1300ab38 100644 --- a/config-example.yaml +++ b/config-example.yaml @@ -22,6 +22,13 @@ listen_addr: 0.0.0.0:8080 # autogenerated if it's missing private_key_path: /var/lib/headscale/private.key +# List of IP prefixes to allocate tailaddresses from. +# Each prefix consists of either an IPv4 or IPv6 address, +# and the associated prefix length, delimited by a slash. +ip_prefixes: + - fd7a:115c:a1e0::/48 + - 100.64.0.0/10 + # DERP is a relay system that Tailscale uses when a direct # connection cannot be established. # https://tailscale.com/blog/how-tailscale-works/#encrypted-tcp-relays-derp diff --git a/db.go b/db.go index 51363256..7b777863 100644 --- a/db.go +++ b/db.go @@ -28,20 +28,26 @@ func (h *Headscale) initDB() error { h.db = db if h.dbType == Postgres { - db.Exec("create extension if not exists \"uuid-ossp\";") + db.Exec(`create extension if not exists "uuid-ossp";`) } + + _ = db.Migrator().RenameColumn(&Machine{}, "ip_address", "ip_addresses") + err = db.AutoMigrate(&Machine{}) if err != nil { return err } + err = db.AutoMigrate(&KV{}) if err != nil { return err } + err = db.AutoMigrate(&Namespace{}) if err != nil { return err } + err = db.AutoMigrate(&PreAuthKey{}) if err != nil { return err diff --git a/dns.go b/dns.go index af6f989d..df896090 100644 --- a/dns.go +++ b/dns.go @@ -14,6 +14,11 @@ const ( ByteSize = 8 ) +const ( + ipv4AddressLength = 32 + ipv6AddressLength = 128 +) + // generateMagicDNSRootDomains generates a list of DNS entries to be included in `Routes` in `MapResponse`. // This list of reverse DNS entries instructs the OS on what subnets and domains the Tailscale embedded DNS // server (listening in 100.100.100.100 udp/53) should be used for. @@ -34,14 +39,28 @@ const ( // From the netmask we can find out the wildcard bits (the bits that are not set in the netmask). // This allows us to then calculate the subnets included in the subsequent class block and generate the entries. -func generateMagicDNSRootDomains( - ipPrefix netaddr.IPPrefix, -) []dnsname.FQDN { - // TODO(juanfont): we are not handing out IPv6 addresses yet - // and in fact this is Tailscale.com's range (note the fd7a:115c:a1e0: range in the fc00::/7 network) - ipv6base := dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") - fqdns := []dnsname.FQDN{ipv6base} +func generateMagicDNSRootDomains(ipPrefixes []netaddr.IPPrefix) []dnsname.FQDN { + fqdns := make([]dnsname.FQDN, 0, len(ipPrefixes)) + for _, ipPrefix := range ipPrefixes { + var generateDNSRoot func(netaddr.IPPrefix) []dnsname.FQDN + switch ipPrefix.IP().BitLen() { + case ipv4AddressLength: + generateDNSRoot = generateIPv4DNSRootDomain + case ipv6AddressLength: + generateDNSRoot = generateIPv6DNSRootDomain + + default: + panic(fmt.Sprintf("unsupported IP version with address length %d", ipPrefix.IP().BitLen())) + } + + fqdns = append(fqdns, generateDNSRoot(ipPrefix)...) + } + + return fqdns +} + +func generateIPv4DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN { // Conversion to the std lib net.IPnet, a bit easier to operate netRange := ipPrefix.IPNet() maskBits, _ := netRange.Mask.Size() @@ -65,6 +84,7 @@ func generateMagicDNSRootDomains( rdnsSlice = append(rdnsSlice, "in-addr.arpa.") rdnsBase := strings.Join(rdnsSlice, ".") + fqdns := make([]dnsname.FQDN, 0, max-min+1) for i := min; i <= max; i++ { fqdn, err := dnsname.ToFQDN(fmt.Sprintf("%d.%s", i, rdnsBase)) if err != nil { @@ -76,6 +96,54 @@ func generateMagicDNSRootDomains( return fqdns } +func generateIPv6DNSRootDomain(ipPrefix netaddr.IPPrefix) []dnsname.FQDN { + const nibbleLen = 4 + + maskBits, _ := ipPrefix.IPNet().Mask.Size() + expanded := ipPrefix.IP().StringExpanded() + nibbleStr := strings.Map(func(r rune) rune { + if r == ':' { + return -1 + } + + return r + }, expanded) + + // TODO?: that does not look the most efficient implementation, + // but the inputs are not so long as to cause problems, + // and from what I can see, the generateMagicDNSRootDomains + // function is called only once over the lifetime of a server process. + prefixConstantParts := []string{} + for i := 0; i < maskBits/nibbleLen; i++ { + prefixConstantParts = append([]string{string(nibbleStr[i])}, prefixConstantParts...) + } + + makeDomain := func(variablePrefix ...string) (dnsname.FQDN, error) { + prefix := strings.Join(append(variablePrefix, prefixConstantParts...), ".") + + return dnsname.ToFQDN(fmt.Sprintf("%s.ip6.arpa", prefix)) + } + + var fqdns []dnsname.FQDN + if maskBits%4 == 0 { + dom, _ := makeDomain() + fqdns = append(fqdns, dom) + } else { + domCount := 1 << (maskBits % nibbleLen) + fqdns = make([]dnsname.FQDN, 0, domCount) + for i := 0; i < domCount; i++ { + varNibble := fmt.Sprintf("%x", i) + dom, err := makeDomain(varNibble) + if err != nil { + continue + } + fqdns = append(fqdns, dom) + } + } + + return fqdns +} + func getMapResponseDNSConfig( dnsConfigOrig *tailcfg.DNSConfig, baseDomain string, diff --git a/dns_test.go b/dns_test.go index 92f7476f..f8f7fb96 100644 --- a/dns_test.go +++ b/dns_test.go @@ -10,8 +10,10 @@ import ( ) func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { - prefix := netaddr.MustParseIPPrefix("100.64.0.0/10") - domains := generateMagicDNSRootDomains(prefix) + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("100.64.0.0/10"), + } + domains := generateMagicDNSRootDomains(prefixes) found := false for _, domain := range domains { @@ -45,8 +47,10 @@ func (s *Suite) TestMagicDNSRootDomains100(c *check.C) { } func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { - prefix := netaddr.MustParseIPPrefix("172.16.0.0/16") - domains := generateMagicDNSRootDomains(prefix) + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("172.16.0.0/16"), + } + domains := generateMagicDNSRootDomains(prefixes) found := false for _, domain := range domains { @@ -69,6 +73,40 @@ func (s *Suite) TestMagicDNSRootDomains172(c *check.C) { c.Assert(found, check.Equals, true) } +// Happens when netmask is a multiple of 4 bits (sounds likely). +func (s *Suite) TestMagicDNSRootDomainsIPv6Single(c *check.C) { + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48"), + } + domains := generateMagicDNSRootDomains(prefixes) + + c.Assert(len(domains), check.Equals, 1) + c.Assert(domains[0].WithTrailingDot(), check.Equals, "0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa.") +} + +func (s *Suite) TestMagicDNSRootDomainsIPv6SingleMultiple(c *check.C) { + prefixes := []netaddr.IPPrefix{ + netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/50"), + } + domains := generateMagicDNSRootDomains(prefixes) + + yieldsRoot := func(dom string) bool { + for _, candidate := range domains { + if candidate.WithTrailingDot() == dom { + return true + } + } + + return false + } + + c.Assert(len(domains), check.Equals, 4) + c.Assert(yieldsRoot("0.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) + c.Assert(yieldsRoot("1.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) + c.Assert(yieldsRoot("2.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) + c.Assert(yieldsRoot("3.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), check.Equals, true) +} + func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { namespaceShared1, err := app.CreateNamespace("shared1") c.Assert(err, check.IsNil) @@ -124,7 +162,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.1", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, AuthKeyID: uint(preAuthKeyInShared1.ID), } app.db.Save(machineInShared1) @@ -142,7 +180,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { Namespace: *namespaceShared2, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.2", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")}, AuthKeyID: uint(preAuthKeyInShared2.ID), } app.db.Save(machineInShared2) @@ -160,7 +198,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { Namespace: *namespaceShared3, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.3", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")}, AuthKeyID: uint(preAuthKeyInShared3.ID), } app.db.Save(machineInShared3) @@ -178,7 +216,7 @@ func (s *Suite) TestDNSConfigMapResponseWithMagicDNS(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(PreAuthKey2InShared1.ID), } app.db.Save(machine2InShared1) @@ -273,7 +311,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.1", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, AuthKeyID: uint(preAuthKeyInShared1.ID), } app.db.Save(machineInShared1) @@ -291,7 +329,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { Namespace: *namespaceShared2, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.2", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")}, AuthKeyID: uint(preAuthKeyInShared2.ID), } app.db.Save(machineInShared2) @@ -309,7 +347,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { Namespace: *namespaceShared3, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.3", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")}, AuthKeyID: uint(preAuthKeyInShared3.ID), } app.db.Save(machineInShared3) @@ -327,7 +365,7 @@ func (s *Suite) TestDNSConfigMapResponseWithoutMagicDNS(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(preAuthKey2InShared1.ID), } app.db.Save(machine2InShared1) diff --git a/docs/running-headscale-linux.md b/docs/running-headscale-linux.md index 7a62fcc6..973a6eb8 100644 --- a/docs/running-headscale-linux.md +++ b/docs/running-headscale-linux.md @@ -44,7 +44,7 @@ touch /var/lib/headscale/db.sqlite touch /etc/headscale/config.yaml ``` -It is **strongly recommended** to copy and modifiy the [example configuration](../config-example.yaml) +It is **strongly recommended** to copy and modify the [example configuration](../config-example.yaml) from the [headscale repository](../) 6. Start the headscale server: diff --git a/gen/go/headscale/v1/device.pb.go b/gen/go/headscale/v1/device.pb.go index 302c2653..a3b5911a 100644 --- a/gen/go/headscale/v1/device.pb.go +++ b/gen/go/headscale/v1/device.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/device.proto package v1 diff --git a/gen/go/headscale/v1/headscale.pb.go b/gen/go/headscale/v1/headscale.pb.go index dd27265d..ad8a50d7 100644 --- a/gen/go/headscale/v1/headscale.pb.go +++ b/gen/go/headscale/v1/headscale.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/headscale.proto package v1 diff --git a/gen/go/headscale/v1/machine.pb.go b/gen/go/headscale/v1/machine.pb.go index deafbe5c..07613697 100644 --- a/gen/go/headscale/v1/machine.pb.go +++ b/gen/go/headscale/v1/machine.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/machine.proto package v1 @@ -82,7 +82,7 @@ type Machine struct { MachineKey string `protobuf:"bytes,2,opt,name=machine_key,json=machineKey,proto3" json:"machine_key,omitempty"` NodeKey string `protobuf:"bytes,3,opt,name=node_key,json=nodeKey,proto3" json:"node_key,omitempty"` DiscoKey string `protobuf:"bytes,4,opt,name=disco_key,json=discoKey,proto3" json:"disco_key,omitempty"` - IpAddress string `protobuf:"bytes,5,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` + IpAddresses []string `protobuf:"bytes,5,rep,name=ip_addresses,json=ipAddresses,proto3" json:"ip_addresses,omitempty"` Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` Namespace *Namespace `protobuf:"bytes,7,opt,name=namespace,proto3" json:"namespace,omitempty"` Registered bool `protobuf:"varint,8,opt,name=registered,proto3" json:"registered,omitempty"` @@ -154,11 +154,11 @@ func (x *Machine) GetDiscoKey() string { return "" } -func (x *Machine) GetIpAddress() string { +func (x *Machine) GetIpAddresses() []string { if x != nil { - return x.IpAddress + return x.IpAddresses } - return "" + return nil } func (x *Machine) GetName() string { @@ -1026,129 +1026,129 @@ var file_headscale_v1_machine_proto_rawDesc = []byte{ 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1d, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x65, 0x61, 0x75, 0x74, 0x68, 0x6b, - 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf9, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, + 0x65, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfd, 0x04, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6e, 0x6f, 0x64, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x4b, 0x65, 0x79, 0x12, 0x1d, 0x0a, - 0x0a, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x35, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x52, 0x09, 0x6e, 0x61, - 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x72, 0x65, 0x67, 0x69, 0x73, - 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x0e, - 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x37, - 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6c, - 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c, 0x61, 0x73, 0x74, 0x5f, - 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75, 0x70, 0x64, 0x61, 0x74, - 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, - 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, - 0x66, 0x75, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, 0x06, 0x65, 0x78, 0x70, - 0x69, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x3a, 0x0a, - 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0d, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x52, 0x0a, 0x70, - 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x4b, 0x65, 0x79, 0x12, 0x21, 0x0a, + 0x0c, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x05, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0b, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x65, 0x73, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x35, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0a, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x65, 0x64, 0x12, 0x45, 0x0a, 0x0f, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x09, + 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1c, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x52, 0x0e, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65, 0x6e, 0x12, 0x50, 0x0a, 0x16, 0x6c, + 0x61, 0x73, 0x74, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x5f, 0x75, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x75, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x66, 0x75, 0x6c, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x32, 0x0a, + 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x22, 0x48, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, - 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, - 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x4a, - 0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x79, 0x12, 0x3a, 0x0a, 0x0c, 0x70, 0x72, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, + 0x79, 0x52, 0x0a, 0x70, 0x72, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4b, 0x65, 0x79, 0x12, 0x39, 0x0a, + 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x22, 0x48, 0x0a, 0x16, 0x52, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x22, 0x4a, 0x0a, 0x17, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, + 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, + 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, + 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x32, + 0x0a, 0x11, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x49, 0x64, 0x22, 0x45, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, + 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, + 0x22, 0x17, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x45, 0x78, 0x70, + 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, + 0x22, 0x48, 0x0a, 0x15, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, - 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x32, 0x0a, 0x11, 0x47, 0x65, - 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x45, - 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, - 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x17, 0x0a, 0x15, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x35, 0x0a, 0x14, 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x22, 0x48, 0x0a, 0x15, - 0x45, 0x78, 0x70, 0x69, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, - 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x33, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, - 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, - 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x49, 0x0a, 0x14, 0x4c, - 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x08, 0x6d, 0x61, - 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x52, 0x0a, 0x13, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, - 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x47, 0x0a, 0x14, 0x53, 0x68, - 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x22, 0x54, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, - 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, - 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, - 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09, 0x6e, - 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, - 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x49, 0x0a, 0x16, 0x55, 0x6e, 0x73, - 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, - 0x68, 0x69, 0x6e, 0x65, 0x22, 0x77, 0x0a, 0x19, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x33, 0x0a, 0x13, 0x4c, 0x69, + 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x22, 0x4d, 0x0a, - 0x1a, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, - 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, - 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, - 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x2a, 0x82, 0x01, 0x0a, - 0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, - 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, - 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, - 0x48, 0x4f, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x4b, 0x45, 0x59, 0x10, 0x01, 0x12, 0x17, - 0x0a, 0x13, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, - 0x44, 0x5f, 0x43, 0x4c, 0x49, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x45, 0x47, 0x49, 0x53, - 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f, 0x49, 0x44, 0x43, 0x10, - 0x03, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, - 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, - 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, + 0x49, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, + 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x52, 0x08, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x52, 0x0a, 0x13, 0x53, 0x68, + 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, + 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x47, + 0x0a, 0x14, 0x53, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, + 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, + 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x54, 0x0a, 0x15, 0x55, 0x6e, 0x73, 0x68, 0x61, + 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1d, 0x0a, 0x0a, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x49, 0x64, 0x12, + 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x22, 0x49, 0x0a, + 0x16, 0x55, 0x6e, 0x73, 0x68, 0x61, 0x72, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, + 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, + 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, + 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x22, 0x77, 0x0a, 0x19, 0x44, 0x65, 0x62, 0x75, + 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x6f, 0x75, + 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x6f, 0x75, 0x74, 0x65, + 0x73, 0x22, 0x4d, 0x0a, 0x1a, 0x44, 0x65, 0x62, 0x75, 0x67, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x2f, 0x0a, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x15, 0x2e, 0x68, 0x65, 0x61, 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, + 0x2a, 0x82, 0x01, 0x0a, 0x0e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x12, 0x1f, 0x0a, 0x1b, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, + 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, + 0x45, 0x44, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, + 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x41, 0x55, 0x54, 0x48, 0x5f, 0x4b, 0x45, 0x59, + 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x52, 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, + 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x43, 0x4c, 0x49, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x52, + 0x45, 0x47, 0x49, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x4d, 0x45, 0x54, 0x48, 0x4f, 0x44, 0x5f, 0x4f, + 0x49, 0x44, 0x43, 0x10, 0x03, 0x42, 0x29, 0x5a, 0x27, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x75, 0x61, 0x6e, 0x66, 0x6f, 0x6e, 0x74, 0x2f, 0x68, 0x65, 0x61, + 0x64, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x76, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/gen/go/headscale/v1/namespace.pb.go b/gen/go/headscale/v1/namespace.pb.go index f8af539e..341f8ca0 100644 --- a/gen/go/headscale/v1/namespace.pb.go +++ b/gen/go/headscale/v1/namespace.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/namespace.proto package v1 diff --git a/gen/go/headscale/v1/preauthkey.pb.go b/gen/go/headscale/v1/preauthkey.pb.go index 82b16bf8..1169a89a 100644 --- a/gen/go/headscale/v1/preauthkey.pb.go +++ b/gen/go/headscale/v1/preauthkey.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/preauthkey.proto package v1 diff --git a/gen/go/headscale/v1/routes.pb.go b/gen/go/headscale/v1/routes.pb.go index e32e1d15..ba40856d 100644 --- a/gen/go/headscale/v1/routes.pb.go +++ b/gen/go/headscale/v1/routes.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 -// protoc v3.18.1 +// protoc v3.17.3 // source: headscale/v1/routes.proto package v1 diff --git a/gen/openapiv2/headscale/v1/headscale.swagger.json b/gen/openapiv2/headscale/v1/headscale.swagger.json index 2274e22c..f1635ac2 100644 --- a/gen/openapiv2/headscale/v1/headscale.swagger.json +++ b/gen/openapiv2/headscale/v1/headscale.swagger.json @@ -775,8 +775,11 @@ "discoKey": { "type": "string" }, - "ipAddress": { - "type": "string" + "ipAddresses": { + "type": "array", + "items": { + "type": "string" + } }, "name": { "type": "string" diff --git a/integration_common_test.go b/integration_common_test.go index 31bae514..94291fc6 100644 --- a/integration_common_test.go +++ b/integration_common_test.go @@ -8,22 +8,48 @@ import ( "fmt" "time" + "inet.af/netaddr" + "github.com/ory/dockertest/v3" "github.com/ory/dockertest/v3/docker" ) const DOCKER_EXECUTE_TIMEOUT = 10 * time.Second +var IpPrefix4 = netaddr.MustParseIPPrefix("100.64.0.0/10") +var IpPrefix6 = netaddr.MustParseIPPrefix("fd7a:115c:a1e0::/48") + +type ExecuteCommandConfig struct { + timeout time.Duration +} + +type ExecuteCommandOption func(*ExecuteCommandConfig) error + +func ExecuteCommandTimeout(timeout time.Duration) ExecuteCommandOption { + return ExecuteCommandOption(func(conf *ExecuteCommandConfig) error { + conf.timeout = timeout + return nil + }) +} + func ExecuteCommand( resource *dockertest.Resource, cmd []string, env []string, + options ...ExecuteCommandOption, ) (string, error) { var stdout bytes.Buffer var stderr bytes.Buffer - // TODO(kradalby): Make configurable - timeout := DOCKER_EXECUTE_TIMEOUT + execConfig := ExecuteCommandConfig{ + timeout: DOCKER_EXECUTE_TIMEOUT, + } + + for _, opt := range options { + if err := opt(&execConfig); err != nil { + return "", fmt.Errorf("execute-command/options: %w", err) + } + } type result struct { exitCode int @@ -62,16 +88,33 @@ func ExecuteCommand( } return stdout.String(), nil - case <-time.After(timeout): + case <-time.After(execConfig.timeout): - return "", fmt.Errorf("command timed out after %s", timeout) + return "", fmt.Errorf("command timed out after %s", execConfig.timeout) } } func DockerRestartPolicy(config *docker.HostConfig) { - // set AutoRemove to true so that stopped container goes away by itself - config.AutoRemove = true + // set AutoRemove to true so that stopped container goes away by itself on error *immediately*. + // when set to false, containers remain until the end of the integration test. + config.AutoRemove = false config.RestartPolicy = docker.RestartPolicy{ Name: "no", } } + +func DockerAllowLocalIPv6(config *docker.HostConfig) { + if config.Sysctls == nil { + config.Sysctls = make(map[string]string, 1) + } + config.Sysctls["net.ipv6.conf.all.disable_ipv6"] = "0" +} + +func DockerAllowNetworkAdministration(config *docker.HostConfig) { + config.CapAdd = append(config.CapAdd, "NET_ADMIN") + config.Mounts = append(config.Mounts, docker.HostMount{ + Type: "bind", + Source: "/dev/net/tun", + Target: "/dev/net/tun", + }) +} diff --git a/integration_test.go b/integration_test.go index 0379429e..ee89ef28 100644 --- a/integration_test.go +++ b/integration_test.go @@ -164,9 +164,7 @@ func (s *IntegrationTestSuite) tailscaleContainer( Name: hostname, Networks: []*dockertest.Network{&s.network}, Cmd: []string{ - "tailscaled", - "--tun=userspace-networking", - "--socks5-server=localhost:1055", + "tailscaled", "--tun=tsdev", }, } @@ -174,6 +172,8 @@ func (s *IntegrationTestSuite) tailscaleContainer( tailscaleBuildOptions, tailscaleOptions, DockerRestartPolicy, + DockerAllowLocalIPv6, + DockerAllowNetworkAdministration, ) if err != nil { log.Fatalf("Could not start resource: %s", err) @@ -372,70 +372,74 @@ func (s *IntegrationTestSuite) TestListNodes() { func (s *IntegrationTestSuite) TestGetIpAddresses() { for _, scales := range s.namespaces { - ipPrefix := netaddr.MustParseIPPrefix("100.64.0.0/10") ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) - for hostname := range scales.tailscales { - s.T().Run(hostname, func(t *testing.T) { - ip, ok := ips[hostname] + for hostname, _ := range scales.tailscales { + ips := ips[hostname] + for _, ip := range ips { + s.T().Run(hostname, func(t *testing.T) { + assert.NotNil(t, ip) - assert.True(t, ok) - assert.NotNil(t, ip) + fmt.Printf("IP for %s: %s\n", hostname, ip) - fmt.Printf("IP for %s: %s\n", hostname, ip) - - // c.Assert(ip.Valid(), check.IsTrue) - assert.True(t, ip.Is4()) - assert.True(t, ipPrefix.Contains(ip)) - }) + // c.Assert(ip.Valid(), check.IsTrue) + assert.True(t, ip.Is4() || ip.Is6()) + switch { + case ip.Is4(): + assert.True(t, IpPrefix4.Contains(ip)) + case ip.Is6(): + assert.True(t, IpPrefix6.Contains(ip)) + } + }) + } } } } // TODO(kradalby): fix this test -// We need some way to impot ipnstate.Status from multiple go packages. +// We need some way to import ipnstate.Status from multiple go packages. // Currently it will only work with 1.18.x since that is the last // version we have in go.mod // func (s *IntegrationTestSuite) TestStatus() { -// for _, scales := range s.namespaces { -// ips, err := getIPs(scales.tailscales) -// assert.Nil(s.T(), err) +// for _, scales := range s.namespaces { +// ips, err := getIPs(scales.tailscales) +// assert.Nil(s.T(), err) // -// for hostname, tailscale := range scales.tailscales { -// s.T().Run(hostname, func(t *testing.T) { -// command := []string{"tailscale", "status", "--json"} +// for hostname, tailscale := range scales.tailscales { +// s.T().Run(hostname, func(t *testing.T) { +// command := []string{"tailscale", "status", "--json"} // -// fmt.Printf("Getting status for %s\n", hostname) -// result, err := ExecuteCommand( -// &tailscale, -// command, -// []string{}, -// ) -// assert.Nil(t, err) +// fmt.Printf("Getting status for %s\n", hostname) +// result, err := ExecuteCommand( +// &tailscale, +// command, +// []string{}, +// ) +// assert.Nil(t, err) // -// var status ipnstate.Status -// err = json.Unmarshal([]byte(result), &status) -// assert.Nil(s.T(), err) +// var status ipnstate.Status +// err = json.Unmarshal([]byte(result), &status) +// assert.Nil(s.T(), err) // -// // TODO(kradalby): Replace this check with peer length of SAME namespace -// // Check if we have as many nodes in status -// // as we have IPs/tailscales -// // lines := strings.Split(result, "\n") -// // assert.Equal(t, len(ips), len(lines)-1) -// // assert.Equal(t, len(scales.tailscales), len(lines)-1) +// // TODO(kradalby): Replace this check with peer length of SAME namespace +// // Check if we have as many nodes in status +// // as we have IPs/tailscales +// // lines := strings.Split(result, "\n") +// // assert.Equal(t, len(ips), len(lines)-1) +// // assert.Equal(t, len(scales.tailscales), len(lines)-1) // -// peerIps := getIPsfromIPNstate(status) +// peerIps := getIPsfromIPNstate(status) // -// // Check that all hosts is present in all hosts status -// for ipHostname, ip := range ips { -// if hostname != ipHostname { -// assert.Contains(t, peerIps, ip) -// } -// } -// }) -// } -// } +// // Check that all hosts is present in all hosts status +// for ipHostname, ip := range ips { +// if hostname != ipHostname { +// assert.Contains(t, peerIps, ip) +// } +// } +// }) +// } +// } // } func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP { @@ -448,16 +452,19 @@ func getIPsfromIPNstate(status ipnstate.Status) []netaddr.IP { return ips } -func (s *IntegrationTestSuite) TestPingAllPeers() { +func (s *IntegrationTestSuite) TestPingAllPeersByAddress() { for _, scales := range s.namespaces { ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) for hostname, tailscale := range scales.tailscales { - for peername, ip := range ips { - s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + for peername, peerIPs := range ips { + for i, ip := range peerIPs { // We currently cant ping ourselves, so skip that. - if peername != hostname { + if peername == hostname { + continue + } + s.T().Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) { // We are only interested in "direct ping" which means what we // might need a couple of more attempts before reaching the node. command := []string{ @@ -469,9 +476,8 @@ func (s *IntegrationTestSuite) TestPingAllPeers() { } fmt.Printf( - "Pinging from %s (%s) to %s (%s)\n", + "Pinging from %s to %s (%s)\n", hostname, - ips[hostname], peername, ip, ) @@ -483,8 +489,8 @@ func (s *IntegrationTestSuite) TestPingAllPeers() { assert.Nil(t, err) fmt.Printf("Result for %s: %s\n", hostname, result) assert.Contains(t, result, "pong") - } - }) + }) + } } } } @@ -553,17 +559,17 @@ func (s *IntegrationTestSuite) TestSharedNodes() { // TODO(juanfont): We have to find out why do we need to wait time.Sleep(100 * time.Second) // Wait for the nodes to receive updates - mainIps, err := getIPs(main.tailscales) - assert.Nil(s.T(), err) - sharedIps, err := getIPs(shared.tailscales) assert.Nil(s.T(), err) for hostname, tailscale := range main.tailscales { - for peername, ip := range sharedIps { - s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + for peername, peerIPs := range sharedIps { + for i, ip := range peerIPs { // We currently cant ping ourselves, so skip that. - if peername != hostname { + if peername == hostname { + continue + } + s.T().Run(fmt.Sprintf("%s-%s-%d", hostname, peername, i), func(t *testing.T) { // We are only interested in "direct ping" which means what we // might need a couple of more attempts before reaching the node. command := []string{ @@ -575,9 +581,8 @@ func (s *IntegrationTestSuite) TestSharedNodes() { } fmt.Printf( - "Pinging from %s (%s) to %s (%s)\n", + "Pinging from %s to %s (%s)\n", hostname, - mainIps[hostname], peername, ip, ) @@ -589,8 +594,8 @@ func (s *IntegrationTestSuite) TestSharedNodes() { assert.Nil(t, err) fmt.Printf("Result for %s: %s\n", hostname, result) assert.Contains(t, result, "pong") - } - }) + }) + } } } } @@ -599,9 +604,19 @@ func (s *IntegrationTestSuite) TestTailDrop() { for _, scales := range s.namespaces { ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) - apiURLs, err := getAPIURLs(scales.tailscales) assert.Nil(s.T(), err) + retry := func(times int, sleepInverval time.Duration, doWork func() error) (err error) { + for attempts := 0; attempts < times; attempts++ { + err = doWork() + if err == nil { + return + } + time.Sleep(sleepInverval) + } + return + } + for hostname, tailscale := range scales.tailscales { command := []string{"touch", fmt.Sprintf("/tmp/file_from_%s", hostname)} _, err := ExecuteCommand( @@ -610,63 +625,31 @@ func (s *IntegrationTestSuite) TestTailDrop() { []string{}, ) assert.Nil(s.T(), err) - for peername, ip := range ips { + for peername, _ := range ips { + if peername == hostname { + continue + } s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { - if peername != hostname { - // Under normal circumstances, we should be able to send a file - // using `tailscale file cp` - but not in userspace networking mode - // So curl! - peerAPI, ok := apiURLs[ip] - assert.True(t, ok) - - // TODO(juanfont): We still have some issues with the test infrastructure, so - // lets run curl multiple times until it works. - attempts := 0 - var err error - for { - command := []string{ - "curl", - "--retry-connrefused", - "--retry-delay", - "30", - "--retry", - "10", - "--connect-timeout", - "60", - "-X", - "PUT", - "--upload-file", - fmt.Sprintf("/tmp/file_from_%s", hostname), - fmt.Sprintf( - "%s/v0/put/file_from_%s", - peerAPI, - hostname, - ), - } - fmt.Printf( - "Sending file from %s (%s) to %s (%s)\n", - hostname, - ips[hostname], - peername, - ip, - ) - _, err = ExecuteCommand( - &tailscale, - command, - []string{"ALL_PROXY=socks5://localhost:1055"}, - ) - if err == nil { - break - } else { - time.Sleep(10 * time.Second) - attempts++ - if attempts > 10 { - break - } - } - } - assert.Nil(t, err) + command := []string{ + "tailscale", "file", "cp", + fmt.Sprintf("/tmp/file_from_%s", hostname), + fmt.Sprintf("%s:", peername), } + retry(10, 1*time.Second, func() error { + fmt.Printf( + "Sending file from %s to %s\n", + hostname, + peername, + ) + _, err := ExecuteCommand( + &tailscale, + command, + []string{}, + ExecuteCommandTimeout(60*time.Second), + ) + return err + }) + assert.Nil(t, err) }) } } @@ -684,32 +667,70 @@ func (s *IntegrationTestSuite) TestTailDrop() { ) assert.Nil(s.T(), err) for peername, ip := range ips { + if peername == hostname { + continue + } s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { - if peername != hostname { - command := []string{ - "ls", - fmt.Sprintf("/tmp/file_from_%s", peername), - } - fmt.Printf( - "Checking file in %s (%s) from %s (%s)\n", - hostname, - ips[hostname], - peername, - ip, - ) - result, err := ExecuteCommand( - &tailscale, - command, - []string{}, - ) - assert.Nil(t, err) - fmt.Printf("Result for %s: %s\n", peername, result) - assert.Equal( - t, - result, - fmt.Sprintf("/tmp/file_from_%s\n", peername), - ) + command := []string{ + "ls", + fmt.Sprintf("/tmp/file_from_%s", peername), } + fmt.Printf( + "Checking file in %s (%s) from %s (%s)\n", + hostname, + ips[hostname], + peername, + ip, + ) + result, err := ExecuteCommand( + &tailscale, + command, + []string{}, + ) + assert.Nil(t, err) + fmt.Printf("Result for %s: %s\n", peername, result) + assert.Equal( + t, + fmt.Sprintf("/tmp/file_from_%s\n", peername), + result, + ) + }) + } + } + } +} + +func (s *IntegrationTestSuite) TestPingAllPeersByHostname() { + for namespace, scales := range s.namespaces { + ips, err := getIPs(scales.tailscales) + assert.Nil(s.T(), err) + for hostname, tailscale := range scales.tailscales { + for peername, _ := range ips { + if peername == hostname { + continue + } + s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { + command := []string{ + "tailscale", "ping", + "--timeout=10s", + "--c=20", + "--until-direct=true", + fmt.Sprintf("%s.%s.headscale.net", peername, namespace), + } + + fmt.Printf( + "Pinging using hostname from %s to %s\n", + hostname, + peername, + ) + result, err := ExecuteCommand( + &tailscale, + command, + []string{}, + ) + assert.Nil(t, err) + fmt.Printf("Result for %s: %s\n", hostname, result) + assert.Contains(t, result, "pong") }) } } @@ -721,32 +742,31 @@ func (s *IntegrationTestSuite) TestMagicDNS() { ips, err := getIPs(scales.tailscales) assert.Nil(s.T(), err) for hostname, tailscale := range scales.tailscales { - for peername, ip := range ips { + for peername, ips := range ips { + if peername == hostname { + continue + } s.T().Run(fmt.Sprintf("%s-%s", hostname, peername), func(t *testing.T) { - if peername != hostname { - command := []string{ - "tailscale", "ping", - "--timeout=10s", - "--c=20", - "--until-direct=true", - fmt.Sprintf("%s.%s.headscale.net", peername, namespace), - } + command := []string{ + "tailscale", "ip", + fmt.Sprintf("%s.%s.headscale.net", peername, namespace), + } - fmt.Printf( - "Pinging using Hostname (magicdns) from %s (%s) to %s (%s)\n", - hostname, - ips[hostname], - peername, - ip, - ) - result, err := ExecuteCommand( - &tailscale, - command, - []string{}, - ) - assert.Nil(t, err) - fmt.Printf("Result for %s: %s\n", hostname, result) - assert.Contains(t, result, "pong") + fmt.Printf( + "Resolving name %s from %s\n", + peername, + hostname, + ) + result, err := ExecuteCommand( + &tailscale, + command, + []string{}, + ) + assert.Nil(t, err) + fmt.Printf("Result for %s: %s\n", hostname, result) + + for _, ip := range ips { + assert.Contains(t, result, ip.String()) } }) } @@ -754,8 +774,8 @@ func (s *IntegrationTestSuite) TestMagicDNS() { } } -func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, error) { - ips := make(map[string]netaddr.IP) +func getIPs(tailscales map[string]dockertest.Resource) (map[string][]netaddr.IP, error) { + ips := make(map[string][]netaddr.IP) for hostname, tailscale := range tailscales { command := []string{"tailscale", "ip"} @@ -768,12 +788,17 @@ func getIPs(tailscales map[string]dockertest.Resource) (map[string]netaddr.IP, e return nil, err } - ip, err := netaddr.ParseIP(strings.TrimSuffix(result, "\n")) - if err != nil { - return nil, err + for _, address := range strings.Split(result, "\n") { + address = strings.TrimSuffix(address, "\n") + if len(address) < 1 { + continue + } + ip, err := netaddr.ParseIP(address) + if err != nil { + return nil, err + } + ips[hostname] = append(ips[hostname], ip) } - - ips[hostname] = ip } return ips, nil diff --git a/integration_test/etc/config.yaml b/integration_test/etc/config.yaml index 6f68f304..63af7ebb 100644 --- a/integration_test/etc/config.yaml +++ b/integration_test/etc/config.yaml @@ -2,6 +2,9 @@ log_level: trace acl_policy_path: "" db_type: sqlite3 ephemeral_node_inactivity_timeout: 30m +ip_prefixes: + - fd7a:115c:a1e0::/48 + - 100.64.0.0/10 dns_config: base_domain: headscale.net magic_dns: true diff --git a/machine.go b/machine.go index de1764e1..64fd11b8 100644 --- a/machine.go +++ b/machine.go @@ -1,6 +1,7 @@ package headscale import ( + "database/sql/driver" "encoding/json" "errors" "fmt" @@ -23,6 +24,7 @@ const ( errMachineNotFound = Error("machine not found") errMachineAlreadyRegistered = Error("machine already registered") errMachineRouteIsNotAvailable = Error("route is not available on machine") + errMachineAddressesInvalid = Error("failed to parse machine addresses") ) // Machine is a Headscale client. @@ -31,7 +33,7 @@ type Machine struct { MachineKey string `gorm:"type:varchar(64);unique_index"` NodeKey string DiscoKey string - IPAddress string + IPAddresses MachineAddresses Name string NamespaceID uint Namespace Namespace `gorm:"foreignKey:NamespaceID"` @@ -64,6 +66,47 @@ func (machine Machine) isRegistered() bool { return machine.Registered } +type MachineAddresses []netaddr.IP + +func (ma MachineAddresses) ToStringSlice() []string { + strSlice := make([]string, 0, len(ma)) + for _, addr := range ma { + strSlice = append(strSlice, addr.String()) + } + + return strSlice +} + +func (ma *MachineAddresses) Scan(destination interface{}) error { + switch value := destination.(type) { + case string: + addresses := strings.Split(value, ",") + *ma = (*ma)[:0] + for _, addr := range addresses { + if len(addr) < 1 { + continue + } + parsed, err := netaddr.ParseIP(addr) + if err != nil { + return err + } + *ma = append(*ma, parsed) + } + + return nil + + default: + return fmt.Errorf("%w: unexpected data type %T", errMachineAddressesInvalid, destination) + } +} + +// Value return json value, implement driver.Valuer interface. +func (ma MachineAddresses) Value() (driver.Value, error) { + addresses := strings.Join(ma.ToStringSlice(), ",") + + return addresses, nil +} + // isExpired returns whether the machine registration has expired. func (machine Machine) isExpired() bool { // If Expiry is not set, the client has not indicated that @@ -385,14 +428,18 @@ func (h *Headscale) isOutdated(machine *Machine) bool { } lastChange := h.getLastStateChange(namespaces...) + lastUpdate := machine.CreatedAt + if machine.LastSuccessfulUpdate != nil { + lastUpdate = *machine.LastSuccessfulUpdate + } log.Trace(). Caller(). Str("machine", machine.Name). - Time("last_successful_update", *machine.LastSuccessfulUpdate). - Time("last_state_change", lastChange). + Time("last_successful_update", lastChange). + Time("last_state_change", lastUpdate). Msgf("Checking if %s is missing updates", machine.Name) - return machine.LastSuccessfulUpdate.Before(lastChange) + return lastUpdate.Before(lastChange) } func (machine Machine) String() string { @@ -478,22 +525,12 @@ func (machine Machine) toNode( } addrs := []netaddr.IPPrefix{} - ip, err := netaddr.ParseIPPrefix(fmt.Sprintf("%s/32", machine.IPAddress)) - if err != nil { - log.Trace(). - Caller(). - Str("ip", machine.IPAddress). - Msgf("Failed to parse IP Prefix from IP: %s", machine.IPAddress) - - return nil, err + for _, machineAddress := range machine.IPAddresses { + ip := netaddr.IPPrefixFrom(machineAddress, machineAddress.BitLen()) + addrs = append(addrs, ip) } - addrs = append(addrs, ip) // missing the ipv6 ? - allowedIPs := []netaddr.IPPrefix{} - allowedIPs = append( - allowedIPs, - ip, - ) // we append the node own IP, as it is required by the clients + allowedIPs := append([]netaddr.IPPrefix{}, addrs...) // we append the node own IP, as it is required by the clients if includeRoutes { routesStr := []string{} @@ -600,11 +637,11 @@ func (machine *Machine) toProto() *v1.Machine { Id: machine.ID, MachineKey: machine.MachineKey, - NodeKey: machine.NodeKey, - DiscoKey: machine.DiscoKey, - IpAddress: machine.IPAddress, - Name: machine.Name, - Namespace: machine.Namespace.toProto(), + NodeKey: machine.NodeKey, + DiscoKey: machine.DiscoKey, + IpAddresses: machine.IPAddresses.ToStringSlice(), + Name: machine.Name, + Namespace: machine.Namespace.toProto(), Registered: machine.Registered, @@ -703,7 +740,7 @@ func (h *Headscale) RegisterMachine( return nil, err } - ip, err := h.getAvailableIP() + ips, err := h.getAvailableIPs() if err != nil { log.Error(). Caller(). @@ -717,10 +754,10 @@ func (h *Headscale) RegisterMachine( log.Trace(). Caller(). Str("machine", machine.Name). - Str("ip", ip.String()). + Str("ip", strings.Join(ips.ToStringSlice(), ",")). Msg("Found IP for host") - machine.IPAddress = ip.String() + machine.IPAddresses = ips machine.NamespaceID = namespace.ID machine.Registered = true machine.RegisterMethod = RegisterMethodCLI @@ -730,7 +767,7 @@ func (h *Headscale) RegisterMachine( log.Trace(). Caller(). Str("machine", machine.Name). - Str("ip", ip.String()). + Str("ip", strings.Join(ips.ToStringSlice(), ",")). Msg("Machine registered with the database") return machine, nil diff --git a/machine_test.go b/machine_test.go index eb090072..1cc4d40c 100644 --- a/machine_test.go +++ b/machine_test.go @@ -6,6 +6,7 @@ import ( "time" "gopkg.in/check.v1" + "inet.af/netaddr" ) func (s *Suite) TestGetMachine(c *check.C) { @@ -199,3 +200,24 @@ func (s *Suite) TestExpireMachine(c *check.C) { c.Assert(machineFromDB.isExpired(), check.Equals, true) } + +func (s *Suite) TestSerdeAddressStrignSlice(c *check.C) { + input := MachineAddresses([]netaddr.IP{ + netaddr.MustParseIP("192.0.2.1"), + netaddr.MustParseIP("2001:db8::1"), + }) + serialized, err := input.Value() + c.Assert(err, check.IsNil) + if serial, ok := serialized.(string); ok { + c.Assert(serial, check.Equals, "192.0.2.1,2001:db8::1") + } + + var deserialized MachineAddresses + err = deserialized.Scan(serialized) + c.Assert(err, check.IsNil) + + c.Assert(len(deserialized), check.Equals, len(input)) + for i := range deserialized { + c.Assert(deserialized[i], check.Equals, input[i]) + } +} diff --git a/namespaces_test.go b/namespaces_test.go index 9793e608..d07deb96 100644 --- a/namespaces_test.go +++ b/namespaces_test.go @@ -4,6 +4,7 @@ import ( "github.com/rs/zerolog/log" "gopkg.in/check.v1" "gorm.io/gorm" + "inet.af/netaddr" ) func (s *Suite) TestCreateAndDestroyNamespace(c *check.C) { @@ -146,7 +147,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.1", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, AuthKeyID: uint(preAuthKeyShared1.ID), } app.db.Save(machineInShared1) @@ -164,7 +165,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { Namespace: *namespaceShared2, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.2", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.2")}, AuthKeyID: uint(preAuthKeyShared2.ID), } app.db.Save(machineInShared2) @@ -182,7 +183,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { Namespace: *namespaceShared3, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.3", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.3")}, AuthKeyID: uint(preAuthKeyShared3.ID), } app.db.Save(machineInShared3) @@ -200,7 +201,7 @@ func (s *Suite) TestGetMapResponseUserProfiles(c *check.C) { Namespace: *namespaceShared1, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(preAuthKey2Shared1.ID), } app.db.Save(machine2InShared1) diff --git a/oidc.go b/oidc.go index 120a4cff..a47863ff 100644 --- a/oidc.go +++ b/oidc.go @@ -126,6 +126,7 @@ var oidcCallbackTemplate = template.Must( `), ) +// TODO: Why is the entire machine registration logic duplicated here? // OIDCCallback handles the callback from the OIDC endpoint // Retrieves the mkey from the state cache and adds the machine to the users email namespace // TODO: A confirmation page for new machines should be added to avoid phishing vulnerabilities @@ -316,7 +317,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) { return } - ip, err := h.getAvailableIP() + ips, err := h.getAvailableIPs() if err != nil { log.Error(). Caller(). @@ -330,7 +331,7 @@ func (h *Headscale) OIDCCallback(ctx *gin.Context) { return } - machine.IPAddress = ip.String() + machine.IPAddresses = ips machine.NamespaceID = namespace.ID machine.Registered = true machine.RegisterMethod = RegisterMethodOIDC diff --git a/poll.go b/poll.go index d7aa12e9..bcd6c33f 100644 --- a/poll.go +++ b/poll.go @@ -1,8 +1,10 @@ package headscale import ( + "context" "encoding/json" "errors" + "fmt" "io" "net/http" "time" @@ -154,14 +156,33 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { Str("id", ctx.Param("id")). Str("machine", machine.Name). Msg("Loading or creating update channel") - updateChan := make(chan struct{}) - pollDataChan := make(chan []byte) + // TODO: could probably remove all that duplication once generics land. + closeChanWithLog := func(channel interface{}, name string) { + log.Trace(). + Str("handler", "PollNetMap"). + Str("machine", machine.Name). + Str("channel", "Done"). + Msg(fmt.Sprintf("Closing %s channel", name)) + + switch c := channel.(type) { + case (chan struct{}): + close(c) + + case (chan []byte): + close(c) + } + } + + const chanSize = 8 + updateChan := make(chan struct{}, chanSize) + defer closeChanWithLog(updateChan, "updateChan") + + pollDataChan := make(chan []byte, chanSize) + defer closeChanWithLog(pollDataChan, "pollDataChan") keepAliveChan := make(chan []byte) - - cancelKeepAlive := make(chan struct{}) - defer close(cancelKeepAlive) + defer closeChanWithLog(keepAliveChan, "keepAliveChan") if req.OmitPeers && !req.Stream { log.Info(). @@ -174,7 +195,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { // even tho the comments in the tailscale code dont explicitly say so. updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "endpoint-update"). Inc() - go func() { updateChan <- struct{}{} }() + updateChan <- struct{}{} return } else if req.OmitPeers && req.Stream { @@ -195,7 +216,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { Str("handler", "PollNetMap"). Str("machine", machine.Name). Msg("Sending initial map") - go func() { pollDataChan <- data }() + pollDataChan <- data log.Info(). Str("handler", "PollNetMap"). @@ -203,7 +224,7 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { Msg("Notifying peers") updateRequestsFromNode.WithLabelValues(machine.Name, machine.Namespace.Name, "full-update"). Inc() - go func() { updateChan <- struct{}{} }() + updateChan <- struct{}{} h.PollNetMapStream( ctx, @@ -213,7 +234,6 @@ func (h *Headscale) PollNetMapHandler(ctx *gin.Context) { pollDataChan, keepAliveChan, updateChan, - cancelKeepAlive, ) log.Trace(). Str("handler", "PollNetMap"). @@ -233,16 +253,20 @@ func (h *Headscale) PollNetMapStream( pollDataChan chan []byte, keepAliveChan chan []byte, updateChan chan struct{}, - cancelKeepAlive chan struct{}, ) { - go h.scheduledPollWorker( - cancelKeepAlive, - updateChan, - keepAliveChan, - machineKey, - mapRequest, - machine, - ) + { + ctx, cancel := context.WithCancel(ctx.Request.Context()) + defer cancel() + + go h.scheduledPollWorker( + ctx, + updateChan, + keepAliveChan, + machineKey, + mapRequest, + machine, + ) + } ctx.Stream(func(writer io.Writer) bool { log.Trace(). @@ -392,10 +416,14 @@ func (h *Headscale) PollNetMapStream( updateRequestsReceivedOnChannel.WithLabelValues(machine.Name, machine.Namespace.Name). Inc() if h.isOutdated(machine) { + var lastUpdate time.Time + if machine.LastSuccessfulUpdate != nil { + lastUpdate = *machine.LastSuccessfulUpdate + } log.Debug(). Str("handler", "PollNetMapStream"). Str("machine", machine.Name). - Time("last_successful_update", *machine.LastSuccessfulUpdate). + Time("last_successful_update", lastUpdate). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). Msgf("There has been updates since the last successful update to %s", machine.Name) data, err := h.getMapResponse(machineKey, mapRequest, machine) @@ -464,10 +492,14 @@ func (h *Headscale) PollNetMapStream( Msg("Cannot update machine LastSuccessfulUpdate") } } else { + var lastUpdate time.Time + if machine.LastSuccessfulUpdate != nil { + lastUpdate = *machine.LastSuccessfulUpdate + } log.Trace(). Str("handler", "PollNetMapStream"). Str("machine", machine.Name). - Time("last_successful_update", *machine.LastSuccessfulUpdate). + Time("last_successful_update", lastUpdate). Time("last_state_change", h.getLastStateChange(machine.Namespace.Name)). Msgf("%s is up to date", machine.Name) } @@ -507,42 +539,13 @@ func (h *Headscale) PollNetMapStream( Msg("Cannot update machine LastSeen") } - log.Trace(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Name). - Str("channel", "Done"). - Msg("Cancelling keepAlive channel") - cancelKeepAlive <- struct{}{} - - log.Trace(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Name). - Str("channel", "Done"). - Msg("Closing update channel") - // h.closeUpdateChannel(m) - close(updateChan) - - log.Trace(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Name). - Str("channel", "Done"). - Msg("Closing pollData channel") - close(pollDataChan) - - log.Trace(). - Str("handler", "PollNetMapStream"). - Str("machine", machine.Name). - Str("channel", "Done"). - Msg("Closing keepAliveChan channel") - close(keepAliveChan) - return false } }) } func (h *Headscale) scheduledPollWorker( - cancelChan <-chan struct{}, + ctx context.Context, updateChan chan<- struct{}, keepAliveChan chan<- []byte, machineKey key.MachinePublic, @@ -554,7 +557,7 @@ func (h *Headscale) scheduledPollWorker( for { select { - case <-cancelChan: + case <-ctx.Done(): return case <-keepAliveTicker.C: diff --git a/proto/headscale/v1/machine.proto b/proto/headscale/v1/machine.proto index ed99f002..9b032122 100644 --- a/proto/headscale/v1/machine.proto +++ b/proto/headscale/v1/machine.proto @@ -18,7 +18,7 @@ message Machine { string machine_key = 2; string node_key = 3; string disco_key = 4; - string ip_address = 5; + repeated string ip_addresses = 5; string name = 6; Namespace namespace = 7; diff --git a/sharing_test.go b/sharing_test.go index fd7634da..b7fef4e2 100644 --- a/sharing_test.go +++ b/sharing_test.go @@ -2,6 +2,7 @@ package headscale import ( "gopkg.in/check.v1" + "inet.af/netaddr" ) func CreateNodeNamespace( @@ -26,7 +27,7 @@ func CreateNodeNamespace( NamespaceID: namespace.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: ip, + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.1")}, AuthKeyID: uint(pak1.ID), } app.db.Save(machine) @@ -214,7 +215,7 @@ func (s *Suite) TestComplexSharingAcrossNamespaces(c *check.C) { NamespaceID: namespace1.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(pak4.ID), } app.db.Save(machine4) @@ -294,7 +295,7 @@ func (s *Suite) TestDeleteSharedMachine(c *check.C) { NamespaceID: namespace1.ID, Registered: true, RegisterMethod: RegisterMethodAuthKey, - IPAddress: "100.64.0.4", + IPAddresses: []netaddr.IP{netaddr.MustParseIP("100.64.0.4")}, AuthKeyID: uint(pak4n1.ID), } app.db.Save(machine4) diff --git a/utils.go b/utils.go index 7011e63d..9e85599f 100644 --- a/utils.go +++ b/utils.go @@ -133,61 +133,78 @@ func encode( return privKey.SealTo(*pubKey, b), nil } -func (h *Headscale) getAvailableIP() (*netaddr.IP, error) { - ipPrefix := h.cfg.IPPrefix +func (h *Headscale) getAvailableIPs() (ips MachineAddresses, err error) { + ipPrefixes := h.cfg.IPPrefixes + for _, ipPrefix := range ipPrefixes { + var ip *netaddr.IP + ip, err = h.getAvailableIP(ipPrefix) + if err != nil { + return + } + ips = append(ips, *ip) + } + return +} + +func GetIPPrefixEndpoints(na netaddr.IPPrefix) (network, broadcast netaddr.IP) { + ipRange := na.Range() + network = ipRange.From() + broadcast = ipRange.To() + + return +} + +// TODO: Is this concurrency safe? +// What would happen if multiple hosts were to register at the same time? +// Would we attempt to assign the same addresses to multiple nodes? +func (h *Headscale) getAvailableIP(ipPrefix netaddr.IPPrefix) (*netaddr.IP, error) { usedIps, err := h.getUsedIPs() if err != nil { return nil, err } + ipPrefixNetworkAddress, ipPrefixBroadcastAddress := GetIPPrefixEndpoints(ipPrefix) + // Get the first IP in our prefix - ip := ipPrefix.IP() + ip := ipPrefixNetworkAddress.Next() for { if !ipPrefix.Contains(ip) { return nil, errCouldNotAllocateIP } - // Some OS (including Linux) does not like when IPs ends with 0 or 255, which - // is typically called network or broadcast. Lets avoid them and continue - // to look when we get one of those traditionally reserved IPs. - ipRaw := ip.As4() - if ipRaw[3] == 0 || ipRaw[3] == 255 { + switch { + case ip.Compare(ipPrefixBroadcastAddress) == 0: + fallthrough + case containsIPs(usedIps, ip): + fallthrough + case ip.IsZero() || ip.IsLoopback(): ip = ip.Next() continue - } - if ip.IsZero() && - ip.IsLoopback() { - ip = ip.Next() - - continue - } - - if !containsIPs(usedIps, ip) { + default: return &ip, nil } - - ip = ip.Next() } } func (h *Headscale) getUsedIPs() ([]netaddr.IP, error) { - var addresses []string - h.db.Model(&Machine{}).Pluck("ip_address", &addresses) + // FIXME: This really deserves a better data model, + // but this was quick to get running and it should be enough + // to begin experimenting with a dual stack tailnet. + var addressesSlices []string + h.db.Model(&Machine{}).Pluck("ip_addresses", &addressesSlices) - ips := make([]netaddr.IP, len(addresses)) - for index, addr := range addresses { - if addr != "" { - ip, err := netaddr.ParseIP(addr) - if err != nil { - return nil, fmt.Errorf("failed to parse ip from database: %w", err) - } - - ips[index] = ip + ips := make([]netaddr.IP, 0, len(h.cfg.IPPrefixes)*len(addressesSlices)) + for _, slice := range addressesSlices { + var a MachineAddresses + err := a.Scan(slice) + if err != nil { + return nil, fmt.Errorf("failed to read ip from database: %w", err) } + ips = append(ips, a...) } return ips, nil diff --git a/utils_test.go b/utils_test.go index 95722a83..feb44d5a 100644 --- a/utils_test.go +++ b/utils_test.go @@ -6,17 +6,18 @@ import ( ) func (s *Suite) TestGetAvailableIp(c *check.C) { - ip, err := app.getAvailableIP() + ips, err := app.getAvailableIPs() c.Assert(err, check.IsNil) expected := netaddr.MustParseIP("10.27.0.1") - c.Assert(ip.String(), check.Equals, expected.String()) + c.Assert(len(ips), check.Equals, 1) + c.Assert(ips[0].String(), check.Equals, expected.String()) } func (s *Suite) TestGetUsedIps(c *check.C) { - ip, err := app.getAvailableIP() + ips, err := app.getAvailableIPs() c.Assert(err, check.IsNil) namespace, err := app.CreateNamespace("test_ip") @@ -38,22 +39,24 @@ func (s *Suite) TestGetUsedIps(c *check.C) { Registered: true, RegisterMethod: RegisterMethodAuthKey, AuthKeyID: uint(pak.ID), - IPAddress: ip.String(), + IPAddresses: ips, } app.db.Save(&machine) - ips, err := app.getUsedIPs() + usedIps, err := app.getUsedIPs() c.Assert(err, check.IsNil) expected := netaddr.MustParseIP("10.27.0.1") - c.Assert(ips[0], check.Equals, expected) + c.Assert(len(usedIps), check.Equals, 1) + c.Assert(usedIps[0], check.Equals, expected) machine1, err := app.GetMachineByID(0) c.Assert(err, check.IsNil) - c.Assert(machine1.IPAddress, check.Equals, expected.String()) + c.Assert(len(machine1.IPAddresses), check.Equals, 1) + c.Assert(machine1.IPAddresses[0], check.Equals, expected) } func (s *Suite) TestGetMultiIp(c *check.C) { @@ -61,7 +64,7 @@ func (s *Suite) TestGetMultiIp(c *check.C) { c.Assert(err, check.IsNil) for index := 1; index <= 350; index++ { - ip, err := app.getAvailableIP() + ips, err := app.getAvailableIPs() c.Assert(err, check.IsNil) pak, err := app.CreatePreAuthKey(namespace.Name, false, false, nil) @@ -80,59 +83,64 @@ func (s *Suite) TestGetMultiIp(c *check.C) { Registered: true, RegisterMethod: RegisterMethodAuthKey, AuthKeyID: uint(pak.ID), - IPAddress: ip.String(), + IPAddresses: ips, } app.db.Save(&machine) } - ips, err := app.getUsedIPs() + usedIps, err := app.getUsedIPs() c.Assert(err, check.IsNil) - c.Assert(len(ips), check.Equals, 350) + c.Assert(len(usedIps), check.Equals, 350) - c.Assert(ips[0], check.Equals, netaddr.MustParseIP("10.27.0.1")) - c.Assert(ips[9], check.Equals, netaddr.MustParseIP("10.27.0.10")) - c.Assert(ips[300], check.Equals, netaddr.MustParseIP("10.27.1.47")) + c.Assert(usedIps[0], check.Equals, netaddr.MustParseIP("10.27.0.1")) + c.Assert(usedIps[9], check.Equals, netaddr.MustParseIP("10.27.0.10")) + c.Assert(usedIps[300], check.Equals, netaddr.MustParseIP("10.27.1.45")) // Check that we can read back the IPs machine1, err := app.GetMachineByID(1) c.Assert(err, check.IsNil) + c.Assert(len(machine1.IPAddresses), check.Equals, 1) c.Assert( - machine1.IPAddress, + machine1.IPAddresses[0], check.Equals, - netaddr.MustParseIP("10.27.0.1").String(), + netaddr.MustParseIP("10.27.0.1"), ) machine50, err := app.GetMachineByID(50) c.Assert(err, check.IsNil) + c.Assert(len(machine50.IPAddresses), check.Equals, 1) c.Assert( - machine50.IPAddress, + machine50.IPAddresses[0], check.Equals, - netaddr.MustParseIP("10.27.0.50").String(), + netaddr.MustParseIP("10.27.0.50"), ) - expectedNextIP := netaddr.MustParseIP("10.27.1.97") - nextIP, err := app.getAvailableIP() + expectedNextIP := netaddr.MustParseIP("10.27.1.95") + nextIP, err := app.getAvailableIPs() c.Assert(err, check.IsNil) - c.Assert(nextIP.String(), check.Equals, expectedNextIP.String()) + c.Assert(len(nextIP), check.Equals, 1) + c.Assert(nextIP[0].String(), check.Equals, expectedNextIP.String()) // If we call get Available again, we should receive // the same IP, as it has not been reserved. - nextIP2, err := app.getAvailableIP() + nextIP2, err := app.getAvailableIPs() c.Assert(err, check.IsNil) - c.Assert(nextIP2.String(), check.Equals, expectedNextIP.String()) + c.Assert(len(nextIP2), check.Equals, 1) + c.Assert(nextIP2[0].String(), check.Equals, expectedNextIP.String()) } func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) { - ip, err := app.getAvailableIP() + ips, err := app.getAvailableIPs() c.Assert(err, check.IsNil) expected := netaddr.MustParseIP("10.27.0.1") - c.Assert(ip.String(), check.Equals, expected.String()) + c.Assert(len(ips), check.Equals, 1) + c.Assert(ips[0].String(), check.Equals, expected.String()) namespace, err := app.CreateNamespace("test_ip") c.Assert(err, check.IsNil) @@ -156,8 +164,9 @@ func (s *Suite) TestGetAvailableIpMachineWithoutIP(c *check.C) { } app.db.Save(&machine) - ip2, err := app.getAvailableIP() + ips2, err := app.getAvailableIPs() c.Assert(err, check.IsNil) - c.Assert(ip2.String(), check.Equals, expected.String()) + c.Assert(len(ips2), check.Equals, 1) + c.Assert(ips2[0].String(), check.Equals, expected.String()) }