Skip to content

Commit 4102371

Browse files
committed
Add auto-updater sidecar, version reporting, network sync, and IPv6 fix
- pilot-updater sidecar: checks GitHub Releases, downloads archives, verifies SHA256 checksums, replaces binaries, restarts daemon - Version reporting: daemon sends build-time version to registry, displayed in pilotctl info and network members - Network sync: periodic reconciliation of network memberships, policies, and member tags from registry (5-min interval) - Fix IPv6 end-to-end: skip LAN address optimization when tunnel and peer use different address families - Security audit fixes: registry auth, replication auth, per-port accept, P2P handshake signing - Build: updater added to release archives, checksums.txt generated
1 parent df10c1f commit 4102371

23 files changed

Lines changed: 3005 additions & 204 deletions

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ build:
2626
go build -o $(BINDIR)/echo ./examples/go/echo
2727
go build -o $(BINDIR)/dataexchange ./examples/go/dataexchange
2828
go build -o $(BINDIR)/eventstream ./examples/go/eventstream
29+
go build -o $(BINDIR)/updater ./cmd/updater
2930
go build -o $(BINDIR)/secure ./examples/go/secure
3031

3132
test:
@@ -72,6 +73,7 @@ build-linux:
7273
GOOS=linux GOARCH=amd64 go build -o $(BINDIR)/nameserver-linux ./cmd/nameserver
7374
GOOS=linux GOARCH=amd64 go build -o $(BINDIR)/gateway-linux ./cmd/gateway
7475
@test -d cmd/pilot-admin && GOOS=linux GOARCH=amd64 go build -o $(BINDIR)/pilot-admin-linux ./cmd/pilot-admin || true
76+
GOOS=linux GOARCH=amd64 go build -o $(BINDIR)/updater-linux ./cmd/updater
7577
GOOS=linux GOARCH=amd64 go build -o $(BINDIR)/echo-linux ./examples/go/echo
7678
GOOS=linux GOARCH=amd64 go build -o $(BINDIR)/client-linux ./examples/go/client
7779
GOOS=linux GOARCH=amd64 go build -o $(BINDIR)/webserver-linux ./examples/go/webserver
@@ -86,7 +88,7 @@ ci: vet test build build-linux
8688
@echo "CI: all checks passed"
8789

8890
# All binaries included in release archives
89-
RELEASE_BINS := daemon pilotctl gateway registry beacon rendezvous nameserver
91+
RELEASE_BINS := daemon pilotctl gateway registry beacon rendezvous nameserver updater
9092

9193
# Cross-platform release builds
9294
release:
@@ -104,6 +106,7 @@ release:
104106
-C $(BINDIR)/release/$$os-$$arch .; \
105107
rm -rf $(BINDIR)/release/$$os-$$arch; \
106108
done
109+
@cd $(BINDIR)/release && shasum -a 256 *.tar.gz > checksums.txt
107110
@echo "Release archives in $(BINDIR)/release/"
108111

109112
# Console (web management UI) — requires CGo for SQLite

cmd/daemon/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ func main() {
9393
WebhookURL: *webhookURL,
9494
AdminToken: *adminToken,
9595
Networks: parseNetworkIDs(*networks),
96+
Version: version,
9697
})
9798

