test_edge_discovery.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import ipaddress
  2. import socket
  3. import pytest
  4. from constants import protocols
  5. from cli import CloudflaredCli
  6. from util import get_tunnel_connector_id, LOGGER, wait_tunnel_ready, write_config
  7. class TestEdgeDiscovery:
  8. def _extra_config(self, protocol, edge_ip_version):
  9. config = {
  10. "protocol": protocol,
  11. }
  12. if edge_ip_version:
  13. config["edge-ip-version"] = edge_ip_version
  14. return config
  15. @pytest.mark.parametrize("protocol", protocols())
  16. def test_default_only(self, tmp_path, component_tests_config, protocol):
  17. """
  18. This test runs a tunnel to connect via IPv4-only edge addresses (default is unset "--edge-ip-version 4")
  19. """
  20. if self.has_ipv6_only():
  21. pytest.skip("Host has IPv6 only support and current default is IPv4 only")
  22. self.expect_address_connections(
  23. tmp_path, component_tests_config, protocol, None, self.expect_ipv4_address)
  24. @pytest.mark.parametrize("protocol", protocols())
  25. def test_ipv4_only(self, tmp_path, component_tests_config, protocol):
  26. """
  27. This test runs a tunnel to connect via IPv4-only edge addresses
  28. """
  29. if self.has_ipv6_only():
  30. pytest.skip("Host has IPv6 only support")
  31. self.expect_address_connections(
  32. tmp_path, component_tests_config, protocol, "4", self.expect_ipv4_address)
  33. @pytest.mark.parametrize("protocol", protocols())
  34. def test_ipv6_only(self, tmp_path, component_tests_config, protocol):
  35. """
  36. This test runs a tunnel to connect via IPv6-only edge addresses
  37. """
  38. if self.has_ipv4_only():
  39. pytest.skip("Host has IPv4 only support")
  40. self.expect_address_connections(
  41. tmp_path, component_tests_config, protocol, "6", self.expect_ipv6_address)
  42. @pytest.mark.parametrize("protocol", protocols())
  43. def test_auto_ip64(self, tmp_path, component_tests_config, protocol):
  44. """
  45. This test runs a tunnel to connect via auto with a preference of IPv6 then IPv4 addresses for a dual stack host
  46. This test also assumes that the host has IPv6 preference.
  47. """
  48. if not self.has_dual_stack(address_family_preference=socket.AddressFamily.AF_INET6):
  49. pytest.skip("Host does not support dual stack with IPv6 preference")
  50. self.expect_address_connections(
  51. tmp_path, component_tests_config, protocol, "auto", self.expect_ipv6_address)
  52. @pytest.mark.parametrize("protocol", protocols())
  53. def test_auto_ip46(self, tmp_path, component_tests_config, protocol):
  54. """
  55. This test runs a tunnel to connect via auto with a preference of IPv4 then IPv6 addresses for a dual stack host
  56. This test also assumes that the host has IPv4 preference.
  57. """
  58. if not self.has_dual_stack(address_family_preference=socket.AddressFamily.AF_INET):
  59. pytest.skip("Host does not support dual stack with IPv4 preference")
  60. self.expect_address_connections(
  61. tmp_path, component_tests_config, protocol, "auto", self.expect_ipv4_address)
  62. def expect_address_connections(self, tmp_path, component_tests_config, protocol, edge_ip_version, assert_address_type):
  63. config = component_tests_config(
  64. self._extra_config(protocol, edge_ip_version))
  65. config_path = write_config(tmp_path, config.full_config)
  66. LOGGER.debug(config)
  67. with CloudflaredCli(config, config_path, LOGGER):
  68. wait_tunnel_ready(tunnel_url=config.get_url(),
  69. require_min_connections=4)
  70. cfd_cli = CloudflaredCli(config, config_path, LOGGER)
  71. tunnel_id = config.get_tunnel_id()
  72. info = cfd_cli.get_tunnel_info(tunnel_id)
  73. connector_id = get_tunnel_connector_id()
  74. connector = next(
  75. (c for c in info["conns"] if c["id"] == connector_id), None)
  76. assert connector, f"Expected connection info from get tunnel info for the connected instance: {info}"
  77. conns = connector["conns"]
  78. assert conns == None or len(
  79. conns) == 4, f"There should be 4 connections registered: {conns}"
  80. for conn in conns:
  81. origin_ip = conn["origin_ip"]
  82. assert origin_ip, f"No available origin_ip for this connection: {conn}"
  83. assert_address_type(origin_ip)
  84. def expect_ipv4_address(self, address):
  85. assert type(ipaddress.ip_address(
  86. address)) is ipaddress.IPv4Address, f"Expected connection from origin to be a valid IPv4 address: {address}"
  87. def expect_ipv6_address(self, address):
  88. assert type(ipaddress.ip_address(
  89. address)) is ipaddress.IPv6Address, f"Expected connection from origin to be a valid IPv6 address: {address}"
  90. def get_addresses(self):
  91. """
  92. Returns a list of addresses for the host.
  93. """
  94. host_addresses = socket.getaddrinfo(
  95. "region1.v2.argotunnel.com", 7844, socket.AF_UNSPEC, socket.SOCK_STREAM)
  96. assert len(
  97. host_addresses) > 0, "No addresses returned from getaddrinfo"
  98. return host_addresses
  99. def has_dual_stack(self, address_family_preference=None):
  100. """
  101. Returns true if the host has dual stack support and can optionally check
  102. the provided IP family preference.
  103. """
  104. dual_stack = not self.has_ipv6_only() and not self.has_ipv4_only()
  105. if address_family_preference:
  106. address = self.get_addresses()[0]
  107. return dual_stack and address[0] == address_family_preference
  108. return dual_stack
  109. def has_ipv6_only(self):
  110. """
  111. Returns True if the host has only IPv6 address support.
  112. """
  113. return self.attempt_connection(socket.AddressFamily.AF_INET6) and not self.attempt_connection(socket.AddressFamily.AF_INET)
  114. def has_ipv4_only(self):
  115. """
  116. Returns True if the host has only IPv4 address support.
  117. """
  118. return self.attempt_connection(socket.AddressFamily.AF_INET) and not self.attempt_connection(socket.AddressFamily.AF_INET6)
  119. def attempt_connection(self, address_family):
  120. """
  121. Returns True if a successful socket connection can be made to the
  122. remote host with the provided address family to validate host support
  123. for the provided address family.
  124. """
  125. address = None
  126. for a in self.get_addresses():
  127. if a[0] == address_family:
  128. address = a
  129. break
  130. if address is None:
  131. # Couldn't even lookup the address family so we can't connect
  132. return False
  133. af, socktype, proto, canonname, sockaddr = address
  134. s = None
  135. try:
  136. s = socket.socket(af, socktype, proto)
  137. except OSError:
  138. return False
  139. try:
  140. s.connect(sockaddr)
  141. except OSError:
  142. s.close()
  143. return False
  144. s.close()
  145. return True