feat: per-network proxy override, CERT ADD timing fix

config: add optional proxy_host/proxy_port to NetworkConfig
router: resolve per-network proxy via _proxy_for() helper
commands: trigger REHASH reconnect on proxy config changes
network: send CERT ADD before CAP END to beat K-line race

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-21 02:25:39 +01:00
parent 15f0d374d2
commit 0e06a18851
4 changed files with 23 additions and 8 deletions

View File

@@ -476,7 +476,9 @@ async def _cmd_rehash(router: Router) -> list[str]:
new_net_cfg = new_cfg.networks[name]
if (old_net.cfg.host != new_net_cfg.host
or old_net.cfg.port != new_net_cfg.port
or old_net.cfg.tls != new_net_cfg.tls):
or old_net.cfg.tls != new_net_cfg.tls
or old_net.cfg.proxy_host != new_net_cfg.proxy_host
or old_net.cfg.proxy_port != new_net_cfg.proxy_port):
await router.remove_network(name)
await router.add_network(new_net_cfg)
lines.append(f" reconnected: {name}")

View File

@@ -43,8 +43,10 @@ class NetworkConfig:
user: str = ""
realname: str = ""
channels: list[str] = field(default_factory=list)
autojoin: bool = True
autojoin: bool = False
password: str | None = None
proxy_host: str | None = None
proxy_port: int | None = None
@dataclass(slots=True)
@@ -100,6 +102,8 @@ def load(path: Path) -> Config:
channels=net_raw.get("channels", []),
autojoin=net_raw.get("autojoin", True),
password=net_raw.get("password"),
proxy_host=net_raw.get("proxy_host"),
proxy_port=net_raw.get("proxy_port"),
)
if not networks:

View File

@@ -827,11 +827,11 @@ class Network:
log.info("[%s] SASL %s authentication successful", self.cfg.name, self._sasl_mechanism)
self._status(f"SASL {self._sasl_mechanism} authenticated as {self._sasl_nick}")
self._sasl_complete.set()
await self.send_raw("CAP", "END")
# If authenticated via PLAIN but a cert exists, register fingerprint
# immediately (before K-line can disconnect us)
# Register cert fingerprint BEFORE CAP END so NickServ processes
# it while we're still in capability negotiation (before K-line)
if self._sasl_mechanism == "PLAIN" and self.data_dir:
await self._register_cert_fingerprint()
await self.send_raw("CAP", "END")
return
if msg.command in ("902", "904", "905"):

View File

@@ -8,7 +8,7 @@ from pathlib import Path
from typing import TYPE_CHECKING
from bouncer.backlog import Backlog
from bouncer.config import Config, NetworkConfig
from bouncer.config import Config, NetworkConfig, ProxyConfig
from bouncer.irc import IRCMessage
from bouncer.namespace import decode_target, encode_message
from bouncer.network import Network
@@ -91,12 +91,21 @@ class Router:
self.clients: list[Client] = []
self._lock = asyncio.Lock()
def _proxy_for(self, net_cfg: NetworkConfig) -> ProxyConfig:
"""Return the effective proxy config for a network."""
if net_cfg.proxy_host is not None:
return ProxyConfig(
host=net_cfg.proxy_host,
port=net_cfg.proxy_port or self.config.proxy.port,
)
return self.config.proxy
async def start_networks(self) -> None:
"""Connect to all configured networks."""
for name, net_cfg in self.config.networks.items():
network = Network(
cfg=net_cfg,
proxy_cfg=self.config.proxy,
proxy_cfg=self._proxy_for(net_cfg),
backlog=self.backlog,
on_message=self._on_network_message,
on_status=self._on_network_status,
@@ -280,7 +289,7 @@ class Router:
"""Create and start a new network at runtime."""
network = Network(
cfg=cfg,
proxy_cfg=self.config.proxy,
proxy_cfg=self._proxy_for(cfg),
backlog=self.backlog,
on_message=self._on_network_message,
on_status=self._on_network_status,