This repository has been archived by the owner on Jul 11, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 134
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tools: add redbpf-tcp-knock, a simple TCP knocking tool
Can be used as: redbpf-tcp-knock -i eth0 -k 1112 -k 1113 -p 1111 to allow access to the TCP service listening on port 1111 only after a peer has tried to connect (knock) to ports 1112 and 1113 in that order.
- Loading branch information
1 parent
58f23c0
commit d8db68e
Showing
6 changed files
with
372 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
// Copyright 2019-2020 Authors of Red Sift | ||
// | ||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or | ||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or | ||
// http://opensource.org/licenses/MIT>, at your option. This file may not be | ||
// copied, modified, or distributed except according to those terms. | ||
#![no_std] | ||
#![no_main] | ||
use probes::knock::{Connection, Knock, KnockAttempt, PortSequence, MAX_SEQ_LEN}; | ||
use redbpf_probes::xdp::prelude::*; | ||
|
||
program!(0xFFFFFFFE, "GPL"); | ||
|
||
const TCP_FLAG_SYN: u16 = 0x0002u16.to_be(); | ||
|
||
#[map("sequence")] | ||
static mut sequence: HashMap<u8, PortSequence> = HashMap::with_max_entries(1); | ||
|
||
#[map("knocks")] | ||
static mut knocks: HashMap<u32, Knock> = HashMap::with_max_entries(1024); | ||
|
||
#[map("knock_attempts")] | ||
static mut knock_attempts: PerfMap<KnockAttempt> = PerfMap::with_max_entries(1024); | ||
|
||
#[map("connections")] | ||
static mut connections: PerfMap<Connection> = PerfMap::with_max_entries(1024); | ||
|
||
#[xdp("knock")] | ||
pub fn probe(ctx: XdpContext) -> XdpResult { | ||
// only process TCP packets | ||
let tcp = match ctx.transport()? { | ||
t @ Transport::TCP(_) => t, | ||
_ => return Ok(XdpAction::Pass), | ||
}; | ||
let ip = unsafe { *ctx.ip()? }; | ||
|
||
// get the knock sequence as configured by user space | ||
let target_seq = unsafe { | ||
let seq_id = 0u8; | ||
sequence.get_mut(&seq_id).ok_or(NetworkError::Other)? | ||
}; | ||
|
||
// we only process SYN packets, all other packets can go through | ||
if !has_flag(&tcp, TCP_FLAG_SYN) { | ||
return Ok(XdpAction::Pass); | ||
} | ||
|
||
// get the knock data for the source IP address | ||
let mut knock = unsafe { | ||
let key = ip.saddr; | ||
match knocks.get_mut(&key) { | ||
Some(k) => k, | ||
None => { | ||
let knock = Knock::new(target_seq.target); | ||
knocks.set(&key, &knock); | ||
knocks.get_mut(&key).ok_or(NetworkError::Other)? | ||
} | ||
} | ||
}; | ||
|
||
// this peer has already completed the knock sequence so data can pass | ||
if knock.complete == 1 { | ||
return Ok(XdpAction::Pass); | ||
} | ||
|
||
if tcp.dest() == target_seq.target as u16 { | ||
// block a connection attempt to the target port if the knock sequence | ||
// is incomplete | ||
if !target_seq.is_complete(&knock.sequence) { | ||
// notify user space that we're blocking the connection | ||
let conn = Connection { | ||
source_ip: u32::from_be(ip.saddr), | ||
allowed: 0, | ||
}; | ||
unsafe { connections.insert(&ctx, &MapData::new(conn)) } | ||
|
||
return Ok(XdpAction::Drop); | ||
} | ||
|
||
// mark the knock as complete, so that for successive connections we | ||
// exit a bit earlier and we only notify user space once. | ||
knock.complete = 1; | ||
|
||
// notify user space that we're allowing the connection | ||
let conn = Connection { | ||
source_ip: u32::from_be(ip.saddr), | ||
allowed: 1, | ||
}; | ||
unsafe { connections.insert(&ctx, &MapData::new(conn)) } | ||
|
||
return Ok(XdpAction::Pass); | ||
} | ||
|
||
// this is a SYN packet directed to a port that is not the target port, | ||
// so process it as a knock attempt. | ||
|
||
// restart wrong knock sequences once they reach the target sequence length. | ||
// The verifier needs to know that there's an upper bound to | ||
// knock.sequence.len so we check for both target_seq.len and MAX_SEQ_LEN | ||
if knock.sequence.len >= target_seq.len || knock.sequence.len >= MAX_SEQ_LEN { | ||
knock.sequence.len = 0; | ||
} | ||
knock.sequence.ports[knock.sequence.len] = tcp.dest(); | ||
knock.sequence.len += 1; | ||
|
||
// notify user space that ip.saddr knocked on tcp.dest() | ||
let attempt = KnockAttempt { | ||
source_ip: u32::from_be(ip.saddr), | ||
padding: 0, | ||
sequence: knock.sequence.clone(), | ||
}; | ||
unsafe { knock_attempts.insert(&ctx, &MapData::new(attempt)) } | ||
|
||
return Ok(XdpAction::Pass); | ||
} | ||
|
||
#[inline] | ||
fn has_flag(tcp: &Transport, flag: u16) -> bool { | ||
if let Transport::TCP(hdr) = tcp { | ||
let flags = unsafe { *(&(**hdr)._bitfield_1 as *const _ as *const u16) }; | ||
return flags & flag != 0; | ||
} | ||
|
||
return false; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
pub const MAX_SEQ_LEN: usize = 4; | ||
#[derive(Debug, Clone)] | ||
#[repr(C)] | ||
pub struct PortSequence { | ||
pub ports: [u16; MAX_SEQ_LEN], | ||
pub len: usize, | ||
pub target: u64, | ||
} | ||
|
||
impl PortSequence { | ||
#[inline] | ||
pub fn is_complete(&self, other: &PortSequence) -> bool { | ||
if self.len != other.len { | ||
return false; | ||
} | ||
for i in 0..self.len { | ||
if self.ports[i] != other.ports[i] { | ||
return false; | ||
} | ||
} | ||
true | ||
} | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
#[repr(C)] | ||
pub struct Knock { | ||
pub sequence: PortSequence, | ||
pub complete: u64, | ||
} | ||
|
||
impl Knock { | ||
pub fn new(target: u64) -> Knock { | ||
Knock { | ||
sequence: PortSequence { | ||
ports: [0u16; MAX_SEQ_LEN], | ||
len: 0, | ||
target, | ||
}, | ||
complete: 0, | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug)] | ||
#[repr(C)] | ||
pub struct KnockAttempt { | ||
pub source_ip: u32, | ||
pub padding: u32, | ||
pub sequence: PortSequence, | ||
} | ||
|
||
#[derive(Debug)] | ||
#[repr(C)] | ||
pub struct Connection { | ||
pub source_ip: u32, | ||
pub allowed: u32, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
#![no_std] | ||
pub mod bindings; | ||
pub mod iotop; | ||
pub mod knock; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
// Copyright 2019-2020 Authors of Red Sift | ||
// | ||
// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or | ||
// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or | ||
// http://opensource.org/licenses/MIT>, at your option. This file may not be | ||
// copied, modified, or distributed except according to those terms. | ||
use futures::stream::StreamExt; | ||
use getopts::Options; | ||
use redbpf::{load::Loader, xdp, HashMap}; | ||
use std::env; | ||
use std::net::Ipv4Addr; | ||
use std::path::PathBuf; | ||
use std::process; | ||
use std::ptr; | ||
use tokio; | ||
use tokio::runtime::Runtime; | ||
use tokio::signal; | ||
|
||
use probes::knock::{Connection, KnockAttempt, PortSequence, MAX_SEQ_LEN}; | ||
|
||
fn main() { | ||
let opts = match parse_opts() { | ||
Some(o) => o, | ||
None => process::exit(1), | ||
}; | ||
|
||
let mut runtime = Runtime::new().unwrap(); | ||
let _ = runtime.block_on(async { | ||
let interface = Some(opts.interface); | ||
let mut loader = Loader::new() | ||
.xdp(interface.map(String::from), xdp::Flags::default()) | ||
.load_file( | ||
&PathBuf::from(env!("CARGO_MANIFEST_DIR")) | ||
.join("probes/target/release/bpf-programs/knock/knock.elf"), | ||
) | ||
.await | ||
.expect("error loading probe"); | ||
let mut sequence = PortSequence { | ||
ports: [0; MAX_SEQ_LEN], | ||
len: opts.knock.len(), | ||
target: opts.port as u64, | ||
}; | ||
sequence.ports[..opts.knock.len()].copy_from_slice(&opts.knock); | ||
|
||
let seq_map = loader | ||
.module | ||
.maps | ||
.iter() | ||
.find(|m| m.name == "sequence") | ||
.unwrap(); | ||
let seq_map = HashMap::<u8, PortSequence>::new(seq_map).unwrap(); | ||
seq_map.set(0u8, sequence); | ||
|
||
tokio::spawn(async move { | ||
while let Some((name, events)) = loader.events.next().await { | ||
for event in events { | ||
match name.as_str() { | ||
"knock_attempts" => { | ||
let knock = unsafe { ptr::read(event.as_ptr() as *const KnockAttempt) }; | ||
let seq = &knock.sequence; | ||
println!( | ||
"Received knock from {} sequence {}", | ||
Ipv4Addr::from(knock.source_ip), | ||
seq.ports[..seq.len] | ||
.iter() | ||
.enumerate() | ||
.map(|(i, port)| { | ||
if i == seq.len - 1 { | ||
format!("*{}", port) | ||
} else { | ||
format!("{}", port) | ||
} | ||
}) | ||
.collect::<Vec<String>>() | ||
.join(" ") | ||
) | ||
} | ||
"connections" => { | ||
let conn = unsafe { ptr::read(event.as_ptr() as *const Connection) }; | ||
println!( | ||
"{} access from {:?}", | ||
if conn.allowed == 1 { | ||
"Allowed" | ||
} else { | ||
"Blocked" | ||
}, | ||
Ipv4Addr::from(conn.source_ip) | ||
); | ||
} | ||
_ => panic!("unexpected event"), | ||
} | ||
} | ||
} | ||
}); | ||
|
||
signal::ctrl_c().await | ||
}); | ||
} | ||
|
||
struct Opts { | ||
interface: String, | ||
knock: Vec<u16>, | ||
port: u16, | ||
} | ||
|
||
fn parse_opts() -> Option<Opts> { | ||
let args: Vec<String> = env::args().collect(); | ||
let program = args[0].clone(); | ||
|
||
let mut opts = Options::new(); | ||
opts.optmulti( | ||
"k", | ||
"knock", | ||
&format!( | ||
"TCP port on which peers have to knock on, can be used up to {} times", | ||
MAX_SEQ_LEN | ||
), | ||
"KNOCK", | ||
); | ||
opts.reqopt( | ||
"p", | ||
"port", | ||
"the port to open on completion of the given knock sequence", | ||
"PORT", | ||
); | ||
opts.reqopt( | ||
"i", | ||
"interface", | ||
"the network interface to listen on", | ||
"INTERFACE", | ||
); | ||
opts.optflag("h", "help", "print this help menu"); | ||
|
||
let matches = match opts.parse(&args[1..]) { | ||
Ok(m) => m, | ||
Err(f) => { | ||
eprintln!("{}\n", f); | ||
print_usage(&program, opts); | ||
return None; | ||
} | ||
}; | ||
|
||
if matches.opt_present("h") { | ||
print_usage(&program, opts); | ||
return None; | ||
} | ||
let interface = matches.opt_str("i"); | ||
let knock = matches.opt_strs("k"); | ||
let port = matches.opt_str("p"); | ||
if interface.is_none() || knock.is_empty() || port.is_none() { | ||
print_usage(&program, opts); | ||
return None; | ||
}; | ||
|
||
if knock.len() > MAX_SEQ_LEN { | ||
eprintln!( | ||
"Knock sequence too long: {} maximum is {}", | ||
knock.len(), | ||
MAX_SEQ_LEN | ||
); | ||
return None; | ||
} | ||
|
||
let knock = knock.iter().map(|p| p.parse::<u16>().unwrap()).collect(); | ||
let port = port.unwrap().parse::<u16>().unwrap(); | ||
|
||
Some(Opts { | ||
interface: interface.unwrap(), | ||
knock, | ||
port, | ||
}) | ||
} | ||
|
||
fn print_usage(program: &str, opts: Options) { | ||
let brief = format!("Usage: {} [options]", program); | ||
print!("{}", opts.usage(&brief)); | ||
} |