9899
if err := d.Start(); err != nil {

cmd/pilotctl/main.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3658,6 +3658,9 @@ func cmdInfo() {
36583658
}
36593659

36603660
fmt.Printf("Pilot Protocol Daemon\n")
3661+
if v, ok := info["version"].(string); ok && v != "" {
3662+
fmt.Printf(" Version: %s\n", v)
3663+
}
36613664
fmt.Printf(" Node ID: %d\n", int(info["node_id"].(float64)))
36623665
fmt.Printf(" Address: %s\n", info["address"])
36633666
if hostname, ok := info["hostname"].(string); ok && hostname != "" {
@@ -4542,11 +4545,12 @@ func cmdNetworkMembers(args []string) {
45424545
fmt.Println("no members")
45434546
return
45444547
}
4545-
fmt.Printf("%-12s %-20s %-10s\n", "NODE ID", "HOSTNAME", "PUBLIC")
4548+
fmt.Printf("%-12s %-20s %-12s %-10s\n", "NODE ID", "HOSTNAME", "VERSION", "PUBLIC")
45464549
for _, n := range nodes {
45474550
nm, _ := n.(map[string]interface{})
45484551
nodeID := uint32(nm["node_id"].(float64))
45494552
hostname, _ := nm["hostname"].(string)
4553+
ver, _ := nm["version"].(string)
45504554
public := false
45514555
if p, ok := nm["public"].(bool); ok {
45524556
public = p
@@ -4558,7 +4562,10 @@ func cmdNetworkMembers(args []string) {
45584562
if hostname == "" {
45594563
hostname = "-"
45604564
}
4561-
fmt.Printf("%-12d %-20s %-10s\n", nodeID, hostname, vis)
4565+
if ver == "" {
4566+
ver = "-"
4567+
}
4568+
fmt.Printf("%-12d %-20s %-12s %-10s\n", nodeID, hostname, ver, vis)
45624569
}
45634570
}
45644571

cmd/updater/main.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"log/slog"
8+
"os"
9+
"os/signal"
10+
"syscall"
11+
"time"
12+
13+
"github.com/TeoSlayer/pilotprotocol/pkg/logging"
14+
"github.com/TeoSlayer/pilotprotocol/pkg/updater"
15+
)
16+
17+
var version = "dev"
18+
19+
func main() {
20+
interval := flag.Duration("interval", 1*time.Hour, "check interval for new releases")
21+
repo := flag.String("repo", "TeoSlayer/pilotprotocol", "GitHub owner/repo to check for releases")
22+
installDir := flag.String("install-dir", "", "directory containing pilot binaries (required)")
23+
showVersion := flag.Bool("version", false, "print version and exit")
24+
logLevel := flag.String("log-level", "info", "log level (debug, info, warn, error)")
25+
logFormat := flag.String("log-format", "text", "log format (text, json)")
26+
flag.Parse()
27+
28+
if *showVersion {
29+
fmt.Println(version)
30+
os.Exit(0)
31+
}
32+
33+
if *installDir == "" {
34+
log.Fatal("-install-dir is required")
35+
}
36+
37+
logging.Setup(*logLevel, *logFormat)
38+
39+
slog.Info("pilot-updater starting",
40+
"version", version,
41+
"repo", *repo,
42+
"install_dir", *installDir,
43+
"interval", *interval,
44+
)
45+
46+
u := updater.New(updater.Config{
47+
CheckInterval: *interval,
48+
Repo: *repo,
49+
InstallDir: *installDir,
50+
Version: version,
51+
})
52+
53+
u.Start()
54+
55+
sig := make(chan os.Signal, 1)
56+
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
57+
<-sig
58+
59+
slog.Info("shutting down")
60+
u.Stop()
61+
}

data-exchange-policy.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
"match": "true",
2626
"actions": [{"type": "deny"}]
2727
},
28+
{
29+
"name": "allow-echo",
30+
"on": "datagram",
31+
"match": "port == 7",
32+
"actions": [{"type": "allow"}]
33+
},
2834
{
2935
"name": "allow-text",
3036
"on": "datagram",

internal/fsutil/fsutil.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func AppendSync(path string, data []byte) error {
2727
// This ensures the target file is never left in a truncated state.
2828
func AtomicWrite(path string, data []byte) error {
2929
tmp := path + ".tmp"
30-
f, err := os.Create(tmp)
30+
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
3131
if err != nil {
3232
return err
3333
}

internal/fsutil/fsutil_test.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package fsutil
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestAtomicWriteCreatesFile(t *testing.T) {
10+
t.Parallel()
11+
dir := t.TempDir()
12+
path := filepath.Join(dir, "test.json")
13+
14+
data := []byte(`{"key":"value"}`)
15+
if err := AtomicWrite(path, data); err != nil {
16+
t.Fatalf("AtomicWrite: %v", err)
17+
}
18+
19+
got, err := os.ReadFile(path)
20+
if err != nil {
21+
t.Fatalf("ReadFile: %v", err)
22+
}
23+
if string(got) != string(data) {
24+
t.Fatalf("content mismatch: %q", got)
25+
}
26+
}
27+
28+
func TestAtomicWritePermissions0600(t *testing.T) {
29+
t.Parallel()
30+
dir := t.TempDir()
31+
path := filepath.Join(dir, "perms.json")
32+
33+
if err := AtomicWrite(path, []byte("secret")); err != nil {
34+
t.Fatalf("AtomicWrite: %v", err)
35+
}
36+
37+
info, err := os.Stat(path)
38+
if err != nil {
39+
t.Fatalf("Stat: %v", err)
40+
}
41+
if perm := info.Mode().Perm(); perm != 0600 {
42+
t.Fatalf("expected 0600, got %04o", perm)
43+
}
44+
}
45+
46+
func TestAtomicWriteOverwrite(t *testing.T) {
47+
t.Parallel()
48+
dir := t.TempDir()
49+
path := filepath.Join(dir, "overwrite.json")
50+
51+
if err := AtomicWrite(path, []byte("first")); err != nil {
52+
t.Fatalf("first write: %v", err)
53+
}
54+
if err := AtomicWrite(path, []byte("second")); err != nil {
55+
t.Fatalf("second write: %v", err)
56+
}
57+
58+
got, err := os.ReadFile(path)
59+
if err != nil {
60+
t.Fatalf("ReadFile: %v", err)
61+
}
62+
if string(got) != "second" {
63+
t.Fatalf("expected 'second', got %q", got)
64+
}
65+
66+
// Permissions still 0600 after overwrite
67+
info, _ := os.Stat(path)
68+
if perm := info.Mode().Perm(); perm != 0600 {
69+
t.Fatalf("expected 0600 after overwrite, got %04o", perm)
70+
}
71+
}
72+
73+
func TestAtomicWriteNoTempFileRemains(t *testing.T) {
74+
t.Parallel()
75+
dir := t.TempDir()
76+
path := filepath.Join(dir, "clean.json")
77+
78+
if err := AtomicWrite(path, []byte("data")); err != nil {
79+
t.Fatalf("AtomicWrite: %v", err)
80+
}
81+
82+
if _, err := os.Stat(path + ".tmp"); !os.IsNotExist(err) {
83+
t.Fatal("temp file should not exist after successful write")
84+
}
85+
}
86+
87+
func TestAtomicWriteEmptyData(t *testing.T) {
88+
t.Parallel()
89+
dir := t.TempDir()
90+
path := filepath.Join(dir, "empty.json")
91+
92+
if err := AtomicWrite(path, []byte{}); err != nil {
93+
t.Fatalf("AtomicWrite empty: %v", err)
94+
}
95+
96+
got, err := os.ReadFile(path)
97+
if err != nil {
98+
t.Fatalf("ReadFile: %v", err)
99+
}
100+
if len(got) != 0 {
101+
t.Fatalf("expected empty file, got %d bytes", len(got))
102+
}
103+
}
104+
105+
func TestAtomicWriteLargeData(t *testing.T) {
106+
t.Parallel()
107+
dir := t.TempDir()
108+
path := filepath.Join(dir, "large.json")
109+
110+
data := make([]byte, 1<<20) // 1MB
111+
for i := range data {
112+
data[i] = byte(i % 256)
113+
}
114+
115+
if err := AtomicWrite(path, data); err != nil {
116+
t.Fatalf("AtomicWrite: %v", err)
117+
}
118+
119+
got, err := os.ReadFile(path)
120+
if err != nil {
121+
t.Fatalf("ReadFile: %v", err)
122+
}
123+
if len(got) != len(data) {
124+
t.Fatalf("size mismatch: %d != %d", len(got), len(data))
125+
}
126+
}
127+
128+
func TestAtomicWriteBadDirectory(t *testing.T) {
129+
t.Parallel()
130+
err := AtomicWrite("/nonexistent/dir/file.json", []byte("data"))
131+
if err == nil {
132+
t.Fatal("expected error for nonexistent directory")
133+
}
134+
}
135+
136+
func TestAppendSyncCreatesFile(t *testing.T) {
137+
t.Parallel()
138+
dir := t.TempDir()
139+
path := filepath.Join(dir, "append.log")
140+
141+
if err := AppendSync(path, []byte("line1\n")); err != nil {
142+
t.Fatalf("AppendSync: %v", err)
143+
}
144+
145+
got, err := os.ReadFile(path)
146+
if err != nil {
147+
t.Fatalf("ReadFile: %v", err)
148+
}
149+
if string(got) != "line1\n" {
150+
t.Fatalf("expected 'line1\\n', got %q", got)
151+
}
152+
}
153+
154+
func TestAppendSyncAppends(t *testing.T) {
155+
t.Parallel()
156+
dir := t.TempDir()
157+
path := filepath.Join(dir, "multi.log")
158+
159+
if err := AppendSync(path, []byte("a\n")); err != nil {
160+
t.Fatalf("first AppendSync: %v", err)
161+
}
162+
if err := AppendSync(path, []byte("b\n")); err != nil {
163+
t.Fatalf("second AppendSync: %v", err)
164+
}
165+
166+
got, err := os.ReadFile(path)
167+
if err != nil {
168+
t.Fatalf("ReadFile: %v", err)
169+
}
170+
if string(got) != "a\nb\n" {
171+
t.Fatalf("expected 'a\\nb\\n', got %q", got)
172+
}
173+
}
174+
175+
func TestAppendSyncPermissions0600(t *testing.T) {
176+
t.Parallel()
177+
dir := t.TempDir()
178+
path := filepath.Join(dir, "perms.log")
179+
180+
if err := AppendSync(path, []byte("x")); err != nil {
181+
t.Fatalf("AppendSync: %v", err)
182+
}
183+
184+
info, err := os.Stat(path)
185+
if err != nil {
186+
t.Fatalf("Stat: %v", err)
187+
}
188+
if perm := info.Mode().Perm(); perm != 0600 {
189+
t.Fatalf("expected 0600, got %04o", perm)
190+
}
191+
}
192+
193+
func TestAppendSyncBadDirectory(t *testing.T) {
194+
t.Parallel()
195+
err := AppendSync("/nonexistent/dir/file.log", []byte("data"))
196+
if err == nil {
197+
t.Fatal("expected error for nonexistent directory")
198+
}
199+
}

0 commit comments

Comments
 (0)