dtls

package
v0.9.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: MIT Imports: 19 Imported by: 0

README

internal/dtls

The subset of DTLS 1.2 the datagram VPN channels need, in both client and server roles. Two handshake shapes:

  • PSK + AES-GCM — AnyConnect's data channel, where the pre-shared key comes from the already-established CSTP/TLS session via an RFC 5705 exporter.
  • ECDHE-ECDSA + AES-GCM — Fortinet's data channel, a certificate-based key exchange (Fortinet gateways present an ECDSA cert).

Deliberately not a general-purpose DTLS stack: only AEAD suites (no CBC/MAC, so no padding-oracle surface), built on the standard library's AES-GCM/HMAC/SHA-2.

Specifications

  • RFC 6347 — DTLS 1.2 (record layer, flights, retransmission, cookie).
  • RFC 4279 — PSK cipher suites; RFC 5705 — keying-material exporter (AnyConnect PSK).
  • RFC 5289 — ECDHE-ECDSA AES-GCM suites (Fortinet).

Handshake flights

DTLS runs over UDP, so it adds a stateless cookie (anti-amplification) and flight retransmission on top of the TLS handshake:

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: ClientHello (no cookie)
    S->>C: HelloVerifyRequest (cookie)
    C->>S: ClientHello (cookie echoed)
    Note over S: cookie verifies source address — no state until now
    S->>C: ServerHello [, Certificate, ServerKeyExchange] , ServerHelloDone
    C->>S: [ClientKeyExchange,] ChangeCipherSpec, Finished
    Note over S: install epoch-1 read keys, verify client Finished
    S->>C: ChangeCipherSpec, Finished
    Note over C,S: epoch 1 — application datagrams

API surface

  • Client(conn net.Conn, cfg Config) (*Conn, error) / Server(conn, cfg).
  • ConfigPSK, Certificate, RootCAs, InsecureSkipVerify, HandshakeTimeout.
  • Connnet.Conn; Read returns io.EOF on a decrypted close_notify.
  • Peek helpers for the udpmux admission callback: IsClientHello(datagram), ClientHelloSessionID(datagram).

