mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 11:03:34 +00:00
add tsnet experiment
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
// tsnet-load launches N userspace Tailscale nodes and has each one periodically
|
||||
// ping a tailnet peer (or HTTP-GET a subnet-routed host).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// TS_AUTHKEY=tskey-... go run . -count 10 -interval 5s
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
const (
|
||||
httpTarget = "http://10.210.1.2"
|
||||
pingTarget = "100.92.123.72"
|
||||
)
|
||||
|
||||
func main() {
|
||||
authKey := flag.String("authkey", os.Getenv("TS_AUTHKEY"),
|
||||
"Tailscale reusable ephemeral auth key (or set TS_AUTHKEY).")
|
||||
count := flag.Int("count", 10, "Number of tsnet nodes to launch.")
|
||||
interval := flag.Duration("interval", 5*time.Second, "Interval between actions per node.")
|
||||
hostnamePrefix := flag.String("hostname-prefix", "uncloud", "Hostname prefix for tsnet nodes.")
|
||||
stateRoot := flag.String("state-dir", "./state", "Directory to keep tsnet state (default: a fresh temp dir).")
|
||||
flag.Parse()
|
||||
|
||||
if *authKey == "" {
|
||||
log.Fatal("missing Tailscale auth key: pass -authkey or set TS_AUTHKEY.")
|
||||
}
|
||||
|
||||
root := *stateRoot
|
||||
if root == "" {
|
||||
var err error
|
||||
root, err = os.MkdirTemp("", "tsnet-load-*")
|
||||
if err != nil {
|
||||
log.Fatalf("mktemp: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
} else if err := os.MkdirAll(root, 0o700); err != nil {
|
||||
log.Fatalf("mkdir state dir: %v", err)
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < *count; i++ {
|
||||
wg.Add(1)
|
||||
go func(id int) {
|
||||
defer wg.Done()
|
||||
runNode(ctx, id, *authKey, *hostnamePrefix, root, *interval)
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func runNode(ctx context.Context, id int, authKey, prefix, stateRoot string, interval time.Duration) {
|
||||
logPrefix := fmt.Sprintf("[node-%02d] ", id)
|
||||
logger := log.New(os.Stderr, logPrefix, log.LstdFlags|log.Lmsgprefix)
|
||||
|
||||
s := &tsnet.Server{
|
||||
Hostname: fmt.Sprintf("%s-%02d", prefix, id),
|
||||
AuthKey: authKey,
|
||||
Ephemeral: true,
|
||||
Dir: filepath.Join(stateRoot, fmt.Sprintf("node-%02d", id)),
|
||||
// Silence per-node tsnet chatter. Comment out to debug.
|
||||
Logf: func(string, ...any) {},
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
upCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
|
||||
defer cancel()
|
||||
status, err := s.Up(upCtx)
|
||||
if err != nil {
|
||||
logger.Printf("tsnet up: %v", err)
|
||||
return
|
||||
}
|
||||
logger.Printf("up as %s (%v)", s.Hostname, status.TailscaleIPs)
|
||||
|
||||
// Accept subnet routes advertised by other tailnet peers. tsnet defaults
|
||||
// to RouteAll=false, so without this 10.210.0.0/24 etc. are unreachable.
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
logger.Printf("local client: %v", err)
|
||||
return
|
||||
}
|
||||
if _, err := lc.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
RouteAllSet: true,
|
||||
Prefs: ipn.Prefs{RouteAll: true},
|
||||
}); err != nil {
|
||||
logger.Printf("edit prefs (RouteAll): %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{DialContext: s.Dial},
|
||||
Timeout: 10 * time.Second,
|
||||
}
|
||||
_ = httpClient
|
||||
|
||||
// Stagger ticks so all nodes don't fire in lockstep.
|
||||
time.Sleep(time.Duration(rand.Int64N(int64(interval))))
|
||||
|
||||
tick := time.NewTicker(interval)
|
||||
defer tick.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-tick.C:
|
||||
doPing(ctx, lc, logger)
|
||||
//doHTTP(ctx, httpClient, logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doPing(ctx context.Context, lc *tailscale.LocalClient, logger *log.Logger) {
|
||||
pctx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
res, err := lc.Ping(pctx, netip.MustParseAddr(pingTarget), tailcfg.PingICMP)
|
||||
if err != nil {
|
||||
logger.Printf("ping %s: %v", pingTarget, err)
|
||||
return
|
||||
}
|
||||
if res.Err != "" {
|
||||
logger.Printf("ping %s: %s", pingTarget, res.Err)
|
||||
return
|
||||
}
|
||||
logger.Printf("ping %s ok in %.1fms via %s", pingTarget, res.LatencySeconds*1000, res.Endpoint)
|
||||
}
|
||||
|
||||
func doHTTP(ctx context.Context, c *http.Client, logger *log.Logger) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, httpTarget, nil)
|
||||
if err != nil {
|
||||
logger.Printf("http build req: %v", err)
|
||||
return
|
||||
}
|
||||
start := time.Now()
|
||||
resp, err := c.Do(req)
|
||||
if err != nil {
|
||||
logger.Printf("http %s: %v", httpTarget, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
n, _ := io.Copy(io.Discard, resp.Body)
|
||||
logger.Printf("http %s: %s, %d bytes in %v", httpTarget, resp.Status, n, time.Since(start).Round(time.Millisecond))
|
||||
}
|
||||
Reference in New Issue
Block a user