diff --git a/api/app.py b/api/app.py index 87baa41..0f44975 100644 --- a/api/app.py +++ b/api/app.py @@ -487,6 +487,12 @@ def update_config(): net_result = network_manager.apply_domain(domain) all_restarted.extend(net_result.get('restarted', [])) all_warnings.extend(net_result.get('warnings', [])) + # Regenerate Caddyfile — virtual host names change with the domain + import ip_utils as _ip_domain + _cur_id = config_manager.configs.get('_identity', {}) + _cur_range = _cur_id.get('ip_range', os.environ.get('CELL_IP_RANGE', '172.20.0.0/16')) + _cur_name = _cur_id.get('cell_name', os.environ.get('CELL_NAME', 'mycell')) + _ip_domain.write_caddyfile(_cur_range, _cur_name, domain, '/app/config/caddy/Caddyfile') # Apply cell name change to DNS hostname record if identity_updates.get('cell_name'): @@ -496,6 +502,12 @@ def update_config(): cn_result = network_manager.apply_cell_name(old_name, new_name) all_restarted.extend(cn_result.get('restarted', [])) all_warnings.extend(cn_result.get('warnings', [])) + # Regenerate Caddyfile — main virtual host name changes with cell_name + import ip_utils as _ip_name + _cur_id2 = config_manager.configs.get('_identity', {}) + _cur_range2 = _cur_id2.get('ip_range', os.environ.get('CELL_IP_RANGE', '172.20.0.0/16')) + _cur_domain2 = identity_updates.get('domain') or _cur_id2.get('domain', os.environ.get('CELL_DOMAIN', 'cell')) + _ip_name.write_caddyfile(_cur_range2, new_name, _cur_domain2, '/app/config/caddy/Caddyfile') # Apply ip_range change: regenerate DNS records, update virtual IPs + firewall rules if identity_updates.get('ip_range'): @@ -514,6 +526,9 @@ def update_config(): # Write new .env with updated IPs (and current ports) for next container start env_file = os.environ.get('COMPOSE_ENV_FILE', '/app/.env.compose') ip_utils.write_env_file(new_range, env_file, _collect_service_ports(config_manager.configs)) + # Regenerate Caddyfile with new VIPs + ip_utils.write_caddyfile(new_range, cur_cell_name, cur_domain, + '/app/config/caddy/Caddyfile') # Mark ALL containers as needing restart; network_recreate signals that # docker compose down is required before up (Docker can't change subnet in-place) _set_pending_restart( diff --git a/api/ip_utils.py b/api/ip_utils.py index 60491e2..007d17e 100644 --- a/api/ip_utils.py +++ b/api/ip_utils.py @@ -129,6 +129,84 @@ def get_virtual_ips(ip_range: str) -> Dict[str, str]: } +def write_caddyfile(ip_range: str, cell_name: str, domain: str, path: str) -> bool: + """ + Generate the Caddy reverse-proxy config from the current ip_range, cell_name, and domain. + + Must be called after any ip_range or domain change so Caddy routes correctly. + Container-internal ports are fixed by docker-compose and never change. + Returns True on success. + """ + try: + ips = get_service_ips(ip_range) + caddy_ip = ips['caddy'] + vip_calendar = ips['vip_calendar'] + vip_files = ips['vip_files'] + vip_mail = ips['vip_mail'] + vip_webdav = ips['vip_webdav'] + content = f"""\ +{{ + auto_https off +}} + +# Main cell domain — no service-IP restriction needed +http://{cell_name}.{domain}, http://{caddy_ip}:80 {{ + handle /api/* {{ + reverse_proxy cell-api:3000 + }} + handle /calendar* {{ + reverse_proxy cell-radicale:5232 + }} + handle /files* {{ + reverse_proxy cell-filegator:8080 + }} + handle /webmail* {{ + reverse_proxy cell-rainloop:8888 + }} + handle {{ + reverse_proxy cell-webui:80 + }} +}} + +# Per-service virtual IPs — each gets its own IP so iptables can target them +http://calendar.{domain}, http://{vip_calendar}:80 {{ + reverse_proxy cell-radicale:5232 +}} + +http://files.{domain}, http://{vip_files}:80 {{ + reverse_proxy cell-filegator:8080 +}} + +http://mail.{domain}, http://webmail.{domain}, http://{vip_mail}:80 {{ + reverse_proxy cell-rainloop:8888 +}} + +http://webdav.{domain}, http://{vip_webdav}:80 {{ + reverse_proxy cell-webdav:80 +}} + +http://api.{domain} {{ + reverse_proxy cell-api:3000 +}} + +# Catch-all for direct IP / localhost +:80 {{ + handle /api/* {{ + reverse_proxy cell-api:3000 + }} + handle {{ + reverse_proxy cell-webui:80 + }} +}} +""" + os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) + with open(path, 'w') as f: + f.write(content) + return True + except Exception: + return False + + def write_env_file(ip_range: str, path: str, ports: Optional[Dict[str, int]] = None) -> bool: """ Write (or overwrite) the docker-compose .env file with IPs and ports. diff --git a/scripts/setup_cell.py b/scripts/setup_cell.py index 78111fb..adb01b6 100644 --- a/scripts/setup_cell.py +++ b/scripts/setup_cell.py @@ -44,7 +44,6 @@ REQUIRED_DIRS = [ ] REQUIRED_FILES = [ - 'config/caddy/Caddyfile', 'config/dns/Corefile', 'config/dhcp/dnsmasq.conf', 'config/ntp/chrony.conf', @@ -205,6 +204,17 @@ def write_compose_env(ip_range: str): print(f'[WARN] Could not write .env — containers will use built-in default IPs/ports') +def write_caddy_config(ip_range: str, cell_name: str, domain: str): + """Generate Caddyfile with correct VIPs and hostnames for this cell.""" + sys.path.insert(0, os.path.join(ROOT, 'api')) + import ip_utils + caddyfile = os.path.join(ROOT, 'config', 'caddy', 'Caddyfile') + if ip_utils.write_caddyfile(ip_range, cell_name, domain, caddyfile): + print(f'[CREATED] config/caddy/Caddyfile (subnet={ip_range} domain={domain})') + else: + print(f'[WARN] Could not write Caddyfile') + + def _read_existing_ip_range() -> str: """Read ip_range from existing cell_config.json if present, else return None.""" cfg_path = os.path.join(ROOT, 'config', 'api', 'cell_config.json') @@ -237,6 +247,7 @@ def main(): write_wg0_conf(priv, vpn_address, wg_port) write_cell_config(cell_name, domain, wg_port) write_compose_env(ip_range) + write_caddy_config(ip_range, cell_name, domain) print() print('--- Setup complete! Run: make start ---')