<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lucid Fabrics</title>
    <description>The latest articles on DEV Community by Lucid Fabrics (@lucidfabrics).</description>
    <link>https://dev.to/lucidfabrics</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3767434%2Fb25194cb-256d-4783-8f82-8af97834a87c.jpg</url>
      <title>DEV Community: Lucid Fabrics</title>
      <link>https://dev.to/lucidfabrics</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucidfabrics"/>
    <language>en</language>
    <item>
      <title>How Dreo Fans Actually Pair Over Bluetooth</title>
      <dc:creator>Lucid Fabrics</dc:creator>
      <pubDate>Fri, 31 Jul 2026 05:02:26 +0000</pubDate>
      <link>https://dev.to/lucidfabrics/how-dreo-fans-actually-pair-over-bluetooth-2195</link>
      <guid>https://dev.to/lucidfabrics/how-dreo-fans-actually-pair-over-bluetooth-2195</guid>
      <description>&lt;p&gt;I spent two days implementing &lt;a href="https://www.dreo.com" rel="noopener noreferrer"&gt;Dreo&lt;/a&gt;'s Bluetooth pairing protocol from decompiled Android source. I wrote unit tests for it. I felt pretty good about it. Then I pointed it at an actual fan, and the fan ignored every single byte I sent.&lt;/p&gt;

&lt;p&gt;The code wasn't buggy. It was just for a completely different product.&lt;/p&gt;

&lt;p&gt;This was my first time reverse-engineering anything. I'd never decompiled an app, never hooked a running process, never looked at a BLE capture in my life. I mention that partly as a disclaimer and partly as encouragement, because the thing I was most scared of turned out to be the easy part, and the thing that actually cost me days was ordinary stubbornness about a wrong assumption.&lt;/p&gt;

&lt;p&gt;So below is the protocol that really works: the service UUIDs, the message types, and the ordering rule that ate an entire evening before I worked out what was going on. As far as I can tell nobody had written this down anywhere. If you have a Dreo fan and you'd like to get it onto your WiFi without using their phone app, this should be everything you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it went wrong
&lt;/h2&gt;

&lt;p&gt;The whole thing started because I wanted to turn a fan on from my keyboard. That was it, that was the entire ambition. So I started building a little macOS menu bar app for it, called Windbar, and assumed the hard part would be the UI.&lt;/p&gt;

&lt;p&gt;The cloud side turned out to be straightforward. You log in over REST, you get a token, you open a WebSocket, and you send commands down it. Fine. The interesting part was setting up a brand new fan, because that happens over Bluetooth, directly between your phone and the fan, and nobody had documented any of it.&lt;/p&gt;

&lt;p&gt;So I did the obvious thing and pulled the Android APK apart to read what it does.&lt;/p&gt;

&lt;p&gt;It told me a very clear story. GATT service &lt;code&gt;FFB4&lt;/code&gt;, WiFi password RSA-encrypted into a fixed byte layout, every field neatly accounted for. I implemented the whole thing, wrote tests around the byte layout, and went to bed happy.&lt;/p&gt;

&lt;p&gt;The real fan advertised service &lt;code&gt;FFFF&lt;/code&gt; and had no interest in anything I sent it.&lt;/p&gt;

&lt;p&gt;Here's the annoying part: the &lt;code&gt;FFB4&lt;/code&gt; code was real. It just belongs to a &lt;em&gt;different Bluetooth SDK that the same app also ships&lt;/em&gt;, for other Dreo product lines. Nothing in the decompiled source says "by the way, this path is dead for the fan sitting on your desk." Static analysis will happily hand you a complete, internally consistent, and totally wrong picture of the world, and your unit tests will nod along, because all they're really checking is that your code matches your assumption.&lt;/p&gt;

&lt;p&gt;That's what got me. Not that I was wrong, but that everything available to me said I was right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually worked
&lt;/h2&gt;

&lt;p&gt;Eventually I stopped reading and started watching. I patched the Android app with a &lt;a href="https://frida.re" rel="noopener noreferrer"&gt;Frida&lt;/a&gt; gadget, hooked its Bluetooth calls, and paired a fan for real while listening in.&lt;/p&gt;

&lt;p&gt;Ten minutes of that was worth more than the two days of reading.&lt;/p&gt;

&lt;p&gt;There's no RSA anywhere. There's no &lt;code&gt;FFB4&lt;/code&gt;. The protocol is &lt;a href="https://www.rfc-editor.org/rfc/rfc8949.html" rel="noopener noreferrer"&gt;CBOR&lt;/a&gt; messages going over one write characteristic and one notify characteristic, and the WiFi password crosses in &lt;strong&gt;cleartext&lt;/strong&gt;. Bluetooth's own link encryption is the only thing protecting it, which is worth sitting with for a second if you've ever typed your home WiFi password into a fan.&lt;/p&gt;

&lt;p&gt;Worth being precise about how bad that is, since "cleartext WiFi password" sounds worse than it is. To capture it you'd need to be within Bluetooth range during the handful of seconds that a fan is actually being paired, on a channel-hopping link you'd have to be sniffing already. It isn't remotely exploitable and it isn't sitting there waiting for you. It's still the kind of thing you'd rather see wrapped in application-level encryption, which is presumably what the unused RSA code in the other SDK was for.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tools, for anyone else doing this for the first time
&lt;/h3&gt;

