🚀 feat: Release v1.1.0

Bumped version to v1.1.0 across all project files.

🏗️ Infrastructure & Templates:
- 🐳 Docker Compose: Enhanced security, added persistent volume support.
- 📋 Cosmos templates: Updated with new environment variable defaults.

🛠️ Scripts:
- dashboard.sh: Implemented API token authentication, improved error handling, and added a notification UI.
- fingerprint.sh: Refactored for clearer output formatting, robust validation, and a comprehensive help message.

 General:
- Improved configuration management, health checks, and monitoring integration.
This commit is contained in:
rE-Bo0t.bx1
2025-11-08 00:00:49 +08:00
parent 39f63a071b
commit e2a9b4d398
20 changed files with 1700 additions and 1585 deletions

View File

@@ -5,13 +5,15 @@
set -e
# Configuration
VERSION="1.1.1"
VERSION="1.1.0"
DASHBOARD_PORT="${DASHBOARD_PORT:-8080}"
DASHBOARD_BIND="${DASHBOARD_BIND:-127.0.0.1}" # ⚠️ CHANGED: Secure default
ENABLE_DASHBOARD="${ENABLE_DASHBOARD:-true}"
REFRESH_INTERVAL="${REFRESH_INTERVAL:-10}"
MULTI_RELAY="${MULTI_RELAY:-false}"
MAX_CONNECTIONS="${MAX_CONNECTIONS:-5}" # 🔒 NEW: Rate limiting
API_TOKEN="${API_TOKEN:-}" # 🔒 NEW: API authentication token
LOG_RETENTION="${LOG_RETENTION:-100}" # 📝 NEW: Number of log lines to keep
# Trap for clean exit
trap 'cleanup' INT TERM
@@ -19,9 +21,19 @@ trap 'cleanup' INT TERM
cleanup() {
echo ""
echo "🛑 Dashboard shutting down..."
# Clean up any temporary files
[ -f "/tmp/dashboard.html" ] && rm -f "/tmp/dashboard.html"
[ -f "/tmp/dashboard.pid" ] && rm -f "/tmp/dashboard.pid"
exit 0
}
# Enhanced error handling
handle_error() {
echo "❌ Error: $1" >&2
logger -t "dashboard" "Error: $1"
exit 1
}
# Parse arguments
for arg in "$@"; do
case "$arg" in
@@ -38,6 +50,7 @@ OPTIONS:
--bind ADDR Bind address (default: 127.0.0.1)
--refresh SEC Auto-refresh interval (default: 10)
--multi Enable multi-relay support
--token TOKEN API authentication token
--help, -h Show this help message
ENVIRONMENT VARIABLES:
@@ -47,6 +60,8 @@ ENVIRONMENT VARIABLES:
REFRESH_INTERVAL Auto-refresh in seconds
MULTI_RELAY Multi-relay mode (true/false)
MAX_CONNECTIONS Max concurrent connections (default: 5)
API_TOKEN API authentication token
LOG_RETENTION Number of log lines to keep (default: 100)
⚠️ SECURITY NOTICE:
Default binding is 127.0.0.1 (localhost only).
@@ -55,6 +70,9 @@ ENVIRONMENT VARIABLES:
⚠️ WARNING: External exposure without authentication is NOT recommended!
Use a reverse proxy (nginx/caddy) with authentication for production.
🔒 SECURITY: Set API_TOKEN to protect API endpoints:
API_TOKEN=your-secure-token
FEATURES:
• Real-time relay status monitoring
@@ -64,6 +82,7 @@ FEATURES:
• Error/warning alerts
• Multi-relay management (optional)
• Mobile-responsive design
• API authentication (with token)
ENDPOINTS:
http://localhost:8080/ Main dashboard
@@ -79,6 +98,7 @@ DOCKER INTEGRATION:
# For external access (use with caution):
environment:
- DASHBOARD_BIND=0.0.0.0
- API_TOKEN=your-secure-token
ports:
- "8080:8080"
@@ -101,6 +121,11 @@ EOF
REFRESH_INTERVAL="$1"
shift
;;
--token)
shift
API_TOKEN="$1"
shift
;;
--multi)
MULTI_RELAY="true"
;;
@@ -122,20 +147,42 @@ fi
# Security warning for external binding
if [ "$DASHBOARD_BIND" = "0.0.0.0" ]; then
echo "⚠️ WARNING: Dashboard is bound to 0.0.0.0 (all interfaces)"
echo "⚠️ This exposes the dashboard without authentication!"
echo "⚠️ This exposes dashboard without authentication!"
if [ -z "$API_TOKEN" ]; then
echo "⚠️ Consider setting API_TOKEN for API protection."
else
echo "✅ API endpoints are protected with token authentication."
fi
echo "⚠️ Consider using a reverse proxy with authentication."
echo ""
fi
# Check for netcat
if ! command -v nc > /dev/null 2>&1; then
echo "❌ Error: netcat (nc) is required"
echo "💡 Install with: apk add netcat-openbsd"
exit 1
handle_error "netcat (nc) is required. Install with: apk add netcat-openbsd"
fi
# Connection counter (simple rate limiting)
CONNECTION_COUNT=0
echo $$ > /tmp/dashboard.pid
# Enhanced authentication check
check_api_auth() {
AUTH_HEADER="$1"
if [ -n "$API_TOKEN" ]; then
# Extract token from Authorization header
TOKEN=$(echo "$AUTH_HEADER" | sed -n 's/.*Bearer *\([^ ]*\).*/\1p')
if [ "$TOKEN" != "$API_TOKEN" ]; then
echo "HTTP/1.1 401 Unauthorized"
echo "Content-Type: application/json"
echo "Connection: close"
echo ""
echo '{"error":"Unauthorized"}'
return 1
fi
fi
return 0
}
# Function to generate dashboard HTML
generate_dashboard() {
@@ -143,7 +190,14 @@ generate_dashboard() {
STATUS_JSON=$(/usr/local/bin/status --json 2>/dev/null || echo '{}')
HEALTH_JSON=$(/usr/local/bin/health --json 2>/dev/null || echo '{}')
cat << 'EOF'
# Cache the HTML to avoid regenerating on every request
if [ -f "/tmp/dashboard.html" ] && [ $(find /tmp/dashboard.html -mmin -1 2>/dev/null) ]; then
cat /tmp/dashboard.html
return
fi
# Generate new HTML and cache it
cat << 'EOF' > /tmp/dashboard.html
<!DOCTYPE html>
<html lang="en">
<head>
@@ -341,6 +395,31 @@ generate_dashboard() {
font-size: 14px;
color: #4b5563;
}
.notification {
position: fixed;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.95);
padding: 15px 20px;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
max-width: 300px;
transform: translateX(400px);
transition: transform 0.3s ease;
}
.notification.show {
transform: translateX(0);
}
.notification.error {
border-left: 4px solid #ef4444;
}
.notification.success {
border-left: 4px solid #10b981;
}
</style>
</head>
<body>
@@ -458,11 +537,32 @@ generate_dashboard() {
🔄 Auto-refresh: <span id="countdown">${REFRESH_INTERVAL}</span>s
</div>
<div class="notification" id="notification"></div>
<script>
let refreshInterval = ${REFRESH_INTERVAL};
let countdown = refreshInterval;
let lastStatus = null;
function showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = 'notification ' + type;
notification.classList.add('show');
setTimeout(() => {
notification.classList.remove('show');
}, 3000);
}
function updateStatus(data) {
// Check for status changes
if (lastStatus && lastStatus.status !== data.status) {
showNotification('Status changed to: ' + data.status,
data.status === 'healthy' ? 'success' : 'error');
}
lastStatus = data;
// Update overall status
const statusEl = document.getElementById('overall-status');
statusEl.className = 'status-badge status-' + (data.status || 'unknown');
@@ -517,9 +617,15 @@ generate_dashboard() {
function refreshData() {
fetch('/api/status')
.then(response => response.json())
.then(response => {
if (!response.ok) throw new Error('Network response was not ok');
return response.json();
})
.then(data => updateStatus(data))
.catch(error => console.error('Error fetching status:', error));
.catch(error => {
console.error('Error fetching status:', error);
showNotification('Failed to fetch status data', 'error');
});
}
function copyFingerprint() {
@@ -530,6 +636,10 @@ generate_dashboard() {
const original = el.textContent;
el.textContent = '✅ Copied!';
setTimeout(() => el.textContent = original, 2000);
showNotification('Fingerprint copied to clipboard', 'success');
}).catch(err => {
console.error('Failed to copy: ', err);
showNotification('Failed to copy fingerprint', 'error');
});
}
}
@@ -550,11 +660,20 @@ generate_dashboard() {
</body>
</html>
EOF
# Return the cached HTML
cat /tmp/dashboard.html
}
# Function to handle API requests
handle_api() {
REQUEST_PATH="$1"
AUTH_HEADER="$2"
# Check authentication for API endpoints
if [ "$REQUEST_PATH" != "/" ] && ! check_api_auth "$AUTH_HEADER"; then
return
fi
case "$REQUEST_PATH" in
"/api/status")
@@ -619,30 +738,39 @@ echo "🎨 Starting Tor Relay Dashboard v${VERSION}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🌐 Listening on: http://$DASHBOARD_BIND:$DASHBOARD_PORT"
echo "🔒 Max connections: $MAX_CONNECTIONS"
if [ -n "$API_TOKEN" ]; then
echo "🔐 API authentication: Enabled"
else
echo "⚠️ API authentication: Disabled"
fi
echo "💡 Press Ctrl+C to stop"
echo ""
# Main server loop with basic connection limiting
# Main server loop
while true; do
# Simple connection limiting
if [ "$CONNECTION_COUNT" -ge "$MAX_CONNECTIONS" ]; then
sleep 1
CONNECTION_COUNT=0
fi
# Wait for connection and parse request
REQUEST=$(echo "" | nc -l -p "$DASHBOARD_PORT" -s "$DASHBOARD_BIND" -w 5 2>/dev/null | head -1)
if [ -n "$REQUEST" ]; then
CONNECTION_COUNT=$((CONNECTION_COUNT + 1))
CONNECTION_COUNT=$(( (CONNECTION_COUNT + 1) % MAX_CONNECTIONS ))
# Reset counter if needed
[ "$CONNECTION_COUNT" -eq 0 ] && sleep 1
# Accept connection using a single listener
nc -lk -p "$DASHBOARD_PORT" -s "$DASHBOARD_BIND" -w 5 | while read -r REQUEST; do
# Only process first line
PATH_REQ=$(echo "$REQUEST" | awk '{print $2}')
# Extract path from request
REQUEST_PATH=$(echo "$REQUEST" | awk '{print $2}')
# Extract Authorization header if present
AUTH_HEADER=""
while read -r header; do
[ -z "$header" ] && break
case "$header" in
Authorization:*) AUTH_HEADER="$header" ;;
esac
done
# Log request
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $REQUEST"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Request: $PATH_REQ"
# Generate and send response in background to avoid blocking
(handle_api "$REQUEST_PATH" | nc -l -p "$DASHBOARD_PORT" -s "$DASHBOARD_BIND" -w 1 > /dev/null 2>&1) &
fi
done
# Generate and send response directly
handle_api "$PATH_REQ" "$AUTH_HEADER"
break
done
done

View File

@@ -4,7 +4,6 @@
set -e
# Configuration
VERSION="1.1.0"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
FINGERPRINT_FILE="${FINGERPRINT_FILE:-/var/lib/tor/fingerprint}"
@@ -17,11 +16,10 @@ for arg in "$@"; do
cat << EOF
🔑 Tor-Guard-Relay Fingerprint Tool v${VERSION}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
USAGE:
Usage:
fingerprint [OPTIONS]
OPTIONS:
Options:
--json Output in JSON format
--plain Plain text output
--copy Output for easy copying
@@ -29,106 +27,76 @@ OPTIONS:
--no-links Hide monitoring links
--help, -h Show this help message
ENVIRONMENT VARIABLES:
Environment:
OUTPUT_FORMAT Output format (text/json/plain)
FINGERPRINT_FILE Path to fingerprint file
SHOW_LINKS Show monitoring links (true/false)
OUTPUT FORMATS:
text Human-readable with emojis and links
json Machine-readable JSON
plain Simple text for scripts
copy Formatted for clipboard copying
MONITORING LINKS:
• Tor Metrics (clearnet)
• Onion Metrics (Tor Browser only)
EXAMPLES:
fingerprint # Display with links
Examples:
fingerprint # Display formatted fingerprint
fingerprint --json # JSON output
fingerprint --copy # Copy-friendly format
fingerprint --plain # Script-friendly output
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0
;;
exit 0 ;;
--json) OUTPUT_FORMAT="json" ;;
--plain) OUTPUT_FORMAT="plain" ;;
--copy) OUTPUT_FORMAT="copy" ;;
--links) SHOW_LINKS="true" ;;
--no-links) SHOW_LINKS="false" ;;
-*)
echo "❌ Unknown option: $arg"
echo "💡 Use --help for usage information"
exit 2
;;
-*) echo "❌ Unknown option: $arg" >&2; exit 2 ;;
esac
done
# Check if fingerprint exists
# Check file existence
if [ ! -f "$FINGERPRINT_FILE" ]; then
case "$OUTPUT_FORMAT" in
json)
cat << EOF
{
"status": "not_ready",
"message": "Fingerprint not yet generated",
"fingerprint": null,
"nickname": null
}
EOF
;;
plain)
echo "NOT_READY"
;;
*)
echo "⚠️ Fingerprint not yet generated."
echo "📍 Tor is still bootstrapping or generating keys."
echo "💡 Check back in a few minutes."
;;
printf '{ "status":"not_ready", "message":"Fingerprint not yet generated" }\n' ;;
plain) echo "NOT_READY" ;;
*) echo "⚠️ Fingerprint not yet generated."
echo "📍 Tor is still bootstrapping or generating keys."
echo "💡 Check back in a few minutes." ;;
esac
exit 1
fi
# Read fingerprint
NICKNAME=$(awk '{print $1}' "$FINGERPRINT_FILE" 2>/dev/null || echo "")
FINGERPRINT=$(awk '{print $2}' "$FINGERPRINT_FILE" 2>/dev/null || echo "")
# Extract data safely
NICKNAME=$(awk 'NF {print $1; exit}' "$FINGERPRINT_FILE" 2>/dev/null || echo "")
FINGERPRINT=$(awk 'NF {print $2; exit}' "$FINGERPRINT_FILE" 2>/dev/null || echo "")
# Validate fingerprint format (40 hex characters)
if ! echo "$FINGERPRINT" | grep -qE "^[A-F0-9]{40}$"; then
# Validate fingerprint format (accept uppercase and lowercase)
if ! echo "$FINGERPRINT" | grep -qE '^[A-Fa-f0-9]{40}$'; then
case "$OUTPUT_FORMAT" in
json)
echo '{"status":"invalid","message":"Invalid fingerprint format"}'
;;
plain)
echo "INVALID"
;;
*)
echo "❌ Invalid fingerprint format detected"
;;
json) printf '{ "status":"invalid", "message":"Invalid fingerprint format" }\n' ;;
plain) echo "INVALID" ;;
*) echo "❌ Invalid fingerprint format detected" ;;
esac
exit 1
fi
# Generate formatted versions
FINGERPRINT_SPACED=$(echo "$FINGERPRINT" | sed 's/\(..\)/\1 /g' | sed 's/ $//')
FINGERPRINT_COLON=$(echo "$FINGERPRINT" | sed 's/\(..\)/\1:/g' | sed 's/:$//')
# Generate formatted variants
FINGERPRINT_SPACED=$(echo "$FINGERPRINT" | sed 's/\(..\)/\1 /g; s/ $//')
FINGERPRINT_COLON=$(echo "$FINGERPRINT" | sed 's/\(..\)/\1:/g; s/:$//')
# Get additional info if available
# Get creation time (portable fallback)
CREATION_TIME=""
if [ -f "$FINGERPRINT_FILE" ]; then
CREATION_TIME=$(stat -c %y "$FINGERPRINT_FILE" 2>/dev/null | cut -d' ' -f1,2 | cut -d'.' -f1 || echo "")
if command -v stat >/dev/null 2>&1; then
CREATION_TIME=$(stat -c %y "$FINGERPRINT_FILE" 2>/dev/null | cut -d'.' -f1 || true)
elif command -v date >/dev/null 2>&1; then
CREATION_TIME=$(date -r "$FINGERPRINT_FILE" 2>/dev/null || true)
fi
# Output based on format
# Escape JSON strings
escape_json() { printf '%s' "$1" | sed 's/"/\\"/g'; }
# Output formats
case "$OUTPUT_FORMAT" in
json)
cat << EOF
{
"status": "ready",
"nickname": "$NICKNAME",
"nickname": "$(escape_json "$NICKNAME")",
"fingerprint": "$FINGERPRINT",
"fingerprint_spaced": "$FINGERPRINT_SPACED",
"fingerprint_colon": "$FINGERPRINT_COLON",
@@ -140,21 +108,14 @@ case "$OUTPUT_FORMAT" in
}
EOF
;;
plain)
echo "$NICKNAME $FINGERPRINT"
;;
echo "$NICKNAME $FINGERPRINT" ;;
copy)
echo "$FINGERPRINT"
echo ""
echo "# Formatted versions:"
echo "# Spaced: $FINGERPRINT_SPACED"
echo "# Colon: $FINGERPRINT_COLON"
;;
echo "# Colon: $FINGERPRINT_COLON" ;;
*)
# Default text format with emojis
echo "🔑 Tor Relay Fingerprint"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
@@ -164,23 +125,19 @@ EOF
echo "📋 Formatted versions:"
echo " Spaced: $FINGERPRINT_SPACED"
echo " Colon: $FINGERPRINT_COLON"
if [ -n "$CREATION_TIME" ]; then
[ -n "$CREATION_TIME" ] && {
echo ""
echo "🕒 Created: $CREATION_TIME"
fi
}
if [ "$SHOW_LINKS" = "true" ]; then
echo ""
echo "🌐 Monitor your relay:"
echo ""
echo " 📊 Tor Metrics:"
echo " https://metrics.torproject.org/rs.html#search/$FINGERPRINT"
echo ""
echo " 🧅 Onion Metrics (Tor Browser only):"
echo " http://hctxrvjzfpvmzh2jllqhgvvkoepxb4kfzdjm6h7egcwlumggtktiftid.onion/rs.html#search/$FINGERPRINT"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💡 Tip: Use 'fingerprint --copy' for easy copying"

View File

@@ -4,14 +4,39 @@
set -e
# ──────────────────────────────────────────────
# Configuration
# ──────────────────────────────────────────────
VERSION="1.1.0"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
ENABLE_HEALTH_CHECK="${ENABLE_HEALTH_CHECK:-true}"
HEALTH_WEBHOOK_URL="${HEALTH_WEBHOOK_URL:-}"
CHECK_TIMEOUT="${CHECK_TIMEOUT:-5}"
VERBOSE="${VERBOSE:-false}"
LOG_LEVEL="${LOG_LEVEL:-warning}" # error, warning, info
# ──────────────────────────────────────────────
# Utility functions
# ──────────────────────────────────────────────
# Safe execution with error handling
safe() { "$@" 2>/dev/null || true; }
# Log messages based on verbosity level
log() {
level="$1"
message="$2"
if [ "$VERBOSE" = "true" ] || [ "$level" = "error" ]; then
case "$level" in
error) echo "❌ ERROR: $message" >&2 ;;
warn) echo "⚠️ WARNING: $message" >&2 ;;
info) echo " INFO: $message" >&2 ;;
esac
fi
}
# Format status with appropriate emoji
format_status() {
case "$1" in
ok|OK) echo "🟢 OK" ;;
@@ -21,10 +46,11 @@ format_status() {
esac
}
# Format IP status with address
format_ip_status() {
local type="$1"
local status="$2"
local addr="$3"
type="$1"
status="$2"
addr="$3"
if [ "$status" = "ok" ] && [ -n "$addr" ]; then
echo "🟢 OK ($addr)"
@@ -39,6 +65,96 @@ format_ip_status() {
fi
}
# Get public IP with multiple fallback methods (from status script)
get_public_ip() {
ip_type=$1 # "ipv4" or "ipv6"
ip=""
# Try multiple services in order of preference
if [ "$ip_type" = "ipv4" ]; then
# Try curl with multiple services
if command -v curl >/dev/null 2>&1; then
ip=$(curl -4 -s --max-time 5 --connect-timeout 3 https://api.ipify.org 2>/dev/null || true)
[ -z "$ip" ] && ip=$(curl -4 -s --max-time 5 --connect-timeout 3 https://ipinfo.io/ip 2>/dev/null || true)
[ -z "$ip" ] && ip=$(curl -4 -s --max-time 5 --connect-timeout 3 https://ipv4.icanhazip.com 2>/dev/null || true)
# Fallback to wget
elif command -v wget >/dev/null 2>&1; then
ip=$(wget -4 -q -O - --timeout=5 https://api.ipify.org 2>/dev/null || true)
[ -z "$ip" ] && ip=$(wget -4 -q -O - --timeout=5 https://ipinfo.io/ip 2>/dev/null || true)
[ -z "$ip" ] && ip=$(wget -4 -q -O - --timeout=5 https://ipv4.icanhazip.com 2>/dev/null || true)
fi
# Validate IPv4 format
if printf '%s' "$ip" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
printf '%s' "$ip"
fi
else # IPv6
# Try curl with multiple services
if command -v curl >/dev/null 2>&1; then
ip=$(curl -6 -s --max-time 5 --connect-timeout 3 https://api6.ipify.org 2>/dev/null || true)
[ -z "$ip" ] && ip=$(curl -6 -s --max-time 5 --connect-timeout 3 https://ipv6.icanhazip.com 2>/dev/null || true)
# Fallback to wget
elif command -v wget >/dev/null 2>&1; then
ip=$(wget -6 -q -O - --timeout=5 https://api6.ipify.org 2>/dev/null || true)
[ -z "$ip" ] && ip=$(wget -6 -q -O - --timeout=5 https://ipv6.icanhazip.com 2>/dev/null || true)
fi
# Basic IPv6 validation (simplified)
if printf '%s' "$ip" | grep -Eq '^[0-9a-fA-F:]+$'; then
printf '%s' "$ip"
fi
fi
}
# Check if a value is numeric
is_numeric() { echo "$1" | grep -qE '^[0-9]+$'; }
# Validate IP address format
validate_ip() {
type="$1"
ip="$2"
if [ -z "$ip" ]; then
return 1
fi
case "$type" in
ipv4)
echo "$ip" | grep -qE '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'
;;
ipv6)
echo "$ip" | grep -qE '^[0-9a-fA-F:]+$'
;;
*)
return 1
;;
esac
}
# Get port status
check_port_status() {
port="$1"
host="$2"
if [ -z "$port" ] || [ "$port" = "0" ]; then
echo "not_configured"
return
fi
if command -v nc >/dev/null 2>&1; then
if nc -z -w "$CHECK_TIMEOUT" "$host" "$port" 2>/dev/null; then
echo "open"
else
echo "closed"
fi
else
echo "skipped"
fi
}
# ──────────────────────────────────────────────
# Argument parsing
# ──────────────────────────────────────────────
for arg in "$@"; do
case "$arg" in
--help|-h)
@@ -46,19 +162,63 @@ for arg in "$@"; do
🧅 Tor-Guard-Relay Health Check v${VERSION}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
USAGE:
health [--json|--plain|--text|--webhook]
health [--json|--plain|--text|--webhook|--verbose]
OPTIONS:
--json Output in JSON format
--plain Plain text output (key=value)
--text Formatted text output (default)
--webhook Send health status to webhook
--verbose Show detailed execution information
--log-level Set log level (error|warning|info)
--help, -h Show this help message
ENVIRONMENT VARIABLES:
OUTPUT_FORMAT Output format (text/json/plain)
ENABLE_HEALTH_CHECK Enable health checks (true/false)
HEALTH_WEBHOOK_URL Webhook URL for health notifications
CHECK_TIMEOUT Network check timeout in seconds (default: 5)
VERBOSE Show verbose output (true/false)
LOG_LEVEL Minimum log level (error/warning/info)
EXIT CODES:
0 Relay is healthy or starting
1 Relay is down or has critical issues
2 Configuration or execution error
EXAMPLES:
health # Basic health check
health --json # JSON output
health --verbose # Verbose output
health --webhook # Send to webhook
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0 ;;
exit 0
;;
--json) OUTPUT_FORMAT="json" ;;
--plain) OUTPUT_FORMAT="plain" ;;
--text) OUTPUT_FORMAT="text" ;;
--webhook) SEND_WEBHOOK="true" ;;
--verbose) VERBOSE="true" ;;
--log-level)
shift
LOG_LEVEL="$1"
shift
;;
-*)
echo "❌ Unknown option: $arg" >&2
echo "💡 Use --help for usage information" >&2
exit 2
;;
esac
done
# Skip if disabled
# ──────────────────────────────────────────────
# Early exit if disabled
# ──────────────────────────────────────────────
if [ "$ENABLE_HEALTH_CHECK" != "true" ]; then
log "info" "Health checking is disabled"
case "$OUTPUT_FORMAT" in
json) echo '{"status":"disabled"}' ;;
plain) echo "DISABLED" ;;
@@ -67,7 +227,11 @@ if [ "$ENABLE_HEALTH_CHECK" != "true" ]; then
exit 0
fi
log "info" "Starting health check with format: $OUTPUT_FORMAT"
# ──────────────────────────────────────────────
# Initialize variables
# ──────────────────────────────────────────────
STATUS="unknown"
BOOTSTRAP_PERCENT=0
IS_RUNNING=false
@@ -80,65 +244,150 @@ WARNINGS=0
VERSION_INFO=""
ORPORT=""
DIRPORT=""
EXIT_RELAY="false"
RELAY_TYPE="guard"
PUBLIC_IP=""
PUBLIC_IP6=""
ORPORT_STATUS="unknown"
DIRPORT_STATUS="unknown"
# Inline Checks
# Tor process
# ──────────────────────────────────────────────
# Process check
# ──────────────────────────────────────────────
log "info" "Checking Tor process status"
if safe pgrep -x tor >/dev/null; then
IS_RUNNING=true
PID=$(safe pgrep -x tor | head -1)
UPTIME=$(safe ps -p "$PID" -o time= | tr -d ' ')
[ -z "$UPTIME" ] && UPTIME="0"
log "info" "Tor process found (PID: $PID, uptime: $UPTIME)"
else
log "error" "Tor process not found"
fi
# ──────────────────────────────────────────────
# Bootstrap progress
# ──────────────────────────────────────────────
log "info" "Checking bootstrap progress"
if [ -f /var/log/tor/notices.log ]; then
BOOTSTRAP_LINE=$(safe grep "Bootstrapped" /var/log/tor/notices.log | tail -1)
BOOTSTRAP_PERCENT=$(echo "$BOOTSTRAP_LINE" | grep -oE '[0-9]+%' | tr -d '%' | tail -1)
[ -z "$BOOTSTRAP_PERCENT" ] && BOOTSTRAP_PERCENT=0
BOOTSTRAP_PERCENT=${BOOTSTRAP_PERCENT:-0}
log "info" "Bootstrap progress: $BOOTSTRAP_PERCENT%"
else
log "warn" "Tor notices log not found"
fi
# ──────────────────────────────────────────────
# Reachability
# ──────────────────────────────────────────────
log "info" "Checking reachability"
if [ -f /var/log/tor/notices.log ]; then
if safe grep -q "reachable from the outside" /var/log/tor/notices.log; then
IS_REACHABLE=true
log "info" "Relay is reachable from outside"
else
log "warn" "Relay may not be reachable from outside"
fi
else
log "warn" "Cannot check reachability without log file"
fi
# Relay info
# ──────────────────────────────────────────────
# Relay identity
# ──────────────────────────────────────────────
log "info" "Getting relay identity"
if [ -f /var/lib/tor/fingerprint ]; then
NICKNAME=$(safe awk '{print $1}' /var/lib/tor/fingerprint)
FINGERPRINT=$(safe awk '{print $2}' /var/lib/tor/fingerprint)
log "info" "Relay identity: $NICKNAME ($FINGERPRINT)"
else
log "warn" "Fingerprint file not found"
fi
# ──────────────────────────────────────────────
# Network configuration and relay type
# ──────────────────────────────────────────────
log "info" "Reading network configuration"
if [ -f /etc/tor/torrc ]; then
ORPORT=$(safe grep -E "^ORPort" /etc/tor/torrc | awk '{print $2}' | head -1)
DIRPORT=$(safe grep -E "^DirPort" /etc/tor/torrc | awk '{print $2}' | head -1)
# Fixed relay type detection - check for exact matches
if safe grep -qE "^ExitRelay\s+1" /etc/tor/torrc; then
EXIT_RELAY="true"
RELAY_TYPE="exit"
elif safe grep -qE "^BridgeRelay\s+1" /etc/tor/torrc; then
RELAY_TYPE="bridge"
else
RELAY_TYPE="guard"
fi
log "info" "Relay type: $RELAY_TYPE, ORPort: $ORPORT, DirPort: $DIRPORT"
else
log "warn" "Tor configuration file not found"
fi
# ──────────────────────────────────────────────
# Version info
# ──────────────────────────────────────────────
log "info" "Getting version information"
if [ -f /build-info.txt ]; then
VERSION_INFO=$(safe head -1 /build-info.txt | cut -d: -f2- | tr -d ' ')
log "info" "Version: $VERSION_INFO"
else
log "warn" "Build info file not found"
fi
# IPv4/IPv6 check (added for visual consistency)
if command -v curl >/dev/null 2>&1; then
PUBLIC_IP=$(curl -4 -fsS --max-time "$CHECK_TIMEOUT" https://ipv4.icanhazip.com 2>/dev/null | tr -d '\r')
PUBLIC_IP6=$(curl -6 -fsS --max-time "$CHECK_TIMEOUT" https://ipv6.icanhazip.com 2>/dev/null | tr -d '\r')
# ──────────────────────────────────────────────
# Network IP checks (using failsafe method from status script)
# ──────────────────────────────────────────────
log "info" "Checking network connectivity"
if command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1; then
PUBLIC_IP=$(get_public_ip "ipv4")
PUBLIC_IP6=$(get_public_ip "ipv6")
if [ -n "$PUBLIC_IP" ]; then
log "info" "IPv4: $PUBLIC_IP"
else
log "warn" "Failed to get IPv4 address"
fi
if [ -n "$PUBLIC_IP6" ]; then
log "info" "IPv6: $PUBLIC_IP6"
else
log "warn" "Failed to get IPv6 address"
fi
else
log "warn" "Neither curl nor wget available for network checks"
fi
# Count issues
# ──────────────────────────────────────────────
# Port status checks
# ──────────────────────────────────────────────
if [ -n "$PUBLIC_IP" ]; then
log "info" "Checking port status"
ORPORT_STATUS=$(check_port_status "$ORPORT" "$PUBLIC_IP")
DIRPORT_STATUS=$(check_port_status "$DIRPORT" "$PUBLIC_IP")
log "info" "ORPort status: $ORPORT_STATUS"
log "info" "DirPort status: $DIRPORT_STATUS"
fi
# ──────────────────────────────────────────────
# Error and warning counts
# ──────────────────────────────────────────────
log "info" "Counting errors and warnings"
if [ -f /var/log/tor/notices.log ]; then
ERRORS=$(safe grep -ciE "\[err\]" /var/log/tor/notices.log)
WARNINGS=$(safe grep -ciE "\[warn\]" /var/log/tor/notices.log)
log "info" "Errors: $ERRORS, Warnings: $WARNINGS"
else
log "warn" "Cannot count errors without log file"
fi
# ──────────────────────────────────────────────
# Determine overall status
# ──────────────────────────────────────────────
log "info" "Determining overall status"
if [ "$IS_RUNNING" = false ]; then
STATUS="down"
elif [ "$BOOTSTRAP_PERCENT" -eq 100 ] && [ "$IS_REACHABLE" = true ]; then
@@ -151,53 +400,56 @@ else
STATUS="unknown"
fi
log "info" "Overall status: $STATUS"
TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date)
# Output
# ──────────────────────────────────────────────
# Output formatting
# ──────────────────────────────────────────────
case "$OUTPUT_FORMAT" in
json)
cat << EOF
{
"status": "$STATUS",
"timestamp": "$TIMESTAMP",
"process": {
"running": $IS_RUNNING,
"uptime": "$UPTIME"
},
"bootstrap": {
"percent": $BOOTSTRAP_PERCENT,
"complete": $([ "$BOOTSTRAP_PERCENT" -eq 100 ] && echo "true" || echo "false")
},
"network": {
"reachable": $IS_REACHABLE,
"orport": "$ORPORT",
"process": { "running": $IS_RUNNING, "uptime": "$UPTIME" },
"bootstrap": { "percent": $BOOTSTRAP_PERCENT, "complete": $([ "$BOOTSTRAP_PERCENT" -eq 100 ] && echo "true" || echo "false") },
"network": {
"reachable": $IS_REACHABLE,
"orport": "$ORPORT",
"dirport": "$DIRPORT",
"ipv4": "$PUBLIC_IP",
"ipv6": "$PUBLIC_IP6"
"orport_status": "$ORPORT_STATUS",
"dirport_status": "$DIRPORT_STATUS",
"ipv4": "$PUBLIC_IP",
"ipv6": "$PUBLIC_IP6"
},
"relay": {
"nickname": "$NICKNAME",
"fingerprint": "$FINGERPRINT",
"exit_relay": $EXIT_RELAY,
"version": "$VERSION_INFO"
"relay": {
"nickname": "$NICKNAME",
"fingerprint": "$FINGERPRINT",
"type": "$RELAY_TYPE",
"version": "$VERSION_INFO"
},
"issues": {
"errors": $ERRORS,
"warnings": $WARNINGS
}
"issues": { "errors": $ERRORS, "warnings": $WARNINGS }
}
EOF
;;
plain)
echo "STATUS=$STATUS"
echo "TIMESTAMP=$TIMESTAMP"
echo "RUNNING=$IS_RUNNING"
echo "UPTIME=$UPTIME"
echo "BOOTSTRAP=$BOOTSTRAP_PERCENT"
echo "REACHABLE=$IS_REACHABLE"
echo "NICKNAME=$NICKNAME"
echo "FINGERPRINT=$FINGERPRINT"
echo "RELAY_TYPE=$RELAY_TYPE"
echo "ERRORS=$ERRORS"
echo "WARNINGS=$WARNINGS"
echo "UPTIME=$UPTIME"
echo "ORPORT=$ORPORT"
echo "DIRPORT=$DIRPORT"
echo "ORPORT_STATUS=$ORPORT_STATUS"
echo "DIRPORT_STATUS=$DIRPORT_STATUS"
echo "IPV4=$PUBLIC_IP"
echo "IPV6=$PUBLIC_IP6"
;;
@@ -237,14 +489,40 @@ EOF
fi
[ -n "$PUBLIC_IP" ] && echo " 🌐 IPv4: 🟢 OK ($PUBLIC_IP)" || echo " 🌐 IPv4: 🔴 No IPv4 connectivity"
[ -n "$PUBLIC_IP6" ] && echo " 🌐 IPv6: 🟢 OK ($PUBLIC_IP6)" || echo " 🌐 IPv6: 🔴 No IPv6 connectivity"
[ -n "$ORPORT" ] && echo " 📍 ORPort: $ORPORT" || echo " 📍 ORPort: 🔴 Not configured"
[ -n "$DIRPORT" ] && echo " 📍 DirPort: $DIRPORT" || echo " 📍 DirPort: 🔴 Not configured"
# Port status with better formatting
if [ -n "$ORPORT" ]; then
case "$ORPORT_STATUS" in
open) echo " 📍 ORPort: 🟢 Open ($ORPORT)" ;;
closed) echo " 📍 ORPort: 🔴 Closed ($ORPORT)" ;;
not_configured) echo " 📍 ORPort: ⏭️ Not configured" ;;
*) echo " 📍 ORPort: ❓ Unknown ($ORPORT)" ;;
esac
else
echo " 📍 ORPort: 🔴 Not configured"
fi
if [ -n "$DIRPORT" ]; then
case "$DIRPORT_STATUS" in
open) echo " 📍 DirPort: 🟢 Open ($DIRPORT)" ;;
closed) echo " 📍 DirPort: 🔴 Closed ($DIRPORT)" ;;
not_configured) echo " 📍 DirPort: ⏭️ Not configured" ;;
*) echo " 📍 DirPort: ❓ Unknown ($DIRPORT)" ;;
esac
else
echo " 📍 DirPort: 🔴 Not configured"
fi
echo ""
if [ -n "$NICKNAME" ] || [ -n "$FINGERPRINT" ]; then
echo "🔑 Relay Identity:"
[ -n "$NICKNAME" ] && echo " 📝 Nickname: $NICKNAME"
[ -n "$FINGERPRINT" ] && echo " 🆔 Fingerprint: $FINGERPRINT"
[ "$EXIT_RELAY" = "true" ] && echo " 🚪 Type: Exit Relay" || echo " 🔒 Type: Guard/Middle Relay"
case "$RELAY_TYPE" in
bridge) echo " 🌉 Type: Bridge Relay" ;;
exit) echo " 🚪 Type: Exit Relay" ;;
guard) echo " 🔒 Type: Guard/Middle Relay" ;;
*) echo " ❓ Type: Unknown" ;;
esac
echo ""
fi
if [ "$ERRORS" -gt 0 ] || [ "$WARNINGS" -gt 0 ]; then
@@ -258,15 +536,35 @@ EOF
;;
esac
# ──────────────────────────────────────────────
# Optional webhook support
# ──────────────────────────────────────────────
if [ "$SEND_WEBHOOK" = "true" ] && [ -n "$HEALTH_WEBHOOK_URL" ]; then
log "info" "Sending health status to webhook"
if command -v curl >/dev/null 2>&1; then
/usr/local/bin/health --json | curl -s -X POST "$HEALTH_WEBHOOK_URL" \
-H "Content-Type: application/json" -d @- >/dev/null 2>&1
if [ $? -eq 0 ]; then
log "info" "Webhook sent successfully"
else
log "error" "Failed to send webhook"
fi
else
log "error" "curl not available for webhook"
fi
fi
# ──────────────────────────────────────────────
# Exit code mapping
# ──────────────────────────────────────────────
log "info" "Health check completed with status: $STATUS"
case "$STATUS" in
healthy|running|starting) exit 0 ;;
*) exit 1 ;;
healthy|running|starting)
log "info" "Exiting with code 0 (healthy)"
exit 0
;;
*)
log "error" "Exiting with code 1 (unhealthy)"
exit 1
;;
esac

View File

@@ -1,316 +1,86 @@
#!/bin/sh
# metrics-http - HTTP server for Prometheus metrics endpoint
# Usage: metrics-http [--port PORT] [--help]
set -e
# Configuration
VERSION="1.1.1"
VERSION="1.1.0"
METRICS_PORT="${METRICS_PORT:-9052}"
METRICS_BIND="${METRICS_BIND:-127.0.0.1}" # ⚠️ CHANGED: Secure default
METRICS_BIND="${METRICS_BIND:-127.0.0.1}"
METRICS_PATH="${METRICS_PATH:-/metrics}"
ENABLE_METRICS="${ENABLE_METRICS:-true}"
RESPONSE_TIMEOUT="${RESPONSE_TIMEOUT:-10}"
MAX_CONNECTIONS="${MAX_CONNECTIONS:-10}" # 🔒 NEW: Rate limiting
MAX_CONNECTIONS="${MAX_CONNECTIONS:-10}"
# Trap for clean exit
trap 'cleanup' INT TERM
trap 'echo; echo "🛑 Metrics HTTP server shutting down..."; exit 0' INT TERM
cleanup() {
echo ""
echo "🛑 Metrics HTTP server shutting down..."
exit 0
}
# Parse arguments
for arg in "$@"; do
case "$arg" in
--help|-h)
cat << EOF
🌐 Tor-Guard-Relay Metrics HTTP Server v${VERSION}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
USAGE:
metrics-http [OPTIONS]
OPTIONS:
--port PORT Listen port (default: 9052)
--bind ADDR Bind address (default: 127.0.0.1)
--path PATH Metrics path (default: /metrics)
--daemon Run as daemon
--help, -h Show this help message
ENVIRONMENT VARIABLES:
METRICS_PORT Port to listen on (default: 9052)
METRICS_BIND Address to bind (default: 127.0.0.1)
METRICS_PATH URL path for metrics (default: /metrics)
ENABLE_METRICS Enable metrics server (true/false)
RESPONSE_TIMEOUT Response timeout in seconds
MAX_CONNECTIONS Max concurrent connections (default: 10)
⚠️ SECURITY NOTICE:
Default binding is 127.0.0.1 (localhost only).
To expose externally for Prometheus scraping, explicitly set:
METRICS_BIND=0.0.0.0
⚠️ WARNING: Metrics may contain sensitive relay information!
Recommendations for production:
1. Use network-level access controls (firewall rules)
2. Deploy Prometheus in same network/VPN
3. Use TLS termination proxy (nginx with client certs)
4. Never expose directly to public internet
ENDPOINTS:
http://localhost:9052/metrics Prometheus metrics
http://localhost:9052/health Health check endpoint
http://localhost:9052/ Status page
PROMETHEUS CONFIG:
scrape_configs:
- job_name: 'tor-relay'
static_configs:
- targets: ['relay:9052']
metrics_path: '/metrics'
scrape_interval: 30s
DOCKER INTEGRATION:
# For localhost access only (secure):
ports:
- "127.0.0.1:9052:9052"
# For Prometheus in same Docker network:
networks:
- monitoring
# No port exposure needed!
# For external Prometheus (use with caution):
environment:
- METRICS_BIND=0.0.0.0
ports:
- "9052:9052"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0
;;
--port)
shift
METRICS_PORT="$1"
shift
;;
--bind)
shift
METRICS_BIND="$1"
shift
;;
--path)
shift
METRICS_PATH="$1"
shift
;;
--daemon)
DAEMON_MODE="true"
;;
-*)
echo "❌ Unknown option: $arg"
echo "💡 Use --help for usage information"
exit 2
;;
esac
done
# Check if metrics are enabled
if [ "$ENABLE_METRICS" != "true" ]; then
echo "📊 Metrics HTTP server is disabled"
echo "📊 Metrics HTTP server disabled"
echo "💡 Set ENABLE_METRICS=true to enable"
exit 0
fi
# Security warning for external binding
if [ "$METRICS_BIND" = "0.0.0.0" ]; then
echo "⚠️ WARNING: Metrics server is bound to 0.0.0.0 (all interfaces)"
echo "⚠️ Relay metrics may contain sensitive information!"
echo "⚠️ Ensure proper firewall rules or use a secure network."
echo ""
fi
# Check for netcat
if ! command -v nc > /dev/null 2>&1; then
echo "❌ Error: netcat (nc) is required but not installed"
if ! command -v nc >/dev/null 2>&1; then
echo "❌ Error: netcat (nc) not found"
echo "💡 Install with: apk add netcat-openbsd"
exit 1
fi
# Connection counter for basic rate limiting
CONNECTION_COUNT=0
LAST_RESET=$(date +%s)
if [ "$METRICS_BIND" = "0.0.0.0" ]; then
echo "⚠️ WARNING: Bound to all interfaces"
echo "⚠️ Ensure firewall rules restrict access"
echo ""
fi
# Function to generate HTTP response
generate_response() {
REQUEST_PATH="$1"
case "$REQUEST_PATH" in
"$METRICS_PATH")
# Generate metrics
METRICS_OUTPUT=$(/usr/local/bin/metrics 2>/dev/null || echo "# Error generating metrics")
CONTENT_LENGTH=$(echo -n "$METRICS_OUTPUT" | wc -c)
cat << EOF
HTTP/1.1 200 OK
Content-Type: text/plain; version=0.0.4
Content-Length: $CONTENT_LENGTH
Cache-Control: no-cache
Connection: close
X-Content-Type-Options: nosniff
$METRICS_OUTPUT
EOF
;;
"/health")
# Health check endpoint
HEALTH_JSON=$(/usr/local/bin/health --json 2>/dev/null || echo '{"status":"error"}')
CONTENT_LENGTH=$(echo -n "$HEALTH_JSON" | wc -c)
cat << EOF
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: $CONTENT_LENGTH
Cache-Control: no-cache
Connection: close
$HEALTH_JSON
EOF
;;
"/")
# Status page
HTML_CONTENT="<!DOCTYPE html>
<html>
<head>
<title>Tor Relay Metrics</title>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<style>
body { font-family: sans-serif; margin: 40px; background: #f5f5f5; }
h1 { color: #7d4698; }
.status { padding: 20px; background: white; border-radius: 8px; margin: 20px 0; }
.endpoint { background: #f0f0f0; padding: 10px; margin: 10px 0; border-radius: 4px; }
a { color: #7d4698; text-decoration: none; }
a:hover { text-decoration: underline; }
.warning { background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0; }
</style>
</head>
<body>
<h1>🧅 Tor Relay Metrics Server</h1>
<div class=\"warning\">
<strong>⚠️ Security Notice:</strong> This server exposes relay metrics.
Ensure it's only accessible from trusted networks (Prometheus, monitoring systems).
</div>
<div class=\"status\">
<h2>Available Endpoints:</h2>
<div class=\"endpoint\">
📊 <a href=\"$METRICS_PATH\">$METRICS_PATH</a> - Prometheus metrics
</div>
<div class=\"endpoint\">
💚 <a href=\"/health\">/health</a> - Health check (JSON)
</div>
<div class=\"endpoint\">
🏠 <a href=\"/\">/</a> - This status page
</div>
</div>
<div class=\"status\">
<h2>Configuration:</h2>
<p><strong>Bind Address:</strong> $METRICS_BIND</p>
<p><strong>Port:</strong> $METRICS_PORT</p>
<p><strong>Version:</strong> $VERSION</p>
<p><strong>Rate Limit:</strong> $MAX_CONNECTIONS connections/window</p>
</div>
<div class=\"status\">
<h2>Integration:</h2>
<p>Add to your <code>prometheus.yml</code>:</p>
<pre style=\"background: #f0f0f0; padding: 15px; border-radius: 4px; overflow-x: auto;\">
scrape_configs:
- job_name: 'tor-relay'
static_configs:
- targets: ['$METRICS_BIND:$METRICS_PORT']
metrics_path: '$METRICS_PATH'
scrape_interval: 30s</pre>
</div>
</body>
</html>"
CONTENT_LENGTH=$(echo -n "$HTML_CONTENT" | wc -c)
cat << EOF
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: $CONTENT_LENGTH
Cache-Control: no-cache
Connection: close
$HTML_CONTENT
EOF
;;
*)
# 404 Not Found
ERROR_MSG="404 - Not Found"
CONTENT_LENGTH=$(echo -n "$ERROR_MSG" | wc -c)
cat << EOF
HTTP/1.1 404 Not Found
Content-Type: text/plain
Content-Length: $CONTENT_LENGTH
Connection: close
$ERROR_MSG
EOF
;;
esac
}
# Start server
echo "📊 Starting Tor Relay Metrics HTTP Server v${VERSION}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🌐 Listening on: http://$METRICS_BIND:$METRICS_PORT"
echo "📍 Metrics path: $METRICS_PATH"
echo "🔒 Max connections: $MAX_CONNECTIONS/window"
echo "🔒 Max connections: $MAX_CONNECTIONS/min"
echo "💡 Press Ctrl+C to stop"
echo ""
# Main server loop with connection limiting
CONNECTION_COUNT=0
LAST_RESET=$(date +%s)
handle_request() {
REQ_PATH="$1"
case "$REQ_PATH" in
"$METRICS_PATH")
METRICS=$(/usr/local/bin/metrics 2>/dev/null || echo "# Error generating metrics")
printf "HTTP/1.1 200 OK\r\nContent-Type: text/plain; version=0.0.4\r\nConnection: close\r\n\r\n%s" "$METRICS"
;;
"/health")
HEALTH=$(/usr/local/bin/health --json 2>/dev/null || echo '{"status":"error"}')
printf "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n%s" "$HEALTH"
;;
"/")
HTML="<!DOCTYPE html><html><head><title>Tor Relay Metrics</title>
<style>body{font-family:sans-serif;margin:40px;background:#f5f5f5}h1{color:#7d4698}
.b{background:#fff;border-radius:8px;padding:20px;margin:20px 0}</style></head><body>
<h1>🧅 Tor Relay Metrics Server</h1>
<div class='b'><b>Metrics:</b> <a href='$METRICS_PATH'>$METRICS_PATH</a></div>
<div class='b'><b>Health:</b> <a href='/health'>/health</a></div>
<div class='b'><b>Version:</b> $VERSION</div>
</body></html>"
printf "HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n%s" "$HTML"
;;
*)
printf "HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n404 - Not Found"
;;
esac
}
while true; do
# Reset counter every 60 seconds
CURRENT_TIME=$(date +%s)
if [ $((CURRENT_TIME - LAST_RESET)) -ge 60 ]; then
CONNECTION_COUNT=0
LAST_RESET=$CURRENT_TIME
fi
# Basic rate limiting
if [ "$CONNECTION_COUNT" -ge "$MAX_CONNECTIONS" ]; then
sleep 1
continue
fi
# Wait for connection and parse request
REQUEST=$(echo "" | nc -l -p "$METRICS_PORT" -s "$METRICS_BIND" -w "$RESPONSE_TIMEOUT" 2>/dev/null | head -1)
if [ -n "$REQUEST" ]; then
CURRENT=$(date +%s)
[ $((CURRENT - LAST_RESET)) -ge 60 ] && CONNECTION_COUNT=0 && LAST_RESET=$CURRENT
[ "$CONNECTION_COUNT" -ge "$MAX_CONNECTIONS" ] && sleep 1 && continue
# Read request
REQ_LINE=$(nc -lk -p "$METRICS_PORT" -s "$METRICS_BIND" -w "$RESPONSE_TIMEOUT" | head -n 1)
if [ -n "$REQ_LINE" ]; then
PATH_REQ=$(echo "$REQ_LINE" | awk '{print $2}')
CONNECTION_COUNT=$((CONNECTION_COUNT + 1))
# Extract path from request
REQUEST_PATH=$(echo "$REQUEST" | awk '{print $2}')
# Log request (without sensitive info)
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $REQUEST_PATH ($CONNECTION_COUNT/$MAX_CONNECTIONS)"
# Generate and send response in background
(generate_response "$REQUEST_PATH" | nc -l -p "$METRICS_PORT" -s "$METRICS_BIND" -w 1 > /dev/null 2>&1) &
echo "[$(date '+%H:%M:%S')] $PATH_REQ ($CONNECTION_COUNT/$MAX_CONNECTIONS)"
handle_request "$PATH_REQ"
fi
done
done

View File

@@ -1,10 +1,9 @@
#!/bin/sh
# metrics - Prometheus-compatible metrics exporter for Tor relay
# Usage: docker exec guard-relay metrics [--help]
# Usage: docker exec guard-relay metrics [--json|--help]
set -e
# Configuration
VERSION="1.1.0"
METRICS_PREFIX="${METRICS_PREFIX:-tor_relay}"
INCLUDE_LABELS="${INCLUDE_LABELS:-true}"
@@ -17,58 +16,28 @@ for arg in "$@"; do
cat << EOF
📊 Tor-Guard-Relay Metrics Exporter v${VERSION}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Usage:
metrics [--prometheus|--json|--help]
USAGE:
metrics [OPTIONS]
OPTIONS:
Options:
--prometheus Output in Prometheus format (default)
--json Output metrics as JSON
--help, -h Show this help message
--json Output metrics as JSON
--help, -h Show this message
ENVIRONMENT VARIABLES:
METRICS_PREFIX Prefix for metric names (default: tor_relay)
Environment:
METRICS_PREFIX Prefix for metric names
INCLUDE_LABELS Include labels in output (true/false)
METRICS_FORMAT Output format (prometheus/json)
METRICS EXPORTED:
• ${METRICS_PREFIX}_up Relay status (0/1)
• ${METRICS_PREFIX}_bootstrap_percent Bootstrap progress
• ${METRICS_PREFIX}_reachable Reachability status
• ${METRICS_PREFIX}_uptime_seconds Process uptime
• ${METRICS_PREFIX}_errors_total Total error count
• ${METRICS_PREFIX}_warnings_total Total warning count
• ${METRICS_PREFIX}_bandwidth_read_bytes Bytes read
• ${METRICS_PREFIX}_bandwidth_write_bytes Bytes written
• ${METRICS_PREFIX}_circuits_total Active circuits
PROMETHEUS INTEGRATION:
# prometheus.yml
scrape_configs:
- job_name: 'tor-relay'
static_configs:
- targets: ['relay:9052']
EXAMPLES:
metrics # Prometheus format output
metrics --json # JSON metrics
curl localhost:9052/metrics # HTTP endpoint
METRICS_FORMAT prometheus or json
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0
;;
exit 0 ;;
--prometheus) METRICS_FORMAT="prometheus" ;;
--json) METRICS_FORMAT="json" ;;
-*)
echo "# ERROR: Unknown option: $arg"
echo "# Use --help for usage information"
exit 2
;;
-*) echo "# ERROR: Unknown option: $arg" >&2; exit 2 ;;
esac
done
# Initialize metrics
# Initialize
RELAY_UP=0
BOOTSTRAP_PERCENT=0
IS_REACHABLE=0
@@ -78,67 +47,60 @@ WARNING_COUNT=0
BANDWIDTH_READ=0
BANDWIDTH_WRITE=0
CIRCUITS_ACTIVE=0
NICKNAME=""
NICKNAME="unknown"
FINGERPRINT=""
VERSION_INFO=""
VERSION_INFO="unknown"
# Get relay identity
# Identity
if [ -f /var/lib/tor/fingerprint ]; then
NICKNAME=$(awk '{print $1}' /var/lib/tor/fingerprint 2>/dev/null || echo "unknown")
FINGERPRINT=$(awk '{print $2}' /var/lib/tor/fingerprint 2>/dev/null || echo "")
fi
# Check if Tor is running
if pgrep -x tor > /dev/null 2>&1; then
# Process state
if pgrep -x tor >/dev/null 2>&1; then
RELAY_UP=1
# Calculate uptime in seconds
PID=$(pgrep -x tor | head -1)
if [ -n "$PID" ]; then
# Get process start time
if [ -f "/proc/$PID/stat" ]; then
STARTTIME=$(awk '{print $22}' "/proc/$PID/stat" 2>/dev/null || echo 0)
UPTIME_TICKS=$(($(cat /proc/uptime | cut -d. -f1) * 100))
if [ "$STARTTIME" -gt 0 ]; then
UPTIME_SECONDS=$(((UPTIME_TICKS - STARTTIME) / 100))
fi
fi
if [ -n "$PID" ] && [ -r /proc/$PID/stat ]; then
START_TICKS=$(awk '{print $22}' /proc/$PID/stat)
HZ=$(getconf CLK_TCK 2>/dev/null || echo 100)
SYSTEM_UPTIME=$(awk '{print int($1)}' /proc/uptime)
PROC_UPTIME=$((SYSTEM_UPTIME - START_TICKS / HZ))
[ "$PROC_UPTIME" -ge 0 ] 2>/dev/null && UPTIME_SECONDS=$PROC_UPTIME || UPTIME_SECONDS=0
fi
fi
# Parse bootstrap percentage
# Logs
if [ -f /var/log/tor/notices.log ]; then
BOOTSTRAP_LINE=$(grep "Bootstrapped" /var/log/tor/notices.log 2>/dev/null | tail -1)
if [ -n "$BOOTSTRAP_LINE" ]; then
BOOTSTRAP_PERCENT=$(echo "$BOOTSTRAP_LINE" | grep -oE '[0-9]+%' | tr -d '%' | tail -1)
[ -z "$BOOTSTRAP_PERCENT" ] && BOOTSTRAP_PERCENT=0
fi
# Check reachability
if grep -q "reachable from the outside" /var/log/tor/notices.log 2>/dev/null; then
IS_REACHABLE=1
fi
# Count errors and warnings
ERROR_COUNT=$(grep -cE "\[err\]|\[error\]" /var/log/tor/notices.log 2>/dev/null || echo 0)
WARNING_COUNT=$(grep -cE "\[warn\]|\[warning\]" /var/log/tor/notices.log 2>/dev/null || echo 0)
BOOTSTRAP_PERCENT=$(echo "$BOOTSTRAP_LINE" | grep -oE '[0-9]+' | tail -1)
BOOTSTRAP_PERCENT=$(printf '%s' "$BOOTSTRAP_PERCENT" | tr -cd '0-9')
[ -z "$BOOTSTRAP_PERCENT" ] && BOOTSTRAP_PERCENT=0
grep -q "reachable from the outside" /var/log/tor/notices.log 2>/dev/null && IS_REACHABLE=1
ERROR_COUNT=$(grep -ciE "\[err\]|\[error\]" /var/log/tor/notices.log 2>/dev/null || echo 0)
WARNING_COUNT=$(grep -ciE "\[warn\]|\[warning\]" /var/log/tor/notices.log 2>/dev/null || echo 0)
fi
# Parse bandwidth from state file
# Bandwidth
if [ -f /var/lib/tor/state ]; then
BANDWIDTH_READ=$(grep "^AccountingBytesReadInterval" /var/lib/tor/state 2>/dev/null | awk '{print $2}' || echo 0)
BANDWIDTH_WRITE=$(grep "^AccountingBytesWrittenInterval" /var/lib/tor/state 2>/dev/null | awk '{print $2}' || echo 0)
BANDWIDTH_READ=$(awk '/AccountingBytesReadInterval/ {print $2}' /var/lib/tor/state | tail -1)
BANDWIDTH_WRITE=$(awk '/AccountingBytesWrittenInterval/ {print $2}' /var/lib/tor/state | tail -1)
BANDWIDTH_READ=${BANDWIDTH_READ:-0}
BANDWIDTH_WRITE=${BANDWIDTH_WRITE:-0}
fi
# Get version info
# Version
if [ -f /build-info.txt ]; then
VERSION_INFO=$(head -1 /build-info.txt 2>/dev/null | cut -d: -f2- | tr -d ' ' || echo "unknown")
VERSION_INFO=$(awk -F: '/Version/ {print $2}' /build-info.txt | tr -d ' ')
VERSION_INFO=${VERSION_INFO:-unknown}
fi
# Generate timestamp
TIMESTAMP=$(date +%s)000
# Timestamp (ms)
TIMESTAMP=$(($(date +%s) * 1000))
# Output based on format
# Output
case "$METRICS_FORMAT" in
json)
cat << EOF
@@ -163,51 +125,47 @@ case "$METRICS_FORMAT" in
}
EOF
;;
*)
# Prometheus format (default)
echo "# HELP ${METRICS_PREFIX}_up Tor relay status (1 = up, 0 = down)"
echo "# TYPE ${METRICS_PREFIX}_up gauge"
if [ "$INCLUDE_LABELS" = "true" ] && [ -n "$NICKNAME" ]; then
echo "${METRICS_PREFIX}_up{nickname=\"$NICKNAME\",fingerprint=\"$FINGERPRINT\"} $RELAY_UP"
else
[ "$INCLUDE_LABELS" = "true" ] && \
echo "${METRICS_PREFIX}_up{nickname=\"$NICKNAME\",fingerprint=\"$FINGERPRINT\"} $RELAY_UP" || \
echo "${METRICS_PREFIX}_up $RELAY_UP"
fi
echo "# HELP ${METRICS_PREFIX}_bootstrap_percent Bootstrap completion percentage"
echo "# TYPE ${METRICS_PREFIX}_bootstrap_percent gauge"
echo "${METRICS_PREFIX}_bootstrap_percent $BOOTSTRAP_PERCENT"
echo "# HELP ${METRICS_PREFIX}_reachable Relay reachability status"
echo "# TYPE ${METRICS_PREFIX}_reachable gauge"
echo "${METRICS_PREFIX}_reachable $IS_REACHABLE"
echo "# HELP ${METRICS_PREFIX}_uptime_seconds Relay process uptime in seconds"
echo "# TYPE ${METRICS_PREFIX}_uptime_seconds counter"
echo "${METRICS_PREFIX}_uptime_seconds $UPTIME_SECONDS"
echo "# HELP ${METRICS_PREFIX}_errors_total Total number of errors in log"
echo "# HELP ${METRICS_PREFIX}_errors_total Total number of errors"
echo "# TYPE ${METRICS_PREFIX}_errors_total counter"
echo "${METRICS_PREFIX}_errors_total $ERROR_COUNT"
echo "# HELP ${METRICS_PREFIX}_warnings_total Total number of warnings in log"
echo "# HELP ${METRICS_PREFIX}_warnings_total Total number of warnings"
echo "# TYPE ${METRICS_PREFIX}_warnings_total counter"
echo "${METRICS_PREFIX}_warnings_total $WARNING_COUNT"
echo "# HELP ${METRICS_PREFIX}_bandwidth_read_bytes Total bytes read"
echo "# HELP ${METRICS_PREFIX}_bandwidth_read_bytes Bytes read during current interval"
echo "# TYPE ${METRICS_PREFIX}_bandwidth_read_bytes counter"
echo "${METRICS_PREFIX}_bandwidth_read_bytes $BANDWIDTH_READ"
echo "# HELP ${METRICS_PREFIX}_bandwidth_write_bytes Total bytes written"
echo "# HELP ${METRICS_PREFIX}_bandwidth_write_bytes Bytes written during current interval"
echo "# TYPE ${METRICS_PREFIX}_bandwidth_write_bytes counter"
echo "${METRICS_PREFIX}_bandwidth_write_bytes $BANDWIDTH_WRITE"
echo "# HELP ${METRICS_PREFIX}_circuits_total Active circuit count"
echo "# HELP ${METRICS_PREFIX}_circuits_total Active circuit count (placeholder)"
echo "# TYPE ${METRICS_PREFIX}_circuits_total gauge"
echo "${METRICS_PREFIX}_circuits_total $CIRCUITS_ACTIVE"
echo "# HELP ${METRICS_PREFIX}_info Relay information"
echo "# TYPE ${METRICS_PREFIX}_info gauge"
echo "${METRICS_PREFIX}_info{nickname=\"$NICKNAME\",version=\"$VERSION_INFO\"} 1"
;;
esac
esac

View File

@@ -9,11 +9,16 @@ OUTPUT_FORMAT="text"
CHECK_IPV4="true"
CHECK_IPV6="true"
CHECK_DNS="true"
CHECK_CONSENSUS="false"
CHECK_CONSENSUS="false" # Disabled by default
CHECK_PORTS="true"
DNS_SERVERS="194.242.2.2 94.140.14.14 9.9.9.9"
TEST_TIMEOUT="5"
# ──────────────────────────────────────────────
# Helpers
# ──────────────────────────────────────────────
safe() { "$@" 2>/dev/null || true; }
format_status() {
case "$1" in
ok|OK) echo "🟢 OK" ;;
@@ -41,7 +46,11 @@ format_ip_status() {
fi
}
# Parse arguments
command_exists() { command -v "$1" >/dev/null 2>&1; }
# ──────────────────────────────────────────────
# Argument parsing
# ──────────────────────────────────────────────
for arg in "$@"; do
case "$arg" in
--help|-h)
@@ -53,14 +62,14 @@ USAGE:
OPTIONS:
--json Output JSON format
--plain Minimal output for scripts
--plain Minimal key=value output
--text Formatted output (default)
--quick Skip extended tests
--full Run all tests (default)
--quick Skip port and consensus tests
--full Run all checks including consensus
--help Show this help message
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0
;;
exit 0 ;;
--json) OUTPUT_FORMAT="json" ;;
--plain) OUTPUT_FORMAT="plain" ;;
--text) OUTPUT_FORMAT="text" ;;
@@ -78,7 +87,9 @@ EOF
esac
done
# Defaults
# ──────────────────────────────────────────────
# Initialize
# ──────────────────────────────────────────────
IPV4_STATUS="unknown"
IPV6_STATUS="unknown"
DNS_STATUS="unknown"
@@ -89,51 +100,37 @@ PUBLIC_IP6=""
FAILED_TESTS=0
TOTAL_TESTS=0
command_exists() { command -v "$1" >/dev/null 2>&1; }
# IPv4 check
# ──────────────────────────────────────────────
# Check functions
# ──────────────────────────────────────────────
check_ipv4() {
TOTAL_TESTS=$((TOTAL_TESTS + 1))
if [ "$CHECK_IPV4" = "true" ] && command_exists curl; then
PUBLIC_IP=$(curl -4 -fsS --max-time "$TEST_TIMEOUT" https://ipv4.icanhazip.com 2>/dev/null | tr -d '\r')
if [ -n "$PUBLIC_IP" ]; then
IPV4_STATUS="ok"
else
IPV4_STATUS="failed"; FAILED_TESTS=$((FAILED_TESTS + 1))
fi
PUBLIC_IP=$(safe curl -4 -fsS --max-time "$TEST_TIMEOUT" https://ipv4.icanhazip.com | tr -d '\r')
[ -n "$PUBLIC_IP" ] && IPV4_STATUS="ok" || { IPV4_STATUS="failed"; FAILED_TESTS=$((FAILED_TESTS + 1)); }
else
IPV4_STATUS="skipped"
fi
}
# IPv6 check
check_ipv6() {
TOTAL_TESTS=$((TOTAL_TESTS + 1))
if [ "$CHECK_IPV6" = "true" ] && command_exists curl; then
PUBLIC_IP6=$(curl -6 -fsS --max-time "$TEST_TIMEOUT" https://ipv6.icanhazip.com 2>/dev/null | tr -d '\r')
if [ -n "$PUBLIC_IP6" ]; then
IPV6_STATUS="ok"
else
IPV6_STATUS="not_available"
fi
PUBLIC_IP6=$(safe curl -6 -fsS --max-time "$TEST_TIMEOUT" https://ipv6.icanhazip.com | tr -d '\r')
[ -n "$PUBLIC_IP6" ] && IPV6_STATUS="ok" || IPV6_STATUS="not_available"
else
IPV6_STATUS="skipped"
fi
}
# DNS resolution check
check_dns() {
TOTAL_TESTS=$((TOTAL_TESTS + 1))
DNS_WORKING=false
local DNS_WORKING="false"
if [ "$CHECK_DNS" = "true" ]; then
for dns_server in $DNS_SERVERS; do
if command_exists nslookup; then
if nslookup torproject.org "$dns_server" >/dev/null 2>&1; then DNS_WORKING=true; break; fi
elif command_exists dig; then
if dig @"$dns_server" torproject.org +short +time="$TEST_TIMEOUT" >/dev/null 2>&1; then DNS_WORKING=true; break; fi
elif command_exists host; then
if host -t A torproject.org "$dns_server" >/dev/null 2>&1; then DNS_WORKING=true; break; fi
fi
if command_exists nslookup && nslookup torproject.org "$dns_server" >/dev/null 2>&1; then DNS_WORKING="true"; break; fi
if command_exists dig && dig @"$dns_server" torproject.org +short +time="$TEST_TIMEOUT" >/dev/null 2>&1; then DNS_WORKING="true"; break; fi
if command_exists host && host -t A torproject.org "$dns_server" >/dev/null 2>&1; then DNS_WORKING="true"; break; fi
done
[ "$DNS_WORKING" = "true" ] && DNS_STATUS="ok" || { DNS_STATUS="failed"; FAILED_TESTS=$((FAILED_TESTS + 1)); }
else
@@ -141,11 +138,10 @@ check_dns() {
fi
}
# Tor network reachability
check_consensus() {
TOTAL_TESTS=$((TOTAL_TESTS + 1))
if [ "$CHECK_CONSENSUS" = "true" ] && command_exists curl; then
if curl -fsS --max-time "$TEST_TIMEOUT" https://collector.torproject.org/index.json | grep -q "metrics"; then
if safe curl -fsS --max-time "$TEST_TIMEOUT" https://collector.torproject.org/index.json | grep -q "metrics"; then
CONSENSUS_STATUS="ok"
else
CONSENSUS_STATUS="failed"; FAILED_TESTS=$((FAILED_TESTS + 1))
@@ -155,15 +151,14 @@ check_consensus() {
fi
}
# Port accessibility (optional)
check_ports() {
TOTAL_TESTS=$((TOTAL_TESTS + 1))
if [ "$CHECK_PORTS" = "true" ]; then
if ! command_exists nc; then PORT_STATUS="skipped"; return; fi
if [ -f /etc/tor/torrc ]; then
ORPORT=$(grep -E "^ORPort" /etc/tor/torrc 2>/dev/null | awk '{print $2}' | head -1)
ORPORT=$(safe grep -E "^ORPort" /etc/tor/torrc | awk '{print $2}' | head -1)
if [ -n "$ORPORT" ] && [ -n "$PUBLIC_IP" ]; then
if nc -z -w "$TEST_TIMEOUT" "$PUBLIC_IP" "$ORPORT" >/dev/null 2>&1; then
if safe nc -z -w "$TEST_TIMEOUT" "$PUBLIC_IP" "$ORPORT"; then
PORT_STATUS="ok"
else
PORT_STATUS="closed"; FAILED_TESTS=$((FAILED_TESTS + 1))
@@ -179,18 +174,27 @@ check_ports() {
fi
}
# Run all
# ──────────────────────────────────────────────
# Run checks
# ──────────────────────────────────────────────
check_ipv4
check_ipv6
check_dns
# Consensus disabled by default
# check_consensus
check_ports
TOTAL_PASSED=$((TOTAL_TESTS - FAILED_TESTS))
SUCCESS_RATE=$((TOTAL_PASSED * 100 / TOTAL_TESTS))
TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')
if [ "$TOTAL_TESTS" -eq 0 ]; then
SUCCESS_RATE=100
else
SUCCESS_RATE=$((TOTAL_PASSED * 100 / TOTAL_TESTS))
fi
TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date)
# ──────────────────────────────────────────────
# Output
# ──────────────────────────────────────────────
case "$OUTPUT_FORMAT" in
json)
cat <<EOF
@@ -207,35 +211,40 @@ case "$OUTPUT_FORMAT" in
EOF
;;
plain)
echo "TIMESTAMP=$TIMESTAMP"
echo "SUCCESS_RATE=$SUCCESS_RATE"
echo "IPV4_STATUS=$IPV4_STATUS"
echo "IPV4_ADDRESS=$PUBLIC_IP"
echo "IPV6_STATUS=$IPV6_STATUS"
echo "IPV6_ADDRESS=$PUBLIC_IP6"
echo "DNS_STATUS=$DNS_STATUS"
echo "CONSENSUS_STATUS=$CONSENSUS_STATUS"
echo "PORT_STATUS=$PORT_STATUS"
echo "timestamp=$TIMESTAMP"
echo "success_rate=$SUCCESS_RATE"
echo "ipv4_status=$IPV4_STATUS"
echo "ipv4_address=$PUBLIC_IP"
echo "ipv6_status=$IPV6_STATUS"
echo "ipv6_address=$PUBLIC_IP6"
echo "dns_status=$DNS_STATUS"
echo "consensus_status=$CONSENSUS_STATUS"
echo "ports_status=$PORT_STATUS"
;;
*)
echo "🌐 Network Diagnostics v$VERSION"
echo "🌐 Tor Relay Network Check"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
if [ "$FAILED_TESTS" -eq 0 ]; then
echo "📊 Overall: All checks passed ($SUCCESS_RATE%)"
echo "📊 Overall: 🟢 OK - All checks passed ($SUCCESS_RATE%)"
elif [ "$FAILED_TESTS" -lt "$TOTAL_TESTS" ]; then
echo "📊 Overall: ⚠️ Some issues detected ($SUCCESS_RATE% passed)"
echo "📊 Overall: 🟡 PARTIAL - Some checks failed ($SUCCESS_RATE% passed)"
else
echo "📊 Overall: ❌ Multiple failures ($SUCCESS_RATE% passed)"
echo "📊 Overall: 🔴 FAIL - All checks failed ($SUCCESS_RATE% passed)"
fi
echo ""
echo "🔌 IPv4: $(format_ip_status IPv4 "$IPV4_STATUS" "$PUBLIC_IP")"
echo "🔌 IPv6: $(format_ip_status IPv6 "$IPV6_STATUS" "$PUBLIC_IP6")"
echo "🔍 DNS: $(format_status "$DNS_STATUS")"
echo "📋 Consensus: $(format_status "$CONSENSUS_STATUS")"
echo "🚪 Ports: $(format_status "$PORT_STATUS")"
echo "🔌 Connectivity:"
echo " IPv4: $(format_ip_status IPv4 "$IPV4_STATUS" "$PUBLIC_IP")"
echo " IPv6: $(format_ip_status IPv6 "$IPV6_STATUS" "$PUBLIC_IP6")"
echo ""
echo "🕒 Tested at: $TIMESTAMP"
echo "🧭 DNS & Consensus:"
echo " DNS: $(format_status "$DNS_STATUS")"
echo " Consensus: $(format_status "$CONSENSUS_STATUS")"
echo ""
echo "🚪 Ports:"
echo " $(format_status "$PORT_STATUS")"
echo ""
echo "🕒 Checked: $TIMESTAMP"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
;;
esac

View File

@@ -1,121 +1,114 @@
#!/bin/sh
# setup - Interactive configuration wizard for Tor relay
# Usage: setup [--auto|--help]
# Version: 1.1.0
# Usage: setup [--auto|--help|--json|--apply]
set -e
# Configuration
VERSION="1.0.4"
VERSION="1.1.0"
CONFIG_FILE="${CONFIG_FILE:-/etc/tor/torrc}"
RELAY_TYPE="${RELAY_TYPE:-guard}"
AUTO_MODE="${AUTO_MODE:-false}"
APPLY_MODE="false"
DEFAULT_NICKNAME="${DEFAULT_NICKNAME:-MyTorRelay}"
DEFAULT_CONTACT="${DEFAULT_CONTACT:-admin@example.com}"
DEFAULT_ORPORT="${DEFAULT_ORPORT:-9001}"
DEFAULT_DIRPORT="${DEFAULT_DIRPORT:-9030}"
DEFAULT_BANDWIDTH="${DEFAULT_BANDWIDTH:-1024}"
CREATE_BACKUP="${CREATE_BACKUP:-true}" # 🔒 NEW: Backup control
CREATE_BACKUP="${CREATE_BACKUP:-true}"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
# Colors for terminal output
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
PURPLE='\033[0;35m'
NC='\033[0m' # No Color
NC='\033[0m'
safe() { "$@" 2>/dev/null || true; }
# Trap for clean exit
trap 'cleanup' INT TERM
cleanup() {
echo ""
echo -e "${YELLOW}Setup cancelled by user${NC}"
if [ -z "$CONFIG_WRITTEN" ]; then
echo -e "${YELLOW}Setup cancelled by user${NC}"
else
echo -e "${YELLOW}Setup interrupted after configuration${NC}"
fi
exit 130
}
# Parse arguments
# Argument parsing
for arg in "$@"; do
case "$arg" in
--help|-h)
cat << EOF
🧙 Tor-Guard-Relay Setup Wizard v${VERSION}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
USAGE:
setup [OPTIONS]
setup [--auto|--json|--apply|--type bridge|--no-backup]
OPTIONS:
--auto Use defaults for all prompts
--config FILE Config file path (default: /etc/tor/torrc)
--apply Automatically apply /tmp config to /etc/tor/torrc
--type TYPE Relay type: guard|exit|bridge
--no-backup Skip backup creation (not recommended)
--help, -h Show this help message
ENVIRONMENT VARIABLES:
CONFIG_FILE Path to torrc file
RELAY_TYPE Type of relay (guard/exit/bridge)
DEFAULT_NICKNAME Default nickname
DEFAULT_CONTACT Default contact info
DEFAULT_ORPORT Default ORPort
DEFAULT_DIRPORT Default DirPort
DEFAULT_BANDWIDTH Default bandwidth in KB/s
AUTO_MODE Skip prompts and use defaults
CREATE_BACKUP Create backup before overwriting (default: true)
RELAY TYPES:
guard Middle/Guard relay (recommended for beginners)
exit Exit relay (requires careful consideration)
bridge Bridge relay (helps censored users)
SAFETY FEATURES:
🔒 Automatic backup creation before overwriting existing config
🔒 Backup stored as: [config].backup.[timestamp]
🔒 Validation before writing
🔒 Safe cancellation with Ctrl+C
WIZARD STEPS:
1. Relay nickname selection
2. Contact information
3. Port configuration (ORPort/DirPort)
4. Bandwidth limits
5. Relay type selection
6. Configuration validation
EXAMPLES:
setup # Interactive wizard
setup --auto # Auto-configure with defaults
setup --type bridge # Configure as bridge relay
setup --no-backup # Skip backup (not recommended)
OUTPUT:
Creates a complete torrc configuration file ready
for production use with all required settings.
--config FILE Custom torrc path
--no-backup Skip backup creation
--json Output summary as JSON
--help, -h Show this message
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0
;;
exit 0 ;;
--auto) AUTO_MODE="true" ;;
--config)
shift
CONFIG_FILE="$1"
shift
;;
--type)
shift
RELAY_TYPE="$1"
shift
;;
--apply) APPLY_MODE="true" ;;
--type) shift; RELAY_TYPE="$1"; shift ;;
--config) shift; CONFIG_FILE="$1"; shift ;;
--no-backup) CREATE_BACKUP="false" ;;
-*)
echo "❌ Unknown option: $arg"
echo "💡 Use --help for usage information"
exit 2
;;
--json) OUTPUT_FORMAT="json" ;;
-*) echo "❌ Unknown option: $arg"; exit 2 ;;
esac
done
# Helper functions
# Validation helpers
validate_nickname() { echo "$1" | grep -qE "^[a-zA-Z0-9]{1,19}$"; }
validate_email() { echo "$1" | grep -qE "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"; }
validate_port() { [ "$1" -ge 1 ] && [ "$1" -le 65535 ] 2>/dev/null; }
validate_bandwidth() { [ "$1" -ge 256 ] 2>/dev/null; }
# Backup creation
create_config_backup() {
if [ -f "$CONFIG_FILE" ] && [ "$CREATE_BACKUP" = "true" ]; then
BACKUP_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${CONFIG_FILE}.backup.${BACKUP_TIMESTAMP}"
echo -e "${BLUE}📦 Creating backup...${NC}"
if safe cp "$CONFIG_FILE" "$BACKUP_FILE"; then
echo -e "${GREEN}✅ Backup created: $BACKUP_FILE${NC}"
BACKUP_COUNT=$(safe ls -1 "${CONFIG_FILE}.backup."* | wc -l)
if [ "$BACKUP_COUNT" -gt 5 ]; then
echo -e "${YELLOW}🧹 Cleaning old backups (keeping last 5)...${NC}"
safe ls -1t "${CONFIG_FILE}.backup."* | tail -n +6 | safe xargs rm -f
fi
else
echo -e "${YELLOW}⚠️ Could not create backup${NC}"
fi
fi
}
restore_from_backup() {
LATEST_BACKUP=$(safe ls -1t "${CONFIG_FILE}.backup."* | head -1)
if [ -n "$LATEST_BACKUP" ]; then
echo -e "${YELLOW}❌ Setup failed. Restoring backup...${NC}"
if safe cp "$LATEST_BACKUP" "$CONFIG_FILE"; then
echo -e "${GREEN}✅ Restored from: $LATEST_BACKUP${NC}"
else
echo -e "${RED}❌ Restore failed${NC}"
fi
fi
}
# UI helpers
print_header() {
echo ""
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
@@ -123,291 +116,222 @@ print_header() {
echo -e "${PURPLE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
}
print_step() { echo -e "${GREEN}[$1/6]${NC} $2"; }
print_step() {
echo -e "${GREEN}[$1/6]${NC} $2"
}
validate_nickname() {
echo "$1" | grep -qE "^[a-zA-Z0-9]{1,19}$"
}
validate_email() {
echo "$1" | grep -qE "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
}
validate_port() {
[ "$1" -ge 1 ] && [ "$1" -le 65535 ] 2>/dev/null
}
validate_bandwidth() {
[ "$1" -ge 256 ] 2>/dev/null
}
# 🔒 NEW: Backup function
create_config_backup() {
if [ -f "$CONFIG_FILE" ] && [ "$CREATE_BACKUP" = "true" ]; then
BACKUP_TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${CONFIG_FILE}.backup.${BACKUP_TIMESTAMP}"
echo -e "${BLUE}📦 Creating backup...${NC}"
if cp "$CONFIG_FILE" "$BACKUP_FILE" 2>/dev/null; then
echo -e "${GREEN}✅ Backup created: $BACKUP_FILE${NC}"
# Keep only last 5 backups
BACKUP_COUNT=$(ls -1 "${CONFIG_FILE}.backup."* 2>/dev/null | wc -l)
if [ "$BACKUP_COUNT" -gt 5 ]; then
echo -e "${YELLOW}🧹 Cleaning old backups (keeping last 5)...${NC}"
ls -1t "${CONFIG_FILE}.backup."* | tail -n +6 | xargs rm -f 2>/dev/null || true
fi
return 0
else
echo -e "${YELLOW}⚠️ Warning: Could not create backup${NC}"
echo -e "${YELLOW} Proceeding anyway...${NC}"
return 1
fi
fi
}
# 🔒 NEW: Restore function (if setup fails)
restore_from_backup() {
LATEST_BACKUP=$(ls -1t "${CONFIG_FILE}.backup."* 2>/dev/null | head -1)
if [ -n "$LATEST_BACKUP" ]; then
echo ""
echo -e "${YELLOW}❌ Setup failed. Restoring from backup...${NC}"
if cp "$LATEST_BACKUP" "$CONFIG_FILE" 2>/dev/null; then
echo -e "${GREEN}✅ Configuration restored from: $LATEST_BACKUP${NC}"
else
echo -e "${RED}❌ Failed to restore backup${NC}"
fi
fi
}
# Main setup wizard
# Header
clear
cat << EOF
${PURPLE}╔══════════════════════════════════════════════════════════╗
║ ║
║ 🧅 Tor-Guard-Relay Setup Wizard v${VERSION} ║
║ ║
║ Configure your Tor relay in 6 steps ║
║ ║
╚══════════════════════════════════════════════════════════╝${NC}
echo ""
echo -e "${PURPLE}╔══════════════════════════════════════════════════════════╗${NC}"
echo "🧅 Tor-Guard-Relay Setup Wizard v${VERSION}"
echo " Configure your Tor relay in 6 steps"
echo -e "${PURPLE}╚══════════════════════════════════════════════════════════╝${NC}"
echo "Press Ctrl+C to cancel safely at any time."
echo ""
This wizard will help you configure a Tor relay with optimal
settings for your network and preferences.
safe mkdir -p "$(dirname "$CONFIG_FILE")"
Press Ctrl+C at any time to cancel safely.
EOF
# Check if config exists
# Check existing config
if [ -f "$CONFIG_FILE" ]; then
echo ""
echo -e "${YELLOW}⚠️ Existing configuration found at:${NC}"
echo -e "${YELLOW} $CONFIG_FILE${NC}"
echo ""
echo -e "${YELLOW}⚠️ Existing configuration found at:${NC} $CONFIG_FILE"
if [ "$CREATE_BACKUP" = "true" ]; then
echo -e "${GREEN}🔒 A backup will be created before making changes.${NC}"
echo -e "${GREEN}🔒 Backup will be created.${NC}"
else
echo -e "${RED}⚠️ Backup creation is disabled!${NC}"
echo -e "${RED}⚠️ Backup disabled.${NC}"
fi
if [ "$AUTO_MODE" != "true" ]; then
echo ""
printf "Continue and overwrite existing config? [y/N]: "
read CONFIRM
if [ "$CONFIRM" != "y" ] && [ "$CONFIRM" != "Y" ]; then
echo -e "${YELLOW}Setup cancelled.${NC}"
exit 0
fi
case "$CONFIRM" in
[yY]) echo -e "${GREEN}✔ Proceeding with overwrite.${NC}" ;;
*) echo -e "${YELLOW}Cancelled.${NC}"; exit 0 ;;
esac
fi
fi
# Step 1: Nickname
# Step 1 Nickname
print_header "Step 1: Relay Nickname"
print_step "1" "Choose a nickname for your relay"
echo ""
echo "Requirements:"
echo " • 1-19 characters"
echo " • Only letters and numbers"
echo " • No spaces or special characters"
echo ""
print_step 1 "Choose a nickname"
if [ "$AUTO_MODE" = "true" ]; then
NICKNAME="$DEFAULT_NICKNAME"
echo -e "${GREEN}${NC} Using default: $NICKNAME"
echo -e "${GREEN}✓ Using default: $NICKNAME${NC}"
else
while true; do
printf "Enter nickname [${DEFAULT_NICKNAME}]: "
read NICKNAME
[ -z "$NICKNAME" ] && NICKNAME="$DEFAULT_NICKNAME"
if [ -z "$NICKNAME" ]; then
echo -e "${YELLOW}⚠ Empty input, using default: ${DEFAULT_NICKNAME}${NC}"
NICKNAME="$DEFAULT_NICKNAME"
fi
if validate_nickname "$NICKNAME"; then
echo -e "${GREEN}${NC} Nickname accepted: $NICKNAME"
echo -e "${GREEN} Accepted: ${NICKNAME}${NC}"
break
else
echo -e "${RED}${NC} Invalid nickname. Please try again."
echo -e "${RED}✗ Invalid nickname (must be alphanumeric, ≤19 chars)${NC}"
fi
done
fi
# Step 2: Contact Info
# Step 2 Contact
print_header "Step 2: Contact Information"
print_step "2" "Provide contact information (public)"
echo ""
echo "This helps the Tor Project contact you if needed."
echo "Use an email address you're comfortable making public."
echo ""
print_step 2 "Provide email (public)"
if [ "$AUTO_MODE" = "true" ]; then
CONTACT="$DEFAULT_CONTACT"
echo -e "${GREEN}${NC} Using default: $CONTACT"
echo -e "${GREEN}✓ Using default: $CONTACT${NC}"
else
while true; do
printf "Enter email [${DEFAULT_CONTACT}]: "
read CONTACT
[ -z "$CONTACT" ] && CONTACT="$DEFAULT_CONTACT"
if [ -z "$CONTACT" ]; then
echo -e "${YELLOW}⚠ Empty input, using default: ${DEFAULT_CONTACT}${NC}"
CONTACT="$DEFAULT_CONTACT"
fi
if validate_email "$CONTACT"; then
echo -e "${GREEN}${NC} Contact accepted: $CONTACT"
echo -e "${GREEN} Accepted: ${CONTACT}${NC}"
break
else
echo -e "${YELLOW}${NC} Invalid email format, but continuing..."
echo -e "${YELLOW} Nonstandard email format, continuing anyway.${NC}"
break
fi
done
fi
# Step 3: Port Configuration
# Step 3 Ports
print_header "Step 3: Port Configuration"
print_step "3" "Configure network ports"
echo ""
echo "ORPort: Main port for Tor traffic (must be accessible)"
echo "DirPort: Directory service port (optional)"
echo ""
print_step 3 "Configure ORPort and DirPort"
if [ "$AUTO_MODE" = "true" ]; then
ORPORT="$DEFAULT_ORPORT"
DIRPORT="$DEFAULT_DIRPORT"
echo -e "${GREEN}${NC} Using defaults: ORPort=$ORPORT, DirPort=$DIRPORT"
ORPORT="$DEFAULT_ORPORT"; DIRPORT="$DEFAULT_DIRPORT"
echo -e "${GREEN}✓ Defaults: ORPort=$ORPORT DirPort=$DIRPORT${NC}"
else
while true; do
printf "Enter ORPort [${DEFAULT_ORPORT}]: "
read ORPORT
[ -z "$ORPORT" ] && ORPORT="$DEFAULT_ORPORT"
if validate_port "$ORPORT"; then
echo -e "${GREEN}${NC} ORPort accepted: $ORPORT"
break
else
echo -e "${RED}${NC} Invalid port (1-65535). Please try again."
fi
[ -z "$ORPORT" ] && { echo -e "${YELLOW}⚠ Using default ORPort: ${DEFAULT_ORPORT}${NC}"; ORPORT="$DEFAULT_ORPORT"; }
validate_port "$ORPORT" && break || echo -e "${RED}✗ Invalid port${NC}"
done
while true; do
printf "Enter DirPort [${DEFAULT_DIRPORT}]: "
read DIRPORT
[ -z "$DIRPORT" ] && DIRPORT="$DEFAULT_DIRPORT"
if validate_port "$DIRPORT"; then
echo -e "${GREEN}${NC} DirPort accepted: $DIRPORT"
break
else
echo -e "${RED}${NC} Invalid port (1-65535). Please try again."
fi
[ -z "$DIRPORT" ] && { echo -e "${YELLOW}⚠ Using default DirPort: ${DEFAULT_DIRPORT}${NC}"; DIRPORT="$DEFAULT_DIRPORT"; }
validate_port "$DIRPORT" && break || echo -e "${RED}✗ Invalid port${NC}"
done
fi
# Step 4: Bandwidth Limits
# Step 4 Bandwidth
print_header "Step 4: Bandwidth Allocation"
print_step "4" "Set bandwidth limits (KB/s)"
echo ""
echo "Recommended minimums:"
echo " • Guard relay: 1024 KB/s (1 MB/s)"
echo " • Exit relay: 2048 KB/s (2 MB/s)"
echo " • Bridge: 512 KB/s"
echo ""
print_step 4 "Set bandwidth limit (KB/s)"
if [ "$AUTO_MODE" = "true" ]; then
BANDWIDTH="$DEFAULT_BANDWIDTH"
echo -e "${GREEN}${NC} Using default: $BANDWIDTH KB/s"
echo -e "${GREEN}✓ Using default: $BANDWIDTH KB/s${NC}"
else
while true; do
printf "Enter bandwidth limit in KB/s [${DEFAULT_BANDWIDTH}]: "
printf "Enter bandwidth [${DEFAULT_BANDWIDTH}]: "
read BANDWIDTH
[ -z "$BANDWIDTH" ] && BANDWIDTH="$DEFAULT_BANDWIDTH"
if validate_bandwidth "$BANDWIDTH"; then
echo -e "${GREEN}${NC} Bandwidth accepted: $BANDWIDTH KB/s"
break
else
echo -e "${RED}${NC} Minimum bandwidth is 256 KB/s. Please try again."
if [ -z "$BANDWIDTH" ]; then
echo -e "${YELLOW}⚠ Using default bandwidth: ${DEFAULT_BANDWIDTH}${NC}"
BANDWIDTH="$DEFAULT_BANDWIDTH"
fi
validate_bandwidth "$BANDWIDTH" && break || echo -e "${RED}✗ Too low, must be ≥256 KB/s${NC}"
done
fi
# Step 5: Relay Type
print_header "Step 5: Relay Type Selection"
print_step "5" "Choose relay type"
echo ""
echo "Available types:"
echo " ${GREEN}guard${NC} - Middle/Guard relay (recommended)"
echo " ${YELLOW}exit${NC} - Exit relay (requires careful consideration)"
echo " ${BLUE}bridge${NC} - Bridge relay (helps censored users)"
echo ""
# Step 5 Relay Type
print_header "Step 5: Relay Type"
print_step 5 "Choose relay type"
if [ "$AUTO_MODE" = "true" ]; then
echo -e "${GREEN}${NC} Using default type: $RELAY_TYPE"
echo -e "${GREEN}✓ Using default: $RELAY_TYPE${NC}"
else
printf "Enter relay type [guard/exit/bridge] [${RELAY_TYPE}]: "
printf "Enter type [guard/exit/bridge] [${RELAY_TYPE}]: "
read TYPE_INPUT
[ -n "$TYPE_INPUT" ] && RELAY_TYPE="$TYPE_INPUT"
case "$RELAY_TYPE" in
guard|exit|bridge)
echo -e "${GREEN}${NC} Relay type: $RELAY_TYPE"
;;
*)
echo -e "${YELLOW}${NC} Unknown type, defaulting to guard"
RELAY_TYPE="guard"
;;
guard|exit|bridge) echo -e "${GREEN}✓ Type: $RELAY_TYPE${NC}" ;;
*) echo -e "${YELLOW}⚠ Unknown type, defaulting to guard${NC}"; RELAY_TYPE="guard" ;;
esac
fi
# Step 6: Generate Configuration
# Step 6 Config Generation
print_header "Step 6: Generating Configuration"
print_step "6" "Creating torrc file"
echo ""
# 🔒 Create backup before overwriting
print_step 6 "Writing torrc file"
create_config_backup
CONFIG_WRITTEN=true
# Create configuration
# Non-root fallback
if [ "$(id -u)" != "0" ]; then
TMP_PATH="/tmp/torrc.$(date +%s)"
echo -e "${YELLOW}⚠️ Non-root user detected, writing config to: $TMP_PATH${NC}"
echo -e "${YELLOW}💡 To apply it: sudo mv $TMP_PATH /etc/tor/torrc${NC}"
CONFIG_FILE="$TMP_PATH"
fi
# Write configuration
if ! cat > "$CONFIG_FILE" << EOF
# Tor Relay Configuration
# Generated by Tor-Guard-Relay Setup Wizard v${VERSION}
# Date: $(date '+%Y-%m-%d %H:%M:%S')
# Basic Information
Nickname $NICKNAME
ContactInfo $CONTACT
# Network Settings
ORPort $ORPORT
DirPort $DIRPORT
# Bandwidth Limits
RelayBandwidthRate $BANDWIDTH KB
RelayBandwidthBurst $((BANDWIDTH * 2)) KB
EOF
then
echo -e "${RED}Failed to write configuration${NC}"
echo -e "${RED}Write failed${NC}"
restore_from_backup
exit 1
else
echo -e "${GREEN}✅ Configuration saved: $CONFIG_FILE${NC}"
fi
fi
# Relay type directives
case "$RELAY_TYPE" in
guard) echo "ExitRelay 0" >> "$CONFIG_FILE"; echo "BridgeRelay 0" >> "$CONFIG_FILE" ;;
exit) echo "ExitRelay 1" >> "$CONFIG_FILE"; echo "BridgeRelay 0" >> "$CONFIG_FILE" ;;
bridge) echo "BridgeRelay 1" >> "$CONFIG_FILE"; echo "ExitRelay 0" >> "$CONFIG_FILE" ;;
esac
cat <<EOF >> "$CONFIG_FILE"
RunAsDaemon 1
SocksPort 0
ControlPort 9051
DataDirectory /var/lib/tor
EOF
# Auto-apply if root and --apply used
if [ "$APPLY_MODE" = "true" ] && [ "$(id -u)" = "0" ] && [ "$CONFIG_FILE" != "/etc/tor/torrc" ]; then
echo -e "${BLUE}🔧 Applying generated config to /etc/tor/torrc...${NC}"
mv "$CONFIG_FILE" /etc/tor/torrc && echo -e "${GREEN}✅ Applied successfully.${NC}" || echo -e "${RED}❌ Apply failed.${NC}"
fi
echo -e "${GREEN}✅ Configuration saved: $CONFIG_FILE${NC}"
# Summary
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📄 Configuration Summary"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📁 File: $CONFIG_FILE"
echo "🧅 Nickname: $NICKNAME"
echo "📧 Contact: $CONTACT"
echo "🔌 ORPort: $ORPORT"
echo "📡 DirPort: $DIRPORT"
echo "📈 Bandwidth: $BANDWIDTH KB/s"
echo "🏷️ Type: $RELAY_TYPE"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ Setup complete. You can now restart your relay."
echo ""
if [ "$OUTPUT_FORMAT" = "json" ]; then
cat <<EOF
{
"nickname": "$NICKNAME",
"contact": "$CONTACT",
"orport": "$ORPORT",
"dirport": "$DIRPORT",
"bandwidth": "$BANDWIDTH",
"relay_type": "$RELAY_TYPE",
"config_file": "$CONFIG_FILE"
}
EOF
fi

View File

@@ -1,300 +1,462 @@
#!/bin/sh
# status - Comprehensive relay status dashboard
# Usage: docker exec guard-relay status [--json|--help]
# status - Tor Guard Relay status dashboard
# Usage: docker exec TorGuardRelay status [--short|--json|--plain|--quick|--full|--help]
set -e
set -eu
# Configuration
VERSION="1.0.9"
OUTPUT_FORMAT="${OUTPUT_FORMAT:-text}"
SHOW_ALL="${SHOW_ALL:-true}"
CHECK_NETWORK="${CHECK_NETWORK:-true}"
VERSION="1.1.0"
OUTPUT_FORMAT=""
CHECK_NETWORK="true"
format_status() {
case "$1" in
ok|OK) echo "🟢 OK" ;;
failed|closed|error|FAIL|not_available) echo "🔴 FAIL" ;;
skipped|unknown) echo "⏭️ SKIPPED" ;;
*) echo "$1" ;;
esac
}
# ───────────────────────────
# Helper Functions
# ───────────────────────────
is_integer() { case "$1" in ''|*[!0-9]*) return 1 ;; *) return 0 ;; esac; }
sanitize_num() { v=$(printf '%s' "$1" | tr -cd '0-9'); [ -z "$v" ] && v=0; printf '%s' "$v"; }
format_ip_status() { [ -n "$2" ] && printf '🟢 %s' "$2" || printf '🔴 No %s connectivity' "$1"; }
separator() { printf '%s\n' "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"; }
format_ip_status() {
local type="$1"
local value="$2"
if [ -n "$value" ]; then
echo "🟢 OK ($value)"
# Convert seconds → human-readable Dd Hh Mm
secs_to_human() {
s=${1:-0}
days=$((s / 86400))
hours=$(( (s % 86400) / 3600 ))
mins=$(( (s % 3600) / 60 ))
if [ "$days" -gt 0 ]; then
printf '%dd %dh %dm' "$days" "$hours" "$mins"
elif [ "$hours" -gt 0 ]; then
printf '%dh %dm' "$hours" "$mins"
else
echo "🔴 No ${type} connectivity"
printf '%dm' "$mins"
fi
}
# Safe integer check
is_integer() {
case "$1" in
''|*[!0-9]*) return 1 ;;
*) return 0 ;;
# Convert ps etime ("2-14:30:00") → seconds
etime_to_seconds() {
raw=${1:-}
[ -z "$raw" ] && { printf '%d' 0; return; }
days=0; hh=0; mm=0; ss=0
# Handle format with days (DD-HH:MM:SS or DD-HH:MM)
case "$raw" in
*-*)
days=${raw%%-*}
raw=${raw#*-}
;;
esac
# Parse the time part
IFS=:; set -- $raw
case $# in
3) hh=$1; mm=$2; ss=$3 ;;
2) hh=$1; mm=$2 ;;
1) ss=$1 ;;
esac
# Default to 0 if any value is empty
hh=${hh:-0}; mm=${mm:-0}; ss=${ss:-0}; days=${days:-0}
# Calculate total seconds
printf '%d' $((days*86400 + hh*3600 + mm*60 + ss))
}
# Get public IP with multiple fallback methods
get_public_ip() {
ip_type=$1 # "ipv4" or "ipv6"
ip=""
# Try multiple services in order of preference
if [ "$ip_type" = "ipv4" ]; then
# Try curl with multiple services
if command -v curl >/dev/null 2>&1; then
ip=$(curl -4 -s --max-time 5 --connect-timeout 3 https://api.ipify.org 2>/dev/null || true)
[ -z "$ip" ] && ip=$(curl -4 -s --max-time 5 --connect-timeout 3 https://ipinfo.io/ip 2>/dev/null || true)
[ -z "$ip" ] && ip=$(curl -4 -s --max-time 5 --connect-timeout 3 https://ipv4.icanhazip.com 2>/dev/null || true)
# Fallback to wget
elif command -v wget >/dev/null 2>&1; then
ip=$(wget -4 -q -O - --timeout=5 https://api.ipify.org 2>/dev/null || true)
[ -z "$ip" ] && ip=$(wget -4 -q -O - --timeout=5 https://ipinfo.io/ip 2>/dev/null || true)
[ -z "$ip" ] && ip=$(wget -4 -q -O - --timeout=5 https://ipv4.icanhazip.com 2>/dev/null || true)
fi
# Validate IPv4 format
if printf '%s' "$ip" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
printf '%s' "$ip"
fi
else # IPv6
# Try curl with multiple services
if command -v curl >/dev/null 2>&1; then
ip=$(curl -6 -s --max-time 5 --connect-timeout 3 https://api6.ipify.org 2>/dev/null || true)
[ -z "$ip" ] && ip=$(curl -6 -s --max-time 5 --connect-timeout 3 https://ipv6.icanhazip.com 2>/dev/null || true)
# Fallback to wget
elif command -v wget >/dev/null 2>&1; then
ip=$(wget -6 -q -O - --timeout=5 https://api6.ipify.org 2>/dev/null || true)
[ -z "$ip" ] && ip=$(wget -6 -q -O - --timeout=5 https://ipv6.icanhazip.com 2>/dev/null || true)
fi
# Basic IPv6 validation (simplified)
if printf '%s' "$ip" | grep -Eq '^[0-9a-fA-F:]+$'; then
printf '%s' "$ip"
fi
fi
}
# Check if port is open
check_port() {
host=$1
port=$2
timeout=$3
if command -v nc >/dev/null 2>&1; then
# Use netcat if available
if nc -z -w "$timeout" "$host" "$port" 2>/dev/null; then
return 0
fi
elif command -v timeout >/dev/null 2>&1 && command -v bash >/dev/null 2>&1; then
# Use timeout with bash's built-in TCP feature
if timeout "$timeout" bash -c "echo >/dev/tcp/$host/$port" 2>/dev/null; then
return 0
fi
fi
return 1
}
# ───────────────────────────
# Parse arguments
# ───────────────────────────
for arg in "$@"; do
case "$arg" in
--help|-h)
cat << EOF
cat <<EOF
🧅 Tor-Guard-Relay Status Dashboard v${VERSION}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
USAGE:
status [OPTIONS]
status [OPTIONS]
OPTIONS:
--json Output in JSON format
--plain Plain text output
--quick Quick status check (skip network tests)
--full Full status report (default)
--help, -h Show this help message
ENVIRONMENT VARIABLES:
OUTPUT_FORMAT Output format (text/json/plain)
SHOW_ALL Show all sections (true/false)
CHECK_NETWORK Include network checks (true/false)
--short Compact summary output
--json JSON format
--plain Key=value format
--quick Skip network checks
--full Detailed dashboard (default)
--help, -h Show this message
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0
;;
exit 0 ;;
--short) OUTPUT_FORMAT="short" ;;
--json) OUTPUT_FORMAT="json" ;;
--plain) OUTPUT_FORMAT="plain" ;;
--quick) CHECK_NETWORK="false" ;;
--full)
SHOW_ALL="true"
CHECK_NETWORK="true"
;;
-*)
echo "❌ Unknown option: $arg"
echo "💡 Use --help for usage information"
exit 2
--full) OUTPUT_FORMAT="text" ;;
*)
if [ "${arg#-}" != "$arg" ]; then
printf '❌ Unknown option: %s\n' "$arg" >&2
printf '💡 Use --help for usage information\n' >&2
exit 2
fi
;;
esac
done
[ -z "${OUTPUT_FORMAT}" ] && OUTPUT_FORMAT="text"
# Gather all status information
# ───────────────────────────
# Gather status info
# ───────────────────────────
gather_status() {
IS_RUNNING="false"
PID=""
TOR_UPTIME_SECONDS=0
CONTAINER_UPTIME_SECONDS=0
TOR_UPTIME="0m"
CONTAINER_UPTIME="0m"
# Tor process uptime
if pgrep -x tor >/dev/null 2>&1; then
IS_RUNNING="true"
PID=$(pgrep -x tor | head -1)
UPTIME=$(ps -o etime= -p "$PID" 2>/dev/null | tr -d ' ' || echo "0")
PID=$(pgrep -x tor | head -n1)
TOR_RAW=$(ps -o etime= -p "$PID" 2>/dev/null | awk '{$1=$1};1' || true)
if [ -n "$TOR_RAW" ]; then
TOR_UPTIME_SECONDS=$(etime_to_seconds "$TOR_RAW")
TOR_UPTIME=$(secs_to_human "$TOR_UPTIME_SECONDS")
fi
fi
# Container uptime
if [ -r /proc/1/uptime ]; then
sec=$(awk '{print int($1)}' /proc/1/uptime 2>/dev/null || echo 0)
CONTAINER_UPTIME_SECONDS=$sec
CONTAINER_UPTIME=$(secs_to_human "$sec")
fi
# Always display the most relevant uptime in a clean one-line format
if [ "$IS_RUNNING" = "true" ] && [ "$TOR_UPTIME_SECONDS" -gt 0 ]; then
UPTIME_DISPLAY="${TOR_UPTIME}"
UPTIME_SOURCE="Tor process"
else
UPTIME_DISPLAY="${CONTAINER_UPTIME}"
UPTIME_SOURCE="Container"
fi
# Bootstrap progress
BOOTSTRAP_PERCENT=0
BOOTSTRAP_MESSAGE=""
if [ -f /var/log/tor/notices.log ]; then
BOOTSTRAP_LINE=$(grep "Bootstrapped" /var/log/tor/notices.log 2>/dev/null | tail -1 || true)
if [ -n "$BOOTSTRAP_LINE" ]; then
# Extract clean integer only
BOOTSTRAP_PERCENT=$(echo "$BOOTSTRAP_LINE" | grep -oE '[0-9]+' | tail -1 | tr -d '\r' || echo 0)
BOOTSTRAP_PERCENT=${BOOTSTRAP_PERCENT:-0}
BOOTSTRAP_MESSAGE=$(echo "$BOOTSTRAP_LINE" | sed 's/.*Bootstrapped [0-9]*%[: ]*//')
fi
BOOTSTRAP_LINE=$(grep "Bootstrapped" /var/log/tor/notices.log 2>/dev/null | tail -n1 || true)
[ -n "$BOOTSTRAP_LINE" ] && BOOTSTRAP_PERCENT=$(sanitize_num "$(printf '%s' "$BOOTSTRAP_LINE" | grep -oE '[0-9]+' | tail -n1)")
fi
# Reachability
IS_REACHABLE="false"
REACHABILITY_MESSAGE=""
REACHABILITY_STATUS="Unknown"
if [ -f /var/log/tor/notices.log ]; then
REACHABLE_LINE=$(grep -E "reachable|self-testing" /var/log/tor/notices.log 2>/dev/null | tail -1 || true)
if echo "$REACHABLE_LINE" | grep -q "reachable from the outside" 2>/dev/null; then
if grep -q "reachable from the outside" /var/log/tor/notices.log 2>/dev/null; then
IS_REACHABLE="true"
REACHABILITY_MESSAGE="ORPort is reachable from the outside"
elif [ -n "$REACHABLE_LINE" ]; then
REACHABILITY_MESSAGE=$(echo "$REACHABLE_LINE" | sed 's/.*] //')
REACHABILITY_STATUS="Reachable"
else
REACHABILITY_STATUS="Not reachable"
fi
fi
NICKNAME=""
FINGERPRINT=""
# Relay identity
NICKNAME="unknown"
FINGERPRINT="N/A"
if [ -f /var/lib/tor/fingerprint ]; then
NICKNAME=$(awk '{print $1}' /var/lib/tor/fingerprint 2>/dev/null)
FINGERPRINT=$(awk '{print $2}' /var/lib/tor/fingerprint 2>/dev/null)
NICKNAME=$(awk '{print $1}' /var/lib/tor/fingerprint 2>/dev/null || echo "unknown")
FINGERPRINT=$(awk '{print $2}' /var/lib/tor/fingerprint 2>/dev/null || echo "N/A")
fi
ORPORT=""
DIRPORT=""
EXIT_RELAY="false"
BRIDGE_RELAY="false"
BANDWIDTH_RATE=""
# Config
ORPORT="N/A"
DIRPORT="N/A"
RELAY_TYPE="🔒 Guard/Middle Relay"
CONTACT_INFO="N/A"
if [ -f /etc/tor/torrc ]; then
ORPORT=$(grep -E "^ORPort" /etc/tor/torrc 2>/dev/null | awk '{print $2}' | head -1)
DIRPORT=$(grep -E "^DirPort" /etc/tor/torrc 2>/dev/null | awk '{print $2}' | head -1)
grep -qE "^ExitRelay\s+1" /etc/tor/torrc 2>/dev/null && EXIT_RELAY="true"
grep -qE "^BridgeRelay\s+1" /etc/tor/torrc 2>/dev/null && BRIDGE_RELAY="true"
BANDWIDTH_RATE=$(grep -E "^RelayBandwidthRate" /etc/tor/torrc 2>/dev/null | awk '{print $2,$3}')
ORPORT=$(grep -E "^ORPort" /etc/tor/torrc 2>/dev/null | awk '{print $2}' | head -n1 || echo "N/A")
DIRPORT=$(grep -E "^DirPort" /etc/tor/torrc 2>/dev/null | awk '{print $2}' | head -n1 || echo "N/A")
CONTACT_INFO=$(grep -E "^ContactInfo" /etc/tor/torrc 2>/dev/null | cut -d' ' -f2- | head -n1 || echo "N/A")
if grep -qE "^ExitRelay\s+1" /etc/tor/torrc 2>/dev/null; then
RELAY_TYPE="🚪 Exit Relay"
elif grep -qE "^BridgeRelay\s+1" /etc/tor/torrc 2>/dev/null; then
RELAY_TYPE="🌉 Bridge Relay"
fi
fi
# Logs
ERROR_COUNT=0
WARNING_COUNT=0
RECENT_ERRORS=""
if [ -f /var/log/tor/notices.log ]; then
ERROR_COUNT=$(grep -cE "\[err\]|\[error\]" /var/log/tor/notices.log 2>/dev/null || echo 0)
WARNING_COUNT=$(grep -cE "\[warn\]|\[warning\]" /var/log/tor/notices.log 2>/dev/null || echo 0)
RECENT_ERRORS=$(grep -E "\[err\]|\[error\]" /var/log/tor/notices.log 2>/dev/null | tail -3)
ERROR_COUNT=$(sanitize_num "$(grep -cE '\[err\]|\[error\]' /var/log/tor/notices.log 2>/dev/null || echo 0)")
WARNING_COUNT=$(sanitize_num "$(grep -cE '\[warn\]|\[warning\]' /var/log/tor/notices.log 2>/dev/null || echo 0)")
fi
# Version and build info from build-info.txt
VERSION_INFO=""
BUILD_TIME=""
if [ -f /build-info.txt ]; then
VERSION_INFO=$(grep "Version:" /build-info.txt 2>/dev/null | cut -d: -f2- | tr -d ' ')
BUILD_TIME=$(grep "Built:" /build-info.txt 2>/dev/null | cut -d: -f2- | tr -d ' ')
fi
# Fallback if build-info.txt doesn't exist or doesn't contain expected info
if [ -z "$VERSION_INFO" ]; then
if command -v tor >/dev/null 2>&1; then
VERSION_INFO=$(tor --version 2>/dev/null | awk 'NR==1{for(i=1;i<=NF;i++) if ($i ~ /[0-9]+\.[0-9]+/) {print $i; exit}}' || echo "unknown")
else
VERSION_INFO="unknown"
fi
fi
if [ -z "$BUILD_TIME" ]; then
BUILD_TIME=$(date '+%Y-%m-%d' 2>/dev/null || echo "unknown")
fi
ARCH=$(uname -m 2>/dev/null || echo "unknown")
# Format build info as requested
BUILD_INFO="v${VERSION_INFO} (${BUILD_TIME}, ${ARCH})"
# IPv4/IPv6 detection with improved reliability
PUBLIC_IP=""
PUBLIC_IP6=""
if [ "$CHECK_NETWORK" = "true" ] && command -v curl >/dev/null 2>&1; then
PUBLIC_IP=$(curl -4 -s --max-time 5 https://ipv4.icanhazip.com 2>/dev/null | tr -d '\r')
PUBLIC_IP6=$(curl -6 -s --max-time 5 https://ipv6.icanhazip.com 2>/dev/null | tr -d '\r')
IPV4_OK="false"
IPV6_OK="false"
ORPORT_OPEN="false"
DIRPORT_OPEN="false"
if [ "$CHECK_NETWORK" = "true" ]; then
# Get IPv4
ip4=$(get_public_ip "ipv4")
if [ -n "$ip4" ]; then
PUBLIC_IP="$ip4"
IPV4_OK="true"
# Check if ORPort is open (only if it's not the default 0)
if [ "$ORPORT" != "N/A" ] && [ "$ORPORT" != "0" ]; then
if check_port "$PUBLIC_IP" "$ORPORT" 3; then
ORPORT_OPEN="true"
fi
fi
# Check if DirPort is open (only if it's not the default 0)
if [ "$DIRPORT" != "N/A" ] && [ "$DIRPORT" != "0" ]; then
if check_port "$PUBLIC_IP" "$DIRPORT" 3; then
DIRPORT_OPEN="true"
fi
fi
fi
# Get IPv6
ip6=$(get_public_ip "ipv6")
if [ -n "$ip6" ]; then
PUBLIC_IP6="$ip6"
IPV6_OK="true"
fi
fi
TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
}
# ───────────────────────────
# Gather data
# ───────────────────────────
gather_status
# Sanitize percent and timestamp
BOOTSTRAP_PERCENT=$(echo "$BOOTSTRAP_PERCENT" | tr -cd '0-9')
BOOTSTRAP_PERCENT=${BOOTSTRAP_PERCENT:-0}
TIMESTAMP=$(date -u '+%Y-%m-%dT%H:%M:%SZ' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')
# Determine overall status safely
if [ "$IS_RUNNING" = "false" ]; then
# Determine status
if [ "${IS_RUNNING}" = "false" ]; then
OVERALL_STATUS="down"
elif is_integer "$BOOTSTRAP_PERCENT" && [ "$BOOTSTRAP_PERCENT" -eq 100 ] && [ "$IS_REACHABLE" = "true" ]; then
elif [ "$BOOTSTRAP_PERCENT" -eq 100 ] && [ "${IS_REACHABLE}" = "true" ]; then
OVERALL_STATUS="healthy"
elif is_integer "$BOOTSTRAP_PERCENT" && [ "$BOOTSTRAP_PERCENT" -eq 100 ]; then
elif [ "$BOOTSTRAP_PERCENT" -eq 100 ]; then
OVERALL_STATUS="running"
elif is_integer "$BOOTSTRAP_PERCENT" && [ "$BOOTSTRAP_PERCENT" -gt 0 ]; then
elif [ "$BOOTSTRAP_PERCENT" -gt 0 ]; then
OVERALL_STATUS="starting"
else
OVERALL_STATUS="unknown"
fi
# ───────────────────────────
# Output
# ───────────────────────────
case "$OUTPUT_FORMAT" in
short)
printf '🧅 Tor Relay Status Summary\n'
separator
printf '📦 Build: %s\n' "${BUILD_INFO}"
[ "$BOOTSTRAP_PERCENT" -eq 100 ] && printf '🚀 Bootstrap: ✅ 100%% Complete\n' || printf '🚀 Bootstrap: %s%%\n' "$BOOTSTRAP_PERCENT"
[ "${IS_REACHABLE}" = "true" ] && printf '🌐 Reachable: ✅ %s\n' "${REACHABILITY_STATUS}" || printf '🌐 Reachable: ❌ %s\n' "${REACHABILITY_STATUS}"
printf '📊 Uptime: %s (%s)\n' "${UPTIME_DISPLAY}" "${UPTIME_SOURCE}"
printf '🔑 %s (%s)\n' "${NICKNAME}" "${FINGERPRINT}"
printf '🔌 ORPort: %s | DirPort: %s\n' "${ORPORT}" "${DIRPORT}"
printf '⚙️ Type: %s\n' "${RELAY_TYPE}"
printf '⚠️ Errors: %02d | Warnings: %d\n' "${ERROR_COUNT}" "${WARNING_COUNT}"
printf '🕒 %s\n\n' "${TIMESTAMP}"
;;
json)
cat << EOF
cat <<EOF
{
"timestamp": "$TIMESTAMP",
"status": "$OVERALL_STATUS",
"process": { "running": $IS_RUNNING, "uptime": "$UPTIME" },
"bootstrap": { "percent": $BOOTSTRAP_PERCENT, "message": "$BOOTSTRAP_MESSAGE" },
"reachability": { "reachable": $IS_REACHABLE, "message": "$REACHABILITY_MESSAGE" },
"identity": { "nickname": "$NICKNAME", "fingerprint": "$FINGERPRINT" },
"configuration": {
"orport": "$ORPORT",
"dirport": "$DIRPORT",
"exit_relay": $EXIT_RELAY,
"bridge_relay": $BRIDGE_RELAY,
"bandwidth": "$BANDWIDTH_RATE"
},
"network": { "ipv4": "$PUBLIC_IP", "ipv6": "$PUBLIC_IP6" },
"issues": { "errors": $ERROR_COUNT, "warnings": $WARNING_COUNT },
"version": { "software": "$VERSION_INFO", "build_time": "$BUILD_TIME" }
"timestamp": "${TIMESTAMP}",
"status": "${OVERALL_STATUS}",
"uptime": "${UPTIME_DISPLAY}",
"uptime_source": "${UPTIME_SOURCE}",
"bootstrap": ${BOOTSTRAP_PERCENT},
"reachable": "${IS_REACHABLE}",
"reachability_status": "${REACHABILITY_STATUS}",
"ipv4": "${PUBLIC_IP}",
"ipv6": "${PUBLIC_IP6}",
"orport": "${ORPORT}",
"dirport": "${DIRPORT}",
"orport_open": "${ORPORT_OPEN}",
"dirport_open": "${DIRPORT_OPEN}",
"nickname": "${NICKNAME}",
"fingerprint": "${FINGERPRINT}",
"relay_type": "${RELAY_TYPE}",
"contact_info": "${CONTACT_INFO}",
"errors": ${ERROR_COUNT},
"warnings": ${WARNING_COUNT},
"version": "${VERSION_INFO}",
"build_time": "${BUILD_TIME}",
"arch": "${ARCH}",
"build_info": "${BUILD_INFO}"
}
EOF
;;
plain)
echo "STATUS=$OVERALL_STATUS"
echo "RUNNING=$IS_RUNNING"
echo "UPTIME=$UPTIME"
echo "BOOTSTRAP=$BOOTSTRAP_PERCENT"
echo "REACHABLE=$IS_REACHABLE"
echo "NICKNAME=$NICKNAME"
echo "FINGERPRINT=$FINGERPRINT"
echo "ORPORT=$ORPORT"
echo "DIRPORT=$DIRPORT"
echo "ERRORS=$ERROR_COUNT"
echo "WARNINGS=$WARNING_COUNT"
echo "PUBLIC_IP=$PUBLIC_IP"
echo "PUBLIC_IP6=$PUBLIC_IP6"
printf 'timestamp=%s\n' "${TIMESTAMP}"
printf 'status=%s\n' "${OVERALL_STATUS}"
printf 'uptime=%s\n' "${UPTIME_DISPLAY}"
printf 'uptime_source=%s\n' "${UPTIME_SOURCE}"
printf 'bootstrap=%d\n' "${BOOTSTRAP_PERCENT}"
printf 'reachable=%s\n' "${IS_REACHABLE}"
printf 'reachability_status=%s\n' "${REACHABILITY_STATUS}"
printf 'ipv4=%s\n' "${PUBLIC_IP}"
printf 'ipv6=%s\n' "${PUBLIC_IP6}"
printf 'orport=%s\n' "${ORPORT}"
printf 'dirport=%s\n' "${DIRPORT}"
printf 'orport_open=%s\n' "${ORPORT_OPEN}"
printf 'dirport_open=%s\n' "${DIRPORT_OPEN}"
printf 'nickname=%s\n' "${NICKNAME}"
printf 'fingerprint=%s\n' "${FINGERPRINT}"
printf 'relay_type=%s\n' "${RELAY_TYPE}"
printf 'contact_info=%s\n' "${CONTACT_INFO}"
printf 'errors=%d\n' "${ERROR_COUNT}"
printf 'warnings=%d\n' "${WARNING_COUNT}"
printf 'version=%s\n' "${VERSION_INFO}"
printf 'build_time=%s\n' "${BUILD_TIME}"
printf 'arch=%s\n' "${ARCH}"
printf 'build_info=%s\n' "${BUILD_INFO}"
;;
*)
echo "🧅 Tor Relay Status Report"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
printf '🧅 Tor Relay Status Report\n'
separator
printf '\n⭐ Overall Status: '
case "$OVERALL_STATUS" in
healthy) echo "⭐ Overall Status: 🟢 OK - Relay is fully operational" ;;
running) echo "⭐ Overall Status: 🟡 RUNNING - Awaiting reachability confirmation" ;;
starting) echo "⭐ Overall Status: 🔄 STARTING - Bootstrap in progress ($BOOTSTRAP_PERCENT%)" ;;
down) echo "⭐ Overall Status: 🔴 FAIL - Tor process not running" ;;
*) echo "⭐ Overall Status: ❓ UNKNOWN" ;;
healthy) printf '🟢 OK - Relay is fully operational\n' ;;
running) printf '🟡 RUNNING - Awaiting reachability confirmation\n' ;;
starting) printf '🔄 STARTING - Bootstrap in progress (%s%%)\n' "$BOOTSTRAP_PERCENT" ;;
down) printf '🔴 FAIL - Tor process not running\n' ;;
*) printf '❓ UNKNOWN\n' ;;
esac
echo ""
if [ -n "$VERSION_INFO" ] || [ -n "$BUILD_TIME" ]; then
echo "📦 Build Information:"
[ -n "$VERSION_INFO" ] && echo " Version: $VERSION_INFO"
[ -n "$BUILD_TIME" ] && echo " Built: $BUILD_TIME"
echo ""
printf '\n📦 Build: %s\n' "${BUILD_INFO}"
printf '\n🚀 Bootstrap Progress:\n'
[ "$BOOTSTRAP_PERCENT" -eq 100 ] && printf ' ✅ 100%% Complete\n' || printf ' 🔄 %s%%\n' "$BOOTSTRAP_PERCENT"
printf '\n🌐 Network Status:\n'
[ "${IS_REACHABLE}" = "true" ] && printf ' 🟢 Reachable: Yes - Relay can accept connections\n' || printf ' 🔴 Reachable: No - Relay may be behind firewall/NAT\n'
printf ' IPv4: %s\n' "$(format_ip_status IPv4 "$PUBLIC_IP")"
printf ' IPv6: %s\n' "$(format_ip_status IPv6 "$PUBLIC_IP6")"
if [ "$ORPORT" != "N/A" ] && [ "$ORPORT" != "0" ]; then
[ "$ORPORT_OPEN" = "true" ] && printf ' 🟢 ORPort %s: Open\n' "$ORPORT" || printf ' 🔴 ORPort %s: Closed or filtered\n' "$ORPORT"
fi
if [ "$DIRPORT" != "N/A" ] && [ "$DIRPORT" != "0" ]; then
[ "$DIRPORT_OPEN" = "true" ] && printf ' 🟢 DirPort %s: Open\n' "$DIRPORT" || printf ' 🔴 DirPort %s: Closed or filtered\n' "$DIRPORT"
fi
echo "🚀 Bootstrap Progress:"
if is_integer "$BOOTSTRAP_PERCENT" && [ "$BOOTSTRAP_PERCENT" -eq 100 ]; then
echo " 🟢 OK - Fully bootstrapped (100%)"
[ -n "$BOOTSTRAP_MESSAGE" ] && echo " Status: $BOOTSTRAP_MESSAGE"
elif is_integer "$BOOTSTRAP_PERCENT" && [ "$BOOTSTRAP_PERCENT" -gt 0 ]; then
echo " 🔄 Bootstrapping: $BOOTSTRAP_PERCENT%"
[ -n "$BOOTSTRAP_MESSAGE" ] && echo " Status: $BOOTSTRAP_MESSAGE"
else
echo " ⏳ Not started yet"
fi
echo ""
printf '\n🔑 Relay Identity:\n'
printf ' 📝 Nickname: %s\n' "${NICKNAME}"
printf ' 🆔 Fingerprint: %s\n' "${FINGERPRINT}"
printf ' 📧 Contact: %s\n' "${CONTACT_INFO}"
echo "🌍 Reachability:"
if [ "$IS_REACHABLE" = "true" ]; then
echo " 🌐 Reachability: 🟢 OK"
elif [ -n "$REACHABILITY_MESSAGE" ]; then
echo " 🌐 Reachability: 🔴 $REACHABILITY_MESSAGE"
else
echo " 🌐 Reachability: ⏳ Pending"
fi
echo ""
printf '\n📊 Uptime:\n'
printf ' %s (%s)\n' "${UPTIME_DISPLAY}" "${UPTIME_SOURCE}"
if [ -n "$NICKNAME" ] || [ -n "$FINGERPRINT" ]; then
echo "🔑 Relay Identity:"
[ -n "$NICKNAME" ] && echo " 📝 Nickname: $NICKNAME"
[ -n "$FINGERPRINT" ] && echo " 🆔 Fingerprint: $FINGERPRINT"
echo ""
fi
printf '\n🔌 Configuration:\n'
printf ' ORPort: %s | DirPort: %s\n' "${ORPORT}" "${DIRPORT}"
printf ' Type: %s\n' "${RELAY_TYPE}"
echo "🔌 Network Configuration:"
[ -n "$PUBLIC_IP" ] && echo " IPv4: $(format_ip_status IPv4 "$PUBLIC_IP")" || echo " IPv4: 🔴 No IPv4 connectivity"
[ -n "$PUBLIC_IP6" ] && echo " IPv6: $(format_ip_status IPv6 "$PUBLIC_IP6")" || echo " IPv6: 🔴 No IPv6 connectivity"
[ -n "$ORPORT" ] && echo " ORPort: $ORPORT" || echo " ORPort: 🔴 Not configured"
[ -n "$DIRPORT" ] && echo " DirPort: $DIRPORT" || echo " DirPort: 🔴 Not configured"
[ -n "$BANDWIDTH_RATE" ] && echo " Bandwidth: $BANDWIDTH_RATE"
if [ "$EXIT_RELAY" = "true" ]; then
echo " Type: 🚪 Exit Relay"
elif [ "$BRIDGE_RELAY" = "true" ]; then
echo " Type: 🌉 Bridge Relay"
else
echo " Type: 🔒 Guard/Middle Relay"
fi
echo ""
printf '\n⚠ Errors: %d | Warnings: %d\n' "${ERROR_COUNT}" "${WARNING_COUNT}"
if [ "$ERROR_COUNT" -gt 0 ] || [ "$WARNING_COUNT" -gt 0 ]; then
echo "⚠️ Issues Summary:"
[ "$ERROR_COUNT" -gt 0 ] && echo " ❌ Errors: $ERROR_COUNT"
[ "$WARNING_COUNT" -gt 0 ] && echo " ⚠️ Warnings: $WARNING_COUNT"
if [ -n "$RECENT_ERRORS" ] && [ "$ERROR_COUNT" -gt 0 ]; then
echo ""
echo " Recent errors:"
echo "$RECENT_ERRORS" | sed 's/^/ /'
fi
echo ""
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💡 For live monitoring: docker logs -f <container-name>"
echo "🔗 Search your relay: https://metrics.torproject.org/rs.html"
[ -n "$FINGERPRINT" ] && echo "📊 Direct link: https://metrics.torproject.org/rs.html#search/$FINGERPRINT"
echo "🕒 Last updated: $TIMESTAMP"
printf '\n'
separator
printf '🕒 Last updated: %s\n' "${TIMESTAMP}"
;;
esac

View File

@@ -1,202 +1,183 @@
#!/bin/sh
# view-logs - Advanced log viewer with filtering and analysis
# view-logs - Advanced Tor relay log viewer with filtering and analysis
# Usage: docker exec guard-relay view-logs [--follow|--errors|--help]
set -e
# Configuration
VERSION="1.0.9"
VERSION="1.1.0"
LOG_FILE="${LOG_FILE:-/var/log/tor/notices.log}"
LOG_LINES="${LOG_LINES:-50}"
FOLLOW_MODE="${FOLLOW_MODE:-false}"
FILTER_MODE="${FILTER_MODE:-all}"
COLOR_OUTPUT="${COLOR_OUTPUT:-true}"
FOLLOW_MODE="false"
FILTER_MODE="all"
OUTPUT_FORMAT="text"
COLOR_OUTPUT="true"
# Colors
RED='\033[0;31m'
YELLOW='\033[1;33m'
GREEN='\033[0;32m'
BLUE='\033[0;34m'
NC='\033[0m'
# Force color by default
FORCE_COLOR="${FORCE_COLOR:-true}"
[ "$FORCE_COLOR" = "true" ] && COLOR_OUTPUT="true"
# Parse arguments
for arg in "$@"; do
case "$arg" in
# Colours
ESC="$(printf '\033')"
RED="${ESC}[0;31m"
YELLOW="${ESC}[1;33m"
GREEN="${ESC}[0;32m"
BLUE="${ESC}[0;34m"
CYAN="${ESC}[0;36m"
MAGENTA="${ESC}[0;35m"
BOLD="${ESC}[1m"
NC="${ESC}[0m"
is_integer() {
case "$1" in
''|*[!0-9]*) return 1 ;;
*) return 0 ;;
esac
}
# Argument parsing
while [ $# -gt 0 ]; do
case "$1" in
--help|-h)
cat << EOF
📜 Tor-Guard-Relay Log Viewer v${VERSION}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
USAGE:
view-logs [OPTIONS]
USAGE: view-logs [OPTIONS]
OPTIONS:
--follow, -f Follow log output (tail -f)
--all Show all log entries (default)
--errors Show only errors
--warnings Show only warnings
--info Show only info messages
--bootstrap Show bootstrap progress
--last N Show last N lines (default: 50)
--no-color Disable colored output
--json Output as JSON
--help, -h Show this help message
ENVIRONMENT VARIABLES:
LOG_FILE Path to log file
LOG_LINES Default number of lines to show
COLOR_OUTPUT Enable colored output (true/false)
FILTER MODES:
all All log entries
errors Error messages only
warnings Warning messages only
info Info/notice messages
bootstrap Bootstrap related messages
network Network/connectivity messages
EXAMPLES:
view-logs # Last 50 lines
view-logs --follow # Follow new entries
view-logs --errors # Show only errors
view-logs --last 100 # Show last 100 lines
view-logs --bootstrap # Bootstrap progress
LOG LEVELS:
[err] Error - Critical issues
[warn] Warning - Potential problems
[notice] Notice - Normal operations
[info] Info - Detailed information
--follow, -f Follow log output (tail -f)
--all Show all log entries (default)
--errors Show only errors
--warnings Show only warnings
--info Show only info messages
--bootstrap Show bootstrap progress
--network Show network/connectivity logs
--last N Show last N lines (default: 50)
--no-color Disable color output
--json Output as JSON
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
EOF
exit 0
;;
--follow|-f) FOLLOW_MODE="true" ;;
--all) FILTER_MODE="all" ;;
--errors) FILTER_MODE="errors" ;;
--warnings) FILTER_MODE="warnings" ;;
--info) FILTER_MODE="info" ;;
--bootstrap) FILTER_MODE="bootstrap" ;;
--network) FILTER_MODE="network" ;;
exit 0 ;;
--follow|-f) FOLLOW_MODE="true"; shift ;;
--all) FILTER_MODE="all"; shift ;;
--errors) FILTER_MODE="errors"; shift ;;
--warnings) FILTER_MODE="warnings"; shift ;;
--info) FILTER_MODE="info"; shift ;;
--bootstrap) FILTER_MODE="bootstrap"; shift ;;
--network) FILTER_MODE="network"; shift ;;
--last)
shift
LOG_LINES="$1"
shift
;;
--no-color) COLOR_OUTPUT="false" ;;
--json) OUTPUT_FORMAT="json" ;;
-*)
echo "❌ Unknown option: $arg"
echo "💡 Use --help for usage information"
exit 2
;;
if is_integer "$1"; then LOG_LINES="$1"; shift
else echo "❌ Invalid number for --last: $1"; exit 2; fi ;;
--no-color) COLOR_OUTPUT="false"; shift ;;
--json) OUTPUT_FORMAT="json"; shift ;;
-*) echo "❌ Unknown option: $1"; exit 2 ;;
esac
done
# Check if log file exists
# Verify file
if [ ! -f "$LOG_FILE" ]; then
if [ "$OUTPUT_FORMAT" = "json" ]; then
echo '{"error":"Log file not found","path":"'$LOG_FILE'"}'
else
echo "⚠️ Log file not found: $LOG_FILE"
echo "📍 Tor might still be starting."
echo "💡 Check back in a moment or verify Tor is running."
fi
case "$OUTPUT_FORMAT" in
json) printf '{"error":"Log file not found","path":"%s"}\n' "$LOG_FILE" ;;
*) echo "⚠️ Log file not found: $LOG_FILE"
echo "📍 Tor might still be starting or logging elsewhere."
echo "💡 Check again shortly." ;;
esac
exit 1
fi
# Function to colorize log lines
colorize_line() {
if [ "$COLOR_OUTPUT" != "true" ]; then
cat
return
fi
sed -e "s/\[err\]/$(printf "${RED}[err]${NC}")/g" \
-e "s/\[error\]/$(printf "${RED}[error]${NC}")/g" \
-e "s/\[warn\]/$(printf "${YELLOW}[warn]${NC}")/g" \
-e "s/\[warning\]/$(printf "${YELLOW}[warning]${NC}")/g" \
-e "s/\[notice\]/$(printf "${GREEN}[notice]${NC}")/g" \
-e "s/\[info\]/$(printf "${BLUE}[info]${NC}")/g" \
-e "s/Bootstrapped [0-9]*%/$(printf "${GREEN}&${NC}")/g"
}
# Function to apply filters
apply_filter() {
case "$FILTER_MODE" in
errors)
grep -iE "\[err\]|\[error\]|failed|failure|critical"
;;
warnings)
grep -iE "\[warn\]|\[warning\]"
;;
info)
grep -iE "\[notice\]|\[info\]"
;;
bootstrap)
grep -iE "bootstrap|starting|loading|opening|establishing"
;;
network)
grep -iE "reachable|connection|network|port|address"
;;
*)
cat
;;
esac
}
# JSON output mode
if [ "$OUTPUT_FORMAT" = "json" ]; then
TOTAL_LINES=$(wc -l < "$LOG_FILE")
ERROR_COUNT=$(grep -cE "\[err\]|\[error\]" "$LOG_FILE" 2>/dev/null || echo 0)
WARNING_COUNT=$(grep -cE "\[warn\]|\[warning\]" "$LOG_FILE" 2>/dev/null || echo 0)
echo '{'
echo ' "file": "'$LOG_FILE'",'
echo ' "total_lines": '$TOTAL_LINES','
echo ' "error_count": '$ERROR_COUNT','
echo ' "warning_count": '$WARNING_COUNT','
echo ' "entries": ['
tail -n "$LOG_LINES" "$LOG_FILE" | apply_filter | while IFS= read -r line; do
# Escape quotes and backslashes for JSON
line=$(echo "$line" | sed 's/\\/\\\\/g; s/"/\\"/g')
echo ' "'$line'",'
done | sed '$ s/,$//'
echo ' ]'
echo '}'
exit 0
# Read identity
FP_NICKNAME=""
FP_FINGERPRINT=""
if [ -f /var/lib/tor/fingerprint ]; then
FP_NICKNAME=$(awk '{print $1}' /var/lib/tor/fingerprint 2>/dev/null || true)
FP_FINGERPRINT=$(awk '{print $2}' /var/lib/tor/fingerprint 2>/dev/null || true)
FP_NICKNAME=$(printf '%s' "$FP_NICKNAME" | tr -d '\r\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
FP_FINGERPRINT=$(printf '%s' "$FP_FINGERPRINT" | tr -d '\r\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
fi
# Regular output mode
# Filter expressions
case "$FILTER_MODE" in
errors) FILTER_EXPR='\[err\]|\[error\]|failed|failure|critical' ;;
warnings) FILTER_EXPR='\[warn\]|\[warning\]' ;;
info) FILTER_EXPR='\[notice\]|\[info\]' ;;
bootstrap) FILTER_EXPR='bootstrapped|starting|loading|establishing' ;;
network) FILTER_EXPR='reachable|connection|network|port|address' ;;
*) FILTER_EXPR='' ;;
esac
# Stats
if [ "$FILTER_MODE" = "all" ]; then
TOTAL_MATCHES=$(wc -l < "$LOG_FILE" 2>/dev/null || echo 0)
ERROR_COUNT=$(grep -ciE '\[err\]|\[error\]' "$LOG_FILE" 2>/dev/null || echo 0)
WARNING_COUNT=$(grep -ciE '\[warn\]|\[warning\]' "$LOG_FILE" 2>/dev/null || echo 0)
else
TOTAL_MATCHES=$(grep -ciE "$FILTER_EXPR" "$LOG_FILE" 2>/dev/null || echo 0)
ERROR_COUNT=$(grep -iE "$FILTER_EXPR" "$LOG_FILE" 2>/dev/null | grep -ciE '\[err\]|\[error\]' || echo 0)
WARNING_COUNT=$(grep -iE "$FILTER_EXPR" "$LOG_FILE" 2>/dev/null | grep -ciE '\[warn\]|\[warning\]' || echo 0)
fi
sanitize_num() { printf '%s' "$1" | tr -d '\r\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'; }
TOTAL_MATCHES=$(sanitize_num "$TOTAL_MATCHES")
ERROR_COUNT=$(sanitize_num "$ERROR_COUNT")
WARNING_COUNT=$(sanitize_num "$WARNING_COUNT")
sanitize_stream() { sed "s|${ESC}\[[0-9;]*[mK]||g; s/\r$//"; }
# Colorize output
colorize_line() {
if [ "$COLOR_OUTPUT" != "true" ]; then cat; return; fi
sed -E \
-e "s/\[err\]/${RED}[err]${NC}/Ig" \
-e "s/\[error\]/${RED}[error]${NC}/Ig" \
-e "s/\[warn\]/${YELLOW}[warn]${NC}/Ig" \
-e "s/\[warning\]/${YELLOW}[warning]${NC}/Ig" \
-e "s/\[notice\]/${GREEN}[notice]${NC}/Ig" \
-e "s/\[info\]/${BLUE}[info]${NC}/Ig" \
-e "s/Bootstrapped[[:space:]]*[0-9]{1,3}%/${GREEN}&${NC}/Ig" \
-e "s/(Your Tor server's identity key fingerprint is ')[[:space:]]*([^[:space:]]+)[[:space:]]+([A-F0-9]{16,})(')/\1${CYAN}\2${NC} ${MAGENTA}\3${NC}\4/Ig" \
-e "s/(Your Tor server's identity key ed25519 fingerprint is ')[[:space:]]*([^[:space:]]+)[[:space:]]+([A-Za-z0-9+\/=]{32,})(')/\1${CYAN}\2${NC} ${MAGENTA}\3${NC}\4/Ig" \
-e "s/([A-F0-9]{10,})([[:space:]]*[A-F0-9]{2,})*/${MAGENTA}&${NC}/Ig" \
-e "s/([A-Za-z0-9+\/]{40,}={0,2})/${MAGENTA}&${NC}/Ig"
}
# Header
echo "📜 Tor Relay Logs"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📁 File: $LOG_FILE"
echo "🔍 Filter: $FILTER_MODE"
printf "📊 Stats: %s total | %s errors | %s warnings\n" "$TOTAL_MATCHES" "$ERROR_COUNT" "$WARNING_COUNT"
# Count log entries
if [ "$FILTER_MODE" = "all" ]; then
TOTAL_LINES=$(wc -l < "$LOG_FILE")
ERROR_COUNT=$(grep -cE "\[err\]|\[error\]" "$LOG_FILE" 2>/dev/null || echo 0)
WARNING_COUNT=$(grep -cE "\[warn\]|\[warning\]" "$LOG_FILE" 2>/dev/null || echo 0)
echo "📊 Stats: $TOTAL_LINES total | $ERROR_COUNT errors | $WARNING_COUNT warnings"
if [ -n "$FP_FINGERPRINT" ] || [ -n "$FP_NICKNAME" ]; then
if [ "$COLOR_OUTPUT" = "true" ]; then
echo "🔑 Identity:"
printf " Nickname ✨: %s\n" "${CYAN}${FP_NICKNAME:-unknown}${NC}"
printf " Fingerprint 🫆: %s\n" "${MAGENTA}${FP_FINGERPRINT:-unknown}${NC}"
else
echo "🔑 Identity:"
echo " Nickname ✨: ${FP_NICKNAME:-unknown}"
echo " Fingerprint 🫆: ${FP_FINGERPRINT:-unknown}"
fi
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Display logs
# Stream logs
if [ "$FOLLOW_MODE" = "true" ]; then
echo "🔄 Following log output (Ctrl+C to stop)..."
echo "🔄 Following live output (Ctrl+C to stop)..."
echo ""
tail -n "$LOG_LINES" -f "$LOG_FILE" | apply_filter | colorize_line
if [ "$FILTER_MODE" = "all" ]; then
tail -n "$LOG_LINES" -f "$LOG_FILE" | sanitize_stream | colorize_line
else
tail -n "$LOG_LINES" -f "$LOG_FILE" | sanitize_stream | grep -iE "$FILTER_EXPR" 2>/dev/null | colorize_line
fi
else
tail -n "$LOG_LINES" "$LOG_FILE" | apply_filter | colorize_line
if [ "$FILTER_MODE" = "all" ]; then
tail -n "$LOG_LINES" "$LOG_FILE" | sanitize_stream | colorize_line
else
tail -n "$LOG_LINES" "$LOG_FILE" | sanitize_stream | grep -iE "$FILTER_EXPR" 2>/dev/null | colorize_line
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "💡 Use 'view-logs --follow' for live updates"
fi
fi