DOCA Documentation v3.0.0

NAT64 Example

Network Address Translation (NAT64) is a network address translation mechanism designed to enable connections between IPv6 clients and IPv4 servers. NAT64 serves as a gateway, allowing devices on IPv6-only networks to access resources that are available over IPv4. Unlike RFC 6052, which uses an algorithmic mapping of IPv6 prefix to embed IPv4 addresses within IPv6 addresses, this example demonstrates Stateful NAT64 where the gateway maintains a mapping table for active flows. The mapping table maintains a per entry user policy.

When translating an IP packet, various packet fields can be saved between IPv6 and IPv4. To accommodate this, create a metadata structure to hold these temporary values:

Copy
Copied!
            

struct ip_data_t { bit<16> packet_len_field; bit<8> ttl; bit<6> tos_field; bit<2> ecn_field; bit<8> ip_protocol; }

Next, instantiate a NvHeaderDataTemplateobject. This object is used to define the header and field values that will be pushed into the packet. The template requires two items:

  • A struct containing the set of headers that will be pushed. The headers are pushed in the order in which they appear in the struct, and can be natively supported headers for custom headers.

  • An annotation @nv_header_data_fields on the NvHeaderDataTemplateobject that serves as an initializer to the struct of headers. Each header must be initialized in the order in which they appear in the structure, and each header field must be initialized in the order in which they appear in the header.

Copy
Copied!
            

