From 5505354660135aa447c66ff361e0ea27557fd908 Mon Sep 17 00:00:00 2001 From: Teyut Date: Sun, 21 Sep 2025 02:59:18 +0200 Subject: [PATCH 1/5] Add support for the PROXY protocol When there is a proxy in front of tinyproxy, the latter does not see the actual IP address of the client. This is a problem when using tools like fail2ban that parse the tinyproxy log to find and ban clients that try to break the authentication using brute force. Some proxies like stunnel or HAProxy support the PROXY protocol to overcome that issue, so that the next proxy in the chain can tell what the IP address of the original client is. See https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt This commit adds support for clients that use the PROXY protocol to extract the real client IP address and report it in the logs. This is only implemented when the new ClientUsesProxyProtocol parameter is set to Yes. It should only be enabled when the client is trusted and known to be a proxy that support that protocol. --- docs/man5/tinyproxy.conf.txt.in | 13 +++++++ etc/tinyproxy.conf.in | 14 ++++++++ src/conf-tokens.c | 3 +- src/conf-tokens.gperf | 1 + src/conf-tokens.h | 1 + src/conf.c | 15 +++++++++ src/conf.h | 2 ++ src/reqs.c | 60 +++++++++++++++++++++++++++++++++ 8 files changed, 108 insertions(+), 1 deletion(-) diff --git a/docs/man5/tinyproxy.conf.txt.in b/docs/man5/tinyproxy.conf.txt.in index 9938ce19..121da353 100644 --- a/docs/man5/tinyproxy.conf.txt.in +++ b/docs/man5/tinyproxy.conf.txt.in @@ -412,6 +412,19 @@ put the outermost URL here (the address which the end user types into his/her browser). If this option is not set then no rewriting of redirects occurs. +=item B + +Set this option to `Yes` when the tinyproxy's client is a proxy that +uses the PROXY protocol, which inserts a line before the HTTP request +line to indicates the IP address of the real client. + +For instance, when using stunnel in front of tinyproxy and the +BasicAuth option is used, if the PROXY protocol is used, tinyproxy +will report the actual client IP address in the log instead of the IP +address of the stunnel server, so that the log can be parsed by +tools like fail2ban to ban clients that try to brute force the +authentication. + =back =head1 BUGS diff --git a/etc/tinyproxy.conf.in b/etc/tinyproxy.conf.in index b7d46a71..3d9ea162 100644 --- a/etc/tinyproxy.conf.in +++ b/etc/tinyproxy.conf.in @@ -327,3 +327,17 @@ ViaProxyName "tinyproxy" # If not set then no rewriting occurs. # #ReverseBaseURL "http://localhost:8888/" + +# +# The tinyproxy's client is a proxy that uses the PROXY protocol, which +# inserts a line before the HTTP request line to indicates the IP +# address of the real client. +# +# For instance, when using stunnel in front of tinyproxy and the +# BasicAuth option is used, if the PROXY protocol is used, tinyproxy +# will report the actual client IP address in the log instead of the IP +# address of the stunnel server, so that the log can be parsed by +# tools like fail2ban to ban clients that try to brute force the +# authentication. +# +#ClientUsesProxyProtocol Yes diff --git a/src/conf-tokens.c b/src/conf-tokens.c index 4463d230..3007190d 100644 --- a/src/conf-tokens.c +++ b/src/conf-tokens.c @@ -59,7 +59,8 @@ config_directive_find (register const char *str, register size_t len) {"basicauth", CD_basicauth}, {"basicauthrealm", CD_basicauthrealm}, {"addheader", CD_addheader}, - {"maxrequestsperchild", CD_maxrequestsperchild} + {"maxrequestsperchild", CD_maxrequestsperchild}, + {"clientusesproxyprotocol", CD_clientusesproxyprotocol} }; for(i=0;iclient_uses_proxyproto, line, &match[2]); + + if (r) { + return r; + } + + if (conf->client_uses_proxyproto) + log_message (LOG_INFO, "Expecting PROXY protocol header."); + return 0; +} + static HANDLE_FUNC (handle_defaulterrorfile) { return set_string_arg (&conf->errorpage_undef, line, &match[2]); diff --git a/src/conf.h b/src/conf.h index 0b25afa4..9650d250 100644 --- a/src/conf.h +++ b/src/conf.h @@ -77,6 +77,8 @@ struct config_s { unsigned int disable_viaheader; /* boolean */ + unsigned int client_uses_proxyproto; /* boolean */ + /* * Error page support. Map error numbers to file paths. */ diff --git a/src/reqs.c b/src/reqs.c index 52135a03..6156218a 100644 --- a/src/reqs.c +++ b/src/reqs.c @@ -93,6 +93,7 @@ static int read_request_line (struct conn_s *connptr) { ssize_t len; + unsigned int proxy_line = TRUE; retry: len = readline (connptr->client_fd, &connptr->request_line); @@ -117,6 +118,65 @@ static int read_request_line (struct conn_s *connptr) goto retry; } + if (proxy_line && config->client_uses_proxyproto) { + /* + * Parse the PROXY protocol line which looks like that: + * PROXY \r\n + * can be "TCP4" for IPv4/TCP or "TCP6" for IPv6/TCP + * and are IPv4 or IPv6 addresses + * and are TCP port numbers + */ + + char *p = connptr->request_line; + char *parts[6]; + int p_idx = 0; + + proxy_line = FALSE; + + if (strncmp (connptr->request_line, "PROXY ", 6)) { + log_message (LOG_WARNING, "Bad PROXY line: %s", + connptr->request_line); + + safefree (connptr->request_line); + return -1; + } + + /* Split the line in fields separated by a space character */ + while (p_idx < 6 && *p) { + parts[p_idx++] = p; + + while (*p != ' ' && *p) + ++p; + + if (*p) { + *p = 0; + ++p; + } + } + + if (!(p_idx == 6 && (!strcmp (parts[1], "TCP6") + || !strcmp (parts[1], "TCP4")))) { + /* Rebuild the PROXY line for logging */ + while (p_idx > 1) { + parts[--p_idx][-1] = ' '; + } + log_message (LOG_WARNING, "Bad PROXY line: %s", + connptr->request_line); + + safefree (connptr->request_line); + return -1; + } + + /* Use the source address given by the proxy */ + if (connptr->client_ip_addr) + safefree (connptr->client_ip_addr); + + connptr->client_ip_addr = safestrdup (parts[2]); + + safefree (connptr->request_line); + goto retry; + } + log_message (LOG_CONN, "Request (file descriptor %d): %s", connptr->client_fd, connptr->request_line); From 8629c2e58aa2bfc958619dc5cac8081e4b3c3963 Mon Sep 17 00:00:00 2001 From: Teyut Date: Sun, 21 Sep 2025 21:26:04 +0200 Subject: [PATCH 2/5] Parse the PROXY line asap Reading the PROXY line is the first thing done in handle_connection() so that the client IP address is updated before it is needed by the rest of the code (ACL, logging, etc.). The parsing implementation is now less generic and updates the sockaddr_union structure directly. --- src/reqs.c | 148 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 92 insertions(+), 56 deletions(-) diff --git a/src/reqs.c b/src/reqs.c index 6156218a..0413dfc7 100644 --- a/src/reqs.c +++ b/src/reqs.c @@ -86,93 +86,124 @@ ((len) > 0 && (header[0] == ' ' || header[0] == '\t')) /* - * Read in the first line from the client (the request line for HTTP - * connections. The request line is allocated from the heap, but it must - * be freed in another function. + * Read and parse the PROXY protocol v1 line, and update the client address. */ -static int read_request_line (struct conn_s *connptr) +static int read_proxy_line (struct conn_s *connptr, union sockaddr_union* addr) { + int result = -1; + char *line = NULL; ssize_t len; - unsigned int proxy_line = TRUE; + char *src_addr, *end; + int is_ipv6; + int ret; -retry: - len = readline (connptr->client_fd, &connptr->request_line); + len = readline (connptr->client_fd, &line); if (len <= 0) { log_message (LOG_ERR, - "read_request_line: Client (file descriptor: %d) " + "read_proxy_line: Client (file descriptor: %d) " "closed socket before read.", connptr->client_fd); - return -1; + goto cleanup; } /* * Strip the new line and carriage return from the string. */ - if (chomp (connptr->request_line, len) == len) { + if (chomp (line, len) == len) { /* * If the number of characters removed is the same as the - * length then it was a blank line. Free the buffer and - * try again (since we're looking for a request line.) + * length then it was a blank line. */ - safefree (connptr->request_line); - goto retry; + log_message (LOG_ERR, "Empty PROXY line found"); + + goto cleanup; } - if (proxy_line && config->client_uses_proxyproto) { - /* - * Parse the PROXY protocol line which looks like that: - * PROXY \r\n - * can be "TCP4" for IPv4/TCP or "TCP6" for IPv6/TCP - * and are IPv4 or IPv6 addresses - * and are TCP port numbers - */ + /* + * The PROXY line looks like that: + * PROXY \r\n + * can be "TCP4" for IPv4/TCP or "TCP6" for IPv6/TCP + * and are IPv4 or IPv6 addresses + * and are TCP port numbers* + */ + + /* Expect "PROXY TCP4 " or "PROXY TCP6 " */ + if (!(!strncmp (line, "PROXY TCP", 9) + && (line[9] == '4' || line[9] == '6') + && line[10] == ' ')) { + log_message (LOG_ERR, "Bad PROXY line: %s", line); - char *p = connptr->request_line; - char *parts[6]; - int p_idx = 0; + goto cleanup; + } - proxy_line = FALSE; + is_ipv6 = line[9] == '6' ? TRUE : FALSE; - if (strncmp (connptr->request_line, "PROXY ", 6)) { - log_message (LOG_WARNING, "Bad PROXY line: %s", - connptr->request_line); + /* Only extract the source address string */ + src_addr = line + 11; + end = strchr (src_addr, ' '); + *end = '\0'; - safefree (connptr->request_line); - return -1; + /* Update "addr" with the given address */ + if (is_ipv6) { + if (addr->v4.sin_family != AF_INET6) { + addr->v4.sin_family = AF_INET6; + /* Set unused IPv6 fields to 0 */ + addr->v6.sin6_flowinfo = 0; + addr->v6.sin6_scope_id = 0; + } + ret = inet_pton (AF_INET6, src_addr, &addr->v6.sin6_addr); + } else { + if (addr->v4.sin_family != AF_INET) { + addr->v4.sin_family = AF_INET; } + ret = inet_pton (AF_INET, src_addr, &addr->v4.sin_addr); + } - /* Split the line in fields separated by a space character */ - while (p_idx < 6 && *p) { - parts[p_idx++] = p; + if (ret != 1) { + log_message (LOG_ERR, + "Cannot convert address from PROXY line: %s", + src_addr); - while (*p != ' ' && *p) - ++p; + goto cleanup; + } - if (*p) { - *p = 0; - ++p; - } - } + result = 0; - if (!(p_idx == 6 && (!strcmp (parts[1], "TCP6") - || !strcmp (parts[1], "TCP4")))) { - /* Rebuild the PROXY line for logging */ - while (p_idx > 1) { - parts[--p_idx][-1] = ' '; - } - log_message (LOG_WARNING, "Bad PROXY line: %s", - connptr->request_line); +cleanup: - safefree (connptr->request_line); - return -1; - } + safefree (line); - /* Use the source address given by the proxy */ - if (connptr->client_ip_addr) - safefree (connptr->client_ip_addr); + return result; +} - connptr->client_ip_addr = safestrdup (parts[2]); +/* + * Read in the first line from the client (the request line for HTTP + * connections. The request line is allocated from the heap, but it must + * be freed in another function. + */ +static int read_request_line (struct conn_s *connptr) +{ + ssize_t len; + +retry: + len = readline (connptr->client_fd, &connptr->request_line); + if (len <= 0) { + log_message (LOG_ERR, + "read_request_line: Client (file descriptor: %d) " + "closed socket before read.", connptr->client_fd); + return -1; + } + + /* + * Strip the new line and carriage return from the string. + */ + if (chomp (connptr->request_line, len) == len) { + /* + * If the number of characters removed is the same as the + * length then it was a blank line. Free the buffer and + * try again (since we're looking for a request line.) + */ safefree (connptr->request_line); goto retry; } @@ -1655,6 +1686,11 @@ void handle_connection (struct conn_s *connptr, union sockaddr_union* addr) char sock_ipaddr[IP_LENGTH]; char peer_ipaddr[IP_LENGTH]; + if (config->client_uses_proxyproto && read_proxy_line (connptr, addr)) { + close (fd); + return; + } + getpeer_information (addr, peer_ipaddr, sizeof(peer_ipaddr)); if (config->bindsame) From 13d0ff5d3f75d05fb752dcdd17ff59aebc78499d Mon Sep 17 00:00:00 2001 From: Teyut Date: Sun, 21 Sep 2025 21:37:57 +0200 Subject: [PATCH 3/5] Indicate which PROXY version is supported --- docs/man5/tinyproxy.conf.txt.in | 3 ++- etc/tinyproxy.conf.in | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/man5/tinyproxy.conf.txt.in b/docs/man5/tinyproxy.conf.txt.in index 121da353..8007f1bb 100644 --- a/docs/man5/tinyproxy.conf.txt.in +++ b/docs/man5/tinyproxy.conf.txt.in @@ -416,7 +416,8 @@ no rewriting of redirects occurs. Set this option to `Yes` when the tinyproxy's client is a proxy that uses the PROXY protocol, which inserts a line before the HTTP request -line to indicates the IP address of the real client. +line to indicates the IP address of the real client. Only version 1 +(human-readable header format) is supported. For instance, when using stunnel in front of tinyproxy and the BasicAuth option is used, if the PROXY protocol is used, tinyproxy diff --git a/etc/tinyproxy.conf.in b/etc/tinyproxy.conf.in index 3d9ea162..25466e36 100644 --- a/etc/tinyproxy.conf.in +++ b/etc/tinyproxy.conf.in @@ -331,7 +331,8 @@ ViaProxyName "tinyproxy" # # The tinyproxy's client is a proxy that uses the PROXY protocol, which # inserts a line before the HTTP request line to indicates the IP -# address of the real client. +# address of the real client. Only version 1 (human-readable header +# format) is supported. # # For instance, when using stunnel in front of tinyproxy and the # BasicAuth option is used, if the PROXY protocol is used, tinyproxy From ec8f8d69d5e478b5812f53ec8d1b9921f8b55435 Mon Sep 17 00:00:00 2001 From: Teyut Date: Mon, 22 Sep 2025 16:08:46 +0200 Subject: [PATCH 4/5] Typo --- docs/man5/tinyproxy.conf.txt.in | 2 +- etc/tinyproxy.conf.in | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/man5/tinyproxy.conf.txt.in b/docs/man5/tinyproxy.conf.txt.in index 8007f1bb..8719d15f 100644 --- a/docs/man5/tinyproxy.conf.txt.in +++ b/docs/man5/tinyproxy.conf.txt.in @@ -416,7 +416,7 @@ no rewriting of redirects occurs. Set this option to `Yes` when the tinyproxy's client is a proxy that uses the PROXY protocol, which inserts a line before the HTTP request -line to indicates the IP address of the real client. Only version 1 +line to indicate the IP address of the real client. Only version 1 (human-readable header format) is supported. For instance, when using stunnel in front of tinyproxy and the diff --git a/etc/tinyproxy.conf.in b/etc/tinyproxy.conf.in index 25466e36..d15f9574 100644 --- a/etc/tinyproxy.conf.in +++ b/etc/tinyproxy.conf.in @@ -330,7 +330,7 @@ ViaProxyName "tinyproxy" # # The tinyproxy's client is a proxy that uses the PROXY protocol, which -# inserts a line before the HTTP request line to indicates the IP +# inserts a line before the HTTP request line to indicate the IP # address of the real client. Only version 1 (human-readable header # format) is supported. # From 0bed8e2be2557dcb0be04b74ecdb9d03cead4f59 Mon Sep 17 00:00:00 2001 From: Teyut Date: Fri, 26 Sep 2025 00:07:24 +0200 Subject: [PATCH 5/5] Simplify implementation of read_proxy_line() --- src/reqs.c | 41 +++++++++-------------------------------- 1 file changed, 9 insertions(+), 32 deletions(-) diff --git a/src/reqs.c b/src/reqs.c index 0413dfc7..1a701f20 100644 --- a/src/reqs.c +++ b/src/reqs.c @@ -94,8 +94,7 @@ static int read_proxy_line (struct conn_s *connptr, union sockaddr_union* addr) char *line = NULL; ssize_t len; char *src_addr, *end; - int is_ipv6; - int ret; + sa_family_t af; len = readline (connptr->client_fd, &line); if (len <= 0) { @@ -106,19 +105,6 @@ static int read_proxy_line (struct conn_s *connptr, union sockaddr_union* addr) goto cleanup; } - /* - * Strip the new line and carriage return from the string. - */ - if (chomp (line, len) == len) { - /* - * If the number of characters removed is the same as the - * length then it was a blank line. - */ - log_message (LOG_ERR, "Empty PROXY line found"); - - goto cleanup; - } - /* * The PROXY line looks like that: * PROXY \r\n @@ -131,35 +117,26 @@ static int read_proxy_line (struct conn_s *connptr, union sockaddr_union* addr) if (!(!strncmp (line, "PROXY TCP", 9) && (line[9] == '4' || line[9] == '6') && line[10] == ' ')) { +bad_syntax: log_message (LOG_ERR, "Bad PROXY line: %s", line); goto cleanup; } - is_ipv6 = line[9] == '6' ? TRUE : FALSE; + af = line[9] == '6' ? AF_INET6 : AF_INET; /* Only extract the source address string */ src_addr = line + 11; end = strchr (src_addr, ' '); + if (end == NULL) + goto bad_syntax; + *end = '\0'; /* Update "addr" with the given address */ - if (is_ipv6) { - if (addr->v4.sin_family != AF_INET6) { - addr->v4.sin_family = AF_INET6; - /* Set unused IPv6 fields to 0 */ - addr->v6.sin6_flowinfo = 0; - addr->v6.sin6_scope_id = 0; - } - ret = inet_pton (AF_INET6, src_addr, &addr->v6.sin6_addr); - } else { - if (addr->v4.sin_family != AF_INET) { - addr->v4.sin_family = AF_INET; - } - ret = inet_pton (AF_INET, src_addr, &addr->v4.sin_addr); - } - - if (ret != 1) { + memset (addr, 0, sizeof(*addr)); + addr->v4.sin_family = af; + if (inet_pton (af, src_addr, SOCKADDR_UNION_ADDRESS(addr)) != 1) { log_message (LOG_ERR, "Cannot convert address from PROXY line: %s", src_addr);