mirror of
https://github.com/tale/headplane.git
synced 2026-07-26 07:48:14 +00:00
feat: pull local endpoints/addresses from host info
This commit is contained in:
@@ -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
@@ -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 = () => [
|
||||
|
||||
@@ -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 machine’s IP address depending on the destination."
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user