Skip to content

Commit

Permalink
Add zerotier-wrapper
Browse files Browse the repository at this point in the history
Changes:

* Add zerotier-wrapper as extension entrypoint, this wrapper:
  * handles PID cleanup and creation
  * works instead of the upstream zerotier symlinks for zerotier-cli and zerotier-idtool
  * takes configuration from ENV vars ZEROTIER_NETWORK and optionally ZEROTIER_IDENTITY_SECRET
  * ZEROTIER_NETWORK must be set, this is the ID of the network that zerotier attempts to join after start up (typically a manual process)
  * If ZEROTIER_IDENTITY_SECRET is optionally set this is written out and used by zerotier to authenticate as the node
  * If ZEROTIER_IDENTITY_SECRET is not set a new identity is created
  * logs the various lifecycle steps in a verbose way

* Remove unused mounts (zerotier is compiled statically so /lib etc aren't needed)
* Removes the aforementioned symlinks for zerotier-cli and zerotier-idtool as they aren't relevant without shell
* Adds some usage docs in network/zerotier/README.md
* Adds the renovate comment for zerotier version
  • Loading branch information
rob-htl committed Feb 20, 2025
1 parent 75ea5ab commit efc8004
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 22 deletions.
2 changes: 1 addition & 1 deletion network/vars.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ TAILSCALE_VERSION: 1.78.1
LLDPD_VERSION: 1.0.19
# renovate: datasource=github-releases depName=cloudflare/cloudflared
CLOUDFLARED_VERSION: 2024.12.1

# renovate: datasource=github-releases depName=zerotier/ZeroTierOne
ZEROTIER_VERSION: 1.14.2
60 changes: 60 additions & 0 deletions network/zerotier/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# ZeroTier

Adds https://zerotier.com network interfaces as system extensions.
This means you can access your Talos nodes from machines you have configured
with ZeroTier, creating a secure overlay network.

## Installation

See [Installing Extensions](https://github.com/siderolabs/extensions#installing-extensions).

## Usage

Configure the extension via `ExtensionServiceConfig` document.

```yaml
---
apiVersion: v1alpha1
kind: ExtensionServiceConfig
name: zerotier
environment:
- ZEROTIER_NETWORK=<your network id>
```
Then apply the patch to your node's MachineConfigs
```bash
talosctl patch mc -p @zerotier-config.yaml
```

You can then verify that it is in place with the following command

```bash
talosctl get extensionserviceconfigs

NODE NAMESPACE TYPE ID VERSION
mynode runtime ExtensionServiceConfig zerotier 1
```

## Configuration

The extension can be configured through environment variables:

- `ZEROTIER_NETWORK`: The network ID to join (required)
- `ZEROTIER_IDENTITY_SECRET`: Optional pre-existing identity to use (format: "address:0:public:private")

### Using an existing identity

If you want to maintain the same ZeroTier identity across rebuilds or different nodes, you can specify an existing identity:

```yaml
---
apiVersion: v1alpha1
kind: ExtensionServiceConfig
name: zerotier
environment:
- ZEROTIER_NETWORK=<your network id>
- ZEROTIER_IDENTITY_SECRET=<identity string>
```
If no identity is provided, a new one will be generated automatically. (You may need to authorize this node in your Zerotier network according to your network policies before it will recieve an IP address).
7 changes: 3 additions & 4 deletions network/zerotier/pkg.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ variant: alpine
shell: /toolchain/bin/bash
dependencies:
- stage: base
- stage: zerotier-wrapper
install:
- libstdc++
steps:
Expand All @@ -28,10 +29,8 @@ steps:
- |
mkdir -p /rootfs/usr/local/lib/containers/zerotier/usr/local/bin/
cp -pr zerotier-one /rootfs/usr/local/lib/containers/zerotier/usr/local/bin/
cp -pr /rootfs/usr/local/bin/zerotier-wrapper /rootfs/usr/local/lib/containers/zerotier/usr/local/bin/
chmod +x /rootfs/usr/local/lib/containers/zerotier/usr/local/bin/zerotier-*
cd /rootfs/usr/local/lib/containers/zerotier/usr/local/bin
ln -sf zerotier-one zerotier-cli
ln -sf zerotier-one zerotier-idtool
- |
mkdir -p /rootfs/usr/local/etc/containers/zerotier/usr/local/etc/zerotier/state
cp /pkg/zerotier.yaml /rootfs/usr/local/etc/containers/
Expand All @@ -42,7 +41,7 @@ steps:
cp /pkg/manifest.yaml /extensions-validator-rootfs/manifest.yaml
/extensions-validator validate --rootfs=/extensions-validator-rootfs --pkg-name="${PKG_NAME}"
- |
[[ $(/rootfs/usr/local/lib/containers/zerotier/usr/local/bin/zerotier-cli -v) == *{{ .ZEROTIER_VERSION }}* ]]
[[ $(/rootfs/usr/local/lib/containers/zerotier/usr/local/bin/zerotier-one -v) == *{{ .ZEROTIER_VERSION }}* ]]
finalize:
- from: /rootfs
to: /rootfs
Expand Down
5 changes: 5 additions & 0 deletions network/zerotier/zerotier-wrapper/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module zerotier-wrapper

go 1.23.0

require golang.org/x/sys v0.30.0
2 changes: 2 additions & 0 deletions network/zerotier/zerotier-wrapper/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
232 changes: 232 additions & 0 deletions network/zerotier/zerotier-wrapper/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
package main

import (
"bytes"
"errors"
"fmt"
"log"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"time"

"golang.org/x/sys/unix"
)

const (
zerotierPath = "/var/lib/zerotier-one"
identityPath = "/var/lib/zerotier-one/identity.secret"
identityPubPath = "/var/lib/zerotier-one/identity.public"
pidFile = "/var/lib/zerotier-one/zerotier-one.pid"
zerotierBinPath = "/usr/local/bin/zerotier-one"
)

func main() {
log.Printf("zerotier-wrapper: initializing...")

// Ensure the ZeroTier state directory exists.
if err := os.MkdirAll(zerotierPath, 0755); err != nil {
log.Fatalf("failed to create state directory: %v", err)
}

// Ensure identity configuration.
identitySource, err := ensureIdentity()
if err != nil {
log.Fatalf("identity configuration failed: %v", err)
}
log.Printf("identity configured (source: %s)", identitySource)

// Cleanup any existing zerotier-one process.
if err := cleanupProcess(); err != nil {
log.Fatalf("process cleanup failed: %v", err)
}

// If ZEROTIER_NETWORK env var is set, join network using zerotier-one -q.
if network := os.Getenv("ZEROTIER_NETWORK"); network != "" {
log.Printf("will join network %s after startup", network)
go func() {
time.Sleep(2 * time.Second)
if err := joinNetwork(network); err != nil {
log.Printf("failed to join network: %v", err)
} else {
log.Printf("joined network %s", network)
}
}()
}

// Start zerotier-one process.
cmd := exec.Command(zerotierBinPath, "-U", zerotierPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Start(); err != nil {
log.Fatalf("error starting zerotier-one: %v", err)
}

// Write the PID file.
pidStr := strconv.Itoa(cmd.Process.Pid)
if err := os.WriteFile(pidFile, []byte(pidStr), 0644); err != nil {
log.Printf("failed to write PID file: %v", err)
}

// Forward termination signals to the zerotier-one process.
ch := make(chan os.Signal, 1)
signal.Notify(ch, unix.SIGINT, unix.SIGTERM)
sig := <-ch
log.Printf("received signal %v, forwarding to zerotier-one process", sig)
if err := cmd.Process.Signal(sig); err != nil {
log.Fatalf("error sending signal to zerotier-one: %v", err)
}

if err := cmd.Wait(); err != nil {
log.Fatalf("zerotier-one exited with error: %v", err)
}
}

// ensureIdentity checks for an existing identity file, validates it if found,
// or else uses the identity from the ZEROTIER_IDENTITY_SECRET environment variable (after validation)
// or generates a new one using "zerotier-one -i generate".
func ensureIdentity() (string, error) {
// If the identity file exists, validate its contents.
if _, err := os.Stat(identityPath); err == nil {
data, err := os.ReadFile(identityPath)
if err != nil {
return "", fmt.Errorf("failed to read existing identity: %w", err)
}
identity := strings.TrimSpace(string(data))
log.Printf("found existing identity at %s, validating...", identityPath)
if err := validateIdentity(identity); err != nil {
return "", fmt.Errorf("existing identity failed validation: %w", err)
}
log.Printf("existing identity validated")
return "existing", nil
} else if !errors.Is(err, os.ErrNotExist) {
return "", fmt.Errorf("failed to stat identity file: %w", err)
}

// Check for identity in environment.
if identity := os.Getenv("ZEROTIER_IDENTITY_SECRET"); identity != "" {
log.Printf("found identity in ZEROTIER_IDENTITY_SECRET environment variable, validating...")
if err := validateIdentity(identity); err != nil {
return "", fmt.Errorf("environment identity invalid: %w", err)
}
log.Printf("environment identity validated")
if err := writeIdentity(identity); err != nil {
return "", fmt.Errorf("failed to write identity from environment: %w", err)
}
return "environment", nil
}

// Generate a new identity using "zerotier-one -i generate".
log.Printf("generating new identity using zerotier-one -i generate")
cmd := exec.Command(zerotierBinPath, "-i", "generate")
var out bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to generate identity: %w", err)
}
identity := strings.TrimSpace(out.String())
if err := validateIdentity(identity); err != nil {
return "", fmt.Errorf("generated identity failed validation: %w", err)
}
if err := writeIdentity(identity); err != nil {
return "", fmt.Errorf("failed to write generated identity: %w", err)
}
return "generated", nil
}

// validateIdentity runs "zerotier-one -i validate <identity>" to ensure the identity is valid.
func validateIdentity(identity string) error {
cmd := exec.Command(zerotierBinPath, "-i", "validate", identity)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("identity validation failed: %w", err)
}
return nil
}

// writeIdentity writes the complete identity string (all four parts) to identity.secret,
// while writing only the first three parts (separated by ':') to identity.public.
func writeIdentity(identity string) error {
parts := strings.Split(identity, ":")
if len(parts) != 4 {
return fmt.Errorf("invalid identity format: expected 4 parts, got %d", len(parts))
}

// Write the secret identity file with the full identity.
if err := os.WriteFile(identityPath, []byte(identity), 0600); err != nil {
return fmt.Errorf("failed to write secret identity: %w", err)
}
log.Printf("wrote secret identity to %s", identityPath)

// Write the public identity file with only the first 3 parts.
public := strings.Join(parts[:3], ":")
if err := os.WriteFile(identityPubPath, []byte(public), 0644); err != nil {
return fmt.Errorf("failed to write public identity: %w", err)
}
log.Printf("wrote public identity to %s", identityPubPath)

return nil
}

// cleanupProcess checks for an existing PID file; if found, it kills the process and removes the file.
func cleanupProcess() error {
if _, err := os.Stat(pidFile); err == nil {
pid, err := getProcessId()
if err != nil {
return fmt.Errorf("error reading pid file: %w", err)
}
if err := killProcess(pid); err != nil {
return fmt.Errorf("error killing process: %w", err)
}
if err := os.Remove(pidFile); err != nil {
return fmt.Errorf("error removing pid file: %w", err)
}
log.Printf("cleaned up existing process (PID %d)", pid)
} else if !errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("failed to stat pid file: %w", err)
} else {
log.Printf("no PID file found, no existing process to clean up")
}
return nil
}

