/*
 dhcp_release6 --iface <interface> --client-id <client-id> --server-id
 server-id --iaid <iaid>  --ip <IP>  [--dry-run] [--help]
 MUST be run as root - will fail otherwise
 */

/* Send a DHCPRELEASE message  to IPv6 multicast address  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 client-id argument is colon-separated hex string and mandatory. Normally
 it can be found in leases file both on client and server

 The server-id argument is colon-separated hex string and mandatory. Normally
 it can be found in leases file both on client and server.
 
 The iaid argument is numeric string and mandatory. Normally
 it can be found in leases file both on client and server.
 
 IP is an IPv6 address to release
 
 If --dry-run is specified, dhcp_release6 just prints hexadecimal representation of
 packet to send to stdout and exits.
 
 If --help is specified, dhcp_release6 print usage information to stdout and exits
 
 
 
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <getopt.h>
#include <errno.h>
#include <unistd.h>

#define NOT_REPLY_CODE 115
typedef unsigned char u8;
typedef unsigned short u16;
typedef unsigned int u32;

enum DHCP6_TYPES
  {
    SOLICIT = 1,
    ADVERTISE = 2,
    REQUEST = 3,
    CONFIRM = 4,
    RENEW = 5,
    REBIND = 6,
    REPLY = 7,
    RELEASE = 8,
    DECLINE = 9,
    RECONFIGURE = 10,
    INFORMATION_REQUEST = 11,
    RELAY_FORW = 12,
    RELAY_REPL = 13
    
  };

enum DHCP6_OPTIONS
  {
    CLIENTID = 1,
    SERVERID = 2,
    IA_NA = 3,
    IA_TA = 4,
    IAADDR = 5,
    ORO = 6,
    PREFERENCE = 7,
    ELAPSED_TIME = 8,
    RELAY_MSG = 9,
    AUTH = 11,
    UNICAST = 12,
    STATUS_CODE = 13,
    RAPID_COMMIT = 14,
    USER_CLASS = 15,
    VENDOR_CLASS = 16,
    VENDOR_OPTS = 17,
    INTERFACE_ID = 18,
    RECONF_MSG = 19,
    RECONF_ACCEPT = 20,
  };

enum DHCP6_STATUSES
  {
    SUCCESS = 0,
    UNSPEC_FAIL = 1,
    NOADDR_AVAIL=2,
    NO_BINDING  = 3,
    NOT_ON_LINK = 4,
    USE_MULTICAST =5
  };

static struct option longopts[] = {
  {"ip", required_argument, 0, 'a' },
  {"server-id", required_argument, 0, 's' },
  {"client-id", required_argument, 0, 'c' },
  {"iface", required_argument, 0, 'n' },
  {"iaid", required_argument, 0, 'i' },
  {"dry-run", no_argument, 0, 'd' },
  {"help", no_argument, 0, 'h' },
  {0,     0,           0,   0 }
};

const short DHCP6_CLIENT_PORT = 546;
const short DHCP6_SERVER_PORT = 547;

const char*  DHCP6_MULTICAST_ADDRESS = "ff02::1:2";

struct dhcp6_option {
  uint16_t type;
  uint16_t len;
  char  value[1024];
};

struct dhcp6_iaaddr_option {
  uint16_t type;
  uint16_t len;
  struct in6_addr ip;
  uint32_t preferred_lifetime;
  uint32_t valid_lifetime;
};

struct dhcp6_iana_option {
  uint16_t type;
  uint16_t len;
  uint32_t iaid;
  uint32_t t1;
  uint32_t t2;
  char options[1024];
};


struct dhcp6_packet {
  size_t len;
  char buf[2048];  
};

size_t pack_duid(const char* str, char* dst)
{
  char* tmp = strdup(str);
  char* tmp_to_free = tmp;
  char *ptr;
  uint8_t write_pos = 0;
  while ((ptr = strtok (tmp, ":")))
    {
      dst[write_pos] = (uint8_t) strtol(ptr, NULL, 16);
      write_pos += 1;
      tmp = NULL;
    }
  
  free(tmp_to_free);
  return write_pos;
}

struct dhcp6_option create_client_id_option(const char* duid)
{
  struct dhcp6_option option;
  option.type = htons(CLIENTID);
  bzero(option.value, sizeof(option.value));
  option.len  = htons(pack_duid(duid, option.value));
  return option;
}

struct dhcp6_option create_server_id_option(const char* duid)
{
  struct dhcp6_option   option;
  option.type = htons(SERVERID);
  bzero(option.value, sizeof(option.value));
  option.len  = htons(pack_duid(duid, option.value));
  return option;
}

struct dhcp6_iaaddr_option create_iaadr_option(const char* ip)
{
  struct dhcp6_iaaddr_option result;
  result.type =htons(IAADDR);
  /* no suboptions needed here, so length is 24  */
  result.len = htons(24);
  result.preferred_lifetime = 0;
  result.valid_lifetime = 0;
  int s = inet_pton(AF_INET6, ip, &(result.ip));
  if (s <= 0) {
    if (s == 0)
      fprintf(stderr, "Not in presentation format");
    else
      perror("inet_pton");
    exit(EXIT_FAILURE);
  }

  return result;
}

struct dhcp6_iana_option  create_iana_option(const char * iaid, struct dhcp6_iaaddr_option  ia_addr)
{
  struct dhcp6_iana_option  result;
  result.type = htons(IA_NA);
  result.iaid = htonl(atoi(iaid));
  result.t1 = 0;
  result.t2 = 0;
  result.len = htons(12 + ntohs(ia_addr.len) + 2 * sizeof(uint16_t));
  memcpy(result.options, &ia_addr, ntohs(ia_addr.len) + 2 * sizeof(uint16_t));
  return result;
}

struct dhcp6_packet create_release_packet(const char* iaid, const char* ip, const char* client_id, const char* server_id)
{
  struct dhcp6_packet result;
  bzero(result.buf, sizeof(result.buf));
  /* message_type */
  result.buf[0] = RELEASE;
  /* tx_id */
  bzero(result.buf+1, 3);
  
  struct dhcp6_option client_option = create_client_id_option(client_id);
  struct dhcp6_option server_option = create_server_id_option(server_id);
  struct dhcp6_iaaddr_option iaaddr_option = create_iaadr_option(ip);
  struct dhcp6_iana_option iana_option = create_iana_option(iaid, iaaddr_option);
  int offset = 4;
  memcpy(result.buf + offset, &client_option, ntohs(client_option.len) + 2*sizeof(uint16_t));
  offset += (ntohs(client_option.len)+ 2 *sizeof(uint16_t) );
  memcpy(result.buf + offset, &server_option, ntohs(server_option.len) + 2*sizeof(uint16_t) );
  offset += (ntohs(server_option.len)+ 2* sizeof(uint16_t));
  memcpy(result.buf + offset, &iana_option, ntohs(iana_option.len) + 2*sizeof(uint16_t) );
  offset += (ntohs(iana_option.len)+ 2* sizeof(uint16_t));
  result.len = offset;
  return result;
}

static void copy_option_string(char *dst, size_t dst_len, const char *src, size_t src_len)
{
  if (dst_len == 0)
    return;

  if (src_len >= dst_len)
    src_len = dst_len - 1;

  memcpy(dst, src, src_len);
  dst[src_len] = '\0';
}

