import { useCallback, useEffect, useRef, useState } from "react"; import { Browser } from "@wailsio/runtime"; import { Settings as SettingsSvc, Profiles as ProfilesSvc, Update as UpdateSvc, Debug as DebugSvc, Connection as ConnectionSvc, } from "../../../bindings/github.com/netbirdio/netbird/client/ui-wails/services"; import type { Config, DebugBundleResult, } from "../../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js"; import { Check, Copy, FolderOpen, Loader2 } from "lucide-react"; import netbirdAppIcon from "@/assets/logos/netbird-app-icon.svg"; import pkg from "../../../package.json"; import { useStatus } from "@/hooks/useStatus"; import { Button } from "@/components/Button"; import FancyToggleSwitch from "@/components/FancyToggleSwitch"; import { HelpText } from "@/components/HelpText"; import { Input } from "@/components/Input"; import { Label } from "@/components/Label"; import { cn } from "@/lib/cn"; import { MainRightSide } from "@/layouts/MainRightSide.tsx"; import { VerticalTabs } from "@/components/VerticalTabs.tsx"; import { SettingsNavigationTriggers } from "@/modules/settings/SettingsNavigationTriggers.tsx"; type Ctx = { cfg: Config; setField: (k: K, v: Config[K]) => void; }; const SAVE_DEBOUNCE_MS = 400; const buildPayload = ( cfg: Config, profileName: string, username: string, ) => ({ profileName, username, managementUrl: cfg.managementUrl, adminUrl: cfg.adminUrl, interfaceName: cfg.interfaceName, wireguardPort: cfg.wireguardPort, mtu: cfg.mtu, preSharedKey: cfg.preSharedKey, disableAutoConnect: cfg.disableAutoConnect, serverSshAllowed: cfg.serverSshAllowed, rosenpassEnabled: cfg.rosenpassEnabled, rosenpassPermissive: cfg.rosenpassPermissive, disableNotifications: cfg.disableNotifications, lazyConnectionEnabled: cfg.lazyConnectionEnabled, blockInbound: cfg.blockInbound, networkMonitor: cfg.networkMonitor, disableClientRoutes: cfg.disableClientRoutes, disableServerRoutes: cfg.disableServerRoutes, disableDns: cfg.disableDns, blockLanAccess: cfg.blockLanAccess, enableSshRoot: cfg.enableSshRoot, enableSshSftp: cfg.enableSshSftp, enableSshLocalPortForwarding: cfg.enableSshLocalPortForwarding, enableSshRemotePortForwarding: cfg.enableSshRemotePortForwarding, disableSshAuth: cfg.disableSshAuth, sshJwtCacheTtl: cfg.sshJwtCacheTtl, }); export const Settings = () => { const [active, setActive] = useState("general"); const [username, setUsername] = useState(""); const [profile, setProfile] = useState(""); const [cfg, setCfg] = useState(null); const [error, setError] = useState(null); const dirtyRef = useRef(false); const load = useCallback(async () => { try { const u = await ProfilesSvc.Username(); const activeProfile = await ProfilesSvc.GetActive(); const profileName = activeProfile.profileName || "default"; setUsername(u); setProfile(profileName); const c = await SettingsSvc.GetConfig({ profileName, username: u, }); setCfg(c); setError(null); } catch (e) { setError(String(e)); } }, []); useEffect(() => { load(); }, [load]); const setField: Ctx["setField"] = (k, v) => { dirtyRef.current = true; setCfg((c) => (c ? { ...c, [k]: v } : c)); }; const saveNow = useCallback(async () => { if (!cfg) return; try { await SettingsSvc.SetConfig(buildPayload(cfg, profile, username)); setError(null); } catch (e) { setError(String(e)); } }, [cfg, profile, username]); useEffect(() => { if (!cfg || !dirtyRef.current) return; const t = setTimeout(saveNow, SAVE_DEBOUNCE_MS); return () => clearTimeout(t); }, [cfg, saveNow]); return ( {error && (

{error}

)}
{!cfg ? (
Loading…
) : (
)}
); }; const SectionGroup = ({ title, children, }: { title: string; children: React.ReactNode; }) => (

{title}

{children}
); function GeneralSection({ cfg, setField, onSaveServer, }: Ctx & { onSaveServer: () => void | Promise }) { return ( <> setField("disableAutoConnect", !v)} label={"Connect on startup"} helpText={ "Automatically connect to NetBird when the app launches." } /> setField("disableNotifications", !v)} label={"Show notifications"} helpText={ "Show desktop notifications for connection events and updates." } />
The NetBird management server this client connects to. Saving will reconnect to apply the new server.
setField("managementUrl", e.target.value) } />
); } function NetworkSection({ cfg, setField }: Ctx) { return ( <> setField("lazyConnectionEnabled", v)} label={"Lazy connections"} helpText={ "Only establish peer tunnels on first traffic instead of eagerly at startup." } /> setField("networkMonitor", v)} label={"Network monitor"} helpText={ "Reconnect automatically when the host network changes (Wi-Fi switch, VPN, sleep/wake)." } /> setField("disableDns", !v)} label={"Enable DNS"} helpText={ "Apply NetBird-managed DNS settings to the host resolver." } /> setField("disableClientRoutes", !v)} label={"Enable client routes"} helpText={ "Accept routes advertised by other peers so this client can reach their networks." } /> setField("disableServerRoutes", !v)} label={"Enable server routes"} helpText={ "Advertise this host's local routes to other peers." } /> ); } function SecuritySection({ cfg, setField }: Ctx) { return ( <> setField("blockInbound", v)} label={"Block inbound traffic"} helpText={ "Drop all unsolicited inbound traffic on the NetBird interface." } /> setField("blockLanAccess", v)} label={"Block LAN access"} helpText={ "Prevent peers from reaching this host's local network." } /> setField("rosenpassEnabled", v)} label={"Quantum-resistant encryption"} helpText={ "Add a post-quantum key exchange (Rosenpass) on top of WireGuard." } > setField("rosenpassPermissive", v)} label={"Permissive mode"} helpText={ "Allow connections to peers without quantum-resistance support." } /> ); } function SshSection({ cfg, setField }: Ctx) { const sshOff = !cfg.serverSshAllowed; return ( <> setField("serverSshAllowed", v)} label={"Allow SSH"} helpText={ "Run the NetBird SSH server on this host so other peers can connect to it." } /> setField("enableSshRoot", v)} label={"Allow root login"} helpText={ "Permit incoming SSH sessions to authenticate as root." } disabled={sshOff} /> setField("enableSshSftp", v)} label={"Enable SFTP"} helpText={"Allow file transfers over the NetBird SSH server."} disabled={sshOff} /> setField("enableSshLocalPortForwarding", v)} label={"Local port forwarding"} helpText={ "Allow clients to forward local ports through this host." } disabled={sshOff} /> setField("enableSshRemotePortForwarding", v) } label={"Remote port forwarding"} helpText={ "Allow clients to expose remote ports back through this host." } disabled={sshOff} /> setField("disableSshAuth", v)} label={"Disable SSH auth"} helpText={ "Skip JWT authentication for incoming SSH sessions. Insecure — diagnostics only." } disabled={sshOff} />
How long verified JWTs are cached before re-validation. Shorter values increase load on the management server; longer values delay revocation.
setField( "sshJwtCacheTtl", Number(e.target.value), ) } customSuffix={ s } disabled={sshOff} />
); } function AdvancedSection({ cfg, setField }: Ctx) { return ( <>
Optional WireGuard pre-shared key for an extra layer of symmetric encryption. Must match the value configured on every peer in the network. setField("preSharedKey", e.target.value) } />
setField("interfaceName", e.target.value)} />
setField("wireguardPort", Number(e.target.value)) } /> setField("mtu", Number(e.target.value)) } />
); } const NETBIRD_UPLOAD_URL = "https://debug.netbird.io/upload"; const TRACE_LOG_FILE_COUNT = 5; const PLAIN_LOG_FILE_COUNT = 1; type Stage = | { kind: "idle" } | { kind: "preparing-trace" } | { kind: "reconnecting" } | { kind: "capturing"; remainingSec: number; totalSec: number } | { kind: "restoring-level" } | { kind: "bundling" } | { kind: "uploading" } | { kind: "done"; result: DebugBundleResult; uploadAttempted: boolean } | { kind: "error"; message: string }; const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); function TroubleshootingSection({ profile, username, }: { profile: string; username: string; }) { const [anonymize, setAnonymize] = useState(true); const [systemInfo, setSystemInfo] = useState(true); const [upload, setUpload] = useState(false); const [trace, setTrace] = useState(false); const [traceMinutes, setTraceMinutes] = useState(3); const [stage, setStage] = useState({ kind: "idle" }); const isRunning = stage.kind !== "idle" && stage.kind !== "done" && stage.kind !== "error"; const reset = () => setStage({ kind: "idle" }); const run = async () => { const uploadUrl = upload ? NETBIRD_UPLOAD_URL : ""; try { let originalLevel = "info"; if (trace) { setStage({ kind: "preparing-trace" }); try { const cur = await DebugSvc.GetLogLevel(); if (cur?.level) originalLevel = cur.level; } catch { // best effort } await DebugSvc.SetLogLevel({ level: "trace" }); setStage({ kind: "reconnecting" }); try { await ConnectionSvc.Down(); } catch { // already down } await ConnectionSvc.Up({ profileName: profile, username }); const totalSec = Math.max( 1, Math.min(30, traceMinutes), ) * 60; for (let remaining = totalSec; remaining > 0; remaining--) { setStage({ kind: "capturing", remainingSec: remaining, totalSec, }); await sleep(1000); } setStage({ kind: "restoring-level" }); try { await DebugSvc.SetLogLevel({ level: originalLevel }); } catch { // restore is best-effort } } setStage({ kind: "bundling" }); const logFileCount = trace ? TRACE_LOG_FILE_COUNT : PLAIN_LOG_FILE_COUNT; if (uploadUrl) setStage({ kind: "uploading" }); const result = await DebugSvc.Bundle({ anonymize, systemInfo, uploadUrl, logFileCount, }); setStage({ kind: "done", result, uploadAttempted: Boolean(uploadUrl), }); } catch (e) { setStage({ kind: "error", message: String(e) }); } }; return (

A debug bundle helps NetBird support investigate connection problems. It's a zip file with logs and system details from this device.

setTraceMinutes( Math.max( 1, Math.min( 30, Number(e.target.value) || 1, ), ), ) } customSuffix={ min } disabled={isRunning} />
{stage.kind === "error" && ( )}
); } function BundleStatus({ stage }: { stage: Stage }) { if (stage.kind === "idle") return null; if ( stage.kind === "preparing-trace" || stage.kind === "reconnecting" || stage.kind === "capturing" || stage.kind === "restoring-level" || stage.kind === "bundling" || stage.kind === "uploading" ) { return (

{stageLabel(stage)}

); } if (stage.kind === "error") { return (
{stage.message}
); } return ; } function stageLabel(stage: Stage): string { switch (stage.kind) { case "preparing-trace": return "Switching to trace logging…"; case "reconnecting": return "Reconnecting NetBird…"; case "capturing": { const fmt = (s: number) => `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; return `Capturing logs — ${fmt( stage.totalSec - stage.remainingSec, )} / ${fmt(stage.totalSec)}`; } case "restoring-level": return "Restoring previous log level…"; case "bundling": return "Building bundle…"; case "uploading": return "Uploading to NetBird…"; default: return ""; } } function BundleResult({ result, uploaded, }: { result: DebugBundleResult; uploaded: boolean; }) { const uploadFailed = uploaded && !result.uploadedKey; return (
{uploaded && result.uploadedKey && (

Bundle uploaded

Share this key with NetBird support so they can find your bundle.

)} {uploadFailed && (
Upload failed {result.uploadFailureReason ? `: ${result.uploadFailureReason}` : "."}{" "} The bundle is still saved locally.
)} {result.path && (

{uploaded && result.uploadedKey ? "A local copy was also saved at:" : "Bundle saved to:"}

You may need admin privileges to open this file.

)}
); } function CopyableValue({ value, mono = false, large = false, }: { value: string; mono?: boolean; large?: boolean; }) { const [copied, setCopied] = useState(false); const onCopy = async () => { try { await navigator.clipboard.writeText(value); setCopied(true); setTimeout(() => setCopied(false), 1500); } catch { // ignore } }; const onReveal = () => { void Browser.OpenURL(`file://${value}`).catch(() => {}); }; return (
{value} {value.startsWith("/") || value.match(/^[A-Za-z]:\\/) ? ( ) : null}
); } const LEGAL_LINKS: { label: string; url: string }[] = [ { label: "Imprint", url: "https://netbird.io/imprint" }, { label: "Privacy", url: "https://netbird.io/privacy" }, { label: "CLA", url: "https://netbird.io/cla" }, { label: "Terms of Service", url: "https://netbird.io/terms" }, ]; function openUrl(url: string) { void Browser.OpenURL(url).catch(() => window.open(url, "_blank")); } function AboutSection() { const { status } = useStatus(); const guiVersion = pkg.version; const daemonVersion = status?.daemonVersion ?? "—"; const updateVersion = (status?.events ?? []) .map((e) => e.metadata?.["new_version_available"]) .find((v): v is string => Boolean(v)); const triggerUpdate = () => { UpdateSvc.Trigger().catch(() => {}); }; return (
{"NetBird"}

NetBird

GUI v{guiVersion}
Client v{daemonVersion}
{updateVersion && (

Version {updateVersion} is available.

)}

© {new Date().getFullYear()} NetBird. All Rights Reserved.

{LEGAL_LINKS.map((link, i) => ( {i > 0 && ( · )} ))}
); }