Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/interfaces/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ export interface Account {
jwt_allow_groups: string[];
regular_users_view_blocked: boolean;
routing_peer_dns_resolution_enabled: boolean;
dns_domain: string;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const ResourceSingleAddressInput = ({ value, onChange }: Props) => {

// Case 1: If it has characters (potential domain) but is not a CIDR block
if (hasChars && !isCIDRBlock) {
if (!validator.isValidDomainWithWildcard(value)) {
if (!validator.isValidDomain(value)) {
return "Please enter a valid domain, e.g. intra.example.com or *.example.com";
}
return ""; // Valid domain
Expand Down
144 changes: 117 additions & 27 deletions src/modules/settings/NetworkSettingsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import Breadcrumbs from "@components/Breadcrumbs";
import Button from "@components/Button";
import FancyToggleSwitch from "@components/FancyToggleSwitch";
import HelpText from "@components/HelpText";
import InlineLink from "@components/InlineLink";
import { Input } from "@components/Input";
import { Label } from "@components/Label";
import { notify } from "@components/Notification";
import { useHasChanges } from "@hooks/useHasChanges";
import * as Tabs from "@radix-ui/react-tabs";
import { useApiCall } from "@utils/api";
import { GlobeIcon, NetworkIcon } from "lucide-react";
import React, { useState } from "react";
import { validator } from "@utils/helpers";
import { isNetBirdHosted } from "@utils/netbird";
import { ExternalLinkIcon, GlobeIcon, NetworkIcon } from "lucide-react";
import React, { useMemo, useState } from "react";
import { useSWRConfig } from "swr";
import SettingsIcon from "@/assets/icons/SettingsIcon";
import { Account } from "@/interfaces/Account";
Expand All @@ -13,18 +21,23 @@ type Props = {
account: Account;
};

export default function NetworkSettingsTab({ account }: Props) {
export default function NetworkSettingsTab({ account }: Readonly<Props>) {
const { mutate } = useSWRConfig();
const saveRequest = useApiCall<Account>("/accounts/" + account.id);
const saveRequest = useApiCall<Account>("/accounts/" + account.id, true);

const [routingPeerDNSSetting, setRoutingPeerDNSSetting] = useState(
account.settings.routing_peer_dns_resolution_enabled,
);
const [customDNSDomain, setCustomDNSDomain] = useState(
account.settings.dns_domain || "",
);

const toggleSetting = async (toggle: boolean) => {
const toggleNetworkDNSSetting = async (toggle: boolean) => {
notify({
title: "Save Network Settings",
description: "Network settings successfully saved.",
title: "DNS Wildcard Routing",
description: `DNS Wildcard Routing successfully ${
toggle ? "enabled" : "disabled"
}.`,
promise: saveRequest
.put({
id: account.id,
Expand All @@ -37,10 +50,43 @@ export default function NetworkSettingsTab({ account }: Props) {
setRoutingPeerDNSSetting(toggle);
mutate("/accounts");
}),
loadingMessage: "Saving the network settings...",
loadingMessage: "Updating DNS wildcard setting...",
});
};

const { hasChanges, updateRef } = useHasChanges([customDNSDomain]);

const saveChanges = async () => {
notify({
title: "Custom DNS Domain",
description: `Custom DNS Domain successfully updated.`,
promise: saveRequest
.put({
id: account.id,
settings: {
...account.settings,
dns_domain: customDNSDomain || "",
},
})
.then(() => {
mutate("/accounts");
updateRef([customDNSDomain]);
}),
loadingMessage: "Updating Custom DNS domain...",
});
};

const domainError = useMemo(() => {
if (customDNSDomain == "") return "";
const valid = validator.isValidDomain(customDNSDomain, {
allowWildcard: false,
allowOnlyTld: false,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
allowOnlyTld: false,
allowOnlyTld: true,

This should be allowed, no? E.g. if someone wants peer1.netbird or something like that

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@heisbrot This ^^. Why not allow TLD-only custom domains?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alindt Currently, that's a backend limitation. We will fix it once the backend is ready.

});
if (!valid) {
return "Please enter a valid domain, e.g. example.com or intra.example.com";
}
}, [customDNSDomain]);

return (
<Tabs.Content value={"networks"}>
<div className={"p-default py-6 max-w-2xl"}>
Expand All @@ -51,36 +97,80 @@ export default function NetworkSettingsTab({ account }: Props) {
icon={<SettingsIcon size={13} />}
/>
<Breadcrumbs.Item
href={"/settings#network"}
label={"Network"}
href={"/settings?tab=networks"}
label={"Networks"}
icon={<NetworkIcon size={14} />}
active
/>
</Breadcrumbs>
<div className={"flex items-start justify-between"}>
<h1>Networks</h1>
<div>
<h1>Networks</h1>
</div>
<Button
variant={"primary"}
disabled={!hasChanges}
onClick={saveChanges}
>
Save Changes
</Button>
</div>

<div className={"flex flex-col gap-6 w-full mt-8"}>
<div>
<FancyToggleSwitch
value={routingPeerDNSSetting}
onChange={toggleSetting}
label={
<>
<GlobeIcon size={15} />
Enable DNS Wildcard Routing
</>
}
helpText={
<>
Allow routing using DNS wildcards. This requires NetBird
client v0.35 or higher. Changes will only take effect after
restarting the clients.
</>
<div
className={
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
}
/>
>
<div className={"min-w-[330px]"}>
<Label>DNS Domain</Label>
<HelpText>
Specify a custom DNS domain for your network. This will be
used for all your peers.
</HelpText>
</div>
<div className={"w-full"}>
<Input
placeholder={
isNetBirdHosted() ? "netbird.cloud" : "netbird.selfhosted"
}
errorTooltip={true}
errorTooltipPosition={"top"}
error={domainError}
value={customDNSDomain}
onChange={(e) => setCustomDNSDomain(e.target.value)}
/>
</div>
</div>
</div>

<FancyToggleSwitch
value={routingPeerDNSSetting}
onChange={toggleNetworkDNSSetting}
label={
<>
<GlobeIcon size={15} />
Enable DNS Wildcard Routing
</>
}
helpText={
<>
Allow routing using DNS wildcards. This requires NetBird client
v0.35 or higher. Changes will only take effect after restarting
the clients.{" "}
<InlineLink
href={
"https://docs.netbird.io/how-to/accessing-entire-domains-within-networks#enabling-dns-wildcard-routing"
}
target={"_blank"}
>
Learn more
<ExternalLinkIcon size={12} />
</InlineLink>
</>
}
/>
</div>
</div>
</Tabs.Content>
Expand Down
62 changes: 26 additions & 36 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,47 +41,37 @@ export const sleep = (ms: number) => {
};

export const validator = {
isValidDomain: (domain: string) => {
const unicodeDomain =
/^(?!.*\.\.)(?!.*\.$)(?!.*\s)(?:(?!-)(?!.*--)[a-zA-Z0-9\u00A1-\uFFFF-]{1,63}(?<!-)\.)+(?!-)(?!.*--)[a-zA-Z0-9\u00A1-\uFFFF-]{2,63}$/u;
try {
const minMaxChars = [1, 255];
const isValidDomainLength =
domain.length >= minMaxChars[0] && domain.length <= minMaxChars[1];
const includesDot = domain.includes(".");
const hasNoWhitespace = !domain.includes(" ");
return (
unicodeDomain.test(domain) &&
includesDot &&
hasNoWhitespace &&
isValidDomainLength
);
} catch (e) {
return false;
}
},
isValidDomainWithWildcard: (domain: string) => {
// Basic checks
if (!domain || domain.length > 255 || domain.includes(" ")) {
return false;
}
isValidDomain: (
domain: string,
options?: { allowWildcard?: boolean; allowOnlyTld?: boolean },
) => {
const { allowWildcard = true, allowOnlyTld = true } = options || {
allowWildcard: true,
allowOnlyTld: true,
};

// Handle wildcard
if (domain.includes("*")) {
if (!domain.startsWith("*.") || domain.indexOf("*", 1) !== -1) {
try {
const includesAtLeastOneDot = domain.includes(".");
const hasWhitespace = domain.includes(" ");
const domainRegex =
/^(?!-)[a-z0-9\u00a1-\uffff-*]{0,63}(?<!-)(\.[a-z0-9\u00a1-\uffff-*]{0,63})*$/i;
const isValidUnicodeDomain = domainRegex.test(domain);
if (domain.length < 1 || domain.length > 255) {
return false;
}
domain = "sub" + domain.slice(1); // Replace * with valid subdomain for testing
}

// Split and validate each part
const parts = domain.split(".");
if (parts.length < 2) {
if (!allowWildcard && domain.startsWith("*.")) {
return false;
}
if (allowWildcard && !domain.startsWith("*.") && domain.includes("*")) {
return false;
}
if (!allowOnlyTld && domain.startsWith(".")) {
return false;
}
return includesAtLeastOneDot && isValidUnicodeDomain && !hasWhitespace;
} catch (error) {
return false;
}

const validPart = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/;
return parts.every((part) => validPart.test(part));
},
isValidEmail: (email: string) => {
const regExp = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$/i;
Expand Down