feat(TALE-36): support exit node enabling/disabling

This commit is contained in:
Aarnav Tale 2024-11-06 16:48:14 -05:00
parent 6e55f442fd
commit e1c87412d4
No known key found for this signature in database
2 changed files with 109 additions and 4 deletions

View File

@ -62,6 +62,24 @@ export async function menuAction(request: ActionFunctionArgs['request']) {
return json({ message: 'Route updated' })
}
case 'exit-node': {
if (!data.has('routes') || !data.has('enabled')) {
return json({ message: 'No route or enabled provided' }, {
status: 400,
})
}
const routes = data.get('routes')?.toString().split(',') ?? []
const enabled = data.get('enabled') === 'true'
const postfix = enabled ? 'enable' : 'disable'
await Promise.all(routes.map(async (route) => {
await post(`v1/routes/${route}/${postfix}`, session.get('hsApiKey')!)
}))
return json({ message: 'Exit node updated' })
}
case 'move': {
if (!data.has('to')) {
return json({ message: 'No destination provided' }, {

View File

@ -1,9 +1,10 @@
import { useFetcher } from '@remix-run/react'
import { type Dispatch, type SetStateAction } from 'react'
import { Dispatch, SetStateAction, useMemo } from 'react'
import Dialog from '~/components/Dialog'
import Switch from '~/components/Switch'
import { type Machine, type Route } from '~/types'
import Link from '~/components/Link'
import { Machine, Route } from '~/types'
import { cn } from '~/utils/cn'
interface RoutesProps {
@ -16,6 +17,22 @@ interface RoutesProps {
export default function Routes({ machine, routes, state }: RoutesProps) {
const fetcher = useFetcher()
// This is much easier with Object.groupBy but it's too new for us
const { exit, subnet } = routes.reduce((acc, route) => {
if (route.prefix === '::/0' || route.prefix === '0.0.0.0/0') {
acc.exit.push(route)
return acc
}
acc.subnet.push(route)
return acc
}, { exit: [], subnet: [] })
const exitEnabled = useMemo(() => {
if (exit.length !== 2) return false
return exit[0].enabled && exit[1].enabled
}, [exit])
return (
<Dialog>
<Dialog.Panel control={state}>
@ -26,9 +43,19 @@ export default function Routes({ machine, routes, state }: RoutesProps) {
{' '}
{machine.givenName}
</Dialog.Title>
<Dialog.Text className="font-bold">
Subnet routes
</Dialog.Text>
<Dialog.Text>
Connect to devices you can&apos;t install Tailscale on
by advertising IP ranges as subnet routes.
{' '}
<Link
to="https://tailscale.com/kb/1019/subnets"
name="Tailscale Subnets Documentation"
>
Learn More
</Link>
</Dialog.Text>
<div className={cn(
'rounded-lg overflow-y-auto my-2',
@ -36,7 +63,7 @@ export default function Routes({ machine, routes, state }: RoutesProps) {
'border border-zinc-200 dark:border-zinc-700',
)}
>
{routes.length === 0
{subnet.length === 0
? (
<div
className={cn(
@ -51,7 +78,7 @@ export default function Routes({ machine, routes, state }: RoutesProps) {
</div>
)
: undefined}
{routes.map(route => (
{subnet.map(route => (
<div
key={route.node.id}
className={cn(
@ -80,6 +107,66 @@ export default function Routes({ machine, routes, state }: RoutesProps) {
</div>
))}
</div>
<Dialog.Text className="font-bold mt-8">
Exit nodes
</Dialog.Text>
<Dialog.Text>
Allow your network to route internet traffic through this machine.
{' '}
<Link
to="https://tailscale.com/kb/1103/exit-nodes"
name="Tailscale Exit-node Documentation"
>
Learn More
</Link>
</Dialog.Text>
<div className={cn(
'rounded-lg overflow-y-auto my-2',
'divide-y divide-zinc-200 dark:divide-zinc-700 align-top',
'border border-zinc-200 dark:border-zinc-700',
)}
>
{exit.length === 0
? (
<div
className={cn(
'flex py-4 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-center',
'text-ui-600 dark:text-ui-300',
)}
>
<p>
This machine is not an exit node.
</p>
</div>
) : (
<div
className={cn(
'flex py-2 px-4 bg-ui-100 dark:bg-ui-800',
'items-center justify-between',
)}
>
<p>
Use as exit node
</p>
<Switch
defaultSelected={exitEnabled}
label="Enabled"
onChange={(checked) => {
const form = new FormData()
form.set('id', machine.id)
form.set('_method', 'exit-node')
form.set('routes', exit.map(route => route.id).join(','))
form.set('enabled', String(checked))
fetcher.submit(form, {
method: 'POST',
})
}}
/>
</div>
)}
</div>
<div className="mt-6 flex justify-end gap-2 mt-6">
<Dialog.Action
variant="cancel"