Compare commits
4
Commits
webnet
...
f961db8925
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f961db8925 | ||
|
|
fde5f11895 | ||
|
|
756ba1d5ec | ||
|
|
68670f938b |
@@ -228,7 +228,7 @@ func buildWasm(dev bool) ([]byte, error) {
|
||||
// to fail for unclosed files.
|
||||
defer outputFile.Close()
|
||||
|
||||
args := []string{"build", "-tags", "tailscale_go,osusergo,netgo,nethttpomithttp2,omitidna,omitpemdecrypt"}
|
||||
args := []string{"build", "-tags", "tailscale_go,osusergo,netgo,omitidna,omitpemdecrypt"}
|
||||
if !dev {
|
||||
if *devControl != "" {
|
||||
return nil, fmt.Errorf("Development control URL can only be used in dev mode.")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -280,6 +280,11 @@ type Impl struct {
|
||||
packetsInFlight map[stack.TransportEndpointID]struct{}
|
||||
}
|
||||
|
||||
// Stack returns the underlying gVisor network stack.
|
||||
func (ns *Impl) Stack() *stack.Stack {
|
||||
return ns.ipstack
|
||||
}
|
||||
|
||||
const nicID = 1
|
||||
|
||||
// maxUDPPacketSize is the maximum size of a UDP packet we copy in
|
||||
|
||||
Reference in New Issue
Block a user