mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-09-23 00:30:43 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a3fc271da |
No files matched your search
@@ -70,6 +70,7 @@ import ReverseProxiesProvider, {
|
|||||||
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
|
import { ReverseProxyFlatTargetsTabContent } from "@/modules/reverse-proxy/targets/flat/ReverseProxyFlatTargetsTabContent";
|
||||||
import { PeerEditIPModal } from "@/modules/peer/PeerEditIPModal";
|
import { PeerEditIPModal } from "@/modules/peer/PeerEditIPModal";
|
||||||
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
|
import { PeerSSHToggle } from "@/modules/peer/PeerSSHToggle";
|
||||||
|
import { isSSHSupportedOnOS } from "@/modules/remote-access/osSupport";
|
||||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||||
import { PeerExpirationSettings } from "@/modules/peer/PeerExpirationSettings";
|
import { PeerExpirationSettings } from "@/modules/peer/PeerExpirationSettings";
|
||||||
@@ -430,6 +431,7 @@ const PeerOverviewTabContent = () => {
|
|||||||
const { peer } = usePeer();
|
const { peer } = usePeer();
|
||||||
const { permission } = usePermissions();
|
const { permission } = usePermissions();
|
||||||
const { selectedGroups, setSelectedGroups } = usePeerSettings();
|
const { selectedGroups, setSelectedGroups } = usePeerSettings();
|
||||||
|
const isSSHSupported = isSSHSupportedOnOS(peer?.os);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"px-8"}>
|
<div className={"px-8"}>
|
||||||
@@ -458,11 +460,14 @@ const PeerOverviewTabContent = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PeerSSHToggle />
|
{isSSHSupported && <PeerSSHToggle />}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Label>Remote Access</Label>
|
<Label>Remote Access</Label>
|
||||||
<HelpText>Connect directly to this peer via SSH or RDP.</HelpText>
|
<HelpText>
|
||||||
|
Connect directly to this peer via{" "}
|
||||||
|
{isSSHSupported ? "SSH or RDP" : "RDP"}.
|
||||||
|
</HelpText>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<SSHButton peer={peer} />
|
<SSHButton peer={peer} />
|
||||||
<RDPButton peer={peer} />
|
<RDPButton peer={peer} />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
useNetBirdClient,
|
useNetBirdClient,
|
||||||
} from "@/modules/remote-access/useNetBirdClient";
|
} from "@/modules/remote-access/useNetBirdClient";
|
||||||
import {
|
import {
|
||||||
isNativeSSHSupported,
|
usesStandardSSHPort,
|
||||||
isNetbirdSSHProtocolSupported,
|
isNetbirdSSHProtocolSupported,
|
||||||
} from "@utils/version";
|
} from "@utils/version";
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
|||||||
if (isSSHConnected || isSSHConnecting) return;
|
if (isSSHConnected || isSSHConnecting) return;
|
||||||
connected.current = false;
|
connected.current = false;
|
||||||
try {
|
try {
|
||||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
const aclPort = usesStandardSSHPort(peer.version) ? "22022" : port;
|
||||||
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
||||||
? "netbird-ssh"
|
? "netbird-ssh"
|
||||||
: "tcp";
|
: "tcp";
|
||||||
@@ -122,7 +122,7 @@ function SSHTerminal({ username, port, peer, ipVersion }: Props) {
|
|||||||
connected.current = true;
|
connected.current = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const aclPort = isNativeSSHSupported(peer.version) ? "22022" : port;
|
const aclPort = usesStandardSSHPort(peer.version) ? "22022" : port;
|
||||||
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
const protocol = isNetbirdSSHProtocolSupported(peer.version)
|
||||||
? "netbird-ssh"
|
? "netbird-ssh"
|
||||||
: "tcp";
|
: "tcp";
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 290 KiB |
@@ -11,19 +11,19 @@ import ModalHeader from "@components/modal/ModalHeader";
|
|||||||
import Paragraph from "@components/Paragraph";
|
import Paragraph from "@components/Paragraph";
|
||||||
import Separator from "@components/Separator";
|
import Separator from "@components/Separator";
|
||||||
import Steps from "@components/Steps";
|
import Steps from "@components/Steps";
|
||||||
import { Lightbox } from "@components/ui/Lightbox";
|
|
||||||
import { Mark } from "@components/ui/Mark";
|
import { Mark } from "@components/ui/Mark";
|
||||||
import { cn } from "@utils/helpers";
|
import { cn } from "@utils/helpers";
|
||||||
import { ExternalLinkIcon, PlusCircle, TerminalSquare } from "lucide-react";
|
import { ExternalLinkIcon, PlusCircle, TerminalSquare } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import sshImage from "@/assets/ssh/ssh-client.png";
|
|
||||||
import { SegmentedTabs } from "@components/SegmentedTabs";
|
import { SegmentedTabs } from "@components/SegmentedTabs";
|
||||||
|
import AndroidIcon from "@/assets/icons/AndroidIcon";
|
||||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||||
|
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||||
|
import { isNetbirdSSHProtocolSupported } from "@utils/version";
|
||||||
|
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||||
import { Peer } from "@/interfaces/Peer";
|
import { Peer } from "@/interfaces/Peer";
|
||||||
import { PeerSSHPolicyModal } from "@/modules/peer/PeerSSHPolicyModal";
|
import { PeerSSHPolicyModal } from "@/modules/peer/PeerSSHPolicyModal";
|
||||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
|
||||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
@@ -39,6 +39,11 @@ export const PeerSSHInstructions = ({
|
|||||||
peer,
|
peer,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [client, setClient] = useState("cli");
|
const [client, setClient] = useState("cli");
|
||||||
|
// Peers already on v0.61.0+ don't need to be told which release started
|
||||||
|
// requiring a policy.
|
||||||
|
const peerRequiresSSHPolicy = isNetbirdSSHProtocolSupported(
|
||||||
|
peer?.version ?? "",
|
||||||
|
);
|
||||||
const [policyModal, setPolicyModal] = useState(false);
|
const [policyModal, setPolicyModal] = useState(false);
|
||||||
|
|
||||||
// Enabling the SSH server and root login require root, or an administrator on
|
// Enabling the SSH server and root login require root, or an administrator on
|
||||||
@@ -75,46 +80,60 @@ export const PeerSSHInstructions = ({
|
|||||||
<NetBirdIcon size={16} />
|
<NetBirdIcon size={16} />
|
||||||
Desktop Client
|
Desktop Client
|
||||||
</SegmentedTabs.Trigger>
|
</SegmentedTabs.Trigger>
|
||||||
|
<SegmentedTabs.Trigger value={"android"}>
|
||||||
|
<AndroidIcon size={16} className={"fill-nb-gray-400"} />
|
||||||
|
Android
|
||||||
|
</SegmentedTabs.Trigger>
|
||||||
</SegmentedTabs.List>
|
</SegmentedTabs.List>
|
||||||
</SegmentedTabs>
|
</SegmentedTabs>
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
{client === "cli" ? (
|
<Steps.Step step={1}>
|
||||||
<Steps.Step step={1}>
|
{client === "cli" && (
|
||||||
<p className={"font-normal"}>
|
<>
|
||||||
If you are using NetBird via CLI, you can enable SSH by
|
<p className={"font-normal"}>
|
||||||
running{" "}
|
If you are using NetBird via CLI, you can enable SSH by
|
||||||
{isWindows
|
running{" "}
|
||||||
? "these commands in an elevated prompt"
|
{isWindows
|
||||||
: "these commands as root"}
|
? "these commands in an elevated prompt"
|
||||||
. Run the first one only if NetBird is already running. On a
|
: "these commands as root"}
|
||||||
machine where you do not have those rights, an administrator
|
. Run the first one only if NetBird is already running. On a
|
||||||
has to run them.
|
machine where you do not have those rights, an administrator
|
||||||
</p>
|
has to run them.
|
||||||
<Code codeToCopy={`${prefix}netbird down`}>
|
</p>
|
||||||
<Code.Line>{`${prefix}netbird down`}</Code.Line>
|
<Code codeToCopy={`${prefix}netbird down`}>
|
||||||
</Code>
|
<Code.Line>{`${prefix}netbird down`}</Code.Line>
|
||||||
<Code>
|
</Code>
|
||||||
<Code.Line>{`${prefix}netbird up --allow-server-ssh --enable-ssh-root`}</Code.Line>
|
<Code>
|
||||||
</Code>
|
<Code.Line>{`${prefix}netbird up --allow-server-ssh --enable-ssh-root`}</Code.Line>
|
||||||
</Steps.Step>
|
</Code>
|
||||||
) : (
|
</>
|
||||||
<Steps.Step step={1}>
|
)}
|
||||||
|
|
||||||
|
{client === "gui" && (
|
||||||
<p className={"font-normal"}>
|
<p className={"font-normal"}>
|
||||||
If you are using NetBird via the Desktop Client, click on the
|
If you are using NetBird via the Desktop Client, click on the
|
||||||
NetBird tray icon, go to <Mark>Settings</Mark> and click{" "}
|
NetBird tray icon, open <Mark>Settings</Mark> and turn on{" "}
|
||||||
<Mark>Allow SSH</Mark>. If you want to enable Root Login go to{" "}
|
<Mark>Enable SSH Server</Mark> on the <Mark>SSH</Mark> tab. To
|
||||||
<Mark>Settings > Advanced Settings</Mark> and enable SSH
|
log in as {isWindows ? "an administrator" : "root"}, enable{" "}
|
||||||
Root Login under the SSH tab.
|
<Mark>Allow Root Login</Mark> on the same tab.
|
||||||
</p>
|
</p>
|
||||||
<Lightbox image={sshImage} />
|
)}
|
||||||
</Steps.Step>
|
|
||||||
)}
|
{client === "android" && (
|
||||||
|
<p className={"font-normal"}>
|
||||||
|
If you are using NetBird on Android, open the app, tap{" "}
|
||||||
|
<Mark>Settings</Mark>, then <Mark>Advanced</Mark> and turn on{" "}
|
||||||
|
<Mark>Enable SSH Server</Mark>.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Steps.Step>
|
||||||
|
|
||||||
<Steps.Step step={2}>
|
<Steps.Step step={2}>
|
||||||
<p className={"font-normal"}>
|
<p className={"font-normal"}>
|
||||||
Starting from NetBird v0.61.0, SSH requires an explicit access
|
{peerRequiresSSHPolicy
|
||||||
control policy to allow SSH connections to this machine.
|
? "SSH requires an explicit access control policy to allow SSH connections to this machine."
|
||||||
|
: "Starting from NetBird v0.61.0, SSH requires an explicit access control policy to allow SSH connections to this machine."}
|
||||||
</p>
|
</p>
|
||||||
<div className={"mt-2"}>
|
<div className={"mt-2"}>
|
||||||
<Button
|
<Button
|
||||||
@@ -128,8 +147,8 @@ export const PeerSSHInstructions = ({
|
|||||||
</Steps.Step>
|
</Steps.Step>
|
||||||
<Steps.Step step={3} line={false}>
|
<Steps.Step step={3} line={false}>
|
||||||
<p className={"font-normal"}>
|
<p className={"font-normal"}>
|
||||||
Once the NetBird SSH server is allowed on the client, <br />
|
Once the NetBird SSH server is enabled on the client, <br />
|
||||||
click <Mark>Confirm & Enable</Mark> below to finish the setup.
|
click <Mark>Finish Setup</Mark> below to complete the setup.
|
||||||
</p>
|
</p>
|
||||||
</Steps.Step>
|
</Steps.Step>
|
||||||
</Steps>
|
</Steps>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import useFetchApi from "@utils/api";
|
import useFetchApi from "@utils/api";
|
||||||
import { Peer } from "@/interfaces/Peer";
|
import { Peer } from "@/interfaces/Peer";
|
||||||
import { Policy } from "@/interfaces/Policy";
|
import { Policy } from "@/interfaces/Policy";
|
||||||
import { isNativeSSHSupported } from "@utils/version";
|
import { usesStandardSSHPort } from "@utils/version";
|
||||||
|
|
||||||
export const usePeerSSHPolicyCheck = (peer?: Peer) => {
|
export const usePeerSSHPolicyCheck = (peer?: Peer) => {
|
||||||
const { data: policies, isLoading } = useFetchApi<Policy[]>(
|
const { data: policies, isLoading } = useFetchApi<Policy[]>(
|
||||||
@@ -66,7 +66,7 @@ export const usePeerSSHPolicyCheck = (peer?: Peer) => {
|
|||||||
!hasSSHPolicy &&
|
!hasSSHPolicy &&
|
||||||
!isLoading &&
|
!isLoading &&
|
||||||
!!peer?.ssh_enabled &&
|
!!peer?.ssh_enabled &&
|
||||||
isNativeSSHSupported(peer.version);
|
usesStandardSSHPort(peer.version);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
peerPolicies,
|
peerPolicies,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
} from "@components/DropdownMenu";
|
} from "@components/DropdownMenu";
|
||||||
import FullTooltip from "@components/FullTooltip";
|
import FullTooltip from "@components/FullTooltip";
|
||||||
import { notify } from "@components/Notification";
|
import { notify } from "@components/Notification";
|
||||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
|
||||||
import { IconInfoCircle } from "@tabler/icons-react";
|
import { IconInfoCircle } from "@tabler/icons-react";
|
||||||
import {
|
import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -27,9 +26,9 @@ import { useSWRConfig } from "swr";
|
|||||||
import { useBypass, useBypassedPeers } from "@/cloud/edr/useBypass";
|
import { useBypass, useBypassedPeers } from "@/cloud/edr/useBypass";
|
||||||
import { usePeer } from "@/contexts/PeerProvider";
|
import { usePeer } from "@/contexts/PeerProvider";
|
||||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
|
||||||
import { ExitNodeDropdownButton } from "@/modules/exit-node/ExitNodeDropdownButton";
|
import { ExitNodeDropdownButton } from "@/modules/exit-node/ExitNodeDropdownButton";
|
||||||
import { useIntegrations } from "@/modules/integrations/edr/useIntegrations";
|
import { useIntegrations } from "@/modules/integrations/edr/useIntegrations";
|
||||||
|
import { isSSHSupportedOnOS } from "@/modules/remote-access/osSupport";
|
||||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||||
import InlineLink from "@components/InlineLink";
|
import InlineLink from "@components/InlineLink";
|
||||||
@@ -126,21 +125,15 @@ export default function PeerActionCell() {
|
|||||||
const showRevokeBypass = isBypassed && canBypass;
|
const showRevokeBypass = isBypassed && canBypass;
|
||||||
const showApprovalGroup = showApprove || showBypass || showRevokeBypass;
|
const showApprovalGroup = showApprove || showBypass || showRevokeBypass;
|
||||||
|
|
||||||
|
const isSSHSupported = isSSHSupportedOnOS(peer?.os);
|
||||||
|
|
||||||
const showSSHButton = useMemo(() => {
|
const showSSHButton = useMemo(() => {
|
||||||
|
if (!isSSHSupported) return false;
|
||||||
const isClientSSHEnabled = peer?.local_flags?.server_ssh_allowed;
|
const isClientSSHEnabled = peer?.local_flags?.server_ssh_allowed;
|
||||||
const isDashboardSSHEnabled = peer?.ssh_enabled;
|
const isDashboardSSHEnabled = peer?.ssh_enabled;
|
||||||
if (isDashboardSSHEnabled) return true;
|
if (isDashboardSSHEnabled) return true;
|
||||||
return !isClientSSHEnabled;
|
return !isClientSSHEnabled;
|
||||||
}, [peer]);
|
}, [peer, isSSHSupported]);
|
||||||
|
|
||||||
// The Connect column previously hosted SSH / RDP entry points. We
|
|
||||||
// fold those into the action menu — gated on a non-mobile, online
|
|
||||||
// peer — so the table loses a column and the connect affordance is
|
|
||||||
// one click away inside the three-dot menu.
|
|
||||||
const peerOs = getOperatingSystem(peer?.os);
|
|
||||||
const isMobile =
|
|
||||||
peerOs === OperatingSystem.ANDROID || peerOs === OperatingSystem.IOS;
|
|
||||||
const showRemoteAccessItems = !isMobile && !!peer.connected;
|
|
||||||
|
|
||||||
const toggleLoginExpiration = async () => {
|
const toggleLoginExpiration = async () => {
|
||||||
const text = peer.login_expiration_enabled ? "disabled" : "enabled";
|
const text = peer.login_expiration_enabled ? "disabled" : "enabled";
|
||||||
@@ -255,7 +248,7 @@ export default function PeerActionCell() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showRemoteAccessItems && (
|
{peer.connected && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<SSHButton peer={peer} isDropdown={true} />
|
<SSHButton peer={peer} isDropdown={true} />
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@components/DropdownMenu";
|
} from "@components/DropdownMenu";
|
||||||
import FullTooltip from "@components/FullTooltip";
|
import FullTooltip from "@components/FullTooltip";
|
||||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
|
||||||
import { IconChevronDown } from "@tabler/icons-react";
|
import { IconChevronDown } from "@tabler/icons-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { usePeer } from "@/contexts/PeerProvider";
|
import { usePeer } from "@/contexts/PeerProvider";
|
||||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
|
||||||
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
import { RDPButton } from "@/modules/remote-access/rdp/RDPButton";
|
||||||
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
import { SSHButton } from "@/modules/remote-access/ssh/SSHButton";
|
||||||
import { cn } from "@utils/helpers";
|
import { cn } from "@utils/helpers";
|
||||||
@@ -16,10 +14,6 @@ import { cn } from "@utils/helpers";
|
|||||||
export const PeerConnectButton = () => {
|
export const PeerConnectButton = () => {
|
||||||
const { peer } = usePeer();
|
const { peer } = usePeer();
|
||||||
const isConnected = peer.connected;
|
const isConnected = peer.connected;
|
||||||
const os = getOperatingSystem(peer?.os);
|
|
||||||
const isMobile = os === OperatingSystem.ANDROID || os === OperatingSystem.IOS;
|
|
||||||
|
|
||||||
if (isMobile) return;
|
|
||||||
|
|
||||||
return isConnected ? (
|
return isConnected ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||||
|
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the NetBird SSH server can run on the peer's operating system.
|
||||||
|
* The client ships it everywhere except iOS.
|
||||||
|
*
|
||||||
|
* There is no equivalent for RDP: the server is not ours, so any operating
|
||||||
|
* system may be running one.
|
||||||
|
*/
|
||||||
|
export const isSSHSupportedOnOS = (os?: string) =>
|
||||||
|
getOperatingSystem(os ?? "") !== OperatingSystem.IOS;
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
import Button from "@components/Button";
|
import Button from "@components/Button";
|
||||||
import { DropdownMenuItem } from "@components/DropdownMenu";
|
import { DropdownMenuItem } from "@components/DropdownMenu";
|
||||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
|
||||||
import { CircleHelpIcon, MonitorIcon } from "lucide-react";
|
import { CircleHelpIcon, MonitorIcon } from "lucide-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { usePermissions } from "@/contexts/PermissionsProvider";
|
import { usePermissions } from "@/contexts/PermissionsProvider";
|
||||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
|
||||||
import { Peer } from "@/interfaces/Peer";
|
import { Peer } from "@/interfaces/Peer";
|
||||||
import { RDPTooltip } from "@/modules/remote-access/rdp/RDPTooltip";
|
import { RDPTooltip } from "@/modules/remote-access/rdp/RDPTooltip";
|
||||||
|
|
||||||
@@ -13,18 +11,9 @@ type Props = {
|
|||||||
isDropdown?: boolean;
|
isDropdown?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const RDP_SUPPORTED_OS = new Set([
|
|
||||||
OperatingSystem.WINDOWS,
|
|
||||||
OperatingSystem.LINUX,
|
|
||||||
OperatingSystem.FREEBSD,
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const RDPButton = ({ peer, isDropdown = false }: Props) => {
|
export const RDPButton = ({ peer, isDropdown = false }: Props) => {
|
||||||
const { permission } = usePermissions();
|
const { permission } = usePermissions();
|
||||||
|
|
||||||
const os = getOperatingSystem(peer?.os);
|
|
||||||
if (!RDP_SUPPORTED_OS.has(os)) return null;
|
|
||||||
|
|
||||||
const disabled = !peer.connected || !permission.peers.update;
|
const disabled = !peer.connected || !permission.peers.update;
|
||||||
const hasPermission = permission.peers.update;
|
const hasPermission = permission.peers.update;
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { usePermissions } from "@/contexts/PermissionsProvider";
|
|||||||
import { Peer } from "@/interfaces/Peer";
|
import { Peer } from "@/interfaces/Peer";
|
||||||
import { SSHCredentialsModal } from "@/modules/remote-access/ssh/SSHCredentialsModal";
|
import { SSHCredentialsModal } from "@/modules/remote-access/ssh/SSHCredentialsModal";
|
||||||
import { SSHTooltip } from "@/modules/remote-access/ssh/SSHTooltip";
|
import { SSHTooltip } from "@/modules/remote-access/ssh/SSHTooltip";
|
||||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
import { isSSHSupportedOnOS } from "@/modules/remote-access/osSupport";
|
||||||
import { OperatingSystem } from "@/interfaces/OperatingSystem";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
peer: Peer;
|
peer: Peer;
|
||||||
@@ -25,52 +24,45 @@ export const SSHButton = ({ peer, isDropdown = false }: Props) => {
|
|||||||
|
|
||||||
const hasPermission = permission.peers.update;
|
const hasPermission = permission.peers.update;
|
||||||
|
|
||||||
const os = getOperatingSystem(peer?.os);
|
if (!isSSHSupportedOnOS(peer?.os)) return null;
|
||||||
const isSSHSupported = os !== OperatingSystem.IOS;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isSSHSupported && (
|
<>
|
||||||
<>
|
{modal && (
|
||||||
{modal && (
|
<SSHCredentialsModal open={modal} onOpenChange={setModal} peer={peer} />
|
||||||
<SSHCredentialsModal
|
)}
|
||||||
open={modal}
|
<div>
|
||||||
onOpenChange={setModal}
|
<SSHTooltip
|
||||||
peer={peer}
|
isOnline={peer.connected}
|
||||||
/>
|
isSSHEnabled={isSSHEnabled}
|
||||||
)}
|
hasPermission={hasPermission}
|
||||||
<div>
|
side={isDropdown ? "left" : "top"}
|
||||||
<SSHTooltip
|
>
|
||||||
isOnline={peer.connected}
|
{isDropdown ? (
|
||||||
isSSHEnabled={isSSHEnabled}
|
<DropdownMenuItem
|
||||||
hasPermission={hasPermission}
|
onClick={() => setModal(true)}
|
||||||
side={isDropdown ? "left" : "top"}
|
disabled={disabled}
|
||||||
>
|
className={"w-full"}
|
||||||
{isDropdown ? (
|
>
|
||||||
<DropdownMenuItem
|
<div className={"flex gap-3 items-center w-full"}>
|
||||||
onClick={() => setModal(true)}
|
<TerminalIcon size={14} className={"shrink-0"} />
|
||||||
disabled={disabled}
|
|
||||||
className={"w-full"}
|
|
||||||
>
|
|
||||||
<div className={"flex gap-3 items-center w-full"}>
|
|
||||||
<TerminalIcon size={14} className={"shrink-0"} />
|
|
||||||
SSH
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => setModal(true)}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<TerminalIcon size={16} />
|
|
||||||
SSH
|
SSH
|
||||||
{disabled && <CircleHelpIcon size={12} />}
|
</div>
|
||||||
</Button>
|
</DropdownMenuItem>
|
||||||
)}
|
) : (
|
||||||
</SSHTooltip>
|
<Button
|
||||||
</div>
|
variant="secondary"
|
||||||
</>
|
size="sm"
|
||||||
)
|
onClick={() => setModal(true)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<TerminalIcon size={16} />
|
||||||
|
SSH
|
||||||
|
{disabled && <CircleHelpIcon size={12} />}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</SSHTooltip>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -13,7 +13,7 @@ import ModalHeader from "@components/modal/ModalHeader";
|
|||||||
import Paragraph from "@components/Paragraph";
|
import Paragraph from "@components/Paragraph";
|
||||||
import Separator from "@components/Separator";
|
import Separator from "@components/Separator";
|
||||||
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
import { getOperatingSystem } from "@hooks/useOperatingSystem";
|
||||||
import { isNativeSSHSupported } from "@utils/version";
|
import { usesStandardSSHPort } from "@utils/version";
|
||||||
import {
|
import {
|
||||||
ChevronsLeftRightEllipsis,
|
ChevronsLeftRightEllipsis,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
@@ -39,7 +39,7 @@ export const SSHCredentialsModal = ({ open, onOpenChange, peer }: Props) => {
|
|||||||
: "root",
|
: "root",
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialPort = isNativeSSHSupported(peer.version) ? "22" : "44338";
|
const initialPort = usesStandardSSHPort(peer.version) ? "22" : "44338";
|
||||||
const [port, setPort] = useState(initialPort);
|
const [port, setPort] = useState(initialPort);
|
||||||
|
|
||||||
const userNameError = useMemo(() => {
|
const userNameError = useMemo(() => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { compareVersions, isNativeSSHSupported, isNewerVersion } from "./version";
|
import { compareVersions, isNewerVersion, usesStandardSSHPort } from "./version";
|
||||||
|
|
||||||
describe("isNativeSSHSupported", () => {
|
describe("usesStandardSSHPort", () => {
|
||||||
it.each([
|
it.each([
|
||||||
{ version: "v0.59.9", shouldSupport: false, desc: "below minimum" },
|
{ version: "v0.59.9", shouldSupport: false, desc: "below minimum" },
|
||||||
{ version: "v0.59.10", shouldSupport: false, desc: "below minimum" },
|
{ version: "v0.59.10", shouldSupport: false, desc: "below minimum" },
|
||||||
@@ -36,8 +36,14 @@ describe("isNativeSSHSupported", () => {
|
|||||||
shouldSupport: true,
|
shouldSupport: true,
|
||||||
desc: "CI build suffix",
|
desc: "CI build suffix",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
version: "dev-16f7e1e14",
|
||||||
|
shouldSupport: true,
|
||||||
|
desc: "commit-stamped mobile build",
|
||||||
|
},
|
||||||
|
{ version: "", shouldSupport: true, desc: "unreported version" },
|
||||||
])("$version → $shouldSupport ($desc)", ({ version, shouldSupport }) => {
|
])("$version → $shouldSupport ($desc)", ({ version, shouldSupport }) => {
|
||||||
expect(isNativeSSHSupported(version)).toBe(shouldSupport);
|
expect(usesStandardSSHPort(version)).toBe(shouldSupport);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,6 +114,12 @@ describe("isNewerVersion (update available)", () => {
|
|||||||
expected: false,
|
expected: false,
|
||||||
desc: "development build",
|
desc: "development build",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
current: "dev-16f7e1e14",
|
||||||
|
latest: "0.77.0",
|
||||||
|
expected: false,
|
||||||
|
desc: "commit-stamped mobile build",
|
||||||
|
},
|
||||||
{ current: "0.77.0", latest: "", expected: false, desc: "unknown latest" },
|
{ current: "0.77.0", latest: "", expected: false, desc: "unknown latest" },
|
||||||
])("$current → $latest → $expected ($desc)", ({ current, latest, expected }) => {
|
])("$current → $latest → $expected ($desc)", ({ current, latest, expected }) => {
|
||||||
expect(isNewerVersion(current, latest)).toBe(expected);
|
expect(isNewerVersion(current, latest)).toBe(expected);
|
||||||
|
|||||||
+26
-34
@@ -62,6 +62,17 @@ const releaseParts = (version: string): number[] =>
|
|||||||
return Number.isNaN(parsed) ? 0 : parsed;
|
return Number.isNaN(parsed) ? 0 : parsed;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a version string carries a numeric release to compare against.
|
||||||
|
*
|
||||||
|
* Builds made outside the release pipeline don't: the "development" tag of the
|
||||||
|
* Go builds, and the commit-stamped builds the mobile clients report
|
||||||
|
* ("dev-16f7e1e14"). Neither does a peer that reports no version at all.
|
||||||
|
* Feature gates treat all of them as current rather than as release 0.
|
||||||
|
*/
|
||||||
|
export const hasReleaseVersion = (version: string): boolean =>
|
||||||
|
/^v?\d/i.test(version?.trim() ?? "");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compare semantic versions.
|
* Compare semantic versions.
|
||||||
* Returns true if version >= minVersion.
|
* Returns true if version >= minVersion.
|
||||||
@@ -84,37 +95,17 @@ export const compareVersions = (
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a version string names a non-release build. Mirrors the management
|
|
||||||
* server's version.IsDevelopmentVersion: the literal "development" plus the
|
|
||||||
* "ci-" and "dev-" prefixes it stamps on snapshot builds ("ci-7470fbdd").
|
|
||||||
*
|
|
||||||
* Such a string carries no release to compare against. releaseParts() reads
|
|
||||||
* its leading word as 0, so without this check every snapshot install would
|
|
||||||
* see the current release as newer and nag about an update forever.
|
|
||||||
*/
|
|
||||||
export const isDevelopmentVersion = (version: string): boolean => {
|
|
||||||
const bare = version.trim().replace(/^v/i, "");
|
|
||||||
return (
|
|
||||||
bare.startsWith("development") ||
|
|
||||||
bare.startsWith("ci-") ||
|
|
||||||
bare.startsWith("dev-")
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true when `latest` is a strictly newer release than `current` — i.e.
|
* Returns true when `latest` is a strictly newer release than `current` — i.e.
|
||||||
* an update is available. Only release components decide: an enterprise build
|
* an update is available. Only release components decide: an enterprise build
|
||||||
* ("0.77.0+enterprise.1") is up to date against the "0.77.0" it was built from,
|
* ("0.77.0+enterprise.1") is up to date against the "0.77.0" it was built from,
|
||||||
* matching how the management server evaluates it server-side.
|
* matching how the management server evaluates it server-side.
|
||||||
*
|
*
|
||||||
* Development and snapshot builds never report an update, in either position.
|
* Builds without a release ("development", "dev-16f7e1e14", "ci-7470fbdd")
|
||||||
|
* never report an update, in either position.
|
||||||
*/
|
*/
|
||||||
export const isNewerVersion = (current: string, latest: string): boolean => {
|
export const isNewerVersion = (current: string, latest: string): boolean => {
|
||||||
if (!current || !latest) return false;
|
if (!hasReleaseVersion(current) || !hasReleaseVersion(latest)) return false;
|
||||||
if (isDevelopmentVersion(current) || isDevelopmentVersion(latest)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentParts = releaseParts(current);
|
const currentParts = releaseParts(current);
|
||||||
const latestParts = releaseParts(latest);
|
const latestParts = releaseParts(latest);
|
||||||
@@ -139,26 +130,27 @@ export const isNewerVersion = (current: string, latest: string): boolean => {
|
|||||||
export const isRoutingPeerSupported = (version: string, os: string) => {
|
export const isRoutingPeerSupported = (version: string, os: string) => {
|
||||||
const operatingSystem = getOperatingSystem(os);
|
const operatingSystem = getOperatingSystem(os);
|
||||||
if (operatingSystem == OperatingSystem.LINUX) return true;
|
if (operatingSystem == OperatingSystem.LINUX) return true;
|
||||||
if (version == "development") return true;
|
if (!hasReleaseVersion(version)) return true;
|
||||||
return compareVersions(version, "0.36.6");
|
return compareVersions(version, "0.36.6");
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if native SSH is supported.
|
* Check if the peer's SSH server listens on the standard port 22, which it does
|
||||||
* Supported starting from NetBird v0.60.0+.
|
* starting from NetBird v0.60.0+. Older clients listen on 44338 instead.
|
||||||
|
* Access control rules address such a server on 22022, and a legacy one on
|
||||||
|
* whichever port the connection uses.
|
||||||
|
*
|
||||||
|
* Only a version that reports a release older than 0.60.0 is treated as legacy,
|
||||||
|
* so development and unreported versions get the current behaviour.
|
||||||
* @param version
|
* @param version
|
||||||
*/
|
*/
|
||||||
export const isNativeSSHSupported = (version: string) => {
|
export const usesStandardSSHPort = (version: string) =>
|
||||||
if (version == "development") return true;
|
!hasReleaseVersion(version) || compareVersions(version, "0.60.0");
|
||||||
return compareVersions(version, "0.60.0");
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if NetBird SSH protocol is supported.
|
* Check if NetBird SSH protocol is supported.
|
||||||
* Supported starting from NetBird v0.61.0+.
|
* Supported starting from NetBird v0.61.0+.
|
||||||
* @param version
|
* @param version
|
||||||
*/
|
*/
|
||||||
export const isNetbirdSSHProtocolSupported = (version: string) => {
|
export const isNetbirdSSHProtocolSupported = (version: string) =>
|
||||||
if (version == "development") return true;
|
!hasReleaseVersion(version) || compareVersions(version, "0.61.0");
|
||||||
return compareVersions(version, "0.61.0");
|
|
||||||
};
|
|
||||||
Reference in new issue
Block a user