From d942aa9321d27981efe13bc56bc231de5631253a Mon Sep 17 00:00:00 2001 From: Brian Hartvigsen Date: Fri, 9 Apr 2021 13:46:04 -0600 Subject: [PATCH] Support Cisco Umbrella/OpenDNS Device ID & Remote IP This is based on the information at https://docs.umbrella.com/umbrella-api/docs/identifying-dns-traffic and https://docs.umbrella.com/umbrella-api/docs/identifying-dns-traffic2 . Using --umbrella by itself will enable Remote IP reporting. This can not be used for any policy filtering in Cisco Umbrella/OpenDNS. Additional information can be supplied using specific option specifications, multiple can be separated by a comma: --umbrella=orgid:1234,deviceid=0123456789abcdef Specifies that you want to report organization 1234 using device 0123456789abcdef. For Cisco Umbrella Enterprise, see "Register (Create) a device" (https://docs.umbrella.com/umbrella-api/docs/create-a-device) for how to get a Device ID and "Organization ID endpoint" (https://docs.umbrella.com/umbrella-api/docs/organization-endpoint) to get organizations ID. For OpenDNS Home Users, there is no organization, see Registration API endpoint (https://docs.umbrella.com/umbrella-api/docs/registration-api-endpoint2) for how to get a Device ID. Asset ID should be ignored unless specifically instructed to use by support. Signed-off-by: Brian Hartvigsen --- man/dnsmasq.8 | 8 +++++- src/dns-protocol.h | 1 + src/dnsmasq.h | 7 +++++- src/edns0.c | 63 ++++++++++++++++++++++++++++++++++++++++++++++ src/option.c | 59 ++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 135 insertions(+), 3 deletions(-) diff --git a/man/dnsmasq.8 b/man/dnsmasq.8 index fce580f..eec77af 100644 --- a/man/dnsmasq.8 +++ b/man/dnsmasq.8 @@ -711,7 +711,13 @@ will add the /24 and /96 subnets of the requestor for IPv4 and IPv6 requestors, will add 1.2.3.0/24 for IPv4 requestors and ::/0 for IPv6 requestors. .B --add-subnet=1.2.3.4/24,1.2.3.4/24 will add 1.2.3.0/24 for both IPv4 and IPv6 requestors. - +.TP +.B --umbrella[=deviceid:[,orgid:]] +Embeds the requestor's IP address in DNS queries forwarded upstream. +If device id or organization id are specified, the information is +included in the forwarded queries and may be able to be used in +filtering policies and reporting. The order of the deviceid and orgid +attributes is irrelevant, but must be separated by a comma. .TP .B \-c, --cache-size= Set the size of dnsmasq's cache. The default is 150 names. Setting the cache size to zero disables caching. Note: huge cache size impacts performance. diff --git a/src/dns-protocol.h b/src/dns-protocol.h index 941bea6..8ad1964 100644 --- a/src/dns-protocol.h +++ b/src/dns-protocol.h @@ -82,6 +82,7 @@ #define EDNS0_OPTION_CLIENT_SUBNET 8 /* IANA */ #define EDNS0_OPTION_NOMDEVICEID 65073 /* Nominum temporary assignment */ #define EDNS0_OPTION_NOMCPEID 65074 /* Nominum temporary assignment */ +#define EDNS0_OPTION_UMBRELLA 20292 /* Cisco Umbrella temporary assignment */ struct dns_header { u16 id; diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 1e21005..95dc8ae 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -270,7 +270,9 @@ struct event_desc { #define OPT_SINGLE_PORT 60 #define OPT_LEASE_RENEW 61 #define OPT_LOG_DEBUG 62 -#define OPT_LAST 63 +#define OPT_UMBRELLA 63 +#define OPT_UMBRELLA_DEVID 64 +#define OPT_LAST 65 #define OPTION_BITS (sizeof(unsigned int)*8) #define OPTION_SIZE ( (OPT_LAST/OPTION_BITS)+((OPT_LAST%OPTION_BITS)!=0) ) @@ -1061,6 +1063,9 @@ extern struct daemon { int port, query_port, min_port, max_port; unsigned long local_ttl, neg_ttl, max_ttl, min_cache_ttl, max_cache_ttl, auth_ttl, dhcp_ttl, use_dhcp_ttl; char *dns_client_id; + u32 umbrella_org; + u32 umbrella_asset; + u8 umbrella_device[8]; struct hostsfile *addn_hosts; struct dhcp_context *dhcp, *dhcp6; struct ra_interface *ra_interfaces; diff --git a/src/edns0.c b/src/edns0.c index c85b318..7bd26b8 100644 --- a/src/edns0.c +++ b/src/edns0.c @@ -427,6 +427,66 @@ int check_source(struct dns_header *header, size_t plen, unsigned char *pseudohe return 1; } +/* See https://docs.umbrella.com/umbrella-api/docs/identifying-dns-traffic for + * detailed information on packet formating. + */ +#define UMBRELLA_VERSION 1 +#define UMBRELLA_TYPESZ 2 + +#define UMBRELLA_ASSET 0x0004 +#define UMBRELLA_ASSETSZ sizeof(daemon->umbrella_asset) +#define UMBRELLA_ORG 0x0008 +#define UMBRELLA_ORGSZ sizeof(daemon->umbrella_org) +#define UMBRELLA_IPV4 0x0010 +#define UMBRELLA_IPV6 0x0020 +#define UMBRELLA_DEVICE 0x0040 +#define UMBRELLA_DEVICESZ sizeof(daemon->umbrella_device) + +struct umbrella_opt { + u8 magic[4]; + u8 version; + u8 flags; + /* We have 4 possible fields since we'll never send both IPv4 and + * IPv6, so using the larger of the two to calculate max buffer size. + * Each field also has a type header. So the following accounts for + * the type headers and each field size to get a max buffer size. + */ + u8 fields[4 * UMBRELLA_TYPESZ + UMBRELLA_ORGSZ + IN6ADDRSZ + UMBRELLA_DEVICESZ + UMBRELLA_ASSETSZ]; +}; + +static size_t add_umbrella_opt(struct dns_header *header, size_t plen, unsigned char *limit, union mysockaddr *source, int *cacheable) +{ + *cacheable = 0; + + struct umbrella_opt opt = {{"ODNS"}, UMBRELLA_VERSION, 0, {}}; + u8 *u = &opt.fields[0]; + + if (daemon->umbrella_org) { + PUTSHORT(UMBRELLA_ORG, u); + PUTLONG(daemon->umbrella_org, u); + } + + int family = source->sa.sa_family; + PUTSHORT(family == AF_INET ? UMBRELLA_IPV4 : UMBRELLA_IPV6, u); + int size = family == AF_INET ? INADDRSZ : IN6ADDRSZ; + memcpy(u, get_addrp(source, family), size); + u += size; + + if (option_bool(OPT_UMBRELLA_DEVID)) { + PUTSHORT(UMBRELLA_DEVICE, u); + memcpy(u, (char *)&daemon->umbrella_device, UMBRELLA_DEVICESZ); + u += UMBRELLA_DEVICESZ; + } + + if (daemon->umbrella_asset) { + PUTSHORT(UMBRELLA_ASSET, u); + PUTLONG(daemon->umbrella_asset, u); + } + + int len = u - &opt.magic[0]; + return add_pseudoheader(header, plen, (unsigned char *)limit, PACKETSZ, EDNS0_OPTION_UMBRELLA, (unsigned char *)&opt, len, 0, 1); +} + /* Set *check_subnet if we add a client subnet option, which needs to checked in the reply. Set *cacheable to zero if we add an option which the answer may depend on. */ @@ -445,6 +505,9 @@ size_t add_edns0_config(struct dns_header *header, size_t plen, unsigned char *l if (daemon->dns_client_id) plen = add_pseudoheader(header, plen, limit, PACKETSZ, EDNS0_OPTION_NOMCPEID, (unsigned char *)daemon->dns_client_id, strlen(daemon->dns_client_id), 0, 1); + + if (option_bool(OPT_UMBRELLA)) + plen = add_umbrella_opt(header, plen, limit, source, cacheable); if (option_bool(OPT_CLIENT_SUBNET)) { diff --git a/src/option.c b/src/option.c index 6de5914..e8926a4 100644 --- a/src/option.c +++ b/src/option.c @@ -170,6 +170,7 @@ struct myoption { #define LOPT_PXE_VENDOR 361 #define LOPT_DYNHOST 362 #define LOPT_LOG_DEBUG 363 +#define LOPT_UMBRELLA 364 #ifdef HAVE_GETOPT_LONG static const struct option opts[] = @@ -345,6 +346,7 @@ static const struct myoption opts[] = { "dhcp-ignore-clid", 0, 0, LOPT_IGNORE_CLID }, { "dynamic-host", 1, 0, LOPT_DYNHOST }, { "log-debug", 0, 0, LOPT_LOG_DEBUG }, + { "umbrella", 2, 0, LOPT_UMBRELLA }, { NULL, 0, 0, 0 } }; @@ -527,6 +529,7 @@ static struct { { LOPT_DUMPFILE, ARG_ONE, "", gettext_noop("Path to debug packet dump file"), NULL }, { LOPT_DUMPMASK, ARG_ONE, "", gettext_noop("Mask which packets to dump"), NULL }, { LOPT_SCRIPT_TIME, OPT_LEASE_RENEW, NULL, gettext_noop("Call dhcp-script when lease expiry changes."), NULL }, + { LOPT_UMBRELLA, ARG_ONE, "[=]", gettext_noop("Send Cisco Umbrella identifiers including remote IP."), NULL }, { 0, 0, NULL, NULL, NULL } }; @@ -653,7 +656,7 @@ static char *canonicalise_opt(char *s) return ret; } -static int atoi_check(char *a, int *res) +static int numeric_check(char *a) { char *p; @@ -666,10 +669,29 @@ static int atoi_check(char *a, int *res) if (*p < '0' || *p > '9') return 0; + return 1; +} + +static int atoi_check(char *a, int *res) +{ + if (!numeric_check(a)) + return 0; *res = atoi(a); return 1; } +static int strtoul_check(char *a, u32 *res) +{ + if (!numeric_check(a)) + return 0; + *res = strtoul(a, NULL, 10); + if (errno == ERANGE) { + errno = 0; + return 0; + } + return 1; +} + static int atoi_check16(char *a, int *res) { if (!(atoi_check(a, res)) || @@ -2409,6 +2431,41 @@ static int one_opt(int option, char *arg, char *errstr, char *gen_err, int comma daemon->dns_client_id = opt_string_alloc(arg); break; + case LOPT_UMBRELLA: /* --umbrella */ + set_option_bool(OPT_UMBRELLA); + while (arg) { + comma = split(arg); + if (strstr(arg, "deviceid:")) { + arg += 9; + if (strlen(arg) != 16) + ret_err(gen_err); + for (char *p = arg; *p; p++) { + if (!isxdigit((int)*p)) + ret_err(gen_err); + } + set_option_bool(OPT_UMBRELLA_DEVID); + + u8 *u = daemon->umbrella_device; + char word[3]; + for (u8 i = 0; i < sizeof(daemon->umbrella_device); i++, arg+=2) { + memcpy(word, &(arg[0]), 2); + *u++ = strtoul(word, NULL, 16); + } + } + else if (strstr(arg, "orgid:")) { + if (!strtoul_check(arg+6, &daemon->umbrella_org)) { + ret_err(gen_err); + } + } + else if (strstr(arg, "assetid:")) { + if (!strtoul_check(arg+8, &daemon->umbrella_asset)) { + ret_err(gen_err); + } + } + arg = comma; + } + break; + case LOPT_ADD_MAC: /* --add-mac */ if (!arg) set_option_bool(OPT_ADD_MAC);