TL;DR: USB-passing a Bluetooth adapter into a VM makes the chip drop its firmware, and the guest can only recover if it has the right driver and firmware blobs. Full distros often manage it; Home Assistant OS, ChimeraOS, Bazzite and other trimmed or immutable guests frequently don't, and Intel onboard combo chips (BE200, AX210, AX211) are the worst offenders. This tool skips the handoff entirely: the adapter stays on the host and is shared to the VM over the network - two commands, survives reboots. Try plain USB passthrough first, it's simpler and works for plenty of setups.
Pair your Xbox / PlayStation controller, headphones, or sensors inside your gaming VM (ChimeraOS, Bazzite, Home Assistant, plain Linux) - even with Bluetooth chips that "can't be passed through". LXC containers are covered too, via a different trick (yes, the one the forums say is impossible).
- You built a gaming VM on Proxmox. Everything works... except Bluetooth.
- Your controller just blinks, blinks, blinks, and gives up.
- You bought an Intel BE200 / AX210 card because a forum said so. Still nothing.
- You tried
qm set ... -usb, saw the device in the VM, and it still refused to work. - Every thread ends with someone saying "just use a USB cable".
It's not your fault, and your hardware is not broken.
Nearly all Bluetooth adapters clear their firmware when they're re-enumerated on USB, so
after a passthrough the guest has to load it again from scratch. That works when the guest
has the matching driver and firmware blobs, which is why a full Debian or Arch VM often
passes through fine. It falls apart when the guest doesn't: Home Assistant OS and gaming
distros ship trimmed kernels and incomplete linux-firmware, and Intel's CNVi combo chips
(BE200, AX2xx) are especially unforgiving, they land in a bootloader state the guest can't
talk it out of.
So this isn't "passthrough never works". It's "passthrough depends on your guest getting the firmware handshake right, and some guests can't". This tool removes that dependency by never handing the chip over at all.
Plain USB passthrough works for plenty of setups - a normal dongle into a normal Linux distro (Debian, Ubuntu) often just works:
qm set <vmid> -usb0 host=<vendor:product>If that gives you working Bluetooth in your VM, stop reading - you don't need this project. Worth trying a cheap USB dongle too: a plain dongle passed into a full distro is the simplest setup there is, and if your guest has the firmware for it, it just works.
The adapter drops its firmware when it's re-enumerated on USB, so the guest has to load it again. Most passthrough failures are simply a guest missing that firmware, and that is fixable without any of this. Inside the VM:
dmesg | grep -i -A2 bluetooth | grep -i "firmware\|failed"If you see Direct firmware load for ... failed, the guest is just missing the blob.
Install it and reboot the VM:
# Debian (needs the non-free-firmware component enabled)
sudo apt install firmware-iwlwifi # Intel chips; firmware-realtek, firmware-atheros etc. for others
# Ubuntu
sudo apt install linux-firmware # note: this package does not exist on Debian
# Fedora / ChimeraOS / Bazzite
sudo rpm-ostree install linux-firmware # usually already presentThat alone fixes a lot of cases, and if it fixes yours, use plain passthrough and skip this project.
This tool is for when that isn't available or doesn't help: Intel onboard combo chips (BE200/AX2xx) that land in a bootloader state the guest can't talk them out of, gaming distros (ChimeraOS/Bazzite trimmed kernels + quirky chip firmware), Home Assistant OS and other appliance images where you can't install firmware packages at all, trimmed cloud kernels, and dongles that wedge after a few VM restarts.
The chip stays on your Proxmox host, where it works. A tiny bridge streams it into the VM over the local network. Your VM sees a completely normal Bluetooth adapter. Latency is lower than the controller's own radio delay - you will never feel it.
Built on BlueZ. The bridge itself is btproxy, an existing tool from the official
BlueZ project (GPL-2.0-or-later), not reimplemented here. This
project is the install/systemd tooling around it. See
THIRD_PARTY_LICENSES.md for the full attribution and license text.
CI rebuilds it from upstream source on every commit and fails if a single byte differs. Full provenance, and a one-command check you can run yourself, in About that bundled binary below.
On the Proxmox host:
curl -fsSL https://raw.githubusercontent.com/lucid-fabrics/proxmox-bluetooth/main/install.sh | sudo bashYou'll see something like this:
==> Bluetooth adapters on this machine:
[0] hci0 - 70:08:10:A4:F1:45 (USB)
==> All adapters healthy. You are good to go.
==> Downloading btproxy (BlueZ 5.66 build, GPL-2.0-or-later, not our code)...
==> Checksum verified against the hash pinned in this script.
==> Bluetooth (hci0) is now shared on 192.168.1.3:9700.
Now run this INSIDE your VM:
curl -fsSL https://raw.githubusercontent.com/lucid-fabrics/proxmox-bluetooth/main/install.sh | sudo bash -s -- 192.168.1.3
!! Port 9700 is open to your whole LAN and has no password.
Whichever machine connects first gets the Bluetooth chip.
Once you know your VM's IP, restrict it:
curl -fsSL .../install.sh | sudo bash -s -- --allow <vm-ip>
That last warning is not boilerplate, it is the honest default: read
Is this secure? before leaving it that way. (The Downloading and
Checksum lines appear when the script runs on its own; from a git clone it uses the local
bin/ copy instead.)
Don't want to pipe a script into root, or run a binary you didn't build? Reasonable.
--build compiles the bridge from official BlueZ source on your machine instead, and
Four ways to install covers that plus the fully manual route.
What to do: copy that last line exactly (your IP will be different) and run it inside the VM.
Inside the VM, you'll see:
==> Done. This VM now has working Bluetooth. Go pair your controller.
What to do: open Bluetooth settings in the VM and pair like normal. That's it.
Both sides auto-start at boot and auto-reconnect. Set it up once, forget it exists.
All four land on the same result. They differ only in how much you take on trust, listed from least to most. The one-liner above is number 3.
1. No script, no binary from us. MANUAL_INSTALL.md walks through
building btproxy from official BlueZ source and writing the systemd units by hand.
Follow all of it (including the small restart wrapper, which stops the bridge from wedging
after a VM reboot) and you get the same result. install.sh only automates this.
2. Our script, but no binary from us.
curl -fsSL -O https://raw.githubusercontent.com/lucid-fabrics/proxmox-bluetooth/main/install.sh
less install.sh # readable bash, nothing minified or obfuscated
sudo bash install.sh --build--build compiles btproxy on your machine from the kernel.org BlueZ 5.66 tarball, with
the tarball's own SHA-256 pinned in build.sh, and installs that. Nothing prebuilt from
this repo is downloaded or run. It needs apt-get and a few minutes, which is why it is
opt-in rather than the default: immutable guests (ChimeraOS, Bazzite) have no way to
install a toolchain and can only use a prebuilt binary.
Already have btproxy? Then you need none of this. install.sh uses whatever
btproxy is on PATH and installs no copy of its own, so pacman -S bluez-utils on Arch
or a hand-built binary from step 1 is enough, and --uninstall will not touch it.
3. Read it first, then run it. Same as above without --build: the script fetches
bin/btproxy-x86_64, verifies it against the SHA-256 pinned in the script itself, and
refuses to install anything that doesn't match. That binary is
reproducible from upstream source in one command.
4. Clone it, so everything is present before you start and the script uses the local
bin/ copy rather than downloading:
git clone https://github.com/lucid-fabrics/proxmox-bluetooth
cd proxmox-bluetooth && sudo ./install.shFull provenance, in four points.
It isn't ours. bin/btproxy-x86_64 is an unmodified build of btproxy from
BlueZ itself. This project wrote the install tooling around it,
not the bridge. Full attribution, all three upstream licenses (GPL-2.0-or-later,
LGPL-2.1-or-later, BSD-2-Clause) and a written source offer are in
THIRD_PARTY_LICENSES.md.
Why not just apt install it? Because on the distros involved here, btproxy isn't
packaged. bluez obviously is, but that package doesn't include this particular tool:
| Distro | Ships btproxy? |
|
|---|---|---|
| Debian / Ubuntu | no | dpkg -L bluez bluez-test-tools | grep -c btproxy → 0 |
| Fedora (and Fedora Atomic: ChimeraOS, Bazzite) | no | dnf repoquery -l bluez | grep btproxy → nothing |
| Arch | yes | it's in bluez-utils |
Proxmox is Debian, and the guests this exists for are Debian/Ubuntu or Fedora Atomic, so
in practice building from source is the only route. On Arch, genuinely just
pacman -S bluez-utils: install.sh then finds that btproxy on PATH and installs no
binary at all, or you can skip this repo entirely and take just the systemd units from
MANUAL_INSTALL.md.
Where it runs. It is x86_64 only (there is no ARM build; on arm64 you must build
your own, see MANUAL_INSTALL.md). It's compiled on Debian 12 on
purpose, so it needs nothing newer than GLIBC_2.34 and links only against glibc:
| Target | glibc | |
|---|---|---|
| Proxmox 8 host (Debian 12) | 2.36 | starts cleanly |
| Proxmox 9 host (Debian 13) | 2.41 | full bridge tested end to end on real hardware |
| Debian 12+ / Ubuntu 22.04+ guests | 2.35+ | starts cleanly |
| Fedora / ChimeraOS / Bazzite guests | 2.38+ | starts cleanly |
Anything with glibc 2.34 or newer will run it. "Starts cleanly" means exactly that, verified in containers for those rows; only the Proxmox 9 row has had the whole bridge exercised against a real adapter. CI fails if a change ever raises the requirement past glibc 2.36, since that would silently break Proxmox 8.
Verify it yourself, in about a minute. The expected hash is pinned inside
install.sh, so it is part of the script you already read, not fetched from the same
place as the binary:
grep BTPROXY_SHA256 install.sh # expected hash
sha256sum bin/btproxy-x86_64 # what you actually haveinstall.sh refuses to install anything that doesn't match, and deletes it.
It reproduces byte-for-byte. Building BlueZ 5.66 on Debian 12 yields exactly the
committed binary, same SHA-256, and CI checks that on every commit: it rebuilds from the
kernel.org tarball and fails if the result differs from what's in bin/. So the badge
isn't "some binary compiles", it's "the file you're about to run is that source, compiled".
Confirm it yourself in one command:
docker run --rm --platform linux/amd64 -v "$PWD":/w -w /w debian:bookworm bash -c \
'bash build.sh && cmp bin/btproxy-x86_64 build/btproxy-x86_64 && echo IDENTICAL'(--platform linux/amd64 matters on an ARM host, without it Docker pulls the arm64 image
and you'd compare an aarch64 build against an x86_64 one.)
(Reproducibility is pinned to that container image; a different distro or compiler version can produce different bytes from the same source, which is why both the shipped build and CI use Debian 12.) Or build your own and run that instead, MANUAL_INSTALL.md walks through it.
If your Proxmox host can see it, your VM can have it. Run this on the host:
curl -fsSL https://raw.githubusercontent.com/lucid-fabrics/proxmox-bluetooth/main/install.sh | sudo bash -s -- --checkIf it's healthy, you'll see:
==> Bluetooth adapters on this machine:
[0] hci0 - 70:08:10:A4:F1:45 (USB)
==> All adapters healthy. You are good to go.
What to do: nothing - run the install command above.
If your chip is the stuck-Intel-chip case, you'll see:
!! hci0 is NOT responding (stuck in bootloader).
Fix: shut down, flip the power supply switch OFF for 15 seconds, boot.
A reboot or the front power button is NOT enough. See README.
What to do: exactly what it says. This looks extreme but it's the one thing that actually works - see the FAQ below for why.
Confirmed by real people (add yours with a PR):
| Hardware | Status |
|---|---|
| Intel BE200 | ✅ Tested - this repo exists because of it |
| Intel AX200 / AX210 / AX211 | ✅ Same family, same behavior |
| MediaTek MT7921 / MT7922 | ✅ Standard Linux support |
| Generic CSR / Realtek USB dongles | ✅ Anything your host's Linux drives |
| UGREEN "BT 6.0" dongles (Barrot chip) | ❌ Broken firmware on Linux, bridge or not. Avoid. |
Run these on whichever machine they apply to. Under curl | bash append them after
-s --, e.g. curl -fsSL <url>/install.sh | sudo bash -s -- --status.
| Command | Where | What it does |
|---|---|---|
install.sh |
host | Share this machine's Bluetooth |
install.sh <host-ip> |
VM | Connect to a shared adapter |
install.sh --check |
host | List adapters and flag a stuck chip |
install.sh --status |
either | Start here when something's wrong. Shows both sides |
install.sh --report |
either | Print a paste-able diagnostic bundle for a bug report |
install.sh --adapter N |
host | Pick a chip when there's more than one |
install.sh --allow <ip/cidr> |
host | Restrict port 9700 to one address |
install.sh --allow-any |
host | Remove that restriction |
install.sh --build |
either | Compile btproxy from BlueZ source instead of using ours |
install.sh --pause |
host | Take Bluetooth back temporarily (until reboot) |
install.sh --resume |
host | Hand it back to the VM |
install.sh --lxc <ctid> |
host | Share host Bluetooth with an LXC container (filtered D-Bus) |
install.sh --lxc-remove <ctid> |
host | Undo the LXC share for one container |
install.sh --uninstall |
either | Remove everything, restore normal Bluetooth |
Run --status first, it tells you which side is broken. Then:
| Symptom | Look at |
|---|---|
| No adapter appears in the VM | journalctl -u btproxy-client -n 30 in the VM |
| Host says it's sharing, VM never connects | Wrong IP, or a --allow rule blocking the VM's real source address. Check with ip route get <host-ip> inside the VM |
| Adapter appears, won't pair | bluetoothctl show in the VM; if Powered: no, run bluetoothctl power on |
| Worked, then stopped after a VM reboot | systemctl restart btproxy-server on the host |
--check says the chip is stuck |
Full power-off at the PSU switch for 15s, see the FAQ below |
On the VM side, --status now probes the host itself and tells the difference between
"sharing is paused on the host", "the host's --allow rule blocks this VM", and
"wrong IP or host down" - so start there before reading logs.
Still stuck? Run --report on both machines and open an issue with the two outputs -
it collects status, logs, adapters and firewall state in one paste-able block.
No. The bridge adds well under a millisecond on the same machine. Bluetooth itself is slower.
Yes - the bridge is protocol-transparent (it forwards raw Bluetooth traffic, it doesn't understand or filter it). Anything that works on a normal Linux Bluetooth adapter works: controllers, audio, HID, BLE sensors for Home Assistant.
Yes. It behaves exactly like a normal adapter in the VM - two controllers plus headphones is fine. The one-at-a-time limit is about VMs (one VM owns the chip), not devices.
No. The card you already have works. Even the old one you replaced probably worked.
It didn't die - it went to sleep. Gaming distros auto-suspend after idle like a
Steam Deck, and a VM with GPU passthrough never wakes from that. Turn suspend off
for good inside the VM:
sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target
Known quirk in their input layer, not in Bluetooth: run
sudo systemctl restart inputplumber, then turn the controller off and on. Fixed.
It is stuck in its blank boot state. Shut the machine down and flip the power supply switch off for 15 seconds, then boot. A reboot is not enough. The front power button is not enough. This one trick cost us a full day - you're welcome.
--check lists every adapter it finds (hci0, hci1, ...) with its MAC address so you
can tell them apart, and if it finds more than one it won't guess - it'll ask you to pick.
Share a specific one with ./install.sh --adapter 1. Bridging two chips into two different
VMs at the same time isn't supported yet (one install per host today) - open an issue if
you need it, it's a small change.
Just run the client one-liner in the new VM - the host serves whichever VM connects
(one at a time). Nothing to clean up after a dead VM. If the old VM is still running,
stop its client first (--uninstall there). Only footnote: pairings live inside the
guest, so pair your devices once in the new VM.
Yes. Those distros replace the system image on update but keep /etc and /var - which
is exactly where this installs. Your bridge and pairings come back on their own.
Yes, while sharing. A server in a closet rarely misses it. Need it back for a moment?
--pause returns the chip to the host, --resume hands it back to the VM (which
reconnects by itself). Note --pause is not permanent: it stops the service but leaves it
enabled, so a host reboot starts sharing again. --uninstall removes everything for good.
Be aware of the trade-off. btproxy speaks plain, unauthenticated TCP on port 9700, so by
default whichever machine on your LAN connects first gets the Bluetooth chip - and raw
HCI access means that machine can drive the radio, not just read from it.
Lock it to your VM:
./install.sh --allow 192.168.1.50 # a single VM
./install.sh --allow 192.168.1.0/24 # or a trusted subnetThat installs an nftables rule (in its own proxmox-bluetooth table, so it won't disturb
pve-firewall) that accepts port 9700 from that address and drops it from everything else.
It's tied to the service and loads on boot; if the rule ever fails to load the service
refuses to start rather than come up unprotected. --uninstall removes it, and the setting
is remembered, so a later plain re-install won't quietly reopen the port (use
--allow-any if you actually want it open again).
Use your VM's actual source IP. A machine with several interfaces may reach the host
from an address you didn't expect, check with ip route get <host-ip> inside the VM.
There is no encryption either, so treat this as a trusted-LAN tool: fine between a host and its own VM, not something to route across networks you don't control.
That's antennas, not the bridge. M.2 cards need their two little antenna cables connected; a bare card inside a metal case has almost no reach. USB dongles: a front port or a short extension beats the back-panel ports next to all your other cables.
The bridge is for Linux guests (it relies on Linux's Bluetooth stack). Windows VMs
usually don't have this problem: passing a USB dongle straight through with
qm set <vmid> -usb0 host=<id> just works there. This tool exists because Linux
guests choke where Windows shrugs.
Supported, and it works differently than VMs - because it has to. The kernel only
allows Bluetooth sockets in the initial network namespace, so BlueZ can never run
inside a container, no matter how privileged (this is why every "bind the device /
cgroup allow" forum recipe fails). Instead, the host keeps the adapter and runs
bluetoothd, and the container talks to it through a filtered D-Bus proxy that
exposes org.bluez and nothing else:
curl -fsSL https://raw.githubusercontent.com/lucid-fabrics/proxmox-bluetooth/main/install.sh | sudo bash -s -- --lxc <ctid>Works for privileged and unprivileged containers. It prints the one line to add to
your app (an env var, or a -v mount for Docker/Home Assistant inside the container).
Ideal for Home Assistant BLE sensors; pairings live on the host in /var/lib/bluetooth.
Note: a host either bridges its adapter to a VM or shares it with containers, not both
at once - the VM bridge works precisely by stopping host bluetoothd.
No - any Linux host with KVM VMs (or even two separate machines). Proxmox is just where it hurts the most.
For the curious: what's actually happening
Intel CNVi Bluetooth (BE200/AX2xx) requires the host's btusb/btintel driver to load
its firmware at boot. Any passthrough handoff (USB redirect, vfio, driver unbind) resets
the chip to its ROM bootloader, and the guest can never complete the firmware handshake.
The bridge is btproxy from the official BlueZ source tree (never shipped in distro
packages). On the host it opens the adapter in HCI user-channel mode and serves raw HCI
over TCP. In the guest it creates a virtual controller via hci_vhci and pipes the
stream into it. BlueZ in the guest neither knows nor cares.
Systemd units: btproxy-server.service (host, replaces bluetooth.service) and
btproxy-client.service (guest, ordered before bluetooth.service via drop-in).
The bundled binary is built from bluez 5.66 tools/btproxy with plain -O2 (replacing
autoconf's default -g -O2, so no debug info survives the strip). It links only against
glibc. See build.sh, and THIRD_PARTY_LICENSES.md
for the three upstream licenses that binary carries.
This fix cost a full day of head-scratching, three "dead" reboots, and one very real walk to the power supply switch - so nobody else has to lose that day. Tools like this stay free and maintained for exactly one reason: people who were helped choose to help the next person in line. If that's you right now, thank you. It genuinely keeps this alive.
Same script, --uninstall, on whichever machine you want to restore.
This repository's own code (install.sh, build.sh, docs) is MIT, see LICENSE.
The bundled bin/btproxy-x86_64 binary is unmodified third-party BlueZ code under
GPL-2.0-or-later (with LGPL-2.1 and BSD-2-Clause parts), see
THIRD_PARTY_LICENSES.md. install.sh writes that notice and the
GPL source offer to /usr/local/share/doc/proxmox-bluetooth/ alongside the binary.
An independent community project, not affiliated with or endorsed by Proxmox Server Solutions GmbH or the BlueZ project. "Proxmox" is a registered trademark of Proxmox Server Solutions GmbH; other names belong to their respective owners.

