2 * Copyright (C) 1996-2025 The Squid Software Foundation and contributors
4 * Squid software is distributed under GPLv2+ license and includes
5 * contributions from numerous individuals and organizations.
6 * Please see the COPYING and CONTRIBUTORS files for details.
9 /* DEBUG: section 89 NAT / IP Interception */
11 // Enable hack to workaround Solaris 10 IPFilter breakage
12 #define BUILDING_SQUID_IP_INTERCEPT_CC 1
15 #include "comm/Connection.h"
16 #include "compat/socket.h"
17 #include "compat/unistd.h"
19 #include "ip/Intercept.h"
21 #include "src/tools.h"
27 #if !defined(IPFILTER_VERSION)
28 #define IPFILTER_VERSION 5000004
32 #include <sys/param.h>
35 #include <sys/ioccom.h>
38 #include <sys/ioctl.h>
40 #if HAVE_NETINET_IP6_H
41 #include <netinet/ip6.h>
43 #if HAVE_NETINET_TCP_H
44 #include <netinet/tcp.h>
51 #elif HAVE_NETINET_IPL_H
52 #include <netinet/ipl.h>
54 #if USE_SOLARIS_IPFILTER_MINOR_T_HACK
57 #if HAVE_IP_FIL_COMPAT_H
58 #include <ip_fil_compat.h>
59 #elif HAVE_NETINET_IP_FIL_COMPAT_H
60 #include <netinet/ip_fil_compat.h>
61 #elif HAVE_IP_COMPAT_H
62 #include <ip_compat.h>
63 #elif HAVE_NETINET_IP_COMPAT_H
64 #include <netinet/ip_compat.h>
68 #elif HAVE_NETINET_IP_FIL_H
69 #include <netinet/ip_fil.h>
73 #elif HAVE_NETINET_IP_NAT_H
74 #include <netinet/ip_nat.h>
77 #endif /* IPF_TRANSPARENT required headers */
80 #include <sys/ioctl.h>
81 #include <sys/fcntl.h>
83 #include <netinet/in.h>
84 #if HAVE_NET_PF_PFVAR_H
85 #include <net/pf/pfvar.h>
86 #endif /* HAVE_NET_PF_PFVAR_H */
88 #include <net/pfvar.h>
89 #endif /* HAVE_NET_PFVAR_H */
90 #endif /* PF_TRANSPARENT required headers */
93 /* <climits> must be before including netfilter_ipv4.h */
96 #include <linux/netfilter_ipv4.h>
97 #if HAVE_LINUX_NETFILTER_IPV6_IP6_TABLES_H
98 /* 2013-07-01: Pablo the Netfilter maintainer is rejecting patches
99 * which will enable C++ compilers to build the Netfilter public headers.
100 * We can auto-detect its presence and attempt to use in case he ever
101 * changes his mind or things get cleaned up some other way.
102 * But until then are usually forced to hard-code the getsockopt() code
103 * for IPv6 NAT lookups.
105 #include <linux/netfilter_ipv6/ip6_tables.h>
107 #if !defined(IP6T_SO_ORIGINAL_DST)
108 #define IP6T_SO_ORIGINAL_DST 80 // stolen with prejudice from the above file.
110 #endif /* LINUX_NETFILTER required headers */
112 // single global instance for access by other components.
113 Ip::Intercept
Ip::Interceptor
;
116 Ip::Intercept::StopTransparency(const char *str
)
118 if (transparentActive_
) {
119 debugs(89, DBG_IMPORTANT
, "Stopping full transparency: " << str
);
120 transparentActive_
= 0;
125 Ip::Intercept::NetfilterInterception(const Comm::ConnectionPointer
&newConn
)
128 struct sockaddr_storage lookup
;
129 socklen_t len
= newConn
->local
.isIPv6() ? sizeof(sockaddr_in6
) : sizeof(sockaddr_in
);
130 newConn
->local
.getSockAddr(lookup
, AF_UNSPEC
);
133 * Try NAT lookup for REDIRECT or DNAT targets. */
134 if ( xgetsockopt(newConn
->fd
,
135 newConn
->local
.isIPv6() ? IPPROTO_IPV6
: IPPROTO_IP
,
136 newConn
->local
.isIPv6() ? IP6T_SO_ORIGINAL_DST
: SO_ORIGINAL_DST
,
139 const auto xerrno
= errno
;
140 debugs(89, DBG_IMPORTANT
, "ERROR: NF getsockopt(ORIGINAL_DST) failed on " << newConn
<< ": " << xstrerr(xerrno
));
143 newConn
->local
= lookup
;
144 debugs(89, 5, "address NAT: " << newConn
);
154 Ip::Intercept::StartTransparency()
156 // --enable-linux-netfilter
157 // --enable-pf-transparent
158 // --enable-ipfw-transparent
159 #if (LINUX_NETFILTER && defined(IP_TRANSPARENT)) || \
160 (PF_TRANSPARENT && defined(SO_BINDANY)) || \
161 (IPFW_TRANSPARENT && defined(IP_BINDANY))
162 transparentActive_
= 1;
164 throw TextException("requires TPROXY feature to be enabled by ./configure", Here());
169 Ip::Intercept::StartInterception()
171 // --enable-linux-netfilter
172 // --enable-ipfw-transparent
173 // --enable-ipf-transparent
174 // --enable-pf-transparent
175 #if IPF_TRANSPARENT || LINUX_NETFILTER || IPFW_TRANSPARENT || PF_TRANSPARENT
176 interceptActive_
= 1;
178 throw TextException("requires NAT Interception feature to be enabled by ./configure", Here());
183 Ip::Intercept::IpfwInterception(const Comm::ConnectionPointer
&newConn
)
186 return UseInterceptionAddressesLookedUpEarlier(__FUNCTION__
, newConn
);
193 /// Assume that getsockname() has been called already and provided the necessary
194 /// TCP packet details. There is no way to identify whether they came from NAT.
195 /// Trust the user configured properly.
197 Ip::Intercept::UseInterceptionAddressesLookedUpEarlier(const char * const caller
, const Comm::ConnectionPointer
&newConn
)
199 // paranoid: ./configure should prohibit these combinations
200 #if LINUX_NETFILTER && PF_TRANSPARENT && !USE_NAT_DEVPF
201 static_assert(!"--enable-linux-netfilter is incompatible with --enable-pf-transparent --without-nat-devpf");
203 #if LINUX_NETFILTER && IPFW_TRANSPARENT
204 static_assert(!"--enable-linux-netfilter is incompatible with --enable-ipfw-transparent");
206 // --enable-linux-netfilter is compatible with --enable-ipf-transparent
208 debugs(89, 5, caller
<< " uses " << newConn
);
213 Ip::Intercept::IpfInterception(const Comm::ConnectionPointer
&newConn
)
215 #if IPF_TRANSPARENT /* --enable-ipf-transparent */
217 struct natlookup natLookup
;
218 static int natfd
= -1;
221 // all fields must be set to 0
222 memset(&natLookup
, 0, sizeof(natLookup
));
223 // for NAT lookup set local and remote IP:port's
224 if (newConn
->remote
.isIPv6()) {
225 #if HAVE_STRUCT_NATLOOKUP_NL_INIPADDR_IN6
227 newConn
->local
.getInAddr(natLookup
.nl_inipaddr
.in6
);
228 newConn
->remote
.getInAddr(natLookup
.nl_outipaddr
.in6
);
232 newConn
->local
.getInAddr(natLookup
.nl_inipaddr
.in4
);
233 newConn
->remote
.getInAddr(natLookup
.nl_outipaddr
.in4
);
235 #else /* HAVE_STRUCT_NATLOOKUP_NL_INIPADDR_IN6 */
236 // warn once every 10 at critical level, then push down a level each repeated event
237 static int warningLevel
= DBG_CRITICAL
;
238 debugs(89, warningLevel
, "Your IPF (IPFilter) NAT does not support IPv6. Please upgrade it.");
239 warningLevel
= (warningLevel
+ 1) % 10;
242 newConn
->local
.getInAddr(natLookup
.nl_inip
);
243 newConn
->remote
.getInAddr(natLookup
.nl_outip
);
244 #endif /* HAVE_STRUCT_NATLOOKUP_NL_INIPADDR_IN6 */
245 natLookup
.nl_inport
= htons(newConn
->local
.port());
246 natLookup
.nl_outport
= htons(newConn
->remote
.port());
247 // ... and the TCP flag
248 natLookup
.nl_flags
= IPN_TCP
;
254 natfd
= xopen(IPNAT_NAME
, O_RDONLY
, 0);
256 natfd
= xopen(IPL_NAT
, O_RDONLY
, 0);
264 const auto xerrno
= errno
;
265 debugs(89, DBG_IMPORTANT
, "ERROR: IPF (IPFilter) NAT open failed: " << xstrerr(xerrno
));
269 #if defined(IPFILTER_VERSION) && (IPFILTER_VERSION >= 4000027)
271 memset(&obj
, 0, sizeof(obj
));
272 obj
.ipfo_rev
= IPFILTER_VERSION
;
273 obj
.ipfo_size
= sizeof(natLookup
);
274 obj
.ipfo_ptr
= &natLookup
;
275 obj
.ipfo_type
= IPFOBJ_NATLOOKUP
;
277 x
= ioctl(natfd
, SIOCGNATL
, &obj
);
280 * IP-Filter changed the type for SIOCGNATL between
281 * 3.3 and 3.4. It also changed the cmd value for
282 * SIOCGNATL, so at least we can detect it. We could
283 * put something in configure and use ifdefs here, but
284 * this seems simpler.
286 static int siocgnatl_cmd
= SIOCGNATL
& 0xff;
287 if (63 == siocgnatl_cmd
) {
288 struct natlookup
*nlp
= &natLookup
;
289 x
= ioctl(natfd
, SIOCGNATL
, &nlp
);
291 x
= ioctl(natfd
, SIOCGNATL
, &natLookup
);
294 #endif /* defined(IPFILTER_VERSION) ... */
296 const auto xerrno
= errno
;
297 if (xerrno
!= ESRCH
) {
298 debugs(89, DBG_IMPORTANT
, "ERROR: IPF (IPFilter) NAT lookup failed: ioctl(SIOCGNATL) (v=" << IPFILTER_VERSION
<< "): " << xstrerr(xerrno
));
303 debugs(89, 9, "address: " << newConn
);
306 #if HAVE_STRUCT_NATLOOKUP_NL_REALIPADDR_IN6
307 if (newConn
->remote
.isIPv6())
308 newConn
->local
= natLookup
.nl_realipaddr
.in6
;
310 newConn
->local
= natLookup
.nl_realipaddr
.in4
;
312 newConn
->local
= natLookup
.nl_realip
;
314 newConn
->local
.port(ntohs(natLookup
.nl_realport
));
315 debugs(89, 5, "address NAT: " << newConn
);
321 #endif /* --enable-ipf-transparent */
326 Ip::Intercept::PfInterception(const Comm::ConnectionPointer
&newConn
)
328 #if PF_TRANSPARENT /* --enable-pf-transparent */
331 return UseInterceptionAddressesLookedUpEarlier("recent PF version", newConn
);
333 #else /* USE_NAT_DEVPF / --with-nat-devpf */
335 struct pfioc_natlook nl
;
336 static int pffd
= -1;
339 pffd
= xopen("/dev/pf", O_RDONLY
);
342 const auto xerrno
= errno
;
343 debugs(89, DBG_IMPORTANT
, "ERROR: PF open failed: " << xstrerr(xerrno
));
347 memset(&nl
, 0, sizeof(struct pfioc_natlook
));
349 if (newConn
->remote
.isIPv6()) {
350 newConn
->remote
.getInAddr(nl
.saddr
.v6
);
351 newConn
->local
.getInAddr(nl
.daddr
.v6
);
354 newConn
->remote
.getInAddr(nl
.saddr
.v4
);
355 newConn
->local
.getInAddr(nl
.daddr
.v4
);
359 nl
.sport
= htons(newConn
->remote
.port());
360 nl
.dport
= htons(newConn
->local
.port());
362 nl
.proto
= IPPROTO_TCP
;
363 nl
.direction
= PF_OUT
;
365 if (ioctl(pffd
, DIOCNATLOOK
, &nl
)) {
366 const auto xerrno
= errno
;
367 if (xerrno
!= ENOENT
) {
368 debugs(89, DBG_IMPORTANT
, "ERROR: PF lookup failed: ioctl(DIOCNATLOOK): " << xstrerr(xerrno
));
372 debugs(89, 9, "address: " << newConn
);
375 if (newConn
->remote
.isIPv6())
376 newConn
->local
= nl
.rdaddr
.v6
;
378 newConn
->local
= nl
.rdaddr
.v4
;
379 newConn
->local
.port(ntohs(nl
.rdport
));
380 debugs(89, 5, "address NAT: " << newConn
);
383 #endif /* --with-nat-devpf */
386 #endif /* --enable-pf-transparent */
391 Ip::Intercept::LookupNat(const Comm::Connection
&aConn
)
393 debugs(89, 5, "address BEGIN: me/client= " << aConn
.local
<< ", destination/me= " << aConn
.remote
);
394 assert(interceptActive_
);
396 Comm::ConnectionPointer newConn
= &aConn
;
397 return NetfilterInterception(newConn
) || IpfwInterception(newConn
) || // use sock-opts to return client address
398 PfInterception(newConn
) || IpfInterception(newConn
); // use ioctl to return client address AND destination address
402 Ip::Intercept::ProbeForTproxy(Ip::Address
&test
)
404 bool doneSuid
= false;
406 #if _SQUID_LINUX_ && defined(IP_TRANSPARENT) // Linux
407 # define soLevel SOL_IP
408 # define soFlag IP_TRANSPARENT
410 #elif defined(SO_BINDANY) // OpenBSD 4.7+ and NetBSD with PF
411 # define soLevel SOL_SOCKET
412 # define soFlag SO_BINDANY
416 #elif defined(IP_BINDANY) // FreeBSD with IPFW
417 # define soLevel IPPROTO_IP
418 # define soFlag IP_BINDANY
424 #if defined(soLevel) && defined(soFlag)
426 debugs(3, 3, "Detect TPROXY support on port " << test
);
428 if (!Ip::EnableIpv6
&& test
.isIPv6() && !test
.setIPv4()) {
429 debugs(3, DBG_CRITICAL
, "Cannot use TPROXY for " << test
<< " because IPv6 support is disabled");
438 /* Probe to see if the Kernel TPROXY support is IPv6-enabled */
440 debugs(3, 3, "...Probing for IPv6 TPROXY support.");
442 struct sockaddr_in6 tmp_ip6
;
443 Ip::Address tmp
= "::2";
445 tmp
.getSockAddr(tmp_ip6
);
447 if ( (tmp_sock
= xsocket(PF_INET6
, SOCK_STREAM
, IPPROTO_TCP
)) >= 0 &&
448 xsetsockopt(tmp_sock
, soLevel
, soFlag
, &tos
, sizeof(int)) == 0 &&
449 xbind(tmp_sock
, (struct sockaddr
*)&tmp_ip6
, sizeof(struct sockaddr_in6
)) == 0 ) {
451 debugs(3, 3, "IPv6 TPROXY support detected. Using.");
463 if ( test
.isIPv6() && !test
.setIPv4() ) {
464 debugs(3, DBG_CRITICAL
, "TPROXY lacks IPv6 support for " << test
);
470 /* Probe to see if the Kernel TPROXY support is IPv4-enabled (aka present) */
472 debugs(3, 3, "...Probing for IPv4 TPROXY support.");
474 struct sockaddr_in tmp_ip4
;
475 Ip::Address tmp
= "127.0.0.2";
477 tmp
.getSockAddr(tmp_ip4
);
479 if ( (tmp_sock
= xsocket(PF_INET
, SOCK_STREAM
, IPPROTO_TCP
)) >= 0 &&
480 xsetsockopt(tmp_sock
, soLevel
, soFlag
, &tos
, sizeof(int)) == 0 &&
481 xbind(tmp_sock
, (struct sockaddr
*)&tmp_ip4
, sizeof(struct sockaddr_in
)) == 0 ) {
483 debugs(3, 3, "IPv4 TPROXY support detected. Using.");
495 debugs(3, 3, "TPROXY setsockopt() not supported on this platform. Disabling TPROXY on port " << test
);