feat: pull local endpoints/addresses from host info

This commit is contained in:
Aarnav Tale
2026-04-11 12:24:58 -04:00
parent 93be180479
commit fccd2eefc4
6 changed files with 195 additions and 151 deletions
+18 -12
View File
@@ -14,18 +14,18 @@ export interface AttributeProps {
export default function Attribute({ name, value, tooltip, isCopyable }: AttributeProps) {
return (
<dl className="flex items-center gap-1 text-sm">
<dl className="group/attr flex items-baseline gap-1 text-sm">
<dt
className={cn(
"w-1/3 sm:w-1/4 lg:w-1/3 shrink-0 min-w-0",
"text-mist-500 dark:text-mist-400",
tooltip ? "flex items-center gap-1" : undefined,
"text-mist-600 dark:text-mist-300",
tooltip ? "flex items-baseline gap-1" : undefined,
)}
>
{name}
{tooltip ? (
<Tooltip content={tooltip}>
<Info className="size-4" />
<Info className="size-3.5 translate-y-0.5 opacity-40 transition-opacity hover:opacity-100" />
</Tooltip>
) : undefined}
</dt>
@@ -64,16 +64,22 @@ export default function Attribute({ name, value, tooltip, isCopyable }: Attribut
<div suppressHydrationWarning className="truncate">
{value}
</div>
{isCopyable ? (
<div>
<Check className="hidden size-4 data-copied:block" />
<Copy className="block size-4 data-copied:hidden" />
</div>
) : undefined}
<div className="opacity-0 transition-opacity group-hover/attr:opacity-100">
<Check className="hidden size-3.5 data-copied:block" />
<Copy className="block size-3.5 data-copied:hidden" />
</div>
</button>
) : (
<div className="relative min-w-0 truncate" suppressHydrationWarning>
{value}
<div className="relative min-w-0" suppressHydrationWarning>
{value.includes("\n") ? (
value.split("\n").map((line) => (
<div key={line} className="truncate">
{line}
</div>
))
) : (
<div className="truncate">{value}</div>
)}
</div>
)}
</dd>
+1 -1
View File
@@ -7,7 +7,7 @@ import ToastProvider from "~/utils/toast-provider";
import type { Route } from "./+types/root";
import { ErrorBanner } from "./components/error-banner";
import "@fontsource-variable/inter/wght.css";
import "@fontsource-variable/inter/opsz.css";
import "./tailwind.css";
export const meta: MetaFunction = () => [
+9 -2
View File
@@ -288,7 +288,9 @@ export default function Page({
) : undefined}
</div>
<div className="flex flex-col gap-1">
<p className="text-sm font-semibold uppercase opacity-75">Addresses</p>
<p className="text-sm font-semibold text-mist-600 uppercase dark:text-mist-300">
Addresses
</p>
<Attribute
isCopyable
name="Tailscale IPv4"
@@ -315,9 +317,14 @@ export default function Page({
value={`${node.givenName}.${magic}`}
/>
) : undefined}
{stats?.Endpoints ? (
<Attribute name="Endpoints" value={stats?.Endpoints?.join("\n") ?? "—"} />
) : undefined}
{stats ? (
<>
<p className="mt-4 text-sm font-semibold uppercase opacity-75">Client Connectivity</p>
<p className="mt-4 text-sm font-semibold text-mist-600 uppercase dark:text-mist-300">
Client Connectivity
</p>
<Attribute
name="Varies"
tooltip="Whether the machine is behind a difficult NAT that varies the machines IP address depending on the destination."
+6
View File
@@ -55,6 +55,12 @@
}
}
body {
font-optical-sizing: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
@supports (scrollbar-gutter: stable) {
html {
scrollbar-gutter: stable;
+141 -135
View File
@@ -3,209 +3,215 @@
// https://github.com/tailscale/tailscale/blob/main/tailcfg/tailcfg.go#L816
export interface HostInfo {
/**
* Custom identifier we use to determine if its an agent or not
*/
HeadplaneAgent?: boolean;
/**
* Custom identifier we use to determine if its an agent or not
*/
HeadplaneAgent?: boolean;
/** Version of this code (in version.Long format) */
IPNVersion?: string;
/** Version of this code (in version.Long format) */
IPNVersion?: string;
/** Logtail ID of frontend instance */
FrontendLogID?: string;
/** Logtail ID of frontend instance */
FrontendLogID?: string;
/** Logtail ID of backend instance */
BackendLogID?: string;
/** Logtail ID of backend instance */
BackendLogID?: string;
/** Operating system the client runs on (a version.OS value) */
OS?: string;
/** Operating system the client runs on (a version.OS value) */
OS?: string;
/**
* Version of the OS, if available.
*
* - Android: "10", "11", "12", etc.
* - iOS/macOS: "15.6.1", "12.4.0", etc.
* - Windows: "10.0.19044.1889", etc.
* - FreeBSD: "12.3-STABLE", etc.
* - Linux (pre-1.32): "Debian 10.4; kernel=xxx; container; env=kn"
* - Linux (1.32+): Kernel version, e.g., "5.10.0-17-amd64".
*/
OSVersion?: string;
/**
* Version of the OS, if available.
*
* - Android: "10", "11", "12", etc.
* - iOS/macOS: "15.6.1", "12.4.0", etc.
* - Windows: "10.0.19044.1889", etc.
* - FreeBSD: "12.3-STABLE", etc.
* - Linux (pre-1.32): "Debian 10.4; kernel=xxx; container; env=kn"
* - Linux (1.32+): Kernel version, e.g., "5.10.0-17-amd64".
*/
OSVersion?: string;
/** Whether the client is running in a container (best-effort detection) */
Container?: boolean;
/** Whether the client is running in a container (best-effort detection) */
Container?: boolean;
/** Host environment type as a string */
Env?: string;
/** Host environment type as a string */
Env?: string;
/** Distribution name (e.g., "debian", "ubuntu", "nixos") */
Distro?: string;
/** Distribution name (e.g., "debian", "ubuntu", "nixos") */
Distro?: string;
/** Distribution version (e.g., "20.04") */
DistroVersion?: string;
/** Distribution version (e.g., "20.04") */
DistroVersion?: string;
/** Distribution code name (e.g., "jammy", "bullseye") */
DistroCodeName?: string;
/** Distribution code name (e.g., "jammy", "bullseye") */
DistroCodeName?: string;
/** Used to disambiguate Tailscale clients that run using tsnet */
App?: string;
/** Used to disambiguate Tailscale clients that run using tsnet */
App?: string;
/** Whether a desktop was detected on Linux */
Desktop?: boolean;
/** Whether a desktop was detected on Linux */
Desktop?: boolean;
/** Tailscale package identifier ("choco", "appstore", etc.; empty if unknown) */
Package?: string;
/** Tailscale package identifier ("choco", "appstore", etc.; empty if unknown) */
Package?: string;
/** Mobile phone model (e.g., "Pixel 3a", "iPhone12,3") */
DeviceModel?: string;
/** Mobile phone model (e.g., "Pixel 3a", "iPhone12,3") */
DeviceModel?: string;
/** macOS/iOS APNs device token for notifications (future support for Android) */
PushDeviceToken?: string;
/** macOS/iOS APNs device token for notifications (future support for Android) */
PushDeviceToken?: string;
/** Name of the host the client runs on */
Hostname?: string;
/** Name of the host the client runs on */
Hostname?: string;
/** Indicates whether the host is blocking incoming connections */
ShieldsUp?: boolean;
/** Indicates whether the host is blocking incoming connections */
ShieldsUp?: boolean;
/** Indicates this node exists in netmap because it's owned by a shared-to user */
ShareeNode?: boolean;
/** Indicates this node exists in netmap because it's owned by a shared-to user */
ShareeNode?: boolean;
/** Indicates user has opted out of sending logs and support */
NoLogsNoSupport?: boolean;
/** Indicates user has opted out of sending logs and support */
NoLogsNoSupport?: boolean;
/** Indicates the node wants the option to receive ingress connections */
WireIngress?: boolean;
/** Indicates the node wants the option to receive ingress connections */
WireIngress?: boolean;
/** Indicates node has opted-in to admin-console-driven remote updates */
AllowsUpdate?: boolean;
/** Indicates node has opted-in to admin-console-driven remote updates */
AllowsUpdate?: boolean;
/** Current host's machine type (e.g., uname -m) */
Machine?: string;
/** Current host's machine type (e.g., uname -m) */
Machine?: string;
/** `GOARCH` value of the built binary */
GoArch?: string;
/** `GOARCH` value of the built binary */
GoArch?: string;
/** Architecture variant (e.g., GOARM, GOAMD64) of the built binary */
GoArchVar?: string;
/** Architecture variant (e.g., GOARM, GOAMD64) of the built binary */
GoArchVar?: string;
/** Go version the binary was built with */
GoVersion?: string;
/** Go version the binary was built with */
GoVersion?: string;
/** Set of IP ranges this client can route */
RoutableIPs?: string[];
/** Set of IP ranges this client can route */
RoutableIPs?: string[];
/** Set of ACL tags this node wants to claim */
RequestTags?: string[];
/** Set of ACL tags this node wants to claim */
RequestTags?: string[];
/** MAC addresses to send Wake-on-LAN packets to wake this node */
WoLMACs?: string[];
/** MAC addresses to send Wake-on-LAN packets to wake this node */
WoLMACs?: string[];
/** Services advertised by this machine */
Services?: Service[];
/** Services advertised by this machine */
Services?: Service[];
/** Networking information about the node */
NetInfo?: NetInfo;
/** Networking information about the node */
NetInfo?: NetInfo;
/** SSH host keys if advertised */
sshHostKeys?: string[];
/** SSH host keys if advertised */
sshHostKeys?: string[];
/** Cloud provider information (if any) */
Cloud?: string;
/** Cloud provider information (if any) */
Cloud?: string;
/** Indicates if the client is running in userspace (netstack) mode */
Userspace?: boolean;
/** Indicates if the client is running in userspace (netstack) mode */
Userspace?: boolean;
/** Indicates if the client's subnet router is running in userspace (netstack) mode */
UserspaceRouter?: boolean;
/** Indicates if the client's subnet router is running in userspace (netstack) mode */
UserspaceRouter?: boolean;
/** Indicates if the client is running the app-connector service */
AppConnector?: boolean;
/** Indicates if the client is running the app-connector service */
AppConnector?: boolean;
/** Opaque hash of the most recent list of tailnet services (indicates config updates) */
ServicesHash?: string;
/** WireGuard endpoints (public IP:port pairs) from the network map */
Endpoints?: string[];
/** Geographical location data about the Tailscale host (optional) */
Location?: Location;
/** Home DERP region ID */
HomeDERP?: number;
/** Opaque hash of the most recent list of tailnet services (indicates config updates) */
ServicesHash?: string;
/** Geographical location data about the Tailscale host (optional) */
Location?: Location;
}
/** Represents a network service advertised by a node */
interface Service {
/** Protocol type (e.g., "tcp", "udp", "peerapi4") */
Proto: string;
/** Protocol type (e.g., "tcp", "udp", "peerapi4") */
Proto: string;
/** Port number */
Port: number;
/** Port number */
Port: number;
/** Textual description of the service (usually the process name) */
Description?: string;
/** Textual description of the service (usually the process name) */
Description?: string;
}
/** Networking information for a Tailscale node */
interface NetInfo {
/** Indicates if NAT mappings vary based on destination IP */
MappingVariesByDestIP?: boolean;
/** Indicates if NAT mappings vary based on destination IP */
MappingVariesByDestIP?: boolean;
/** Indicates if the router supports hairpinning */
HairPinning?: boolean;
/** Indicates if the router supports hairpinning */
HairPinning?: boolean;
/** Indicates if the host has IPv6 internet connectivity */
WorkingIPv6?: boolean;
/** Indicates if the host has IPv6 internet connectivity */
WorkingIPv6?: boolean;
/** Indicates if the OS supports IPv6 */
OSHasIPv6?: boolean;
/** Indicates if the OS supports IPv6 */
OSHasIPv6?: boolean;
/** Indicates if the host has UDP internet connectivity */
WorkingUDP?: boolean;
/** Indicates if the host has UDP internet connectivity */
WorkingUDP?: boolean;
/** Indicates if ICMPv4 works (empty if not checked) */
WorkingICMPv4?: boolean;
/** Indicates if ICMPv4 works (empty if not checked) */
WorkingICMPv4?: boolean;
/** Indicates if there is an existing portmap open (UPnP, PMP, PCP) */
HavePortMap?: boolean;
/** Indicates if there is an existing portmap open (UPnP, PMP, PCP) */
HavePortMap?: boolean;
/** Indicates if UPnP appears present on the LAN (empty if not checked) */
UPnP?: boolean;
/** Indicates if UPnP appears present on the LAN (empty if not checked) */
UPnP?: boolean;
/** Indicates if NAT-PMP appears present on the LAN (empty if not checked) */
PMP?: boolean;
/** Indicates if NAT-PMP appears present on the LAN (empty if not checked) */
PMP?: boolean;
/** Indicates if PCP appears present on the LAN (empty if not checked) */
PCP?: boolean;
/** Indicates if PCP appears present on the LAN (empty if not checked) */
PCP?: boolean;
/** Preferred DERP region ID */
PreferredDERP?: number;
/** Preferred DERP region ID */
PreferredDERP?: number;
/** Current link type ("wired", "wifi", "mobile") */
LinkType?: string;
/** Current link type ("wired", "wifi", "mobile") */
LinkType?: string;
/** Fastest recent time to reach various DERP STUN servers (seconds) */
DERPLatency?: Record<string, number>;
/** Fastest recent time to reach various DERP STUN servers (seconds) */
DERPLatency?: Record<string, number>;
/** Firewall mode on Linux-specific configurations */
FirewallMode?: string;
/** Firewall mode on Linux-specific configurations */
FirewallMode?: string;
}
/** Represents the geographical location of a Tailscale host */
interface Location {
/** Country name (user-friendly, properly capitalized) */
Country?: string;
/** Country name (user-friendly, properly capitalized) */
Country?: string;
/** ISO 3166-1 alpha-2 country code (upper case) */
CountryCode?: string;
/** ISO 3166-1 alpha-2 country code (upper case) */
CountryCode?: string;
/** City name (user-friendly, properly capitalized) */
City?: string;
/** City name (user-friendly, properly capitalized) */
City?: string;
/** City code to disambiguate between cities (e.g., IATA, ICAO, ISO 3166-2) */
CityCode?: string;
/** City code to disambiguate between cities (e.g., IATA, ICAO, ISO 3166-2) */
CityCode?: string;
/** Latitude of the node (in degrees, optional) */
Latitude?: number;
/** Latitude of the node (in degrees, optional) */
Latitude?: number;
/** Longitude of the node (in degrees, optional) */
Longitude?: number;
/** Longitude of the node (in degrees, optional) */
Longitude?: number;
/** Priority for exit node selection (0 means no priority, negative not allowed) */
Priority?: number;
/** Priority for exit node selection (0 means no priority, negative not allowed) */
Priority?: number;
}
+20 -1
View File
@@ -133,11 +133,30 @@ func (s *TSAgent) FetchAllHostInfo(ctx context.Context) (map[string]json.RawMess
return
}
data, err := json.Marshal(whois.Node.Hostinfo)
// Merge hostinfo with connection status from PeerStatus
var merged map[string]any
raw, err := json.Marshal(whois.Node.Hostinfo)
if err != nil {
log.Debug("Failed to marshal hostinfo for %s (%s): %s", nodeID, ip, err)
return
}
if err := json.Unmarshal(raw, &merged); err != nil {
log.Debug("Failed to unmarshal hostinfo for %s (%s): %s", nodeID, ip, err)
return
}
endpoints := make([]string, len(whois.Node.Endpoints))
for i, ep := range whois.Node.Endpoints {
endpoints[i] = ep.String()
}
merged["Endpoints"] = endpoints
merged["HomeDERP"] = whois.Node.HomeDERP
data, err := json.Marshal(merged)
if err != nil {
log.Debug("Failed to marshal merged info for %s (%s): %s", nodeID, ip, err)
return
}
mu.Lock()
result[nodeID] = json.RawMessage(data)