feat: implement optional seccomp-BPF syscall denylist alongside Landlock and improve tools/UI

This commit is contained in:
Ivan 2026-07-22 15:43:45 -05:00
parent 3547991f28
commit b27028a7fb
No known key found for this signature in database
25 changed files with 831 additions and 275 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

Binary file not shown.

View file

@ -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,

View 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

View file

@ -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>

View file

@ -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,

View file

@ -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" },

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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": "Открыть",

View file

@ -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": "打开",

View file

@ -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 = {

View file

@ -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 = {

View file

@ -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

View 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)

View file

@ -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");

View file

@ -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");
});
});