| Line 1... |
Line 1... |
| 1 |
#! /usr/bin/env perl
|
1 |
#! /usr/bin/env perl
|
| - |
|
2 |
|
| 2 |
# Copyright (c) 2025, Daily Data, Inc.
|
3 |
# Copyright (c) 2025, Daily Data, Inc.
|
| 3 |
# All rights reserved.
|
4 |
# All rights reserved.
|
| 4 |
#
|
5 |
#
|
| 5 |
# Redistribution and use in source and binary forms, with or without modification,
|
6 |
# Redistribution and use in source and binary forms, with or without modification,
|
| 6 |
# are permitted provided that the following conditions are met:
|
7 |
# are permitted provided that the following conditions are met:
|
| Line 22... |
Line 23... |
| 22 |
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
23 |
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
| 23 |
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
24 |
# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
| 24 |
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
25 |
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
| 25 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
26 |
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
| 26 |
# DAMAGE.
|
27 |
# DAMAGE.
|
| - |
|
28 |
#
|
| - |
|
29 |
# This script defines the routers that can be managed by loadOpnSense.pl
|
| - |
|
30 |
# It allows adding, editing, and removing router configurations stored in a JSON file.
|
| - |
|
31 |
#
|
| - |
|
32 |
# Change History:
|
| - |
|
33 |
# v1.0.0 2025-10-01 RWR
|
| - |
|
34 |
# Initial version
|
| 27 |
|
35 |
|
| 28 |
use strict;
|
36 |
use strict;
|
| 29 |
use warnings;
|
37 |
use warnings;
|
| 30 |
|
38 |
|
| - |
|
39 |
# use libraries from the directory this script is in
|
| - |
|
40 |
BEGIN {
|
| - |
|
41 |
use FindBin;
|
| - |
|
42 |
use File::Spec;
|
| - |
|
43 |
# use libraries from the directory this script is in
|
| - |
|
44 |
use Cwd 'abs_path';
|
| - |
|
45 |
use File::Basename;
|
| - |
|
46 |
use lib dirname( abs_path( __FILE__ ) );
|
| - |
|
47 |
}
|
| - |
|
48 |
|
| 31 |
use JSON qw( decode_json encode_json );
|
49 |
use JSON qw( decode_json encode_json );
|
| 32 |
use Data::Dumper;
|
50 |
use Data::Dumper;
|
| 33 |
use lib '.';
|
- |
|
| 34 |
use opnsense;
|
51 |
use opnsense;
|
| 35 |
|
52 |
|
| - |
|
53 |
our $VERSION = "1.0.0";
|
| - |
|
54 |
|
| 36 |
# default config file name if not specified on command line
|
55 |
# Default config file name if not specified on command line. Defaults to 'routers.json' in the script directory
|
| 37 |
my $configFileName = 'config.json';
|
56 |
my $configFileName = $FindBin::RealBin . '/routers.json';
|
| - |
|
57 |
# fields used in the router configuration
|
| 38 |
my @fields = ( 'url', 'apiKey', 'apiSecret', 'ovpnIndex', 'template', 'hostname', 'localport' );
|
58 |
my @fields = ( 'url', 'apiKey', 'apiSecret', 'ovpnIndex', 'template', 'hostname', 'localPort' );
|
| - |
|
59 |
|
| - |
|
60 |
# Check if running as root
|
| - |
|
61 |
if ($> != 0) {
|
| - |
|
62 |
die "Error: This script must be run as root.\n";
|
| - |
|
63 |
}
|
| 39 |
|
64 |
|
| - |
|
65 |
#-----------------------------
|
| - |
|
66 |
# usage: Print usage message
|
| - |
|
67 |
#-----------------------------
|
| 40 |
sub usage {
|
68 |
sub usage {
|
| 41 |
print "Usage: $0 <opnsense_config_file>\n";
|
69 |
print "Usage: $0 <opnsenseConfigFile>\n";
|
| 42 |
exit 1;
|
70 |
exit 1;
|
| 43 |
}
|
71 |
}
|
| 44 |
|
72 |
|
| 45 |
# readConfig
|
73 |
#-----------------------------
|
| 46 |
# Reads in the configuration file and returns a hash ref of the data
|
74 |
# readConfig: Reads in the configuration file and returns a hash ref of the data
|
| 47 |
# if file does not exist, returns empty hash ref
|
75 |
# If file does not exist, returns empty hash ref
|
| 48 |
# file - name of file to read (default config.json if not specified)
|
76 |
# file - name of file to read (default $configFileName)
|
| - |
|
77 |
#-----------------------------
|
| 49 |
sub readConfig {
|
78 |
sub readConfig {
|
| 50 |
my ( $file ) = @_;
|
79 |
my ($file) = @_;
|
| 51 |
my $data = {};
|
80 |
my $data = {};
|
| 52 |
if ( -e $file ) {
|
81 |
if (-e $file) {
|
| 53 |
open( my $fh, '<', $file ) or die "Cannot open $file: $!";
|
82 |
open(my $fh, '<', $file) or die "Cannot open $file: $!";
|
| 54 |
local $/; # slurp mode
|
83 |
local $/; # slurp mode
|
| 55 |
my $json_text = <$fh>;
|
84 |
my $jsonText = <$fh>;
|
| 56 |
close $fh;
|
85 |
close $fh;
|
| 57 |
$data = decode_json($json_text);
|
86 |
$data = decode_json($jsonText);
|
| 58 |
}
|
87 |
}
|
| 59 |
return $data;
|
88 |
return $data;
|
| 60 |
}
|
89 |
}
|
| 61 |
|
90 |
|
| - |
|
91 |
#-----------------------------
|
| - |
|
92 |
# writeConfig: Writes the given data to the specified file in JSON format
|
| - |
|
93 |
# file - name of file to write (default $configFileName)
|
| - |
|
94 |
# data - reference to the data to write
|
| - |
|
95 |
# permissions on the file are set to 0600 and ownership to root
|
| - |
|
96 |
# since it contains sensitive information
|
| - |
|
97 |
#-----------------------------
|
| 62 |
sub writeConfig {
|
98 |
sub writeConfig {
|
| 63 |
my ( $file, $data ) = @_;
|
99 |
my ( $file, $data ) = @_;
|
| 64 |
open( my $fh, '>', $file ) or die "Cannot open $file: $!";
|
100 |
open( my $fh, '>', $file ) or die "Cannot open $file: $!";
|
| 65 |
print $fh encode_json($data);
|
101 |
print $fh encode_json($data);
|
| 66 |
close $fh;
|
102 |
close $fh;
|
| - |
|
103 |
chmod 0600, $file or warn "Could not set permissions on $file: $!";
|
| - |
|
104 |
chown 0, 0, $file or warn "Could not set ownership to root on $file: $!";
|
| 67 |
}
|
105 |
}
|
| 68 |
|
106 |
|
| - |
|
107 |
#-----------------------------
|
| - |
|
108 |
# trim: Remove leading and trailing whitespace from a string
|
| - |
|
109 |
# s - the string to trim
|
| - |
|
110 |
#-----------------------------
|
| 69 |
sub trim {
|
111 |
sub trim {
|
| 70 |
my ($s) = @_;
|
112 |
my ($s) = @_;
|
| 71 |
$s =~ s/^\s+|\s+$//g if defined $s;
|
113 |
$s =~ s/^\s+|\s+$//g if defined $s;
|
| 72 |
return $s;
|
114 |
return $s;
|
| 73 |
}
|
115 |
}
|
| 74 |
|
116 |
|
| - |
|
117 |
#-----------------------------
|
| - |
|
118 |
# menuSelect: Display a menu of options and prompt the user to select one
|
| - |
|
119 |
# prompt - the prompt to display to the user
|
| - |
|
120 |
# options - reference to an array of option strings
|
| - |
|
121 |
# additionalText - reference to an array of additional text strings to display next to each option
|
| - |
|
122 |
# returns the selected option string (1-based index)
|
| - |
|
123 |
#-----------------------------
|
| 75 |
sub menuSelect {
|
124 |
sub menuSelect {
|
| 76 |
my ( $prompt, $options, $additionalText ) = @_;
|
125 |
my ( $prompt, $options, $additionalText ) = @_;
|
| 77 |
print "$prompt\n";
|
126 |
print "$prompt\n";
|
| 78 |
for my $i ( 0 .. $#$options ) {
|
127 |
for my $i ( 0 .. $#$options ) {
|
| 79 |
print " " . ($i+1) . ". " . $options->[$i] . ($additionalText->[$i] ? $additionalText->[$i] : "") . "\n";
|
128 |
print " " . ($i+1) . ". " . $options->[$i] . ($additionalText->[$i] ? $additionalText->[$i] : "") . "\n";
|
| 80 |
}
|
129 |
}
|
| 81 |
print "Select option (1-" . ($#$options + 1) . "): ";
|
130 |
print "Select option (1-" . ($#$options + 1) . "): ";
|
| 82 |
my $sel = <STDIN>;
|
131 |
my $sel = <STDIN>;
|
| 83 |
chomp $sel;
|
132 |
chomp $sel;
|
| 84 |
if ( $sel !~ /^\d+$/ || $sel < 1 || $sel > ($#$options + 1) ) {
|
133 |
if ( $sel !~ /^\d+$/ || $sel < 1 || $sel > ($#$options + 1) ) {
|
| 85 |
die "Invalid selection\n";
|
134 |
warn "Invalid selection\n";
|
| - |
|
135 |
return menuSelect( $prompt, $options, $additionalText );
|
| 86 |
}
|
136 |
}
|
| 87 |
return $options->[$sel - 1];
|
137 |
return $options->[$sel - 1];
|
| 88 |
}
|
138 |
}
|
| 89 |
|
139 |
|
| - |
|
140 |
#-----------------------------
|
| - |
|
141 |
# selectOvpnIndex: Prompts the user to select an ovpnIndex from the list
|
| - |
|
142 |
# in the OPNsense router specified in the config hash
|
| - |
|
143 |
# config - reference to the router's configuration hash
|
| - |
|
144 |
# returns the selected ovpnIndex string or undef if none selected
|
| - |
|
145 |
#-----------------------------
|
| - |
|
146 |
sub selectOvpnIndex {
|
| - |
|
147 |
my ( $config ) = @_;
|
| - |
|
148 |
if ( defined $config->{'url'} && defined $config->{'apiKey'} && defined $config->{'apiSecret'} ) {
|
| - |
|
149 |
my $opnsense = new opnsense(
|
| - |
|
150 |
url => $config->{'url'},
|
| - |
|
151 |
apiKey => $config->{'apiKey'},
|
| - |
|
152 |
apiSecret => $config->{'apiSecret'},
|
| - |
|
153 |
);
|
| - |
|
154 |
my $providers = $opnsense->getVpnProviders();
|
| - |
|
155 |
if ( ref($providers) eq 'HASH' && keys %$providers ) {
|
| - |
|
156 |
my @providerIndex = sort keys %$providers;
|
| - |
|
157 |
my @additionalText = map {
|
| - |
|
158 |
defined $config->{'ovpnIndex'} &&
|
| - |
|
159 |
($_ eq $config->{'ovpnIndex'} ? " (current) " : " - ") . $providers->{$_}
|
| - |
|
160 |
} @providerIndex;
|
| - |
|
161 |
my $selectedProvider = &menuSelect( "Select VPN Provider", \@providerIndex, \@additionalText );
|
| - |
|
162 |
return $selectedProvider;
|
| - |
|
163 |
} else {
|
| - |
|
164 |
warn "No VPN providers found or error retrieving providers. Please check your connection details.\n";
|
| - |
|
165 |
}
|
| - |
|
166 |
} else {
|
| - |
|
167 |
warn "Please set url, apiKey, and apiSecret before selecting ovpnIndex.\n";
|
| - |
|
168 |
}
|
| - |
|
169 |
return undef;
|
| - |
|
170 |
}
|
| - |
|
171 |
|
| - |
|
172 |
#-----------------------------
|
| - |
|
173 |
# editRouter: Edit the configuration for a router
|
| - |
|
174 |
# config - reference to the router's configuration hash
|
| - |
|
175 |
# routerName - name of the router being edited
|
| - |
|
176 |
# keys - reference to an array of keys (fields) that can be edited
|
| - |
|
177 |
# works on one router at a time, allows setting fields for that router
|
| - |
|
178 |
# returns true if configuration was saved, false if not
|
| - |
|
179 |
#-----------------------------
|
| 90 |
sub editRouter {
|
180 |
sub editRouter {
|
| 91 |
my ( $config, $keys ) = @_;
|
181 |
my ( $config, $routerName, $keys ) = @_;
|
| 92 |
my $option = '';
|
182 |
my $option = '';
|
| - |
|
183 |
my $saved = 1;
|
| - |
|
184 |
my $currentRouter = $config->{$routerName};
|
| 93 |
$config->{'template'} = 'PlainOpenVPN' unless defined $config->{'template'};
|
185 |
$currentRouter->{'template'} = 'PlainOpenVPN' unless defined $currentRouter->{'template'};
|
| 94 |
my $vpnProviders = ();
|
186 |
my $vpnProviders = ();
|
| 95 |
while ( $option ne 'Done' ) {
|
187 |
while ( $option ne 'Done' ) {
|
| 96 |
print "\nEditing router configuration:\n";
|
188 |
print "\nEditing router configuration for $routerName" . ($saved ? "" : " (Changed)") . ":\n";
|
| 97 |
$option = &menuSelect(
|
189 |
$option = &menuSelect(
|
| 98 |
"Select field to edit",
|
190 |
"Select field to edit",
|
| 99 |
[ @$keys, 'Done' ],
|
191 |
[ @$keys, 'Save Config', 'Done' ],
|
| 100 |
[ map { defined $config->{$_} ? " (current: $config->{$_})" : " (not set)" } @$keys, '' ]
|
192 |
[ map { defined $currentRouter->{$_} ? " (current: $currentRouter->{$_})" : " (not set)" } @$keys, '' ]
|
| 101 |
);
|
193 |
);
|
| 102 |
if ( $option ne 'Done' ) {
|
- |
|
| 103 |
if ( $option eq 'ovpnIndex' ) {
|
194 |
if ( $option eq 'ovpnIndex' ) {
|
| 104 |
# special handling for ovpnIndex to show available providers
|
195 |
$currentRouter->{'ovpnIndex'} = &selectOvpnIndex( $currentRouter );
|
| 105 |
if ( defined $config->{'url'} && defined $config->{'apiKey'} && defined $config->{'apiSecret'} ) {
|
- |
|
| 106 |
my $opnsense = new opnsense(
|
196 |
$saved = 0; # mark as unsaved
|
| 107 |
url => $config->{'url'},
|
- |
|
| 108 |
apiKey => $config->{'apiKey'},
|
197 |
} elsif ( $option eq 'Save Config' ) {
|
| 109 |
apiSecret => $config->{'apiSecret'},
|
198 |
writeConfig( $configFileName, $config );
|
| 110 |
);
|
- |
|
| 111 |
my $providers = $opnsense->get_vpn_providers();
|
- |
|
| 112 |
if ( ref($providers) eq 'HASH' && keys %$providers ) {
|
- |
|
| 113 |
#die Dumper( $providers );
|
- |
|
| 114 |
my @providerIndex = sort keys %$providers;
|
- |
|
| 115 |
my @additionalText = map {
|
- |
|
| 116 |
defined $config->{'ovpnIndex'} &&
|
- |
|
| 117 |
($_ eq $config->{'ovpnIndex'} ? " (current) " : "") . $providers->{$_}
|
- |
|
| 118 |
} @providerIndex;
|
199 |
$saved = 1; # mark as saved
|
| 119 |
my $selectedProvider = &menuSelect( "Select VPN Provider", \@providerIndex, \@additionalText );
|
- |
|
| 120 |
$config->{'ovpnIndex'} = $selectedProvider;
|
200 |
print "Configuration saved to $configFileName\n";
|
| 121 |
next; # skip the rest of the loop
|
- |
|
| 122 |
} else {
|
201 |
} elsif ($option eq 'Done') {
|
| 123 |
print "No VPN providers found or error retrieving providers. Please check your connection details.\n";
|
202 |
$option = &menuSelect( "Exiting configuration editor, but changes were not saved.", [ 'Exit', 'Save and Exit' ] );
|
| 124 |
}
|
- |
|
| 125 |
} else {
|
- |
|
| 126 |
print "Please set url, apiKey, and apiSecret before selecting ovpnIndex.\n";
|
- |
|
| 127 |
next; # skip the rest of the loop
|
203 |
if ( $option eq 'Save and Exit' ) {
|
| 128 |
}
|
- |
|
| 129 |
} else {
|
- |
|
| 130 |
print "Enter value for $option: ";
|
204 |
writeConfig( $configFileName, $config );
|
| 131 |
my $value = <STDIN>;
|
- |
|
| 132 |
chomp $value;
|
- |
|
| 133 |
$value = trim($value);
|
205 |
$saved = 1; # mark as saved
|
| 134 |
$config->{$option} = $value;
|
206 |
print "Configuration saved to $configFileName\n";
|
| 135 |
}
|
207 |
}
|
| - |
|
208 |
last;
|
| - |
|
209 |
} else {
|
| - |
|
210 |
$saved = 0; # mark as unsaved
|
| - |
|
211 |
print "Enter value for $option: ";
|
| - |
|
212 |
my $value = <STDIN>;
|
| - |
|
213 |
chomp $value;
|
| - |
|
214 |
$value = trim($value);
|
| - |
|
215 |
$currentRouter->{$option} = $value if $value ne '';
|
| 136 |
}
|
216 |
}
|
| 137 |
}
|
217 |
}
|
| 138 |
return $config;
|
218 |
return $saved;
|
| 139 |
}
|
219 |
}
|
| 140 |
|
220 |
|
| 141 |
$configFileName = shift if defined shift;
|
221 |
$configFileName = shift if @ARGV;
|
| 142 |
|
- |
|
| - |
|
222 |
# read the configuration file, if it exists, and place hash in $config
|
| 143 |
my $config = &readConfig( $configFileName );
|
223 |
my $config = &readConfig( $configFileName );
|
| 144 |
|
- |
|
| - |
|
224 |
# display menu of existing routers plus option to add new router
|
| 145 |
my $routerName = &menuSelect( "Select router to configure", [ keys %$config, 'Add new router' ] );
|
225 |
my $routerName = &menuSelect( "Select router to configure", [ keys %$config, 'Add new router' ] );
|
| - |
|
226 |
# adding a new router, so get the name, then create an empty hashref for it
|
| 146 |
if ( $routerName eq 'Add new router' ) {
|
227 |
if ( $routerName eq 'Add new router' ) {
|
| 147 |
print "Enter new router name: ";
|
228 |
print "Enter new router name: ";
|
| 148 |
$routerName = <STDIN>;
|
229 |
$routerName = <STDIN>;
|
| 149 |
chomp $routerName;
|
230 |
chomp $routerName;
|
| 150 |
$routerName = trim($routerName);
|
231 |
$routerName = trim($routerName);
|
| 151 |
$config->{$routerName} = {};
|
232 |
$config->{$routerName} = {};
|
| 152 |
}
|
233 |
}
|
| - |
|
234 |
# edit the router configuration
|
| - |
|
235 |
&editRouter( $config, $routerName, \@fields );
|
| 153 |
|
236 |
|
| 154 |
$config->{$routerName} = &editRouter( $config->{$routerName}, \@fields );
|
- |
|
| 155 |
|
- |
|
| 156 |
#print Dumper( $config );
|
- |
|
| 157 |
|
237 |
|
| 158 |
writeConfig( $configFileName, $config );
|
- |
|
| 159 |
|
238 |
1;
|
| - |
|
239 |
|
| 160 |
Generated by GNU Enscript 1.6.5.90.
|
240 |
Generated by GNU Enscript 1.6.5.90.
|