Skip to content

[Bug]: Docker support #1468

@yujqiao

Description

@yujqiao

What happened?

I run home assistant core in a docker container and got exceptions at

idxdata = btmgmt_sync.send('ReadControllerIndexList', None)

Unable to open PF_BLUETOOTH socket

I've followed the setcap instructions and no luck. I've also tried run btmgmt info in the container, but got the same error.

I think the root cause is moby/moby#16208 and the docker network namespace doesn't have access to the raw hci device. However, a workaround is mentioned in the issue, which is to bind mount the outside network namespace into the container, and then enter the namespace for the bluetooth process.

Based on the idea, I've made a patch and it worked on my instance.

The patch

  • hardcode the target network namespace /rootns/net
  • enter the namespace for any bluetooth opreations ( setns is a per thread call )
    • HCIdump will setns before enter the running loop
    • Upon init, bt_helper.py will start a temporary thread, setns and then call btmgmt to retrieve available bluetooth devices.

However I don't have the knowledge about home assistant config flow and don't know how to make it with ui and yaml configuration.

Here's my patch.

From 7176b3aa6db94015f0487a1c8cc013f6e9ab96a4 Mon Sep 17 00:00:00 2001
From: Yujia Qiao <ping@yqiao.me>
Date: Sat, 31 May 2025 15:54:35 +0800
Subject: [PATCH] run in target netns

Signed-off-by: Yujia Qiao <ping@yqiao.me>
---
 __init__.py    | 10 +++++++++-
 bt_helpers.py  | 24 +++++++++++++++++++++++-
 config_flow.py |  1 -
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/__init__.py b/__init__.py
index 8e989b1de53a..0fc88f895daa 100755
--- a/__init__.py
+++ b/__init__.py
@@ -4,6 +4,7 @@ import copy
 import json
 import logging
 from threading import Thread
+import os

 import aioblescan as aiobs
 import janus
@@ -19,7 +20,7 @@ from homeassistant.helpers.device_registry import DeviceEntry
 from homeassistant.util import dt

 from .ble_parser import BleParser
-from .bt_helpers import (BT_INTERFACES, BT_MULTI_SELECT, DEFAULT_BT_INTERFACE,
+from .bt_helpers import (BT_INTERFACES, BT_MULTI_SELECT, DEFAULT_BT_INTERFACE, NETNS,
                          reset_bluetooth)
 from .const import (AES128KEY24_REGEX, AES128KEY32_REGEX,
                     AUTO_BINARY_SENSOR_LIST, AUTO_MANUFACTURER_DICT,
@@ -626,6 +627,13 @@ class HCIdump(Thread):

     def run(self):
         """Run HCIdump thread."""
+        target_netns = NETNS
+        if target_netns:
+            # setns to the namespace located at target_netns
+            fd = os.open(target_netns, os.O_RDONLY)
+            os.setns(fd, os.CLONE_NEWNET)
+            os.close(fd)
+            _LOGGER.debug(f"Entered network namesapce {target_netns}")
         while True:
             _LOGGER.debug("HCIdump thread: Run")
             mysocket = {}
diff --git a/bt_helpers.py b/bt_helpers.py
index 19cb4d1c10c5..025d008af977 100755
--- a/bt_helpers.py
+++ b/bt_helpers.py
@@ -1,6 +1,8 @@
 """BT helpers for ble_monitor."""
 import logging
 import time
+import os
+import threading

 import pyric.utils.rfkill as rfkill
 from btsocket import btmgmt_protocol, btmgmt_sync
@@ -79,6 +81,24 @@ class MGMTBluetoothCtl:
             return True
         return False

+def run_in_netns(netns_path, func, *args, **kwargs):
+    result_container = {}
+
+    def target():
+        try:
+            with open(netns_path, 'r') as fd:
+                os.setns(fd.fileno(), os.CLONE_NEWNET)
+            result_container['result'] = func(*args, **kwargs)
+        except Exception as e:
+            result_container['error'] = e
+
+    thread = threading.Thread(target=target)
+    thread.start()
+    thread.join()
+
+    if 'error' in result_container:
+        raise result_container['error']
+    return result_container.get('result')

 # Bluetooth interfaces available on the system
 def hci_get_mac(iface_list=None):
@@ -159,7 +179,9 @@ def reset_bluetooth(hci):
         )


-BT_INTERFACES = hci_get_mac([0, 1, 2, 3])
+NETNS = '/rootns/net'
+
+BT_INTERFACES = run_in_netns(NETNS, lambda: hci_get_mac([0, 1, 2, 3]))
 if BT_INTERFACES:
     DEFAULT_BT_INTERFACE = list(BT_INTERFACES.items())[0][1]
     DEFAULT_HCI_INTERFACE = list(BT_INTERFACES.items())[0][0]

Sensor type

No response

Relevant log output

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions