Hello,

I have been thinking about a method that would allow userland programs
to create dynamic rules. I have come up with the following:

* Create a new rule type called 'chain'. It has the combined syntax of
  pass and block. It is parsed and installed as a normal rule and
  defaults to block. It must contain a (unique) label. 

* Userland programs address the rule using the label, and add their
  own 'chain-rules' to the list of chain-rules contained in the rule.

* A chain-rule contains an address/port pair for souce and destination
  of the rule. It can also contain fields for any other customizations
  that may be needed, including a hit-count and a timeout.

* When a rule with chain action is hit, the list of chain-rules is
  traversed and if a match is found, the rule acts as a 'pass' rule.
  If there is no match, it acts as a block rule.

* This idea can be extended to NAT/RDR rules by including a chain flag
  and labels.


Advantages:

* Admin can completely control the behaviour of the application that
  is going to insert the rule.

* Low overhead.

* Application interface is very simple. Labels used in pf.conf are
  passed to the application and application performs a single ioctl
  with unknown information set to zero. Application needs no further
  knowledge of rules or system.

* Chain-rules expire on a hit count or age. Crashed applications do
  not leave rules behind.


Disadvantages:

* NAT/RDR does not have labels yet.

* a better name? proxy/extern/dynamic come to mind.


Implementation:

I have attached a draft implementation. It is for tcp and udp only.
chain-rules are one-shot but have expiration timers. I have
also included a test application that adds chain-rules to rule for a
given port.


Comments ?

Can
Index: pf.c
===================================================================
RCS file: /cvs/src/sys/net/pf.c,v
retrieving revision 1.247
diff -u -p -r1.247 pf.c
--- pf.c        2002/10/05 21:17:57     1.247
+++ pf.c        2002/10/06 15:08:12
@@ -142,6 +142,7 @@ int                 *pftm_timeouts[PFTM_MAX] = { &pftm
 
 struct pool             pf_tree_pl, pf_rule_pl, pf_nat_pl, pf_sport_pl;
 struct pool             pf_rdr_pl, pf_state_pl, pf_binat_pl, pf_addr_pl;
+struct pool             pf_chain_pl, pf_chainlist_pl;
 
 void                    pf_addrcpy(struct pf_addr *, struct pf_addr *,
                            u_int8_t);
@@ -615,6 +616,7 @@ pf_purge_timeout(void *arg)
        s = splsoftnet();
        pf_purge_expired_states();
        pf_purge_expired_fragments();
+       pf_purge_expired_chains();
        splx(s);
 
        timeout_add(to, pftm_interval * hz);
@@ -659,6 +661,33 @@ pf_purge_expired_states(void)
        }
 }
 
