DHCPv6
======

The system provides a Python object :data:`alc.dhcpv6` to inspect and modify DHCPv6 packets.

.. _alc_dot_dhcpv6:

The :data:`alc.dhcpv6` object
-------------------------------

Attributes
^^^^^^^^^^

The object exposes the header fields as attributes on the object.
Depending on the message-type (client-server or relay), different attributes are supported.
Trying to fetch an attribute from a different message-type raises an AttributeError.

.. list-table:: Attributes defined on client-server messages
   :header-rows: 1
   
   * - Attribute
     - Field
     - Access
     - Type
   * - pkt_len
     - total length, in bytes, of the original DHCPv6-layer
     - ro
     - int
   * - msg_type
     - msg-type
     - ro
     - bytes
   * - transaction_id
     - transaction-id
     - ro
     - bytes

.. list-table:: Attributes defined on relay messages
   :header-rows: 1
   
   * - Attribute
     - Field
     - Access
     - Type
   * - pkt_len
     - total length, in bytes, of the original DHCPv6-layer
     - ro
     - int
   * - msg_type
     - msg-type
     - ro
     - bytes
   * - hop_count
     - hop-count
     - rw
     - bytes
   * - link_addr
     - link-address
     - rw
     - bytes
   * - peer_addr
     - peer-address
     - rw
     - bytes
    
Methods
^^^^^^^

.. method:: alc.Dhcp6.drop()

   Drops the packet.

.. method:: alc.Dhcp6.getOptionList()

   Returns a tuple of option-codes that are present in the top-level packet.

   - The order of the elements is the same as they appear in the packet.
   - If there are multiple instances of the same option, the option-code also appears multiple times in the tuple.
 
 ::   
 
   # for a c/s packet with options
   # Elapsed Time/Client Identifier/IANA/FQDN/Vendor Class/Option Request
   print(alc.dhcpv6.getOptionList())
   # (8, 1, 3, 39, 16, 6)

.. method:: alc.Dhcp6.get(op_code, /)

   Returns a tuple of all values of options with the specified option-code. The values are returned as the exact bytestrings as they appear in the packet.
   
   - If the specified option does not exist, the result is an empty tuple: ()
   - If a specific instance has zero length or no values, the corresponding bytestring in the result tuple is an empty bytestring: b""
   
   :param int option_code: option-code to fetch
   :rtype: tuple of bytestrings

 ::

   # For a packet with status-code option (code=0, msg="Address Assigned"):
   print(alc.dhcpv6.get(13))
   # (b‘\x00\x00Address Assigned’,)

.. method:: alc.Dhcp6.set(op_code, value, /)

   Deletes existing options of the specified code and inserts new options with the specified code and value. Multiple values can be specified in a tuple, in which case an option is added for each item in the tuple.

   :param int option_code: option-code to set
   :param bytes|tuple(bytes) value: value of the option; the length is computed from this value
 
 ::
 
   # To insert an address lease time option(51) with value 60:
   alc.dhcpv6.set(51, b'\x00\x00\x00\x3c')


.. method:: alc.Dhcp6.clear(op_code, /)

   Remove all options with specified option-code.

   :param int option_code: option-code to clear

.. method:: alc.Dhcp6.get_iana()

   Retrieves the IA_NA(3) option as an :ref:`OPDL<OPDL>` data structure.

   :rtype: :ref:`OPDL<OPDL>`

.. method:: alc.Dhcp6.set_iana(opdl, /)

   Sets the IA_NA(3) option as an :ref:`OPDL<OPDL>` data structure.

   :param OPDL value: IA_NA option to be set

.. method:: alc.Dhcp6.get_iata()

   Retrieves the IA_TA(4) option as an :ref:`OPDL<OPDL>` data structure.

   :rtype: :ref:`OPDL<OPDL>`

.. method:: alc.Dhcp6.set_iata(opdl, /)

   Sets the IA_TA(3) option as an :ref:`OPDL<OPDL>` data structure.

   :param OPDL value: IA_TA option to be set

.. method:: alc.Dhcp6.get_iapd()

   Retrieves the IA_PD(25) option as an :ref:`OPDL<OPDL>` data structure.

   :rtype: :ref:`OPDL<OPDL>`

