mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-07-09 16:00:05 +00:00
feat: enhance system diagnostics with kernel capabilities and improve… (#266)
Some checks failed
Deploy documentation / build-and-deploy (push) Has been cancelled
Some checks failed
Deploy documentation / build-and-deploy (push) Has been cancelled
This commit is contained in:
parent
3b75791581
commit
862e504072
18 changed files with 604 additions and 190 deletions
3
.vscode/extensions.json
vendored
3
.vscode/extensions.json
vendored
|
|
@ -2,7 +2,6 @@
|
|||
"recommendations": [
|
||||
"golang.go",
|
||||
"chaunceyxie.vscode-markdown-paste-image-for-vuepress",
|
||||
"ms-vscode.vscode-typescript-next",
|
||||
"spencerwmiles.vscode-task-buttons"
|
||||
"ms-vscode.vscode-typescript-next"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
# B4 - Bye Bye Big Bro
|
||||
|
||||
## [1.71.1] - 2026-06-29
|
||||
|
||||
- ADDED: **System Info now shows the router's kernel capabilities** - a new section reports whether the router supports the underlying features that advanced modes rely on.
|
||||
- FIXED: **Telegram bridge could be slow to connect or drop transfers** - in some setups b4 got in the way of its own connection out to Telegram, slowing the first connect and occasionally cutting transfers short; b4 now keeps its own traffic clear.
|
||||
- FIXED: **A named device sometimes showed as its address instead of its name** - on the dashboard and Traffic page a device you had named could appear as a plain hardware address while it was off the network (for example connected over VPN); its saved name now stays shown, and named devices remain listed in Device Filtering with an "offline" label.
|
||||
|
||||
## [1.71.0] - 2026-06-28
|
||||
|
||||
- ADDED: **The Logs page is easier to read** - log lines are now colour-coded by importance, and each type (error, warning, info, and so on) can be shown or hidden with one click.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
# B4 - Bye Bye Big Bro
|
||||
|
||||
## [1.71.1] - 2026-06-29
|
||||
|
||||
- ДОБАВЛЕНО: **«Сведения о системе» теперь показывают возможности ядра роутера** - новый раздел сообщает, поддерживает ли роутер низкоуровневые функции, на которые опираются продвинутые режимы.
|
||||
- ИСПРАВЛЕНО: **Мост Telegram мог медленно подключаться или обрывать передачу** - в некоторых конфигурациях b4 мешал собственному соединению с Telegram, что замедляло первое подключение и иногда прерывало передачу; теперь b4 не трогает собственный трафик.
|
||||
- ИСПРАВЛЕНО: **Устройство с заданным именем иногда показывалось своим адресом вместо имени** - на главой и странице «Трафик» устройство, которому вы задали имя, могло отображаться обычным аппаратным адресом, пока оно не в сети (например, подключено по VPN); теперь его сохранённое имя продолжает показываться, а устройства с именем остаются в списке фильтрации устройств с пометкой «не в сети».
|
||||
|
||||
## [1.71.0] - 2026-06-28
|
||||
|
||||
- ДОБАВЛЕНО: **Страницу логов стало удобнее читать** - строки логов теперь выделяются цветом по важности, а каждый тип (ошибка, предупреждение, информация и так далее) можно показать или скрыть одним кликом.
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ type DeviceInfo struct {
|
|||
IsPrivate bool `json:"is_private"`
|
||||
Alias string `json:"alias,omitempty"`
|
||||
IsManual bool `json:"is_manual,omitempty"`
|
||||
IsOnline bool `json:"is_online"`
|
||||
}
|
||||
|
||||
type DevicesResponse struct {
|
||||
|
|
@ -300,7 +301,12 @@ func (api *API) handleDevices(w http.ResponseWriter, r *http.Request) {
|
|||
sourceName, _ := globalPool.Dhcp.SourceInfo()
|
||||
mappings := globalPool.Dhcp.GetAllMappings()
|
||||
hostnames := globalPool.Dhcp.GetAllHostnames()
|
||||
hostnamesNorm := make(map[string]string, len(hostnames))
|
||||
for mac, hn := range hostnames {
|
||||
hostnamesNorm[normalizeMAC(mac)] = hn
|
||||
}
|
||||
devices := make([]DeviceInfo, 0, len(mappings))
|
||||
seen := make(map[string]struct{}, len(mappings))
|
||||
|
||||
for ip, macAddr := range mappings {
|
||||
var vendor string
|
||||
|
|
@ -320,14 +326,47 @@ func (api *API) handleDevices(w http.ResponseWriter, r *http.Request) {
|
|||
isManual = d.IsManual
|
||||
}
|
||||
|
||||
seen[normalizeMAC(macAddr)] = struct{}{}
|
||||
devices = append(devices, DeviceInfo{
|
||||
MAC: macAddr,
|
||||
IP: ip,
|
||||
Hostname: hostnames[macAddr],
|
||||
Hostname: hostnamesNorm[normalizeMAC(macAddr)],
|
||||
Vendor: vendor,
|
||||
IsPrivate: isPrivate,
|
||||
Alias: alias,
|
||||
IsManual: isManual,
|
||||
IsOnline: true,
|
||||
})
|
||||
}
|
||||
|
||||
for i := range cfg.Queue.Devices.Devices {
|
||||
d := &cfg.Queue.Devices.Devices[i]
|
||||
if d.Name == "" || d.MAC == "" {
|
||||
continue
|
||||
}
|
||||
mac := normalizeMAC(d.MAC)
|
||||
if _, ok := seen[mac]; ok {
|
||||
continue
|
||||
}
|
||||
seen[mac] = struct{}{}
|
||||
|
||||
var vendor string
|
||||
isPrivate := isPrivateMAC(mac)
|
||||
if isPrivate {
|
||||
vendor = "Private"
|
||||
} else if cfg.Queue.Devices.VendorLookup {
|
||||
vendor = ouiDB.Lookup(mac)
|
||||
}
|
||||
|
||||
devices = append(devices, DeviceInfo{
|
||||
MAC: d.MAC,
|
||||
IP: d.IP,
|
||||
Hostname: hostnamesNorm[mac],
|
||||
Vendor: vendor,
|
||||
IsPrivate: isPrivate,
|
||||
Alias: d.Name,
|
||||
IsManual: d.IsManual,
|
||||
IsOnline: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ func (api *API) buildDiagnostics() Diagnostics {
|
|||
return Diagnostics{
|
||||
System: collectSystemInfo(),
|
||||
B4: collectB4Info(cfg.ConfigPath, serviceManager),
|
||||
Kernel: collectKernelModules(),
|
||||
Kernel: collectKernelModules(cfg),
|
||||
Tools: collectTools(),
|
||||
Network: collectNetworkInterfaces(),
|
||||
Engine: collectEngineInfo(cfg),
|
||||
|
|
@ -595,7 +595,7 @@ func (api *API) collectGeodataInfo() DiagGeodata {
|
|||
return info
|
||||
}
|
||||
|
||||
func collectKernelModules() DiagKernel {
|
||||
func collectKernelModules(cfg *config.Config) DiagKernel {
|
||||
modules := []string{"xt_NFQUEUE", "nfnetlink_queue", "xt_connbytes", "xt_multiport", "nf_conntrack"}
|
||||
result := DiagKernel{Modules: make([]DiagModule, 0, len(modules))}
|
||||
|
||||
|
|
@ -624,6 +624,34 @@ func collectKernelModules() DiagKernel {
|
|||
result.Modules = append(result.Modules, dm)
|
||||
}
|
||||
|
||||
tproxyOK, tproxyMissing, tproxyPkgs := tables.ProbeTProxyCapability(cfg)
|
||||
tproxyCap := DiagCapability{
|
||||
Name: "transparent_proxy",
|
||||
Available: tproxyOK,
|
||||
Missing: tproxyMissing,
|
||||
Packages: tproxyPkgs,
|
||||
}
|
||||
if tproxyOK {
|
||||
tproxyCap.Detail = "TPROXY + socket match available (proxy and mtproto-ws routing modes work)"
|
||||
} else {
|
||||
tproxyCap.Detail = "TPROXY/socket match unavailable — transparent redirect (proxy and mtproto-ws routing, e.g. Telegram bridge) will not work"
|
||||
}
|
||||
result.Capabilities = append(result.Capabilities, tproxyCap)
|
||||
|
||||
connmarkOK, connmarkMissing, connmarkPkgs := tables.ProbeConnmarkCapability(cfg)
|
||||
connmarkCap := DiagCapability{
|
||||
Name: "reply_side_bypass",
|
||||
Available: connmarkOK,
|
||||
Missing: connmarkMissing,
|
||||
Packages: connmarkPkgs,
|
||||
}
|
||||
if connmarkOK {
|
||||
connmarkCap.Detail = "conntrack mark save/restore available (b4 exempts its own marked connections from reply-side processing)"
|
||||
} else {
|
||||
connmarkCap.Detail = "conntrack mark unavailable — b4 cannot exempt its own upstream (e.g. MTProto WS bridge) from reply-side processing; minor reliability impact"
|
||||
}
|
||||
result.Capabilities = append(result.Capabilities, connmarkCap)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -142,7 +142,8 @@ type DiagGeodata struct {
|
|||
}
|
||||
|
||||
type DiagKernel struct {
|
||||
Modules []DiagModule `json:"modules"`
|
||||
Modules []DiagModule `json:"modules"`
|
||||
Capabilities []DiagCapability `json:"capabilities,omitempty"`
|
||||
}
|
||||
|
||||
type DiagModule struct {
|
||||
|
|
@ -150,6 +151,14 @@ type DiagModule struct {
|
|||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type DiagCapability struct {
|
||||
Name string `json:"name"`
|
||||
Available bool `json:"available"`
|
||||
Missing []string `json:"missing,omitempty"`
|
||||
Packages []string `json:"packages,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
type DiagTools struct {
|
||||
Firewall []DiagTool `json:"firewall"`
|
||||
Required []DiagTool `json:"required"`
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
generateIpVariants,
|
||||
asnStorage,
|
||||
stripPort,
|
||||
resolveDeviceName,
|
||||
} from "@utils";
|
||||
import { colors } from "@design";
|
||||
import { useWebSocket } from "@context/B4WsProvider";
|
||||
|
|
@ -136,7 +137,8 @@ export function ConnectionsPage() {
|
|||
const ipMap: Record<string, string> = {};
|
||||
for (const d of data.devices || []) {
|
||||
const normalized = d.mac.toUpperCase().replaceAll("-", ":");
|
||||
map[normalized] = d.alias || d.vendor || "";
|
||||
const name = resolveDeviceName(d);
|
||||
map[normalized] = name === d.mac ? "" : name;
|
||||
if (d.ip) ipMap[d.ip] = normalized;
|
||||
}
|
||||
for (const ip of data.router_ips || []) {
|
||||
|
|
|
|||
|
|
@ -402,6 +402,19 @@ export const DevicesSettings = ({ config, onChange }: DevicesSettingsProps) => {
|
|||
sx={{ fontSize: "0.7rem", height: 20 }}
|
||||
/>
|
||||
)}
|
||||
{!device.is_manual &&
|
||||
device.is_online === false && (
|
||||
<Chip
|
||||
label={t("core.devices.offline")}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{
|
||||
fontSize: "0.7rem",
|
||||
height: 20,
|
||||
color: colors.text.secondary,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</TableCell>
|
||||
<TableCell onClick={(e) => e.stopPropagation()}>
|
||||
|
|
|
|||
|
|
@ -20,128 +20,7 @@ import { colors, spacing } from "@design";
|
|||
import { B4Dialog } from "@common/B4Dialog";
|
||||
import { useSnackbar } from "@context/SnackbarProvider";
|
||||
import { copyText } from "@utils";
|
||||
|
||||
interface SystemInfoDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface DiagSystem {
|
||||
hostname: string;
|
||||
distro?: string;
|
||||
os: string;
|
||||
arch: string;
|
||||
kernel: string;
|
||||
cpu_cores: number;
|
||||
mem_total_mb: number;
|
||||
mem_avail_mb: number;
|
||||
is_docker: boolean;
|
||||
}
|
||||
|
||||
interface DiagB4 {
|
||||
version: string;
|
||||
commit: string;
|
||||
build_date: string;
|
||||
service_manager: string;
|
||||
config_path: string;
|
||||
running: boolean;
|
||||
pid?: number;
|
||||
memory_mb?: string;
|
||||
uptime?: string;
|
||||
}
|
||||
|
||||
interface DiagModule {
|
||||
name: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface DiagTool {
|
||||
name: string;
|
||||
found: boolean;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
interface DiagMount {
|
||||
path: string;
|
||||
available: string;
|
||||
writable: boolean;
|
||||
}
|
||||
|
||||
interface DiagInterface {
|
||||
name: string;
|
||||
mac?: string;
|
||||
addrs?: string[];
|
||||
up: boolean;
|
||||
mtu: number;
|
||||
}
|
||||
|
||||
interface DiagRuleGroup {
|
||||
title: string;
|
||||
rules: string[];
|
||||
}
|
||||
|
||||
interface DiagFirewall {
|
||||
backend: string;
|
||||
nfqueue_works: boolean;
|
||||
flow_offload: string;
|
||||
rule_groups?: DiagRuleGroup[];
|
||||
}
|
||||
|
||||
interface DiagTUN {
|
||||
running: boolean;
|
||||
device_name: string;
|
||||
device_up: boolean;
|
||||
mtu?: number;
|
||||
address?: string;
|
||||
address_v6?: string;
|
||||
out_interface?: string;
|
||||
out_gateway?: string;
|
||||
resolved_src?: string;
|
||||
capture?: string;
|
||||
route_table?: number;
|
||||
reply_capture: boolean;
|
||||
packets_forwarded: number;
|
||||
forward_errors: number;
|
||||
ipv6_dropped: number;
|
||||
}
|
||||
|
||||
interface DiagEngine {
|
||||
mode: string;
|
||||
tun?: DiagTUN;
|
||||
}
|
||||
|
||||
interface DiagGeodata {
|
||||
geosite_configured: boolean;
|
||||
geosite_path?: string;
|
||||
geosite_size?: string;
|
||||
geoip_configured: boolean;
|
||||
geoip_path?: string;
|
||||
geoip_size?: string;
|
||||
total_domains: number;
|
||||
total_ips: number;
|
||||
}
|
||||
|
||||
interface DiagPaths {
|
||||
binary: string;
|
||||
config: string;
|
||||
error_log?: string;
|
||||
geosite?: string;
|
||||
geoip?: string;
|
||||
data_dir?: string;
|
||||
}
|
||||
|
||||
interface Diagnostics {
|
||||
system: DiagSystem;
|
||||
b4: DiagB4;
|
||||
kernel: { modules: DiagModule[] };
|
||||
tools: { firewall: DiagTool[]; required: DiagTool[]; optional: DiagTool[] };
|
||||
network: { interfaces: DiagInterface[] };
|
||||
engine: DiagEngine;
|
||||
firewall: DiagFirewall;
|
||||
geodata: DiagGeodata;
|
||||
storage: DiagMount[];
|
||||
paths: DiagPaths;
|
||||
}
|
||||
import { Diagnostics, SystemInfoDialogProps } from "@models/sysinfo";
|
||||
|
||||
export const SystemInfoDialog = ({ open, onClose }: SystemInfoDialogProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
|
@ -458,10 +337,11 @@ export const SystemInfoDialog = ({ open, onClose }: SystemInfoDialogProps) => {
|
|||
</Stack>,
|
||||
)}
|
||||
{data.engine.tun.address &&
|
||||
row(t("settings.SystemInfo.tunAddress"), data.engine.tun.address)}
|
||||
{data.engine.tun.mtu
|
||||
? row("MTU", data.engine.tun.mtu)
|
||||
: null}
|
||||
row(
|
||||
t("settings.SystemInfo.tunAddress"),
|
||||
data.engine.tun.address,
|
||||
)}
|
||||
{data.engine.tun.mtu ? row("MTU", data.engine.tun.mtu) : null}
|
||||
{row(
|
||||
t("settings.SystemInfo.tunEgress"),
|
||||
data.engine.tun.out_interface || "auto",
|
||||
|
|
@ -633,6 +513,59 @@ export const SystemInfoDialog = ({ open, onClose }: SystemInfoDialogProps) => {
|
|||
listRow(m.name, statusChip(m.status)),
|
||||
)}
|
||||
|
||||
{data.kernel.capabilities && data.kernel.capabilities.length > 0 && (
|
||||
<>
|
||||
{sectionTitle(t("settings.SystemInfo.kernelCapabilities"))}
|
||||
{data.kernel.capabilities.map((cap) => {
|
||||
const label =
|
||||
cap.name === "transparent_proxy"
|
||||
? t("settings.SystemInfo.capTransparentProxy")
|
||||
: cap.name === "reply_side_bypass"
|
||||
? t("settings.SystemInfo.capReplySideBypass")
|
||||
: cap.name;
|
||||
return (
|
||||
<Stack key={cap.name} spacing={0.3} sx={{ py: 0.3, px: 1 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: colors.text.primary,
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</Typography>
|
||||
{boolChip(
|
||||
cap.available,
|
||||
t("settings.SystemInfo.capAvailable"),
|
||||
t("settings.SystemInfo.capUnavailable"),
|
||||
)}
|
||||
</Stack>
|
||||
{!cap.available &&
|
||||
cap.packages &&
|
||||
cap.packages.length > 0 && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: colors.text.secondary,
|
||||
fontSize: "0.7rem",
|
||||
}}
|
||||
>
|
||||
{t("settings.SystemInfo.capInstallHint", {
|
||||
packages: cap.packages.join(" "),
|
||||
})}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{sectionTitle(t("settings.SystemInfo.firewallTools"))}
|
||||
{data.tools.firewall.map((tool) =>
|
||||
listRow(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useMemo, useState } from "react";
|
||||
import { DeviceInfo, devicesApi } from "@b4.devices";
|
||||
import { resolveDeviceName, resolveDeviceMeta } from "@utils";
|
||||
|
||||
export function useDeviceNames() {
|
||||
const [devices, setDevices] = useState<DeviceInfo[]>([]);
|
||||
|
|
@ -19,19 +20,12 @@ export function useDeviceNames() {
|
|||
|
||||
const getDeviceName = (mac: string): string => {
|
||||
const dev = deviceMap[mac];
|
||||
if (dev?.alias) return dev.alias;
|
||||
if (dev?.hostname) return dev.hostname;
|
||||
if (dev?.vendor && dev.vendor !== "Private") return `${dev.vendor} (${mac})`;
|
||||
return mac;
|
||||
return dev ? resolveDeviceName(dev) : mac;
|
||||
};
|
||||
|
||||
const getDeviceMeta = (mac: string): string => {
|
||||
const dev = deviceMap[mac];
|
||||
if (!dev) return "";
|
||||
const parts: string[] = [];
|
||||
if (dev.ip) parts.push(dev.ip);
|
||||
if (dev.vendor && dev.vendor !== "Private") parts.push(dev.vendor);
|
||||
return parts.join(" · ");
|
||||
return dev ? resolveDeviceMeta(dev) : "";
|
||||
};
|
||||
|
||||
return { devices, deviceMap, getDeviceName, getDeviceMeta };
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@
|
|||
"macAddress": "MAC Address",
|
||||
"deviceName": "Name",
|
||||
"ip": "IP",
|
||||
"manual": "manual"
|
||||
"manual": "manual",
|
||||
"offline": "offline"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
|
@ -620,6 +621,12 @@
|
|||
"tunForwardErrors": "Forward Errors",
|
||||
"tunIpv6Dropped": "IPv6 Dropped",
|
||||
"kernelModules": "Kernel Modules",
|
||||
"kernelCapabilities": "Kernel Capabilities",
|
||||
"capTransparentProxy": "Transparent proxy (TPROXY)",
|
||||
"capReplySideBypass": "Reply-side bypass (connmark)",
|
||||
"capAvailable": "available",
|
||||
"capUnavailable": "unavailable",
|
||||
"capInstallHint": "Install: {{packages}}",
|
||||
"firewallTools": "Firewall Tools (at least one needed)",
|
||||
"requiredTools": "Required Tools",
|
||||
"optionalTools": "Optional Tools",
|
||||
|
|
|
|||
|
|
@ -93,7 +93,8 @@
|
|||
"macAddress": "MAC адрес",
|
||||
"deviceName": "Имя",
|
||||
"ip": "IP",
|
||||
"manual": "вручную"
|
||||
"manual": "вручную",
|
||||
"offline": "не в сети"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
|
|
@ -616,6 +617,12 @@
|
|||
"tunForwardErrors": "Ошибки пересылки",
|
||||
"tunIpv6Dropped": "Отброшено IPv6",
|
||||
"kernelModules": "Модули ядра",
|
||||
"kernelCapabilities": "Возможности ядра",
|
||||
"capTransparentProxy": "Прозрачный прокси (TPROXY)",
|
||||
"capReplySideBypass": "Обход обратного трафика (connmark)",
|
||||
"capAvailable": "доступно",
|
||||
"capUnavailable": "недоступно",
|
||||
"capInstallHint": "Установите: {{packages}}",
|
||||
"firewallTools": "Firewall (нужна хотя бы одна)",
|
||||
"requiredTools": "Необходимые утилиты",
|
||||
"optionalTools": "Опциональные утилиты",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export interface DeviceInfo {
|
|||
alias?: string;
|
||||
country: string;
|
||||
is_manual?: boolean;
|
||||
is_online?: boolean;
|
||||
}
|
||||
|
||||
export interface DevicesResponse {
|
||||
|
|
|
|||
129
src/http/ui/src/models/sysinfo.ts
Normal file
129
src/http/ui/src/models/sysinfo.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
interface DiagSystem {
|
||||
hostname: string;
|
||||
distro?: string;
|
||||
os: string;
|
||||
arch: string;
|
||||
kernel: string;
|
||||
cpu_cores: number;
|
||||
mem_total_mb: number;
|
||||
mem_avail_mb: number;
|
||||
is_docker: boolean;
|
||||
}
|
||||
|
||||
interface DiagB4 {
|
||||
version: string;
|
||||
commit: string;
|
||||
build_date: string;
|
||||
service_manager: string;
|
||||
config_path: string;
|
||||
running: boolean;
|
||||
pid?: number;
|
||||
memory_mb?: string;
|
||||
uptime?: string;
|
||||
}
|
||||
|
||||
interface DiagModule {
|
||||
name: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface DiagCapability {
|
||||
name: string;
|
||||
available: boolean;
|
||||
missing?: string[];
|
||||
packages?: string[];
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
interface DiagTool {
|
||||
name: string;
|
||||
found: boolean;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
interface DiagMount {
|
||||
path: string;
|
||||
available: string;
|
||||
writable: boolean;
|
||||
}
|
||||
|
||||
interface DiagInterface {
|
||||
name: string;
|
||||
mac?: string;
|
||||
addrs?: string[];
|
||||
up: boolean;
|
||||
mtu: number;
|
||||
}
|
||||
|
||||
interface DiagRuleGroup {
|
||||
title: string;
|
||||
rules: string[];
|
||||
}
|
||||
|
||||
interface DiagFirewall {
|
||||
backend: string;
|
||||
nfqueue_works: boolean;
|
||||
flow_offload: string;
|
||||
rule_groups?: DiagRuleGroup[];
|
||||
}
|
||||
|
||||
interface DiagTUN {
|
||||
running: boolean;
|
||||
device_name: string;
|
||||
device_up: boolean;
|
||||
mtu?: number;
|
||||
address?: string;
|
||||
address_v6?: string;
|
||||
out_interface?: string;
|
||||
out_gateway?: string;
|
||||
resolved_src?: string;
|
||||
capture?: string;
|
||||
route_table?: number;
|
||||
reply_capture: boolean;
|
||||
packets_forwarded: number;
|
||||
forward_errors: number;
|
||||
ipv6_dropped: number;
|
||||
}
|
||||
|
||||
interface DiagEngine {
|
||||
mode: string;
|
||||
tun?: DiagTUN;
|
||||
}
|
||||
|
||||
interface DiagGeodata {
|
||||
geosite_configured: boolean;
|
||||
geosite_path?: string;
|
||||
geosite_size?: string;
|
||||
geoip_configured: boolean;
|
||||
geoip_path?: string;
|
||||
geoip_size?: string;
|
||||
total_domains: number;
|
||||
total_ips: number;
|
||||
}
|
||||
|
||||
interface DiagPaths {
|
||||
binary: string;
|
||||
config: string;
|
||||
error_log?: string;
|
||||
geosite?: string;
|
||||
geoip?: string;
|
||||
data_dir?: string;
|
||||
}
|
||||
|
||||
export interface SystemInfoDialogProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export interface Diagnostics {
|
||||
system: DiagSystem;
|
||||
b4: DiagB4;
|
||||
kernel: { modules: DiagModule[]; capabilities?: DiagCapability[] };
|
||||
tools: { firewall: DiagTool[]; required: DiagTool[]; optional: DiagTool[] };
|
||||
network: { interfaces: DiagInterface[] };
|
||||
engine: DiagEngine;
|
||||
firewall: DiagFirewall;
|
||||
geodata: DiagGeodata;
|
||||
storage: DiagMount[];
|
||||
paths: DiagPaths;
|
||||
}
|
||||
|
|
@ -8,6 +8,20 @@ export function wsUrl(path: string): string {
|
|||
return protocol + location.host + path + query;
|
||||
}
|
||||
|
||||
export function resolveDeviceName(d: DeviceInfo): string {
|
||||
if (d.alias) return d.alias;
|
||||
if (d.hostname) return d.hostname;
|
||||
if (d.vendor && d.vendor !== "Private") return `${d.vendor} (${d.mac})`;
|
||||
return d.mac;
|
||||
}
|
||||
|
||||
export function resolveDeviceMeta(d: DeviceInfo): string {
|
||||
const parts: string[] = [];
|
||||
if (d.ip) parts.push(d.ip);
|
||||
if (d.vendor && d.vendor !== "Private") parts.push(d.vendor);
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
export function sortDevices(
|
||||
devices: DeviceInfo[],
|
||||
isSelected: (mac: string) => boolean,
|
||||
|
|
|
|||
|
|
@ -20,10 +20,11 @@ type IPTablesManager struct {
|
|||
useLegacy bool
|
||||
multiportSupport map[string]bool // per-binary cache (iptables vs ip6tables may differ)
|
||||
connbytesSupport map[string]error // per-binary cache
|
||||
connmarkSupport map[string]bool // per-binary cache
|
||||
}
|
||||
|
||||
func NewIPTablesManager(cfg *config.Config, useLegacy bool) *IPTablesManager {
|
||||
return &IPTablesManager{cfg: cfg, useLegacy: useLegacy, multiportSupport: make(map[string]bool), connbytesSupport: make(map[string]error)}
|
||||
return &IPTablesManager{cfg: cfg, useLegacy: useLegacy, multiportSupport: make(map[string]bool), connbytesSupport: make(map[string]error), connmarkSupport: make(map[string]bool)}
|
||||
}
|
||||
|
||||
func (im *IPTablesManager) iptablesBin() string {
|
||||
|
|
@ -45,7 +46,7 @@ func (im *IPTablesManager) checkConnbytesSupport(ipt string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
supported, probeErr := im.probeModuleInTempChain(ipt, []string{"-p", "tcp", "-m", "connbytes", "--connbytes-dir", "original",
|
||||
supported, probeErr := im.probeModuleInTempChain(ipt, "filter", []string{"-p", "tcp", "-m", "connbytes", "--connbytes-dir", "original",
|
||||
"--connbytes-mode", "packets", "--connbytes", "0:10", "-j", "ACCEPT"})
|
||||
if supported {
|
||||
im.connbytesSupport[ipt] = nil
|
||||
|
|
@ -64,7 +65,7 @@ func (im *IPTablesManager) hasMultiportSupport(ipt string) bool {
|
|||
return result
|
||||
}
|
||||
|
||||
supported, _ := im.probeModuleInTempChain(ipt, []string{"-p", "tcp", "-m", "multiport", "--dports", "80,443", "-j", "ACCEPT"})
|
||||
supported, _ := im.probeModuleInTempChain(ipt, "filter", []string{"-p", "tcp", "-m", "multiport", "--dports", "80,443", "-j", "ACCEPT"})
|
||||
im.multiportSupport[ipt] = supported
|
||||
if supported {
|
||||
log.Tracef("IPTABLES[%s]: multiport module is available", ipt)
|
||||
|
|
@ -74,20 +75,40 @@ func (im *IPTablesManager) hasMultiportSupport(ipt string) bool {
|
|||
return supported
|
||||
}
|
||||
|
||||
func (im *IPTablesManager) hasConnmarkSupport(ipt string) bool {
|
||||
if result, ok := im.connmarkSupport[ipt]; ok {
|
||||
return result
|
||||
}
|
||||
|
||||
matchOK, _ := im.probeModuleInTempChain(ipt, "mangle", []string{"-m", "connmark", "--mark", "0x8000/0x8000", "-j", "RETURN"})
|
||||
targetOK, _ := im.probeModuleInTempChain(ipt, "mangle", []string{"-j", "CONNMARK", "--save-mark", "--nfmask", "0x8000", "--ctmask", "0x8000"})
|
||||
supported := matchOK && targetOK
|
||||
im.connmarkSupport[ipt] = supported
|
||||
if supported {
|
||||
log.Tracef("IPTABLES[%s]: connmark module is available", ipt)
|
||||
} else {
|
||||
log.Warnf("IPTABLES[%s]: connmark module not available; b4's own marked connections (e.g. MTProto WS bridge upstream) will not be exempted from reply-side processing", ipt)
|
||||
}
|
||||
return supported
|
||||
}
|
||||
|
||||
// probeModuleInTempChain tests whether a rule spec is accepted by iptables
|
||||
// using a temporary chain, so the probe never touches live traffic.
|
||||
func (im *IPTablesManager) probeModuleInTempChain(ipt string, testSpec []string) (bool, error) {
|
||||
// using a temporary chain in the given table, so the probe never touches live
|
||||
// traffic. Probe in the table where the rule will actually be installed - some
|
||||
// targets (e.g. TPROXY) are table-restricted, so probing in the wrong table can
|
||||
// yield a false negative.
|
||||
func (im *IPTablesManager) probeModuleInTempChain(ipt, table string, testSpec []string) (bool, error) {
|
||||
const tmpChain = "B4_MODULE_TEST"
|
||||
_, _ = run(ipt, "-w", "-t", "filter", "-F", tmpChain)
|
||||
_, _ = run(ipt, "-w", "-t", "filter", "-X", tmpChain)
|
||||
if _, err := run(ipt, "-w", "-t", "filter", "-N", tmpChain); err != nil {
|
||||
_, _ = run(ipt, "-w", "-t", table, "-F", tmpChain)
|
||||
_, _ = run(ipt, "-w", "-t", table, "-X", tmpChain)
|
||||
if _, err := run(ipt, "-w", "-t", table, "-N", tmpChain); err != nil {
|
||||
return false, fmt.Errorf("could not create probe chain %s: %w", tmpChain, err)
|
||||
}
|
||||
defer func() {
|
||||
_, _ = run(ipt, "-w", "-t", "filter", "-F", tmpChain)
|
||||
_, _ = run(ipt, "-w", "-t", "filter", "-X", tmpChain)
|
||||
_, _ = run(ipt, "-w", "-t", table, "-F", tmpChain)
|
||||
_, _ = run(ipt, "-w", "-t", table, "-X", tmpChain)
|
||||
}()
|
||||
_, err := run(append([]string{ipt, "-w", "-t", "filter", "-A", tmpChain}, testSpec...)...)
|
||||
_, err := run(append([]string{ipt, "-w", "-t", table, "-A", tmpChain}, testSpec...)...)
|
||||
return err == nil, err
|
||||
}
|
||||
|
||||
|
|
@ -576,6 +597,17 @@ func (manager *IPTablesManager) buildManifest() (Manifest, error) {
|
|||
Rule{manager: manager, IPT: ipt, Table: "mangle", Chain: "OUTPUT", Action: "A",
|
||||
Spec: []string{"-j", chainName}},
|
||||
)
|
||||
|
||||
if cfg.Queue.Mark != 0 && manager.hasConnmarkSupport(ipt) {
|
||||
markMaskHex := fmt.Sprintf("0x%x", cfg.Queue.Mark)
|
||||
rules = append(rules,
|
||||
Rule{manager: manager, IPT: ipt, Table: "mangle", Chain: "OUTPUT", Action: "I",
|
||||
Spec: []string{"-m", "mark", "--mark", markAccept, "-j", "CONNMARK",
|
||||
"--save-mark", "--nfmask", markMaskHex, "--ctmask", markMaskHex}},
|
||||
Rule{manager: manager, IPT: ipt, Table: "mangle", Chain: preChainName, Action: "I",
|
||||
Spec: []string{"-m", "connmark", "--mark", markAccept, "-j", "RETURN"}},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.System.Tables.Masquerade.Enabled {
|
||||
|
|
|
|||
|
|
@ -233,6 +233,11 @@ func (n *NFTablesManager) Apply() error {
|
|||
if err := n.addRule("output", "oifname", `"lo"`, "return"); err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Queue.Mark != 0 {
|
||||
if err := n.addRule("output", "meta", "mark", "&", markAccept, "==", markAccept, "ct", "mark", "set", "ct", "mark", "|", markAccept); err != nil {
|
||||
log.Warnf("nftables: connmark save unavailable; b4's own marked connections (e.g. MTProto WS bridge upstream) will not be exempted from reply-side processing: %v", err)
|
||||
}
|
||||
}
|
||||
if err := n.addRule("output", "meta", "mark", "&", markAccept, "==", markAccept, "accept"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -244,6 +249,12 @@ func (n *NFTablesManager) Apply() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if cfg.Queue.Mark != 0 {
|
||||
if err := n.addRule("prerouting", "ct", "mark", "&", markAccept, "==", markAccept, "return"); err != nil {
|
||||
log.Warnf("nftables: connmark reply-side bypass unavailable: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Collect TCP and UDP ports
|
||||
tcpPorts := cfg.CollectTCPPorts()
|
||||
var tcpPortExpr string
|
||||
|
|
|
|||
|
|
@ -40,47 +40,230 @@ const proxyRulePriority = 3
|
|||
|
||||
const proxyLocalDeliveryTable = 252
|
||||
|
||||
var proxyNftPreflightOnce sync.Once
|
||||
var (
|
||||
proxyNftPreflightOnce sync.Once
|
||||
proxyIptPreflightOnce [2]sync.Once // [0]=iptables, [1]=iptables-legacy
|
||||
tproxyProbeMu sync.Mutex
|
||||
)
|
||||
|
||||
func tproxyMissingNft() (missing []string, probed bool) {
|
||||
_, _ = run("sh", "-c", "modprobe -q nft_tproxy 2>/dev/null || true")
|
||||
_, _ = run("sh", "-c", "modprobe -q nft_socket 2>/dev/null || true")
|
||||
|
||||
tproxyProbeMu.Lock()
|
||||
defer tproxyProbeMu.Unlock()
|
||||
|
||||
const probeTable = "_b4_proxy_probe"
|
||||
_, _ = run("nft", "delete", "table", "inet", probeTable)
|
||||
if _, err := run("nft", "add", "table", "inet", probeTable); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
defer func() { _, _ = run("nft", "delete", "table", "inet", probeTable) }()
|
||||
if _, err := run("nft", "add", "chain", "inet", probeTable, "test"); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if _, err := run("nft", "add", "rule", "inet", probeTable, "test",
|
||||
"socket", "transparent", "1", "drop"); err != nil {
|
||||
missing = append(missing, "nft_socket")
|
||||
}
|
||||
if _, err := run("nft", "add", "rule", "inet", probeTable, "test",
|
||||
"ip", "protocol", "tcp", "tproxy", "ip", "to", ":1", "drop"); err != nil {
|
||||
missing = append(missing, "nft_tproxy")
|
||||
}
|
||||
return missing, true
|
||||
}
|
||||
|
||||
func tproxyMissingIpt(legacy bool) (missing []string, probed bool) {
|
||||
_, _ = run("sh", "-c", "modprobe -q nf_tproxy_ipv4 2>/dev/null || true")
|
||||
_, _ = run("sh", "-c", "modprobe -q nf_tproxy_ipv6 2>/dev/null || true")
|
||||
_, _ = run("sh", "-c", "modprobe -q xt_TPROXY 2>/dev/null || true")
|
||||
_, _ = run("sh", "-c", "modprobe -q xt_socket 2>/dev/null || true")
|
||||
|
||||
ipt := backendIPTables
|
||||
if legacy {
|
||||
ipt = backendIPTablesLegacy
|
||||
}
|
||||
if !hasBinary(ipt) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
tproxyProbeMu.Lock()
|
||||
defer tproxyProbeMu.Unlock()
|
||||
|
||||
const probeChain = "B4_PROXY_PROBE"
|
||||
_, _ = run(ipt, "-w", "-t", "mangle", "-F", probeChain)
|
||||
_, _ = run(ipt, "-w", "-t", "mangle", "-X", probeChain)
|
||||
if _, err := run(ipt, "-w", "-t", "mangle", "-N", probeChain); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
defer func() {
|
||||
_, _ = run(ipt, "-w", "-t", "mangle", "-F", probeChain)
|
||||
_, _ = run(ipt, "-w", "-t", "mangle", "-X", probeChain)
|
||||
}()
|
||||
|
||||
if _, err := run(ipt, "-w", "-t", "mangle", "-A", probeChain,
|
||||
"-p", "tcp", "-m", "socket", "--transparent", "-j", "ACCEPT"); err != nil {
|
||||
missing = append(missing, "xt_socket")
|
||||
}
|
||||
if _, err := run(ipt, "-w", "-t", "mangle", "-A", probeChain,
|
||||
"-p", "tcp", "-j", "TPROXY", "--on-port", "1", "--tproxy-mark", "0x1/0x1"); err != nil {
|
||||
missing = append(missing, "xt_TPROXY")
|
||||
}
|
||||
return missing, true
|
||||
}
|
||||
|
||||
func tproxyPkgsFor(missing []string) []string {
|
||||
pkgs := make([]string, 0, len(missing))
|
||||
for _, m := range missing {
|
||||
switch m {
|
||||
case "nft_socket":
|
||||
pkgs = append(pkgs, "kmod-nft-socket")
|
||||
case "nft_tproxy":
|
||||
pkgs = append(pkgs, "kmod-nft-tproxy")
|
||||
case "xt_socket":
|
||||
pkgs = append(pkgs, "kmod-ipt-socket")
|
||||
case "xt_TPROXY":
|
||||
pkgs = append(pkgs, "kmod-ipt-tproxy")
|
||||
}
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// ProbeTProxyCapability reports whether transparent proxy / TPROXY redirection
|
||||
// (used by proxy and mtproto-ws routing modes) is usable on the active firewall
|
||||
// backend, along with any missing kernel modules and the packages providing them.
|
||||
func ProbeTProxyCapability(cfg *config.Config) (available bool, missing []string, packages []string) {
|
||||
var miss []string
|
||||
var probed bool
|
||||
backend := detectFirewallBackend(cfg)
|
||||
if backend == backendNFTables {
|
||||
miss, probed = tproxyMissingNft()
|
||||
} else {
|
||||
miss, probed = tproxyMissingIpt(backend == backendIPTablesLegacy)
|
||||
}
|
||||
return probed && len(miss) == 0, miss, tproxyPkgsFor(miss)
|
||||
}
|
||||
|
||||
func connmarkMissingNft() (missing []string, probed bool) {
|
||||
_, _ = run("sh", "-c", "modprobe -q nft_ct 2>/dev/null || true")
|
||||
|
||||
tproxyProbeMu.Lock()
|
||||
defer tproxyProbeMu.Unlock()
|
||||
|
||||
const probeTable = "_b4_connmark_probe"
|
||||
_, _ = run("nft", "delete", "table", "inet", probeTable)
|
||||
if _, err := run("nft", "add", "table", "inet", probeTable); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
defer func() { _, _ = run("nft", "delete", "table", "inet", probeTable) }()
|
||||
if _, err := run("nft", "add", "chain", "inet", probeTable, "test"); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if _, err := run("nft", "add", "rule", "inet", probeTable, "test",
|
||||
"ct", "mark", "set", "ct", "mark", "|", "0x8000"); err != nil {
|
||||
missing = append(missing, "nft_ct")
|
||||
}
|
||||
return missing, true
|
||||
}
|
||||
|
||||
func connmarkMissingIpt(legacy bool) (missing []string, probed bool) {
|
||||
_, _ = run("sh", "-c", "modprobe -q xt_connmark 2>/dev/null || true")
|
||||
_, _ = run("sh", "-c", "modprobe -q xt_CONNMARK 2>/dev/null || true")
|
||||
|
||||
ipt := backendIPTables
|
||||
if legacy {
|
||||
ipt = backendIPTablesLegacy
|
||||
}
|
||||
if !hasBinary(ipt) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
tproxyProbeMu.Lock()
|
||||
defer tproxyProbeMu.Unlock()
|
||||
|
||||
const probeChain = "B4_CONNMARK_PROBE"
|
||||
_, _ = run(ipt, "-w", "-t", "mangle", "-F", probeChain)
|
||||
_, _ = run(ipt, "-w", "-t", "mangle", "-X", probeChain)
|
||||
if _, err := run(ipt, "-w", "-t", "mangle", "-N", probeChain); err != nil {
|
||||
return nil, false
|
||||
}
|
||||
defer func() {
|
||||
_, _ = run(ipt, "-w", "-t", "mangle", "-F", probeChain)
|
||||
_, _ = run(ipt, "-w", "-t", "mangle", "-X", probeChain)
|
||||
}()
|
||||
|
||||
if _, err := run(ipt, "-w", "-t", "mangle", "-A", probeChain,
|
||||
"-m", "connmark", "--mark", "0x8000/0x8000", "-j", "RETURN"); err != nil {
|
||||
missing = append(missing, "xt_connmark")
|
||||
}
|
||||
if _, err := run(ipt, "-w", "-t", "mangle", "-A", probeChain,
|
||||
"-j", "CONNMARK", "--save-mark", "--nfmask", "0x8000", "--ctmask", "0x8000"); err != nil {
|
||||
missing = append(missing, "xt_CONNMARK")
|
||||
}
|
||||
return missing, true
|
||||
}
|
||||
|
||||
func connmarkPkgsFor(missing []string) []string {
|
||||
seen := map[string]bool{}
|
||||
pkgs := make([]string, 0, len(missing))
|
||||
add := func(p string) {
|
||||
if !seen[p] {
|
||||
seen[p] = true
|
||||
pkgs = append(pkgs, p)
|
||||
}
|
||||
}
|
||||
for _, m := range missing {
|
||||
switch m {
|
||||
case "nft_ct":
|
||||
add("kmod-nft-core")
|
||||
case "xt_connmark", "xt_CONNMARK":
|
||||
add("kmod-ipt-conntrack-extra")
|
||||
}
|
||||
}
|
||||
return pkgs
|
||||
}
|
||||
|
||||
// ProbeConnmarkCapability reports whether the conntrack-mark save/restore used
|
||||
// by the reply-side self-bypass (so b4 doesn't intercept its own marked
|
||||
// connections, e.g. the MTProto WS bridge upstream) is usable on the active
|
||||
// firewall backend.
|
||||
func ProbeConnmarkCapability(cfg *config.Config) (available bool, missing []string, packages []string) {
|
||||
var miss []string
|
||||
var probed bool
|
||||
backend := detectFirewallBackend(cfg)
|
||||
if backend == backendNFTables {
|
||||
miss, probed = connmarkMissingNft()
|
||||
} else {
|
||||
miss, probed = connmarkMissingIpt(backend == backendIPTablesLegacy)
|
||||
}
|
||||
return probed && len(miss) == 0, miss, connmarkPkgsFor(miss)
|
||||
}
|
||||
|
||||
func proxyNftPreflight() {
|
||||
proxyNftPreflightOnce.Do(func() {
|
||||
_, _ = run("sh", "-c", "modprobe -q nft_tproxy 2>/dev/null || true")
|
||||
_, _ = run("sh", "-c", "modprobe -q nft_socket 2>/dev/null || true")
|
||||
|
||||
const probeTable = "_b4_proxy_probe"
|
||||
_, _ = run("nft", "delete", "table", "inet", probeTable)
|
||||
if _, err := run("nft", "add", "table", "inet", probeTable); err != nil {
|
||||
missing, probed := tproxyMissingNft()
|
||||
if !probed || len(missing) == 0 {
|
||||
return
|
||||
}
|
||||
defer func() { _, _ = run("nft", "delete", "table", "inet", probeTable) }()
|
||||
if _, err := run("nft", "add", "chain", "inet", probeTable, "test"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var missing []string
|
||||
if _, err := run("nft", "add", "rule", "inet", probeTable, "test",
|
||||
"socket", "transparent", "1", "drop"); err != nil {
|
||||
missing = append(missing, "nft_socket")
|
||||
}
|
||||
if _, err := run("nft", "add", "rule", "inet", probeTable, "test",
|
||||
"ip", "protocol", "tcp", "tproxy", "ip", "to", ":1", "drop"); err != nil {
|
||||
missing = append(missing, "nft_tproxy")
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
pkgs := make([]string, 0, len(missing))
|
||||
for _, m := range missing {
|
||||
switch m {
|
||||
case "nft_socket":
|
||||
pkgs = append(pkgs, "kmod-nft-socket")
|
||||
case "nft_tproxy":
|
||||
pkgs = append(pkgs, "kmod-nft-tproxy")
|
||||
}
|
||||
}
|
||||
log.Errorf("Routing (proxy mode): missing kernel module(s) %s — proxy diversion inactive. Required package(s): %s",
|
||||
strings.Join(missing, ", "), strings.Join(pkgs, " "))
|
||||
strings.Join(missing, ", "), strings.Join(tproxyPkgsFor(missing), " "))
|
||||
})
|
||||
}
|
||||
|
||||
func proxyIptPreflight(legacy bool) {
|
||||
idx := 0
|
||||
if legacy {
|
||||
idx = 1
|
||||
}
|
||||
proxyIptPreflightOnce[idx].Do(func() {
|
||||
missing, probed := tproxyMissingIpt(legacy)
|
||||
if !probed || len(missing) == 0 {
|
||||
return
|
||||
}
|
||||
log.Errorf("Routing (proxy/mtproto-ws mode): missing kernel module(s) %s — transparent diversion inactive; traffic for affected sets will NOT be redirected (e.g. the Telegram WS bridge will hang at \"Connecting…\"). Required package(s): %s",
|
||||
strings.Join(missing, ", "), strings.Join(tproxyPkgsFor(missing), " "))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -168,6 +351,7 @@ func routeEnsureProxyRule(be routeBackend, cfg *config.Config, set *config.SetCo
|
|||
}
|
||||
ensureProxyOutputBaseRulesNft(cfg, st, queueMark)
|
||||
default:
|
||||
proxyIptPreflight(legacy)
|
||||
if err := be.ensureChain(st.chainOut, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue