//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(), EventBus: sys.Bus.Get(), }) if err != nil { return nil, err } sys.Set(engine) 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 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{} sys.NetstackRouter.Set(true) sys.Tun.Get().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 }