You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
223 lines
9.6 KiB
C++
223 lines
9.6 KiB
C++
// SPDX-FileCopyrightText: 2026 Reion Wong <reionwong@gmail.com>
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
#include "cutefish/networkmanager/types.h"
|
|
#include "cutefish/networkmanager/vpn_schema.h"
|
|
|
|
using namespace cutefish::networkmanager;
|
|
namespace schema = cutefish::networkmanager::vpn::schema;
|
|
using Strings = std::map<std::string, std::string>;
|
|
|
|
static schema::FormField formItem(const std::string& kind, const Strings& data, const std::string& key)
|
|
{
|
|
for (const schema::FormField& item : schema::formFields(kind, data)) {
|
|
if (item.key == key)
|
|
return item;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static std::string get(const Strings& map, const std::string& key)
|
|
{
|
|
const auto found = map.find(key);
|
|
return found == map.end() ? std::string() : found->second;
|
|
}
|
|
|
|
TEST(VpnSchema, EveryKindHasAServerField)
|
|
{
|
|
EXPECT_EQ(schema::kinds().size(), 11u);
|
|
for (const std::string& kind : schema::kinds()) {
|
|
const std::vector<schema::Field> fields = schema::fields(kind);
|
|
EXPECT_FALSE(fields.empty()) << kind;
|
|
bool required = false;
|
|
for (const schema::Field& field : fields) {
|
|
required |= field.required;
|
|
EXPECT_TRUE(!field.key.empty() && !field.label.empty()) << kind;
|
|
if (field.type == schema::Type::Choice)
|
|
EXPECT_GE(field.choices.size(), 2u) << kind << "/" << field.key;
|
|
}
|
|
EXPECT_TRUE(required) << kind;
|
|
}
|
|
EXPECT_TRUE(schema::fields("wireguard").empty());
|
|
EXPECT_TRUE(schema::exportable("openvpn"));
|
|
EXPECT_FALSE(schema::exportable("pptp"));
|
|
}
|
|
|
|
TEST(VpnSchema, RequiredFieldsAreChecked)
|
|
{
|
|
Strings data, secrets;
|
|
std::string error;
|
|
EXPECT_FALSE(schema::apply("openvpn", {}, {}, {}, false, data, secrets, &error));
|
|
EXPECT_NE(error.find("server"), std::string::npos);
|
|
EXPECT_FALSE(schema::apply("wireguard", {}, {}, {}, false, data, secrets, &error));
|
|
EXPECT_FALSE(schema::apply("openvpn", {{"remote", "vpn.example.com"}, {"port", "70000"}}, {}, {}, false, data,
|
|
secrets, &error));
|
|
EXPECT_NE(error.find("Port"), std::string::npos);
|
|
}
|
|
|
|
TEST(VpnSchema, OpenVpnRoundTrip)
|
|
{
|
|
Strings data, secrets;
|
|
std::string error;
|
|
const schema::Values values{
|
|
{"remote", " vpn.example.com:1194 "},
|
|
{"connection-type", "password-tls"},
|
|
{"username", "alice"},
|
|
{"ca", "/etc/ca.pem"},
|
|
{"cert", "/home/a/cert.pem"},
|
|
{"key", "/home/a/key.pem"},
|
|
{"proto-tcp", "true"},
|
|
{"port", "443"},
|
|
{"compress", "lz4"},
|
|
{"dev-type", ""},
|
|
// Hidden while the proxy is off: must not be written.
|
|
{"proxy-server", "proxy.example.com"},
|
|
};
|
|
ASSERT_TRUE(schema::apply("openvpn", values, {{"cert-pass", SecretNotRequired}},
|
|
{{"password", "s3cret"}, {"cert-pass", "ignored"}}, false, data, secrets, &error))
|
|
<< error;
|
|
EXPECT_EQ(get(data, "remote"), "vpn.example.com:1194");
|
|
EXPECT_EQ(get(data, "connection-type"), "password-tls");
|
|
EXPECT_EQ(get(data, "proto-tcp"), "yes");
|
|
EXPECT_EQ(get(data, "port"), "443");
|
|
EXPECT_EQ(get(data, "compress"), "lz4");
|
|
EXPECT_FALSE(data.count("dev-type"));
|
|
EXPECT_FALSE(data.count("proxy-server"));
|
|
EXPECT_EQ(get(data, "password-flags"), "1");
|
|
EXPECT_EQ(get(data, "cert-pass-flags"), "4");
|
|
EXPECT_EQ(get(secrets, "password"), "s3cret");
|
|
EXPECT_FALSE(secrets.count("cert-pass"));
|
|
|
|
// Reading it back shows the same form.
|
|
EXPECT_TRUE(formItem("openvpn", data, "proto-tcp").checked);
|
|
EXPECT_EQ(formItem("openvpn", data, "compress").value, "lz4");
|
|
EXPECT_EQ(formItem("openvpn", data, "cert-pass").storage, uint32_t(SecretNotRequired));
|
|
EXPECT_EQ(formItem("openvpn", data, "password").storage, uint32_t(SecretAgentOwned));
|
|
EXPECT_EQ(formItem("openvpn", data, "password").value, "");
|
|
|
|
// A shared profile's passwords belong to the system; "ask every time" keeps none.
|
|
Strings sharedData, sharedSecrets;
|
|
EXPECT_TRUE(schema::apply("openvpn", values, {{"password", SecretNotSaved}}, {{"password", "typed"}}, true,
|
|
sharedData, sharedSecrets, &error));
|
|
EXPECT_EQ(get(sharedData, "password-flags"), "2");
|
|
EXPECT_EQ(get(sharedData, "cert-pass-flags"), "0");
|
|
EXPECT_TRUE(sharedSecrets.empty());
|
|
}
|
|
|
|
TEST(VpnSchema, KeysTheFormDoesNotKnowStay)
|
|
{
|
|
Strings data{{"remote", "old.example.com"}, {"tls-remote", "legacy"}, {"cipher", "AES-256-GCM"}};
|
|
Strings secrets;
|
|
EXPECT_TRUE(schema::apply("openvpn", {{"remote", "new.example.com"}, {"cipher", ""}}, {}, {}, false, data, secrets,
|
|
nullptr));
|
|
EXPECT_EQ(get(data, "remote"), "new.example.com");
|
|
EXPECT_EQ(get(data, "tls-remote"), "legacy");
|
|
EXPECT_FALSE(data.count("cipher"));
|
|
// A choice value from another editor is offered as itself.
|
|
const auto choices = formItem("openvpn", {{"auth", "whirlpool"}}, "auth").choices;
|
|
ASSERT_FALSE(choices.empty());
|
|
EXPECT_EQ(choices.back().value, "whirlpool");
|
|
}
|
|
|
|
TEST(VpnSchema, PppOptionsAreInvertedAndExclusive)
|
|
{
|
|
Strings data, secrets;
|
|
EXPECT_TRUE(schema::apply("pptp",
|
|
{{"gateway", "pptp.example.com"},
|
|
{"refuse-pap", "false"},
|
|
{"refuse-chap", "true"},
|
|
{"nobsdcomp", "false"},
|
|
{"require-mppe", "require-mppe-128"},
|
|
{"lcp-echo-interval", "true"}},
|
|
{}, {}, false, data, secrets, nullptr));
|
|
EXPECT_EQ(get(data, "refuse-pap"), "yes");
|
|
EXPECT_FALSE(data.count("refuse-chap"));
|
|
EXPECT_EQ(get(data, "nobsdcomp"), "yes");
|
|
EXPECT_EQ(get(data, "require-mppe-128"), "yes");
|
|
EXPECT_FALSE(data.count("require-mppe"));
|
|
EXPECT_EQ(get(data, "lcp-echo-interval"), "30");
|
|
EXPECT_EQ(get(data, "lcp-echo-failure"), "5");
|
|
|
|
EXPECT_FALSE(formItem("pptp", data, "refuse-pap").checked);
|
|
EXPECT_TRUE(formItem("pptp", data, "refuse-chap").checked);
|
|
EXPECT_EQ(formItem("pptp", data, "require-mppe").value, "require-mppe-128");
|
|
EXPECT_TRUE(formItem("pptp", data, "lcp-echo-interval").checked);
|
|
|
|
// Switching encryption to "any" replaces the 128-bit key, and the echo off removes both keys.
|
|
EXPECT_TRUE(schema::apply("pptp",
|
|
{{"gateway", "pptp.example.com"},
|
|
{"require-mppe", "require-mppe"},
|
|
{"lcp-echo-interval", "false"}},
|
|
{}, {}, false, data, secrets, nullptr));
|
|
EXPECT_EQ(get(data, "require-mppe"), "yes");
|
|
EXPECT_FALSE(data.count("require-mppe-128"));
|
|
EXPECT_FALSE(data.count("lcp-echo-interval"));
|
|
EXPECT_FALSE(data.count("lcp-echo-failure"));
|
|
}
|
|
|
|
TEST(VpnSchema, SstpDefaultsAreYes)
|
|
{
|
|
// tls-ext and refuse-pap default to yes: only turning them off writes a key.
|
|
EXPECT_TRUE(formItem("sstp", {}, "tls-ext").checked);
|
|
EXPECT_FALSE(formItem("sstp", {}, "refuse-pap").checked);
|
|
Strings data, secrets;
|
|
EXPECT_TRUE(schema::apply("sstp",
|
|
{{"gateway", "sstp.example.com"},
|
|
{"tls-ext", "false"},
|
|
{"refuse-pap", "true"},
|
|
{"refuse-chap", "false"}},
|
|
{}, {}, false, data, secrets, nullptr));
|
|
EXPECT_EQ(get(data, "tls-ext"), "no");
|
|
EXPECT_FALSE(data.count("refuse-pap"));
|
|
EXPECT_EQ(get(data, "refuse-chap"), "yes");
|
|
EXPECT_FALSE(formItem("sstp", data, "tls-ext").checked);
|
|
}
|
|
|
|
TEST(VpnSchema, LegacyTypeKeysFollowTheFlags)
|
|
{
|
|
Strings data, secrets;
|
|
EXPECT_TRUE(schema::apply("vpnc",
|
|
{{"IPSec gateway", "vpn.example.com"}, {"IPSec ID", "group"}, {"IKE Authmode", "true"}},
|
|
{{"Xauth password", SecretNotSaved}, {"IPSec secret", SecretNotRequired}},
|
|
{{"Xauth password", "x"}}, false, data, secrets, nullptr));
|
|
EXPECT_EQ(get(data, "xauth-password-type"), "ask");
|
|
EXPECT_EQ(get(data, "ipsec-secret-type"), "unused");
|
|
EXPECT_EQ(get(data, "IKE Authmode"), "hybrid");
|
|
EXPECT_TRUE(secrets.empty());
|
|
EXPECT_TRUE(schema::apply("libreswan", {{"right", "vpn.example.com"}, {"rekey", "true"}},
|
|
{{"pskvalue", SecretAgentOwned}}, {{"pskvalue", "group"}}, false, data, secrets, nullptr));
|
|
EXPECT_EQ(get(data, "pskinputmodes"), "save");
|
|
EXPECT_EQ(get(data, "rekey"), "no");
|
|
EXPECT_EQ(get(secrets, "pskvalue"), "group");
|
|
}
|
|
|
|
TEST(VpnSchema, FortiOtpFlagsAreNumbers)
|
|
{
|
|
Strings data, secrets;
|
|
EXPECT_TRUE(schema::apply("fortisslvpn",
|
|
{{"gateway", "f.example.com"}, {"otp-flags", "true"}, {"2fa-flags", "true"}}, {}, {},
|
|
false, data, secrets, nullptr));
|
|
EXPECT_EQ(get(data, "otp-flags"), "2");
|
|
EXPECT_EQ(get(data, "2fa-flags"), "1");
|
|
EXPECT_TRUE(formItem("fortisslvpn", data, "otp-flags").checked);
|
|
EXPECT_TRUE(schema::apply("fortisslvpn", {{"gateway", "f.example.com"}, {"otp-flags", "false"}}, {}, {}, false,
|
|
data, secrets, nullptr));
|
|
// Off is the absent key, which the plugin reads as flags 0.
|
|
EXPECT_FALSE(data.count("otp-flags"));
|
|
EXPECT_FALSE(data.count("2fa-flags"));
|
|
}
|
|
|
|
TEST(VpnSchema, ConditionsAcceptTheEmptyValue)
|
|
{
|
|
const std::vector<schema::Field> fields = schema::fields("l2tp");
|
|
schema::Field user;
|
|
for (const schema::Field& field : fields) {
|
|
if (field.key == "user")
|
|
user = field;
|
|
}
|
|
EXPECT_TRUE(schema::shown(user, fields, {{"user-auth-type", ""}}));
|
|
EXPECT_FALSE(schema::shown(user, fields, {{"user-auth-type", "tls"}}));
|
|
}
|