feat(haproxy): Add exposed services integration and fix cert key naming

- Fix HAProxy certificate key naming (.key -> .crt.key) for directory loading
- Add auto-fix in container startup script for existing certificates
- Add list_exposed_services RPC method to fetch services from secubox-exposure
- Add dynamic port scanning for running services discovery
- Add "Quick Select" dropdown in Add Server modal for service auto-fill
- Bump luci-app-haproxy to 1.0.0-r8
- Bump secubox-app-haproxy to 1.0.0-r15

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2026-01-26 08:34:57 +01:00
parent 62cf871eeb
commit c9075bc190
7 changed files with 321 additions and 106 deletions

View File

@ -11,7 +11,7 @@ LUCI_PKGARCH:=all
PKG_NAME:=luci-app-haproxy
PKG_VERSION:=1.0.0
PKG_RELEASE:=7
PKG_RELEASE:=8
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_LICENSE:=MIT

View File

@ -282,6 +282,12 @@ var callGetLogs = rpc.declare({
expect: { logs: '' }
});
var callListExposedServices = rpc.declare({
object: 'luci.haproxy',
method: 'list_exposed_services',
expect: { services: [] }
});
// ============================================
// Helper Functions
// ============================================
@ -367,6 +373,9 @@ return baseclass.extend({
validate: callValidate,
getLogs: callGetLogs,
// Exposed services
listExposedServices: callListExposedServices,
// Helpers
getDashboardData: getDashboardData
});

View File

@ -23,7 +23,8 @@ return view.extend({
var backends = (result && result.backends) || result || [];
return Promise.all([
Promise.resolve(backends),
api.listServers('')
api.listServers(''),
api.listExposedServices()
]);
});
},
@ -33,6 +34,8 @@ return view.extend({
var backends = data[0] || [];
var serversResult = data[1] || {};
var servers = (serversResult && serversResult.servers) || serversResult || [];
var exposedResult = data[2] || {};
self.exposedServices = (exposedResult && exposedResult.services) || exposedResult || [];
// Group servers by backend
var serversByBackend = {};
@ -405,9 +408,45 @@ return view.extend({
showAddServerModal: function(backend) {
var self = this;
var exposedServices = self.exposedServices || [];
// Build service selector options
var serviceOptions = [E('option', { 'value': '' }, '-- Select a service --')];
exposedServices.forEach(function(svc) {
var label = svc.name + ' (' + svc.address + ':' + svc.port + ')';
if (svc.category) label += ' [' + svc.category + ']';
serviceOptions.push(E('option', {
'value': JSON.stringify(svc),
'data-name': svc.name,
'data-address': svc.address,
'data-port': svc.port
}, label));
});
ui.showModal('Add Server to ' + backend.name, [
E('div', { 'style': 'max-width: 500px;' }, [
// Quick service selector
exposedServices.length > 0 ? E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Quick Select'),
E('div', { 'class': 'cbi-value-field' }, [
E('select', {
'id': 'modal-service-select',
'class': 'cbi-input-select',
'style': 'width: 100%;',
'change': function(ev) {
var val = ev.target.value;
if (val) {
var svc = JSON.parse(val);
document.getElementById('modal-server-name').value = svc.name;
document.getElementById('modal-server-address').value = svc.address;
document.getElementById('modal-server-port').value = svc.port;
}
}
}, serviceOptions),
E('small', { 'style': 'color: var(--hp-text-muted); display: block; margin-top: 4px;' },
'Select a known service to auto-fill details, or enter manually below')
])
]) : null,
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' }, 'Server Name'),
E('div', { 'class': 'cbi-value-field' }, [

View File

@ -1285,6 +1285,74 @@ method_get_logs() {
json_dump
}
# List exposed services (from secubox-exposure config)
method_list_exposed_services() {
json_init
json_add_array "services"
# Load known services from exposure config
if uci -q show secubox-exposure >/dev/null 2>&1; then
config_load "secubox-exposure"
config_foreach _add_exposed_service known
fi
# Also scan listening ports for dynamic discovery
if command -v netstat >/dev/null 2>&1; then
netstat -tlnp 2>/dev/null | grep LISTEN | while read line; do
local addr_port=$(echo "$line" | awk '{print $4}')
local port=$(echo "$addr_port" | awk -F: '{print $NF}')
local proc=$(echo "$line" | awk '{print $7}' | cut -d'/' -f2)
# Skip if already added from known services or common system ports
case "$port" in
22|53|80|443|8404) continue ;;
esac
# Only add if process name is useful
if [ -n "$proc" ] && [ "$proc" != "-" ] && [ "$proc" != "unknown" ]; then
json_add_object
json_add_string "id" "dynamic_${proc}_${port}"
json_add_string "name" "$proc"
json_add_int "port" "$port"
json_add_string "address" "127.0.0.1"
json_add_string "category" "detected"
json_add_boolean "dynamic" 1
json_close_object
fi
done
fi
json_close_array
json_dump
}
_add_exposed_service() {
local section="$1"
local default_port config_path category actual_port
config_get default_port "$section" default_port ""
config_get config_path "$section" config_path ""
config_get category "$section" category "app"
[ -z "$default_port" ] && return
# Try to get actual port from UCI config if available
actual_port="$default_port"
if [ -n "$config_path" ]; then
local configured_port=$(uci -q get "$config_path" 2>/dev/null)
[ -n "$configured_port" ] && actual_port="$configured_port"
fi
json_add_object
json_add_string "id" "$section"
json_add_string "name" "$section"
json_add_int "port" "$actual_port"
json_add_string "address" "127.0.0.1"
json_add_string "category" "$category"
json_add_boolean "dynamic" 0
json_close_object
}
# Main RPC interface
case "$1" in
list)
@ -1326,7 +1394,8 @@ case "$1" in
"reload": {},
"generate": {},
"validate": {},
"get_logs": { "lines": "integer" }
"get_logs": { "lines": "integer" },
"list_exposed_services": {}
}
EOF
;;
@ -1369,6 +1438,7 @@ EOF
generate) method_generate ;;
validate) method_validate ;;
get_logs) method_get_logs ;;
list_exposed_services) method_list_exposed_services ;;
esac
;;
esac

View File

@ -270,13 +270,24 @@ EOF
echo -e " ${CYAN}Port:${NC} $onion_port -> 127.0.0.1:$local_port"
echo ""
# Save to UCI
# Save to exposure UCI
uci set "${CONFIG_NAME}.${service}=service"
uci set "${CONFIG_NAME}.${service}.port=$local_port"
uci set "${CONFIG_NAME}.${service}.tor=1"
uci set "${CONFIG_NAME}.${service}.tor_onion=$onion"
uci set "${CONFIG_NAME}.${service}.tor_port=$onion_port"
uci commit "$CONFIG_NAME"
# Sync to Tor Shield UCI
local hs_name="hs_${service}"
uci set "tor-shield.${hs_name}=hidden_service"
uci set "tor-shield.${hs_name}.name=${service}"
uci set "tor-shield.${hs_name}.enabled=1"
uci set "tor-shield.${hs_name}.local_port=${local_port}"
uci set "tor-shield.${hs_name}.onion_port=${onion_port}"
uci set "tor-shield.${hs_name}.onion_address=${onion}"
uci commit tor-shield
log_ok "Synced to Tor Shield"
else
log_err "Failed to generate onion address"
return 1
@ -335,22 +346,77 @@ cmd_tor_remove() {
# Remove directory
rm -rf "$hidden_dir"
# Update UCI
# Update exposure UCI
uci delete "${CONFIG_NAME}.${service}.tor" 2>/dev/null
uci delete "${CONFIG_NAME}.${service}.tor_onion" 2>/dev/null
uci delete "${CONFIG_NAME}.${service}.tor_port" 2>/dev/null
uci commit "$CONFIG_NAME"
# Remove from Tor Shield UCI
local hs_name="hs_${service}"
if uci -q get "tor-shield.${hs_name}" >/dev/null 2>&1; then
uci delete "tor-shield.${hs_name}"
uci commit tor-shield
log_ok "Removed from Tor Shield"
fi
# Restart Tor
/etc/init.d/tor restart 2>/dev/null || systemctl restart tor 2>/dev/null
log_ok "Hidden service removed"
}
cmd_tor_sync() {
load_config
log_info "Syncing hidden services to Tor Shield..."
local synced=0
# List from filesystem and sync to Tor Shield
if [ -d "$TOR_HIDDEN_DIR" ]; then
for dir in "$TOR_HIDDEN_DIR"/*/; do
[ -d "$dir" ] || continue
local svc=$(basename "$dir")
local onion=""
[ -f "$dir/hostname" ] && onion=$(cat "$dir/hostname")
# Get port from torrc
local port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{print $2}')
local local_port=$(grep -A1 "HiddenServiceDir $dir" "$TOR_CONFIG" 2>/dev/null | grep HiddenServicePort | awk '{split($3,a,":"); print a[2]}')
if [ -n "$onion" ]; then
local hs_name="hs_${svc}"
if ! uci -q get "tor-shield.${hs_name}" >/dev/null 2>&1; then
log_info "Adding $svc to Tor Shield"
uci set "tor-shield.${hs_name}=hidden_service"
uci set "tor-shield.${hs_name}.name=${svc}"
uci set "tor-shield.${hs_name}.enabled=1"
uci set "tor-shield.${hs_name}.local_port=${local_port:-80}"
uci set "tor-shield.${hs_name}.onion_port=${port:-80}"
uci set "tor-shield.${hs_name}.onion_address=${onion}"
synced=$((synced + 1))
fi
fi
done
fi
if [ "$synced" -gt 0 ]; then
uci commit tor-shield
log_ok "Synced $synced hidden service(s) to Tor Shield"
else
log_info "All hidden services already synced"
fi
}
# ============================================================================
# HAPROXY SSL BACKENDS
# HAPROXY SSL BACKENDS (UCI-based integration with haproxyctl)
# ============================================================================
# Sanitize name for UCI section (replace dots/hyphens with underscores)
sanitize_uci_name() {
echo "$1" | sed 's/[.-]/_/g'
}
cmd_ssl_add() {
local service="$1"
local domain="$2"
@ -379,80 +445,66 @@ cmd_ssl_add() {
fi
fi
# Check if HAProxy config exists
if [ ! -f "$HAPROXY_CONFIG" ]; then
log_err "HAProxy config not found: $HAPROXY_CONFIG"
# Check if haproxyctl exists
if [ ! -x "/usr/sbin/haproxyctl" ]; then
log_err "haproxyctl not found. Is secubox-app-haproxy installed?"
return 1
fi
# Check if already configured
if grep -q "backend ${service}_backend" "$HAPROXY_CONFIG"; then
log_warn "Backend for $service already exists in HAProxy config"
return 0
fi
# Sanitize names for UCI
local backend_name="$service"
local vhost_name=$(sanitize_uci_name "$domain")
log_info "Adding SSL backend for $service ($domain -> 127.0.0.1:$local_port)"
# Create backend config
local backend_config="
# Backend for $service (added by secubox-exposure)
backend ${service}_backend
mode http
option httpchk GET /
http-request set-header X-Forwarded-Proto https
server ${service} 127.0.0.1:$local_port check
"
# Add ACL to https frontend
local acl_line=" acl host_${service} hdr(host) -i $domain"
local use_line=" use_backend ${service}_backend if host_${service}"
# Check if https-in frontend exists
if grep -q "frontend https-in" "$HAPROXY_CONFIG"; then
# Add ACL and use_backend before the default_backend line
sed -i "/frontend https-in/,/default_backend/ {
/default_backend/ i\\
$acl_line\\
$use_line
}" "$HAPROXY_CONFIG"
# Check if backend already exists in UCI
if uci -q get "haproxy.${backend_name}" >/dev/null 2>&1; then
log_warn "Backend '$backend_name' already exists in HAProxy UCI config"
else
log_warn "No https-in frontend found. Adding basic HTTPS frontend."
cat >> "$HAPROXY_CONFIG" << EOF
frontend https-in
bind *:443 ssl crt $HAPROXY_CERTS/
mode http
option httplog
$acl_line
$use_line
default_backend default_backend
EOF
# Create backend in HAProxy UCI config
log_info "Adding backend '$backend_name' (127.0.0.1:$local_port)"
uci set "haproxy.${backend_name}=backend"
uci set "haproxy.${backend_name}.name=${backend_name}"
uci set "haproxy.${backend_name}.mode=http"
uci set "haproxy.${backend_name}.balance=roundrobin"
uci set "haproxy.${backend_name}.enabled=1"
uci add_list "haproxy.${backend_name}.server=${service} 127.0.0.1:${local_port} check"
fi
# Add backend at end of file
echo "$backend_config" >> "$HAPROXY_CONFIG"
# Check if vhost already exists
if uci -q get "haproxy.${vhost_name}" >/dev/null 2>&1; then
log_warn "Vhost for '$domain' already exists"
else
# Create vhost in HAProxy UCI config
log_info "Adding vhost '$domain' -> backend '$backend_name'"
uci set "haproxy.${vhost_name}=vhost"
uci set "haproxy.${vhost_name}.domain=${domain}"
uci set "haproxy.${vhost_name}.backend=${backend_name}"
uci set "haproxy.${vhost_name}.ssl=1"
uci set "haproxy.${vhost_name}.ssl_redirect=1"
uci set "haproxy.${vhost_name}.enabled=1"
fi
# Save to UCI
# Commit HAProxy UCI changes
uci commit haproxy
# Also save to exposure UCI for tracking
uci set "${CONFIG_NAME}.${service}=service"
uci set "${CONFIG_NAME}.${service}.port=$local_port"
uci set "${CONFIG_NAME}.${service}.ssl=1"
uci set "${CONFIG_NAME}.${service}.ssl_domain=$domain"
uci commit "$CONFIG_NAME"
log_ok "HAProxy backend added for $service"
log_ok "HAProxy UCI config updated"
log_info "Domain: $domain -> 127.0.0.1:$local_port"
log_warn "Note: You need to add SSL certificate for $domain to $HAPROXY_CERTS/"
log_info "Reloading HAProxy..."
# Reload HAProxy (in LXC container)
if [ -x "/usr/sbin/haproxyctl" ]; then
/usr/sbin/haproxyctl reload
else
lxc-attach -n haproxy -- /etc/init.d/haproxy reload 2>/dev/null || \
/etc/init.d/haproxy reload 2>/dev/null
fi
# Regenerate and reload HAProxy
log_info "Regenerating HAProxy config..."
/usr/sbin/haproxyctl generate
log_info "Reloading HAProxy..."
/usr/sbin/haproxyctl reload
log_ok "SSL backend configured"
log_warn "Note: Ensure SSL certificate exists for $domain"
}
cmd_ssl_list() {
@ -463,21 +515,27 @@ cmd_ssl_list() {
printf "%-15s %-30s %-20s\n" "SERVICE" "DOMAIN" "BACKEND"
printf "%-15s %-30s %-20s\n" "---------------" "------------------------------" "--------------------"
# Parse from HAProxy config
if [ -f "$HAPROXY_CONFIG" ]; then
grep -E "^backend .+_backend$" "$HAPROXY_CONFIG" | while read line; do
local backend=$(echo "$line" | awk '{print $2}')
local service=$(echo "$backend" | sed 's/_backend$//')
# Read from HAProxy UCI config (vhosts with their backends)
local found=0
for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do
local domain=$(uci -q get "haproxy.${vhost}.domain")
local backend=$(uci -q get "haproxy.${vhost}.backend")
local enabled=$(uci -q get "haproxy.${vhost}.enabled")
# Get domain from ACL
local domain=$(grep "acl host_${service} " "$HAPROXY_CONFIG" | awk '{print $NF}')
[ "$enabled" != "1" ] && continue
[ -z "$domain" ] && continue
# Get server line
local server=$(grep -A5 "backend $backend" "$HAPROXY_CONFIG" | grep "server " | awk '{print $3}')
# Get server from backend
local server=""
if [ -n "$backend" ]; then
server=$(uci -q get "haproxy.${backend}.server" | head -1 | awk '{print $2}')
fi
printf "%-15s %-30s %-20s\n" "$service" "${domain:-N/A}" "${server:-N/A}"
done
fi
printf "%-15s %-30s %-20s\n" "${backend:-N/A}" "$domain" "${server:-N/A}"
found=1
done
[ "$found" = "0" ] && echo " No SSL backends configured"
echo ""
}
@ -491,38 +549,51 @@ cmd_ssl_remove() {
load_config
if [ ! -f "$HAPROXY_CONFIG" ]; then
log_err "HAProxy config not found"
# Check if haproxyctl exists
if [ ! -x "/usr/sbin/haproxyctl" ]; then
log_err "haproxyctl not found"
return 1
fi
if ! grep -q "backend ${service}_backend" "$HAPROXY_CONFIG"; then
log_err "No backend found for $service"
local backend_name="$service"
local removed=0
# Find and remove vhosts pointing to this backend
for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do
local vhost_backend=$(uci -q get "haproxy.${vhost}.backend")
if [ "$vhost_backend" = "$backend_name" ]; then
log_info "Removing vhost '$vhost'"
uci delete "haproxy.${vhost}"
removed=1
fi
done
# Remove backend if it exists
if uci -q get "haproxy.${backend_name}" >/dev/null 2>&1; then
log_info "Removing backend '$backend_name'"
uci delete "haproxy.${backend_name}"
removed=1
fi
if [ "$removed" = "0" ]; then
log_err "No backend or vhost found for '$service'"
return 1
fi
log_info "Removing SSL backend for $service"
# Commit HAProxy UCI changes
uci commit haproxy
# Remove ACL and use_backend lines
sed -i "/acl host_${service} /d" "$HAPROXY_CONFIG"
sed -i "/use_backend ${service}_backend/d" "$HAPROXY_CONFIG"
# Remove backend block
sed -i "/# Backend for $service/,/^$/d" "$HAPROXY_CONFIG"
sed -i "/^backend ${service}_backend$/,/^$/d" "$HAPROXY_CONFIG"
# Update UCI
# Update exposure UCI
uci delete "${CONFIG_NAME}.${service}.ssl" 2>/dev/null
uci delete "${CONFIG_NAME}.${service}.ssl_domain" 2>/dev/null
uci commit "$CONFIG_NAME"
# Reload HAProxy
if [ -x "/usr/sbin/haproxyctl" ]; then
/usr/sbin/haproxyctl reload
else
lxc-attach -n haproxy -- /etc/init.d/haproxy reload 2>/dev/null || \
/etc/init.d/haproxy reload 2>/dev/null
fi
# Regenerate and reload HAProxy
log_info "Regenerating HAProxy config..."
/usr/sbin/haproxyctl generate
log_info "Reloading HAProxy..."
/usr/sbin/haproxyctl reload
log_ok "SSL backend removed"
}
@ -563,16 +634,19 @@ cmd_status() {
fi
echo ""
# HAProxy backends
# HAProxy backends (from UCI)
local ssl_backends=0
[ -f "$HAPROXY_CONFIG" ] && ssl_backends=$(grep -c "^backend.*_backend$" "$HAPROXY_CONFIG" 2>/dev/null || echo 0)
echo -e "${BLUE}HAProxy SSL Backends:${NC} $ssl_backends"
if [ "$ssl_backends" -gt 0 ] && [ -f "$HAPROXY_CONFIG" ]; then
grep -E "^backend .+_backend$" "$HAPROXY_CONFIG" | while read line; do
local backend=$(echo "$line" | awk '{print $2}' | sed 's/_backend$//')
echo " - $backend"
done
fi
echo -e "${BLUE}HAProxy SSL Backends:${NC}"
for vhost in $(uci show haproxy 2>/dev/null | grep "=vhost$" | cut -d'.' -f2 | cut -d'=' -f1); do
local domain=$(uci -q get "haproxy.${vhost}.domain")
local backend=$(uci -q get "haproxy.${vhost}.backend")
local enabled=$(uci -q get "haproxy.${vhost}.enabled")
[ "$enabled" != "1" ] && continue
[ -z "$domain" ] && continue
echo " - ${backend}: ${domain}"
ssl_backends=$((ssl_backends + 1))
done
[ "$ssl_backends" = "0" ] && echo " (none configured)"
echo ""
echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}"
}
@ -592,6 +666,7 @@ COMMANDS:
tor add <svc> [port] Create Tor hidden service
tor list List hidden services
tor remove <svc> Remove hidden service
tor sync Sync hidden services to Tor Shield
ssl add <svc> <domain> Add HAProxy SSL backend
ssl list List SSL backends
@ -636,7 +711,8 @@ case "$1" in
add) cmd_tor_add "$3" "$4" "$5" ;;
list) cmd_tor_list ;;
remove) cmd_tor_remove "$3" ;;
*) log_err "Usage: secubox-exposure tor {add|list|remove}"; exit 1 ;;
sync) cmd_tor_sync ;;
*) log_err "Usage: secubox-exposure tor {add|list|remove|sync}"; exit 1 ;;
esac
;;
ssl)

View File

@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=secubox-app-haproxy
PKG_VERSION:=1.0.0
PKG_RELEASE:=14
PKG_RELEASE:=15
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
PKG_LICENSE:=MIT

View File

@ -246,6 +246,20 @@ echo "Config: $CONFIG_FILE"
ls -la /opt/haproxy/
ls -la /opt/haproxy/certs/ 2>/dev/null || echo "No certs dir"
# Fix certificate key naming for HAProxy compatibility
# HAProxy expects .crt.key when it finds a .crt file in the directory
if [ -d "/opt/haproxy/certs" ]; then
for crt in /opt/haproxy/certs/*.crt; do
[ -f "$crt" ] || continue
base="${crt%.crt}"
# If .key exists but .crt.key doesn't, rename it
if [ -f "${base}.key" ] && [ ! -f "${crt}.key" ]; then
echo "[haproxy] Renaming ${base}.key -> ${crt}.key"
mv "${base}.key" "${crt}.key"
fi
done
fi
# Wait for config
if [ ! -f "$CONFIG_FILE" ]; then
echo "[haproxy] Config not found, generating default..."
@ -637,6 +651,13 @@ cmd_cert_add() {
log_info "Creating combined PEM for HAProxy..."
cat "$CERTS_PATH/$domain.fullchain.pem" "$CERTS_PATH/$domain.key" > "$CERTS_PATH/$domain.pem"
chmod 600 "$CERTS_PATH/$domain.pem"
# HAProxy expects key files named <cert>.key when loading .crt files from directory
# Rename the key file to match the .crt file naming convention
if [ -f "$CERTS_PATH/$domain.crt" ] && [ -f "$CERTS_PATH/$domain.key" ]; then
mv "$CERTS_PATH/$domain.key" "$CERTS_PATH/$domain.crt.key"
chmod 600 "$CERTS_PATH/$domain.crt.key"
fi
fi
# Restart HAProxy if it was running