初めに
前回はソケットを用いてHTTPリクエストを送信する実験を行いました。
(参考)https://qiita.com/earthen94/items/2ba51c53671f7fc4f2b4
今回は通信の仕組みをより深く理解する為、自分でIPおよびTCPヘッダを組み立て、もう一台のパソコンへTCPのSYNパケットを送信する実験を行います。
前提知識
IP (ネットワーク層)
IPアドレスを以て宛先を指定し、機器間の通信を実現する。
信頼性なし。パケットが紛失したり、順序が前後する可能性がある。
ただ送信するのみで事前に接続を確立することはない。
TCP (トランスポート層)
ポート番号を以てアプリケーション間の通信を実現する。
信頼性あり。パケットの再送、順序制御、誤り検出を行う。
データ送信の前に接続を確立する。(TCP 3ウェイハンドシェイク)
実験
送信元PC
ソースをコンパイルし実行する。
test@test-ThinkPad-X280:~/tcpip$ gcc raw_syn.c -o raw_syn
test@test-ThinkPad-X280:~/tcpip$ sudo ./raw_syn
SYN packet sent
受信PC
IPアドレスを確認する。
tcpdumpを使い受信したパケットを即時で表示する
ubuntu@ubuntu:~$ hostname -I
192.168.68.96
ubuntu@ubuntu:~$ ip link show
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp3s0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc fq_codel state DOWN mode DEFAULT group default qlen 1000
link/ether 2c:f0:5d:68:df:5b brd ff:ff:ff:ff:ff:ff
3: wlo1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DORMANT group default qlen 1000
link/ether a4:b1:c1:a2:9b:a3 brd ff:ff:ff:ff:ff:ff
altname wlp0s20f3
ubuntu@ubuntu:~$ sudo tcpdump -i wlo1 tcp and port 80
C
少し長いですがやっていることは単純です。
datagram(IPヘッダ+TCPヘッダが連続したバッファ)に必要なデータを詰めて宛先のPC(192.168.68.96 80番ポート)へ送信しているだけです。
raw_syn.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/ip.h> // IPヘッダ用
#include <netinet/tcp.h> // TCPヘッダ用
#include <arpa/inet.h>
#include <netinet/in.h>
// チェックサム計算(IPヘッダとTCPヘッダで使う)
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 + (sum >> 16);
answer=(short)~sum;
return(answer);
}
int main() {
// 送信先IP、送信元IPは適宜書き換える(筆者はhostname -Iで確認した)
char source_ip[20] = "192.168.68.245"; // 自分のマシンのIPアドレスに変更
char dest_ip[20] = "192.168.68.96"; // 宛先IPアドレス
// raw socket作成
int s = socket (PF_INET, // IPv4
SOCK_RAW, // RAWにすることでIPヘッダやTCPヘッダを自由に作成・操作可能となる
IPPROTO_TCP // TCP指定
);
if(s < 0) {
perror("Socket error");
exit(1);
}
// IPヘッダ+TCPヘッダを格納するバッファ
char datagram[4096];
// バッファを0で埋める
memset(datagram, 0, 4096);
// IPヘッダポインタ
struct iphdr *iph = (struct iphdr *) datagram;
// TCPヘッダポインタ(IPヘッダの後ろ)
struct tcphdr *tcph = (struct tcphdr *) (datagram + sizeof(struct iphdr));
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(80); // HTTPのポート
sin.sin_addr.s_addr = inet_addr(dest_ip);
/*
誤解し易いので注記
iph,tcphポインタはdatagramを指しており、iph->(要素)へ値を代入すると
datagramへ直接値を書き込んでいることになる。
*/
// IPヘッダ作成
iph->ihl = 5; // ヘッダ長(5×4=20バイト) (標準の長さ)
iph->version = 4; // IPv4
iph->tos = 0; // 今は殆ど使われず0で問題ない模様
iph->tot_len = htons(sizeof(struct iphdr) + sizeof(struct tcphdr)); // パケット全体の長さ(IPヘッダ+データ、バイト単位)
iph->id = htons(54321);
iph->frag_off = 0;
iph->ttl = 64; // 生存時間
iph->protocol = IPPROTO_TCP; // TCPは6、UDPは17
iph->check = 0; // 計算前は0
iph->saddr = inet_addr(source_ip); // 送信元IPアドレス
iph->daddr = sin.sin_addr.s_addr; // 宛先IPアドレス
// IPヘッダチェックサム計算
iph->check = csum((unsigned short *)datagram, iph->ihl<<2);
// TCPヘッダ作成
tcph->source = htons(12345); // 適当な送信元ポート番号
tcph->dest = htons(80); // 宛先ポート番号
tcph->seq = htonl(0);
tcph->ack_seq = 0; // 0(SYN送信時はまだACKなし)
tcph->doff = 5; // TCPヘッダ長(20バイト)
tcph->syn = 1; // SYNフラグセット 1 → SYNパケットとして送る(接続開始の合図)
tcph->window = htons(5840);
tcph->check = 0; // 計算前は0
tcph->urg_ptr = 0; // 0 → 緊急データなし
// TCPチェックサム計算用の擬似ヘッダを作る構造体
struct pseudo_header {
unsigned int source_address;
unsigned int dest_address;
unsigned char placeholder;
unsigned char protocol;
unsigned short tcp_length;
};
// 擬似ヘッダとTCPヘッダを結合したバッファ
char pseudo_packet[4096];
struct pseudo_header psh;
psh.source_address = inet_addr(source_ip);
psh.dest_address = sin.sin_addr.s_addr;
psh.placeholder = 0;
psh.protocol = IPPROTO_TCP;
psh.tcp_length = htons(sizeof(struct tcphdr));
// 擬似ヘッダをコピー
memcpy(pseudo_packet, &psh, sizeof(struct pseudo_header));
// TCPヘッダを擬似ヘッダの後にコピー
memcpy(pseudo_packet + sizeof(struct pseudo_header), tcph, sizeof(struct tcphdr));
tcph->check = csum((unsigned short*) pseudo_packet, sizeof(struct pseudo_header) + sizeof(struct tcphdr));
// 通常のソケット通信では、IPヘッダはOS(カーネル)が自動で付け加える。
// Raw Socketで自分でIPヘッダを作る場合は、
// カーネルに二重でIPヘッダを付けられないように設定をする必要がある。
// IPPROTO_IP:IP層の設定
// IP_HDRINCL:1(true)に設定すると、自分でIPヘッダを作成して送ることを意味する
// oneはただの有効(true)
// setsockoptのoptvalはvoidポインタなので、値を直接渡せず
// 値のアドレス(ポインタ)を渡す必要があるというCの関数仕様によるもの
int one = 1;
const int *val = &one;
if (setsockopt (s, IPPROTO_IP, IP_HDRINCL, val, sizeof(one)) < 0) {
perror("setsockopt() error");
exit(1);
}
// パケット送信
if (sendto (s, datagram, ntohs(iph->tot_len), 0, (struct sockaddr *) &sin, sizeof(sin)) < 0) {
perror("sendto failed");
} else {
printf ("SYN packet sent\n");
}
close(s);
return 0;
}


