mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 15:30:59 +00:00
136 lines
6.2 KiB
Python
136 lines
6.2 KiB
Python
"""Combinations that are refused *before* anything starts, and the reasons given.
|
|
|
|
`Transport.check()` is where an impossible combination is turned away, and it is asked
|
|
twice: at creation with an empty chain, for what the *service* alone makes impossible,
|
|
and again whenever a chain is pushed, for what the *chain* makes impossible. A row that
|
|
is created happily and then refuses to start every time is a trap.
|
|
|
|
A refused edit must also change nothing. `_apply_chain(service_id, undo)` rolls the write
|
|
back when the transport rejects the resulting chain — without it an operator is told
|
|
their filter was rejected and then finds it in the list, and the next start fails for the
|
|
reason they thought they had avoided.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from integration.conftest import add_regex_filter, start_and_settle
|
|
|
|
pytestmark = pytest.mark.instance
|
|
|
|
#: One binary and one pair of base chains per filter position, at `-307 + position` and
|
|
#: `107 + position`. That is what makes the ceiling real rather than arbitrary: each
|
|
#: filter costs a process and a reassembly pass.
|
|
MAX_CHAIN_POSITIONS = 8
|
|
|
|
|
|
def _fill_chain(api, service_id: str, count: int):
|
|
for position in range(count):
|
|
assert api.services_add_filter(service_id, "regex", f"f{position}"), \
|
|
f"could not attach filter {position} while the service was stopped"
|
|
|
|
|
|
def test_a_queued_chain_longer_than_the_ceiling_refuses_the_start(api, protected):
|
|
"""A stopped service is only configuration, so the filters are stored.
|
|
|
|
The refusal arrives when there is a chain to push — which for a stopped service is
|
|
the moment it is started. That is the "asked twice" design: at creation, with an
|
|
empty chain, for what the *service* makes impossible, and again whenever a chain is
|
|
pushed, for what the *chain* does.
|
|
"""
|
|
from integration.conftest import Layer
|
|
|
|
service_id, _, _ = protected(Layer("nfqueue"), name="long")
|
|
_fill_chain(api, service_id, MAX_CHAIN_POSITIONS + 1)
|
|
|
|
why = api.services_start_error(service_id)
|
|
assert why is not None, \
|
|
f"a chain of {MAX_CHAIN_POSITIONS + 1} filters started on the queued layer"
|
|
assert str(MAX_CHAIN_POSITIONS) in why, why
|
|
|
|
|
|
def test_the_ceiling_counts_active_filters_so_switching_one_off_lets_it_start(
|
|
api, protected):
|
|
"""Deactivating is the way out the refusal itself offers, so it has to work."""
|
|
from integration.conftest import Layer
|
|
|
|
service_id, _, _ = protected(Layer("nfqueue"), name="deact")
|
|
_fill_chain(api, service_id, MAX_CHAIN_POSITIONS + 1)
|
|
assert api.services_start_error(service_id) is not None
|
|
|
|
spare = api.services_filters(service_id)[-1]["filter_id"]
|
|
assert api.services_edit_filter(service_id, spare, active=False)
|
|
assert api.services_start_error(service_id) is None, \
|
|
"switching a filter off did not bring the chain under the ceiling"
|
|
|
|
|
|
def test_a_refused_chain_change_leaves_the_chain_as_it_was(api, protected):
|
|
"""Told it was rejected, and then finding it in the list, is the failure this prevents.
|
|
|
|
Asked of a *running* service, which is where a chain is pushed as it is edited:
|
|
`_apply_chain(service_id, undo)` rolls the write back when the transport refuses the
|
|
resulting chain. Without it the next start fails for the reason the operator thought
|
|
they had avoided.
|
|
"""
|
|
from integration.conftest import Layer
|
|
|
|
service_id, _, _ = protected(Layer("nfqueue"), name="undo")
|
|
_fill_chain(api, service_id, MAX_CHAIN_POSITIONS)
|
|
start_and_settle(api, service_id)
|
|
before = api.services_filters(service_id)
|
|
assert len(before) == MAX_CHAIN_POSITIONS
|
|
|
|
why = api.services_add_filter_error(service_id, "regex", "one-too-many")
|
|
assert why is not None, "a filter past the ceiling was attached to a running service"
|
|
after = api.services_filters(service_id)
|
|
assert len(after) == len(before), \
|
|
f"a refused filter was stored anyway: {len(before)} -> {len(after)}"
|
|
assert [f["name"] for f in after] == [f["name"] for f in before], str(after)
|
|
|
|
|
|
def test_the_chain_can_be_reordered_and_the_order_is_what_runs(api, protected,
|
|
inspecting_layer):
|
|
"""`parse_ruleset` groups patterns by their filter id and keeps the positions the
|
|
backend sent. An earlier version grouped every block and then the Python, which turned
|
|
the chain into a set — a filter placed after another could run before it, and nothing
|
|
said so."""
|
|
service_id, _, _ = protected(inspecting_layer, name="order")
|
|
add_regex_filter(api, service_id, "FIRST", name="alpha")
|
|
add_regex_filter(api, service_id, "SECOND", name="beta")
|
|
chain = api.services_filters(service_id)
|
|
assert [link["name"] for link in chain] == ["alpha", "beta"], str(chain)
|
|
|
|
reversed_ids = [link["filter_id"] for link in reversed(chain)]
|
|
assert api.services_reorder_filters(service_id, reversed_ids)
|
|
assert [link["name"] for link in api.services_filters(service_id)] == ["beta", "alpha"]
|
|
|
|
|
|
def test_editing_a_pattern_inside_a_running_chain_costs_nobody_their_connection(
|
|
api, protected, inspecting_layer):
|
|
"""Changing the *shape* of a queued chain cannot be pushed — the shape is the
|
|
arrangement of processes and rules, so `NfqueueTransport.reload` raises
|
|
`ChainShapeChanged` and the service is rebuilt, saying so. Editing a pattern inside an
|
|
existing filter is a different thing entirely."""
|
|
import base64
|
|
import time
|
|
|
|
from helpers.traffic import Channel
|
|
|
|
service_id, server, port = protected(inspecting_layer, name="live")
|
|
filter_id, pattern_id = add_regex_filter(api, service_id, "BLOCKME")
|
|
start_and_settle(api, service_id)
|
|
channel = Channel(server, port, inspecting_layer.ipv6)
|
|
|
|
server.connect_client()
|
|
try:
|
|
server.send_packet(b"hello")
|
|
assert server.recv_packet() == b"hello"
|
|
assert api.services_edit_regex(service_id, filter_id, pattern_id,
|
|
regex=base64.b64encode(b"STOPME").decode())
|
|
time.sleep(1.0)
|
|
server.send_packet(b"still here")
|
|
assert server.recv_packet() == b"still here", \
|
|
"a pattern edit dropped a connection the filter was holding"
|
|
finally:
|
|
server.close_client()
|
|
assert channel.is_blocked(b"carrying STOPME")
|