blob: b9cdb39f3b645db7891ef84f673d7cce9d663f69 [file] [log] [blame]
/* probe-srp.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>
#include "srp.h"
#include "dns-msg.h"
#include "ioloop.h"
#include "srp-mdns-proxy.h"
#include "state-machine.h"
#include "thread-service.h"
#include "service-tracker.h"
#include "probe-srp.h"
struct probe_state {
int ref_count;
comm_t *connection;
wakeup_t *wakeup;
thread_service_t *service;
void *context;
void (*callback)(thread_service_t *service, void *context, bool succeeded);
route_state_t *route_state;
dns_wire_t question;
int num_retransmissions, retransmission_delay;
uint16_t question_length;
};
static void
probe_state_finalize(probe_state_t *probe_state)
{
if (probe_state->wakeup != NULL) {
ioloop_wakeup_release(probe_state->wakeup);
}
free(probe_state);
}
static void
probe_srp_done(void *context, bool succeeded)
{
probe_state_t *probe_state = context;
struct in6_addr *address;
int port;
thread_service_t *service = probe_state->service;
// Note: we should still have references both to probe_state and to service here because they each held references
// to each other.
probe_state->service = NULL;
service->probe_state = NULL;
if (service->service_type == anycast_service) {
address = &service->u.anycast.address;
port = 53;
} else {
address = &service->u.unicast.address;
port = (service->u.unicast.port[0] << 8) | service->u.unicast.port[1];
}
SEGMENTED_IPv6_ADDR_GEN_SRP(address->s6_addr, addr_buf);
if (!succeeded) {
INFO("service " PRI_SEGMENTED_IPv6_ADDR_SRP " not responding on port %d",
SEGMENTED_IPv6_ADDR_PARAM_SRP(address->s6_addr, addr_buf), port);
service->checking = false;
service->ignore = true; // Don't consider this service when deciding what to advertise
service->remove = false; // Keep the service around so we don't keep probing it.
service->responding = false;
} else {
INFO("service " PRI_SEGMENTED_IPv6_ADDR_SRP " responded on port %d",
SEGMENTED_IPv6_ADDR_PARAM_SRP(address->s6_addr, addr_buf), port);
service->checking = false;
service->ignore = false;
service->remove = false;
service->checked = true;
}
service->responding = true;
if (probe_state->callback != NULL) {
probe_state->callback(probe_state->service, probe_state->context, succeeded);
probe_state->callback = NULL;
}
probe_state->context = NULL;
ioloop_comm_cancel(probe_state->connection); // Cancel the connection (should result in the state being released)
thread_service_release(service);
if (probe_state->wakeup != NULL) {
ioloop_cancel_wake_event(probe_state->wakeup);
}
RELEASE_HERE(probe_state, probe_state); // The thread_service_t's reference to the probe state
}
static void
probe_srp_datagram(comm_t *UNUSED connection, message_t *message, void *context)
{
#ifdef PROBE_SRP_TCP
(void)message;
// We should never get a datagram
ERROR("got a datagram on %p", context);
#else
probe_state_t *probe_state = context;
SEGMENTED_IPv6_ADDR_GEN_SRP(&address->sin6.sin6_addr, addr_buf);
INFO("datagram from " PRI_SEGMENTED_IPv6_ADDR_SRP " on port %d xid %x (question xid %x)",
SEGMENTED_IPv6_ADDR_PARAM_SRP(&probe_state->connection->address.sin6.sin6_addr, addr_buf),
ntohs(probe_state->connection->address.sin6.sin6_port), message->wire.id, probe_state->question.id);
if (message->wire.id != probe_state->question.id) {
return; // not a response to the question we asked
}
dns_message_t *dns_message = NULL;
if (!dns_wire_parse(&dns_message, &message->wire, message->length, false)) {
// Not a valid response, who knows what happened?
return;
}
dns_message_free(dns_message);
probe_srp_done(context, true);
#endif
}
static void
probe_srp_connection_finalize(void *context)
{
probe_state_t *probe_state = context;
probe_state->connection = NULL;
RELEASE_HERE(probe_state, probe_state);
}
static void
probe_srp_disconnected(comm_t *UNUSED connection, void *context, int UNUSED error)
{
probe_state_t *probe_state = context;
// We can get here either because the connection object got canceled or because we made a TCP connection that
// failed to connect. If we haven't signaled "done" yet, probe_state->service will be non-NULL.
if (probe_state->service != NULL) {
probe_srp_done(context, false);
}
// Once we've gotten the connection disconnect event, we should not get any more callbacks from the connection
// object.
if (probe_state->connection != NULL) {
ioloop_comm_release(probe_state->connection);
}
}
#ifndef PROBE_SRP_TCP
static void probe_srp_schedule_retransmission(probe_state_t *probe_state);
static void
probe_srp_retransmit(void *context)
{
probe_state_t *probe_state = context;
// Only retransmit three times.
probe_state->num_retransmissions++;
INFO("num_retransmissions = %d, time = %lg", probe_state->num_retransmissions, probe_state->retransmission_delay / 1000.0);
if (probe_state->num_retransmissions > 3) {
probe_srp_done(context, false); // fail
ioloop_comm_cancel(probe_state->connection);
} else {
// Schedule a retransmission with exponential backoff
probe_srp_schedule_retransmission(probe_state);
// Send the question
struct iovec iov;
iov.iov_len = probe_state->question_length;
iov.iov_base = &probe_state->question;
ioloop_send_message(probe_state->connection, NULL, &iov, 1);
}
}
static void
probe_srp_context_release(void *context)
{
probe_state_t *probe_state = context;
RELEASE_HERE(probe_state, probe_state);
}
static void
probe_srp_schedule_retransmission(probe_state_t *probe_state)
{
if (probe_state->wakeup == NULL) {
probe_state->wakeup = ioloop_wakeup_create();
if (probe_state->wakeup == NULL) {
ERROR("can't allocate probe state wakeup");
probe_srp_done(probe_state, false);
ioloop_comm_cancel(probe_state->connection);
return;
}
}
int next_time = probe_state->retransmission_delay + srp_random16() % probe_state->retransmission_delay;
probe_state->retransmission_delay *= 2;
ioloop_add_wake_event(probe_state->wakeup, probe_state, probe_srp_retransmit, probe_srp_context_release, next_time);
RETAIN_HERE(probe_state, probe_state); // The wakeup holds a reference to probe_state.
}
#endif // PROBE_SRP_TCP
static void
probe_srp_connected(comm_t *connection, void *context)
{
#ifdef PROBE_SRP_TCP
probe_srp_done(context, true);
#else
(void)connection;
probe_state_t *probe_state = context;
// Initialize a DNS message to send to the target
memset(&probe_state->question, 0, DNS_HEADER_SIZE);
dns_towire_state_t towire;
memset(&towire, 0, sizeof(towire));
towire.p = &probe_state->question.data[0];
towire.lim = &probe_state->question.data[0] + DNS_DATA_SIZE;
towire.message = &probe_state->question;
// Set up the header.
probe_state->question.id = srp_random16();
probe_state->question.bitfield = 0;
dns_qr_set(&probe_state->question, dns_qr_query);
dns_opcode_set(&probe_state->question, dns_opcode_query);
probe_state->question.qdcount = htons(1); // Just ask one question
// Query localhost.
dns_full_name_to_wire(NULL, &towire, "localhost");
dns_u16_to_wire(&towire, dns_rrtype_a);
dns_u16_to_wire(&towire, dns_qclass_in);
probe_state->question_length = towire.p - (uint8_t *)&probe_state->question;
// We're not in a hurry; the goal is to probe.
probe_state->retransmission_delay = 1000; // milliseconds
probe_state->num_retransmissions = 0;
// Schedule the first send
probe_srp_schedule_retransmission(probe_state);
#endif // PROBE_SRP_TCP
}
static probe_state_t *
probe_state_create(addr_t *address, thread_service_t *service, void *context,
void (*callback)(thread_service_t *service, void *context, bool succeeded))
{
probe_state_t *ret = NULL, *probe_state = calloc(1, sizeof(*probe_state));
if (probe_state == NULL) {
INFO("failed to create probe state");
goto out;
}
RETAIN_HERE(probe_state, probe_state); // Retain for the caller
// tls stream stable opportunistic
probe_state->connection = ioloop_connection_create(address, false, false, false, false,
probe_srp_datagram, probe_srp_connected, probe_srp_disconnected,
probe_srp_connection_finalize, probe_state);
if (probe_state->connection == NULL) {
INFO("failed to create connection");
goto out;
}
RETAIN_HERE(probe_state, probe_state); // for connection
SEGMENTED_IPv6_ADDR_GEN_SRP(&address->sin6.sin6_addr, addr_buf);
INFO("probing service " PRI_SEGMENTED_IPv6_ADDR_SRP " on port %d",
SEGMENTED_IPv6_ADDR_PARAM_SRP(&address->sin6.sin6_addr, addr_buf), ntohs(address->sin6.sin6_port));
probe_state->context = context;
probe_state->callback = callback;
service->last_probe_time = srp_time();
service->probe_state = probe_state;
RETAIN_HERE(service->probe_state, probe_state);
probe_state->service = service;
thread_service_retain(probe_state->service);
// connection holds the only reference to probe_state.
ret = probe_state;
probe_state = NULL;
out:
if (probe_state != NULL) {
RELEASE_HERE(probe_state, probe_state);
}
if (ret == NULL && callback != NULL) {
dispatch_async(dispatch_get_main_queue(), ^{
callback(service, context, true); // We claim success here because this should never fail; if it does, it's our problem.
});
}
return ret;
}
// If we've been asked to probe a service, go through the list.
static probe_state_t *
probe_srp_anycast_service(thread_service_t *service, void *context,
void (*callback)(thread_service_t *service, void *context, bool succeeded))
{
addr_t address;
memset(&address, 0, sizeof(address));
memcpy(&address.sin6.sin6_addr, &service->u.anycast.address, sizeof(service->u.anycast.address));
address.sin6.sin6_port = htons(53);
address.sa.sa_family = AF_INET6;
return probe_state_create(&address, service, context, callback);
}
static probe_state_t *
probe_srp_unicast_service(thread_service_t *service, void *context,
void (*callback)(thread_service_t *service, void *context, bool succeeded))
{
if (service->checking || service->user) {
return NULL;
}
addr_t address;
memset(&address, 0, sizeof(address));
address.sa.sa_family = AF_INET6;
memcpy(&address.sin6.sin6_addr, &service->u.unicast.address, sizeof(address.sin6.sin6_addr));
memcpy(&address.sin6.sin6_port, service->u.unicast.port, sizeof(address.sin6.sin6_port));
return probe_state_create(&address, service, context, callback);
}
void
probe_srp_service(thread_service_t *service, void *context,
void (*callback)(thread_service_t *service, void *context, bool succeeded))
{
probe_state_t *probe_state;
if (service->service_type == unicast_service) {
probe_state = probe_srp_unicast_service(service, context, callback);
} else if (service->service_type == anycast_service){
probe_state = probe_srp_anycast_service(service, context, callback);
} else {
FAULT("bogus service type in probe_srp_service: %d", service->service_type);
if (callback != NULL) {
dispatch_async(dispatch_get_main_queue(), ^{
callback(service, context, false); // False because this isn't a valid service
});
}
return;
}
// probe_state_create returns this retained, but we don't store the pointer.
RELEASE_HERE(probe_state, probe_state);
}
// Local Variables:
// mode: C
// tab-width: 4
// c-file-style: "bsd"
// c-basic-offset: 4
// fill-column: 120
// indent-tabs-mode: nil
// End: