feat: support 0.26+ routes only

This commit is contained in:
Aarnav Tale
2025-05-16 11:05:20 -04:00
parent 59b32c0842
commit eda8ed0b3e
7 changed files with 78 additions and 62 deletions
+5 -5
View File
@@ -46,16 +46,16 @@ export default function Routes({ node, isOpen, setIsOpen }: RoutesProps) {
</TableList.Item> </TableList.Item>
) : undefined} ) : undefined}
{subnets.map((route) => ( {subnets.map((route) => (
<TableList.Item key={route.id}> <TableList.Item key={route}>
<p>{route.prefix}</p> <p>{route}</p>
<Switch <Switch
defaultSelected={route.enabled} defaultSelected={node.approvedRoutes.includes(route)}
label="Enabled" label="Enabled"
onChange={(checked) => { onChange={(checked) => {
const form = new FormData(); const form = new FormData();
form.set('action_id', 'update_routes'); form.set('action_id', 'update_routes');
form.set('node_id', node.id); form.set('node_id', node.id);
form.set('routes', [route.id].join(',')); form.set('routes', [route].join(','));
form.set('enabled', String(checked)); form.set('enabled', String(checked));
fetcher.submit(form, { fetcher.submit(form, {
@@ -95,7 +95,7 @@ export default function Routes({ node, isOpen, setIsOpen }: RoutesProps) {
form.set( form.set(
'routes', 'routes',
node.customRouting.exitRoutes node.customRouting.exitRoutes
.map((route) => route.id) .map((route) => route)
.join(','), .join(','),
); );
+32 -6
View File
@@ -188,6 +188,12 @@ async function updateRoutes(
nodeId: string, nodeId: string,
context: LoadContext, context: LoadContext,
) { ) {
const { node } = await context.client.get<{ node: Machine }>(
`v1/node/${nodeId}`,
apiKey,
);
const newApproved = node.approvedRoutes;
const routes = formData.get('routes')?.toString(); const routes = formData.get('routes')?.toString();
if (!routes) { if (!routes) {
throw data('Missing `routes` in the form data.', { throw data('Missing `routes` in the form data.', {
@@ -209,12 +215,32 @@ async function updateRoutes(
}); });
} }
const postfix = enabled === 'true' ? 'enable' : 'disable'; if (enabled === 'true') {
await Promise.all( for (const route of allRoutes) {
allRoutes.map(async (route) => { // If already approved, skip, otherwise add to approved
await context.client.post(`v1/routes/${route}/${postfix}`, apiKey); if (newApproved.includes(route)) {
}), continue;
); }
newApproved.push(route);
}
} else {
for (const route of allRoutes) {
// If not approved, skip, otherwise remove from approved
if (!newApproved.includes(route)) {
continue;
}
const index = newApproved.indexOf(route);
if (index > -1) {
newApproved.splice(index, 1);
}
}
}
await context.client.post(`v1/node/${nodeId}/approve_routes`, apiKey, {
routes: newApproved,
});
return { message: 'Routes updated' }; return { message: 'Routes updated' };
} }
+6 -10
View File
@@ -11,7 +11,7 @@ import Link from '~/components/Link';
import StatusCircle from '~/components/StatusCircle'; import StatusCircle from '~/components/StatusCircle';
import Tooltip from '~/components/Tooltip'; import Tooltip from '~/components/Tooltip';
import type { LoadContext } from '~/server'; import type { LoadContext } from '~/server';
import type { Machine, Route, User } from '~/types'; import type { Machine, User } from '~/types';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
import { getOSInfo, getTSVersion } from '~/utils/host-info'; import { getOSInfo, getTSVersion } from '~/utils/host-info';
import { mapNodes } from '~/utils/node-info'; import { mapNodes } from '~/utils/node-info';
@@ -37,20 +37,16 @@ export async function loader({
} }
} }
const [machine, { routes }, { users }] = await Promise.all([ const [machine, { users }] = await Promise.all([
context.client.get<{ node: Machine }>( context.client.get<{ node: Machine }>(
`v1/node/${params.id}`, `v1/node/${params.id}`,
session.get('api_key')!, session.get('api_key')!,
), ),
context.client.get<{ routes: Route[] }>(
'v1/routes',
session.get('api_key')!,
),
context.client.get<{ users: User[] }>('v1/user', session.get('api_key')!), context.client.get<{ users: User[] }>('v1/user', session.get('api_key')!),
]); ]);
const [node] = mapNodes([machine.node], routes); const lookup = await context.agents?.lookup([machine.node.nodeKey]);
const lookup = await context.agents?.lookup([node.nodeKey]); const [node] = mapNodes([machine.node], lookup);
return { return {
node, node,
@@ -160,7 +156,7 @@ export default function Page() {
) : ( ) : (
<ul className="leading-normal"> <ul className="leading-normal">
{node.customRouting.subnetApprovedRoutes.map((route) => ( {node.customRouting.subnetApprovedRoutes.map((route) => (
<li key={route.id}>{route.prefix}</li> <li key={route}>{route}</li>
))} ))}
</ul> </ul>
)} )}
@@ -192,7 +188,7 @@ export default function Page() {
) : ( ) : (
<ul className="leading-normal"> <ul className="leading-normal">
{node.customRouting.subnetWaitingRoutes.map((route) => ( {node.customRouting.subnetWaitingRoutes.map((route) => (
<li key={route.id}>{route.prefix}</li> <li key={route}>{route}</li>
))} ))}
</ul> </ul>
)} )}
+3 -9
View File
@@ -2,12 +2,11 @@ import { InfoIcon } from '@primer/octicons-react';
import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router'; import type { ActionFunctionArgs, LoaderFunctionArgs } from 'react-router';
import { useLoaderData } from 'react-router'; import { useLoaderData } from 'react-router';
import Code from '~/components/Code'; import Code from '~/components/Code';
import { ErrorPopup } from '~/components/Error';
import Link from '~/components/Link'; import Link from '~/components/Link';
import Tooltip from '~/components/Tooltip'; import Tooltip from '~/components/Tooltip';
import type { LoadContext } from '~/server'; import type { LoadContext } from '~/server';
import { Capabilities } from '~/server/web/roles'; import { Capabilities } from '~/server/web/roles';
import type { Machine, Route, User } from '~/types'; import type { Machine, User } from '~/types';
import cn from '~/utils/cn'; import cn from '~/utils/cn';
import { mapNodes } from '~/utils/node-info'; import { mapNodes } from '~/utils/node-info';
import MachineRow from './components/machine-row'; import MachineRow from './components/machine-row';
@@ -41,15 +40,11 @@ export async function loader({
Capabilities.write_machines, Capabilities.write_machines,
); );
const [{ nodes }, { routes }, { users }] = await Promise.all([ const [{ nodes }, { users }] = await Promise.all([
context.client.get<{ nodes: Machine[] }>( context.client.get<{ nodes: Machine[] }>(
'v1/node', 'v1/node',
session.get('api_key')!, session.get('api_key')!,
), ),
context.client.get<{ routes: Route[] }>(
'v1/routes',
session.get('api_key')!,
),
context.client.get<{ users: User[] }>('v1/user', session.get('api_key')!), context.client.get<{ users: User[] }>('v1/user', session.get('api_key')!),
]); ]);
@@ -61,12 +56,11 @@ export async function loader({
} }
const stats = await context.agents?.lookup(nodes.map((node) => node.nodeKey)); const stats = await context.agents?.lookup(nodes.map((node) => node.nodeKey));
const populatedNodes = mapNodes(nodes, routes, stats); const populatedNodes = mapNodes(nodes, stats);
return { return {
populatedNodes, populatedNodes,
nodes, nodes,
routes,
users, users,
magic, magic,
server: context.config.headscale.url, server: context.config.headscale.url,
+5
View File
@@ -26,4 +26,9 @@ export interface Machine {
validTags: string[]; validTags: string[];
givenName: string; givenName: string;
online: boolean; online: boolean;
// Added in Headscale 0.26+
approvedRoutes: string[];
availableRoutes: string[];
subnetRoutes: string[];
} }
+25 -31
View File
@@ -1,52 +1,46 @@
import { HostInfo, Machine, Route } from '~/types'; import { HostInfo, Machine, Route } from '~/types';
export interface PopulatedNode extends Machine { export interface PopulatedNode extends Machine {
routes: Route[]; routes: string[];
hostInfo?: HostInfo; hostInfo?: HostInfo;
expired: boolean; expired: boolean;
customRouting: { customRouting: {
exitRoutes: Route[]; exitRoutes: string[];
exitApproved: boolean; exitApproved: boolean;
subnetApprovedRoutes: Route[]; subnetApprovedRoutes: string[];
subnetWaitingRoutes: Route[]; subnetWaitingRoutes: string[];
}; };
} }
export function mapNodes( export function mapNodes(
nodes: Machine[], nodes: Machine[],
routes: Route[],
stats?: Record<string, HostInfo> | undefined, stats?: Record<string, HostInfo> | undefined,
): PopulatedNode[] { ): PopulatedNode[] {
return nodes.map((node) => { return nodes.map((node) => {
const nodeRoutes = routes.filter((route) => route.node.id === node.id); const customRouting = {
const customRouting = nodeRoutes.reduce<PopulatedNode['customRouting']>( exitRoutes: node.availableRoutes.filter(
(acc, route) => { (route) => route === '::/0' || route === '0.0.0.0/0',
if (route.prefix === '::/0' || route.prefix === '0.0.0.0/0') { ),
acc.exitRoutes.push(route); exitApproved: node.approvedRoutes.some(
if (route.enabled) { (route) => route === '::/0' || route === '0.0.0.0/0',
acc.exitApproved = true; ),
} subnetApprovedRoutes: node.approvedRoutes.filter(
} else { (route) =>
if (route.enabled) { route !== '::/0' &&
acc.subnetApprovedRoutes.push(route); route !== '0.0.0.0/0' &&
} else { node.availableRoutes.includes(route),
acc.subnetWaitingRoutes.push(route); ),
} subnetWaitingRoutes: node.availableRoutes.filter(
} (route) =>
route !== '::/0' &&
return acc; route !== '0.0.0.0/0' &&
}, !node.approvedRoutes.includes(route),
{ ),
exitRoutes: [], } satisfies PopulatedNode['customRouting'];
exitApproved: false,
subnetApprovedRoutes: [],
subnetWaitingRoutes: [],
},
);
return { return {
...node, ...node,
routes: nodeRoutes, routes: Array.from(new Set(node.availableRoutes)),
hostInfo: stats?.[node.nodeKey], hostInfo: stats?.[node.nodeKey],
customRouting, customRouting,
expired: expired:
+2 -1
View File
@@ -8,7 +8,7 @@ networks:
driver: "bridge" driver: "bridge"
services: services:
headscale: headscale:
image: "headscale/headscale:0.25.1" image: "headscale/headscale:0.26.0-debug"
container_name: "headscale" container_name: "headscale"
labels: labels:
me.tale.headplane.target: headscale me.tale.headplane.target: headscale
@@ -21,5 +21,6 @@ services:
- "./test:/etc/headscale" - "./test:/etc/headscale"
ports: ports:
- "8080:8080" - "8080:8080"
- "9090:9090"
environment: environment:
TZ: "America/New_York" TZ: "America/New_York"