/* Copyright (c) 2006 Simon Kelley

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; version 2 dated June, 1991.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
*/

/* dhcp_release <interface> <address> <MAC address> <client_id>
   MUST be run as root - will fail otherwise. */

/* Send a DHCPRELEASE message via the specified interface 
   to tell the local DHCP server to delete a particular lease. 
   
   The interface argument is the interface in which a DHCP
   request _would_ be received if it was coming from the client, 
   rather than being faked up here.
   
   The address argument is a dotted-quad IP addresses and mandatory. 
   
   The MAC address is colon separated hex, and is mandatory. It may be 
   prefixed by an address-type byte followed by -, eg

   10-11:22:33:44:55:66

   but if the address-type byte is missing it is assumed to be 1, the type 
   for ethernet. This encoding is the one used in dnsmasq lease files.

   The client-id is optional. If it is "*" then it treated as being missing.
*/

#include <sys/types.h> 
#include <netinet/in.h>
#include <net/if.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <net/if_arp.h>
#include <sys/ioctl.h>
#include <linux/types.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <errno.h>

#define DHCP_CHADDR_MAX          16
#define BOOTREQUEST              1
#define DHCP_COOKIE              0x63825363
#define OPTION_SERVER_IDENTIFIER 54
#define OPTION_CLIENT_ID         61
#define OPTION_MESSAGE_TYPE      53
#define OPTION_END               255
#define DHCPRELEASE              7
#define DHCP_SERVER_PORT         67

typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

struct dhcp_packet {
  u8 op, htype, hlen, hops;
  u32 xid;
  u16 secs, flags;
  struct in_addr ciaddr, yiaddr, siaddr, giaddr;
  u8 chaddr[DHCP_CHADDR_MAX], sname[64], file[128];
  u32 cookie;
  unsigned char options[308];
};

static struct iovec iov;

static int expand_buf(struct iovec *iov, size_t size)
{
  void *new;

  if (size <= iov->iov_len)
    return 1;

  if (!(new = malloc(size)))
    {
      errno = ENOMEM;
      return 0;
    }

  if (iov->iov_base)
    {
      memcpy(new, iov->iov_base, iov->iov_len);
      free(iov->iov_base);
    }

  iov->iov_base = new;
  iov->iov_len = size;

  return 1;
}

static ssize_t netlink_recv(int fd)
{
  struct msghdr msg;
  ssize_t rc;

  msg.msg_control = NULL;
  msg.msg_controllen = 0;
  msg.msg_name = NULL;
  msg.msg_namelen = 0;
  msg.msg_iov = &iov;
  msg.msg_iovlen = 1;
    
  while (1)
    {
      msg.msg_flags = 0;
      while ((rc = recvmsg(fd, &msg, MSG_PEEK)) == -1 && errno == EINTR);
      
      /* 2.2.x doesn't support MSG_PEEK at all, returning EOPNOTSUPP, so we just grab a 
         big buffer and pray in that case. */
      if (rc == -1 && errno == EOPNOTSUPP)
        {
          if (!expand_buf(&iov, 2000))
            return -1;
          break;
        }
      
      if (rc == -1 || !(msg.msg_flags & MSG_TRUNC))
        break;
            
      if (!expand_buf(&iov, iov.iov_len + 100))
        return -1;
    }

  /* finally, read it for real */
  while ((rc = recvmsg(fd, &msg, 0)) == -1 && errno == EINTR);
  
  return rc;
}

static int parse_hex(char *in, unsigned char *out, int maxlen, int *mac_type)
{
  int i = 0;
  char *r;
    
  if (mac_type)
    *mac_type = 0;
  
  while (maxlen == -1 || i < maxlen)
    {
      for (r = in; *r != 0 && *r != ':' && *r != '-'; r++);
      if (*r == 0)
        maxlen = i;
      
      if (r != in )
        {
          if (*r == '-' && i == 0 && mac_type)
           {
              *r = 0;
              *mac_type = strtol(in, NULL, 16);
              mac_type = NULL;
           }
          else
            {
              *r = 0;
	      out[i] = strtol(in, NULL, 16);
              i++;
            }
        }
      in = r+1;
    }
    return i;
}

static int is_same_net(struct in_addr a, struct in_addr b, struct in_addr mask)
{
  return (a.s_addr & mask.s_addr) == (b.s_addr & mask.s_addr);
}

static struct in_addr find_interface(struct in_addr client, int fd, unsigned int index, int ifrfd, struct ifreq *ifr)
{
  struct sockaddr_nl addr;
  struct nlmsghdr *h;
  ssize_t len;
 
  struct {
    struct nlmsghdr nlh;
    struct rtgenmsg g; 
  } req;

  addr.nl_family = AF_NETLINK;
  addr.nl_pad = 0;
  addr.nl_groups = 0;
  addr.nl_pid = 0; /* address to kernel */

