Compare commits

...

4 Commits

Author SHA1 Message Date
Eduard Gert
43bc069a49 Increase ssh detection timeout (#512)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2025-11-21 10:32:50 +01:00
Eduard Gert
936de0f4f3 Add ssh policy info for peers (#511)
Some checks failed
build and push / build_n_push (push) Has been cancelled
2025-11-20 14:29:14 +01:00
Eduard Gert
d81b75a946 Bump browser ssh versions for ssh rewrite (#510)
Some checks failed
build and push / build_n_push (push) Has been cancelled
* Bump browser ssh versions for ssh rewrite

* Remove cypress temporary
2025-11-18 17:07:58 +01:00
Eduard Gert
a632eeeef0 Remove dns0eu (#508) 2025-11-10 14:21:58 +01:00
21 changed files with 369 additions and 141 deletions

View File

@@ -91,7 +91,6 @@
"@types/chroma-js": "^3.1.1",
"@types/js-cookie": "^3.0.6",
"eslint-config-next": "^14.2.28",
"cypress": "^13.13.0",
"postcss": "^8",
"prettier": "3.0.3",
"tailwindcss": "^3"

View File

@@ -108,6 +108,7 @@ function SSHTerminal({ username, port, peer }: Props) {
if (!peer.id) return;
if (connected.current) return;
connected.current = true;
try {
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
const rules = [`tcp/${aclPort}`];
@@ -121,7 +122,7 @@ function SSHTerminal({ username, port, peer }: Props) {
sshConnectedOnce.current = true;
}
} catch (error) {
console.error("Connection failed:", error);
console.error("Connection error:", error);
}
};

View File

@@ -1,12 +0,0 @@
<svg width="573" height="148" viewBox="0 0 573 148" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_4)">
<path d="M0.739014 125.602V33.09H28.239C34.491 33.09 39.919 34.274 44.524 36.638C49.128 39.004 52.698 42.341 55.233 46.65C57.767 50.958 59.034 56.071 59.034 61.984V96.58C59.034 102.41 57.767 107.5 55.233 111.85C52.698 116.203 49.128 119.581 44.523 121.99C39.918 124.397 34.491 125.601 28.239 125.601L0.739014 125.602ZM16.58 111.028H28.24C32.801 111.028 36.433 109.741 39.138 107.163C41.841 104.587 43.193 101.06 43.193 96.581V61.984C43.193 57.592 41.841 54.107 39.138 51.529C36.433 48.952 32.801 47.663 28.239 47.663H16.58V111.028ZM76.396 125.602V33.09H95.786L121.512 107.86C121.216 104.673 120.942 101.483 120.688 98.292C120.386 94.5366 120.154 90.776 119.991 87.012C119.821 83.169 119.738 79.812 119.738 76.938V33.09H134.185V125.602H114.795L89.323 50.832C89.491 53.283 89.703 56.239 89.956 59.702C90.2137 63.2478 90.4251 66.7967 90.59 70.348C90.758 73.982 90.844 77.318 90.844 80.36V125.602H76.396ZM181.582 126.869C175.245 126.869 169.752 125.812 165.107 123.701C160.46 121.591 156.889 118.569 154.398 114.64C151.905 110.711 150.616 106.086 150.533 100.763H166.374C166.374 104.565 167.747 107.543 170.494 109.698C173.238 111.852 176.976 112.929 181.709 112.929C186.271 112.929 189.839 111.874 192.417 109.761C194.993 107.65 196.282 104.735 196.282 101.017C196.282 97.892 195.374 95.167 193.558 92.842C191.74 90.52 189.142 88.936 185.764 88.09L175.119 85.175C167.852 83.318 162.256 79.979 158.327 75.164C154.398 70.348 152.434 64.518 152.434 57.675C152.434 52.438 153.616 47.875 155.982 43.988C158.347 40.103 161.705 37.103 166.058 34.99C170.408 32.88 175.54 31.822 181.455 31.822C190.409 31.822 197.506 34.125 202.745 38.729C207.983 43.335 210.645 49.523 210.73 57.295H194.888C194.888 53.663 193.704 50.812 191.34 48.741C188.974 46.671 185.637 45.636 181.328 45.636C177.188 45.636 173.978 46.608 171.697 48.551C169.416 50.495 168.275 53.24 168.275 56.788C168.275 60 169.141 62.724 170.873 64.962C172.603 67.202 175.119 68.786 178.413 69.714L189.439 72.756C196.789 74.616 202.407 77.932 206.294 82.704C210.179 87.478 212.124 93.371 212.124 100.383C212.124 105.623 210.856 110.248 208.322 114.26C205.787 118.273 202.239 121.378 197.677 123.574C193.114 125.77 187.748 126.869 181.582 126.869ZM257.366 126.869C251.366 126.869 246.17 125.729 241.778 123.448C237.384 121.167 233.985 117.957 231.577 113.816C229.169 109.678 227.965 104.818 227.965 99.242V59.45C227.965 53.874 229.169 49.017 231.577 44.876C233.984 40.738 237.384 37.526 241.778 35.245C246.17 32.964 251.366 31.823 257.366 31.823C263.449 31.823 268.665 32.963 273.017 35.245C277.367 37.526 280.747 40.738 283.156 44.876C285.563 49.016 286.767 53.874 286.767 59.45V99.243C286.767 104.819 285.563 109.679 283.156 113.817C280.748 117.957 277.346 121.167 272.954 123.449C268.56 125.729 263.364 126.869 257.366 126.869ZM257.366 113.183C261.758 113.183 265.265 111.915 267.885 109.381C270.502 106.846 271.813 103.468 271.813 99.242V59.45C271.813 55.227 270.503 51.846 267.885 49.312C265.265 46.777 261.758 45.51 257.366 45.51C252.972 45.51 249.466 46.777 246.848 49.312C244.228 51.846 242.918 55.227 242.918 59.45V99.243C242.918 103.469 244.228 106.847 246.848 109.382C249.465 111.916 252.972 113.183 257.366 113.183ZM257.366 87.583C254.915 87.583 252.909 86.781 251.346 85.175C249.782 83.571 249.002 81.5 249.002 78.965C249.002 76.431 249.762 74.403 251.283 72.883C252.803 71.362 254.831 70.601 257.366 70.601C259.901 70.601 261.928 71.361 263.449 72.883C264.969 74.403 265.73 76.431 265.73 78.966C265.73 81.5 264.97 83.571 263.45 85.176C261.928 86.781 259.9 87.583 257.366 87.583Z" fill="black"/>
<path d="M332.69 126.999C329.057 126.999 326.164 125.941 324.01 123.831C321.855 121.72 320.778 118.847 320.778 115.213C320.778 111.581 321.855 108.686 324.01 106.532C326.164 104.378 329.057 103.3 332.69 103.3C336.322 103.3 339.217 104.378 341.372 106.532C343.526 108.686 344.603 111.582 344.603 115.213C344.603 118.847 343.526 121.72 341.372 123.831C339.217 125.941 336.322 126.999 332.691 126.999H332.69ZM381.862 125.732V33.219H437.369V47.032H397.449V71.112H432.934V84.798H397.45V111.918H437.37V125.732H381.862ZM484.766 126.999C475.725 126.999 468.65 124.528 463.539 119.585C458.426 114.643 455.872 107.906 455.872 99.372V33.219H471.84V99.245C471.84 103.639 472.937 107.061 475.135 109.51C477.331 111.962 480.54 113.185 484.766 113.185C488.905 113.185 492.095 111.962 494.334 109.51C496.572 107.06 497.693 103.639 497.693 99.245V33.219H513.66V99.372C513.66 107.906 511.126 114.642 506.057 119.585C500.987 124.528 493.891 126.999 484.767 126.999H484.766Z" fill="#686868"/>
<path d="M549 0H573V148H549V0Z" fill="#FFCC03"/>
</g>
<defs>
<clipPath id="clip0_4_4">
<rect width="573" height="148" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -1,12 +0,0 @@
<svg width="573" height="148" viewBox="0 0 573 148" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_4_4)">
<path d="M0.739014 125.602V33.09H28.239C34.491 33.09 39.919 34.274 44.524 36.638C49.128 39.004 52.698 42.341 55.233 46.65C57.767 50.958 59.034 56.071 59.034 61.984V96.58C59.034 102.41 57.767 107.5 55.233 111.85C52.698 116.203 49.128 119.581 44.523 121.99C39.918 124.397 34.491 125.601 28.239 125.601L0.739014 125.602ZM16.58 111.028H28.24C32.801 111.028 36.433 109.741 39.138 107.163C41.841 104.587 43.193 101.06 43.193 96.581V61.984C43.193 57.592 41.841 54.107 39.138 51.529C36.433 48.952 32.801 47.663 28.239 47.663H16.58V111.028ZM76.396 125.602V33.09H95.786L121.512 107.86C121.216 104.673 120.942 101.483 120.688 98.292C120.386 94.5366 120.154 90.776 119.991 87.012C119.821 83.169 119.738 79.812 119.738 76.938V33.09H134.185V125.602H114.795L89.323 50.832C89.491 53.283 89.703 56.239 89.956 59.702C90.2137 63.2478 90.4251 66.7967 90.59 70.348C90.758 73.982 90.844 77.318 90.844 80.36V125.602H76.396ZM181.582 126.869C175.245 126.869 169.752 125.812 165.107 123.701C160.46 121.591 156.889 118.569 154.398 114.64C151.905 110.711 150.616 106.086 150.533 100.763H166.374C166.374 104.565 167.747 107.543 170.494 109.698C173.238 111.852 176.976 112.929 181.709 112.929C186.271 112.929 189.839 111.874 192.417 109.761C194.993 107.65 196.282 104.735 196.282 101.017C196.282 97.892 195.374 95.167 193.558 92.842C191.74 90.52 189.142 88.936 185.764 88.09L175.119 85.175C167.852 83.318 162.256 79.979 158.327 75.164C154.398 70.348 152.434 64.518 152.434 57.675C152.434 52.438 153.616 47.875 155.982 43.988C158.347 40.103 161.705 37.103 166.058 34.99C170.408 32.88 175.54 31.822 181.455 31.822C190.409 31.822 197.506 34.125 202.745 38.729C207.983 43.335 210.645 49.523 210.73 57.295H194.888C194.888 53.663 193.704 50.812 191.34 48.741C188.974 46.671 185.637 45.636 181.328 45.636C177.188 45.636 173.978 46.608 171.697 48.551C169.416 50.495 168.275 53.24 168.275 56.788C168.275 60 169.141 62.724 170.873 64.962C172.603 67.202 175.119 68.786 178.413 69.714L189.439 72.756C196.789 74.616 202.407 77.932 206.294 82.704C210.179 87.478 212.124 93.371 212.124 100.383C212.124 105.623 210.856 110.248 208.322 114.26C205.787 118.273 202.239 121.378 197.677 123.574C193.114 125.77 187.748 126.869 181.582 126.869ZM257.366 126.869C251.366 126.869 246.17 125.729 241.778 123.448C237.384 121.167 233.985 117.957 231.577 113.816C229.169 109.678 227.965 104.818 227.965 99.242V59.45C227.965 53.874 229.169 49.017 231.577 44.876C233.984 40.738 237.384 37.526 241.778 35.245C246.17 32.964 251.366 31.823 257.366 31.823C263.449 31.823 268.665 32.963 273.017 35.245C277.367 37.526 280.747 40.738 283.156 44.876C285.563 49.016 286.767 53.874 286.767 59.45V99.243C286.767 104.819 285.563 109.679 283.156 113.817C280.748 117.957 277.346 121.167 272.954 123.449C268.56 125.729 263.364 126.869 257.366 126.869ZM257.366 113.183C261.758 113.183 265.265 111.915 267.885 109.381C270.502 106.846 271.813 103.468 271.813 99.242V59.45C271.813 55.227 270.503 51.846 267.885 49.312C265.265 46.777 261.758 45.51 257.366 45.51C252.972 45.51 249.466 46.777 246.848 49.312C244.228 51.846 242.918 55.227 242.918 59.45V99.243C242.918 103.469 244.228 106.847 246.848 109.382C249.465 111.916 252.972 113.183 257.366 113.183ZM257.366 87.583C254.915 87.583 252.909 86.781 251.346 85.175C249.782 83.571 249.002 81.5 249.002 78.965C249.002 76.431 249.762 74.403 251.283 72.883C252.803 71.362 254.831 70.601 257.366 70.601C259.901 70.601 261.928 71.361 263.449 72.883C264.969 74.403 265.73 76.431 265.73 78.966C265.73 81.5 264.97 83.571 263.45 85.176C261.928 86.781 259.9 87.583 257.366 87.583Z" fill="black"/>
<path d="M332.69 126.999C329.057 126.999 326.164 125.941 324.01 123.831C321.855 121.72 320.778 118.847 320.778 115.213C320.778 111.581 321.855 108.686 324.01 106.532C326.164 104.378 329.057 103.3 332.69 103.3C336.322 103.3 339.217 104.378 341.372 106.532C343.526 108.686 344.603 111.582 344.603 115.213C344.603 118.847 343.526 121.72 341.372 123.831C339.217 125.941 336.322 126.999 332.691 126.999H332.69ZM381.862 125.732V33.219H437.369V47.032H397.449V71.112H432.934V84.798H397.45V111.918H437.37V125.732H381.862ZM484.766 126.999C475.725 126.999 468.65 124.528 463.539 119.585C458.426 114.643 455.872 107.906 455.872 99.372V33.219H471.84V99.245C471.84 103.639 472.937 107.061 475.135 109.51C477.331 111.962 480.54 113.185 484.766 113.185C488.905 113.185 492.095 111.962 494.334 109.51C496.572 107.06 497.693 103.639 497.693 99.245V33.219H513.66V99.372C513.66 107.906 511.126 114.642 506.057 119.585C500.987 124.528 493.891 126.999 484.767 126.999H484.766Z" fill="#359CEF"/>
<path d="M549 0H573V148H549V0Z" fill="#FFCC03"/>
</g>
<defs>
<clipPath id="clip0_4_4">
<rect width="573" height="148" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@@ -4,7 +4,18 @@ import md5 from "crypto-js/md5";
import React, { useEffect, useState } from "react";
import { usePermissions } from "@/contexts/PermissionsProvider";
const initialAnnouncements: Announcement[] = [];
const initialAnnouncements: Announcement[] = [
{
tag: "New",
text: "NetBird v0.60.0 - Identity-aware, private SSH over your NetBird network.",
link: "https://docs.netbird.io/how-to/ssh",
linkText: "Documentation",
variant: "default", // "default" or "important"
isExternal: true,
closeable: true,
isCloudOnly: false,
},
];
export interface Announcement extends AnnouncementVariant {
tag: string;

View File

@@ -138,6 +138,7 @@ export default function PeerProvider({
<PeerSSHInstructions
open={sshInstructionsModal}
onOpenChange={setSSHInstructionsModal}
peer={peer}
onSuccess={() => toggleSSH(true)}
/>
)}

View File

@@ -104,50 +104,4 @@ export const NameserverPresets: Record<string, NameserverGroup> = {
enabled: true,
search_domains_enabled: false,
},
DNS0: {
name: "DNS0.EU",
description: "DNS0.EU DNS Servers",
primary: true,
domains: [],
nameservers: [
{
ip: "193.110.81.0",
ns_type: "udp",
port: 53,
id: "1",
},
{
ip: "185.253.5.0",
ns_type: "udp",
port: 53,
id: "2",
},
],
groups: [],
enabled: true,
search_domains_enabled: false,
},
DNS0Zero: {
name: "DNS0.EU Zero",
description: "DNS0.EU Zero DNS Servers",
primary: true,
domains: [],
nameservers: [
{
ip: "193.110.81.9",
ns_type: "udp",
port: 53,
id: "1",
},
{
ip: "185.253.5.9",
ns_type: "udp",
port: 53,
id: "2",
},
],
groups: [],
enabled: true,
search_domains_enabled: false,
},
};

View File

@@ -45,7 +45,7 @@ import React, { useMemo, useState } from "react";
import AccessControlIcon from "@/assets/icons/AccessControlIcon";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { Group } from "@/interfaces/Group";
import { Policy, Protocol } from "@/interfaces/Policy";
import { Policy, PolicyRuleResource, Protocol } from "@/interfaces/Policy";
import { PostureCheck } from "@/interfaces/PostureCheck";
import { useAccessControl } from "@/modules/access-control/useAccessControl";
import { PostureCheckTab } from "@/modules/posture-checks/ui/PostureCheckTab";
@@ -116,6 +116,9 @@ type ModalProps = {
postureCheckTemplates?: PostureCheck[];
useSave?: boolean;
allowEditPeers?: boolean;
initialProtocol?: Protocol;
initialPorts?: number[];
initialDestinationResource?: PolicyRuleResource;
};
export function AccessControlModalContent({
@@ -128,6 +131,9 @@ export function AccessControlModalContent({
initialDestinationGroups,
initialName,
initialDescription,
initialProtocol,
initialPorts,
initialDestinationResource,
}: Readonly<ModalProps>) {
const { permission } = usePermissions();
@@ -170,6 +176,9 @@ export function AccessControlModalContent({
initialDestinationGroups,
initialName,
initialDescription,
initialPorts,
initialProtocol,
initialDestinationResource,
});
const [tab, setTab] = useState(() => {

View File

@@ -6,7 +6,12 @@ import { useEffect, useMemo, useRef, useState } from "react";
import { useSWRConfig } from "swr";
import { usePolicies } from "@/contexts/PoliciesProvider";
import { Group } from "@/interfaces/Group";
import { Policy, PortRange, Protocol } from "@/interfaces/Policy";
import {
Policy,
PolicyRuleResource,
PortRange,
Protocol,
} from "@/interfaces/Policy";
import { PostureCheck } from "@/interfaces/PostureCheck";
import useGroupHelper from "@/modules/groups/useGroupHelper";
import { usePostureCheck } from "@/modules/posture-checks/usePostureCheck";
@@ -18,6 +23,9 @@ type Props = {
initialDestinationGroups?: Group[] | string[];
initialName?: string;
initialDescription?: string;
initialProtocol?: Protocol;
initialPorts?: number[];
initialDestinationResource?: PolicyRuleResource;
};
// TODO add reducer
@@ -29,6 +37,9 @@ export const useAccessControl = ({
initialName,
initialDescription,
onSuccess,
initialProtocol,
initialPorts,
initialDestinationResource,
}: Props = {}) => {
const { data: allPostureChecks, isLoading: isPostureChecksLoading } =
useFetchApi<PostureCheck[]>("/posture-checks");
@@ -75,6 +86,7 @@ export const useAccessControl = ({
const [enabled, setEnabled] = useState<boolean>(policy?.enabled ?? true);
const [ports, setPorts] = useState<number[]>(() => {
if (initialPorts) return initialPorts;
if (!firstRule) return [];
if (firstRule.ports == undefined) return [];
if (firstRule.ports.length > 0) {
@@ -93,7 +105,7 @@ export const useAccessControl = ({
});
const [protocol, setProtocol] = useState<Protocol>(
firstRule ? firstRule.protocol : "all",
firstRule ? firstRule.protocol : initialProtocol ?? "all",
);
const [direction, setDirection] = useState<Direction>(() => {
if (!firstRule) return "bi";
@@ -131,7 +143,7 @@ export const useAccessControl = ({
);
const [destinationResource, setDestinationResource] = useState(
firstRule?.destinationResource,
firstRule?.destinationResource ?? initialDestinationResource,
);
const { updateOrCreateAndNotify: checkToCreate } = usePostureCheck({});

View File

@@ -5,8 +5,6 @@ import { ExternalLinkIcon, GlobeIcon } from "lucide-react";
import Image, { StaticImageData } from "next/image";
import React, { useState } from "react";
import CloudflareLogo from "@/assets/nameservers/cloudflare.svg";
import DNS0Logo from "@/assets/nameservers/dns0.svg";
import DNS0ZeroLogo from "@/assets/nameservers/dns0-zero.svg";
import GoogleLogo from "@/assets/nameservers/google.svg";
import Quad9Logo from "@/assets/nameservers/quad9.svg";
import { Group } from "@/interfaces/Group";
@@ -66,9 +64,9 @@ export function NameserverTemplateModalContent({
onePresetSelection,
}: Readonly<ModalProps>) {
return (
<ModalContent maxWidthClass={"max-w-5xl"} showClose={true}>
<ModalContent maxWidthClass={"max-w-xl"} showClose={true}>
<div className={"px-8 py-3 flex flex-col gap-6 mt-4"}>
<div className={"grid grid-cols-1 md:grid-cols-2 gap-4"}>
<div className={"grid grid-cols-1 md:grid-cols-1 gap-4"}>
<NameserverTemplate
onClick={() => onePresetSelection(NameserverPresets.Google)}
src={GoogleLogo}
@@ -87,25 +85,6 @@ export function NameserverTemplateModalContent({
}
href={"https://www.cloudflare.com/learning/dns/what-is-1.1.1.1/"}
/>
<NameserverTemplate
onClick={() => onePresetSelection(NameserverPresets.DNS0)}
src={DNS0Logo}
title={"DNS0.EU DNS"}
description={
"A free, sovereign and GDPR-compliant DNS resolver with a strong focus on security to protect the citizens and organizations of the European Union."
}
href={"https://www.dns0.eu/"}
/>
<NameserverTemplate
onClick={() => onePresetSelection(NameserverPresets.DNS0Zero)}
src={DNS0ZeroLogo}
title={"DNS0.EU Zero DNS"}
description={
"Increase the catch rate for malicious domains by combining human-vetted threat intelligence with advanced heuristics that automatically identify high-risk patterns."
}
href={"https://www.dns0.eu/zero"}
/>
<NameserverTemplate
onClick={() => onePresetSelection(NameserverPresets.Quad9)}
src={Quad9Logo}

View File

@@ -9,26 +9,37 @@ import {
} from "@components/modal/Modal";
import ModalHeader from "@components/modal/ModalHeader";
import Paragraph from "@components/Paragraph";
import { SegmentedTabs } from "@components/SegmentedTabs";
import Separator from "@components/Separator";
import Steps from "@components/Steps";
import { Lightbox } from "@components/ui/Lightbox";
import { Mark } from "@components/ui/Mark";
import { cn } from "@utils/helpers";
import { ExternalLinkIcon, TerminalSquare } from "lucide-react";
import { ExternalLinkIcon, PlusCircle, TerminalSquare } from "lucide-react";
import * as React from "react";
import { useState } from "react";
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
import sshImage from "@/assets/ssh/ssh-client.png";
import { Peer } from "@/interfaces/Peer";
import { PeerSSHPolicyModal } from "@/modules/peer/PeerSSHPolicyModal";
import { Terminal } from "@/modules/remote-access/ssh/Terminal";
type Props = {
open?: boolean;
onOpenChange?: (open: boolean) => void;
onSuccess?: () => void;
peer?: Peer;
};
export const PeerSSHInstructions = ({
open,
onOpenChange,
onSuccess,
peer,
}: Props) => {
const [client, setClient] = useState("cli");
const [policyModal, setPolicyModal] = useState(false);
return (
<Modal open={open} onOpenChange={onOpenChange}>
<ModalContent
@@ -39,36 +50,70 @@ export const PeerSSHInstructions = ({
icon={<TerminalSquare size={16} className={"text-netbird"} />}
title={"Enable SSH Access"}
description={
"Allow remote SSH access to this machine from other connected network participants. NetBird's embedded SSH server is running on port 44338."
"Allow remote SSH access from other connected network participants."
}
color={"netbird"}
/>
<Separator />
<div className={"px-8 py-3 flex flex-col gap-0 z-0"}>
<div className={"px-8 py-3 flex flex-col gap-0 z-0 mt-1"}>
<SegmentedTabs value={client} onChange={setClient}>
<SegmentedTabs.List className={"rounded-lg border"}>
<SegmentedTabs.Trigger value={"cli"}>
<TerminalSquare size={16} />
CLI
</SegmentedTabs.Trigger>
<SegmentedTabs.Trigger value={"gui"}>
<NetBirdIcon size={16} />
Desktop Client
</SegmentedTabs.Trigger>
</SegmentedTabs.List>
</SegmentedTabs>
<Steps>
<Steps.Step step={1}>
<p className={"font-normal"}>
If you are using NetBird via CLI, you can enable SSH by running
</p>
<Code codeToCopy={"netbird down"}>
<Code.Line>{`netbird down # if NetBird is already running`}</Code.Line>
</Code>
<Code>
<Code.Line>{`netbird up --allow-server-ssh`}</Code.Line>
</Code>
</Steps.Step>
{client === "cli" ? (
<Steps.Step step={1}>
<p className={"font-normal"}>
If you are using NetBird via CLI, you can enable SSH by
running
</p>
<Code codeToCopy={"netbird down"}>
<Code.Line>{`netbird down # if NetBird is already running`}</Code.Line>
</Code>
<Code>
<Code.Line>{`netbird up --allow-server-ssh --enable-ssh-root`}</Code.Line>
</Code>
</Steps.Step>
) : (
<Steps.Step step={1}>
<p className={"font-normal"}>
If you are using NetBird via the Desktop Client, click on the
NetBird tray icon, go to <Mark>Settings</Mark> and click{" "}
<Mark>Allow SSH</Mark>. If you want to enable Root Login go to{" "}
<Mark>Settings &gt; Advanced Settings</Mark> and enable SSH
Root Login under the SSH tab.
</p>
<Lightbox image={sshImage} />
</Steps.Step>
)}
<Steps.Step step={2}>
<p className={"font-normal"}>
If you are using NetBird via the Desktop Client, click on the
NetBird tray icon, go to <Mark>Settings</Mark> and click{" "}
<Mark>Allow SSH</Mark> <br />
Starting from NetBird v0.60.0, SSH requires an explicit access
control policy that allows <Mark>TCP</Mark> traffic on port{" "}
<Mark>22</Mark>
</p>
<Lightbox image={sshImage} />
<div className={"mt-2"}>
<Button
variant={"secondary"}
onClick={() => setPolicyModal(true)}
>
<PlusCircle size={16} />
Create SSH Policy
</Button>
</div>
</Steps.Step>
<Steps.Step step={3} line={false}>
<p className={"font-normal"}>
Once the NetBird SSH server is allowed on the client, <br />
@@ -96,15 +141,17 @@ export const PeerSSHInstructions = ({
<Button variant={"secondary"}>Cancel</Button>
</ModalClose>
<Button
variant={"primary"}
onClick={onSuccess}
data-cy={"create-setup-key"}
>
<Button variant={"primary"} onClick={onSuccess}>
Confirm & Enable
</Button>
</div>
</ModalFooter>
<PeerSSHPolicyModal
open={policyModal}
onOpenChange={setPolicyModal}
peer={peer}
/>
</ModalContent>
</Modal>
);

View File

@@ -0,0 +1,38 @@
import { Callout } from "@components/Callout";
import { InlineButtonLink } from "@components/InlineLink";
import { cn } from "@utils/helpers";
import * as React from "react";
import { useState } from "react";
import { Peer } from "@/interfaces/Peer";
import { PeerSSHPolicyModal } from "@/modules/peer/PeerSSHPolicyModal";
import { usePeerSSHPolicyCheck } from "@/modules/peer/usePeerSSHPolicyCheck";
type Props = {
peer?: Peer;
className?: string;
};
export const PeerSSHPolicyInfo = ({ peer, className }: Props) => {
const { showSSHPolicyInfo } = usePeerSSHPolicyCheck(peer);
const [policyModal, setPolicyModal] = useState(false);
return (
showSSHPolicyInfo && (
<>
<Callout className={cn("max-w-xl", className)} variant={"warning"}>
<span>
Starting from NetBird v0.60.0, SSH requires an explicit access
control policy that allows TCP traffic on port 22.{" "}
<InlineButtonLink onClick={() => setPolicyModal(true)}>
Create SSH Policy
</InlineButtonLink>
</span>
</Callout>
<PeerSSHPolicyModal
open={policyModal}
onOpenChange={setPolicyModal}
peer={peer}
/>
</>
)
);
};

View File

@@ -0,0 +1,35 @@
import { Modal } from "@components/modal/Modal";
import * as React from "react";
import { Peer } from "@/interfaces/Peer";
import { PolicyRuleResource } from "@/interfaces/Policy";
import { AccessControlModalContent } from "@/modules/access-control/AccessControlModal";
type Props = {
open: boolean;
onOpenChange: (open: boolean) => void;
peer?: Peer;
};
export const PeerSSHPolicyModal = ({ open, onOpenChange, peer }: Props) => {
return (
<Modal open={open} onOpenChange={onOpenChange}>
<AccessControlModalContent
key={open ? "1" : "0"}
initialPorts={[22]}
initialProtocol={"tcp"}
initialName={"SSH Access"}
initialDestinationResource={
peer
? ({
id: peer.id,
type: "peer",
} as PolicyRuleResource)
: undefined
}
onSuccess={async (p) => {
onOpenChange(false);
}}
/>
</Modal>
);
};

View File

@@ -4,6 +4,7 @@ import { LockIcon, TerminalSquare } from "lucide-react";
import * as React from "react";
import { usePeer } from "@/contexts/PeerProvider";
import { usePermissions } from "@/contexts/PermissionsProvider";
import { PeerSSHPolicyInfo } from "@/modules/peer/PeerSSHPolicyInfo";
export const PeerSSHToggle = () => {
const { permission } = usePermissions();
@@ -42,6 +43,7 @@ export const PeerSSHToggle = () => {
}
/>
</FullTooltip>
<PeerSSHPolicyInfo peer={peer} />
</>
);
};

View File

@@ -0,0 +1,77 @@
import useFetchApi from "@utils/api";
import { Peer } from "@/interfaces/Peer";
import { Policy } from "@/interfaces/Policy";
import { isNativeSSHSupported } from "@utils/version";
export const usePeerSSHPolicyCheck = (peer?: Peer) => {
const { data: policies, isLoading } = useFetchApi<Policy[]>(
"/policies",
true,
false,
);
const peerGroupIds = peer?.groups?.map((p) => p.id);
const peerPolicies = policies?.filter((policy) => {
// Skip disabled policies
if (!policy?.enabled) return false;
const rule = policy?.rules?.[0];
if (!rule) return false;
// Skip icmp and udp
if (rule.protocol === "icmp" || rule.protocol === "udp") return false;
// Check resource and groups
const isPeerInDestinationResource =
rule.destinationResource?.id === peer?.id;
const isPeerInDestinationGroup =
rule.destinations?.some((group) => {
const groupId = typeof group === "string" ? group : group?.id;
return peerGroupIds?.includes(groupId);
}) ?? false;
const isPeerInDestination =
isPeerInDestinationResource || isPeerInDestinationGroup;
// If bidirectional, also check if peer is in source
let isPeerInSource = false;
if (rule.bidirectional) {
const isPeerInSourceResource = rule.sourceResource?.id === peer?.id;
const isPeerInSourceGroup =
rule.sources?.some((group) => {
const groupId = typeof group === "string" ? group : group?.id;
return peerGroupIds?.includes(groupId);
}) ?? false;
isPeerInSource = isPeerInSourceResource || isPeerInSourceGroup;
}
const isInSourceOrDestination = isPeerInDestination || isPeerInSource;
if (!isInSourceOrDestination) return false;
if (rule.protocol === "all") return true;
// Check ports
const hasNoPortRestrictions = rule.ports === undefined;
const hasExplicitPort22 = rule.ports?.includes("22");
const hasPort22InRange = rule.port_ranges?.some(
(range) => 22 >= range.start && 22 <= range.end,
);
return hasNoPortRestrictions || hasExplicitPort22 || hasPort22InRange;
});
const hasSSHPolicy = (peerPolicies?.length ?? 0) > 0;
const showSSHPolicyInfo =
!hasSSHPolicy &&
!isLoading &&
!!peer?.ssh_enabled &&
isNativeSSHSupported(peer.version);
return {
peerPolicies,
isCheckLoading: isLoading,
hasSSHPolicy,
showSSHPolicyInfo,
};
};

View File

@@ -1,4 +1,3 @@
import FullTooltip from "@components/FullTooltip";
import InlineLink from "@components/InlineLink";
import {
Tooltip,
@@ -8,13 +7,14 @@ import {
} from "@components/Tooltip";
import MemoizedNetBirdIcon from "@components/ui/MemoizedNetBirdIcon";
import { getOperatingSystem } from "@hooks/useOperatingSystem";
import { parseVersionString } from "@utils/version";
import { compareVersions } 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";
import { PeerOperatingSystemIcon } from "@/modules/peers/PeerOperatingSystemIcon";
import FullTooltip from "@components/FullTooltip";
type Props = {
version: string;
@@ -31,7 +31,8 @@ export default function PeerVersionCell({ version, os, serial }: Props) {
operatingSystem === OperatingSystem.ANDROID
)
return false;
return parseVersionString(version) < parseVersionString(latestVersion);
if (!latestVersion) return false;
return !compareVersions(version, latestVersion);
}, [os, version, latestVersion]);
const updateIcon = useMemo(() => {

View File

@@ -24,6 +24,8 @@ export enum SSHStatus {
export const SSH_DOCS_LINK =
"https://docs.netbird.io/how-to/browser-client#ssh-connection";
const SSH_DETECTION_TIMEOUT_MS = 20000;
export const useSSH = (client: any) => {
const [status, setStatus] = useState(SSHStatus.DISCONNECTED);
const [config, setConfig] = useState<SSHConfig | null>(null);
@@ -38,12 +40,37 @@ export const useSSH = (client: any) => {
setStatus(SSHStatus.CONNECTING);
setConfig(config);
setError("");
try {
let requiresJwt = false;
try {
requiresJwt = await client.detectSSHServerType(
config.hostname,
config.port,
SSH_DETECTION_TIMEOUT_MS,
);
console.log("Detection:", { requiresJwt, hasToken: !!accessToken });
} catch (detectionErr) {
console.error(
"Detection failed, falling back to pubkey:",
detectionErr,
);
}
if (requiresJwt && !accessToken) {
console.error("No access token available");
setError("No access token available");
setStatus(SSHStatus.DISCONNECTED);
setConfig(null);
return SSHStatus.DISCONNECTED;
}
const ssh = await client.createSSHConnection(
config.hostname,
config.port,
config.username,
requiresJwt ? accessToken : undefined,
);
ssh.onclose = () => {
@@ -57,7 +84,7 @@ export const useSSH = (client: any) => {
setStatus(SSHStatus.CONNECTED);
return SSHStatus.CONNECTED;
} catch (err) {
console.error("SSH connection failed:", err);
console.error("Connection failed:", err);
session.current = null;
setStatus(SSHStatus.DISCONNECTED);
setError("SSH connection failed. Check the console for details.");
@@ -65,7 +92,7 @@ export const useSSH = (client: any) => {
return SSHStatus.DISCONNECTED;
}
},
[client, status],
[client, status, accessToken],
);
const disconnect = useCallback(() => {

View File

@@ -209,11 +209,11 @@ export const useNetBirdClient = () => {
}, []);
const detectSSHServerType = useCallback(
async (host: string, port: number): Promise<boolean> => {
async (host: string, port: number, timeoutMs: number): Promise<boolean> => {
if (!netBirdClient.current?.detectSSHServerType) {
throw new Error("NetBird client not ready");
}
return netBirdClient.current.detectSSHServerType(host, port);
return netBirdClient.current.detectSSHServerType(host, port, timeoutMs);
},
[],
);
@@ -228,7 +228,12 @@ export const useNetBirdClient = () => {
if (!netBirdClient.current?.createSSHConnection) {
throw new Error("Go client not ready");
}
return netBirdClient.current.createSSHConnection(host, port, username);
return netBirdClient.current.createSSHConnection(
host,
port,
username,
jwtToken,
);
},
[],
);

View File

@@ -68,7 +68,7 @@ const loadConfig = (): Config => {
googleAnalyticsID: configJson?.googleAnalyticsID || undefined,
googleTagManagerID: configJson?.googleTagManagerID || undefined,
wasmPath:
configJson.wasmPath || "https://pkgs.netbird.io/wasm/client/v0.59.11",
configJson?.wasmPath || "https://pkgs.netbird.io/wasm/client/v0.60.2",
} as Config;
};

34
src/utils/version.test.ts Normal file
View File

@@ -0,0 +1,34 @@
import { isNativeSSHSupported } from "./version.js";
console.log("=== Testing isNativeSSHSupported ===");
const sshTestCases = [
{ version: "v0.59.9", shouldSupport: false },
{ version: "v0.59.10", shouldSupport: false },
{ version: "v0.59.11", shouldSupport: false },
{ version: "v0.60.0", shouldSupport: true },
{ version: "v0.60.1", shouldSupport: true },
{ version: "v0.61.0", shouldSupport: true },
{ version: "v1.0.0", shouldSupport: true },
// Edge cases
{ version: "development", shouldSupport: true, desc: "development build" },
{ version: "0.60.0", shouldSupport: true, desc: "no v prefix" },
{ version: "0.59.11", shouldSupport: false, desc: "no v prefix" },
{ version: "v0.60.0-beta", shouldSupport: true, desc: "with suffix" },
{ version: "v0.60.0-rc1", shouldSupport: true, desc: "with rc suffix" },
{ version: "v0.59.9-beta", shouldSupport: false, desc: "old version with suffix" },
];
let failures = 0;
sshTestCases.forEach(({ version, shouldSupport, desc }) => {
const result = isNativeSSHSupported(version);
const status = result === shouldSupport ? "✓" : "✗";
if (result !== shouldSupport) failures++;
const label = desc ? `${version.padEnd(15)} (${desc})` : version.padEnd(15);
console.log(
`${status} ${label}${result.toString().padStart(5)} (expected: ${shouldSupport})`,
);
});
console.log(`\n${failures} test(s) failed`);
process.exit(failures > 0 ? 1 : 0);

View File

@@ -36,9 +36,30 @@ export const getLatestNetbirdRelease = async (
}
};
export const parseVersionString = (version: string | undefined) => {
if (!version) return -1;
return parseInt(version.replace(/\D/g, ""));
/**
* Compare semantic versions.
* Returns true if version >= minVersion.
*/
export const compareVersions = (
version: string,
minVersion: string,
): boolean => {
const parseVersion = (v: string): number[] => {
return v.replace(/^v/, "").split(".").map(Number);
};
const vParts = parseVersion(version);
const minParts = parseVersion(minVersion);
for (let i = 0; i < Math.max(vParts.length, minParts.length); i++) {
const vPart = vParts[i] || 0;
const minPart = minParts[i] || 0;
if (vPart > minPart) return true;
if (vPart < minPart) return false;
}
return true;
};
/**
@@ -51,16 +72,15 @@ export const isRoutingPeerSupported = (version: string, os: string) => {
const operatingSystem = getOperatingSystem(os);
if (operatingSystem == OperatingSystem.LINUX) return true;
if (version == "development") return true;
const versionNumber = parseVersionString(version);
return versionNumber >= 366;
return compareVersions(version, "0.36.6");
};
/**
* Check if native SSH is supported
* Check if native SSH is supported.
* Supported starting from NetBird v0.60.0+.
* @param version
*/
export const isNativeSSHSupported = (version: string) => {
if (version == "development") return true;
const versionNumber = parseVersionString(version);
return versionNumber >= 999999;
return compareVersions(version, "0.60.0");
};