]> git.kaiwu.me - haproxy.git/commitdiff
REGTESTS: connection: test the NetScaler CIP protocol
authorWilly Tarreau <w@1wt.eu>
Tue, 22 Sep 2026 19:49:06 +0000 (19:49 +0000)
committerWilly Tarreau <w@1wt.eu>
Wed, 30 Sep 2026 15:25:37 +0000 (17:25 +0200)
Neither the "accept-netscaler-cip" bind keyword nor the
"tcp-request connection expect-netscaler-cip layer4" action had any
coverage, the notes listing them as doable now that the expect-proxy
test shows the pattern, the only obstacle being that the CIP header has
to be built by hand. It is built with sendhex here, in its standard
form, and the test follows the structure of the expect-proxy one.

The first frontend declares the magic number on its bind line, which
makes the header mandatory: a well-formed IPv4 header, then an IPv6 one,
replace the addresses of the session while fc_src keeps reporting the
real peer, whereas a wrong magic, an encapsulated protocol which is not
TCP, and a request with no header at all are all rejected.

The second frontend uses the action, which only expects the header from
the sources matching its condition. Note that the action reads the
magic number from the bind line, so without "accept-netscaler-cip"
there the expected magic is zero, which the test documents.

reg-tests/connection/netscaler_cip.vtc [new file with mode: 0644]

diff --git a/reg-tests/connection/netscaler_cip.vtc b/reg-tests/connection/netscaler_cip.vtc
new file mode 100644 (file)
index 0000000..6cb72c0
--- /dev/null
@@ -0,0 +1,128 @@
+varnishtest "Test the NetScaler Client IP insertion protocol"
+
+# Neither the "accept-netscaler-cip" bind keyword nor the
+# "tcp-request connection expect-netscaler-cip layer4" action had any
+# coverage. Both make haproxy consume a CIP header in front of the
+# request and take the addresses of the connection from the IP and TCP
+# headers it encapsulates, the same way the PROXY protocol does, which
+# reg-tests/connection/expect_proxy.vtc covers.
+#
+# The header is built by hand here. In its standard form it is made of:
+#
+#   12345678      the magic number, which the bind line declares
+#   0000002c      the CIP length, which the parser does not check
+#   0000          the CIP type, zero for the standard format
+#   0028          the length of the encapsulated headers
+#
+# followed by that many bytes of IP and TCP headers. Only the addresses,
+# the ports, the IP version and the protocol are looked at, so the
+# checksums are left to zero.
+#
+# The addresses announced are deliberately different from the real ones
+# so that the src and dst fetches show which of the two was used.
+
+feature ignore_unknown_macro
+
+haproxy h1 -conf {
+    global
+    .if feature(THREAD)
+        thread-groups 1
+    .endif
+        chroot /
+
+    defaults
+       mode http
+       timeout connect "${HAPROXY_TEST_TIMEOUT-5s}"
+       timeout client  "${HAPROXY_TEST_TIMEOUT-5s}"
+       timeout server  "${HAPROXY_TEST_TIMEOUT-5s}"
+
+    # the bind keyword makes the header mandatory for every connection
+    frontend fe1
+       bind "fd@${fe1}" accept-netscaler-cip 305419896
+       http-request return status 200
+       http-after-response set-header x-src "%[src]:%[src_port]"
+       http-after-response set-header x-dst "%[dst]:%[dst_port]"
+       http-after-response set-header x-fcsrc "%[fc_src]"
+
+    # the action only expects it from the connections matching its
+    # condition. Note that it reads the magic number from the bind line,
+    # which has none here, so the expected magic is zero.
+    frontend fe2
+       bind "fd@${fe2}"
+       tcp-request connection expect-netscaler-cip layer4 if { src 127.0.0.0/8 }
+       http-request return status 200
+       http-after-response set-header x-src "%[src]:%[src_port]"
+       http-after-response set-header x-dst "%[dst]:%[dst_port]"
+
+    # here the condition never matches, so no header is expected
+    frontend fe3
+       bind "fd@${fe3}"
+       tcp-request connection expect-netscaler-cip layer4 if { src 10.0.0.0/8 }
+       http-request return status 200
+       http-after-response set-header x-src "%[src]:%[src_port]"
+} -start
+
+# 12345678 0000002c 0000 0028              the CIP header
+# 45 00 0028 0001 0000 40 06 0000          IPv4, 40 bytes, TCP
+# c0a80001 0a000002                        192.168.0.1 -> 10.0.0.2
+# 04d2 0051 00000000 00000000 5002 2000 0000 0000    1234 -> 81
+client c1 -connect ${h1_fe1_sock} {
+       sendhex "12345678 0000002c 0000 0028 4500002800010000400600 00 c0a80001 0a000002 04d2005100000000000000005002200000000000"
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-src   == "192.168.0.1:1234"
+       expect resp.http.x-dst   == "10.0.0.2:81"
+       # only the session addresses are replaced: fc_src still reports the
+       # real peer of the connection
+       expect resp.http.x-fcsrc == "127.0.0.1"
+} -run
+
+# the same in IPv6: 60 00000000 0014 06 40, then the two 16-byte
+# addresses and the same TCP header, that is 60 bytes announced
+client c2 -connect ${h1_fe1_sock} {
+       sendhex "12345678 00000040 0000 003c 60000000 0014 06 40 20010db8000000000000000000000001 20010db8000000000000000000000002 04d2005100000000000000005002200000000000"
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-src == "2001:db8::1:1234"
+       expect resp.http.x-dst == "2001:db8::2:81"
+} -run
+
+# a header whose magic is not the declared one is rejected
+client c3 -connect ${h1_fe1_sock} {
+       sendhex "deadbeef 0000002c 0000 0028 4500002800010000400600 00 c0a80001 0a000002 04d2005100000000000000005002200000000000"
+       expect_close
+} -run
+
+# a valid magic, but the encapsulated protocol is not TCP (17 = UDP)
+client c4 -connect ${h1_fe1_sock} {
+       sendhex "12345678 0000002c 0000 0028 4500002800010000401100 00 c0a80001 0a000002 04d2005100000000000000005002200000000000"
+       expect_close
+} -run
+
+# the header is mandatory here, a request without one is rejected
+client c5 -connect ${h1_fe1_sock} {
+       txreq
+       expect_close
+} -run
+
+# the action expects the header only from the loopback, and with a zero
+# magic since its bind line declares none
+client c6 -connect ${h1_fe2_sock} {
+       sendhex "00000000 0000002c 0000 0028 4500002800010000400600 00 c0a80001 0a000002 04d2005100000000000000005002200000000000"
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-src == "192.168.0.1:1234"
+       expect resp.http.x-dst == "10.0.0.2:81"
+} -run
+
+# the condition does not match the loopback, so no header is expected
+# and the real addresses are reported
+client c7 -connect ${h1_fe3_sock} {
+       txreq
+       rxresp
+       expect resp.status == 200
+       expect resp.http.x-src ~ "^127[.]0[.]0[.]1:[0-9]+$"
+} -run