因为要使用 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包头 | 数据 ] 的内容。