feat(tsconnect): add whoIs, queryDNS, ping, suggestExitNode WASM bindings, peerAPI/localAPI access #5
@@ -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) {
|
return makePromise(func() (any, error) {
|
||||||
ipp, err := netip.ParseAddrPort(addrPort)
|
// Accept both "ip:port" and bare "ip" (port 0 still resolves by IP).
|
||||||
if err != nil {
|
var ipp netip.AddrPort
|
||||||
return nil, fmt.Errorf("whoIs: invalid addr:port %q: %w", addrPort, err)
|
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)
|
n, u, ok := i.lb.WhoIs(proto, ipp)
|
||||||
if !ok {
|
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 {
|
func (i *jsIPN) queryDNS(name string, queryType int) js.Value {
|
||||||
return makePromise(func() (any, error) {
|
return makePromise(func() (any, error) {
|
||||||
res, resolvers, err := i.lb.QueryDNS(name, dnsmessage.Type(queryType))
|
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
|
var p dnsmessage.Parser
|
||||||
if _, err := p.Start(res); err != nil {
|
if _, err := p.Start(res); err != nil {
|
||||||
return nil, fmt.Errorf("queryDNS: parsing response: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ping: invalid IP %q: %w", ip, err)
|
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)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
pr, err := i.lb.Ping(ctx, addr, tailcfg.PingType(pingType), size)
|
pr, err := i.lb.Ping(ctx, addr, tailcfg.PingType(pingType), size)
|
||||||
|
|||||||
Reference in New Issue
Block a user