feat(tsconnect/wasm): add hasShares filter to listDrivePeers

PeerCapabilityTaildriveSharer says a peer may share with us, not that it
does, so listDrivePeers is a superset of the peers actually exposing
shares. listDrivePeers now takes an options object; with
{hasShares: true} each candidate's taildrive root is probed with a
Depth-1 PROPFIND and only peers listing at least one share are kept.

The probe and its multistatus parsing live in cmd/tsconnect/driveprobe
so they can be tested without syscall/js. Probes run in parallel with a
bounded worker count, a per-probe timeout and a bounded response read;
a probe that fails drops that peer and is logged rather than failing
the call, so the filter is positive-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-02 23:32:03 +00:00
co-authored by Claude
parent 7e9868f50e
commit 3b239fe9e2
5 changed files with 454 additions and 4 deletions
+41 -3
View File
@@ -6,14 +6,17 @@
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"sync"
"syscall/js"
"tailscale.com/cmd/tsconnect/driveprobe"
"tailscale.com/drive"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
@@ -228,11 +231,37 @@ func wireDriveJS(i *jsIPN, driveFS *jsFileSystemForRemote, m map[string]any) {
return nil
})
m["listDrivePeers"] = js.FuncOf(func(_ js.Value, _ []js.Value) any {
return i.listDrivePeers()
m["listDrivePeers"] = js.FuncOf(func(_ js.Value, args []js.Value) any {
var hasShares bool
if len(args) > 0 && !args[0].IsUndefined() && !args[0].IsNull() {
if v := args[0].Get("hasShares"); v.Type() == js.TypeBoolean {
hasShares = v.Bool()
}
}
return i.listDrivePeers(hasShares)
})
}
// filterPeersWithShares keeps only the peers whose Taildrive endpoint lists at
// least one share for us, preserving the order of the input.
func (i *jsIPN) filterPeersWithShares(peers []jsDrivePeer) []jsDrivePeer {
urls := make([]string, len(peers))
for n, p := range peers {
urls[n] = p.PeerAPIURL
}
client := &http.Client{Transport: i.lb.Dialer().PeerAPITransport()}
results := driveprobe.HasSharesMulti(context.Background(), client, urls, log.Printf)
kept := make([]jsDrivePeer, 0, len(peers))
for n, p := range peers {
if results[n] {
kept = append(kept, p)
}
}
return kept
}
type jsDrivePeer struct {
Name string `json:"name"`
PeerAPIURL string `json:"peerAPIURL"`
@@ -248,7 +277,12 @@ type jsDrivePeer struct {
//
// The cap means a peer is allowed to share with us, not that it currently
// exposes any share, so the result is a superset of the peers with shares.
func (i *jsIPN) listDrivePeers() js.Value {
//
// hasShares narrows it to peers we confirmed are exposing at least one share,
// at the cost of one peerAPI round-trip per candidate (run in parallel). It is
// a positive filter: a peer we could not reach is left out, which is not the
// same as knowing it has no shares.
func (i *jsIPN) listDrivePeers(hasShares bool) js.Value {
return makePromise(func() (any, error) {
if !i.lb.DriveAccessEnabled() {
return "[]", nil
@@ -301,6 +335,10 @@ func (i *jsIPN) listDrivePeers() js.Value {
})
}
if hasShares && len(peers) > 0 {
peers = i.filterPeersWithShares(peers)
}
b, err := json.Marshal(peers)
if err != nil {
return nil, fmt.Errorf("listDrivePeers: marshal: %w", err)
+1 -1
View File
@@ -20,7 +20,7 @@ func initDriveForRemote(_ *tsd.System) *jsFileSystemForRemote { return nil }
func wireDriveJS(_ *jsIPN, _ *jsFileSystemForRemote, _ map[string]any) {}
// listDrivePeers returns an empty list when the drive feature is omitted.
func (i *jsIPN) listDrivePeers() js.Value {
func (i *jsIPN) listDrivePeers(_ bool) js.Value {
return makePromise(func() (any, error) {
return "[]", nil
})