Curl an IP address on the local network

I have a multi container project running on a raspberrypi3, setup with docker-compose. One of the containers it is running is Home Assistant. Another creates a wifi network from the raspberry pi.
A number of IoT devices are connected to this wifi network.

Some of those IoT devices periodically require a firmware update. This can be done over the air by curling an endpoint - eg http://10.42.0.57/ota?update=true
This works fine from the Host OS, but I would like to run this from the Home Assistant container as part of an automation there.

When I try to do this, I get this error:

[homeassistant.components.rest_command] Client error http://10.42.0.57/ota?update=true

When I try to run this manually from the command line when logged into the HA container, I get this error:

curl http://10.42.0.57/ota?update=true
curl: (7) Failed to connect to 10.42.0.57 port 80: Connection refused

The same command works as expected on the Host OS.

How do I configure this container to safely allow connection to an IP address on the local network?

Thanks

Hello @peteretep thank you for your message!

could you please confirm the Home Assistant app that you are using? is this from balenaHub ? If that’s the case i see that the container is privileged and that there are some ports redirections:

ports:
      - 80:8123

Could you please test this port redirection?

Hello Peter, thank you for your message!

What HomeAssistant application are you using? the one from balenaHub

I see that the HomeAssistant app on balenaHub has the container privileged and the port 80 redirected to 8123. Could you please test that?

Hi @mpous - thanks for the reply. Not using the balenaHub version, but it is similar.
Docker compose file looks like this:

homeassistant:
    build:
      dockerfile: ./docker/Dockerfile
      context: ./homeassistant
      args:
        ha_machine_name: raspberrypi3
        ha_version: 2021.3.3
        ven_version: 0.4.8
    ports:
      - "80:8123"
    restart: always

It’s not a privileged container - should it be? What are the downsides to making it privileged?

Hey @peteretep, is the wifi network created by the other service using a unique subnet different from the one on the RPi eth0 interface? Can these IoT devices on the wifi network otherwise communicate with HA? It might help to see the entire docker-compose file including the service that creates the wifi network since the issue appears to be communication between the two networks.

The wifi network is created by the hems-supervisor service, using a python script that looks like this:

#!/usr/bin/env python
import logging
import os
import time
import typing
import uuid

import dbus
import requests
import sentry_sdk

logging.basicConfig(level=logging.DEBUG)

DEBUG = False

def get_device_name_and_uuid() -> typing.Tuple[str, str]:
    """
    Get the Balena device name and UUID from environment variables.
    """
    BALENA_DEVICE_NAME_AT_INIT = ""
    BALENA_DEVICE_UUID = ""

    if "BALENA_DEVICE_UUID" in os.environ.keys():
        BALENA_DEVICE_UUID = os.environ["BALENA_DEVICE_UUID"]
    else:
        logging.warning("$BALENA_DEVICE_UUID not set in environment!")

    if "BALENA_DEVICE_NAME_AT_INIT" in os.environ.keys():
        BALENA_DEVICE_NAME_AT_INIT = os.environ["BALENA_DEVICE_NAME_AT_INIT"]
    else:
        logging.warning("$BALENA_DEVICE_NAME_AT_INIT not set in environment!")

    logging.debug(
        "BALENA_DEVICE_NAME_AT_INIT={},BALENA_DEVICE_UUID={}".format(
            BALENA_DEVICE_NAME_AT_INIT, BALENA_DEVICE_UUID
        )
    )

    return BALENA_DEVICE_NAME_AT_INIT, BALENA_DEVICE_UUID


def setup_network(device_name: str, device_uuid: str):
    """
    This function configures the network on the host by sending a message to the host DBUS based on:
    https://www.balena.io/docs/reference/OS/network/2.x/#changing-the-network-at-runtime

    It broadly maps onto nm-settings conf (ref: https://developer.gnome.org/NetworkManager/stable/nm-settings.html).

    If changes are required: to infer DBUS signatures used for marshalling data you need to cross-reference nm-settings with DBUS type signatures defined in https://dbus.freedesktop.org/doc/api/html/group__DBusProtocol.html .
    """

    s_con = dbus.Dictionary(
        {
            "id": "balena-hotspot",
            "type": "802-11-wireless",
            "autoconnect": dbus.Boolean(True),
            "autoconnect-retries": dbus.types.Int32(0),
            "interface-name": "wlan0",
            "mdns": dbus.types.Int32(1),
        }
    )

    s_wifi = dbus.Dictionary(
        {
            "band": "bg",
            "mode": "ap",
            "ssid": dbus.ByteArray(
                str("ccoop-hems-{}".format(device_uuid[:4])).encode("utf8")
            ),
            "powersave": dbus.types.UInt32(2),
        }
    )

    s_wsec = dbus.Dictionary(
        {
            "key-mgmt": "wpa-psk",
            "proto": dbus.types.Array([b"rsn"], "s"),
            "psk": device_name,
        }
    )

    s_ip4 = dbus.Dictionary({"method": "shared"})

    s_ip6 = dbus.Dictionary(
        {
            "method": "ignore",
        }
    )

    con = dbus.Dictionary(
        {
            "connection": s_con,
            "802-11-wireless": s_wifi,
            "802-11-wireless-security": s_wsec,
            "ipv4": s_ip4,
            "ipv6": s_ip6,
        }
    )

    bus = dbus.SystemBus()

    proxy = bus.get_object(
        "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/Settings"
    )
    settings = dbus.Interface(proxy, "org.freedesktop.NetworkManager.Settings")
    ##
    logging.debug(
        "Network initialisation successful. The following network config was sent to host dbus:"
        + str(con)
    )

    settings.AddConnection(con)


def restart_supervisor_service():
    """
    Use balena supervisor API to restart the container in hope of BALENA_DEVICE_NAME_AT_INIT and BALENA_DEVICE_UUID being initialised next time!
    """
    logging.warning(
        "BALENA_DEVICE_UUID and/or BALENA_NAME_AT_INIT is not set in environment!!! Attempting to restart hems-supervisor container..."
    )
    BALENA_SUPERVISOR_ADDRESS = os.environ["BALENA_SUPERVISOR_ADDRESS"]
    BALENA_APP_ID = os.environ["BALENA_APP_ID"]
    BALENA_SUPERVISOR_API_KEY = os.environ["BALENA_SUPERVISOR_API_KEY"]

    request_url = "{}/v2/applications/{}/restart-service?apikey={}".format(
        BALENA_SUPERVISOR_ADDRESS, BALENA_APP_ID, BALENA_SUPERVISOR_API_KEY
    )
    request_data = {"serviceName": "hems-supervisor"}
    logging.debug(
        "Attempting to restart supervisor ... url: {} data: {}".format(
            request_url, request_data
        )
    )
    while True:
        try:
            response = requests.post(request_url, data=request_data, timeout=10)
        except:
            logging.error(
                "Whilst attempting to connect to supervisor API to restart an exception occurred! Retrying... "
            )

    # Should never get to here as the service should restart
    return


if __name__ == "__main__":

    # Get device name and UUID from env variables
    device_name, device_uuid = get_device_name_and_uuid()
    if DEBUG:
        device_name = ""
        device_uuid = ""
    logging.debug(f"device_name={device_name},device_uuid={device_uuid}")
    # Check to see if device name and UUID initialised properly, if not restart container and hope for the best!
    if device_name == "" or device_uuid == "":
        time.sleep(3)  # delay restart
        restart_supervisor_service()

    # If name/UUID set start wifi
    setup_network(device_name, device_uuid)

Full docker compose file:

version: "2"
volumes:
  carboncoop-hems-shared-data:
  carboncoop-hems-homeassistant-data:
  carboncoop-hems-homeassistant-config-storage:
  carboncoop-hems-mosquitto-data:
  carboncoop-hems-supervisor-data:
