1#!/usr/bin/python 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 if net_test.LINUX_VERSION < (4, 14, 0): 70 return 71 # Prior to 4.15 this sysctl is not namespace aware. 72 if net_test.LINUX_VERSION < (4, 15, 0) and not os.path.exists(BH_TIMEOUT_SYSCTL): 73 return 74 timeout = self.GetSysctl(BH_TIMEOUT_SYSCTL) 75 76 # Write to timeout to clear any pre-existing blackhole condition 77 self.SetSysctl(BH_TIMEOUT_SYSCTL, timeout) 78 79 def CheckConnectOption(self, version): 80 ip_layer = {4: scapy.IP, 6: scapy.IPv6}[version] 81 netid = self.RandomNetid() 82 s = self.TFOClientSocket(version, netid) 83 84 self.clearTcpMetrics(version, netid) 85 self.clearBlackhole() 86 87 # Connect the first time. 88 remoteaddr = self.GetRemoteAddress(version) 89 with self.assertRaisesErrno(EINPROGRESS): 90 s.connect((remoteaddr, 53)) 91 self.assertSocketNotConnected(s) 92 93 # Expect a SYN handshake with an empty TFO option. 94 myaddr = self.MyAddress(version, netid) 95 port = s.getsockname()[1] 96 self.assertNotEqual(0, port) 97 desc, syn = packets.SYN(53, version, myaddr, remoteaddr, port, seq=None) 98 syn.getlayer("TCP").options = [(TCPOPT_FASTOPEN, "")] 99 msg = "Fastopen connect: expected %s" % desc 100 syn = self.ExpectPacketOn(netid, msg, syn) 101 syn = ip_layer(str(syn)) 102 103 # Receive a SYN+ACK with a TFO cookie and expect the connection to proceed 104 # as normal. 105 desc, synack = packets.SYNACK(version, remoteaddr, myaddr, syn) 106 synack.getlayer("TCP").options = [ 107 (TCPOPT_FASTOPEN, "helloT"), ("NOP", None), ("NOP", None)] 108 self.ReceivePacketOn(netid, synack) 109 synack = ip_layer(str(synack)) 110 desc, ack = packets.ACK(version, myaddr, remoteaddr, synack) 111 msg = "First connect: got SYN+ACK, expected %s" % desc 112 self.ExpectPacketOn(netid, msg, ack) 113 self.assertSocketConnected(s) 114 s.close() 115 desc, rst = packets.RST(version, myaddr, remoteaddr, synack) 116 msg = "Closing client socket, expecting %s" % desc 117 self.ExpectPacketOn(netid, msg, rst) 118 119 # Connect to the same destination again. Expect the connect to succeed 120 # without sending a SYN packet. 121 s = self.TFOClientSocket(version, netid) 122 s.connect((remoteaddr, 53)) 123 self.assertSocketNotConnected(s) 124 self.ExpectNoPacketsOn(netid, "Second TFO connect, expected no packets") 125 126 # Issue a write and expect a SYN with data. 127 port = s.getsockname()[1] 128 s.send(net_test.UDP_PAYLOAD) 129 desc, syn = packets.SYN(53, version, myaddr, remoteaddr, port, seq=None) 130 t = syn.getlayer(scapy.TCP) 131 t.options = [ (TCPOPT_FASTOPEN, "helloT"), ("NOP", None), ("NOP", None)] 132 t.payload = scapy.Raw(net_test.UDP_PAYLOAD) 133 msg = "TFO write, expected %s" % desc 134 self.ExpectPacketOn(netid, msg, syn) 135 136 @unittest.skipUnless(net_test.LINUX_VERSION >= (4, 9, 0), "not yet backported") 137 def testConnectOptionIPv4(self): 138 self.CheckConnectOption(4) 139 140 @unittest.skipUnless(net_test.LINUX_VERSION >= (4, 9, 0), "not yet backported") 141 def testConnectOptionIPv6(self): 142 self.CheckConnectOption(6) 143 144 145if __name__ == "__main__": 146 unittest.main() 147