3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++で仮想NICを作る(Linux)

Last updated at Posted at 2025-12-06

はじめに

この記事では、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のプログラム (パケットを待ち受ける)

コマンドライン引数でデバイス名を設定します。

vnic.cpp
#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パケットの上にメッセージを付けて送ります。

sender.cpp
#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を作りました。

需要あったらいいな

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?