diff --git a/images_build/client/Dockerfile b/images_build/client/Dockerfile index 640e20e..e42b6b7 100644 --- a/images_build/client/Dockerfile +++ b/images_build/client/Dockerfile @@ -6,7 +6,7 @@ ADD dign_client /opt/dign_client RUN sed -i s@/dl-cdn.alpinelinux.org/@/mirrors.ustc.edu.cn/@g /etc/apk/repositories \ && apk update \ - && apk add curl-dev gcc libc-dev curl gzip \ + && apk add curl-dev gcc libc-dev curl gzip libpcap-dev\ && pip3 install pycurl \ && pip3 install httpstat \ && pip3 install CIUnitTest \ @@ -14,6 +14,7 @@ RUN sed -i s@/dl-cdn.alpinelinux.org/@/mirrors.ustc.edu.cn/@g /etc/apk/repositor && pip3 install dnspython \ && pip3 install prettytable \ && pip3 install pyyaml \ + && pip3 install scapy \ && mv /opt/dign_client/etc/client.conf /opt/dign_client/etc/client.conf.sample WORKDIR /opt/dign_client diff --git a/images_build/client/dign_client/bin/client.py b/images_build/client/dign_client/bin/client.py index 01e1651..bb07bdd 100644 --- a/images_build/client/dign_client/bin/client.py +++ b/images_build/client/dign_client/bin/client.py @@ -20,6 +20,13 @@ import logging import copy from prettytable import PrettyTable,NONE,HEADER import yaml +from urllib.parse import urlparse + +from scapy.all import * +from scapy.layers.inet import IP, TCP +from scapy.sendrecv import AsyncSniffer + +from urllib.parse import urlparse, parse_qs class ConfigLoader: DEFAULT_CONFIGS = { @@ -326,43 +333,93 @@ class CommandParser: class ServerAddressBuilder: IPv4_4TH_OCTET_LEFT_EDGE = 101 - DOMAIN_TO_PORT_LIST = [ - ('sha384.badssl.selftest.gdnt-cloud.website', 443), - ('sha256.badssl.selftest.gdnt-cloud.website', 443), - ('expired.badssl.selftest.gdnt-cloud.website', 443), - ('self-signed.badssl.selftest.gdnt-cloud.website', 443), - ('untrusted-root.badssl.selftest.gdnt-cloud.website', 443), - ('web-replay.badssl.selftest.gdnt-cloud.website', 80), - ('web-replay.badssl.selftest.gdnt-cloud.website', 443), - ('testing-download.badssl.selftest.gdnt-cloud.website', 443), - ('http.badssl.selftest.gdnt-cloud.website', 80), - ('http-credit-card.badssl.selftest.gdnt-cloud.website', 80), - ('http-dynamic-login.badssl.selftest.gdnt-cloud.website', 80), - ('http-login.badssl.selftest.gdnt-cloud.website', 80), - ('sha512.badssl.selftest.gdnt-cloud.website', 443), - ('rsa2048.badssl.selftest.gdnt-cloud.website', 443), - ('rsa4096.badssl.selftest.gdnt-cloud.website', 443), - ('testing-firewall-filter-host.badssl.selftest.gdnt-cloud.website', 80), - ('testing-firewall-filter-url.badssl.selftest.gdnt-cloud.website', 80), - ('testing-proxy-filter-host.badssl.selftest.gdnt-cloud.website', 80), - ('testing-proxy-filter-url.badssl.selftest.gdnt-cloud.website', 80), - ('testing-rate-limit-0bps.badssl.selftest.gdnt-cloud.website', 80), - ('testing-rate-limit-0bps.badssl.selftest.gdnt-cloud.website', 443), - ('testing-rate-limit-1000gbps.badssl.selftest.gdnt-cloud.website', 80), - ('testing-rate-limit-1000gbps.badssl.selftest.gdnt-cloud.website', 443) - ] + IPv4_1_TO_3TH_OCTET = "192.0.2" def __init__(self, service_function_index: int): self._service_function_index = service_function_index self._ipv4_4th_octet = self.IPv4_4TH_OCTET_LEFT_EDGE + self._service_function_index - @property - def resolves(self) -> list: - return [f"{domain}:{port}:192.0.2.{self._ipv4_4th_octet}" for domain, port in self.DOMAIN_TO_PORT_LIST] + def read_resolves_by_url(self, url): + parsed_url = urlparse(url) + port = parsed_url.port + if not port: + if parsed_url.scheme == 'https': + port = 443 + elif parsed_url.scheme == 'http': + port = 80 + return [f"{parsed_url.hostname}:{port}:{self.IPv4_1_TO_3TH_OCTET}.{self._ipv4_4th_octet}"] + + + # @property + # def resolves(self) -> list: + # return [f"{domain}:{port}:192.0.2.{self._ipv4_4th_octet}" for domain, port in self.DOMAIN_TO_PORT_LIST] @property def nameservers(self) -> list: - return ['192.0.2.%d' % self._ipv4_4th_octet] + return [f'{self.IPv4_1_TO_3TH_OCTET}.{self._ipv4_4th_octet}'] + +class TcpPacketsCapture: + IFACE = "net1" + DSCP_KEY = ["DSCP"] + def __init__(self, server_ip, server_port): + self._server_ip = server_ip + self._server_port = server_port + self._quadruple_to_dscp_table = {} + self._build_filter() + + # def _read_server_ip_and_port(self): + + def _build_filter(self): + self._filter = f"tcp and src host {self._server_ip} and src port {self._server_port}" + + def start(self): + self._sniff_thread = AsyncSniffer(iface=self.IFACE, prn=self._packet_callback, filter=self._filter) + self._sniff_thread.start() + + def _packet_callback(self, packet): + if IP in packet and TCP in packet: + src_ip = packet[IP].src + if src_ip == self._server_ip: + dscp_value = self._read_dscp_value_from_packet(packet) + quadruple = self._build_quadruple(packet, True) + self._update_quadruple_to_dscp_table(quadruple, dscp_value) + + def _read_dscp_value_from_packet(self, packet): + ip_header = packet[IP] + return (ip_header.tos & 0xfc) >> 2 + + def _build_quadruple(self, packet, is_s2c): + src_ip = packet[IP].src + dst_ip = packet[IP].dst + src_port = packet[TCP].sport + dst_port = packet[TCP].dport + if is_s2c: + return f"{dst_ip}:{dst_port},{src_ip}:{src_port}" + else: + return f"{src_ip}:{src_port},{dst_ip}:{dst_port}" + + def _update_quadruple_to_dscp_table(self, quadruple, dscp_value): + self._quadruple_to_dscp_table[quadruple] = dscp_value + + def stop(self): + self._sniff_thread.stop() + self._sniff_thread.join() + + def read_dscp_value_by_quadruple(self, quadruple): + if quadruple in self._quadruple_to_dscp_table: + return self._quadruple_to_dscp_table[quadruple] + else: + return None + +class TcpPacketsCaptureAnalyzer: + def is_dscp_equal(self, present_dscp, desired_dscp): + if present_dscp is None: + return False, f"Error: Not read DSCP value." + + if present_dscp == desired_dscp: + return True, None + else: + return False, f"Error: Failed to verify DSCP value. Present DSCP: {present_dscp}, desired DSCP: {desired_dscp}." class URLTransferBuilder: def __init__(self, url: str, request_resolve: list, conn_timeout: int, max_recv_speed_large: int): @@ -375,6 +432,10 @@ class URLTransferBuilder: self._response_buffer = BytesIO() self._error_info = None self._size_download = None + self._local_ip = None + self._local_port = None + self._remote_ip = None + self._remote_port = None def _setup_connection(self): self._response_buffer = BytesIO() @@ -389,6 +450,10 @@ class URLTransferBuilder: self._conn.perform() self._response_code = self._conn.getinfo(self._conn.RESPONSE_CODE) self._size_download = self._conn.getinfo(pycurl.SIZE_DOWNLOAD) + self._local_ip = self._conn.getinfo(pycurl.LOCAL_IP) + self._local_port = self._conn.getinfo(pycurl.LOCAL_PORT) + self._remote_ip = self._conn.getinfo(pycurl.PRIMARY_IP) + self._remote_port = self._conn.getinfo(pycurl.PRIMARY_PORT) def _close_connection(self): self._conn.close() @@ -418,6 +483,10 @@ class URLTransferBuilder: def size_download(self): return self._size_download + @property + def quadruple(self): + return f"{self._local_ip}:{self._local_port},{self._remote_ip}:{self._remote_port}" + class HttpURLTransferBuilder(URLTransferBuilder): def _perform_connection(self): super()._perform_connection() @@ -784,6 +853,7 @@ class ShapingCaseRunner: def __init__(self) -> None: self._analyzer = URLTransferResponseAnalyzer() self._dns_analyzer = DNSResponseAnalyzer() + self._capture_analyzer = TcpPacketsCaptureAnalyzer() def rate_limit_0bps_protocol_http(self, url, resolves, conn_timeout, max_recv_speed_large): conn = HttpURLTransferBuilder(url, resolves, conn_timeout, max_recv_speed_large) @@ -802,19 +872,31 @@ class ShapingCaseRunner: return True, None def rate_limit_1000gbps_protocol_http(self, url, resolves, conn_timeout, max_recv_speed_large): + server_ip, server_port = self._read_server_ip_and_port_from_resolve(resolves) + capture = TcpPacketsCapture(server_ip, server_port) + capture.start() conn = HttpURLTransferBuilder(url, resolves, conn_timeout, max_recv_speed_large) conn.connect() + capture.stop() is_error_none = self._analyzer.is_pycurl_error_none(conn.error_info) if not is_error_none[0]: return False, is_error_none[1] is_code_equal = self._analyzer.is_response_code_equal(conn.response_code, 200) if not is_code_equal[0]: return False, is_code_equal[1] + present_dscp = capture.read_dscp_value_by_quadruple(conn.quadruple) + is_dscp_equal = self._capture_analyzer.is_dscp_equal(present_dscp, 8) + if not is_dscp_equal[0]: + return False, is_dscp_equal[1] return True, None def rate_limit_1000gbps_protocol_https(self, url, resolves, conn_timeout, max_recv_speed_large): + server_ip, server_port = self._read_server_ip_and_port_from_resolve(resolves) + capture = TcpPacketsCapture(server_ip, server_port) + capture.start() conn = HttpsURLTransferBuilder(url, resolves, conn_timeout, max_recv_speed_large) conn.connect() + capture.stop() is_error_none = self._analyzer.is_pycurl_error_none(conn.error_info) if not is_error_none[0]: return False, is_error_none[1] @@ -824,8 +906,17 @@ class ShapingCaseRunner: is_cert_matched = self._analyzer.is_cert_issuer_matched(conn.cert_issuer, r'\bCN[\s]*=[\s]*BadSSL\b') if not is_cert_matched[0]: return False, is_cert_matched[1] + present_dscp = capture.read_dscp_value_by_quadruple(conn.quadruple) + is_dscp_equal = self._capture_analyzer.is_dscp_equal(present_dscp, 8) + if not is_dscp_equal[0]: + return False, is_dscp_equal[1] return True, None + def _read_server_ip_and_port_from_resolve(self, resolves): + resolve = resolves[0] + resolve_split = resolve.split(":") + return resolve_split[2], resolve_split[1] + class FirewallCasesRunner: def __init__(self) -> None: self._analyzer = URLTransferResponseAnalyzer() @@ -1415,24 +1506,25 @@ class DiagnoseCasesRunner: print(format(("Service function name: " + str(sf_pair["name"]) + ",Test end time: " + end_timestamp),'=^100s')) def _run_cases_in_one_service_function_id(self, service_function_id): - resolve_Builder = ServerAddressBuilder(service_function_id) + resolve_builder = ServerAddressBuilder(service_function_id) exporter = ResultExportBuilder() for case_info in self._cases_info: if re.fullmatch(self._case_names_regexp, case_info["name"]): - self._run_one_case(case_info, resolve_Builder, exporter) + self._run_one_case(case_info, resolve_builder, exporter) exporter.export() - def _run_one_case(self, case_info, resolve_Builder, exporter): + def _run_one_case(self, case_info, resolve_builder, exporter): conn_timeout = self._get_conn_timeout_from_configs(case_info["name"]) max_recv_speed_large = self._get_max_recv_speed_large_from_configs(case_info["name"]) test_func = case_info.get("test_function") if test_func: if case_info["protocol_type"] == "http" or case_info["protocol_type"] == "https": - ret = test_func(case_info["request_content"], resolve_Builder.resolves, conn_timeout, max_recv_speed_large) + resolve = resolve_builder.read_resolves_by_url(case_info["request_content"]) + ret = test_func(case_info["request_content"], resolve, conn_timeout, max_recv_speed_large) if case_info["protocol_type"] == "dns": - ret = test_func(case_info["request_content"], resolve_Builder.nameservers, conn_timeout) + ret = test_func(case_info["request_content"], resolve_builder.nameservers, conn_timeout) exporter.append_case_result(case_info["name"], ret)