diff options
| -rw-r--r-- | DEDICATION | 2 | ||||
| -rw-r--r-- | Mk/release.mk | 2 | ||||
| -rw-r--r-- | man5/opentracker.conf.5 | 4 | ||||
| -rw-r--r-- | src/ot_config.c | 6 | ||||
| -rw-r--r-- | src/ot_socket.c | 8 | ||||
| -rw-r--r-- | tests/integration/test_config.py | 26 | ||||
| -rw-r--r-- | tests/integration/test_livesync.py | 53 |
7 files changed, 70 insertions, 31 deletions
diff --git a/DEDICATION b/DEDICATION new file mode 100644 index 0000000..eca7d99 --- /dev/null +++ b/DEDICATION | |||
| @@ -0,0 +1,2 @@ | |||
| 1 | This release is dedicated to Felix von Leitner, whose libowfat carried | ||
| 2 | opentracker for its first eighteen years. | ||
diff --git a/Mk/release.mk b/Mk/release.mk index 5c4afe9..e5336e0 100644 --- a/Mk/release.mk +++ b/Mk/release.mk | |||
| @@ -2,7 +2,7 @@ | |||
| 2 | # In a checkout it is absent and git answers instead. A release is always | 2 | # In a checkout it is absent and git answers instead. A release is always |
| 3 | # tagged, so in a tarball both of these are that tag, and the gitweb link | 3 | # tagged, so in a tarball both of these are that tag, and the gitweb link |
| 4 | # is to it. Between releases there is no tag and the version is the commit. | 4 | # is to it. Between releases there is no tag and the version is the commit. |
| 5 | VERSION=1.0.0 | 5 | VERSION=2.0.0 |
| 6 | 6 | ||
| 7 | TARBALL_VERSION=test -r .tarball-version && read v < .tarball-version && echo "$$v" | 7 | TARBALL_VERSION=test -r .tarball-version && read v < .tarball-version && echo "$$v" |
| 8 | HAVE_GIT=command -v git >/dev/null && test -d .git | 8 | HAVE_GIT=command -v git >/dev/null && test -d .git |
diff --git a/man5/opentracker.conf.5 b/man5/opentracker.conf.5 index 9a836ae..2250fe5 100644 --- a/man5/opentracker.conf.5 +++ b/man5/opentracker.conf.5 | |||
| @@ -90,7 +90,9 @@ technically sits in a range IANA assigns on request. | |||
| 90 | 90 | ||
| 91 | A cluster of its own is better served by an address out of 239.192.0.0/14, | 91 | A cluster of its own is better served by an address out of 239.192.0.0/14, |
| 92 | which RFC 2365 sets aside for private use. Packets carry a ttl of 1 either | 92 | which RFC 2365 sets aside for private use. Packets carry a ttl of 1 either |
| 93 | way and never leave the local link. | 93 | way and never leave the local link. Must be a valid mulitcast address from |
| 94 | .Pa 224.0.0.0/4 | ||
| 95 | . | ||
| 94 | .Pp | 96 | .Pp |
| 95 | .It livesync.cluster.node_ip Ar ip_address | 97 | .It livesync.cluster.node_ip Ar ip_address |
| 96 | Specifies one trusted IP address for sync between trackers running in a cluster. Can be added more than once. | 98 | Specifies one trusted IP address for sync between trackers running in a cluster. Can be added more than once. |
diff --git a/src/ot_config.c b/src/ot_config.c index 4064d1b..6b51c61 100644 --- a/src/ot_config.c +++ b/src/ot_config.c | |||
| @@ -320,7 +320,7 @@ static int parse_configfile(char *config_filename) { | |||
| 320 | int off = scan_ip6_port(p, tmpip, &tmpport); | 320 | int off = scan_ip6_port(p, tmpip, &tmpport); |
| 321 | 321 | ||
| 322 | if (!off || !listen_scan_options(p + off, &shared, &workers)) | 322 | if (!off || !listen_scan_options(p + off, &shared, &workers)) |
| 323 | goto parse_error; | 323 | exerr("listen.tcp, listen.udp and listen.tcp_udp want an address, optionally a port and options."); |
| 324 | if (kind != CONFIG_LISTEN_UDP) | 324 | if (kind != CONFIG_LISTEN_UDP) |
| 325 | listen_append(tmpip, tmpport, FLAG_TCP, shared, workers), ++bound; | 325 | listen_append(tmpip, tmpport, FLAG_TCP, shared, workers), ++bound; |
| 326 | if (kind != CONFIG_LISTEN_TCP) | 326 | if (kind != CONFIG_LISTEN_TCP) |
| @@ -382,7 +382,7 @@ static int parse_configfile(char *config_filename) { | |||
| 382 | #ifdef WANT_SYNC_LIVE | 382 | #ifdef WANT_SYNC_LIVE |
| 383 | case CONFIG_LIVESYNC_CLUSTER_GROUP: | 383 | case CONFIG_LIVESYNC_CLUSTER_GROUP: |
| 384 | if (!ip_parse(p, tmpip) || livesync_set_group(tmpip) == -1) | 384 | if (!ip_parse(p, tmpip) || livesync_set_group(tmpip) == -1) |
| 385 | goto parse_error; | 385 | exerr("livesync.cluster.group wants a multicast address, out of 224.0.0.0/4."); |
| 386 | break; | 386 | break; |
| 387 | case CONFIG_LIVESYNC_CLUSTER_NODE_IP: | 387 | case CONFIG_LIVESYNC_CLUSTER_NODE_IP: |
| 388 | if (!scan_ip6_net(p, &tmpnet)) | 388 | if (!scan_ip6_net(p, &tmpnet)) |
| @@ -446,7 +446,7 @@ static void help(char *name) { | |||
| 446 | fprintf(stderr, "\nExample: ./opentracker -i 127.0.0.1 -p 6969 -P 6969 -f ./opentracker.conf -i 10.1.1.23 -p 2710 -p 80\n"); | 446 | fprintf(stderr, "\nExample: ./opentracker -i 127.0.0.1 -p 6969 -P 6969 -f ./opentracker.conf -i 10.1.1.23 -p 2710 -p 80\n"); |
| 447 | fprintf(stderr, " Here -i 127.0.0.1 selects the ip address for the next -p 6969 and -P 6969.\n"); | 447 | fprintf(stderr, " Here -i 127.0.0.1 selects the ip address for the next -p 6969 and -P 6969.\n"); |
| 448 | fprintf(stderr, " If no port is bound from config file or command line, the last address given\n"); | 448 | fprintf(stderr, " If no port is bound from config file or command line, the last address given\n"); |
| 449 | fprintf(stderr, " (or ::1 if none is set) will be used on port 6969.\n"); | 449 | fprintf(stderr, " is used on port 6969, and every interface if none was given.\n"); |
| 450 | } | 450 | } |
| 451 | #undef HELPLINE | 451 | #undef HELPLINE |
| 452 | 452 | ||
diff --git a/src/ot_socket.c b/src/ot_socket.c index 79bb01a..b6a1803 100644 --- a/src/ot_socket.c +++ b/src/ot_socket.c | |||
| @@ -263,15 +263,9 @@ static int socket_v4_bind(const ot_ip6 ip, uint16_t port) { | |||
| 263 | 263 | ||
| 264 | int socket_mcast_join(const ot_ip6 group, const ot_ip6 interface, uint16_t port) { | 264 | int socket_mcast_join(const ot_ip6 group, const ot_ip6 interface, uint16_t port) { |
| 265 | struct ip_mreq mreq; | 265 | struct ip_mreq mreq; |
| 266 | ot_ip6 any; | ||
| 267 | int s; | 266 | int s; |
| 268 | 267 | ||
| 269 | /* The group is what is listened to, so the socket takes the wildcard and | 268 | if ((s = socket_v4_bind(group, port)) == -1) |
| 270 | the join names the interface to receive it on */ | ||
| 271 | memcpy(any, ip_v4mapped_prefix, sizeof(ip_v4mapped_prefix)); | ||
| 272 | memset(any + sizeof(ip_v4mapped_prefix), 0, 4); | ||
| 273 | |||
| 274 | if ((s = socket_v4_bind(any, port)) == -1) | ||
| 275 | return -1; | 269 | return -1; |
| 276 | 270 | ||
| 277 | memset(&mreq, 0, sizeof(mreq)); | 271 | memset(&mreq, 0, sizeof(mreq)); |
diff --git a/tests/integration/test_config.py b/tests/integration/test_config.py index 1a84fa7..2f6bdc4 100644 --- a/tests/integration/test_config.py +++ b/tests/integration/test_config.py | |||
| @@ -1,7 +1,9 @@ | |||
| 1 | """Settings the tracker has to accept, and still work with. | 1 | """Settings the tracker has to accept, and still work with. |
| 2 | 2 | ||
| 3 | An unrecognised key makes opentracker refuse to start, so a test that it | 3 | An unrecognised key only warns, so coming up proves nothing on its own and a |
| 4 | comes up at all is a test that the key was understood. | 4 | test that cares has to look at what the tracker said. A key that is |
| 5 | recognised but cannot be parsed is another matter, and for the listen | ||
| 6 | statements it stops the tracker. | ||
| 5 | """ | 7 | """ |
| 6 | 8 | ||
| 7 | import unittest | 9 | import unittest |
| @@ -14,6 +16,7 @@ class TcpWorkers(TrackerTestCase): | |||
| 14 | 16 | ||
| 15 | def test_the_tracker_serves_with_tcp_workers_asked_for(self): | 17 | def test_the_tracker_serves_with_tcp_workers_asked_for(self): |
| 16 | self.assertEqual(self.tracker.announce(self.hash_for("tcpworkers"))[b"complete"], 1) | 18 | self.assertEqual(self.tracker.announce(self.hash_for("tcpworkers"))[b"complete"], 1) |
| 19 | self.assertNotIn("Unhandled line", self.tracker.output()) | ||
| 17 | 20 | ||
| 18 | 21 | ||
| 19 | class WorkersOnAListenStatement(unittest.TestCase): | 22 | class WorkersOnAListenStatement(unittest.TestCase): |
| @@ -27,3 +30,22 @@ class WorkersOnAListenStatement(unittest.TestCase): | |||
| 27 | self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints)) | 30 | self.assertEqual(complaints, [], "the tracker reported:\n" + "\n".join(complaints)) |
| 28 | finally: | 31 | finally: |
| 29 | tracker._cleanup() | 32 | tracker._cleanup() |
| 33 | |||
| 34 | |||
| 35 | class AListenLineThatDoesNotParse(unittest.TestCase): | ||
| 36 | """Warning and carrying on left a tracker that had been told where to | ||
| 37 | listen coming up on its default instead, which is every interface.""" | ||
| 38 | |||
| 39 | def refuses(self, line): | ||
| 40 | with self.assertRaises(AssertionError) as caught: | ||
| 41 | Tracker(line) | ||
| 42 | return str(caught.exception) | ||
| 43 | |||
| 44 | def test_an_address_that_is_not_one_stops_the_tracker(self): | ||
| 45 | self.assertIn("want an address", self.refuses("listen.tcp 10.0.0.300:7999\n")) | ||
| 46 | |||
| 47 | def test_so_does_a_port_out_of_range(self): | ||
| 48 | self.assertIn("want an address", self.refuses("listen.udp 127.0.0.1:99999\n")) | ||
| 49 | |||
| 50 | def test_so_does_an_option_it_does_not_know(self): | ||
| 51 | self.assertIn("want an address", self.refuses("listen.tcp 127.0.0.1:7999 wokers=2\n")) | ||
diff --git a/tests/integration/test_livesync.py b/tests/integration/test_livesync.py index 52e871f..f4cb2db 100644 --- a/tests/integration/test_livesync.py +++ b/tests/integration/test_livesync.py | |||
| @@ -10,7 +10,7 @@ import struct | |||
| 10 | import subprocess | 10 | import subprocess |
| 11 | import unittest | 11 | import unittest |
| 12 | 12 | ||
| 13 | from harness import TRACKER, TrackerTestCase | 13 | from harness import TRACKER, Tracker, TrackerTestCase |
| 14 | 14 | ||
| 15 | # enum { OT_SYNC_PEER4, OT_SYNC_PEER6 } in src/ot_livesync.c | 15 | # enum { OT_SYNC_PEER4, OT_SYNC_PEER6 } in src/ot_livesync.c |
| 16 | SYNC_PEER4 = 0 | 16 | SYNC_PEER4 = 0 |
| @@ -168,6 +168,13 @@ class LiveSyncUnconfigured(TrackerTestCase): | |||
| 168 | 168 | ||
| 169 | # RFC 2365 sets 239.192.0.0/14 aside for exactly this kind of use | 169 | # RFC 2365 sets 239.192.0.0/14 aside for exactly this kind of use |
| 170 | OWN_GROUP = "239.192.0.7" | 170 | OWN_GROUP = "239.192.0.7" |
| 171 | # A group nothing in this suite ever joins. Deliberately not the default one: | ||
| 172 | # other trackers here do join that, on ports the harness picks at random, and | ||
| 173 | # a socket bound to the wildcard address shares a port's multicast traffic | ||
| 174 | # with anything else on the host that has joined the group it is addressed | ||
| 175 | # to. Two clusters sharing a host and a port is pathological in the field but | ||
| 176 | # routine in a test suite. | ||
| 177 | UNJOINED_GROUP = "239.192.0.99" | ||
| 171 | 178 | ||
| 172 | 179 | ||
| 173 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") | 180 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") |
| @@ -182,27 +189,39 @@ class LiveSyncGroup(TrackerTestCase): | |||
| 182 | group=OWN_GROUP) | 189 | group=OWN_GROUP) |
| 183 | self.assertTrue(self.tracker.wait_until(lambda: self.tracker.torrent_count() == 1)) | 190 | self.assertTrue(self.tracker.wait_until(lambda: self.tracker.torrent_count() == 1)) |
| 184 | 191 | ||
| 185 | def test_the_default_group_is_left_alone(self): | 192 | def test_another_group_is_left_alone(self): |
| 186 | """A cluster that picked its own group should not hear the one every | 193 | """A cluster that picked its own group hears nothing from the others.""" |
| 187 | other opentracker is on.""" | ||
| 188 | info_hash = self.hash_for("not-our-group") | 194 | info_hash = self.hash_for("not-our-group") |
| 189 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.3.2", 6881, FLAG_SEEDING))])) | 195 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.3.2", 6881, FLAG_SEEDING))]), |
| 196 | group=UNJOINED_GROUP) | ||
| 190 | self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != 0, timeout=1)) | 197 | self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != 0, timeout=1)) |
| 191 | 198 | ||
| 192 | 199 | ||
| 193 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") | 200 | @unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE") |
| 194 | class LiveSyncGroupRefused(TrackerTestCase): | 201 | class LiveSyncGroupRefused(unittest.TestCase): |
| 195 | """A unicast address is not a group. The line is refused like any other | 202 | """A cluster that asked for a group of its own must not quietly end up on |
| 196 | bad config line, which leaves the default standing.""" | 203 | the one every other opentracker is on, so a line that does not name a |
| 204 | group stops the tracker rather than falling back.""" | ||
| 197 | 205 | ||
| 198 | config = ("livesync.cluster.group 10.0.0.1\n" | 206 | def start_with(self, group): |
| 199 | "livesync.cluster.listen 127.0.0.1:{livesync_port}\n" | 207 | return Tracker("livesync.cluster.group %s\n" |
| 200 | "livesync.cluster.node_ip 127.0.0.1\n") | 208 | "livesync.cluster.listen 127.0.0.1:{livesync_port}\n" |
| 209 | "livesync.cluster.node_ip 127.0.0.1\n" % group) | ||
| 201 | 210 | ||
| 202 | def test_the_line_is_refused(self): | 211 | def test_a_unicast_address_stops_the_tracker(self): |
| 203 | self.assertIn("Parse error", self.tracker.output()) | 212 | with self.assertRaises(AssertionError) as caught: |
| 213 | self.start_with("10.0.0.1") | ||
| 214 | self.assertIn("multicast address", str(caught.exception)) | ||
| 204 | 215 | ||
| 205 | def test_and_the_default_group_still_works(self): | 216 | def test_so_does_something_that_is_not_an_address(self): |
| 206 | info_hash = self.hash_for("fallback-group") | 217 | with self.assertRaises(AssertionError) as caught: |
| 207 | self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.3.3", 6881, FLAG_SEEDING))])) | 218 | self.start_with("not-an-address") |
| 208 | self.assertTrue(self.tracker.wait_until(lambda: self.tracker.torrent_count() == 1)) | 219 | self.assertIn("multicast address", str(caught.exception)) |
| 220 | |||
| 221 | def test_a_group_at_either_end_of_the_range_is_taken(self): | ||
| 222 | for group in ("224.0.0.1", "239.255.255.255"): | ||
| 223 | tracker = self.start_with(group) | ||
| 224 | try: | ||
| 225 | self.assertEqual(tracker.torrent_count(), 0) | ||
| 226 | finally: | ||
| 227 | tracker.stop() | ||
