Add backend/internal/tailscale/tailscale.go
This commit is contained in:
parent
c890bfd1c3
commit
58c55c436f
1 changed files with 105 additions and 0 deletions
105
backend/internal/tailscale/tailscale.go
Normal file
105
backend/internal/tailscale/tailscale.go
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Package tailscale manages an embedded Tailscale node via tsnet.
|
||||
// The node connects to a tailnet using an auth key and exposes a
|
||||
// net.Listener that the rest of the app can use for local-only HTTP.
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
// Node wraps a tsnet.Server and provides helpers.
|
||||
type Node struct {
|
||||
srv *tsnet.Server
|
||||
hostname string
|
||||
}
|
||||
|
||||
// Config holds settings for the embedded Tailscale node.
|
||||
type Config struct {
|
||||
// Hostname is the name this device will appear as on the tailnet.
|
||||
Hostname string
|
||||
// AuthKey is a Tailscale auth key (reusable or ephemeral).
|
||||
// If empty the node will print a login URL to stderr.
|
||||
AuthKey string
|
||||
// StateDir is where tsnet persists its state.
|
||||
// Defaults to <user config dir>/moonrelay/tsnet.
|
||||
StateDir string
|
||||
}
|
||||
|
||||
// New creates and starts a new embedded Tailscale node.
|
||||
func New(ctx context.Context, cfg Config) (*Node, error) {
|
||||
stateDir := cfg.StateDir
|
||||
if stateDir == "" {
|
||||
// In Docker the TS_STATE_DIR env var points to the mounted volume.
|
||||
// Fall back to the user config dir for native / dev installs.
|
||||
if envDir := os.Getenv("TS_STATE_DIR"); envDir != "" {
|
||||
stateDir = envDir
|
||||
} else {
|
||||
cfgDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("finding config dir: %w", err)
|
||||
}
|
||||
stateDir = filepath.Join(cfgDir, "moonrelay", "tsnet")
|
||||
}
|
||||
}
|
||||
if err := os.MkdirAll(stateDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("creating state dir: %w", err)
|
||||
}
|
||||
|
||||
s := &tsnet.Server{
|
||||
Hostname: cfg.Hostname,
|
||||
AuthKey: cfg.AuthKey,
|
||||
Dir: stateDir,
|
||||
Logf: func(format string, args ...any) { log.Printf("[tsnet] "+format, args...) },
|
||||
}
|
||||
|
||||
// Bring the node up — this blocks until connected (or auth required).
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting local client: %w", err)
|
||||
}
|
||||
|
||||
// Wait for the node to be ready.
|
||||
if _, err := lc.Status(ctx); err != nil {
|
||||
// Not fatal — node may need interactive login.
|
||||
log.Printf("tailscale status: %v", err)
|
||||
}
|
||||
|
||||
return &Node{srv: s, hostname: cfg.Hostname}, nil
|
||||
}
|
||||
|
||||
// Listen returns a net.Listener bound on the Tailscale interface.
|
||||
func (n *Node) Listen(network, addr string) (net.Listener, error) {
|
||||
return n.srv.Listen(network, addr)
|
||||
}
|
||||
|
||||
// LocalAddr returns the Tailscale IPv4 address of this node, or empty string.
|
||||
func (n *Node) LocalAddr() string {
|
||||
lc, err := n.srv.LocalClient()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
st, err := lc.Status(context.Background())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
for _, pfx := range st.TailscaleIPs {
|
||||
if pfx.Is4() {
|
||||
addr := netip.AddrFrom4(pfx.As4())
|
||||
return addr.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Close shuts down the node.
|
||||
func (n *Node) Close() error {
|
||||
return n.srv.Close()
|
||||
}
|
||||
Loading…
Reference in a new issue