Minimal disposable Wi-Fi hotspot from Qubes OS

Original forum link
https://forum.qubes-os.org/t/36514
Original poster
MellowPoison
Editors
MellowPoison
Created at
2025-10-03 16:27:05
Last wiki edit
2025-10-06 07:11:06
Revisions
1 revision
Posts count
1
Likes count
5
Tags
configuration, networking

This guide is for creating a minimal disposable WiFi hotspot qube.

Note: You can paste the clipboard content into the xterm using Shift+Insert shortcut or by clicking on the middle mouse button inside the xterm window.

Install the debian-13-minimal template using the “Qubes Template Manager” tool if you don’t have it this template. Update the debian-13-minimal template using the “Qubes Update” tool. Clone the debian-13-minimal and name it d13m-hotspot.

Start the d13m-hotspot qube and open its root terminal using this command in dom0 terminal:

qvm-run -u root `d13m-hotspot` xterm &
Run the following commands in the d13m-hotspot xterm root terminal:
apt -y install qubes-core-agent-networking qubes-usb-proxy dnsmasq hostapd dunst
sed -i 's/\(^[ \t]*font[ \t]*=\)\(.*\)/\1 Sans 12/' /etc/xdg/dunst/dunstrc
systemctl mask dnsmasq hostapd
mkdir -p /opt/hotspot
python3 -c 'import urllib.request
urllib.request.install_opener(urllib.request.build_opener(urllib.request.ProxyHandler({"https": "127.0.0.1:8082"})))
urllib.request.urlretrieve("https://github.com/morrownr/USB-WiFi/raw/refs/heads/main/home/AP_Mode/hostapd-WiFi4.conf", "/etc/hostapd/hostapd-WiFi4.conf")
urllib.request.urlretrieve("https://github.com/morrownr/USB-WiFi/raw/refs/heads/main/home/AP_Mode/hostapd-WiFi5.conf", "/etc/hostapd/hostapd-WiFi5.conf")
urllib.request.urlretrieve("https://github.com/morrownr/USB-WiFi/raw/refs/heads/main/home/AP_Mode/hostapd-WiFi6.conf", "/etc/hostapd/hostapd-WiFi6.conf")
urllib.request.urlretrieve("https://github.com/morrownr/USB-WiFi/raw/refs/heads/main/home/AP_Mode/hostapd-WiFi7.conf", "/etc/hostapd/hostapd-WiFi7.conf")'
sed -i '/^bridge=/d' /etc/hostapd/hostapd-WiFi*.conf
cat << 'EOF' | sudo tee /etc/dnsmasq.d/hotspot.conf > /dev/null
interface=wlan0
dhcp-range=10.42.0.2,10.42.0.200,255.255.255.0,24h
EOF
cat << 'EOF' | tee /etc/udev/rules.d/99-wireless-detect.rules > /dev/null
SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", ACTION=="move", RUN+="/opt/hotspot/wifi-hotspot-add.sh '%E{INTERFACE}'"
SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", ACTION=="remove", RUN+="/opt/hotspot/wifi-hotspot-remove.sh '%E{INTERFACE}'"
EOF
cat << 'EOF' | tee /opt/hotspot/wifi-hotspot-add.sh > /dev/null
#!/bin/sh
if [ "$(qubesdb-read /type)" = "DispVM" ]; then
    WIFI_INTERFACE="$1"
    WIFI_SSID="$(qubesdb-read /vm-config/wifi-ssid)"
    WIFI_PASS="$(qubesdb-read /vm-config/wifi-pass)"
    WIFI_GEN="$(qubesdb-read /vm-config/wifi-gen)"
    WIFI_CHANNEL="$(qubesdb-read /vm-config/wifi-channel)"
    WIFI_COUNTRY="$(qubesdb-read /vm-config/wifi-country-code)"
    WIFI_BSSID="$(qubesdb-read /vm-config/wifi-bssid)"
    WIFI_BSSID_RND="$(dd if=/dev/urandom bs=1024 count=1 2>/dev/null | md5sum | sed -e 's/^\(.\)\(..\)\(..\)\(..\)\(..\)\(..\).*$/\1a:\2:\3:\4:\5:\6/')"
    if [ -n "$WIFI_SSID" ]; then
        case $WIFI_GEN in
            [4-7])
                ln -sf /etc/hostapd/hostapd-WiFi"$WIFI_GEN".conf /etc/hostapd/hostapd.conf
                sed -i 's/\(^ssid=\)\(.*\)/\1'"$(echo "$WIFI_SSID" | sed -e 's/[\/&]/\\&/g')"'/' /etc/hostapd/hostapd.conf
                if [ -n "$WIFI_PASS" ]; then
                    sed -i 's/\(^wpa_passphrase=\)\(.*\)/\1'"$(echo "$WIFI_PASS" | sed -e 's/[\/&]/\\&/g')"'/' /etc/hostapd/hostapd.conf
                else
                    sed -i 's/\(^wpa=\)\(.*\)/\10/' /etc/hostapd/hostapd.conf
                fi
                if [ -n "$WIFI_COUNTRY" ]; then
                    country_codes_2016="AF\nAX\nAL\nDZ\nAS\nAD\nAO\nAI\nAQ\nAG\nAR\nAM\nAW\nAU\nAT\nAZ\nBS\nBH\nBD\nBB\nBY\nBE\nBZ\nBJ\nBM\nBT\nBO\nBQ\nBA\nBW\nBV\nBR\nIO\nBN\nBG\nBF\nBI\nKH\nCM\nCA\nCV\nKY\nCF\nTD\nCL\nCN\nCX\nCC\nCO\nKM\nCG\nCD\nCK\nCR\nCI\nHR\nCU\nCW\nCY\nCZ\nDK\nDJ\nDM\nDO\nEC\nEG\nSV\nGQ\nER\nEE\nET\nFK\nFO\nFJ\nFI\nFR\nGF\nPF\nTF\nGA\nGM\nGE\nDE\nGH\nGI\nGR\nGL\nGD\nGP\nGU\nGT\nGG\nGN\nGW\nGY\nHT\nHM\nVA\nHN\nHK\nHU\nIS\nIN\nID\nIR\nIQ\nIE\nIM\nIL\nIT\nJM\nJP\nJE\nJO\nKZ\nKE\nKI\nKP\nKR\nKW\nKG\nLA\nLV\nLB\nLS\nLR\nLY\nLI\nLT\nLU\nMO\nMK\nMG\nMW\nMY\nMV\nML\nMT\nMH\nMQ\nMR\nMU\nYT\nMX\nFM\nMD\nMC\nMN\nME\nMS\nMA\nMZ\nMM\nNA\nNR\nNP\nNL\nNC\nNZ\nNI\nNE\nNG\nNU\nNF\nMP\nNO\nOM\nPK\nPW\nPS\nPA\nPG\nPY\nPE\nPH\nPN\nPL\nPT\nPR\nQA\nRE\nRO\nRU\nRW\nBL\nSH\nKN\nLC\nMF\nPM\nVC\nWS\nSM\nST\nSA\nSN\nRS\nSC\nSL\nSG\nSX\nSK\nSI\nSB\nSO\nZA\nGS\nSS\nES\nLK\nSD\nSR\nSJ\nSZ\nSE\nCH\nSY\nTW\nTJ\nTZ\nTH\nTL\nTG\nTK\nTO\nTT\nTN\nTR\nTM\nTC\nTV\nUG\nUA\nAE\nGB\nUS\nUM\nUY\nUZ\nVU\nVE\nVN\nVG\nVI\nWF\nEH\nYE\nZM\nZW\n"
                    if echo -ne $country_codes_2016 | grep -q -x -F "$WIFI_COUNTRY"; then
                        sed -i 's/\(^country_code=\)\(.*\)/\1'"$(echo "$WIFI_COUNTRY" | sed -e 's/[\/&]/\\&/g')"'/' /etc/hostapd/hostapd.conf
                    else
                        logger -t "WIFI_HOTSPOT" "WiFi hotspot start failed: Country code is invalid."
                        nohup sh -c '/opt/hotspot/notify.sh "WiFi hotspot" "WiFi hotspot start failed: Country code is invalid."' >/dev/null 2>&1 &
                        exit 1
                    fi
                else
                    sed -i '/^country_code=/d' /etc/hostapd/hostapd.conf
                fi
                if [ -n "$WIFI_CHANNEL" ]; then
                    sed -i 's/\(^channel=\)\(.*\)/\1'"$(echo "$WIFI_CHANNEL" | sed -e 's/[\/&]/\\&/g')"'/' /etc/hostapd/hostapd.conf
                fi
                if [ "$WIFI_BSSID" = "random" ]; then
                    echo "bssid=$WIFI_BSSID_RND" >> /etc/hostapd/hostapd.conf
                elif [ -n "$WIFI_BSSID" ]; then
                    case "$WIFI_BSSID" in
                        **$(printf '\n')*)
                            logger -t "WIFI_HOTSPOT" "WiFi hotspot start failed: BSSID is invalid."
                            nohup sh -c '/opt/hotspot/notify.sh "WiFi hotspot" "WiFi hotspot start failed: BSSID is invalid."' >/dev/null 2>&1 &
                            exit 1
                            ;;
                        *)
                            if echo "$WIFI_BSSID" | grep -qEe '^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$'; then
                                echo "bssid=$WIFI_BSSID" >> /etc/hostapd/hostapd.conf
                            else
                                logger -t "WIFI_HOTSPOT" "WiFi hotspot start failed: BSSID is invalid."
                                nohup sh -c '/opt/hotspot/notify.sh "WiFi hotspot" "WiFi hotspot start failed: BSSID is invalid."' >/dev/null 2>&1 &
                                exit 1
                            fi
                            ;;
                    esac
                fi
                sed -i 's/\(^interface=\)\(.*\)/\1'"$WIFI_INTERFACE"'/' /etc/hostapd/hostapd.conf /etc/dnsmasq.d/hotspot.conf
                if nft create chain ip qubes hotspot-input-$WIFI_INTERFACE >/dev/null 2>&1; then
                    nft add rule ip qubes custom-input jump hotspot-input-$WIFI_INTERFACE
                fi
                nft add rule ip qubes hotspot-input-$WIFI_INTERFACE iifname $WIFI_INTERFACE meta l4proto udp udp dport 67 accept
                nft add chain ip qubes hotspot-dnat-dns-$WIFI_INTERFACE '{ type nat hook prerouting priority dstnat - 1; policy accept; }' >/dev/null 2>&1
                nft add rule ip qubes hotspot-dnat-dns-$WIFI_INTERFACE iifname $WIFI_INTERFACE ip daddr 10.42.0.1 udp dport 53 dnat to 10.139.1.1
                nft add rule ip qubes hotspot-dnat-dns-$WIFI_INTERFACE iifname $WIFI_INTERFACE ip daddr 10.42.0.1 tcp dport 53 dnat to 10.139.1.1
                ip link set dev $WIFI_INTERFACE down
                if [ "$WIFI_BSSID" = "random" ]; then
                    ip link set dev $WIFI_INTERFACE address "$WIFI_BSSID_RND"
                elif [ -n "$WIFI_BSSID" ]; then
                    ip link set dev $WIFI_INTERFACE address "$WIFI_BSSID"
                fi
                ip link set dev $WIFI_INTERFACE up
                ip a add 10.42.0.1/24 dev $WIFI_INTERFACE
                systemctl enable --now dnsmasq hostapd
                touch /run/hostapd_configured
                nohup sh -c '/opt/hotspot/notify.sh "WiFi hotspot" "WiFi hotspot with SSID '"$WIFI_SSID"' started."' >/dev/null 2>&1 &
                ;;
            *)
                logger -t "WIFI_HOTSPOT" "WiFi hotspot start failed: WiFi generation is empty or invalid."
                nohup sh -c '/opt/hotspot/notify.sh "WiFi hotspot" "WiFi hotspot start failed: WiFi generation is empty or invalid."' >/dev/null 2>&1 &
                exit 1
        esac
    else
        logger -t "WIFI_HOTSPOT" "WiFi hotspot start failed: SSID is empty"
        nohup sh -c '/opt/hotspot/notify.sh "WiFi hotspot" "WiFi hotspot start failed: SSID is empty"' >/dev/null 2>&1 &
        exit 1
    fi