uint16_t parse_iana_suboption(char* buf, size_t len)
{
  size_t current_pos = 0;
  char option_value[1024];
  while (current_pos < len)
    {
      if (len - current_pos < 2 * sizeof(uint16_t))
        return UNSPEC_FAIL;

      uint16_t option_type, option_len;
      memcpy(&option_type,buf + current_pos, sizeof(uint16_t));
      memcpy(&option_len,buf + current_pos + sizeof(uint16_t), sizeof(uint16_t));
      option_type = ntohs(option_type);
      option_len = ntohs(option_len);
      current_pos += 2 * sizeof(uint16_t);
      if (option_len > len - current_pos)
        return UNSPEC_FAIL;

      if (option_type == STATUS_CODE)
	{
	  uint16_t status;
	  if (option_len < sizeof(uint16_t))
	    return UNSPEC_FAIL;

	  memcpy(&status, buf + current_pos, sizeof(uint16_t));
	  status = ntohs(status);
	  if (status != SUCCESS)
	    {
	      copy_option_string(option_value, sizeof(option_value),
				 buf + current_pos + sizeof(uint16_t),
				 option_len - sizeof(uint16_t));
	      fprintf(stderr, "Error: %s\n", option_value);
            }
	  return status;
        }

      current_pos += option_len;
    }

  return -2;
}

int16_t parse_packet(char* buf, size_t len)
{
  int16_t ret = -1;
  if (len < 4)
    return UNSPEC_FAIL;

  uint8_t type = buf[0];
  /*skipping tx id. you need it, uncomment following line
    uint16_t tx_id = ntohs((buf[1] <<16) + (buf[2] <<8) + buf[3]);
  */
  size_t current_pos = 4;
  if (type != REPLY )
    return NOT_REPLY_CODE;
  
  char option_value[1024];
  while (current_pos < len)
    {
      if (len - current_pos < 2 * sizeof(uint16_t))
	return UNSPEC_FAIL;

      uint16_t option_type, option_len;
      memcpy(&option_type,buf + current_pos, sizeof(uint16_t));
      memcpy(&option_len,buf + current_pos + sizeof(uint16_t), sizeof(uint16_t));
      option_type = ntohs(option_type);
      option_len = ntohs(option_len);
      current_pos += 2 * sizeof(uint16_t);
      if (option_len > len - current_pos)
	return UNSPEC_FAIL;

      if (option_type == STATUS_CODE)
	{
	  uint16_t status;
	  if (option_len < sizeof(uint16_t))
	    return UNSPEC_FAIL;

	  memcpy(&status, buf + current_pos, sizeof(uint16_t));
	  status = ntohs(status);
	  if (status != SUCCESS)
	    {
	      copy_option_string(option_value, sizeof(option_value),
				 buf + current_pos + sizeof(uint16_t),
				 option_len - sizeof(uint16_t));
	      fprintf(stderr, "Error: %d %s\n", status, option_value);
	      return status;
	    }

	  /* Got success status, return that if there's no specific error in an IA_NA. */
	  ret = SUCCESS;   
        }

      if (option_type == IA_NA )
	{
	  if (option_len < 24)
	    return UNSPEC_FAIL;

	  uint16_t result = parse_iana_suboption(buf + current_pos +24, option_len -24);
	  if (result)
	    return result;
	}
      
      current_pos += option_len;
    }

  return ret;
}

void usage(const char* arg, FILE* stream)
{
  const char* usage_string ="--ip IPv6 --iface IFACE --server-id SERVER_ID --client-id CLIENT_ID --iaid IAID [--dry-run] | --help";
  fprintf (stream, "Usage: %s %s\n", arg, usage_string);   
}

static void fail_fatal(const char *errstr, int exitcode)
{
  perror(errstr);
  exit(exitcode);
}

