# ACS URL Discovery over PPPoE Interface
### Feature Guide · TR-069 / CWMP

---

|                    |                                              |
|--------------------|----------------------------------------------|
| **Standard**       | Broadband Forum TR-069                       |
| **Version**        | 1.0 — May 2026                               |
| **Prepared by**    | Suvendhu Hansa                               |

---

## Table of Contents

1. [Overview](#1-overview)
2. [Background & Standards](#2-background-and-standards-reference)
3. [Server-Side Configuration](#3-server-side-configuration)
4. [Design Rationale](#4-design-rationale)
5. [Implementation](#5-implementation)
6. [Protocol Reference](#6-protocol-reference)
7. [Deployment](#7-deployment)
8. [Summary](#8-summary)

---

## 1. Overview

This document describes ACS (Auto-Configuration Server) URL discovery for TR-069-compliant
CPE devices operating over a PPPoE WAN interface. The feature follows the TR-069 requirement
for **post-IP bootstrap**, where a device that has already obtained an IP address through
IPCP must use **DHCPINFORM** — rather than **DHCPDISCOVER** — to retrieve additional
configuration options such as the ACS URL from a DHCP server.

A custom C utility (`pppoe-acs-discover`) constructs and transmits a compliant DHCPINFORM
packet carrying DHCP **Option 60** (Vendor Class Identifier set to `dslforum.org`) and
requesting **Option 43** (Vendor Encapsulated Options containing the ACS URL).

```
┌─────────────────────────────────────────────────────────────────────┐
│                                                                     │
│  DHCPINFORM sent over pppoe-wan  --►  DHCPACK received with         │
│  Option 43 (TLV sub-option 1)    --►  ACS URL parsed and applied    │
│  for TR-069 client configuration.     Full end-to-end integration.  │
└─────────────────────────────────────────────────────────────────────┘
```

---

## 2. Background and Standards Reference

### 2.1 TR-069 ACS Bootstrap Requirement

The TR-069 specification defines the CPE WAN Management Protocol (CWMP), which enables
remote management of CPE devices by an ACS. For a CPE to initiate a TR-069 session, it
must first discover the URL of the ACS. One of the defined discovery mechanisms involves
DHCP **Option 43** (Vendor Encapsulated Options), where the CPE identifies itself with
**Option 60** set to `dslforum.org` and requests the ACS URL in return.

For a PPPoE device, the IP address is assigned via IPCP (IP Control Protocol). Because
the IP is already assigned, standard DHCP rules (RFC 2131) dictate that if a device needs
additional configuration options without requesting a new IP, it must use DHCPINFORM
populating `ciaddr` with its already assigned address.
This is the "Post-IP" bootstrap step — the device already has connectivity but needs to
discover its management endpoint.

---

### 2.2 Post-IP Bootstrap — Sequence Diagram

```
  CPE  ·  pppoe-wan (10.102.95.100)           Server  ·  ppp0 (10.102.95.1)
  ─────────────────────────────────────────────────────────────────────────────

          │                                                      │
  ────────┤  [1] PPPoE session up                                │
          │      IPCP assigns 10.102.95.100 to CPE               │
          │                                                      │
          │──── DHCPINFORM ─────────────────────────────────────►│
          │     src    10.102.95.100 : 68                        │  [2] vendor class
          │     dst    255.255.255.255 : 67                      │      "dslforum.org"
          │     ciaddr 10.102.95.100                             │      matched
          │     Opt 53 = 8 (INFORM)                              │      → build
          │     Opt 60 = "dslforum.org"                          │        Opt 43 TLV
          │     Opt 55 = [43]  flags = unicast                   │
          │                                                      │
          │◄─── DHCPACK ─────────────────────────────────────────│
          │     src    10.102.95.1 : 67                          │
          │     dst    10.102.95.100 : 68                        │
          │     Opt 53 = 5 (ACK)                                 │
          │     Opt 43 = TLV { 01 : 1d : https://acs... }        │
          │     Opt 60 = "dslforum.org"                          │
          │                                                      │
  ────────┤  [3] parse ACS URL from Option 43 sub-option 1       │
          │      uci set cwmp.acs.dhcp_url = <url>               │
          │      ubus call uci commit                            │
          │                                                      │
          │──── TR-069 SESSION BEGINS ──────────────────────────►│
          │                                                      │
```

---

### 2.3 DHCP Option 43 — TLV Byte Layout

Option 43 wraps the ACS URL in a TLV (Type-Length-Value) sub-option:

```
  byte 0      byte 1      bytes 2 … N+1
  ┌──────────┬──────────┬─────────────────────────────────────────────┐
  │   0x01   │   0x1d   │  68 74 74 70 73 3a 2f 2f 61 63 73 2e …      │
  ├──────────┼──────────┼─────────────────────────────────────────────┤
  │  Type    │  Length  │  Value  (ASCII)                             │
  │  = 1     │  = 29    │  "https://acs.example.com:7547/"            │
  │  ACS URL │          │                                             │
  └──────────┴──────────┴─────────────────────────────────────────────┘
```

Full hex encoding for `https://acs.example.com:7547/`:

```
01 1d  68 74 74 70 73 3a 2f 2f 61 63 73 2e 65 78 61
       6d 70 6c 65 2e 63 6f 6d 3a 37 35 34 37 2f
```

| Field    |  Bytes  | Value                                        |
|----------|---------|----------------------------------------------|
| Type     |  1      | `0x01` — sub-option 1 = ACS URL              |
| Length   |  1      | `0x1d` = 29 decimal                          |
| Value    |  29     | ASCII string `https://acs.example.com:7547/` |

---

## 3. Server-Side Configuration for Testing

### 3.1 Network Topology

```
  ┌──────────────────────────────────────┐      ┌──────────────────────────────────────┐
  │  CPE                                 │      │  Server                              │
  ├──────────────────────────────────────┤      ├──────────────────────────────────────┤
  │  Physical WAN                        │      │  Physical NIC                        │
  │    ae_wan                            │      │    eth1                              │
  │    MAC  00:AA:BB:01:23:41            │      │                                      │
  ├──────────────────────────────────────┤      ├──────────────────────────────────────┤
  │  PPPoE (logical)                     │      │  PPPoE (logical)                     │
  │    pppoe-wan                         │      │    ppp0                              │
  │    10.102.95.100 / 32                │      │    10.102.95.1                       │
  ├──────────────────────────────────────┤      ├──────────────────────────────────────┤
  │  Software                            │      │  Software                            │
  │    icwmpd       (TR-069 client)      │      │    pppoe-server  (rp-pppoe)          │
  │    pppoe-acs-discover                │      │    dnsmasq       (DHCP + Opt 43)     │
  └──────────────────┬───────────────────┘      └──────────────────┬───────────────────┘
                     │                                             │
                     ├──── Physical Ethernet  (ae_wan <-> eth1) ───┤
                     ╞══════════════ PPPoE Session  0x60 ══════════╡
                     │                                             │
```

### 3.2 dnsmasq Server Command

dnsmasq is the recommended DHCP server for this feature (see §6 for why ISC dhcpd
cannot be used).

```bash
dnsmasq \
  --no-daemon       \  # run in foreground for logging
  --port=0          \  # disable DNS (avoid port 53 conflict)
  --interface=ppp0  \  # listen on the PPPoE session interface
  --bind-interfaces \  # bind exclusively to ppp0, not 0.0.0.0
  --no-resolv       \  # ignore /etc/resolv.conf
  --no-hosts        \  # ignore /etc/hosts
  --log-dhcp        \  # log all DHCP transactions
  --dhcp-range=10.102.95.100,10.102.95.100,255.255.255.0,12h \
  --dhcp-option=vendor:dslforum.org,1,https://acs.example.com:7547/
  #                 ^-- Option 43 sub-option 1 returned only when
  #                     client Option 60 = "dslforum.org"
```

---

## 4. Design Rationale

Standard DHCP client utilities do not implement a **DHCPINFORM** mode. Their core
limitation is that they always begin with **DHCPDISCOVER** — they cannot send
**DHCPINFORM** as the first message. **DHCPINFORM** requires a client to already
hold an IP (`ciaddr`) and send it directly, bypassing the discovery cycle entirely.
This is architecturally incompatible with conventional DHCP client design.

```
  Standard DHCP client state machine (simplified)
  ─────────────────────────────────────────────────────────────────────

  START --► SELECTING --► REQUESTING --► BOUND --► RENEWING
                │                │
                │ DHCPDISCOVER   │ DHCPREQUEST
                │ (broadcast)    │
                ▼                ▼
           wait for         wait for
           DHCPOFFER        DHCPACK

  [N/A]  No DHCPINFORM branch exists.  ciaddr is only populated
         after a full lease cycle — which never happens over PPPoE.
```

```
┌─────────────────────────────────────────────────────────────────────┐
│  LIMITATION                                                         │
│                                                                     │
│  Standard DHCP clients fill ciaddr only after completing a full     │
│  DHCP lease cycle. In PPPoE the IP is assigned by IPCP, so a        │
│  conventional client never reaches that point.                      │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│  DECISION                                                           │
│                                                                     │
│  A custom C utility is the only viable path. Standalone executable. │
│  No dependencies beyond libc.                                       │
└─────────────────────────────────────────────────────────────────────┘
```

---

## 5. Implementation

### 5.1 DHCP Packet Construction

The utility hand-crafts a DHCP packet over a raw UDP socket bound to the
PPPoE interface. Every field is set explicitly to produce a valid DHCPINFORM.

| Field / Option | Value                | Rationale                                                          |
|----------------|----------------------|--------------------------------------------------------------------|
| `op`           | `1`                  | BOOTREQUEST                                                        |
| `htype / hlen` | `1 / 6`              | Ethernet hardware type, 6-byte MAC                                 |
| `xid`          | `getpid()`           | Unique transaction ID per invocation                               |
| `flags`        | `0x0000`             | Unicast — server replies directly to `ciaddr`                      |
| `ciaddr`       | `10.102.95.100`      | **IPCP-assigned IP** — key field distinguishing INFORM vs DISCOVER |
| `chaddr`       | `00:AA:BB:01:23:41`  | MAC of physical WAN interface                                      |
| Option 53      | `8`                  | DHCPINFORM message type                                            |
| Option 60      | `"dslforum.org"`     | Vendor class — triggers Option 43 response from server             |
| Option 55      | `[43]`               | Parameter request list — only ask for ACS URL                      |
| Option 255     | —                    | End of options                                                     |

### 5.2 Response Parsing

The utility handles two Option 43 encoding styles:

| Format          | Detection                              | Parsing                                              |
|-----------------|----------------------------------------|------------------------------------------------------|
| **TLV**         | First byte `0x01`, byte 1 is length    | Read sub-option type → length → value (ACS URL)      |
| **Plain text**  | All bytes are printable ASCII          | Treat entire option value as the URL string directly |

The TLV format is the standard TR-069 encoding. Plain-text fallback covers
non-standard server configurations.

### 5.3 Usage

```bash
# Syntax
pppoe-acs-discover <interface> <client_ip> <client_mac> <server_ip>

# Example
pppoe-acs-discover pppoe-wan 10.102.95.100 00:AA:BB:01:23:41 10.102.95.1
```

---

## 6. Protocol Reference

### 6.1 Packet Exchange

```
  CPE  (pppoe-wan · 10.102.95.100)              Server  (ppp0 · 10.102.95.1)
  ─────────────────────────────────────────────────────────────────────────────

        │                                                           │
        │  ┌──────────────────────────────────────────────┐         │
        │  │  DHCPINFORM                                  │         │
        │  │  src    10.102.95.100 : 68                   │         │
        │  │  dst    255.255.255.255 : 67                 │         │
        │  │  xid    0x7dcc                               │         │
        │  │  ciaddr 10.102.95.100                        │         │
        │  │  Opt 53 = 8  (DHCPINFORM)                    │         │
        │  │  Opt 60 = "dslforum.org"                     │         │
        │  │  Opt 55 = [43]   flags = 0x0000 (unicast)    │         │
        │  └──────────────────────────────────────────────┘         │
        │──────────────────────────────────────────────────────────►│
        │                                                           │
        │                             ┌──────────────────────────┐  │
        │                             │  vendor class matched    │  │
        │                             │  "dslforum.org"          │  │
        │                             │  → build Option 43 TLV   │  │
        │                             └──────────────────────────┘  │
        │                                                           │
        │◄──────────────────────────────────────────────────────────│
        │  ┌──────────────────────────────────────────────┐         │
        │  │  DHCPACK                                     │         │
        │  │  src       10.102.95.1 : 67                  │         │
        │  │  dst       10.102.95.100 : 68                │         │
        │  │  xid       0x7dcc  [OK - matched]            │         │
        │  │  Server-ID 10.102.95.1                       │         │
        │  │  Opt 53    = 5  (DHCPACK)                    │         │
        │  │  Opt 43    = TLV [01:1d:68:74:74:70:…]       │         │
        │  │  Opt 60    = "dslforum.org"                  │         │
        │  │  flags     = 0x0000 (unicast)                │         │
        │  └──────────────────────────────────────────────┘         │
        │                                                           │
```

### 6.2 Utility Output — CPE Side

```
root@cpe:~# pppoe-acs-discover pppoe-wan 10.102.95.100 00:aa:bb:01:23:41 10.102.95.1
──────────────────────────────────────────────────────
  Interface  :  pppoe-wan
  Client IP  :  10.102.95.100
  Server IP  :  10.102.95.1
  Vendor     :  dslforum.org
  Using MAC  :  00:aa:bb:01:23:41

Sending DHCPINFORM...  Sent 548 bytes
Waiting for DHCPACK (5 s timeout)...
Reply from: 10.102.95.1

──── DHCPACK received ────────────────────────────────
  DHCP Message Type  :  5 (ACK)
  Server ID          :  10.102.95.1
  Option 43 (Vendor Encapsulated):
    Raw bytes  :  01 1d 68 74 74 70 73 3a 2f 2f 61 63 73
                  2e 65 78 61 6d 70 6c 65 2e 63 6f 6d 3a
                  37 35 34 37 2f
    ACS URL    :  https://acs.example.com:7547/
──────────────────────────────────────────────────────
```

### 6.3 Server-Side Log — dnsmasq

```
root@server:~# dnsmasq --log-dhcp [...]
──────────────────────────────────────────────────────
dnsmasq-dhcp: DHCPINFORM(ppp0)  10.102.95.100   00:aa:bb:01:23:41
dnsmasq-dhcp: tags: ppp0
dnsmasq-dhcp: vendor class: dslforum.org
dnsmasq-dhcp: requested options: 43:vendor-encap

dnsmasq-dhcp: DHCPACK(ppp0)     10.102.95.100   00:aa:bb:01:23:41
dnsmasq-dhcp: sent size: 31  option: 43  vendor-encap
  01:1d:68:74:74:70:73:3a:2f:2f:61:63:73:2e:65:78:61:
  6d:70:6c:65:2e:63:6f:6d:3a:37:35:34:37:2f
dnsmasq-dhcp: sent size: 12  option: 60  vendor-class  dslforum.org
──────────────────────────────────────────────────────
```

---

## 7. Deployment

### 7.1 End-to-End Hotplug Integration

The hotplug script (`20-pppoe-acs-discovery`) fires automatically when the WAN PPPoE
interface comes up. No pre-configured ACS URL is needed — the URL is fetched live and
written to the cwmp UCI config before `icwmpd` starts its TR-069 session.

```
  ┌─────────────────────────────────────────────────────────────────────┐
  │  EVENT  Interface event fires                                       │
  │         ACTION=ifup  ·  INTERFACE=wan  ·  DEVICE=pppoe-wan          │
  └──────────────────────────────┬──────────────────────────────────────┘
                                 │
                                 ▼
  ┌─────────────────────────────────────────────────────────────────────┐
  │  STEP 1  Detect IPCP addresses                                      │
  │                                                                     │
  │  ifstatus <interface>                                               │
  │  CLIENT_IP = 10.102.95.100     (IPCP-assigned to CPE)               │
  │  PEER_IP   = 10.102.95.1       (IPCP remote end / DHCP server)      │
  └──────────────────────────────┬──────────────────────────────────────┘
                                 │
                                 ▼
  ┌─────────────────────────────────────────────────────────────────────┐
  │  STEP 2  ACS URL discovery                                          │
  │                                                                     │
  │  pppoe-acs-discover pppoe-wan $CLIENT_IP $CLIENT_MAC $PEER_IP       │
  │                                                                     │
  │    CPE ──── DHCPINFORM ─────────────────────────────────► Server    │
  │             ciaddr / Opt 60="dslforum.org" / Opt 55=[43]            │
  │    CPE ◄─── DHCPACK ───────────────────────────────────── Server    │
  │             Opt 43 = TLV { 01 : 1d : https://acs.../ }              │
  │                                                                     │
  │  ACS_URL = "https://acs.example.com:7547/"                          │
  └──────────────────────────────┬──────────────────────────────────────┘
                                 │
                                 ▼
  ┌─────────────────────────────────────────────────────────────────────┐
  │  STEP 3  Write to UCI                                               │
  │                                                                     │
  │  uci set cwmp.acs.dhcp_url="$ACS_URL"                               │
  │  ubus call uci commit '{"config":"cwmp"}'                           │
  └──────────────────────────────┬──────────────────────────────────────┘
                                 │
                                 ▼
  ╔═════════════════════════════════════════════════════════════════════╗
  ║  TR-069 session initiated  ·  Zero-Touch Provisioning complete      ║
  ╚═════════════════════════════════════════════════════════════════════╝
```

### 7.2 Build-Time Configuration

The feature is controlled by the `ICWMP_ENABLE_ACS_DISCOVERY_OVER_PPPOE` compile-time
flag. It is **enabled by default**.

| Method            | How to set                                                 |
|-------------------|------------------------------------------------------------|
| CMake             | `-DICWMP_ENABLE_ACS_DISCOVERY_OVER_PPPOE=ON` (default)     |
| CMake (disable)   | `-DICWMP_ENABLE_ACS_DISCOVERY_OVER_PPPOE=OFF`              |
| Package config    | `CONFIG_ICWMP_ENABLE_ACS_DISCOVERY_OVER_PPPOE=y` (default) |

When the flag is enabled, the build produces the `pppoe-acs-discover` utility tool
and installs both the utility tool (`/usr/sbin/pppoe-acs-discover`) and the hotplug
script (`/etc/hotplug.d/iface/20-pppoe-acs-discovery`). When disabled, neither component
is built or installed and the feature is fully absent from the image.

### 7.3 Flow Summary

| Step | Component              | Action                                                         |
|------|------------------------|----------------------------------------------------------------|
| 1    | Interface hotplug      | Fires on `ifup` of `wan`; passes `DEVICE=pppoe-wan`            |
| 2    | IP detection           | Reads IPCP client IP, peer IP, and MAC from interface status   |
| 3    | `pppoe-acs-discover`   | Sends DHCPINFORM; receives DHCPACK; parses Option 43 TLV       |
| 4    | UCI commit             | Writes ACS URL to `cwmp` config atomically                     |
| 5    | `icwmpd`               | Connects to ACS to begin TR-069 session                        |

---

## 8. Summary

This feature enables TR-069 ACS URL discovery for CPE devices operating over a PPPoE
WAN interface, implementing the standard DHCPINFORM mechanism defined in RFC 2131 in a
PPPoE context. The ACS URL is delivered via DHCP Option 43 in compliance with the TR-069
vendor class identification scheme (`dslforum.org`).

The `pppoe-acs-discover` utility is a lightweight, dependency-free C program that fills
a genuine gap left by standard DHCP client tools, which cannot send DHCPINFORM when the
IP address has been assigned externally via IPCP. The utility integrates with the interface
hotplug system, enabling fully automatic ACS URL provisioning from the moment a PPPoE
session is established — with no pre-configured ACS URL required in the device.

For testing purpose server-side architecture requires **dnsmasq** as the DHCP server, owing
to its native support for PPP interfaces — a capability absent in the ISC DHCP stack. The
vendor-class-conditional Option 43 response ensures that only TR-069-identified CPE
devices receive the ACS URL, leaving all other DHCP clients unaffected.

```
┌─────────────────────────────────────────────────────────────────────┐
│  COMPONENTS                                                         │
│                                                                     │
│  pppoe-acs-discover                                                 │
│    Custom C utility.  Sends DHCPINFORM, receives DHCPACK,           │
│    parses Option 43 TLV, prints ACS URL to stdout.                  │
│    No external dependencies.                                        │
│                                                                     │
│  20-pppoe-acs-discovery                                             │
│    Interface hotplug script.  Triggers on WAN ifup, invokes         │
│    pppoe-acs-discover, writes ACS URL to UCI, commits config.       │
│    No manual intervention required.                                 │
└─────────────────────────────────────────────────────────────────────┘
```

---

_ACS URL Discovery over PPPoE · Broadband Forum TR-069 / CWMP · v1.0 · May 2026_