  req.nlh.nlmsg_len = sizeof(req);
  req.nlh.nlmsg_type = RTM_GETADDR;
  req.nlh.nlmsg_flags = NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST | NLM_F_ACK; 
  req.nlh.nlmsg_pid = 0;
  req.nlh.nlmsg_seq = 1;
  req.g.rtgen_family = AF_INET; 

  if (sendto(fd, (void *)&req, sizeof(req), 0, 
	     (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
      perror("sendto failed");
      exit(1);
    }
  
  while (1)
    {
      if ((len = netlink_recv(fd)) == -1)
	{
	  perror("netlink");
	  exit(1);
	}

      for (h = (struct nlmsghdr *)iov.iov_base; NLMSG_OK(h, (size_t)len); h = NLMSG_NEXT(h, len))
	if (h->nlmsg_type == NLMSG_DONE)
          {
	    /* No match found, return first address as src/dhcp.c code does */
	    ifr->ifr_addr.sa_family = AF_INET;
	    if (ioctl(ifrfd, SIOCGIFADDR, ifr) != -1)
	      return ((struct sockaddr_in *)&ifr->ifr_addr)->sin_addr;
	    else
	      {
		fprintf(stderr, "error: local IPv4 address not found\n");
		exit(1);
	      }
          }
	else if (h->nlmsg_type == RTM_NEWADDR)
          {
            struct ifaddrmsg *ifa = NLMSG_DATA(h);  
            struct rtattr *rta;
            unsigned int len1 = h->nlmsg_len - NLMSG_LENGTH(sizeof(*ifa));
            
            if (ifa->ifa_index == index && ifa->ifa_family == AF_INET)
              {
                struct in_addr netmask, addr;
                
                netmask.s_addr = htonl(0xffffffff << (32 - ifa->ifa_prefixlen));
                addr.s_addr = 0;
                
                for (rta = IFA_RTA(ifa); RTA_OK(rta, len1); rta = RTA_NEXT(rta, len1))
		  if (rta->rta_type == IFA_LOCAL)
		    addr = *((struct in_addr *)(rta+1));
		
                if (addr.s_addr && is_same_net(addr, client, netmask))
		  return addr;
	      }
	  }
    }
 
  exit(0);
}

int main(int argc, char **argv)
{ 
  struct in_addr server, lease;
  int mac_type;
  struct dhcp_packet packet;
  unsigned char *p = packet.options;
  struct sockaddr_in dest;
  struct ifreq ifr;
  int fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP);
  int nl = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);

  if (argc < 4 || argc > 5)
    { 
      fprintf(stderr, "usage: dhcp_release <interface> <addr> <mac> [<client_id>]\n");
      exit(1);
    }

  if (fd == -1 || nl == -1)
    {
      perror("cannot create socket");
      exit(1);
    }
  
  /* This voodoo fakes up a packet coming from the correct interface, which really matters for 
     a DHCP server */
  memset(&ifr, 0, sizeof(ifr));
  strncpy(ifr.ifr_name, argv[1], sizeof(ifr.ifr_name)-1);
  ifr.ifr_name[sizeof(ifr.ifr_name)-1] = '\0';
  if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, &ifr, sizeof(ifr)) == -1)
    {
      perror("cannot setup interface");
      exit(1);
    }
  
  if (inet_pton(AF_INET, argv[2], &lease.s_addr) < 1)
    {
      perror("invalid ip address");
      exit(1);
    }
  
  server = find_interface(lease, nl, if_nametoindex(argv[1]), fd, &ifr);
  
  memset(&packet, 0, sizeof(packet));
 
  packet.hlen = parse_hex(argv[3], packet.chaddr, DHCP_CHADDR_MAX, &mac_type);
  if (mac_type == 0)
    packet.htype = ARPHRD_ETHER;
  else
    packet.htype = mac_type;

  packet.op = BOOTREQUEST;
  packet.ciaddr = lease;
  packet.cookie = htonl(DHCP_COOKIE);

  *(p++) = OPTION_MESSAGE_TYPE;
  *(p++) = 1;
  *(p++) = DHCPRELEASE;

  *(p++) = OPTION_SERVER_IDENTIFIER;
  *(p++) = sizeof(server);
  memcpy(p, &server, sizeof(server));
  p += sizeof(server);

  if (argc == 5 && strcmp(argv[4], "*") != 0)
    {
      unsigned int clid_len = parse_hex(argv[4], p+2, 255, NULL);
      *(p++) = OPTION_CLIENT_ID;
      *(p++) = clid_len;
      p += clid_len;
    }
  
  *(p++) = OPTION_END;
 
  dest.sin_family = AF_INET;
  dest.sin_port = ntohs(DHCP_SERVER_PORT);
  dest.sin_addr = server;

  if (sendto(fd, &packet, sizeof(packet), 0, 
	     (struct sockaddr *)&dest, sizeof(dest)) == -1)
    {
      perror("sendto failed");
      exit(1);
    }

  return 0;
}
