]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
haproxy: add --haproxy-clientip flag to spoof client IPs
authorRaito Bezarius <masterancpp@gmail.com>
Thu, 16 Mar 2023 13:20:11 +0000 (14:20 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Mon, 5 Jun 2023 18:08:37 +0000 (20:08 +0200)
CURLOPT_HAPROXY_CLIENT_IP in the library

Closes #10779

21 files changed:
docs/cmdline-opts/Makefile.inc
docs/cmdline-opts/haproxy-clientip.d [new file with mode: 0644]
docs/libcurl/curl_easy_setopt.3
docs/libcurl/opts/CURLOPT_HAPROXY_CLIENT_IP.3 [new file with mode: 0644]
docs/libcurl/opts/Makefile.inc
docs/libcurl/symbols-in-versions
docs/options-in-versions
include/curl/curl.h
include/curl/typecheck-gcc.h
lib/cf-haproxy.c
lib/easyoptions.c
lib/setopt.c
lib/urldata.h
src/tool_cfgable.c
src/tool_cfgable.h
src/tool_getparam.c
src/tool_listhelp.c
src/tool_operate.c
tests/data/Makefile.inc
tests/data/test3201 [new file with mode: 0644]
tests/data/test3202 [new file with mode: 0644]

index 149db5878a7a8ba637aa670b320ecdc321db391b..10cc14b03a7678a020110a046fe593fac212be61 100644 (file)
@@ -96,6 +96,7 @@ DPAGES = \
   globoff.d \
   happy-eyeballs-timeout-ms.d \
   haproxy-protocol.d \
+  haproxy-clientip.d \
   head.d \
   header.d \
   help.d \
diff --git a/docs/cmdline-opts/haproxy-clientip.d b/docs/cmdline-opts/haproxy-clientip.d
new file mode 100644 (file)
index 0000000..22eda07
--- /dev/null
@@ -0,0 +1,29 @@
+c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+SPDX-License-Identifier: curl
+Long: haproxy-clientip
+Help: Sets client IP in HAProxy PROXY protocol v1 header
+Protocols: HTTP
+Added: 8.2.0
+Category: http proxy
+Example: --haproxy-clientip $IP
+See-also: proxy
+Multi: single
+---
+Sets a client IP in HAProxy PROXY protocol v1 header at the beginning of the
+connection.
+
+For valid requests, IPv4 addresses must be indicated as a series of exactly
+4 integers in the range [0..255] inclusive written in decimal representation
+separated by exactly one dot between each other. Heading zeroes are not
+permitted in front of numbers in order to avoid any possible confusion
+with octal numbers. IPv6 addresses must be indicated as series of 4 hexadecimal
+digits (upper or lower case) delimited by colons between each other, with the
+acceptance of one double colon sequence to replace the largest acceptable range
+of consecutive zeroes. The total number of decoded bits must exactly be 128.
+
+Otherwise, any string can be accepted for the client IP and will be sent.
+
+It replaces `--haproxy-protocol` if used, it is not necessary to specify both flags.
+
+This option is primarily useful when sending test requests to
+verify a service is working as intended.
index b7257c425ca8bf5094c394bf816779bf62d03b07..784495d6ec7bcd88df3fb949c1209865842bec0e 100644 (file)
@@ -208,6 +208,8 @@ Socks5 GSSAPI NEC mode. See \fICURLOPT_SOCKS5_GSSAPI_NEC(3)\fP
 Proxy authentication service name. \fICURLOPT_PROXY_SERVICE_NAME(3)\fP
 .IP CURLOPT_HAPROXYPROTOCOL
 Send an HAProxy PROXY protocol v1 header. See \fICURLOPT_HAPROXYPROTOCOL(3)\fP
+.IP CURLOPT_HAPROXY_CLIENT_IP
+Spoof the client IP in an HAProxy PROXY protocol v1 header. See \fICURLOPT_HAPROXY_CLIENT_IP(3)\fP
 .IP CURLOPT_SERVICE_NAME
 Authentication service name. \fICURLOPT_SERVICE_NAME(3)\fP
 .IP CURLOPT_INTERFACE
diff --git a/docs/libcurl/opts/CURLOPT_HAPROXY_CLIENT_IP.3 b/docs/libcurl/opts/CURLOPT_HAPROXY_CLIENT_IP.3
new file mode 100644 (file)
index 0000000..6c54ee8
--- /dev/null
@@ -0,0 +1,63 @@
+.\" **************************************************************************
+.\" *                                  _   _ ____  _
+.\" *  Project                     ___| | | |  _ \| |
+.\" *                             / __| | | | |_) | |
+.\" *                            | (__| |_| |  _ <| |___
+.\" *                             \___|\___/|_| \_\_____|
+.\" *
+.\" * Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
+.\" *
+.\" * This software is licensed as described in the file COPYING, which
+.\" * you should have received as part of this distribution. The terms
+.\" * are also available at https://curl.se/docs/copyright.html.
+.\" *
+.\" * You may opt to use, copy, modify, merge, publish, distribute and/or sell
+.\" * copies of the Software, and permit persons to whom the Software is
+.\" * furnished to do so, under the terms of the COPYING file.
+.\" *
+.\" * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+.\" * KIND, either express or implied.
+.\" *
+.\" * SPDX-License-Identifier: curl
+.\" *
+.\" **************************************************************************
+.\"
+.TH CURLOPT_HAPROXY_CLIENT_IP 3 "8 May 2023" libcurl libcurl
+.SH NAME
+CURLOPT_HAPROXY_CLIENT_IP \- set HAProxy PROXY protocol client IP
+.SH SYNOPSIS
+.nf
+#include <curl/curl.h>
+
+CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HAPROXY_CLIENT_IP,
+                          char *client_ip);
+.fi
+.SH DESCRIPTION
+When this parameter is set to a valid IPv4 or IPv6, the library will
+not send this address in the HAProxy PROXY protocol
+v1 header at beginning of the connection.
+
+This option is primarily useful when sending test requests to verify that
+a service is working as intended.
+
+.SH DEFAULT
+no HAProxy header will be sent
+.SH PROTOCOLS
+HTTP, HAProxy PROTOCOL
+.SH EXAMPLE
+.nf
+CURL *curl = curl_easy_init();
+if(curl) {
+  CURLcode ret;
+  curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/");
+  curl_easy_setopt(curl, CURLOPT_HAPROXY_CLIENT_IP, "1.1.1.1");
+  ret = curl_easy_perform(curl);
+}
+.fi
+.SH AVAILABILITY
+Along with HTTP. Added in 8.2.0.
+.SH RETURN VALUE
+Returns CURLE_OK if HTTP is enabled, and CURLE_UNKNOWN_OPTION if not.
+.SH SEE ALSO
+.BR CURLOPT_PROXY "(3), "
+.BR CURLOPT_HAPROXYPROTOCOL "(3), "
index d4da3167f9804edeff8ee4081319560da1f5a334..8577b0317f2cd4176a4e18addab1b21f9fbbae0e 100644 (file)
@@ -186,6 +186,7 @@ man_MANS =                                      \
   CURLOPT_GSSAPI_DELEGATION.3                   \
   CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.3           \
   CURLOPT_HAPROXYPROTOCOL.3                     \
+  CURLOPT_HAPROXY_CLIENT_IP.3                   \
   CURLOPT_HEADER.3                              \
   CURLOPT_HEADERDATA.3                          \
   CURLOPT_HEADERFUNCTION.3                      \
index d82401f38b361e72a77f12e6c590b956b137a6d9..7b08c62c0bdd426ea4a2bcb3da00809c2afc93be 100644 (file)
@@ -643,6 +643,7 @@ CURLOPT_FTPSSLAUTH              7.12.2
 CURLOPT_GSSAPI_DELEGATION       7.22.0
 CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS 7.59.0
 CURLOPT_HAPROXYPROTOCOL         7.60.0
+CURLOPT_HAPROXY_CLIENT_IP       8.2.0
 CURLOPT_HEADER                  7.1
 CURLOPT_HEADERDATA              7.10
 CURLOPT_HEADERFUNCTION          7.7.2
index a4307b6abff4dbba3117b0078782975384e0c4d9..d11d139e04a6b812e9d255f9583316933710bc45 100644 (file)
@@ -82,6 +82,7 @@
 --globoff (-g)                       7.6
 --happy-eyeballs-timeout-ms          7.59.0
 --haproxy-protocol                   7.60.0
+--haproxy-clientip                   8.2.0
 --head (-I)                          4.0
 --header (-H)                        5.0
 --help (-h)                          4.0
index e033361a8d13f3b86f4b7460fb5018a75c201742..38393b9684762af8f266d207a463cd5ccb60647d 100644 (file)
@@ -781,7 +781,7 @@ typedef enum {
   CURLPROXY_HTTP_1_0 = 1,   /* added in 7.19.4, force to use CONNECT
                                HTTP/1.0  */
   CURLPROXY_HTTPS = 2,  /* HTTPS but stick to HTTP/1 added in 7.52.0 */
-  CURLPROXY_HTTPS2 = 3, /* HTTPS and attempt HTTP/2 added in 8.1.0 */
+  CURLPROXY_HTTPS2 = 3, /* HTTPS and attempt HTTP/2 added in 8.2.0 */
   CURLPROXY_SOCKS4 = 4, /* support added in 7.15.2, enum existed already
                            in 7.10 */
   CURLPROXY_SOCKS5 = 5, /* added in 7.10 */
@@ -2207,6 +2207,9 @@ typedef enum {
   /* Can leak things, gonna exit() soon */
   CURLOPT(CURLOPT_QUICK_EXIT, CURLOPTTYPE_LONG, 322),
 
+  /* set a specific client IP for HAProxy PROXY protocol header? */
+  CURLOPT(CURLOPT_HAPROXY_CLIENT_IP, CURLOPTTYPE_STRINGPOINT, 323),
+
   CURLOPT_LASTENTRY /* the last unused */
 } CURLoption;
 
index bc8d7a78ce98011f7f38b145cc6f308d867cc7c9..b880f3dc60552ee53a259c84001b9dca0ce9bc5a 100644 (file)
@@ -280,6 +280,7 @@ CURLWARNING(_curl_easy_getinfo_err_curl_off_t,
    (option) == CURLOPT_FTP_ALTERNATIVE_TO_USER ||                             \
    (option) == CURLOPT_FTPPORT ||                                             \
    (option) == CURLOPT_HSTS ||                                                \
+   (option) == CURLOPT_HAPROXY_CLIENT_IP ||                                   \
    (option) == CURLOPT_INTERFACE ||                                           \
    (option) == CURLOPT_ISSUERCERT ||                                          \
    (option) == CURLOPT_KEYPASSWD ||                                           \
index 86d7fd1837d90eba0ca0a683488e339246062669..97bd5fa580ca9b5405c6e363905938c5717fd0d5 100644 (file)
@@ -71,6 +71,7 @@ static CURLcode cf_haproxy_date_out_set(struct Curl_cfilter*cf,
   struct cf_haproxy_ctx *ctx = cf->ctx;
   CURLcode result;
   const char *tcp_version;
+  const char *client_ip;
 
   DEBUGASSERT(ctx);
   DEBUGASSERT(ctx->state == HAPROXY_INIT);
@@ -82,11 +83,15 @@ static CURLcode cf_haproxy_date_out_set(struct Curl_cfilter*cf,
 #endif /* USE_UNIX_SOCKETS */
   /* Emit the correct prefix for IPv6 */
   tcp_version = cf->conn->bits.ipv6 ? "TCP6" : "TCP4";
+  if(data->set.str[STRING_HAPROXY_CLIENT_IP])
+    client_ip = data->set.str[STRING_HAPROXY_CLIENT_IP];
+  else
+    client_ip = data->info.conn_primary_ip;
 
   result = Curl_dyn_addf(&ctx->data_out, "PROXY %s %s %s %i %i\r\n",
                          tcp_version,
                          data->info.conn_local_ip,
-                         data->info.conn_primary_ip,
+                         client_ip,
                          data->info.conn_local_port,
                          data->info.conn_primary_port);
 
index 1e588c37666fa69b93859e97ce6d4f5aa836c8d3..e69c658b0c718e3e198582993559b48e6ca36acf 100644 (file)
@@ -120,6 +120,7 @@ struct curl_easyoption Curl_easyopts[] = {
   {"HAPPY_EYEBALLS_TIMEOUT_MS", CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS,
    CURLOT_LONG, 0},
   {"HAPROXYPROTOCOL", CURLOPT_HAPROXYPROTOCOL, CURLOT_LONG, 0},
+  {"HAPROXY_CLIENT_IP", CURLOPT_HAPROXY_CLIENT_IP, CURLOT_STRING, 0},
   {"HEADER", CURLOPT_HEADER, CURLOT_LONG, 0},
   {"HEADERDATA", CURLOPT_HEADERDATA, CURLOT_CBPTR, 0},
   {"HEADERFUNCTION", CURLOPT_HEADERFUNCTION, CURLOT_FUNCTION, 0},
@@ -372,6 +373,6 @@ struct curl_easyoption Curl_easyopts[] = {
  */
 int Curl_easyopts_check(void)
 {
-  return ((CURLOPT_LASTENTRY%10000) != (322 + 1));
+  return ((CURLOPT_LASTENTRY%10000) != (323 + 1));
 }
 #endif
index afe7f7416a8a98c0fff7fefd77f5d6e786efd2eb..b05162a556a10f64b3b00e21262f0e52a3d8f082 100644 (file)
@@ -1867,6 +1867,15 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
      */
     data->set.haproxyprotocol = (0 != va_arg(param, long)) ? TRUE : FALSE;
     break;
+  case CURLOPT_HAPROXY_CLIENT_IP:
+    /*
+     * Set the client IP to send through HAProxy PROXY protocol
+     */
+    result = Curl_setstropt(&data->set.str[STRING_HAPROXY_CLIENT_IP],
+                            va_arg(param, char *));
+    /* We enable implicitly the HAProxy protocol if we use this flag. */
+    data->set.haproxyprotocol = TRUE;
+    break;
 #endif
   case CURLOPT_INTERFACE:
     /*
index f02e6654143e1e802627c290e21b430aa9711d65..ef81fc01b83c01aa091aa50b9a090ec02cbe47dc 100644 (file)
@@ -1563,6 +1563,7 @@ enum dupstring {
   STRING_DNS_LOCAL_IP6,
   STRING_SSL_EC_CURVES,
   STRING_AWS_SIGV4, /* Parameters for V4 signature */
+  STRING_HAPROXY_CLIENT_IP,     /* CURLOPT_HAPROXY_CLIENT_IP */
 
   /* -- end of null-terminated strings -- */
 
index 2031bd926862b33cc9224773b29197adee1de631..04f0b05f445c4a4277c758105ce16589772bae58 100644 (file)
@@ -54,6 +54,7 @@ static void free_config_fields(struct OperationConfig *config)
   Curl_safefree(config->useragent);
   Curl_safefree(config->altsvc);
   Curl_safefree(config->hsts);
+  Curl_safefree(config->haproxy_clientip);
   curl_slist_free_all(config->cookies);
   Curl_safefree(config->cookiejar);
   curl_slist_free_all(config->cookiefiles);
index a0442ff4341eeca5f80d46d7cffa7f09a43965a8..7cd6e12eccac438feaa6558d5a6cbb0fcfe0669a 100644 (file)
@@ -278,6 +278,7 @@ struct OperationConfig {
   long happy_eyeballs_timeout_ms; /* happy eyeballs timeout in milliseconds.
                                      0 is valid. default: CURL_HET_DEFAULT. */
   bool haproxy_protocol;          /* whether to send HAProxy protocol v1 */
+  char *haproxy_clientip;         /* client IP for HAProxy protocol */
   bool disallow_username_in_url;  /* disallow usernames in URLs */
   char *aws_sigv4;
   enum {
index cee56d0baf26c43524c10ab1516cf8ef23fbe528..29ed6516b458b76126eb71c53345ec1c95b3abb1 100644 (file)
@@ -122,6 +122,7 @@ static const struct LongShort aliases[]= {
   {"*x", "krb4",                     ARG_STRING},
          /* 'krb4' is the previous name */
   {"*X", "haproxy-protocol",         ARG_BOOL},
+  {"*P", "haproxy-clientip",         ARG_STRING},
   {"*y", "max-filesize",             ARG_STRING},
   {"*z", "disable-eprt",             ARG_BOOL},
   {"*Z", "eprt",                     ARG_BOOL},
@@ -1055,6 +1056,9 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */
       case 'X': /* --haproxy-protocol */
         config->haproxy_protocol = toggle;
         break;
+      case 'P': /* --haproxy-clientip */
+        GetStr(&config->haproxy_clientip, nextarg);
+        break;
       case 'y': /* --max-filesize */
         {
           curl_off_t value;
index b6f85fcb81138d980c576bdaed5bbac999df6cd4..1e6bd722733f6aaa6701bdebff885b2950de6bec 100644 (file)
@@ -249,6 +249,9 @@ const struct helptxt helptext[] = {
   {"    --haproxy-protocol",
    "Send HAProxy PROXY protocol v1 header",
    CURLHELP_HTTP | CURLHELP_PROXY},
+  {"    --haproxy-clientip",
+    "Sets the HAProxy PROXY protocol v1 client IP",
+    CURLHELP_HTTP | CURLHELP_PROXY},
   {"-I, --head",
    "Show document info only",
    CURLHELP_HTTP | CURLHELP_FTP | CURLHELP_FILE},
index 9dea412b0329627938d6533f46a12ad8bb5c4480..a72e8b6d27e7357abeb10472075af3541eaafab5 100644 (file)
@@ -2161,6 +2161,11 @@ static CURLcode single_transfer(struct GlobalConfig *global,
         if(config->haproxy_protocol)
           my_setopt(curl, CURLOPT_HAPROXYPROTOCOL, 1L);
 
+        /* new in 8.2.0 */
+        if(config->haproxy_clientip)
+          my_setopt_str(curl, CURLOPT_HAPROXY_CLIENT_IP,
+              config->haproxy_clientip);
+
         if(config->disallow_username_in_url)
           my_setopt(curl, CURLOPT_DISALLOW_USERNAME_IN_URL, 1L);
 
index f054175edb50ea79e381bbd7b3ad6b9a6c0f6936..91f2275a9897bfe5f43f8c5f347db61b53143605 100644 (file)
@@ -258,4 +258,5 @@ test3016 test3017 test3018 test3019 test3020 test3021 test3022 test3023 \
 test3024 test3025 test3026 test3027 test3028 test3029 test3030 \
 \
 test3100 test3101 \
-test3200
+test3200 \
+test3201 test3202
diff --git a/tests/data/test3201 b/tests/data/test3201
new file mode 100644 (file)
index 0000000..4fb0b8f
--- /dev/null
@@ -0,0 +1,63 @@
+<testcase>
+<info>
+<keywords>
+HTTP
+HTTP GET
+proxy
+haproxy
+</keywords>
+</info>
+
+#
+# Server-side
+<reply name="%TESTNUMBER">
+<data nocheck="yes">
+HTTP/1.1 200 OK
+Date: Tue, 09 Nov 2010 14:49:00 GMT
+Server: test-server/fake
+Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT
+ETag: "21025-dc7-39462498"
+Accept-Ranges: bytes
+Content-Length: 6
+Connection: close
+Content-Type: text/html
+Funny-head: barkbark
+
+-foo-
+</data>
+</reply>
+
+#
+# Client-side
+<client>
+<server>
+http
+</server>
+<name>
+HTTP GET when PROXY Protocol enabled and spoofed client IP
+</name>
+<command>
+http://%HOSTIP:%HTTPPORT/%TESTNUMBER --haproxy-clientip "192.168.1.1" -H "Testno: %TESTNUMBER"
+</command>
+<features>
+proxy
+</features>
+</client>
+
+#
+# Verify data after the test has been "shot"
+<verify>
+<strippart>
+s/^PROXY TCP4 %CLIENTIP 192.168.1.1 (\d*) %HTTPPORT/proxy-line/
+</strippart>
+<protocol>
+proxy-line\r
+GET /%TESTNUMBER HTTP/1.1\r
+Host: %HOSTIP:%HTTPPORT\r
+User-Agent: curl/%VERSION\r
+Accept: */*\r
+Testno: %TESTNUMBER\r
+\r
+</protocol>
+</verify>
+</testcase>
diff --git a/tests/data/test3202 b/tests/data/test3202
new file mode 100644 (file)
index 0000000..8990abc
--- /dev/null
@@ -0,0 +1,67 @@
+<testcase>
+<info>
+<keywords>
+HTTP
+HTTP GET
+proxy
+haproxy
+IPv6
+</keywords>
+</info>
+#
+# Server-side
+<reply>
+<data nocheck="yes">
+HTTP/1.1 200 OK
+Date: Tue, 09 Nov 2010 14:49:00 GMT
+Server: test-server/fake
+Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT
+ETag: "21025-dc7-39462498"
+Accept-Ranges: bytes
+Content-Length: 6
+Connection: close
+Content-Type: text/html
+Funny-head: yesyes
+
+These data aren't actually sent to the client
+</data>
+</reply>
+
+#
+# Client-side
+<client>
+<features>
+ipv6
+</features>
+<server>
+http-ipv6
+</server>
+ <name>
+HTTP-IPv6 GET with PROXY protocol with spoofed client IP
+ </name>
+ <command>
+-g "http://%HOST6IP:%HTTP6PORT/%TESTNUMBER" --haproxy-clientip "2001:db8::"
+</command>
+<features>
+proxy
+</features>
+</client>
+
+#
+# Verify data after the test has been "shot"
+<verify>
+# Strip off the (random) local port number. This test used to use a fixed
+# local port number that frequently causes the test to fail
+<strippart>
+s/PROXY TCP6 ::1 2001:db8:: (\d+) (\d+)/PROXY TCP6 ::1 2001:db8:: $2/
+</strippart>
+<protocol>
+PROXY TCP6 ::1 2001:db8:: %HTTP6PORT\r
+GET /%TESTNUMBER HTTP/1.1\r
+Host: %HOST6IP:%HTTP6PORT\r
+User-Agent: curl/%VERSION\r
+Accept: */*\r
+\r
+</protocol>
+</verify>
+</testcase>