On Sun, Sep 03, 2023 at 04:12:35AM +0200, Alexandr Nedvedicky wrote:
> Hello,
> 
> so there is actually bug. I was able to reproduce it with very simple
> rules on my router:
> 
>     set skip on em1
>     block return all
>     pass out on em0 from 192.168.2.0/24 to any nat-to(em0)
> 
> em1 is interface, facing to LAN
> em0 is interface to internet where NAT happens.
> 
> I did use a scapy to generate ICMP host unreachable:
>     d = '77.75.77.222'
>     s = '192.168.2.5'
>     sp = 1234
>     dp = 12345
>     pkt = Ether()/IP(dst=d)/ICMP(type=3, code=1, )/\
>           IP(src=d', dst=s)/UDP(sport=sp, dport=dp)
>     sendp(pkt, iface='iwn0')
> 
> the packet has been sent from my notebook via LAN towards router.  to my
> surprise router forwards packet untranslated without creating a state.
> 
> the packet indeed matches the 'pass out on em0 rule...'
> however 'keep state' and 'nat-to' actions are ignored. To understand
> why we must look here at pf_test_rule():
> 
> 4475         if (pd->virtual_proto != PF_VPROTO_FRAGMENT
> 4476             && !ctx.state_icmp && r->keep_state) {
> 4477
> ....
> 4491                 action = pf_create_state(pd, r, a, ctx.nr, &skw, &sks,
> 4492                     &rewrite, sm, ctx.tag, &ctx.rules, &ctx.act, 
> ctx.sns);
> 4493
> 4494                 if (action != PF_PASS)
> 4495                         goto cleanup;
> 4496                 if (sks != skw) {
> 4497                         struct pf_state_key     *sk;
> 4498
> 4499                         if (pd->dir == PF_IN)
> 4500                                 sk = sks;
> 4501                         else
> 4502                                 sk = skw;
> 4503                         rewrite += pf_translate(pd,
> 4504                             &sk->addr[pd->af == pd->naf ? pd->sidx : 
> pd->didx],
> 4505                             sk->port[pd->af == pd->naf ? pd->sidx : 
> pd->didx],
> 4506                             &sk->addr[pd->af == pd->naf ? pd->didx : 
> pd->sidx],
> 4507                             sk->port[pd->af == pd->naf ? pd->didx : 
> pd->sidx],
> 4508                             virtual_type, ctx.icmp_dir);
> 4509                 }
> 4510
> 4511 #ifdef INET6
> 4512                 if (rewrite && skw->af != sks->af)
> 4513                         action = PF_AFRT;
> 4514 #endif /* INET6 */
> 4515
> 4516         } else {
> 4517                 while ((ctx.ri = SLIST_FIRST(&ctx.rules))) {
> 4518                         SLIST_REMOVE_HEAD(&ctx.rules, entry);
> 4519                         pool_put(&pf_rule_item_pl, ctx.ri);
> 4520                 }
> 4521         }
> 
> note '!ctx.state_icmp' at line 4476. This variable is being set
> by pf_icmp_mapping() function very early in pf_test_rule() function.
> 
> 4354         switch (pd->virtual_proto) {
> 4355         case IPPROTO_ICMP:
> 4356                 ctx.icmptype = pd->hdr.icmp.icmp_type;
> 4357                 ctx.icmpcode = pd->hdr.icmp.icmp_code;
> 4358                 ctx.state_icmp = pf_icmp_mapping(pd, ctx.icmptype,
> 4359                     &ctx.icmp_dir, &virtual_id, &virtual_type);
> 4360                 if (ctx.icmp_dir == PF_IN) {
> 4361                         pd->osport = pd->nsport = virtual_id;
> 
> for ICMP error messages the ctx.state_icmp value is set to one.
> It happens right here in pf_icmp_mapping():
> 
> 2582                 /* These ICMP types map to other connections */
> 2583                 case ICMP_UNREACH:
> 2584                 case ICMP_SOURCEQUENCH:
> 2585                 case ICMP_REDIRECT:
> 2586                 case ICMP_TIMXCEED:
> 2587                 case ICMP_PARAMPROB:
> 2588                         /* These will not be used, but set them anyway */
> 2589                         *icmp_dir = PF_IN;
> 2590                         *virtual_type = htons(type);
> 2591                         *virtual_id = 0;
> 2592                         return (1);  /* These types match to another 
> state */
> 
> 
> this explains why pf ignores 'keep state' and 'nat-to' action for ICMP errors.
> 
> It's tempting to fix things up in pf_test_rule() in else branch lines 
> 4517-4520.
> However I'm afraid this approach may create some undesired side-effect.
> in my opinion is to fix pf_match_rule() function, so ICMP error message
> will no longer match 'keep state' rule. Diff below is for IPv4. I still
> need to think of more about IPv6. My gut feeling is it will be very similar.
> 
> thanks and
> regards
> sashan

Hi Alexandr,

Give me until monday to test this patch as I'm not at home currently.  Thank
you for taking time out of your WE to test this.  Emotionally I'm happy and
sad at the same time.  Happy, that the report was not wrong, and sad that it
is correct.  Also my custom spoofer has once again found a bug in OpenBSD I 
don't know if I should be proud of that, or not.

Best Regards,
-peter



> --------8<---------------8<---------------8<------------------8<--------
> diff --git a/sys/net/pf.c b/sys/net/pf.c
> index 4f0fc3f91a9..0993aed85fb 100644
> --- a/sys/net/pf.c
> +++ b/sys/net/pf.c
> @@ -4148,6 +4148,9 @@ enter_ruleset:
>                           (r->rule_flag & PFRULE_STATESLOPPY) == 0 &&
>                           ctx->icmp_dir != PF_IN),
>                               TAILQ_NEXT(r, entries));
> +                     /* icmp packet must match existing state */
> +                     PF_TEST_ATTRIB(r->keep_state && ctx->state_icmp,
> +                             TAILQ_NEXT(r, entries);
>                       break;
>  
>               case IPPROTO_ICMPV6:

-- 
Over thirty years experience on Unix-like Operating Systems starting with QNX.

Reply via email to