#!/usr/bin/env lua -- SPDX-License-Identifier: GPL-2.0-or-later -- -- Copyright (C) 2015 Red Hat, Inc. -- -- Script for importing/converting OpenVPN configuration files for NetworkManager -- In general, the implementation follows the logic of import() from -- https://git.gnome.org/browse/network-manager-openvpn/tree/properties/import-export.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 unquote(str) return (string.gsub(str, "^([\"\'])(.*)%1$", "%2")) end function parse_ipv4_to_bytes(ip_addr) local b1,b2,b3,b4 = ip_addr:match("^(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)%.(%d%d?%d?)$") b1 = tonumber(b1) b2 = tonumber(b2) b3 = tonumber(b3) b4 = tonumber(b4) return b1, b2, b3, b4 end function is_ipv4(ip_addr) local b1,b2,b3,b4 = parse_ipv4_to_bytes(ip_addr) if not b1 or (b1 > 255) then return false end if not b2 or (b2 > 255) then return false end if not b3 or (b3 > 255) then return false end if not b4 or (b4 > 255) then return false end return true end function ip_mask_to_prefix(mask) local b, prefix local b1,b2,b3,b4 = parse_ipv4_to_bytes(mask) if b4 ~= 0 then prefix = 24 b = b4 elseif b3 ~= 0 then prefix = 16 b = b3 elseif b2 ~= 0 then prefix = 8 b = b2 else prefix = 0 b = b1 end while b ~= 0 do prefix = prefix + 1 b = bit32.band(0x000000FF, bit32.lshift(b, 1)) end return prefix 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 OpenVPN configuration to NetworkManager") print("Usage:") print(" " .. basename .. " ") print(" - converts OpenVPN config to NetworkManager keyfile") print("") print(" " .. basename .. " --import ...") print(" - imports OpenVPN config(s) to NetworkManager") os.exit(1) end ------------------------------------------- -- Functions for VPN options translation -- ------------------------------------------- function set_bool(t, option, value) g_switches[option] = true end function handle_yes(t, option, value) t[option] = "yes" end function handle_generic(t, option, value) if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end t[option] = value[2] end function handle_generic_unquote(t, option, value) if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end t[option] = unquote(value[2]) end function handle_number(t, option, value) if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end if not tonumber(value[2]) then io.stderr:write(string.format("Warning: ignoring not numeric value '%s' for option '%s'\n", value[2], value[1])) return end t[option] = value[2] end function handle_proto(t, option, value) if not value[2] then io.stderr:write("Warning: ignoring invalid option 'proto'\n") end if value[2] == "tcp" or value[3] == "tcp-client" or value[2] == "tcp-server" then t[option] = "yes" end end function handle_comp_lzo(t, option, value) value[2] = value[2] or "adaptive" if value[2] == "no" then value[2] = "no-by-default" elseif value[2] ~= "yes" and value[2] ~= "adaptive" then io.stderr:write(string.format("Warning: ignoring invalid argument '%s' in option 'comp-lzo'\n", value[2])) return end t[option] = value[2] end function handle_dev_type(t, option, value) if value[2] ~= "tun" and value[2] ~= "tap" then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) end t[option] = value[2] end function handle_remote(t, option, value) local rem if not value[2] then io.stderr:write("Warning: ignoring invalid option 'remote'\n") return end rem = value[2] if tonumber(value[3]) then rem = rem .. ":" .. value[3] end if value[4] == "udp" or value[4] == "tcp" then rem = rem .. ":" .. value[4] end if t[option] then t[option] = t[option] .. " " .. rem else t[option] = rem end g_switches[value[1]] = true end function handle_port(t, option, value) if tonumber(value[2]) then t[option] = value[2] end end function handle_proxy(t, option, value) if not value[2] then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end if value[4] then io.stderr:write(string.format("Warning: the third argument of '%s' is not supported yet\n", value[1])) end t[option[1]] = string.gsub(value[1], "-proxy", "") t[option[2]] = value[2] t[option[3]] = value[3] end function handle_ifconfig(t, option, value) if not (value[2] and value[3]) then io.stderr:write("Warning: ignoring invalid option 'ifconfig'\n") return end t[option[1]] = value[2] t[option[2]] = value[3] end function handle_keepalive(t, option, value) if (not (value[2] and value[3])) or (not tonumber(value[2]) or not tonumber(value[3])) then io.stderr:write("Warning: ignoring invalid option 'keepalive'; two numbers required\n") return end t[option[1]] = value[2] t[option[2]] = value[3] end function handle_path(t, option, value) if value[1] == "pkcs12" then t["ca"] = value[2] t["cert"] = value[2] t["key"] = value[2] else t[option] = value[2] end end function handle_secret(t, option, value) t[option[1]] = value[2] t[option[2]] = value[3] g_switches[value[1]]= true end function handle_remote_cert_tls(t, option, value) if value[2] ~= "client" and value[2] ~= "server" then io.stderr:write(string.format("Warning: ignoring invalid option '%s'\n", value[1])) return end t[option] = value[2] end function handle_routes(t, option, value) if not value[2] then io.stderr:write("Warning: invalid option 'route'\n") return end netmask = (value[3] and value[3] ~= "default") and value[3] or "255.255.255.255" gateway = (value[4] and value[4] ~= "default") and value[4] or "0.0.0.0" metric = (value[5] and value[5] ~= "default") and value[5] or "0" if not is_ipv4(value[2]) then if value[2] == "vpn_gateway" or value[2] == "net_gateway" or value[2] == "remote_host" then io.stderr:write(string.format("Warning: sorry, the '%s' keyword is not supported by NetworkManager in option '%s'\n", value[2], value[1])) else io.stderr:write(string.format("Warning: '%s' is not a valid IPv4 address in option '%s'\n", value[2], value[1])) end return end if not is_ipv4(netmask) then io.stderr:write(string.format("Warning: '%s' is not a valid IPv4 netmask in option '%s'\n", netmask, value[1])) return end if not is_ipv4(gateway) then if gateway == "vpn_gateway" or gateway == "net_gateway" or gateway == "remote_host" then io.stderr:write(string.format("Warning: sorry, the '%s' keyword is not supported by NetworkManager in option '%s'\n", gateway, value[1])) else io.stderr:write(string.format("Warning: '%s' is not a valid IPv4 gateway in option '%s'\n", gateway, value[1])) end return end if not tonumber(metric) then io.stderr:write(string.format("Warning: '%s' is not a valid metric in option '%s'\n", metric, value[1])) return end if not t[option] then t[option] = {} end t[option][#t[option]+1] = {value[2], netmask, gateway, metric} end function handle_verify_x509_name(t, option, value) if not value[2] then io.stderr:write("Warning: missing argument in option 'verify-x509-name'\n") return end value[2] = unquote(value[2]) value[3] = value[3] or "subject" if value[3] ~= "subject" and value[3] ~= "name" and value[3] ~= "name-prefix" then io.stderr:write(string.format("Warning: ignoring invalid value '%s' for type in option '%s'\n", value[3], value[1])) return end t[option] = value[3] .. ":" .. value[2] end -- global variables g_vpn_data = {} g_ip4_data = {} g_switches = {} vpn2nm = { ["auth"] = { nm_opt="auth", func=handle_generic, tbl=g_vpn_data }, ["auth-user-pass"] = { nm_opt="auth-user-pass", func=set_bool, tbl={} }, ["ca"] = { nm_opt="ca", func=handle_path, tbl=g_vpn_data }, ["cert"] = { nm_opt="cert", func=handle_path, tbl=g_vpn_data }, ["cipher"] = { nm_opt="cipher", func=handle_generic, tbl=g_vpn_data }, ["client"] = { nm_opt="client", func=set_bool, tbl={} }, ["comp-lzo"] = { nm_opt="comp-lzo", func=handle_comp_lzo, tbl=g_vpn_data }, ["dev"] = { nm_opt="dev", func=handle_generic, tbl=g_vpn_data }, ["dev-type"] = { nm_opt="dev-type", func=handle_dev_type, tbl=g_vpn_data }, ["float"] = { nm_opt="float", func=handle_yes, tbl=g_vpn_data }, ["fragment"] = { nm_opt="fragment-size", func=handle_generic, tbl=g_vpn_data }, ["http-proxy"] = { nm_opt={"proxy-type", "proxy-server", "proxy-port"}, func=handle_proxy, tbl=g_vpn_data }, ["http-proxy-retry"] = { nm_opt="proxy-retry", func=handle_yes, tbl=g_vpn_data }, ["ifconfig"] = { nm_opt={"local-ip", "remote-ip"}, func=handle_ifconfig, tbl=g_vpn_data }, ["keepalive"] = { nm_opt={"ping", "ping-restart"}, func=handle_keepalive, tbl=g_vpn_data }, ["key"] = { nm_opt="key", func=handle_path, tbl=g_vpn_data }, ["keysize"] = { nm_opt="keysize", func=handle_generic, tbl=g_vpn_data }, ["max-routes"] = { nm_opt="max-routes", func=handle_number, tbl=g_vpn_data }, ["mssfix"] = { nm_opt="mssfix", func=handle_yes, tbl=g_vpn_data }, ["ns-cert-type"] = { nm_opt="ns-cert-type", func=handle_remote_cert_tls, tbl=g_vpn_data }, ["ping"] = { nm_opt="ping", func=handle_number, tbl=g_vpn_data }, ["ping-exit"] = { nm_opt="ping-exit", func=handle_number, tbl=g_vpn_data }, ["ping-restart"] = { nm_opt="ping-restart", func=handle_number, tbl=g_vpn_data }, ["pkcs12"] = { nm_opt="client", func=handle_path, tbl=g_vpn_data }, ["port"] = { nm_opt="port", func=handle_port, tbl=g_vpn_data }, ["proto"] = { nm_opt="proto-tcp", func=handle_proto, tbl=g_vpn_data }, ["remote"] = { nm_opt="remote", func=handle_remote, tbl=g_vpn_data }, ["remote-cert-tls"] = { nm_opt="remote-cert-tls", func=handle_remote_cert_tls, tbl=g_vpn_data }, ["remote-random"] = { nm_opt="remote-random", func=handle_yes, tbl=g_vpn_data }, ["reneg-sec"] = { nm_opt="reneg-seconds", func=handle_generic, tbl=g_vpn_data }, ["route"] = { nm_opt="routes", func=handle_routes, tbl=g_ip4_data }, ["rport"] = { nm_opt="port", func=handle_port, tbl=g_vpn_data }, ["secret"] = { nm_opt={"static-key", "static-key-direction"}, func=handle_secret, tbl=g_vpn_data }, ["socks-proxy"] = { nm_opt={"proxy-type", "proxy-server", "proxy-port"}, func=handle_proxy, tbl=g_vpn_data }, ["socks-proxy-retry"] = { nm_opt="proxy-retry", func=handle_yes, tbl=g_vpn_data }, ["tls-auth"] = { nm_opt={"ta", "ta-dir"}, func=handle_secret, tbl=g_vpn_data }, ["tls-cipher"] = { nm_opt="tls-cipher", func=handle_generic_unquote, tbl=g_vpn_data }, ["tls-client"] = { nm_opt="client", func=set_bool, tbl={} }, ["tls-remote"] = { nm_opt="tls-remote", func=handle_generic_unquote, tbl=g_vpn_data }, ["tun-ipv6"] = { nm_opt="tun-ipv6", func=handle_yes, tbl=g_vpn_data }, ["tun-mtu"] = { nm_opt="tunnel-mtu", func=handle_generic, tbl=g_vpn_data }, ["verify-x509-name"] = { nm_opt="verify-x509-name", func=handle_verify_x509_name,tbl=g_vpn_data }, } ------------------------------------------------------------ -- Read and convert the config into the global g_vpn_data -- ----------------------------------------------------------- function read_and_convert(in_file) local function line_split(line) local t={} local i, idx = 1, 1 local delim = "\"" while true do local a,b = line:find("%S+", idx) if not a then break end local str = line:sub(a,b) local quote = nil if str:sub(1,1) == delim and str:sub(#str,#str) ~= delim then quote = (line.." "):find(delim.."%s", b + 1) end if quote then t[i] = line:sub(a, quote) idx = quote + 1 else t[i] = str idx = b + 1 end i = i + 1 end return t 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"..val)) end end until true end -- check some inter-option dependencies if not g_switches["client"] and not g_switches["secret"] then local msg = in_file .. ": Not a valid OpenVPN client configuration" return false, msg end if not g_switches["remote"] then local msg = in_file .. ": Not a valid OpenVPN configuration (no remote)" return false, msg end -- set 'connection-type' g_vpn_data["connection-type"] = "tls" have_sk = g_switches["secret"] ~= nil have_ca = g_vpn_data["ca"] ~= nil have_certs = ve_ca and g_vpn_data["cert"] and g_vpn_data["key"] if g_switches["auth-user-pass"] then if have_certs then g_vpn_data["connection-type"] = "password-tls" elseif have_ca then g_vpn_data["connection-type"] = "tls" end elseif have_certs then g_vpn_data["connection-type"] = "tls" elseif have_sk then g_vpn_data["connection-type"] = "static-key" 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__ type=vpn autoconnect=no [ipv4] method=auto never-default=true __ROUTES_PLACEHOLDER__ [ipv6] method=auto [vpn] service-type=org.freedesktop.NetworkManager.openvpn ]] connection = connection .. vpn_settings_to_text(g_vpn_data) local routes = "" for idx, r in ipairs(g_ip4_data["routes"] or {}) do routes = routes .. string.format("routes%d=%s/%s,%s,%s\n", idx, r[1], ip_mask_to_prefix(r[2]), r[3], r[4]) end connection = string.gsub(connection, "__NAME_PLACEHOLDER__", (out_file:gsub(".*/", ""))) connection = string.gsub(connection, "__UUID_PLACEHOLDER__", uuid()) 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() s_con = NM.SettingConnection.new() s_ip4 = NM.SettingIP4Config.new() s_vpn = NM.SettingVpn.new() s_con[NM.SETTING_CONNECTION_ID] = name s_con[NM.SETTING_CONNECTION_UUID] = uuid() s_ip4[NM.SETTING_IP_CONFIG_METHOD] = NM.SETTING_IP4_CONFIG_METHOD_AUTO s_con[NM.SETTING_CONNECTION_TYPE] = "vpn" s_vpn[NM.SETTING_VPN_SERVICE_TYPE] = "org.freedesktop.NetworkManager.openvpn" -- add routes local AF_INET = 2 for _, r in ipairs(g_ip4_data["routes"] or {}) do route = NM.IPRoute.new(AF_INET, r[1], ip_mask_to_prefix(r[2]), r[3], r[4]) 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 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 = 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_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