dnsbase
Copyright(c) Viktor Dukhovni 2026
LicenseBSD-3-Clause
Maintainerietf-dane@dukhovni.org
Stabilityunstable
Safe HaskellNone
LanguageGHC2024

Net.DNSBase.Extensible

Description

The two classes TypeExtensible and ValueExtensible are the hooks that an already-extensible RR-data or EDNS-option codec uses to admit user-supplied additions at conf-build time. TypeExtensible admits a new Haskell type as the extension; ValueExtensible admits a runtime value. Most callers never write an instance of either: the common cases are registering a brand-new RR type or EDNS option that doesn't itself need an extension hook, or supplying an extension to an already extensible codec. The sections below walk through each of these in turn, and finish with the steps for defining a new extensible codec of your own.

Synopsis

Extension classes

class TypeExtensible a v where Source #

RR-data or EDNS-option types whose codec admits a type-driven extension. See Adding a custom RR type for an example of this mechanism in use, and Writing a type-driven extensible codec for the steps to implement a type-driven extensible codec of your own.

Associated Types

type TypeExtensionArg a b Source #

Constraint a caller's extension type b must satisfy.

Methods

extendByType :: forall t b -> (t ~ a, TypeExtensionArg t b) => v -> v Source #

Fold a caller's extension type b into the existing codec context value of type v.

class ValueExtensible a v where Source #

RR-data or EDNS-option types whose codec admits a value-driven extension. See Adding a valud-driven extension for an example of this mechanism in use, and Writing a value-driven extensible codec for the steps to implement a value-driven extensible codec of your own.

Associated Types

type ValueExtensionArg a b Source #

Constraint a caller's extension value type b must satisfy.

Methods

extendByValue :: forall t -> t ~ a => forall b. ValueExtensionArg t b => b -> v -> v Source #

Fold a caller-supplied value of type b into the existing codec context value of type v.

Instances

Instances details
ValueExtensible O_ede (IntMap ShortByteString) Source # 
Instance details

Defined in Net.DNSBase.EDNS.Option.EDE

Associated Types

type ValueExtensionArg O_ede b 
Instance details

Defined in Net.DNSBase.EDNS.Option.EDE

Methods

extendByValue :: forall t -> t ~ O_ede => forall b. ValueExtensionArg t b => b -> IntMap ShortByteString -> IntMap ShortByteString Source #

Extending the library

The sections below detail the available extension mechanisms, showing workable code fragments, and even complete programs.

Adding a custom RR type

The library decodes recognised RR types into matching KnownRData values and falls back to OpaqueRData for unrecognised codepoints.

To teach the resolver about an RR type the library does not natively handle, declare a new KnownRData instance for your data type and pass the type to registerRRtype when building a ResolverConf.

1. Declare the RRTYPE and the associated data type.

The example below adds an alternative defintion of the usual DNS A RRtype. Stored as Word32 value, and presented in hexadecimal. First define the required RRTYPE:

pattern EXT_HEXA :: RRTYPE
pattern EXT_HEXA = RRTYPE 1

Next a type to carry the decoded form:

data T_ext_hexa = T_EXT_HEXA Word32 deriving (Eq, Ord, Show)

2. Give it a presentation form.

Presentable objects print values in DNS (zone file) presentation form.

-- | CPS Presentation form is 8 hex nibbles
instance Presentable T_ext_hexa where
    present (T_EXT_HEXA w) = \k -> B.word32HexFixed w <> k

3. Implement the KnownRData instance.

The instance carries the RR-type code, an optional presentation override for the type name, the wire encoder, and the wire decoder. Plain RR types — those with no per-type extension state — leave RDataExtensionVal at its () default; the three leading _ patterns on rdDecode discard the required type argument, the (trivial) extension value and the fixed data length. The get32 parser checks that at least 4 bytes are available, and the message decoder calling rdDecode checks that the entire payload was consumed.

instance KnownRData T_ext_hexa where
    rdType _ = EXT_HEXA
    -- Specify the RRTYPE name
    rdTypePres _ = present @String "HEXA"
    -- Implement the fixed size encoder
    rdEncode (T_EXT_HEXA w) = putSizedBuilder $! mbWord32 w
    -- The decoder wraps the payload inside 'Net.DNSBase.RData.RData'
    rdDecode _ _ _ = RData . T_EXT_HEXA <$> get32

