Skip to content

Commit 2272a1d

Browse files
authored
Add Exit Nodes (#374)
* Add exit node feature * Fix spelling * Hide masquerade for exit nodes * Add exit node information to peers list * Change exit node button, add indicator to peers table * Add steps to route modal * Add hook to check if peer has exit nodes * Hide exit node indicator for regular users * Add documentation links
1 parent fc3da50 commit 2272a1d

19 files changed

+535
-169
lines changed

src/app/(dashboard)/peer/page.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ import { getOperatingSystem } from "@/hooks/useOperatingSystem";
5757
import { OperatingSystem } from "@/interfaces/OperatingSystem";
5858
import type { Peer } from "@/interfaces/Peer";
5959
import PageContainer from "@/layouts/PageContainer";
60+
import { AddExitNodeButton } from "@/modules/exit-node/AddExitNodeButton";
61+
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
6062
import useGroupHelper from "@/modules/groups/useGroupHelper";
6163
import AddRouteDropdownButton from "@/modules/peer/AddRouteDropdownButton";
6264
import PeerRoutesTable from "@/modules/peer/PeerRoutesTable";
@@ -127,6 +129,7 @@ function PeerOverview() {
127129
};
128130

129131
const { isUser } = useLoggedInUser();
132+
const hasExitNodes = useHasExitNodes(peer);
130133

131134
return (
132135
<PageContainer>
@@ -342,7 +345,8 @@ function PeerOverview() {
342345
</Paragraph>
343346
</div>
344347
<div className={"inline-flex gap-4 justify-end"}>
345-
<div>
348+
<div className={"gap-4 flex"}>
349+
<AddExitNodeButton peer={peer} firstTime={!hasExitNodes} />
346350
<AddRouteDropdownButton />
347351
</div>
348352
</div>

src/components/PeerSelector.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ export function PeerSelector({
121121
<PopoverTrigger asChild>
122122
<button
123123
className={cn(
124-
"min-h-[42px] w-full relative items-center group",
124+
"min-h-[46px] w-full relative items-center group",
125125
"border border-neutral-200 dark:border-nb-gray-700 justify-between py-2 px-3",
126126
"rounded-md bg-white text-sm dark:bg-nb-gray-900/40 flex dark:text-neutral-400/70 text-neutral-500 cursor-pointer enabled:hover:dark:bg-nb-gray-900/50",
127127
"disabled:opacity-40 disabled:cursor-default",

src/components/SquareIcon.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const iconVariant = cva(
1515
green: "bg-green-950 border-green-500 text-green-500",
1616
purple: "bg-purple-950 border-purple-500 text-purple-500",
1717
indigo: "bg-indigo-950 border-indigo-500 text-indigo-500",
18+
yellow: "bg-yellow-950 border-yellow-400 text-yellow-400",
1819
},
1920
size: {
2021
small: "w-8 h-8",

src/components/modal/ModalHeader.tsx

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ interface Props extends IconVariant {
99
description: string | React.ReactNode;
1010
className?: string;
1111
margin?: string;
12+
truncate?: boolean;
1213
}
1314
export default function ModalHeader({
1415
icon,
@@ -17,14 +18,19 @@ export default function ModalHeader({
1718
color = "netbird",
1819
className = "pb-6 px-8",
1920
margin = "mt-0",
21+
truncate = false,
2022
}: Props) {
2123
return (
22-
<div className={className}>
23-
<div className={"flex items-start gap-5 pr-10"}>
24+
<div className={cn(className, "min-w-0")}>
25+
<div className={"flex items-start gap-5 pr-10 min-w-0"}>
2426
{icon && <SquareIcon color={color} icon={icon} />}
25-
<div>
27+
<div className={"min-w-0"}>
2628
<h2 className={"text-lg my-0 leading-[1.5]"}>{title}</h2>
27-
<Paragraph className={cn("text-sm", margin)}>{description}</Paragraph>
29+
<Paragraph
30+
className={cn("text-sm", margin, truncate && "!block truncate")}
31+
>
32+
{description}
33+
</Paragraph>
2834
</div>
2935
</div>
3036
</div>

src/components/ui/TextWithTooltip.tsx

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,21 @@ export default function TextWithTooltip({
2424
<FullTooltip
2525
disabled={charCount <= maxChars || hideTooltip}
2626
interactive={false}
27-
className={"truncate w-full"}
27+
className={"truncate w-full min-w-0"}
2828
content={
29-
<div className={"max-w-xs break-all whitespace-normal"}>{text}</div>
29+
<div className={"max-w-xs break-all whitespace-normal text-xs"}>
30+
{text}
31+
</div>
3032
}
3133
>
32-
<span className={cn(className, "truncate")}>
33-
{charCount > maxChars ? text && `${text.slice(0, maxChars)}...` : text}
34-
</span>
34+
<div
35+
className={"w-full min-w-0 inline-block"}
36+
style={{
37+
maxWidth: `${maxChars - 2}ch`,
38+
}}
39+
>
40+
<div className={cn(className, "truncate")}>{text}</div>
41+
</div>
3542
</FullTooltip>
3643
);
3744
}

src/modules/common-table-rows/ActiveInactiveRow.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ type Props = {
1010
leftSection?: React.ReactNode;
1111
text?: string | React.ReactNode;
1212
className?: string;
13+
additionalInfo?: React.ReactNode;
1314
};
1415
export default function ActiveInactiveRow({
1516
active,
@@ -18,11 +19,12 @@ export default function ActiveInactiveRow({
1819
leftSection,
1920
inactiveDot = "gray",
2021
className,
22+
additionalInfo,
2123
}: Props) {
2224
return (
2325
<div
2426
className={cn(
25-
"flex gap-3 dark:text-neutral-300 text-neutral-500 min-w-[250px] max-w-[250px]",
27+
"gap-3 dark:text-neutral-300 text-neutral-500 min-w-0",
2628
className,
2729
)}
2830
>
@@ -34,9 +36,12 @@ export default function ActiveInactiveRow({
3436
inactiveDot={inactiveDot}
3537
className={"mt-1 shrink-0"}
3638
/>
37-
<div className={"flex flex-col"}>
38-
<div className={" font-medium"}>
39+
<div className={"flex flex-col min-w-0"}>
40+
<div
41+
className={"font-medium flex gap-2 items-center justify-center"}
42+
>
3943
<TextWithTooltip text={text as string} maxChars={25} />
44+
{additionalInfo}
4045
</div>
4146
{children}
4247
</div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import Button from "@components/Button";
2+
import { Modal } from "@components/modal/Modal";
3+
import { IconCirclePlus, IconDirectionSign } from "@tabler/icons-react";
4+
import * as React from "react";
5+
import { useState } from "react";
6+
import { Peer } from "@/interfaces/Peer";
7+
import { ExitNodeHelpTooltip } from "@/modules/exit-node/ExitNodeHelpTooltip";
8+
import { RouteModalContent } from "@/modules/routes/RouteModal";
9+
10+
type Props = {
11+
peer?: Peer;
12+
firstTime?: boolean;
13+
};
14+
export const AddExitNodeButton = ({ peer, firstTime = false }: Props) => {
15+
const [modal, setModal] = useState(false);
16+
17+
return (
18+
<>
19+
<ExitNodeHelpTooltip>
20+
<Button variant={"secondary"} onClick={() => setModal(true)}>
21+
{!firstTime ? (
22+
<>
23+
<IconCirclePlus size={16} />
24+
Add Exit Node
25+
</>
26+
) : (
27+
<>
28+
<IconDirectionSign size={16} className={"text-yellow-400"} />
29+
Setup Exit Node
30+
</>
31+
)}
32+
</Button>
33+
</ExitNodeHelpTooltip>
34+
<Modal open={modal} onOpenChange={setModal}>
35+
{modal && (
36+
<RouteModalContent
37+
onSuccess={() => setModal(false)}
38+
peer={peer}
39+
isFirstExitNode={firstTime}
40+
exitNode={true}
41+
/>
42+
)}
43+
</Modal>
44+
</>
45+
);
46+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { DropdownMenuItem } from "@components/DropdownMenu";
2+
import { Modal } from "@components/modal/Modal";
3+
import { getOperatingSystem } from "@hooks/useOperatingSystem";
4+
import { IconDirectionSign } from "@tabler/icons-react";
5+
import * as React from "react";
6+
import { useState } from "react";
7+
import { OperatingSystem } from "@/interfaces/OperatingSystem";
8+
import { Peer } from "@/interfaces/Peer";
9+
import { RouteModalContent } from "@/modules/routes/RouteModal";
10+
11+
type Props = {
12+
peer: Peer;
13+
};
14+
15+
export const ExitNodeDropdownButton = ({ peer }: Props) => {
16+
const [modal, setModal] = useState(false);
17+
const isLinux = getOperatingSystem(peer.os) === OperatingSystem.LINUX;
18+
19+
return isLinux ? (
20+
<>
21+
<DropdownMenuItem onClick={() => setModal(true)}>
22+
<div className={"flex gap-3 items-center w-full"}>
23+
<IconDirectionSign size={14} className={"shrink-0"} />
24+
<div className={"flex justify-between items-center w-full"}>
25+
Add Exit Node
26+
</div>
27+
</div>
28+
</DropdownMenuItem>
29+
<Modal open={modal} onOpenChange={setModal}>
30+
{modal && (
31+
<RouteModalContent
32+
onSuccess={() => setModal(false)}
33+
peer={peer}
34+
exitNode={true}
35+
/>
36+
)}
37+
</Modal>
38+
</>
39+
) : null;
40+
};
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import FullTooltip from "@components/FullTooltip";
2+
import InlineLink from "@components/InlineLink";
3+
import { ExternalLinkIcon } from "lucide-react";
4+
import * as React from "react";
5+
6+
type Props = {
7+
children: React.ReactNode;
8+
hoverButton?: boolean;
9+
};
10+
export const ExitNodeHelpTooltip = ({
11+
children,
12+
hoverButton = false,
13+
}: Props) => {
14+
return (
15+
<FullTooltip
16+
hoverButton={hoverButton}
17+
content={
18+
<div className={"text-xs max-w-xs"}>
19+
An exit node is a network route that routes all your internet traffic
20+
through one of your peers.
21+
<div className={"mt-2"}>
22+
Learn more about{" "}
23+
<InlineLink
24+
href={
25+
"https://docs.netbird.io/how-to/configuring-default-routes-for-internet-traffic"
26+
}
27+
target={"_blank"}
28+
className={"mr-1"}
29+
>
30+
Exit Nodes
31+
<ExternalLinkIcon size={10} />
32+
</InlineLink>
33+
in our documentation.
34+
</div>
35+
</div>
36+
}
37+
>
38+
{children}
39+
</FullTooltip>
40+
);
41+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import FullTooltip from "@components/FullTooltip";
2+
import { IconDirectionSign } from "@tabler/icons-react";
3+
import * as React from "react";
4+
import { Peer } from "@/interfaces/Peer";
5+
import { useHasExitNodes } from "@/modules/exit-node/useHasExitNodes";
6+
7+
type Props = {
8+
peer: Peer;
9+
};
10+
export const ExitNodePeerIndicator = ({ peer }: Props) => {
11+
const hasExitNode = useHasExitNodes(peer);
12+
13+
return hasExitNode ? (
14+
<FullTooltip
15+
content={
16+
<div className={"text-xs max-w-xs"}>
17+
This peer has an exit node. Traffic from the configured distribution
18+
groups will be routed through this peer.
19+
</div>
20+
}
21+
>
22+
<IconDirectionSign size={15} className={"text-yellow-400 shrink-0"} />
23+
</FullTooltip>
24+
) : null;
25+
};

0 commit comments

Comments
 (0)