netinet*: Fix redirects for connections from localhost

Redirect rules use PFIL_IN and PFIL_OUT events to allow packet filter
rules to change the destination address and port for a connection.
Typically, the rule triggers on an input event when a packet is received
by a router and the destination address and/or port is changed to
implement the redirect. When a reply packet on this connection is output
to the network, the rule triggers again, reversing the modification.

When the connection is initiated on the same host as the packet filter,
it is initially output via lo0 which queues it for input processing.
This causes an input event on the lo0 interface, allowing redirect
processing to rewrite the destination and create state for the
connection. However, when the reply is received, no corresponding output
event is generated; instead, the packet is delivered to the higher level
protocol (e.g. tcp or udp) without reversing the redirect, the reply is
not matched to the connection and the packet is dropped (for tcp, a
connection reset is also sent).

This commit fixes the problem by adding a second packet filter call in
the input path. The second call happens right before the handoff to
higher level processing and provides the missing output event to allow
the redirect's reply processing to perform its rewrite. This extra
processing is disabled by default and can be enabled using pfilctl:

	pfilctl link -o pf:default-out inet-local
	pfilctl link -o pf:default-out6 inet6-local

PR:		268717
Reviewed-by:	kp, melifaro
MFC-after:	2 weeks
Differential Revision: https://reviews.freebsd.org/D40256
This commit is contained in:
Doug Rabson 2023-05-24 14:11:37 +01:00
parent dc10368634
commit 5ab151574c
8 changed files with 127 additions and 12 deletions

View file

@ -136,7 +136,9 @@ SYSCTL_BOOL(_net_inet_ip, OID_AUTO, source_address_validation,
CTLFLAG_VNET | CTLFLAG_RW, &VNET_NAME(ip_sav), true,
"Drop incoming packets with source address that is a local address");
VNET_DEFINE(pfil_head_t, inet_pfil_head); /* Packet filter hooks */
/* Packet filter hooks */
VNET_DEFINE(pfil_head_t, inet_pfil_head);
VNET_DEFINE(pfil_head_t, inet_local_pfil_head);
static struct netisr_handler ip_nh = {
.nh_name = "ip",
@ -327,6 +329,10 @@ ip_vnet_init(void *arg __unused)
args.pa_headname = PFIL_INET_NAME;
V_inet_pfil_head = pfil_head_register(&args);
args.pa_flags = PFIL_OUT;
args.pa_headname = PFIL_INET_LOCAL_NAME;
V_inet_local_pfil_head = pfil_head_register(&args);
if (hhook_head_register(HHOOK_TYPE_IPSEC_IN, AF_INET,
&V_ipsec_hhh_in[HHOOK_IPSEC_INET],
HHOOK_WAITOK | HHOOK_HEADISINVNET) != 0)
@ -816,6 +822,20 @@ ours:
return;
#endif /* IPSTEALTH */
/*
* We are going to ship the packet to the local protocol stack. Call the
* filter again for this 'output' action, allowing redirect-like rules
* to adjust the source address.
*/
if (PFIL_HOOKED_OUT(V_inet_local_pfil_head)) {
if (pfil_mbuf_out(V_inet_local_pfil_head, &m, V_loif, NULL) !=
PFIL_PASS)
return;
if (m == NULL) /* consumed by filter */
return;
ip = mtod(m, struct ip *);
}
/*
* Attempt reassembly; if it succeeds, proceed.
* ip_reass() will return a different mbuf.

View file

@ -255,6 +255,10 @@ VNET_DECLARE(struct pfil_head *, inet_pfil_head);
#define V_inet_pfil_head VNET(inet_pfil_head)
#define PFIL_INET_NAME "inet"
VNET_DECLARE(struct pfil_head *, inet_local_pfil_head);
#define V_inet_local_pfil_head VNET(inet_local_pfil_head)
#define PFIL_INET_LOCAL_NAME "inet-local"
void in_delayed_cksum(struct mbuf *m);
/* Hooks for ipfw, dummynet, divert etc. Most are declared in raw_ip.c */

View file

@ -208,6 +208,7 @@ SYSCTL_PROC(_net_inet6_ip6, IPV6CTL_INTRDQMAXLEN, intr_direct_queue_maxlen,
#endif
VNET_DEFINE(pfil_head_t, inet6_pfil_head);
VNET_DEFINE(pfil_head_t, inet6_local_pfil_head);
VNET_PCPUSTAT_DEFINE(struct ip6stat, ip6stat);
VNET_PCPUSTAT_SYSINIT(ip6stat);
@ -245,6 +246,10 @@ ip6_vnet_init(void *arg __unused)
args.pa_headname = PFIL_INET6_NAME;
V_inet6_pfil_head = pfil_head_register(&args);
args.pa_flags = PFIL_OUT;
args.pa_headname = PFIL_INET6_LOCAL_NAME;
V_inet6_local_pfil_head = pfil_head_register(&args);
if (hhook_head_register(HHOOK_TYPE_IPSEC_IN, AF_INET6,
&V_ipsec_hhh_in[HHOOK_IPSEC_INET6],
HHOOK_WAITOK | HHOOK_HEADISINVNET) != 0)
@ -884,6 +889,20 @@ passin:
return;
}
/*
* We are going to ship the packet to the local protocol stack. Call the
* filter again for this 'output' action, allowing redirect-like rules
* to adjust the source address.
*/
if (PFIL_HOOKED_OUT(V_inet6_local_pfil_head)) {
if (pfil_mbuf_out(V_inet6_local_pfil_head, &m, V_loif, NULL) !=
PFIL_PASS)
return;
if (m == NULL) /* consumed by filter */
return;
ip6 = mtod(m, struct ip6_hdr *);
}
/*
* Tell launch routine the next header
*/

View file

@ -325,6 +325,10 @@ VNET_DECLARE(struct pfil_head *, inet6_pfil_head);
#define V_inet6_pfil_head VNET(inet6_pfil_head)
#define PFIL_INET6_NAME "inet6"
VNET_DECLARE(struct pfil_head *, inet6_local_pfil_head);
#define V_inet6_local_pfil_head VNET(inet6_local_pfil_head)
#define PFIL_INET6_LOCAL_NAME "inet6-local"
#ifdef IPSTEALTH
VNET_DECLARE(int, ip6stealth);
#define V_ip6stealth VNET(ip6stealth)

View file

@ -9,6 +9,7 @@ ATF_TESTS_SH+= \
dummynet \
pass_block \
nat \
rdr \
tos \
fragments \
forward

View file

@ -26,17 +26,19 @@
# SUCH DAMAGE.
. $(atf_get_srcdir)/utils.subr
. $(atf_get_srcdir)/runner.subr
atf_test_case "basic" "cleanup"
basic_head()
{
atf_set descr 'Basic rdr test'
atf_set descr 'Basic IPv4 NAT test'
atf_set require.user root
}
basic_body()
{
pft_init
firewall=$1
firewall_init $firewall
nat_init $firewall
epair=$(vnet_mkepair)
@ -48,10 +50,13 @@ basic_body()
jexec alcatraz ifconfig ${epair}b 192.0.2.1/24 up
jexec alcatraz sysctl net.inet.ip.forwarding=1
# Enable pf!
jexec alcatraz pfctl -e
pft_set_rules alcatraz \
"rdr pass on ${epair}b proto tcp from any to 198.51.100.0/24 port 1234 -> 192.0.2.1 port 4321"
# Enable redirect filter rule
firewall_config alcatraz ${firewall} \
"pf" \
"rdr pass on ${epair}b proto tcp from any to 198.51.100.0/24 port 1234 -> 192.0.2.1 port 4321" \
"ipfnat" \
"rdr ${epair}b from any to 198.51.100.0/24 port = 1234 -> 192.0.2.1 port 4321 tcp"
echo "foo" | jexec alcatraz nc -N -l 4321 &
sleep 1
@ -64,10 +69,69 @@ basic_body()
basic_cleanup()
{
pft_cleanup
firewall=$1
firewall_cleanup $firewall
}
atf_init_test_cases()
local_redirect_head()
{
atf_add_test_case "basic"
atf_set descr 'Redirect local traffic test'
atf_set require.user root
}
local_redirect_body()
{
firewall=$1
firewall_init $firewall
nat_init $firewall
bridge=$(vnet_mkbridge)
ifconfig ${bridge} 192.0.2.1/24 up
epair1=$(vnet_mkepair)
epair2=$(vnet_mkepair)
vnet_mkjail first ${epair1}b
ifconfig ${epair1}a up
ifconfig ${bridge} addm ${epair1}a
jexec first ifconfig ${epair1}b 192.0.2.2/24 up
jexec first ifconfig lo0 127.0.0.1/8 up
vnet_mkjail second ${epair2}b
ifconfig ${epair2}a up
ifconfig ${bridge} addm ${epair2}a
jexec second ifconfig ${epair2}b 192.0.2.3/24 up
jexec second ifconfig lo0 127.0.0.1/8 up
jexec second sysctl net.inet.ip.forwarding=1
# Enable redirect filter rule
firewall_config second ${firewall} \
"pf" \
"rdr pass proto tcp from any to 192.0.2.3/24 port 1234 -> 192.0.2.2 port 4321" \
"ipfnat" \
"rdr '*' from any to 192.0.2.3/24 port = 1234 -> 192.0.2.2 port 4321 tcp"
echo "foo" | jexec first nc -N -l 4321 &
sleep 1
# Verify that second can use its rule to redirect local connections to first
result=$(jexec second nc -N -w 3 192.0.2.3 1234)
if [ "$result" != "foo" ]; then
atf_fail "Redirect failed"
fi
}
local_redirect_cleanup()
{
firewall=$1
firewall_cleanup $firewall
}
setup_tests \
basic \
pf \
ipfnat \
local_redirect \
pf \
ipfnat

View file

@ -58,12 +58,16 @@ firewall_config()
jexec ${jname} pfctl -e
jexec ${jname} pfctl -F all
jexec ${jname} pfctl -f $cwd/pf.rule
jexec ${jname} pfilctl link -o pf:default-out inet-local
jexec ${jname} pfilctl link -o pf:default-out6 inet6-local
elif [ ${fw} == "ipf" ]; then
jexec ${jname} ipf -E
jexec ${jname} ipf -Fa -f $cwd/ipf.rule
elif [ ${fw} == "ipfnat" ]; then
jexec ${jname} service ipfilter start
jexec ${jname} ipnat -CF -f $cwd/ipfnat.rule
jexec ${jname} pfilctl link -o ipfilter:default-ip4 inet-local
jexec ${jname} pfilctl link -o ipfilter:default-ip6 inet6-local
else
atf_fail "$fw is not a valid firewall to configure"
fi

View file

@ -25,7 +25,6 @@ ATF_TESTS_SH+= altq \
pfsync \
prio \
proxy \
rdr \
ridentifier \
route_to \
rtable \