diff --git a/cmd/tsconnect/wasm/wasm_js.go b/cmd/tsconnect/wasm/wasm_js.go index 13806e271..76faa4924 100644 --- a/cmd/tsconnect/wasm/wasm_js.go +++ b/cmd/tsconnect/wasm/wasm_js.go @@ -12,6 +12,8 @@ package main import ( "bytes" "context" + "crypto/tls" + "crypto/x509" "encoding/hex" "encoding/json" "fmt" @@ -40,6 +42,7 @@ import ( "tailscale.com/ipn/store/mem" "tailscale.com/logpolicy" "tailscale.com/logtail" + "tailscale.com/net/bakedroots" "tailscale.com/net/netns" "tailscale.com/net/tsdial" "tailscale.com/safesocket" @@ -237,6 +240,17 @@ func newIPN(jsConfig js.Value) map[string]any { } 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 { return makePromise(func() (any, error) { var transportProto tcpip.TransportProtocolNumber