NAT64 for Contiki-NG
Overview
NAT64 allows IPv6-only IoT devices to communicate with IPv4 servers on the Internet. The border router translates between the two protocols so that any 6LoWPAN node can reach IPv4 hosts without running a dual stack.
Contiki-NG includes a built-in NAT64 gateway that runs inside the native
border router. It requires no external software, kernel modules, or
separate DNS servers — just pass --nat64 when starting the border
router. There is no longer any need for Jool, TAYGA, or another
external NAT64 implementation alongside the border router, and no
separate DNS64 resolver: the border router process handles both.
Implementation approach and trade-offs
Unlike a traditional NAT64 box, this gateway does not translate at
the IPv4/IPv6 packet level. Instead, it terminates each IoT-side flow
inside the border-router process and re-emits the payload over a
regular host socket (AF_INET, BSD socket() API):
One host UDP socket per active UDP flow.
One host TCP socket per active TCP flow, terminated on both sides (TCP splice proxy).
One unprivileged ICMP socket (
SOCK_DGRAM,IPPROTO_ICMP) per active ping flow.
Practical consequences of this design:
The gateway consumes host OS resources per session (file descriptors, kernel socket buffers). A traditional packet-level NAT64 keeps only a small in-memory binding entry per flow. The number of concurrent sessions is therefore capped — see
NAT64_MAX_SESSIONS,NAT64_MAX_TCP_SESSIONSandNAT64_MAX_SESSIONS_PER_NODEunder Configuration.No raw IPv4 socket and no privileged kernel features required. The native border router still needs
CAP_NET_ADMINfor the TUN device, but NAT64 itself runs with ordinary user-level sockets (and with the unprivileged ICMP feature for ping).Outbound, client-initiated flows only. IPv4 hosts cannot initiate connections back to IoT nodes through the gateway (no V4 INIT bindings, no hairpinning).
DNS payloads are rewritten in place (DNS64); all other UDP/TCP payloads are forwarded opaquely, so DTLS works end-to-end through the gateway.
TCP throughput is intentionally throttled to small ACK-paced segments so each segment fits a single 802.15.4 frame without 6LoWPAN fragmentation. This keeps lossy-link delivery robust at the cost of bulk-data throughput. See
NAT64_TCP_SEGMENT_SIZE.
For a full RFC 6146 compliance matrix and a list of intentionally
omitted features (hairpinning, full TCP state machine, configurable
per-protocol timeouts, …), see os/services/nat64/README.md.
How it works
IPv4 addresses are embedded in IPv6 addresses using the well-known NAT64
prefix 64:ff9b::/96 (RFC 6052). For example, the IPv4 address 8.8.8.8
is represented as 64:ff9b::808:808.
When an IoT node sends a packet to a 64:ff9b:: destination, the border
router:
Extracts the IPv4 address from the destination.
Opens a kernel socket (UDP or TCP) to the IPv4 server.
Forwards the payload.
Translates the response back into an IPv6 packet and delivers it to the IoT node.
DNS queries are handled automatically: the built-in DNS64 translator rewrites AAAA queries to A queries on the outbound path, and synthesizes AAAA records with the NAT64 prefix from A records in the response.
Quick start
1. Build the border router
cd examples/rpl-border-router
make TARGET=native
2. Start the border router with NAT64
For a serial-connected IoT network:
sudo ./build/native/border-router.native --nat64 -s /dev/ttyUSB0 fd00::1/64
For use with the Cooja simulator:
sudo ./build/native/border-router.native --nat64 -a localhost -p 60001 fd00::1/64
The --nat64 flag enables the NAT64 gateway. The last argument sets the
IPv6 prefix for the IoT network.
3. Configure IoT nodes
IoT nodes need no special configuration for UDP. To use DNS resolution,
configure the DNS server address to point to an IPv4 DNS server via the
NAT64 prefix. For example, to use Google DNS (8.8.8.8):
#define RESOLV_CONF_SUPPORTS_MDNS 0
#include "net/ipv6/uip-nameserver.h"
#include "net/ipv6/ip64-addr.h"
uip_ipaddr_t dns_server;
/* 64:ff9b::808:808 = 8.8.8.8 via NAT64 */
uip_nat64addr(&dns_server, 8, 8, 8, 8);
uip_nameserver_update(&dns_server, UIP_NAMESERVER_INFINITE_LIFETIME);
The node can then use resolv_query() to look up hostnames. The border
router’s DNS64 translator ensures that the responses contain
NAT64-prefixed IPv6 addresses.
Supported protocols
Protocol |
Support |
|---|---|
UDP |
Full — CoAP, DNS, and other UDP-based protocols work transparently. |
TCP |
Full — the border router runs a TCP splice proxy that handles connection setup, data forwarding, and teardown. |
ICMP |
Echo Request/Reply (ping) is forwarded via Linux unprivileged ICMP sockets. Destination Unreachable errors are synthesized toward the IoT node when packets cannot be delivered. Other ICMP types are not translated. |
End-to-end security and application protocols
The gateway forwards UDP and TCP payloads opaquely (the only payload it ever rewrites is DNS on port 53), so end-to-end security between the IoT node and the IPv4 server works without modification:
DTLS works over the UDP path. Note two operational caveats:
The gateway does not segment UDP. Large handshake flights (e.g., certificate-based ciphersuites) rely on 6LoWPAN fragmentation and can be slow on lossy links. PSK or raw-public-key ciphersuites keep flights small.
A UDP session that is idle longer than
NAT64_SESSION_TIMEOUT(default 5 minutes) loses its kernel-side binding. To keep a DTLS association alive across long idle periods, use DTLS Connection ID (RFC 9146) or send keepalives within the timeout window.
MQTT and other long-lived TCP sessions must send keepalives within
NAT64_SESSION_TIMEOUT, otherwise the session is reaped.
Security model
The gateway is designed to sit between an untrusted 6LoWPAN network and the upstream IPv4 Internet. The following defences are enforced unconditionally:
Forbidden IPv4 destinations are rejected before any socket is opened. Loopback, RFC 1918 private ranges, link-local, multicast, shared-address space, documentation prefixes, and other special-use ranges (RFC 5735 / RFC 6890) cannot be reached via NAT64. This prevents a compromised IoT node from using the border router as a proxy to attack the local network.
Source-prefix validation (RFC 6146 §3.5) drops IPv6 packets whose source already matches the NAT64 prefix, preventing routing loops.
Per-node session cap (
NAT64_MAX_SESSIONS_PER_NODE, default 8) limits how many concurrent sessions a single IoT node can hold, bounding resource consumption from a misbehaving or hostile node.UDP sockets are connected to the peer so the kernel filters incoming datagrams by source address. This blocks spoofed responses (a DNS cache-poisoning vector) from being forwarded into the 6LoWPAN network.
TCP initial sequence numbers are generated per RFC 6528 using HMAC-SHA-256 keyed with a secret read from
/dev/urandomat startup. The border router refuses to start if/dev/urandomis unavailable rather than fall back to a predictable key.
Example application
The examples/nat64/ directory contains a demo that performs a DNS
lookup and sends UDP probes and an HTTP GET request through NAT64:
# In one terminal, start Cooja with the NAT64 simulation:
cd examples/nat64
make nat64-dns-cooja.csc
# In another terminal, start the border router:
sudo ./run-nat64-br.sh
See examples/nat64/nat64-dns-client.c for the application code.
Configuration
Build-time options
These can be set in project-conf.h:
Option |
Default |
Description |
|---|---|---|
|
0 |
Set to 1 to enable TCP (required for HTTP). |
|
1 |
Number of DNS cache entries. |
|
1 |
Set to 0 when using only unicast DNS via NAT64. |
Gateway tuning
These are compile-time defines for the border router (set via CFLAGS
or in the border router’s project configuration):
Option |
Default |
Description |
|---|---|---|
|
128 |
Maximum concurrent NAT64 sessions. |
|
16 |
Maximum concurrent TCP connections. |
|
8 |
Per-node session limit (DoS protection). |
|
|
Idle timeout after which a UDP or TCP session binding is reaped. A flat timeout is used for both protocols; long-lived sessions must send keepalives within this window. |
|
76 |
Size in bytes of each ACK-paced TCP segment delivered to the IoT node. Chosen to fit a single IEEE 802.15.4 frame without 6LoWPAN fragmentation. Larger values improve throughput at the cost of fragment loss on lossy links. |
|
256 |
Maximum file descriptors in the select loop. |
6LoWPAN compression context (throughput tuning)
NAT64 traffic carries IPv6 source/destination addresses in the
NAT64 prefix (64:ff9b::/96 by default). Without an IPHC
compression context for that prefix, each address occupies 16
bytes inline in the compressed 6LoWPAN frame; with a context,
the upper 64 bits are derived from the context and only 8 bytes
are inline. That saves ~8 bytes per packet, which is enough
headroom to raise NAT64_TCP_SEGMENT_SIZE from 76 to ~84 bytes
(about a 10% TCP throughput improvement) and reduces 6LoWPAN
fragmentation pressure on UDP responses and DTLS handshakes.
To enable, include the helper header on both sides:
/* In the IoT node's project-conf.h: */
#include "services/nat64/nat64-6lowpan.h"
The border router picks up the matching context automatically:
examples/rpl-border-router/project-conf.h includes the same
header when MAKE_WITH_NAT64=1 is set on the make command line.
Both ends MUST agree on the prefix bytes and the context number. If only one side is configured, decompression on the other side silently drops NAT64 frames — there is no error message at the application layer. This compile-time symmetric requirement is a limitation of the current Contiki-NG 6LoWPAN implementation.
Future work: once Contiki-NG implements RFC 6775 §4.2 (6LoWPAN Context Option in Router Advertisements), the BR will advertise the context dynamically and IoT nodes will pick it up over the air, removing the recompile-everything-on-both-sides requirement.
If your deployment uses a non-standard NAT64 prefix configured
via ip64_addr_set_prefix(), override NAT64_6LOWPAN_PREFIX_BYTES
to match before including the header — both ends must use the
same value.
Standards compliance
The implementation follows RFC 6146 (Stateful NAT64) for the features
relevant to an IoT border router. See os/services/nat64/README.md for
a detailed compliance analysis, including which RFC requirements are
implemented and which are intentionally omitted with rationale.
Key RFCs:
RFC 6146 — Stateful NAT64 (core translation mechanism)
RFC 6147 — DNS64 (DNS query/response translation)
RFC 6052 — IPv6 addressing of IPv4/IPv6 translators (NAT64 prefix)
RFC 6528 — Defending against sequence number attacks (TCP ISN generation)
RFC 5735 / RFC 6890 — Special-use IPv4 address registries (forbidden destinations)
RFC 9146 — DTLS Connection ID (recommended for long-lived DTLS associations)