8.4 Restricting Access Based on Hostname or IP Address

8.4.1 Problem

You want to restrict access to the network based on hostname or IP address.

8.4.2 Solution

First, get the IP address of the remote connection, and verify that the address has a hostname associated with it. To ensure that the hostname is not being spoofed (i.e., the address reverses to one hostname, but the hostname does not map to that IP address), look up the hostname and compare the resulting IP address with the IP address of the connection; if the IP addresses do not match, the hostname is likely being spoofed.

Next, compare the IP address and/or hostname with a set of rules that determine whether to grant the remote connection access.

8.4.3 Discussion

Restricting access based on the remote connection's IP address or hostname is risky at best. The hostname and/or IP address could be spoofed, or the remote system could be compromised with an attacker in control. Address-based access control is no substitute for strong authentication methods.

The first step in restricting access from the network based on hostname or IP address is to ensure that the remote connection is not engaging in a DNS spoofing attack. No foolproof method exists for guaranteeing that the address is not being spoofed, though the code presented here can provide a reasonable assurance for most cases. In particular, if the DNS server for the domain that an IP address reverse-maps to has been compromised, there is no way to know.

The first code listing that we present implements a worker function, check_spoofdns( ), which performs a set of DNS lookups and compares the results. The first lookup retrieves the hostname to which an IP address maps. An IP address does not necessarily have to reverse-map to a hostname, so if this first lookup yields no mapping, it is generally safe to assume that no spoofing is taking place.

If the IP address does map to a hostname, a lookup is performed on that hostname to retrieve the IP address or addresses to which it maps. The hostname should exist, but if it does not, the connection should be considered suspect. Although it is possible that something funny is going on with the remote connection, the lack of a name-to- address mapping could be innocent.

Each of the addresses returned by the hostname lookup is compared against the IP address of the remote connection. If the IP address of the remote connection is not matched, the likelihood of a spoofing attack is high, though still not guaranteed. If the IP address of the remote connection is matched, the code assumes that no spoofing attack is taking place.

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
   
#define SPC_ERROR_NOREVERSE   1 /* IP address does not map to a hostname */
#define SPC_ERROR_NOHOSTNAME  2 /* Reversed hostname does not exist      */
#define SPC_ERROR_BADHOSTNAME 3 /* IP addresses do not match             */
#define SPC_ERROR_HOSTDENIED  4 /* TCP/SPC Wrappers denied host access   */
   
static int check_spoofdns(int sockfd, struct sockaddr_in *addr, char **name) {
  int            addrlen, i;
  char           *hostname;
  struct hostent *he;
   
  *name = 0;
  for (;;) {
    addrlen = sizeof(struct sockaddr_in);
    if (getpeername(sockfd, (struct sockaddr *)addr, &addrlen) != -1) break;
    if (errno != EINTR && errno != EAGAIN) return -1;
  }
   
  for (;;) {
    he = gethostbyaddr((char *)&addr->sin_addr, sizeof(addr->sin_addr), AF_INET);
    if (he) break;
    if (h_errno =  = HOST_NOT_FOUND) {
      endhostent(  );
      return SPC_ERROR_NOREVERSE;
    }
    if (h_errno != TRY_AGAIN) {
      endhostent(  );
      return -1;
    }
  }
   
  hostname = strdup(he->h_name);
  for (;;) {
    if ((he = gethostbyname(hostname)) != 0) break;
    if (h_errno =  = HOST_NOT_FOUND) {
      endhostent(  );
      free(hostname);
      return SPC_ERROR_NOHOSTNAME;
    }
    if (h_errno != TRY_AGAIN) {
      endhostent(  );
      free(hostname);
      return -1;
    }
  }
   
  /* Check all IP addresses returned for the hostname.  If one matches, return
   * 0 to indicate that the address is not likely being spoofed.
   */
  for (i = 0;  he->h_addr_list[i];  i++)
    if (*(in_addr_t *)he->h_addr_list[i] =  = addr->sin_addr.s_addr) {
      *name = hostname;
      endhostent(  );
      return 0;
    }
   
  /* No matches.  Spoofing very likely */
  free(hostname);
  endhostent(  );
  return SPC_ERROR_BADHOSTNAME;
}