fi
EOF
chmod +x /opt/hotspot/wifi-hotspot-add.sh
cat << 'EOF' | tee /opt/hotspot/wifi-hotspot-remove.sh > /dev/null
#!/bin/sh
if [ "$(qubesdb-read /type)" = "DispVM" ] && [ -e /run/hostapd_configured ]; then
    WIFI_INTERFACE=$1
    WIFI_GEN=$(qubesdb-read /vm-config/wifi-gen)
    systemctl disable --now dnsmasq hostapd
    ip a del 10.42.0.1/24 dev $WIFI_INTERFACE
    nft flush chain ip qubes hotspot-input-$WIFI_INTERFACE >/dev/null 2>&1
    nft flush chain ip qubes hotspot-dnat-dns-$WIFI_INTERFACE >/dev/null 2>&1
    rm /run/hostapd_configured >/dev/null 2>&1
    nohup sh -c '/opt/hotspot/notify.sh "WiFi hotspot" "WiFi hotspot stopped."' >/dev/null 2>&1 &
fi
EOF
chmod +x /opt/hotspot/wifi-hotspot-remove.sh
cat << 'EOF' | tee /opt/hotspot/notify.sh > /dev/null
#!/bin/sh
notify()
{
    #Detect the name of the display in use
    local display=":$(ls /tmp/.X11-unix/* | sed 's#/tmp/.X11-unix/X##' | head -n 1)"

    #Detect the user using such display
    local user=$(who | grep '('$display')' | awk '{print $1}' | head -n 1)

    #Detect the id of the user
    local uid=$(id -u $user)

    sudo -u $user DISPLAY=$display DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus systemctl --user start dunst
    sudo -u $user DISPLAY=$display DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$uid/bus dunstify "$@"
}

