File indexing completed on 2024-04-21 16:20:25

0001 /*
0002     SPDX-FileCopyrightText: 2008 Will Stephenson <wstephenson@kde.org>
0003     SPDX-FileCopyrightText: 2011-2012 Rajeesh K Nambiar <rajeeshknambiar@gmail.com>
0004     SPDX-FileCopyrightText: 2011 Ilia Kats <ilia-kats@gmx.net>
0005     SPDX-FileCopyrightText: 2012-2016 Lamarque V. Souza <lamarque@kde.org>
0006 
0007     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0008 */
0009 
0010 #include "openvpn.h"
0011 
0012 #include <QLatin1Char>
0013 #include <QRegularExpression>
0014 #include <QStandardPaths>
0015 #include <QStringBuilder>
0016 
0017 #include <KLocalizedString>
0018 #include <KMessageBox>
0019 #include <KPluginFactory>
0020 #include <QRegularExpression>
0021 #include <kwidgetsaddons_version.h>
0022 
0023 #include <NetworkManagerQt/Connection>
0024 #include <NetworkManagerQt/Ipv4Setting>
0025 #include <NetworkManagerQt/VpnSetting>
0026 
0027 #include "openvpnauth.h"
0028 #include "openvpnwidget.h"
0029 
0030 #include <arpa/inet.h>
0031 
0032 #include "nm-openvpn-service.h"
0033 
0034 K_PLUGIN_CLASS_WITH_JSON(OpenVpnUiPlugin, "plasmanetworkmanagement_openvpnui.json")
0035 
0036 #define AUTH_TAG "auth"
0037 #define AUTH_USER_PASS_TAG "auth-user-pass"
0038 #define CA_TAG "ca"
0039 #define CERT_TAG "cert"
0040 #define CIPHER_TAG "cipher"
0041 #define CLIENT_TAG "client"
0042 #define COMPRESS_TAG "compress"
0043 #define COMP_TAG "comp-lzo"
0044 #define DEV_TAG "dev"
0045 #define FRAGMENT_TAG "fragment"
0046 #define IFCONFIG_TAG "ifconfig"
0047 #define KEY_TAG "key"
0048 #define MSSFIX_TAG "mssfix"
0049 #define PKCS12_TAG "pkcs12"
0050 #define PORT_TAG "port"
0051 #define PROTO_TAG "proto"
0052 #define HTTP_PROXY_TAG "http-proxy"
0053 #define HTTP_PROXY_RETRY_TAG "http-proxy-retry"
0054 #define SOCKS_PROXY_TAG "socks-proxy"
0055 #define SOCKS_PROXY_RETRY_TAG "socks-proxy-retry"
0056 #define REMOTE_TAG "remote"
0057 #define RENEG_SEC_TAG "reneg-sec"
0058 #define RPORT_TAG "rport"
0059 #define SECRET_TAG "secret"
0060 #define TLS_AUTH_TAG "tls-auth"
0061 #define TLS_CRYPT_TAG "tls-crypt"
0062 #define TLS_CLIENT_TAG "tls-client"
0063 #define TLS_REMOTE_TAG "tls-remote"
0064 #define TUNMTU_TAG "tun-mtu"
0065 #define KEY_DIRECTION_TAG "key-direction"
0066 
0067 #define BEGIN_KEY_CA_TAG "<ca>"
0068 #define END_KEY_CA_TAG "</ca>"
0069 #define BEGIN_KEY_CERT_TAG "<cert>"
0070 #define END_KEY_CERT_TAG "</cert>"
0071 #define BEGIN_KEY_KEY_TAG "<key>"
0072 #define END_KEY_KEY_TAG "</key>"
0073 #define BEGIN_KEY_SECRET_TAG "<secret>"
0074 #define END_KEY_SECRET_TAG "</secret>"
0075 #define BEGIN_TLS_AUTH_TAG "<tls-auth>"
0076 #define END_TLS_AUTH_TAG "</tls-auth>"
0077 #define BEGIN_TLS_CRYPT_TAG "<tls-crypt>"
0078 #define END_TLS_CRYPT_TAG "</tls-crypt>"
0079 
0080 #define PROC_TYPE_TAG "Proc-Type: 4,ENCRYPTED"
0081 #define PKCS8_TAG "-----BEGIN ENCRYPTED PRIVATE KEY-----"
0082 
0083 QString localCertPath()
0084 {
0085     return QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1String("/networkmanagement/certificates/");
0086 }
0087 
0088 QString unQuote(QString &certVal, const QString &fileName)
0089 {
0090     /* Unquote according to openvpn rules
0091      * Unquoted filename is returned, and @certVal is modified
0092      * to the leftover string
0093      */
0094     int nextSep;
0095     QString certFile = certVal.trimmed();
0096     if (certFile.startsWith('"') || certFile.startsWith('\'')) { // Quoted
0097         certFile.remove(0, 1); // Remove the starting quote
0098         nextSep = 0;
0099         while ((nextSep = certFile.indexOf(QRegularExpression(QStringLiteral("\"|'")), nextSep)) != -1) {
0100             if (nextSep > 0 && certFile.at(nextSep - 1) != '\\') { // Quote not escaped
0101                 certVal = certFile.right(certFile.length() - nextSep - 1); // Leftover string
0102                 certFile.truncate(nextSep); // Quoted string
0103                 break;
0104             }
0105         }
0106     } else {
0107         nextSep = certFile.indexOf(QRegularExpression(QStringLiteral("\\s"))); // First whitespace
0108         if (nextSep != -1) {
0109             certVal = certFile.right(certFile.length() - nextSep - 1); // Leftover
0110             certFile = certFile.left(nextSep); // value
0111         } else {
0112             certVal.clear();
0113         }
0114     }
0115     certFile.replace("\\\\", "\\"); // Replace '\\' with '\'
0116     certFile.replace("\\ ", " "); // Replace escaped space with space
0117     if (QFileInfo(certFile).isRelative()) {
0118         certFile = QFileInfo(fileName).dir().absolutePath() + QLatin1Char('/') + certFile;
0119     }
0120     return certFile;
0121 }
0122 
0123 bool isEncrypted(const QString &fileName)
0124 {
0125     bool encrypted = false;
0126     // TODO: if is_pkcs12(fileName) return true;
0127     // NOTE: will have to use SEC_PKCS12DecoderStart and friends from <p12.h>, which will
0128     //       build a new dependency on nss-devel. See NetworkManager/libnm-util/crypto_nss.c+453
0129 
0130     QFile inFile(fileName);
0131     if (!inFile.open(QFile::ReadOnly)) {
0132         return false;
0133     }
0134     QTextStream in(&inFile);
0135     while (!in.atEnd()) {
0136         const QString line = in.readLine();
0137         if (!line.isEmpty() && (line.startsWith(PROC_TYPE_TAG) || line.startsWith(PKCS8_TAG))) {
0138             encrypted = true;
0139             break;
0140         }
0141     }
0142     inFile.close();
0143     return encrypted;
0144 }
0145 
0146 OpenVpnUiPlugin::OpenVpnUiPlugin(QObject *parent, const QVariantList &)
0147     : VpnUiPlugin(parent)
0148 {
0149 }
0150 
0151 OpenVpnUiPlugin::~OpenVpnUiPlugin() = default;
0152 
0153 SettingWidget *OpenVpnUiPlugin::widget(const NetworkManager::VpnSetting::Ptr &setting, QWidget *parent)
0154 {
0155     auto wid = new OpenVpnSettingWidget(setting, parent);
0156     return wid;
0157 }
0158 
0159 SettingWidget *OpenVpnUiPlugin::askUser(const NetworkManager::VpnSetting::Ptr &setting, const QStringList &hints, QWidget *parent)
0160 {
0161     return new OpenVpnAuthWidget(setting, hints, parent);
0162 }
0163 
0164 QString OpenVpnUiPlugin::suggestedFileName(const NetworkManager::ConnectionSettings::Ptr &connection) const
0165 {
0166     return connection->id() + QStringLiteral("_openvpn.conf");
0167 }
0168 
0169 QStringList OpenVpnUiPlugin::supportedFileExtensions() const
0170 {
0171     return {QStringLiteral("*.ovpn"), QStringLiteral("*.conf")};
0172 }
0173 
0174 VpnUiPlugin::ImportResult OpenVpnUiPlugin::importConnectionSettings(const QString &fileName)
0175 {
0176     GError *error = nullptr;
0177 
0178     GSList *plugins = nm_vpn_plugin_info_list_load();
0179 
0180     NMVpnPluginInfo *plugin_info = nm_vpn_plugin_info_list_find_by_service(plugins, "org.freedesktop.NetworkManager.openvpn");
0181 
0182     if (!plugin_info) {
0183         return VpnUiPlugin::ImportResult::fail(i18n("NetworkManager is missing support for OpenVPN"));
0184     }
0185 
0186     NMVpnEditorPlugin *plugin = nm_vpn_plugin_info_load_editor_plugin(plugin_info, &error);
0187 
0188     NMConnection *connection = nm_vpn_editor_plugin_import(plugin, fileName.toUtf8().constData(), &error);
0189 
0190     if (!connection) {
0191         const QString errorMessage = QString::fromUtf8(error->message);
0192         g_error_free(error);
0193 
0194         return VpnUiPlugin::ImportResult::fail(errorMessage);
0195     }
0196 
0197     return VpnUiPlugin::ImportResult::pass(connection);
0198 }
0199 
0200 QString OpenVpnUiPlugin::saveFile(QTextStream &in, const QString &endTag, const QString &connectionName, const QString &fileName)
0201 {
0202     const QString certificatesDirectory = localCertPath() + connectionName;
0203     const QString absoluteFilePath = certificatesDirectory + '/' + fileName;
0204     QFile outFile(absoluteFilePath);
0205 
0206     QDir().mkpath(certificatesDirectory);
0207     if (!outFile.open(QFile::WriteOnly | QFile::Text)) {
0208         KMessageBox::information(nullptr, i18n("Error saving file %1: %2", absoluteFilePath, outFile.errorString()));
0209         return {};
0210     }
0211 
0212     QTextStream out(&outFile);
0213     while (!in.atEnd()) {
0214         const QString line = in.readLine();
0215 
0216         if (line.indexOf(endTag) >= 0) {
0217             break;
0218         }
0219 
0220         out << line << "\n";
0221     }
0222 
0223     outFile.close();
0224     return absoluteFilePath;
0225 }
0226 
0227 QString OpenVpnUiPlugin::tryToCopyToCertificatesDirectory(const QString &connectionName, const QString &sourceFilePath)
0228 {
0229     const QString certificatesDirectory = localCertPath();
0230     const QString absoluteFilePath = certificatesDirectory + connectionName + '_' + QFileInfo(sourceFilePath).fileName();
0231 
0232     QFile sourceFile(sourceFilePath);
0233 
0234     QDir().mkpath(certificatesDirectory);
0235     if (!sourceFile.copy(absoluteFilePath)) {
0236         KMessageBox::information(nullptr, i18n("Error copying certificate to %1: %2", absoluteFilePath, sourceFile.errorString()));
0237         return sourceFilePath;
0238     }
0239 
0240     return absoluteFilePath;
0241 }
0242 
0243 VpnUiPlugin::ExportResult OpenVpnUiPlugin::exportConnectionSettings(const NetworkManager::ConnectionSettings::Ptr &connection, const QString &fileName)
0244 {
0245     QFile expFile(fileName);
0246     if (!expFile.open(QIODevice::WriteOnly | QIODevice::Text)) {
0247         return VpnUiPlugin::ExportResult::fail("Could not open file for writing");
0248     }
0249 
0250     NMStringMap dataMap;
0251     NMStringMap secretData;
0252 
0253     NetworkManager::VpnSetting::Ptr vpnSetting = connection->setting(NetworkManager::Setting::Vpn).dynamicCast<NetworkManager::VpnSetting>();
0254     dataMap = vpnSetting->data();
0255     secretData = vpnSetting->secrets();
0256 
0257     QString line;
0258     QString cacert, user_cert, private_key;
0259 
0260     line = QString(CLIENT_TAG) + '\n';
0261     expFile.write(line.toLatin1());
0262     line = QString(REMOTE_TAG) + ' ' + dataMap[NM_OPENVPN_KEY_REMOTE]
0263         + (dataMap[NM_OPENVPN_KEY_PORT].isEmpty() ? "\n" : (' ' + dataMap[NM_OPENVPN_KEY_PORT]) + '\n');
0264     expFile.write(line.toLatin1());
0265     if (dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_TLS //
0266         || dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_PASSWORD //
0267         || dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_PASSWORD_TLS) {
0268         if (!dataMap[NM_OPENVPN_KEY_CA].isEmpty()) {
0269             cacert = dataMap[NM_OPENVPN_KEY_CA];
0270         }
0271     }
0272     if (dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_TLS //
0273         || dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_PASSWORD_TLS) {
0274         if (!dataMap[NM_OPENVPN_KEY_CERT].isEmpty()) {
0275             user_cert = dataMap[NM_OPENVPN_KEY_CERT];
0276         }
0277         if (!dataMap[NM_OPENVPN_KEY_KEY].isEmpty()) {
0278             private_key = dataMap[NM_OPENVPN_KEY_KEY];
0279         }
0280     }
0281     // Handle PKCS#12 (all certs are the same file)
0282     if (!cacert.isEmpty() && !user_cert.isEmpty() && !private_key.isEmpty() && cacert == user_cert && cacert == private_key) {
0283         line = QString("%1 \"%2\"\n").arg(PKCS12_TAG, cacert);
0284         expFile.write(line.toLatin1());
0285     } else {
0286         if (!cacert.isEmpty()) {
0287             line = QString("%1 \"%2\"\n").arg(CA_TAG, cacert);
0288             expFile.write(line.toLatin1());
0289         }
0290         if (!user_cert.isEmpty()) {
0291             line = QString("%1 \"%2\"\n").arg(CERT_TAG, user_cert);
0292             expFile.write(line.toLatin1());
0293         }
0294         if (!private_key.isEmpty()) {
0295             line = QString("%1 \"%2\"\n").arg(KEY_TAG, private_key);
0296             expFile.write(line.toLatin1());
0297         }
0298     }
0299     if (dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_TLS //
0300         || dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_STATIC_KEY //
0301         || dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_PASSWORD //
0302         || dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_PASSWORD_TLS) {
0303         line = QString(AUTH_USER_PASS_TAG) + '\n';
0304         expFile.write(line.toLatin1());
0305         if (!dataMap[NM_OPENVPN_KEY_TLS_REMOTE].isEmpty()) {
0306             line = QString(TLS_REMOTE_TAG) + " \"" + dataMap[NM_OPENVPN_KEY_TLS_REMOTE] + "\"\n";
0307             expFile.write(line.toLatin1());
0308         }
0309         if (!dataMap[NM_OPENVPN_KEY_TA].isEmpty()) {
0310             line = QString(TLS_AUTH_TAG) + " \"" + dataMap[NM_OPENVPN_KEY_TA] + '\"'
0311                 + (dataMap[NM_OPENVPN_KEY_TA_DIR].isEmpty() ? "\n" : (' ' + dataMap[NM_OPENVPN_KEY_TA_DIR]) + '\n');
0312             expFile.write(line.toLatin1());
0313         }
0314     }
0315     if (dataMap[NM_OPENVPN_KEY_CONNECTION_TYPE] == NM_OPENVPN_CONTYPE_STATIC_KEY) {
0316         line = QString(SECRET_TAG) + " \"" + dataMap[NM_OPENVPN_KEY_STATIC_KEY] + '\"'
0317             + (dataMap[NM_OPENVPN_KEY_STATIC_KEY_DIRECTION].isEmpty() ? "\n" : (' ' + dataMap[NM_OPENVPN_KEY_STATIC_KEY_DIRECTION]) + '\n');
0318         expFile.write(line.toLatin1());
0319     }
0320     if (dataMap.contains(NM_OPENVPN_KEY_RENEG_SECONDS) && !dataMap[NM_OPENVPN_KEY_RENEG_SECONDS].isEmpty()) {
0321         line = QString(RENEG_SEC_TAG) + ' ' + dataMap[NM_OPENVPN_KEY_RENEG_SECONDS] + '\n';
0322         expFile.write(line.toLatin1());
0323     }
0324     if (!dataMap[NM_OPENVPN_KEY_CIPHER].isEmpty()) {
0325         line = QString(CIPHER_TAG) + ' ' + dataMap[NM_OPENVPN_KEY_CIPHER] + '\n';
0326         expFile.write(line.toLatin1());
0327     }
0328     if (dataMap[NM_OPENVPN_KEY_COMP_LZO] == "adaptive") {
0329         line = QString(COMP_TAG) + " adaptive\n";
0330         expFile.write(line.toLatin1());
0331     }
0332     if (dataMap[NM_OPENVPN_KEY_COMPRESS] == "yes") {
0333         line = QString(COMPRESS_TAG) + " yes\n";
0334         expFile.write(line.toLatin1());
0335     }
0336     if (dataMap[NM_OPENVPN_KEY_COMPRESS] == "lzo") {
0337         line = QString(COMPRESS_TAG) + " lzo\n";
0338         expFile.write(line.toLatin1());
0339     }
0340     if (dataMap[NM_OPENVPN_KEY_COMPRESS] == "lz4") {
0341         line = QString(COMPRESS_TAG) + " lz4\n";
0342         expFile.write(line.toLatin1());
0343     }
0344     if (dataMap[NM_OPENVPN_KEY_COMPRESS] == "lz4-v2") {
0345         line = QString(COMPRESS_TAG) + " lz4-v2\n";
0346         expFile.write(line.toLatin1());
0347     }
0348     if (dataMap[NM_OPENVPN_KEY_MSSFIX] == "yes") {
0349         line = QString(MSSFIX_TAG) + '\n';
0350         expFile.write(line.toLatin1());
0351     }
0352     if (!dataMap[NM_OPENVPN_KEY_TUNNEL_MTU].isEmpty()) {
0353         line = QString(TUNMTU_TAG) + ' ' + dataMap[NM_OPENVPN_KEY_TUNNEL_MTU] + '\n';
0354         expFile.write(line.toLatin1());
0355     }
0356     if (!dataMap[NM_OPENVPN_KEY_FRAGMENT_SIZE].isEmpty()) {
0357         line = QString(FRAGMENT_TAG) + ' ' + dataMap[NM_OPENVPN_KEY_FRAGMENT_SIZE] + '\n';
0358         expFile.write(line.toLatin1());
0359     }
0360     line = QString(DEV_TAG) + (dataMap[NM_OPENVPN_KEY_TAP_DEV] == "yes" ? " tap\n" : " tun\n");
0361     expFile.write(line.toLatin1());
0362     line = QString(PROTO_TAG) + (dataMap[NM_OPENVPN_KEY_PROTO_TCP] == "yes" ? " tcp\n" : " udp\n");
0363     expFile.write(line.toLatin1());
0364     // Proxy stuff
0365     if (!dataMap[NM_OPENVPN_KEY_PROXY_TYPE].isEmpty()) {
0366         QString proxy_port = dataMap[NM_OPENVPN_KEY_PROXY_PORT];
0367         if (dataMap[NM_OPENVPN_KEY_PROXY_TYPE] == "http" && !dataMap[NM_OPENVPN_KEY_PROXY_SERVER].isEmpty() && dataMap.contains(NM_OPENVPN_KEY_PROXY_PORT)) {
0368             if (proxy_port.toInt() == 0) {
0369                 proxy_port = "8080";
0370             }
0371             line = QString(HTTP_PROXY_TAG) + ' ' + dataMap[NM_OPENVPN_KEY_PROXY_SERVER] + ' ' + proxy_port
0372                 + (dataMap[NM_OPENVPN_KEY_HTTP_PROXY_USERNAME].isEmpty() ? "\n" : (' ' + fileName + "-httpauthfile") + '\n');
0373             expFile.write(line.toLatin1());
0374             if (dataMap[NM_OPENVPN_KEY_PROXY_RETRY] == "yes") {
0375                 line = QString(HTTP_PROXY_RETRY_TAG) + '\n';
0376                 expFile.write(line.toLatin1());
0377             }
0378             // If there is a username, need to write an authfile
0379             if (!dataMap[NM_OPENVPN_KEY_HTTP_PROXY_USERNAME].isEmpty()) {
0380                 QFile authFile(fileName + "-httpauthfile");
0381                 if (authFile.open(QFile::WriteOnly | QFile::Text)) {
0382                     line = dataMap[NM_OPENVPN_KEY_HTTP_PROXY_USERNAME]
0383                         + (dataMap[NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD].isEmpty() ? "\n" : (dataMap[NM_OPENVPN_KEY_HTTP_PROXY_PASSWORD] + '\n'));
0384                     authFile.write(line.toLatin1());
0385                     authFile.close();
0386                 }
0387             }
0388         } else if (dataMap[NM_OPENVPN_KEY_PROXY_TYPE] == "socks" && !dataMap[NM_OPENVPN_KEY_PROXY_SERVER].isEmpty()
0389                    && dataMap.contains(NM_OPENVPN_KEY_PROXY_PORT)) {
0390             if (proxy_port.toInt() == 0) {
0391                 proxy_port = "1080";
0392             }
0393             line = QString(SOCKS_PROXY_TAG) + dataMap[NM_OPENVPN_KEY_PROXY_SERVER] + ' ' + proxy_port + '\n';
0394             expFile.write(line.toLatin1());
0395             if (dataMap[NM_OPENVPN_KEY_PROXY_RETRY] == "yes") {
0396                 line = QString(SOCKS_PROXY_RETRY_TAG) + '\n';
0397                 expFile.write(line.toLatin1());
0398             }
0399         }
0400     }
0401 
0402     NetworkManager::Ipv4Setting::Ptr ipv4Setting = connection->setting(NetworkManager::Setting::Ipv4).dynamicCast<NetworkManager::Ipv4Setting>();
0403     // Export X-NM-Routes
0404     if (!ipv4Setting->routes().isEmpty()) {
0405         QString routes;
0406         for (const NetworkManager::IpRoute &route : ipv4Setting->routes()) {
0407             routes += route.ip().toString() % QLatin1Char('/') % QString::number(route.prefixLength()) % QLatin1Char(' ');
0408         }
0409         if (!routes.isEmpty()) {
0410             routes = "X-NM-Routes " + routes.trimmed();
0411             expFile.write(routes.toLatin1() + '\n');
0412         }
0413     }
0414     // Add hard-coded stuff
0415     expFile.write(
0416         "nobind\n"
0417         "auth-nocache\n"
0418         "script-security 2\n"
0419         "persist-key\n"
0420         "persist-tun\n"
0421         "user nobody\n"
0422         "group nobody\n");
0423     expFile.close();
0424     return VpnUiPlugin::ExportResult::pass();
0425 }
0426 
0427 #include "openvpn.moc"