/* $NetBSD: announce.c,v 1.2 2017/01/28 21:31:44 christos Exp $ */
/*
* Copyright (c) 2008 Apple Inc. All Rights Reserved.
*
* Export of this software from the United States of America may require
* a specific license from the United States Government. It is the
* responsibility of any person or organization contemplating export to
* obtain such a license before exporting.
*
* WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
* distribute this software and its documentation for any purpose and
* without fee is hereby granted, provided that the above copyright
* notice appear in all copies and that both that copyright notice and
* this permission notice appear in supporting documentation, and that
* the name of Apple Inc. not be used in advertising or publicity pertaining
* to distribution of the software without specific, written prior
* permission. Apple Inc. makes no representations about the suitability of
* this software for any purpose. It is provided "as is" without express
* or implied warranty.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
* WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
*
*/
#include "kdc_locl.h"
#if defined(__APPLE__) && defined(HAVE_GCD)
#include <CoreFoundation/CoreFoundation.h>
#include <SystemConfiguration/SCDynamicStore.h>
#include <SystemConfiguration/SCDynamicStoreCopySpecific.h>
#include <SystemConfiguration/SCDynamicStoreKey.h>
#include <dispatch/dispatch.h>
#include <asl.h>
#include <resolv.h>
#include <dns_sd.h>
#include <err.h>
static krb5_kdc_configuration *announce_config;
static krb5_context announce_context;
struct entry {
DNSRecordRef recordRef;
char *domain;
char *realm;
#define F_EXISTS 1
#define F_PUSH 2
int flags;
struct entry *next;
};
/* #define REGISTER_SRV_RR */
static struct entry *g_entries = NULL;
static CFStringRef g_hostname = NULL;
static DNSServiceRef g_dnsRef = NULL;
static SCDynamicStoreRef g_store = NULL;
static dispatch_queue_t g_queue = NULL;
#define LOG(...) asl_log(NULL, NULL, ASL_LEVEL_INFO, __VA_ARGS__)
static void create_dns_sd(void);
static void destroy_dns_sd(void);
static void update_all(SCDynamicStoreRef, CFArrayRef, void *);
/* parameters */
static CFStringRef NetworkChangedKey_BackToMyMac = CFSTR("Setup:/Network/BackToMyMac");
static char *
CFString2utf8(CFStringRef string)
{
size_t size;
char *str;
size = 1 + CFStringGetMaximumSizeForEncoding(CFStringGetLength(string), kCFStringEncodingUTF8);
str = malloc(size);
if (str == NULL)
return NULL;
if (CFStringGetCString(string, str, size, kCFStringEncodingUTF8) == false) {
free(str);
return NULL;
}
return str;
}
/*
*
*/
static void
retry_timer(void)
{
dispatch_source_t s;
dispatch_time_t t;
s = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
0, 0, g_queue);
t = dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC);
dispatch_source_set_timer(s, t, 0, NSEC_PER_SEC);
dispatch_source_set_event_handler(s, ^{
create_dns_sd();
dispatch_release(s);
});
dispatch_resume(s);
}
/*
*
*/
static void
create_dns_sd(void)
{
DNSServiceErrorType error;
dispatch_source_t s;
error = DNSServiceCreateConnection(&g_dnsRef);
if (error) {
retry_timer();
return;
}
dispatch_suspend(g_queue);
s = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
DNSServiceRefSockFD(g_dnsRef),
0, g_queue);
dispatch_source_set_event_handler(s, ^{
DNSServiceErrorType ret = DNSServiceProcessResult(g_dnsRef);
/* on error tear down and set timer to recreate */
if (ret != kDNSServiceErr_NoError && ret != kDNSServiceErr_Transient) {
dispatch_source_cancel(s);
}
});
dispatch_source_set_cancel_handler(s, ^{
destroy_dns_sd();
retry_timer();
dispatch_release(s);
});
dispatch_resume(s);
/* Do the first update ourself */
update_all(g_store, NULL, NULL);
dispatch_resume(g_queue);
}
static void
domain_add(const char *domain, const char *realm, int flag)
{
struct entry *e;
for (e = g_entries; e != NULL; e = e->next) {
if (strcmp(domain, e->domain) == 0 && strcmp(realm, e->realm) == 0) {
e->flags |= flag;
return;
}
}
LOG("Adding realm %s to domain %s", realm, domain);
e = calloc(1, sizeof(*e));
if (e == NULL)
return;
e->domain = strdup(domain);
e->realm = strdup(realm);
if (e->domain == NULL || e->realm == NULL) {
free(e->domain);
free(e->realm);
free(e);
return;
}
e->flags = flag | F_PUSH; /* if we allocate, we push */
e->next = g_entries;
g_entries = e;
}
struct addctx {
int flags;
const char *realm;
};
static void
domains_add(const void *key, const void *value, void *context)
{
char *str = CFString2utf8((CFStringRef)value);
struct addctx *ctx = context;
if (str == NULL)
return;
if (str[0] != '\0')
domain_add(str, ctx->realm, F_EXISTS | ctx->flags);
free(str);
}
static void
dnsCallback(DNSServiceRef sdRef __attribute__((unused)),
DNSRecordRef RecordRef __attribute__((unused)),
DNSServiceFlags flags __attribute__((unused)),
DNSServiceErrorType errorCode __attribute__((unused)),
void *context __attribute__((unused)))
{
}
#ifdef REGISTER_SRV_RR
/*
* Register DNS SRV rr for the realm.
*/
static const char *register_names[2] = {
"_kerberos._tcp",
"_kerberos._udp"
};
static struct {
DNSRecordRef *val;
size_t len;
} srvRefs = { NULL, 0 };
static void
register_srv(const char *realm, const char *hostname, int port)
{
unsigned char target[1024];
int i;
int size;
/* skip registering LKDC realms */
if (strncmp(realm, "LKDC:", 5) == 0)
return;
/* encode SRV-RR */
target[0] = 0; /* priority */
target[1] = 0; /* priority */
target[2] = 0; /* weight */
target[3] = 0; /* weigth */
target[4] = (port >> 8) & 0xff; /* port */
target[5] = (port >> 0) & 0xff; /* port */
size = dn_comp(hostname, target + 6, sizeof(target) - 6, NULL, NULL);
if (size < 0)
return;
size += 6;
LOG("register SRV rr for realm %s hostname %s:%d", realm, hostname, port);
for (i = 0; i < sizeof(register_names)/sizeof(register_names[0]); i++) {
char name[kDNSServiceMaxDomainName];
DNSServiceErrorType error;
void *ptr;
ptr = realloc(srvRefs.val, sizeof(srvRefs.val[0]) * (srvRefs.len + 1));
if (ptr == NULL)
errx(1, "malloc: out of memory");
srvRefs.val = ptr;
DNSServiceConstructFullName(name, NULL, register_names[i], realm);
error = DNSServiceRegisterRecord(g_dnsRef,
&srvRefs.val[srvRefs.len],
kDNSServiceFlagsUnique | kDNSServiceFlagsShareConnection,
0,
name,
kDNSServiceType_SRV,
kDNSServiceClass_IN,
size,
target,
0,
dnsCallback,
NULL);
if (error) {
LOG("Failed to register SRV rr for realm %s: %d", realm, error);
} else
srvRefs.len++;
}
}
static void
unregister_srv_realms(void)
{
if (g_dnsRef) {
for (i = 0; i < srvRefs.len; i++)
DNSServiceRemoveRecord(g_dnsRef, srvRefs.val[i], 0);
}
free(srvRefs.val);
srvRefs.len = 0;
srvRefs.val = NULL;
}
static void
register_srv_realms(CFStringRef host)
{
krb5_error_code ret;
char *hostname;
size_t i;
/* first unregister old names */
hostname = CFString2utf8(host);
if (hostname == NULL)
return;
for(i = 0; i < announce_config->num_db; i++) {
char **realms, **r;
if (announce_config->db[i]->hdb_get_realms == NULL)
continue;
ret = (announce_config->db[i]->hdb_get_realms)(announce_context, &realms);
if (ret == 0) {
for (r = realms; r && *r; r++)
register_srv(*r, hostname, 88);
krb5_free_host_realm(announce_context, realms);
}
}
free(hostname);
}
#endif /* REGISTER_SRV_RR */
static void
update_dns(void)
{
DNSServiceErrorType error;
struct entry **e = &g_entries;
char *hostname;
hostname = CFString2utf8(g_hostname);
if (hostname == NULL)
return;
while (*e != NULL) {
/* remove if this wasn't updated */
if (((*e)->flags & F_EXISTS) == 0) {
struct entry *drop = *e;
*e = (*e)->next;
LOG("Deleting realm %s from domain %s",
drop->realm, drop->domain);
if (drop->recordRef && g_dnsRef)
DNSServiceRemoveRecord(g_dnsRef, drop->recordRef, 0);
free(drop->domain);
free(drop->realm);
free(drop);
continue;
}
if ((*e)->flags & F_PUSH) {
struct entry *update = *e;
char *dnsdata, *name;
size_t len;
len = strlen(update->realm);
asprintf(&dnsdata, "%c%s", (int)len, update->realm);
if (dnsdata == NULL)
errx(1, "malloc");
asprintf(&name, "_kerberos.%s.%s", hostname, update->domain);
if (name == NULL)
errx(1, "malloc");
if (update->recordRef)
DNSServiceRemoveRecord(g_dnsRef, update->recordRef, 0);
error = DNSServiceRegisterRecord(g_dnsRef,
&update->recordRef,
kDNSServiceFlagsShared | kDNSServiceFlagsAllowRemoteQuery,
0,
name,
kDNSServiceType_TXT,
kDNSServiceClass_IN,
len+1,
dnsdata,
0,
dnsCallback,
NULL);
free(name);
free(dnsdata);
if (error)
errx(1, "failure to update entry for %s/%s",
update->domain, update->realm);
}
e = &(*e)->next;
}
free(hostname);
}
static void
update_entries(SCDynamicStoreRef store, const char *realm, int flags)
{
CFDictionaryRef btmm;
/* we always announce in the local domain */
domain_add("local", realm, F_EXISTS | flags);
/* announce btmm */
btmm = SCDynamicStoreCopyValue(store, NetworkChangedKey_BackToMyMac);
if (btmm) {
struct addctx addctx;
addctx.flags = flags;
addctx.realm = realm;
CFDictionaryApplyFunction(btmm, domains_add, &addctx);
CFRelease(btmm);
}
}
static void
update_all(SCDynamicStoreRef store, CFArrayRef changedKeys, void *info)
{
struct entry *e;
CFStringRef host;
int i, flags = 0;
LOG("something changed, running update");
host = SCDynamicStoreCopyLocalHostName(store);
if (host == NULL)
return;
if (g_hostname == NULL || CFStringCompare(host, g_hostname, 0) != kCFCompareEqualTo) {
if (g_hostname)
CFRelease(g_hostname);
g_hostname = CFRetain(host);
flags = F_PUSH; /* if hostname has changed, force push */
#ifdef REGISTER_SRV_RR
register_srv_realms(g_hostname);
#endif
}
for (e = g_entries; e != NULL; e = e->next)
e->flags &= ~(F_EXISTS|F_PUSH);
for(i = 0; i < announce_config->num_db; i++) {
krb5_error_code ret;
char **realms, **r;
if (announce_config->db[i]->hdb_get_realms == NULL)
continue;
ret = (announce_config->db[i]->hdb_get_realms)(announce_context, announce_config->db[i], &realms);
if (ret == 0) {
for (r = realms; r && *r; r++)
update_entries(store, *r, flags);
krb5_free_host_realm(announce_context, realms);
}
}
update_dns();
CFRelease(host);
}
static void
delete_all(void)
{
struct entry *e;
for (e = g_entries; e != NULL; e = e->next)
e->flags &= ~(F_EXISTS|F_PUSH);
update_dns();
if (g_entries != NULL)
errx(1, "Failed to remove all bonjour entries");
}
static void
destroy_dns_sd(void)
{
if (g_dnsRef == NULL)
return;
delete_all();
#ifdef REGISTER_SRV_RR
unregister_srv_realms();
#endif
DNSServiceRefDeallocate(g_dnsRef);
g_dnsRef = NULL;
}
static SCDynamicStoreRef
register_notification(void)
{
SCDynamicStoreRef store;
CFStringRef computerNameKey;
CFMutableArrayRef keys;
computerNameKey = SCDynamicStoreKeyCreateHostNames(kCFAllocatorDefault);
store = SCDynamicStoreCreate(kCFAllocatorDefault, CFSTR("Network watcher"),
update_all, NULL);
if (store == NULL)
errx(1, "SCDynamicStoreCreate");
keys = CFArrayCreateMutable(kCFAllocatorDefault, 2, &kCFTypeArrayCallBacks);
if (keys == NULL)
errx(1, "CFArrayCreateMutable");
CFArrayAppendValue(keys, computerNameKey);
CFArrayAppendValue(keys, NetworkChangedKey_BackToMyMac);
if (SCDynamicStoreSetNotificationKeys(store, keys, NULL) == false)
errx(1, "SCDynamicStoreSetNotificationKeys");
CFRelease(computerNameKey);
CFRelease(keys);
if (!SCDynamicStoreSetDispatchQueue(store, g_queue))
errx(1, "SCDynamicStoreSetDispatchQueue");
return store;
}
#endif
void
bonjour_announce(krb5_context context, krb5_kdc_configuration *config)
{
#if defined(__APPLE__) && defined(HAVE_GCD)
g_queue = dispatch_queue_create("com.apple.kdc_announce", NULL);
if (!g_queue)
errx(1, "dispatch_queue_create");
g_store = register_notification();
announce_config = config;
announce_context = context;
create_dns_sd();
#endif
}