Skip to content
This repository has been archived by the owner on Jul 11, 2023. It is now read-only.

Commit

Permalink
tools: add redbpf-tcp-knock, a simple TCP knocking tool
Browse files Browse the repository at this point in the history
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
alessandrod committed Mar 3, 2020
1 parent 58f23c0 commit d8db68e
Show file tree
Hide file tree
Showing 6 changed files with 372 additions and 0 deletions.
6 changes: 6 additions & 0 deletions redbpf-tools/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,9 @@ probes = {path = "./probes" }
redbpf = { version = "^0.9.11", features = ["build", "load"], path = "../redbpf" }
tokio = { version = "0.2.4", features = ["rt-core", "io-driver", "macros", "signal", "time"] }
futures = "0.3"
getopts = "0.2"

[[bin]]
name = "knock"
path = "src/knock/main.rs"
required-features = ["probes"]
5 changes: 5 additions & 0 deletions redbpf-tools/probes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ path = "src/lib.rs"
name = "iotop"
path = "src/iotop/main.rs"
required-features = ["probes"]

[[bin]]
name = "knock"
path = "src/knock/main.rs"
required-features = ["probes"]
125 changes: 125 additions & 0 deletions redbpf-tools/probes/src/knock/main.rs
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;
}
58 changes: 58 additions & 0 deletions redbpf-tools/probes/src/knock/mod.rs
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,
}
1 change: 1 addition & 0 deletions redbpf-tools/probes/src/lib.rs
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;
177 changes: 177 additions & 0 deletions redbpf-tools/src/bin/redbpf-tcp-knock.rs
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));
}

0 comments on commit d8db68e

Please sign in to comment.