# Controller discovery via DHCP

DHCP can be employed as a method for Agents to discover Controllers. The DHCPv4 Vendor-Identifying Vendor-Specific Information Option (option code 125) can be used to provide information to Agents about a single Controller.

## DHCP option 125 sub-options

The option 125 data must include the Broadband Forum Enterprise ID (3561, `0x00000DE9`). The following sub-options are recognised within that enterprise block:

| Sub-option | Code | Type | Description |
|---|---|---|---|
| URL | 25 | string | Controller endpoint URL. Supported schemes: `mqtt://`, `mqtts://`, `ws://`, `wss://`. Format: `scheme://host:port/path_or_topic`. |
| ProvisioningCode | 26 | string | Provisioning code written to the controller UCI section. |
| RetryMinimumWaitInterval | 27 | uint16 | Minimum wait interval in seconds before retrying a failed connection. |
| RetryIntervalMultiplier | 28 | uint16 | Retry interval multiplier applied on each failure (in 1/1000ths). |
| EndpointID | 29 | string | USP EndpointID of the controller. Used to match against existing discovered controllers. |

URLs using any scheme other than `mqtt://`, `mqtts://`, `ws://`, or `wss://` are silently ignored.
FQDN-only URLs (no scheme) are detected but not yet processed. This has been queued in future TODO lists.

## First-boot credential provisioning

The script `/etc/uci-defaults/94-obuspa_set_credential` runs exactly once at first boot (via the uci-defaults mechanism). It writes the device-identity credentials that will later be injected into every MQTT controller discovered via DHCP.

**Username** — assembled as `{OUI}-{SerialNumber}`:
- OUI is read via `get_manufacturer_oui()`, falling back to `db -q get device.deviceinfo.ManufacturerOUI`
- SerialNumber is read via `get_serial_number()`, falling back to `db -q get device.deviceinfo.SerialNumber`
- Example: `AABBCC-SN12345678`

**Password** — read via `get_acs_password()`. Falls back to `iopsys` if the call fails or returns empty.

These values are stored in the UCI global section:
```
config obuspa 'global'
        option username '{OUI}-{SerialNumber}'
        option password '{acs_password}'
```

When the DHCP hook script creates a `dhcpmqtt` section for a newly discovered MQTT or MQTTS controller, it reads these values and writes them as `Username` and `Password` on the mqtt section.

## Enable DHCP discovery

To enable controller discovery via DHCP, the Agent must have `dhcp_discovery` set to `1` (the default) in its UCI global section. The `interface` option specifies which network interface to watch for DHCP events.

```
config obuspa 'global'
        option enabled '1'
        option interface 'wan'
        option dhcp_discovery '1'
```

| Option | Definition | Default |
|---|---|---|
| enabled | Enable/disable obuspa daemon | 1 |
| interface | Network interface to watch for DHCP events | wan |
| dhcp_discovery | Enable DHCP controller discovery | 1 |

If `interface` is not set, the script auto-detects it: a device acting as a DHCP client on its LAN (`network.lan.proto == dhcp`) sets the interface to `lan`; otherwise it defaults to `wan`.

## Configuring DHCP option 125 on the server

OBUSPA supports MQTT and WebSocket controllers discovered via DHCP. The controller URL in sub-option 25 determines the protocol and encryption settings. Both a mandatory `URL` (sub-option 25) and `EndpointID` (sub-option 29) must be present for a new controller to be created.

The examples below use these consistent values:

- ProvisioningCode: `iopsys-client`
- EndpointID: `self::controller.iopsystest.com`
- Host: `10.0.0.1`

### MQTT (`mqtt://`)

Use for a controller reachable via an unencrypted MQTT broker.

URL format: `mqtt://host:port/topic`

The path component is the MQTT topic on which the controller listens for USP messages.

**Resulting UCI configuration written by the discovery script:**

