diff --git a/slapos/promise/plugin/check_surykatka_json.py b/slapos/promise/plugin/check_surykatka_json.py
index 3f6603bc717abf7aff78592d14b4fcb7031a28d3..7d5a8407c737975bf6eefbd75d79600278d1497b 100644
--- a/slapos/promise/plugin/check_surykatka_json.py
+++ b/slapos/promise/plugin/check_surykatka_json.py
@@ -232,6 +232,53 @@ class RunPromise(GenericPromise):
             "OK resolver %s returned expected set of IPs %s" % (
               entry['resolver_ip'], ' '.join(sorted(ip_set)),))
 
+  def senseTcpServer(self):
+    key = 'tcp_server'
+
+    def appendError(msg, *args):
+      self.error = True
+      self.appendMessage(key + ': ERROR ' + msg % args)
+
+    if key not in self.surykatka_json:
+      appendError("%r not in %r", key, self.json_file)
+      return
+
+    url = self.getConfig('url')
+    parsed_url = urlparse(url)
+    hostname = parsed_url.hostname
+    if parsed_url.port is not None:
+      port = parsed_url.port
+    else:
+      if parsed_url.scheme == 'https':
+        port = 443
+      else:
+        port = 80
+    ip_set = set(self.getConfig('ip-list', '').split())
+
+    entry_list = [
+      q for q in self.surykatka_json[key]
+      if hostname in [
+        r.strip() for r in q['domain'].split(',')] and q['port'] == port]
+    if len(entry_list) == 0:
+      appendError('No data')
+      return
+    if len(ip_set) > 0:
+      self.appendMessage('%s:' % (key,))
+      for ip in sorted(ip_set):
+        ok = False
+        for entry in entry_list:
+          if entry['ip'] == ip:
+            if entry['state'] == 'closed':
+              ok = False
+              break
+            if entry['state'] == 'open':
+              ok = True
+        if ok:
+          self.appendMessage('OK IP %s:%s' % (ip, port))
+        else:
+          self.error = True
+          self.appendMessage('ERROR IP %s:%s' % (ip, port))
+
   def senseElapsedTime(self):
     key = 'elapsed_time'
     surykatka_key = 'http_query'
@@ -306,6 +353,7 @@ class RunPromise(GenericPromise):
             self.senseBotStatus()
           elif report == 'http_query':
             self.senseDnsQuery()
+            self.senseTcpServer()
             self.senseHttpQuery()
             self.senseSslCertificate()
             self.senseElapsedTime()
diff --git a/slapos/test/promise/plugin/test_check_surykatka_json.py b/slapos/test/promise/plugin/test_check_surykatka_json.py
index 9096a908c6b37b5c7f1841e028ad5baf96d866e2..69b6778e72c52f3f85a755c3bce0e6b630db7c74 100644
--- a/slapos/test/promise/plugin/test_check_surykatka_json.py
+++ b/slapos/test/promise/plugin/test_check_surykatka_json.py
@@ -282,6 +282,22 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "response": "127.0.0.1, 127.0.0.2"
         },
       ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
     })
     self.configureLauncher(enable_anomaly=True)
     self.launcher.run()
@@ -290,6 +306,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       "https://www.erp5.com/ : "
       "dns_query: OK resolver 1.2.3.4 returned expected set of IPs 127.0.0.1 "
       "127.0.0.2 "
+      "tcp_server: OK IP 127.0.0.1:443 OK IP 127.0.0.2:443 "
       "http_query: OK IP 127.0.0.1 status_code 302 OK IP 127.0.0.2 "
       "status_code 302 "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
@@ -361,6 +378,22 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "response": "127.0.0.1, 127.0.0.2"
         },
       ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
     })
     self.configureLauncher(enable_anomaly=True)
     self.launcher.run()
@@ -369,6 +402,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       "https://www.erp5.com/ : "
       "dns_query: OK resolver 1.2.3.4 returned expected set of IPs 127.0.0.1 "
       "127.0.0.2 "
