blob: f05b2bb5feb4d8f1465ac6ef85797ff2be70ef2b [file] [log] [blame] [edit]
/* service-tracker.c
*
* Copyright (c) 2023-2024 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.
*
* Track services on the Thread mesh.
*/
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <pwd.h>
#include <errno.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <time.h>
#include <dns_sd.h>
#include <net/if.h>
#include <inttypes.h>
#include <sys/resource.h>
#include <netinet/icmp6.h>
#include "srp.h"
#include "dns-msg.h"
#include "srp-crypto.h"
#include "ioloop.h"
#include "srp-gw.h"
#include "srp-proxy.h"
#include "srp-mdns-proxy.h"
#include "dnssd-proxy.h"
#include "config-parse.h"
#include "cti-services.h"
#include "thread-device.h"
#include "state-machine.h"
#include "thread-service.h"
#include "service-tracker.h"
#include "probe-srp.h"
#include "adv-ctl-server.h"
struct service_tracker_callback {
service_tracker_callback_t *next;
void (*context_release)(void *NONNULL context);
void (*callback)(void *context);
void *context;
};
struct service_tracker {
int ref_count;
uint64_t id;
route_state_t *route_state;
srp_server_t *server_state;
cti_connection_t NULLABLE thread_service_context;
service_tracker_callback_t *callbacks;
thread_service_t *NULLABLE thread_services;
uint16_t rloc16;
bool user_service_seen;
};
static uint64_t service_tracker_serial_number = 0;
static void
service_tracker_finalize(service_tracker_t *tracker)
{
thread_service_t *next;
for (thread_service_t *service = tracker->thread_services; service != NULL; service = next) {
next = service->next;
thread_service_release(service);
}
free(tracker);
}
static void
service_tracker_context_release(void *context)
{
service_tracker_t *tracker = context;
if (tracker != NULL) {
RELEASE_HERE(tracker, service_tracker);
}
}
RELEASE_RETAIN_FUNCS(service_tracker);
void
service_tracker_thread_service_note(service_tracker_t *tracker, thread_service_t *tservice, const char *event_description)
{
char owner_id[20];
snprintf(owner_id, sizeof(owner_id), "[ST%lld]", tracker->id);
thread_service_note(owner_id, tservice, event_description);
}
typedef struct state_debug_accumulator {
char change[20]; // " +stable +user +ncp"
char *p_change;
size_t left;
bool changed;
} accumulator_t;
static void
service_tracker_flags_accumulator_init(accumulator_t *accumulator)
{
memset(accumulator, 0, sizeof(*accumulator));
accumulator->p_change = accumulator->change;
accumulator->left = sizeof(accumulator->change);
}
static void
service_tracker_flags_accumulate(accumulator_t *accumulator, bool previous, bool cur, const char *name)
{
size_t len;
if (previous != cur) {
snprintf(accumulator->p_change, accumulator->left, "%s%s%s",
accumulator->p_change == accumulator->change ? "" : " ", cur ? "+" : "-", name);
len = strlen(accumulator->p_change);
accumulator->p_change += len;
accumulator->left -= len;
accumulator->changed = true;
}
}
static void
service_tracker_callback(void *context, cti_service_vec_t *services, cti_status_t status)
{
service_tracker_t *tracker = context;
size_t i;
thread_service_t **pservice = &tracker->thread_services, *service = NULL;
tracker->user_service_seen = false;
if (status == kCTIStatus_Disconnected || status == kCTIStatus_DaemonNotRunning) {
INFO("[ST%lld] disconnected", tracker->id);
cti_events_discontinue(tracker->thread_service_context);
tracker->thread_service_context = NULL;
RELEASE_HERE(tracker, service_tracker); // Not expecting any more callbacks.
return;
}
if (status != kCTIStatus_NoError) {
ERROR("[ST%lld] %d", tracker->id, status);
} else {
// Delete any SRP services that are not in the list provided by Thread.
while (*pservice != NULL) {
service = *pservice;
if (service->service_type == unicast_service) {
struct thread_unicast_service *uservice = &service->u.unicast;
for (i = 0; i < services->num; i++) {
cti_service_t *cti_service = services->services[i];
// Is this a valid SRP service?
if (IS_SRP_SERVICE(cti_service)) {
// Is this service still present?
if (!memcmp(&uservice->address, cti_service->server, 16) &&
!memcmp(&uservice->port, &cti_service->server[16], 2)) {
break;
}
}
}
} else if (service->service_type == anycast_service) {
struct thread_anycast_service *aservice = &service->u.anycast;
for (i = 0; i < services->num; i++) {
cti_service_t *cti_service = services->services[i];
// Is this a valid SRP anycast service?
if (IS_SRP_ANYCAST_SERVICE(cti_service)) {
// Is this service still present?
if (service->rloc16 == cti_service->rloc16 &&
aservice->sequence_number == cti_service->service[1]) {
break;
}
}
}
} else if (service->service_type == pref_id) {
struct thread_pref_id *pref_id = &service->u.pref_id;
for (i = 0; i < services->num; i++) {
cti_service_t *cti_service = services->services[i];
// Is this an SRP service?
if (IS_PREF_ID_SERVICE(cti_service)) {
// Is this service still present?
if (!memcmp(&pref_id->partition_id, cti_service->server, 4) &&
!memcmp(pref_id->prefix, &cti_service->server[4], 5))
{
break;
}
}
}
} else {
i = services->num;
}
if (i == services->num) {
service_tracker_thread_service_note(tracker, service, "went away");
*pservice = service->next;
thread_service_release(service);
service = NULL;
} else {
// We'll re-initialize these flags from the service list when we check for duplicates.
service->previous_user = service->user;
service->user = false;
service->previous_stable = service->stable;
service->stable = false;
service->previous_ncp = service->ncp;
service->ncp = false;
pservice = &service->next;
service->ignore = false;
}
}
// Add any services that are not present.
for (i = 0; i < services->num; i++) {
cti_service_t *cti_service = services->services[i];
for (service = tracker->thread_services; service != NULL; service = service->next) {
if (IS_SRP_SERVICE(cti_service) && service->service_type == unicast_service) {
if (!memcmp(&service->u.unicast.address, cti_service->server, 16) &&
!memcmp(&service->u.unicast.port, &cti_service->server[16], 2)) {
break;
}
} else if (IS_SRP_ANYCAST_SERVICE(cti_service) && service->service_type == anycast_service) {
uint8_t sequence_number = cti_service->service[1];
if (service->rloc16 == cti_service->rloc16 &&
service->u.anycast.sequence_number == sequence_number) {
break;
}
} else if (IS_PREF_ID_SERVICE(cti_service) && service->service_type == pref_id) {
if (!memcmp(&service->u.pref_id.partition_id, cti_service->server, 4) &&
!memcmp(service->u.pref_id.prefix, &cti_service->server[4], 5))
{
break;
}
}
}
if (service == NULL) {
bool save = false;
if (IS_SRP_SERVICE(cti_service)) {
service = thread_service_unicast_create(cti_service->rloc16, cti_service->server,
&cti_service->server[16], cti_service->service_id);
save = true;
} else if (IS_SRP_ANYCAST_SERVICE(cti_service)) {
uint8_t sequence_number = cti_service->service[1];
service = thread_service_anycast_create(cti_service->rloc16, sequence_number,
cti_service->service_id);
save = true;
} else if (IS_PREF_ID_SERVICE(cti_service)) {
save = true;
service = thread_service_pref_id_create(cti_service->rloc16, cti_service->server,
&cti_service->server[4], cti_service->service_id);
}
if (save) {
if (service == NULL) {
ERROR("[ST%lld] no memory for service.", tracker->id);
} else {
service_tracker_thread_service_note(tracker, service, "showed up");
*pservice = service;
pservice = &service->next;
}
}
}
// Also, since we're combing the list, update ncp, user and stable flags. Note that a service can
// appear more than once in the thread service list.
if (service != NULL) {
if (cti_service->flags & kCTIFlag_NCP) {
service->ncp = true;
} else {
service->user = true;
tracker->user_service_seen = true;
}
if (cti_service->flags & kCTIFlag_Stable) {
service->stable = true;
}
}
}
accumulator_t accumulator;
for (service = tracker->thread_services; service != NULL; service = service->next) {
// For unicast services, see if there's also an anycast service on the same RLOC16.
if (service->service_type == unicast_service) {
service->u.unicast.anycast_also_present = false;
for (thread_service_t *aservice = tracker->thread_services; aservice != NULL; aservice = aservice->next)
{
if (aservice->service_type == anycast_service && aservice->rloc16 == service->rloc16) {
service->u.unicast.anycast_also_present = true;
}
}
}
service_tracker_flags_accumulator_init(&accumulator);
service_tracker_flags_accumulate(&accumulator, service->previous_ncp, service->ncp, "ncp");
service_tracker_flags_accumulate(&accumulator, service->previous_stable, service->ncp, "stable");
service_tracker_flags_accumulate(&accumulator, service->previous_user, service->user, "user");
if (accumulator.changed) {
service_tracker_thread_service_note(tracker, service, accumulator.change);
}
}
// At this point the thread prefix list contains the same information as what we just received.
// Call any callbacks to trigger updates based on new information.
for (service_tracker_callback_t *callback = tracker->callbacks; callback != NULL; callback = callback->next) {
callback->callback(callback->context);
}
if (!tracker->user_service_seen && tracker->server_state != NULL &&
tracker->server_state->awaiting_service_removal)
{
tracker->server_state->awaiting_service_removal = false;
adv_ctl_thread_shutdown_status_check(tracker->server_state);
}
}
}
bool
service_tracker_local_service_seen(service_tracker_t *tracker)
{
return tracker->user_service_seen;
}
service_tracker_t *
service_tracker_create(srp_server_t *server_state)
{
service_tracker_t *ret = NULL;
service_tracker_t *tracker = calloc(1, sizeof(*ret));
if (tracker == NULL) {
ERROR("[ST%lld] no memory", ++service_tracker_serial_number);
goto exit;
}
RETAIN_HERE(tracker, service_tracker);
tracker->id = ++service_tracker_serial_number;
tracker->server_state = server_state;
ret = tracker;
tracker = NULL;
exit:
if (tracker != NULL) {
RELEASE_HERE(tracker, service_tracker);
}
return ret;
}
void
service_tracker_start(service_tracker_t *tracker)
{
if (tracker->thread_service_context != NULL) {
cti_events_discontinue(tracker->thread_service_context);
tracker->thread_service_context = NULL;
INFO("[ST%lld] restarting", tracker->id);
if (tracker->ref_count != 1) {
RELEASE_HERE(tracker, service_tracker); // Release the old retain for the callback.
} else {
FAULT("service tracker reference count should not be 1 here!");
}
}
int status = cti_get_service_list(tracker->server_state, &tracker->thread_service_context,
tracker, service_tracker_callback, NULL);
if (status != kCTIStatus_NoError) {
INFO("[ST%lld] service list get failed: %d", tracker->id, status);
return;
}
INFO("[ST%lld] service list get started", tracker->id);
RETAIN_HERE(tracker, service_tracker); // for the callback.
}
bool
service_tracker_callback_add(service_tracker_t *tracker,
void (*callback)(void *context), void (*context_release)(void *context), void *context)
{
bool ret = false;
service_tracker_callback_t **tpp;
// It's an error for two callbacks to have the same context
for (tpp = &tracker->callbacks; *tpp != NULL; tpp = &(*tpp)->next) {
if ((*tpp)->context == context) {
FAULT("[ST%lld] duplicate context %p", tracker->id, context);
goto exit;
}
}
service_tracker_callback_t *tracker_callback = calloc(1, sizeof(*tracker_callback));
if (tracker_callback == NULL) {
ERROR("[ST%lld] no memory", tracker->id);
goto exit;
}
tracker_callback->callback = callback;
tracker_callback->context_release = context_release;
tracker_callback->context = context;
// The callback list holds a reference to the tracker
if (tracker->callbacks == NULL) {
RETAIN_HERE(tracker, service_tracker);
}
// Keep the callback on the list.
*tpp = tracker_callback;
ret = true;
exit:
return ret;
}
static void
service_tracker_callback_free(service_tracker_callback_t *callback)
{
if (callback->context_release != NULL) {
callback->context_release(callback->context);
}
free(callback);
}
void
service_tracker_stop(service_tracker_t *tracker)
{
if (tracker == NULL) {
return;
}
if (tracker->thread_service_context != NULL) {
cti_events_discontinue(tracker->thread_service_context);
tracker->thread_service_context = NULL;
RELEASE_HERE(tracker, service_tracker);
}
}
void
service_tracker_cancel(service_tracker_t *tracker)
{
if (tracker == NULL) {
return;
}
service_tracker_stop(tracker);
if (tracker->callbacks != NULL) {
service_tracker_callback_t *next;
for (service_tracker_callback_t *callback = tracker->callbacks; callback != NULL; callback = next) {
next = callback->next;
service_tracker_callback_free(callback);
}
tracker->callbacks = NULL;
// Release the reference held by the callback list.
RELEASE_HERE(tracker, service_tracker);
}
}
void
service_tracker_callback_cancel(service_tracker_t *tracker, void *context)
{
if (tracker == NULL) {
return;
}
for (service_tracker_callback_t **tpp = &tracker->callbacks; *tpp != NULL; tpp = &((*tpp)->next)) {
service_tracker_callback_t *callback = *tpp;
if (callback->context == context) {
*tpp = callback->next;
service_tracker_callback_free(callback);
return;
}
}
}
static int
service_tracker_get_winning_anycast_sequence_number(service_tracker_t *NULLABLE tracker)
{
if (tracker == NULL) {
return -1;
}
int winning_sequence_number = -1;
// Find the sequence number that would win.
for (thread_service_t *service = tracker->thread_services; service != NULL; service = service->next)
{
if (service->ignore) {
continue;
}
if ((int)service->u.anycast.sequence_number > winning_sequence_number) {
winning_sequence_number = service->u.anycast.sequence_number;
}
}
return winning_sequence_number;
}
thread_service_t *
service_tracker_services_get(service_tracker_t *NULLABLE tracker)
{
if (tracker != NULL) {
return tracker->thread_services;
}
return NULL;
}
// Check to see if a service exists that matches the service passed in as an argument, and if it is still validated.
// Service object might be a different object
bool
service_tracker_verified_service_still_exists(service_tracker_t *NULLABLE tracker, thread_service_t *old_service)
{
if (tracker == NULL || old_service == NULL) {
return false;
}
for (thread_service_t *service = tracker->thread_services; service != NULL; service = service->next) {
if (service->ignore) {
continue;
}
if (service->responding && old_service->service_type == service->service_type) {
if (service->service_type == unicast_service) {
if (service->u.unicast.port == old_service->u.unicast.port &&
!memcmp(&service->u.unicast.address, &old_service->u.unicast.address,
sizeof(service->u.unicast.address)))
{
return true;
}
} else if (service->service_type == anycast_service) {
if (service->u.anycast.sequence_number == old_service->u.anycast.sequence_number) {
return true;
}
}
FAULT("old_service type is bogus: %d", old_service->service_type);
return false;
}
}
return false;
}
// If true, there is a service on the list that we've verified. Return it (caller must retain if saving pointer).
thread_service_t *
service_tracker_verified_service_get(service_tracker_t *NULLABLE tracker)
{
if (tracker == NULL) {
return false;
}
for (thread_service_t *service = tracker->thread_services; service != NULL; service = service->next) {
if (service->ignore) {
continue;
}
if (service->checked && service->responding) {
return service;
}
}
return false;
}
// Check for an unverified service on the list. If we are currently checking a service, return that service.
thread_service_t *
service_tracker_unverified_service_get(service_tracker_t *NULLABLE tracker, thread_service_type_t service_type)
{
thread_service_t *ret = NULL;
if (tracker != NULL) {
for (thread_service_t *service = tracker->thread_services; service != NULL; service = service->next) {
if (service_type != any_service && service_type != service->service_type) {
continue;
}
if (service->ignore) {
continue;
}
if (service->checking) {
return service;
}
if (ret == NULL && !service->checked && !service->probe_state && !service->user) {
ret = service;
}
}
}
if (tracker != NULL && ret != NULL) {
char buf[128];
snprintf(buf, sizeof(buf), "service_tracker_unverified_service_get returning %p", ret);
service_tracker_thread_service_note(tracker, ret, buf);
}
return ret;
}
static void
service_tracker_probe_callback(thread_service_t *UNUSED service, void *context, bool UNUSED succeeded)
{
service_tracker_t *tracker = context;
// Notify consumers of service tracker callbacks that something has changed.
for (service_tracker_callback_t *callback = tracker->callbacks; callback != NULL; callback = callback->next) {
callback->callback(callback->context);
}
}
// Find a service that is not currently being probed and has not been probed, and start probing it. If we are already probing a service,
// or if there are no services remaining to probe, do nothing.
void
service_tracker_verify_next_service(service_tracker_t *NULLABLE tracker)
{
thread_service_t *service;
if (tracker == NULL) {
return;
}
int winning_sequence_number = service_tracker_get_winning_anycast_sequence_number(tracker);
for (service = tracker->thread_services; service != NULL; service = service->next) {
if (service->probe_state) {
return;
}
// For anycast services, if not on the winning sequence number, don't check
if (service->service_type == anycast_service && service->u.anycast.sequence_number != winning_sequence_number) {
continue;
}
// If this is our service, we don't need to check it.
if (service->user) {
continue;
}
// If we've probed it recently, don't probe it again yet.
if (srp_time() - service->last_probe_time < 300)
{
continue;
}
if (service->checked) {
continue;
}
// If we didn't continue, yet, it's because we found a service we can probe, so probe it.
RETAIN_HERE(tracker, service_tracker); // For the srp probe
probe_srp_service(service, tracker, service_tracker_probe_callback, service_tracker_context_release);
return;
}
}
void
service_tracker_cancel_probes(service_tracker_t *NULLABLE tracker)
{
if (tracker == NULL) {
return;
}
for (thread_service_t *service = tracker->thread_services; service != NULL; service = service->next) {
if (service->probe_state) {
probe_srp_service_probe_cancel(service);
}
}
}
// Local Variables:
// mode: C
// tab-width: 4
// c-file-style: "bsd"
// c-basic-offset: 4
// fill-column: 120
// indent-tabs-mode: nil
// End: