|
|
|
@@ -12,19 +12,29 @@ package main
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
"crypto/x509"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"math/rand/v2"
|
|
|
|
|
"net"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/netip"
|
|
|
|
|
"strconv"
|
|
|
|
|
"strings"
|
|
|
|
|
"syscall/js"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
|
"gvisor.dev/gvisor/pkg/tcpip"
|
|
|
|
|
"gvisor.dev/gvisor/pkg/tcpip/adapters/gonet"
|
|
|
|
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
|
|
|
|
|
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
|
|
|
|
|
"gvisor.dev/gvisor/pkg/tcpip/transport/icmp"
|
|
|
|
|
"gvisor.dev/gvisor/pkg/waiter"
|
|
|
|
|
"tailscale.com/control/controlclient"
|
|
|
|
|
"tailscale.com/ipn"
|
|
|
|
|
"tailscale.com/ipn/ipnauth"
|
|
|
|
@@ -33,6 +43,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"
|
|
|
|
@@ -154,6 +165,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
|
|
|
|
dialer: dialer,
|
|
|
|
|
srv: srv,
|
|
|
|
|
lb: lb,
|
|
|
|
|
ns: ns,
|
|
|
|
|
controlURL: controlURL,
|
|
|
|
|
authKey: authKey,
|
|
|
|
|
hostname: hostname,
|
|
|
|
@@ -208,6 +220,38 @@ func newIPN(jsConfig js.Value) map[string]any {
|
|
|
|
|
url := args[0].String()
|
|
|
|
|
return jsIPN.fetch(url)
|
|
|
|
|
}),
|
|
|
|
|
"dial": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
if len(args) != 2 {
|
|
|
|
|
log.Printf("Usage: dial(network, addr)")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return jsIPN.dial(args[0].String(), args[1].String())
|
|
|
|
|
}),
|
|
|
|
|
"listen": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
if len(args) != 2 {
|
|
|
|
|
log.Printf("Usage: listen(network, addr)")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return jsIPN.listen(args[0].String(), args[1].String())
|
|
|
|
|
}),
|
|
|
|
|
"listenICMP": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
if len(args) != 1 {
|
|
|
|
|
log.Printf("Usage: listenICMP(network)")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -215,6 +259,7 @@ type jsIPN struct {
|
|
|
|
|
dialer *tsdial.Dialer
|
|
|
|
|
srv *ipnserver.Server
|
|
|
|
|
lb *ipnlocal.LocalBackend
|
|
|
|
|
ns *netstack.Impl
|
|
|
|
|
controlURL string
|
|
|
|
|
authKey string
|
|
|
|
|
hostname string
|
|
|
|
@@ -531,6 +576,275 @@ func (i *jsIPN) fetch(url string) js.Value {
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *jsIPN) dial(network, addr string) js.Value {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
conn, err := i.dialer.UserDial(ctx, network, addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return wrapConn(conn), nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (i *jsIPN) listen(network, addr string) js.Value {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
switch network {
|
|
|
|
|
case "tcp", "tcp4", "tcp6":
|
|
|
|
|
// netstack.ListenTCP only accepts tcp4/tcp6; bare "tcp"
|
|
|
|
|
// defaults to IPv4 to match net.Listen's typical behavior
|
|
|
|
|
// when given an unspecified address.
|
|
|
|
|
n := network
|
|
|
|
|
if n == "tcp" {
|
|
|
|
|
n = "tcp4"
|
|
|
|
|
}
|
|
|
|
|
ln, err := i.ns.ListenTCP(n, addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return wrapTCPListener(ln), nil
|
|
|
|
|
case "udp", "udp4", "udp6":
|
|
|
|
|
pc, err := i.ns.ListenPacket(network, addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return wrapPacketConn(pc), nil
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unsupported network %q", network)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
var networkProto tcpip.NetworkProtocolNumber
|
|
|
|
|
|
|
|
|
|
switch network {
|
|
|
|
|
case "icmp4", "icmp":
|
|
|
|
|
transportProto = icmp.ProtocolNumber4
|
|
|
|
|
networkProto = ipv4.ProtocolNumber
|
|
|
|
|
case "icmp6":
|
|
|
|
|
transportProto = icmp.ProtocolNumber6
|
|
|
|
|
networkProto = ipv6.ProtocolNumber
|
|
|
|
|
default:
|
|
|
|
|
return nil, fmt.Errorf("unsupported network %q (use \"icmp4\" or \"icmp6\")", network)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
st := i.ns.Stack()
|
|
|
|
|
var wq waiter.Queue
|
|
|
|
|
ep, nserr := st.NewEndpoint(transportProto, networkProto, &wq)
|
|
|
|
|
if nserr != nil {
|
|
|
|
|
return nil, fmt.Errorf("creating ICMP endpoint: %v", nserr)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pc := gonet.NewUDPConn(&wq, ep)
|
|
|
|
|
return wrapPacketConn(pc), nil
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// wrapConn exposes a net.Conn to JavaScript with binary (Uint8Array) I/O.
|
|
|
|
|
func wrapConn(conn net.Conn) map[string]any {
|
|
|
|
|
return map[string]any{
|
|
|
|
|
"read": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
buf := make([]byte, 65536)
|
|
|
|
|
n, err := conn.Read(buf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
arr := js.Global().Get("Uint8Array").New(n)
|
|
|
|
|
js.CopyBytesToJS(arr, buf[:n])
|
|
|
|
|
return arr, nil
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
"write": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
data := args[0]
|
|
|
|
|
buf := make([]byte, data.Get("length").Int())
|
|
|
|
|
js.CopyBytesToGo(buf, data)
|
|
|
|
|
n, err := conn.Write(buf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return n, nil
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
"close": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return conn.Close() != nil
|
|
|
|
|
}),
|
|
|
|
|
"localAddr": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return conn.LocalAddr().String()
|
|
|
|
|
}),
|
|
|
|
|
"remoteAddr": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return conn.RemoteAddr().String()
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// wrapTCPListener exposes a net.Listener to JavaScript as an object with
|
|
|
|
|
// accept/close/addr methods plus a Symbol.asyncIterator implementation, so
|
|
|
|
|
// callers can write `for await (const conn of listener)`.
|
|
|
|
|
func wrapTCPListener(ln net.Listener) js.Value {
|
|
|
|
|
obj := js.Global().Get("Object").New()
|
|
|
|
|
obj.Set("accept", js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
conn, err := ln.Accept()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return wrapConn(conn), nil
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
obj.Set("close", js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return ln.Close() != nil
|
|
|
|
|
}))
|
|
|
|
|
obj.Set("addr", js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return ln.Addr().String()
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
asyncIterSym := js.Global().Get("Symbol").Get("asyncIterator")
|
|
|
|
|
iterFactory := js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
iter := js.Global().Get("Object").New()
|
|
|
|
|
iter.Set("next", js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
conn, err := ln.Accept()
|
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, net.ErrClosed) {
|
|
|
|
|
return map[string]any{
|
|
|
|
|
"value": js.Undefined(),
|
|
|
|
|
"done": true,
|
|
|
|
|
}, nil
|
|
|
|
|
}
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return map[string]any{
|
|
|
|
|
"value": wrapConn(conn),
|
|
|
|
|
"done": false,
|
|
|
|
|
}, nil
|
|
|
|
|
})
|
|
|
|
|
}))
|
|
|
|
|
return iter
|
|
|
|
|
})
|
|
|
|
|
js.Global().Get("Reflect").Call("set", obj, asyncIterSym, iterFactory)
|
|
|
|
|
return obj
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// wrapPacketConn exposes a net.PacketConn to JavaScript with binary (Uint8Array) I/O.
|
|
|
|
|
func wrapPacketConn(pc net.PacketConn) map[string]any {
|
|
|
|
|
return map[string]any{
|
|
|
|
|
"readFrom": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
buf := make([]byte, 65536)
|
|
|
|
|
n, addr, err := pc.ReadFrom(buf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
arr := js.Global().Get("Uint8Array").New(n)
|
|
|
|
|
js.CopyBytesToJS(arr, buf[:n])
|
|
|
|
|
return map[string]any{
|
|
|
|
|
"data": arr,
|
|
|
|
|
"addr": addr.String(),
|
|
|
|
|
}, nil
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
"writeTo": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return makePromise(func() (any, error) {
|
|
|
|
|
data := args[0]
|
|
|
|
|
addrStr := args[1].String()
|
|
|
|
|
buf := make([]byte, data.Get("length").Int())
|
|
|
|
|
js.CopyBytesToGo(buf, data)
|
|
|
|
|
addr, err := resolveUDPAddr(addrStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
n, err := pc.WriteTo(buf, addr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return n, nil
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
"close": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return pc.Close() != nil
|
|
|
|
|
}),
|
|
|
|
|
"localAddr": js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
|
|
|
return pc.LocalAddr().String()
|
|
|
|
|
}),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// resolveUDPAddr parses an address string that is either "host:port" or just
|
|
|
|
|
// an IP (for ICMP, where port defaults to 0).
|
|
|
|
|
func resolveUDPAddr(s string) (*net.UDPAddr, error) {
|
|
|
|
|
host, portStr, err := net.SplitHostPort(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Bare IP address without port (used for ICMP).
|
|
|
|
|
ip := net.ParseIP(s)
|
|
|
|
|
if ip == nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid address: %s", s)
|
|
|
|
|
}
|
|
|
|
|
return &net.UDPAddr{IP: ip}, nil
|
|
|
|
|
}
|
|
|
|
|
ip := net.ParseIP(host)
|
|
|
|
|
if ip == nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid IP: %s", host)
|
|
|
|
|
}
|
|
|
|
|
port, err := strconv.Atoi(portStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("invalid port: %s", portStr)
|
|
|
|
|
}
|
|
|
|
|
return &net.UDPAddr{IP: ip, Port: port}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type termWriter struct {
|
|
|
|
|
f js.Value
|
|
|
|
|
}
|
|
|
|
|