Files
citrixpot/test_network_capture.py

101 lines
4.8 KiB
Python

"""Offline tests: fixture sockets only; no external connections."""
import json
import os
import pathlib
import socket
import struct
import tempfile
import threading
from unittest.mock import patch
from types import SimpleNamespace
import network_capture as net
from evidence import Evidence
def run():
for address in ['127.0.0.1', '10.0.0.1', '169.254.169.254', '192.168.1.1', '224.0.0.1', '0.0.0.0', '100.64.0.1', '192.0.2.1', '::1']:
try:
net.public_ip(address)
raise AssertionError(address)
except ValueError:
pass
assert net.public_ip('8.8.8.8') == '8.8.8.8'
with tempfile.TemporaryDirectory() as tmp:
net.STOP = pathlib.Path(tmp) / 'STOP'
peers = pathlib.Path(tmp) / 'peers.json'
peers.write_text(json.dumps(['10.0.0.1', '8.8.8.8', 'bad', '127.0.0.1', 134744072]))
peers.chmod(0o600)
actual_stat = os.stat(peers)
# Root ownership is mocked only for this unit check on unprivileged Macs.
with patch.object(net.os, 'fstat', return_value=SimpleNamespace(
st_mode=actual_stat.st_mode, st_uid=0, st_mtime=actual_stat.st_mtime)):
assert net.allowed_peers(peers) == {'8.8.8.8'}
evidence = Evidence(pathlib.Path(tmp) / 'evidence')
server = net.Server(('127.0.0.1', 0), net.Broker, evidence=evidence, peers=str(pathlib.Path(tmp) / 'absent'))
worker = threading.Thread(target=server.serve_forever, daemon=True)
worker.start()
original_connect = socket.create_connection
outgoing = []
def fixture_connect(destination, timeout=None, **kwargs):
assert destination == ('8.8.8.8', 80), destination
outgoing.append(destination)
broker_end, fixture_end = socket.socketpair()
def echo():
with fixture_end:
while True:
data = fixture_end.recv(16384)
if not data:
break
fixture_end.sendall(data)
threading.Thread(target=echo, daemon=True).start()
return broker_end
with patch.object(net.socket, 'create_connection', fixture_connect):
with original_connect(server.server_address, timeout=3) as client:
net.send_message(client, {'op': 'connect', 'ip': '127.0.0.1', 'port': 80})
assert not net.read_message(client)['ok']
with original_connect(server.server_address, timeout=3) as client:
net.send_message(client, {'op': 'connect', 'ip': '8.8.8.8', 'port': 4444})
assert not net.read_message(client)['ok']
with original_connect(server.server_address, timeout=3) as client:
net.send_message(client, {'op': 'connect', 'ip': '8.8.8.8', 'port': 80})
assert net.read_message(client)['ok']
payload = b'\x00malware-fixture\xff\r\n'
client.sendall(payload)
data = b''
while len(data) < len(payload):
data += client.recv(1024)
assert data == payload
client.shutdown(socket.SHUT_WR)
assert client.recv(1) == b''
assert len(outgoing) == 1
server.shutdown()
server.server_close()
rows = [json.loads(line) for line in (evidence.directory / 'events.jsonl').read_text().splitlines()]
chunks = [r for r in rows if r['event'] == 'network_chunk']
assert {r['direction'] for r in chunks} == {'guest_to_remote', 'remote_to_guest'}
for row in chunks:
assert (evidence.directory / row['artifact']).read_bytes() == payload
assert row['time_ns'] and row['connection_id']
# DNS parser rejects malformed/compressed questions; AAAA returns no answer.
assert net.dns_reply(b'bad', ('unused', 0)) is None
question = struct.pack('!6H', 7, 0x100, 1, 0, 0, 0) + b'\x07example\x03com\0' + struct.pack('!HH', 28, 1)
reply = net.dns_reply(question, ('unused', 0))
assert struct.unpack('!6H', reply[:12])[3] == 0
# Capturing failure closes streams before further forward and persists STOP.
evidence.quota = evidence.used
failing = net.Server(('127.0.0.1', 0), net.Broker, evidence=evidence, peers='absent')
threading.Thread(target=failing.serve_forever, daemon=True).start()
with original_connect(failing.server_address, timeout=3) as client:
net.send_message(client, {'op': 'connect', 'ip': '8.8.8.8', 'port': 80})
assert client.recv(1) == b''
assert net.STOP.exists() and failing.failed.is_set()
failing.shutdown()
failing.server_close()
evidence.close()
print('network capture offline checks passed')
if __name__ == '__main__':
run()