1. Overview
In Part 2 of the series, we identified the UART debug interface on the Unitree Go2 Pro, obtained a root shell using 1-Day based tooling, and mapped out the eMMC partition layout and OTA package structure extracted from the firmware. That analysis confirmed the partition layout, boot chain, and the presence of robot-specific software under the /unitree directory.
However, knowing what binaries exist on the filesystem is a fundamentally different problem from knowing in what order those binaries actually start after the robot powers on, and which of them communicate with the outside world. Because a robot is a system built from dozens of independent cooperating processes, correctly identifying the attack surface requires understanding who starts what, when, and critically which of those processes expose an externally reachable entry point.
In Series #3, we therefore begin by analyzing the init sequence immediately after system boot and the master_service orchestrator that manages robot services. From there, we use the observed service topology and official architecture documentation to select externally exposed components as candidate attack surface. Finally, we take the first of those candidates the RTSP video streaming service dissect its internal structure through decompiled code, and derive a Data Flow Diagram (DFD).
2. Identify System Services
2.1. init
To understand the runtime structure of a system, you need to know which services start automatically at boot, which process monitors them, and what policy governs restarting a service that has exited. init analysis is therefore a prerequisite for any meaningful service topology work. Our goal here was to understand the runtime structure first, identify the services, and then build a foundation for selecting analysis candidates based on external interface exposure.
2.1.1. PID 1 and the SysV Compatibility Layer
The Go2โs boot process follows a dual-init architecture where OS-level initialization and robot-system-level initialization are kept separate. Standard systemd handles OS initialization while maintaining a SysVinit compatibility layer for legacy support, Unitreeโs own orchestrator, master_service, handles initialization of robot-function services.
root@Unitree:~# ps -p 1 -o pid,comm,args
PID COMMAND COMMAND
1 systemd /sbin/init
root@Unitree:~# ls -l /proc/1/exe
lrwxrwxrwx 1 root root 0 Jun 17 20:29 /proc/1/exe -> /usr/lib/systemd/systemd
root@Unitree:~# cat /etc/inittab
cat: /etc/inittab: No such file or directory
root@Unitree:~# systemctl get-default
multi-user.target
Bash
๋ณต์ฌ
Running ps confirms that PID 1 on the Unitree Go2 is the standard Ubuntu systemd. The /sbin/init invoked by the kernel is a symlink to /usr/lib/systemd/systemd, and the absence of /etc/inittab that core SysVinit configuration file confirms this is a systemd-based environment. The default target, which plays the same role as a run-level in SysV, is multi-user.target.
root@Unitree:~# ls -la /etc/init.d/
total 124
drwxr-xr-x 2 root root 4096 Jun 10 19:54 .
drwxr-xr-x 117 root root 4096 Jun 20 17:41 ..
-rwxr-xr-x 1 root root 5574 Nov 5 2019 alsa-utils
-rwxr-xr-x 1 root root 2920 May 23 2025 apport
-rwxr-xr-x 1 root root 2401 Aug 21 2018 avahi-daemon
-rwxr-xr-x 1 root root 2968 Feb 26 2020 bluetooth
-rwxr-xr-x 1 root root 3059 Feb 11 2020 cron
-rwxr-xr-x 1 root root 3152 Sep 30 2019 dbus
-rwxr-xr-x 1 root root 1905 May 3 2019 dhcpcd
-rwxr-xr-x 1 root root 3033 Nov 10 2020 gdm3
-rwxr-xr-x 1 root root 1893 Aug 14 2019 hostapd
-rwxr-xr-x 1 root root 3809 Jul 29 2019 hwclock.sh
-rwxr-xr-x 1 root root 2044 Feb 19 2020 kmod
-rwxr-xr-x 1 root root 3205 Jan 10 2024 master_service
-rwxr-xr-x 1 root root 1942 Nov 27 2021 network-manager
-rwxr-xr-x 1 root root 4445 Jan 29 2019 networking
-rwxr-xr-x 1 root root 3126 Jan 10 2024 ota_box
-rwxr-xr-x 1 root root 612 Feb 9 2019 pppd-dns
-rwxr-xr-x 1 root root 924 Feb 14 2020 procps
-rwxr-xr-x 1 root root 469 Oct 16 2020 pulseaudio-enable-autospawn
-rwxr-xr-x 1 root root 4417 Nov 12 2022 rsync
-rwxr-xr-x 1 root root 2224 Feb 11 2020 saned
-rwxr-xr-x 1 root root 3939 Apr 4 2023 ssh
-rwxr-xr-x 1 root root 6872 Mar 15 2023 udev
-rwxr-xr-x 1 root root 1416 Nov 11 2020 udhcpd
-rwxr-xr-x 1 root root 1391 Apr 25 2022 unattended-upgrades
-rwxr-xr-x 1 root root 2757 Jan 20 2017 x11-common
root@Unitree:~# ls -la /lib/systemd/system-generators/systemd-sysv-generator
-rwxr-xr-x 1 root root 26624 Mar 15 2023 /lib/systemd/system-generators/systemd-sysv-generator
Bash
๋ณต์ฌ
Not every service is defined as a native systemd unit file. /etc/init.d contains 25 LSB-format SysV scripts, each with execution symlinks in /etc/rcS.d and /etc/rc2.d.
systemd-sysv-generator is the tool that automatically converts legacy SysV init scripts (/etc/init.d/*) into systemd units at boot time. During boot, systemd uses this generator to convert any SysV script that doesnโt have a same-named native unit into a .service unit and starts it. If a native unit with the same name already exists, the SysV script is ignored.
2.1.2. Service Startup Sequence by Boot Phase
root@Unitree:~# ls -1 /etc/rcS.d/
S01alsa-utils
S01kmod
S01networking
S01pppd-dns
S01procps
S01udev
S01x11-common
root@Unitree:~# ls -1 /etc/rc2.d/
K01hostapd
S01apport
S01avahi-daemon
S01bluetooth
S01cron
S01dbus
S01dhcpcd
S01gdm3
S01master_service
S01ota_box
S01pulseaudio-enable-autospawn
S01rsync
S01saned
S01ssh
S01udhcpd
S01unattended-upgrades
Bash
๋ณต์ฌ
There are 7 SysV start symlinks in rcS.d and 16 in rc2.d. The rcS.d entries cover device nodes, kernel modules, sysctl, network interfaces, and audio initialization (udev, kmod, procps, networking, alsa-utils). The rc2.d entries cover OS-level network services (dbus, dhcpcd, ssh, bluetooth, avahi-daemon, udhcpd, rsync) along with Unitreeโs own master_service and ota_box.
root@Unitree:~# head -20 /etc/init.d/master_service
#! /bin/sh
###BEGIN INIT INFO
# Provides: master_service
# Required-Start: $local_fs $network $named
# Required-Stop: $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: master service init script.
###END INIT INFO
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/unitree/module/master_service/master_service
NAME=master_service
PIDFILE=/unitree/var/run/master_service.pid
root@Unitree:~# head -20 /etc/init.d/ota_box
#! /bin/sh
###BEGIN INIT INFO
# Provides: ota_box
# Required-Start: $local_fs $network $named master_service
# Required-Stop: $remote_fs
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: ota_box init script.
###END INIT INFO
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/unitree/ota/engine/ota_boxed
NAME=ota_box
PIDFILE=/unitree/var/run/ota_boxed.pid
Bash
๋ณต์ฌ
Among the 25 scripts under /etc/init.d/, the master_service and ota_box scripts both invoke Unitree authored binaries located under /unitree. Because neither has a same named native systemd unit, both go through systemd-sysv-generatorโs conversion process before being launched.
master_service internally starts 30+ robot-function processes, while ota_box is declared to start only after master_service is already running.
Based on the above findings, the Go2โs init process can be expressed as three distinct layers:
Go2's three-layer init structure
โข
Layer 1: systemd (PID 1) and native systemd units handle OS initialization.
โข
Layer 2: systemd-sysv-generator converts any SysV scripts in rcS.d and rc2.d that lack a same named native unit (e.g., master_service, ota_box) into .service units and runs them.
โข
Layer 3: At the multi-user.target phase, master_service starts and manages each functional domain of the robot. ota_box follows sequentially immediately after.
The complete boot sequence for the Unitree Go2, incorporating deeper analysis of the base scripts, is shown below.
Go2 initial boot sequence
2.2. master_service
Next, we dive into master_service itself. As we saw in the init analysis, it plays a central role inside the Go2.
Running systemctl status master_service shows that the SysV script launches one master_service daemon via start-stop-daemon, which then forks one child process that giving us two master_service processes in total. We can also see that various module binaries and Python scripts are managed as child processes of master_service.service.
root@Unitree:/etc/init.d# systemctl status master_service
โ master_service.service - LSB: master service init script.
Loaded: loaded (/etc/init.d/master_service; generated)
Active: active (running) since Wed 2026-06-17 19:03:45 CST; 44min ago
Docs: man:systemd-sysv-generator(8)
Tasks: 443 (limit: 9478)
Memory: 1.1G
CGroup: /system.slice/master_service.service
โโ 669 /unitree/module/master_service/master_service
โโ 674 /unitree/module/master_service/master_service
โโ 966 /unitree/module/network_manager/upper_bluetooth/btgatt-server -i hci0 -r -v
โโ 988 /unitree/sbin/iox-roudi
โโ 1002 /unitree/module/basic_service/basic_service
โโ 1336 /unitree/module/video_hub/go2_videohub_rtsp_server
โโ 1632 /unitree/module/video_hub/videohub high
โโ 1748 /unitree/module/webrtc_bridge/bin/unitreeWebRTCClientMaster
โโ 1777 /unitree/module/webrtc_bridge/src/webrtc_dds_bridge/xfkTon
...
Plain Text
๋ณต์ฌ
โข
Loaded: ... generated: This unit was generated from a SysV script by systemd-sysv-generator.
โข
Tasks: 443, Memory: 1.1G: The entire set of robot functions is contained within this single CGroup.
In other words, master_service is the orchestrator that starts and stops every robot function and monitors system state. If systemd is the service manager for the robotโs operating system, master_service is the service manager for the robot application layer. Runtime information gathered on master_service is summarized in Table 1 below.
Item | Value |
Init script | /etc/init.d/master_service |
Binary | /unitree/module/master_service/master_service (ELF 64-bit aarch64, ~9.2 MB, not stripped) |
Linked libraries | libpthread, libcrypto.so.1.1, libstdc++, libc, etc. |
Local config | /unitree/module/master_service/master_service.json (1,080 bytes) |
Policy directory | /unitree/etc/master_service/ |
Launch method | start-stop-daemon --background --make-pidfile |
Process structure | 1 parent (guardian) + 1 forked child (worker) |
Protection | Virbox packing + obfuscation |
[Table 1] master_service basic information
The master_service binary is packed and obfuscated with Virbox, a commercial code-protection tool, which makes direct static analysis impractical. We therefore ran the binary inside a QEMU environment, extracted the function code restored to memory at runtime (i.e., unpacked it), and performed analysis on the extracted image.
2.2.1. Load Configuration
The first thing master_service does is load and parse its configuration file, master_service.json.
Configuration file has a .json extension, but file reports it as data, and the first four bytes are the magic bytes FMX\x01 that indicating it is packed in the FMX (File MiXer) container format.
$ file master_service.json
master_service.json: data
$ hexdump -C master_service.json | head -1
00000000 46 4d 58 01 ... |FMX.............|
Plain Text
๋ณต์ฌ
FMX container format and its decryption structure are covered in detail in Vรญctor Mayoral-Vilchesโs prior research The Cybersecurity of a Humanoid Robot [1], which analyzed the Unitree G1, so we wonโt go into depth here.
Source : https://arxiv.org/html/2509.14096v1
In our research, we used that prior work as a lead to trace the path by which Go2โs master_service reads its configuration, verify the contents, and then focus on the behavioral structure of master_service as revealed in the decrypted configuration.
The decrypted master_service.json contents are as follows:
{
"ServiceName": "MasterService",
"IPCParameter": {
"SocketFileName": "/unitree/var/run/master_service.sock",
"ThreadNumber": 5,
"QueueLength": 10,
"Timeout": 10000000
},
"Parameter": {
"ChildServiceDirectory": "/unitree/etc/master_service/service",
"ChildCmdDirectory": "/unitree/etc/master_service/cmd",
"ChildInitFile": "/unitree/etc/master_service/init",
"ChildPlanFile": "/unitree/etc/master_service/plan",
"ChildPathFile": "/unitree/etc/master_service/path",
"ChildOnceFile": "/unitree/etc/master_service/once",
"ChildForbidFile": "/unitree/etc/master_service/forbid",
"ChildManualFile": "/unitree/etc/master_service/manual",
"ChildProtectFile": "/unitree/etc/master_service/protect",
"ChildConflictFile": "/unitree/etc/master_service/conflict",
"ChildExecuteDelay": 100000,
"ChildInforceSecurity": 1,
"ChildServiceMonitorInterval": 5000000,
"ChildServiceMaxProtectTimes": 30
}
}
JSON
๋ณต์ฌ
Setting aside the ServiceName key, the configuration is divided into two top-level fields: IPCParameter and Parameter.
โข
IPCParameter: Defines the Unix domain socket for communication with the local CLI client mscli.
โข
Parameter: Specifies, across 14 entries, which files to read to populate the service list, execution order, policies, and monitoring interval. The actual launch commands for each service live not in this config file, but in the directory pointed to by ChildServiceDirectory.
2.2.2. Service Definitions and Policy Files
Under /unitree/etc/master_service/, which Parameter points to, are service definitions, one-shot commands, and policy files. Service definitions (service/) are plain JSON; cmd/ and policy files use the same FMX format as master_service.json.
Path | Role | Details |
service/ | Start/Stop/Status commands per service | 32 definitions, plaintext |
cmd/ | One-shot command definitions | 10 entries: am-init, core-init, lo-multicast, pd-init, ota-update, etc. |
init | Initial execution list at boot | 7 cmd/service entries |
plan | Allowlist of all managed cmd/service items | Includes names not present in service/ |
manual | Items that donโt auto-start; triggered by mode-switch requests | 4gcm, sport_mode, mcf, ai_sport, etc. |
once | Items to execute only once | basic_service, pd-init, ota-update, etc. |
forbid | Execution blocklist | 1 test entry |
protect | Services to monitor and auto-restart | basic_service (1 entry) |
conflict | Groups of mutually exclusive services | 5 motion-control services in 1 group |
path | Helper aliases โ binary paths and MD5 hashes | 3 entries |
[Table 2] master_service policy layout
Using webrtc_bridge as an example, a service definition in service/ looks like this:
{
"Start": {
"Cmd": "ulimit -c unlimited; export CYCLONEDDS_URI=/unitree/etc/cyclonedds_noshm.xml;/unitree/sbin/start-stop-daemon --start --background --make-pidfile --pidfile=/unitree/var/run/webrtc_bridge.pid --exec /unitree/module/webrtc_bridge/bin/unitreeWebRTCClientMaster",
"ExpectCode": [0]
},
"Stop": {
"Cmd": "/unitree/sbin/start-stop-daemon --signal=KILL --stop --pidfile=/unitree/var/run/webrtc_bridge.pid --exec /unitree/module/webrtc_bridge/bin/unitreeWebRTCClientMaster",
"ExpectCode": [0, 1]
},
"Status": {
"Cmd": "/unitree/sbin/start-stop-daemon --status --pidfile=/unitree/var/run/webrtc_bridge.pid --exec /unitree/module/webrtc_bridge/bin/unitreeWebRTCClientMaster",
"AliveCode": [0],
"DeadCode": [1, 3]
}
}
JSON
๋ณต์ฌ
โข
Start.Cmd / ExpectCode: The start command and the exit codes considered successful.
โข
Status.Cmd / AliveCode / DeadCode: Liveness is determined based on a PID file under /unitree/var/run.
Each serviceโs execution method is ultimately defined as a shell command string. Some services receive their DDS configuration by having the CYCLONEDDS_URI environment variable injected via this command string, which will be discussed further in the RTSP analysis in Section 4.
The conflict policy groups the motion-control family as a single exclusive set:
[
["advanced_sport", "ai_sport", "mcf", "sport_mode", "wheeled_sport"]
]
JSON
๋ณต์ฌ
Services in this group are managed exclusively so that no two can become the active controller simultaneously. The same names also appear in manual, meaning they donโt start automatically at boot and are instead launched on demand via mode switch requests.
2.2.3. Initial Service Launch
After loading the policies, master_service executes one-shot commands and base services in the order defined by the init list:
pd-init // cmd
lo-multicast // cmd
upper_bluetooth // service (BLE GATT server: btgatt-server)
ota-box // cmd
ota-update // cmd
iox-roudi // service
basic_service // service
Plain Text
๋ณต์ฌ
โข
iox-roudi: The central management daemon (RouDi) for the iceoryx shared memory IPC middleware. It manages the shared-memory region used when DDS communication on the same machine uses shared memory transport.
Immediately after boot, the master_service log shows PID filenames printed in rapid succession:
root@Unitree:~# journalctl -b 0 -o short-precise | grep 'master_service\[922\]'
Jun 20 16:59:58.362485 Unitree master_service[922]: 4gcm.pid
Jun 20 16:59:58.368151 Unitree master_service[922]: audio_player_service.pid
Jun 20 16:59:58.369852 Unitree master_service[922]: audiohub.pid
...
Jun 20 16:59:58.412850 Unitree master_service[922]: robot_state_service.pid
Plain Text
๋ณต์ฌ
One of the cmd/ definitions contains the following command:
cd /unitree/var/run; ls *.pid | while read pidfile; do
echo $pidfile;
[ "$pidfile" != "master_service.pid" ] && [ "$pidfile" != "ota_boxed.pid" ] && rm -f $pidfile;
done
Bash
๋ณต์ฌ
This prints the name of each PID file in /unitree/var/run and then removes all of them except master_service.pid and ota_boxed.pid. The log output matches this command exactly, so the log represents the cleanup of stale PID files left over from the previous boot. Given the commandโs content and its position as the first entry in the init list, this cmd is likely pd-init.
Once cleanup finishes, each serviceโs Start.Cmd is executed. After boot completes, /unitree/var/run contains 30 PID files including master_service.pid.
Both the initial launch and RPC request handling flow through the same ChildExecutor path. The recovered symbols and runtime addresses are:
0x2147d0 ChildExecutor::StartService(std::string const&, bool)
0x56aba8 ChildExecutor::StatusService(std::string const&)
0x228280 ChildExecutor::ExecuteServiceStart(ChildService const&)
0x23e3d8 ChildExecutor::ExecuteServiceStatus(ChildService const&)
0x52c3f8 ChildExecutor::CallSystem(...)
0x2ed318 ChildExecutor::IsPlanedService(std::string const&)
0x21f3e0 ChildExecutor::IsPlanedCmd(std::string const&)
0x1efae0 ChildExecutor::IsForbidden(std::string const&)
0x1ec170 ChildExecutor::IsManual(std::string const&)
0x20c110 ChildExecutor::IsOnce(std::string const&)
0x1e85b0 ChildExecutor::IsOnceDeny(std::string const&)
0x21aa60 ChildExecutor::CheckConfliction(std::string const&)
0x20a380 ChildExecutor::CheckServiceProtectCount(std::string const&, int&)
Plain Text
๋ณต์ฌ
Reconstructing the policy check sequence from individual functions and their call relationships yields the following pseudocode. This is not a recovery of the C++ but a representation of the flow for explanatory purposes and the actual implementation includes additional error code handling, state table updates, and locking.
// ChildExecutor::StartService policy flow (reconstructed pseudocode)
bool StartService(const std::string &name, bool force)
{
if (!IsPlanedService(name) || IsForbidden(name)) // plan / forbid
return false;
if (!AllowedByManualOnce(IsManual(name), IsOnce(name),
IsOnceDeny(name), force)) // manual / once
return false;
if (!CheckConfliction(name)) // conflict
return false;
ChildService svc = service_map.at(name); // service/ definition
if (ExecuteServiceStatus(svc) == ALIVE) // Status.Cmd
return true;
int rc = ExecuteServiceStart(svc); // Start.Cmd โ CallSystem
return svc.ExpectCode.contains(rc);
}
C++
๋ณต์ฌ
The service start decision proceeds in the order plan โ forbid โ manual/once โ conflict โ Status โ Start, and the final execution passes the service definitionโs shell command string to CallSystem.
2.2.4. Local RPC Based Service Control
master_service receives local RPC requests over the Unix domain socket at /unitree/var/run/master_service.sock. The client sending requests is /unitree/sbin/mscli. OTA logic, mode-switch scripts, and operator shells never invoke a service directly. Instead, they send requests to master_service through mscli.
The expected usage patterns for mscli are as follows. These are inferred from the analyzed RPC structure rather than observed execution results.
/unitree/sbin/mscli listservice
/unitree/sbin/mscli startservice basic_service
/unitree/sbin/mscli stopservice sport_mode
/unitree/sbin/mscli restartservice webrtc_bridge
/unitree/sbin/mscli listcmd
/unitree/sbin/mscli executecmd lo-multicast
Bash
๋ณต์ฌ
From the recovered MasterService::Register(), 12 RPC handler registration points were identified:
GetServiceState -> 0xb000e4
ListServiceState -> 0xb002d4
StartService -> 0xb004f8
StopService -> 0xb00654
RestartService -> 0xb007ac
ReloadService -> 0xb00904
RemoveService -> 0xb00aa0
GetServiceEnable -> 0xb00b68
GetCmdState -> 0xb00cf0
ListCmdState -> 0xb00ef4
ExecuteCmd -> 0xb01130
RemoveCmd -> 0xb012cc
Plain Text
๋ณต์ฌ
Processing a single mscli startservice basic_service call looks like this:
mscli request handling flow
This socket is a local Unix socket and therefore not directly reachable from the network. However, any service that accepts external input and needs to start or stop other services will ultimately route through this RPC. The OTA component weโll select in Section 3 uses this path before and after module installation.
2.2.5. Health Monitoring and Auto-Restart
The monitor thread checks service health at intervals defined by ChildServiceMonitorInterval (5,000,000), which corresponds to a 5-second cycle if the unit is microseconds. Currently, only basic_service is registered in the protect policy, with a maximum protection count of 30, as specified by ChildServiceMaxProtectTimes.
Reconstructing the decision flow from the configuration values and CheckServiceProtectCount call:
// monitor thread behavior (reconstructed pseudocode; restart condition details are inferred)
while (running) {
usleep(5000000); // 5 seconds if unit is ฮผs
for (const auto &svc : protect_map) { // currently: basic_service
if (StatusService(svc.name) == DEAD) {
int count = 0;
if (CheckServiceProtectCount(svc.name, count) && count < 30)
StartService(svc.name, true);
}
}
}
C++
๋ณต์ฌ
This structure has implications for dynamic analysis. Killing basic_service will cause master_service to restart it first, and repeated kills will eventually exhaust the protection count and prevent further restarts. For safe analysis, stop the target service cleanly via mscli stopservice before proceeding.
2.2.6. Overall Operation and Managed Services
To summarize, master_service reads the configuration file (ยง2.2.1), builds service/command/policy tables (ยง2.2.2), and then performs initial launch (ยง2.2.3), RPC request handling (ยง2.2.4), and health monitoring (ยง2.2.5). The overall flow is:
master_service initialization and execution flow
Category | Service (binary) | Role |
Network/
Communication | iox-roudi, net_switcher, 4gcm, webrtc_bridge (unitreeWebRTCClientMaster), webrtc_signal_server (xfkTon), webrtc_multicast_responder | Shared-memory IPC, WiFi/4G switching, WebRTC signaling/relay, robot discovery response |
Core Control | basic_service, motion_switcher, robot_state, mcf (mcf_main), sbus_handle | Hardware bridge, motion mode switching, state management, motion framework, remote control input |
Sensors/Perception | unitree_lidar, unitree_lidar_slam (uslam_server), obstacles_avoid, voxel_height_mapping, gesture_recognition | LiDAR, SLAM, obstacle avoidance, height map, gesture recognition |
Media | video_hub (videohub), go2_videohub_rtsp_server, audio_hub (audiohub), audio_player_service, vui_service | Video capture, RTSP video delivery, audio, voice UI |
OTA | ota_box (ota_boxed) | Cloud command reception and update processing |
AI/Misc | bashrunner, battery_guard, chat_go, pet_go, programming_actuator, utrack (uwb_runner) | Command execution, battery monitoring, conversation, pet mode, user programs, UWB tracking |
Onboarding | upper_bluetooth (btgatt-server) | WiFi credential delivery via BLE GATT |
[Table 3] master_service managed services by category
The 30 runtime PID files and service/ definitions identified during master_service analysis have been organized by function category in Table 3. This table provides the foundation for tracing how external input enters via gateway services and which internal services it reaches.
3. Identify Attack Surface
3.1. Identify External Interfaces
By combining the service topology observed in master_service, the listening port and process mapping from the live device, the app connection behavior, and the official hardware architecture diagramโs external communication interface data, we identified the components that receive input from outside the robot.
Since internal robot communications, such as DDS topics and master_service.sock, require access through an external entry point, we prioritized identifying entry points directly accessible over wireless or network connections.
Unitree Go2 hardware architecture diagram [2]
The official hardware diagram shows that the CPU supports external communication via Wi-Fi/BLE, 4G, a debugging RJ45, and camera. The RJ45 connects to an external Exchange Board, which in turn connects to expansion devices such as the Orin NX and navigation LiDAR. The robot is also equipped with an onboard camera that transmits video to the Unitree app. Further analysis confirmed that RTSP is used for video transmission.
Each interface maps to the following services:
HW Interface | Physical Interface | Connected Services |
Wi-Fi / BLE | wlan0 (STA), wlan1 (AP), hci0 (BLE) | BLE onboarding (btgatt-server), RTSP/WebRTC signaling exposed on the shared network |
4G | wwan0 | Cloud path: OTA (MQTT/CDN), WebRTC remote connection |
Debugging Interface RJ45 | eth0
(internal wired network) | Expansion device connection, RTSP multicast output interface (multicast-iface=eth0) |
Video | /dev/video0 | Source and communication services for RTSP/WebRTC video (videohub, go2_videohub_rtsp_server) |
[Table 4] Hardware interfaces and service mapping
The first connection between the robot and the app begins over BLE. When a user adds the robot in the app, it discovers the robot via BLE and sends the connection mode (AP mode / Wi-Fi mode) and WiFi credentials over BLE.
Robot addition screen in the app
Touch Add Robot Dog causes the app to scan for nearby robots over BLE.
Connection mode selection screen
Once the robot is discovered, the user selects a WiFi connection mode. Of the two options, AP Mode has the phone connect directly to the robotโs hotspot (wlan1, 192.168.12.1), while Wi-Fi Mode has both robot and phone connect to the same router (wlan0).
Depending on the selected mode, the robot reconfigures either hostapd or wpa_supplicant.
Robot connected to a router in Wi-Fi Mode
In Wi-Fi Mode, the robot receives an address from the routerโs DHCP, and the app discovers the robotโs SN/IP via UDP multicast on port 10131. Regardless of mode, once a WiFi connection is established, the following services are exposed on the shared network (from ss -tulnp, key entries):
Port | Process | Purpose |
TCP 22 | sshd | Remote shell |
TCP 80 / 9991 / 10001 | xfkTon (webrtc_signal_server) | WebRTC signaling |
TCP 8551 | go2_videohub_rtsp_server | RTSP video stream |
UDP 7400 / 7401 | DDS participant processes | DDS discovery |
UDP 5353 | avahi-daemon | mDNS |
UDP 67 / 68 | udhcpd / dhcpcd | DHCP server (AP mode) / client |
UDP 10131 | multicast_responder.py | Robot SN/IP discovery response |
[Table 5] Network listening ports and processes
Among these, SSH, mDNS, and DHCP are Ubuntu services. The external entry points for Unitreeโs robot-specific services narrow down to the following four components:
Component | External Channel | Responsible Service | Selection Rationale |
BLE | Bluetooth Low Energy | upper_bluetooth (btgatt-server) | Onboarding path open before any WiFi connection is established; receives connection mode and WiFi credentials |
RTSP | WiFi/LAN (TCP 8551, UDP multicast) | go2_videohub_rtsp_server, video_hub | Delivers camera video to clients on the same network |
WebRTC / DDS | WiFi/LAN signaling ๏ธ internal DDS, Internet (AWS KVS) | webrtc_signal_server (xfkTon), webrtc_bridge | Real-time control/video path for the app; a bridge that feeds external messages into the internal DDS bus |
OTA | Cloud (MQTT) โ CDN โ robot | ota_box (ota_boxed) | The path through which external packages are installed on the robot; uses mscli to replace services |
[Table 6] Selected external components for analysis
Mapping the connection flow from Go2โs external interfaces down to internal components and services, we get the following diagram:
External components and internal service connections
In this series entry, we analyze the RTSP component first. BLE, WebRTC/DDS, and OTA will be covered in Series #4.
4. RTSP Component Analysis
4.1. Basic Components
root@Unitree:~# grep -rl 'video_hub\|videohub\|rtsp' /unitree/etc/master_service/
/unitree/etc/master_service/service/video_hub
/unitree/etc/master_service/service/go2_videohub_rtsp_server
Bash
๋ณต์ฌ
Images and video captured by the robotโs onboard camera are delivered to the app through dedicated protocols and internal processes. RTSP is the primary protocol used for this delivery, and the RTSP related processes are also launched by Unitreeโs custom master_service orchestrator via JSON configurations under /unitree/etc/master_service/service/.
Section 4 starts with the terminology around the RTSP component, then examines how the key services and server behave, analyzes how the request handling logic is structured, and finally documents the DFD showing how data actually flows through the RTSP component.
4.1.1. RTSP Terms
โข
RTSP / RTP / SDP
โฆ
RTSP: A text-based protocol for controlling streaming playback.
โช
Syntactically similar to HTTP; opens a session via the sequence OPTIONS โ DESCRIBE โ SETUP โ PLAY.
โช
Does not carry the actual video payload.
โฆ
RTP: The protocol that delivers actual video data, typically over UDP.
โฆ
SDP: A document describing codec and encoding parameters, delivered in the body of a DESCRIBE response.
โข
GStreamer / gst-rtsp-server
โฆ
GStreamer: A pipeline framework that processes media by chaining source/filter/sink elements with !.
โฆ
gst-rtsp-server: A library (libgstrtspserver-1.0) that mounts a pipeline onto a path (e.g., /video) and exposes it as a standards-compliant RTSP server.
4.1.2. Video Streaming Architecture
Go2โs video streaming is not a single RTSP server but a three-layer architecture with distinct roles:
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
Plain Text
๋ณต์ฌ
Layer | Process (PID) | Role |
Layer 1: Capture/Encoding | videohub high (1609) | Encodes USB camera video to H.264 and publishes via UDP multicast and DDS topics |
Layer 2: RTSP Relay | go2_videohub_rtsp_server (1257) | Receives multicast RTP and re-publishes it as rtsp://<robot_ip>:8551/video |
Layer 3: Remote Transmission | unitreeWebRTCClientMaster (1705), xfkTon (1739), multicast_responder.py (1714) | Transmits DDS video remotely to AWS KVS over WebRTC (detailed in Series #4) |
[Table 7] Video streaming process structure
root@Unitree:~# cat /unitree/etc/master_service/service/go2_videohub_rtsp_server
{
"Start": {
"Cmd": "/unitree/sbin/start-stop-daemon --start --background --make-pidfile\
--pidfile=/unitree/var/run/go2_videohub_rtsp_server.pid\
--exec /unitree/module/video_hub/go2_videohub_rtsp_server",
"ExpectCode": [0]
},
...
}
root@Unitree:~# cat /unitree/etc/master_service/service/video_hub
{
"Start": {
"Cmd": "export CYCLONEDDS_URI=/unitree/etc/cyclonedds.xml;\
/unitree/sbin/start-stop-daemon --start --background --make-pidfile\
--pidfile=/unitree/var/run/videohub.pid\
--exec /unitree/module/video_hub/videohub high",
"ExpectCode": [0]
},
...
}
Bash
๋ณต์ฌ
Layer 1 and Layer 2 differ from the very start in how theyโre launched. Looking at master_service service definitions, the RTSP server starts via start-stop-daemon, while the capture layer (Layer 1) video_hub is launched with CYCLONEDDS_URI injected as an environment variable to apply DDS security configuration.
root@Unitree:~# ldd /unitree/module/video_hub/go2_videohub_rtsp_server
libgstrtspserver-1.0.so.0 => /unitree/lib/libgstrtspserver-1.0.so.0
libgstreamer-1.0.so.0 => /lib/aarch64-linux-gnu/libgstreamer-1.0.so.0
libgstrtsp-1.0.so.0 => /lib/aarch64-linux-gnu/libgstrtsp-1.0.so.0
libgstrtp-1.0.so.0 => /lib/aarch64-linux-gnu/libgstrtp-1.0.so.0
...
root@Unitree:~# ldd /unitree/module/video_hub/videohub
libddsc.so.0 => /unitree/lib/libddsc.so.0
libgstreamer-1.0.so.0 => /lib/aarch64-linux-gnu/libgstreamer-1.0.so.0
libssl.so.1.1 => /lib/aarch64-linux-gnu/libssl.so.1.1
libcrypto.so.1.1 => /lib/aarch64-linux-gnu/libcrypto.so.1.1
...
Bash
๋ณต์ฌ
The difference also shows up in dynamic library dependencies. go2_videohub_rtsp_server links only against GStreamer RTSP libraries (libgstrtspserver-1.0, libgstrtsp-1.0, libgstrtp-1.0), whereas videohub links against GStreamer (libgstreamer-1.0) together with CycloneDDS (libddsc.so.0) and the DDS Security libraries libssl.so.1.1 / libcrypto.so.1.1.
Both processes use GStreamer to handle video, but only videohub operates under DDS security configuration.
4.2. RTSP-related Service Operations
4.2.1. videohub Operation
root@Unitree:~# strings /unitree/module/video_hub/videohub \
| grep -iE 'rt/|video|pipeline|230\.|1720|720|360|h264|v4l2'
stream720p
stream360p
/home/unitree/sjy/videohub/videohub.c
rt/frontvideostream
rt/api/videohub/response
rt/api/videohub/request
rt/videohub/inner
v4l2src device=/dev/video0 ! image/jpeg, height=1080, width=1920, framerate=15/1 ! \
tee name=img1080 ! queue ! jpegparse ! mppjpegdec format=23 ! tee name=vid ! \
queue ! mpph264enc bps=3000000 header-mode=1 height=720 width=1280 ! tee name=enc ! \
queue ! appsink name=stream720p \
vid. ! queue ! mpph264enc bps=800000 header-mode=1 height=360 width=640 ! \
appsink name=stream360p \
img1080. ! queue ! appsink name=image1080p
enc. ! queue ! rtph264pay ! udpsink host=230.1.1.1 port=1720 multicast-iface=eth0 sync=false
/dev/video0
unitree_go::msg::dds_::Go2FrontVideoData_
..
Bash
๋ณต์ฌ
We analyzed the Layer 1 videohub binary to understand its internal behavior. Running strings on the binary reveals 4 topic format strings beginning with rt/, along with a hardcoded capture and encoding pipeline. The pipeline code below has been reformatted to clarify the branching structure. The actual string ordering in the binary differs from the representation shown below.
v4l2src device=/dev/video0
!image/jpeg, height=1080, width=1920, framerate=15/1
!tee name=img1080
โโ img1080. ! queue ! appsink name=image1080p โ 1080p JPEG (raw)
โโ ! queue ! jpegparse ! mppjpegdec format=23
!tee name=vid
โโ ! queue ! mpph264enc bps=3000000 header-mode=1 height=720 width=1280
โ ! tee name=enc
โ โโ ! queue ! appsink name=stream720p โ 720p H264 (for DDS)
โ โโ ! queue ! rtph264pay
โ ! udpsink host=230.1.1.1 port=1720 multicast-iface=eth0 โ multicast
โโ ! queue ! mpph264enc bps=800000 header-mode=1 height=360 width=640
!appsink name=stream360p โ 360p H264 (low quality)
Bash
๋ณต์ฌ
The camera (/dev/video0) produces 1080p JPEG at 15fps, immediately split into two branches. One branch goes straight to the 1080p JPEG output (image1080p) without decoding, while the other decodes (mppjpegdec) and then H.264 encodes at 720p and 360p respectively. The 720p output branches once more: one goes to an appsink (stream720p) and the other to multicast output (udpsink). Note that the multicast output (udpsink) is not always attached.
Resolution | Encoder | Bitrate | Output |
720p | mpph264enc | 3 Mbps | appsink stream720p โ DDS publish, multicast 230.1.1.1:1720 (only when 4 โค ver โค 6) |
360p | mpph264enc | 800 kbps | appsink stream360p โ DDS publish (low quality) |
1080p | JPEG raw | โ | appsink image1080p โ internal JPEG buffer (for snapshot API api_id=1001 responses) |
[Table 8] videohub encoding branches
Decompiling the main function (0x2440) of videohub yields:
int main(int argc, const char **argv, const char **envp)
{
assignToThisCore(2); // CPU core 2 affinity
bool multicast = isExternalApiRequired(); // firmware-version based
check_video_device(); // /dev/video0 watchdog
check_camera_config(); // /unitree/etc/rgb_camera/
load_camera_config(); // v4l2 backlight_compensation
// DDS initialization
participant = dds_create_participant(DDS_DOMAIN_DEFAULT, ...);
dds_create_topic(..., "rt/frontvideostream", Go2FrontVideoData);
dds_create_topic(..., "rt/api/videohub/response", Response);
dds_create_topic(..., "rt/api/videohub/request", Request);
dds_create_topic(..., "rt/videohub/inner", String);
writer_stream = dds_create_writer(..., frontvideostream);
writer_resp = dds_create_writer(..., response);
reader_req = dds_create_reader(..., request, qos, videohub_request_data_available_handler);
reader_inner = dds_create_reader(..., inner, qos, videohub_inner_data_available_handler);
// GStreamer
gst_init(&argc, &argv);
pipeline_str = malloc(0x4B0);
memcpy(pipeline_str, BASE_PIPELINE, 0x1B2); // 434 bytes
if (multicast)
strcpy(pipeline_str + 433,
" enc. ! queue ! rtph264pay ! udpsink host=230.1.1.1 port=1720 "
"multicast-iface=eth0 sync=false");
pipeline = gst_parse_launch(pipeline_str);
// appsink callbacks
stream720p โ encoded_video_sample (โ DDS)
stream360p โ encoded_video_sample (โ DDS)
image1080p โ image_sample (โ internal JPEG buffer)
gst_element_set_state(pipeline, GST_STATE_PLAYING);
while (1) { // bus poll
bus_msg = gst_bus_timed_pop_filtered(bus, 1sec, GST_MESSAGE_ANY);
if (bus_msg == NULL) {
if (sample720p_count <= 4) break; // < 4fps at 720p โ exit
reset_counters();
}
...
}
gst_element_set_state(pipeline, GST_STATE_NULL);
}
C
๋ณต์ฌ
Looking at the key logic, during DDS initialization, two writers are created (video stream topic, API response topic) and two readers (API request topic, internal topic).
GStreamer initialization follows: the base pipeline string (434 bytes) is copied, and only when multicast is true is the udpsink segment appended via strcpy.
After building the pipeline, appsink callbacks are wired up. 720p and 360p H.264 frames go through encoded_video_sample to be published on DDS, while 1080p JPEG frames go through image_sample to be stored in an internal buffer. The detailed path from DDS-published video stream to WebRTC output will be covered in Series #4.
After the GStreamer state is set to PLAYING, the main loop polls the bus every second, exiting if the 720p frame rate drops to 4 frames or fewer per second.
The isExternalApiRequired() function (0x6d88) that determines multicast:
bool isExternalApiRequired()
{
fp = fopen("/unitree/robot/basic/ver", "r");
fgets(buf, 100, fp);
ver_val = strtol(buf + 6, NULL, 10);
return (ver_val - 4) <= 2; // unsigned comparison โ true only when 4 โค ver_val โค 6
}
C
๋ณต์ฌ
It reads the integer starting from the 7th character of /unitree/robot/basic/ver and returns true only when that value is between 4 and 6. The actual ver value observed on the Go2 was:
root@Unitree:~# cat /unitree/robot/basic/ver
11421302
Plain Text
๋ณต์ฌ
Reading from the 7th character yields 02, which is interpreted as 2. Since this value is outside the valid range, multicast output is not generated by default.
videohub main() flow
This diagram summarizes videohubโs fundamental behavior based on the analysis above. In short: videohub verifies the camera device and configuration, initializes DDS and GStreamer, and runs the capture-and-encode pipeline. Encoded video is published to DDS via appsink callbacks, while the main loop monitors bus messages and 720p frame counts to watch over pipeline health.
4.2.2. RTSP Server Operation
int main(int argc, const char **argv, const char **envp)
{
..
loop = g_main_loop_new(NULL, FALSE);
server = gst_rtsp_server_new();
g_object_set(server, "service", "8551", NULL); // port configuration
factory = gst_rtsp_media_factory_new();
s = malloc(0x4B0);
sprintf(s, "udpsrc address=230.1.1.1 port=1720 multicast-iface=eth0 "
"! queue ! application/x-rtp, media=video, encoding-name=H264 "
"! rtph264depay ! rtph264pay name=pay0 pt=96");
gst_rtsp_media_factory_set_launch(factory, s); // pipeline (hardcoded)
gst_rtsp_media_factory_set_shared(factory, TRUE); // shared across clients
mounts = gst_rtsp_server_get_mount_points(server);
gst_rtsp_mount_points_add_factory(mounts, "/video", factory);
gst_rtsp_server_attach(server, NULL); // 2nd arg NULL โ bind to 0.0.0.0
g_print("stream ready at rtsp://127.0.0.1:%s%s\n", port, subdir);
g_main_loop_run(loop);
}
C
๋ณต์ฌ
The Layer 2 RTSP server (go2_videohub_rtsp_server) does not implement RTSP parsing or socket handling itself. Its import list contains no socket functions like accept/recv/read, and the main function (0x108c) simply calls GStreamer APIs and hands control to the GLib main loop.
A closer examination of the core logic reveals that the pipeline receives multicast RTP streams via udpsrc and performs H.264 depacketization and repacketization (rtph264depay ! rtph264pay) without re-encoding.
The set_shared(TRUE) setting allows multiple clients to share a single pipeline. Additionally, the /video mount point is hardcoded in the global variable subdir and cannot be modified through the CLI.
Because master_service launches go2_videohub_rtsp_server without arguments, it always starts with port=8551 and mount=/video.
The second argument of gst_rtsp_server_attach() specifies the event loop context (GMainContext), rather than the listen address. A NULL value indicates that the default context is used. The listen address is configured through gst_rtsp_server_set_address(), which is not called in this sequence. Therefore, the server uses the default listen address.
The string stream ready at rtsp://127.0.0.1:8551/video found in the binary is used only for logging and does not represent the actual listen address. Instead, the RTSP server binds to all network interfaces (0.0.0.0).
root@Unitree:~# ss -tlnup | grep -E '8551|1720|1013'
udp UNCONN 0 0 0.0.0.0:10131 0.0.0.0:* users:(("python3",pid=1714,fd=5))
udp UNCONN 0 0 0.0.0.0:10131 0.0.0.0:* users:(("python3",pid=1714,fd=4))
tcp LISTEN 0 5 0.0.0.0:8551 0.0.0.0:* users:(("go2_videohub_rt",pid=1257,fd=4))
Plain Text
๋ณต์ฌ
UDP 1720 is multicast transmit-only from videohub, so it doesnโt appear in the listen list.
RTSP server main() flow
In summary, as shown in the figure above, the binary creates an RTSP server object, registers a hardcoded pipeline at /video, then hands control to the main loop. All subsequent client connection acceptance, request parsing, and method dispatching are delegated to the GStreamer RTSP library.
4.2.3. Session Flow
Combining the individual analyses of videohub and go2_videohub_rtsp_server, we can trace a complete session from camera to client.
The RTSP server opens TCP 8551 at startup and waits for client connections, but the actual video to deliver only arrives via UDP once videohubโs multicast output is active.
RTSP session flow
root@Unitree:~# curl -v rtsp://127.0.0.1:8551/video
* Connected to 127.0.0.1 (127.0.0.1) port 8551 (#0)
> OPTIONS * RTSP/1.0
> CSeq: 1
> User-Agent: curl/7.68.0
>
(waiting for response โ hang โ ^C)
Plain Text
๋ณต์ฌ
Testing confirmed that when multicast is disabled by default, the TCP connection is successfully established, but the RTSP OPTIONS request receives no response. This occurs because the media pipeline cannot transition to a ready state without incoming RTP packets from the upstream multicast source.
As a result, the RTSP server remains active regardless of the multicast state. However, successful video streaming requires both the version condition in videohub and the eth0 interface condition to be satisfied simultaneously.
4.3. Data Flow Analysis
4.3.1. External Entry Points
The RTSP serverโs entry points are not limited to TCP 8551. Because the RTSP server receives RTP from 230.1.1.1:1720 via udpsrc and de-packetizes it with rtph264depay, RTP packets arriving on the multicast group also constitute input processed by the RTSP server.
Entry Point | Processing Layer | Responsible Library |
TCP 8551
(RTSP control) | Request parser / method dispatch / transport negotiation | libgstrtsp, libgstrtspserver |
UDP 230.1.1.1:1720 (RTP media) | H.264 de-packetization | libgstrtp.so (gstreamer1.0-plugins-good) |
[Table 9] RTSP component entry points
4.3.2. DFD
Based on the analysis so far, the video data flow can be divided into two paths originating from the robot's onboard camera. The LAN path transmits video through multicast RTP and RTSP, while the remote path uses DDS and WebRTC. The following Data Flow Diagram (DFD) illustrates the RTSP component and its relationship with both transmission paths.
RTSP component DFD
Entry Point | Protocol | Binding | Process | Purpose |
8551 | TCP | 0.0.0.0 | go2_videohub_rtsp_server | RTSP control |
230.1.1.1:1720 | UDP multicast | Transmit-only (eth0) | videohub โ RTSP server | H.264 RTP media |
10131 | UDP | 0.0.0.0 | multicast_responder.py | Robot SN/IP discovery response |
[Table 10] RTSP entry points
The structure the DFD reveals has the following implications:
1.
The RTSP path spans two networks: RTP transmitted by videohub enters the RTSP server via the multicast group on the wired internal network (eth0), while the RTSP server delivers video to clients on the WiFi LAN.
2.
The protection mechanisms differ by path. videohub receives the CycloneDDS Security configuration (cyclonedds.xml) and publishes video to DDS with that security context. On the WebRTC side, unitreeWebRTCClientMaster uses SigV4 signing for KVS signaling. By contrast, the RTSP path is handled at the GStreamer layer and is decoupled from the DDS security context.
5. Conclusion
This entry answered the question left open by Series #2: โWho starts the robotโs services, in what order, and which of those services are exposed externally?โ Hereโs a summary of the key findings:
โข
init : The Go2 initialization process consists of three layers: systemd, the SysV compatibility layer, and master_service. During the multi-user boot phase, system services such as SSH, Bluetooth, and DHCP are started alongside Unitree-specific services, including master_service and ota_box.
โข
master_service: Reads configuration and policy files to manage 32 service definitions; launches services via shell commands after checking policies in plan โ forbid โ manual/once โ conflict order. Handles runtime control via local RPC (mscli) and periodic health monitoring.
โข
Attack surface: From this service map and network exposure data, four components touching the outside world were selected: BLE, RTSP, WebRTC/DDS, and OTA.
โข
RTSP: The RTSP service acts as a lightweight wrapper around gst-rtsp-server, delegating connection acceptance and request handling for external RTSP clients to the library. It delivers multicast video streams from videohub to clients using a hardcoded pipeline and mount path (/video). However, multicast output is enabled only when the value of /unitree/robot/basic/ver satisfies the specified version condition.
The RTSP service uses two network endpoints: TCP port 8551 for RTSP client connections and UDP port 1720 for receiving multicast RTP streams. Video is transmitted from the internal wired network to Wi-Fi LAN clients through the RTSP server.
In the next part (#4), we will analyze how the WebRTC and DDS bridge enables remote video transmission and forwards external messages to the internal DDS bus. We will also examine how BLE, which is accessible before a Wi-Fi connection is established, receives connection credentials and how these credentials are structured. Finally, we will provide a comprehensive overview of the data flow from external interfaces to the robot's internal components.
6. References
โข
[1] V. Mayoral-Vilches, โThe Cybersecurity of a Humanoid Robotโ, arXiv:2509.14096, 2025. https://arxiv.org/abs/2509.14096
โข
[2] Unitree Robotics, โAbout Go2 โ Hardware Architectureโ, Go2 Developer Documentation. https://support.unitree.com/home/en/developer/about_Go2#heading-3
















๏ธ internal DDS, Internet (AWS KVS)



