#!/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