Browse Source

haproxy: add --haproxy-clientip flag to spoof client IPs

CURLOPT_HAPROXY_CLIENT_IP in the library

Closes #10779
Raito Bezarius 1 year ago
parent
commit
0a75964d0d

+ 1 - 0
docs/cmdline-opts/Makefile.inc

@@ -96,6 +96,7 @@ DPAGES = \
   globoff.d \
   happy-eyeballs-timeout-ms.d \
   haproxy-protocol.d \
+  haproxy-clientip.d \
   head.d \
   header.d \
   help.d \

+ 29 - 0
docs/cmdline-opts/haproxy-clientip.d

@@ -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.

+ 2 - 0
docs/libcurl/curl_easy_setopt.3

@@ -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

+ 63 - 0
docs/libcurl/opts/CURLOPT_HAPROXY_CLIENT_IP.3

@@ -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), "

+ 1 - 0
docs/libcurl/opts/Makefile.inc

@@ -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                      \

+ 1 - 0
docs/libcurl/symbols-in-versions

@@ -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

+ 1 - 0
docs/options-in-versions

@@ -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

+ 4 - 1
include/curl/curl.h

@@ -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;
 

+ 1 - 0
include/curl/typecheck-gcc.h

@@ -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 ||                                           \

+ 6 - 1
lib/cf-haproxy.c

@@ -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);
 

+ 2 - 1
lib/easyoptions.c

@@ -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

+ 9 - 0
lib/setopt.c

@@ -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:
     /*

+ 1 - 0
lib/urldata.h

@@ -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 -- */
 

+ 1 - 0
src/tool_cfgable.c

@@ -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);

+ 1 - 0
src/tool_cfgable.h

@@ -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 {

+ 4 - 0
src/tool_getparam.c

@@ -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;

+ 3 - 0
src/tool_listhelp.c

@@ -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},

+ 5 - 0
src/tool_operate.c

@@ -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);
 

+ 2 - 1
tests/data/Makefile.inc

@@ -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

+ 63 - 0
tests/data/test3201

@@ -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
+GET /%TESTNUMBER HTTP/1.1
+Host: %HOSTIP:%HTTPPORT
+User-Agent: curl/%VERSION
+Accept: */*
+Testno: %TESTNUMBER
+
+</protocol>
+</verify>
+</testcase>

+ 67 - 0
tests/data/test3202

@@ -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
+GET /%TESTNUMBER HTTP/1.1
+Host: %HOST6IP:%HTTP6PORT
+User-Agent: curl/%VERSION
+Accept: */*
+
+</protocol>
+</verify>
+</testcase>