The next code listing contains several worker functions as well as the function spc_host_init( ), which requires a single argument that is the name of a file from which access restriction information is to be read. The access restriction information is read from the file and stored in an in-memory list, which is then used by spc_host_check( ) (we'll describe that function shortly).

Access restriction information read by spc_host_init( ) is required to be in a very specific format. Whitespace is mostly ignored, and lines beginning with a hash mark (#) or a semicolon (;) are considered comments and ignored. Any other line in the file must begin with either "allow:" or "deny:" to indicate the type of rule.

Following the rule type is a whitespace-separated list of addresses that are to be either allowed or denied access. Addresses may be hostnames or IP addresses. IP addresses may be specified as an address and mask or simply as an address. In the former case, the address may contain up to four parts, where each part must be expressed in decimal (ranging from 0 to 255), and a period (.) must be used to separate them. A forward slash (/) separates the address from the mask, and the mask is expressed as the number of bits to set. Table 8-1 lists example representations that are accepted as valid.

Table 8-1. Example address representations accepted by spc_host_init( )

Representation

Meaning

www.oreilly.com

The host to which the reverse-and-forward maps www.oreilly.com will be matched.

12.109.142.4

Only the specific address 12.109.142.4 will be matched.

10/24

Any address starting with 10 will be matched.

192.168/16

Any address starting with 192.168 will be matched.

If any errors are encountered when parsing the access restriction data file, a message containing the name of the file and the line number is printed. Parsing of the file then continues on the next line. Fatal errors (e.g., out of memory) are also noted in a similar fashion, but parsing terminates immediately and any data successfully parsed so far is thrown away.

When spc_host_init( ) completes successfully (even if parse errors are encountered), it will return 1; otherwise, it will return 0.

#define SPC_HOST_ALLOW 1
#define SPC_HOST_DENY  0
   
typedef struct {
  int       action;
  char      *name;
  in_addr_t addr;
  in_addr_t mask;
} spc_hostrule_t;
           
static int            spc_host_rulecount;
static spc_hostrule_t *spc_host_rules;
   
static int add_rule(spc_hostrule_t *rule) {
  spc_hostrule_t *tmp;
   
  if (!(spc_host_rulecount % 256)) {
    if (!(tmp = (spc_hostrule_t *)realloc(spc_host_rules,
                  sizeof(spc_host_rulecount) * (spc_host_rulecount + 256))))
      return 0;
    spc_host_rules = tmp;
  }   
  spc_host_rules[spc_host_rulecount++] = *rule;
  return 1;
}     
      
static void free_rules(void) {
  int i;
   
  if (spc_host_rules) {
    for (i = 0;  i < spc_host_rulecount;  i++)
      if (spc_host_rules[i].name) free(spc_host_rules[i].name);
    free(spc_host_rules);
    spc_host_rulecount = 0;
    spc_host_rules = 0;
  }
}
   
static in_addr_t parse_addr(char *str) {
  int       shift = 24;
  char      *tmp;
  in_addr_t addr = 0;
   
  for (tmp = str;  *tmp;  tmp++) {
    if (*tmp =  = '.') {
      *tmp = 0;
      addr |= (atoi(str) << shift);
      str = tmp + 1;
      if ((shift -= 8) < 0) return INADDR_NONE;
    } else if (!isdigit(*tmp)) return INADDR_NONE;
  }
  addr |= (atoi(str) << shift);
  return htonl(addr);
}
   
static in_addr_t make_mask(int bits) {
  in_addr_t mask;
   
  bits = (bits < 0 ? 0 : (bits > 32 ? 32 : bits));
  for (mask = 0;  bits--;  mask |= (1 << (31 - bits)));
   
  return htonl(mask);
}
   
int spc_host_init(const char *filename) {
  int            lineno = 0;
  char           *buf, *p, *slash, *tmp;
  FILE           *f;
  size_t         bufsz, len = 0;
  spc_hostrule_t rule;
   
  if (!(f = fopen(filename, "r"))) return 0;
  if (!(buf = (char *)malloc(256))) {
    fclose(f);
    return 0;
  }
  while (fgets(buf + len, bufsz - len, f) != 0) {
    len += strlen(buf + len);
    if (buf[len - 1] != '\n') {
      if (!(buf = (char *)realloc((tmp = buf), bufsz += 256))) {
        fprintf(stderr, "%s line %d: out of memory\n", filename, ++lineno);
        free(tmp);
        fclose(f); 
        free_rules(  );
        return 0;
      }
      continue;
    }
    buf[--len] = 0;
    lineno++;
    for (tmp = buf;  *tmp && isspace(*tmp);  tmp++) len--;
    while (len && isspace(tmp[len - 1])) len--;
    tmp[len] = 0;
    len = 0;
    if (!tmp[0] || tmp[0] =  = '#' || tmp[0] =  = ';') continue;
  
    memset(&rule, 0, sizeof(rule));
    if (strncasecmp(tmp, "allow:", 6) && strncasecmp(tmp, "deny:", 5)) {
      fprintf(stderr, "%s line %d: parse error; continuing anyway.\n",
              filename, lineno);
      continue;
    } 
  
    if (!strncasecmp(tmp, "deny:", 5)) {
      rule.action = SPC_HOST_DENY;
      tmp += 5;
    } else {
      rule.action = SPC_HOST_ALLOW;
      tmp += 6;
    }
    while (*tmp && isspace(*tmp)) tmp++; 
    if (!*tmp) {  
      fprintf(stderr, "%s line %d: parse error; continuing anyway.\n",
              filename, lineno);
      continue;
    }
   
    for (p = tmp;  *p;  tmp = p) {
      while (*p && !isspace(*p)) p++;
      if (*p) *p++ = 0;
      if ((slash = strchr(tmp, '/')) != 0) {
        *slash++ = 0;
        rule.name = 0;
        rule.addr = parse_addr(tmp);
        rule.mask = make_mask(atoi(slash));
      } else {
        if (inet_addr(tmp) =  = INADDR_NONE) rule.name = strdup(tmp);
        else {
          rule.name = 0;
          rule.addr = inet_addr(tmp);
          rule.mask = 0xFFFFFFFF;
        }
      }
      if (!add_rule(&rule)) {
        fprintf(stderr, "%s line %d: out of memory\n", filename, lineno);
        free(buf);
        fclose(f);
        free_rules(  );
        return 0;
      }
    }
  }
  free(buf);
  fclose(f);
  return 1;
}

Finally, the function spc_host_check( ) performs access restriction checks. If the remote connection should be allowed, the return will be 0. If some kind of error unrelated to access restriction occurs (e.g., out of memory, bad socket descriptor, etc.), the return will be -1. Otherwise, one of the following error constants may be returned:

SPC_ERROR_NOREVERSE

Indicates that the IP address of the remote connection has no reverse mapping. If strict checking is not being done, this error code will not be returned.

SPC_ERROR_NOHOSTNAME

Indicates that the IP address of the remote connection reverse-maps to a hostname that does not map to any IP address. This condition does not necessarily indicate that a DNS spoofing attack is taking place; however, we do recommend that you treat it as such.

SPC_ERROR_BADHOSTNAME

Indicates that the likelihood of a DNS spoofing attack is high. The IP address of the remote connection does not match any of the IP addresses that its hostname maps to.

SPC_ERROR_HOSTDENIED

Indicates that no DNS spoofing attack is believed to be taking place, but the access restriction rules have matched the remote address with a deny rule.

The function spc_host_check( ) has the following signature:

int spc_host_check(int sockfd, int strict, int action);

This function has the following arguments:

sockfd

Socket descriptor for the remote connection. This argument is used solely to obtain the IP address of the remote connection.

strict

Boolean value indicating whether strict DNS spoofing checks are to be done. If this argument is specified as 0, IP addresses that do not have a reverse mapping will be allowed; otherwise, SPC_ERROR_NOREVERSE will be returned for such connections.

action

Default action to take if the remote IP address does not match any of the defined access restriction rules. It may be specified as either SPC_HOST_ALLOW or SPC_HOST_DENY. Any other value will be treated as equivalent to SPC_HOST_DENY.

You may use spc_host_check( ) without using spc_host_init( ), in which case it will essentially only perform DNS spoofing checks. If you do not use spc_host_init( ), spc_host_check( ) will have an empty rule set, and it will always use the default action if the remote connection passes the DNS spoofing checks.

int spc_host_check(int sockfd, int strict, int action) {
  int                i, rc;
  char               *hostname;
  struct sockaddr_in addr;
   
  if ((rc = check_spoofdns(sockfd, &addr, &hostname)) =  = -1) return -1;
  if (rc && (rc != SPC_ERROR_NOREVERSE || strict)) return rc;
   
  for (i = 0;  i < spc_host_rulecount;  i++) {
    if (spc_host_rules[i].name) {
      if (hostname && !strcasecmp(hostname, spc_host_rules[i].name)) {
        free(hostname);
        return (spc_host_rules[i].action =  = SPC_HOST_ALLOW);
      }
    } else {
      if ((addr.sin_addr.s_addr & spc_host_rules[i].mask) =  =
          spc_host_rules[i].addr) {
        free(hostname);
        return (spc_host_rules[i].action =  = SPC_HOST_ALLOW);
      }
    }
  }
  
  if (hostname) free(hostname);
  return (action =  = SPC_HOST_ALLOW);
}