From da66016c7f56d54050b1e6ef455bd879dc41213b Mon Sep 17 00:00:00 2001 From: Vladimir Dubrovin <3proxy@3proxy.ru> Date: Wed, 26 Aug 2026 14:34:44 +0300 Subject: [PATCH] Transparent moved to main code (TransparentPlugin removed), BSD pf support added (cherry picked from commit 88b3225bdff11bc48735da5ef38613b3394b3bb1) --- CMakeLists.txt | 46 +++- Makefile.FreeBSD | 15 +- Makefile.Linux | 8 +- Makefile.Solaris | 2 +- Makefile.unix | 8 +- doc/html/howtoe.html | 184 ++++++++++++++ doc/html/howtor.html | 171 +++++++++++++ doc/html/plugins/TransparentPlugin.html | 55 ++-- doc/html/plugins/TransparentPlugin.ru.html | 59 +++-- man/3proxy.cfg.5 | 48 ++++ src/3proxy.c | 6 + src/Makefile.inc | 7 +- src/conf.c | 14 + src/plugins/TransparentPlugin/CMakeLists.txt | 6 - src/plugins/TransparentPlugin/Makefile | 1 - src/plugins/TransparentPlugin/Makefile.inc | 10 - .../TransparentPlugin/transparent_plugin.c | 128 ---------- src/transparent.c | 240 ++++++++++++++++++ tests/cases/transparent.py | 155 +++++++++++ 19 files changed, 972 insertions(+), 191 deletions(-) delete mode 100644 src/plugins/TransparentPlugin/CMakeLists.txt delete mode 100644 src/plugins/TransparentPlugin/Makefile delete mode 100644 src/plugins/TransparentPlugin/Makefile.inc delete mode 100644 src/plugins/TransparentPlugin/transparent_plugin.c create mode 100644 src/transparent.c create mode 100644 tests/cases/transparent.py diff --git a/CMakeLists.txt b/CMakeLists.txt index a425f5e..632dd89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,8 +4,20 @@ cmake_minimum_required(VERSION 3.16) -# Read version from RELEASE file -file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/RELEASE" PROJECT_VERSION LIMIT_COUNT 1) +# Read the version. A release branch carries RELEASE, a development branch +# 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 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_WSAPOLL "Use WSAPoll instead of select() (Windows 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_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 StringsPlugin TrafficPlugin - TransparentPlugin FilePlugin ) @@ -215,7 +227,6 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "FreeBSD|Darwin|OpenBSD|NetBSD") set(DEFAULT_PLUGINS StringsPlugin TrafficPlugin - TransparentPlugin FilePlugin ) @@ -232,7 +243,6 @@ else() set(DEFAULT_PLUGINS StringsPlugin TrafficPlugin - TransparentPlugin FilePlugin ) endif() @@ -241,6 +251,25 @@ if(3PROXY_USE_HTTPSRV) add_compile_definitions(WITH_HTTPSRV) 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 # elsewhere, e.g. CFLAGS if(NOT 3PROXY_USE_UNIX_SOCKETS) @@ -471,6 +500,10 @@ if(PCRE2_FOUND) target_sources(3proxy PRIVATE src/pcre.c) endif() +if(3PROXY_TRANSPARENT_BUILT) + target_sources(3proxy PRIVATE src/transparent.c) +endif() + target_include_directories(3proxy PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src ${CMAKE_CURRENT_SOURCE_DIR}/src/libs @@ -917,7 +950,7 @@ endif() # Summary message(STATUS "") message(STATUS "3proxy configuration summary:") -message(STATUS " Version: ${PROJECT_VERSION}") +message(STATUS " Version: ${PROJECT_VERSION_FULL}") message(STATUS " Platform: ${CMAKE_SYSTEM_NAME}") message(STATUS " Compiler: ${CMAKE_C_COMPILER_ID} ${CMAKE_C_COMPILER_VERSION}") 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_OPENSSL: ${3PROXY_USE_OPENSSL}") message(STATUS " USE_PCRE2: ${3PROXY_USE_PCRE2}") +message(STATUS " TRANSPARENT: ${3PROXY_TRANSPARENT_BUILT}") message(STATUS " USE_PAM: ${3PROXY_USE_PAM}") message(STATUS " USE_ODBC: ${3PROXY_USE_ODBC}") message(STATUS " USE_POLL: ${3PROXY_USE_POLL}") diff --git a/Makefile.FreeBSD b/Makefile.FreeBSD index eddee6f..7865d04 100644 --- a/Makefile.FreeBSD +++ b/Makefile.FreeBSD @@ -44,7 +44,20 @@ AFTERCLEAN = (find . -type f -name "*.o" -delete && find src/ -type f -name "Mak TYPECOMMAND = cat COMPATLIBS = 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 \\n\#include \\n\#include \\n\#include \\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) LDFLAGS += -static CFLAGS += -DNOPLUGINS -DNOSTDRESOLVE -DNOCRYPT diff --git a/Makefile.Linux b/Makefile.Linux index 46597b7..48b48d8 100644 --- a/Makefile.Linux +++ b/Makefile.Linux @@ -50,8 +50,12 @@ MAKEFILE = Makefile.Linux # PamAuth requires libpam, you may require pam-devel package to be installed # SSLPlugin requires -lcrypto -lssl #LIBS = -lcrypto -lssl -ldl -#PLUGINS = SSLPlugin StringsPlugin TrafficPlugin PCREPlugin TransparentPlugin PamAuth -PLUGINS ?= StringsPlugin TrafficPlugin TransparentPlugin FilePlugin +#PLUGINS = StringsPlugin TrafficPlugin PamAuth LdapPlugin +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) LDFLAGS += -static CFLAGS += -DNOPLUGINS -DNOSTDRESOLVE -DNOCRYPT diff --git a/Makefile.Solaris b/Makefile.Solaris index 1fb7693..f5c2537 100644 --- a/Makefile.Solaris +++ b/Makefile.Solaris @@ -34,7 +34,7 @@ AFTERCLEAN = (find . -type f -name "*.o" -delete && find src/ -type f -name "Mak TYPECOMMAND = cat COMPATLIBS = MAKEFILE = Makefile.Solaris -PLUGINS = StringsPlugin TrafficPlugin TransparentPlugin FilePlugin +PLUGINS = StringsPlugin TrafficPlugin FilePlugin WOLFSSL_CHECK = $(shell printf "\#include \\n\#include \\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) diff --git a/Makefile.unix b/Makefile.unix index c0bb8b1..ec71ad5 100644 --- a/Makefile.unix +++ b/Makefile.unix @@ -46,7 +46,13 @@ AFTERCLEAN = (find . -type f -name "*.o" -delete && find src/ -type f -name "Mak TYPECOMMAND = cat COMPATLIBS = 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) LDFLAGS += -static CFLAGS += -DNOPLUGINS -DNOSTDRESOLVE -DNOCRYPT diff --git a/doc/html/howtoe.html b/doc/html/howtoe.html index 14b9747..1eea04d 100644 --- a/doc/html/howtoe.html +++ b/doc/html/howtoe.html @@ -37,6 +37,7 @@
  • How to set up TLS/SSL (https proxy, mTLS)
  • How to create CA and certificates for SSL
  • How to use PCRE filtering (regular expressions)
  • +
  • How to proxy transparently
  • How to limit service access
  • How to create a user list
  • How to limit user access to resources @@ -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 client.crt + +
  • How to proxy transparently +

    +A transparent proxy serves clients that were never configured to use one. A +packet filter redirects their connections to 3proxy, and the +transparent 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 plugin line it +needed is no longer required. +

    +

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

    +

    +Without it a service has to get a destination from somewhere else: an HTTP +proxy falls back to the Host 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. +

    +

    +tlspr 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. +

    +

    +A configuration for web and TLS traffic: +

    +log /var/log/3proxy.log D
    +auth iponly
    +allow *
    +
    +# the destination comes from the redirection for the services below
    +transparent
    +
    +# ordinary web traffic, redirected here from port 80
    +proxy -p3129 -e192.0.2.10
    +
    +# TLS, redirected here from port 443: tlspr would otherwise have only the
    +# name in the handshake, and this gives it the address as well
    +tlspr -p3143 -e192.0.2.10
    +
    +notransparent
    +
    +

    +-e 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. +

    + +

    Linux, iptables. For traffic the machine forwards for others: +

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

    +Traffic the machine generates itself passes through OUTPUT instead, where the +proxy's own connections have to be excluded: +

    +# 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
    +
    +# 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
    +
    + +

    Linux, nftables (Debian 11+, Ubuntu 22.04+, RHEL 8+, Fedora, Arch, +where nftables is what iptables is a front end for): +

    +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
    +	}
    +}
    +
    +

    +Load it with nft -f, and keep it across reboots in +/etc/nftables.conf (Debian, Ubuntu) or +/etc/sysconfig/nftables.conf (RHEL, Fedora). A table name cannot begin +with a digit, which is why the table above is not called 3proxy. +

    + +

    Linux, firewalld (RHEL, CentOS Stream, Fedora, openSUSE). Redirect an +incoming port on a zone: +

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

    +firewalld has no exclusion for the proxy's own traffic in that form, so put +that part in a direct rule: +

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

    Linux, ufw (Ubuntu, Debian). ufw has no command for redirection; +add the rules to /etc/ufw/before.rules, above the *filter block: +

    +*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
    +
    + +

    +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 +/dev/pf - so rdr 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 divert-to and FreeBSD ipfw fwd both do. +

    +

    +The mechanism is chosen automatically, and transparent takes an +argument for the installations that need to pin it: auto (the default), +netfilter, pf, or socket 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. +

    + +

    FreeBSD, NetBSD and OpenBSD, pf. Redirect in /etc/pf.conf, +excluding the address the proxy connects from: +

    +rdr pass on em0 inet proto tcp from ! 192.0.2.10 to any port 80  -> 127.0.0.1 port 3129
    +rdr pass on em0 inet proto tcp from ! 192.0.2.10 to any port 443 -> 127.0.0.1 port 3143
    +
    +

    +Load with pfctl -f /etc/pf.conf. 3proxy looks the destination up in pf's +state table, so it needs to read /dev/pf: either run it as root, or give +its account access to that device. +

    + +

    OpenBSD, divert-to is an alternative which leaves the destination on +the socket, and needs no access to /dev/pf: +

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

    FreeBSD, ipfw. fwd delivers the connection locally without +rewriting it, which also leaves the destination on the socket: +

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

    macOS has /dev/pf but ships no header for it, so a macOS build +has no pf lookup, and macOS has neither divert-to nor ipfw to leave the +address on the socket. Transparent proxying is not usable there, even though +the commands exist in a macOS build. +

    + +

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

    +
  • How to use PCRE filtering (regular expressions)

    Since version 0.9.7, PCRE (Perl Compatible Regular Expressions) filtering is built into diff --git a/doc/html/howtor.html b/doc/html/howtor.html index 4513e2b..b1af3da 100644 --- a/doc/html/howtor.html +++ b/doc/html/howtor.html @@ -37,6 +37,7 @@

  • Как настроить TLS/SSL (https прокси, mTLS)
  • Как создать CA и сертификаты для SSL
  • Как использовать PCRE-фильтрацию (регулярные выражения)
  • +
  • Как сделать транспарентный прокси
  • Как ограничить доступ к службе
  • Как создать список пользователей
  • Как ограничить доступ пользователей к ресурсам @@ -992,6 +993,176 @@ openssl verify -x509_strict -CAfile ca.crt server.crt openssl verify -x509_strict -CAfile ca.crt client.crt + +
  • Как сделать транспарентный прокси +

    +Транспарентный прокси обслуживает клиентов, которые не настроены на работу +через прокси. Пакетный фильтр перенаправляет их соединения на 3proxy, а команда +transparent указывает сервису брать адрес назначения у фильтра, а не из +запроса. Всё остальное работает как обычно: правила доступа, родительские +прокси, ограничения и логирование видят настоящий адрес назначения. Работает в +Linux и BSD. С версии 1.0.1 встроено в бинарник, раньше это был отдельный +TransparentPlugin, и строка plugin больше не нужна. +

    +

    +Команда даёт и адрес, и порт назначения, поэтому сервис обслуживает всё, что +на него перенаправлено, а не один порт с одним адресом назначения. +

    +

    +Без неё сервис берёт адрес откуда-то ещё: HTTP-прокси - из заголовка +Host, порт-маппер - из своей конфигурации. Именно получение адреса от +фильтра позволяет работать с остальными протоколами и делает адрес +достоверным, а не заявленным клиентом. +

    +

    +Нагляднее всего это с tlspr. Без перенаправления трафика (или без +резолва имён на него через DNS) на него вообще ничего не попадёт. С +перенаправлением он получает и имя из TLS handshake, и адрес назначения - +именно это позволяет писать правила доступа по именам хостов: имя из handshake +проверяется в ACL, а соединение идёт на тот адрес, куда шёл клиент. Без адреса +пришлось бы резолвить имя самостоятельно, то есть делать ещё один запрос и +получать ещё один ответ. +

    +

    +Конфигурация для веб- и TLS-трафика: +

    +log /var/log/3proxy.log D
    +auth iponly
    +allow *
    +
    +# для сервисов ниже адрес назначения берётся из перенаправления
    +transparent
    +
    +# обычный веб-трафик, перенаправленный сюда с порта 80
    +proxy -p3129 -e192.0.2.10
    +
    +# TLS, перенаправленный сюда с порта 443: без этого у tlspr было бы только
    +# имя из handshake, а так есть и адрес
    +tlspr -p3143 -e192.0.2.10
    +
    +notransparent
    +
    +

    +-e задаёт сервисам собственный адрес для исходящих соединений. Именно +этот адрес исключается в правилах перенаправления, и это исключение не даёт +соединениям самого прокси попасть обратно в него. Без исключения соединение, +которое 3proxy устанавливает к серверу назначения, попадает под то же правило, +возвращается в 3proxy, и трафик зацикливается. Можно вместо этого запускать +3proxy под отдельной учётной записью и исключать её - так лучше, если у машины +один адрес. +

    + +

    Linux, iptables. Для транзитного трафика: +

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

    +Трафик самой машины проходит через цепочку OUTPUT, где соединения прокси нужно +исключить: +

    +# по адресу, с которого сервисы устанавливают соединения
    +iptables -t nat -A OUTPUT -p tcp --dport 80 ! -s 192.0.2.10 -j REDIRECT --to-ports 3129
    +
    +# либо по учётной записи, под которой работает 3proxy
    +iptables -t nat -A OUTPUT -p tcp --dport 80 -m owner ! --uid-owner proxy3 \
    +    -j REDIRECT --to-ports 3129
    +
    + +

    Linux, nftables (Debian 11+, Ubuntu 22.04+, RHEL 8+, Fedora, Arch): +

    +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
    +	}
    +}
    +
    +

    +Загружается через nft -f, сохраняется в /etc/nftables.conf +(Debian, Ubuntu) или /etc/sysconfig/nftables.conf (RHEL, Fedora). Имя +таблицы не может начинаться с цифры, поэтому таблица называется не 3proxy. +

    + +

    Linux, firewalld (RHEL, CentOS Stream, Fedora, openSUSE): +

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

    +Исключение для трафика самого прокси в таком виде не задаётся, для него нужно +прямое правило: +

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

    Linux, ufw (Ubuntu, Debian). В ufw нет команды для перенаправления, +правила добавляются в /etc/ufw/before.rules перед блоком *filter: +

    +*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
    +
    + +

    +В Linux 3proxy спрашивает у ядра, куда шло соединение. В BSD он спрашивает у pf, +который хранит исходный адрес назначения в таблице состояний, через +/dev/pf - поэтому работают правила rdr, и 3proxy должен иметь +доступ к этому устройству. Если перенаправление оставляет адрес на самом сокете, +используется он: так делают OpenBSD divert-to и FreeBSD ipfw fwd. +

    +

    +Механизм выбирается автоматически, а команда transparent принимает +аргумент для случаев, когда его надо зафиксировать: auto (по умолчанию), +netfilter, pf или socket для чтения адреса с сокета. Режим, +которого нет в сборке, отвергается, поэтому конфигурация, написанная для другой +платформы, не запустится вместо того, чтобы молча делать что-то другое. +

    + +

    FreeBSD, NetBSD, OpenBSD, pf. Перенаправление в /etc/pf.conf с +исключением адреса, с которого соединяется прокси: +

    +rdr pass on em0 inet proto tcp from ! 192.0.2.10 to any port 80  -> 127.0.0.1 port 3129
    +rdr pass on em0 inet proto tcp from ! 192.0.2.10 to any port 443 -> 127.0.0.1 port 3143
    +
    +

    +Загружается через pfctl -f /etc/pf.conf. 3proxy ищет адрес назначения в +таблице состояний pf, поэтому ему нужен доступ на чтение к /dev/pf: либо +запуск от root, либо права на устройство для его учётной записи. +

    + +

    OpenBSD, divert-to - альтернатива, оставляющая адрес на сокете, доступ +к /dev/pf при этом не нужен: +

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

    FreeBSD, ipfw. fwd доставляет соединение локально, не переписывая +его, и адрес тоже остаётся на сокете: +

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

    macOS: /dev/pf есть, но заголовочных файлов для него нет, +поэтому в сборке под macOS нет обращения к pf, а ни divert-to, ни ipfw в +macOS нет. Транспарентное проксирование там неприменимо, хотя команды в сборке +присутствуют. +

    +
  • Как использовать PCRE-фильтрацию (регулярные выражения)

    Начиная с версии 0.9.7 фильтрация PCRE встроена в 3proxy при компиляции с поддержкой diff --git a/doc/html/plugins/TransparentPlugin.html b/doc/html/plugins/TransparentPlugin.html index 93e993c..b9a09ad 100644 --- a/doc/html/plugins/TransparentPlugin.html +++ b/doc/html/plugins/TransparentPlugin.html @@ -1,31 +1,56 @@ -

    3proxy TransparentPlugin (Linux/BSD only)

    +

    3proxy transparent proxying (Linux/BSD only)

    -This plugin can turn 3proxy into a transparent proxy for virtually any TCP-based protocol -and use all 3proxy features - redirections, parent proxies, ACLs, traffic limitations, -etc. The TransparentPlugin takes the destination IP:port from Linux and uses this -information as the target IP in the proxy. An example usage: +Transparent proxying is part of 3proxy itself since 1.0.1. It was the separate +TransparentPlugin before that, and the plugin line that used to load it +is no longer needed: the transparent and notransparent commands +are always available on the platforms that can redirect a connection. + +

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

    -plugin /path/to/TransparentPlugin.ld.so transparent_plugin
     log /path/to/log
     auth iponly
     allow * * * 80
     parent 1000 http 0.0.0.0 0
     allow *
     parent 1000 socks5 SOCKS5_IP SOCKS5_PORT USER PASSWORD
    +
     transparent
    -tcppm -iLOCAL_IP 12345 127.0.0.1 11111
    +tcppm -eLOCAL_IP 12345 127.0.0.1 11111
     notransparent
     proxy
     
    -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. -

    Download:

    -
      -
    • Plugin is included in 3proxy 0.8 -
    +

    +Any TCP traffic redirected to port 12345 is routed through the parent SOCKSv5 +proxy and logged, with the URLs of web requests visible in the log. The +'127.0.0.1 11111' arguments are not used in that case: they are replaced by the +destination the client was trying to reach. +

    + +

    +The destination is looked up in pf on the BSDs, through /dev/pf, and +asked of the kernel on Linux; a redirection that leaves the address on the +socket is used where there is one. transparent takes an optional +auto, netfilter, pf or socket to pin that choice. +

    +

    +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 -e and exclude it in the rules, or run 3proxy as its +own account and exclude that account. +

    + +

    +Redirection rules for iptables, nftables, firewalld, ufw and pf are in +How to proxy transparently, and the +commands are described in 3proxy.cfg(5). +

    © Vladimir Dubrovin, License: BSD style diff --git a/doc/html/plugins/TransparentPlugin.ru.html b/doc/html/plugins/TransparentPlugin.ru.html index 229cf13..f340dcd 100644 --- a/doc/html/plugins/TransparentPlugin.ru.html +++ b/doc/html/plugins/TransparentPlugin.ru.html @@ -1,33 +1,56 @@ -

    Плагин TransparentPlugin 3proxy (только для Linux/BSD)

    +

    Транспарентное проксирование 3proxy (только для Linux/BSD)

    -Плагин превращает 3proxy в транспарентный прокси для практически любых TCP-соединений -и позволяет прозрачно для клиентов использовать весь фунционал прокси - редиректоры, -родительские прокси, ACLи, ограничения трафика. TransparentPlugin получает IP:port -назначения от Linux и использует эту информацию в качестве конечного адреса назначения. -
    -Пример использования: +Начиная с 1.0.1 транспарентное проксирование встроено в 3proxy. Раньше это был +отдельный TransparentPlugin, и строка plugin, которой он загружался, +больше не нужна: команды transparent и notransparent доступны +всегда на тех платформах, где соединение можно перенаправить. + +

    +3proxy становится транспарентным прокси практически для любых TCP-соединений, +причём весь остальной функционал работает как обычно - редиректоры, +родительские прокси, ACLи, ограничения трафика и логирование. IP и порт +назначения берутся у пакетного фильтра, перенаправившего соединение, и +используются как адрес назначения проксируемого соединения. +

    -plugin /path/to/TransparentPlugin.ld.so transparent_plugin
     log /path/to/log
     auth iponly
     allow * * * 80
     parent 1000 http 0.0.0.0 0
     allow *
     parent 1000 socks5 SOCKS5_IP SOCKS5_PORT USER PASSWORD
    +
     transparent
    -tcppm -iLOCAL_IP 12345 127.0.0.1 11111
    +tcppm -eLOCAL_IP 12345 127.0.0.1 11111
     notransparent
     proxy
     
    -Теперь любые TCP-соединения транспарентно перенаправленные в локальный порт 12345 -будут прологгированы и перенаправлены в родительский SOCKSv5 proxy, при этом для -HTTP-запросов по порту TCP/80 будут видны параметры HTTP-запроса. -Параметры '127.0.0.1 11111' в данном случае не оказывают влияния, т.к. -будут перезаписываться IP и портом назначения для каждого TCP-соединения соответственно. -

    Загрузить:

    -
      -
    • Плагин включен в дистрибутив 3proxy 0.8 -
    + +

    +Любой TCP-трафик, перенаправленный на порт 12345, пойдёт через родительский +SOCKSv5 прокси и будет залогирован, URL веб-запросов видны в логе. Аргументы +'127.0.0.1 11111' в этом случае не используются: они заменяются адресом, к +которому обращался клиент. +

    + +

    +В BSD адрес назначения ищется в pf через /dev/pf, в Linux запрашивается у +ядра; если перенаправление оставляет адрес на сокете, используется он. Команда +transparent принимает необязательный аргумент auto, +netfilter, pf или socket, чтобы зафиксировать выбор. +

    +

    +Правила перенаправления не должны попадать на соединения, которые устанавливает +сам 3proxy, иначе трафик возвращается в прокси и зацикливается. Задайте сервису +адрес для исходящих соединений через -e и исключите его в правилах, либо +запускайте 3proxy под отдельной учётной записью и исключайте её. +

    + +

    +Правила перенаправления для iptables, nftables, firewalld, ufw и pf приведены в +описании транспарентного проксирования, +команды описаны в 3proxy.cfg(5). +

    © Vladimir Dubrovin, License: BSD style diff --git a/man/3proxy.cfg.5 b/man/3proxy.cfg.5 index cf76cdb..746f8ae 100644 --- a/man/3proxy.cfg.5 +++ b/man/3proxy.cfg.5 @@ -1192,6 +1192,54 @@ the format: Note: double quotes are required because the password contains a $ sign. .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 .br empty the active access list. The access list must be flushed every time you create a diff --git a/src/3proxy.c b/src/3proxy.c index b762a0e..007ed8a 100644 --- a/src/3proxy.c +++ b/src/3proxy.c @@ -13,6 +13,9 @@ void ssl_install(void); #ifdef WITH_PCRE void pcre_install(void); #endif +#ifdef WITH_TRANSPARENT +void transparent_install(void); +#endif #ifndef _WIN32 #include #ifndef NOPLUGINS @@ -529,6 +532,9 @@ int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int #ifdef WITH_PCRE pcre_install(); #endif +#ifdef WITH_TRANSPARENT + transparent_install(); +#endif freeconf(&conf); initcommands(); diff --git a/src/Makefile.inc b/src/Makefile.inc index 532295c..b2c04ce 100644 --- a/src/Makefile.inc +++ b/src/Makefile.inc @@ -116,6 +116,9 @@ srvsocks$(OBJSUFFICS): socks.c proxy.h structures.h srvwebadmin$(OBJSUFFICS): webadmin.c proxy.h structures.h $(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 $(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 $(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) - $(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) +$(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) $(TRANSPARENT_OBJS) $(COMPATLIBS) $(LIBS) $(PCRE_LIBS) diff --git a/src/conf.c b/src/conf.c index 0b271e7..d1a0e57 100644 --- a/src/conf.c +++ b/src/conf.c @@ -18,6 +18,9 @@ void ssl_install(void); #ifdef WITH_PCRE void pcre_install(void); #endif +#ifdef WITH_TRANSPARENT +void transparent_install(void); +#endif #ifndef _WIN32 #include #include @@ -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_client_mode(int argc, unsigned char **argv); #endif +#ifdef WITH_TRANSPARENT +int h_transparent(int argc, unsigned char **argv); +int h_notransparent(int argc, unsigned char **argv); +#endif #ifdef WITH_PCRE int h_pcre(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_certcache", h_certcache, 2, 2}, #endif +#ifdef WITH_TRANSPARENT + {NULL, "transparent", h_transparent, 1, 2}, + {NULL, "notransparent", h_notransparent, 1, 1}, +#endif #ifdef WITH_PCRE {NULL, "pcre", h_pcre, 4, 0}, {NULL, "pcre_rewrite", h_pcre_rewrite, 5, 0}, @@ -2212,6 +2223,9 @@ int reload (void){ #endif #ifdef WITH_PCRE pcre_install(); +#endif +#ifdef WITH_TRANSPARENT + transparent_install(); #endif conf.paused++; freeconf(&conf); diff --git a/src/plugins/TransparentPlugin/CMakeLists.txt b/src/plugins/TransparentPlugin/CMakeLists.txt deleted file mode 100644 index 2f78811..0000000 --- a/src/plugins/TransparentPlugin/CMakeLists.txt +++ /dev/null @@ -1,6 +0,0 @@ -# TransparentPlugin -# Works on Linux (with netfilter), BSD and macOS (without netfilter support) - -add_3proxy_plugin(TransparentPlugin - SOURCES transparent_plugin.c -) diff --git a/src/plugins/TransparentPlugin/Makefile b/src/plugins/TransparentPlugin/Makefile deleted file mode 100644 index e7c51ad..0000000 --- a/src/plugins/TransparentPlugin/Makefile +++ /dev/null @@ -1 +0,0 @@ -include Makefile.var diff --git a/src/plugins/TransparentPlugin/Makefile.inc b/src/plugins/TransparentPlugin/Makefile.inc deleted file mode 100644 index e0c2bb8..0000000 --- a/src/plugins/TransparentPlugin/Makefile.inc +++ /dev/null @@ -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) diff --git a/src/plugins/TransparentPlugin/transparent_plugin.c b/src/plugins/TransparentPlugin/transparent_plugin.c deleted file mode 100644 index d22b352..0000000 --- a/src/plugins/TransparentPlugin/transparent_plugin.c +++ /dev/null @@ -1,128 +0,0 @@ -/* - 3APA3A simplest proxy server - (c) 2002-2026 by Vladimir Dubrovin - - please read License Agreement - -*/ - - -#ifdef WITH_NETFILTER -#include -#endif -#include "../../structures.h" -#include "../../proxy.h" -#ifdef WITH_NETFILTER -#include -#include -#include -#include -#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(¶m->sincr) == AF_INET6?SOL_IPV6: -#endif -#endif - SOL_IP, SO_ORIGINAL_DST,(struct sockaddr *) ¶m->req, &len) || !memcmp((char *)SAADDR(¶m->req), "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", SAADDRLEN(¶m->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(¶m->sincl) == AF_INET || *SAFAMILY(¶m->sincl) == AF_INET6){ - param->req = param->sincl; - param->sincl = param->srv->intsa; - } -#endif - pl->myinet_ntop(*SAFAMILY(¶m->req), SAADDR(¶m->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 diff --git a/src/transparent.c b/src/transparent.c new file mode 100644 index 0000000..01809a0 --- /dev/null +++ b/src/transparent.c @@ -0,0 +1,240 @@ +/* + 3APA3A simplest proxy server + (c) 2002-2026 by Vladimir Dubrovin + + please read License Agreement + +*/ + +#include "structures.h" +#include "proxy.h" + +#ifdef WITH_TRANSPARENT + +#ifdef WITH_NETFILTER +#include +#include +#include +#include +#include +#endif + +#ifdef WITH_PF +#include +#include +#include +#include +#include +#include +#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(¶m->sincr) == AF_INET6){ + nl.af = AF_INET6; + memcpy(&nl.saddr.v6, SAADDR(¶m->sincr), 16); + memcpy(&nl.daddr.v6, SAADDR(¶m->sincl), 16); + } + else +#endif + { + nl.af = AF_INET; + memcpy(&nl.saddr.v4, SAADDR(¶m->sincr), 4); + memcpy(&nl.daddr.v4, SAADDR(¶m->sincl), 4); + } + nl.sport = *SAPORT(¶m->sincr); + nl.dport = *SAPORT(¶m->sincl); + + if(ioctl(pf_device, DIOCNATLOOK, &nl)) return 1; + + memset(¶m->req, 0, sizeof(param->req)); + *SAFAMILY(¶m->req) = nl.af; +#ifndef NOIPV6 + if(nl.af == AF_INET6) memcpy(SAADDR(¶m->req), &nl.rdaddr.v6, 16); + else +#endif + memcpy(SAADDR(¶m->req), &nl.rdaddr.v4, 4); + *SAPORT(¶m->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(¶m->sincr) == AF_INET6?SOL_IPV6: +#endif +#endif + SOL_IP, SO_ORIGINAL_DST, (struct sockaddr *) ¶m->req, &len) + || !memcmp((char *)SAADDR(¶m->req), "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0", SAADDRLEN(¶m->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(¶m->sincl) != AF_INET && *SAFAMILY(¶m->sincl) != AF_INET6) + return 1; + if(*SAPORT(¶m->sincl) == *SAPORT(¶m->srv->intsa) + && (SAISNULL(¶m->srv->intsa) + || !memcmp(SAADDR(¶m->sincl), SAADDR(¶m->srv->intsa), SAADDRLEN(¶m->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(¶m->req), SAADDR(¶m->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 diff --git a/tests/cases/transparent.py b/tests/cases/transparent.py new file mode 100644 index 0000000..0a729c7 --- /dev/null +++ b/tests/cases/transparent.py @@ -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))