git: aeddee83341e - main - pfctl: Split pool parsing into separate functions

From: Kajetan Staszkiewicz <ks_at_FreeBSD.org>
Date: Fri, 28 Mar 2025 16:08:53 UTC
The branch main has been updated by ks:

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

commit aeddee83341eb3b847fb4862db5cd0a553bf4a36
Author:     Kajetan Staszkiewicz <ks@FreeBSD.org>
AuthorDate: 2025-03-28 16:08:34 +0000
Commit:     Kajetan Staszkiewicz <ks@FreeBSD.org>
CommitDate: 2025-03-28 16:08:34 +0000

    pfctl: Split pool parsing into separate functions
    
    The pf pools are used in NAT, route-to and af-to rules. Some parts of
    code are duplicated between them. Create functions apply_redirspec(),
    apply_nat_ports() and apply_rdr_ports() to handle the common tasks.
    
    Simplify data structures used for pool parsing. Move the contents of
    struct redirection to struct redirspec. Map all ways of parsing pools
    directly onto struct redirspec. Name various forms of struct redirspect
    to hint where they are used.
    
    Remove struct redirspec *rroute from struct filter_opts, because
    filter_opts is bzero()'ed after the route part of rule is parsed, and
    thus can't be used.
    
    Add tests to ensure that parsing and error messages behave as expected.
    The tests have been written and tested with pfctl from before this
    patch.
    
    This is prerequisite for adding support for OpenBSD NAT syntax.
    
    Reviewed by:            kp
    Approved by:            kp (mentor)
    Sponsored by:           InnoGames GmbH
    Differential Revision:  https://reviews.freebsd.org/D49218
---
 sbin/pfctl/parse.y                   | 569 ++++++++++++++++-------------------
 sbin/pfctl/pfctl.c                   |  10 +
 sbin/pfctl/pfctl_parser.h            |   1 +
 sbin/pfctl/tests/files/Makefile      |   2 +-
 sbin/pfctl/tests/files/pf1026.ok     |   2 +-
 sbin/pfctl/tests/files/pf1027.ok     |   2 +-
 sbin/pfctl/tests/files/pf1028.in     |   1 +
 sbin/pfctl/tests/files/pf1028.ok     |   1 +
 sbin/pfctl/tests/files/pf1029.in     |   1 +
 sbin/pfctl/tests/files/pf1029.ok     |   1 +
 sbin/pfctl/tests/files/pf1030.in     |   1 +
 sbin/pfctl/tests/files/pf1030.ok     |   1 +
 sbin/pfctl/tests/files/pf1031.in     |   1 +
 sbin/pfctl/tests/files/pf1031.ok     |   1 +
 sbin/pfctl/tests/files/pf1032.in     |   1 +
 sbin/pfctl/tests/files/pf1032.ok     |   1 +
 sbin/pfctl/tests/files/pf1033.fail   |   1 +
 sbin/pfctl/tests/files/pf1033.in     |   1 +
 sbin/pfctl/tests/files/pf1034.fail   |   1 +
 sbin/pfctl/tests/files/pf1034.in     |   1 +
 sbin/pfctl/tests/files/pf1035.in     |   1 +
 sbin/pfctl/tests/files/pf1035.ok     |   1 +
 sbin/pfctl/tests/files/pf1036.in     |   1 +
 sbin/pfctl/tests/files/pf1036.ok     |   1 +
 sbin/pfctl/tests/files/pf1037.in     |   1 +
 sbin/pfctl/tests/files/pf1037.ok     |   1 +
 sbin/pfctl/tests/files/pf1038.in     |   1 +
 sbin/pfctl/tests/files/pf1038.ok     |   1 +
 sbin/pfctl/tests/files/pf1039.in     |   1 +
 sbin/pfctl/tests/files/pf1039.ok     |   1 +
 sbin/pfctl/tests/files/pf1040.fail   |   1 +
 sbin/pfctl/tests/files/pf1040.in     |   1 +
 sbin/pfctl/tests/files/pf1040.ok     |   1 +
 sbin/pfctl/tests/files/pf1041.in     |   1 +
 sbin/pfctl/tests/files/pf1041.ok     |   1 +
 sbin/pfctl/tests/files/pf1042.fail   |   1 +
 sbin/pfctl/tests/files/pf1042.in     |   1 +
 sbin/pfctl/tests/files/pf1043.fail   |   1 +
 sbin/pfctl/tests/files/pf1043.in     |   1 +
 sbin/pfctl/tests/files/pf1044.in     |   1 +
 sbin/pfctl/tests/files/pf1044.ok     |   1 +
 sbin/pfctl/tests/files/pf1045.in     |   1 +
 sbin/pfctl/tests/files/pf1045.ok     |   1 +
 sbin/pfctl/tests/files/pf1046.fail   |   1 +
 sbin/pfctl/tests/files/pf1046.in     |   1 +
 sbin/pfctl/tests/files/pf1047.fail   |   1 +
 sbin/pfctl/tests/files/pf1047.in     |   1 +
 sbin/pfctl/tests/files/pf1048.in     |   1 +
 sbin/pfctl/tests/files/pf1048.ok     |   1 +
 sbin/pfctl/tests/files/pf1049.in     |   1 +
 sbin/pfctl/tests/files/pf1049.ok     |   1 +
 sbin/pfctl/tests/files/pf1050.in     |   1 +
 sbin/pfctl/tests/files/pf1050.ok     |   1 +
 sbin/pfctl/tests/files/pf1051.in     |   1 +
 sbin/pfctl/tests/files/pf1051.ok     |   1 +
 sbin/pfctl/tests/files/pf1052.in     |   1 +
 sbin/pfctl/tests/files/pf1052.ok     |   1 +
 sbin/pfctl/tests/files/pf1053.in     |   1 +
 sbin/pfctl/tests/files/pf1053.ok     |   1 +
 sbin/pfctl/tests/files/pf1054.in     |   3 +
 sbin/pfctl/tests/files/pf1054.ok     |   1 +
 sbin/pfctl/tests/files/pf1055.in     |   1 +
 sbin/pfctl/tests/files/pf1055.ok     |   1 +
 sbin/pfctl/tests/files/pf1056.in     |   1 +
 sbin/pfctl/tests/files/pf1056.ok     |   1 +
 sbin/pfctl/tests/files/pf1057.in     |   1 +
 sbin/pfctl/tests/files/pf1057.ok     |   1 +
 sbin/pfctl/tests/files/pf1058.in     |   1 +
 sbin/pfctl/tests/files/pf1058.ok     |   1 +
 sbin/pfctl/tests/files/pf1059.in     |   1 +
 sbin/pfctl/tests/files/pf1059.ok     |   1 +
 sbin/pfctl/tests/files/pf1060.in     |   1 +
 sbin/pfctl/tests/files/pf1060.ok     |   1 +
 sbin/pfctl/tests/files/pf1061.in     |   1 +
 sbin/pfctl/tests/files/pf1061.ok     |   1 +
 sbin/pfctl/tests/files/pf1062.in     |   1 +
 sbin/pfctl/tests/files/pf1062.ok     |   1 +
 sbin/pfctl/tests/files/pf1063.in     |   1 +
 sbin/pfctl/tests/files/pf1063.ok     |   1 +
 sbin/pfctl/tests/files/pf1064.in     |   1 +
 sbin/pfctl/tests/files/pf1064.ok     |   1 +
 sbin/pfctl/tests/pfctl_test.c        | 188 ++++++++++--
 sbin/pfctl/tests/pfctl_test_list.inc |  37 +++
 83 files changed, 547 insertions(+), 341 deletions(-)

diff --git a/sbin/pfctl/parse.y b/sbin/pfctl/parse.y
index 8f732bdf268a..ea81efd7fd94 100644
--- a/sbin/pfctl/parse.y
+++ b/sbin/pfctl/parse.y
@@ -227,10 +227,6 @@ struct range {
 	int		 b;
 	int		 t;
 };
-struct redirection {
-	struct node_host	*host;
-	struct range		 rport;
-};
 
 static struct pool_opts {
 	int			 marker;
@@ -245,9 +241,10 @@ static struct pool_opts {
 } pool_opts;
 
 struct redirspec {
-	struct redirection	 *rdr;
+	struct node_host	*host;
+	struct range		 rport;
 	struct pool_opts	 pool_opts;
-	int		 af;
+	int			 af;
 };
 
 static struct filter_opts {
@@ -302,9 +299,8 @@ static struct filter_opts {
 		struct node_host	*addr;
 		u_int16_t		port;
 	}			 divert;
-	struct redirspec	 nat;
-	struct redirspec	 rdr;
-	struct redirspec	 rroute;
+	struct redirspec	 *nat;
+	struct redirspec	 *rdr;
 	/* new-style scrub opts */
 	int			 nodf;
 	int			 minttl;
@@ -382,9 +378,11 @@ void		 expand_eth_rule(struct pfctl_eth_rule *,
 		    struct node_mac *, struct node_mac *,
 		    struct node_host *, struct node_host *, const char *,
 		    const char *);
+int		 apply_rdr_ports(struct pfctl_rule *r, struct pfctl_pool *, struct redirspec *);
+int		 apply_nat_ports(struct pfctl_pool *, struct redirspec *);
+int		 apply_redirspec(struct pfctl_pool *, struct redirspec *);
 void		 expand_rule(struct pfctl_rule *, struct node_if *,
 		    struct redirspec *, struct redirspec *, struct redirspec *,
-		    struct node_host *, struct node_host *, struct node_host *,
 		    struct node_proto *, struct node_os *, struct node_host *,
 		    struct node_port *, struct node_host *, struct node_port *,
 		    struct node_uid *, struct node_gid *, struct node_if *,
@@ -463,13 +461,10 @@ typedef struct {
 		} etheraddr;
 		char			*bridge_to;
 		struct {
-			struct node_host	*host;
+			struct redirspec	*redirspec;
 			u_int8_t		 rt;
-			u_int8_t		 pool_opts;
-			sa_family_t		 af;
-			struct pf_poolhashkey	*key;
 		}			 route;
-		struct redirection	*redirection;
+		struct redirspec	*redirspec;
 		struct {
 			int			 action;
 			struct node_state_opt	*options;
@@ -554,14 +549,15 @@ int	parseport(char *, struct range *r, int);
 %type	<v.fromto>		fromto l3fromto
 %type	<v.peer>		ipportspec from to
 %type	<v.host>		ipspec toipspec xhost host dynaddr host_list
-%type	<v.host>		redir_host_list redirspec
-%type	<v.host>		route_host route_host_list routespec
+%type	<v.host>		redir_host redir_host_list routespec
+%type	<v.host>		route_host route_host_list
 %type	<v.os>			os xos os_list
 %type	<v.port>		portspec port_list port_item
 %type	<v.uid>			uids uid_list uid_item
 %type	<v.gid>			gids gid_list gid_item
 %type	<v.route>		route
-%type	<v.redirection>		redirection redirpool
+%type	<v.redirspec>		no_port_redirspec port_redirspec route_redirspec
+%type	<v.redirspec>		binat_redirspec nat_redirspec
 %type	<v.string>		label stringall tag anchorname
 %type	<v.string>		string varstring numberstring
 %type	<v.keep_state>		keep
@@ -965,7 +961,8 @@ anchorrule	: ANCHOR anchorname dir quick interface af proto fromto
 				YYERROR;
 			}
 
-			memset(&r, 0, sizeof(r));
+			pfctl_init_rule(&r);
+
 			if (pf->astack[pf->asd + 1]) {
 				if ($2 && strchr($2, '/') != NULL) {
 					free($2);
@@ -978,7 +975,7 @@ anchorrule	: ANCHOR anchorname dir quick interface af proto fromto
 				pfctl_anchor_setup(&r,
 				    &pf->astack[pf->asd]->ruleset,
 				    $2 ? $2 : pf->alast->name);
-		
+
 				if (r.anchor == NULL)
 					err(1, "anchorrule: unable to "
 					    "create ruleset");
@@ -1082,7 +1079,7 @@ anchorrule	: ANCHOR anchorname dir quick interface af proto fromto
 			decide_address_family($8.src.host, &r.af);
 			decide_address_family($8.dst.host, &r.af);
 
-			expand_rule(&r, $5, NULL, NULL, NULL, NULL, NULL, NULL,
+			expand_rule(&r, $5, NULL, NULL, NULL,
 			    $7, $8.src_os, $8.src.host, $8.src.port, $8.dst.host,
 			    $8.dst.port, $9.uid, $9.gid, $9.rcv, $9.icmpspec,
 			    pf->astack[pf->asd + 1] ? pf->alast->name : $2);
@@ -1097,7 +1094,8 @@ anchorrule	: ANCHOR anchorname dir quick interface af proto fromto
 				YYERROR;
 			}
 
-			memset(&r, 0, sizeof(r));
+			pfctl_init_rule(&r);
+
 			r.action = PF_NAT;
 			r.af = $4;
 			r.rtableid = $7;
@@ -1105,7 +1103,7 @@ anchorrule	: ANCHOR anchorname dir quick interface af proto fromto
 			decide_address_family($6.src.host, &r.af);
 			decide_address_family($6.dst.host, &r.af);
 
-			expand_rule(&r, $3, NULL, NULL, NULL, NULL, NULL, NULL,
+			expand_rule(&r, $3, NULL, NULL, NULL,
 			    $5, $6.src_os, $6.src.host, $6.src.port, $6.dst.host,
 			    $6.dst.port, 0, 0, 0, 0, $2);
 			free($2);
@@ -1118,7 +1116,8 @@ anchorrule	: ANCHOR anchorname dir quick interface af proto fromto
 				YYERROR;
 			}
 
-			memset(&r, 0, sizeof(r));
+			pfctl_init_rule(&r);
+
 			r.action = PF_RDR;
 			r.af = $4;
 			r.rtableid = $7;
@@ -1147,7 +1146,7 @@ anchorrule	: ANCHOR anchorname dir quick interface af proto fromto
 				r.dst.port_op = $6.dst.port->op;
 			}
 
-			expand_rule(&r, $3, NULL, NULL, NULL, NULL, NULL, NULL,
+			expand_rule(&r, $3, NULL, NULL, NULL,
 			    $5, $6.src_os, $6.src.host, $6.src.port, $6.dst.host,
 			    $6.dst.port, 0, 0, 0, 0, $2);
 			free($2);
@@ -1160,7 +1159,8 @@ anchorrule	: ANCHOR anchorname dir quick interface af proto fromto
 				YYERROR;
 			}
 
-			memset(&r, 0, sizeof(r));
+			pfctl_init_rule(&r);
+
 			r.action = PF_BINAT;
 			r.af = $4;
 			r.rtableid = $7;
@@ -1425,7 +1425,7 @@ scrubrule	: scrubaction dir logquick interface af proto fromto scrub_opts
 			if (check_rulestate(PFCTL_STATE_SCRUB))
 				YYERROR;
 
-			memset(&r, 0, sizeof(r));
+			pfctl_init_rule(&r);
 
 			r.action = $1.b1;
 			r.direction = $2;
@@ -1470,7 +1470,7 @@ scrubrule	: scrubaction dir logquick interface af proto fromto scrub_opts
 			r.match_tag_not = $8.match_tag_not;
 			r.rtableid = $8.rtableid;
 
-			expand_rule(&r, $4, NULL, NULL, NULL, NULL, NULL, NULL,
+			expand_rule(&r, $4, NULL, NULL, NULL,
 			    $6, $7.src_os, $7.src.host, $7.src.port, $7.dst.host,
 			    $7.dst.port, NULL, NULL, NULL, NULL, "");
 		}
@@ -1587,7 +1587,7 @@ antispoof	: ANTISPOOF logquick antispoof_ifspc af antispoof_opts {
 				YYERROR;
 
 			for (i = $3; i; i = i->next) {
-				bzero(&r, sizeof(r));
+				pfctl_init_rule(&r);
 
 				r.action = PF_DROP;
 				r.direction = PF_IN;
@@ -1635,9 +1635,9 @@ antispoof	: ANTISPOOF logquick antispoof_ifspc af antispoof_opts {
 				}
 
 				if (h != NULL)
-					expand_rule(&r, j, NULL, NULL, NULL, NULL, NULL, NULL,
-					    NULL, NULL, h, NULL, NULL, NULL, NULL, NULL,
-					    NULL, NULL, "");
+					expand_rule(&r, j, NULL, NULL, NULL,
+					    NULL, NULL, h, NULL, NULL, NULL,
+					    NULL, NULL, NULL, NULL, "");
 
 				if ((i->ifa_flags & IFF_LOOPBACK) == 0) {
 					bzero(&r, sizeof(r));
@@ -1657,9 +1657,10 @@ antispoof	: ANTISPOOF logquick antispoof_ifspc af antispoof_opts {
 					else
 						h = ifa_lookup(i->ifname, 0);
 					if (h != NULL)
-						expand_rule(&r, NULL, NULL, NULL, NULL, NULL,
-						    NULL, NULL, NULL, NULL, h, NULL, NULL,
-						    NULL, NULL, NULL, NULL, NULL, "");
+						expand_rule(&r, NULL, NULL,
+						    NULL, NULL, NULL, NULL, h,
+						    NULL, NULL, NULL, NULL,
+						    NULL, NULL, NULL, "");
 				} else
 					free(hh);
 			}
@@ -2371,7 +2372,7 @@ pfrule		: action dir logquick interface route af proto fromto
 			if (check_rulestate(PFCTL_STATE_FILTER))
 				YYERROR;
 
-			memset(&r, 0, sizeof(r));
+			pfctl_init_rule(&r);
 
 			r.action = $1.b1;
 			switch ($1.b2) {
@@ -2728,44 +2729,14 @@ pfrule		: action dir logquick interface route af proto fromto
 					YYERROR;
 				}
 				r.rt = $5.rt;
-				r.route.opts = $5.pool_opts;
-				if ($5.key != NULL)
-					memcpy(&r.route.key, $5.key,
-					    sizeof(struct pf_poolhashkey));
-			}
-			if (r.rt) {
-				decide_address_family($5.host, &r.af);
+				decide_address_family($5.redirspec->host, &r.af);
 				if (!(r.rule_flag & PFRULE_AFTO))
-					remove_invalid_hosts(&$5.host, &r.af);
-				if ($5.host == NULL) {
+					remove_invalid_hosts(&($5.redirspec->host), &r.af);
+				if ($5.redirspec->host == NULL) {
 					yyerror("no routing address with "
 					    "matching address family found.");
 					YYERROR;
 				}
-				if ((r.route.opts & PF_POOL_TYPEMASK) ==
-				    PF_POOL_NONE && ($5.host->next != NULL ||
-				    $5.host->addr.type == PF_ADDR_TABLE ||
-				    DYNIF_MULTIADDR($5.host->addr)))
-					r.route.opts |= PF_POOL_ROUNDROBIN;
-				if ($5.host->next != NULL &&
-				    !PF_POOL_DYNTYPE(r.route.opts)) {
-					yyerror("address pool option "
-					    "not supported by type");
-				}
-				if (!PF_POOL_DYNTYPE(r.route.opts) &&
-				    (disallow_table($5.host,
-				    "tables are not supported by pool type") ||
-				    disallow_alias($5.host,
-				    "interface (%s) is not supported by pool type")))
-					YYERROR;
-				if ($5.host->next != NULL) {
-					if ((r.route.opts & PF_POOL_TYPEMASK) !=
-					    PF_POOL_ROUNDROBIN) {
-						yyerror("r.route.opts must "
-						    "be PF_POOL_ROUNDROBIN");
-						YYERROR;
-					}
-				}
 			}
 			if ($9.queues.qname != NULL) {
 				if (strlcpy(r.qname, $9.queues.qname,
@@ -2824,19 +2795,10 @@ pfrule		: action dir logquick interface route af proto fromto
 			}
 
 			if ($9.marker & FOM_AFTO) {
-				r.naf = $9.nat.af;
-
-				r.nat.opts = $9.nat.pool_opts.type;
-				r.nat.opts |= $9.nat.pool_opts.opts;
-
-				if (!PF_POOL_DYNTYPE(r.nat.opts) &&
-				    disallow_table($9.nat.rdr->host, "tables are not "
-				    "supported by pool type"))
-					YYERROR;
+				r.naf = $9.nat->af;
 			}
 
-			expand_rule(&r, $4, &$9.nat, &$9.rdr, &$9.rroute,
-			    NULL, $9.nat.rdr ? $9.nat.rdr->host : NULL, $5.host,
+			expand_rule(&r, $4, $9.nat, $9.rdr, $5.redirspec,
 			    $7, $8.src_os, $8.src.host, $8.src.port, $8.dst.host,
 			    $8.dst.port, $9.uid, $9.gid, $9.rcv, $9.icmpspec, "");
 		}
@@ -3053,8 +3015,8 @@ filter_opt	: USER uids {
 				filter_opts.marker |= FOM_SCRUB_TCP;
 			filter_opts.marker |= $3.marker;
 		}
-		| AFTO af FROM redirspec pool_opts {
-			if (filter_opts.nat.rdr) {
+		| AFTO af FROM port_redirspec {
+			if (filter_opts.nat) {
 				yyerror("cannot respecify af-to");
 				YYERROR;
 			}
@@ -3062,26 +3024,18 @@ filter_opt	: USER uids {
 				yyerror("no address family specified");
 				YYERROR;
 			}
+
+			filter_opts.nat = $4;
+			filter_opts.nat->af = $2;
 			if ($4->af && $4->af != $2) {
 				yyerror("af-to addresses must be in the "
 				   "target address family");
 				YYERROR;
 			}
-			filter_opts.nat.af = $2;
-			filter_opts.nat.rdr = calloc(1, sizeof(struct redirection));
-			if (filter_opts.nat.rdr == NULL)
-				err(1, "af-to: calloc");
-			filter_opts.nat.rdr->host = $4;
-			memcpy(&filter_opts.nat.pool_opts, &$5,
-			    sizeof(filter_opts.nat.pool_opts));
-			filter_opts.rdr.rdr =
-			    calloc(1, sizeof(struct redirection));
-			bzero(&filter_opts.rdr.pool_opts,
-			    sizeof(filter_opts.rdr.pool_opts));
 			filter_opts.marker |= FOM_AFTO;
 		}
-		| AFTO af FROM redirspec pool_opts TO redirspec pool_opts {
-			if (filter_opts.nat.rdr) {
+		| AFTO af FROM port_redirspec TO port_redirspec {
+			if (filter_opts.nat) {
 				yyerror("cannot respecify af-to");
 				YYERROR;
 			}
@@ -3089,26 +3043,16 @@ filter_opt	: USER uids {
 				yyerror("no address family specified");
 				YYERROR;
 			}
-				if (($4->af && $4->af != $2) ||
-				($7->af && $7->af != $2)) {
+			filter_opts.nat = $4;
+			filter_opts.nat->af = $2;
+			filter_opts.rdr = $6;
+			filter_opts.rdr->af = $2;
+			if (($4->af && $4->host->af != $2) ||
+			    ($6->af && $6->host->af != $2)) {
 				yyerror("af-to addresses must be in the "
 				   "target address family");
 				YYERROR;
 			}
-			filter_opts.nat.af = $2;
-			filter_opts.nat.rdr = calloc(1, sizeof(struct redirection));
-			if (filter_opts.nat.rdr == NULL)
-				err(1, "af-to: calloc");
-			filter_opts.nat.rdr->host = $4;
-			memcpy(&filter_opts.nat.pool_opts, &$5,
-			    sizeof(filter_opts.nat.pool_opts));
-			filter_opts.rdr.af = $2;
-			filter_opts.rdr.rdr = calloc(1, sizeof(struct redirection));
-			if (filter_opts.rdr.rdr == NULL)
-				err(1, "af-to: calloc");
-			filter_opts.rdr.rdr->host = $7;
-			memcpy(&filter_opts.nat.pool_opts, &$8,
-			    sizeof(filter_opts.nat.pool_opts));
 			filter_opts.marker |= FOM_AFTO;
 		}
 		| filter_sets
@@ -4580,7 +4524,7 @@ portstar	: numberstring			{
 		}
 		;
 
-redirspec	: host				{ $$ = $1; }
+redir_host	: host				{ $$ = $1; }
 		| '{' optnl redir_host_list '}'	{ $$ = $3; }
 		;
 
@@ -4592,20 +4536,41 @@ redir_host_list	: host optnl			{ $$ = $1; }
 		}
 		;
 
-redirpool	: /* empty */			{ $$ = NULL; }
-		| ARROW redirspec		{
-			$$ = calloc(1, sizeof(struct redirection));
+/* Redirection without port */
+no_port_redirspec: redir_host pool_opts	{
+			$$ = calloc(1, sizeof(struct redirspec));
 			if ($$ == NULL)
-				err(1, "redirection: calloc");
-			$$->host = $2;
+				err(1, "redirspec: calloc");
+			$$->host = $1;
+			$$->pool_opts = $2;
 			$$->rport.a = $$->rport.b = $$->rport.t = 0;
 		}
-		| ARROW redirspec PORT portstar	{
-			$$ = calloc(1, sizeof(struct redirection));
+		;
+
+/* Redirection with optional port */
+port_redirspec	: no_port_redirspec;
+		| redir_host PORT portstar pool_opts {
+			$$ = calloc(1, sizeof(struct redirspec));
 			if ($$ == NULL)
-				err(1, "redirection: calloc");
-			$$->host = $2;
-			$$->rport = $4;
+				err(1, "redirspec: calloc");
+			$$->host = $1;
+			$$->rport = $3;
+			$$->pool_opts = $4;
+		}
+
+/* Redirection with an arrow and an optional port: FreeBSD NAT rules */
+nat_redirspec	: ARROW port_redirspec {
+			$$ = $2;
+		}
+		;
+
+/* Redirection with interfaces and without ports: route-to rules */
+route_redirspec	: routespec pool_opts {
+			$$ = calloc(1, sizeof(struct redirspec));
+			if ($$ == NULL)
+				err(1, "redirspec: calloc");
+			$$->host = $1;
+			$$->pool_opts = $2;
 		}
 		;
 
@@ -4756,18 +4721,18 @@ pool_opt	: BITMASK	{
 		}
 		;
 
-redirection	: /* empty */			{ $$ = NULL; }
+binat_redirspec	: /* empty */			{ $$ = NULL; }
 		| ARROW host			{
-			$$ = calloc(1, sizeof(struct redirection));
+			$$ = calloc(1, sizeof(struct redirspec));
 			if ($$ == NULL)
-				err(1, "redirection: calloc");
+				err(1, "redirspec: calloc");
 			$$->host = $2;
 			$$->rport.a = $$->rport.b = $$->rport.t = 0;
 		}
 		| ARROW host PORT portstar	{
-			$$ = calloc(1, sizeof(struct redirection));
+			$$ = calloc(1, sizeof(struct redirspec));
 			if ($$ == NULL)
-				err(1, "redirection: calloc");
+				err(1, "redirspec: calloc");
 			$$->host = $2;
 			$$->rport = $4;
 		}
@@ -4808,7 +4773,7 @@ nataction	: no NAT natpasslog {
 		;
 
 natrule		: nataction interface af proto fromto tag tagged rtable
-		    redirpool pool_opts
+		    nat_redirspec
 		{
 			struct pfctl_rule	r;
 			struct node_state_opt	*o;
@@ -4816,7 +4781,7 @@ natrule		: nataction interface af proto fromto tag tagged rtable
 			if (check_rulestate(PFCTL_STATE_NAT))
 				YYERROR;
 
-			memset(&r, 0, sizeof(r));
+			pfctl_init_rule(&r);
 
 			r.action = $1.b1;
 			r.natpass = $1.b2;
@@ -4875,110 +4840,6 @@ natrule		: nataction interface af proto fromto tag tagged rtable
 				}
 				if (check_netmask($9->host, r.af))
 					YYERROR;
-
-				r.rdr.proxy_port[0] = ntohs($9->rport.a);
-
-				switch (r.action) {
-				case PF_RDR:
-					if (!$9->rport.b && $9->rport.t &&
-					    $5.dst.port != NULL) {
-						r.rdr.proxy_port[1] =
-						    ntohs($9->rport.a) +
-						    (ntohs(
-						    $5.dst.port->port[1]) -
-						    ntohs(
-						    $5.dst.port->port[0]));
-					} else
-						r.rdr.proxy_port[1] =
-						    ntohs($9->rport.b);
-					break;
-				case PF_NAT:
-					r.rdr.proxy_port[1] =
-					    ntohs($9->rport.b);
-					if (!r.rdr.proxy_port[0] &&
-					    !r.rdr.proxy_port[1]) {
-						r.rdr.proxy_port[0] =
-						    PF_NAT_PROXY_PORT_LOW;
-						r.rdr.proxy_port[1] =
-						    PF_NAT_PROXY_PORT_HIGH;
-					} else if (!r.rdr.proxy_port[1])
-						r.rdr.proxy_port[1] =
-						    r.rdr.proxy_port[0];
-					break;
-				default:
-					break;
-				}
-
-				r.rdr.opts = $10.type;
-				if ((r.rdr.opts & PF_POOL_TYPEMASK) ==
-				    PF_POOL_NONE && ($9->host->next != NULL ||
-				    $9->host->addr.type == PF_ADDR_TABLE ||
-				    DYNIF_MULTIADDR($9->host->addr)))
-					r.rdr.opts = PF_POOL_ROUNDROBIN;
-				if (!PF_POOL_DYNTYPE(r.rdr.opts) &&
-				    (disallow_table($9->host,
-				    "tables are not supported by pool type") ||
-				    disallow_alias($9->host,
-				    "interface (%s) is not supported by pool type")))
-					YYERROR;
-				if ($9->host->next != NULL) {
-					if ((r.rdr.opts & PF_POOL_TYPEMASK) !=
-					    PF_POOL_ROUNDROBIN) {
-						yyerror("only round-robin "
-						    "valid for multiple "
-						    "redirection addresses");
-						YYERROR;
-					}
-				}
-			}
-
-			if ($10.key != NULL)
-				memcpy(&r.rdr.key, $10.key,
-				    sizeof(struct pf_poolhashkey));
-
-			 if ($10.opts)
-				r.rdr.opts |= $10.opts;
-
-			if ($10.staticport) {
-				if (r.action != PF_NAT) {
-					yyerror("the 'static-port' option is "
-					    "only valid with nat rules");
-					YYERROR;
-				}
-				if (r.rdr.proxy_port[0] !=
-				    PF_NAT_PROXY_PORT_LOW &&
-				    r.rdr.proxy_port[1] !=
-				    PF_NAT_PROXY_PORT_HIGH) {
-					yyerror("the 'static-port' option can't"
-					    " be used when specifying a port"
-					    " range");
-					YYERROR;
-				}
-				r.rdr.proxy_port[0] = 0;
-				r.rdr.proxy_port[1] = 0;
-			}
-
-			if ($10.mape.offset) {
-				if (r.action != PF_NAT) {
-					yyerror("the 'map-e-portset' option is"
-					    " only valid with nat rules");
-					YYERROR;
-				}
-				if ($10.staticport) {
-					yyerror("the 'map-e-portset' option"
-					    " can't be used 'static-port'");
-					YYERROR;
-				}
-				if (r.rdr.proxy_port[0] !=
-				    PF_NAT_PROXY_PORT_LOW &&
-				    r.rdr.proxy_port[1] !=
-				    PF_NAT_PROXY_PORT_HIGH) {
-					yyerror("the 'map-e-portset' option"
-					    " can't be used when specifying"
-					    " a port range");
-					YYERROR;
-				}
-				r.rdr.mape = $10.mape;
 			}
 
 			o = keep_state_defaults;
@@ -4996,16 +4857,14 @@ natrule		: nataction interface af proto fromto tag tagged rtable
 				o = o->next;
 			}
 
-			expand_rule(&r, $2, NULL, NULL, NULL,
-			    $9 == NULL ? NULL : $9->host, NULL, NULL, $4,
+			expand_rule(&r, $2, NULL, $9, NULL, $4,
 			    $5.src_os, $5.src.host, $5.src.port, $5.dst.host,
 			    $5.dst.port, 0, 0, 0, 0, "");
-			free($9);
 		}
 		;
 
 binatrule	: no BINAT natpasslog interface af proto FROM ipspec toipspec tag
-		    tagged rtable redirection
+		    tagged rtable binat_redirspec
 		{
 			struct pfctl_rule	binat;
 			struct pf_pooladdr	*pa;
@@ -5016,7 +4875,7 @@ binatrule	: no BINAT natpasslog interface af proto FROM ipspec toipspec tag
 			    "permitted as a binat destination"))
 				YYERROR;
 
-			memset(&binat, 0, sizeof(binat));
+			pfctl_init_rule(&binat);
 
 			if ($1 && $3.b1) {
 				yyerror("\"pass\" not valid with \"no\"");
@@ -5249,36 +5108,23 @@ routespec	: route_host			{ $$ = $1; }
 		;
 
 route		: /* empty */			{
-			$$.host = NULL;
-			$$.rt = 0;
-			$$.pool_opts = 0;
+			$$.rt = PF_NOPFROUTE;
 		}
 		| FASTROUTE {
 			/* backwards-compat */
-			$$.host = NULL;
-			$$.rt = 0;
-			$$.pool_opts = 0;
+			$$.rt = PF_NOPFROUTE;
 		}
-		| ROUTETO routespec pool_opts {
-			$$.host = $2;
+		| ROUTETO route_redirspec {
 			$$.rt = PF_ROUTETO;
-			$$.pool_opts = $3.type | $3.opts;
-			if ($3.key != NULL)
-				$$.key = $3.key;
+			$$.redirspec = $2;
 		}
-		| REPLYTO routespec pool_opts {
-			$$.host = $2;
+		| REPLYTO route_redirspec {
 			$$.rt = PF_REPLYTO;
-			$$.pool_opts = $3.type | $3.opts;
-			if ($3.key != NULL)
-				$$.key = $3.key;
+			$$.redirspec = $2;
 		}
-		| DUPTO routespec pool_opts {
-			$$.host = $2;
+		| DUPTO route_redirspec {
 			$$.rt = PF_DUPTO;
-			$$.pool_opts = $3.type | $3.opts;
-			if ($3.key != NULL)
-				$$.key = $3.key;
+			$$.redirspec = $2;
 		}
 		;
 
@@ -6161,12 +6007,144 @@ expand_eth_rule(struct pfctl_eth_rule *r,
 	FREE_LIST(struct node_host, ipdsts);
 }
 
+int
+apply_rdr_ports(struct pfctl_rule *r, struct pfctl_pool *rpool, struct redirspec *rs)
+{
+	if (rs == NULL)
+		return 0;
+
+	rpool->proxy_port[0] = ntohs(rs->rport.a);
+
+	if (!rs->rport.b && rs->rport.t && r->dst.port != NULL) {
+		rpool->proxy_port[1] = ntohs(rs->rport.a) +
+		    (ntohs(r->dst.port[1]) - ntohs(r->dst.port[0]));
+	} else
+		r->rdr.proxy_port[1] = ntohs(rs->rport.b);
+
+	if (rs->pool_opts.staticport) {
+		yyerror("the 'static-port' option is only valid with nat rules");
+		return 1;
+	}
+
+	if (rs->pool_opts.mape.offset) {
+		yyerror("the 'map-e-portset' option is only valid with nat rules");
+		return 1;
+	}
+
+	return 0;
+}
+
+int
+apply_nat_ports(struct pfctl_pool *rpool, struct redirspec *rs)
+{
+	if (rs == NULL)
+		return 0;
+
+	rpool->proxy_port[0] = ntohs(rs->rport.a);
+	rpool->proxy_port[1] = ntohs(rs->rport.b);
+	if (!rpool->proxy_port[0] && !rpool->proxy_port[1]) {
+		rpool->proxy_port[0] =  PF_NAT_PROXY_PORT_LOW;
+		rpool->proxy_port[1] =  PF_NAT_PROXY_PORT_HIGH;
+	} else if (!rpool->proxy_port[1])
+		rpool->proxy_port[1] = rpool->proxy_port[0];
+
+	if (rs->pool_opts.staticport) {
+		if (rpool->proxy_port[0] != PF_NAT_PROXY_PORT_LOW &&
+		    rpool->proxy_port[1] != PF_NAT_PROXY_PORT_HIGH) {
+			yyerror("the 'static-port' option can't"
+			    " be used when specifying a port"
+			    " range");
+			return 1;
+		}
+		rpool->proxy_port[0] = 0;
+		rpool->proxy_port[1] = 0;
+	}
+
+	if (rs->pool_opts.mape.offset) {
+		if (rs->pool_opts.staticport) {
+			yyerror("the 'map-e-portset' option"
+			    " can't be used 'static-port'");
+			return 1;
+		}
+		if (rpool->proxy_port[0] != PF_NAT_PROXY_PORT_LOW &&
+		    rpool->proxy_port[1] != PF_NAT_PROXY_PORT_HIGH) {
+			yyerror("the 'map-e-portset' option"
+			    " can't be used when specifying"
+			    " a port range");
+			return 1;
+		}
+		rpool->mape = rs->pool_opts.mape;
+	}
+
+	return 0;
+}
+
+int
+apply_redirspec(struct pfctl_pool *rpool, struct redirspec *rs)
+{
+	struct node_host	*h;
+	struct pf_pooladdr	*pa;
+
+	if (rs == NULL)
+		return 0;
+
+	rpool->opts = rs->pool_opts.type;
+
+	if ((rpool->opts & PF_POOL_TYPEMASK) == PF_POOL_NONE &&
+	    (rs->host->next != NULL ||
+	    rs->host->addr.type == PF_ADDR_TABLE ||
+	    DYNIF_MULTIADDR(rs->host->addr)))
+		rpool->opts = PF_POOL_ROUNDROBIN;
+
+	if (!PF_POOL_DYNTYPE(rpool->opts) &&
+	    (disallow_table(rs->host, "tables are not supported by pool type") ||
+	    disallow_alias(rs->host, "interface (%s) is not supported by pool type")))
+		return 1;
+
+	if (rs->host->next != NULL &&
+	    ((rpool->opts & PF_POOL_TYPEMASK) != PF_POOL_ROUNDROBIN)) {
+		yyerror("r.route.opts must be PF_POOL_ROUNDROBIN");
+		return 1;
+	}
+
+	if (rs->host->next != NULL) {
+		if ((rpool->opts & PF_POOL_TYPEMASK) !=
+		    PF_POOL_ROUNDROBIN) {
+			yyerror("only round-robin valid for multiple "
+			    "redirection addresses");
+			return 1;
+		}
+	}
+
+	rpool->opts |= rs->pool_opts.opts;
+
+	if (rs->pool_opts.key != NULL)
+		memcpy(&(rpool->key), rs->pool_opts.key,
+		    sizeof(struct pf_poolhashkey));
+
+	TAILQ_INIT(&(rpool->list));
+	for (h = rs->host; h != NULL; h = h->next) {
+		pa = calloc(1, sizeof(struct pf_pooladdr));
+		if (pa == NULL)
+			err(1, "expand_rule: calloc");
+		pa->addr = h->addr;
+		if (h->ifname != NULL) {
+			if (strlcpy(pa->ifname, h->ifname,
+			    sizeof(pa->ifname)) >= sizeof(pa->ifname))
+				errx(1, "expand_rule: strlcpy");
+		} else
+			pa->ifname[0] = 0;
+		TAILQ_INSERT_TAIL(&(rpool->list), pa, entries);
+	}
+
+	return 0;
+}
+
 void
 expand_rule(struct pfctl_rule *r,
     struct node_if *interfaces, struct redirspec *nat,
     struct redirspec *rdr, struct redirspec *route,
-    struct node_host *rdr_hosts, struct node_host *nat_hosts,
-    struct node_host *route_hosts, struct node_proto *protos,
+    struct node_proto *protos,
     struct node_os *src_oses, struct node_host *src_hosts,
     struct node_port *src_ports, struct node_host *dst_hosts,
     struct node_port *dst_ports, struct node_uid *uids, struct node_gid *gids,
@@ -6178,8 +6156,7 @@ expand_rule(struct pfctl_rule *r,
 	char			 label[PF_RULE_MAX_LABEL_COUNT][PF_RULE_LABEL_SIZE];
 	char			 tagname[PF_TAG_NAME_SIZE];
 	char			 match_tagname[PF_TAG_NAME_SIZE];
-	struct pf_pooladdr	*pa;
-	struct node_host	*h, *osrch, *odsth;
+	struct node_host	*osrch, *odsth;
 	u_int8_t		 flags, flagset, keep_state;
 
 	memcpy(label, r->label, sizeof(r->label));
@@ -6317,52 +6294,16 @@ expand_rule(struct pfctl_rule *r,
 			r->os_fingerprint = PF_OSFP_ANY;
 		}
 
-		TAILQ_INIT(&r->rdr.list);
-		for (h = rdr_hosts; h != NULL; h = h->next) {
-			pa = calloc(1, sizeof(struct pf_pooladdr));
-			if (pa == NULL)
-				err(1, "expand_rule: calloc");
-			pa->addr = h->addr;
-			if (h->ifname != NULL) {
-				if (strlcpy(pa->ifname, h->ifname,
-				    sizeof(pa->ifname)) >=
-				    sizeof(pa->ifname))
-					errx(1, "expand_rule: strlcpy");
-			} else
-				pa->ifname[0] = 0;
-			TAILQ_INSERT_TAIL(&r->rdr.list, pa, entries);
-		}
-		TAILQ_INIT(&r->nat.list);
-		for (h = nat_hosts; h != NULL; h = h->next) {
-			pa = calloc(1, sizeof(struct pf_pooladdr));
-			if (pa == NULL)
-				err(1, "expand_rule: calloc");
-			pa->addr = h->addr;
-			if (h->ifname != NULL) {
-				if (strlcpy(pa->ifname, h->ifname,
-				    sizeof(pa->ifname)) >=
-				    sizeof(pa->ifname))
-					errx(1, "expand_rule: strlcpy");
-			} else
-				pa->ifname[0] = 0;
-			TAILQ_INSERT_TAIL(&r->nat.list, pa, entries);
-		}
-		TAILQ_INIT(&r->route.list);
-		for (h = route_hosts; h != NULL; h = h->next) {
-			pa = calloc(1, sizeof(struct pf_pooladdr));
-			if (pa == NULL)
-				err(1, "expand_rule: calloc");
-			pa->addr = h->addr;
-			if (h->ifname != NULL) {
-				if (strlcpy(pa->ifname, h->ifname,
-				    sizeof(pa->ifname)) >=
-				    sizeof(pa->ifname))
-					errx(1, "expand_rule: strlcpy");
-			} else
-				pa->ifname[0] = 0;
-			TAILQ_INSERT_TAIL(&r->route.list, pa, entries);
*** 920 LINES SKIPPED ***