feat(tsconnect): expose dialTLS to JS
Add ipn.dialTLS(addr, opts?) which dials a TCP connection through the Tailscale dialer and performs a TLS handshake on top, returning a JS Conn just like ipn.dial. WASM has no system root pool, so verification defaults to the baked-in LetsEncrypt ISRG roots already linked via net/bakedroots. That covers any tailnet HTTPS endpoint provisioned via `tailscale cert`. Callers can override with opts.caCerts (PEM) or bypass entirely with opts.insecureSkipVerify, and override SNI with opts.serverName. Marginal binary cost is ~10 KiB on top of the existing ~31.6 MiB wasm: crypto/tls and the x509 verification path are already pulled in by control/controlclient and net/tlsdial.
This commit is contained in:
@@ -12,6 +12,8 @@ package main
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -40,6 +42,7 @@ import (
|
|||||||
"tailscale.com/ipn/store/mem"
|
"tailscale.com/ipn/store/mem"
|
||||||
"tailscale.com/logpolicy"
|
"tailscale.com/logpolicy"
|
||||||
"tailscale.com/logtail"
|
"tailscale.com/logtail"
|
||||||
|
"tailscale.com/net/bakedroots"
|
||||||
"tailscale.com/net/netns"
|
"tailscale.com/net/netns"
|
||||||
"tailscale.com/net/tsdial"
|
"tailscale.com/net/tsdial"
|
||||||
"tailscale.com/safesocket"
|
"tailscale.com/safesocket"
|
||||||
@@ -237,6 +240,17 @@ func newIPN(jsConfig js.Value) map[string]any {
|
|||||||
}
|
}
|
||||||
return jsIPN.listenICMP(args[0].String())
|
return jsIPN.listenICMP(args[0].String())
|
||||||
}),
|
}),
|
||||||
|
"dialTLS": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||||
|
if len(args) < 1 || len(args) > 2 {
|
||||||
|
log.Printf("Usage: dialTLS(addr, opts?)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var opts js.Value
|
||||||
|
if len(args) == 2 {
|
||||||
|
opts = args[1]
|
||||||
|
}
|
||||||
|
return jsIPN.dialTLS(args[0].String(), opts)
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,6 +597,53 @@ func (i *jsIPN) listen(network, addr string) js.Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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: host,
|
||||||
|
RootCAs: bakedroots.Get(),
|
||||||
|
}
|
||||||
|
if !opts.IsUndefined() && !opts.IsNull() {
|
||||||
|
if sn := opts.Get("serverName"); sn.Type() == js.TypeString {
|
||||||
|
cfg.ServerName = sn.String()
|
||||||
|
}
|
||||||
|
if iv := opts.Get("insecureSkipVerify"); iv.Type() == js.TypeBoolean {
|
||||||
|
cfg.InsecureSkipVerify = iv.Bool()
|
||||||
|
}
|
||||||
|
if ca := opts.Get("caCerts"); ca.Type() == js.TypeString {
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if !pool.AppendCertsFromPEM([]byte(ca.String())) {
|
||||||
|
return nil, fmt.Errorf("caCerts: no valid PEM certificates found")
|
||||||
|
}
|
||||||
|
cfg.RootCAs = pool
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConn, err := i.dialer.UserDial(ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tlsConn := tls.Client(rawConn, cfg)
|
||||||
|
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||||
|
rawConn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return wrapConn(tlsConn), nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (i *jsIPN) listenICMP(network string) js.Value {
|
func (i *jsIPN) listenICMP(network string) js.Value {
|
||||||
return makePromise(func() (any, error) {
|
return makePromise(func() (any, error) {
|
||||||
var transportProto tcpip.TransportProtocolNumber
|
var transportProto tcpip.TransportProtocolNumber
|
||||||
|
|||||||
Reference in New Issue
Block a user