Skip to content

Instantly share code, notes, and snippets.

@miminashi
Last active September 21, 2025 09:04
Show Gist options
  • Select an option

  • Save miminashi/f42d8a47de59f9e53b5e6684db83792c to your computer and use it in GitHub Desktop.

Select an option

Save miminashi/f42d8a47de59f9e53b5e6684db83792c to your computer and use it in GitHub Desktop.
UDPでイーサネットフレームをカプセル化するだけのインチキVPN by Gemini
簡易VPNプログラム (simple_vpn)
概要
このプログラムは、VPNの基本的な原理(L2 over UDP)を学習するために作成された、非常にシンプルなVPNもどきです。
指定した2つのホスト間で、仮想的なイーサネットセグメントを構築します。
具体的には、片方のホストのTAPデバイス(仮想ネットワークインターフェース)を通過するイーサネットフレームをキャプチャし、UDPパケットに詰めて(カプセル化して)もう片方のホストに送信します。受信側は、UDPパケットから元のイーサネットフレームを取り出し、自身のTAPデバイスに書き込みます。これにより、あたかも2つのホストが同じLANに接続されているかのように通信できます。
注意: このプログラムには認証も暗号化も実装されていません。通信内容はすべて平文でネットワーク上を流れます。学習目的でのみ使用してください。
動作環境
* Linux
* Cコンパイラ (gccなど)
コンパイル方法
gcc -o simple_vpn simple_vpn.c
使い方
このプログラムを実行するには、2台のホスト(ホストA, ホストB)が必要です。
以下の手順は、両方のホストで root 権限で実行する必要があります。
1. ネットワーク設定
ホストA
(物理IPアドレスが 192.168.1.10 の場合)
# TAPデバイスを作成
# (プログラムが自動で作成しますが、事前に手動で作成・設定することも可能です)
# ip tuntap add dev tap0 mode tap user $(whoami)
# TAPデバイスにIPアドレスを割り当てる
ip addr add 10.0.0.1/24 dev tap0
# TAPデバイスを有効化する
ip link set tap0 up
ホストB
(物理IPアドレスが 192.168.1.20 の場合)
# TAPデバイスにIPアドレスを割り当てる
ip addr add 10.0.0.2/24 dev tap0
# TAPデバイスを有効化する
ip link set tap0 up
*10.0.0.0/24 の部分は、既存のネットワークと重複しないプライベートIPアドレス空間を任意に選んでください。
2. プログラムの実行
作成した simple_vpn を実行します。コマンドライン引数には、相手ホストの物理IPアドレスを指定します。
ホストA
相手(ホストB)のIPアドレス 192.168.1.20 を指定します。
sudo ./simple_vpn 192.168.1.20
ホストB
相手(ホストA)のIPアドレス 192.168.1.10 を指定します。
sudo ./simple_vpn 192.168.1.10
sudo が必要なのは、TAPデバイス /dev/net/tun にアクセスするために特権が必要だからです。
3. 動作確認
プログラムが両方のホストで起動したら、新しいターミナルを開き、TAPデバイスに割り当てたIPアドレスに対して ping を実行します。
ホストAからホストBへ
ping 10.0.0.2
ホストBからホストAへ
ping 10.0.0.1
ping の応答があれば、仮想的なネットワークが正しく構築されています。simple_vpn を実行しているターミナルには、パケットを送受信するたびにログが表示されます。
仕組みのポイント
* TAPデバイス: tap0 は、OSカーネルが提供する仮想的なネットワークインターフェースです。このインターフェースに書き込まれたデータはイーサネットフレームとして扱われ、このインターフェースから読み出されるデータもイーサネットフレームです。
* カプセル化: プログラムはtap0からイーサネットフレームを丸ごと読み込み、それをUDPパケットのペイロード(データ部分)として相手に送信します。
* select()システムコール: tap0からの入力と、UDPソケットからの入力を同時に待ち受ける(I/O多重化)ためにselect()を使用しています。これにより、効率的に双方向の通信を実現しています。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <net/if.h>
#include <linux/if_tun.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <errno.h>
#define BUFSIZE 2000
#define PORT 5555
#define TAP_DEVICE_NAME "tap0"
/**
* @brief TAPデバイスを作成し、ファイルディスクリプタを返す
* @param dev デバイス名 (e.g., "tap0")
* @return 成功した場合はファイルディスクリプタ、失敗した場合は-1
*/
int tap_alloc(char *dev) {
struct ifreq ifr;
int fd, err;
char *clonedev = "/dev/net/tun";
// /dev/net/tunキャラクタデバイスを開く
if ((fd = open(clonedev, O_RDWR)) < 0) {
perror("Opening /dev/net/tun");
return fd;
}
// ifreq構造体をゼロクリア
memset(&ifr, 0, sizeof(ifr));
/*
* IFF_TAP: TAPデバイス(イーサネットフレーム)を指定
* IFF_NO_PI: パケット情報ヘッダを付けない
*/
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
// デバイス名をifr構造体にコピー
if (*dev) {
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
}
// ioctlでカーネルにTAPデバイスの作成を要求
if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
perror("ioctl(TUNSETIFF)");
close(fd);
return err;
}
// 実際に作成されたデバイス名がifr.ifr_nameに返ってくるのでコピー
strcpy(dev, ifr.ifr_name);
return fd;
}
int main(int argc, char *argv[]) {
int tap_fd, sock_fd;
struct sockaddr_in local, remote;
char tap_name[IFNAMSIZ];
unsigned char buffer[BUFSIZE];
// --- コマンドライン引数のチェック ---
if (argc != 2) {
fprintf(stderr, "Usage: %s <remote_ip>\n", argv[0]);
exit(1);
}
char *remote_ip = argv[1];
// --- TAPデバイスの初期化 ---
strcpy(tap_name, TAP_DEVICE_NAME);
tap_fd = tap_alloc(tap_name);
if (tap_fd < 0) {
fprintf(stderr, "Failed to create TAP device.\n");
exit(1);
}
printf("Successfully created TAP device: %s\n", tap_name);
// --- UDPソケットの初期化 ---
// ソケット作成
if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket()");
exit(1);
}
// 自ホストのアドレス設定
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(PORT);
// ソケットにアドレスをバインド
if (bind(sock_fd, (struct sockaddr*)&local, sizeof(local)) < 0) {
perror("bind()");
exit(1);
}
// 相手ホストのアドレス設定
memset(&remote, 0, sizeof(remote));
remote.sin_family = AF_INET;
remote.sin_port = htons(PORT);
if (inet_aton(remote_ip, &remote.sin_addr) == 0) {
fprintf(stderr, "Invalid remote IP address\n");
exit(1);
}
printf("Connecting to remote peer at %s:%d\n", remote_ip, PORT);
// --- メインループ (I/O多重化) ---
while (1) {
fd_set read_fds;
int max_fd;
FD_ZERO(&read_fds);
FD_SET(tap_fd, &read_fds);
FD_SET(sock_fd, &read_fds);
max_fd = (tap_fd > sock_fd) ? tap_fd : sock_fd;
// tapデバイスかUDPソケットのどちらかでデータが読み込み可能になるまで待機
if (select(max_fd + 1, &read_fds, NULL, NULL, NULL) < 0) {
perror("select()");
exit(1);
}
// --- TAPデバイスから読み込み -> UDPソケットへ書き込み ---
if (FD_ISSET(tap_fd, &read_fds)) {
int nread = read(tap_fd, buffer, BUFSIZE);
if (nread < 0) {
perror("read() from TAP");
exit(1);
}
// 読み込んだイーサネットフレームをUDPで相手に送信
int nwrite = sendto(sock_fd, buffer, nread, 0, (struct sockaddr*)&remote, sizeof(remote));
if (nwrite < 0) {
perror("sendto()");
exit(1);
}
printf("TAP -> UDP: %d bytes sent\n", nwrite);
}
// --- UDPソケットから読み込み -> TAPデバイスへ書き込み ---
if (FD_ISSET(sock_fd, &read_fds)) {
socklen_t remote_len = sizeof(remote);
int nread = recvfrom(sock_fd, buffer, BUFSIZE, 0, NULL, NULL);
if (nread < 0) {
perror("recvfrom()");
exit(1);
}
// 受信したデータをTAPデバイスに書き込む
int nwrite = write(tap_fd, buffer, nread);
if (nwrite < 0) {
perror("write() to TAP");
exit(1);
}
printf("UDP -> TAP: %d bytes written\n", nwrite);
}
}
close(sock_fd);
close(tap_fd);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment