From: Willy Tarreau Date: Tue, 22 Sep 2026 19:49:06 +0000 (+0000) Subject: REGTESTS: connection: test the NetScaler CIP protocol X-Git-Tag: v3.5-dev8~26 X-Git-Url: https://git.kaiwu.me/$%7BGITURL%7D$%7Bcid%7D?a=commitdiff_plain;h=4e25df570d08a41f149d0a118ca319bb91afebff;p=haproxy.git REGTESTS: connection: test the NetScaler CIP protocol 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. --- diff --git a/reg-tests/connection/netscaler_cip.vtc b/reg-tests/connection/netscaler_cip.vtc new file mode 100644 index 000000000..6cb72c013 --- /dev/null +++ b/reg-tests/connection/netscaler_cip.vtc @@ -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