b4/Makefile
Daniel Lavrushin c48fa3eef6
issue-429-432 (#438)
* Enhance nftables rules to include connection tracking direction

- Added rules to track the direction of original and reply packets in the nftables configuration for better handling of TCP traffic on port 443.
- Updated existing rules to incorporate connection tracking direction, ensuring that original and reply packets are processed correctly.
- Adjusted the conditions for packet acceptance and queuing to improve traffic management and performance.
A TCP port filter that covers client source ports makes b4 skip outgoing TCP
Fixes #429
Domain-only sets leave the first segment of Chrome's split ClientHello untouched
Fixes #430
A TCP Fast Open SYN that carries a ClientHello is fragmented like a data segment
Fixes #431
b4's own fakes and fragments count toward the per-connection queue window
Fixes #432

* issue-427-428 (#435)

* feat(config): add TCP port overlap detection with ephemeral ranges

* feat(direction): implement queue direction handling and add tests

* Enhance packet direction handling and flow management in TUN

- Introduced a new `Direction` type in the `engine` package to represent packet directions.
- Updated `firstNLimiter` to return packet direction alongside admission status.
- Enhanced flow counting logic to track the direction of TCP flows (original vs reply).
- Implemented direction-based packet forwarding in the `Engine` to ensure correct routing.
- Added tests to verify the correct handling of packet directions and flow records.
- Refactored existing tests to utilize the new direction handling logic.
- Introduced new utility functions for determining TCP direction based on conntrack rules.

* fix(queue): queue router traffic once, clamp MSS before the queue, give b4's own packets their own window

- Router-local packets were queued twice, at OUTPUT and again at POSTROUTING
  (regression since 1.26.1): b4 now jumps from OUTPUT only when device
  filtering moves the B4 jump to FORWARD.
- nftables: the MSS clamp rules ran after the queue rules, so queued SYN-ACKs
  and router SYNs were never clamped (since 1.39.1); they now lead prerouting,
  output and forward, as on iptables.
- Packets b4 sends into a connection carry a mark bit of their own (0x2000000).
  The OUTPUT CONNMARK save no longer tags those connections with the queue
  mark (regression since 1.71.1): their SYN-ACKs and RSTs reach b4 again, so
  RST protection, RST escalation and TTL learning work on them; their server
  data stays unqueued unless a set uses an incoming mode.
- #432: connections b4 has sent into get the window again on top (2N TCP, 2M
  UDP), so its own fakes and fragments no longer use up the client's packets.
  Diagnostics and the installer compare a flowtable guard with 2*max(N,M).
- TCP to private destinations is accepted unprocessed again (regression since
  1.22.0): inbound connections to port-forwarded LAN servers broke when a set
  matched.
- nftables keeps counting the reply window in original packets; the port
  classifier warning gets its own dashboard event.

* fix(quic): a QUIC drop holds for the whole connection, not only the queue window (#428)

When a set drops or rejects a QUIC connection (udp.mode drop or reject, or a
block set), b4 now keeps it dropped past the first datagrams the queue rules
hand it. Before, a connection whose first datagram had been let through (a
ClientHello split over two Initials, as Chrome sends it, decided on the
second) used up its queue window on the dropped datagrams and the rest
reached the server, so a blocked site could open over QUIC.

- nftables: the flow goes into a timed set (b4_udp_drop4/6, 120 s) dropped
  in b4_chain ahead of the UDP window rule.
- iptables, TUN with xt_connbytes, and alongside the nftables set: b4
  deletes the flow's conntrack entry through ctnetlink, so its later
  datagrams start a fresh entry inside the window and reach b4 again. Every
  delete names a complete tuple and carries a mark guard that turns a
  tuple-less request into a no-op instead of a table flush; a self-test
  decides once whether conntrack netlink is usable.
- TUN without xt_connbytes: the engine's own packet count lets a dropped
  connection through to b4 past its limit.
- System Info shows whether drops hold (nf_conntrack_netlink,
  kmod-nf-conntrack-netlink on OpenWrt, which the installer recommends).

* fix(queue): drop the wider window, hold only real QUIC, keep held flows decidable, docs

- Revert the #432 window extension (2N/2M on connections b4 sent into) and
  the TUN doubling for tracked re-sends: letting the strategies reach client
  data past about 10 KB put the default pastseq fakes inside the stream and
  broke TLS uploads through a default-strategy set (measured 21-26 of 80),
  and the combo delay hit the first request after the handshake. #432 stays
  open until fakes cannot land inside the stream.
- The return for connections b4 sent into matches bit 0x2000000 with the
  queue mark clear, so foreign whole-value marks that carry both bits are not
  taken for b4's.
- A QUIC drop holds a flow only when the datagram is QUIC or belongs to a
  QUIC flow b4 named; other UDP to a learned address is dropped as before.
- nftables: the drop set is written from a background writer over one
  netlink socket, waiting behind another nf_tables transaction is no longer
  a failure, and a settings change empties the drop sets (with conntrack
  netlink) so held connections are decided again.
- RST protection judges the first connection to an address b4 has not
  learned yet by the SYN-ACK it saw before the connection matched a set.
- A queue mark containing bit 0x2000000 is warned about.
- The iptables monitor checks for a TCP queue rule on one listing line;
  System Info lists nf_conntrack_netlink only for iptables; make test-netns
  runs the conntrack netlink tests after the tables tests.
- Docs EN/RU, AI topics and changelog for the queue direction, the marks,
  router traffic, the MSS clamp order, QUIC drops and private destinations.

* fix(queue): direction match on duplication rules, private guard only without an address match, refuse a new queue mark with bit 25

From the Copilot review of #438.

- Duplication rules carry the ORIGINAL direction match wherever the
  kernel has it: B4 (-m conntrack --ctdir ORIGINAL), b4_chain (ct
  direction original) and TUN's duplication steer rules. Without it a
  wide port filter let a reply from a duplication address, such as a
  local service answering a client in that range, be queued at
  POSTROUTING or steered into the TUN and duplicated as client
  traffic, since the hook and the TUN take such packets as original.
  The TUN tests that kept these rules port-only now require the match.
- The private-destination guard in handleTCPPacket applies only when
  no set's IP targets cover the address on that port. A set that lists
  10.0.0.0/8 keeps its strategy, as before this branch; SNI-matched
  and port-only sets still leave port-forwarded LAN servers alone.
- Saving a queue mark with bit 0x2000000 as a new value is refused
  (queue.mark, mark_conflict). A configuration that already holds one
  keeps loading and saving with the warning, so an upgrade does not
  stop the service.
- Docs (EN, RU): capture.md and marks.md drop the duplication
  exemption from the direction text, the zapret note follows (its
  untracked packets are no longer queued toward duplication
  addresses), marks.md lists the refusal, matching.md the exception
  for listed private addresses.

* fix(config): refuse a mark with bit 25 only where NFQUEUE mode newly takes it

From Copilot's review of 50e71425: the save-time refusal also turned
down a TUN configuration's mark such as 0x2008000, although TUN adds
0x10000000 to its packets and never 0x2000000, and loading such a
configuration from disk only warns in NFQUEUE mode. The refusal now
applies when a save leaves NFQUEUE mode with a mark that has bit
0x2000000 and the running configuration did not already run NFQUEUE
mode with that mark, so leaving TUN mode with one is still refused.
marks.md (EN, RU) says NFQUEUE mode.

* docs: what an ASUS connection-mark rewrite does to b4's replies

Measured on an ASUS RT-BE88U with AiProtection on: the firmware writes
its own value into the connection mark of device connections from their
start, and the values of all 24 test connections had bit 15, the
default queue mark. The return for b4's own connections in
B4_PREROUTING then skipped all 235 replies of 6 HTTPS connections and
all 6 answers of DNS queries to an outside resolver. marks.md and the
three AI topics said the server's packets are queued as on any other
connection in that case, which holds only for values without the queue
bit, such as the 0x10C3D seen with QoS on. They now say the firmware
can remove bit 0x2000000, and that a value with every queue-mark bit
hides the connection's replies, the SYN-ACK and resets included.

marks.md (EN, RU) also says that b4 refuses a queue mark with bit
0x2000000 when a save would put it on NFQUEUE mode for the first time,
instead of accepting it with a warning.

* test(ctnl): refuse to flush conntrack when the namespace check cannot prove isolation

From Copilot's review of 35cea3d9: with B4_NETNS_TEST=1 and root, a
failed lookup of /proc/self/ns/net or /proc/1/ns/net skipped the
isolation check, and the tuple-less delete test could then flush the
conntrack table of the namespace it ran in, the host's included.

Failing closed on any failed lookup would refuse make test-netns
itself: inside unshare -Urnm, /proc/1/ns/net answers EACCES. When a
lookup fails, the tests now refuse to run if any interface other than
loopback is up. A fresh namespace passes: the kernel's fallback tunnel
devices (gre0, sit0, tunl0 and the like) are created down. Checked: the
rig passes, and the same rig with a dummy interface up is refused.

* docs(capture): the drop set holds by addresses and ports

From Copilot's review of 0dcc9308: on nftables, a new QUIC connection
that a device opens from the same UDP port to the same server address
and port within the 2 minutes is dropped by the same set element,
whatever name it carries. It takes a domain-only set and a client that
reuses one UDP socket for several connections. Exempting Initials from
the drop set and deleting the element when b4 accepts a replacement
would change the kernel rules and weaken the hold on routers without
nf_conntrack_netlink, so the code stays and internals/capture.md (EN,
RU) states the limit.

* fix(nfq): Match any IP address does not count as listing a private address

From the final review of #438: since 50e71425 the private-destination
guard skipped TCP to a private address whenever any address target of
a set matched it. A set with Match any IP address (0.0.0.0/0, ::/0)
and no source devices matches every private address, so a ClientHello
from the internet to a port-forwarded LAN server got that set's
strategy again, and a name match by another set could learn the LAN
server's address.

The guard now lets TCP to a private address through to processing only
when the matched set's most specific network holding it lies inside
the private ranges (SuffixSet.MatchedIPNet, utils.IsPrivateNet): a set
that lists 10.0.0.0/8 or 192.168.1.50 keeps its strategy, the catch-all
does not.

The test cases cover the catch-all, a private range next to the
catch-all, and assert that an untouched ClientHello is neither logged
nor read, so the "port the set leaves out" case now fails when the
guard moves above the port check (checked with go test -overlay on
both mutations).

Docs (EN, RU): matching.md states the exception and what b4 learns,
dscp.md and routing.md name the private-address exception for learned
destinations, and the dscp.enabled topic follows. The port-forward
changelog bullet drops its last sentence, a caveat on a FIXED line.

* fix(nfq): an overlapping private target on another port no longer hides the set that takes the port

From Copilot's review of dde38ff0: the private-destination guard asked
only about the set the address match picked, after the port check had
cleared it. With set A listing 10.1.2.3/32 on port 8443 and set B
listing 10.0.0.0/8 and the ClientHello's domain on 443, A won the
address match, the port check cleared it, and the guard accepted the
packet before the name match could pick B. Without the guard (main) B
handled it.

SuffixSet.ListsPrivateTCPTarget answers the guard's question directly:
does any set that applies to the device (source devices, IP family)
list the destination through a private network and take the port. If
so, b4 handles the connection as usual; Match any IP address still
lists no private address. It replaces MatchedIPNet.

Tests: the matcher case table (overlap, port, catch-all, source device,
IP family, IPv6) and the handler overlap case; the overlap and
catch-all cases both fail with the guard reverted to "no address
match" (go test -overlay). matching.md (EN, RU) says a set that
applies to the device lists the address, and that b4, not
necessarily that set, then handles the connection as usual.
2026-10-17 18:57:28 +02:00

381 lines
No EOL
15 KiB
Makefile

-include .env
export
# Build configuration
VERSION ?= 1.0.0
VERSION_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
VERSION_DATE := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
BINARY_NAME := b4
SRC_DIR := ./src
OUT_DIR := ./out
# Build flags
CGO_ENABLED ?= 0
LDFLAGS := -s -w -X main.Version=$(VERSION) -X main.Commit=$(VERSION_COMMIT) -X main.Date=$(VERSION_DATE)
BUILDFLAGS := -trimpath
# Linux architectures
LINUX_ARCHS := 386 amd64 arm64 armv5 armv6 armv7 \
loong64 mips mipsle mips64 mips64le \
mips_softfloat mipsle_softfloat \
ppc64 ppc64le riscv64 s390x
# Android architectures (optional)
ANDROID_ARCHS := amd64 arm64 armv7
ANDROID_MIN_API := 21
# Default target
.DEFAULT_GOAL := build
.PHONY: swagger
swagger:
@if [ "$(VERSION)" = "1.0.0" ]; then echo "ERROR: specify VERSION, e.g. make swagger VERSION=1.48.0"; exit 1; fi
@echo "Generating Swagger docs..."
@cd $(SRC_DIR) && go run github.com/swaggo/swag/cmd/swag@v1.16.4 init --generalInfo main.go --output tools/docs --parseDependency
@sed -i 's/"version": *"[^"]*"/"version": "$(VERSION)"/' $(SRC_DIR)/tools/docs/swagger.json
@cp $(SRC_DIR)/tools/docs/swagger.json ./docs/static/swagger.json
@rm -rf $(SRC_DIR)/tools/docs
@mkdir -p docs/static/swagger-versions
@cp docs/static/swagger.json "docs/static/swagger-versions/v$(VERSION).json"
@echo "[" > docs/static/swagger-versions/index.json
@first=true; for f in $$(ls docs/static/swagger-versions/v*.json | sort -rV); do \
ver=$$(basename "$$f" .json); \
[ "$$first" = true ] && first=false || echo "," >> docs/static/swagger-versions/index.json; \
echo " \"$$ver\"" >> docs/static/swagger-versions/index.json; \
done
@echo "]" >> docs/static/swagger-versions/index.json
@echo "Swagger spec (v$(VERSION)) copied to docs/static/"
# Build for current platform
.PHONY: build
build: swagger
@echo "Building $(BINARY_NAME) $(VERSION) for current platform..."
@mkdir -p $(OUT_DIR)
go -C $(SRC_DIR) build $(BUILDFLAGS) -ldflags "$(LDFLAGS)" -o ../$(OUT_DIR)/$(BINARY_NAME)
# Build for all Linux architectures
.PHONY: build-all
build-all: clean
@echo "Building $(BINARY_NAME) $(VERSION) for all Linux architectures..."
@mkdir -p $(OUT_DIR)/assets
@$(MAKE) --no-print-directory linux-all
@echo "Build complete! Assets in $(OUT_DIR)/assets/"
# Build for specific architecture
# Usage: make linux-amd64
.PHONY: linux-%
linux-%:
@$(eval ARCH := $(subst linux-,,$@))
@$(eval TARGET := $(ARCH))
@case $(ARCH) in \
armv5) GOARCH=arm GOARM=5 ;; \
armv6) GOARCH=arm GOARM=6 ;; \
armv7) GOARCH=arm GOARM=7 ;; \
mips_softfloat) GOARCH=mips GOMIPS=softfloat ;; \
mipsle_softfloat) GOARCH=mipsle GOMIPS=softfloat ;; \
*) GOARCH=$(ARCH) ;; \
esac; \
$(MAKE) --no-print-directory build-target GOOS=linux GOARCH=$$GOARCH GOARM=$$GOARM GOMIPS=$$GOMIPS TARGET=$(TARGET)
# Build all Linux targets
.PHONY: linux-all
linux-all:
@for arch in $(LINUX_ARCHS); do \
$(MAKE) --no-print-directory linux-$$arch; \
done
# Android builds (optional - requires ANDROID_NDK_HOME)
.PHONY: android
android:
@if [ -z "$$ANDROID_NDK_HOME" ]; then \
echo "Error: ANDROID_NDK_HOME not set. Skipping Android builds."; \
exit 1; \
fi
@echo "Building for Android..."
@for arch in $(ANDROID_ARCHS); do \
$(MAKE) --no-print-directory android-$$arch; \
done
# Build specific Android architecture
.PHONY: android-%
android-%:
@$(eval ARCH := $(subst android-,,$@))
@case $(ARCH) in \
amd64) CC="$$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-*/bin/x86_64-linux-android$(ANDROID_MIN_API)-clang" GOARCH=amd64 ;; \
arm64) CC="$$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-*/bin/aarch64-linux-android$(ANDROID_MIN_API)-clang" GOARCH=arm64 ;; \
armv7) CC="$$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-*/bin/armv7a-linux-androideabi$(ANDROID_MIN_API)-clang" GOARCH=arm GOARM=7 ;; \
*) echo "Unsupported Android arch: $(ARCH)"; exit 1 ;; \
esac; \
$(MAKE) --no-print-directory build-target GOOS=android GOARCH=$$GOARCH GOARM=$$GOARM TARGET=$(ARCH) CGO_ENABLED=1 CC=$$CC
# Generic build target (internal use)
.PHONY: build-target
build-target:
@OUT_PATH="$(OUT_DIR)/$(GOOS)-$(TARGET)"
@echo " → $(GOOS)/$(TARGET)"
@mkdir -p "$(OUT_DIR)/$(GOOS)-$(TARGET)" "$(OUT_DIR)/assets"
@GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) GOMIPS=$(GOMIPS) CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) \
go -C $(SRC_DIR) build $(BUILDFLAGS) -ldflags "$(LDFLAGS)" \
-o ../$(OUT_DIR)/$(GOOS)-$(TARGET)/$(BINARY_NAME)
@tar -czf "$(OUT_DIR)/assets/$(BINARY_NAME)-$(GOOS)-$(TARGET).tar.gz" \
-C "$(OUT_DIR)/$(GOOS)-$(TARGET)" "$(BINARY_NAME)"
@sha256sum "$(OUT_DIR)/assets/$(BINARY_NAME)-$(GOOS)-$(TARGET).tar.gz" \
> "$(OUT_DIR)/assets/$(BINARY_NAME)-$(GOOS)-$(TARGET).tar.gz.sha256"
# Quick builds for common platforms
.PHONY: amd64
amd64: linux-amd64
.PHONY: arm64
arm64: linux-arm64
.PHONY: arm
arm: linux-armv7
# Development helpers
.PHONY: run
run: build
sudo $(OUT_DIR)/$(BINARY_NAME)
.PHONY: test-netns
test-netns:
@echo "Building the tables test binary..."
@go -C $(SRC_DIR) test -c -o ../$(OUT_DIR)/tables.test ./tables/
@tables=0; ctnl=0; \
echo "Running the routing tests in a private network namespace..."; \
unshare -Urnm sh -c 'mount -t sysfs sysfs /sys 2>/dev/null || echo "warning: could not remount /sys; interface-kind tests will skip" >&2; B4_NETNS_TEST=1 exec $(OUT_DIR)/tables.test -test.run TestNetns -test.v' || tables=$$?; \
echo "Building the conntrack netlink test binary..."; \
if go -C $(SRC_DIR) test -c -o ../$(OUT_DIR)/ctnl.test ./ctnl/; then \
echo "Running the conntrack netlink tests in a private network namespace..."; \
unshare -Urnm sh -c 'B4_NETNS_TEST=1 exec $(OUT_DIR)/ctnl.test -test.run TestNetns -test.v' || ctnl=$$?; \
else \
ctnl=1; \
fi; \
echo "test-netns: tables netns tests exit $$tables, conntrack netlink netns tests exit $$ctnl"; \
[ "$$tables" -eq 0 ] && [ "$$ctnl" -eq 0 ]
.PHONY: install
install: build
sudo cp $(OUT_DIR)/$(BINARY_NAME) /usr/local/bin/
.PHONY: uninstall
uninstall:
sudo rm -f /usr/local/bin/$(BINARY_NAME)
# Clean build artifacts
.PHONY: clean
clean:
@echo "Cleaning build artifacts..."
@rm -rf $(OUT_DIR)
# Show version info
.PHONY: version
version:
@echo "Version: $(VERSION)"
@echo "Commit: $(VERSION_COMMIT)"
@echo "Date: $(VERSION_DATE)"
.PHONY: build-installer
build-installer:
@echo "Building installer script..."
@./installer/_build.sh
.PHONY: watch-installer
watch-installer:
@./installer/_watch.sh
.PHONY: build-docker
build-docker:
@echo "Building Docker image..."
@DOCKER_BUILDKIT=1 docker build -t b4:test .
.PHONY: run-docker
run-docker:
@echo "Running Docker container..."
@docker run --rm -it --cap-add=NET_ADMIN --cap-add=NET_RAW -p 7000:7000 -v ./out/linux-amd64:/etc/b4 b4:test --config /etc/b4/b4.json
.PHONY: gen-defaults
gen-defaults:
@echo "Generating UI defaults from Go..."
@cd $(SRC_DIR) && go run tools/gendefaults.go
.PHONY: build-ui
build-ui: gen-defaults
@echo "Building web UI..."
@cd src/http/ui && pnpm build
@echo "Web UI build complete."
HUB_DIR := ./hub
HUB_VERSION ?= dev
HUB_SOURCE := $(shell git describe --tags --always --dirty 2>/dev/null || echo unknown)
HUB_LDFLAGS := -s -w -X main.Version=$(HUB_VERSION) -X main.Source=$(HUB_SOURCE)
HUB_DATA ?= $(HUB_DIR)/data
HUB_LISTEN ?= 0.0.0.0:7100
HUB_PUBLIC_URL ?= http://127.0.0.1:7100
HUB_UPSTREAM ?= https://hub.b4core.app
HUB_MIRROR_DATA ?= $(HUB_DIR)/data-mirror
HUB_MIRROR_LISTEN ?= 0.0.0.0:7101
.PHONY: hub-build-ui
hub-build-ui:
@echo "Building hub admin console..."
@cd $(HUB_DIR)/ui && VITE_APP_VERSION="$(HUB_VERSION)" pnpm build
@echo "Hub admin console build complete."
.PHONY: hub-build
hub-build: hub-build-ui
@echo "Building hub service..."
@mkdir -p $(OUT_DIR)
@CGO_ENABLED=0 go -C $(HUB_DIR) build $(BUILDFLAGS) -ldflags "$(HUB_LDFLAGS)" -o ../$(OUT_DIR)/b4hub ./cmd/b4hub
@echo "Hub build complete: $(OUT_DIR)/b4hub"
HUB_ARCHS := amd64 arm64
.PHONY: hub-linux-%
hub-linux-%: hub-build-ui
@$(MAKE) --no-print-directory hub-go-linux-$(subst hub-linux-,,$@) HUB_VERSION=$(HUB_VERSION)
.PHONY: hub-go-linux-%
hub-go-linux-%:
@$(eval HUB_ARCH := $(subst hub-go-linux-,,$@))
@echo "Building hub service for linux/$(HUB_ARCH)..."
@mkdir -p $(OUT_DIR)/linux-$(HUB_ARCH) $(OUT_DIR)/assets
@GOOS=linux GOARCH=$(HUB_ARCH) CGO_ENABLED=0 go -C $(HUB_DIR) build $(BUILDFLAGS) -ldflags "$(HUB_LDFLAGS)" -o ../$(OUT_DIR)/linux-$(HUB_ARCH)/b4hub ./cmd/b4hub
@cp $(OUT_DIR)/linux-$(HUB_ARCH)/b4hub $(OUT_DIR)/b4hub-linux-$(HUB_ARCH)
@tar -czf "$(OUT_DIR)/assets/b4hub-linux-$(HUB_ARCH).tar.gz" -C "$(OUT_DIR)/linux-$(HUB_ARCH)" b4hub
@sha256sum "$(OUT_DIR)/assets/b4hub-linux-$(HUB_ARCH).tar.gz" > "$(OUT_DIR)/assets/b4hub-linux-$(HUB_ARCH).tar.gz.sha256"
@echo "Hub build complete: $(OUT_DIR)/assets/b4hub-linux-$(HUB_ARCH).tar.gz"
.PHONY: hub-linux-all
hub-linux-all: hub-build-ui
@for arch in $(HUB_ARCHS); do $(MAKE) --no-print-directory hub-go-linux-$$arch HUB_VERSION=$(HUB_VERSION); done
.PHONY: hub-docker
hub-docker:
@docker build -f $(HUB_DIR)/Dockerfile --build-arg VERSION=$(HUB_VERSION) --build-arg B4_SOURCE=$(HUB_SOURCE) -t lavrushin/b4hub:$(HUB_VERSION) .
HUB_DEPLOY_HOST ?=
HUB_DEPLOY_KEY ?=
HUB_DEPLOY_SSH := ssh $(if $(HUB_DEPLOY_KEY),-i $(HUB_DEPLOY_KEY)) -o StrictHostKeyChecking=accept-new $(HUB_DEPLOY_HOST)
.PHONY: hub-deploy
hub-deploy:
@if [ -z "$(HUB_DEPLOY_HOST)" ]; then \
echo "Error: HUB_DEPLOY_HOST (user@host) must be set in .env or on the command line"; \
exit 1; \
fi
@echo "Updating b4hub on $(HUB_DEPLOY_HOST) to the latest released image..."
@$(HUB_DEPLOY_SSH) 'cd /opt/b4hub && sudo docker compose pull -q && sudo docker compose up -d && sleep 5 && curl -fsS http://127.0.0.1:7100/b4/health && echo && sudo docker compose exec -T b4hub b4hub version'
@echo "Hub deploy complete."
.PHONY: hub-test
hub-test:
@go -C $(HUB_DIR) test ./...
.PHONY: hub-types
hub-types:
@go -C $(HUB_DIR) test ./internal/web -run TestAdminTypes -count=1 -update-types
.PHONY: hub-keygen
hub-keygen: hub-build
@$(OUT_DIR)/b4hub keygen --data $(HUB_DATA)
.PHONY: hub-run
hub-run: hub-build
@$(OUT_DIR)/b4hub serve --data $(HUB_DATA) --listen $(HUB_LISTEN) --public-url $(HUB_PUBLIC_URL)
.PHONY: hub-seed
hub-seed:
@go -C $(HUB_DIR) run ./cmd/b4hub-seed --data $(abspath $(HUB_DATA)) --public-url $(HUB_PUBLIC_URL) --reset
.PHONY: hub-mirror
hub-mirror: hub-build
@$(OUT_DIR)/b4hub mirror --data $(HUB_MIRROR_DATA) --upstream $(HUB_UPSTREAM) --listen $(HUB_MIRROR_LISTEN) --public-url http://127.0.0.1:7101
SFTP_PORT ?= 22
SSH_OPTS ?= -o StrictHostKeyChecking=no -o IPQoS=none
B4_RESTART_CMD ?= /opt/etc/init.d/S99b4
.PHONY: deploy-%
deploy-%:
@$(eval ARCH := $(subst deploy-,,$@))
@if [ ! -f .env ]; then \
echo "Error: .env file not found. Create one from .env.example"; \
exit 1; \
fi
@if [ -z "$(SFTP_HOST)" ] || [ -z "$(SFTP_PATH)" ] || [ -z "$(SFTP_USER)" ]; then \
echo "Error: SFTP_HOST, SFTP_USER and SFTP_PATH must be set in .env"; \
exit 1; \
fi
@$(MAKE) --no-print-directory build-ui
@$(MAKE) --no-print-directory $(ARCH)
@$(MAKE) --no-print-directory linux-$(ARCH)
@echo "Uploading to $(SFTP_USER)@$(SFTP_HOST):$(SFTP_PATH)/$(BINARY_NAME).new (b4 stays up; avoids ETXTBSY)..."
@if [ -n "$(SFTP_PASS)" ]; then \
echo "put $(OUT_DIR)/linux-$(ARCH)/$(BINARY_NAME) $(SFTP_PATH)/$(BINARY_NAME).new" | \
sshpass -p '$(SFTP_PASS)' sftp $(SSH_OPTS) -P $(SFTP_PORT) $(SFTP_USER)@$(SFTP_HOST) || exit 1; \
else \
echo "put $(OUT_DIR)/linux-$(ARCH)/$(BINARY_NAME) $(SFTP_PATH)/$(BINARY_NAME).new" | \
sftp $(SSH_OPTS) -P $(SFTP_PORT) $(SFTP_USER)@$(SFTP_HOST) || exit 1; \
fi
@echo "Swapping binary + restarting b4 (detached, survives the restart blip)..."
@if [ -n "$(SFTP_PASS)" ]; then \
sshpass -p '$(SFTP_PASS)' ssh $(SSH_OPTS) -p $(SFTP_PORT) $(SFTP_USER)@$(SFTP_HOST) \
'nohup sh -c "$(B4_RESTART_CMD) stop 2>/dev/null; sleep 1; mv -f $(SFTP_PATH)/$(BINARY_NAME).new $(SFTP_PATH)/$(BINARY_NAME); chmod +x $(SFTP_PATH)/$(BINARY_NAME); $(B4_RESTART_CMD) start" >/tmp/b4-deploy.log 2>&1 </dev/null &'; \
else \
ssh $(SSH_OPTS) -p $(SFTP_PORT) $(SFTP_USER)@$(SFTP_HOST) \
'nohup sh -c "$(B4_RESTART_CMD) stop 2>/dev/null; sleep 1; mv -f $(SFTP_PATH)/$(BINARY_NAME).new $(SFTP_PATH)/$(BINARY_NAME); chmod +x $(SFTP_PATH)/$(BINARY_NAME); $(B4_RESTART_CMD) start" >/tmp/b4-deploy.log 2>&1 </dev/null &'; \
fi
@echo "Deploy dispatched. b4 restarts on the router within a few seconds (see /tmp/b4-deploy.log)."
# Show help
.PHONY: help
help:
@echo "B4 Makefile - Linux packet processor build system"
@echo ""
@echo "Common targets:"
@printf " %-25s %s\n" "make" "Build for current platform"
@printf " %-25s %s\n" "make build-all" "Build for all Linux architectures"
@printf " %-25s %s\n" "make amd64" "Build for Linux amd64"
@printf " %-25s %s\n" "make arm64" "Build for Linux arm64"
@printf " %-25s %s\n" "make arm" "Build for Linux armv7"
@printf " %-25s %s\n" "make android" "Build for all Android architectures (requires ANDROID_NDK_HOME)"
@printf " %-25s %s\n" "make run" "Build and run with sudo"
@printf " %-25s %s\n" "make test-netns" "Run the routing egress and conntrack netlink tests in a network namespace"
@printf " %-25s %s\n" "make install" "Install to /usr/local/bin"
@printf " %-25s %s\n" "make clean" "Remove build artifacts"
@printf " %-25s %s\n" "make build-installer" "Build the installer script"
@printf " %-25s %s\n" "make watch-installer" "Watch and rebuild installer on changes"
@printf " %-25s %s\n" "make build-ui" "Build the web UI"
@printf " %-25s %s\n" "make hub-build-ui" "Build the hub admin console (pnpm build in hub/ui)"
@printf " %-25s %s\n" "make hub-build" "Build the community hub service into out/b4hub (HUB_VERSION, default dev; runs hub-build-ui first)"
@printf " %-25s %s\n" "make hub-linux-amd64" "Cross-compile the hub service (also arm64), tarball + sha256 into out/assets"
@printf " %-25s %s\n" "make hub-linux-all" "Cross-compile the hub service for every release architecture"
@printf " %-25s %s\n" "make hub-docker" "Build the lavrushin/b4hub:VERSION image from hub/Dockerfile"
@printf " %-25s %s\n" "make hub-deploy" "Pull the latest released b4hub image on the box in .env (HUB_DEPLOY_HOST, HUB_DEPLOY_KEY) and restart it"
@printf " %-25s %s\n" "make hub-test" "Run the hub service tests"
@printf " %-25s %s\n" "make hub-types" "Regenerate the console's API types from the Go views"
@printf " %-25s %s\n" "make hub-keygen" "Create a development hub key under hub/data"
@printf " %-25s %s\n" "make hub-run" "Run the hub service locally (HUB_LISTEN, default 0.0.0.0:7100)"
@printf " %-25s %s\n" "make hub-mirror" "Run a child hub mirroring HUB_UPSTREAM on port 7101"
@printf " %-25s %s\n" "make deploy-<arch>" "Build and upload via SFTP (requires .env)"
@printf " %-25s %s\n" "make help" "Show this help"
@echo ""
@echo "Architecture-specific builds:"
@printf " %-25s %s\n" "make linux-<arch>" "Build for specific Linux architecture"
@printf " %-25s %s\n" "make android-<arch>" "Build for specific Android architecture"
@echo ""
@echo "Available Linux architectures:"
@echo " $(LINUX_ARCHS)"
@echo ""
@echo "Available Android architectures:"
@echo " $(ANDROID_ARCHS)"
# Prevent make from treating command-line targets as files
.PHONY: %