blob: 7559ac77a0eaeb8dd64e5548acbbe390f128fb2e [file] [view]
# Commissioning Proxy Cluster
The Commissioning Proxy cluster (cluster ID 0x0455) provides a proxy service
that allows a Commissioner to use commissioning transports not supported
locally, or to extend its commissioning range. Commissioners can use the proxy
to discover and establish a connection to commissionable devices that are
reachable by the proxy. The proxy connection acts as a tunnel through which the
Commissioner can run a PASE session with the commissionable device and
ultimately commission it.
## Overview
This directory contains an implementation of the Matter Commissioning Proxy
cluster server (`CommissioningProxyCluster.{h,cpp}`) using the code-driven
`DefaultServerCluster` pattern.
The cluster owns **all transport-agnostic behavior**: command validation,
feature-based attribute/command gating, transport/WiFiBand constraint checks,
the writable-attribute storage and change-reporting, and — via three subsystems
it composes — proxy-session bookkeeping, `ProxyMessage` routing, the
background-scan result cache, and multi-transport `ProxyScanRequest`
aggregation:
| Subsystem | Responsibility |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `CommissioningProxySessionManager` | Session-id allocation, id→{transport,fabric} table, per-fabric isolation, `ProxyMessage` routing + response-timeout timer |
| `CommissioningProxyScanCache` | `CachedResults` / `NumCachedResults` (one entry per device, per-entry TTL, `MaxCachedResults` cap) |
| `CommissioningProxyScanAggregator` | Combines a multi-transport `ProxyScanRequest` into one `ProxyScanResponse` |
A fourth transport-agnostic component, `CommissioningProxyBgScanRegistry`, ships
with the cluster but is **instantiated per transport and owned by the driver**
rather than composed by the cluster:
`ProxyBackGroundScanStartRequest`/`ProxyBackGroundScanStopRequest` fan out to
every matching driver, so the per-fabric records they arbitrate are necessarily
transport-local. It holds the per-fabric scan requests and their lifetime
timers, the spec transport/band overlap arithmetic on Stop, and the
paused/deferred state used while the radio is held by a connect or foreground
scan — leaving the driver only the hardware start/stop. See
[Background scanning](#background-scanning).
The application supplies **only the platform-specific transport work**. A BLE
driver, `CommissioningProxyBleTransport`, ships with the cluster and needs only
a `CommissioningProxyBleAdapter` from the application; for any other physical
transport the application implements the `CommissioningProxyTransport` driver
interface itself and registers it with the cluster. There is no do-everything
application delegate: static device capabilities are passed to the constructor
via `Config`, writable-attribute state is owned by the cluster, and the driver
is scoped to transport actions only. See
[Architecture](#architecture-why-a-driver-not-a-delegate) below.
### How it works
A Commissioner that lacks (or is out of range of) the transport a commissionable
device advertises on uses the proxy as a tunnel. The flow is:
1. **Discover** — the Commissioner sends `ProxyScanRequest` (or subscribes to
the background-scan cache); the proxy scans its local transports and returns
the commissionable devices it can see as `ScanResultStruct` entries.
2. **Connect** — the Commissioner sends `ProxyConnectRequest` naming a single
transport plus the target device (address/discriminator/VID/PID). The proxy
opens a transport connection to the device and replies with a unique
`SessionID` in the `ProxyConnectResponse`.
3. **Tunnel** — the Commissioner drives the PASE handshake (and the rest of
commissioning) by sending each Matter packet in a `ProxyMessageRequest` keyed
by `SessionID`; the proxy forwards it over the transport and returns the
device's reply in the `ProxyMessageResponse`. The proxy is a dumb relay — the
PASE session is end-to-end between the Commissioner and the device.
4. **Disconnect** — the Commissioner sends `ProxyDisconnectRequest` with the
`SessionID` to close an established session, or with a null `SessionID` to
cancel an in-flight connect; the proxy tears the transport connection down.
The cluster server itself is **transport-agnostic**: it validates the requested
transport against the set it advertises, then dispatches the work to the
registered `CommissioningProxyTransport` driver whose `GetTransportType()`
matches the request's transport bit. Today drivers exist for **BLE** (BTP) and
**Wi-Fi PAF** (PAFTP) — see the transport integration sections below.
## Features
The cluster supports the following optional features:
- **WiFiNetworkInterface (WI)**: Gates the Wi-Fi-band surface — the `WiFiBand`
attribute and the `WiFiBand(s)` command fields. It does **not** gate the
Wi-Fi PAF transport itself (see Transports below).
- **BackgroundScan (BGS)**: Allows background scanning of commissioning
transports, with results cached in the `CachedResults` / `NumCachedResults`
attributes.
## Transports
The transports the proxy advertises in the `Transport` attribute (and accepts in
`ProxyConnectRequest` / `ProxyScanRequest`) are exactly the ones for which a
driver has been registered (`RegisterTransport`), independent of the `WI`
feature. An application typically registers a driver per compiled-in transport:
| Transport (`CapabilitiesBitmap`) | Driver registered when | Wire protocol |
| -------------------------------- | ----------------------------------- | -------------------- |
| `kBle` (BTP) | `CONFIG_NETWORK_LAYER_BLE` | BTP over BLE |
| `kWiFiPAF` (PAFTP) | `CHIP_DEVICE_CONFIG_ENABLE_WIFIPAF` | PAFTP over Wi-Fi NAN |
`ProxyConnectRequest` selects **exactly one** transport per session. A request
naming a transport with no registered driver is rejected with
`INVALID_TRANSPORT_TYPE`. (The spec's `NTL` bit is not currently supported.)
## Architecture: why a driver, not a delegate
Per the SDK cluster guidance, a cluster that triggers platform-specific actions
uses a **Delegate (or Driver) interface**, while a cluster's own attribute state
is pushed through setters rather than fetched from a delegate. This cluster
follows that split precisely, so instead of one do-everything delegate there are
four homes for what such a delegate would otherwise hold:
| Concern | Lives in | Why |
| ----------------------------------------------------------------- | --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Transport actions (Connect / Scan / SendMessage / Disconnect / …) | `CommissioningProxyTransport` driver | The only genuinely platform-specific surface (BlueZ GATT, wpa_supplicant NAN). |
| Writable attributes (`ScanMaxTime`, `CacheTimeout`) + cache view | The cluster (members + setters) and its subsystems | The cluster owns change-reporting, so a driver can never forget `NotifyAttributeChanged`. |
| Static capabilities (`MaxSessions`, `MaxCachedResults`, bands) | `Config` (constructor argument) | Fixed device facts, not actions. |
| Background-scan fabric records, band arithmetic, pause/resume | `CommissioningProxyBgScanRegistry` (one per driver) | Transport-agnostic logic, but held per transport because background Start/Stop fan out to every matching driver. |
**How multiple platforms plug in.** A platform provides one
`CommissioningProxyTransport` implementation per physical transport and
registers it with `RegisterTransport()`. The cluster dispatches each command to
the driver whose `GetTransportType()` matches the request's transport bit, and
drivers report async results back through the cluster's shared subsystems
(`Sessions()`, `ScanCache()`, `ScanAggregator()`). Background scanning is
inherited the same way: the driver holds a `CommissioningProxyBgScanRegistry`
and implements its three-method `HardwareControl` hook. A new platform therefore
writes only its GATT/NAN driver and inherits all session/scan/message
bookkeeping unchanged.
`RegisterTransport()` is used (rather than a single constructor-injected
delegate) because there can be several drivers — one per transport — and they
are typically constructed after the cluster, whose endpoint is only known at
registration time.
## Usage
For new applications using the `CodeDrivenDataModelProvider`, instantiate and
register the cluster directly. This provides the most flexibility and control.
### 1. Implement a transport driver
BLE needs no driver of your own — register the cluster's own
`CommissioningProxyBleTransport` (see
[BLE Transport Integration](#ble-transport-integration)). For a transport the
SDK does not ship, create a class that inherits from
`chip::app::Clusters::CommissioningProxy::CommissioningProxyTransport` and
implement its virtual methods — one class per physical transport; Wi-Fi PAF is
the example below. A driver only handles the transport-specific work; the
cluster performs all spec validation first and owns the session/scan/message
bookkeeping. Methods run on the Matter task; the driver reports async results
back through its host cluster (given in `SetHost`) via `Sessions()`,
`ScanCache()`, and `ScanAggregator()`.
```cpp
#include <app/clusters/commissioning-proxy-server/CommissioningProxyTransport.h>
class MyPafTransport : public chip::app::Clusters::CommissioningProxy::CommissioningProxyTransport
{
public:
CapabilitiesBitmap GetTransportType() const override { return CapabilitiesBitmap::kWiFiPAF; }
void SetHost(CommissioningProxyCluster * cluster) override { mHost = cluster; }
// Open a transport connection. On success, allocate a session via
// mHost->Sessions().AllocSessionId()/RegisterSession() and call
// commandObj->AddResponse() with a ProxyConnectResponse carrying the sessionID.
Protocols::InteractionModel::Status Connect(chip::app::CommandHandler * commandObj,
const DataModel::InvokeRequest & request, uint16_t discriminator,
System::Clock::Seconds16 timeout) override;
// Forward a Matter packet; deliver the reply via
// mHost->Sessions().DispatchMessageResponse(), having first checked it
// with ValidateCommissioneeMessage() (see below).
CHIP_ERROR SendMessage(uint16_t sessionId, chip::System::PacketBufferHandle && buf) override;
// Report foreground-scan results to mHost->ScanAggregator().Contribute();
// background-scan results to mHost->ScanCache().Report().
Protocols::InteractionModel::Status Scan(System::Clock::Seconds16 scanMaxTime) override;
// Background scan: forward to the driver's CommissioningProxyBgScanRegistry,
// which owns the per-fabric records, lifetime timers and band arithmetic
// (see Background scanning below).
Protocols::InteractionModel::Status BgScanStart(System::Clock::Seconds16 timeout, BitMask<WiFiBandBitmap> wiFiBands,
FabricIndex fabricIndex, NodeId nodeId) override;
Protocols::InteractionModel::Status BgScanStop(BitMask<CapabilitiesBitmap> transport, BitMask<WiFiBandBitmap> wiFiBands,
FabricIndex fabricIndex, NodeId nodeId) override;
// ... CancelPendingConnect / Disconnect / OnAllSessionsClosed /
// IsConnectPending / Shutdown
private:
CommissioningProxyCluster * mHost = nullptr;
};
```
### 2. Instantiate the cluster and register the driver(s)
Construct the `CommissioningProxyCluster` for the endpoint (the endpoint id is a
constructor argument; `Config` carries the fixed feature set and device
capabilities), then register a driver per available transport.
```cpp
#include <app/clusters/commissioning-proxy-server/CommissioningProxyCluster.h>
#include <app/server-cluster/ServerClusterInterfaceRegistry.h>
using namespace chip::app::Clusters::CommissioningProxy;
MyPafTransport gPafTransport;
chip::BitMask<Feature> gFeatures(Feature::kBackgroundScan, Feature::kWiFiNetworkInterface);
// Supplies the response-timeout and scan-watchdog timers.
chip::app::DefaultTimerDelegate gTimerDelegate;
// Config: featureFlags plus the supported Wi-Fi bands. MaxSessions and
// MaxCachedResults are Fixed-quality attributes and come from
// CHIP_CONFIG_COMMISSIONING_PROXY_MAX_SESSIONS / _MAX_CACHED_RESULTS.
chip::app::RegisteredServerCluster<CommissioningProxyCluster> gCPCluster(
CommissioningProxyEndpoint, CommissioningProxyCluster::Config(gFeatures), gTimerDelegate);
void SetUpProxy()
{
gCPCluster.Cluster().RegisterTransport(gPafTransport); // before registration/Startup
}
```
### 3. Register the Cluster
In your application's initialization sequence, register the cluster instance
with the `CodegenDataModelProvider`:
```cpp
#include <data-model-providers/codegen/CodegenDataModelProvider.h>
void ApplicationInit()
{
VerifyOrDie(chip::app::CodegenDataModelProvider::Instance().Registry().Register(
gCPCluster.Registration()) == CHIP_NO_ERROR);
}
```
For a complete working example of the device wiring, see
`examples/all-devices-app/all-devices-common/device/types/commissioning-proxy/`.
It registers the cluster's `CommissioningProxyBleTransport` and supplies the
platform adapters for both transports from
`examples/all-devices-app/posix/linux/`.
## Transport driver methods
A `CommissioningProxyTransport` driver implements the following. Each is invoked
only after the cluster has validated the request; the driver does the
transport-specific work and reports results back through the host cluster's
subsystems.
| Method | Description |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `GetTransportType()` | The single transport bit this driver services (`kBle` / `kWiFiPAF`) |
| `SetHost()` | Bind the host cluster (set to null at cluster teardown) |
| `Connect()` | Open a transport session; allocate + register it via `Sessions()` |
| `SendMessage()` | Forward a packet; reply routed back via `Sessions()` |
| `Scan()` | Foreground scan; results reported to `ScanAggregator()` |
| `BgScanStart()` | Start a background scan for the given `(fabricIndex, nodeId)`; normally forwarded to the driver's `CommissioningProxyBgScanRegistry::Start()`. Results reported to `ScanCache()` (BGS) |
| `BgScanStop()` | Stop a background scan for the given `(fabricIndex, nodeId)`; normally forwarded to `CommissioningProxyBgScanRegistry::Stop()` (BGS) |
| `CancelPendingConnect()` | Cancel an in-flight connect (null-SessionID disconnect) |
| `Disconnect()` | Tear down an active proxy session |
| `OnAllSessionsClosed()` | Notified when the last session across all transports closes |
| `IsConnectPending()` | Whether a connect is in flight (counted against `MaxSessions`) |
| `Shutdown()` | Cancel driver timers/state before cluster destruction |
Note there is **no** `ProxyScanRequest`/`ProxyMessageRequest`/etc. delegate
hook: those commands' spec logic, session tracking, message routing, and scan
aggregation live in the cluster and its subsystems; the driver only exposes the
transport primitives above.
**Every message from the commissionee must be checked before it is forwarded.**
Call `CommissioningProxySessionManager::ValidateCommissioneeMessage()` on it. If
that returns an error, the message must not be forwarded, and the driver must
terminate the transport connection and remove the session. The spec requires
both, so this is a conformance requirement, not an option.
`DispatchMessageResponse()` repeats the check and drops invalid data, but it
cannot tear the session down: that part is the driver's.
## Background scanning
`ProxyBackGroundScanStartRequest`/`ProxyBackGroundScanStopRequest` are
per-fabric and MAY name several transports, so the cluster validates the request
(BGS/WI feature gating, reserved transport and band bits, supported bands) and
then fans it out to every registered driver whose transport bit is set, passing
the requesting fabric index and node id taken from the subject descriptor. Start
reports the first non-success status a driver returned; Stop returns `NOT_FOUND`
only when no driver had a matching record.
Everything a driver then has to do that is _not_ radio work is provided by
`CommissioningProxyBgScanRegistry` — one instance per driver, constructed with a
reference to the driver's `HardwareControl` implementation:
| Registry call | Responsibility |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Start()` | Add or refresh the `(fabricIndex, nodeId)` record and its transport/band mask, arm its lifetime timer, start the hardware if it is the first record |
| `Stop()` | Remove the requested transports/bands from the record; stop the hardware once no records remain |
| `Pause()` | Suspend the hardware scan while the radio is needed for a connect or foreground scan; records stay registered; a second call does nothing |
| `ResumeIfNeeded()` | Restart a paused scan once the radio is free; no-op if not paused or if no records remain |
| `Shutdown()` | Cancel every lifetime timer and stop the hardware scan if the registry owns it |
`Start()` with `timeoutSecs == 0` means no lifetime timer, i.e. scan until an
explicit Stop. `Stop()` applies the spec's transport/band arithmetic: a zero
transport mask means "stop the listed bands only"; a record left with no
transports or no bands is removed; `SUCCESS` is returned even when nothing
overlapped, and `NOT_FOUND` only when the fabric has no record at all.
The driver supplies the only transport-specific parts via `HardwareControl`:
| Hook | Contract |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `StartHardwareScan()` | Start or resume the hardware scan, wiring the driver's own discovery callback (return codes below) |
| `StopHardwareScan()` | Stop the hardware scan; called only while the registry owns the radio, never while paused |
| `ClearCachedResults()` | Drop cached results for the bands that stopped (`host->ScanCache().ClearTransport(<transport>, bands)`); 0 = all |
`StartHardwareScan()` returns `CHIP_NO_ERROR` when the scan is running,
`CHIP_ERROR_BUSY` when the radio is currently held — the registry keeps the
records, stays paused and retries on the next `ResumeIfNeeded()` — and any other
error is a hard failure that rejects the triggering `Start()`.
A driver therefore wires background scanning up as a small `HardwareControl`
implementation plus one registry instance, and forwards the two driver methods
to it:
```cpp
#include <app/clusters/commissioning-proxy-server/CommissioningProxyBgScanRegistry.h>
#include <platform/DefaultTimerDelegate.h>
// The registry drives these hooks from its own lifetime timer and from resume, i.e.
// outside the driver's call stack, so the hardware object holds its own host pointer.
// MyPafTransport::SetHost() hands it over: mHost = cluster; sHardware.SetHost(cluster);
class MyPafBgScanHardware : public CommissioningProxyBgScanRegistry::HardwareControl
{
public:
void SetHost(CommissioningProxyCluster * host) { mHost = host; }
// Returns CHIP_ERROR_BUSY when the single scanner is held by a connect or a
// foreground scan; the registry then defers and retries on resume.
CHIP_ERROR StartHardwareScan() override { return StartMyPlatformScan(OnBgScanDiscovery); }
void StopHardwareScan() override { StopMyPlatformScan(); }
// bands names the bands that stopped; 0 means the transport stopped entirely.
void ClearCachedResults(BitMask<WiFiBandBitmap> bands) override
{
if (mHost != nullptr)
{
mHost->ScanCache().ClearTransport(CapabilitiesBitmap::kWiFiPAF, bands);
}
}
private:
CommissioningProxyCluster * mHost = nullptr;
};
// Declared before the registry so it outlives it: the registry's destructor may
// call back into these hooks.
MyPafBgScanHardware sHardware;
chip::app::DefaultTimerDelegate sBgScanTimerDelegate;
CommissioningProxyBgScanRegistry sBgScan(sHardware, sBgScanTimerDelegate);
Status MyPafTransport::BgScanStart(System::Clock::Seconds16 timeout, BitMask<WiFiBandBitmap> wiFiBands, FabricIndex fabricIndex,
NodeId nodeId)
{
return sBgScan.Start(fabricIndex, nodeId, GetTransportType(), wiFiBands, timeout);
}
Status MyPafTransport::BgScanStop(BitMask<CapabilitiesBitmap> transport, BitMask<WiFiBandBitmap> wiFiBands,
FabricIndex fabricIndex, NodeId nodeId)
{
return sBgScan.Stop(fabricIndex, nodeId, transport, wiFiBands);
}
```
`CHIP_ERROR_BUSY` is the load-bearing case: on BLE the scanner is owned by a
connect or foreground scan, and on Wi-Fi PAF a `ProxyConnect` owns the single
NAN subscribe slot. A driver reports the conflict from `StartHardwareScan()`,
calls `Pause()` when it takes the radio and `ResumeIfNeeded()` when it releases
it, and the registry keeps the Commissioner's background scan registered across
the gap. `ResumeIfNeeded()` must be wrapped in `PlatformMgr().ScheduleWork()` if
the "radio freed" path could otherwise re-enter the driver.
## Async Command Handling
`ProxyConnectRequest` and `ProxyMessageRequest` are asynchronous — they return
`std::nullopt` from `InvokeCommand` to prevent an immediate response, and call
`CommandHandler::AddResponse()` or `CommandHandler::AddStatus()` from a
transport callback once the operation completes.
To keep the exchange alive across the async operation, store a
`CommandHandler::Handle` in the driver's pending-operation state — not on the
stack, which unwinds when `InvokeCommand` returns — and extend the exchange
response timeout:
```cpp
// mPending outlives InvokeCommand; the handle must live there, not here
mPending.handle = CommandHandler::Handle(commandObj);
if (auto * ec = commandObj->GetExchangeContext())
{
ec->SetResponseTimeout(chip::System::Clock::Seconds16(responseTimeout + 10));
}
// … return std::nullopt from InvokeCommand …
// Later, in your transport callback:
auto * handler = mPending.handle.Get();
if (handler != nullptr)
{
handler->AddResponse(commandPath, response);
}
```
## BLE Transport Integration
`CommissioningProxyBleTransport` ships with the cluster as the separate
`ble-transport` target, so an application that proxies over another transport —
or a platform with no BLE stack — can depend on `commissioning-proxy-server`
without pulling in `src/ble`. It drives a BTP connection through
`chip::Ble::BleLayer`:
- `Connect()` — on the first BLE connect the proxy flips its own BLE role from
peripheral to central, then calls
`BleLayer::NewBleConnectionByDiscriminator()` to open an L2CAP/BTP
connection to the commissionee.
- `SendMessage()` — calls `BLEEndPoint::Send()` to push the tunneled
commissioning packet over BTP.
- `Disconnect()` — calls `BLEEndPoint::Close()` to drop the connection.
Incoming BTP messages are routed back to the cluster via a `ProxyBleDelegate`
(`chip::Ble::BleLayerDelegate`) that wraps the original `BleLayer` transport,
matches the connection against the active session slots, checks the message with
`ValidateCommissioneeMessage()` (closing the endpoint if it fails), and calls
`host->Sessions().DispatchMessageResponse()`.
### The platform adapter
Two BLE operations have no portable form, so they are not in the transport:
switching the local BLE role from peripheral to central, and driving a scan for
commissionable devices. The application supplies both by implementing
`CommissioningProxyBleAdapter` and passing it to the transport's constructor:
```cpp
class MyBleProxyAdapter : public CommissioningProxyBleAdapter
{
CHIP_ERROR EnableCentralRole() override;
CHIP_ERROR StartScan(DiscoveryCallback cb, void * context) override;
void StopScan() override;
};
MyBleProxyAdapter gBleAdapter;
CommissioningProxyBleTransport gBleTransport(gBleAdapter, gTimerDelegate);
```
`examples/all-devices-app/posix/linux/CommissioningProxyBleAdapter.cpp` is a
worked implementation over `BLEManagerImpl`, wired up in that app's
`posix/linux/DeviceFactoryPlatformOverride.cpp`.
## Wi-Fi PAF Transport Integration
`CommissioningProxyPafTransport` ships with the cluster as the separate
`paf-transport` target, on the same terms as `ble-transport`: an application
that proxies over BLE only — or a platform with no Wi-Fi PAF stack — can depend
on `commissioning-proxy-server` without pulling in `src/wifipaf`. It drives NAN
and PAFTP sessions through `chip::WiFiPAF::WiFiPAFLayer`:
- `Connect()` — calls `ConnectivityMgr().WiFiPAFSubscribe()` to open a PAF
session to the commissionee discriminator.
- `SendMessage()` — calls `WiFiPAFLayer::SendMessage()` to send the tunneled
commissioning packet over PAFTP (the PAFTP endpoint, not the raw NAN
transmit, which would bypass framing).
- `Disconnect()` — calls `WiFiPAFLayer::RmPafSession()`, closes the PAFTP
endpoint, and cancels the NAN subscribe that backed the session.
Incoming PAF messages are routed back to the cluster via a `ProxyPafDelegate`
(`chip::WiFiPAF::WiFiPAFLayerDelegate`) that wraps the original transport,
matches the peer against the active session slots, checks the message with
`ValidateCommissioneeMessage()` (returning the error if it fails, which makes
the PAFTP endpoint close itself), and calls
`host->Sessions().DispatchMessageResponse()`.
### The platform adapter
Session setup and teardown above are portable — they go through the generic
`ConnectivityManager` facade. Discovery is not, so it is not in the transport:
scanning for commissionable devices, and recovering the subscribe id the
platform assigned to a request, both need the platform implementation. The
application supplies them by implementing `CommissioningProxyPafAdapter`:
```cpp
class MyPafProxyAdapter : public CommissioningProxyPafAdapter
{
CHIP_ERROR StartForegroundScan(System::Clock::Seconds16 window, DiscoveryCallback onDevice,
ScanCompleteCallback onDone, void * context) override;
CHIP_ERROR StartBackgroundScan(DiscoveryCallback cb, void * context) override;
void StopBackgroundScan() override;
uint32_t PendingConnectSubscribeId() const override;
};
MyPafProxyAdapter gPafAdapter;
CommissioningProxyPafTransport gPafTransport(gPafAdapter, gTimerDelegate);
```
Unlike BLE, the platform owns the scan window: `StartForegroundScan()` takes the
duration and the adapter signals completion, so the transport arms no scan timer
of its own.
`examples/all-devices-app/posix/linux/CommissioningProxyPafAdapter.cpp` is a
worked implementation over `ConnectivityManagerImpl`, and is also where the
platform's peer descriptor is unpacked into the interface's scalars so no
platform type reaches the cluster.
## Driving the Proxy from a Commissioner
Everything above is the proxy side. The commissioner side is `chip-tool`, which
implements the tunnel as a pairing mode: `pairing proxy` establishes a CASE
session to the proxy, sends `ProxyConnectRequest`, then carries every PASE and
commissioning packet to the commissionee inside `ProxyMessageRequest` and
`ProxyMessageResponse`.
The proxy must already be commissioned onto the fabric, since the commissioner
needs a CASE session before it can tunnel anything:
```bash
chip-tool pairing onnetwork <proxy-node-id> 20202021
chip-tool commissioningproxy proxy-scan-request 0x0A <proxy-node-id> <endpoint> \
--WiFiBands 0x01 --allow-large-payload true --timeout 30
chip-tool pairing proxy <node-id> <ssid> <password> <pin> <discriminator> \
<proxy-node-id> <proxy-connect-timeout> ble|wifipaf \
--proxy-endpoint <endpoint> [--proxy-wifi-band 2g4|5g]
```
`ProxyScanRequest` carries the large-message quality, so it needs
`--allow-large-payload true`; the background-scan commands do not.
`<proxy-connect-timeout>` is in seconds, with `0` meaning no timeout.
Full argument documentation, worked examples, and the background-scan commands
are in
[the chip-tool README](../../../../examples/chip-tool/README.md#commission-a-device-through-a-commissioning-proxy)
and
[Working with the CHIP Tool](../../../../docs/development_controllers/chip-tool/chip_tool_guide.md#commissioning-through-a-commissioning-proxy).
## Cluster State
The cluster tracks proxy state internally:
- `kState_CPDisconnected` — no active proxy session
- `kState_CPConnected` — a transport session (BLE or Wi-Fi PAF) is open and
ready to forward messages
State transitions:
```text
ProxyConnectRequest ──► transport connect success ──► kState_CPConnected
kState_CPConnected ──► ProxyDisconnectRequest ──► kState_CPConnected
(sessions remain)
kState_CPConnected ──► ProxyDisconnectRequest ──► kState_CPDisconnected
for the last session
kState_CPConnected ──► peer closes the transport ──► kState_CPDisconnected
connection, last session
```
With `MaxSessions > 1` several sessions can be open at once, so a
`ProxyDisconnectRequest` only returns the cluster to `kState_CPDisconnected` —
and only notifies the transports via `OnAllSessionsClosed()` — once no session
is left.
A session can also end without a `ProxyDisconnectRequest`, when the commissionee
closes the transport connection. A transport reports that through
`SetDisconnectedIfLastSession()` rather than setting the state itself: the
cluster counts the sessions of every transport, plus any connect still in
flight, so the last session on one transport is not necessarily the last session
on the proxy.