<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="rfc2629.xslt"?>

<rfc xmlns:xi="http://www.w3.org/2001/XInclude"
     category="info"
     docName="draft-udpn-protocol-00"
     ipr="trust200902"
     submissionType="independent"
     version="3"
     xml:lang="en">

  <front>
    <title abbrev="UDPN">UDPN: UDP Datagram Privacy Network Protocol Version 1.0</title>

    <seriesInfo name="Internet-Draft" value="draft-udpn-protocol-00"/>

    <author fullname="Denis Samsonov" initials="D." surname="Samsonov">
      <organization/>
      <address>
        <email>i@denjs.com</email>
        <uri>https://denjs.com</uri>
      </address>
    </author>

    <date year="2026" month="May"/>

    <area>Security</area>
    <workgroup>Independent Submission</workgroup>

    <keyword>VPN</keyword>
    <keyword>tunnel</keyword>
    <keyword>DPI</keyword>
    <keyword>obfuscation</keyword>
    <keyword>Noise</keyword>
    <keyword>ChaCha20</keyword>
    <keyword>DTLS</keyword>

    <abstract>
      <t>
        This document specifies the UDP Datagram Privacy Network (UDPN) protocol,
        version 1.0. UDPN provides an authenticated, encrypted Layer 3 tunnel over
        UDP with traffic obfuscation designed to resist deep packet inspection (DPI)
        and active probing. All packets are wrapped in DTLS 1.2 ApplicationData
        records. The protocol uses the Noise_NK handshake pattern with X25519
        Diffie-Hellman and ChaCha20-Poly1305 AEAD encryption.
      </t>
    </abstract>
  </front>

  <middle>

    <section anchor="intro" numbered="true" toc="default">
      <name>Introduction</name>
      <t>
        UDPN creates a Layer 3 IP tunnel over UDP suitable for networks where deep
        packet inspection, port blocking, or active probing is used against tunneling
        traffic.
      </t>
      <t>Three threat models are addressed:</t>
      <ol type="a">
        <li>
          <t>Passive observation: all payload bytes are encrypted and computationally
          indistinguishable from random data.</t>
        </li>
        <li>
          <t>Active probing: the server silently discards every packet it cannot
          cryptographically verify. An adversary receives no response whatsoever,
          making the endpoint indistinguishable from a closed UDP port.</t>
        </li>
        <li>
          <t>Traffic correlation: periodic port hopping changes the UDP 5-tuple;
          random per-packet padding obscures payload lengths; jittered keepalive
          intervals resist timing fingerprinting.</t>
        </li>
      </ol>
      <t>
        All packets are formatted as DTLS 1.2 ApplicationData records to blend with
        legitimate DTLS/CoAP traffic on the wire.
      </t>
    </section>

    <section anchor="terminology" numbered="true" toc="default">
      <name>Terminology</name>
      <t>
        The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
        "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
        document are to be interpreted as described in BCP 14
        <xref target="RFC2119"/> <xref target="RFC8174"/> when, and only when,
        they appear in all capitals.
      </t>
      <dl newline="false" spacing="normal">
        <dt>Initiator</dt>
        <dd>The client endpoint that initiates the session.</dd>
        <dt>Responder</dt>
        <dd>The server endpoint that accepts sessions.</dd>
        <dt>Session</dt>
        <dd>A cryptographic context active between a successful Noise handshake
        and the next teardown event.</dd>
        <dt>DTLS Epoch</dt>
        <dd>A 16-bit session identifier assigned by the Responder. Stable for
        the whole session lifetime. Not related to DTLS rekeying.</dd>
        <dt>Hop Epoch</dt>
        <dd>A 16-bit counter incremented by the Initiator at each port-hop
        event, carried inside the encrypted payload.</dd>
        <dt>Nonce</dt>
        <dd>The 64-bit value used as the ChaCha20-Poly1305 nonce, carried in
        the DTLS sequence-number field.</dd>
      </dl>
    </section>

    <section anchor="overview" numbered="true" toc="default">
      <name>Protocol Overview</name>
      <t>A UDPN session proceeds as follows:</t>
      <artwork type="ascii-art"><![CDATA[
   Initiator                              Responder
       |                                      |
       |  DTLS[epoch=0, seq=0]                |
       |  routing_tag(4) + Noise msg1  ──────>|
       |                                      |  verify routing_tag
       |                                      |  Noise ReadMessage1
       |                                      |  assign session_epoch
       |  DTLS[epoch=0, seq=random]           |
       |  Noise msg2  <───────────────────────|
       |                                      |
       |  derive transport keys               |  derive transport keys
       |  set DTLS epoch = session_epoch      |
       |  TUN interface UP                    |  TUN interface UP
       |                                      |
       |  DTLS[epoch=session_epoch, seq=N]    |
       |  DATA/KEEPALIVE/...  <──────────────>|
      ]]></artwork>
    </section>

    <section anchor="packet-format" numbered="true" toc="default">
      <name>Packet Format</name>

      <section anchor="dtls-header" numbered="true" toc="default">
        <name>DTLS Record Header</name>
        <t>Every UDPN packet starts with a 13-byte DTLS 1.2 record header:</t>
        <artwork type="ascii-art"><![CDATA[
    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Type = 0x17  | Ver = 0xFE    | Ver = 0xFD    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |         DTLS Epoch (16)       |   Sequence bits 47..32 (16)   |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                 Sequence bits 31..0 (32)                      |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |        Payload Length (16)    |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        ]]></artwork>
        <dl newline="false" spacing="normal">
          <dt>Type (8 bits)</dt>
          <dd>MUST be 0x17 (DTLS ApplicationData). Packets with any other
          value MUST be silently discarded.</dd>
          <dt>Version (16 bits)</dt>
          <dd>MUST be 0xFE 0xFD (DTLS 1.2) per <xref target="RFC6347"/>. Mismatch causes silent discard.</dd>
          <dt>DTLS Epoch (16 bits)</dt>
          <dd>0x0000 for handshake packets; session_epoch for transport packets.
          Stable for the session lifetime.</dd>
          <dt>Sequence (48 bits, big-endian)</dt>
          <dd>Serves as both DTLS sequence number and ChaCha20-Poly1305 nonce
          (zero-extended to 64 bits, stored little-endian per RFC 8439). MUST
          be monotonically increasing per session. MUST NOT be reset on port hops.</dd>
          <dt>Length (16 bits)</dt>
          <dd>Byte count of the following payload.</dd>
        </dl>
      </section>

      <section anchor="handshake-packets" numbered="true" toc="default">
        <name>Handshake Packets</name>
        <t>
          Handshake packets use DTLS Epoch = 0. A packet with DTLS Epoch = 0
          and DTLS payload length less than 4 bytes MUST be silently discarded.
          Such a packet cannot contain a valid routing_tag (msg1 minimum: 36+ bytes)
          nor a complete Noise msg2 (minimum 32 bytes for e_pub alone).
        </t>
        <t>The msg1 payload layout is:</t>
        <artwork type="ascii-art"><![CDATA[
   Offset  Size  Field
   ──────  ────  ─────────────────────────────────────────────
        0     4  routing_tag   BLAKE2s(e_pub||s_pub)[0:4]
        4    32  e_pub         Initiator ephemeral public key
       36     *  ciphertext    ChaCha20-Poly1305(inner_payload)
     36+*    16  AEAD_tag      Poly1305 authentication tag
        ]]></artwork>
        <t>The msg2 payload layout (no routing_tag) is:</t>
        <artwork type="ascii-art"><![CDATA[
   Offset  Size  Field
   ──────  ────  ─────────────────────────────────────────────
        0    32  e_pub         Responder ephemeral public key
       32     *  ciphertext    ChaCha20-Poly1305(inner_payload)
     32+*    16  AEAD_tag      Poly1305 authentication tag
        ]]></artwork>
        <t>
          In both messages the AEAD tag immediately follows the ciphertext,
          matching standard AEAD.Seal() output per <xref target="RFC8439"/>.
        </t>
      </section>

      <section anchor="transport-packets" numbered="true" toc="default">
        <name>Transport Packets</name>
        <t>
          Transport packets use DTLS Epoch = session_epoch. Their payload is
          the output of a single AEAD.Seal() call: ciphertext followed immediately
          by the 16-byte Poly1305 authentication tag.
        </t>
      </section>

      <section anchor="inner-header" numbered="true" toc="default">
        <name>Inner Header</name>
        <t>
          The first 8 bytes of every transport plaintext are the inner header:
        </t>
        <artwork type="ascii-art"><![CDATA[
    0                   1                   2                   3
    0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |  Type (8)     |  Flags (8)    |      Hop Epoch (16)           |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                   Inner Sequence (32)                         |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        ]]></artwork>
        <dl newline="false" spacing="normal">
          <dt>Type (8 bits)</dt>
          <dd>0x01 DATA, 0x06 KEEPALIVE, 0x07 KEEPALIVE_ACK, 0x08 DISCONNECT.
          Unknown values MUST be silently discarded.</dd>
          <dt>Flags (8 bits)</dt>
          <dd>Reserved. MUST be 0x00 on send; MUST be ignored on receive.</dd>
          <dt>Hop Epoch (16 bits, big-endian)</dt>
          <dd>Current port-hop counter, starting at 0. Wraps modulo 65536.</dd>
          <dt>Inner Sequence (32 bits, big-endian)</dt>
          <dd>Per-direction monotonic counter for sliding-window replay protection.
          Starts at 0 after each new session.</dd>
        </dl>
        <t>
          Total per-packet overhead: 13 (DTLS) + 8 (inner header) + 16 (AEAD tag)
          = 37 bytes, plus padding.
        </t>
      </section>
    </section>

    <section anchor="crypto" numbered="true" toc="default">
      <name>Cryptographic Design</name>

      <section anchor="keypair" numbered="true" toc="default">
        <name>Keypair Generation</name>
        <t>
          Each connection uses a unique X25519 keypair generated by the Responder.
          The private key is kept exclusively by the Responder. The public key is
          distributed to the Initiator out-of-band (e.g., a configuration file).
          Key clamping follows <xref target="RFC7748"/> Section 5.
        </t>
      </section>

      <section anchor="noise" numbered="true" toc="default">
        <name>Noise_NK Handshake</name>
        <t>
          UDPN uses the Noise Protocol Framework <xref target="NOISE"/> with:
        </t>
        <artwork type="ascii-art"><![CDATA[
   Pattern:  NK
   DH:       25519 (X25519, RFC 7748)
   Cipher:   ChaChaPoly (ChaCha20-Poly1305, RFC 8439)
   Hash:     SHA256

   Full protocol name: Noise_NK_25519_ChaChaPoly_SHA256

   Pattern NK:
      <- s           (premessage: Responder static public key)
      ...
      -> e, es       (msg1: Initiator ephemeral + DH)
      <- e, ee       (msg2: Responder ephemeral + DH)
        ]]></artwork>
        <t>
          The NK pattern provides: Responder authentication (Initiator knows its
          public key); Initiator anonymity (Responder learns nothing about Initiator
          identity); and forward secrecy (both ephemeral keys are discarded after
          the handshake).
        </t>
        <t>Prologue: empty byte sequence (zero bytes).</t>
      </section>

      <section anchor="transport-keys" numbered="true" toc="default">
        <name>Transport Keys</name>
        <t>
          Upon completing the handshake, both parties call split() to derive two
          ChaCha20-Poly1305 keys (c1, c2). The Initiator sends with c1 and receives
          with c2. The Responder sends with c2 and receives with c1. The transport
          key is NOT rotated within a session; each new session derives fresh
          independent keys through new ephemeral DH.
        </t>
      </section>

      <section anchor="nonce" numbered="true" toc="default">
        <name>AEAD Nonce Construction</name>
        <t>
          ChaCha20-Poly1305 requires a 96-bit (12-byte) nonce, constructed as follows:
        </t>
        <artwork type="ascii-art"><![CDATA[
   nonce[0..3]  = 0x00 0x00 0x00 0x00   (4 zero bytes)
   nonce[4..11] = DTLS Sequence          (64-bit, little-endian)

   Step-by-step:
   (1) Read DTLS Sequence from wire: 6 bytes big-endian -> uint64
       (upper 16 bits = 0).
   (2) Encode uint64 as little-endian into nonce[4..11].

   Example: DTLS Sequence = 1 (0x000000000001 on wire)
      Decoded uint64:  0x0000000000000001
      nonce[4..11]:    01 00 00 00 00 00 00 00  (little-endian)
        ]]></artwork>
        <t>
          This follows <xref target="RFC8439"/> Section 2.4. Additional Data (AD)
          passed to AEAD is always empty.
        </t>
      </section>

      <section anchor="routing-tag" numbered="true" toc="default">
        <name>Routing Tag</name>
        <t>
          The Initiator prepends a 4-byte routing tag to msg1 to allow the Responder
          to identify the target connection without attempting O(N) Noise decryptions.
          The tag uses BLAKE2s-256 <xref target="BLAKE2"/>:
        </t>
        <artwork type="ascii-art"><![CDATA[
   routing_tag = BLAKE2s-256(e_pub || s_pub)[0:4]
        ]]></artwork>
        <t>
          where e_pub is the Initiator's ephemeral public key and s_pub is the
          Responder's static public key for the target connection. The Responder
          scans connections computing BLAKE2s-256(e_pub || conn.s_pub)[0:4] until
          a match is found, then performs one full Noise decryption to authenticate.
        </t>
        <t>
          Note: the routing tag does not eliminate the X25519 operation. It reduces
          Nx(X25519+AEAD) to NxBLAKE2s + 1x(X25519+AEAD). At N=1000
          this is approximately 500ms reduced to 1ms for the lookup phase.
        </t>
        <t>
          Collision probability is approximately N/2^32 per handshake attempt. On
          collision, both matching connections are tried; the AEAD step resolves
          the ambiguity.
        </t>
      </section>
    </section>

    <section anchor="handshake" numbered="true" toc="default">
      <name>Handshake</name>

      <section anchor="msg1" numbered="true" toc="default">
        <name>Initiator Handshake Message (msg1)</name>
        <t>
          The Initiator sends a UDP packet with DTLS Epoch = 0 and Sequence = 0.
          The DTLS payload contains: routing_tag (4 bytes), followed by the Noise
          msg1 bytes (e_pub + ciphertext + tag).
        </t>
        <t>The encrypted inner_payload contains:</t>
        <artwork type="ascii-art"><![CDATA[
   Offset  Size  Field
   ──────  ────  ─────────────────────────────────────────────
        0     8  conn_id_hint  BLAKE2s derivative of conn id
        8     2  hop_interval  Hop interval in minutes (uint16 BE)
       10     4  pool_hash     HMAC-SHA256 of port pool (uint32 BE,
                               first 4 bytes)
       14     P  padding       P random bytes (P >= padding.min)
        ]]></artwork>
        <t>
          The conn_id_hint, hop_interval, and pool_hash fields are informational.
          The Responder validates minimum payload size (14 bytes) but does not
          use these values for session routing or authentication.
        </t>
      </section>

      <section anchor="msg2" numbered="true" toc="default">
        <name>Responder Handshake Message (msg2)</name>
        <t>
          The Responder replies with DTLS Epoch = 0 and a random Sequence value.
          The DTLS payload is the Noise msg2 bytes (e_pub + ciphertext + tag).
        </t>
        <t>The encrypted inner_payload contains:</t>
        <artwork type="ascii-art"><![CDATA[
   Offset  Size  Field
   ──────  ────  ─────────────────────────────────────────────
        0     2  session_epoch  DTLS epoch for this session (uint16 BE)
        2     P  padding        P random bytes
        ]]></artwork>
        <t>
          session_epoch is a randomly chosen value in [0x0001..0xFFFE]. The
          Responder MUST NOT assign session_epoch = 0x0000, as zero is reserved to
          identify handshake packets; transport packets with epoch 0 would be
          indistinguishable from handshake packets. The Responder MUST also verify
          the epoch is not in use by another active session; if a collision is found,
          a new value MUST be drawn.
        </t>
      </section>

      <section anchor="post-handshake" numbered="true" toc="default">
        <name>Post-Handshake State</name>
        <t>After a successful exchange, both parties:</t>
        <ol type="a">
          <li><t>Derive transport keys via split().</t></li>
          <li><t>Reset the DTLS Sequence (Noise nonce) counter to 0.</t></li>
          <li><t>Reset the Inner Sequence counter to 0.</t></li>
          <li><t>Reset the inner sliding-window replay state.</t></li>
        </ol>
        <t>The Initiator additionally sets its DTLS Epoch to session_epoch and
        brings the TUN interface UP. The Responder records the session DTLS Epoch,
        Initiator src IP:port, and local UDP port, and brings its TUN interface UP.
        If a session already existed for this connection, it is evicted first.</t>
      </section>
    </section>

    <section anchor="transport" numbered="true" toc="default">
      <name>Transport</name>

      <section anchor="sending" numbered="true" toc="default">
        <name>Sending a Packet</name>
        <ol spacing="normal" type="1">
          <li><t>Claim nonce N = dtlsSeqTx.fetch_add(1) atomically.</t></li>
          <li><t>Claim Inner Sequence S = innerTxSeq.fetch_add(1) atomically.</t></li>
          <li><t>Choose padding P = padding.min + PRNG(padding.max + 1).</t></li>
          <li><t>Construct plaintext: inner_header (8 bytes) || L3 packet || P zero bytes.</t></li>
          <li><t>Encrypt: ciphertext = AEAD_Encrypt(send_key, nonce=N, AD="", plaintext).</t></li>
          <li><t>Construct DTLS record: Type=0x17, Ver=0xFEFD, Epoch=session_epoch,
          Seq=N, Length=len(ciphertext).</t></li>
          <li><t>Enqueue for transmission.</t></li>
        </ol>
      </section>

      <section anchor="receiving" numbered="true" toc="default">
        <name>Receiving a Packet</name>
        <ol spacing="normal" type="1">
          <li><t>Check DTLS Type=0x17, Version=0xFEFD. On mismatch: DISCARD silently.</t></li>
          <li><t>If DTLS Epoch = 0: route to handshake processing.</t></li>
          <li><t>Look up active session by DTLS Epoch. If none: DISCARD silently.</t></li>
          <li><t>ACL check (server only). On failure: DISCARD, increment acl_drop.</t></li>
          <li><t>Decrypt: plaintext = AEAD_Decrypt(recv_key, nonce=DTLS_Seq, AD="", ciphertext).
          On failure: DISCARD, increment replay_drop.</t></li>
          <li><t>Parse inner header from plaintext[0..7].</t></li>
          <li><t>Sliding-window check on Inner Sequence. On failure: DISCARD, increment replay_drop.</t></li>
          <li><t>Update keepalive watchdog (touch last-seen timestamp).</t></li>
          <li><t>Server only: update routing state (see <xref target="hop-reception"/>).</t></li>
          <li><t>Dispatch on inner Type (see <xref target="inner-types"/>).</t></li>
        </ol>
      </section>

      <section anchor="inner-types" numbered="true" toc="default">
        <name>Inner Packet Types</name>
        <dl newline="false" spacing="normal">
          <dt>DATA (0x01)</dt>
          <dd>Write payload bytes (offset 8+) to the TUN interface. The IP/IPv6
          header length field defines the packet boundary; trailing padding is
          ignored.</dd>
          <dt>KEEPALIVE (0x06)</dt>
          <dd>Respond asynchronously with KEEPALIVE_ACK.</dd>
          <dt>KEEPALIVE_ACK (0x07)</dt>
          <dd>No action beyond the watchdog touch in step 8.</dd>
          <dt>DISCONNECT (0x08)</dt>
          <dd>Stop keepalive manager, bring TUN DOWN, clear session state.
          Sender SHOULD transmit three copies to improve delivery probability.</dd>
        </dl>
      </section>
    </section>

    <section anchor="keepalive" numbered="true" toc="default">
      <name>Keepalive</name>
      <t>
        Both endpoints send KEEPALIVE packets at random intervals drawn from
        [T*0.8, T*1.2] where T is the configured keepalive interval in seconds.
        Jitter prevents interval-based fingerprinting.
      </t>
      <t>
        The watchdog fires after T * timeout_factor seconds of inactivity
        (default timeout_factor = 3, minimum 1). On timeout, the Initiator tears
        down the session and schedules reconnection; the Responder tears down and
        waits for a new handshake.
      </t>
    </section>

    <section anchor="port-hopping" numbered="true" toc="default">
      <name>Port Hopping</name>

      <section anchor="port-selection" numbered="true" toc="default">
        <name>Port Selection</name>
        <t>Given a sorted pool P of N port numbers:</t>
        <artwork type="ascii-art"><![CDATA[
   SelectPort(key, session_id, epoch, direction, P):
      mac = HMAC-SHA256(key=key,
                        data=uint64_BE(session_id) || uint16_BE(epoch)
                             || uint8(direction))
      index = uint16_BE(mac[0:2]) mod N
      return P[index]

   direction = 0 for destination port
   direction = 1 for source port
        ]]></artwork>
        <t>
          The direction byte ensures dst_port and src_port are derived
          independently, eliminating structural collisions for all pool sizes.
          If SelectPort(key, sid, epoch, 1, P) == SelectPort(key, sid, epoch, 0, P),
          the source port MUST be advanced to the next entry in the pool.
        </t>
        <t>In UDPN v1.0, key = Responder static public key, session_id = 0.</t>
      </section>

      <section anchor="hop-procedure" numbered="true" toc="default">
        <name>Hop Procedure (Initiator)</name>
        <t>At each hop_interval (minutes):</t>
        <ol spacing="normal" type="1">
          <li><t>Increment hop_epoch by 1 (mod 65536).</t></li>
          <li><t>Compute dst_port = SelectPort(s_pub, 0, hop_epoch, 0, pool).</t></li>
          <li><t>Compute src_port = SelectPort(s_pub, 0, hop_epoch, 1, pool).</t></li>
          <li><t>Update internal dstAddr to (server_ip, dst_port).</t></li>
          <li><t>Rebind local UDP socket to src_port. On bind failure, try other
          pool ports (excluding dst_port). If all fail, skip this hop.</t></li>
          <li><t>Begin sending with Hop Epoch = hop_epoch in the inner header.</t></li>
        </ol>
        <t>
          The DTLS Sequence (Noise nonce) MUST NOT be reset on a hop event.
          Resetting would cause replay detection failures on the Responder.
        </t>
      </section>

      <section anchor="hop-reception" numbered="true" toc="default">
        <name>Hop Reception (Responder)</name>
        <t>
          In step 9 of <xref target="receiving"/>, the Responder updates routing
          state as follows:
        </t>
        <t>If inner.HopEpoch == current hop_epoch:</t>
        <ul>
          <li><t>Update dstAddr to packet's src IP:port.</t></li>
          <li><t>Update replyPort to packet's dst port.</t></li>
        </ul>
        <t>If inner.HopEpoch != current hop_epoch, compute
        delta = (incoming - current) mod 65536:</t>
        <dl newline="false" spacing="normal">
          <dt>delta in [1..4] — ACCEPT</dt>
          <dd>Update hop_epoch, dstAddr, replyPort. Touch keepalive watchdog.</dd>
          <dt>delta in [5..32767] — REJECT</dt>
          <dd>hop_epoch, dstAddr, and replyPort are NOT updated. The packet
          payload IS delivered normally (AEAD already verified authenticity).
          Log a warning.</dd>
          <dt>delta in [32768..65535] — ignore</dt>
          <dd>Old epoch (reordered packet). Process payload, ignore hop field.</dd>
        </dl>
        <t>
          After a hop, packets arriving with the old hop_epoch are still accepted
          as long as they pass AEAD verification (same session key across hops)
          and the inner sequence window check. No explicit grace timer is required.
        </t>
      </section>
    </section>

    <section anchor="replay" numbered="true" toc="default">
      <name>Replay Protection</name>

      <section anchor="aead-replay" numbered="true" toc="default">
        <name>AEAD Layer</name>
        <t>
          The DTLS Sequence is a monotonically increasing uint64 counter per session,
          never reset within a session. Replay of any packet with a previously used
          DTLS Sequence fails AEAD verification. This is the primary replay barrier.
        </t>
      </section>

      <section anchor="window-replay" numbered="true" toc="default">
        <name>Inner Sequence Window</name>
        <t>
          A 1024-packet sliding window operates on the 32-bit Inner Sequence field,
          providing secondary duplicate detection for reordered packets (e.g., ECMP
          path changes or Wi-Fi retransmissions).
        </t>
        <t>Let WINDOW_SIZE = 1024. Processing Inner Sequence S (uint32):</t>
        <artwork type="ascii-art"><![CDATA[
   diff = (S - maxSeen) mod 2^32

   if diff < 2^31:                      # S is newer than maxSeen
      if diff >= WINDOW_SIZE:
         bits = 0                        # far ahead -- reset bitmap
      elif diff > 0:
         bits <<= diff                   # slide window forward
      maxSeen = S
      bits[0] |= 1                       # mark S as received
      return ACCEPT

   else:                                 # S is older than maxSeen
      offset = (maxSeen - S) mod 2^32
      if offset >= WINDOW_SIZE: return DISCARD   # too old
      if bit[offset] is set:   return DISCARD   # duplicate
      set bit[offset]
      return ACCEPT
        ]]></artwork>
        <t>
          The window is reset at each new session. Window size rationale: 1024
          absorbs reordering from ECMP routing and Wi-Fi retransmission buffers
          without false positives. WireGuard uses 2048; OpenVPN uses 128.
          Implementation: the 1024-bit bitmap is stored as [16]uint64.
        </t>
      </section>

      <section anchor="handshake-replay" numbered="true" toc="default">
        <name>Handshake Replay</name>
        <t>
          The Responder maintains a TTL cache of Initiator ephemeral public keys
          (e_pub) observed in msg1, with a TTL of 30 minutes. If an e_pub is seen
          a second time, the packet is silently discarded. Memory: approximately
          32 bytes per entry; at 1 handshake/second over 30 minutes ≈ 57 KB.
        </t>
      </section>
    </section>

    <section anchor="padding" numbered="true" toc="default">
      <name>Padding</name>
      <t>
        Each outgoing packet includes random padding:
        pad_length = padding.min + PRNG(padding.max + 1),
        where PRNG is a non-cryptographic uniform PRNG (PCG algorithm).
        Padding length is not security-sensitive; only unpredictability of sizes
        is required. Default values: padding.min = 16, padding.max = 128.
        Padding bytes on the wire are zero; receivers ignore them.
      </t>
    </section>

    <section anchor="lifecycle" numbered="true" toc="default">
      <name>Session Lifecycle</name>
      <artwork type="ascii-art"><![CDATA[
   IDLE ──────────────────────────────────────> CONNECTING
   CONNECTING ─(msg2 received)────────────────> ESTABLISHED
   CONNECTING ─(timeout * 3 attempts)─────────> IDLE (reconnect delay)
   ESTABLISHED ─(keepalive timeout)────────────> IDLE (reconnect delay)
   ESTABLISHED ─(DISCONNECT received)──────────> IDLE (reconnect delay)
   ESTABLISHED ─(hop timer fires)──────────────> HOPPING -> ESTABLISHED
      ]]></artwork>
      <t>
        Initiator reconnect delay: configurable (default 5 seconds).
        On graceful shutdown (SIGTERM/SIGINT), three DISCONNECT packets are sent
        in quick succession, followed by a 300 ms drain delay before exit.
      </t>
    </section>

    <section anchor="security" numbered="true" toc="default">
      <name>Security Considerations</name>

      <section anchor="probing" numbered="true" toc="default">
        <name>Active Probing Resistance</name>
        <t>
          The Responder MUST silently discard: packets with DTLS type other than
          0x17; packets with DTLS version other than 0xFEFD; epoch-0 packets failing
          Noise msg1 decryption; transport packets failing AEAD verification; packets
          with epoch not matching any active session. Under no circumstances MUST the
          Responder send any response to an unauthenticated packet, including ICMP
          Port Unreachable.
        </t>
      </section>

      <section anchor="forward-secrecy" numbered="true" toc="default">
        <name>Forward Secrecy</name>
        <t>
          The Noise_NK handshake derives session keys from a combination of the
          Initiator's fresh ephemeral key pair and the Responder's fresh ephemeral
          key pair. Compromise of the Responder's static private key after a session
          concludes does NOT reveal session keys.
        </t>
      </section>

      <section anchor="replay-security" numbered="true" toc="default">
        <name>Replay Attacks</name>
        <t>Three independent mechanisms prevent replay: (a) handshake ephemeral
        key cache; (b) AEAD nonce monotonicity; (c) inner sequence sliding window.</t>
      </section>

      <section anchor="traffic-analysis" numbered="true" toc="default">
        <name>Traffic Analysis</name>
        <t>
          Mitigations: random per-packet padding obscures sizes; keepalive jitter
          (+/-20%) obscures timing; port hopping changes the UDP 5-tuple.
          Limitations: total traffic volume and packet rate are not obscured; an
          adversary observing traffic before and after a hop may correlate flows
          by timing proximity.
        </t>
      </section>

      <section anchor="chacha-rationale" numbered="true" toc="default">
        <name>Choice of ChaCha20-Poly1305</name>
        <t>
          ChaCha20-Poly1305 is preferred over AES-256-GCM because its software
          implementation is constant-time regardless of hardware AES support.
          AES-GCM without AES-NI instructions is vulnerable to cache-timing attacks
          <xref target="BERNSTEIN"/>. On virtualised platforms (KVM/QEMU), AES-NI
          passthrough cannot be universally guaranteed.
        </t>
      </section>

      <section anchor="pmtud" numbered="true" toc="default">
        <name>Path MTU Discovery</name>
        <t>
          UDPN does not implement PMTUD for the outer UDP encapsulation; outer
          packets are sent without the DF bit. Inner oversized L3 packets are
          dropped and replaced with ICMP Fragmentation Needed (IPv4,
          <xref target="RFC1191"/>) or ICMPv6 Packet Too Big (IPv6,
          <xref target="RFC1981"/>) messages. Operators SHOULD configure
          tun_mtu = min(path_mtu, 1500) - 37.
        </t>
      </section>
    </section>

    <section anchor="impl-notes" numbered="true" toc="default">
      <name>Implementation Notes</name>

      <section anchor="impl-tun" numbered="true" toc="default">
        <name>TUN Interface</name>
        <t>
          The TUN file descriptor MUST be opened without O_NONBLOCK. Reads are
          performed via blocking syscall with the goroutine locked to its OS thread
          (runtime.LockOSThread). TUN MTU = configured_mtu - 37.
        </t>
      </section>

      <section anchor="impl-epoll" numbered="true" toc="default">
        <name>Server Transport (epoll)</name>
        <t>
          With N listening sockets (typically 257), a naive one-goroutine-per-socket
          model creates N OS threads blocked in recvfrom(2). The RECOMMENDED approach
          is edge-triggered epoll(7) with 2 worker goroutines, each draining ready
          file descriptors until EAGAIN.
        </t>
      </section>

      <section anchor="impl-sendmmsg" numbered="true" toc="default">
        <name>Batch UDP Send (sendmmsg)</name>
        <t>
          Implementations SHOULD use sendmmsg(2) to send multiple outgoing packets
          per syscall. A batch size of 32 is RECOMMENDED. Fall back to individual
          sendmsg(2) if unavailable.
        </t>
      </section>

      <section anchor="impl-lockfree" numbered="true" toc="default">
        <name>Lock-Free Encrypt Path</name>
        <t>For maximum throughput, the encrypt hot path SHOULD be lock-free:</t>
        <ul>
          <li><t>Transport state pointer: atomic.Pointer (nil = no session).</t></li>
          <li><t>DTLS epoch and Hop epoch: packed into a single atomic uint32.</t></li>
          <li><t>DTLS Sequence (Noise nonce): atomic uint64, incremented with Add.</t></li>
          <li><t>Inner Sequence: atomic uint32, incremented with Add.</t></li>
        </ul>
        <t>
          Multiple goroutines can encrypt concurrently without mutex contention,
          each atomically claiming a unique nonce. The AEAD object (cipher.AEAD)
          is created once at session start and reused; Seal/Open are goroutine-safe.
        </t>
      </section>
    </section>

    <section anchor="iana" numbered="true" toc="default">
      <name>IANA Considerations</name>
      <t>
        This document has no IANA actions. UDPN uses UDP ports chosen by the
        operator. The ciphertext carried in DTLS content type 0x17 is
        indistinguishable from random data and is not registered with IANA.
      </t>
    </section>

  </middle>

  <back>
    <references>
      <name>References</name>

      <references>
        <name>Normative References</name>

        <reference anchor="RFC2119">
          <front>
            <title>Key words for use in RFCs to Indicate Requirement Levels</title>
            <author fullname="S. Bradner" initials="S." surname="Bradner"/>
            <date month="March" year="1997"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="2119"/>
          <seriesInfo name="DOI" value="10.17487/RFC2119"/>
        </reference>

        <reference anchor="RFC8174">
          <front>
            <title>Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words</title>
            <author fullname="B. Leiba" initials="B." surname="Leiba"/>
            <date month="May" year="2017"/>
          </front>
          <seriesInfo name="BCP" value="14"/>
          <seriesInfo name="RFC" value="8174"/>
          <seriesInfo name="DOI" value="10.17487/RFC8174"/>
        </reference>

        <reference anchor="RFC7748">
          <front>
            <title>Elliptic Curves for Security</title>
            <author fullname="A. Langley" initials="A." surname="Langley"/>
            <author fullname="M. Hamburg" initials="M." surname="Hamburg"/>
            <author fullname="S. Turner" initials="S." surname="Turner"/>
            <date month="January" year="2016"/>
          </front>
          <seriesInfo name="RFC" value="7748"/>
          <seriesInfo name="DOI" value="10.17487/RFC7748"/>
        </reference>

        <reference anchor="RFC8439">
          <front>
            <title>ChaCha20 and Poly1305 for IETF Protocols</title>
            <author fullname="Y. Nir" initials="Y." surname="Nir"/>
            <author fullname="A. Langley" initials="A." surname="Langley"/>
            <date month="June" year="2018"/>
          </front>
          <seriesInfo name="RFC" value="8439"/>
          <seriesInfo name="DOI" value="10.17487/RFC8439"/>
        </reference>

        <reference anchor="RFC6347">
          <front>
            <title>Datagram Transport Layer Security Version 1.2</title>
            <author fullname="E. Rescorla" initials="E." surname="Rescorla"/>
            <author fullname="N. Modadugu" initials="N." surname="Modadugu"/>
            <date month="January" year="2012"/>
          </front>
          <seriesInfo name="RFC" value="6347"/>
          <seriesInfo name="DOI" value="10.17487/RFC6347"/>
        </reference>

      </references>

      <references>
        <name>Informative References</name>

        <reference anchor="NOISE">
          <front>
            <title>The Noise Protocol Framework, Revision 34</title>
            <author fullname="T. Perrin" initials="T." surname="Perrin"/>
            <date year="2018"/>
          </front>
          <refcontent>https://noiseprotocol.org/noise.html</refcontent>
        </reference>

        <reference anchor="BLAKE2">
          <front>
            <title>BLAKE2: simpler, smaller, fast as MD5</title>
            <author fullname="J-P. Aumasson" initials="J-P." surname="Aumasson"/>
            <author fullname="S. Neves" initials="S." surname="Neves"/>
            <author fullname="Z. Wilcox-O'Hearn" initials="Z." surname="Wilcox-O'Hearn"/>
            <author fullname="C. Winnerlein" initials="C." surname="Winnerlein"/>
            <date year="2013"/>
          </front>
          <refcontent>https://www.blake2.net/</refcontent>
        </reference>

        <reference anchor="BERNSTEIN">
          <front>
            <title>Cache-timing attacks on AES</title>
            <author fullname="D.J. Bernstein" initials="D.J." surname="Bernstein"/>
            <date year="2005"/>
          </front>
          <refcontent>https://cr.yp.to/antiforgery/cachetiming-20050414.pdf</refcontent>
        </reference>

        <reference anchor="RFC1191">
          <front>
            <title>Path MTU Discovery</title>
            <author fullname="J. Mogul" initials="J." surname="Mogul"/>
            <author fullname="S. Deering" initials="S." surname="Deering"/>
            <date month="November" year="1990"/>
          </front>
          <seriesInfo name="RFC" value="1191"/>
          <seriesInfo name="DOI" value="10.17487/RFC1191"/>
        </reference>

        <reference anchor="RFC1981">
          <front>
            <title>Path MTU Discovery for IP version 6</title>
            <author fullname="J. McCann" initials="J." surname="McCann"/>
            <author fullname="S. Deering" initials="S." surname="Deering"/>
            <author fullname="J. Mogul" initials="J." surname="Mogul"/>
            <date month="August" year="1996"/>
          </front>
          <seriesInfo name="RFC" value="1981"/>
          <seriesInfo name="DOI" value="10.17487/RFC1981"/>
        </reference>

      </references>
    </references>
  </back>

</rfc>
