package network::vpn::openvpn; use base qw(network::vpn); use strict; use common; sub get_type { 'openvpn' } sub get_description { "OpenVPN" } sub get_packages { 'openvpn', if_(supports_pkcs11(), qw(openct opensc pcsc-lite)) } my $pkcs11_tokens; sub prepare { require services; if (supports_pkcs11()) { #- don't fail if SmartCard daemons can't start, it's not mandatory services::start_not_running_service($_) foreach qw(openct pcscd); cache_pkcs11_tokens(); } 1; } sub read_config { my ($connection) = @_; my ($pkcs11_slot, $pkcs11_id); foreach (cat_($connection->get_config_path)) { /^\s*proto\s+tcp/ and $connection->{tcp} = 1; /^\s*dev\s+([[:alpha:]]+)(\d*)/ and ($connection->{dev_type}, $connection->{dev_number}) = ($1, $2); if (/^\s*remote\s+(\S+)(?:\s+(\w+))?/) { $connection->{gateway} = $1; $connection->{port} = $2; } if (/^\s*tls-auth\s+(\S+)(?:\s+(\d+))?/) { $connection->{'tls-auth'} = $1; $connection->{key_direction} = $2; } foreach my $type (qw(ca cert key pkcs12 secret)) { /^\s*$type\s+(.+)/ and $connection->{$type . '_file'} = $1; } /^\s*pkcs11-slot\s+(.+)/ and $pkcs11_slot = $1; /^\s*pkcs11-id\s+(.+)/ and $pkcs11_id = $1; /^\s*ns-cert-type\s+server/ and $connection->{check_server} = 1; /^\s*auth-user-pass/ and $connection->{'auth-user-pass'} = 1; if (/^\s*ifconfig\s+(\S+)\s+(\S+)/) { $connection->{local_ip} = $1; $connection->{remote_ip} = $2; $connection->{addressing} = 'manual'; } /^\s*cipher\s+(.+)/ and $connection->{cipher} = $1; /^\s*keysize\s+(.+)/ and $connection->{keysize} = $1; } if (exists $connection->{secret_file}) { $connection->{type} = 'static'; $connection->{key} = delete $connection->{secret_file}; } else { $connection->{type} = 'pki'; $connection->{key} = delete $connection->{key_file}; } if (defined $pkcs11_slot && defined $pkcs11_id) { my $tokens = $connection->get_pkcs11_tokens('no_cache'); $connection->{pkcs11_object} = find { $_->{slot} eq $pkcs11_slot && $_->{id} eq $pkcs11_id } @{$tokens->{objects}}; $connection->{pkcs11_object} ||= {}; } } sub write_config { my ($connection) = @_; my $file = $connection->get_config_path; unless (-e $file) { mkdir_p(dirname($file)); cp_f("/usr/share/openvpn/sample-config-files/client.conf", $file); } delete $connection->{keysize} if !$connection->{cipher}; if ($connection->{type} eq 'static') { $connection->{secret_file} = delete $connection->{key}; delete $connection->{ca_file}; delete $connection->{cert_file}; delete $connection->{key_file}; } else { $connection->{key_file} = delete $connection->{key}; delete $connection->{secret_file}; } my @config = $connection->build_config; substInFile { foreach my $conf (@config) { if (/^;?$conf->{key}\b/) { if ($conf->{comment}) { $_ = ";$_" unless /^;/; } else { $_ = "$conf->{key} $conf->{value}\n"; $conf->{comment} = 1; } last; } } $_ .= join('', map { if_(!$_->{comment}, "$_->{key} $_->{value}\n") } @config) if eof; } $file; } sub get_key_settings { my ($connection) = @_; my %ciphers = get_ciphers(); my @types = ( pki => N("X509 Public Key Infrastructure"), static => N("Static Key"), ); my $pkcs11_separator = '/'; my $tokens = $connection->get_pkcs11_tokens; my %types = @types; [ { label => N("Type"), val => \$connection->{type}, list => first(list2kv(@types)), format => sub { $types{$_[0]} }, }, (supports_pkcs11() ? { label => "PKCS #11 token", type => 'list', list => [ undef, @{$tokens->{objects}} ], format => sub { $_[0] ? join($pkcs11_separator, $tokens->{slots}{$_[0]{slot}}{label} || $_[0]{slot}, $_[0]{label} || $_[0]{id}) : N("None") }, val => \$connection->{pkcs11_object}, separator => $pkcs11_separator, allow_empty_list => 1, disabled => sub { $connection->{type} ne 'pki' || $connection->{cert_file} || $connection->{key_file} || $connection->{pkcs12_file} }, } : ()), { label => "PKCS #12", type => 'file', val => \$connection->{pkcs12_file}, disabled => sub { $connection->{type} ne 'pki' || $connection->{cert_file} || $connection->{key_file} || $connection->{pkcs11_object} }, }, { label => #-PO: please don't translate the CA acronym N("Certificate Authority (CA)"), type => 'file', val => \$connection->{ca_file}, disabled => sub { $connection->{type} ne 'pki' }, }, { label => N("Certificate"), type => 'file', val => \$connection->{cert_file}, disabled => sub { $connection->{type} ne 'pki' || $connection->{pkcs11_object} || $connection->{pkcs12_file} }, }, { label => N("Key"), type => 'file', val => \$connection->{key}, disabled => sub { $connection->{type} eq 'pki' && ($connection->{pkcs11_object} || $connection->{pkcs12_file}) }, }, { label => N("TLS control channel key"), type => 'file', val => \$connection->{'tls-auth'}, disabled => sub { $connection->{type} ne 'pki' }, advanced => 1, }, { label => N("Key direction"), type => 'list', val => \$connection->{key_direction}, list => [ undef, 0, 1 ], format => sub { defined($_[0]) ? $_[0] : N("None") }, advanced => 1, }, { text => N("Authenticate using username and password"), type => 'bool', val => \$connection->{'auth-user-pass'}, advanced => 1, }, { text => N("Check server certificate"), type => 'bool', val => \$connection->{check_server}, advanced => 1, }, { label => N("Cipher algorithm"), type => 'list', val => \$connection->{cipher}, list => [ '', keys %ciphers ], format => sub { exists $ciphers{$_[0]} ? $ciphers{$_[0]} : N("Default") }, advanced => 1, }, { label => N("Size of cipher key"), val => \$connection->{keysize}, advanced => 1, disabled => sub { !$connection->{cipher} || $connection->{cipher} eq 'none' }, }, ]; } sub get_settings { my ($connection) = @_; my @addressing = ( automatic => N("Get from server"), manual => N("Manual configuration"), ); my %addressing = @addressing; [ { label => N("Gateway"), val => \$connection->{gateway}, }, { label => N("Gateway port"), val => \$connection->{port}, advanced => 1, }, { label => N("IP address"), val => \$connection->{addressing}, list => first(list2kv(@addressing)), format => sub { $addressing{$_[0]} }, }, { label => N("Local IP address"), val => \$connection->{local_ip}, disabled => sub { $connection->{addressing} ne 'manual' }, }, { label => N("Remote IP address"), val => \$connection->{remote_ip}, disabled => sub { $connection->{addressing} ne 'manual' }, }, { text => N("Use TCP protocol"), type => 'bool', val => \$connection->{tcp}, advanced => 1, }, { label => N("Virtual network device type"), type => 'list', list => [ 'tun', 'tap' ], val => \$connection->{dev_type}, advanced => 1, }, { label => N("Virtual network device number (optional)"), val => \$connection->{dev_number}, advanced => 1, }, ]; } my $lib = arch() =~ /x86_64/ ? "lib64" : "lib"; my $openvpn_default_pkcs11_provider = find { -e $_ } ( "/usr/local/$lib/libetpkcs11.so", "/usr/$lib/opensc-pkcs11.so", ); my $supports_pkcs11; sub supports_pkcs11 { if (!defined $supports_pkcs11) { require run_program; $supports_pkcs11 = to_bool(run_program::rooted($::prefix, '/usr/sbin/openvpn', '--show-pkcs11-slots', '')); } $supports_pkcs11; } sub get_pkcs11_tokens { my ($_class, $o_no_cache) = @_; cache_pkcs11_tokens() if !defined $pkcs11_tokens && !$o_no_cache; $pkcs11_tokens; } sub cache_pkcs11_tokens { $pkcs11_tokens = { slots => {}, objects => [] }; my $slot_id; foreach (run_program::rooted_get_stdout($::prefix, '/usr/bin/pkcs11-tool', '--module', $openvpn_default_pkcs11_provider, '-L')) { if (/^Slot\s+(\d+)\s+(.+)$/) { $slot_id = $2 ne '(empty)' ? $1 : undef; $pkcs11_tokens->{slots}{$slot_id}{name} = $2 if defined $slot_id; } elsif (/^\s+token\s+label:\s+(.+)$/) { $pkcs11_tokens->{slots}{$slot_id}{label} = $1 if defined $slot_id; } elsif (/^\s+flags:\s*(.*)/) { my @flags = split(/\s*,\s*/, $1); if (defined $slot_id && !member("token present", @flags)) { delete $pkcs11_tokens->{slots}{$slot_id}; undef $slot_id; } } } foreach $slot_id (keys %{$pkcs11_tokens->{slots}}) { my ($type, $label); my @stdout; #- do rooted_get_stdout manually because pkcs11-tool may exit with non-zero code with proprietary modules run_program::rooted($::prefix, '/usr/bin/pkcs11-tool', '>', \@stdout, '--module', $openvpn_default_pkcs11_provider, '-O', '--slot', $slot_id); foreach (@stdout) { if (/^(\S.*?)\s+Object/) { $type = $1; undef $label; } elsif (/^\s+label:\s+(.+)$/) { $label = $1; } elsif (/^\s+ID:\s+(.+)$/ && $type eq 'Public Key') { push @{$pkcs11_tokens->{objects}}, { id => $1, label => $label, slot => $slot_id }; } } } } sub build_config { my ($connection) = @_; ( { key => 'client', comment => to_bool($connection->{secret_file}) }, { key => 'proto', $connection->{tcp} ? (value => 'tcp') : (comment => 1) }, { key => 'dev', value => $connection->{dev_type} . $connection->{dev_number} }, { key => 'remote', value => join(' ', $connection->{gateway}, if_($connection->{port}, $connection->{port})) }, (map { +{ key => $_, $connection->{$_ . '_file'} ? (value => $connection->{$_ . '_file'}) : (comment => 1) } } qw(ca cert key pkcs12 secret)), { key => 'pkcs11-providers', $connection->{pkcs11_object} ? (value => $openvpn_default_pkcs11_provider) : (comment => 1) }, { key => 'pkcs11-slot-type', $connection->{pkcs11_object} ? (value => 'id') : (comment => 1) }, { key => 'pkcs11-slot', $connection->{pkcs11_object} ? (value => $connection->{pkcs11_object}{slot}) : (comment => 1) }, { key => 'pkcs11-id-type', $connection->{pkcs11_object} ? (value => 'id') : (comment => 1) }, { key => 'pkcs11-id', $connection->{pkcs11_object} ? (value => $connection->{pkcs11_object}{id}) : (comment => 1) }, { key => 'tls-auth', $connection->{'tls-auth'} ? (value => join(' ', $connection->{'tls-auth'}, if_($connection->{key_direction}, $connection->{key_direction}))) : (comment => 1) }, { key => 'ns-cert-type', $connection->{check_server} ? (value => 'server') : (comment => 1) }, { key => 'auth-user-pass', comment => !$connection->{'auth-user-pass'} }, { key => 'ifconfig', $connection->{addressing} eq 'manual' ? (value => join(' ', $connection->{local_ip}, $connection->{remote_ip})) : (comment => 1) }, { key => 'cipher', $connection->{cipher} ? (value => $connection->{cipher}) : (comment => 1) }, { key => 'keysize', $connection->{keysize} ? (value => $connection->{keysize}) : (comment => 1) }, ); } sub get_ciphers() { my @ciphers = chomp_(`/usr/sbin/openvpn --show-ciphers`); #- drop header shift @ciphers while $ciphers[0] =~ /^\S/; none => N("None"), map { if_($_, first(split(' ', $_)), $_) } @ciphers; } sub start { my ($connection, $o_in) = @_; $connection->read_config if keys %$connection <= 1; my %interactive_passwords = if_($o_in, ( if_($connection->{'auth-user-pass'}, 'Auth' => 1), if_($connection->{pkcs11_object}, 'Token' => 1), )); my $port = 2222; my $started = $connection->_run('start', if_(%interactive_passwords, '--management', "127.0.0.1", $port, '--management-query-passwords', )); $started && (!%interactive_passwords || $connection->ask_passwords($o_in, $port, \%interactive_passwords)); } sub ask_passwords { my ($_connection, $in, $port, $interactive_passwords) = @_; require Net::Telnet; my $t = new Net::Telnet; $t->open(host => "localhost", port => $port, errmode => "return"); my $wait; while (1) { $wait ||= $in->wait_message(N("VPN connection"), N("Starting connection..")); my ($_pre, $match) = $t->waitfor(string => ">PASSWORD:", string => ">NEED-OK:", errmode => "return", #- don't quit if all interactive passwords have been entered #- in case some passwords are incorrect #- though, use a smaller timeout timeout => (%$interactive_passwords ? 20 : 5)); if (!defined $match) { my $msg = $t->errmsg; #- no more interactive password is required, success $t->close, return 1 if $msg =~ /timed-out/ && !%$interactive_passwords; $t->close, return; #- potential failure } elsif ($match eq ">NEED-OK:") { my ($type, $msg) = $t->getline =~ /'(.*)'.* MSG:/; undef $wait; my $ret = $in->ask_okcancel(N("VPN connection"), $type eq 'token-insertion-request' ? N("Please insert your token") : $msg); $t->print(qq(needok "$type" ) . ($ret ? "ok" : "cancel")); $t->close, return if !$ret; } elsif ($match eq ">PASSWORD:") { my ($full_type) = $t->getline =~ /'(.*)'/; #- assume type is "Auth" if not token my $type = $full_type =~ /\btoken$/ ? 'Token' : 'Auth'; my ($username, $password); undef $wait; my $ret = $in->ask_from(N("VPN connection"), '', $type eq 'Token' ? [ { label => N("PIN number"), val => \$password, hidden => 1 } ] : [ { label => N("Account Login (user name)"), val => \$username }, { label => N("Account Password"), val => \$password, hidden => 1 }, ]); if ($ret) { delete $interactive_passwords->{$type}; $t->print(qq(username "$full_type" "$username")) if $username; $t->print(qq(password "$full_type" "$password")); } else { $t->close, return; } } } } 1;