+      "tcp_server: OK IP 127.0.0.1:443 OK IP 127.0.0.2:443 "
       "http_query: OK IP 127.0.0.1 status_code 302 OK IP 127.0.0.2 "
       "status_code 302 "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
@@ -426,7 +460,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_future60d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -435,6 +470,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: OK IP 127.0.0.1 status_code 302 OK IP 127.0.0.2 "
       "status_code 302 "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
@@ -504,6 +540,22 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "date": "Tue, 28 Feb 2023 12:40:29 -0000",
             "response": "127.0.0.1, 127.0.0.2"
         },
+      ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
       ]
     })
     self.configureLauncher(enable_anomaly=True)
@@ -513,6 +565,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       "https://www.erp5.com/ : "
       "dns_query: OK resolver 1.2.3.4 returned expected set of IPs 127.0.0.1 "
       "127.0.0.2 "
+      "tcp_server: OK IP 127.0.0.1:443 OK IP 127.0.0.2:443 "
       "http_query: OK IP 127.0.0.1 status_code 302 OK IP 127.0.0.2 "
       "status_code 302 "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
@@ -568,6 +621,22 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "response": "127.0.0.1, 127.0.0.2"
         },
       ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "open",
+            "port": 80,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 80,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
     })
     self.configureLauncher(enable_anomaly=True)
     self.launcher.run()
@@ -576,6 +645,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       "http://www.erp5.com/ : "
       "dns_query: OK resolver 1.2.3.4 returned expected set of IPs "
       "127.0.0.1 127.0.0.2 "
+      "tcp_server: OK IP 127.0.0.1:80 OK IP 127.0.0.2:80 "
       "http_query: OK IP 127.0.0.1 status_code 302 OK IP 127.0.0.2 "
       "status_code 302"
     )
@@ -620,6 +690,22 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "response": "127.0.0.1, 127.0.0.2"
         },
       ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "open",
+            "port": 80,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 80,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
     })
     self.configureLauncher(enable_anomaly=True)
     self.launcher.run()
@@ -672,6 +758,22 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "response": "127.0.0.1, 127.0.0.2"
         },
       ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "open",
+            "port": 80,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 80,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -745,6 +847,22 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "response": "176.31.129.213"
         },
       ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
     })
     self.configureLauncher(enable_anomaly=True)
     self.launcher.run()
@@ -800,6 +918,22 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "response": "127.0.0.1, 127.0.0.2"
         },
       ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
     })
     self.configureLauncher(enable_anomaly=True)
     self.launcher.run()
@@ -837,7 +971,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_future1d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -846,6 +981,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: OK IP 127.0.0.1 status_code 302 "
       "ssl_certificate: ERROR IP 127.0.0.1 will expire in < 2 days"
     )
@@ -876,7 +1012,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_future14d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -885,6 +1022,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: OK IP 127.0.0.1 status_code 302 "
       "ssl_certificate: ERROR IP 127.0.0.1 will expire in < 15 days"
     )
@@ -915,7 +1053,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_past14d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": []
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -924,6 +1063,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: OK IP 127.0.0.1 status_code 302 "
       "ssl_certificate: ERROR IP 127.0.0.1 will expire in < 15 days")
 
@@ -954,7 +1094,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_future60d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -963,6 +1104,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: ERROR No data "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
       "127.0.0.2 will expire in > 15 days "
@@ -994,7 +1136,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_future60d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1003,6 +1146,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: ERROR 'http_query' not in %(json_file)r "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
       "127.0.0.2 will expire in > 15 days "
@@ -1044,7 +1188,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       "ssl_certificate": [
       ],
       "dns_query": [
-      ]
+      ],
+      "tcp_server": []
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1053,6 +1198,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: OK IP 127.0.0.1 status_code 302 OK IP 127.0.0.2 "
       "status_code 302 "
       "ssl_certificate: ERROR No data"
@@ -1089,7 +1235,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "url": "https://www.erp5.org/"
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": []
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1098,6 +1245,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: OK IP 127.0.0.1 status_code 302 OK IP 127.0.0.2 "
       "status_code 302 "
       "ssl_certificate: ERROR No key 'ssl_certificate'. If the error "
