When someone opens a terminal for this site, Hammerhead lists the session in the site's config. The agent connects out to Hammerhead and joins that connection to its own SSH server, so no port has to be open on an edge. The agent now reports where its SSH server listens and that it can relay. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
115 lines
4.8 KiB
Go
115 lines
4.8 KiB
Go
// Bamboo is the site agent: run it inside a private network to expose services from there.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"git.veillax.com/Veillax/bamboo"
|
|
"git.veillax.com/Veillax/bamboo/discover"
|
|
"git.veillax.com/Veillax/bamboo/sshd"
|
|
"git.veillax.com/Veillax/reef/dotenv"
|
|
"git.veillax.com/Veillax/reef/selfupdate"
|
|
)
|
|
|
|
// version is set at build time (deploy/build.sh).
|
|
var version = "dev"
|
|
|
|
func env(k, def string) string {
|
|
if v := os.Getenv(k); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
|
|
func envFloat(k string, def float64) float64 {
|
|
if f, err := strconv.ParseFloat(os.Getenv(k), 64); err == nil && f > 0 {
|
|
return f
|
|
}
|
|
return def
|
|
}
|
|
|
|
func main() {
|
|
// secrets (tokens, DNS-provider credentials) can live in a .env file instead of the command line
|
|
envFile := dotenv.ArgPath(os.Args[1:], ".env")
|
|
if _, err := dotenv.Load(envFile); err != nil {
|
|
log.Fatalf("read %s: %v", envFile, err)
|
|
}
|
|
hh := flag.String("hammerhead", env("BAMBOO_HAMMERHEAD", ""), "URL of hammerhead")
|
|
token := flag.String("token", env("BAMBOO_TOKEN", ""), "site token from the hammerhead UI")
|
|
data := flag.String("data", env("BAMBOO_DATA", "./bamboo-data"), "directory for this agent's WireGuard key")
|
|
dockerOn := flag.Bool("docker", os.Getenv("BAMBOO_DOCKER") == "1", "report local Docker containers so they can be picked as targets in the UI")
|
|
discoverOn := flag.Bool("discover", os.Getenv("BAMBOO_DISCOVER") == "1", "report services listening on this machine as suggested targets in the UI (Linux)")
|
|
discoverSubnets := flag.String("discover-subnets", os.Getenv("BAMBOO_DISCOVER_SUBNETS"), "comma-separated private subnets to scan for services, e.g. 192.168.1.0/24 (at most /22 each; off by default)")
|
|
discoverPorts := flag.String("discover-ports", os.Getenv("BAMBOO_DISCOVER_PORTS"), "TCP ports to try on scanned addresses, e.g. 22,80,443,8000-8010 (default: a short list of common ones)")
|
|
discoverRate := flag.Float64("discover-rate", envFloat("BAMBOO_DISCOVER_RATE", discover.DefaultRate), "most connection attempts per second discovery may make")
|
|
sshOn := flag.Bool("ssh", os.Getenv("BAMBOO_SSH") == "1", "run a built-in SSH server (public keys only) so this machine can be reached through a tunnelled port")
|
|
sshListen := flag.String("ssh-listen", env("BAMBOO_SSH_LISTEN", sshd.DefaultListen), "address the SSH server listens on; point a Hammerhead port at it from this site")
|
|
sshKeys := flag.String("ssh-keys", os.Getenv("BAMBOO_SSH_KEYS"), "authorized_keys file listing who may sign in (default: authorized_keys in the data folder)")
|
|
flag.String("env", ".env", "file to read secrets and settings from")
|
|
autoUpdate := flag.Bool("auto-update", os.Getenv("BAMBOO_AUTO_UPDATE") == "1", "check for new releases every few hours and update and restart by itself (Linux and macOS)")
|
|
showVersion := flag.Bool("version", false, "print the version and exit")
|
|
flag.Parse()
|
|
if *showVersion {
|
|
os.Stdout.WriteString("bamboo " + version + "\n")
|
|
return
|
|
}
|
|
if *hh == "" || *token == "" {
|
|
flag.Usage()
|
|
os.Exit(2)
|
|
}
|
|
log.Printf("bamboo %s starting", version)
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
|
defer stop()
|
|
upd := selfupdate.Config{Server: "https://git.veillax.com", Repo: "Veillax/bamboo", App: "bamboo", Current: version, Logf: log.Printf}
|
|
if *autoUpdate {
|
|
go selfupdate.Run(ctx, upd)
|
|
}
|
|
dc := discover.Config{Local: *discoverOn || *discoverSubnets != "", Rate: *discoverRate}
|
|
var err error
|
|
if dc.Subnets, err = discover.ParseSubnets(*discoverSubnets); err != nil {
|
|
log.Fatalf("-discover-subnets: %v", err)
|
|
}
|
|
if dc.Ports, err = discover.ParsePorts(*discoverPorts); err != nil {
|
|
log.Fatalf("-discover-ports: %v", err)
|
|
}
|
|
sshAddr := "" // reported to Hammerhead, so its terminal can reach the server through this agent
|
|
if *sshOn {
|
|
if !sshd.Supported {
|
|
log.Fatal("-ssh: the SSH server is not available on this platform")
|
|
}
|
|
keys := *sshKeys
|
|
if keys == "" {
|
|
keys = filepath.Join(*data, "authorized_keys")
|
|
}
|
|
srv, err := sshd.New(sshd.Config{Listen: *sshListen, AuthorizedKeys: keys, HostKey: filepath.Join(*data, "ssh_host_ed25519_key"), Logf: log.Printf})
|
|
if err != nil {
|
|
log.Fatalf("-ssh: %v", err)
|
|
}
|
|
sshAddr = *sshListen
|
|
log.Printf("ssh: listening on %s, host key %s", *sshListen, srv.Fingerprint())
|
|
go func() {
|
|
if err := srv.Run(ctx); err != nil {
|
|
log.Printf("ssh: stopped: %v", err)
|
|
}
|
|
}()
|
|
}
|
|
b, err := bamboo.New(bamboo.Config{Hammerhead: strings.TrimRight(*hh, "/"), Token: *token, DataDir: *data, Docker: *dockerOn, Discover: dc, SSH: sshAddr,
|
|
Version: version, AutoUpdate: *autoUpdate,
|
|
UpdateNow: func(ctx context.Context, progress func(string, int, string)) (string, error) {
|
|
c := upd
|
|
c.Progress = func(e selfupdate.Event) { progress(e.Stage, e.Pct, e.Line) }
|
|
return selfupdate.CheckOnce(ctx, c)
|
|
}})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
b.Run(ctx)
|
|
}
|