dns-rfc2136: use TCP to query SOA records
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a71d24adea..56235e756f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,7 @@ Certbot adheres to [Semantic Versioning](https://semver.org/).
 * acme.standalone.BaseRequestHandlerWithLogging and
   acme.standalone.simple_tls_sni_01_server have been deprecated and will be
   removed in a future release of the library.
+* certbot-dns-rfc2136 now use TCP to query SOA records.
 
 ### Fixed
 
diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py
index 2061374e0e8..ee71c9681d7 100644
--- a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py
+++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136.py
@@ -206,7 +206,11 @@ def _query_soa(self, domain_name):
         request.flags ^= dns.flags.RD
 
         try:
-            response = dns.query.udp(request, self.server, port=self.port)
+            try:
+                response = dns.query.tcp(request, self.server, port=self.port)
+            except OSError as e:
+                logger.debug('TCP query failed, fallback to UDP: %s', e)
+                response = dns.query.udp(request, self.server, port=self.port)
             rcode = response.rcode()
 
             # Authoritative Answer bit should be set
diff --git a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py
index d800f1ec7c2..1950ee62e71 100644
--- a/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py
+++ b/certbot-dns-rfc2136/certbot_dns_rfc2136/dns_rfc2136_test.py
@@ -162,7 +162,7 @@ def test_find_domain_wraps_errors(self):
             self.rfc2136_client._find_domain,
             'foo.bar.'+DOMAIN)
 
-    @mock.patch("dns.query.udp")
+    @mock.patch("dns.query.tcp")
     def test_query_soa_found(self, query_mock):
         query_mock.return_value = mock.MagicMock(answer=[mock.MagicMock()], flags=dns.flags.AA)
         query_mock.return_value.rcode.return_value = dns.rcode.NOERROR
@@ -173,7 +173,7 @@ def test_query_soa_found(self, query_mock):
         query_mock.assert_called_with(mock.ANY, SERVER, port=PORT)
         self.assertTrue(result)
 
-    @mock.patch("dns.query.udp")
+    @mock.patch("dns.query.tcp")
     def test_query_soa_not_found(self, query_mock):
         query_mock.return_value.rcode.return_value = dns.rcode.NXDOMAIN
 
@@ -183,7 +183,7 @@ def test_query_soa_not_found(self, query_mock):
         query_mock.assert_called_with(mock.ANY, SERVER, port=PORT)
         self.assertFalse(result)
 
-    @mock.patch("dns.query.udp")
+    @mock.patch("dns.query.tcp")
     def test_query_soa_wraps_errors(self, query_mock):
         query_mock.side_effect = Exception
 
@@ -193,6 +193,20 @@ def test_query_soa_wraps_errors(self, query_mock):
             self.rfc2136_client._query_soa,
             DOMAIN)
 
+    @mock.patch("dns.query.udp")
+    @mock.patch("dns.query.tcp")
+    def test_query_soa_fallback_to_udp(self, tcp_mock, udp_mock):
+        tcp_mock.side_effect = OSError
+        udp_mock.return_value = mock.MagicMock(answer=[mock.MagicMock()], flags=dns.flags.AA)
+        udp_mock.return_value.rcode.return_value = dns.rcode.NOERROR
+
+        # _query_soa | pylint: disable=protected-access
+        result = self.rfc2136_client._query_soa(DOMAIN)
+
+        tcp_mock.assert_called_with(mock.ANY, SERVER, port=PORT)
+        udp_mock.assert_called_with(mock.ANY, SERVER, port=PORT)
+        self.assertTrue(result)
+
 
 if __name__ == "__main__":
     unittest.main()  # pragma: no cover

certbot-dns-rfc2136: Use TCP queries to improve the network robust. Fixes #7502.