From 946e157d85fb9497d02a711b7c32797b968a2e2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Klime=C5=A1?= Date: Tue, 10 Feb 2015 08:19:12 +0100 Subject: contrib/scripts: nm-import-vpnc - script for importing Cisco VPN configs to NM (cherry picked from commit db0cf1e7f621061c70a806e10acc3152921130c8) --- contrib/scripts/nm-import-vpnc | 432 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100755 contrib/scripts/nm-import-vpnc diff --git a/contrib/scripts/nm-import-vpnc b/contrib/scripts/nm-import-vpnc new file mode 100755 index 0000000000..18554efb1f --- /dev/null +++ b/contrib/scripts/nm-import-vpnc @@ -0,0 +1,432 @@ +#!/usr/bin/env lua +-- -*- Mode: Lua; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +-- vim: ft=lua ts=2 sts=2 sw=2 et ai +-- +-- 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 2 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, write to the Free Software Foundation, Inc., +-- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +-- +-- Copyright 2015 Red Hat, Inc. +-- +-- +-- Script for importing/converting Cisco VPN configuration files (.pcf) to NetworkManager +-- In general, the implementation follows the logic of import() from +-- https://git.gnome.org/browse/network-manager-vpnc/tree/properties/nm-vpnc.c +-- + +---------------------- +-- Helper functions -- +---------------------- +function read_all(in_file) + local f, msg = io.open(in_file, "r") + if not f then return nil, msg; end + local content = f:read("*all") + f:close() + return content +end + +function uuid() + math.randomseed(os.time()) + local template ='xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' + local uuid = string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and math.random(0, 0xf) or math.random(8, 0xb) + return string.format('%x', v) + end) + return uuid +end + +function vpn_settings_to_text(vpn_settings) + local t = {} + for k,v in pairs(vpn_settings) do + t[#t+1] = k.."="..v + end + return table.concat(t, "\n") +end + +function usage() + local basename = string.match(arg[0], '[^/\\]+$') or arg[0] + print(basename .. " - convert/import Cisco VPN (.pcf) configuration to NetworkManager") + print("Usage:") + print(" " .. basename .. " ") + print(" - converts Cisco VPN config to NetworkManager keyfile") + print("") + print(" " .. basename .. " --import ...") + print(" - imports Cisco VPN config(s) to NetworkManager") + os.exit(1) +end + + +------------------------------------------- +-- Functions for VPN options translation -- +------------------------------------------- +function set_option(t, option, value) + g_switches[value[1]] = value[2] +end +function handle_generic(t, option, value) + t[option] = value[2] +end +function handle_yes(t, option, value) + t[option] = "yes" +end +function handle_bool(t, option, value) + if tonumber(value[2]) == 1 then + t[option] = "true" + elseif tonumber(value[2]) == 0 then + t[option] = "false" + else + io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) + end +end +function handle_DHGroup(t, option, value) + local dhgroups = { [1]="dh1", [2]="dh2", [5]="dh5" } + dhgroup = dhgroups[tonumber(value[2])] + if not dhgroup then io.stderr:write(string.format("Warning: invalid value for 'DHGroup': %s\n", value[2])) end + t[option] = dhgroup +end +function handle_PeerTimeout(t, option, value) + if not value[2] then io.stderr:write("Warning: ignoring invalid option 'PeerTimeout'\n") end + if tonumber(value[2]) == 0 or (tonumber(value[2]) >=10 and tonumber(value[2] <= 86400)) then + t[option] = value[2] + else io.stderr:write(string.format("Warning: invalid value for 'PeerTimeout': %s\n", value[2])) end +end +function handle_(t, option, value) + io.stderr:write("Warning: enc_GroupPwd: encrypted group passwords are not supported by this script.\n") +end +function handle_TunnelingMode(t, option, value) + if value[2] == 1 then + io.stderr:write("Warning: TCP tunneling is not supported by vpnc. " .. + "The connection will be used with TCP tunneling disabled, " .. + "however it may not work as expected.\n") + end +end +function handle_UseLegacyIKEPort(t, option, value) + if value[2] ~= 0 then + t[option] = 500 + end +end +function handle_routes(t, option, value) + local function splitroutes(str) + local sep, fields = " ", {} + local pattern = string.format("([^%s]+)", sep) + str:gsub(pattern, + function(c) + local c1,c2 = c:match("^(%d+%.%d+%.%d+%.%d+)/(%d+)$") + if c1 then + fields[#fields+1] = { c1, c2 } + else + io.stderr:write("Warning: ignoring invalid route: '" .. c .. "'\n") + end + end) + return fields + end + t[option] = splitroutes(value[2]) +end + +-- global variables - +g_vpn_data = {} +g_vpn_pwds = {} +g_con_data = {} +g_ip4_data = {} +g_switches = {} + +vpn2nm = { + ["Description"] = { nm_opt="id", func=handle_generic, tbl=g_con_data }, + ["InterfaceName"] = { nm_opt="interface-name", func=handle_generic, tbl=g_con_data }, + ["EnableLocalLAN"] = { nm_opt="never-default", func=handle_bool, tbl=g_ip4_data }, + ["X-NM-Routes"] = { nm_opt="routes", func=handle_routes, tbl=g_ip4_data }, + ["Host"] = { nm_opt="IPSec gateway", func=handle_generic, tbl=g_vpn_data }, + ["GroupName"] = { nm_opt="IPSec ID", func=handle_generic, tbl=g_vpn_data }, + ["Username"] = { nm_opt="Xauth username", func=handle_generic, tbl=g_vpn_data }, + ["UserPassword"] = { nm_opt="Xauth password", func=handle_generic, tbl=g_vpn_pwds }, + ["SaveUserPassword"] = { nm_opt="", func=set_option, tbl={} }, + ["GroupPwd"] = { nm_opt="IPSec secret", func=handle_generic, tbl=g_vpn_pwds }, + ["DHGroup"] = { nm_opt="IKE DH Group", func=handle_DHGroup, tbl=g_vpn_data }, + ["NTDomain"] = { nm_opt="Domain", func=handle_generic, tbl=g_vpn_data }, + ["SingleDES"] = { nm_opt="Enable Single DES", func=handle_yes, tbl=g_vpn_data }, + ["EnableNat"] = { nm_opt="", func=set_option, tbl={} }, + ["X-NM-Use-NAT-T"] = { nm_opt="", func=set_option, tbl={} }, + ["X-NM-Force-NAT-T"] = { nm_opt="", func=set_option, tbl={} }, + ["X-NM-SaveGroupPassword"] = { nm_opt="", func=set_option, tbl={} }, + ["UseLegacyIKEPort"] = { nm_opt="Local Port", func=handle_UseLegacyIKEPort, tbl=g_vpn_data }, + ["PeerTimeout"] = { nm_opt="DPD idle timeout (our side)", func=handle_PeerTimeout, tbl=g_vpn_data }, + ["TunnelingMode"] = { nm_opt="", func=handle_TunnelingMode, tbl= {} }, + ["enc_UserPassword"] = { nm_opt="", func=handle_enc_pwd, tbl= {} }, + ["enc_GroupPwd"] = { nm_opt="", func=handle_enc_pwd, tbl= {} }, +} + +------------------------------------------------------ +-- Read and convert the config into the global vars -- +------------------------------------------------------ +function read_and_convert(in_file) + local function line_split(str) + -- split at '=' character + local sep, fields = "=", {} + local pattern = string.format("([^%s]+)%s(.+)", sep, sep) + fields[1], fields[2] = str:match(pattern) + return fields + end + + in_text, msg = read_all(in_file) + if not in_text then return false, msg end + + -- loop through the config and convert it + for line in in_text:gmatch("[^\r\n]+") do + repeat + -- skip comments and empty lines + if line:find("^%s*[#;]") or line:find("^%s*$") then break end + -- trim leading and trailing spaces + line = line:find("^%s*$") and "" or line:match("^%s*(.*%S)") + + local words = line_split(line) + local val = vpn2nm[words[1]] + if val then + if type(val) == "table" then val.func(val.tbl, val.nm_opt, words) + else print(string.format("debug: '%s': val=%s", line, val)) end + end + until true + end + + -- check if mandatory options exist + if not g_vpn_data["IPSec gateway"] then + local msg = in_file .. ": Not a valid Cisco VPN configuration (no Host)" + return false, msg + end + if not g_vpn_data["IPSec ID"] then + local msg = in_file .. ": Not a valid OpenVPN configuration (no GroupName)" + return false, msg + end + + -- process inter-option dependencies + -- NAT traversal mode + local natt_mode = { + NONE = "none", + NATT = "natt", + NATT_ALWAYS = "force-natt", + CISCO = "cisco-udp" + } + g_vpn_data["NAT Traversal Mode"] = natt_mode.CISCO + if tonumber(g_switches["EnableNat"]) == 0 then + g_vpn_data["NAT Traversal Mode"] = natt_mode.NONE + elseif tonumber(g_switches["EnableNat"]) == 1 then + if tonumber(g_switches["X-NM-Force-NAT-T"]) == 1 then + g_vpn_data["NAT Traversal Mode"] = natt_mode.NATT_ALWAYS + elseif tonumber(g_switches["X-NM-Use-NAT-T"]) == 1 then + g_vpn_data["NAT Traversal Mode"] = natt_mode.NATT + end + else + io.stderr:write("Warning: invalid value for EnableNat\n") + g_vpn_data["NAT Traversal Mode"] = natt_mode.CISCO + end + + -- set secret flags + g_vpn_data["Xauth password-flags"] = 1 + if tonumber(g_switches["SaveUserPassword"]) == 1 then + g_vpn_data["xauth-password-type"] = "save" + else + g_vpn_data["Xauth password-flags"] = 3 + end + if g_vpn_data["IPSec ID"] then + g_vpn_data["IPSec ID-flags"] = 1 + end + if g_switches["X-NM-SaveGroupPassword"] then + if tonumber(g_switches["X-NM-SaveGroupPassword"]) == 1 then + g_vpn_data["ipsec-secret-type"] = "save" + g_vpn_data["IPSec ID-flags"] = 1 + else + g_vpn_data["IPSec ID-flags"] = 3 + end + else + g_vpn_data["ipsec-secret-type"] = "save" + end + + return true +end + + +-------------------------------------------------------- +-- Create and write connection file in keyfile format -- +-------------------------------------------------------- +function write_vpn_to_keyfile(in_file, out_file) + connection = [[ +[connection] +id=__NAME_PLACEHOLDER__ +uuid=__UUID_PLACEHOLDER__ +__IFNAME_PLACEHOLDER__ +type=vpn +autoconnect=no + +[ipv4] +method=auto +never-default=__NEVER_DEFAULT_PLACEHOLDER__ +__ROUTES_PLACEHOLDER__ + +[ipv6] +method=auto + +[vpn] +service-type=org.freedesktop.NetworkManager.vpnc +]] + connection = connection .. vpn_settings_to_text(g_vpn_data) + connection = connection .. "\n\n[vpn-secrets]\n" + connection = connection .. vpn_settings_to_text(g_vpn_pwds) + + local con_name = g_con_data["id"] or (out_file:gsub(".*/", "")) + local ifname = g_con_data["interface-name"] + local never_default = g_ip4_data["never-default"] or "false" + local routes = "" + if ifname then ifname = "interface-name="..ifname.."\n" else ifname = "" end + for idx, r in ipairs(g_ip4_data["routes"] or {}) do + routes = routes .. string.format("routes%d=%s/%s\n", idx, r[1], r[2]) + end + + connection = string.gsub(connection, "__NAME_PLACEHOLDER__", con_name) + connection = string.gsub(connection, "__UUID_PLACEHOLDER__", uuid()) + connection = string.gsub(connection, "__IFNAME_PLACEHOLDER__\n", ifname) + connection = string.gsub(connection, "__NEVER_DEFAULT_PLACEHOLDER__", never_default) + connection = string.gsub(connection, "__ROUTES_PLACEHOLDER__\n", routes) + + -- write output file + local f, err = io.open(out_file, "w") + if not f then io.stderr:write(err) return false end + f:write(connection) + f:close() + + local ofname = out_file:gsub(".*/", "") + io.stderr:write("Successfully converted VPN configuration: " .. in_file .. " => " .. out_file .. "\n") + io.stderr:write("To use the connection, do:\n") + io.stderr:write("# cp " .. out_file .. " /etc/NetworkManager/system-connections\n") + io.stderr:write("# chmod 600 /etc/NetworkManager/system-connections/" .. ofname .. "\n") + io.stderr:write("# nmcli con load /etc/NetworkManager/system-connections/" .. ofname .. "\n") + return true +end + +--------------------------------------------- +-- Import VPN connection to NetworkManager -- +--------------------------------------------- +function import_vpn_to_NM(filename) + local lgi = require 'lgi' + local GLib = lgi.GLib + local NM = lgi.NM + + -- function creating NMConnection + local function create_profile(name) + local profile = NM.SimpleConnection.new() + local never_default = g_ip4_data["never-default"] == "true" + + s_con = NM.SettingConnection.new() + s_vpn = NM.SettingVpn.new() + s_ip4 = NM.SettingIP4Config.new() + + s_con[NM.SETTING_CONNECTION_ID] = name + s_con[NM.SETTING_CONNECTION_UUID] = uuid() + s_con[NM.SETTING_CONNECTION_INTERFACE_NAME] = g_con_data["interface-name"] + s_con[NM.SETTING_CONNECTION_TYPE] = "vpn" + s_vpn[NM.SETTING_VPN_SERVICE_TYPE] = "org.freedesktop.NetworkManager.vpnc" + s_ip4[NM.SETTING_IP_CONFIG_METHOD] = NM.SETTING_IP4_CONFIG_METHOD_AUTO + s_ip4[NM.SETTING_IP_CONFIG_NEVER_DEFAULT] = never_default + + -- add routes + local AF_INET = 2 + for _, r in ipairs(g_ip4_data["routes"] or {}) do + route = NM.IPRoute.new(AF_INET, r[1], r[2], nil, -1) + s_ip4:add_route(route) + end + + -- add vpn data + for k,v in pairs(g_vpn_data) do + s_vpn:add_data_item(k, v) + end + -- add vpn secrets + for k,v in pairs(g_vpn_pwds) do + s_vpn:add_secret(k, v) + end + + profile:add_setting(s_con) + profile:add_setting(s_vpn) + profile:add_setting(s_ip4) + return profile + end + + -- callback function for add_connection() + local function added_cb(client, result, data) + local con,err,code = client:add_connection_finish(result) + if con then + print(string.format("%s: Imported to NetworkManager: %s - %s", + filename, con:get_uuid(), con:get_id())) + else + io.stderr:write(code .. ": " .. err .. "\n"); + return false + end + main_loop:quit() + end + + local profile_name = g_con_data["id"] or string.match(filename, '[^/\\]+$') or filename + main_loop = GLib.MainLoop(nil, false) + local con = create_profile(profile_name) + local client = NM.Client.new() + + -- send the connection to NetworkManager + client:add_connection_async(con, true, nil, added_cb, nil) + + -- run main loop so that the callback could be called + main_loop:run() + return true +end + + +--------------------------- +-- Main code starts here -- +--------------------------- +local import_mode = false +local infile, outfile + +-- parse command-line arguments +if not arg[1] or arg[1] == "--help" or arg[1] == "-h" then usage() end +if arg[1] == "--import" or arg[1] == "-i" then + infile = arg[2] + if not infile then usage() end + import_mode = true +else + infile = arg[1] + outfile = arg[2] + if not infile or not outfile then usage() end + if arg[3] then usage() end +end + +if import_mode then + -- check if lgi is available + local success,msg = pcall(require, 'lgi') + if not success then + io.stderr:write("Lua lgi module is not available, please install it (usually lua-lgi package)\n") + -- print(msg) + os.exit(1) + end + -- read configs, convert them and import to NM + for i = 2, #arg do + ok, err_msg = read_and_convert(arg[i]) + if ok then import_vpn_to_NM(arg[i]) + else io.stderr:write(err_msg .. "\n") end + -- reset global vars + g_vpn_data = {} + g_vpn_pwds = {} + g_con_data = {} + g_ip4_data = {} + g_switches = {} + end +else + -- read configs, convert them and write as NM keyfile connection + ok, err_msg = read_and_convert(infile) + if ok then write_vpn_to_keyfile(infile, outfile) + else io.stderr:write(err_msg .. "\n") end +end + -- cgit v1.2.1