Phase 5: extended connectivity — WireGuard ext, OpenVPN, Tor exit routing
- ConnectivityManager: per-peer exit routing via iptables fwmark/policy tables (wg_ext=0x10/t110, openvpn=0x20/t120, tor=0x30/t130) - Dedicated PIC_CONNECTIVITY chains (mangle+nat), kill-switch FORWARD DROP - Config upload with sanitization: strips PostUp/PostDown and OVpn script dirs - Peer exit_via field added to peer registry (backward-compat, default=default) - 7 Flask routes at /api/connectivity/* - Connectivity.jsx: 693-line frontend with exit cards, peer assignment table - 72 new tests for ConnectivityManager (72 passing) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+103
@@ -43,6 +43,7 @@ from managers import (
|
||||
cell_link_manager, auth_manager, setup_manager,
|
||||
caddy_manager,
|
||||
ddns_manager, service_store_manager,
|
||||
connectivity_manager,
|
||||
firewall_manager, EventType,
|
||||
)
|
||||
# Re-exports: tests do `from app import CellManager` and `from app import _resolve_peer_dns`
|
||||
@@ -379,6 +380,11 @@ def _apply_startup_enforcement():
|
||||
service_store_manager.reapply_on_startup()
|
||||
except Exception as _sse:
|
||||
logger.warning(f"service_store reapply_on_startup failed (non-fatal): {_sse}")
|
||||
# Phase 5: re-apply extended-connectivity policy routing rules
|
||||
try:
|
||||
connectivity_manager.apply_routes()
|
||||
except Exception as _ce:
|
||||
logger.warning(f"connectivity apply_routes failed (non-fatal): {_ce}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Startup enforcement failed (non-fatal): {e}")
|
||||
|
||||
@@ -724,6 +730,103 @@ def clear_health_history():
|
||||
service_alert_counters = {}
|
||||
return jsonify({'message': 'Health history cleared'})
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 5 — Extended connectivity routes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@app.route('/api/connectivity/status', methods=['GET'])
|
||||
def connectivity_status():
|
||||
"""Return connectivity manager status (configured exits, peer counts)."""
|
||||
try:
|
||||
return jsonify(connectivity_manager.get_status())
|
||||
except Exception as e:
|
||||
logger.error(f"connectivity_status: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/connectivity/exits', methods=['GET'])
|
||||
def connectivity_list_exits():
|
||||
"""List configured exits and their state."""
|
||||
try:
|
||||
return jsonify({'exits': connectivity_manager.list_exits()})
|
||||
except Exception as e:
|
||||
logger.error(f"connectivity_list_exits: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/connectivity/exits/wireguard', methods=['POST'])
|
||||
def connectivity_upload_wireguard():
|
||||
"""Upload an external WireGuard config (becomes wg_ext0)."""
|
||||
try:
|
||||
data = request.get_json(silent=True) or {}
|
||||
conf_text = data.get('conf_text', '')
|
||||
if not isinstance(conf_text, str) or not conf_text.strip():
|
||||
return jsonify({'ok': False, 'error': 'conf_text is required'}), 400
|
||||
result = connectivity_manager.upload_wireguard_ext(conf_text)
|
||||
if result.get('ok'):
|
||||
return jsonify(result)
|
||||
return jsonify(result), 400
|
||||
except Exception as e:
|
||||
logger.error(f"connectivity_upload_wireguard: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/connectivity/exits/openvpn', methods=['POST'])
|
||||
def connectivity_upload_openvpn():
|
||||
"""Upload an OpenVPN profile (.ovpn)."""
|
||||
try:
|
||||
data = request.get_json(silent=True) or {}
|
||||
ovpn_text = data.get('ovpn_text', '')
|
||||
name = data.get('name', 'default')
|
||||
if not isinstance(ovpn_text, str) or not ovpn_text.strip():
|
||||
return jsonify({'ok': False, 'error': 'ovpn_text is required'}), 400
|
||||
result = connectivity_manager.upload_openvpn(ovpn_text, name=name)
|
||||
if result.get('ok'):
|
||||
return jsonify(result)
|
||||
return jsonify(result), 400
|
||||
except Exception as e:
|
||||
logger.error(f"connectivity_upload_openvpn: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/connectivity/exits/apply', methods=['POST'])
|
||||
def connectivity_apply_routes():
|
||||
"""Idempotently re-apply all connectivity policy routing rules."""
|
||||
try:
|
||||
result = connectivity_manager.apply_routes()
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
logger.error(f"connectivity_apply_routes: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/connectivity/peers/<peer_name>/exit', methods=['PUT'])
|
||||
def connectivity_set_peer_exit(peer_name: str):
|
||||
"""Assign a peer to an egress exit type."""
|
||||
try:
|
||||
data = request.get_json(silent=True) or {}
|
||||
exit_via = data.get('exit_via')
|
||||
if not isinstance(exit_via, str):
|
||||
return jsonify({'ok': False, 'error': 'exit_via is required'}), 400
|
||||
result = connectivity_manager.set_peer_exit(peer_name, exit_via)
|
||||
if result.get('ok'):
|
||||
return jsonify(result)
|
||||
return jsonify(result), 400
|
||||
except Exception as e:
|
||||
logger.error(f"connectivity_set_peer_exit({peer_name}): {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/connectivity/peers', methods=['GET'])
|
||||
def connectivity_get_peer_exits():
|
||||
"""Return {peer_name: exit_type} for all peers."""
|
||||
try:
|
||||
return jsonify({'peers': connectivity_manager.get_peer_exits()})
|
||||
except Exception as e:
|
||||
logger.error(f"connectivity_get_peer_exits: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
debug = os.environ.get('FLASK_DEBUG', '0') == '1'
|
||||
app.run(host='0.0.0.0', port=3000, debug=debug)
|
||||
Reference in New Issue
Block a user