Search
moon
sun

Security Research on Quadruped Robot #4

태그
RedSpider
Embedded
Property
로봇TF블로그표지_4.png
작성날짜
2026/10/26
⚙
Table of Contents

1. Introduction

In Part 3, we walked through the init sequence and master_service, the service orchestrator, to see in what order the Unitree Go2’s internal services start and how they are supervised. We then combined that service map with the listening ports on a real unit and Unitree’s published hardware diagram, and picked four components that take input from outside the robot: BLE, RTSP, WebRTC/DDS, and OTA. Of those, we covered the structure and DFD of the RTSP video streaming service first.
Part 4 analyzes the remaining three components the same way, in this order: BLE, the WebRTC/DDS bridge, and OTA. BLE is where the app and the robot first connect. The bridge carries the app’s real-time control messages onto the robot’s internal DDS bus, and OTA takes commands from the cloud, downloads packages, and installs them. The order follows the robot’s life from the moment a user first pairs it, through everyday use, to updates.
As with RTSP in Part 3, each component is covered as basic building blocks → main operational flow → data flow (DFD). Chapter 5 then puts all four components, RTSP included, into one integrated DFD that runs from the external interfaces all the way to the robot’s internal services.

2. BLE Component Analysis

2.1. Basic Building Blocks

The first link between the robot and the app is made over BLE. Before a user can control the robot over the network, the smartphone app has to hand the robot a connection mode and WiFi credentials over short-range radio. This initial setup step, where network settings are pushed to a device that has no network yet, is called onboarding (or provisioning). Go2 uses BLE for it.
Once onboarding is done and the robot joins WiFi, control, video, and status streams all move to WebRTC over WiFi, and BLE stays around only as a secondary setup channel. Communication between the app and the robot therefore splits into two phases: BLE onboarding, and real-time control over WiFi + WebRTC. We start with what the robot-side server does during BLE onboarding.

2.1.1. BLE / GATT Terminology

•
BLE (Bluetooth Low Energy)
◦
Low-power short-range wireless standard introduced in Bluetooth 4.0
•
Low-power short-range wireless standard introduced in Bluetooth 4.0
•
GATT (Generic Attribute Profile)
◦
The standard structure BLE devices use to exchange data
◦
A Service holds one or more Characteristics; a characteristic holds the actual value plus Descriptors, which carry metadata about that value
◦
Services, characteristics, and descriptors are identified by UUIDs (16-bit short form or 128-bit), and at runtime each one is addressed by a 16-bit Handle
◦
Characteristics have properties such as Read / Write / Notify / Indicate; Notify is used when the server pushes a value to the client on its own
•
The standard structure BLE devices use to exchange data
•
A Service holds one or more Characteristics; a characteristic holds the actual value plus Descriptors, which carry metadata about that value
•
Services, characteristics, and descriptors are identified by UUIDs (16-bit short form or 128-bit), and at runtime each one is addressed by a 16-bit Handle
•
Characteristics have properties such as Read / Write / Notify / Indicate; Notify is used when the server pushes a value to the client on its own
•
ATT (Attribute Protocol)
◦
The transport underneath GATT; the first byte of a PDU is an opcode that identifies the operation
◦
Writes come in two forms: Write Request (0x12), which expects a response, and Write Command (0x52), which does not
•
The transport underneath GATT; the first byte of a PDU is an opcode that identifies the operation
•
Writes come in two forms: Write Request (0x12), which expects a response, and Write Command (0x52), which does not
•
Advertising and Scanning
◦
Broadcast packets a device sends periodically to announce itself before any connection exists
◦
Common fields include Local Name (device name) and Manufacturer Specific Data (vendor-defined data, AD type 0xFF)
•
CCCD (Client Characteristic Configuration Descriptor, 0x2902)
◦
Descriptor the client writes to turn Notify/Indicate on or off for a given characteristic

2.1.2. The btgatt-server Binary and How It Starts

The daemon that handles BLE onboarding on the robot is /unitree/module/network_manager/upper_bluetooth/btgatt-server. It is a customized build of btgatt-server, the example program that ships with BlueZ 5.55, and the build path string go2_bluez-5.55-advance left in the binary shows it is a separate custom build.[1] The binary is an aarch64 PIE and is not stripped, so function and global variable names are all still there.
As the path suggests, btgatt-server belongs to the network_manager module. network_manager is updated over OTA, so the module version and implementation details vary with the firmware version.
btgatt-server is launched by master_service, which we analyzed in Part 3. After boot, master_service reads Start.Cmd from /unitree/etc/master_service/service/upper_bluetooth and starts it as a persistent background daemon through start-stop-daemon.
start-stop-daemon --start --background --make-pidfile --pidfile=/unitree/var/run/btgatt-server.pid --exec .../btgatt-server -i hci0 -r -v
Plain Text
복사
•
i hci0 selects the HCI adapter, r enables the custom 0xFFE0 service, and v turns on verbose logging. Right after entering main(), the process runs wifi_init.sh and upper_bluetooth_init.sh to bring the adapter up and enable LE advertising and connections. It then builds the GATT DB and waits for connections on the ATT channel.
[ 1.567640] usb 2-1.6: New USB device found, idVendor=0bda, idProduct=a85b, bcdDevice= 0.00 [ 1.567648] usb 2-1.6: Manufacturer: Realtek ... [ 2.530297] rtk_btusb: Realtek Bluetooth USB driver ver 3.1.6d45ddf.20220519-142432 [ 2.560169] rtk_btusb: read_ver_rsp->lmp_subver = 0x8852 [ 2.560175] rtk_btusb: config filename rtl8852bu_config [ 2.570385] rtk_btusb: fw name is rtl8852bu_fw ... [ 2.803314] RTW: rtl8852bu v1.19.3-45-g7583a804a.20230505 [ 3.438853] RTW: rtw_ndev_init(wlan0) if1 mac_addr=xx:xx:xx:xx:xx:xx [ 3.439099] RTW: rtw_ndev_init(wlan1) if2 mac_addr=xx:xx:xx:xx:xx:xx [ 3.439384] RTW: [HW-CAP] ic_name:rtl8852b
Plain Text
복사
upper_bluetooth_init.sh cycles the adapter with hciconfig hci0 down/up, sets the advertising interval, and then runs hciconfig hci0 pscan followed by btmgmt le on, connectable on, and advertising on. hci0 is the Bluetooth side of a Realtek RTL8852BU combo chip. In the kernel log you can see the rtk_btusb driver and the rtl8852bu_fw firmware load for a USB-attached Realtek device (0bda:a85b). The WiFi driver for the same chip (rtl8852bu) creates both the wlan0 (STA) and wlan1 (AP) interfaces.
So one USB device, 0bda:a85b, gets both a Bluetooth driver (rtk_btusb) and a WiFi driver (RTW, rtl8852bu). The Bluetooth side identifies the chip by lmp_subver = 0x8852 and loads rtl8852bu_fw; the WiFi side creates two interfaces, wlan0 and wlan1. In practice, AP mode uses wlan1 (192.168.12.1) and STA mode uses wlan0.
flowchart TB
    subgraph R1["① launch"]
        direction LR
        A["Power ON<br/>master_service"] -->|"Start.Cmd<br/>(service/upper_bluetooth)"| B["start-stop-daemon<br/>btgatt-server -i hci0 -r -v"]
        B --> C["wifi_init.sh /<br/>upper_bluetooth_init.sh"]
    end
    subgraph R2["② GATT server setup"]
        direction LR
        D["set_ble_adv_and_response()<br/>Local Name + manufacturer data"] --> E["Build GATT DB<br/>GAP / GATT / 0xFFE0"]
        E --> F["L2CAP ATT socket<br/>accept loop"]
        F --> G["0xFFE2 write<br/>write_cb → receive_manager"]
    end
    R1 --> R2
Mermaid
복사

2.1.3. GATT DB Layout

The entire GATT DB is built in main(). Across the whole binary, gatt_db_service_add_characteristic is called from exactly six places, all of them inside main. The three service registrations (gatt_db_add_service) and three descriptor registrations (gatt_db_service_add_descriptor) are in main as well. Because the symbols survived, even the names of the callbacks attached to each characteristic are right there.
This is the part of main in the btgatt_server binary that actually builds the GATT DB:
// main(): GATT DB setup (pseudo code) // 1. GAP service bt_uuid16_create(&uuid, 0x1800); svc = gatt_db_add_service(db, &uuid, primary, 6); bt_uuid16_create(&uuid, 0x2A00); // Device Name gatt_db_service_add_characteristic(svc, &uuid, ..., 0x82 /* READ | EXT_PROP */, gap_device_name_read_cb, gap_device_name_write_cb, ...); bt_uuid16_create(&uuid, 0x2900); // Characteristic Extended Properties gatt_db_service_add_descriptor(svc, &uuid, ..., gap_device_name_ext_prop_read_cb, NULL, ...); bt_uuid16_create(&uuid, 0x2A01); // Appearance gatt_db_service_add_characteristic(svc, &uuid, ..., 0x02 /* READ */, NULL, NULL, ...); gatt_db_service_set_active(svc, true); // 2. GATT service bt_uuid16_create(&uuid, 0x1801); svc = gatt_db_add_service(db, &uuid, primary, 4); bt_uuid16_create(&uuid, 0x2A05); // Service Changed gatt_db_service_add_characteristic(svc, &uuid, ..., 0x22 /* READ | INDICATE */, gatt_service_changed_cb, NULL, ...); bt_uuid16_create(&uuid, 0x2902); // CCCD gatt_db_service_add_descriptor(svc, &uuid, ..., gatt_svc_chngd_ccc_read_cb, gatt_svc_chngd_ccc_write_cb, ...); gatt_db_service_set_active(svc, true); // 3. Custom service bt_uuid16_create(&uuid, 0xFFE0); svc = gatt_db_add_service(db, &uuid, primary, 8); bt_uuid16_create(&uuid, 0xFFE1); // response channel gatt_db_service_add_characteristic(svc, &uuid, ..., 0x10 /* NOTIFY */, NULL, NULL, NULL); bt_uuid16_create(&uuid, 0x2902); // CCCD (0xFFE1 subscription) gatt_db_service_add_descriptor(svc, &uuid, ..., hr_msrmt_ccc_read_cb, hr_msrmt_ccc_write_cb, ...); bt_uuid16_create(&uuid, 0x2A38); // Body Sensor Location gatt_db_service_add_characteristic(svc, &uuid, ..., 0x02 /* READ */, NULL, NULL, ...); bt_uuid16_create(&uuid, 0xFFE2); // command channel gatt_db_service_add_characteristic(svc, &uuid, ..., 0x08 /* WRITE */, NULL, hr_control_point_write_cb, ...); ... gatt_db_service_set_active(svc, true);
C
복사
For every service, characteristic, and descriptor, the code builds a 16-bit UUID with bt_uuid16_create and then registers it with the matching add function.
The result is two standard 16-bit services and one custom service.
Service
Characteristic
Property / Handle
Callback
Purpose
0x1800 GAP
0x2A00 Device Name
R/W
gap_device_name_read_cb / gap_device_name_write_cb
Device name
0x2A01 Appearance
R
-
Appearance
0x1801 GATT
0x2A05 Service Changed
Indicate
gatt_service_changed_cb
Service change notification
0xFFE0 (Custom)
0xFFE1
Notify, value handle 0x000D
-
Robot → app response channel
0xFFE2
Write, value handle 0x0012
hr_control_point_write_cb
App → robot command channel
0x2A38 Body Sensor Location
R
-
Not used for onboarding
[Table 1] btgatt-server GATT DB layout
The onboarding protocol itself uses only two of these: commands come in on 0xFFE2 (Write) and responses go out on 0xFFE1 (Notify). 0x2A38 plays no part in onboarding.
The same UUIDs appear on the app side. The app’s BLE constants class BleConstant defines all three, and actual transmission appears to go through BleSendManager.write(UUID_SERVER, UUID_WRITE, …).
com.unitree.lib_ble.core.BleConstant UUID_SERVER = 0000FFE0-0000-1000-8000-00805F9B34FB UUID_WRITE = 0000FFE2-0000-1000-8000-00805F9B34FB UUID_NOTI = 0000FFE1-0000-1000-8000-00805F9B34FB
Plain Text
복사

2.2. Onboarding Flow

Onboarding runs from advertising to WiFi setup in this order:
1.
The robot announces itself through advertising.
2.
The app finds the advertisement and opens a GATT connection.
3.
A handshake command authenticates the session.
4.
The app sends the WiFi mode, SSID, and password in turn, changing the robot’s network configuration.

2.2.1. Advertising

The advertising data is assembled directly by set_ble_adv_and_response() using an HCI command (LE Set Advertising Data). The Local Name is the "Go2_" prefix followed by the code value from the file /unitree/robot/basic/bluetooth, and the Manufacturer Specific Data carries device identification info.
The advertised name is built by update_test_device_name. test_device_name[20] in the .data section starts out holding "Go2_", and the code value read from /unitree/robot/basic/bluetooth is appended to it.
.data:0000000000060010 EXPORT test_device_name .data:0000000000060010 ; char test_device_name[20] .data:0000000000060010 test_device_name DCB 0x47, 0x6F, 0x32, 0x5F, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 .data:0000000000060010 ; DATA XREF: main+814↑o .data:0000000000060010 ; main+81C↑o ...
C
복사
0x47 0x6F 0x32 0x5F is "Go2_" in ASCII, and the remaining 16 bytes are zero.
// update_test_device_name v0 = fopen(go2_ble_name_file, "r"); // /unitree/robot/basic/bluetooth if ( v0 ) { for ( i = v0; !feof(i); __isoc99_fscanf(i, "%s", ble_name_data) ) ; fclose(i); v2 = strlen(test_device_name); // "Go2_" → 4 v3 = __stpcpy_chk(&test_device_name[v2], ble_name_data, 20); __printf_chk(1, "strlen(test_device_name)=%d\n", v3 - (unsigned int)test_device_name); __printf_chk(1, " test_device_name =%s\n", test_device_name); }
C
복사
update_test_device_name appends the code value to the end of the string in this buffer.
// DogBluetoothActivity — scan result filter private final Set<String> validPrefixes = SetsKt.setOf((Object[]) new String[]{"Go2_", "Uni_", "As2_", "XG_"}); public void onLeScan(BleDevice bleDevice) { String deviceName = bleDevice.getName(); for (String prefix : validPrefixes) if (StringsKt.startsWith$default(deviceName, prefix, ...)) { z2 = true; break; } ... // on match, add to deviceList → shown in the list }
Java
복사
The app’s scan screen (DogBluetoothActivity) only lists devices whose advertised name starts with "Go2_", "Uni_", "As2_", or "XG_". There are several prefixes because the same app serves not just Go2 but also G1, B2, A2, and other models.
Decompiling the app shows that this filter is just a prefix set and a string comparison.
[Figure 1] BLE scan results in the app

2.2.2. Connection, Handshake, and WiFi Setup Sequence

When the user picks the robot from the list, the app opens a GATT connection and subscribes to notifications on 0xFFE1, the response channel. Right after connecting, the app’s BleConnectionManager sends a handshake command to authenticate the session. Once that succeeds, it sends the settings in the order 0x03 (WiFi mode) → 0x04 (SSID) → 0x05 (password) → 0x06 (country code + apply).
Every frame is wrapped in AES and written to 0xFFE2, and the robot returns the result of each step as a 0xFFE1 notification.
sequenceDiagram
    participant App as App (lib_ble)
    participant Robot as btgatt-server
    participant Net as WiFi setup scripts

    Note over Robot: advertising on at boot
    Robot-->>App: Advertising (Local Name Go2_code + manufacturer data)
    App->>Robot: GATT connect
    App->>Robot: CCCD(0x2902) subscribe → ready for FFE1 notify

    Note over App,Robot: Handshake (session auth)
    App->>Robot: 0xFFE2 write: AES( 52 | len | cmd | payload | cksum )
    Robot->>Robot: decrypt → verify → set session flag
    Robot-->>App: 0xFFE1 notify: AES( 51 | cmd | result )

    Note over App,Robot: WiFi provisioning
    App->>Robot: 0x03 WIFI_TYPE (1=AP / 2=STA)
    Robot-->>App: ack
    App->>Robot: 0x04 WIFI_ACCOUNT (SSID, 14B chunks)
    Robot-->>App: ssid receive ok
    App->>Robot: 0x05 WIFI_PWD (password, 14B chunks)
    Robot-->>App: pwd receive ok
    App->>Robot: 0x06 COUNTRY + remote switch (apply)
    Robot->>Net: AP → hostapd_restart.sh / STA → wpa_supplicant_restart.sh
    Net-->>Robot: WiFi restarted
    Robot-->>App: 0xFFE1 notify: result
Mermaid
복사
The app’s frame builder (BleUtilsKt.getSendData) changes the frame layout depending on the value length. Short values go out in one shot as "52" + len + cmd + data + checksum. Longer values such as the SSID or password are cut into 14-byte pieces and sent as chunks of the form "52" + len + cmd + index + total + data + checksum. The command codes are defined in the app’s DeviceOperate.
Code
Name
Meaning
0x01
SECRET
Handshake
0x02
GET_SN
Query serial number
0x03
WIFI_TYPE
Set WiFi mode (AP=01, STA=02, NET=03)
0x04
WIFI_ACCOUNT
Send SSID
0x05
WIFI_PWD
Send password
0x06
COUNTRY
Country code + apply settings
0x07
GET_AP_MAC
Query MAC address
0x08
DISCONNECTED
Disconnect
[Table 2] BLE command codes defined in the app
Once the settings take effect, AP mode pins the robot’s IP at 192.168.12.1. In STA mode, the app looks for the robot on UDP port 10131 of the multicast groups 231.1.1.1 and 239.255.1.1. When the robot answers with its SN and IP on UDP port 10134, the app moves on to the WebRTC connection.

2.3. Command Handling

So how is a command that arrives over BLE actually handled inside the robot? We break it into three stages and go through them in order.

2.3.1. The Command Branch Added to the Write Handler

// branch added by Unitree in write_cb (BlueZ src/shared/gatt-server.c) v11 = *(unsigned __int8 *)pdu; // pdu[0:2] = target ATT handle, v11 = low byte ... v20 = v11 == 18; // handle low byte == 0x12 (FFE2 value handle) if ( v20 ) receive_manager((bt_att_chan *)pdu, v9, (bt_gatt_server *)user_data, ...); gatt_db_attribute_write(attribute, ...); // stock BlueZ handling from here on
C
복사
For ATT writes, btgatt-server uses write_cb from src/shared/gatt-server.c, BlueZ’s shared library, as is.[1] Attribute lookup, permission checks, and value storage are the same as upstream BlueZ. Unitree added a single branch inside that function: if the low byte of the target handle is 0x12 (the value handle 0x0012 of 0xFFE2), it calls the command parser receive_manager before the default write handling.

2.3.2. Frame Decryption and the Command Set

[0] 0x52 'R' request magic (responses use 0x51 'Q') [1] packet_len [2] command [3..4] index/mode current/total chunk index, or wifi_mode [5..] payload [len-1] checksum = (uint8) -(sum of preceding bytes)
Plain Text
복사
receive_manager decrypts the incoming value and then checks the checksum, the magic, and the fields, in that order. The decrypted plaintext frame follows the layout above.
void __fastcall receive_manager(bt_att_chan *chan, unsigned __int16 opcode, bt_gatt_server *pdu, uint16_t length, void *user_data) { ... v5 = opcode - 2; // length to decrypt = ATT value length decrypt_data((char *)&chan->att + 2, v5, &decrypted_data); puts("check_incoming_pkt_accuracy()"); ... // checksum: two's complement of the byte sum must equal the last byte if ( (unsigned __int8)*(&decrypted_data + (unsigned __int16)v5 - 1) != v12 ) { __printf_chk(1, "check result error!"); return; } if ( decrypted_data != 82 ) // 0x52 'R' = request magic return; v13 = v49; // decrypted[1] = packet_len ... v14 = v50; // decrypted[2] = command // v51 = decrypted[3..4] = index/mode, v52 = &decrypted[5] = payload ... }
C
복사
This is the part of the receive_manager decompilation that deals with that layout. The second argument is named opcode, but it is actually used as the PDU length.
Parsing goes like this:
1.
Skip the 2-byte handle in the PDU and extract the value.
2.
Decrypt it with decrypt_data.
3.
Verify the checksum in the last byte (two’s complement of the sum of the preceding bytes).
4.
Check that the first byte is the request magic 0x52 'R'.
5.
Extract packet_len, command, index/mode, and payload.
6.
Branch to a handler based on command.
encrypt_data(send_pdu, n, v60); bt_gatt_server_send_notification(pdu, 0xDu, (const uint8_t *)v60, n, 0); // 0x0D = FFE1 value handle
C
복사
For the response, send_pdu puts the response magic 0x51 'Q' and the result together, encrypts it with encrypt_data, and sends it to 0xFFE1 via bt_gatt_server_send_notification(..., 0x0D, ...).
In terms of handles: commands come in on 0xFFE2 (value handle 0x0012) and responses go out on 0xFFE1 (value handle 0x000D).
flowchart TB
    subgraph R1["① receive / decrypt"]
        direction LR
        A["0xFFE2 write<br/>(ATT PDU)"] --> B["extract value<br/>(skip 2B handle)"]
        B --> C["decrypt_data"]
    end
    subgraph R2["② validate"]
        direction LR
        D{"checksum OK?"} -->|Yes| E{"decrypted[0] ==<br/>0x52 'R'?"}
        D -->|No| X["drop"]
        E -->|No| X
    end
    subgraph R3["③ dispatch"]
        direction LR
        F["packet_len / command<br/>index / payload"] --> G{"command"}
        G -->|"handshake"| H["verify →<br/>set session flag"]
        G -->|"0x02 ~ 0x07"| K["SN / MAC query,<br/>WiFi setup"]
        G -->|"0x08 / 0x0A"| I["disconnect /<br/>keep-alive"]
        K --> L["encrypt response →<br/>0xFFE1 notify (0x000D)"]
    end
    R1 --> R2
    R2 -->|Yes| R3
Mermaid
복사
The commands handled by the robot’s receive_manager are listed in Table 3.
command
Function
What it does
0x01
Handshake
Authenticates the session and sets the session flag (valid_incoming_user)
0x02
Query serial number
Reads the SN file and returns it in chunks
0x03
Reset WiFi settings
Sets wifi_mode (1=AP / 2=STA), clears receive buffers
0x04
Receive SSID
Accumulates chunks in index order
0x05
Receive password
Accumulates chunks in index order
0x06
Country code + apply
Spawns remoteConnectionSettingThreadFunction and wifiSettingThreadFunction threads
0x07
Query MAC
Returns the result of ioctl(SIOCGIFHWADDR, "wlan1")
0x08
Disconnect
activeDisconnectThreadFunction
0x0A
keep-alive
Drops the BLE connection if nothing arrives for 3 periods
[Table 3] receive_manager command set
// BleUtilsKt — request frame assembly arrayList.add("52"); // magic arrayList.add(BluetoothUtils.format(src.size() + 4)); // length arrayList.add(operate.getNum()); // command code arrayList.addAll(src); // data // → "52 <len> <cmd> <data...>" → encrypt → 0xFFE2 write // DeviceOperate — command code definitions SECRET("01"), GET_SN("02"), WIFI_TYPE("03"), WIFI_ACCOUNT("04"), WIFI_PWD("05"), COUNTRY("06"), GET_AP_MAC("07"), DISCONNECTED("08"), ...
Java
복사
The app code shows the same frame structure we saw on the robot after decryption. Request frames are assembled by BleUtilsKt, and the command codes are defined in the DeviceOperate enum.
The app’s WIFI_ACCOUNT and WIFI_PWD map directly onto the robot’s 0x04 and 0x05. The SSID and password the user types in are wrapped with these command codes in ConnectActivity and sent.

2.3.3. Application-Layer Encryption and Session Authentication

BLE onboarding frames travel encrypted with AES at the application layer. decrypt_data unwraps incoming frames and encrypt_data wraps responses. network_manager, the module btgatt-server belongs to, is updated over OTA, so the details of the encryption and handshake differ between module versions. The app likewise switches its handling based on the BLE version the robot reports.
The session authentication structure is the same across versions. When the handshake command passes verification, the session flag valid_incoming_user is set to 1, and from then on commands 0x02–0x07 (queries and WiFi setup) are processed. What changes between versions is the handshake stage in front of that; the WiFi setup command set itself is identical.

2.4. Data Flow Analysis

2.4.1. Actual UUID Data and Entry Points

To check that the GATT DB layout recovered by static analysis matches the real device, we enumerated the GATT services on our test unit with a GATT DB scanner script.
[Service] 0000ffe0-... (handle 0x000b) Go2 custom provisioning/command container [Char] 0000ffe1-... h=0x000c [N] response notify (value handle 0x000d) [Desc] 00002902-... h=0x000e CCC (subscribe on/off) [Char] 0000ffe2-... h=0x0011 [W] command write channel [Char] 00002a38-... h=0x000f [R] [Service] 00001801-... (handle 0x0007) standard GATT [Char] 00002a05-... h=0x0008 [R,I] Service Changed [Desc] 00002902-... h=0x000a CCC => 2 services / 4 characteristics
Plain Text
복사
The 0x1800 GAP service is registered in main() but did not show up in the scanner’s enumeration. Apart from that, the services, characteristics, and handles matched the static analysis. The advertising data does not carry a 128-bit service UUID either; Go2 is identified by its Go2_* name.
Entry point
UUID / Handle
Direction
Purpose
Advertising
Local Name, Manufacturer Specific Data (0xFF)
Robot → outside
Device discovery
Command write
0xFFE2 / value 0x0012
App → robot
Enters the command parser receive_manager
Response notify
0xFFE1 / value 0x000D
Robot → app
Encrypted responses
[Table 4] BLE component entry points
Table 4 lists the BLE component’s entry points derived from the analysis so far.

2.4.2. Handling of Onboarding Data

Data crossing BLE falls into two kinds, depending on direction.
•
Robot → app (queries): 0x02 opens /unitree/robot/basic/code (the SN file) read-only and returns it in chunks. 0x07 reads the hardware address of wlan1 with ioctl(SIOCGIFHWADDR) and returns it.
•
App → robot (settings): The WiFi mode, SSID, and password received through 0x03–0x06 are passed to the network setup scripts by wifiSettingThreadFunction, the thread spawned by 0x06. In AP mode it runs hostapd_restart.sh, and in STA mode wpa_supplicant_restart.sh, with the SSID and password as arguments. 0x06 also starts the remote connection settings thread remoteConnectionSettingThreadFunction, which changes the value of /unitree/etc/remote_connection/RemoteConnectionEnabled.
Each script updates the SSID and password entries in the hostapd or wpa_supplicant config file (/etc/hostapd/hostapd.conf, /etc/wpa_supplicant/wpa_supplicant-wlan0.conf) and then restarts that service.

2.4.3. DFD

Putting the analysis together gives the data flow below. The BLE component is a wireless entry point that is open before any WiFi connection exists, and the robot’s network configuration depends on the outcome of onboarding.
---
config:
  layout: elk
---
flowchart LR
    subgraph RADIO["Trust boundary: BLE radio range"]
        APP["Unitree app<br/>(lib_ble)"]
    end
    subgraph ROBOT["Trust boundary: Go2 robot host"]
        P1("1.0 advertising<br/>set_ble_adv_and_response")
        D1[("D1 robot/basic<br/>bluetooth, code")]
        P2("2.0 ATT server<br/>write_cb")
        P3("3.0 command parser<br/>receive_manager")
        P4("4.0 WiFi apply<br/>wifiSettingThreadFunction")
        P5("5.0 network scripts<br/>hostapd / wpa_supplicant")
        D2[("D2 hostapd /<br/>wpa_supplicant conf")]
    end
    subgraph WIFI["Trust boundary: WiFi network"]
        NET["wlan1 (AP)<br/>wlan0 (STA)"]
    end
    APP <-->|"scan / adv · scan rsp"| P1
    APP <-->|"0xFFE2 write /<br/>0xFFE1 notify"| P2
    D1 -->|"code / SN"| P1
    P2 <-->|"command / response"| P3
    D1 -->|"SN"| P3
    P3 -->|"mode / SSID / PW"| P4
    P4 -->|"SSID / PW"| P5
    P5 -->|"update"| D2
    D2 -->|"restart"| NET
Mermaid
복사
The BLE path goes through the robot and comes out on the WiFi network. Values sent by the app pass through receive_manager → the settings thread → the setup scripts and end up in the hostapd/wpa_supplicant configuration. What that produces is the very WiFi network on which RTSP (covered in Part 3) and, later, WebRTC are exposed.

3. WebRTC / DDS Bridge Component Analysis

3.1. Basic Building Blocks

After BLE onboarding gets the robot onto WiFi, the app sets up a WebRTC session with the robot and sends JSON control messages of the form {type, topic, data} over a DataChannel opened on top of it. A bridge process inside the robot relays these messages to DDS topics (rt/*) of the same name.
Inside the robot there is a bus tied together by DDS, a publish/subscribe middleware, and services such as motion control, audio, LiDAR/SLAM, and the cameras talk to each other over it. External WebRTC traffic can reach this internal bus only through the bridge. Chapter 3 looks at which processes make up the bridge and how an external message becomes a DDS topic.

3.1.1. WebRTC / DataChannel / DDS Terminology

•
WebRTC
◦
WebRTC: A set of standardized technologies for low-latency P2P audio, video, and data between browsers. Go2 uses it as the real-time control channel between the app and the robot
◦
Signaling: The negotiation channel two endpoints use to exchange session parameters. The WebRTC standard does not define it, so each application implements its own; on Go2 this is the HTTP signaling (TCP 9991) in xfkTon
◦
SDP offer/answer: A session description with codecs, transport, ICE candidates, DTLS fingerprints, and so on. The app sends an offer and the robot returns an answer
◦
ICE / STUN / TURN: The procedure for finding a working path between two endpoints behind NAT (ICE), plus servers for discovering the public address (STUN) and relaying traffic (TURN)
◦
DTLS: TLS for UDP; once a path is chosen, it derives session keys and encrypts media/data
◦
SCTP DataChannel: A channel on top of DTLS that carries arbitrary application data. Go2’s control JSON flows over it
•
DDS
◦
DDS (Data Distribution Service): A data-centric publish/subscribe middleware standardized by the OMG. Go2 uses Eclipse Cyclone DDS as its implementation[2]
◦
Topic: A unit of data flow identified by a name and a data type (e.g. rt/api/sport/request, type unitree_api::msg::dds_::Request_)
◦
DataWriter / DataReader: The entities that write to and read from a topic. They don’t need to know about each other; matching topics is enough to connect them
◦
Domain: A logical bus that isolates DDS traffic. Only participants in the same domain discover each other
◦
RTPS: The DDS wire protocol. Participants are discovered over UDP multicast by default
◦
QoS: Policies for reliability, durability, deadlines, history, and other aspects of delivery
A single remote-control interaction on Go2 goes SDP exchange over HTTP signaling → path selection with ICE → encrypted channel via DTLS → SCTP DataChannel opened → control JSON sent and received → published to a DDS topic.

3.1.2. Signaling Server and Bridge Processes

root@Unitree:~# ps aux | grep -iE 'rtsp|stream|video|webrtc' root 1257 ... /unitree/module/video_hub/go2_videohub_rtsp_server root 1609 ... /unitree/module/video_hub/videohub high root 1705 ... /unitree/module/webrtc_bridge/bin/unitreeWebRTCClientMaster root 1714 ... /bin/python3 /unitree/module/webrtc_bridge/src/webrtc_dds_bridge/multicast_responder.py root 1739 ... /unitree/module/webrtc_bridge/src/webrtc_dds_bridge/xfkTon
C
복사
The bridge is split across several daemons under the /unitree/module/webrtc_bridge module, and master_service starts each of them as a separate service.
Service
Binary
DDS config
Role
webrtc_multicast_responder
multicast_responder.py
Not used (plain sockets only)
Answers robot IP discovery in STA mode
webrtc_signal_server
xfkTon
cyclonedds.xml (SHM on)
Listens on TCP 80 / 9991 / 10001 (signaling on 9991), relays signaling ↔︎ DDS
webrtc_bridge
unitreeWebRTCClientMaster
cyclonedds_noshm.xml (SHM off)
WebRTC endpoint + DataChannel ↔︎ DDS bridge
Media sources
videohub / audiohub / go2_videohub_rtsp_server
cyclonedds.xml
Publish/subscribe video (H.264) and audio (Opus) over DDS
Libraries
libkvsWebrtcClient.so / libkvsWebrtcSignalingClient.so
-
Fork based on the AWS KVS WebRTC C SDK
[Table 5] Processes that make up the WebRTC/DDS bridge
The roles are split: xfkTon owns the network-facing signaling, while unitreeWebRTCClientMaster owns the WebRTC media/data endpoint and the DDS bridge logic. The two processes are not wired together through function calls; they pass SDP over DDS topics. The offer that xfkTon decrypts is delivered to the bridge on the rt/xfk_webrtcreq topic, and the answer the bridge produces comes back on rt/xfk_webrtcres.
Broken down, unitreeWebRTCClientMaster does four things:
1.
Session open/close: Takes an offer, sets up the session struct, PeerConnection, DataChannel, and media transceivers, and returns an answer.
2.
Downlink publishing: Parses JSON arriving on the DataChannel, looks the topic up in the publish whitelist, and calls dds_write.
3.
Uplink delivery: Subscribes (dds_take) to response and status topics from the robot’s services, turns them into JSON, and sends them back to the requesting session.
4.
Media relay: Moves the app’s audio onto DDS, and camera frames from DDS onto the WebRTC video track.

3.2. Signaling and Session Handling

3.2.1. Signaling Flow

External clients never connect to the bridge directly; they always go through xfkTon (TCP 9991). Signaling has two steps: a handshake and the offer submission.
sequenceDiagram
    autonumber
    participant APP as App / WebRTC client
    participant XFK as xfkTon (TCP 9991)
    participant SIG as DDS signaling topics<br/>rt/xfk_webrtcreq / res
    participant BR as unitreeWebRTCClientMaster
    participant BUS as Robot DDS domain (eth0)

    APP->>XFK: POST /con_notify
    XFK-->>APP: {data1 = RSA pubkey (AES-GCM), data2}
    APP->>APP: generate session key k, encrypt offer
    APP->>XFK: POST /con_ing_(path) {data1 = AES(offer, k), data2 = RSA(k)}
    XFK->>XFK: recover k, decrypt offer
    XFK->>SIG: DDS write String_ (offer)
    SIG->>BR: rt/xfk_webrtcreq
    BR->>BR: create session, parse id / token, PeerConnection
    BR->>SIG: answer → rt/xfk_webrtcres
    SIG->>XFK: answer
    XFK-->>APP: HTTP response (AES(answer, k))
    Note over APP,BR: ICE / DTLS handshake, SCTP DataChannel open
    APP->>BR: DataChannel {type, topic, data}
    BR->>BUS: whitelist check → dds_write
    BUS->>BR: response / telemetry (subscribed topics)
    BR->>APP: {type:'res', topic, data} to requesting session
Mermaid
복사
1.
Handshake (POST /con_notify)
•
When a client calls this endpoint, the robot returns {data1, data2}.
•
data1 holds the robot’s ephemeral RSA public key encrypted with AES-128-GCM, and data2 is a version flag indicating the encryption scheme.
•
On v1.1.15 and later, data2 is 3 and data1 is decrypted with a per-device AES key. That key is generated during BLE binding and reaches the owner’s account through the cloud. On the robot it is stored as /unitree/etc/key/aes_key.bin.
•
When a client calls this endpoint, the robot returns {data1, data2}.
•
data1 holds the robot’s ephemeral RSA public key encrypted with AES-128-GCM, and data2 is a version flag indicating the encryption scheme.
•
On v1.1.15 and later, data2 is 3 and data1 is decrypted with a per-device AES key. That key is generated during BLE binding and reaches the owner’s account through the cloud. On the robot it is stored as /unitree/etc/key/aes_key.bin.
1.
Offer submission (POST /con_ing_<path>)
•
The client generates a random session key k, encrypts the offer JSON with AES under k, encrypts k with the robot’s RSA public key, and sends both as a {data1, data2} body.
•
The <path> at the end of the URL is derived from a token appended to the end of data1 in the con_notify response.
•
The client generates a random session key k, encrypts the offer JSON with AES under k, encrypts k with the robot’s RSA public key, and sends both as a {data1, data2} body.
•
The <path> at the end of the URL is derived from a token appended to the end of data1 in the con_notify response.
xfkTon recovers k with its RSA private key, decrypts the offer, wraps the plaintext in a std_msgs::msg::dds_::String_ message, and publishes it to DDS rt/xfk_webrtcreq. The answer from the bridge travels back the other way (rt/xfk_webrtcres → xfkTon → HTTP response encrypted with k) to the client.
When the app connects remotely through the cloud (robot-api.unitree.com) and a TURN server, the structure doesn’t change: the same keys and encryption are used as on the LAN xfkTon path. The remote path also uses AWS KVS signaling, where unitreeWebRTCClientMaster authenticates to KVS with temporary IoT credentials and SigV4 signatures.

3.2.2. Session Creation and Offer Parsing

The bridge subscribes to both signaling sources: rt/xfk_webrtcreq, which comes through xfkTon, and rt/webrtcreq, a direct source.
// select_webrtc_request_source_topic_candidate (unitreeWebRTCClientMaster) void __fastcall select_webrtc_request_source_topic_candidate(__int64 a1, const char *a2) { if ( !strcmp(a2, "rt/webrtcreq") ) { *(_DWORD *)(a1 + 46944) = 1; } else if ( !strcmp(a2, "rt/xfk_webrtcreq") ) { *(_DWORD *)(a1 + 46944) = 0; } }
C
복사
select_webrtc_request_source_topic_candidate records which topic the offer came in on at offset +46944 of the session struct (1 = rt/webrtcreq, 0 = rt/xfk_webrtcreq). That value decides whether the answer goes back on rt/webrtcres or rt/xfk_webrtcres. According to the topic inventory, the publisher of rt/webrtcreq is robot_state_service.
When an offer arrives, webrtc_create_streaming_session allocates the session struct with calloc(1, 46960). Creating a PeerConnection, registering ICE and DataChannel callbacks, and then attaching video/audio transceivers appears to follow the session setup flow in the AWS KVS WebRTC C SDK sample (kvsWebRTCClientMaster).[3] The binary name unitreeWebRTCClientMaster looks borrowed from that sample too. What Unitree added on top:
•
Track and stream names (unitreeVideoTrack, unitreeAudioTrack, unitreeMediaStream)
•
Recording of the signaling source (+46944, via select_webrtc_request_source_topic_candidate above)
•
Storage fields for the offer’s id/token (getOfferId/getOfferToken below)
•
Two function pointer tables for action dispatch (sess+46864, sess+46896)
The plaintext offer is JSON of the form {sdp, type, token, id}. On a LAN connection id holds a value like STA_localNetwork, and the app requests a DataChannel labeled "data", a receive-only video transceiver, and a send/receive audio transceiver. The identifying fields of the offer JSON are pulled out by getOfferId (offer.id → sess+25873) and getOfferToken (offer.token → sess+26274) and stored in fixed buffers inside the session struct. getTurnServerInfo handles TURN server info the same way.
// getOfferId — src: webrtcUtils.c:1682 root = sub_1C821C(offerJson); // parse offer JSON node = sub_1C9CEC(root, "id"); if (node) strcpy((char*)(sess + 25873), node[4]); // offer.id → sess+25873 // getOfferToken — src: webrtcUtils.c:1724 node = sub_1C9CEC(root, "token"); if (node) strcpy((char*)(sess + 26274), node[4]); // offer.token → sess+26274
C
복사
sub_1C9CEC looks up a node by key in a JSON object, and node[4] (offset 0x20) is the string value. The source file name (webrtcUtils.c) and line numbers left in the binary made it possible to recover the function names.

3.3. DataChannel ↔︎ DDS Bridge Logic

3.3.1. Downlink Pipeline

Messages arriving on the DataChannel are parsed in dataChannelOnMessageCallback. It branches on the type field into subscribe/unsubscribe/heartbeat/messages (req, msg), and for actual control messages it looks the topic up in the publish whitelist hash table.
The whitelist is a fixed set of 31 topics that register_dds_publisher fills in at boot, each registered as a (topic name, type, writer callback) triple. On a hit, the topic’s own writer callback copies the JSON fields into a DDS struct and calls dds_write; on a miss the message is dropped with Invalid Topic. There is no publish path to any topic outside the whitelist.
flowchart LR
    IN["DataChannel inbound<br/>{type, topic, data}"] --> PARSE["dataChannelOnMessageCallback<br/>parse"]
    PARSE -->|"type"| BR1["subscribe / unsubscribe /<br/>heartbeat"]
    PARSE -->|"req / msg"| WL{"publish whitelist<br/>lookup (31)"}
    WL -->|"miss"| REJ["Invalid Topic /<br/>drop"]
    WL -->|"hit"| WR["per-topic writer<br/>write_request ..."]
    WR -->|"JSON → DDS struct<br/>dds_write"| BUS["Robot DDS domain<br/>rt/api/*/request ..."]
Mermaid
복사

3.3.2. Publish Whitelist (31 Topics → Writer Mapping)

For each topic, the registration function builds a publish endpoint in the order dds_create_topic → QoS setup → dds_create_writer → hashTablePut. The table below lists the 31 publish topics recovered from the registration block and the writer that handles each.
Topic
Writer callback
rt/api/{sport, motion_switcher, config, vui, gpt, robot_state, audiohub, gas_sensor, fourg_agent, obstacles_avoid, uwbswitch, pet, assistant_recorder, bashrunner, programming_actuator}/request (15)
write_request (shared)
rt/api/rm_con/request
write_rm_con_service_request
rt/api/videohub/request
write_api_videohub_request
rt/qt_command
write_qt_command
rt/qt_add_node
write_qt_node_cmd
rt/qt_add_edge
write_qt_edge_cmd
rt/wirelesscontroller
write_wireless_controller
rt/wirelesscontroller_unprocessed
write_wireless_controller_unprocessed
rt/arm_Command
write_armstring
rt/rtc/state, rt/rtc_status, rt/videohub/inner, rt/uwbswitch, rt/servicestateactivate, rt/utlidar/switch, rt/uslam/client_command, rt/programming_actuator/command (8)
write_string / write_rtc_status / write_videohub_inner
[Table 6] Publish whitelist and writer mapping
Notably, the 15 rt/api/*/request topics all share one writer, write_request. It reads header.identity.{id, api_id}, parameter, and header.policy.{priority, noreply} from the JSON, fills a unitree_api::Request_ struct, and publishes it. The bridge only decides whether a topic may be published; what an api_id means is up to the service subscribed to that topic.
void write_request(__int64 ctx, __int64 json) { printf("write_request() topicName =%s\n", *(const char**)(ctx+24)); header = sub_1C9CEC(json, "header"); identity = sub_1C9CEC(header, "identity"); sub_1C9CEC(identity, <id key>); // header.identity.id if ( sub_1C9CEC(identity, "api_id") ) { // check api_id exists param = sub_1C9CEC(json, "parameter"); if ( param && param[4] ) { // check parameter string exists policy = sub_1C9CEC(header, "policy"); priority = policy ? sub_1C9CEC(policy,"priority")[10] : 0; noreply = (policy && sub_1C9CEC(policy,"noreply")[10]==1) ? 1 : 0; imp_thunk_dds_write(); // publish to the request topic } } }
C
복사
A message is published only if both api_id and parameter are present; a missing policy falls back to defaults. The topic name is bound to the writer context (ctx+24) at registration time, which is how one function serves all 15 topics.
write_qt_command, write_qt_node_cmd, and write_qt_edge_cmd are writers for QT interaction nodes and share a sequence conversion function that turns a JSON array into a DDS sequence (length + buffer). The bashrunner, programming_actuator, and assistant_recorder topics get additional deadline, durability, and history QoS settings.

3.3.3. Uplink Pipeline and Media Relay

The uplink runs the other way: it subscribes to 44 topics carrying responses and status from the robot’s services and sends them back to the client. Readers are registered in the order hashTablePut → dds_create_topic → dds_create_listener → dds_lset_data_available → dds_create_reader. When the listener signals that a sample has arrived, the per-topic callback pulls it out with dds_take and serializes it to JSON. RPC responses take the form {type:"res", topic, data}, and subscribed status topics {type:"msg", topic, data}.
At send time the bridge walks the session array and delivers over the DataChannel only to the one session whose request-id / uuid matches. Responses are kept per session so that a reply to user A’s request never ends up in user B’s session. The main callbacks are:
Callback
Target
do_response_data_take
Shared for rt/api/*/response
do_res_take
Responses with a base64 body
do_lf_lowstate_data_take, do_sportmode_state_data_take
Robot state
do_lidar_voxel_map_data_take, do_uslam_point_cloud2_*
LiDAR / SLAM
do_string_data_take
String notifications
[Table 7] Main uplink callbacks
The flow of do_response_data_take, which handles rt/api/*/response, shows how per-session routing works.
void do_response_data_take(int reader, __int64 a2) { v17 = imp_thunk_dds_take(); sample = *(a2+8); // response sample obj = sub_1CA5EC(); // new JSON object sub_1C9E78(obj, "type", "res"); sub_1C9E78(obj, "topic", *(a2+16)); // topic name // build data.header.{identity{api_id,id}, status{code}}, data.data ... s = sub_1C8268(obj); // serialize JSON hasBody = *(_DWORD*)(sample+36) != 0; // binary body present? if (hasBody) sub_1D330C(s, body, v28); // assemble [n][plen][header][body] // walk sessions, match request-id → send only to the requesting session for (i = 0; i < session_count; i++) { sess = session_array[i]; for (j = 0; j <= 39; j++) if (sess.pendingReqId[j] == sample.reqId) { if (hasBody) send_on_datachannel_dispatch_thunk(sess, 1, v28[1]); // binary else send_on_datachannel_dispatch_thunk(sess, 0, s); // string break; } } }
C
복사
Each session holds up to 40 pending request-ids, and a response sample goes only to the session holding a matching request-id. Responses with a body are sent as a binary frame prefixed with the header JSON length (n) and body length (plen); responses without one go out as a plain JSON string.
In the media relay, audio and video flow in opposite directions.
•
Audio (app → robot): audioFrameHandler writes WebRTC audio frames to DDS rt/audioreceiver. The writer is created when the first frame arrives.
•
Video (robot → app): A bridge reader receives DDS rt/frontvideostream (Go2FrontVideoData_), published by videohub, and feeds it into each session’s video transceiver with writeFrame.

3.4. Data Flow Analysis

3.4.1. DDS Topic Reachability

Reachability here means whether a user connected over the WebRTC DataChannel can cause a publish or subscribe on a given rt/* topic. A participant that joins the DDS bus directly over the wire (eth0) can reach every topic, but WiFi and remote users have to go through the bridge whitelist, which cuts the reachable set down considerably.
We identified 119 rt/* topics in total. Of those, the bridge can publish to 31 and subscribes to 44; both numbers were recovered from the registration blocks in the bridge binary.
Category
Representative topics
Direction
Service API (rt/api/* request/response)
sport, motion_switcher, config, vui, gpt, robot_state, audiohub, pet, videohub, etc.
Requests published, responses subscribed
Low-level motion
rt/lowcmd, rt/lowstate, rt/lf/lowstate, rt/sportmodestate
State subscribed
LiDAR / mapping
rt/utlidar/* (cloud, grid_map, voxel_map, switch, etc.)
Mostly PointCloud2 subscriptions
SLAM / localization
rt/uslam/* (client_command, cloud_map, localization, etc.)
Commands published + state subscribed
Positioning / network
rt/gnss, rt/uwbstate, rt/uwbswitch, rt/public_network_status
Mixed
Audio / video
rt/audiosender, rt/audioreceiver, rt/frontvideostream, rt/videohub/inner
Media
AI / interaction
rt/gpt_cmd, rt/gpt_state, rt/gesture/result, rt/pet/flowfeedback
Mixed
WebRTC / remote
rt/webrtcreq, rt/webrtcres, rt/xfk_webrtcreq, rt/xfk_webrtcres
Signaling
Robot arm
rt/arm_Command, rt/arm_Feedback, rt/arm/action/state
Mixed
System status
rt/servicestate, rt/selftest, rt/multiplestate
Status
Controller input
rt/wirelesscontroller, rt/wirelesscontroller_unprocessed
Published
QT interaction
rt/qt_command, rt/qt_add_node, rt/qt_add_edge, rt/qt_notice
Published / subscribed
[Table 8] DDS topic inventory by category (119 topics)

3.4.2. DDS Domain, QoS, and Security (Authentication) Configuration

Dumping ddsls output and participant info shows that Go2’s DDS participants run on the eth0 interface with domain Id="any". Each participant advertises a udp/239.255.0.1:7400 multicast address and a udp/192.168.123.161:<port> unicast address. In other words, the robot’s internal bus is CycloneDDS multicast (UDP 7400) on the internal wired network (192.168.123.0/24).
ddsls lists 24 participants, including unitreeWebRTCClientMaster, xfkTon, vui_service, robot_state_service, motion_switcher_service, videohub, audiohub, unitree_lidar_server, obstacles_avoid, basic_service, and sbus_handle. Eight of them are python3 processes.
Shared memory (SHM) settings differ by process: most robot services use cyclonedds.xml with SHM on, while the bridge uses cyclonedds_noshm.xml with SHM off.
<Security><Authentication> <Library initFunction="init_authentication" finalizeFunction="finalize_authentication" path="/unitree/lib/libdds_security_auth.so" /> <IdentityCA>file:/unitree/etc/ds/<CA>.crt</IdentityCA> <IdentityCertificate>file:/unitree/etc/ds/<cert>.crt</IdentityCertificate> <PrivateKey>file:/unitree/etc/ds/<key>.crt</PrivateKey> </Authentication></Security>
XML
복사
The DDS security settings show that cyclonedds.xml enables the DDS Security Authentication plugin.[2]
Joining the DDS bus over the wire requires authenticating with a certificate signed by the IdentityCA and its private key. WiFi and remote users, on the other hand, reach internal topics through the bridge without that certificate, and how far they can reach is set by the bridge’s publish whitelist.

3.4.3. DFD

The DFD below shows the bridge component’s data flow. There are three zones with different trust levels, and an external message crosses the boundaries in the order signaling → bridge → DDS domain.
---
config:
  layout: dagre
  flowchart:
    nodeSpacing: 18
    rankSpacing: 28
---
flowchart LR
    subgraph EXT["Trust boundary: WiFi LAN / Internet"]
        APP["Unitree app<br/>(local / remote)"]
        CLOUD["Unitree cloud<br/>robot-api / TURN"]
    end
    subgraph EDGE["Trust boundary: Go2 robot host (WebRTC bridge)"]
        P4("4.0 discovery<br/>multicast_responder.py")
        D1[("D1 device key<br/>aes_key.bin")]
        P1("1.0 signaling<br/>xfkTon :80 / 9991 / 10001")
        P2("2.0 WebRTC session<br/>unitreeWebRTCClientMaster")
        P3("3.0 DDS I/O<br/>publish 31 / subscribe 44")
    end
    subgraph DDSD["Trust boundary: robot DDS domain (eth0, cert auth)"]
        SVC["rt/api/* services<br/>sport / config / vui / pet ..."]
        MEDIA["videohub / audiohub"]
    end
    APP <-->|"remote signaling / TURN"| CLOUD
    APP <-->|"UDP 10131 / SN + IP"| P4
    APP -->|"con_notify / con_ing"| P1
    D1 -->|"key"| P1
    P1 <-->|"offer / answer"| P2
    APP <-->|"DataChannel"| P2
    P2 <-->|"whitelist check"| P3
    P3 <-->|"dds_write / dds_take"| SVC
    P3 <-->|"audio / video"| MEDIA
Mermaid
복사
Entry point
Protocol
Process
Purpose
9991
TCP (HTTP)
xfkTon
Signaling (con_notify, con_ing)
80 / 10001
TCP
xfkTon
Listening ports (purpose unconfirmed)
DataChannel
SCTP / DTLS
unitreeWebRTCClientMaster
Control JSON in and out
10131 → 10134
UDP multicast
multicast_responder.py
Receives robot SN/IP discovery queries / replies
7400
UDP multicast (eth0)
DDS participant processes
DDS discovery (internal wired network)
[Table 9] WebRTC/DDS entry points
1.
The bridge is the only way an external message reaches the internal DDS bus. Signaling is protected by an encrypted channel using a per-device key, and the publish scope is fixed at the 31 whitelisted topics.
2.
Inter-process communication uses DDS as well. xfkTon and the bridge exchange offers and answers over the signaling topics (rt/xfk_webrtcreq/res), which makes those topics the point where external signaling meets the internal bus.

4. OTA / UPK Component Analysis

4.1. Basic Building Blocks

BLE, RTSP, and WebRTC are how users interact with the robot. OTA is different in kind from those three: it is how the cloud replaces the robot’s software itself. In Part 3 we got as far as the init script ota_box starting at the same multi-user stage as master_service. Chapter 4 follows this OTA daemon from receiving a command to installing a package.
[Figure 2] Unitree’s official OTA guide

4.1.1. OTA / UPK / MQTT / CDN Terminology

•
OTA (Over-The-Air)
◦
A way for a device to receive firmware/software over the network and update itself, with no cable or manual steps
•
A way for a device to receive firmware/software over the network and update itself, with no cable or manual steps
•
UPK
◦
Unitree’s custom update package container (.upk)
◦
Consists of a 112-byte header (magic UTPK) + TEA-encrypted payload + an inner TAR structure (see Part 2 of the series for the format)
•
Unitree’s custom update package container (.upk)
•
Consists of a 112-byte header (magic UTPK) + TEA-encrypted payload + an inner TAR structure (see Part 2 of the series for the format)
•
MQTT
◦
A lightweight publish/subscribe messaging protocol designed for low-bandwidth environments
◦
A client (the robot) connects to a broker (the server) and subscribes to topics, then receives messages published to those topics in real time
◦
mqtts:// is MQTT wrapped in TLS
•
A lightweight publish/subscribe messaging protocol designed for low-bandwidth environments
•
A client (the robot) connects to a broker (the server) and subscribes to topics, then receives messages published to those topics in real time
•
mqtts:// is MQTT wrapped in TLS
•
mTLS (mutual TLS)
◦
Two-way TLS authentication in which server and client each verify the other’s certificate
•
Two-way TLS authentication in which server and client each verify the other’s certificate
•
CDN
◦
A delivery network that serves large files from geographically distributed servers. Go2 firmware packages are downloaded from a CDN over HTTPS
•
A delivery network that serves large files from geographically distributed servers. Go2 firmware packages are downloaded from a CDN over HTTPS
•
MD5 checksum
◦
An integrity check that computes a 128-bit hash over a file’s contents and compares it to see whether the received file is the expected one
•
An integrity check that computes a 128-bit hash over a file’s contents and compares it to see whether the received file is the expected one

4.1.2. OTA Orchestration Daemon

Go2’s OTA is split into an orchestration daemon that receives and directs commands and a utility suite that does the actual work. The top-level daemon is ota_boxed (version v1.2.1.4).
As we saw in Part 3, ota_boxed is started via start-stop-daemon by the SysV init script /etc/init.d/ota_box. The executable is /unitree/ota/engine/ota_boxed and the PID file is /unitree/var/run/ota_boxed.pid. The init script’s Required-Start lists $local_fs, $network, $named, and master_service, so it starts only after networking and DNS are ready and master_service is up. ota_boxed is also on master_service’s list of managed services, which means it is managed by both the init script and master_service.
The daemon’s settings live in ota_boxed.json, with values like MQTTAuth: 1, CheckUpdateInterval: 5, and ReportVersionInterval: 1800. Logs accumulate under /unitree/var/log/ota_box.
From boot to reporting results, ota_boxed is responsible for:
1.
Keeping a TLS connection to the Unitree cloud MQTT broker after boot.
2.
Subscribing to its own device-specific command topic, cmd/<SN>.
3.
Interpreting incoming JSON commands to decide which helper utility to run and with which arguments.
4.
Running the helper utility as a child process.
5.
Sending the outcome back on the MQTT report topic.
ota_boxed acts as the control plane, relaying “cloud command → local utility run”. ota_boxed and ota_utils are protected binaries, so we unpacked them separately before analysis.

4.1.3. Package Processing Engine

ota_utils (/unitree/ota/engine/bin/ota_utils) is an ARM64 CLI executable that runs the download, unpack, verify, and install state machine for package updates. Its initialization constructor sets up the paths of its sibling utilities as strings, which reveals the makeup of the whole OTA suite.
// initialization constructor sub_9343F8 std::string::basic_string(&unk_D89B28, "/unitree/ota/engine", v89); sub_925AB8(qword_D89B48, &unk_D89B28, "/bin/ota_utils"); sub_925AB8(qword_D89B68, &unk_D89B28, "/bin/ota_module_utils"); sub_925AB8(qword_D89B88, &unk_D89B28, "/bin/ota_engine_utils"); sub_925AB8(qword_D89BA8, &unk_D89B28, "/bin/ota_report_utils"); sub_925AB8(qword_D89BC8, &unk_D89B28, "/bin/ota_remote_utils"); sub_925AB8(qword_D89BE8, &unk_D89B28, "/bin/ota_verify_utils"); sub_925AB8(qword_D89C08, &unk_D89B28, "/bin/ota_download_utils"); std::string::basic_string(&unk_D89C28, "/unitree/sbin/ota_pipe_cli", v89); std::string::basic_string(&unk_D89C48, "/unitree/var/run/ota_boxed.sock", v89);
C
복사
sub_925AB8 concatenates two strings: it appends a utility name to the base path /unitree/ota/engine to build a global string. The resulting paths are listed below.
Path
Role
/unitree/ota/engine/bin/ota_utils
Full package processing engine
/unitree/ota/engine/bin/ota_module_utils
Per-module install / rollback / removal
/unitree/ota/engine/bin/ota_engine_utils
Update / rollback of the OTA engine itself
/unitree/ota/engine/bin/ota_report_utils
Progress and result reporting
/unitree/ota/engine/bin/ota_remote_utils
Remote handling (inferred from the name)
/unitree/ota/engine/bin/ota_verify_utils
Handles standalone verification requests (MD5)
/unitree/ota/engine/bin/ota_download_utils
libcurl-based downloader
/unitree/sbin/ota_pipe_cli
Pipe CLI (inferred from the name)
/unitree/var/run/ota_boxed.sock
Unix domain socket for local IPC
[Table 10] OTA utility suite
ota_utils is a dynamically linked PIE binary with roughly 47,989 functions and 30,148 strings. libcurl, libtar, libssh2, protobuf, beanstalk-client, paho-mqtt, and others are statically linked in, while OpenSSL is imported dynamically. The downloading (libcurl), unpacking (libtar), and reporting (protobuf/beanstalk) described below all run on top of this stack.
ota_boxed and ota_utils form a delegation chain: ota_boxed takes the cloud command and runs ota_utils (or another helper), and ota_utils in turn runs ota_module_utils during module processing. The process chain spans several levels: daemon → engine → module utility.

4.2. OTA Process Flow

The backbone of the OTA flow is receive MQTT cmd/<SN> → parse JSON → run helper → download / MD5 verify / install → report result. Per unit of work, one MQTT message = one OTACmdArgs = one helper run + one result report. The cloud sends verification, download, and installation to the robot as separate MQTT messages, in order.

4.2.1. Startup and Subscribing to MQTT cmd/<SN>

After boot, ota_boxed reads ServerUri from the MQTT config file (/unitree/etc/mqtt/mqtt_client_conf.json), connects to the broker, and subscribes to a topic based on its own serial number. The connection uses AuthType=1 (mTLS) with server certificate verification turned on. The connection settings include KeepAliveInterval: 10, AutoReconnect, and ReconnectInterval: 10, and the broker address is robot-mqtt (CN) or global-robot-mqtt (everywhere else) depending on region.
ssl option enable_server_cert_auth:1, verify:1 connect to the MQTT server success. subscribe topic success. topic:cmd/<SN>
Plain Text
복사
With this design the robot opens no listening port for OTA. The robot makes the outbound connection to the broker first and receives commands over it, so an OTA command reaches the robot only if it is published through the broker to that robot’s cmd/<SN> topic.

4.2.2. Message Fields and Command Dispatch

An incoming MQTT message goes through this callback chain:
MQTTMessageCallback → MQTTMessageHandler → enqueue OTACmdArgs → OTAThreadFunction → OTABox::ExecuteOTACmd
Plain Text
복사
ExecuteOTACmd uses eight fields: command (JSON cmd), token, module, version, package, url, sign, and g (stage). Despite its name, sign is not a digital signature but the expected MD5 value. The JSON cmd value decides which helper runs.
JSON cmd
Helper executed
Role
updatePackage
ota_utils -c updatepackage ... -g <stage>
Full package update (download / install stage)
verifyPackage / verifyPackageRequest
ota_verify_utils -c verifypackage ...
Check package presence and MD5
updateModule
ota_module_utils -c updatemodule ...
Update a single module
updateModule + module=ota_engine
ota_engine_utils -c update ...
Update the OTA engine itself
fallbackPackage
ota_module_utils -c fallbackpackage ...
Roll back the full package
fallbackModule
ota_module_utils -c fallbackmodule ...
Roll back a module
removeModule
ota_module_utils -c removemodule ...
Remove a module (except ota_engine)
[Table 11] OTA commands and the helpers they run
For example, updatePackage runs ota_utils with a command line like this:
/unitree/ota/engine/bin/ota_utils -c updatepackage -v <version> -p <package> -u <url> -i <sign> -t <token> -g <stage> -r
Plain Text
복사
ota_utils parses these arguments with the getopt string "c:v:m:p:u:i:t:s:g:rh" and branches to a per-command handler (updatepackage → OTAUtils::DoUpdatePackage, and so on).
Option
Meaning
-c
command (updatepackage / updatemodule / fallbackmodule / removemodule)
-v
Target version
-m
Module name
-p
Package file name
-u
Package URL
-i
sign (expected MD5)
-g
Update stage (download / install)
-t
OTA token
-r
Enable result reporting
[Table 12] ota_utils CLI options
Argument parsing and command dispatch are both in sub_923B28, which is called right after main.
sub_925AB8(v23, &unk_D88218, "/etc/ota_engine.json"); sub_925AB8(v24, &unk_D88218, "/etc/ota_engine_log_conf.json"); ... strcpy(shortopts, "c:v:m:p:u:i:t:s:g:rh"); while ( 1 ) { v22 = getopt(a1, a2, shortopts); ... } ... if (sub_925B44(v25, "updatepackage")) v21 = sub_9266DC(v33, v26, v28, v29, v30, v31, v32); // token, ver, pkg, url, sign, stage else if (sub_925B44(v25, "updatemodule")) v21 = sub_92691C(v33, v26, v27, v28, v29, v30, v31); else if (sub_925B44(v25, "fallbackmodule")) v21 = sub_926B38(v33, v26, v27, v28); else v21 = sub_926DA8(v33, v26, v27); // removemodule
C
복사
If the command is none of the four, an earlier check prints command is not supported. and exits. sub_9266DC, the handler for updatepackage, is OTAUtils::DoUpdatePackage. It builds an OTAUpdateTask from the parsed arguments and drives the install state machine.

4.2.3. Download Stage / Install Stage

A full package update does not run end to end from a single command. The cloud sends the same cmd value, updatePackage, as separate messages per stage (config value TowStageUpdateEnable:1). The order is pre-verification request → download stage → install stage. Below is a download trigger message found in the logs of a real unit.
{ "cmd": "updatePackage", "msgId": "2390f5ad43534a57a53d57e993c11fcb", "package": { "name": "package_1.1.11.5_GO2_Edu_Max_Pro_X_1761128358872.upk", "sign": "716f34ec0391e5a461b26df2fa11eb3e", "size": 341397620, "url": "https://firmware-cdn.unitree.com/firmware/release/package_1.1.11.5_GO2_Edu_Max_Pro_X_1761128358872.upk", "version": "1.1.11" }, "stage": "download", "targetPackageVersion": "1.1.2" }
JSON
복사
cmd is the first-level action branch and stage the second-level stage branch. The package.url to download is not hardcoded on the robot; the message specifies it. After the download finishes, the cloud polls progress roughly every 10 seconds and then sends the install-stage message separately.
sequenceDiagram
    autonumber
    participant C as Unitree cloud (MQTT 17883)
    participant B as ota_boxed (sub cmd/SN)
    participant U as ota_utils / helpers
    participant CDN as firmware-cdn.unitree.com

    B->>C: subscribe cmd/SN + report version
    C-->>B: verifyPackageRequest
    B->>U: ota_verify_utils -c verifypackage (MD5)
    U-->>B: result
    Note over C,B: download stage
    C-->>B: updatePackage {package{url, sign, size, version}, stage: download}
    B->>U: ota_utils -c updatepackage -u url -i md5 -g download -r
    U->>CDN: HTTPS GET url (Range 256KB chunks)
    CDN-->>U: .upk (≈341MB)
    U->>U: MD5(payload) == sign
    loop every ~10s
        C-->>B: getOtaRunningTaskIdRequest
        B-->>C: token (running)
    end
    Note over C,B: install stage (separate message)
    C-->>B: updatePackage {..., stage: install}
    B->>U: ota_utils -c updatepackage -g install -r
    U->>U: MD5 re-check → unpack → module backup → install cmds
    U-->>B: done
    B-->>C: final report
Mermaid
복사
In this flow the app only issues instructions and shows progress. The app’s HardwareUpdate2Activity screen shows a two-step progress bar for download and install, which maps directly onto the robot-side stage split. The app tells the robot to download and install through the firmware API on robot-api (CN) or global-robot-api (INTL), and queries the running task about every 10 seconds. Despite its name, the firmware/package/download API doesn’t fetch the file either; it tells the robot to start downloading and gets an updateId back. The package binary never passes through the app; it moves only between the cloud and the robot.

4.3. Package Handling and Verification

4.3.1. Where the Download URL Comes From, and TLS Handling

The download URL is set by package.url in the MQTT message. The firmware distribution infrastructure we actually observed is Unitree’s own CDN (firmware-cdn.unitree.com, unitree-firmware-cdn.unitree.com) and Alibaba Cloud OSS (unitree-firmware.oss-accelerate.aliyuncs.com). The command channel (MQTT) and the data channel (HTTPS download) are separate, and the cloud connection runs over both WiFi (wlan0) and 4G (wwan0).
flowchart LR
    subgraph Robot["Go2"]
        OTA["ota_boxed (sub cmd/SN)"]
        ENG["ota_utils / ota_download_utils"]
        NET["wlan0 (WiFi) + wwan0 (4G)"]
    end
    subgraph UnitreeCloud["Unitree cloud"]
        MQ["robot-mqtt:17883 / global-robot-mqtt:17883<br/>(mqtts, mTLS)"]
        FW["firmware-cdn.unitree.com"]
    end
    subgraph Aliyun["Alibaba Cloud"]
        OSS["unitree-firmware.oss-accelerate.aliyuncs.com"]
    end
    OTA <-->|"command / report (mTLS)"| MQ
    ENG -->|"HTTPS download"| FW
    ENG -->|"HTTPS download"| OSS
    ENG --> OTA
    NET -. "4G always-on" .-> UnitreeCloud
Mermaid
복사
The command channel (MQTT) connects with both server certificate chain verification (enableServerCertAuth) and hostname verification (verify) turned on. That is the ssl option enable_server_cert_auth:1, verify:1 line in the MQTT connection log.
For the data channel (HTTPS), ota_utils runs ota_download_utils -u <url> -o <packageFile> when needed, and the CurlDownloader (libcurl) inside it does the actual transfer. Downloader settings are read from /unitree/ota/engine/etc/ota_download_utils.json (DownloadDir=/unitree/ota/download, LimitRate=0.65).
The download first checks the remote file info (HTTP 200, file time, size) and then repeatedly requests 256KB (0x40000) chunks with CURLOPT_RANGE, making it a resumable download. The point is to pull firmware hundreds of MB in size (about 341MB measured) over an unreliable network without starting over.
Here is a reconstruction of the download loop in CurlDownloader::Download. It is pseudocode to explain the flow, not the original code.
// CurlDownloader::Download download loop (reconstructed pseudocode) LoadInfo(); // load previous download info GetRemoteFileInfo(); // check HTTP 200, filetime, size current = downloaded_size; // resume position while (current < remote_size) { end = min(current + 0x40000, remote_size); CURLOPT_RANGE = "%ld-%ld" (current, end - 1); curl_easy_perform(...); WriteCallback -> Update -> UpdateData; // flush to temp file once the buffer exceeds 0x1fffff current = end; } Complete(); // if sizes match, rename temp → output and delete info
C
복사
If earlier download info exists, CheckInfo compares the remote file’s time and size against the stored values. If either has changed, or the temp file is larger than the remote size, it is wiped with Cleanup and the download starts from scratch.

4.3.2. UPK Integrity Check (MD5 Checksum)

In the OTA flow, package integrity is checked by comparing MD5 checksums. The protocol’s sign field carries the expected MD5 value, and two functions handle verification.
Function
Role
VerifyPackage
Checks the file size condition, then calls VerifyFile
VerifyFile
Checks the file exists → computes MD5 → compares with sign
[Table 13] Package verification functions
# VerifyPackage logic summary if PackageFileSize > 0: if GetFileSize(package) != PackageFileSize: return 40 if sign is not empty: if VerifyFile(package, sign) == false: return 40 return 0
Python
복사
VerifyFile runs ExistFile → MD5Checksum → Compare(computed, sign, false) and lowercases both sides before comparing, so the check is case-insensitive. When VerifyFile returns false, VerifyPackage turns that into result code 40; on a match it returns 0. 45 (file not found) is what ota_verify_utils reports for standalone verification requests.
Verification happens at two points.
•
Download stage: DoDownload first uses VerifyFile to see if the file is already on disk, and skips the download if the MD5 matches. If the file is missing or different, it downloads and then verifies again with VerifyPackage.
•
Install stage: DoUnpack runs VerifyPackage again before unpacking, and on failure returns 40 along with unpack verify error.
So MD5 is checked twice: once right after download and again right before install.

4.3.3.Running Install Commands

The install stage runs as a state machine driven by OTAUpdateTask::Execute, with the task state stepping from 3 up to 9.
flowchart TB
    subgraph R1["install stage (state 3 ~ 6)"]
        direction LR
        ST(("start")) --> U["Unpack<br/>state 3"] --> PU["PreUpdate<br/>state 4"] --> CP["CmdPre<br/>state 5"] --> UP["Update<br/>state 6"]
        U -.->|"fail"| FAIL(("return<br/>error"))
        PU -.->|"fail"| FAIL
        UP -.->|"fail"| FAIL
    end
    subgraph R2["install stage (state 7 ~ 9)"]
        direction LR
        UK["UpdatePkg<br/>state 7"] --> CO["CmdPost<br/>state 8"] --> CL["Cleanup<br/>state 9"] --> DONE(("install stage<br/>done"))
    end
    R1 --> R2
Mermaid
복사
State
Step
3
Unpack the package (UPK → TAR extraction)
4
pre-update: parse <unpackDir>/package.json, build the module list
5
Run CmdPre
6
Update modules
7
Update package
8
Run CmdPost
9
Cleanup
[Table 14] Install steps by state
State transitions work by calling each step function in turn, logging on success, and bumping the state value.
sub_939350(a1+40, 3); if (state==3) { sub_96A70C(a1); ... "unpack package success." ; state=4 } // Unpack if (state==4) { sub_96A744(a1); ... "pre update success." ; state=5 } // pre-update (manifest parsing) if (state==5) { sub_96A77C(a1); ... "execute cmdpre success." ; state=6 } // CmdPre if (state==6) { sub_96A7C0(a1); ... "update success." ; state=7 } // update (modules) if (state==7) { sub_96A8B8(a1); ... "update pkg success." ; state=8 } // update-pkg if (state==8) { sub_96A8F0(a1); ... "execute cmdpost success." ; state=9 } // CmdPost if (state==9) { sub_96A934(a1); ... "cleanup success." } // cleanup ... "install stage done. token:"
C
복사
The unpack in state 3 goes through several layers of parsing. The UPK header parser (Package::UnpackFile) reads the name, size, seed, and sign from the 112-byte header; the encrypted container is decrypted by the Tea::DecodeFile family; and Tar::ExtractAll uses libtar to extract it onto the filesystem. In state 4, the package.json manifest in the extracted directory is read and turned into a module list (PackageModuleDesc: Name, Version, File, Sign, etc.) and a list of commands to run (CmdDesc: Cmd, ExpectCode, IgnoreUnexpected, Delay).
The last step of installation, the one that actually changes the system, is running the install commands written in the manifest. Module handling is done by ota_module_utils, which runs the Cmd strings in module.json’s Install.CmdPreList/CmdPostList through OTATask::ExecuteCmd. One example is a service reload command such as /unitree/sbin/mscli reloadservice basic_service.
After replacing module files, OTA reloads or removes services through mscli, the local RPC client of master_service (analyzed in Part 3), so this step connects back to Part 3. Before an installed module actually runs, it goes through the path ota_boxed → ota_utils → ota_module_utils → mscli → master_service.

4.4. Data Flow Analysis

4.4.1. Entry Points

The OTA component differs from the others in that the robot opens no listening port for it. External input comes in over outbound connections the robot opens first.
Entry point
Protocol
Process
Purpose
MQTT cmd/<SN>
MQTTS (TCP 17883, mTLS)
ota_boxed
Receive OTA commands, report results
Package download response
HTTPS
ota_download_utils (CurlDownloader)
Receive the UPK file
ota_boxed.sock
Unix domain socket
ota_boxed / helpers
Local IPC, progress reporting
Config files
File
ota_boxed / ota_utils
mqtt_client_conf.json, ota_engine.json, ota_download_utils.json
[Table 15] OTA entry points

4.4.2. DFD

---
config:
  layout: dagre
  flowchart:
    nodeSpacing: 18
    rankSpacing: 28
---
flowchart TB
    subgraph INET["Trust boundary: Internet"]
        MQ["Unitree MQTT broker<br/>robot-mqtt:17883"]
        CDN["firmware CDN / OSS"]
    end
    subgraph ROBOT["Trust boundary: Go2 robot host"]
        D1[("D1 mqtt_client_conf.json<br/>client certificate")]
        P1("1.0 OTA daemon<br/>ota_boxed")
        P2("2.0 package engine<br/>ota_utils")
        P3("3.0 downloader<br/>ota_download_utils")
        D2[("D2 /unitree/ota/download<br/>*.upk")]
        D3[("D3 unpack dir<br/>package.json / module.json")]
        P4("4.0 module installer<br/>ota_module_utils")
        P5("5.0 service manager<br/>master_service (mscli)")
        D4[("D4 /unitree/module/*")]
    end
    MQ <-->|"cmd/SN JSON / report (mTLS)"| P1
    D1 -->|"ServerUri / cert"| P1
    P1 -->|"helper cmdline"| P2
    P2 -->|"-u url -o file"| P3
    CDN -->|"UPK (HTTPS)"| P3
    P3 -->|"write"| D2
    P2 <-->|"MD5 verify, unpack"| D2
    P2 -->|"extract"| D3
    D3 -->|"module list / Cmd"| P4
    P4 -->|"replace files"| D4
    P4 -->|"mscli reload / remove"| P5
    P5 -.->|"start / monitor"| D4
Mermaid
복사
Walking through the OTA DFD:
1.
The OTA path starts on the Internet and reaches all the way to the robot’s module directory and master_service. Where the other components exchange data with running services, OTA changes the services themselves.
2.
Commands and data arrive on different channels. Commands come over the mTLS-protected MQTT channel, packages come as an HTTPS download from whatever URL the message names, and the two are tied together by the sign (MD5) value.
3.
Processing is spread across several processes. It crosses process boundaries in the order daemon (ota_boxed) → engine (ota_utils) → downloader/module utilities → mscli, and each step hands information to the next through command-line arguments and manifest files.

5. Data Flow from External Components

5.1. Component Summary

The four external components analyzed across Parts 3 and 4, in one table:
Component
External channel
Owning process
Internal target
Protection
BLE
Bluetooth LE (GATT 0xFFE2/0xFFE1)
btgatt-server
hostapd / wpa_supplicant (WiFi config)
App-layer AES + handshake session auth
RTSP
WiFi/LAN TCP 8551, wired multicast UDP 1720
go2_videohub_rtsp_server, videohub
Camera (/dev/video0)
Handled at the GStreamer layer
WebRTC/DDS
WiFi/LAN TCP 80/9991/10001 + DataChannel, cloud TURN
xfkTon, unitreeWebRTCClientMaster
DDS domain (rt/* topics)
Signaling RSA + AES (per-device key), 31-topic publish whitelist
OTA
Internet MQTTS 17883 + HTTPS download
ota_boxed, ota_utils suite
Module files, master_service (mscli)
MQTT mTLS + server certificate verification, MD5 checksum
[Table 16] Summary of external components
The four components line up along how the robot is actually used. BLE creates the WiFi network, RTSP and WebRTC carry video and control over that network, and OTA replaces these services from the cloud.

5.2. Integrated DFD

Merging the four component DFDs gives the diagram below. There are five trust boundaries: BLE radio range, WiFi LAN, Internet, robot host, and the internal wired network (DDS domain).
---
config:
  layout: elk
---
flowchart TB
    subgraph RADIO["Trust boundary: BLE radio range"]
        APPB["Unitree app (BLE)"]
    end
    subgraph INET["Trust boundary: Internet"]
        APPR["Unitree app (remote)"]
        CLOUD["Unitree cloud<br/>robot-api / TURN / MQTT"]
        CDN["firmware CDN / OSS"]
    end
    subgraph WIFI["Trust boundary: WiFi LAN (wlan0 STA / wlan1 AP)"]
        APPL["Unitree app (local)"]
        LANC["LAN client"]
    end
    subgraph ROBOT["Trust boundary: Go2 robot host"]
        BLE("BLE<br/>btgatt-server")
        NET("hostapd /<br/>wpa_supplicant")
        OTA("OTA<br/>ota_boxed / ota_utils")
        MS("master_service")
        DISC("discovery<br/>multicast_responder.py")
        XFK("signaling<br/>xfkTon :80 / 9991 / 10001")
        BR("WebRTC bridge<br/>unitreeWebRTCClientMaster")
        RTSP("RTSP<br/>go2_videohub_rtsp_server :8551")
        CAM["Camera<br/>/dev/video0"]
        VH("videohub")
    end
    subgraph WIRED["Trust boundary: internal wired network (eth0, DDS domain)"]
        DDS["DDS bus (CycloneDDS, UDP 7400)<br/>rt/api/* services"]
        MCG["Multicast<br/>230.1.1.1:1720"]
    end
    APPB <-->|"GATT 0xFFE2 / 0xFFE1"| BLE
    BLE -->|"mode / SSID / PW"| NET
    APPR <-->|"signaling / TURN"| CLOUD
    CLOUD -->|"MQTT cmd/SN (mTLS)"| OTA
    CDN -->|"UPK (HTTPS)"| OTA
    OTA -->|"mscli reload / remove"| MS
    CLOUD <-->|"relay"| BR
    APPL <-->|"discovery"| DISC
    APPL -->|"con_notify / con_ing"| XFK
    APPL <-->|"DataChannel"| BR
    XFK <-->|"offer / answer"| BR
    LANC <-->|"RTSP / RTP H.264"| RTSP
    MCG -->|"RTP H.264"| RTSP
    CAM --> VH
    VH -->|"RTP H.264"| MCG
    VH -->|"rt/frontvideostream"| DDS
    BR <-->|"dds_write / dds_take"| DDS
    MS -.->|"start / monitor"| DDS
Mermaid
복사
Trust boundary
Entering component
Entry points
BLE radio range → robot
BLE
GATT 0xFFE2 write, advertising
WiFi LAN → robot
RTSP, WebRTC/DDS
TCP 8551, TCP 80/9991/10001, DataChannel, UDP 10131
Internet → robot
WebRTC/DDS, OTA
Cloud TURN relay, MQTT cmd/<SN>, HTTPS download
Robot ↔︎ internal wired network
RTSP, WebRTC/DDS
UDP 230.1.1.1:1720, DDS UDP 7400
[Table 17] Entry points by trust boundary
Looking across components on the integrated DFD, a few things stand out.
1.
Each component has exactly one path by which external input reaches internal services. BLE leads to the WiFi configuration, RTSP to the camera feed, WebRTC to the DDS bus, and OTA to master_service and the module directory.
2.
The internal wired network (eth0) serves as the internal bus. RTSP multicast and DDS discovery both run on it, and input from WiFi or the Internet only reaches it by way of each component’s processes.
3.
The same data flows along more than one path. Camera video goes out two ways, RTSP (LAN) and WebRTC (remote), and the per-device AES key is created during BLE binding and then used in WebRTC signaling.
4.
master_service sits behind every path. It starts and supervises the BLE, RTSP, and WebRTC processes, and OTA tells it to swap services via mscli.

6. Conclusion

Across Parts 3 and 4 we answered three questions: who starts the robot’s services and in what order, which of them are exposed to the outside, and along what paths external input travels into the robot.
•
BLE: btgatt-server, built on the BlueZ example, takes commands on 0xFFE2 (Write) and responds on 0xFFE1 (Notify). Commands pass through a branch added to BlueZ’s write_cb and are decrypted and parsed in receive_manager. After a handshake authenticates the session, it receives the WiFi mode and SSID/password and reconfigures hostapd or wpa_supplicant. The details of the application-layer encryption and handshake depend on the network_manager module version.
•
WebRTC/DDS: Signaling (xfkTon, TCP 9991) and the WebRTC endpoint/bridge (unitreeWebRTCClientMaster) are separate processes that exchange offers and answers over DDS topics. JSON arriving on the DataChannel is published to DDS through a 31-topic publish whitelist, and responses come back from 44 subscribed topics to the requesting session only. The internal DDS bus is CycloneDDS multicast on eth0, with the Authentication plugin enabled.
•
OTA: ota_boxed receives commands on the cloud MQTT topic cmd/<SN> and runs the ota_utils suite. Updates are split into a download stage and an install stage; packages are checked twice with an MD5 checksum before unpacking, and master_service’s services are replaced through the manifest commands and mscli.
Finally, we tied all four components, including RTSP from Part 3, into an integrated DFD that shows on a single page the five trust boundaries (BLE radio range, WiFi LAN, Internet, robot host, internal wired network) and the entry points between them. You can now follow, in one diagram, which processes each of Go2’s external interfaces passes through on its way to the WiFi configuration, the camera, the DDS bus, and master_service.

7. References

1.
BlueZ Project, “BlueZ — Official Linux Bluetooth protocol stack”. https://git.kernel.org/pub/scm/bluetooth/bluez.git/
2.
Eclipse Foundation, “Eclipse Cyclone DDS Documentation”. https://cyclonedds.io/docs/
3.
Amazon Web Services, “Amazon Kinesis Video Streams with WebRTC SDK in C”. https://github.com/awslabs/amazon-kinesis-video-streams-webrtc-sdk-c