summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--man5/opentracker.conf.515
-rw-r--r--opentracker.conf.sample22
-rw-r--r--src/ot_config.c10
-rw-r--r--src/ot_livesync.c18
-rw-r--r--src/ot_livesync.h4
-rw-r--r--tests/integration/harness.py4
-rw-r--r--tests/integration/test_config.py26
-rw-r--r--tests/integration/test_livesync.py63
8 files changed, 143 insertions, 19 deletions
diff --git a/man5/opentracker.conf.5 b/man5/opentracker.conf.5
index 67e624f..2250fe5 100644
--- a/man5/opentracker.conf.5
+++ b/man5/opentracker.conf.5
@@ -79,7 +79,20 @@ Specifies the path to the stats location. You can configure opentracker to appea
79Specifies the IP address or network of the reverse proxies. Opentracker will take the X-Forwarded-For address instead of the source IP address. Can be added more than once. 79Specifies the IP address or network of the reverse proxies. Opentracker will take the X-Forwarded-For address instead of the source IP address. Can be added more than once.
80.Pp 80.Pp
81.It livesync.cluster.listen Ar ip_address:port 81.It livesync.cluster.listen Ar ip_address:port
82Specifies the IP address and port opentracker will listen on for incoming live sync packets to keep a cluster of opentrackers synchronized. 82Specifies the IP address and port opentracker will listen on for incoming live
83sync packets to keep a cluster of opentrackers synchronized. Every member of a
84cluster has to agree on the port, which is also the one packets are sent to.
85.Pp
86.It livesync.cluster.group Ar ip_address
87Specifies the multicast group a cluster talks in. For reaosns of backwards
88comaptibility with legacy trackers, it defaults to 224.0.23.5, which,
89technically sits in a range IANA assigns on request.
90
91A cluster of its own is better served by an address out of 239.192.0.0/14,
92which RFC 2365 sets aside for private use. Packets carry a ttl of 1 either
93way and never leave the local link. Must be a valid mulitcast address from
94.Pa 224.0.0.0/4
95.
83.Pp 96.Pp
84.It livesync.cluster.node_ip Ar ip_address 97.It livesync.cluster.node_ip Ar ip_address
85Specifies one trusted IP address for sync between trackers running in a cluster. Can be added more than once. 98Specifies one trusted IP address for sync between trackers running in a cluster. Can be added more than once.
diff --git a/opentracker.conf.sample b/opentracker.conf.sample
index 3cd30dc..7b35369 100644
--- a/opentracker.conf.sample
+++ b/opentracker.conf.sample
@@ -138,14 +138,22 @@
138# livesync.cluster.listen 192.168.0.1:9696 138# livesync.cluster.listen 192.168.0.1:9696
139# 139#
140# Note that two udp sockets will be opened. One on ip address 0.0.0.0 140# Note that two udp sockets will be opened. One on ip address 0.0.0.0
141# port 9696, that will join the multicast group 224.0.23.5 for incoming 141# port 9696, that will join the multicast group for incoming udp packets
142# udp packets and one on ip address 192.168.0.1 port 9696 for outgoing 142# and one on ip address 192.168.0.1 port 9696 for outgoing ones. Every
143# udp packets. The group is the same for every opentracker, so a cluster 143# member of a cluster has to agree on the port as well as the group.
144# is kept to itself by the node_ip list below and by the ttl of 1 that 144#
145# keeps packets on the local link. 145# The group a cluster talks in.
146#
147# livesync.cluster.group 239.192.23.5
148#
149# For reaosns of backwards comaptibility with legacy trackers, it
150# defaults to 224.0.23.5, which, technically sits in a range IANA
151# assigns on request.
152#
153# A cluster of your own is better served by an address out of
154# 239.192.0.0/14, which RFC 2365 sets aside for private use. Packets
155# carry a ttl of 1 either way, so they never leave the local link.
146# 156#
147# As of now one and only one ip address must be given, if opentracker
148# was built with the WANT_SYNC_LIVE feature.
149# 157#
150 158
151# IV) Sync between trackers running in a cluster is restricted to packets 159# IV) Sync between trackers running in a cluster is restricted to packets
diff --git a/src/ot_config.c b/src/ot_config.c
index 95136b5..6b51c61 100644
--- a/src/ot_config.c
+++ b/src/ot_config.c
@@ -208,6 +208,7 @@ typedef enum {
208 CONFIG_TRACKER_REDIRECT_URL, 208 CONFIG_TRACKER_REDIRECT_URL,
209 CONFIG_LIVESYNC_CLUSTER_NODE_IP, 209 CONFIG_LIVESYNC_CLUSTER_NODE_IP,
210 CONFIG_LIVESYNC_CLUSTER_LISTEN, 210 CONFIG_LIVESYNC_CLUSTER_LISTEN,
211 CONFIG_LIVESYNC_CLUSTER_GROUP,
211} config_kind; 212} config_kind;
212 213
213typedef struct { 214typedef struct {
@@ -247,6 +248,7 @@ static const config_key config_keys[] = {
247#ifdef WANT_SYNC_LIVE 248#ifdef WANT_SYNC_LIVE
248 CONFIG_KEY("livesync.cluster.node_ip", CONFIG_LIVESYNC_CLUSTER_NODE_IP), 249 CONFIG_KEY("livesync.cluster.node_ip", CONFIG_LIVESYNC_CLUSTER_NODE_IP),
249 CONFIG_KEY("livesync.cluster.listen", CONFIG_LIVESYNC_CLUSTER_LISTEN), 250 CONFIG_KEY("livesync.cluster.listen", CONFIG_LIVESYNC_CLUSTER_LISTEN),
251 CONFIG_KEY("livesync.cluster.group", CONFIG_LIVESYNC_CLUSTER_GROUP),
250#endif 252#endif
251}; 253};
252static const size_t config_entry_count = sizeof(config_keys) / sizeof(config_keys[0]); 254static const size_t config_entry_count = sizeof(config_keys) / sizeof(config_keys[0]);
@@ -318,7 +320,7 @@ static int parse_configfile(char *config_filename) {
318 int off = scan_ip6_port(p, tmpip, &tmpport); 320 int off = scan_ip6_port(p, tmpip, &tmpport);
319 321
320 if (!off || !listen_scan_options(p + off, &shared, &workers)) 322 if (!off || !listen_scan_options(p + off, &shared, &workers))
321 goto parse_error; 323 exerr("listen.tcp, listen.udp and listen.tcp_udp want an address, optionally a port and options.");
322 if (kind != CONFIG_LISTEN_UDP) 324 if (kind != CONFIG_LISTEN_UDP)
323 listen_append(tmpip, tmpport, FLAG_TCP, shared, workers), ++bound; 325 listen_append(tmpip, tmpport, FLAG_TCP, shared, workers), ++bound;
324 if (kind != CONFIG_LISTEN_TCP) 326 if (kind != CONFIG_LISTEN_TCP)
@@ -378,6 +380,10 @@ static int parse_configfile(char *config_filename) {
378 set_config_option(&g_redirecturl, p); 380 set_config_option(&g_redirecturl, p);
379 break; 381 break;
380#ifdef WANT_SYNC_LIVE 382#ifdef WANT_SYNC_LIVE
383 case CONFIG_LIVESYNC_CLUSTER_GROUP:
384 if (!ip_parse(p, tmpip) || livesync_set_group(tmpip) == -1)
385 exerr("livesync.cluster.group wants a multicast address, out of 224.0.0.0/4.");
386 break;
381 case CONFIG_LIVESYNC_CLUSTER_NODE_IP: 387 case CONFIG_LIVESYNC_CLUSTER_NODE_IP:
382 if (!scan_ip6_net(p, &tmpnet)) 388 if (!scan_ip6_net(p, &tmpnet))
383 goto parse_error; 389 goto parse_error;
@@ -440,7 +446,7 @@ static void help(char *name) {
440 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");
441 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");
442 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");
443 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");
444} 450}
445#undef HELPLINE 451#undef HELPLINE
446 452
diff --git a/src/ot_livesync.c b/src/ot_livesync.c
index 11b75d2..85577f7 100644
--- a/src/ot_livesync.c
+++ b/src/ot_livesync.c
@@ -22,8 +22,10 @@
22 22
23#ifdef WANT_SYNC_LIVE 23#ifdef WANT_SYNC_LIVE
24 24
25ot_ip6 groupip_1 = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 224, 0, 23, 5}; 25/* The multicast group, legacy opentrackers joined by default 224.0.23.5, so it
26static uint16_t g_port = LIVESYNC_PORT; 26 stays the default */
27static ot_ip6 g_group = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 224, 0, 23, 5};
28static uint16_t g_port = LIVESYNC_PORT;
27 29
28#define LIVESYNC_INCOMING_BUFFSIZE (256 * 256) 30#define LIVESYNC_INCOMING_BUFFSIZE (256 * 256)
29 31
@@ -96,6 +98,14 @@ void livesync_deinit(void) {
96 close(g_socket_out); 98 close(g_socket_out);
97} 99}
98 100
101int livesync_set_group(const ot_ip6 group) {
102 /* v4 multicast is 224.0.0.0/4, and the socket layer has nothing else */
103 if (!ip_is_v4mapped(group) || (group[12] & 0xf0) != 0xe0)
104 return -1;
105 memcpy(g_group, group, sizeof(ot_ip6));
106 return 0;
107}
108
99void livesync_bind_mcast(ot_ip6 ip, uint16_t port) { 109void livesync_bind_mcast(ot_ip6 ip, uint16_t port) {
100 if (!ip_is_v4mapped(ip)) 110 if (!ip_is_v4mapped(ip))
101 exerr("v6 mcast support not yet available."); 111 exerr("v6 mcast support not yet available.");
@@ -105,7 +115,7 @@ void livesync_bind_mcast(ot_ip6 ip, uint16_t port) {
105 115
106 g_port = port; 116 g_port = port;
107 117
108 if ((g_socket_in = socket_mcast_join(groupip_1, ip, port)) == -1) 118 if ((g_socket_in = socket_mcast_join(g_group, ip, port)) == -1)
109 exerr("Error: Cant bind live sync incoming socket."); 119 exerr("Error: Cant bind live sync incoming socket.");
110 120
111 if ((g_socket_out = socket_mcast_bind(ip, port, 0)) == -1) 121 if ((g_socket_out = socket_mcast_bind(ip, port, 0)) == -1)
@@ -125,7 +135,7 @@ static void livesync_issue_peersync(sync_buffer *buf) {
125 has modified the protected element */ 135 has modified the protected element */
126 pthread_mutex_unlock(&g_outbuf_mutex); 136 pthread_mutex_unlock(&g_outbuf_mutex);
127 137
128 socket_mcast_send(g_socket_out, mycopy, fill, groupip_1, g_port); 138 socket_mcast_send(g_socket_out, mycopy, fill, g_group, g_port);
129} 139}
130 140
131static void livesync_handle_peersync(struct ot_workstruct *ws, size_t peer_size) { 141static void livesync_handle_peersync(struct ot_workstruct *ws, size_t peer_size) {
diff --git a/src/ot_livesync.h b/src/ot_livesync.h
index d8d20b3..cf3c64c 100644
--- a/src/ot_livesync.h
+++ b/src/ot_livesync.h
@@ -48,6 +48,10 @@
48void livesync_init(void); 48void livesync_init(void);
49void livesync_deinit(void); 49void livesync_deinit(void);
50 50
51/* The group to join and to send to, (defaults to 224.0.23.5).
52 Returns -1 if group is not a v4 multicast address. */
53int livesync_set_group(const ot_ip6 group);
54
51/* Join multicast group for listening and create sending socket */ 55/* Join multicast group for listening and create sending socket */
52void livesync_bind_mcast(ot_ip6 ip, uint16_t port); 56void livesync_bind_mcast(ot_ip6 ip, uint16_t port);
53 57
diff --git a/tests/integration/harness.py b/tests/integration/harness.py
index a9d38ca..10aa6de 100644
--- a/tests/integration/harness.py
+++ b/tests/integration/harness.py
@@ -192,7 +192,7 @@ class Tracker:
192 def torrent_count(self): 192 def torrent_count(self):
193 return int(self.stats("torr").split()[0]) 193 return int(self.stats("torr").split()[0])
194 194
195 def livesync_send(self, payload): 195 def livesync_send(self, payload, group=LIVESYNC_GROUP):
196 """Put a packet on the live sync group. 196 """Put a packet on the live sync group.
197 197
198 It has to go to the group rather than straight at the port: the 198 It has to go to the group rather than straight at the port: the
@@ -204,7 +204,7 @@ class Tracker:
204 sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton("127.0.0.1")) 204 sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton("127.0.0.1"))
205 sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) 205 sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1)
206 sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1) 206 sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1)
207 sock.sendto(payload, (LIVESYNC_GROUP, self.livesync_port)) 207 sock.sendto(payload, (group, self.livesync_port))
208 finally: 208 finally:
209 sock.close() 209 sock.close()
210 210
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
3An unrecognised key makes opentracker refuse to start, so a test that it 3An unrecognised key only warns, so coming up proves nothing on its own and a
4comes up at all is a test that the key was understood. 4test that cares has to look at what the tracker said. A key that is
5recognised but cannot be parsed is another matter, and for the listen
6statements it stops the tracker.
5""" 7"""
6 8
7import unittest 9import 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
19class WorkersOnAListenStatement(unittest.TestCase): 22class 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
35class 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 3a9ee58..f4cb2db 100644
--- a/tests/integration/test_livesync.py
+++ b/tests/integration/test_livesync.py
@@ -10,7 +10,7 @@ import struct
10import subprocess 10import subprocess
11import unittest 11import unittest
12 12
13from harness import TRACKER, TrackerTestCase 13from 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
16SYNC_PEER4 = 0 16SYNC_PEER4 = 0
@@ -164,3 +164,64 @@ class LiveSyncUnconfigured(TrackerTestCase):
164 for i in range(200): 164 for i in range(200):
165 self.tracker.announce(self.hash_for("no-cluster-%d" % i), port=6881 + i, left=0) 165 self.tracker.announce(self.hash_for("no-cluster-%d" % i), port=6881 + i, left=0)
166 self.assertEqual(self.tracker.torrent_count(), 200) 166 self.assertEqual(self.tracker.torrent_count(), 200)
167
168
169# RFC 2365 sets 239.192.0.0/14 aside for exactly this kind of use
170OWN_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.
177UNJOINED_GROUP = "239.192.0.99"
178
179
180@unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE")
181class LiveSyncGroup(TrackerTestCase):
182 config = ("livesync.cluster.group %s\n"
183 "livesync.cluster.listen 127.0.0.1:{livesync_port}\n"
184 "livesync.cluster.node_ip 127.0.0.1\n" % OWN_GROUP)
185
186 def test_the_configured_group_is_joined(self):
187 info_hash = self.hash_for("own-group")
188 self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.3.1", 6881, FLAG_SEEDING))]),
189 group=OWN_GROUP)
190 self.assertTrue(self.tracker.wait_until(lambda: self.tracker.torrent_count() == 1))
191
192 def test_another_group_is_left_alone(self):
193 """A cluster that picked its own group hears nothing from the others."""
194 info_hash = self.hash_for("not-our-group")
195 self.tracker.livesync_send(packet(SYNC_PEER4, [(info_hash, peer4("10.1.3.2", 6881, FLAG_SEEDING))]),
196 group=UNJOINED_GROUP)
197 self.assertFalse(self.tracker.wait_until(lambda: self.tracker.torrent_count() != 0, timeout=1))
198
199
200@unittest.skipUnless(has_livesync(), "this build has no WANT_SYNC_LIVE")
201class LiveSyncGroupRefused(unittest.TestCase):
202 """A cluster that asked for a group of its own must not quietly end up on
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."""
205
206 def start_with(self, group):
207 return Tracker("livesync.cluster.group %s\n"
208 "livesync.cluster.listen 127.0.0.1:{livesync_port}\n"
209 "livesync.cluster.node_ip 127.0.0.1\n" % group)
210
211 def test_a_unicast_address_stops_the_tracker(self):
212 with self.assertRaises(AssertionError) as caught:
213 self.start_with("10.0.0.1")
214 self.assertIn("multicast address", str(caught.exception))
215
216 def test_so_does_something_that_is_not_an_address(self):
217 with self.assertRaises(AssertionError) as caught:
218 self.start_with("not-an-address")
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()