services:
  homeassistant:
    build:
      dockerfile: ./docker/Dockerfile
      context: ./homeassistant
      args:
        ha_machine_name: raspberrypi3
        ha_version: 2021.3.3
        ven_version: 0.4.8
    ports:
      - "80:8123"
    restart: always
    volumes:
      - carboncoop-hems-shared-data:/carboncoop-hems-shared-data
      - carboncoop-hems-homeassistant-data:/carboncoop-hems-homeassistant-data
      - carboncoop-hems-homeassistant-config-storage:/config/.storage
    environment:
      CCOOP_HEMS_CA_CERT_URL:
      CCOOP_HEMS_HUB_REG_URL: 
      CCOOP_HEMS_VTN_ADDRESS: 
  mosquitto:
    image: arm32v6/eclipse-mosquitto:1.6
    ports:
      - "1883:1883"
    restart: always
    volumes:
      - carboncoop-hems-shared-data:/carboncoop-hems-shared-data
      - carboncoop-hems-mosquitto-data:/carboncoop-hems-mosquitto-data
  hems-supervisor:
    build:
      dockerfile: ./docker/Dockerfile
      context: ./supervisor
    restart: always
    volumes:
      - carboncoop-hems-shared-data:/carboncoop-hems-shared-data
      - carboncoop-hems-supervisor-data:/carboncoop-hems-supervisor-data
    command: "/bin/sh -c 'while :; do python /usr/src/app/network.py; sleep 24h;done;'"
    environment:
      - DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket
    labels:
      io.balena.features.dbus: "true"
      io.balena.features.supervisor-api: "true"
  telegraf:
    build:
      dockerfile: ./docker/Dockerfile
      context: ./telegraf
    restart: always
    volumes:
      - carboncoop-hems-shared-data:/carboncoop-hems-shared-data
    environment:
      TELEMETRY_SERVER_URL:

The IoT devices are communicating with the mosquitto service, and that is communicating with HA.

Hey @peteretep, thanks for sharing those detailed files! That’s quite a nice setup you have there.

My theory at the moment is when wlan0 is created on the hostOS via dbus (via hems-supervisor) the HA service is already running and does not get that interface mapped as a route in the container. We can test this a few ways:

  • restart the HA service after the wlan0 interface has been created and see if you can curl the IoT device
  • run the route command from inside the HA container to see if any routes exist for the IoT subnet
  • temporarily run the HA service in host network mode (I expect this to work, but only as a test)

If the issue is indeed that the HS container doesn’t receive the new routes, it may be possible to hardcode the expected route with an HA startup script so it’s already there when wlan0 is created.

Thanks for the reply.
I have restarted the HA service on a running system with the WiFi up and run route on the HA container.

Both before and after restart it gives the same:

bash-5.0# route
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
default         172.17.0.1      0.0.0.0         UG    0      0        0 eth0
172.17.0.0      *               255.255.0.0     U     0      0        0 eth0
bash-5.0# curl http://10.42.0.59/ota
curl: (7) Failed to connect to 10.42.0.59 port 80: Connection refused

Hey @peteretep, it looks like we have ruled out a race condition. I guess it makes sense that the routes always look the same from inside the container, it’s the bridge that’s missing the extra routing, not the container.

I think your best bet is to enable host networking for the HA service to guarantee it always has access to the wlan0 IoT devices.

As a long-shot you could try adding an additional network in your compose file in order to provide the new route, but I haven’t been able to test it yet as all of my devices are on the same subnet and it may still not achieve the additional routing on the host that we need. Feel free to try it yourself though as it won’t break anything.

homeassistant:
    build:
      dockerfile: ./docker/Dockerfile
      context: ./homeassistant
      args:
        ha_machine_name: raspberrypi3
        ha_version: 2021.3.3
        ven_version: 0.4.8
    ports:
      - "80:8123"
    restart: always
    networks:
      - default
      - iot_net

networks:
  iot_net:
    driver: bridge
    ipam:
      driver: default
      config:
        - subnet: 10.42.0.0/24
          gateway: 10.42.0.1

Hi, I would like to follow up on this, were you able to solve the issue? Otherwise have you tried host networking as my colleague suggested? Thanks

Thanks and sorry for the delay - I have been pulled away from this problem for the moment so haven’t had a chance to try.

No problem, thanks for the update.