
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
    <channel>
        <title><![CDATA[ The Cloudflare Blog ]]></title>
        <description><![CDATA[ Get the latest news on how products at Cloudflare are built, technologies used, and join the teams helping to build a better Internet. ]]></description>
        <link>https://blog.cloudflare.com</link>
        <atom:link href="https://blog.cloudflare.com/" rel="self" type="application/rss+xml"/>
        <language>en-us</language>
        <image>
            <url>https://blog.cloudflare.com/favicon.png</url>
            <title>The Cloudflare Blog</title>
            <link>https://blog.cloudflare.com</link>
        </image>
        <lastBuildDate>Thu, 02 Apr 2026 06:40:05 GMT</lastBuildDate>
        <item>
            <title><![CDATA[Lost in transit: debugging dropped packets from negative header lengths]]></title>
            <link>https://blog.cloudflare.com/lost-in-transit-debugging-dropped-packets-from-negative-header-lengths/</link>
            <pubDate>Mon, 26 Jun 2023 13:00:56 GMT</pubDate>
            <description><![CDATA[ In this post, we'll provide some insight into the process of investigating networking issues and how to begin debugging issues in the kernel using pwru and kprobe tracepoints ]]></description>
            <content:encoded><![CDATA[ <p></p><p>Previously, I wrote about building <a href="/high-availability-load-balancers-with-maglev/">network load balancers with the maglev scheduler</a>, which we use for ingress into our Kubernetes clusters. At the time of that post we were using Foo-over-UDP encapsulation with virtual interfaces, one for each Internet Protocol version for each worker node.</p><p>To reduce operational toil managing the traffic director nodes, we've recently switched to using IP Virtual Server's (IPVS) native support for encapsulation. Much to our surprise, instead of a smooth change, we instead observed significant drops in bandwidth and failing API requests. In this post I'll discuss the impact observed, the multi-week search for the root cause, and the ultimate fix.</p>
    <div>
      <h3>Recap and the change</h3>
      <a href="#recap-and-the-change">
        
      </a>
    </div>
    <p>To support our requirements we've been creating virtual interfaces on our traffic directors configured to encapsulate traffic with Foo-Over-UDP (FOU). In this encapsulation new UDP and IP headers are added to the original packet. When the worker node receives this packet, the kernel removes the outer headers and injects the inner packet back into the network stack. Each virtual interface would be assigned a private IP, which would be configured to send traffic to these private IPs in "direct" mode.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/166RG9mgcpaP80xGDieN46/a25fe61233073af4b3132591bb379353/image4-24.png" />
            
            </figure><p>This configuration presents several problems for our operations teams.</p><p>For example, each Kubernetes worker node needs a separate virtual interface on the traffic director, and each of the interfaces requires their own private IP. The pairs of virtual interfaces and private IPs were only used by this system, but still needed to be tracked in our configuration management system. To ensure the interfaces were created and configured properly on each director we had to run complex health checks, which added to the lag between provisioning a new worker node and the director being ready to send it traffic. Finally, the header for FOU also lacks a way to signal the "next protocol" of the inner packet, requiring a separate virtual interface for IPv4 and IPv6. Each of these problems individually contributed a small amount of toil, but taken together, gave us impetus to find a better alternative.</p><p>In the time since we had originally implemented this system, IPVS has added native support for encapsulation. This would allow us to eliminate provisioning virtual interfaces (and their corresponding private IPs), and be able to use newly provisioned workers without delay.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4CLprcnKtcCYzpCCnDkjl2/97ba996da2c9ee81c95ec2e97ed08ff9/image1-40.png" />
            
            </figure><p>IPVS doesn't support Foo-over-UDP as an encapsulation type. As part of this project we switched to a similar option, <a href="https://datatracker.ietf.org/doc/html/draft-ietf-intarea-gue-09">Generic UDP Encapsulation</a> (GUE). This encapsulation option does include the "next protocol", allowing one listener on the worker nodes to support both IPv4 and IPv6.</p>
    <div>
      <h3>What went wrong?</h3>
      <a href="#what-went-wrong">
        
      </a>
    </div>
    <p>When we make changes to our Kubernetes clusters, we go through several layers of testing. This change had been deployed to the traffic directors in front of our staging environments, where it had been running for several weeks. However, due to the nature of this bug, the type of traffic to our staging environment did not trigger the underlying bug.</p><p>We began a slow rollout of this change to one production cluster, and after a few hours we began observing issues reaching services behind Kubernetes Load Balancers. The behavior observed was very interesting: the vast majority of requests had no issues, but a small percentage of requests corresponding to large HTTP request payloads or gRPC had significant latency. However, large responses had no corresponding latency increase. There was no corresponding increase in latency seen to any requests to our ingress controllers, though we could observe a small drop in overall requests per second.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/LhkHSdh6ronJadcny3OGI/6a5d6ab9108296646e70932763e423af/image6-9.png" />
            
            </figure><p>Through debugging after the incident, we discovered that the traffic directors were dropping packets that exceeded the Internet maximum transmission unit (MTU) of 1500 bytes, despite the packets being smaller than the actual MTU configured in our internal networks. Once dropped, the original host would fragment and resend packets until they were small enough to pass through the traffic directors. Dropping one packet is inconvenient, but unlikely to be noticed. However, when making a request with large payloads it is very likely that all packets would be dropped and need to be individually fragmented and resent.</p><p>When every packet is dropped and has to be  resent, the network latency can add up to several seconds, exceeding the request timeouts configured by applications. This would cause the request to fail. necessitating retries by applications. As more traffic directors were reconfigured, these retries were less likely to succeed, leading to slower processing and causing the backlog of queued work to increase.</p><p>As you can see this small, but consistent, number of dropped packets could cause a domino effect into much larger problems. Once it became clear there was a problem, we reverted traffic directors to their previous configuration, and this quickly restored traffic to previous levels. From this we knew something about the change caused this problem.</p>
    <div>
      <h3>Finding the culprit</h3>
      <a href="#finding-the-culprit">
        
      </a>
    </div>
    <p>With the symptoms of the issues identified, we started to try to understand the root cause. Once the root cause was understood, we could come up with a satisfactory solution.</p><p>Knowing the packets were larger than the Internet MTU, our first thought was that this was a misconfiguration of the machine in our configuration management tool. However, we found the interface MTUs were all set as expected, and there were no overriding MTUs in the routing table. We also found that sending packets from the director that exceeded the Internet MTU worked fine with no drops.</p><p>Cilium has developed a debugging tool <a href="https://github.com/cilium/pwru">pwru</a>, short for "packet, where are you?", that uses eBPF to aid in debugging the kernel networking state. We used pwru in our staging environment and found the location where the packet had been dropped.</p>
            <pre><code>sudo pwru --output-skb --output-tuple --per-cpu-buffer 2097152 --output-file pwru.log</code></pre>
            <p>This captures tracing data for all packets that reach the traffic director, and saves the trace data to "pwru.log". There are filters built into <code>pwru</code> to select matching packets, but unfortunately they could not be used as the packet was being modified by the encapsulation. Instead, we used grep afterwards to find a matching packet, and then filtered farther based on the pointer in the first column.</p>
            <pre><code>0xffff947712f34400      9        [&lt;empty&gt;]               packet_rcv netns=4026531840 mark=0x0 ifindex=4 proto=8 mtu=1600 len=1512 172.70.2.6:49756-&gt;198.51.100.150:8000(tcp)
0xffff947712f34400      9        [&lt;empty&gt;]                 skb_push netns=4026531840 mark=0x0 ifindex=4 proto=8 mtu=1600 len=1512 172.70.2.6:49756-&gt;198.51.100.150:8000(tcp)
0xffff947712f34400      9        [&lt;empty&gt;]              consume_skb netns=4026531840 mark=0x0 ifindex=4 proto=8 mtu=1600 len=1512 172.70.2.6:49756-&gt;198.51.100.150:8000(tcp)
[ ... snip ... ]
0xffff947712f34400      9        [&lt;empty&gt;]         inet_gso_segment netns=4026531840 mark=0x0 ifindex=7 proto=8 mtu=1600 len=1544 172.70.4.34:33259-&gt;172.70.64.149:5501(udp)
0xffff947712f34400      9        [&lt;empty&gt;]        udp4_ufo_fragment netns=4026531840 mark=0x0 ifindex=7 proto=8 mtu=1600 len=1524 172.70.4.34:33259-&gt;172.70.64.149:5501(udp)
0xffff947712f34400      9        [&lt;empty&gt;]   skb_udp_tunnel_segment netns=4026531840 mark=0x0 ifindex=7 proto=8 mtu=1600 len=1524 172.70.4.34:33259-&gt;172.70.64.149:5501(udp)
0xffff947712f34400      9        [&lt;empty&gt;] kfree_skb_reason(SKB_DROP_REASON_NOT_SPECIFIED) netns=4026531840 mark=0x0 ifindex=7 proto=8 mtu=1600 len=1558 172.70.4.34:33259-&gt;172.70.64.149:5501(udp)</code></pre>
            <p>Looking at the lines logged for this packet, we can observe some behavior. We originally received a TCP packet for the load balancer IP. However, when the packet is dropped, it is a UDP packet destined for the worker's IP on the port we use for GUE. We can surmise that the packet was being processed and encapsulated by IPVS, and form a theory it was being dropped on the egress path from the director node. We could also see that when the packet was dropped, the packet was still smaller than the calculated MTU.</p><p>We can visualize this change by applying this information to our GUE encapsulation diagram shown earlier. The byte totals of the encapsulated packet is 1544, matching the length <code>pwru</code> logged entering <code>inet_gso_segment</code> above.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4oBm7taQSl2U4UH3reulvJ/c0a00891b2e1e7c3228e3c13ab7546ac/image2-38.png" />
            
            </figure><p>The trace above tells us what kernel functions are entered, but does not tell us if or how the program flow left. This means we don't know in which function kfree_skb_reason was called. Fortunately <code>pwru</code> can print a stacktrace when functions are entered.</p>
            <pre><code>0xffff9868b7232e00 	19       	[pwru] kfree_skb_reason(SKB_DROP_REASON_NOT_SPECIFIED) netns=4026531840 mark=0x0 ifindex=7 proto=8 mtu=1600 len=1558 172.70.4.34:63336-&gt;172.70.72.206:5501(udp)
kfree_skb_reason
validate_xmit_skb
__dev_queue_xmit
ip_finish_output2
ip_vs_tunnel_xmit   	[ip_vs]
ip_vs_in_hook   [ip_vs]
nf_hook_slow
ip_local_deliver
ip_sublist_rcv_finish
ip_sublist_rcv
ip_list_rcv
__netif_receive_skb_list_core
netif_receive_skb_list_internal
napi_complete_done
mlx5e_napi_poll [mlx5_core]
__napi_poll
net_rx_action
__softirqentry_text_start
__irq_exit_rcu
common_interrupt
asm_common_interrupt
audit_filter_syscall
__audit_syscall_exit
syscall_exit_work
syscall_exit_to_user_mode
do_syscall_64
entry_SYSCALL_64_after_hwframe</code></pre>
            <p>This <code>stacktrace</code> shows that <code>kfree_skb_reason</code> was called from within the <code>validate_xmit_skb</code> function and this is called from <code>ip_vs_tunnel_xmit</code>. However, when looking at the <a href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/net/core/dev.c?h=v6.1.32&amp;id=76ba310227d2490018c271f1ecabb6c0a3212eb0#n3660">implementation of validate_xmit_skb</a>, we find there are three paths to <code>kfree_skb</code>. How can we determine which path is taken?</p><p>In addition to the eBPF support used by pwru, Linux has support for attaching dynamic tracepoints using <code>perf-probe</code>. After installing the kernel source code and debugging symbols, we can ask the <code>kprobe</code> mechanism what lines of <code>validate_xmit_skb</code> we can create a dynamic tracepoint. It prints the line numbers for the line we can attach a tracepoint onto.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6ddSe06sv8S2w4lAXfZGFi/729a4c2bfb990122bc8287005117c97a/image5-16.png" />
            
            </figure><p>Unfortunately, we can't create a tracepoint on the goto lines, but we can attach tracepoints around them, using the context to determine how control flowed through this function. In addition to specifying a line number, additional arguments can be specified to include local variables. The skb variable is a pointer to a structure that represents this packet, which can be logged to separate packets in case more than one is being processed at a time.</p>
            <pre><code>sudo perf probe --add 'validate_xmit_skb:17 skb'
sudo perf probe --add 'validate_xmit_skb:20 skb'
sudo perf probe --add 'validate_xmit_skb:24 skb'
sudo perf probe --add 'validate_xmit_skb:32 skb'</code></pre>
            <p>Access to these tracepoints could be recorded by using <code>perf-record</code> and providing the tracepoint names given when they were added.</p>
            <pre><code>sudo perf record -a -e 'probe:validate_xmit_skb_L17,probe:validate_xmit_skb_L20,probe:validate_xmit_skb_L24,probe:validate_xmit_skb_L32'</code></pre>
            <p>The tests can be rerun so some packets are logged, before using <code>perf-script</code> to read the generated "perf.data" file. When inspecting the output file, we found that all the packets that were dropped were coming from the control flow of <code>netif_needs_gso</code> succeeding (that is, from the goto on line 18). We continued to create and record tracepoints, following the failing control flow, until execution reached <a href="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/net/ipv4/udp_offload.c?h=v6.1.32&amp;id=76ba310227d2490018c271f1ecabb6c0a3212eb0#n15"><code>__skb_udp_tunnel_segment</code></a>.</p><p>When <code>netif_needs_gso</code> returns false, we do not see packet drops and no problems are reported. It returns true when the packet is larger than the maximum segment size (MSS) advertised by our peer in the connection handshake. For IPv4, the MSS is usually 40 bytes smaller than the MTU (though this can be adjusted by the application or system configuration). For most packets the traffic directors see, the MSS is 40 bytes less than the Internet MTU of 1500, or in this case 1460.</p><p>The tracepoints in this function showed that control flow was leaving through the error case on line 33: that kernel was unable to allocate memory for the tunnel header. GUE was designed to have a minimal tunnel header, so this seemed suspicious. We updated the probe to also record the calculated <code>tnl_hlen</code>, and reran the tests. To our surprise the value recorded by the probes was "-2". Huh, somehow the encapsulated packet got smaller?</p>
            <pre><code>static struct sk_buff *__skb_udp_tunnel_segment(struct sk_buff *skb,
    netdev_features_t features,
    struct sk_buff *(*gso_inner_segment)(struct sk_buff *skb,
   				  	netdev_features_t features),
    __be16 new_protocol, bool is_ipv6)
{
    int tnl_hlen = skb_inner_mac_header(skb) - skb_transport_header(skb); // tunnel header length computed here
    bool remcsum, need_csum, offload_csum, gso_partial;
    struct sk_buff *segs = ERR_PTR(-EINVAL);
    struct udphdr *uh = udp_hdr(skb);
    u16 mac_offset = skb-&gt;mac_header;
    __be16 protocol = skb-&gt;protocol;
    u16 mac_len = skb-&gt;mac_len;
    int udp_offset, outer_hlen;
    __wsum partial;
    bool need_ipsec;

    if (unlikely(!pskb_may_pull(skb, tnl_hlen))) // allocation failed due to negative number.
   	 goto out;</code></pre>
            
    <div>
      <h3>Ultimate fix</h3>
      <a href="#ultimate-fix">
        
      </a>
    </div>
    <p>At this point the kernel's behavior was a bit baffling: why would the tunnel header be computed to be a negative number? To answer this question, we added two more probes. The first was added to <code>ip_vs_in_hook</code>, a hook function that is called as packets enter and leave IPVS code. The second probe was added to <code>__dev_queue_xmit</code>, which is called when preparing to ask the hardware device to transmit the packet. To both of these probes we also logged some of the fields of the sk_buff struct by using the <code>"pointer-&gt;field"</code> syntax. These fields are offsets into the packet's data for the packet's headers, as well as corresponding offsets for the encapsulated headers.</p><ul><li><p>The <code>mac_header</code> and <code>inner_mac_header</code> are offsets to the packet's layer two header. For Ethernet this includes the MAC addresses for the frame, but also other metadata such as the EtherType field giving the type the next protocol.</p></li><li><p>The <code>network_header</code> and <code>inner_network_header</code> fields are offsets to the packet's layer three header. For our purposes, this would be the header for IPv4 or IPv6.</p></li><li><p>The <code>transport_header</code> and <code>inner_transport_header</code> fields are offset to the packet's layer four header, such as TCP, UDP, or ICMP.</p></li></ul>
            <pre><code>sudo perf probe -m ip_vs --add 'ip_vs_in_hook dev=skb-&gt;dev-&gt;name:string skb skb-&gt;inner_mac_header skb-&gt;inner_network_header skb-&gt;inner_transport_header skb-&gt;mac_header skb-&gt;network_header skb-&gt;transport_header skb-&gt;ipvs_property skb-&gt;len:u skb-&gt;data_len:u'
sudo perf probe --add '__dev_queue_xmit dev=skb-&gt;dev-&gt;name:string skb skb-&gt;inner_mac_header skb-&gt;inner_network_header skb-&gt;inner_transport_header skb-&gt;mac_header skb-&gt;network_header skb-&gt;transport_header skb-&gt;len:u skb-&gt;data_len:u'</code></pre>
            <p>When we review the tracepoints using perf-script, we can notice something interesting with these values.</p>
            <pre><code>swapper     0 [030] 79090.376151:    probe:ip_vs_in_hook: (ffffffffc0feebe0) dev="vlan100" skb=0xffff9ca661e90200 inner_mac_header=0x0 inner_network_header=0x0 inner_transport_header=0x0 mac_header=0x44 network_header=0x52 transport_header=0x66 len=1512 data_len=12
swapper     0 [030] 79090.376153:    probe:ip_vs_in_hook: (ffffffffc0feebe0) dev="vlan100" skb=0xffff9ca661e90200 inner_mac_header=0x44 inner_network_header=0x52 inner_transport_header=0x66 mac_header=0x44 network_header=0x32 transport_header=0x46 len=1544 data_len=12
swapper     0 [030] 79090.376155: probe:__dev_queue_xmit: (ffffffff85070e60) dev="vlan100" skb=0xffff9ca661e90200 inner_mac_header=0x44 inner_network_header=0x52 inner_transport_header=0x66 mac_header=0x44 network_header=0x32 transport_header=0x46 len=1558 data_len=12</code></pre>
            <p>When the packet reaches <code>ip_vs_in_hook</code> on the way into IPVS, it only has outer packet headers. This makes sense, as the packet hasn't been encapsulated by IPVS yet. When the same hook is called again as the packet leaves IPVS, we see the encapsulation is already completed. We also find that the outer MAC header and the inner MAC header are at the same offset. Knowing how the tunnel header length is calculated in <code>__skb_udp_tunnel_segment</code>, we can also see where "-2" comes from. The <code>inner_mac_header</code> is at offset 0x44, while the <code>transport_header</code> is at offset 0x46.</p><p>When packets pass through network interfaces, the code for the interface reserves some space for the MAC header. For example, on an Ethernet interface some space is reserved for the Ethernet headers. FOU and GUE do not use link-layer addressing like Ethernet so no space is needed to be reserved. When we were using the virtual interfaces with FOU before it was correctly handling this case by setting the inner MAC header offset to the same as the inner network offset, effectively making it zero bytes long.</p><p>When using the encapsulation inside IPVS, this was not being accounted for, resulting in the inner MAC header offset being invalid. When packets did not need to be segmented this was a harmless bug. When segmenting, however, the tunnel header size needed to be known to duplicate it to all segmented packets.</p><p>I created a patch to correct the MAC header offset in IPVS's encapsulation code. The correction to the inner header offsets needed to be duplicated for IPv4 and IPv6.</p>
            <pre><code>diff --git a/net/netfilter/ipvs/ip_vs_xmit.c b/net/netfilter/ipvs/ip_vs_xmit.c
index c7652da78c88..9193e109e6b3 100644
--- a/net/netfilter/ipvs/ip_vs_xmit.c
+++ b/net/netfilter/ipvs/ip_vs_xmit.c
@@ -1207,6 +1207,7 @@ ip_vs_tunnel_xmit(struct sk_buff *skb, struct ip_vs_conn *cp,
 	skb-&gt;transport_header = skb-&gt;network_header;
 
 	skb_set_inner_ipproto(skb, next_protocol);
+	skb_set_inner_mac_header(skb, skb_inner_network_offset(skb));
 
 	if (tun_type == IP_VS_CONN_F_TUNNEL_TYPE_GUE) {
 		bool check = false;
@@ -1349,6 +1350,7 @@ ip_vs_tunnel_xmit_v6(struct sk_buff *skb, struct ip_vs_conn *cp,
 	skb-&gt;transport_header = skb-&gt;network_header;
 
 	skb_set_inner_ipproto(skb, next_protocol);
+	skb_set_inner_mac_header(skb, skb_inner_network_offset(skb));
 
 	if (tun_type == IP_VS_CONN_F_TUNNEL_TYPE_GUE) {
 		bool check = false;</code></pre>
            <p>When the patch was included in our kernel and deployed the difference in end-to-end request latency was immediately noticeable. We also no longer observed packet drops for requests with large payloads.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4SVrX3EiwZFhiRRkN1BMar/0f450c536cc0e2f3f6535dc23eb0b37e/image7-8.png" />
            
            </figure><p>I've <a href="https://lore.kernel.org/all/20230609205842.2333727-1-terin@cloudflare.com/T/#u">submitted the Linux kernel patch upstream</a>, which is currently queued for inclusion in future releases of the kernel.</p>
    <div>
      <h3>Conclusion</h3>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>Through this post, we hoped to have provided some insight into the process of investigating networking issues and how to begin debugging issues in the kernel using <code>pwru</code> and <code>kprobe</code> tracepoints. This investigation also led to a Linux kernel patch upstream. It also allowed us to safely roll out IPVS's native encapsulation.</p> ]]></content:encoded>
            <category><![CDATA[UDP]]></category>
            <category><![CDATA[Deep Dive]]></category>
            <guid isPermaLink="false">4XFiBPDirEDvUVaOc0IHH8</guid>
            <dc:creator>Terin Stock</dc:creator>
        </item>
        <item>
            <title><![CDATA[Kubectl with Cloudflare Zero Trust]]></title>
            <link>https://blog.cloudflare.com/kubectl-with-zero-trust/</link>
            <pubDate>Fri, 24 Jun 2022 14:08:51 GMT</pubDate>
            <description><![CDATA[ Using Cloudflare Zero Trust with Kubernetes to enable kubectl without SOCKS proxies ]]></description>
            <content:encoded><![CDATA[ 
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/6qFUylvIuAuqfStdkrEDvb/c289f67a78b01fc25c7ec2d910aa2f02/Proxyless-KubeCTL-Support.png" />
            
            </figure><p>Cloudflare is a heavy user of Kubernetes for engineering workloads: it's used to power the backend of our APIs, to handle batch-processing such as analytics aggregation and bot detection, and engineering tools such as our <a href="https://www.cloudflare.com/learning/serverless/glossary/what-is-ci-cd/">CI/CD pipelines</a>. But between load balancers, API servers, etcd, ingresses, and pods, the surface area exposed by Kubernetes can be rather large.</p><p>In this post, we share a little bit about how our engineering team dogfoods Cloudflare Zero Trust to secure Kubernetes — and enables kubectl without proxies.</p>
    <div>
      <h2>Our General Approach to Kubernetes Security</h2>
      <a href="#our-general-approach-to-kubernetes-security">
        
      </a>
    </div>
    <p>As part of our security measures, we heavily limit what can access our clusters over the network. Where a network service is exposed, we add additional protections, such as requiring Cloudflare Access authentication or Mutual TLS (or both) to access ingress resources.</p><p>These network restrictions include access to the cluster's API server. Without access to this, engineers at Cloudflare would not be able to use tools like kubectl to introspect their team's resources. While we believe Continuous Deployments and GitOps are best practices, allowing developers to use the Kubernetes API aids in troubleshooting and increasing developer velocity. Not having access would have been a deal breaker.</p><p>To satisfy our security requirements, we're using Cloudflare Zero Trust, and we wanted to share how we're using it, and the process that brought us here.</p>
    <div>
      <h3>Before Zero Trust</h3>
      <a href="#before-zero-trust">
        
      </a>
    </div>
    <p>In the world before <a href="https://www.cloudflare.com/learning/security/glossary/what-is-zero-trust/">Zero Trust</a>, engineers could access the Kubernetes API by connecting to a VPN appliance. While this is common across the industry, and it does allow access to the API, it also dropped engineers as clients into the internal network: they had much more network access than necessary.</p><p>We weren't happy with this situation, but it was the status quo for several years. At the beginning of 2020, we retired our VPN and thus the Kubernetes team needed to find another solution.</p>
    <div>
      <h3>Kubernetes with Cloudflare Tunnels</h3>
      <a href="#kubernetes-with-cloudflare-tunnels">
        
      </a>
    </div>
    <p>At the time we worked closely with the team developing Cloudflare Tunnels to add support for handling <a href="/releasing-kubectl-support-in-access/">kubectl connections using Access</a> and cloudflared tunnels.</p><p>While this worked for our engineering users, it was a significant hurdle to on-boarding new employees. Each Kubernetes cluster required its own tunnel connection from the engineer's device, which made shuffling between clusters annoying. While kubectl supported connecting through SOCKS proxies, this support was not universal to all tools in the Kubernetes ecosystem.</p><p>We continued using this solution internally while we worked towards a better solution.</p>
    <div>
      <h2>Kubernetes with Zero Trust</h2>
      <a href="#kubernetes-with-zero-trust">
        
      </a>
    </div>
    <p>Since the launch of Cloudflare One, we've been dogfooding the Zero Trust agent in various configurations. At first we'd been using it to implement secure <a href="https://www.cloudflare.com/learning/dns/what-is-dns/">DNS</a> with 1.1.1.1. As time went on, we began to use it to dogfood additional Zero Trust features.</p><p>We're now leveraging the private network routing in Cloudflare Zero Trust to allow engineers to access the Kubernetes APIs without needing to setup cloudflared tunnels or configure kubectl and other Kubernetes ecosystem tools to use tunnels. This isn't something specific to Cloudflare, you can do this for your team today!</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/3BYeJk98ZxUpcycaPOjfOi/be71df17cceed62dd8df98c6f39d5e9d/Kubectl-WIth-Warp-Diagram--2-.png" />
            
            </figure>
    <div>
      <h3>Configuring Zero Trust</h3>
      <a href="#configuring-zero-trust">
        
      </a>
    </div>
    <p>We use a <a href="https://github.com/cloudflare/terraform-provider-cloudflare">configuration management tool</a> for our Zero Trust configuration to enable infrastructure-as-code, which we've adapted below. However, the same configuration can be achieved using the Cloudflare Zero Trust dashboard.</p><p>The first thing we need to do is create a new tunnel. This tunnel will be used to connect the Cloudflare edge network to the Kubernetes API. We run the tunnel endpoints within Kubernetes, using configuration shown later in this post.</p>
            <pre><code>resource "cloudflare_argo_tunnel" "k8s_zero_trust_tunnel" {
  account_id = var.account_id
  name       = "k8s_zero_trust_tunnel"
  secret     = var.tunnel_secret
}</code></pre>
            <p>The "tunnel_secret" secret should be a 32-byte random number, which you should temporarily save as we'll reuse it later for the Kubernetes setup later.</p><p>After we've created the tunnel, we need to create the routes so the Cloudflare network knows what traffic to send through the tunnel.</p>
            <pre><code>resource "cloudflare_tunnel_route" "k8s_zero_trust_tunnel_ipv4" {
  account_id = var.account_id
  tunnel_id  = cloudflare_argo_tunnel.k8s_zero_trust_tunnel.id
  network    = "198.51.100.101/32"
  comment    = "Kubernetes API Server (IPv4)"
}
 
resource "cloudflare_tunnel_route" "k8s_zero_trust_tunnel_ipv6" {
  account_id = var.account_id
  tunnel_id  = cloudflare_argo_tunnel.k8s_zero_trust_tunnel.id
  network    = "2001:DB8::101/128"
  comment    = "Kubernetes API Server (IPv6)"
}</code></pre>
            <p>We support accessing the Kubernetes API via both IPv4 and IPv6 addresses, so we configure routes for both. If you're connecting to your API server via a hostname, these IP addresses should match what is returned via a DNS lookup.</p><p>Next we'll configure settings for Cloudflare Gateway so that it's compatible with the API servers and clients.</p>
            <pre><code>resource "cloudflare_teams_list" "k8s_apiserver_ips" {
  account_id = var.account_id
  name       = "Kubernetes API IPs"
  type       = "IP"
  items      = ["198.51.100.101/32", "2001:DB8::101/128"]
}
 
resource "cloudflare_teams_rule" "k8s_apiserver_zero_trust_http" {
  account_id  = var.account_id
  name        = "Don't inspect Kubernetes API"
  description = "Allow connections from kubectl to API"
  precedence  = 10000
  action      = "off"
  enabled     = true
  filters     = ["http"]
  traffic     = format("any(http.conn.dst_ip[*] in $%s)", replace(cloudflare_teams_list.k8s_apiserver_ips.id, "-", ""))
}</code></pre>
            <p>As we use mutual TLS between clients and the API server, and not all the traffic between kubectl and the API servers are HTTP, we've disabled HTTP inspection for these connections.</p><p>You can pair these rules with additional Zero Trust rules, such as <a href="https://developers.cloudflare.com/cloudflare-one/identity/devices/">device attestation</a>, <a href="https://developers.cloudflare.com/cloudflare-one/policies/filtering/enforce-sessions/">session lifetimes</a>, and <a href="https://developers.cloudflare.com/cloudflare-one/policies/filtering/identity-selectors/">user and group</a> access policies to further customize your security.</p>
    <div>
      <h3>Deploying Tunnels</h3>
      <a href="#deploying-tunnels">
        
      </a>
    </div>
    <p>Once you have your tunnels created and configured, you can deploy their endpoints into your network. We've chosen to deploy the tunnels as pods, as this allows us to use Kubernetes's deployment strategies for rolling out upgrades and handling node failures.</p>
            <pre><code>apiVersion: v1
kind: ConfigMap
metadata:
  name: tunnel-zt
  namespace: example
  labels:
    tunnel: api-zt
data:
  config.yaml: |
    tunnel: 8e343b13-a087-48ea-825f-9783931ff2a5
    credentials-file: /opt/zt/creds/creds.json
    metrics: 0.0.0.0:8081
    warp-routing:
        enabled: true</code></pre>
            <p>This creates a Kubernetes ConfigMap with a basic configuration that enables WARP routing for the tunnel ID specified. You can get this tunnel ID from your configuration management system, the Cloudflare Zero Trust dashboard, or by running the following command from another device logged into the same account.</p><p><code>cloudflared tunnel list</code></p><p>Next, we'll need to create a secret for our tunnel credentials. While you should use a secret management system, for simplicity we'll create one directly here.</p>
            <pre><code>jq -cn --arg accountTag $CF_ACCOUNT_TAG \
       --arg tunnelID $CF_TUNNEL_ID \
       --arg tunnelName $CF_TUNNEL_NAME \
       --arg tunnelSecret $CF_TUNNEL_SECRET \
   '{AccountTag: $accountTag, TunnelID: $tunnelID, TunnelName: $tunnelName, TunnelSecret: $tunnelSecret}' | \
kubectl create secret generic -n example tunnel-creds --from-file=creds.json=/dev/stdin</code></pre>
            <p>This creates a new secret "tunnel-creds" in the "example" namespace with the credentials file the tunnel expects.</p><p>Now we can deploy the tunnels themselves. We deploy multiple replicas to ensure some are always available, even while nodes are being drained.</p>
            <pre><code>apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    tunnel: api-zt
  name: tunnel-api-zt
  namespace: example
spec:
  replicas: 3
  selector:
    matchLabels:
      tunnel: api-zt
  strategy:
    rollingUpdate:
      maxSurge: 0
      maxUnavailable: 1
  template:
    metadata:
      labels:
        tunnel: api-zt
    spec:
      containers:
        - args:
            - tunnel
            - --config
            - /opt/zt/config/config.yaml
            - run
          env:
            - name: GOMAXPROCS
              value: "2"
            - name: TZ
              value: UTC
          image: cloudflare/cloudflared:2022.5.3
          livenessProbe:
            failureThreshold: 1
            httpGet:
              path: /ready
              port: 8081
            initialDelaySeconds: 10
            periodSeconds: 10
          name: tunnel
          ports:
            - containerPort: 8081
              name: http-metrics
          resources:
            limits:
              cpu: "1"
              memory: 100Mi
          volumeMounts:
            - mountPath: /opt/zt/config
              name: config
              readOnly: true
            - mountPath: /opt/zt/creds
              name: creds
              readOnly: true
      volumes:
        - secret:
            name: tunnel-creds
          name: creds
        - configMap:
            name: tunnel-api-zt
          name: config</code></pre>
            
    <div>
      <h2>Using Kubectl with Cloudflare Zero Trust</h2>
      <a href="#using-kubectl-with-cloudflare-zero-trust">
        
      </a>
    </div>
    
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2BGV6YJLH0Hnt1vh19F2lC/07c8806cd7c65674ddac42f8d2923b93/Screen-Shot-2022-06-23-at-3.43.37-PM.png" />
            
            </figure><p>After deploying the Cloudflare Zero Trust agent, members of your team can now access the Kubernetes API without needing to set up any special SOCKS tunnels!</p>
            <pre><code>kubectl version --short
Client Version: v1.24.1
Server Version: v1.24.1</code></pre>
            
    <div>
      <h2>What's next?</h2>
      <a href="#whats-next">
        
      </a>
    </div>
    <p>If you try this out, send us your feedback! We're continuing to improve Zero Trust for non-HTTP workflows.</p> ]]></content:encoded>
            <category><![CDATA[Cloudflare One Week]]></category>
            <category><![CDATA[Security]]></category>
            <category><![CDATA[Zero Trust]]></category>
            <category><![CDATA[Kubernetes]]></category>
            <guid isPermaLink="false">4VWDm5LM1jLa6cjopWrCRu</guid>
            <dc:creator>Terin Stock</dc:creator>
        </item>
        <item>
            <title><![CDATA[Automated Origin CA for Kubernetes]]></title>
            <link>https://blog.cloudflare.com/automated-origin-ca-for-kubernetes/</link>
            <pubDate>Fri, 13 Nov 2020 12:00:00 GMT</pubDate>
            <description><![CDATA[ Today we're releasing origin-ca-issuer, an extension to cert-manager integrating with Cloudflare Origin CA to easily create and renew certificates for your account's domains. ]]></description>
            <content:encoded><![CDATA[ <p></p><p>In 2016, we launched the <a href="/cloudflare-ca-encryption-origin/">Cloudflare Origin CA</a>, a certificate authority optimized for making it easy to secure the connection between Cloudflare and an origin server. Running our own CA has allowed us to support fast issuance and renewal, simple and effective revocation, and wildcard certificates for our users.</p><p>Out of the box, managing <a href="https://www.cloudflare.com/application-services/products/ssl/">TLS certificates</a> and keys within Kubernetes can be challenging and error prone. The secret resources have to be constructed correctly, as components expect secrets with specific fields. Some forms of domain verification require manually rotating secrets to pass. Once you're successful, don't forget to renew before the certificate expires!</p><p><a href="https://cert-manager.io/">cert-manager</a> is a project to fill this operational gap, providing Kubernetes resources that <a href="https://www.cloudflare.com/application-services/solutions/certificate-lifecycle-management/">manage the lifecycle of a certificate.</a> Today we're releasing <a href="https://github.com/cloudflare/origin-ca-issuer">origin-ca-issuer</a>, an extension to cert-manager integrating with Cloudflare Origin CA to easily create and renew certificates for your account's domains.</p>
    <div>
      <h2>Origin CA Integration</h2>
      <a href="#origin-ca-integration">
        
      </a>
    </div>
    
    <div>
      <h3>Creating an Issuer</h3>
      <a href="#creating-an-issuer">
        
      </a>
    </div>
    <p>After installing cert-manager and origin-ca-issuer, you can create an OriginIssuer resource. This resource creates a binding between cert-manager and the Cloudflare API for an account. Different issuers may be connected to different Cloudflare accounts in the same Kubernetes cluster.</p>
            <pre><code>apiVersion: cert-manager.k8s.cloudflare.com/v1
kind: OriginIssuer
metadata:
  name: prod-issuer
  namespace: default
spec:
  signatureType: OriginECC
  auth:
    serviceKeyRef:
      name: service-key
      key: key
      ```</code></pre>
            <p>This creates a new OriginIssuer named "prod-issuer" that issues certificates using ECDSA signatures, and the secret "service-key" in the same namespace is used to authenticate to the Cloudflare API.</p>
    <div>
      <h3>Signing an Origin CA Certificate</h3>
      <a href="#signing-an-origin-ca-certificate">
        
      </a>
    </div>
    <p>After creating an OriginIssuer, we can now create a Certificate with cert-manager. This defines the domains, including wildcards, that the certificate should be issued for, how long the certificate should be valid, and when cert-manager should renew the certificate.</p>
            <pre><code>apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com
  namespace: default
spec:
  # The secret name where cert-manager
  # should store the signed certificate.
  secretName: example-com-tls
  dnsNames:
    - example.com
  # Duration of the certificate.
  duration: 168h
  # Renew a day before the certificate expiration.
  renewBefore: 24h
  # Reference the Origin CA Issuer you created above,
  # which must be in the same namespace.
  issuerRef:
    group: cert-manager.k8s.cloudflare.com
    kind: OriginIssuer
    name: prod-issuer
</code></pre>
            <p>Once created, cert-manager begins managing the lifecycle of this certificate, including creating the key material, crafting a certificate signature request (CSR), and constructing a certificate request that will be processed by the origin-ca-issuer.</p><p>When signed by the Cloudflare API, the certificate will be made available, along with the private key, in the Kubernetes secret specified within the secretName field. You'll be able to use this certificate on servers proxied behind Cloudflare.</p>
    <div>
      <h3>Extra: Ingress Support</h3>
      <a href="#extra-ingress-support">
        
      </a>
    </div>
    <p>If you're using an Ingress controller, you can use cert-manager's <a href="https://cert-manager.io/docs/usage/ingress/">Ingress support</a> to automatically manage Certificate resources based on your Ingress resource.</p>
            <pre><code>apiVersion: networking/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/issuer: prod-issuer
    cert-manager.io/issuer-kind: OriginIssuer
    cert-manager.io/issuer-group: cert-manager.k8s.cloudflare.com
  name: example
  namespace: default
spec:
  rules:
    - host: example.com
      http:
        paths:
          - backend:
              serviceName: examplesvc
              servicePort: 80
            path: /
  tls:
    # specifying a host in the TLS section will tell cert-manager 
    # what DNS SANs should be on the created certificate.
    - hosts:
        - example.com
      # cert-manager will create this secret
      secretName: example-tls
</code></pre>
            
    <div>
      <h2>Building an External cert-manager Issuer</h2>
      <a href="#building-an-external-cert-manager-issuer">
        
      </a>
    </div>
    <p>An external cert-manager issuer is a specialized Kubernetes controller. There's no direct communication between cert-manager and external issuers at all; this means that you can use any existing tools and best practices for developing controllers to develop an external issuer.</p><p>We've decided to use the excellent <a href="https://github.com/kubernetes-sigs/controller-runtime">controller-runtime</a> project to build origin-ca-issuer, running two reconciliation controllers.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/MASdtZ7CMaW8JO3SCgb5t/4956cb2ac00a920a9901605adf1e9f68/image1-4.png" />
            
            </figure>
    <div>
      <h3>OriginIssuer Controller</h3>
      <a href="#originissuer-controller">
        
      </a>
    </div>
    <p>The OriginIssuer controller watches for creation and modification of OriginIssuer custom resources. The controllers create a Cloudflare API client using the details and credentials referenced. This client API instance will later be used to sign certificates through the API. The controller will periodically retry to create an API client; once it is successful, it updates the OriginIssuer's status to be ready.</p>
    <div>
      <h3>CertificateRequest Controller</h3>
      <a href="#certificaterequest-controller">
        
      </a>
    </div>
    <p>The CertificateRequest controller watches for the creation and modification of cert-manager's CertificateRequest resources. These resources are created automatically by cert-manager as needed during a certificate's lifecycle.</p><p>The controller looks for Certificate Requests that reference a known OriginIssuer, this reference is copied by cert-manager from the origin Certificate resource, and ignores all resources that do not match. The controller then verifies the OriginIssuer is in the ready state, before transforming the certificate request into an API request using the previously created clients.</p><p>On a successful response, the signed certificate is added to the certificate request, and which cert-manager will use to create or update the secret resource. On an unsuccessful request, the controller will periodically retry.</p>
    <div>
      <h2>Learn More</h2>
      <a href="#learn-more">
        
      </a>
    </div>
    <p>Up-to-date documentation and complete installation instructions can be found in our <a href="https://github.com/cloudflare/origin-ca-issuer">GitHub repository</a>. Feedback and contributions are greatly appreciated. If you're interested in Kubernetes at Cloudflare, including building controllers like these, <a href="https://www.cloudflare.com/careers/jobs/">we're hiring</a>.</p> ]]></content:encoded>
            <category><![CDATA[Kubernetes]]></category>
            <category><![CDATA[SSL]]></category>
            <category><![CDATA[TLS]]></category>
            <category><![CDATA[Security]]></category>
            <category><![CDATA[API]]></category>
            <guid isPermaLink="false">7akG4xBepli4ZP133CXBCf</guid>
            <dc:creator>Terin Stock</dc:creator>
        </item>
        <item>
            <title><![CDATA[High Availability Load Balancers with Maglev]]></title>
            <link>https://blog.cloudflare.com/high-availability-load-balancers-with-maglev/</link>
            <pubDate>Wed, 10 Jun 2020 11:00:00 GMT</pubDate>
            <description><![CDATA[ We own and operate physical infrastructure for our backend services. We need an effective way to route arbitrary TCP and UDP traffic between services and also from outside these data centers. ]]></description>
            <content:encoded><![CDATA[ <p></p>
    <div>
      <h2>Background</h2>
      <a href="#background">
        
      </a>
    </div>
    <p>We run many backend services that power our customer dashboard, APIs, and features available at our edge. We own and operate physical infrastructure for our backend services. We need an effective way to route arbitrary TCP and UDP traffic between services and also from outside these data centers.</p><p>Previously, all traffic for these backend services would pass through several layers of stateful TCP proxies and NATs before reaching an available instance. This solution worked for several years, but as we grew it caused our service and operations teams many issues. Our service teams needed to deal with drops of availability, and our operations teams had much toil when needing to do maintenance on load balancer servers.</p>
    <div>
      <h2>Goals</h2>
      <a href="#goals">
        
      </a>
    </div>
    <p>With the experience with our stateful TCP proxy and NAT solutions in mind, we had several goals for a replacement load balancing service, while remaining on our own infrastructure:</p><ol><li><p>Preserve source IPs through routing decisions to destination servers. This allows us to support servers that require client IP addresses as part of their operation, without workarounds such as X-Forwarded-For headers or the PROXY TCP extension.</p></li><li><p>Support an architecture where backends are located across many racks and subnets. This prevents solutions that cannot be routed by existing network equipment.</p></li><li><p>Allow operation teams to perform maintenance with zero downtime. We should be able to remove load balancers at any time without causing any connection resets or downtime for services.</p></li><li><p>Use Linux tools and features that are commonplace and well-tested. There are a lot of very cool networking features in Linux we could experiment with, but we wanted to optimize for least surprising behavior for operators who do not primarily work with these load balancers.</p></li><li><p>No explicit connection synchronization between load balancers. We found that communication between load balancers significantly increased the system complexity, allowing for more opportunities for things to go wrong.</p></li><li><p>Allow for staged rollout from the previous load balancer implementation. We should be able to migrate the traffic of specific services between the two implementations to find issues and gain confidence in the system.</p></li></ol>
    <div>
      <h2>Reaching Zero Downtime</h2>
      <a href="#reaching-zero-downtime">
        
      </a>
    </div>
    
    <div>
      <h3>Problems</h3>
      <a href="#problems">
        
      </a>
    </div>
    
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2CMbFBlBDHc4rzZ458SE03/d1a7f034913bb4d188048d4f14620867/image6-1.png" />
            
            </figure><p>Previously, when traffic arrived at our backend data centers, our routers would pick and forward packets to one of the L4 load balancers servers it knew about.  These L4 load balancers would determine what service the traffic was for, then forward the traffic to one of the service's L7 servers.</p><p>This architecture worked fine during normal operations. However, issues would quickly surface whenever the set of load balancers changed. Our routers would forward traffic to the new set and it was very likely traffic would arrive to a different load balancer than before. As each load balancer maintained its own connection state, it would be unable to forward  traffic for these new in-progress connections. These connections would then be reset, potentially causing errors for our customers.</p>
    <div>
      <h3>Consistent Hashing</h3>
      <a href="#consistent-hashing">
        
      </a>
    </div>
    
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2sQ0ZsemGl1ZjnxB8SaBZ2/dee6b3b33c0d088b8f2e5378a714ae5a/image5-1.png" />
            
            </figure><p>During normal operations, our new architecture has similar behavior to the previous design. A L4 load balancer would be selected by our routers, which would then forward traffic to a service's L7 server.</p><p>There's a significant change when the set of load balancers changes. As our load balancers are now stateless, it doesn't matter which load balancer our router selects to forward traffic to, they'll end up reaching the same backend server.</p>
    <div>
      <h2>Implementation</h2>
      <a href="#implementation">
        
      </a>
    </div>
    
    <div>
      <h3>BGP</h3>
      <a href="#bgp">
        
      </a>
    </div>
    <p>Our load balancer servers announce service IP addresses to our data centers’ routers using BGP, unchanged from the previous solution. Our routers choose which load balancers will receive packets based on a routing strategy called equal-cost multi-path routing (ECMP).</p><p>ECMP hashes information from packets to pick a path for that packet. The hash function used by routers is often fixed in firmware. Routers that chose a poor hashing function, or chose bad inputs, can create unbalanced network and server load, or break assumptions made by the protocol layer.</p><p>We worked with our networking team to ensure ECMP is configured on our routers to hash only based on the packet's 5-tuple—the protocol, source address and port, and destination address and port.</p><p>For maintenance, our operators can withdraw the BGP session and traffic will transparently shift to other load balancers. However, if a load balancer suddenly becomes unavailable, such as with a kernel panic or power failure, there is a short delay before the BGP keepalive mechanism fails and routers terminate the session.</p><p>It's possible for routers to terminate BGP sessions after a much shorter delay using the Bidirectional Forwarding Detection (BFD) protocol between the router and load balancers. Different routers have different limitations and restrictions on BFD that makes it difficult to use in an environment heavily using L2 link aggregation and VXLANs.</p><p>We're continuing to work with our networking team to find solutions to reduce the time to terminate BGP sessions, using tools and configurations they're most comfortable with.</p>
    <div>
      <h3>Selecting Backends with Maglev</h3>
      <a href="#selecting-backends-with-maglev">
        
      </a>
    </div>
    <p>To ensure all load balancers are sending traffic to the same backends, we decided to use the <a href="https://research.google/pubs/pub44824/">Maglev connection scheduler</a>. Maglev is a consistent hash scheduler hashing a 5-tuple of information from each packet—the protocol, source address and port, and destination address and port—to determine a backend server.</p><p>By being a consistent hash, the same backend server is chosen by every load balancer for a packet without needing to persist any connection state. This allows us to transparently move traffic between load balancers without requiring explicit connection synchronization between them.</p>
    <div>
      <h3>IPVS and Foo-Over-UDP</h3>
      <a href="#ipvs-and-foo-over-udp">
        
      </a>
    </div>
    <p>Where possible, we wanted to use commonplace and reliable Linux features. Linux has implemented a powerful layer 4 load balancer, the IP Virtual Server (IPVS), since the early 2000s. IPVS has supported the Maglev scheduler <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=039f32e8cdea29b4d0680df7a83817b5ec4166e1">since Linux 4.18</a>.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2FK6ZVvqjjOVdFwdvTZM3L/5617f15dd5cf447bff3a47e7dff5702f/image2-3.png" />
            
            </figure><p>Our load balancer and application servers are spread across multiple racks and subnets. To route traffic from the load balancer we opted to use Foo-Over-UDP encapsulation.</p><p>In Foo-Over-UDP encapsulation a new IP and UDP header are added around the original packet. When these packets arrive on the destination server, the Linux kernel removes the outer IP and UDP headers and inserts the inner payload back into the networking stack for processing as if the packet had originally been received on that server.</p><p>Compared to other encapsulation methods—such as IPIP, GUE, and GENEVE—we felt Foo-Over-UDP struck a nice balance between features and flexibility. Direct Server Return, where application servers reply directly to clients and bypass the load balancers, was implemented as a byproduct of the encapsulation. There was no state associated with the encapsulation, each server only required one encapsulation interface to receive traffic from all load balancers.</p><p><b>Example Load Balancer Configuration</b></p>
            <pre><code># Load in the kernel modules required for IPVS and FOU.
$ modprobe ip_vs &amp;&amp; modprobe ip_vs_mh &amp;&amp; modprobe fou

# Create one tunnel between the load balancer and
# an application server. The IPs are the machines'
# real IPs on the network.
$ ip link add name lbtun1 type ipip \
remote 192.0.2.1 local 192.0.2.2 ttl 2 \
encap fou encap-sport auto encap-dport 5555

# Inform the kernel about the VIPs that might be announced here.
$ ip route add table local local 198.51.100.0/24 \
dev lo proto kernel

# Give the tunnel an IP address local to this machine.
# Traffic on this machine destined for this IP address will
# be sent down the tunnel.
$ ip route add 203.0.113.1 dev lbtun1 scope link

# Tell IPVS about the service, and that it should use the
# Maglev scheduler.
$ ipvsadm -A -t 198.51.100.1:80 -s mh

# Tell IPVS about a backend for this service.
$ ipvsadm -a -t 198.51.100.1:80 -r 203.0.113.1:80</code></pre>
            <p><b>Example Application Server Configuration</b></p>
            <pre><code># The kernel module may need to be loaded.
$ modprobe fou

# Setup an IPIP receiver.
# ipproto 4 = IPIP (not IPv4)
$ ip fou add port 5555 ipproto 4

# Bring up the tunnel.
$ ip link set dev tunl0 up

# Disable reverse path filtering on tunnel interface.
$ sysctl -w net.ipv4.conf.tunl0.rp_filter=0
$ sysctl -w net.ipv4.conf.all.rp_filter=0</code></pre>
            <p>IPVS does not support Foo-Over-UDP as a packet forwarding method. To work around this limitation, we've created virtual interfaces that implement Foo-Over-UDP encapsulation. We can then use IPVS's direct packet forwarding method along with the kernel routing table to choose a specific interface.</p><p>Linux is often configured to ignore packets that arrive on an interface that is different from the interface used for replies. As packets will now be arriving on the virtual "tunl0" interface, we need to disable reverse path filtering on this interface. The kernel uses the higher value of the named and "all" interfaces, so you may need to decrease "all" and adjust other interfaces.</p>
    <div>
      <h3>MTUs and Encapsulation</h3>
      <a href="#mtus-and-encapsulation">
        
      </a>
    </div>
    <p>The maximum IPv4 packet size, or maximum transmission unit (MTU), we accept from the internet is 1500 bytes. To ensure we did not fragment these packets during encapsulation we increased our internal MTUs from the default to accommodate the IP and UDP headers.</p><p>The team had underestimated the complexity of changing the MTU across all our racks of equipment. We had to adjust the MTU across all our routers and switches, of our bonded and VXLAN interfaces, and finally our Foo-Over-UDP encapsulation. Even with a carefully orchestrated rollout, and we still uncovered MTU-related bugs with our switches and server stack, many of which manifested first as issues on other parts of the network.</p>
    <div>
      <h3>Node Agent</h3>
      <a href="#node-agent">
        
      </a>
    </div>
    <p>We've written a Go agent running on each load balancer that synchronizes with a control plane layer that's tracking the location of services. The agent then configures the system based on active services and available backend servers.</p><p>To configure IPVS and the routing table we're using packages built upon the <a href="https://github.com/mdlayher/netlink">netlink</a> Go package. We're <a href="https://github.com/cloudflare/ipvs">open sourcing</a> the IPVS netlink package we built today, which supports querying, creating and updating IPVS virtual servers, destinations, and statistics.</p><p>Unfortunately, there is no official programming interface for iptables, so we must instead execute the iptables binary. The agent computes an ideal set of iptables chains and rules, which is then reconciled with the live rules.</p><p><b>Subset of iptables for a service</b></p>
            <pre><code>*filter
-A INPUT -d 198.51.100.0/24 -m comment --comment \
"leif:nhAi5v93jwQYcJuK" -j LEIFUR-LB
-A LEIFUR-LB -d 198.51.100.1/32 -p tcp -m comment --comment \
"leif:G4qtNUVFCkLCu4yt" -m multiport --dports 80 -j LEIFUR-GQ4OKHRLCJYOWIN9
-A LEIFUR-GQ4OKHRLCJYOWIN9 -s 10.0.0.0/8 -m comment --comment \
"leif:G4qtNUVFCkLCu4yt" -j ACCEPT
-A LEIFUR-GQ4OKHRLCJYOWIN9 -s 172.16.0.0/12 -m comment --comment \
"leif:0XxZ2OwlQWzIYFTD" -j ACCEPT</code></pre>
            <p>The iptables output of a rule may differ significantly from the input given by our ideal rule. To avoid needing to parse the entire iptables rule in our comparisons, we store a hash of the rule, including the position in the chain, as an iptables comment. We then can compare the comment to our ideal rule to determine if we need to take any actions. On chains that are shared (such as INPUT) the agent ignores unmanaged rules.</p>
    <div>
      <h2>Kubernetes Integration</h2>
      <a href="#kubernetes-integration">
        
      </a>
    </div>
    <p>We use the network load balancer described here as a <a href="https://www.cloudflare.com/learning/performance/cloud-load-balancing-lbaas/">cloud load balancer</a> for Kubernetes. A controller assigns virtual IP addresses to Kubernetes services requesting a load balancer IP. These IPs get configured by the agent in IPVS. Traffic is directed to a subset of cluster nodes for handling by kube-proxy, unless the External Traffic Policy is set to "Local" in which case the traffic is sent to the specific backends the workloads are running on.</p><p>This allows us to have internal Kubernetes clusters that better replicate the load balancer behavior of managed clusters on cloud providers. Services running Kubernetes, such as ingress controllers, <a href="https://www.cloudflare.com/learning/security/api/what-is-an-api-gateway/">API gateways</a>, and databases, have access to correct client IP addresses of load balanced traffic.</p>
    <div>
      <h2>Future Work</h2>
      <a href="#future-work">
        
      </a>
    </div>
    <ul><li><p>Continuing a close eye on future developments of IPVS and alternatives, including nftlb and Express Data Path (XDP) and eBPF.</p></li><li><p>Migrate to nftables. The "flat priorities" and lack of programmable interface for iptables makes it ill-suited for including automated rules alongside rules added by operators. We hope as more projects and operations move to nftables we'll be able to switch without creating a "blind-spot" to operations.</p></li><li><p>Failures of a load balancer can result in temporary outages due to BGP hold timers. We'd like to improve how we're handling the failures with BGP sessions.</p></li><li><p>Investigate using Lightweight Tunnels to reduce the number of Foo-Over-UDP interfaces are needed on the load balancer nodes.</p></li></ul>
    <div>
      <h2>Additional Reading</h2>
      <a href="#additional-reading">
        
      </a>
    </div>
    <ul><li><p><a href="https://vincent.bernat.im/en/blog/2018-multi-tier-loadbalancer"><i>Multi-tier load-balancing with Linux</i></a>. Vincent Bernat (2018). Describes a network load balancer using IPVS and IPIP.</p></li><li><p><a href="https://githubengineering.com/introducing-glb/"><i>Introducing the GitHub Load Balancer</i></a>. Joe Williams, Theo Julienne (2017). Describes a similar split layer 4 and layer 7 architecture, using consistent hashing and Foo-Over-UDP. They seemed to have limitations with IPVS that looked to have been resolved.</p></li><li><p><a href="https://lwn.net/Articles/614348/"><i>Foo over UDP</i></a>. Jonathan Corbet (2014). Describes the basics of IPIP and Foo-Over-UDP, which was just introduced at the time.</p></li><li><p><a href="https://www.netdevconf.org/0.1/sessions/11.html"><i>UDP encapsulation, FOU, GUE, &amp; RCO</i></a>. Tom Herbert (2015). Describes the different UDP encapsulation options.</p></li><li><p><a href="https://archive.nanog.org/sites/default/files/1_Saino_Hashing_On_Broken_Assumptions.pdf"><i>Hashing on broken assumptions</i></a>. Lorenzo Saino (2017). Farther discussion on hashing difficulties with ECMP.</p></li><li><p><a href="https://archive.nanog.org/meetings/nanog45/presentations/Monday/Scholl_BFD_N45.pdf"><i>BFD (Bidirectional Forwarding Detection): Does it work and is it worth it?</i></a>. Tom School (2009). Discussion on BFD with common protocols and where it can become a problem.</p></li></ul> ]]></content:encoded>
            <category><![CDATA[Load Balancing]]></category>
            <category><![CDATA[Product News]]></category>
            <category><![CDATA[Speed & Reliability]]></category>
            <guid isPermaLink="false">6l38CAxcJrVhPKDMQvLXWx</guid>
            <dc:creator>Terin Stock</dc:creator>
        </item>
        <item>
            <title><![CDATA[Accelerating Node.js applications with HTTP/2 Server Push]]></title>
            <link>https://blog.cloudflare.com/accelerating-node-js-applications-with-http-2-server-push/</link>
            <pubDate>Tue, 16 Aug 2016 12:17:19 GMT</pubDate>
            <description><![CDATA[ In April, we announced support for HTTP/2 Server Push via the HTTP Link header. My coworker John has demonstrated how easy it is to add Server Push to an example PHP application.  We wanted to make it easy to improve the performance of contemporary websites built with Node.js.  ]]></description>
            <content:encoded><![CDATA[ <p>In April, we announced support for <a href="https://www.cloudflare.com/http2/server-push/">HTTP/2 Server Push</a> via the HTTP <code>Link</code> header. My coworker John has demonstrated how easy it is to <a href="/using-http-2-server-push-with-php/">add Server Push to an example PHP</a> application.</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2cTvkkBKYrTnHNyFrfyrYI/05d92efb1903cd4700d8a9020e430b40/489477622_594bf9e3d9_z.jpg" />
            
            </figure><p><a href="https://creativecommons.org/licenses/by/2.0/">CC BY 2.0</a> <a href="https://www.flickr.com/photos/nickyfern/489477622/in/photolist-KfGDm-4WuA7B-4WySRS-a8ijnt-4WuByk-4WuB9M-bE3e6V-665C6K-eeQRx1-b97viM-qzYJ8z-9n2aTS-8EsaQK-aqxV42-jWDkD2-jKyWKv-jzs7yy-9TKcYn-4iAQTa-ECjYJ-96MvXy-bP43v2-rEmKWR-5p3k5r-pVZhDo-os1Njy-5CEEnU-8RhgUX-7JV4jr-9bC7me-sSKqA-72Mppz-maZfyL-6KfUge-dDvw4S-ngyFgY-aPxqsP-7b24QV-5opdXV-sEA7Xj-mZohS-8GFkMr-twfh93-7ZXqtJ-dvRbXj-a8mRiU-4NCzeD-qFXKpj-4n7K7j-34D66Q">image</a> by <a href="https://www.flickr.com/photos/nickyfern/">Nicky Fernandes</a></p><p>We wanted to make it easy to improve the performance of contemporary websites built with <a href="https://nodejs.org/">Node.js</a>. we developed the <a href="https://github.com/cloudflare/netjet">netjet</a> middleware to parse the generated HTML and automatically add the <code>Link</code> headers. When used with an example Express application you can see the headers being added:</p>
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/4yzfZDbCTajZyJDNnbqrUm/ab28553b5d5e878e10b93f115a0f602e/2016-08-11_13-32-45.png" />
            
            </figure><p>We use <a href="https://ghost.org/">Ghost</a> to power this blog, so if your browser supports HTTP/2 you have already benefited from Server Push without realizing it! More on that below.</p><p>In netjet, we use the <a href="https://github.com/posthtml/posthtml">PostHTML</a> project to parse the HTML with a custom plugin. Right now it is looking for images, scripts and external stylesheets. You can implement this same technique in other environments too.</p><p>Putting an HTML parser in the response stack has a downside: it will increase the page load latency (or "time to first byte"). In most cases, the added latency will be overshadowed by other parts of your application, such as database access. However, netjet includes an adjustable LRU cache keyed by <code>ETag</code> headers, allowing netjet to insert <code>Link</code> headers quickly on pages already parsed.</p><p>If you are designing a brand new application, however, you should consider storing metadata on embedded resources alongside your content, eliminating the HTML parse, and possible latency increase, entirely.</p><p>Netjet is compatible with any Node.js HTML framework that supports Express-like middleware. Getting started is as simple as adding netjet to the beginning of your middleware chain.</p>
            <pre><code>var express = require('express');
var netjet = require('netjet');
var root = '/path/to/static/folder';

express()
  .use(netjet({
    cache: {
      max: 100
    }
  }))
  .use(express.static(root))
  .listen(1337);</code></pre>
            <p>With a little more work, you can even use netjet without frameworks.</p>
            <pre><code>var http = require('http');
var netjet = require('netjet');

var port = 1337;
var hostname = 'localhost';
var preload = netjet({
  cache: {
    max: 100
  }
});

var server = http.createServer(function (req, res) {
  preload(req, res, function () {
      res.statusCode = 200;
      res.setHeader('Content-Type', 'text/html');
      res.end('&lt;!doctype html&gt;&lt;h1&gt;Hello World&lt;/h1&gt;');
  });
});

server.listen(port, hostname, function () {
  console.log('Server running at http://' + hostname + ':' + port+ '/');
});</code></pre>
            <p>See the <a href="https://www.npmjs.com/package/netjet">netjet documentation</a> for more information on the supported options.</p>
    <div>
      <h2>Seeing what’s pushed</h2>
      <a href="#seeing-whats-pushed">
        
      </a>
    </div>
    
            <figure>
            
            <img src="https://cf-assets.www.cloudflare.com/zkvhlag99gkb/2qCaQEt8gl74hBdoZVEzBS/d29a6028893b87b888cad5ba438b3a78/2016-08-02_10-49-33.png" />
            
            </figure><p>Chrome's Developer Tools makes it easy to verify that your site is using Server Push. The Network tab shows pushed assets with "Push" included as part of the initiator.</p><p>Unfortunately, Firefox's Developers Tools don't yet directly expose if the resource pushed. You can, however, check for the <code>cf-h2-pushed</code> header in the page's response headers, which contains a list of resources that CloudFlare offered browsers over Server Push.</p><p>Contributions to improve netjet or the documentation are greatly appreciated. I'm excited to hear where people are using netjet.</p>
    <div>
      <h2>Ghost and Server Push</h2>
      <a href="#ghost-and-server-push">
        
      </a>
    </div>
    <p>Ghost is one such exciting integration. With the aid of the Ghost team, I've integrated netjet, and it has been available as an opt-in beta since version 0.8.0.</p><p>If you are running a Ghost instance, you can enable Server Push by modifying the server's <code>config.js</code> file and add the <code>preloadHeaders</code> option to the <code>production</code> configuration block.</p>
            <pre><code>production: { 
  url: 'https://my-ghost-blog.com', 
  preloadHeaders: 100, 
  // ... 
}</code></pre>
            <p>Ghost has put together <a href="http://support.ghost.org/preload-headers/">a support article</a> for Ghost(Pro) customers.</p>
    <div>
      <h2>Conclusion</h2>
      <a href="#conclusion">
        
      </a>
    </div>
    <p>With netjet, your Node.js applications can start to use browser preloading and, when used with CloudFlare, HTTP/2 Server Push today.</p><p>At CloudFlare, we're excited to make tools to help increase the performance of websites. If you find this interesting, we are <a href="https://www.cloudflare.com/join-our-team/">hiring</a> in Austin, Texas; Champaign, Illinois; London; San Francisco; and Singapore.</p> ]]></content:encoded>
            <category><![CDATA[HTTP2]]></category>
            <category><![CDATA[php]]></category>
            <category><![CDATA[Server Push]]></category>
            <category><![CDATA[Speed & Reliability]]></category>
            <category><![CDATA[JavaScript]]></category>
            <guid isPermaLink="false">257O1UR4TetOqSjjwVfDCV</guid>
            <dc:creator>Terin Stock</dc:creator>
        </item>
    </channel>
</rss>