[網路]Socket - 網路程式的大門

我們在寫網路相關的程式時,最底層就是與 socket 交互。socket 是專用於網路通訊的 system call。什麼是 system call 呢?我們知道作業系統有分 kernel 和 user space,從 user space 要進行 I/O 操作需要透過 system call 去呼叫 kernel space,因為只有 kernel space 才有權限進行 I/O 的操作。

在 Linux 中,使用 socket 宣告一個 object 會回傳一個 file descriptor。在 Linux 中,一切都是文件,連 socket object 也不例外,file descriptor 在這裡的作用就是指向那個文件的 descriptor。

下面簡單介紹本專案會用到的 socket 基本 API 的使用,首先開啟 socket 就是宣告了一個 socket 的 object:

sock = Socket(family, type, proto)

  • family
    family 指的是某個通訊協定,例如 IPv4、IPv6 等等。如果指定 IPv4 則只對 IPv4 的通訊協定的封包進行處理,例如 AF_PACKET,它能直接從網卡讀取和寫入數據。

  • type
    type 則是封包數據的格式,主要有 SOCK_STREAM 和 SOCK_DGRAM,分別代表 TCP 和 UDP。還有更原始的格式 SOCK_RAW,能自行組裝數據包。由於我們想監聽所有封包,所以會使用 SOCK_RAW。

  • proto
    proto 則是 protocol 的意思,通常預設為 0。我們的應用要監聽所有封包,所以設定為 0x0003 來監聽所有 Ethernet 上的封包。

定義好 socket file descriptor 後,下面是 socket object 常用的 API。

  • bind((addr, port))
    需監聽和發送的地址和 port,地址若為 0.0.0.0 則代表所有,也能監聽所有網卡。

  • recv(1024)
    如果有封包進來,kernel space 會響應這個函數,讓我們的應用程式可以對封包進行處理。若沒有封包,則會被 block 在這裡。函數回傳的是小於等於 1024 長度的 string,看封包大小而定。

  • send(packet)
    packet 就是我們做成的封包,在後續的課程會教大家怎麼製作自己的封包,怎麼計算 checksum。socket 會根據封包裡的 MAC address 和 IP address 進行路由。

除了上述這些 API,還有很多其他功能,有興趣的讀者可以深入研究。

範例

最後,我們試著用 socket API 來獲取封包的來源 MAC 地址。首先,我們需要監聽所有封包,所以 familytypeproto 分別為 AF_PACKETSOCK_RAW0x0003,完整如下:

1
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0003))

接下來,我們需要綁定一個 network interface,可以透過指令 ifconfig 來查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ifconfig
eth0 Link encap:Ethernet HWaddr 02:42:ac:11:00:02
inet addr:172.17.0.2 Bcast:172.17.255.255 Mask:255.255.0.0
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:3496 errors:0 dropped:0 overruns:0 frame:0
TX packets:1692 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:0
RX bytes:4623164 (4.6 MB) TX bytes:93240 (93.2 KB)

lo Link encap:Local Loopback
inet addr:127.0.0.1 Mask:255.0.0.0
UP LOOPBACK RUNNING MTU:65536 Metric:1
RX packets:36 errors:0 dropped:0 overruns:0 frame:0
TX packets:36 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:1800 (1.8 KB) TX bytes:1800 (1.8 KB)

在我的環境裡,有 eth0 這個 network interface,所以我決定監聽它:

1
sock.bind(("eth0", 0))

監聽的方式如下,需要一個無限循環去監測,如果沒有新封包則會被 block 在 sock.recvfrom

1
2
while True:
packet, _ = sock.recvfrom(65565)

一個封包的來源 MAC 地址會在封包的前六個 bytes,我們需要用到 Python 自帶的 struct library 來解析:

1
2
src_mac_header = packet[:6]
struct.unpack('!6s', src_mac_header)

驚嘆號代表的是 bytes 順序是 big endian,6s 代表解包 6 個 char,也能解包 integer 等等,具體代表的意思可以參考這裡。所以解包之後,我們就能取得來源的 MAC 地址,完整的程式碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
import socket, struct

#define ETH_P_ALL 0x0003 /* Every packet (be careful!!!) */
sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0003))
sock.bind(("eth0",0))

while True:
packet, _ = sock.recvfrom(65565)
src_mac_header = packet[:6]
src_mac = struct.unpack('!6s', src_mac_header)[0]
mac = map('{:02x}'.format, src_mac)
print(':'.join(mac))

實驗方式:可以跑這個程式的電腦發送 curl 請求,可以看到在我的環境裡,eth0 的 MAC address 為 02:42:ac:11:00:02,然後我們下指令對自己的電腦產生封包:

1
#> curl --interface eth0 127.0.0.1

在另外一個 terminal 視窗,就可以看到自己的 MAC address 被 print 出來了!

總結

  • Socket 是網路通訊的基礎工具,常用 API 包括 bind、recv 和 send
  • 範例程式展示如何監聽封包並取得來源 MAC 地址