Wi-Fi hotspot from Qubes OS

I found why it didn’t work, as I push the rules in rc.local when a wifi device is found, for some reasons the table is flushed by Qubes OS rules, I guess it’s why it’s better to use a dispatcher in Network Manager, however I have not been able to have it triggering the script for now.

Among weird stuff, Graphene OS does not see the Wi-Fi network when the qube is running Debian 12, but it sees and connects fine when the qube is running Fedora 42.

Whether it’s running Debian 12 or Fedora 42, it works fine with everything else but Graphene OS. :joy:

I forgot that you also need to make the script executable:

chmod +x /rw/bind-dirs/etc/NetworkManager/dispatcher.d/zz-hotspot

I figured that, but I dont understand in which case the script starts because we do nothing wth the WiFi device, we do not put it up.

The script is executed when the device will go up/down/(or other possible states).
Network manager should bring the interface up/down, but you need to set autoconnect for it:

nmcli dev wifi hotspot ifname "$WIFI_INTERFACE" con-name hotspot ssid "your_ssid_name_here" password "the_PSK_password"
nmcli connection modify hotspot connection.autoconnect yes

But in your version, where do you create the hotspot, still in rc.local and you defer the firewall changes to the dispatcher?

This configuration of disposable template works for me:

mkdir -p /rw/config/qubes-bind-dirs.d/
cat << 'EOF' | tee -a /rw/config/qubes-bind-dirs.d/50_user.conf > /dev/null
binds+=( '/etc/udev/rules.d/99-wireless-detect.rules' )
EOF
mkdir -p /rw/bind-dirs/etc/udev/rules.d/
cat << 'EOF' | tee /rw/bind-dirs/etc/udev/rules.d/99-wireless-detect.rules > /dev/null
SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", ACTION=="move", RUN+="/rw/config/wifi-hotspot-add.sh '%E{INTERFACE}'"
SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", ACTION=="remove", RUN+="/rw/config/wifi-hotspot-remove.sh '%E{INTERFACE}'"
EOF

cat << 'EOF' | tee /rw/config/wifi-hotspot-add.sh > /dev/null
#!/bin/sh
if [ "$(qubesdb-read /qubes-vm-persistence)" = "none" ]
then
    WIFI_INTERFACE=$1
    sed -i "s/^interface-name=.*/interface-name=$WIFI_INTERFACE/g" /rw/config/NM-system-connections/Hotspot.nmconnection
    nmcli conn reload
    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
fi
EOF
chmod +x /rw/config/wifi-hotspot-add.sh
cat << 'EOF' | tee /rw/config/wifi-hotspot-remove.sh > /dev/null
#!/bin/sh
if [ "$(qubesdb-read /qubes-vm-persistence)" = "none" ]
then
    WIFI_INTERFACE=$1
    if ! nft create chain ip qubes hotspot-input-$WIFI_INTERFACE >/dev/null 2>&1; then
        nft flush chain ip qubes hotspot-input-$WIFI_INTERFACE
    fi
fi
EOF
chmod +x /rw/config/wifi-hotspot-remove.sh

mkdir -p /rw/config/NM-system-connections
cat << 'EOF' | tee /rw/config/NM-system-connections/Hotspot.nmconnection > /dev/null
[connection]
id=Hotspot
type=wifi
autoconnect=true
interface-name=

[wifi]
mode=ap
ssid=your_ssid_name_here

[wifi-security]
group=ccmp;
key-mgmt=wpa-psk
pairwise=ccmp;
proto=rsn;
psk=the_PSK_password

[ipv4]
method=shared

[ipv6]
addr-gen-mode=default
method=ignore

[proxy]
EOF
chmod 600 /rw/config/NM-system-connections/Hotspot.nmconnection

mkdir -p /rw/config/qubes-bind-dirs.d/
cat << 'EOF' | tee -a /rw/config/qubes-bind-dirs.d/50_user.conf > /dev/null
binds+=( '/etc/NetworkManager/dispatcher.d/zz-hotspot' )
EOF
mkdir -p /rw/bind-dirs/etc/NetworkManager/dispatcher.d
cat << 'EOF' | tee /rw/bind-dirs/etc/NetworkManager/dispatcher.d/zz-hotspot  > /dev/null
#!/bin/sh
if [ "$(qubesdb-read /qubes-vm-persistence)" = "none" ]
then
    WIFI_INTERFACE=$1
    INTERFACE_IS_WIRELESS=$(cat /proc/net/wireless | grep $WIFI_INTERFACE)
    if [ -n "$INTERFACE_IS_WIRELESS" ] && [ "$2" = "up" ]; then
        nft flush chain ip qubes dnat-dns
        nft add rule ip qubes dnat-dns iifname $WIFI_INTERFACE ip daddr 10.42.0.1 udp dport 53 dnat to 10.139.1.1
        nft add rule ip qubes dnat-dns iifname $WIFI_INTERFACE ip daddr 10.42.0.1 tcp dport 53 dnat to 10.139.1.1
    fi
fi
EOF
chmod +x /rw/bind-dirs/etc/NetworkManager/dispatcher.d/zz-hotspot

On second thought, you can do it without dispatcher script:

mkdir -p /rw/config/qubes-bind-dirs.d/
cat << 'EOF' | tee -a /rw/config/qubes-bind-dirs.d/50_user.conf > /dev/null
binds+=( '/etc/udev/rules.d/99-wireless-detect.rules' )
EOF
mkdir -p /rw/bind-dirs/etc/udev/rules.d/
cat << 'EOF' | tee /rw/bind-dirs/etc/udev/rules.d/99-wireless-detect.rules > /dev/null
SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", ACTION=="move", RUN+="/rw/config/wifi-hotspot-add.sh '%E{INTERFACE}'"
SUBSYSTEM=="net", ENV{DEVTYPE}=="wlan", ACTION=="remove", RUN+="/rw/config/wifi-hotspot-remove.sh '%E{INTERFACE}'"
EOF

cat << 'EOF' | tee /rw/config/wifi-hotspot-add.sh > /dev/null
#!/bin/sh
if [ "$(qubesdb-read /qubes-vm-persistence)" = "none" ]
then
    WIFI_INTERFACE=$1
    sed -i "s/^interface-name=.*/interface-name=$WIFI_INTERFACE/g" /rw/config/NM-system-connections/Hotspot.nmconnection
    nmcli conn reload
    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
fi
EOF
chmod +x /rw/config/wifi-hotspot-add.sh
cat << 'EOF' | tee /rw/config/wifi-hotspot-remove.sh > /dev/null
#!/bin/sh
if [ "$(qubesdb-read /qubes-vm-persistence)" = "none" ]
then
    WIFI_INTERFACE=$1
    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
fi
EOF
chmod +x /rw/config/wifi-hotspot-remove.sh

mkdir -p /rw/config/NM-system-connections
cat << 'EOF' | tee /rw/config/NM-system-connections/Hotspot.nmconnection > /dev/null
[connection]
id=Hotspot
type=wifi
autoconnect=true
interface-name=

[wifi]
mode=ap
ssid=your_ssid_name_here

[wifi-security]
group=ccmp;
key-mgmt=wpa-psk
pairwise=ccmp;
proto=rsn;
psk=the_PSK_password

[ipv4]
method=shared

[ipv6]
addr-gen-mode=default
method=ignore

[proxy]
EOF
chmod 600 /rw/config/NM-system-connections/Hotspot.nmconnection

del forgot that the script is called only for the wireless interfaces

I’m not really familiar with UDEV rules, would it work if you start a HVM qube with a PCI wifi interface? It’s not moved nor removed at first glance.

It won’t work for PCI devices, because they are added before Qubes OS bind-mount will run.
But it’ll work if you add udev rules to the template.
Or you can try to use /rw/config/rc.local-early.d/ in the disposable template to configure udev rules:

mkdir -p /rw/config/rc.local-early.d/
cat << 'EOF' | tee /rw/config/rc.local-early.d/udev.rc > /dev/null
#!/bin/sh
ln -s /rw/bind-dirs/etc/udev/rules.d/99-wireless-detect.rules /etc/udev/rules.d/99-wireless-detect.rules
udevadm control --reload-rules
udevadm trigger
EOF

But this is untested and I’m not sure if udevadm control --reload-rules && udevadm trigger is needed.

Updated to:

mkdir -p /rw/config/rc.local-early.d/
cat << 'EOF' | tee /rw/config/rc.local-early.d/udev.rc > /dev/null
#!/bin/sh
ln -s /rw/bind-dirs/etc/udev/rules.d/99-wireless-detect.rules /etc/udev/rules.d/99-wireless-detect.rules
udevadm control --reload-rules
udevadm trigger
EOF

Although your solution is more elegant and certainly supports adding / removing devices without restarting the qube, I find it a lot more complex and fragile than my solution :thinking: What do you think?

On my side, I solved the DNS problem by creating a new custom-dnat-dns chain that has a higher priority than the default one, so I create the chain and its rules once and I’m done with it.

nft add chain ip qubes custom-dnat-dns '{ type nat hook prerouting priority -100; policy accept; }'
nft add rule  ip qubes custom-dnat-dns iifname "$WIFI_INTERFACE" ip daddr 10.42.0.1 udp dport 53 dnat to 10.139.1.1
nft add rule  ip qubes custom-dnat-dns iifname "$WIFI_INTERFACE" ip daddr 10.42.0.1 tcp dport 53 dnat to 10.139.1.1

Which part do you think could be fragile? Maybe I’d be able to fix it.
Or do you mean it could be fragile because it’s complex without excessive testing? That could be so.

Regarding your solution, I think you should fix this part:

This can create a race condition.
For example, if nmcli failed to configure the hotspot without sleep, then maybe you could parse the output of nmcli dev in an additional while true loop with sleep and wait for your interface to show up there.

priority -100 is the same as priority dstnat:
Netfilter hooks - nftables wiki
So the custom-dnat-dns chain would have the same priority as the dnat-dns chain.
But I think it doesn’t matter, because the rules in these chains won’t interfere with each other.
Note: priority dstnat - 1 == priority -101

Indeed

Right, I tried -200 first but it was not supported for nat, so I used -100 but -101 is accepted by nftables. But as you said, this does not even matter.

There are multiple parts:

  • udev rules
  • dispatcher
  • rc.local

This is not really tested too from my understanding. That’s why I say it’s fragile.

I need to figure what happens here, it does not always fail, I’m not even sure it’s useful. Do you know if the rc.local script has logs somewhere?

I’ve checked it and it doesn’t work, because the rc.local-early.d scripts are called too late.

There is no dispatched and no rc.local, only udev is used.
The downside is that you need to create udev rules in the template for PCI devices to work.
I’ll try to look into it, maybe I’ll find a solution.

You can do it like this:

{
# some commands that you want to run
# the stdout and stderr are redirected to syslog
} > >(logger -t $0) 2>&1

@MellowPoison : may I suggest you to open a new topic under “Community Guides”, call it “Wi-Fi hotspot from Qubes OS: the mellow way”, and have it separate from @solene 's thread? I’m serious. It’s hard to follow the back and forth, and it is not clear if all the suggestions are incorporated or not in the first post.

2 Likes

Sure, here it is:

Also, I’ve ended up doing it like this:

sudo mkdir -p /rw/config/rc.local.d/
cat << 'EOF' | sudo tee /rw/config/rc.local.d/udev.rc > /dev/null
#!/bin/sh
WIFI_INTERFACE=$(cat /proc/net/wireless | sed -n 3p | cut -d: -f1)
if [ -n "$WIFI_INTERFACE" ]; then
    /rw/config/wifi-hotspot-add.sh $WIFI_INTERFACE
fi
EOF
sudo chmod +x /rw/config/rc.local.d/udev.rc
1 Like

Hi, i created sys-hotspot HVM with Wifi from PCI. The script did not start the wifi hotspot on boot. I manually entered the command sudo nmcli dev wifi hotspot ifname <iface> ssid test-hotspot password Pass1234!

This started a hotspot but my GOS phone could not find it. Could not connect to hidden wifi either. A spare laptop was able to connect but no network access.

# On the client (phone, laptop) ip addr show # Gave an IP address ping -c 3 8.8.8.8 # Could not reach network

I am well regarded.

Any ideas?