Stash commit 📦️
This commit is contained in:
parent
23ce3ef304
commit
b49f55796f
@ -1,3 +1,4 @@
|
|||||||
|
#[allow(clippy::upper_case_acronyms)]
|
||||||
use std::{
|
use std::{
|
||||||
io, net::IpAddr,
|
io, net::IpAddr,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
sync::{Mutex, Arc},
|
sync::{Mutex, Arc}, time::Duration, thread::sleep,
|
||||||
time::Duration,
|
net::{Ipv4Addr, IpAddr}, vec::Vec, io};
|
||||||
thread::sleep,
|
|
||||||
net::IpAddr,
|
|
||||||
vec::Vec, io};
|
|
||||||
|
|
||||||
use log::{trace, warn, error};
|
use log::{trace, warn, error};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
@ -12,13 +9,8 @@ use tokio::{
|
|||||||
use nbd::server::Blocks;
|
use nbd::server::Blocks;
|
||||||
use icmp::IcmpSocket;
|
use icmp::IcmpSocket;
|
||||||
use crate::{
|
use crate::{
|
||||||
ICMP_PACKET, IPStore,
|
ICMP_PACKET, BYTE_COUNT, PACKET_SIZE, SIZE,
|
||||||
pinger::connect,
|
IPStore, checksum, connect, get_rt};
|
||||||
checksum,
|
|
||||||
get_rt};
|
|
||||||
|
|
||||||
const SIZE: usize = 64; // Bits of data in each ping
|
|
||||||
static WORD_COUNT: usize = SIZE / 8; // Number of 16-bit words in each ping
|
|
||||||
|
|
||||||
type Message = (usize, usize, Vec<u8>); // Message type for the write channel
|
type Message = (usize, usize, Vec<u8>); // Message type for the write channel
|
||||||
|
|
||||||
@ -56,7 +48,7 @@ impl PingStore {
|
|||||||
let offset = store.pings.lock().unwrap().len() * 7;
|
let offset = store.pings.lock().unwrap().len() * 7;
|
||||||
let mut ping = Ping::new();
|
let mut ping = Ping::new();
|
||||||
for j in 0..7 {
|
for j in 0..7 {
|
||||||
ping.add(ips.dsts[offset + j].ip);
|
ping.add(IpAddr::V4(ips.dsts[offset + j].ip));
|
||||||
}
|
}
|
||||||
store.pings.lock().unwrap().push(ping);
|
store.pings.lock().unwrap().push(ping);
|
||||||
store.size += 1;
|
store.size += 1;
|
||||||
@ -124,7 +116,7 @@ impl PingStore {
|
|||||||
|
|
||||||
impl Blocks for PingStore {
|
impl Blocks for PingStore {
|
||||||
fn read_at(&self, buf: &mut [u8], off: u64) -> io::Result<()> {
|
fn read_at(&self, buf: &mut [u8], off: u64) -> io::Result<()> {
|
||||||
let addr = (off as usize).div_floor(WORD_COUNT);
|
let addr = (off as usize).div_floor(BYTE_COUNT);
|
||||||
let buflen = buf.len();
|
let buflen = buf.len();
|
||||||
let mut res = vec![];
|
let mut res = vec![];
|
||||||
for pingspan in 0..buflen.div_ceil(SIZE) {
|
for pingspan in 0..buflen.div_ceil(SIZE) {
|
||||||
@ -136,10 +128,10 @@ impl Blocks for PingStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn write_at(&self, buf: &[u8], off: u64) -> io::Result<()> {
|
fn write_at(&self, buf: &[u8], off: u64) -> io::Result<()> {
|
||||||
let addr = (off as usize).div_ceil(WORD_COUNT);
|
let addr = (off as usize).div_ceil(BYTE_COUNT);
|
||||||
let buflen = buf.len();
|
let buflen = buf.len();
|
||||||
get_rt("channel pusher", num_cpus::get().div_floor(3) + 1).block_on(
|
get_rt("channel pusher", num_cpus::get().div_floor(3) + 1).block_on(
|
||||||
self.write(buf, addr, (addr * WORD_COUNT) - off as usize, buflen))?;
|
self.write(buf, addr, (addr * BYTE_COUNT) - off as usize, buflen))?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
61
src/lib.rs
61
src/lib.rs
@ -1,20 +1,68 @@
|
|||||||
#![feature(bigint_helper_methods)]
|
#![feature(bigint_helper_methods)]
|
||||||
#![feature(async_closure)]
|
//#![feature(async_closure)]
|
||||||
#![feature(int_roundings)]
|
#![feature(int_roundings)]
|
||||||
#![feature(never_type)]
|
#![feature(never_type)]
|
||||||
#![feature(ip)]
|
#![feature(ip)]
|
||||||
|
|
||||||
pub mod scanner;
|
pub mod scanner;
|
||||||
pub mod pinger;
|
//pub mod pinger;
|
||||||
pub mod blocks;
|
pub mod blocks;
|
||||||
pub mod store;
|
mod store;
|
||||||
pub use blocks::PingStore;
|
pub use blocks::PingStore;
|
||||||
pub use scanner::Scanner;
|
pub use scanner::Scanner;
|
||||||
pub use pinger::{
|
//pub use pinger::Pinger;
|
||||||
ICMP_PACKET, TIMEOUT, Pinger};
|
|
||||||
pub use store::IPStore;
|
pub use store::IPStore;
|
||||||
|
|
||||||
|
/// ICMP packet header template
|
||||||
|
pub const ICMP_PACKET: [u8; 8] = [
|
||||||
|
8, 0, // 8 Echo ping request, code 0
|
||||||
|
0, 0, // Index 2 and 3 are for ICMP checksum
|
||||||
|
0xde, 0xad, // Identifier
|
||||||
|
0, 1, // Sequence numbers
|
||||||
|
];
|
||||||
|
|
||||||
|
pub const SIZE: usize = 64; // Bytes of echo data in each ping
|
||||||
|
pub const PACKET_SIZE: usize = SIZE + 16; // Echo data size plus rest of ICMP packet
|
||||||
|
pub const BYTE_COUNT: usize = SIZE / 8;
|
||||||
|
|
||||||
|
pub static mut TIMEOUT: Option<std::time::Duration> = None;
|
||||||
|
|
||||||
|
/// Get an IcmpSocket from IpAddr and set the global timeout on the socket
|
||||||
|
pub fn connect(ip: std::net::IpAddr) -> icmp::IcmpSocket {
|
||||||
|
let sock = match icmp::IcmpSocket::connect(ip) {
|
||||||
|
Err(err) => panic!("Unable to open ICMP socket: {err:?}"),
|
||||||
|
Ok(sock) => sock,
|
||||||
|
};
|
||||||
|
let timeout = unsafe { TIMEOUT };
|
||||||
|
if timeout.is_none() {
|
||||||
|
return sock;
|
||||||
|
}
|
||||||
|
if let Err(err) = sock.set_write_timeout(timeout) {
|
||||||
|
log::debug!("unable to set write timeout on socket: {}", err);
|
||||||
|
};
|
||||||
|
if let Err(err) = sock.set_read_timeout(timeout) {
|
||||||
|
log::debug!("unable to set read timeout on socket: {}", err);
|
||||||
|
};
|
||||||
|
sock
|
||||||
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
/// Get a random IPv4 address
|
||||||
|
pub fn rand_ip4() -> std::net::Ipv4Addr {
|
||||||
|
std::net::Ipv4Addr::new(rand::random(), rand::random(), rand::random(), rand::random())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Get a random globaly accessible IP address
|
||||||
|
pub fn rand_ip() -> std::net::Ipv4Addr {
|
||||||
|
let mut ip = rand_ip4();
|
||||||
|
while !ip.is_global() { ip = rand_ip4() };
|
||||||
|
//std::net::IpAddr::V4(ip)
|
||||||
|
ip
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
/// Calculate the "internet checksum"
|
||||||
pub fn checksum(bytes: &[u8]) -> [u8; 2] {
|
pub fn checksum(bytes: &[u8]) -> [u8; 2] {
|
||||||
let mut calc = internet_checksum::Checksum::new();
|
let mut calc = internet_checksum::Checksum::new();
|
||||||
calc.add_bytes(bytes);
|
calc.add_bytes(bytes);
|
||||||
@ -22,12 +70,13 @@ pub fn checksum(bytes: &[u8]) -> [u8; 2] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
|
/// Get a new tokio multi thread runtime
|
||||||
pub fn get_rt(name: &str, threads: usize) -> tokio::runtime::Runtime {
|
pub fn get_rt(name: &str, threads: usize) -> tokio::runtime::Runtime {
|
||||||
match tokio::runtime::Builder::new_multi_thread()
|
match tokio::runtime::Builder::new_multi_thread()
|
||||||
.worker_threads(threads)
|
.worker_threads(threads)
|
||||||
.thread_name(name)
|
.thread_name(name)
|
||||||
.build() {
|
.build() {
|
||||||
Err(err) => panic!("Unable to build tokio runtime! {:?}", err),
|
Err(err) => panic!("Unable to build tokio runtime! {err:?}"),
|
||||||
Ok(rt) => rt,
|
Ok(rt) => rt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,40 +6,33 @@ use std::{
|
|||||||
use log::{trace, debug, info};
|
use log::{trace, debug, info};
|
||||||
use icmp::IcmpSocket;
|
use icmp::IcmpSocket;
|
||||||
use rand::random;
|
use rand::random;
|
||||||
use crate::checksum;
|
use crate::{
|
||||||
|
ICMP_PACKET, PACKET_SIZE,
|
||||||
pub static mut TIMEOUT: Option<Duration> = None;
|
TIMEOUT, SIZE, checksum};
|
||||||
|
|
||||||
pub static ICMP_PACKET: [u8; 8] = [
|
|
||||||
8, 0, // 8 Echo ping request, code 0
|
|
||||||
0, 0, // Index 2 and 3 are for ICMP checksum
|
|
||||||
0xde, 0xad, // Identifier
|
|
||||||
0, 1, // Sequence numbers
|
|
||||||
];
|
|
||||||
|
|
||||||
pub struct PingResponse {
|
pub struct PingResponse {
|
||||||
pub round_trip: usize,
|
pub round_trip: usize,
|
||||||
pub corrupt: bool,
|
pub corrupt: bool,
|
||||||
pub small: bool,
|
pub small: bool,
|
||||||
pub data: [u8; 84],
|
pub data: [u8; PACKET_SIZE],
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type PingResult = Result<PingResponse, io::Error>;
|
pub type PingResult = Result<PingResponse, io::Error>;
|
||||||
|
|
||||||
pub struct Pinger {
|
pub struct Pinger {
|
||||||
pub data: [u8; 64],
|
pub data: [u8; SIZE],
|
||||||
pub seq: [u8; 2],
|
pub seq: [u8; 2],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pinger {
|
impl Pinger {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
data: [0; 64],
|
data: [0; SIZE],
|
||||||
seq: [1, 0],
|
seq: [1, 0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn data(mut self, data: [u8; 64]) -> Self {
|
pub fn data(mut self, data: [u8; SIZE]) -> Self {
|
||||||
self.data = data;
|
self.data = data;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -68,7 +61,7 @@ impl Pinger {
|
|||||||
};
|
};
|
||||||
self.inc_seq();
|
self.inc_seq();
|
||||||
|
|
||||||
let mut response: [u8; 64 + 20] = [0; 64 + 20];
|
let mut response: [u8; PACKET_SIZE] = [0; PACKET_SIZE];
|
||||||
|
|
||||||
match socket.recv(&mut response) {
|
match socket.recv(&mut response) {
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -78,7 +71,7 @@ impl Pinger {
|
|||||||
|
|
||||||
Ok(size) => {
|
Ok(size) => {
|
||||||
let mut result = PingResponse::new(response, start.elapsed().as_millis() as usize);
|
let mut result = PingResponse::new(response, start.elapsed().as_millis() as usize);
|
||||||
if size != 84 {
|
if size != PACKET_SIZE {
|
||||||
debug!("Didn't revice full response from \"{}\"", ip);
|
debug!("Didn't revice full response from \"{}\"", ip);
|
||||||
trace!("Response in question: {:?}", response);
|
trace!("Response in question: {:?}", response);
|
||||||
result.corrupt = true;
|
result.corrupt = true;
|
||||||
@ -141,7 +134,7 @@ pub fn rand_ip() -> IpAddr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PingResponse {
|
impl PingResponse {
|
||||||
fn new(data: [u8; 84], round_trip: usize) -> Self {
|
fn new(data: [u8; PACKET_SIZE], round_trip: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
round_trip,
|
round_trip,
|
||||||
corrupt: false,
|
corrupt: false,
|
||||||
@ -153,7 +146,7 @@ impl PingResponse {
|
|||||||
|
|
||||||
impl Default for PingResponse {
|
impl Default for PingResponse {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new([0; 84], 0)
|
Self::new([0; PACKET_SIZE], 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
111
src/scanner.rs
111
src/scanner.rs
@ -1,32 +1,43 @@
|
|||||||
use std::{
|
use std::{
|
||||||
vec::Vec, net::{Ipv4Addr, IpAddr},
|
time::{Duration, Instant},
|
||||||
time::Duration, thread::sleep};
|
net::{Ipv4Addr, IpAddr},
|
||||||
|
thread::sleep, io,
|
||||||
|
vec::Vec};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use log::{trace, debug, info, error};
|
use log::{trace, debug, info, error};
|
||||||
use tokio::task;
|
use icmp::IcmpSocket;
|
||||||
|
use tokio::{task, };
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
pinger::rand_ip,
|
ICMP_PACKET, PACKET_SIZE, SIZE,
|
||||||
IPStore, Pinger};
|
IPStore, checksum, connect,
|
||||||
|
rand_ip};
|
||||||
|
|
||||||
//static PROBE_CHK: [u8; 2] = [0x51, 0x59];
|
static PROBE: [u8; SIZE] = [0x66; SIZE];
|
||||||
static PROBE: [u8; 64] = [0x66; 64];
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Scanner {
|
pub struct Scanner {
|
||||||
pub dsts: Vec<Destination>,
|
pub dsts: Vec<Destination>,
|
||||||
pub dead: Vec<IpAddr>,
|
pub dead: Vec<Ipv4Addr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone)]
|
#[derive(Deserialize, Serialize, Clone)]
|
||||||
pub struct Destination {
|
pub struct Destination {
|
||||||
pub round_trip: usize, // Round trip in ms
|
pub round_trip: usize, // Round trip in ms
|
||||||
pub small: bool,
|
pub small: bool,
|
||||||
pub ip: IpAddr,
|
pub ip: Ipv4Addr,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ScanResult = Result<Destination, IpAddr>;
|
struct PingResponse {
|
||||||
|
pub round_trip: usize,
|
||||||
|
pub corrupt: bool,
|
||||||
|
pub small: bool,
|
||||||
|
pub data: [u8; PACKET_SIZE],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PingResult = Result<PingResponse, io::Error>;
|
||||||
|
pub type ScanResult = Result<Destination, Ipv4Addr>;
|
||||||
|
|
||||||
impl Scanner {
|
impl Scanner {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
@ -43,16 +54,16 @@ impl Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn next_ip(&self) -> IpAddr {
|
fn next_ip(&self) -> Ipv4Addr {
|
||||||
// let mut ip = rand_ip();
|
let mut ip = rand_ip();
|
||||||
let mut ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 138));
|
// let mut ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 138));
|
||||||
while self.scanned(&ip) {
|
while self.scanned(&ip) {
|
||||||
ip = rand_ip(); }
|
ip = rand_ip(); }
|
||||||
ip
|
ip
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn scanned(&self, ip: &IpAddr) -> bool {
|
fn scanned(&self, ip: &Ipv4Addr) -> bool {
|
||||||
for dead in &self.dead {
|
for dead in &self.dead {
|
||||||
if ip == dead { return true; }
|
if ip == dead { return true; }
|
||||||
}
|
}
|
||||||
@ -64,43 +75,65 @@ impl Scanner {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn pop_futs(&mut self, futs: &mut Vec<task::JoinHandle<ScanResult>>) {
|
|
||||||
let fut = futs.pop().expect("Unable to pop of futures vec during mass_scan()!");
|
|
||||||
match fut.await {
|
|
||||||
Err(err) => {
|
|
||||||
error!("Joing future: {:?}", err);
|
|
||||||
},
|
|
||||||
Ok(res) => match res {
|
|
||||||
Ok(dst) => self.dsts.push(dst),
|
|
||||||
Err(ip) => self.dead.push(ip),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn mass_scan(&mut self, throttle: usize, parallel: usize, limit: usize, rand: bool) {
|
pub async fn mass_scan(&mut self, throttle: usize, parallel: usize, limit: usize, rand: bool) {
|
||||||
info!("Starting mass scan with a limit of {} and {} to randomized IP order!", limit, rand);
|
info!("Starting mass scan with a limit of {} and {} to randomized IP order!", limit, rand);
|
||||||
let duration = Duration::from_millis(throttle as u64);
|
let duration = Duration::from_millis(throttle as u64);
|
||||||
let mut futs: Vec<task::JoinHandle<ScanResult>> = vec![];
|
let mut futs: Vec<task::JoinHandle<()>> = vec![];
|
||||||
|
let listner = task::spawn(listner());
|
||||||
|
|
||||||
for _i in 0..limit {
|
for _i in 0..limit {
|
||||||
if futs.len() >= parallel {
|
if futs.len() >= parallel {
|
||||||
self.pop_futs(&mut futs).await;
|
pop_futs(&mut futs).await;
|
||||||
}
|
}
|
||||||
futs.push(task::spawn(scan(self.next_ip())));
|
futs.push(task::spawn(ping(self.next_ip(), &PROBE)));
|
||||||
sleep(duration);
|
sleep(duration);
|
||||||
}
|
}
|
||||||
while !futs.is_empty() {
|
while !futs.is_empty() { pop_futs(&mut futs).await; }
|
||||||
self.pop_futs(&mut futs).await }
|
listner.abort();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn scan(ip: IpAddr) -> Result<Destination, IpAddr> {
|
async fn pop_futs(futs: &mut Vec<task::JoinHandle<()>>) {
|
||||||
trace!("Scanning IP \"{}\"", ip);
|
futs.pop().expect("Unable to pop of futures vec during mass_scan()!").await;
|
||||||
if let Ok(res) = Pinger::new().data(PROBE).ping(ip) {
|
}
|
||||||
info!("Good result scanning IP \"{}\": {}ms", ip, res.round_trip);
|
|
||||||
if res.corrupt { return Err(ip) }
|
async fn ping(ip: Ipv4Addr, data: &[u8]) {
|
||||||
Ok(Destination { ip, small: res.small, round_trip: res.round_trip })
|
trace!("Scanning IP \"{ip}\"");
|
||||||
} else { Err(ip) }
|
let mut pack = ICMP_PACKET.to_vec();
|
||||||
|
|
||||||
|
pack[4..7].copy_from_slice(&ip.octets());
|
||||||
|
pack.append(&mut data.to_vec());
|
||||||
|
|
||||||
|
let checksum = checksum(&pack);
|
||||||
|
pack[2] = checksum[0];
|
||||||
|
pack[3] = checksum[1];
|
||||||
|
|
||||||
|
let mut socket = connect(IpAddr::V4(ip));
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
match socket.send(&pack) {
|
||||||
|
Ok(size) => if size != pack.len() { debug!("Sent {size} bytes of {} bytes to {ip}", pack.len()) },
|
||||||
|
Err(err) => debug!("Unable to ping IP {ip}: {err:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn listner() -> ! {
|
||||||
|
let socket = match IcmpSocket::connect(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))) {
|
||||||
|
Err(err) => panic!("Unable to open listening socket: {err:?}"),
|
||||||
|
Ok(sock) => sock,
|
||||||
|
};
|
||||||
|
let mut handles = vec![];
|
||||||
|
loop {
|
||||||
|
let mut response: [u8; PACKET_SIZE] = [0; PACKET_SIZE];
|
||||||
|
match socket.recv(&mut response) {
|
||||||
|
Ok(size) => handles.push(task::spawn(handler(response, size))),
|
||||||
|
Err(err) => info!("Error reading a ping reply: {err:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler(response: [u8; PACKET_SIZE], size: usize) {
|
||||||
|
todo!();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Scanner {
|
impl Default for Scanner {
|
||||||
|
@ -2,7 +2,7 @@ use std::{
|
|||||||
fs::OpenOptions,
|
fs::OpenOptions,
|
||||||
io::{Write, Read},
|
io::{Write, Read},
|
||||||
str::from_utf8,
|
str::from_utf8,
|
||||||
net::IpAddr,
|
net::Ipv4Addr,
|
||||||
vec::Vec};
|
vec::Vec};
|
||||||
|
|
||||||
use log::{trace, debug, error};
|
use log::{trace, debug, error};
|
||||||
@ -14,7 +14,7 @@ use crate::{
|
|||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
pub struct IPStore {
|
pub struct IPStore {
|
||||||
pub dsts: Vec<Destination>,
|
pub dsts: Vec<Destination>,
|
||||||
pub dead: Vec<IpAddr>,
|
pub dead: Vec<Ipv4Addr>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IPStore {
|
impl IPStore {
|
||||||
|
Loading…
Reference in New Issue
Block a user