nghttpx: Read QUIC keying materials from file
Add --frontend-quic-secret-file to read QUIC keying materials from file. --frontend-quic-connection-id-encryption-key was removed in favor of this new option.
This commit is contained in:
parent
c40309ae8e
commit
308c73bfa2
|
@ -341,9 +341,9 @@ configure script with ``--enable-http3``.
|
||||||
For nghttpx to reload configurations and swapping its executable while
|
For nghttpx to reload configurations and swapping its executable while
|
||||||
gracefully terminating old worker processes, eBPF is required. Run
|
gracefully terminating old worker processes, eBPF is required. Run
|
||||||
the configure script with ``--enable-http3 --with-libbpf`` to build
|
the configure script with ``--enable-http3 --with-libbpf`` to build
|
||||||
eBPF program. The Connection ID encryption key must be set with
|
eBPF program. The QUIC keying material must be set with
|
||||||
``--frontend-quic-connection-id-encryption-key`` and must not change
|
``--frontend-quic-secret-file`` in order to keep the existing
|
||||||
in order to keep the existing connections alive during reload.
|
connections alive during reload.
|
||||||
|
|
||||||
The detailed steps to build HTTP/3 enabled h2load and nghttpx follow.
|
The detailed steps to build HTTP/3 enabled h2load and nghttpx follow.
|
||||||
|
|
||||||
|
|
|
@ -534,10 +534,10 @@ nghttpx does not support HTTP/3 on backend connection.
|
||||||
|
|
||||||
Hot swapping (SIGUSR2) or configuration reload (SIGHUP) require eBPF
|
Hot swapping (SIGUSR2) or configuration reload (SIGHUP) require eBPF
|
||||||
program. Without eBPF, old worker processes keep getting HTTP/3
|
program. Without eBPF, old worker processes keep getting HTTP/3
|
||||||
traffic and do not work as intended. Connection ID encryption key
|
traffic and do not work as intended. The QUIC keying material to
|
||||||
must be set with
|
encrypt Connection ID must be set with
|
||||||
:option:`--frontend-quic-connection-id-encryption-key` and must not
|
:option:`--frontend-quic-secret-file` and must provide the existing
|
||||||
change in order to keep the existing connections alive during reload.
|
keys in order to keep the existing connections alive during reload.
|
||||||
|
|
||||||
In order announce that HTTP/3 endpoint is available, you should
|
In order announce that HTTP/3 endpoint is available, you should
|
||||||
specify alt-svc header field. For example, the following options send
|
specify alt-svc header field. For example, the following options send
|
||||||
|
|
|
@ -192,8 +192,8 @@ OPTIONS = [
|
||||||
"frontend-quic-qlog-dir",
|
"frontend-quic-qlog-dir",
|
||||||
"frontend-quic-require-token",
|
"frontend-quic-require-token",
|
||||||
"frontend-quic-congestion-controller",
|
"frontend-quic-congestion-controller",
|
||||||
"frontend-quic-connection-id-encryption-key",
|
|
||||||
"frontend-quic-server-id",
|
"frontend-quic-server-id",
|
||||||
|
"frontend-quic-secret-file",
|
||||||
]
|
]
|
||||||
|
|
||||||
LOGVARS = [
|
LOGVARS = [
|
||||||
|
|
56
src/shrpx.cc
56
src/shrpx.cc
|
@ -1859,14 +1859,6 @@ void fill_default_config(Config *config) {
|
||||||
|
|
||||||
upstreamconf.congestion_controller = NGTCP2_CC_ALGO_CUBIC;
|
upstreamconf.congestion_controller = NGTCP2_CC_ALGO_CUBIC;
|
||||||
|
|
||||||
// TODO Not really nice to generate random key here, but fine for
|
|
||||||
// now.
|
|
||||||
if (RAND_bytes(upstreamconf.cid_encryption_key.data(),
|
|
||||||
upstreamconf.cid_encryption_key.size()) != 1) {
|
|
||||||
assert(0);
|
|
||||||
abort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RAND_bytes(upstreamconf.server_id.data(),
|
if (RAND_bytes(upstreamconf.server_id.data(),
|
||||||
upstreamconf.server_id.size()) != 1) {
|
upstreamconf.server_id.size()) != 1) {
|
||||||
assert(0);
|
assert(0);
|
||||||
|
@ -3253,14 +3245,31 @@ HTTP/3 and QUIC:
|
||||||
? "cubic"
|
? "cubic"
|
||||||
: "bbr")
|
: "bbr")
|
||||||
<< R"(
|
<< R"(
|
||||||
--frontend-quic-connection-id-encryption-key=<HEXSTRING>
|
--frontend-quic-secret-file=<PATH>
|
||||||
Specify Connection ID encryption key. The encryption
|
Path to file that contains secure random data to be used
|
||||||
key must be 16 bytes, and it must be encoded in hex
|
as QUIC keying materials. It is used to derive keys for
|
||||||
string (which is 32 bytes long). If this option is
|
encrypting tokens and Connection IDs. It is not used to
|
||||||
omitted, new key is generated. In order to survive QUIC
|
encrypt QUIC packets. Each line of this file must
|
||||||
connection in a configuration reload event, old and new
|
contain exactly 136 bytes hex-encoded string (when
|
||||||
configuration must have this option and share the same
|
decoded the byte string is 68 bytes long). The first 2
|
||||||
key.
|
bits of decoded byte string are used to identify the
|
||||||
|
keying material. An empty line or a line which starts
|
||||||
|
'#' is ignored. The file can contain more than one
|
||||||
|
keying materials. Because the identifier is 2 bits, at
|
||||||
|
most 4 keying materials are read and the remaining data
|
||||||
|
is discarded. The first keying material in the file is
|
||||||
|
primarily used for encryption and decryption for new
|
||||||
|
connection. The other ones are used to decrypt data for
|
||||||
|
the existing connections. Specifying multiple keying
|
||||||
|
materials enables key rotation. Please note that key
|
||||||
|
rotation does not occur automatically. User should
|
||||||
|
update files or change options values and restart
|
||||||
|
nghttpx gracefully. If opening or reading given file
|
||||||
|
fails, all loaded keying materials are discarded and it
|
||||||
|
is treated as if none of this option is given. If this
|
||||||
|
option is not given or an error occurred while opening
|
||||||
|
or reading a file, a keying material is generated
|
||||||
|
internally on startup and reload.
|
||||||
--frontend-quic-server-id=<HEXSTRING>
|
--frontend-quic-server-id=<HEXSTRING>
|
||||||
Specify server ID encoded in Connection ID to identify
|
Specify server ID encoded in Connection ID to identify
|
||||||
this particular server instance. Connection ID is
|
this particular server instance. Connection ID is
|
||||||
|
@ -4067,10 +4076,10 @@ int main(int argc, char **argv) {
|
||||||
182},
|
182},
|
||||||
{SHRPX_OPT_FRONTEND_QUIC_CONGESTION_CONTROLLER.c_str(),
|
{SHRPX_OPT_FRONTEND_QUIC_CONGESTION_CONTROLLER.c_str(),
|
||||||
required_argument, &flag, 183},
|
required_argument, &flag, 183},
|
||||||
{SHRPX_OPT_FRONTEND_QUIC_CONNECTION_ID_ENCRYPTION_KEY.c_str(),
|
|
||||||
required_argument, &flag, 184},
|
|
||||||
{SHRPX_OPT_FRONTEND_QUIC_SERVER_ID.c_str(), required_argument, &flag,
|
{SHRPX_OPT_FRONTEND_QUIC_SERVER_ID.c_str(), required_argument, &flag,
|
||||||
185},
|
185},
|
||||||
|
{SHRPX_OPT_FRONTEND_QUIC_SECRET_FILE.c_str(), required_argument, &flag,
|
||||||
|
186},
|
||||||
{nullptr, 0, nullptr, 0}};
|
{nullptr, 0, nullptr, 0}};
|
||||||
|
|
||||||
int option_index = 0;
|
int option_index = 0;
|
||||||
|
@ -4948,17 +4957,16 @@ int main(int argc, char **argv) {
|
||||||
cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_CONGESTION_CONTROLLER,
|
cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_CONGESTION_CONTROLLER,
|
||||||
StringRef{optarg});
|
StringRef{optarg});
|
||||||
break;
|
break;
|
||||||
case 184:
|
|
||||||
// --frontend-quic-connection-id-encryption-key
|
|
||||||
cmdcfgs.emplace_back(
|
|
||||||
SHRPX_OPT_FRONTEND_QUIC_CONNECTION_ID_ENCRYPTION_KEY,
|
|
||||||
StringRef{optarg});
|
|
||||||
break;
|
|
||||||
case 185:
|
case 185:
|
||||||
// --frontend-quic-server-id
|
// --frontend-quic-server-id
|
||||||
cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_SERVER_ID,
|
cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_SERVER_ID,
|
||||||
StringRef{optarg});
|
StringRef{optarg});
|
||||||
break;
|
break;
|
||||||
|
case 186:
|
||||||
|
// --frontend-quic-secret-file
|
||||||
|
cmdcfgs.emplace_back(SHRPX_OPT_FRONTEND_QUIC_SECRET_FILE,
|
||||||
|
StringRef{optarg});
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -230,6 +230,69 @@ read_tls_ticket_key_file(const std::vector<StringRef> &files,
|
||||||
return ticket_keys;
|
return ticket_keys;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef ENABLE_HTTP3
|
||||||
|
std::shared_ptr<QUICKeyingMaterials>
|
||||||
|
read_quic_secret_file(const StringRef &path) {
|
||||||
|
constexpr size_t expectedlen =
|
||||||
|
SHRPX_QUIC_SECRET_RESERVEDLEN + SHRPX_QUIC_SECRETLEN + SHRPX_QUIC_SALTLEN;
|
||||||
|
|
||||||
|
auto qkms = std::make_shared<QUICKeyingMaterials>();
|
||||||
|
auto &kms = qkms->keying_materials;
|
||||||
|
|
||||||
|
std::ifstream f(path.c_str());
|
||||||
|
if (!f) {
|
||||||
|
LOG(ERROR) << "frontend-quic-secret-file: could not open file " << path;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<char, 4096> buf;
|
||||||
|
|
||||||
|
while (f.getline(buf.data(), buf.size())) {
|
||||||
|
if (f.gcount() == 1 || buf[0] == '#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto s = StringRef{std::begin(buf), std::begin(buf) + f.gcount() - 1};
|
||||||
|
if (s.size() != expectedlen * 2 || !util::is_hex_string(s)) {
|
||||||
|
LOG(ERROR) << "frontend-quic-secret-file: each line must be a "
|
||||||
|
<< expectedlen * 2 << " bytes hex encoded string";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
kms.emplace_back();
|
||||||
|
auto &qkm = kms.back();
|
||||||
|
|
||||||
|
auto p = std::begin(s);
|
||||||
|
|
||||||
|
util::decode_hex(std::begin(qkm.reserved),
|
||||||
|
StringRef{p, p + qkm.reserved.size()});
|
||||||
|
p += qkm.reserved.size() * 2;
|
||||||
|
util::decode_hex(std::begin(qkm.secret),
|
||||||
|
StringRef{p, p + qkm.secret.size()});
|
||||||
|
p += qkm.secret.size() * 2;
|
||||||
|
util::decode_hex(std::begin(qkm.salt), StringRef{p, p + qkm.salt.size()});
|
||||||
|
p += qkm.salt.size() * 2;
|
||||||
|
|
||||||
|
assert(static_cast<size_t>(p - std::begin(s)) == expectedlen * 2);
|
||||||
|
|
||||||
|
qkm.id = qkm.reserved[0] & 0xc0;
|
||||||
|
|
||||||
|
if (kms.size() == 4) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (f.bad()) {
|
||||||
|
LOG(ERROR)
|
||||||
|
<< "frontend-quic-secret-file: error occurred while reading file "
|
||||||
|
<< path;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return qkms;
|
||||||
|
}
|
||||||
|
#endif // ENABLE_HTTP3
|
||||||
|
|
||||||
FILE *open_file_for_write(const char *filename) {
|
FILE *open_file_for_write(const char *filename) {
|
||||||
std::array<char, STRERROR_BUFSIZE> errbuf;
|
std::array<char, STRERROR_BUFSIZE> errbuf;
|
||||||
|
|
||||||
|
@ -2344,6 +2407,9 @@ int option_lookup_token(const char *name, size_t namelen) {
|
||||||
if (util::strieq_l("backend-http2-window-siz", name, 24)) {
|
if (util::strieq_l("backend-http2-window-siz", name, 24)) {
|
||||||
return SHRPX_OPTID_BACKEND_HTTP2_WINDOW_SIZE;
|
return SHRPX_OPTID_BACKEND_HTTP2_WINDOW_SIZE;
|
||||||
}
|
}
|
||||||
|
if (util::strieq_l("frontend-quic-secret-fil", name, 24)) {
|
||||||
|
return SHRPX_OPTID_FRONTEND_QUIC_SECRET_FILE;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'g':
|
case 'g':
|
||||||
if (util::strieq_l("http2-no-cookie-crumblin", name, 24)) {
|
if (util::strieq_l("http2-no-cookie-crumblin", name, 24)) {
|
||||||
|
@ -2689,10 +2755,6 @@ int option_lookup_token(const char *name, size_t namelen) {
|
||||||
case 42:
|
case 42:
|
||||||
switch (name[41]) {
|
switch (name[41]) {
|
||||||
case 'y':
|
case 'y':
|
||||||
if (util::strieq_l("frontend-quic-connection-id-encryption-ke", name,
|
|
||||||
41)) {
|
|
||||||
return SHRPX_OPTID_FRONTEND_QUIC_CONNECTION_ID_ENCRYPTION_KEY;
|
|
||||||
}
|
|
||||||
if (util::strieq_l("tls-session-cache-memcached-address-famil", name,
|
if (util::strieq_l("tls-session-cache-memcached-address-famil", name,
|
||||||
41)) {
|
41)) {
|
||||||
return SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_ADDRESS_FAMILY;
|
return SHRPX_OPTID_TLS_SESSION_CACHE_MEMCACHED_ADDRESS_FAMILY;
|
||||||
|
@ -4029,18 +4091,6 @@ int parse_config(Config *config, int optid, const StringRef &opt,
|
||||||
}
|
}
|
||||||
#endif // ENABLE_HTTP3
|
#endif // ENABLE_HTTP3
|
||||||
|
|
||||||
return 0;
|
|
||||||
case SHRPX_OPTID_FRONTEND_QUIC_CONNECTION_ID_ENCRYPTION_KEY:
|
|
||||||
#ifdef ENABLE_HTTP3
|
|
||||||
if (optarg.size() != config->quic.upstream.cid_encryption_key.size() * 2 ||
|
|
||||||
!util::is_hex_string(optarg)) {
|
|
||||||
LOG(ERROR) << opt << ": must be a hex-string";
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
util::decode_hex(std::begin(config->quic.upstream.cid_encryption_key),
|
|
||||||
optarg);
|
|
||||||
#endif // ENABLE_HTTP3
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
case SHRPX_OPTID_FRONTEND_QUIC_SERVER_ID:
|
case SHRPX_OPTID_FRONTEND_QUIC_SERVER_ID:
|
||||||
#ifdef ENABLE_HTTP3
|
#ifdef ENABLE_HTTP3
|
||||||
|
@ -4052,6 +4102,12 @@ int parse_config(Config *config, int optid, const StringRef &opt,
|
||||||
util::decode_hex(std::begin(config->quic.upstream.server_id), optarg);
|
util::decode_hex(std::begin(config->quic.upstream.server_id), optarg);
|
||||||
#endif // ENABLE_HTTP3
|
#endif // ENABLE_HTTP3
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
case SHRPX_OPTID_FRONTEND_QUIC_SECRET_FILE:
|
||||||
|
#ifdef ENABLE_HTTP3
|
||||||
|
config->quic.upstream.secret_file = make_string_ref(config->balloc, optarg);
|
||||||
|
#endif // ENABLE_HTTP3
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
case SHRPX_OPTID_CONF:
|
case SHRPX_OPTID_CONF:
|
||||||
LOG(WARN) << "conf: ignored";
|
LOG(WARN) << "conf: ignored";
|
||||||
|
|
|
@ -391,10 +391,10 @@ constexpr auto SHRPX_OPT_FRONTEND_QUIC_REQUIRE_TOKEN =
|
||||||
StringRef::from_lit("frontend-quic-require-token");
|
StringRef::from_lit("frontend-quic-require-token");
|
||||||
constexpr auto SHRPX_OPT_FRONTEND_QUIC_CONGESTION_CONTROLLER =
|
constexpr auto SHRPX_OPT_FRONTEND_QUIC_CONGESTION_CONTROLLER =
|
||||||
StringRef::from_lit("frontend-quic-congestion-controller");
|
StringRef::from_lit("frontend-quic-congestion-controller");
|
||||||
constexpr auto SHRPX_OPT_FRONTEND_QUIC_CONNECTION_ID_ENCRYPTION_KEY =
|
|
||||||
StringRef::from_lit("frontend-quic-connection-id-encryption-key");
|
|
||||||
constexpr auto SHRPX_OPT_FRONTEND_QUIC_SERVER_ID =
|
constexpr auto SHRPX_OPT_FRONTEND_QUIC_SERVER_ID =
|
||||||
StringRef::from_lit("frontend-quic-server-id");
|
StringRef::from_lit("frontend-quic-server-id");
|
||||||
|
constexpr auto SHRPX_OPT_FRONTEND_QUIC_SECRET_FILE =
|
||||||
|
StringRef::from_lit("frontend-quic-secret-file");
|
||||||
|
|
||||||
constexpr size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8;
|
constexpr size_t SHRPX_OBFUSCATED_NODE_LENGTH = 8;
|
||||||
|
|
||||||
|
@ -606,10 +606,18 @@ struct TLSCertificate {
|
||||||
};
|
};
|
||||||
|
|
||||||
#ifdef ENABLE_HTTP3
|
#ifdef ENABLE_HTTP3
|
||||||
struct QUICSecret {
|
struct QUICKeyingMaterial {
|
||||||
std::array<uint8_t, SHRPX_QUIC_STATELESS_RESET_SECRETLEN>
|
std::array<uint8_t, SHRPX_QUIC_SECRET_RESERVEDLEN> reserved;
|
||||||
stateless_reset_secret;
|
std::array<uint8_t, SHRPX_QUIC_SECRETLEN> secret;
|
||||||
std::array<uint8_t, SHRPX_QUIC_TOKEN_SECRETLEN> token_secret;
|
std::array<uint8_t, SHRPX_QUIC_SALTLEN> salt;
|
||||||
|
std::array<uint8_t, SHRPX_QUIC_CID_ENCRYPTION_KEYLEN> cid_encryption_key;
|
||||||
|
// Identifier of this keying material. Only the first 2 bits are
|
||||||
|
// used.
|
||||||
|
uint8_t id;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct QUICKeyingMaterials {
|
||||||
|
std::vector<QUICKeyingMaterial> keying_materials;
|
||||||
};
|
};
|
||||||
#endif // ENABLE_HTTP3
|
#endif // ENABLE_HTTP3
|
||||||
|
|
||||||
|
@ -765,8 +773,8 @@ struct QUICConfig {
|
||||||
ngtcp2_cc_algo congestion_controller;
|
ngtcp2_cc_algo congestion_controller;
|
||||||
bool early_data;
|
bool early_data;
|
||||||
bool require_token;
|
bool require_token;
|
||||||
std::array<uint8_t, SHRPX_QUIC_CID_ENCRYPTION_KEYLEN> cid_encryption_key;
|
|
||||||
std::array<uint8_t, SHRPX_QUIC_SERVER_IDLEN> server_id;
|
std::array<uint8_t, SHRPX_QUIC_SERVER_IDLEN> server_id;
|
||||||
|
StringRef secret_file;
|
||||||
} upstream;
|
} upstream;
|
||||||
struct {
|
struct {
|
||||||
StringRef prog_file;
|
StringRef prog_file;
|
||||||
|
@ -1220,12 +1228,12 @@ enum {
|
||||||
SHRPX_OPTID_FRONTEND_MAX_REQUESTS,
|
SHRPX_OPTID_FRONTEND_MAX_REQUESTS,
|
||||||
SHRPX_OPTID_FRONTEND_NO_TLS,
|
SHRPX_OPTID_FRONTEND_NO_TLS,
|
||||||
SHRPX_OPTID_FRONTEND_QUIC_CONGESTION_CONTROLLER,
|
SHRPX_OPTID_FRONTEND_QUIC_CONGESTION_CONTROLLER,
|
||||||
SHRPX_OPTID_FRONTEND_QUIC_CONNECTION_ID_ENCRYPTION_KEY,
|
|
||||||
SHRPX_OPTID_FRONTEND_QUIC_DEBUG_LOG,
|
SHRPX_OPTID_FRONTEND_QUIC_DEBUG_LOG,
|
||||||
SHRPX_OPTID_FRONTEND_QUIC_EARLY_DATA,
|
SHRPX_OPTID_FRONTEND_QUIC_EARLY_DATA,
|
||||||
SHRPX_OPTID_FRONTEND_QUIC_IDLE_TIMEOUT,
|
SHRPX_OPTID_FRONTEND_QUIC_IDLE_TIMEOUT,
|
||||||
SHRPX_OPTID_FRONTEND_QUIC_QLOG_DIR,
|
SHRPX_OPTID_FRONTEND_QUIC_QLOG_DIR,
|
||||||
SHRPX_OPTID_FRONTEND_QUIC_REQUIRE_TOKEN,
|
SHRPX_OPTID_FRONTEND_QUIC_REQUIRE_TOKEN,
|
||||||
|
SHRPX_OPTID_FRONTEND_QUIC_SECRET_FILE,
|
||||||
SHRPX_OPTID_FRONTEND_QUIC_SERVER_ID,
|
SHRPX_OPTID_FRONTEND_QUIC_SERVER_ID,
|
||||||
SHRPX_OPTID_FRONTEND_READ_TIMEOUT,
|
SHRPX_OPTID_FRONTEND_READ_TIMEOUT,
|
||||||
SHRPX_OPTID_FRONTEND_WRITE_TIMEOUT,
|
SHRPX_OPTID_FRONTEND_WRITE_TIMEOUT,
|
||||||
|
@ -1377,6 +1385,11 @@ std::unique_ptr<TicketKeys>
|
||||||
read_tls_ticket_key_file(const std::vector<StringRef> &files,
|
read_tls_ticket_key_file(const std::vector<StringRef> &files,
|
||||||
const EVP_CIPHER *cipher, const EVP_MD *hmac);
|
const EVP_CIPHER *cipher, const EVP_MD *hmac);
|
||||||
|
|
||||||
|
#ifdef ENABLE_HTTP3
|
||||||
|
std::shared_ptr<QUICKeyingMaterials>
|
||||||
|
read_quic_secret_file(const StringRef &path);
|
||||||
|
#endif // ENABLE_HTTP3
|
||||||
|
|
||||||
// Returns string representation of |proto|.
|
// Returns string representation of |proto|.
|
||||||
StringRef strproto(Proto proto);
|
StringRef strproto(Proto proto);
|
||||||
|
|
||||||
|
|
|
@ -298,8 +298,6 @@ int ConnectionHandler::create_single_worker() {
|
||||||
#endif // HAVE_MRUBY
|
#endif // HAVE_MRUBY
|
||||||
|
|
||||||
#ifdef ENABLE_HTTP3
|
#ifdef ENABLE_HTTP3
|
||||||
single_worker_->set_quic_secret(quic_secret_);
|
|
||||||
|
|
||||||
if (single_worker_->setup_quic_server_socket() != 0) {
|
if (single_worker_->setup_quic_server_socket() != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -404,8 +402,6 @@ int ConnectionHandler::create_worker_thread(size_t num) {
|
||||||
# endif // HAVE_MRUBY
|
# endif // HAVE_MRUBY
|
||||||
|
|
||||||
# ifdef ENABLE_HTTP3
|
# ifdef ENABLE_HTTP3
|
||||||
worker->set_quic_secret(quic_secret_);
|
|
||||||
|
|
||||||
if ((!apiconf.enabled || i != 0) &&
|
if ((!apiconf.enabled || i != 0) &&
|
||||||
worker->setup_quic_server_socket() != 0) {
|
worker->setup_quic_server_socket() != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
|
@ -1047,25 +1043,14 @@ int ConnectionHandler::forward_quic_packet(const UpstreamAddr *faddr,
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ConnectionHandler::create_quic_secret() {
|
void ConnectionHandler::set_quic_keying_materials(
|
||||||
auto quic_secret = std::make_shared<QUICSecret>();
|
std::shared_ptr<QUICKeyingMaterials> qkms) {
|
||||||
|
quic_keying_materials_ = std::move(qkms);
|
||||||
|
}
|
||||||
|
|
||||||
if (generate_quic_stateless_reset_secret(
|
const std::shared_ptr<QUICKeyingMaterials> &
|
||||||
quic_secret->stateless_reset_secret.data()) != 0) {
|
ConnectionHandler::get_quic_keying_materials() const {
|
||||||
LOG(ERROR) << "Failed to generate QUIC Stateless Reset secret";
|
return quic_keying_materials_;
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (generate_quic_token_secret(quic_secret->token_secret.data()) != 0) {
|
|
||||||
LOG(ERROR) << "Failed to generate QUIC token secret";
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
quic_secret_ = std::move(quic_secret);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConnectionHandler::set_cid_prefixes(
|
void ConnectionHandler::set_cid_prefixes(
|
||||||
|
@ -1287,14 +1272,13 @@ int ConnectionHandler::quic_ipc_read() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto config = get_config();
|
auto &qkm = quic_keying_materials_->keying_materials.front();
|
||||||
auto &quicconf = config->quic;
|
|
||||||
|
|
||||||
std::array<uint8_t, SHRPX_QUIC_DECRYPTED_DCIDLEN> decrypted_dcid;
|
std::array<uint8_t, SHRPX_QUIC_DECRYPTED_DCIDLEN> decrypted_dcid;
|
||||||
|
|
||||||
if (decrypt_quic_connection_id(
|
if (decrypt_quic_connection_id(decrypted_dcid.data(),
|
||||||
decrypted_dcid.data(), dcid + SHRPX_QUIC_CID_PREFIX_OFFSET,
|
dcid + SHRPX_QUIC_CID_PREFIX_OFFSET,
|
||||||
quicconf.upstream.cid_encryption_key.data()) != 0) {
|
qkm.cid_encryption_key.data()) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -198,7 +198,8 @@ public:
|
||||||
const Address &local_addr, const uint8_t *cid_prefix,
|
const Address &local_addr, const uint8_t *cid_prefix,
|
||||||
const uint8_t *data, size_t datalen);
|
const uint8_t *data, size_t datalen);
|
||||||
|
|
||||||
int create_quic_secret();
|
void set_quic_keying_materials(std::shared_ptr<QUICKeyingMaterials> qkms);
|
||||||
|
const std::shared_ptr<QUICKeyingMaterials> &get_quic_keying_materials() const;
|
||||||
|
|
||||||
void set_cid_prefixes(
|
void set_cid_prefixes(
|
||||||
const std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>>
|
const std::vector<std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN>>
|
||||||
|
@ -263,7 +264,7 @@ private:
|
||||||
# ifdef HAVE_LIBBPF
|
# ifdef HAVE_LIBBPF
|
||||||
std::vector<BPFRef> quic_bpf_refs_;
|
std::vector<BPFRef> quic_bpf_refs_;
|
||||||
# endif // HAVE_LIBBPF
|
# endif // HAVE_LIBBPF
|
||||||
std::shared_ptr<QUICSecret> quic_secret_;
|
std::shared_ptr<QUICKeyingMaterials> quic_keying_materials_;
|
||||||
std::vector<SSL_CTX *> quic_all_ssl_ctx_;
|
std::vector<SSL_CTX *> quic_all_ssl_ctx_;
|
||||||
std::vector<std::vector<SSL_CTX *>> quic_indexed_ssl_ctx_;
|
std::vector<std::vector<SSL_CTX *>> quic_indexed_ssl_ctx_;
|
||||||
#endif // ENABLE_HTTP3
|
#endif // ENABLE_HTTP3
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
#include "shrpx_quic.h"
|
#include "shrpx_quic.h"
|
||||||
#include "shrpx_worker.h"
|
#include "shrpx_worker.h"
|
||||||
#include "shrpx_http.h"
|
#include "shrpx_http.h"
|
||||||
|
#include "shrpx_connection_handler.h"
|
||||||
#ifdef HAVE_MRUBY
|
#ifdef HAVE_MRUBY
|
||||||
# include "shrpx_mruby.h"
|
# include "shrpx_mruby.h"
|
||||||
#endif // HAVE_MRUBY
|
#endif // HAVE_MRUBY
|
||||||
|
@ -217,21 +218,17 @@ int get_new_connection_id(ngtcp2_conn *conn, ngtcp2_cid *cid, uint8_t *token,
|
||||||
auto upstream = static_cast<Http3Upstream *>(user_data);
|
auto upstream = static_cast<Http3Upstream *>(user_data);
|
||||||
auto handler = upstream->get_client_handler();
|
auto handler = upstream->get_client_handler();
|
||||||
auto worker = handler->get_worker();
|
auto worker = handler->get_worker();
|
||||||
|
auto conn_handler = worker->get_connection_handler();
|
||||||
|
auto &qkms = conn_handler->get_quic_keying_materials();
|
||||||
|
auto &qkm = qkms->keying_materials.front();
|
||||||
|
|
||||||
auto config = get_config();
|
if (generate_quic_connection_id(*cid, cidlen, worker->get_cid_prefix(),
|
||||||
auto &quicconf = config->quic;
|
qkm.id, qkm.cid_encryption_key.data()) != 0) {
|
||||||
|
|
||||||
if (generate_quic_connection_id(
|
|
||||||
*cid, cidlen, worker->get_cid_prefix(),
|
|
||||||
quicconf.upstream.cid_encryption_key.data()) != 0) {
|
|
||||||
return NGTCP2_ERR_CALLBACK_FAILURE;
|
return NGTCP2_ERR_CALLBACK_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &quic_secret = worker->get_quic_secret();
|
if (generate_quic_stateless_reset_token(token, *cid, qkm.secret.data(),
|
||||||
auto &secret = quic_secret->stateless_reset_secret;
|
qkm.secret.size()) != 0) {
|
||||||
|
|
||||||
if (generate_quic_stateless_reset_token(token, *cid, secret.data(),
|
|
||||||
secret.size()) != 0) {
|
|
||||||
return NGTCP2_ERR_CALLBACK_FAILURE;
|
return NGTCP2_ERR_CALLBACK_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -487,11 +484,13 @@ int Http3Upstream::handshake_completed() {
|
||||||
|
|
||||||
auto path = ngtcp2_conn_get_path(conn_);
|
auto path = ngtcp2_conn_get_path(conn_);
|
||||||
auto worker = handler_->get_worker();
|
auto worker = handler_->get_worker();
|
||||||
auto &quic_secret = worker->get_quic_secret();
|
auto conn_handler = worker->get_connection_handler();
|
||||||
auto &secret = quic_secret->token_secret;
|
auto &qkms = conn_handler->get_quic_keying_materials();
|
||||||
|
auto &qkm = qkms->keying_materials.front();
|
||||||
|
|
||||||
if (generate_token(token.data(), tokenlen, path->remote.addr,
|
if (generate_token(token.data(), tokenlen, path->remote.addr,
|
||||||
path->remote.addrlen, secret.data()) != 0) {
|
path->remote.addrlen, qkm.secret.data(),
|
||||||
|
qkm.secret.size()) != 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -513,6 +512,7 @@ int Http3Upstream::init(const UpstreamAddr *faddr, const Address &remote_addr,
|
||||||
int rv;
|
int rv;
|
||||||
|
|
||||||
auto worker = handler_->get_worker();
|
auto worker = handler_->get_worker();
|
||||||
|
auto conn_handler = worker->get_connection_handler();
|
||||||
|
|
||||||
auto callbacks = ngtcp2_callbacks{
|
auto callbacks = ngtcp2_callbacks{
|
||||||
nullptr, // client_initial
|
nullptr, // client_initial
|
||||||
|
@ -557,11 +557,14 @@ int Http3Upstream::init(const UpstreamAddr *faddr, const Address &remote_addr,
|
||||||
auto &quicconf = config->quic;
|
auto &quicconf = config->quic;
|
||||||
auto &http3conf = config->http3;
|
auto &http3conf = config->http3;
|
||||||
|
|
||||||
|
auto &qkms = conn_handler->get_quic_keying_materials();
|
||||||
|
auto &qkm = qkms->keying_materials.front();
|
||||||
|
|
||||||
ngtcp2_cid scid;
|
ngtcp2_cid scid;
|
||||||
|
|
||||||
if (generate_quic_connection_id(
|
if (generate_quic_connection_id(scid, SHRPX_QUIC_SCIDLEN,
|
||||||
scid, SHRPX_QUIC_SCIDLEN, worker->get_cid_prefix(),
|
worker->get_cid_prefix(), qkm.id,
|
||||||
quicconf.upstream.cid_encryption_key.data()) != 0) {
|
qkm.cid_encryption_key.data()) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -608,12 +611,8 @@ int Http3Upstream::init(const UpstreamAddr *faddr, const Address &remote_addr,
|
||||||
params.original_dcid = initial_hd.dcid;
|
params.original_dcid = initial_hd.dcid;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &quic_secret = worker->get_quic_secret();
|
rv = generate_quic_stateless_reset_token(
|
||||||
auto &stateless_reset_secret = quic_secret->stateless_reset_secret;
|
params.stateless_reset_token, scid, qkm.secret.data(), qkm.secret.size());
|
||||||
|
|
||||||
rv = generate_quic_stateless_reset_token(params.stateless_reset_token, scid,
|
|
||||||
stateless_reset_secret.data(),
|
|
||||||
stateless_reset_secret.size());
|
|
||||||
if (rv != 0) {
|
if (rv != 0) {
|
||||||
ULOG(ERROR, this) << "generate_quic_stateless_reset_token failed";
|
ULOG(ERROR, this) << "generate_quic_stateless_reset_token failed";
|
||||||
return -1;
|
return -1;
|
||||||
|
|
|
@ -144,7 +144,7 @@ int quic_send_packet(const UpstreamAddr *faddr, const sockaddr *remote_sa,
|
||||||
}
|
}
|
||||||
|
|
||||||
int generate_quic_retry_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
int generate_quic_retry_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
||||||
const uint8_t *server_id,
|
const uint8_t *server_id, uint8_t km_id,
|
||||||
const uint8_t *key) {
|
const uint8_t *key) {
|
||||||
assert(cidlen == SHRPX_QUIC_SCIDLEN);
|
assert(cidlen == SHRPX_QUIC_SCIDLEN);
|
||||||
|
|
||||||
|
@ -154,6 +154,8 @@ int generate_quic_retry_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
||||||
|
|
||||||
cid.datalen = cidlen;
|
cid.datalen = cidlen;
|
||||||
|
|
||||||
|
cid.data[0] = (cid.data[0] & 0x3f) | km_id;
|
||||||
|
|
||||||
auto p = cid.data + SHRPX_QUIC_CID_PREFIX_OFFSET;
|
auto p = cid.data + SHRPX_QUIC_CID_PREFIX_OFFSET;
|
||||||
|
|
||||||
std::copy_n(server_id, SHRPX_QUIC_SERVER_IDLEN, p);
|
std::copy_n(server_id, SHRPX_QUIC_SERVER_IDLEN, p);
|
||||||
|
@ -162,7 +164,8 @@ int generate_quic_retry_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
||||||
}
|
}
|
||||||
|
|
||||||
int generate_quic_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
int generate_quic_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
||||||
const uint8_t *cid_prefix, const uint8_t *key) {
|
const uint8_t *cid_prefix, uint8_t km_id,
|
||||||
|
const uint8_t *key) {
|
||||||
assert(cidlen == SHRPX_QUIC_SCIDLEN);
|
assert(cidlen == SHRPX_QUIC_SCIDLEN);
|
||||||
|
|
||||||
if (RAND_bytes(cid.data, cidlen) != 1) {
|
if (RAND_bytes(cid.data, cidlen) != 1) {
|
||||||
|
@ -171,6 +174,8 @@ int generate_quic_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
||||||
|
|
||||||
cid.datalen = cidlen;
|
cid.datalen = cidlen;
|
||||||
|
|
||||||
|
cid.data[0] = (cid.data[0] & 0x3f) | km_id;
|
||||||
|
|
||||||
auto p = cid.data + SHRPX_QUIC_CID_PREFIX_OFFSET;
|
auto p = cid.data + SHRPX_QUIC_CID_PREFIX_OFFSET;
|
||||||
|
|
||||||
std::copy_n(cid_prefix, SHRPX_QUIC_CID_PREFIXLEN, p);
|
std::copy_n(cid_prefix, SHRPX_QUIC_CID_PREFIXLEN, p);
|
||||||
|
@ -257,32 +262,16 @@ int generate_quic_stateless_reset_token(uint8_t *token, const ngtcp2_cid &cid,
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int generate_quic_stateless_reset_secret(uint8_t *secret) {
|
|
||||||
if (RAND_bytes(secret, SHRPX_QUIC_STATELESS_RESET_SECRETLEN) != 1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int generate_quic_token_secret(uint8_t *secret) {
|
|
||||||
if (RAND_bytes(secret, SHRPX_QUIC_TOKEN_SECRETLEN) != 1) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int generate_retry_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
int generate_retry_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
||||||
socklen_t salen, const ngtcp2_cid &retry_scid,
|
socklen_t salen, const ngtcp2_cid &retry_scid,
|
||||||
const ngtcp2_cid &odcid, const uint8_t *token_secret) {
|
const ngtcp2_cid &odcid, const uint8_t *secret,
|
||||||
|
size_t secretlen) {
|
||||||
auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch())
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
auto stokenlen = ngtcp2_crypto_generate_retry_token(
|
auto stokenlen = ngtcp2_crypto_generate_retry_token(
|
||||||
token, token_secret, SHRPX_QUIC_TOKEN_SECRETLEN, sa, salen, &retry_scid,
|
token, secret, secretlen, sa, salen, &retry_scid, &odcid, t);
|
||||||
&odcid, t);
|
|
||||||
if (stokenlen < 0) {
|
if (stokenlen < 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -294,15 +283,16 @@ int generate_retry_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
||||||
|
|
||||||
int verify_retry_token(ngtcp2_cid &odcid, const uint8_t *token, size_t tokenlen,
|
int verify_retry_token(ngtcp2_cid &odcid, const uint8_t *token, size_t tokenlen,
|
||||||
const ngtcp2_cid &dcid, const sockaddr *sa,
|
const ngtcp2_cid &dcid, const sockaddr *sa,
|
||||||
socklen_t salen, const uint8_t *token_secret) {
|
socklen_t salen, const uint8_t *secret,
|
||||||
|
size_t secretlen) {
|
||||||
|
|
||||||
auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch())
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
if (ngtcp2_crypto_verify_retry_token(&odcid, token, tokenlen, token_secret,
|
if (ngtcp2_crypto_verify_retry_token(&odcid, token, tokenlen, secret,
|
||||||
SHRPX_QUIC_TOKEN_SECRETLEN, sa, salen,
|
secretlen, sa, salen, &dcid,
|
||||||
&dcid, 10 * NGTCP2_SECONDS, t) != 0) {
|
10 * NGTCP2_SECONDS, t) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -310,13 +300,13 @@ int verify_retry_token(ngtcp2_cid &odcid, const uint8_t *token, size_t tokenlen,
|
||||||
}
|
}
|
||||||
|
|
||||||
int generate_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
int generate_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
||||||
size_t salen, const uint8_t *token_secret) {
|
size_t salen, const uint8_t *secret, size_t secretlen) {
|
||||||
auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch())
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
auto stokenlen = ngtcp2_crypto_generate_regular_token(
|
auto stokenlen = ngtcp2_crypto_generate_regular_token(
|
||||||
token, token_secret, SHRPX_QUIC_TOKEN_SECRETLEN, sa, salen, t);
|
token, secret, secretlen, sa, salen, t);
|
||||||
if (stokenlen < 0) {
|
if (stokenlen < 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
@ -327,18 +317,48 @@ int generate_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
||||||
}
|
}
|
||||||
|
|
||||||
int verify_token(const uint8_t *token, size_t tokenlen, const sockaddr *sa,
|
int verify_token(const uint8_t *token, size_t tokenlen, const sockaddr *sa,
|
||||||
socklen_t salen, const uint8_t *token_secret) {
|
socklen_t salen, const uint8_t *secret, size_t secretlen) {
|
||||||
auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
auto t = std::chrono::duration_cast<std::chrono::nanoseconds>(
|
||||||
std::chrono::system_clock::now().time_since_epoch())
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
.count();
|
.count();
|
||||||
|
|
||||||
if (ngtcp2_crypto_verify_regular_token(token, tokenlen, token_secret,
|
if (ngtcp2_crypto_verify_regular_token(token, tokenlen, secret, secretlen, sa,
|
||||||
SHRPX_QUIC_TOKEN_SECRETLEN, sa, salen,
|
salen, 3600 * NGTCP2_SECONDS,
|
||||||
3600 * NGTCP2_SECONDS, t) != 0) {
|
t) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int generate_quic_connection_id_encryption_key(uint8_t *key, size_t keylen,
|
||||||
|
const uint8_t *secret,
|
||||||
|
size_t secretlen,
|
||||||
|
const uint8_t *salt,
|
||||||
|
size_t saltlen) {
|
||||||
|
constexpr uint8_t info[] = "connection id encryption key";
|
||||||
|
ngtcp2_crypto_md sha256;
|
||||||
|
ngtcp2_crypto_md_init(
|
||||||
|
&sha256, reinterpret_cast<void *>(const_cast<EVP_MD *>(EVP_sha256())));
|
||||||
|
|
||||||
|
if (ngtcp2_crypto_hkdf(key, keylen, &sha256, secret, secretlen, salt, saltlen,
|
||||||
|
info, str_size(info)) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUICKeyingMaterial *
|
||||||
|
select_quic_keying_material(const QUICKeyingMaterials &qkms,
|
||||||
|
const uint8_t *cid) {
|
||||||
|
for (auto &qkm : qkms.keying_materials) {
|
||||||
|
if (((*cid) & 0xc0) == qkm.id) {
|
||||||
|
return &qkm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &qkms.keying_materials.front();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace shrpx
|
} // namespace shrpx
|
||||||
|
|
|
@ -60,6 +60,8 @@ bool operator==(const ngtcp2_cid &lhs, const ngtcp2_cid &rhs);
|
||||||
namespace shrpx {
|
namespace shrpx {
|
||||||
|
|
||||||
struct UpstreamAddr;
|
struct UpstreamAddr;
|
||||||
|
struct QUICKeyingMaterials;
|
||||||
|
struct QUICKeyingMaterial;
|
||||||
|
|
||||||
constexpr size_t SHRPX_QUIC_SCIDLEN = 20;
|
constexpr size_t SHRPX_QUIC_SCIDLEN = 20;
|
||||||
constexpr size_t SHRPX_QUIC_SERVER_IDLEN = 2;
|
constexpr size_t SHRPX_QUIC_SERVER_IDLEN = 2;
|
||||||
|
@ -69,10 +71,11 @@ constexpr size_t SHRPX_QUIC_CID_PREFIX_OFFSET = 1;
|
||||||
constexpr size_t SHRPX_QUIC_DECRYPTED_DCIDLEN = 16;
|
constexpr size_t SHRPX_QUIC_DECRYPTED_DCIDLEN = 16;
|
||||||
constexpr size_t SHRPX_QUIC_CID_ENCRYPTION_KEYLEN = 16;
|
constexpr size_t SHRPX_QUIC_CID_ENCRYPTION_KEYLEN = 16;
|
||||||
constexpr size_t SHRPX_QUIC_MAX_UDP_PAYLOAD_SIZE = 1472;
|
constexpr size_t SHRPX_QUIC_MAX_UDP_PAYLOAD_SIZE = 1472;
|
||||||
constexpr size_t SHRPX_QUIC_STATELESS_RESET_SECRETLEN = 32;
|
|
||||||
constexpr size_t SHRPX_QUIC_TOKEN_SECRETLEN = 32;
|
|
||||||
constexpr size_t SHRPX_QUIC_CONN_CLOSE_PKTLEN = 256;
|
constexpr size_t SHRPX_QUIC_CONN_CLOSE_PKTLEN = 256;
|
||||||
constexpr size_t SHRPX_QUIC_STATELESS_RESET_BURST = 100;
|
constexpr size_t SHRPX_QUIC_STATELESS_RESET_BURST = 100;
|
||||||
|
constexpr size_t SHRPX_QUIC_SECRET_RESERVEDLEN = 4;
|
||||||
|
constexpr size_t SHRPX_QUIC_SECRETLEN = 32;
|
||||||
|
constexpr size_t SHRPX_QUIC_SALTLEN = 32;
|
||||||
|
|
||||||
ngtcp2_tstamp quic_timestamp();
|
ngtcp2_tstamp quic_timestamp();
|
||||||
|
|
||||||
|
@ -82,11 +85,12 @@ int quic_send_packet(const UpstreamAddr *faddr, const sockaddr *remote_sa,
|
||||||
size_t gso_size);
|
size_t gso_size);
|
||||||
|
|
||||||
int generate_quic_retry_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
int generate_quic_retry_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
||||||
const uint8_t *server_id,
|
const uint8_t *server_id, uint8_t km_id,
|
||||||
const uint8_t *key);
|
const uint8_t *key);
|
||||||
|
|
||||||
int generate_quic_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
int generate_quic_connection_id(ngtcp2_cid &cid, size_t cidlen,
|
||||||
const uint8_t *cid_prefix, const uint8_t *key);
|
const uint8_t *cid_prefix, uint8_t km_id,
|
||||||
|
const uint8_t *key);
|
||||||
|
|
||||||
int encrypt_quic_connection_id(uint8_t *dest, const uint8_t *src,
|
int encrypt_quic_connection_id(uint8_t *dest, const uint8_t *src,
|
||||||
const uint8_t *key);
|
const uint8_t *key);
|
||||||
|
@ -103,23 +107,31 @@ int generate_quic_stateless_reset_token(uint8_t *token, const ngtcp2_cid &cid,
|
||||||
const uint8_t *secret,
|
const uint8_t *secret,
|
||||||
size_t secretlen);
|
size_t secretlen);
|
||||||
|
|
||||||
int generate_quic_stateless_reset_secret(uint8_t *secret);
|
|
||||||
|
|
||||||
int generate_quic_token_secret(uint8_t *secret);
|
|
||||||
|
|
||||||
int generate_retry_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
int generate_retry_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
||||||
socklen_t salen, const ngtcp2_cid &retry_scid,
|
socklen_t salen, const ngtcp2_cid &retry_scid,
|
||||||
const ngtcp2_cid &odcid, const uint8_t *token_secret);
|
const ngtcp2_cid &odcid, const uint8_t *secret,
|
||||||
|
size_t secretlen);
|
||||||
|
|
||||||
int verify_retry_token(ngtcp2_cid &odcid, const uint8_t *token, size_t tokenlen,
|
int verify_retry_token(ngtcp2_cid &odcid, const uint8_t *token, size_t tokenlen,
|
||||||
const ngtcp2_cid &dcid, const sockaddr *sa,
|
const ngtcp2_cid &dcid, const sockaddr *sa,
|
||||||
socklen_t salen, const uint8_t *token_secret);
|
socklen_t salen, const uint8_t *secret,
|
||||||
|
size_t secretlen);
|
||||||
|
|
||||||
int generate_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
int generate_token(uint8_t *token, size_t &tokenlen, const sockaddr *sa,
|
||||||
size_t salen, const uint8_t *token_secret);
|
size_t salen, const uint8_t *secret, size_t secretlen);
|
||||||
|
|
||||||
int verify_token(const uint8_t *token, size_t tokenlen, const sockaddr *sa,
|
int verify_token(const uint8_t *token, size_t tokenlen, const sockaddr *sa,
|
||||||
socklen_t salen, const uint8_t *token_secret);
|
socklen_t salen, const uint8_t *secret, size_t secretlen);
|
||||||
|
|
||||||
|
int generate_quic_connection_id_encryption_key(uint8_t *key, size_t keylen,
|
||||||
|
const uint8_t *secret,
|
||||||
|
size_t secretlen,
|
||||||
|
const uint8_t *salt,
|
||||||
|
size_t saltlen);
|
||||||
|
|
||||||
|
const QUICKeyingMaterial *
|
||||||
|
select_quic_keying_material(const QUICKeyingMaterials &qkms,
|
||||||
|
const uint8_t *cid);
|
||||||
|
|
||||||
} // namespace shrpx
|
} // namespace shrpx
|
||||||
|
|
||||||
|
|
|
@ -126,14 +126,20 @@ int QUICConnectionHandler::handle_packet(const UpstreamAddr *faddr,
|
||||||
if (it == std::end(connections_)) {
|
if (it == std::end(connections_)) {
|
||||||
std::array<uint8_t, SHRPX_QUIC_DECRYPTED_DCIDLEN> decrypted_dcid;
|
std::array<uint8_t, SHRPX_QUIC_DECRYPTED_DCIDLEN> decrypted_dcid;
|
||||||
|
|
||||||
|
auto &qkms = conn_handler->get_quic_keying_materials();
|
||||||
|
const QUICKeyingMaterial *qkm = nullptr;
|
||||||
|
|
||||||
if (dcidlen == SHRPX_QUIC_SCIDLEN) {
|
if (dcidlen == SHRPX_QUIC_SCIDLEN) {
|
||||||
if (decrypt_quic_connection_id(
|
qkm = select_quic_keying_material(*qkms.get(), dcid);
|
||||||
decrypted_dcid.data(), dcid + SHRPX_QUIC_CID_PREFIX_OFFSET,
|
|
||||||
quicconf.upstream.cid_encryption_key.data()) != 0) {
|
if (decrypt_quic_connection_id(decrypted_dcid.data(),
|
||||||
|
dcid + SHRPX_QUIC_CID_PREFIX_OFFSET,
|
||||||
|
qkm->cid_encryption_key.data()) != 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!std::equal(std::begin(decrypted_dcid),
|
if (qkm != &qkms->keying_materials.front() ||
|
||||||
|
!std::equal(std::begin(decrypted_dcid),
|
||||||
std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN,
|
std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN,
|
||||||
worker_->get_cid_prefix())) {
|
worker_->get_cid_prefix())) {
|
||||||
auto quic_lwp =
|
auto quic_lwp =
|
||||||
|
@ -170,15 +176,26 @@ int QUICConnectionHandler::handle_packet(const UpstreamAddr *faddr,
|
||||||
|
|
||||||
switch (ngtcp2_accept(&hd, data, datalen)) {
|
switch (ngtcp2_accept(&hd, data, datalen)) {
|
||||||
case 0: {
|
case 0: {
|
||||||
// If we get Initial and it has the CID prefix of this worker, it
|
// If we get Initial and it has the CID prefix of this worker,
|
||||||
// is likely that client is intentionally use the our prefix.
|
// it is likely that client is intentionally use the prefix.
|
||||||
// Just drop it.
|
// Just drop it.
|
||||||
if (dcidlen == SHRPX_QUIC_SCIDLEN &&
|
if (dcidlen == SHRPX_QUIC_SCIDLEN) {
|
||||||
std::equal(std::begin(decrypted_dcid),
|
if (qkm != &qkms->keying_materials.front()) {
|
||||||
|
qkm = &qkms->keying_materials.front();
|
||||||
|
|
||||||
|
if (decrypt_quic_connection_id(decrypted_dcid.data(),
|
||||||
|
dcid + SHRPX_QUIC_CID_PREFIX_OFFSET,
|
||||||
|
qkm->cid_encryption_key.data()) != 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (std::equal(std::begin(decrypted_dcid),
|
||||||
std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN,
|
std::begin(decrypted_dcid) + SHRPX_QUIC_CID_PREFIXLEN,
|
||||||
worker_->get_cid_prefix())) {
|
worker_->get_cid_prefix())) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (worker_->get_graceful_shutdown()) {
|
if (worker_->get_graceful_shutdown()) {
|
||||||
send_connection_close(faddr, version, hd.dcid, hd.scid, remote_addr,
|
send_connection_close(faddr, version, hd.dcid, hd.scid, remote_addr,
|
||||||
|
@ -197,14 +214,18 @@ int QUICConnectionHandler::handle_packet(const UpstreamAddr *faddr,
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &quic_secret = worker_->get_quic_secret();
|
if (dcidlen != SHRPX_QUIC_SCIDLEN) {
|
||||||
auto &secret = quic_secret->token_secret;
|
// Initial packets with token must have DCID chosen by server.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto qkm = select_quic_keying_material(*qkms.get(), dcid);
|
||||||
|
|
||||||
switch (hd.token.base[0]) {
|
switch (hd.token.base[0]) {
|
||||||
case NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY:
|
case NGTCP2_CRYPTO_TOKEN_MAGIC_RETRY:
|
||||||
if (verify_retry_token(odcid, hd.token.base, hd.token.len, hd.dcid,
|
if (verify_retry_token(odcid, hd.token.base, hd.token.len, hd.dcid,
|
||||||
&remote_addr.su.sa, remote_addr.len,
|
&remote_addr.su.sa, remote_addr.len,
|
||||||
secret.data()) != 0) {
|
qkm->secret.data(), qkm->secret.size()) != 0) {
|
||||||
if (LOG_ENABLED(INFO)) {
|
if (LOG_ENABLED(INFO)) {
|
||||||
LOG(INFO) << "Failed to validate Retry token from remote="
|
LOG(INFO) << "Failed to validate Retry token from remote="
|
||||||
<< util::to_numeric_addr(&remote_addr);
|
<< util::to_numeric_addr(&remote_addr);
|
||||||
|
@ -229,7 +250,8 @@ int QUICConnectionHandler::handle_packet(const UpstreamAddr *faddr,
|
||||||
break;
|
break;
|
||||||
case NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR:
|
case NGTCP2_CRYPTO_TOKEN_MAGIC_REGULAR:
|
||||||
if (verify_token(hd.token.base, hd.token.len, &remote_addr.su.sa,
|
if (verify_token(hd.token.base, hd.token.len, &remote_addr.su.sa,
|
||||||
remote_addr.len, secret.data()) != 0) {
|
remote_addr.len, qkm->secret.data(),
|
||||||
|
qkm->secret.size()) != 0) {
|
||||||
if (LOG_ENABLED(INFO)) {
|
if (LOG_ENABLED(INFO)) {
|
||||||
LOG(INFO) << "Failed to validate token from remote="
|
LOG(INFO) << "Failed to validate token from remote="
|
||||||
<< util::to_numeric_addr(&remote_addr);
|
<< util::to_numeric_addr(&remote_addr);
|
||||||
|
@ -422,11 +444,15 @@ int QUICConnectionHandler::send_retry(
|
||||||
auto config = get_config();
|
auto config = get_config();
|
||||||
auto &quicconf = config->quic;
|
auto &quicconf = config->quic;
|
||||||
|
|
||||||
|
auto conn_handler = worker_->get_connection_handler();
|
||||||
|
auto &qkms = conn_handler->get_quic_keying_materials();
|
||||||
|
auto &qkm = qkms->keying_materials.front();
|
||||||
|
|
||||||
ngtcp2_cid retry_scid;
|
ngtcp2_cid retry_scid;
|
||||||
|
|
||||||
if (generate_quic_retry_connection_id(
|
if (generate_quic_retry_connection_id(
|
||||||
retry_scid, SHRPX_QUIC_SCIDLEN, quicconf.upstream.server_id.data(),
|
retry_scid, SHRPX_QUIC_SCIDLEN, quicconf.upstream.server_id.data(),
|
||||||
quicconf.upstream.cid_encryption_key.data()) != 0) {
|
qkm.id, qkm.cid_encryption_key.data()) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,12 +463,9 @@ int QUICConnectionHandler::send_retry(
|
||||||
ngtcp2_cid_init(&idcid, ini_dcid, ini_dcidlen);
|
ngtcp2_cid_init(&idcid, ini_dcid, ini_dcidlen);
|
||||||
ngtcp2_cid_init(&iscid, ini_scid, ini_scidlen);
|
ngtcp2_cid_init(&iscid, ini_scid, ini_scidlen);
|
||||||
|
|
||||||
auto &quic_secret = worker_->get_quic_secret();
|
|
||||||
auto &secret = quic_secret->token_secret;
|
|
||||||
|
|
||||||
if (generate_retry_token(token.data(), tokenlen, &remote_addr.su.sa,
|
if (generate_retry_token(token.data(), tokenlen, &remote_addr.su.sa,
|
||||||
remote_addr.len, retry_scid, idcid,
|
remote_addr.len, retry_scid, idcid,
|
||||||
secret.data()) != 0) {
|
qkm.secret.data(), qkm.secret.size()) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -539,11 +562,12 @@ int QUICConnectionHandler::send_stateless_reset(const UpstreamAddr *faddr,
|
||||||
|
|
||||||
ngtcp2_cid_init(&cid, dcid, dcidlen);
|
ngtcp2_cid_init(&cid, dcid, dcidlen);
|
||||||
|
|
||||||
auto &quic_secret = worker_->get_quic_secret();
|
auto conn_handler = worker_->get_connection_handler();
|
||||||
auto &secret = quic_secret->stateless_reset_secret;
|
auto &qkms = conn_handler->get_quic_keying_materials();
|
||||||
|
auto &qkm = qkms->keying_materials.front();
|
||||||
|
|
||||||
rv = generate_quic_stateless_reset_token(token.data(), cid, secret.data(),
|
rv = generate_quic_stateless_reset_token(token.data(), cid, qkm.secret.data(),
|
||||||
secret.size());
|
qkm.secret.size());
|
||||||
if (rv != 0) {
|
if (rv != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -933,14 +933,14 @@ int Worker::create_quic_server_socket(UpstreamAddr &faddr) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto &quicconf = config->quic;
|
|
||||||
|
|
||||||
constexpr uint32_t key_high_idx = 1;
|
constexpr uint32_t key_high_idx = 1;
|
||||||
constexpr uint32_t key_low_idx = 2;
|
constexpr uint32_t key_low_idx = 2;
|
||||||
|
|
||||||
|
auto &qkms = conn_handler_->get_quic_keying_materials();
|
||||||
|
auto &qkm = qkms->keying_materials.front();
|
||||||
|
|
||||||
if (bpf_map_update_elem(bpf_map__fd(sk_info), &key_high_idx,
|
if (bpf_map_update_elem(bpf_map__fd(sk_info), &key_high_idx,
|
||||||
quicconf.upstream.cid_encryption_key.data(),
|
qkm.cid_encryption_key.data(), BPF_ANY) != 0) {
|
||||||
BPF_ANY) != 0) {
|
|
||||||
LOG(FATAL) << "Failed to update key_high_idx sk_info: "
|
LOG(FATAL) << "Failed to update key_high_idx sk_info: "
|
||||||
<< xsi_strerror(errno, errbuf.data(), errbuf.size());
|
<< xsi_strerror(errno, errbuf.data(), errbuf.size());
|
||||||
close(fd);
|
close(fd);
|
||||||
|
@ -948,7 +948,7 @@ int Worker::create_quic_server_socket(UpstreamAddr &faddr) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bpf_map_update_elem(bpf_map__fd(sk_info), &key_low_idx,
|
if (bpf_map_update_elem(bpf_map__fd(sk_info), &key_low_idx,
|
||||||
quicconf.upstream.cid_encryption_key.data() + 8,
|
qkm.cid_encryption_key.data() + 8,
|
||||||
BPF_ANY) != 0) {
|
BPF_ANY) != 0) {
|
||||||
LOG(FATAL) << "Failed to update key_low_idx sk_info: "
|
LOG(FATAL) << "Failed to update key_low_idx sk_info: "
|
||||||
<< xsi_strerror(errno, errbuf.data(), errbuf.size());
|
<< xsi_strerror(errno, errbuf.data(), errbuf.size());
|
||||||
|
@ -1010,14 +1010,6 @@ int Worker::create_quic_server_socket(UpstreamAddr &faddr) {
|
||||||
|
|
||||||
const uint8_t *Worker::get_cid_prefix() const { return cid_prefix_.data(); }
|
const uint8_t *Worker::get_cid_prefix() const { return cid_prefix_.data(); }
|
||||||
|
|
||||||
void Worker::set_quic_secret(const std::shared_ptr<QUICSecret> &secret) {
|
|
||||||
quic_secret_ = secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::shared_ptr<QUICSecret> &Worker::get_quic_secret() const {
|
|
||||||
return quic_secret_;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UpstreamAddr *Worker::find_quic_upstream_addr(const Address &local_addr) {
|
const UpstreamAddr *Worker::find_quic_upstream_addr(const Address &local_addr) {
|
||||||
std::array<char, NI_MAXHOST> host;
|
std::array<char, NI_MAXHOST> host;
|
||||||
|
|
||||||
|
|
|
@ -370,10 +370,6 @@ public:
|
||||||
|
|
||||||
const uint8_t *get_cid_prefix() const;
|
const uint8_t *get_cid_prefix() const;
|
||||||
|
|
||||||
void set_quic_secret(const std::shared_ptr<QUICSecret> &secret);
|
|
||||||
|
|
||||||
const std::shared_ptr<QUICSecret> &get_quic_secret() const;
|
|
||||||
|
|
||||||
# ifdef HAVE_LIBBPF
|
# ifdef HAVE_LIBBPF
|
||||||
bool should_attach_bpf() const;
|
bool should_attach_bpf() const;
|
||||||
|
|
||||||
|
@ -412,7 +408,6 @@ private:
|
||||||
std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN> cid_prefix_;
|
std::array<uint8_t, SHRPX_QUIC_CID_PREFIXLEN> cid_prefix_;
|
||||||
std::vector<UpstreamAddr> quic_upstream_addrs_;
|
std::vector<UpstreamAddr> quic_upstream_addrs_;
|
||||||
std::vector<std::unique_ptr<QUICListener>> quic_listeners_;
|
std::vector<std::unique_ptr<QUICListener>> quic_listeners_;
|
||||||
std::shared_ptr<QUICSecret> quic_secret_;
|
|
||||||
#endif // ENABLE_HTTP3
|
#endif // ENABLE_HTTP3
|
||||||
|
|
||||||
std::shared_ptr<DownstreamConfig> downstreamconf_;
|
std::shared_ptr<DownstreamConfig> downstreamconf_;
|
||||||
|
|
|
@ -519,10 +519,51 @@ int worker_process_event_loop(WorkerProcessConfig *wpconf) {
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef ENABLE_HTTP3
|
#ifdef ENABLE_HTTP3
|
||||||
if (conn_handler->create_quic_secret() != 0) {
|
auto &quicconf = config->quic;
|
||||||
|
|
||||||
|
std::shared_ptr<QUICKeyingMaterials> qkms;
|
||||||
|
|
||||||
|
if (!quicconf.upstream.secret_file.empty()) {
|
||||||
|
qkms = read_quic_secret_file(quicconf.upstream.secret_file);
|
||||||
|
if (!qkms) {
|
||||||
|
LOG(WARN) << "Use QUIC keying materials generated internally";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!qkms) {
|
||||||
|
qkms = std::make_shared<QUICKeyingMaterials>();
|
||||||
|
qkms->keying_materials.resize(1);
|
||||||
|
|
||||||
|
auto &qkm = qkms->keying_materials.front();
|
||||||
|
|
||||||
|
if (RAND_bytes(qkm.reserved.data(), qkm.reserved.size()) != 1) {
|
||||||
|
LOG(ERROR) << "Failed to generate QUIC secret reserved data";
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (RAND_bytes(qkm.secret.data(), qkm.secret.size()) != 1) {
|
||||||
|
LOG(ERROR) << "Failed to generate QUIC secret";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RAND_bytes(qkm.salt.data(), qkm.salt.size()) != 1) {
|
||||||
|
LOG(ERROR) << "Failed to generate QUIC salt";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto &qkm : qkms->keying_materials) {
|
||||||
|
if (generate_quic_connection_id_encryption_key(
|
||||||
|
qkm.cid_encryption_key.data(), qkm.cid_encryption_key.size(),
|
||||||
|
qkm.secret.data(), qkm.secret.size(), qkm.salt.data(),
|
||||||
|
qkm.salt.size()) != 0) {
|
||||||
|
LOG(ERROR) << "Failed to generate QUIC Connection ID encryption key";
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conn_handler->set_quic_keying_materials(std::move(qkms));
|
||||||
|
|
||||||
conn_handler->set_cid_prefixes(wpconf->cid_prefixes);
|
conn_handler->set_cid_prefixes(wpconf->cid_prefixes);
|
||||||
conn_handler->set_quic_lingering_worker_processes(
|
conn_handler->set_quic_lingering_worker_processes(
|
||||||
wpconf->quic_lingering_worker_processes);
|
wpconf->quic_lingering_worker_processes);
|
||||||
|
|
Loading…
Reference in New Issue