Implementation notes & caveats

  • The coalesced-final-flight bug is fixed here — don't reintroduce it. GnuTLS (hence openconnect) packs ClientKeyExchange + ChangeCipherSpec + Finished into one datagram. The server reads flight 4 in two passes with key installation between them, so the epoch-1 Finished arrives before its keys exist. Undecryptable records are now stashed and replayed (deferred records, drainDeferred) after the keys install, rather than dropped. Regression-tested network-free by TestECDHEServerAcceptsCoalescedFinalFlight.
  • HelloVerifyRequest is mandatory server-side — no per-peer state is allocated until the client echoes a valid source-bound cookie (DTLS's anti-amplification defence). Pairs with udpmux's admission model.
  • AnyConnect needs the RFC 5705 exporter, which requires TLS 1.3 or EMS. If the CSTP/TLS session offers neither, the PSK is underivable and a silent fallback to the TLS tunnel is the correct behaviour — see the AnyConnect docs.
  • The replay window is per-epoch (RFC 6347), distinct from every other window in the tree; not the shared internal/replay.
  • The record layer is allocation-guarded. seal allocates once (the explicit nonce is written into the output buffer and Seal appends the ciphertext after it) and open not at all (in place). Each aeadState seals xor opens and runs single-goroutine (Conn.Write under writeMu, Conn.Read under readMu), so the reused nonce/additional-data scratch needs no lock and does not escape. TestRecordAllocations pins it; numbers are in the root README.md.
  • The package doc comment predates the ECDHE work and mentions only PSK; the cert-based path is real (see Config.Certificate/RootCAs and the ECDHE tests).

Documentation

Overview

Package dtls implements the subset of DTLS 1.2 (RFC 6347) that the AnyConnect data channel needs: a pre-shared-key handshake with AES-GCM, in both the client and server roles.

It is deliberately not a general-purpose DTLS stack. AnyConnect's PSK-NEGOTIATE mode derives the pre-shared key from the already-established CSTP/TLS session with an RFC 5705 exporter, so there are no certificates, no chain validation and no key exchange to negotiate — which removes most of what makes DTLS large. What remains is the record layer, the PSK handshake flights, and the reliability machinery DTLS needs because it runs over UDP: retransmission, fragmentation and reassembly, and replay detection.

Only AEAD cipher suites are supported, so there is no CBC/MAC-then-encrypt path and none of the padding-oracle surface that comes with it. Everything is built on the standard library's AES-GCM, HMAC and SHA-2.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClientHelloSessionID

func ClientHelloSessionID(datagram []byte) ([]byte, bool)

ClientHelloSessionID extracts the session-id from a datagram that looks like an initial ClientHello, reporting false for anything else.

func IsClientHello

func IsClientHello(datagram []byte) bool

IsClientHello reports whether a datagram is an initial ClientHello. It is for a server whose sessions are not keyed by anything in the hello — Fortinet's certificate-based channel authorises the flow afterwards, with a cookie inside the established session, so all the demultiplexer needs to know here is that this datagram is plausibly the start of a handshake.

Types

type Config

type Config struct {
	// PSK is the pre-shared key. For AnyConnect it comes from an RFC 5705
	// exporter on the CSTP/TLS session, so it is already bound to that session.
	PSK []byte
	// PSKIdentity names the key. AnyConnect uses a fixed identity.
	PSKIdentity []byte
	// SessionID is placed in the ClientHello's session-id field. AnyConnect
	// carries the hex-decoded X-DTLS-App-ID here, which is how a server ties the
	// UDP flow back to the HTTPS session that authorised it.
	SessionID []byte

	// Certificate is the server's certificate and key for a certificate-based
	// (ECDHE-ECDSA) handshake. When set, the connection uses ECDHE rather than
	// PSK. It is ignored on a client.
	Certificate *tls.Certificate
	// InsecureSkipVerify, on a client, skips X.509 chain and hostname validation
	// of the server certificate. The ServerKeyExchange signature is still checked
	// against the presented certificate regardless -- that is what proves the
	// server holds the key -- so this relaxes only trust in the issuer.
	InsecureSkipVerify bool
	// VerifyPeerCertificate, on a client, receives the server's raw certificate
	// chain to pin or otherwise check it; a non-nil error aborts the handshake.
	VerifyPeerCertificate func(rawCerts [][]byte) error
	// ServerName is the expected certificate hostname, checked during chain
	// validation unless InsecureSkipVerify is set.
	ServerName string
	// RootCAs is the set of trust anchors chain validation uses; nil uses the
	// host's. A private gateway signed by a private CA is the ordinary case here.
	RootCAs *x509.CertPool

	// MTU bounds handshake fragments. Zero uses a conservative default.
	MTU int
	// HandshakeTimeout bounds the whole handshake.
	HandshakeTimeout time.Duration
}

Config parameters one DTLS connection.

It selects the key exchange by what is set: a PSK gives the AnyConnect pre-shared-key handshake, and a Certificate gives Fortinet's certificate-based ECDHE handshake. The two are never mixed on one connection.

type Conn

type Conn struct {
	// contains filtered or unexported fields
}

Conn is an established DTLS connection carrying application datagrams. It wraps a net.Conn that must deliver whole datagrams — a connected UDP socket, or one side of a demultiplexing server.

Read and Write are datagram-oriented: each Write becomes one record and each Read returns one record's payload, so unlike a stream there is no framing for the caller to do.

func Client

func Client(conn net.Conn, cfg Config) (*Conn, error)

Client performs a DTLS handshake in the client role and returns the established connection.

func Server

func Server(conn net.Conn, cfg Config) (*Conn, error)

Server performs a DTLS handshake in the server role. It answers the client's first ClientHello with a HelloVerifyRequest, so no state is kept for a peer that has not proven it can receive at its claimed address.

func (*Conn) CipherSuite

func (c *Conn) CipherSuite() uint16

CipherSuite reports the negotiated suite, for logging.

func (*Conn) Close

func (c *Conn) Close() error

Close sends a close_notify and closes the underlying connection.

func (*Conn) LocalAddr

func (c *Conn) LocalAddr() net.Addr

LocalAddr and RemoteAddr expose the underlying connection's addresses.

func (*Conn) Read

func (c *Conn) Read(b []byte) (int, error)

Read returns the payload of the next application record.

func (*Conn) RemoteAddr

func (c *Conn) RemoteAddr() net.Addr

func (*Conn) SetDeadline

func (c *Conn) SetDeadline(t time.Time) error

SetDeadline and friends pass through to the underlying connection.

func (*Conn) SetReadDeadline

func (c *Conn) SetReadDeadline(t time.Time) error

func (*Conn) SetWriteDeadline

func (c *Conn) SetWriteDeadline(t time.Time) error

func (*Conn) Write

func (c *Conn) Write(b []byte) (int, error)

Write sends one application datagram.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL