Transparent moved to main code (TransparentPlugin removed), BSD pf support added

This commit is contained in:
Vladimir Dubrovin 2026-08-26 14:34:44 +03:00
parent 85b753ce96
commit 88b3225bdf
19 changed files with 972 additions and 191 deletions

View File

@ -4,8 +4,20 @@
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
# Read version from RELEASE file # Read the version. A release branch carries RELEASE, a development branch
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/RELEASE" PROJECT_VERSION LIMIT_COUNT 1) # DEVEL, whose version may have a suffix that project() will not take.
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/RELEASE")
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/RELEASE" PROJECT_VERSION_FULL LIMIT_COUNT 1)
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/DEVEL")
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/DEVEL" PROJECT_VERSION_FULL LIMIT_COUNT 1)
else()
message(FATAL_ERROR "Neither RELEASE nor DEVEL found: cannot tell the version")
endif()
string(STRIP "${PROJECT_VERSION_FULL}" PROJECT_VERSION_FULL)
string(REGEX MATCH "^[0-9]+(\\.[0-9]+)*" PROJECT_VERSION "${PROJECT_VERSION_FULL}")
if(NOT PROJECT_VERSION)
message(FATAL_ERROR "No version number in '${PROJECT_VERSION_FULL}'")
endif()
project(3proxy project(3proxy
VERSION ${PROJECT_VERSION} VERSION ${PROJECT_VERSION}
@ -55,6 +67,7 @@ option(3PROXY_USE_SPLICE "Build Linux splice() support, slower than read/write f
option(3PROXY_USE_POLL "Use poll() instead of select() (Unix only)" ON) option(3PROXY_USE_POLL "Use poll() instead of select() (Unix only)" ON)
option(3PROXY_USE_WSAPOLL "Use WSAPoll instead of select() (Windows only)" ON) option(3PROXY_USE_WSAPOLL "Use WSAPoll instead of select() (Windows only)" ON)
option(3PROXY_USE_NETFILTER "Enable Linux netfilter support (Linux only)" ON) option(3PROXY_USE_NETFILTER "Enable Linux netfilter support (Linux only)" ON)
option(3PROXY_USE_TRANSPARENT "Build transparent proxying support (Linux and BSD only)" ON)
option(3PROXY_USE_UNIX_SOCKETS "Enable Unix domain socket support (Unix only)" ON) option(3PROXY_USE_UNIX_SOCKETS "Enable Unix domain socket support (Unix only)" ON)
option(3PROXY_USE_HTTPSRV "Build the HTTP server and the admin interface on top of it" ON) option(3PROXY_USE_HTTPSRV "Build the HTTP server and the admin interface on top of it" ON)
@ -193,7 +206,6 @@ elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(DEFAULT_PLUGINS set(DEFAULT_PLUGINS
StringsPlugin StringsPlugin
TrafficPlugin TrafficPlugin
TransparentPlugin
FilePlugin FilePlugin
) )
@ -215,7 +227,6 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "FreeBSD|Darwin|OpenBSD|NetBSD")
set(DEFAULT_PLUGINS set(DEFAULT_PLUGINS
StringsPlugin StringsPlugin
TrafficPlugin TrafficPlugin
TransparentPlugin
FilePlugin FilePlugin
) )
@ -232,7 +243,6 @@ else()
set(DEFAULT_PLUGINS set(DEFAULT_PLUGINS
StringsPlugin StringsPlugin
TrafficPlugin TrafficPlugin
TransparentPlugin
FilePlugin FilePlugin
) )
endif() endif()
@ -241,6 +251,25 @@ if(3PROXY_USE_HTTPSRV)
add_compile_definitions(WITH_HTTPSRV) add_compile_definitions(WITH_HTTPSRV)
endif() endif()
# Transparent proxying needs a redirection that leaves the original
# destination where 3proxy reads it: the kernel on Linux, the socket on the
# BSDs. That means netfilter, OpenBSD divert-to or FreeBSD ipfw fwd. NetBSD
# and macOS rewrite the destination instead and are left out.
if(3PROXY_USE_TRANSPARENT AND (CMAKE_SYSTEM_NAME STREQUAL "Linux"
OR CMAKE_SYSTEM_NAME MATCHES "FreeBSD|OpenBSD|NetBSD"))
add_compile_definitions(WITH_TRANSPARENT)
set(3PROXY_TRANSPARENT_BUILT ON)
# pf keeps the original destination in its state table, which is read
# through /dev/pf. macOS has the device but ships no header for it.
include(CheckIncludeFiles)
check_include_files("sys/types.h;sys/socket.h;net/if.h;net/pfvar.h" HAVE_PFVAR_H)
if(HAVE_PFVAR_H)
add_compile_definitions(WITH_PF)
endif()
else()
set(3PROXY_TRANSPARENT_BUILT OFF)
endif()
# Unix domain sockets off: NO_UN also undefines WITH_UN if it arrives from # Unix domain sockets off: NO_UN also undefines WITH_UN if it arrives from
# elsewhere, e.g. CFLAGS # elsewhere, e.g. CFLAGS
if(NOT 3PROXY_USE_UNIX_SOCKETS) if(NOT 3PROXY_USE_UNIX_SOCKETS)
@ -471,6 +500,10 @@ if(PCRE2_FOUND)
target_sources(3proxy PRIVATE src/pcre.c) target_sources(3proxy PRIVATE src/pcre.c)
endif() endif()
if(3PROXY_TRANSPARENT_BUILT)
target_sources(3proxy PRIVATE src/transparent.c)
endif()
target_include_directories(3proxy PRIVATE target_include_directories(3proxy PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src
${CMAKE_CURRENT_SOURCE_DIR}/src/libs ${CMAKE_CURRENT_SOURCE_DIR}/src/libs
@ -917,7 +950,7 @@ endif()
# Summary # Summary
message(STATUS "") message(STATUS "")
message(STATUS "3proxy configuration summary:") message(STATUS "3proxy configuration summary:")
message(STATUS " Version: ${PROJECT_VERSION}") message(STATUS " Version: ${PROJECT_VERSION_FULL}")
message(STATUS " Platform: ${CMAKE_SYSTEM_NAME}") message(STATUS " Platform: ${CMAKE_SYSTEM_NAME}")
message(STATUS " Compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}") message(STATUS " Compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}")
message(STATUS " Build type: ${CMAKE_BUILD_TYPE}") message(STATUS " Build type: ${CMAKE_BUILD_TYPE}")
@ -927,6 +960,7 @@ message(STATUS " BUILD_SHARED: ${3PROXY_BUILD_SHARED}")
message(STATUS " USE_WOLFSSL: ${3PROXY_USE_WOLFSSL}") message(STATUS " USE_WOLFSSL: ${3PROXY_USE_WOLFSSL}")
message(STATUS " USE_OPENSSL: ${3PROXY_USE_OPENSSL}") message(STATUS " USE_OPENSSL: ${3PROXY_USE_OPENSSL}")
message(STATUS " USE_PCRE2: ${3PROXY_USE_PCRE2}") message(STATUS " USE_PCRE2: ${3PROXY_USE_PCRE2}")
message(STATUS " TRANSPARENT: ${3PROXY_TRANSPARENT_BUILT}")
message(STATUS " USE_PAM: ${3PROXY_USE_PAM}") message(STATUS " USE_PAM: ${3PROXY_USE_PAM}")
message(STATUS " USE_ODBC: ${3PROXY_USE_ODBC}") message(STATUS " USE_ODBC: ${3PROXY_USE_ODBC}")
message(STATUS " USE_POLL: ${3PROXY_USE_POLL}") message(STATUS " USE_POLL: ${3PROXY_USE_POLL}")

View File

@ -44,7 +44,20 @@ AFTERCLEAN = (find . -type f -name "*.o" -delete && find src/ -type f -name "Mak
TYPECOMMAND = cat TYPECOMMAND = cat
COMPATLIBS = COMPATLIBS =
MAKEFILE = Makefile.FreeBSD MAKEFILE = Makefile.FreeBSD
PLUGINS ?= StringsPlugin TrafficPlugin TransparentPlugin FilePlugin PLUGINS ?= StringsPlugin TrafficPlugin FilePlugin
# Transparent proxying, built in. The destination of a redirected connection
# comes from pf where its headers are available, and from the socket where a
# redirection leaves it there (OpenBSD divert-to, FreeBSD ipfw fwd). macOS
# has /dev/pf but ships no pfvar.h, so only the socket route is built there
# and no macOS redirection leaves the address on the socket.
CFLAGS += -DWITH_TRANSPARENT
TRANSPARENT_OBJS = transparent$(OBJSUFFICS)
PF_CHECK ?= $(shell printf "\#include <sys/types.h>\\n\#include <sys/socket.h>\\n\#include <net/if.h>\\n\#include <net/pfvar.h>\\n int main(){return 0;}" | tr -d \\\\ | $(CC) -x c $(CFLAGS) -o testpf.o -c - 2>/dev/null && rm testpf.o && echo true||echo false)
ifeq ($(PF_CHECK), true)
CFLAGS += -DWITH_PF
endif
ifeq ($(STATIC), true) ifeq ($(STATIC), true)
LDFLAGS += -static LDFLAGS += -static
CFLAGS += -DNOPLUGINS -DNOSTDRESOLVE -DNOCRYPT CFLAGS += -DNOPLUGINS -DNOSTDRESOLVE -DNOCRYPT

View File

@ -50,8 +50,12 @@ MAKEFILE = Makefile.Linux
# PamAuth requires libpam, you may require pam-devel package to be installed # PamAuth requires libpam, you may require pam-devel package to be installed
# SSLPlugin requires -lcrypto -lssl # SSLPlugin requires -lcrypto -lssl
#LIBS = -lcrypto -lssl -ldl #LIBS = -lcrypto -lssl -ldl
#PLUGINS = SSLPlugin StringsPlugin TrafficPlugin PCREPlugin TransparentPlugin PamAuth #PLUGINS = StringsPlugin TrafficPlugin PamAuth LdapPlugin
PLUGINS ?= StringsPlugin TrafficPlugin TransparentPlugin FilePlugin PLUGINS ?= StringsPlugin TrafficPlugin FilePlugin
# Transparent proxying, built in: it needs the packet filter of the platform
CFLAGS += -DWITH_TRANSPARENT
TRANSPARENT_OBJS = transparent$(OBJSUFFICS)
ifeq ($(STATIC), true) ifeq ($(STATIC), true)
LDFLAGS += -static LDFLAGS += -static
CFLAGS += -DNOPLUGINS -DNOSTDRESOLVE -DNOCRYPT CFLAGS += -DNOPLUGINS -DNOSTDRESOLVE -DNOCRYPT

View File

@ -34,7 +34,7 @@ AFTERCLEAN = (find . -type f -name "*.o" -delete && find src/ -type f -name "Mak
TYPECOMMAND = cat TYPECOMMAND = cat
COMPATLIBS = COMPATLIBS =
MAKEFILE = Makefile.Solaris MAKEFILE = Makefile.Solaris
PLUGINS = StringsPlugin TrafficPlugin TransparentPlugin FilePlugin PLUGINS = StringsPlugin TrafficPlugin FilePlugin
WOLFSSL_CHECK = $(shell printf "\#include <wolfssl/options.h>\\n\#include <wolfssl/openssl/ssl.h>\\n int main(){return 0;}" | tr -d \\\\ | $(CC) -x c $(CFLAGS) -o testwssl.o - 2>/dev/null && $(CC) $(LDFLAGS) -o testwssl testwssl.o -lwolfssl 2>/dev/null && rm testwssl testwssl.o && echo true||echo false) WOLFSSL_CHECK = $(shell printf "\#include <wolfssl/options.h>\\n\#include <wolfssl/openssl/ssl.h>\\n int main(){return 0;}" | tr -d \\\\ | $(CC) -x c $(CFLAGS) -o testwssl.o - 2>/dev/null && $(CC) $(LDFLAGS) -o testwssl testwssl.o -lwolfssl 2>/dev/null && rm testwssl testwssl.o && echo true||echo false)
ifeq ($(WOLFSSL_CHECK), true) ifeq ($(WOLFSSL_CHECK), true)

View File

@ -46,7 +46,13 @@ AFTERCLEAN = (find . -type f -name "*.o" -delete && find src/ -type f -name "Mak
TYPECOMMAND = cat TYPECOMMAND = cat
COMPATLIBS = COMPATLIBS =
MAKEFILE = Makefile.unix MAKEFILE = Makefile.unix
PLUGINS ?= StringsPlugin TrafficPlugin TransparentPlugin FilePlugin PLUGINS ?= StringsPlugin TrafficPlugin FilePlugin
# Transparent proxying is not built here: this makefile is for the systems
# without a redirection 3proxy can read the original destination from. Where
# the platform has one - OpenBSD divert-to is the case that fits - uncomment:
#CFLAGS += -DWITH_TRANSPARENT
#TRANSPARENT_OBJS = transparent$(OBJSUFFICS)
ifeq ($(STATIC), true) ifeq ($(STATIC), true)
LDFLAGS += -static LDFLAGS += -static
CFLAGS += -DNOPLUGINS -DNOSTDRESOLVE -DNOCRYPT CFLAGS += -DNOPLUGINS -DNOSTDRESOLVE -DNOCRYPT

View File

@ -37,6 +37,7 @@
<li><a href="#SSLPLUGIN">How to set up TLS/SSL (https proxy, mTLS)</a></li> <li><a href="#SSLPLUGIN">How to set up TLS/SSL (https proxy, mTLS)</a></li>
<li><a href="#CERTIFICATES">How to create CA and certificates for SSL</a></li> <li><a href="#CERTIFICATES">How to create CA and certificates for SSL</a></li>
<li><a href="#PCRE">How to use PCRE filtering (regular expressions)</a></li> <li><a href="#PCRE">How to use PCRE filtering (regular expressions)</a></li>
<li><a href="#TRANSPARENT">How to proxy transparently</a></li>
<li><A HREF="#AUTH">How to limit service access</a> <li><A HREF="#AUTH">How to limit service access</a>
<li><A HREF="#USERS">How to create a user list</a> <li><A HREF="#USERS">How to create a user list</a>
<li><A HREF="#ACL">How to limit user access to resources</a> <li><A HREF="#ACL">How to limit user access to resources</a>
@ -981,6 +982,189 @@ openssl pkcs12 -export -out client.p12 -passout pass: \
openssl verify -x509_strict -CAfile ca.crt server.crt openssl verify -x509_strict -CAfile ca.crt server.crt
openssl verify -x509_strict -CAfile ca.crt client.crt openssl verify -x509_strict -CAfile ca.crt client.crt
</pre> </pre>
<li><a name="TRANSPARENT"><i>How to proxy transparently</i></a>
<p>
A transparent proxy serves clients that were never configured to use one. A
packet filter redirects their connections to 3proxy, and the
<b>transparent</b> command tells the service to take the destination from the
filter instead of from the request. Every other feature applies as usual:
access rules, parent proxies, limits and logging all see the real destination.
It works on Linux and on the BSDs. Since 1.0.1 it is part of the binary; before
that it was the separate TransparentPlugin, and the <b>plugin</b> line it
needed is no longer required.
</p>
<p>
The command supplies both the address and the port the client was trying to
reach, so a service can serve whatever was redirected to it rather than one
port with one target.
</p>
<p>
Without it a service has to get a destination from somewhere else: an HTTP
proxy falls back to the <b>Host</b> header, and a port mapper uses the address
it was configured with. Taking the destination from the filter is what makes
the other protocols work, and what makes the address authoritative rather than
something the client claimed.
</p>
<p>
<b>tlspr</b> is the clearest case. Nothing reaches it at all unless traffic is
redirected to it, or the clients resolve the names to it through DNS. With a
redirection it gets the address as well as the name from the handshake, and
that is what lets access rules be written with host names: the name from the
handshake is matched, and the connection still goes to the address the client
was going to. Without the address it would have to resolve the name itself,
which is a second lookup and a second answer.
</p>
<p>
A configuration for web and TLS traffic:
</p><pre>
log /var/log/3proxy.log D
auth iponly
allow *
&#35; the destination comes from the redirection for the services below
transparent
&#35; ordinary web traffic, redirected here from port 80
proxy -p3129 -e192.0.2.10
&#35; TLS, redirected here from port 443: tlspr would otherwise have only the
&#35; name in the handshake, and this gives it the address as well
tlspr -p3143 -e192.0.2.10
notransparent
</pre>
<p>
<b>-e</b> gives those services an address of their own to connect from. That
address is what the redirection rules exclude, and excluding it is what stops
the proxy's own connections from being redirected back into it. Without an
exclusion the connection 3proxy makes to the origin matches the same rule,
returns to 3proxy, and the traffic goes round until something gives out.
Running 3proxy as its own account and excluding that account works too, and is
the better choice when the machine has one address.
</p>
<p><b>Linux, iptables</b>. For traffic the machine forwards for others:
</p><pre>
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-ports 3129
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-ports 3143
</pre>
<p>
Traffic the machine generates itself passes through OUTPUT instead, where the
proxy's own connections have to be excluded:
</p><pre>
&#35; by the address the services connect from
iptables -t nat -A OUTPUT -p tcp --dport 80 ! -s 192.0.2.10 -j REDIRECT --to-ports 3129
&#35; or by the account 3proxy runs as
iptables -t nat -A OUTPUT -p tcp --dport 80 -m owner ! --uid-owner proxy3 \
-j REDIRECT --to-ports 3129
</pre>
<p><b>Linux, nftables</b> (Debian 11+, Ubuntu 22.04+, RHEL 8+, Fedora, Arch,
where nftables is what iptables is a front end for):
</p><pre>
table ip proxy3 {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iif "eth0" tcp dport 80 redirect to :3129
iif "eth0" tcp dport 443 redirect to :3143
}
chain output {
type nat hook output priority dstnat; policy accept;
meta skuid != "proxy3" tcp dport 80 redirect to :3129
meta skuid != "proxy3" tcp dport 443 redirect to :3143
}
}
</pre>
<p>
Load it with <b>nft -f</b>, and keep it across reboots in
<b>/etc/nftables.conf</b> (Debian, Ubuntu) or
<b>/etc/sysconfig/nftables.conf</b> (RHEL, Fedora). A table name cannot begin
with a digit, which is why the table above is not called 3proxy.
</p>
<p><b>Linux, firewalld</b> (RHEL, CentOS Stream, Fedora, openSUSE). Redirect an
incoming port on a zone:
</p><pre>
firewall-cmd --permanent --zone=internal --add-forward-port=port=80:proto=tcp:toport=3129
firewall-cmd --permanent --zone=internal --add-forward-port=port=443:proto=tcp:toport=3143
firewall-cmd --reload
</pre>
<p>
firewalld has no exclusion for the proxy's own traffic in that form, so put
that part in a direct rule:
</p><pre>
firewall-cmd --permanent --direct --add-rule ipv4 nat OUTPUT 0 \
-p tcp --dport 80 -m owner ! --uid-owner proxy3 -j REDIRECT --to-ports 3129
firewall-cmd --reload
</pre>
<p><b>Linux, ufw</b> (Ubuntu, Debian). ufw has no command for redirection;
add the rules to <b>/etc/ufw/before.rules</b>, above the <b>*filter</b> block:
</p><pre>
*nat
:PREROUTING ACCEPT [0:0]
-A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-ports 3129
-A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-ports 3143
COMMIT
</pre>
<p>
On Linux 3proxy asks the kernel where the connection was going. On the BSDs it
asks pf, which keeps the original destination in its state table, through
<b>/dev/pf</b> - so <b>rdr</b> rules work, and 3proxy has to be able to read
that device. Where a redirection leaves the destination on the socket instead,
that is used: OpenBSD <b>divert-to</b> and FreeBSD <b>ipfw fwd</b> both do.
</p>
<p>
The mechanism is chosen automatically, and <b>transparent</b> takes an
argument for the installations that need to pin it: <b>auto</b> (the default),
<b>netfilter</b>, <b>pf</b>, or <b>socket</b> for reading it off the socket. A
mode the build has no code for is refused, so a configuration written for
another platform fails where it is wrong instead of quietly doing something
else.
</p>
<p><b>FreeBSD, NetBSD and OpenBSD, pf</b>. Redirect in <b>/etc/pf.conf</b>,
excluding the address the proxy connects from:
</p><pre>
rdr pass on em0 inet proto tcp from ! 192.0.2.10 to any port 80 -&gt; 127.0.0.1 port 3129
rdr pass on em0 inet proto tcp from ! 192.0.2.10 to any port 443 -&gt; 127.0.0.1 port 3143
</pre>
<p>
Load with <b>pfctl -f /etc/pf.conf</b>. 3proxy looks the destination up in pf's
state table, so it needs to read <b>/dev/pf</b>: either run it as root, or give
its account access to that device.
</p>
<p><b>OpenBSD, divert-to</b> is an alternative which leaves the destination on
the socket, and needs no access to <b>/dev/pf</b>:
</p><pre>
pass in on em0 inet proto tcp to any port 80 divert-to 127.0.0.1 port 3129
pass in on em0 inet proto tcp to any port 443 divert-to 127.0.0.1 port 3143
</pre>
<p><b>FreeBSD, ipfw</b>. <b>fwd</b> delivers the connection locally without
rewriting it, which also leaves the destination on the socket:
</p><pre>
ipfw add fwd 127.0.0.1,3129 tcp from any to any 80 in recv em0
ipfw add fwd 127.0.0.1,3143 tcp from any to any 443 in recv em0
</pre>
<p><b>macOS</b> has <b>/dev/pf</b> but ships no header for it, so a macOS build
has no pf lookup, and macOS has neither <b>divert-to</b> nor ipfw to leave the
address on the socket. Transparent proxying is not usable there, even though
the commands exist in a macOS build.
</p>
<p>
Check the result by asking for a host through a redirected port and reading
the log: the request should appear with the address the client asked for,
which is what it would look like through a configured proxy.
</p>
<li><a name="PCRE"><i>How to use PCRE filtering (regular expressions)</i></a> <li><a name="PCRE"><i>How to use PCRE filtering (regular expressions)</i></a>
<p> <p>
Since version 0.9.7, PCRE (Perl Compatible Regular Expressions) filtering is built into Since version 0.9.7, PCRE (Perl Compatible Regular Expressions) filtering is built into

View File

@ -37,6 +37,7 @@
<li><a href="#SSLPLUGIN">Как настроить TLS/SSL (https прокси, mTLS)</a></li> <li><a href="#SSLPLUGIN">Как настроить TLS/SSL (https прокси, mTLS)</a></li>
<li><a href="#CERTIFICATES">Как создать CA и сертификаты для SSL</a></li> <li><a href="#CERTIFICATES">Как создать CA и сертификаты для SSL</a></li>
<li><a href="#PCRE">Как использовать PCRE-фильтрацию (регулярные выражения)</a></li> <li><a href="#PCRE">Как использовать PCRE-фильтрацию (регулярные выражения)</a></li>
<li><a href="#TRANSPARENT">Как сделать транспарентный прокси</a></li>
<li><a href="#AUTH">Как ограничить доступ к службе</a> <li><a href="#AUTH">Как ограничить доступ к службе</a>
<li><a href="#USERS">Как создать список пользователей</a> <li><a href="#USERS">Как создать список пользователей</a>
<li><a href="#ACL">Как ограничить доступ пользователей к ресурсам</a> <li><a href="#ACL">Как ограничить доступ пользователей к ресурсам</a>
@ -992,6 +993,176 @@ openssl verify -x509_strict -CAfile ca.crt server.crt
openssl verify -x509_strict -CAfile ca.crt client.crt openssl verify -x509_strict -CAfile ca.crt client.crt
</pre> </pre>
<li><a name="TRANSPARENT"><i>Как сделать транспарентный прокси</i></a>
<p>
Транспарентный прокси обслуживает клиентов, которые не настроены на работу
через прокси. Пакетный фильтр перенаправляет их соединения на 3proxy, а команда
<b>transparent</b> указывает сервису брать адрес назначения у фильтра, а не из
запроса. Всё остальное работает как обычно: правила доступа, родительские
прокси, ограничения и логирование видят настоящий адрес назначения. Работает в
Linux и BSD. С версии 1.0.1 встроено в бинарник, раньше это был отдельный
TransparentPlugin, и строка <b>plugin</b> больше не нужна.
</p>
<p>
Команда даёт и адрес, и порт назначения, поэтому сервис обслуживает всё, что
на него перенаправлено, а не один порт с одним адресом назначения.
</p>
<p>
Без неё сервис берёт адрес откуда-то ещё: HTTP-прокси - из заголовка
<b>Host</b>, порт-маппер - из своей конфигурации. Именно получение адреса от
фильтра позволяет работать с остальными протоколами и делает адрес
достоверным, а не заявленным клиентом.
</p>
<p>
Нагляднее всего это с <b>tlspr</b>. Без перенаправления трафика (или без
резолва имён на него через DNS) на него вообще ничего не попадёт. С
перенаправлением он получает и имя из TLS handshake, и адрес назначения -
именно это позволяет писать правила доступа по именам хостов: имя из handshake
проверяется в ACL, а соединение идёт на тот адрес, куда шёл клиент. Без адреса
пришлось бы резолвить имя самостоятельно, то есть делать ещё один запрос и
получать ещё один ответ.
</p>
<p>
Конфигурация для веб- и TLS-трафика:
</p><pre>
log /var/log/3proxy.log D
auth iponly
allow *
&#35; для сервисов ниже адрес назначения берётся из перенаправления
transparent
&#35; обычный веб-трафик, перенаправленный сюда с порта 80
proxy -p3129 -e192.0.2.10
&#35; TLS, перенаправленный сюда с порта 443: без этого у tlspr было бы только
&#35; имя из handshake, а так есть и адрес
tlspr -p3143 -e192.0.2.10
notransparent
</pre>
<p>
<b>-e</b> задаёт сервисам собственный адрес для исходящих соединений. Именно
этот адрес исключается в правилах перенаправления, и это исключение не даёт
соединениям самого прокси попасть обратно в него. Без исключения соединение,
которое 3proxy устанавливает к серверу назначения, попадает под то же правило,
возвращается в 3proxy, и трафик зацикливается. Можно вместо этого запускать
3proxy под отдельной учётной записью и исключать её - так лучше, если у машины
один адрес.
</p>
<p><b>Linux, iptables</b>. Для транзитного трафика:
</p><pre>
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-ports 3129
iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-ports 3143
</pre>
<p>
Трафик самой машины проходит через цепочку OUTPUT, где соединения прокси нужно
исключить:
</p><pre>
&#35; по адресу, с которого сервисы устанавливают соединения
iptables -t nat -A OUTPUT -p tcp --dport 80 ! -s 192.0.2.10 -j REDIRECT --to-ports 3129
&#35; либо по учётной записи, под которой работает 3proxy
iptables -t nat -A OUTPUT -p tcp --dport 80 -m owner ! --uid-owner proxy3 \
-j REDIRECT --to-ports 3129
</pre>
<p><b>Linux, nftables</b> (Debian 11+, Ubuntu 22.04+, RHEL 8+, Fedora, Arch):
</p><pre>
table ip proxy3 {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
iif "eth0" tcp dport 80 redirect to :3129
iif "eth0" tcp dport 443 redirect to :3143
}
chain output {
type nat hook output priority dstnat; policy accept;
meta skuid != "proxy3" tcp dport 80 redirect to :3129
meta skuid != "proxy3" tcp dport 443 redirect to :3143
}
}
</pre>
<p>
Загружается через <b>nft -f</b>, сохраняется в <b>/etc/nftables.conf</b>
(Debian, Ubuntu) или <b>/etc/sysconfig/nftables.conf</b> (RHEL, Fedora). Имя
таблицы не может начинаться с цифры, поэтому таблица называется не 3proxy.
</p>
<p><b>Linux, firewalld</b> (RHEL, CentOS Stream, Fedora, openSUSE):
</p><pre>
firewall-cmd --permanent --zone=internal --add-forward-port=port=80:proto=tcp:toport=3129
firewall-cmd --permanent --zone=internal --add-forward-port=port=443:proto=tcp:toport=3143
firewall-cmd --reload
</pre>
<p>
Исключение для трафика самого прокси в таком виде не задаётся, для него нужно
прямое правило:
</p><pre>
firewall-cmd --permanent --direct --add-rule ipv4 nat OUTPUT 0 \
-p tcp --dport 80 -m owner ! --uid-owner proxy3 -j REDIRECT --to-ports 3129
firewall-cmd --reload
</pre>
<p><b>Linux, ufw</b> (Ubuntu, Debian). В ufw нет команды для перенаправления,
правила добавляются в <b>/etc/ufw/before.rules</b> перед блоком <b>*filter</b>:
</p><pre>
*nat
:PREROUTING ACCEPT [0:0]
-A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-ports 3129
-A PREROUTING -i eth0 -p tcp --dport 443 -j REDIRECT --to-ports 3143
COMMIT
</pre>
<p>
В Linux 3proxy спрашивает у ядра, куда шло соединение. В BSD он спрашивает у pf,
который хранит исходный адрес назначения в таблице состояний, через
<b>/dev/pf</b> - поэтому работают правила <b>rdr</b>, и 3proxy должен иметь
доступ к этому устройству. Если перенаправление оставляет адрес на самом сокете,
используется он: так делают OpenBSD <b>divert-to</b> и FreeBSD <b>ipfw fwd</b>.
</p>
<p>
Механизм выбирается автоматически, а команда <b>transparent</b> принимает
аргумент для случаев, когда его надо зафиксировать: <b>auto</b> (по умолчанию),
<b>netfilter</b>, <b>pf</b> или <b>socket</b> для чтения адреса с сокета. Режим,
которого нет в сборке, отвергается, поэтому конфигурация, написанная для другой
платформы, не запустится вместо того, чтобы молча делать что-то другое.
</p>
<p><b>FreeBSD, NetBSD, OpenBSD, pf</b>. Перенаправление в <b>/etc/pf.conf</b> с
исключением адреса, с которого соединяется прокси:
</p><pre>
rdr pass on em0 inet proto tcp from ! 192.0.2.10 to any port 80 -&gt; 127.0.0.1 port 3129
rdr pass on em0 inet proto tcp from ! 192.0.2.10 to any port 443 -&gt; 127.0.0.1 port 3143
</pre>
<p>
Загружается через <b>pfctl -f /etc/pf.conf</b>. 3proxy ищет адрес назначения в
таблице состояний pf, поэтому ему нужен доступ на чтение к <b>/dev/pf</b>: либо
запуск от root, либо права на устройство для его учётной записи.
</p>
<p><b>OpenBSD, divert-to</b> - альтернатива, оставляющая адрес на сокете, доступ
к <b>/dev/pf</b> при этом не нужен:
</p><pre>
pass in on em0 inet proto tcp to any port 80 divert-to 127.0.0.1 port 3129
pass in on em0 inet proto tcp to any port 443 divert-to 127.0.0.1 port 3143
</pre>
<p><b>FreeBSD, ipfw</b>. <b>fwd</b> доставляет соединение локально, не переписывая
его, и адрес тоже остаётся на сокете:
</p><pre>
ipfw add fwd 127.0.0.1,3129 tcp from any to any 80 in recv em0
ipfw add fwd 127.0.0.1,3143 tcp from any to any 443 in recv em0
</pre>
<p><b>macOS</b>: <b>/dev/pf</b> есть, но заголовочных файлов для него нет,
поэтому в сборке под macOS нет обращения к pf, а ни <b>divert-to</b>, ни ipfw в
macOS нет. Транспарентное проксирование там неприменимо, хотя команды в сборке
присутствуют.
</p>
<li><a name="PCRE"><i>Как использовать PCRE-фильтрацию (регулярные выражения)</i></a> <li><a name="PCRE"><i>Как использовать PCRE-фильтрацию (регулярные выражения)</i></a>
<p> <p>
Начиная с версии 0.9.7 фильтрация PCRE встроена в 3proxy при компиляции с поддержкой Начиная с версии 0.9.7 фильтрация PCRE встроена в 3proxy при компиляции с поддержкой

View File

@ -1,31 +1,56 @@
<h3>3proxy TransparentPlugin (Linux/BSD only)</h3> <h3>3proxy transparent proxying (Linux/BSD only)</h3>
This plugin can turn 3proxy into a transparent proxy for virtually any TCP-based protocol Transparent proxying is part of 3proxy itself since 1.0.1. It was the separate
and use all 3proxy features - redirections, parent proxies, ACLs, traffic limitations, TransparentPlugin before that, and the <b>plugin</b> line that used to load it
etc. The TransparentPlugin takes the destination IP:port from Linux and uses this is no longer needed: the <b>transparent</b> and <b>notransparent</b> commands
information as the target IP in the proxy. An example usage: are always available on the platforms that can redirect a connection.
<p>
It turns 3proxy into a transparent proxy for virtually any TCP-based protocol,
with the rest of 3proxy applying as usual - redirections, parent proxies, ACLs,
traffic limitations and logging. The destination IP and port come from the
packet filter that redirected the connection, and are used as the target of the
proxied connection.
</p>
<pre> <pre>
plugin /path/to/TransparentPlugin.ld.so transparent_plugin
log /path/to/log log /path/to/log
auth iponly auth iponly
allow * * * 80 allow * * * 80
parent 1000 http 0.0.0.0 0 parent 1000 http 0.0.0.0 0
allow * allow *
parent 1000 socks5 SOCKS5_IP SOCKS5_PORT USER PASSWORD parent 1000 socks5 SOCKS5_IP SOCKS5_PORT USER PASSWORD
transparent transparent
tcppm -iLOCAL_IP 12345 127.0.0.1 11111 tcppm -eLOCAL_IP 12345 127.0.0.1 11111
notransparent notransparent
proxy proxy
</pre> </pre>
Now, any TCP traffic transparently redirected to port 12345 will be routed via
the parent SOCKSv5 proxy and logged; all URLs for web requests are visible in logs.
The parameters '127.0.0.1 11111' in this case are not used and are overwritten by
the destination IP:port for each transparent connection.
<h4>Download:</h4> <p>
<ul> Any TCP traffic redirected to port 12345 is routed through the parent SOCKSv5
<li>Plugin is included in 3proxy 0.8 proxy and logged, with the URLs of web requests visible in the log. The
</li></ul> '127.0.0.1 11111' arguments are not used in that case: they are replaced by the
destination the client was trying to reach.
</p>
<p>
The destination is looked up in pf on the BSDs, through <b>/dev/pf</b>, and
asked of the kernel on Linux; a redirection that leaves the address on the
socket is used where there is one. <b>transparent</b> takes an optional
<b>auto</b>, <b>netfilter</b>, <b>pf</b> or <b>socket</b> to pin that choice.
</p>
<p>
The redirection rules must not match the connections 3proxy itself makes, or
the traffic returns to the proxy and loops. Give the service an address to
connect from with <b>-e</b> and exclude it in the rules, or run 3proxy as its
own account and exclude that account.
</p>
<p>
Redirection rules for iptables, nftables, firewalld, ufw and pf are in
<a href="../howtoe.html#TRANSPARENT">How to proxy transparently</a>, and the
commands are described in 3proxy.cfg(5).
</p>
&copy; Vladimir Dubrovin, License: BSD style &copy; Vladimir Dubrovin, License: BSD style

View File

@ -1,33 +1,56 @@
<h3>Плагин TransparentPlugin 3proxy (только для Linux/BSD)</h3> <h3>Транспарентное проксирование 3proxy (только для Linux/BSD)</h3>
Плагин превращает 3proxy в транспарентный прокси для практически любых TCP-соединений Начиная с 1.0.1 транспарентное проксирование встроено в 3proxy. Раньше это был
и позволяет прозрачно для клиентов использовать весь фунционал прокси - редиректоры, отдельный TransparentPlugin, и строка <b>plugin</b>, которой он загружался,
родительские прокси, ACLи, ограничения трафика. TransparentPlugin получает IP:port больше не нужна: команды <b>transparent</b> и <b>notransparent</b> доступны
назначения от Linux и использует эту информацию в качестве конечного адреса назначения. всегда на тех платформах, где соединение можно перенаправить.
<br>
Пример использования: <p>
3proxy становится транспарентным прокси практически для любых TCP-соединений,
причём весь остальной функционал работает как обычно - редиректоры,
родительские прокси, ACLи, ограничения трафика и логирование. IP и порт
назначения берутся у пакетного фильтра, перенаправившего соединение, и
используются как адрес назначения проксируемого соединения.
</p>
<pre> <pre>
plugin /path/to/TransparentPlugin.ld.so transparent_plugin
log /path/to/log log /path/to/log
auth iponly auth iponly
allow * * * 80 allow * * * 80
parent 1000 http 0.0.0.0 0 parent 1000 http 0.0.0.0 0
allow * allow *
parent 1000 socks5 SOCKS5_IP SOCKS5_PORT USER PASSWORD parent 1000 socks5 SOCKS5_IP SOCKS5_PORT USER PASSWORD
transparent transparent
tcppm -iLOCAL_IP 12345 127.0.0.1 11111 tcppm -eLOCAL_IP 12345 127.0.0.1 11111
notransparent notransparent
proxy proxy
</pre> </pre>
Теперь любые TCP-соединения транспарентно перенаправленные в локальный порт 12345
будут прологгированы и перенаправлены в родительский SOCKSv5 proxy, при этом для <p>
HTTP-запросов по порту TCP/80 будут видны параметры HTTP-запроса. Любой TCP-трафик, перенаправленный на порт 12345, пойдёт через родительский
Параметры '127.0.0.1 11111' в данном случае не оказывают влияния, т.к. SOCKSv5 прокси и будет залогирован, URL веб-запросов видны в логе. Аргументы
будут перезаписываться IP и портом назначения для каждого TCP-соединения соответственно. '127.0.0.1 11111' в этом случае не используются: они заменяются адресом, к
<h4>Загрузить:</h4> которому обращался клиент.
<ul> </p>
<li>Плагин включен в дистрибутив 3proxy 0.8
</li></ul> <p>
В BSD адрес назначения ищется в pf через <b>/dev/pf</b>, в Linux запрашивается у
ядра; если перенаправление оставляет адрес на сокете, используется он. Команда
<b>transparent</b> принимает необязательный аргумент <b>auto</b>,
<b>netfilter</b>, <b>pf</b> или <b>socket</b>, чтобы зафиксировать выбор.
</p>
<p>
Правила перенаправления не должны попадать на соединения, которые устанавливает
сам 3proxy, иначе трафик возвращается в прокси и зацикливается. Задайте сервису
адрес для исходящих соединений через <b>-e</b> и исключите его в правилах, либо
запускайте 3proxy под отдельной учётной записью и исключайте её.
</p>
<p>
Правила перенаправления для iptables, nftables, firewalld, ufw и pf приведены в
<a href="../howtor.html#TRANSPARENT">описании транспарентного проксирования</a>,
команды описаны в 3proxy.cfg(5).
</p>
&copy; Vladimir Dubrovin, License: BSD style &copy; Vladimir Dubrovin, License: BSD style

View File

@ -1192,6 +1192,54 @@ the format:
Note: double quotes are required because the password contains a $ sign. Note: double quotes are required because the password contains a $ sign.
.br .br
.BR transparent
\fI[auto|netfilter|pf|socket]\fR
.br
Take the destination of a connection, both address and port, from the packet
filter that redirected it, instead of from the request. It applies to services declared after it, and
\fBnotransparent\fR turns it off again for the services after that. Built into the
binary since 1.0.1, and previously the separate TransparentPlugin.
.br
On Linux the kernel is asked, so \fBiptables\fR or \fBnftables\fR
redirection is enough. On the BSDs pf is asked through \fB/dev/pf\fR, which
3proxy must be able to read, so \fBrdr\fR rules work; a redirection that
leaves the destination on the socket is used where there is one, as
\fBdivert-to\fR on OpenBSD and \fBipfw fwd\fR on FreeBSD do. macOS ships no
header for pf and has neither of those, so the commands exist in a macOS build
but cannot be used.
.br
The mechanism is chosen automatically. The optional argument pins it for an
installation that has more than one: \fBauto\fR is the default,
\fBnetfilter\fR asks the Linux kernel, \fBpf\fR looks the connection up in
the packet filter, and \fBsocket\fR reads the address off the socket. A mode
the build has no code for is refused rather than ignored.
.br
A connection that reaches a \fBsocket\fR mode service without having been
redirected is refused: its destination is the address the service listens on,
and using that would send the service to itself.
.br
A redirected connection carries no destination of its own, so without this the
service uses whatever it would use otherwise: the \fBHost\fR header for an
HTTP request, or the address a port mapper was configured with. \fBtlspr\fR
receives nothing at all unless traffic is redirected to it or the clients
resolve names to it, and with a redirection it has the address as well as the
name from the handshake, which is what allows access rules to be written with
host names. With it, every service reaches the address the
client was trying to reach, and access rules, parents, limits and logging apply
to it as usual.
.br
The redirection rules must not match the connections the proxy itself makes to
those destinations, or the traffic returns to the proxy and loops. Give the
service an outgoing address with \fB-e\fR and exclude that address in the rules,
or run 3proxy as its own user and exclude that user. See the
.B TRANSPARENT PROXYING
section of the documentation for rules per platform.
.br
.BR notransparent
.br
Stop taking the destination from the packet filter for the services declared
after it.
.B flush .B flush
.br .br
empty the active access list. The access list must be flushed every time you create a empty the active access list. The access list must be flushed every time you create a

View File

@ -13,6 +13,9 @@ void ssl_install(void);
#ifdef WITH_PCRE #ifdef WITH_PCRE
void pcre_install(void); void pcre_install(void);
#endif #endif
#ifdef WITH_TRANSPARENT
void transparent_install(void);
#endif
#ifndef _WIN32 #ifndef _WIN32
#include <sys/resource.h> #include <sys/resource.h>
#ifndef NOPLUGINS #ifndef NOPLUGINS
@ -529,6 +532,9 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int
#ifdef WITH_PCRE #ifdef WITH_PCRE
pcre_install(); pcre_install();
#endif #endif
#ifdef WITH_TRANSPARENT
transparent_install();
#endif
freeconf(&conf); freeconf(&conf);
initcommands(); initcommands();

View File

@ -116,6 +116,9 @@ srvsocks$(OBJSUFFICS): socks.c proxy.h structures.h
srvwebadmin$(OBJSUFFICS): webadmin.c proxy.h structures.h srvwebadmin$(OBJSUFFICS): webadmin.c proxy.h structures.h
$(CC) $(COUT)srvwebadmin$(OBJSUFFICS) $(CFLAGS) webadmin.c $(CC) $(COUT)srvwebadmin$(OBJSUFFICS) $(CFLAGS) webadmin.c
transparent$(OBJSUFFICS): transparent.c proxy.h structures.h
$(CC) $(COUT)transparent$(OBJSUFFICS) $(CFLAGS) transparent.c
srvhttpsrv$(OBJSUFFICS): httpsrv.c proxy.h structures.h srvhttpsrv$(OBJSUFFICS): httpsrv.c proxy.h structures.h
$(CC) $(COUT)srvhttpsrv$(OBJSUFFICS) $(CFLAGS) httpsrv.c $(CC) $(COUT)srvhttpsrv$(OBJSUFFICS) $(CFLAGS) httpsrv.c
@ -191,6 +194,6 @@ ssl$(OBJSUFFICS): ssl.c structures.h proxy.h ssl.h
pcre$(OBJSUFFICS): pcre.c structures.h pcre$(OBJSUFFICS): pcre.c structures.h
$(CC) $(COUT)pcre$(OBJSUFFICS) $(CFLAGS) $(DEFINEOPTION)WITH_PCRE pcre.c $(CC) $(COUT)pcre$(OBJSUFFICS) $(CFLAGS) $(DEFINEOPTION)WITH_PCRE pcre.c
$(BUILDDIR)3proxy$(EXESUFFICS): 3proxy$(OBJSUFFICS) mainfunc$(OBJSUFFICS) srvproxy$(OBJSUFFICS) srvpop3p$(OBJSUFFICS) srvimapp$(OBJSUFFICS) srvsmtpp$(OBJSUFFICS) srvftppr$(OBJSUFFICS) srvsocks$(OBJSUFFICS) srvtcppm$(OBJSUFFICS) srvtlspr$(OBJSUFFICS) srvauto$(OBJSUFFICS) srvudppm$(OBJSUFFICS) sockmap$(OBJSUFFICS) udpsockmap$(OBJSUFFICS) sockgetchar$(OBJSUFFICS) common$(OBJSUFFICS) auth$(OBJSUFFICS) acl$(OBJSUFFICS) limiter$(OBJSUFFICS) redirect$(OBJSUFFICS) authradius$(OBJSUFFICS) hash$(OBJSUFFICS) hashtables$(OBJSUFFICS) resolve$(OBJSUFFICS) sql$(OBJSUFFICS) conf$(OBJSUFFICS) log$(OBJSUFFICS) datatypes$(OBJSUFFICS) md4$(OBJSUFFICS) md5$(OBJSUFFICS) blake2$(OBJSUFFICS) 3proxy_crypt$(OBJSUFFICS) base64$(OBJSUFFICS) ftp$(OBJSUFFICS) stringtable$(OBJSUFFICS) $(HTTPSRV_OBJS) srvdnspr$(OBJSUFFICS) plugins$(OBJSUFFICS) mdhash$(OBJSUFFICS) $(SSL_OBJS) $(PCRE_OBJS) $(COMPATLIBS) $(VERSIONDEP) $(BUILDDIR)3proxy$(EXESUFFICS): 3proxy$(OBJSUFFICS) mainfunc$(OBJSUFFICS) srvproxy$(OBJSUFFICS) srvpop3p$(OBJSUFFICS) srvimapp$(OBJSUFFICS) srvsmtpp$(OBJSUFFICS) srvftppr$(OBJSUFFICS) srvsocks$(OBJSUFFICS) srvtcppm$(OBJSUFFICS) srvtlspr$(OBJSUFFICS) srvauto$(OBJSUFFICS) srvudppm$(OBJSUFFICS) sockmap$(OBJSUFFICS) udpsockmap$(OBJSUFFICS) sockgetchar$(OBJSUFFICS) common$(OBJSUFFICS) auth$(OBJSUFFICS) acl$(OBJSUFFICS) limiter$(OBJSUFFICS) redirect$(OBJSUFFICS) authradius$(OBJSUFFICS) hash$(OBJSUFFICS) hashtables$(OBJSUFFICS) resolve$(OBJSUFFICS) sql$(OBJSUFFICS) conf$(OBJSUFFICS) log$(OBJSUFFICS) datatypes$(OBJSUFFICS) md4$(OBJSUFFICS) md5$(OBJSUFFICS) blake2$(OBJSUFFICS) 3proxy_crypt$(OBJSUFFICS) base64$(OBJSUFFICS) ftp$(OBJSUFFICS) stringtable$(OBJSUFFICS) $(HTTPSRV_OBJS) srvdnspr$(OBJSUFFICS) plugins$(OBJSUFFICS) mdhash$(OBJSUFFICS) $(SSL_OBJS) $(PCRE_OBJS) $(TRANSPARENT_OBJS) $(COMPATLIBS) $(VERSIONDEP)
$(LN) $(LNOUT)$(BUILDDIR)3proxy$(EXESUFFICS) $(LDFLAGS) $(VERFILE) 3proxy$(OBJSUFFICS) mainfunc$(OBJSUFFICS) auth$(OBJSUFFICS) acl$(OBJSUFFICS) limiter$(OBJSUFFICS) redirect$(OBJSUFFICS) authradius$(OBJSUFFICS) hash$(OBJSUFFICS) hashtables$(OBJSUFFICS) resolve$(OBJSUFFICS) sql$(OBJSUFFICS) conf$(OBJSUFFICS) datatypes$(OBJSUFFICS) srvauto$(OBJSUFFICS) srvproxy$(OBJSUFFICS) srvpop3p$(OBJSUFFICS) srvimapp$(OBJSUFFICS) srvsmtpp$(OBJSUFFICS) srvftppr$(OBJSUFFICS) srvsocks$(OBJSUFFICS) srvtcppm$(OBJSUFFICS) srvtlspr$(OBJSUFFICS) srvudppm$(OBJSUFFICS) sockmap$(OBJSUFFICS) udpsockmap$(OBJSUFFICS) sockgetchar$(OBJSUFFICS) common$(OBJSUFFICS) log$(OBJSUFFICS) 3proxy_crypt$(OBJSUFFICS) md4$(OBJSUFFICS) md5$(OBJSUFFICS) blake2$(OBJSUFFICS) base64$(OBJSUFFICS) ftp$(OBJSUFFICS) stringtable$(OBJSUFFICS) $(HTTPSRV_OBJS) srvdnspr$(OBJSUFFICS) plugins$(OBJSUFFICS) mdhash$(OBJSUFFICS) $(SSL_OBJS) $(PCRE_OBJS) $(COMPATLIBS) $(LIBS) $(PCRE_LIBS) $(LN) $(LNOUT)$(BUILDDIR)3proxy$(EXESUFFICS) $(LDFLAGS) $(VERFILE) 3proxy$(OBJSUFFICS) mainfunc$(OBJSUFFICS) auth$(OBJSUFFICS) acl$(OBJSUFFICS) limiter$(OBJSUFFICS) redirect$(OBJSUFFICS) authradius$(OBJSUFFICS) hash$(OBJSUFFICS) hashtables$(OBJSUFFICS) resolve$(OBJSUFFICS) sql$(OBJSUFFICS) conf$(OBJSUFFICS) datatypes$(OBJSUFFICS) srvauto$(OBJSUFFICS) srvproxy$(OBJSUFFICS) srvpop3p$(OBJSUFFICS) srvimapp$(OBJSUFFICS) srvsmtpp$(OBJSUFFICS) srvftppr$(OBJSUFFICS) srvsocks$(OBJSUFFICS) srvtcppm$(OBJSUFFICS) srvtlspr$(OBJSUFFICS) srvudppm$(OBJSUFFICS) sockmap$(OBJSUFFICS) udpsockmap$(OBJSUFFICS) sockgetchar$(OBJSUFFICS) common$(OBJSUFFICS) log$(OBJSUFFICS) 3proxy_crypt$(OBJSUFFICS) md4$(OBJSUFFICS) md5$(OBJSUFFICS) blake2$(OBJSUFFICS) base64$(OBJSUFFICS) ftp$(OBJSUFFICS) stringtable$(OBJSUFFICS) $(HTTPSRV_OBJS) srvdnspr$(OBJSUFFICS) plugins$(OBJSUFFICS) mdhash$(OBJSUFFICS) $(SSL_OBJS) $(PCRE_OBJS) $(TRANSPARENT_OBJS) $(COMPATLIBS) $(LIBS) $(PCRE_LIBS)

View File

@ -18,6 +18,9 @@ void ssl_install(void);
#ifdef WITH_PCRE #ifdef WITH_PCRE
void pcre_install(void); void pcre_install(void);
#endif #endif
#ifdef WITH_TRANSPARENT
void transparent_install(void);
#endif
#ifndef _WIN32 #ifndef _WIN32
#include <sys/resource.h> #include <sys/resource.h>
#include <pwd.h> #include <pwd.h>
@ -1793,6 +1796,10 @@ int h_server_verify(int argc, unsigned char **argv);
int h_no_server_verify(int argc, unsigned char **argv); int h_no_server_verify(int argc, unsigned char **argv);
int h_client_mode(int argc, unsigned char **argv); int h_client_mode(int argc, unsigned char **argv);
#endif #endif
#ifdef WITH_TRANSPARENT
int h_transparent(int argc, unsigned char **argv);
int h_notransparent(int argc, unsigned char **argv);
#endif
#ifdef WITH_PCRE #ifdef WITH_PCRE
int h_pcre(int argc, unsigned char **argv); int h_pcre(int argc, unsigned char **argv);
int h_pcre_rewrite(int argc, unsigned char **argv); int h_pcre_rewrite(int argc, unsigned char **argv);
@ -1922,6 +1929,10 @@ struct commands commandhandlers[]={
{NULL, "ssl_client_mode", h_client_mode, 1, 2}, {NULL, "ssl_client_mode", h_client_mode, 1, 2},
{NULL, "ssl_certcache", h_certcache, 2, 2}, {NULL, "ssl_certcache", h_certcache, 2, 2},
#endif #endif
#ifdef WITH_TRANSPARENT
{NULL, "transparent", h_transparent, 1, 2},
{NULL, "notransparent", h_notransparent, 1, 1},
#endif
#ifdef WITH_PCRE #ifdef WITH_PCRE
{NULL, "pcre", h_pcre, 4, 0}, {NULL, "pcre", h_pcre, 4, 0},
{NULL, "pcre_rewrite", h_pcre_rewrite, 5, 0}, {NULL, "pcre_rewrite", h_pcre_rewrite, 5, 0},
@ -2212,6 +2223,9 @@ int reload (void){
#endif #endif
#ifdef WITH_PCRE #ifdef WITH_PCRE
pcre_install(); pcre_install();
#endif
#ifdef WITH_TRANSPARENT
transparent_install();
#endif #endif
conf.paused++; conf.paused++;
freeconf(&conf); freeconf(&conf);

View File

@ -1,6 +0,0 @@
# TransparentPlugin
# Works on Linux (with netfilter), BSD and macOS (without netfilter support)
add_3proxy_plugin(TransparentPlugin
SOURCES transparent_plugin.c
)

View File

@ -1 +0,0 @@
include Makefile.var

View File

@ -1,10 +0,0 @@
all: $(BUILDDIR)TransparentPlugin$(DLSUFFICS)
transparent_plugin$(OBJSUFFICS): transparent_plugin.c
$(CC) $(CFLAGS) $(DCFLAGS) transparent_plugin.c
$(BUILDDIR)TransparentPlugin$(DLSUFFICS): transparent_plugin$(OBJSUFFICS)
$(LN) $(LNOUT)../../$(BUILDDIR)TransparentPlugin$(DLSUFFICS) $(LDFLAGS) $(DLFLAGS) transparent_plugin$(OBJSUFFICS)

View File

@ -1,128 +0,0 @@
/*
3APA3A simplest proxy server
(c) 2002-2026 by Vladimir Dubrovin <vlad@3proxy.org>
please read License Agreement
*/
#ifdef WITH_NETFILTER
#include <sys/utsname.h>
#endif
#include "../../structures.h"
#include "../../proxy.h"
#ifdef WITH_NETFILTER
#include <sys/types.h>
#include <sys/socket.h>
#include <limits.h>
#include <linux/netfilter_ipv4.h>
#endif
#ifdef __cplusplus
extern "C" {
#endif
static struct pluginlink * pl;
static int transparent_loaded = 0;
static void* transparent_filter_open(void * idata, struct srvparam * param){
return idata;
}
static FILTER_ACTION transparent_filter_client(void *fo, struct clientparam * param, void** fc){
char addrbuf[64];
#ifdef WITH_NETFILTER
socklen_t len;
len = sizeof(param->req);
#ifdef SO_ORIGINAL_DST
if(getsockopt(param->clisock,
#ifndef NOIPV6
#ifdef SOL_IPV6
*SAFAMILY(&param->sincr) == AF_INET6?SOL_IPV6:
#endif
#endif
SOL_IP, SO_ORIGINAL_DST,(struct sockaddr *) &param->req, &len) || !memcmp((char *)SAADDR(&param->req), "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", SAADDRLEN(&param->req))){
return PASS;
}
#else
#error No SO_ORIGINAL_DST defined
param->srv->logfunc(param, (unsigned char *)"transparent_plugin: No SO_ORIGINAL_DST defined");
return REJECT;
#endif
#else
if(*SAFAMILY(&param->sincl) == AF_INET || *SAFAMILY(&param->sincl) == AF_INET6){
param->req = param->sincl;
param->sincl = param->srv->intsa;
}
#endif
pl->myinet_ntop(*SAFAMILY(&param->req), SAADDR(&param->req), (char *)addrbuf, sizeof(addrbuf));
if(param->hostname) pl->freefunc(param->hostname);
param->hostname = (unsigned char *)pl->strdupfunc(addrbuf);
param->sinsr = param->req;
return PASS;
}
static void transparent_filter_clear(void *fo){
}
static void transparent_filter_close(void *fo){
}
static struct filter transparent_filter = {
NULL,
"Transparent filter",
"Transparent filter",
transparent_filter_open,
transparent_filter_client,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
transparent_filter_clear,
transparent_filter_close
};
static int h_transparent(int argc, unsigned char **argv){
transparent_filter.filter_open = transparent_filter_open;
return 0;
}
static int h_notransparent(int argc, unsigned char **argv){
transparent_filter.filter_open = NULL;
return 0;
}
static struct commands transparent_commandhandlers[] = {
{transparent_commandhandlers+1, "transparent", h_transparent, 1, 1},
{NULL, "notransparent", h_notransparent, 1, 1}
};
#ifdef WATCOM
#pragma aux transparent_plugin "*" parm caller [ ] value struct float struct routine [eax] modify [eax ecx edx]
#undef PLUGINCALL
#define PLUGINCALL
#endif
PLUGINAPI int PLUGINCALL transparent_plugin (struct pluginlink * pluginlink,
int argc, char** argv){
pl = pluginlink;
if(!transparent_loaded){
transparent_loaded = 1;
transparent_filter.next = pl->conf->filters;
pl->conf->filters = &transparent_filter;
transparent_commandhandlers[1].next = pl->commandhandlers->next;
pl->commandhandlers->next = transparent_commandhandlers;
}
return 0;
}
#ifdef __cplusplus
}
#endif

240
src/transparent.c Normal file
View File

@ -0,0 +1,240 @@
/*
3APA3A simplest proxy server
(c) 2002-2026 by Vladimir Dubrovin <vlad@3proxy.org>
please read License Agreement
*/
#include "structures.h"
#include "proxy.h"
#ifdef WITH_TRANSPARENT
#ifdef WITH_NETFILTER
#include <sys/utsname.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <limits.h>
#include <linux/netfilter_ipv4.h>
#endif
#ifdef WITH_PF
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <fcntl.h>
#include <net/if.h>
#include <net/pfvar.h>
#endif
/* Where the address the client was trying to reach is read from.
AUTO uses what the platform offers, which is the only thing an
installation usually needs. The rest name one mechanism, for a machine
that has more than one and redirects with a particular one.
*/
#define TRANSPARENT_AUTO 0
#define TRANSPARENT_NETFILTER 1
#define TRANSPARENT_PF 2
#define TRANSPARENT_SOCKET 3
static struct pluginlink * pl;
static int transparent_loaded = 0;
static int transparent_mode = TRANSPARENT_AUTO;
#ifdef WITH_PF
static int pf_device = -1;
/* Ask the packet filter what the connection was addressed to before it was
redirected. pf keeps that in its state table rather than on the socket,
so it has to be looked up with the addresses of both ends.
*/
static int transparent_pf(struct clientparam *param)
{
struct pfioc_natlook nl;
if(pf_device < 0){
pf_device = open("/dev/pf", O_RDONLY);
if(pf_device < 0) return 1;
}
memset(&nl, 0, sizeof(nl));
nl.proto = IPPROTO_TCP;
nl.direction = PF_OUT;
#ifndef NOIPV6
if(*SAFAMILY(&param->sincr) == AF_INET6){
nl.af = AF_INET6;
memcpy(&nl.saddr.v6, SAADDR(&param->sincr), 16);
memcpy(&nl.daddr.v6, SAADDR(&param->sincl), 16);
}
else
#endif
{
nl.af = AF_INET;
memcpy(&nl.saddr.v4, SAADDR(&param->sincr), 4);
memcpy(&nl.daddr.v4, SAADDR(&param->sincl), 4);
}
nl.sport = *SAPORT(&param->sincr);
nl.dport = *SAPORT(&param->sincl);
if(ioctl(pf_device, DIOCNATLOOK, &nl)) return 1;
memset(&param->req, 0, sizeof(param->req));
*SAFAMILY(&param->req) = nl.af;
#ifndef NOIPV6
if(nl.af == AF_INET6) memcpy(SAADDR(&param->req), &nl.rdaddr.v6, 16);
else
#endif
memcpy(SAADDR(&param->req), &nl.rdaddr.v4, 4);
*SAPORT(&param->req) = nl.rdport;
return 0;
}
#endif
#ifdef WITH_NETFILTER
/* Linux keeps the original address for the connection it redirected. */
static int transparent_netfilter(struct clientparam *param)
{
socklen_t len = sizeof(param->req);
#ifdef SO_ORIGINAL_DST
if(getsockopt(param->clisock,
#ifndef NOIPV6
#ifdef SOL_IPV6
*SAFAMILY(&param->sincr) == AF_INET6?SOL_IPV6:
#endif
#endif
SOL_IP, SO_ORIGINAL_DST, (struct sockaddr *) &param->req, &len)
|| !memcmp((char *)SAADDR(&param->req), "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", SAADDRLEN(&param->req))){
return 1;
}
return 0;
#else
#error No SO_ORIGINAL_DST defined
#endif
}
#endif
/* Some redirections leave the original address on the socket itself, so the
local address of the accepted connection is what the client asked for.
A connection which was not redirected at all arrives at the address the
service listens on, and taking that as the destination would send the
service to itself. Refuse instead of making the connection.
*/
static int transparent_socket(struct clientparam *param)
{
if(*SAFAMILY(&param->sincl) != AF_INET && *SAFAMILY(&param->sincl) != AF_INET6)
return 1;
if(*SAPORT(&param->sincl) == *SAPORT(&param->srv->intsa)
&& (SAISNULL(&param->srv->intsa)
|| !memcmp(SAADDR(&param->sincl), SAADDR(&param->srv->intsa), SAADDRLEN(&param->sincl))))
return 2;
param->req = param->sincl;
param->sincl = param->srv->intsa;
return 0;
}
static void* transparent_filter_open(void * idata, struct srvparam * param){
return idata;
}
static FILTER_ACTION transparent_filter_client(void *fo, struct clientparam * param, void** fc){
char addrbuf[64];
int res = 1;
#ifdef WITH_NETFILTER
if(transparent_mode == TRANSPARENT_AUTO || transparent_mode == TRANSPARENT_NETFILTER)
res = transparent_netfilter(param);
#endif
#ifdef WITH_PF
if(res && (transparent_mode == TRANSPARENT_AUTO || transparent_mode == TRANSPARENT_PF))
res = transparent_pf(param);
#endif
if(res && (transparent_mode == TRANSPARENT_AUTO || transparent_mode == TRANSPARENT_SOCKET)){
res = transparent_socket(param);
if(res == 2){
param->srv->logfunc(param, (unsigned char *)"transparent: connection was not redirected");
return REJECT;
}
}
/* Nothing knows where this was going: leave the request alone, so the
service decides as it would without the command. */
if(res) return PASS;
pl->myinet_ntop(*SAFAMILY(&param->req), SAADDR(&param->req), (char *)addrbuf, sizeof(addrbuf));
if(param->hostname) pl->freefunc(param->hostname);
param->hostname = (unsigned char *)pl->strdupfunc(addrbuf);
param->sinsr = param->req;
return PASS;
}
static void transparent_filter_clear(void *fo){
}
static void transparent_filter_close(void *fo){
}
static struct filter transparent_filter = {
NULL,
"Transparent filter",
"Transparent filter",
transparent_filter_open,
transparent_filter_client,
NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
transparent_filter_clear,
transparent_filter_close
};
int h_transparent(int argc, unsigned char **argv){
transparent_mode = TRANSPARENT_AUTO;
if(argc > 1){
if(!strcmp((char *)argv[1], "auto")) transparent_mode = TRANSPARENT_AUTO;
else if(!strcmp((char *)argv[1], "netfilter")){
#ifndef WITH_NETFILTER
fprintf(stderr, "transparent: netfilter is not available in this build\n");
return 1;
#else
transparent_mode = TRANSPARENT_NETFILTER;
#endif
}
else if(!strcmp((char *)argv[1], "pf")){
#ifndef WITH_PF
fprintf(stderr, "transparent: pf is not available in this build\n");
return 1;
#else
transparent_mode = TRANSPARENT_PF;
#endif
}
else if(!strcmp((char *)argv[1], "socket")) transparent_mode = TRANSPARENT_SOCKET;
else {
fprintf(stderr, "transparent: unknown mode %s, expected auto, netfilter, pf or socket\n", argv[1]);
return 1;
}
}
transparent_filter.filter_open = transparent_filter_open;
return 0;
}
int h_notransparent(int argc, unsigned char **argv){
transparent_filter.filter_open = NULL;
return 0;
}
void transparent_install(void){
pl = &pluginlink;
/* A reload runs this again: the filter is a single static entry, so it
is only linked in once, and the commands decide whether it acts. */
if(!transparent_loaded){
transparent_loaded = 1;
transparent_filter.next = pl->conf->filters;
pl->conf->filters = &transparent_filter;
}
transparent_filter.filter_open = NULL;
transparent_mode = TRANSPARENT_AUTO;
}
#endif

155
tests/cases/transparent.py Normal file
View File

@ -0,0 +1,155 @@
"""Transparent proxying: the destination comes from the redirection.
A redirected connection no longer says where it was going, so the proxy has
to ask the packet filter. That means a real redirection rule, which needs
privilege, so the case skips unless it can install one and remove it again.
The rule must not catch the proxy's own connection to the origin, or the
traffic goes round for ever. Here the proxy is given an outgoing address of
its own and the rule excludes it, which is the arrangement the documentation
recommends.
"""
import os
import platform
import shutil
import subprocess
ORIGIN_ADDR = "127.0.0.9" # where the client believes it is going
PROXY_ADDR = "127.0.0.8" # the source the proxy connects from
DECOY_ADDR = "127.0.0.7" # a second server, to show where traffic went
def _run(command):
done = subprocess.run(command, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=30)
return done.returncode, done.stdout.decode("utf-8", "replace").strip()
def _iptables_rule(action, origin_port, proxy_port):
return ["iptables", "-t", "nat", action, "OUTPUT",
"-p", "tcp", "-d", ORIGIN_ADDR, "--dport", str(origin_port),
"!", "-s", PROXY_ADDR,
"-j", "REDIRECT", "--to-ports", str(proxy_port)]
def _config(t, name, body):
"""Run a configuration that has no service, and return what it said."""
return t.run_config(name, body + "\nnot_a_command\n")
def run(t):
# --- the command and its modes ------------------------------------
# These need no redirection, so they run wherever the feature is built.
out = _config(t, "transparent_probe", "log\ntransparent")
if "'transparent'" in out:
t.skip("transparent proxying (not built in this configuration)")
return
for mode in ("auto", "socket"):
t.not_contains(_config(t, "mode_" + mode, f"log\ntransparent {mode}"),
"transparent:", f"the {mode} mode is accepted")
t.contains(_config(t, "mode_bogus", "log\ntransparent bogus"),
"unknown mode", "an unknown mode is refused")
# A mode the build has no code for is refused rather than ignored, so a
# configuration written for another platform fails where it is wrong
# instead of quietly doing something else.
for mode, built in (("netfilter", platform.system() == "Linux"), ("pf", False)):
out = _config(t, "mode_" + mode, f"log\ntransparent {mode}")
if built:
t.not_contains(out, "not available", f"the {mode} mode is accepted where it exists")
elif "not available" in out:
t.ok(f"the {mode} mode is refused where it does not exist")
else:
t.skip(f"the {mode} mode (built here, nothing to check)")
t.not_contains(_config(t, "notransparent", "log\ntransparent\nnotransparent"),
"'notransparent'", "notransparent is accepted")
# --- and the redirection itself ------------------------------------
if platform.system() != "Linux":
# The BSDs need a redirection that leaves the original destination on
# the socket - divert-to on OpenBSD, ipfw fwd on FreeBSD - and macOS
# has neither, so there is nothing to set up here.
t.skip(f"transparent proxying (no redirection to set up on {platform.system()})")
return
if os.geteuid() != 0 or not shutil.which("iptables"):
t.skip("transparent proxying (needs root and iptables to redirect)")
return
origin_port = t.free_port()
decoy_port = t.free_port()
mapper_port = t.free_port()
plain_port = t.free_port()
t.start("transparent", f"""
log
auth iponly
allow *
http * /echo* echo
httpsrv -p{origin_port} -i{ORIGIN_ADDR}
# a second server, to tell apart where a connection actually went
flush
auth iponly
allow *
http * * data size=13
httpsrv -p{decoy_port} -i{DECOY_ADDR}
# a port mapper aimed at the decoy: with the destination taken from
# the redirection instead, it goes to the origin
flush
auth iponly
allow *
transparent
tcppm -e{PROXY_ADDR} {mapper_port} {DECOY_ADDR} {decoy_port}
notransparent
# the same mapper without it, which keeps going to the decoy
flush
auth iponly
allow *
tcppm -e{PROXY_ADDR} {plain_port} {DECOY_ADDR} {decoy_port}
""", ports=[(ORIGIN_ADDR, origin_port), (DECOY_ADDR, decoy_port),
mapper_port, plain_port])
code, out = _run(_iptables_rule("-A", origin_port, mapper_port))
if code:
t.skip(f"transparent proxying (could not add a redirect rule: {out})")
return
try:
# The client asks for the address it wants; the rule sends the
# connection to the mapper instead, and the mapper has to work out
# where it was headed.
r = t.http(f"http://{ORIGIN_ADDR}:{origin_port}/echo")
t.eq(200, r.status, "a redirected connection reaches its destination")
t.contains(r, "path=/echo", "the request arrives unchanged")
t.contains(r, f"host={ORIGIN_ADDR}:{origin_port}",
"the client still believes it is talking to the origin")
t.contains(r, f"peer.addr={PROXY_ADDR}",
"the origin is reached from the proxy's own address")
# That address is what the rule excludes, which is what stops the
# proxy's own connection from being redirected back into itself.
t.not_contains(r, "size=13", "the connection did not go to the decoy")
# Without the command the mapper has no reason to look, and goes
# where it was configured to go.
_run(_iptables_rule("-D", origin_port, mapper_port))
code, out = _run(_iptables_rule("-A", origin_port, plain_port))
if code:
t.skip("the mapper without the command (could not move the rule)")
else:
r = t.http(f"http://{ORIGIN_ADDR}:{origin_port}/echo")
t.eq(13, r.length,
"without the command the connection goes to the configured target")
t.not_contains(r, "path=/echo",
"and never reaches the address the client asked for")
_run(_iptables_rule("-D", origin_port, plain_port))
finally:
# leave the machine as it was found, whatever happened above
_run(_iptables_rule("-D", origin_port, mapper_port))
_run(_iptables_rule("-D", origin_port, plain_port))