[1/3] ovpn-functions.pl: Add a helper to get the status of N2N connections

Message ID 20261027222110.2642078-2-antonio@mandelbit.com
State New
Headers
Series OpenVPN: Fix N2N connections shown as DISCONNECTED while up |

Commit Message

Antonio Quartulli 27 Oct 2026, 10:21 p.m. UTC
OpenVPN switches the management state to AUTH and WAIT when a new
session is started while a tunnel is up, and keeps it there if that
session never completes (e.g. a stray packet from the internet), although
the tunnel keeps working.

Therefore determine the status from the state history: an N2N connection
is up if it has reached CONNECTED and has not restarted since.

Signed-off-by: Antonio Quartulli <antonio@mandelbit.com>
---
 config/cfgroot/ovpn-functions.pl   | 96 ++++++++++++++++++++++++++++++
 config/rootfiles/common/configroot |  1 +
 lfs/configroot                     |  1 +
 3 files changed, 98 insertions(+)
 create mode 100644 config/cfgroot/ovpn-functions.pl
  

Patch

diff --git a/config/cfgroot/ovpn-functions.pl b/config/cfgroot/ovpn-functions.pl
new file mode 100644
index 000000000..3c6c02660
--- /dev/null
+++ b/config/cfgroot/ovpn-functions.pl
@@ -0,0 +1,96 @@ 
+#!/usr/bin/perl
+###############################################################################
+#                                                                             #
+# IPFire.org - A linux based firewall                                         #
+# Copyright (C) 2026  Antonio Quartulli <antonio@mandelbit.com>               #
+#                                                                             #
+# This program is free software: you can redistribute it and/or modify        #
+# it under the terms of the GNU General Public License as published by        #
+# the Free Software Foundation, either version 3 of the License, or           #
+# (at your option) any later version.                                         #
+#                                                                             #
+# This program is distributed in the hope that it will be useful,             #
+# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               #
+# GNU General Public License for more details.                                #
+#                                                                             #
+# You should have received a copy of the GNU General Public License           #
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.       #
+#                                                                             #
+###############################################################################
+
+package OpenVPN;
+
+use strict;
+use Net::Telnet;
+
+# Returns "CONNECTED" or "DISCONNECTED" for the N2N connection whose
+# management interface listens on the given port.
+#
+# The current state alone cannot be trusted: when a new session is started
+# while the tunnel is up (a peer that gives up, or just a stray packet from
+# the internet), OpenVPN switches the state to AUTH and WAIT and keeps it
+# there if that session never completes, although the tunnel keeps working.
+#
+# Therefore we look at the state history: the tunnel is up if the process
+# has reached CONNECTED and has not restarted since. A tunnel that goes down
+# is detected by ping-restart, which logs RECONNECTING.
+sub n2n_status($) {
+	my $port = shift;
+
+	# Create a new Telnet session
+	my $telnet = new Net::Telnet(
+		Port    => $port,
+		Timeout => 3,
+		Errmode => "return",
+	);
+
+	# Connect
+	$telnet->open("127.0.0.1") or return "DISCONNECTED";
+
+	# Fetch the state history
+	my @output = $telnet->cmd(
+		String => "state all",
+		Prompt => "/(?:^|\n)(?:END|ERROR:.*)\n/"
+	);
+
+	$telnet->close();
+
+	# Collect all states, skipping the banner and any notifications
+	my @states;
+
+	foreach my $line (@output) {
+		if ($line =~ m/^\d+,([A-Z_]+),/) {
+			push(@states, $1);
+		}
+	}
+
+	return "DISCONNECTED" unless (@states);
+
+	# The history always starts with CONNECTING, unless it has been truncated.
+	# A truncated history comes from a process that has not been restarted
+	# for a long time, which does not happen to an unconnected tunnel
+	# because of ping-restart. OpenVPN keeps the last 100 states only, so
+	# ~50 stray packets within one ping-restart interval after a restart
+	# could still make a down tunnel look connected.
+	my $connected = ($states[0] ne "CONNECTING");
+
+	foreach my $state (@states) {
+		if ($state eq "CONNECTED") {
+			$connected = 1;
+
+		# The process starts (CONNECTING, logged once), restarts
+		# (RECONNECTING) or exits (EXITING)
+		} elsif ($state =~ m/^(CONNECTING|RECONNECTING|EXITING)$/) {
+			$connected = 0;
+		}
+
+		# Any other state (WAIT, AUTH, ...) is either part of the initial
+		# connection or an attempt to start a new session that does not
+		# affect the existing one
+	}
+
+	return $connected ? "CONNECTED" : "DISCONNECTED";
+}
+
+1;
diff --git a/config/rootfiles/common/configroot b/config/rootfiles/common/configroot
index 72e05e865..ffe1ebe72 100644
--- a/config/rootfiles/common/configroot
+++ b/config/rootfiles/common/configroot
@@ -138,6 +138,7 @@  var/ipfire/network-functions.pl
 var/ipfire/optionsfw
 #var/ipfire/optionsfw/settings
 var/ipfire/ovpn
+var/ipfire/ovpn-functions.pl
 #var/ipfire/ovpn/ccd.conf
 #var/ipfire/ovpn/ccdroute
 #var/ipfire/ovpn/ccdroute2
diff --git a/lfs/configroot b/lfs/configroot
index 3df566f50..0b14f6501 100644
--- a/lfs/configroot
+++ b/lfs/configroot
@@ -82,6 +82,7 @@  $(TARGET) :
 	cp $(DIR_SRC)/config/cfgroot/location-functions.pl	$(CONFIG_ROOT)/
 	cp $(DIR_SRC)/config/cfgroot/ipblocklist-functions.pl	$(CONFIG_ROOT)/
 	cp $(DIR_SRC)/config/cfgroot/ids-functions.pl		$(CONFIG_ROOT)/
+	cp $(DIR_SRC)/config/cfgroot/ovpn-functions.pl		$(CONFIG_ROOT)/
 	cp $(DIR_SRC)/config/cfgroot/wireguard-functions.pl	$(CONFIG_ROOT)/
 	cp $(DIR_SRC)/config/cfgroot/lang.pl			$(CONFIG_ROOT)/
 	cp $(DIR_SRC)/config/cfgroot/countries.pl		$(CONFIG_ROOT)/