はじめに
この記事では、C++で仮想NICを作ります。
目標として、作った仮想NICにIPパケットを送ると、仮想NICのコンソールに送られてきたペイロードを表示させるまでやります。
前提条件
- Ubuntu 24.04.3 LTS
- gcc version 13.3.0
- x86_64
- UbuntuはWSLで動かしてます
TUN/TAP
TUN/TAPはUnix系のシステムで利用できる仮想的なネットワークインターフェイスです。(wiki)
今回はこれを使って実装します。
仮想NICのプログラム (パケットを待ち受ける)
コマンドライン引数でデバイス名を設定します。
#include <iostream>
#include <vector>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <iomanip>
#include <assert.h>
// MTUより大きく設定
const int DEFAULT_BUFFER_SIZE = 2048;
// 16進数とASCIIでダンプする関数
void hex_dump(const unsigned char *buffer, int length)
{
std::cout << "Packet received (" << length << " bytes):" << std::endl;
for (int i = 0; i < length; i += 16)
{
// オフセット表示
std::cout << std::hex << std::setw(4) << std::setfill('0') << i << ": ";
// 16進数部分
for (int j = 0; j < 16; ++j)
{
if (i + j < length)
{
std::cout << std::hex << std::setw(2) << std::setfill('0')
<< (int)buffer[i + j] << " ";
}
else
{
std::cout << " ";
}
}
std::cout << " | ";
// ASCII部分
for (int j = 0; j < 16; ++j)
{
if (i + j < length)
{
unsigned char c = buffer[i + j];
// 印字可能文字かチェック
if (c >= 32 && c <= 126)
{
std::cout << c;
}
else
{
std::cout << ".";
}
}
}
std::cout << std::dec << std::endl;
}
std::cout << "----------------------------------------------------" << std::endl;
}
int open_dev(const char *dev)
{
int fd = open(dev, O_RDWR);
assert(fd >= 0);
return fd;
}
void init_ifreq(struct ifreq *ifr, const char *device_name)
{
std::memset(ifr, 0, sizeof(struct ifreq));
// フラグ設定:
// IFF_TUN : IPパケット (Layer 3)
// IFF_NO_PI : パケット情報のヘッダを付与しない (純粋なIPパケットだけ受け取る)
ifr->ifr_flags = IFF_TUN | IFF_NO_PI;
// インターフェース名を指定 (例: tun0)
std::strncpy(ifr->ifr_name, device_name, IFNAMSIZ);
}
void create_tun(int fd, struct ifreq *ifr)
{
int err;
if ((err = ioctl(fd, TUNSETIFF, (void *)ifr)) < 0)
{
std::cerr << "Error creating TUN device." << std::endl;
close(fd);
exit(1);
}
}
int main(const int argc, const char *argv[])
{
struct ifreq ifr;
int fd, err;
const char *clonedev = "/dev/net/tun";
unsigned char buffer[DEFAULT_BUFFER_SIZE];
if (argc < 2)
{
std::cerr << "Usage: " << argv[0] << " <device_name>" << std::endl;
std::cerr << "Example: " << argv[0] << " vnic0" << std::endl;
return 1;
}
// TUNを開く
fd = open_dev(clonedev);
// ifreqの初期化
init_ifreq(&ifr, argv[1]);
// デバイスの作成 (登録)
create_tun(fd, &ifr);
std::cout << "Interface " << ifr.ifr_name << " created." << std::endl;
std::cout << "Please configure IP address in another terminal." << std::endl;
std::cout << "Waiting for packets..." << std::endl;
// パケット読み取りループ
while (true)
{
ssize_t nread = read(fd, buffer, sizeof(buffer));
if (nread < 0)
{
std::cerr << "Error reading from interface." << std::endl;
break;
}
// ペイロードの表示
hex_dump(buffer, nread);
}
close(fd);
return 0;
}
仮想NIC実行
sudo つけてください。
sudo ./vnic vnic0
ip a で仮想NICを確認
- 仮想NIC実行前
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet 10.255.255.254/32 brd 10.255.255.254 scope global lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1280 qdisc mq state UP group default qlen 1000
link/ether 00:15:5d:f5:bd:cd brd ff:ff:ff:ff:ff:ff
inet 172.26.85.222/20 brd 172.26.95.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::215:5dff:fef5:bdcd/64 scope link
valid_lft forever preferred_lft forever
- 仮想NIC実行中
$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet 10.255.255.254/32 brd 10.255.255.254 scope global lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1280 qdisc mq state UP group default qlen 1000
link/ether 00:15:5d:f5:bd:cd brd ff:ff:ff:ff:ff:ff
inet 172.26.85.222/20 brd 172.26.95.255 scope global eth0
valid_lft forever preferred_lft forever
inet6 fe80::215:5dff:fef5:bdcd/64 scope link
valid_lft forever preferred_lft forever
3: vnic0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 500
link/none
vnic0 というデバイスができました。IPアドレスが無いし起動してないですね。
IPアドレスを割り振って起動する
# IPアドレス (10.0.0.1) を割り当て
sudo ip addr add 10.0.0.1/24 dev vnic0
# インターフェースを起動
sudo ip link set up dev vnic0
3: vnic0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
link/none
inet 10.0.0.1/24 scope global vnic0
valid_lft forever preferred_lft forever
inet6 fe80::7ab9:3aa6:ac4d:768b/64 scope link stable-privacy
valid_lft forever preferred_lft forever
IPアドレスが割り当てられました。
仮想NICがなにかパケットを受け取ってます。
先頭4bitが6なのでIPv6です。宛先アドレスがFF02::2なのでRS(Router Solicitation)です。
Interface vnic0 created.
Please configure IP address in another terminal.
Waiting for packets...
Packet received (48 bytes):
0000: 60 00 00 00 00 08 3a ff fe 80 00 00 00 00 00 00 | `.....:.........
0010: 7a b9 3a a6 ac 4d 76 8b ff 02 00 00 00 00 00 00 | z.:..Mv.........
0020: 00 00 00 00 00 00 00 02 85 00 a4 fe 00 00 00 00 | ................
----------------------------------------------------
Packet received (48 bytes):
0000: 60 00 00 00 00 08 3a ff fe 80 00 00 00 00 00 00 | `.....:.........
0010: 7a b9 3a a6 ac 4d 76 8b ff 02 00 00 00 00 00 00 | z.:..Mv.........
0020: 00 00 00 00 00 00 00 02 85 00 a4 fe 00 00 00 00 | ................
----------------------------------------------------
邪魔なのでとりあえずIPv6を無効化します。デバイス名の指定に注意してください。
sudo sysctl -w net.ipv6.conf.vnic0.disable_ipv6=1
IPパケットを送る
socket 関数で生ソケットを作ってIPパケットの上にメッセージを付けて送ります。
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <assert.h>
#define MAX_MESSAGE_SIZE 1024
// チェックサム計算関数 (IPヘッダに必須)
unsigned short csum(unsigned short *ptr, int nbytes)
{
long sum;
unsigned short oddbyte;
short answer;
sum = 0;
while (nbytes > 1)
{
sum += *ptr++;
nbytes -= 2;
}
if (nbytes == 1)
{
oddbyte = 0;
*((unsigned char *)&oddbyte) = *(unsigned char *)ptr;
sum += oddbyte;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
answer = (short)~sum;
return answer;
}
void init_ipv4header(struct iphdr *iph, const char *source_ip, const char *dest_ip)
{
// IPヘッダの設定
iph->version = 4; // IPv4
iph->ihl = 5; // ヘッダ長 (5 * 32bit = 20 bytes)
iph->tos = 0; // Type of Service
// iph->tot_len = htons(sizeof(struct iphdr) + payload_len); // 全長
iph->id = htons(54321); // ID (適当な値)
iph->frag_off = 0; // フラグメントオフセット
iph->ttl = 255; // Time to Live
iph->protocol = 253; // プロトコル番号 (253, 254は実験用)
iph->check = 0; // チェックサム計算前は0にする
iph->saddr = inet_addr(source_ip);
iph->daddr = inet_addr(dest_ip);
}
int main(int argc, char *argv[])
{
if (argc != 3)
{
std::cerr << "Usage: " << argv[0] << " <Source IP> <Dest IP>" << std::endl;
std::cerr << "Example: " << argv[0] << " 10.0.0.1 10.0.0.2" << std::endl;
return 1;
}
const char *source_ip = argv[1];
const char *dest_ip = argv[2];
char *message_ptr;
// 生ソケットの作成 (AF_INET, SOCK_RAW, IPPROTO_RAW)
int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);
assert(sock >= 0);
// IP_HDRINCL: IPヘッダを自作することをカーネルに伝える
int one = 1;
assert(setsockopt(sock, IPPROTO_IP, IP_HDRINCL, &one, sizeof(one)) >= 0);
// パケットバッファの準備
char packet[4096];
memset(packet, 0, sizeof(packet));
// IPヘッダ構造体のポインタ設定
struct iphdr *iph = (struct iphdr *)packet;
init_ipv4header(iph, source_ip, dest_ip);
// 送信先構造体の設定 (sendto用)
struct sockaddr_in dest;
dest.sin_family = AF_INET;
dest.sin_addr.s_addr = inet_addr(dest_ip);
message_ptr = packet + sizeof(struct iphdr);
// ペイロード入力ループ
while (true)
{
// ペイロード文字列の入力
std::memset(message_ptr, 0, MAX_MESSAGE_SIZE);
std::cout << "Enter payload string (max " << MAX_MESSAGE_SIZE - 1 << " chars): ";
std::cin.getline(message_ptr, MAX_MESSAGE_SIZE);
if (std::cin.eof())
{
break;
}
// ペイロード長の計算とIPヘッダの更新
int payload_len = strlen(message_ptr);
if (payload_len + sizeof(struct iphdr) > sizeof(packet))
{
std::cerr << "Payload too large!" << std::endl;
continue;
}
iph->tot_len = htons(sizeof(struct iphdr) + payload_len);
iph->check = 0;
iph->check = csum((unsigned short *)packet, sizeof(struct iphdr));
// パケット送信
assert(sendto(sock, packet, ntohs(iph->tot_len), 0, (struct sockaddr *)&dest, sizeof(dest)) >= 0);
}
close(sock);
return 0;
}
実行
sudo つけて実行してください。第1引数が送信元で、第2引数が送信先アドレスです。送信元はいくつでもいいです。送信先は仮想NICのアドレスを10.0.0.1/24にしたため、10.0.0.0/24のネットワークのアドレスにしてください。
$ sudo ./sender 10.0.0.1 10.0.0.2
Enter payload string (max 1023 chars): 12345678
Enter payload string (max 1023 chars): abcdefgh
Enter payload string (max 1023 chars): aaaaaaaa
IPヘッダーの後ろにIPパケットより上のデータが見えますね。
Packet received (28 bytes):
0000: 45 00 00 1c d4 31 00 00 ff fd d2 b0 0a 00 00 01 | E....1..........
0010: 0a 00 00 02 31 32 33 34 35 36 37 38 | ....12345678
----------------------------------------------------
Packet received (28 bytes):
0000: 45 00 00 1c d4 31 00 00 ff fd d2 b0 0a 00 00 01 | E....1..........
0010: 0a 00 00 02 61 62 63 64 65 66 67 68 | ....abcdefgh
----------------------------------------------------
Packet received (28 bytes):
0000: 45 00 00 1c d4 31 00 00 ff fd d2 b0 0a 00 00 01 | E....1..........
0010: 0a 00 00 02 61 61 61 61 61 61 61 61 | ....aaaaaaaa
----------------------------------------------------
まとめ
本記事では、C++で仮想NICを作りました。
需要あったらいいな