pfSense Gaming NAT Setup, Updated for 2.9.0

Back in 2022 I wrote up how I got open NAT working for my kids' gaming PCs/consoles on pfSense. That post is still getting hits, but pfSense has moved on since then — this is the same goal, updated for pfSense CE 2.9.0, including a couple of things that didn't exist back in 2022.

The short version of what changed: the UPnP service got renamed and rebuilt, and there's a genuinely new Outbound NAT option built specifically for the "multiple consoles fighting over NAT type" problem. Both are covered below.


1. NAT Reflection — System / Advanced / Firewall & NAT

Unchanged from the original post: head to the Network Address Translation section and leave NAT reflection disabled unless you have a specific reason to need it. This part of pfSense hasn't moved.


2. Create an alias for your gaming devices — Firewall / Aliases / IP

Also unchanged in concept: create an alias (mine's still called gamingPCs) containing the IPs of every gaming PC/console on the network. Give each device a static IP or a static DHCP reservation first — everything below depends on these addresses not changing.


3. Outbound NAT — the part that's actually new

This is the biggest change since the original post. pfSense 2.9.0 added an Endpoint-Independent "Port Restricted Cone" NAT option at Firewall > NAT > Outbound, and it changes what I'd recommend depending on how many gaming devices you have.

First, switch Outbound NAT mode to Hybrid (keeps automatic rules for everything else, manual rules for your gaming alias) and add a rule for your alias:

  • Interface: WAN
  • Source: your gamingPCs alias
  • Translation > Address: WAN address (this dropdown also lists your other interfaces/aliases — make sure you pick the WAN one, not LAN or another local interface)

Then pick one of two options for that rule, depending on your situation:

  • One console/PC: check Static Port. This preserves the source port across connections, which is what gets you Moderate/Type 2 NAT and lets most peer-to-peer game traffic through directly.
  • Multiple consoles/PCs: use the new Port Restricted Cone option (labeled "Enable EIM-NAT for UDP connections" in the rule editor) instead of Static Port. Static Port on multiple devices at once causes them to fight over the same external port; Port Restricted Cone keeps mappings consistent per-remote-host without that collision. This is exactly the scenario the original post ran into with more than one kid gaming at the same time.

Don't enable both Static Port and Port Restricted Cone on the same rule — they conflict. Netgate still flags this feature as experimental as of 2.9.0, so if you hit weirdness, that's a known state, not something you broke.


4. UPnP — now under a new name

The old Services > UPnP & NAT-PMP page from the original post is gone. As of recent pfSense versions the daemon was replaced, and the page is now Services > UPnP IGD & PCP. Functionally it does the same job (lets consoles/games open their own ports automatically), but the labels changed:

  • Enable port mapping service — the master on/off switch (was "Enable UPnP & NAT-PMP")
  • Allow UPnP IGD Port Mapping — classic UPnP, what most consoles use
  • Allow PCP/NAT-PMP Port Mapping — PCP and the older NAT-PMP standard
  • External Interface — your WAN
  • Internal Interfaces — LAN (or wherever your gaming devices live)

If you want to restrict which devices are allowed to create mappings, turn on Default Deny and add explicit allow rules in the permissions box, one line per device:

allow 1024-65535 <gaming-pc-ip> 1024-65535

You can check what's actually been opened at any given time under Status > UPnP IGD & PCP.


Putting it together

For most households, this is the combination that actually gets you consistent multiplayer connectivity in 2.9.0:

  • NAT reflection: off
  • gamingPCs alias with every device's static IP
  • Hybrid Outbound NAT rule for that alias — Static Port for a single device, Port Restricted Cone for multiple
  • UPnP IGD & PCP enabled on LAN, optionally restricted to just the gaming alias via Default Deny

One thing that hasn't changed since 2022: don't chase "Open NAT" (Type 1) on purpose. It's the least secure NAT type and exposes your devices directly — Moderate (Type 2/B) via the steps above is what actually fixes matchmaking problems for the vast majority of games, and console vendors treat it as the target, not Open.

Hope this saves someone the trouble of hunting through forum threads like I had to the first time around.


Further reading: pfSense's own gaming configuration guide and the Outbound NAT documentation cover console-specific quirks (Switch, Xbox, PlayStation) in more depth than fits here.

Building a Redundant Active Directory: A Second DC, Then a Clean Rebuild on Debian 13

This is a write-up of rebuilding my home lab's Active Directory to be actually redundant: adding a second domain controller, then rebuilding the original DC from scratch on Debian 13 rather than doing an in-place upgrade. What follows is what worked, and more usefully, the gotchas that ate most of the time.

Why bother

The real trigger was unrelated to AD: one of my Proxmox hosts needed to move to PVE9, but it was the sole host running my domain controller VM. Rebooting it for the upgrade would have taken down DNS/AD for the entire network, not just its own services. Rather than just live-migrating the DC somewhere else for the reboot, I decided it was time to actually have two DCs like a normal person.


Step 1: Bring up a second DC (dc2)

Cloned a Debian 13 cloud template, joined it to the domain as a full DC, copied sysvol over with tar --xattrs --acls, and ran ntacl sysvolreset. Pointed the router's secondary DNS entry at it. Two things broke immediately:

  • Samba's internal DNS server was IPv4-silent. It was binding only [::]:53 and then failing to bind 0.0.0.0:53 with NT_STATUS_ADDRESS_ALREADY_ASSOCIATED — a bind-order difference between the Samba version on the old DC and this new one. Fixed with explicit binding in smb.conf instead of relying on wildcard binds:
    interfaces = lo eth0
    bind interfaces only = yes
    I also disabled IPv6 entirely on this box rather than deal with dual-stack for now.
  • cloud-init's default /etc/hosts broke GSS-TSIG self-canonicalization. The stock 127.0.1.1 dc2 ... entry confuses Samba's DNS update auth. More on the actual fix for this below — the first thing I tried didn't survive a reboot.

Step 2: Rebuild the original DC as dc1

With a second healthy DC in place, I could safely rebuild the original DC (Debian 12, an older Samba version) instead of doing a risky in-place OS + 5-minor-version Samba upgrade. dc2 had already shaken out the whole recipe, so this rebuild carried a lot less risk than the alternative.

Built a new VM on a different host, Debian 13 cloud template, given a temporary IP so it wouldn't collide with the still-running original DC. Same package recipe as dc2, but with every fix already applied up front — no debugging needed this time. Joined it as a third DC, copied sysvol, ran ntacl sysvolreset.

Then transferred every FSMO role from the old DC (which held all seven, having been the only DC until dc2 joined) over to the new one, one at a time:

samba-tool fsmo transfer --role=SchemaMaster -U administrator
samba-tool fsmo transfer --role=NamingMaster -U administrator
samba-tool fsmo transfer --role=InfrastructureMaster -U administrator
samba-tool fsmo transfer --role=RidAllocationMaster -U administrator
samba-tool fsmo transfer --role=PdcEmulationMaster -U administrator
samba-tool fsmo transfer --role=DomainDnsZonesMaster -U administrator
samba-tool fsmo transfer --role=ForestDnsZonesMaster -U administrator

Once that was confirmed, the old DC was cleanly demoted, not just deleted:

samba-tool domain demote -U administrator

This matters — just deleting the VM leaves orphaned NTDS Settings / connection-object metadata behind in AD, the same kind of stale crumb I'd hit earlier from an aborted join attempt. After the demote, I confirmed it was actually clean: the old DC's name no longer resolved at all, no leftover deletion artifacts showed up in samba-tool drs showrepl on either remaining DC, and dbcheck --cross-ncs came back with 0 errors on both.

The old DC's VM was powered off (not deleted, yet) once the new one was verified.


Step 3: Move the new DC onto the old DC's IP

Rather than pick a new address for the rebuilt DC and have to update every host's resolver and the router's DNS config, I moved it onto the original DC's old IP. This is where most of the actual pain was.

  • cloud-init only applies netplan config once per instance-id. Changing the VM's IP config at the hypervisor level and rebooting does nothing — the guest just keeps its old address, since cloud-init already "did its job" for that instance. Had to edit /etc/netplan/50-cloud-init.yaml directly and run netplan apply (no reboot needed), then drop network: {config: disabled} into /etc/cloud/cloud.cfg.d/ so cloud-init wouldn't fight it on the next boot.
  • netplan's default link-local addressing quietly re-enables IPv6 per-interface, overriding the global net.ipv6.conf.all.disable_ipv6=1 sysctl. The per-interface sysctl gets reset independently. Needed link-local: [] in the netplan ethernets stanza — the global sysctl alone isn't enough.
  • Samba doesn't tolerate its bound IP disappearing out from under it. After the address change it failed with NT_STATUS_ADDRESS_NOT_ASSOCIATED and needed a manual systemctl restart samba-ad-dc.
  • A DC's own DNS record doesn't update itself when its IP changes — same broken self-registration path as the PTR issue below. Needed a manual delete/re-add of the A record, plus a fresh PTR record in the reverse zone.
  • Even after the DNS record was correct, samba-tool drs replicate kept failing with WERR_HOST_UNREACHABLE. Network and ports were fine. Turned out systemd-resolved's own cache on the peer DC — separate entirely from Samba's own DNS zone data — still had the old address cached. resolvectl flush-caches on the peer fixed it instantly. Any client with a cached lookup for a DC that changes IP needs the same treatment (one of my workstations had a stale entry too); otherwise it just self-expires on the zone's TTL eventually.
  • samba-tool dns add/delete over Kerberos can hang for a surprisingly long time (one case took over a minute) when a DC's own SRV record briefly points at an unreachable candidate first during a transition. Forcing NTLM and skipping KDC discovery avoids it:
    samba-tool dns add ... -U administrator -k no

Small bonus: getting both DCs onto matching Samba versions mostly fixed a separate samba_dnsupdate TSIG verification issue I'd been living with since dc2 first joined. Before, every dynamic DNS update failed with a TSIG verify failure. After, only one record still does (a low-impact SRV locator record) — added manually the same way, and not worth chasing further given how narrow it's become.


Step 4: Actually verify both DCs survive a reboot

Two fixes made earlier for dc2 looked right but turned out not to be durable. Only caught this by actually rebooting both DCs for real, not just re-applying config live.

  • manage_etc_hosts: false as a cloud-init drop-in never actually worked. The Debian 13 cloud template ships its own per-instance userdata that sets manage_etc_hosts: true, and that always wins over a system-level config drop-in in cloud-init's merge order. A reboot silently reverted /etc/hosts back to the default 127.0.1.1 entry every time. This broke the DC's own hostname resolution badly enough that DNS RPC calls against itself failed instantly with a transport-connection-refused error — which looks exactly like an auth problem, not a hosts-file problem, so it took a while to trace.

    The real fix was editing /etc/cloud/templates/hosts.debian.tmpl directly — this is the actual template that gets rendered fresh on every boot, not something userdata can override. Swapped the default 127.0.1.1 line for each DC's real static IP. Verified surviving an actual reboot on both boxes. Deliberately did not bake this into the base cloud template itself, since that would just hide the regression for the next person cloning it — it's a fast two-line fix to apply per-VM after cloning.

  • The IPv6 link-local fix from Step 3 also didn't originally exist on dc2 — it predated that discovery, and IPv6 quietly came back after a reboot test. Applied and reboot-verified there too.

End state

Two Debian 13 DCs, matching Samba versions, replication and dbcheck clean on both. The new DC holds all seven FSMO roles. The original DC's VM is powered off but not yet deleted, kept around until I'm confident nothing else is still expecting it to exist.

Notes / things I'd tell past-me

  • If you're about to do a risky in-place upgrade on your only DC, stop and build a second DC first. It turns "one scary in-place upgrade" into "one already-debugged rebuild with a safety net."
  • Demote, don't delete. A raw VM delete leaves orphaned AD metadata behind that's annoying to clean up later.
  • cloud-init's per-instance netplan/hosts state is stickier than it looks — both the network config and the hosts file need per-VM overrides that survive a real reboot, and the only way to be sure is to actually reboot and check, not just re-apply config live.
  • DNS has more caches than you think: the AD-integrated zone itself, plus systemd-resolved's cache on every peer, plus every client's own resolver cache. An IP move isn't done until all of them agree.

Fedora KDE: Installing the xone Driver for the Xbox Wireless Adapter

These are the steps that worked for getting the Microsoft Xbox Wireless Adapter running on Fedora KDE using the xone driver.

Use case

This is for:

Xbox One / Xbox Series controller
→ Microsoft Xbox Wireless Adapter USB dongle
→ Fedora KDE
→ xone driver

This is not the Bluetooth setup. For Bluetooth, xpadneo is usually the better driver.


1. Enable the xone COPR repository

sudo dnf copr enable sentry/xone
sudo dnf install -y xone akmod-xone

The packages installed were:

xone-1:0.5.8-1.fc44.x86_64
akmod-xone-1:0.5.8-1.fc44.x86_64

One thing to note: on Fedora, the xone package did not include the firmware helper script.

Checking the installed files showed:

rpm -ql xone
/usr/lib/modprobe.d/60-xone.conf
/usr/lib/modules-load.d/xone.conf
/usr/share/licenses/xone
/usr/share/licenses/xone/LICENSE

And:

rpm -ql akmod-xone
/usr/src/akmods/xone-kmod-0.5.8-1.fc44.src.rpm
/usr/src/akmods/xone-kmod.latest

So the COPR package installed the akmod source and config, but not the firmware download helper.


2. Build the xone kernel modules

sudo akmods --force
sudo dracut --force

Verify the modules were built:

find /usr/lib/modules/$(uname -r) -iname '*xone*'

Working output looked like this:

/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone
/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone/xone_dongle.ko.xz
/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone/xone_gip.ko.xz
/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone/xone_gip_chatpad.ko.xz
/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone/xone_gip_gamepad.ko.xz
/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone/xone_gip_headset.ko.xz
/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone/xone_gip_madcatz_glam.ko.xz
/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone/xone_gip_madcatz_strat.ko.xz
/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone/xone_gip_pdp_jaguar.ko.xz
/usr/lib/modules/7.0.14-201.fc44.x86_64/extra/xone/xone_wired.ko.xz

3. Install the wireless dongle firmware

The driver loaded, but the Xbox wireless adapter needed firmware. Since the Fedora package did not include the helper script, the firmware was installed using the upstream xone repository.

Install the requirements:

sudo dnf install -y git cabextract

Clone the upstream repo:

cd /tmp
git clone https://github.com/medusalix/xone.git

The firmware installer was present in the cloned repo, but it needed executable permissions:

chmod +x /tmp/xone/install/firmware.sh

Then run it:

/tmp/xone/install/firmware.sh

Successful output looked like:

The firmware for the wireless dongle is subject to Microsoft's Terms of Use:
https://www.microsoft.com/en-us/legal/terms-of-use

Press enter to continue!

driver.cab: WARNING; possible 16256 extra bytes at end of file.
Extracting cabinet: driver.cab
  extracting FW_ACC_00U.bin

All done, no errors.
FW_ACC_00U.bin: OK

4. Fix the firmware filename expected by the driver

After installing the firmware, the module still logged this error:

xone-dongle 3-5:1.0: Direct firmware load for xone_dongle_02e6.bin failed with error -2

The -2 means “file not found.” The firmware installer downloaded:

FW_ACC_00U.bin

But the driver expected:

xone_dongle_02e6.bin

Check the firmware files:

sudo find /lib/firmware /usr/lib/firmware -iname '*xone*' -o -iname 'FW_ACC_00U.bin'

The working system had:

/lib/firmware/FW_ACC_00U.bin
/lib/firmware/xone_dongle_02e6.bin
/usr/lib/firmware/FW_ACC_00U.bin
/usr/lib/firmware/xone_dongle_02e6.bin

If xone_dongle_02e6.bin is missing, copy the downloaded firmware to the name the driver expects:

sudo install -D -m 0644 /lib/firmware/FW_ACC_00U.bin /lib/firmware/xone_dongle_02e6.bin
sudo install -D -m 0644 /usr/lib/firmware/FW_ACC_00U.bin /usr/lib/firmware/xone_dongle_02e6.bin

Then relabel for SELinux:

sudo restorecon -v /lib/firmware/xone_dongle_02e6.bin /usr/lib/firmware/xone_dongle_02e6.bin

5. Reload the xone modules

Unplug the Xbox wireless adapter first.

Then unload the modules:

sudo modprobe -r \
  xone_dongle \
  xone_gip_headset \
  xone_gip_gamepad \
  xone_gip_madcatz_strat \
  xone_gip_chatpad \
  xone_wired \
  xone_gip

Reload the dongle module:

sudo modprobe xone_dongle

Plug the dongle back in.


6. Verify the driver

Check loaded modules:

lsmod | grep xone

Working output looked like:

xone_gip_headset       28672  0
xone_dongle            69632  0
cfg80211             1609728  1 xone_dongle
xone_wired             24576  0
xone_gip_gamepad       20480  0
xone_gip_madcatz_strat 12288  0
xone_gip_chatpad       12288  0
xone_gip               81920  6 xone_dongle,xone_gip_gamepad,xone_gip_madcatz_strat,xone_wired,xone_gip_chatpad,xone_gip_headset
ff_memless             24576  1 xone_gip_gamepad

Check the kernel log:

dmesg | grep -iE 'xone|xbox|gip|firmware' | tail -50

Before the firmware fix, the log showed:

Direct firmware load for xone_dongle_02e6.bin failed with error -2

After installing the firmware under the expected name and reloading the module, the new firmware-missing error should no longer appear.


7. Optional controller test

Install joystick tools:

sudo dnf install -y joystick

Check joystick devices:

ls -l /dev/input/js*

Test the controller:

jstest --event /dev/input/js0

If /dev/input/js0 is not the controller, check the other /dev/input/js* devices.


Notes

  • The xone modules are out-of-tree modules, so Fedora may log a kernel taint message. That is expected.
  • If Secure Boot is enabled, unsigned akmod modules may fail to load unless signed/enrolled.
  • The Fedora COPR package handled the kernel modules, but the firmware helper had to be run from the upstream repo.
  • The important firmware filename for this adapter was xone_dongle_02e6.bin.

Getting the Original Xbox One Wireless Adapter Working on CachyOS

I ran into a weird issue getting the original large USB Wireless Adapter for Xbox One controllers working under CachyOS. The controller worked fine over Bluetooth, but I wanted to use the Microsoft USB wireless adapter instead.

This write-up covers what finally worked for me, including the issue where the adapter was being grabbed by the wrong Linux driver.

This is specifically for the original/fat Xbox One Wireless Adapter, not the smaller second-generation Xbox wireless adapter.


Hardware involved

The adapter shows up as:

045e:02e6 Microsoft Corp. XBOX ACC

In dmesg, it appears as:

Product: XBOX ACC
Manufacturer: Microsoft Inc.

The short version

The working setup was:

  • Install xone-dkms
  • Install the Xbox wireless dongle firmware
  • Make sure DKMS builds against the correct CachyOS kernel headers
  • Prevent mt76x2u from claiming the original Xbox adapter
  • Force xone_dongle to load at boot
  • Disable USB autosuspend for the adapter

Install the required packages

First install DKMS, joystick testing tools, and the matching CachyOS kernel headers.

sudo pacman -S --needed dkms linux-cachyos linux-cachyos-headers joystick jstest-gtk

If you are using a different CachyOS kernel, install the matching headers instead. For example:

sudo pacman -S --needed linux-cachyos-lts-headers
sudo pacman -S --needed linux-cachyos-bore-headers

Check your running kernel with:

uname -r

Then install xone and the dongle firmware from the AUR:

yay -S xone-dkms xone-dongle-firmware

Check DKMS status:

dkms status

A good result looks something like:

xone/0.5.8, 7.0.10-1-cachyos, x86_64: installed

Important: install the correct headers

One mistake I made was installing the stock Arch linux-headers package while actually booted into the CachyOS kernel.

That caused an error like this:

ERROR: Missing 7.0.10-arch1-1 kernel modules tree for module xone/0.5.8.

The fix was to remove the stock Arch headers and use the CachyOS headers instead:

sudo pacman -Rns linux-headers
sudo pacman -S --needed linux-cachyos linux-cachyos-headers dkms
sudo dkms autoinstall -k "$(uname -r)"

Then verify:

dkms status

The confusing part: there is no xone module

After DKMS showed xone as installed, this command still failed:

sudo modprobe xone

With:

modprobe: FATAL: Module xone not found

That is because the package is called xone, but the actual modules have names like:

xone_dongle
xone_gip
xone_gip_gamepad
xone_wired

You can confirm the installed modules with:

find "/lib/modules/$(uname -r)" -type f \( -iname '*xone*' -o -iname '*gip*' -o -iname '*xow*' \)

Load the dongle driver with:

sudo modprobe xone_dongle

The actual problem: mt76x2u was stealing the adapter

The original Xbox One wireless adapter can be incorrectly claimed by the Linux mt76x2u driver. Internally, this adapter behaves enough like a MediaTek wireless device that the kernel can attach the wrong driver first.

The bad log looked like this:

usb 3-5: New USB device found, idVendor=045e, idProduct=02e6
usb 3-5: Product: XBOX ACC
usb 3-5: Manufacturer: Microsoft Inc.
mt76x2u 3-5:1.0: ASIC revision: 76320044
mt76x2u 3-5:1.0: Firmware Version: 0.0.00

That meant the Xbox adapter was being handled by mt76x2u instead of xone_dongle.

The working module state should look more like this:

xone_gip_gamepad
xone_dongle
xone_gip

And mt76x2u should not be loaded for this adapter.


Temporary fix

Unplug the Xbox wireless adapter first.

Then run:

sudo modprobe -r mt76x2u 2>/dev/null
sudo modprobe xone_dongle

Plug the adapter back in.

Then pair the controller using the physical buttons:

  1. Press the pairing button on the Xbox USB wireless adapter.
  2. Hold the pairing button on the Xbox controller.
  3. Wait for the light to go solid.

Do not pair through KDE Bluetooth when using the USB adapter.

Test the controller with:

jstest-gtk

Or:

ls /dev/input/js*
jstest /dev/input/js0

Permanent fix: blacklist mt76x2u

Create a blacklist file:

sudo vim /etc/modprobe.d/blacklist-xbox-one-wireless-adapter.conf

Add:

# Original/fat Xbox One Wireless Adapter can be incorrectly claimed by mt76x2u.
# Let xone_dongle handle it instead.
blacklist mt76x2u

Then rebuild initramfs:

sudo mkinitcpio -P

Reboot:

sudo reboot

After reboot, check:

lsmod | grep -E 'xone|mt76x2u'

Expected good result:

xone_gip_gamepad
xone_dongle
xone_gip

There should be no mt76x2u listed.

Warning

Blacklisting mt76x2u can disable USB Wi-Fi adapters that use that MediaTek driver. If you do not use a MediaTek USB Wi-Fi dongle, this should be harmless.


Force xone_dongle to load at boot

I also created a modules-load file so the Xbox dongle driver loads at boot.

sudo vim /etc/modules-load.d/xone.conf

Add:

xone_dongle

Then rebuild initramfs again:

sudo mkinitcpio -P

Reboot and verify:

lsmod | grep -E 'xone|mt76x2u'

USB port weirdness after reboot

After getting the driver issue fixed, I still noticed that the adapter sometimes only worked after moving it to a different USB port.

The bad log looked like this:

xone-dongle 3-5:1.0: xone_mt76_write_register: control message failed: -121
xone-dongle 3-5:1.0: xone_dongle_pairing_timeout: disable pairing failed: -19

Then after moving USB ports, it worked:

input: Microsoft Xbox Controller

That suggested the driver was working, but the USB port/device state was getting wedged.


Disable USB autosuspend for the adapter

Create a udev rule:

sudo vim /etc/udev/rules.d/99-xbox-one-wireless-adapter.rules

Add:

# Original/fat Xbox One Wireless Adapter: Microsoft XBOX ACC 045e:02e6
# Prevent USB autosuspend/reset weirdness with xone_dongle.
ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="045e", ATTR{idProduct}=="02e6", TEST=="power/control", ATTR{power/control}="on"

Reload udev rules:

sudo udevadm control --reload-rules
sudo udevadm trigger

Unplug and replug the adapter.

Verify the power setting:

for d in /sys/bus/usb/devices/*; do
  if [ -f "$d/idVendor" ] && [ -f "$d/idProduct" ]; then
    if grep -q 045e "$d/idVendor" && grep -q 02e6 "$d/idProduct"; then
      echo "$d"
      cat "$d/power/control"
    fi
  fi
done

Expected:

on

Recommended physical USB setup

For this original adapter, I would avoid:

  • Front-panel USB ports
  • Unpowered USB hubs
  • Ports shared with RGB controllers or audio devices
  • Ports that seem to power-save or reset oddly

Use a rear motherboard USB-A port if possible. If one port consistently works after reboot, leave the adapter there.


Optional recovery script

If the adapter gets wedged, this script reloads the relevant modules.

Create the script:

sudo vim /usr/local/bin/xbox-dongle-reset

Add:

#!/usr/bin/env bash
set -euo pipefail

echo "Unloading xone modules..."
sudo modprobe -r xone_gip_gamepad xone_dongle xone_gip 2>/dev/null || true

echo "Unloading conflicting mt76x2u if present..."
sudo modprobe -r mt76x2u 2>/dev/null || true

sleep 1

echo "Loading xone_dongle..."
sudo modprobe xone_dongle

echo "Done. Now unplug/replug the Xbox wireless adapter if it does not reconnect automatically."

Make it executable:

sudo chmod +x /usr/local/bin/xbox-dongle-reset

Run it with:

xbox-dongle-reset

Useful troubleshooting commands

Check USB detection:

lsusb | grep -i -E 'xbox|microsoft|045e'

Check loaded modules:

lsmod | grep -E 'xone|mt76|xpad'

Check kernel logs:

sudo dmesg | grep -i -E 'xbox|xone|gip|045e|02e6|mt76|firmware' | tail -120

Check input devices:

ls /dev/input/js*

Check whether Linux sees the controller:

grep -i -A8 -B2 'xbox\|microsoft\|xone' /proc/bus/input/devices

Test input:

jstest-gtk

Final working state

The final working setup had:

xone_gip_gamepad
xone_dongle
xone_gip

No mt76x2u grabbing the adapter.

The adapter showed as:

045e:02e6 Microsoft Corp. XBOX ACC

And once paired successfully, dmesg showed:

input: Microsoft Xbox Controller

At that point the Xbox controller worked normally through the original USB wireless adapter on CachyOS.