dnsbase-1.0.0.0: src/Net/DNSBase/EDNS/Option/ECS.hs
{-|
Module : Net.DNSBase.EDNS.Option.ECS
Description : EDNS Client Subnet option (RFC 7871)
Copyright : (c) Viktor Dukhovni, 2026
License : BSD-3-Clause
Maintainer : ietf-dane@dukhovni.org
Stability : unstable
The Client Subnet EDNS option lets a recursive resolver forward
a prefix of the original client's address to an authoritative
server, so that authority-side answers (CDNs and similar) can
be tailored to the client's network location. The
specification, including privacy considerations, is
[RFC 7871](https://datatracker.ietf.org/doc/html/rfc7871).
-}
module Net.DNSBase.EDNS.Option.ECS
( O_ecs(..)
) where
import Net.DNSBase.Decode.Internal.State
import Net.DNSBase.EDNS.Internal.OptNum
import Net.DNSBase.EDNS.Internal.Option
import Net.DNSBase.Encode.Internal.State
import Net.DNSBase.Encode.Internal.Metric
import Net.DNSBase.Internal.Present
import Net.DNSBase.Internal.Util
-- | The Client Subnet EDNS option
-- ([RFC 7871 section 6](https://tools.ietf.org/html/rfc7871#section-6))
-- — three fields: a source prefix length, a scope prefix length,
-- and a (possibly truncated) IP address whose 16-bit @FAMILY@
-- field is implicit in the constructor's 'IP' value (@1@ for
-- 'IPv4', @2@ for 'IPv6').
--
-- > +0 (MSB) +1 (LSB)
-- > +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
-- > | OPTION-CODE |
-- > +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
-- > | OPTION-LENGTH |
-- > +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
-- > | FAMILY |
-- > +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
-- > | SOURCE PREFIX-LENGTH | SCOPE PREFIX-LENGTH |
-- > +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
-- > | ADDRESS... /
-- > +---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
--
-- The address is masked and truncated to the source prefix length
-- on encode and zero-padded on decode. A @FAMILY@ value other
-- than 1 or 2 fails the decoder (a future revision may decode
-- such values as an opaque option instead).
data O_ecs = O_ECS Word8 Word8 IP deriving (Eq, Show)
instance Presentable O_ecs where
present (O_ECS srcbits scopebits ip) =
present srcbits
. presentSp scopebits
. presentSp ip
instance KnownEdnsOption O_ecs where
optNum _ = ECS
{-# INLINE optNum #-}
optEncode (O_ECS srcbits scopebits ip) = case ip of
IPv4 ip4 -> do
-- XXX: More precise error?
when (srcbits < 0 || srcbits > 32) $
failWith CantEncode
let (!q, !r) = (srcbits + 7) `quotRem` 8
!w = fromIPv4w ip4
putSizedBuilder $
mbWord16 1
<> mbWord8 srcbits
<> mbWord8 scopebits
<> encWord w q r
IPv6 ip6 -> do
-- XXX: More precise error?
when (srcbits < 0 || srcbits > 128) $
failWith CantEncode
let (!q, !r) = (srcbits + 7) `quotRem` 8
(!w0, !w1, !w2, !w3) = fromIPv6w ip6
putSizedBuilder $
mbWord16 2
<> mbWord8 srcbits
<> mbWord8 scopebits
<> encWord w0 q r
<> encWord w1 (q - 4) r
<> encWord w2 (q - 8) r
<> encWord w3 (q - 12) r
optDecode _ _ = getECS
encWord :: Word32 -> Word8 -> Word8 -> SizedBuilder
encWord !w !q !r = case min 4 q of
4 -> mbWord32 (w .&. mask)
3 -> (mbWord16 . fromIntegral) (w `unsafeShiftR` 16) <>
(mbWord8 . fromIntegral) ((w `unsafeShiftR` 8) .&. mask)
2 -> (mbWord16 . fromIntegral) ((w `unsafeShiftR` 16) .&. mask)
1 -> (mbWord8 . fromIntegral) ((w `unsafeShiftR` 24) .&. mask)
_ -> mempty
where
mask | q <= 4
, s <- fromEnum $ 7 - r
, s > 0 = (0xffff_ffff `unsafeShiftR` s) `unsafeShiftL` s
| otherwise = 0xffff_ffff
-- | Decode an EDNS Client Subnet (ECS) option according to the provided
-- OPTION-LENGTH Parameter to determine how many bytes the address has been
-- truncated to.
--
-- Values of the FAMILY field other than 1 (IPv4) or 2 (IPv6) are rejected
-- and cause the decoder to fail.
getECS :: Int -- ^ OPTION-LENGTH field
-> SGet EdnsOption
getECS n = do
ecs_family <- get16
ecs_source <- get8
ecs_scope <- get8
case ecs_family of
1 -> do
ecs_addr <- getIPv4Net (n - 4)
return $ EdnsOption $ O_ECS ecs_source ecs_scope (IPv4 ecs_addr)
2 -> do
ecs_addr <- getIPv6Net (n - 4)
return $ EdnsOption $ O_ECS ecs_source ecs_scope (IPv6 ecs_addr)
f -> failSGet $ "unsupported ECS family " ++ show f
-- XXX : consider using alternate constructor instead of failure