| /* srp-dns-proxy.c |
| * |
| * Copyright (c) 2018-2021 Apple Inc. All rights reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * https://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| * |
| * This is a DNSSD Service Registration Protocol gateway. The purpose of this is to make it possible |
| * for SRP clients to update DNS servers that don't support SRP. |
| * |
| * The way it works is that this gateway listens on port ANY:53 and forwards either to another port on |
| * the same host (not recommended) or to any port (usually 53) on a different host. Requests are accepted |
| * over both TCP and UDP in principle, but UDP requests should be from constrained nodes, and rely on |
| * network topology for authentication. |
| * |
| * Note that this is not a full DNS proxy, so you can't just put it in front of a DNS server. |
| */ |
| |
| // Get DNS server IP address |
| // Get list of permitted source subnets for TCP updates |
| // Get list of permitted source subnet/interface tuples for UDP updates |
| // Set up UDP listener |
| // Set up TCP listener (no TCP Fast Open) |
| // Event loop |
| // Transaction processing: |
| // 1. If UDP, validate that it's from a subnet that is valid for the interface on which it was received. |
| // 2. If TCP, validate that it's from a permitted subnet |
| // 3. Check that the message is a valid SRP update according to the rules |
| // 4. Check the signature |
| // 5. Do a DNS Update with prerequisites to prevent overwriting a host record with the same owner name but |
| // a different key. |
| // 6. Send back the response |
| |
| #define __APPLE_USE_RFC_3542 |
| |
| #include <stdlib.h> |
| #include <string.h> |
| #include <stdio.h> |
| #include <unistd.h> |
| #include <errno.h> |
| #include <sys/socket.h> |
| #include <netinet/in.h> |
| #include <arpa/inet.h> |
| #include <fcntl.h> |
| #include <sys/time.h> |
| #include <dns_sd.h> |
| |
| #include "srp.h" |
| #include "dns-msg.h" |
| #include "srp-crypto.h" |
| #include "ioloop.h" |
| #include "srp-gw.h" |
| #include "config-parse.h" |
| #include "srp-proxy.h" |
| |
| static addr_t dns_server; |
| static dns_name_t *service_update_zone; // The zone to update when we receive an update for default.service.arpa. |
| static hmac_key_t *key; |
| |
| static int |
| usage(const char *progname) |
| { |
| ERROR("usage: %s -s <addr> <port> -k <key-file> -t <subnet> ... -u <ifname> <subnet> ...", progname); |
| ERROR(" -s can only appear once."); |
| ERROR(" -k can appear once."); |
| ERROR(" -t can only appear once, and is followed by one or more subnets."); |
| ERROR(" -u can appear more than once, is followed by one interface name, and"); |
| ERROR(" one or more subnets."); |
| ERROR(" <addr> is an IPv4 address or IPv6 address."); |
| ERROR(" <port> is a UDP port number."); |
| ERROR(" <key-file> is a file containing an HMAC-SHA256 key for authenticating updates to the auth server."); |
| ERROR(" <subnet> is an IP address followed by a slash followed by the prefix width."); |
| ERROR(" <ifname> is the printable name of the interface."); |
| ERROR("ex: srp-gw -s 2001:DB8::1 53 -k srp.key -t 2001:DB8:1300::/48 -u en0 2001:DB8:1300:1100::/56"); |
| return 1; |
| } |
| |
| // Free the data structures into which the SRP update was parsed. The pointers to the various DNS objects that these |
| // structures point to are owned by the parsed DNS message, and so these do not need to be freed here. |
| void |
| update_free_parts(service_instance_t *service_instances, service_instance_t *added_instances, |
| service_t *services, dns_host_description_t *host_description) |
| { |
| service_instance_t *sip; |
| service_t *sp; |
| |
| for (sip = service_instances; sip; ) { |
| service_instance_t *next = sip->next; |
| free(sip); |
| sip = next; |
| } |
| for (sip = added_instances; sip; ) { |
| service_instance_t *next = sip->next; |
| free(sip); |
| sip = next; |
| } |
| for (sp = services; sp; ) { |
| service_t *next = sp->next; |
| free(sp); |
| sp = next; |
| } |
| if (host_description != NULL) { |
| free(host_description); |
| } |
| } |
| |
| // Free all the stuff that we accumulated while processing the SRP update. |
| void |
| update_free(update_t *update) |
| { |
| // Free all of the structures we collated RRs into: |
| update_free_parts(update->instances, update->added_instances, update->services, update->host); |
| // We don't need to free the zone name: it's either borrowed from the message, |
| // or it's service_update_zone, which is static. |
| message_free(update->message); |
| dns_message_free(update->parsed_message); |
| free(update); |
| } |
| |
| |
| #define name_to_wire(towire, name) name_to_wire_(towire, name, __LINE__) |
| void |
| name_to_wire_(dns_towire_state_t *towire, dns_name_t *name, int line) |
| { |
| // Does compression... |
| dns_concatenate_name_to_wire_(towire, name, NULL, NULL, line); |
| } |
| |
| void |
| rdata_to_wire(dns_towire_state_t *towire, dns_rr_t *rr) |
| { |
| dns_rdlength_begin(towire); |
| |
| // These are the only types we expect to see. If something else were passed, it would be written as rdlen=0. |
| switch(rr->type) { |
| case dns_rrtype_ptr: |
| name_to_wire(towire, rr->data.ptr.name); |
| break; |
| |
| case dns_rrtype_srv: |
| dns_u16_to_wire(towire, rr->data.srv.priority); |
| dns_u16_to_wire(towire, rr->data.srv.weight); |
| dns_u16_to_wire(towire, rr->data.srv.port); |
| name_to_wire(towire, rr->data.srv.name); |
| break; |
| |
| case dns_rrtype_txt: |
| dns_rdata_raw_data_to_wire(towire, rr->data.txt.data, rr->data.txt.len); |
| break; |
| |
| case dns_rrtype_key: |
| dns_u16_to_wire(towire, rr->data.key.flags); |
| dns_u8_to_wire(towire, rr->data.key.protocol); |
| dns_u8_to_wire(towire, rr->data.key.algorithm); |
| dns_rdata_raw_data_to_wire(towire, rr->data.key.key, rr->data.key.len); |
| break; |
| |
| case dns_rrtype_a: |
| dns_rdata_raw_data_to_wire(towire, &rr->data.a, sizeof rr->data.a); |
| break; |
| |
| case dns_rrtype_aaaa: |
| dns_rdata_raw_data_to_wire(towire, &rr->data.aaaa, sizeof rr->data.aaaa); |
| break; |
| } |
| |
| dns_rdlength_end(towire); |
| } |
| |
| // We only list the types we are using--there are other types that we don't support. |
| typedef enum prereq_type prereq_type_t; |
| enum prereq_type { |
| update_rrset_equals, // RFC 2136 section 2.4.2: RRset Exists (Value Dependent) |
| update_name_not_in_use, // RFC 2136 section 2.4.5: Name Is Not In Use |
| }; |
| |
| void |
| add_prerequisite(dns_wire_t *msg, dns_towire_state_t *towire, prereq_type_t ptype, dns_name_t *name, dns_rr_t *rr) |
| { |
| char namebuf[DNS_MAX_NAME_SIZE + 1]; |
| if (ntohs(msg->nscount) != 0 || ntohs(msg->arcount) != 0) { |
| ERROR("%s: adding prerequisite after updates", dns_name_print(name, namebuf, sizeof namebuf)); |
| towire->truncated = true; |
| } |
| name_to_wire(towire, name); |
| switch(ptype) { |
| case update_rrset_equals: |
| dns_u16_to_wire(towire, rr->type); |
| dns_u16_to_wire(towire, rr->qclass); |
| dns_ttl_to_wire(towire, 0); |
| rdata_to_wire(towire, rr); |
| break; |
| case update_name_not_in_use: |
| dns_u16_to_wire(towire, dns_rrtype_any); // TYPE |
| dns_u16_to_wire(towire, dns_qclass_none); // CLASS |
| dns_ttl_to_wire(towire, 0); // TTL |
| dns_u16_to_wire(towire, 0); // RDLEN |
| break; |
| } |
| msg->ancount = htons(ntohs(msg->ancount) + 1); |
| } |
| |
| // We actually only support one type of delete, so it's a bit silly to specify it, but in principle we might |
| // want more later. |
| typedef enum delete_type delete_type_t; |
| enum delete_type { |
| delete_name, // RFC 2136 section 2.5.3: Delete all RRsets from a name |
| }; |
| |
| void |
| add_delete(dns_wire_t *msg, dns_towire_state_t *towire, delete_type_t dtype, dns_name_t *name) |
| { |
| name_to_wire(towire, name); |
| switch(dtype) { |
| case delete_name: |
| dns_u16_to_wire(towire, dns_rrtype_any); // TYPE |
| dns_u16_to_wire(towire, dns_qclass_any); // CLASS |
| dns_ttl_to_wire(towire, 0); // TTL |
| dns_u16_to_wire(towire, 0); // RDLEN |
| break; |
| } |
| msg->nscount = htons(ntohs(msg->nscount) + 1); |
| } |
| |
| // Copy the RR we received in the SRP update out in wire format. |
| |
| void |
| add_rr(dns_wire_t *msg, dns_towire_state_t *towire, dns_name_t *name, dns_rr_t *rr) |
| { |
| if (rr != NULL) { |
| name_to_wire(towire, name); |
| dns_u16_to_wire(towire, rr->type); // TYPE |
| dns_u16_to_wire(towire, rr->qclass); // CLASS |
| dns_ttl_to_wire(towire, rr->ttl); // TTL |
| rdata_to_wire(towire, rr); // RDLEN |
| msg->nscount = htons(ntohs(msg->nscount) + 1); |
| } |
| } |
| |
| // Construct an update of the specified type, assuming that the record being updated |
| // either exists or does not exist, depending on the value of exists. Actual records |
| // to be update are taken from the update_t. |
| // |
| // Analysis: |
| // |
| // The goal of the update is to either bring the zone to the state described in the SRP update, or |
| // determine that the state described in the SRP update conflicts with what is already present in |
| // the zone. |
| // |
| // Possible scenarios: |
| // 1. Update and Zone are the same (A and AAAA records may differ): |
| // Prerequisites: |
| // a. for each instance: KEY RR exists on instance name and is the same |
| // b. for host: KEY RR exists on host name and is the same |
| // Update: |
| // a. for each instance: delete all records on instance name, add KEY RR, add SRV RR, add TXT RR |
| // b. for host: delete host instance, add A, AAAA and KEY RRs |
| // c. for each service: add PTR record pointing on service name to service instance name |
| // |
| // We should try 1 first, because it should be the steady state case; that is, it should be what happens |
| // most of the time. |
| // If 1 fails, then we could have some service instances present and others not. There is no way to |
| // know without trying. We can at this point either try to add each service instance in a separate update, |
| // or assume that none are present and add them all at once, and then if this fails add them individually. |
| // I think that it makes sense to try them all first, because that should be the second most common case: |
| // |
| // 2. Nothing in update is present in zone: |
| // Prerequisites: |
| // a. For each instance: instance name is not in use |
| // b. Host name is not in use |
| // Update: |
| // a. for each instance: add KEY RR, add SRV RR, add TXT RR on instance name |
| // b. for host: add A, AAAA and KEY RRs on host name |
| // c. for each service: add PTR record pointing on service name to service instance name |
| // |
| // If either (1) or (2) works, we're done. If both fail, then we need to do the service instance updates |
| // and host update one by one. This is a bit nasty because we actually have to try twice: once assuming |
| // the RR exists, and once assuming it doesn't. If any of the instance updates fail, or the host update |
| // fails, we delete all the ones that succeeded. |
| // |
| // In the cases other than (1) and (2), we can add all the service PTRs in the host update, because they're |
| // only added if the host update succeeds; if it fails, we have to go back and remove all the service |
| // instances. |
| // |
| // One open question for the SRP document: we probably want to signal whether the conflict is with the |
| // hostname or one of the service instance names. We can do this with an EDNS(0) option. |
| // |
| // The flow will be: |
| // - Try to update assuming everything is there already (case 1) |
| // - Try to update assuming nothing is there already (case 2) |
| // - For each service instance: |
| // - Try to update assuming it's not there; if this succeeds, add this instance to the list of |
| // instances that have been added. If not: |
| // - Try to update assuming it is there |
| // - If this fails, go to fail |
| // - Try to update the host (and also services) assuming the host is not there. If this fails: |
| // - Try to update the host (and also services) assuming the host is there. If this succeeds: |
| // - return success |
| // fail: |
| // - For each service instance in the list of instances that have been added: |
| // - delete all records on the instance name. |
| // |
| // One thing that isn't accounted for here: it's possible that a previous update added some but not all |
| // instances in the current update. Subsequently, some other device may have claimed an instance that is |
| // present but in conflict in the current update. In this case, all of the instances prior to that one |
| // in the update will actually have been updated by this update, but then the update as a whole will fail. |
| // I think this is unlikely to be an actual problem, and there's no way to address it without a _lot_ of |
| // complexity. |
| |
| bool |
| construct_update(update_t *update) |
| { |
| dns_towire_state_t towire; |
| dns_wire_t *msg = update->update; // Solely to reduce the amount of typing. |
| service_instance_t *instance; |
| service_t *service; |
| host_addr_t *host_addr; |
| |
| // Set up the message constructor |
| memset(&towire, 0, sizeof towire); |
| towire.p = &msg->data[0]; // We start storing RR data here. |
| towire.lim = &msg->data[0] + update->update_max; // This is the limit to how much we can store. |
| towire.message = msg; |
| |
| // Initialize the update message... |
| memset(msg, 0, DNS_HEADER_SIZE); |
| dns_qr_set(msg, dns_qr_query); |
| dns_opcode_set(msg, dns_opcode_update); |
| msg->id = srp_random16(); |
| |
| // An update always has one question, which is the zone name. |
| msg->qdcount = htons(1); |
| name_to_wire(&towire, update->zone_name); |
| dns_u16_to_wire(&towire, dns_rrtype_soa); |
| dns_u16_to_wire(&towire, dns_qclass_in); |
| |
| switch(update->state) { |
| case connect_to_server: |
| ERROR("Update construction requested when still connecting."); |
| update->update_length = 0; |
| return false; |
| |
| // Do a DNS Update for a service instance |
| case refresh_existing: |
| // Add a "KEY exists and is <x> and a PTR exists and is <x> prerequisite for each instance being updated. |
| for (instance = update->instances; instance; instance = instance->next) { |
| add_prerequisite(msg, &towire, update_rrset_equals, instance->name, update->host->key); |
| } |
| add_prerequisite(msg, &towire, update_rrset_equals, update->host->name, update->host->key); |
| // Now add a delete for each service instance |
| for (instance = update->instances; instance; instance = instance->next) { |
| add_delete(msg, &towire, delete_name, instance->name); |
| } |
| add_delete(msg, &towire, delete_name, update->host->name); |
| |
| add_instances: |
| // Now add the update for each instance. |
| for (instance = update->instances; instance; instance = instance->next) { |
| add_rr(msg, &towire, instance->name, update->host->key); |
| add_rr(msg, &towire, instance->name, instance->srv); |
| add_rr(msg, &towire, instance->name, instance->txt); |
| } |
| // Add the update for each service |
| for (service = update->services; service; service = service->next) { |
| add_rr(msg, &towire, service->rr->name, service->rr); |
| } |
| // Add the host records... |
| add_rr(msg, &towire, update->host->name, update->host->key); |
| for (host_addr = update->host->addrs; host_addr; host_addr = host_addr->next) { |
| add_rr(msg, &towire, update->host->name, &host_addr->rr); |
| } |
| break; |
| |
| case create_nonexistent: |
| // Add a "name not in use" prerequisite for each instance being updated. |
| for (instance = update->instances; instance; instance = instance->next) { |
| add_prerequisite(msg, &towire, update_name_not_in_use, instance->name, (dns_rr_t *)NULL); |
| } |
| add_prerequisite(msg, &towire, update_name_not_in_use, update->host->name, (dns_rr_t *)NULL); |
| goto add_instances; |
| |
| case create_nonexistent_instance: |
| // The only prerequisite is that this specific service instance doesn't exist. |
| add_prerequisite(msg, &towire, update_name_not_in_use, update->instance->name, (dns_rr_t *)NULL); |
| goto add_instance; |
| |
| case refresh_existing_instance: |
| // If instance already exists, prerequisite is that it has the same key, and we also have to |
| // delete all RRs on the name before adding our RRs, in case they have changed. |
| add_prerequisite(msg, &towire, update_rrset_equals, update->instance->name, update->host->key); |
| add_delete(msg, &towire, delete_name, update->instance->name); |
| add_instance: |
| add_rr(msg, &towire, update->instance->name, update->host->key); |
| add_rr(msg, &towire, update->instance->name, update->instance->srv); |
| add_rr(msg, &towire, update->instance->name, update->instance->txt); |
| break; |
| |
| case create_nonexistent_host: |
| add_prerequisite(msg, &towire, update_name_not_in_use, update->host->name, (dns_rr_t *)NULL); |
| goto add_host; |
| |
| case refresh_existing_host: |
| add_prerequisite(msg, &towire, update_rrset_equals, update->host->name, update->host->key); |
| add_delete(msg, &towire, delete_name, update->host->name); |
| // Add the service PTRs here--these don't need to be in a separate update, because if we get here |
| // the only thing that can make adding them not okay is if adding the host fails. |
| // Add the update for each service |
| for (service = update->services; service; service = service->next) { |
| add_rr(msg, &towire, service->rr->name, service->rr); |
| } |
| add_host: |
| // Add the host records... |
| add_rr(msg, &towire, update->host->name, update->host->key); |
| for (host_addr = update->host->addrs; host_addr; host_addr = host_addr->next) { |
| add_rr(msg, &towire, update->host->name, &host_addr->rr); |
| } |
| break; |
| |
| case delete_failed_instance: |
| // Delete all the instances we successfull added before discovering a problem. |
| // It is possible in principle that these could have been overwritten by some other |
| // process and we could be deleting the wrong stuff, but in practice this should |
| // never happen if these are legitimately managed by SRP. Once a name has been |
| // claimed by SRP, it should continue to be managed by SRP until its lease expires |
| // and SRP deletes it, at which point it is of course fair game. |
| for (instance = update->instances; instance; instance = instance->next) { |
| add_delete(msg, &towire, delete_name, instance->name); |
| } |
| break; |
| } |
| if (towire.error != 0) { |
| ERROR("construct_update: error %s while generating update at line %d", strerror(towire.error), towire.line); |
| return false; |
| } |
| update->update_length = towire.p - (uint8_t *)msg; |
| return true; |
| } |
| |
| void |
| update_finished(update_t *update, int rcode) |
| { |
| comm_t *comm = update->client; |
| struct iovec iov; |
| dns_wire_t response; |
| INFO("Update Finished, rcode = " PUB_S_SRP, dns_rcode_name(rcode)); |
| |
| memset(&response, 0, DNS_HEADER_SIZE); |
| response.id = update->message->wire.id; |
| response.bitfield = update->message->wire.bitfield; |
| dns_rcode_set(&response, rcode); |
| dns_qr_set(&response, dns_qr_response); |
| |
| iov.iov_base = &response; |
| iov.iov_len = DNS_HEADER_SIZE; |
| |
| comm->send_response(comm, update->message, &iov, 1); |
| |
| // If success, construct a response |
| // If fail, send a quick status code |
| // Signal host name conflict and instance name conflict using different rcodes (?) |
| // Okay, so if there's a host name/instance name conflict, and the host name has the right key, then |
| // the instance name is actually bogus and should be overwritten. |
| // If the host has the wrong key, and the instance is present, then the instance is also bogus. |
| // So in each of these cases, perhaps we should just gc the instance. |
| // This would mean that there is nothing to signal: either the instance is a mismatch, and we |
| // overwrite it and return success, or the host is a mismatch and we gc the instance and return failure. |
| ioloop_close(&update->server->io); |
| update_free(update); |
| } |
| |
| void |
| update_send(update_t *update) |
| { |
| struct iovec iov[4]; |
| dns_towire_state_t towire; |
| dns_wire_t *msg = update->update; |
| struct timeval tv; |
| uint8_t *p_mac; |
| #ifdef DEBUG_DECODE_UPDATE |
| dns_message_t *decoded; |
| #endif |
| |
| // Set up the message constructor |
| memset(&towire, 0, sizeof towire); |
| towire.p = (uint8_t *)msg + update->update_length; // We start storing RR data here. |
| towire.lim = &msg->data[0] + update->update_max; // This is the limit to how much we can store. |
| towire.message = msg; |
| towire.p_rdlength = NULL; |
| towire.p_opt = NULL; |
| |
| // If we have a key, sign the message with the key using TSIG HMAC-SHA256. |
| if (key != NULL) { |
| // Maintain an IOV with the bits of the message that we need to sign. |
| iov[0].iov_base = msg; |
| |
| name_to_wire(&towire, key->name); |
| iov[0].iov_len = towire.p - (uint8_t *)iov[0].iov_base; |
| dns_u16_to_wire(&towire, dns_rrtype_tsig); // RRTYPE |
| iov[1].iov_base = towire.p; |
| dns_u16_to_wire(&towire, dns_qclass_any); // CLASS |
| dns_ttl_to_wire(&towire, 0); // TTL |
| iov[1].iov_len = towire.p - (uint8_t *)iov[1].iov_base; |
| // The message digest skips the RDLEN field. |
| dns_rdlength_begin(&towire); // RDLEN |
| iov[2].iov_base = towire.p; |
| dns_full_name_to_wire(NULL, &towire, "hmac-sha256."); // Algorithm Name |
| gettimeofday(&tv, NULL); |
| dns_u48_to_wire(&towire, tv.tv_sec); // Time since epoch |
| dns_u16_to_wire(&towire, 300); // Fudge interval |
| // (clocks can be skewed by up to 5 minutes) |
| // Message digest doesn't cover MAC size or MAC fields, for obvious reasons, nor original message ID. |
| iov[2].iov_len = towire.p - (uint8_t *)iov[2].iov_base; |
| dns_u16_to_wire(&towire, SRP_SHA256_DIGEST_SIZE); // MAC Size |
| p_mac = towire.p; // MAC |
| if (!towire.error) { |
| if (towire.p + SRP_SHA256_DIGEST_SIZE >= towire.lim) { |
| towire.error = ENOBUFS; |
| towire.truncated = true; |
| towire.line = __LINE__; |
| } else { |
| towire.p += SRP_SHA256_DIGEST_SIZE; |
| } |
| } |
| // We have to copy the message ID into the tsig signature; this is because in some cases, although not this one, |
| // the message ID will be overwritten. So the copy of the ID is what's validated, but it's copied into the |
| // header for validation, so we don't include it when generating the hash. |
| dns_rdata_raw_data_to_wire(&towire, &msg->id, sizeof msg->id); |
| iov[3].iov_base = towire.p; |
| dns_u16_to_wire(&towire, 0); // TSIG Error (always 0 on send). |
| dns_u16_to_wire(&towire, 0); // Other Len (MBZ?) |
| iov[3].iov_len = towire.p - (uint8_t *)iov[3].iov_base; |
| dns_rdlength_end(&towire); |
| |
| // Okay, we have stored the TSIG signature, now compute the message digest. |
| srp_hmac_iov(key, p_mac, SRP_SHA256_DIGEST_SIZE, &iov[0], 4); |
| msg->arcount = htons(ntohs(msg->arcount) + 1); |
| update->update_length = towire.p - (const uint8_t *)msg; |
| } |
| |
| if (towire.error != 0) { |
| ERROR("update_send: error \"%s\" while generating update at line %d", |
| strerror(towire.error), towire.line); |
| update_finished(update, dns_rcode_servfail); |
| return; |
| } |
| |
| #ifdef DEBUG_DECODE_UPDATE |
| if (!dns_wire_parse(&decoded, msg, update->update_length, false)) { |
| ERROR("Constructed message does not successfully parse."); |
| update_finished(update, dns_rcode_servfail); |
| return; |
| } |
| #endif |
| |
| // Transmit the update |
| iov[0].iov_base = update->update; |
| iov[0].iov_len = update->update_length; |
| update->server->send_response(update->server, update->message, iov, 1); |
| } |
| |
| void |
| update_connect_callback(comm_t *comm) |
| { |
| update_t *update = comm->context; |
| |
| // Once we're connected, construct the first update. |
| INFO("Connected to " PUB_S_SRP ".", comm->name); |
| // STATE CHANGE: connect_to_server -> refresh_existing |
| update->state = refresh_existing; |
| if (!construct_update(update)) { |
| update_finished(update, dns_rcode_servfail); |
| return; |
| } |
| update_send(update); |
| } |
| |
| const char *NONNULL |
| update_state_name(update_state_t state) |
| { |
| switch(state) { |
| case connect_to_server: |
| return "connect_to_server"; |
| case create_nonexistent: |
| return "create_nonexistent"; |
| case refresh_existing: |
| return "refresh_existing"; |
| case create_nonexistent_instance: |
| return "create_nonexistent_instance"; |
| case refresh_existing_instance: |
| return "refresh_existing_instance"; |
| case create_nonexistent_host: |
| return "create_nonexistent_host"; |
| case refresh_existing_host: |
| return "refresh_existing_host"; |
| case delete_failed_instance: |
| return "delete_failed_instance"; |
| } |
| return "unknown state"; |
| } |
| |
| void |
| update_finalize(io_t *context) |
| { |
| } |
| |
| void |
| update_disconnect_callback(comm_t *comm, int error) |
| { |
| update_t *update = comm->context; |
| |
| if (update->state == connect_to_server) { |
| INFO(PUB_S_SRP " disconnected: " PUB_S_SRP, comm->name, strerror(error)); |
| update_finished(update, dns_rcode_servfail); |
| } else { |
| // This could be bad if any updates succeeded. |
| ERROR("%s disconnected during update in state %s: %s", |
| comm->name, update_state_name(update->state), strerror(error)); |
| update_finished(update, dns_rcode_servfail); |
| } |
| } |
| |
| void |
| update_reply_callback(comm_t *comm) |
| { |
| update_t *update = comm->context; |
| dns_wire_t *wire = &comm->message->wire; |
| char namebuf[DNS_MAX_NAME_SIZE + 1], namebuf1[DNS_MAX_NAME_SIZE + 1]; |
| service_instance_t **pinstance; |
| update_state_t initial_state; |
| service_instance_t *initial_instance; |
| |
| initial_instance = update->instance; |
| initial_state = update->state; |
| |
| INFO("Message from " PUB_S_SRP " in state " PUB_S_SRP ", rcode = " PUB_S_SRP ".", comm->name, |
| update_state_name(update->state), dns_rcode_name(dns_rcode_get(wire))); |
| |
| // Sanity check the response |
| if (dns_qr_get(wire) == dns_qr_query) { |
| ERROR("Received a query from the authoritative server!"); |
| update_finished(update, dns_rcode_servfail); |
| return; |
| } |
| if (dns_opcode_get(wire) != dns_opcode_update) { |
| ERROR("Received a response with opcode %d from the authoritative server!", |
| dns_opcode_get(wire)); |
| update_finished(update, dns_rcode_servfail); |
| return; |
| } |
| if (update->update == NULL) { |
| ERROR("Received a response from auth server when no update has been sent yet."); |
| update_finished(update, dns_rcode_servfail); |
| } |
| // This isn't an error in the protocol, because we might be pipelining. But we _aren't_ pipelining, |
| // so there is only one message in flight. So the message IDs should match. |
| if (update->update->id != wire->id) { |
| ERROR("Response doesn't have the expected id: %x != %x.", wire->id, update->update->id); |
| update_finished(update, dns_rcode_servfail); |
| } |
| |
| // Handle the case where the update succeeded. |
| switch(dns_rcode_get(wire)) { |
| case dns_rcode_noerror: |
| switch(update->state) { |
| case connect_to_server: // Can't get a response when connecting. |
| invalid: |
| ERROR("Invalid rcode \"%s\" for state %s", |
| dns_rcode_name(dns_rcode_get(wire)), update_state_name(update->state)); |
| update_finished(update, dns_rcode_servfail); |
| return; |
| |
| case create_nonexistent: |
| DM_NAME_GEN_SRP(update->host->name, freshly_added_name_buf); |
| INFO("SRP Update for host " PRI_DM_NAME_SRP " was freshly added.", |
| DM_NAME_PARAM_SRP(update->host->name, freshly_added_name_buf)); |
| update_finished(update, dns_rcode_noerror); |
| return; |
| |
| case refresh_existing: |
| DM_NAME_GEN_SRP(update->host->name, refreshed_name_buf); |
| INFO("SRP Update for host " PRI_DM_NAME_SRP " was refreshed.", |
| DM_NAME_PARAM_SRP(update->host->name, refreshed_name_buf)); |
| update_finished(update, dns_rcode_noerror); |
| return; |
| |
| case create_nonexistent_instance: |
| DM_NAME_GEN_SRP(update->instance->name, create_instance_buf); |
| INFO("Instance create for " PRI_DM_NAME_SRP " succeeded", |
| DM_NAME_PARAM_SRP(update->instance->name, create_instance_buf)); |
| // If we created a new instance, we need to remember it in case we have to undo it. |
| // To do that, we have to take it off the list. |
| for (pinstance = &update->instances; *pinstance != NULL; pinstance = &((*pinstance)->next)) { |
| if (*pinstance == update->instance) { |
| break; |
| } |
| } |
| *pinstance = update->instance->next; |
| // If there are no more instances to update, then do the host add. |
| if (*pinstance == NULL) { |
| // STATE CHANGE: create_nonexistent_instance -> create_nonexistent_host |
| update->state = create_nonexistent_host; |
| } else { |
| // Not done yet, do the next one. |
| update->instance = *pinstance; |
| } |
| break; |
| |
| case refresh_existing_instance: |
| DM_NAME_GEN_SRP(update->instance->name, refreshed_instance_buf); |
| INFO("Instance refresh for " PRI_S_SRP " succeeded", |
| DM_NAME_PARAM_SRP(update->instance->name, refreshed_instance_buf)); |
| |
| // Move on to the next instance to update. |
| update->instance = update->instance->next; |
| // If there are no more instances to update, then do the host add. |
| if (update->instance == NULL) { |
| // STATE CHANGE: refresh_existing_instance -> create_nonexistent_host |
| update->state = create_nonexistent_host; |
| } else { |
| // Not done yet, do the next one. |
| // STATE CHANGE: refresh_existing_instance -> create_nonexistent_instance |
| update->state = create_nonexistent_instance; |
| } |
| break; |
| |
| case create_nonexistent_host: |
| DM_NAME_GEN_SRP(update->instance->name, new_host_buf); |
| INFO("SRP Update for new host " PRI_S_SRP " was successful.", |
| DM_NAME_PARAM_SRP(update->instance->name, new_host_buf)); |
| update_finished(update, dns_rcode_noerror); |
| return; |
| |
| case refresh_existing_host: |
| DM_NAME_GEN_SRP(update->instance->name, existing_host_buf); |
| INFO("SRP Update for existing host " PRI_S_SRP " was successful.", |
| DM_NAME_PARAM_SRP(update->instance->name, existing_host_buf)); |
| update_finished(update, dns_rcode_noerror); |
| return; |
| |
| case delete_failed_instance: |
| DM_NAME_GEN_SRP(update->host->name, failed_instance_buf); |
| INFO("Instance deletes for host %s succeeded", |
| DM_NAME_PARAM_SRP(update->host->name, failed_instance_buf)); |
| update_finished(update, update->fail_rcode); |
| return; |
| } |
| break; |
| |
| // We will get NXRRSET if we were adding an existing host with the prerequisite that a KEY |
| // RR exist on the name with the specified value. Some other KEY RR may exist, or there may |
| // be no such RRSET; we can't tell from this response. |
| case dns_rcode_nxrrset: |
| switch(update->state) { |
| case connect_to_server: // Can't get a response while connecting. |
| case create_nonexistent: // Can't get nxdomain when creating. |
| case create_nonexistent_instance: // same |
| case create_nonexistent_host: // same |
| case delete_failed_instance: // There are no prerequisites for deleting failed instances, so |
| // in principle this should never fail. |
| goto invalid; |
| |
| case refresh_existing: |
| // If we get an NXDOMAIN when doing a refresh, it means either that there is a conflict, |
| // or that one of the instances we are refreshing doesn't exist. So now do the instances |
| // one at a time. |
| |
| // STATE CHANGE: refresh_existing -> create_nonexistent |
| update->state = create_nonexistent; |
| update->instance = update->instances; |
| break; |
| |
| case refresh_existing_instance: |
| // In this case, we tried to update an existing instance and found that the prerequisite |
| // didn't match. This means either that there is a conflict, or else that the instance |
| // expired and was deleted between the time that we attempted to create it and the time |
| // we attempted to update it. We could account for this with an create_nonexistent_instance_again |
| // state, but currently do not. |
| |
| // If we have added some instances, we need to delete them before we send the fail response. |
| if (update->added_instances != NULL) { |
| // STATE CHANGE: refresh_existing_instance -> delete_failed_instance |
| update->state = delete_failed_instance; |
| delete_added_instances: |
| update->instance = update->added_instances; |
| update->fail_rcode = dns_rcode_get(wire); |
| break; |
| } else { |
| update_finished(update, dns_rcode_get(wire)); |
| return; |
| } |
| |
| case refresh_existing_host: |
| // In this case, there is a conflicting host entry. This means that all the service |
| // instances that exist and are owned by the key we are using are bogus, whether we |
| // created them or they were already there. However, it is not our mission to remove |
| // pre-existing messes here, so we'll just delete the ones we added. |
| if (update->added_instances != NULL) { |
| // STATE CHANGE: refresh_existing_host -> delete_failed_instance |
| update->state = delete_failed_instance; |
| goto delete_added_instances; |
| } |
| update_finished(update, dns_rcode_get(wire)); |
| return; |
| } |
| break; |
| // We get YXDOMAIN if we specify a prerequisite that the name not exist, but it does exist. |
| case dns_rcode_yxdomain: |
| switch(update->state) { |
| case connect_to_server: // We can't get a response while connecting. |
| case refresh_existing: // If we are refreshing, our prerequisites are all looking for |
| case refresh_existing_instance: // a specific RR with a specific value, so we can never get |
| case refresh_existing_host: // YXDOMAIN. |
| case delete_failed_instance: // And if we are deleting failed instances, we should never get an error. |
| goto invalid; |
| |
| case create_nonexistent: |
| // If we get an NXDOMAIN when doing a refresh, it means either that there is a conflict, |
| // or that one of the instances we are refreshing doesn't exist. So now do the instances |
| // one at a time. |
| |
| // STATE CHANGE: create_nonexistent -> create_nonexistent_instance |
| update->state = create_nonexistent_instance; |
| update->instance = update->instances; |
| break; |
| |
| case create_nonexistent_instance: |
| // STATE CHANGE: create_nonexistent_instance -> refresh_existing_instance |
| update->state = refresh_existing_instance; |
| break; |
| |
| case create_nonexistent_host: |
| // STATE CHANGE: create_nonexistent_host -> refresh_existing_host |
| update->state = refresh_existing_host; |
| break; |
| } |
| break; |
| |
| case dns_rcode_notauth: |
| ERROR("DNS Authoritative server does not think we are authorized to update it, please fix."); |
| update_finished(update, dns_rcode_servfail); |
| return; |
| |
| // We may want to return different error codes or do more informative logging for some of these: |
| case dns_rcode_formerr: |
| case dns_rcode_servfail: |
| case dns_rcode_notimp: |
| case dns_rcode_refused: |
| case dns_rcode_yxrrset: |
| case dns_rcode_notzone: |
| case dns_rcode_dsotypeni: |
| default: |
| goto invalid; |
| } |
| |
| if (update->state != initial_state) { |
| INFO("Update state changed from " PUB_S_SRP " to " PUB_S_SRP, update_state_name(initial_state), |
| update_state_name(update->state)); |
| } |
| if (update->instance != initial_instance) { |
| DM_NAME_GEN_SRP(initial_instance->name, initial_name_buf); |
| DM_NAME_GEN_SRP(update->instance->name, updated_name_buf); |
| INFO("Update instance changed from " PRI_DM_NAME_SRP " to " PRI_DM_NAME_SRP, |
| DM_NAME_PARAM_SRP(initial_instance->name, initial_name_buf), |
| DM_NAME_PARAM_SRP(update->instance->name, updated_name_buf)); |
| } |
| if (construct_update(update)) { |
| update_send(update); |
| } else { |
| ERROR("Failed to construct update"); |
| update_finished(update, dns_rcode_servfail); |
| } |
| return; |
| } |
| |
| bool |
| srp_update_start(comm_t *connection, dns_message_t *parsed_message, dns_host_description_t *host, |
| service_instance_t *instance, service_t *service, dns_name_t *update_zone, |
| uint32_t lease_time, uint32_t key_lease_time) |
| { |
| update_t *update; |
| |
| // Allocate the data structure |
| update = calloc(1, sizeof *update); |
| if (update == NULL) { |
| ERROR("start_dns_update: unable to allocate update structure!"); |
| return false; |
| } |
| // Allocate the buffer in which updates will be constructed. |
| update->update = calloc(1, DNS_MAX_UDP_PAYLOAD); |
| if (update->update == NULL) { |
| ERROR("start_dns_update: unable to allocate update message buffer."); |
| return false; |
| } |
| update->update_max = DNS_DATA_SIZE; |
| |
| // Retain the stuff we're supposed to send. |
| update->host = host; |
| update->instances = instance; |
| update->services = service; |
| update->parsed_message = parsed_message; |
| update->message = connection->message; |
| update->state = connect_to_server; |
| update->zone_name = update_zone; |
| update->client = connection; |
| |
| // Start the connection to the server |
| update->server = ioloop_connect(&dns_server, false, true, update_reply_callback, |
| update_connect_callback, update_disconnect_callback, update_finalize, update); |
| if (update->server == NULL) { |
| free(update); |
| return false; |
| } |
| INFO("Connecting to auth server."); |
| return true; |
| } |
| |
| static bool |
| key_handler(void *context, const char *filename, char **hunks, int num_hunks, int lineno) |
| { |
| hmac_key_t *key = context; |
| long val; |
| char *endptr; |
| size_t len; |
| uint8_t keybuf[SRP_SHA256_DIGEST_SIZE]; |
| int error; |
| |
| // Validate the constant-size stuff first. |
| if (strcasecmp(hunks[1], "in")) { |
| ERROR("Expecting tsig key class IN, got %s.", hunks[1]); |
| return false; |
| } |
| |
| if (strcasecmp(hunks[2], "key")) { |
| ERROR("expecting tsig key type KEY, got %s", hunks[2]); |
| return false; |
| } |
| |
| // There's not much meaning to be extracted from the flags. |
| val = strtol(hunks[3], &endptr, 10); |
| if (*endptr != 0 || endptr == hunks[3]) { |
| ERROR("Invalid key flags: %s", hunks[3]); |
| return false; |
| } |
| |
| // The protocol number as produced by BIND will always be 3, meaning DNSSEC, but of |
| // course we aren't using this key for DNSSEC, so it's not clear that we should take |
| // this seriously; hence we just check to see that it's a number. |
| val = strtol(hunks[4], &endptr, 10); |
| if (*endptr != 0 || endptr == hunks[4]) { |
| ERROR("Invalid protocol number: %s", hunks[4]); |
| return false; |
| } |
| |
| // The key algorithm should be HMAC-SHA253. BIND uses 163, but this is not registered |
| // with IANA. So again, we don't actually require this, but we do validate it so that |
| // if someone generated the wrong key type, they'll get a message. |
| val = strtol(hunks[5], &endptr, 10); |
| if (*endptr != 0 || endptr == hunks[5]) { |
| ERROR("Invalid protocol number: %s", hunks[5]); |
| return false; |
| } |
| if (val != 163) { |
| INFO("Warning: Protocol number for HMAC-SHA256 TSIG KEY is not 163, but %ld", val); |
| } |
| |
| key->name = dns_pres_name_parse(hunks[0]); |
| if (key->name == NULL) { |
| ERROR("Invalid key name: %s", hunks[0]); |
| return false; |
| } |
| |
| error = srp_base64_parse(hunks[6], &len, keybuf, sizeof keybuf); |
| if (error != 0) { |
| ERROR("Invalid HMAC-SHA256 key: %s", strerror(errno)); |
| goto fail; |
| } |
| |
| // The key should be 32 bytes (256 bits). |
| if (len == 0) { |
| ERROR("Invalid (null) secret for key %s", hunks[0]); |
| goto fail; |
| } |
| key->secret = malloc(len); |
| if (key->secret == NULL) { |
| ERROR("Unable to allocate space for secret for key %s", hunks[0]); |
| fail: |
| dns_name_free(key->name); |
| key->name = NULL; |
| return false; |
| } |
| memcpy(key->secret, keybuf, len); |
| key->length = len; |
| key->algorithm = SRP_HMAC_TYPE_SHA256; |
| return true; |
| } |
| |
| config_file_verb_t key_verbs[] = { |
| { NULL, 7, 7, key_handler } |
| }; |
| #define NUMKEYVERBS ((sizeof key_verbs) / sizeof (config_file_verb_t)) |
| |
| hmac_key_t * |
| parse_hmac_key_file(const char *filename) |
| { |
| hmac_key_t *key = calloc(1, sizeof *key); |
| if (key == NULL) { |
| ERROR("No memory for tsig key structure."); |
| return NULL; |
| } |
| if (!config_parse(key, filename, key_verbs, NUMKEYVERBS)) { |
| ERROR("Failed to parse key file."); |
| free(key); |
| return NULL; |
| } |
| return key; |
| } |
| |
| int |
| main(int argc, char **argv) |
| { |
| int i; |
| subnet_t *tcp_validators = NULL; |
| udp_validator_t *udp_validators = NULL; |
| udp_validator_t *NULLABLE *NONNULL up = &udp_validators; |
| subnet_t *NULLABLE *NONNULL nt = &tcp_validators; |
| subnet_t *NULLABLE *NONNULL sp; |
| addr_t pref; |
| uint16_t port; |
| socklen_t len, prefalen; |
| char *s, *p; |
| int width; |
| bool got_server = false; |
| |
| // Read the configuration from the command line. |
| for (i = 1; i < argc; i++) { |
| if (!strcmp(argv[i], "-s")) { |
| if (got_server) { |
| ERROR("only one authoritative server can be specified."); |
| return usage(argv[0]); |
| } |
| if (++i == argc) { |
| ERROR("-s is missing dns server IP address."); |
| return usage(argv[0]); |
| } |
| len = getipaddr(&dns_server, argv[i]); |
| if (!len) { |
| ERROR("Invalid IP address: %s.", argv[i]); |
| return usage(argv[0]); |
| } |
| if (++i == argc) { |
| ERROR("-s is missing dns server port."); |
| return usage(argv[0]); |
| } |
| port = strtol(argv[i], &s, 10); |
| if (s == argv[i] || s[0] != '\0') { |
| ERROR("Invalid port number: %s", argv[i]); |
| return usage(argv[0]); |
| } |
| if (dns_server.sa.sa_family == AF_INET) { |
| dns_server.sin.sin_port = htons(port); |
| } else { |
| dns_server.sin6.sin6_port = htons(port); |
| } |
| got_server = true; |
| } else if (!strcmp(argv[i], "-k")) { |
| if (++i == argc) { |
| ERROR("-k is missing key file name."); |
| return usage(argv[0]); |
| } |
| key = parse_hmac_key_file(argv[i]); |
| // Someething should already have printed the error message. |
| if (key == NULL) { |
| return 1; |
| } |
| } else if (!strcmp(argv[i], "-t") || !strcmp(argv[i], "-u")) { |
| if (!strcmp(argv[i], "-u")) { |
| if (++i == argc) { |
| ERROR("-u is missing interface name."); |
| return usage(argv[0]); |
| } |
| *up = calloc(1, sizeof **up); |
| if (*up == NULL) { |
| ERROR("udp_validators: out of memory."); |
| return usage(argv[0]); |
| } |
| (*up)->ifname = strdup(argv[i]); |
| if ((*up)->ifname == NULL) { |
| ERROR("udp validators: ifname: out of memory."); |
| return usage(argv[0]); |
| } |
| sp = &((*up)->subnets); |
| } else { |
| sp = nt; |
| } |
| |
| if (++i == argc) { |
| ERROR("%s requires at least one prefix.", argv[i - 1]); |
| return usage(argv[0]); |
| } |
| s = strchr(argv[i], '/'); |
| if (s == NULL) { |
| ERROR("%s is not a prefix.", argv[i]); |
| return usage(argv[0]); |
| } |
| *s = 0; |
| ++s; |
| prefalen = getipaddr(&pref, argv[i]); |
| if (!prefalen) { |
| ERROR("%s is not a valid prefix address.", argv[i]); |
| return usage(argv[0]); |
| } |
| width = strtol(s, &p, 10); |
| if (s == p || p[0] != '\0') { |
| ERROR("%s (prefix width) is not a number.", p); |
| return usage(argv[0]); |
| } |
| if (width < 0 || |
| (pref.sa.sa_family == AF_INET && width > 32) || |
| (pref.sa.sa_family == AF_INET6 && width > 64)) { |
| ERROR("%s is not a valid prefix length for %s", p, |
| pref.sa.sa_family == AF_INET ? "IPv4" : "IPv6"); |
| return usage(argv[0]); |
| } |
| |
| *nt = calloc(1, sizeof **nt); |
| if (!*nt) { |
| ERROR("tcp_validators: out of memory."); |
| return 1; |
| } |
| |
| (*nt)->preflen = width; |
| (*nt)->family = pref.sa.sa_family; |
| if (pref.sa.sa_family == AF_INET) { |
| memcpy((*nt)->bytes, &pref.sin.sin_addr, 4); |
| } else { |
| memcpy((*nt)->bytes, &pref.sin6.sin6_addr, 8); |
| } |
| |
| // *up will be non-null for -u and null for -t. |
| if (*up) { |
| up = &((*up)->next); |
| } else { |
| nt = sp; |
| } |
| } |
| } |
| if (!got_server) { |
| ERROR("No authoritative DNS server specified to take updates!"); |
| return 1; |
| } |
| |
| if (!ioloop_init()) { |
| return 1; |
| } |
| |
| if (!srp_proxy_listen("home.arpa")) { |
| return 1; |
| } |
| |
| // For now, hardcoded, should be configurable |
| service_update_zone = dns_pres_name_parse("home.arpa"); |
| |
| do { |
| int something = 0; |
| something = ioloop_events(0); |
| INFO("dispatched %d events.", something); |
| } while (1); |
| } |
| |
| // Local Variables: |
| // mode: C |
| // tab-width: 4 |
| // c-file-style: "bsd" |
| // c-basic-offset: 4 |
| // fill-column: 108 |
| // indent-tabs-mode: nil |
| // End: |