diff --git a/README.md b/README.md index e533fa6..77a09df 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ That said, OpenVPN still makes sense when you need: - CLI interface for automation and scripting (non-interactive mode with JSON output) - Certificate renewal for both client and server certificates - List and monitor connected clients +- Immediate client disconnect on certificate revocation (via management interface) - Uses [official OpenVPN repositories](https://community.openvpn.net/openvpn/wiki/OpenvpnSoftwareRepos) when possible for the latest stable releases - Firewall rules and forwarding managed seamlessly (native firewalld and nftables support, iptables fallback) - Configurable VPN subnets (IPv4: default `10.8.0.0/24`, IPv6: default `fd42:42:42:42::/112`) @@ -135,7 +136,7 @@ For automation and scripting, use the CLI interface: # List clients ./openvpn-install.sh client list -# Revoke a client +# Revoke a client (immediately disconnects if connected) ./openvpn-install.sh client revoke alice ``` diff --git a/openvpn-install.sh b/openvpn-install.sh index c0d943b..a5aaeb7 100755 --- a/openvpn-install.sh +++ b/openvpn-install.sh @@ -2574,20 +2574,21 @@ function installOpenVPN() { installOpenVPNRepo log_info "Installing OpenVPN and dependencies..." + # socat is used for communicating with the OpenVPN management interface (client disconnect on revoke) if [[ $OS =~ (debian|ubuntu) ]]; then - run_cmd_fatal "Installing OpenVPN" apt-get install -y openvpn iptables openssl curl ca-certificates tar dnsutils + run_cmd_fatal "Installing OpenVPN" apt-get install -y openvpn iptables openssl curl ca-certificates tar dnsutils socat elif [[ $OS == 'centos' ]]; then - run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils 'policycoreutils-python*' + run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat 'policycoreutils-python*' elif [[ $OS == 'oracle' ]]; then - run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils policycoreutils-python-utils + run_cmd_fatal "Installing OpenVPN" yum install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat policycoreutils-python-utils elif [[ $OS == 'amzn2023' ]]; then - run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils + run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat elif [[ $OS == 'fedora' ]]; then - run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils policycoreutils-python-utils + run_cmd_fatal "Installing OpenVPN" dnf install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat policycoreutils-python-utils elif [[ $OS == 'opensuse' ]]; then - run_cmd_fatal "Installing OpenVPN" zypper install -y openvpn iptables openssl ca-certificates curl tar bind-utils + run_cmd_fatal "Installing OpenVPN" zypper install -y openvpn iptables openssl ca-certificates curl tar bind-utils socat elif [[ $OS == 'arch' ]]; then - run_cmd_fatal "Installing OpenVPN" pacman --needed --noconfirm -Syu openvpn iptables openssl ca-certificates curl tar bind + run_cmd_fatal "Installing OpenVPN" pacman --needed --noconfirm -Syu openvpn iptables openssl ca-certificates curl tar bind socat fi # Verify ChaCha20-Poly1305 compatibility if selected @@ -2946,8 +2947,12 @@ tls-cipher $CC_CIPHER tls-ciphersuites $TLS13_CIPHERSUITES client-config-dir ccd status /var/log/openvpn/status.log +management /var/run/openvpn/server.sock unix verb 3" >>/etc/openvpn/server/server.conf + # Create management socket directory + run_cmd_fatal "Creating management socket directory" mkdir -p /var/run/openvpn + # Create client-config-dir dir run_cmd_fatal "Creating client config directory" mkdir -p /etc/openvpn/server/ccd # Create log dir @@ -3727,9 +3732,30 @@ function revokeClient() { run_cmd "Removing IP assignment" sed -i "/^$CLIENT,.*/d" /etc/openvpn/server/ipp.txt run_cmd "Backing up index" cp /etc/openvpn/server/easy-rsa/pki/index.txt{,.bk} + # Disconnect the client if currently connected + disconnectClient "$CLIENT" + log_success "Certificate for client $CLIENT revoked." } +# Disconnect a client via the management interface +function disconnectClient() { + local client_name="$1" + local mgmt_socket="/var/run/openvpn/server.sock" + + if [[ ! -S "$mgmt_socket" ]]; then + log_warning "Management socket not found. Client may still be connected until they reconnect." + return 0 + fi + + log_info "Disconnecting client $client_name..." + if echo "kill $client_name" | socat - UNIX-CONNECT:"$mgmt_socket" >/dev/null 2>&1; then + log_success "Client $client_name disconnected." + else + log_warning "Could not disconnect client (they may not be connected)." + fi +} + function renewClient() { local client_cert_duration_days diff --git a/test/Dockerfile.server b/test/Dockerfile.server index 5c0814b..3fa829d 100644 --- a/test/Dockerfile.server +++ b/test/Dockerfile.server @@ -15,6 +15,7 @@ ENV ENABLE_NFTABLES=${ENABLE_NFTABLES} # Install basic dependencies based on the OS # dnsutils/bind-utils provides dig for DNS testing with Unbound +# Note: socat is installed by openvpn-install.sh during OpenVPN installation RUN if command -v apt-get >/dev/null; then \ apt-get update && apt-get install -y --no-install-recommends \ iproute2 iptables curl procps systemd systemd-sysv dnsutils jq \ diff --git a/test/client-entrypoint.sh b/test/client-entrypoint.sh index e8d8da7..a7406ee 100755 --- a/test/client-entrypoint.sh +++ b/test/client-entrypoint.sh @@ -192,36 +192,35 @@ echo "PASS: Can ping VPN gateway with revoke test client" # Signal server that we're connected with revoke test client touch /shared/revoke-client-connected -# Wait for server to signal us to disconnect -echo "Waiting for server to signal disconnect..." -while [ ! -f /shared/revoke-client-disconnect ]; do - sleep 2 -done - -# Disconnect -echo "Disconnecting revoke test client..." -pkill openvpn || true - -# Wait for openvpn to fully exit and tun0 to be released -WAITED=0 -MAX_WAIT_DISCONNECT=10 -while (pgrep openvpn >/dev/null || ip addr show tun0 2>/dev/null | grep -q "inet ") && [ $WAITED -lt $MAX_WAIT_DISCONNECT ]; do +# Wait for server to revoke and auto-disconnect us via management interface +# We detect disconnect by checking if ping to VPN gateway fails +echo "Waiting for server to revoke certificate and disconnect us..." +DISCONNECT_DETECTED=false +for i in $(seq 1 60); do + if ! ping -c 1 -W 2 "$VPN_GATEWAY" >/dev/null 2>&1; then + echo "Disconnect detected: cannot ping VPN gateway" + DISCONNECT_DETECTED=true + break + fi sleep 1 - WAITED=$((WAITED + 1)) + echo "Still connected, waiting for revoke/disconnect ($i/60)..." done -# Verify disconnected -if ip addr show tun0 2>/dev/null | grep -q "inet "; then - echo "FAIL: tun0 still has IP after disconnect" +if [ "$DISCONNECT_DETECTED" = true ]; then + echo "PASS: Client was auto-disconnected by revoke" + # Kill openvpn process to clean up + pkill openvpn 2>/dev/null || true + sleep 1 +else + echo "FAIL: Client was not disconnected within 60 seconds" exit 1 fi -echo "PASS: Disconnected successfully" -# Signal server that we're disconnected +# Signal server that we detected the disconnect touch /shared/revoke-client-disconnected -# Wait for server to revoke the certificate and signal us to reconnect -echo "Waiting for server to revoke certificate and signal reconnect..." +# Wait for server to signal us to try reconnecting +echo "Waiting for server to signal reconnect attempt..." while [ ! -f /shared/revoke-try-reconnect ]; do sleep 2 done diff --git a/test/server-entrypoint.sh b/test/server-entrypoint.sh index 7f73af4..524dffd 100755 --- a/test/server-entrypoint.sh +++ b/test/server-entrypoint.sh @@ -135,6 +135,39 @@ fi echo "All required files present" +# ===================================================== +# Verify management interface configuration +# ===================================================== +echo "" +echo "=== Verifying Management Interface Configuration ===" + +# Verify management socket is configured in server.conf +if grep -q "management /var/run/openvpn/server.sock unix" /etc/openvpn/server/server.conf; then + echo "PASS: Management interface configured in server.conf" +else + echo "FAIL: Management interface not found in server.conf" + grep "management" /etc/openvpn/server/server.conf || echo "No management directive found" + exit 1 +fi + +# Verify management socket directory exists +if [ -d /var/run/openvpn ]; then + echo "PASS: Management socket directory exists" +else + echo "FAIL: Management socket directory /var/run/openvpn not found" + exit 1 +fi + +# Verify socat is available (needed for management interface communication) +if command -v socat >/dev/null 2>&1; then + echo "PASS: socat is available" +else + echo "FAIL: socat is not installed (required for management interface)" + exit 1 +fi + +echo "=== Management Interface Configuration Verified ===" + # ===================================================== # Test duplicate client name handling # ===================================================== @@ -769,18 +802,8 @@ fi echo "=== Server Status Tests PASSED ===" -# Signal client to disconnect before revocation -touch /shared/revoke-client-disconnect - -# Wait for client to disconnect -echo "Waiting for client to disconnect..." -while [ ! -f /shared/revoke-client-disconnected ]; do - sleep 2 -done -echo "Client disconnected" - -# Now revoke the certificate -echo "Revoking certificate for '$REVOKE_CLIENT'..." +# Now revoke the certificate (this should auto-disconnect the client via management interface) +echo "Revoking certificate for '$REVOKE_CLIENT' (should auto-disconnect client)..." REVOKE_OUTPUT="/tmp/revoke-output.log" (bash /opt/openvpn-install.sh client revoke "$REVOKE_CLIENT" --force) 2>&1 | tee "$REVOKE_OUTPUT" || true @@ -801,6 +824,22 @@ else exit 1 fi +# Wait for client to confirm it was disconnected by the revoke +echo "Waiting for client to confirm auto-disconnect..." +DISCONNECT_WAIT=0 +while [ ! -f /shared/revoke-client-disconnected ] && [ $DISCONNECT_WAIT -lt 60 ]; do + sleep 2 + DISCONNECT_WAIT=$((DISCONNECT_WAIT + 2)) + echo "Waiting for disconnect confirmation... ($DISCONNECT_WAIT/60s)" +done + +if [ -f /shared/revoke-client-disconnected ]; then + echo "PASS: Client was auto-disconnected by revoke command" +else + echo "FAIL: Client was not disconnected within 60 seconds" + exit 1 +fi + # Signal client to try reconnecting (should fail) touch /shared/revoke-try-reconnect @@ -1025,8 +1064,41 @@ done echo "PASS: Client connected with passphrase-protected certificate" echo "=== PASSPHRASE Support Tests PASSED ===" + +# ===================================================== +# Test management interface is running +# ===================================================== echo "" -echo "=== All Revocation Tests PASSED ===" +echo "=== Testing Management Interface ===" + +MGMT_SOCKET="/var/run/openvpn/server.sock" + +# Verify management socket exists and is accessible +if [ -S "$MGMT_SOCKET" ]; then + echo "PASS: Management socket exists at $MGMT_SOCKET" +else + echo "FAIL: Management socket not found at $MGMT_SOCKET" + ls -la /var/run/openvpn/ || true + exit 1 +fi + +# Test that we can communicate with the management interface +echo "Testing management interface communication..." +MGMT_STATUS=$(echo "status" | socat - UNIX-CONNECT:"$MGMT_SOCKET" 2>&1 | head -20) +if echo "$MGMT_STATUS" | grep -q "CLIENT LIST"; then + echo "PASS: Management interface is responsive" + echo "Status output:" + echo "$MGMT_STATUS" +else + echo "FAIL: Management interface not responding correctly" + echo "Response: $MGMT_STATUS" + exit 1 +fi + +echo "=== Management Interface Tests PASSED ===" + +echo "" +echo "=== All Tests PASSED ===" # Server tests complete - systemd keeps the container running via /sbin/init # OpenVPN service (openvpn-server@server) continues independently