wip
This commit is contained in:
parent
2ed63e526a
commit
84fd5222a1
5
.gitignore
vendored
5
.gitignore
vendored
@ -16,7 +16,4 @@ Cargo.lock
|
|||||||
# already existing elements were commented out
|
# already existing elements were commented out
|
||||||
|
|
||||||
/target
|
/target
|
||||||
#Cargo.lock
|
/libs/*/target
|
||||||
|
|
||||||
# Built packages
|
|
||||||
/tars
|
|
@ -15,6 +15,7 @@ categories = []
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
[dependencies]
|
[dependencies]
|
||||||
protomask-tun = { path = "protomask-tun", version = "0.1.0" }
|
protomask-tun = { path = "protomask-tun", version = "0.1.0" }
|
||||||
|
addrmap = { path = "libs/addrmap", version = "0.1.0" }
|
||||||
tokio = { version = "1.29.1", features = [
|
tokio = { version = "1.29.1", features = [
|
||||||
"macros",
|
"macros",
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct IpBimap {
|
||||||
|
v4_to_v6: FxHashMap<u32, u128>,
|
||||||
|
v6_to_v4: FxHashMap<u128, u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IpBimap {
|
||||||
|
/// Construct a new `IpBimap`
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
v4_to_v6: FxHashMap::default(),
|
||||||
|
v6_to_v4: FxHashMap::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert a new mapping
|
||||||
|
pub fn insert(&mut self, v4: u32, v6: u128) {
|
||||||
|
self.v4_to_v6.insert(v4, v6);
|
||||||
|
self.v6_to_v4.insert(v6, v4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a mapping
|
||||||
|
pub fn remove(&mut self, v4: u32, v6: u128) {
|
||||||
|
self.v4_to_v6.remove(&v4);
|
||||||
|
self.v6_to_v4.remove(&v6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the IPv6 address for a given IPv4 address
|
||||||
|
pub fn get_v6(&self, v4: u32) -> Option<u128> {
|
||||||
|
self.v4_to_v6.get(&v4).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the IPv4 address for a given IPv6 address
|
||||||
|
pub fn get_v4(&self, v6: u128) -> Option<u32> {
|
||||||
|
self.v6_to_v4.get(&v6).copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the map contains a given IPv4 address
|
||||||
|
pub fn contains_v4(&self, v4: u32) -> bool {
|
||||||
|
self.v4_to_v6.contains_key(&v4)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the map contains a given IPv6 address
|
||||||
|
pub fn contains_v6(&self, v6: u128) -> bool {
|
||||||
|
self.v6_to_v4.contains_key(&v6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for IpBimap {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
16
libs/packetpeeper/Cargo.toml
Normal file
16
libs/packetpeeper/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "packetpeeper"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Evan Pratten <ewpratten@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
description = "Tooks for acting on a raw byte slice as a packet"
|
||||||
|
readme = "README.md"
|
||||||
|
homepage = "https://github.com/ewpratten/protomask/libs/packetpeeper"
|
||||||
|
documentation = "https://docs.rs/packetpeeper"
|
||||||
|
repository = "https://github.com/ewpratten/protomask"
|
||||||
|
license = "GPL-3.0"
|
||||||
|
keywords = []
|
||||||
|
categories = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
|
2
libs/packetpeeper/src/lib.rs
Normal file
2
libs/packetpeeper/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod types;
|
||||||
|
pub mod ops;
|
16
libs/packetpeeper/src/ops/ip.rs
Normal file
16
libs/packetpeeper/src/ops/ip.rs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
use crate::types::IpPacket;
|
||||||
|
|
||||||
|
pub fn get_protocol<'a>(packet: IpPacket<'a>) -> u8 {
|
||||||
|
packet[0] >> 4
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_proto_version_ipv4(){
|
||||||
|
let packet = [0x40u8, 0x00u8, 0x00u8, 0x00u8];
|
||||||
|
assert_eq!(get_protocol(&packet), 4);
|
||||||
|
}
|
||||||
|
}
|
1
libs/packetpeeper/src/ops/mod.rs
Normal file
1
libs/packetpeeper/src/ops/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod ip;
|
6
libs/packetpeeper/src/types.rs
Normal file
6
libs/packetpeeper/src/types.rs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
pub type IpPacket<'a> = &'a [u8];
|
||||||
|
pub type IcmpPacket<'a> = &'a [u8];
|
||||||
|
pub type Icmpv6Packet<'a> = &'a [u8];
|
||||||
|
pub type TcpPacket<'a> = &'a [u8];
|
||||||
|
pub type UdpPacket<'a> = &'a [u8];
|
@ -40,214 +40,214 @@
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
||||||
// // use std::{
|
use std::{
|
||||||
// // collections::HashMap,
|
collections::HashMap,
|
||||||
// // net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||||
// // time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
// // };
|
};
|
||||||
|
|
||||||
// // use bimap::BiHashMap;
|
use bimap::BiHashMap;
|
||||||
// // use ipnet::Ipv4Net;
|
use ipnet::Ipv4Net;
|
||||||
|
|
||||||
// // use crate::utils::metrics::{IPV4_POOL_SIZE, IPV4_POOL_RESERVED};
|
use crate::utils::metrics::{IPV4_POOL_SIZE, IPV4_POOL_RESERVED};
|
||||||
|
|
||||||
|
|
||||||
// // /// Possible errors thrown in the address reservation process
|
/// Possible errors thrown in the address reservation process
|
||||||
// // #[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
// // pub enum TableError {
|
pub enum TableError {
|
||||||
// // #[error("Address already reserved: {0}")]
|
#[error("Address already reserved: {0}")]
|
||||||
// // AddressAlreadyReserved(IpAddr),
|
AddressAlreadyReserved(IpAddr),
|
||||||
// // #[error("IPv4 address has no IPv6 mapping: {0}")]
|
#[error("IPv4 address has no IPv6 mapping: {0}")]
|
||||||
// // NoIpv6Mapping(Ipv4Addr),
|
NoIpv6Mapping(Ipv4Addr),
|
||||||
// // #[error("Address pool depleted")]
|
#[error("Address pool depleted")]
|
||||||
// // AddressPoolDepleted,
|
AddressPoolDepleted,
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // /// A NAT address table
|
/// A NAT address table
|
||||||
// // #[derive(Debug)]
|
#[derive(Debug)]
|
||||||
// // pub struct Nat64Table {
|
pub struct Nat64Table {
|
||||||
// // /// All possible IPv4 addresses that can be used
|
/// All possible IPv4 addresses that can be used
|
||||||
// // ipv4_pool: Vec<Ipv4Net>,
|
ipv4_pool: Vec<Ipv4Net>,
|
||||||
// // /// Current reservations
|
/// Current reservations
|
||||||
// // reservations: BiHashMap<Ipv6Addr, Ipv4Addr>,
|
reservations: BiHashMap<Ipv6Addr, Ipv4Addr>,
|
||||||
// // /// The timestamp of each reservation (used for pruning)
|
/// The timestamp of each reservation (used for pruning)
|
||||||
// // reservation_times: HashMap<(Ipv6Addr, Ipv4Addr), Option<Instant>>,
|
reservation_times: HashMap<(Ipv6Addr, Ipv4Addr), Option<Instant>>,
|
||||||
// // /// The maximum amount of time to reserve an address pair for
|
/// The maximum amount of time to reserve an address pair for
|
||||||
// // reservation_timeout: Duration,
|
reservation_timeout: Duration,
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // impl Nat64Table {
|
impl Nat64Table {
|
||||||
// // /// Construct a new NAT64 table
|
/// Construct a new NAT64 table
|
||||||
// // ///
|
///
|
||||||
// // /// **Arguments:**
|
/// **Arguments:**
|
||||||
// // /// - `ipv4_pool`: The pool of IPv4 addresses to use in the mapping process
|
/// - `ipv4_pool`: The pool of IPv4 addresses to use in the mapping process
|
||||||
// // /// - `reservation_timeout`: The amount of time to reserve an address pair for
|
/// - `reservation_timeout`: The amount of time to reserve an address pair for
|
||||||
// // pub fn new(ipv4_pool: Vec<Ipv4Net>, reservation_timeout: Duration) -> Self {
|
pub fn new(ipv4_pool: Vec<Ipv4Net>, reservation_timeout: Duration) -> Self {
|
||||||
// // // Track the total pool size
|
// Track the total pool size
|
||||||
// // let total_size: usize = ipv4_pool.iter().map(|net| net.hosts().count()).sum();
|
let total_size: usize = ipv4_pool.iter().map(|net| net.hosts().count()).sum();
|
||||||
// // IPV4_POOL_SIZE.set(total_size as i64);
|
IPV4_POOL_SIZE.set(total_size as i64);
|
||||||
|
|
||||||
// // Self {
|
Self {
|
||||||
// // ipv4_pool,
|
ipv4_pool,
|
||||||
// // reservations: BiHashMap::new(),
|
reservations: BiHashMap::new(),
|
||||||
// // reservation_times: HashMap::new(),
|
reservation_times: HashMap::new(),
|
||||||
// // reservation_timeout,
|
reservation_timeout,
|
||||||
// // }
|
}
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // /// Make a reservation for an IP address pair for eternity
|
/// Make a reservation for an IP address pair for eternity
|
||||||
// // pub fn add_infinite_reservation(
|
pub fn add_infinite_reservation(
|
||||||
// // &mut self,
|
&mut self,
|
||||||
// // ipv6: Ipv6Addr,
|
ipv6: Ipv6Addr,
|
||||||
// // ipv4: Ipv4Addr,
|
ipv4: Ipv4Addr,
|
||||||
// // ) -> Result<(), TableError> {
|
) -> Result<(), TableError> {
|
||||||
// // // Check if either address is already reserved
|
// Check if either address is already reserved
|
||||||
// // self.prune();
|
self.prune();
|
||||||
// // self.track_utilization();
|
self.track_utilization();
|
||||||
// // if self.reservations.contains_left(&ipv6) {
|
if self.reservations.contains_left(&ipv6) {
|
||||||
// // return Err(TableError::AddressAlreadyReserved(ipv6.into()));
|
return Err(TableError::AddressAlreadyReserved(ipv6.into()));
|
||||||
// // } else if self.reservations.contains_right(&ipv4) {
|
} else if self.reservations.contains_right(&ipv4) {
|
||||||
// // return Err(TableError::AddressAlreadyReserved(ipv4.into()));
|
return Err(TableError::AddressAlreadyReserved(ipv4.into()));
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // // Add the reservation
|
// Add the reservation
|
||||||
// // self.reservations.insert(ipv6, ipv4);
|
self.reservations.insert(ipv6, ipv4);
|
||||||
// // self.reservation_times.insert((ipv6, ipv4), None);
|
self.reservation_times.insert((ipv6, ipv4), None);
|
||||||
// // log::info!("Added infinite reservation: {} -> {}", ipv6, ipv4);
|
log::info!("Added infinite reservation: {} -> {}", ipv6, ipv4);
|
||||||
// // Ok(())
|
Ok(())
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // /// Check if a given address exists in the table
|
/// Check if a given address exists in the table
|
||||||
// // pub fn contains(&self, address: &IpAddr) -> bool {
|
pub fn contains(&self, address: &IpAddr) -> bool {
|
||||||
// // match address {
|
match address {
|
||||||
// // IpAddr::V4(ipv4) => self.reservations.contains_right(ipv4),
|
IpAddr::V4(ipv4) => self.reservations.contains_right(ipv4),
|
||||||
// // IpAddr::V6(ipv6) => self.reservations.contains_left(ipv6),
|
IpAddr::V6(ipv6) => self.reservations.contains_left(ipv6),
|
||||||
// // }
|
}
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // /// Get or assign an IPv4 address for the given IPv6 address
|
/// Get or assign an IPv4 address for the given IPv6 address
|
||||||
// // pub fn get_or_assign_ipv4(&mut self, ipv6: Ipv6Addr) -> Result<Ipv4Addr, TableError> {
|
pub fn get_or_assign_ipv4(&mut self, ipv6: Ipv6Addr) -> Result<Ipv4Addr, TableError> {
|
||||||
// // // Prune old reservations
|
// Prune old reservations
|
||||||
// // self.prune();
|
self.prune();
|
||||||
// // self.track_utilization();
|
self.track_utilization();
|
||||||
|
|
||||||
// // // If the IPv6 address is already reserved, return the IPv4 address
|
// If the IPv6 address is already reserved, return the IPv4 address
|
||||||
// // if let Some(ipv4) = self.reservations.get_by_left(&ipv6) {
|
if let Some(ipv4) = self.reservations.get_by_left(&ipv6) {
|
||||||
// // // Update the reservation time
|
// Update the reservation time
|
||||||
// // self.reservation_times
|
self.reservation_times
|
||||||
// // .insert((ipv6, *ipv4), Some(Instant::now()));
|
.insert((ipv6, *ipv4), Some(Instant::now()));
|
||||||
|
|
||||||
// // // Return the v4 address
|
// Return the v4 address
|
||||||
// // return Ok(*ipv4);
|
return Ok(*ipv4);
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // // Otherwise, try to assign a new IPv4 address
|
// Otherwise, try to assign a new IPv4 address
|
||||||
// // for ipv4_net in &self.ipv4_pool {
|
for ipv4_net in &self.ipv4_pool {
|
||||||
// // for ipv4 in ipv4_net.hosts() {
|
for ipv4 in ipv4_net.hosts() {
|
||||||
// // // Check if this address is available for use
|
// Check if this address is available for use
|
||||||
// // if !self.reservations.contains_right(&ipv4) {
|
if !self.reservations.contains_right(&ipv4) {
|
||||||
// // // Add the reservation
|
// Add the reservation
|
||||||
// // self.reservations.insert(ipv6, ipv4);
|
self.reservations.insert(ipv6, ipv4);
|
||||||
// // self.reservation_times
|
self.reservation_times
|
||||||
// // .insert((ipv6, ipv4), Some(Instant::now()));
|
.insert((ipv6, ipv4), Some(Instant::now()));
|
||||||
// // log::info!("Assigned new reservation: {} -> {}", ipv6, ipv4);
|
log::info!("Assigned new reservation: {} -> {}", ipv6, ipv4);
|
||||||
// // return Ok(ipv4);
|
return Ok(ipv4);
|
||||||
// // }
|
}
|
||||||
// // }
|
}
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // // If we get here, we failed to find an available address
|
// If we get here, we failed to find an available address
|
||||||
// // Err(TableError::AddressPoolDepleted)
|
Err(TableError::AddressPoolDepleted)
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // /// Try to find an IPv6 address for the given IPv4 address
|
/// Try to find an IPv6 address for the given IPv4 address
|
||||||
// // pub fn get_reverse(&mut self, ipv4: Ipv4Addr) -> Result<Ipv6Addr, TableError> {
|
pub fn get_reverse(&mut self, ipv4: Ipv4Addr) -> Result<Ipv6Addr, TableError> {
|
||||||
// // // Prune old reservations
|
// Prune old reservations
|
||||||
// // self.prune();
|
self.prune();
|
||||||
// // self.track_utilization();
|
self.track_utilization();
|
||||||
|
|
||||||
// // // If the IPv4 address is already reserved, return the IPv6 address
|
// If the IPv4 address is already reserved, return the IPv6 address
|
||||||
// // if let Some(ipv6) = self.reservations.get_by_right(&ipv4) {
|
if let Some(ipv6) = self.reservations.get_by_right(&ipv4) {
|
||||||
// // // Update the reservation time
|
// Update the reservation time
|
||||||
// // self.reservation_times
|
self.reservation_times
|
||||||
// // .insert((*ipv6, ipv4), Some(Instant::now()));
|
.insert((*ipv6, ipv4), Some(Instant::now()));
|
||||||
|
|
||||||
// // // Return the v6 address
|
// Return the v6 address
|
||||||
// // return Ok(*ipv6);
|
return Ok(*ipv6);
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // // Otherwise, there is no matching reservation
|
// Otherwise, there is no matching reservation
|
||||||
// // Err(TableError::NoIpv6Mapping(ipv4))
|
Err(TableError::NoIpv6Mapping(ipv4))
|
||||||
// // }
|
}
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // impl Nat64Table {
|
impl Nat64Table {
|
||||||
// // /// Prune old reservations
|
/// Prune old reservations
|
||||||
// // fn prune(&mut self) {
|
fn prune(&mut self) {
|
||||||
// // let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
// // // Prune from the reservation map
|
// Prune from the reservation map
|
||||||
// // self.reservations.retain(|v6, v4| {
|
self.reservations.retain(|v6, v4| {
|
||||||
// // if let Some(Some(time)) = self.reservation_times.get(&(*v6, *v4)) {
|
if let Some(Some(time)) = self.reservation_times.get(&(*v6, *v4)) {
|
||||||
// // let keep = now - *time < self.reservation_timeout;
|
let keep = now - *time < self.reservation_timeout;
|
||||||
// // if !keep {
|
if !keep {
|
||||||
// // log::info!("Pruned reservation: {} -> {}", v6, v4);
|
log::info!("Pruned reservation: {} -> {}", v6, v4);
|
||||||
// // }
|
}
|
||||||
// // keep
|
keep
|
||||||
// // } else {
|
} else {
|
||||||
// // true
|
true
|
||||||
// // }
|
}
|
||||||
// // });
|
});
|
||||||
|
|
||||||
// // // Remove all times assigned to reservations that no longer exist
|
// Remove all times assigned to reservations that no longer exist
|
||||||
// // self.reservation_times.retain(|(v6, v4), _| {
|
self.reservation_times.retain(|(v6, v4), _| {
|
||||||
// // self.reservations.contains_left(v6) && self.reservations.contains_right(v4)
|
self.reservations.contains_left(v6) && self.reservations.contains_right(v4)
|
||||||
// // });
|
});
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // fn track_utilization(&self) {
|
fn track_utilization(&self) {
|
||||||
// // // Count static and dynamic in a single pass
|
// Count static and dynamic in a single pass
|
||||||
// // let (total_dynamic_reservations, total_static_reservations) = self
|
let (total_dynamic_reservations, total_static_reservations) = self
|
||||||
// // .reservation_times
|
.reservation_times
|
||||||
// // .iter()
|
.iter()
|
||||||
// // .map(|((_v6, _v4), time)| match time {
|
.map(|((_v6, _v4), time)| match time {
|
||||||
// // Some(_) => (1, 0),
|
Some(_) => (1, 0),
|
||||||
// // None => (0, 1),
|
None => (0, 1),
|
||||||
// // })
|
})
|
||||||
// // .fold((0, 0), |(a1, a2), (b1, b2)| (a1 + b1, a2 + b2));
|
.fold((0, 0), |(a1, a2), (b1, b2)| (a1 + b1, a2 + b2));
|
||||||
|
|
||||||
// // // Track the values
|
// Track the values
|
||||||
// // IPV4_POOL_RESERVED
|
IPV4_POOL_RESERVED
|
||||||
// // .with_label_values(&["dynamic"])
|
.with_label_values(&["dynamic"])
|
||||||
// // .set(i64::from(total_dynamic_reservations));
|
.set(i64::from(total_dynamic_reservations));
|
||||||
// // IPV4_POOL_RESERVED
|
IPV4_POOL_RESERVED
|
||||||
// // .with_label_values(&["static"])
|
.with_label_values(&["static"])
|
||||||
// // .set(i64::from(total_static_reservations));
|
.set(i64::from(total_static_reservations));
|
||||||
// // }
|
}
|
||||||
// // }
|
}
|
||||||
|
|
||||||
// // #[cfg(test)]
|
#[cfg(test)]
|
||||||
// // mod tests {
|
mod tests {
|
||||||
// // use super::*;
|
use super::*;
|
||||||
|
|
||||||
// // #[test]
|
#[test]
|
||||||
// // fn test_add_infinite_reservation() {
|
fn test_add_infinite_reservation() {
|
||||||
// // let mut table = Nat64Table::new(
|
let mut table = Nat64Table::new(
|
||||||
// // vec![Ipv4Net::new(Ipv4Addr::new(192, 0, 2, 0), 24).unwrap()],
|
vec![Ipv4Net::new(Ipv4Addr::new(192, 0, 2, 0), 24).unwrap()],
|
||||||
// // Duration::from_secs(60),
|
Duration::from_secs(60),
|
||||||
// // );
|
);
|
||||||
|
|
||||||
// // // Add a reservation
|
// Add a reservation
|
||||||
// // table
|
table
|
||||||
// // .add_infinite_reservation("2001:db8::1".parse().unwrap(), "192.0.2.1".parse().unwrap())
|
.add_infinite_reservation("2001:db8::1".parse().unwrap(), "192.0.2.1".parse().unwrap())
|
||||||
// // .unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// // // Check that it worked
|
// Check that it worked
|
||||||
// // assert_eq!(
|
assert_eq!(
|
||||||
// // table
|
table
|
||||||
// // .reservations
|
.reservations
|
||||||
// // .get_by_left(&"2001:db8::1".parse().unwrap()),
|
.get_by_left(&"2001:db8::1".parse().unwrap()),
|
||||||
// // Some(&"192.0.2.1".parse().unwrap())
|
Some(&"192.0.2.1".parse().unwrap())
|
||||||
// // );
|
);
|
||||||
// // }
|
}
|
||||||
// // }
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user