Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# B4 - Bye Bye Big Bro

## [1.66.1] - 2026-06-08

- FIXED: **Dummy network interfaces were missing from the interface lists** - a dummy interface (for example `dummy0`) did not appear in Settings, so it could not be picked for monitoring or NAT masquerade. Any dummy interface that is up now shows up in both lists.
- FIXED: **Telegram desktop kept reconnecting every 30-60 seconds over the Telegram bridge** - a background keep-alive added in 1.66.0 to hold idle connections open actually sent a signal Telegram's servers rejected, so the connection dropped and rebuilt itself about twice a minute (you would see parts of the Telegram window flicker). That keep-alive has been removed, so connections stay healthy on their own again.

## [1.66.0] - 2026-06-07

- ADDED: **Blocking stats on the Dashboard** - when a set uses Block (blackhole) mode and actually blocks something, the Dashboard now shows a "Blackhole" panel with the total number of blocked attempts, the most-blocked domains, and which devices ran into the most blocks. The panel stays hidden until there is something to show, so it never clutters the page when nothing is being blocked. Blocked connections are also tagged with a "block" label on the Traffic page so you can spot them in the live feed.
- ADDED: **Control over the Telegram server-list backup** - to reach Telegram, b4 fetches Telegram's data center list from Telegram's official address, and falls back to a backup copy hosted by the b4 author only if that is blocked (`lavrush.in`). Settings -> MTProto Proxy now has a "DC list fallback mirror" switch to turn it off or point it at your own copy.
- FIXED: **Set Routing now respects your device choices** - if you used Settings -> Devices to make b4 work for only some devices (or to exclude some), that choice was ignored by a set's Routing tab. Whatever the set did with the traffic (send it to another network interface or an upstream proxy, run it through the Telegram bridge, or block it) happened for every device regardless. Routing now applies only to the devices you picked. You can also give an individual set its own device list in its settings, so a single set can route, bridge, or block traffic for just specific devices without affecting the rest.
- FIXED: **The router itself showed up as a wrongly-named device on the Traffic page** - b4 mistook the router's own address for a regular client and listed it as a separate device (sometimes guessed as a phone brand), filing the router's own connections under it. b4 now recognizes its own interfaces and labels that traffic simply as "Router".
- FIXED: **Discord voice and video calls could drop when blocking UDP** - on a set with UDP set to Drop or Reject, Discord calls could break because b4 did not recognize Discord's call traffic. b4 now lets it through (when "Filter STUN" is on, the default), so calls keep working.
- FIXED: **b4 would not start on some Asus Merlin routers** - on certain firmware (for example the MerlinWRT) b4 quit right after starting and showed a confusing message about a missing `xt_connbytes` feature, even though the router actually supported it. The real cause was b4 using a newer firewall command option that the router's built-in tool did not understand. b4 now adapts to the router's own tools automatically, so it starts normally with nothing extra to install.
- FIXED: **Telegram no longer disconnects with a "proxy is not configured correctly" message after sitting idle** - when a Telegram connection through b4 was left idle for a while, your network could quietly drop it in the background, and Telegram would show "The proxy you are using is not configured correctly and will be disabled" even though the proxy was working fine. b4 now keeps these connections alive on its own, so they stay open and reconnect cleanly instead of being dropped.
- FIXED: **Connections page showed nothing when the device clock was out of sync** - the time filter (30s / 1m / 5m / 15m) and the per-connection activity graph compared the live data against the browser's own clock, so a computer whose clock was off by even ~30 seconds could see an empty list and empty activity bars. Filtering and the activity graph now use the timestamps in the connection data itself, so they work regardless of the device clock or its timezone.
- FIXED: **b4 would not start on some Asus Merlin routers** - on certain firmware (for example the MerlinWRT) b4 quit right after starting and showed a confusing message about a missing `xt_connbytes` feature, even though the router actually supported it. The real cause was b4 using a newer firewall command option that the router's built-in tool did not understand. b4 now adapts to the router's own tools automatically, so it starts normally with nothing extra to install.- FIXED: **Connections page showed nothing when the device clock was out of sync** - the time filter (30s / 1m / 5m / 15m) and the per-connection activity graph compared the live data against the browser's own clock, so a computer whose clock was off by even ~30 seconds could see an empty list and empty activity bars. Filtering and the activity graph now use the timestamps in the connection data itself, so they work regardless of the device clock or its timezone.
- FIXED: **A device could still show up twice on the Traffic page** - one device sometimes appeared as two rows, once under its name and once under its bare IP. b4 now recognizes these as the same device and shows it only once.
- FIXED: **Discovery found no working strategy when site names were entered with quotes** - if you added domains to Discovery wrapped in quotation marks (for example `"discord.com","youtube.com"`, as happens when pasting a copied list), b4 kept the quote marks as part of each name, so every lookup failed and Discovery reported nothing working for any site. Surrounding quotes are now removed automatically. [#241](https://github.com/DanielLavrushin/b4/issues/241)

Expand Down
9 changes: 5 additions & 4 deletions src/http/handler/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ func getSystemInterfaces() ([]string, error) {

// Known virtual/internal interface prefixes to exclude
excludePrefixes := []string{
"lo", "dummy", "gre", "erspan", "ifb", "imq",
"lo", "gre", "erspan", "ifb", "imq",
"ip6_vti", "ip6gre", "ip6tnl", "ip_vti", "sit",
"spu_", "bcmsw", "blog",
}
Expand Down Expand Up @@ -220,12 +220,13 @@ func getSystemInterfaces() ([]string, error) {
} else {
// On host, require IP or known useful prefix
addrs, _ := iface.Addrs()
isBridgeOrWireless := strings.HasPrefix(iface.Name, "br") ||
allowedWithoutIP := strings.HasPrefix(iface.Name, "br") ||
strings.HasPrefix(iface.Name, "wl") ||
strings.HasPrefix(iface.Name, "tun") ||
strings.HasPrefix(iface.Name, "tap")
strings.HasPrefix(iface.Name, "tap") ||
strings.HasPrefix(iface.Name, "dummy")

if len(addrs) > 0 || isBridgeOrWireless {
if len(addrs) > 0 || allowedWithoutIP {
ifaceNames = append(ifaceNames, iface.Name)
}
}
Expand Down
64 changes: 17 additions & 47 deletions src/mtproto/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ import (
)

const (
maxConnections = 512
relayBufSize = 65536
wsKeepaliveInterval = 30 * time.Second
maxConnections = 512
relayBufSize = 65536
)

type Server struct {
Expand Down Expand Up @@ -290,15 +289,19 @@ func (s *Server) handleConn(raw net.Conn) {
if _, ok := dcConn.Conn.(*wsConn); ok {
splitter = newMsgSplitter(result.ProtoTag)
}
s.relay(result.Conn, dcConn, splitter, fmt.Sprintf("%s %s<->DC%d", tag, clientAddr, result.DC))
s.relay(result.Conn, dcConn, splitter, fmt.Sprintf("%s %s<->DC%d via %s", tag, clientAddr, result.DC, transport))
}

func (s *Server) relay(client, dc io.ReadWriteCloser, splitter *msgSplitter, label string) {
relayConns(client, dc, splitter, label, &s.bufPool)
}

func relayConns(client, dc io.ReadWriteCloser, splitter *msgSplitter, label string, bufPool *sync.Pool) {
errCh := make(chan error, 2)
type relayEnd struct {
dir string
err error
}
endCh := make(chan relayEnd, 2)
start := time.Now()
var upBytes, downBytes atomic.Int64

Expand All @@ -324,7 +327,7 @@ func relayConns(client, dc io.ReadWriteCloser, splitter *msgSplitter, label stri
}
counter.Store(total)
log.Debugf("%s %s: %d bytes, err=%v", label, dir, total, err)
errCh <- err
endCh <- relayEnd{dir: dir, err: err}
}

cpSplit := func(dst io.Writer, src io.Reader, dir string, counter *atomic.Int64) {
Expand Down Expand Up @@ -354,7 +357,7 @@ func relayConns(client, dc io.ReadWriteCloser, splitter *msgSplitter, label stri
}
counter.Store(total)
log.Debugf("%s %s: %d bytes, err=%v", label, dir, total, err)
errCh <- err
endCh <- relayEnd{dir: dir, err: err}
}

if splitter != nil {
Expand All @@ -364,48 +367,15 @@ func relayConns(client, dc io.ReadWriteCloser, splitter *msgSplitter, label stri
}
go cp(client, dc, "DC->client", &downBytes)

stopKA := startWSKeepalive(dc, label)
<-errCh
close(stopKA)
first := <-endCh
_ = client.Close()
_ = dc.Close()
<-errCh
log.Infof("%s closed: up=%d down=%d in %dms", label, upBytes.Load(), downBytes.Load(), time.Since(start).Milliseconds())
}

func asWSConn(c io.ReadWriteCloser) *wsConn {
switch v := c.(type) {
case *wsConn:
return v
case *ObfuscatedConn:
if w, ok := v.Conn.(*wsConn); ok {
return w
}
}
return nil
}
<-endCh

func startWSKeepalive(dc io.ReadWriteCloser, label string) chan struct{} {
stop := make(chan struct{})
ws := asWSConn(dc)
if ws == nil {
return stop
up, down := upBytes.Load(), downBytes.Load()
stale := ""
if first.dir == "DC->client" && down == 0 {
stale = " stale-upstream?"
}
go func() {
t := time.NewTicker(wsKeepaliveInterval)
defer t.Stop()
for {
select {
case <-stop:
return
case <-t.C:
if err := ws.sendPing(); err != nil {
log.Debugf("%s ws keepalive ping failed: %v -> closing upstream", label, err)
_ = dc.Close()
return
}
}
}
}()
return stop
log.Infof("%s closed: first=%s err=%v up=%d down=%d in %dms%s", label, first.dir, first.err, up, down, time.Since(start).Milliseconds(), stale)
}
2 changes: 1 addition & 1 deletion src/mtproto/transparent.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (b *TransparentBridge) Handle(client net.Conn, origIP net.IP, origPort int)
}
defer dcConn.Close()

label := fmt.Sprintf("%s %s<->DC%d(transparent)", tag, client.RemoteAddr(), dc)
label := fmt.Sprintf("%s %s<->DC%d(transparent via %s)", tag, client.RemoteAddr(), dc, transport)
log.Infof("%s bridge relay %s:%d -> DC%d via %s [dc-from=%s]", tag, origIP, origPort, dc, transport, dcSrc)

var splitter *msgSplitter
Expand Down
7 changes: 0 additions & 7 deletions src/mtproto/wsdial.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,13 +271,6 @@ func (c *wsConn) writeFrame(op byte, payload []byte) error {
return err
}

func (c *wsConn) sendPing() error {
if c.closed.Load() {
return net.ErrClosed
}
return c.writeFrame(wsOpcodePing, nil)
}

func dialWS(host, sni, path string, timeout time.Duration, mark uint) (net.Conn, error) {
if path == "" {
path = "/apiws"
Expand Down