| Copyright | (c) Viktor Dukhovni 2026 |
|---|---|
| License | BSD-3-Clause |
| Maintainer | ietf-dane@dukhovni.org |
| Stability | unstable |
| Safe Haskell | None |
| Language | GHC2024 |
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
- class TypeExtensible a v where
- type TypeExtensionArg a b
- extendByType :: forall t b -> (t ~ a, TypeExtensionArg t b) => v -> v
- class ValueExtensible a v where
- type ValueExtensionArg a b
- extendByValue :: forall t -> t ~ a => forall b. ValueExtensionArg t b => b -> v -> v
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
| ValueExtensible O_ede (IntMap ShortByteString) Source # | |||||
Defined in Net.DNSBase.EDNS.Option.EDE Associated Types
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 <> k3. 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 <$> get324. 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) k3. 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_OHTTPextendRRwithType 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_ohttpAnd 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
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:
- Extension state types:
RDataExtensionVal, vs.OptionExtensionVal- Extension state default values:
rdataExtensionValoptionExtensionVal
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 bextendRRwithType (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 mextendEdnsOptionWithValue (or
extendRRwithValue for an RR type)
passes each caller-supplied value to extendByValue.