prepare
tcp
rfc793
Transmission Control Protocol [Page ii]
rfc7414
A Roadmap for Transmission Control Protocol (TCP)
rfc2398
Some Testing Tools for TCP Implementors
rfc2525
2 . Known implementation problems
ip
pnet
tun/tap
tun/tap
Universal TUN/TAP device driver ¶
Let’s do it
part 1 make it work
- 新建一个rust项目
cargo new nb_trust –bin
添加依赖
[dependencies]
tun-tap = “0.1.2”
代码
use std::io;
fn main() -> io::Result<()> {
let nic = tun_tap::Iface::new("tun0", tun_tap::Mode::Tun)?;
let mut buf = [0u8; 1504];
let nbytes = nic.recv(&mut buf[..])?;
eprintln!("read {} bytes: {:x?}", nbytes, &buf[..nbytes]);
Ok(())
}
编译rust
cargo b --release
sudo setcap cap_net_admin=eip ./target/release/nb_trust
设置环境
sudo ip addr add 192.168.0.1/24 dev tun0
sudo ip link set up dev tun0
测试
nb_trust git:(master) ✗ sudo ./target/release/nb_trust
read 52 bytes: [0, 0, 86, dd, 60, 0, 0, 0, 0, 8, 3a, ff, fe, 80, 0, 0, 0, 0, 0, 0, 47, 29, a4, 4e, 5c, b7, 70, db, ff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 85, 0, c4, 2c, 0, 0, 0, 0]
so it is a little annoying here, we would like to write a bash.
#!/bin/bash
cargo b --release
sudo setcap cap_net_admin=eip ./target/release/nb_trust
./target/release/nb_trust &
pid=$!
sudo ip addr add 192.168.0.1/24 dev tun0
sudo ip link set up dev tun0
wait $pid
part 2 recognizes ip protocol and filter
在part 1 我们已经可以收到包了,因为我们想要focus on tcp,所以ip相关的一些东西只需要识别出来就好。
让我们改造下我们的代码, 我们主要做以下几件事
- 持续地收包并打印出来
- 现在我们要处理的对象的认知要从一个1504的buf转为一个packet,我们都知道一个正常的packet的mtu是1500,这多出来的4字节实际上是一个packet header.
- 识别一些ip level的信息。
ping test
ping -I tun0 192.168.0.2
所以当我们可以连续不断地收到数据包之后, 我们不希望只是无序地打印出来,我们希望能够识别这些协议分别是什么?它带有怎样的数据。
由于4字节的头部信息由如下组成:
If flag IFF_NO_PI is not set each frame format is:
Flags [2 bytes]
Proto [2 bytes]
Raw protocol(IP, IPv6, etc) frame.
所以我们只需要解析这4字节即可知道flag和proto
以及,我们需要改造一下我们的run.sh, 做一些clean up的工作,以便我们进行重复测试。
trap "kill $pid" INT TERM
添加这条命令方便我们在脚本被中断时,优雅地终止其启动的子进程。关键在于正确管理 $pid
的生命周期,确保信号触发时能准确杀死目标进程。
测试:
read 48 bytes, flags: 0, proto: 86dd: [60, 0, 0, 0, 0, 8, 3a, ff, fe, 80, 0, 0, 0, 0, 0, 0, c0, ed, 51, f1, 2a, 79, 54, 90, ff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 85, 0, eb, 4e, 0, 0, 0, 0]
以下网址包含protocol number对应的协议:
但是86dd是什么?
测试
试一下ping呢?
read 84 bytes, flags: 0, proto: 800: [45, 0, 0, 54, fa, fd, 40, 0, 40, 1, be, 57, c0, a8, 0, 1, c0, a8, 0, 2, 8, 0, d, dd, 1d, be, 0, 5, 4a, 49, b3, 67, 0, 0, 0, 0, 3, dc, c, 0, 0, 0, 0, 0, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 1a, 1b, 1c, 1d, 1e, 1f, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 2a, 2b, 2c, 2d, 2e, 2f, 30, 31, 32, 33, 34, 35, 36, 37]
protocol 800
OH~这显然是一个ip包,所以我们现在识别出了ipv4包,并且可以先只处理ipv4包,这是一个好消息!
part 3 parse ip and tcp header
我们在part2已经成功识别到ip proto并过滤了其他类型的pkt了。所以现在可以识别一些ip proto的字段,由于我们的重点不在这里,我们可以借助一些前人已经写好的轮子。
在 crates.io 中 etherparse 看起来不错。将它加入配置中
etherparse = "0.8.0"
现在我们可以通过Ipv4HeaderSlice解析字段
match etherparse::Ipv4HeaderSlice::from_slice(&mutbuf[4..nbytes]) {
/* --snipped-- */
}
那么我们关注ip的哪些字段呢?我们可以看一下ip头部都带有哪些信息:
RFC 791:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 元组信息,在ip头部中肯定是原地址和目的地址
- payload 的长度
So, now our code turns to this
match etherparse::Ipv4HeaderSlice::from_slice(&mut buf[4..nbytes]) {
Ok(p) => {
let src = p.source_addr();
let dst = p.destination_addr();
let proto = p.protocol();
let payload_len = p.payload_len();
eprintln!(
"{} -> {} {}b of protocol {}",
src, dst, payload_len, proto,
);
}
Err(e) => {
eprintln!("ignoreing weired packet {:?}", e);
continue;
}
}
测试
- ping: ping -I tun0 192.168.0.2
192.168.0.1 -> 192.168.0.2 64b of protocol 1
- nc tcp: nc 192.168.0.2 80
192.168.0.1 -> 192.168.0.2 40b of protocol 6
我们以及知道proto 1代表icmp, 那proto 6代表tcp么?
是的,以及udp的proto number为17.
所以这实际上是我们这个项目的开始。
我们不会专注于解释tcp header,而是实现protocol。
所以我们可以增加对tcpHeader的解释,直接调库即可
match etherparse::TcpHeaderSlice::from_slice(&mut buf[ETH_HEADER_BYTES + p.slice().len()..nbytes]) {
Ok(p) => {
eprintln!(
"{} -> {} {}b of tcp from port {} to {}",
src,
dst,
payload_len,
p.source_port(),
p.destination_port(),
);
}
Err(e) => {
eprintln!("ignoreing weired packet {:?}", e);
continue;
}
}
测试:
192.168.0.1 -> 192.168.0.2 40b of tcp from port 17664 to 60
继续解析tcp heder
如何标识一个tcp会话(session)?一个明显的特征是四元组,即
src_ip, src_port, dst_ip, dst_port
所以我们可以以四元组构造一个Quad结构,后面会用到
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)]
struct Quad {
src: (Ipv4Addr, u16),
dst: (Ipv4Addr, u16),
}
将tcp解析相关的代码组成一个mod
mod tcp
pubstruct State {}
impl Default for State {
fn default() -> Self {
Self {}
}
}
impl State {
pubfnon_packet<'a>(
&mutself,
iph: etherparse::Ipv4HeaderSlice<'a>,
tcph: etherparse::TcpHeaderSlice<'a>,
data: &'a [u8],
) {
eprintln!(
"{}:{} -> {}:{} {}b of tcp",
iph.source_addr(),
tcph.source_port(),
iph.destination_addr(),
tcph.destination_port(),
data.len(),
);
}
}
在解析ipheader后我们就可以解析tcpheader了
match etherparse::Ipv4HeaderSlice::from_slice(&buf[ETH_HEADER_BYTES..nbytes]) {
Ok(iph) => {
let src = iph.source_addr();
let dst = iph.destination_addr();
let proto = iph.protocol();
if proto != PROTO_TCP {
/* only handle tcp now */
eprintln!("ignoreing packet proto {:?}", proto);
continue;
}
let ip_hdr_size = iph.slice().len();
match etherparse::TcpHeaderSlice::from_slice(
&buf[ETH_HEADER_BYTES + iph.slice().len()..nbytes],
) {
Ok(tcph) => {
let datai = ETH_HEADER_BYTES + ip_hdr_size + tcph.slice().len();
connections
.entry(Quad {
src: (src, tcph.source_port()),
dst: (dst, tcph.destination_port()),
})
.or_default()
.on_packet(iph, tcph, &buf[datai..nbytes]);
}
Err(e) => {
eprintln!("ignoreing weired packet {:?}", e);
continue;
}
}
}
Err(e) => {
eprintln!("ignoreing weired packet {:?}", e);
continue;
}
}
测试
nc 192.168.0.2 443
192.168.0.1:48814 -> 192.168.0.2:443 0b of tcp
修改一下我们的run脚本,使得只有编译成功的时候才配置相关环境
#!/bin/bash
cargo b --release
ext=$?
if [[ $ext -ne 0 ]]; then
exit $ext
fi
sudo setcap cap_net_admin=eip ./target/release/nb_trust
./target/release/nb_trust &
pid=$!
sudo ip addr add 192.168.0.1/24 dev tun0
sudo ip link set up dev tun0
trap "kill $pid" INT TERM
wait $pid
part 4 tcp protocol startup
对于tcp来说最重要的是其控制协议