[HEADS UP!] IPFW Ideas: possible SoC 2008 candidate
Vadim Goncharov
vadim_nuclight at mail.ru
Sun Mar 23 18:52:16 UTC 2008
Hi!
[Sorry if it is too late for SoC, but I was unexpectedly busy last 3 days
and couldn't finish this text earlier.]
This is a proposal for ipfw improving ideas and architectural changes.
Some of them are independent of each other and could be implemented
without ABI breaking in STABLE, but, whether all of these will be a
SoC 2008 candidate or not, should be finally implemented in FreeBSD.
The only question is what should be corrected, so please discuss it :)
This text also includes slightly changed and/or generalized ideas from:
http://lists.freebsd.org/pipermail/freebsd-ipfw/2007-April/002931.html
All syntax examples are only to give idea, this should be discussed.
1. Major changings (ABI breaking is necesary).
1.1. Dynamic rules reorganizing.
Description:
Current ipfw's dynamic rules are not suitable for several advanced
tricks. For example, it is not possible to use saved information about
current state of connection in the firewall rules elsewhere, and it is
not possible to change that state from firewall also.
Wanted features:
* Ability to create/delete dynamic rule in any state via some API or ABI
from all parts of system: userland, ipfw rules, other kernel modules.
This can be useful for:
a) Creating dynamic rule in the middle of connection, not only setup:
ipfw add pipe 1 ip from any to any tagged 412 keep-state-middle
This allows to change handling of connection after some event,
e.g. L7 filtering by ng_bpf + ng_tag discovered that a connection
belongs to some class by analyzing packet payload, and from now on
connection should go directly with dynamic rules, but never sent
again to expensive L7 processing.
Currently you can use just "keep-state" for this, but ipfw will
not see SYN's and rule will be subject to sysctl
net.inet.ip.fw.dyn_rst_lifetime - by default expires after
1 second, which is undesirable for many cases.
b) Ability to save/load dynamic rules in userland with files, e.g.,
to continue after reboot.
c) Ability to exchange with rules state with other machine with ipfw,
e.g., two firewalls in a CARP failover.
d) Creation of rule with specified state and parameter before actual
connection would be established. E.g. imagine a by-default-closed
firewall with a netgraph(4) module analyzing FTP control
connection and giving commands to ipfw to open dynamic "holes" for
data connections, thus elimanating current practice of opening
ports in the entire range 1024-65535 (insecure, yes).
One can think about providing direct exchange between libalias(3)'s
alias_link and ipfw dynamic rules, but that's a subject for further
research.
* Additional fields in dynamic rules to keep arbitrary info for
specific connection, and opcodes for loading and storing that values
from other parts of firewall or elsewhere. This will allow to
implement a pf(4)'s "scrub" maximum TTL enforcing on connection,
but not only that - generic data storage allows any future extensions.
* Ability to change dynamic rule's parent rule "on the fly" (just changing
a pointer to which static rule's ACTION_PTR to jump, yes). The latter
will allow aforementioned distinguishing of connection packets
before/after L7 processing in the case where packets are always
classified to flows before any processing takes place - that example
with "keep-state-middle" assumed that main firewall is stateless,
only L7-matched packets are subject to be dynamic. And this allows to
reassign an action for dynamic rule:
...
ipfw add 100 check-state
...
ipfw add 200 skipto 500 ip from any to any keep-state
...
ipfw add 500 netgraph 41 ip from any to any
ipfw add 600 change-parent 800 ip from any to any tagged 412
ipfw add 700 allow ip from any to any
ipfw add 800 pipe 1 ip from any to any
* More types for dynamic rules system would allow not only "keep-state"
and "limit", but rather be extensible to something more. E.g., current
"limit" rules just drop packets if limit is reached - but user
possibly wants an option to process them with another rule afterwards.
Possible implementation:
* For arbitrary info: add a union of one uint32_t or two uint16_t's or
four uint8_t's two each dynamic rules and operations to load/store
those values (or may be an uint64_t and two uin32_t's and so on?..).
Also add one void* to allow to store more data if one needs to.
* Make a special netgraph node (or extend ng_ipfw) which will broadcast
every change in dynamic rules to all it's hooks (how many to bundle
into one mbuf should be customizable). Every input with structs of
the same format will result in addition or deletion of dynamic rules
in ipfw. A netgraph node method of work provides flexible and extensible
way to manipulate dynamic rules: you can connect to it protocol-trackers
which will insert rules for secondary connection (e.g. FTP); you can
connect to it userland tool which will log all dynamic rule changing
or will do load/save of rules in a file; you can connect to it an
ng_ksocket(4) node with UDP to broadcast to someone or TCP to connect
to another machine with the same setup to provide CARP failover.
Note that node should not do delivery/retransmission checks as
pfsync(4) does, because this is a task for someone other (to keep
modularity), but two such nodes on different machines connected to
each other should provide automatic rules synchronizing without
additional actions after initial setup.
1.2. Userland (and other subsystems) interaction, modularity, rulesets.
Description:
Currently /sbin/ipfw2 is a custom-made parser which communicates with
the kernel via setsockopt() calls. It is sometimes hard to extend with
new features due to complex code. Using a socket instead a /dev entry
means you always need to be root (uid 0) to both read firewall
configuration and to change it. In-kernel protocol is also sometimes
hard to extend, while some addional entire-ruleset features are useful.
Wanted features:
* Parser's code (sbin/ipfw2.c) should be reviewed and possibly
rewritten using lex(1)/yacc(1). Syntax is ocmplicated, however, and
it may be not possible to not implement all of it exactly. This
should be further investigated.
* It may be desirable to give some other user ability to at least read
config and may be to write, as /dev/bpf* permissions allow it for
tcpdump(1).
* Device entry could also improve modularity: currently to add a new
IP_FW_* socket option, you have to modify netinet/raw_ip.c, which
means you can't just recompile /sbin/ipfw and ipfw kernel module.
* The same applies to other ipfw-related facilities: dummynet, divert,
NAT. It can be good to keep them configurable by some other means
rather than tweaking raw_ip.c. It can be useful to separate dummynet
and divert to it's own facilities to be able to use them without
ipfw, e.g., from netgraph(4). Related to this is a problem with IPSEC
interaction - if you use it with divert(4) on output, then on return
from divert packets will be IPSEC'ed again because in ip_output()
IPSEC is called before pfil(9). It could be useful to add an option
for user (in addition to existing behaviour, to not break POLA) to
call IPSEC processing from specified place in ruleset just like all
others:
ipfw add ipsec ip from any to any out
* As patch about using rule counters is currently discussed in ipfw@, it
is useful to add ability to change rule counters to arbitrary values
rather than providing the only "zero" action. This is closely related
with an option of restore ipfw's static ruleset without losing
counter values. Currently you can save "ipfw list" to file, do an
"awk '{print "add " $0}'" on it and then load it again (e.g. after
reboot). It must be possible to do the same with "ipfw show". Syntax
example for providing counters with "ipfw add" - all cases are
distinguishable (current syntax allow only first two):
ipfw add allow ip from any to any # select next rule number
ipfw add 100 allow ip from any to any # exact rule number specified
ipfw add 1234 76845 allow ip from any to any # counters without rulenum
ipfw add 100 1234 76845 allow ip from any to any # rulenum and counters
* Static ruleset loading and saving is closely related with ruleset
precompilation and atomic commits. Imagine a rulesets with thousands
of rules: if a packet arrives in the middle of ruleset updating,
strange effects can occur. Of course, you can achieve the same
results with sets, by disabling new set and atomically swapping them
later, but that is not always comfortable. Precomplilation of the
whole ruleset and then atomically installing it ("transaction commit")
requires an implementation which will also allow saving and loading
precompiled ruleset in binary form - good for routers where 20K-rules
script can be processed for several minutes.
* Precompiled binary rules can also be used for the same rule setting
from both other kernel subsytems and other machines (CARP again).
Thus, generic binary rule format/protocol (not only for /dev) might
be invented. Moreover, compiled ruleset format may be different from
current linked list, which has disadvantages of both initial "skipto"
(and planned "call/return", see next section) and disabled-set-rules
are still traversed. Precompiled form of opcodes-only allows to do
quick jumps, easy running of cross-rule optimizations (and even
possibility to compile ipfw opcodes to machine code like BPF_JITTER
for bpf(4) for more speed). This has disadvantages of separate rule
counters keeping and not-so-transparent need for user to recompile
every time, so should be further investigated.
* About several rulesets, for different interfaces (or hacks like
per-interface setting of rule number to jump to on it): I think that
this is unnecessary and unfriendly to user - having one rulesets is
simpler, and you usually need common checks on packets. So "commit"
precompiled rules, "call/return" actions (see next section) and stack
virtualization via "vimage" should serve all practical purposes.
Possible implementation:
General view is clear from features description. One also can think about
netgraph(4) node for this (again) and/or something like shared memory
pages between kernel and userland, to not allocate memory in kernel
twice for big rulesets.
2. Independent (minor) changes, which can be possible without ABI breakage.
2.1. call/return rule actions.
Description of feature:
A "skipto" rule is known as a useful tool to optimize packet flow
through ruleset, also able to assign several actions to a dynamic rule
(because dynamic rule on match simply jumps to action part of parent
rule). But it can only jump forward, not backwards, for the same reason
as bpf(4) assember instruction: to prevent infinite loops in packet flow
which will cause machine to hang network operations. This can be
addressed by introducing a pair of instructions, call and return, which
remembers position to return in the stack of some kind. Because return is
always done to the next rule after calling one (by number, as with
divert/skipto), it is guaranteed that infinite loops can't occur, even
in case of calling one rule many times by simply proceeding to next rule
after stcak overflow.
Thus call/return pair allows to organize some kind of subroutines, with
the trick that issuing actual number lets to jump to the middle of
subroutine, as in assembly language:
ipfw add 100 call 600 ip from any to any in recv $internal
ipfw add 100 call 700 ip from any to any in recv $external
...
ipfw add 500 allow ip from any to any
ipfw add 600 deny ip from any to any not antispoof
ipfw add 700 deny tcp from any to any 135,445
ipfw add 900 return // for both those calls
It should be noted again that calls are made by rule numbers, so in the
following example the first "call 700" will pass control to rule 301,
not second rule 300.
ipfw add 300 call 700 ...
ipfw add 300 call 800 ...
ipfw add 301 count ip ...
Allowing to use "tablearg" in "call" would be very useful. Parser should
allow both version of "return", with some conditions (ususal rule body)
and without them (like "check-state").
Possible implementation:
Relatively easy. Allocate a mbuf tag for a stack of uint16_t rule
numbers and a stack top pointer on first "call" for mbuf. The only thing
to care are divert etc. calls, and distinguishing input and output
passes (firewall can be called several times for each), thus stack
underflow and overflow should be carefully analyzed. May be two tag
types, one for input and one for output.
It is difficult, however, to get this performing well, because of
linked-list nature of ruleset and inability to cache pointer to skip
destination, as done with "skipto" currently, because there can be
several locations (even tablearg). Possible solutions may be to keep
a cache to, say, 256 points in the list (rulenum / 256) to reduce
looking after this point (effectively equivalent to hash on rulenum).
Or to have compiled rulesets where offset to jump is easily calculated
(see previous section).
2.2. Tables and tableargs.
Tables are very powerful way to both increase processing speed and
conveniently reduce rule maintaing cost for user, especially with
tableargs. Tables, however, are currently limited to IPv4
addresses/masks as keys and uint32_t's as values. Table keys should be
extended to another data types: IPv6 addresses, interface name strings:
ipfw add allow ip from any to table6(1) in recv stringtable(2)
or
ipfw call tablearg ip from any to any via stringtable(3)
The latter will be very handy for routers with e.g. 2000 VLAN or ng*
interfaces, with separate client and rules for each.
Tableargs should also be expanded to 16 bytes, to be able to store IPv6
address ot uint64_t for checking e.g. in rule counters. It is
questionable whether tableargs could also be short (< 16 bytes) strings
like interfaces' names.
Due to implementation difficulties of distinguishing whether action
parameter is a valid value or a tablearg (you usualyy have only one
invalid value out ouf 65536 which is get assigned as tablearg
indicator), I suggest adding operations like "settablearg" which will
set tablearg without actual table used, e.g., from saved arbitrary info
from dynamic rules (see section 1.1) or even packet header. So, values
for "computed goto" or something like registers still be used by
tablearg (just generalizing definition of table), or, at least this
should be so in opcode level - user could be present with some other
keyword, but I don't see any point in hiding this details.
Number of tables of all types should be configurable via sysctl or at
least loader tunable rather than current hradcoded number (128).
2.3. Time limit counter.
An opcode for a token bucket and/or leaky bucket should be introduced.
This will have a one counter changed with timer and other changed by
actual packets. We currently have O_LOG opcode looking similar to this,
but O_LOG has nothing to deal with timer. Proposed opcode must be useful
at least for limiting a number of connections per second, but any other
possible use is appreciated, from simplest shaping without dummynet to
more exotic like counter "price" coefficinets allowing to build an
in-kernel billing solely on ipfw counters.
It is questionable where values of counters should be stored, due to
locking optimising - directly, as with O_LOG, or separately addressable
space like tables.
2.4. Action rules and parameters.
Change ACTION_PTR handling in kernel and preparing in compiler to allow
actions and their parameters to be placed in any order (except for
opcodes where order is required, e.g. prob). This would easily allow
placing several opcodes of the same type to action part, e.g.:
ipfw add count tag 1 tag 2 tag 3 ip from any to any
and using actions and their parameters interchangeably, like having
a rule without actual action opcode (only parameter instead), e.g. use
"tag" or "altq" as action too (equals to "count").
2.5. Just to mention: modip, counter limits, fragments.
These patches are already currently discussed in ipfw@, but included
here just to not forget. These are "modip" action, allowing to modify IP
header (DSCP, ToS, TTL) and corresponding match rule options, and a rule
option to match when rule counters are less then specified number
packets or bytes (possibly from dynamic rule's counters), may be
a tablearg. This is also related with mentioned in section 1.2 ability
to control rule counters.
Adding a few keywords for O_FRAG more fragment matching (not only
non-first fragment), e.g. for sending to specialized netgraph(4)
reassembling module, is also desirable.
That's all for today. Any comments, additions, corrections are welcome!
--
WBR, Vadim Goncharov. ICQ#166852181 mailto:vadim_nuclight at mail.ru
[Moderator of RU.ANTI-ECOLOGY][FreeBSD][http://antigreen.org][LJ:/nuclight]
More information about the freebsd-ipfw
mailing list