4. Plug it into the resolver.

registerRRtype installs the new RData type. Application-registed RR types take precedence over any built-in types with the same RRTYPE, except for a small set of RFC-reserved slots (e.g., the OPT pseudo-RR) where the library entry always wins.

withHEXA :: ResolverConf -> ResolverConf
withHEXA = registerRRtype T_ext_hexa

Applying the above to defaultResolvConf you get a configuration that associates the custom data type with its RRTYPE. This can be passed to makeResolvSeed, whose output can in turn be used to spawn one or more resolver instances via withResolver.

You can find a complete demo program on Github. The expected output with google.com as the command-line argument, should be similar to:

www.google.com. 208 IN HEXA 8efb9977
www.google.com. 208 IN HEXA 8efb9c77
www.google.com. 208 IN HEXA 8efb9677
www.google.com. 208 IN HEXA 8efb9a77
www.google.com. 208 IN HEXA 8efb9d77
www.google.com. 208 IN HEXA 8efb9b77
www.google.com. 208 IN HEXA 8efb9777
www.google.com. 208 IN HEXA 8efb9877

Adding a custom EDNS option

The library decodes recognised EDNS options into matching KnownEdnsOption values and falls back to OpaqueOption for unrecognised codepoints.

To teach the resolver about an EDNS option the library does not natively handle, declare a KnownEdnsOption instance for your data type and pass the type to registerEdnsOption when building a ResolverConf.

1. Declare the OptNum and the associated data type.

The example below adds support for sending and receiving the DNS COOKIE option (applications would need to decide when to send cookies, what to put in them, and what to do with cookies received from servers).

pattern EXT_COOKIE :: OptNum
pattern EXT_COOKIE = OptNum 10

Next a type to carry the decoded form:

import Data.ByteString.Builder as BS
import Data.ByteString.Short as SBS

data T_ext_cookie = T_EXT_COOKIE
    { clientCookie :: Word64              -- ^ Fixed length
    , serverCookie :: SBS.ShortByteString -- ^ Possibly empty
    } deriving (Eq, Ord, Show)

2. Give it a presentation form.

Presentable prints DNS object values in ASCII text presentation form.

instance Presentable T_ext_cookie where
    present T_EXT_COOKIE {..} k =
         B.word64HexFixed clientCookie
         <> present @Bytes16 (coerce serverCookie) k

3. Implement the KnownEdnsOption instance.

The instance carries the EDNS option code, an optional presentation override for the type name, the wire encoder, and the wire decoder. Plain EDNS option types — those with no per-type extension state — leave RDataExtensionVal at its () default; the three leading _ patterns on optDecode discard the required type argument, the (trivial) extension value and the fixed data length. The get64 parser checks that at least 8 bytes are available, and the message decoder calling optDecode checks that the entire payload was consumed.

instance KnownEdnsOption T_ext_cookie where
    optNum _ = EXT_COOKIE

    optPres _ = present @String "COOKIE"

    optEncode T_EXT_COOKIE {..} = putSizedBuilder $!
        mbWord64 clientCookie
        <> mbShortByteString serverCookie

    optDecode _ _ len = do
         clientCookie <- get64
         serverCookie <-
             if | len == 8 -> pure mempty
                | len > 40 -> failSGet "Server cookie too long"
                | len < 16 -> failSGet "Server cookie too short"
                | otherwise -> getShortNByteString (len - 8)
         pure $ EdnsOption $ T_EXT_COOKIE {..}

4. Plug it into the resolver.

registerEdnsOption installs the new KnownEdnsOption type. Application-registed EDNS option types take precedence over any built-in types with the same OptNum.

withCookie :: ResolverConf -> ResolverConf
withCookie = registerEdnsOption T_ext_cookie

Applying the above to defaultResolvConf you get a configuration that associates the custom data type with its OptNum. This can be passed to makeResolvSeed, whose output can in turn be used to spawn one or more resolver instances via withResolver.

