1)两台主机间独立传输的数据包流发生的任何错误,都需要进行修复;
2)丢失的数据包也需要进行重传,知道成功发送至目的地址
3)如果数据包到达时顺序错乱,则要将这些数据包重组会正确的顺序
4)丢弃重复的数据包,保证数据流中的信息没有荣誉
计算机网络和电磁信号理论中,对共享同一通信信道的多个信号,进行区分是个常见的问题。
IP网络层上,唯一可见的就是向特定主机传输的数据包。
Source IP --> Destination IP
进行通信的两台机器上的网络栈,需要支持多个程序互不影响地同时进行交互,故同时使用IP和端口号来标识源机器与目标机器。
Source (IP : port num) --> Destination (IP :port num)
对于同一特定会话中发送的数据包,4个值完全相同,响应数据包则把源IP地址与目标IP地址调换,把源端口与目标端口号调换。
1) 在IP为192.168.1.9机器上架设一台DNS服务器,向操作系统请求使用53端口的权限(标准DNS端口),用来接收发送至UDP端口的数据包。
2) IP为192.168.1.30的客户机想查询服务器。客户机在内存中构造该查询,请求操作系统将查询封装为UDP数据包并发送,到达目标地址时,目的地址的程序,识别发送该查询的客户端并返回响应,而客户端没有声明请求端口号,则随机分配44137号端口
则数据包通过Source(192.168.1.30:44137) -> Destination (192.168.1.9:53)
发送至目标地址的53号端口。
DNS服务器构造完成了相应信息,请求操作系统发送UDP数据包作为响应。该数据包中的源目标IP和端口与请求数据包正好想法,故,服务器的响应就能直接发回给请求方了。
Source(192.168.1.9:53) -> Destination (192.168.1.30:44137)
UDP机制相当简单,仅使用IP地址和端口号进行识别,将数据包发送至目标地址。
3)客户端如何获悉需要连接的端口号?
无法监听这些端口,可以防止将一些运行程序伪装成重要的系统服务,可以在主机提供公司分发Linux命令行账户时,排上用处。
import socket
socket.getservbyname('domain')
O]53
同样可以使用socket模块提供的更复杂的getaddrinfo()函数来解析端口名。
在决定如何设计网络编程API时,Py没有重复造轮子。在底层,标准库对兼容POSIX系统的,网络操作的底层系统进行了封装,并为所有普通原始调用,提供基于对象的接口。封装后的Py函数名与原始系统调用名相同。故可以使用熟知的方法来调用传统系统,在需要时进行底层操作系统调用,无需使用特定语言API,提供和C相同的系统调用集合。
无论是Win还是POSIX(Linux/Mac OS),网络操作背后的系统调用都是围绕着套接字(socket)来进行的。套接字是一个通信断点,OS使用整数来标识套接字,而Py使用socket.socket表示。该对象内部维护了OS标识套接字的整数(可调用fileno()方法查看)。当调用socket.socket对象的方法,请求使用该套接字的系统调用时,该对象都会自动使用,内部维护的套接字整数标识符。
# 2-1 /chapter02/udp_local.py
import argparse, socket
from datetime import datetime
MAX_BYTES = 65535 # 64KB,3w2k个汉字
def server(port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('127.0.0.1', port))
print('Listening at {}'.format(sock.getsockname()))
while True:
data, address = sock.recvfrom(MAX_BYTES)
text = data.decode('ascii')
print('The client at {} says {!r}'.format(address, text))
data = text.encode('ascii')
sock.sendto(data, address)
def client(port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
text = 'The time is {}'.format(datetime.now())
data = text.encode('ascii')
sock.sendto(data, ('127.0.0.1', port))
print('The OS assigned me the address {}'.format(sock.getsockname()))
data, address = sock.recvfrom(MAX_BYTES) # Danger!
text = data.decode('ascii')
print('The server {} replied {!r}'.format(address, text))
if __name__ == '__main__':
choices = {'client': client, 'server': server}
parser = argparse.ArgumentParser(description='Send and receive UDP locally')
parser.add_argument('role', choices=choices, help='which role to play')
parser.add_argument('-p', metavar='PORT', type=int, default=1060, help='UDP port (default 1060)')
args = parser.parse_args()
funciont = choices[args.role]
funciont(args.p)
(venv) >python udp_local.py client
O]
The OS assigned me the address ('0.0.0.0', 63042)
The server ('127.0.0.1', 1060) replied 'The time is 2019-11-27 20:33:33.395549'
(venv) >python udp_local.py client
O]
The OS assigned me the address ('0.0.0.0', 63053)
The server ('127.0.0.1', 1060) replied 'The time is 2019-11-27 20:33:43.011358'
尽管recvfrom()返回了传入的数据包地址,但是代码没有检查该数据报的源地址,即没有验证该数据报是否确实是服务器发回的响应。
把服务器的响应延迟一段时间,Win需要再服务器接受请求和发送响应两步间添加一个time.sleep();Linux和Mac,键入Ctrl+Z就可以暂停服务器
$ python3 udp_local.py server
O]
Listening at ('127.0.0.1', 1060)
^Z
[1]+ Stopped python3 udp_local.py server
运行客户端,客户端会发送数据报,过期等待,直到收到服务器的响应
$ python3 udp_local.py client
O]
The OS assigned me the address ('0.0.0.0', 47935)
攻击者伪造一个服务器的响应,在服务器返回真实响应前,先发送伪造的数据报。客户端会接收任何数据报,没有对响应做任何检查,则客户端认为伪造的响应来自服务器。Py中启动快速会话来发送一个伪造的数据包
>>> import socket
>>> sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
>>> sock.sendto('FAKE'.encode('ascii'), ('127.0.0.1', 47935))
O]
4
客户端会结束等待,将该第三方响应看做是服务器的响应。
The server ('127.0.0.1', 55437) replied 'FAKE'
键入fg将暂停的服务器恢复运行;
1) 设计/使用在请求中包含唯一标识符/请求ID的协议,响应中重复特定请求的唯一标识符/请求ID,只要ID的取值范围足够大,就无法在短时间内简单地把所有可能正确的ID数据包都发一遍,故,攻击者虽然知道信息,但还需构造正确的ID
2) 检查响应数据包的地址与请求数据包的地址是否相同,也可用connect()来阻止其他地址想客户端发送数据包。
如果数据包丢失,代码会变得复杂,如下,该程序中的服务器未始终响应客户端的请求,而是随机选择,只对收到的一半客户端请求作出相应。
# 2-2 chapter02/udp_remote.py
import argparse, random, socket, sys
MAX_BYTES = 65535
def server(interface, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((interface, port))
print('Listening at', sock.getsockname())
while True:
data, address = sock.recvfrom(MAX_BYTES)
if random.random() < 0.5:
print('Pretending to drop packet from {}'.format(address))
continue
text = data.decode('ascii')
print('The client at {} says {!r}'.format(address, text))
message = 'Your data was {} bytes long'.format(len(data))
sock.sendto(message.encode('ascii'), address)
def client(hostname, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
hostname = sys.argv[2]
sock.connect((hostname, port))
print('Client socket name is {}'.format(sock.getsockname()))
delay = 0.1 # seconds
text = 'This is another message'
data = text.encode('ascii')
while True:
sock.send(data)
print('Waiting up to {} seconds for a reply'.format(delay))
sock.settimeout(delay)
try:
data = sock.recv(MAX_BYTES)
except socket.timeout:
delay *= 2 # wait even longer for the next request
if delay > 2.0:
raise RuntimeError('I think the server is down')
else:
break # we are done, and can stop looping
print('The server says {!r}'.format(data.decode('ascii')))
if __name__ == '__main__':
choices = {'client':client, 'server': server}
parser = argparse.ArgumentParser(description='Send and receive UDP, pretending packets are often dropped')
parser.add_argument('role', choices=choices, help='which role to take')
parser.add_argument('host', help='interface the server listens at ;host the client sends to')
parser.add_argument('-p', metavar='PORT', type=int, default=1060, help='UDP port (default 1060')
args = parser.parse_args()
function = choices[args.role]
function(args.host, args.p)
$ python3 udp_remote.py server ""
O]
Listening at ('0.0.0.0', 1060)
服务器每次收到请求,使用random()来决定是否对该请求作出应答,无需不断运行客户端来等待真正丢包现象的发生,无论服务器是否响应,都会输出一条信息,就能跟踪服务器的运行状态了。
1)响应时间较长,但即将传回客户端
2)响应或请求在传输中丢失了,无法到达客户端
3)服务器宕机,无法响应
故,UDP客户端必须选择一个等待时间。一旦超过,就重发,因此,客户端不会在调用recv()后永久暂停,而是调用套接字的settimeout()方法,告诉OS,客户端进行一个套接字操作的最长等待时间是delay秒,一旦超过,抛出socket.timeout异常,recv()中断
$ python3 udp_remote.py client hostname
Client socket name is ('127.0.0.1', 55861)
Waiting up to 0.1 seconds for a reply
Waiting up to 0.2 seconds for a reply
The server says 'Your data was 23 bytes long'
$ python3 udp_remote.py client hostname
Client socket name is ('127.0.0.0', 59263)
Waiting up to 0.1 seconds for a reply
The server says 'Your data was 23 bytes long'
所有的丢包都是服务器模拟的,如果服务器宕机,无法通过UDP来判断,故,客户端的最佳时间就是在进行足够多的尝试后放弃,结束服务器进程,然后重新运行客户端
1) 显示的bind()调用:发生在serv,指定serv要使用的IP和port
2) 隐式绑定:发生在cli,第一次尝试使用一个socket时,OS会随机分配临时port
1)使用sendto()指定每个数据包的目标IP,使用recvfrom()接收响应,并检查响应数据包的返回IP,看是否曾经向该地址发送过请求。
2)创建socket后,用connect()与其目标IP链接,使用send()和recv()进行通信。OS会将不需要的数据包过滤,只支持同时与一台服务器交互的情况,因为同一socket上重复运行connect()不会增加目标IP。
1) connect()连接UDP的socket,只是简单地将连接的IP写入OS的内存,供之后调用send()和recv()使用
2) 使用connect()/通过返回地址手动过滤不需要的数据包,并不能确保安全,攻击者很容易伪造出拥有Serv返回IP的数据包,这种使用另一台PC的返回IP来发送数据包的行为叫做电子欺骗(spoofing),协议设计者在设计安全协议时需要考虑的首要问题
如果自己设计一套UDP请求和响应机制,需要给每个请求加上一个序列号,保证接收的响应包含相同的序列号。Serv端,只需把请求的ID复制到相应的响应中。
1) 使用指数退避的Cli会重复发送请求,请求ID可以将响应与重复发送的请求正确对应起来;还能解决比较罕见的重复问题:网络结构的冗余,偶尔也会在Serv和Cli间产生同一数据包的两个脚本,造成的重复问题也可以由请求ID解决。
2) 一个范围合适且使用random生成的0~N的请求ID可以使Cli接收伪造响应的可能性大大降低。单仍然不是真正的安全,只有在攻击者无法获取网络通信信息,只能进行最简单的电子欺骗攻击时,才能起到保护作用,真正的安全意味着,攻击者可以获取通信数据,并插入任何信息,我们的客户端仍然能受到保护。
1) Serv选择‘127.0.0.1‘表示只接收来自本机上其他运行程序的数据包
2) 使用空字符串‘‘作为通配符,接收通过该服务器的任何网络接口收到的数据包
3) 提供该Serv的某个外部IP接口的IP,服务器只会监听传输至该IP的数据包
$ python udp_remote.py server 192.168.5.130
Listening at ('192.168.5.130', 1060)
使用另一台机器仍然可以连接到这一IP
$ python udp_remote.py client hostname
Client socket name is ('192.168.5.10', 35084)
Waiting up to 0.1 seconds for a reply
The server says 'Your data was 23 bytes'
同一台机器上运行Cli脚本,并通过自环接口来连接Serv的话,Serv永远不会做出响应
$python udp_remote.py client 127.0.0.1
Client socket name is ('127.0.0.1', 60251)
Waiting up to 0.1 seconds for a reply
Traceback (most recent call last):
...
socket.error: [Errno 111] Connection refused
实际上Serv还是会发送数据包,OS会检测是否有打开的port没有像网络发送数据包,如果有请求发送至该port,那么OS会马上响应,通知Cli该连接是不可用的。只有使用自环接口时,UDP才能够返回"拒绝连接"的响应。真实网络上通信,Cli发送的数据包无法判断Serv是否有接受接受该请求的port。
使用同一机器再次运行客户端,使用外部IP连接Serv
$ python udp_remote.py client 192.168.5.130
Client socket name is ('192.168.5.130', 34919)
Waiting up to 0.1 seconds for a reply
The server says 'Your data was 23 bytes'
本地的运行程序可以使用任何本机的IP向Serv发送请求,即使使用该IP地址与本机上的其他服务通信。
不能通过自环接口/通配符IP(原因是通配符包含了127.0.0.1)来运行第二个Serv,可以使用第一个Serv没有监听的外部IP接口,如果机器有多个远程接口,可以启动更多Serv,每个Serv监听一个远程接口。
$python udp_remote.py server 127.0.0.1
$python udp_remote.py server 127.0.0.1
Traceback (most recent call last):
...
OSError: [Errno 98] Address alredy in use
$python udp_remote.py server
Traceback (most recent call last):
...
OSError: [Errno 98] Address alredy in use
$python udp_remote.py server 192.168.5.130
Listening at ('192.168.5.130', 1060)
使用UDP的Cli发送数据包,每个请求只会被一个Serv接收,该Serv的IP就是对应UDP请求数据包的特定目标IP。
较大的数据包在传输过程中更易发生丢包现象,因为只要分隔出的任一小数据包没有传至目标IP,便无法重组出原始的大数据包,正在监听的OS就无法正确接收了。
# 2-3 chapter02/big_sender.py
import IN, argparse, socket
if not hasattr(IN, 'IP_MTU'):
raise RuntimeError('cannot perform MTU discovery on this combination of operating system and Python distribution')
def send_big_datagram(host, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.setsockopt(socket.IPPROTO_IP, IN.IP_MTU_DISCOVER, IN.IP_PMTUDISC_DO)
sock.connect((host, port))
try:
sock.send(b'#' * 65000)
except socket.error:
print('Alas, the datagram did not make it')
max_mtu = sock.getsockopt(socket.IPPROTO_IP, IN.IP_MTU)
print('Actual MTU: {}'.format(max_mtu))
else:
print('The big datagram was send!')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Send UDP packet to get MTU')
parser.add_argument('host', help='the host to which to target ther patcket')
parser.add_argument('-p', metavar = 'PORT', type=int, default=1060, help='UDP port (default 1060')
args = parser.parse_args()
send_big_datagram(args.host, args.p)
运行该程序,只允许发送不草果1500B的物理数据包
$ python big_sender.py hostname
Alas, the datagram did not make it
Actual MTU:1500
可以通过getsockopt()和setsockopt()获取并设置套接字选项。
设置套接字选项,设置套接字选项的调用比获取套接字选项的调用多一个参数
1) 给出所属选项组的名称
2) 给出要设置的选项名
value = s.getsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST)
s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, vale)
通过广播,可以将数据报的目标地址设置为本机连接的整个子网,然后使用物理网卡将数据报广播,这样就无需再复制该数据包并单独将其发送给所有连接至该子网的主机了。
由于出现了更为
原文:https://www.cnblogs.com/wangxue533/p/11939457.html