feat(tsconnect): expose exit node selection to JS

Add exit node support to the wasm JS bridge:

- Include `exitNodeOption` and `stableNodeID` on each peer in the
  notifyNetMap payload so callers can identify which peers are exit
  nodes and reference them by stable ID.
- Call `notifyExitNode(stableNodeID)` whenever prefs change, so
  callers can track which exit node (if any) is currently active.
- Expose `setExitNode(stableNodeID)` — sets ExitNodeID via EditPrefs.
- Expose `setExitNodeEnabled(enabled)` — toggles the last-used exit
  node on/off via SetUseExitNodeEnabled.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
webnet
Codinget 4 days ago
parent f961db8925
commit b04b4f7751
  1. 44
      cmd/tsconnect/wasm/wasm_js.go

@ -45,6 +45,7 @@ import (
"tailscale.com/logtail"
"tailscale.com/net/bakedroots"
"tailscale.com/net/netns"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tsdial"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
@ -252,6 +253,20 @@ func newIPN(jsConfig js.Value) map[string]any {
}
return jsIPN.dialTLS(args[0].String(), opts)
}),
"setExitNode": js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
log.Printf("Usage: setExitNode(stableNodeID)")
return nil
}
return jsIPN.setExitNode(args[0].String())
}),
"setExitNodeEnabled": js.FuncOf(func(this js.Value, args []js.Value) any {
if len(args) != 1 {
log.Printf("Usage: setExitNodeEnabled(enabled)")
return nil
}
return jsIPN.setExitNodeEnabled(args[0].Bool())
}),
}
}
@ -333,6 +348,8 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
},
Online: p.Online().Clone(),
TailscaleSSHEnabled: p.Hostinfo().TailscaleSSHEnabled(),
ExitNodeOption: tsaddr.ContainsExitRoutes(p.AllowedIPs()),
StableNodeID: string(p.StableID()),
}
}),
LockedOut: nm.TKAEnabled && nm.SelfNode.KeySignature().Len() == 0,
@ -343,6 +360,9 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
log.Printf("Could not generate JSON netmap: %v", err)
}
}
if n.Prefs.Valid() {
jsCallbacks.Call("notifyExitNode", string(n.Prefs.ExitNodeID()))
}
if n.BrowseToURL != nil {
jsCallbacks.Call("notifyBrowseToURL", *n.BrowseToURL)
}
@ -576,6 +596,24 @@ func (i *jsIPN) fetch(url string) js.Value {
})
}
func (i *jsIPN) setExitNode(stableNodeID string) js.Value {
return makePromise(func() (any, error) {
mp := &ipn.MaskedPrefs{
ExitNodeIDSet: true,
Prefs: ipn.Prefs{ExitNodeID: tailcfg.StableNodeID(stableNodeID)},
}
_, err := i.lb.EditPrefs(mp)
return nil, err
})
}
func (i *jsIPN) setExitNodeEnabled(enabled bool) js.Value {
return makePromise(func() (any, error) {
_, err := i.lb.SetUseExitNodeEnabled(ipnauth.Self, enabled)
return nil, err
})
}
func (i *jsIPN) dial(network, addr string) js.Value {
return makePromise(func() (any, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
@ -875,8 +913,10 @@ type jsNetMapSelfNode struct {
type jsNetMapPeerNode struct {
jsNetMapNode
Online *bool `json:"online,omitempty"`
TailscaleSSHEnabled bool `json:"tailscaleSSHEnabled"`
Online *bool `json:"online,omitempty"`
TailscaleSSHEnabled bool `json:"tailscaleSSHEnabled"`
ExitNodeOption bool `json:"exitNodeOption"`
StableNodeID string `json:"stableNodeID"`
}
type jsStateStore struct {

Loading…
Cancel
Save