| 1 | /* $NetBSD: ip_dns_pxy.c,v 1.3 2012/07/22 14:27:51 darrenr Exp $ */ |
| 2 | |
| 3 | /* |
| 4 | * Copyright (C) 2012 by Darren Reed. |
| 5 | * |
| 6 | * See the IPFILTER.LICENCE file for details on licencing. |
| 7 | * |
| 8 | * Id: ip_dns_pxy.c,v 1.1.1.2 2012/07/22 13:45:10 darrenr Exp |
| 9 | */ |
| 10 | |
| 11 | #define IPF_DNS_PROXY |
| 12 | |
| 13 | /* |
| 14 | * map ... proxy port dns/udp 53 { block .cnn.com; } |
| 15 | */ |
| 16 | typedef struct ipf_dns_filter { |
| 17 | struct ipf_dns_filter *idns_next; |
| 18 | char *idns_name; |
| 19 | int idns_namelen; |
| 20 | int idns_pass; |
| 21 | } ipf_dns_filter_t; |
| 22 | |
| 23 | |
| 24 | typedef struct ipf_dns_softc_s { |
| 25 | ipf_dns_filter_t *ipf_p_dns_list; |
| 26 | ipfrwlock_t ipf_p_dns_rwlock; |
| 27 | u_long ipf_p_dns_compress; |
| 28 | u_long ipf_p_dns_toolong; |
| 29 | u_long ipf_p_dns_nospace; |
| 30 | } ipf_dns_softc_t; |
| 31 | |
| 32 | int ipf_p_dns_allow_query(ipf_dns_softc_t *, dnsinfo_t *); |
| 33 | int ipf_p_dns_ctl(ipf_main_softc_t *, void *, ap_ctl_t *); |
| 34 | int ipf_p_dns_del(ipf_main_softc_t *, ap_session_t *); |
| 35 | int ipf_p_dns_get_name(ipf_dns_softc_t *, char *, int, char *, int); |
| 36 | int ipf_p_dns_inout(void *, fr_info_t *, ap_session_t *, nat_t *); |
| 37 | int ipf_p_dns_match(fr_info_t *, ap_session_t *, nat_t *); |
| 38 | int ipf_p_dns_match_names(ipf_dns_filter_t *, char *, int); |
| 39 | int ipf_p_dns_new(void *, fr_info_t *, ap_session_t *, nat_t *); |
| 40 | void *ipf_p_dns_soft_create(ipf_main_softc_t *); |
| 41 | void ipf_p_dns_soft_destroy(ipf_main_softc_t *, void *); |
| 42 | |
| 43 | typedef struct { |
| 44 | u_char dns_id[2]; |
| 45 | u_short dns_ctlword; |
| 46 | u_short dns_qdcount; |
| 47 | u_short dns_ancount; |
| 48 | u_short dns_nscount; |
| 49 | u_short dns_arcount; |
| 50 | } ipf_dns_hdr_t; |
| 51 | |
| 52 | #define DNS_QR(x) ((ntohs(x) & 0x8000) >> 15) |
| 53 | #define DNS_OPCODE(x) ((ntohs(x) & 0x7800) >> 11) |
| 54 | #define DNS_AA(x) ((ntohs(x) & 0x0400) >> 10) |
| 55 | #define DNS_TC(x) ((ntohs(x) & 0x0200) >> 9) |
| 56 | #define DNS_RD(x) ((ntohs(x) & 0x0100) >> 8) |
| 57 | #define DNS_RA(x) ((ntohs(x) & 0x0080) >> 7) |
| 58 | #define DNS_Z(x) ((ntohs(x) & 0x0070) >> 4) |
| 59 | #define DNS_RCODE(x) ((ntohs(x) & 0x000f) >> 0) |
| 60 | |
| 61 | |
| 62 | void * |
| 63 | ipf_p_dns_soft_create(ipf_main_softc_t *softc) |
| 64 | { |
| 65 | ipf_dns_softc_t *softd; |
| 66 | |
| 67 | KMALLOC(softd, ipf_dns_softc_t *); |
| 68 | if (softd == NULL) |
| 69 | return NULL; |
| 70 | |
| 71 | bzero((char *)softd, sizeof(*softd)); |
| 72 | RWLOCK_INIT(&softd->ipf_p_dns_rwlock, "ipf dns rwlock" ); |
| 73 | |
| 74 | return softd; |
| 75 | } |
| 76 | |
| 77 | |
| 78 | void |
| 79 | ipf_p_dns_soft_destroy(ipf_main_softc_t *softc, void *arg) |
| 80 | { |
| 81 | ipf_dns_softc_t *softd = arg; |
| 82 | ipf_dns_filter_t *idns; |
| 83 | |
| 84 | while ((idns = softd->ipf_p_dns_list) != NULL) { |
| 85 | KFREES(idns->idns_name, idns->idns_namelen); |
| 86 | idns->idns_name = NULL; |
| 87 | idns->idns_namelen = 0; |
| 88 | softd->ipf_p_dns_list = idns->idns_next; |
| 89 | KFREE(idns); |
| 90 | } |
| 91 | RW_DESTROY(&softd->ipf_p_dns_rwlock); |
| 92 | |
| 93 | KFREE(softd); |
| 94 | } |
| 95 | |
| 96 | |
| 97 | int |
| 98 | ipf_p_dns_ctl(ipf_main_softc_t *softc, void *arg, ap_ctl_t *ctl) |
| 99 | { |
| 100 | ipf_dns_softc_t *softd = arg; |
| 101 | ipf_dns_filter_t *tmp, *idns, **idnsp; |
| 102 | int error = 0; |
| 103 | |
| 104 | /* |
| 105 | * To make locking easier. |
| 106 | */ |
| 107 | KMALLOC(tmp, ipf_dns_filter_t *); |
| 108 | |
| 109 | WRITE_ENTER(&softd->ipf_p_dns_rwlock); |
| 110 | for (idnsp = &softd->ipf_p_dns_list; (idns = *idnsp) != NULL; |
| 111 | idnsp = &idns->idns_next) { |
| 112 | if (idns->idns_namelen != ctl->apc_dsize) |
| 113 | continue; |
| 114 | if (!strncmp(ctl->apc_data, idns->idns_name, |
| 115 | idns->idns_namelen)) |
| 116 | break; |
| 117 | } |
| 118 | |
| 119 | switch (ctl->apc_cmd) |
| 120 | { |
| 121 | case APC_CMD_DEL : |
| 122 | if (idns == NULL) { |
| 123 | IPFERROR(80006); |
| 124 | error = ESRCH; |
| 125 | break; |
| 126 | } |
| 127 | *idnsp = idns->idns_next; |
| 128 | idns->idns_next = NULL; |
| 129 | KFREES(idns->idns_name, idns->idns_namelen); |
| 130 | idns->idns_name = NULL; |
| 131 | idns->idns_namelen = 0; |
| 132 | KFREE(idns); |
| 133 | break; |
| 134 | case APC_CMD_ADD : |
| 135 | if (idns != NULL) { |
| 136 | IPFERROR(80007); |
| 137 | error = EEXIST; |
| 138 | break; |
| 139 | } |
| 140 | if (tmp == NULL) { |
| 141 | IPFERROR(80008); |
| 142 | error = ENOMEM; |
| 143 | break; |
| 144 | } |
| 145 | idns = tmp; |
| 146 | tmp = NULL; |
| 147 | idns->idns_namelen = ctl->apc_dsize; |
| 148 | idns->idns_name = ctl->apc_data; |
| 149 | idns->idns_pass = ctl->apc_arg; |
| 150 | idns->idns_next = NULL; |
| 151 | *idnsp = idns; |
| 152 | ctl->apc_data = NULL; |
| 153 | ctl->apc_dsize = 0; |
| 154 | break; |
| 155 | default : |
| 156 | IPFERROR(80009); |
| 157 | error = EINVAL; |
| 158 | break; |
| 159 | } |
| 160 | RWLOCK_EXIT(&softd->ipf_p_dns_rwlock); |
| 161 | |
| 162 | if (tmp != NULL) { |
| 163 | KFREE(tmp); |
| 164 | tmp = NULL; |
| 165 | } |
| 166 | |
| 167 | return error; |
| 168 | } |
| 169 | |
| 170 | |
| 171 | /* ARGSUSED */ |
| 172 | int |
| 173 | ipf_p_dns_new(void *arg, fr_info_t *fin, ap_session_t *aps, nat_t *nat) |
| 174 | { |
| 175 | dnsinfo_t *di; |
| 176 | int dlen; |
| 177 | |
| 178 | if (fin->fin_v != 4) |
| 179 | return -1; |
| 180 | |
| 181 | dlen = fin->fin_dlen - sizeof(udphdr_t); |
| 182 | if (dlen < sizeof(ipf_dns_hdr_t)) { |
| 183 | /* |
| 184 | * No real DNS packet is smaller than that. |
| 185 | */ |
| 186 | return -1; |
| 187 | } |
| 188 | |
| 189 | aps->aps_psiz = sizeof(dnsinfo_t); |
| 190 | KMALLOCS(di, dnsinfo_t *, sizeof(dnsinfo_t)); |
| 191 | if (di == NULL) { |
| 192 | printf("ipf_dns_new:KMALLOCS(%zu) failed\n" , sizeof(*di)); |
| 193 | return -1; |
| 194 | } |
| 195 | |
| 196 | MUTEX_INIT(&di->dnsi_lock, "dns lock" ); |
| 197 | |
| 198 | aps->aps_data = di; |
| 199 | |
| 200 | dlen = fin->fin_dlen - sizeof(udphdr_t); |
| 201 | COPYDATA(fin->fin_m, fin->fin_hlen + sizeof(udphdr_t), |
| 202 | MIN(dlen, sizeof(di->dnsi_buffer)), di->dnsi_buffer); |
| 203 | di->dnsi_id = (di->dnsi_buffer[0] << 8) | di->dnsi_buffer[1]; |
| 204 | return 0; |
| 205 | } |
| 206 | |
| 207 | |
| 208 | /* ARGSUSED */ |
| 209 | int |
| 210 | ipf_p_dns_del(ipf_main_softc_t *softc, ap_session_t *aps) |
| 211 | { |
| 212 | #ifdef USE_MUTEXES |
| 213 | dnsinfo_t *di = aps->aps_data; |
| 214 | |
| 215 | MUTEX_DESTROY(&di->dnsi_lock); |
| 216 | #endif |
| 217 | KFREES(aps->aps_data, aps->aps_psiz); |
| 218 | aps->aps_data = NULL; |
| 219 | aps->aps_psiz = 0; |
| 220 | return 0; |
| 221 | } |
| 222 | |
| 223 | |
| 224 | /* |
| 225 | * Tries to match the base string (in our ACL) with the query from a packet. |
| 226 | */ |
| 227 | int |
| 228 | ipf_p_dns_match_names(ipf_dns_filter_t *idns, char *query, int qlen) |
| 229 | { |
| 230 | int blen; |
| 231 | char *base; |
| 232 | |
| 233 | blen = idns->idns_namelen; |
| 234 | base = idns->idns_name; |
| 235 | |
| 236 | if (blen > qlen) |
| 237 | return 1; |
| 238 | |
| 239 | if (blen == qlen) |
| 240 | return strncasecmp(base, query, qlen); |
| 241 | |
| 242 | /* |
| 243 | * If the base string string is shorter than the query, allow the |
| 244 | * tail of the base to match the same length tail of the query *if*: |
| 245 | * - the base string starts with a '*' (*cnn.com) |
| 246 | * - the base string represents a domain (.cnn.com) |
| 247 | * as otherwise it would not be possible to block just "cnn.com" |
| 248 | * without also impacting "foocnn.com", etc. |
| 249 | */ |
| 250 | if (*base == '*') { |
| 251 | base++; |
| 252 | blen--; |
| 253 | } else if (*base != '.') |
| 254 | return 1; |
| 255 | |
| 256 | return strncasecmp(base, query + qlen - blen, blen); |
| 257 | } |
| 258 | |
| 259 | |
| 260 | int |
| 261 | ipf_p_dns_get_name(ipf_dns_softc_t *softd, char *start, int len, char *buffer, |
| 262 | int buflen) |
| 263 | { |
| 264 | char *s, *t, clen; |
| 265 | int slen, blen; |
| 266 | |
| 267 | s = start; |
| 268 | t = buffer; |
| 269 | slen = len; |
| 270 | blen = buflen - 1; /* Always make room for trailing \0 */ |
| 271 | |
| 272 | while (*s != '\0') { |
| 273 | clen = *s; |
| 274 | if ((clen & 0xc0) == 0xc0) { /* Doesn't do compression */ |
| 275 | softd->ipf_p_dns_compress++; |
| 276 | return 0; |
| 277 | } |
| 278 | if (clen > slen) { |
| 279 | softd->ipf_p_dns_toolong++; |
| 280 | return 0; /* Does the name run off the end? */ |
| 281 | } |
| 282 | if ((clen + 1) > blen) { |
| 283 | softd->ipf_p_dns_nospace++; |
| 284 | return 0; /* Enough room for name+.? */ |
| 285 | } |
| 286 | s++; |
| 287 | bcopy(s, t, clen); |
| 288 | t += clen; |
| 289 | s += clen; |
| 290 | *t++ = '.'; |
| 291 | slen -= clen; |
| 292 | blen -= (clen + 1); |
| 293 | } |
| 294 | |
| 295 | *(t - 1) = '\0'; |
| 296 | return s - start; |
| 297 | } |
| 298 | |
| 299 | |
| 300 | int |
| 301 | ipf_p_dns_allow_query(ipf_dns_softc_t *softd, dnsinfo_t *dnsi) |
| 302 | { |
| 303 | ipf_dns_filter_t *idns; |
| 304 | int len; |
| 305 | |
| 306 | len = strlen(dnsi->dnsi_buffer); |
| 307 | |
| 308 | for (idns = softd->ipf_p_dns_list; idns != NULL; idns = idns->idns_next) |
| 309 | if (ipf_p_dns_match_names(idns, dnsi->dnsi_buffer, len) == 0) |
| 310 | return idns->idns_pass; |
| 311 | return 0; |
| 312 | } |
| 313 | |
| 314 | |
| 315 | /* ARGSUSED */ |
| 316 | int |
| 317 | ipf_p_dns_inout(void *arg, fr_info_t *fin, ap_session_t *aps, nat_t *nat) |
| 318 | { |
| 319 | ipf_dns_softc_t *softd = arg; |
| 320 | ipf_dns_hdr_t *dns; |
| 321 | dnsinfo_t *di; |
| 322 | char *data; |
| 323 | int dlen, q, rc = 0; |
| 324 | |
| 325 | if (fin->fin_dlen < sizeof(*dns)) |
| 326 | return APR_ERR(1); |
| 327 | |
| 328 | dns = (ipf_dns_hdr_t *)((char *)fin->fin_dp + sizeof(udphdr_t)); |
| 329 | |
| 330 | q = dns->dns_qdcount; |
| 331 | |
| 332 | data = (char *)(dns + 1); |
| 333 | dlen = fin->fin_dlen - sizeof(*dns) - sizeof(udphdr_t); |
| 334 | |
| 335 | di = aps->aps_data; |
| 336 | |
| 337 | READ_ENTER(&softd->ipf_p_dns_rwlock); |
| 338 | MUTEX_ENTER(&di->dnsi_lock); |
| 339 | |
| 340 | for (; (dlen > 0) && (q > 0); q--) { |
| 341 | int len; |
| 342 | |
| 343 | len = ipf_p_dns_get_name(softd, data, dlen, di->dnsi_buffer, |
| 344 | sizeof(di->dnsi_buffer)); |
| 345 | if (len == 0) { |
| 346 | rc = 1; |
| 347 | break; |
| 348 | } |
| 349 | rc = ipf_p_dns_allow_query(softd, di); |
| 350 | if (rc != 0) |
| 351 | break; |
| 352 | data += len; |
| 353 | dlen -= len; |
| 354 | } |
| 355 | MUTEX_EXIT(&di->dnsi_lock); |
| 356 | RWLOCK_EXIT(&softd->ipf_p_dns_rwlock); |
| 357 | |
| 358 | return APR_ERR(rc); |
| 359 | } |
| 360 | |
| 361 | |
| 362 | /* ARGSUSED */ |
| 363 | int |
| 364 | ipf_p_dns_match(fr_info_t *fin, ap_session_t *aps, nat_t *nat) |
| 365 | { |
| 366 | dnsinfo_t *di = aps->aps_data; |
| 367 | ipf_dns_hdr_t *dnh; |
| 368 | |
| 369 | if ((fin->fin_dlen < sizeof(u_short)) || (fin->fin_flx & FI_FRAG)) |
| 370 | return -1; |
| 371 | |
| 372 | dnh = (ipf_dns_hdr_t *)((char *)fin->fin_dp + sizeof(udphdr_t)); |
| 373 | if (((dnh->dns_id[0] << 8) | dnh->dns_id[1]) != di->dnsi_id) |
| 374 | return -1; |
| 375 | return 0; |
| 376 | } |
| 377 | |