new file mode 100644
@@ -0,0 +1,6 @@
+1793050400,CONNECTING,,,,,,
+1793050417,CONNECTED,SUCCESS,10.8.0.1,127.0.0.1,21195,,
+1793050448,AUTH_PENDING,,,,,,
+1793054017,RECONNECTING,ping-restart,,,,,
+1793054022,WAIT,,,,,,
+END
new file mode 100644
@@ -0,0 +1,7 @@
+1793050400,CONNECTING,,,,,,
+1793050401,WAIT,,,,,,
+1793050402,AUTH,,,,,,
+1793050403,GET_CONFIG,,,,,,
+1793050404,ASSIGN_IP,,10.8.0.1,,,,
+1793050417,CONNECTED,SUCCESS,10.8.0.1,127.0.0.1,21195,,
+END
new file mode 100644
@@ -0,0 +1,8 @@
+1793050400,CONNECTING,,,,,,
+1793050401,WAIT,,,,,,
+1793050402,AUTH,,,,,,
+1793050404,ASSIGN_IP,,10.8.0.1,,,,
+1793050417,CONNECTED,SUCCESS,10.8.0.1,127.0.0.1,21195,,
+1793054017,AUTH,,,,,,
+1793054018,WAIT,,,,,,
+END
new file mode 100644
@@ -0,0 +1 @@
+END
new file mode 100644
@@ -0,0 +1,7 @@
+1793050400,CONNECTING,,,,,,
+1793050401,WAIT,,,,,,
+1793050402,AUTH,,,,,,
+1793050404,ASSIGN_IP,,10.8.0.1,,,,
+1793050417,CONNECTED,SUCCESS,10.8.0.1,127.0.0.1,21195,,
+1793054017,EXITING,SIGTERM,,,,,
+END
new file mode 100644
@@ -0,0 +1,6 @@
+1793050400,CONNECTING,,,,,,
+1793050401,WAIT,,,,,,
+1793050402,AUTH,,,,,,
+1793050462,WAIT,,,,,,
+1793050463,AUTH,,,,,,
+END
new file mode 100644
@@ -0,0 +1,11 @@
+>HOLD:Waiting for hold release:0
+1793050400,CONNECTING,,,,,,
+>STATE:1793050401,WAIT,,,,,,
+1793050401,WAIT,,,,,,
+1793050402,AUTH,,,,,,
+>LOG:1793050403,I,Peer Connection Initiated
+1793050417,CONNECTED,SUCCESS,10.8.0.1,127.0.0.1,21195,,
+>STATE:1793054017,AUTH,,,,,,
+1793054017,AUTH,,,,,,
+1793054018,WAIT,,,,,,
+END
new file mode 100644
@@ -0,0 +1,11 @@
+1793050400,CONNECTING,,,,,,
+1793050401,WAIT,,,,,,
+1793050402,AUTH,,,,,,
+1793050404,ASSIGN_IP,,10.8.0.1,,,,
+1793050417,CONNECTED,SUCCESS,10.8.0.1,127.0.0.1,21195,,
+1793054017,RECONNECTING,ping-restart,,,,,
+1793054022,WAIT,,,,,,
+1793054023,AUTH,,,,,,
+1793054024,GET_CONFIG,,,,,,
+1793054025,CONNECTED,SUCCESS,10.8.0.1,127.0.0.1,21195,,
+END
new file mode 100644
@@ -0,0 +1,9 @@
+1793050400,CONNECTING,,,,,,
+1793050401,WAIT,,,,,,
+1793050402,AUTH,,,,,,
+1793050404,ASSIGN_IP,,10.8.0.1,,,,
+1793050417,CONNECTED,SUCCESS,10.8.0.1,127.0.0.1,21195,,
+1793054017,RECONNECTING,ping-restart,,,,,
+1793054022,WAIT,,,,,,
+1793054023,AUTH,,,,,,
+END
new file mode 100644
@@ -0,0 +1,3 @@
+1793050417,AUTH,,,,,,
+1793050418,WAIT,,,,,,
+END
new file mode 100644
@@ -0,0 +1,73 @@
+#!/usr/bin/perl
+#
+# Runs OpenVPN::n2n_status() against a fake OpenVPN management interface
+# and prints the result.
+#
+# Usage: n2n_status.pl <ovpn-functions.pl> <data file|--no-server|--close>
+#
+# The fake management interface sends the banner, waits for "state all"
+# and replies with the content of the data file.
+
+use strict;
+use warnings;
+use IO::Socket::INET;
+
+my ($functions, $mode) = @ARGV;
+
+require $functions;
+
+my $server = IO::Socket::INET->new(
+ LocalAddr => "127.0.0.1",
+ LocalPort => 0,
+ Proto => "tcp",
+ Listen => 1,
+ ReuseAddr => 1,
+) or die "Could not create socket: $!\n";
+
+my $port = $server->sockport();
+
+# Nothing listening on the port
+if ($mode eq "--no-server") {
+ $server->close();
+ print OpenVPN::n2n_status($port) . "\n";
+ exit 0;
+}
+
+my $pid = fork() // die "Could not fork: $!\n";
+
+if ($pid == 0) {
+ my $client = $server->accept() or exit 1;
+
+ # Close the connection right away
+ if ($mode eq "--close") {
+ $client->close();
+ exit 0;
+ }
+
+ open(my $fh, "<", $mode) or die "Could not open $mode: $!\n";
+ my @reply = <$fh>;
+ close($fh);
+
+ $client->autoflush(1);
+ print $client ">INFO:OpenVPN Management Interface Version 5 -- type 'help' for more info\r\n";
+
+ while (my $line = <$client>) {
+ last if ($line =~ m/^state all\r?\n/);
+ }
+
+ foreach my $line (@reply) {
+ chomp($line);
+ print $client "$line\r\n";
+ }
+
+ # Wait for the client to disconnect
+ while (<$client>) {}
+
+ exit 0;
+}
+
+$server->close();
+
+print OpenVPN::n2n_status($port) . "\n";
+
+waitpid($pid, 0);
new file mode 100755
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+SCRIPT_PATH="$(dirname "$(readlink -f "$0")")"
+
+ROOT="$(readlink -f "${SCRIPT_PATH}/../../../..")"
+
+. ${ROOT}/tests/lib.sh
+
+n2n_status() {
+ perl "${SCRIPT_PATH}/n2n_status.pl" "${ROOT}/config/cfgroot/ovpn-functions.pl" "$@"
+}
+
+# Run n2n_status() against a fake management interface replying with the given
+# state history and check the result
+test_n2n_status() {
+ local data="${1}"
+ local expected="${2}"
+ local status
+
+ status="$(n2n_status "${data}")"
+
+ if [[ "${status}" == "${expected}" ]]; then
+ log_test_succeded "n2n_status() returns '${status}' for '${data##*/}'"
+ return 0
+ else
+ log_test_failed "n2n_status() returns '${status}' for '${data##*/}' and not '${expected}'"
+ return 1
+ fi
+}
+
+if ! perl -MNet::Telnet -e 1 &>/dev/null; then
+ log_test_failed "The perl module Net::Telnet is not installed"
+ exit 1
+fi
+
+# The tunnel came up
+test_n2n_status "${SCRIPT_PATH}/data/connected" "CONNECTED"
+
+# A new session that never completes does not affect the tunnel (#13897)
+test_n2n_status "${SCRIPT_PATH}/data/connected_stray_session" "CONNECTED"
+
+# Notifications are ignored
+test_n2n_status "${SCRIPT_PATH}/data/notifications" "CONNECTED"
+
+# The tunnel went down and came back up
+test_n2n_status "${SCRIPT_PATH}/data/reconnected" "CONNECTED"
+
+# The process has been running for a long time
+test_n2n_status "${SCRIPT_PATH}/data/truncated" "CONNECTED"
+
+# The tunnel went down
+test_n2n_status "${SCRIPT_PATH}/data/reconnecting" "DISCONNECTED"
+
+# The tunnel never came up
+test_n2n_status "${SCRIPT_PATH}/data/never_connected" "DISCONNECTED"
+
+# A state containing "END" does not end the history early
+test_n2n_status "${SCRIPT_PATH}/data/auth_pending" "DISCONNECTED"
+
+# The process is exiting
+test_n2n_status "${SCRIPT_PATH}/data/exiting" "DISCONNECTED"
+
+# No states at all
+test_n2n_status "${SCRIPT_PATH}/data/empty" "DISCONNECTED"
+
+# The management interface closes the connection
+test_n2n_status --close "DISCONNECTED"
+
+# The management interface is not running
+test_n2n_status --no-server "DISCONNECTED"