]>
Commit | Line | Data |
---|---|---|
19fa75a9 A |
1 | /* dnssd-relay.c |
2 | * | |
3 | * Copyright (c) 2019 Apple Computer, Inc. All rights reserved. | |
4 | * | |
5 | * Licensed under the Apache License, Version 2.0 (the "License"); | |
6 | * you may not use this file except in compliance with the License. | |
7 | * You may obtain a copy of the License at | |
8 | * | |
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |
10 | * | |
11 | * Unless required by applicable law or agreed to in writing, software | |
12 | * distributed under the License is distributed on an "AS IS" BASIS, | |
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
14 | * See the License for the specific language governing permissions and | |
15 | * limitations under the License. | |
16 | * | |
17 | * This is a Discovery Proxy module for the SRP gateway. | |
18 | * | |
19 | * The motivation here is that it makes sense to co-locate the SRP relay and the Discovery Proxy because | |
20 | * these functions are likely to co-exist on the same node, listening on the same port. For homenet-style | |
21 | * name resolution, we need a DNS proxy that implements DNSSD Discovery Proxy for local queries, but | |
22 | * forwards other queries to an ISP resolver. The SRP gateway is already expecting to do this. | |
23 | * This module implements the functions required to allow the SRP gateway to also do Discovery Relay. | |
24 | * | |
25 | * The Discovery Proxy relies on Apple's DNS-SD library and the mDNSResponder DNSSD server, which is included | |
26 | * in Apple's open source mDNSResponder package, available here: | |
27 | * | |
28 | * https://opensource.apple.com/tarballs/mDNSResponder/ | |
29 | */ | |
30 | ||
31 | #define __APPLE_USE_RFC_3542 | |
32 | ||
33 | #include <stdlib.h> | |
34 | #include <string.h> | |
35 | #include <stdio.h> | |
36 | #include <unistd.h> | |
37 | #include <errno.h> | |
38 | #include <sys/socket.h> | |
39 | #include <netinet/in.h> | |
40 | #include <arpa/inet.h> | |
41 | #include <fcntl.h> | |
42 | #include <sys/time.h> | |
43 | #include <ctype.h> | |
44 | #include <sys/types.h> | |
45 | #include <ifaddrs.h> | |
46 | #include <net/if.h> | |
47 | ||
48 | #include "dns_sd.h" | |
49 | #include "srp.h" | |
50 | #include "dns-msg.h" | |
51 | #include "srp-crypto.h" | |
52 | #define DNSMessageHeader dns_wire_t | |
53 | #include "dso.h" | |
54 | #include "ioloop.h" | |
55 | #include "srp-tls.h" | |
56 | #include "config-parse.h" | |
57 | ||
58 | // Enumerate the list of interfaces, map them to interface indexes, give each one a name | |
59 | // Have a tree of subdomains for matching | |
60 | ||
61 | // Configuration file settings | |
62 | uint16_t udp_port; | |
63 | uint16_t tcp_port; | |
64 | uint16_t tls_port; | |
65 | #define MAX_ADDRS 10 | |
66 | char *listen_addrs[MAX_ADDRS]; | |
67 | int num_listen_addrs = 0; | |
68 | char *publish_addrs[MAX_ADDRS]; | |
69 | int num_publish_addrs = 0; | |
70 | char *tls_cacert_filename = NULL; | |
71 | char *tls_cert_filename = "/etc/dnssd-relay/server.crt"; | |
72 | char *tls_key_filename = "/etc/dnssd-relay/server.key"; | |
73 | ||
74 | // Code | |
75 | ||
76 | int64_t dso_transport_idle(void *context, int64_t now, int64_t next_event) | |
77 | { | |
78 | return next_event; | |
79 | } | |
80 | ||
81 | void | |
82 | dp_simple_response(comm_t *comm, int rcode) | |
83 | { | |
84 | if (comm->send_response) { | |
85 | struct iovec iov; | |
86 | dns_wire_t response; | |
87 | memset(&response, 0, DNS_HEADER_SIZE); | |
88 | ||
89 | // We take the ID and the opcode from the incoming message, because if the | |
90 | // header has been mangled, we (a) wouldn't have gotten here and (b) don't | |
91 | // have any better choice anyway. | |
92 | response.id = comm->message->wire.id; | |
93 | dns_qr_set(&response, dns_qr_response); | |
94 | dns_opcode_set(&response, dns_opcode_get(&comm->message->wire)); | |
95 | dns_rcode_set(&response, rcode); | |
96 | iov.iov_base = &response; | |
97 | iov.iov_len = DNS_HEADER_SIZE; // No RRs | |
98 | comm->send_response(comm, comm->message, &iov, 1); | |
99 | } | |
100 | } | |
101 | ||
102 | bool | |
103 | dso_send_formerr(dso_state_t *dso, const dns_wire_t *header) | |
104 | { | |
105 | comm_t *transport = dso->transport; | |
106 | (void)header; | |
107 | dp_simple_response(transport, dns_rcode_formerr); | |
108 | return true; | |
109 | } | |
110 | ||
111 | static void dso_message(comm_t *comm, const dns_wire_t *header, dso_state_t *dso) | |
112 | { | |
113 | switch(dso->primary.opcode) { | |
114 | case kDSOType_DNSPushSubscribe: | |
115 | dns_push_subscription_change("DNS Push Subscribe", comm, header, dso); | |
116 | break; | |
117 | case kDSOType_DNSPushUnsubscribe: | |
118 | dns_push_subscription_change("DNS Push Unsubscribe", comm, header, dso); | |
119 | break; | |
120 | ||
121 | case kDSOType_DNSPushReconfirm: | |
122 | dns_push_reconfirm(comm, header, dso); | |
123 | break; | |
124 | ||
125 | case kDSOType_DNSPushUpdate: | |
126 | INFO("dso_message: bogus push update message %d", dso->primary.opcode); | |
127 | dso_drop(dso); | |
128 | break; | |
129 | ||
130 | default: | |
131 | INFO("dso_message: unexpected primary TLV %d", dso->primary.opcode); | |
132 | dp_simple_response(comm, dns_rcode_dsotypeni); | |
133 | break; | |
134 | } | |
135 | // XXX free the message if we didn't consume it. | |
136 | } | |
137 | ||
138 | static void dns_push_callback(void *context, const void *event_context, | |
139 | dso_state_t *dso, dso_event_type_t eventType) | |
140 | { | |
141 | const dns_wire_t *message; | |
142 | switch(eventType) | |
143 | { | |
144 | case kDSOEventType_DNSMessage: | |
145 | // We shouldn't get here because we already handled any DNS messages | |
146 | message = event_context; | |
147 | INFO("dns_push_callback: DNS Message (opcode=%d) received from " PRI_S_SRP, dns_opcode_get(message), | |
148 | dso->remote_name); | |
149 | break; | |
150 | case kDSOEventType_DNSResponse: | |
151 | // We shouldn't get here because we already handled any DNS messages | |
152 | message = event_context; | |
153 | INFO("dns_push_callback: DNS Response (opcode=%d) received from " PRI_S_SRP, dns_opcode_get(message), | |
154 | dso->remote_name); | |
155 | break; | |
156 | case kDSOEventType_DSOMessage: | |
157 | INFO("dns_push_callback: DSO Message (Primary TLV=%d) received from " PRI_S_SRP, | |
158 | dso->primary.opcode, dso->remote_name); | |
159 | message = event_context; | |
160 | dso_message((comm_t *)context, message, dso); | |
161 | break; | |
162 | case kDSOEventType_DSOResponse: | |
163 | INFO("dns_push_callback: DSO Response (Primary TLV=%d) received from " PRI_S_SRP, | |
164 | dso->primary.opcode, dso->remote_name); | |
165 | break; | |
166 | ||
167 | case kDSOEventType_Finalize: | |
168 | INFO("dns_push_callback: Finalize"); | |
169 | break; | |
170 | ||
171 | case kDSOEventType_Connected: | |
172 | INFO("dns_push_callback: Connected to " PRI_S_SRP, dso->remote_name); | |
173 | break; | |
174 | ||
175 | case kDSOEventType_ConnectFailed: | |
176 | INFO("dns_push_callback: Connection to " PRI_S_SRP " failed", dso->remote_name); | |
177 | break; | |
178 | ||
179 | case kDSOEventType_Disconnected: | |
180 | INFO("dns_push_callback: Connection to " PRI_S_SRP " disconnected", dso->remote_name); | |
181 | break; | |
182 | case kDSOEventType_ShouldReconnect: | |
183 | INFO("dns_push_callback: Connection to " PRI_S_SRP " should reconnect (not for a server)", dso->remote_name); | |
184 | break; | |
185 | case kDSOEventType_Inactive: | |
186 | INFO("dns_push_callback: Inactivity timer went off, closing connection."); | |
187 | // XXX | |
188 | break; | |
189 | case kDSOEventType_Keepalive: | |
190 | INFO("dns_push_callback: should send a keepalive now."); | |
191 | break; | |
192 | case kDSOEventType_KeepaliveRcvd: | |
193 | INFO("dns_push_callback: keepalive received."); | |
194 | break; | |
195 | case kDSOEventType_RetryDelay: | |
196 | INFO("dns_push_callback: keepalive received."); | |
197 | break; | |
198 | } | |
199 | } | |
200 | ||
201 | void | |
202 | dp_dns_query(comm_t *comm, dns_rr_t *question) | |
203 | { | |
204 | int rcode; | |
205 | dnssd_query_t *query = dp_query_generate(comm, question, false, &rcode); | |
206 | const char *failnote = NULL; | |
207 | if (!query) { | |
208 | dp_simple_response(comm, rcode); | |
209 | return; | |
210 | } | |
211 | ||
212 | // For regular DNS queries, copy the ID, etc. | |
213 | query->response->id = comm->message->wire.id; | |
214 | query->response->bitfield = comm->message->wire.bitfield; | |
215 | dns_rcode_set(query->response, dns_rcode_noerror); | |
216 | ||
217 | // For DNS queries, we need to return the question. | |
218 | query->response->qdcount = htons(1); | |
219 | if (query->iface != NULL) { | |
220 | TOWIRE_CHECK("name", &query->towire, dns_name_to_wire(NULL, &query->towire, query->name)); | |
221 | TOWIRE_CHECK("enclosing_domain", &query->towire, | |
222 | dns_full_name_to_wire(&query->enclosing_domain_pointer, | |
223 | &query->towire, query->iface->domain)); | |
224 | } else { | |
225 | TOWIRE_CHECK("full name", &query->towire, dns_full_name_to_wire(NULL, &query->towire, query->name)); | |
226 | } | |
227 | TOWIRE_CHECK("TYPE", &query->towire, dns_u16_to_wire(&query->towire, question->type)); // TYPE | |
228 | TOWIRE_CHECK("CLASS", &query->towire, dns_u16_to_wire(&query->towire, question->qclass)); // CLASS | |
229 | if (failnote != NULL) { | |
230 | ERROR("dp_dns_query: failure encoding question: " PUB_S_SRP, failnote); | |
231 | goto fail; | |
232 | } | |
233 | ||
234 | // We should check for OPT RR, but for now assume it's there. | |
235 | query->is_edns0 = true; | |
236 | ||
237 | if (!dp_query_start(comm, query, &rcode, dns_query_callback)) { | |
238 | fail: | |
239 | dp_simple_response(comm, rcode); | |
240 | free(query->name); | |
241 | free(query); | |
242 | return; | |
243 | } | |
244 | ||
245 | // XXX make sure that finalize frees this. | |
246 | if (comm->message) { | |
247 | query->question = comm->message; | |
248 | comm->message = NULL; | |
249 | } | |
250 | } | |
251 | ||
252 | void dso_transport_finalize(comm_t *comm) | |
253 | { | |
254 | dso_state_t *dso = comm->dso; | |
255 | INFO("dso_transport_finalize: " PRI_S_SRP, dso->remote_name); | |
256 | if (comm) { | |
257 | ioloop_close(&comm->io); | |
258 | } | |
259 | free(dso); | |
260 | comm->dso = NULL; | |
261 | } | |
262 | ||
263 | void dns_evaluate(comm_t *comm) | |
264 | { | |
265 | dns_rr_t question; | |
266 | unsigned offset = 0; | |
267 | ||
268 | // Drop incoming responses--we're a server, so we only accept queries. | |
269 | if (dns_qr_get(&comm->message->wire) == dns_qr_response) { | |
270 | return; | |
271 | } | |
272 | ||
273 | // If this is a DSO message, see if we have a session yet. | |
274 | switch(dns_opcode_get(&comm->message->wire)) { | |
275 | case dns_opcode_dso: | |
276 | if (!comm->tcp_stream) { | |
277 | ERROR("DSO message received on non-tcp socket " PRI_S_SRP, comm->name); | |
278 | dp_simple_response(comm, dns_rcode_notimp); | |
279 | return; | |
280 | } | |
281 | ||
282 | if (!comm->dso) { | |
283 | comm->dso = dso_create(true, 0, comm->name, dns_push_callback, comm, comm); | |
284 | if (!comm->dso) { | |
285 | ERROR("Unable to create a dso context for " PRI_S_SRP, comm->name); | |
286 | dp_simple_response(comm, dns_rcode_servfail); | |
287 | ioloop_close(&comm->io); | |
288 | return; | |
289 | } | |
290 | comm->dso->transport_finalize = dso_transport_finalize; | |
291 | } | |
292 | dso_message_received(comm->dso, (uint8_t *)&comm->message->wire, comm->message->length); | |
293 | break; | |
294 | ||
295 | case dns_opcode_query: | |
296 | // In theory this is permitted but it can't really be implemented because there's no way | |
297 | // to say "here's the answer for this, and here's why that failed. | |
298 | if (ntohs(comm->message->wire.qdcount) != 1) { | |
299 | dp_simple_response(comm, dns_rcode_formerr); | |
300 | return; | |
301 | } | |
302 | if (!dns_rr_parse(&question, comm->message->wire.data, comm->message->length, &offset, 0)) { | |
303 | dp_simple_response(comm, dns_rcode_formerr); | |
304 | return; | |
305 | } | |
306 | dp_dns_query(comm, &question); | |
307 | dns_rrdata_free(&question); | |
308 | break; | |
309 | ||
310 | // No support for other opcodes yet. | |
311 | default: | |
312 | dp_simple_response(comm, dns_rcode_notimp); | |
313 | break; | |
314 | } | |
315 | } | |
316 | ||
317 | void dns_input(comm_t *comm) | |
318 | { | |
319 | dns_evaluate(comm); | |
320 | if (comm->message != NULL) { | |
321 | message_free(comm->message); | |
322 | comm->message = NULL; | |
323 | } | |
324 | } | |
325 | ||
326 | static int | |
327 | usage(const char *progname) | |
328 | { | |
329 | ERROR("usage: " PUB_S_SRP, progname); | |
330 | ERROR("ex: dnssd-proxy"); | |
331 | return 1; | |
332 | } | |
333 | ||
334 | // Called whenever we get a connection. | |
335 | void | |
336 | connected(comm_t *comm) | |
337 | { | |
338 | INFO("connection from " PRI_S_SRP, comm->name); | |
339 | return; | |
340 | } | |
341 | ||
342 | static bool config_string_handler(char **ret, const char *filename, const char *string, int lineno, bool tdot, | |
343 | bool ldot) | |
344 | { | |
345 | char *s; | |
346 | int add_trailing_dot = 0; | |
347 | int add_leading_dot = ldot ? 1 : 0; | |
348 | int len = strlen(string); | |
349 | ||
350 | // Space for NUL and leading dot. | |
351 | if (tdot && len > 0 && string[len - 1] != '.') { | |
352 | add_trailing_dot = 1; | |
353 | } | |
354 | s = malloc(strlen(string) + add_leading_dot + add_trailing_dot + 1); | |
355 | if (s == NULL) { | |
356 | ERROR("Unable to allocate domain name " PRI_S_SRP, string); | |
357 | return false; | |
358 | } | |
359 | *ret = s; | |
360 | if (ldot) { | |
361 | *s++ = '.'; | |
362 | } | |
363 | strcpy(s, string); | |
364 | if (add_trailing_dot) { | |
365 | s[len] = '.'; | |
366 | s[len + 1] = 0; | |
367 | } | |
368 | return true; | |
369 | } | |
370 | ||
371 | // Config file parsing... | |
372 | static bool interface_handler(void *context, const char *filename, char **hunks, int num_hunks, int lineno) | |
373 | { | |
374 | interface_t *interface = calloc(1, sizeof *interface); | |
375 | if (interface == NULL) { | |
376 | ERROR("Unable to allocate interface " PUB_S_SRP, hunks[1]); | |
377 | return false; | |
378 | } | |
379 | ||
380 | interface->name = strdup(hunks[1]); | |
381 | if (interface->name == NULL) { | |
382 | ERROR("Unable to allocate interface name " PUB_S_SRP, hunks[1]); | |
383 | free(interface); | |
384 | return false; | |
385 | } | |
386 | ||
387 | if (!strcmp(hunks[0], "nopush")) { | |
388 | interface->no_push = true; | |
389 | } | |
390 | ||
391 | if (new_served_domain(interface, hunks[2]) == NULL) { | |
392 | free(interface->name); | |
393 | free(interface); | |
394 | return false; | |
395 | } | |
396 | return true; | |
397 | } | |
398 | ||
399 | static bool port_handler(void *context, const char *filename, char **hunks, int num_hunks, int lineno) | |
400 | { | |
401 | char *ep = NULL; | |
402 | long port = strtol(hunks[1], &ep, 10); | |
403 | if (port < 0 || port > 65535 || *ep != 0) { | |
404 | ERROR("Invalid port number: " PUB_S_SRP, hunks[1]); | |
405 | return false; | |
406 | } | |
407 | if (!strcmp(hunks[0], "udp-port")) { | |
408 | udp_port = port; | |
409 | } else if (!strcmp(hunks[0], "tcp-port")) { | |
410 | tcp_port = port; | |
411 | } else if (!strcmp(hunks[0], "tls-port")) { | |
412 | tls_port = port; | |
413 | } | |
414 | return true; | |
415 | } | |
416 | ||
417 | static bool listen_addr_handler(void *context, const char *filename, char **hunks, int num_hunks, int lineno) | |
418 | { | |
419 | if (num_listen_addrs == MAX_ADDRS) { | |
420 | ERROR("Only %d IPv4 listen addresses can be configured.", MAX_ADDRS); | |
421 | return false; | |
422 | } | |
423 | return config_string_handler(&listen_addrs[num_listen_addrs++], filename, hunks[1], lineno, false, false); | |
424 | } | |
425 | ||
426 | static bool tls_key_handler(void *context, const char *filename, char **hunks, int num_hunks, int lineno) | |
427 | { | |
428 | return config_string_handler(&tls_key_filename, filename, hunks[1], lineno, false, false); | |
429 | } | |
430 | ||
431 | static bool tls_cert_handler(void *context, const char *filename, char **hunks, int num_hunks, int lineno) | |
432 | { | |
433 | return config_string_handler(&tls_cert_filename, filename, hunks[1], lineno, false, false); | |
434 | } | |
435 | ||
436 | static bool tls_cacert_handler(void *context, const char *filename, char **hunks, int num_hunks, int lineno) | |
437 | { | |
438 | return config_string_handler(&tls_cacert_filename, filename, hunks[1], lineno, false, false); | |
439 | } | |
440 | ||
441 | config_file_verb_t dp_verbs[] = { | |
442 | { "interface", 3, 3, interface_handler }, // interface <name> <domain> | |
443 | { "nopush", 3, 3, interface_handler }, // nopush <name> <domain> | |
444 | { "udp-port", 2, 2, port_handler }, // udp-port <number> | |
445 | { "tcp-port", 2, 2, port_handler }, // tcp-port <number> | |
446 | { "tls-port", 2, 2, port_handler }, // tls-port <number> | |
447 | { "tls-key", 2, 2, tls_key_handler }, // tls-key <filename> | |
448 | { "tls-cert", 2, 2, tls_cert_handler }, // tls-cert <filename> | |
449 | { "tls-cacert", 2, 2, tls_cacert_handler }, // tls-cacert <filename> | |
450 | { "listen-addr", 2, 2, listen_addr_handler }, // listen-addr <IP address> | |
451 | }; | |
452 | #define NUMCFVERBS ((sizeof dp_verbs) / sizeof (config_file_verb_t)) | |
453 | ||
454 | int | |
455 | main(int argc, char **argv) | |
456 | { | |
457 | int i; | |
458 | comm_t *listener[4 + MAX_ADDRS]; | |
459 | int num_listeners = 0; | |
460 | ||
461 | udp_port = tcp_port = 53; | |
462 | tls_port = 853; | |
463 | ||
464 | // Parse command line arguments | |
465 | for (i = 1; i < argc; i++) { | |
466 | return usage(argv[0]); | |
467 | } | |
468 | ||
469 | // Read the config file | |
470 | if (!config_parse(NULL, "/etc/dnssd-relay.cf", dp_verbs, NUMCFVERBS)) { | |
471 | return 1; | |
472 | } | |
473 | ||
474 | map_interfaces(); | |
475 | ||
476 | if (!srp_tls_init()) { | |
477 | return 1; | |
478 | } | |
479 | ||
480 | if (!ioloop_init()) { | |
481 | return 1; | |
482 | } | |
483 | ||
484 | for (i = 0; i < num_listen_addrs; i++) { | |
485 | listener[num_listeners] = setup_listener_socket(AF_UNSPEC, IPPROTO_TCP, true, | |
486 | tls_port, listen_addrs[i], "DNS TLS Listener", dns_input, | |
487 | connected, 0); | |
488 | if (listener[num_listeners] == NULL) { | |
489 | ERROR("TLS4 listener: fail."); | |
490 | return 1; | |
491 | } | |
492 | num_listeners++; | |
493 | } | |
494 | ||
495 | // If we haven't been given any addresses to listen on, listen on an IPv4 address and an IPv6 address. | |
496 | if (num_listen_addrs == 0) { | |
497 | listener[num_listeners] = setup_listener_socket(AF_INET, IPPROTO_TCP, true, tls_port, NULL, | |
498 | "IPv4 DNS TLS Listener", dns_input, 0, 0); | |
499 | if (listener[num_listeners] == NULL) { | |
500 | ERROR("UDP4 listener: fail."); | |
501 | return 1; | |
502 | } | |
503 | num_listeners++; | |
504 | ||
505 | listener[num_listeners] = setup_listener_socket(AF_INET6, IPPROTO_TCP, true, tls_port, NULL, | |
506 | "IPv6 DNS TLS Listener", dns_input, 0, 0); | |
507 | if (listener[num_listeners] == NULL) { | |
508 | ERROR("UDP6 listener: fail."); | |
509 | return 1; | |
510 | } | |
511 | num_listeners++; | |
512 | } | |
513 | ||
514 | for (i = 0; i < num_listeners; i++) { | |
515 | INFO("Started " PRI_S_SRP, listener[i]->name); | |
516 | } | |
517 | ||
518 | do { | |
519 | int something = 0; | |
520 | something = ioloop_events(0); | |
521 | INFO("dispatched %d events.", something); | |
522 | } while (1); | |
523 | } | |
524 | ||
525 | // Local Variables: | |
526 | // mode: C | |
527 | // tab-width: 4 | |
528 | // c-file-style: "bsd" | |
529 | // c-basic-offset: 4 | |
530 | // fill-column: 108 | |
531 | // indent-tabs-mode: nil | |
532 | // End: |