j=0
while [ $j -le 60 ]; do
    if ls /tmp/.X11-unix/* >/dev/null 2>1; then
        notify "$@"
        exit 0
    fi
    sleep 1
    j=$(( j + 1 ))
done
exit 1
EOF
chmod +x /opt/hotspot/notify.sh

Create a new app qube named sys-hotspot-dvm based on the d13m-hotspot template and with a net qube set to (none). Open the Qube Settings of the sys-hotspot-dvm qube and in the Advanced tab enable the Disposable template option, then press OK. Start the sys-hotspot-dvm qube and open its root terminal using this command in dom0 terminal:

qvm-run -u root sys-hotspot-dvm xterm &
Run the following commands in the sys-hotspot-dvm xterm root terminal:
mkdir -p /rw/config/rc.local.d/
cat << 'EOF' | tee /rw/config/rc.local.d/hotspot.rc > /dev/null
#!/bin/sh
if [ "$(qubesdb-read /type)" = "DispVM" ]; then
    systemctl unmask dnsmasq hostapd
    udevadm trigger --subsystem-match=net --action=move --property-match=DEVTYPE=wlan
fi
EOF
chmod +x /rw/config/rc.local.d/hotspot.rc

Create a new named disposable qube named sys-hotspot based on the sys-hotspot-dvm disposable template and with a net qube set to the one that you want for your client WiFi devices connected to the hotspot to use e.g. sys-firewall. Open the Qube Settings of the sys-hotspot qube and in the Advanced tab configure it as follows: - Initial memory: 350 MB - Include in memory balancing: disabled - Provides network: enabled - Virtualization - Mode: PVH if you only want to attach USB WiFi adapter or HVM if you want to attach PCI or USB adapter

If you want to use a PCI WiFi adapter, then in the Qube Settings -> Devices tab you need to connect your PCI WiFi adapter to the sys-hotspot qube.

Now you need to configure your sys-hotspot to use yur specific wireless settings: SSID/passphrase/etc. You need to configure this in dom0 terminal using the following commands:

Set the SSID of the WiFi hotspot:

qvm-features sys-hotspot vm-config.wifi-ssid 'MySSID'

Set the passphrase of the WiFi hotspot:

qvm-features sys-hotspot vm-config.wifi-pass 'MyWIFIPassword'

Set the WiFi generation of the WiFi hotspot:

qvm-features sys-hotspot vm-config.wifi-gen '4'
Supported values are: 4, 5, 6, 7. E.g. for the WiFi 7 hotspot set the value to 7. Make sure to select the WiFi generation that is supported by your WiFi adapter.

Set the channel of the WiFi hotspot:

qvm-features sys-hotspot vm-config.wifi-channel '6'
If the channel is not set or set to the empty value '', then the default channel from the hostapd config file will be used. You can try setting the channel to 0 or acs_survey to enable the Automatic Channel Selection, but it didn't work for my WiFi adapter, so it's untested.

Set the Country Code of the WiFi hotspot (in the ISO 3166-1 alpha-2 format):

qvm-features sys-hotspot vm-config.wifi-country-code 'US'
If the country code is not set or set to the empty value '', then the country_code will be removed from the hostapd config.

Set the BSSID and MAC address of the WiFi hotspot, supported options: - Use the default factory MAC address of the WiFi adapter:

qvm-features sys-hotspot vm-config.wifi-bssid ''

You may also need to edit the default /etc/hostapd/hostapd-WiFi*.conf files to configure them for your specific WiFi adapter.

You can persistently attach USB WiFi adapter to your WiFi hotspot qube so that it'll be attached automatically when you start the qube using this command in dom0 terminal:

qvm-usb attach --persistent sys-hotspot sys-usb:1-2.3
Change the sys-usb:1-2.3 to the correct BACKEND:DEVID of your USB adapter from the output of this command in dom0 terminal:
qvm-usb ls