Pushes WiFi credentials from Mac to all Pi devices within BLE range. Like Apple Share WiFi, but for Raspberry Pi.
sequenceDiagram
participant Mac as Mac<br>(wifi-push)
participant Pi as Pi 1..N<br>(wifi-scanner)
participant WiFi as WiFi Router
Note over Pi: Service starts on boot<br>WiFi connected → sleeping
Note over Pi: WiFi drops → BLE scan starts
Note over Mac: wifi-push --ssid X<br>starts BLE peripheral
Pi->>Mac: X25519 ephemeral public key
Note over Mac: Derive shared secret (HKDF)<br>Encrypt (AES-256-GCM)<br>Sign (SSH key)
Mac->>Pi: Encrypted + signed payload
Note over Pi: Verify signature<br>Decrypt credentials
Pi->>WiFi: nmcli connect (SSID + PSK)
Note over Pi: WiFi restored → back to sleep
Typical recovery time: ~6 seconds from BLE discovery to WiFi connected.
| Component | Runs on | Description |
|---|---|---|
wifi-push (Go) |
Mac / Linux | BLE peripheral, broadcasts signed WiFi credentials |
wifi-scanner (Go) |
Pi | BLE scanner — single static binary, no dependencies |
Both components are Go binaries (~4-5 MB). No Python, no pip, no external dependencies. Mac side uses CoreBluetooth via CGo, Linux uses BlueZ D-Bus (pure Go).
Mac: build wifi-push, have an SSH key (~/.ssh/id_rsa or id_ed25519).
Pi (one-time):
# 1. Copy binary
scp wifi-scanner USER@PI:/usr/local/bin/
# 2. Add Mac's SSH key
ssh-copy-id USER@PI
# 3. Set Bluetooth to LE-only mode
sudo sed -i 's/^#ControllerMode = dual/ControllerMode = le/' /etc/bluetooth/main.conf
sudo systemctl restart bluetooth
# 4. Add polkit rule for NetworkManager (see detailed setup below)
# 5. Create systemd service (see detailed setup below)
sudo systemctl enable --now ble-wifi-setupDetailed instructions for each step — below.
- Encryption: ECDH (X25519) + AES-256-GCM — each connection is uniquely encrypted
- Authentication: payload signed with Mac's SSH key (RSA or Ed25519)
- Verification: Pi verifies signature via
~/.ssh/authorized_keys - Replay protection: ephemeral ECDH keys (unique shared secret per connection) + 60s cooldown per SSID
- Forward secrecy: compromising the SSH key does not reveal past sessions
- Rate limiting: 30s cooldown per device to prevent BLE spam; handshake flood protection (5 failures/60s blocks device)
- Payload size limit: 16KB max BLE payload to prevent memory exhaustion
- No extra keys needed — uses existing SSH keys
# Mac side (wifi-push) — on Mac:
go build -ldflags="-s -w" -o wifi-push ./cmd/wifi-push
# Pi side (wifi-scanner) — cross-compilation:
# Pi 5, Pi 4, Pi Zero 2W (arm64)
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o wifi-scanner ./cmd/wifi-scanner
# Pi Zero W (arm32)
GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -o wifi-scanner ./cmd/wifi-scannerscp wifi-scanner USER@PI_HOST:/usr/local/bin/wifi-scanner# On Mac:
ssh-copy-id USER@PI_HOSTThe Mac's public key (~/.ssh/id_rsa.pub or ~/.ssh/id_ed25519.pub) will be added to ~/.ssh/authorized_keys on the Pi — the same key is used for WiFi Push verification.
Set ControllerMode = le in /etc/bluetooth/main.conf:
sudo sed -i 's/^#ControllerMode = dual/ControllerMode = le/' /etc/bluetooth/main.conf
sudo systemctl restart bluetoothVerify:
bluetoothctl show | grep Powered
# Should be: Powered: yes
sudo btmgmt info | grep "current settings"
# Should contain "le" but NOT "br/edr"This is required — without it BlueZ tries classic Bluetooth (BR/EDR) to connect to Mac and fails with br-connection-profile-unavailable. Pi only needs BLE for wifi-push.
If Bluetooth is off: sudo rfkill unblock bluetooth && sudo systemctl restart bluetooth.
The scanner runs as a regular user and manages WiFi via nmcli. Permission is required:
sudo tee /etc/polkit-1/rules.d/50-wifi-push.rules > /dev/null << 'EOF'
polkit.addRule(function(action, subject) {
var allowed = [
"org.freedesktop.NetworkManager.network-control",
"org.freedesktop.NetworkManager.wifi.share.protected",
"org.freedesktop.NetworkManager.settings.modify.system"
];
if (allowed.indexOf(action.id) !== -1 && subject.isInGroup("netdev")) {
return polkit.Result.YES;
}
});
EOFsudo tee /etc/systemd/system/ble-wifi-setup.service << 'EOF'
[Unit]
Description=BLE WiFi Push Scanner
After=bluetooth.target
Requires=bluetooth.target
[Service]
ExecStart=/usr/local/bin/wifi-scanner
User=USER
Group=USER
SupplementaryGroups=bluetooth netdev
Environment=WIFI_PUSH_AUTHORIZED_KEYS=/home/USER/.ssh/authorized_keys
RuntimeDirectory=wifi-push
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOFReplace USER with your username on the Pi.
sudo systemctl daemon-reload
sudo systemctl enable --now ble-wifi-setup.serviceOn Mac:
# Push WiFi to all Pi devices in range (password will be prompted)
./wifi-push --ssid "HomeNetwork"
# Via environment variable
WIFI_PUSH_PASSWORD="secret123" ./wifi-push --ssid "HomeNetwork"
# With specific key and duration
./wifi-push --ssid "Office" --key ~/.ssh/id_rsa --duration 60Broadcasts a BLE beacon for 120 seconds (default). All Pi devices in range will pick up the settings.
The scanner operates in two modes:
| WiFi state | Scanner behavior |
|---|---|
| Connected | Sleeping — BLE radio off, CPU ~0% |
| Disconnected | Actively scanning BLE every 10 seconds |
WiFi check occurs every --scan-interval seconds (default 10). If booted without WiFi — scanner starts BLE scan immediately.
Both wifi-scanner and wifi-push handle SIGTERM/SIGINT for graceful shutdown (clean BLE disconnect, D-Bus cleanup).
Resources on Pi Zero 2W: ~12 MB RAM, ~0% CPU in idle mode.
wifi-push [flags]
--ssid STRING WiFi SSID (required)
--password STRING WiFi password (or WIFI_PUSH_PASSWORD env; otherwise — prompt)
--key PATH path to SSH private key (default ~/.ssh/id_rsa)
--duration N broadcast duration in seconds (default 120, range 10-3600)
wifi-scanner [flags]
--authorized-keys PATH path to authorized_keys (default ~/.ssh/authorized_keys)
--scan-interval N seconds between scans (default 10, range 1-300)
Environment variable WIFI_PUSH_AUTHORIZED_KEYS is also supported.
- Service UUID:
a1b2c3d4-e5f6-7890-abcd-ef1234567890 - Handshake UUID:
a1b2c3d4-e5f6-7890-abcd-ef1234567892(write — Pi → Mac) - Payload UUID:
a1b2c3d4-e5f6-7890-abcd-ef1234567891(read — Mac → Pi) - Advertised name:
wifi-push
Handshake (Pi → Mac): [32B X25519 ephemeral public key]
Encrypted payload (Mac → Pi, max 16 KiB):
[32B mac_eph_pub][12B nonce][2B ct_len][AES-GCM ciphertext+tag][2B sig_len][signature]
Signature covers: mac_eph_pub + nonce + ciphertext (encrypt-then-sign).
HKDF: SHA-256, salt=nil, info=wifi-push-v2.
cmd/
wifi-push/main.go — Mac CLI: key loading, BLE peripheral
wifi-scanner/main.go — Pi CLI: key loading, BLE scanner
internal/
ble/scanner.go — BLE scanner orchestrator (Pi, BlueZ D-Bus)
ble/connection.go — BLE GATT connect + read logic
ble/signals.go — D-Bus signal handling
ble/wificheck.go — WiFi state checking (nmcli)
bluez/adapter.go — shared BlueZ adapter discovery (D-Bus)
bluez/constants.go — shared BlueZ/D-Bus constants
config/constants.go — protocol constants and payload sizes
config/validate.go — SSID/PSK validation + conn-name sanitization
crypto/ecdh.go — X25519 ECDH + HKDF + AES-256-GCM encrypt/decrypt
crypto/sign.go — RSA / Ed25519 signing
crypto/verify.go — RSA / Ed25519 signature verification
keys/authorized_keys.go — SSH authorized_keys parsing
keys/private_key.go — SSH private key loading
keys/trusted_key.go — TrustedKey interface + implementations
protocol/protocol.go — payload parsing + decryption (Pi)
protocol/encrypt.go — payload assembly + encryption (Mac)
push/push.go — shared types
push/peripheral_linux.go — BLE peripheral via BlueZ D-Bus
push/peripheral_darwin.go — BLE peripheral via CoreBluetooth (CGo)
push/peripheral_darwin.m — Objective-C CoreBluetooth wrapper
push/handshake.go — handshake processing helpers
push/ratelimit.go — per-device BLE handshake rate limiting
wifi/wifi.go — WifiApplier interface
wifi/nmcli.go — NmcliApplier: WiFi setup via nmcli
# Logs
sudo journalctl -u ble-wifi-setup -f
# Status (shows sleeping/scanning)
sudo systemctl status ble-wifi-setup
# Restart
sudo systemctl restart ble-wifi-setup
# Stop
sudo systemctl stop ble-wifi-setup| Problem | Solution |
|---|---|
No valid public keys found |
Check the path to authorized_keys and that ssh-copy-id was run |
No Bluetooth adapter found |
sudo systemctl restart bluetooth + sudo rfkill unblock bluetooth |
br-connection-profile-unavailable |
Set ControllerMode = le in /etc/bluetooth/main.conf |
Insufficient privileges |
Add the polkit rule (step 4) and make sure the user is in the netdev group |
| WiFi not connecting | nmcli device wifi list — check if Pi can see the network |
timeout waiting for services |
Restart bluetooth and ble-wifi-setup |
| Scanner doesn't start after reboot | Verify: systemctl is-enabled ble-wifi-setup = enabled |
- BLE range ~10m
wifi-pushon macOS requires CGo (Xcode Command Line Tools)- RSA-4096 signature ~512 bytes — requires MTU negotiation (CoreBluetooth handles this automatically)