mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 21:42:05 +00:00
144 lines
6.5 KiB
Python
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"
|