15. Client Classification

15.1. Client Classification Overview

In certain cases it is useful to differentiate among different types of clients and treat them accordingly. Common reasons include:

  • The clients represent different pieces of topology, e.g. a cable modem is not the same as the clients behind that modem.

  • The clients have different behavior, e.g. a smartphone behaves differently from a laptop.

  • The clients require different values for some options, e.g. a docsis3.0 cable modem requires different settings from a docsis2.0 cable modem.

To make management easier, different clients can be grouped into a client class to receive common options.

An incoming packet can be associated with a client class in several ways:

  • Implicitly, using a vendor class option or another built-in condition.

  • Using an expression which evaluates to true.

  • Using static host reservations, a shared network, a subnet, etc.

  • Using a hook.

Client classification can be used to change the behavior of almost any part of the DHCP message processing. There are currently nine mechanisms that take advantage of client classification:

  • Dropping queries.

  • Subnet selection.

  • Pool selection.

  • Lease limiting.

  • Rate limiting.

  • DDNS tuning.

  • Defining DHCPv4 private (codes 224-254) and code 43 options.

  • Assigning different options.

  • Setting specific options for use with the TFTP server address and the boot file field for DHCPv4 cable modems.

15.1.1. Classification Steps

The classification process is conducted in several steps:

  1. The ALL class is associated with the incoming packet.

  2. Vendor class options are processed.

  3. Classes with matching expressions and not marked for later evaluation ("on request" or depending on the KNOWN/UNKNOWN built-in classes) are processed in the order they are defined in the configuration; the boolean expression is evaluated and, if it returns true (a match), the incoming packet is associated with the class.

  4. If a private or code 43 DHCPv4 option is received, it is decoded following its client-class or global (or, for option 43, last-resort) definition.

  5. When the incoming packet belongs to the special class DROP, it is dropped and an informational message is logged with the packet information.

Note

The pkt4_receive and pkt6_receive callouts are called here.

  1. When the early-global-reservations-lookup global parameter is configured to true, the process looks up global reservations and partially performs steps 8, 9, and 10. The lookup is limited to global reservations; if one is found the KNOWN class is set, but if none is found the UNKNOWN class is not set.

  2. A subnet is chosen, possibly based on the class information when some subnets are reserved. More precisely: when choosing a subnet, the server iterates over all of the subnets that are feasible given the information found in the packet (client address, relay address, etc.). It uses the first subnet it finds that either has no class associated with it, or has a class which matches one of the packet's classes.

Note

The subnet4_select and subnet6_select callouts are called here.

  1. The server looks for host reservations. If an identifier from the incoming packet matches a host reservation in the subnet or shared network, the packet is associated with the KNOWN class and all classes of the host reservation. If a reservation is not found, the packet is assigned to the UNKNOWN class.

  2. Classes with matching expressions - directly, or indirectly using the KNOWN/UNKNOWN built-in classes and not marked for later evaluation ("on request") - are processed in the order they are defined in the configuration; the boolean expression is evaluated and, if it returns true (a match), the incoming packet is associated with the class. After a subnet is selected, the server determines whether there is a reservation for a given client. Therefore, it is not possible to use the UNKNOWN class to select a shared network or a subnet. For the KNOWN class, only global reservations are used and the early-global-reservations-lookup parameter must be configured to true.

  3. When the incoming packet belongs to the special class DROP, it is dropped and an informational message is logged with the packet information. Since Kea version 1.9.8, it is permissible to make the DROP class dependent on the KNOWN/UNKNOWN classes.

  4. If needed, addresses and prefixes from pools are assigned, possibly based on the class information when some pools are reserved for class members.

Note

The lease4_select, lease4_renew, lease6_select, lease6_renew, and lease6_rebind callouts are called here.

  1. Classes marked as "required" are evaluated in the order in which they are listed: first pools, then the subnet, and finally the shared network that assigned resources belong to.

  2. Options are assigned, again possibly based on the class information in the order that classes were associated with the incoming packet. For DHCPv4 private and code 43 options, this includes option definitions specified within classes.

Note

Care should be taken with client classification, as it is easy for clients that do not meet any class criteria to be denied service altogether.

15.2. Built-in Client Classes

Some classes are built-in, so they do not need to be explicitly defined. They can be defined if there is a need to associate lease lifetimes, option data, etc. with them.

Vendor class information is the primary example: the server checks whether an incoming DHCPv4 packet includes the vendor class identifier option (60) or an incoming DHCPv6 packet includes the vendor class option (16). If it does, the content of that option is prepended with VENDOR_CLASS_ and the result is interpreted as a class for that packet. The content that is considered is the whole class identifier for DHCPv4, and the first vendor class data field for DHCPv6. The enterprise number and subsequent vendor class data fields are not used for the purpose of classification. For example, modern cable modems send such options with value docsis3.0, so the packet belongs to class VENDOR_CLASS_docsis3.0.

The HA_ prefix is used by libdhcp_ha.so to designate certain servers to process DHCP packets as a result of load balancing. The class name is constructed by prepending the HA_ prefix to the name of the server which should process the DHCP packet. This server uses an appropriate pool or subnet to allocate IP addresses (and/or prefixes), based on the assigned client classes. The details can be found in libdhcp_ha.so: High Availability Outage Resilience for Kea Servers.

The SPAWN_ prefix is used by template classes to generate spawned class names at runtime. The spawned class name is constructed by prepending the SPAWN_ prefix to the template class name and the evaluated value: SPAWN_<template-class-name>_<evaluated-value>. More details can be found in Configuring Classes.

The BOOTP class is used by libdhcp_bootp.so to classify and respond to inbound BOOTP queries.

The SKIP_DDNS class is used by the DDNS-tuning hook library to suppress DDNS updates on a per client basis.

Other examples are the ALL class, to which all incoming packets belong, and the KNOWN class, assigned when host reservations exist for a particular client. By convention, the names of built-in classes begin with all capital letters.

Currently recognized built-in class names are ALL, KNOWN and UNKNOWN, and the prefixes VENDOR_CLASS_, HA_, AFTER_, EXTERNAL_, SKIP_DDNS. Although the AFTER_ prefix is a provision for an as-yet-unwritten hook, the EXTERNAL_ prefix can be freely used; built-in classes are implicitly defined so they never raise warnings if they do not appear in the configuration.

15.3. Using Expressions in Classification

The expression portion of a classification definition contains operators and values. All values are currently strings; operators take a string or strings and return another string. When all the operations have completed, the result should be a value of true or false. The packet belongs to the class (and the class name is added to the list of classes) if the result is true. Expressions are written in standard format and can be nested.

Expressions are pre-processed during the parsing of the configuration file and converted to an internal representation. This allows certain types of errors to be caught and logged during parsing. Examples of these errors include an incorrect number or type of argument to an operator. The evaluation code also checks for this class of error and generally throws an exception, though this should not occur in a normally functioning system.

Other issues, such as the starting position of a substring being outside of the substring or an option not existing in the packet, result in the operator returning an empty string.

Dependencies between classes are also checked. For instance, forward dependencies are rejected when the configuration is parsed; an expression can only depend on already-defined classes (including built-in classes) which are evaluated in a previous or the same evaluation phase. This does not apply to the KNOWN or UNKNOWN classes.

List of classification values

Name

Example expression

Example value

String literal

'example'

'example'

Hexadecimal string literal

0x5a7d

'Z}'

IP address literal

10.0.0.1

0x0a000001

Integer literal

123

'123'

Binary content of the option

option[123].hex

'(content of the option)'

Option existence

option[123].exists

'true'

Binary content of the sub-option

option[12].option[34].hex

'(content of the sub-option)'

Sub-Option existence

option[12].option[34].exists

'true'

Client class membership

member('foobar')

'true'

Known client

known

member('KNOWN')

Unknown client

unknown

not member('KNOWN')

DHCPv4 relay agent sub-option

relay4[123].hex

'(content of the RAI sub-option)'

DHCPv6 Relay Options

relay6[nest].option[code].hex

(value of the option)

DHCPv6 Relay Peer Address

relay6[nest].peeraddr

2001:DB8::1

DHCPv6 Relay Link Address

relay6[nest].linkaddr

2001:DB8::1

Interface name of packet

pkt.iface

eth0

Source address of packet

pkt.src

10.1.2.3

Destination address of packet

pkt.dst

10.1.2.3

Length of packet

pkt.len

513

Hardware address in DHCPv4 packet

pkt4.mac

0x010203040506

Hardware length in DHCPv4 packet

pkt4.hlen

6

Hardware type in DHCPv4 packet

pkt4.htype

6

ciaddr field in DHCPv4 packet

pkt4.ciaddr

192.0.2.1

giaddr field in DHCPv4 packet

pkt4.giaddr

192.0.2.1

yiaddr field in DHCPv4 packet

pkt4.yiaddr

192.0.2.1

siaddr field in DHCPv4 packet

pkt4.siaddr

192.0.2.1

Message type in DHCPv4 packet

pkt4.msgtype

1

Transaction ID (xid) in DHCPv4 packet

pkt4.transid

12345

Message type in DHCPv6 packet

pkt6.msgtype

1

Transaction ID in DHCPv6 packet

pkt6.transid

12345

Vendor option existence (any vendor)

vendor[*].exists

'true'

Vendor option existence (specific vendor)

vendor[4491].exists

'true'

Enterprise-id from vendor option

vendor.enterprise

4491

Vendor sub-option existence

vendor[4491].option[1].exists

'true'

Vendor sub-option content

vendor[4491].option[1].hex

docsis3.0

Vendor class option existence (any vendor)

vendor-class[*].exists

'true'

Vendor class option existence (specific vendor)

vendor-class[4491].exists

'true'

Enterprise-id from vendor class option

vendor-class.enterprise

4491

First data chunk from vendor class option

vendor-class[4491].data

docsis3.0

Specific data chunk from vendor class option

vendor-class[4491].data[3]

docsis3.0

Notes:

  • Hexadecimal strings are converted into a string as expected. The starting 0X or 0x is removed, and if the string is an odd number of characters a "0" is prepended to it.

  • IP addresses are converted into strings of length 4 or 16. IPv4, IPv6, and IPv4-embedded IPv6 (e.g. IPv4-mapped IPv6) addresses are supported.

  • Integers in an expression are converted to 32-bit unsigned integers and are represented as four-byte strings; for example, 123 is represented as 0x0000007b. All expressions that return numeric values use 32-bit unsigned integers, even if the field in the packet is smaller. In general, it is easier to use decimal notation to represent integers, but it is also possible to use hexadecimal notation. When writing an integer in hexadecimal, care should be taken to make sure the value is represented as 32 bits, e.g. use 0x00000001 instead of 0x1 or 0x01. Also, make sure the value is specified in network order, e.g. 1 is represented as 0x00000001.

  • option[code].hex extracts the value of the option with the code code from the incoming packet. If the packet does not contain the option, it returns an empty string. The string is presented as a byte string of the option payload, without the type code or length fields.

  • option[code].exists checks whether an option with the code code is present in the incoming packet. It can be used with empty options.

  • member('foobar') checks whether the packet belongs to the client class foobar. To avoid dependency loops, the configuration file parser verifies whether client classes were already defined or are built-in, i.e., beginning with VENDOR_CLASS_, AFTER_ (for the to-come "after" hook) and EXTERNAL_ or equal to ALL, KNOWN, UNKNOWN, etc.

    known and unknown are shorthand for member('KNOWN') and not member('KNOWN'). Note that the evaluation of any expression using the KNOWN class (directly or indirectly) is deferred after the host reservation lookup (i.e. when the KNOWN or UNKNOWN partition is determined).

  • relay4[code].hex attempts to extract the value of the sub-option code from the option inserted as the DHCPv4 Relay Agent Information (82) option. If the packet does not contain a RAI option, or the RAI option does not contain the requested sub-option, the expression returns an empty string. The string is presented as a byte string of the option payload without the type code or length fields. This expression is allowed in DHCPv4 only.

  • relay4 shares the same representation types as option; for instance, relay4[code].exists is supported.

  • relay6[nest] allows access to the encapsulations used by any DHCPv6 relays that forwarded the packet. The nest level specifies the relay from which to extract the information, with a value of 0 indicating the relay closest to the DHCPv6 server. Negative values allow relays to be specified counting from the DHCPv6 client, with -1 indicating the relay closest to the client. If the requested encapsulation does not exist, an empty string "" is returned. This expression is allowed in DHCPv6 only.

  • relay6[nest].option[code] shares the same representation types as option; for instance, relay6[nest].option[code].exists is supported.

  • Expressions starting with pkt4 can be used only in DHCPv4. They allow access to DHCPv4 message fields.

  • pkt6 refers to information from the client request. To access any information from an intermediate relay, use relay6. pkt6.msgtype and pkt6.transid output a 4-byte binary string for the message type or transaction ID. For example, the message type SOLICIT is 0x00000001 or simply 1, as in pkt6.msgtype == 1.

  • "Vendor option" means the Vendor-Identifying Vendor-Specific Information option in DHCPv4 (code 125; see Section 4 of RFC 3925) and the Vendor-Specific Information Option in DHCPv6 (code 17, defined in Section 21.17 of RFC 8415). "Vendor class option" means the Vendor-Identifying Vendor Class Option in DHCPv4 (code 124; see Section 3 of RFC 3925) in DHCPv4 and the Class Option in DHCPv6 (code 16; see Section 21.16 of RFC 8415). Vendor options may have sub-options that are referenced by their codes. Vendor class options do not have sub-options, but rather data chunks, which are referenced by index value. Index 0 means the first data chunk, index 1 is for the second data chunk (if present), etc.

  • In the vendor and vendor-class constructs an asterisk (*) or 0 can be used to specify a wildcard enterprise-id value, i.e. it will match any enterprise-id value.

  • Vendor Class Identifier (option 60 in DHCPv4) can be accessed using the option[60] expression.

  • RFC 3925 and RFC 8415 allow for multiple instances of vendor options to appear in a single message. The client classification code currently examines the first instance if more than one appear. For the vendor.enterprise and vendor-class.enterprise expressions, the value from the first instance is returned. Please submit a feature request on the Kea GitLab site to request support for multiple instances.

List of classification expressions

Name

Example

Description

Equal

'foo' == 'bar'

Compare the two values and return true or false

Match

match('foo.*', 'foobar')

Match a regular expression with a value.

Not

not ('foo' == 'bar')

Logical negation

And

('foo' == 'bar') and ('bar' == 'foo')

Logical and

Or

('foo' == 'bar') or ('bar' == 'foo')

Logical or

Substring

substring('foobar',0,3)

Return the requested substring

Concat

concat('foo','bar')

Return the concatenation of the strings

Concat (operator +)

'foo' + 'bar'

Return the concatenation of the strings

Ifelse

ifelse('foo' == 'bar','us','them')

Return the branch value according to the condition

Hexstring

hexstring('foo', '-')

Converts the value to a hexadecimal string, e.g. 66-6F-6F

Lcase

lcase('LoWeR')

Converts the value of a string expression to lower case, e.g. 'lower'

Ucase

ucase('uPpEr')

Converts the value of a string expression to upper case, e.g. 'UPPER'

Split

split('foo.bar', '.', 2)

Return the second field, splitting on dots.

List of conversion-to-text expressions

Name

Example

Description

AddressToText

addrtotext (192.10.0.1) addrtotext (2003:db8::)

Represent the 4 bytes of an IPv4 address or the 16 bytes of an IPv6 address in human readable format

Int8ToText

int8totext (-1)

Represents the 8-bit signed integer in text format

Int16ToText

int16totext (-1)

Represents the 16-bit signed integer in text format

Int32ToText

int32totext (-1)

Represents the 32-bit signed integer in text format

UInt8ToText

uint8totext (255)

Represents the 8-bit unsigned integer in text format

UInt16ToText

uint16totext (65535)

Represents the 16-bit unsigned integer in text format

UInt32ToText

uint32totext (4294967295)

Represents the 32-bit unsigned integer in text format

Notes:

The conversion operators can be used to transform data from binary to the text representation. The only requirement is that the input data type length matches an expected value.

The AddressToText token expects 4 bytes for IPv4 addresses or 16 bytes for IPv6 addresses. The Int8ToText and UInt8ToText tokens expect 1 byte, the Int16ToText and UInt16ToText tokens expect 2 bytes, and Int32ToText and UInt32ToText expect 4 bytes. For all conversion tokens, if the data length is 0, the result string is empty.

15.3.1. Predicates

The two predicates are Equal and Match. They can be used to build other common predicates, for instance:

not (substring('foobar', 3, 3) == 'bar')
match('foo.*', lcase('FooBar'))
match('.*foo.*', 'is it foo or bar')
match('^.*foo.*$', 'is it foo or bar')

So inequality, case insensitive pattern matching or pattern search.

Note

Detected invalid regular expressions are considered as syntax errors, runtime exceptions during match are handled as no match.

Warning

Be careful with the match operator as it can result in very poor performance leading to regular expression denial of service (ReDoS).

15.3.2. Logical Operators

The Not, And, and Or logical operators are the common operators. Not has the highest precedence and Or the lowest. And and Or are (left) associative. Parentheses around a logical expression can be used to enforce a specific grouping; for instance, in "A and (B or C)". Without parentheses, "A and B or C" means "(A and B) or C".

15.3.3. Substring

The substring operator substring(value, start, length) accepts both positive and negative values for the starting position and the length. For start, a value of 0 is the first byte in the string while -1 is the last byte. If the starting point is outside of the original string an empty string is returned. length is the number of bytes to extract. A negative number means to count towards the beginning of the string but does not include the byte pointed to by start. The special value all means to return all bytes from start to the end of the string. If the length is longer than the remaining portion of the string, then the entire remaining portion is returned. Some examples may be helpful:

substring('foobar', 0, 6) == 'foobar'
substring('foobar', 3, 3) == 'bar'
substring('foobar', 3, all) == 'bar'
substring('foobar', 1, 4) == 'ooba'
substring('foobar', -5, 4) == 'ooba'
substring('foobar', -1, -3) == 'oba'
substring('foobar', 4, -2) == 'ob'
substring('foobar', 10, 2) == ''

15.3.4. Concat

The concat function concat(string1, string2) returns the concatenation of its two arguments. For instance:

concat('foo', 'bar') == 'foobar'

For user convenience, Kea version 1.9.8 added an associative operator version of the concat function. For instance:

'abc' + 'def' + 'ghi' + 'jkl' + '...'

is the same as:

concat(concat(concat(concat('abc', 'def'), 'ghi'), 'jkl'), '...')

or:

concat('abc', concat('def', concat('ghi', concat('jkl', '...'))))

or:

'abcdefghijkl...'

15.3.5. Split

The split operator split(value, delimiters, field-number) accepts a list of characters to use as delimiters and a positive field number of the desired field when the value is split into fields separated by the delimiters. Adjacent delimiters are not compressed out, rather they result in an empty string for that field number. If value is an empty string, the result will be an empty string. If the delimiters list is empty, the result will be the original value. If the field-number is less than one or larger than the number of fields, the result will be an empty string. Some examples follow:

split ('one.two..four', '.', 1) == 'one'
split ('one.two..four', '.', 2) == 'two'
split ('one.two..four', '.', 3) == ''
split ('one.two..four', '.', 4) == 'four'
split ('one.two..four', '.', 5) == ''

Note

To use a hard-to-escape character as a delimiter, use its ASCII hex value. For example, split by single quote using 0x27: split(option[39].text, 0x27, 1)

15.3.6. Ifelse

The ifelse function ifelse(cond, iftrue, ifelse) returns the iftrue or ifelse branch value following the boolean condition cond. For instance:

ifelse(option[230].exists, option[230].hex, 'none')

15.3.7. Hexstring

The hexstring function hexstring(binary, separator) returns the binary value as its hexadecimal string representation: pairs of hexadecimal digits separated by the separator, e.g ':', '-', '' (empty separator).

hexstring(pkt4.mac, ':')

Note

The expression for each class is executed on each packet received. If the expressions are overly complex, the time taken to execute them may impact the performance of the server. Administrators who need complex or time-consuming expressions should consider writing a hook to perform the necessary work.

15.4. Configuring Classes

A client class definition can contain the following properties:
  • The name parameter is mandatory and must be unique among all classes.

  • The test expression is not mandatory and represents a string containing the logical expression used to determine membership in the class. The entire expression is included in double quotes ("). The result should evaluate to a boolean value (true or false).

  • The template-test expression is not mandatory and represents a string containing the logical expression used to generate a spawning class. The entire expression is included in double quotes ("). The result should evaluate to a string value representing the variable part of the spawned class name. If the resulting string is empty, no spawning class is generated. The resulting spawned class has the following generated name format: SPAWN_<template-class-name>_<evaluated-value>. After classes are evaluated and a spawned class is generated, the corresponding template class name is also associated with the packet. An Option specified in a spawned class will take precedence over the same option if set in its template class.

  • The option-data list is not mandatory and contains options that should be assigned to members of this class. In the case of a template class, these options are assigned to the generated spawned class.

  • The option-def list is not mandatory and is used to define custom options.

  • The only-if-required has been replaced with only-in-additional-list and is now deprecated. It will still be accepted as input for a time to allow users to migrate but will eventually be unsupported.

  • The only-in-additional-list flag is not mandatory; when its value is set to false (the default), membership is determined during classification and is available for subnet selection, for instance. When the value is set to true, membership is evaluated only if the class appears in an evaluate-additional-classes list and is usable only for option configuration.

  • The user-context is not mandatory and represents a map with user-defined data and possibly configuration options for hook libraries.

  • The next-server parameter is not mandatory and configures the siaddr field in packets associated with this class. It is used in DHCPv4 only.

  • The server-hostname is not mandatory and configures the sname field in packets associated with this class. It is used in DHCPv4 only.

  • The boot-file-name is not mandatory and configures the file field in packets associated with this class. It is used in DHCPv4 only.

  • The valid-lifetime, min-valid-lifetime, and max-valid-lifetime are not mandatory and configure the valid lifetime fields for this client class.

  • The preferred-lifetime, min-preferred-lifetime, and max-preferred-lifetime are not mandatory and configure the preferred lifetime fields for this client class. It is used in DHCPv6 only.

Note

test and template-test are mutually exclusive in a client class definition. Use either one, or neither, but not both. If both are provided, the configuration is rejected.

In the following example, the class named Client_foo is defined. It is comprised of all clients whose client IDs (option 61) start with the string foo. Members of this class will be given 192.0.2.1 and 192.0.2.2 as their domain name servers.

"Dhcp4": {
    "client-classes": [
        {
            "name": "Client_foo",
            "test": "substring(option[61].hex,0,3) == 'foo'",
            "option-data": [
                {
                    "name": "domain-name-servers",
                    "code": 6,
                    "space": "dhcp4",
                    "csv-format": true,
                    "data": "192.0.2.1, 192.0.2.2"
                }
            ]
        },
        ...
    ],
    ...
}

The next example shows a client class named "Client_enterprise" being defined for use by the DHCPv6 server. It is comprised of all clients whose client identifiers start with the given hex string, which would indicate a DUID based on an enterprise ID of 0x0002AABBCCDD. Members of this class will be given 2001:db8:0::1 and 2001:db8:2::1 as their domain name servers.

"Dhcp6": {
    "client-classes": [
        {
            "name": "Client_enterprise",
            "test": "substring(option[1].hex,0,6) == 0x0002AABBCCDD",
            "option-data": [
                {
                    "name": "dns-servers",
                    "code": 23,
                    "space": "dhcp6",
                    "csv-format": true,
                    "data": "2001:db8:0::1, 2001:db8:2::1"
                }
            ]
        },
        ...
    ],
    ...
}

It is also possible to have both left and right operands of the evaluated expression processed at runtime. Expressions related to packets can appear in the expression as many times as needed; there is no limit. However, each token has a small impact on performance and excessively complex expressions may cause a bottleneck.

"Dhcp4": {
    "client-classes": [
        {
            "name": "Infrastructure",
            "test": "option[82].option[2].hex == pkt4.mac",
            ...
        },
        ...
    ],
    ...
}

15.4.1. Template Classes

The template-test parameter indicates that the class is a template class.

"Dhcp4": {
    "client-classes": [
        {
            "name": "Client-ID",
            "template-test": "substring(option[61].hex,0,3)",
            ...
        },
        ...
    ],
    ...
}

If the received DHCPv4 packet contains option 61, then the first three bytes represent the value foo in ASCII, and the spawned class uses the SPAWN_Client-ID_foo name. Both the SPAWN_Client-ID_foo and Client-ID classes are associated with the packet.

Note

Template classes can also be used to spawn classes which match regular classes, effectively associating the regular class to the packet. To achieve this, the regular class must also contain the fixed part of the spawned class name:

SPAWN_<template-class-name-used-to-activate-this-regular-class>_<evaluated-value-filtering-this-regular-class>

"Dhcp6": {
    "client-classes": [
        {
            "name": "SPAWN_Client-ID_foobar",
            "test": "substring(option[1].hex,0,6) == 0x0002AABBCCDD",
            ...
        },
        {
            "name": "Client-ID",
            "template-test": "substring(option[1].hex,0,6)",
            ...
        },
        ...
    ],
    ...
}

If the received DHCPv6 packet contains option 1 (client identifier) with hex value 0x0002AABBCCDD, then the SPAWN_Client-ID_foobar is associated with the packet. Moreover, if the first six bytes represent value foobar in ASCII, then the spawned class uses the SPAWN_Client-ID_foobar name, effectively associating the regular class to the packet. In this second case, both the SPAWN_Client-ID_foobar and Client-ID classes are associated with the packet. The test expression on the regular class SPAWN_Client-ID_foobar is not mandatory and can be omitted, but it is used here with a different match expression for example purposes.

Usually the test and template-test expressions are evaluated before subnet selection, but in some cases it is useful to evaluate it later when the subnet, shared network, or pools are known but output-option processing has not yet been done. For this purpose, the only-in-additional-list flag, which is false by default, allows the evaluation of the test expression or the template-test expression only when it is required by the class's presence in the evaluate-additional-classes list of the selected pool, subnet, or shared network.

The evaluate-additional-classes list, which is valid for pool, subnet, and shared-network scope, specifies the classes which are evaluated in the second pass before output-option processing. The list is built in same precedence order as the option data, i.e. an option data item in a subnet takes precedence over one in a shared network. An additional class in a subnet is added before one in a shared network. The mechanism is related to the only-in-additional-list flag but it is not mandatory that the flag be set to true.

Note

The template-test expression can also be used to filter generated spawned classes, so that they are created only when needed by using the ifelse instruction.

"Dhcp4": {
    "client-classes": [
        {
            "name": "Client-ID",
            "template-test": "ifelse(substring(option[61].hex,4,3) == 'foo', substring(option[12].hex,0,12), '')",
            ...
        },
        ...
    ],
    ...
}

Note

The template classes can be used to configure limits which, just like options, are associated with the spawned class. This permits the configuration of limits that apply to all packets associated with a class spawned at runtime, according to the template-test expression in the parent template class. For a more detailed description of how to configure limits using the limits hook library, see Configuration. For example, using the configuration below, ingress DHCPv6 packets that have client ID values (in the format expressed by the Kea evaluator) foobar and foofoo both amount to the same limit of 60 packets per day, while other packets that have the first three hextets different than foo are put in separate rate-limiting buckets.

"Dhcp6": {
    "client-classes": [
        {
            "name": "Client-ID",
            "template-test": "substring(option[1].hex,0,3)",
            "user-context" : {
                "limits": {
                    "rate-limit": "60 packets per day"
                }
            },
            ...
        },
        ...
    ],
    ...
}

15.5. Using Static Host Reservations in Classification

Classes can be statically assigned to clients using techniques described in Reserving Client Classes in DHCPv4 and Reserving Client Classes in DHCPv6.

Subnet host reservations are searched after subnet selection. Global host reservations are searched at the same time by default but the early-global-reservations-lookup allows to change this behavior into searching them before the subnet selection.

Pool selection is performed after all host reservations lookups.

15.6. Configuring Subnets With Class Information

In certain cases it is beneficial to restrict access to certain subnets only to clients that belong to a given class, using the client-class keyword when defining the subnet.

Let's assume that the server is connected to a network segment that uses the 192.0.2.0/24 prefix. The administrator of that network has decided that addresses from the range 192.0.2.10 to 192.0.2.20 will be managed by the DHCPv4 server. Only clients belonging to client class Client_foo are allowed to use this subnet. Such a configuration can be achieved in the following way:

"Dhcp4": {
    "client-classes": [
        {
            "name": "Client_foo",
            "test": "substring(option[61].hex,0,3) == 'foo'",
            "option-data": [
                {
                    "name": "domain-name-servers",
                    "code": 6,
                    "space": "dhcp4",
                    "csv-format": true,
                    "data": "192.0.2.1, 192.0.2.2"
                }
            ]
        },
        ...
    ],
    "subnet4": [
        {
            "id": 1,
            "subnet": "192.0.2.0/24",
            "pools": [ { "pool": "192.0.2.10 - 192.0.2.20" } ],
            "client-class": "Client_foo"
        },
        ...
    ],
    ...
}

The following example shows how to restrict access to a DHCPv6 subnet. This configuration restricts use of the addresses in the range 2001:db8:1::1 to 2001:db8:1::FFFF to members of the "Client_enterprise" class.

"Dhcp6": {
    "client-classes": [
        {
            "name": "Client_enterprise",
            "test": "substring(option[1].hex,0,6) == 0x0002AABBCCDD",
            "option-data": [
                {
                    "name": "dns-servers",
                    "code": 23,
                    "space": "dhcp6",
                    "csv-format": true,
                    "data": "2001:db8:0::1, 2001:db8:2::1"
                }
            ]
        },
        ...
    ],
    "subnet6": [
        {
            "id": 1,
            "subnet": "2001:db8:1::/64",
            "pools": [ { "pool": "2001:db8:1::-2001:db8:1::ffff" } ],
            "client-class": "Client_enterprise"
        }
    ],
    ...
}

15.7. Configuring Pools With Class Information

Similar to subnets, in certain cases access to certain address or prefix pools must be restricted to only clients that belong to a given class, using the client-class when defining the pool.

Let's assume that the server is connected to a network segment that uses the 192.0.2.0/24 prefix. The administrator of that network has decided that addresses from the range 192.0.2.10 to 192.0.2.20 are going to be managed by the DHCPv4 server. Only clients belonging to client class Client_foo are allowed to use this pool. Such a configuration can be achieved in the following way:

"Dhcp4": {
    "client-classes": [
        {
            "name": "Client_foo",
            "test": "substring(option[61].hex,0,3) == 'foo'",
            "option-data": [
                {
                    "name": "domain-name-servers",
                    "code": 6,
                    "space": "dhcp4",
                    "csv-format": true,
                    "data": "192.0.2.1, 192.0.2.2"
                }
            ]
        },
        ...
    ],
    "subnet4": [
        {
            "id": 1,
            "subnet": "192.0.2.0/24",
            "pools": [
                {
                    "pool": "192.0.2.10 - 192.0.2.20",
                    "client-class": "Client_foo"
                }
            ]
        },
        ...
    ],
    ...
}

The following example shows how to restrict access to an address pool. This configuration restricts use of the addresses in the range 2001:db8:1::1 to 2001:db8:1::FFFF to members of the "Client_enterprise" class.

"Dhcp6": {
    "client-classes": [
        {
            "name": "Client_enterprise_",
            "test": "substring(option[1].hex,0,6) == 0x0002AABBCCDD",
            "option-data": [
                {
                    "name": "dns-servers",
                    "code": 23,
                    "space": "dhcp6",
                    "csv-format": true,
                    "data": "2001:db8:0::1, 2001:db8:2::1"
                }
            ]
        },
        ...
    ],
    "subnet6": [
        {
            "id": 1,

            "subnet": "2001:db8:1::/64",

            "pools": [
                {
                    "pool": "2001:db8:1::-2001:db8:1::ffff",
                    "client-class": "Client_foo"
                }
            ]
        },
        ...
    ],
    ...
}

15.8. Class Priority

Client classes in Kea follow the order in which they are specified in the configuration (vs. alphabetical order). Required classes follow the order in which they are required.

When determining which client-class information (comprised of options, lease lifetimes, or DHCPv4 field values) is part of the class definitions to be included in the response, the server examines the union of options from all of the assigned classes. If two or more classes include the same class information, the value from the first assigned class is used. ALL is always the first class, i.e. the class with the highest priority, and matching required classes are last, so they have the lowest priority.

Options defined in classes override any global options, and in turn are overridden by options defined for an individual subnet, shared network, pool, or reservation.

On the other hand, lease lifetimes and DHCPv4 field values defined at class scope override any values defined globally, in a subnet scope, or in a shared-network scope.

As an example, imagine that an incoming packet matches two classes. Class foo defines values for an NTP server (option 42 in DHCPv4) and an SMTP server (option 69 in DHCPv4), while class bar defines values for an NTP server and a POP3 server (option 70 in DHCPv4). The server examines the three options - NTP, SMTP, and POP3 - and returns any that the client requested. As the NTP server was defined twice, the server chooses only one of the values for the reply; the class from which the value is obtained is determined as explained in the previous paragraphs.

15.9. Option Class-Tagging

Option class-tagging allows an option value to conditionally applied to the response based on the client's class membership. The effect is similar to using an if-block in ISC DHCP to conditionally include options at a given scope. Class-tagging is done by specifying a list of one of more class names in the option's client-classes entry.

Consider a case where members of a given class need the same value for one option but a subnet-specific value for another option. The following example shows class-tagging used to give clients who belong to "GROUP1" a subnet-specific value for option "bar", while giving all members of "GROUP1" the same value for option "foo":

{
    "client-classes": [
    {
        "name": "GROUP1",
        "test":"substring(option[123].hex,0,4) == 'ONE'",
        "option-data": [{
            "name": "foo",
            "data": "somefile.txt"
        }]
    }],
    "subnet4": [
    {
        "id": 1,
        "subnet": "178.16.1.0/24",
        "option-data": [{
            "client-classes": ["GROUP1"],
            "name": "bar",
            "data": 123
        }]
    },
    {
        "id": 2,
        "subnet": "178.16.2.0/24",
        "option-data": [
        {
            "client-classes": ["GROUP1"],
            "name": "bar",
            "data": 789
        }]
    }]
}

The client-classes list is allowed in an option specification at any scope. Option class-tagging is enforced at the time options are being added to the response which occurs after lease assignment just before the response is to be sent to the client.

When never-send for an option is true at any scope, all client-classes entries for that option are ignored. The option will not included.

When always-send is true at any scope, the option will be included unless, the option determined by scope specifies a client-classes list that does not contain any of the client's classes.

Otherwise, An option requested by the client will be included in the response if the option either does not specify client-classes or the client belongs to at least one of the classes in client-classes.

When an option's class-tag does not match, it is as though the option was not specified at that scope. In the following example the option "foo" is specified for both the subnet and the pool. The pool specification includes a class-tag that limits the option to members of class "melon":

{
    "id": 100,
    "subnet": "178.16.1.0/24",
    "option-data": [{
        "name": "foo",
        "data": 456
    }],
    "pools": [{
        "pool": "178.16.1.100 - 178.16.1.200",
        "option-data": [{
            "name": "foo",
            "data": 123,
            "client-classes" : [ "melon" ]
        }]
    }]
}

Clients that match class "melon" will have a value of 123 for option "foo", while clients that do not match "melon" will have a value of 456 for option "foo".

Note

Though examples above are for DHCPv4, class-tagging syntax and behavior is the same for DHCPv6.

15.10. Classes and Hooks

Hooks may be used to classify packets. This may be useful if the expression would be complex or time-consuming to write, and could be better or more easily written as code. Once the hook has added the proper class name to the packet, the rest of the classification system will work as expected in choosing a subnet and selecting options. For a description of hooks, see Hook Libraries; for information on configuring classes, see Configuring Classes and Configuring Subnets With Class Information.

15.11. Debugging Expressions

While constructing classification expressions, administrators may find it useful to enable logging; see Logging for a more complete description of the logging facility.

To enable the debug statements in the classification system, the severity must be set to DEBUG and the debug level to at least 55. The specific loggers are kea-dhcp4.eval and kea-dhcp6.eval.

To understand the logging statements, it is essential to understand a bit about how expressions are evaluated; for a more complete description, refer to [the design document](https://gitlab.isc.org/isc-projects/kea/-/wikis/designs/client-classification-design). In brief, there are two structures used during the evaluation of an expression: a list of tokens which represent the expressions, and a value stack which represents the values being manipulated.

The list of tokens is created when the configuration file is processed, with most expressions and values being converted to a token. The list is organized in reverse Polish notation. During execution, the list is traversed in order; as each token is executed, it is able to pop values from the top of the stack and eventually push its result on the top of the stack. Imagine the following expression:

"test": "substring(option[61].hex,0,3) == 'foo'",

This will result in the following tokens:

option, number (0), number (3), substring, text ('foo'), equals

In this example, the first three tokens will simply push values onto the stack. The substring token will then remove those three values and compute a result that it places on the stack. The text option also places a value on the stack, and finally the equals token removes the two tokens on the stack and places its result on the stack.

When debug logging is enabled, each time a token is evaluated it emits a log message indicating the values of any objects that were popped off of the value stack, and any objects that were pushed onto the value stack.

The values are displayed as either text, if the command is known to use text values, or hexadecimal, if the command either uses binary values or can manipulate either text or binary values. For expressions that pop multiple values off the stack, the values are displayed in the order they were popped. For most expressions this will not matter, but for the concat expression the values are displayed in reverse order from their written order in the expression.

Let us assume that the following test has been entered into the configuration. This example skips most of the configuration to concentrate on the test.

"test": "substring(option[61].hex,0,3) == 'foo'",

The logging might then resemble this:

2016-05-19 13:35:04.163 DEBUG [kea.eval/44478] EVAL_DEBUG_OPTION Pushing option 61 with value 0x666F6F626172
2016-05-19 13:35:04.164 DEBUG [kea.eval/44478] EVAL_DEBUG_STRING Pushing text string '0'
2016-05-19 13:35:04.165 DEBUG [kea.eval/44478] EVAL_DEBUG_STRING Pushing text string '3'
2016-05-19 13:35:04.166 DEBUG [kea.eval/44478] EVAL_DEBUG_SUBSTRING Popping length 3, start 0, string 0x666F6F626172 pushing result 0x666F6F
2016-05-19 13:35:04.167 DEBUG [kea.eval/44478] EVAL_DEBUG_STRING Pushing text string 'foo'
2016-05-19 13:35:04.168 DEBUG [kea.eval/44478] EVAL_DEBUG_EQUAL Popping 0x666F6F and 0x666F6F pushing result 'true'

Note

The debug logging may be quite verbose if there are multiple expressions to evaluate; it is intended as an aid in helping create and debug expressions. Administrators should plan to disable debug logging when expressions are working correctly. Users may also wish to include only one set of expressions at a time in the configuration file while debugging them, to limit the log statements. For example, when adding a new set of expressions, an administrator might find it more convenient to create a configuration file that only includes the new expressions until they are working correctly, and then add the new set to the main configuration file.