fix: enhance flow offloading detection and handling for improved performance and diagnostics

This commit is contained in:
Daniel Lavrushin 2026-08-08 15:21:53 +02:00 committed by Daniel Lavrushin
parent 8b8286b88f
commit 3ea48d91f2
14 changed files with 498 additions and 37 deletions

View file

@ -17,6 +17,7 @@
- ADDED: **A warm spare connection to your Cloudflare Worker** - one ready connection per Worker and data center, in place of the 65-90 ms handshake every new Telegram connection paid for.
- FIXED: **Traffic to a Cloudflare Worker was cut into one small write per Telegram message** - that framing was only ever required by Telegram's own WebSocket edge.
- CHANGED: **Several Worker addresses are tried in a random order** - they were tried strictly as written, so the first absorbed all the traffic and all the rate limiting.
- FIXED: **Flow offloading was reported as a problem even when it had been set up to leave b4 alone** - the check could not tell a fast path that skips b4 entirely from one held back until b4 has seen the start of a connection.
## [1.74.2] - 2026-08-02

View file

@ -17,6 +17,7 @@
- ДОБАВЛЕНО: **Прогретое запасное соединение к вашему Cloudflare Worker** - одно готовое соединение на каждый Worker и дата-центр вместо рукопожатия в 65-90 мс, которое оплачивало каждое новое соединение Telegram.
- ИСПРАВЛЕНО: **Трафик к Cloudflare Worker резался на мелкие записи, по одной на сообщение Telegram** - такая нарезка требовалась только собственному WebSocket-узлу Telegram.
- ИЗМЕНЕНО: **Несколько адресов Worker перебираются в случайном порядке** - они перебирались строго как записаны, поэтому первый принимал весь трафик и все ограничения по частоте запросов.
- ИСПРАВЛЕНО: **Flow offloading отмечался как проблема, даже когда был настроен так, чтобы не мешать b4** - проверка не отличала быстрый путь, полностью обходящий b4, от того, который включается только после начала соединения.
## [1.74.2] - 2026-08-02

View file

@ -143,3 +143,34 @@ Then start b4 again:
### Slow speed / video stuttering
Check the **Software flow offloading** setting under Network -> Firewall. Try turning it on or off - on some devices this affects b4 performance.
### Keeping flow offloading and b4 together
Flow offloading moves an established connection to a fast path that skips the netfilter hooks b4 works from, so with it enabled b4 runs but never sees the traffic. On weaker hardware turning it off costs a lot of throughput.
b4 only inspects the first packets of a connection (19 for TCP, 8 for UDP by default, see `queue.tcp_conn_bytes_limit` and `queue.udp_conn_bytes_limit`), so offloading can be delayed until b4 is done. Edit `/usr/share/firewall4/templates/ruleset.uc` and find:
```text
meta l4proto { tcp, udp } flow offload @ft;
```
Replace it with:
```text
meta l4proto { tcp, udp } ct original packets ge 40 flow offload @ft;
```
Then reload the firewall:
```bash
fw4 restart
```
Points to check:
- The threshold has to stay above the packet limits configured in b4. Raising `tcp_conn_bytes_limit` above the threshold puts the connection on the fast path before b4 is finished with it.
- `ct original packets` reads conntrack accounting. If `sysctl net.netfilter.nf_conntrack_acct` returns `0`, the counter stays at zero, the rule never matches and nothing is offloaded at all.
- Sets with **Duplicate** enabled for TCP are inspected for the whole life of the connection, so no threshold is high enough for them.
- The file belongs to the `firewall4` package and is overwritten by package upgrades and by sysupgrade.
The system diagnostics (Settings -> System info, and the installer's diagnostics screen) report the threshold they find and compare it against b4's own limits.

View file

@ -143,3 +143,34 @@ nft delete table inet b4_mangle 2>/dev/null
### Низкая скорость / тормозит видео
Проверьте настройку **Software flow offloading** в разделе Network → Firewall. Попробуйте включить или выключить её - на некоторых устройствах это влияет на производительность b4.
### Как совместить flow offloading и b4
Flow offloading переводит установленное соединение на быстрый путь, минуя хуки netfilter, с которыми работает b4: при включённом offloading b4 запущен, но трафика не видит. На слабом железе выключение offloading заметно снижает пропускную способность.
b4 разбирает только первые пакеты соединения (по умолчанию 19 для TCP и 8 для UDP, параметры `queue.tcp_conn_bytes_limit` и `queue.udp_conn_bytes_limit`), поэтому offloading можно отложить до момента, когда b4 уже отработал. Откройте `/usr/share/firewall4/templates/ruleset.uc` и найдите строку:
```text
meta l4proto { tcp, udp } flow offload @ft;
```
Замените её на:
```text
meta l4proto { tcp, udp } ct original packets ge 40 flow offload @ft;
```
Перезапустите файрвол:
```bash
fw4 restart
```
На что обратить внимание:
- Порог должен оставаться выше лимитов пакетов, заданных в b4. Если поднять `tcp_conn_bytes_limit` выше порога, соединение уйдёт на быстрый путь раньше, чем b4 с ним закончит.
- `ct original packets` читает счётчики conntrack. Если `sysctl net.netfilter.nf_conntrack_acct` возвращает `0`, счётчик остаётся нулевым, правило не срабатывает и offloading не включается вообще.
- Сеты с включённым **Duplicate** для TCP разбираются на протяжении всего соединения, для них не подойдёт никакой порог.
- Файл принадлежит пакету `firewall4` и перезаписывается при обновлении пакета и при sysupgrade.
Системная диагностика (Настройки -> Информация о системе и экран диагностики установщика) показывает найденный порог и сравнивает его с лимитами b4.

View file

@ -639,6 +639,70 @@ _ipt_connbytes_works() {
--connbytes-mode packets --connbytes 0:10 -j ACCEPT
}
_nft_flow_guard() {
awk '
/flow add @|flow offload @/ {
g = 0
for (i = 1; i <= NF; i++) {
if ($i == "ct" && $(i + 1) == "original" && $(i + 2) == "packets") {
op = $(i + 3)
val = $(i + 4) + 0
if (op == ">=" || op == "ge") g = val
else if (op == ">" || op == "gt") g = val + 1
}
}
if (!seen || g < min) min = g
seen = 1
}
END { print (seen ? min : 0) }
'
}
_ipt_flow_guard() {
awk '
/FLOWOFFLOAD/ {
g = 0
if (index($0, "--connbytes-dir original") && index($0, "--connbytes-mode packets") && !index($0, "! --connbytes ")) {
for (i = 1; i <= NF; i++) {
if ($i == "--connbytes") {
split($(i + 1), b, ":")
g = b[1] + 0
}
}
}
if (!seen || g < min) min = g
seen = 1
}
END { print (seen ? min : 0) }
'
}
_b4_queue_window() {
_qw_tcp=19
_qw_udp=8
if [ -n "$B4_CONFIG_FILE" ] && [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then
_qw_tcp=$(jq -r '.queue.tcp_conn_bytes_limit // 19' "$B4_CONFIG_FILE" 2>/dev/null || echo 19)
_qw_udp=$(jq -r '.queue.udp_conn_bytes_limit // 8' "$B4_CONFIG_FILE" 2>/dev/null || echo 8)
fi
case "$_qw_tcp" in '' | *[!0-9]*) _qw_tcp=19 ;; esac
case "$_qw_udp" in '' | *[!0-9]*) _qw_udp=8 ;; esac
if [ "$_qw_udp" -gt "$_qw_tcp" ]; then
echo "$_qw_udp"
else
echo "$_qw_tcp"
fi
}
_b4_duplicate_sets() {
if [ -z "$B4_CONFIG_FILE" ] || [ ! -f "$B4_CONFIG_FILE" ] || ! command_exists jq; then
echo 0
return 0
fi
_ds=$(jq -r '[.sets[]? | select(.enabled == true) | select(.tcp.duplicate.enabled == true)] | length' "$B4_CONFIG_FILE" 2>/dev/null || echo 0)
case "$_ds" in '' | *[!0-9]*) _ds=0 ;; esac
echo "$_ds"
}
_queue_functional() {
case "$1" in
nftables) _nft_queue_works ;;
@ -3615,6 +3679,7 @@ action_sysinfo() {
fi
_flow_offload=""
_flow_guard=0
if command_exists nft; then
_nft_ruleset=$(nft list ruleset 2>/dev/null)
if echo "$_nft_ruleset" | grep -q "flow add @\|flow offload @"; then
@ -3623,6 +3688,7 @@ action_sysinfo() {
else
_flow_offload="software"
fi
_flow_guard=$(echo "$_nft_ruleset" | _nft_flow_guard)
fi
fi
if [ -z "$_flow_offload" ]; then
@ -3635,14 +3701,22 @@ action_sysinfo() {
else
_flow_offload="software"
fi
_flow_guard=$(echo "$_ipt_filter" | _ipt_flow_guard)
break
fi
done
fi
if [ -n "$_flow_offload" ]; then
printf " ${RED} WARN${NC} %s\n" "Flow offloading active (${_flow_offload}) — bypasses b4; disable it for b4 to work" >&2
else
if [ -z "$_flow_offload" ]; then
printf " ${GREEN} OK${NC} %s\n" "Flow offloading off (b4 can intercept traffic)" >&2
elif [ "$_flow_guard" -gt "$(_b4_queue_window)" ]; then
printf " ${GREEN} OK${NC} %s\n" "Flow offloading active (${_flow_offload}), held off until packet ${_flow_guard} of a connection - b4 still sees the start of every connection" >&2
if [ "$(_b4_duplicate_sets)" != "0" ]; then
printf " ${YELLOW} WARN${NC} %s\n" "A routing set has TCP duplication enabled, which needs every packet of the connection - turn flow offloading off or disable duplication" >&2
fi
else
printf " ${RED} WARN${NC} %s\n" "Flow offloading active (${_flow_offload}) - bypasses b4; disable it for b4 to work" >&2
printf " ${DIM} Or hold it off until b4 has seen the start of a connection: in /usr/share/firewall4/templates/ruleset.uc${NC}\n" >&2
printf " ${DIM} replace 'flow offload @ft' with 'ct original packets ge 40 flow offload @ft', then: fw4 restart${NC}\n" >&2
fi
echo ""

View file

@ -295,11 +295,13 @@ action_sysinfo() {
fi
fi
# Flow offloading offloaded flows skip the netfilter hooks where b4's
# Flow offloading - offloaded flows skip the netfilter hooks where b4's
# NFQUEUE rules live, so b4 never sees the traffic (common cause of "b4
# installed but nothing is bypassed" on OpenWrt). Detect the active runtime
# state, not just the UCI config.
# installed but nothing is bypassed" on OpenWrt). A flowtable guarded by a
# packet counter is fine as long as it lets more packets through the slow
# path than b4 queues. Detect the active runtime state, not the UCI config.
_flow_offload=""
_flow_guard=0
if command_exists nft; then
_nft_ruleset=$(nft list ruleset 2>/dev/null)
if echo "$_nft_ruleset" | grep -q "flow add @\|flow offload @"; then
@ -308,6 +310,7 @@ action_sysinfo() {
else
_flow_offload="software"
fi
_flow_guard=$(echo "$_nft_ruleset" | _nft_flow_guard)
fi
fi
if [ -z "$_flow_offload" ]; then
@ -320,14 +323,22 @@ action_sysinfo() {
else
_flow_offload="software"
fi
_flow_guard=$(echo "$_ipt_filter" | _ipt_flow_guard)
break
fi
done
fi
if [ -n "$_flow_offload" ]; then
printf " ${RED} WARN${NC} %s\n" "Flow offloading active (${_flow_offload}) — bypasses b4; disable it for b4 to work" >&2
else
if [ -z "$_flow_offload" ]; then
printf " ${GREEN} OK${NC} %s\n" "Flow offloading off (b4 can intercept traffic)" >&2
elif [ "$_flow_guard" -gt "$(_b4_queue_window)" ]; then
printf " ${GREEN} OK${NC} %s\n" "Flow offloading active (${_flow_offload}), held off until packet ${_flow_guard} of a connection - b4 still sees the start of every connection" >&2
if [ "$(_b4_duplicate_sets)" != "0" ]; then
printf " ${YELLOW} WARN${NC} %s\n" "A routing set has TCP duplication enabled, which needs every packet of the connection - turn flow offloading off or disable duplication" >&2
fi
else
printf " ${RED} WARN${NC} %s\n" "Flow offloading active (${_flow_offload}) - bypasses b4; disable it for b4 to work" >&2
printf " ${DIM} Or hold it off until b4 has seen the start of a connection: in /usr/share/firewall4/templates/ruleset.uc${NC}\n" >&2
printf " ${DIM} replace 'flow offload @ft' with 'ct original packets ge 40 flow offload @ft', then: fw4 restart${NC}\n" >&2
fi
# --- Tools & dependencies ---

View file

@ -609,6 +609,70 @@ _ipt_connbytes_works() {
--connbytes-mode packets --connbytes 0:10 -j ACCEPT
}
_nft_flow_guard() {
awk '
/flow add @|flow offload @/ {
g = 0
for (i = 1; i <= NF; i++) {
if ($i == "ct" && $(i + 1) == "original" && $(i + 2) == "packets") {
op = $(i + 3)
val = $(i + 4) + 0
if (op == ">=" || op == "ge") g = val
else if (op == ">" || op == "gt") g = val + 1
}
}
if (!seen || g < min) min = g
seen = 1
}
END { print (seen ? min : 0) }
'
}
_ipt_flow_guard() {
awk '
/FLOWOFFLOAD/ {
g = 0
if (index($0, "--connbytes-dir original") && index($0, "--connbytes-mode packets") && !index($0, "! --connbytes ")) {
for (i = 1; i <= NF; i++) {
if ($i == "--connbytes") {
split($(i + 1), b, ":")
g = b[1] + 0
}
}
}
if (!seen || g < min) min = g
seen = 1
}
END { print (seen ? min : 0) }
'
}
_b4_queue_window() {
_qw_tcp=19
_qw_udp=8
if [ -n "$B4_CONFIG_FILE" ] && [ -f "$B4_CONFIG_FILE" ] && command_exists jq; then
_qw_tcp=$(jq -r '.queue.tcp_conn_bytes_limit // 19' "$B4_CONFIG_FILE" 2>/dev/null || echo 19)
_qw_udp=$(jq -r '.queue.udp_conn_bytes_limit // 8' "$B4_CONFIG_FILE" 2>/dev/null || echo 8)
fi
case "$_qw_tcp" in '' | *[!0-9]*) _qw_tcp=19 ;; esac
case "$_qw_udp" in '' | *[!0-9]*) _qw_udp=8 ;; esac
if [ "$_qw_udp" -gt "$_qw_tcp" ]; then
echo "$_qw_udp"
else
echo "$_qw_tcp"
fi
}
_b4_duplicate_sets() {
if [ -z "$B4_CONFIG_FILE" ] || [ ! -f "$B4_CONFIG_FILE" ] || ! command_exists jq; then
echo 0
return 0
fi
_ds=$(jq -r '[.sets[]? | select(.enabled == true) | select(.tcp.duplicate.enabled == true)] | length' "$B4_CONFIG_FILE" 2>/dev/null || echo 0)
case "$_ds" in '' | *[!0-9]*) _ds=0 ;; esac
echo "$_ds"
}
_queue_functional() {
case "$1" in
nftables) _nft_queue_works ;;

View file

@ -45,7 +45,7 @@ func (api *API) buildDiagnostics() Diagnostics {
Tools: collectTools(),
Network: collectNetworkInterfaces(),
Engine: collectEngineInfo(cfg),
Firewall: collectFirewallInfo(),
Firewall: collectFirewallInfo(cfg),
Geodata: api.collectGeodataInfo(),
Storage: collectStorage(),
Paths: collectPaths(cfg.ConfigPath, cfg.System.Logging.ErrorFilePath(), cfg.System.Geo.GeoSitePath, cfg.System.Geo.GeoIpPath),
@ -268,14 +268,15 @@ func collectNetworkInterfaces() DiagNetwork {
return DiagNetwork{Interfaces: result}
}
func collectFirewallInfo() DiagFirewall {
func collectFirewallInfo(cfg *config.Config) DiagFirewall {
info := DiagFirewall{Backend: detectFirewallBackend()}
info.RuleGroups = append(info.RuleGroups, collectNftRuleGroups()...)
info.RuleGroups = append(info.RuleGroups, collectIptablesRuleGroups(info.Backend)...)
info.NFQueueWorks = testNFQueue(info.Backend)
info.FlowOffload = detectFlowOffload()
info.FlowOffload, info.FlowOffloadGuard = detectFlowOffload()
info.FlowOffloadSafe = flowOffloadSafe(info.FlowOffload, info.FlowOffloadGuard, cfg)
return info
}
@ -478,21 +479,38 @@ func collectTUNInfo(cfg *config.Config) *DiagTUN {
// detectFlowOffload reports whether netfilter flow offloading is active on the
// system. Offloaded flows take a fast path that skips the forward/postrouting
// hooks where b4's NFQUEUE rules live, so an active flowtable means b4 never
// sees the traffic. Returns "hardware", "software" or "off".
func detectFlowOffload() string {
// hooks where b4's NFQUEUE rules live, so an unguarded flowtable means b4 never
// sees the traffic. Returns "hardware", "software" or "off", plus the smallest
// original-direction packet count that has to pass before a flow is offloaded
// (0 when offloading starts immediately).
func detectFlowOffload() (string, int) {
if _, err := exec.LookPath("nft"); err == nil {
out, err := exec.Command("nft", "list", "ruleset").CombinedOutput()
if err == nil {
s := string(out)
if strings.Contains(s, "flow add @") || strings.Contains(s, "flow offload @") {
for _, line := range strings.Split(s, "\n") {
l := strings.TrimSpace(line)
if strings.HasPrefix(l, "flags") && strings.Contains(l, "offload") {
return "hardware"
}
hardware := false
mode := ""
guard := 0
for _, line := range strings.Split(s, "\n") {
l := strings.TrimSpace(line)
if strings.HasPrefix(l, "flags") && strings.Contains(l, "offload") {
hardware = true
continue
}
return "software"
if !strings.Contains(l, "flow add @") && !strings.Contains(l, "flow offload @") {
continue
}
g := nftFlowOffloadGuard(l)
if mode == "" || g < guard {
guard = g
}
mode = "software"
}
if mode != "" {
if hardware {
mode = "hardware"
}
return mode, guard
}
}
}
@ -502,15 +520,110 @@ func detectFlowOffload() string {
continue
}
out, err := exec.Command(bin, append(tables.WaitArgs(bin), "-t", "filter", "-S")...).CombinedOutput()
if err == nil && strings.Contains(string(out), "FLOWOFFLOAD") {
if strings.Contains(string(out), "--hw") {
return "hardware"
}
return "software"
if err != nil || !strings.Contains(string(out), "FLOWOFFLOAD") {
continue
}
mode := "software"
guard := 0
first := true
for _, line := range strings.Split(string(out), "\n") {
if !strings.Contains(line, "FLOWOFFLOAD") {
continue
}
if strings.Contains(line, "--hw") {
mode = "hardware"
}
g := iptablesFlowOffloadGuard(line)
if first || g < guard {
guard = g
first = false
}
}
return mode, guard
}
return "off"
return "off", 0
}
// nftFlowOffloadGuard returns the minimum number of original-direction packets a
// connection must carry before the given nftables flow-offload rule applies.
// Only "ct original packets" guards count: a guard on any other counter cannot
// be compared against b4's original-direction connbytes window.
func nftFlowOffloadGuard(rule string) int {
idx := strings.Index(rule, "ct original packets")
if idx < 0 {
return 0
}
rest := strings.TrimSpace(rule[idx+len("ct original packets"):])
for _, op := range []string{">=", "ge", ">", "gt"} {
if !strings.HasPrefix(rest, op) {
continue
}
fields := strings.Fields(rest[len(op):])
if len(fields) == 0 {
return 0
}
n, err := strconv.Atoi(fields[0])
if err != nil || n <= 0 {
return 0
}
if op == ">" || op == "gt" {
return n + 1
}
return n
}
return 0
}
// iptablesFlowOffloadGuard is the xt_FLOWOFFLOAD equivalent of
// nftFlowOffloadGuard, reading the lower bound of an original-direction
// packet-mode connbytes match on the same rule.
func iptablesFlowOffloadGuard(rule string) int {
if !strings.Contains(rule, "--connbytes-dir original") || !strings.Contains(rule, "--connbytes-mode packets") {
return 0
}
if strings.Contains(rule, "! --connbytes ") {
return 0
}
idx := strings.Index(rule, "--connbytes ")
if idx < 0 {
return 0
}
fields := strings.Fields(rule[idx+len("--connbytes "):])
if len(fields) == 0 {
return 0
}
lower, _, _ := strings.Cut(fields[0], ":")
n, err := strconv.Atoi(lower)
if err != nil || n <= 0 {
return 0
}
return n
}
// flowOffloadSafe reports whether an active flowtable still lets b4 do its job:
// every offloaded flow has to stay on the slow path for longer than the widest
// connbytes window b4 queues on. Sets with TCP duplication are queued for the
// whole life of the connection, so no guard is wide enough for them.
func flowOffloadSafe(mode string, guard int, cfg *config.Config) bool {
if mode == "off" {
return true
}
if guard <= 0 || cfg == nil {
return false
}
if dup4, dup6 := cfg.CollectDuplicateIPs(); len(dup4) > 0 || len(dup6) > 0 {
return false
}
window := cfg.Queue.TCPConnBytesLimit
if cfg.Queue.UDPConnBytesLimit > window {
window = cfg.Queue.UDPConnBytesLimit
}
return guard > window
}
func testNFQueue(backend string) bool {

View file

@ -0,0 +1,112 @@
package handler
import (
"testing"
"github.com/daniellavrushin/b4/config"
)
func TestNftFlowOffloadGuard(t *testing.T) {
cases := []struct {
name string
rule string
want int
}{
{"unguarded", "meta l4proto { tcp, udp } flow add @ft", 0},
{"ge", "meta l4proto { tcp, udp } ct original packets ge 30 flow offload @ft", 30},
{"symbolic ge", "meta l4proto { tcp, udp } ct original packets >= 40 flow add @ft", 40},
{"gt", "meta l4proto { tcp, udp } ct original packets gt 29 flow add @ft", 30},
{"symbolic gt", "meta l4proto { tcp, udp } ct original packets > 29 flow add @ft", 30},
{"reply counter is not a guard", "ct reply packets ge 30 flow add @ft", 0},
{"total counter is not a guard", "ct packets ge 30 flow add @ft", 0},
{"zero threshold", "ct original packets ge 0 flow add @ft", 0},
{"garbage threshold", "ct original packets ge abc flow add @ft", 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := nftFlowOffloadGuard(tc.rule); got != tc.want {
t.Errorf("nftFlowOffloadGuard(%q) = %d, want %d", tc.rule, got, tc.want)
}
})
}
}
func TestIptablesFlowOffloadGuard(t *testing.T) {
cases := []struct {
name string
rule string
want int
}{
{"unguarded", "-A FORWARD -j FLOWOFFLOAD", 0},
{"guarded", "-A FORWARD -m connbytes --connbytes 30:0 --connbytes-mode packets --connbytes-dir original -j FLOWOFFLOAD", 30},
{"open upper bound", "-A FORWARD -m connbytes --connbytes 40: --connbytes-mode packets --connbytes-dir original -j FLOWOFFLOAD", 40},
{"bytes mode is not a guard", "-A FORWARD -m connbytes --connbytes 30:0 --connbytes-mode bytes --connbytes-dir original -j FLOWOFFLOAD", 0},
{"reply direction is not a guard", "-A FORWARD -m connbytes --connbytes 30:0 --connbytes-mode packets --connbytes-dir reply -j FLOWOFFLOAD", 0},
{"inverted match is not a guard", "-A FORWARD -m connbytes ! --connbytes 0:29 --connbytes-mode packets --connbytes-dir original -j FLOWOFFLOAD", 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := iptablesFlowOffloadGuard(tc.rule); got != tc.want {
t.Errorf("iptablesFlowOffloadGuard(%q) = %d, want %d", tc.rule, got, tc.want)
}
})
}
}
func TestFlowOffloadSafe(t *testing.T) {
newCfg := func() *config.Config {
cfg := config.DefaultConfig
return &cfg
}
t.Run("off is always safe", func(t *testing.T) {
if !flowOffloadSafe("off", 0, newCfg()) {
t.Error("expected off to be safe")
}
})
t.Run("unguarded is unsafe", func(t *testing.T) {
if flowOffloadSafe("software", 0, newCfg()) {
t.Error("expected unguarded offload to be unsafe")
}
})
t.Run("guard above the queue window is safe", func(t *testing.T) {
cfg := newCfg()
if !flowOffloadSafe("hardware", cfg.Queue.TCPConnBytesLimit+1, cfg) {
t.Error("expected guard above the window to be safe")
}
})
t.Run("guard equal to the queue window is unsafe", func(t *testing.T) {
cfg := newCfg()
if flowOffloadSafe("software", cfg.Queue.TCPConnBytesLimit, cfg) {
t.Error("expected guard equal to the window to be unsafe")
}
})
t.Run("udp window is taken into account", func(t *testing.T) {
cfg := newCfg()
cfg.Queue.TCPConnBytesLimit = 10
cfg.Queue.UDPConnBytesLimit = 25
if flowOffloadSafe("software", 20, cfg) {
t.Error("expected guard below the udp window to be unsafe")
}
if !flowOffloadSafe("software", 26, cfg) {
t.Error("expected guard above both windows to be safe")
}
})
t.Run("duplication is never safe", func(t *testing.T) {
cfg := newCfg()
set := &config.SetConfig{Enabled: true}
set.TCP.Duplicate.Enabled = true
set.Targets.IpsToMatch = []string{"1.2.3.4"}
cfg.Sets = []*config.SetConfig{set}
if flowOffloadSafe("software", 1000, cfg) {
t.Error("expected duplication sets to make any guard unsafe")
}
})
}

View file

@ -119,10 +119,12 @@ type DiagTUN struct {
}
type DiagFirewall struct {
Backend string `json:"backend"`
NFQueueWorks bool `json:"nfqueue_works"`
FlowOffload string `json:"flow_offload"`
RuleGroups []DiagRuleGroup `json:"rule_groups,omitempty"`
Backend string `json:"backend"`
NFQueueWorks bool `json:"nfqueue_works"`
FlowOffload string `json:"flow_offload"`
FlowOffloadGuard int `json:"flow_offload_guard,omitempty"`
FlowOffloadSafe bool `json:"flow_offload_safe"`
RuleGroups []DiagRuleGroup `json:"rule_groups,omitempty"`
}
type DiagRuleGroup struct {

View file

@ -68,6 +68,23 @@ export const SystemInfoDialog = ({ open, onClose }: SystemInfoDialogProps) => {
);
};
const flowOffloadText = (fw: Diagnostics["firewall"]) => {
if (fw.flow_offload === "off") return t("settings.SystemInfo.flowOffloadOff");
const hw = fw.flow_offload === "hardware";
if (fw.flow_offload_safe)
return t(
hw
? "settings.SystemInfo.flowOffloadHwGuarded"
: "settings.SystemInfo.flowOffloadSwGuarded",
{ packets: fw.flow_offload_guard ?? 0 },
);
return t(
hw
? "settings.SystemInfo.flowOffloadHw"
: "settings.SystemInfo.flowOffloadSw",
);
};
const boolChip = (ok: boolean, yesLabel: string, noLabel: string) => (
<Chip
size="small"
@ -397,11 +414,9 @@ export const SystemInfoDialog = ({ open, onClose }: SystemInfoDialogProps) => {
{row(
t("settings.SystemInfo.flowOffload"),
boolChip(
data.firewall.flow_offload === "off",
t("settings.SystemInfo.flowOffloadOff"),
data.firewall.flow_offload === "hardware"
? t("settings.SystemInfo.flowOffloadHw")
: t("settings.SystemInfo.flowOffloadSw"),
data.firewall.flow_offload_safe,
flowOffloadText(data.firewall),
flowOffloadText(data.firewall),
),
)}
{data.firewall.rule_groups &&

View file

@ -656,6 +656,8 @@
"flowOffloadOff": "off",
"flowOffloadSw": "software (bypasses b4)",
"flowOffloadHw": "hardware (bypasses b4)",
"flowOffloadSwGuarded": "software, after {{packets}} packets",
"flowOffloadHwGuarded": "hardware, after {{packets}} packets",
"network": "Network",
"engine": "Engine",
"engineMode": "Mode",

View file

@ -652,6 +652,8 @@
"flowOffloadOff": "выкл",
"flowOffloadSw": "программный (в обход b4)",
"flowOffloadHw": "аппаратный (в обход b4)",
"flowOffloadSwGuarded": "программный, после {{packets}} пакетов",
"flowOffloadHwGuarded": "аппаратный, после {{packets}} пакетов",
"network": "Сеть",
"engine": "Движок",
"engineMode": "Режим",

View file

@ -64,6 +64,8 @@ interface DiagFirewall {
backend: string;
nfqueue_works: boolean;
flow_offload: string;
flow_offload_guard?: number;
flow_offload_safe: boolean;
rule_groups?: DiagRuleGroup[];
}