wiki:Dhcp4o6Design

This is a draft document and is subject to discussion and changes.

Marcin: Although my review on 31 August 2015 is aimed to be sufficient to start implementation of the feature, I wouldn't close the design yet. The text may need some more thorough review. That shouldn't, however, have any major impact on the implementation work.

Design for DHCPv4-over-DHCPv6 (DHCP4o6)

This is a design document for the DHCP4o6 functionality that is planned for one of the upcoming Kea releases. The plan is to include this functionality in Kea 1.0 release planned in late November 2015, but as the primary developers are not working for ISC, it is impossible to make any guarantees regarding the timeline. Please consider it a stretch goal for 1.0. Everyone involved will do their best to make it happen, but we make no promises.

Marcin: A reference/link to RFC7341 is needed here. We may also clarify that the design is limited to the server side only, as we don't ship client software with Kea. However, it may be worth to mention what client software we have available for testing the feature as we develop it.

Design goals

Design goals are described in Dhcp4o6Requirements.

Overview

In plain DHCPv6 deployment, the server looks into information provided by the relay agent to determine exact place where the client is located. There are several mechanisms that allow making that determination, but the most common one is to use relay agent's IPv6 address. Relay agent inserts its address and the client's link local address in peer-addr and link-addr fields in RELAY-FORW message. Those fields are then used by the server to select appropriate configuration, depending on the client location. Typically server has a number of IPv6 subnets configured, with certain pools of addresses configured for allocation. Server attempts to match relay's IPv6 address into each subnet. Note that the address is typically outside of the dynamic allocation pool.The information provided by the relay agent is also used when transmitting back RELAY-REPLY. That way, the relay agent, doesn't need to keep any state of prior messages and every needed information is available in the returning message.

The biggest problem that requires solving in any DHCP4o6 is the question of client's location. The dual-stack client, being connected to IPv6-only network is able to send only IPv6 packets, in particular DHCP4o6-specific DHCPV4-QUERY messages. Those are relayed by DHCPv6 relay agents to the server. The server has to make a decision which IPv4 subnet to select, but it needs to use IPv6 information to do that. That raises a problem that is tricky to solve.

Marcin: While I think that saying that it is correct in most cases that the IPv6 information must be used to select subnet we should leave a room in this text for other possibilities. For example, the DHCPv4 client renewing a lease would set the ciaddr which could well be used to select the IPv4 subnet. Admittedly, before the client renews it would need to obtain its configuration first, which would be likely based on the IPv6 configuration of some sort, but I wouldn't like making the impression that the IPv4 subnet can only be selected based on the IPv6 configuration. Also, perhaps the interface name could be used for the same purpose. Client classification performed on the DHCPv4 server side is another way of assigning a subnet.

Previous attempt

The previous attempt, described in #3357 and published at https://github.com/gnocuil/DHCPv4oDHCPv6 and later updated on trac3357 branch, assumed that there are two servers, jointly providing DHCP4o6. The DHCPv6 server processed the incoming message and passed the decapsulated packet along to the DHCPv4 server. That would work only in very simple deployments with all 4o6 clients connected to a single network. There was a discussion about passing extra information, but IIRC no code was written to that effect.

Marcin: I think some code has been written to cover passing extra parameters between the DHCPv6 and DHCPv4 servers instances.

The idea was to do subnet selection matching on the DHCPv6 server side and then pass subnet-id to the DHCPv4 along with the packet. v4 server had to match subnet-id with its own IPv4 subnet information. This approach was usable in test deployments, where number of subnets was small. However, it would cause significant problem if deployed in larger scale. There would have to be two configurations tightly coupled: one for DHCPv6 and one for DHCPv4. Those two would have to exactly match their subnet-ids. Maintaining such tightly coupled configurations would be very problematic. Any differences in the subnet-ids would result in clients getting addresses from wrong subnet. This would make the whole solution fragile and difficult to deploy and maintain. Hence the C1. requirement ("configuration must be as simple as possible").

DHCPv6 and DHCPv4 servers need to exchange data. The communication model chosen was Unix sockets. That's ok in general case, but the servers should open and maintain a single connection for all packets. The communication also must be stateless on the DHCPv6 side, i.e. the server must decapsulate DHCP4o6 packet, send its interior DHCPv4 packet and continue processing other packets, without waiting for the response. Once the DHCPv4 response comes back, it should wrap it in the DHCP4o6 packet and then send back to the 4o6 client. This is critically important. The old approach was very fragile and would break down on the first DHCPv4 packet that failed sanity checks. The DHCPv6 server would decapsulate it, send to DHCPv4 that would drop and never send back any response. Then the DHCPv6 would be stuck in endless wait for response that would never came.

