parse dig's output
This commit is contained in:
parent
fc7cf970a5
commit
7ad5bacbdc
|
@ -85,7 +85,6 @@ impl StoppedAuthoritativeNameServer {
|
|||
/// - one SOA record, with the primary name server set to the name server domain
|
||||
/// - one NS record, with the name server domain set as the only available name server for
|
||||
/// `zone`
|
||||
/// - one A record, that maps the name server domain to its IPv4 address
|
||||
/// - one NS + A record pair, for each referral in the `referrals` list
|
||||
/// - the A records in the `a_records` list
|
||||
pub fn start<'a>(
|
||||
|
@ -119,10 +118,6 @@ impl StoppedAuthoritativeNameServer {
|
|||
domain: zone.clone(),
|
||||
ns: nameserver.clone(),
|
||||
});
|
||||
zone_file.record(record::A {
|
||||
domain: nameserver,
|
||||
ipv4_addr: container.ipv4_addr(),
|
||||
});
|
||||
|
||||
for referral in referrals {
|
||||
zone_file.referral(referral)
|
||||
|
@ -153,45 +148,35 @@ fn nsd_conf(domain: &Domain) -> String {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
client::{RecordType, Recurse},
|
||||
Client,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tld_ns() -> Result<()> {
|
||||
let tld_ns = AuthoritativeNameServer::start(Domain("com.")?, &[], &[])?;
|
||||
fn simplest() -> Result<()> {
|
||||
let com_domain = Domain("com.")?;
|
||||
let tld_ns = AuthoritativeNameServer::start(com_domain.clone(), &[], &[])?;
|
||||
let ip_addr = tld_ns.ipv4_addr();
|
||||
|
||||
let client = Container::run()?;
|
||||
let output = client.output(&["dig", &format!("@{ip_addr}"), "SOA", "com."])?;
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(Recurse::No, ip_addr, RecordType::SOA, &com_domain)?;
|
||||
|
||||
assert!(output.status.success());
|
||||
eprintln!("{}", output.stdout);
|
||||
assert!(output.stdout.contains("status: NOERROR"));
|
||||
assert!(output.status.is_noerror());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_ns() -> Result<()> {
|
||||
let root_ns = AuthoritativeNameServer::start(Domain::ROOT, &[], &[])?;
|
||||
let ip_addr = root_ns.ipv4_addr();
|
||||
|
||||
let client = Container::run()?;
|
||||
let output = client.output(&["dig", &format!("@{ip_addr}"), "SOA", "."])?;
|
||||
|
||||
assert!(output.status.success());
|
||||
eprintln!("{}", output.stdout);
|
||||
assert!(output.stdout.contains("status: NOERROR"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn root_ns_with_referral() -> Result<()> {
|
||||
fn with_referral() -> Result<()> {
|
||||
let expected_ip_addr = Ipv4Addr::new(172, 17, 200, 1);
|
||||
let com_domain = Domain("com.")?;
|
||||
let root_ns = AuthoritativeNameServer::start(
|
||||
Domain::ROOT,
|
||||
&[Referral {
|
||||
domain: Domain("com.")?,
|
||||
domain: com_domain.clone(),
|
||||
ipv4_addr: expected_ip_addr,
|
||||
ns: Domain("primary.tld-server.com.")?,
|
||||
}],
|
||||
|
@ -199,12 +184,10 @@ mod tests {
|
|||
)?;
|
||||
let ip_addr = root_ns.ipv4_addr();
|
||||
|
||||
let client = Container::run()?;
|
||||
let output = client.output(&["dig", &format!("@{ip_addr}"), "NS", "com."])?;
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(Recurse::No, ip_addr, RecordType::NS, &com_domain)?;
|
||||
|
||||
assert!(output.status.success());
|
||||
eprintln!("{}", output.stdout);
|
||||
assert!(output.stdout.contains("status: NOERROR"));
|
||||
assert!(output.status.is_noerror());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
408
src/client.rs
Normal file
408
src/client.rs
Normal file
|
@ -0,0 +1,408 @@
|
|||
use core::array;
|
||||
use core::result::Result as CoreResult;
|
||||
use core::str::FromStr;
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use crate::container::Container;
|
||||
use crate::{Domain, Error, Result};
|
||||
|
||||
pub struct Client {
|
||||
inner: Container,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
inner: Container::run()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn dig(
|
||||
&self,
|
||||
recurse: Recurse,
|
||||
server: Ipv4Addr,
|
||||
record_type: RecordType,
|
||||
domain: &Domain<'_>,
|
||||
) -> Result<DigOutput> {
|
||||
let output = self.inner.stdout(&[
|
||||
"dig",
|
||||
recurse.as_str(),
|
||||
&format!("@{server}"),
|
||||
record_type.as_str(),
|
||||
domain.as_str(),
|
||||
])?;
|
||||
|
||||
output.parse()
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum RecordType {
|
||||
A,
|
||||
NS,
|
||||
SOA,
|
||||
}
|
||||
|
||||
impl RecordType {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
RecordType::A => "A",
|
||||
RecordType::SOA => "SOA",
|
||||
RecordType::NS => "NS",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Recurse {
|
||||
Yes,
|
||||
No,
|
||||
}
|
||||
|
||||
impl Recurse {
|
||||
fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Recurse::Yes => "+recurse",
|
||||
Recurse::No => "+norecurse",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DigOutput {
|
||||
pub flags: DigFlags,
|
||||
pub status: DigStatus,
|
||||
pub answer: Vec<Record>,
|
||||
// TODO(if needed) other sections
|
||||
}
|
||||
|
||||
impl FromStr for DigOutput {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
const FLAGS_PREFIX: &str = ";; flags: ";
|
||||
const STATUS_PREFIX: &str = ";; ->>HEADER<<- opcode: QUERY, status: ";
|
||||
const ANSWER_HEADER: &str = ";; ANSWER SECTION:";
|
||||
|
||||
fn not_found(prefix: &str) -> String {
|
||||
format!("`{prefix}` line was not found")
|
||||
}
|
||||
|
||||
fn more_than_once(prefix: &str) -> String {
|
||||
format!("`{prefix}` line was found more than once")
|
||||
}
|
||||
|
||||
fn missing(prefix: &str, delimiter: &str) -> String {
|
||||
format!("`{prefix}` line is missing a {delimiter}")
|
||||
}
|
||||
|
||||
let mut flags = None;
|
||||
let mut status = None;
|
||||
let mut answer = None;
|
||||
|
||||
let mut lines = input.lines();
|
||||
while let Some(line) = lines.next() {
|
||||
if let Some(unprefixed) = line.strip_prefix(FLAGS_PREFIX) {
|
||||
let (flags_text, _rest) = unprefixed
|
||||
.split_once(';')
|
||||
.ok_or_else(|| missing(FLAGS_PREFIX, "semicolon (;)"))?;
|
||||
|
||||
if flags.is_some() {
|
||||
return Err(more_than_once(FLAGS_PREFIX).into());
|
||||
}
|
||||
|
||||
flags = Some(flags_text.parse()?);
|
||||
} else if let Some(unprefixed) = line.strip_prefix(STATUS_PREFIX) {
|
||||
let (status_text, _rest) = unprefixed
|
||||
.split_once(',')
|
||||
.ok_or_else(|| missing(STATUS_PREFIX, "comma (,)"))?;
|
||||
|
||||
if status.is_some() {
|
||||
return Err(more_than_once(STATUS_PREFIX).into());
|
||||
}
|
||||
|
||||
status = Some(status_text.parse()?);
|
||||
} else if line.starts_with(ANSWER_HEADER) {
|
||||
if answer.is_some() {
|
||||
return Err(more_than_once(ANSWER_HEADER).into());
|
||||
}
|
||||
|
||||
let mut records = vec![];
|
||||
for line in lines.by_ref() {
|
||||
if line.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
records.push(line.parse()?);
|
||||
}
|
||||
|
||||
answer = Some(records);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
flags: flags.ok_or_else(|| not_found(FLAGS_PREFIX))?,
|
||||
status: status.ok_or_else(|| not_found(STATUS_PREFIX))?,
|
||||
answer: answer.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq)]
|
||||
pub struct DigFlags {
|
||||
pub qr: bool,
|
||||
pub recursion_desired: bool,
|
||||
pub recursion_available: bool,
|
||||
pub authoritative_answer: bool,
|
||||
}
|
||||
|
||||
impl FromStr for DigFlags {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> std::prelude::v1::Result<Self, Self::Err> {
|
||||
let mut qr = false;
|
||||
let mut recursion_desired = false;
|
||||
let mut recursion_available = false;
|
||||
let mut authoritative_answer = false;
|
||||
|
||||
for flag in input.split_whitespace() {
|
||||
match flag {
|
||||
"qr" => qr = true,
|
||||
"rd" => recursion_desired = true,
|
||||
"ra" => recursion_available = true,
|
||||
"aa" => authoritative_answer = true,
|
||||
_ => return Err(format!("unknown flag: {flag}").into()),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
qr,
|
||||
recursion_desired,
|
||||
recursion_available,
|
||||
authoritative_answer,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum DigStatus {
|
||||
NOERROR,
|
||||
NXDOMAIN,
|
||||
REFUSED,
|
||||
}
|
||||
|
||||
impl DigStatus {
|
||||
#[must_use]
|
||||
pub fn is_noerror(&self) -> bool {
|
||||
matches!(self, Self::NOERROR)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for DigStatus {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
let status = match input {
|
||||
"NXDOMAIN" => Self::NXDOMAIN,
|
||||
"NOERROR" => Self::NOERROR,
|
||||
"REFUSED" => Self::REFUSED,
|
||||
_ => return Err(format!("unknown status: {input}").into()),
|
||||
};
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum Record {
|
||||
A(A),
|
||||
SOA(SOA),
|
||||
}
|
||||
|
||||
impl Record {
|
||||
pub fn try_into_a(self) -> CoreResult<A, Self> {
|
||||
if let Self::A(v) = self {
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Record {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
let record_type = input
|
||||
.split_whitespace()
|
||||
.nth(3)
|
||||
.ok_or("record is missing the type column")?;
|
||||
|
||||
let record = match record_type {
|
||||
"A" => Record::A(input.parse()?),
|
||||
"NS" => todo!(),
|
||||
"SOA" => Record::SOA(input.parse()?),
|
||||
_ => return Err(format!("unknown record type: {record_type}").into()),
|
||||
};
|
||||
|
||||
Ok(record)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct A {
|
||||
pub domain: Domain<'static>,
|
||||
pub ttl: u32,
|
||||
pub ipv4_addr: Ipv4Addr,
|
||||
}
|
||||
|
||||
impl FromStr for A {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
let mut columns = input.split_whitespace();
|
||||
|
||||
let [Some(domain), Some(ttl), Some(class), Some(record_type), Some(ipv4_addr), None] =
|
||||
array::from_fn(|_| columns.next())
|
||||
else {
|
||||
return Err("expected 5 columns".into());
|
||||
};
|
||||
|
||||
if record_type != "A" {
|
||||
return Err(format!("tried to parse `{record_type}` record as an A record").into());
|
||||
}
|
||||
|
||||
if class != "IN" {
|
||||
return Err(format!("unknown class: {class}").into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
domain: domain.parse()?,
|
||||
ttl: ttl.parse()?,
|
||||
ipv4_addr: ipv4_addr.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug)]
|
||||
pub struct SOA {
|
||||
pub domain: Domain<'static>,
|
||||
pub ttl: u32,
|
||||
pub nameserver: Domain<'static>,
|
||||
pub admin: Domain<'static>,
|
||||
pub serial: u32,
|
||||
pub refresh: u32,
|
||||
pub retry: u32,
|
||||
pub expire: u32,
|
||||
pub minimum: u32,
|
||||
}
|
||||
|
||||
impl FromStr for SOA {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
let mut columns = input.split_whitespace();
|
||||
|
||||
let [Some(domain), Some(ttl), Some(class), Some(record_type), Some(nameserver), Some(admin), Some(serial), Some(refresh), Some(retry), Some(expire), Some(minimum), None] =
|
||||
array::from_fn(|_| columns.next())
|
||||
else {
|
||||
return Err("expected 11 columns".into());
|
||||
};
|
||||
|
||||
if record_type != "SOA" {
|
||||
return Err(format!("tried to parse `{record_type}` record as a SOA record").into());
|
||||
}
|
||||
|
||||
if class != "IN" {
|
||||
return Err(format!("unknown class: {class}").into());
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
domain: domain.parse()?,
|
||||
ttl: ttl.parse()?,
|
||||
nameserver: nameserver.parse()?,
|
||||
admin: admin.parse()?,
|
||||
serial: serial.parse()?,
|
||||
refresh: refresh.parse()?,
|
||||
retry: retry.parse()?,
|
||||
expire: expire.parse()?,
|
||||
minimum: minimum.parse()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn nxdomain() -> Result<()> {
|
||||
// $ dig nonexistent.domain.
|
||||
let input = "
|
||||
; <<>> DiG 9.18.18-0ubuntu0.22.04.1-Ubuntu <<>> nonexistent.domain.
|
||||
;; global options: +cmd
|
||||
;; Got answer:
|
||||
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 45583
|
||||
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
|
||||
|
||||
;; OPT PSEUDOSECTION:
|
||||
; EDNS: version: 0, flags:; udp: 1232
|
||||
;; QUESTION SECTION:
|
||||
;nonexistent.domain. IN A
|
||||
|
||||
;; Query time: 3 msec
|
||||
;; SERVER: 192.168.1.1#53(192.168.1.1) (UDP)
|
||||
;; WHEN: Tue Feb 06 15:00:12 UTC 2024
|
||||
;; MSG SIZE rcvd: 47
|
||||
";
|
||||
|
||||
let output: DigOutput = input.parse()?;
|
||||
|
||||
assert_eq!(DigStatus::NXDOMAIN, output.status);
|
||||
assert_eq!(
|
||||
DigFlags {
|
||||
qr: true,
|
||||
recursion_desired: true,
|
||||
recursion_available: true,
|
||||
..DigFlags::default()
|
||||
},
|
||||
output.flags
|
||||
);
|
||||
assert!(output.answer.is_empty());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_a_record() -> Result<()> {
|
||||
let input = "a.root-servers.net. 3600000 IN A 198.41.0.4";
|
||||
let a: A = input.parse()?;
|
||||
|
||||
assert_eq!("a.root-servers.net.", a.domain.as_str());
|
||||
assert_eq!(3600000, a.ttl);
|
||||
assert_eq!(Ipv4Addr::new(198, 41, 0, 4), a.ipv4_addr);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_parse_soa_record() -> Result<()> {
|
||||
let input = ". 15633 IN SOA a.root-servers.net. nstld.verisign-grs.com. 2024020501 1800 900 604800 86400";
|
||||
|
||||
let soa: SOA = input.parse()?;
|
||||
|
||||
assert_eq!(".", soa.domain.as_str());
|
||||
assert_eq!(15633, soa.ttl);
|
||||
assert_eq!("a.root-servers.net.", soa.nameserver.as_str());
|
||||
assert_eq!("nstld.verisign-grs.com.", soa.admin.as_str());
|
||||
assert_eq!(2024020501, soa.serial);
|
||||
assert_eq!(1800, soa.refresh);
|
||||
assert_eq!(900, soa.retry);
|
||||
assert_eq!(604800, soa.expire);
|
||||
assert_eq!(86400, soa.minimum);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -95,6 +95,18 @@ impl Container {
|
|||
command.output()?.try_into()
|
||||
}
|
||||
|
||||
/// Similar to `Self::output` but checks `command_and_args` ran successfully and only
|
||||
/// returns the stdout
|
||||
pub fn stdout(&self, command_and_args: &[&str]) -> Result<String> {
|
||||
let output = self.output(command_and_args)?;
|
||||
|
||||
if output.status.success() {
|
||||
Ok(output.stdout)
|
||||
} else {
|
||||
Err(format!("[{}] `{command_and_args:?}` failed", self.name).into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Similar to `std::process::Command::status` but runs `command_and_args` in the container
|
||||
pub fn status(&self, command_and_args: &[&str]) -> Result<ExitStatus> {
|
||||
let mut command = Command::new("docker");
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use core::fmt;
|
||||
use core::str::FromStr;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::Result;
|
||||
use crate::{Error, Result};
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Domain<'a> {
|
||||
inner: Cow<'a, str>,
|
||||
}
|
||||
|
@ -48,6 +49,20 @@ impl<'a> Domain<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromStr for Domain<'static> {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(input: &str) -> Result<Self> {
|
||||
Ok(Domain(input)?.into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Domain<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(self, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Domain<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.inner)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::sync::atomic::{self, AtomicUsize};
|
||||
|
||||
pub use crate::authoritative_name_server::AuthoritativeNameServer;
|
||||
pub use crate::client::Client;
|
||||
pub use crate::domain::Domain;
|
||||
pub use crate::recursive_resolver::RecursiveResolver;
|
||||
|
||||
|
@ -10,6 +11,7 @@ pub type Result<T> = core::result::Result<T, Error>;
|
|||
const CHMOD_RW_EVERYONE: &str = "666";
|
||||
|
||||
mod authoritative_name_server;
|
||||
mod client;
|
||||
pub mod container;
|
||||
mod domain;
|
||||
pub mod record;
|
||||
|
|
|
@ -41,8 +41,9 @@ impl Drop for RecursiveResolver {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
client::{RecordType, Recurse},
|
||||
record::{self, Referral},
|
||||
AuthoritativeNameServer, Domain,
|
||||
AuthoritativeNameServer, Client, Domain,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
@ -51,6 +52,7 @@ mod tests {
|
|||
fn can_resolve() -> Result<()> {
|
||||
let expected_ipv4_addr = Ipv4Addr::new(1, 2, 3, 4);
|
||||
let needle = Domain("example.nameservers.com.")?;
|
||||
|
||||
let root_ns = AuthoritativeNameServer::reserve()?;
|
||||
let com_ns = AuthoritativeNameServer::reserve()?;
|
||||
|
||||
|
@ -105,28 +107,16 @@ mod tests {
|
|||
let resolver = RecursiveResolver::start(roots)?;
|
||||
let resolver_ip_addr = resolver.ipv4_addr();
|
||||
|
||||
let container = Container::run()?;
|
||||
let output = container.output(&[
|
||||
"dig",
|
||||
&format!("@{}", resolver_ip_addr),
|
||||
&needle.to_string(),
|
||||
])?;
|
||||
let client = Client::new()?;
|
||||
let output = client.dig(Recurse::Yes, resolver_ip_addr, RecordType::A, &needle)?;
|
||||
|
||||
eprintln!("{}", output.stdout);
|
||||
assert!(output.status.is_noerror());
|
||||
|
||||
assert!(output.status.success());
|
||||
assert!(output.stdout.contains("status: NOERROR"));
|
||||
let [answer] = output.answer.try_into().unwrap();
|
||||
let a = answer.try_into_a().unwrap();
|
||||
|
||||
let mut found = false;
|
||||
let needle = needle.to_string();
|
||||
for line in output.stdout.lines() {
|
||||
if line.starts_with(&needle) {
|
||||
found = true;
|
||||
assert!(line.ends_with(&expected_ipv4_addr.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
assert!(found);
|
||||
assert_eq!(needle, a.domain);
|
||||
assert_eq!(expected_ipv4_addr, a.ipv4_addr);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user