resolvectl: use varlink instead of dbus for ResolveRecord
This commit is contained in:
+67
-103
@@ -279,7 +279,7 @@ static void print_ifindex_comment(int printed_so_far, int ifindex) {
|
||||
ansi_grey(), ifname, ansi_normal());
|
||||
}
|
||||
|
||||
static int varlink_log_resolve_error(const char *name, const char *error_id, sd_json_variant *reply) {
|
||||
static int varlink_log_resolve_error(const char *name, const char *error_id, sd_json_variant *reply, bool warn_missing) {
|
||||
int r;
|
||||
|
||||
assert(name);
|
||||
@@ -342,9 +342,13 @@ static int varlink_log_resolve_error(const char *name, const char *error_id, sd_
|
||||
return log_error_errno(ret, "%s: resolve call failed: DNSSEC validation failed: %s%s", name, error.result, strempty(msg_extended));
|
||||
|
||||
if (error.rcode != _DNS_RCODE_INVALID) {
|
||||
if (error.rcode == DNS_RCODE_NXDOMAIN)
|
||||
if (error.rcode == DNS_RCODE_NXDOMAIN) {
|
||||
if (!warn_missing)
|
||||
return -ENXIO;
|
||||
|
||||
return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "%s: resolve call failed: Name '%s' not found%s%s",
|
||||
name, error.query_string ?: name, error.ede_rcode >= 0 ? ":" : "", strempty(msg_extended));
|
||||
}
|
||||
|
||||
return log_error_errno(ret, "%s: resolve call failed: Could not resolve '%s', server or network returned error: %s%s",
|
||||
name, error.query_string ?: name, FORMAT_DNS_RCODE(error.rcode), strempty(msg_extended));
|
||||
@@ -403,7 +407,7 @@ static int resolve_host(const char *name) {
|
||||
ts = now(CLOCK_MONOTONIC) - ts;
|
||||
|
||||
if (!isempty(error_id))
|
||||
return varlink_log_resolve_error(name, error_id, v);
|
||||
return varlink_log_resolve_error(name, error_id, v, /* warn_missing = */ true);
|
||||
|
||||
_cleanup_(resolve_hostname_reply_done) ResolveHostnameReply reply = {};
|
||||
r = dispatch_resolve_hostname_reply(/* name = */ NULL, v, SD_JSON_LOG, &reply);
|
||||
@@ -489,7 +493,7 @@ static int resolve_address(int family, const union in_addr_union *address, int i
|
||||
ts = now(CLOCK_MONOTONIC) - ts;
|
||||
|
||||
if (!isempty(error_id))
|
||||
return varlink_log_resolve_error(pretty, error_id, v);
|
||||
return varlink_log_resolve_error(pretty, error_id, v, /* warn_missing = */ true);
|
||||
|
||||
_cleanup_(resolve_address_reply_done) ResolveAddressReply reply = {};
|
||||
r = dispatch_resolve_address_reply(/* name = */ NULL, v, SD_JSON_LOG, &reply);
|
||||
@@ -520,15 +524,10 @@ static int resolve_address(int family, const union in_addr_union *address, int i
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int output_rr_packet(const void *d, size_t l, int ifindex) {
|
||||
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
|
||||
static int output_rr_packet(DnsResourceRecord *rr, int ifindex) {
|
||||
int r;
|
||||
|
||||
assert(d || l == 0);
|
||||
|
||||
r = dns_resource_record_new_from_raw(&rr, d, l);
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to parse RR: %m");
|
||||
assert(rr);
|
||||
|
||||
if (sd_json_format_enabled(arg_json_format_flags)) {
|
||||
_cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
|
||||
@@ -611,14 +610,7 @@ static bool single_label_nonsynthetic(const char *name) {
|
||||
return !streq(name, first_label);
|
||||
}
|
||||
|
||||
static int resolve_record(sd_bus *bus, const char *name, uint16_t class, uint16_t type, bool warn_missing) {
|
||||
_cleanup_(sd_bus_message_unrefp) sd_bus_message *req = NULL, *reply = NULL;
|
||||
_cleanup_(sd_bus_error_free) sd_bus_error error = SD_BUS_ERROR_NULL;
|
||||
_cleanup_free_ char *idnafied = NULL;
|
||||
bool needs_authentication = false;
|
||||
unsigned n = 0;
|
||||
uint64_t flags;
|
||||
usec_t ts;
|
||||
static int resolve_record(const char *name, uint16_t class, uint16_t type, bool warn_missing) {
|
||||
int r;
|
||||
|
||||
assert(name);
|
||||
@@ -629,6 +621,7 @@ static int resolve_record(sd_bus *bus, const char *name, uint16_t class, uint16_
|
||||
log_notice("(Note that search domains are not appended when --type= is specified. "
|
||||
"Please specify fully qualified domain names, or remove --type= switch from invocation in order to request regular hostname resolution.)");
|
||||
|
||||
_cleanup_free_ char *idnafied = NULL;
|
||||
r = idna_candidate(name, &idnafied);
|
||||
if (r < 0)
|
||||
return r;
|
||||
@@ -637,85 +630,76 @@ static int resolve_record(sd_bus *bus, const char *name, uint16_t class, uint16_
|
||||
"Please specify translated domain names — i.e. '%s' — when resolving raw records, or remove --type= switch from invocation in order to request regular hostname resolution.",
|
||||
idnafied);
|
||||
|
||||
r = bus_message_new_method_call(bus, &req, bus_resolve_mgr, "ResolveRecord");
|
||||
_cleanup_(sd_varlink_unrefp) sd_varlink *vl = NULL;
|
||||
r = varlink_connect_with_query_timeout(&vl);
|
||||
if (r < 0)
|
||||
return bus_log_create_error(r);
|
||||
|
||||
r = sd_bus_message_append(req, "isqqt", arg_ifindex, name, class, type, arg_flags);
|
||||
if (r < 0)
|
||||
return bus_log_create_error(r);
|
||||
|
||||
ts = now(CLOCK_MONOTONIC);
|
||||
|
||||
r = sd_bus_call(bus, req, SD_RESOLVED_QUERY_TIMEOUT_USEC, &error, &reply);
|
||||
if (r < 0) {
|
||||
if (warn_missing || r != -ENXIO)
|
||||
log_error("%s: resolve call failed: %s", name, bus_error_message(&error, r));
|
||||
return r;
|
||||
}
|
||||
|
||||
usec_t ts = now(CLOCK_MONOTONIC);
|
||||
|
||||
const char *error_id = NULL;
|
||||
sd_json_variant *v = NULL;
|
||||
r = sd_varlink_callbo(
|
||||
vl,
|
||||
"io.systemd.Resolve.ResolveRecord",
|
||||
&v,
|
||||
&error_id,
|
||||
SD_JSON_BUILD_PAIR_STRING("name", name),
|
||||
SD_JSON_BUILD_PAIR_UNSIGNED("type", type),
|
||||
JSON_BUILD_PAIR_CONDITION_UNSIGNED(arg_ifindex > 0, "ifindex", arg_ifindex),
|
||||
JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("class", class),
|
||||
JSON_BUILD_PAIR_UNSIGNED_NON_ZERO("flags", arg_flags));
|
||||
if (r < 0)
|
||||
return log_error_errno(r, "Failed to issue varlink call: %m");
|
||||
|
||||
ts = now(CLOCK_MONOTONIC) - ts;
|
||||
|
||||
r = sd_bus_message_enter_container(reply, 'a', "(iqqay)");
|
||||
if (!isempty(error_id)) {
|
||||
if (streq(error_id, "io.systemd.Resolve.ResourceRecordTypeInvalidForQuery"))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified resource record type %" PRIu16 " may not be used in a query", type);
|
||||
|
||||
if (streq(error_id, "io.systemd.Resolve.ResourceRecordTypeObsolete"))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Specified DNS resource record type %" PRIu16 " is obsolete", type);
|
||||
|
||||
return varlink_log_resolve_error(name, error_id, v, warn_missing);
|
||||
}
|
||||
|
||||
_cleanup_(resolve_record_reply_done) ResolveRecordReply reply = {};
|
||||
r = dispatch_resolve_record_reply(/* name = */ NULL, v, SD_JSON_LOG, &reply);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
return r;
|
||||
|
||||
while ((r = sd_bus_message_enter_container(reply, 'r', "iqqay")) > 0) {
|
||||
uint16_t c, t;
|
||||
int ifindex;
|
||||
const void *d;
|
||||
size_t l;
|
||||
|
||||
assert_cc(sizeof(int) == sizeof(int32_t));
|
||||
|
||||
r = sd_bus_message_read(reply, "iqq", &ifindex, &c, &t);
|
||||
bool needs_authentication = false;
|
||||
FOREACH_ARRAY(record, reply.records, reply.n_records) {
|
||||
_cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
|
||||
r = dns_resource_record_new_from_raw(&rr, record->raw.iov_base, record->raw.iov_len);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
|
||||
r = sd_bus_message_read_array(reply, 'y', &d, &l);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
|
||||
r = sd_bus_message_exit_container(reply);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
return log_error_errno(r, "Failed to parse RR: %m");
|
||||
|
||||
if (arg_raw == RAW_PACKET) {
|
||||
uint64_t u64 = htole64(l);
|
||||
uint64_t u64 = htole64(record->raw.iov_len);
|
||||
|
||||
fwrite(&u64, sizeof(u64), 1, stdout);
|
||||
fwrite(d, 1, l, stdout);
|
||||
fwrite(record->raw.iov_base, 1, record->raw.iov_len, stdout);
|
||||
} else {
|
||||
r = output_rr_packet(d, l, ifindex);
|
||||
r = output_rr_packet(rr, record->ifindex);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
if (dns_type_needs_authentication(t))
|
||||
if (dns_type_needs_authentication(rr->key->type))
|
||||
needs_authentication = true;
|
||||
|
||||
n++;
|
||||
}
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
|
||||
r = sd_bus_message_exit_container(reply);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
|
||||
r = sd_bus_message_read(reply, "t", &flags);
|
||||
if (r < 0)
|
||||
return bus_log_parse_error(r);
|
||||
|
||||
if (n == 0) {
|
||||
if (reply.n_records == 0) {
|
||||
if (warn_missing)
|
||||
log_error("%s: no records found", name);
|
||||
return -ESRCH;
|
||||
}
|
||||
|
||||
print_source(flags, ts);
|
||||
print_source(reply.flags, ts);
|
||||
|
||||
if ((flags & SD_RESOLVED_AUTHENTICATED) == 0 && needs_authentication) {
|
||||
if ((reply.flags & SD_RESOLVED_AUTHENTICATED) == 0 && needs_authentication) {
|
||||
fflush(stdout);
|
||||
|
||||
fprintf(stderr, "\n%s"
|
||||
@@ -730,12 +714,11 @@ static int resolve_record(sd_bus *bus, const char *name, uint16_t class, uint16_
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int resolve_rfc4501(sd_bus *bus, const char *name) {
|
||||
static int resolve_rfc4501(const char *name) {
|
||||
uint16_t type = 0, class = 0;
|
||||
const char *p, *q, *n;
|
||||
int r;
|
||||
|
||||
assert(bus);
|
||||
assert(name);
|
||||
assert(startswith(name, "dns:"));
|
||||
|
||||
@@ -833,7 +816,7 @@ static int resolve_rfc4501(sd_bus *bus, const char *name) {
|
||||
if (type == 0)
|
||||
type = arg_type ?: DNS_TYPE_A;
|
||||
|
||||
return resolve_record(bus, n, class, type, true);
|
||||
return resolve_record(n, class, type, true);
|
||||
|
||||
invalid:
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EINVAL),
|
||||
@@ -843,21 +826,16 @@ invalid:
|
||||
VERB(verb_query, "query", "HOSTNAME|ADDRESS…", 2, VERB_ANY, 0,
|
||||
"Resolve domain names, IPv4 and IPv6 addresses");
|
||||
static int verb_query(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
||||
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
|
||||
int ret = 0, r;
|
||||
|
||||
r = acquire_bus(&bus);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (arg_type != 0)
|
||||
STRV_FOREACH(p, strv_skip(argv, 1))
|
||||
RET_GATHER(ret, resolve_record(bus, *p, arg_class, arg_type, true));
|
||||
RET_GATHER(ret, resolve_record(*p, arg_class, arg_type, true));
|
||||
|
||||
else
|
||||
STRV_FOREACH(p, strv_skip(argv, 1)) {
|
||||
if (startswith(*p, "dns:"))
|
||||
RET_GATHER(ret, resolve_rfc4501(bus, *p));
|
||||
RET_GATHER(ret, resolve_rfc4501(*p));
|
||||
else {
|
||||
int family, ifindex;
|
||||
union in_addr_union a;
|
||||
@@ -1074,10 +1052,9 @@ static int verb_service(int argc, char *argv[], uintptr_t _data, void *userdata)
|
||||
}
|
||||
|
||||
#if HAVE_OPENSSL
|
||||
static int resolve_openpgp(sd_bus *bus, const char *address) {
|
||||
static int resolve_openpgp(const char *address) {
|
||||
int r;
|
||||
|
||||
assert(bus);
|
||||
assert(address);
|
||||
|
||||
const char *domain = strrchr(address, '@');
|
||||
@@ -1108,7 +1085,6 @@ static int resolve_openpgp(sd_bus *bus, const char *address) {
|
||||
log_debug("Looking up \"%s\".", full);
|
||||
|
||||
r = resolve_record(
|
||||
bus,
|
||||
full,
|
||||
arg_class ?: DNS_CLASS_IN,
|
||||
arg_type ?: DNS_TYPE_OPENPGPKEY,
|
||||
@@ -1128,7 +1104,6 @@ static int resolve_openpgp(sd_bus *bus, const char *address) {
|
||||
log_debug("Looking up \"%s\".", full);
|
||||
|
||||
return resolve_record(
|
||||
bus,
|
||||
full,
|
||||
arg_class ?: DNS_CLASS_IN,
|
||||
arg_type ?: DNS_TYPE_OPENPGPKEY,
|
||||
@@ -1140,18 +1115,13 @@ VERB(verb_openpgp, "openpgp", "EMAIL@DOMAIN…", 2, VERB_ANY, 0,
|
||||
"Query OpenPGP public key");
|
||||
static int verb_openpgp(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
||||
#if HAVE_OPENSSL
|
||||
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
|
||||
int r, ret = 0;
|
||||
|
||||
r = acquire_bus(&bus);
|
||||
if (r < 0)
|
||||
return r;
|
||||
int ret = 0;
|
||||
|
||||
if (sd_json_format_enabled(arg_json_format_flags))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Use --json=pretty with --type= to acquire resource record information in JSON format.");
|
||||
|
||||
STRV_FOREACH(p, strv_skip(argv, 1))
|
||||
RET_GATHER(ret, resolve_openpgp(bus, *p));
|
||||
RET_GATHER(ret, resolve_openpgp(*p));
|
||||
|
||||
return ret;
|
||||
#else
|
||||
@@ -1159,13 +1129,12 @@ static int verb_openpgp(int argc, char *argv[], uintptr_t _data, void *userdata)
|
||||
#endif
|
||||
}
|
||||
|
||||
static int resolve_tlsa(sd_bus *bus, const char *family, const char *address) {
|
||||
static int resolve_tlsa(const char *family, const char *address) {
|
||||
const char *port;
|
||||
uint16_t port_num = 443;
|
||||
_cleanup_free_ char *full = NULL;
|
||||
int r;
|
||||
|
||||
assert(bus);
|
||||
assert(address);
|
||||
|
||||
port = strrchr(address, ':');
|
||||
@@ -1186,7 +1155,7 @@ static int resolve_tlsa(sd_bus *bus, const char *family, const char *address) {
|
||||
|
||||
log_debug("Looking up \"%s\".", full);
|
||||
|
||||
return resolve_record(bus, full,
|
||||
return resolve_record(full,
|
||||
arg_class ?: DNS_CLASS_IN,
|
||||
arg_type ?: DNS_TYPE_TLSA, true);
|
||||
}
|
||||
@@ -1198,17 +1167,12 @@ static bool service_family_is_valid(const char *s) {
|
||||
VERB(verb_tlsa, "tlsa", "DOMAIN[:PORT]…", 2, VERB_ANY, 0,
|
||||
"Query TLS public key");
|
||||
static int verb_tlsa(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
||||
_cleanup_(sd_bus_flush_close_unrefp) sd_bus *bus = NULL;
|
||||
const char *family = "tcp";
|
||||
char **args;
|
||||
int r, ret = 0;
|
||||
int ret = 0;
|
||||
|
||||
assert(argc >= 2);
|
||||
|
||||
r = acquire_bus(&bus);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (sd_json_format_enabled(arg_json_format_flags))
|
||||
return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "Use --json=pretty with --type= to acquire resource record information in JSON format.");
|
||||
|
||||
@@ -1219,7 +1183,7 @@ static int verb_tlsa(int argc, char *argv[], uintptr_t _data, void *userdata) {
|
||||
args = strv_skip(argv, 1);
|
||||
|
||||
STRV_FOREACH(p, args)
|
||||
RET_GATHER(ret, resolve_tlsa(bus, family, *p));
|
||||
RET_GATHER(ret, resolve_tlsa(family, *p));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "sd-json.h"
|
||||
|
||||
#include "dns-packet.h"
|
||||
#include "iovec-util.h"
|
||||
#include "json-util.h"
|
||||
#include "resolve-varlink-util.h"
|
||||
|
||||
@@ -175,3 +176,74 @@ int dispatch_resolve_address_reply(const char *name, sd_json_variant *variant, s
|
||||
*ret = TAKE_STRUCT(reply);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void resolved_record_done(ResolvedRecord *record) {
|
||||
if (!record)
|
||||
return;
|
||||
|
||||
iovec_done(&record->raw);
|
||||
}
|
||||
|
||||
static int dispatch_resolved_record(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
|
||||
static const sd_json_dispatch_field resolved_record_dispatch_table[] = {
|
||||
{ "ifindex", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_ifindex, offsetof(ResolvedRecord, ifindex), SD_JSON_RELAX },
|
||||
{ "raw", _SD_JSON_VARIANT_TYPE_INVALID, json_dispatch_unbase64_iovec, offsetof(ResolvedRecord, raw), SD_JSON_MANDATORY },
|
||||
{ "rr", _SD_JSON_VARIANT_TYPE_INVALID, NULL, 0, 0 },
|
||||
{},
|
||||
};
|
||||
ResolvedRecord *ret = ASSERT_PTR(userdata);
|
||||
int r;
|
||||
|
||||
_cleanup_(resolved_record_done) ResolvedRecord record = {};
|
||||
r = sd_json_dispatch(variant, resolved_record_dispatch_table, flags & ~SD_JSON_MANDATORY, &record);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret = TAKE_STRUCT(record);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int dispatch_resolved_record_array(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
|
||||
ResolveRecordReply *reply = ASSERT_PTR(userdata);
|
||||
int r;
|
||||
|
||||
sd_json_variant *v;
|
||||
JSON_VARIANT_ARRAY_FOREACH(v, variant) {
|
||||
if (!GREEDY_REALLOC0(reply->records, reply->n_records + 1))
|
||||
return json_log_oom(variant, flags);
|
||||
|
||||
r = dispatch_resolved_record(name, v, flags, &reply->records[reply->n_records++]);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void resolve_record_reply_done(ResolveRecordReply *reply) {
|
||||
if (!reply)
|
||||
return;
|
||||
|
||||
FOREACH_ARRAY(record, reply->records, reply->n_records)
|
||||
resolved_record_done(record);
|
||||
reply->records = mfree(reply->records);
|
||||
reply->n_records = 0;
|
||||
}
|
||||
|
||||
int dispatch_resolve_record_reply(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata) {
|
||||
static const sd_json_dispatch_field resolve_record_reply_dispatch_table[] = {
|
||||
{ "flags", _SD_JSON_VARIANT_TYPE_INVALID, sd_json_dispatch_uint64, offsetof(ResolveRecordReply, flags), SD_JSON_MANDATORY },
|
||||
{ "rrs", SD_JSON_VARIANT_ARRAY, dispatch_resolved_record_array, 0, SD_JSON_MANDATORY },
|
||||
{},
|
||||
};
|
||||
ResolveRecordReply *ret = ASSERT_PTR(userdata);
|
||||
int r;
|
||||
|
||||
_cleanup_(resolve_record_reply_done) ResolveRecordReply reply = {};
|
||||
r = sd_json_dispatch(variant, resolve_record_reply_dispatch_table, flags | SD_JSON_ALLOW_EXTENSIONS, &reply);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
*ret = TAKE_STRUCT(reply);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include <sys/uio.h>
|
||||
|
||||
#include "in-addr-util.h"
|
||||
#include "shared-forward.h"
|
||||
|
||||
@@ -47,3 +49,18 @@ typedef struct ResolveAddressReply {
|
||||
void resolve_address_reply_done(ResolveAddressReply *reply);
|
||||
|
||||
int dispatch_resolve_address_reply(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
|
||||
|
||||
typedef struct ResolvedRecord {
|
||||
int ifindex;
|
||||
struct iovec raw;
|
||||
} ResolvedRecord;
|
||||
|
||||
typedef struct ResolveRecordReply {
|
||||
uint64_t flags;
|
||||
ResolvedRecord *records;
|
||||
size_t n_records;
|
||||
} ResolveRecordReply;
|
||||
|
||||
void resolve_record_reply_done(ResolveRecordReply *reply);
|
||||
|
||||
int dispatch_resolve_record_reply(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata);
|
||||
|
||||
Reference in New Issue
Block a user