firegex/tests/integration/test_addresses.py
2026-09-13 15:54:56 +02:00

144 lines
6.5 KiB
Python

"""Where a service is reachable, and changing that while it runs.
A service is one thing to protect and one way of intercepting it; *where* it answers is a
list. One daemon routinely answers on more than one address — a v4 and a v6 one, a public
and an internal one — and all of them deserve the same chain. Two services with
hand-copied chains is how one of them silently stops being protected.
"""
import time
import pytest
from integration.conftest import RELOAD, add_regex_filter, start_and_settle
from helpers.net import free_port
from helpers.traffic import Channel
pytestmark = pytest.mark.instance
@pytest.fixture
def running(api, protected, filtering_layer):
service_id, server, port = protected(filtering_layer, name="addr")
add_regex_filter(api, service_id, "BLOCKME")
start_and_settle(api, service_id)
channel = Channel(server, port, filtering_layer.ipv6, filtering_layer.tls)
return service_id, server, port, channel
def test_a_service_reports_where_it_is_reachable(api, running, filtering_layer):
service_id, _, port, _ = running
addresses = api.services_addresses(service_id)
assert len(addresses) == 1 and addresses[0]["port"] == port, str(addresses)
def test_the_only_address_cannot_be_removed_and_the_refusal_says_why(api, running):
"""A service with nowhere to answer is not a service, it is a row."""
service_id, _, _, _ = running
only = api.services_addresses(service_id)[0]["address_id"]
why = api.services_delete_address_error(service_id, only)
assert why is not None, "the last address was removed"
assert "only address" in why, why
def test_adding_an_address_to_a_running_service_drops_nothing(
api, running, stand_in, certificate, filtering_layer):
"""Not a restart.
The datapath is already up and already enforcing the chain, so this installs the
rules that point one more address at it. The connections on the other addresses are
untouched.
"""
service_id, first, first_port, first_channel = running
cert = certificate(filtering_layer.ip) if filtering_layer.tls else None
second = stand_in(filtering_layer.ipv6, tls=cert)
why = api.services_add_address_error(service_id, filtering_layer.ip, second.port)
assert why is None, why
assert len(api.services_addresses(service_id)) == 2
time.sleep(1.0)
second_channel = Channel(second, second.port, filtering_layer.ipv6, filtering_layer.tls)
assert second_channel.gets_through(b"harmless traffic"), \
"the new address is not carrying traffic"
assert second_channel.is_blocked(b"carrying BLOCKME"), \
"the same chain is not filtering the new address"
assert first_channel.gets_through(b"harmless traffic"), \
"the original address stopped working"
def test_the_log_says_an_address_was_added_without_dropping_anything(
api, running, stand_in, certificate, filtering_layer):
service_id, _, _, _ = running
cert = certificate(filtering_layer.ip) if filtering_layer.tls else None
second = stand_in(filtering_layer.ipv6, tls=cert)
assert api.services_add_address_error(service_id, filtering_layer.ip, second.port) is None
time.sleep(RELOAD)
entries = api.services_logs(service_id)
assert any("also protecting" in entry["text"] for entry in entries), str(entries[-4:])
def test_a_removed_address_stops_being_filtered_and_the_rest_keeps_going(
api, running, stand_in, certificate, filtering_layer):
"""`address_removed` drops the address from the service's own list rather than
re-reading the table, because the caller takes the rules back *before* deleting the
row — re-reading would put it straight back, and the next restart would re-protect an
address that no longer exists."""
service_id, _, _, first_channel = running
cert = certificate(filtering_layer.ip) if filtering_layer.tls else None
second = stand_in(filtering_layer.ipv6, tls=cert)
assert api.services_add_address_error(service_id, filtering_layer.ip, second.port) is None
time.sleep(1.0)
gone = [a for a in api.services_addresses(service_id)
if a["port"] == second.port][0]["address_id"]
assert api.services_delete_address(service_id, gone)
time.sleep(1.0)
second_channel = Channel(second, second.port, filtering_layer.ipv6, filtering_layer.tls)
assert second_channel.gets_through(b"carrying BLOCKME"), \
"the removed address is still being filtered"
assert first_channel.is_blocked(b"carrying BLOCKME"), \
"removing one address stopped the others being protected"
def test_an_address_can_be_a_network_interface_name(api, running):
"""An interface instead of a fixed IP or CIDR.
Matched with `meta iifname` inbound and `meta oifname` outbound, so the traffic is
intercepted on the interface without firegex needing to know an address for it.
"""
service_id, _, _, _ = running
port = free_port()
why = api.services_add_address_error(service_id, "lo", port)
assert why is None, why
listed = [a for a in api.services_addresses(service_id) if a["ip_int"] == "lo"]
assert len(listed) == 1, str(api.services_addresses(service_id))
assert api.services_delete_address(service_id, listed[0]["address_id"])
def test_an_interface_is_refused_on_the_hand_off_layer(api, protected, external_layer):
"""The return rule rewrites the source address, which needs a concrete IP.
Refused when the address is added rather than accepted and broken at start time.
"""
service_id, _, _ = protected(external_layer, name="iface")
why = api.services_add_address_error(service_id, "lo", free_port(),
proxy_ip=external_layer.ip,
proxy_port=free_port())
assert why is not None, "an interface was accepted on the hand-off layer"
assert "interface" in why.lower(), why
def test_two_services_cannot_claim_the_same_address_and_protocol(
api, protected, service, inspecting_layer):
"""`(ip, port, proto)` is the uniqueness key, which is why the protocol is
denormalised onto each address: a TCP service and a UDP one may share an address,
exactly as the kernel allows, and two TCP ones may not."""
_, _, port = protected(inspecting_layer, name="claim")
clash = api.services_add_error(
name=f"clash-{port}", transport=inspecting_layer.transport, proto="tcp",
addresses=[{"ip_int": inspecting_layer.ip, "port": port}],
)
assert clash is not None, "two services claimed the same address and protocol"