This is an automated email from the ASF dual-hosted git repository.

weizhouapache pushed a commit to branch network-namespace
in repository https://gitbox.apache.org/repos/asf/cloudstack-extensions.git


The following commit(s) were added to refs/heads/network-namespace by this push:
     new 6877726  NE: fix apparmor and dnsmasq/haproxy on SUSE15
6877726 is described below

commit 6877726784f8f27210b20d3ae8943be18fc01646
Author: Wei Zhou <[email protected]>
AuthorDate: Thu Aug 27 16:36:57 2026 +0200

    NE: fix apparmor and dnsmasq/haproxy on SUSE15
---
 Network-Namespace/network-namespace-wrapper.sh | 212 ++++++++++++++++++++++---
 1 file changed, 191 insertions(+), 21 deletions(-)

diff --git a/Network-Namespace/network-namespace-wrapper.sh 
b/Network-Namespace/network-namespace-wrapper.sh
index 027a526..c2113ed 100755
--- a/Network-Namespace/network-namespace-wrapper.sh
+++ b/Network-Namespace/network-namespace-wrapper.sh
@@ -382,6 +382,31 @@ nat_chain()    { echo "${CHAIN_PREFIX}_${1}"; }
 filter_chain()   { echo "${CHAIN_PREFIX}_FWD_${1}"; }
 firewall_chain() { echo "${CHAIN_PREFIX}_FWRULES_${1}"; }
 acl_chain()      { echo "${CHAIN_PREFIX}_ACL_${1}"; }
+# Per-public-IP ingress chain in the mangle table (PREROUTING, before DNAT).
+# Must match FW_INGRESS_PREFIX in cmd_apply_fw_rules' embedded Python — that
+# code rebuilds/replaces these chains from the full firewall-rule payload,
+# so anything created here under the same name is properly superseded the
+# next time apply-fw-rules runs.
+fw_ingress_chain() { echo "CS_EXTNET_FWI_${1}"; }
+
+# Ensure a default-DROP mangle ingress chain exists for a public IP that has
+# no firewall rules of its own yet — e.g. a static-NAT IP right after
+# enable-static-nat, before any explicit Firewall rule has been created for
+# it. Without this, a newly static-NAT'd IP has no ingress chain at all and
+# falls through the default-ACCEPT policy of the top-level FORWARD chain,
+# making the VM reachable on every port despite CloudStack's contract that
+# enabling static NAT alone must NOT open any traffic. Idempotent — safe to
+# call on every add-static-nat.
+_ensure_default_deny_ingress_chain() {
+    local public_ip="$1"
+    local chain; chain=$(fw_ingress_chain "${public_ip}")
+    ip netns exec "${NAMESPACE}" iptables -t mangle -N "${chain}" 2>/dev/null 
|| return 0
+    ip netns exec "${NAMESPACE}" iptables -t mangle \
+        -A PREROUTING -d "${public_ip}/32" -j "${chain}"
+    ip netns exec "${NAMESPACE}" iptables -t mangle -A "${chain}" -j DROP
+    ip netns exec "${NAMESPACE}" iptables -t mangle \
+        -I "${chain}" 1 -m state --state RELATED,ESTABLISHED -j RETURN
+}
 
 ensure_public_ip_on_namespace() {
     local public_ip="$1" public_cidr="$2" pveth_n="$3" pveth_h="$4" addr_spec 
prefix
@@ -830,6 +855,18 @@ cmd_shutdown_network() {
 
     log "shutdown-network: network=${NETWORK_ID} ns=${NAMESPACE} 
guest_type=${GUEST_TYPE} vpc=${VPC_ID}"
 
+    # Stop per-network services (dnsmasq, radvd, haproxy, apache2, 
passwd-server)
+    # BEFORE removing any veth below. These services bind directly to the
+    # guest/public veth interfaces; yanking an interface out from under a
+    # still-running dnsmasq/haproxy/radvd causes spurious errors (e.g.
+    # dnsmasq logs "error binding DHCP socket to device <veth>" right as it
+    # receives SIGTERM) instead of a clean shutdown.
+    _svc_stop_dnsmasq
+    _svc_stop_radvd
+    _svc_stop_haproxy
+    _svc_stop_apache2
+    _svc_stop_passwd_server
+
     # iptables NAT/FILTER chains only exist for non-Shared networks (Shared
     # networks have no routing/NAT role; chains are never created for them).
     if [ "${GUEST_TYPE}" = "isolated" ]; then
@@ -896,13 +933,6 @@ cmd_shutdown_network() {
         fi
     fi
 
-    # Stop per-network services (dnsmasq, radvd, haproxy, apache2, 
passwd-server)
-    _svc_stop_dnsmasq
-    _svc_stop_radvd
-    _svc_stop_haproxy
-    _svc_stop_apache2
-    _svc_stop_passwd_server
-
     # For isolated and Shared networks delete the namespace directly.
     # VPC namespaces are shared across tiers and must only be deleted
     # when the last tier is destroyed (shutdown-vpc).
@@ -1234,6 +1264,22 @@ cmd_add_static_nat() {
 
     ensure_public_ip_on_namespace "${PUBLIC_IP}" "${PUBLIC_CIDR}" "${pveth_n}" 
"${pveth_h}"
 
+    # Block all ingress to this IP by default until an explicit Firewall
+    # rule opens it (matches cmd_apply_fw_rules' handling of static-NAT IPs
+    # with no rules yet — see _ensure_default_deny_ingress_chain). Must run
+    # before the DNAT rule below: mangle PREROUTING is evaluated before nat
+    # PREROUTING in netfilter's packet path, so this DROP takes effect
+    # regardless of ordering here, but doing it first keeps the "no traffic
+    # until firewall opens it" contract true for the whole time this IP is
+    # static-NAT'd, with no gap.
+    #
+    # VPC tiers only: skip this. VPC ingress is governed entirely by
+    # NetworkACL (the ACL_<netid> filter chain), not the Firewall service —
+    # a mangle-level DROP here would run upstream of that chain and block
+    # traffic the tier's ACL explicitly allows, with no way for the ACL to
+    # ever override it.
+    [ -z "${VPC_ID}" ] && _ensure_default_deny_ingress_chain "${PUBLIC_IP}"
+
     # DNAT: inbound public IP → private IP (PREROUTING)
     ip netns exec "${NAMESPACE}" iptables -t nat \
         -C "${nchain_pr}" -d "${PUBLIC_IP}" -j DNAT --to-destination 
"${PRIVATE_IP}" 2>/dev/null || \
@@ -1486,9 +1532,14 @@ _find_apache2_bin() {
 }
 
 _find_apache2_modules_dir() {
+    # openSUSE ships httpd modules directly under /usr/lib64/apache2(-prefork)/
+    # rather than in a "modules" subdirectory (Debian/Ubuntu: 
.../apache2/modules,
+    # RHEL family: .../httpd/modules). Prefer the MPM-matched -prefork 
directory
+    # since /usr/sbin/httpd on openSUSE is a prefork-only build.
     for d in /usr/lib/apache2/modules /usr/lib64/apache2/modules \
               /usr/libexec/apache2   /usr/lib/httpd/modules \
-              /usr/libexec/httpd  /usr/lib64/httpd/modules/; do
+              /usr/libexec/httpd  /usr/lib64/httpd/modules/ \
+              /usr/lib64/apache2-prefork /usr/lib64/apache2; do
         [ -d "${d}" ] && echo "${d}" && return
     done
     echo "/usr/lib/apache2/modules"
@@ -1568,8 +1619,80 @@ EOF
     log "dnsmasq: wrote config $(_dnsmasq_conf) (dns_enabled=${dns_enabled})"
 }
 
+# On distros that confine per-network-service daemons with AppArmor (e.g.
+# openSUSE), the packaged profile only allows the daemon to read/write its
+# own well-known system paths (/etc/dnsmasq*, /etc/haproxy/*,
+# /var/lib/libvirt/dnsmasq/, /var/lib/haproxy/stats*, ...). It has no
+# allowance for the extension's own runtime paths under
+# /var/lib/cloudstack/extnet-*/ or /var/log/cloudstack/extensions/*/, so the
+# daemon fails with "Permission denied" opening them even though the files
+# themselves are 0644/0755 root:root — the profile silently supersedes the
+# files' own DAC permissions. Grant access via each profile's supported
+# local-override include (`include if exists <local/usr.sbin.NAME>`)
+# instead of editing the packaged profile, which a package update would
+# overwrite. No-op when AppArmor isn't installed/enabled or the named
+# profile isn't present (Ubuntu/Rocky/Oracle Linux don't confine these
+# daemons this way).
+#
+# Args: $1 = profile filename under /etc/apparmor.d/ (e.g. usr.sbin.dnsmasq)
+#       $2... = one or more raw AppArmor rule lines to ensure are present
+_ensure_apparmor_access() {
+    local profile_name="$1"; shift
+    command -v aa-status >/dev/null 2>&1 || return 0
+    aa-status --enabled >/dev/null 2>&1 || return 0
+    local profile="/etc/apparmor.d/${profile_name}"
+    [ -f "${profile}" ] || return 0
+
+    local override_dir="/etc/apparmor.d/local"
+    local override="${override_dir}/${profile_name}"
+    mkdir -p "${override_dir}"
+    local rule changed=false
+    for rule in "$@"; do
+        if ! grep -qF "${rule}" "${override}" 2>/dev/null; then
+            echo "${rule}" >> "${override}"
+            log "apparmor: granted ${profile_name} access: ${rule}"
+            changed=true
+        fi
+    done
+    if [ "${changed}" = "true" ] && command -v apparmor_parser >/dev/null 
2>&1; then
+        apparmor_parser -r "${profile}" 2>/dev/null \
+            && log "apparmor: reloaded profile ${profile}" \
+            || log "apparmor: failed to reload profile ${profile} (will retry 
next call)"
+    fi
+}
+
+_ensure_apparmor_dnsmasq_access() {
+    _ensure_apparmor_access "usr.sbin.dnsmasq" \
+        "/var/lib/cloudstack/extnet-*/** rwk," \
+        "/var/log/cloudstack/extensions/*/dnsmasq-*.log w,"
+}
+
+_ensure_apparmor_haproxy_access() {
+    # Beyond read/write/lock, a graceful reload (-sf <pid>) has the new
+    # haproxy process preserve the running one's admin/stats UNIX socket by
+    # hard-linking haproxy.sock.<pid>.bak -> haproxy.sock. AppArmor mediates
+    # link(2) as its own permission bit ('l'), separate from r/w/k, so
+    # without it the reload fails with "Permission denied ... trying to
+    # preserve previous UNIX socket" even though rwk already covers the
+    # same path (confirmed against the kernel audit log: `operation="link"
+    # ... requested_mask="l" denied_mask="l"`).
+    _ensure_apparmor_access "usr.sbin.haproxy" \
+        "/var/lib/cloudstack/extnet-*/** rwkl,"
+}
+
 _svc_start_or_reload_dnsmasq() {
     _require_binary dnsmasq
+    # The namespace may already be gone by the time this runs — e.g. a
+    # remove-dhcp-entry/remove-dns-entry call for a NIC that arrives after
+    # the network itself was already torn down (shutdown-network deletes
+    # the namespace). Starting dnsmasq against a nonexistent namespace
+    # always fails; treat it as a no-op rather than a fatal error, the
+    # same way _require_binary treats a missing binary during teardown.
+    if ! ip netns list 2>/dev/null | grep -q "^${NAMESPACE}\b"; then
+        log "dnsmasq: namespace ${NAMESPACE} no longer exists — skipping 
start/reload"
+        return 0
+    fi
+    _ensure_apparmor_dnsmasq_access
     local pid_f; pid_f=$(_dnsmasq_pid)
     if [ -f "${pid_f}" ] && kill -0 "$(cat "${pid_f}")" 2>/dev/null; then
         log "dnsmasq: sending SIGHUP to reload (pid=$(cat "${pid_f}"))"
@@ -1782,24 +1905,53 @@ PYEOF
 
 _svc_reload_haproxy() {
     _require_binary haproxy
+    # The namespace may already be gone by the time this runs — e.g.
+    # removeFromLoadBalancerRule during VM expunge re-applies LB rules for
+    # a network that was already torn down. Starting haproxy against a
+    # nonexistent namespace always fails; treat it as a no-op rather than
+    # a fatal error, same as _svc_start_or_reload_dnsmasq.
+    if ! ip netns list 2>/dev/null | grep -q "^${NAMESPACE}\b"; then
+        log "haproxy: namespace ${NAMESPACE} no longer exists — skipping 
start/reload"
+        return 0
+    fi
     local conf_f; conf_f=$(_haproxy_conf)
     local pid_f;  pid_f=$(_haproxy_pid)
 
+    _ensure_apparmor_haproxy_access
+
+    # _die_unless_namespace_race <error output> — the pre-flight `ip netns
+    # list` check above is inherently racy: a concurrent shutdown-network
+    # (e.g. a background cleanup thread tearing down this same network)
+    # can delete the namespace in the window between that check and the
+    # actual `ip netns exec` below. When that specific race is what
+    # happened, the tool being exec'd reports it losing the namespace out
+    # from under it, not a config/environment problem — treat that as the
+    # same benign no-op the pre-flight check itself grants, instead of
+    # failing the whole apply-lb-rules call over a network that's already
+    # gone anyway.
+    _die_unless_namespace_race() {
+        if [[ "$1" == *"Cannot open network namespace"* ]]; then
+            log "haproxy: namespace ${NAMESPACE} disappeared mid-start (race 
with concurrent teardown) — skipping"
+            return 0
+        fi
+        log "haproxy start error: $1"
+        die "haproxy failed to start — check ${conf_f} and ${LOG_FILE} for 
details"
+    }
+
+    local start_err
     if [ -f "${pid_f}" ] && kill -0 "$(cat "${pid_f}")" 2>/dev/null; then
         log "haproxy: reloading"
-        if ! ip netns exec "${NAMESPACE}" haproxy -f "${conf_f}" -p "${pid_f}" 
\
-                -sf "$(cat "${pid_f}")" 2>/dev/null; then
-            log "WARNING: haproxy reload failed; attempting fresh start"
+        start_err=$(ip netns exec "${NAMESPACE}" haproxy -f "${conf_f}" -p 
"${pid_f}" \
+                -sf "$(cat "${pid_f}")" 2>&1) || {
+            log "WARNING: haproxy reload failed (${start_err}); attempting 
fresh start"
             rm -f "${pid_f}" 2>/dev/null || true
-            if ! ip netns exec "${NAMESPACE}" haproxy -f "${conf_f}" -p 
"${pid_f}" 2>/dev/null; then
-                die "haproxy failed to start — check ${conf_f} and ${LOG_FILE} 
for details"
-            fi
-        fi
+            start_err=$(ip netns exec "${NAMESPACE}" haproxy -f "${conf_f}" -p 
"${pid_f}" 2>&1) || \
+                _die_unless_namespace_race "${start_err}"
+        }
     else
         log "haproxy: starting in namespace ${NAMESPACE}"
-        if ! ip netns exec "${NAMESPACE}" haproxy -f "${conf_f}" -p "${pid_f}" 
2>/dev/null; then
-            die "haproxy failed to start — check ${conf_f} and ${LOG_FILE} for 
details"
-        fi
+        start_err=$(ip netns exec "${NAMESPACE}" haproxy -f "${conf_f}" -p 
"${pid_f}" 2>&1) || \
+            _die_unless_namespace_race "${start_err}"
     fi
 }
 
@@ -1876,11 +2028,19 @@ CGISCRIPT
     chmod +x "${cgi}"
 
     # ---- Detect MPM module ----
+    # Some builds (e.g. openSUSE's /usr/sbin/httpd) compile the MPM in
+    # statically rather than shipping it as a loadable .so — neither
+    # mod_mpm_event.so nor mod_mpm_prefork.so exists anywhere on such
+    # hosts. Loading a module that isn't there is fatal (httpd refuses to
+    # start), so only emit the LoadModule line when the .so actually
+    # exists; otherwise assume the running binary already has an MPM built in.
     local mpm_mod="mpm_event_module"
     local mpm_so="mod_mpm_event.so"
     if [ ! -f "${mods}/${mpm_so}" ] && [ -f "${mods}/mod_mpm_prefork.so" ]; 
then
         mpm_mod="mpm_prefork_module"; mpm_so="mod_mpm_prefork.so"
     fi
+    local mpm_line=""
+    [ -f "${mods}/${mpm_so}" ] && mpm_line="LoadModule ${mpm_mod} 
${mods}/${mpm_so}"
 
     # ---- Check for authz_core (required in apache2 >= 2.4) ----
     local authz_line=""
@@ -1906,7 +2066,7 @@ Listen ${listen_ip}:80
 #User ${apuser}
 #Group ${apuser}
 
-LoadModule ${mpm_mod} ${mods}/${mpm_so}
+${mpm_line}
 LoadModule cgi_module ${mods}/mod_cgi.so
 LoadModule alias_module ${mods}/mod_alias.so
 ${unixd_line}
@@ -1930,6 +2090,14 @@ EOF
 
 _svc_start_or_reload_apache2() {
     _require_binary apache2 httpd
+    # The namespace may already be gone by the time this runs (e.g. a
+    # restore-network/remove-dns-entry call arriving after the network was
+    # already torn down). Starting apache2 against a nonexistent namespace
+    # always fails; treat it as a no-op, same as dnsmasq/haproxy above.
+    if ! ip netns list 2>/dev/null | grep -q "^${NAMESPACE}\b"; then
+        log "apache2: namespace ${NAMESPACE} no longer exists — skipping 
start/reload"
+        return 0
+    fi
     local bin; bin=$(_find_apache2_bin)
     local pid_f; pid_f=$(_apache2_pid)
 
@@ -1939,9 +2107,11 @@ _svc_start_or_reload_apache2() {
             log "WARNING: apache2 graceful restart failed"
     else
         log "apache2: starting in namespace ${NAMESPACE}"
-        if ! ip netns exec "${NAMESPACE}" "${bin}" -f "$(_apache2_conf)" -k 
start 2>/dev/null; then
+        local start_err
+        start_err=$(ip netns exec "${NAMESPACE}" "${bin}" -f 
"$(_apache2_conf)" -k start 2>&1) || {
+            log "apache2 start error: ${start_err}"
             die "apache2/httpd failed to start — check $(_apache2_conf) and 
${LOG_FILE} for details"
-        fi
+        }
     fi
 
     # Allow metadata traffic inbound to the namespace (INPUT) from guest 
subnet only.

Reply via email to