struct v4_headers_t { nv_ipv4_h nat_v4; }   control nat64( inout nv_headers_t headers, in nv_standard_metadata_t std_meta, inout ip_data_t user_meta, inout nv_empty_metadata_t pkt_out_meta ) { @nv_header_data_fields( nat_v4 = { version = 4, ihl = 5, diffserv = 0, ecn = 0, total_len = 0, identification = 0, flags = 2, // Do not fragment frag_offset = 0, ttl = 64, protocol = 6, hdr_checksum = 0xf93a, // Prealculated CRC based on this pattern data src_addr = 0,          // must be zero for TCP pseudo-header CRC             dst_addr = 0, } ) NvHeaderDataTemplate<v4_headers_t>() v4DataTemplate;

The P4 action contains the logical steps to translate the incoming IPv6 packet to an IPv4 packet. The steps are:

  1. Save the necessary IPv6 fields in the user_metadata struct, such as the TTL and DSCP fields. These will be restored later.

  2. Calculate the new packet length field.

  3. Push the new IPv4 header, with mostly empty fields. The IPv4 addresses are set to zero for the pseudo-header CRC calculation. Since the push operation leaves the packet in an intermediate state, set the do_reparse = false so that the offsets for IP_START_OF_HEADER are not updated yet.

  4. Pop the IPv6 header with packet updates and reparse disabled.

  5. Change the L2 ethertype to IPv4 and trigger a reparse now the packet header rearrangement is complete.

  6. Copy the user_metadata to the new IPv4 header, and set the source and destination IP addresses.

Copy
Copied!
            

action translate64(nv_ipv4_addr_t src_ip, nv_ipv4_addr_t dst_ip) { ip64_counter.count(); user_meta.packet_len_field = headers.ipv6.payload_length; user_meta.packet_len_field += 20; user_meta.ip_protocol = headers.ipv6.next_header; user_meta.ttl = headers.ipv6.hop_limit; user_meta.tos_field = headers.ipv6.diffserv; user_meta.ecn_field = headers.ipv6.ecn; // partial TCP pseudo-header CRC calculation headers.ipv6.src_addr = 0; headers.ipv6.dst_addr = 0; // push a mostly empty IPv4 header with the partial checksum nv_push_headers(headers, v4DataTemplate, { }, NvAnchor.IP_START_OF_HEADER, 0, false, false); // remove IPv6 header without updates or reparse as packet is in mid-modification nv_pop_headers(headers, NvAnchor.IP_START_OF_HEADER, NvAnchor.L4_START_OF_HEADER, false, false); // change the ethertype to IPv4 nv_set_l2_ethertype(headers, 16w0x0800); // Trigger reparse nv_force_reparse(headers); // restore the remaining IP fields     headers.ipv4.src_addr = src_ip; headers.ipv4.dst_addr = dst_ip; headers.ipv4.ttl = user_meta.ttl; headers.ipv4.total_len = user_meta.packet_len_field; nv_set_ip_protocol(headers, user_meta.ip_protocol); nv_set_ip_ecn(headers, user_meta.ecn_field); nv_set_ip_dscp(headers, user_meta.tos_field); }

Based on the desired policy, a P4 table with the desired keyset is defined. In the apply block, the packet is determined to be a valid IPv6 packet. The table is applied during the final step; when a match is found, NAT64 is performed, and the packet is sent to a specific port.

Copy
Copied!
            

table ipv6_to_ipv4 { key = { headers.ethernet.src_addr : exact; } actions = { translate64; NoAction; } default_action = NoAction; direct_counter = ip64_counter; }   apply { if (std_meta.is_l4_ok == 1) { if (ipv6_to_ipv4.apply().hit) { nv_send_to_port(3); } } }

See below for the complete DPL example.

Copy
Copied!
            

/* * SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. * SPDX-License-Identifier: LicenseRef-NvidiaProprietary * * NVIDIA CORPORATION, its affiliates and licensors retain all intellectual * property and proprietary rights in and to this material, related * documentation and any modifications thereto. Any use, reproduction, * disclosure or distribution of this material and related documentation * without an express license agreement from NVIDIA CORPORATION or * its affiliates is strictly prohibited. */   #include <doca_model.p4> #include <doca_headers.p4> #include <doca_externs.p4> #include <doca_parser.p4>   /* * This basic application demonstrates a NAT from IPv6 to IPv4 and IPv4 to IPv6. * Unlike RFC 6052, this version of NAT64 is dynamic in the sense that it varies * per entry depending on the user's policy. */   struct ip_data_t { bit<16> packet_len_field; bit<8> ttl; bit<6> tos_field; bit<2> ecn_field; bit<8> ip_protocol; }   struct v4_headers_t { nv_ipv4_h nat_v4; }   struct v6_headers_t { nv_ipv6_h nat_v6; }   control nat64( inout nv_headers_t headers, in nv_standard_metadata_t std_meta, inout ip_data_t user_meta, inout nv_empty_metadata_t pkt_out_meta ) { NvDirectCounter(NvCounterType.PACKETS_AND_BYTES) ip64_counter;   @nv_header_data_fields( nat_v4 = { // WORD 1 version = 4, ihl = 5, diffserv = 0, ecn = 0, // WORD 2 total_len = 0, // WORD 3 identification = 0, // WORD 4 flags = 2, // Do not fragment frag_offset = 0, // WORD 5 ttl = 64, protocol = 6, // WORD 6 hdr_checksum = 0xf93a, // Prealculated CRC based on this pattern data // WORD 7 src_addr = 0, // must be zero for TCP pseudo-header CRC // WORD 9 dst_addr = 0, // must be zero for TCP pseudo-header CRC } ) NvHeaderDataTemplate<v4_headers_t>() v4DataTemplate;   action drop() { nv_drop(); }   action translate64(nv_ipv4_addr_t src_ip, nv_ipv4_addr_t dst_ip) { ip64_counter.count(); user_meta.packet_len_field = headers.ipv6.payload_length; user_meta.packet_len_field += 20; user_meta.ip_protocol = headers.ipv6.next_header; user_meta.ttl = headers.ipv6.hop_limit; user_meta.tos_field = headers.ipv6.diffserv; user_meta.ecn_field = headers.ipv6.ecn; // partial TCP pseudo-header CRC calculation headers.ipv6.src_addr = 0; headers.ipv6.dst_addr = 0; // push a mostly empty IPv4 header with the partial checksum nv_push_headers(headers, v4DataTemplate, { }, NvAnchor.IP_START_OF_HEADER, 0, false, false); // remove IPv6 header without updates or reparse as packet is in mid-modification nv_pop_headers(headers, NvAnchor.IP_START_OF_HEADER, NvAnchor.L4_START_OF_HEADER, false, false); // change the ethertype to IPv4 nv_set_l2_ethertype(headers, 16w0x0800); // Trigger reparse nv_force_reparse(headers); // restore the remaining IP fields headers.ipv4.src_addr = src_ip; headers.ipv4.dst_addr = dst_ip; headers.ipv4.ttl = user_meta.ttl; headers.ipv4.total_len = user_meta.packet_len_field; nv_set_ip_protocol(headers, user_meta.ip_protocol); nv_set_ip_ecn(headers, user_meta.ecn_field); nv_set_ip_dscp(headers, user_meta.tos_field); }   table ipv6_to_ipv4 { key = { headers.ethernet.src_addr : exact; } actions = { translate64; NoAction; } default_action = NoAction; direct_counter = ip64_counter; }   apply { if (std_meta.is_l4_ok == 1) { if (ipv6_to_ipv4.apply().hit) { nv_send_to_port(3); } } } }   control nat46( inout nv_headers_t headers, in nv_standard_metadata_t std_meta, inout ip_data_t user_meta, inout nv_empty_metadata_t pkt_out_meta ) { NvDirectCounter(NvCounterType.PACKETS_AND_BYTES) ip46_counter;   @nv_header_data_fields( nat_v6 = { // WORD 1 version = 6, diffserv = 5, ecn = 0, flow_label = 0, // WORD 3 payload_length = 0, // WORD 4 next_header = 6, hop_limit = 64, // WORD 5 src_addr = 0, // must be zero for TCP pseudo-header CRC // WORD 9 dst_addr = 0, // must be zero for TCP pseudo-header CRC } ) NvHeaderDataTemplate<v6_headers_t>() v6DataTemplate;   action drop() { nv_drop(); }   action translate46(nv_ipv6_addr_t src_ip, nv_ipv6_addr_t dst_ip) { ip46_counter.count(); user_meta.packet_len_field = headers.ipv4.total_len; user_meta.packet_len_field -= 20; user_meta.ip_protocol = headers.ipv4.protocol; user_meta.ttl = headers.ipv4.ttl; user_meta.tos_field = headers.ipv4.diffserv; user_meta.ecn_field = headers.ipv4.ecn; // partial TCP pseudo-header CRC calculation headers.ipv4.src_addr = 0; headers.ipv4.dst_addr = 0; // push a mostly empty IPv6 header and with the partial checksum nv_push_headers(headers, v6DataTemplate, { }, NvAnchor.IP_START_OF_HEADER, 0, false, false); // remove IPv4 header without updates or reparse as packet is in mid-modification nv_pop_headers(headers, NvAnchor.IP_START_OF_HEADER, NvAnchor.L4_START_OF_HEADER, false, false); // change the ethertype to IPv6 nv_set_l2_ethertype(headers, 16w0x86dd); // Trigger reparse nv_force_reparse(headers); // restore the remaining IP fields headers.ipv6.src_addr = src_ip; headers.ipv6.dst_addr = dst_ip; headers.ipv6.hop_limit = user_meta.ttl; headers.ipv6.payload_length = user_meta.packet_len_field; nv_set_ip_protocol(headers, user_meta.ip_protocol); nv_set_ip_ecn(headers, user_meta.ecn_field); nv_set_ip_dscp(headers, user_meta.tos_field); }   table ipv4_to_ipv6 { key = { headers.ethernet.src_addr : exact; } actions = { translate46; NoAction; } default_action = NoAction; direct_counter = ip46_counter; }   apply { if (std_meta.is_l4_ok == 1) { if (ipv4_to_ipv6.apply().hit) { nv_send_to_port(3); } } } }   control nat(inout nv_headers_t headers, in nv_standard_metadata_t std_meta, inout ip_data_t user_meta, inout nv_empty_metadata_t pkt_out_meta) {   nat64() v6_to_v4; nat46() v4_to_v6;   apply { if (headers.ipv6.isValid()) { v6_to_v4.apply(headers, std_meta, user_meta, pkt_out_meta); nv_send_to_port(2); } else if (headers.ipv4.isValid()) { v4_to_v6.apply(headers, std_meta, user_meta, pkt_out_meta); nv_send_to_port(2); } // unknown traffic nv_send_to_port(1); } }   NvDocaPipeline( nv_fixed_parser(), nat() ) main;

© Copyright 2025, NVIDIA. Last updated on Jul 10, 2025.