A CLI tool for sending and receiving Nebulous.io chat messages through Frida.
The Frida agent first intercepts a real outgoing UDP chat packet from the game and stores it as a template. After that, the CLI can build new chat packets from the same client network context and send them through sendto or send.
The same agent also hooks recvfrom and recv so incoming chat packets can be parsed and printed in the CLI in real time.
- Python 3.9+.
- The Python
fridapackage. adbavailable inPATH.- An Android device or Android emulator visible through
adb. frida-serveralready copied to the Android target, usually at/data/local/tmp/frida-server.- Root access through
adb shell su -c ...to startfrida-server. - Nebulous.io running on the target device.
The specific emulator does not matter. Any emulator or physical Android device should work as long as adb devices can see it, the frida-server version is compatible with the installed Python frida package, and the game process can be attached to.
Standard USB/ADB flow, when the device is already visible to Frida:
python chat_cli.pyTCP/ADB flow, when you need to connect to the target by ip:port first:
python chat_cli.py --adb 127.0.0.1:62001Custom frida-server path on the Android target:
python chat_cli.py --adb 127.0.0.1:62001 --frida-server-path /data/local/tmp/frida-serverStart without chat logging:
python chat_cli.py --no-logPackage-style execution is also available:
python -m nebulous_chat_cli --adb 127.0.0.1:62001Inside the CLI, use /help to list available commands.
When started with --adb HOST:PORT, the program does the following:
- Checks that
adbis available. - Runs
adb connect HOST:PORT. - Runs
adb -s HOST:PORT wait-for-device. - Checks
frida-serverwithadb -s HOST:PORT shell su -c "pidof frida-server". - If the server is not running, starts it through
su -c:
adb -s HOST:PORT shell su -c "nohup /data/local/tmp/frida-server >/dev/null 2>&1 &"- Waits for a Frida device with the id
HOST:PORT. - Attaches to the
Nebulous.ioprocess.
Important: the CLI does not download or upload frida-server automatically. The binary must already exist on the device or emulator. If you use Android Wireless Debugging with pairing, run adb pair beforehand; the CLI only automates adb connect HOST:PORT.
- Start an Android emulator or connect a device.
- Make sure the target is visible through ADB:
adb devices- Make sure
frida-serverexists on the Android target, for example:
adb -s 127.0.0.1:62001 shell su -c "ls -l /data/local/tmp/frida-server"- Open Nebulous.io and join a room.
- Start the CLI:
python chat_cli.py --adb 127.0.0.1:62001- In the game, manually send any message to the public chat.
- Wait until the console prints a log like this:
[CHAT TEMPLATE] source=sendto fd=... nick="..." msg="hello"
- You can now type messages directly in the CLI. Public chat is the default outgoing chat:
> test
> hello
> yoo how are you
Incoming chat messages are printed automatically while the CLI is running:
[CHAT] [123456] Rush: hello
[CLAN] [123456] Rush: clan hello
[CHAT] [987654] OtherPlayer: hi
[PM] [123456] Rush: private hello
For clan/private sending, manually send one message from that in-game chat first. The agent captures one template per chat kind and then only rewrites the message field.
/status show the template/fd/nick/rate/max/recv state
/max 128 set maxLenBytes
/rate 1000 set the rate limit in milliseconds
/send game set default outgoing chat: game, clan, or private
/game hello send one message to public chat
/clan hello send one message to clan chat
/pm 123 hello send a private message to account/player id 123
/show all show incoming: all, off, game, clan, or private
/recv on enable incoming chat display
/recv off disable incoming chat display
/log status show chat log state and current file
/log on enable chat logging
/log off disable chat logging
/log list show recent chat log files
/log show print the last 200 lines from the current log
/log show 1 print the last 200 lines from log #1 in /log list
/clearrecv clear incoming chat counters and dedupe state
/clear clear the captured template
/help show help
/exit exit
/quit exit
Plain text without a leading / is sent to the current default outgoing chat through Frida RPC.
Use /send clan to make plain text go to clan chat, and /send game to switch back.
Incoming chat display is enabled by default. Use /show clan to display only clan messages while still logging all incoming chat kinds. Use /show off to keep the terminal quiet while logging still remains active. Use /recv off only if you want the agent to stop forwarding incoming chat events entirely.
Incoming and successfully sent chat messages are logged by default. Each CLI session writes separate timestamped files per chat kind in logs/:
logs/chat_2026-05-04_22-10-30_game.log
logs/chat_2026-05-04_22-10-30_clan.log
logs/chat_2026-05-04_22-10-30_private.log
Log lines are plain text so they can be opened, searched, or scrolled later:
[2026-05-04 22:10:31] RECV CHAT [123456] Rush: hello
[2026-05-04 22:10:35] SEND CLAN [self] MyNick: hi there {via=send bytes=8 packetLen=42}
Use /log off and /log on while the CLI is running, or start with --no-log to disable logging immediately. Use /log list to find logs by date/time and /log show 1 to print a selected log back into the console.
Incoming chat output supports optional ANSI colors. By default, colors are enabled only for interactive terminals and disabled when NO_COLOR is set.
Create chat_colors.json next to chat_cli.py to customize chat colors:
{
"enabled": "auto",
"chat": {
"prefix": "yellow",
"id": "green",
"nick": "green",
"message": "default"
},
"kinds": {
"clan": {
"prefix": "cyan",
"id": "bright_cyan",
"nick": "bright_cyan",
"message": "default"
},
"private": {
"prefix": "magenta",
"id": "bright_magenta",
"nick": "bright_magenta",
"message": "default"
}
}
}enabled can be auto, always, or never. Supported colors are default, black, red, green, yellow, blue, magenta, cyan, white, their bright_* variants, and none.
The agent hooks recvfrom and recv in addition to sendto and send. Because receive buffers are filled after the native function returns, incoming packets are parsed in the hook onLeave handler.
Incoming packets with opcode 0x89 are parsed using the same chat packet structure:
u8 opcode (0x89)
u32be public_id
MUTF8 alias
MUTF8 message
i32be account_id / -1
... tail
The account_id field after the message is shown as the player identifier, for example:
[CHAT] [123456] Rush: hello
The earlier public_id field is still kept in the internal event payload as publicId / publicIdHex for debugging.
To avoid false positives from unrelated binary UDP packets, the receiver filters parsed candidates before printing them. Nicknames and messages must look like clean UTF-8 chat text, and obviously binary strings are ignored.
chat_cli.py- compatibility entrypoint for running the CLI.nebulous_chat_cli/- Python package with the CLI, commands, ADB bootstrap, and Frida lifecycle.agent/src/- modular Frida agent source files.chat_injector_agent.js- bundled agent loaded by the CLI.tests/- Python and JS tests for pure logic.
The agent source files are in agent/src. The root-level chat_injector_agent.js is committed already bundled so that python chat_cli.py works without requiring a JS build step.
If Node.js/npm is installed, rebuild the agent with:
npm install
npm run build:agentThe Frida agent hooks these system functions:
send
sendto
recv
recvfrom
When the game sends or receives a UDP packet, the agent checks:
packet[0] == 0x89
This is the chat packet opcode.
After detection, the packet is parsed as:
u8 opcode (0x89)
u32be public_id
MUTF8 alias
MUTF8 message
i32be account_id / -1
bool unknown
i64be message_id
vararr alias_colors
bool show_broadcast_bubble
u8 alias_font
u32be client_id
bool false
bool false
Strings are stored as:
u16 (big-endian) + UTF-8 bytes
For outgoing messages, the new packet is sent through the same socket:
sendto(fd, ...) // when sockaddr is available
or
send(fd, ...) // fallback
The packet tail is left unchanged:
... ff ff ff ff 00 00 ...
It contains service data: flags, id, client state, and other fields.
