| /* dnssd-client.c |
| * |
| * Copyright (c) 2023 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 file contains code to queue and send updates for Thread services. |
| */ |
| |
| #ifndef LINUX |
| #include <netinet/in.h> |
| #include <net/if.h> |
| #include <netinet6/in6_var.h> |
| #include <netinet6/nd6.h> |
| #include <net/if_media.h> |
| #include <sys/stat.h> |
| #else |
| #define _GNU_SOURCE |
| #include <netinet/in.h> |
| #include <fcntl.h> |
| #include <bsd/stdlib.h> |
| #include <net/if.h> |
| #endif |
| #include <sys/socket.h> |
| #include <sys/ioctl.h> |
| #include <net/route.h> |
| #include <netinet/icmp6.h> |
| #include <stdio.h> |
| #include <unistd.h> |
| #include <errno.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <ctype.h> |
| #include <arpa/inet.h> |
| #include <stdlib.h> |
| #include <stddef.h> |
| #include <dns_sd.h> |
| #include <inttypes.h> |
| #include <signal.h> |
| |
| #ifdef IOLOOP_MACOS |
| #include <xpc/xpc.h> |
| |
| #include <TargetConditionals.h> |
| #include <SystemConfiguration/SystemConfiguration.h> |
| #include <SystemConfiguration/SCPrivate.h> |
| #include <SystemConfiguration/SCNetworkConfigurationPrivate.h> |
| #include <SystemConfiguration/SCNetworkSignature.h> |
| #include <network_information.h> |
| |
| #include <CoreUtils/CoreUtils.h> |
| #endif // IOLOOP_MACOS |
| |
| #include "srp.h" |
| #include "dns-msg.h" |
| #include "ioloop.h" |
| #include "adv-ctl-server.h" |
| #include "srp-crypto.h" |
| |
| #include "cti-services.h" |
| #include "srp-gw.h" |
| #include "srp-proxy.h" |
| #include "srp-mdns-proxy.h" |
| #include "dnssd-proxy.h" |
| #include "srp-proxy.h" |
| #include "route.h" |
| |
| #define STATE_MACHINE_IMPLEMENTATION 1 |
| typedef enum { |
| dnssd_client_state_invalid, |
| dnssd_client_state_startup, |
| dnssd_client_state_not_client, |
| dnssd_client_state_client, |
| } state_machine_state_t; |
| #define state_machine_state_invalid dnssd_client_state_invalid |
| |
| #include "state-machine.h" |
| #include "thread-service.h" |
| #include "service-tracker.h" |
| #include "service-publisher.h" |
| |
| #include "dnssd-client.h" |
| #include "thread-tracker.h" |
| #include "probe-srp.h" |
| |
| struct dnssd_client { |
| int ref_count; |
| state_machine_header_t state_header; |
| char *id; |
| srp_server_t *server_state; |
| cti_connection_t ml_prefix_connection; |
| struct in6_addr mesh_local_prefix; |
| bool have_mesh_local_prefix; |
| bool first_time; |
| bool canceled; |
| DNSRecordRef aaaa_record_ref, ns_record_ref; |
| dnssd_txn_t *shared_txn; |
| thread_service_t *published_service; |
| DNSServiceRef shared_connection; |
| int interface_index; |
| }; |
| |
| static uint64_t dnssd_client_serial_number; |
| |
| static void |
| dnssd_client_finalize(dnssd_client_t *client) |
| { |
| thread_service_release(client->published_service); |
| |
| free(client->id); |
| free(client); |
| } |
| |
| RELEASE_RETAIN_FUNCS(dnssd_client); |
| |
| static void |
| dnssd_client_context_release(void *context) |
| { |
| dnssd_client_t *client = context; |
| RELEASE_HERE(client, dnssd_client); |
| } |
| |
| static void |
| dnssd_client_service_tracker_callback(void *context) |
| { |
| dnssd_client_t *client = context; |
| |
| state_machine_event_t *event = state_machine_event_create(state_machine_event_type_service_list_changed, NULL); |
| if (event == NULL) { |
| ERROR("unable to allocate event to deliver"); |
| return; |
| } |
| state_machine_event_deliver(&client->state_header, event); |
| RELEASE_HERE(event, state_machine_event); |
| } |
| |
| static void |
| dnssd_client_probe_callback(thread_service_t *UNUSED service, void *context, bool UNUSED succeeded) |
| { |
| dnssd_client_t *client = context; |
| state_machine_event_t *event = state_machine_event_create(state_machine_event_type_probe_completed, NULL); |
| if (event == NULL) { |
| ERROR("unable to allocate event to deliver"); |
| return; |
| } |
| state_machine_event_deliver(&client->state_header, event); |
| RELEASE_HERE(event, state_machine_event); |
| } |
| |
| static void |
| dnssd_client_get_mesh_local_prefix_callback(void *context, const char *prefix_string, int status) |
| { |
| dnssd_client_t *client = context; |
| INFO(PUB_S_SRP " %d", prefix_string != NULL ? prefix_string : "<null>", status); |
| if (status != kCTIStatus_NoError || prefix_string == NULL) { |
| goto fail; |
| } |
| |
| char prefix_buf[INET6_ADDRSTRLEN]; |
| |
| const char *prefix_addr_string; |
| char *slash = strchr(prefix_string, '/'); |
| if (slash != NULL) { |
| size_t len = slash - prefix_string; |
| if (len == 0) { |
| ERROR("bogus prefix: " PRI_S_SRP, prefix_string); |
| goto fail; |
| } |
| if (len - 1 > sizeof(prefix_buf)) { |
| ERROR("prefix too long: " PRI_S_SRP, prefix_string); |
| goto fail; |
| } |
| memcpy(prefix_buf, prefix_string, len); |
| prefix_buf[len] = 0; |
| prefix_addr_string = prefix_buf; |
| } else { |
| prefix_addr_string = prefix_string; |
| } |
| if (!inet_pton(AF_INET6, prefix_addr_string, &client->mesh_local_prefix)) { |
| ERROR("prefix syntax incorrect: " PRI_S_SRP, prefix_addr_string); |
| goto fail; |
| } |
| client->have_mesh_local_prefix = true; |
| state_machine_event_t *event = state_machine_event_create(state_machine_event_type_got_mesh_local_prefix, NULL); |
| if (event == NULL) { |
| ERROR("unable to allocate event to deliver"); |
| return; |
| } |
| state_machine_event_deliver(&client->state_header, event); |
| RELEASE_HERE(event, state_machine_event); |
| fail: |
| return; |
| } |
| |
| static void |
| dnssd_client_remove_published_service(dnssd_client_t *client) |
| { |
| if (client->shared_txn != NULL) { |
| ioloop_dnssd_txn_cancel(client->shared_txn); |
| ioloop_dnssd_txn_release(client->shared_txn); |
| client->shared_txn = NULL; |
| } |
| client->aaaa_record_ref = NULL; |
| client->ns_record_ref = NULL; |
| if (client->published_service != NULL) { |
| thread_service_release(client->published_service); |
| client->published_service = NULL; |
| } |
| } |
| |
| static void |
| dnssd_client_publish_failed(dnssd_client_t *client) |
| { |
| dnssd_client_remove_published_service(client); |
| state_machine_event_t *event = state_machine_event_create(state_machine_event_type_daemon_disconnect, NULL); |
| if (event == NULL) { |
| ERROR("unable to allocate event to deliver"); |
| return; |
| } |
| state_machine_event_deliver(&client->state_header, event); |
| RELEASE_HERE(event, state_machine_event); |
| } |
| |
| static void |
| dnssd_client_shared_connection_failed_callback(void *context, int UNUSED status) |
| { |
| dnssd_client_t *client = context; |
| ERROR("daemon connection failed"); |
| dnssd_client_publish_failed(client); |
| } |
| |
| static state_machine_state_t |
| dnssd_client_service_unpublish(dnssd_client_t *client) |
| { |
| if (client->aaaa_record_ref != NULL) { |
| thread_service_note(client->id, client->published_service, "unpublishing service"); |
| } |
| dnssd_client_remove_published_service(client); |
| |
| return dnssd_client_state_not_client; |
| } |
| |
| static void dnssd_client_record_callback(DNSServiceRef UNUSED sdref, DNSRecordRef UNUSED rref, |
| DNSServiceFlags UNUSED flags, DNSServiceErrorType error_code, void *context) |
| { |
| dnssd_client_t *client = context; |
| |
| if (error_code != kDNSServiceErr_NoError) { |
| if (rref == client->aaaa_record_ref) { |
| ERROR("unable to register AAAA record: error code %d", error_code); |
| } else { |
| ERROR("unable to register NS record: error code %d", error_code); |
| } |
| dnssd_client_publish_failed(client); |
| return; |
| } |
| if (rref == client->aaaa_record_ref) { |
| ERROR("successfully registered AAAA record"); |
| } else { |
| ERROR("successfully registered NS record"); |
| } |
| } |
| |
| static state_machine_state_t |
| dnssd_client_service_publish(dnssd_client_t *client) |
| { |
| int err; |
| dns_towire_state_t towire; |
| dns_wire_t message; |
| |
| thread_service_note(client->id, client->published_service, "publishing service"); |
| |
| // Set up advertisement for the DNSSD server: |
| |
| // First we need a shared connection for DNSServiceRegisterRecord. |
| err = DNSServiceCreateConnection(&client->shared_connection); |
| if (err != kDNSServiceErr_NoError) { |
| ERROR("DNSServiceCreateConnection failed"); |
| return dnssd_client_service_unpublish(client); |
| } |
| client->shared_txn = ioloop_dnssd_txn_add(client->shared_connection, client, dnssd_client_context_release, |
| dnssd_client_shared_connection_failed_callback); |
| if (client->shared_txn == NULL) { |
| return dnssd_client_service_unpublish(client); |
| } |
| |
| // Set up AAAA record for dnssd-server.local |
| uint8_t *aaaa_rdata; |
| if (client->published_service->service_type == unicast_service) { |
| aaaa_rdata = &client->published_service->u.unicast.address.s6_addr[0]; |
| } else if (client->published_service->service_type == anycast_service) { |
| aaaa_rdata = &client->published_service->u.anycast.address.s6_addr[0]; |
| } else { |
| ERROR("invalid service type for published service: %d", client->published_service->service_type); |
| return dnssd_client_service_unpublish(client); |
| } |
| err = DNSServiceRegisterRecord(client->shared_connection, &client->aaaa_record_ref, |
| kDNSServiceFlagsKnownUnique, client->interface_index, |
| "dnssd-server.local", kDNSServiceType_AAAA, kDNSServiceClass_IN, 16, aaaa_rdata, 0, |
| dnssd_client_record_callback, client); |
| if (err != kDNSServiceErr_NoError) { |
| ERROR("DNSServiceRegisterRecord failed - record: dnssd-server.local AAAA "); |
| return dnssd_client_service_unpublish(client); |
| } |
| |
| // Set up default.service.arpa NS dnssd-server.local |
| memset(&towire, 0, sizeof(towire)); |
| towire.message = &message; |
| towire.lim = &message.data[DNS_DATA_SIZE]; |
| towire.p = message.data; |
| |
| dns_full_name_to_wire(NULL, &towire, "dnssd-server.local."); |
| err = DNSServiceRegisterRecord(client->shared_connection, &client->ns_record_ref, |
| kDNSServiceFlagsKnownUnique | kDNSServiceFlagsForceMulticast, |
| client->interface_index, |
| "openthread.thread.home.arpa", kDNSServiceType_NS, kDNSServiceClass_IN, |
| towire.p - message.data, message.data, 0, dnssd_client_record_callback, client); |
| if (err != kDNSServiceErr_NoError) { |
| ERROR("DNSServiceRegisterRecord failed - record: " "openthread.thread.home.arpa" " NS " "dnssd-server.local"); |
| return dnssd_client_service_unpublish(client); |
| } |
| |
| #if PUBLISH_LEGACY_BROWSING_DOMAIN_FOR_THREAD_DEVICE |
| |
| memset(&towire, 0, sizeof(towire)); |
| towire.message = &message; |
| towire.lim = &message.data[DNS_DATA_SIZE]; |
| towire.p = message.data; |
| |
| dns_full_name_to_wire(NULL, &towire, "thread.service.arpa."); |
| err = DNSServiceRegisterRecord(client->service_ref, &client->ptr_record_ref, |
| kDNSServiceFlagsKnownUnique, server_state->advertise_interface, AUTOMATIC_BROWSING_DOMAIN, |
| kDNSServiceType_PTR, kDNSServiceClass_IN, towire.p - message.data, message.data, 0, |
| dnssd_client_record_callback, client); |
| if (err != kDNSServiceErr_NoError) { |
| ERROR("DNSServiceRegisterRecord failed - record: " AUTOMATIC_BROWSING_DOMAIN " PTR " "openthread.thread.home.arpa"); |
| return dnssd_client_service_unpublish(client); |
| } |
| #endif |
| |
| return dnssd_client_state_invalid; |
| } |
| |
| static state_machine_state_t dnssd_client_action_startup(state_machine_header_t *state_header, |
| state_machine_event_t *event); |
| static state_machine_state_t dnssd_client_action_not_client(state_machine_header_t *state_header, |
| state_machine_event_t *event); |
| static state_machine_state_t dnssd_client_action_client(state_machine_header_t *state_header, |
| state_machine_event_t *event); |
| |
| // States: |
| // -- not_client: Not a client because server or because no service (in which case hopefully we are becoming server) |
| // -- client: Found a working service, configured as client |
| |
| #define SERVICE_PUB_NAME_DECL(name) dnssd_client_state_##name, #name |
| static state_machine_decl_t dnssd_client_states[] = { |
| { SERVICE_PUB_NAME_DECL(invalid), NULL }, |
| { SERVICE_PUB_NAME_DECL(startup), dnssd_client_action_startup }, |
| { SERVICE_PUB_NAME_DECL(not_client), dnssd_client_action_not_client }, |
| { SERVICE_PUB_NAME_DECL(client), dnssd_client_action_client }, |
| }; |
| #define DNSSD_CLIENT_NUM_STATES ((sizeof(dnssd_client_states)) / (sizeof(state_machine_decl_t))) |
| |
| #define STATE_MACHINE_HEADER_TO_CLIENT(state_header) \ |
| if (state_header->state_machine_type != state_machine_type_dnssd_client) { \ |
| ERROR("state header type isn't omr_client: %d", state_header->state_machine_type); \ |
| return dnssd_client_state_invalid; \ |
| } \ |
| dnssd_client_t *client = state_header->state_object |
| |
| static bool |
| dnssd_client_should_be_client(dnssd_client_t *client) |
| { |
| bool might_publish = false; |
| bool associated = true; |
| bool should_be_client = false; |
| srp_server_t *server_state = client->server_state; |
| |
| if (!service_publisher_could_publish(server_state->service_publisher)) { |
| should_be_client = true; |
| might_publish = true; |
| } |
| |
| if (!thread_tracker_associated_get(server_state->thread_tracker, false)) { |
| associated = false; |
| should_be_client = false; |
| } |
| |
| INFO(PUB_S_SRP PUB_S_SRP PUB_S_SRP, |
| should_be_client ? "could be client" : "can't be client", |
| might_publish ? " might publish" : " won't publish", |
| associated ? "" : " not associated "); |
| return should_be_client; |
| } |
| |
| // We get into this state when there is no SRP service published on the Thread network other than our own. |
| // If we stop publishing (because a BR-based service showed up), then we go to the probe state. |
| static state_machine_state_t |
| dnssd_client_action_startup(state_machine_header_t *state_header, state_machine_event_t *event) |
| { |
| STATE_MACHINE_HEADER_TO_CLIENT(state_header); |
| BR_STATE_ANNOUNCE(client, event); |
| |
| if (client->have_mesh_local_prefix) { |
| return dnssd_client_state_not_client; |
| } |
| return dnssd_client_state_invalid; |
| } |
| |
| // We get into this state when there is no SRP service published on the Thread network other than our own. |
| // If we stop publishing (because a BR-based service showed up), then we go to the probe state. |
| static state_machine_state_t |
| dnssd_client_action_not_client(state_machine_header_t *state_header, state_machine_event_t *event) |
| { |
| STATE_MACHINE_HEADER_TO_CLIENT(state_header); |
| BR_STATE_ANNOUNCE(client, event); |
| |
| if (event == NULL) { |
| if (client->published_service != NULL) { |
| dnssd_client_service_unpublish(client); |
| } |
| } |
| |
| // We do the same thing here for any event we get, because we're just waiting for conditions to be right. |
| if (dnssd_client_should_be_client(client)) { |
| thread_service_t *service; |
| |
| // See if we now have a service that's been successfully probed; if so, publish it. |
| service = service_tracker_verified_service_get(client->server_state->service_tracker); |
| if (service != NULL) { |
| if (client->published_service != NULL) { |
| thread_service_release(client->published_service); |
| } |
| client->published_service = service; |
| thread_service_retain(client->published_service); |
| return dnssd_client_state_client; |
| } |
| |
| // Check to see if we can start a new probe |
| service = service_tracker_unverified_service_get(client->server_state->service_tracker); |
| if (service != NULL) { |
| if (service->service_type == anycast_service) { |
| memcpy(&service->u.anycast.address, &client->mesh_local_prefix, 8); |
| memcpy(&service->u.anycast.address.s6_addr[8], thread_rloc_preamble, 6); |
| service->u.anycast.address.s6_addr[14] = service->rloc16 >> 8; |
| service->u.anycast.address.s6_addr[15] = service->rloc16 & 255; |
| } |
| probe_srp_service(service, client, dnssd_client_probe_callback); |
| RETAIN_HERE(client, dnssd_client); // For the callback |
| return dnssd_client_state_invalid; |
| } |
| |
| // This should only ever happen if we get the service list update before the service publisher gets it. |
| INFO("no service to publish"); |
| return dnssd_client_state_invalid; |
| } |
| return dnssd_client_state_invalid; |
| } |
| |
| // We get into this state when we've published a service. |
| static state_machine_state_t |
| dnssd_client_action_client(state_machine_header_t *state_header, state_machine_event_t *event) |
| { |
| STATE_MACHINE_HEADER_TO_CLIENT(state_header); |
| BR_STATE_ANNOUNCE(client, event); |
| |
| if (event == NULL) { |
| // Publish the service. |
| dnssd_client_service_publish(client); |
| return dnssd_client_state_invalid; |
| } |
| |
| // This can happen if the daemon disconnects. |
| if (client->published_service == NULL) { |
| return dnssd_client_state_not_client; |
| } |
| |
| // If we should no longer be client, unpublish the service and wait |
| if (!dnssd_client_should_be_client(client)) { |
| return dnssd_client_state_not_client; |
| } |
| |
| if (service_tracker_verified_service_still_exists(client->server_state->service_tracker, |
| client->published_service)) { |
| return dnssd_client_state_invalid; |
| } |
| |
| // If we get here, the service we have been publishing is no longer present. |
| return dnssd_client_state_not_client; |
| } |
| |
| void |
| dnssd_client_cancel(dnssd_client_t *client) |
| { |
| service_tracker_callback_cancel(client->server_state->service_tracker, client); |
| thread_tracker_callback_cancel(client->server_state->thread_tracker, client); |
| cti_events_discontinue(client->ml_prefix_connection); |
| client->ml_prefix_connection = NULL; |
| } |
| |
| dnssd_client_t * |
| dnssd_client_create(srp_server_t *server_state) |
| { |
| dnssd_client_t *ret = NULL, *client = calloc(1, sizeof(*client)); |
| if (client == NULL) { |
| return client; |
| } |
| RETAIN_HERE(client, dnssd_client); |
| |
| char client_id_buf[100]; |
| snprintf(client_id_buf, sizeof(client_id_buf), "[SP%lld]", ++dnssd_client_serial_number); |
| client->id = strdup(client_id_buf); |
| if (client->id == NULL) { |
| ERROR("no memory for client ID"); |
| goto out; |
| } |
| client->interface_index = -1; |
| |
| if (!state_machine_header_setup(&client->state_header, |
| client, client->id, |
| state_machine_type_dnssd_client, |
| dnssd_client_states, |
| DNSSD_CLIENT_NUM_STATES)) { |
| ERROR("header setup failed"); |
| goto out; |
| } |
| |
| client->server_state = server_state; |
| if (!service_tracker_callback_add(server_state->service_tracker, dnssd_client_service_tracker_callback, |
| dnssd_client_context_release, client)) |
| { |
| goto out; |
| } |
| RETAIN_HERE(client, dnssd_client); // for service tracker |
| |
| ret = client; |
| client = NULL; |
| out: |
| if (client != NULL) { |
| RELEASE_HERE(client, dnssd_client); |
| } |
| return ret; |
| } |
| |
| static void |
| dnssd_client_get_tunnel_name_callback(void *context, const char *name, cti_status_t status) |
| { |
| dnssd_client_t *client = context; |
| if (status != kCTIStatus_NoError) { |
| INFO("didn't get tunnel name, error code %d", status); |
| return; |
| } |
| client->interface_index = if_nametoindex(name); |
| } |
| |
| void |
| dnssd_client_start(dnssd_client_t *client) |
| { |
| cti_get_mesh_local_prefix(client->server_state, &client->ml_prefix_connection, client, |
| dnssd_client_get_mesh_local_prefix_callback, NULL); |
| cti_get_tunnel_name(client->server_state, client, dnssd_client_get_tunnel_name_callback, NULL); |
| RETAIN_HERE(client, dnssd_client); // for callback |
| state_machine_next_state(&client->state_header, dnssd_client_state_startup); |
| } |
| |
| // Local Variables: |
| // mode: C |
| // tab-width: 4 |
| // c-file-style: "bsd" |
| // c-basic-offset: 4 |
| // fill-column: 120 |
| // indent-tabs-mode: nil |
| // End: |