summaryrefslogtreecommitdiff
path: root/tests/integration/test_livesync.py
blob: 8dd754b51c0a4ec0928acc09a3f177c8c0570caa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
"""Live sync, driven over the multicast group a cluster would use.

Live sync sits behind WANT_SYNC_LIVE, so these only run against a binary
built with it: make check-livesync. Against any other build they skip, which
is what the feature list in -v is for.
"""

import socket
import struct
import subprocess
import unittest

from harness import TRACKER, TrackerTestCase

# enum { OT_SYNC_PEER4, OT_SYNC_PEER6 } in src/ot_livesync.c
SYNC_PEER4 = 0
SYNC_PEER6 = 1

# src/trackerlogic.h
FLAG_SEEDING = 0x80
FLAG_COMPLETED = 0x40
FLAG_STOPPED = 0x20


def has_livesync():
    done = subprocess.run([TRACKER, "-v"], capture_output=True, timeout=30)
    return b"WANT_SYNC_LIVE" in done.stderr


def peer4(address, port, flags):
    """Four bytes of address, two of port, one of flags, one the tracker
    overwrites with its own idea of the time."""
    return socket.inet_aton(address) + struct.pack("!H", port) + bytes([flags, 0])


def peer6(address, port, flags):
    return socket.inet_pton(socket.AF_INET6, address) + struct.pack("!H", port) + bytes([flags, 0])


def packet(kind, entries, tracker_id=0xDEADBEEF):
    body = b"".join(info_hash + peer for info_hash, peer in entries)
    return struct.pack("!I", tracker_id) + struct.pack("!I", kind) + body


@unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE")
class LiveSyncIn(TrackerTestCase):
    config = "livesync.cluster.listen 127.0.0.1:{livesync_port}\nlivesync.cluster.node_ip 127.0.0.1\n"

    def counts(self, info_hash):
        """Seeders and leechers a scrape reports for one torrent."""
        files = self.tracker.scrape(info_hash)[b"files"]
        if info_hash not in files:
            return None
        entry = files[info_hash]
        return entry[b"complete"], entry[b"incomplete"]

    def test_a_synced_seeder_arrives(self):
        info_hash = self.hash_for("sync-seed")
        self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.3", 6881, FLAG_SEEDING))]))
        self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)),
                        "scrape says %r" % (self.counts(info_hash),))

    def test_a_synced_leecher_arrives(self):
        info_hash = self.hash_for("sync-leech")
        self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.4", 6881, 0))]))
        self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (0, 1)),
                        "scrape says %r" % (self.counts(info_hash),))

    def test_a_v6_peer_arrives(self):
        info_hash = self.hash_for("sync-v6")
        self.tracker.livesync_send(packet(SYNC_PEER6, [(info_hash, peer6("2001:db8::1", 6881, FLAG_SEEDING))]))
        self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)),
                        "scrape says %r" % (self.counts(info_hash),))

    def test_one_packet_carries_several_torrents(self):
        hashes = [self.hash_for("sync-many-%d" % i) for i in range(4)]
        self.tracker.livesync_send(packet(SYNC_PEER4, [(h, peer4("10.1.2.%d" % (5 + i), 6881, FLAG_SEEDING))
                                                       for i, h in enumerate(hashes)]))
        self.assertTrue(self.tracker.wait_until(lambda: all(self.counts(h) == (1, 0) for h in hashes)))

    def test_a_stopped_peer_is_taken_away_again(self):
        info_hash = self.hash_for("sync-stop")
        peer = peer4("10.1.2.9", 6881, FLAG_SEEDING)
        self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer)]))
        self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) == (1, 0)))

        self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.9", 6881, FLAG_STOPPED))]))
        self.assertTrue(self.tracker.wait_until(lambda: self.counts(info_hash) in (None, (0, 0))),
                        "scrape still says %r" % (self.counts(info_hash),))

    def test_a_truncated_entry_is_dropped(self):
        """The loop takes whole hash and peer pairs and leaves a short tail
        where it is, so the complete entry before it still lands."""
        good, short = self.hash_for("sync-good"), self.hash_for("sync-short")
        payload = packet(SYNC_PEER4, [(good, peer4("10.1.2.10", 6881, FLAG_SEEDING))]) + short + b"\x0a\x01"
        self.tracker.livesync_send(payload)
        self.assertTrue(self.tracker.wait_until(lambda: self.counts(good) == (1, 0)))
        self.assertIsNone(self.counts(short))

    def test_an_unknown_packet_type_is_ignored(self):
        info_hash = self.hash_for("sync-unknown")
        self.tracker.livesync_send(packet(99, [(info_hash, peer4("10.1.2.11", 6881, FLAG_SEEDING))]))
        self.assertFalse(self.tracker.wait_until(lambda: self.counts(info_hash) is not None, timeout=1))

    def test_a_packet_with_nothing_but_a_header_is_ignored(self):
        before = self.tracker.torrent_count()
        self.tracker.livesync_send(struct.pack("!I", 0xDEADBEEF) + struct.pack("!I", SYNC_PEER4))
        self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != before, timeout=1))


@unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE")
class LiveSyncAccess(TrackerTestCase):
    """Nothing blesses 127.0.0.1 here, so the tracker should take nothing
    from it. Source address checking is all live sync has."""

    config = "livesync.cluster.listen 127.0.0.1:{livesync_port}\nlivesync.cluster.node_ip 10.0.0.1\n"

    def test_an_unblessed_sender_is_ignored(self):
        info_hash = self.hash_for("sync-unblessed")
        self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.2.12", 6881, FLAG_SEEDING))]))
        self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != 0, timeout=1))