Below is a complete program with more detailed comments, if compiled and executed it prints a cookie obtained from the (current at time of writing) authoritative name server for the isc.org domain.

You can find a complete demo program on Github. The expected output should be similar to:

-- ; COOKIE:  <16 client hex nibbles + 32 server hex nibbles>
-- isc.org. 7200 IN NS ns1.isc.org.
-- isc.org. 7200 IN NS ns2.isc.org.
-- isc.org. 7200 IN NS ns3.isc.org.
-- isc.org. 7200 IN NS ns.isc.afilias-nst.info.
-- isc.org. 7200 IN NS nsp.dnsnode.net.

Adding a type-driven extension

Type-driven extensions let a caller extend an already implemented codec by supplying a new Haskell type that meets the codec's extension configuration constraint (TypeExtensionArg). The built-in data types that support this extensionn mechanism are T_svcb and T_https.

Each SVCParamValue is decoded by a KnownSVCParamValue instance registered in the codec's set of known types. Unknown keys fall through to decoding via OpaqueSPV.

To add or override a parameter value, implement a KnownSVCParamValue instance for your data type and configure it in either or both ot T_svcb and T_https by calling extendRRwithType.

An EDNS-option codec that supports type-driven extension would be customised analogously via extendEdnsOptionWithType.

The example below adds the boolean ohttp parameter (RFC 9540, key 8), which carries no payload — its presence or absence in a SVCB or HTTPS record is the entire signal. If the type were missing from the library, this would introduce it, otherwise replaces the underlying SVCB key slot with the specified type.

pattern EXT_OHTTP :: SVCParamKey
pattern EXT_OHTTP = SVCParamKey 8

data SPV_EXT_ohttp = SPV_EXT_OHTTP deriving (Eq, Ord, Show)

instance Presentable SPV_EXT_ohttp where
    present _ = present "ohttp" -- key[=value], no value to add

instance KnownSVCParamValue SPV_EXT_ohttp where
    spvKey _    = EXT_OHTTP
    spvKeyPres _ = present "ohttp" -- For key-only contexts
    encodeSPV _ = pure ()
    decodeSPV _ _ = pure $ SVCParamValue SPV_EXT_OHTTP

extendRRwithType takes an existing type to be extended and the new extension type. It adds the extension type to the set of types known to the extended type. Next, once again build a custom resolver configuration that supports or prefers the new SVCParamValue:

withOHTTP :: ResolverConf -> ResolverConf
withOHTTP = extendRRwithType T_svcb  SPV_EXT_ohttp
          . extendRRwithType T_https SPV_EXT_ohttp

And with the associated resolver seed you're ready to spin up resolvers that use the new data type for this SVCB/HTTPS key value.

makeResolvSeed $ withOHTTP defaultResolvConf >>= \case
    Left why -> ... handle error ...
    Right seed -> withResolver seed \rslv -> ...

As with custom RRTYPE registrations, application-registered SVCParamValue types take precedence over any built-in type for the same SVCB key with the exception of the reserved mandatory key (codepoint 0, RFC 9460 -- section 8), for which the library's implementation is always used.

You can find a complete demo program on Github. Rather than the value-less ohttp key shown above, the demo shadows the standard ipv4hint key (codepoint 4) with a representation that holds each hint as a raw Word32 and presents each address as eight hexadecimal nibbles under a made-up name IPV4HEX, so that the override is visible in the output of an HTTPS lookup against a domain whose records carry ipv4hint values (e.g. cloudflare.com). Other SvcParam entries in the same answer continue to use their built-in decoders.

Adding a value-driven extension

Value-driven extensions let a caller extend an already implemented codec by supplying a runtime value rather than a Haskell type. An example of is the data type that models EDNS Extended DNS Errors: O_ede which on the wire carries a numeric code with an optional text string.

Each EDE numeric code is associated with a descriptive name as part of IANA registration. New codes that are not known to the library can be registered at runtime, and are stored as part of any received EDE options when a message is decoded by the resolver.

extendEdnsOptionWithValue can be used to register an new EDE code to name association, or to override a built-in name, by passing a (code, name) pair. For a given info-code a new registration takes precedence over the library's built-in value (if any).

In this case, no new Haskell type need be defined — the registration is just data, because decoding of just needs to map each number to a string, without any additional custom behaviours.

Any RData codec that supported value-driven extensions would be customised analogously with the use of extendRRwithValue.

Code outline

Expand
withCustomEdeNames :: ResolverConf -> ResolverConf
withCustomEdeNames =
    extendEdnsOptionWithValue O_ede (33, "Frobnicated")
  . extendEdnsOptionWithValue O_ede (34, "Bogosity")

let customConf :: ResolverConf
    customConf = withCustomEdeNames defaultResolverConf
... seed <- makeResolvSeed customConf ...
... withResolver seed ...

Writing a type-driven extensible codec

For library authors designing an RR-data or EDNS-option codec that needs to accept user-supplied typed extensions, the steps below wire up TypeExtensible for use with the existing extendRRwithType / extendEdnsOptionWithType API.

The X_svcb data type illustrates the approach. Its extension state is an IntMap table mapping key numbers to SVCParamValue decoders. Its TypeExtensible instance inserts new entries at their declared spvKey slots.

The recipe is analogous on the RData and EDNS-option sides; they differ only in the type class constraints on the extended types:

and the names of the associated extension value types, and their default (initial) value fields:

1. Choose the codec's extension value type.

This is the state the decoder needs to make sense of known and new DNS data and what the TypeExtensible instance folds new entries into. For SVCB it is a map from a SVCParamKey codepoint to a decoder for the associated KnownSVCParamValue type.

2. Declare the state as part of the type class instance.

Override the associated extension-value type and its default. On the RData side:

instance ... => KnownRData MyType where
    type RDataExtensionVal MyType = SomeState
    rdataExtensionVal _ = baselineState -- the initial value
    ...
    rdDecode _ state len = ... decode using state + len ...

On the EDNS option side:

instance ... => KnownEdnsOption MyType where
    type OptionExtensionVal MyType = SomeState
    optionExtensionVal _ = baselineState -- the initial value
    ...
    optDecode _ state len = ... decode using state + len ...

Plugins are only needed for decoding, with a typed value already in hand the encoder can just use the value's encode method.

3. Define the TypeExtensible instance.

Since the application provided type is not fixed in advance, to be usable it must satisfy some superclass constraint that makes it possible for the extension to provide actual input to the customised decoder. Therefore, the TypeExtensible instance needs to define a Constraint that ensures that the newly registered type supports the requisite methods.

The extendByType method defined as part of the instance is responsible for extracting from the provided type all the information needed to extend its decoder to support the novel type. In the case of SVCB the type is added to the key codepoint number to decoder function map.

instance TypeExtensible MyType SomeState where
    type TypeExtensionArg MyType b = (MyExtensionClass b)
    extendByType _ b state = ... -- update state with b

extendRRwithType (or extendEdnsOptionWithType for an EDNS option) passes each caller-supplied extension to extendByType, which uses to update the state using the methods of MyExtensionClass.

Writing a value-driven extensible codec

For library authors designing an RR-data or EDNS-option codec that needs to accept user-supplied value-driven extensions, the recipe parallels the Type extensible case, with ValueExtensible in place of TypeExtensible doing the work:

The reference built-in example is O_ede, whose extension state is an IntMap of info-code to friendly-name entries and whose ValueExtensible instance inserts new entries into that map.

1. Choose the codec's extension state type, and

2. Adjust the parent-class instance accordinly.

For the EDE EDNS option, this is:

instance KnownEdnsOption O_ede where
    type OptionExtensionVal O_ede = IntMap ShortByteString
    optionExtensionVal _ = baseEdeNames -- initial table
    ...
    optDecode _ state len = ... decode using @state@

3. Implement the ValueExtensible instance. In this case,

the constraint ValueExtensionArg fixes the caller's value type, and extendByValue folds the value in.

instance ValueExtensible O_ede (IntMap ShortByteString) where
    type ValueExtensionArg O_ede b = b ~ (Word16, ShortByteString)
    extendByValue _ (code, name) m =
        IM.insert (fromIntegral code) name m

extendEdnsOptionWithValue (or extendRRwithValue for an RR type) passes each caller-supplied value to extendByValue.