git: aac676988d91 - releng/14.0 - pf: stricter state checking for ICMP and ICMPv6 packets

From: Mark Johnston <markj_at_FreeBSD.org>
Date: Wed, 07 Aug 2024 13:44:43 UTC
The branch releng/14.0 has been updated by markj:

URL: https://cgit.FreeBSD.org/src/commit/?id=aac676988d91aeb89b91fd00d4a5f4e23445fce6

commit aac676988d91aeb89b91fd00d4a5f4e23445fce6
Author:     Kristof Provost <kp@FreeBSD.org>
AuthorDate: 2024-07-09 13:59:33 +0000
Commit:     Mark Johnston <markj@FreeBSD.org>
CommitDate: 2024-08-07 13:31:25 +0000

    pf: stricter state checking for ICMP and ICMPv6 packets
    
    Include
    the ICMP type in one port of the state key, using the type to determine which
    side should be the id, and which should be the type.
    
    Also:
    - Handle ICMP6 messages which are typically sent to multicast addresses but
     recieve unicast replies, by doing fallthrough lookups against the correct
     multicast address.
    - Clear up some mistaken assumptions in the PF code:
      - Not all ICMP packets have an icmp_id, so simulate one based on other
        data if we can, otherwise set it to 0.
     - Don't modify the icmp id field in NAT unless it's echo
     - Use the full range of possible id's when NATing icmp6 echoy
    
    ok henning marco
    testing matthieu todd
    
    Approved by:    so
    Security:       FreeBSD-SA-24:05.pf
    Security:       CVE-2024-6640
    MFC after:      1 day
    Obtained From:  OpenBSD, mcbride <mcbride@openbsd.org> 70bf7555ef4c
    Sponsored by:   Rubicon Communications, LLC ("Netgate")
    
    (cherry picked from commit 534ee17e61ee094ec175703bc50e88ff6587703e)
    (cherry picked from commit 2f6b4611b5b847aee1ff8d5017a0f8a657f4101d)
---
 sys/netpfil/pf/pf.c    | 381 ++++++++++++++++++++++++++++++++++++++-----------
 sys/netpfil/pf/pf_lb.c |  19 ++-
 2 files changed, 317 insertions(+), 83 deletions(-)

diff --git a/sys/netpfil/pf/pf.c b/sys/netpfil/pf/pf.c
index eab79387e867..0491cd7369f5 100644
--- a/sys/netpfil/pf/pf.c
+++ b/sys/netpfil/pf/pf.c
@@ -260,6 +260,8 @@ static void		 pf_change_ap(struct mbuf *, struct pf_addr *, u_int16_t *,
 			    u_int16_t, u_int8_t, sa_family_t);
 static int		 pf_modulate_sack(struct mbuf *, int, struct pf_pdesc *,
 			    struct tcphdr *, struct pf_state_peer *);
