mirror of
https://github.com/Quad4-Software/MeshChatX.git
synced 2026-08-18 09:49:09 -04:00
feat: implement optional seccomp-BPF syscall denylist alongside Landlock and improve tools/UI
This commit is contained in:
parent
3547991f28
commit
b27028a7fb
25 changed files with 831 additions and 275 deletions
|
|
@ -32,6 +32,7 @@ All notable changes to this project will be documented in this file.
|
|||
- Post-install prompts for existing users after upgrades
|
||||
- Coolify-oriented Docker Compose with resource limits for deployments
|
||||
- LXST telephony half-duplex mode, live duplex switching, push-to-talk (packetizer squelch), and richer in-call stats (rates plus mute state on the active call)
|
||||
- Optional Linux seccomp-BPF syscall denylist (libseccomp) alongside Landlock, with auto-detect and `MESHCHAT_SECCOMP` fallback
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
|
|||
|
|
@ -40,4 +40,5 @@ For live stress, run Landlock in a **subprocess** (sandbox applies once per proc
|
|||
- `meshchatx/src/backend/memory_pressure.py`
|
||||
- `meshchatx/src/backend/message_handler.py`
|
||||
- `meshchatx/src/backend/landlock_sandbox.py`
|
||||
- `meshchatx/src/backend/seccomp_sandbox.py` (syscall denylist after Landlock)
|
||||
- `meshchatx/meshchat.py` (conversations/notifications error mapping)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,19 @@ Privacy mode does not disable Reticulum mesh traffic. It limits clearnet fetches
|
|||
|
||||
## Linux sandboxing
|
||||
|
||||
Optional Landlock sandboxing on Linux restricts filesystem access for the backend. See **Linux sandboxing** in Platform guides for Firejail and Bubblewrap examples.
|
||||
On Linux, MeshChatX can enable two complementary in-process sandboxes when supported:
|
||||
|
||||
- **Landlock** restricts filesystem paths the backend may use
|
||||
- **Seccomp-BPF** installs a syscall denylist (via libseccomp) that blocks kernel-admin and related calls a mesh client does not need
|
||||
|
||||
Both auto-enable when available and fall back to a no-op when the platform, kernel, or libraries cannot support them. Override with:
|
||||
|
||||
- `MESHCHAT_LANDLOCK=0` or `1`
|
||||
- `MESHCHAT_SECCOMP=0` or `1`
|
||||
|
||||
Android never enables these in-process sandboxes (the Android app seccomp policy already constrains the process, and Landlock syscalls are blocked there).
|
||||
|
||||
See **Linux sandboxing** in Platform guides for optional Firejail and Bubblewrap wrappers around the host install.
|
||||
|
||||
## Blocking and filtering
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,13 @@ This page shows how to run **`meshchatx`** under **Firejail** or **Bubblewrap**
|
|||
|
||||
These tools do **not** replace a full virtual machine or hardware-enforced boundary. They reduce exposure of your home directory and other paths the process can write to, when you configure them with tight whitelists or bind mounts.
|
||||
|
||||
MeshChatX also applies optional **in-process** Linux sandboxes when available:
|
||||
|
||||
- **Landlock** for filesystem path rules (`MESHCHAT_LANDLOCK=0` to disable)
|
||||
- **Seccomp-BPF** syscall denylist via libseccomp (`MESHCHAT_SECCOMP=0` to disable)
|
||||
|
||||
Those layers fall back cleanly when unsupported. Firejail and Bubblewrap remain useful as an outer wrapper.
|
||||
|
||||
**Containers:** If you already run MeshChatX with Docker or Podman, that is a different isolation model, this document is aimed at **host-installed** `meshchatx` (or `meshchat`).
|
||||
|
||||
## Prerequisites
|
||||
|
|
|
|||
BIN
meshchatx.rsm
BIN
meshchatx.rsm
Binary file not shown.
|
|
@ -105,6 +105,13 @@ from meshchatx.src.backend.landlock_sandbox import (
|
|||
landlock_kernel_supported,
|
||||
landlock_requested,
|
||||
)
|
||||
from meshchatx.src.backend.seccomp_sandbox import (
|
||||
apply_seccomp_sandbox,
|
||||
seccomp_auto_enabled,
|
||||
seccomp_disabled_by_env,
|
||||
seccomp_kernel_supported,
|
||||
seccomp_requested,
|
||||
)
|
||||
from meshchatx.src.backend.legacy_migrator import (
|
||||
assert_migration_context_paths,
|
||||
fresh_storage_at_target,
|
||||
|
|
@ -508,6 +515,7 @@ class ReticulumMeshChat:
|
|||
self.listen_port: int | None = None
|
||||
self.use_https: bool = True
|
||||
self.landlock_active: bool = False
|
||||
self.seccomp_active: bool = False
|
||||
self._pending_identity = identity
|
||||
self._network_setup_lock = threading.Lock()
|
||||
self._network_ready_event = threading.Event()
|
||||
|
|
@ -4912,6 +4920,11 @@ class ReticulumMeshChat:
|
|||
"landlock_auto_enabled": landlock_auto_enabled(),
|
||||
"landlock_disabled_by_env": landlock_disabled_by_env(),
|
||||
"landlock_active": self.landlock_active,
|
||||
"seccomp_kernel_supported": seccomp_kernel_supported(),
|
||||
"seccomp_requested": seccomp_requested(),
|
||||
"seccomp_auto_enabled": seccomp_auto_enabled(),
|
||||
"seccomp_disabled_by_env": seccomp_disabled_by_env(),
|
||||
"seccomp_active": self.seccomp_active,
|
||||
}
|
||||
|
||||
def get_routes(self):
|
||||
|
|
@ -25036,6 +25049,8 @@ def main():
|
|||
public_dir=reticulum_meshchat.public_dir_override or get_file_path("public"),
|
||||
log_dir=resolve_log_dir(),
|
||||
)
|
||||
# Apply after Landlock so landlock_* syscalls are not blocked by the filter.
|
||||
reticulum_meshchat.seccomp_active = apply_seccomp_sandbox()
|
||||
reticulum_meshchat.run(
|
||||
args.host,
|
||||
args.port,
|
||||
|
|
|
|||
268
meshchatx/src/backend/seccomp_sandbox.py
Normal file
268
meshchatx/src/backend/seccomp_sandbox.py
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Optional seccomp-BPF syscall denylist for the backend (Linux only).
|
||||
|
||||
Complements Landlock filesystem rules. Default action is ALLOW with a small
|
||||
denylist of kernel-admin and process-introspection syscalls a mesh client never
|
||||
needs. When libseccomp or the kernel filter is unavailable, apply falls back to
|
||||
a no-op so the process still starts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
logger = logging.getLogger("meshchatx.seccomp")
|
||||
|
||||
# From linux/seccomp.h / libseccomp.h
|
||||
_SCMP_ACT_ALLOW = 0x7FFF0000
|
||||
_SCMP_ACT_ERRNO_BASE = 0x00050000
|
||||
|
||||
# Dangerous or unused by MeshChatX. Names are resolved via libseccomp so unknown
|
||||
# names on older kernels are skipped instead of failing the whole filter.
|
||||
_DENIED_SYSCALLS = (
|
||||
"mount",
|
||||
"umount",
|
||||
"umount2",
|
||||
"pivot_root",
|
||||
"reboot",
|
||||
"kexec_load",
|
||||
"kexec_file_load",
|
||||
"init_module",
|
||||
"finit_module",
|
||||
"delete_module",
|
||||
"swapon",
|
||||
"swapoff",
|
||||
"bpf",
|
||||
"userfaultfd",
|
||||
"open_by_handle_at",
|
||||
"name_to_handle_at",
|
||||
"setns",
|
||||
"unshare",
|
||||
"acct",
|
||||
"ioperm",
|
||||
"iopl",
|
||||
"perf_event_open",
|
||||
"ptrace",
|
||||
"process_vm_readv",
|
||||
"process_vm_writev",
|
||||
"syslog",
|
||||
"quotactl",
|
||||
"lookup_dcookie",
|
||||
"fanotify_init",
|
||||
"open_tree",
|
||||
"move_mount",
|
||||
"fsopen",
|
||||
"fsconfig",
|
||||
"fsmount",
|
||||
"fspick",
|
||||
"mount_setattr",
|
||||
"vhangup",
|
||||
"uselib",
|
||||
"create_module",
|
||||
"query_module",
|
||||
"get_kernel_syms",
|
||||
"nfsservctl",
|
||||
"vm86",
|
||||
"vm86old",
|
||||
"_sysctl",
|
||||
"modify_ldt",
|
||||
)
|
||||
|
||||
|
||||
def _seccomp_env_override() -> bool | None:
|
||||
raw = os.environ.get("MESHCHAT_SECCOMP")
|
||||
if raw is None:
|
||||
return None
|
||||
val = raw.strip().lower()
|
||||
if val in ("false", "0", "no", "off"):
|
||||
return False
|
||||
if val in ("true", "1", "yes", "on"):
|
||||
return True
|
||||
return None
|
||||
|
||||
|
||||
def _is_android() -> bool:
|
||||
return hasattr(sys, "getandroidapilevel")
|
||||
|
||||
|
||||
_seccomp_support_cached: bool | None = None
|
||||
_seccomp_lib_cached = None
|
||||
_seccomp_lib_failed = False
|
||||
|
||||
|
||||
def _load_libseccomp():
|
||||
"""Return a loaded libseccomp CDLL, or None when unavailable."""
|
||||
global _seccomp_lib_cached, _seccomp_lib_failed
|
||||
if _seccomp_lib_failed:
|
||||
return None
|
||||
if _seccomp_lib_cached is not None:
|
||||
return _seccomp_lib_cached
|
||||
|
||||
candidates: list[str | None] = [
|
||||
ctypes.util.find_library("seccomp"),
|
||||
"libseccomp.so.2",
|
||||
"libseccomp.so",
|
||||
]
|
||||
for name in candidates:
|
||||
if not name:
|
||||
continue
|
||||
try:
|
||||
lib = ctypes.CDLL(name)
|
||||
except OSError:
|
||||
continue
|
||||
required = (
|
||||
"seccomp_init",
|
||||
"seccomp_rule_add",
|
||||
"seccomp_load",
|
||||
"seccomp_release",
|
||||
"seccomp_syscall_resolve_name",
|
||||
)
|
||||
if any(not hasattr(lib, attr) for attr in required):
|
||||
continue
|
||||
|
||||
lib.seccomp_init.argtypes = [ctypes.c_uint32]
|
||||
lib.seccomp_init.restype = ctypes.c_void_p
|
||||
lib.seccomp_rule_add.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_uint32,
|
||||
ctypes.c_int,
|
||||
ctypes.c_uint,
|
||||
]
|
||||
lib.seccomp_rule_add.restype = ctypes.c_int
|
||||
lib.seccomp_load.argtypes = [ctypes.c_void_p]
|
||||
lib.seccomp_load.restype = ctypes.c_int
|
||||
lib.seccomp_release.argtypes = [ctypes.c_void_p]
|
||||
lib.seccomp_release.restype = None
|
||||
lib.seccomp_syscall_resolve_name.argtypes = [ctypes.c_char_p]
|
||||
lib.seccomp_syscall_resolve_name.restype = ctypes.c_int
|
||||
|
||||
_seccomp_lib_cached = lib
|
||||
return lib
|
||||
|
||||
_seccomp_lib_failed = True
|
||||
return None
|
||||
|
||||
|
||||
def _act_errno(code: int = errno.EPERM) -> int:
|
||||
return _SCMP_ACT_ERRNO_BASE | (int(code) & 0xFFFF)
|
||||
|
||||
|
||||
def seccomp_library_available() -> bool:
|
||||
"""Return True when libseccomp can be loaded with the symbols we need."""
|
||||
return _load_libseccomp() is not None
|
||||
|
||||
|
||||
def seccomp_kernel_supported() -> bool:
|
||||
"""Return True when this host can install a user seccomp-BPF filter."""
|
||||
global _seccomp_support_cached
|
||||
if _seccomp_support_cached is not None:
|
||||
return _seccomp_support_cached
|
||||
if sys.platform != "linux" or _is_android():
|
||||
_seccomp_support_cached = False
|
||||
return False
|
||||
if not seccomp_library_available():
|
||||
_seccomp_support_cached = False
|
||||
return False
|
||||
# Probe by building an empty ALLOW filter without loading it.
|
||||
lib = _load_libseccomp()
|
||||
if lib is None:
|
||||
_seccomp_support_cached = False
|
||||
return False
|
||||
ctx = lib.seccomp_init(_SCMP_ACT_ALLOW)
|
||||
if not ctx:
|
||||
_seccomp_support_cached = False
|
||||
return False
|
||||
lib.seccomp_release(ctx)
|
||||
_seccomp_support_cached = True
|
||||
return True
|
||||
|
||||
|
||||
def seccomp_requested() -> bool:
|
||||
if sys.platform != "linux" or _is_android():
|
||||
return False
|
||||
override = _seccomp_env_override()
|
||||
if override is False:
|
||||
return False
|
||||
if override is True:
|
||||
return True
|
||||
return seccomp_kernel_supported()
|
||||
|
||||
|
||||
def seccomp_auto_enabled() -> bool:
|
||||
return seccomp_requested() and _seccomp_env_override() is None
|
||||
|
||||
|
||||
def seccomp_disabled_by_env() -> bool:
|
||||
return _seccomp_env_override() is False
|
||||
|
||||
|
||||
def apply_seccomp_sandbox() -> bool:
|
||||
"""Install the denylist filter. Returns True when seccomp-BPF is active.
|
||||
|
||||
Falls back to False (no filter) when unsupported, forced off, or install
|
||||
fails. Never raises into the caller for probe or load failures.
|
||||
"""
|
||||
if not seccomp_requested():
|
||||
return False
|
||||
|
||||
lib = _load_libseccomp()
|
||||
if lib is None:
|
||||
logger.warning(
|
||||
"Seccomp requested but libseccomp is unavailable; continuing without it",
|
||||
)
|
||||
return False
|
||||
|
||||
ctx = lib.seccomp_init(_SCMP_ACT_ALLOW)
|
||||
if not ctx:
|
||||
logger.warning("Seccomp disabled: seccomp_init failed")
|
||||
return False
|
||||
|
||||
denied = 0
|
||||
loaded = False
|
||||
try:
|
||||
for name in _DENIED_SYSCALLS:
|
||||
nr = lib.seccomp_syscall_resolve_name(name.encode("ascii"))
|
||||
if nr < 0:
|
||||
continue
|
||||
rc = lib.seccomp_rule_add(ctx, _act_errno(errno.EPERM), nr, 0)
|
||||
if rc != 0:
|
||||
logger.debug(
|
||||
"Seccomp skip rule for %s: libseccomp rc %s",
|
||||
name,
|
||||
rc,
|
||||
)
|
||||
continue
|
||||
denied += 1
|
||||
|
||||
if denied == 0:
|
||||
logger.warning("Seccomp disabled: no denylist rules could be installed")
|
||||
return False
|
||||
|
||||
rc = lib.seccomp_load(ctx)
|
||||
if rc != 0:
|
||||
logger.warning("Seccomp disabled: seccomp_load failed with rc %s", rc)
|
||||
return False
|
||||
loaded = True
|
||||
except Exception as exc:
|
||||
logger.warning("Seccomp disabled: %s", exc)
|
||||
return False
|
||||
finally:
|
||||
lib.seccomp_release(ctx)
|
||||
|
||||
if not loaded:
|
||||
return False
|
||||
|
||||
if seccomp_auto_enabled():
|
||||
logger.info(
|
||||
"Seccomp-BPF syscall denylist enabled (auto-detected, %s rules)",
|
||||
denied,
|
||||
)
|
||||
else:
|
||||
logger.info("Seccomp-BPF syscall denylist enabled (%s rules)", denied)
|
||||
return True
|
||||
|
|
@ -8,27 +8,32 @@
|
|||
:description="$t('tools.rnstatus.description')"
|
||||
:eyebrow="$t('rnprobe.network_diagnostics')"
|
||||
accent="orange"
|
||||
/>
|
||||
<div
|
||||
class="flex-1 overflow-y-auto w-full px-4 md:px-5 lg:px-8 py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))]"
|
||||
>
|
||||
<div class="space-y-4 w-full max-w-6xl mx-auto">
|
||||
<div class="glass-card space-y-5 rounded-2xl border border-slate-200/70 p-5 dark:border-zinc-700/80">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="primary-chip inline-flex items-center gap-2 px-4 py-2 text-sm"
|
||||
:disabled="isLoading || reloadingRns"
|
||||
@click="refreshStatus"
|
||||
<template #actions>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg bg-orange-600 text-white text-sm font-medium hover:bg-orange-700 disabled:opacity-50 disabled:pointer-events-none"
|
||||
:disabled="isLoading || reloadingRns"
|
||||
@click="refreshStatus"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
icon-name="refresh"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:class="{ 'animate-spin-reverse': isLoading || reloadingRns }"
|
||||
/>
|
||||
<span class="hidden sm:inline">{{
|
||||
reloadingRns ? $t("rnstatus.reloading") : $t("rnstatus.refresh")
|
||||
}}</span>
|
||||
</button>
|
||||
</template>
|
||||
</ToolsPageHeader>
|
||||
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full px-3 sm:px-5 md:px-5 lg:px-8 py-3 sm:py-4 min-w-0">
|
||||
<div class="space-y-0 w-full max-w-6xl xl:max-w-7xl mx-auto min-w-0">
|
||||
<div class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 space-y-3">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<label
|
||||
class="inline-flex cursor-pointer items-center gap-2 rounded-lg border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 px-3 py-2 text-sm text-gray-800 dark:text-zinc-200"
|
||||
>
|
||||
<MaterialDesignIcon
|
||||
icon-name="refresh"
|
||||
class="h-4 w-4 shrink-0"
|
||||
:class="{ 'animate-spin-reverse': isLoading || reloadingRns }"
|
||||
/>
|
||||
{{ reloadingRns ? $t("rnstatus.reloading") : $t("rnstatus.refresh") }}
|
||||
</button>
|
||||
<label class="secondary-chip inline-flex cursor-pointer items-center gap-2 px-4 py-2 text-sm">
|
||||
<input
|
||||
v-model="includeLinkStats"
|
||||
type="checkbox"
|
||||
|
|
@ -51,77 +56,80 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 border-t border-slate-200/70 pt-4 dark:border-zinc-700/80">
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ $t("rnstatus.remote_query") }}
|
||||
</div>
|
||||
<p class="text-xs text-gray-500 dark:text-zinc-500">
|
||||
{{ $t("rnstatus.remote_query_hint") }}
|
||||
</p>
|
||||
<div class="grid gap-3 lg:grid-cols-2">
|
||||
<label class="block space-y-1">
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-zinc-300">{{
|
||||
$t("rnstatus.remote_transport_hash")
|
||||
}}</span>
|
||||
<input
|
||||
v-model="remoteHash"
|
||||
type="text"
|
||||
class="input-field font-mono text-xs"
|
||||
:placeholder="$t('rnstatus.remote_transport_placeholder')"
|
||||
:disabled="reloadingRns"
|
||||
/>
|
||||
</label>
|
||||
<label class="block space-y-1">
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-zinc-300">{{
|
||||
$t("rnstatus.remote_timeout")
|
||||
}}</span>
|
||||
<input
|
||||
v-model.number="remoteTimeout"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
class="input-field text-sm"
|
||||
:disabled="reloadingRns"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<ManagementIdentityPicker v-model="identityPath" :disabled="reloadingRns" default-name="mgmt" />
|
||||
<div v-if="activeRemoteHash" class="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span class="font-mono text-amber-700 dark:text-amber-300">{{
|
||||
$t("rnstatus.remote_active", { hash: activeRemoteHash })
|
||||
<div class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 space-y-3">
|
||||
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{ $t("rnstatus.remote_query") }}
|
||||
</h2>
|
||||
<p class="text-xs text-gray-500 dark:text-zinc-500">
|
||||
{{ $t("rnstatus.remote_query_hint") }}
|
||||
</p>
|
||||
<div class="grid gap-3 lg:grid-cols-2">
|
||||
<label class="block space-y-1">
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-zinc-300">{{
|
||||
$t("rnstatus.remote_transport_hash")
|
||||
}}</span>
|
||||
<button type="button" class="secondary-chip px-2 py-1 text-xs" @click="clearRemote">
|
||||
{{ $t("rnstatus.use_local") }}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
v-model="remoteHash"
|
||||
type="text"
|
||||
class="input-field font-mono text-xs"
|
||||
:placeholder="$t('rnstatus.remote_transport_placeholder')"
|
||||
:disabled="reloadingRns"
|
||||
/>
|
||||
</label>
|
||||
<label class="block space-y-1">
|
||||
<span class="text-xs font-medium text-gray-700 dark:text-zinc-300">{{
|
||||
$t("rnstatus.remote_timeout")
|
||||
}}</span>
|
||||
<input
|
||||
v-model.number="remoteTimeout"
|
||||
type="number"
|
||||
min="1"
|
||||
step="1"
|
||||
class="input-field text-sm"
|
||||
:disabled="reloadingRns"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<ManagementIdentityPicker v-model="identityPath" :disabled="reloadingRns" default-name="mgmt" />
|
||||
<div v-if="activeRemoteHash" class="flex flex-wrap items-center gap-2 text-xs">
|
||||
<span class="font-mono text-amber-700 dark:text-amber-300">{{
|
||||
$t("rnstatus.remote_active", { hash: activeRemoteHash })
|
||||
}}</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center px-2 py-1 rounded-lg border border-gray-300 dark:border-zinc-600 bg-white dark:bg-zinc-900 text-xs font-medium text-gray-800 dark:text-zinc-200 hover:bg-gray-50 dark:hover:bg-zinc-800"
|
||||
@click="clearRemote"
|
||||
>
|
||||
{{ $t("rnstatus.use_local") }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
||||
<div
|
||||
v-if="linkCount !== null || blackholeEnabled !== null"
|
||||
class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div
|
||||
v-if="linkCount !== null"
|
||||
class="rounded-xl border border-blue-200/80 bg-blue-50/90 p-4 text-blue-800 dark:border-blue-800/50 dark:bg-blue-950/30 dark:text-blue-200"
|
||||
class="text-sm font-semibold text-gray-900 dark:text-white tabular-nums"
|
||||
>
|
||||
<div class="text-sm font-semibold">
|
||||
{{ $t("rnstatus.active_links", { count: formatInt(linkCount) }) }}
|
||||
</div>
|
||||
{{ $t("rnstatus.active_links", { count: formatInt(linkCount) }) }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="blackholeEnabled !== null"
|
||||
class="rounded-xl border border-purple-200/80 bg-purple-50/90 p-4 text-purple-900 dark:border-purple-800/50 dark:bg-purple-950/30 dark:text-purple-100"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 font-semibold">
|
||||
<span>{{
|
||||
<div v-if="blackholeEnabled !== null" class="space-y-0.5">
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-white">
|
||||
{{
|
||||
$t("rnstatus.blackhole_label", {
|
||||
state: blackholeEnabled
|
||||
? $t("rnstatus.blackhole_publishing")
|
||||
: $t("rnstatus.blackhole_inactive"),
|
||||
})
|
||||
}}</span>
|
||||
<span class="text-sm font-normal opacity-90">
|
||||
{{ formatInt(blackholeCount) }} Identities
|
||||
</span>
|
||||
}}
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 dark:text-zinc-500 tabular-nums">
|
||||
{{ formatInt(blackholeCount) }} Identities
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -129,14 +137,14 @@
|
|||
|
||||
<div
|
||||
v-if="blackholeSources.length > 0"
|
||||
class="glass-card space-y-3 rounded-2xl border border-slate-200/70 p-5 dark:border-zinc-700/80"
|
||||
class="w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-4 sm:py-6 space-y-3"
|
||||
>
|
||||
<div class="font-semibold text-lg text-gray-900 dark:text-white">Blackhole Sources</div>
|
||||
<div class="grid gap-2">
|
||||
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">Blackhole Sources</h2>
|
||||
<div class="divide-y divide-gray-100 dark:divide-zinc-800/50">
|
||||
<div
|
||||
v-for="source in blackholeSources"
|
||||
:key="source"
|
||||
class="text-sm font-mono bg-gray-50 dark:bg-gray-800 p-2 rounded-sm truncate"
|
||||
class="py-2 text-sm font-mono text-gray-800 dark:text-zinc-200 truncate"
|
||||
>
|
||||
{{ source }}
|
||||
</div>
|
||||
|
|
@ -145,145 +153,161 @@
|
|||
|
||||
<div
|
||||
v-if="interfaces.length === 0 && !isLoading && !reloadingRns"
|
||||
class="glass-card p-8 text-center text-gray-500 dark:text-gray-400"
|
||||
class="w-full py-8 sm:py-12 text-center text-sm text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{{ $t("rnstatus.no_interfaces_found") }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="iface in interfaces"
|
||||
:key="iface.name"
|
||||
class="glass-card overflow-hidden rounded-2xl border border-slate-200/70 dark:border-zinc-700/80"
|
||||
>
|
||||
<div
|
||||
class="flex flex-wrap items-start justify-between gap-3 border-b border-slate-100 bg-slate-50/60 px-4 py-4 dark:border-zinc-800/80 dark:bg-zinc-900/40 sm:px-5"
|
||||
>
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3
|
||||
class="wrap-break-word text-base font-semibold leading-snug text-gray-900 dark:text-white sm:text-lg"
|
||||
>
|
||||
{{ iface.name }}
|
||||
</h3>
|
||||
<span
|
||||
v-if="iface.discovered"
|
||||
class="inline-flex shrink-0 items-center rounded-md bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-900 dark:bg-amber-900/45 dark:text-amber-100"
|
||||
>
|
||||
Discovered
|
||||
</span>
|
||||
<div v-else class="w-full divide-y divide-gray-200/60 dark:divide-zinc-800/60">
|
||||
<div v-for="iface in interfaces" :key="iface.name" class="py-4 sm:py-6 space-y-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1 space-y-1">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<h3
|
||||
class="wrap-break-word text-base font-semibold leading-snug text-gray-900 dark:text-white"
|
||||
>
|
||||
{{ iface.name }}
|
||||
</h3>
|
||||
<span
|
||||
v-if="iface.discovered"
|
||||
class="inline-flex shrink-0 items-center rounded-md bg-amber-100 px-2 py-0.5 text-xs font-semibold text-amber-900 dark:bg-amber-900/45 dark:text-amber-100"
|
||||
>
|
||||
Discovered
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
:class="[
|
||||
iface.status === 'Up'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/45 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900/45 dark:text-red-100',
|
||||
'shrink-0 rounded-full px-3 py-1 text-xs font-semibold',
|
||||
]"
|
||||
>
|
||||
{{ iface.status }}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
:class="[
|
||||
iface.status === 'Up'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/45 dark:text-green-100'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900/45 dark:text-red-100',
|
||||
'shrink-0 rounded-full px-3 py-1 text-xs font-semibold',
|
||||
]"
|
||||
>
|
||||
{{ iface.status }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-x-6 gap-y-4 p-4 text-sm sm:p-5 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div v-if="iface.mode">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.mode") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.mode }}</div>
|
||||
</div>
|
||||
<div v-if="iface.bitrate">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.bitrate") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.bitrate }}</div>
|
||||
</div>
|
||||
<div v-if="iface.rx_bytes_str">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.rx_bytes") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.rx_bytes_str }}</div>
|
||||
</div>
|
||||
<div v-if="iface.tx_bytes_str">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.tx_bytes") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.tx_bytes_str }}</div>
|
||||
</div>
|
||||
<div v-if="iface.rx_packets !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.rx_packets") }}</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.rx_packets }}
|
||||
<div class="grid gap-x-6 gap-y-3 text-sm md:grid-cols-2 lg:grid-cols-3">
|
||||
<div v-if="iface.mode">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.mode") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.mode }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.tx_packets !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.tx_packets") }}</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.tx_packets }}
|
||||
<div v-if="iface.bitrate">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.bitrate") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.bitrate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.clients !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.clients") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatInt(iface.clients) }}
|
||||
<div v-if="iface.rx_bytes_str">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.rx_bytes") }}
|
||||
</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.rx_bytes_str }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.peers !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">Peers</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatInt(iface.peers) }} {{ $t("rnstatus.peers_reachable") }}
|
||||
<div v-if="iface.tx_bytes_str">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.tx_bytes") }}
|
||||
</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.tx_bytes_str }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.noise_floor">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.noise_floor") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.noise_floor }}</div>
|
||||
</div>
|
||||
<div v-if="iface.interference">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.interference") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.interference }}</div>
|
||||
</div>
|
||||
<div v-if="iface.cpu_load">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.cpu_load") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.cpu_load }}</div>
|
||||
</div>
|
||||
<div v-if="iface.cpu_temp">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.cpu_temp") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.cpu_temp }}</div>
|
||||
</div>
|
||||
<div v-if="iface.mem_load">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.memory_load") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.mem_load }}</div>
|
||||
</div>
|
||||
<div v-if="iface.battery_percent !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.battery") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatInt(iface.battery_percent) }}%<span v-if="iface.battery_state">
|
||||
({{ iface.battery_state }})</span
|
||||
>
|
||||
<div v-if="iface.rx_packets !== undefined">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.rx_packets") }}
|
||||
</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.rx_packets }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.network_name">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.network") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.network_name }}</div>
|
||||
</div>
|
||||
<div v-if="iface.incoming_announce_frequency !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.incoming_announces") }}
|
||||
<div v-if="iface.tx_packets !== undefined">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.tx_packets") }}
|
||||
</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.tx_packets }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.incoming_announce_frequency }}/s
|
||||
<div v-if="iface.clients !== undefined">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.clients") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatInt(iface.clients) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.outgoing_announce_frequency !== undefined">
|
||||
<div class="text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.outgoing_announces") }}
|
||||
<div v-if="iface.peers !== undefined">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">Peers</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatInt(iface.peers) }} {{ $t("rnstatus.peers_reachable") }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.outgoing_announce_frequency }}/s
|
||||
<div v-if="iface.noise_floor">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.noise_floor") }}
|
||||
</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.noise_floor }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.airtime">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.airtime") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ iface.airtime.short }}% (15s), {{ iface.airtime.long }}% (1h)
|
||||
<div v-if="iface.interference">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.interference") }}
|
||||
</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.interference }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.channel_load">
|
||||
<div class="text-gray-500 dark:text-gray-400">{{ $t("rnstatus.channel_load") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ iface.channel_load.short }}% (15s), {{ iface.channel_load.long }}% (1h)
|
||||
<div v-if="iface.cpu_load">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.cpu_load") }}
|
||||
</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.cpu_load }}</div>
|
||||
</div>
|
||||
<div v-if="iface.cpu_temp">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.cpu_temp") }}
|
||||
</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.cpu_temp }}</div>
|
||||
</div>
|
||||
<div v-if="iface.mem_load">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.memory_load") }}
|
||||
</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.mem_load }}</div>
|
||||
</div>
|
||||
<div v-if="iface.battery_percent !== undefined">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.battery") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ formatInt(iface.battery_percent) }}%<span v-if="iface.battery_state">
|
||||
({{ iface.battery_state }})</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.network_name">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.network") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">{{ iface.network_name }}</div>
|
||||
</div>
|
||||
<div v-if="iface.incoming_announce_frequency !== undefined">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.incoming_announces") }}
|
||||
</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.incoming_announce_frequency }}/s
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.outgoing_announce_frequency !== undefined">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.outgoing_announces") }}
|
||||
</div>
|
||||
<div class="font-semibold tabular-nums text-gray-900 dark:text-white">
|
||||
{{ iface.outgoing_announce_frequency }}/s
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.airtime">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ $t("rnstatus.airtime") }}</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ iface.airtime.short }}% (15s), {{ iface.airtime.long }}% (1h)
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="iface.channel_load">
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ $t("rnstatus.channel_load") }}
|
||||
</div>
|
||||
<div class="font-semibold text-gray-900 dark:text-white">
|
||||
{{ iface.channel_load.short }}% (15s), {{ iface.channel_load.long }}% (1h)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3164,6 +3164,23 @@
|
|||
: $t("app.landlock_inactive")
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-if="serverSecurity.seccomp_requested !== undefined"
|
||||
class="text-xs text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{{ $t("app.seccomp_status") }}:
|
||||
{{
|
||||
serverSecurity.seccomp_active
|
||||
? serverSecurity.seccomp_auto_enabled
|
||||
? $t("app.seccomp_auto_enabled")
|
||||
: $t("app.seccomp_active")
|
||||
: serverSecurity.seccomp_kernel_supported === false
|
||||
? $t("app.seccomp_kernel_unsupported")
|
||||
: serverSecurity.seccomp_disabled_by_env
|
||||
? $t("app.seccomp_disabled_by_env")
|
||||
: $t("app.seccomp_inactive")
|
||||
}}
|
||||
</div>
|
||||
<div
|
||||
v-if="serverSecurity.is_loopback_bind === false"
|
||||
class="rounded-md border border-amber-500/40 bg-amber-500/10 p-4 space-y-3"
|
||||
|
|
@ -4050,6 +4067,11 @@ export default {
|
|||
landlock_kernel_supported: false,
|
||||
landlock_auto_enabled: false,
|
||||
landlock_disabled_by_env: false,
|
||||
seccomp_requested: false,
|
||||
seccomp_active: false,
|
||||
seccomp_kernel_supported: false,
|
||||
seccomp_auto_enabled: false,
|
||||
seccomp_disabled_by_env: false,
|
||||
},
|
||||
exposureAckFirewall: false,
|
||||
exposureAckVpn: false,
|
||||
|
|
|
|||
|
|
@ -39,32 +39,6 @@ export const CORE_TOOLS_ENTRIES = [
|
|||
titleKey: "tools.rnprobe.title",
|
||||
descriptionKey: "tools.rnprobe.description",
|
||||
},
|
||||
{
|
||||
name: "rncp",
|
||||
route: { name: "rncp" },
|
||||
icon: "swap-horizontal",
|
||||
iconBg: "tool-card__icon bg-green-50 text-green-500 dark:bg-green-900/30 dark:text-green-200",
|
||||
titleKey: "tools.rncp.title",
|
||||
descriptionKey: "tools.rncp.description",
|
||||
},
|
||||
{
|
||||
name: "rnsh",
|
||||
route: { name: "rnsh" },
|
||||
icon: "console-network-outline",
|
||||
iconBg: "tool-card__icon bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-200",
|
||||
titleKey: "tools.rnsh.title",
|
||||
descriptionKey: "tools.rnsh.description",
|
||||
alpha: true,
|
||||
},
|
||||
{
|
||||
name: "rnx",
|
||||
route: { name: "rnx" },
|
||||
icon: "console",
|
||||
iconBg: "tool-card__icon bg-teal-50 text-teal-600 dark:bg-teal-900/30 dark:text-teal-200",
|
||||
titleKey: "tools.rnx.title",
|
||||
descriptionKey: "tools.rnx.description",
|
||||
alpha: true,
|
||||
},
|
||||
{
|
||||
name: "rnstatus",
|
||||
route: { name: "rnstatus" },
|
||||
|
|
@ -90,20 +64,39 @@ export const CORE_TOOLS_ENTRIES = [
|
|||
descriptionKey: "tools.rnpath_trace.description",
|
||||
},
|
||||
{
|
||||
name: "translator",
|
||||
route: { name: "translator" },
|
||||
icon: "translate",
|
||||
iconBg: "tool-card__icon bg-indigo-50 text-indigo-500 dark:bg-indigo-900/30 dark:text-indigo-200",
|
||||
titleKey: "tools.translator.title",
|
||||
descriptionKey: "tools.translator.description",
|
||||
name: "rncp",
|
||||
route: { name: "rncp" },
|
||||
icon: "swap-horizontal",
|
||||
iconBg: "tool-card__icon bg-green-50 text-green-500 dark:bg-green-900/30 dark:text-green-200",
|
||||
titleKey: "tools.rncp.title",
|
||||
descriptionKey: "tools.rncp.description",
|
||||
},
|
||||
{
|
||||
name: "bots",
|
||||
route: { name: "bots" },
|
||||
icon: "robot",
|
||||
iconBg: "tool-card__icon bg-blue-50 text-blue-500 dark:bg-blue-900/30 dark:text-blue-200",
|
||||
titleKey: "tools.bots.title",
|
||||
descriptionKey: "tools.bots.description",
|
||||
name: "rns-filesync",
|
||||
route: { name: "rns-filesync" },
|
||||
icon: "folder-sync",
|
||||
iconBg: "tool-card__icon bg-emerald-50 text-emerald-500 dark:bg-emerald-900/30 dark:text-emerald-200",
|
||||
titleKey: "tools.rns_filesync.title",
|
||||
descriptionKey: "tools.rns_filesync.description",
|
||||
alpha: true,
|
||||
},
|
||||
{
|
||||
name: "rnsh",
|
||||
route: { name: "rnsh" },
|
||||
icon: "console-network-outline",
|
||||
iconBg: "tool-card__icon bg-indigo-50 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-200",
|
||||
titleKey: "tools.rnsh.title",
|
||||
descriptionKey: "tools.rnsh.description",
|
||||
alpha: true,
|
||||
},
|
||||
{
|
||||
name: "rnx",
|
||||
route: { name: "rnx" },
|
||||
icon: "console",
|
||||
iconBg: "tool-card__icon bg-teal-50 text-teal-600 dark:bg-teal-900/30 dark:text-teal-200",
|
||||
titleKey: "tools.rnx.title",
|
||||
descriptionKey: "tools.rnx.description",
|
||||
alpha: true,
|
||||
},
|
||||
{
|
||||
name: "propagation-nodes",
|
||||
|
|
@ -138,6 +131,38 @@ export const CORE_TOOLS_ENTRIES = [
|
|||
descriptionKey: "tools.message_blocklist.description",
|
||||
beta: true,
|
||||
},
|
||||
{
|
||||
name: "bots",
|
||||
route: { name: "bots" },
|
||||
icon: "robot",
|
||||
iconBg: "tool-card__icon bg-blue-50 text-blue-500 dark:bg-blue-900/30 dark:text-blue-200",
|
||||
titleKey: "tools.bots.title",
|
||||
descriptionKey: "tools.bots.description",
|
||||
},
|
||||
{
|
||||
name: "paper-message",
|
||||
route: { name: "paper-message" },
|
||||
icon: "qrcode",
|
||||
iconBg: "tool-card__icon bg-blue-50 text-blue-500 dark:bg-blue-900/30 dark:text-blue-200",
|
||||
titleKey: "tools.paper_message.title",
|
||||
descriptionKey: "tools.paper_message.description",
|
||||
},
|
||||
{
|
||||
name: "translator",
|
||||
route: { name: "translator" },
|
||||
icon: "translate",
|
||||
iconBg: "tool-card__icon bg-indigo-50 text-indigo-500 dark:bg-indigo-900/30 dark:text-indigo-200",
|
||||
titleKey: "tools.translator.title",
|
||||
descriptionKey: "tools.translator.description",
|
||||
},
|
||||
{
|
||||
name: "micron-editor",
|
||||
route: { name: "micron-editor" },
|
||||
icon: "code-tags",
|
||||
iconBg: "tool-card__icon bg-teal-50 text-teal-500 dark:bg-teal-900/30 dark:text-teal-200",
|
||||
titleKey: "tools.micron_editor.title",
|
||||
descriptionKey: "tools.micron_editor.description",
|
||||
},
|
||||
{
|
||||
name: "documentation",
|
||||
route: { name: "documentation" },
|
||||
|
|
@ -154,14 +179,6 @@ export const CORE_TOOLS_ENTRIES = [
|
|||
titleKey: "tools.repository_server.title",
|
||||
descriptionKey: "tools.repository_server.description",
|
||||
},
|
||||
{
|
||||
name: "micron-editor",
|
||||
route: { name: "micron-editor" },
|
||||
icon: "code-tags",
|
||||
iconBg: "tool-card__icon bg-teal-50 text-teal-500 dark:bg-teal-900/30 dark:text-teal-200",
|
||||
titleKey: "tools.micron_editor.title",
|
||||
descriptionKey: "tools.micron_editor.description",
|
||||
},
|
||||
{
|
||||
name: "reticulum-config-editor",
|
||||
route: { name: "reticulum-config-editor" },
|
||||
|
|
@ -170,14 +187,6 @@ export const CORE_TOOLS_ENTRIES = [
|
|||
titleKey: "tools.reticulum_config_editor.title",
|
||||
descriptionKey: "tools.reticulum_config_editor.description",
|
||||
},
|
||||
{
|
||||
name: "paper-message",
|
||||
route: { name: "paper-message" },
|
||||
icon: "qrcode",
|
||||
iconBg: "tool-card__icon bg-blue-50 text-blue-500 dark:bg-blue-900/30 dark:text-blue-200",
|
||||
titleKey: "tools.paper_message.title",
|
||||
descriptionKey: "tools.paper_message.description",
|
||||
},
|
||||
{
|
||||
name: "rnode-flasher",
|
||||
route: { name: "rnode-flasher" },
|
||||
|
|
@ -210,14 +219,6 @@ export const CORE_TOOLS_ENTRIES = [
|
|||
titleKey: "tools.rns_tunnel.title",
|
||||
descriptionKey: "tools.rns_tunnel.description",
|
||||
},
|
||||
{
|
||||
name: "rns-filesync",
|
||||
route: { name: "rns-filesync" },
|
||||
icon: "folder-sync",
|
||||
iconBg: "tool-card__icon bg-emerald-50 text-emerald-500 dark:bg-emerald-900/30 dark:text-emerald-200",
|
||||
titleKey: "tools.rns_filesync.title",
|
||||
descriptionKey: "tools.rns_filesync.description",
|
||||
},
|
||||
{
|
||||
name: "debug-logs",
|
||||
route: { name: "debug-logs" },
|
||||
|
|
|
|||
|
|
@ -515,7 +515,13 @@
|
|||
"cancel_inbound_deliveries_count": "Eingehende abbrechen ({count})",
|
||||
"cancel_inbound_confirm": "{count} eingehende LXMF-Übertragung(en) abbrechen? Große Nachrichten, die gerade heruntergeladen werden, werden gestoppt.",
|
||||
"cancel_inbound_done": "{count} eingehende Übertragung(en) abgebrochen.",
|
||||
"cancel_inbound_failed": "Eingehende Übertragungen konnten nicht abgebrochen werden"
|
||||
"cancel_inbound_failed": "Eingehende Übertragungen konnten nicht abgebrochen werden",
|
||||
"seccomp_status": "Seccomp-BPF-Sandbox (Linux)",
|
||||
"seccomp_active": "Aktiv",
|
||||
"seccomp_inactive": "Nicht aktiv",
|
||||
"seccomp_auto_enabled": "Automatisch aktiviert, wenn libseccomp verfügbar ist",
|
||||
"seccomp_kernel_unsupported": "Seccomp-BPF nicht verfügbar (libseccomp installieren oder Plattform nicht unterstützt)",
|
||||
"seccomp_disabled_by_env": "Deaktiviert über MESHCHAT_SECCOMP=0"
|
||||
},
|
||||
"common": {
|
||||
"open": "Öffnen",
|
||||
|
|
|
|||
|
|
@ -224,6 +224,12 @@
|
|||
"landlock_auto_enabled": "Enabled automatically on this Linux kernel",
|
||||
"landlock_kernel_unsupported": "Kernel does not support Landlock (5.13+ required)",
|
||||
"landlock_disabled_by_env": "Disabled via MESHCHAT_LANDLOCK=0",
|
||||
"seccomp_status": "Seccomp-BPF sandbox (Linux)",
|
||||
"seccomp_active": "Active",
|
||||
"seccomp_inactive": "Not active",
|
||||
"seccomp_auto_enabled": "Enabled automatically when libseccomp is available",
|
||||
"seccomp_kernel_unsupported": "Seccomp-BPF unavailable (install libseccomp, or unsupported platform)",
|
||||
"seccomp_disabled_by_env": "Disabled via MESHCHAT_SECCOMP=0",
|
||||
"settings_map_eyebrow": "Map",
|
||||
"messages_description": "Configure how MeshChat handles message delivery failures. Control automatic retry behavior, attachment retransmission, and fallback mechanisms to ensure reliable message delivery across the mesh network.",
|
||||
"auto_resend_title": "Auto resend when peer announces",
|
||||
|
|
|
|||
|
|
@ -515,7 +515,13 @@
|
|||
"cancel_inbound_deliveries_count": "Cancelar entrantes ({count})",
|
||||
"cancel_inbound_confirm": "¿Cancelar {count} transferencia(s) entrante(s) de LXMF? Se detendrán los mensajes grandes que se estén descargando.",
|
||||
"cancel_inbound_done": "Se cancelaron {count} transferencia(s) entrante(s).",
|
||||
"cancel_inbound_failed": "No se pudieron cancelar las entregas entrantes"
|
||||
"cancel_inbound_failed": "No se pudieron cancelar las entregas entrantes",
|
||||
"seccomp_status": "Sandbox Seccomp-BPF (Linux)",
|
||||
"seccomp_active": "Activo",
|
||||
"seccomp_inactive": "Inactivo",
|
||||
"seccomp_auto_enabled": "Activado automáticamente cuando libseccomp está disponible",
|
||||
"seccomp_kernel_unsupported": "Seccomp-BPF no disponible (instala libseccomp o plataforma no compatible)",
|
||||
"seccomp_disabled_by_env": "Desactivado con MESHCHAT_SECCOMP=0"
|
||||
},
|
||||
"common": {
|
||||
"open": "Abierto",
|
||||
|
|
|
|||
|
|
@ -515,7 +515,13 @@
|
|||
"cancel_inbound_deliveries_count": "Peruuta saapuvat ({count})",
|
||||
"cancel_inbound_confirm": "Perutaanko {count} saapuvaa LXMF-siirtoa? Suurten, juuri ladattavien viestien lataus pysähtyy.",
|
||||
"cancel_inbound_done": "Peruttiin {count} saapuvaa siirtoa.",
|
||||
"cancel_inbound_failed": "Saapuvia siirtoja ei voitu perua"
|
||||
"cancel_inbound_failed": "Saapuvia siirtoja ei voitu perua",
|
||||
"seccomp_status": "Seccomp-BPF-hiekkalaatikko (Linux)",
|
||||
"seccomp_active": "Aktiivinen",
|
||||
"seccomp_inactive": "Ei aktiivinen",
|
||||
"seccomp_auto_enabled": "Käytössä automaattisesti, kun libseccomp on saatavilla",
|
||||
"seccomp_kernel_unsupported": "Seccomp-BPF ei ole käytettävissä (asenna libseccomp tai alusta ei tuettu)",
|
||||
"seccomp_disabled_by_env": "Pois käytöstä MESHCHAT_SECCOMP=0"
|
||||
},
|
||||
"common": {
|
||||
"open": "Avaa",
|
||||
|
|
|
|||
|
|
@ -515,7 +515,13 @@
|
|||
"cancel_inbound_deliveries_count": "Annuler entrants ({count})",
|
||||
"cancel_inbound_confirm": "Annuler {count} transfert(s) entrant(s) LXMF ? Les grands messages en cours de téléchargement seront arrêtés.",
|
||||
"cancel_inbound_done": "{count} transfert(s) entrant(s) annulé(s).",
|
||||
"cancel_inbound_failed": "Échec de l'annulation des livraisons entrantes"
|
||||
"cancel_inbound_failed": "Échec de l'annulation des livraisons entrantes",
|
||||
"seccomp_status": "Bac à sable Seccomp-BPF (Linux)",
|
||||
"seccomp_active": "Actif",
|
||||
"seccomp_inactive": "Inactif",
|
||||
"seccomp_auto_enabled": "Activé automatiquement lorsque libseccomp est disponible",
|
||||
"seccomp_kernel_unsupported": "Seccomp-BPF indisponible (installez libseccomp, ou plateforme non prise en charge)",
|
||||
"seccomp_disabled_by_env": "Désactivé via MESHCHAT_SECCOMP=0"
|
||||
},
|
||||
"common": {
|
||||
"open": "Ouvrir",
|
||||
|
|
|
|||
|
|
@ -515,7 +515,13 @@
|
|||
"cancel_inbound_deliveries_count": "Annulla in arrivo ({count})",
|
||||
"cancel_inbound_confirm": "Annullare {count} trasferimento/i LXMF in arrivo? I messaggi grandi in download verranno interrotti.",
|
||||
"cancel_inbound_done": "Annullati {count} trasferimento/i in arrivo.",
|
||||
"cancel_inbound_failed": "Impossibile annullare le consegne in arrivo"
|
||||
"cancel_inbound_failed": "Impossibile annullare le consegne in arrivo",
|
||||
"seccomp_status": "Sandbox Seccomp-BPF (Linux)",
|
||||
"seccomp_active": "Attivo",
|
||||
"seccomp_inactive": "Non attivo",
|
||||
"seccomp_auto_enabled": "Abilitato automaticamente quando libseccomp è disponibile",
|
||||
"seccomp_kernel_unsupported": "Seccomp-BPF non disponibile (installa libseccomp o piattaforma non supportata)",
|
||||
"seccomp_disabled_by_env": "Disabilitato tramite MESHCHAT_SECCOMP=0"
|
||||
},
|
||||
"common": {
|
||||
"open": "Apri",
|
||||
|
|
|
|||
|
|
@ -515,7 +515,13 @@
|
|||
"cancel_inbound_deliveries_count": "Inkomend annuleren ({count})",
|
||||
"cancel_inbound_confirm": "{count} inkomende LXMF-overdracht(en) annuleren? Grote berichten die nu worden gedownload stoppen.",
|
||||
"cancel_inbound_done": "{count} inkomende overdracht(en) geannuleerd.",
|
||||
"cancel_inbound_failed": "Inkomende leveringen konden niet worden geannuleerd"
|
||||
"cancel_inbound_failed": "Inkomende leveringen konden niet worden geannuleerd",
|
||||
"seccomp_status": "Seccomp-BPF-sandbox (Linux)",
|
||||
"seccomp_active": "Actief",
|
||||
"seccomp_inactive": "Niet actief",
|
||||
"seccomp_auto_enabled": "Automatisch ingeschakeld wanneer libseccomp beschikbaar is",
|
||||
"seccomp_kernel_unsupported": "Seccomp-BPF niet beschikbaar (installeer libseccomp of platform niet ondersteund)",
|
||||
"seccomp_disabled_by_env": "Uitgeschakeld via MESHCHAT_SECCOMP=0"
|
||||
},
|
||||
"common": {
|
||||
"open": "Openen",
|
||||
|
|
|
|||
|
|
@ -515,7 +515,13 @@
|
|||
"cancel_inbound_deliveries_count": "Отменить входящие ({count})",
|
||||
"cancel_inbound_confirm": "Отменить {count} входящую LXMF-передачу(и)? Крупные сообщения, которые сейчас загружаются, будут остановлены.",
|
||||
"cancel_inbound_done": "Отменено входящих передач: {count}.",
|
||||
"cancel_inbound_failed": "Не удалось отменить входящие доставки"
|
||||
"cancel_inbound_failed": "Не удалось отменить входящие доставки",
|
||||
"seccomp_status": "Песочница Seccomp-BPF (Linux)",
|
||||
"seccomp_active": "Активна",
|
||||
"seccomp_inactive": "Не активна",
|
||||
"seccomp_auto_enabled": "Включается автоматически при наличии libseccomp",
|
||||
"seccomp_kernel_unsupported": "Seccomp-BPF недоступен (установите libseccomp или платформа не поддерживается)",
|
||||
"seccomp_disabled_by_env": "Отключено через MESHCHAT_SECCOMP=0"
|
||||
},
|
||||
"common": {
|
||||
"open": "Открыть",
|
||||
|
|
|
|||
|
|
@ -515,7 +515,13 @@
|
|||
"cancel_inbound_deliveries_count": "取消传入({count})",
|
||||
"cancel_inbound_confirm": "取消 {count} 个传入 LXMF 传输?正在下载的大消息将停止。",
|
||||
"cancel_inbound_done": "已取消 {count} 个传入传输。",
|
||||
"cancel_inbound_failed": "无法取消传入投递"
|
||||
"cancel_inbound_failed": "无法取消传入投递",
|
||||
"seccomp_status": "Seccomp-BPF 沙箱(Linux)",
|
||||
"seccomp_active": "已启用",
|
||||
"seccomp_inactive": "未启用",
|
||||
"seccomp_auto_enabled": "在提供 libseccomp 时自动启用",
|
||||
"seccomp_kernel_unsupported": "Seccomp-BPF 不可用(请安装 libseccomp,或平台不支持)",
|
||||
"seccomp_disabled_by_env": "已通过 MESHCHAT_SECCOMP=0 禁用"
|
||||
},
|
||||
"common": {
|
||||
"open": "打开",
|
||||
|
|
|
|||
|
|
@ -160,6 +160,11 @@ APP_INFO_BODY_SCHEMA: dict = {
|
|||
"landlock_auto_enabled": {"type": "boolean"},
|
||||
"landlock_disabled_by_env": {"type": "boolean"},
|
||||
"landlock_active": {"type": "boolean"},
|
||||
"seccomp_kernel_supported": {"type": "boolean"},
|
||||
"seccomp_requested": {"type": "boolean"},
|
||||
"seccomp_auto_enabled": {"type": "boolean"},
|
||||
"seccomp_disabled_by_env": {"type": "boolean"},
|
||||
"seccomp_active": {"type": "boolean"},
|
||||
},
|
||||
"additionalProperties": True,
|
||||
}
|
||||
|
|
@ -175,6 +180,11 @@ _SERVER_BIND_STATUS_SCHEMA: dict = {
|
|||
"landlock_auto_enabled": {"type": "boolean"},
|
||||
"landlock_disabled_by_env": {"type": "boolean"},
|
||||
"landlock_active": {"type": "boolean"},
|
||||
"seccomp_kernel_supported": {"type": "boolean"},
|
||||
"seccomp_requested": {"type": "boolean"},
|
||||
"seccomp_auto_enabled": {"type": "boolean"},
|
||||
"seccomp_disabled_by_env": {"type": "boolean"},
|
||||
"seccomp_active": {"type": "boolean"},
|
||||
}
|
||||
|
||||
API_V1_STATUS_SCHEMA: dict = {
|
||||
|
|
|
|||
|
|
@ -175,6 +175,11 @@ def test_status_schema_accepts_starting_and_failed_envelopes():
|
|||
"landlock_auto_enabled": False,
|
||||
"landlock_disabled_by_env": False,
|
||||
"landlock_active": False,
|
||||
"seccomp_kernel_supported": False,
|
||||
"seccomp_requested": False,
|
||||
"seccomp_auto_enabled": False,
|
||||
"seccomp_disabled_by_env": False,
|
||||
"seccomp_active": False,
|
||||
}
|
||||
assert_matches_schema(starting, API_V1_STATUS_SCHEMA)
|
||||
failed = {
|
||||
|
|
|
|||
|
|
@ -398,6 +398,11 @@ def test_status_schema_fuzz_valid_envelopes(status, stage, network_ready, error)
|
|||
"landlock_auto_enabled": False,
|
||||
"landlock_disabled_by_env": False,
|
||||
"landlock_active": False,
|
||||
"seccomp_kernel_supported": False,
|
||||
"seccomp_requested": False,
|
||||
"seccomp_auto_enabled": False,
|
||||
"seccomp_disabled_by_env": False,
|
||||
"seccomp_active": False,
|
||||
}
|
||||
if error is not None:
|
||||
sample["error"] = error
|
||||
|
|
|
|||
122
tests/backend/test_seccomp_sandbox.py
Normal file
122
tests/backend/test_seccomp_sandbox.py
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
# SPDX-License-Identifier: 0BSD
|
||||
|
||||
"""Tests for optional seccomp-BPF sandbox."""
|
||||
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from meshchatx.src.backend import seccomp_sandbox as sc
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _reset_seccomp_caches():
|
||||
sc._seccomp_support_cached = None
|
||||
sc._seccomp_lib_cached = None
|
||||
sc._seccomp_lib_failed = False
|
||||
yield
|
||||
sc._seccomp_support_cached = None
|
||||
sc._seccomp_lib_cached = None
|
||||
sc._seccomp_lib_failed = False
|
||||
|
||||
|
||||
def test_seccomp_requested_non_linux():
|
||||
with (
|
||||
patch.object(sc.sys, "platform", "darwin"),
|
||||
patch.object(sc, "_is_android", return_value=False),
|
||||
):
|
||||
assert sc.seccomp_requested() is False
|
||||
|
||||
|
||||
def test_seccomp_requested_respects_disable_env(monkeypatch):
|
||||
monkeypatch.setenv("MESHCHAT_SECCOMP", "0")
|
||||
with (
|
||||
patch.object(sc.sys, "platform", "linux"),
|
||||
patch.object(sc, "_is_android", return_value=False),
|
||||
):
|
||||
assert sc.seccomp_requested() is False
|
||||
assert sc.seccomp_disabled_by_env() is True
|
||||
|
||||
|
||||
def test_seccomp_requested_force_enable_env(monkeypatch):
|
||||
monkeypatch.setenv("MESHCHAT_SECCOMP", "1")
|
||||
with (
|
||||
patch.object(sc.sys, "platform", "linux"),
|
||||
patch.object(sc, "_is_android", return_value=False),
|
||||
):
|
||||
assert sc.seccomp_requested() is True
|
||||
assert sc.seccomp_auto_enabled() is False
|
||||
|
||||
|
||||
def test_seccomp_auto_when_supported(monkeypatch):
|
||||
monkeypatch.delenv("MESHCHAT_SECCOMP", raising=False)
|
||||
with (
|
||||
patch.object(sc.sys, "platform", "linux"),
|
||||
patch.object(sc, "_is_android", return_value=False),
|
||||
patch.object(sc, "seccomp_kernel_supported", return_value=True),
|
||||
):
|
||||
assert sc.seccomp_requested() is True
|
||||
assert sc.seccomp_auto_enabled() is True
|
||||
|
||||
|
||||
def test_seccomp_auto_off_when_unsupported(monkeypatch):
|
||||
monkeypatch.delenv("MESHCHAT_SECCOMP", raising=False)
|
||||
with (
|
||||
patch.object(sc.sys, "platform", "linux"),
|
||||
patch.object(sc, "_is_android", return_value=False),
|
||||
patch.object(sc, "seccomp_kernel_supported", return_value=False),
|
||||
):
|
||||
assert sc.seccomp_requested() is False
|
||||
|
||||
|
||||
def test_seccomp_kernel_supported_false_on_android():
|
||||
with patch.object(sc, "_is_android", return_value=True):
|
||||
assert sc.seccomp_kernel_supported() is False
|
||||
|
||||
|
||||
def test_apply_seccomp_falls_back_when_not_requested(monkeypatch):
|
||||
monkeypatch.setenv("MESHCHAT_SECCOMP", "0")
|
||||
assert sc.apply_seccomp_sandbox() is False
|
||||
|
||||
|
||||
def test_apply_seccomp_falls_back_without_libseccomp(monkeypatch):
|
||||
monkeypatch.setenv("MESHCHAT_SECCOMP", "1")
|
||||
with (
|
||||
patch.object(sc.sys, "platform", "linux"),
|
||||
patch.object(sc, "_is_android", return_value=False),
|
||||
patch.object(sc, "_load_libseccomp", return_value=None),
|
||||
):
|
||||
assert sc.apply_seccomp_sandbox() is False
|
||||
|
||||
|
||||
def test_apply_seccomp_loads_denylist(monkeypatch):
|
||||
monkeypatch.setenv("MESHCHAT_SECCOMP", "1")
|
||||
lib = MagicMock()
|
||||
ctx = object()
|
||||
lib.seccomp_init.return_value = ctx
|
||||
lib.seccomp_syscall_resolve_name.side_effect = lambda name: {
|
||||
b"mount": 165,
|
||||
b"ptrace": 101,
|
||||
b"bpf": 321,
|
||||
}.get(name, -1)
|
||||
lib.seccomp_rule_add.return_value = 0
|
||||
lib.seccomp_load.return_value = 0
|
||||
|
||||
with (
|
||||
patch.object(sc.sys, "platform", "linux"),
|
||||
patch.object(sc, "_is_android", return_value=False),
|
||||
patch.object(sc, "_load_libseccomp", return_value=lib),
|
||||
):
|
||||
assert sc.apply_seccomp_sandbox() is True
|
||||
|
||||
lib.seccomp_init.assert_called_once_with(sc._SCMP_ACT_ALLOW)
|
||||
assert lib.seccomp_rule_add.call_count >= 3
|
||||
lib.seccomp_load.assert_called_once_with(ctx)
|
||||
lib.seccomp_release.assert_called_once_with(ctx)
|
||||
|
||||
|
||||
@pytest.mark.skipif(sys.platform != "linux", reason="seccomp probe requires Linux")
|
||||
def test_seccomp_kernel_supported_on_linux():
|
||||
supported = sc.seccomp_kernel_supported()
|
||||
assert isinstance(supported, bool)
|
||||
|
|
@ -83,6 +83,14 @@ describe("ToolsPage.vue", () => {
|
|||
expect(rnshRow?.text()).toContain("tools.alpha_badge");
|
||||
});
|
||||
|
||||
it("shows an alpha badge on the rns-filesync tool", () => {
|
||||
const wrapper = mountToolsPage();
|
||||
const filesync = wrapper.vm.tools.find((tool) => tool.name === "rns-filesync");
|
||||
expect(filesync?.alpha).toBe(true);
|
||||
const filesyncRow = wrapper.findAll(".tool-row").find((row) => row.text().includes("tools.rns_filesync.title"));
|
||||
expect(filesyncRow?.text()).toContain("tools.alpha_badge");
|
||||
});
|
||||
|
||||
it("clears search query when close button is clicked", async () => {
|
||||
const wrapper = mountToolsPage();
|
||||
const searchInput = wrapper.find("input");
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ describe("filesync packaging contracts", () => {
|
|||
const tools = readSource("meshchatx/src/frontend/js/registries/coreToolsEntries.js");
|
||||
expect(tools).toContain('name: "rns-filesync"');
|
||||
expect(tools).toContain('route: { name: "rns-filesync" }');
|
||||
const block = tools.slice(tools.indexOf('name: "rns-filesync"'), tools.indexOf('name: "debug-logs"'));
|
||||
const block = tools.slice(tools.indexOf('name: "rns-filesync"'), tools.indexOf('name: "rnsh"'));
|
||||
expect(block).not.toContain("comingSoon");
|
||||
expect(block).toContain("alpha: true");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue