From 6fbe8fc4be44ece5a1fe098751529b3bafad2ccf Mon Sep 17 00:00:00 2001 From: Matteo Manfredi Date: Mon, 2 Dec 2024 02:44:20 +0100 Subject: [PATCH 1/3] Update tailscale extension - feat: add \`netcheck\` command - Initial commit --- extensions/tailscale/CHANGELOG.md | 4 + extensions/tailscale/package.json | 10 +- extensions/tailscale/src/netcheck.tsx | 151 ++++++++++++++++++++++++++ extensions/tailscale/src/shared.tsx | 63 +++++++++++ 4 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 extensions/tailscale/src/netcheck.tsx diff --git a/extensions/tailscale/CHANGELOG.md b/extensions/tailscale/CHANGELOG.md index 28707715a25..0c4ec0d25ec 100644 --- a/extensions/tailscale/CHANGELOG.md +++ b/extensions/tailscale/CHANGELOG.md @@ -1,5 +1,9 @@ # Tailscale Changelog +## [Add new features] - 2024-12-02 + +- Add `netcheck` command + ## [Improvement] - 2024-11-08 - Provide UI indicator, and HUD message on `connect (tailscale up)` and `disconnect (tailscale down)` commands diff --git a/extensions/tailscale/package.json b/extensions/tailscale/package.json index e29cc2d4820..7a74ba853fa 100644 --- a/extensions/tailscale/package.json +++ b/extensions/tailscale/package.json @@ -13,7 +13,8 @@ "itsmingjie", "erics118", "brandenw", - "j3lte" + "j3lte", + "itsmatteomanf" ], "categories": [ "Developer Tools", @@ -90,6 +91,13 @@ "subtitle": "Tailscale", "description": "Tailscale down", "mode": "no-view" + }, + { + "name": "netcheck", + "title": "Netcheck", + "subtitle": "Tailscale", + "description": "Tailscale netcheck", + "mode": "view" } ], "preferences": [ diff --git a/extensions/tailscale/src/netcheck.tsx b/extensions/tailscale/src/netcheck.tsx new file mode 100644 index 00000000000..75433f0ef75 --- /dev/null +++ b/extensions/tailscale/src/netcheck.tsx @@ -0,0 +1,151 @@ +import { List, Icon, ActionPanel, Action, Color } from "@raycast/api"; +import { useEffect, useState } from "react"; +import { getErrorDetails, ErrorDetails, getNetcheck, getDerpMap, NetcheckResponse, Derp } from "./shared"; + +const getDerps = (netcheck: NetcheckResponse, setPreferredDERP: (derp: Derp) => void) => { + // the netcheck json response contains a map of region IDs to latencies, so we need to map them to the actual region data + // the pretty printed command does that behind the scenes + const _derpMap = getDerpMap(); + return Object.keys(netcheck.RegionLatency) + .map((key) => { + const _key = parseInt(key); + const derp = { + id: key, + code: _derpMap[_key].RegionCode, + name: _derpMap[_key].RegionName, + latency: netcheck.RegionLatency[key] ? (netcheck.RegionLatency[key] / 1000000).toFixed(1) : undefined, + latencies: { + v4: netcheck.RegionV4Latency[key] ? (netcheck.RegionV4Latency[key] / 1000000).toFixed(1) : undefined, + v6: netcheck.RegionV6Latency[key] ? (netcheck.RegionV6Latency[key] / 1000000).toFixed(1) : undefined, + }, + nodes: _derpMap[_key].Nodes, + }; + netcheck.PreferredDERP === _key && setPreferredDERP(derp); + return derp; + }) + .sort((a, b) => { + if (a.latency && b.latency) return parseFloat(a.latency) - parseFloat(b.latency); + if (a.latency) return 1; + if (b.latency) return -1; + return 0; + }); +}; + +export default function MyDeviceList() { + const [netcheck, setNetcheck] = useState(); + const [portMappings, setPortMappings] = useState([]); + const [derps, setDerps] = useState([]); + const [preferredDERP, setPreferredDERP] = useState(); + const [error, setError] = useState(); + + useEffect(() => { + async function fetch() { + try { + const _netcheck = getNetcheck(); + setDerps(getDerps(_netcheck, setPreferredDERP)); + setNetcheck(_netcheck); + setPortMappings( + ["UPnP", "PMP", "PCP"].reduce((acc, mapping) => { + _netcheck[mapping] && acc.push(mapping); + return acc; + }, [] as string[]), + ); + } catch (error) { + setError(getErrorDetails(error, "Couldn’t load netcheck.")); + } + } + fetch(); + }, []); + + return ( + + {error ? ( + + ) : ( + <> + + + {netcheck?.IPv4 && } + {netcheck?.IPv4 && } + + } + /> + + {netcheck?.IPv6 && } + {netcheck?.IPv6 && } + + } + /> + + + + + {derps.map((derp) => ( + + + + + {derp.latency && } + + } + /> + ))} + + + )} + + ); +} + +export function derpDetails(derp: Derp) { + return ( + + + + + + {derp.latencies.v4 && } + {derp.latencies.v6 && } + + + {derp.nodes?.map((node) => ( + + + {node.IPv4 && } + {node.IPv6 && } + + } + /> + ))} + + + ); +} diff --git a/extensions/tailscale/src/shared.tsx b/extensions/tailscale/src/shared.tsx index 19b973cc631..0f0303142ea 100644 --- a/extensions/tailscale/src/shared.tsx +++ b/extensions/tailscale/src/shared.tsx @@ -60,6 +60,56 @@ export type StatusResponse = { >; }; +/** + * NetcheckResponse are the fields returned by `tailscale netcheck --format json`. + * These are mentioned to not be stable and may change in the future. Doubtful, but possible. + */ +export type NetcheckResponse = { + UDP: boolean; + IPv4: boolean; + GlobalV4: string; + IPv6: boolean; + GlobalV6: string; + MappingVariesByDestIP: boolean; + UPnP: boolean; + PMP: boolean; + PCP: boolean; + PreferredDERP: number; + RegionLatency: Record; + RegionV4Latency: Record; + RegionV6Latency: Record; +}; + +export type DerpRegion = { + RegionId: number; + RegionCode: string; + RegionName: string; + Latitude: number; + Longitude: number; + Nodes: DerpNode[]; +}; + +type DerpNode = { + Name: string; + RegionID: number; + HostName: string; + IPv4: string; + IPv6: string; + CanPort80: boolean; +}; + +export type Derp = { + id: string; + code: string; + name: string; + latency: string | undefined; + latencies: { + v4: string | undefined; + v6: string | undefined; + }; + nodes: DerpNode[]; +}; + export function getStatus(peers = true) { const resp = tailscale(`status --json --peers=${peers}`); const data = JSON.parse(resp) as StatusResponse; @@ -69,6 +119,19 @@ export function getStatus(peers = true) { return data; } +export function getNetcheck() { + const resp = tailscale("netcheck --format json"); + return JSON.parse(resp); +} + +/** + * This funtion relies on a debug command, so it may not be stable on the returned value. + */ +export function getDerpMap() { + const resp = tailscale("debug netmap"); + return JSON.parse(resp).DERPMap.Regions as DerpRegion[]; +} + export function getDevices(status: StatusResponse) { const devices: Device[] = []; const self = status.Self; From 76538103f207ab7b5c8586e2edd64e91349f7298 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Per=20Nielsen=20Tik=C3=A6r?= Date: Thu, 2 Jan 2025 09:25:30 +0100 Subject: [PATCH 2/3] Update CHANGELOG.md --- extensions/tailscale/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/tailscale/CHANGELOG.md b/extensions/tailscale/CHANGELOG.md index 0c4ec0d25ec..319d2b686e4 100644 --- a/extensions/tailscale/CHANGELOG.md +++ b/extensions/tailscale/CHANGELOG.md @@ -1,6 +1,6 @@ # Tailscale Changelog -## [Add new features] - 2024-12-02 +## [Add new features] - {PR_MERGE_DATE} - Add `netcheck` command From e67fff860cad7e0dc1a7bc75157ab95b21a6cc7d Mon Sep 17 00:00:00 2001 From: raycastbot Date: Thu, 2 Jan 2025 08:31:14 +0000 Subject: [PATCH 3/3] Update CHANGELOG.md and optimise images --- extensions/tailscale/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/tailscale/CHANGELOG.md b/extensions/tailscale/CHANGELOG.md index 319d2b686e4..0667584f436 100644 --- a/extensions/tailscale/CHANGELOG.md +++ b/extensions/tailscale/CHANGELOG.md @@ -1,6 +1,6 @@ # Tailscale Changelog -## [Add new features] - {PR_MERGE_DATE} +## [Add new features] - 2025-01-02 - Add `netcheck` command