Skip to content

Commit c8ad0a8

Browse files
refactor: streamline DNS probing logic and enhance IP connectivity ch… (#251)
1 parent 1269652 commit c8ad0a8

48 files changed

Lines changed: 1477 additions & 774 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/docs.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@ jobs:
1212
build-and-deploy:
1313
runs-on: ubuntu-latest
1414
steps:
15-
- uses: actions/checkout@v4
15+
- uses: actions/checkout@v6
1616
with:
1717
fetch-depth: 0
1818

1919
- name: Setup PNPM
20-
uses: pnpm/action-setup@v4
20+
uses: pnpm/action-setup@v6
2121
with:
2222
version: 10.18.2
2323
run_install: false
2424

25-
- uses: actions/setup-node@v4
25+
- uses: actions/setup-node@v6
2626
with:
2727
node-version: 22
2828
cache: "pnpm"

.github/workflows/release.yml

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,27 @@ jobs:
2828
runs-on: ubuntu-latest
2929
steps:
3030
- name: Checkout code
31-
uses: actions/checkout@v4
31+
uses: actions/checkout@v6
3232

3333
- name: Setup PNPM
34-
uses: pnpm/action-setup@v4
34+
uses: pnpm/action-setup@v6
3535
with:
3636
version: 10.18.2
3737
run_install: false
3838

3939
- name: Setup Node.js
40-
uses: actions/setup-node@v4
40+
uses: actions/setup-node@v6
4141
with:
4242
node-version: 22
4343
cache: "pnpm"
4444
cache-dependency-path: |
4545
./src/http/ui/pnpm-lock.yaml
4646
4747
- name: Setup Go
48-
uses: actions/setup-go@v5
48+
uses: actions/setup-go@v6
4949
with:
5050
go-version-file: src/go.mod
51+
cache-dependency-path: src/go.sum
5152

5253
- name: Generate UI defaults
5354
working-directory: src
@@ -60,7 +61,7 @@ jobs:
6061
VITE_APP_VERSION="$VERSION" pnpm build
6162
6263
- name: Upload UI artifacts
63-
uses: actions/upload-artifact@v4
64+
uses: actions/upload-artifact@v7
6465
with:
6566
name: ui-build
6667
path: src/http/ui/dist
@@ -72,15 +73,16 @@ jobs:
7273
runs-on: ubuntu-latest
7374
steps:
7475
- name: Checkout code
75-
uses: actions/checkout@v4
76+
uses: actions/checkout@v6
7677

7778
- name: Setup Go
78-
uses: actions/setup-go@v5
79+
uses: actions/setup-go@v6
7980
with:
8081
go-version-file: src/go.mod
82+
cache-dependency-path: src/go.sum
8183

8284
- name: Download UI artifacts
83-
uses: actions/download-artifact@v4
85+
uses: actions/download-artifact@v8
8486
with:
8587
name: ui-build
8688
path: src/http/ui/dist
@@ -119,15 +121,16 @@ jobs:
119121

120122
steps:
121123
- name: Checkout code
122-
uses: actions/checkout@v4
124+
uses: actions/checkout@v6
123125

124126
- name: Setup Go
125-
uses: actions/setup-go@v5
127+
uses: actions/setup-go@v6
126128
with:
127129
go-version-file: src/go.mod
130+
cache-dependency-path: src/go.sum
128131

129132
- name: Download UI artifacts
130-
uses: actions/download-artifact@v4
133+
uses: actions/download-artifact@v8
131134
with:
132135
name: ui-build
133136
path: src/http/ui/dist
@@ -139,7 +142,7 @@ jobs:
139142
make linux-${ARCH} VERSION="$VERSION"
140143
141144
- name: Upload artifacts
142-
uses: actions/upload-artifact@v4
145+
uses: actions/upload-artifact@v7
143146
with:
144147
name: ${{ env.BINARY_NAME }}-linux-${{ matrix.arch }}
145148
path: out/assets/*
@@ -153,10 +156,10 @@ jobs:
153156
contents: write
154157
steps:
155158
- name: Checkout code
156-
uses: actions/checkout@v4
159+
uses: actions/checkout@v6
157160

158161
- name: Download all artifacts
159-
uses: actions/download-artifact@v4
162+
uses: actions/download-artifact@v8
160163
with:
161164
pattern: ${{ env.BINARY_NAME }}-*
162165
path: release-assets
@@ -187,7 +190,7 @@ jobs:
187190
echo "EOF" >> $GITHUB_OUTPUT
188191
189192
- name: Create GitHub Release
190-
uses: softprops/action-gh-release@v2
193+
uses: softprops/action-gh-release@v3
191194
with:
192195
tag_name: v${{ env.VERSION }}
193196
name: v${{ env.VERSION }}
@@ -210,7 +213,7 @@ jobs:
210213
packages: write
211214
steps:
212215
- name: Checkout code
213-
uses: actions/checkout@v4
216+
uses: actions/checkout@v6
214217

215218
- name: Set up QEMU
216219
uses: docker/setup-qemu-action@v3
@@ -265,7 +268,7 @@ jobs:
265268
if: always()
266269
steps:
267270
- name: Delete artifacts
268-
uses: geekyeggo/delete-artifact@v5
271+
uses: geekyeggo/delete-artifact@v6
269272
with:
270273
name: ${{ env.BINARY_NAME }}-*
271274
failOnError: false

changelog.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# B4 - Bye Bye Big Bro
22

3+
## [1.68.0] - 2026-06-10
4+
5+
- FIXED: **Discovery found nothing on connections that tamper with DNS** - on networks where the provider tampers with DNS (so ordinary name lookups came back wrong or empty), Discovery could not look up its test sites and reported that nothing worked, even when a working setup existed. It looks those sites up over encrypted DNS instead, and saves the result set to use encrypted DNS (DNS-over-HTTPS) for its lookups by default, which holds up where plain DNS is tampered with.
6+
- FIXED: **Cancelling Discovery could crash b4** - pressing Cancel while a search was testing several sites at once could crash the whole service and force a restart.
7+
- FIXED: **Stopping b4 could leave its firewall rules on the router** - the cleanup at shutdown could be undone at the last moment, so the rules stayed in place after exit.
8+
- FIXED: **Routing through another interface broke after that interface changed its address** - when the chosen outgoing interface (for example a VPN or a modem) got a new address, the set's traffic kept using the old one until b4 was restarted.
9+
- FIXED: **Incomplete cleanup on stop** - stopping b4 (or `--clear-iptables`) left traces behind: adjusted system network settings, stray routing entries when two sets shared one outgoing interface, and a firewall rule that piled up with proxy or bridge routing.
10+
- FIXED: **Saving settings could leave the running configuration out of step** - changing the SOCKS5 proxy or your sets (adding domains or IPs, creating, editing, reordering, deleting, or bulk-toggling) could save a result that did not match what was actually running.
11+
312
## [1.67.2] - 2026-06-10
413

514
- FIXED: **Web UI update reported success but kept the old version on some setups** - where b4 isn't managed by a normal service (for example running directly in a container), the update replaced the program on disk but never restarted the running copy. It now stops the old copy and relaunches the new one, so the update actually takes effect.
@@ -702,7 +711,7 @@
702711

703712
## [1.10.1] - 2025-11-03
704713

705-
- IMPROVED: Intermittent connection failures where blocked sites would randomly fail to load in certain browsers (`Safari`, `Firefox`, `Chrome`). Connections _should_ now be more stable and reliable across all browsers by optimizing packet fragmentation strategy.
714+
- IMPROVED: Intermittent connection failures where blocked sites would randomly fail to load in certain browsers (`Safari`, `Firefox`, `Chrome`). Connections *should* now be more stable and reliable across all browsers by optimizing packet fragmentation strategy.
706715

707716
## [1.10.0] - 2025-11-02
708717

changelog_ru.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# B4 - Bye Bye Big Bro
22

3+
## [1.68.0] - 2026-06-10
4+
5+
- ИСПРАВЛЕНО: **Дискавери ничего не находило на подключениях с подменой DNS** - там, где провайдер подменяет DNS, Дискавери не могло определить адреса тестовых сайтов и сообщало, что ничего не работает, даже когда рабочая конфигурация существовала. Поиск адресов идёт через зашифрованный DNS, а сохранённый сет по умолчанию использует DNS-over-HTTPS.
6+
- ИСПРАВЛЕНО: **Отмена Дискавери могла привести к падению b4** - нажатие «Отмена» во время поиска, проверявшего несколько сайтов одновременно, могло обрушить всю службу, и её приходилось перезапускать.
7+
- ИСПРАВЛЕНО: **Остановка b4 могла оставлять его сетевые правила на роутере** - очистка при завершении работы могла быть отменена в последний момент, и правила оставались после выхода.
8+
- ИСПРАВЛЕНО: **Маршрутизация через другой интерфейс ломалась после смены его адреса** - когда выбранный исходящий интерфейс (например, VPN или модем) получал новый адрес, трафик сета продолжал использовать старый до перезапуска b4.
9+
- ИСПРАВЛЕНО: **Неполная очистка при остановке** - остановка b4 (или `--clear-iptables`) оставляла следы: изменённые системные сетевые настройки, лишние маршрутные записи, когда два сета использовали один исходящий интерфейс, и накапливающееся сетевое правило при маршрутизации через прокси или мост.
10+
- ИСПРАВЛЕНО: **Сохранение настроек могло привести к расхождению между сохранённой и работающей конфигурацией** - изменение SOCKS5-прокси или ваших сетов (добавление доменов или IP, создание, редактирование, изменение порядка, удаление или массовое включение/выключение) могло сохранить результат, не соответствующий тому, что фактически работало.
11+
312
## [1.67.2] - 2026-06-10
413

514
- ИСПРАВЛЕНО: **Обновление через веб-интерфейс сообщало об успехе, но на некоторых установках оставалась старая версия** - там, где b4 не управляется обычной службой (например, при запуске прямо в контейнере), обновление заменяло программу на диске, но не перезапускало работающую копию. Теперь b4 останавливает старую копию и запускает новую, поэтому обновление действительно вступает в силу.

src/detector/classify.go

Lines changed: 27 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -1,127 +1,44 @@
11
package detector
22

3-
import (
4-
"strings"
5-
)
3+
import "github.com/daniellavrushin/b4/netprobe"
64

7-
type tlsStage int
5+
type DomainStatus = netprobe.DomainStatus
86

97
const (
10-
stageConnect tlsStage = iota
11-
stageHandshake
12-
stageRead
8+
DomainOk = netprobe.DomainOk
9+
DomainTLSDPI = netprobe.DomainTLSDPI
10+
DomainTLSMITM = netprobe.DomainTLSMITM
11+
DomainTLSSpoof = netprobe.DomainTLSSpoof
12+
DomainTLSAlert = netprobe.DomainTLSAlert
13+
DomainTLSReset = netprobe.DomainTLSReset
14+
DomainTLSDrop = netprobe.DomainTLSDrop
15+
DomainSYNDrop = netprobe.DomainSYNDrop
16+
DomainTCP16 = netprobe.DomainTCP16
17+
DomainISPPage = netprobe.DomainISPPage
18+
DomainBlocked = netprobe.DomainBlocked
19+
DomainDNSFake = netprobe.DomainDNSFake
20+
DomainTimeout = netprobe.DomainTimeout
21+
DomainError = netprobe.DomainError
1322
)
1423

24+
type tlsStage = netprobe.TLSStage
25+
1526
const (
16-
tcp16MinBytes = 12 * 1024
17-
tcp16MaxBytes = 69 * 1024
27+
stageConnect = netprobe.StageConnect
28+
stageHandshake = netprobe.StageHandshake
29+
stageRead = netprobe.StageRead
1830
)
1931

32+
const tcp16MaxBytes = netprobe.TCP16MaxBytes
33+
2034
func ClassifyTLSError(err error) (DomainStatus, string) {
21-
return ClassifyTLSErrorStaged(err, stageHandshake, 0)
35+
return netprobe.ClassifyTLSError(err)
2236
}
2337

2438
func ClassifyTLSErrorStaged(err error, stage tlsStage, bytesRead int) (DomainStatus, string) {
25-
if err == nil {
26-
return DomainOk, ""
27-
}
28-
29-
msg := strings.ToLower(err.Error())
30-
31-
isTimeout := strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline exceeded") || strings.Contains(msg, "timed out")
32-
isEOF := strings.Contains(msg, "eof")
33-
isReset := strings.Contains(msg, "connection reset") || strings.Contains(msg, "reset by peer")
34-
35-
if stage == stageRead && isTimeout && bytesRead >= tcp16MinBytes && bytesRead <= tcp16MaxBytes {
36-
return DomainTCP16, "Read stalled after TSPU fat-flow window (12-69KB)"
37-
}
38-
39-
if isTimeout {
40-
switch stage {
41-
case stageConnect:
42-
return DomainSYNDrop, "TCP SYN dropped (no handshake)"
43-
case stageHandshake:
44-
return DomainTLSDrop, "TLS handshake timed out (drop)"
45-
default:
46-
return DomainTimeout, "Connection timed out"
47-
}
48-
}
49-
50-
if strings.Contains(msg, "wrong version number") {
51-
return DomainTLSSpoof, "Non-TLS response received (DPI replacement)"
52-
}
53-
for _, p := range []string{"record overflow", "oversized", "record layer failure", "decode error", "decoding error", "illegal parameter", "bad record mac", "decryption failed"} {
54-
if strings.Contains(msg, p) {
55-
return DomainTLSSpoof, "Garbage TLS response (DPI injection)"
56-
}
57-
}
58-
59-
if strings.Contains(msg, "alert") || strings.Contains(msg, "unrecognized name") || strings.Contains(msg, "handshake failure") {
60-
switch {
61-
case strings.Contains(msg, "unrecognized name"):
62-
return DomainTLSAlert, "SNI blocked (unrecognized name)"
63-
case strings.Contains(msg, "protocol version"):
64-
return DomainTLSAlert, "TLS protocol version alert"
65-
default:
66-
return DomainTLSAlert, "TLS alert (DPI disruption)"
67-
}
68-
}
69-
70-
if isReset {
71-
if stage == stageHandshake || stage == stageConnect {
72-
return DomainTLSReset, "TCP RST during handshake (active reset)"
73-
}
74-
return DomainTLSReset, "TCP RST during transfer"
75-
}
76-
77-
if isEOF {
78-
if stage == stageHandshake || bytesRead == 0 {
79-
return DomainTLSReset, "Connection terminated (EOF injection)"
80-
}
81-
return DomainTLSReset, "Connection dropped during transfer (EOF)"
82-
}
83-
84-
for _, p := range []string{"self-signed", "self signed", "unknown authority", "certificate has expired", "certificate is not valid", "hostname mismatch", "name mismatch", "x509", "certificate"} {
85-
if strings.Contains(msg, p) {
86-
return DomainTLSMITM, "Certificate substitution (possible MITM)"
87-
}
88-
}
89-
90-
if strings.Contains(msg, "no shared cipher") || strings.Contains(msg, "cipher") {
91-
return DomainTLSMITM, "Cipher mismatch (possible MITM)"
92-
}
93-
94-
if strings.Contains(msg, "refused") {
95-
return DomainBlocked, "Connection refused"
96-
}
97-
if strings.Contains(msg, "no such host") || strings.Contains(msg, "no address") {
98-
return DomainError, "DNS resolution failed"
99-
}
100-
if strings.Contains(msg, "internal error") {
101-
return DomainError, "TLS internal error"
102-
}
103-
104-
return DomainError, err.Error()
39+
return netprobe.ClassifyTLSErrorStaged(err, stage, bytesRead)
10540
}
10641

107-
func ClassifyHTTPResponse(statusCode int, location string, body string) (DomainStatus, string) {
108-
if statusCode == 451 {
109-
return DomainISPPage, "HTTP 451 Unavailable For Legal Reasons"
110-
}
111-
112-
locLower := strings.ToLower(location)
113-
for _, marker := range BlockMarkers {
114-
if strings.Contains(locLower, marker) {
115-
return DomainISPPage, "Redirect to ISP block page: " + location
116-
}
117-
}
118-
119-
bodyLower := strings.ToLower(body)
120-
for _, marker := range BodyBlockMarkers {
121-
if strings.Contains(bodyLower, marker) {
122-
return DomainISPPage, "ISP block page detected in response body"
123-
}
124-
}
125-
126-
return DomainOk, ""
42+
func ClassifyHTTPResponse(statusCode int, location, body string) (DomainStatus, string) {
43+
return netprobe.ClassifyHTTPResponse(statusCode, location, body)
12744
}

0 commit comments

Comments
 (0)