@@ -1148,7 +1296,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_future60d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": []
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1157,6 +1306,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: ERROR IP 127.0.0.1 expected status_code 302 != 301 OK IP "
       "127.0.0.2 status_code 301 "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
@@ -1195,7 +1345,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_future60d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1204,6 +1355,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: ERROR IP 127.0.0.1 expected status_code %s != 301 "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
       "127.0.0.2 will expire in > 15 days" % (explanation,)
@@ -1266,7 +1418,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_future60d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1275,6 +1428,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: OK IP 127.0.0.1 status_code 301 OK IP 127.0.0.4 "
       "status_code 301 "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
@@ -1326,7 +1480,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": self.time_future60d
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1335,6 +1490,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: ERROR IP 127.0.0.1 expected status_code 302 != 301 OK IP "
       "127.0.0.4 status_code 301 "
       "ssl_certificate: OK IP 127.0.0.1 will expire in > 15 days OK IP "
@@ -1380,7 +1536,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
           "not_after": None
         }
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1389,6 +1546,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: ERROR IP 127.0.0.1 expected status_code 302 != 301 OK IP "
       "127.0.0.4 status_code 301 "
       "ssl_certificate: ERROR IP 127.0.0.1 no information ERROR IP 127.0.0.2 "
@@ -1410,7 +1568,8 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       ],
       "ssl_certificate": [
       ],
-      "dns_query": []
+      "dns_query": [],
+      "tcp_server": [],
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1419,6 +1578,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
       "http_query: ERROR No data "
       "ssl_certificate: ERROR No data "
       "elapsed_time: ERROR No data"
@@ -1439,6 +1599,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       ],
       "ssl_certificate": [
       ],
+      "tcp_server": []
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1447,6 +1608,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       self.getPromiseResult(self.promise_name),
       "https://www.erp5.com/ : "
       "dns_query: ERROR 'dns_query' not in %(json_file)r "
+      "tcp_server: ERROR No data "
       "http_query: ERROR No data "
       "ssl_certificate: ERROR No data "
       "elapsed_time: ERROR No data" % {'json_file': self.json_file}
@@ -1483,6 +1645,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "response": "127.0.0.1, 127.0.0.2"
         },
       ],
+      "tcp_server": []
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1492,6 +1655,7 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       "https://www.erp5.com/ : "
       "dns_query: ERROR resolver 1.2.3.4 expected 127.0.0.1 127.0.0.2 != "
       "127.0.0.2 127.0.0.3 "
+      "tcp_server: ERROR No data "
       "http_query: ERROR No data "
       "ssl_certificate: ERROR No data "
       "elapsed_time: ERROR No data"
@@ -1528,6 +1692,22 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
             "response": "127.0.0.1, 127.0.0.2"
         },
       ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
     })
     self.configureLauncher(enable_anomaly=True)
     with self.assertRaises(PromiseError):
@@ -1537,7 +1717,166 @@ class TestCheckSurykatkaJSONHttpQuery(CheckSurykatkaJSONMixin):
       "https://www.erp5.com/ : "
       "dns_query: ERROR resolver 1.2.3.4 expected 127.0.0.1 127.0.0.2 != "
       "empty-reply "
+      "tcp_server: OK IP 127.0.0.1:443 OK IP 127.0.0.2:443 "
+      "http_query: ERROR No data "
+      "ssl_certificate: ERROR No data "
+      "elapsed_time: ERROR No data"
+    )
+
+  def test_tcp_server_no_ip(self):
+    self.writeSurykatkaPromise(
+      {
+        'report': 'http_query',
+        'json-file': self.json_file,
+        'url': 'https://www.erp5.com/',
+        'status-code': '301',
+        'ip-list': '127.0.0.1 127.0.0.2',
+      }
+    )
+    self.writeSurykatkaJson({
+      "http_query": [
+      ],
+      "ssl_certificate": [
+      ],
+      "dns_query": [
+      ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.3",
+            "state": "filtered",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.4",
+            "state": "open",
+            "port": 80,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
+    })
+    self.configureLauncher(enable_anomaly=True)
+    with self.assertRaises(PromiseError):
+      self.launcher.run()
+    self.assertFailedMessage(
+      self.getPromiseResult(self.promise_name),
+      "https://www.erp5.com/ : "
+      "dns_query: ERROR No data "
+      "tcp_server: ERROR IP 127.0.0.1:443 ERROR IP 127.0.0.2:443 "
+      "http_query: ERROR No data "
+      "ssl_certificate: ERROR No data "
+      "elapsed_time: ERROR No data"
+    )
+
+  def test_tcp_server_mismatch(self):
+    self.writeSurykatkaPromise(
+      {
+        'report': 'http_query',
+        'json-file': self.json_file,
+        'url': 'https://www.erp5.com/',
+        'status-code': '301',
+        'ip-list': '127.0.0.1 127.0.0.2',
+      }
+    )
+    self.writeSurykatkaJson({
+      "http_query": [
+      ],
+      "ssl_certificate": [
+      ],
+      "dns_query": [
+      ],
+      "tcp_server": [
+        {
+            "ip": "127.0.0.1",
+            "state": "filtered",
+            "port": 443,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+        {
+            "ip": "127.0.0.2",
+            "state": "open",
+            "port": 80,
+            "date": "Tue, 28 Feb 2023 09:27:26 -0000",
+            "domain": "www.erp5.com"
+        },
+      ]
+    })
+    self.configureLauncher(enable_anomaly=True)
+    with self.assertRaises(PromiseError):
+      self.launcher.run()
+    self.assertFailedMessage(
+      self.getPromiseResult(self.promise_name),
+      "https://www.erp5.com/ : "
+      "dns_query: ERROR No data "
+      "tcp_server: ERROR IP 127.0.0.1:443 ERROR IP 127.0.0.2:443 "
       "http_query: ERROR No data "
       "ssl_certificate: ERROR No data "
       "elapsed_time: ERROR No data"
     )
+
+  def test_tcp_server_no_entry(self):
+    self.writeSurykatkaPromise(
+      {
+        'report': 'http_query',
+        'json-file': self.json_file,
+        'url': 'https://www.erp5.com/',
+        'status-code': '301',
+        'ip-list': '127.0.0.1 127.0.0.2',
+      }
+    )
+    self.writeSurykatkaJson({
+      "http_query": [
+      ],
+      "ssl_certificate": [
+      ],
+      "dns_query": [
+      ],
+      "tcp_server": [
+      ]
+    })
+    self.configureLauncher(enable_anomaly=True)
+    with self.assertRaises(PromiseError):
+      self.launcher.run()
+    self.assertFailedMessage(
+      self.getPromiseResult(self.promise_name),
+      "https://www.erp5.com/ : "
+      "dns_query: ERROR No data "
+      "tcp_server: ERROR No data "
+      "http_query: ERROR No data "
+      "ssl_certificate: ERROR No data "
+      "elapsed_time: ERROR No data"
+    )
+
+  def test_tcp_server_no_key(self):
+    self.writeSurykatkaPromise(
+      {
+        'report': 'http_query',
+        'json-file': self.json_file,
+        'url': 'https://www.erp5.com/',
+        'status-code': '301',
+        'ip-list': '127.0.0.1 127.0.0.2',
+      }
+    )
+    self.writeSurykatkaJson({
+      "http_query": [
+      ],
+      "ssl_certificate": [
+      ],
+      "dns_query": [
+      ],
+    })
+    self.configureLauncher(enable_anomaly=True)
+    with self.assertRaises(PromiseError):
+      self.launcher.run()
+    self.assertFailedMessage(
+      self.getPromiseResult(self.promise_name),
+      "https://www.erp5.com/ : "
+      "dns_query: ERROR No data "
+      "tcp_server: ERROR 'tcp_server' not in %(json_file)r "
+      "http_query: ERROR No data "
+      "ssl_certificate: ERROR No data "
+      "elapsed_time: ERROR No data" % {'json_file': self.json_file}
+    )