+int			 pf_icmp_mapping(struct pf_pdesc *, u_int8_t, int *,
+			    int *, u_int16_t *, u_int16_t *);
 static void		 pf_change_icmp(struct pf_addr *, u_int16_t *,
 			    struct pf_addr *, struct pf_addr *, u_int16_t,
 			    u_int16_t *, u_int16_t *, u_int16_t *,
@@ -304,6 +306,10 @@ static int		 pf_test_state_tcp(struct pf_kstate **,
 static int		 pf_test_state_udp(struct pf_kstate **,
 			    struct pfi_kkif *, struct mbuf *, int,
 			    void *, struct pf_pdesc *);
+int			 pf_icmp_state_lookup(struct pf_state_key_cmp *,
+			    struct pf_pdesc *, struct pf_kstate **, struct mbuf *,
+			    int, struct pfi_kkif *, u_int16_t, u_int16_t,
+			    int, int *, int);
 static int		 pf_test_state_icmp(struct pf_kstate **,
 			    struct pfi_kkif *, struct mbuf *, int,
 			    void *, struct pf_pdesc *, u_short *);
@@ -353,6 +359,8 @@ extern struct proc *pf_purge_proc;
 
 VNET_DEFINE(struct pf_limit, pf_limits[PF_LIMIT_MAX]);
 
+enum { PF_ICMP_MULTI_NONE, PF_ICMP_MULTI_SOLICITED, PF_ICMP_MULTI_LINK };
+
 #define	PACKET_UNDO_NAT(_m, _pd, _off, _s)		\
 	do {								\
 		struct pf_state_key *nk;				\
@@ -1695,6 +1703,142 @@ pf_isforlocal(struct mbuf *m, int af)
 	return (false);
 }
 
+int
+pf_icmp_mapping(struct pf_pdesc *pd, u_int8_t type,
+    int *icmp_dir, int *multi, u_int16_t *icmpid, u_int16_t *icmptype)
+{
+	/*
+	 * ICMP types marked with PF_OUT are typically responses to
+	 * PF_IN, and will match states in the opposite direction.
+	 * PF_IN ICMP types need to match a state with that type.
+	 */
+	*icmp_dir = PF_OUT;
+	*multi = PF_ICMP_MULTI_LINK;
+	/* Queries (and responses) */
+	switch (type) {
+	case ICMP_ECHO:
+		*icmp_dir = PF_IN;
+	case ICMP_ECHOREPLY:
+		*icmptype = ICMP_ECHO;
+		*icmpid = pd->hdr.icmp.icmp_id;
+		break;
+
+	case ICMP_TSTAMP:
+		*icmp_dir = PF_IN;
+	case ICMP_TSTAMPREPLY:
+		*icmptype = ICMP_TSTAMP;
+		*icmpid = 0; /* Time is not a secret. */
+		break;
+
+	case ICMP_IREQ:
+		*icmp_dir = PF_IN;
+	case ICMP_IREQREPLY:
+		*icmptype = ICMP_IREQ;
+		*icmpid = 0; /* Nothing sane to match on! */
+		break;
+
+	case ICMP_MASKREQ:
+		*icmp_dir = PF_IN;
+	case ICMP_MASKREPLY:
+		*icmptype = ICMP_MASKREQ;
+		*icmpid = 0; /* Nothing sane to match on! */
+		break;
+
+	case ICMP_IPV6_WHEREAREYOU:
+		*icmp_dir = PF_IN;
+	case ICMP_IPV6_IAMHERE:
+		*icmptype = ICMP_IPV6_WHEREAREYOU;
+		*icmpid = 0; /* Nothing sane to match on! */
+		break;
+
+	case ICMP_MOBILE_REGREQUEST:
+		*icmp_dir = PF_IN;
+	case ICMP_MOBILE_REGREPLY:
+		*icmptype = ICMP_MOBILE_REGREQUEST;
+		*icmpid = 0; /* Nothing sane to match on! */
+		break;
+
+	case ICMP_ROUTERSOLICIT:
+		*icmp_dir = PF_IN;
+	case ICMP_ROUTERADVERT:
+		*icmptype = ICMP_ROUTERSOLICIT;
+		*icmpid = 0; /* Nothing sane to match on! */
+		break;
+
+#ifdef INET6
+	case ICMP6_ECHO_REQUEST:
+		*icmp_dir = PF_IN;
+	case ICMP6_ECHO_REPLY:
+		*icmptype = ICMP6_ECHO_REQUEST;
+		*icmpid = pd->hdr.icmp6.icmp6_id;
+		break;
+
+	case MLD_LISTENER_QUERY:
+		*icmp_dir = PF_IN;
+	case MLD_LISTENER_REPORT: {
+		*icmptype = MLD_LISTENER_QUERY;
+		*icmpid = 0;
+		break;
+	}
+
+	/* ICMP6_FQDN and ICMP6_NI query/reply are the same type as ICMP6_WRU */
+	case ICMP6_WRUREQUEST:
+		*icmp_dir = PF_IN;
+	case ICMP6_WRUREPLY:
+		*icmptype = ICMP6_WRUREQUEST;
+		*icmpid = 0; /* Nothing sane to match on! */
+		break;
+
+	case MLD_MTRACE:
+		*icmp_dir = PF_IN;
+	case MLD_MTRACE_RESP:
+		*icmptype = MLD_MTRACE;
+		*icmpid = 0; /* Nothing sane to match on! */
+		break;
+
+	case ND_NEIGHBOR_SOLICIT:
+		*icmp_dir = PF_IN;
+	case ND_NEIGHBOR_ADVERT: {
+		*icmptype = ND_NEIGHBOR_SOLICIT;
+		*multi = PF_ICMP_MULTI_SOLICITED;
+		*icmpid = 0;
+		break;
+	}
+
+#endif /* INET6 */
+	/* These ICMP types map to other connections */
+	case ICMP_UNREACH:
+	case ICMP_SOURCEQUENCH:
+	case ICMP_REDIRECT:
+	case ICMP_TIMXCEED:
+	case ICMP_PARAMPROB:
+#ifdef INET6
+	/*
+	 * ICMP6_TIME_EXCEEDED is the same type as ICMP_UNREACH
+	 * ND_REDIRECT can't be in this list because the triggering packet
+	 * header is optional.
+	 */
+	case ICMP6_PACKET_TOO_BIG:
+#endif /* INET6 */
+		/* These will not be used, but set them anyways */
+		*icmp_dir = PF_IN;
+		*icmptype = htons(type);
+		*icmpid = 0;
+		return (1);	/* These types are matched to other state */
+	/*
+	 * All remaining ICMP types get their own states,
+	 * and will only match in one direction.
+	 */
+	default:
+		*icmp_dir = PF_IN;
+		*icmptype = type;
+		*icmpid = 0;
+		break;
+	}
+	HTONS(*icmptype);
+	return (0);
+}
+
 void
 pf_intr(void *v)
 {
@@ -4353,8 +4497,8 @@ pf_test_rule(struct pf_krule **rm, struct pf_kstate **sm, struct pfi_kkif *kif,
 	int			 tag = -1;
 	int			 asd = 0;
 	int			 match = 0;
-	int			 state_icmp = 0;
-	u_int16_t		 sport = 0, dport = 0;
+	int			 state_icmp = 0, icmp_dir, multi;
+	u_int16_t		 sport = 0, dport = 0, virtual_type, virtual_id;
 	u_int16_t		 bproto_sum = 0, bip_sum = 0;
 	u_int8_t		 icmptype = 0, icmpcode = 0;
 	struct pf_kanchor_stackframe	anchor_stack[PF_ANCHOR_STACKSIZE];
@@ -4388,33 +4532,37 @@ pf_test_rule(struct pf_krule **rm, struct pf_kstate **sm, struct pfi_kkif *kif,
 	case IPPROTO_ICMP:
 		if (pd->af != AF_INET)
 			break;
-		sport = dport = pd->hdr.icmp.icmp_id;
 		hdrlen = sizeof(pd->hdr.icmp);
 		icmptype = pd->hdr.icmp.icmp_type;
 		icmpcode = pd->hdr.icmp.icmp_code;
-
-		if (icmptype == ICMP_UNREACH ||
-		    icmptype == ICMP_SOURCEQUENCH ||
-		    icmptype == ICMP_REDIRECT ||
-		    icmptype == ICMP_TIMXCEED ||
-		    icmptype == ICMP_PARAMPROB)
-			state_icmp++;
+		state_icmp = pf_icmp_mapping(pd, icmptype,
+		    &icmp_dir, &multi, &virtual_id, &virtual_type);
+		if (icmp_dir == PF_IN) {
+			sport = virtual_id;
+			dport = virtual_type;
+		} else {
+			sport = virtual_type;
+			dport = virtual_id;
+		}
 		break;
 #endif /* INET */
 #ifdef INET6
 	case IPPROTO_ICMPV6:
 		if (af != AF_INET6)
 			break;
-		sport = dport = pd->hdr.icmp6.icmp6_id;
 		hdrlen = sizeof(pd->hdr.icmp6);
 		icmptype = pd->hdr.icmp6.icmp6_type;
 		icmpcode = pd->hdr.icmp6.icmp6_code;
+		state_icmp = pf_icmp_mapping(pd, icmptype,
+		    &icmp_dir, &multi, &virtual_id, &virtual_type);
+		if (icmp_dir == PF_IN) {
+			sport = virtual_id;
+			dport = virtual_type;
+		} else {
+			sport = virtual_type;
+			dport = virtual_id;
+		}
 
-		if (icmptype == ICMP6_DST_UNREACH ||
-		    icmptype == ICMP6_PACKET_TOO_BIG ||
-		    icmptype == ICMP6_TIME_EXCEEDED ||
-		    icmptype == ICMP6_PARAM_PROB)
-			state_icmp++;
 		break;
 #endif /* INET6 */
 	default:
@@ -4508,7 +4656,6 @@ pf_test_rule(struct pf_krule **rm, struct pf_kstate **sm, struct pfi_kkif *kif,
 		}
 #ifdef INET
 		case IPPROTO_ICMP:
-			nk->port[0] = nk->port[1];
 			if (PF_ANEQ(saddr, &nk->addr[pd->sidx], AF_INET))
 				pf_change_a(&saddr->v4.s_addr, pd->ip_sum,
 				    nk->addr[pd->sidx].v4.s_addr, 0);
@@ -4517,11 +4664,12 @@ pf_test_rule(struct pf_krule **rm, struct pf_kstate **sm, struct pfi_kkif *kif,
 				pf_change_a(&daddr->v4.s_addr, pd->ip_sum,
 				    nk->addr[pd->didx].v4.s_addr, 0);
 
-			if (nk->port[1] != pd->hdr.icmp.icmp_id) {
+			if (virtual_type == ICMP_ECHO &&
+			     nk->port[pd->sidx] != pd->hdr.icmp.icmp_id) {
 				pd->hdr.icmp.icmp_cksum = pf_cksum_fixup(
 				    pd->hdr.icmp.icmp_cksum, sport,
-				    nk->port[1], 0);
-				pd->hdr.icmp.icmp_id = nk->port[1];
+				    nk->port[pd->sidx], 0);
+				pd->hdr.icmp.icmp_id = nk->port[pd->sidx];
 				pd->sport = &pd->hdr.icmp.icmp_id;
 			}
 			m_copyback(m, off, ICMP_MINLEN, (caddr_t)&pd->hdr.icmp);
@@ -4529,7 +4677,6 @@ pf_test_rule(struct pf_krule **rm, struct pf_kstate **sm, struct pfi_kkif *kif,
 #endif /* INET */
 #ifdef INET6
 		case IPPROTO_ICMPV6:
-			nk->port[0] = nk->port[1];
 			if (PF_ANEQ(saddr, &nk->addr[pd->sidx], AF_INET6))
 				pf_change_a6(saddr, &pd->hdr.icmp6.icmp6_cksum,
 				    &nk->addr[pd->sidx], 0);
@@ -5911,15 +6058,73 @@ pf_test_state_sctp(struct pf_kstate **state, struct pfi_kkif *kif,
 	return (PF_PASS);
 }
 
+int
+pf_icmp_state_lookup(struct pf_state_key_cmp *key, struct pf_pdesc *pd,
+    struct pf_kstate **state, struct mbuf *m, int direction, struct pfi_kkif *kif,
+    u_int16_t icmpid, u_int16_t type, int icmp_dir, int *iidx, int multi)
+{
+	key->af = pd->af;
+	key->proto = pd->proto;
+	if (icmp_dir == PF_IN) {
+		*iidx = pd->sidx;
+		key->port[pd->sidx] = icmpid;
+		key->port[pd->didx] = type;
+	} else {
+		*iidx = pd->didx;
+		key->port[pd->sidx] = type;
+		key->port[pd->didx] = icmpid;
+	}
+	if (pd->af == AF_INET6 && multi != PF_ICMP_MULTI_NONE) {
+		switch (multi) {
+		case PF_ICMP_MULTI_SOLICITED:
+			key->addr[pd->sidx].addr32[0] = IPV6_ADDR_INT32_MLL;
+			key->addr[pd->sidx].addr32[1] = 0;
+			key->addr[pd->sidx].addr32[2] = IPV6_ADDR_INT32_ONE;
+			key->addr[pd->sidx].addr32[3] = pd->src->addr32[3];
+			key->addr[pd->sidx].addr8[12] = 0xff;
+			break;
+		case PF_ICMP_MULTI_LINK:
+			key->addr[pd->sidx].addr32[0] = IPV6_ADDR_INT32_MLL;
+			key->addr[pd->sidx].addr32[1] = 0;
+			key->addr[pd->sidx].addr32[2] = 0;
+			key->addr[pd->sidx].addr32[3] = IPV6_ADDR_INT32_ONE;
+			break;
+		}
+	} else
+		PF_ACPY(&key->addr[pd->sidx], pd->src, key->af);
+	PF_ACPY(&key->addr[pd->didx], pd->dst, key->af);
+
+	STATE_LOOKUP(kif, key, *state, pd);
+
+	/* Is this ICMP message flowing in right direction? */
+	if ((*state)->rule.ptr->type &&
+	    (((*state)->direction == direction) ?
+	    PF_IN : PF_OUT) != icmp_dir) {
+		if (V_pf_status.debug >= PF_DEBUG_MISC) {
+			printf("pf: icmp type %d in wrong direction (%d): ",
+			    icmp_dir, pd->dir);
+			pf_print_state(*state);
+			printf("\n");
+		}
+		return (PF_DROP);
+	}
+	return (-1);
+}
+
 static int
 pf_test_state_icmp(struct pf_kstate **state, struct pfi_kkif *kif,
     struct mbuf *m, int off, void *h, struct pf_pdesc *pd, u_short *reason)
 {
 	struct pf_addr  *saddr = pd->src, *daddr = pd->dst;
-	u_int16_t	 icmpid = 0, *icmpsum;
+	u_int16_t	*icmpsum, virtual_id, virtual_type;
 	u_int8_t	 icmptype, icmpcode;
-	int		 state_icmp = 0;
+	int		 icmp_dir, iidx, ret, multi;
 	struct pf_state_key_cmp key;
+#ifdef INET
+	u_int16_t	 icmpid;
+#endif
+
+	MPASS(*state == NULL);
 
 	bzero(&key, sizeof(key));
 	switch (pd->proto) {
@@ -5929,49 +6134,43 @@ pf_test_state_icmp(struct pf_kstate **state, struct pfi_kkif *kif,
 		icmpcode = pd->hdr.icmp.icmp_code;
 		icmpid = pd->hdr.icmp.icmp_id;
 		icmpsum = &pd->hdr.icmp.icmp_cksum;
-
-		if (icmptype == ICMP_UNREACH ||
-		    icmptype == ICMP_SOURCEQUENCH ||
-		    icmptype == ICMP_REDIRECT ||
-		    icmptype == ICMP_TIMXCEED ||
-		    icmptype == ICMP_PARAMPROB)
-			state_icmp++;
 		break;
 #endif /* INET */
 #ifdef INET6
 	case IPPROTO_ICMPV6:
 		icmptype = pd->hdr.icmp6.icmp6_type;
 		icmpcode = pd->hdr.icmp6.icmp6_code;
+#ifdef INET
 		icmpid = pd->hdr.icmp6.icmp6_id;
+#endif
 		icmpsum = &pd->hdr.icmp6.icmp6_cksum;
-
-		if (icmptype == ICMP6_DST_UNREACH ||
-		    icmptype == ICMP6_PACKET_TOO_BIG ||
-		    icmptype == ICMP6_TIME_EXCEEDED ||
-		    icmptype == ICMP6_PARAM_PROB)
-			state_icmp++;
 		break;
 #endif /* INET6 */
 	}
 
-	if (!state_icmp) {
+	if (pf_icmp_mapping(pd, icmptype, &icmp_dir, &multi,
+	    &virtual_id, &virtual_type) == 0) {
 		/*
 		 * ICMP query/reply message not related to a TCP/UDP packet.
 		 * Search for an ICMP state.
 		 */
-		key.af = pd->af;
-		key.proto = pd->proto;
-		key.port[0] = key.port[1] = icmpid;
-		if (pd->dir == PF_IN)	{	/* wire side, straight */
-			PF_ACPY(&key.addr[0], pd->src, key.af);
-			PF_ACPY(&key.addr[1], pd->dst, key.af);
-		} else {			/* stack side, reverse */
-			PF_ACPY(&key.addr[1], pd->src, key.af);
-			PF_ACPY(&key.addr[0], pd->dst, key.af);
+		ret = pf_icmp_state_lookup(&key, pd, state, m, pd->dir,
+		    kif, virtual_id, virtual_type, icmp_dir, &iidx,
+		    PF_ICMP_MULTI_NONE);
+		if (ret >= 0) {
+			if (ret == PF_DROP && pd->af == AF_INET6 &&
+			    icmp_dir == PF_OUT) {
+				if (*state != NULL)
+					PF_STATE_UNLOCK((*state));
+				ret = pf_icmp_state_lookup(&key, pd, state, m,
+				    pd->dir, kif, virtual_id, virtual_type,
+				    icmp_dir, &iidx, multi);
+				if (ret >= 0)
+					return (ret);
+			} else
+				return (ret);
 		}
 
-		STATE_LOOKUP(kif, &key, *state, pd);
-
 		(*state)->expire = time_uptime;
 		(*state)->timeout = PFTM_ICMP_ERROR_REPLY;
 
@@ -5994,14 +6193,14 @@ pf_test_state_icmp(struct pf_kstate **state, struct pfi_kkif *kif,
 					    pd->ip_sum,
 					    nk->addr[pd->didx].v4.s_addr, 0);
 
-				if (nk->port[0] !=
+				if (nk->port[iidx] !=
 				    pd->hdr.icmp.icmp_id) {
 					pd->hdr.icmp.icmp_cksum =
 					    pf_cksum_fixup(
 					    pd->hdr.icmp.icmp_cksum, icmpid,
-					    nk->port[pd->sidx], 0);
+					    nk->port[iidx], 0);
 					pd->hdr.icmp.icmp_id =
-					    nk->port[pd->sidx];
+					    nk->port[iidx];
 				}
 
 				m_copyback(m, off, ICMP_MINLEN,
@@ -6366,13 +6565,15 @@ pf_test_state_icmp(struct pf_kstate **state, struct pfi_kkif *kif,
 				return (PF_DROP);
 			}
 
-			key.af = pd2.af;
-			key.proto = IPPROTO_ICMP;
-			PF_ACPY(&key.addr[pd2.sidx], pd2.src, key.af);
-			PF_ACPY(&key.addr[pd2.didx], pd2.dst, key.af);
-			key.port[0] = key.port[1] = iih.icmp_id;
+			icmpid = iih.icmp_id;
+			pf_icmp_mapping(&pd2, iih.icmp_type,
+			    &icmp_dir, &multi, &virtual_id, &virtual_type);
 
-			STATE_LOOKUP(kif, &key, *state, pd);
+			ret = pf_icmp_state_lookup(&key, &pd2, state, m,
+			    pd->dir, kif, virtual_id, virtual_type,
+			    icmp_dir, &iidx, PF_ICMP_MULTI_NONE);
+			if (ret >= 0)
+				return (ret);
 
 			/* translate source/destination address, if necessary */
 			if ((*state)->key[PF_SK_WIRE] !=
@@ -6382,21 +6583,23 @@ pf_test_state_icmp(struct pf_kstate **state, struct pfi_kkif *kif,
 
 				if (PF_ANEQ(pd2.src,
 				    &nk->addr[pd2.sidx], pd2.af) ||
-				    nk->port[pd2.sidx] != iih.icmp_id)
-					pf_change_icmp(pd2.src, &iih.icmp_id,
+				    (virtual_type == ICMP_ECHO &&
+				    nk->port[iidx] != iih.icmp_id))
+					pf_change_icmp(pd2.src,
+					    (virtual_type == ICMP_ECHO) ?
+					    &iih.icmp_id : NULL,
 					    daddr, &nk->addr[pd2.sidx],
-					    nk->port[pd2.sidx], NULL,
+					    (virtual_type == ICMP_ECHO) ?
+					    nk->port[iidx] : 0, NULL,
 					    pd2.ip_sum, icmpsum,
 					    pd->ip_sum, 0, AF_INET);
 
 				if (PF_ANEQ(pd2.dst,
-				    &nk->addr[pd2.didx], pd2.af) ||
-				    nk->port[pd2.didx] != iih.icmp_id)
-					pf_change_icmp(pd2.dst, &iih.icmp_id,
-					    saddr, &nk->addr[pd2.didx],
-					    nk->port[pd2.didx], NULL,
-					    pd2.ip_sum, icmpsum,
-					    pd->ip_sum, 0, AF_INET);
+				    &nk->addr[pd2.didx], pd2.af))
+					pf_change_icmp(pd2.dst, NULL, NULL,
+					    &nk->addr[pd2.didx], 0, NULL,
+					    pd2.ip_sum, icmpsum, pd->ip_sum, 0,
+					    AF_INET);
 
 				m_copyback(m, off, ICMP_MINLEN, (caddr_t)&pd->hdr.icmp);
 				m_copyback(m, ipoff2, sizeof(h2), (caddr_t)&h2);
@@ -6418,13 +6621,25 @@ pf_test_state_icmp(struct pf_kstate **state, struct pfi_kkif *kif,
 				return (PF_DROP);
 			}
 
-			key.af = pd2.af;
-			key.proto = IPPROTO_ICMPV6;
-			PF_ACPY(&key.addr[pd2.sidx], pd2.src, key.af);
-			PF_ACPY(&key.addr[pd2.didx], pd2.dst, key.af);
-			key.port[0] = key.port[1] = iih.icmp6_id;
-
-			STATE_LOOKUP(kif, &key, *state, pd);
+			pf_icmp_mapping(&pd2, iih.icmp6_type,
+			    &icmp_dir, &multi, &virtual_id, &virtual_type);
+			ret = pf_icmp_state_lookup(&key, &pd2, state, m,
+			    pd->dir, kif, virtual_id, virtual_type,
+			    icmp_dir, &iidx, PF_ICMP_MULTI_NONE);
+			if (ret >= 0) {
+				if (ret == PF_DROP && pd->af == AF_INET6 &&
+				    icmp_dir == PF_OUT) {
+					if (*state != NULL)
+						PF_STATE_UNLOCK((*state));
+					ret = pf_icmp_state_lookup(&key, pd,
+					    state, m, pd->dir, kif,
+					    virtual_id, virtual_type,
+					    icmp_dir, &iidx, multi);
+					if (ret >= 0)
+						return (ret);
+				} else
+					return (ret);
+			}
 
 			/* translate source/destination address, if necessary */
 			if ((*state)->key[PF_SK_WIRE] !=
@@ -6434,19 +6649,21 @@ pf_test_state_icmp(struct pf_kstate **state, struct pfi_kkif *kif,
 
 				if (PF_ANEQ(pd2.src,
 				    &nk->addr[pd2.sidx], pd2.af) ||
-				    nk->port[pd2.sidx] != iih.icmp6_id)
-					pf_change_icmp(pd2.src, &iih.icmp6_id,
+				    ((virtual_type == ICMP6_ECHO_REQUEST) &&
+				    nk->port[pd2.sidx] != iih.icmp6_id))
+					pf_change_icmp(pd2.src,
+					    (virtual_type == ICMP6_ECHO_REQUEST)
+					    ? &iih.icmp6_id : NULL,
 					    daddr, &nk->addr[pd2.sidx],
-					    nk->port[pd2.sidx], NULL,
+					    (virtual_type == ICMP6_ECHO_REQUEST)
+					    ? nk->port[iidx] : 0, NULL,
 					    pd2.ip_sum, icmpsum,
 					    pd->ip_sum, 0, AF_INET6);
 
 				if (PF_ANEQ(pd2.dst,
-				    &nk->addr[pd2.didx], pd2.af) ||
-				    nk->port[pd2.didx] != iih.icmp6_id)
-					pf_change_icmp(pd2.dst, &iih.icmp6_id,
-					    saddr, &nk->addr[pd2.didx],
-					    nk->port[pd2.didx], NULL,
+				    &nk->addr[pd2.didx], pd2.af))
+					pf_change_icmp(pd2.dst, NULL, NULL,
+					    &nk->addr[pd2.didx], 0, NULL,
 					    pd2.ip_sum, icmpsum,
 					    pd->ip_sum, 0, AF_INET6);
 
diff --git a/sys/netpfil/pf/pf_lb.c b/sys/netpfil/pf/pf_lb.c
index eb3d087e3df6..4fcad7e578a8 100644
--- a/sys/netpfil/pf/pf_lb.c
+++ b/sys/netpfil/pf/pf_lb.c
@@ -225,6 +225,23 @@ pf_get_sport(sa_family_t af, u_int8_t proto, struct pf_krule *r,
 	if (pf_map_addr(af, r, saddr, naddr, NULL, &init_addr, sn))
 		return (1);
 
+	if (proto == IPPROTO_ICMP) {
+		if (*nport == htons(ICMP_ECHO)) {
+			low = 1;
+			high = 65535;
+		} else
+			return (0);	/* Don't try to modify non-echo ICMP */
+	}
+#ifdef INET6
+	if (proto == IPPROTO_ICMPV6) {
+		if (*nport == htons(ICMP6_ECHO_REQUEST)) {
+			low = 1;
+			high = 65535;
+		} else
+			return (0);	/* Don't try to modify non-echo ICMP */
+	}
+#endif /* INET6 */
+
 	bzero(&key, sizeof(key));
 	key.af = af;
 	key.proto = proto;
@@ -633,7 +650,7 @@ pf_get_translation(struct pf_pdesc *pd, struct mbuf *m, int off,
 	switch (r->action) {
 	case PF_NAT:
 		if (pd->proto == IPPROTO_ICMP) {
-			low  = 1;
+			low = 1;
 			high = 65535;
 		} else {
 			low  = r->rpool.proxy_port[0];