Marcin: Since we had a discussion multiple times about use of unix sockets versus IP/UDP sockets we should more elaborate on the design choice here. Note that there was an argument raised that loopback interface could be used for this purpose and low port numbers for preventing spoofing attacks. The implementation should not preclude this possibility.

Therefore the part that selected IPv6 subnet in the DHCPv6 code, passed the subnet-id to DHCPv4 and then IPv4 subnet selection based on that subnet-id should be discarded. There is a number of other parts of the code that can be reused: DHCPV4-QUERY/DHCPV4-RESPONSE parsing and handling, encapsulation, etc.

Another problem with this particular patch is that it is based on very old code base. It was branched off from bind10-1.1.0, which is well over 1,5 years old. It would be difficult to merge it to modern code. To avoid repeating the problem with the new approach, we will try to split the work into smaller pieces (tickets) and will address them one by one.

Reception path

The following design hopefully addresses all issues raised.

The DHCPv6 server, when configured to support DHCP4o6, opens up a unix socket (see CommandSocketFactory::create() in src/lib/config/command_socket_factory.h, similar code is already used to open control socket in Kea; and IfaceMgr::addExternalSocket() in src/lib/dhcp/iface_mgr.h). That socket will be used to transmit 4o6 packets to the DHCPv4 server.

Marcin: The DHCPv4o6 implementation should not use the code in the CommandSocketFactory, because it logically serves a different purpose. If we want to reuse the code there, the common bits should be moved to a common place.

Marcin: Another, perhaps obvious, remark is that the socket should get closed when the server is reconfigured such that DHCPv4o6 is disabled.

The DHCPv6 component is responsible for receiving 4o6 packets. If 4o6 functionality is not enabled, such incoming packets are dropped.

The DHCPv6 needs to communicate to the DHCPv4 server interface name and the IPv6 source address the packet was received on (that is necessary if the 4o6 server has multiple links over which it receives packets on). To do so, it uses the following format:

[Complete 4o6 packet][interface-name option][source ipv6 address option]

Marcin: Does order of options matter? IMHO, it shouldn't matter. I also think that we should really keep it extensible and not preclude new options from being included there. Hence, having a 2 byte long length field preceding the [interface-name-option] would be useful.

and sends the whole buffer (message + 2 extra options) over unix socket to the v4 server, using Transport4o6 class. See a section below for this class description.

The DHCPv4 server, when configured to support DHCP4o6, opens up a unix socket. That socket will be used to receive 4o6 packets from the DHCPv6 server.

The DHCPv4 server adds external socket (see CommandSocketFactory::create() in src/lib/config/command_socket_factory.h and IfaceMgr::addExternalSocket() in src/lib/dhcp/iface_mgr.h)), along with a callback dedicated to receiving DHCP4o6 packets. It instantiates the Pkt4o6 (a class derived from Pkt4) and returns it. It will follow the normal processing in DHCPv4 code.

Pkt4o6 is a class derived from Pkt4 with extra fields and methods for storing additional v6 elements. While they should not be impact DHCPv4 processing much (except subnet selection), they will be required when the server will be building on-wire response. See dedicated section below for Pkt4o6 details.

Callback used in external sockets requires slight update of how callbacks are handled. Currently callbacks are void() methods that can't return any packets (see IfaceMgr::receive4() in src/lib/dhcp/iface_mgr.cc:953 which triggers callbacks).

Marcin: I don't understand why the callback should return the data. The callback should read the data from the socket and then process them as needed. However, the code in the Dhcp4Srv class may need to be restructured to provide a common processing logic for packets received over callback or the usual way..

The configuration proposed for the DHCPv6 part is as follows:

"Dhcp6": {
    "Dhcp4o6": {
        "socket-name": "/tmp/dhcp4o6.socket";
        "socket-type": "unix";
    },

    ... // Other DHCPv6 configuration parameters here. They're irrelevant to the DHCP4o6 operation.
}

This v6 configuration will look the same, regardless of the complexity of the configuration. This configuration structure has the benefit of being extensible. It is possible that in the future additional configuration tweaks would be necessary. Those parameters would appear in the "Dhcp4o6" context.

Marcin: It will be useful to have a enable/disable switch so as you don't have to remove the socket name from the configuration to disable the functionality. We may also require parameter that defines the IPv6 address of the DHCPv4o6 server so as we can make the client send the request to unicast. Perhaps even multiple such addresses should be configurable?

