mirror of
https://github.com/DanielLavrushin/b4.git
synced 2026-07-09 16:00:05 +00:00
1-70-1 fixes (#261)
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
b41171562a
commit
6a216255c3
28 changed files with 1074 additions and 138 deletions
|
|
@ -1,5 +1,13 @@
|
|||
# B4 - Bye Bye Big Bro
|
||||
|
||||
## [1.70.1] - 2026-06-23
|
||||
|
||||
- ADDED: **System diagnostics now show the active engine and the firewall rules b4 has set up** - the diagnostics page tells you whether b4 is running in NFQUEUE or TUN mode (so TUN setups no longer look like a failed firewall), and lists the firewall rules b4 currently maintains, making it easier to see what is in place and share when asking for help.
|
||||
- CHANGED: **b4's internal firewall marks moved to a less-contended range** - b4 tags its own packets with a firewall mark (fwmark); the values it used overlapped the range Tailscale and some other router apps use, which could misroute traffic when they ran alongside b4. The internal marks now use high bits that other tools rarely touch.
|
||||
- FIXED: **Bypass stopped working for mobile apps (for example the YouTube app on Android) on gateway/container setups with NAT Masquerade** - the fake and fragmented packets b4 sends to defeat DPI were no longer given the gateway's public address, so they never reached the server. This mainly hit QUIC traffic, which phone apps prefer, while ordinary websites over TCP kept working.
|
||||
- FIXED: **A set could fail to start when it mixed a country/service list with its own listed addresses** - if an address you added by hand was already covered by a chosen list (for example adding Instagram addresses alongside the Facebook list), b4 saw them as overlapping and refused to start.
|
||||
- FIXED: **In TUN mode every device appeared under the router's public address, so traffic could not be told apart by device** - the TUN engine tags packets with the router's WAN address before b4 inspects them, which flattened the connection logs, per-device statistics, and per-device set rules onto that one address and hid the phone, laptop, or TV behind it.
|
||||
|
||||
## [1.70.0] - 2026-06-21
|
||||
|
||||
- ADDED: **New engine for devices without NFQUEUE** - some minimal devices lack the kernel modules b4 normally needs, so it could not run on them. A new mode under Settings → Feature Flags routes traffic through a virtual interface (TUN) instead. It inspects only the first packets of each TLS (port 443) and DNS connection, like the normal engine, rather than carrying every packet, and leaves the device's default route in place; on kernels that cannot count per-connection packets it routes the whole default route through the TUN instead. Strategy discovery (auto-tuning) still needs NFQUEUE, so it cannot run on these devices.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
# B4 - Bye Bye Big Bro
|
||||
|
||||
## [1.70.1] - 2026-06-23
|
||||
|
||||
- ДОБАВЛЕНО: **Системная диагностика теперь показывает активный движок и правила фаервола, созданные b4** - на странице диагностики видно, в каком режиме работает b4 — NFQUEUE или TUN (поэтому в режиме TUN это больше не выглядит как ошибка фаервола), и приводится список правил фаервола, которые b4 сейчас поддерживает, так что проще понять, что настроено, и приложить эту информацию при обращении за помощью.
|
||||
- ИЗМЕНЕНО: **Внутренние firewall-метки b4 перенесены в менее загруженный диапазон** - b4 помечает свои пакеты firewall-меткой (fwmark); прежние значения пересекались с диапазоном, который используют Tailscale и некоторые другие приложения на роутере, из-за чего трафик мог маршрутизироваться неправильно при их совместной работе с b4. Внутренние метки теперь используют старшие биты, которые другие инструменты затрагивают редко.
|
||||
- ИСПРАВЛЕНО: **Обход перестал работать для мобильных приложений (например, приложение YouTube на Android) на шлюзах/контейнерах с NAT Masquerade** - поддельные и фрагментированные пакеты, которые b4 отправляет для обхода DPI, больше не получали публичный адрес шлюза и не доходили до сервера. В основном это затрагивало трафик QUIC, который предпочитают мобильные приложения, тогда как обычные сайты по TCP продолжали работать.
|
||||
- ИСПРАВЛЕНО: **Сет мог не запускаться, если в нём смешаны список страны/сервиса и собственные адреса** - если добавленный вручную адрес уже входил в выбранный список (например, адреса Instagram вместе со списком Facebook), b4 считал их пересекающимися и отказывался запускаться.
|
||||
- ИСПРАВЛЕНО: **В режиме TUN все устройства отображались под публичным адресом роутера, поэтому трафик нельзя было различить по устройству** - движок TUN помечает пакеты адресом WAN роутера до того, как b4 их обработает, из-за чего журнал соединений, статистика по устройствам и правила сетов с привязкой к устройству схлопывались до одного адреса и за ним терялись телефон, ноутбук или телевизор.
|
||||
|
||||
## [1.70.0] - 2026-06-21
|
||||
|
||||
- ДОБАВЛЕНО: **Новый движок для устройств без NFQUEUE** - на некоторых минимальных устройствах нет модулей ядра, которые обычно нужны b4, поэтому он на них не запускался. Новый режим в «Настройки → Функции» пропускает трафик через виртуальный интерфейс (TUN). Он обрабатывает только первые пакеты каждого соединения TLS (порт 443) и DNS, как обычный движок, а не весь трафик, и не трогает маршрут по умолчанию устройства; на ядрах, которые не умеют считать пакеты по соединению, он вместо этого направляет в TUN весь маршрут по умолчанию. Подбор стратегий (Дискавери) по-прежнему требует NFQUEUE, поэтому на таких устройствах он не работает.
|
||||
|
|
|
|||
|
|
@ -257,11 +257,18 @@ func (c *Config) Validate() error {
|
|||
}
|
||||
|
||||
if c.Queue.Mode == "tun" {
|
||||
const tunReservedMarkBits = uint(engine.TunSteerMark | engine.TunClientMark | engine.ReinjectMarkBit)
|
||||
const tunReservedMarkBits = uint(engine.TunSteerMark | engine.ClientMark | engine.ReinjectMarkBit)
|
||||
if m := c.MainInjectedMark(); m&tunReservedMarkBits != 0 {
|
||||
v.addf("queue.mark", "mark_conflict", map[string]any{"mark": fmt.Sprintf("0x%x", m)},
|
||||
"queue mark 0x%x overlaps reserved TUN mark bits (0x%x steer, 0x%x client, 0x%x reinject); choose a mark clear of those bits",
|
||||
m, uint(engine.TunSteerMark), uint(engine.TunClientMark), uint(engine.ReinjectMarkBit))
|
||||
m, uint(engine.TunSteerMark), uint(engine.ClientMark), uint(engine.ReinjectMarkBit))
|
||||
return v.result()
|
||||
}
|
||||
} else if c.System.Tables.Masquerade {
|
||||
if m := c.MainInjectedMark(); m&uint(engine.ClientMark) != 0 {
|
||||
v.addf("queue.mark", "mark_conflict", map[string]any{"mark": fmt.Sprintf("0x%x", m)},
|
||||
"queue mark 0x%x overlaps the reserved client mark bit (0x%x) used by NAT masquerade; choose a mark clear of that bit",
|
||||
m, uint(engine.ClientMark))
|
||||
return v.result()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -369,6 +369,20 @@ func TestValidate_QueueFields(t *testing.T) {
|
|||
t.Errorf("expected reserved-TUN-bits message, got %q", f.Message)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nfqueue mode rejects mark overlapping client mark bit", func(t *testing.T) {
|
||||
cfg := NewConfig()
|
||||
cfg.System.Tables.Masquerade = true
|
||||
cfg.Queue.Mark = engine.ClientMark
|
||||
ve := mustValidationErr(t, cfg.Validate())
|
||||
f := findField(ve, "queue.mark", "mark_conflict")
|
||||
if f == nil {
|
||||
t.Fatalf("missing queue.mark mark_conflict; got %+v", ve.Fields)
|
||||
}
|
||||
if !strings.Contains(f.Message, "client mark bit") {
|
||||
t.Errorf("expected client-mark-bit message, got %q", f.Message)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidate_RequiredSetID(t *testing.T) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
package engine
|
||||
|
||||
const (
|
||||
TunSteerMark = 0x80000
|
||||
TunClientMark = 0x100000
|
||||
ReinjectMarkBit = 0x800000
|
||||
TunSteerMark = 0x40000000
|
||||
ClientMark = 0x20000000
|
||||
ReinjectMarkBit = 0x10000000
|
||||
)
|
||||
|
||||
type PacketVerdict int
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import (
|
|||
"github.com/daniellavrushin/b4/geodat"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
"github.com/daniellavrushin/b4/nfq"
|
||||
b4tun "github.com/daniellavrushin/b4/tun"
|
||||
"github.com/daniellavrushin/b4/utils"
|
||||
"github.com/daniellavrushin/b4/watchdog"
|
||||
"golang.org/x/sys/unix"
|
||||
|
|
@ -43,8 +44,13 @@ var (
|
|||
discoveryRuntime *discovery.Runtime
|
||||
globalWatchdog *watchdog.Watchdog
|
||||
globalAIManager *ai.Manager
|
||||
globalTUNEngine *b4tun.Engine
|
||||
)
|
||||
|
||||
func SetTUNEngine(e *b4tun.Engine) {
|
||||
globalTUNEngine = e
|
||||
}
|
||||
|
||||
func SetAIManager(m *ai.Manager) {
|
||||
globalAIManager = m
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/engine"
|
||||
"github.com/daniellavrushin/b4/tables"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
|
@ -38,6 +40,7 @@ func (api *API) handleDiagnostics(w http.ResponseWriter, r *http.Request) {
|
|||
Kernel: collectKernelModules(),
|
||||
Tools: collectTools(),
|
||||
Network: collectNetworkInterfaces(),
|
||||
Engine: collectEngineInfo(cfg),
|
||||
Firewall: collectFirewallInfo(),
|
||||
Geodata: api.collectGeodataInfo(),
|
||||
Storage: collectStorage(),
|
||||
|
|
@ -264,46 +267,10 @@ func collectNetworkInterfaces() DiagNetwork {
|
|||
}
|
||||
|
||||
func collectFirewallInfo() DiagFirewall {
|
||||
info := DiagFirewall{}
|
||||
info := DiagFirewall{Backend: detectFirewallBackend()}
|
||||
|
||||
if _, err := exec.LookPath("nft"); err == nil {
|
||||
out, err := exec.Command("nft", "list", "table", "inet", "b4_mangle").CombinedOutput()
|
||||
if err == nil && len(out) > 0 {
|
||||
info.Backend = "nftables"
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.HasPrefix(line, "table") && !strings.HasPrefix(line, "}") && !strings.HasPrefix(line, "chain") {
|
||||
info.ActiveRules = append(info.ActiveRules, line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if info.Backend == "" {
|
||||
for _, bin := range []string{"iptables", "iptables-legacy"} {
|
||||
if _, err := exec.LookPath(bin); err != nil {
|
||||
continue
|
||||
}
|
||||
out, err := exec.Command(bin, append(tables.WaitArgs(bin), "-t", "mangle", "-S", "B4")...).CombinedOutput()
|
||||
if err == nil && len(out) > 0 {
|
||||
if bin == "iptables-legacy" {
|
||||
info.Backend = "iptables-legacy"
|
||||
} else {
|
||||
info.Backend = "iptables"
|
||||
}
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
if line != "" {
|
||||
info.ActiveRules = append(info.ActiveRules, line)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if info.Backend == "" {
|
||||
info.Backend = "none"
|
||||
}
|
||||
info.RuleGroups = append(info.RuleGroups, collectNftRuleGroups()...)
|
||||
info.RuleGroups = append(info.RuleGroups, collectIptablesRuleGroups(info.Backend)...)
|
||||
|
||||
info.NFQueueWorks = testNFQueue(info.Backend)
|
||||
info.FlowOffload = detectFlowOffload()
|
||||
|
|
@ -311,6 +278,202 @@ func collectFirewallInfo() DiagFirewall {
|
|||
return info
|
||||
}
|
||||
|
||||
func detectFirewallBackend() string {
|
||||
if _, err := exec.LookPath("nft"); err == nil {
|
||||
out, err := exec.Command("nft", "list", "table", "inet", "b4_mangle").CombinedOutput()
|
||||
if err == nil && len(out) > 0 {
|
||||
return "nftables"
|
||||
}
|
||||
}
|
||||
|
||||
for _, bin := range []string{"iptables", "iptables-legacy"} {
|
||||
if _, err := exec.LookPath(bin); err != nil {
|
||||
continue
|
||||
}
|
||||
out, err := exec.Command(bin, append(tables.WaitArgs(bin), "-t", "mangle", "-S", "B4")...).CombinedOutput()
|
||||
if err == nil && len(out) > 0 {
|
||||
if bin == "iptables-legacy" {
|
||||
return "iptables-legacy"
|
||||
}
|
||||
return "iptables"
|
||||
}
|
||||
}
|
||||
|
||||
return "none"
|
||||
}
|
||||
|
||||
func collectNftRuleGroups() []DiagRuleGroup {
|
||||
if _, err := exec.LookPath("nft"); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
out, err := exec.Command("nft", "list", "tables").CombinedOutput()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var groups []DiagRuleGroup
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
f := strings.Fields(line)
|
||||
if len(f) != 3 || f[0] != "table" {
|
||||
continue
|
||||
}
|
||||
family, name := f[1], f[2]
|
||||
if !strings.HasPrefix(name, "b4_") {
|
||||
continue
|
||||
}
|
||||
|
||||
tout, terr := exec.Command("nft", "list", "table", family, name).CombinedOutput()
|
||||
if terr != nil {
|
||||
continue
|
||||
}
|
||||
if rules := nonEmptyLines(string(tout)); len(rules) > 0 {
|
||||
groups = append(groups, DiagRuleGroup{
|
||||
Title: fmt.Sprintf("nft %s %s", family, name),
|
||||
Rules: rules,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func collectIptablesRuleGroups(backend string) []DiagRuleGroup {
|
||||
bin := "iptables"
|
||||
if backend == "iptables-legacy" {
|
||||
bin = "iptables-legacy"
|
||||
}
|
||||
if _, err := exec.LookPath(bin); err != nil {
|
||||
return nil
|
||||
}
|
||||
wait := tables.WaitArgs(bin)
|
||||
|
||||
var groups []DiagRuleGroup
|
||||
|
||||
for _, chain := range []string{"B4", "B4_PREROUTING"} {
|
||||
out, err := exec.Command(bin, append(append([]string{}, wait...), "-t", "mangle", "-S", chain)...).CombinedOutput()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if rules := nonEmptyLines(string(out)); len(rules) > 0 {
|
||||
groups = append(groups, DiagRuleGroup{
|
||||
Title: fmt.Sprintf("iptables mangle %s", chain),
|
||||
Rules: rules,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for _, table := range []string{"mangle", "nat", "raw", "filter"} {
|
||||
out, err := exec.Command(bin, append(append([]string{}, wait...), "-t", table, "-S")...).CombinedOutput()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var rules []string
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
l := strings.TrimSpace(line)
|
||||
if l == "" || !isB4IptablesRule(l) {
|
||||
continue
|
||||
}
|
||||
rules = append(rules, l)
|
||||
}
|
||||
if len(rules) > 0 {
|
||||
groups = append(groups, DiagRuleGroup{
|
||||
Title: fmt.Sprintf("iptables %s (b4)", table),
|
||||
Rules: rules,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
func isB4IptablesRule(l string) bool {
|
||||
if strings.HasPrefix(l, "-N B4") || strings.HasPrefix(l, "-A B4") {
|
||||
return false
|
||||
}
|
||||
clientMark := fmt.Sprintf("0x%x/0x%x", engine.ClientMark, engine.ClientMark)
|
||||
return strings.Contains(l, "b4_") ||
|
||||
strings.Contains(l, "b4tun") ||
|
||||
strings.Contains(l, "NFQUEUE") ||
|
||||
strings.Contains(l, "-j B4") ||
|
||||
strings.Contains(l, clientMark)
|
||||
}
|
||||
|
||||
func nonEmptyLines(s string) []string {
|
||||
var out []string
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
if strings.TrimRight(line, " \t") == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, strings.TrimRight(line, " \t"))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func collectEngineInfo(cfg *config.Config) DiagEngine {
|
||||
mode := cfg.Queue.Mode
|
||||
if mode == "" {
|
||||
mode = "nfqueue"
|
||||
}
|
||||
|
||||
de := DiagEngine{Mode: mode}
|
||||
if mode == "tun" {
|
||||
de.TUN = collectTUNInfo(cfg)
|
||||
}
|
||||
|
||||
return de
|
||||
}
|
||||
|
||||
func collectTUNInfo(cfg *config.Config) *DiagTUN {
|
||||
tunCfg := cfg.Queue.TUN
|
||||
t := &DiagTUN{
|
||||
DeviceName: tunCfg.DeviceName,
|
||||
Address: tunCfg.Address,
|
||||
AddressV6: tunCfg.AddressV6,
|
||||
OutInterface: tunCfg.OutInterface,
|
||||
OutGateway: tunCfg.OutGateway,
|
||||
RouteTable: tunCfg.RouteTable,
|
||||
}
|
||||
|
||||
if globalTUNEngine != nil {
|
||||
di := globalTUNEngine.DiagInfo()
|
||||
t.Running = true
|
||||
if di.DeviceName != "" {
|
||||
t.DeviceName = di.DeviceName
|
||||
}
|
||||
if di.Address != "" {
|
||||
t.Address = di.Address
|
||||
}
|
||||
if di.AddressV6 != "" {
|
||||
t.AddressV6 = di.AddressV6
|
||||
}
|
||||
if di.OutInterface != "" {
|
||||
t.OutInterface = di.OutInterface
|
||||
}
|
||||
if di.OutGateway != "" {
|
||||
t.OutGateway = di.OutGateway
|
||||
}
|
||||
if di.RouteTable != 0 {
|
||||
t.RouteTable = di.RouteTable
|
||||
}
|
||||
t.ResolvedSrc = di.ResolvedSrc
|
||||
t.Capture = di.Capture
|
||||
t.ReplyCapture = di.ReplyCapture
|
||||
t.PacketsForwarded = di.PacketsForwarded
|
||||
t.ForwardErrors = di.ForwardErrors
|
||||
t.IPv6Dropped = di.IPv6Dropped
|
||||
}
|
||||
|
||||
if t.DeviceName != "" {
|
||||
if iface, err := net.InterfaceByName(t.DeviceName); err == nil {
|
||||
t.DeviceUp = iface.Flags&net.FlagUp != 0
|
||||
t.MTU = iface.MTU
|
||||
}
|
||||
}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ type Diagnostics struct {
|
|||
Kernel DiagKernel `json:"kernel"`
|
||||
Tools DiagTools `json:"tools"`
|
||||
Network DiagNetwork `json:"network"`
|
||||
Engine DiagEngine `json:"engine"`
|
||||
Firewall DiagFirewall `json:"firewall"`
|
||||
Geodata DiagGeodata `json:"geodata"`
|
||||
Storage []DiagMount `json:"storage"`
|
||||
|
|
@ -94,11 +95,39 @@ type DiagInterface struct {
|
|||
MTU int `json:"mtu"`
|
||||
}
|
||||
|
||||
type DiagEngine struct {
|
||||
Mode string `json:"mode"`
|
||||
TUN *DiagTUN `json:"tun,omitempty"`
|
||||
}
|
||||
|
||||
type DiagTUN struct {
|
||||
Running bool `json:"running"`
|
||||
DeviceName string `json:"device_name"`
|
||||
DeviceUp bool `json:"device_up"`
|
||||
MTU int `json:"mtu,omitempty"`
|
||||
Address string `json:"address,omitempty"`
|
||||
AddressV6 string `json:"address_v6,omitempty"`
|
||||
OutInterface string `json:"out_interface,omitempty"`
|
||||
OutGateway string `json:"out_gateway,omitempty"`
|
||||
ResolvedSrc string `json:"resolved_src,omitempty"`
|
||||
Capture string `json:"capture,omitempty"`
|
||||
RouteTable int `json:"route_table,omitempty"`
|
||||
ReplyCapture bool `json:"reply_capture"`
|
||||
PacketsForwarded uint64 `json:"packets_forwarded"`
|
||||
ForwardErrors uint64 `json:"forward_errors"`
|
||||
IPv6Dropped uint64 `json:"ipv6_dropped"`
|
||||
}
|
||||
|
||||
type DiagFirewall struct {
|
||||
Backend string `json:"backend"`
|
||||
NFQueueWorks bool `json:"nfqueue_works"`
|
||||
FlowOffload string `json:"flow_offload"`
|
||||
ActiveRules []string `json:"active_rules,omitempty"`
|
||||
Backend string `json:"backend"`
|
||||
NFQueueWorks bool `json:"nfqueue_works"`
|
||||
FlowOffload string `json:"flow_offload"`
|
||||
RuleGroups []DiagRuleGroup `json:"rule_groups,omitempty"`
|
||||
}
|
||||
|
||||
type DiagRuleGroup struct {
|
||||
Title string `json:"title"`
|
||||
Rules []string `json:"rules"`
|
||||
}
|
||||
|
||||
type DiagGeodata struct {
|
||||
|
|
|
|||
|
|
@ -75,11 +75,39 @@ interface DiagInterface {
|
|||
mtu: number;
|
||||
}
|
||||
|
||||
interface DiagRuleGroup {
|
||||
title: string;
|
||||
rules: string[];
|
||||
}
|
||||
|
||||
interface DiagFirewall {
|
||||
backend: string;
|
||||
nfqueue_works: boolean;
|
||||
flow_offload: string;
|
||||
active_rules?: 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 {
|
||||
|
|
@ -108,6 +136,7 @@ interface Diagnostics {
|
|||
kernel: { modules: DiagModule[] };
|
||||
tools: { firewall: DiagTool[]; required: DiagTool[]; optional: DiagTool[] };
|
||||
network: { interfaces: DiagInterface[] };
|
||||
engine: DiagEngine;
|
||||
firewall: DiagFirewall;
|
||||
geodata: DiagGeodata;
|
||||
storage: DiagMount[];
|
||||
|
|
@ -381,9 +410,110 @@ export const SystemInfoDialog = ({ open, onClose }: SystemInfoDialogProps) => {
|
|||
data.geodata.total_ips.toLocaleString(),
|
||||
)}
|
||||
|
||||
{sectionTitle(t("settings.SystemInfo.engine"))}
|
||||
{row(
|
||||
t("settings.SystemInfo.engineMode"),
|
||||
<Chip
|
||||
size="small"
|
||||
label={
|
||||
data.engine.mode === "tun"
|
||||
? t("settings.SystemInfo.engineTun")
|
||||
: t("settings.SystemInfo.engineNfqueue")
|
||||
}
|
||||
sx={{
|
||||
bgcolor: `${colors.secondary}22`,
|
||||
color: colors.secondary,
|
||||
fontSize: "0.7rem",
|
||||
height: 22,
|
||||
}}
|
||||
/>,
|
||||
)}
|
||||
|
||||
{data.engine.mode === "tun" && data.engine.tun && (
|
||||
<>
|
||||
{row(
|
||||
t("settings.SystemInfo.tunDevice"),
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={spacing.sm}
|
||||
alignItems="center"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: colors.text.primary,
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
{data.engine.tun.device_name}
|
||||
</Typography>
|
||||
{boolChip(
|
||||
data.engine.tun.device_up,
|
||||
"UP",
|
||||
data.engine.tun.running
|
||||
? "DOWN"
|
||||
: t("settings.SystemInfo.tunStopped"),
|
||||
)}
|
||||
</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.tunEgress"),
|
||||
data.engine.tun.out_interface || "auto",
|
||||
)}
|
||||
{data.engine.tun.out_gateway &&
|
||||
row(
|
||||
t("settings.SystemInfo.tunGateway"),
|
||||
data.engine.tun.out_gateway,
|
||||
)}
|
||||
{data.engine.tun.resolved_src &&
|
||||
row(
|
||||
t("settings.SystemInfo.tunSource"),
|
||||
data.engine.tun.resolved_src,
|
||||
)}
|
||||
{data.engine.tun.capture &&
|
||||
row(
|
||||
t("settings.SystemInfo.tunCapture"),
|
||||
data.engine.tun.capture,
|
||||
)}
|
||||
{!!data.engine.tun.route_table &&
|
||||
row(
|
||||
t("settings.SystemInfo.tunRouteTable"),
|
||||
data.engine.tun.route_table,
|
||||
)}
|
||||
{data.engine.tun.running && (
|
||||
<>
|
||||
{row(
|
||||
t("settings.SystemInfo.tunForwarded"),
|
||||
data.engine.tun.packets_forwarded.toLocaleString(),
|
||||
)}
|
||||
{row(
|
||||
t("settings.SystemInfo.tunForwardErrors"),
|
||||
boolChip(
|
||||
data.engine.tun.forward_errors === 0,
|
||||
"0",
|
||||
String(data.engine.tun.forward_errors),
|
||||
),
|
||||
)}
|
||||
{data.engine.tun.ipv6_dropped > 0 &&
|
||||
row(
|
||||
t("settings.SystemInfo.tunIpv6Dropped"),
|
||||
data.engine.tun.ipv6_dropped.toLocaleString(),
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{sectionTitle(t("settings.SystemInfo.firewall"))}
|
||||
{row(t("settings.SystemInfo.fwBackend"), data.firewall.backend)}
|
||||
{row("NFQUEUE", boolChip(data.firewall.nfqueue_works, "OK", "FAIL"))}
|
||||
{data.engine.mode !== "tun" &&
|
||||
row("NFQUEUE", boolChip(data.firewall.nfqueue_works, "OK", "FAIL"))}
|
||||
{row(
|
||||
t("settings.SystemInfo.flowOffload"),
|
||||
boolChip(
|
||||
|
|
@ -394,8 +524,8 @@ export const SystemInfoDialog = ({ open, onClose }: SystemInfoDialogProps) => {
|
|||
: t("settings.SystemInfo.flowOffloadSw"),
|
||||
),
|
||||
)}
|
||||
{data.firewall.active_rules &&
|
||||
data.firewall.active_rules.length > 0 && (
|
||||
{data.firewall.rule_groups &&
|
||||
data.firewall.rule_groups.length > 0 && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
|
|
@ -403,24 +533,42 @@ export const SystemInfoDialog = ({ open, onClose }: SystemInfoDialogProps) => {
|
|||
py: 0.5,
|
||||
bgcolor: `${colors.background.dark}88`,
|
||||
borderRadius: 1,
|
||||
maxHeight: 150,
|
||||
maxHeight: 240,
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{data.firewall.active_rules.map((rule) => (
|
||||
<Typography
|
||||
key={rule}
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: colors.text.secondary,
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.65rem",
|
||||
display: "block",
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
{rule}
|
||||
</Typography>
|
||||
{data.firewall.rule_groups.map((group) => (
|
||||
<Box key={group.title} sx={{ mb: 0.5 }}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: colors.secondary,
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.65rem",
|
||||
fontWeight: 600,
|
||||
display: "block",
|
||||
lineHeight: 1.8,
|
||||
}}
|
||||
>
|
||||
{group.title}
|
||||
</Typography>
|
||||
{group.rules.map((rule, i) => (
|
||||
<Typography
|
||||
key={`${group.title}-${i}`}
|
||||
variant="caption"
|
||||
sx={{
|
||||
color: colors.text.secondary,
|
||||
fontFamily: "monospace",
|
||||
fontSize: "0.65rem",
|
||||
display: "block",
|
||||
lineHeight: 1.5,
|
||||
whiteSpace: "pre",
|
||||
}}
|
||||
>
|
||||
{rule}
|
||||
</Typography>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -604,6 +604,21 @@
|
|||
"flowOffloadSw": "software (bypasses b4)",
|
||||
"flowOffloadHw": "hardware (bypasses b4)",
|
||||
"network": "Network",
|
||||
"engine": "Engine",
|
||||
"engineMode": "Mode",
|
||||
"engineTun": "TUN",
|
||||
"engineNfqueue": "NFQUEUE",
|
||||
"tunDevice": "TUN Device",
|
||||
"tunStopped": "stopped",
|
||||
"tunAddress": "TUN Address",
|
||||
"tunEgress": "Egress Interface",
|
||||
"tunGateway": "Egress Gateway",
|
||||
"tunSource": "Source IP",
|
||||
"tunCapture": "Capture",
|
||||
"tunRouteTable": "Route Table",
|
||||
"tunForwarded": "Packets Forwarded",
|
||||
"tunForwardErrors": "Forward Errors",
|
||||
"tunIpv6Dropped": "IPv6 Dropped",
|
||||
"kernelModules": "Kernel Modules",
|
||||
"firewallTools": "Firewall Tools (at least one needed)",
|
||||
"requiredTools": "Required Tools",
|
||||
|
|
@ -1859,4 +1874,4 @@
|
|||
"doh_url_must_be_https": "Set \"{{set}}\": DNS-over-HTTPS URL must start with https://",
|
||||
"mark_conflict": "Mark value {{mark}} conflicts with reserved bits"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -600,6 +600,21 @@
|
|||
"flowOffloadSw": "программный (в обход b4)",
|
||||
"flowOffloadHw": "аппаратный (в обход b4)",
|
||||
"network": "Сеть",
|
||||
"engine": "Движок",
|
||||
"engineMode": "Режим",
|
||||
"engineTun": "TUN",
|
||||
"engineNfqueue": "NFQUEUE",
|
||||
"tunDevice": "TUN-устройство",
|
||||
"tunStopped": "остановлен",
|
||||
"tunAddress": "TUN-адрес",
|
||||
"tunEgress": "Исходящий интерфейс",
|
||||
"tunGateway": "Исходящий шлюз",
|
||||
"tunSource": "IP источника",
|
||||
"tunCapture": "Захват",
|
||||
"tunRouteTable": "Таблица маршрутов",
|
||||
"tunForwarded": "Переслано пакетов",
|
||||
"tunForwardErrors": "Ошибки пересылки",
|
||||
"tunIpv6Dropped": "Отброшено IPv6",
|
||||
"kernelModules": "Модули ядра",
|
||||
"firewallTools": "Firewall (нужна хотя бы одна)",
|
||||
"requiredTools": "Необходимые утилиты",
|
||||
|
|
@ -1857,4 +1872,4 @@
|
|||
"doh_url_must_be_https": "Сет \"{{set}}\": URL DNS-over-HTTPS должен начинаться с https://",
|
||||
"mark_conflict": "Значение mark {{mark}} конфликтует с зарезервированными битами"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
src/main.go
29
src/main.go
|
|
@ -129,6 +129,10 @@ func runB4(cmd *cobra.Command, args []string) error {
|
|||
appCtx, appCancel := context.WithCancel(context.Background())
|
||||
defer appCancel()
|
||||
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer signal.Stop(sigChan)
|
||||
|
||||
aiManager := ai.NewManager(cfg.System.AI, cfg.ConfigPath)
|
||||
handler.SetAIManager(aiManager)
|
||||
|
||||
|
|
@ -200,6 +204,7 @@ func runB4(cmd *cobra.Command, args []string) error {
|
|||
if clearTables {
|
||||
log.Infof("Clearing iptables rules as requested (--clear-iptables)")
|
||||
clearErr := tables.ClearRules(&cfg)
|
||||
b4tun.RestoreFromState()
|
||||
tables.RoutingClearAll()
|
||||
b4tun.ClearStaleArtifacts(&cfg)
|
||||
if clearErr != nil {
|
||||
|
|
@ -234,6 +239,7 @@ func runB4(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
log.Infof("Loaded targets: %d domains, %d IPs across %d sets", totalDomains, totalIps, len(cfg.Sets))
|
||||
b4tun.RestoreFromState()
|
||||
tables.RoutingClearAll()
|
||||
|
||||
isTUN := cfg.Queue.Mode == "tun"
|
||||
|
|
@ -334,6 +340,25 @@ func runB4(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
shutdownHandled := false
|
||||
defer func() {
|
||||
if shutdownHandled {
|
||||
return
|
||||
}
|
||||
c := cfgPtr.Load()
|
||||
if tunEngine != nil {
|
||||
tunEngine.Stop()
|
||||
if !c.System.Tables.SkipSetup {
|
||||
tables.ClearMasqueradeOnly(c)
|
||||
tables.ClearMSSClampOnly(c)
|
||||
tables.RevertConntrackSysctls()
|
||||
}
|
||||
} else if !c.System.Tables.SkipSetup {
|
||||
tables.ClearRules(c)
|
||||
}
|
||||
tables.RoutingClearAll()
|
||||
}()
|
||||
|
||||
tproxyResolver.Set(pool.GetMatcher())
|
||||
|
||||
// Start internal web server if configured
|
||||
|
|
@ -342,6 +367,7 @@ func runB4(cmd *cobra.Command, args []string) error {
|
|||
metrics.RecordEvent("error", fmt.Sprintf("Failed to start web server: %v", err))
|
||||
return log.Errorf("failed to start web server: %w", err)
|
||||
}
|
||||
handler.SetTUNEngine(tunEngine)
|
||||
|
||||
// Start SOCKS5 server if configured.
|
||||
socks5Server := socks5.NewServer(&cfg)
|
||||
|
|
@ -418,8 +444,6 @@ func runB4(cmd *cobra.Command, args []string) error {
|
|||
metrics.RecordEvent("info", "B4 is fully operational")
|
||||
|
||||
// Wait for shutdown signal
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-sigChan
|
||||
|
||||
log.Infof("Received signal: %v, shutting down gracefully", sig)
|
||||
|
|
@ -435,6 +459,7 @@ func runB4(cmd *cobra.Command, args []string) error {
|
|||
tproxyMgr.Stop()
|
||||
|
||||
// Perform graceful shutdown with timeout
|
||||
shutdownHandled = true
|
||||
return gracefulShutdown(cfgPtr.Load(), pool, tunEngine, httpServer, socks5Server, mtprotoServer, metrics, discoveryRT)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -180,6 +180,14 @@ func (w *Worker) parseIPHeaders(raw []byte) (*pktInfo, bool) {
|
|||
return nil, false
|
||||
}
|
||||
|
||||
if w.srcResolver != nil && v == IPv4 && (p.proto == 6 || p.proto == 17) && len(raw) >= p.ihl+4 {
|
||||
sport := uint16(raw[p.ihl])<<8 | uint16(raw[p.ihl+1])
|
||||
dport := uint16(raw[p.ihl+2])<<8 | uint16(raw[p.ihl+3])
|
||||
if lan, ok := w.srcResolver.resolve(p.proto, p.src, sport, p.dst, dport); ok {
|
||||
p.src = lan
|
||||
}
|
||||
}
|
||||
|
||||
p.srcStr = p.src.String()
|
||||
p.dstStr = p.dst.String()
|
||||
p.srcMac = w.getMacByIp(p.srcStr)
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/engine"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
"github.com/daniellavrushin/b4/metrics"
|
||||
"github.com/daniellavrushin/b4/quic"
|
||||
|
|
@ -66,7 +67,12 @@ func (w *Worker) Start() error {
|
|||
return err
|
||||
}
|
||||
w.sock = s
|
||||
w.clientSock = s
|
||||
cs, err := sock.NewSenderWithMark(int(mark) | engine.ClientMark)
|
||||
if err != nil {
|
||||
s.Close()
|
||||
return err
|
||||
}
|
||||
w.clientSock = cs
|
||||
|
||||
c := nfqueue.Config{
|
||||
NfQueue: w.qnum,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package nfq
|
|||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -26,6 +27,31 @@ func NewWorkerWithQueue(cfg *config.Config, qnum uint16) *Worker {
|
|||
return w
|
||||
}
|
||||
|
||||
func (p *Pool) EnableTUNSourceResolver(wanIP string) {
|
||||
if p.tunSrc == nil {
|
||||
if f, err := os.Open(conntrackPath); err != nil {
|
||||
log.Warnf("TUN: per-device source attribution unavailable (%s not readable: %v); device logging/filtering will show the uplink address in TUN mode", conntrackPath, err)
|
||||
return
|
||||
} else {
|
||||
f.Close()
|
||||
}
|
||||
p.tunSrc = newTunSrcResolver(wanIP)
|
||||
} else {
|
||||
p.tunSrc.setWAN(wanIP)
|
||||
}
|
||||
for _, w := range p.Workers {
|
||||
w.srcResolver = p.tunSrc
|
||||
}
|
||||
log.Infof("TUN: source attribution enabled (recovering LAN source from conntrack; uplink %s)", wanIP)
|
||||
}
|
||||
|
||||
func (p *Pool) UpdateTUNSourceWAN(wanIP string) {
|
||||
if p.tunSrc == nil || wanIP == "" {
|
||||
return
|
||||
}
|
||||
p.tunSrc.setWAN(wanIP)
|
||||
}
|
||||
|
||||
func NewPool(cfg *config.Config) *Pool {
|
||||
threads := cfg.Queue.Threads
|
||||
start := uint16(cfg.Queue.StartNum)
|
||||
|
|
|
|||
190
src/nfq/tun_srcmap.go
Normal file
190
src/nfq/tun_srcmap.go
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
package nfq
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
conntrackPath = "/proc/net/nf_conntrack"
|
||||
tunSrcCacheTTL = 5 * time.Minute
|
||||
tunSrcNegativeTTL = 2 * time.Second
|
||||
)
|
||||
|
||||
type tunSrcEntry struct {
|
||||
ip string
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
type tunSrcResolver struct {
|
||||
wanIP atomic.Value
|
||||
path string
|
||||
|
||||
mu sync.Mutex
|
||||
cache map[string]tunSrcEntry
|
||||
}
|
||||
|
||||
func newTunSrcResolver(wanIP string) *tunSrcResolver {
|
||||
r := &tunSrcResolver{
|
||||
path: conntrackPath,
|
||||
cache: make(map[string]tunSrcEntry),
|
||||
}
|
||||
r.wanIP.Store(wanIP)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *tunSrcResolver) setWAN(wanIP string) {
|
||||
if cur, _ := r.wanIP.Load().(string); cur == wanIP {
|
||||
return
|
||||
}
|
||||
r.wanIP.Store(wanIP)
|
||||
r.mu.Lock()
|
||||
r.cache = make(map[string]tunSrcEntry)
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
func cacheKey(proto uint8, sport, dport uint16, dst string) string {
|
||||
var b strings.Builder
|
||||
b.WriteByte(proto)
|
||||
b.WriteString(strconv.Itoa(int(sport)))
|
||||
b.WriteByte(':')
|
||||
b.WriteString(strconv.Itoa(int(dport)))
|
||||
b.WriteByte('@')
|
||||
b.WriteString(dst)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func (r *tunSrcResolver) resolve(proto uint8, src net.IP, sport uint16, dst net.IP, dport uint16) (net.IP, bool) {
|
||||
wan, _ := r.wanIP.Load().(string)
|
||||
if wan == "" {
|
||||
return nil, false
|
||||
}
|
||||
srcStr := src.String()
|
||||
if srcStr != wan {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
key := cacheKey(proto, sport, dport, dst.String())
|
||||
now := time.Now()
|
||||
|
||||
r.mu.Lock()
|
||||
if e, ok := r.cache[key]; ok && now.Before(e.expires) {
|
||||
r.mu.Unlock()
|
||||
if e.ip == "" {
|
||||
return nil, false
|
||||
}
|
||||
if ip := net.ParseIP(e.ip); ip != nil {
|
||||
return ip, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
lan := r.lookupConntrack(proto, wan, sport, dst.String(), dport)
|
||||
|
||||
r.mu.Lock()
|
||||
if lan == "" {
|
||||
r.cache[key] = tunSrcEntry{expires: now.Add(tunSrcNegativeTTL)}
|
||||
} else {
|
||||
r.cache[key] = tunSrcEntry{ip: lan, expires: now.Add(tunSrcCacheTTL)}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
|
||||
if lan == "" {
|
||||
return nil, false
|
||||
}
|
||||
if ip := net.ParseIP(lan); ip != nil {
|
||||
return ip, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func protoName(proto uint8) string {
|
||||
switch proto {
|
||||
case 6:
|
||||
return "tcp"
|
||||
case 17:
|
||||
return "udp"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (r *tunSrcResolver) lookupConntrack(proto uint8, wan string, sport uint16, dst string, dport uint16) string {
|
||||
pname := protoName(proto)
|
||||
if pname == "" {
|
||||
return ""
|
||||
}
|
||||
f, err := os.Open(r.path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
wantSport := strconv.Itoa(int(sport))
|
||||
wantDport := strconv.Itoa(int(dport))
|
||||
|
||||
sc := bufio.NewScanner(f)
|
||||
sc.Buffer(make([]byte, 64*1024), 1024*1024)
|
||||
for sc.Scan() {
|
||||
line := sc.Text()
|
||||
if !strings.HasPrefix(line, pname+" ") && !strings.Contains(line, " "+pname+" ") {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(line, "dst="+wan) {
|
||||
continue
|
||||
}
|
||||
if lan := matchConntrackLine(line, wan, wantSport, dst, wantDport); lan != "" {
|
||||
return lan
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func matchConntrackLine(line, wan, sport, dst, dport string) string {
|
||||
var origSrc, origDst, replyDst string
|
||||
var origDport, replyDport string
|
||||
dstN, dportN := 0, 0
|
||||
|
||||
for _, tok := range strings.Fields(line) {
|
||||
switch {
|
||||
case strings.HasPrefix(tok, "src="):
|
||||
if origSrc == "" {
|
||||
origSrc = tok[4:]
|
||||
}
|
||||
case strings.HasPrefix(tok, "dst="):
|
||||
v := tok[4:]
|
||||
if dstN == 0 {
|
||||
origDst = v
|
||||
} else if dstN == 1 {
|
||||
replyDst = v
|
||||
}
|
||||
dstN++
|
||||
case strings.HasPrefix(tok, "dport="):
|
||||
v := tok[6:]
|
||||
if dportN == 0 {
|
||||
origDport = v
|
||||
} else if dportN == 1 {
|
||||
replyDport = v
|
||||
}
|
||||
dportN++
|
||||
}
|
||||
}
|
||||
|
||||
if origDst != dst || origDport != dport {
|
||||
return ""
|
||||
}
|
||||
if replyDst != wan || replyDport != sport {
|
||||
return ""
|
||||
}
|
||||
if origSrc == "" || origSrc == wan {
|
||||
return ""
|
||||
}
|
||||
return origSrc
|
||||
}
|
||||
79
src/nfq/tun_srcmap_test.go
Normal file
79
src/nfq/tun_srcmap_test.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
package nfq
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func writeConntrack(t *testing.T, content string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
p := filepath.Join(dir, "nf_conntrack")
|
||||
if err := os.WriteFile(p, []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func newResolverWith(t *testing.T, wan, content string) *tunSrcResolver {
|
||||
r := newTunSrcResolver(wan)
|
||||
r.path = writeConntrack(t, content)
|
||||
return r
|
||||
}
|
||||
|
||||
func TestTunSrcResolveBasic(t *testing.T) {
|
||||
line := "ipv4 2 tcp 6 431999 ESTABLISHED src=192.168.1.100 dst=104.18.8.37 sport=49874 dport=443 src=104.18.8.37 dst=94.189.76.227 sport=443 dport=49874 [ASSURED] mark=0 use=1\n"
|
||||
r := newResolverWith(t, "94.189.76.227", line)
|
||||
|
||||
got, ok := r.resolve(6, net.ParseIP("94.189.76.227"), 49874, net.ParseIP("104.18.8.37"), 443)
|
||||
if !ok || got.String() != "192.168.1.100" {
|
||||
t.Fatalf("want 192.168.1.100, got %v ok=%v", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunSrcResolvePortRemap(t *testing.T) {
|
||||
// SNAT remapped the source port 49874 -> 44092; b4 sees sport=44092.
|
||||
line := "ipv4 2 tcp 6 431999 ESTABLISHED src=192.168.1.50 dst=104.18.8.37 sport=49874 dport=443 src=104.18.8.37 dst=94.189.76.227 sport=443 dport=44092 [ASSURED] use=1\n"
|
||||
r := newResolverWith(t, "94.189.76.227", line)
|
||||
|
||||
got, ok := r.resolve(6, net.ParseIP("94.189.76.227"), 44092, net.ParseIP("104.18.8.37"), 443)
|
||||
if !ok || got.String() != "192.168.1.50" {
|
||||
t.Fatalf("want 192.168.1.50, got %v ok=%v", got, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunSrcResolveGateAndMiss(t *testing.T) {
|
||||
line := "ipv4 2 tcp 6 100 ESTABLISHED src=192.168.1.100 dst=104.18.8.37 sport=49874 dport=443 src=104.18.8.37 dst=94.189.76.227 sport=443 dport=49874 use=1\n"
|
||||
r := newResolverWith(t, "94.189.76.227", line)
|
||||
|
||||
if _, ok := r.resolve(6, net.ParseIP("192.168.1.5"), 49874, net.ParseIP("104.18.8.37"), 443); ok {
|
||||
t.Fatal("non-WAN source must not be resolved")
|
||||
}
|
||||
if _, ok := r.resolve(6, net.ParseIP("94.189.76.227"), 49874, net.ParseIP("8.8.8.8"), 443); ok {
|
||||
t.Fatal("unknown flow must not resolve")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunSrcResolveNoWAN(t *testing.T) {
|
||||
r := newTunSrcResolver("")
|
||||
if _, ok := r.resolve(6, net.ParseIP("94.189.76.227"), 49874, net.ParseIP("104.18.8.37"), 443); ok {
|
||||
t.Fatal("empty WAN must disable resolution")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunSrcSetWANClearsCache(t *testing.T) {
|
||||
line := "tcp 6 100 ESTABLISHED src=192.168.1.100 dst=104.18.8.37 sport=49874 dport=443 src=104.18.8.37 dst=94.189.76.227 sport=443 dport=49874 use=1\n"
|
||||
r := newResolverWith(t, "94.189.76.227", line)
|
||||
if _, ok := r.resolve(6, net.ParseIP("94.189.76.227"), 49874, net.ParseIP("104.18.8.37"), 443); !ok {
|
||||
t.Fatal("expected resolve before WAN change")
|
||||
}
|
||||
r.setWAN("203.0.113.9")
|
||||
r.mu.Lock()
|
||||
n := len(r.cache)
|
||||
r.mu.Unlock()
|
||||
if n != 0 {
|
||||
t.Fatalf("setWAN should clear cache, have %d entries", n)
|
||||
}
|
||||
}
|
||||
|
|
@ -21,6 +21,7 @@ type Pool struct {
|
|||
Dhcp *dhcp.Manager
|
||||
stopCleanup chan struct{}
|
||||
state *runtimeState
|
||||
tunSrc *tunSrcResolver
|
||||
}
|
||||
|
||||
type PacketInfo struct {
|
||||
|
|
@ -50,4 +51,5 @@ type Worker struct {
|
|||
tlsCache *tlsInfoCache
|
||||
connTracker *connStateTracker
|
||||
destState *destStateTracker
|
||||
srcResolver *tunSrcResolver
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func (w *Worker) InitSender() error {
|
|||
}
|
||||
w.sock = s
|
||||
if tun {
|
||||
cs, err := sock.NewSenderWithMark(engine.TunClientMark)
|
||||
cs, err := sock.NewSenderWithMark(engine.ClientMark)
|
||||
if err != nil {
|
||||
w.sock.Close()
|
||||
w.sock = nil
|
||||
|
|
@ -58,7 +58,13 @@ func (w *Worker) InitSender() error {
|
|||
}
|
||||
w.clientSock = cs
|
||||
} else {
|
||||
w.clientSock = s
|
||||
cs, err := sock.NewSenderWithMark(reinjectMark | engine.ClientMark)
|
||||
if err != nil {
|
||||
w.sock.Close()
|
||||
w.sock = nil
|
||||
return err
|
||||
}
|
||||
w.clientSock = cs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/engine"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
)
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ func (im *IPTablesManager) ApplyMasquerade() error {
|
|||
masqSpec = []string{"-o", iface, "-j", "MASQUERADE"}
|
||||
}
|
||||
|
||||
returnSpec := []string{"-m", "mark", "--mark", im.masqMarkAccept(), "-j", "RETURN"}
|
||||
returnSpec := []string{"-m", "mark", "--mark", im.masqClientMark(), "-j", "RETURN"}
|
||||
checkRet := append([]string{iptBin, "-w", "-t", "nat", "-C", "POSTROUTING"}, returnSpec...)
|
||||
if _, err := run(checkRet...); err != nil {
|
||||
insRet := append([]string{iptBin, "-w", "-t", "nat", "-I", "POSTROUTING"}, returnSpec...)
|
||||
|
|
@ -77,10 +78,12 @@ func (im *IPTablesManager) ClearMasquerade() {
|
|||
}
|
||||
}
|
||||
|
||||
retArgs := []string{iptBin, "-w", "-t", "nat", "-D", "POSTROUTING", "-m", "mark", "--mark", im.masqMarkAccept(), "-j", "RETURN"}
|
||||
for {
|
||||
if _, err := run(retArgs...); err != nil {
|
||||
break
|
||||
for _, mk := range []string{im.masqClientMark(), im.masqMarkAccept()} {
|
||||
retArgs := []string{iptBin, "-w", "-t", "nat", "-D", "POSTROUTING", "-m", "mark", "--mark", mk, "-j", "RETURN"}
|
||||
for {
|
||||
if _, err := run(retArgs...); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -92,6 +95,10 @@ func (im *IPTablesManager) masqMarkAccept() string {
|
|||
return fmt.Sprintf("0x%x/0x%x", im.cfg.Queue.Mark, im.cfg.Queue.Mark)
|
||||
}
|
||||
|
||||
func (im *IPTablesManager) masqClientMark() string {
|
||||
return fmt.Sprintf("0x%x/0x%x", engine.ClientMark, engine.ClientMark)
|
||||
}
|
||||
|
||||
func (im *IPTablesManager) iptablesBin() string {
|
||||
if im.useLegacy {
|
||||
return backendIPTablesLegacy
|
||||
|
|
@ -406,6 +413,7 @@ func (manager *IPTablesManager) buildManifest() (Manifest, error) {
|
|||
if cfg.Queue.Mark == 0 {
|
||||
markAccept = "0x8000/0x8000"
|
||||
}
|
||||
markClient := fmt.Sprintf("0x%x/0x%x", engine.ClientMark, engine.ClientMark)
|
||||
|
||||
var ipsets []IPSet
|
||||
var chains []Chain
|
||||
|
|
@ -651,7 +659,7 @@ func (manager *IPTablesManager) buildManifest() (Manifest, error) {
|
|||
masqSpec = []string{"-o", iface, "-j", "MASQUERADE"}
|
||||
}
|
||||
rules = append(rules,
|
||||
Rule{manager: manager, IPT: ipt, Table: "nat", Chain: "POSTROUTING", Action: "I", Spec: []string{"-m", "mark", "--mark", markAccept, "-j", "RETURN"}},
|
||||
Rule{manager: manager, IPT: ipt, Table: "nat", Chain: "POSTROUTING", Action: "I", Spec: []string{"-m", "mark", "--mark", markClient, "-j", "RETURN"}},
|
||||
Rule{manager: manager, IPT: ipt, Table: "nat", Chain: "POSTROUTING", Action: "A", Spec: masqSpec},
|
||||
)
|
||||
}
|
||||
|
|
@ -971,10 +979,12 @@ func (ipt *IPTablesManager) clearB4JumpRules() {
|
|||
}
|
||||
}
|
||||
}
|
||||
for {
|
||||
_, err := run(iptBin, "-w", "-t", "nat", "-D", "POSTROUTING", "-m", "mark", "--mark", ipt.masqMarkAccept(), "-j", "RETURN")
|
||||
if err != nil {
|
||||
break
|
||||
for _, mk := range []string{ipt.masqClientMark(), ipt.masqMarkAccept()} {
|
||||
for {
|
||||
_, err := run(iptBin, "-w", "-t", "nat", "-D", "POSTROUTING", "-m", "mark", "--mark", mk, "-j", "RETURN")
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/daniellavrushin/b4/config"
|
||||
"github.com/daniellavrushin/b4/engine"
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
)
|
||||
|
||||
|
|
@ -399,13 +400,9 @@ func (n *NFTablesManager) ApplyMasquerade() error {
|
|||
return fmt.Errorf("failed to create nat postrouting chain: %w", err)
|
||||
}
|
||||
|
||||
markValue := n.cfg.Queue.Mark
|
||||
if markValue == 0 {
|
||||
markValue = 0x8000
|
||||
}
|
||||
markAccept := fmt.Sprintf("0x%x", markValue)
|
||||
markClient := fmt.Sprintf("0x%x", engine.ClientMark)
|
||||
if _, err := n.runNft("add", "rule", "ip", nftNatTableName, nftNatChainName,
|
||||
"meta", "mark", "&", markAccept, "==", markAccept, "return"); err != nil {
|
||||
"meta", "mark", "&", markClient, "==", markClient, "return"); err != nil {
|
||||
return fmt.Errorf("failed to add masquerade mark-bypass rule: %w", err)
|
||||
}
|
||||
|
||||
|
|
@ -532,7 +529,7 @@ func (n *NFTablesManager) ApplyMSSClamp() error {
|
|||
hasV6 := len(e.IPv6) > 0 && cfg.Queue.IPv6Enabled
|
||||
|
||||
if hasV4 {
|
||||
if err := n.createSet(setName4, "ipv4_addr", "flags interval ;"); err != nil {
|
||||
if err := n.createSet(setName4, "ipv4_addr", "flags interval ; auto-merge ;"); err != nil {
|
||||
return fmt.Errorf("failed to create set MSS ipv4 set: %w", err)
|
||||
}
|
||||
if err := n.addSetElements(setName4, e.IPv4); err != nil {
|
||||
|
|
@ -540,7 +537,7 @@ func (n *NFTablesManager) ApplyMSSClamp() error {
|
|||
}
|
||||
}
|
||||
if hasV6 {
|
||||
if err := n.createSet(setName6, "ipv6_addr", "flags interval ;"); err != nil {
|
||||
if err := n.createSet(setName6, "ipv6_addr", "flags interval ; auto-merge ;"); err != nil {
|
||||
return fmt.Errorf("failed to create set MSS ipv6 set: %w", err)
|
||||
}
|
||||
if err := n.addSetElements(setName6, e.IPv6); err != nil {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ const (
|
|||
tunCaptureChain = "B4_TUN"
|
||||
tunProbeChain = "B4_TUN_PROBE"
|
||||
defaultSteerMark = engine.TunSteerMark
|
||||
defaultClientMark = engine.TunClientMark
|
||||
defaultClientMark = engine.ClientMark
|
||||
captureRulePrio = 90
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -40,8 +40,8 @@ func TestEqualStringSet(t *testing.T) {
|
|||
|
||||
func TestSteerMarkDefault(t *testing.T) {
|
||||
r := &routeManager{}
|
||||
if r.steerMarkStr() != "0x80000/0x80000" {
|
||||
t.Errorf("steerMarkStr = %q, want 0x80000/0x80000", r.steerMarkStr())
|
||||
if r.steerMarkStr() != "0x40000000/0x40000000" {
|
||||
t.Errorf("steerMarkStr = %q, want 0x40000000/0x40000000", r.steerMarkStr())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,11 +69,11 @@ func TestSteerSpecsMultiport(t *testing.T) {
|
|||
if !strings.Contains(all, "-p udp -m multiport --dports 443") || !strings.Contains(all, "--connbytes 0:8") {
|
||||
t.Errorf("missing udp multiport/connbytes:\n%s", all)
|
||||
}
|
||||
if !strings.Contains(all, "-p udp --dport 53 -j MARK --set-xmark 0x80000/0x80000") {
|
||||
if !strings.Contains(all, "-p udp --dport 53 -j MARK --set-xmark 0x40000000/0x40000000") {
|
||||
t.Errorf("missing DNS steer rule:\n%s", all)
|
||||
}
|
||||
for _, s := range joined {
|
||||
if !strings.HasSuffix(s, "-j MARK --set-xmark 0x80000/0x80000") {
|
||||
if !strings.HasSuffix(s, "-j MARK --set-xmark 0x40000000/0x40000000") {
|
||||
t.Errorf("steer spec does not end in MARK: %q", s)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package tun
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
|
@ -56,13 +55,10 @@ func ClearStaleArtifacts(cfg *config.Config) {
|
|||
cleared = true
|
||||
}
|
||||
|
||||
if clearOwnedRoutingTable(steerMarkMatch(), captureTable) {
|
||||
if clearOwnedRoutingTable(captureTable) {
|
||||
cleared = true
|
||||
}
|
||||
if clearOwnedRoutingTable(reinjectMarkMatch(), routeTable) {
|
||||
cleared = true
|
||||
}
|
||||
if clearOwnedRoutingTable(fmt.Sprintf("0x%x", cfg.MainInjectedMark()), routeTable) {
|
||||
if clearOwnedRoutingTable(routeTable) {
|
||||
cleared = true
|
||||
}
|
||||
|
||||
|
|
@ -76,10 +72,6 @@ func ClearStaleArtifacts(cfg *config.Config) {
|
|||
}
|
||||
}
|
||||
|
||||
func steerMarkMatch() string {
|
||||
return fmt.Sprintf("0x%x/0x%x", defaultSteerMark, defaultSteerMark)
|
||||
}
|
||||
|
||||
func clearTunSNAT(device string) bool {
|
||||
out, err := run("iptables", "-t", "nat", "-S", "POSTROUTING")
|
||||
if err != nil {
|
||||
|
|
@ -102,23 +94,18 @@ func clearTunSNAT(device string) bool {
|
|||
return cleared
|
||||
}
|
||||
|
||||
func clearOwnedRoutingTable(fwmark string, table int) bool {
|
||||
func clearOwnedRoutingTable(table int) bool {
|
||||
tableStr := strconv.Itoa(table)
|
||||
out, err := run("ip", "rule", "show")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
bare := strings.SplitN(fwmark, "/", 2)[0]
|
||||
marks := make(map[string]struct{})
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
if ruleFieldValue(line, "lookup") != tableStr {
|
||||
continue
|
||||
}
|
||||
fw := ruleFieldValue(line, "fwmark")
|
||||
if fw == "" {
|
||||
continue
|
||||
}
|
||||
if fw == fwmark || fw == bare || strings.HasPrefix(fw, bare+"/") {
|
||||
if fw := ruleFieldValue(line, "fwmark"); fw != "" {
|
||||
marks[fw] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
46
src/tun/diag.go
Normal file
46
src/tun/diag.go
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package tun
|
||||
|
||||
import "sync/atomic"
|
||||
|
||||
type DiagInfo struct {
|
||||
DeviceName string
|
||||
Address string
|
||||
AddressV6 string
|
||||
OutInterface string
|
||||
OutGateway string
|
||||
ResolvedSrc string
|
||||
Capture string
|
||||
RouteTable int
|
||||
Mark uint
|
||||
ReplyCapture bool
|
||||
SkipTables bool
|
||||
PacketsForwarded uint64
|
||||
ForwardErrors uint64
|
||||
IPv6Dropped uint64
|
||||
}
|
||||
|
||||
func (e *Engine) DiagInfo() DiagInfo {
|
||||
di := DiagInfo{
|
||||
DeviceName: e.tunName,
|
||||
PacketsForwarded: atomic.LoadUint64(&e.fwdCount),
|
||||
ForwardErrors: atomic.LoadUint64(&e.fwdErrCount),
|
||||
IPv6Dropped: atomic.LoadUint64(&e.v6DropCount),
|
||||
}
|
||||
|
||||
if r := e.routes; r != nil {
|
||||
r.mu.Lock()
|
||||
di.Address = r.tunAddr
|
||||
di.AddressV6 = r.tunAddrV6
|
||||
di.OutInterface = r.outIface
|
||||
di.OutGateway = r.outGateway
|
||||
di.ResolvedSrc = r.srcIP
|
||||
di.Capture = r.resolvedCapture
|
||||
di.RouteTable = r.routeTable
|
||||
di.Mark = r.mark
|
||||
di.ReplyCapture = r.replyCapture
|
||||
di.SkipTables = r.skipTables
|
||||
r.mu.Unlock()
|
||||
}
|
||||
|
||||
return di
|
||||
}
|
||||
|
|
@ -17,6 +17,10 @@ func reinjectMarkMatch() string {
|
|||
return fmt.Sprintf("0x%x/0x%x", engine.ReinjectMarkBit, engine.ReinjectMarkBit)
|
||||
}
|
||||
|
||||
func clientMarkMatch() string {
|
||||
return fmt.Sprintf("0x%x/0x%x", engine.ClientMark, engine.ClientMark)
|
||||
}
|
||||
|
||||
func interfaceMTU(iface string) int {
|
||||
b, err := os.ReadFile("/sys/class/net/" + iface + "/mtu")
|
||||
if err != nil {
|
||||
|
|
@ -40,9 +44,10 @@ type routeManager struct {
|
|||
skipTables bool
|
||||
savedDefault string
|
||||
savedRPFilter string
|
||||
fwdRulesAdded bool
|
||||
snatAdded bool
|
||||
notrackAdded bool
|
||||
fwdRulesAdded bool
|
||||
snatAdded bool
|
||||
notrackAdded bool
|
||||
clientNotrackAdded bool
|
||||
|
||||
captureTable int
|
||||
tcpPorts []string
|
||||
|
|
@ -85,6 +90,12 @@ func resolveDefaultEgress(skipDev string) (iface, gw, src string, ok bool) {
|
|||
return "", "", "", false
|
||||
}
|
||||
|
||||
func (r *routeManager) currentSrcIP() string {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
return r.srcIP
|
||||
}
|
||||
|
||||
func (r *routeManager) setupNAT() {
|
||||
markStr := reinjectMarkMatch()
|
||||
|
||||
|
|
@ -104,16 +115,28 @@ func (r *routeManager) setupNAT() {
|
|||
log.Warnf("TUN: no source IP derived for %s; forwarded LAN traffic will not be NAT'd and replies will not return", r.outIface)
|
||||
}
|
||||
|
||||
r.notrackAdded = r.installNotrack(markStr, "re-injected packets")
|
||||
r.clientNotrackAdded = r.installNotrack(clientMarkMatch(), "client-direction re-injects")
|
||||
}
|
||||
|
||||
func (r *routeManager) installNotrack(markStr, label string) bool {
|
||||
notrack := []string{"-m", "mark", "--mark", markStr, "-j", "CT", "--notrack"}
|
||||
if _, err := run(append([]string{"iptables", "-t", "raw", "-C", "OUTPUT"}, notrack...)...); err != nil {
|
||||
if _, err := run(append([]string{"iptables", "-t", "raw", "-A", "OUTPUT"}, notrack...)...); err != nil {
|
||||
log.Infof("TUN: NOTRACK not installed for mark %s (no raw table here); conntrack sysctls + SNAT cover it, so this is harmless: %v", markStr, err)
|
||||
} else {
|
||||
r.notrackAdded = true
|
||||
log.Infof("TUN: NOTRACK installed for re-injected packets (mark %s)", markStr)
|
||||
if _, err := run(append([]string{"iptables", "-t", "raw", "-C", "OUTPUT"}, notrack...)...); err == nil {
|
||||
return true
|
||||
}
|
||||
if _, err := run(append([]string{"iptables", "-t", "raw", "-A", "OUTPUT"}, notrack...)...); err != nil {
|
||||
log.Infof("TUN: NOTRACK not installed for mark %s (no raw table here); conntrack sysctls + SNAT cover it, so this is harmless: %v", markStr, err)
|
||||
return false
|
||||
}
|
||||
log.Infof("TUN: NOTRACK installed for %s (mark %s)", label, markStr)
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *routeManager) removeNotrack(markStr string) {
|
||||
for {
|
||||
if _, err := run("iptables", "-t", "raw", "-D", "OUTPUT", "-m", "mark", "--mark", markStr, "-j", "CT", "--notrack"); err != nil {
|
||||
break
|
||||
}
|
||||
} else {
|
||||
r.notrackAdded = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -132,14 +155,13 @@ func (r *routeManager) removeSNAT() {
|
|||
func (r *routeManager) teardownNAT() {
|
||||
r.removeSNAT()
|
||||
if r.notrackAdded {
|
||||
markStr := reinjectMarkMatch()
|
||||
for {
|
||||
if _, err := run("iptables", "-t", "raw", "-D", "OUTPUT", "-m", "mark", "--mark", markStr, "-j", "CT", "--notrack"); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
r.removeNotrack(reinjectMarkMatch())
|
||||
r.notrackAdded = false
|
||||
}
|
||||
if r.clientNotrackAdded {
|
||||
r.removeNotrack(clientMarkMatch())
|
||||
r.clientNotrackAdded = false
|
||||
}
|
||||
}
|
||||
|
||||
func (r *routeManager) applyForwarding() int {
|
||||
|
|
@ -255,6 +277,7 @@ func (r *routeManager) setup() error {
|
|||
}
|
||||
|
||||
r.resolvedCapture = r.resolveCaptureMode()
|
||||
r.saveState()
|
||||
var capErr error
|
||||
if r.resolvedCapture == "ports" {
|
||||
capErr = r.setupPortCapture(srcIP)
|
||||
|
|
@ -427,6 +450,7 @@ func (r *routeManager) refreshEgress() bool {
|
|||
r.savedRPFilter = ""
|
||||
r.loosenRPFilter()
|
||||
}
|
||||
r.saveState()
|
||||
|
||||
tableStr := strconv.Itoa(r.routeTable)
|
||||
run("ip", "route", "flush", "table", tableStr)
|
||||
|
|
@ -502,13 +526,18 @@ func (r *routeManager) ensureNAT() {
|
|||
r.snatAdded = true
|
||||
}
|
||||
}
|
||||
if r.notrackAdded {
|
||||
markStr := reinjectMarkMatch()
|
||||
notrack := []string{"-m", "mark", "--mark", markStr, "-j", "CT", "--notrack"}
|
||||
if _, err := run(append([]string{"iptables", "-t", "raw", "-C", "OUTPUT"}, notrack...)...); err != nil {
|
||||
if _, err := run(append([]string{"iptables", "-t", "raw", "-A", "OUTPUT"}, notrack...)...); err == nil {
|
||||
log.Infof("TUN: reconcile restored NOTRACK (mark %s)", markStr)
|
||||
}
|
||||
r.ensureNotrack(&r.notrackAdded, reinjectMarkMatch())
|
||||
r.ensureNotrack(&r.clientNotrackAdded, clientMarkMatch())
|
||||
}
|
||||
|
||||
func (r *routeManager) ensureNotrack(added *bool, markStr string) {
|
||||
if !*added {
|
||||
return
|
||||
}
|
||||
notrack := []string{"-m", "mark", "--mark", markStr, "-j", "CT", "--notrack"}
|
||||
if _, err := run(append([]string{"iptables", "-t", "raw", "-C", "OUTPUT"}, notrack...)...); err != nil {
|
||||
if _, err := run(append([]string{"iptables", "-t", "raw", "-A", "OUTPUT"}, notrack...)...); err == nil {
|
||||
log.Infof("TUN: reconcile restored NOTRACK (mark %s)", markStr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -604,6 +633,8 @@ func (r *routeManager) teardown() {
|
|||
r.restoreRPFilter()
|
||||
}
|
||||
|
||||
removeStateFile()
|
||||
|
||||
log.Infof("TUN: routing teardown complete")
|
||||
}
|
||||
|
||||
|
|
|
|||
104
src/tun/state.go
Normal file
104
src/tun/state.go
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
package tun
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/daniellavrushin/b4/log"
|
||||
)
|
||||
|
||||
var stateFileCandidates = []string{"/var/run/b4-tun.state", "/run/b4-tun.state"}
|
||||
|
||||
type persistedState struct {
|
||||
Capture string `json:"capture"`
|
||||
SavedDefault string `json:"saved_default"`
|
||||
OutIface string `json:"out_iface"`
|
||||
SavedRPFilter string `json:"saved_rp_filter"`
|
||||
TunName string `json:"tun_name"`
|
||||
RouteTable int `json:"route_table"`
|
||||
Mark uint `json:"mark"`
|
||||
SrcIP string `json:"src_ip"`
|
||||
SkipTables bool `json:"skip_tables"`
|
||||
}
|
||||
|
||||
func writeStateFile(st persistedState) {
|
||||
data, err := json.Marshal(st)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, path := range stateFileCandidates {
|
||||
if err := os.WriteFile(path, append(data, '\n'), 0600); err == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Tracef("TUN: could not persist teardown state to any of %v", stateFileCandidates)
|
||||
}
|
||||
|
||||
func readStateFile() (persistedState, string, bool) {
|
||||
for _, path := range stateFileCandidates {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var st persistedState
|
||||
if err := json.Unmarshal(data, &st); err != nil {
|
||||
log.Warnf("TUN: ignoring corrupt teardown state %s: %v", path, err)
|
||||
os.Remove(path)
|
||||
continue
|
||||
}
|
||||
return st, path, true
|
||||
}
|
||||
return persistedState{}, "", false
|
||||
}
|
||||
|
||||
func removeStateFile() {
|
||||
for _, path := range stateFileCandidates {
|
||||
os.Remove(path)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *routeManager) saveState() {
|
||||
writeStateFile(persistedState{
|
||||
Capture: r.resolvedCapture,
|
||||
SavedDefault: r.savedDefault,
|
||||
OutIface: r.outIface,
|
||||
SavedRPFilter: r.savedRPFilter,
|
||||
TunName: r.tunName,
|
||||
RouteTable: r.routeTable,
|
||||
Mark: r.mark,
|
||||
SrcIP: r.srcIP,
|
||||
SkipTables: r.skipTables,
|
||||
})
|
||||
}
|
||||
|
||||
func RestoreFromState() bool {
|
||||
st, path, ok := readStateFile()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
restored := false
|
||||
if st.Capture == "default" && st.SavedDefault != "" {
|
||||
cur, _ := run("ip", "-4", "route", "show", "default")
|
||||
hijacked := st.TunName != "" && strings.Contains(cur, "dev "+st.TunName)
|
||||
if strings.TrimSpace(cur) == "" || hijacked {
|
||||
args := append([]string{"ip", "route", "replace"}, strings.Fields(st.SavedDefault)...)
|
||||
if _, err := run(args...); err != nil {
|
||||
log.Errorf("TUN: failed to restore default route from stale state: %v", err)
|
||||
} else {
|
||||
log.Infof("TUN: restored default route left by a previous run: %s", st.SavedDefault)
|
||||
restored = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if st.SavedRPFilter != "" && st.OutIface != "" {
|
||||
if err := os.WriteFile(rpFilterPath(st.OutIface), []byte(st.SavedRPFilter+"\n"), 0644); err != nil {
|
||||
log.Tracef("TUN: could not restore rp_filter for %s from stale state: %v", st.OutIface, err)
|
||||
}
|
||||
}
|
||||
|
||||
os.Remove(path)
|
||||
return restored
|
||||
}
|
||||
|
|
@ -160,6 +160,10 @@ func (e *Engine) Start() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if !cfg.System.Tables.SkipSetup {
|
||||
e.pool.EnableTUNSourceResolver(e.routes.currentSrcIP())
|
||||
}
|
||||
|
||||
threads := cfg.Queue.Threads
|
||||
if threads < 1 {
|
||||
threads = 1
|
||||
|
|
@ -201,10 +205,12 @@ func (e *Engine) reconcileLoop() {
|
|||
case <-e.trigger:
|
||||
if e.routes != nil {
|
||||
e.routes.reconcile()
|
||||
e.pool.UpdateTUNSourceWAN(e.routes.currentSrcIP())
|
||||
}
|
||||
case <-ticker.C:
|
||||
if e.routes != nil {
|
||||
e.routes.reconcile()
|
||||
e.pool.UpdateTUNSourceWAN(e.routes.currentSrcIP())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue