HexBot supports passive DCC CHAT for remote administration. Users with sufficient flags connect directly from their IRC client, enter a password, and share a live console with other connected admins.
DCC CHAT uses per-user passwords, the same model Eggdrop has used for 30 years. The hostmask in the DCC handshake tells the bot which handle is claiming to connect; the password proves it. This closes the known spoofing gap on networks where a single vhost persists across nick changes (e.g. Rizon), and it works uniformly on services-free networks like EFNet.
Key properties:
- Passwords are hashed with scrypt before storage — the bot never keeps plaintext.
- Hostmask patterns continue to gate in-channel flag checks (
.op,.say, pluginpubbinds) — prompting on every channel message is not a workable UX. - DCC CHAT has its own socket-local prompt channel, so it can always ask for a password without looking clumsy.
- Bot must have a public IPv4 address — passive DCC means users connect to the bot, not the other way around. A VPS or dedicated server works; a home connection behind NAT requires port forwarding.
- Required flags — configurable, default
+m(master). Users without the required flags are rejected before the password prompt. - A per-user password — set by an admin via
.chpassbefore the user can connect. - Tested clients: irssi, WeeChat, HexChat, mIRC
A DCC session requires three things: a matching hostmask, the required flags, and a password set by an admin. Step through these once.
If you're running in a container without REPL access, seed the owner's password from an env var instead. Add password_env to the owner block in config/bot.json and set the variable in config/bot.env:
"owner": {
"handle": "admin",
"hostmask": "*!yourident@your.host.here",
"password_env": "HEX_OWNER_PASSWORD"
}HEX_OWNER_PASSWORD=choose-a-strong-password
On first boot the bot reads the env var, hashes it with scrypt, and stores it. Subsequent boots leave the stored hash alone, so rotations via .chpass persist across restarts (same lifecycle as MYSQL_ROOT_PASSWORD). If DCC is enabled and the owner has no password set — either from the env var or a previous .chpass — the bot logs a loud warning at startup so operators don't silently hit a DCC rejection later.
To force a re-seed from the env var (for example, after losing the password), clear the owner's password_hash row in the database and restart the bot.
Join a channel the bot is in and run:
/whois yournick
Look for the nick!ident@host line, e.g. admin!myident@my.vps.com.
Bot commands (.adduser, .flags, etc.) are only available from the REPL or a DCC session — not via IRC private message. Start the bot with --repl and add yourself:
hexbot> .adduser yourhandle *!myident@my.vps.com m
Replace *!myident@my.vps.com with your actual hostmask pattern. Use * as a wildcard for parts that may vary (e.g., *!*@my.static.ip). For the owner, use flag n instead of m.
From the REPL:
hexbot> .chpass yourhandle <newpassword>
Passwords must be at least 8 characters. The bot confirms with chpass: password for "yourhandle" has been updated. You can rotate later from inside a DCC session with .chpass <newpassword> (self-form) or, as an owner, .chpass <handle> <newpassword>.
.chpass is rejected if issued over IRC PRIVMSG — passwords must never travel on the wire in the clear. The only valid transports are the REPL and an existing DCC session.
hexbot> .flags yourhandle
The bot's ip field must be the address your server is reachable on from the internet:
curl -4 ifconfig.me
# or
ip -4 addr show eth0 | grep 'inet ' | awk '{print $2}' | cut -d/ -f1If running behind a load balancer or inside a private network, use the public-facing IP, not the private one.
Open the port range you configure in bot.json so incoming connections can reach the bot:
# ufw (Ubuntu/Debian)
sudo ufw allow 49152:49171/tcp
# firewalld (RHEL/Fedora)
sudo firewall-cmd --permanent --add-port=49152-49171/tcp
sudo firewall-cmd --reload
# raw iptables
sudo iptables -A INPUT -p tcp --dport 49152:49171 -j ACCEPT"dcc": {
"enabled": true,
"ip": "203.0.113.42",
"port_range": [49152, 49171],
"require_flags": "m",
"max_sessions": 5,
"idle_timeout_ms": 300000
}Replace 203.0.113.42 with the bot's public IPv4 address. This is what gets sent to the user's client — it must be reachable from outside.
| Key | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | false |
Enable DCC CHAT |
ip |
string | — | Bot's public IPv4 address |
port_range |
[number, number] | — | Inclusive range for passive DCC listeners |
require_flags |
string | "m" |
Flags needed to connect (m = master, n = owner) |
max_sessions |
number | 5 |
Maximum concurrent DCC sessions |
idle_timeout_ms |
number | 300000 |
Idle disconnect timeout in ms (default 5 minutes) |
After the DCC CHAT handshake completes, the bot sends a single Enter your password: prompt on the socket. Type your password and press enter. On success the banner and console are shown; on failure the bot sends DCC CHAT: bad password. and closes the connection. Repeated failures from the same hostmask escalate into a temporary lockout (exponential backoff, matching the bot-link auth policy).
Sessions that have never had a password set are rejected with a one-line notice pointing at .chpass. Ask an admin to run .chpass <handle> <newpassword> from the REPL and try again.
/dcc chat hexbot
/dcc chat hexbot
Go to Server → DCC Chat → Open DCC Chat and enter the bot's nick, or type in the server window:
/dcc chat hexbot
/dcc chat hexbot
On connect you will see a banner like:
(HexBot ASCII art logo)
Hi yourhandle, I am hexbot. The local time is 12:00:00 AM (UTC) on April 12th, 2026.
Logged in as: yourhandle (yournick!~ident@your.host)
Your flags: +nm
Console: 1 other(s) here: adminhandle
Use .help for basic help.
Use .help <command> for help on a specific command.
Use .who to see who is currently on the console.
Use .console to view or change your log-subscription flags.
Commands start with '.' (like '.quit' or '.help')
Everything else goes out to the console.
Any line beginning with . is treated as a bot command — the same commands available in the REPL:
.help
.plugins
.reload chanmod
.say #channel hello
.flags yourhandle
Your permission flags are enforced — you can only run commands you have flags for.
These work only inside a DCC session:
| Command | Description |
|---|---|
.who |
List connected console users and uptime |
.console |
Show or modify this session's console flags (see below) |
.console +d |
Add flags (here: debug / dispatcher) to your session |
.console -m |
Remove flags from your session |
.console <h> +o |
Owner-only: set stored flags for another handle |
.quit |
Disconnect from the console |
.exit |
Alias for .quit |
The DCC console is a filtered live view of the bot's log. Every log
line the bot writes to stdout is also offered to every DCC session. Each
session decides which categories it wants to see via its .console
flags (+mojkpbsdw).
| Letter | Category | Sources that map to it |
|---|---|---|
m |
bot messages / services / memo | [bot], [dcc], [services], [memo], [database] |
o |
operator actions / mode changes | [plugin:chanmod], [plugin:chanset], [irc-commands], [ban-store] |
k |
kicks / bans / channel protection | [channel-protection], [plugin:chanmod#k] |
j |
joins / parts / signoffs / nicks | [channel-state], [plugin:greeter], [plugin:seen] |
p |
public chat / command dispatch | [command-handler], [plugin-loader] |
b |
botnet / botlink | [botlink:hub], [botlink:leaf], [dcc-relay] |
s |
server / connection | [connection], [reconnect], [irc-bridge], [sts] |
d |
debug / dispatcher | [dispatcher] and every debug-level line |
w |
warnings / errors | any warn / error line regardless of source |
+mojw on first connect — bot messages, operator actions, join/part
activity, and warnings. Dispatcher debug chatter and public command
routing are off by default; turn them on only while debugging. The
default is a compile-time constant (DEFAULT_CONSOLE_FLAGS in
src/core/dcc-console-flags.ts); there is no config knob for it.
.console → Console flags: +mojw
.console +p → subscribe to command-handler dispatch
.console -j → stop seeing joins/parts
.console +mojkdwpbs → everything (firehose)
.console +all → sugar: add every known flag
.console -all +mw → reset to messages + warnings only
.console alice +o → set default flags for another handle (owner only)
Flags are persisted per handle in the kv store — the next time that
handle connects, their last choice is restored. Unknown letters are
rejected with Unknown console flag: <letter>.
- "I want to watch threats only" →
.console -all +kw - "I'm debugging a plugin" →
.console +dp - "I only want to hear about errors" →
.console -all +w - "Firehose everything" →
.console +all
Note that the private-notice mirror (which forwards services chatter from NickServ, ChanServ, LimitServ, etc. into the DCC console) is a separate path from the log sink. NickServ ACC/STATUS replies from the bot's own permission-verification path are filtered out of that mirror automatically — you'll only ever see services notices that HexBot did not already consume internally.
Any line that does not start with . is broadcast to all other connected users:
hello everyone
<yourhandle> hello everyone ← echoed back to you
← other sessions see: <yourhandle> hello everyone
When users connect or disconnect you will see:
*** otheradmin has joined the console
*** otheradmin has left the console
When the REPL is being used locally, you will see:
*** REPL: .reload chanmod
- Authentication is password-based, following the Eggdrop model. The hostmask tells the bot which handle is connecting; the password hash proves it. Scrypt is the KDF — no plaintext is ever stored.
- The password travels over the DCC TCP connection in the clear (same as NickServ IDENTIFY on most IRC networks). This is acceptable for the threat model — a passive eavesdropper on the socket already has a path to every subsequent console command. TLS DCC (DCC SCHAT) is out of scope; operators who need end-to-end encryption should run a bot-to-user tunnel at the transport layer.
.chpassis rejected on the IRC PRIVMSG path — passwords only flow via REPL or an existing DCC session. Do not route command-by-DM as a workaround.- Keep
require_flagsatmorn— do not lower it tooorvwithout understanding the risk. - The bot only supports passive DCC — it opens the TCP port, the user connects. Active DCC (user opens port, bot dials out) is not supported and will be rejected with a notice.
- Repeated bad-password attempts from the same hostmask trigger a per-identity lockout with exponential backoff; this matches the bot-link auth policy.
- Sessions idle for longer than
idle_timeout_msare automatically disconnected. The prompt phase has a shorter 30-second timer — stalled prompts are killed quickly.
The bot sends its offer as a CTCP message. Some clients suppress these. Check your client's DCC or CTCP log. In irssi: /lastlog dcc. In WeeChat: open the irc.server.<name> raw buffer.
The NOTICE text will tell you why. Most pre-connection rejections return a generic request denied message to avoid leaking internal state to unauthenticated users; check the bot's log for the specific reason.
| Notice contains | Cause | Fix |
|---|---|---|
passive DCC CHAT |
Your client sent active DCC (non-zero ip/port) | Enable passive/reverse DCC in your client settings |
request denied |
Hostmask not registered, insufficient flags, session limit, duplicate session, or port exhaustion | Check the bot log for the specific reason; common fixes: .adduser, .flags handle +m, wait for a session slot |
no password set |
The matched user has no password_hash on file (sent on the DCC socket, not via NOTICE) |
Ask an admin to run .chpass <handle> <newpass> from the REPL |
bad password |
You typed the wrong password at the prompt | Try again; after several failures the hostmask is locked out |
too many failed |
Per-hostmask lockout in effect after repeated password failures | Wait out the lockout (escalates exponentially); fix the password |
Password prompt timed |
You took longer than 30s to answer the prompt | Reconnect and type the password promptly |
The bot opens a TCP port and waits 30 seconds for your client to connect. If your client cannot reach the port:
- Confirm the bot's
ipis the correct public IP (not a private/internal address). - Confirm the firewall allows inbound TCP on the configured port range.
- Test reachability:
nc -zv <bot-ip> 49152from your machine. If it times out, it's a firewall/routing issue, not a bot issue. - Check if the bot is behind a NAT (e.g., cloud VM with a private IP that maps to a public IP) — in that case, the
ipfield must be the external public IP, not the private one shown byip addr.
This is usually a readline/encoding issue. Try a different IRC client. irssi and WeeChat have the most reliable DCC CHAT implementations.
Make sure your irssi has DCC enabled. Check: /set dcc_autoaccept. You should see the offer in the status window and accept with /dcc chat hexbot or accept the incoming offer.