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

CURLOPT_HAPROXY_CLIENT_IP in the library

Closes #10779
This commit is contained in:
Raito Bezarius 2023-03-16 14:20:11 +01:00 committed by Daniel Stenberg
parent 9ad23c38e5
commit 0a75964d0d
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
21 changed files with 267 additions and 4 deletions

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

View File

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

View 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

View File

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

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

View 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

View 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

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

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

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

View 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

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

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

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

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

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

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

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

View 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

63
tests/data/test3201 Normal file
View File

@ -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
tests/data/test3202 Normal file
View File

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