diff --git a/src/app/(dashboard)/peer/page.tsx b/src/app/(dashboard)/peer/page.tsx
index bda0bac6..4db08092 100644
--- a/src/app/(dashboard)/peer/page.tsx
+++ b/src/app/(dashboard)/peer/page.tsx
@@ -62,6 +62,7 @@ import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import AddRouteDropdownButton from "@/modules/peer/AddRouteDropdownButton";
import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
+import {SelectDropdown} from "@components/select/SelectDropdown";
export default function PeerPage() {
const queryParameter = useSearchParams();
@@ -86,6 +87,9 @@ function PeerOverview() {
const [loginExpiration, setLoginExpiration] = useState(
peer.login_expiration_enabled,
);
+ const [ipv6Enabled, setIpv6Enabled] = useState(
+ peer.ipv6_enabled,
+ );
const [selectedGroups, setSelectedGroups, { getAllGroupCalls }] =
useGroupHelper({
initial: peerGroups,
@@ -108,10 +112,11 @@ function PeerOverview() {
ssh,
selectedGroups,
loginExpiration,
+ ipv6Enabled
]);
const updatePeer = async () => {
- const updateRequest = update(name, ssh, loginExpiration);
+ const updateRequest = update(name, ssh, loginExpiration, ipv6Enabled);
const groupCalls = getAllGroupCalls();
const batchCall = groupCalls
? [...groupCalls, updateRequest]
@@ -122,7 +127,7 @@ function PeerOverview() {
promise: Promise.all(batchCall).then(() => {
mutate("/peers/" + peer.id);
mutate("/groups");
- updateHasChangedRef([name, ssh, selectedGroups, loginExpiration]);
+ updateHasChangedRef([name, ssh, selectedGroups, loginExpiration, ipv6Enabled]);
}),
loadingMessage: "Saving the peer...",
});
@@ -137,11 +142,11 @@ function PeerOverview() {
}
+ href={"/peers"}
+ label={"Peers"}
+ icon={}
/>
-
+
@@ -149,11 +154,11 @@ function PeerOverview() {
-
+
{!isUser && (
)}
-
+
@@ -191,9 +196,9 @@ function PeerOverview() {
router.push("/peers")}
+ variant={"default"}
+ className={"w-full"}
+ onClick={() => router.push("/peers")}
>
Cancel
@@ -209,7 +214,7 @@ function PeerOverview() {
-
+
<>
-
+
Login expiration is disabled for all peers added
with an setup-key.
@@ -231,7 +236,7 @@ function PeerOverview() {
>
) : (
<>
-
+
{`You don't have the required permissions to update this
setting.`}
@@ -249,7 +254,7 @@ function PeerOverview() {
onChange={setLoginExpiration}
label={
<>
-
+
Login Expiration
>
}
@@ -265,7 +270,7 @@ function PeerOverview() {
"flex gap-2 items-center !text-nb-gray-300 text-xs"
}
>
-
+
{`You don't have the required permissions to update this
setting.`}
@@ -286,7 +291,7 @@ function PeerOverview() {
}
label={
<>
-
+
SSH Access
>
}
@@ -308,7 +313,7 @@ function PeerOverview() {
"flex gap-2 items-center !text-nb-gray-300 text-xs"
}
>
-
+
{`You don't have the required permissions to update this
setting.`}
@@ -327,11 +332,47 @@ function PeerOverview() {
/>
+
+ IPv6 Support
+
+ Whether to enable IPv6, disable it, or enable IPv6 automatically.
+ Overrides groupwide setting if set to something else than Automatic.
+ Automatic enables IPv6 if it is enabled by at least one group or if the peer is used in at least one
+ IPv6 route.
+
+
+
+
+ IPv6 Support requires a recent version of the NetBird client as well as a supported OS (Linux with nftables).
+
+
+ }
+ className={"w-full block"}
+ disabled={peer.ipv6_supported}
+ >
+
+
+
-
+
{isLinux && !isUser ? (
@@ -346,12 +387,12 @@ function PeerOverview() {
-
+
) : null}
@@ -360,8 +401,8 @@ function PeerOverview() {
);
}
-function PeerInformationCard({ peer }: { peer: Peer }) {
- const { isLoading, getRegionByPeer } = useCountries();
+function PeerInformationCard({peer}: { peer: Peer }) {
+ const {isLoading, getRegionByPeer} = useCountries();
const countryText = useMemo(() => {
return getRegionByPeer(peer);
@@ -375,13 +416,23 @@ function PeerInformationCard({ peer }: { peer: Peer }) {
copyText={"NetBird IP-Address"}
label={
<>
-
- NetBird IP-Address
+
+ NetBird IPv4-Address
>
}
value={peer.ip}
/>
+
+
+ NetBird IPv6-Address
+ >
+ }
+ value={peer.ip6}
+ />
+
[
...previous,
- { name: name, peers: groupPeers },
+ { name: name, peers: groupPeers, ipv6_enabled: false },
]);
}
if (max == 1 && values.length == 1) {
- onChange([{ name: name, id: group?.id, peers: groupPeers }]);
+ onChange([{ name: name, id: group?.id, peers: groupPeers, ipv6_enabled: group == null ? false : group.ipv6_enabled }]);
} else {
onChange((previous) => [
...previous,
- { name: name, id: group?.id, peers: groupPeers },
+ { name: name, id: group?.id, peers: groupPeers, ipv6_enabled: group == null ? false : group.ipv6_enabled },
]);
}
diff --git a/src/contexts/PeerProvider.tsx b/src/contexts/PeerProvider.tsx
index 714e71b5..96434478 100644
--- a/src/contexts/PeerProvider.tsx
+++ b/src/contexts/PeerProvider.tsx
@@ -24,7 +24,8 @@ const PeerContext = React.createContext(
name: string,
ssh: boolean,
loginExpiration: boolean,
- approval_required?: boolean,
+ ipv6_enabled: string,
+ approval_required?: boolean
) => Promise;
openSSHDialog: () => Promise;
deletePeer: () => void;
@@ -65,7 +66,8 @@ export default function PeerProvider({ children, peer }: Props) {
name: string,
ssh: boolean,
loginExpiration: boolean,
- approval_required?: boolean,
+ ipv6_enabled: string,
+ approval_required?: boolean
) => {
return peerRequest.put(
{
@@ -78,6 +80,7 @@ export default function PeerProvider({ children, peer }: Props) {
: peer.login_expiration_enabled,
approval_required:
approval_required == undefined ? undefined : approval_required,
+ ipv6_enabled: ipv6_enabled != undefined ? ipv6_enabled : peer.ipv6_enabled
},
`/${peer.id}`,
);
diff --git a/src/interfaces/Group.ts b/src/interfaces/Group.ts
index 8a6d91a3..b93f197c 100644
--- a/src/interfaces/Group.ts
+++ b/src/interfaces/Group.ts
@@ -3,6 +3,7 @@ export interface Group {
name: string;
peers?: GroupPeer[] | string[];
peers_count?: number;
+ ipv6_enabled: boolean
}
export interface GroupPeer {
diff --git a/src/interfaces/Nameserver.ts b/src/interfaces/Nameserver.ts
index 50ad75c6..7d97a601 100644
--- a/src/interfaces/Nameserver.ts
+++ b/src/interfaces/Nameserver.ts
@@ -58,6 +58,18 @@ export const NameserverPresets: Record = {
port: 53,
id: "2",
},
+ {
+ ip: "2001:4860:4860::8888",
+ ns_type: "udp",
+ port: 53,
+ id: "3",
+ },
+ {
+ ip: "2001:4860:4860::8844",
+ ns_type: "udp",
+ port: 53,
+ id: "4",
+ },
],
groups: [],
enabled: true,
@@ -81,6 +93,18 @@ export const NameserverPresets: Record = {
port: 53,
id: "2",
},
+ {
+ ip: "2606:4700:4700::1111",
+ ns_type: "udp",
+ port: 53,
+ id: "3",
+ },
+ {
+ ip: "2606:4700:4700::1001",
+ ns_type: "udp",
+ port: 53,
+ id: "4",
+ },
],
groups: [],
enabled: true,
@@ -104,6 +128,18 @@ export const NameserverPresets: Record = {
port: 53,
id: "2",
},
+ {
+ ip: "2620:fe::fe",
+ ns_type: "udp",
+ port: 53,
+ id: "3",
+ },
+ {
+ ip: "2620:fe::9",
+ ns_type: "udp",
+ port: 53,
+ id: "4",
+ },
],
groups: [],
enabled: true,
diff --git a/src/interfaces/Peer.ts b/src/interfaces/Peer.ts
index 9e09bc86..4e6f711c 100644
--- a/src/interfaces/Peer.ts
+++ b/src/interfaces/Peer.ts
@@ -5,6 +5,7 @@ export interface Peer {
id?: string;
name: string;
ip: string;
+ ip6?: string,
connected: boolean;
last_seen: Date;
os: string;
@@ -19,6 +20,8 @@ export interface Peer {
last_login: Date;
login_expired: boolean;
login_expiration_enabled: boolean;
+ ipv6_supported: boolean,
+ ipv6_enabled: string,
approval_required: boolean;
city_name: string;
country_code: string;
diff --git a/src/modules/dns-nameservers/NameserverModal.tsx b/src/modules/dns-nameservers/NameserverModal.tsx
index a7ec7630..334b5e1d 100644
--- a/src/modules/dns-nameservers/NameserverModal.tsx
+++ b/src/modules/dns-nameservers/NameserverModal.tsx
@@ -332,6 +332,11 @@ export function NameserverModalContent({
Add Nameserver
+
+
+ Note that if the IP address is an IPv6 address, it will only be distributed to IPv6-enabled peers.
+ To ensure best reliability, always include at least one IPv4 address when adding nameservers.
+
diff --git a/src/modules/groups/useGroupHelper.tsx b/src/modules/groups/useGroupHelper.tsx
index 1b6cdde6..7398ce10 100644
--- a/src/modules/groups/useGroupHelper.tsx
+++ b/src/modules/groups/useGroupHelper.tsx
@@ -80,6 +80,7 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
return groupRequest.put(
{
name: g.name,
+ ipv6_enabled: g.ipv6_enabled,
peers: newPeerGroups
? newPeerGroups.map((p) => {
const groupPeer = p as GroupPeer;
@@ -111,6 +112,7 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
return groupRequest.put(
{
name: selectedGroup.name,
+ ipv6_enabled: selectedGroup.ipv6_enabled,
peers: peers,
},
`/${selectedGroup.id}`,
@@ -121,6 +123,7 @@ export default function useGroupHelper({ initial = [], peer }: Props) {
return groupRequest
.post({
name: selectedGroup.name,
+ ipv6_enabled: selectedGroup.ipv6_enabled,
peers: groupPeers || [],
})
.then((group) => {
diff --git a/src/modules/peers/PeerActionCell.tsx b/src/modules/peers/PeerActionCell.tsx
index 3310b1bf..7e4f2aaa 100644
--- a/src/modules/peers/PeerActionCell.tsx
+++ b/src/modules/peers/PeerActionCell.tsx
@@ -35,6 +35,7 @@ export default function PeerActionCell() {
peer.name,
peer.ssh_enabled,
!peer.login_expiration_enabled,
+ peer.ipv6_enabled,
).then(() => {
mutate("/peers");
mutate("/groups");
@@ -52,6 +53,7 @@ export default function PeerActionCell() {
peer.name,
!peer.ssh_enabled,
peer.login_expiration_enabled,
+ peer.ipv6_enabled,
).then(() => {
mutate("/peers");
mutate("/groups");
diff --git a/src/modules/peers/PeerAddressCell.tsx b/src/modules/peers/PeerAddressCell.tsx
index 145046b6..e9cbf75b 100644
--- a/src/modules/peers/PeerAddressCell.tsx
+++ b/src/modules/peers/PeerAddressCell.tsx
@@ -46,7 +46,7 @@ export default function PeerAddressCell({ peer }: Props) {
{peer.dns_label}
+ {peer.ip6 != null ? (
+
+
+ {peer.ip6}
+
+
+ ) : null}
diff --git a/src/modules/peers/PeerAddressTooltipContent.tsx b/src/modules/peers/PeerAddressTooltipContent.tsx
index 912f7676..9bfd697e 100644
--- a/src/modules/peers/PeerAddressTooltipContent.tsx
+++ b/src/modules/peers/PeerAddressTooltipContent.tsx
@@ -25,9 +25,16 @@ export const PeerAddressTooltipContent = ({ peer }: Props) => {
>
}
- label={"NetBird IP"}
+ label={"NetBird IPv4"}
value={peer.ip}
/>
+ {peer.ip6 != null ? (
+ }
+ label={"NetBird IPv6"}
+ value={peer.ip6}
+ />
+ ) : null}
}
label={"Public IP"}
diff --git a/src/modules/peers/PeerStatusCell.tsx b/src/modules/peers/PeerStatusCell.tsx
index cd5efd5b..fc009a71 100644
--- a/src/modules/peers/PeerStatusCell.tsx
+++ b/src/modules/peers/PeerStatusCell.tsx
@@ -40,6 +40,7 @@ export default function PeerStatusCell({ peer }: Props) {
peer.name,
peer.ssh_enabled,
peer.login_expiration_enabled,
+ peer.ipv6_enabled,
false,
).then(() => {
mutate("/peers");
diff --git a/src/modules/peers/PeersTable.tsx b/src/modules/peers/PeersTable.tsx
index 2b77aaf9..6ce34d54 100644
--- a/src/modules/peers/PeersTable.tsx
+++ b/src/modules/peers/PeersTable.tsx
@@ -55,6 +55,10 @@ const PeersTableColumns: ColumnDef[] = [
accessorKey: "ip",
sortingFn: "text",
},
+ {
+ accessorKey: "ip6",
+ sortingFn: "text",
+ },
{
id: "user_name",
accessorFn: (peer) => (peer.user ? peer.user?.name : "Unknown"),
@@ -195,6 +199,7 @@ export default function PeersTable({ peers, isLoading }: Props) {
group_name_strings: false,
group_names: false,
ip: false,
+ ip6: false,
user_name: false,
user_email: false,
actions: !isUser,
diff --git a/src/modules/settings/GroupsActionCell.tsx b/src/modules/settings/GroupsActionCell.tsx
index 1b26cf5f..3443e1fd 100644
--- a/src/modules/settings/GroupsActionCell.tsx
+++ b/src/modules/settings/GroupsActionCell.tsx
@@ -3,11 +3,13 @@ import FullTooltip from "@components/FullTooltip";
import { notify } from "@components/Notification";
import { useApiCall } from "@utils/api";
import { Trash2 } from "lucide-react";
-import React from "react";
+import React, {useMemo} from "react";
import { useSWRConfig } from "swr";
import { useDialog } from "@/contexts/DialogProvider";
import { SetupKey } from "@/interfaces/SetupKey";
import { GroupUsage } from "@/modules/settings/useGroupsUsage";
+import {ToggleSwitch} from "@components/ToggleSwitch";
+import type {Group, GroupPeer} from "@/interfaces/Group";
type Props = {
group: GroupUsage;
diff --git a/src/modules/settings/GroupsIPv6Cell.tsx b/src/modules/settings/GroupsIPv6Cell.tsx
new file mode 100644
index 00000000..a03c69a1
--- /dev/null
+++ b/src/modules/settings/GroupsIPv6Cell.tsx
@@ -0,0 +1,52 @@
+import Button from "@components/Button";
+import FullTooltip from "@components/FullTooltip";
+import { notify } from "@components/Notification";
+import { useApiCall } from "@utils/api";
+import { Trash2 } from "lucide-react";
+import React, {useMemo} from "react";
+import { useSWRConfig } from "swr";
+import { useDialog } from "@/contexts/DialogProvider";
+import { SetupKey } from "@/interfaces/SetupKey";
+import { GroupUsage } from "@/modules/settings/useGroupsUsage";
+import {ToggleSwitch} from "@components/ToggleSwitch";
+import type {Group, GroupPeer} from "@/interfaces/Group";
+
+type Props = {
+ group: GroupUsage;
+};
+export default function GroupsIPv6Cell({ group }: Props) {
+ const updateRequest = useApiCall("/groups/" + group.id);
+ const { mutate } = useSWRConfig();
+
+ const ipv6IsEnabled = useMemo(() => {
+ return group.original_group.ipv6_enabled;
+ }, [group]);
+
+ const handleIpv6Change = async (newValue: boolean) => {
+ return updateRequest.put(
+ {
+ name: group.name,
+ peers: group.original_group.peers?.map((p) => {
+ if (typeof p == "string") {
+ return p
+ } else {
+ return p.id
+ }
+ }),
+ ipv6_enabled: newValue
+ },
+ ).then((g) => {
+ mutate("/groups")
+ });
+ };
+
+ return (
+
+ handleIpv6Change(!ipv6IsEnabled)}
+ >
+
+ );
+}
diff --git a/src/modules/settings/GroupsTable.tsx b/src/modules/settings/GroupsTable.tsx
index eae6cd9c..70bf1296 100644
--- a/src/modules/settings/GroupsTable.tsx
+++ b/src/modules/settings/GroupsTable.tsx
@@ -18,6 +18,7 @@ import ActiveInactiveRow from "@/modules/common-table-rows/ActiveInactiveRow";
import GroupsActionCell from "@/modules/settings/GroupsActionCell";
import GroupsCountCell from "@/modules/settings/GroupsCountCell";
import useGroupsUsage, { GroupUsage } from "@/modules/settings/useGroupsUsage";
+import GroupsIPv6Cell from "@/modules/settings/GroupsIPv6Cell";
// Peers, Access Controls, DNS, Routes, Setup Keys, Users
export const GroupsTableColumns: ColumnDef[] = [
@@ -176,6 +177,16 @@ export const GroupsTableColumns: ColumnDef[] = [
);
},
},
+ {
+ id: "ipv6",
+ header: ({ column }) => {
+ return IPv6 ;
+ },
+ accessorFn: row => row.original_group.ipv6_enabled,
+ cell: ({ row }) => (
+
+ ),
+ },
{
accessorKey: "id",
header: "",
diff --git a/src/modules/settings/useGroupsUsage.tsx b/src/modules/settings/useGroupsUsage.tsx
index 6e085d56..d484f2c1 100644
--- a/src/modules/settings/useGroupsUsage.tsx
+++ b/src/modules/settings/useGroupsUsage.tsx
@@ -10,6 +10,7 @@ import { User } from "@/interfaces/User";
export interface GroupUsage {
id: string;
name: string;
+ original_group: Group;
peers_count: number;
policies_count: number;
nameservers_count: number;
@@ -125,6 +126,7 @@ export default function useGroupsUsage() {
return {
id: group.id,
name: group.name,
+ original_group: group,
peers_count: group.peers_count,
policies_count: policyCount,
nameservers_count: nameserverCount,