int send_release_packet(const char* iface, struct dhcp6_packet* packet)
{
  struct sockaddr_in6 server_addr, client_addr;
  char response[1400];
  int sock = socket(PF_INET6, SOCK_DGRAM, 0);
  int i = 0;
  if (sock < 0)
    {
      perror("creating socket");
      return -1;
    }
  
    if (setsockopt(sock, SOL_SOCKET, 25, iface, strlen(iface)) == -1)
      {
        perror("SO_BINDTODEVICE");
        close(sock);
        return -1;
      }
    
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin6_family = AF_INET6;
    client_addr.sin6_family = AF_INET6;
    client_addr.sin6_port = htons(DHCP6_CLIENT_PORT);
    client_addr.sin6_flowinfo = 0;
    client_addr.sin6_scope_id =0;
    if (inet_pton(AF_INET6, "::", &client_addr.sin6_addr) <= 0)
      fail_fatal("inet_pton", 5);
    if (bind(sock, (struct sockaddr*)&client_addr, sizeof(struct sockaddr_in6)) != 0)
      perror("bind"); /* continue on bind error */
    if (inet_pton(AF_INET6, DHCP6_MULTICAST_ADDRESS, &server_addr.sin6_addr) <= 0)
      fail_fatal("inet_pton", 5);
    server_addr.sin6_port = htons(DHCP6_SERVER_PORT);
    ssize_t recv_size = 0;
    int result;
    for (i = 0; i < 5; i++)
      {
        if (sendto(sock, packet->buf, packet->len, 0, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
	  fail_fatal("sendto failed", 4);
	
        recv_size = recvfrom(sock, response, sizeof(response), MSG_DONTWAIT, NULL, 0);
        if (recv_size == -1)
	  {
            if (errno == EAGAIN)
	      {
		sleep(1);
		continue;
	      }
	    else
	      {
                perror("recvfrom");
		result = UNSPEC_FAIL;
	      }
	  }
	else
	  {
	    result = parse_packet(response, recv_size);
	    if (result == NOT_REPLY_CODE)
	      {
		sleep(1);
		continue;
	      }
	  }
        close(sock);
        return result;
      }

    close(sock);
    fprintf(stderr, "Response timed out\n");
    return -1;   
}


int main(int argc, char *  const argv[])
{
  const char* UNINITIALIZED = "";
  const char* iface = UNINITIALIZED;
  const char* ip = UNINITIALIZED;
  const char* client_id = UNINITIALIZED;
  const char* server_id = UNINITIALIZED;
  const char* iaid = UNINITIALIZED;
  int dry_run = 0;
  while (1)
    {
      int option_index = 0;
      int c = getopt_long(argc, argv, "a:s:c:n:i:hd", longopts, &option_index);
      if (c == -1)
	break;
        
      switch(c)
	{
	case 0:
	  if (longopts[option_index].flag !=0)
	    break;
	  
	  printf ("option %s", longopts[option_index].name);
	  if (optarg)
	    printf (" with arg %s", optarg);
	  printf ("\n");
	  break;

	case 'i':
	  iaid = optarg;
	  break;
	case 'n':
	  iface = optarg;
	  break;
	case 'a':
	  ip = optarg;
	  break;
	case 'c':
	  client_id = optarg;
	  break;
	case 'd':
	  dry_run = 1;
	  break;
	case 's':
	  server_id = optarg;
	  break;
	case 'h':
	  usage(argv[0], stdout);
	  return 0;
	case '?':
	  usage(argv[0], stderr);
	  return -1;
	default:
	  abort();
	  
        }
    }
  
  if (iaid == UNINITIALIZED)
    {
      fprintf(stderr, "Missing required iaid parameter\n");
      usage(argv[0], stderr);
      return -1;
    }
  
    if (server_id == UNINITIALIZED)
      {
        fprintf(stderr, "Missing required server-id parameter\n");
        usage(argv[0], stderr);
        return -1;
      }
    
    if (client_id == UNINITIALIZED)
      {
        fprintf(stderr, "Missing required client-id parameter\n");
        usage(argv[0], stderr);
        return -1;
      }
    
    if (ip == UNINITIALIZED)
      {
        fprintf(stderr, "Missing required ip parameter\n");
        usage(argv[0], stderr);
        return -1;
      }
    
    if (iface == UNINITIALIZED)
      {
	fprintf(stderr, "Missing required iface parameter\n");
        usage(argv[0], stderr);
        return -1;
      }

    
    
    struct dhcp6_packet packet = create_release_packet(iaid, ip, client_id, server_id);

    if (dry_run)
      {
        uint16_t i;

        for(i=0; i<packet.len; i++)
	  printf("%hhx", packet.buf[i]);
        
        printf("\n");
        return 0;
      }

    return send_release_packet(iface, &packet);
}
