diff --git a/README.MD b/README.MD index bda31b3..2d920ee 100644 --- a/README.MD +++ b/README.MD @@ -29,6 +29,10 @@ DockFlare automates Cloudflare Tunnel ingress rule management based on Docker co - Tunnel & agent status. - Start/Stop agent controls. - Managed ingress rule list with status, container ID, deletion time, and "Force Delete" option. + - **List of all Cloudflare Tunnels on Account (NEW)**: Shows all tunnels on the configured Cloudflare account (not just the one managed by this DockFlare instance), including their name, ID, status, creation date, and active connections. Users can click a '+' icon next to each tunnel to dynamically load and view its associated CNAME DNS records found within the configured zones. + - Real-time activity logs streamed directly to the browser. + - Dark/Light mode toggle. + - Basic reconciliation status display. - **Real-time Log Streaming**: View logs in real-time using Server-Sent Events (SSE). - **Content Security Policy (CSP)**: Ensures secure loading of resources and compatibility with reverse proxies. diff --git a/app.py b/app.py index 169cfa4..282fd5c 100644 --- a/app.py +++ b/app.py @@ -53,6 +53,10 @@ EXTERNAL_TUNNEL_ID = os.getenv('EXTERNAL_TUNNEL_ID') # Network scanning configuration SCAN_ALL_NETWORKS = os.getenv('SCAN_ALL_NETWORKS', 'false').lower() in ['true', '1', 't', 'yes'] +# Tunnel DNS Scanning to show DNS entrys for other +# for function get_dns_records_for_tunnel +TUNNEL_DNS_SCAN_ZONE_NAMES_STR = os.getenv('TUNNEL_DNS_SCAN_ZONE_NAMES', '') +TUNNEL_DNS_SCAN_ZONE_NAMES = [name.strip() for name in TUNNEL_DNS_SCAN_ZONE_NAMES_STR.split(',') if name.strip()] # Settings that are only required when NOT using external cloudflared TUNNEL_NAME = os.getenv("TUNNEL_NAME", "dockflared-tunnel") @@ -2085,6 +2089,82 @@ def update_cloudflare_config(): # If we reached here, either there were no changes or the update was successful return True +def get_all_account_cloudflare_tunnels(): + if not CF_ACCOUNT_ID: + logging.warning("CF_ACCOUNT_ID is not configured. Cannot list all Cloudflare tunnels from the account.") + return [] + if not CF_API_TOKEN: + logging.error("Cloudflare API token not configured. Cannot list all account tunnels.") + return [] + + endpoint = f"/accounts/{CF_ACCOUNT_ID}/cfd_tunnel" + params = { + "is_deleted": "false" + } + + logging.info(f"Attempting to list all Cloudflare tunnels for account ID {CF_ACCOUNT_ID} with params: {params}") + try: + response_data = cf_api_request("GET", endpoint, params=params) + tunnels = response_data.get("result", []) + + if isinstance(tunnels, list): + logging.info(f"Successfully retrieved {len(tunnels)} Cloudflare tunnels from the account (any status).") + + desired_statuses = {"healthy", "degraded", "down", "inactive"} + filtered_tunnels = [ + tunnel for tunnel in tunnels if tunnel.get("status") in desired_statuses + ] + + logging.info(f"Returning {len(filtered_tunnels)} tunnels after client-side status check for relevant statuses.") + filtered_tunnels.sort(key=lambda t: t.get("name", "").lower()) + return filtered_tunnels + else: + logging.error(f"Unexpected data format for account tunnels list: {type(tunnels)}. Expected a list. Response: {response_data}") + return [] + except requests.exceptions.RequestException as e: + logging.error(f"API error listing all Cloudflare tunnels for the account: {e}") + if hasattr(e, 'response') and e.response is not None: + if e.response.status_code == 403: + logging.error("Permission denied (403) listing account tunnels. Ensure API token has 'Account:Cloudflare Tunnel:Read' permission for the account.") + elif e.response.status_code == 400: + logging.error(f"Bad Request (400) listing account tunnels. API Response: {e.response.text}") + return [] + except Exception as e: + logging.error(f"Unexpected error listing all Cloudflare tunnels for the account: {e}", exc_info=True) + return [] + +def get_dns_records_for_tunnel(zone_id, tunnel_id): + """Fetches CNAME records in a specific zone pointing to a specific tunnel.""" + if not zone_id or not tunnel_id: + logging.warning("get_dns_records_for_tunnel: Missing zone_id or tunnel_id.") + return [] + + expected_cname_content = f"{tunnel_id}.cfargotunnel.com" + + endpoint = f"/zones/{zone_id}/dns_records" + params = { + "type": "CNAME", + "content": expected_cname_content, + "per_page": 100 # Get up to 100 records, handle pagination if more are expected + } + logging.info(f"Fetching DNS records for tunnel {tunnel_id} in zone {zone_id} with content {expected_cname_content}") + + try: + response_data = cf_api_request("GET", endpoint, params=params) # Your existing cf_api_request + dns_records = response_data.get("result", []) + if isinstance(dns_records, list): + + return [{"name": record.get("name"), "id": record.get("id"), "zone_id": zone_id} for record in dns_records if record.get("name")] + else: + logging.error(f"Unexpected data format for DNS records list in zone {zone_id}: {type(dns_records)}") + return [] + except requests.exceptions.RequestException as e: + logging.error(f"API error fetching DNS records for tunnel {tunnel_id} in zone {zone_id}: {e}") + return [] + except Exception as e: + logging.error(f"Unexpected error fetching DNS records for tunnel {tunnel_id} in zone {zone_id}: {e}", exc_info=True) + return [] + app = Flask(__name__) app.secret_key = os.urandom(24) app.config['PREFERRED_URL_SCHEME'] = 'https' # Default for url_for, but client-side JS will fix @@ -2144,6 +2224,41 @@ def get_display_token(token): return "Not available" return f"{token[:5]}...{token[-5:]}" if len(token) > 10 else "Token retrieved (short)" +@app.route('/tunnel-dns-records/') +def tunnel_dns_records(tunnel_id): + if not tunnel_id: + return jsonify({"error": "Tunnel ID is required"}), 400 + + all_found_dns_records = [] + + zone_ids_to_scan = set() + + if CF_ZONE_ID: # Your global CF_ZONE_ID + zone_ids_to_scan.add(CF_ZONE_ID) + + for zone_name in TUNNEL_DNS_SCAN_ZONE_NAMES: # The list loaded from env var + resolved_zone_id = get_zone_id_from_name(zone_name) # Your existing function + if resolved_zone_id: + zone_ids_to_scan.add(resolved_zone_id) + else: + logging.warning(f"Could not resolve Zone ID for configured scan name: {zone_name}") + + if not zone_ids_to_scan: + logging.warning(f"No Zone IDs configured or resolved for DNS scan for tunnel {tunnel_id}.") + + return jsonify({"dns_records": [], "message": "No zones configured for DNS scan."}) + + for zone_id in zone_ids_to_scan: + records_in_zone = get_dns_records_for_tunnel(zone_id, tunnel_id) + if records_in_zone: # records_in_zone is a list of dicts + + all_found_dns_records.extend(records_in_zone) + + all_found_dns_records.sort(key=lambda r: r.get("name", "").lower()) + + logging.info(f"Found {len(all_found_dns_records)} DNS records for tunnel {tunnel_id} across {len(zone_ids_to_scan)} zones.") + return jsonify({"dns_records": all_found_dns_records}) + @app.context_processor def inject_protocol(): """Inject protocol info into all templates with more reliable detection.""" @@ -2195,7 +2310,7 @@ def status_page(): docker_available = docker_client is not None external_cloudflared = USE_EXTERNAL_CLOUDFLARED external_tunnel_id = EXTERNAL_TUNNEL_ID - + all_account_tunnels_list = get_all_account_cloudflare_tunnels() return render_template('status_page.html', tunnel_state=template_tunnel_state, agent_state=template_agent_state, @@ -2205,7 +2320,11 @@ def status_page(): docker_available=docker_available, external_cloudflared=external_cloudflared, external_tunnel_id=external_tunnel_id, - rules=rules_for_template) + rules=rules_for_template, + all_account_tunnels=all_account_tunnels_list, + CF_ACCOUNT_ID_CONFIGURED=bool(CF_ACCOUNT_ID), # Pass boolean flag + ACCOUNT_ID_FOR_DISPLAY=CF_ACCOUNT_ID if CF_ACCOUNT_ID else "Not Configured" # Pass actual ID or placeholder + ) @app.route('/ping') def ping(): diff --git a/images/status_web.png b/images/status_web.png index ade8bd7..691ec8a 100644 Binary files a/images/status_web.png and b/images/status_web.png differ diff --git a/templates/status_page.html b/templates/status_page.html index bed764c..d7d15ff 100644 --- a/templates/status_page.html +++ b/templates/status_page.html @@ -21,14 +21,34 @@ - Dockflare - Status + DockFlare - Cloudflare Tunnel ingress Manager - - - - + + .connection-badge { display: inline-block; padding: 0.2em 0.4em; font-size: 85%; font-weight: 600; line-height: 1; text-align: center; white-space: nowrap; vertical-align: baseline; border-radius: 0.25rem; background-color: #6366f1; color: white; margin-right: 4px; margin-bottom: 4px; } + html.dark .connection-badge { background-color: #818cf8; } + .warning-text { color: #ef4444; font-weight: bold; } + html.dark .warning-text { color: #f87171; } +
-
- Beta Build +
+ stable 1.4.0
@@ -128,7 +153,6 @@

Tunnel & Agent Status

-

Cloudflare API & Tunnel Setup

{% set status_box_class = 'border-blue-500 bg-blue-50 dark:bg-blue-900/20' %} @@ -147,7 +171,6 @@ {% endif %}
-

Tunnel Details

@@ -161,7 +184,6 @@ {% endif %}
-

Agent Control

{% if agent_state.last_action_status %} @@ -175,22 +197,17 @@

Last Action: {{ agent_state.last_action_status }}

{% endif %} -
{% if external_cloudflared %}
External Mode: - - Using External Cloudflared - + Using External Cloudflared
External ID: - - {{ external_tunnel_id }} - + {{ external_tunnel_id }}

External cloudflared mode enabled. DockFlare manages tunnel configuration and DNS records, but not the agent container.

@@ -204,36 +221,27 @@ {% if agent_state.container_status == 'running' %} {% set indicator_class = 'bg-green-500 animate-pulse-fast' %} {% elif agent_state.container_status in ['exited', 'dead', 'not_found'] %} {% set indicator_class = 'bg-red-500' %} {% elif agent_state.container_status == 'docker_unavailable' %} {% set indicator_class = 'bg-gray-500' %} - {% elif agent_state.container_status in ['created', 'paused', 'restarting'] %} - {% set indicator_class = 'bg-amber-500' %} + {% elif agent_state.container_status in ['created', 'paused', 'restarting'] %} {% set indicator_class = 'bg-amber-500' %} {% endif %} - {% set status_text_class = 'text-gray-600 dark:text-gray-400' %} {% if agent_state.container_status=='running' %} {% set status_text_class = 'text-green-600 dark:text-green-400' %} {% elif agent_state.container_status in ['exited', 'dead', 'not_found', 'docker_unavailable'] %} {% set status_text_class = 'text-red-600 dark:text-red-400' %} - {% elif agent_state.container_status in ['created', 'paused', 'restarting'] %} - {% set status_text_class = 'text-amber-600 dark:text-amber-400' %} + {% elif agent_state.container_status in ['created', 'paused', 'restarting'] %} {% set status_text_class = 'text-amber-600 dark:text-amber-400' %} {% endif %} - - {{ agent_state.container_status.replace('_',' ') if agent_state.container_status else 'Unknown' }} - + {{ agent_state.container_status.replace('_',' ') if agent_state.container_status else 'Unknown' }}
Agent Name: - - {{ cloudflared_container_name }} - + {{ cloudflared_container_name }}
-
{% set base_button_style = 'w-full sm:w-auto inline-flex items-center justify-center px-5 py-2 rounded-md text-white font-semibold shadow-md transition-all duration-200 ease-in-out transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none disabled:shadow-none' %} {% set start_button_style = 'bg-green-600 hover:bg-green-700 focus:ring-green-500 disabled:bg-green-300 dark:disabled:bg-green-800' %} {% set stop_button_style = 'bg-red-600 hover:bg-red-700 focus:ring-red-500 disabled:bg-red-300 dark:disabled:bg-red-800' %} {% set is_startable = tunnel_state.get('id') and tunnel_state.get('token') and docker_available %} {% set is_stoppable = docker_available %} - {% if agent_state.container_status=='running' %}
@@ -255,7 +263,6 @@

Managed Ingress Rules

- {% if rules %}
@@ -273,47 +280,31 @@ {% for hostname, details in rules.items()|sort %} - + + {{ hostname }} + + {% if hostname.startswith('*.') %} + wildcard + {% endif %} + + - + @@ -327,6 +318,87 @@ +
+
+

+ All Cloudflare Tunnels on Account +

+ {% if CF_ACCOUNT_ID_CONFIGURED %} +

+ Displaying tunnels for Cloudflare Account ID: {{ ACCOUNT_ID_FOR_DISPLAY | e }}. +
+ This list shows all tunnels found on the account, not just the one managed by this DockFlare instance. Click the '+' icon next to a tunnel to view its associated DNS records. +

+ {% if all_account_tunnels is defined and all_account_tunnels %} +
+
- - {{ hostname }} - - {% if hostname.startswith('*.') %} - - wildcard - - {% endif %} - - {{ details.service }} - {{ details.service }} {% set status_text_class = 'text-orange-600 dark:text-orange-400' if 'pending' in details.status else 'text-green-600 dark:text-green-400' %} {% set status_bg_class = 'bg-orange-100 dark:bg-orange-900/30' if 'pending' in details.status else 'bg-green-100 dark:bg-green-900/30' %} - - {{ details.status.replace('_',' ') }} - - - - {{ details.container_id[:12] if details.container_id else 'N/A' }} - + {{ details.status.replace('_',' ') }} {{ details.container_id[:12] if details.container_id else 'N/A' }} {% if details.status=='pending_deletion' and details.delete_at %} -
- {{ details.delete_at.strftime('%H:%M %d-%b-%Y') }} - (calculating...) -
- {% else %} - N/A - {% endif %} +
+ {{ details.delete_at.strftime('%H:%M %d-%b-%Y') }} + (calculating...) +
+ {% else %} N/A {% endif %}
- +
+ + + + + + + + + + + + {% for tunnel in all_account_tunnels %} + + + + + + + + + + + + {% endfor %} + +
+/-Tunnel NameTunnel IDStatusCreated AtConnections
+ + {{ tunnel.name | e }}{{ tunnel.id | e }} + + {{ tunnel.status | capitalize | e }} + + + {% if tunnel.created_at %}{{ tunnel.created_at.split('T')[0] | e }}{% else %}N/A{% endif %} + + {% if tunnel.connections and tunnel.connections is iterable and not tunnel.connections is string %} + {% for conn in tunnel.connections %}{{ conn.colo_name | e }}{% endfor %} + {% if tunnel.connections | length > 0 %}({{ tunnel.connections | length }} total){% else %}(0 total){% endif %} + {% elif tunnel.connections %}{{ tunnel.connections | e }}{% else %}None{% endif %} +
+
+ {% elif all_account_tunnels is defined %} +

No Cloudflare Tunnels found for Account ID: {{ ACCOUNT_ID_FOR_DISPLAY | e }}.

+

This could also mean an error occurred fetching them (check DockFlare logs). Ensure your CF_API_TOKEN has 'Account:Cloudflare Tunnel:Read' permission for the account level.

+ {% else %} +

Could not retrieve tunnel information (all_account_tunnels variable not available in template).

+ {% endif %} + {% else %} +

The CF_ACCOUNT_ID environment variable is not configured. This section cannot be displayed.

+ {% endif %} +
+ +

@@ -335,304 +407,122 @@
Connecting to log stream...

- - \ No newline at end of file