```
config controller 'dhcpcontroller'
        option dhcp_discovered '1'
        option Enable '1'
        option EndpointID 'self::controller.iopsystest.com'
        option ProvisioningCode 'iopsys-client'
        option Protocol 'MQTT'
        option Topic '/usp/controller'
        option mqtt 'dhcpmqtt'
        option assigned_role_name 'full_access'

config mqtt 'dhcpmqtt'
        option Enable '1'
        option BrokerAddress '10.0.0.1'
        option BrokerPort '1883'
        option TransportProtocol 'TCP/IP'
        option ProtocolVersion '5.0'
        option Username 'AABBCC-SN12345678'
        option Password 'device-acs-password'

config mtp 'dhcpmtp'
        option Enable '1'
        option Protocol 'MQTT'
        option ResponseTopicConfigured '/usp/AABBCC_SN12345678/endpoint'
        option mqtt 'dhcpmqtt'
```

**Option 125 hex encoding** for URL `mqtt://10.0.0.1:1883/usp/controller`:

```
00:00:0D:E9:55:19:23:6D:71:74:74:3A:2F:2F:31:30:2E:30:2E:30:2E:31:3A:31:38:38:33:2F:75:73:70:2F:63:6F:6E:74:72:6F:6C:6C:65:72:1A:0D:69:6F:70:73:79:73:2D:63:6C:69:65:6E:74:1D:1F:73:65:6C:66:3A:3A:63:6F:6E:74:72:6F:6C:6C:65:72:2E:69:6F:70:73:79:73:74:65:73:74:2E:63:6F:6D
```

| Bytes | Hex | Decoded |
|---|---|---|
| Enterprise ID | `00:00:0D:E9` | BBF Enterprise ID (3561) |
| Total length | `55` | 85 bytes of sub-option data |
| Sub-option code | `19` | URL (25) |
| URL length | `23` | 35 bytes |
| URL value | `6D:71:74:74:3A:2F:2F:31:30:2E:30:2E:30:2E:31:3A:31:38:38:33:2F:75:73:70:2F:63:6F:6E:74:72:6F:6C:6C:65:72` | `mqtt://10.0.0.1:1883/usp/controller` |
| Sub-option code | `1A` | ProvisioningCode (26) |
| PC length | `0D` | 13 bytes |
| PC value | `69:6F:70:73:79:73:2D:63:6C:69:65:6E:74` | `iopsys-client` |
| Sub-option code | `1D` | EndpointID (29) |
| EID length | `1F` | 31 bytes |
| EID value | `73:65:6C:66:3A:3A:63:6F:6E:74:72:6F:6C:6C:65:72:2E:69:6F:70:73:79:73:74:65:73:74:2E:63:6F:6D` | `self::controller.iopsystest.com` |

### MQTT over TLS (`mqtts://`)

Use for a controller broker that requires TLS.

URL format: `mqtts://host:port/topic`

The UCI configuration is identical to the plain MQTT case except:
- `BrokerPort '8883'`
- `TransportProtocol 'TLS'`

**Resulting UCI configuration written by the discovery script:**

```
config controller 'dhcpcontroller'
        option dhcp_discovered '1'
        option Enable '1'
        option EndpointID 'self::controller.iopsystest.com'
        option ProvisioningCode 'iopsys-client'
        option Protocol 'MQTT'
        option Topic '/usp/controller'
        option mqtt 'dhcpmqtt'
        option assigned_role_name 'full_access'

config mqtt 'dhcpmqtt'
        option Enable '1'
        option BrokerAddress '10.0.0.1'
        option BrokerPort '8883'
        option TransportProtocol 'TLS'
        option ProtocolVersion '5.0'
        option Username 'AABBCC-SN12345678'
        option Password 'device-acs-password'

config mtp 'dhcpmtp'
        option Enable '1'
        option Protocol 'MQTT'
        option ResponseTopicConfigured '/usp/AABBCC_SN12345678/endpoint'
        option mqtt 'dhcpmqtt'
```

**Option 125 hex encoding** for URL `mqtts://10.0.0.1:8883/usp/controller`:

```
00:00:0D:E9:56:19:24:6D:71:74:74:73:3A:2F:2F:31:30:2E:30:2E:30:2E:31:3A:38:38:38:33:2F:75:73:70:2F:63:6F:6E:74:72:6F:6C:6C:65:72:1A:0D:69:6F:70:73:79:73:2D:63:6C:69:65:6E:74:1D:1F:73:65:6C:66:3A:3A:63:6F:6E:74:72:6F:6C:6C:65:72:2E:69:6F:70:73:79:73:74:65:73:74:2E:63:6F:6D
```

| Bytes | Hex | Decoded |
|---|---|---|
| Enterprise ID | `00:00:0D:E9` | BBF Enterprise ID (3561) |
| Total length | `56` | 86 bytes of sub-option data |
| Sub-option code | `19` | URL (25) |
| URL length | `24` | 36 bytes |
| URL value | `6D:71:74:74:73:3A:2F:2F:31:30:2E:30:2E:30:2E:31:3A:38:38:38:33:2F:75:73:70:2F:63:6F:6E:74:72:6F:6C:6C:65:72` | `mqtts://10.0.0.1:8883/usp/controller` |
| Sub-option code | `1A` | ProvisioningCode (26) |
| PC length | `0D` | 13 bytes |
| PC value | `69:6F:70:73:79:73:2D:63:6C:69:65:6E:74` | `iopsys-client` |
| Sub-option code | `1D` | EndpointID (29) |
| EID length | `1F` | 31 bytes |
| EID value | `73:65:6C:66:3A:3A:63:6F:6E:74:72:6F:6C:6C:65:72:2E:69:6F:70:73:79:73:74:65:73:74:2E:63:6F:6D` | `self::controller.iopsystest.com` |

### WebSocket (`ws://`)

Use for a controller reachable via an unencrypted WebSocket connection.

URL format: `ws://host:port/path`

**Resulting UCI configuration written by the discovery script:**

```
config controller 'dhcpcontroller'
        option dhcp_discovered '1'
        option Enable '1'
        option EndpointID 'self::controller.iopsystest.com'
        option ProvisioningCode 'iopsys-client'
        option Protocol 'WebSocket'
        option Host '10.0.0.1'
        option Port '80'
        option Path '/usp/endpoint'
        option EnableEncryption '0'
        option assigned_role_name 'full_access'

config mtp 'dhcpmtp'
        option Enable '1'
        option Protocol 'WebSocket'
        option Port '80'
        option EnableEncryption '0'
```

**Option 125 hex encoding** for URL `ws://10.0.0.1:80/usp/endpoint`:

```
00:00:0D:E9:4F:19:1D:77:73:3A:2F:2F:31:30:2E:30:2E:30:2E:31:3A:38:30:2F:75:73:70:2F:65:6E:64:70:6F:69:6E:74:1A:0D:69:6F:70:73:79:73:2D:63:6C:69:65:6E:74:1D:1F:73:65:6C:66:3A:3A:63:6F:6E:74:72:6F:6C:6C:65:72:2E:69:6F:70:73:79:73:74:65:73:74:2E:63:6F:6D
```

| Bytes | Hex | Decoded |
|---|---|---|
| Enterprise ID | `00:00:0D:E9` | BBF Enterprise ID (3561) |
| Total length | `4F` | 79 bytes of sub-option data |
| Sub-option code | `19` | URL (25) |
| URL length | `1D` | 29 bytes |
| URL value | `77:73:3A:2F:2F:31:30:2E:30:2E:30:2E:31:3A:38:30:2F:75:73:70:2F:65:6E:64:70:6F:69:6E:74` | `ws://10.0.0.1:80/usp/endpoint` |
| Sub-option code | `1A` | ProvisioningCode (26) |
| PC length | `0D` | 13 bytes |
| PC value | `69:6F:70:73:79:73:2D:63:6C:69:65:6E:74` | `iopsys-client` |
| Sub-option code | `1D` | EndpointID (29) |
| EID length | `1F` | 31 bytes |
| EID value | `73:65:6C:66:3A:3A:63:6F:6E:74:72:6F:6C:6C:65:72:2E:69:6F:70:73:79:73:74:65:73:74:2E:63:6F:6D` | `self::controller.iopsystest.com` |

### WebSocket over TLS (`wss://`)

Use for a controller that requires a TLS WebSocket connection.

URL format: `wss://host:port/path`

The UCI configuration is identical to the plain WebSocket case except:
- `Port '443'`
- `EnableEncryption '1'`

**Resulting UCI configuration written by the discovery script:**

```
config controller 'dhcpcontroller'
        option dhcp_discovered '1'
        option Enable '1'
        option EndpointID 'self::controller.iopsystest.com'
        option ProvisioningCode 'iopsys-client'
        option Protocol 'WebSocket'
        option Host '10.0.0.1'
        option Port '443'
        option Path '/usp/endpoint'
        option EnableEncryption '1'
        option assigned_role_name 'full_access'

config mtp 'dhcpmtp'
        option Enable '1'
        option Protocol 'WebSocket'
        option Port '443'
        option EnableEncryption '1'
```

**Option 125 hex encoding** for URL `wss://10.0.0.1:443/usp/endpoint`:

```
00:00:0D:E9:51:19:1F:77:73:73:3A:2F:2F:31:30:2E:30:2E:30:2E:31:3A:34:34:33:2F:75:73:70:2F:65:6E:64:70:6F:69:6E:74:1A:0D:69:6F:70:73:79:73:2D:63:6C:69:65:6E:74:1D:1F:73:65:6C:66:3A:3A:63:6F:6E:74:72:6F:6C:6C:65:72:2E:69:6F:70:73:79:73:74:65:73:74:2E:63:6F:6D
```

| Bytes | Hex | Decoded |
|---|---|---|
| Enterprise ID | `00:00:0D:E9` | BBF Enterprise ID (3561) |
| Total length | `51` | 81 bytes of sub-option data |
| Sub-option code | `19` | URL (25) |
| URL length | `1F` | 31 bytes |
| URL value | `77:73:73:3A:2F:2F:31:30:2E:30:2E:30:2E:31:3A:34:34:33:2F:75:73:70:2F:65:6E:64:70:6F:69:6E:74` | `wss://10.0.0.1:443/usp/endpoint` |
| Sub-option code | `1A` | ProvisioningCode (26) |
| PC length | `0D` | 13 bytes |
| PC value | `69:6F:70:73:79:73:2D:63:6C:69:65:6E:74` | `iopsys-client` |
| Sub-option code | `1D` | EndpointID (29) |
| EID length | `1F` | 31 bytes |
| EID value | `73:65:6C:66:3A:3A:63:6F:6E:74:72:6F:6C:6C:65:72:2E:69:6F:70:73:79:73:74:65:73:74:2E:63:6F:6D` | `self::controller.iopsystest.com` |

## How DHCP discovery works

OBUSPA installs a udhcpc hook script at `/etc/udhcpc.user.d/udhcpc_obuspa_opt125.user`. udhcpc invokes this script on every DHCP event and passes the raw option data as environment variables. The script processes option 125 in the following sequence:

1. **Interface determination.** The script reads `obuspa.global.interface` from UCI. If not set, it auto-detects based on access role (see [Access role assignment](#access-role-assignment)) and writes the result back to UCI. The DHCP event's `$INTERFACE` env var is then compared against the configured interface. If they do not match, the script exits immediately without taking any action.

2. **Discovery-enabled check.** The script reads `obuspa.global.dhcp_discovery`. If `0`, the script exits. The default is `1`.

3. **Option 125 parsing.** The script reads the `$opt125` environment variable provided by udhcpc (a hex string without separators). It scans for BBF Enterprise ID `0x00000DE9` (decimal 3561). If no matching enterprise block is found, the script exits. Within that block it iterates sub-options and extracts URL (25), ProvisioningCode (26), RetryMinimumWaitInterval (27), RetryIntervalMultiplier (28), and EndpointID (29).

4. **URL scheme dispatch.** The URL scheme is mapped to UCI protocol and transport settings:

   | URL scheme | Protocol | Transport / encryption setting |
   |---|---|---|
   | `mqtt://` | MQTT | `TransportProtocol = TCP/IP` |
   | `mqtts://` | MQTT | `TransportProtocol = TLS` |
   | `ws://` | WebSocket | `EnableEncryption = 0` |
   | `wss://` | WebSocket | `EnableEncryption = 1` |
   | anything else | — | script exits, no action taken |

5. **Controller lookup.** Before creating new UCI sections, the script searches existing UCI controller sections for a match using two criteria, in order:
   - Any section with `dhcp_discovered = 1` is treated as the previously-discovered DHCP controller and is updated in place.
   - Otherwise, any section whose `EndpointID` matches sub-option 29 is also considered a match and updated in place.
   - If no match is found and both URL and EndpointID sub-options are present, new UCI sections are created.

6. **UCI section creation.** For a newly discovered controller, three sections are written:
   - `config controller 'dhcpcontroller'` — holds EndpointID, ProvisioningCode, Protocol, and the `dhcp_discovered '1'` marker flag. For MQTT: `Topic` and a `mqtt` reference to `dhcpmqtt`. For WebSocket: `Host`, `Port`, `Path`, `EnableEncryption`.
   - `config mqtt 'dhcpmqtt'` — holds BrokerAddress, BrokerPort, TransportProtocol, ProtocolVersion, Username, Password. Created for MQTT and MQTTS only.
   - `config mtp 'dhcpmtp'` — holds Protocol, Enable. For MQTT: `ResponseTopicConfigured` and a `mqtt` reference to `dhcpmqtt`. For WebSocket: `Port` and `EnableEncryption`.

7. **MQTT credential injection.** For MQTT and MQTTS protocols, the script reads `obuspa.global.username` and `obuspa.global.password` (provisioned at first boot by `94-obuspa_set_credential`) and writes them as `Username` and `Password` on the `dhcpmqtt` section.

8. **MQTT agent response topic.** For MQTT and MQTTS protocols, the agent needs a topic on which it listens for incoming USP messages. This topic is derived from the **agent's** EndpointID (read from `obuspa.localagent.EndpointID`, or assembled as `{OUI}-{SerialNumber}` if that option is unset): all non-alphanumeric characters are replaced with `_`, then the result is formatted as `/usp/{sanitized}/endpoint`. This value is written to `dhcpmtp.ResponseTopicConfigured`.

   Example: agent EndpointID `proto::device-001` → response topic `/usp/proto__device_001/endpoint`

9. **UCI commit and restart.** If any UCI change was made, the script calls `uci commit obuspa` and deletes `/etc/obuspa/.boot` to force obuspa to re-read UCI on next start. If `/tmp/obuspa/service_mark` exists (indicating the daemon is running), the script calls `init.d/obuspa stop` followed by `init.d/obuspa start` so the new configuration takes effect immediately.

## Access role assignment

When the DHCP hook script creates a controller section, it automatically assigns a role based on the network role of the device. The role is determined by inspecting `network.lan.proto`:

| Condition | Assigned role |
|---|---|
| `network.lan.proto == dhcp` | `extender` |
| Any other value | `full_access` |

A device whose LAN interface obtains its address via DHCP is operating in extender mode (it is not the network gateway) and receives the restricted `extender` role. Devices acting as a gateway receive the `full_access` role.

The role is written to `dhcpcontroller.assigned_role_name`. These role names must exist as entries in `Device.LocalAgent.ControllerTrust.Role.*.Name` on the device. If the named role does not exist, OBUSPA falls back to the Untrusted role.

The same logic governs the auto-detected interface: an extender device sets `obuspa.global.interface = lan`; a gateway device uses `wan`.

For details on defining factory-default roles, see [how_to_setup_factory_default_roles.md](how_to_setup_factory_default_roles.md).
