fix: improve single-instance check to prevent false "another b4 insta… (#220)
Some checks are pending
Deploy documentation / build-and-deploy (push) Waiting to run

This commit is contained in:
Daniel Lavrushin 2026-05-10 17:30:41 +02:00 committed by GitHub
parent 88fe85b516
commit d13be7f96f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 29 deletions

View file

@ -1,5 +1,9 @@
# B4 - Bye Bye Big Bro
## [1.61.4] - 2026-05-10
- FIXED: **False "another b4 instance is already running" error** - b4 could refuse to start after a crash, after restarting from the Web UI, or when running inside containers (for example on MikroTik), even when no other b4 was actually running. The single-instance check is now reliable in those cases.
## [1.61.3] - 2026-05-09
- ADDED: **Custom payload for UDP fake packets** - new "Fake Packet Payload" picker in the UDP fake settings of each set. Choose a captured `.bin` (uploaded in Settings → Payloads, or auto-captured from live QUIC traffic) to use as the body of fake UDP packets. Empty = zero fill (previous behavior). The Settings → Payloads upload form now has an explicit TLS/QUIC protocol selector.

View file

@ -3,6 +3,7 @@ package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
@ -10,7 +11,6 @@ import (
"os/signal"
"runtime/debug"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
@ -87,9 +87,13 @@ func runB4(cmd *cobra.Command, args []string) error {
return nil
}
if err := ensureSingleInstance(); err != nil {
releaseLock, err := ensureSingleInstance()
if err != nil {
return err
}
if releaseLock != nil {
defer releaseLock()
}
initTimezone()
initMemoryLimit()
@ -450,39 +454,60 @@ func gracefulShutdown(cfg *config.Config, pool *nfq.Pool, httpServer *http.Serve
return nil
}
func ensureSingleInstance() error {
self := os.Getpid()
selfComm, err := os.ReadFile("/proc/self/comm")
if err != nil {
return nil
func ensureSingleInstance() (func(), error) {
candidates := []string{"/var/run/b4.pid", "/run/b4.pid"}
var f *os.File
var path string
for _, p := range candidates {
fp, err := os.OpenFile(p, os.O_CREATE|os.O_RDWR|syscall.O_NOFOLLOW, 0600)
if err == nil {
f = fp
path = p
break
}
}
target := strings.TrimSpace(string(selfComm))
if target == "" {
return nil
if f == nil {
return nil, nil
}
entries, err := os.ReadDir("/proc")
if err != nil {
return nil
if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
if !errors.Is(err, syscall.EWOULDBLOCK) && !errors.Is(err, syscall.EAGAIN) {
fmt.Fprintf(os.Stderr, "[INIT] single-instance check skipped: flock(%s): %v\n", path, err)
f.Close()
return nil, nil
}
data, _ := io.ReadAll(f)
pid := strings.TrimSpace(string(data))
f.Close()
if pid == "" {
return nil, fmt.Errorf("another b4 instance is already running (lock: %s)", path)
}
return nil, fmt.Errorf("another b4 instance is already running (pid %s)", pid)
}
for _, e := range entries {
if !e.IsDir() {
continue
}
pid, err := strconv.Atoi(e.Name())
if err != nil || pid == self {
continue
}
data, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
if err != nil {
continue
}
if strings.TrimSpace(string(data)) == target {
return fmt.Errorf("another b4 instance is already running (pid %d)", pid)
}
if err := writePidFile(f, os.Getpid()); err != nil {
fmt.Fprintf(os.Stderr, "[INIT] could not update pidfile %s: %v\n", path, err)
}
return nil
cleanup := func() {
syscall.Flock(int(f.Fd()), syscall.LOCK_UN)
f.Close()
os.Remove(path)
}
return cleanup, nil
}
func writePidFile(f *os.File, pid int) error {
if err := f.Truncate(0); err != nil {
return err
}
if _, err := f.Seek(0, 0); err != nil {
return err
}
if _, err := fmt.Fprintf(f, "%d\n", pid); err != nil {
return err
}
return f.Sync()
}
func initMemoryLimit() {