mirror of
https://github.com/tale/headplane.git
synced 2026-08-29 16:37:10 +00:00
chore: reorganize go code
This commit is contained in:
+4
-32
@@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -31,10 +30,10 @@ import (
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/words"
|
||||
)
|
||||
|
||||
// ControlURL defines the URL to be used for connection to Control.
|
||||
@@ -59,7 +58,7 @@ func NewIPN(jsConfig js.Value) map[string]any {
|
||||
if jsHostname := jsConfig.Get("hostname"); jsHostname.Type() == js.TypeString {
|
||||
hostname = jsHostname.String()
|
||||
} else {
|
||||
hostname = generateHostname()
|
||||
hostname = "blah"
|
||||
}
|
||||
|
||||
lpc := getOrCreateLogPolicyConfig(store)
|
||||
@@ -126,7 +125,8 @@ func NewIPN(jsConfig js.Value) map[string]any {
|
||||
sys.NetstackRouter.Set(true)
|
||||
sys.Tun.Get().Start()
|
||||
|
||||
logid := lpc.PublicID
|
||||
logid := logid.PublicID{}
|
||||
|
||||
srv := ipnserver.New(logf, logid, sys.NetMon.Get())
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginDefault)
|
||||
if err != nil {
|
||||
@@ -595,34 +595,6 @@ func mapSliceView[T any, M any](a views.Slice[T], f func(T) M) []M {
|
||||
return n
|
||||
}
|
||||
|
||||
func filterSlice[T any](a []T, f func(T) bool) []T {
|
||||
n := make([]T, 0, len(a))
|
||||
for _, e := range a {
|
||||
if f(e) {
|
||||
n = append(n, e)
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func generateHostname() string {
|
||||
tails := words.Tails()
|
||||
scales := words.Scales()
|
||||
if rand.IntN(2) == 0 {
|
||||
// JavaScript
|
||||
tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "j") })
|
||||
scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "s") })
|
||||
} else {
|
||||
// WebAssembly
|
||||
tails = filterSlice(tails, func(s string) bool { return strings.HasPrefix(s, "w") })
|
||||
scales = filterSlice(scales, func(s string) bool { return strings.HasPrefix(s, "a") })
|
||||
}
|
||||
|
||||
tail := tails[rand.IntN(len(tails))]
|
||||
scale := scales[rand.IntN(len(scales))]
|
||||
return fmt.Sprintf("%s-%s", tail, scale)
|
||||
}
|
||||
|
||||
const logPolicyStateKey = "log-policy"
|
||||
|
||||
func getOrCreateLogPolicyConfig(state ipn.StateStore) *logpolicy.Config {
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
//go:build js && wasm
|
||||
|
||||
package hp_ipn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
)
|
||||
|
||||
// Represents an in-state Tailscale backend that is WASM friendly.
|
||||
// The bare minimum to have userspace Wireguard networking is a dialer,
|
||||
// a server, and a backend.
|
||||
type TsWasmIpn struct {
|
||||
// The options used to initialize the TsWasmNet module.
|
||||
options *TsWasmNetOptions
|
||||
|
||||
// The Tailscale dialer, which is used to establish connections.
|
||||
dialer *tsdial.Dialer
|
||||
|
||||
// The Tailscale server, which handles incoming connections and requests.
|
||||
server *ipnserver.Server
|
||||
|
||||
// The Tailscale backend, which manages the local state and operations.
|
||||
backend *ipnlocal.LocalBackend
|
||||
}
|
||||
|
||||
// NewTsWasmIpn initializes a new TsWasmIpn instance with the provided options.
|
||||
// This intentionally does not initialize Logtail, as it is only available in
|
||||
// the Tailscale SaaS and not on self-hosted instances.
|
||||
func NewTsWasmIpn(options *TsWasmNetOptions, callbacks *TsWasmNetCallbacks) (*TsWasmIpn, error) {
|
||||
logf := log.Printf // TODO: Update
|
||||
|
||||
netns.SetEnabled(false) // netns is a separate process (not WASM friendly)
|
||||
|
||||
// Base system (NewSystem() creates a bus automatically)
|
||||
// We supply an in-memory store
|
||||
sys := tsd.NewSystem()
|
||||
bus := sys.Bus.Get()
|
||||
sys.Set(new(mem.Store))
|
||||
|
||||
dialer := &tsdial.Dialer{Logf: logf}
|
||||
netmon, err := netmon.New(bus, logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Userspace Wireguard engine
|
||||
engine, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Dialer: dialer,
|
||||
NetMon: netmon,
|
||||
SetSubsystem: sys.Set,
|
||||
ControlKnobs: sys.ControlKnobs(),
|
||||
HealthTracker: sys.HealthTracker(),
|
||||
Metrics: sys.UserMetricsRegistry(),
|
||||
})
|
||||
|
||||
sys.Set(engine)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tun := sys.Tun.Get()
|
||||
msock := sys.MagicSock.Get()
|
||||
dnsman := sys.DNSManager.Get()
|
||||
proxymap := sys.ProxyMapper()
|
||||
wgstack, err := netstack.Create(logf, tun, engine, msock, dialer, dnsman, proxymap)
|
||||
|
||||
sys.Set(wgstack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Configure the local Netstack and Dialer
|
||||
wgstack.ProcessLocalIPs = true
|
||||
wgstack.ProcessSubnets = true
|
||||
sys.NetstackRouter.Set(true)
|
||||
|
||||
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
return wgstack.DialContextTCP(ctx, dst)
|
||||
}
|
||||
|
||||
dialer.NetstackDialUDP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
return wgstack.DialContextUDP(ctx, dst)
|
||||
}
|
||||
|
||||
// Dummy logid for the Tailscale backend
|
||||
logid := logid.PublicID{}
|
||||
tun.Start()
|
||||
|
||||
server := ipnserver.New(logf, logid, sys.NetMon.Get())
|
||||
flags := controlclient.LoginDefault | controlclient.LoginEphemeral | controlclient.LocalBackendStartKeyOSNeutral
|
||||
|
||||
backend, err := ipnlocal.NewLocalBackend(logf, logid, sys, flags)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = wgstack.Start(backend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
server.SetLocalBackend(backend)
|
||||
registerNotifyCallback(callbacks, backend)
|
||||
|
||||
return &TsWasmIpn{
|
||||
options: options,
|
||||
dialer: dialer,
|
||||
server: server,
|
||||
backend: backend,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Starts the WASM backend which will connect to the Tailscale tailnet and
|
||||
// register an ephemeral node viewable in the Tailscale admin console.
|
||||
func (t *TsWasmIpn) Start(ctx context.Context) error {
|
||||
// Blank "socket" is a requirement for WASM
|
||||
// This NEEDS to happen before the LocalBackend is started,
|
||||
listener, err := safesocket.Listen("")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create safesocket listener: %w", err)
|
||||
}
|
||||
|
||||
// Start the server BEFORE the LocalBackend is started
|
||||
go func() {
|
||||
err := t.server.Run(ctx, listener)
|
||||
if err != nil {
|
||||
// TODO: Handle this dispatch using a chan
|
||||
log.Printf("Failed to run Tailscale server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Start the LocalBackend
|
||||
err = t.backend.Start(ipn.Options{
|
||||
AuthKey: t.options.PreAuthKey,
|
||||
UpdatePrefs: &ipn.Prefs{
|
||||
ControlURL: t.options.ControlURL,
|
||||
Hostname: t.options.Hostname,
|
||||
WantRunning: true,
|
||||
RunWebClient: false,
|
||||
LoggedOut: false,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start Tailscale backend: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Tailscale backend started successfully with hostname: %s", t.options.Hostname)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
//go:build js && wasm
|
||||
|
||||
package hp_ipn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall/js"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
// Maps ipn.State values to their string representations for the frontend.
|
||||
var BackendState = map[ipn.State]string{
|
||||
ipn.NoState: "NoState",
|
||||
ipn.Stopped: "Stopped",
|
||||
ipn.Starting: "Starting",
|
||||
ipn.Running: "Running",
|
||||
ipn.InUseOtherUser: "InUseOtherUser",
|
||||
ipn.NeedsMachineAuth: "NeedsMachineAuth",
|
||||
ipn.NeedsLogin: "NeedsLogin",
|
||||
}
|
||||
|
||||
// Represents the callbacks that the TsWasmNet module can invoke to register
|
||||
// data retrieval and notifications on the frontend.
|
||||
type TsWasmNetCallbacks struct {
|
||||
// Changes in the backend state.
|
||||
NotifyState func(ipn.State)
|
||||
|
||||
// Updates to the backend's network map.
|
||||
NotifyNetMap func(*netmap.NetworkMap)
|
||||
|
||||
// If interactive login is required, this passes a login URL.
|
||||
NotifyBrowseToURL func(string)
|
||||
|
||||
// If the process panics, this function is called in go.recover.
|
||||
NotifyPanicRecover func(string)
|
||||
}
|
||||
|
||||
// Parses a JavaScript object containing the necessary callbacks for the
|
||||
// TsWasmNet module to properly interact with the frontend.
|
||||
func ParseTsWasmNetCallbacks(obj js.Value) (*TsWasmNetCallbacks, error) {
|
||||
if obj.IsUndefined() || obj.IsNull() {
|
||||
return nil, errors.New("callbacks object is undefined or null")
|
||||
}
|
||||
|
||||
state, err := validateCallback("NotifyState", obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid callback NotifyState: %w", err)
|
||||
}
|
||||
|
||||
// TODO: This is complicated, as the NetworkMap is a complex type.
|
||||
_, err = validateCallback("NotifyNetMap", obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid callback NotifyNetMap: %w", err)
|
||||
}
|
||||
|
||||
browseURL, err := validateCallback("NotifyBrowseToURL", obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid callback NotifyBrowseToURL: %w", err)
|
||||
}
|
||||
|
||||
panicRecover, err := validateCallback("NotifyPanicRecover", obj)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid callback NotifyPanicRecover: %w", err)
|
||||
}
|
||||
|
||||
return &TsWasmNetCallbacks{
|
||||
NotifyState: func(ipnState ipn.State) {
|
||||
state.Invoke(BackendState[ipnState])
|
||||
},
|
||||
|
||||
NotifyNetMap: func(nm *netmap.NetworkMap) {
|
||||
// TODO: This is complicated
|
||||
},
|
||||
|
||||
NotifyBrowseToURL: func(url string) {
|
||||
browseURL.Invoke(url)
|
||||
},
|
||||
|
||||
NotifyPanicRecover: func(msg string) {
|
||||
panicRecover.Invoke(msg)
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Validates the specified key is a JS function and returns it.
|
||||
func validateCallback(key string, obj js.Value) (*js.Value, error) {
|
||||
val := obj.Get(key)
|
||||
if val.IsUndefined() || val.IsNull() {
|
||||
return nil, errors.New("callback is undefined or null")
|
||||
}
|
||||
|
||||
if val.Type() != js.TypeFunction {
|
||||
return nil, errors.New("callback is not a function")
|
||||
}
|
||||
|
||||
return &val, nil
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//go:build js && wasm
|
||||
|
||||
package hp_ipn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
// Represents the options needed to initialize the TsWasmNet module.
|
||||
type TsWasmNetOptions struct {
|
||||
// Tailscale control URL, e.g., "https://controlplane.tailscale.com"
|
||||
ControlURL string
|
||||
|
||||
// Pre-authentication key for the Tailnet
|
||||
PreAuthKey string
|
||||
|
||||
// Optional hostname, autogenerated if not provided.
|
||||
Hostname string
|
||||
}
|
||||
|
||||
// Parses the provided JS object to validate and extract the TsWasmNetOptions.
|
||||
func ParseTsWasmNetOptions(obj js.Value) (*TsWasmNetOptions, error) {
|
||||
if obj.IsUndefined() || obj.IsNull() {
|
||||
return nil, errors.New("TsWasmNetOptions cannot be undefined or null")
|
||||
}
|
||||
|
||||
cUrl := safeString("ControlURL", obj)
|
||||
preAuthKey := safeString("PreAuthKey", obj)
|
||||
hostname := safeString("Hostname", obj)
|
||||
|
||||
if cUrl == "" || preAuthKey == "" || hostname == "" {
|
||||
return nil, errors.New("missing required fields in TsWasmNetOptions")
|
||||
}
|
||||
|
||||
return &TsWasmNetOptions{
|
||||
ControlURL: cUrl,
|
||||
PreAuthKey: preAuthKey,
|
||||
Hostname: hostname,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Retrieves a string value from a JS object safely.
|
||||
func safeString(key string, obj js.Value) string {
|
||||
if obj.IsUndefined() || obj.IsNull() {
|
||||
return ""
|
||||
}
|
||||
|
||||
val := obj.Get(key)
|
||||
if val.IsUndefined() || val.IsNull() {
|
||||
return ""
|
||||
}
|
||||
|
||||
return val.String()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//go:build js && wasm
|
||||
|
||||
package hp_ipn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
)
|
||||
|
||||
func registerNotifyCallback(callbacks *TsWasmNetCallbacks, lb *ipnlocal.LocalBackend) {
|
||||
lb.SetNotifyCallback(func(n ipn.Notify) {
|
||||
// Panics should be treated with care in a JS/wasm environment.
|
||||
// If a panic occurs, notify the user and either automatically reload
|
||||
// or give the option to reload.
|
||||
|
||||
defer func() {
|
||||
rec := recover()
|
||||
if rec != nil {
|
||||
callbacks.NotifyPanicRecover(fmt.Sprint(rec))
|
||||
}
|
||||
}()
|
||||
|
||||
if n.State != nil {
|
||||
callbacks.NotifyState(*n.State)
|
||||
|
||||
if *n.State == ipn.NeedsLogin {
|
||||
// If the state is NeedsLogin, we need to force an interactive login.
|
||||
go forceInteractiveLogin(lb)
|
||||
}
|
||||
}
|
||||
|
||||
if n.BrowseToURL != nil {
|
||||
callbacks.NotifyBrowseToURL(*n.BrowseToURL)
|
||||
}
|
||||
|
||||
log.Printf("NOTIFY: %+v", n)
|
||||
|
||||
// if nm := n.NetMap; nm != nil {
|
||||
// jsNetMap := jsNetMap{
|
||||
// Self: jsNetMapSelfNode{
|
||||
// jsNetMapNode: jsNetMapNode{
|
||||
// Name: nm.Name,
|
||||
// Addresses: mapSliceView(nm.GetAddresses(), func(a netip.Prefix) string { return a.Addr().String() }),
|
||||
// NodeKey: nm.NodeKey.String(),
|
||||
// MachineKey: nm.MachineKey.String(),
|
||||
// },
|
||||
// MachineStatus: jsMachineStatus[nm.GetMachineStatus()],
|
||||
// },
|
||||
// Peers: mapSlice(nm.Peers, func(p tailcfg.NodeView) jsNetMapPeerNode {
|
||||
// name := p.Name()
|
||||
// if name == "" {
|
||||
// // In practice this should only happen for Hello.
|
||||
// name = p.Hostinfo().Hostname()
|
||||
// }
|
||||
// addrs := make([]string, p.Addresses().Len())
|
||||
// for i, ap := range p.Addresses().All() {
|
||||
// addrs[i] = ap.Addr().String()
|
||||
// }
|
||||
// return jsNetMapPeerNode{
|
||||
// jsNetMapNode: jsNetMapNode{
|
||||
// Name: name,
|
||||
// Addresses: addrs,
|
||||
// MachineKey: p.Machine().String(),
|
||||
// NodeKey: p.Key().String(),
|
||||
// },
|
||||
// Online: p.Online().Clone(),
|
||||
// TailscaleSSHEnabled: p.Hostinfo().TailscaleSSHEnabled(),
|
||||
// }
|
||||
// }),
|
||||
// LockedOut: nm.TKAEnabled && nm.SelfNode.KeySignature().Len() == 0,
|
||||
// }
|
||||
// if jsonNetMap, err := json.Marshal(jsNetMap); err == nil {
|
||||
// jsCallbacks.Call("notifyNetMap", string(jsonNetMap))
|
||||
// } else {
|
||||
// log.Printf("Could not generate JSON netmap: %v", err)
|
||||
// }
|
||||
// }
|
||||
// if n.BrowseToURL != nil {
|
||||
// jsCallbacks.Call("notifyBrowseToURL", *n.BrowseToURL)
|
||||
// }
|
||||
})
|
||||
}
|
||||
|
||||
// To get auth to work, even with a pre-auth key, we need to
|
||||
// force an interactive login on the NeedsLogin state.
|
||||
func forceInteractiveLogin(lb *ipnlocal.LocalBackend) {
|
||||
err := lb.StartLoginInteractive(context.Background())
|
||||
if err != nil {
|
||||
fmt.Printf("Error starting interactive login: %v\n", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user