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() {
@@ -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() { />
+
+ + + 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,