データリンク層の Raw ソケットを作成してデータの送受信を行う


環境

  • Linux
  • Python 3.6 以上

今回は Linux と Python 3 を用いてデータリンクの raw ソケットを作成し,データの送受信を行う手順を説明します.
サンプルコードは ここ にあります.

ソケット API

ソケット API は,アプリケーションからプロセス間通信やネットワーク通信を行うための API です.
主に UNIX や Linux で利用可能です.
Linux では,ソケット API を用いることで比較的容易にデータリンク層を扱うことができます.
以降では,Linux のソケットについて説明します.

データリンク層を扱うソケットの作成

ソケット API を用いてデータの送受信を行うために,ソケットディスクリプタと呼ばれるものを生成する必要があります.
ソケットディスクリプタは,sys/socket.hint socket(int family, int type, int protocol) を使って生成することができます.
第一引数の family には,アドレスファミリーを指定します.
データリンク層を扱う場合は,sys/socket.h に定義されている AF_PACKET を指定します.
第二引数の type には,ソケットタイプを指定します.
データリンク層を扱う場合は,sys/socket.h に定義されている SOCK_RAW を指定します.
第三引数の protocol には,プロトコルを指定します.
すべての Ethernet フレームを取得するには,ETH_P_ALL を,IP パケットを含む Ethernet フレームを取得するには ETH_P_IP を指定します.
Ethernet フレームのプロトコルは,linux/if_ether.h に定義されています.

Python では,ソケット API をラップした標準ライブラリが用意されているので,これを用いてソケットを生成します.
ソケットの利用が終わったら close() するように注意してください.

import socket
ETH_P_ALL = 3
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
s.close()

ネットワークインターフェースの情報をソケットに結びつけるために bind() を利用します.
自身のコンピュータで利用可能なネットワークインターフェースは次のコマンドで確認できます.

$ ip link
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: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:dd:d7:43 brd ff:ff:ff:ff:ff:ff
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:b0:d6:ff brd ff:ff:ff:ff:ff:ff
4: enp0s9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:e6:4d:39 brd ff:ff:ff:ff:ff:ff
5: enp0s10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether 08:00:27:8e:75:44 brd ff:ff:ff:ff:ff:ff

例えば,ネットワークインターフェース enp0s3 をソケットに結びつけるためには,次のように記述します.

import socket
ETH_P_ALL = 3
interface = 'enp0s3'
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
s.bind((interface, 0))
# do something
s.close()

以上で,ネットワークインターフェース enp0s3 を用いてデータリンク層でデータを送受信する準備が整いました.

ソケットを使ったデータの受信

ソケットを使ってデータを受信するには recv() を利用します.
引数にはバッファサイズを指定します.
recv() は,ソケットからデータを受信可能になるまでブロッキングします.
recv() の返戻値は受信したバイト列です.
このバイト列は Ethernet フレームのヘッダとペイロードから構成されます.

enp0s3 に送信されたデータを受信する場合のスクリプトは次のようになります.

import socket
ETH_P_ALL = 3
interface = 'enp0s10'
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
s.bind((interface, 0))
data = s.recv(1514)
print(data)  # => b'\x08\x00\x27\xdd\xd7\x43\x08\x00\x27\x8e\x75\x44\x88\xb5Hi'
s.close()

実行には root 権限が必要ですので,上記のスクリプトは sudo をつけて実行するようにしてください.

データが enp0s3 に送信されると,次のような出力を得ることができます.

$ sudo python3 server.py
b'\x08\x00\x27\xdd\xd7\x43\x08\x00\x27\x8e\x75\x44\x88\xb5Hi'

ソケットを使ったデータの送信

ソケットを使ってデータを送信するには sendall() を利用します.
送信するデータは Ethernet フレームのヘッダとペイロードを含むバイト列でなければなりません.

enp0s10 (08:00:27:8e:75:44) から enp0s3 (08:00:27:dd:d7:43) にデータを送信する場合のスクリプトは次のようになります.

import socket
ETH_P_ALL = 3
interface = 'enp0s10'
dst = b'\x08\x00\x27\xdd\xd7\x43'   # destination MAC address
src = b'\x08\x00\x27\x8e\x75\x44'   # source MAC address
proto = b'\x88\xb5'                 # Ethernet frame type
payload = 'Hello, world!'.encode()  # payload
s = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.htons(ETH_P_ALL))
s.bind((interface, 0))
s.sendall(dst + src + proto + payload)
print('Sent!')
s.close()

実行します.

$ sudo python3 client.py
Sent!

サーバー側に送信したデータが出力されていることを確認してください.