mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-08-15 12:03:49 +00:00
fix: enhance flow offloading detection and handling for improved performance and diagnostics
This commit is contained in:
parent
8b8286b88f
commit
3ea48d91f2
14 changed files with 498 additions and 37 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
80
install.sh
80
install.sh
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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 ---
|
||||
|
|
|
|||
|
|
@ -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 ;;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
112
src/http/handler/flowoffload_test.go
Normal file
112
src/http/handler/flowoffload_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 &&
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -652,6 +652,8 @@
|
|||
"flowOffloadOff": "выкл",
|
||||
"flowOffloadSw": "программный (в обход b4)",
|
||||
"flowOffloadHw": "аппаратный (в обход b4)",
|
||||
"flowOffloadSwGuarded": "программный, после {{packets}} пакетов",
|
||||
"flowOffloadHwGuarded": "аппаратный, после {{packets}} пакетов",
|
||||
"network": "Сеть",
|
||||
"engine": "Движок",
|
||||
"engineMode": "Режим",
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue