summaryrefslogtreecommitdiff
path: root/gemfeed/DRAFT-ipv6test-deployment.html
blob: 32d7d4d758098717353b062671c1eca30a5f7044 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Deploying an IPv6 Test Service on Kubernetes</title>
<link rel="shortcut icon" type="image/gif" href="/favicon.ico" />
<link rel="stylesheet" href="../style.css" />
<link rel="stylesheet" href="style-override.css" />
</head>
<body>
<div class="rfx-overlay-grid"></div>
<div class="rfx-overlay-scanlines"></div>
<div id="rfx-stars"></div>
<div class="rfx-vignette"></div>
<p class="header">
<a href="https://foo.zone">Home</a> | <a href="https://codeberg.org/snonux/foo.zone/src/branch/content-md/gemfeed/DRAFT-ipv6test-deployment.md">Markdown</a> | <a href="gemini://foo.zone/gemfeed/DRAFT-ipv6test-deployment.gmi">Gemini</a>
</p>
<h1 style='display: inline' id='deploying-an-ipv6-test-service-on-kubernetes'>Deploying an IPv6 Test Service on Kubernetes</h1><br />
<br />
<h2 style='display: inline' id='introduction'>Introduction</h2><br />
<br />
<span>This post covers deploying a simple IPv6/IPv4 connectivity test application to the f3s Kubernetes cluster. The application displays visitors&#39; IP addresses and determines whether they&#39;re connecting via IPv6 or IPv4—useful for testing dual-stack connectivity.</span><br />
<br />
<span>The interesting technical challenge was preserving the original client IP address through multiple reverse proxies: from the OpenBSD relayd frontends, through Traefik ingress, to the Apache CGI backend.</span><br />
<br />
<a class='textlink' href='./2024-11-17-f3s-kubernetes-with-freebsd-part-1.html'>f3s series</a><br />
<br />
<h2 style='display: inline' id='architecture-overview'>Architecture Overview</h2><br />
<br />
<span>The request flow looks like this:</span><br />
<br />
<pre>
Client → relayd (OpenBSD) → Traefik (k3s) → Apache + CGI (Pod)
</pre>
<br />
<span>Each hop needs to preserve the client&#39;s real IP address via the <span class='inlinecode'>X-Forwarded-For</span> header.</span><br />
<br />
<h2 style='display: inline' id='the-application'>The Application</h2><br />
<br />
<span>The application is a simple Perl CGI script that:</span><br />
<br />
<span>1. Detects whether the client is using IPv4 or IPv6</span><br />
<span>2. Performs DNS lookups on client and server addresses</span><br />
<span>3. Displays diagnostic information</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><i><font color="#ababab">#!/usr/bin/perl</font></i>
<b><font color="#ffffff">use</font></b><font color="#ff0000"> strict</font><font color="#F3E651">;</font>
<b><font color="#ffffff">use</font></b><font color="#ff0000"> warnings</font><font color="#F3E651">;</font>

<b><font color="#ffffff">print</font></b><font color="#ff0000"> </font><font color="#bb00ff">"Content-type: text/html\n\n"</font><font color="#F3E651">;</font>

<b><font color="#ffffff">my</font></b><font color="#ff0000"> </font><font color="#ff0000">$is_ipv4</font><font color="#ff0000"> </font><font color="#F3E651">=</font><font color="#ff0000"> </font><font color="#F3E651">(</font><font color="#ff0000">$ENV</font><font color="#F3E651">{</font><font color="#ff0000">REMOTE_ADDR</font><font color="#F3E651">}</font><font color="#ff0000"> </font><font color="#F3E651">=~</font><font color="#ff0000"> </font><font color="#bb00ff">/(?:\d+\.){3}\d/</font><font color="#F3E651">);</font>
<b><font color="#ffffff">print</font></b><font color="#ff0000"> </font><font color="#bb00ff">"You are using: "</font><font color="#ff0000"> </font><font color="#F3E651">.</font><font color="#ff0000"> </font><font color="#F3E651">(</font><font color="#ff0000">$is_ipv4</font><font color="#ff0000"> </font><font color="#F3E651">?</font><font color="#ff0000"> </font><font color="#bb00ff">"IPv4"</font><font color="#ff0000"> </font><font color="#F3E651">:</font><font color="#ff0000"> </font><font color="#bb00ff">"IPv6"</font><font color="#F3E651">)</font><font color="#ff0000"> </font><font color="#F3E651">.</font><font color="#ff0000"> </font><font color="#bb00ff">"\n"</font><font color="#F3E651">;</font>
<b><font color="#ffffff">print</font></b><font color="#ff0000"> </font><font color="#bb00ff">"Client address: $ENV{REMOTE_ADDR}\n"</font><font color="#F3E651">;</font>
</pre>
<br />
<h2 style='display: inline' id='docker-image'>Docker Image</h2><br />
<br />
<span>The Docker image uses Apache httpd with CGI and <span class='inlinecode'>mod_remoteip</span> enabled:</span><br />
<br />
<pre>
FROM httpd:2.4-alpine

RUN apk add --no-cache perl bind-tools

# Enable CGI and remoteip modules
RUN sed -i &#39;s/#LoadModule cgid_module/LoadModule cgid_module/&#39; \
      /usr/local/apache2/conf/httpd.conf &amp;&amp; \
    sed -i &#39;s/#LoadModule remoteip_module/LoadModule remoteip_module/&#39; \
      /usr/local/apache2/conf/httpd.conf &amp;&amp; \
    echo &#39;RemoteIPHeader X-Forwarded-For&#39; &gt;&gt; /usr/local/apache2/conf/httpd.conf &amp;&amp; \
    echo &#39;RemoteIPInternalProxy 10.0.0.0/8&#39; &gt;&gt; /usr/local/apache2/conf/httpd.conf &amp;&amp; \
    echo &#39;RemoteIPInternalProxy 192.168.0.0/16&#39; &gt;&gt; /usr/local/apache2/conf/httpd.conf

COPY index.pl /usr/local/apache2/cgi-bin/index.pl
</pre>
<br />
<span>The key is <span class='inlinecode'>mod_remoteip</span>: it reads the <span class='inlinecode'>X-Forwarded-For</span> header and sets <span class='inlinecode'>REMOTE_ADDR</span> to the original client IP. The <span class='inlinecode'>RemoteIPInternalProxy</span> directives tell Apache which upstream proxies to trust.</span><br />
<br />
<h2 style='display: inline' id='kubernetes-deployment'>Kubernetes Deployment</h2><br />
<br />
<span>The Helm chart is straightforward:</span><br />
<br />
<pre>
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ipv6test
  namespace: services
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ipv6test
  template:
    spec:
      containers:
      - name: ipv6test
        image: registry.lan.buetow.org:30001/ipv6test:1.1.0
        ports:
        - containerPort: 80
</pre>
<br />
<h2 style='display: inline' id='configuring-traefik-to-trust-forwarded-headers'>Configuring Traefik to Trust Forwarded Headers</h2><br />
<br />
<span>By default, Traefik overwrites <span class='inlinecode'>X-Forwarded-For</span> with its own view of the client IP (which is the upstream proxy, not the real client). To preserve the original header, Traefik needs to trust the upstream proxies.</span><br />
<br />
<span>In k3s, this is configured via a HelmChartConfig:</span><br />
<br />
<pre>
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
  name: traefik
  namespace: kube-system
spec:
  valuesContent: |-
    additionalArguments:
      - "--entryPoints.web.forwardedHeaders.trustedIPs=192.168.0.0/16,10.0.0.0/8"
      - "--entryPoints.websecure.forwardedHeaders.trustedIPs=192.168.0.0/16,10.0.0.0/8"
</pre>
<br />
<span>This tells Traefik to trust <span class='inlinecode'>X-Forwarded-For</span> headers from the WireGuard tunnel IPs (where relayd connects from) and internal pod networks.</span><br />
<br />
<h2 style='display: inline' id='relayd-configuration'>Relayd Configuration</h2><br />
<br />
<span>The OpenBSD relayd proxy already sets the <span class='inlinecode'>X-Forwarded-For</span> header:</span><br />
<br />
<pre>
http protocol "https" {
    match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
    match request header set "X-Forwarded-Proto" value "https"
}
</pre>
<br />
<h2 style='display: inline' id='ipv4-only-and-ipv6-only-subdomains'>IPv4-Only and IPv6-Only Subdomains</h2><br />
<br />
<span>To properly test IPv4 and IPv6 connectivity separately, three hostnames are configured:</span><br />
<br />
<ul>
<li>ipv6test.f3s.buetow.org - Dual stack (A + AAAA records)</li>
<li>ipv4.ipv6test.f3s.buetow.org - IPv4 only (A record only)</li>
<li>ipv6.ipv6test.f3s.buetow.org - IPv6 only (AAAA record only)</li>
</ul><br />
<span>The NSD zone template dynamically generates the correct record types:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><font color="#F3E651">&lt;%</font><font color="#ff0000"> </font><b><font color="#ffffff">for</font></b><font color="#ff0000"> </font><b><font color="#ffffff">my</font></b><font color="#ff0000"> </font><font color="#ff0000">$host</font><font color="#ff0000"> </font><font color="#F3E651">(</font><font color="#ff0000">@</font><font color="#ff0000">$f3s_hosts</font><font color="#F3E651">)</font><font color="#ff0000"> </font><font color="#F3E651">{</font>
<font color="#ff0000">     </font><b><font color="#ffffff">my</font></b><font color="#ff0000"> </font><font color="#ff0000">$is_ipv6_only</font><font color="#ff0000"> </font><font color="#F3E651">=</font><font color="#ff0000"> </font><font color="#ff0000">$host</font><font color="#ff0000"> </font><font color="#F3E651">=~</font><font color="#ff0000"> </font><font color="#bb00ff">/^ipv6\./</font><font color="#F3E651">;</font>
<font color="#ff0000">     </font><b><font color="#ffffff">my</font></b><font color="#ff0000"> </font><font color="#ff0000">$is_ipv4_only</font><font color="#ff0000"> </font><font color="#F3E651">=</font><font color="#ff0000"> </font><font color="#ff0000">$host</font><font color="#ff0000"> </font><font color="#F3E651">=~</font><font color="#ff0000"> </font><font color="#bb00ff">/^ipv4\./</font><font color="#F3E651">;</font>
<font color="#F3E651">-%&gt;</font>
<font color="#bb00ff">&lt;% unless ($is_ipv6_only) { -%&gt;</font>
<font color="#bb00ff">&lt;%= $host %&gt;</font><font color="#F3E651">.</font><font color="#ff0000">         </font><font color="#bb00ff">300</font><font color="#ff0000"> IN A </font><font color="#bb00ff">&lt;%= $ips-&gt;</font><font color="#F3E651">{</font><font color="#ff0000">current_master</font><font color="#F3E651">}{</font><font color="#ff0000">ipv4</font><font color="#F3E651">}</font><font color="#ff0000"> </font><font color="#F3E651">%&gt;</font>
<font color="#bb00ff">&lt;% } -%&gt;</font>
<font color="#bb00ff">&lt;% unless ($is_ipv4_only) { -%&gt;</font>
<font color="#bb00ff">&lt;%= $host %&gt;</font><font color="#F3E651">.</font><font color="#ff0000">         </font><font color="#bb00ff">300</font><font color="#ff0000"> IN AAAA </font><font color="#bb00ff">&lt;%= $ips-&gt;</font><font color="#F3E651">{</font><font color="#ff0000">current_master</font><font color="#F3E651">}{</font><font color="#ff0000">ipv6</font><font color="#F3E651">}</font><font color="#ff0000"> </font><font color="#F3E651">%&gt;</font>
<font color="#bb00ff">&lt;% } -%&gt;</font>
<font color="#bb00ff">&lt;% } -%&gt;</font>
</pre>
<br />
<span>This ensures:</span><br />
<ul>
<li>Hosts starting with <span class='inlinecode'>ipv6.</span> get only AAAA records</li>
<li>Hosts starting with <span class='inlinecode'>ipv4.</span> get only A records</li>
<li>All other hosts get both A and AAAA records</li>
</ul><br />
<span>The Kubernetes ingress handles all three hostnames, routing to the same backend service.</span><br />
<br />
<h2 style='display: inline' id='tls-certificates-with-subject-alternative-names'>TLS Certificates with Subject Alternative Names</h2><br />
<br />
<span>Since Let&#39;s Encrypt validates domains via HTTP, the IPv6-only subdomain (<span class='inlinecode'>ipv6.ipv6test.f3s.buetow.org</span>) cannot be validated directly—Let&#39;s Encrypt&#39;s validation servers use IPv4. The solution is to include all subdomains as Subject Alternative Names (SANs) in the parent certificate.</span><br />
<br />
<span>The ACME client configuration template dynamically builds the SAN list:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><font color="#F3E651">&lt;%</font><font color="#ff0000"> </font><b><font color="#ffffff">for</font></b><font color="#ff0000"> </font><b><font color="#ffffff">my</font></b><font color="#ff0000"> </font><font color="#ff0000">$host</font><font color="#ff0000"> </font><font color="#F3E651">(</font><font color="#ff0000">@</font><font color="#ff0000">$acme_hosts</font><font color="#F3E651">)</font><font color="#ff0000"> </font><font color="#F3E651">{</font>
<font color="#ff0000">     </font><i><font color="#ababab"># Skip ipv4/ipv6 subdomains - they're included as SANs in parent cert</font></i>
<font color="#ff0000">     </font><b><font color="#ffffff">next</font></b><font color="#ff0000"> </font><b><font color="#ffffff">if</font></b><font color="#ff0000"> </font><font color="#ff0000">$host</font><font color="#ff0000"> </font><font color="#F3E651">=~</font><font color="#ff0000"> </font><font color="#bb00ff">/^(ipv4|ipv6)\./</font><font color="#F3E651">;</font>
<font color="#F3E651">-%&gt;</font>
<font color="#F3E651">&lt;%</font><font color="#ff0000">   </font><b><font color="#ffffff">my</font></b><font color="#ff0000"> </font><font color="#ff0000">@alt_names</font><font color="#ff0000"> </font><font color="#F3E651">=</font><font color="#ff0000"> </font><font color="#F3E651">(</font><font color="#bb00ff">"www.$host"</font><font color="#F3E651">);</font>
<font color="#ff0000">     </font><b><font color="#ffffff">for</font></b><font color="#ff0000"> </font><b><font color="#ffffff">my</font></b><font color="#ff0000"> </font><font color="#ff0000">$sub_host</font><font color="#ff0000"> </font><font color="#F3E651">(</font><font color="#ff0000">@</font><font color="#ff0000">$acme_hosts</font><font color="#F3E651">)</font><font color="#ff0000"> </font><font color="#F3E651">{</font>
<font color="#ff0000">         </font><b><font color="#ffffff">if</font></b><font color="#ff0000"> </font><font color="#F3E651">(</font><font color="#ff0000">$sub_host</font><font color="#ff0000"> </font><font color="#F3E651">=~</font><font color="#ff0000"> </font><font color="#bb00ff">/^(ipv4|ipv6)\.\Q$host\E$/</font><font color="#F3E651">)</font><font color="#ff0000"> </font><font color="#F3E651">{</font>
<font color="#ff0000">             </font><b><font color="#ffffff">push</font></b><font color="#ff0000"> </font><font color="#ff0000">@alt_names</font><font color="#F3E651">,</font><font color="#ff0000"> </font><font color="#ff0000">$sub_host</font><font color="#F3E651">;</font>
<font color="#ff0000">         </font><font color="#F3E651">}</font>
<font color="#ff0000">     </font><font color="#F3E651">}</font>
<font color="#F3E651">-%&gt;</font>
<font color="#ff0000">domain </font><font color="#bb00ff">&lt;%= $host %&gt;</font><font color="#ff0000"> </font><font color="#F3E651">{</font>
<font color="#ff0000">    alternative names </font><font color="#F3E651">{</font><font color="#ff0000"> </font><font color="#bb00ff">&lt;%= join(' ', @alt_names) %&gt;</font><font color="#ff0000"> </font><font color="#F3E651">}</font>
<font color="#ff0000">    </font><font color="#F3E651">...</font>
<font color="#F3E651">}</font>
<font color="#bb00ff">&lt;% } -%&gt;</font>
</pre>
<br />
<span>This generates a single certificate for <span class='inlinecode'>ipv6test.f3s.buetow.org</span> that includes:</span><br />
<ul>
<li>www.ipv6test.f3s.buetow.org</li>
<li>ipv4.ipv6test.f3s.buetow.org</li>
<li>ipv6.ipv6test.f3s.buetow.org</li>
</ul><br />
<h2 style='display: inline' id='dns-and-tls-deployment'>DNS and TLS Deployment</h2><br />
<br />
<span>The DNS records and ACME certificates are managed via Rex automation:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><font color="#ffffff">our</font></b><font color="#ff0000"> </font><font color="#ff0000">@f3s_hosts</font><font color="#ff0000"> </font><font color="#F3E651">=</font><font color="#ff0000"> </font><b><font color="#ffffff">qw</font></b><font color="#F3E651">/</font>
<font color="#ff0000">    </font><font color="#F3E651">...</font>
<font color="#ff0000">    ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org</font>
<font color="#ff0000">    ipv4</font><font color="#F3E651">.</font><font color="#ff0000">ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org</font>
<font color="#ff0000">    ipv6</font><font color="#F3E651">.</font><font color="#ff0000">ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org</font>
<font color="#F3E651">/;</font>

<b><font color="#ffffff">our</font></b><font color="#ff0000"> </font><font color="#ff0000">@acme_hosts</font><font color="#ff0000"> </font><font color="#F3E651">=</font><font color="#ff0000"> </font><b><font color="#ffffff">qw</font></b><font color="#F3E651">/</font>
<font color="#ff0000">    </font><font color="#F3E651">...</font>
<font color="#ff0000">    ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org</font>
<font color="#ff0000">    ipv4</font><font color="#F3E651">.</font><font color="#ff0000">ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org</font>
<font color="#ff0000">    ipv6</font><font color="#F3E651">.</font><font color="#ff0000">ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org</font>
<font color="#F3E651">/;</font>
</pre>
<br />
<span>Running <span class='inlinecode'>rex nsd httpd acme acme_invoke relayd</span> deploys the DNS zone, configures httpd for ACME challenges, obtains the certificates, and reloads relayd.</span><br />
<br />
<h2 style='display: inline' id='testing'>Testing</h2><br />
<br />
<span>Verify DNS records are correct:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><font color="#ff0000">$ dig ipv4</font><font color="#F3E651">.</font><font color="#ff0000">ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org A </font><font color="#F3E651">+</font><font color="#ff0000">short</font>
<font color="#bb00ff">46.23</font><font color="#F3E651">.</font><font color="#bb00ff">94.99</font>

<font color="#ff0000">$ dig ipv4</font><font color="#F3E651">.</font><font color="#ff0000">ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org AAAA </font><font color="#F3E651">+</font><font color="#ff0000">short</font>
<font color="#F3E651">(</font><font color="#ff0000">no output - IPv4 only</font><font color="#F3E651">)</font>

<font color="#ff0000">$ dig ipv6</font><font color="#F3E651">.</font><font color="#ff0000">ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org AAAA </font><font color="#F3E651">+</font><font color="#ff0000">short</font>
<font color="#ff0000">2a03</font><font color="#F3E651">:</font><font color="#bb00ff">6000</font><font color="#F3E651">:</font><font color="#ff0000">6f67</font><font color="#F3E651">:</font><font color="#bb00ff">624</font><font color="#F3E651">::</font><font color="#bb00ff">99</font>

<font color="#ff0000">$ dig ipv6</font><font color="#F3E651">.</font><font color="#ff0000">ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org A </font><font color="#F3E651">+</font><font color="#ff0000">short</font>
<font color="#F3E651">(</font><font color="#ff0000">no output - IPv6 only</font><font color="#F3E651">)</font>
</pre>
<br />
<span>Verify the application shows the correct test type:</span><br />
<br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><font color="#ff0000">$ curl -s https</font><font color="#F3E651">:</font><font color="#ff0000">//ipv</font><font color="#bb00ff">4</font><font color="#F3E651">.</font><font color="#ff0000">ipv6test</font><font color="#F3E651">.</font><font color="#ff0000">f3s</font><font color="#F3E651">.</font><font color="#ff0000">buetow</font><font color="#F3E651">.</font><font color="#ff0000">org/cgi-bin/index</font><font color="#F3E651">.</font><font color="#ff0000">pl </font><font color="#F3E651">|</font><font color="#ff0000"> grep </font><font color="#bb00ff">"Test Results"</font>
<font color="#F3E651">&lt;</font><font color="#ff0000">h3</font><font color="#F3E651">&gt;</font><font color="#ff0000">IPv4 Only Test Results</font><font color="#F3E651">:&lt;</font><font color="#ff0000">/h</font><font color="#bb00ff">3</font><font color="#F3E651">&gt;</font>
</pre>
<br />
<span>The displayed IP should be the real client IP, not an internal cluster address.</span><br />
<br />
<h2 style='display: inline' id='w3c-compliant-html'>W3C Compliant HTML</h2><br />
<br />
<span>The CGI script generates valid HTML5 that passes W3C validation. Key considerations:</span><br />
<br />
<ul>
<li>Proper DOCTYPE, charset, and lang attributes</li>
<li>HTML-escaping command outputs (dig output contains <span class='inlinecode'>&lt;&lt;&gt;&gt;</span> characters)</li>
</ul><br />
<!-- Generator: GNU source-highlight 3.1.9
by Lorenzo Bettini
http://www.lorenzobettini.it
http://www.gnu.org/software/src-highlite -->
<pre><b><font color="#ffffff">sub</font></b><font color="#ff0000"> html_escape </font><font color="#F3E651">{</font>
<font color="#ff0000">    </font><b><font color="#ffffff">my</font></b><font color="#ff0000"> </font><font color="#ff0000">$str</font><font color="#ff0000"> </font><font color="#F3E651">=</font><font color="#ff0000"> </font><b><font color="#ffffff">shift</font></b><font color="#F3E651">;</font>
<font color="#ff0000">    </font><font color="#ff0000">$str</font><font color="#ff0000"> </font><font color="#F3E651">=~</font><font color="#ff0000"> </font><b><font color="#ffffff">s</font></b><font color="#ff0000">/&amp;/&amp;amp;/</font><b><font color="#ffffff">g</font></b><font color="#F3E651">;</font>
<font color="#ff0000">    </font><font color="#ff0000">$str</font><font color="#ff0000"> </font><font color="#F3E651">=~</font><font color="#ff0000"> </font><b><font color="#ffffff">s</font></b><font color="#ff0000">/&lt;/&amp;lt;/</font><b><font color="#ffffff">g</font></b><font color="#F3E651">;</font>
<font color="#ff0000">    </font><font color="#ff0000">$str</font><font color="#ff0000"> </font><font color="#F3E651">=~</font><font color="#ff0000"> </font><b><font color="#ffffff">s</font></b><font color="#ff0000">/&gt;/&amp;gt;/</font><b><font color="#ffffff">g</font></b><font color="#F3E651">;</font>
<font color="#ff0000">    </font><b><font color="#ffffff">return</font></b><font color="#ff0000"> </font><font color="#ff0000">$str</font><font color="#F3E651">;</font>
<font color="#F3E651">}</font>

<b><font color="#ffffff">my</font></b><font color="#ff0000"> </font><font color="#ff0000">$digremote</font><font color="#ff0000"> </font><font color="#F3E651">=</font><font color="#ff0000"> </font><font color="#7bc710">html_escape</font><font color="#F3E651">(</font><font color="#ff0000">`dig </font><font color="#F3E651">-</font><b><font color="#ffffff">x</font></b><font color="#ff0000"> </font><font color="#ff0000">$ENV</font><font color="#F3E651">{</font><font color="#ff0000">REMOTE_ADDR</font><font color="#F3E651">}</font><font color="#ff0000">`</font><font color="#F3E651">);</font>
</pre>
<br />
<span>You can verify the output passes validation:</span><br />
<br />
<a class='textlink' href='https://validator.w3.org/nu/?doc=https%3A%2F%2Fipv6test.f3s.buetow.org%2Fcgi-bin%2Findex.pl'>W3C Validator</a><br />
<br />
<h2 style='display: inline' id='summary'>Summary</h2><br />
<br />
<span>Preserving client IP addresses through multiple reverse proxies requires configuration at each layer:</span><br />
<br />
<span>1. **relayd**: Sets <span class='inlinecode'>X-Forwarded-For</span> header</span><br />
<span>2. **Traefik**: Trusts headers from known proxy IPs via <span class='inlinecode'>forwardedHeaders.trustedIPs</span></span><br />
<span>3. **Apache**: Uses <span class='inlinecode'>mod_remoteip</span> to set <span class='inlinecode'>REMOTE_ADDR</span> from the header</span><br />
<br />
<span>Additional challenges solved:</span><br />
<br />
<ul>
<li>**TLS for IPv6-only hosts**: Use SANs to include all subdomains in a single certificate validated via the dual-stack parent domain</li>
<li>**W3C compliance**: HTML-escape all command outputs to handle special characters</li>
</ul><br />
<span>The configuration is managed via GitOps with ArgoCD, including the Traefik HelmChartConfig.</span><br />
<br />
<a class='textlink' href='https://codeberg.org/snonux/ipv6test'>Source code</a><br />
<a class='textlink' href='https://codeberg.org/snonux/conf/src/branch/master/f3s/ipv6test'>Kubernetes manifests</a><br />
<a class='textlink' href='https://codeberg.org/snonux/conf/src/branch/master/f3s/traefik-config'>Traefik configuration</a><br />
<br />
<span>E-Mail your comments to paul@paulbias.net :-)</span><br />
<br />
<a class='textlink' href='./index.html'>← Back to the index</a><br />
<p class="footer">
    Generated with <a href="https://codeberg.org/snonux/gemtexter">Gemtexter 3.0.1-develop</a> |
    served by <a href="https://www.OpenBSD.org">OpenBSD</a>/<a href="https://man.openbsd.org/relayd.8">relayd(8)</a>+<a href="https://man.openbsd.org/httpd.8">httpd(8)</a> |
    <a href="https://foo.zone/site-mirrors.html">Site Mirrors</a>
    <br />
    Webring: <a href="https://shring.sh/foo.zone/previous">previous</a> | <a href="https://shring.sh">shring</a> | <a href="https://shring.sh/foo.zone/next">next</a>
</p>
<script type="text/javascript" src="../retrofuturistic.js"></script>
</body>
</html>