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"