Subnet Selection

In plain DHCPv4, the subnet selection takes into consideration the following parameters: giaddr (IPv4 address of the relay agent), ciaddr (client IPv4 address), destination IPv4 address, source IPv4 address, name of the interface over which the packet was received, and possible classes. Since most of those parameters will not be available, the IPv4 subnet selection for 4o6 packet will have to rely on the following parameters:

  1. name of the interface over which the packet was received (useful only if clients are connected directly, without relays)
  2. ciaddr (only in renewals, when client already has an address)
  3. IPv6 address that belongs to the IPv6 subnet, where the client is connected in (link-address from the relay agent closest to the client, if connected via relays)
  4. source IPv6 address of the packet (useful if connected via relays)
  5. interface-id of the first relay (first from the client's perspective)

(please compare to the Dhcpv6Srv::selectSubnet() impementation. We do not need this full subnet matching capability, but the bare minimum seems to be at least 1., 2. and 3.

It will be necessary to update the Dhcpv4Srv::selectSubnet() method to use 4o6 information during subnet selection process. Doing an extra check at its beginning (dynamic_cast<Pkt4o6>(query)) and augmenting its operation will be necessary (or call a new method selectSubnet4o6 if the changes are extensive).

Preparing response

It will be necessary to create Pkt4o6, instead of Pkt4 when assembling a response. Dhcpv4Exchange::Dhcpv4Exchange() should check if the packet being responded to is Pkt4o6 and if it is, pass that info to Dhcpv4Exchange::initResponse(). If responding to 4o6 packet, that method should instantiate Pkt4o6 rather than Pkt4. The code has to copy a number of 4o6 specific parameters. (see Dhcpv6Srv::copyClientOptions for example, but keep in mind that more 4o6 specific elements may be required here). Most of the work will be done in Pkt4o6 constructor.

Transmission path

The server will continue preparing response. Once it is ready, it will eventually call Dhcpv4Srv::sendPacket(). This is a simple wrapper around IfaceMgr::send(Pkt4). If the packet is 4o6, it should be sent differently. It must be appended with the interface name option (so the v6 server will know which interface to use), binary representation should be built (using Pkt4o6::pack()) and sent over the unix socket back to the v6 server.

The v6 server, once it receives packet over unix socket, it will create Pkt6 instance, unpack it, extract interface name option, call Pkt6::setIface(), remove the interface name option and then send the packet normally over specified interface, using IfaceMgr::send(Pkt6).

DHCPv4 configuration

The v4 server may require additional information about 4o6 network configuration. In particular, the server may require extra info about the network interface name the v6 server received packets on (useful if v4 server has more than one client-facing interface).

Marcin: I don't understand this use case. Does it mean that the DHCPv4 server may receive the 4o6 packets over network interfaces, rather than the unix socket?

Tomek: No. The packet will be received by the DHCPv6 server, which will record the interface name (for 2 reasons: first, it will be required for sending the response back; and second, as possible v4 subnet selector).

Another useful information would be the IPv6 subnet. This will be used to match link-address field value set up by the IPv6 relays or client's source IPv6 address when sending DHCPv4-QUERY message. Two new parameters are required for this: 4o6-subnet and 4o6-interface.

Marcin: I don't understand this either. How does the ip-address value relate to the 4o6-subnet in this case? It doesn't. The v4 subnet selection is based on matching IPv6 address (either source IPv6 address or link-address field from v6 relay-forw packet).

Note that both are optional, i.e. it is valid to specify both, just one of them or neither. Not specifying any 4o6 parameters means that a given subnet will not be available for 4o6 allocation. That will be useful for deployments that mix native DHCPv4 and DHCP4o6 clients.

#
"Dhcp4": {
    "subnet4": [
        {
            "subnet": "192.0.2.0/24",
            "pools": [ { "pool": "192.0.2.10 - 192.0.2.20" } ],
            "4o6-subnet": "2001:db8::/32",
            "4o6-interface": "ethX",
            ...
        }
    ],
    ...
}

The DHCPv4 server requires, similar to its v6 counterpart, a socket configuration. We currently support unix socket only.

"Dhcp4": {
    "Dhcp4o6": {
        "socket-name": "/tmp/dhcp4o6.socket";
        "socket-type": "unix";
    },

    ... // Other regular DHCPv4 configuration parameters here: subnets, options, option definitions etc.

}

Marcin: And again enable/disable flag would be needed. There is also a question if we should specify some defaults here..

4o6 Interface Name and 4o6 Source IPv6 Address Options

These are two intermittent options required for passing meta-information between v6 and v4 servers. The 4o6 Interface Name option conveys a string that represents interface name the DHCPv6 server received the original message on. It will be later used to transmit the response. The option code used should be OPTION_4O6_INTERFACE 60000. This option should be implemented using existing OptionString? class.

The second option stores source address of the 4o6 packet. This address will be used for two things: first, it will be used to select appropriate IPv4 subnet and second, will be used to transmit the response back to this address. It consists of a single IPv6 address. The option code used should be OPTION_4O6_SRC_ADDRESS 60001.

Discussion: perhaps instead of hijacking a high code number, it would be better to use vendor specific options and use ISC code?

Marcin: Perhaps if it was a vendor option it would naturally solve the problem of making it extensible and the vendor option header would indicate the total length of all options.

Reconfiguration Support

One of the Kea base design assumptions is that it is possible to update server configuration without restarting it. Therefore the code has to support the following reconfiguration scenarios:

  • 4o6 was disabled, but is now enabled
  • 4o6 was enabled, it is still enabled, but the socket information changed
  • 4o6 was enabled, but is now disabled

Transport4o6 class

This class is used for transmitting data between v4 and v6 server. The primary reason to contain this communication code in a single class is hermetization. If the procotol changes in the future, only this particular class would require update.

class Transport4o6 {
public:

   /// @brief Constructor
   ///
   /// socket_info is a structure from the config. file that represents the following JSON structure:
   ///
   /// {
   ///     "socket-type": "unix",
   ///     "socket-name": "/path/to/the/unix/socket"
   /// }
   ///
   /// That structure can be passed directly to CommandSocketFactory::create().
   /// CommandSocket instance (representing an open unix socket) will be returned
   Transport4o6(const isc::data::ConstElementPtr& socket_info);

   /// Used to send the data over
   /// DHCPv6 side will call send( Pkt6.getBuffer() )
   /// DHCPv4 side will call send( Pkt4o6.getBuffer() )
   void send(const isc::util::OutputBuffer& buffer);

   /// Used to receive data
   ///
   /// DHCPv4 side will call:
   /// OptionBuffer buf = receive();
   /// Pkt4o6 pkt(new Pkt4o6(&buf[0], buf.size());
   ///
   /// DHCPv6 side will call:
   /// OptionBuffer buf = receive();
   /// Pkt6Ptr pkt(new Pkt6(&buf[0], buf.size());
   OptionBuffer receive();

   /// destructor. CommandSocket should be destroyed and it will remove the socket.
   ~Transport4o6();

private:

   // Socket object will be stored here
   CommandSocketPtr socket_;   


}

Marcin: I have stated this already above. But, I strongly suggest to not mix the logic of the command socket with the socket that could well be something else and doesn't really fall into category of the command type of socket. Or, the class should be renamed if it is a general purpose class.

Pkt4o6 class

This class must be derived from Pkt4, so it can be passed around existing DHCPv4 code using the normal path. It must contain extra information specific to 4o6. Proposed declaration is as follows:

class Pkt4o6 : public Pkt4 {
public:

    /// @brief Constructor for received query
    ///
    /// data, len = this points to the whole 4o6 packet
    /// The constructor has to parse the whole message as DHCPv6, locate DHCPV4_MSG option and copy content of that
    /// option into Pkt::data_ field. That will later be used during normal unpack operation.
    ///
    /// All other 4o6 options should be stored in options4o6_;
    Pkt4o6(const uint8_t* data, size_t len);

    /// @brief Constructor for constructed response
    ///
    /// This constructor will be used when creating a response.
    /// In particular, it will have to copy over information like
    /// v4 transaction-id and all 4o6 information (like interface name,
    /// remote IPv6 address and any possible relay information).
    Pkt4o6(uint8_t msg_type, uint32_t v4trans_id, const Pkt4o6Ptr& query);

    /// Prepares on-wire representation of the 4o6 packet, including DHCPV4-REPLY (and any possible RELAY-REPLY around it)
    virtual void pack();

    /// No need for dedicated unpack. This will be done in constructor (or in method called from constructor)
    /// Regular Pkt4::unpack() should work as long as Pkt::data_ is filled with the DHCPv4 packet content.
    /// virtual void unpack();

protected:
    OptionCollection options4o6_;

    /// Structure for storing IPv6 relay information as 4o6 message may come through IPv6 relays.
    /// See Pkt6::RelayInfo for details.
    std::vector<RelayInfo> relay_info_;
}

Discussion: Maybe it would be reasonable to use multiple inheritance here (class Pkt4o6: public Pkt4, public Pkt6)? The code on trac3357 (our half-reviewed version of the previous patch) doesn't have common base class for Pkt4 and Pkt6. But the current code on master has a common base class.

Marcin: I don't think that this is a good idea to use multiple inheritance. There seem to be conflicting bits between the two. And the gain would be minimal, while conflicts between implementations of the same methods and class members may actually be significant. Also note that the Pkt4o6 is in essence a DHCPv4 packet with some meta data extracted from the DHCPv6 packet.

Tickets

This is the list of tickets that needs to be completed before DHCP4o6 is considered production ready in Kea. Actual tickets will be created (with number assigned) once the design discussion concludes.

  1. #4027 (Jinmei) Pkt4o6 class
  2. #4106 (Francis) IPC (aka Transport4o6), depends on #4027 and #4107, includes configuration changes for DHCPv6 server.
  3. #4107 (Francis) DHCP4o6 defines (including new options) (new ticket)
  4. #4105 (Tomek) Configuration parameters for 4o6 subnet selection
  5. #4109 (Francis) DHCPv6 processing change for DHCP4o6
  6. #4110 (Francis) DHCPv4 processing (without the subnet selection, which is covered in #4112).
  7. #4112 (Tomek) Subnet selection for 4o6 in DHCPv4 server
  8. #4108 (LinhuiJinmei) DHCPv4 need to reorganize the code flow, so the main run() loop calls processPacket(), which can also be called from somewhere else (e.g. a callback that receives 4o6 packet)
  9. #4111 (Jinmei) empty address list in DHCPv6 option. (ready for review now)
  10. #4273 Documentation (User's Guide)
  11. #4274 Documentation (Developer's Guide)
  12. #4267 (Francis) move final send code outside processPacket - new ticket, not planned initially
  13. #4266 (Francis) run_one server routine - new ticket, not planned initially

Merging all unfinished tickets to hackathon94 branch (trac4109, trac4106, trac4112, trac4110)

Marcin: final is that the design should include both the footprint and performance considerations, as we discussed on one of Kea calls recently.

Francis notes

I ported the ISC DHCP support for DHCPv4-over-DHCPv6 to Kea so I have some comments:

  • first with this ISC DHCP code there is a design documentation (attach below) with for instance a detailed explanation of how subnet selection is performed.
  • both client and server in ISC DHCP use 2 processes (one for DHCPv6, one for DHCPv4) with a UDP channel between them (a socket bound/connected to port/port+1)
  • the port number (a socket prefix name for AF_UNIX) was added into DHCPv4 and DHCPv6 configurations
  • the channel carries the interface name on 16 octets, the client (aka remote) address on 16 octets and the DHCPv6 full packet (this follows exactly the ISC DHCP choice).
  • the channel is implemented by a (common) base class with open/close/send/receive derived/specialized for DHCPv4 and DHCPv6 servers
  • the reception is done by the external socket feature and a specific (per server) callback/handler.
  • I have a Pk4o6 class derived from Pkt4 with a Pkt6Ptr member: 2 constructors (receive/emit), pack routine).
  • to distinguish Pkt4o6 from Pkt4 I added in both a bool isDhcp4o6() method (more efficient and cleaner than to abuse of dynamic_pointer_cast)
  • I extended the receivePacket (by DHCP4o6 packet injection) and sendPacket (by a DHCP4o6 branch) of DHCPv4 server code
  • the subnet selection uses both IPv4 and IPv6 specifications in DHCPv4 server config. I reused a subset of IPv6 subnet (same parser) and I map selected IPv6 subnet to the IPv4 subnet sharing the same ID (a bit brute force but it works)
  • there were no really hard points but I wrote the original code for ISC DHCP so with that and this design document it was enough for an experiment

PS: my idea was not to implement this feature in a future Kea release but to help the review of the ISC DHCP code: now I should get 3 nearly independent pieces of code (client, DHCPv6 server part, DHCPv4 server part)...

Documents:

Stateless

The current design is stateless:

  • (pro) queries are not kept in the DHCPv6 server: no memory leak, no timeout cleanup, etc.
  • (con) the interface name and remote address are needed in the from DHCPv4 server to DHCPv6 server way (it makes the IPC symmetrical)
  • features like delayed ACK is harder to implement in the DHCPv4 server and impossible in the DCHPv6 server
  • the initial query is no longer available when the response has to be forwarded to the client. This impacts the pkt6_send hook point.
Last modified 17 months ago Last modified on Jul 1, 2016, 8:04:29 PM