因為要使用 python 底層發包模塊,也就是 raw socket 發包模式,所以在此深入了解一下 python socket 通信。
涉及到的函數:
import socket
socket()
setsockopt()
sendto()
recvfrom()
因為使用的是原始套接字,所以我們不使用bind/connect函數,參照《unix 網絡編程》
bind 函數僅僅設置本地地址。就輸出而言,調用bind函數設置的是將用於從這個原始套接字發送的所有數據報的源IP地址。如果不調用bind,內核就吧源IP地址設置為外出接口的主IP地址。
connect函數僅僅設置外地地址,同樣因為原始套接字不存在端口號的概念。就輸出而言,調用connect之后我們可以把sendto調用改為write或者send調用,因為目的IP地址已經指定了。
順便說一句,connect函數也是三次握手的發生過程,參見鏈接
套接字參數
官網介紹:
socket.socket([family[, type[, proto]]])
參數說明:
family:協議簇/地址簇。
最常用的就是 socket.AF_INET 了,TCP/UDP 通信均屬於此類型。
PS:有時我們看到的協議常量名為 AF_xxx,有時又是 PF_xxx,可以理解為 address family 和 protocol family,實際使用中是沒有區別的。一般在區分協議的時候習慣使用 PF ,而在區分地址的時候習慣使用AF。可以參照這個鏈接的解釋:
Yes. AF_foo means address family foo, and PF_foo means protocol family foo. In Linux, they are always been the same values, I believe.
Traditionally, the PF_foo constants were used for socket(), but AF_foo in the struct sockaddr structure.
According to man 2 socket, even the (historical) BSD 4.x man page states that "The protocol family generally is the same as the address family", and subsequent standards use AF_* everywhere.
Thus, today, there really should be no difference between AF_foo and PF_foo.
除此之外常用的還有 AF_UNIX/AF_LOCAL ,代表UNIX域協議,屬於IPC(進程間通信)的一種方式;AF_INET6 ,IPv6 通信。
- socket.AF_UNIX 只能夠用於單一的Unix系統進程間通信
- socket.AF_INET 服務器之間網絡通信
- socket.AF_INET6 IPv6
type:socket的類型
官網給出的列表如下:
- socket.SOCK_STREAM
- socket.SOCK_DGRAM
- socket.SOCK_RAW 原始套接字,普通的套接字無法處理ICMP、IGMP等網絡報文,而SOCK_RAW可以;其次,SOCK_RAW也可以處理特殊的IPv4報文;此外,利用原始套接字,可以通過IP_HDRINCL套接字選項由用戶構造IP頭。
還有兩種就是 socket.SOCK_RDM 與 socket.SOCK_SEQPACKET,基本沒見過用
前兩種分別代表 面向流(TCP)和面向數據報(UDP)的socket通信。
(我的理解是:SOCK_RAW = 協議頭部我也自己發)
proto: 協議類型
常見的為
IPPROTO_ICMP = 1
IPPROTO_IP = 0
IPPROTO_RAW = 255
IPPROTO_TCP = 6
IPPROTO_UDP = 17
設置套接字選項
setsockopt:設置套接字選項
socket.setsockopt(level, optname, value)
具體參數查看鏈接
level:參數作用范圍,常見的包括:
SOL_SOCKET SOL應該是指的 SOck Level ,意為套接字層選項,常見的有 SO_REUSEADDR ,可以服用處於 Time_wait 狀態的端口。
IPPROTO_IP IP數據包選項,一個將要用到的是 IP_HDRINCL ,如果是TRUE,IP頭就會隨即將發送的數據一起提交,並從讀取的數據中返回。
還有 IPPROTO_TCP 等,此處不多做介紹。
SOCKET通信例子
TCP/UDP 通信
先放兩個簡單的例子
UDP Server:
import socket
address = ('127.0.0.1', 31500)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.bind(address)
while True:
data, addr = s.recvfrom(2048)
if not data:
print "client has exist"
break
print "received:", data, "from", addr
s.close()
UDP Client:
import socket
address = ('127.0.0.1', 31500)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
msg = raw_input()
if not msg:
break
s.sendto(msg, address)
s.close()
TCP Server:
import socket
address = ('127.0.0.1', 31500)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # s = socket.socket()
s.bind(address)
s.listen(5)
ss, addr = s.accept()
print 'got connected from',addr
ss.send('byebye')
ra = ss.recv(512)
print ra
ss.close()
s.close()
TCP Client:
import socket
address = ('127.0.0.1', 31500)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(address)
data = s.recv(512)
print 'the data received is',data
s.send('hihi')
s.close()
RAW SOCKET 通信
接下來我們看一個raw socket通信的例子
import sys
import socket
from impacket import ImpactDecoder, ImpactPacket
def main():
if len(sys.argv) < 3:
print "Use: %s <src ip> <dst ip>" % sys.argv[0]
print "Use: %s <src ip> <dst ip> <cnt>" % sys.argv[0]
sys.exit(1)
elif len(sys.argv) == 3:
src = sys.argv[1]
dst = sys.argv[2]
cnt = 1
elif len(sys.argv) ==4:
src = sys.argv[1]
dst = sys.argv[2]
cnt = sys.argv[3]
else:
print "Input error!"
sys.exit(1)
#print src, dst
ip = ImpactPacket.IP()
ip.set_ip_src(src)
ip.set_ip_dst(dst)
tcp = ImpactPacket.TCP()
tcp.set_th_sport(55968)
tcp.set_th_dport(80)
tcp.set_th_seq(1)
tcp.set_th_ack(1)
tcp.set_th_flags(0x18)
tcp.set_th_win(64)
tcp.contains( ImpactPacket.Data("GET /att/DIYLife/41264/528 HTTP/1.1\r\nHost: 192.168.111.1\r\nAccept-Encoding: identity\r\n\r\n"))
ip.contains(tcp)
# Open a raw socket. Special permissions are usually required.
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) // 此選項設置使用我們自己構造的IP頭部
seq_id = 0
while cnt >= 1:
# Calculate its checksum.
seq_id = seq_id + 1
tcp.set_th_seq(seq_id)
tcp.calculate_checksum()
# Send it to the target host.
s.sendto(ip.get_packet(), (dst,80))
cnt= cnt -1
if __name__ == '__main__':
main()
看完這幾個例子之后,說一下我的看法。
Socket 的作用就是封裝了各種不同的底層協議,為我們提供一個統一的操作接口。使用socket通信的時候,我們只需要根據協議類型來初始化相應的socket,然后將我們需要寫入的數據傳入該socket即可。
因此,在初始化之后,socket為我們做了這么幾件事情:
- 對於面向流的連接如TCP,可以幫助我們自動完成三次握手(connect函數)和四次揮手(close函數)的過程
- 在我們每次發送數據的時候,將我們要發送的數據根據默認或者你設置的選項包裹好包頭,將其交給網卡的發送緩沖區
- 接受數據的時候,幫助我們去掉包頭
由於不同協議都可以使用同樣的接口進行發送和接受數據,因此,區分不同包頭的過程都是在socket()函數中完成的。
總結
包結構圖

創建四層以上的套接字
直接使用 socket.socket(socket.AF_INET,socket.SOCK_STREAM/socket.SOCK_DGRAM , socket.IPPROTO_TCP) 即可,proto 可以自動推斷(等價於IPPROTO_IP),也可以直接簡寫為s = socket.socket()
意味着我們需要填充的內容僅僅是包結構圖中的 [ 數據 ] 部分的內容
創建三層套接字
- 自行填充TCP頭/UDP頭,IP頭部交給內核填充
意味着我們需要填充的是包結構圖中的 [ TCP包頭 | 數據 ]
此時由於四層協議頭部需要由我們自己填充,就有一個問題:如果是四層以上套接字的話,我們是不用告訴socket協議名的,程序會自動根據你的端口號來區分應用層協議。但是如果你填充四層協議頭的話,socket就必須提前知道是什么協議,用來填充IP頭部的協議字段,也就是說協議字段不能為IPPROTO_IP。
因此,我們就需要傳入 socket 函數的第三個參數。例如我們要自己構造TCP包,可以用 socket.socket(socket.AF_INET,socket.SOCK_RAW , socket.IPPROTO_TCP )
- 自行填充 四層協議頭部和IP頭部(限定是IP協議)
意味着我們需要填充的是包結構圖中的 [ IP包頭 | TCP包頭 | 數據 ] 的內容。
這個和上面那個差不多,只不過我們可以修改IP頭部,一種方式是:
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_TCP)
s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1) # 設置 IP 頭部自己發送
另外一種方式是:
s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
這兩種方式應該都是僅僅限於發送IP協議,所以 Ethernet 頭部的協議字段不用我們填充~
創建二層套接字
方式1:
socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))
自行填充 以太網包頭
意味着我們需要填充的是上圖中的 [ MAC包頭 | IP包頭 | TCP包頭 | 數據 ] 的內容。
方式2:
socket(PF_PACKET, SOCK_DGRAM, htons(ETH_P_IP|ETH_P_ARP|ETH_P_ALL))
使用SOCK_RAW發送的數據必須包含鏈路層的協議頭,接受得到的數據包,包含鏈路層協議頭。而使用SOCK_DGRAM則都不含鏈路層的協議頭。
也即是說,需要填充的是上圖中的 [ IP包頭 | TCP包頭 | 數據 ] 的內容。
