I want to limit incoming connections on a server to 5 per IP. I don't want to put violators into a pf table (overload) or kill the other connections (flush), I just want to not accept new connections from that IP once their limit is reached and then accept them again when they are under the limit. Is this broken or am I holding it wrong?
With a simple pf.conf on the server specifying max-src-conn of 5: set skip on lo block return pass out pass in on egress proto tcp to port 80 keep state \ (source-track rule, max-src-conn 5) Run a dumb webserver that prints the pf states on each new connection, and sleeps for a while before responding to keep the connection open: $ doas ruby require "webrick" server = WEBrick::HTTPServer.new(:Port => 80) server.mount_proc "/" do |req, res| puts "states for #{req.remote_ip}:" system "pfctl -ss | grep ' #{req.remote_ip}.*ESTABLISHED'" sleep 30 end trap "INT" do server.shutdown end server.start ^D Now from another machine (192.168.1.196 in this case) send 20 requests at once to the server (192.168.1.240): $ for f in `jot 20`; do ftp -o - http://192.168.1.240/ &; sleep 0.5; done And on the server, you can see that there are now many more than 5 established states: states for 192.168.1.196: all tcp 192.168.1.240:80 <- 192.168.1.196:16727 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 <- 192.168.1.196:24725 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 <- 192.168.1.196:2436 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 <- 192.168.1.196:16529 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 <- 192.168.1.196:4323 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:45576 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:36693 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:2976 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:9395 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:46616 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:46122 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:22969 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:48742 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:43477 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:29154 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:1876 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:47743 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:43833 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:12074 ESTABLISHED:ESTABLISHED all tcp 192.168.1.240:80 -> 192.168.1.196:17816 ESTABLISHED:SYN_SENT Looking at pf.c, the logic in pf_tcp_track_full seems to indicate that it's accepting the connection (moving it to TCPS_ESTABLISHED), then checking pf_src_connlimit, but the PF_DROP gets ignored because the connection is already established. Shouldn't it not accept the connection if pf_src_connlimit returns 1? This does that: diff --git sys/net/pf.c sys/net/pf.c index 8cb1326a160..89703feab12 100644 --- sys/net/pf.c +++ sys/net/pf.c @@ -481,12 +481,10 @@ pf_src_connlimit(struct pf_state **stp) if ((sn = pf_get_src_node((*stp), PF_SN_NONE)) == NULL) return (0); - sn->conn++; - (*stp)->src.tcp_est = 1; pf_add_threshold(&sn->conn_rate); if ((*stp)->rule.ptr->max_src_conn && - (*stp)->rule.ptr->max_src_conn < sn->conn) { + sn->conn >= (*stp)->rule.ptr->max_src_conn) { pf_status.lcounters[LCNT_SRCCONN]++; bad++; } @@ -497,8 +495,11 @@ pf_src_connlimit(struct pf_state **stp) bad++; } - if (!bad) + if (!bad) { + sn->conn++; + (*stp)->src.tcp_est = 1; return (0); + } if ((*stp)->rule.ptr->overload_tbl) { struct pfr_addr p; @@ -4919,14 +4920,14 @@ pf_tcp_track_full(struct pf_pdesc *pd, struct pf_state **stp, u_short *reason, pf_set_protostate(*stp, psrc, TCPS_CLOSING); if (th->th_flags & TH_ACK) { if (dst->state == TCPS_SYN_SENT) { - pf_set_protostate(*stp, pdst, - TCPS_ESTABLISHED); - if (src->state == TCPS_ESTABLISHED && + if (src->state == TCPS_SYN_SENT && !SLIST_EMPTY(&(*stp)->src_nodes) && pf_src_connlimit(stp)) { REASON_SET(reason, PFRES_SRCLIMIT); return (PF_DROP); } + pf_set_protostate(*stp, pdst, + TCPS_ESTABLISHED); } else if (dst->state == TCPS_CLOSING) pf_set_protostate(*stp, pdst, TCPS_FIN_WAIT_2);