feat: reintroduce missing local dns override (fixes #236)

This commit is contained in:
Aarnav Tale 2025-06-21 13:53:19 -04:00
parent 8819af270d
commit 87b8d64bcc
No known key found for this signature in database
6 changed files with 88 additions and 6 deletions

View File

@ -8,6 +8,7 @@
- The docker container now runs in a non-root, distroless image (closes [#255](https://github.com/tale/headplane/issues/255)).
- You may need to run `chown -R 65532:65532 <host_path>` on your data directory to ensure the container can write to it.
- Removing a Split DNS record will no longer make the split domain unresolvable by clients (closes [#231](https://github.com/tale/headplane/issues/231)).
- Reintroduce the toggle for overriding local DNS settings in the Headscale config (closes [#236](https://github.com/tale/headplane/issues/236)).
### 0.6.0 (May 25, 2025)
- Headplane 0.6.0 now requires **Headscale 0.26.0** or newer.

View File

@ -11,6 +11,7 @@ import cn from '~/utils/cn';
export interface SwitchProps extends AriaSwitchProps {
label: string;
className?: string;
switchClassName?: string;
}
export default function Switch(props: SwitchProps) {
@ -45,6 +46,7 @@ export default function Switch(props: SwitchProps) {
state.isSelected && 'bg-headplane-900 dark:bg-headplane-950',
isFocusVisible && 'ring-2',
props.isDisabled && 'opacity-50',
props.className,
)}
>
<span
@ -53,6 +55,7 @@ export default function Switch(props: SwitchProps) {
'bg-white transition duration-50 ease-in-out',
'translate-x-0 group-selected:translate-x-full',
state.isSelected && 'translate-x-full',
props.switchClassName,
)}
/>
</div>

View File

@ -1,16 +1,24 @@
import { Form } from 'react-router';
import { Info } from 'lucide-react';
import { Form, useSubmit } from 'react-router';
import Button from '~/components/Button';
import Link from '~/components/Link';
import Switch from '~/components/Switch';
import TableList from '~/components/TableList';
import Tooltip from '~/components/Tooltip';
import cn from '~/utils/cn';
import AddNS from '../dialogs/add-ns';
interface Props {
nameservers: Record<string, string[]>;
overrideLocalDns: boolean;
isDisabled: boolean;
}
export default function ManageNS({ nameservers, isDisabled }: Props) {
export default function ManageNS({
nameservers,
isDisabled,
overrideLocalDns,
}: Props) {
return (
<div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Nameservers</h1>
@ -31,6 +39,7 @@ export default function ManageNS({ nameservers, isDisabled }: Props) {
isGlobal={key === 'global'}
isDisabled={isDisabled}
nameservers={nameservers}
overrideLocalDns={overrideLocalDns}
name={key}
/>
))}
@ -45,6 +54,7 @@ interface ListProps {
isGlobal: boolean;
isDisabled: boolean;
nameservers: Record<string, string[]>;
overrideLocalDns: boolean;
name: string;
}
@ -52,6 +62,7 @@ function NameserverList({
isGlobal,
isDisabled,
nameservers,
overrideLocalDns,
name,
}: ListProps) {
const list = isGlobal ? nameservers.global : nameservers[name];
@ -59,12 +70,54 @@ function NameserverList({
return null;
}
const submit = useSubmit();
return (
<div className="mb-8">
<div className="flex items-center justify-between mb-2">
<h2 className="text-md font-medium opacity-80">
{isGlobal ? 'Global Nameservers' : name}
</h2>
{isGlobal ? (
<div className="flex items-center justify-between w-full">
<h2 className="text-md font-medium opacity-80">
Global Nameservers
</h2>
<div className="flex items-center gap-2 text-sm">
<Tooltip>
<Info className="size-4" />
<Tooltip.Body>
When enabled, use the DNS servers listed below to resolve
names outside the tailnet. When disabled (default), devices
will prefer their local DNS configuration.
<Link
to="https://tailscale.com/kb/1054/dns#global-nameservers"
name="Tailscale Global Nameservers Documentation"
>
Learn More
</Link>
</Tooltip.Body>
</Tooltip>
<p>Override DNS servers</p>
<Switch
label="Override local DNS settings"
className="h-[15px] w-[23px] p-[2px]"
switchClassName="h-[9px] w-[9px]"
name="override_dns"
defaultSelected={overrideLocalDns}
onChange={(v) => {
submit(
{
action_id: 'override_dns',
override_dns: v ? 'true' : 'false',
},
{
method: 'POST',
},
);
}}
/>
</div>
</div>
) : (
<h2 className="text-md font-medium opacity-80">{name}</h2>
)}
</div>
<TableList>
{list.length > 0

View File

@ -42,6 +42,8 @@ export async function dnsAction({
return removeRecord(formData, context);
case 'add_record':
return addRecord(formData, context);
case 'override_dns':
return overrideDns(formData, context);
default:
return data({ success: false }, 400);
}
@ -230,3 +232,20 @@ async function addRecord(formData: FormData, context: LoadContext) {
await context.integration?.onConfigChange(context.client);
}
async function overrideDns(formData: FormData, context: LoadContext) {
const override = formData.get('override_dns')?.toString();
if (!override) {
return data({ success: false }, 400);
}
const overrideValue = override === 'true';
await context.hs.patch([
{
path: 'dns.override_local_dns',
value: overrideValue,
},
]);
await context.integration?.onConfigChange(context.client);
}

View File

@ -44,6 +44,7 @@ export async function loader({
nameservers: config.dns.nameservers.global,
splitDns: config.dns.nameservers.split,
searchDomains: config.dns.search_domains,
overrideDns: config.dns.override_local_dns,
extraRecords: context.hs.d,
};
@ -84,7 +85,11 @@ export default function Page() {
</Notice>
)}
<RenameTailnet name={data.baseDomain} isDisabled={isDisabled} />
<ManageNS nameservers={allNs} isDisabled={isDisabled} />
<ManageNS
nameservers={allNs}
isDisabled={isDisabled}
overrideLocalDns={data.overrideDns}
/>
<ManageRecords records={data.extraRecords} isDisabled={isDisabled} />
<ManageDomains
searchDomains={data.searchDomains}

View File

@ -103,6 +103,7 @@ export const headscaleConfig = type({
dns: {
magic_dns: goBool.default(true),
base_domain: 'string = "headscale.net"',
override_local_dns: goBool.default(false),
nameservers: type({
global: type('string[]').default(() => []),
split: type('Record<string, string[]>').default(() => ({})),