.. method:: alc.Dhcp6.set_iapd(opdl, /)

   Sets the IA_PD(3) option as an :ref:`OPDL<OPDL>` data structure.

   :param OPDL value: IA_PD option to be set

.. method:: alc.Dhcp6.get_vendoropts()

   Retrieves the vendor_opts(17) option as an :ref:`OPDL<OPDL>` data structure.

   :rtype: :ref:`OPDL<OPDL>`

.. method:: alc.Dhcp6.set_vendoropts(opdl, /)

   Sets the vendor_opts(17) option as an :ref:`OPDL<OPDL>` data structure.

   :param OPDL value: vendor specific option to set


.. method:: alc.Dhcp6.get_relaymsg()

   Returns the embedded relay-message.

   :rtype: :class:`alc.Dhcpv6`
   :raises RuntimeError: when invoked on a client-server message

   .. note::
      The embedded packet returned from :meth:`alc.Dhcpv6.get_relaymsg` can also be modified in-place.
      However, these changes do not automatically propagate to the outer packet.
      This occurs only when the :meth:`alc.Dhcpv6.set_relaymsg` method is called on the outer packet with the modified embedded packet as the argument.

.. method:: alc.Dhcp6.set_relaymsg(msg, /)

   Replaces the current relay-message option with the specified message.

   :param alc.Dhcpv6 msg: DHCPv6 message to set as the relay-message
   :raises RuntimeError: when invoked on a client-server message


.. _OPDL:

Option Datastructure List (OPDL)
--------------------------------

The OPDL is a nested data structure which represents the value of some DHCPv6 options. 

- An OPDL is a list where each element represents an instance of that option (referred to as an OPD).
  The elements are ordered as they appear in the packet.
- An OPD is a list where each element represents one field in the option.
  The elements are ordered as they appear in the packet.
- There are two types of fields in the OPD:

  - The first fields (if any) are bytestrings and correspond to fixed fields. The number and meaning are specific to the option. 
  - The last field is a dictionary which contains the sub-options, where

    - the keys are the sub-option codes
    - the values are themselves an OPDL if the sub-options are supported, or a bytestring of the whole sub-option if not supported.
    - if there are no sub-options, the dictionary is empty.

In summary: 

- OPDL := [OPD, OPD, ...]
- OPD := [field, field, ..., sub-options]
- field := bytestring
- sub-options := {sub-opt-code: sub-opt-value, ...}
- sub-opt-code := int
- sub-opt-value := bytestring or OPDL

.. list-table:: supported options and fields
  :header-rows: 1

  * - Option
    - Fields
    - Sub-options
  * - IA-NA(3)
    - `[IAID, T1, T2]`
    - IAADDR(5), Status Code(13)
  * - IA-TA(4)
    - `[IAID]`
    - IA Address(5), Status Code(13)
  * - IA-PD(25)
    - `[IAID, T1, T2]`
    - IA Prefix(26), Status Code(13)
  * - Vendor Options(17)
    - `[enterprise-number]`
    - 
  * - IA Address(5)
    - `[ipv6addr, preferred-lifetime, valid-lifetime]`
    - Status Code(13)
  * - Status Code(13)
    - `[status-code, status-message`]
    - 
  * - IA Prefix(26)
    - `[preferred-lifetime, valid-lifetime, prefix-length, ipv6-prefix]`
    - Status Code(13)

.. note::
   The OPDL data structures that have been extracted from a :class:`alc.Dhcpv6` packet can be modified in-place.
   However, these changes do not automatically propagate to packet options.
   This occurs only when the corresponding :meth:`alc.Dhcpv6.set_xxx` method is called with the modified OPDL as the argument.

Example 1: Extract address from IA_NA option
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With an IA_NA option as in the following::

  Identity Association for Non-temporary Address
    Option: Identity Association for the Non-temporary Address (3)
    Length: 40
    Value:  0ff0def10002a30000043800000500182001055860450047...
    IAID:   0ff0def1
    T1:     172800
    T2:     276480
    IA Address: 2001:558:6045:47:45cc:d9f2:5727:eae0
      Option:             IA Address (5)
      Length:             24
      Value:              200105586045004745ccd9f25727eae00005460000054600
      IPv6 address:       2001:558:6045:47:45cc:d9f2:5727:eae0
      Preferred lifetime: 345600
      Valid lifetime:     345600

The following extracts the address from the IA_NA option::

    opdl = alc.dhcpv6.get_iana()
    # opdl: [[b'\x0f\xf0\xde\xf1', b'\x00\x02\xa3\x00', b'\x00\x048\x00', {5: [[b" \x01\x05X`E\x00GE\xcc\xd9\xf2W'\xea\xe0", b'\x00\x05F\x00', b'\x00\x05F\x00', {}]]}]]
    addresses = [hexlify(sub_opd[0]) for opd in opdl for sub_opd in opd[3][5]]
    # addresses: [b'200105586045004745ccd9f25727eae0']

Example 2: Extract information from IA_PD option
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With an IA_PD option as in the following::

  Identity Association for Prefix Delegation
    Option: Identity Association for Prefix Delegation (25)
    Length:  41
    Value:  000000010000070800000b40001a001900000e1000015180...
    IAID:  00000001
    T1:  1800
    T2:  2880
    IA  Prefix 
      Option: IA Prefix (26)
      Length:  25
      Value:  00000e10000151803820010db80002000000000000000000...
      Preferred lifetime: 3600
      Valid lifetime: 86400
      Prefix length: 56
      Prefix address: 2001:db8:2::

The following code extracts the IAID, T1, and T2::

  opdl = alc.dhcpv6.get_iapd()
  # opdl: [[b'\x00\x00\x00\x01', b'\x00\x00\x07\x08', b'\x00\x00\x0b@', {26: [[b'\x00\x00\x0e\x10', b'\x00\x01Q\x80', b'8', b' \x01\r\xb8\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', {}]]}]]
  iaid,t1,t2 = (int.from_bytes(x, 'big') for x in opdl[0][0:3])
  # iaid,t1,t2 = (1, 1800, 2880)

Example 3: Extract Enterprise ID from Vendor Option
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With a Vendor Option as in the following::

  Vendor-specific Information
    Option: Vendor-specific Information (17)
    Length:  40
    Value:  0000197f0001000969612d6e615f3030310002000969612d...
    Enterprise ID: Panthera Networks, Inc. (6527)
    option 
      option code: 1
      option length: 9
      option-data 
    option 
      option code: 2
      option length: 9
      option-data
    option 
      option code: 3
      option length: 1
      option-data
    option 
      option code: 4
      option length: 1
      option-data

The following code extracts the vendor ID::

  opdl = alc.dhcpv6.get_vendoropts()
  # opdl: [[b'\x00\x00\x19\x7f', {4: [b'@'], 1: [b'ia-na_001'], 2: [b'ia-pd_001'], 3: [b'8']}]]
  vendor = int.from_bytes(opdl[0][0], 'big')
  # vendor: 6527

Example 4: Change T1 of IA_NA option in relayed message
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
::

  embed_dhcpv6_packet = alc.dhcpv6.get_relaymsg()
  iana_list = embed_dhcpv6_packet.get_iana()
  iana_list[0][1] = b'\x00\x00\x04\xb0' # change T1 to 1200
  embed_dhcpv6_packet.set_iana(iana_list) #update the IA_NA of the embedded packet
  alc.dhcpv6.set_relaymsg(embed_dhcpv6_packet) #update the Relay Message option

Example 5: Change T1 of IA_NA option in double relayed message
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
::

  embed_lv1_packet = alc.dhcpv6.get_relaymsg()
  embed_lv2_packet = embed_lv1_packet.get_relaymsg()
  iana_list = embed_dhcpv6_packet.get_iana()
  iana_list[0][1] = b'\x00\x00\x04\xb0' # change T1 to 1200
  embed_lv2_packet.set_iana(iana_list)
  embed_lv1_packet.set_relaymsg(embed_lv2_packet)
  alc.dhcpv6.set_relaymsg(embed_lv1_packet)