diff --git a/ROADMAP.md b/ROADMAP.md index b2c2d65..1f8f8c7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -33,8 +33,8 @@ ## v0.3.0 - [x] Client-side TLS (accept TLS from clients) -- [ ] Channel key support (JOIN #channel key) -- [ ] Hot config reload (SIGHUP) +- [x] Channel key support (JOIN #channel key) +- [x] Hot config reload (SIGHUP) - [ ] Systemd service file ## v0.4.0 diff --git a/TASKS.md b/TASKS.md index a946eb8..38b3b4e 100644 --- a/TASKS.md +++ b/TASKS.md @@ -26,6 +26,7 @@ ## Next - [x] P2: Client-side TLS support -- [ ] P2: Channel key support +- [x] P2: Channel key support +- [x] P2: Hot config reload (SIGHUP + REHASH refactor) - [ ] P3: Systemd service file - [ ] P3: Containerfile for podman deployment diff --git a/TODO.md b/TODO.md index b86d71e..fc3a2c5 100644 --- a/TODO.md +++ b/TODO.md @@ -2,8 +2,6 @@ ## Features -- [ ] Channel key support (JOIN #channel key) -- [ ] Hot config reload on SIGHUP - [ ] Web status dashboard - [ ] DCC passthrough - [ ] Per-client backlog tracking (multi-user) diff --git a/docs/CHEATSHEET.md b/docs/CHEATSHEET.md index d31bffb..a52eb26 100644 --- a/docs/CHEATSHEET.md +++ b/docs/CHEATSHEET.md @@ -74,8 +74,9 @@ PASS # authenticate (all networks) /msg *bouncer REHASH # reload config file /msg *bouncer ADDNETWORK name host=h port=N tls=yes nick=n channels=#a,#b /msg *bouncer DELNETWORK name # remove network -/msg *bouncer AUTOJOIN net +#chan # add to autojoin -/msg *bouncer AUTOJOIN net -#chan # remove from autojoin +/msg *bouncer AUTOJOIN net +#chan # add to autojoin +/msg *bouncer AUTOJOIN net +#chan key # add with channel key +/msg *bouncer AUTOJOIN net -#chan # remove from autojoin ``` ### NickServ @@ -174,6 +175,13 @@ notify_proxy = false # use SOCKS5 for notifications Only fires when no clients are attached. +## Hot Reload + +```bash +kill -HUP $(pidof bouncer) # reload config via signal +/msg *bouncer REHASH # reload config via command +``` + ## Config Skeleton ```toml @@ -201,6 +209,7 @@ host / port [networks.] # repeatable host / port / tls nick / channels / autojoin +channel_keys # keys for +k channels password # optional, IRC server PASS ``` diff --git a/docs/USAGE.md b/docs/USAGE.md index 6f80e80..050ad82 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -364,6 +364,7 @@ port = 6697 # server port (default: 6697 if tls, 6667 otherwise) tls = true # use TLS for server connection nick = "mynick" # desired IRC nick (set after probation) channels = ["#test"] # channels to join (after probation) +channel_keys = { "#secret" = "hunter2" } # keys for +k channels (optional) autojoin = true # auto-join channels on ready (default: true) password = "" # IRC server password (optional, for PASS command) ``` @@ -488,10 +489,11 @@ Responses arrive as NOTICE messages from `*bouncer`. | `REHASH` | Reload config file, add/remove/reconnect networks | | `ADDNETWORK key=val ...` | Create a network at runtime | | `DELNETWORK ` | Stop and remove a network | -| `AUTOJOIN +/-#channel` | Add or remove channel from autojoin list | +| `AUTOJOIN +#channel [key]` | Add channel (with optional key for +k channels) | +| `AUTOJOIN -#channel` | Remove channel from autojoin list | **ADDNETWORK keys:** `host` (required), `port`, `tls` (yes/no), `nick`, -`channels` (comma-separated), `password`. +`channels` (comma-separated), `channel_keys` (`#chan=key,...`), `password`. ### NickServ @@ -536,6 +538,7 @@ Responses arrive as NOTICE messages from `*bouncer`. /msg *bouncer ADDNETWORK oftc host=irc.oftc.net port=6697 tls=yes channels=#test /msg *bouncer DELNETWORK oftc /msg *bouncer AUTOJOIN libera +#newchannel +/msg *bouncer AUTOJOIN libera +#secret hunter2 /msg *bouncer AUTOJOIN libera -#oldchannel /msg *bouncer IDENTIFY libera /msg *bouncer REGISTER libera @@ -625,6 +628,62 @@ farm_interval = 3600 # seconds between attempts per network farm_max_accounts = 10 # stop farming when this many verified accounts exist ``` +## Channel Keys + +Channels with mode `+k` require a key to join. Configure keys in TOML: + +```toml +[networks.libera] +channels = ["#secret", "#public"] +channel_keys = { "#secret" = "hunter2" } +``` + +Keys are used automatically during autojoin and KICK rejoin. To add a keyed +channel at runtime: + +``` +/msg *bouncer AUTOJOIN libera +#secret hunter2 +``` + +Removing a channel also clears its key: + +``` +/msg *bouncer AUTOJOIN libera -#secret +``` + +## Hot Reload + +The bouncer reloads its config file on `SIGHUP` or via the `REHASH` command. +Both use the same logic: re-read TOML, diff networks (add/remove/reconnect), +and update mutable fields (channels, channel_keys, nick, password). + +### SIGHUP + +```bash +kill -HUP $(pidof bouncer) +``` + +Results are logged (no client connection needed). Useful for headless +operation (systemd, containers). + +### REHASH command + +``` +/msg *bouncer REHASH +``` + +Results are sent back as NOTICE messages. + +### What changes on reload + +| Field | Effect | +|-------|--------| +| Network host/port/tls/proxy | Network reconnected | +| channels, channel_keys, nick, password | Updated in-place | +| notify_url, notify_cooldown, etc. | Notifier recreated | +| farm_enabled, farm_interval, etc. | Farm started/stopped | +| bind, port, password, client_tls | Warning logged (restart required) | + ## Stopping Press `Ctrl+C` or send `SIGTERM`. The bouncer shuts down gracefully, closing diff --git a/src/bouncer/__main__.py b/src/bouncer/__main__.py index cf3623e..398d3de 100644 --- a/src/bouncer/__main__.py +++ b/src/bouncer/__main__.py @@ -90,6 +90,21 @@ async def _run(config_path: Path, verbose: bool) -> None: for sig in (signal.SIGINT, signal.SIGTERM): loop.add_signal_handler(sig, _signal_handler) + # Hot reload on SIGHUP + async def _sighup_rehash() -> None: + try: + lines = await commands.rehash(router, config_path) + for line in lines: + log.info("REHASH: %s", line) + except Exception: + log.exception("SIGHUP rehash failed") + + def _sighup_handler() -> None: + log.info("SIGHUP received, reloading config...") + asyncio.create_task(_sighup_rehash()) + + loop.add_signal_handler(signal.SIGHUP, _sighup_handler) + await stop_event.wait() server.close()