|
|
|
@@ -370,13 +370,6 @@ func newIPN(jsConfig js.Value, shutdownCh chan struct{}) map[string]any {
|
|
|
|
|
"shutdown": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return jsIPN.shutdown()
|
|
|
|
|
}),
|
|
|
|
|
"setServices": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
if len(args) != 1 {
|
|
|
|
|
log.Printf("Usage: setServices(services)")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return jsIPN.setServices(args[0])
|
|
|
|
|
}),
|
|
|
|
|
"localAPI": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
if len(args) < 2 {
|
|
|
|
|
log.Printf("Usage: localAPI(method, path[, body])")
|
|
|
|
@@ -491,7 +484,6 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
|
|
|
|
NodeKey: nm.NodeKey.String(),
|
|
|
|
|
MachineKey: nm.MachineKey.String(),
|
|
|
|
|
PeerAPIURL: selfPeerAPIURL,
|
|
|
|
|
Services: userServicesFromView(nm.SelfNode.Hostinfo().Services()),
|
|
|
|
|
},
|
|
|
|
|
MachineStatus: jsMachineStatus[nm.GetMachineStatus()],
|
|
|
|
|
},
|
|
|
|
@@ -516,7 +508,6 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
|
|
|
|
MachineKey: p.Machine().String(),
|
|
|
|
|
NodeKey: p.Key().String(),
|
|
|
|
|
PeerAPIURL: peerURL,
|
|
|
|
|
Services: userServicesFromView(p.Hostinfo().Services()),
|
|
|
|
|
},
|
|
|
|
|
Online: p.Online().Clone(),
|
|
|
|
|
TailscaleSSHEnabled: p.Hostinfo().TailscaleSSHEnabled(),
|
|
|
|
@@ -889,18 +880,22 @@ func (i *jsIPN) listen(network, addr string) js.Value {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tlsClientConfigFromJS builds a client tls.Config from optional JS options
|
|
|
|
|
// (serverName, insecureSkipVerify, caCerts). defaultServerName may be empty
|
|
|
|
|
// (STARTTLS upgrade case), in which case serverName must be provided unless
|
|
|
|
|
// insecureSkipVerify is set.
|
|
|
|
|
//
|
|
|
|
|
// On wasm there's no system root pool, so default to the baked-in
|
|
|
|
|
// LetsEncrypt roots (which is what `tailscale cert` uses for tailnet
|
|
|
|
|
// HTTPS endpoints). Callers can override with caCerts (PEM) or bypass
|
|
|
|
|
// entirely with insecureSkipVerify.
|
|
|
|
|
func tlsClientConfigFromJS(defaultServerName string, opts js.Value) (*tls.Config, error) {
|
|
|
|
|
func (i *jsIPN) dialTLS(addr string, opts js.Value) js.Value {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
host, _, err := net.SplitHostPort(addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid address %q: %w", addr, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// On wasm there's no system root pool, so default to the
|
|
|
|
|
// baked-in LetsEncrypt roots (which is what `tailscale cert`
|
|
|
|
|
// uses for tailnet HTTPS endpoints). Callers can override with
|
|
|
|
|
// caCerts (PEM) or bypass entirely with insecureSkipVerify.
|
|
|
|
|
cfg := &tls.Config{
|
|
|
|
|
ServerName: defaultServerName,
|
|
|
|
|
ServerName: host,
|
|
|
|
|
RootCAs: bakedroots.Get(),
|
|
|
|
|
}
|
|
|
|
|
if !opts.IsUndefined() && !opts.IsNull() {
|
|
|
|
@@ -918,26 +913,6 @@ func tlsClientConfigFromJS(defaultServerName string, opts js.Value) (*tls.Config
|
|
|
|
|
cfg.RootCAs = pool
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if cfg.ServerName == "" && !cfg.InsecureSkipVerify {
|
|
|
|
|
return nil, fmt.Errorf("serverName is required unless insecureSkipVerify is set")
|
|
|
|
|
}
|
|
|
|
|
return cfg, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *jsIPN) dialTLS(addr string, opts js.Value) js.Value {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
host, _, err := net.SplitHostPort(addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid address %q: %w", addr, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cfg, err := tlsClientConfigFromJS(host, opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rawConn, err := i.dialer.UserDial(ctx, "tcp", addr)
|
|
|
|
|
if err != nil {
|
|
|
|
@@ -1368,39 +1343,6 @@ func (i *jsIPN) suggestExitNode() js.Value {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *jsIPN) setServices(jsServices js.Value) js.Value {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
n := jsServices.Length()
|
|
|
|
|
svcs := make([]tailcfg.Service, 0, n)
|
|
|
|
|
for idx := range n {
|
|
|
|
|
s := jsServices.Index(idx)
|
|
|
|
|
proto := tailcfg.ServiceProto(s.Get("proto").String())
|
|
|
|
|
port := uint16(s.Get("port").Int())
|
|
|
|
|
var desc string
|
|
|
|
|
if d := s.Get("description"); d.Type() == js.TypeString {
|
|
|
|
|
desc = d.String()
|
|
|
|
|
}
|
|
|
|
|
svcs = append(svcs, tailcfg.Service{Proto: proto, Port: port, Description: desc})
|
|
|
|
|
}
|
|
|
|
|
i.lb.SetExplicitServices(svcs)
|
|
|
|
|
return nil, nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// userServicesFromView converts a hostinfo services slice to jsService entries,
|
|
|
|
|
// filtering out internal peerapi protocol entries (already reflected in peerAPIURL).
|
|
|
|
|
func userServicesFromView(svcs views.Slice[tailcfg.Service]) []jsService {
|
|
|
|
|
out := make([]jsService, 0, svcs.Len())
|
|
|
|
|
for _, s := range svcs.All() {
|
|
|
|
|
switch s.Proto {
|
|
|
|
|
case tailcfg.PeerAPI4, tailcfg.PeerAPI6, tailcfg.PeerAPIDNS:
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
out = append(out, jsService{Proto: string(s.Proto), Port: s.Port, Description: s.Description})
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *jsIPN) localAPI(method, path, body string) js.Value {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
h := localapi.NewHandler(localapi.HandlerConfig{
|
|
|
|
@@ -1480,51 +1422,6 @@ func wrapConn(conn net.Conn) map[string]any {
|
|
|
|
|
"remoteAddr": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return conn.RemoteAddr().String()
|
|
|
|
|
}),
|
|
|
|
|
// upgradeTLS wraps the conn in TLS in place (STARTTLS-style) and
|
|
|
|
|
// returns a new wrapped conn sharing the same underlying net.Conn;
|
|
|
|
|
// the old handle must not be used afterward. On any failure —
|
|
|
|
|
// configuration or handshake — the underlying conn is closed, so
|
|
|
|
|
// callers can treat every rejection as fatal to the connection.
|
|
|
|
|
// With isServer, certPem and keyPem are required; otherwise client
|
|
|
|
|
// options as in dialTLS apply.
|
|
|
|
|
"upgradeTLS": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
opts := js.Undefined()
|
|
|
|
|
if len(args) > 0 {
|
|
|
|
|
opts = args[0]
|
|
|
|
|
}
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
var tlsConn *tls.Conn
|
|
|
|
|
hasOpts := !opts.IsUndefined() && !opts.IsNull()
|
|
|
|
|
if hasOpts && opts.Get("isServer").Type() == js.TypeBoolean && opts.Get("isServer").Bool() {
|
|
|
|
|
certPem := opts.Get("certPem")
|
|
|
|
|
keyPem := opts.Get("keyPem")
|
|
|
|
|
if certPem.Type() != js.TypeString || keyPem.Type() != js.TypeString {
|
|
|
|
|
conn.Close()
|
|
|
|
|
return nil, fmt.Errorf("upgradeTLS: certPem and keyPem are required when isServer is set")
|
|
|
|
|
}
|
|
|
|
|
cert, err := tls.X509KeyPair([]byte(certPem.String()), []byte(keyPem.String()))
|
|
|
|
|
if err != nil {
|
|
|
|
|
conn.Close()
|
|
|
|
|
return nil, fmt.Errorf("upgradeTLS: parsing cert/key: %w", err)
|
|
|
|
|
}
|
|
|
|
|
tlsConn = tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{cert}})
|
|
|
|
|
} else {
|
|
|
|
|
cfg, err := tlsClientConfigFromJS("", opts)
|
|
|
|
|
if err != nil {
|
|
|
|
|
conn.Close()
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
tlsConn = tls.Client(conn, cfg)
|
|
|
|
|
}
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
|
|
|
|
conn.Close()
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return wrapConn(tlsConn), nil
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1682,19 +1579,12 @@ type jsNetMap struct {
|
|
|
|
|
LockedOut bool `json:"lockedOut"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type jsService struct {
|
|
|
|
|
Proto string `json:"proto"`
|
|
|
|
|
Port uint16 `json:"port"`
|
|
|
|
|
Description string `json:"description,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type jsNetMapNode struct {
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Addresses []string `json:"addresses"`
|
|
|
|
|
MachineKey string `json:"machineKey"`
|
|
|
|
|
NodeKey string `json:"nodeKey"`
|
|
|
|
|
PeerAPIURL string `json:"peerAPIURL,omitempty"`
|
|
|
|
|
Services []jsService `json:"services"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type jsNetMapSelfNode struct {
|
|
|
|
|