feat(TALE-30): add support for new DNS configs

This is a breaking change to support 0.23-beta2
This commit is contained in:
Aarnav Tale
2024-08-22 16:55:05 -04:00
parent b8999161a2
commit 5a46fd0a97
13 changed files with 98 additions and 68 deletions
+1 -1
View File
@@ -47,7 +47,7 @@ export default function AddDNS({ records }: Props) {
setIp('') setIp('')
submit({ submit({
'dns_config.extra_records': [ 'dns.extra_records': [
...records, ...records,
{ {
name, name,
@@ -55,7 +55,7 @@ export default function AddNameserver({ nameservers }: Props) {
} }
submit({ submit({
'dns_config.restricted_nameservers': splitNs, 'dns.nameservers.split': splitNs,
}, { }, {
method: 'PATCH', method: 'PATCH',
encType: 'application/json', encType: 'application/json',
@@ -65,7 +65,7 @@ export default function AddNameserver({ nameservers }: Props) {
globalNs.push(ns) globalNs.push(ns)
submit({ submit({
'dns_config.nameservers': globalNs, 'dns.nameservers.global': globalNs,
}, { }, {
method: 'PATCH', method: 'PATCH',
encType: 'application/json', encType: 'application/json',
+1 -1
View File
@@ -63,7 +63,7 @@ export default function DNS({ records, isDisabled }: Props) {
isDisabled={isDisabled} isDisabled={isDisabled}
onPress={() => { onPress={() => {
submit({ submit({
'dns_config.extra_records': records 'dns.extra_records': records
.filter((_, i) => i !== index), .filter((_, i) => i !== index),
}, { }, {
method: 'PATCH', method: 'PATCH',
+2 -3
View File
@@ -134,7 +134,7 @@ export default function Domains({ baseDomain, searchDomains, disabled }: Propert
onPress={() => { onPress={() => {
fetcher.submit({ fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
'dns_config.domains': [...localDomains, newDomain] 'dns.search_domains': [...localDomains, newDomain]
}, { }, {
method: 'PATCH', method: 'PATCH',
encType: 'application/json' encType: 'application/json'
@@ -212,8 +212,7 @@ function Domain({ domain, id, localDomains, isDrag, disabled, fetcher }: DomainP
isDisabled={disabled} isDisabled={disabled}
onPress={() => { onPress={() => {
fetcher.submit({ fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention 'dns.search_domains': localDomains.filter((_, index) => index !== id - 1)
'dns_config.domains': localDomains.filter((_, index) => index !== id - 1)
}, { }, {
method: 'PATCH', method: 'PATCH',
encType: 'application/json' encType: 'application/json'
+1 -1
View File
@@ -42,7 +42,7 @@ export default function Modal({ isEnabled, disabled }: Properties) {
onPress={() => { onPress={() => {
fetcher.submit({ fetcher.submit({
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
'dns_config.magic_dns': !isEnabled 'dns.magic_dns': !isEnabled
}, { }, {
method: 'PATCH', method: 'PATCH',
encType: 'application/json' encType: 'application/json'
+4 -32
View File
@@ -11,11 +11,10 @@ import AddNameserver from './dialogs/nameserver'
interface Props { interface Props {
nameservers: Record<string, string[]> nameservers: Record<string, string[]>
override: boolean
isDisabled: boolean isDisabled: boolean
} }
export default function Nameservers({ nameservers, override, isDisabled }: Props) { export default function Nameservers({ nameservers, isDisabled }: Props) {
return ( return (
<div className="flex flex-col w-2/3"> <div className="flex flex-col w-2/3">
<h1 className="text-2xl font-medium mb-4">Nameservers</h1> <h1 className="text-2xl font-medium mb-4">Nameservers</h1>
@@ -37,7 +36,6 @@ export default function Nameservers({ nameservers, override, isDisabled }: Props
isGlobal={key === 'global'} isGlobal={key === 'global'}
isDisabled={isDisabled} isDisabled={isDisabled}
nameservers={nameservers[key]} nameservers={nameservers[key]}
override={override}
name={key} name={key}
/> />
))} ))}
@@ -57,11 +55,9 @@ interface ListProps {
isDisabled: boolean isDisabled: boolean
nameservers: string[] nameservers: string[]
name: string name: string
override: boolean
} }
function NameserverList({ isGlobal, isDisabled, nameservers, name, override }: ListProps) { function NameserverList({ isGlobal, isDisabled, nameservers, name }: ListProps) {
const [localOverride, setLocalOverride] = useState(override)
const submit = useSubmit() const submit = useSubmit()
return ( return (
@@ -70,30 +66,6 @@ function NameserverList({ isGlobal, isDisabled, nameservers, name, override }: L
<h2 className="text-md font-medium opacity-80"> <h2 className="text-md font-medium opacity-80">
{isGlobal ? 'Global Nameservers' : name} {isGlobal ? 'Global Nameservers' : name}
</h2> </h2>
{isGlobal
? (
<div className="flex gap-2 items-center">
<span className="text-sm opacity-50">
Override local DNS
</span>
<Switch
label="Override local DNS"
defaultSelected={localOverride}
isDisabled={isDisabled}
onChange={() => {
submit({
'dns_config.override_local_dns': !localOverride,
}, {
method: 'PATCH',
encType: 'application/json',
})
setLocalOverride(!localOverride)
}}
/>
</div>
)
: undefined}
</div> </div>
<TableList> <TableList>
{nameservers.map((ns, index) => ( {nameservers.map((ns, index) => (
@@ -111,14 +83,14 @@ function NameserverList({ isGlobal, isDisabled, nameservers, name, override }: L
onPress={() => { onPress={() => {
if (isGlobal) { if (isGlobal) {
submit({ submit({
'dns_config.nameservers': nameservers 'dns.nameservers.global': nameservers
.filter((_, i) => i !== index), .filter((_, i) => i !== index),
}, { }, {
method: 'PATCH', method: 'PATCH',
encType: 'application/json', encType: 'application/json',
}) })
} else { } else {
const key = `dns_config.restricted_nameservers."${name}"` const key = `dns.nameservers.split."${name}"`
submit({ submit({
[key]: nameservers [key]: nameservers
.filter((_, i) => i !== index), .filter((_, i) => i !== index),
+1 -1
View File
@@ -80,7 +80,7 @@ export default function Modal({ name, disabled }: Properties) {
variant='confirm' variant='confirm'
onPress={() => { onPress={() => {
fetcher.submit({ fetcher.submit({
'dns_config.base_domain': newName 'dns.base_domain': newName
}, { }, {
method: 'PATCH', method: 'PATCH',
encType: 'application/json' encType: 'application/json'
+6 -8
View File
@@ -24,13 +24,12 @@ export async function loader() {
const config = await loadConfig() const config = await loadConfig()
const dns = { const dns = {
prefixes: config.prefixes, prefixes: config.prefixes,
magicDns: config.dns_config.magic_dns, magicDns: config.dns.magic_dns,
baseDomain: config.dns_config.base_domain, baseDomain: config.dns.base_domain,
overrideLocal: config.dns_config.override_local_dns, nameservers: config.dns.nameservers.global,
nameservers: config.dns_config.nameservers, splitDns: config.dns.nameservers.split,
splitDns: config.dns_config.restricted_nameservers, searchDomains: config.dns.search_domains,
searchDomains: config.dns_config.domains, extraRecords: config.dns.extra_records,
extraRecords: config.dns_config.extra_records,
} }
return { return {
@@ -87,7 +86,6 @@ export default function Page() {
<RenameModal name={data.baseDomain} disabled={!data.config.write} /> <RenameModal name={data.baseDomain} disabled={!data.config.write} />
<Nameservers <Nameservers
nameservers={allNs} nameservers={allNs}
override={data.overrideLocal}
isDisabled={!data.config.write} isDisabled={!data.config.write}
/> />
+2 -2
View File
@@ -27,8 +27,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
if (context.config.read) { if (context.config.read) {
const config = await loadConfig() const config = await loadConfig()
if (config.dns_config.magic_dns) { if (config.dns.magic_dns) {
magic = config.dns_config.base_domain magic = config.dns.base_domain
} }
} }
+2 -2
View File
@@ -29,8 +29,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
if (context.config.read) { if (context.config.read) {
const config = await loadConfig() const config = await loadConfig()
if (config.dns_config.magic_dns) { if (config.dns.magic_dns) {
magic = config.dns_config.base_domain magic = config.dns.base_domain
} }
} }
+2 -2
View File
@@ -41,8 +41,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
if (context.config.read) { if (context.config.read) {
const config = await loadConfig() const config = await loadConfig()
if (config.dns_config.magic_dns) { if (config.dns.magic_dns) {
magic = config.dns_config.base_domain magic = config.dns.base_domain
} }
} }
+15 -12
View File
@@ -89,18 +89,20 @@ const HeadscaleConfig = z.object({
v6: z.string(), v6: z.string(),
}), }),
dns_config: z.object({ dns: z.object({
override_local_dns: goBool.default(false), magic_dns: goBool.default(true),
nameservers: z.array(z.string()).default([]), base_domain: z.string().default('headscale.net'),
restricted_nameservers: z.record(z.array(z.string())).default({}), nameservers: z.object({
domains: z.array(z.string()).default([]), global: z.array(z.string()).default([]),
split: z.record(z.array(z.string())).default({}),
}).default({ global: [], split: {} }),
search_domains: z.array(z.string()).default([]),
extra_records: z.array(z.object({ extra_records: z.array(z.object({
name: z.string(), name: z.string(),
type: z.literal('A'), type: z.literal('A'),
value: z.string(), value: z.string(),
})).default([]), })).default([]),
magic_dns: goBool.default(false), use_username_in_magic_dns: goBool.default(false),
base_domain: z.string().default('headscale.net'),
}), }),
oidc: z.object({ oidc: z.object({
@@ -225,11 +227,12 @@ export async function loadConfig(path?: string) {
v6: '', v6: '',
}, },
dns_config: loaded.dns_config ?? { dns: loaded.dns ?? {
override_local_dns: false, nameservers: {
nameservers: [], global: [],
restricted_nameservers: {}, split: {},
domains: [], },
search_domains: [],
extra_records: [], extra_records: [],
magic_dns: false, magic_dns: false,
base_domain: 'headscale.net', base_domain: 'headscale.net',
+59 -1
View File
@@ -198,7 +198,7 @@ policy:
# - https://tailscale.com/kb/1081/magicdns/ # - https://tailscale.com/kb/1081/magicdns/
# - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/ # - https://tailscale.com/blog/2021-09-private-dns-with-magicdns/
# #
dns_config: dns_config2:
# Whether to prefer using Headscale provided DNS or use local. # Whether to prefer using Headscale provided DNS or use local.
override_local_dns: true override_local_dns: true
@@ -259,6 +259,64 @@ dns_config:
type: A type: A
value: 1.1.1.1 value: 1.1.1.1
dns:
# Whether to use [MagicDNS](https://tailscale.com/kb/1081/magicdns/).
# Only works if there is at least a nameserver defined.
magic_dns: true
# Defines the base domain to create the hostnames for MagicDNS.
# This domain _must_ be different from the server_url domain.
# `base_domain` must be a FQDN, without the trailing dot.
# The FQDN of the hosts will be
# `hostname.base_domain` (e.g., _myhost.example.com_).
base_domain: example.com
# List of DNS servers to expose to clients.
nameservers:
global:
- 1.1.1.1
- 1.0.0.1
- 2606:4700:4700::1111
- 2606:4700:4700::1001
# NextDNS (see https://tailscale.com/kb/1218/nextdns/).
# "abc123" is example NextDNS ID, replace with yours.
# - https://dns.nextdns.io/abc123
# Split DNS (see https://tailscale.com/kb/1054/dns/),
# a map of domains and which DNS server to use for each.
split:
{}
# foo.bar.com:
# - 1.1.1.1
# darp.headscale.net:
# - 1.1.1.1
# - 8.8.8.8
# Set custom DNS search domains. With MagicDNS enabled,
# your tailnet base_domain is always the first search domain.
search_domains: []
# Extra DNS records
# so far only A-records are supported (on the tailscale side)
# See https://github.com/juanfont/headscale/blob/main/docs/dns-records.md#Limitations
extra_records: []
# - name: "grafana.myvpn.example.com"
# type: "A"
# value: "100.64.0.3"
#
# # you can also put it in one line
# - { name: "prometheus.myvpn.example.com", type: "A", value: "100.64.0.3" }
# DEPRECATED
# Use the username as part of the DNS name for nodes, with this option enabled:
# node1.username.example.com
# while when this is disabled:
# node1.example.com
# This is a legacy option as Headscale has have this wrongly implemented
# while in upstream Tailscale, the username is not included.
use_username_in_magic_dns: false
# Unix socket used for the CLI to connect without authentication # Unix socket used for the CLI to connect without authentication
# Note: for production you will want to set this to something like: # Note: for production you will want to set this to something like:
unix_socket: /var/run/headscale/headscale.sock unix_socket: /var/run/headscale/headscale.sock