Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ repos:
hooks:
- id: isort
- repo: https://github.com/ambv/black
rev: 25.12.0
rev: 26.3.1
hooks:
- id: black
2 changes: 2 additions & 0 deletions framework/deproxy/deproxy_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ def __init__(
segment_size: int,
segment_gap: int,
is_ipv6: bool,
rcv_buf_size: int,
):
asyncore.DeproxyAsyncore.__init__(self, is_ipv6)

Expand All @@ -36,6 +37,7 @@ def __init__(
self.bind_addr = bind_addr
self.segment_size = segment_size or run_config.TCP_SEGMENTATION or 0
self.segment_gap = segment_gap
self.rcv_buf_size = rcv_buf_size
self.__polling_lock: Optional[threading.Lock] = None

self._tcp_logger = logging.LoggerAdapter(
Expand Down
6 changes: 6 additions & 0 deletions framework/deproxy/deproxy_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ def __init__(
conn_addr: Optional[str],
is_ssl: bool,
server_hostname: str,
rcv_buf_size: int,
):
# Initialize the `BaseDeproxy`
super().__init__(
Expand All @@ -65,6 +66,7 @@ def __init__(
segment_size=segment_size,
segment_gap=segment_gap,
is_ipv6=is_ipv6,
rcv_buf_size=rcv_buf_size,
)

self.writable = self._in_connecting_state
Expand Down Expand Up @@ -253,6 +255,10 @@ def _stop_deproxy(self):

def _run_deproxy(self):
self._create_socket()
if self.rcv_buf_size >= 0:
# don't expect that buffer will have exactly the same size as passed to `setsockopt()`,
# the kernel may increase this size.
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, self.rcv_buf_size)
if self.bind_addr:
self._bind(
(self.bind_addr, 0),
Expand Down
3 changes: 3 additions & 0 deletions framework/deproxy/deproxy_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ def __init__(
delay_before_sending_response: float,
hang_on_req_num: int,
pipelined: int,
rcv_buf_size: int,
):
# this variable is needed for tests with common response for all tests in one class.
self._default_response = response
Expand All @@ -193,6 +194,7 @@ def __init__(
segment_size=segment_size,
segment_gap=segment_gap,
is_ipv6=is_ipv6,
rcv_buf_size=rcv_buf_size,
)
base_server.BaseServer.__init__(self, id_)
self.keep_alive = keep_alive
Expand Down Expand Up @@ -351,6 +353,7 @@ def deproxy_srv_initializer(
delay_before_sending_response=server.get("delay_before_sending_response", 0.0),
hang_on_req_num=server.get("hang_on_req_num", 0),
pipelined=server.get("pipelined", 0),
rcv_buf_size=server.get("rcv_buf_size", -1),
)

tester.deproxy_manager.add_server(srv)
Expand Down
3 changes: 2 additions & 1 deletion framework/test_suite/tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ def __create_client_deproxy(self, client: dict, ssl: bool, bind_addr: str):
conn_addr=fill_template(client["addr"], client),
is_ssl=ssl,
server_hostname=fill_template(client.get("ssl_hostname", None), client),
rcv_buf_size=client.get("rcv_buf_size", -1),
)

def __create_client_wrk(self, client, ssl):
Expand Down Expand Up @@ -239,7 +240,7 @@ def __create_client(self, client):
if ctype in ["curl", "deproxy", "deproxy_h2"]:
if client.get("interface", False):
networker = NetWorker(node=remote.client)
(_, bind_addr) = networker.create_interface(len(self.__ips))
_, bind_addr = networker.create_interface(len(self.__ips))
networker.create_route(bind_addr)
self.__ips.append(bind_addr)
else:
Expand Down
51 changes: 5 additions & 46 deletions tests/http2_general/test_h2_frame.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Functional tests for h2 frames."""

__author__ = "Tempesta Technologies, Inc."
__copyright__ = "Copyright (C) 2023-2025 Tempesta Technologies, Inc."
__copyright__ = "Copyright (C) 2023-2026 Tempesta Technologies, Inc."
__license__ = "GPL2"

from h2.connection import ConnectionInputs
Expand Down Expand Up @@ -92,43 +92,6 @@ async def test_empty_data_frame(self):
self.assertTrue(await deproxy_cl.wait_for_connection_close(timeout=5))
deproxy_cl.assert_error_code(expected_error_code=ErrorCodes.PROTOCOL_ERROR)

async def test_opening_same_stream_after_invalid(self):
"""
Try to open stream with invalid HEADERS frame.
Then try to open stream with same id.
Connection should be closed.
"""
await self.start_all_services()
deproxy_cl = self.get_client("deproxy")
deproxy_cl.parsing = False

deproxy_cl.update_initial_settings()
deproxy_cl.send_bytes(deproxy_cl.h2_connection.data_to_send())
await deproxy_cl.wait_for_ack_settings()

stream = deproxy_cl.init_stream_for_send(deproxy_cl.stream_id)
deproxy_cl.h2_connection.state_machine.process_input(ConnectionInputs.SEND_HEADERS)

hf_bad = HeadersFrame(
stream_id=stream.stream_id,
data=deproxy_cl.h2_connection.encoder.encode(self.get_request),
flags=["END_HEADERS", "END_STREAM", "PRIORITY"],
)

hf_bad.stream_weight = 255
hf_bad.depends_on = stream.stream_id
hf_bad.exclusive = False

hf_good = HeadersFrame(
stream_id=stream.stream_id,
data=deproxy_cl.h2_connection.encoder.encode(self.get_request),
flags=["END_HEADERS", "END_STREAM"],
)

deproxy_cl.send_bytes(hf_bad.serialize() + hf_good.serialize())
self.assertTrue(await deproxy_cl.wait_for_reset_stream(stream.stream_id))
self.assertTrue(await deproxy_cl.wait_for_connection_close())

async def test_multiple_empty_headers_frames(self):
"""
An endpoint that receives a HEADERS frame without the END_STREAM flag set
Expand Down Expand Up @@ -656,8 +619,7 @@ async def __send_header_and_data_frames_with_mixed_frames(

@marks.extend_tests_with_tso_gro_gso_enable_disable(mtu=DEFAULT_MTU)
class TestH2FrameEnabledDisabledTsoGroGsoStickyCookie(TestH2FrameEnabledDisabledTsoGroGsoBase):
tempesta = {
"config": """
tempesta = {"config": """
listen 443 proto=h2;
srv_group default {
server ${server_ip}:8000;
Expand All @@ -682,8 +644,7 @@ class TestH2FrameEnabledDisabledTsoGroGsoStickyCookie(TestH2FrameEnabledDisabled
host == "bad.com" -> block;
host == "example.com" -> v_good;
}
"""
}
"""}

@marks.Parameterize.expand(
[
Expand Down Expand Up @@ -711,8 +672,7 @@ async def test_headers_frame_for_local_resp_sticky_cookie(self, name, header_len

@marks.extend_tests_with_tso_gro_gso_enable_disable(mtu=DEFAULT_MTU)
class TestH2FrameEnabledDisabledTsoGroGsoCache(TestH2FrameEnabledDisabledTsoGroGsoBase):
tempesta = {
"config": """
tempesta = {"config": """
listen 443 proto=h2;
srv_group default {
server ${server_ip}:8000;
Expand All @@ -733,8 +693,7 @@ class TestH2FrameEnabledDisabledTsoGroGsoCache(TestH2FrameEnabledDisabledTsoGroG
host == "bad.com" -> block;
host == "example.com" -> v_good;
}
"""
}
"""}

@marks.Parameterize.expand(
[
Expand Down
117 changes: 111 additions & 6 deletions tests/http2_general/test_h2_hpack.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,20 +451,125 @@ async def test_bytes_of_table_size_in_header_frame_2(self):
This dynamic table size update MUST occur at the beginning of the first header
block following the change to the dynamic table size.
RFC 7541 4.2

This test checks that default 4096 table size is advertised to the client when the
client uses table size greater than default and Tempesta FW wants to use 4096 table size.
In other words test verify this statement:
"An encoder can choose to use less capacity than this maximum size" RFC 7541 4.2
"""
await self.start_all_services()

table_size = 12288
client = self.get_client("deproxy")
error_msg = "Tempesta did not add dynamic table size ({0}) before first header block."

client.update_initial_settings(header_table_size=table_size)
# Set the new size to the table to be able to check that it changed when Tempesta returns the new size
client.h2_connection.decoder.header_table_size = table_size
await client.send_request(request=self.post_request, expected_status_code="200")
# Client set HEADER_TABLE_SIZE = 12288 bytes, but Tempesta works with table 4096 bytes
# and this default value for table size.
# Therefore Tempesta does not return bytes of table size in header frame.
client.update_initial_settings(header_table_size=12288)
# and we expect \x3f\xe1\x07 bytes in first header frame
self.assertTrue(
client.check_header_presence_in_last_response_buffer(b"\x3f\xe1\x1f"),
error_msg.format(4096),
)
self.assertEqual(client.h2_connection.decoder.header_table_size, 4096)

async def test_bytes_of_table_size_in_header_frame_of_trailers(self):
"""
1. Client sets SETTINGS_INITIAL_WINDOW_SIZE = 0 bytes;
2. Server return response with body.
3. Tempesta must forward HEADERS frame and wait for a WindowUpdate.
4. Client send SETTINGS frame and wait for a SETTINGS frame with ack settings from
Tempesta.
5. Client send WindowUpdate and wait for a DATA frame.
6. Tempesta responds with the new table size in the first HEADERS frame of trailer.
7. Check that table size is correct.
"""
error_msg = (
"Tempesta did not add dynamic table size ({0}) before first header block in trailer."
)
new_table_size = 2048
resp_body = "x" * 100
await self.start_all_services()

server = self.get_server("deproxy")
server.set_response(
(
"HTTP/1.1 200 OK\r\n"
+ f"Date: {HttpMessage.date_time_string()}\r\n"
+ "Server: debian\r\n"
+ "Transfer-Encoding: chunked\r\n"
+ "Trailer: X-Hash\r\n\r\n"
+ "64\r\n"
+ (resp_body)
+ "\r\n0\r\n"
+ "trail-hash: 0123456789abcdef\r\n\r\n"
)
)

# Set SETTINGS_INITIAL_WINDOW_SIZE = 0
client = self.get_client("deproxy")
client.auto_flow_control = False
client.update_initial_settings(initial_window_size=0)
client.send_bytes(client.h2_connection.data_to_send())
await client.wait_for_ack_settings()

client.make_request(self.get_request)
self.assertTrue(await client.wait_for_headers_frame(stream_id=1))
# send the new table size
client.send_settings_frame(header_table_size=new_table_size)
# ensure that the current table size is equal to default 4096 and the same as default of Tempesta
self.assertEqual(client.h2_connection.decoder.header_table_size, 4096)
Comment on lines +520 to +523
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you change header_table_size here? Why do you send header_table_size=2048 but expected 4096? Sorry, it is difficult test and i think we need to more comments here. It is point 4 from docstring but I don't understand why do we need to change header_table_size here

And the assert self.assertEqual(client.h2_connection.decoder.header_table_size, 4096) checks python side here.

Copy link
Copy Markdown
Contributor Author

@const-t const-t Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added comment, we need this to be sure that size of dynamic table is 4096 at this moment, the same as for Tempesta.

self.assertTrue(
await client.wait_for_ack_settings(),
"Tempesta did not forward the SETTINGS frame when the window size is 0.",
)

client.increment_flow_control_window(stream_id=1, flow_controlled_length=100)

# we are ready to receive body and trailers to that
await client.wait_for_response(strict=True)

self.assertEqual(client.last_response.status, "200", "Status code mismatch.")
self.assertEqual(
client.last_response.body,
resp_body,
"Tempesta returned invalid response body.",
)

# check correctness of the new table size
self.assertTrue(
client.check_header_presence_in_last_response_buffer(b"\x3f\xe1\x0f"),
error_msg.format(new_table_size),
)

# ensure that new table size is applied by Tempesta and the client
self.assertEqual(client.h2_connection.decoder.header_table_size, new_table_size)
self.assertEqual(client.last_response.trailer.get("trail-hash", None), "0123456789abcdef")

async def test_bytes_of_table_size_in_header_frame_not_sent(self):
"""
This dynamic table size update MUST occur at the beginning of the first header
block following the change to the dynamic table size.
RFC 7541 4.2

This test checks that Tempesta FW doesn't send table size update if encoder and
decoder have the same size.
"""
await self.start_all_services()

table_size = 4096
client = self.get_client("deproxy")
error_msg = "Tempesta did not add dynamic table size ({0}) before first header block."

# Client set HEADER_TABLE_SIZE = 4096 bytes and Tempesta works with table 4096 bytes
# therefore Tempesta does not return bytes of table size in header frame.
client.update_initial_settings(header_table_size=table_size)
self.assertEqual(client.h2_connection.decoder.header_table_size, table_size)
await client.send_request(request=self.post_request, expected_status_code="200")
self.assertFalse(
client.check_header_presence_in_last_response_buffer(
b"\x3f\xe1\x1f",
),
client.check_header_presence_in_last_response_buffer(b"\x3f\xe1\x1f"),
"Tempesta added dynamic table size (4096) before first header block.",
)
self.assertEqual(client.h2_connection.decoder.header_table_size, 4096)
Expand Down
Loading