• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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