feat(tsconnect): add whoIs, queryDNS, ping, suggestExitNode WASM bindings, peerAPI/localAPI access #5

Merged
codinget merged 4 commits from feat/query-utils into webnet 2026-05-11 22:51:27 +02:00
Showing only changes of commit 7fd2507611 - Show all commits
+69 -6
View File
@@ -1115,11 +1115,16 @@ func (i *jsIPN) setFunnel(hostname string, port uint16, enabled bool) js.Value {
})
}
func (i *jsIPN) whoIs(addrPort string, proto string) js.Value {
func (i *jsIPN) whoIs(addr string, proto string) js.Value {
return makePromise(func() (any, error) {
ipp, err := netip.ParseAddrPort(addrPort)
if err != nil {
return nil, fmt.Errorf("whoIs: invalid addr:port %q: %w", addrPort, err)
// Accept both "ip:port" and bare "ip" (port 0 still resolves by IP).
var ipp netip.AddrPort
if ap, err := netip.ParseAddrPort(addr); err == nil {
ipp = ap
} else if ip, err := netip.ParseAddr(addr); err == nil {
ipp = netip.AddrPortFrom(ip, 0)
} else {
return nil, fmt.Errorf("whoIs: invalid address %q (want ip:port or ip)", addr)
}
n, u, ok := i.lb.WhoIs(proto, ipp)
if !ok {
@@ -1148,9 +1153,61 @@ func (i *jsIPN) whoIs(addrPort string, proto string) js.Value {
func (i *jsIPN) queryDNS(name string, queryType int) js.Value {
return makePromise(func() (any, error) {
res, resolvers, err := i.lb.QueryDNS(name, dnsmessage.Type(queryType))
if err != nil {
return nil, err
// Detect SERVFAIL with no upstream resolvers (common when an exit node is
// active but the DNS manager forwarder has no configured upstreams). Fall
// back to querying 8.8.8.8 via the dialer (which routes through the exit
// node), then as a last resort use the browser's default name resolver.
needsFallback := err != nil
if !needsFallback && len(resolvers) == 0 && len(res) > 0 {
var hdrParser dnsmessage.Parser
if hdr, hdrErr := hdrParser.Start(res); hdrErr == nil && hdr.RCode == dnsmessage.RCodeServerFailure {
needsFallback = true
}
}
if needsFallback {
qt := dnsmessage.Type(queryType)
if qt != dnsmessage.TypeA && qt != dnsmessage.TypeAAAA {
if err != nil {
return nil, fmt.Errorf("queryDNS: %w (no upstream resolver; only A/AAAA queries support fallback)", err)
}
return nil, fmt.Errorf("queryDNS: no upstream resolver available; only A/AAAA queries support fallback lookup")
}
ctx := context.Background()
d := i.dialer
r := &net.Resolver{
PreferGo: true,
Dial: func(rctx context.Context, network, address string) (net.Conn, error) {
return d.UserDial(rctx, "tcp", "8.8.8.8:53")
},
}
ips, rerr := r.LookupIPAddr(ctx, name)
if rerr != nil {
// Last resort: browser-native resolution (no exit-node routing).
ips, rerr = (&net.Resolver{PreferGo: false}).LookupIPAddr(ctx, name)
if rerr != nil {
return nil, fmt.Errorf("queryDNS: fallback resolution failed: %w", rerr)
}
}
var answers []any
for _, ia := range ips {
ip, ok := netip.AddrFromSlice(ia.IP)
if !ok {
continue
}
ip = ip.Unmap()
if qt == dnsmessage.TypeA && ip.Is4() {
answers = append(answers, ip.String())
} else if qt == dnsmessage.TypeAAAA && ip.Is6() {
answers = append(answers, ip.String())
}
}
return map[string]any{
"answers": answers,
"resolvers": []any{},
}, nil
}
var p dnsmessage.Parser
if _, err := p.Start(res); err != nil {
return nil, fmt.Errorf("queryDNS: parsing response: %w", err)
@@ -1217,6 +1274,12 @@ func (i *jsIPN) ping(ip string, pingType string, size int) js.Value {
if err != nil {
return nil, fmt.Errorf("ping: invalid IP %q: %w", ip, err)
}
switch tailcfg.PingType(pingType) {
case tailcfg.PingDisco, tailcfg.PingTSMP, tailcfg.PingICMP, tailcfg.PingPeerAPI:
// valid
default:
return nil, fmt.Errorf("ping: unknown type %q, must be one of: disco, TSMP, icmp, peerapi", pingType)
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
pr, err := i.lb.Ping(ctx, addr, tailcfg.PingType(pingType), size)