これは何ですか?
3の倍数と3がつくシーケンス番号のときだけアホになる(遅延する)ルーターをつくった
そのヘンテコなルーターの紹介記事です!
Dockerでつくったので、掲載のソースコードをコピペすれば、誰でも再現可能かと思いますので、ぜひに!
完成品はコチラ★
画面左がルーター / 画面右がクライアント(PINGを打つ係)
ナベアツ式ルールに従って、アホになっている(1秒遅延している)
アホじゃないときは、1ミリ秒以下の応答ですね
お待ちかねのゾーン🐥 30~39は全部アホになる
ちゃんと(?)、1秒遅延してからPINGを転送している
なぜつくった?
- Rust勉強のため(ネットワークプログラミングを勉強中)
- ルーター自作でわかるパケットの流れを読んで、回線遅延装置をつくってみたくなったため(その足掛かりとして)
詳細
- 先にお見せしたルーターやPING打つクライアントは、すべてDockerコンテナ
- ネットワークも物理的な配線などは一切なく、すべてDockerネットワーク内の話
前回記事で、すでにDockerネットワークを作成してPINGの疎通確認まで終えている
今回はその続きとして、主にルータをアホにするための細工を施していく
ネットワーク構成図
端末 | ネットワーク | MACアドレス |
---|---|---|
クライアント1号機 | 172.23.1.10/24 | 02:4217:01:0a |
ルータ | 172.23.1.250/24 | 02:4217:01:fa |
ルータ | 172.23.2.250/24 | 02:4217:02:fa |
クライアント2号機 | 172.23.2.10/24 | 02:4217:02:0a |
(後述しますが)、、
今回のルーターはインチキ仕様で、ARPをせずにMACアドレスをハードコーディングしているため、ここでMACアドレスをよく調べておく必要がある
:ac:
がになりますね。MACアドレスにac
が含まれると国旗に変換されてしまいますね。
Dockerfile
- クライアントのDockerfileは、前回記事と同じ
- ルーターはDockerfileを使わない。公式のRustイメージを使う。
entrypoint.sh
ルータは、起動後にcargo run
でRustのプログラムを実行する
プログラムはルータの役割として常駐し続ける
#!/bin/bash
cd /mnt
cargo run eth0 eth1
ルータは2つのネットワークに所属しており、NICを2つ備える
eth0
とeth1
が2つのNICであり、片方から受けたパケットを、もう片方のNICへ転送する
それぞれのNICのIPアドレスとMACアドレスは先述の表のとおり
(NIC余談)Windowsの場合
Linuxではeth0
であるが、Windowsの場合は\\Device\\NPF_{A????????-F???-????-????-E??????}
のような長い文字列になる
具体的にどういう文字列になるかは、以下の記事のコードを実行すればわかる
docker-compose.yml
ポイント
- ルータマシンはデフォルトでポートフォワードが有効になっているので
sysctls
をnet.ipv4.ip_forward=0
とする
version: '3.9'
services:
client1:
build: ./client1
container_name: client1
hostname: client1
tty: true
stdin_open: true
privileged: true
volumes:
- ./client1/mnt:/mnt
networks:
delay-net1:
ipv4_address: 172.23.1.10
entrypoint: /mnt/entrypoint.sh
client2:
build: ./client2
container_name: client2
hostname: client2
tty: true
stdin_open: true
privileged: true
volumes:
- ./client2/mnt:/mnt
networks:
delay-net2:
ipv4_address: 172.23.2.10
entrypoint: /mnt/entrypoint.sh
router:
image: rust
container_name: router
hostname: router
volumes:
- ./:/mnt
networks:
delay-net1:
ipv4_address: 172.23.1.250
delay-net2:
ipv4_address: 172.23.2.250
sysctls:
- net.ipv4.ip_forward=0
entrypoint: /mnt/router/mnt/entrypoint.sh
networks:
delay-net1:
ipam:
driver: default
config:
- subnet: 172.23.1.0/24
delay-net2:
ipam:
driver: default
config:
- subnet: 172.23.2.0/24
クライアントの設定ポイントは前回記事を参照のこと
Rustソースコードmain.rs
Rustで始めるネットワークプログラミングのソースコードを大いに参考にさせていただいた
Rustのスレッドもよくわかっておらず、とりあえず動けばいいコードなので悪しからず
ARPは実装しておらず、Dockerネットワークをつくった段階でMACアドレスを調べて
MACアドレスをハードコーディングするという荒業
use log::info;
use once_cell::sync::Lazy;
use pnet::datalink::NetworkInterface;
use pnet::datalink::{self, Channel::Ethernet};
use pnet::packet::ip::IpNextHeaderProtocol;
use pnet::packet::{ethernet::EthernetPacket, ipv4::Ipv4Packet, Packet};
use std::{env, net::IpAddr};
use std::{thread, time};
static INTERFACE: Lazy<Vec<NetworkInterface>> = Lazy::new(|| pnet::datalink::interfaces());
fn main() {
env::set_var("RUST_LOG", "debug");
env_logger::init();
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
log::error!("Please specify target interface name");
std::process::exit(1);
}
let interface_name1 = &args[1];
let interface_name2 = &args[2];
let interface1 = INTERFACE
.iter()
.find(|iface| iface.name == *interface_name1)
.expect("Failed to get interface1");
let interface2 = INTERFACE
.iter()
.find(|iface| iface.name == *interface_name2)
.expect("Failed to get interface2");
let (mut tx, mut rx) = match datalink::channel(interface1, Default::default()) {
Ok(Ethernet(tx, rx)) => (tx, rx),
Ok(_) => panic!("Unhandled channel type"),
Err(e) => panic!(
"An error occurred when creating the datalink channel: {}",
e
),
};
let (mut txx, mut rxx) = match datalink::channel(interface2, Default::default()) {
Ok(Ethernet(tx, rx)) => (tx, rx),
Ok(_) => panic!("Unhandled channel type"),
Err(e) => panic!(
"An error occurred when creating the datalink channel: {}",
e
),
};
let handle1 = thread::spawn(move || {
let wait_time = time::Duration::from_millis(1000);
let mut x = 1;
loop {
match rx.next() {
Ok(packet_in) => {
let packet = EthernetPacket::new(packet_in).unwrap();
let ip_packet = Ipv4Packet::new(packet.payload());
if let Some(ip_packet) = ip_packet {
println!(
"{0} {1} -> {2} {3}\t{4}",
IpAddr::V4(ip_packet.get_source()),
packet.get_source(),
IpAddr::V4(ip_packet.get_destination()),
packet.get_destination(),
ip_packet.get_next_level_protocol(),
);
if ip_packet.get_next_level_protocol() == IpNextHeaderProtocol(1) {
if x % 3 == 0 || x / 10 % 10 == 3 || x % 10 == 3 {
thread::sleep(wait_time);
info!("🤪<<「{}」", x);
} else {
info!("😐<<「{}」", x);
}
x += 1;
}
}
let mut v1 = vec![2, 66, 172, 23, 2, 10, 2, 66, 172, 23, 2, 250];
v1.extend(&packet_in[12..]);
txx.send_to(&v1, Some(interface2.to_owned()));
}
Err(e) => {
panic!("An error occurred while reading: {}", e);
}
}
}
});
let handle2 = thread::spawn(move || loop {
match rxx.next() {
Ok(packet_in) => {
let packet = EthernetPacket::new(packet_in).unwrap();
let ip_packet = Ipv4Packet::new(packet.payload());
if let Some(ip_packet) = ip_packet {
println!(
"{0} {1} -> {2} {3}\t{4}",
IpAddr::V4(ip_packet.get_source()),
packet.get_source(),
IpAddr::V4(ip_packet.get_destination()),
packet.get_destination(),
ip_packet.get_next_level_protocol(),
);
}
let mut v1 = vec![2, 66, 172, 23, 1, 10, 2, 66, 172, 23, 1, 250];
v1.extend(&packet_in[12..]);
tx.send_to(&v1, Some(interface2.to_owned()));
}
Err(e) => {
panic!("An error occurred while reading: {}", e);
}
}
});
handle1.join().unwrap();
handle2.join().unwrap();
}
宛先MACアドレスと送信元MACアドレスは、u8配列の上位12要素目までにある
本来はARPテーブルを使うところを直接書き換えている
// MACアドレスハードコーディング
let mut v1 = vec![2, 66, 172, 23, 1, 10, 2, 66, 172, 23, 1, 250];
// 入ってきたパケットの上位12バイトを書き換え
v1.extend(&packet_in[12..]);
dependenciesは以下の通り
[dependencies]
default-net = "0.15"
pnet = "0.33.0"
log = "0.4.19"
env_logger = "0.10.0"
once_cell = "1.18.0"
おわりに
Dockerコンテナ上で、Rustイメージをベースとしたルータマシンをつくった
ICMPパケットを受けた回数をカウントして
ナベアツ式ルールによってパケット転送をわざと遅らせてみた
とりあえずやりたいことができたので
今後はRustをきれいに書けるようにしていきつつ
ARPやらNATやらをエセ実装していきたい
参考記事
2024年3月12日追記
もう少し真面目に、現実的に使う場合について
TCPパケットを遅延させる方法
本文では、ICMPだけが遅延される
なぜならば、下記コードの if 分の右辺がIpNextHeaderProtocol(1)
となっているから
if ip_packet.get_next_level_protocol() == IpNextHeaderProtocol(1) {
thread::sleep(wait_time);
}
IpNextHeaderProtocolのカッコ内が 6
であれば、それはTCPを表す
ソースコードにそう書いてある
ちなみ 17
は UDP である
ハードコーディングしたMACアドレスの見方
先頭の6バイトが宛先MACアドレス、後半の6バイトが送信元MACアドレス
後半はおのずと中継ルーターのMACアドレスになる
let mut v1 = vec![/*宛先MACアドレス*/2, 66, 172, 23, 2, 10, /*ここから送信元MACアドレス*/2, 66, 172, 23, 2, 250];