&lt;p&gt;There are really only two jobs here, and it took me a while to understand they're different jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reading the app&lt;/strong&gt; is static analysis. You take the APK, decompile it, and read what the code says it does. This is the part that looks like real reverse engineering in films, and it's genuinely useful for orientation: it's how I found the class names, the message type strings, and the general shape of things. It is also, as covered above, perfectly capable of lying to you with total confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watching the app&lt;/strong&gt; is dynamic analysis, and it's what actually solves the problem. I used &lt;a href="https://frida.re" rel="noopener noreferrer"&gt;Frida&lt;/a&gt;, which lets you inject a bit of JavaScript into a running process and intercept function calls as they happen. The important detail for a beginner: you don't need a rooted phone. Frida ships a "gadget" you can repack into the APK itself, so the app carries the instrumentation with it and runs on an ordinary device.&lt;/p&gt;

&lt;p&gt;(I'd fought with Bluetooth once before, from completely the other end, &lt;a href="https://dev.to/lucidfabrics/bluetooth-in-a-proxmox-vm-shouldnt-be-this-hard-2l1p"&gt;getting it working inside a Proxmox VM&lt;/a&gt;. Different problem, same feeling of the stack quietly lying to you.)&lt;/p&gt;

&lt;p&gt;Once it's in, the hook itself is almost disappointingly small. Android only has so many ways to push bytes over BLE, so you wrap the write call and the notification callback, log the payloads as hex, and then go pair a fan normally with your own phone. The log that comes out the other side is the conversation, in order, exactly as the real app has it. Every answer in this post came from reading that log.&lt;/p&gt;

&lt;p&gt;Two things nobody told me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Capture the whole session from the very first byte.&lt;/strong&gt; I initially hooked things partway through and got the messages but not the opening handshake, which is precisely the part I needed and precisely the part I later wasted an evening on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log timestamps.&lt;/strong&gt; Some of these exchanges are the fan taking its time rather than the fan refusing you, and without timing you can't tell the difference between "rejected" and "still thinking".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For reference, all of this was captured against a Pilot Max tower fan. Other models speak the same protocol as far as I can tell, but I've only watched that one do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocol
&lt;/h2&gt;

&lt;p&gt;This is the part I actually wanted to publish, since nobody else has.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Service&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0000ffff-0000-1000-8000-00805f9b34fb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Write&lt;/td&gt;
&lt;td&gt;&lt;code&gt;00009b01-0000-1000-8000-00805f9b34fb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Notify&lt;/td&gt;
&lt;td&gt;&lt;code&gt;00009b02-0000-1000-8000-00805f9b34fb&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Advertises as&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;DREO&lt;/code&gt; plus a suffix, like &lt;code&gt;DREOpf08s8E&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every message has the same shape, &lt;code&gt;{"t": &amp;lt;type&amp;gt;, "v": &amp;lt;version&amp;gt;, "d": &amp;lt;payload&amp;gt;}&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Who&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;st&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;app&lt;/td&gt;
&lt;td&gt;Set the clock&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rd&lt;/code&gt; / &lt;code&gt;ri&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;both&lt;/td&gt;
&lt;td&gt;Ask the fan who it is, and its answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;app&lt;/td&gt;
&lt;td&gt;Which account and which API host to phone home to&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rw&lt;/code&gt; / &lt;code&gt;wl&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;both&lt;/td&gt;
&lt;td&gt;Ask for a WiFi scan, results streamed back in batches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cw&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;both&lt;/td&gt;
&lt;td&gt;Here are the credentials, then progress reports&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;cf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fan&lt;/td&gt;
&lt;td&gt;Final verdict, including its own internet self-check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ee&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fan&lt;/td&gt;
&lt;td&gt;Something went wrong&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Try it in thirty seconds
&lt;/h3&gt;

&lt;p&gt;If you own a Dreo fan, you can confirm half of this right now without writing anything. Plug the fan in, hold the Oscillation button for about five seconds until the WiFi light blinks, then open any BLE scanner (nRF Connect is fine). You'll see it advertising as &lt;code&gt;DREO&lt;/code&gt; plus a suffix, offering service &lt;code&gt;FFFF&lt;/code&gt;. That's the thing the decompiled source told me didn't exist.&lt;/p&gt;

&lt;p&gt;And here's the message that actually carries your WiFi credentials, &lt;code&gt;cw&lt;/code&gt;, with the field names as they really go over the wire:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a    auth type
b    0
c    channel
m    { ca: 0, cm: 0, da: 0 }
p    password, cleartext
s    ssid
t    { ca: 60000, cm: 60000, da: 30000 }
sc   1 if the fan should self-check its internet after joining
scc  { dm: [ "http://www.google.com", ... ] }   the URLs it probes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things about that block took me longer than they should have. The fan parses the map &lt;strong&gt;positionally rather than by key&lt;/strong&gt;, so the field order above isn't cosmetic, it's part of the format. And the final &lt;code&gt;cw&lt;/code&gt; write omits the &lt;code&gt;v&lt;/code&gt; version field that every other message carries, which I only believed after diffing my bytes against the capture for the third time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ordering rule that cost me an evening
&lt;/h2&gt;

&lt;p&gt;Knowing the messages isn't enough, and this is the bit I wish someone had told me. &lt;strong&gt;The order matters.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you send &lt;code&gt;st&lt;/code&gt;, then &lt;code&gt;rw&lt;/code&gt;, then &lt;code&gt;cw&lt;/code&gt;, the fan refuses to join the network. And I mean it refuses even when the &lt;code&gt;cw&lt;/code&gt; bytes are byte-for-byte identical to a capture of a pairing that worked. I diffed them, more than once, convinced I'd miscounted something. They matched exactly. The fan still said no.&lt;/p&gt;

&lt;p&gt;What it's waiting for is the same opening the real app uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;st  -&amp;gt;  rd  -&amp;gt;  pd  -&amp;gt;  rw  -&amp;gt;  cw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Introduce yourself, mention which account you're acting on behalf of, and &lt;em&gt;then&lt;/em&gt; ask it to join a network. Add those two messages and it works on the first try.&lt;/p&gt;

&lt;p&gt;Looking back, that makes a kind of sense. A protocol isn't really a set of messages, it's a conversation with a state machine you can't see, and the state machine is almost never documented. Capturing one successful run gives you the messages. Capturing it in order is what gives you the protocol.&lt;/p&gt;

&lt;p&gt;That's the whole thing, by the way. Everything above is what you need to pair a Dreo fan yourself, in whatever language you like. It's maybe 250 lines of CBOR encoding and a small state machine, and the encoder has one trap in it: map keys have to keep their insertion order, which Swift dictionaries don't do, and the fan will reject you if they come out shuffled. If you'd rather not write all that, &lt;a href="https://github.com/lucid-fabrics/windbar" rel="noopener noreferrer"&gt;my implementation is here&lt;/a&gt; and it's MIT.&lt;/p&gt;

&lt;p&gt;Once the message ordering was right, the whole protocol collapsed into about four screens of wizard:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F70wl7rig87hixfdqdyh9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F70wl7rig87hixfdqdyh9.png" alt="The Add a Device wizard, explaining how to put the fan into pairing mode before the Mac sets it up over Bluetooth"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Everything in the tables above, hidden behind "The Light Is Blinking".&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug I invented out of thin air
&lt;/h2&gt;

&lt;p&gt;While I was stuck, I managed to convince myself the real problem was Bluetooth packet size.&lt;/p&gt;

&lt;p&gt;The reasoning was lovely. Dreo's app calls &lt;code&gt;requestMtu(512)&lt;/code&gt;, and warns in its own source that large writes fail on peripherals that can't reassemble them. My message was 213 bytes. CoreBluetooth gives you no way to request an MTU at all. So: clearly a platform limitation, clearly a dead end, clearly not my fault.&lt;/p&gt;

&lt;p&gt;Then I printed the number. macOS had already negotiated 515. My 213-byte message fit in a single write with room to spare, and had done all along.&lt;/p&gt;

&lt;p&gt;The theory had been dead on arrival, and I'd have spent a day "fixing" it if I hadn't checked. Honestly, most of the hours I lost on this project went the same way: I believed something I could have verified in thirty seconds.&lt;/p&gt;

&lt;p&gt;I did keep one piece of the dead theory. There's a flag that drops the &lt;code&gt;scc&lt;/code&gt; internet-self-check block from the credentials message, taking it from 213 bytes down to about 78, and it's still in the code. It's off by default, because on the hardware I can actually test the full message is fine. But I only ever measured one Mac talking to one fan, and I'm not confident enough in that sample to delete the smaller path. Being wrong about the diagnosis doesn't mean the escape hatch was a bad idea, it just means it isn't the thing that was broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The device that hid all the others
&lt;/h2&gt;

&lt;p&gt;One more, from the cloud side rather than the Bluetooth side, because it's the bug I'd most like to warn people about.&lt;/p&gt;

&lt;p&gt;The nice thing about Dreo's API is that devices describe themselves. Each one returns a &lt;code&gt;controlsConf&lt;/code&gt; blob saying "I have a Speed control that goes 1 to 12" and "here are my four modes", so the app renders whatever it's told rather than knowing anything about fans. Then I paired a newer model and it came back with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"controlsConf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DR-HPF008S"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No controls, just a pointer at a template that lives inside Dreo's own app binary. Fine, annoying, solvable.&lt;/p&gt;

&lt;p&gt;What wasn't fine: that one device &lt;strong&gt;broke decoding for the entire list&lt;/strong&gt;. The app showed no fans at all and confidently blamed a failed login, so I spent the first while debugging authentication that was working perfectly. One unfamiliar device had taken down every other device in the account.&lt;/p&gt;

&lt;p&gt;Both the device list and the control sections now decode item by item and drop only what they can't parse. If you're deserialising a collection from someone else's API, that's the whole lesson: one weird element should cost you that element, not the collection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell myself at the start
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat decompiled code as evidence, not as truth.&lt;/strong&gt; It shows you what the binary is capable of, never what your particular device actually does. Confirm against real hardware before you build on top of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instrument the real client first.&lt;/strong&gt; Ten minutes of Frida on the official app replaced two days of reading, and it handed me the message ordering that static analysis could never have shown me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure before you theorise.&lt;/strong&gt; If you can check a hypothesis in under a minute, checking it is always cheaper than reasoning about it, no matter how good the reasoning feels.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of those are clever insights. They're the sort of thing every person who does this for a living presumably learned years ago. But I hadn't, and each one cost me real hours, so if you're also doing this for the first time, maybe they'll cost you fewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The app, if you want it
&lt;/h2&gt;

&lt;p&gt;All of this ships in &lt;strong&gt;Windbar&lt;/strong&gt;, a small macOS menu bar app for Dreo fans. You click the icon and your fans are there. There's a global hotkey, and a &lt;code&gt;windbar://toggle&lt;/code&gt; URL so Shortcuts or a Stream Deck can drive it. And it pairs a brand new fan onto WiFi with no phone involved, which is the part this whole post has been about.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5mj5c4rtl48ofmkiy6y3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5mj5c4rtl48ofmkiy6y3.png" width="330" alt="The Windbar menu bar popover showing two fans with mode, speed and oscillation controls"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing you can't see in that screenshot: none of those controls are hardcoded. Dreo's API hands back a &lt;code&gt;controlsConf&lt;/code&gt; blob describing what each device can do, right down to "I have a Speed control that goes 1 to 12" and "here are my four modes and their names", so the app just renders whatever it's told. A tower fan and an air circulator both work without a line of model-specific code. That part was a genuinely pleasant surprise after the Bluetooth ordeal.&lt;/p&gt;

&lt;p&gt;It's Swift 6 with strict concurrency, a hand-rolled CBOR encoder, and 59 tests, none of which touch the network.&lt;/p&gt;

&lt;p&gt;Free, open source, MIT. If the tables above saved you a weekend, the repo is the place to say so:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/lucid-fabrics/windbar" rel="noopener noreferrer"&gt;github.com/lucid-fabrics/windbar&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Two honest caveats before anyone downloads it. It's an independent project, not made by or affiliated with Dreo in any way, it just talks to their fans. And it needs a Dreo account with an actual password: if you signed up with "Continue with Google" or "Sign in with Apple" it won't work at all, because the login endpoint only accepts &lt;code&gt;grant_type=email-password&lt;/code&gt;. There's nothing I can do about that from the outside, and I'd rather you knew now than after installing.&lt;/p&gt;

&lt;p&gt;Two loose ends I'd love help with, if you're the sort of person who reads to the end of posts like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;ee&lt;/code&gt; error message comes back as four bytes, &lt;code&gt;03 02 00 01&lt;/code&gt;, and the official app's own parser expects a completely different shape, so it appears to never actually receive one. I never worked out what it means.&lt;/li&gt;
&lt;li&gt;Everything here was captured against one fan, a Pilot Max tower. If you have a different Dreo model and it speaks this protocol, or refuses to, I'd genuinely like to hear which.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either one, drop it in the comments or open an issue.&lt;/p&gt;

</description>
      <category>bluetoothle</category>
      <category>reverseengineering</category>
      <category>iot</category>
      <category>homeautomation</category>
    </item>
    <item>
      <title>I Spent 150 Commits Fixing These 5 macOS-on-Proxmox Bugs</title>
      <dc:creator>Lucid Fabrics</dc:creator>
      <pubDate>Fri, 31 Jul 2026 03:46:33 +0000</pubDate>
      <link>https://dev.to/lucidfabrics/5-macos-on-proxmox-bugs-that-no-guide-warns-you-about-56b6</link>
      <guid>https://dev.to/lucidfabrics/5-macos-on-proxmox-bugs-that-no-guide-warns-you-about-56b6</guid>
      <description>&lt;p&gt;Back in February I published &lt;a href="https://dev.to/lucidfabrics/i-built-a-one-command-macos-vm-tool-for-proxmox-9-190j"&gt;a post about osx-proxmox-next&lt;/a&gt;, a tool that builds a macOS VM on Proxmox with one command instead of an afternoon of OpenCore plist editing.&lt;/p&gt;

&lt;p&gt;About 1,500 people read it. Some of them installed it. On hardware I don't own.&lt;/p&gt;

&lt;p&gt;That's when the interesting bugs showed up. 150 commits later, here are five failures that don't appear in any macOS-on-Proxmox guide I've found, with the actual root cause for each.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The installer stalls at 100% CPU and nothing moves
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; macOS installer reaches the copy phase. CPU pegged at 100%. Disk IO and network throughput both flat zero. It sits there forever.&lt;/p&gt;

&lt;p&gt;Only on Xeon E5/E7 v2-v4 hosts.&lt;/p&gt;

&lt;p&gt;My first fix was wrong. The stall looked like a network problem, so I assumed the &lt;code&gt;vmxnet3&lt;/code&gt; kext was failing to load during install and swapped those hosts to &lt;code&gt;e1000-82545em&lt;/code&gt;. Shipped it.&lt;/p&gt;

&lt;p&gt;Then &lt;a href="https://github.com/lucid-fabrics/osx-proxmox-next/issues/103" rel="noopener noreferrer"&gt;issue #103&lt;/a&gt; came back from someone with the actual hardware: &lt;code&gt;vmxnet3&lt;/code&gt; got network fine, and &lt;code&gt;e1000-82545em&lt;/code&gt; did not attach at all. I had made it worse.&lt;/p&gt;

&lt;p&gt;The real cause is two layers down. Those chips are genuine HEDT parts with dual-socket / multi-die topology, and &lt;code&gt;-cpu host&lt;/code&gt; leaks that topology straight through to the guest. Pair it with a MacPro7,1 SMBIOS, which macOS treats as multi-socket capable, and XNU's scheduler livelocks under heavy multithreaded IO. The installer copy phase is exactly that workload.&lt;/p&gt;

&lt;p&gt;The fix is to stop passing the host topology through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;_XEON_HEDT_PATTERN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Xeon.*E[57][ -]*\d+ *v([234])&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IGNORECASE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_xeon_hedt_cpu_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_XEON_HEDT_PATTERN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Haswell-noTSX,model=158,stepping=3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Broadwell-noTSX,model=158&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lesson I keep relearning: the symptom showed up at the network layer, the cause lived in CPU topology. Guessing from the symptom cost me a release.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The VM boots into Recovery forever
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Fresh install finishes. Every subsequent boot lands back in Recovery. Selecting macOS in the OpenCore picker works for that boot and never sticks.&lt;/p&gt;

&lt;p&gt;Cause: &lt;code&gt;AllowSetDefault&lt;/code&gt; is off in the stock OpenCore config. That's the setting behind Ctrl+Enter in the boot picker. With it off, you have no way to pin the macOS entry as default, and the on-disk Recovery volume keeps winning the boot race.&lt;/p&gt;

&lt;p&gt;One key in the config patch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="err"&gt;Misc&lt;/span&gt; &lt;span class="err"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;Security&lt;/span&gt; &lt;span class="err"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="py"&gt;AllowSetDefault&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then in the picker: highlight your macOS volume, hit &lt;strong&gt;Ctrl+Enter&lt;/strong&gt;. It sticks.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The boot order after install is backwards from what you'd guess
&lt;/h2&gt;

&lt;p&gt;After macOS finishes installing, the boot order has to be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ide0;virtio0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OpenCore (&lt;code&gt;ide0&lt;/code&gt;) first, macOS disk (&lt;code&gt;virtio0&lt;/code&gt;) second. I shipped a hint suggesting &lt;code&gt;virtio0;ide0&lt;/code&gt;, which reads more natural and is wrong: it boots the macOS disk directly, skips OpenCore entirely, and the VM stalls on first boot with no error.&lt;/p&gt;

&lt;p&gt;macOS on a hypervisor never boots without OpenCore in front of it. Every boot, not just install.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;osx-next-cli post-install &lt;span class="nt"&gt;--vmid&lt;/span&gt; 100 &lt;span class="nt"&gt;--execute&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. "Memory Modules Misconfigured" on every single boot
&lt;/h2&gt;

&lt;p&gt;Cosmetic, but it fires on every boot and it is maddening.&lt;/p&gt;

&lt;p&gt;Every VM gets the MacPro7,1 SMBIOS, and the stock OpenCore image ships no &lt;code&gt;RestrictEvents.kext&lt;/code&gt;. macOS looks at the emulated memory layout, decides a real Mac Pro would never be configured this way, and notifies you about it. Forever.&lt;/p&gt;

&lt;p&gt;Search for this and you will be told to add &lt;code&gt;revpatch=memtab&lt;/code&gt; as a boot-arg. &lt;strong&gt;That does nothing here.&lt;/strong&gt; &lt;code&gt;revpatch=memtab&lt;/code&gt; only affects MacBookAir SMBIOS models. On MacPro7,1 it is a no-op, which is why so many people report it "not working."&lt;/p&gt;

&lt;p&gt;The actual fix is shipping the kext at all. &lt;code&gt;RestrictEvents&lt;/code&gt; 1.1.6 goes into &lt;code&gt;EFI/OC/Kexts&lt;/code&gt;, gets registered in &lt;code&gt;config.plist&lt;/code&gt;, and its default &lt;code&gt;revblock=auto&lt;/code&gt; blocks both &lt;code&gt;MemorySlotNotification&lt;/code&gt; and &lt;code&gt;ExpansionSlotNotification&lt;/code&gt;. No boot-arg needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Apple ID sign-in fails on Sequoia and Tahoe
&lt;/h2&gt;

&lt;p&gt;On older macOS versions, a &lt;code&gt;vmgenid&lt;/code&gt; and a static MAC were enough to sign into iMessage and FaceTime. On Sequoia 15 and Tahoe 26 they are not.&lt;/p&gt;

&lt;p&gt;Apple's DeviceCheck now reads the &lt;code&gt;hv_vmm_present&lt;/code&gt; sysctl. In a VM it returns 1, and sign-in is refused before it ever reaches your credentials.&lt;/p&gt;

&lt;p&gt;The community fix is a string swap in the kernel's string table, applied by OpenCore at boot. Find the &lt;code&gt;hibernatecount&lt;/code&gt; sysctl name, replace it with &lt;code&gt;hv_vmm_present&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find:    ...hibernatehidready\0 hibernatecount\0
Replace: ...hibernatehidready\0 hv_vmm_present\0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lookup now resolves to the hibernate counter, which is 0 on a fresh boot. macOS reads 0, DeviceCheck sees a physical machine, sign-in works.&lt;/p&gt;

&lt;p&gt;It's a kernel patch against a specific string layout, so treat it as something that can break on any macOS point release. It's behind an explicit &lt;code&gt;--apple-services&lt;/code&gt; flag for that reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus: cloning a macOS VM quietly breaks iCloud on both
&lt;/h2&gt;

&lt;p&gt;Proxmox's full clone is a byte copy, which means the clone inherits the source VM's SMBIOS serial, UUID, MLB and ROM. Two machines, one Apple hardware identity. Apple notices, and you get sign-in failures on whichever one loses.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;osx-next-cli clone&lt;/code&gt; full-clones and then immediately regenerates the SMBIOS quad plus &lt;code&gt;vmgenid&lt;/code&gt; and the static MAC, so the clone is a distinct machine as far as Apple is concerned.&lt;/p&gt;




&lt;h2&gt;
  
  
  All of it became a doctor command
&lt;/h2&gt;

&lt;p&gt;Every one of these is a config-level mistake that is invisible until macOS misbehaves in some unrelated-looking way. So they're now 12 checks you can run against any VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;osx-next-cli doctor &lt;span class="nt"&gt;--vmid&lt;/span&gt; 100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Balloon, q35, cores as a power of 2, memory, CPU type, NIC model, agent, smbios1, boot order, and the presence of &lt;code&gt;virtio0&lt;/code&gt; / &lt;code&gt;ide0&lt;/code&gt; / &lt;code&gt;ide2&lt;/code&gt;. It prints OK / WARN / FAIL with a fix hint per line, and it reads the VM config directly, so it works on VMs my tool never touched. If you built your macOS VM by hand from a guide, point it there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Still open: GPU passthrough
&lt;/h2&gt;

&lt;p&gt;The one thing I can't fix from here. Passing a real GPU into a macOS guest needs hardware I don't have to test against, and guessing at it is how you get bugs like #1 above. It's the top request and it's honestly blocked on a test rig.&lt;/p&gt;

&lt;p&gt;Everything else above is shipped and on &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/lucid-fabrics/osx-proxmox-next/main/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Repo: &lt;a href="https://github.com/lucid-fabrics/osx-proxmox-next" rel="noopener noreferrer"&gt;github.com/lucid-fabrics/osx-proxmox-next&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still a solo project. If you hit a bug on hardware I don't own, open an issue. That's how four of the five above got found.&lt;/p&gt;

</description>
      <category>proxmox</category>
      <category>macos</category>
      <category>homelab</category>
      <category>virtualization</category>
    </item>
    <item>
      <title>Proxmox Bluetooth Passthrough: Why It Silently Fails (and the One-Line Fix)</title>
      <dc:creator>Lucid Fabrics</dc:creator>
      <pubDate>Thu, 30 Jul 2026 14:02:39 +0000</pubDate>
      <link>https://dev.to/lucidfabrics/bluetooth-in-a-proxmox-vm-shouldnt-be-this-hard-2l1p</link>
      <guid>https://dev.to/lucidfabrics/bluetooth-in-a-proxmox-vm-shouldnt-be-this-hard-2l1p</guid>
      <description>&lt;p&gt;I run a gaming VM on Proxmox with an AMD GPU passed through. Steam works. Games work. Everything works except Bluetooth.&lt;/p&gt;

&lt;p&gt;My Xbox controller would blink forever trying to pair, and every fix I found online led to the same place: pass a USB Bluetooth device into the VM and watch it fail anyway.&lt;/p&gt;

&lt;p&gt;The forum threads all read the same way. Someone finds a controller. Someone else says "just pass the USB device through." The original poster tries it, and the thread goes quiet. No follow-up. No "it worked."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focmia7j6pzczd463ow3m.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Focmia7j6pzczd463ow3m.jpg" alt="Xbox controller connected inside the Proxmox gaming VM" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why passthrough fails
&lt;/h2&gt;

&lt;p&gt;My board has an Intel BE200, a Wi-Fi 7 and Bluetooth 5.4 combo card found on many modern motherboards. Intel onboard Bluetooth in the BE200, AX200, AX210, and AX211 family depends on the host driver loading firmware at boot.&lt;/p&gt;

&lt;p&gt;A passthrough handoff re-enumerates the USB device and clears that firmware. The guest must load it again. A full Debian, Ubuntu, or Arch guest may have the matching driver and firmware blobs, so passthrough can work there. Trimmed or immutable guests such as ChimeraOS, Bazzite, and Home Assistant OS often do not.&lt;/p&gt;

&lt;p&gt;In my case the guest got stuck here:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reading Intel version command failed (-110)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again and again. I spent hours thinking the chip was defective. It wasn't. The handoff was the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try plain USB passthrough first
&lt;/h2&gt;

&lt;p&gt;A regular USB Bluetooth dongle passed into a normal Linux distribution often works fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;qm &lt;span class="nb"&gt;set&lt;/span&gt; &amp;lt;vmid&amp;gt; &lt;span class="nt"&gt;-usb0&lt;/span&gt; &lt;span class="nv"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;vendor:product&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the guest reports a missing firmware blob, install it and reboot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Debian, with non-free-firmware enabled&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;firmware-iwlwifi

&lt;span class="c"&gt;# Ubuntu&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;linux-firmware

&lt;span class="c"&gt;# Fedora, ChimeraOS, or Bazzite&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;rpm-ostree &lt;span class="nb"&gt;install &lt;/span&gt;linux-firmware
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that fixes it, keep the simpler setup. You do not need this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked: keep the chip on the host
&lt;/h2&gt;

&lt;p&gt;Instead of handing the adapter to the VM, the Proxmox host keeps it initialized. &lt;code&gt;btproxy&lt;/code&gt;, a small tool from the official BlueZ source tree, opens the adapter in raw HCI mode and streams it over TCP.&lt;/p&gt;

&lt;p&gt;The VM runs &lt;code&gt;btproxy&lt;/code&gt; in reverse. It creates a virtual Bluetooth controller through &lt;code&gt;hci_vhci&lt;/code&gt; and feeds it the stream. BlueZ inside the guest sees a normal local adapter. The physical chip never resets, so there is no firmware handoff to fail.&lt;/p&gt;

&lt;p&gt;I packaged the setup as &lt;a href="https://github.com/lucid-fabrics/proxmox-bluetooth" rel="noopener noreferrer"&gt;proxmox-bluetooth&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;On the Proxmox host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/lucid-fabrics/proxmox-bluetooth/main/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer prints the matching command to run inside the VM. Paste that line there. Both services start at boot and reconnect automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6y7s9xtc5fzfbnc4r8m2.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6y7s9xtc5fzfbnc4r8m2.gif" alt="Installing the Bluetooth bridge on the host and VM" width="790" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;My Xbox controller paired within a minute. It has stayed paired through host reboots, VM reboots, and a full AC power cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check before installing
&lt;/h2&gt;

&lt;p&gt;Run this on the host:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/lucid-fabrics/proxmox-bluetooth/main/install.sh | &lt;span class="nb"&gt;sudo &lt;/span&gt;bash &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It lists each adapter and tells you whether it is healthy. If an Intel chip is stuck in its blank boot state, shut the machine down and switch off AC power for 15 seconds. A reboot is not enough because the radio can remain powered.&lt;/p&gt;

&lt;h2&gt;
  
  
  One security detail that matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;btproxy&lt;/code&gt; uses plain, unauthenticated TCP on port 9700. By default, the first machine on your LAN that connects gets the Bluetooth chip. Restrict it to the VM once you know its address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./install.sh &lt;span class="nt"&gt;--allow&lt;/span&gt; 192.168.1.50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That installs an nftables rule without interfering with &lt;code&gt;pve-firewall&lt;/code&gt;. Treat the bridge as a trusted-LAN tool. Do not expose it to the internet.&lt;/p&gt;

&lt;p&gt;The project is MIT licensed. The bundled bridge is unmodified BlueZ code under its upstream licenses, and CI rebuilds it from source on every commit to verify the binary byte for byte.&lt;/p&gt;

&lt;p&gt;Source and installation guide: &lt;a href="https://github.com/lucid-fabrics/proxmox-bluetooth" rel="noopener noreferrer"&gt;github.com/lucid-fabrics/proxmox-bluetooth&lt;/a&gt;&lt;/p&gt;

</description>
      <category>proxmox</category>
      <category>bluetooth</category>
      <category>homelab</category>
      <category>virtualization</category>
    </item>
    <item>
      <title>I Got Tired of Manually Configuring macOS VMs — So I Built a One-Click Solution</title>
      <dc:creator>Lucid Fabrics</dc:creator>
      <pubDate>Thu, 12 Feb 2026 01:15:07 +0000</pubDate>
      <link>https://dev.to/lucidfabrics/i-built-a-one-command-macos-vm-tool-for-proxmox-9-190j</link>
      <guid>https://dev.to/lucidfabrics/i-built-a-one-command-macos-vm-tool-for-proxmox-9-190j</guid>
      <description>&lt;p&gt;Every Proxmox user knows that moment. You want to spin up a macOS VM for testing, iOS development, or just playing around. And then the nightmare begins.&lt;/p&gt;

&lt;p&gt;You Google "macOS Proxmox guide". You find a tutorial from 2021. You copy-paste commands, tweak OpenCore configs, mess up one flag, and suddenly you are staring at a UEFI shell wondering what went wrong. Again.&lt;/p&gt;

&lt;p&gt;I lived this cycle for years. Every. Single. Time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pain Was Real
&lt;/h2&gt;

&lt;p&gt;I would spend hours:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finding a current guide (most are outdated)&lt;/li&gt;
&lt;li&gt;Chaining together &lt;code&gt;qm&lt;/code&gt; commands&lt;/li&gt;
&lt;li&gt;Manually generating SMBIOS serials&lt;/li&gt;
&lt;li&gt;Editing OpenCore config.plist files&lt;/li&gt;
&lt;li&gt;Praying it boots&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And when it finally worked? I would not save my config. So next time, I would do it all over again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I got fed up.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter osx-proxmox-next
&lt;/h2&gt;

&lt;p&gt;I built a tool that handles the entire VM creation process in a guided wizard. No manual commands. No config editing. Just answer a few questions and boom - your macOS VM is ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  What It Does
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;6-step TUI wizard&lt;/strong&gt; - Preflight → OS → Storage → Config → Dry Run → Install&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-detects your hardware&lt;/strong&gt; - CPU vendor, cores, RAM, storage pools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both Intel and AMD supported&lt;/strong&gt; - zero config needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-downloads everything&lt;/strong&gt; - OpenCore ISOs and macOS recovery images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generates SMBIOS identity&lt;/strong&gt; - unique serial, UUID, model for Apple Services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mandatory dry-run&lt;/strong&gt; - see every command before it runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frlly26izdp8z5jv7xk7a.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frlly26izdp8z5jv7xk7a.gif" alt="TUI wizard demo - 6-step guided macOS VM setup on Proxmox" width="800" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Quick Look
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;macOS&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ventura 13&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sonoma 14&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sequoia 15&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tahoe 26&lt;/td&gt;
&lt;td&gt;Stable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What's New (v0.29.3)
&lt;/h2&gt;

&lt;p&gt;Since this post first went up, a lot has shipped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Post-install boot fix&lt;/strong&gt; - one command fixes the boot order after macOS finishes installing. No more UEFI shell surprises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doctor&lt;/strong&gt; - &lt;code&gt;osx-next-cli doctor --vmid &amp;lt;id&amp;gt;&lt;/code&gt; gives any VM a health check and config comparison table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clone&lt;/strong&gt; - duplicate a macOS VM with a fresh SMBIOS identity in one command.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VM edit wizard&lt;/strong&gt; - change CPU, RAM, or storage through the same guided interface.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apple ID fix&lt;/strong&gt; - iCloud and iMessage work without manual kext hunting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No more memory warning&lt;/strong&gt; - OpenCore now ships RestrictEvents, silencing the MacPro7,1 memory misconfiguration warning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HEDT Xeon fix&lt;/strong&gt; - Xeon E5/E7 v2-v4 hosts no longer livelock during the installer copy phase. A safe emulated CPU model is picked automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Older hardware&lt;/strong&gt; - pre-Skylake Intel gets Penryn mode and an e1000 NIC automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs site&lt;/strong&gt; - full docs, FAQ, and troubleshooting at &lt;a href="https://lucid-fabrics.github.io/osx-proxmox-next/" rel="noopener noreferrer"&gt;lucid-fabrics.github.io/osx-proxmox-next&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;macOS on Proxmox is niche. Most guides are buried in forums or outdated. Existing scripts are rough - no validation, no dry run, no error handling.&lt;/p&gt;

&lt;p&gt;I wanted something that just works. Something I could trust. Something that shows me exactly what it will do before it does it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That is osx-proxmox-next.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/lucid-fabrics/osx-proxmox-next/main/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CLI Mode
&lt;/h2&gt;

&lt;p&gt;Prefer command line? Got you covered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Download assets&lt;/span&gt;
osx-next-cli download &lt;span class="nt"&gt;--macos&lt;/span&gt; sonoma

&lt;span class="c"&gt;# Preview (dry run)&lt;/span&gt;
osx-next-cli apply &lt;span class="nt"&gt;--vmid&lt;/span&gt; 910 &lt;span class="nt"&gt;--macos&lt;/span&gt; sequoia &lt;span class="nt"&gt;--cores&lt;/span&gt; 8 &lt;span class="nt"&gt;--memory&lt;/span&gt; 16384 &lt;span class="nt"&gt;--disk&lt;/span&gt; 128

&lt;span class="c"&gt;# Execute&lt;/span&gt;
osx-next-cli apply &lt;span class="nt"&gt;--execute&lt;/span&gt; &lt;span class="nt"&gt;--vmid&lt;/span&gt; 910 &lt;span class="nt"&gt;--macos&lt;/span&gt; sequoia &lt;span class="nt"&gt;--cores&lt;/span&gt; 8 &lt;span class="nt"&gt;--memory&lt;/span&gt; 16384 &lt;span class="nt"&gt;--disk&lt;/span&gt; 128
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  This Is Solo Work
&lt;/h2&gt;

&lt;p&gt;I maintain this project in my free time. No company. No team. Just me, trying to make macOS VMs on Proxmox less painful for anyone else who has to go through it.&lt;/p&gt;

&lt;p&gt;If this saves you hours of frustration - a star on the repo or a coffee would mean a lot.&lt;/p&gt;

&lt;p&gt;☕ &lt;a href="https://ko-fi.com/lucidfabrics" rel="noopener noreferrer"&gt;Ko-fi&lt;/a&gt; | &lt;a href="https://buymeacoffee.com/lucidfabrics" rel="noopener noreferrer"&gt;Buy Me a Coffee&lt;/a&gt; | ⭐ &lt;a href="https://github.com/lucid-fabrics/osx-proxmox-next" rel="noopener noreferrer"&gt;Star on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/lucid-fabrics/osx-proxmox-next" rel="noopener noreferrer"&gt;lucid-fabrics/osx-proxmox-next&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try it. Your future self will thank you.&lt;/p&gt;

</description>
      <category>proxmox</category>
      <category>macos</category>
      <category>homelab</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
