working protocol implementation; with caveats

t1 and t2 are not handled properly in edge cases
retry intervals and limits are not completely according to the spec
we don't handle multiple prefixes at the moment nor do we handle lifetimes
M MODULE/Protocols.pmod/DHCPv6.pmod/DHCPMessage.pike +0 -2
@@ 51,10 51,8 @@ object decode_option(Stdio.Buffer buf) {
  //= Protocols.DHCPv6.option_type_mapping[option_type];
 
   if(!p) throw(Error.Generic("Invalid DHCP Option type " + option_type +".\n"));
-werror("p: %O\n", p);
   object option;
   string s = buf->read_hstring(2);
-  werror("s: %O\n", s);
   option = p(s);
   return option;
 }

          
M MODULE/Protocols.pmod/DHCPv6.pmod/DHCPOption.pike +3 -3
@@ 4,12 4,12 @@ inherit ADT.struct;
 
 protected variant void create(void|string(8bit) s) {
   ::create(s);
-werror("%O(%O)\n", this, s);
+//werror("%O(%O)\n", this, s);
   decode();
 }
 
 protected void decode() {
-werror("%O->decode()\n", this);
+//werror("%O->decode()\n", this);
   decode_body(this);
 }
 

          
@@ 25,7 25,7 @@ string encode() {
   add_int(option_type, 2);
   Stdio.Buffer b = Stdio.Buffer();    
   encode_body(b);
-werror("option type %O, length %O\n", option_type, sizeof(b));
+// werror("option type %O, length %O\n", option_type, sizeof(b));
   add_int(sizeof(b), 2);
   add(b->read());
   return read();

          
M MODULE/Protocols.pmod/DHCPv6.pmod/IAPD.pike +12 -3
@@ 9,13 9,15 @@ array options;
 multiset(int) option_types = (<>);
 
 mixed _encode() {
-  return (["t1": t1, "t2": t2, "iaid": (iaid)]);
+  return (["t1": t1, "t2": t2, "iaid": iaid, "options": options]);
 }
 
 mixed _decode(mixed x) {
   t1 = x->t1;
   t2 = x->t2;
   iaid = x->iaid;
+  options = x->options;
+  register_options();
 }
 
 protected variant void create(.IAID _iaid, int t1_secs, int t2_secs, array _options) {

          
@@ 23,6 25,12 @@ protected variant void create(.IAID _iai
   t1 = t1_secs;
   t2 = t2_secs;
   options = _options;
+  register_options();
+}
+
+void register_options() {
+  foreach(options;; object option) 
+    option_types[option->option_type] = 1;
 }
 
 protected void encode_body(Stdio.Buffer buf) {

          
@@ 30,8 38,9 @@ protected void encode_body(Stdio.Buffer 
   buf->add_int(t1, 4);
   buf->add_int(t2, 4);
   foreach(options;; object option) {
-mixed e = option->encode();
-werror("OPTION: %O => %O\n", option, e);
+werror("OPTION: %O", option);
+mixed e = option->encode(); 
+werror(" => %O\n", e);
      buf->add(e);
   }
 }

          
M MODULE/Protocols.pmod/DHCPv6.pmod/IA_PDOption.pike +9 -2
@@ 56,11 56,18 @@ object decode_option(Stdio.Buffer buf) {
 }
 
 void encode_body(Stdio.Buffer buf) {
-  buf->add_int(3600*24, 4);
-  buf->add_int(3600*36, 4);
+  buf->add_int(preferred_lifetime, 4);
+  buf->add_int(valid_lifetime, 4);
   buf->add_int(prefix, 1);
   if(!address)
     buf->add_int(0, 16);
   else
     buf->add_ints(Protocols.IPv6.parse_addr(address), 2);
 }
+
+int(0..1) eq(mixed other) {
+  return other->prefix == prefix && 
+         other->address == address && 
+         other->preferred_lifetime == preferred_lifetime && 
+         other->valid_lifetime == valid_lifetime;
+}

          
M MODULE/Protocols.pmod/DHCPv6.pmod/OptionRequestOption.pike +5 -9
@@ 1,9 1,9 @@ 
 inherit .DHCPOption;
 
 constant option_type = 6;
-array(array(int)) options;
+array(int) options;
 
-protected void create(array(array(int)) _options) {
+protected void create(array(int) _options) {
   options = _options;
 }
 

          
@@ 21,19 21,15 @@ protected variant void create() {
 
 void encode_body(Stdio.Buffer buf) {
 
-  foreach(options;; array option) {
-     if(sizeof(option) != 2) throw(Error.Generic("OptionRequestOptions must be a multiple of 2.\n"));
-     buf->add_int(option[0], 2);
-     buf->add_int(option[1], 2);
+  foreach(options;; int option) {
+     buf->add_int(option, 2);
   }
 }
 
 void decode_body(Stdio.Buffer buf) {
   options = ({});
   while(sizeof(buf)) {
-    array(int) option = allocate(2);
-    option[0] = buf->read_int(2);
-    option[1] = buf->read_int(2);
+    int option = buf->read_int(2);
     options+=({ option });
   }
 }

          
A => MODULE/Protocols.pmod/DHCPv6.pmod/RebindMessage.pike +3 -0
@@ 0,0 1,3 @@ 
+inherit .DHCPResponseMessage;
+
+constant message_type = .MESSAGE_REBIND;

          
A => MODULE/Protocols.pmod/DHCPv6.pmod/UnicastOption.pike +28 -0
@@ 0,0 1,28 @@ 
+inherit .DHCPOption;
+
+constant option_type = 12;
+
+string address;
+
+mixed _encode() {
+  return ([ "address": address ]);
+}
+
+void _decode(mixed x) {
+  address = x->address;
+}
+
+protected variant void create(string ipv6address) {
+   address = ipv6address;
+}
+
+void encode_body(Stdio.Buffer buf) {
+  if(!address)
+    buf->add_int(0, 16);
+  else
+    buf->add_ints(Protocols.IPv6.parse_addr(address), 2);
+}
+
+void decode_body(Stdio.Buffer buf) {
+  address = Protocols.IPv6.format_addr_short(array_sscanf(buf->read(16), "%2c"*8));
+}

          
M MODULE/Protocols.pmod/DHCPv6.pmod/module.pmod +4 -0
@@ 21,9 21,13 @@ constant OPTION_CLIENT_IDENTIFIER = 1;
 constant OPTION_SERVER_IDENTIFIER = 2;
 constant OPTION_OPTION_REQUEST = 6;
 constant OPTION_ELAPSED_TIME = 8;
+constant OPTION_UNICAST = 12;
 constant OPTION_STATUS_CODE = 13;
 
 constant STATUS_NO_PREFIX_AVAILABLE = 6;
+constant STATUS_NO_BINDING = 3;
+constant STATUS_SUCCESS = 0;
+constant STATUS_UNSPECIFIED_FAILURE = 1;
 
 protected void create() {
   foreach(values(Protocols.DHCPv6);; mixed p) {

          
M dhcpv6_pd.pike +252 -32
@@ 9,6 9,10 @@ constant AWAIT_REPLY = 2;
 
 constant DHCP_BROADCAST_ADDRESS = "FF02::1:2";
 
+constant SOL_MAX_DELAY = 1;
+constant CNF_MAX_DELAY = 1;
+constant REQ_MAX_RC = 10;
+
 string leasefile = "/var/run/dhcpv6_pd_leases";
 string v6if = "net0";
 string identifier;

          
@@ 19,13 23,16 @@ mapping lease_data = ([]);
 int current_state = NOT_AWAITING;
 int current_txn;
 mixed time_out_id;
+mixed t1_call_out;
+mixed t2_call_out;
 float last_timeout = 0.0;
+int keep_trying = 1;
 
 Stdio.UDP dhcp;
 DUID duid;
 IAID iaid;
 
-IAPD current_iapd;
+
 int current_iapd_confirmed;
 
 int main(int argc, array argv) {

          
@@ 44,10 51,14 @@ int main(int argc, array argv) {
     mapping ld;
     mixed e = catch(ld = decode_value(leases));
     if(e) werror("Unable to decode lease data. Backtrace follows.\n%s\n", describe_backtrace(e));
-      else {
+    else if(ld->confirmed) {
       werror("Have leases for Prefix Delegation, will attempt to reconfirm them.\n");
       lease_data = ld;
+      werror("Lease data: %O\n", lease_data);
       have_leases = 1;
+    } else {
+      werror("Have leases, but they are not marked as confirmed. Will attempt to solicit new leases.\n");
+      rebind_failed();
     }
   }
 

          
@@ 59,9 70,10 @@ int main(int argc, array argv) {
 
   if(!have_leases) {
     werror("Scheduling lease solicitation...\n");
-    call_out(begin_solicit, 5, 0);
+    call_out(begin_solicit, random((float)SOL_MAX_DELAY), 0);
   } else {
     werror("Scheduling lease confirmation...\n");
+    call_out(begin_rebind, random((float)CNF_MAX_DELAY), 0);
   }
   return -1;
 }

          
@@ 90,53 102,114 @@ void handle_advertise_message(AdvertiseM
   if(is_actionable_advertise(message)) {
     current_state = NOT_AWAITING;
     if(time_out_id) remove_call_out(time_out_id);
-    
-    current_iapd = message->get_option(OPTION_IAPD);
-    mapping lease_data = ([]);
-    lease_data->current_iapd = current_iapd;
-    lease_data->v6if = v6if;
-    lease_data->identifier = identifier;
-    lease_data->updated = time();
-    lease_data->server_identifier = message->get_option(OPTION_SERVER_IDENTIFIER);
-    lease_data->server_address = addr;
-    Stdio.write_file(leasefile, encode_value(lease_data));
+
+    write_lease(message, addr);
     begin_request(0);
   }
+}        
+
+void write_lease(DHCPMessage message, string addr, int|void confirmed) {
+  object current_iapd = message->get_option(OPTION_IAPD);
+  mapping ld = ([]);
+  ld->current_iapd = current_iapd;
+  ld->v6if = v6if;
+  ld->identifier = identifier;
+  ld->updated = time();
+  ld->server_identifier = message->get_option(OPTION_SERVER_IDENTIFIER);
+  ld->server_unicast = message->get_option(OPTION_UNICAST);
+  ld->server_address = addr;
+  ld->confirmed = confirmed;
+  lease_data = ld;
+  Stdio.write_file(leasefile, encode_value(lease_data));
 }
 
 void handle_reply_message(ReplyMessage message, string addr) {
   if(is_actionable_reply(message)) {
-    current_state = NOT_AWAITING;
-    if(time_out_id) remove_call_out(time_out_id);
+    int have_good_lease = 0;
+    object iapd = message->get_option(OPTION_IAPD);
+
+    foreach(iapd->options;; object option) {
+      if(option->prefix && option->preferred_lifetime)
+        have_good_lease = 1;
+    }
+
+    if(have_good_lease) {
+      current_state = NOT_AWAITING;
+      if(time_out_id) remove_call_out(time_out_id);
+
+      current_iapd_confirmed = 1;
+      object old_option, new_option;
+      if(lease_data && lease_data->confirmed)
+        old_option = lease_data->current_iapd->get_option(OPTION_IA_PDOPTION);
+      
+      write_lease(message, addr, 1);
+
+      if(lease_data && lease_data->confirmed)
+        new_option = lease_data->current_iapd->get_option(OPTION_IA_PDOPTION);
+      write_lease(message, addr, 1);
+      trigger_lease(new_option, old_option->eq(new_option));
+    }
+  }
+}
+
+void trigger_lease(object prefix_option, int has_changed) {
+  werror("LEASE COMPLETED: %O\n", lease_data);
+  if(t1_call_out) remove_call_out(t1_call_out);
+  if(t2_call_out) remove_call_out(t2_call_out);
 
-    current_iapd = message->get_option(OPTION_IAPD);
-  }
+  // TODO we don't handle prefix lifetimes properly; we should do that.
+  int t1 = lease_data->current_iapd->t1;
+  int t2 = lease_data->current_iapd->t2;
+
+  werror("RENEW scheduled for %d seconds from now: %s\n", t1, ctime(time() +  t1)); 
+  werror("REBIND scheduled for %d seconds from now: %s\n", t2, ctime(time() + t2)); 
+
+  t1_call_out = call_out(begin_renew, t1);
+  t2_call_out = call_out(begin_rebind, t2);
+
+  call_out(do_trigger_lease, 0, prefix_option, has_changed);
+}
+
+void do_trigger_lease(IA_PDOption allocation, int has_changed) {
+werror("do_trigger_lease(%O, %O)\n", allocation, has_changed);
+}
+
+void do_trigger_abandon(IA_PDOption allocation) {
+}
+
+void rebind_failed() {
+  werror("Abandoning lease.\n");
+  if(t1_call_out) remove_call_out(t1_call_out);
+  if(t2_call_out) remove_call_out(t2_call_out);
+
+  mapping ld = lease_data;
+
+  lease_data = ([]);
+  current_iapd_confirmed = 0;
+  
+  call_out(do_trigger_abandon, 0, ld->current_iapd->get_option(OPTION_IA_PDOPTION));
+  rm(leasefile); 
+  call_out(begin_solicit, random((float)SOL_MAX_DELAY), 0);
 }
 
 int(0..1) is_actionable_advertise(AdvertiseMessage message) {
 werror("is_actionable_advertise?\n");
 werror("options: %O\n", message->options);
   if(!message->has_option(OPTION_CLIENT_IDENTIFIER)) return 0;
-werror("have client identifier\n");
   if(!message->has_option(OPTION_SERVER_IDENTIFIER)) return 0;
-werror("have server identifier\n");
-  werror("duid: %O -> %O\n", message->get_option(OPTION_CLIENT_IDENTIFIER)->duid->duid, duid->duid);
   if(message->get_option(OPTION_CLIENT_IDENTIFIER)->duid != duid) return 0;
-werror("have matching duid\n");
   if(message->has_option(OPTION_STATUS_CODE) && message->get_option(OPTION_STATUS_CODE)->status_code == STATUS_NO_PREFIX_AVAILABLE) {
     string message = message->get_option(OPTION_STATUS_CODE)->status_message;
     if(message) werror("Received status message in advertisement: %s\n", message);
     return 0;
   }
   if(message->has_option(OPTION_IAPD)) {
-werror("have iapd option\n");
     IAPD iapd = message->get_option(OPTION_IAPD);
     if(!iapd->has_option(OPTION_IA_PDOPTION)) return 0;
     if(iapd->iaid != iaid) return 0;
+// TODO handling of T1 and T2 are not correct
     if(!iapd->t1 || !iapd->t2) return 0;
-werror("have ia pdoption\n");
     IA_PDOption pdo = iapd->get_option(OPTION_IA_PDOPTION);
-    werror("pdo: %O\n", pdo);
     if(!pdo->prefix) return 0;
     return 1;
   }

          
@@ 148,13 221,38 @@ int(0..1) is_actionable_reply(ReplyMessa
   if(!message->has_option(OPTION_CLIENT_IDENTIFIER)) return 0;
   if(!message->has_option(OPTION_SERVER_IDENTIFIER)) return 0;
   if(message->get_option(OPTION_CLIENT_IDENTIFIER)->duid != duid) return 0;
+
+  // TODO: do we need to abandon the lease if we get a status code?
+  if(message->has_option(OPTION_STATUS_CODE) && message->get_option(OPTION_STATUS_CODE)->status_code == STATUS_NO_PREFIX_AVAILABLE) {
+    string message = message->get_option(OPTION_STATUS_CODE)->status_message;
+    werror("Received NO PREFIX AVAILABLE status code\n");
+    if(message) werror("Received status message in advertisement: %s\n", message);
+    return 0;
+  }
+  if(message->has_option(OPTION_STATUS_CODE) && message->get_option(OPTION_STATUS_CODE)->status_code == STATUS_NO_BINDING) {
+    string message = message->get_option(OPTION_STATUS_CODE)->status_message;
+    werror("Received NO BINDING status code\n");
+    if(message) werror("Received status message in advertisement: %s\n", message);
+    rebind_failed();
+    return 0;
+  }
+  if(message->has_option(OPTION_IAPD)) {
+    IAPD iapd = message->get_option(OPTION_IAPD);
+    if(!iapd->has_option(OPTION_IA_PDOPTION)) return 0;
+    if(iapd->iaid != iaid) return 0;
+// TODO handling of T1 and T2 are not correct
+    if(!iapd->t1 || !iapd->t2) return 0;
+    IA_PDOption pdo = iapd->get_option(OPTION_IA_PDOPTION);
+    if(!pdo->prefix) return 0;
+    return 1;
+  }
+
+  return 0;
 }
 
 void got_packet(mapping data, mixed ... args) {
   werror("got_packet(%O, %O)\n", data, args);
   object x = decode_message(data->data);
-  werror("x: %O options: %O\n", x, x->options[0]);
-  werror("o: %O\n", mkmapping(indices(x->options[0]), values(x->options[0])));
   if(current_txn == x->transaction_id) {
     switch(x->message_type) {
       case MESSAGE_ADVERTISE:

          
@@ 184,7 282,7 @@ void got_packet(mapping data, mixed ... 
 
 void send(DHCPMessage message, string dest, int port) {
   string m = message->encode();
-  werror("sending message: %O -> %O on port %d\n", message, m, port);
+  werror("sending message: %O -> %O to %O on port %d\n", message, m, dest, port);
   dhcp->send(dest, port, m);
 }
 

          
@@ 200,12 298,23 @@ void begin_request(int|void since) {
   call_out(send_request, 0, since);
 }
 
+void begin_renew(int since) {
+  if(t1_call_out) remove_call_out(t1_call_out);
+  call_out(send_renew, 0, since);
+}
+
+void begin_rebind(int since) {
+  if(t1_call_out) remove_call_out(t1_call_out);
+  if(t2_call_out) remove_call_out(t2_call_out);
+  call_out(send_rebind, 0, since);  
+}
+
 void receive_timed_out(int since, int attempts) {
   werror("Hit timeout awaiting actionable messages\n");
   int old_state = current_state;
   current_state = NOT_AWAITING;
 
-  if(attempts < 3) { 
+  if(attempts < 5) { 
     float timeout = last_timeout + random(last_timeout * 2.0);
     if(old_state == AWAIT_ADVERTISE)
       call_out(send_solicit, timeout, since, attempts);

          
@@ 213,14 322,109 @@ void receive_timed_out(int since, int at
       call_out(send_request, timeout, since, attempts);
   }
   else {
-   werror("Hit retry limit; backing off.\n");
-   call_out(begin_solicit, 60, 0);
+    if(keep_trying) {
+      werror("Hit retry limit; backing off.\n");
+      call_out(begin_solicit, random((float)60), 0);
+    } else {
+      werror("Hit retry limit; giving up.\n");
+    }
+  }
+}
+
+void renew_timed_out(int since, int attempts) {
+  werror("Hit timeout awaiting actionable messages for our RENEW requests\n");
+  int old_state = current_state;
+  current_state = NOT_AWAITING;
+
+  if(attempts < 5) { 
+    float timeout = last_timeout + random(last_timeout * 2.0);
+    if(old_state == AWAIT_REPLY)
+      call_out(send_renew, timeout, since, attempts);
+  }
+  else {
+    werror("Hit retry limit; backing off.\n");
+    t1_call_out = call_out(begin_renew, random((float)60), 0, 0);
+  }
+}
+
+void rebind_timed_out(int since, int attempts) {
+  werror("Hit timeout awaiting actionable messages for our REBIND requests\n");
+  int old_state = current_state;
+  current_state = NOT_AWAITING;
+
+  if(attempts < 10) { 
+    float timeout = last_timeout + random(last_timeout * 2.0);
+    if(old_state == AWAIT_REPLY)
+      call_out(send_rebind, timeout, since, attempts);
+  }
+  else {
+    werror("Hit retry limit. Abandoning lease\n");
+    rebind_failed();
   }
 }
 
+void send_rebind(int since, int attempts, float timeout) {
+  if(current_state != NOT_AWAITING) {
+    throw(Error.Generic("Can't send REBIND if we're already awaiting a message. Current state = " + current_state + "\n"));
+  } 
+
+  // are we a re-transmission?
+  if(!since) { 
+    since = time();
+    current_txn = generate_transaction_id();
+  }  
+ 
+  object p = RebindMessage(current_txn);
+  object id = ClientIdOption(duid);
+ 
+  p->options += ({id}); 
+  p->options += ({OptionRequestOption(({25}))});
+  p->options += ({ElapsedTimeOption(since)});
+  p->options += ({lease_data->current_iapd});
+ 
+  if(!timeout) timeout = 5.0; 
+  last_timeout = timeout; 
+  time_out_id = call_out(rebind_timed_out, timeout, since, attempts++);
+  current_state = AWAIT_REPLY; 
+ 
+  send_broadcast(p); 
+} 
+
+void send_renew(int since, int attempts, float timeout) {
+  if(current_state != NOT_AWAITING) {
+    throw(Error.Generic("Can't send RENEW if we're already awaiting a message. Current state = " + current_state + "\n"));
+  }
+
+  // are we a re-transmission?
+  if(!since) {
+    since = time();
+    current_txn = generate_transaction_id();
+  } 
+
+  object p = RenewMessage(current_txn);
+  object id = ClientIdOption(duid);
+  object sid = lease_data->server_identifier;
+
+  p->options += ({id});
+  p->options += ({sid});
+  p->options += ({OptionRequestOption(({25}))});
+  p->options += ({ElapsedTimeOption(since)});
+  p->options += ({lease_data->current_iapd});
+
+  if(!timeout) timeout = 5.0;
+  last_timeout = timeout;
+  time_out_id = call_out(renew_timed_out, timeout, since, attempts++);
+  current_state = AWAIT_REPLY;
+
+  if(lease_data->server_unicast)
+     send(p, lease_data->server_unicast, DHCP_SERVER_PORT);
+  else
+     send_broadcast(p);
+}
+
 void send_request(int|void since, int|void attempts, float|void timeout) {
   if(current_state != NOT_AWAITING) {
-    throw(Error.Generic("Can't send SOLICIT if we're already awaiting a message. Current state = " + current_state + "\n"));
+    throw(Error.Generic("Can't send REQUEST if we're already awaiting a message. Current state = " + current_state + "\n"));
   }
 
   // are we a re-transmission?

          
@@ 231,7 435,23 @@ void send_request(int|void since, int|vo
 
   object p = RequestMessage(current_txn);
   object id = ClientIdOption(duid);
-  
+  object sid = lease_data->server_identifier;
+
+  p->options += ({id});
+  p->options += ({sid});
+  p->options += ({OptionRequestOption(({25}))});
+  p->options += ({ElapsedTimeOption(since)});
+  p->options += ({lease_data->current_iapd});
+
+  if(!timeout) timeout = 5.0;
+  last_timeout = timeout;
+  time_out_id = call_out(receive_timed_out, timeout, since, attempts++);
+  current_state = AWAIT_REPLY;
+
+  if(lease_data->server_unicast)
+     send(p, lease_data->server_unicast, DHCP_SERVER_PORT);
+  else
+     send_broadcast(p);
 }
 
 void send_solicit(int|void since, int|void attempts, float|void timeout) {