Compare commits

...

7 Commits

Author SHA1 Message Date
Eduard Gert
d7c5f7e183 Hide update available for mobile devices (#106) (#460)
Some checks failed
build and push / build_n_push (push) Has been cancelled
(cherry picked from commit 7f248ae060385acb1245591bd46e2bb6d53ed908)
2025-04-28 11:58:26 +02:00
Eduard Gert
ebbe865ce0 Add custom dns domain (#458)
* Update domain validator

* Add custom dns domain
2025-04-28 11:58:16 +02:00
Eduard Gert
6c0ab88488 Update domain validator (#459) 2025-04-28 11:53:44 +02:00
Eduard Gert
a50576d851 Fix nameserver port input for Safari (#456) 2025-04-28 11:21:25 +02:00
Eduard Gert
676250266c Fix browse posture checks table filters (#448) 2025-04-07 10:23:07 +02:00
Vladislav Tropnikov
042c65a652 Add display of ID if user does not have email (#450)
* Add display of ID if user does not have email

* Update PeerNameCell.tsx

* Add more possible id parameters

* Hide user if there is nothing

* change id order

* Keep default behavior
2025-03-27 17:30:26 +01:00
Misha Bragin
96f2d39e54 Add CLA 2025-03-18 15:58:05 +01:00
10 changed files with 235 additions and 81 deletions

View File

@@ -0,0 +1,64 @@
## Contributor License Agreement
This Contributor License Agreement (referred to as the "Agreement") is entered into by the individual
submitting this Agreement and NetBird GmbH, c/o Max-Beer-Straße 2-4 Münzstraße 12 10178 Berlin, Germany,
referred to as "NetBird" (collectively, the "Parties"). The Agreement outlines the terms and conditions
under which NetBird may utilize software contributions provided by the Contributor for inclusion in
its software development projects. By submitting this Agreement, the Contributor confirms their acceptance
of the terms and conditions outlined below. The Contributor further represents that they are authorized to
complete this process as described herein.
## 1 Preamble
In order to clarify the IP Rights situation with regard to Contributions from any person or entity, NetBird
must have a contributor license agreement on file to be signed by each Contributor, containing the license
terms below. This license serves as protection for both the Contributor as well as NetBird and its software users;
it does not change Contributors rights to use his/her own Contributions for any other purpose.
## 2 Definitions
2.1 “IP Rights” shall mean all industrial and intellectual property rights, whether registered or not registered, whether created by Contributor or acquired by Contributor from third parties, and similar rights, including (but not limited to) semiconductor property rights, design rights, copyrights (including in the form of database rights and rights to software), all neighbouring rights (Leistungsschutzrechte), trademarks, service marks, titles, internet domain names, trade names and other labelling rights, rights deriving from corresponding applications and registrations of such rights as well as any licenses (Nutzungsrechte) under and entitlements to any such intellectual and industrial property rights.
2.2 "Contribution" shall mean any original work of authorship, including any modifications or additions to an existing work, that is or previously has been intentionally Submitted by Contributor to NetBird for inclusion in, or documentation of any Work.
2.3 "Contributor" shall mean the copyright owner or legal entity authorized by the copyright owner that is concluding this Agreement with NetBird. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
2.4 "Submitted" shall mean any form of electronic, verbal, or written communication sent to NetBird or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, NetBird for the purpose of discussing and improving the Work, but excluding communication that is marked or otherwise designated in writing by Contributor as "Not a Contribution".
2.5 "Work" means any of the products owned or managed by NetBird, in particular, but not exclusively, software.
## 3 Licenses
3.1 Subject to the terms and conditions of this agreement, Contributor hereby grants to NetBird and to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable license to reproduce by any means and in any form, in whole or in part, permanently or temporarily, the Contributions (including loading, displaying, executing, transmitting or storing works for the purpose of executing and processing data or transferring them to video, audio and other data carriers), including the right to distribute, display and present such Contributions and make them available to the public (e.g. via the internet) and to transmit and display such Contributions by any means. The license also includes the right to modify, translate, adapt, edit and otherwise alter the Contributions and to use these results in the same manner as the original Contributions and derivative works. Except for licenses in patents acc. to Sec. 3, such license refers to any IP Rights in the Contributions and derivative works. The Contributor acknowledges that NetBird is not required to credit them by name for their Contribution and agrees to waive any moral rights associated with their Contribution in relation to NetBird or its sublicensees.
3.2 Subject to the terms and conditions of this agreement, Contributor hereby grants to NetBird and to recipients of software distributed by NetBird a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license in the Contributions to make, have made, use, sell, offer to sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by the Contributor which are necessarily infringed by Contributors Contribution(s) alone or by combination of Contributors Contribution(s) with the Work to which such Contribution(s) was Submitted.
3.3 NetBird hereby accepts such licenses.
## 4 Contributors Representations
4.1 Contributor represents that Contributor is legally entitled to grant the above license. If Contributors employer has IP Rights to Contributors Contributions, Contributor represent that he/she has received permission to make Contributions on behalf of such employer, that such employer has waived such IP Rights to the Contributions of Contributor to NetBird, or that such employer has executed a separate contributor license agreement with NetBird.
4.2 Contributor represents that any Contribution is his/her original creation.
4.3 Contributor represents to his/her best knowledge that any Contribution does not violate any third party IP Rights.
4.4 Contributor represents that any Contribution submission includes complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which Contributor is personally aware and which are associated with any part of the Contribution.
4.5 The Contributor represents that their Contribution does not include any work distributed under a copyleft license.
## 5 Information obligation
Contributor agrees to notify NetBird of any facts or circumstances of which Contributor become aware that would make these representations inaccurate in any respect.
## 6 Submission of Third-Party works
Should Contributor wish to submit work that is not Contributors original creation, Contributor may submit it to NetBird separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which Contributor are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
## 7 No Consideration
Unless compensation is mandatory under statutory law, no compensation for any license under this agreement shall be payable.
## 8 Final Provisions
8.1 Laws. This Agreement is governed by the laws of the Federal Republic of Germany.
8.2 Venue. Place of jurisdiction shall, to the extent legally permissible, be Berlin, Germany.
8.3 Severability. If any provision in this agreement is unlawful, invalid or ineffective, it shall not affect the enforceability or effectiveness of the remainder of this agreement. The parties agree to replace any unlawful, invalid or ineffective provision with a provision that comes as close as possible to the commercial intent and purpose of the original provision. This section also applies accordingly to any gaps in the contract.
8.4 Variations. Any variations, amendments or supplements to this Agreement must be in writing. This also applies to any variation of this Section 8.4.

View File

@@ -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;
};
}

View File

@@ -590,7 +590,7 @@ function NameserverInput({
</div>
<Input
maxWidthClass={"max-w-[130px]"}
maxWidthClass={"min-w-[150px] max-w-[150px]"}
customPrefix={"Port"}
placeholder={"53"}
value={port}

View File

@@ -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

View File

@@ -20,6 +20,9 @@ export default function PeerNameCell({ peer, linkToPeer = true }: Props) {
return users?.find((user) => user.id === peer.user_id);
}, [users, peer.user_id]);
const displayUserEmailOrName = userOfPeer?.email || userOfPeer?.name;
const displayUserId = userOfPeer?.id || peer?.user_id;
return (
<div>
<div
@@ -40,7 +43,7 @@ export default function PeerNameCell({ peer, linkToPeer = true }: Props) {
}
>
<div className={"text-nb-gray-400 font-light truncate"}>
{userOfPeer?.email}
{displayUserEmailOrName || (displayUserId && `user: ${displayUserId}`)}
</div>
</ActiveInactiveRow>
</div>

View File

@@ -6,21 +6,30 @@ import {
TooltipTrigger,
} from "@components/Tooltip";
import MemoizedNetBirdIcon from "@components/ui/MemoizedNetBirdIcon";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { parseVersionString } from "@utils/version";
import { ArrowRightIcon, ArrowUpCircleIcon } from "lucide-react";
import * as React from "react";
import { useMemo } from "react";
import { useApplicationContext } from "@/contexts/ApplicationProvider";
import { OperatingSystem } from "@/interfaces/OperatingSystem";
type Props = {
version: string;
os: string;
};
export default function PeerVersionCell({ version }: Props) {
export default function PeerVersionCell({ version, os }: Props) {
const { latestVersion, latestUrl } = useApplicationContext();
const updateAvailable = useMemo(() => {
const operatingSystem = getOperatingSystem(os);
if (
operatingSystem === OperatingSystem.IOS ||
operatingSystem === OperatingSystem.ANDROID
)
return false;
return parseVersionString(version) < parseVersionString(latestVersion);
}, [version, latestVersion]);
}, [os, version, latestVersion]);
const updateIcon = useMemo(() => {
return <ArrowUpCircleIcon size={15} className={"text-netbird"} />;

View File

@@ -154,7 +154,9 @@ const PeersTableColumns: ColumnDef<Peer>[] = [
header: ({ column }) => {
return <DataTableHeader column={column}>Version</DataTableHeader>;
},
cell: ({ row }) => <PeerVersionCell version={row.original.version} />,
cell: ({ row }) => (
<PeerVersionCell version={row.original.version} os={row.original.os} />
),
},
{
id: "status",

View File

@@ -3,14 +3,12 @@ import { Checkbox } from "@components/Checkbox";
import { DataTable } from "@components/table/DataTable";
import DataTableHeader from "@components/table/DataTableHeader";
import DataTableRefreshButton from "@components/table/DataTableRefreshButton";
import { useLocalStorage } from "@hooks/useLocalStorage";
import {
ColumnDef,
RowSelectionState,
SortingState,
} from "@tanstack/react-table";
import useFetchApi from "@utils/api";
import { usePathname } from "next/navigation";
import React, { useState } from "react";
import { useSWRConfig } from "swr";
import { PostureCheck } from "@/interfaces/PostureCheck";
@@ -25,18 +23,14 @@ export default function PostureCheckBrowseTable({ onAdd }: Readonly<Props>) {
const { data: postureChecks, isLoading } =
useFetchApi<PostureCheck[]>("/posture-checks");
const { mutate } = useSWRConfig();
const path = usePathname();
// Default sorting state of the table
const [sorting, setSorting] = useLocalStorage<SortingState>(
"netbird-table-sort" + path,
[
{
id: "name",
desc: true,
},
],
);
const [sorting, setSorting] = useState<SortingState>([
{
id: "name",
desc: true,
},
]);
const [selectedRows, setSelectedRows] = useState<RowSelectionState>({});
@@ -47,6 +41,7 @@ export default function PostureCheckBrowseTable({ onAdd }: Readonly<Props>) {
rowSelection={selectedRows}
setRowSelection={setSelectedRows}
isLoading={isLoading}
keepStateInLocalStorage={false}
text={"Posture Check"}
sorting={sorting}
wrapperClassName={""}

View File

@@ -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";
@@ -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,
@@ -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,
});
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"}>
@@ -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
</>
<div
className={
"flex flex-col gap-1 sm:flex-row w-full sm:gap-4 items-center"
}
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={"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>

View File

@@ -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;