feature: Support verify shaping DSCP value.

This commit is contained in:
fumingwei
2024-05-27 14:46:58 +08:00
parent d023a9f41e
commit 547004c2ec
2 changed files with 128 additions and 35 deletions

View File

@@ -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

View File

@@ -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)