1#!/usr/bin/python3 2# 3# Copyright 2017 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16 17import unittest 18 19from errno import * 20from socket import * 21from scapy import all as scapy 22 23import multinetwork_base 24import net_test 25import os 26import packets 27import tcp_metrics 28 29 30TCPOPT_FASTOPEN = 34 31TCP_FASTOPEN_CONNECT = 30 32BH_TIMEOUT_SYSCTL = "/proc/sys/net/ipv4/tcp_fastopen_blackhole_timeout_sec" 33 34 35class TcpFastOpenTest(multinetwork_base.MultiNetworkBaseTest): 36 37 @classmethod 38 def setUpClass(cls): 39 super(TcpFastOpenTest, cls).setUpClass() 40 cls.tcp_metrics = tcp_metrics.TcpMetrics() 41 42 def TFOClientSocket(self, version, netid): 43 s = net_test.TCPSocket(net_test.GetAddressFamily(version)) 44 net_test.DisableFinWait(s) 45 self.SelectInterface(s, netid, "mark") 46 s.setsockopt(IPPROTO_TCP, TCP_FASTOPEN_CONNECT, 1) 47 return s 48 49 def assertSocketNotConnected(self, sock): 50 self.assertRaisesErrno(ENOTCONN, sock.getpeername) 51 52 def assertSocketConnected(self, sock): 53 sock.getpeername() # No errors? Socket is alive and connected. 54 55 def clearTcpMetrics(self, version, netid): 56 saddr = self.MyAddress(version, netid) 57 daddr = self.GetRemoteAddress(version) 58 self.tcp_metrics.DelMetrics(saddr, daddr) 59 with self.assertRaisesErrno(ESRCH): 60 print(self.tcp_metrics.GetMetrics(saddr, daddr)) 61 62 def assertNoTcpMetrics(self, version, netid): 63 saddr = self.MyAddress(version, netid) 64 daddr = self.GetRemoteAddress(version) 65 with self.assertRaisesErrno(ENOENT): 66 self.tcp_metrics.GetMetrics(saddr, daddr) 67 68 def clearBlackhole(self): 69 # Prior to 4.15 this sysctl is not namespace aware. 70 if net_test.LINUX_VERSION < (4, 15, 0) and not os.path.exists(BH_TIMEOUT_SYSCTL): 71 return 72 timeout = self.GetSysctl(BH_TIMEOUT_SYSCTL) 73 74 # Write to timeout to clear any pre-existing blackhole condition 75 self.SetSysctl(BH_TIMEOUT_SYSCTL, timeout) 76 77 def CheckConnectOption(self, version): 78 ip_layer = {4: scapy.IP, 6: scapy.IPv6}[version] 79 netid = self.RandomNetid() 80 s = self.TFOClientSocket(version, netid) 81 82 self.clearTcpMetrics(version, netid) 83 self.clearBlackhole() 84 85 # Connect the first time. 86 remoteaddr = self.GetRemoteAddress(version) 87 with self.assertRaisesErrno(EINPROGRESS): 88 s.connect((remoteaddr, 53)) 89 self.assertSocketNotConnected(s) 90 91 # Expect a SYN handshake with an empty TFO option. 92 myaddr = self.MyAddress(version, netid) 93 port = s.getsockname()[1] 94 self.assertNotEqual(0, port) 95 desc, syn = packets.SYN(53, version, myaddr, remoteaddr, port, seq=None) 96 syn.getlayer("TCP").options = [(TCPOPT_FASTOPEN, "")] 97 msg = "Fastopen connect: expected %s" % desc 98 syn = self.ExpectPacketOn(netid, msg, syn) 99 syn = ip_layer(bytes(syn)) 100 101 # Receive a SYN+ACK with a TFO cookie and expect the connection to proceed 102 # as normal. 103 desc, synack = packets.SYNACK(version, remoteaddr, myaddr, syn) 104 synack.getlayer("TCP").options = [ 105 (TCPOPT_FASTOPEN, "helloT"), ("NOP", None), ("NOP", None)] 106 self.ReceivePacketOn(netid, synack) 107 synack = ip_layer(bytes(synack)) 108 desc, ack = packets.ACK(version, myaddr, remoteaddr, synack) 109 msg = "First connect: got SYN+ACK, expected %s" % desc 110 self.ExpectPacketOn(netid, msg, ack) 111 self.assertSocketConnected(s) 112 s.close() 113 desc, rst = packets.RST(version, myaddr, remoteaddr, synack) 114 msg = "Closing client socket, expecting %s" % desc 115 self.ExpectPacketOn(netid, msg, rst) 116 117 # Connect to the same destination again. Expect the connect to succeed 118 # without sending a SYN packet. 119 s = self.TFOClientSocket(version, netid) 120 s.connect((remoteaddr, 53)) 121 self.assertSocketNotConnected(s) 122 self.ExpectNoPacketsOn(netid, "Second TFO connect, expected no packets") 123 124 # Issue a write and expect a SYN with data. 125 port = s.getsockname()[1] 126 s.send(net_test.UDP_PAYLOAD) 127 desc, syn = packets.SYN(53, version, myaddr, remoteaddr, port, seq=None) 128 t = syn.getlayer(scapy.TCP) 129 t.options = [ (TCPOPT_FASTOPEN, "helloT"), ("NOP", None), ("NOP", None)] 130 t.payload = scapy.Raw(net_test.UDP_PAYLOAD) 131 msg = "TFO write, expected %s" % desc 132 self.ExpectPacketOn(netid, msg, syn) 133 134 def testConnectOptionIPv4(self): 135 self.CheckConnectOption(4) 136 137 def testConnectOptionIPv6(self): 138 self.CheckConnectOption(6) 139 140 141if __name__ == "__main__": 142 unittest.main() 143