我們在寫網路相關的程式時,最底層就是與 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 地址。首先,我們需要監聽所有封包,所以 family
、type
和 proto
分別為 AF_PACKET
、SOCK_RAW
和 0x0003
,完整如下:
1 | sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(0x0003)) |
接下來,我們需要綁定一個 network interface,可以透過指令 ifconfig
來查看:
1 | ifconfig |
在我的環境裡,有 eth0
這個 network interface,所以我決定監聽它:
1 | sock.bind(("eth0", 0)) |
監聽的方式如下,需要一個無限循環去監測,如果沒有新封包則會被 block 在 sock.recvfrom
:
1 | while True: |
一個封包的來源 MAC 地址會在封包的前六個 bytes,我們需要用到 Python 自帶的 struct library 來解析:
1 | src_mac_header = packet[:6] |
驚嘆號代表的是 bytes 順序是 big endian,6s 代表解包 6 個 char,也能解包 integer 等等,具體代表的意思可以參考這裡。所以解包之後,我們就能取得來源的 MAC 地址,完整的程式碼如下:
1 | import socket, struct |
實驗方式:可以跑這個程式的電腦發送 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 地址