+void pf_purge_expired_chains(void)
+{
+       struct pf_rule *rule;
+       struct pf_chain *cur, *prev;
+       
+       for (rule = TAILQ_FIRST(pf_rules_active);
+            rule != NULL; rule = TAILQ_NEXT(rule, entries)) {
+               if (rule->action != PF_CHAIN)
+                       continue;
+               prev = NULL;
+               cur = LIST_FIRST(rule->chainlist);
+               while (cur != NULL) {
+                       if (cur->expire > time.tv_sec) {
+                               prev = cur;
+                               cur = LIST_NEXT(cur, entries);
+                               continue;
+                       }
+                       LIST_REMOVE(cur, entries);
+                       pool_put(&pf_chain_pl, cur);
+                       if (prev == NULL)
+                               cur = LIST_FIRST(rule->chainlist);
+                       else
+                               cur = LIST_NEXT(prev, entries);
+               }
+       }
+}
+
 int
 pf_dynaddr_setup(struct pf_addr_wrap *aw, u_int8_t af)
 {
@@ -757,6 +786,46 @@ pf_dynaddr_copyout(struct pf_addr_wrap *
 }
 
 void
+pf_chainlist_remove(struct pf_chainlist *clist)
+{
+       struct pf_chain *cr;
+
+       if (clist == NULL)
+               return;
+
+       while ((cr = LIST_FIRST(clist)) != NULL) {
+               LIST_REMOVE(cr, entries);
+               pool_put(&pf_chain_pl, cr);
+       }
+
+       pool_put(&pf_chainlist_pl, clist);
+}
+
+/* assumes label contains space for at least PF_RULE_LABEL_SIZE bytes */
+struct pf_rule *
+pf_get_rule_by_label(char *label)
+{
+       struct pf_rule *r;
+       int n;
+
+       if (label == NULL)
+               return NULL;
+
+       r = TAILQ_FIRST(pf_rules_active);
+       while (r != NULL) {
+               for (n = 0; n < PF_RULE_LABEL_SIZE; n++) {
+                       if (label[n] != r->label[n])
+                               break;
+                       if (label[n] == '\0')
+                               return (r);
+               }
+               r = TAILQ_NEXT(r, entries);
+       }
+
+       return (NULL);
+}
+
+void
 pf_print_host(struct pf_addr *addr, u_int16_t p, u_int8_t af)
 {
        switch(af) {
@@ -1605,8 +1674,9 @@ pf_test_tcp(struct pf_rule **rm, int dir
        uid_t uid;
        gid_t gid;
        struct pf_rule *r;
+       struct pf_chain *cr;
        u_short reason;
-       int rewrite = 0, error;
+       int rewrite = 0, error, action;
 
        *rm = NULL;
 
@@ -1727,14 +1797,49 @@ pf_test_tcp(struct pf_rule **rm, int dir
                (*rm)->packets++;
                (*rm)->bytes += pd->tot_len;
                REASON_SET(&reason, PFRES_MATCH);
+               
+               action = (*rm)->action;
+
+               if (action == PF_CHAIN) {
+                       cr = LIST_FIRST((*rm)->chainlist);
+                       
+                       action = PF_DROP;
+
+                       while (cr != NULL) {
+                               if ((cr->sport == 0 ||
+                                    cr->sport == th->th_sport) &&
+                                   (cr->dport == 0 ||
+                                    cr->dport == th->th_dport) &&
+                                   (PF_AZERO(&cr->src, af) ||
+                                    PF_AEQ(&cr->src, saddr, af)) &&
+                                   (PF_AZERO(&cr->dst, af) ||
+                                    PF_AEQ(&cr->dst, daddr, af)))
+                                       break;
+                               cr = LIST_NEXT(cr, entries);
+                       }
+                       if (cr != NULL) {
+                               int s;
 
+                               if (cr->expire > time.tv_sec)
+                                       action = PF_PASS;
+                               /* one shot for now can add a hit counter
+                                * XXX is splsoftnet here correct?
+                                */
+                               s = splsoftnet();
+                               LIST_REMOVE(cr, entries);
+                               pool_put(&pf_chain_pl, cr);
+                               splx(s);
+                       }
+               }
+
                if ((*rm)->log) {
                        if (rewrite)
                                m_copyback(m, off, sizeof(*th), (caddr_t)th);
+                       /* XXX need to pass action to log */
                        PFLOG_PACKET(ifp, h, m, af, direction, reason, *rm);
                }
 
-               if (((*rm)->action == PF_DROP) &&
+               if ((action == PF_DROP) &&
                    (((*rm)->rule_flag & PFRULE_RETURNRST) ||
                    (*rm)->return_icmp)) {
                        /* undo NAT/RST changes, if they have taken place */
@@ -1757,7 +1862,7 @@ pf_test_tcp(struct pf_rule **rm, int dir
                                    (*rm)->return_icmp & 255, af);
                }
 
-               if ((*rm)->action == PF_DROP) 
+               if (action == PF_DROP) 
                        return (PF_DROP);
        }
 
@@ -1866,8 +1971,9 @@ pf_test_udp(struct pf_rule **rm, int dir
        uid_t uid;
        gid_t gid;
        struct pf_rule *r;
+       struct pf_chain *cr;
        u_short reason;
-       int rewrite = 0, error;
+       int action, rewrite = 0, error;
 
        *rm = NULL;
 
@@ -1990,13 +2096,47 @@ pf_test_udp(struct pf_rule **rm, int dir
                (*rm)->bytes += pd->tot_len;
                REASON_SET(&reason, PFRES_MATCH);
 
+               action = (*rm)->action;
+
+               if (action == PF_CHAIN) {
+                       cr = LIST_FIRST((*rm)->chainlist);
+                       
+                       action = PF_DROP;
+
+                       while (cr != NULL) {
+                               if ((cr->sport == 0 ||
+                                    cr->sport == uh->uh_sport) &&
+                                   (cr->dport == 0 ||
+                                    cr->dport == uh->uh_dport) &&
+                                   (PF_AZERO(&cr->src, af) ||
+                                    PF_AEQ(&cr->src, saddr, af)) &&
+                                   (PF_AZERO(&cr->dst, af) ||
+                                    PF_AEQ(&cr->dst, daddr, af)))
+                                       break;
+                               cr = LIST_NEXT(cr, entries);
+                       }
+                       if (cr != NULL) {
+                               int s;
+
+                               if (cr->expire > time.tv_sec)
+                                       action = PF_PASS;
+                               /* one shot for now can add a hit counter
+                                * XXX is splsoftnet here correct?
+                                */
+                               s = splsoftnet();
+                               LIST_REMOVE(cr, entries);
+                               pool_put(&pf_chain_pl, cr);
+                               splx(s);
+                       }
+               }
+
                if ((*rm)->log) {
                        if (rewrite)
                                m_copyback(m, off, sizeof(*uh), (caddr_t)uh);
                        PFLOG_PACKET(ifp, h, m, af, direction, reason, *rm);
                }
 
-               if (((*rm)->action == PF_DROP) && (*rm)->return_icmp) {
+               if ((action == PF_DROP) && (*rm)->return_icmp) {
                        /* undo NAT/RST changes, if they have taken place */
                        if (nat != NULL ||
                            (binat != NULL && direction == PF_OUT)) {
@@ -2013,7 +2153,7 @@ pf_test_udp(struct pf_rule **rm, int dir
                            (*rm)->return_icmp & 255, af);
                }
 
-               if ((*rm)->action == PF_DROP) 
+               if (action == PF_DROP) 
                        return (PF_DROP);
        }
 
Index: pfvar.h
===================================================================
RCS file: /cvs/src/sys/net/pfvar.h,v
retrieving revision 1.90
diff -u -p -r1.90 pfvar.h
--- pfvar.h     2002/10/05 21:17:57     1.90
+++ pfvar.h     2002/10/06 15:08:22
@@ -38,7 +38,7 @@
 #include <sys/tree.h>
 
 enum   { PF_IN=0, PF_OUT=1 };
-enum   { PF_PASS=0, PF_DROP=1, PF_SCRUB=2 };
+enum   { PF_PASS=0, PF_DROP=1, PF_SCRUB=2, PF_CHAIN=3 };
 enum   { PF_OP_IRG=1, PF_OP_EQ=2, PF_OP_NE=3, PF_OP_LT=4,
          PF_OP_LE=5, PF_OP_GT=6, PF_OP_GE=7, PF_OP_XRG=8 };
 enum   { PF_DEBUG_NONE=0, PF_DEBUG_URGENT=1, PF_DEBUG_MISC=2 };
@@ -216,6 +216,19 @@ struct pf_rule_addr {
        u_int8_t                 noroute;
 };
 
+struct pf_chain {
+       struct pf_addr          src;
+       struct pf_addr          dst;
+       LIST_ENTRY(pf_chain)    entries;
+       u_int32_t               expire;
+       u_int16_t               sport;
+       u_int16_t               dport;
+       u_int8_t                type;
+       u_int8_t                code;   
+};
+
+LIST_HEAD(pf_chainlist, pf_chain);
+
 struct pf_rule {
        struct pf_rule_addr      src;
        struct pf_rule_addr      dst;
@@ -242,6 +255,7 @@ struct pf_rule {
        u_int64_t                packets;
        u_int64_t                bytes;
 
+       struct pf_chainlist     *chainlist;;
        struct ifnet            *ifp;
        struct ifnet            *rt_ifp;
 
@@ -596,6 +610,11 @@ struct pfioc_limit {
        unsigned         limit;
 };
 
+struct pfioc_chain {
+       char             label[PF_RULE_LABEL_SIZE];
+       struct pf_chain  chain;
+};
+
 /*
  * ioctl operations
  */
@@ -641,7 +660,7 @@ struct pfioc_limit {
 #define DIOCGETLIMIT   _IOWR('D', 39, struct pfioc_limit)
 #define DIOCSETLIMIT   _IOWR('D', 40, struct pfioc_limit)
 #define DIOCKILLSTATES _IOWR('D', 41, struct pfioc_state_kill)
-
+#define DIOCCHAINRULE   _IOWR('D', 42, struct pfioc_chain)
 
 #ifdef _KERNEL
 RB_HEAD(pf_state_tree, pf_tree_node);
@@ -682,7 +701,7 @@ extern void                  pf_calc_skip_steps(struct
 extern void                     pf_dynaddr_copyout(struct pf_addr_wrap *);
 extern struct pool              pf_tree_pl, pf_rule_pl, pf_nat_pl;
 extern struct pool              pf_rdr_pl, pf_state_pl, pf_binat_pl,
-                                   pf_addr_pl;
+                                   pf_addr_pl, pf_chainlist_pl, pf_chain_pl;
 extern void                     pf_purge_timeout(void *);
 extern int                      pftm_interval;
 extern int                      pf_compare_rules(struct pf_rule *,
@@ -701,6 +720,8 @@ extern struct ifnet         *status_ifp;
 extern int                     *pftm_timeouts[PFTM_MAX];
 extern void                     pf_addrcpy(struct pf_addr *, struct pf_addr *,
                                    u_int8_t);
+extern void                     pf_chainlist_remove(struct pf_chainlist *);
+extern void                     pf_purge_expired_chains(void);
 
 #ifdef INET
 int    pf_test(int, struct ifnet *, struct mbuf **);
@@ -723,6 +744,8 @@ void        pf_normalize_init(void);
 int    pf_normalize_ip(struct mbuf **, int, struct ifnet *, u_short *);
 void   pf_purge_expired_fragments(void);
 int    pf_routable(struct pf_addr *addr, int af);
+
+extern struct pf_rule *pf_get_rule_by_label(char *);
 
 extern struct pf_rulequeue *pf_rules_active;
 extern struct pf_status pf_status;
Index: pf_ioctl.c
===================================================================
RCS file: /cvs/src/sys/net/pf_ioctl.c,v
retrieving revision 1.8
diff -u -p -r1.8 pf_ioctl.c
--- pf_ioctl.c  2002/08/12 16:41:25     1.8
+++ pf_ioctl.c  2002/10/06 15:08:30
@@ -89,6 +89,10 @@ pfattach(int num)
            NULL);
        pool_init(&pf_addr_pl, sizeof(struct pf_addr_dyn), 0, 0, 0, "pfaddr",
            NULL);
+       pool_init(&pf_chainlist_pl, sizeof(struct pf_chainlist), 0, 0, 0,
+           "pfchlistpl", NULL);
+       pool_init(&pf_chain_pl, sizeof(struct pf_chain), 0, 0, 0, "pfchainpl",
+           NULL);
 
        TAILQ_INIT(&pf_rules[0]);
        TAILQ_INIT(&pf_rules[1]);
@@ -217,6 +221,7 @@ pfioctl(dev_t dev, u_long cmd, caddr_t a
                        TAILQ_REMOVE(pf_rules_inactive, rule, entries);
                        pf_dynaddr_remove(&rule->src.addr);
                        pf_dynaddr_remove(&rule->dst.addr);
+                       pf_chainlist_remove(rule->chainlist);
                        pool_put(&pf_rule_pl, rule);
                }
                *ticket = ++ticket_rules_inactive;
@@ -282,6 +287,19 @@ pfioctl(dev_t dev, u_long cmd, caddr_t a
                        error = EINVAL;
                        break;
                }
+               if (rule->action == PF_CHAIN) {
+                       /* XXX need validation? */
+                       rule->chainlist = pool_get(&pf_chainlist_pl,
+                                                  PR_NOWAIT);
+                       if (rule->chainlist == NULL) {
+                               pf_dynaddr_remove(&rule->src.addr);
+                               pf_dynaddr_remove(&rule->dst.addr);
+                               pool_put(&pf_rule_pl, rule);
+                               error = ENOMEM;
+                               break;
+                       }
+                       LIST_INIT(rule->chainlist);
+               }
                rule->evaluations = rule->packets = rule->bytes = 0;
                TAILQ_INSERT_TAIL(pf_rules_inactive, rule, entries);
                break;
@@ -317,6 +335,7 @@ pfioctl(dev_t dev, u_long cmd, caddr_t a
                        TAILQ_REMOVE(old_rules, rule, entries);
                        pf_dynaddr_remove(&rule->src.addr);
                        pf_dynaddr_remove(&rule->dst.addr);
+                       pf_chainlist_remove(rule->chainlist);
                        pool_put(&pf_rule_pl, rule);
                }
                break;
@@ -419,6 +438,19 @@ pfioctl(dev_t dev, u_long cmd, caddr_t a
                                error = EINVAL;
                                break;
                        }
+                       if (newrule->action == PF_CHAIN) {
+                               /* XXX need validation? */
+                               newrule->chainlist = pool_get(&pf_chainlist_pl,
+                                                PR_NOWAIT);
+                               if (newrule->chainlist == NULL) {
+                                       pf_dynaddr_remove(&newrule->src.addr);
+                                       pf_dynaddr_remove(&newrule->dst.addr);
+                                       pool_put(&pf_rule_pl, newrule);
+                                       error = ENOMEM;
+                                       break;
+                               }
+                               LIST_INIT(newrule->chainlist);
+                       }
                        newrule->evaluations = newrule->packets = 0;
                        newrule->bytes = 0;
                }
@@ -450,6 +482,7 @@ pfioctl(dev_t dev, u_long cmd, caddr_t a
                        TAILQ_REMOVE(pf_rules_active, oldrule, entries);
                        pf_dynaddr_remove(&oldrule->src.addr);
                        pf_dynaddr_remove(&oldrule->dst.addr);
+                       pf_chainlist_remove(oldrule->chainlist);
                        pool_put(&pf_rule_pl, oldrule);
                } else {
                        if (oldrule == NULL)
@@ -1476,6 +1509,44 @@ pfioctl(dev_t dev, u_long cmd, caddr_t a
                TAILQ_FOREACH(rule, pf_rules_active, entries)
                        rule->evaluations = rule->packets =
                            rule->bytes = 0;
+               splx(s);
+               break;
+       }
+
+       case DIOCCHAINRULE: {
+               struct pfioc_chain *pc = (struct pfioc_chain *)addr;
+               struct pf_chain *chain;
+               struct pf_rule *rule;
+
+               s = splsoftnet();
+               rule = pf_get_rule_by_label(pc->label);
+               if (rule == NULL) {
+                       error = ENOENT;
+                       splx(s);
+                       break;
+               }
+
+               if (rule->action != PF_CHAIN) {
+                       error = EINVAL;
+                       splx(s);
+                       break;
+               }
+
+               /* XXX check rule and input chain */
+
+               chain = pool_get(&pf_chain_pl, PR_NOWAIT);
+               if (chain == NULL) {
+                       error = ENOMEM;
+                       splx(s);
+                       break;
+               }
+
+               bcopy(&pc->chain, chain, sizeof(struct pf_chain));
+               chain->expire += time.tv_sec;
+
+               /* XXX does 'entries' need to be cleared before use? 
+                  bzero(&chain->entries, sizeof(chain->entries)); */
+               LIST_INSERT_HEAD(rule->chainlist, chain, entries);
                splx(s);
                break;
        }
? pfctl
? pfctl.cat8
Index: parse.y
===================================================================
RCS file: /cvs/src/sbin/pfctl/parse.y,v
retrieving revision 1.154
diff -u -p -r1.154 parse.y
--- parse.y     2002/09/22 15:30:15     1.154
+++ parse.y     2002/10/06 15:06:18
@@ -243,7 +243,7 @@ typedef struct {
 %token RETURNRST RETURNICMP RETURNICMP6 PROTO INET INET6 ALL ANY ICMPTYPE
 %token ICMP6TYPE CODE KEEP MODULATE STATE PORT RDR NAT BINAT ARROW NODF
 %token MINTTL ERROR ALLOWOPTS FASTROUTE ROUTETO DUPTO NO LABEL
-%token NOROUTE FRAGMENT USER GROUP MAXMSS MAXIMUM TTL
+%token NOROUTE FRAGMENT USER GROUP MAXMSS MAXIMUM TTL CHAIN
 %token FRAGNORM FRAGDROP FRAGCROP
 %token SET OPTIMIZATION TIMEOUT LIMIT LOGINTERFACE
 %token ANTISPOOF FOR
@@ -542,6 +542,7 @@ pfrule              : action dir logquick interface 
 
 action         : PASS                  { $$.b1 = PF_PASS; $$.b2 = $$.w = 0; }
                | BLOCK blockspec       { $$ = $2; $$.b1 = PF_DROP; }
+               | CHAIN blockspec       { $$ = $2; $$.b1 = PF_CHAIN; }
                ;
 
 blockspec      : /* empty */           { $$.b2 = 0; $$.w = 0; }
@@ -1709,6 +1710,10 @@ rule_consistent(struct pf_rule *r)
                yyerror("keep state on block rules doesn't make sense");
                problems++;
        }
+       if (r->action == PF_CHAIN && *r->label == '\0') {
+               yyerror("chain rules require a label");
+               problems++;
+       }
        return (-problems);
 }
 
@@ -2233,6 +2238,7 @@ lookup(char *s)
                { "any",        ANY},
                { "binat",      BINAT},
                { "block",      BLOCK},
+               { "chain",      CHAIN},
                { "code",       CODE},
                { "crop",       FRAGCROP},
                { "drop-ovl",   FRAGDROP},
Index: pfctl_parser.c
===================================================================
RCS file: /cvs/src/sbin/pfctl/pfctl_parser.c,v
retrieving revision 1.94
diff -u -p -r1.94 pfctl_parser.c
--- pfctl_parser.c      2002/07/20 18:58:44     1.94
+++ pfctl_parser.c      2002/10/06 15:06:20
@@ -630,8 +630,8 @@ print_rule(struct pf_rule *r)
        printf("@%d ", r->nr);
        if (r->action == PF_PASS)
                printf("pass ");
-       else if (r->action == PF_DROP) {
-               printf("block ");
+       else if (r->action == PF_DROP || r->action == PF_CHAIN) {
+               printf("%s ", r->action == PF_DROP ? "block":"chain");
                if (r->rule_flag & PFRULE_RETURNRST) {
                        if (!r->return_ttl)
                                printf("return-rst ");
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>

#include <net/if.h>
#include <netinet/in.h>
#include <net/pfvar.h>

#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>

int
ioctl_pf_chain_add(int fd, char *label, int port, int expire)
{
        struct pfioc_chain c;
        bzero(&c.chain, sizeof(c.chain));
        c.chain.src.addr32[0]=htonl(0);
        c.chain.dst.addr32[0]=htonl(0);
        c.chain.sport=htons(0);
        c.chain.dport=htons(port);
        c.chain.expire=expire;

        strncpy(c.label, label, PF_RULE_LABEL_SIZE);

        printf("chain label %s:\n", label);

        if (ioctl(fd, DIOCCHAINRULE, &c)) {
                perror("ioctl");
                return 1;
        }

        return 0;
}

void usage(void)
{
        printf("Usage: test <label> <port> <timeout>\n");
        exit(1);
}

int
main(int argc, char *argv[])
{
        int fd, ret;

        if (argc != 4) {
                usage();
                /* NOTREACHED */
        }
        
        fd = open("/dev/pf", O_EXCL | O_RDWR, 0);
        
        if (fd < 0) {
                perror("open/rw");
                return 1;
        }
        
        ret = ioctl_pf_chain_add(fd, argv[1], atoi(argv[2]), atoi(argv[3]));

        close(fd);

        return ret;
}

Reply via email to