Page MenuHomeFreeBSD

ipfw: flesh out IP_FW3 OPVER_0 compat layer as ipfw_compat
Needs ReviewPublic

Authored by vova_fbsd.ru on Apr 27 2026, 9:25 AM.
Tags
None
Referenced Files
Unknown Object (File)
Sat, Jun 13, 4:22 AM
Unknown Object (File)
Sat, Jun 13, 1:37 AM
Unknown Object (File)
Sat, Jun 13, 1:34 AM
Unknown Object (File)
Sat, Jun 13, 12:39 AM
Unknown Object (File)
Mon, Jun 8, 7:47 AM
Unknown Object (File)
Thu, Jun 4, 7:31 PM
Unknown Object (File)
Thu, Jun 4, 7:18 PM
Unknown Object (File)
Tue, May 26, 11:49 PM

Details

Summary

ipfw: flesh out IP_FW3 OPVER_0 compat layer as ipfw_compat

Commit 4a77657cbc01 ("ipfw: migrate ipfw to 32-bit size rule numbers")
broke the IP_FW3 sockopt ABI: the in-kernel rule representation now
uses 32-bit rule numbers and 32-bit named-object indexes, while old
ipfw(8) binaries from 14.x and earlier speak OPVER_0 with 16-bit
fields. The companion ip_fw_compat.c module added in that commit was
left as an "example": only XADD/XDEL were wired up, the remaining v0
handlers returned EOPNOTSUPP, and the module was not registered in
the build.

Implement enough of the compat layer to make the common 14.x ipfw(8)
operations work against a 15.x kernel:

  • clear_rules_v0, move_rules_v0, manage_sets_v0: convert the v0 ipfw_range_tlv into the v1 layout via the existing check_range_tlv_v0() and dispatch to the shared kernel helpers.
  • dump_config_v0: mirror dump_config() but emit ipfw_obj_ntlv_v0 (16-bit idx) for named objects and a v0-encoded cmd stream for each rule. Add convert_v1_to_v0() as the inverse of the existing convert_v0_to_v1(): O_{CHECK,KEEP,PROBE}_STATE, O_EXTERNAL_{ACTION,INSTANCE}, O_LIMIT, O_IP_*_LOOKUP, O_MAC_*_LOOKUP, O_SKIPTO and O_CALLRETURN are repacked back into their pre-15.x shapes; rules with rulenum > IPFW_DEFAULT_RULE are hidden from v0 callers since they cannot be represented. Dynamic state export is not yet handled and 'ipfw -d show' against an old userland will see an empty state list.
  • support table management XADD, XDEL, XFIND and XLIST(entries): 14.x ipfw(8) sends opheader.version = 1 for these calls (it has always been the "current" wire version for entry operations), so the request bypasses the OPVER_0 registration entirely and lands in the unmodified v1 handler.

    The v1 handlers set ti.uidx = tent->idx and look up the table by searching the tlvs buffer for ntlv.idx == uidx. With a v0 client tent->idx is the legacy marker 1, while ntlv.idx after v1 reinterpretation contains the type/set bytes spilled into the high half (e.g. 0x04000001 for an addr table). The search misses and add_table_entry() returns ESRCH ("table not found").

    Hook the conversion in by replacing the v1 registrations of manage_table_ent_v1, dump_table_v1 and find_table_entry with compat wrappers at module load (and reinstating the originals at unload). The wrappers detect a v0 client by oh->spare != 0 (14.x always writes oh->idx = 1, which becomes oh->spare under v1 layout, while 15.x clients leave both fields zero) and rewrite ipfw_obj_header, ipfw_obj_ntlv and each ipfw_obj_tentry in place to canonical v1 layout: idx/spare cleared, set/type pulled from their v0 byte offsets. The tentry rewrite is bounded by the available buffer since it runs before the underlying handler validates ctlv->count. The same wrappers are also registered under OPVER_0 to cover builds that send version=0.

ipfw_table_value itself reordered fields between v0 and v1; that
only matters for ipfw table N add KEY VALUE with a non-zero VALUE
and is left as future work.

To support the above, expose move_range(), clear_range(),
ipfw_swap_sets(), ipfw_enable_sets(), export_rule1(),
export_cntr1_base() and mark_rule_objects() from ip_fw_sockopt.c, and
move struct dump_args to ip_fw_private.h, renaming it to
struct rule_dump_args to avoid clashing with the unrelated, private
struct dump_args in ip_fw_table.c.

Finally, add sys/modules/ipfw_compat/Makefile and register it in
sys/modules/Makefile so the module is actually built. It depends on
ipfw and is loaded as ipfw_compat.ko.

still I have some open questions how to move with this compatibility layer:

  1. Should it be implement as ipfw_compat (as was drafted in original commit) or better to follow usual route under #ifdef COMPAT_FREEBSD14
Test Plan
  1. Adopt tests in tests/sys/netpfil/ipfw/ to be able to run with historical ipfw (pre v15)
  2. Adopt tests/atf_python/sys/netpfil/ipfw/ to have a section to execute v0 (old) APIs

Adoption of tests above is still in my todo list.

Diff Detail

Repository
rG FreeBSD src repository
Lint
Lint Passed
Unit
No Test Coverage
Build Status
Buildable 72586
Build 69469: arc lint + arc unit

Event Timeline

please upload diffs with full context, either git arc or if manually generating diffs use -U 9999999

vova_fbsd.ru edited the summary of this revision. (Show Details)
vova_fbsd.ru edited the test plan for this revision. (Show Details)

Use arc to submit patch

We had a number talks about the way we should provide backward compatibility for ipfw. These options include (but not limited to):

  • implement netlink control interface instead of KBI (see pfctl)
  • backport new KBI to older branches (see D54763)
  • keep compat KBI using compat module (as per this differential)

While compat module is somewhat "traditional" way, it creates a burden of keeping it up-to-date as CURRENT streams forward. In case of D46183 that burden was big enough to keep Yandex pushing new ipfw code for 5+ years into CURRENT.

Making changes backward-compatible is crucial indeed, so D54763 was born as a new approach to provide backward compatibility yet guard CURRENT code from handling backward compatibility quirks.

My suggestion is

  1. to implement dump_soptcodes_v0() method and make sure IP_FW_DUMP_SOPTCODES is supported for every KBI version in future.
  2. Provide new binaries into older code branches either within distribution or with an additional port

IP_FW_DUMP_SOPTCODES (alongwith getosreldate()) available for all KBI versions should allow userland process to detect a newer ipfw implementation in a clean fashion and switch to a compatible binary without noise in kernel logs.

+1 for this. D54763 wasn't sufficient for my jails use case as getosreldate() isn't reliable.
We should maintain compatibility for this kind of thing _at least_ for 15.x branch and delete it in 16.