func getProcessId() (int, error) {
pidData, err := os.ReadFile(pidFile)
if err != nil {
return 0, err
}
pidData = bytes.TrimRight(pidData, "\n")
pid, err := strconv.Atoi(string(pidData))
if err != nil {
return 0, err
}
return pid, nil
}

func killProcess(pid int) error {
p, err := os.FindProcess(pid)
if err != nil {
return err
}
if err := p.Kill(); err != nil && !errors.Is(err, os.ErrProcessDone) {
return err
}
return nil
}

// joinNetwork uses "zerotier-one -q join <network>" to join the specified network.
func joinNetwork(network string) error {
log.Printf("attempting to join network %s", network)
cmd := exec.Command(zerotierBinPath, "-q", "join", network)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("join network failed: %w", err)
}
return nil
}
24 changes: 24 additions & 0 deletions network/zerotier/zerotier-wrapper/pkg.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: zerotier-wrapper
variant: scratch
shell: /toolchain/bin/bash
dependencies:
- stage: base
steps:
- cachePaths:
- /.cache/go-build
- /go/pkg
build:
- |
export PATH=${PATH}:${TOOLCHAIN}/go/bin
cp -r /pkg/* .
CGO_ENABLED=0 go build -o zerotier-wrapper main.go
install:
- |
mkdir -p /rootfs/usr/local/bin
cp zerotier-wrapper /rootfs/usr/local/bin/zerotier-wrapper
finalize:
- from: /rootfs
to: /rootfs
Loading

0 comments on commit efc8004

Please sign in to comment.