apitype.WaitingFile has no json tags so it serialised as {Name, Size}.
Introduce a local jsWaitingFile struct with json:"name" / json:"size"
so the JS side receives idiomatic camelCase property names.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The io.Copy in PutFile was writing directly to wc, bypassing the
incomingFile wrapper whose Write method increments f.copied and fires
a throttled sendFileNotify on progress. As a result, notifyIncomingFiles
on the JS side only ever fired once (on completion) with received=0,
making progress UI impossible. The original inFile wrapping was lost
during the Android SAF refactor.
Also surface the PartialFile.Done flag through jsIncomingFile so JS can
distinguish the final "transfer complete" notification from in-progress
updates.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
n.Prefs is *PrefsView (a pointer), so calling n.Prefs.Valid() on a
Notify where Prefs is nil auto-dereferenced nil and panicked. The
callback's defer recover() swallowed the panic, which meant every
Notify without Prefs (Health-only, FilesWaiting, IncomingFiles,
OutgoingFiles, etc.) never reached the file-related JS calls.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Export UpdateOutgoingFiles on taildrop.Extension so it can be called
from outside the package (wasm bridge, package main).
- Wrap sendFile's PUT body with progresstracking.NewReader so bytes-sent
is sampled roughly once per second during transfer.
- Create an OutgoingFile entry (with UUID, peer ID, name, declared size)
before the PUT and call UpdateOutgoingFiles on each progress tick and
on completion (setting Finished/Succeeded). This flows into the IPN
notify stream as OutgoingFiles notifications.
- Add jsOutgoingFile struct and wire n.OutgoingFiles into a new
notifyOutgoingFiles callback in run(), mirroring notifyIncomingFiles.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire two new callbacks into the IPN notify stream:
- notifyFilesWaiting: fires when a completed inbound transfer is staged
and ready to retrieve via waitingFiles(). Triggered by n.FilesWaiting
in the notify stream.
- notifyIncomingFiles: fires with a JSON snapshot of in-progress inbound
transfers whenever progress changes (roughly once per second while
active, plus once at completion). The jsIncomingFile struct carries
name, started (Unix ms), declaredSize, and received bytes. An empty
array indicates all active transfers have finished.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add exit node support to the wasm JS bridge:
- Include `exitNodeOption` and `stableNodeID` on each peer in the
notifyNetMap payload so callers can identify which peers are exit
nodes and reference them by stable ID.
- Call `notifyExitNode(stableNodeID)` whenever prefs change, so
callers can track which exit node (if any) is currently active.
- Expose `setExitNode(stableNodeID)` — sets ExitNodeID via EditPrefs.
- Expose `setExitNodeEnabled(enabled)` — toggles the last-used exit
node on/off via SetUseExitNodeEnabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend ipn.listen to also accept "tcp"/"tcp4"/"tcp6" and return a
TCPListener bound to a netstack gonet.TCPListener. The listener
exposes accept/close/addr like a Go net.Listener and additionally
implements Symbol.asyncIterator so JS callers can write:
for await (const conn of listener) { ... }
The async iterator returns done when the listener is closed (via
errors.Is(net.ErrClosed)) and rejects on any other accept error.
Symbol-keyed properties are set via Reflect.set since syscall/js
only exposes string-keyed Set.
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.
Wire up the userspace networking primitives to the JS bridge so
browser callers can initiate outbound and receive inbound traffic
over the Tailscale network:
- ipn.dial(network, addr) wraps a tsdial UserDial into a JS Conn
with read/write/close/localAddr/remoteAddr.
- ipn.listen(network, addr) wraps a netstack ListenPacket into a
JS PacketConn with readFrom/writeTo/close/localAddr.
- ipn.listenICMP("icmp4"|"icmp6"|"icmp") creates a raw ICMP
endpoint on the underlying gVisor stack and wraps it as a
PacketConn for sending/receiving ping traffic.
To support listenICMP, netstack.Impl gains a Stack() accessor that
returns the underlying *stack.Stack so jsIPN can call NewEndpoint
with icmp.ProtocolNumber4/6.
Binary I/O uses js.CopyBytesToGo / js.CopyBytesToJS to move bytes
across the syscall/js boundary without base64 round-trips.
After 1d93bdce2 ("control/controlclient: remove x/net/http2, use
net/http"), the noise control client uses net/http's Transport with
Protocols.SetUnencryptedHTTP2(true). The nethttpomithttp2 build tag
strips the bundled HTTP/2 implementation from net/http, so at runtime
the control client fails the first register request with "http:
Transport does not support unencrypted HTTP/2" and the wasm never
connects.
Drop the tag so the bundled HTTP/2 ships in the wasm binary.
1 week ago
9 changed files with 906 additions and 9 deletions