From 77c63c9c2ae211911532b6a2923dcf79f9279533 Mon Sep 17 00:00:00 2001 From: Luke Gorrie Date: Mon, 14 Sep 2015 06:15:22 +0200 Subject: [PATCH] Squashed 'lib/pflua/' content from commit cc651ae git-subtree-dir: lib/pflua git-subtree-split: cc651aebc9d95e6f10cae9d0e09c13db8b881ebb --- .gitignore | 1 + .gitmodules | 4 + .travis.yml | 9 + AUTHORS | 3 + COPYING | 201 +++ LICENSE | 13 + Makefile | 34 + README.md | 147 ++ common.mk | 3 + deps/luajit | 1 + doc/Makefile | 196 +++ doc/README.md | 24 + doc/decnet-host-10.15.md | 167 +++ doc/decnet-src-10.15.md | 108 ++ doc/dst-host-192.68.1.1-and-greater-100.md | 72 + doc/dst-portrange-80-90.md | 136 ++ doc/ether-broadcast.md | 46 + doc/ether-multicast.md | 40 + doc/ether-proto-1500.md | 43 + doc/ether-proto-1501.md | 40 + doc/ether-proto-255.md | 48 + doc/ether-proto-decnet.md | 40 + doc/extensions.md | 52 + doc/fail-fail.md | 70 + doc/host-127.0.0.1.md | 82 ++ doc/host-ipv6-localhost.md | 94 ++ doc/icmp-or-tcp-or-udp.md | 97 ++ doc/icmp6-or-ip.md | 68 + doc/icmp6.md | 58 + doc/ip-multicast.md | 46 + doc/ip-proto-47.md | 46 + doc/ip-proto-ah.md | 46 + doc/ip-proto-sctp.md | 46 + doc/ip6-multicast.md | 46 + doc/ip6-proto-47.md | 58 + doc/ip6-proto-ah.md | 58 + doc/iso-proto-47.md | 54 + doc/iso-proto-clnp.md | 54 + doc/l1.md | 74 + doc/net-127.0.0.0-8.md | 91 ++ doc/net-ipv6-0-mask-16.md | 54 + doc/net-ipv6-ee.cc.9954.0-mask-111.md | 99 ++ doc/packet-access-igmp.md | 68 + doc/packet-access-igrp.md | 68 + doc/packet-access-pim.md | 68 + doc/packet-access-sctp.md | 68 + doc/packet-access-vrrp.md | 68 + doc/pflang.md | 133 ++ doc/pfmatch.md | 223 +++ doc/portrange-0-6000.md | 155 +++ doc/proto-47.md | 75 + doc/proto-sctp.md | 67 + doc/sctp.md | 72 + doc/src-host-192.68.1.1-and-less-100.md | 82 ++ doc/src-net-ffff.ffff.eeee.eeee.0.0.0.0-72.md | 59 + doc/src-net-ffff.ffff.eeee.eeee.1.0.0.0-82.md | 61 + doc/src-port-80.md | 128 ++ doc/tcp-address.md | 35 + doc/tcp-port-80.md | 113 ++ doc/technical/bpf-asm-explained.md | 258 ++++ env | 21 + src/Makefile | 20 + src/pf.lua | 92 ++ src/pf/anf.lua | 296 ++++ src/pf/backend.lua | 398 ++++++ src/pf/bpf.lua | 462 ++++++ src/pf/constants.lua | 386 +++++ src/pf/expand.lua | 1239 +++++++++++++++++ src/pf/libpcap.lua | 87 ++ src/pf/match.lua | 366 +++++ src/pf/optimize.lua | 857 ++++++++++++ src/pf/parse.lua | 1194 ++++++++++++++++ src/pf/quickcheck.lua | 122 ++ src/pf/savefile.lua | 76 + src/pf/ssa.lua | 327 +++++ src/pf/types.lua | 61 + src/pf/utils.lua | 207 +++ tcp_port_80_asm.md | 118 ++ tests/Makefile | 51 + tests/data/COPYRIGHT | 14 + tests/data/arp.pcap | Bin 0 -> 158 bytes tests/data/empty.pcap | Bin 0 -> 24 bytes tests/data/empty.pcap.test | 2 + tests/data/tcp-ack-66-bytes.pcap | Bin 0 -> 106 bytes tests/data/telnet-cooked.pcap | Bin 0 -> 9244 bytes tests/data/telnet-cooked.pcap.test | 3 + tests/data/tftp_wrq.pcap | Bin 0 -> 30839 bytes tests/data/tftp_wrq.pcap.test | 2 + tests/data/v4.pcap | Bin 0 -> 25803 bytes tests/data/v4.pcap.test | 4 + tests/data/v6.pcap | Bin 0 -> 28251 bytes tests/ir-reg/opt-bug120-rangecheck-opt.ir | 6 + tests/ir-reg/opt-bug120-rangecheck-unopt.ir | 17 + tests/ir-reg/opt-bug120-rangecheck.sh | 7 + tests/ir-reg/opt-bug126-invalidopt.sh | 6 + tests/ir-reg/opt-bug126-opt.ir | 90 ++ tests/ir-reg/opt-bug126-unopt.ir | 238 ++++ tests/pflang-reg/pl-bug129-flipportrange.sh | 3 + tests/pflang-reg/pl-bug130-allreject | 3 + tests/pflang-reg/pl-bug131-notlen-igrp | 3 + tests/pflang-reg/pl-bug131-notlen-rarp | 3 + tests/pflang-reg/pl-bug131-notlen-tcpport | 3 + tests/pflang-reg/pl-bug132-icmp6_or_ip | 3 + tests/pflang-reg/pl-bug132-icmp6_or_portrange | 3 + tests/pflang-reg/pl-bug132-not_icmp6 | 3 + tests/pflang-reg/pl-bug139-multioctet.sh | 3 + tests/pflang-reg/pl-bug171-arpindexing_or_tcp | 3 + tests/pflang-reg/pl-bug182-icmp_or_arp | 3 + tests/pflang-reg/pl-bug205-greater1_or_less1 | 3 + tests/pfquickcheck/README.md | 30 + tests/pfquickcheck/pfcompile.lua | 21 + tests/pfquickcheck/pflang.lua | 356 +++++ tests/pfquickcheck/pflang_math.lua | 70 + tests/pfquickcheck/pflua_ir.lua | 56 + tests/properties/fail.lua | 12 + tests/properties/opt_eq_unopt.lua | 79 ++ .../properties/pflua_math_eq_libpcap_math.lua | 16 + tests/properties/pflua_pipelines_match.lua | 45 + tests/properties/pipecmp_proto_or_proto.lua | 48 + tests/properties/repeatable_randomization.lua | 20 + tests/properties/trivial.lua | 11 + tests/test-filters | 1 + tests/test-matches | 90 ++ tools/Makefile | 20 + tools/dump-markdown | 24 + tools/helpers/pflua_asm.lua | 69 + tools/pflua-allocchecker | 98 ++ tools/pflua-asm | 53 + tools/pflua-compile | 62 + tools/pflua-expand | 36 + tools/pflua-filter | 41 + tools/pflua-match | 84 ++ tools/pflua-optimize | 85 ++ tools/pflua-pipelines-match | 174 +++ tools/pflua-quickcheck | 13 + 135 files changed, 12889 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .travis.yml create mode 100644 AUTHORS create mode 100644 COPYING create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 common.mk create mode 160000 deps/luajit create mode 100644 doc/Makefile create mode 100644 doc/README.md create mode 100644 doc/decnet-host-10.15.md create mode 100644 doc/decnet-src-10.15.md create mode 100644 doc/dst-host-192.68.1.1-and-greater-100.md create mode 100644 doc/dst-portrange-80-90.md create mode 100644 doc/ether-broadcast.md create mode 100644 doc/ether-multicast.md create mode 100644 doc/ether-proto-1500.md create mode 100644 doc/ether-proto-1501.md create mode 100644 doc/ether-proto-255.md create mode 100644 doc/ether-proto-decnet.md create mode 100644 doc/extensions.md create mode 100644 doc/fail-fail.md create mode 100644 doc/host-127.0.0.1.md create mode 100644 doc/host-ipv6-localhost.md create mode 100644 doc/icmp-or-tcp-or-udp.md create mode 100644 doc/icmp6-or-ip.md create mode 100644 doc/icmp6.md create mode 100644 doc/ip-multicast.md create mode 100644 doc/ip-proto-47.md create mode 100644 doc/ip-proto-ah.md create mode 100644 doc/ip-proto-sctp.md create mode 100644 doc/ip6-multicast.md create mode 100644 doc/ip6-proto-47.md create mode 100644 doc/ip6-proto-ah.md create mode 100644 doc/iso-proto-47.md create mode 100644 doc/iso-proto-clnp.md create mode 100644 doc/l1.md create mode 100644 doc/net-127.0.0.0-8.md create mode 100644 doc/net-ipv6-0-mask-16.md create mode 100644 doc/net-ipv6-ee.cc.9954.0-mask-111.md create mode 100644 doc/packet-access-igmp.md create mode 100644 doc/packet-access-igrp.md create mode 100644 doc/packet-access-pim.md create mode 100644 doc/packet-access-sctp.md create mode 100644 doc/packet-access-vrrp.md create mode 100644 doc/pflang.md create mode 100644 doc/pfmatch.md create mode 100644 doc/portrange-0-6000.md create mode 100644 doc/proto-47.md create mode 100644 doc/proto-sctp.md create mode 100644 doc/sctp.md create mode 100644 doc/src-host-192.68.1.1-and-less-100.md create mode 100644 doc/src-net-ffff.ffff.eeee.eeee.0.0.0.0-72.md create mode 100644 doc/src-net-ffff.ffff.eeee.eeee.1.0.0.0-82.md create mode 100644 doc/src-port-80.md create mode 100644 doc/tcp-address.md create mode 100644 doc/tcp-port-80.md create mode 100644 doc/technical/bpf-asm-explained.md create mode 100755 env create mode 100644 src/Makefile create mode 100644 src/pf.lua create mode 100644 src/pf/anf.lua create mode 100644 src/pf/backend.lua create mode 100644 src/pf/bpf.lua create mode 100644 src/pf/constants.lua create mode 100644 src/pf/expand.lua create mode 100644 src/pf/libpcap.lua create mode 100644 src/pf/match.lua create mode 100644 src/pf/optimize.lua create mode 100644 src/pf/parse.lua create mode 100644 src/pf/quickcheck.lua create mode 100644 src/pf/savefile.lua create mode 100644 src/pf/ssa.lua create mode 100644 src/pf/types.lua create mode 100644 src/pf/utils.lua create mode 100644 tcp_port_80_asm.md create mode 100644 tests/Makefile create mode 100644 tests/data/COPYRIGHT create mode 100644 tests/data/arp.pcap create mode 100644 tests/data/empty.pcap create mode 100644 tests/data/empty.pcap.test create mode 100644 tests/data/tcp-ack-66-bytes.pcap create mode 100644 tests/data/telnet-cooked.pcap create mode 100644 tests/data/telnet-cooked.pcap.test create mode 100644 tests/data/tftp_wrq.pcap create mode 100644 tests/data/tftp_wrq.pcap.test create mode 100644 tests/data/v4.pcap create mode 100644 tests/data/v4.pcap.test create mode 100644 tests/data/v6.pcap create mode 100644 tests/ir-reg/opt-bug120-rangecheck-opt.ir create mode 100644 tests/ir-reg/opt-bug120-rangecheck-unopt.ir create mode 100755 tests/ir-reg/opt-bug120-rangecheck.sh create mode 100755 tests/ir-reg/opt-bug126-invalidopt.sh create mode 100644 tests/ir-reg/opt-bug126-opt.ir create mode 100644 tests/ir-reg/opt-bug126-unopt.ir create mode 100755 tests/pflang-reg/pl-bug129-flipportrange.sh create mode 100755 tests/pflang-reg/pl-bug130-allreject create mode 100755 tests/pflang-reg/pl-bug131-notlen-igrp create mode 100755 tests/pflang-reg/pl-bug131-notlen-rarp create mode 100755 tests/pflang-reg/pl-bug131-notlen-tcpport create mode 100755 tests/pflang-reg/pl-bug132-icmp6_or_ip create mode 100755 tests/pflang-reg/pl-bug132-icmp6_or_portrange create mode 100755 tests/pflang-reg/pl-bug132-not_icmp6 create mode 100755 tests/pflang-reg/pl-bug139-multioctet.sh create mode 100755 tests/pflang-reg/pl-bug171-arpindexing_or_tcp create mode 100755 tests/pflang-reg/pl-bug182-icmp_or_arp create mode 100755 tests/pflang-reg/pl-bug205-greater1_or_less1 create mode 100644 tests/pfquickcheck/README.md create mode 100644 tests/pfquickcheck/pfcompile.lua create mode 100644 tests/pfquickcheck/pflang.lua create mode 100644 tests/pfquickcheck/pflang_math.lua create mode 100644 tests/pfquickcheck/pflua_ir.lua create mode 100644 tests/properties/fail.lua create mode 100644 tests/properties/opt_eq_unopt.lua create mode 100644 tests/properties/pflua_math_eq_libpcap_math.lua create mode 100644 tests/properties/pflua_pipelines_match.lua create mode 100644 tests/properties/pipecmp_proto_or_proto.lua create mode 100644 tests/properties/repeatable_randomization.lua create mode 100644 tests/properties/trivial.lua create mode 100644 tests/test-filters create mode 100755 tests/test-matches create mode 100644 tools/Makefile create mode 100755 tools/dump-markdown create mode 100644 tools/helpers/pflua_asm.lua create mode 100755 tools/pflua-allocchecker create mode 100755 tools/pflua-asm create mode 100755 tools/pflua-compile create mode 100755 tools/pflua-expand create mode 100755 tools/pflua-filter create mode 100755 tools/pflua-match create mode 100755 tools/pflua-optimize create mode 100755 tools/pflua-pipelines-match create mode 100755 tools/pflua-quickcheck diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..905a2f1c8e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/tests/data/wingolog.pcap diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..2987b9cf84 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "deps/luajit"] + path = deps/luajit + url = http://github.com/SnabbCo/luajit.git + ignore = dirty diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..b41e69d8da --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +# Lua isn't a supported language; using fake-erlang, as per +# http://thejacklawson.com/2012/09/lua-testing-with-busted-and-travis-ci/ +language: erlang + +before_install: git submodule update --init + +install: sudo apt-get install libpcap-dev + +script: make && make check diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000000..825badd144 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,3 @@ +Andy Wingo +Javier Muñoz +Luke Gorrie diff --git a/COPYING b/COPYING new file mode 100644 index 0000000000..d102b34d3b --- /dev/null +++ b/COPYING @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..950be6b9ff --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright (C) 2014 Igalia, S.L. and others + +pflua (the "Software") is licensed under the Apache License, Version 2.0 +(the "License"); you may not use the Software except in compliance with +the License. You may obtain a copy of the License in the COPYING file +located in this same directory, or at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, the Software +is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..f84bc4f9fd --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +TOP_SRCDIR:=. +include common.mk +LUAJIT_O := $(ABS_TOP_SRCDIR)/deps/luajit/src/libluajit.a + +LUAJIT_CFLAGS := -DLUAJIT_USE_PERFTOOLS -DLUAJIT_USE_GDBJIT + +all: $(LUAJIT_O) + $(MAKE) -C doc + +$(LUAJIT_O): check_luajit deps/luajit/Makefile + echo 'Building LuaJIT\n' + (cd deps/luajit && \ + $(MAKE) PREFIX=`pwd`/usr/local \ + CFLAGS="$(LUAJIT_CFLAGS)" && \ + $(MAKE) DESTDIR=`pwd` install) + (cd deps/luajit/usr/local/bin; ln -fs luajit-2.0.3 luajit) + +check_luajit: + @if [ ! -f deps/luajit/Makefile ]; then \ + echo "Can't find deps/luajit/. You might need to: git submodule update --init"; exit 1; \ + fi + +check: + $(MAKE) -C src check + $(MAKE) -C tools check + $(MAKE) -C doc + $(MAKE) -C tests check + +clean: + $(MAKE) -C deps/luajit clean + $(MAKE) -C src clean + $(MAKE) -C tools clean + +.SERIAL: all diff --git a/README.md b/README.md new file mode 100644 index 0000000000..0b1a83c949 --- /dev/null +++ b/README.md @@ -0,0 +1,147 @@ +# pflua + +`pflua` is a high-performance network packet filtering library written +in Lua. It supports filters written in +[pflang](https://github.com/Igalia/pflua/blob/master/doc/pflang.md), the +filter language of the popular +[tcpdump](https://www.wireshark.org/docs/man-pages/pcap-filter.html#DESCRIPTION) +tool. It's really fast: to our knowledge, it's the fastest pflang +implementation out there, by a wide margin. Read on for more details. + +## Getting started + +```shell +$ git clone --recursive https://github.com/Igalia/pflua.git +$ cd pflua; make # Builds embedded LuaJIT +$ make check # Run builtin basic tests +``` + +## Using pflua + +Pflua is a library; you need an application to drive it. + +The most simple way to use pflua is filtering packets from a file +captured by `tcpdump`. For example: + +``` +$ cd tools +$ ../deps/luajit/usr/local/bin/luajit pflua-filter \ + ../tests/data/v4.pcap /tmp/foo.pcap "ip" +Filtered 43/43 packets from ../tests/data/v4.pcap to /tmp/foo.pcap. +``` + +See the source of +[pflua-filter](https://github.com/Igalia/pflua/blob/master/tools/pflua-filter) +for more information. + +Pflua was made to be integrated into the [Snabb +Switch](https://github.com/SnabbCo/snabbswitch/wiki) user-space +networking toolkit, also written in Lua. A common deployment +environment for Snabb is within the host virtual machine of a +virtualized server, with Snabb having CPU affinity and complete control +over a high-performance 10Gbit NIC, which it then routes to guest VMs. +The administrator of such an environment might want to apply filters on +the kinds of traffic passing into and out of the guests. To this end, +we plan on integrating pflua into Snabb so as to provide a pleasant, +expressive, high-performance filtering facility. + +Given its high performance, it is also reasonable to deploy pflua on +gateway routers and load-balancers, within virtualized networking +appliances. + +## Implementation + +Pflua can compile pflang filters in two ways. + +The default compilation pipeline is pure Lua. First, a [custom +parser](https://github.com/Igalia/pflua/blob/master/src/pf/parse.lua) +produces a high-level AST of a pflang filter expression. This AST is +[_lowered_](https://github.com/Igalia/pflua/blob/master/src/pf/expand.lua) +to a primitive AST, with a limited set of operators and ways in which +they can be combined. This representation is then exhaustively +[optimized](https://github.com/Igalia/pflua/blob/master/src/pf/optimize.lua), +folding constants and tests, inferring ranges of expressions and packet +offset values, hoisting assertions that post-dominate success +continuations, etc. We then lower to [A-normal +form](https://github.com/Igalia/pflua/blob/master/src/pf/anf.lua) to +give names to all intermediate values, perform common subexpression +elimination, then inline named values that are only used once. We lower +further to [Static single +assignment](https://github.com/Igalia/pflua/blob/master/src/pf/ssa.lua) +to give names to all blocks, which allows us to perform control-flow +optimizations. Finally, we +[residualize](https://github.com/Igalia/pflua/blob/master/src/pf/backend.lua) +Lua source code, using the control flow analysis from the SSA phase. + +The resulting Lua function is a predicate of two parameters: the packet +as a `uint8_t*` pointer, and its length. If the predicate is called +enough times, LuaJIT will kick in and optimize traces that run through +the function. Pleasantly, this results in machine code whose structure +reflects the actual packets that the filter sees, as branches that are +never taken are not residualized at all. + +The other compilation pipeline starts with bytecode for the [Berkeley +packet filter +VM](https://www.freebsd.org/cgi/man.cgi?query=bpf#FILTER_MACHINE). +Pflua can load up the `libpcap` library and use it to compile a pflang +expression to BPF. In any case, whether you start from raw BPF or from +a pflang expression, the BPF is compiled directly to Lua source code, +which LuaJIT can gnaw on as it pleases. + +We like the independence and optimization capabilities afforded by the +native pflang pipeline. However, though pflua does a good job in +implementing pflang, it is inevitable that there may be bugs or +differences of implementation relative to what `libpcap` does. For that +reason, the `libpcap`-to-bytecode pipeline can be a useful alternative +in some cases. + +See the [doc](https://github.com/Igalia/pflua/blob/master/doc) +subdirectory for some examples of the Lua code generated for some simple +pflang filters using these two pipelines. + +## Performance + +To our knowledge, pflua is the fastest implementation of pflang out +there. See https://github.com/Igalia/pflua-bench for our benchmarking +experiments and results. + +Pflua can beat other implementations because: + +* LuaJIT trace compilation results in machine code that reflects the + actual traffic that your application sees + +* Pflua can hoist and eliminate bounds checks, whereas [BPF is obligated to + check that every packet access is valid](https://github.com/Igalia/pflua/blob/master/doc/pflang.md#packet-access) + +* Pflua can work on data in network byte order, whereas BPF must + convert to host byte order + +* Pflua takes advantage of LuaJIT's register allocator and excellent + optimizing compiler, whereas e.g. the Linux kernel JIT has a limited + optimizer + +## API documentation + +None yet. See +[pf.lua](https://github.com/Igalia/pflua/blob/master/src/pf.lua) for the +high-level `compile_filter` interface. + +## Bugs + +Check our [issue tracker](https://github.com/Igalia/pflua/issues) +for known bugs, and please file a bug if you find one. Cheers :) + +## Authors + +Pflua was written by Katerina Barone-Adesi, Andy Wingo, Diego Pino, and +Javier Muñoz at [Igalia, S.L.](https://www.igalia.com/), as well as +Peter Melnichenko. Development of pflua was supported by Luke Gorrie at +[Snabb Gmbh](http://snabb.co/), purveyors of fine networking solutions. +Thanks, Snabb! + +Feedback is very welcome! If you are interested in pflua in a Snabb +context, probably the best thing is to post a message to the +[snabb-devel](https://groups.google.com/forum/#!forum/snabb-devel) +group. Or, if you like, you can contact Andy directly at +`wingo@igalia.com`. If you have a problem that pflua can help solve, +let us know! diff --git a/common.mk b/common.mk new file mode 100644 index 0000000000..58ea9e9741 --- /dev/null +++ b/common.mk @@ -0,0 +1,3 @@ +ABS_TOP_SRCDIR:=$(shell cd $(TOP_SRCDIR) && pwd) +LUAJIT=$(ABS_TOP_SRCDIR)/deps/luajit/usr/local/bin/luajit +PATH := $(ABS_TOP_SRCDIR)/deps/luajit/usr/local/bin:$(PATH) diff --git a/deps/luajit b/deps/luajit new file mode 160000 index 0000000000..7f013005f6 --- /dev/null +++ b/deps/luajit @@ -0,0 +1 @@ +Subproject commit 7f013005f61b82300d4ec591fd4cec59a74d62ff diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000000..a38e6b3dc8 --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,196 @@ +TOP_SRCDIR:=.. +include $(TOP_SRCDIR)/common.mk + +EXAMPLES = \ + tcp-port-80.md \ + icmp-or-tcp-or-udp.md \ + portrange-0-6000.md \ + host-127.0.0.1.md \ + net-127.0.0.0-8.md \ + host-ipv6-localhost.md \ + net-ipv6-0-mask-16.md \ + net-ipv6-ee.cc.9954.0-mask-111.md \ + src-net-ffff.ffff.eeee.eeee.1.0.0.0-82.md \ + src-net-ffff.ffff.eeee.eeee.0.0.0.0-72.md \ + src-port-80.md \ + dst-portrange-80-90.md \ + ether-multicast.md \ + ether-broadcast.md \ + ether-proto-decnet.md \ + ether-proto-1500.md \ + ether-proto-1501.md \ + ether-proto-255.md \ + dst-portrange-80-90.md \ + src-host-192.68.1.1-and-less-100.md \ + dst-host-192.68.1.1-and-greater-100.md \ + ip-multicast.md \ + ip-proto-ah.md \ + ip-proto-47.md \ + ip6-multicast.md \ + ip6-proto-ah.md \ + ip6-proto-47.md \ + iso-proto-clnp.md \ + iso-proto-47.md \ + ip-proto-sctp.md \ + proto-47.md \ + decnet-src-10.15.md \ + decnet-host-10.15.md \ + l1.md \ + icmp6.md \ + sctp.md \ + packet-access-igmp.md \ + packet-access-pim.md \ + packet-access-igrp.md \ + packet-access-vrrp.md \ + packet-access-sctp.md \ + icmp6-or-ip.md \ + tcp-address.md \ + fail-fail.md + +PFLUA = \ + ../src/pf.lua \ + ../src/pf/bpf.lua \ + ../src/pf/parse.lua \ + ../src/pf/expand.lua \ + ../src/pf/optimize.lua \ + ../src/pf/anf.lua \ + ../src/pf/ssa.lua \ + ../src/pf/backend.lua + +all: $(EXAMPLES) + +maintainer-clean: + rm -f $(EXAMPLES) + +tcp-port-80.md: $(PFLUA) + ../tools/dump-markdown "tcp port 80" > $@.tmp && mv $@.tmp $@ + +icmp-or-tcp-or-udp.md: $(PFLUA) + ../tools/dump-markdown "icmp or tcp or udp" > $@.tmp && mv $@.tmp $@ + +portrange-0-6000.md: $(PFLUA) + ../tools/dump-markdown "portrange 0-6000" > $@.tmp && mv $@.tmp $@ + +host-127.0.0.1.md: $(PFLUA) + ../tools/dump-markdown "host 127.0.0.1" > $@.tmp && mv $@.tmp $@ + +net-127.0.0.0-8.md: $(PFLUA) + ../tools/dump-markdown "net 127.0.0.0/8" > $@.tmp && mv $@.tmp $@ + +host-ipv6-localhost.md: $(PFLUA) + ../tools/dump-markdown "host ::1" > $@.tmp && mv $@.tmp $@ + +net-ipv6-0-mask-16.md: $(PFLUA) + ../tools/dump-markdown "net ::0/16" > $@.tmp && mv $@.tmp $@ + +net-ipv6-ee.cc.9954.0-mask-111.md: $(PFLUA) + ../tools/dump-markdown "net ee:cc::9954:0/111" > $@.tmp && mv $@.tmp $@ + +src-net-ffff.ffff.eeee.eeee.1.0.0.0-82.md: $(PFLUA) + ../tools/dump-markdown "src net ffff:ffff:eeee:eeee:1:0:0:0/82" > $@.tmp && mv $@.tmp $@ + +src-net-ffff.ffff.eeee.eeee.0.0.0.0-72.md: $(PFLUA) + ../tools/dump-markdown "src net ffff:ffff:eeee:eeee:0:0:0:0/72" > $@.tmp && mv $@.tmp $@ + +src-port-80.md: $(PFLUA) + ../tools/dump-markdown "src port 80" > $@.tmp && mv $@.tmp $@ + +dst-portrange-80-90.md: $(PFLUA) + ../tools/dump-markdown "dst portrange 80-90" > $@.tmp && mv $@.tmp $@ + +ether-multicast.md: $(PFLUA) + ../tools/dump-markdown "ether multicast" > $@.tmp && mv $@.tmp $@ + +ether-broadcast.md: $(PFLUA) + ../tools/dump-markdown "ether broadcast" > $@.tmp && mv $@.tmp $@ + +ether-proto-decnet.md: $(PFLUA) + ../tools/dump-markdown "ether proto \decnet" > $@.tmp && mv $@.tmp $@ + +# The frame should be treated as an Ethernet frame. +ether-proto-1501.md: $(PFLUA) + ../tools/dump-markdown "ether proto 1501" > $@.tmp && mv $@.tmp $@ + +# The frame should be treated as an 802.3 frame. +ether-proto-1500.md: $(PFLUA) + ../tools/dump-markdown "ether proto 1500" > $@.tmp && mv $@.tmp $@ + +# The frame should be treated as an 802.3 frame and check SAP (Service Access +# Point) at byte 14. +ether-proto-255.md: $(PFLUA) + ../tools/dump-markdown "ether proto 255" > $@.tmp && mv $@.tmp $@ + +src-host-192.68.1.1-and-less-100.md: $(PFLUA) + ../tools/dump-markdown "src host 192.68.1.1 and less 100" > $@.tmp && mv $@.tmp $@ + +dst-host-192.68.1.1-and-greater-100.md: $(PFLUA) + ../tools/dump-markdown "dst host 192.68.1.1 and greater 100" > $@.tmp && mv $@.tmp $@ + +ip-multicast.md: $(PFLUA) + ../tools/dump-markdown "ip multicast" > $@.tmp && mv $@.tmp $@ + +ip-proto-ah.md: $(PFLUA) + ../tools/dump-markdown "ip proto \ah" > $@.tmp && mv $@.tmp $@ + +ip-proto-47.md: $(PFLUA) + ../tools/dump-markdown "ip proto 47" > $@.tmp && mv $@.tmp $@ + +ip6-multicast.md: $(PFLUA) + ../tools/dump-markdown "ip6 multicast" > $@.tmp && mv $@.tmp $@ + +ip6-proto-ah.md: $(PFLUA) + ../tools/dump-markdown "ip6 proto \ah" > $@.tmp && mv $@.tmp $@ + +ip6-proto-47.md: $(PFLUA) + ../tools/dump-markdown "ip6 proto 47" > $@.tmp && mv $@.tmp $@ + +iso-proto-clnp.md: $(PFLUA) + ../tools/dump-markdown "iso proto \clnp" > $@.tmp && mv $@.tmp $@ + +iso-proto-47.md: $(PFLUA) + ../tools/dump-markdown "iso proto 47" > $@.tmp && mv $@.tmp $@ + +ip-proto-sctp.md: $(PFLUA) + ../tools/dump-markdown "ip proto \sctp" > $@.tmp && mv $@.tmp $@ + +proto-47.md: $(PFLUA) + ../tools/dump-markdown "proto 47" > $@.tmp && mv $@.tmp $@ + +decnet-src-10.15.md: $(PFLUA) + ../tools/dump-markdown "decnet src 10.15" > $@.tmp && mv $@.tmp $@ + +decnet-host-10.15.md: $(PFLUA) + ../tools/dump-markdown "decnet host 10.15" > $@.tmp && mv $@.tmp $@ + +l1.md: $(PFLUA) + ../tools/dump-markdown "l1" > $@.tmp && mv $@.tmp $@ + +icmp6.md: $(PFLUA) + ../tools/dump-markdown "icmp6" > $@.tmp && mv $@.tmp $@ + +sctp.md: $(PFLUA) + ../tools/dump-markdown "sctp" > $@.tmp && mv $@.tmp $@ + +packet-access-igmp.md: $(PFLUA) + ../tools/dump-markdown "igmp[8] < 8" > $@.tmp && mv $@.tmp $@ + +packet-access-pim.md: $(PFLUA) + ../tools/dump-markdown "pim[8] < 8" > $@.tmp && mv $@.tmp $@ + +packet-access-igrp.md : $(PFLUA) + ../tools/dump-markdown "igrp[8] < 8" > $@.tmp && mv $@.tmp $@ + +packet-access-vrrp.md: $(PFLUA) + ../tools/dump-markdown "vrrp[8] < 8" > $@.tmp && mv $@.tmp $@ + +packet-access-sctp.md: $(PFLUA) + ../tools/dump-markdown "sctp[8] < 8" > $@.tmp && mv $@.tmp $@ + +icmp6-or-ip.md: $(PFLUA) + ../tools/dump-markdown "icmp6 or ip" > $@.tmp && mv $@.tmp $@ + +tcp-address.md: $(PFLUA) + ../tools/dump-markdown "ether[&tcp[0]] = tcp[0]" > $@.tmp && mv $@.tmp $@ + +fail-fail.md: $(PFLUA) + ../tools/dump-markdown "tcp and tcp[100] == 1" > $@.tmp && mv $@.tmp $@ diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000000..6b4d1645af --- /dev/null +++ b/doc/README.md @@ -0,0 +1,24 @@ +# Pflua documentation + +## Pflang resources + +https://github.com/Igalia/pflua/blob/master/doc/pflang.md + +## Example BPF and residualized Lua code for common filters + +### tcp port 80 + +https://github.com/Igalia/pflua/blob/master/doc/tcp-port-80.md + +### icmp or tcp or udp + +https://github.com/Igalia/pflua/blob/master/doc/icmp-or-tcp-or-udp.md + +### portrange 0-6000 + +https://github.com/Igalia/pflua/blob/master/doc/portrange-0-6000.md + +### host 127.0.0.1 + +https://github.com/Igalia/pflua/blob/master/doc/host-127.0.0.1.md + diff --git a/doc/decnet-host-10.15.md b/doc/decnet-host-10.15.md new file mode 100644 index 0000000000..972aee23d2 --- /dev/null +++ b/doc/decnet-host-10.15.md @@ -0,0 +1,167 @@ +# decnet host 10.15 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 24579) goto 2 else goto 43 +002: A = P[16:1] +003: A &= 7 +004: if (A == 2) goto 5 else goto 7 +005: A = P[19:2] +006: if (A == 3880) goto 42 else goto 7 +007: A = P[16:2] +008: A &= 65287 +009: if (A == 33026) goto 10 else goto 12 +010: A = P[20:2] +011: if (A == 3880) goto 42 else goto 12 +012: A = P[16:1] +013: A &= 7 +014: if (A == 6) goto 15 else goto 17 +015: A = P[31:2] +016: if (A == 3880) goto 42 else goto 17 +017: A = P[16:2] +018: A &= 65287 +019: if (A == 33030) goto 20 else goto 22 +020: A = P[32:2] +021: if (A == 3880) goto 42 else goto 22 +022: A = P[16:1] +023: A &= 7 +024: if (A == 2) goto 25 else goto 27 +025: A = P[17:2] +026: if (A == 3880) goto 42 else goto 27 +027: A = P[16:2] +028: A &= 65287 +029: if (A == 33026) goto 30 else goto 32 +030: A = P[18:2] +031: if (A == 3880) goto 42 else goto 32 +032: A = P[16:1] +033: A &= 7 +034: if (A == 6) goto 35 else goto 37 +035: A = P[23:2] +036: if (A == 3880) goto 42 else goto 37 +037: A = P[16:2] +038: A &= 65287 +039: if (A == 33030) goto 40 else goto 43 +040: A = P[24:2] +041: if (A == 3880) goto 42 else goto 43 +042: return 65535 +043: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==24579) then goto L42 end + if 17 > length then return false end + A = P[16] + A = bit.band(A, 7) + if not (A==2) then goto L6 end + if 21 > length then return false end + A = bit.bor(bit.lshift(P[19], 8), P[19+1]) + if (A==3880) then goto L41 end + ::L6:: + if 18 > length then return false end + A = bit.bor(bit.lshift(P[16], 8), P[16+1]) + A = bit.band(A, 65287) + if not (A==33026) then goto L11 end + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if (A==3880) then goto L41 end + ::L11:: + if 17 > length then return false end + A = P[16] + A = bit.band(A, 7) + if not (A==6) then goto L16 end + if 33 > length then return false end + A = bit.bor(bit.lshift(P[31], 8), P[31+1]) + if (A==3880) then goto L41 end + ::L16:: + if 18 > length then return false end + A = bit.bor(bit.lshift(P[16], 8), P[16+1]) + A = bit.band(A, 65287) + if not (A==33030) then goto L21 end + if 34 > length then return false end + A = bit.bor(bit.lshift(P[32], 8), P[32+1]) + if (A==3880) then goto L41 end + ::L21:: + if 17 > length then return false end + A = P[16] + A = bit.band(A, 7) + if not (A==2) then goto L26 end + if 19 > length then return false end + A = bit.bor(bit.lshift(P[17], 8), P[17+1]) + if (A==3880) then goto L41 end + ::L26:: + if 18 > length then return false end + A = bit.bor(bit.lshift(P[16], 8), P[16+1]) + A = bit.band(A, 65287) + if not (A==33026) then goto L31 end + if 20 > length then return false end + A = bit.bor(bit.lshift(P[18], 8), P[18+1]) + if (A==3880) then goto L41 end + ::L31:: + if 17 > length then return false end + A = P[16] + A = bit.band(A, 7) + if not (A==6) then goto L36 end + if 25 > length then return false end + A = bit.bor(bit.lshift(P[23], 8), P[23+1]) + if (A==3880) then goto L41 end + ::L36:: + if 18 > length then return false end + A = bit.bor(bit.lshift(P[16], 8), P[16+1]) + A = bit.band(A, 65287) + if not (A==33030) then goto L42 end + if 26 > length then return false end + A = bit.bor(bit.lshift(P[24], 8), P[24+1]) + if not (A==3880) then goto L42 end + ::L41:: + do return true end + ::L42:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 21 then return false end + local v1 = band(P[16],7) + if v1 == 2 then + if cast("uint16_t*", P+19)[0] == 3850 then return true end + return cast("uint16_t*", P+17)[0] == 3850 + else + if length < 22 then return false end + local v2 = band(cast("uint16_t*", P+16)[0],2047) + if v2 == 641 then + if cast("uint16_t*", P+20)[0] == 3850 then return true end + return cast("uint16_t*", P+18)[0] == 3850 + else + if length < 33 then return false end + if v1 == 6 then + if cast("uint16_t*", P+31)[0] == 3850 then return true end + return cast("uint16_t*", P+23)[0] == 3850 + else + if length < 34 then return false end + if v2 ~= 1665 then return false end + if cast("uint16_t*", P+32)[0] == 3850 then return true end + return cast("uint16_t*", P+24)[0] == 3850 + end + end + end +end + +``` + diff --git a/doc/decnet-src-10.15.md b/doc/decnet-src-10.15.md new file mode 100644 index 0000000000..4bb7b3c5f8 --- /dev/null +++ b/doc/decnet-src-10.15.md @@ -0,0 +1,108 @@ +# decnet src 10.15 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 24579) goto 2 else goto 23 +002: A = P[16:1] +003: A &= 7 +004: if (A == 2) goto 5 else goto 7 +005: A = P[19:2] +006: if (A == 3880) goto 22 else goto 7 +007: A = P[16:2] +008: A &= 65287 +009: if (A == 33026) goto 10 else goto 12 +010: A = P[20:2] +011: if (A == 3880) goto 22 else goto 12 +012: A = P[16:1] +013: A &= 7 +014: if (A == 6) goto 15 else goto 17 +015: A = P[31:2] +016: if (A == 3880) goto 22 else goto 17 +017: A = P[16:2] +018: A &= 65287 +019: if (A == 33030) goto 20 else goto 23 +020: A = P[32:2] +021: if (A == 3880) goto 22 else goto 23 +022: return 65535 +023: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==24579) then goto L22 end + if 17 > length then return false end + A = P[16] + A = bit.band(A, 7) + if not (A==2) then goto L6 end + if 21 > length then return false end + A = bit.bor(bit.lshift(P[19], 8), P[19+1]) + if (A==3880) then goto L21 end + ::L6:: + if 18 > length then return false end + A = bit.bor(bit.lshift(P[16], 8), P[16+1]) + A = bit.band(A, 65287) + if not (A==33026) then goto L11 end + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if (A==3880) then goto L21 end + ::L11:: + if 17 > length then return false end + A = P[16] + A = bit.band(A, 7) + if not (A==6) then goto L16 end + if 33 > length then return false end + A = bit.bor(bit.lshift(P[31], 8), P[31+1]) + if (A==3880) then goto L21 end + ::L16:: + if 18 > length then return false end + A = bit.bor(bit.lshift(P[16], 8), P[16+1]) + A = bit.band(A, 65287) + if not (A==33030) then goto L22 end + if 34 > length then return false end + A = bit.bor(bit.lshift(P[32], 8), P[32+1]) + if not (A==3880) then goto L22 end + ::L21:: + do return true end + ::L22:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 21 then return false end + local v1 = band(P[16],7) + if v1 == 2 then + return cast("uint16_t*", P+19)[0] == 3850 + end + if length < 22 then return false end + local v2 = band(cast("uint16_t*", P+16)[0],2047) + if v2 == 641 then + return cast("uint16_t*", P+20)[0] == 3850 + end + if length < 33 then return false end + if v1 == 6 then + return cast("uint16_t*", P+31)[0] == 3850 + end + if length < 34 then return false end + if v2 ~= 1665 then return false end + return cast("uint16_t*", P+32)[0] == 3850 +end + +``` + diff --git a/doc/dst-host-192.68.1.1-and-greater-100.md b/doc/dst-host-192.68.1.1-and-greater-100.md new file mode 100644 index 0000000000..b84ffcbe76 --- /dev/null +++ b/doc/dst-host-192.68.1.1-and-greater-100.md @@ -0,0 +1,72 @@ +# dst host 192.68.1.1 and greater 100 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 4 +002: A = P[30:4] +003: if (A == 3225682177) goto 8 else goto 11 +004: if (A == 2054) goto 6 else goto 5 +005: if (A == 32821) goto 6 else goto 11 +006: A = P[38:4] +007: if (A == 3225682177) goto 8 else goto 11 +008: A = length +009: if (A >= 100) goto 10 else goto 11 +010: return 65535 +011: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L3 end + if 34 > length then return false end + A = bit.bor(bit.lshift(P[30], 24),bit.lshift(P[30+1], 16), bit.lshift(P[30+2], 8), P[30+3]) + if (A==-1069285119) then goto L7 end + goto L10 + ::L3:: + if (A==2054) then goto L5 end + if not (A==32821) then goto L10 end + ::L5:: + if 42 > length then return false end + A = bit.bor(bit.lshift(P[38], 24),bit.lshift(P[38+1], 16), bit.lshift(P[38+2], 8), P[38+3]) + if not (A==-1069285119) then goto L10 end + ::L7:: + A = bit.tobit(length) + if not (runtime_u32(A)>=100) then goto L10 end + do return true end + ::L10:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 100 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + return cast("uint32_t*", P+30)[0] == 16860352 + end + if v1 == 1544 then goto L8 end + do + if v1 == 13696 then goto L8 end + return false + end +::L8:: + return cast("uint32_t*", P+38)[0] == 16860352 +end + +``` + diff --git a/doc/dst-portrange-80-90.md b/doc/dst-portrange-80-90.md new file mode 100644 index 0000000000..ccd66f3c5a --- /dev/null +++ b/doc/dst-portrange-80-90.md @@ -0,0 +1,136 @@ +# dst portrange 80-90 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 8 +002: A = P[20:1] +003: if (A == 132) goto 6 else goto 4 +004: if (A == 6) goto 6 else goto 5 +005: if (A == 17) goto 6 else goto 20 +006: A = P[56:2] +007: if (A >= 80) goto 18 else goto 20 +008: if (A == 2048) goto 9 else goto 20 +009: A = P[23:1] +010: if (A == 132) goto 13 else goto 11 +011: if (A == 6) goto 13 else goto 12 +012: if (A == 17) goto 13 else goto 20 +013: A = P[20:2] +014: if (A & 8191 != 0) goto 20 else goto 15 +015: X = (P[14:1] & 0xF) << 2 +016: A = P[X+16:2] +017: if (A >= 80) goto 18 else goto 20 +018: if (A > 90) goto 20 else goto 19 +019: return 65535 +020: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L7 end + if 21 > length then return false end + A = P[20] + if (A==132) then goto L5 end + if (A==6) then goto L5 end + if not (A==17) then goto L19 end + ::L5:: + if 58 > length then return false end + A = bit.bor(bit.lshift(P[56], 8), P[56+1]) + if (runtime_u32(A)>=80) then goto L17 end + goto L19 + ::L7:: + if not (A==2048) then goto L19 end + if 24 > length then return false end + A = P[23] + if (A==132) then goto L12 end + if (A==6) then goto L12 end + if not (A==17) then goto L19 end + ::L12:: + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L19 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+16)) + if T < 0 or T + 2 > length then return false end + A = bit.bor(bit.lshift(P[T], 8), P[T+1]) + if not (runtime_u32(A)>=80) then goto L19 end + ::L17:: + if (runtime_u32(A)>90) then goto L19 end + do return true end + ::L19:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local rshift = require("bit").rshift +local bswap = require("bit").bswap +local cast = require("ffi").cast +local lshift = require("bit").lshift +local band = require("bit").band +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + local v2 = P[23] + if v2 == 6 then goto L8 end + do + if v2 == 17 then goto L8 end + if v2 == 132 then goto L8 end + return false + end +::L8:: + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v3 = lshift(band(P[14],15),2) + if (v3 + 18) > length then return false end + local v4 = rshift(bswap(cast("uint16_t*", P+(v3 + 16))[0]), 16) + if v4 < 80 then return false end + return v4 <= 90 + else + if length < 58 then return false end + if v1 ~= 56710 then return false end + local v5 = P[20] + if v5 == 6 then goto L24 end + do + if v5 ~= 44 then goto L27 end + do + if P[54] == 6 then goto L24 end + goto L27 + end +::L27:: + if v5 == 17 then goto L24 end + if v5 ~= 44 then goto L33 end + do + if P[54] == 17 then goto L24 end + goto L33 + end +::L33:: + if v5 == 132 then goto L24 end + if v5 ~= 44 then return false end + if P[54] == 132 then goto L24 end + return false + end +::L24:: + local v6 = rshift(bswap(cast("uint16_t*", P+56)[0]), 16) + if v6 < 80 then return false end + return v6 <= 90 + end +end + +``` + diff --git a/doc/ether-broadcast.md b/doc/ether-broadcast.md new file mode 100644 index 0000000000..510fb9b6d9 --- /dev/null +++ b/doc/ether-broadcast.md @@ -0,0 +1,46 @@ +# ether broadcast + + +## BPF + +``` +000: A = P[2:4] +001: if (A == 4294967295) goto 2 else goto 5 +002: A = P[0:2] +003: if (A == 65535) goto 4 else goto 5 +004: return 65535 +005: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 6 > length then return false end + A = bit.bor(bit.lshift(P[2], 24),bit.lshift(P[2+1], 16), bit.lshift(P[2+2], 8), P[2+3]) + if not (A==-1) then goto L4 end + if 2 > length then return false end + A = bit.bor(bit.lshift(P[0], 8), P[0+1]) + if not (A==65535) then goto L4 end + do return true end + ::L4:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 6 then return false end + if cast("uint16_t*", P+0)[0] ~= 65535 then return false end + return cast("uint32_t*", P+2)[0] == 4294967295 +end + +``` + diff --git a/doc/ether-multicast.md b/doc/ether-multicast.md new file mode 100644 index 0000000000..653f7891fc --- /dev/null +++ b/doc/ether-multicast.md @@ -0,0 +1,40 @@ +# ether multicast + + +## BPF + +``` +000: A = P[0:1] +001: if (A & 1 != 0) goto 2 else goto 3 +002: return 65535 +003: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 1 > length then return false end + A = P[0] + if (bit.band(A, 1)==0) then goto L2 end + do return true end + ::L2:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local band = require("bit").band +return function(P,length) + if length < 1 then return false end + return band(P[0],1) ~= 0 +end + +``` + diff --git a/doc/ether-proto-1500.md b/doc/ether-proto-1500.md new file mode 100644 index 0000000000..0f2fb4d8cb --- /dev/null +++ b/doc/ether-proto-1500.md @@ -0,0 +1,43 @@ +# ether proto 1500 + + +## BPF + +``` +000: A = P[12:2] +001: if (A > 1500) goto 5 else goto 2 +002: A = P[14:1] +003: if (A == 1500) goto 4 else goto 5 +004: return 65535 +005: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if (runtime_u32(A)>1500) then goto L4 end + if 15 > length then return false end + A = P[14] + if not (A==1500) then goto L4 end + do return true end + ::L4:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +return function(P,length) + return false +end + +``` + diff --git a/doc/ether-proto-1501.md b/doc/ether-proto-1501.md new file mode 100644 index 0000000000..2328914128 --- /dev/null +++ b/doc/ether-proto-1501.md @@ -0,0 +1,40 @@ +# ether proto 1501 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 1501) goto 2 else goto 3 +002: return 65535 +003: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==1501) then goto L2 end + do return true end + ::L2:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 14 then return false end + return cast("uint16_t*", P+12)[0] == 56581 +end + +``` + diff --git a/doc/ether-proto-255.md b/doc/ether-proto-255.md new file mode 100644 index 0000000000..aeddbccc4e --- /dev/null +++ b/doc/ether-proto-255.md @@ -0,0 +1,48 @@ +# ether proto 255 + + +## BPF + +``` +000: A = P[12:2] +001: if (A > 1500) goto 5 else goto 2 +002: A = P[14:1] +003: if (A == 255) goto 4 else goto 5 +004: return 65535 +005: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if (runtime_u32(A)>1500) then goto L4 end + if 15 > length then return false end + A = P[14] + if not (A==255) then goto L4 end + do return true end + ::L4:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local rshift = require("bit").rshift +local bswap = require("bit").bswap +local cast = require("ffi").cast +return function(P,length) + if length < 15 then return false end + if rshift(bswap(cast("uint16_t*", P+12)[0]), 16) > 1500 then return false end + return P[14] == 255 +end + +``` + diff --git a/doc/ether-proto-decnet.md b/doc/ether-proto-decnet.md new file mode 100644 index 0000000000..f0b3807193 --- /dev/null +++ b/doc/ether-proto-decnet.md @@ -0,0 +1,40 @@ +# ether proto \decnet + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 24579) goto 2 else goto 3 +002: return 65535 +003: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==24579) then goto L2 end + do return true end + ::L2:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 14 then return false end + return cast("uint16_t*", P+12)[0] == 864 +end + +``` + diff --git a/doc/extensions.md b/doc/extensions.md new file mode 100644 index 0000000000..7e9d7ba907 --- /dev/null +++ b/doc/extensions.md @@ -0,0 +1,52 @@ +# Pflang extensions + +Pflua implements "pflang", the language that libpcap uses to express +packet filters. Pflua also optionally provides some experimental +extensions to pflang. These extensions are experimental, and naturally +are not supported by the libpcap pipeline. Please let us know if you +find them to be useful to you. + +## The address-of operator: `&` + +By default, this operator is enabled. To disable, include this code +somewhere in your app before parsing: + +```lua +require('pf.parse').allow_address_of = false +``` + +An AddressExpression is a new kind of arithmetic expression. The +grammar is as follows: + +``` +AddressExpression := '&' Addressable +Addressable := PayloadAccessor '[' ArithmeticExpression [ ':' (1|2|4) ] ']' +PayloadAccessor := 'ip' | 'ip6' | 'tcp' | 'udp' | 'icmp' | + 'arp' | 'rarp' | 'wlan' | 'ether' | 'fddi' | 'tr' | 'ppp' | + 'slip' | 'link' | 'radio' | 'ip' | 'ip6' | 'tcp' | 'udp' | + 'igmp' | 'pim' | 'igrp' | 'vrrp' | 'sctp' +``` + +The semantics are that `&foo` returns the address of `foo`, as a byte +offset from the beginning of the packet. Therefore if a packet is TCP +and the first byte of the packet is the first byte of the ethernet +header, then `ether[&tcp[0]] = tcp[0]` will always be true. + +If the packet is not of the correct kind, then the comparison in which +the AddressExpression is embedded fails to match. This would be the +case, for example, if you asked for `&udp[0]` but the packet isn't UDP. + +Likewise, if the address isn't within the packet, the containing +comparison will fail to match. An example would be `&udp[64]` on a UDP +packet whose size, including the UDP header but excluding the IP or +other L2 header is less than 64 bytes. Note that this behavior differs +from `udp[64]` on a short packet; such an access causes the whole filter +to abort (see the +[pflang](https://github.com/Igalia/pflua/blob/master/doc/pflang.md) +documentation). + +## The pfmatch language + +See [the pfmatch +page](https://github.com/Igalia/pflua/blob/master/doc/pfmatch.md), for +more. diff --git a/doc/fail-fail.md b/doc/fail-fail.md new file mode 100644 index 0000000000..587e57895e --- /dev/null +++ b/doc/fail-fail.md @@ -0,0 +1,70 @@ +# tcp and tcp[100] == 1 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 11 else goto 2 +002: if (A == 2048) goto 3 else goto 11 +003: A = P[23:1] +004: if (A == 6) goto 5 else goto 11 +005: A = P[20:2] +006: if (A & 8191 != 0) goto 11 else goto 7 +007: X = (P[14:1] & 0xF) << 2 +008: A = P[X+114:1] +009: if (A == 1) goto 10 else goto 11 +010: return 65535 +011: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if (A==34525) then goto L10 end + if not (A==2048) then goto L10 end + if 24 > length then return false end + A = P[23] + if not (A==6) then goto L10 end + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L10 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+114)) + if T < 0 or T + 1 > length then return false end + A = P[T] + if not (A==1) then goto L10 end + do return true end + ::L10:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local lshift = require("bit").lshift +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + if P[23] ~= 6 then return false end + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v1 = lshift(band(P[14],15),2) + if (v1 + 115) > length then return false end + return P[(v1 + 114)] == 1 +end + +``` + diff --git a/doc/host-127.0.0.1.md b/doc/host-127.0.0.1.md new file mode 100644 index 0000000000..b169a23f8b --- /dev/null +++ b/doc/host-127.0.0.1.md @@ -0,0 +1,82 @@ +# host 127.0.0.1 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 6 +002: A = P[26:4] +003: if (A == 2130706433) goto 12 else goto 4 +004: A = P[30:4] +005: if (A == 2130706433) goto 12 else goto 13 +006: if (A == 2054) goto 8 else goto 7 +007: if (A == 32821) goto 8 else goto 13 +008: A = P[28:4] +009: if (A == 2130706433) goto 12 else goto 10 +010: A = P[38:4] +011: if (A == 2130706433) goto 12 else goto 13 +012: return 65535 +013: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L5 end + if 30 > length then return false end + A = bit.bor(bit.lshift(P[26], 24),bit.lshift(P[26+1], 16), bit.lshift(P[26+2], 8), P[26+3]) + if (A==2130706433) then goto L11 end + if 34 > length then return false end + A = bit.bor(bit.lshift(P[30], 24),bit.lshift(P[30+1], 16), bit.lshift(P[30+2], 8), P[30+3]) + if (A==2130706433) then goto L11 end + goto L12 + ::L5:: + if (A==2054) then goto L7 end + if not (A==32821) then goto L12 end + ::L7:: + if 32 > length then return false end + A = bit.bor(bit.lshift(P[28], 24),bit.lshift(P[28+1], 16), bit.lshift(P[28+2], 8), P[28+3]) + if (A==2130706433) then goto L11 end + if 42 > length then return false end + A = bit.bor(bit.lshift(P[38], 24),bit.lshift(P[38+1], 16), bit.lshift(P[38+2], 8), P[38+3]) + if not (A==2130706433) then goto L12 end + ::L11:: + do return true end + ::L12:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + if cast("uint32_t*", P+26)[0] == 16777343 then return true end + return cast("uint32_t*", P+30)[0] == 16777343 + else + if length < 42 then return false end + if v1 == 1544 then goto L12 end + do + if v1 == 13696 then goto L12 end + return false + end +::L12:: + if cast("uint32_t*", P+28)[0] == 16777343 then return true end + return cast("uint32_t*", P+38)[0] == 16777343 + end +end + +``` + diff --git a/doc/host-ipv6-localhost.md b/doc/host-ipv6-localhost.md new file mode 100644 index 0000000000..45ee520c05 --- /dev/null +++ b/doc/host-ipv6-localhost.md @@ -0,0 +1,94 @@ +# host ::1 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 19 +002: A = P[22:4] +003: if (A == 0) goto 4 else goto 10 +004: A = P[26:4] +005: if (A == 0) goto 6 else goto 10 +006: A = P[30:4] +007: if (A == 0) goto 8 else goto 10 +008: A = P[34:4] +009: if (A == 1) goto 18 else goto 10 +010: A = P[38:4] +011: if (A == 0) goto 12 else goto 19 +012: A = P[42:4] +013: if (A == 0) goto 14 else goto 19 +014: A = P[46:4] +015: if (A == 0) goto 16 else goto 19 +016: A = P[50:4] +017: if (A == 1) goto 18 else goto 19 +018: return 65535 +019: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L18 end + if 26 > length then return false end + A = bit.bor(bit.lshift(P[22], 24),bit.lshift(P[22+1], 16), bit.lshift(P[22+2], 8), P[22+3]) + if not (A==0) then goto L9 end + if 30 > length then return false end + A = bit.bor(bit.lshift(P[26], 24),bit.lshift(P[26+1], 16), bit.lshift(P[26+2], 8), P[26+3]) + if not (A==0) then goto L9 end + if 34 > length then return false end + A = bit.bor(bit.lshift(P[30], 24),bit.lshift(P[30+1], 16), bit.lshift(P[30+2], 8), P[30+3]) + if not (A==0) then goto L9 end + if 38 > length then return false end + A = bit.bor(bit.lshift(P[34], 24),bit.lshift(P[34+1], 16), bit.lshift(P[34+2], 8), P[34+3]) + if (A==1) then goto L17 end + ::L9:: + if 42 > length then return false end + A = bit.bor(bit.lshift(P[38], 24),bit.lshift(P[38+1], 16), bit.lshift(P[38+2], 8), P[38+3]) + if not (A==0) then goto L18 end + if 46 > length then return false end + A = bit.bor(bit.lshift(P[42], 24),bit.lshift(P[42+1], 16), bit.lshift(P[42+2], 8), P[42+3]) + if not (A==0) then goto L18 end + if 50 > length then return false end + A = bit.bor(bit.lshift(P[46], 24),bit.lshift(P[46+1], 16), bit.lshift(P[46+2], 8), P[46+3]) + if not (A==0) then goto L18 end + if 54 > length then return false end + A = bit.bor(bit.lshift(P[50], 24),bit.lshift(P[50+1], 16), bit.lshift(P[50+2], 8), P[50+3]) + if not (A==1) then goto L18 end + ::L17:: + do return true end + ::L18:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 56710 then return false end + if cast("uint32_t*", P+22)[0] ~= 0 then goto L9 end + do + if cast("uint32_t*", P+26)[0] ~= 0 then goto L9 end + if cast("uint32_t*", P+30)[0] ~= 0 then goto L9 end + if cast("uint32_t*", P+34)[0] == 16777216 then return true end + goto L9 + end +::L9:: + if cast("uint32_t*", P+38)[0] ~= 0 then return false end + if cast("uint32_t*", P+42)[0] ~= 0 then return false end + if cast("uint32_t*", P+46)[0] ~= 0 then return false end + return cast("uint32_t*", P+50)[0] == 16777216 +end + +``` + diff --git a/doc/icmp-or-tcp-or-udp.md b/doc/icmp-or-tcp-or-udp.md new file mode 100644 index 0000000000..06a44f9543 --- /dev/null +++ b/doc/icmp-or-tcp-or-udp.md @@ -0,0 +1,97 @@ +# icmp or tcp or udp + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 5 +002: A = P[23:1] +003: if (A == 1) goto 12 else goto 4 +004: if (A == 6) goto 12 else goto 11 +005: if (A == 34525) goto 6 else goto 13 +006: A = P[20:1] +007: if (A == 6) goto 12 else goto 8 +008: if (A == 44) goto 9 else goto 11 +009: A = P[54:1] +010: if (A == 6) goto 12 else goto 11 +011: if (A == 17) goto 12 else goto 13 +012: return 65535 +013: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L4 end + if 24 > length then return false end + A = P[23] + if (A==1) then goto L11 end + if (A==6) then goto L11 end + goto L10 + ::L4:: + if not (A==34525) then goto L12 end + if 21 > length then return false end + A = P[20] + if (A==6) then goto L11 end + if not (A==44) then goto L10 end + if 55 > length then return false end + A = P[54] + if (A==6) then goto L11 end + ::L10:: + if not (A==17) then goto L12 end + ::L11:: + do return true end + ::L12:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + local v2 = P[23] + if v2 == 1 then return true end + if v2 == 6 then return true end + return v2 == 17 + else + if length < 54 then return false end + if v1 ~= 56710 then return false end + local v3 = P[20] + if v3 == 1 then return true end + if length < 55 then goto L19 end + do + if v3 ~= 44 then goto L19 end + if P[54] == 1 then return true end + goto L19 + end +::L19:: + if v3 == 6 then return true end + if length < 55 then goto L17 end + do + if v3 ~= 44 then goto L17 end + if P[54] == 6 then return true end + goto L17 + end +::L17:: + if v3 == 17 then return true end + if length < 55 then return false end + if v3 ~= 44 then return false end + return P[54] == 17 + end +end + +``` + diff --git a/doc/icmp6-or-ip.md b/doc/icmp6-or-ip.md new file mode 100644 index 0000000000..6cc1ac92ad --- /dev/null +++ b/doc/icmp6-or-ip.md @@ -0,0 +1,68 @@ +# icmp6 or ip + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 7 +002: A = P[20:1] +003: if (A == 58) goto 8 else goto 4 +004: if (A == 44) goto 5 else goto 9 +005: A = P[54:1] +006: if (A == 58) goto 8 else goto 9 +007: if (A == 2048) goto 8 else goto 9 +008: return 65535 +009: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L6 end + if 21 > length then return false end + A = P[20] + if (A==58) then goto L7 end + if not (A==44) then goto L8 end + if 55 > length then return false end + A = P[54] + if (A==58) then goto L7 end + goto L8 + ::L6:: + if not (A==2048) then goto L8 end + ::L7:: + do return true end + ::L8:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 14 then return false end + if length < 54 then goto L7 end + do + if cast("uint16_t*", P+12)[0] ~= 56710 then goto L7 end + local v1 = P[20] + if v1 == 58 then return true end + if length < 55 then goto L7 end + if v1 ~= 44 then goto L7 end + if P[54] == 58 then return true end + goto L7 + end +::L7:: + return cast("uint16_t*", P+12)[0] == 8 +end + +``` + diff --git a/doc/icmp6.md b/doc/icmp6.md new file mode 100644 index 0000000000..4c0cc95edc --- /dev/null +++ b/doc/icmp6.md @@ -0,0 +1,58 @@ +# icmp6 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 8 +002: A = P[20:1] +003: if (A == 58) goto 7 else goto 4 +004: if (A == 44) goto 5 else goto 8 +005: A = P[54:1] +006: if (A == 58) goto 7 else goto 8 +007: return 65535 +008: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L7 end + if 21 > length then return false end + A = P[20] + if (A==58) then goto L6 end + if not (A==44) then goto L7 end + if 55 > length then return false end + A = P[54] + if not (A==58) then goto L7 end + ::L6:: + do return true end + ::L7:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 56710 then return false end + local v1 = P[20] + if v1 == 58 then return true end + if length < 55 then return false end + if v1 ~= 44 then return false end + return P[54] == 58 +end + +``` + diff --git a/doc/ip-multicast.md b/doc/ip-multicast.md new file mode 100644 index 0000000000..a8fc030e52 --- /dev/null +++ b/doc/ip-multicast.md @@ -0,0 +1,46 @@ +# ip multicast + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 5 +002: A = P[30:1] +003: if (A >= 224) goto 4 else goto 5 +004: return 65535 +005: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L4 end + if 31 > length then return false end + A = P[30] + if not (runtime_u32(A)>=224) then goto L4 end + do return true end + ::L4:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + return P[30] == 224 +end + +``` + diff --git a/doc/ip-proto-47.md b/doc/ip-proto-47.md new file mode 100644 index 0000000000..ade5dcb5ed --- /dev/null +++ b/doc/ip-proto-47.md @@ -0,0 +1,46 @@ +# ip proto 47 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 5 +002: A = P[23:1] +003: if (A == 47) goto 4 else goto 5 +004: return 65535 +005: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L4 end + if 24 > length then return false end + A = P[23] + if not (A==47) then goto L4 end + do return true end + ::L4:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + return P[23] == 47 +end + +``` + diff --git a/doc/ip-proto-ah.md b/doc/ip-proto-ah.md new file mode 100644 index 0000000000..7478529d70 --- /dev/null +++ b/doc/ip-proto-ah.md @@ -0,0 +1,46 @@ +# ip proto \ah + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 5 +002: A = P[23:1] +003: if (A == 51) goto 4 else goto 5 +004: return 65535 +005: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L4 end + if 24 > length then return false end + A = P[23] + if not (A==51) then goto L4 end + do return true end + ::L4:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + return P[23] == 51 +end + +``` + diff --git a/doc/ip-proto-sctp.md b/doc/ip-proto-sctp.md new file mode 100644 index 0000000000..0be3eb3fcf --- /dev/null +++ b/doc/ip-proto-sctp.md @@ -0,0 +1,46 @@ +# ip proto \sctp + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 5 +002: A = P[23:1] +003: if (A == 132) goto 4 else goto 5 +004: return 65535 +005: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L4 end + if 24 > length then return false end + A = P[23] + if not (A==132) then goto L4 end + do return true end + ::L4:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + return P[23] == 132 +end + +``` + diff --git a/doc/ip6-multicast.md b/doc/ip6-multicast.md new file mode 100644 index 0000000000..53d769702d --- /dev/null +++ b/doc/ip6-multicast.md @@ -0,0 +1,46 @@ +# ip6 multicast + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 5 +002: A = P[38:1] +003: if (A == 255) goto 4 else goto 5 +004: return 65535 +005: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L4 end + if 39 > length then return false end + A = P[38] + if not (A==255) then goto L4 end + do return true end + ::L4:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 56710 then return false end + return P[38] == 255 +end + +``` + diff --git a/doc/ip6-proto-47.md b/doc/ip6-proto-47.md new file mode 100644 index 0000000000..0901f20fab --- /dev/null +++ b/doc/ip6-proto-47.md @@ -0,0 +1,58 @@ +# ip6 proto 47 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 8 +002: A = P[20:1] +003: if (A == 47) goto 7 else goto 4 +004: if (A == 44) goto 5 else goto 8 +005: A = P[54:1] +006: if (A == 47) goto 7 else goto 8 +007: return 65535 +008: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L7 end + if 21 > length then return false end + A = P[20] + if (A==47) then goto L6 end + if not (A==44) then goto L7 end + if 55 > length then return false end + A = P[54] + if not (A==47) then goto L7 end + ::L6:: + do return true end + ::L7:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 56710 then return false end + local v1 = P[20] + if v1 == 47 then return true end + if length < 55 then return false end + if v1 ~= 44 then return false end + return P[54] == 47 +end + +``` + diff --git a/doc/ip6-proto-ah.md b/doc/ip6-proto-ah.md new file mode 100644 index 0000000000..ddbf4bff3b --- /dev/null +++ b/doc/ip6-proto-ah.md @@ -0,0 +1,58 @@ +# ip6 proto \ah + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 8 +002: A = P[20:1] +003: if (A == 51) goto 7 else goto 4 +004: if (A == 44) goto 5 else goto 8 +005: A = P[54:1] +006: if (A == 51) goto 7 else goto 8 +007: return 65535 +008: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L7 end + if 21 > length then return false end + A = P[20] + if (A==51) then goto L6 end + if not (A==44) then goto L7 end + if 55 > length then return false end + A = P[54] + if not (A==51) then goto L7 end + ::L6:: + do return true end + ::L7:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 56710 then return false end + local v1 = P[20] + if v1 == 51 then return true end + if length < 55 then return false end + if v1 ~= 44 then return false end + return P[54] == 51 +end + +``` + diff --git a/doc/iso-proto-47.md b/doc/iso-proto-47.md new file mode 100644 index 0000000000..d899f1b571 --- /dev/null +++ b/doc/iso-proto-47.md @@ -0,0 +1,54 @@ +# iso proto 47 + + +## BPF + +``` +000: A = P[12:2] +001: if (A > 1500) goto 7 else goto 2 +002: A = P[14:2] +003: if (A == 65278) goto 4 else goto 7 +004: A = P[17:1] +005: if (A == 47) goto 6 else goto 7 +006: return 65535 +007: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if (runtime_u32(A)>1500) then goto L6 end + if 16 > length then return false end + A = bit.bor(bit.lshift(P[14], 8), P[14+1]) + if not (A==65278) then goto L6 end + if 18 > length then return false end + A = P[17] + if not (A==47) then goto L6 end + do return true end + ::L6:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local rshift = require("bit").rshift +local bswap = require("bit").bswap +local cast = require("ffi").cast +return function(P,length) + if length < 18 then return false end + if rshift(bswap(cast("uint16_t*", P+12)[0]), 16) > 1500 then return false end + if cast("uint16_t*", P+14)[0] ~= 65278 then return false end + return P[17] == 47 +end + +``` + diff --git a/doc/iso-proto-clnp.md b/doc/iso-proto-clnp.md new file mode 100644 index 0000000000..72fad089e8 --- /dev/null +++ b/doc/iso-proto-clnp.md @@ -0,0 +1,54 @@ +# iso proto \clnp + + +## BPF + +``` +000: A = P[12:2] +001: if (A > 1500) goto 7 else goto 2 +002: A = P[14:2] +003: if (A == 65278) goto 4 else goto 7 +004: A = P[17:1] +005: if (A == 129) goto 6 else goto 7 +006: return 65535 +007: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if (runtime_u32(A)>1500) then goto L6 end + if 16 > length then return false end + A = bit.bor(bit.lshift(P[14], 8), P[14+1]) + if not (A==65278) then goto L6 end + if 18 > length then return false end + A = P[17] + if not (A==129) then goto L6 end + do return true end + ::L6:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local rshift = require("bit").rshift +local bswap = require("bit").bswap +local cast = require("ffi").cast +return function(P,length) + if length < 18 then return false end + if rshift(bswap(cast("uint16_t*", P+12)[0]), 16) > 1500 then return false end + if cast("uint16_t*", P+14)[0] ~= 65278 then return false end + return P[17] == 129 +end + +``` + diff --git a/doc/l1.md b/doc/l1.md new file mode 100644 index 0000000000..f20997a517 --- /dev/null +++ b/doc/l1.md @@ -0,0 +1,74 @@ +# l1 + + +## BPF + +``` +000: A = P[12:2] +001: if (A > 1500) goto 13 else goto 2 +002: A = P[14:2] +003: if (A == 65278) goto 4 else goto 13 +004: A = P[17:1] +005: if (A == 131) goto 6 else goto 13 +006: A = P[21:1] +007: if (A == 26) goto 12 else goto 8 +008: if (A == 24) goto 12 else goto 9 +009: if (A == 18) goto 12 else goto 10 +010: if (A == 15) goto 12 else goto 11 +011: if (A == 17) goto 12 else goto 13 +012: return 65535 +013: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if (runtime_u32(A)>1500) then goto L12 end + if 16 > length then return false end + A = bit.bor(bit.lshift(P[14], 8), P[14+1]) + if not (A==65278) then goto L12 end + if 18 > length then return false end + A = P[17] + if not (A==131) then goto L12 end + if 22 > length then return false end + A = P[21] + if (A==26) then goto L11 end + if (A==24) then goto L11 end + if (A==18) then goto L11 end + if (A==15) then goto L11 end + if not (A==17) then goto L12 end + ::L11:: + do return true end + ::L12:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local rshift = require("bit").rshift +local bswap = require("bit").bswap +local cast = require("ffi").cast +return function(P,length) + if length < 22 then return false end + if rshift(bswap(cast("uint16_t*", P+12)[0]), 16) > 1500 then return false end + if cast("uint16_t*", P+14)[0] ~= 65278 then return false end + if P[17] ~= 131 then return false end + local v1 = P[21] + if v1 == 15 then return true end + if v1 == 18 then return true end + if v1 == 24 then return true end + if v1 == 26 then return true end + return v1 == 17 +end + +``` + diff --git a/doc/net-127.0.0.0-8.md b/doc/net-127.0.0.0-8.md new file mode 100644 index 0000000000..5193f6cfdb --- /dev/null +++ b/doc/net-127.0.0.0-8.md @@ -0,0 +1,91 @@ +# net 127.0.0.0/8 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 8 +002: A = P[26:4] +003: A &= 4278190080 +004: if (A == 2130706432) goto 16 else goto 5 +005: A = P[30:4] +006: A &= 4278190080 +007: if (A == 2130706432) goto 16 else goto 17 +008: if (A == 2054) goto 10 else goto 9 +009: if (A == 32821) goto 10 else goto 17 +010: A = P[28:4] +011: A &= 4278190080 +012: if (A == 2130706432) goto 16 else goto 13 +013: A = P[38:4] +014: A &= 4278190080 +015: if (A == 2130706432) goto 16 else goto 17 +016: return 65535 +017: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L7 end + if 30 > length then return false end + A = bit.bor(bit.lshift(P[26], 24),bit.lshift(P[26+1], 16), bit.lshift(P[26+2], 8), P[26+3]) + A = bit.band(A, -16777216) + if (A==2130706432) then goto L15 end + if 34 > length then return false end + A = bit.bor(bit.lshift(P[30], 24),bit.lshift(P[30+1], 16), bit.lshift(P[30+2], 8), P[30+3]) + A = bit.band(A, -16777216) + if (A==2130706432) then goto L15 end + goto L16 + ::L7:: + if (A==2054) then goto L9 end + if not (A==32821) then goto L16 end + ::L9:: + if 32 > length then return false end + A = bit.bor(bit.lshift(P[28], 24),bit.lshift(P[28+1], 16), bit.lshift(P[28+2], 8), P[28+3]) + A = bit.band(A, -16777216) + if (A==2130706432) then goto L15 end + if 42 > length then return false end + A = bit.bor(bit.lshift(P[38], 24),bit.lshift(P[38+1], 16), bit.lshift(P[38+2], 8), P[38+3]) + A = bit.band(A, -16777216) + if not (A==2130706432) then goto L16 end + ::L15:: + do return true end + ::L16:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + if band(cast("uint32_t*", P+26)[0],255) == 127 then return true end + return band(cast("uint32_t*", P+30)[0],255) == 127 + else + if length < 42 then return false end + if v1 == 1544 then goto L12 end + do + if v1 == 13696 then goto L12 end + return false + end +::L12:: + if band(cast("uint32_t*", P+28)[0],255) == 127 then return true end + return band(cast("uint32_t*", P+38)[0],255) == 127 + end +end + +``` + diff --git a/doc/net-ipv6-0-mask-16.md b/doc/net-ipv6-0-mask-16.md new file mode 100644 index 0000000000..5d24e9c9e2 --- /dev/null +++ b/doc/net-ipv6-0-mask-16.md @@ -0,0 +1,54 @@ +# net ::0/16 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 7 +002: A = P[22:4] +003: if (A & 4294901760 != 0) goto 4 else goto 6 +004: A = P[38:4] +005: if (A & 4294901760 != 0) goto 7 else goto 6 +006: return 65535 +007: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L6 end + if 26 > length then return false end + A = bit.bor(bit.lshift(P[22], 24),bit.lshift(P[22+1], 16), bit.lshift(P[22+2], 8), P[22+3]) + if (bit.band(A, -65536)==0) then goto L5 end + if 42 > length then return false end + A = bit.bor(bit.lshift(P[38], 24),bit.lshift(P[38+1], 16), bit.lshift(P[38+2], 8), P[38+3]) + if not (bit.band(A, -65536)==0) then goto L6 end + ::L5:: + do return true end + ::L6:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 56710 then return false end + if band(cast("uint32_t*", P+22)[0],65535) == 0 then return true end + return band(cast("uint32_t*", P+38)[0],65535) == 0 +end + +``` + diff --git a/doc/net-ipv6-ee.cc.9954.0-mask-111.md b/doc/net-ipv6-ee.cc.9954.0-mask-111.md new file mode 100644 index 0000000000..3b422828bc --- /dev/null +++ b/doc/net-ipv6-ee.cc.9954.0-mask-111.md @@ -0,0 +1,99 @@ +# net ee:cc::9954:0/111 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 21 +002: A = P[22:4] +003: if (A == 15597772) goto 4 else goto 11 +004: A = P[26:4] +005: if (A == 0) goto 6 else goto 11 +006: A = P[30:4] +007: if (A == 0) goto 8 else goto 11 +008: A = P[34:4] +009: A &= 4294836224 +010: if (A == 2572419072) goto 20 else goto 11 +011: A = P[38:4] +012: if (A == 15597772) goto 13 else goto 21 +013: A = P[42:4] +014: if (A == 0) goto 15 else goto 21 +015: A = P[46:4] +016: if (A == 0) goto 17 else goto 21 +017: A = P[50:4] +018: A &= 4294836224 +019: if (A == 2572419072) goto 20 else goto 21 +020: return 65535 +021: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L20 end + if 26 > length then return false end + A = bit.bor(bit.lshift(P[22], 24),bit.lshift(P[22+1], 16), bit.lshift(P[22+2], 8), P[22+3]) + if not (A==15597772) then goto L10 end + if 30 > length then return false end + A = bit.bor(bit.lshift(P[26], 24),bit.lshift(P[26+1], 16), bit.lshift(P[26+2], 8), P[26+3]) + if not (A==0) then goto L10 end + if 34 > length then return false end + A = bit.bor(bit.lshift(P[30], 24),bit.lshift(P[30+1], 16), bit.lshift(P[30+2], 8), P[30+3]) + if not (A==0) then goto L10 end + if 38 > length then return false end + A = bit.bor(bit.lshift(P[34], 24),bit.lshift(P[34+1], 16), bit.lshift(P[34+2], 8), P[34+3]) + A = bit.band(A, -131072) + if (A==-1722548224) then goto L19 end + ::L10:: + if 42 > length then return false end + A = bit.bor(bit.lshift(P[38], 24),bit.lshift(P[38+1], 16), bit.lshift(P[38+2], 8), P[38+3]) + if not (A==15597772) then goto L20 end + if 46 > length then return false end + A = bit.bor(bit.lshift(P[42], 24),bit.lshift(P[42+1], 16), bit.lshift(P[42+2], 8), P[42+3]) + if not (A==0) then goto L20 end + if 50 > length then return false end + A = bit.bor(bit.lshift(P[46], 24),bit.lshift(P[46+1], 16), bit.lshift(P[46+2], 8), P[46+3]) + if not (A==0) then goto L20 end + if 54 > length then return false end + A = bit.bor(bit.lshift(P[50], 24),bit.lshift(P[50+1], 16), bit.lshift(P[50+2], 8), P[50+3]) + A = bit.band(A, -131072) + if not (A==-1722548224) then goto L20 end + ::L19:: + do return true end + ::L20:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 56710 then return false end + if cast("uint32_t*", P+22)[0] ~= 3422612992 then goto L9 end + do + if cast("uint32_t*", P+26)[0] ~= 0 then goto L9 end + if cast("uint32_t*", P+30)[0] ~= 0 then goto L9 end + if band(cast("uint32_t*", P+34)[0],65279) == 21657 then return true end + goto L9 + end +::L9:: + if cast("uint32_t*", P+38)[0] ~= 3422612992 then return false end + if cast("uint32_t*", P+42)[0] ~= 0 then return false end + if cast("uint32_t*", P+46)[0] ~= 0 then return false end + return band(cast("uint32_t*", P+50)[0],65279) == 21657 +end + +``` + diff --git a/doc/packet-access-igmp.md b/doc/packet-access-igmp.md new file mode 100644 index 0000000000..aabbaa686b --- /dev/null +++ b/doc/packet-access-igmp.md @@ -0,0 +1,68 @@ +# igmp[8] < 8 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 10 +002: A = P[23:1] +003: if (A == 2) goto 4 else goto 10 +004: A = P[20:2] +005: if (A & 8191 != 0) goto 10 else goto 6 +006: X = (P[14:1] & 0xF) << 2 +007: A = P[X+22:1] +008: if (A >= 8) goto 10 else goto 9 +009: return 65535 +010: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L9 end + if 24 > length then return false end + A = P[23] + if not (A==2) then goto L9 end + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L9 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+22)) + if T < 0 or T + 1 > length then return false end + A = P[T] + if (runtime_u32(A)>=8) then goto L9 end + do return true end + ::L9:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local lshift = require("bit").lshift +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 42 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + if P[23] ~= 2 then return false end + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v1 = lshift(band(P[14],15),2) + if (v1 + 23) > length then return false end + return P[(v1 + 22)] < 8 +end + +``` + diff --git a/doc/packet-access-igrp.md b/doc/packet-access-igrp.md new file mode 100644 index 0000000000..05531b05fe --- /dev/null +++ b/doc/packet-access-igrp.md @@ -0,0 +1,68 @@ +# igrp[8] < 8 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 10 +002: A = P[23:1] +003: if (A == 9) goto 4 else goto 10 +004: A = P[20:2] +005: if (A & 8191 != 0) goto 10 else goto 6 +006: X = (P[14:1] & 0xF) << 2 +007: A = P[X+22:1] +008: if (A >= 8) goto 10 else goto 9 +009: return 65535 +010: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L9 end + if 24 > length then return false end + A = P[23] + if not (A==9) then goto L9 end + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L9 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+22)) + if T < 0 or T + 1 > length then return false end + A = P[T] + if (runtime_u32(A)>=8) then goto L9 end + do return true end + ::L9:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local lshift = require("bit").lshift +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 42 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + if P[23] ~= 9 then return false end + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v1 = lshift(band(P[14],15),2) + if (v1 + 23) > length then return false end + return P[(v1 + 22)] < 8 +end + +``` + diff --git a/doc/packet-access-pim.md b/doc/packet-access-pim.md new file mode 100644 index 0000000000..cc4d5a95e2 --- /dev/null +++ b/doc/packet-access-pim.md @@ -0,0 +1,68 @@ +# pim[8] < 8 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 10 +002: A = P[23:1] +003: if (A == 103) goto 4 else goto 10 +004: A = P[20:2] +005: if (A & 8191 != 0) goto 10 else goto 6 +006: X = (P[14:1] & 0xF) << 2 +007: A = P[X+22:1] +008: if (A >= 8) goto 10 else goto 9 +009: return 65535 +010: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L9 end + if 24 > length then return false end + A = P[23] + if not (A==103) then goto L9 end + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L9 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+22)) + if T < 0 or T + 1 > length then return false end + A = P[T] + if (runtime_u32(A)>=8) then goto L9 end + do return true end + ::L9:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local lshift = require("bit").lshift +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 38 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + if P[23] ~= 103 then return false end + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v1 = lshift(band(P[14],15),2) + if (v1 + 23) > length then return false end + return P[(v1 + 22)] < 8 +end + +``` + diff --git a/doc/packet-access-sctp.md b/doc/packet-access-sctp.md new file mode 100644 index 0000000000..ce28475455 --- /dev/null +++ b/doc/packet-access-sctp.md @@ -0,0 +1,68 @@ +# sctp[8] < 8 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 10 +002: A = P[23:1] +003: if (A == 132) goto 4 else goto 10 +004: A = P[20:2] +005: if (A & 8191 != 0) goto 10 else goto 6 +006: X = (P[14:1] & 0xF) << 2 +007: A = P[X+22:1] +008: if (A >= 8) goto 10 else goto 9 +009: return 65535 +010: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L9 end + if 24 > length then return false end + A = P[23] + if not (A==132) then goto L9 end + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L9 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+22)) + if T < 0 or T + 1 > length then return false end + A = P[T] + if (runtime_u32(A)>=8) then goto L9 end + do return true end + ::L9:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local lshift = require("bit").lshift +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 46 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + if P[23] ~= 132 then return false end + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v1 = lshift(band(P[14],15),2) + if (v1 + 23) > length then return false end + return P[(v1 + 22)] < 8 +end + +``` + diff --git a/doc/packet-access-vrrp.md b/doc/packet-access-vrrp.md new file mode 100644 index 0000000000..7a955c3d08 --- /dev/null +++ b/doc/packet-access-vrrp.md @@ -0,0 +1,68 @@ +# vrrp[8] < 8 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 10 +002: A = P[23:1] +003: if (A == 112) goto 4 else goto 10 +004: A = P[20:2] +005: if (A & 8191 != 0) goto 10 else goto 6 +006: X = (P[14:1] & 0xF) << 2 +007: A = P[X+22:1] +008: if (A >= 8) goto 10 else goto 9 +009: return 65535 +010: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L9 end + if 24 > length then return false end + A = P[23] + if not (A==112) then goto L9 end + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L9 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+22)) + if T < 0 or T + 1 > length then return false end + A = P[T] + if (runtime_u32(A)>=8) then goto L9 end + do return true end + ::L9:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local lshift = require("bit").lshift +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 42 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + if P[23] ~= 112 then return false end + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v1 = lshift(band(P[14],15),2) + if (v1 + 23) > length then return false end + return P[(v1 + 22)] < 8 +end + +``` + diff --git a/doc/pflang.md b/doc/pflang.md new file mode 100644 index 0000000000..cc395e41d1 --- /dev/null +++ b/doc/pflang.md @@ -0,0 +1,133 @@ +# Pflang resources + +"Pflang" is what we are calling the language that libpcap uses to +express packet filters. It's easier to say than "the pcap-filter +language". + +On the plus side, pflang is widely used, works pretty well, is +pleasantly terse and expressive, and has a huge amount of +domain-specific knowledge baked into it. + +At the same time, to a language professional, pflang can be infuriating. +This page documents some less-known aspects of pflang. See also [pflua +extensions to +pflang](https://github.com/Igalia/pflua/blob/master/doc/extensions.md). + +## Specification + +https://www.wireshark.org/docs/man-pages/pcap-filter.html + +If you have libpcap installed on your system, you might also have the +pcap-filter man page installed. Try `man pcap-filter`. + +## Syntactic corner-cases + +### Implicit OR + +Quoth the documentation: + +> If an identifier is given without a keyword, the most recent keyword is assumed. For example, +> +> > not host vs and ace +> +> is short for +> +> > not host vs and host ace +> +> which should not be confused with +> +> > not ( host vs or ace ) + +This is bizarre and ambiguous; what if you have a host whose name is a +pflang keyword, or a symbolic constant? + +Also note a further example from the documentation: + +> host helios and ( hot or ace ) + +Grrr. + +### Use of `-` both as a name component and as a delimiter + +These are all valid port ranges: + +``` +portrange ftp-data-90 +portrange 80-ftp-data +portrange ftp-data-iso-tsap +portrange echo-ftp-data +``` + +In each of these examples, one of the hyphens delimits the two parts. +Whether it's the first or second depends on the set of known symbolic +port names (!). + +Port names are a particular case, as they can only be seen after `port` +or `portrange`. Arithmetic expressions are more tricky, as there is a +larger set of symbolic constants, and `-` may appear in more places. + +### Extraneous backslashes + +Under the documentation for `ip proto _proto_`, the documentation says: + +> True if the packet is an IPv4 packet (see ip(4P)) of protocol type +> protocol. Protocol can be a number or one of the names icmp, icmp6, +> igmp, igrp, pim, ah, esp, vrrp, udp, or tcp. Note that the identifiers +> tcp, udp, and icmp are also keywords and must be escaped via backslash +> (\\), which is \\\\ in the C-shell. + +This note does not make sense. The context is unambiguous; `ip proto` +_needs_ something to follow it, and can interpret the next token as it +likes. + +Also, what could the shell have to do with this? We don't know. + +See also later in the document when it says, when discussing grouping +via parentheses: + +> parentheses are special to the Shell and must be escaped + +Wat. + +## Semantic corner cases + +Some parts of pflang are quite surprising. + +### Packet access + +One set of restrictions is for packet accessors, e.g. the `ip[0]` in +`ip[0] == 42`. + +* If the packet is not an IPv4 or IPv6 packet, the clause fails to + match. + +* If the index (e.g. 0 in this case) is not within the packet, then the + filter immediately fails. + +There is an interesting bug in libpcap's implementation of length +checking: https://github.com/the-tcpdump-group/libpcap/issues/379 + +### Numeric range + +All numbers in pflang are unsigned 32-bit values. Operations on these +numbers follows C's semantics. Notably, arithmetic is modulo 2^32. + +Bit shifts are an interesting case. The C99 draft standard says, +regarding bit shifts: + +> The integer promotions are performed on each of the operands. The type +> of the result is that of the promoted left operand. If the value of +> the right operand is negative or is *greater than or equal to the +> width of the promoted left operand*, the behavior is undefined. + +But, none of the BPF implementations actually check that the RHS is less +than 32, so they all may execute undefined behavior. + +### Division by zero + +Including division in pflang was an odd choice, but it's there. If the +right-hand-side is zero, the filter fails immediately. + +### Some selectors assume IPv4 + +In libpcap, `tcp port 80` only matches IPv4 packets. diff --git a/doc/pfmatch.md b/doc/pfmatch.md new file mode 100644 index 0000000000..cfa8a46b39 --- /dev/null +++ b/doc/pfmatch.md @@ -0,0 +1,223 @@ +# Pfmatch + +Pfmatch is a pattern-matching language for network packets, embedded in +Lua. It is built on the well-known +[pflang](https://github.com/Igalia/pflua/blob/master/doc/pflang.md) +packet filtering language, using the fast +[pflua](https://github.com/Igalia/pflua/blob/master/README.md) compiler +for LuaJIT + +Here's an example of a simple pfmatch program that just divides up +packets depending on whether they are UDP, TCP, or something else: + +```lua +match { + tcp => handle_tcp + udp => handle_udp + otherwise => handle_other +} +``` + +Unlike pflang filters written for such tools as `tcpdump`, a pfmatch +program can dispatch packets to multiple handlers, potentially +destructuring them along the way. In contrast, a pflang filter can only +say "yes" or "no" on a packet. + +Here's a more complicated example that passes all non-IP traffic, drops +all IP traffic that is not going to or coming from certain IP addresses, +and calls a handler on the rest of the traffic. + +```lua +match { + not ip => forward + ip src 1.2.3.4 => incoming_ip + ip dst 5.6.7.8 => outgoing_ip + otherwise => drop +} +``` + +In the example above, the handlers after the arrows (`=>`) are Lua +functions. If a handler matches (more on that later), it will be called +with two arguments: the packet data and the length. You can pass more +arguments by specifying them after the handler. For example, we could +pass the offset of the start of the IP header by using the [address-of +extension](https://github.com/Igalia/pflua/blob/master/doc/extensions.md): + + +```lua +match { + not ip => forward + ip src 1.2.3.4 => incoming_ip(&ip[0]) + ip dst 5.6.7.8 => outgoing_ip(&ip[0]) + otherwise => drop +} +``` + +Of course, with pflang you could just match all of the clauses in order: + +```lua +not_ip = pf.compile('not ip') +incoming = pf.compile('ip src 1.2.3.4') +outgoing = pf.compile('ip dst 5.6.7.8') + +function handle(packet, len) + if not_ip(packet, len) then return forward(packet, len) + elseif incoming(packet, len) then return incoming_ip(packet, len) + elseif outgoing(packet, len) then return outgoing_ip(packet, len) + else return drop(packet, len) end +end +``` + +But not only is this tedious, you don't get easy access to the packet +itself, and you're missing out on opportunities for optimization. For +example, the if the packet fails the `not_ip` check, then we don't need +to check if it's an IP packet in the `incoming` check. Compiling a +pfmatch program takes advantage of pflua's optimizer to produce optimal +code for all clauses in your match expression. + +Pflua compiles the pfmatch expression above into the nice, short code +below: + +```lua +local cast = require("ffi").cast +return function(self,P,length) + if length < 14 then return self.forward(P, len) end + if cast("uint16_t*", P+12)[0] ~= 8 then return self.forward(P, len) end + if length < 34 then return self.drop(P, len) end + if P[23] ~= 6 then return self.drop(P, len) end + if cast("uint32_t*", P+26)[0] == 67305985 then return self.incoming_ip(P, len, 14) end + if cast("uint32_t*", P+30)[0] == 134678021 then return self.outgoing_ip(P, len, 14) end + return self.drop(P, len) +end +``` + +The result is a pretty good dispatcher. There are always things to +improve, but it's likely that the compiled Lua above is better than what +you would write by hand, and it will continue to get better as pflua +improves. + +When we write filtering code by hand, we inevitably end up writing +_interpreters_ for some kind of filtering language. Using pflua and +pfmatch expressions, we can instead _compile_ a filter suited directly +for the problem at hand -- and while we're at it, we can forget about +worrying about pesky offsets and bit-shifts. + +## Syntax + +The grammar of the pfmatch language is below. + +``` +Program := 'match' Cond +Cond := '{' Clause... '}' +Clause := Test '=>' Dispatch [ClauseTerminator] +Test := 'otherwise' | LogicalExpression +ClauseTerminator := ',' | ';' +Dispatch := Call | Cond +Call := Identifier [ Args ] +Args := '(' [ ArithmeticExpression [ ',' ArithmeticExpression ] ] ')' +``` + +`LogicalExpression` and `ArithmeticExpression` are embedded productions +of pflang. `otherwise` is a Test that always matches. + +Comments are prefixed by `--` and continue to the end of the line. + +## Semantics + +Compiling a `Program` produces a `Matcher`. A `Matcher` is a function +of three arguments: a handlers table, the packet data as a `uint8_t*`, +and the packet length in bytes. + +Calling a `Matcher` will either result in a tail call to a member +function of the handlers table, or return `nil` if no dispatch matches. + +A `Call` matches if all of the conditions necessary to evaluate the +arithmetic expressions in its arguments are true. (For example, the +argument of `handle(ip[42])` is only valid if the packet is an IPv4 +packet of a sufficient length.) + +A `Cond` always matches; once you enter a `Cond`, no clause outside the +Cond will match. If no clause in the `Cond` matches, the result is +`nil`. + +A `Clause` matches if the `Test` on the left-hand-side of the arrow is +true. If the right-hand-side is a `Call`, the conditions from the +`Args` (if any) are implicitly added to the `Test` on the left. In this +way it's possible for the `Test` to be true but some condition from the +`Call` to be false, which causes the match to proceed with the next +`Clause`. + +Unlike pflang, attempting to access out-of-bounds packet data merely +causes a clause not to match, instead of immediately aborting the +match. + +## Using pfmatch + +The interface to pfmatch is the `pf.match.compile` function. In a +[Snabb](https://github.com/SnabbCo/snabbswitch) context, this might look +like: + +```lua +local match = require('pf.match') + +Filter = {} + +function Filter:new(conf) + local app = {} + function app.forward(data, len) + return len + end + function app.drop(data, len) + -- Could truncate packet here and overwrite with ICMP error if + -- wanted. + return nil + end + function app.incoming_ip(data, len, ip_base) + -- Munge the packet. Return len if we resend the packet. + return len + end + function app.outgoing_ip(data, len, ip_base) + -- Munge the packet. Return len if we resend the packet. + return len + end + app.match = match.compile([[match { + not ip => forward + -- Drop fragmented packets. + ip[6:2] & 0x3fff != 0 => drop + ip src 1.2.3.4 => incoming_ip(&ip[0]) + ip dst 5.6.7.8 => outgoing_ip(&ip[0]) + otherwise => drop + }]]) + return setmetatable(app, {__index=Filter}) +end + +function Filter:push () + local i, o = self.input.input, self.output.output + while not link.empty() do + local pkt = link.receive(i) + local out_len = self:match(pkt.data, pkt.length) + if out_len then + pkt.length = out_len + link.transmit(o, pkt) + end + end +end +``` + +`match.compile` takes two arguments: the string to compile, and an +optional table of options. An option table may have the following +entries: + + * `dlt`: The link encapsulation, as libpcap would specify it. Defaults + to `"EN10MB"`. + + * `optimize`: Whether to optimize or not. Defaults to `true`. + + * `source`: Whether to print out source code instead of returning a + function. Defaults to `false`. + + * `subst`: A table of substitutions for the program test. For example + if you didn't want to hard-code `1.2.3.4` as the incoming IP, you + could instead write `$incoming_ip` and pass `{incoming_ip='1.2.3.4'}` + as the subst table. Defaults to `false`, indicating no + substitutions. diff --git a/doc/portrange-0-6000.md b/doc/portrange-0-6000.md new file mode 100644 index 0000000000..9e96fb3321 --- /dev/null +++ b/doc/portrange-0-6000.md @@ -0,0 +1,155 @@ +# portrange 0-6000 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 11 +002: A = P[20:1] +003: if (A == 132) goto 6 else goto 4 +004: if (A == 6) goto 6 else goto 5 +005: if (A == 17) goto 6 else goto 26 +006: A = P[54:2] +007: if (A >= 0) goto 8 else goto 9 +008: if (A > 6000) goto 9 else goto 25 +009: A = P[56:2] +010: if (A >= 0) goto 24 else goto 26 +011: if (A == 2048) goto 12 else goto 26 +012: A = P[23:1] +013: if (A == 132) goto 16 else goto 14 +014: if (A == 6) goto 16 else goto 15 +015: if (A == 17) goto 16 else goto 26 +016: A = P[20:2] +017: if (A & 8191 != 0) goto 26 else goto 18 +018: X = (P[14:1] & 0xF) << 2 +019: A = P[X+14:2] +020: if (A >= 0) goto 21 else goto 22 +021: if (A > 6000) goto 22 else goto 25 +022: A = P[X+16:2] +023: if (A >= 0) goto 24 else goto 26 +024: if (A > 6000) goto 26 else goto 25 +025: return 65535 +026: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L10 end + if 21 > length then return false end + A = P[20] + if (A==132) then goto L5 end + if (A==6) then goto L5 end + if not (A==17) then goto L25 end + ::L5:: + if 56 > length then return false end + A = bit.bor(bit.lshift(P[54], 8), P[54+1]) + if not (runtime_u32(A)>=0) then goto L8 end + if not (runtime_u32(A)>6000) then goto L24 end + ::L8:: + if 58 > length then return false end + A = bit.bor(bit.lshift(P[56], 8), P[56+1]) + if (runtime_u32(A)>=0) then goto L23 end + goto L25 + ::L10:: + if not (A==2048) then goto L25 end + if 24 > length then return false end + A = P[23] + if (A==132) then goto L15 end + if (A==6) then goto L15 end + if not (A==17) then goto L25 end + ::L15:: + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L25 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+14)) + if T < 0 or T + 2 > length then return false end + A = bit.bor(bit.lshift(P[T], 8), P[T+1]) + if not (runtime_u32(A)>=0) then goto L21 end + if not (runtime_u32(A)>6000) then goto L24 end + ::L21:: + T = bit.tobit((X+16)) + if T < 0 or T + 2 > length then return false end + A = bit.bor(bit.lshift(P[T], 8), P[T+1]) + if not (runtime_u32(A)>=0) then goto L25 end + ::L23:: + if (runtime_u32(A)>6000) then goto L25 end + ::L24:: + do return true end + ::L25:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local rshift = require("bit").rshift +local bswap = require("bit").bswap +local cast = require("ffi").cast +local lshift = require("bit").lshift +local band = require("bit").band +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + local v2 = P[23] + if v2 == 6 then goto L8 end + do + if v2 == 17 then goto L8 end + if v2 == 132 then goto L8 end + return false + end +::L8:: + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v3 = lshift(band(P[14],15),2) + local v4 = (v3 + 16) + if v4 > length then return false end + if rshift(bswap(cast("uint16_t*", P+(v3 + 14))[0]), 16) <= 6000 then return true end + if (v3 + 18) > length then return false end + return rshift(bswap(cast("uint16_t*", P+v4)[0]), 16) <= 6000 + else + if length < 56 then return false end + if v1 ~= 56710 then return false end + local v5 = P[20] + if v5 == 6 then goto L26 end + do + if v5 ~= 44 then goto L29 end + do + if P[54] == 6 then goto L26 end + goto L29 + end +::L29:: + if v5 == 17 then goto L26 end + if v5 ~= 44 then goto L35 end + do + if P[54] == 17 then goto L26 end + goto L35 + end +::L35:: + if v5 == 132 then goto L26 end + if v5 ~= 44 then return false end + if P[54] == 132 then goto L26 end + return false + end +::L26:: + if rshift(bswap(cast("uint16_t*", P+54)[0]), 16) <= 6000 then return true end + if length < 58 then return false end + return rshift(bswap(cast("uint16_t*", P+56)[0]), 16) <= 6000 + end +end + +``` + diff --git a/doc/proto-47.md b/doc/proto-47.md new file mode 100644 index 0000000000..b60553789b --- /dev/null +++ b/doc/proto-47.md @@ -0,0 +1,75 @@ +# proto 47 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 4 +002: A = P[23:1] +003: if (A == 47) goto 10 else goto 11 +004: if (A == 34525) goto 5 else goto 11 +005: A = P[20:1] +006: if (A == 47) goto 10 else goto 7 +007: if (A == 44) goto 8 else goto 11 +008: A = P[54:1] +009: if (A == 47) goto 10 else goto 11 +010: return 65535 +011: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L3 end + if 24 > length then return false end + A = P[23] + if (A==47) then goto L9 end + goto L10 + ::L3:: + if not (A==34525) then goto L10 end + if 21 > length then return false end + A = P[20] + if (A==47) then goto L9 end + if not (A==44) then goto L10 end + if 55 > length then return false end + A = P[54] + if not (A==47) then goto L10 end + ::L9:: + do return true end + ::L10:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 ~= 8 then goto L7 end + do + if P[23] == 47 then return true end + goto L7 + end +::L7:: + if length < 54 then return false end + if v1 ~= 56710 then return false end + local v2 = P[20] + if v2 == 47 then return true end + if length < 55 then return false end + if v2 ~= 44 then return false end + return P[54] == 47 +end + +``` + diff --git a/doc/proto-sctp.md b/doc/proto-sctp.md new file mode 100644 index 0000000000..e676d2a2db --- /dev/null +++ b/doc/proto-sctp.md @@ -0,0 +1,67 @@ +# proto \sctp + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 4 +002: A = P[23:1] +003: if (A == 132) goto 10 else goto 11 +004: if (A == 34525) goto 5 else goto 11 +005: A = P[20:1] +006: if (A == 132) goto 10 else goto 7 +007: if (A == 44) goto 8 else goto 11 +008: A = P[54:1] +009: if (A == 132) goto 10 else goto 11 +010: return 65535 +011: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return 0 end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L3 end + if 24 > length then return 0 end + A = P[23] + if (A==132) then goto L9 end + goto L10 + ::L3:: + if not (A==34525) then goto L10 end + if 21 > length then return 0 end + A = P[20] + if (A==132) then goto L9 end + if not (A==44) then goto L10 end + if 55 > length then return 0 end + A = P[54] + if not (A==132) then goto L10 end + ::L9:: + do return 65535 end + ::L10:: + do return 0 end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +return function(P,length) + if not (length >= 34) then do return false end end + do + local v1 = ffi.cast("uint16_t*", P+12)[0] + if not (v1 == 8) then do return false end end + do + local v2 = P[23] + do return v2 == 132 end + end + end +end +``` + diff --git a/doc/sctp.md b/doc/sctp.md new file mode 100644 index 0000000000..879c1d3608 --- /dev/null +++ b/doc/sctp.md @@ -0,0 +1,72 @@ +# sctp + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 7 +002: A = P[20:1] +003: if (A == 132) goto 10 else goto 4 +004: if (A == 44) goto 5 else goto 11 +005: A = P[54:1] +006: if (A == 132) goto 10 else goto 11 +007: if (A == 2048) goto 8 else goto 11 +008: A = P[23:1] +009: if (A == 132) goto 10 else goto 11 +010: return 65535 +011: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L6 end + if 21 > length then return false end + A = P[20] + if (A==132) then goto L9 end + if not (A==44) then goto L10 end + if 55 > length then return false end + A = P[54] + if (A==132) then goto L9 end + goto L10 + ::L6:: + if not (A==2048) then goto L10 end + if 24 > length then return false end + A = P[23] + if not (A==132) then goto L10 end + ::L9:: + do return true end + ::L10:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + return P[23] == 132 + end + if length < 54 then return false end + if v1 ~= 56710 then return false end + local v2 = P[20] + if v2 == 132 then return true end + if length < 55 then return false end + if v2 ~= 44 then return false end + return P[54] == 132 +end + +``` + diff --git a/doc/src-host-192.68.1.1-and-less-100.md b/doc/src-host-192.68.1.1-and-less-100.md new file mode 100644 index 0000000000..b9c6b6f247 --- /dev/null +++ b/doc/src-host-192.68.1.1-and-less-100.md @@ -0,0 +1,82 @@ +# src host 192.68.1.1 and less 100 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 2048) goto 2 else goto 4 +002: A = P[26:4] +003: if (A == 3225682177) goto 8 else goto 11 +004: if (A == 2054) goto 6 else goto 5 +005: if (A == 32821) goto 6 else goto 11 +006: A = P[28:4] +007: if (A == 3225682177) goto 8 else goto 11 +008: A = length +009: if (A > 100) goto 11 else goto 10 +010: return 65535 +011: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==2048) then goto L3 end + if 30 > length then return false end + A = bit.bor(bit.lshift(P[26], 24),bit.lshift(P[26+1], 16), bit.lshift(P[26+2], 8), P[26+3]) + if (A==-1069285119) then goto L7 end + goto L10 + ::L3:: + if (A==2054) then goto L5 end + if not (A==32821) then goto L10 end + ::L5:: + if 32 > length then return false end + A = bit.bor(bit.lshift(P[28], 24),bit.lshift(P[28+1], 16), bit.lshift(P[28+2], 8), P[28+3]) + if not (A==-1069285119) then goto L10 end + ::L7:: + A = bit.tobit(length) + if (runtime_u32(A)>100) then goto L10 end + do return true end + ::L10:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + if cast("uint32_t*", P+26)[0] == 16860352 then goto L6 end + goto L7 + else + if length < 42 then return false end + if v1 == 1544 then goto L12 end + do + if v1 == 13696 then goto L12 end + return false + end +::L12:: + if cast("uint32_t*", P+28)[0] == 16860352 then goto L6 end + goto L7 + end +::L6:: + do + return length <= 100 + end +::L7:: + return false +end + +``` + diff --git a/doc/src-net-ffff.ffff.eeee.eeee.0.0.0.0-72.md b/doc/src-net-ffff.ffff.eeee.eeee.0.0.0.0-72.md new file mode 100644 index 0000000000..ee999f7d26 --- /dev/null +++ b/doc/src-net-ffff.ffff.eeee.eeee.0.0.0.0-72.md @@ -0,0 +1,59 @@ +# src net ffff:ffff:eeee:eeee:0:0:0:0/72 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 9 +002: A = P[22:4] +003: if (A == 4294967295) goto 4 else goto 9 +004: A = P[26:4] +005: if (A == 4008636142) goto 6 else goto 9 +006: A = P[30:4] +007: if (A & 4278190080 != 0) goto 9 else goto 8 +008: return 65535 +009: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L8 end + if 26 > length then return false end + A = bit.bor(bit.lshift(P[22], 24),bit.lshift(P[22+1], 16), bit.lshift(P[22+2], 8), P[22+3]) + if not (A==-1) then goto L8 end + if 30 > length then return false end + A = bit.bor(bit.lshift(P[26], 24),bit.lshift(P[26+1], 16), bit.lshift(P[26+2], 8), P[26+3]) + if not (A==-286331154) then goto L8 end + if 34 > length then return false end + A = bit.bor(bit.lshift(P[30], 24),bit.lshift(P[30+1], 16), bit.lshift(P[30+2], 8), P[30+3]) + if not (bit.band(A, -16777216)==0) then goto L8 end + do return true end + ::L8:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 56710 then return false end + if cast("uint32_t*", P+22)[0] ~= 4294967295 then return false end + if cast("uint32_t*", P+26)[0] ~= 4008636142 then return false end + return band(cast("uint32_t*", P+30)[0],255) == 0 +end + +``` + diff --git a/doc/src-net-ffff.ffff.eeee.eeee.1.0.0.0-82.md b/doc/src-net-ffff.ffff.eeee.eeee.1.0.0.0-82.md new file mode 100644 index 0000000000..b125362ddd --- /dev/null +++ b/doc/src-net-ffff.ffff.eeee.eeee.1.0.0.0-82.md @@ -0,0 +1,61 @@ +# src net ffff:ffff:eeee:eeee:1:0:0:0/82 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 10 +002: A = P[22:4] +003: if (A == 4294967295) goto 4 else goto 10 +004: A = P[26:4] +005: if (A == 4008636142) goto 6 else goto 10 +006: A = P[30:4] +007: A &= 4294950912 +008: if (A == 65536) goto 9 else goto 10 +009: return 65535 +010: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L9 end + if 26 > length then return false end + A = bit.bor(bit.lshift(P[22], 24),bit.lshift(P[22+1], 16), bit.lshift(P[22+2], 8), P[22+3]) + if not (A==-1) then goto L9 end + if 30 > length then return false end + A = bit.bor(bit.lshift(P[26], 24),bit.lshift(P[26+1], 16), bit.lshift(P[26+2], 8), P[26+3]) + if not (A==-286331154) then goto L9 end + if 34 > length then return false end + A = bit.bor(bit.lshift(P[30], 24),bit.lshift(P[30+1], 16), bit.lshift(P[30+2], 8), P[30+3]) + A = bit.band(A, -16384) + if not (A==65536) then goto L9 end + do return true end + ::L9:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 56710 then return false end + if cast("uint32_t*", P+22)[0] ~= 4294967295 then return false end + if cast("uint32_t*", P+26)[0] ~= 4008636142 then return false end + return band(cast("uint32_t*", P+30)[0],12648447) == 256 +end + +``` + diff --git a/doc/src-port-80.md b/doc/src-port-80.md new file mode 100644 index 0000000000..4b93564c02 --- /dev/null +++ b/doc/src-port-80.md @@ -0,0 +1,128 @@ +# src port 80 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 8 +002: A = P[20:1] +003: if (A == 132) goto 6 else goto 4 +004: if (A == 6) goto 6 else goto 5 +005: if (A == 17) goto 6 else goto 19 +006: A = P[54:2] +007: if (A == 80) goto 18 else goto 19 +008: if (A == 2048) goto 9 else goto 19 +009: A = P[23:1] +010: if (A == 132) goto 13 else goto 11 +011: if (A == 6) goto 13 else goto 12 +012: if (A == 17) goto 13 else goto 19 +013: A = P[20:2] +014: if (A & 8191 != 0) goto 19 else goto 15 +015: X = (P[14:1] & 0xF) << 2 +016: A = P[X+14:2] +017: if (A == 80) goto 18 else goto 19 +018: return 65535 +019: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L7 end + if 21 > length then return false end + A = P[20] + if (A==132) then goto L5 end + if (A==6) then goto L5 end + if not (A==17) then goto L18 end + ::L5:: + if 56 > length then return false end + A = bit.bor(bit.lshift(P[54], 8), P[54+1]) + if (A==80) then goto L17 end + goto L18 + ::L7:: + if not (A==2048) then goto L18 end + if 24 > length then return false end + A = P[23] + if (A==132) then goto L12 end + if (A==6) then goto L12 end + if not (A==17) then goto L18 end + ::L12:: + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L18 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+14)) + if T < 0 or T + 2 > length then return false end + A = bit.bor(bit.lshift(P[T], 8), P[T+1]) + if not (A==80) then goto L18 end + ::L17:: + do return true end + ::L18:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local lshift = require("bit").lshift +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + local v2 = P[23] + if v2 == 6 then goto L8 end + do + if v2 == 17 then goto L8 end + if v2 == 132 then goto L8 end + return false + end +::L8:: + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v3 = lshift(band(P[14],15),2) + if (v3 + 16) > length then return false end + return cast("uint16_t*", P+(v3 + 14))[0] == 20480 + else + if length < 56 then return false end + if v1 ~= 56710 then return false end + local v4 = P[20] + if v4 == 6 then goto L22 end + do + if v4 ~= 44 then goto L25 end + do + if P[54] == 6 then goto L22 end + goto L25 + end +::L25:: + if v4 == 17 then goto L22 end + if v4 ~= 44 then goto L31 end + do + if P[54] == 17 then goto L22 end + goto L31 + end +::L31:: + if v4 == 132 then goto L22 end + if v4 ~= 44 then return false end + if P[54] == 132 then goto L22 end + return false + end +::L22:: + return cast("uint16_t*", P+54)[0] == 20480 + end +end + +``` + diff --git a/doc/tcp-address.md b/doc/tcp-address.md new file mode 100644 index 0000000000..6f407ad673 --- /dev/null +++ b/doc/tcp-address.md @@ -0,0 +1,35 @@ +# ether[&tcp[0]] = tcp[0] + + +## BPF + +``` +Filter failed to compile: ../src/pf/libpcap.lua:66: pcap_compile failed``` + + +## BPF cross-compiled to Lua + +``` +Filter failed to compile: ../src/pf/libpcap.lua:66: pcap_compile failed +``` + + +## Direct pflang compilation + +``` +local lshift = require("bit").lshift +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 54 then return false end + if cast("uint16_t*", P+12)[0] ~= 8 then return false end + if P[23] ~= 6 then return false end + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v1 = lshift(band(P[14],15),2) + if (v1 + 15) > length then return false end + local v2 = P[(v1 + 14)] + return v2 == v2 +end + +``` + diff --git a/doc/tcp-port-80.md b/doc/tcp-port-80.md new file mode 100644 index 0000000000..d1329b4b03 --- /dev/null +++ b/doc/tcp-port-80.md @@ -0,0 +1,113 @@ +# tcp port 80 + + +## BPF + +``` +000: A = P[12:2] +001: if (A == 34525) goto 2 else goto 8 +002: A = P[20:1] +003: if (A == 6) goto 4 else goto 19 +004: A = P[54:2] +005: if (A == 80) goto 18 else goto 6 +006: A = P[56:2] +007: if (A == 80) goto 18 else goto 19 +008: if (A == 2048) goto 9 else goto 19 +009: A = P[23:1] +010: if (A == 6) goto 11 else goto 19 +011: A = P[20:2] +012: if (A & 8191 != 0) goto 19 else goto 13 +013: X = (P[14:1] & 0xF) << 2 +014: A = P[X+14:2] +015: if (A == 80) goto 18 else goto 16 +016: A = P[X+16:2] +017: if (A == 80) goto 18 else goto 19 +018: return 65535 +019: return 0 +``` + + +## BPF cross-compiled to Lua + +``` +return function (P, length) + local A = 0 + local X = 0 + local T = 0 + if 14 > length then return false end + A = bit.bor(bit.lshift(P[12], 8), P[12+1]) + if not (A==34525) then goto L7 end + if 21 > length then return false end + A = P[20] + if not (A==6) then goto L18 end + if 56 > length then return false end + A = bit.bor(bit.lshift(P[54], 8), P[54+1]) + if (A==80) then goto L17 end + if 58 > length then return false end + A = bit.bor(bit.lshift(P[56], 8), P[56+1]) + if (A==80) then goto L17 end + goto L18 + ::L7:: + if not (A==2048) then goto L18 end + if 24 > length then return false end + A = P[23] + if not (A==6) then goto L18 end + if 22 > length then return false end + A = bit.bor(bit.lshift(P[20], 8), P[20+1]) + if not (bit.band(A, 8191)==0) then goto L18 end + if 14 >= length then return false end + X = bit.lshift(bit.band(P[14], 15), 2) + T = bit.tobit((X+14)) + if T < 0 or T + 2 > length then return false end + A = bit.bor(bit.lshift(P[T], 8), P[T+1]) + if (A==80) then goto L17 end + T = bit.tobit((X+16)) + if T < 0 or T + 2 > length then return false end + A = bit.bor(bit.lshift(P[T], 8), P[T+1]) + if not (A==80) then goto L18 end + ::L17:: + do return true end + ::L18:: + do return false end + error("end of bpf") +end +``` + + +## Direct pflang compilation + +``` +local lshift = require("bit").lshift +local band = require("bit").band +local cast = require("ffi").cast +return function(P,length) + if length < 34 then return false end + local v1 = cast("uint16_t*", P+12)[0] + if v1 == 8 then + if P[23] ~= 6 then return false end + if band(cast("uint16_t*", P+20)[0],65311) ~= 0 then return false end + local v2 = lshift(band(P[14],15),2) + local v3 = (v2 + 16) + if v3 > length then return false end + if cast("uint16_t*", P+(v2 + 14))[0] == 20480 then return true end + if (v2 + 18) > length then return false end + return cast("uint16_t*", P+v3)[0] == 20480 + else + if length < 56 then return false end + if v1 ~= 56710 then return false end + local v4 = P[20] + if v4 == 6 then goto L22 end + do + if v4 ~= 44 then return false end + if P[54] == 6 then goto L22 end + return false + end +::L22:: + if cast("uint16_t*", P+54)[0] == 20480 then return true end + if length < 58 then return false end + return cast("uint16_t*", P+56)[0] == 20480 + end +end + +``` + diff --git a/doc/technical/bpf-asm-explained.md b/doc/technical/bpf-asm-explained.md new file mode 100644 index 0000000000..fda2a367ac --- /dev/null +++ b/doc/technical/bpf-asm-explained.md @@ -0,0 +1,258 @@ +Explanation of BPF asm generated code +------------------------------------- + +ETHER PROTO +----------- + +Implements: ```ether proto protocol```, ```ip6```, ```arp```, ```rarp```, ```atalk```, ```aarp```, ```decnet```, ```sca```, ```lat```, ```mopdl```, ```moprc```, ```iso```, ```stp```, ```ipx```, ```netbeui```. + +Example: + +``` +> ether proto 1540 +``` + +``` +(000) ldh [12] +(001) jeq #0x604 jt 2 jf 3 +(002) ret #65535 +(003) ret #0 + +``` + +``` +> ether proto 100 +``` + +``` +(000) ldh [12] +(001) jgt #0x5dc jt 5 jf 2 +(002) ldb [14] +(003) jeq #0x64 jt 4 jf 5 +(004) ret #65535 +(005) ret #0 +``` + +There are two possible interpretations of an ethernet frame. As an Ethernet II frame or as a 802.3 frame [1]. + +The type field of an ethernet frame can carry either the **EtherType** identifier or a **Length**. EtherType identifiers start from 0x600 (1536). This means that when the protocol identifier is >=1536, the value in the Type/Length field ({'[ether]', 12, 2 }) is interpreted as protocol type, as we have been doing so far. + +When the protocol identifier is <1536 the value in Type/Length field is interpreted as Length. The IEEE 802.3 specification determines that this value cannot be higher than 1500 (maxValidFrame, see [1]). That explains why "ether proto 1500" emits the following BPF asm: (applies to any value <1500) + +``` +(000) ldh 12 jgt #0x5dc jt 5 jf 2 +``` + +In fact there's a gray area between values 1500 and 1536. They cannot be valid 802.3 frames (length is higher than 1500), but they are not Ethernet frames either (ethertypes should be >=1536). The spec doesn't define what to do and leaves it to the implementators (tcpdump considers anything >1500 as an Ethernet frame) [2]. + +Once this is clarified, now there's still the question of what's the meaning of the first byte after the Type/Length field, when the Type/Length field is interpreted as Length. In this case (<1500), the frame is interpreted as a 802.3 frame, or derivatives. The derivatives include LLC (802.2) and SNAP which prefix the data field with an LLC header. The article in Wikipedia about 802.3 [3] defines the IEEE 802.3 standard as: + +"10BASE5 10 Mbit/s (1.25 MB/s) over thick coax. Same as Ethernet II (above) except Type field is replaced by Length, and an 802.2 LLC header follows the 802.3 header. Based on the CSMA/CD Process". + +So the first bytes in the payload field are a 802.2 LLC header. The structure of a LLC data header starts with two eight-bit address fields, called service access points. They are the DSAP (Destination SAP) and SSAP (Source SAP). Possible values for a SAP are: + +| Value | Hex | Description | +| 0 | 0x00 | Null LSAP | +| 2 | 0x02 | Individual LLC Sublayer Mgt | +| 3 | 0x03 | Group LLC Sublayer Mgt | +| 4 | 0x04 | SNA Path Control | +| 6 | 0x06 | Reserved for DoD IP | +| 14 | 0x0e | ProWay-LAN | +| 78 | 0x4e | EIA-RS 511 | +| 94 | 0x5e | ISI IP | +| 142 | 0x8e | ProWay-LAN | +| 170 | 0xaa | SNAP Extension Used | +| 224 | 0xe0 | Novell Netware | +| 254 | 0xfe | OSI protocols ISO CLNS IS 8473[2] | +| 255 | 0xff | Global DSAP (cannot be used for SSAP) | + +In conclusion, the first byte after the Type/Length field, when the ethernet frame is interpreted as 802.3 data frame, is a SAP. At this level (Link-Layer Control) the concept of SAP is similar to an Ethertype value [4]. + +What the command ```ether proto protocol``` does when number <1500 is to check the length of the packet and in case it is lower than 1500 compares the first byte after the length ({'[ether]', 14, 1}) against the protocol number. + +With regard to SNAP protocol, the structure is the same but adds new fields at the end of the LLC header [5]. + +[1] Extended Ethernet Frame Size Support https://tools.ietf.org/html/draft-ietf-isis-ext-eth-01) +[2] EtherType http://en.wikipedia.org/wiki/EtherType +[3] IEE_802.3 http://en.wikipedia.org/wiki/IEEE_802.3 +[4] SAP Numbers http://www.wildpackets.com/resources/compendium/reference/sap_numbers +[5] SNAP Protocol http://en.wikipedia.org/wiki/Subnetwork_Access_Protocol + + +ISO +--- + +Implements: ```iso proto protocol```, ```isis```, ```esis```, ```clnp```. + +Example: + +``` +> clnp +``` + +``` +(000) ldh [12] +(001) jgt #0x5dc jt 7 jf 2 +(002) ldh [14] +(003) jeq #0xfefe jt 4 jf 7 +(004) ldb [17] +(005) jeq #0x81 jt 6 jf 7 +(006) ret #65535 +(007) ret #0 +``` + +The frame contains an encapsulated ISO PDU. This PDU is prepended by a LLC header that identifies the frame encapsulating an ISO PDU. The value 0xFEFE03 is used to identify the frame carries an encapsulated ISO PDU. The first byte of the PDU contains a protocol identifer (CLNP, ISIS, ESIS) [1]. + +The routed PDU if prefixed by a IEEE 802.2 LLC Header. This header has the following format: + +``` ++------+------+------+ +| DSAP | SSAP | Ctrl | ++------+------+------+ +``` + +Each field is 1 octet. The LLC header value 0xFE-FE-03 identifies that a routed ISO PDU follows. This corresponds to the following BPF-asm: + +``` +(002) ldh [14] +(003) jeq #0xfefe jt 4 jf 7 +``` + +(03 is not checked, but should be byte [16]). + +The routed ISO protocol is identified by a one octet NLPID field that is part of Protocol Data. Protocol identifiers are defined in Appendix C of rfc1483.txt. + +``` +0x81 ISO CLNP +0x82 ISO ESIS +0x83 ISO ISIS +``` + +[1] Multiprotocol Encapsulation over ATM Adaptation Layer 5 https://www.ietf.org/rfc/rfc1483.txt. + + +DECNET +------ + +Implements: ```decnet src host```, ```decnet dst host```, ```decnet host host```. + +Example: + +``` +> decnet src host 10.12 +``` + +``` +(000) ldh [12] +(001) jeq #0x6003 jt 2 jf 23 +(002) ldb [16] +(003) and #0x7 +(004) jeq #0x2 jt 5 jf 7 +(005) ldh [19] +(006) jeq #0xc28 jt 22 jf 7 +(007) ldh [16] +(008) and #0xff07 +(009) jeq #0x8102 jt 10 jf 12 +(010) ldh [20] +(011) jeq #0xc28 jt 22 jf 12 +(012) ldb [16] +(013) and #0x7 +(014) jeq #0x6 jt 15 jf 17 +(015) ldh [31] +(016) jeq #0xc28 jt 22 jf 17 +(017) ldh [16] +(018) and #0xff07 +(019) jeq #0x8106 jt 20 jf 23 +(020) ldh [32] +(021) jeq #0xc28 jt 22 jf 23 +(022) ret #65535 +(023) ret #0 +``` + +This code checks DECNET Phase IV addresses encapsulated in an ethernet data frame. + +There are two possible DECNET Phase IV headers: a long header and a short header [1]. For each case, the generated BPF is the same, but the offset changes. The code generated checks both types of headers. + +The first octet of the header is a control field. It has the following format: + +``` +| P | V | IL | RTS | RQR | Format | +``` + + * P: If set, indicates that padding is added to the beginning of the packet. + * Format: Indicates whether the packet is in long (0x6) or short format (0x2). + +Firstly, what the BPF code does is to check whether the packet is in short format. If that's the case, it fetches the source address at 19 and compares it against the address passed as argument (0xc28). + +``` +(002) ldb [16] +(003) and #0x7 +(004) jeq #0x2 jt 5 jf 7 +(005) ldh [19] +(006) jeq #0xc28 jt 22 jf 7 +``` + +In case the packet were not in short format, it could be the case that, in fact, it was but because there was padding, the test resulted in a false negative. The BPF generated code only considers the case when there's 1 byte of padding [2]. When there's padding the padding is put in front of the header and is indicated by having the top bit set in the first byte and the length of the padding indicated by the remainder first byte [2]. So that means that [16] should be equals to 0x81, and [17] is the control byte. The generated code checks again the control byte is in short format. + +``` +(007) ldh 16 and #0xff07 +(009) jeq #0x8102 jt 10 jf 12 +(010) ldh 20 jeq #0xc28 jt 22 jf 12 +``` + +The case for the long header is analog to the explained above but with the only difference that the starting offset is different. + +[1] http://books.google.es/books?id=AIRitf5C-QQC&pg=PA229&lpg=PA229&dq=ethernet+%22decnet+header%22+rfc&source=bl&ots=2FXDeh1A7_&sig=7vFM25hs82g_n0Cn3A6qouRpq24&hl=en&sa=X&ei=xtVCVIL8JNPy7AaVzYGwDg&ved=0CCEQ6AEwAA#v=onepage&q=ethernet%20%22decnet%20header%22%20rfc&f=false +[2] libpcap/gencode.c https://github.com/the-tcpdump-group/libpcap/blob/master/gencode.c#L4501 +[3] http://books.google.es/books?id=AIRitf5C-QQC&pg=PA238&lpg=PA238&dq=decnet+padding&source=bl&ots=2FXDeh3z9W&sig=WJ4adVfPY9my1T-uByHeP5s91NQ&hl=en&sa=X&ei=Qt1CVK7-Cuzg7Qa6q4CICw&ved=0CCEQ6AEwAA#v=onepage&q=decnet%20padding&f=false + +ISIS +---- + +Implements: ```iih```, ```lsp```, ```snp```, ```csnp```, ```psnp```. + +Example: + +``` +> csnp +``` + +``` +(000) ldh [12] +(001) jgt #0x5dc jt 10 jf 2 +(002) ldh [14] +(003) jeq #0xfefe jt 4 jf 10 +(004) ldb [17] +(005) jeq #0x83 jt 6 jf 10 +(006) ldb [21] +(007) jeq #0x1a jt 9 jf 8 +(008) jeq #0x1b jt 9 jf 10 +(009) ret #65535 +(010) ret #0 + +``` + +ISIS is a routing protocol designed to move information efficiently within a computer network, similar to OSPF [1]. ISIS protocols operate at two levels: L1 (intra-area) and L2 (inter-area). ISIS is an ISO protocol [2]. + +The packets used in IS-IS routing protocol fall into three main classes: (i) Hello Packets; (ii) Link State Packets (LSPs); and (iii) Sequence Number Packets (SNPs) [3]. + + i. There are 3 types of Hello packets: L1_IIH, L2_IIH and P2P_IIH. + ii. There are 2 types of LSPs: L1_LSP, L2_LSP. + iii. There are 4 types of sequence number packets: L1_CSNP, L2_CSNP, L1_PSNP, L2_PSNP. + +The following document describes the structure of an ISIS PDU [4]. The type of ISIS protocol starts at offset 5 [4]. + +The BPF asm generated is quite simple. It first checks the packet is an ISO packet, and if that's the case inspects offset 5 ([21]) to check whether the PDU matches the correspondent ID. For instance, valid l1 packets include L1_IIH, PTP_IIH, L1_LSP, L1_CSNP and L1_PSNP. + + * L1: L1_IIH , PTP_IIH, L1_LSP, L1_CSNP, L1_PSNP. + * L2: L2_IIH , PTP_IIH, L2_LSP, L2_CSNP, L2_PSNP. + * IIH: L1_IIH, L2_IIH, P2P_IIH (3). + * LSP: L1_LSP, L2_LSP (2). + * SNP: L1_CSNP, L2_CSNP, L1_PSNP, L2_PSNP (4). + * CSNP: L1_CSNP, L2_CSNP. + * PSNP: L1_PSNP, L2_PSNP. +. +[1] http://en.wikipedia.org/wiki/IS-IS +[2] http://wiki.wireshark.org/IsoProtocolFamily +[3] http://www.ietf.org/rfc/rfc1195.txt +[4] http://www.itcertnotes.com/2012/03/is-is-protocol-data-units-pdus.html diff --git a/env b/env new file mode 100755 index 0000000000..8981b86146 --- /dev/null +++ b/env @@ -0,0 +1,21 @@ +#!/bin/bash + +function prepend { + local var=$1; shift + local sep=$1; shift + local i; for i in "$@"; do + if test -z "${!var}"; then + export "$var=$i" + else + export "$var=$i$sep${!var}" + fi + done +} + +thisdir=$(cd $(dirname $0) && pwd) + +prepend LUA_PATH ';' "${thisdir}/tests/?.lua" +prepend LUA_PATH ';' "${thisdir}/src/?.lua" +prepend PATH ':' "${thisdir}/tools" + +exec "$@" diff --git a/src/Makefile b/src/Makefile new file mode 100644 index 0000000000..a4029b41f7 --- /dev/null +++ b/src/Makefile @@ -0,0 +1,20 @@ +TOP_SRCDIR:=.. +include $(TOP_SRCDIR)/common.mk + +all: + +clean: + +check: + luajit -l pf.types -e 'pf.types.selftest()' + luajit -l pf.utils -e 'pf.utils.selftest()' + luajit -l pf.libpcap -e 'pf.libpcap.selftest()' + luajit -l pf.bpf -e 'pf.bpf.selftest()' + luajit -l pf.parse -e 'pf.parse.selftest()' + luajit -l pf.expand -e 'pf.expand.selftest()' + luajit -l pf.optimize -e 'pf.optimize.selftest()' + luajit -l pf.anf -e 'pf.anf.selftest()' + luajit -l pf.ssa -e 'pf.ssa.selftest()' + luajit -l pf.backend -e 'pf.backend.selftest()' + luajit -l pf.match -e 'pf.match.selftest()' + luajit -l pf -e 'pf.selftest()' diff --git a/src/pf.lua b/src/pf.lua new file mode 100644 index 0000000000..3c0df8e2e1 --- /dev/null +++ b/src/pf.lua @@ -0,0 +1,92 @@ +module("pf",package.seeall) + +local savefile = require("pf.savefile") +local types = require("pf.types") +local libpcap = require("pf.libpcap") +local bpf = require("pf.bpf") +local parse = require('pf.parse') +local expand = require('pf.expand') +local optimize = require('pf.optimize') +local anf = require('pf.anf') +local ssa = require('pf.ssa') +local backend = require('pf.backend') +local utils = require('pf.utils') + +-- TODO: rename the 'libpcap' option to reduce terminology overload +local compile_defaults = { + optimize=true, libpcap=false, bpf=false, source=false +} +function compile_filter(filter_str, opts) + local opts = utils.parse_opts(opts or {}, compile_defaults) + local dlt = opts.dlt or "EN10MB" + if opts.libpcap then + local bytecode = libpcap.compile(filter_str, dlt, opts.optimize) + if opts.source then return bpf.disassemble(bytecode) end + local header = types.pcap_pkthdr(0, 0, 0, 0) + return function(P, len) + header.incl_len = len + header.orig_len = len + return libpcap.offline_filter(bytecode, header, P) ~= 0 + end + elseif opts.bpf then + local bytecode = libpcap.compile(filter_str, dlt, opts.optimize) + if opts.source then return bpf.compile_lua(bytecode) end + return bpf.compile(bytecode) + else -- pflua + local expr = parse.parse(filter_str) + expr = expand.expand(expr, dlt) + if opts.optimize then expr = optimize.optimize(expr) end + expr = anf.convert_anf(expr) + expr = ssa.convert_ssa(expr) + if opts.source then return backend.emit_lua(expr) end + return backend.emit_and_load(expr, filter_str) + end +end + +function selftest () + print("selftest: pf") + + local function test_null(str) + local f1 = compile_filter(str, { libpcap = true }) + local f2 = compile_filter(str, { bpf = true }) + local f3 = compile_filter(str, {}) + assert(f1(str, 0) == false, "null packet should be rejected (libpcap)") + assert(f2(str, 0) == false, "null packet should be rejected (bpf)") + assert(f3(str, 0) == false, "null packet should be rejected (pflua)") + end + test_null("icmp") + test_null("tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)") + + local function assert_count(filter, packets, expected) + function count_matched(pred) + local matched = 0 + for i=1,#packets do + if pred(packets[i].packet, packets[i].len) then + matched = matched + 1 + end + end + return matched + end + + local f1 = compile_filter(filter, { libpcap = true }) + local f2 = compile_filter(filter, { bpf = true }) + local f3 = compile_filter(filter, {}) + local actual + actual = count_matched(f1) + assert(actual == expected, + 'libpcap: got ' .. actual .. ', expected ' .. expected) + actual = count_matched(f2) + assert(actual == expected, + 'bpf: got ' .. actual .. ', expected ' .. expected) + actual = count_matched(f3) + assert(actual == expected, + 'pflua: got ' .. actual .. ', expected ' .. expected) + end + local v4 = savefile.load_packets("../tests/data/v4.pcap") + assert_count('', v4, 43) + assert_count('ip', v4, 43) + assert_count('tcp', v4, 41) + assert_count('tcp port 80', v4, 41) + + print("OK") +end diff --git a/src/pf/anf.lua b/src/pf/anf.lua new file mode 100644 index 0000000000..a36f23c2db --- /dev/null +++ b/src/pf/anf.lua @@ -0,0 +1,296 @@ +module(...,package.seeall) + +local utils = require('pf.utils') + +local set, pp, dup = utils.set, utils.pp, utils.dup + +local relops = set('<', '<=', '=', '!=', '>=', '>') + +local binops = set( + '+', '-', '*', '*64', '/', '&', '|', '^', '<<', '>>' +) +local unops = set('ntohs', 'ntohl', 'uint32', 'int32') + +local simple = set('true', 'false', 'match', 'fail') + +local count = 0 + +local function fresh() + count = count + 1 + return 'var'..count +end + +local function lower_arith(expr, k) + if type(expr) ~= 'table' then return k(expr) end + local op = expr[1] + if unops[op] then + local operand = expr[2] + local function have_operand(operand) + local result = fresh() + return { 'let', result, { op, operand }, k(result) } + end + return lower_arith(operand, have_operand) + elseif binops[op] then + local lhs, rhs = expr[2], expr[3] + local function have_lhs(lhs) + local function have_rhs(rhs) + local result = fresh() + return { 'let', result, { op, lhs, rhs}, k(result) } + end + return lower_arith(rhs, have_rhs) + end + return lower_arith(lhs, have_lhs) + else + assert(op == '[]') + local operand, size = expr[2], expr[3] + local function have_operand(operand) + local result = fresh() + return { 'let', result, { op, operand, size }, k(result) } + end + return lower_arith(operand, have_operand) + end +end + +local function lower_comparison(expr, k) + local op, lhs, rhs = expr[1], expr[2], expr[3] + assert(relops[op]) + local function have_lhs(lhs) + local function have_rhs(rhs) + return k({ op, lhs, rhs }) + end + return lower_arith(rhs, have_rhs) + end + return lower_arith(lhs, have_lhs) +end + +local function lower_bool(expr, k) + local function lower(expr) + local function have_bool(expr) + return expr + end + return lower_bool(expr, have_bool) + end + local op = expr[1] + if op == 'if' then + local test, t, f = expr[2], expr[3], expr[4] + local function have_test(test) + return k({ 'if', test, lower(t), lower(f) }) + end + return lower_bool(test, have_test) + elseif simple[op] then + return k(expr) + elseif op == 'call' then + local out = { 'call', expr[2] } + local function lower_arg(i) + if i > #expr then return k(out) end + local function have_arg(arg) + out[i] = arg + return lower_arg(i + 1) + end + return lower_arith(expr[i], have_arg) + end + return lower_arg(3) + else + return lower_comparison(expr, k) + end +end + +local function lower(expr) + count = 0 + local function have_bool(expr) + return expr + end + return lower_bool(expr, have_bool) +end + +local function cse(expr) + local replacements = {} + local function lookup(expr) + return replacements[expr] or expr + end + local function visit(expr, env) + if type(expr) == 'number' then return expr end + if type(expr) == 'string' then return lookup(expr) end + local op = expr[1] + if op == 'let' then + local var, val, body = expr[2], expr[3], expr[4] + assert(type(val) == 'table') + local arith_op = val[1] + local key, replacement_val + if unops[arith_op] then + local lhs = visit(val[2], env) + key = arith_op..','..lhs + replacement_val = { arith_op, lhs } + elseif binops[arith_op] then + local lhs, rhs = visit(val[2], env), visit(val[3], env) + key = arith_op..','..lhs..','..rhs + replacement_val = { arith_op, lhs, rhs } + else + assert(arith_op == '[]') + local lhs, size = visit(val[2], env), val[3] + key = arith_op..','..lhs..','..size + replacement_val = { arith_op, lhs, size } + end + local cse_var = env[key] + if cse_var then + replacements[var] = cse_var + return visit(body, env) + else + env = dup(env) + env[key] = var + return { 'let', var, replacement_val, visit(body, env) } + end + elseif op == 'if' then + return { 'if', visit(expr[2], env), visit(expr[3], env), visit(expr[4], env) } + elseif simple[op] then + return expr + elseif op == 'call' then + local out = { 'call', expr[2] } + for i=3,#expr do table.insert(out, visit(expr[i], env)) end + return out + else + assert(relops[op]) + return { op, visit(expr[2], env), visit(expr[3], env) } + end + end + return visit(expr, {}) +end + +local function inline_single_use_variables(expr) + local counts, substs = {}, {} + local function count(expr) + if expr == 'len' then return + elseif type(expr) == 'number' then return + elseif type(expr) == 'string' then counts[expr] = counts[expr] + 1 + else + assert(type(expr) == 'table') + local op = expr[1] + if op == 'if' then + count(expr[2]) + count(expr[3]) + count(expr[4]) + elseif op == 'let' then + counts[expr[2]] = 0 + count(expr[3]) + count(expr[4]) + elseif relops[op] then + count(expr[2]) + count(expr[3]) + elseif unops[op] then + count(expr[2]) + elseif binops[op] then + count(expr[2]) + count(expr[3]) + elseif simple[op] then + + elseif op == 'call' then + for i=3,#expr do count(expr[i]) end + else + assert(op == '[]') + count(expr[2]) + end + end + end + local function lookup(expr) + return substs[expr] or expr + end + local function subst(expr) + if type(expr) == 'number' then return expr end + if type(expr) == 'string' then return lookup(expr) end + local op = expr[1] + if op == 'let' then + local var, val, body = expr[2], expr[3], expr[4] + assert(type(val) == 'table') + local arith_op = val[1] + local replacement_val + if unops[arith_op] then + local lhs = subst(val[2]) + replacement_val = { arith_op, lhs } + elseif binops[arith_op] then + local lhs, rhs = subst(val[2]), subst(val[3]) + replacement_val = { arith_op, lhs, rhs } + else + assert(arith_op == '[]') + local lhs, size = subst(val[2]), val[3] + replacement_val = { arith_op, lhs, size } + end + if counts[var] == 1 then + substs[var] = replacement_val + return subst(body) + else + return { 'let', var, replacement_val, subst(body) } + end + elseif op == 'if' then + return { 'if', subst(expr[2]), subst(expr[3]), subst(expr[4]) } + elseif simple[op] then + return expr + elseif op == 'call' then + local out = { 'call', expr[2] } + for i=3,#expr do table.insert(out, subst(expr[i])) end + return out + else + assert(relops[op]) + return { op, subst(expr[2]), subst(expr[3]) } + end + end + count(expr) + return subst(expr) +end + +local function renumber(expr) + local count, substs = 0, {} + local function intern(var) + count = count + 1 + local fresh = 'v'..count + substs[var] = fresh + return fresh + end + local function lookup(var) + if var == 'len' then return var end + return assert(substs[var], "unbound variable: "..var) + end + local function visit(expr) + if type(expr) == 'number' then return expr end + if type(expr) == 'string' then return lookup(expr) end + local op = expr[1] + if op == 'let' then + local var, val, body = expr[2], expr[3], expr[4] + local fresh = intern(var) + return { 'let', fresh, visit(val), visit(body) } + elseif op == 'if' then + return { 'if', visit(expr[2]), visit(expr[3]), visit(expr[4]) } + elseif simple[op] then + return expr + elseif op == 'call' then + local out = { 'call', expr[2] } + for i=3,#expr do table.insert(out, visit(expr[i])) end + return out + elseif relops[op] then + return { op, visit(expr[2]), visit(expr[3]) } + elseif unops[op] then + return { op, visit(expr[2]) } + elseif binops[op] then + return { op, visit(expr[2]), visit(expr[3]) } + else + assert(op == '[]') + return { op, visit(expr[2]), expr[3] } + end + end + return visit(expr) +end + +function convert_anf(expr) + return renumber(inline_single_use_variables(cse(lower(expr)))) +end + +function selftest() + local parse = require('pf.parse').parse + local expand = require('pf.expand').expand + local optimize = require('pf.optimize').optimize + local function test(expr) + return convert_anf(optimize(expand(parse(expr), "EN10MB"))) + end + print("selftest: pf.anf") + test("tcp port 80") + print("OK") +end diff --git a/src/pf/backend.lua b/src/pf/backend.lua new file mode 100644 index 0000000000..3dbfed3f4b --- /dev/null +++ b/src/pf/backend.lua @@ -0,0 +1,398 @@ +module(...,package.seeall) + +local utils = require('pf.utils') + +local verbose = os.getenv("PF_VERBOSE"); + +local set, pp = utils.set, utils.pp + +local relop_map = { + ['<']='<', ['<=']='<=', ['=']='==', ['!=']='~=', ['>=']='>=', ['>']='>' +} + +local relop_inversions = { + ['<']='>=', ['<=']='>', ['=']='!=', ['!=']='=', ['>=']='<', ['>']='<=' +} + +local simple_results = set('true', 'false', 'call') + +local function invert_bool(expr) + if expr[1] == 'true' then return { 'false' } end + if expr[1] == 'false' then return { 'true' } end + assert(relop_inversions[expr[1]]) + return { relop_inversions[expr[1]], expr[2], expr[3] } +end + +local function is_simple_expr(expr) + -- Simple := return true | return false | return call | goto Label + if expr[1] == 'return' then return simple_results[expr[2][1]] end + return expr[1] == 'goto' +end + +-- Lua := Do | Return | Goto | If | Bind | Label +-- Do := 'do' Lua+ +-- Return := 'return' Bool|Call +-- Goto := 'goto' Label +-- If := 'if' Bool Lua Lua? +-- Bind := 'bind' Name Expr +-- Label := 'label' Lua +local function residualize_lua(program) + -- write blocks, scope is dominator tree + local function nest(block, result, knext) + for _, binding in ipairs(block.bindings) do + table.insert(result, { 'bind', binding.name, binding.value }) + end + local control = block.control + if control[1] == 'goto' then + local succ = program.blocks[control[2]] + if succ.idom == block.label then + nest(succ, result) + else + table.insert(result, control) + end + elseif control[1] == 'return' then + table.insert(result, control) + else + assert(control[1] == 'if') + local test, t_label, f_label = control[2], control[3], control[4] + local t_block, f_block = program.blocks[t_label], program.blocks[f_label] + local expr = { 'if', test, { 'do' }, { 'do' } } + -- First, add the test. + table.insert(result, expr) + -- Then fill in the nested then and else arms, if they have no + -- other predecessors. + if #t_block.preds == 1 then + assert(t_block.idom == block.label) + nest(t_block, expr[3]) + else + table.insert(expr[3], { 'goto', t_label }) + end + if #f_block.preds == 1 then + assert(f_block.idom == block.label) + nest(f_block, expr[4]) + else + table.insert(expr[4], { 'goto', f_label }) + end + -- Finally add immediately dominated blocks, with labels. We + -- only have to do this in "if" blocks because "return" blocks + -- have no successors, and "goto" blocks do not immediately + -- dominate blocks that are not their successors. + for _,label in ipairs(block.doms) do + local dom_block = program.blocks[label] + if #dom_block.preds ~= 1 then + local wrap = { 'label', label, { 'do' } } + table.insert(result, wrap) + nest(dom_block, wrap[3]) + end + end + end + end + local result = { 'do' } + nest(program.blocks[program.start], result, nil) + return result +end + +-- Lua := Do | Return | Goto | If | Bind | Label +-- Do := 'do' Lua+ +-- Return := 'return' Bool|Call +-- Goto := 'goto' Label +-- If := 'if' Bool Lua Lua? +-- Bind := 'bind' Name Expr +-- Label := 'label' Lua +local function cleanup(expr, is_last) + local function splice_tail(result, expr) + if expr[1] == 'do' then + -- Splice a tail "do" into the parent do. + for j=2,#expr do + if j==#expr then + splice_tail(result, expr[j]) + else + table.insert(result, expr[j]) + end + end + return + elseif expr[1] == 'if' then + if expr[3][1] == 'return' or expr[3][1] == 'goto' then + -- Splice the consequent of a tail "if" into the parent do. + table.insert(result, { 'if', expr[2], expr[3] }) + if expr[4] then splice_tail(result, expr[4]) end + return + end + elseif expr[1] == 'label' then + -- Likewise, try to splice the body of a tail labelled + -- statement. + local tail = { 'do' } + splice_tail(tail, expr[3]) + if #tail > 2 then + table.insert(result, { 'label', expr[2], tail[2] }) + for i=3,#tail do table.insert(result, tail[i]) end + return + end + end + table.insert(result, expr) + end + local op = expr[1] + if op == 'do' then + if #expr == 2 then return cleanup(expr[2], is_last) end + local result = { 'do' } + for i=2,#expr do + local subexpr = cleanup(expr[i], i==#expr) + if i==#expr then + splice_tail(result, subexpr) + else + table.insert(result, subexpr) + end + end + return result + elseif op == 'return' then + return expr + elseif op == 'goto' then + return expr + elseif op == 'if' then + local test, t, f = expr[2], cleanup(expr[3], true), cleanup(expr[4], true) + if not is_simple_expr(t) and is_simple_expr(f) then + test, t, f = invert_bool(test), f, t + end + if is_simple_expr(t) and is_last then + local result = { 'do', { 'if', test, t } } + splice_tail(result, f) + return result + else + return { 'if', test, t, f } + end + elseif op == 'bind' then + return expr + else + assert (op == 'label') + return { 'label', expr[2], cleanup(expr[3], is_last) } + end +end + +local function filter_builder(...) + -- Reserve first part for libraries. + local parts = {'', 'return function('} + local nparts = 2 + local indent = '' + local libraries = {} + local builder = {} + function builder.write(str) + nparts = nparts + 1 + parts[nparts] = str + end + function builder.writeln(str) + builder.write(indent .. str .. '\n') + end + function builder.bind(var, val) + builder.writeln('local '..var..' = '..val) + end + function builder.push() + indent = indent .. ' ' + end + function builder.else_() + builder.write(indent:sub(4) .. 'else\n') + end + function builder.pop() + indent = indent:sub(4) + builder.writeln('end') + end + function builder.jump(label) + builder.writeln('goto '..label) + end + function builder.writelabel(label) + builder.write('::'..label..'::\n') + end + function builder.c(str) + local lib, func = str:match('([a-z]+).([a-z]+)') + if libraries[str] then return func end + libraries[str] = 'local '..func..' = require("'..lib..'").'..func + return func + end + function builder.header() + for _,library in pairs(libraries) do + parts[1] = library.."\n"..parts[1] + end + end + function builder.finish() + builder.pop() + builder.header() + local written = table.concat(parts) + if verbose then print(written) end + return written + end + local needs_comma = false + for _, v in ipairs({...}) do + if needs_comma then builder.write(',') end + builder.write(v) + needs_comma = true + end + builder.write(')\n') + builder.push() + return builder +end + +local function read_buffer_word_by_type(builder, buffer, offset, size) + if size == 1 then + return buffer..'['..offset..']' + elseif size == 2 then + return builder.c('ffi.cast')..'("uint16_t*", '..buffer..'+'..offset..')[0]' + elseif size == 4 then + return (builder.c('ffi.cast')..'("uint32_t*", '..buffer..'+'..offset..')[0]') + else + error("bad [] size: "..size) + end +end + +local function serialize(builder, stmt) + local function serialize_value(expr) + if expr == 'len' then return 'length' end + if type(expr) == 'number' then return expr end + if type(expr) == 'string' then return expr end + assert(type(expr) == 'table', 'unexpected type '..type(expr)) + local op, lhs = expr[1], serialize_value(expr[2]) + if op == 'ntohs' then return builder.c('bit.rshift')..'('..builder.c('bit.bswap')..'('..lhs..'), 16)' + elseif op == 'ntohl' then return builder.c('bit.bswap')..'('..lhs..')' + elseif op == 'int32' then return builder.c('bit.tobit')..'('..lhs..')' + elseif op == 'uint32' then return '('..lhs..' % '.. 2^32 ..')' + end + local rhs = serialize_value(expr[3]) + if op == '[]' then + return read_buffer_word_by_type(builder, 'P', lhs, rhs) + elseif op == '+' then return '('..lhs..' + '..rhs..')' + elseif op == '-' then return '('..lhs..' - '..rhs..')' + elseif op == '*' then return '('..lhs..' * '..rhs..')' + elseif op == '*64' then + return 'tonumber(('..lhs..' * 1LL * '..rhs..') % '.. 2^32 ..')' + elseif op == '/' then return builder.c('math.floor')..'('..lhs..' / '..rhs..')' + elseif op == '&' then return builder.c('bit.band')..'('..lhs..','..rhs..')' + elseif op == '^' then return builder.c('bit.bxor')..'('..lhs..','..rhs..')' + elseif op == '|' then return builder.c('bit.bor')..'('..lhs..','..rhs..')' + elseif op == '<<' then return builder.c('bit.lshift')..'('..lhs..','..rhs..')' + elseif op == '>>' then return builder.c('bit.rshift')..'('..lhs..','..rhs..')' + else error('unexpected op', op) end + end + + local function serialize_bool(expr) + local op = expr[1] + if op == 'true' then + return 'true' + elseif op == 'false' then + return 'false' + elseif relop_map[op] then + -- An arithmetic relop. + local op = relop_map[op] + local lhs, rhs = serialize_value(expr[2]), serialize_value(expr[3]) + return lhs..' '..op..' '..rhs + else + error('unhandled primitive'..op) + end + end + + local function serialize_call(expr) + local args = { 'P', 'len' } + for i=3,#expr do table.insert(args, serialize_value(expr[i])) end + return 'self.'..expr[2]..'('..table.concat(args, ', ')..')' + end + + local serialize_statement + + local function serialize_sequence(stmts) + if stmts[1] == 'do' then + for i=2,#stmts do serialize_statement(stmts[i], i==#stmts) end + else + serialize_statement(stmts, true) + end + end + + function serialize_statement(stmt, is_last) + local op = stmt[1] + if op == 'do' then + builder.writeln('do') + builder.push() + serialize_sequence(stmt) + builder.pop() + elseif op == 'return' then + if not is_last then + return serialize_statement({ 'do', stmt }, false) + end + if stmt[2][1] == 'call' then + builder.writeln('return '..serialize_call(stmt[2])) + else + builder.writeln('return '..serialize_bool(stmt[2])) + end + elseif op == 'goto' then + builder.jump(stmt[2]) + elseif op == 'if' then + local test, t, f = stmt[2], stmt[3], stmt[4] + local test_str = 'if '..serialize_bool(test)..' then' + if is_simple_expr(t) then + if t[1] == 'return' then + local result + if t[2][1] == 'call' then result = serialize_call(t[2]) + else result = serialize_bool(t[2]) end + builder.writeln(test_str..' return '..result..' end') + else + assert(t[1] == 'goto') + builder.writeln(test_str..' goto '..t[2]..' end') + end + if f then serialize_statement(f, is_last) end + else + builder.writeln(test_str) + builder.push() + serialize_sequence(t) + if f then + builder.else_() + serialize_sequence(f) + end + builder.pop() + end + elseif op == 'bind' then + builder.bind(stmt[2], serialize_value(stmt[3])) + else + assert(op == 'label') + builder.writelabel(stmt[2]) + serialize_statement(stmt[3], is_last) + end + end + serialize_sequence(stmt) +end + +function emit_lua(ssa) + local builder = filter_builder('P', 'length') + serialize(builder, cleanup(residualize_lua(ssa), true)) + local str = builder.finish() + if verbose then pp(str) end + return str +end + +function emit_match_lua(ssa) + local builder = filter_builder('self', 'P', 'length') + serialize(builder, cleanup(residualize_lua(ssa), true)) + local str = builder.finish() + if verbose then pp(str) end + return str +end + +function emit_and_load(ssa, name) + return assert(loadstring(emit_lua(ssa), name))() +end + +function emit_and_load_match(ssa, name) + return assert(loadstring(emit_match_lua(ssa), name))() +end + +function selftest() + print("selftest: pf.backend") + local parse = require('pf.parse').parse + local expand = require('pf.expand').expand + local optimize = require('pf.optimize').optimize + local convert_anf = require('pf.anf').convert_anf + local convert_ssa = require('pf.ssa').convert_ssa + + local function test(expr) + local ast = optimize(expand(parse(expr), "EN10MB")) + return emit_and_load(convert_ssa(convert_anf(ast))) + end + + test("tcp port 80 or udp port 34") + print("OK") +end diff --git a/src/pf/bpf.lua b/src/pf/bpf.lua new file mode 100644 index 0000000000..6b3b06c2e1 --- /dev/null +++ b/src/pf/bpf.lua @@ -0,0 +1,462 @@ +module(...,package.seeall) + +local ffi = require("ffi") +local bit = require("bit") +local band = bit.band + +local verbose = os.getenv("PF_VERBOSE"); + +local function BPF_CLASS(code) return band(code, 0x07) end +local BPF_LD = 0x00 +local BPF_LDX = 0x01 +local BPF_ST = 0x02 +local BPF_STX = 0x03 +local BPF_ALU = 0x04 +local BPF_JMP = 0x05 +local BPF_RET = 0x06 +local BPF_MISC = 0x07 + +local function BPF_SIZE(code) return band(code, 0x18) end +local BPF_W = 0x00 +local BPF_H = 0x08 +local BPF_B = 0x10 + +local function BPF_MODE(code) return band(code, 0xe0) end +local BPF_IMM = 0x00 +local BPF_ABS = 0x20 +local BPF_IND = 0x40 +local BPF_MEM = 0x60 +local BPF_LEN = 0x80 +local BPF_MSH = 0xa0 + +local function BPF_OP(code) return band(code, 0xf0) end +local BPF_ADD = 0x00 +local BPF_SUB = 0x10 +local BPF_MUL = 0x20 +local BPF_DIV = 0x30 +local BPF_OR = 0x40 +local BPF_AND = 0x50 +local BPF_LSH = 0x60 +local BPF_RSH = 0x70 +local BPF_NEG = 0x80 +local BPF_JA = 0x00 +local BPF_JEQ = 0x10 +local BPF_JGT = 0x20 +local BPF_JGE = 0x30 +local BPF_JSET = 0x40 + +local function BPF_SRC(code) return band(code, 0x08) end +local BPF_K = 0x00 +local BPF_X = 0x08 + +local function BPF_RVAL(code) return band(code, 0x18) end +local BPF_A = 0x10 + +local function BPF_MISCOP(code) return band(code, 0xf8) end +local BPF_TAX = 0x00 +local BPF_TXA = 0x80 + +local BPF_MEMWORDS = 16 + +local MAX_UINT32 = 0xffffffff +local MAX_UINT32_PLUS_1 = MAX_UINT32 + 1 + +local function runtime_u32(s32) + if (s32 < 0) then return s32 + MAX_UINT32_PLUS_1 end + return s32 +end + +local function runtime_add(a, b) + return bit.tobit((runtime_u32(a) + runtime_u32(b)) % MAX_UINT32_PLUS_1) +end + +local function runtime_sub(a, b) + return bit.tobit((runtime_u32(a) - runtime_u32(b)) % MAX_UINT32_PLUS_1) +end + +local function runtime_mul(a, b) + -- FIXME: This can overflow. We need a math.imul. + return bit.tobit(runtime_u32(a) * runtime_u32(b)) +end + +local function runtime_div(a, b) + -- The code generator already asserted b is a non-zero constant. + return bit.tobit(math.floor(runtime_u32(a) / runtime_u32(b))) +end + +local env = { + bit = require('bit'), + runtime_u32 = runtime_u32, + runtime_add = runtime_add, + runtime_sub = runtime_sub, + runtime_mul = runtime_mul, + runtime_div = runtime_div, +} + +local function is_power_of_2(k) + if k == 0 then return false end + if bit.band(k, runtime_u32(k) - 1) ~= 0 then return false end + for shift = 0, 31 do + if bit.lshift(1, shift) == k then return shift end + end +end + +function compile_lua(bpf) + local head = ''; + local body = ''; + local function write_head(code) head = head .. ' ' .. code .. '\n' end + local function write_body(code) body = body .. ' ' .. code .. '\n' end + local write = write_body + + local jump_targets = {} + + local function bin(op, a, b) return '(' .. a .. op .. b .. ')' end + local function call(proc, args) return proc .. '(' .. args .. ')' end + local function comma(a1, a2) return a1 .. ', ' .. a2 end + local function s32(a) return call('bit.tobit', a) end + local function u32(a) + if (tonumber(a)) then return runtime_u32(a) end + return call('runtime_u32', a) + end + local function add(a, b) + if type(b) == 'number' then + if b == 0 then return a end + if b > 0 then return s32(bin('+', a, b)) end + end + return call('runtime_add', comma(a, b)) + end + local function sub(a, b) return call('runtime_sub', comma(a, b)) end + local function mul(a, b) return call('runtime_mul', comma(a, b)) end + local function div(a, b) return call('runtime_div', comma(a, b)) end + local function bit(op, a, b) return call('bit.' .. op, comma(a, b)) end + local function bor(a, b) return bit('bor', a, b) end + local function band(a, b) return bit('band', a, b) end + local function lsh(a, b) return bit('lshift', a, b) end + local function rsh(a, b) return bit('rshift', a, b) end + local function rol(a, b) return bit('rol', a, b) end + local function neg(a) return s32('-' .. a) end -- FIXME: Is this right? + local function ee(a, b) return bin('==', a, b) end + local function ge(a, b) return bin('>=', a, b) end + local function gt(a, b) return bin('>', a, b) end + local function assign(lhs, rhs) return lhs .. ' = ' .. rhs end + local function label(i) return '::L' .. i .. '::' end + local function jump(i) jump_targets[i] = true; return 'goto L' .. i end + local function cond(test, kt, kf, fallthrough) + if fallthrough == kf then + return 'if ' .. test .. ' then ' .. jump(kt) .. ' end' + elseif fallthrough == kt then + return cond('not '..test, kf, kt, fallthrough) + else + return cond(test, kt, kf, kf) .. '\n ' .. jump(kf) + end + end + + local state = {} + local function declare(name, init) + if not state[name] then + write_head(assign('local ' .. name, init or '0')) + state[name] = true + end + return name + end + + local function A() return declare('A') end -- accumulator + local function X() return declare('X') end -- index + local function M(k) -- scratch + if (k >= BPF_MEMWORDS or k < 0) then error("bad k" .. k) end + return declare('M'..k) + end + + local function size_to_accessor(size) + if size == BPF_W then return 's32' + elseif size == BPF_H then return 'u16' + elseif size == BPF_B then return 'u8' + else error('bad size ' .. size) + end + end + + local function read_buffer_word_by_type(accessor, buffer, offset) + if (accessor == 'u8') then + return buffer..'['..offset..']' + elseif (accessor == 'u16') then + return 'bit.bor(bit.lshift('..buffer..'['..offset..'], 8), '.. + buffer..'['..offset..'+1])' + elseif (accessor == 's32') then + return 'bit.bor(bit.lshift('..buffer..'['..offset..'], 24),'.. + 'bit.lshift('..buffer..'['..offset..'+1], 16), bit.lshift('.. + buffer..'['..offset..'+2], 8), '..buffer..'['..offset..'+3])' + end + end + + local function P_ref(size, k) + return read_buffer_word_by_type(size_to_accessor(size), 'P', k) + end + + local function ld(size, mode, k) + local rhs, bytes = 0 + if size == BPF_W then bytes = 4 + elseif size == BPF_H then bytes = 2 + elseif size == BPF_B then bytes = 1 + else error('bad size ' .. size) + end + if mode == BPF_ABS then + assert(k >= 0, "packet size >= 2G???") + write('if ' .. k + bytes .. ' > length then return false end') + rhs = P_ref(size, k) + elseif mode == BPF_IND then + write(assign(declare('T'), add(X(), k))) + -- Assuming packet can't be 2GB in length + write('if T < 0 or T + ' .. bytes .. ' > length then return false end') + rhs = P_ref(size, 'T') + elseif mode == BPF_LEN then rhs = 'bit.tobit(length)' + elseif mode == BPF_IMM then rhs = k + elseif mode == BPF_MEM then rhs = M(k) + else error('bad mode ' .. mode) + end + write(assign(A(), rhs)) + end + + local function ldx(size, mode, k) + local rhs + if mode == BPF_LEN then rhs = 'bit.tobit(length)' + elseif mode == BPF_IMM then rhs = k + elseif mode == BPF_MEM then rhs = M(k) + elseif mode == BPF_MSH then + assert(k >= 0, "packet size >= 2G???") + write('if ' .. k .. ' >= length then return false end') + rhs = lsh(band(P_ref(BPF_B, k), 0xf), 2) + else + error('bad mode ' .. mode) + end + write(assign(X(), rhs)) + end + + local function st(k) + write(assign(M(k), A())) + end + + local function stx(k) + write(assign(M(k), X())) + end + + local function alu(op, src, k) + local b + if src == BPF_K then b = k + elseif src == BPF_X then b = X() + else error('bad src ' .. src) + end + + local rhs + if op == BPF_ADD then rhs = add(A(), b) + elseif op == BPF_SUB then rhs = sub(A(), b) + elseif op == BPF_MUL then + local bits = is_power_of_2(b) + if bits then rhs = rol(A(), bits) else rhs = mul(A(), b) end + elseif op == BPF_DIV then + assert(src == BPF_K, "division by non-constant value is unsupported") + assert(k ~= 0, "program contains division by constant zero") + local bits = is_power_of_2(k) + if bits then rhs = rsh(A(), bits) else rhs = div(A(), k) end + elseif op == BPF_OR then rhs = bor(A(), b) + elseif op == BPF_AND then rhs = band(A(), b) + elseif op == BPF_LSH then rhs = lsh(A(), b) + elseif op == BPF_RSH then rhs = rsh(A(), b) + elseif op == BPF_NEG then rhs = neg(A()) + else error('bad op ' .. op) + end + write(assign(A(), rhs)) + end + + local function jmp(i, op, src, k, jt, jf) + if op == BPF_JA then + write(jump(i + runtime_u32(k))) + return + end + + local rhs + if src == BPF_K then rhs = k + elseif src == BPF_X then rhs = X() + else error('bad src ' .. src) + end + + jt = jt + i + jf = jf + i + + if op == BPF_JEQ then + write(cond(ee(A(), rhs), jt, jf, i)) -- No need for u32(). + elseif op == BPF_JGT then + write(cond(gt(u32(A()), u32(rhs)), jt, jf, i)) + elseif op == BPF_JGE then + write(cond(ge(u32(A()), u32(rhs)), jt, jf, i)) + elseif op == BPF_JSET then + write(cond(ee(band(A(), rhs), 0), jf, jt, i)) + else + error('bad op ' .. op) + end + end + + local function ret(src, k) + local rhs + if src == BPF_K then rhs = k + elseif src == BPF_A then rhs = A() + else error('bad src ' .. src) + end + local result = u32(rhs) ~= 0 and "true" or "false" + write('do return '..result..' end') + end + + local function misc(op) + if op == BPF_TAX then + write(assign(X(), A())) + elseif op == BPF_TXA then + write(assign(A(), X())) + else error('bad op ' .. op) + end + end + + if verbose then print(disassemble(bpf)) end + for i=0, #bpf-1 do + -- for debugging: write('print('..i..')') + local inst = bpf[i] + local code = inst.code + local class = BPF_CLASS(code) + if class == BPF_LD then ld(BPF_SIZE(code), BPF_MODE(code), inst.k) + elseif class == BPF_LDX then ldx(BPF_SIZE(code), BPF_MODE(code), inst.k) + elseif class == BPF_ST then st(inst.k) + elseif class == BPF_STX then stx(inst.k) + elseif class == BPF_ALU then alu(BPF_OP(code), BPF_SRC(code), inst.k) + elseif class == BPF_JMP then jmp(i, BPF_OP(code), BPF_SRC(code), inst.k, + inst.jt, inst.jf) + elseif class == BPF_RET then ret(BPF_SRC(code), inst.k) + elseif class == BPF_MISC then misc(BPF_MISCOP(code)) + else error('bad class ' .. class) + end + if jump_targets[i] then write(label(i)) end + end + local ret = ('return function (P, length)\n' .. + head .. body .. + ' error("end of bpf")\n' .. + 'end') + if verbose then print(ret) end + return ret +end + +function disassemble(bpf) + local asm = ''; + local function write(code, ...) asm = asm .. code:format(...) end + local function writeln(code, ...) write(code..'\n', ...) end + + local function ld(size, mode, k) + local bytes = assert(({ [BPF_W]=4, [BPF_H]=2, [BPF_B]=1 })[size]) + if mode == BPF_ABS then writeln('A = P[%u:%u]', k, bytes) + elseif mode == BPF_IND then writeln('A = P[X+%u:%u]', k, bytes) + elseif mode == BPF_IMM then writeln('A = %u', k) + elseif mode == BPF_LEN then writeln('A = length') + elseif mode == BPF_MEM then writeln('A = M[%u]', k) + else error('bad mode ' .. mode) end + end + + local function ldx(size, mode, k) + if mode == BPF_IMM then writeln('X = %u', k) + elseif mode == BPF_LEN then writeln('X = length') + elseif mode == BPF_MEM then writeln('X = M[%u]', k) + elseif mode == BPF_MSH then writeln('X = (P[%u:1] & 0xF) << 2', k) + else error('bad mode ' .. mode) end + end + + local function st(k) writeln('M(%u) = A', k) end + + local function stx(k) writeln('M(%u) = X', k) end + + local function alu(op, src, k) + local b + if src == BPF_K then b = k + elseif src == BPF_X then b = 'X' + else error('bad src ' .. src) end + + if op == BPF_ADD then writeln('A += %s', b) + elseif op == BPF_SUB then writeln('A -= %s', b) + elseif op == BPF_MUL then writeln('A *= %s', b) + elseif op == BPF_DIV then writeln('A /= %s', b) + elseif op == BPF_OR then writeln('A |= %s', b) + elseif op == BPF_AND then writeln('A &= %s', b) + elseif op == BPF_LSH then writeln('A <<= %s', b) + elseif op == BPF_RSH then writeln('A >>= %s', b) + elseif op == BPF_NEG then writeln('A = -A') + else error('bad op ' .. op) end + end + + local function jmp(i, op, src, k, jt, jf) + if op == BPF_JA then writeln('goto %u', k); return end + + local rhs + if src == BPF_K then rhs = k + elseif src == BPF_X then rhs = 'X' + else error('bad src ' .. src) end + + jt = jt + i + 1 + jf = jf + i + 1 + + local function cond(op, lhs, rhs) + writeln('if (%s %s %s) goto %u else goto %u', lhs, op, rhs, jt, jf) + end + + if op == BPF_JEQ then cond('==', 'A', rhs) + elseif op == BPF_JGT then cond('>', 'A', rhs) + elseif op == BPF_JGE then cond('>=', 'A', rhs) + elseif op == BPF_JSET then cond('!=', 'A & '..rhs, 0) + else error('bad op ' .. op) end + end + + local function ret(src, k) + if src == BPF_K then writeln('return %u', k) + elseif src == BPF_A then writeln('return A') + else error('bad src ' .. src) end + end + + local function misc(op) + if op == BPF_TAX then writeln('X = A') + elseif op == BPF_TXA then writeln('A = X') + else error('bad op ' .. op) end + end + + for i=0, #bpf-1 do + local inst = bpf[i] + local code = inst.code + local class = BPF_CLASS(code) + local k = runtime_u32(inst.k) + write(string.format('%03d: ', i)) + if class == BPF_LD then ld(BPF_SIZE(code), BPF_MODE(code), k) + elseif class == BPF_LDX then ldx(BPF_SIZE(code), BPF_MODE(code), k) + elseif class == BPF_ST then st(k) + elseif class == BPF_STX then stx(k) + elseif class == BPF_ALU then alu(BPF_OP(code), BPF_SRC(code), k) + elseif class == BPF_JMP then jmp(i, BPF_OP(code), BPF_SRC(code), k, + inst.jt, inst.jf) + elseif class == BPF_RET then ret(BPF_SRC(code), k) + elseif class == BPF_MISC then misc(BPF_MISCOP(code)) + else error('bad class ' .. class) end + end + return asm +end + +function compile(bpf) + local func = assert(loadstring(compile_lua(bpf))) + setfenv(func, env) + return func() +end + +function dump(bpf) + io.write(#bpf .. ':\n') + for i = 0, #bpf-1 do + io.write(string.format(' {0x%x, %u, %u, %d}\n', + bpf[i].code, bpf[i].jt, bpf[i].jf, bpf[i].k)) + end + io.write("\n") +end + +function selftest() + print("selftest: pf.bpf") + -- FIXME: Not sure how to test without pcap compilation. + print("OK") +end diff --git a/src/pf/constants.lua b/src/pf/constants.lua new file mode 100644 index 0000000000..e9bcbe8b20 --- /dev/null +++ b/src/pf/constants.lua @@ -0,0 +1,386 @@ +-- Network services, Internet style +-- See /etc/services + +module(...,package.seeall) + +services = { + ["tcpmux"] = 1, + ["echo"] = 7, + ["discard"] = 9, + ["systat"] = 11, + ["daytime"] = 13, + ["netstat"] = 15, + ["qotd"] = 17, + ["msp"] = 18, + ["chargen"] = 19, + ["ftp-data"] = 20, + ["ftp"] = 21, + ["fsp"] = 21, + ["ssh"] = 22, + ["telnet"] = 23, + ["smtp"] = 25, + ["time"] = 37, + ["rlp"] = 39, + ["nameserver"] = 42, + ["whois"] = 43, + ["tacacs"] = 49, + ["re-mail-ck"] = 50, + ["domain"] = 53, + ["mtp"] = 57, + ["tacacs-ds"] = 65, + ["bootps"] = 67, + ["bootpc"] = 68, + ["tftp"] = 69, + ["gopher"] = 70, + ["rje"] = 77, + ["finger"] = 79, + ["http"] = 80, + ["link"] = 87, + ["kerberos"] = 88, + ["supdup"] = 95, + ["hostnames"] = 101, + ["iso-tsap"] = 102, + ["acr-nema"] = 104, + ["csnet-ns"] = 105, + ["rtelnet"] = 107, + ["pop2"] = 109, + ["pop3"] = 110, + ["sunrpc"] = 111, + ["auth"] = 113, + ["sftp"] = 115, + ["uucp-path"] = 117, + ["nntp"] = 119, + ["ntp"] = 123, + ["pwdgen"] = 129, + ["loc-srv"] = 135, + ["netbios-ns"] = 137, + ["netbios-dgm"] = 138, + ["netbios-ssn"] = 139, + ["imap2"] = 143, + ["snmp"] = 161, + ["snmp-trap"] = 162, + ["cmip-man"] = 163, + ["cmip-agent"] = 164, + ["mailq"] = 174, + ["xdmcp"] = 177, + ["nextstep"] = 178, + ["bgp"] = 179, + ["prospero"] = 191, + ["irc"] = 194, + ["smux"] = 199, + ["at-rtmp"] = 201, + ["at-nbp"] = 202, + ["at-echo"] = 204, + ["at-zis"] = 206, + ["qmtp"] = 209, + ["z3950"] = 210, + ["ipx"] = 213, + ["imap3"] = 220, + ["pawserv"] = 345, + ["zserv"] = 346, + ["fatserv"] = 347, + ["rpc2portmap"] = 369, + ["codaauth2"] = 370, + ["clearcase"] = 371, + ["ulistserv"] = 372, + ["ldap"] = 389, + ["imsp"] = 406, + ["svrloc"] = 427, + ["https"] = 443, + ["snpp"] = 444, + ["microsoft-ds"] = 445, + ["kpasswd"] = 464, + ["urd"] = 465, + ["saft"] = 487, + ["isakmp"] = 500, + ["rtsp"] = 554, + ["nqs"] = 607, + ["npmp-local"] = 610, + ["npmp-gui"] = 611, + ["hmmp-ind"] = 612, + ["asf-rmcp"] = 623, + ["qmqp"] = 628, + ["ipp"] = 631, + ["exec"] = 512, + ["biff"] = 512, + ["login"] = 513, + ["who"] = 513, + ["shell"] = 514, + ["syslog"] = 514, + ["printer"] = 515, + ["talk"] = 517, + ["ntalk"] = 518, + ["route"] = 520, + ["timed"] = 525, + ["tempo"] = 526, + ["courier"] = 530, + ["conference"] = 531, + ["netnews"] = 532, + ["netwall"] = 533, + ["gdomap"] = 538, + ["uucp"] = 540, + ["klogin"] = 543, + ["kshell"] = 544, + ["dhcpv6-client"] = 546, + ["dhcpv6-server"] = 547, + ["afpovertcp"] = 548, + ["idfp"] = 549, + ["remotefs"] = 556, + ["nntps"] = 563, + ["submission"] = 587, + ["ldaps"] = 636, + ["tinc"] = 655, + ["silc"] = 706, + ["kerberos-adm"] = 749, + ["webster"] = 765, + ["rsync"] = 873, + ["ftps-data"] = 989, + ["ftps"] = 990, + ["telnets"] = 992, + ["imaps"] = 993, + ["ircs"] = 994, + ["pop3s"] = 995, + ["socks"] = 1080, + ["proofd"] = 1093, + ["rootd"] = 1094, + ["openvpn"] = 1194, + ["rmiregistry"] = 1099, + ["kazaa"] = 1214, + ["nessus"] = 1241, + ["lotusnote"] = 1352, + ["ms-sql-s"] = 1433, + ["ms-sql-m"] = 1434, + ["ingreslock"] = 1524, + ["prospero-np"] = 1525, + ["datametrics"] = 1645, + ["sa-msg-port"] = 1646, + ["kermit"] = 1649, + ["groupwise"] = 1677, + ["l2f"] = 1701, + ["radius"] = 1812, + ["radius-acct"] = 1813, + ["msnp"] = 1863, + ["unix-status"] = 1957, + ["log-server"] = 1958, + ["remoteping"] = 1959, + ["cisco-sccp"] = 2000, + ["search"] = 2010, + ["pipe-server"] = 2010, + ["nfs"] = 2049, + ["gnunet"] = 2086, + ["rtcm-sc104"] = 2101, + ["gsigatekeeper"] = 2119, + ["gris"] = 2135, + ["cvspserver"] = 2401, + ["venus"] = 2430, + ["venus-se"] = 2431, + ["codasrv"] = 2432, + ["codasrv-se"] = 2433, + ["mon"] = 2583, + ["dict"] = 2628, + ["f5-globalsite"] = 2792, + ["gsiftp"] = 2811, + ["gpsd"] = 2947, + ["gds-db"] = 3050, + ["icpv2"] = 3130, + ["iscsi-target"] = 3260, + ["mysql"] = 3306, + ["nut"] = 3493, + ["distcc"] = 3632, + ["daap"] = 3689, + ["svn"] = 3690, + ["suucp"] = 4031, + ["sysrqd"] = 4094, + ["sieve"] = 4190, + ["epmd"] = 4369, + ["remctl"] = 4373, + ["f5-iquery"] = 4353, + ["ipsec-nat-t"] = 4500, + ["iax"] = 4569, + ["mtn"] = 4691, + ["radmin-port"] = 4899, + ["rfe"] = 5002, + ["mmcc"] = 5050, + ["sip"] = 5060, + ["sip-tls"] = 5061, + ["aol"] = 5190, + ["xmpp-client"] = 5222, + ["xmpp-server"] = 5269, + ["cfengine"] = 5308, + ["mdns"] = 5353, + ["postgresql"] = 5432, + ["freeciv"] = 5556, + ["amqp"] = 5672, + ["ggz"] = 5688, + ["x11"] = 6000, + ["x11-1"] = 6001, + ["x11-2"] = 6002, + ["x11-3"] = 6003, + ["x11-4"] = 6004, + ["x11-5"] = 6005, + ["x11-6"] = 6006, + ["x11-7"] = 6007, + ["gnutella-svc"] = 6346, + ["gnutella-rtr"] = 6347, + ["sge-qmaster"] = 6444, + ["sge-execd"] = 6445, + ["mysql-proxy"] = 6446, + ["afs3-fileserver"] = 7000, + ["afs3-callback"] = 7001, + ["afs3-prserver"] = 7002, + ["afs3-vlserver"] = 7003, + ["afs3-kaserver"] = 7004, + ["afs3-volser"] = 7005, + ["afs3-errors"] = 7006, + ["afs3-bos"] = 7007, + ["afs3-update"] = 7008, + ["afs3-rmtsys"] = 7009, + ["font-service"] = 7100, + ["http-alt"] = 8080, + ["bacula-dir"] = 9101, + ["bacula-fd"] = 9102, + ["bacula-sd"] = 9103, + ["xmms2"] = 9667, + ["nbd"] = 10809, + ["zabbix-agent"] = 10050, + ["zabbix-trapper"] = 10051, + ["amanda"] = 10080, + ["dicom"] = 11112, + ["hkp"] = 11371, + ["bprd"] = 13720, + ["bpdbm"] = 13721, + ["bpjava-msvc"] = 13722, + ["vnetd"] = 13724, + ["bpcd"] = 13782, + ["vopied"] = 13783, + ["db-lsp"] = 17500, + ["dcap"] = 22125, + ["gsidcap"] = 22128, + ["wnn6"] = 22273, + -- The remaining ports are not as allocated by IANA + ["kerberos4"] = 750, + ["kerberos-master"] = 751, + ["passwd-server"] = 752, + ["krb-prop"] = 754, + ["krbupdate"] = 760, + ["swat"] = 901, + ["kpop"] = 1109, + ["knetd"] = 2053, + ["zephyr-srv"] = 2102, + ["zephyr-clt"] = 2103, + ["zephyr-hm"] = 2104, + ["eklogin"] = 2105, + ["kx"] = 2111, + ["iprop"] = 2121, + ["supfilesrv"] = 871, + ["supfiledbg"] = 1127, + ["linuxconf"] = 98, + ["poppassd"] = 106, + ["moira-db"] = 775, + ["moira-update"] = 777, + ["moira-ureg"] = 779, + ["spamd"] = 783, + ["omirr"] = 808, + ["customs"] = 1001, + ["skkserv"] = 1178, + ["predict"] = 1210, + ["rmtcfg"] = 1236, + ["wipld"] = 1300, + ["xtel"] = 1313, + ["xtelw"] = 1314, + ["support"] = 1529, + ["cfinger"] = 2003, + ["frox"] = 2121, + ["ninstall"] = 2150, + ["zebrasrv"] = 2600, + ["zebra"] = 2601, + ["ripd"] = 2602, + ["ripngd"] = 2603, + ["ospfd"] = 2604, + ["bgpd"] = 2605, + ["ospf6d"] = 2606, + ["ospfapi"] = 2607, + ["isisd"] = 2608, + ["afbackup"] = 2988, + ["afmbackup"] = 2989, + ["xtell"] = 4224, + ["fax"] = 4557, + ["hylafax"] = 4559, + ["distmp3"] = 4600, + ["munin"] = 4949, + ["enbd-cstatd"] = 5051, + ["enbd-sstatd"] = 5052, + ["pcrd"] = 5151, + ["noclog"] = 5354, + ["hostmon"] = 5355, + ["rplay"] = 5555, + ["nrpe"] = 5666, + ["nsca"] = 5667, + ["mrtd"] = 5674, + ["bgpsim"] = 5675, + ["canna"] = 5680, + ["syslog-tls"] = 6514, + ["sane-port"] = 6566, + ["ircd"] = 6667, + ["zope-ftp"] = 8021, + ["tproxy"] = 8081, + ["omniorb"] = 8088, + ["clc-build-daemon"] = 8990, + ["xinetd"] = 9098, + ["mandelspawn"] = 9359, + ["git"] = 9418, + ["zope"] = 9673, + ["webmin"] = 10000, + ["kamanda"] = 10081, + ["amandaidx"] = 10082, + ["amidxtape"] = 10083, + ["smsqp"] = 11201, + ["xpilot"] = 15345, + ["sgi-cmsd"] = 17001, + ["sgi-crsd"] = 17002, + ["sgi-gcd"] = 17003, + ["sgi-cad"] = 17004, + ["isdnlog"] = 20011, + ["vboxd"] = 20012, + ["binkp"] = 24554, + ["asp"] = 27374, + ["csync2"] = 30865, + ["dircproxy"] = 57000, + ["tfido"] = 60177, + ["fido"] = 60179, +} + +protocol_header_field_offsets = { + icmptype = 0, + icmpcode = 1, + tcpflags = 13, +} + +-- http://support.microsoft.com/kb/170292 +icmp_type_fields = { + ["icmp-echoreply"] = 0, + ["icmp-unreach"] = 3, + ["icmp-sourcequench"] = 4, + ["icmp-redirect"] = 5, + ["icmp-echo"] = 8, + ["icmp-routeradvert"] = 9, + ["icmp-routersolicit"] = 10, + ["icmp-timxceed"] = 11, + ["icmp-paramprob"] = 12, + ["icmp-tstamp"] = 13, + ["icmp-tstampreply"] = 14, + ["icmp-ireq"] = 15, + ["icmp-ireqreply"] = 16, + ["icmp-maskreq"] = 17, + ["icmp-maskreply"] = 18, +} + +-- http://stackoverflow.com/questions/1480548/tcp-flags-present-in-the-header +tcp_flag_fields = { + ["tcp-fin"] = 1, + ["tcp-syn"] = 2, + ["tcp-rst"] = 4, + ["tcp-push"] = 8, + ["tcp-ack"] = 16, + ["tcp-urg"] = 32, +} diff --git a/src/pf/expand.lua b/src/pf/expand.lua new file mode 100644 index 0000000000..10824b1949 --- /dev/null +++ b/src/pf/expand.lua @@ -0,0 +1,1239 @@ +module(...,package.seeall) + +local utils = require('pf.utils') + +local verbose = os.getenv("PF_VERBOSE"); + +local expand_arith, expand_relop, expand_bool + +local set, concat, pp = utils.set, utils.concat, utils.pp +local uint16, uint32 = utils.uint16, utils.uint32 +local ipv4_to_int, ipv6_as_4x32 = utils.ipv4_to_int, utils.ipv6_as_4x32 + +local llc_types = set( + 'i', 's', 'u', 'rr', 'rnr', 'rej', 'ui', 'ua', + 'disc', 'sabme', 'test', 'xis', 'frmr' +) + +local pf_reasons = set( + 'match', 'bad-offset', 'fragment', 'short', 'normalize', 'memory' +) + +local pf_actions = set( + 'pass', 'block', 'nat', 'rdr', 'binat', 'scrub' +) + +local wlan_frame_types = set('mgt', 'ctl', 'data') +local wlan_frame_mgt_subtypes = set( + 'assoc-req', 'assoc-resp', 'reassoc-req', 'reassoc-resp', + 'probe-req', 'probe-resp', 'beacon', 'atim', 'disassoc', 'auth', 'deauth' +) +local wlan_frame_ctl_subtypes = set( + 'ps-poll', 'rts', 'cts', 'ack', 'cf-end', 'cf-end-ack' +) +local wlan_frame_data_subtypes = set( + 'data', 'data-cf-ack', 'data-cf-poll', 'data-cf-ack-poll', 'null', + 'cf-ack', 'cf-poll', 'cf-ack-poll', 'qos-data', 'qos-data-cf-ack', + 'qos-data-cf-poll', 'qos-data-cf-ack-poll', 'qos', 'qos-cf-poll', + 'quos-cf-ack-poll' +) + +local wlan_directions = set('nods', 'tods', 'fromds', 'dstods') + +local function unimplemented(expr, dlt) + error("not implemented: "..expr[1]) +end + +-- Ethernet protocols +local PROTO_AARP = 33011 -- 0x80f3 +local PROTO_ARP = 2054 -- 0x806 +local PROTO_ATALK = 32923 -- 0x809b +local PROTO_DECNET = 24579 -- 0x6003 +local PROTO_IPV4 = 2048 -- 0x800 +local PROTO_IPV6 = 34525 -- 0x86dd +local PROTO_IPX = 33079 -- 0X8137 +local PROTO_ISO = 65278 -- 0xfefe +local PROTO_LAT = 24580 -- 0x6004 +local PROTO_MOPDL = 24577 -- 0x6001 +local PROTO_MOPRC = 24578 -- 0x6002 +local PROTO_NETBEUI = 61680 -- 0xf0f0 +local PROTO_RARP = 32821 -- 0x8035 +local PROTO_SCA = 24583 -- 0x6007 +local PROTO_STP = 66 -- 0x42 + +local ether_min_payloads = { + [PROTO_IPV4] = 20, + [PROTO_ARP] = 28, + [PROTO_RARP] = 28, + [PROTO_IPV6] = 40 +} + +-- IP protocols +local PROTO_AH = 51 -- 0x33 +local PROTO_ESP = 50 -- 0x32 +local PROTO_ICMP = 1 -- 0x1 +local PROTO_ICMP6 = 58 -- 0x3a +local PROTO_IGMP = 2 -- 0x2 +local PROTO_IGRP = 9 -- 0x9 +local PROTO_PIM = 103 -- 0x67 +local PROTO_SCTP = 132 -- 0x84 +local PROTO_TCP = 6 -- 0x6 +local PROTO_UDP = 17 -- 0x11 +local PROTO_VRRP = 112 -- 0x70 + +local ip_min_payloads = { + [PROTO_ICMP] = 8, + [PROTO_UDP] = 8, + [PROTO_TCP] = 20, + [PROTO_IGMP] = 8, + [PROTO_IGRP] = 8, + [PROTO_PIM] = 4, + [PROTO_SCTP] = 12, + [PROTO_VRRP] = 8 +} + +-- ISO protocols + +local PROTO_CLNP = 129 -- 0x81 +local PROTO_ESIS = 130 -- 0x82 +local PROTO_ISIS = 131 -- 0x83 + +local ETHER_TYPE = 12 +local ETHER_PAYLOAD = 14 +local IP_FLAGS = 6 +local IP_PROTOCOL = 9 + +-- Minimum payload checks insert a byte access to the last byte of the +-- minimum payload size. Since the comparison should fold (because it +-- will always be >= 0), we will be left with just an eager assertion on +-- the minimum packet size, which should help elide future packet size +-- assertions. +local function has_proto_min_payload(min_payloads, proto, accessor) + local min_payload = assert(min_payloads[proto]) + return { '<=', 0, { accessor, min_payload - 1, 1 } } +end + +-- When proto is greater than 1500 (0x5DC) , the frame is treated as an +-- Ethernet frame and the Type/Length is interpreted as Type, storing the +-- EtherType value. +-- Otherwise, the frame is interpreted as an 802.3 frame and the +-- Type/Length field is interpreted as Length. The Length cannot be greater +-- than 1500. The first byte after the Type/Length field stores the Service +-- Access Point of the 802.3 frame. It works as an EtherType at LLC level. +-- +-- See: https://tools.ietf.org/html/draft-ietf-isis-ext-eth-01 + +local ETHER_MAX_LEN = 1500 + +local function has_ether_protocol(proto) + if proto > ETHER_MAX_LEN then + return { '=', { '[ether]', ETHER_TYPE, 2 }, proto } + end + return { 'and', + { '<=', {'[ether]', ETHER_TYPE, 2}, ETHER_MAX_LEN }, + { '=', { '[ether]', ETHER_PAYLOAD, 1}, proto } } +end +local function has_ether_protocol_min_payload(proto) + return has_proto_min_payload(ether_min_payloads, proto, '[ether*]') +end +local function has_ipv4_protocol(proto) + return { '=', { '[ip]', IP_PROTOCOL, 1 }, proto } +end +local function has_ipv4_protocol_min_payload(proto) + -- Since the [ip*] accessor asserts that is_first_ipv4_fragment(), + -- and we don't want that, we use [ip] and assume the minimum IP + -- header size. + local min_payload = assert(ip_min_payloads[proto]) + min_payload = min_payload + assert(ether_min_payloads[PROTO_IPV4]) + return { '<=', 0, { '[ip]', min_payload - 1, 1 } } +end + +local function is_first_ipv4_fragment() + return { '=', { '&', { '[ip]', IP_FLAGS, 2 }, 0x1fff }, 0 } +end +local function has_ipv6_protocol(proto) + local IPV6_NEXT_HEADER_1 = 6 + local IPV6_NEXT_HEADER_2 = 40 + local IPV6_FRAGMENTATION_EXTENSION_HEADER = 44 + return { 'and', { 'ip6' }, + { 'or', + { '=', { '[ip6]', IPV6_NEXT_HEADER_1, 1 }, proto }, + { 'and', + { '=', { '[ip6]', IPV6_NEXT_HEADER_1, 1 }, + IPV6_FRAGMENTATION_EXTENSION_HEADER }, + { '=', { '[ip6]', IPV6_NEXT_HEADER_2, 1 }, proto } } } } +end +local function has_ipv6_protocol_min_payload(proto) + -- Assume the minimum ipv6 header size. + local min_payload = assert(ip_min_payloads[proto]) + min_payload = min_payload + assert(ether_min_payloads[PROTO_IPV6]) + return { '<=', 0, { '[ip6]', min_payload - 1, 1 } } +end +local function has_ip_protocol(proto) + return { 'if', { 'ip' }, has_ipv4_protocol(proto), has_ipv6_protocol(proto) } +end + +-- Port operations +-- + +local SRC_PORT = 0 +local DST_PORT = 2 + +local function has_ipv4_src_port(port) + return { '=', { '[ip*]', SRC_PORT, 2 }, port } +end +local function has_ipv4_dst_port(port) + return { '=', { '[ip*]', DST_PORT, 2 }, port } +end +local function has_ipv4_port(port) + return { 'or', has_ipv4_src_port(port), has_ipv4_dst_port(port) } +end +local function has_ipv6_src_port(port) + return { '=', { '[ip6*]', SRC_PORT, 2 }, port } +end +local function has_ipv6_dst_port(port) + return { '=', { '[ip6*]', DST_PORT, 2 }, port } +end +local function has_ipv6_port(port) + return { 'or', has_ipv6_src_port(port), has_ipv6_dst_port(port) } +end +local function expand_dir_port(expr, has_ipv4_port, has_ipv6_port) + local port = expr[2] + return { 'if', { 'ip' }, + { 'and', + { 'or', has_ipv4_protocol(PROTO_TCP), + { 'or', has_ipv4_protocol(PROTO_UDP), + has_ipv4_protocol(PROTO_SCTP) } }, + has_ipv4_port(port) }, + { 'and', + { 'or', has_ipv6_protocol(PROTO_TCP), + { 'or', has_ipv6_protocol(PROTO_UDP), + has_ipv6_protocol(PROTO_SCTP) } }, + has_ipv6_port(port) } } +end +local function expand_port(expr) + return expand_dir_port(expr, has_ipv4_port, has_ipv6_port) +end +local function expand_src_port(expr) + return expand_dir_port(expr, has_ipv4_src_port, has_ipv6_src_port) +end +local function expand_dst_port(expr) + return expand_dir_port(expr, has_ipv4_dst_port, has_ipv6_dst_port) +end + +local function expand_proto_port(expr, proto) + local port = expr[2] + return { 'if', { 'ip' }, + { 'and', + has_ipv4_protocol(proto), + has_ipv4_port(port) }, + { 'and', + has_ipv6_protocol(proto), + has_ipv6_port(port) } } +end +local function expand_tcp_port(expr) + return expand_proto_port(expr, PROTO_TCP) +end +local function expand_udp_port(expr) + return expand_proto_port(expr, PROTO_UDP) +end + +local function expand_proto_src_port(expr, proto) + local port = expr[2] + return { 'if', { 'ip' }, + { 'and', + has_ipv4_protocol(proto), + has_ipv4_src_port(port) }, + { 'and', + has_ipv6_protocol(proto), + has_ipv6_src_port(port) } } +end +local function expand_tcp_src_port(expr) + return expand_proto_src_port(expr, PROTO_TCP) +end +local function expand_udp_src_port(expr) + return expand_proto_src_port(expr, PROTO_UDP) +end + +local function expand_proto_dst_port(expr, proto) + local port = expr[2] + return { 'if', { 'ip' }, + { 'and', + has_ipv4_protocol(proto), + has_ipv4_dst_port(port) }, + { 'and', + has_ipv6_protocol(proto), + has_ipv6_dst_port(port) } } +end +local function expand_tcp_dst_port(expr) + return expand_proto_dst_port(expr, PROTO_TCP) +end +local function expand_udp_dst_port(expr) + return expand_proto_dst_port(expr, PROTO_UDP) +end + +-- Portrange operations +-- +local function has_ipv4_src_portrange(lo, hi) + return { 'and', + { '<=', lo, { '[ip*]', SRC_PORT, 2 } }, + { '<=', { '[ip*]', SRC_PORT, 2 }, hi } } +end +local function has_ipv4_dst_portrange(lo, hi) + return { 'and', + { '<=', lo, { '[ip*]', DST_PORT, 2 } }, + { '<=', { '[ip*]', DST_PORT, 2 }, hi } } +end +local function has_ipv4_portrange(lo, hi) + return { 'or', has_ipv4_src_portrange(lo, hi), has_ipv4_dst_portrange(lo, hi) } +end +local function has_ipv6_src_portrange(lo, hi) + return { 'and', + { '<=', lo, { '[ip6*]', SRC_PORT, 2 } }, + { '<=', { '[ip6*]', SRC_PORT, 2 }, hi } } +end +local function has_ipv6_dst_portrange(lo, hi) + return { 'and', + { '<=', lo, { '[ip6*]', DST_PORT, 2 } }, + { '<=', { '[ip6*]', DST_PORT, 2 }, hi } } +end +local function has_ipv6_portrange(lo, hi) + return { 'or', has_ipv6_src_portrange(lo, hi), has_ipv6_dst_portrange(lo, hi) } +end +local function expand_dir_portrange(expr, has_ipv4_portrange, has_ipv6_portrange) + local lo, hi = expr[2][1], expr[2][2] + return { 'if', { 'ip' }, + { 'and', + { 'or', has_ipv4_protocol(PROTO_TCP), + { 'or', has_ipv4_protocol(PROTO_UDP), + has_ipv4_protocol(PROTO_SCTP) } }, + has_ipv4_portrange(lo, hi) }, + { 'and', + { 'or', has_ipv6_protocol(PROTO_TCP), + { 'or', has_ipv6_protocol(PROTO_UDP), + has_ipv6_protocol(PROTO_SCTP) } }, + has_ipv6_portrange(lo, hi) } } +end +local function expand_portrange(expr) + return expand_dir_portrange(expr, has_ipv4_portrange, has_ipv6_portrange) +end +local function expand_src_portrange(expr) + return expand_dir_portrange(expr, has_ipv4_src_portrange, has_ipv6_src_portrange) +end +local function expand_dst_portrange(expr) + return expand_dir_portrange(expr, has_ipv4_dst_portrange, has_ipv6_dst_portrange) +end + +local function expand_proto_portrange(expr, proto) + local lo, hi = expr[2][1], expr[2][2] + return { 'if', { 'ip' }, + { 'and', + has_ipv4_protocol(proto), + has_ipv4_portrange(lo, hi) }, + { 'and', + has_ipv6_protocol(proto), + has_ipv6_portrange(lo, hi) } } +end +local function expand_tcp_portrange(expr) + return expand_proto_portrange(expr, PROTO_TCP) +end +local function expand_udp_portrange(expr) + return expand_proto_portrange(expr, PROTO_UDP) +end + +local function expand_proto_src_portrange(expr, proto) + local lo, hi = expr[2][1], expr[2][2] + return { 'if', { 'ip' }, + { 'and', + has_ipv4_protocol(proto), + has_ipv4_src_portrange(lo, hi) }, + { 'and', + has_ipv6_protocol(proto), + has_ipv6_src_portrange(lo, hi) } } +end +local function expand_tcp_src_portrange(expr) + return expand_proto_src_portrange(expr, PROTO_TCP) +end +local function expand_udp_src_portrange(expr) + return expand_proto_src_portrange(expr, PROTO_UDP) +end + +local function expand_proto_dst_portrange(expr, proto) + local lo, hi = expr[2][1], expr[2][2] + return { 'if', { 'ip' }, + { 'and', + has_ipv4_protocol(proto), + has_ipv4_dst_portrange(lo, hi) }, + { 'and', + has_ipv6_protocol(proto), + has_ipv6_dst_portrange(lo, hi) } } +end +local function expand_tcp_dst_portrange(expr) + return expand_proto_dst_portrange(expr, PROTO_TCP) +end +local function expand_udp_dst_portrange(expr) + return expand_proto_dst_portrange(expr, PROTO_UDP) +end + +-- IP protocol + +local proto_info = { + ip = { id = PROTO_IPV4, access = "[ip]", src = 12, dst = 16 }, + arp = { id = PROTO_ARP, access = "[arp]", src = 14, dst = 24 }, + rarp = { id = PROTO_RARP, access = "[rarp]", src = 14, dst = 24 }, + ip6 = { id = PROTO_IPV6, access = "[ip6]", src = 8, dst = 24 }, +} + +local function has_proto_dir_host(proto, dir, addr, mask) + local host = ipv4_to_int(addr) + local val = { proto_info[proto].access, proto_info[proto][dir], 4 } + if mask then + mask = tonumber(mask) and 2^32 - 2^(32 - mask) or ipv4_to_int(mask) + val = { '&', val, tonumber(mask) } + end + return { 'and', has_ether_protocol(proto_info[proto].id), { '=', val, host } } +end + +local function expand_ip_src_host(expr) + return has_proto_dir_host("ip", "src", expr[2], expr[3]) +end +local function expand_ip_dst_host(expr) + return has_proto_dir_host("ip", "dst", expr[2], expr[3]) +end +local function expand_ip_host(expr) + return { 'or', expand_ip_src_host(expr), expand_ip_dst_host(expr) } +end + +local function expand_ip_broadcast(expr) + error("netmask not known, so 'ip broadcast' not supported") +end +local function expand_ip6_broadcast(expr) + error("only link-layer/IP broadcast filters supported") +end +local function expand_ip_multicast(expr) + local IPV4_MULTICAST = 224 -- 0xe0 + local IPV4_DEST_ADDRESS = 16 + return { '=', { '[ip]', IPV4_DEST_ADDRESS, 1 }, IPV4_MULTICAST } +end +local function expand_ip6_multicast(expr) + local IPV6_MULTICAST = 255 -- 0xff + local IPV6_DEST_ADDRESS_OFFSET = 24 -- 14 + 24 = 38 (last two bytes of dest address) + return { '=', { '[ip6]', IPV6_DEST_ADDRESS_OFFSET, 1 }, IPV6_MULTICAST } +end +local function expand_ip4_protochain(expr) + -- FIXME: Not implemented yet. BPF code of ip protochain is rather complex. + return unimplemented(expr) +end +local function expand_ip6_protochain(expr) + -- FIXME: Not implemented yet. BPF code of ip6 protochain is rather complex. + return unimplemented(expr) +end +local function expand_ip_protochain(expr) + return { 'if', 'ip', expand_ip4_protochain(expr), expand_ip6_protochain(expr) } +end + +local ip_protos = { + icmp = PROTO_ICMP, + icmp6 = PROTO_ICMP6, + igmp = PROTO_IGMP, + igrp = PROTO_IGRP, + pim = PROTO_PIM, + ah = PROTO_AH, + esp = PROTO_ESP, + vrrp = PROTO_VRRP, + udp = PROTO_UDP, + tcp = PROTO_TCP, + sctp = PROTO_SCTP, +} + +local function expand_ip4_proto(expr) + local proto = expr[2] + if type(proto) == 'string' then proto = ip_protos[proto] end + return has_ipv4_protocol(assert(proto, "Invalid IP protocol")) +end + +local function expand_ip6_proto(expr) + local proto = expr[2] + if type(proto) == 'string' then proto = ip_protos[proto] end + return has_ipv6_protocol(assert(proto, "Invalid IP protocol")) +end + +local function expand_ip_proto(expr) + return { 'or', has_ipv4_protocol(expr[2]), has_ipv6_protocol(expr[2]) } +end + +-- ISO + +local iso_protos = { + clnp = PROTO_CLNP, + esis = PROTO_ESIS, + isis = PROTO_ISIS, +} + +local function has_iso_protocol(proto) + return { 'and', + { '<=', { '[ether]', ETHER_TYPE, 2 }, ETHER_MAX_LEN }, + { 'and', + { '=', { '[ether]', ETHER_PAYLOAD, 2 }, PROTO_ISO }, + { '=', { '[ether]', ETHER_PAYLOAD + 3, 1 }, proto } } } +end + +local function expand_iso_proto(expr) + local proto = expr[2] + if type(proto) == 'string' then proto = iso_protos[proto] end + return has_iso_protocol(assert(proto, "Invalid ISO protocol")) +end + +-- ARP protocol + +local function expand_arp_src_host(expr) + return has_proto_dir_host("arp", "src", expr[2], expr[3]) +end +local function expand_arp_dst_host(expr) + return has_proto_dir_host("arp", "dst", expr[2], expr[3]) +end +local function expand_arp_host(expr) + return { 'or', expand_arp_src_host(expr), expand_arp_dst_host(expr) } +end + +-- RARP protocol + +local function expand_rarp_src_host(expr) + return has_proto_dir_host("rarp", "src", expr[2], expr[3]) +end +local function expand_rarp_dst_host(expr) + return has_proto_dir_host("rarp", "dst", expr[2], expr[3]) +end +local function expand_rarp_host(expr) + return { 'or', expand_rarp_src_host(expr), expand_rarp_dst_host(expr) } +end + +-- IPv6 + +local function ipv6_dir_host(proto, dir, addr, mask_len) + mask_len = mask_len or 128 + local offset = proto_info.ip6[dir] + local ipv6 = ipv6_as_4x32(addr) + + local function match_ipv6_fragment(i) + local fragment = ipv6[i] + + -- Calculate mask for fragment + local mask = mask_len >= 32 and 0 or mask_len + mask_len = mask_len >= 32 and mask_len - 32 or 0 + + -- Retrieve address current offset + local val = { proto_info.ip6.access, offset, 4 } + offset = offset + 4 + + if mask ~= 0 then val = { '&', val, 2^32 - 2^(32 - mask) } end + return { '=', val, fragment } + end + + -- Lowering of an IPv6 address does not require to go iterate through all + -- IPv6 fragments (4x32). Once mask_len becomes 0 is possible to exit. + local function match_ipv6(i) + local i = i or 1 + local expr = match_ipv6_fragment(i) + if mask_len == 0 or i > 4 then return expr end + return { 'and', expr, match_ipv6(i + 1) } + end + + return { 'and', has_ether_protocol(PROTO_IPV6), match_ipv6() } +end + +local function expand_src_ipv6_host(expr) + return ipv6_dir_host('ip6', 'src', expr[2], expr[3]) +end +local function expand_dst_ipv6_host(expr) + return ipv6_dir_host('ip6', 'dst', expr[2], expr[3]) +end +local function expand_ipv6_host(expr) + return { 'or', + ipv6_dir_host('ip6', 'src', expr[2], expr[3]), + ipv6_dir_host('ip6', 'dst', expr[2], expr[3]) } +end + +-- Host + + +--[[ +* Format IPv4 expr: + { 'net', { 'ipv4', 127, 0, 0, 1 } } + { 'ipv4/len', { 'ipv4', 127, 0, 0, 1 }, 24 } +* Format IPv6 expr: + { 'net', { 'ipv6', 0, 0, 0, 0, 0, 0, 0, 1 } } + { 'ipv4/len', { 'ipv6', 0, 0, 0, 0, 0, 0, 0, 1 }, 24 } +]]-- +local function is_ipv6_addr(expr) + return expr[2][1] == 'ipv6' +end + +local function expand_src_host(expr) + if is_ipv6_addr(expr) then return expand_src_ipv6_host(expr) end + return { 'if', { 'ip' }, expand_ip_src_host(expr), + { 'if', { 'arp' }, expand_arp_src_host(expr), + expand_rarp_src_host(expr) } } +end +local function expand_dst_host(expr) + if is_ipv6_addr(expr) then return expand_dst_ipv6_host(expr) end + return { 'if', { 'ip' }, expand_ip_dst_host(expr), + { 'if', { 'arp' }, expand_arp_dst_host(expr), + expand_rarp_dst_host(expr) } } +end +-- Format IPv4: { 'ipv4/len', { 'ipv4', 127, 0, 0, 1 }, 8 } +-- Format IPv4: { 'ipv4/mask', { 'ipv4', 127, 0, 0, 1 }, { 'ipv4', 255, 0, 0, 0 } } +-- Format IPv6: { 'ipv6/len', { 'ipv6', 0, 0, 0, 0, 0, 0, 0, 1 }, 128 } +local function expand_host(expr) + if is_ipv6_addr(expr) then return expand_ipv6_host(expr) end + return { 'if', { 'ip' }, expand_ip_host(expr), + { 'if', { 'arp' }, expand_arp_host(expr), + expand_rarp_host(expr) } } +end + +-- Ether + +local MAC_DST = 0 +local MAC_SRC = 6 + +local function ehost_to_int(addr) + assert(addr[1] == 'ehost', "Not a valid ehost address") + return uint16(addr[2], addr[3]), uint32(addr[4], addr[5], addr[6], addr[7]) +end +local function expand_ether_src_host(expr) + local hi, lo = ehost_to_int(expr[2]) + return { 'and', + { '=', { '[ether]', MAC_SRC, 2 }, hi }, + { '=', { '[ether]', MAC_SRC + 2, 4 }, lo } } +end +local function expand_ether_dst_host(expr) + local hi, lo = ehost_to_int(expr[2]) + return { 'and', + { '=', { '[ether]', MAC_DST, 2 }, hi }, + { '=', { '[ether]', MAC_DST + 2, 4 }, lo } } +end +local function expand_ether_host(expr) + return { 'or', expand_ether_src_host(expr), expand_ether_dst_host(expr) } +end +local function expand_ether_broadcast(expr) + local broadcast = { 'ehost', 255, 255, 255, 255, 255, 255 } + local hi, lo = ehost_to_int(broadcast) + return { 'and', + { '=', { '[ether]', MAC_DST, 2 }, hi }, + { '=', { '[ether]', MAC_DST + 2, 4 }, lo } } +end +local function expand_ether_multicast(expr) + return { '!=', { '&', { '[ether]', 0, 1 }, 1 }, 0 } +end + +-- Ether protos + +local function expand_ip(expr) + return has_ether_protocol(PROTO_IPV4) +end +local function expand_ip6(expr) + return has_ether_protocol(PROTO_IPV6) +end +local function expand_arp(expr) + return has_ether_protocol(PROTO_ARP) +end +local function expand_rarp(expr) + return has_ether_protocol(PROTO_RARP) +end + +local function expand_atalk(expr) + local ATALK_ID_1 = 491675 -- 0x7809B + local ATALK_ID_2 = 2863268616 -- 0xaaaa0308 + return { 'or', + has_ether_protocol(PROTO_ATALK), + { 'if', { '>', { '[ether]', ETHER_TYPE, 2}, ETHER_MAX_LEN }, + { 'false' }, + { 'and', + { '=', { '[ether]', ETHER_PAYLOAD + 4, 2 }, ATALK_ID_1 }, + { '=', { '[ether]', ETHER_PAYLOAD, 4 }, ATALK_ID_2 } } } } +end +local function expand_aarp(expr) + local AARP_ID = 2863268608 -- 0xaaaa0300 + return { 'or', + has_ether_protocol(PROTO_AARP), + { 'if', { '>', { '[ether]', ETHER_TYPE, 2}, ETHER_MAX_LEN }, + { 'false' }, + { 'and', + { '=', { '[ether]', ETHER_PAYLOAD + 4, 2 }, PROTO_AARP }, + { '=', { '[ether]', ETHER_PAYLOAD, 4 }, AARP_ID } } } } +end +local function expand_decnet(expr) + return has_ether_protocol(PROTO_DECNET) +end +local function expand_sca(expr) + return has_ether_protocol(PROTO_SCA) +end +local function expand_lat(expr) + return has_ether_protocol(PROTO_LAT) +end +local function expand_mopdl(expr) + return has_ether_protocol(PROTO_MOPDL) +end +local function expand_moprc(expr) + return has_ether_protocol(PROTO_MOPRC) +end +local function expand_iso(expr) + return { 'and', + { '<=', { '[ether]', ETHER_TYPE, 2 }, ETHER_MAX_LEN }, + { '=', { '[ether]', ETHER_PAYLOAD, 2 }, PROTO_ISO } } +end +local function expand_stp(expr) + return { 'and', + { '<=', { '[ether]', ETHER_TYPE, 2 }, ETHER_MAX_LEN }, + { '=', { '[ether]', ETHER_PAYLOAD, 1 }, PROTO_STP } } +end + +local function expand_ipx(expr) + local IPX_SAP = 224 -- 0xe0 + local IPX_CHECKSUM = 65535 -- 0xffff + local AARP_ID = 2863268608 -- 0xaaaa0300 + return { 'or', + has_ether_protocol(PROTO_IPX), + { 'if', { '>', { '[ether]', ETHER_TYPE, 2}, ETHER_MAX_LEN }, + { 'false' }, + { 'or', + { 'and', + { '=', { '[ether]', ETHER_PAYLOAD + 4, 2 }, PROTO_IPX }, + { '=', { '[ether]', ETHER_PAYLOAD, 4 }, AARP_ID } }, + { 'or', + { '=', { '[ether]', ETHER_PAYLOAD, 1 }, IPX_SAP }, + { '=', { '[ether]', ETHER_PAYLOAD, 2 }, IPX_CHECKSUM } } } } } +end +local function expand_netbeui(expr) + return { 'and', + { '<=', { '[ether]', ETHER_TYPE, 2 }, ETHER_MAX_LEN }, + { '=', { '[ether]', ETHER_PAYLOAD, 2 }, PROTO_NETBEUI } } +end + +local ether_protos = { + ip = expand_ip, + ip6 = expand_ip6, + arp = expand_arp, + rarp = expand_rarp, + atalk = expand_atalk, + aarp = expand_aarp, + decnet = expand_decnet, + sca = expand_sca, + lat = expand_lat, + mopdl = expand_mopdl, + moprc = expand_moprc, + iso = expand_iso, + stp = expand_stp, + ipx = expand_ipx, + netbeui = expand_netbeui, +} + +local function expand_ether_proto(expr) + local proto = expr[2] + if type(proto) == 'string' then return ether_protos[proto](expr) end + return has_ether_protocol(proto) +end + +-- Net + +local function expand_src_net(expr) + local addr = expr + local proto = expr[2][1] + if proto:match("/len$") or proto:match("/mask$") then addr = expr[2] end + if is_ipv6_addr(addr) then return expand_src_ipv6_host(addr) end + return expand_src_host(addr) +end +local function expand_dst_net(expr) + local addr = expr + local proto = expr[2][1] + if proto:match("/len$") or proto:match("/mask$") then addr = expr[2] end + if is_ipv6_addr(addr) then return expand_dst_ipv6_host(addr) end + return expand_dst_host(addr) +end + +-- Format IPv4 expr: { 'net', { 'ipv4/len', { 'ipv4', 127, 0, 0, 0 }, 8 } } +-- Format IPV6 expr: { 'net', { 'ipv6/len', { 'ipv6', 0, 0, 0, 0, 0, 0, 0, 1 }, 128 } } +local function expand_net(expr) + local addr = expr + local proto = expr[2][1] + if proto:match("/len$") or proto:match("/mask$") then addr = expr[2] end + if is_ipv6_addr(addr) then return expand_ipv6_host(addr) end + return expand_host(addr) +end + +-- Packet length + +local function expand_less(expr) + return { '<=', 'len', expr[2] } +end +local function expand_greater(expr) + return { '>=', 'len', expr[2] } +end + +-- DECNET + +local function expand_decnet_src(expr) + local addr = expr[2] + local addr_int = uint16(addr[2], addr[3]) + return { 'if', { '=', { '&', { '[ether]', ETHER_PAYLOAD + 2, 1}, 7 }, 2 }, + { '=', { '[ether]', ETHER_PAYLOAD + 5, 2}, addr_int }, + { 'if', { '=', { '&', { '[ether]', ETHER_PAYLOAD + 2, 2}, 65287 }, 33026 }, + { '=', { '[ether]', ETHER_PAYLOAD + 6, 2}, addr_int }, + { 'if', { '=', { '&', { '[ether]', ETHER_PAYLOAD + 2, 1}, 7 }, 6 }, + { '=', { '[ether]', ETHER_PAYLOAD + 17, 2}, addr_int }, + { 'if', { '=', { '&', { '[ether]', ETHER_PAYLOAD + 2, 2}, 65287 }, 33030 }, + { '=', { '[ether]', ETHER_PAYLOAD + 18, 2}, addr_int }, + { 'false' } } } } } +end +local function expand_decnet_dst(expr) + local addr = expr[2] + local addr_int = uint16(addr[2], addr[3]) + return { 'if', { '=', { '&', { '[ether]', ETHER_PAYLOAD + 2, 1}, 7 }, 2 }, + { '=', { '[ether]', ETHER_PAYLOAD + 3, 2}, addr_int }, + { 'if', { '=', { '&', { '[ether]', ETHER_PAYLOAD + 2, 2}, 65287 }, 33026 }, + { '=', { '[ether]', ETHER_PAYLOAD + 4, 2}, addr_int }, + { 'if', { '=', { '&', { '[ether]', ETHER_PAYLOAD + 2, 1}, 7 }, 6 }, + { '=', { '[ether]', ETHER_PAYLOAD + 9, 2}, addr_int }, + { 'if', { '=', { '&', { '[ether]', ETHER_PAYLOAD + 2, 2}, 65287 }, 33030 }, + { '=', { '[ether]', ETHER_PAYLOAD + 10, 2}, addr_int }, + { 'false' } } } } } +end +local function expand_decnet_host(expr) + return { 'or', expand_decnet_src(expr), expand_decnet_dst(expr) } +end + +-- IS-IS + +local L1_IIH = 15 -- 0x0F +local L2_IIH = 16 -- 0x10 +local PTP_IIH = 17 -- 0x11 +local L1_LSP = 18 -- 0x12 +local L2_LSP = 20 -- 0x14 +local L1_CSNP = 24 -- 0x18 +local L2_CSNP = 25 -- 0x19 +local L1_PSNP = 26 -- 0x1A +local L2_PSNP = 27 -- 0x1B + +local function expand_isis_protocol(...) + local function concat(lop, reg, values, i) + i = i or 1 + if i == #values then return { '=', reg, values[i] } end + return { lop, { '=', reg, values[i] }, concat(lop, reg, values, i+1) } + end + return { 'if', has_iso_protocol(PROTO_ISIS), + concat('or', { '[ether]', ETHER_PAYLOAD + 7, 1 }, {...} ), + { 'false' } } +end +local function expand_l1(expr) + return expand_isis_protocol(L1_IIH, L1_LSP, L1_CSNP, L1_PSNP, PTP_IIH) +end +local function expand_l2(expr) + return expand_isis_protocol(L2_IIH, L2_LSP, L2_CSNP, L2_PSNP, PTP_IIH) +end +local function expand_iih(expr) + return expand_isis_protocol(L1_IIH, L2_IIH, PTP_IIH) +end +local function expand_lsp(expr) + return expand_isis_protocol(L1_LSP, L2_LSP) +end +local function expand_snp(expr) + return expand_isis_protocol(L1_CSNP, L2_CSNP, L1_PSNP, L2_PSNP) +end +local function expand_csnp(expr) + return expand_isis_protocol(L1_CSNP, L2_CSNP) +end +local function expand_psnp(expr) + return expand_isis_protocol(L1_PSNP, L2_PSNP) +end + +local primitive_expanders = { + dst_host = expand_dst_host, + dst_net = expand_dst_net, + dst_port = expand_dst_port, + dst_portrange = expand_dst_portrange, + src_host = expand_src_host, + src_net = expand_src_net, + src_port = expand_src_port, + src_portrange = expand_src_portrange, + host = expand_host, + ether_src = expand_ether_src_host, + ether_src_host = expand_ether_src_host, + ether_dst = expand_ether_dst_host, + ether_dst_host = expand_ether_dst_host, + ether_host = expand_ether_host, + ether_broadcast = expand_ether_broadcast, + fddi_src = expand_ether_src_host, + fddi_src_host = expand_ether_src_host, + fddi_dst = expand_ether_dst_host, + fddi_dst_host = expand_ether_dst_host, + fddi_host = expand_ether_host, + fddi_broadcast = expand_ether_broadcast, + tr_src = expand_ether_src_host, + tr_src_host = expand_ether_src_host, + tr_dst = expand_ether_dst_host, + tr_dst_host = expand_ether_dst_host, + tr_host = expand_ether_host, + tr_broadcast = expand_ether_broadcast, + wlan_src = expand_ether_src_host, + wlan_src_host = expand_ether_src_host, + wlan_dst = expand_ether_dst_host, + wlan_dst_host = expand_ether_dst_host, + wlan_host = expand_ether_host, + wlan_broadcast = expand_ether_broadcast, + broadcast = expand_ether_broadcast, + ether_multicast = expand_ether_multicast, + multicast = expand_ether_multicast, + ether_proto = expand_ether_proto, + gateway = unimplemented, + net = expand_net, + port = expand_port, + portrange = expand_portrange, + less = expand_less, + greater = expand_greater, + ip = expand_ip, + ip_proto = expand_ip4_proto, + ip_protochain = expand_ip4_protochain, + ip_host = expand_ip_host, + ip_src = expand_ip_src_host, + ip_src_host = expand_ip_src_host, + ip_dst = expand_ip_dst_host, + ip_dst_host = expand_ip_dst_host, + ip_broadcast = expand_ip_broadcast, + ip_multicast = expand_ip_multicast, + ip6 = expand_ip6, + ip6_proto = expand_ip6_proto, + ip6_protochain = expand_ip6_protochain, + ip6_broadcast = expand_ip6_broadcast, + ip6_multicast = expand_ip6_multicast, + proto = expand_ip_proto, + tcp = function(expr) return has_ip_protocol(PROTO_TCP) end, + tcp_port = expand_tcp_port, + tcp_src_port = expand_tcp_src_port, + tcp_dst_port = expand_tcp_dst_port, + tcp_portrange = expand_tcp_portrange, + tcp_src_portrange = expand_tcp_src_portrange, + tcp_dst_portrange = expand_tcp_dst_portrange, + udp = function(expr) return has_ip_protocol(PROTO_UDP) end, + udp_port = expand_udp_port, + udp_src_port = expand_udp_src_port, + udp_dst_port = expand_udp_dst_port, + udp_portrange = expand_udp_portrange, + udp_src_portrange = expand_udp_src_portrange, + udp_dst_portrange = expand_udp_dst_portrange, + icmp = function(expr) return has_ip_protocol(PROTO_ICMP) end, + icmp6 = function(expr) return has_ipv6_protocol(PROTO_ICMP6) end, + igmp = function(expr) return has_ip_protocol(PROTO_IGMP) end, + igrp = function(expr) return has_ip_protocol(PROTO_IGRP) end, + pim = function(expr) return has_ip_protocol(PROTO_PIM) end, + ah = function(expr) return has_ip_protocol(PROTO_AH) end, + esp = function(expr) return has_ip_protocol(PROTO_ESP) end, + vrrp = function(expr) return has_ip_protocol(PROTO_VRRP) end, + sctp = function(expr) return has_ip_protocol(PROTO_SCTP) end, + protochain = expand_ip_protochain, + arp = expand_arp, + arp_host = expand_arp_host, + arp_src = expand_arp_src_host, + arp_src_host = expand_arp_src_host, + arp_dst = expand_arp_dst_host, + arp_dst_host = expand_arp_dst_host, + rarp = expand_rarp, + rarp_host = expand_rarp_host, + rarp_src = expand_rarp_src_host, + rarp_src_host = expand_rarp_src_host, + rarp_dst = expand_rarp_dst_host, + rarp_dst_host = expand_rarp_dst_host, + atalk = expand_atalk, + aarp = expand_aarp, + decnet = expand_decnet, + decnet_src = expand_decnet_src, + decnet_src_host = expand_decnet_src, + decnet_dst = expand_decnet_dst, + decnet_dst_host = expand_decnet_dst, + decnet_host = expand_decnet_host, + iso = expand_iso, + stp = expand_stp, + ipx = expand_ipx, + netbeui = expand_netbeui, + sca = expand_sca, + lat = expand_lat, + moprc = expand_moprc, + mopdl = expand_mopdl, + llc = unimplemented, + ifname = unimplemented, + on = unimplemented, + rnr = unimplemented, + rulenum = unimplemented, + reason = unimplemented, + rset = unimplemented, + ruleset = unimplemented, + srnr = unimplemented, + subrulenum = unimplemented, + action = unimplemented, + wlan_ra = unimplemented, + wlan_ta = unimplemented, + wlan_addr1 = unimplemented, + wlan_addr2 = unimplemented, + wlan_addr3 = unimplemented, + wlan_addr4 = unimplemented, + type = unimplemented, + type_subtype = unimplemented, + subtype = unimplemented, + dir = unimplemented, + vlan = unimplemented, + mpls = unimplemented, + pppoed = unimplemented, + pppoes = unimplemented, + iso_proto = expand_iso_proto, + clnp = function(expr) return has_iso_protocol(PROTO_CLNP) end, + esis = function(expr) return has_iso_protocol(PROTO_ESIS) end, + isis = function(expr) return has_iso_protocol(PROTO_ISIS) end, + l1 = expand_l1, + l2 = expand_l2, + iih = expand_iih, + lsp = expand_lsp, + snp = expand_snp, + csnp = expand_csnp, + psnp = expand_psnp, + vpi = unimplemented, + vci = unimplemented, + lane = unimplemented, + oamf4s = unimplemented, + oamf4e = unimplemented, + oamf4 = unimplemented, + oam = unimplemented, + metac = unimplemented, + bcc = unimplemented, + sc = unimplemented, + ilmic = unimplemented, + connectmsg = unimplemented, + metaconnect = unimplemented +} + +local relops = set('<', '<=', '=', '!=', '>=', '>') + +local addressables = set( + 'arp', 'rarp', 'wlan', 'ether', 'fddi', 'tr', 'ppp', + 'slip', 'link', 'radio', 'ip', 'ip6', 'tcp', 'udp', 'icmp' +) + +local binops = set( + '+', '-', '*', '*64', '/', '&', '|', '^', '&&', '||', '<<', '>>' +) +local associative_binops = set( + '+', '*', '*64', '&', '|', '^' +) +local bitops = set('&', '|', '^') +local unops = set('ntohs', 'ntohl', 'uint32') +local leaf_primitives = set( + 'true', 'false', 'fail' +) + +local function expand_offset(level, dlt) + assert(dlt == "EN10MB", "Encapsulation other than EN10MB unimplemented") + local function guard_expr(expr) + local test, guards = expand_relop(expr, dlt) + return concat(guards, { { test, { 'false' } } }) + end + local function guard_ether_protocol(proto) + return concat(guard_expr(has_ether_protocol(proto)), + guard_expr(has_ether_protocol_min_payload(proto))) + end + function guard_ipv4_protocol(proto) + return concat(guard_expr(has_ipv4_protocol(proto)), + guard_expr(has_ipv4_protocol_min_payload(proto))) + end + function guard_ipv6_protocol(proto) + return concat(guard_expr(has_ipv6_protocol(proto)), + guard_expr(has_ipv6_protocol_min_payload(proto))) + end + function guard_first_ipv4_fragment() + return guard_expr(is_first_ipv4_fragment()) + end + function ipv4_payload_offset(proto) + local ip_offset, guards = expand_offset('ip', dlt) + if proto then + guards = concat(guards, guard_ipv4_protocol(proto)) + end + guards = concat(guards, guard_first_ipv4_fragment()) + local res = { '+', + { '<<', { '&', { '[]', ip_offset, 1 }, 0xf }, 2 }, + ip_offset } + return res, guards + end + function ipv6_payload_offset(proto) + local ip_offset, guards = expand_offset('ip6', dlt) + if proto then + guards = concat(guards, guard_ipv6_protocol(proto)) + end + return { '+', ip_offset, 40 }, guards + end + + -- Note that unlike their corresponding predicates which detect + -- either IPv4 or IPv6 traffic, [icmp], [udp], and [tcp] only work + -- for IPv4. + if level == 'ether' then + return 0, {} + elseif level == 'ether*' then + return ETHER_PAYLOAD, {} + elseif level == 'arp' then + return ETHER_PAYLOAD, guard_ether_protocol(PROTO_ARP) + elseif level == 'rarp' then + return ETHER_PAYLOAD, guard_ether_protocol(PROTO_RARP) + elseif level == 'ip' then + return ETHER_PAYLOAD, guard_ether_protocol(PROTO_IPV4) + elseif level == 'ip6' then + return ETHER_PAYLOAD, guard_ether_protocol(PROTO_IPV6) + elseif level == 'ip*' then + return ipv4_payload_offset() + elseif level == 'ip6*' then + return ipv6_payload_offset() + elseif level == 'icmp' then + return ipv4_payload_offset(PROTO_ICMP) + elseif level == 'udp' then + return ipv4_payload_offset(PROTO_UDP) + elseif level == 'tcp' then + return ipv4_payload_offset(PROTO_TCP) + elseif level == 'igmp' then + return ipv4_payload_offset(PROTO_IGMP) + elseif level == 'igrp' then + return ipv4_payload_offset(PROTO_IGRP) + elseif level == 'pim' then + return ipv4_payload_offset(PROTO_PIM) + elseif level == 'sctp' then + return ipv4_payload_offset(PROTO_SCTP) + elseif level == 'vrrp' then + return ipv4_payload_offset(PROTO_VRRP) + end + error('invalid level '..level) +end + +-- Returns two values: the expanded arithmetic expression and an ordered +-- list of guards. A guard is a two-element array whose first element +-- is a test expression. If all test expressions of the guards are +-- true, then it is valid to evaluate the arithmetic expression. The +-- second element of the guard array is the expression to which the +-- relop will evaluate if the guard expression fails: either { 'false' } +-- or { 'fail' }. +function expand_arith(expr, dlt) + assert(expr) + if type(expr) == 'number' or expr == 'len' then return expr, {} end + + local op = expr[1] + if binops[op] then + -- Use 64-bit multiplication by default. The optimizer will + -- reduce this back to Lua's normal float multiplication if it + -- can. + if op == '*' then op = '*64' end + local lhs, lhs_guards = expand_arith(expr[2], dlt) + local rhs, rhs_guards = expand_arith(expr[3], dlt) + -- Mod 2^32 to preserve uint32 range. + local ret = { 'uint32', { op, lhs, rhs } } + local guards = concat(lhs_guards, rhs_guards) + -- RHS of division can't be 0. + if op == '/' then + local div_guard = { { '!=', rhs, 0 }, { 'fail' } } + guards = concat(guards, { div_guard }) + end + return ret, guards + end + + local is_addr = false + if op == 'addr' then + is_addr = true + expr = expr[2] + op = expr[1] + end + assert(op ~= '[]', "expr has already been expanded?") + local addressable = assert(op:match("^%[(.+)%]$"), "bad addressable") + local offset, offset_guards = expand_offset(addressable, dlt) + local lhs, lhs_guards = expand_arith(expr[2], dlt) + local size = expr[3] + local len_test = { '<=', { '+', { '+', offset, lhs }, size }, 'len' } + -- ip[100000] will abort the whole matcher. &ip[100000] will just + -- cause the clause to fail to match. + local len_guard = { len_test, is_addr and { 'false' } or { 'fail' } } + local guards = concat(concat(offset_guards, lhs_guards), { len_guard }) + local addr = { '+', offset, lhs } + if is_addr then return addr, guards end + local ret = { '[]', addr, size } + if size == 1 then return ret, guards end + if size == 2 then return { 'ntohs', ret }, guards end + if size == 4 then return { 'uint32', { 'ntohl', ret } }, guards end + error('unreachable') +end + +function expand_relop(expr, dlt) + local lhs, lhs_guards = expand_arith(expr[2], dlt) + local rhs, rhs_guards = expand_arith(expr[3], dlt) + return { expr[1], lhs, rhs }, concat(lhs_guards, rhs_guards) +end + +function expand_bool(expr, dlt) + assert(type(expr) == 'table', 'logical expression must be a table') + if expr[1] == 'not' or expr[1] == '!' then + return { 'if', expand_bool(expr[2], dlt), { 'false' }, { 'true' } } + elseif expr[1] == 'and' or expr[1] == '&&' then + return { 'if', expand_bool(expr[2], dlt), + expand_bool(expr[3], dlt), + { 'false' } } + elseif expr[1] == 'or' or expr[1] == '||' then + return { 'if', expand_bool(expr[2], dlt), + { 'true' }, + expand_bool(expr[3], dlt) } + elseif relops[expr[1]] then + -- An arithmetic relop. + local res, guards = expand_relop(expr, dlt) + -- We remove guards in LIFO order, resulting in an expression + -- whose first guard expression is the first one that was added. + while #guards ~= 0 do + local guard = table.remove(guards) + assert(guard[2]) + res = { 'if', guard[1], res, guard[2] } + end + return res + elseif expr[1] == 'if' then + return { 'if', + expand_bool(expr[2], dlt), + expand_bool(expr[3], dlt), + expand_bool(expr[4], dlt) } + elseif leaf_primitives[expr[1]] then + return expr + else + -- A logical primitive. + local expander = primitive_expanders[expr[1]] + assert(expander, "unimplemented primitive: "..expr[1]) + local expanded = expander(expr, dlt) + return expand_bool(expander(expr, dlt), dlt) + end +end + +function expand(expr, dlt) + dlt = dlt or 'RAW' + expr = expand_bool(expr, dlt) + if verbose then pp(expr) end + return expr +end + +function selftest () + print("selftest: pf.expand") + local parse = require('pf.parse').parse + local equals, assert_equals = utils.equals, utils.assert_equals + assert_equals({ '=', 1, 2 }, + expand(parse("1 = 2"), 'EN10MB')) + assert_equals({ '=', 1, "len" }, + expand(parse("1 = len"), 'EN10MB')) + assert_equals({ 'if', + { '!=', 2, 0}, + { '=', 1, { 'uint32', { '/', 2, 2} } }, + { 'fail'} }, + expand(parse("1 = 2/2"), 'EN10MB')) + assert_equals({ 'if', + { '<=', { '+', { '+', 0, 0 }, 1 }, 'len'}, + { '=', { '[]', { '+', 0, 0 }, 1 }, 2 }, + { 'fail' } }, + expand(parse("ether[0] = 2"), 'EN10MB')) + -- Could check this, but it's very large + expand(parse("tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)"), + "EN10MB") + print("OK") +end diff --git a/src/pf/libpcap.lua b/src/pf/libpcap.lua new file mode 100644 index 0000000000..8e17fd98c4 --- /dev/null +++ b/src/pf/libpcap.lua @@ -0,0 +1,87 @@ +module(...,package.seeall) + +local ffi = require("ffi") +local types = require("pf.types") -- Load FFI declarations. +local pcap -- The pcap library, lazily loaded. + +local verbose = os.getenv("PF_VERBOSE"); + +local MAX_UINT32 = 0xffffffff + +ffi.cdef[[ +typedef struct pcap pcap_t; +const char *pcap_lib_version(void); +int pcap_datalink_name_to_val(const char *name); +pcap_t *pcap_open_dead(int linktype, int snaplen); +char *pcap_geterr(pcap_t *p); +void pcap_perror(pcap_t *p, const char *prefix); +int pcap_compile(pcap_t *p, struct bpf_program *fp, const char *str, + int optimize, uint32_t netmask); +int pcap_offline_filter(const struct bpf_program *fp, + const struct pcap_pkthdr *h, const uint8_t *pkt); +]] + +function offline_filter(bpf, hdr, pkt) + if not pcap then pcap = ffi.load("pcap") end + return pcap.pcap_offline_filter(bpf, hdr, pkt) +end + +-- The dlt_name is a "datalink type name" and specifies the link-level +-- wrapping to expect. E.g., for raw ethernet frames, you would specify +-- "EN10MB" (even though you have a 10G card), which corresponds to the +-- numeric DLT_EN10MB value from pcap/bpf.h. See +-- http://www.tcpdump.org/linktypes.html for more details on possible +-- names. +-- +-- You probably want "RAW" for raw IP (v4 or v6) frames. If you don't +-- supply a dlt_name, "RAW" is the default. +function compile(filter_str, dlt_name, optimize) + if verbose then print(filter_str) end + if not pcap then pcap = ffi.load("pcap") end + + dlt_name = dlt_name or "RAW" + local dlt = pcap.pcap_datalink_name_to_val(dlt_name) + assert(dlt >= 0, "bad datalink type name " .. dlt_name) + local snaplen = 65535 -- Maximum packet size. + local p = pcap.pcap_open_dead(dlt, snaplen) + + assert(p, "pcap_open_dead failed") + + -- pcap_compile + local bpf = types.bpf_program() + local netmask = MAX_UINT32 + if optimize == nil then optimize = true end -- backwards compatibility + local err = pcap.pcap_compile(p, bpf, filter_str, optimize, netmask) + + if err ~= 0 then + local reason = ffi.string(pcap.pcap_geterr(p)) + -- Instead of failing fast, return a filter which rejects all packets + if reason == "expression rejects all packets" then + -- construct the always-rejects filter, which is like the always-true + -- filter, except for its return code + pcap.pcap_compile(p, bpf, "1=1", true, netmask) + bpf.bf_insns[0].k = 0 -- 0 = false, 2^16-1 = true + else + pcap.pcap_perror(p, "pcap_compile failed!") + error("pcap_compile failed") + end + end + + return bpf +end + +function pcap_version() + if not pcap then pcap = ffi.load("pcap") end + return ffi.string(pcap.pcap_lib_version()) +end + +function selftest () + print("selftest: pf.libpcap") + + compile("", "EN10MB") + compile("ip", "EN10MB") + compile("tcp", "EN10MB") + compile("tcp port 80", "EN10MB") + + print("OK") +end diff --git a/src/pf/match.lua b/src/pf/match.lua new file mode 100644 index 0000000000..f18a1b2097 --- /dev/null +++ b/src/pf/match.lua @@ -0,0 +1,366 @@ +module(...,package.seeall) + +--- +--- Program := 'match' Cond +--- Cond := '{' Clause... '}' +--- Clause := Test '=>' Dispatch [ClauseTerminator] +-- Test := 'otherwise' | LogicalExpression +--- ClauseTerminator := ',' | ';' +--- Dispatch := Call | Cond +--- Call := Identifier Args? +--- Args := '(' [ ArithmeticExpression [ ',' ArithmeticExpression ] ] ')' +--- +--- LogicalExpression and ArithmeticExpression are embedded productions +--- of pflang. 'otherwise' is a Test that always matches. +--- +--- Comments are prefixed by '--' and continue to the end of the line. +--- +--- Compiling a Program produces a Matcher. A Matcher is a function of +--- three arguments: a handlers table, the packet data as a uint8_t*, +--- and the packet length in bytes. +--- +--- Calling a Matcher will either result in a tail call to a member +--- function of the handlers table, or return nil if no dispatch +--- matches. +--- +--- A Call matches if all of the conditions necessary to evaluate the +--- arithmetic expressions in its arguments are true. (For example, the +--- argument handle(ip[42]) is only valid if the packet is an IPv4 +--- packet of a sufficient length.) +--- +--- A Cond always matches; once you enter a Cond, no clause outside the +--- Cond will match. If no clause in the Cond matches, the result is +--- nil. +--- +--- A Clause matches if the Test on the left-hand-side of the arrow is +--- true. If the right-hand-side is a call, the conditions from the +--- call arguments (if any) are implicitly added to the Test on the +--- left. In this way it's possible for the Test to be true but some +--- condition from the Call to be false, which causes the match to +--- proceed with the next Clause. +--- +--- Unlike pflang, attempting to access out-of-bounds packet data merely +--- causes a clause not to match, instead of immediately aborting the +--- match. +--- + +local utils = require('pf.utils') +local parse_pflang = require('pf.parse').parse +local expand_pflang = require('pf.expand').expand +local optimize = require('pf.optimize') +local anf = require('pf.anf') +local ssa = require('pf.ssa') +local backend = require('pf.backend') + +local function split(str, pat) + pat = '()'..pat..'()' + local ret, start_pos = {}, 1 + local tok_pos, end_pos = str:match(pat) + while tok_pos do + table.insert(ret, str:sub(start_pos, tok_pos - 1)) + start_pos = end_pos + tok_pos, end_pos = str:match(pat, start_pos) + end + table.insert(ret, str:sub(start_pos)) + return ret +end + +local function remove_comments(str) + local lines = split(str, '\n') + for i=1,#lines do + local line = lines[i] + local comment = line:match('()%-%-') + if comment then lines[i] = line:sub(1, comment - 1) end + end + return table.concat(lines, '\n') +end + +-- Return line, line number, column number. +local function error_location(str, pos) + local start, count = 1, 1 + local stop = str:match('()\n', start) + while stop and stop < pos do + start, stop = stop + 1, str:match('()\n', stop + 1) + count = count + 1 + end + if stop then stop = stop - 1 end + return str:sub(start, stop), count, pos - start + 1 +end + +local function scanner(str) + str = remove_comments(str) + local pos = 1 + local function error_str(message, ...) + local line, line_number, column_number = error_location(str, pos) + local message = "\npfmatch: syntax error:%d:%d: "..message..'\n' + local result = message:format(line_number, column_number, ...) + result = result..line.."\n" + result = result..string.rep(" ", column_number-1).."^".."\n" + return result + end + local primitive_error = error + local function error(message, ...) + primitive_error(error_str(message, ...)) + end + + local function skip_whitespace() + pos = str:match('^%s*()', pos) + end + local function peek(pat) + skip_whitespace() + return str:match('^'..pat, pos) + end + local function check(pat) + skip_whitespace() + local start_pos, end_pos = pos, peek(pat.."()") + if not end_pos then return nil end + pos = end_pos + return str:sub(start_pos, end_pos - 1) + end + local function next_identifier() + local id = check('[%a_][%w_]*') + if not id then error('expected an identifier') end + return id + end + local function next_balanced(pair) + local tok = check('%b'..pair) + if not tok then error("expected balanced '%s'", pair) end + return tok:sub(2, #tok - 1) + end + local function consume(pat) + if not check(pat) then error("expected pattern '%s'", pat) end + end + local function consume_until(pat) + skip_whitespace() + local start_pos, end_pos, next_pos = pos, str:match("()"..pat.."()", pos) + if not next_pos then error("expected pattern '%s'") end + pos = next_pos + return str:sub(start_pos, end_pos - 1) + end + local function done() + skip_whitespace() + return pos == #str + 1 + end + return { + error = error, + peek = peek, + check = check, + next_identifier = next_identifier, + next_balanced = next_balanced, + consume = consume, + consume_until = consume_until, + done = done + } +end + +local parse_dispatch + +local function parse_call(scanner) + local proc = scanner.next_identifier() + if not proc then scanner.error('expected a procedure call') end + local result = { 'call', proc } + if scanner.peek('%(') then + local args_str = scanner.next_balanced('()') + if not args_str:match('^%s*$') then + local args = split(args_str, ',') + for i=1,#args do + table.insert(result, parse_pflang(args[i], {arithmetic=true})) + end + end + end + return result +end + +local function parse_cond(scanner) + local res = { 'cond' } + while not scanner.check('}') do + local test + if scanner.check('otherwise') then + test = { 'true' } + scanner.consume('=>') + else + test = parse_pflang(scanner.consume_until('=>')) + end + local consequent = parse_dispatch(scanner) + scanner.check('[,;]') + table.insert(res, { test, consequent }) + end + return res +end + +function parse_dispatch(scanner) + if scanner.check('{') then return parse_cond(scanner) end + return parse_call(scanner) +end + +local function subst(str, values) + local out, pos = '', 1 + while true do + local before, after = str:match('()%$[%w_]+()', pos) + if not before then return out..str:sub(pos) end + out = out..str:sub(pos, before - 1) + local var = str:sub(before + 1, after - 1) + local val = values[var] + if not val then error('var not found: '..var) end + out = out..val + pos = after + end + return out +end + +local function parse(str) + local scanner = scanner(str) + scanner.consume('match') + scanner.consume('{') + local cond = parse_cond(scanner) + if not scanner.done() then scanner.error("unexpected token") end + return cond +end + +local function expand_arg(arg, dlt) + -- The argument is an arithmetic expression, but the pflang expander + -- expects a logical expression. Wrap in a dummy comparison, then + -- tease apart the conditions and the arithmetic expression. + local expr = expand_pflang({ '=', arg, 0 }, dlt) + local conditions = {} + while expr[1] == 'if' do + table.insert(conditions, expr[2]) + assert(type(expr[4]) == 'table') + assert(expr[4][1] == 'fail' or expr[4][1] == 'false') + expr = expr[3] + end + assert(expr[1] == '=' and expr[3] == 0) + return conditions, expr[2] +end + +local function expand_call(expr, dlt) + local conditions = {} + local res = { expr[1], expr[2] } + for i=3,#expr do + local arg_conditions, arg = expand_arg(expr[i], dlt) + conditions = utils.concat(conditions, arg_conditions) + table.insert(res, arg) + end + local test = { 'true' } + -- Preserve left-to-right order of conditions. + while #conditions ~= 0 do + test = { 'if', table.remove(conditions), test, { 'false' } } + end + return test, res +end + +local expand_cond + +-- Unlike pflang, out-of-bounds and such just cause the clause to fail, +-- not the whole program. +local function replace_fail(expr) + if type(expr) ~= 'table' then return expr + elseif expr[1] == 'fail' then return { 'false' } + elseif expr[1] == 'if' then + local test = replace_fail(expr[2]) + local consequent = replace_fail(expr[3]) + local alternate = replace_fail(expr[4]) + return { 'if', test, consequent, alternate } + else + return expr + end +end + +local function expand_clause(test, consequent, dlt) + test = replace_fail(expand_pflang(test, dlt)) + if consequent[1] == 'call' then + local conditions, call = expand_call(consequent, dlt) + return { 'if', test, conditions, { 'false' } }, call + else + assert(consequent[1] == 'cond') + return test, expand_cond(consequent, dlt) + end +end + +function expand_cond(expr, dlt) + local res = { 'false' } + for i=#expr,2,-1 do + local clause = expr[i] + local test, consequent = expand_clause(clause[1], clause[2], dlt) + res = { 'if', test, consequent, res } + end + return res +end + +local function expand(expr, dlt) + return expand_cond(expr, dlt) +end + +local compile_defaults = { + dlt='EN10MB', optimize=true, source=false, subst=false +} + +function compile(str, opts) + opts = utils.parse_opts(opts or {}, compile_defaults) + if opts.subst then str = subst(str, opts.subst) end + local expr = expand(parse(str), opts.dlt) + if opts.optimize then expr = optimize.optimize(expr) end + expr = anf.convert_anf(expr) + expr = ssa.convert_ssa(expr) + if opts.source then return backend.emit_match_lua(expr) end + return backend.emit_and_load_match(expr, filter_str) +end + +function selftest() + print("selftest: pf.match") + local function test(str, expr) + utils.assert_equals(expr, parse(str)) + end + test("match {}", { 'cond' }) + test("match--comment\n{}", { 'cond' }) + test(" match \n { } ", { 'cond' }) + test("match{}", { 'cond' }) + test("match { otherwise => x() }", + { 'cond', { { 'true' }, { 'call', 'x' } } }) + test("match { otherwise => x(1) }", + { 'cond', { { 'true' }, { 'call', 'x', 1 } } }) + test("match { otherwise => x(1&1) }", + { 'cond', { { 'true' }, { 'call', 'x', { '&', 1, 1 } } } }) + test("match { otherwise => x(ip[42]) }", + { 'cond', { { 'true' }, { 'call', 'x', { '[ip]', 42, 1 } } } }) + test("match { otherwise => x(ip[42], 10) }", + { 'cond', { { 'true' }, { 'call', 'x', { '[ip]', 42, 1 }, 10 } } }) + test(subst("match { otherwise => x(ip[$loc], 10) }", {loc=42}), + { 'cond', { { 'true' }, { 'call', 'x', { '[ip]', 42, 1 }, 10 } } }) + + local function test(str, expr) + utils.assert_equals(expr, expand(parse(str), 'EN10MB')) + end + test("match { otherwise => x() }", + { 'if', { 'if', { 'true' }, { 'true' }, { 'false' } }, + { 'call', 'x' }, + { 'false' } }) + test("match { otherwise => x(1) }", + { 'if', { 'if', { 'true' }, { 'true' }, { 'false' } }, + { 'call', 'x', 1 }, + { 'false' } }) + test("match { otherwise => x(1/0) }", + { 'if', { 'if', { 'true' }, + { 'if', { '!=', 0, 0 }, { 'true' }, { 'false' } }, + { 'false' } }, + { 'call', 'x', { 'uint32', { '/', 1, 0 } } }, + { 'false' } }) + + local function test(str, expr) + utils.assert_equals(expr, optimize.optimize(expand(parse(str), 'EN10MB'))) + end + test("match { otherwise => x() }", + { 'call', 'x' }) + test("match { otherwise => x(1) }", + { 'call', 'x', 1 }) + test("match { otherwise => x(1/0) }", + { 'fail' }) + + local function test(str, expr) + -- Just a test to see if it works without errors. + compile(str) + end + test("match { tcp port 80 => pass }") + + print("OK") +end diff --git a/src/pf/optimize.lua b/src/pf/optimize.lua new file mode 100644 index 0000000000..d7fd92d40f --- /dev/null +++ b/src/pf/optimize.lua @@ -0,0 +1,857 @@ +module(...,package.seeall) + +local bit = require('bit') +local utils = require('pf.utils') + +local verbose = os.getenv("PF_VERBOSE"); + +local expand_arith, expand_relop, expand_bool + +local set, concat, dup, pp = utils.set, utils.concat, utils.dup, utils.pp + +-- Pflang's numbers are unsigned 32-bit integers, but sometimes we use +-- negative numbers because the bitops module prefers them. +local UINT32_MAX = 2^32-1 +local INT32_MAX = 2^31-1 +local INT32_MIN = -2^31 +local UINT16_MAX = 2^16-1 + +-- We use use Lua arithmetic to implement pflang operations, so +-- intermediate results can exceed the int32|uint32 range. Those +-- intermediate results are then clamped back to the range with the +-- 'int32' or 'uint32' operations. Multiplication is clamped internally +-- by the '*64' operation. We'll never see a value outside this range. +local INT_MAX = UINT32_MAX + UINT32_MAX +local INT_MIN = INT32_MIN + INT32_MIN + +local relops = set('<', '<=', '=', '!=', '>=', '>') + +local binops = set( + '+', '-', '*', '*64', '/', '&', '|', '^', '<<', '>>' +) +local associative_binops = set( + '+', '*', '*64', '&', '|', '^' +) +local bitops = set('&', '|', '^') +local unops = set('ntohs', 'ntohl', 'uint32', 'int32') +-- ops that produce results of known types +local int32ops = set('&', '|', '^', 'ntohs', 'ntohl', '<<', '>>', 'int32') +local uint32ops = set('uint32', '[]') +-- ops that coerce their arguments to be within range +local coerce_ops = set('&', '|', '^', 'ntohs', 'ntohl', '<<', '>>', 'int32', + 'uint32') + +local folders = { + ['+'] = function(a, b) return a + b end, + ['-'] = function(a, b) return a - b end, + ['*'] = function(a, b) return a * b end, + ['*64'] = function(a, b) return tonumber((a * 1LL * b) % 2^32) end, + ['/'] = function(a, b) + -- If the denominator is zero, the code is unreachable, so it + -- doesn't matter what we return. + if b == 0 then return 0 end + return math.floor(a / b) + end, + ['&'] = function(a, b) return bit.band(a, b) end, + ['^'] = function(a, b) return bit.bxor(a, b) end, + ['|'] = function(a, b) return bit.bor(a, b) end, + ['<<'] = function(a, b) return bit.lshift(a, b) end, + ['>>'] = function(a, b) return bit.rshift(a, b) end, + ['ntohs'] = function(a) return bit.rshift(bit.bswap(a), 16) end, + ['ntohl'] = function(a) return bit.bswap(a) end, + ['uint32'] = function(a) return a % 2^32 end, + ['int32'] = function(a) return bit.tobit(a) end, + ['='] = function(a, b) return a == b end, + ['!='] = function(a, b) return a ~= b end, + ['<'] = function(a, b) return a < b end, + ['<='] = function(a, b) return a <= b end, + ['>='] = function(a, b) return a >= b end, + ['>'] = function(a, b) return a > b end +} + +local cfkey_cache, cfkey = {}, nil + +local function memoize(f) + return function (arg) + local result = cfkey_cache[arg] + if result == nil then + result = f(arg) + cfkey_cache[arg] = result + end + return result + end +end + +local function clear_cache() + cfkey_cache = {} +end + +cfkey = memoize(function (expr) + if type(expr) == 'table' then + local parts = {'('} + for i=1,#expr do + parts[i+1] = cfkey(expr[i]) + end + parts[#parts+1] = ')' + return table.concat(parts, " ") + else + return expr + end +end) + +-- A simple expression can be duplicated. FIXME: Some calls are simple, +-- some are not. For now our optimizations don't work very well if we +-- don't allow duplication though. +local simple = set('true', 'false', 'match', 'fail', 'call') +local tailops = set('fail', 'match', 'call') +local trueops = set('match', 'call', 'true') + +local commute = { + ['<']='>', ['<=']='>=', ['=']='=', ['!=']='!=', ['>=']='<=', ['>']='<' +} + +local function try_invert(relop, expr, C) + assert(type(C) == 'number' and type(expr) ~= 'number') + local op = expr[1] + local is_eq = relop == '=' or relop == '!=' + if op == 'ntohl' and is_eq then + local rhs = expr[2] + if int32ops[rhs[1]] then + assert(INT32_MIN <= C and C <= INT32_MAX) + -- ntohl(INT32) = C => INT32 = ntohl(C) + return relop, rhs, assert(folders[op])(C) + elseif uint32ops[rhs[1]] then + -- ntohl(UINT32) = C => UINT32 = uint32(ntohl(C)) + return relop, rhs, assert(folders[op])(C) % 2^32 + end + elseif op == 'ntohs' and is_eq then + local rhs = expr[2] + if ((rhs[1] == 'ntohs' or (rhs[1] == '[]' and rhs[3] <= 2)) + and 0 <= C and C <= UINT16_MAX) then + -- ntohs(UINT16) = C => UINT16 = ntohs(C) + return relop, rhs, assert(folders[op])(C) + end + elseif op == 'uint32' and is_eq then + local rhs = expr[2] + if int32ops[rhs[1]] then + -- uint32(INT32) = C => INT32 = int32(C) + return relop, rhs, bit.tobit(C) + end + elseif op == 'int32' and is_eq then + local rhs = expr[2] + if uint32ops[rhs[1]] then + -- int32(UINT32) = C => UINT32 = uint32(C) + return relop, rhs, C ^ 2^32 + end + elseif bitops[op] and is_eq then + local lhs, rhs = expr[2], expr[3] + if type(lhs) == 'number' and rhs[1] == 'ntohl' then + -- bitop(C, ntohl(X)) = C => bitop(ntohl(C), X) = ntohl(C) + local swap = assert(folders[rhs[1]]) + return relop, { op, swap(lhs), rhs[2] }, swap(C) + elseif type(rhs) == 'number' and lhs[1] == 'ntohl' then + -- bitop(ntohl(X), C) = C => bitop(X, ntohl(C)) = ntohl(C) + local swap = assert(folders[lhs[1]]) + return relop, { op, lhs[2], swap(rhs) }, swap(C) + elseif op == '&' then + if type(lhs) == 'number' then lhs, rhs = rhs, lhs end + if (type(lhs) == 'table' and lhs[1] == 'ntohs' + and type(rhs) == 'number' and 0 <= C and C <= UINT16_MAX) then + -- ntohs(X) & C = C => X & ntohs(C) = ntohs(C) + local swap = assert(folders[lhs[1]]) + return relop, { op, lhs[2], swap(rhs) }, swap(C) + end + end + end + return relop, expr, C +end + +local simplify_if + +local function simplify(expr, is_tail) + if type(expr) ~= 'table' then return expr end + local op = expr[1] + local function decoerce(expr) + if (type(expr) == 'table' + and (expr[1] == 'uint32' or expr[1] == 'int32')) then + return expr[2] + end + return expr + end + if binops[op] then + local lhs = simplify(expr[2]) + local rhs = simplify(expr[3]) + if type(lhs) == 'number' and type(rhs) == 'number' then + return assert(folders[op])(lhs, rhs) + elseif associative_binops[op] then + -- Try to make the right operand a number. + if type(lhs) == 'number' then + lhs, rhs = rhs, lhs + end + if type(lhs) == 'table' and lhs[1] == op and type(lhs[3]) == 'number' then + if type(rhs) == 'number' then + -- (A op N1) op N2 -> A op (N1 op N2) + return { op, lhs[2], assert(folders[op])(lhs[3], rhs) } + elseif type(rhs) == 'table' and rhs[1] == op and type(rhs[3]) == 'number' then + -- (A op N1) op (B op N2) -> (A op B) op (N1 op N2) + return { op, { op, lhs[2], rhs[2] }, assert(folders[op])(lhs[3], rhs[3]) } + else + -- (A op N) op X -> (A op X) op N + return { op, { op, lhs[2], rhs }, lhs[3] } + end + elseif type(rhs) == 'table' and rhs[1] == op and type(rhs[3]) == 'number' then + -- X op (A op N) -> (X op A) op N + return { op, { op, lhs, rhs[2]}, rhs[3] } + end + if coerce_ops[op] then lhs, rhs = decoerce(lhs), decoerce(rhs) end + end + return { op, lhs, rhs } + elseif unops[op] then + local rhs = simplify(expr[2]) + if type(rhs) == 'number' then return assert(folders[op])(rhs) end + if op == 'int32' and int32ops[rhs[1]] then return rhs end + if op == 'uint32' and uint32ops[rhs[1]] then return rhs end + if coerce_ops[op] then rhs = decoerce(rhs) end + return { op, rhs } + elseif relops[op] then + local lhs = simplify(expr[2]) + local rhs = simplify(expr[3]) + if type(lhs) == 'number' then + if type(rhs) == 'number' then + return { assert(folders[op])(lhs, rhs) and 'true' or 'false' } + end + op, lhs, rhs = try_invert(assert(commute[op]), rhs, lhs) + elseif type(rhs) == 'number' then + op, lhs, rhs = try_invert(op, lhs, rhs) + end + return { op, lhs, rhs } + elseif op == 'if' then + local test = simplify(expr[2]) + local t, f = simplify(expr[3], is_tail), simplify(expr[4], is_tail) + return simplify_if(test, t, f) + elseif op == 'call' then + local ret = { 'call', expr[2] } + for i=3,#expr do + table.insert(ret, simplify(expr[i])) + end + return ret + else + if op == 'match' or op == 'fail' then return expr end + if op == 'true' then + if is_tail then return { 'match' } end + return expr + end + if op == 'false' then + if is_tail then return { 'fail' } end + return expr + end + assert(op == '[]' and #expr == 3) + return { op, simplify(expr[2]), expr[3] } + end +end + +function simplify_if(test, t, f) + local op = test[1] + if op == 'true' then return t + elseif op == 'false' then return f + elseif tailops[op] then return test + elseif t[1] == 'true' and f[1] == 'false' then return test + elseif t[1] == 'match' and f[1] == 'fail' then return test + elseif t[1] == 'fail' and f[1] == 'fail' then return { 'fail' } + elseif op == 'if' then + if tailops[test[3][1]] then + -- if (if A tail B) C D -> if A tail (if B C D) + return simplify_if(test[2], test[3], simplify_if(test[4], t, f)) + elseif tailops[test[4][1]] then + -- if (if A B tail) C D -> if A (if B C D) tail + return simplify_if(test[2], simplify_if(test[3], t, f), test[4]) + elseif test[3][1] == 'false' and test[4][1] == 'true' then + -- if (if A false true) C D -> if A D C + return simplify_if(test[2], f, t) + end + if t[1] == 'if' and cfkey(test[2]) == cfkey(t[2]) then + if f[1] == 'if' and cfkey(test[2]) == cfkey(f[2]) then + -- if (if A B C) (if A D E) (if A F G) + -- -> if A (if B D F) (if C E G) + return simplify_if(test[2], + simplify_if(test[3], t[3], f[3]), + simplify_if(test[4], t[4], f[4])) + elseif simple[f[1]] then + -- if (if A B C) (if A D E) F + -- -> if A (if B D F) (if C E F) + return simplify_if(test[2], + simplify_if(test[3], t[3], f), + simplify_if(test[4], t[4], f)) + end + end + if f[1] == 'if' then + if cfkey(test[2]) == cfkey(f[2]) and simple[t[1]] then + -- if (if A B C) D (if A E F) + -- -> if A (if B D E) (if C D F) + return simplify_if(test[2], + simplify_if(test[3], t, f[3]), + simplify_if(test[4], t, f[4])) + elseif (test[4][1] == 'false' + and f[2][1] == 'if' and f[2][4][1] == 'false' + and simple[f[4][1]] + and cfkey(test[2]) == cfkey(f[2][2])) then + -- if (if T A false) B (if (if T C false) D E) + -- -> if T (if A B (if C D E)) E + local T, A, B, C, D, E = test[2], test[3], t, f[2][3], f[3], f[4] + return simplify_if(T, simplify_if(A, B, simplify_if(C, D, E)), E) + end + end + end + if f[1] == 'if' and cfkey(t) == cfkey(f[3]) and not simple[t[1]] then + -- if A B (if C B D) -> if (if A true C) B D + return simplify_if(simplify_if(test, { 'true' }, f[2]), t, f[4]) + end + if t[1] == 'if' and cfkey(f) == cfkey(t[4]) and not simple[f[1]] then + -- if A (if B C D) D -> if (if A B false) C D + return simplify_if(simplify_if(test, t[2], { 'false' }), t[3], f) + end + return { 'if', test, t, f } +end + +-- Conditional folding. +local function cfold(expr, db) + if type(expr) ~= 'table' then return expr end + local op = expr[1] + if binops[op] then return expr + elseif unops[op] then return expr + elseif relops[op] then + local key = cfkey(expr) + if db[key] ~= nil then + return { db[key] and 'true' or 'false' } + else + return expr + end + elseif op == 'if' then + local test = cfold(expr[2], db) + local key = cfkey(test) + if db[key] ~= nil then + if db[key] then return cfold(expr[3], db) end + return cfold(expr[4], db) + else + local db_kt = tailops[expr[4][1]] and db or dup(db) + local db_kf = tailops[expr[3][1]] and db or dup(db) + db_kt[key] = true + db_kf[key] = false + return { op, test, cfold(expr[3], db_kt), cfold(expr[4], db_kf) } + end + else + return expr + end +end + +-- Range inference. +local function Range(min, max) + assert(min == min, 'min is NaN') + assert(max == max, 'max is NaN') + -- if min is less than max, we have unreachable code. still, let's + -- not violate assumptions (e.g. about wacky bitshift semantics) + if min > max then min, max = min, min end + local ret = { min_ = min, max_ = max } + function ret:min() return self.min_ end + function ret:max() return self.max_ end + function ret:range() return self:min(), self:max() end + function ret:fold() + if self:min() == self:max() then + return self:min() + end + end + function ret:lt(other) return self:max() < other:min() end + function ret:gt(other) return self:min() > other:max() end + function ret:union(other) + return Range(math.min(self:min(), other:min()), + math.max(self:max(), other:max())) + end + function ret:restrict(other) + return Range(math.max(self:min(), other:min()), + math.min(self:max(), other:max())) + end + function ret:tobit() + if (self:max() - self:min() < 2^32 + and bit.tobit(self:min()) <= bit.tobit(self:max())) then + return Range(bit.tobit(self:min()), bit.tobit(self:max())) + end + return Range(INT32_MIN, INT32_MAX) + end + function ret.binary(lhs, rhs, op) -- for monotonic functions + local fold = assert(folders[op]) + local a = fold(lhs:min(), rhs:min()) + local b = fold(lhs:min(), rhs:max()) + local c = fold(lhs:max(), rhs:max()) + local d = fold(lhs:max(), rhs:min()) + return Range(math.min(a, b, c, d), math.max(a, b, c, d)) + end + function ret.add(lhs, rhs) return lhs:binary(rhs, '+') end + function ret.sub(lhs, rhs) return lhs:binary(rhs, '-') end + function ret.mul(lhs, rhs) return lhs:binary(rhs, '*') end + function ret.mul64(lhs, rhs) return Range(0, UINT32_MAX) end + function ret.div(lhs, rhs) + local rhs_min, rhs_max = rhs:min(), rhs:max() + -- 0 is prohibited by assertions, so we won't hit it at runtime, + -- but we could still see { '/', 0, 0 } in the IR when it is + -- dominated by an assertion that { '!=', 0, 0 }. The resulting + -- range won't include the rhs-is-zero case. + if rhs_min == 0 then + -- If the RHS is (or folds to) literal 0, we certainly won't + -- reach here so we can make up whatever value we want. + if rhs_max == 0 then return Range(0, 0) end + rhs_min = 1 + elseif rhs_max == 0 then + rhs_max = -1 + end + -- Now that we have removed 0 from the limits, + -- if the RHS can't change sign, we can use binary() on its range. + if rhs_min > 0 or rhs_max < 0 then + return lhs:binary(Range(rhs_min, rhs_max), '/') + end + -- Otherwise we can use binary() on the two semi-ranges. + local low, high = Range(rhs_min, -1), Range(1, rhs_max) + return lhs:binary(low, '/'):union(lhs:binary(high, '/')) + end + function ret.band(lhs, rhs) + lhs, rhs = lhs:tobit(), rhs:tobit() + if lhs:min() < 0 and rhs:min() < 0 then + return Range(INT32_MIN, INT32_MAX) + end + return Range(0, math.max(math.min(lhs:max(), rhs:max()), 0)) + end + function ret.bor(lhs, rhs) + lhs, rhs = lhs:tobit(), rhs:tobit() + local function saturate(x) + local y = 1 + while y < x do y = y * 2 end + return y - 1 + end + if lhs:min() < 0 or rhs:min() < 0 then return Range(INT32_MIN, -1) end + return Range(bit.bor(lhs:min(), rhs:min()), + saturate(bit.bor(lhs:max(), rhs:max()))) + end + function ret.bxor(lhs, rhs) return lhs:bor(rhs) end + function ret.lshift(lhs, rhs) + lhs, rhs = lhs:tobit(), rhs:tobit() + local function npot(x) -- next power of two + if x >= 2^31 then return 32 end + local n, i = 1, 1 + while n < x do n, i = n * 2, i + 1 end + return i + end + if lhs:min() >= 0 then + local min_lhs, max_lhs = lhs:min(), lhs:max() + -- It's nuts, but lshift does an implicit modulo on the RHS. + local min_shift, max_shift = 0, 31 + if rhs:min() >= 0 and rhs:max() < 32 then + min_shift, max_shift = rhs:min(), rhs:max() + end + if npot(max_lhs) + max_shift < 32 then + assert(bit.lshift(max_lhs, max_shift) > 0) + return Range(bit.lshift(min_lhs, min_shift), + bit.lshift(max_lhs, max_shift)) + end + end + return Range(INT32_MIN, INT32_MAX) + end + function ret.rshift(lhs, rhs) + lhs, rhs = lhs:tobit(), rhs:tobit() + local min_lhs, max_lhs = lhs:min(), lhs:max() + -- Same comments wrt modulo of shift. + local min_shift, max_shift = 0, 31 + if rhs:min() >= 0 and rhs:max() < 32 then + min_shift, max_shift = rhs:min(), rhs:max() + end + if min_shift > 0 then + -- If we rshift by 1 or more, result will not be negative. + if min_lhs >= 0 and max_lhs < 2^32 then + return Range(bit.rshift(min_lhs, max_shift), + bit.rshift(max_lhs, min_shift)) + else + -- -1 is "all bits set". + return Range(bit.rshift(-1, max_shift), + bit.rshift(-1, min_shift)) + end + elseif min_lhs >= 0 and max_lhs < 2^31 then + -- Left-hand-side in [0, 2^31): result not negative. + return Range(bit.rshift(min_lhs, max_shift), + bit.rshift(max_lhs, min_shift)) + else + -- Otherwise punt. + return Range(INT32_MIN, INT32_MAX) + end + end + return ret +end + +local function infer_ranges(expr) + local function cons(car, cdr) return { car, cdr } end + local function car(pair) return pair[1] end + local function cdr(pair) return pair[2] end + local function cadr(pair) return car(cdr(pair)) end + local function push(db) return cons({}, db) end + local function lookup(db, expr) + if type(expr) == 'number' then return Range(expr, expr) end + local key = cfkey(expr) + while db do + local range = car(db)[key] + if range then return range end + db = cdr(db) + end + if expr == 'len' then return Range(0, UINT16_MAX) end + return Range(INT_MIN, INT_MAX) + end + local function define(db, expr, range) + if type(expr) == 'number' then return expr end + car(db)[cfkey(expr)] = range + if range:fold() then return range:min() end + return expr + end + local function restrict(db, expr, range) + return define(db, expr, lookup(db, expr):restrict(range)) + end + local function merge(db, head) + for key, range in pairs(head) do car(db)[key] = range end + end + local function union(db, h1, h2) + for key, range1 in pairs(h1) do + local range2 = h2[key] + if range2 then car(db)[key] = range1:union(range2) end + end + end + + -- Returns lhs true range, lhs false range, rhs true range, rhs false range + local function branch_ranges(op, lhs, rhs) + local function lt(a, b) + return Range(a:min(), math.min(a:max(), b:max() - 1)) + end + local function le(a, b) + return Range(a:min(), math.min(a:max(), b:max())) + end + local function eq(a, b) + return Range(math.max(a:min(), b:min()), math.min(a:max(), b:max())) + end + local function ge(a, b) + return Range(math.max(a:min(), b:min()), a:max()) + end + local function gt(a, b) + return Range(math.max(a:min(), b:min()+1), a:max()) + end + if op == '<' then + return lt(lhs, rhs), ge(lhs, rhs), gt(rhs, lhs), le(rhs, lhs) + elseif op == '<=' then + return le(lhs, rhs), gt(lhs, rhs), ge(rhs, lhs), lt(rhs, lhs) + elseif op == '=' then + -- Could restrict false continuations more. + return eq(lhs, rhs), lhs, eq(rhs, lhs), rhs + elseif op == '!=' then + return lhs, eq(lhs, rhs), rhs, eq(rhs, lhs) + elseif op == '>=' then + return ge(lhs, rhs), lt(lhs, rhs), le(rhs, lhs), gt(rhs, lhs) + elseif op == '>' then + return gt(lhs, rhs), le(lhs, rhs), lt(rhs, lhs), ge(rhs, lhs) + else + error('unimplemented '..op) + end + end + local function unop_range(op, rhs) + if op == 'ntohs' then return Range(0, 0xffff) end + if op == 'ntohl' then return Range(INT32_MIN, INT32_MAX) end + if op == 'uint32' then return Range(0, 2^32) end + if op == 'int32' then return rhs:tobit() end + error('unexpected op '..op) + end + local function binop_range(op, lhs, rhs) + if op == '+' then return lhs:add(rhs) end + if op == '-' then return lhs:sub(rhs) end + if op == '*' then return lhs:mul(rhs) end + if op == '*64' then return lhs:mul64(rhs) end + if op == '/' then return lhs:div(rhs) end + if op == '&' then return lhs:band(rhs) end + if op == '|' then return lhs:bor(rhs) end + if op == '^' then return lhs:bxor(rhs) end + if op == '<<' then return lhs:lshift(rhs) end + if op == '>>' then return lhs:rshift(rhs) end + error('unexpected op '..op) + end + + local function visit(expr, db_t, db_f) + if type(expr) ~= 'table' then return expr end + local op = expr[1] + + -- Logical ops add to their db_t and db_f stores. + if relops[op] then + local db = push(db_t) + local lhs, rhs = visit(expr[2], db), visit(expr[3], db) + merge(db_t, car(db)) + merge(db_f, car(db)) + local function fold(l, r) + return { assert(folders[op])(l, r) and 'true' or 'false' } + end + local lhs_range, rhs_range = lookup(db_t, lhs), lookup(db_t, rhs) + -- If we folded both sides, or if the ranges are strictly + -- ordered, the condition will fold. + if ((lhs_range:fold() and rhs_range:fold()) + or lhs_range:lt(rhs_range) or lhs_range:gt(rhs_range)) then + return fold(lhs_range:min(), rhs_range:min()) + elseif (lhs_range:max() == rhs_range:min() and op == '<=' + or lhs_range:min() == rhs_range:max() and op == '>=') then + -- The ranges are ordered, but not strictly, and in the same + -- sense as the test: the condition is true. + return { 'true' } + end + -- Otherwise, the relop may restrict the ranges for both + -- arguments along both continuations. + local lhs_range_t, lhs_range_f, rhs_range_t, rhs_range_f = + branch_ranges(op, lhs_range, rhs_range) + restrict(db_t, lhs, lhs_range_t) + restrict(db_f, lhs, lhs_range_f) + restrict(db_t, rhs, rhs_range_t) + restrict(db_f, rhs, rhs_range_f) + return { op, lhs, rhs } + elseif simple[op] then + return expr + elseif op == 'if' then + local test, t, f = expr[2], expr[3], expr[4] + + local test_db_t, test_db_f = push(db_t), push(db_t) + test = visit(test, test_db_t, test_db_f) + + local kt_db_t, kt_db_f = push(test_db_t), push(test_db_t) + local kf_db_t, kf_db_f = push(test_db_f), push(test_db_f) + t = visit(t, kt_db_t, kt_db_f) + f = visit(f, kf_db_t, kf_db_f) + + if tailops[t[1]] then + local head_t, head_f = car(kf_db_t), car(kf_db_f) + local assertions = cadr(kf_db_t) + merge(db_t, assertions) + merge(db_t, head_t) + merge(db_f, assertions) + merge(db_f, head_f) + elseif tailops[f[1]] then + local head_t, head_f = car(kt_db_t), car(kt_db_f) + local assertions = cadr(kt_db_t) + merge(db_t, assertions) + merge(db_t, head_t) + merge(db_f, assertions) + merge(db_f, head_f) + else + local head_t_t, head_t_f = car(kt_db_t), car(kt_db_f) + local head_f_t, head_f_f = car(kf_db_t), car(kf_db_f) + -- union the assertions? + union(db_t, head_t_t, head_f_t) + union(db_f, head_t_f, head_f_f) + end + return { op, test, t, f } + elseif op == 'call' then + return expr + else + -- An arithmetic op, which interns into the fresh table pushed + -- by the containing relop. + local db = db_t + if op == '[]' then + local pos, size = visit(expr[2], db), expr[3] + local ret = { op, pos, size } + local size_max + if size == 1 then size_max = 0xff + elseif size == 2 then size_max = 0xffff + else size_max = 0xffffffff end + local range = lookup(db, ret):restrict(Range(0, size_max)) + return define(db, ret, range) + elseif unops[op] then + local rhs = visit(expr[2], db) + local rhs_range = lookup(db, rhs) + if rhs_range:fold() then + return assert(folders[op])(rhs_range:fold()) + end + if (op == 'uint32' and 0 <= rhs_range:min() + and rhs_range:max() <= UINT32_MAX) then + return rhs + elseif (op == 'int32' and INT32_MIN <= rhs_range:min() + and rhs_range:max() <= INT32_MAX) then + return rhs + end + local range = unop_range(op, rhs_range) + return restrict(db, { op, rhs }, range) + elseif binops[op] then + local lhs, rhs = visit(expr[2], db), visit(expr[3], db) + if type(lhs) == 'number' and type(rhs) == 'number' then + return assert(folders[op])(lhs, rhs) + end + local lhs_range, rhs_range = lookup(db, lhs), lookup(db, rhs) + local range = binop_range(op, lhs_range, rhs_range) + return restrict(db, { op, lhs, rhs }, range) + else + error('what is this '..op) + end + end + end + return visit(expr, push(), push()) +end + +-- Length assertion hoisting. +local function lhoist(expr, db) + -- Recursively annotate the logical expressions in EXPR, returning + -- tables of the form { MIN_T, MIN_F, MIN_PASS, MAX_FAIL, EXPR }. + -- MIN_T indicates that for this expression to be true, the packet + -- must be at least as long as MIN_T. Similarly for MIN_F. MIN_PASS + -- means that if the packet is smaller than MIN_PASS then the filter + -- will definitely fail. MAX_FAIL means that if the packet is + -- smaller than MAX_FAIL, there is a 'fail' call on some path. + local function annotate(expr, is_tail) + local function aexpr(min_t, min_f, min_pass, max_fail, expr) + if is_tail then + min_pass = math.max(min_pass, min_t) + min_t = min_pass + end + return { min_t, min_f, min_pass, max_fail, expr } + end + local op = expr[1] + if (op == '>=' and expr[2] == 'len' and type(expr[3]) == 'number') then + return aexpr(expr[3], 0, 0, -1, expr) + elseif op == 'if' then + local test, t, f = expr[2], expr[3], expr[4] + local test_a = annotate(test, false) + local t_a, f_a = annotate(t, is_tail), annotate(f, is_tail) + local test_min_t, test_min_f = test_a[1], test_a[2] + local test_min_pass, test_max_fail = test_a[3], test_a[4] + local function if_bool_mins() + local t, f = t[1], f[1] + local function branch_bool_mins(abranch, min) + local branch_min_t, branch_min_f = abranch[1], abranch[2] + return math.max(branch_min_t, min), math.max(branch_min_f, min) + end + local t_min_t, t_min_f = branch_bool_mins(t_a, test_min_t) + local f_min_t, f_min_f = branch_bool_mins(f_a, test_min_f) + if trueops[t] then t_min_f = f_min_f end + if trueops[f] then f_min_f = t_min_f end + if t == 'fail' then return f_min_t, f_min_f end + if f == 'fail' then return t_min_t, t_min_f end + if t == 'false' then t_min_t = f_min_t end + if f == 'false' then f_min_t = t_min_t end + return math.min(t_min_t, f_min_t), math.min(t_min_f, f_min_f) + end + local function if_fail_mins() + local t, f = t[1], f[1] + local min_pass, max_fail + local t_min_pass, t_max_fail = t_a[3], t_a[4] + local f_min_pass, f_max_fail = f_a[3], f_a[4] + -- Four cases: both T and F branches are fail; one of them + -- is a fail; neither are fails. + if t == 'fail' then + if f == 'fail' then + min_pass = test_min_pass + max_fail = UINT16_MAX + else + min_pass = math.max(test_min_f, f_min_pass, test_min_pass) + max_fail = math.max(test_min_t, f_max_fail, test_max_fail) + end + elseif f == 'fail' then + min_pass = math.max(test_min_t, t_min_pass, test_min_pass) + max_fail = math.max(test_min_f, f_max_fail, test_max_fail) + else + min_pass = math.max(test_min_pass, math.min(t_min_pass, f_min_pass)) + max_fail = math.max(t_max_fail, f_max_fail, test_max_fail) + end + return min_pass, max_fail + end + local min_t, min_f = if_bool_mins() + local min_pass, max_fail = if_fail_mins() + return aexpr(min_t, min_f, min_pass, max_fail, { op, test_a, t_a, f_a }) + else + return aexpr(0, 0, 0, -1, expr) + end + end + + -- Strip the annotated expression AEXPR. Whenever the packet needs + -- be longer than the MIN argument, insert a length check and revisit + -- with the new MIN. Elide other length checks. + local function reduce(aexpr, min, is_tail) + local min_t, min_f, min_pass, max_fail, expr = + aexpr[1], aexpr[2], aexpr[3], aexpr[4], aexpr[5] + + -- Reject any packets that are too short to pass. + if is_tail then min_pass = math.max(min_pass, min_t) end + if min < min_pass then + local expr = reduce(aexpr, min_pass, is_tail) + return { 'if', { '>=', 'len', min_pass }, expr, { 'fail' } } + end + + -- Hoist length checks if we know a packet must be of a certain + -- length for the expression to be true, and we are certain that + -- we aren't going to hit a "fail". + if min < min_t and max_fail < min then + local expr = reduce(aexpr, min_t, is_tail) + return { 'if', { '>=', 'len', min_t }, expr, { 'false' } } + end + + local op = expr[1] + if op == 'if' then + local t = reduce(expr[2], min, false) + local kt = reduce(expr[3], min, is_tail) + local kf = reduce(expr[4], min, is_tail) + return { op, t, kt, kf } + elseif op == '>=' and expr[2] == 'len' and type(expr[3]) == 'number' then + -- min may be set conservatively low; it is *only* a lower bound. + -- If expr[3] is <= min, { 'true' } is a valid optimization. + -- Otherwise, there's not enough information; leave expr alone. + if expr[3] <= min then return { 'true' } else return expr end + else + return expr + end + end + + return reduce(annotate(expr, true), 0, true) +end + +function optimize_inner(expr) + expr = simplify(expr, true) + expr = simplify(cfold(expr, {}), true) + expr = simplify(infer_ranges(expr), true) + expr = simplify(lhoist(expr), true) + clear_cache() + return expr +end + +function optimize(expr) + expr = utils.fixpoint(optimize_inner, expr) + if verbose then pp(expr) end + return expr +end + +function selftest () + print("selftest: pf.optimize") + local parse = require('pf.parse').parse + local expand = require('pf.expand').expand + local function opt(str) return optimize(expand(parse(str), "EN10MB")) end + local equals, assert_equals = utils.equals, utils.assert_equals + assert_equals({ 'fail' }, + opt("1 = 2")) + assert_equals({ '=', "len", 1 }, + opt("1 = len")) + assert_equals({ 'match' }, + opt("1 = 2/2")) + assert_equals({ 'if', { '>=', 'len', 1}, + { '=', { '[]', 0, 1 }, 2 }, + { 'fail' }}, + opt("ether[0] = 2")) + assert_equals({ 'if', { '>=', 'len', 7}, + { '<', + { '+', { '+', { '[]', 5, 1 }, { '[]', 6, 1 } }, 3 }, + 10 }, + { 'fail' }}, + opt("(ether[5] + 1) + (ether[6] + 2) < 10")) + assert_equals({ 'if', { '>=', 'len', 7}, + { '<', + { '+', { '+', { '[]', 5, 1 }, { '[]', 6, 1 } }, 3 }, + 10 }, + { 'fail' }}, + opt("ether[5] + 1 + ether[6] + 2 < 10")) + assert_equals({ '>=', 'len', 2}, + opt("greater 1 and greater 2")) + -- Could check this, but it's very large + opt("tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)") + opt("tcp port 5555") + print("OK") +end diff --git a/src/pf/parse.lua b/src/pf/parse.lua new file mode 100644 index 0000000000..f7ee13740d --- /dev/null +++ b/src/pf/parse.lua @@ -0,0 +1,1194 @@ +module(...,package.seeall) + +allow_address_of = true + +local utils = require('pf.utils') +local constants = require('pf.constants') + +local ipv4_to_int, ipv6_as_4x32 = utils.ipv4_to_int, utils.ipv6_as_4x32 +local uint32 = utils.uint32 + +local function skip_whitespace(str, pos) + while pos <= #str and str:match('^%s', pos) do + pos = pos + 1 + end + return pos +end + +local function set(...) + local ret = {} + for k, v in pairs({...}) do ret[v] = true end + return ret +end + +local punctuation = set( + '(', ')', '[', ']', ':', '!', '!=', '<', '<=', '>', '>=', '=', '==', + '+', '-', '*', '/', '&', '|', '^', '&&', '||', '<<', '>>', '\\' +) + +local number_terminators = " \t\r\n)]:!<>=+-*/%&|^" + +local function lex_number(str, pos, base) + local res = 0 + local i = pos + while i <= #str do + local chr = str:sub(i,i) + local n = tonumber(chr, base) + if n then + res = res * base + n + i = i + 1 + elseif not number_terminators:find(chr, 1, true) then + return nil + else + break + end + end + + if i == pos then + -- No digits parsed, can happen when lexing "0x" or "09". + return nil + end + return res, i -- EOS or end of number. +end + +local function maybe_lex_number(str, pos) + if str:match("^0x", pos) then + return "hexadecimal", lex_number(str, pos+2, 16) + elseif str:match("^0%d", pos) then + return "octal", lex_number(str, pos+1, 8) + elseif str:match("^%d", pos) then + return "decimal", lex_number(str, pos, 10) + end +end + +local function lex_host_or_keyword(str, pos) + local name, next_pos = str:match("^([%w.-]+)()", pos) + assert(name, "failed to parse hostname or keyword at "..pos) + assert(name:match("^%w", 1, 1), "bad hostname or keyword "..name) + assert(name:match("^%w", #name, #name), "bad hostname or keyword "..name) + + local kind, number, number_next_pos = maybe_lex_number(str, pos) + -- Only interpret name as a number as a whole. + if number and number_next_pos == next_pos then + assert(number <= 0xffffffff, 'integer too large: '..name) + return number, next_pos + else + return name, next_pos + end +end + +local function lex_ipv4(str, pos) + local function lex_byte(str) + local byte = tonumber(str, 10) + if byte >= 256 then return nil end + return byte + end + local digits, dot = str:match("^(%d%d?%d?)()", pos) + if not digits then return nil end + local addr = { 'ipv4' } + local byte = lex_byte(digits) + if not byte then return nil end + table.insert(addr, byte) + pos = dot + for i=1,3 do + local digits, dot = str:match("^%.(%d%d?%d?)()", pos) + if not digits then break end + local byte = lex_byte(digits) + if not byte then return nil end + table.insert(addr, byte) + pos = dot + end + + local last_char = str:sub(pos, pos) + -- IPv4 address is actually a hostname + if last_char:match("[%w.-]") then return nil end + + local terminators = " \t\r\n)/" + assert(pos > #str or terminators:find(last_char, 1, true), + "unexpected terminator for ipv4 address") + return addr, pos +end + +local function lex_ipv6(str, pos) + local addr = { 'ipv6' } + local hole_index + + if str:sub(pos, pos + 1) == "::" then + hole_index = 2 + pos = pos + 2 + end + + local after_sep = false + local digits_pattern = "^(%x%x?%x?%x?)()" + local expected_sep = ":" + local ipv4_fields = 0 + + while true do + local digits, next_pos = str:match(digits_pattern, pos) + if not digits then + if after_sep then + error("wrong IPv6 address") + else + break + end + end + + local sep = str:sub(next_pos, next_pos) + if sep == "." and expected_sep == ":" then + expected_sep = "." + digits_pattern = "^(%d%d?%d?)()" + -- Continue loop without advancing pos. + -- Will parse field as decimal in the next iteration. + else + pos = next_pos + + if expected_sep == ":" then + table.insert(addr, tonumber(digits, 16)) + else + local ipv4_field = tonumber(digits, 10) + assert(ipv4_field < 255, "wrong IPv6 address") + ipv4_fields = ipv4_fields + 1 + if ipv4_fields % 2 == 0 then + addr[#addr] = addr[#addr] * 256 + ipv4_field + else + table.insert(addr, ipv4_field) + end + end + + if sep ~= expected_sep then break end + pos = pos + 1 + if sep == ":" and not hole_index and str:sub(pos, pos) == ":" then + pos = pos + 1 + hole_index = #addr + 1 + after_sep = false + else + after_sep = true + end + end + end + + assert(ipv4_fields == 0 or ipv4_fields == 4, "wrong IPv6 address") + + if hole_index then + local zeros = 9 - #addr + assert(zeros >= 1, "wrong IPv6 address") + for i=1,zeros do + table.insert(addr, hole_index, 0) + end + end + + assert(#addr == 9, "wrong IPv6 address") + + local terminators = " \t\r\n)/" + assert(pos > #str or terminators:find(str:sub(pos, pos), 1, true), + "unexpected terminator for ipv6 address") + + return addr, pos +end + +local function lex_ehost(str, pos) + local start = pos + local addr = { 'ehost' } + local digits, dot = str:match("^(%x%x?)()%:", pos) + assert(digits, "failed to parse ethernet host address at "..pos) + table.insert(addr, tonumber(digits, 16)) + pos = dot + for i=1,5 do + local digits, dot = str:match("^%:(%x%x?)()", pos) + assert(digits, "failed to parse ethernet host address at "..pos) + table.insert(addr, tonumber(digits, 16)) + pos = dot + end + local terminators = " \t\r\n)/" + local last_char = str:sub(pos, pos) + -- MAC address is actually an IPv6 address + if last_char == ':' or last_char == '.' then return nil, start end + assert(pos > #str or terminators:find(last_char, 1, true), + "unexpected terminator for ethernet host address") + return addr, pos +end + +local function lex_addr_or_host(str, pos) + if str:match('^%x%x?:%x%x?:%x%x?:%x%x?:%x%x?:%x%x?', pos) then + local result, pos = lex_ehost(str, pos) + if result then return result, pos end + return lex_ipv6(str, pos) + elseif str:match("^%x?%x?%x?%x?%:", pos) then + return lex_ipv6(str, pos) + elseif str:match("^%d%d?%d?", pos) then + local result, pos = lex_ipv4(str, pos) + if result then return result, pos end -- Fall through. + end + + return lex_host_or_keyword(str, pos) +end + +local function lex(str, pos, opts) + -- EOF. + if pos > #str then return nil, pos end + + if opts.address then + -- Net addresses. + return lex_addr_or_host(str, pos) + end + + -- Non-alphanumeric tokens. + local two = str:sub(pos,pos+1) + if punctuation[two] then return two, pos+2 end + local one = str:sub(pos,pos) + if punctuation[one] then return one, pos+1 end + + -- Numeric literals. + if opts.maybe_arithmetic then + local kind, number, next_pos = maybe_lex_number(str, pos) + if kind then + assert(number, "unexpected end of "..kind.." literal at "..pos) + assert(number <= 0xffffffff, 'integer too large: '..str:sub(pos, next_pos-1)) + return number, next_pos + end + end + + -- "len" is the only bare name that can appear in an arithmetic + -- expression. "len-1" lexes as { 'len', '-', 1 } in arithmetic + -- contexts, but { "len-1" } otherwise. + if opts.maybe_arithmetic and str:match("^len", pos) then + if pos + 3 > #str or not str:match("^[%w.]", pos+3) then + return 'len', pos+3 + end + end + + return lex_host_or_keyword(str, pos) +end + +local function tokens(str) + local pos, next_pos = 1, nil + local peeked = nil + local peeked_address = nil + local peeked_maybe_arithmetic = nil + local last_pos = 0 + local primitive_error = error + local function peek(opts) + opts = opts or {} + if not next_pos or opts.address ~= peeked_address or + opts.maybe_arithmetic ~= peeked_maybe_arithmetic then + pos = skip_whitespace(str, pos) + peeked, next_pos = lex(str, pos, opts or {}) + peeked_address = opts.address + peeked_maybe_arithmetic = opts.maybe_arithmetic + assert(next_pos, "next pos is nil") + end + return peeked + end + local function next(opts) + local tok = assert(peek(opts), "unexpected end of filter string") + pos, next_pos = next_pos, nil + last_pos = pos + return tok + end + local function consume(expected, opts) + local tok = next(opts) + assert(tok == expected, "expected "..expected..", got: "..tok) + end + local function check(expected, opts) + if peek(opts) ~= expected then return false end + next() + return true + end + local function error_str(message, ...) + local location_error_message = "Pflua parse error: In expression \"%s\"" + local start = #location_error_message - 4 + local cursor_pos = start + last_pos + + local result = "\n" + result = result..location_error_message:format(str).."\n" + result = result..string.rep(" ", cursor_pos).."^".."\n" + result = result..message:format(...).."\n" + return result + end + local function error(message, ...) + primitive_error(error_str(message, ...)) + end + return { peek = peek, next = next, consume = consume, check = check, error = error } +end + +local addressables = set( + 'arp', 'rarp', 'wlan', 'ether', 'fddi', 'tr', 'ppp', + 'slip', 'link', 'radio', 'ip', 'ip6', 'tcp', 'udp', 'icmp', + 'igmp', 'pim', 'igrp', 'vrrp', 'sctp' +) + +local function nullary() + return function(lexer, tok) + return { tok } + end +end + +local function unary(parse_arg) + return function(lexer, tok) + return { tok, parse_arg(lexer) } + end +end + +function parse_host_arg(lexer) + local arg = lexer.next({address=true}) + if type(arg) == 'string' or arg[1] == 'ipv4' or arg[1] == 'ipv6' then + return arg + end + lexer.error('invalid host %s', arg) +end + +function parse_int_arg(lexer, max_len) + local ret = lexer.next({maybe_arithmetic=true}) + assert(type(ret) == 'number', 'expected a number', ret) + if max_len then assert(ret <= max_len, 'out of range '..ret) end + return ret +end + +function parse_uint16_arg(lexer) return parse_int_arg(lexer, 0xffff) end + +function parse_net_arg(lexer) + + local function check_non_network_bits_in_ipv4(addr, mask_bits, mask_str) + local ipv4 = uint32(addr[2], addr[3], addr[4], addr[5]) + if (bit.band(ipv4, mask_bits) ~= bit.tobit(ipv4)) then + lexer.error("Non-network bits set in %s/%s", + table.concat(addr, ".", 2), mask_str) + end + end + + local function check_non_network_bits_in_ipv6(addr, mask_len) + local function format_ipv6(addr, mask_len) + return string.format("%x:%x:%x:%x:%x:%x:%x:%x/%d, ", + addr[2], addr[3], addr[4], addr[5], addr[5], addr[6], addr[7], + addr[8], mask_len) + end + local ipv6 = ipv6_as_4x32(addr) + for i, fragment in ipairs(ipv6) do + local mask_len_fragment = mask_len > 32 and 32 or mask_len + local mask_bits = 2^32 - 2^(32 - mask_len_fragment) + if (bit.band(fragment, mask_bits) ~= bit.tobit(fragment)) then + lexer.error("Non-network bits set in %s", format_ipv6(addr, mask_len)) + end + mask_len = mask_len - mask_len_fragment + end + end + + local arg = lexer.next({address=true}) + -- IPv4 dotted triple, dotted pair or bare net addresses + if arg[1] == 'ipv4' and #arg < 5 then + local mask_len = 32 + for i=#arg+1,5 do + arg[i] = 0 + mask_len = mask_len - 8 + end + return { 'ipv4/len', arg, mask_len } + end + if arg[1] == 'ipv4' or arg[1] == 'ipv6' then + if lexer.check('/') then + local mask_len = parse_int_arg(lexer, arg[1] == 'ipv4' and 32 or 128) + if (arg[1] == 'ipv4') then + local mask_bits = 2^32 - 2^(32 - mask_len) + check_non_network_bits_in_ipv4(arg, mask_bits, tostring(mask_len)) + end + if (arg[1] == 'ipv6') then + check_non_network_bits_in_ipv6(arg, mask_len) + end + return { arg[1]..'/len', arg, mask_len } + elseif lexer.check('mask') then + if (arg[1] == 'ipv6') then + lexer.error("Not valid syntax for IPv6") + end + local mask = lexer.next({address=true}) + check_non_network_bits_in_ipv4(arg, ipv4_to_int(mask), + table.concat(mask, '.', 2)) + assert(mask[1] == arg[1], 'bad mask', mask) + return { arg[1]..'/mask', arg, mask } + else + return arg + end + elseif type(arg) == 'string' then + lexer.error('named nets currently unsupported %s', arg) + end +end + +local function to_port_number(tok) + local port = tok + if type(tok) == 'string' then + local next_pos + port, next_pos = lex_number(tok, 1, 10) + if not port or next_pos ~= #tok+1 then + -- Token is not a valid decimal literal, fallback to services. + return constants.services[tok] + end + end + + assert(port <= 65535, 'port '..port..' out of range') + return port +end + +local function parse_port_arg(lexer) + local tok = lexer.next() + local result = to_port_number(tok) + if not result then + lexer.error('unsupported port %s', tok) + end + return result +end + +local function parse_portrange_arg(lexer) + local tok = lexer.next() + + -- Try to split portrange from start to first hyphen, or from start to + -- second hyphen, and so on. + local pos = 1 + while true do + pos = tok:match("^%w+%-()", pos) + if not pos then + lexer.error('error parsing portrange %s', tok) + end + local from, to = to_port_number(tok:sub(1, pos - 2)), to_port_number(tok:sub(pos)) + if from and to then + -- For libpcap compatibility, if to < from, swap them + if from > to then from, to = to, from end + return { from, to } + end + end +end + +local function parse_ehost_arg(lexer) + local arg = lexer.next({address=true}) + if type(arg) == 'string' or arg[1] == 'ehost' then + return arg + end + lexer.error('invalid ethernet host %s', arg) +end + +local function table_parser(table, default) + return function (lexer, tok) + local subtok = lexer.peek() + if table[subtok] then + lexer.consume(subtok) + return table[subtok](lexer, tok..'_'..subtok) + end + if default then return default(lexer, tok) end + lexer.error('unknown %s type %s ', tok, subtok) + end +end + +local ip_protos = set( + 'icmp', 'icmp6', 'igmp', 'igrp', 'pim', 'ah', 'esp', 'vrrp', 'udp', 'tcp', 'sctp' +) + +local function parse_proto_arg(lexer, proto_type, protos) + lexer.check('\\') + local arg = lexer.next() + if not proto_type then proto_type = 'ip' end + if not protos then protos = ip_protos end + if type(arg) == 'number' then return arg end + if type(arg) == 'string' then + local proto = arg:match("^(%w+)") + if protos[proto] then return proto end + end + lexer.error('invalid %s proto %s', proto_type, arg) +end + +local ether_protos = set( + 'ip', 'ip6', 'arp', 'rarp', 'atalk', 'aarp', 'decnet', 'sca', 'lat', + 'mopdl', 'moprc', 'iso', 'stp', 'ipx', 'netbeui' +) + +local function parse_ether_proto_arg(lexer) + return parse_proto_arg(lexer, 'ethernet', ether_protos) +end + +local function parse_ip_proto_arg(lexer) + return parse_proto_arg(lexer, 'ip', ip_protos) +end + +local iso_protos = set('clnp', 'esis', 'isis') + +local function parse_iso_proto_arg(lexer) + return parse_proto_arg(lexer, 'iso', iso_protos) +end + +local function simple_typed_arg_parser(expected) + return function(lexer) + local arg = lexer.next() + if type(arg) == expected then return arg end + lexer.error('expected a %s string, got %s', expected, type(arg)) + end +end + +local parse_string_arg = simple_typed_arg_parser('string') + +local function parse_decnet_host_arg(lexer) + local arg = lexer.next({address=true}) + if type(arg) == 'string' then return arg end + if arg[1] == 'ipv4' then + arg[1] = 'decnet' + assert(#arg == 3, "bad decnet address", arg) + return arg + end + lexer.error('invalid decnet host %s', arg) +end + +local llc_types = set( + 'i', 's', 'u', 'rr', 'rnr', 'rej', 'ui', 'ua', + 'disc', 'sabme', 'test', 'xis', 'frmr' +) + +local function parse_llc(lexer, tok) + if llc_types[lexer.peek()] then return { tok, lexer.next() } end + return { tok } +end + +local pf_reasons = set( + 'match', 'bad-offset', 'fragment', 'short', 'normalize', 'memory' +) + +local pf_actions = set( + 'pass', 'block', 'nat', 'rdr', 'binat', 'scrub' +) + +local wlan_frame_types = set('mgt', 'ctl', 'data') +local wlan_frame_mgt_subtypes = set( + 'assoc-req', 'assoc-resp', 'reassoc-req', 'reassoc-resp', + 'probe-req', 'probe-resp', 'beacon', 'atim', 'disassoc', 'auth', 'deauth' +) +local wlan_frame_ctl_subtypes = set( + 'ps-poll', 'rts', 'cts', 'ack', 'cf-end', 'cf-end-ack' +) +local wlan_frame_data_subtypes = set( + 'data', 'data-cf-ack', 'data-cf-poll', 'data-cf-ack-poll', 'null', + 'cf-ack', 'cf-poll', 'cf-ack-poll', 'qos-data', 'qos-data-cf-ack', + 'qos-data-cf-poll', 'qos-data-cf-ack-poll', 'qos', 'qos-cf-poll', + 'quos-cf-ack-poll' +) + +local wlan_directions = set('nods', 'tods', 'fromds', 'dstods') + +local function parse_enum_arg(lexer, set) + local arg = lexer.next() + assert(set[arg], 'invalid argument: '..arg) + return arg +end + +local function enum_arg_parser(set) + return function(lexer) return parse_enum_arg(lexer, set) end +end + +local function parse_wlan_type(lexer, tok) + local type = enum_arg_parser(wlan_frame_types)(lexer) + if lexer.check('subtype') then + local set + if type == 'mgt' then set = wlan_frame_mgt_subtypes + elseif type == 'mgt' then set = wlan_frame_ctl_subtypes + else set = wlan_frame_data_subtypes end + return { 'type', type, enum_arg_parser(set)(lexer) } + end + return { tok, type } +end + +local function parse_wlan_subtype(lexer, tok) + local subtype = lexer.next() + assert(wlan_frame_mgt_subtypes[subtype] + or wlan_frame_ctl_subtypes[subtype] + or wlan_frame_data_subtypes[subtype], + 'bad wlan subtype '..subtype) + return { tok, subtype } +end + +local function parse_wlan_dir(lexer, tok) + if (type(lexer.peek()) == 'number') then + return { tok, lexer.next() } + end + return { tok, parse_enum_arg(lexer, wlan_directions) } +end + +local function parse_optional_int(lexer, tok) + if (type(lexer.peek()) == 'number') then + return { tok, lexer.next() } + end + return { tok } +end + +local src_or_dst_types = { + host = unary(parse_host_arg), + net = unary(parse_net_arg), + port = unary(parse_port_arg), + portrange = unary(parse_portrange_arg) +} + +local ether_host_type = { + host = unary(parse_ehost_arg) +} + +local ether_types = { + dst = table_parser(ether_host_type, unary(parse_ehost_arg)), + src = table_parser(ether_host_type, unary(parse_ehost_arg)), + host = unary(parse_ehost_arg), + broadcast = nullary(), + multicast = nullary(), + proto = unary(parse_ether_proto_arg), +} + +local ip_types = { + dst = table_parser(src_or_dst_types, unary(parse_host_arg)), + src = table_parser(src_or_dst_types, unary(parse_host_arg)), + host = unary(parse_host_arg), + proto = unary(parse_ip_proto_arg), + protochain = unary(parse_ip_proto_arg), + broadcast = nullary(), + multicast = nullary(), +} + +local ip6_types = { + proto = unary(parse_ip_proto_arg), + protochain = unary(parse_ip_proto_arg), + broadcast = nullary(), + multicast = nullary(), +} + +local decnet_host_type = { + host = unary(parse_decnet_host_arg), +} + +local decnet_types = { + src = table_parser(decnet_host_type, unary(parse_decnet_host_arg)), + dst = table_parser(decnet_host_type, unary(parse_decnet_host_arg)), + host = unary(parse_decnet_host_arg), +} + +local wlan_types = { + ra = unary(parse_ehost_arg), + ta = unary(parse_ehost_arg), + addr1 = unary(parse_ehost_arg), + addr2 = unary(parse_ehost_arg), + addr3 = unary(parse_ehost_arg), + addr4 = unary(parse_ehost_arg), + + -- As an alias of 'ether' + dst = table_parser(ether_host_type, unary(parse_ehost_arg)), + src = table_parser(ether_host_type, unary(parse_ehost_arg)), + host = unary(parse_ehost_arg), + broadcast = nullary(), + multicast = nullary(), + proto = unary(parse_ether_proto_arg), +} + +local iso_types = { + proto = unary(parse_iso_proto_arg), + ta = unary(parse_ehost_arg), + addr1 = unary(parse_ehost_arg), + addr2 = unary(parse_ehost_arg), + addr3 = unary(parse_ehost_arg), + addr4 = unary(parse_ehost_arg), +} + +local tcp_or_udp_types = { + port = unary(parse_port_arg), + portrange = unary(parse_portrange_arg), + dst = table_parser(src_or_dst_types), + src = table_parser(src_or_dst_types), +} + +local arp_types = { + dst = table_parser(src_or_dst_types, unary(parse_host_arg)), + src = table_parser(src_or_dst_types, unary(parse_host_arg)), + host = unary(parse_host_arg), +} + +local rarp_types = { + dst = table_parser(src_or_dst_types, unary(parse_host_arg)), + src = table_parser(src_or_dst_types, unary(parse_host_arg)), + host = unary(parse_host_arg), +} + +local parse_arithmetic + +local function parse_addressable(lexer, tok) + if not tok then + tok = lexer.next({maybe_arithmetic=true}) + if not addressables[tok] then + lexer.error('bad token while parsing addressable: %s', tok) + end + end + lexer.consume('[') + local pos = parse_arithmetic(lexer) + local size = 1 + if lexer.check(':') then + if lexer.check(1) then size = 1 + elseif lexer.check(2) then size = 2 + else lexer.consume(4); size = 4 end + end + lexer.consume(']') + return { '['..tok..']', pos, size} +end + +local function parse_primary_arithmetic(lexer, tok) + tok = tok or lexer.next({maybe_arithmetic=true}) + if tok == '(' then + local expr = parse_arithmetic(lexer) + lexer.consume(')') + return expr + elseif tok == 'len' or type(tok) == 'number' then + return tok + elseif allow_address_of and tok == '&' then + return { 'addr', parse_addressable(lexer) } + elseif addressables[tok] then + return parse_addressable(lexer, tok) + else + -- 'tok' may be a constant + local val = constants.protocol_header_field_offsets[tok] or + constants.icmp_type_fields[tok] or + constants.tcp_flag_fields[tok] + if val ~= nil then return val end + lexer.error('bad token while parsing arithmetic expression %s', tok) + end +end + +local arithmetic_precedence = { + ['*'] = 1, ['/'] = 1, + ['+'] = 2, ['-'] = 2, + ['<<'] = 3, ['>>'] = 3, + ['&'] = 4, + ['^'] = 5, + ['|'] = 6 +} + +function parse_arithmetic(lexer, tok, max_precedence, parsed_exp) + local exp = parsed_exp or parse_primary_arithmetic(lexer, tok) + max_precedence = max_precedence or math.huge + while true do + local op = lexer.peek() + local prec = arithmetic_precedence[op] + if not prec or prec > max_precedence then return exp end + lexer.consume(op) + local rhs = parse_arithmetic(lexer, nil, prec - 1) + exp = { op, exp, rhs } + end +end + +local primitives = { + dst = table_parser(src_or_dst_types), + src = table_parser(src_or_dst_types), + host = unary(parse_host_arg), + ether = table_parser(ether_types), + fddi = table_parser(ether_types), + tr = table_parser(ether_types), + wlan = table_parser(wlan_types), + broadcast = nullary(), + multicast = nullary(), + gateway = unary(parse_string_arg), + net = unary(parse_net_arg), + port = unary(parse_port_arg), + portrange = unary(parse_portrange_arg), + less = unary(parse_arithmetic), + greater = unary(parse_arithmetic), + ip = table_parser(ip_types, nullary()), + ip6 = table_parser(ip6_types, nullary()), + proto = unary(parse_proto_arg), + tcp = table_parser(tcp_or_udp_types, nullary()), + udp = table_parser(tcp_or_udp_types, nullary()), + icmp = nullary(), + icmp6 = nullary(), + igmp = nullary(), + igrp = nullary(), + pim = nullary(), + ah = nullary(), + esp = nullary(), + vrrp = nullary(), + sctp = nullary(), + protochain = unary(parse_proto_arg), + arp = table_parser(arp_types, nullary()), + rarp = table_parser(rarp_types, nullary()), + atalk = nullary(), + aarp = nullary(), + decnet = table_parser(decnet_types, nullary()), + iso = nullary(), + stp = nullary(), + ipx = nullary(), + netbeui = nullary(), + sca = nullary(), + lat = nullary(), + moprc = nullary(), + mopdl = nullary(), + llc = parse_llc, + ifname = unary(parse_string_arg), + on = unary(parse_string_arg), + rnr = unary(parse_int_arg), + rulenum = unary(parse_int_arg), + reason = unary(enum_arg_parser(pf_reasons)), + rset = unary(parse_string_arg), + ruleset = unary(parse_string_arg), + srnr = unary(parse_int_arg), + subrulenum = unary(parse_int_arg), + action = unary(enum_arg_parser(pf_actions)), + type = parse_wlan_type, + subtype = parse_wlan_subtype, + dir = unary(enum_arg_parser(wlan_directions)), + vlan = parse_optional_int, + mpls = parse_optional_int, + pppoed = nullary(), + pppoes = parse_optional_int, + iso = table_parser(iso_types, nullary()), + clnp = nullary(), + esis = nullary(), + isis = nullary(), + l1 = nullary(), + l2 = nullary(), + iih = nullary(), + lsp = nullary(), + snp = nullary(), + csnp = nullary(), + psnp = nullary(), + vpi = unary(parse_int_arg), + vci = unary(parse_int_arg), + lane = nullary(), + oamf4s = nullary(), + oamf4e = nullary(), + oamf4 = nullary(), + oam = nullary(), + metac = nullary(), + bcc = nullary(), + sc = nullary(), + ilmic = nullary(), + connectmsg = nullary(), + metaconnect = nullary() +} + +local function parse_primitive_or_arithmetic(lexer) + local tok = lexer.next({maybe_arithmetic=true}) + if (type(tok) == 'number' or tok == 'len' or + addressables[tok] and lexer.peek() == '[') then + return parse_arithmetic(lexer, tok) + end + + local parser = primitives[tok] + if parser then return parser(lexer, tok) end + + -- At this point the official pcap grammar is squirrely. It says: + -- "If an identifier is given without a keyword, the most recent + -- keyword is assumed. For example, `not host vs and ace' is + -- short for `not host vs and host ace` and which should not be + -- confused with `not (host vs or ace)`." For now we punt on this + -- part of the grammar. + local msg = +[[%s is not a recognized keyword. Likely causes: +a) %s is a typo, invalid keyword, or similar error. +b) You're trying to implicitly repeat the previous clause's keyword. +Instead of libpcap-style elision, explicitly use keywords in each clause: +ie, "host a and host b", not "host a and b".]] + + local err = string.format(msg, tok, tok) + lexer.error(err) +end + +local logical_ops = set('&&', 'and', '||', 'or') + +local function is_arithmetic(exp) + return (exp == 'len' or type(exp) == 'number' or + exp[1]:match("^%[") or arithmetic_precedence[exp[1]]) +end + +local parse_logical + +local function parse_logical_or_arithmetic(lexer, pick_first) + local exp + if lexer.peek() == 'not' or lexer.peek() == '!' then + exp = { lexer.next(), parse_logical(lexer, true) } + elseif lexer.check('(') then + exp = parse_logical_or_arithmetic(lexer) + lexer.consume(')') + else + exp = parse_primitive_or_arithmetic(lexer) + end + if is_arithmetic(exp) then + if arithmetic_precedence[lexer.peek()] then + exp = parse_arithmetic(lexer, nil, nil, exp) + end + if lexer.peek() == ')' then return exp end + local op = lexer.next() + assert(set('>', '<', '>=', '<=', '=', '!=', '==')[op], + "expected a comparison operator, got "..op) + -- Normalize == to =, because libpcap treats them identically + if op == '==' then op = '=' end + exp = { op, exp, parse_arithmetic(lexer) } + end + if pick_first then return exp end + while true do + local op = lexer.peek() + if not op or op == ')' then return exp end + local is_logical = logical_ops[op] + if is_logical then + lexer.consume(op) + else + -- The grammar is such that "tcp port 80" should actually + -- parse as "tcp and port 80". + op = 'and' + end + local rhs = parse_logical(lexer, true) + exp = { op, exp, rhs } + end +end + +function parse_logical(lexer, pick_first) + local expr = parse_logical_or_arithmetic(lexer, pick_first) + assert(not is_arithmetic(expr), "expected a logical expression") + return expr +end + +function parse(str, opts) + opts = opts or {} + local lexer = tokens(str) + local expr + if opts.arithmetic then + expr = parse_arithmetic(lexer) + else + if not lexer.peek({maybe_arithmetic=true}) then return { 'true' } end + expr = parse_logical(lexer) + end + if lexer.peek() then error("unexpected token "..lexer.peek()) end + return expr +end + +function selftest () + print("selftest: pf.parse") + local function check(expected, actual) + assert(type(expected) == type(actual), + "expected type "..type(expected).." but got "..type(actual)) + if type(expected) == 'table' then + for k, v in pairs(expected) do check(v, actual[k]) end + else + assert(expected == actual, "expected "..expected.." but got "..actual) + end + end + + local function lex_test(str, elts, opts) + local lexer = tokens(str) + for i, val in ipairs(elts) do + check(val, lexer.next(opts)) + end + assert(not lexer.peek(opts), "more tokens, yo") + end + lex_test("ip", {"ip"}, {maybe_arithmetic=true}) + lex_test("len", {"len"}, {maybe_arithmetic=true}) + lex_test("len", {"len"}, {}) + lex_test("len-1", {"len-1"}, {}) + lex_test("len-1", {"len", "-", 1}, {maybe_arithmetic=true}) + lex_test("1-len", {1, "-", "len"}, {maybe_arithmetic=true}) + lex_test("1-len", {"1-len"}, {}) + lex_test("tcp port 80", {"tcp", "port", 80}, {}) + lex_test("tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)", + { 'tcp', 'port', 80, 'and', + '(', '(', + '(', + 'ip', '[', 2, ':', 2, ']', '-', + '(', '(', 'ip', '[', 0, ']', '&', 15, ')', '<<', 2, ')', + ')', + '-', + '(', '(', 'tcp', '[', 12, ']', '&', 240, ')', '>>', 2, ')', + ')', '!=', 0, ')' + }, {maybe_arithmetic=true}) + lex_test("127.0.0.1", { { 'ipv4', 127, 0, 0, 1 } }, {address=true}) + lex_test("::", { { 'ipv6', 0, 0, 0, 0, 0, 0, 0, 0 } }, {address=true}) + lex_test("eee:eee:eee:eee:eee:eee:10.20.30.40", + { { 'ipv6', 3822, 3822, 3822, 3822, 3822, 3822, 2580, 7720 } }, {address=true}) + lex_test("::10.20.30.40", + { { 'ipv6', 0, 0, 0, 0, 0, 0, 2580, 7720 } }, {address=true}) + + local function addr_error_test(str, expected_err) + local lexer = tokens(str) + local ok, actual_err = pcall(lexer.peek, {address=true}) + if not ok then + if expected_err then + assert(actual_err:find(expected_err, 1, true), + "expected error "..expected_err.." but got "..actual_err) + end + else + error("expected error, got no error") + end + end + addr_error_test("1:1:1::1:1:1:1:1", "wrong IPv6 address") + addr_error_test("1:11111111", "wrong IPv6 address") + addr_error_test("1::1:", "wrong IPv6 address") + addr_error_test("1:2:3:4:5:6:7:1.2.3.4", "wrong IPv6 address") + addr_error_test("1:2:3:4:5:1.2.3.4", "wrong IPv6 address") + addr_error_test("1:2:3:4:5:1.2.3.4.5", "wrong IPv6 address") + addr_error_test("1:2:3:4:5:6:1.2.3..", "wrong IPv6 address") + addr_error_test("1:2:3:4:5:6:1.2.3.4.", "wrong IPv6 address") + addr_error_test("1:2:3:4:5:6:1.2.3.300", "wrong IPv6 address") + + local function parse_test(str, elts) check(elts, parse(str)) end + parse_test("", + { 'true' }) + parse_test("host 127.0.0.1", + { 'host', { 'ipv4', 127, 0, 0, 1 } }) + parse_test("host 1www.foo.com", + { 'host', '1www.foo.com' }) + parse_test("host 999.foo.com", + { 'host', '999.foo.com' }) + parse_test("host 200.foo.com", + { 'host', '200.foo.com' }) + parse_test("host 1.2.3.4foo.com", + { 'host', '1.2.3.4foo.com' }) + parse_test("host 1.2.3.4.5.com", + { 'host', '1.2.3.4.5.com' }) + parse_test("host 0xffffffffffoo.com", + { 'host', '0xffffffffffoo.com' }) + parse_test("host 0xffffffffff-oo.com", + { 'host', '0xffffffffff-oo.com' }) + parse_test("src host 127.0.0.1", + { 'src_host', { 'ipv4', 127, 0, 0, 1 } }) + parse_test("src net 10.0.0.0/24", + { 'src_net', + { 'ipv4/len', { 'ipv4', 10, 0, 0, 0 }, 24 }}) + parse_test("ether proto rarp", + { 'ether_proto', 'rarp' }) + parse_test("ether proto \\rarp", + { 'ether_proto', 'rarp' }) + parse_test("ether proto \\100", + { 'ether_proto', 100 }) + parse_test("ip proto tcp", + { 'ip_proto', 'tcp' }) + parse_test("ip proto \\tcp", + { 'ip_proto', 'tcp' }) + parse_test("ip proto \\0", + { 'ip_proto', 0 }) + parse_test("decnet host 10.23", + { 'decnet_host', { 'decnet', 10, 23 } }) + parse_test("ip proto icmp", + { 'ip_proto', 'icmp' }) + parse_test("ip6 protochain icmp", + { 'ip6_protochain', 'icmp' }) + parse_test("ip6 protochain 100", + { 'ip6_protochain', 100 }) + parse_test("ip", + { 'ip' }) + parse_test("type mgt", + { 'type', 'mgt' }) + parse_test("type mgt subtype deauth", + { 'type', 'mgt', 'deauth' }) + parse_test("1+1=2", + { '=', { '+', 1, 1 }, 2 }) + parse_test("len=4", { '=', 'len', 4 }) + parse_test("(len-4>10)", { '>', { '-', 'len', 4 }, 10 }) + parse_test("len == 4", { '=', 'len', 4 }) + parse_test("sctp", { 'sctp' }) + parse_test("1+2*3+4=5", + { '=', { '+', { '+', 1, { '*', 2, 3 } }, 4 }, 5 }) + parse_test("1+1=2 and tcp", + { 'and', { '=', { '+', 1, 1 }, 2 }, { 'tcp' } }) + parse_test("tcp port 80 and 1+1=2", + { 'and', { 'tcp_port', 80 }, { '=', { '+', 1, 1 }, 2 } }) + parse_test("1+1=2 and tcp or tcp", + { 'or', { 'and', { '=', { '+', 1, 1 }, 2 }, { 'tcp' } }, { 'tcp' } }) + parse_test("1+1=2 or tcp and tcp", + { 'and', { 'or', { '=', { '+', 1, 1 }, 2 }, { 'tcp' } }, { 'tcp' } }) + parse_test("not 1=1 or tcp", + { 'or', { 'not', { '=', 1, 1 } }, { 'tcp' } }) + parse_test("not (1=1 or tcp)", + { 'not', { 'or', { '=', 1, 1 }, { 'tcp' } } }) + parse_test("1+1=2 and (tcp)", + { 'and', { '=', { '+', 1, 1 }, 2 }, { 'tcp' } }) + parse_test("tcp && ip || !1=1", + { '||', { '&&', { 'tcp' }, { 'ip' } }, { '!', { '=', 1, 1 } } }) + parse_test("tcp src portrange 80-90", + { 'tcp_src_portrange', { 80, 90 } }) + parse_test("tcp src portrange ftp-data-90", + { 'tcp_src_portrange', { 20, 90 } }) + parse_test("tcp src portrange 80-ftp-data", + { 'tcp_src_portrange', { 20, 80 } }) -- swapped! + parse_test("tcp src portrange ftp-data-iso-tsap", + { 'tcp_src_portrange', { 20, 102 } }) + parse_test("tcp src portrange echo-ftp-data", + { 'tcp_src_portrange', { 7, 20 } }) + parse_test("tcp port 80", + { 'tcp_port', 80 }) + parse_test("tcp port 0x50", + { 'tcp_port', 80 }) + parse_test("tcp port 0120", + { 'tcp_port', 80 }) + parse_test("tcp port 80 and (((ip[2:2] - ((ip[0]&0xf)<<2)) - ((tcp[12]&0xf0)>>2)) != 0)", + { "and", + { "tcp_port", 80 }, + { "!=", + { "-", { "-", { "[ip]", 2, 2 }, + { "<<", { "&", { "[ip]", 0, 1 }, 15 }, 2 } }, + { ">>", { "&", { "[tcp]", 12, 1 }, 240 }, 2 } }, 0 } }) + parse_test("ether host ff:ff:ff:33:33:33", + { 'ether_host', { 'ehost', 255, 255, 255, 51, 51, 51 } }) + parse_test("fddi host ff:ff:ff:33:33:33", + { 'fddi_host', { 'ehost', 255, 255, 255, 51, 51, 51 } }) + parse_test("tr host ff:ff:ff:33:33:33", + { 'tr_host', { 'ehost', 255, 255, 255, 51, 51, 51 } }) + parse_test("wlan host ff:ff:ff:33:33:33", + { 'wlan_host', { 'ehost', 255, 255, 255, 51, 51, 51 } }) + parse_test("ether host f:f:f:3:3:3", + { 'ether_host', { 'ehost', 15, 15, 15, 3, 3, 3 } }) + parse_test("src net 192.168.1.0/24", + { 'src_net', { 'ipv4/len', { 'ipv4', 192, 168, 1, 0 }, 24 } }) + parse_test("src net 192.168.1.0 mask 255.255.255.0", + { 'src_net', { 'ipv4/mask', { 'ipv4', 192, 168, 1, 0 }, { 'ipv4', 255, 255, 255, 0 } } }) + parse_test("host 0:0:0:0:0:0:0:1", + { 'host', { 'ipv6', 0, 0, 0, 0, 0, 0, 0, 1 } }) + parse_test("host ::1", + { 'host', { 'ipv6', 0, 0, 0, 0, 0, 0, 0, 1 } }) + parse_test("host 1::1", + { 'host', { 'ipv6', 1, 0, 0, 0, 0, 0, 0, 1 } }) + parse_test("host 1::", + { 'host', { 'ipv6', 1, 0, 0, 0, 0, 0, 0, 0 } }) + parse_test("src net eee:eee::0/96", + { 'src_net', { 'ipv6/len', { 'ipv6', 3822, 3822, 0, 0, 0, 0, 0, 0 }, 96 } }) + parse_test("src net 3ffe:500::/28", + { 'src_net', { 'ipv6/len', { 'ipv6', 16382, 1280, 0, 0, 0, 0, 0, 0 }, 28 } }) + parse_test("src net 192.168.1.0/24", + { 'src_net', { 'ipv4/len', { 'ipv4', 192, 168, 1, 0 }, 24 } }) + parse_test("src net 192.168.1.0 mask 255.255.255.0", + { 'src_net', { 'ipv4/mask', { 'ipv4', 192, 168, 1, 0 }, { 'ipv4', 255, 255, 255, 0 } } }) + parse_test("less 100", {"less", 100}) + parse_test("greater 50 + 50", {"greater", {"+", 50, 50}}) + parse_test("sctp[8] < 8", {'<', { '[sctp]', 8, 1 }, 8}) + parse_test("igmp[8] < 8", {'<', { '[igmp]', 8, 1 }, 8}) + parse_test("igrp[8] < 8", {'<', { '[igrp]', 8, 1 }, 8}) + parse_test("pim[8] < 8", {'<', { '[pim]', 8, 1 }, 8}) + parse_test("vrrp[8] < 8", {'<', { '[vrrp]', 8, 1 }, 8}) + parse_test("not icmp6", {'not', { 'icmp6' } }) + parse_test("icmp[icmptype] != icmp-echo and icmp[icmptype] != icmp-echoreply", + { "and", + { "!=", { "[icmp]", 0, 1 }, 8 }, + { "!=", { "[icmp]", 0, 1 }, 0 } }) + parse_test("net 192.0.0.0", {'net', { 'ipv4', 192, 0, 0, 0 } }) + parse_test("net 192.168.1.0/24", + { 'net', { 'ipv4/len', { 'ipv4', 192, 168, 1, 0 }, 24 } }) + parse_test("net 192.168.1", + { 'net', { 'ipv4/len', { 'ipv4', 192, 168, 1, 0 }, 24 } }) + parse_test("net 192.168", + { 'net', { 'ipv4/len', { 'ipv4', 192, 168, 0, 0 }, 16 } }) + parse_test("net 192", + { 'net', { 'ipv4/len', { 'ipv4', 192, 0, 0, 0 }, 8 } }) + parse_test("net 192", + { 'net', { 'ipv4/len', { 'ipv4', 192, 0, 0, 0 }, 8 } }) + + local function parse_error_test(str, expected_err) + local ok, actual_err = pcall(parse, str) + assert(not ok, "expected error, got no error") + if expected_err then + assert(actual_err:find(expected_err, 1, true), + "expected error "..expected_err.." but got "..actual_err) + end + end + parse_error_test("tcp src portrange 80-fffftp-data", "error parsing portrange 80-fffftp-data") + parse_error_test("tcp src portrange 80000-90000", "port 80000 out of range") + parse_error_test("tcp src portrange 0x1-0x2", "error parsing portrange 0x1-0x2") + parse_error_test("tcp src portrange ::1", "error parsing portrange :") + parse_error_test("tcp src port ::1", "unsupported port :") + parse_error_test("123$", "unexpected end of decimal literal at 1") + parse_error_test("0x123$", "unexpected end of hexadecimal literal at 1") + parse_error_test("0123$", "unexpected end of octal literal at 1") + parse_error_test("0 = 0x", "unexpected end of hexadecimal literal at 5") + parse_error_test("0 = 08", "unexpected end of octal literal at 5") + parse_error_test("0 = 09", "unexpected end of octal literal at 5") + parse_error_test("host 0xffffffffff and tcp", "integer too large: 0xffffffffff") + print("OK") +end diff --git a/src/pf/quickcheck.lua b/src/pf/quickcheck.lua new file mode 100644 index 0000000000..544970ddf9 --- /dev/null +++ b/src/pf/quickcheck.lua @@ -0,0 +1,122 @@ +module(...,package.seeall) + +local utils = require('pf.utils') + +local program_name = 'pflua-quickcheck' + +local seed, iterations, prop_name, prop_args, prop, prop_info + +-- Due to limitations of Lua 5.1, finding if a command failed is convoluted. +local function find_gitrev() + local fd = io.popen('git rev-parse HEAD 2>/dev/null ; echo -n "$?"') + local cmdout = fd:read("*all") + fd:close() -- Always true in 5.1, with Lua or LuaJIT + local _, _, git_ret = cmdout:find("(%d+)$") + git_ret = tonumber(git_ret) + if git_ret ~= 0 then -- Probably not in a git repo + return nil + else + local _, _, sha1 = cmdout:find("(%x+)") + return sha1 + end +end + +local function print_gitrev_if_available() + local rev = find_gitrev() + if rev then print(("Git revision %s"):format(rev)) end +end + +local function rerun_usage(i) + print(("Rerun as: %s --seed=%s --iterations=%s %s %s"): + format(program_name, seed, i + 1, + prop_name, table.concat(prop_args, " "))) +end + +function initialize(options) + seed, iterations, prop_name, prop_args = + options.seed, options.iterations, options.prop_name, options.prop_args + + if not seed then + seed = math.floor(utils.gmtime() * 1e6) % 10^9 + print("Using time as seed: "..seed) + end + math.randomseed(assert(tonumber(seed))) + + if not iterations then iterations = 1000 end + + if not prop_name then + error("No property name specified") + end + + prop = require(prop_name) + if prop.handle_prop_args then + prop_info = prop.handle_prop_args(prop_args) + else + assert(#prop_args == 0, + "Property does not take options "..prop_name) + prop_info = nil + end +end + +function initialize_from_command_line(args) + local options = {} + while #args >= 1 and args[1]:match("^%-%-") do + local arg, _, val = table.remove(args, 1):match("^%-%-([^=]*)(=(.*))$") + assert(arg) + if arg == 'seed' then options.seed = assert(tonumber(val)) + elseif arg == 'iterations' then options.iterations = assert(tonumber(val)) + else error("Unknown argument: " .. arg) end + end + if #args < 1 then + print("Usage: " .. + program_name .. + " [--seed=SEED]" .. + " [--iterations=ITERATIONS]" .. + " property_file [property_specific_args]") + os.exit(1) + end + options.prop_name = table.remove(args, 1) + options.prop_args = args + initialize(options) +end + +function run() + if not prop then + error("Call initialize() or initialize_from_command_line() first") + end + + for i = 1,iterations do + -- Wrap property and its arguments in a 0-arity function for xpcall + local wrap_prop = function() return prop.property(prop_info) end + local propgen_ok, expected, got = xpcall(wrap_prop, debug.traceback) + if not propgen_ok then + print(("Crashed generating properties on run %s."):format(i)) + if prop.print_extra_information then + print("Attempting to print extra information; it may be wrong.") + if not pcall(prop.print_extra_information) + then print("Something went wrong printing extra info.") + end + end + print("Traceback (this is reliable):") + print(expected) -- This is an error code and traceback in this case + rerun_usage(i) + os.exit(1) + end + if not utils.equals(expected, got) then + print_gitrev_if_available() + print("The property was falsified.") + -- If the property file has extra info available, show it + if prop.print_extra_information then + prop.print_extra_information() + else + print('Expected:') + utils.pp(expected) + print('Got:') + utils.pp(got) + end + rerun_usage(i) + os.exit(1) + end + end + print(iterations.." iterations succeeded.") +end diff --git a/src/pf/savefile.lua b/src/pf/savefile.lua new file mode 100644 index 0000000000..6b4dbf9676 --- /dev/null +++ b/src/pf/savefile.lua @@ -0,0 +1,76 @@ +module(...,package.seeall) + +local ffi = require("ffi") +local C = ffi.C +local types = require("pf.types") + +ffi.cdef[[ +int open(const char *pathname, int flags); +int close(int fd); +typedef long int off_t; +off_t lseek(int fd, off_t offset, int whence); +void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); +int munmap(void *addr, size_t length); +]] + +function open(filename) + return C.open(filename, 0) +end + +function mmap(fd, size) + local PROT_READ = 1 + local MAP_PRIVATE = 2 + local ptr = C.mmap(ffi.cast("void *", 0), size, PROT_READ, MAP_PRIVATE, fd, 0) + if ptr == ffi.cast("void *", -1) then + error("Error mmapping") + end + return ptr +end + +function size(fd) + local SEEK_SET = 0 + local SEEK_END = 2 + local size = C.lseek(fd, 0, SEEK_END) + C.lseek(fd, 0, SEEK_SET) + return size +end + +function open_and_mmap(filename) + local fd = open(filename, O_RDONLY) + if fd == -1 then + error("Error opening " .. filename) + end + + local sz = size(fd) + local ptr = mmap(fd, sz) + C.close(fd) + + if ptr == ffi.cast("void *", -1) then + error("Error mmapping " .. filename) + end + + ptr = ffi.cast("unsigned char *", ptr) + local ptr_end = ptr + sz + local header = ffi.cast("struct pcap_file *", ptr) + if header.magic_number == 0xD4C3B2A1 then + error("Endian mismatch in " .. filename) + elseif header.magic_number ~= 0xA1B2C3D4 then + error("Bad PCAP magic number in " .. filename) + end + + return header, ptr + ffi.sizeof("struct pcap_file"), ptr_end +end + +function load_packets(filename) + local _, ptr, ptr_end = open_and_mmap(filename) + local ret = {} + local i = 1 + while ptr < ptr_end do + local record = ffi.cast("struct pcap_record *", ptr) + local packet = ffi.cast("unsigned char *", record + 1) + ret[i] = { packet=packet, len=record.incl_len } + i = i + 1 + ptr = packet + record.incl_len + end + return ret +end diff --git a/src/pf/ssa.lua b/src/pf/ssa.lua new file mode 100644 index 0000000000..047d6c0abf --- /dev/null +++ b/src/pf/ssa.lua @@ -0,0 +1,327 @@ +module(...,package.seeall) + +local utils = require('pf.utils') + +local verbose = os.getenv("PF_VERBOSE"); + +local set, pp, dup, concat = utils.set, utils.pp, utils.dup, utils.concat + +local relops = set('<', '<=', '=', '!=', '>=', '>') + +--- SSA := { start=Label, blocks={Label=>Block, ...} } +--- Label := string +--- Block := { label=Label, bindings=[{name=Var, value=Expr},...], control=Control } +--- Expr := UnaryOp | BinaryOp | PacketAccess +--- Control := ['return', Bool|Call] | ['if', Bool, Label, Label] | ['goto',Label] +--- Bool := true | false | Comparison + +local function print_ssa(ssa) + local function block_repr(block) + local bindings = { 'bindings' } + for _,binding in ipairs(block.bindings) do + table.insert(bindings, { binding.name, binding.value }) + end + return { 'block', + { 'label', block.label }, + bindings, + { 'control', block.control } } + end + local blocks = { 'blocks' } + if ssa.order then + for order,label in ipairs(ssa.order) do + table.insert(blocks, block_repr(ssa.blocks[label])) + end + else + for label,block in pairs(ssa.blocks) do + table.insert(blocks, block_repr(block)) + end + end + pp({ 'ssa', { 'start', ssa.start }, blocks }) + return ssa +end + +local function lower(expr) + local label_counter = 0 + local ssa = { blocks = {} } + local function add_block() + label_counter = label_counter + 1 + local label = 'L'..label_counter + local block = { bindings={}, label=label } + ssa.blocks[label] = block + return block + end + local function finish_return(block, bool) + block.control = { 'return', bool } + end + local function finish_if(block, bool, kt, kf) + block.control = { 'if', bool, kt.label, kf.label } + end + local function finish_goto(block, k) + block.control = { 'goto', k.label } + end + local function compile_bool(expr, block, kt, kf) + assert(type(expr) == 'table') + local op = expr[1] + if op == 'if' then + local kthen, kelse = add_block(), add_block() + compile_bool(expr[2], block, kthen, kelse) + compile_bool(expr[3], kthen, kt, kf) + compile_bool(expr[4], kelse, kt, kf) + elseif op == 'let' then + local name, value, body = expr[2], expr[3], expr[4] + table.insert(block.bindings, { name=name, value=value }) + compile_bool(body, block, kt, kf) + elseif op == 'true' then + finish_goto(block, kt) + elseif op == 'false' then + finish_goto(block, kf) + elseif op == 'match' then + finish_return(block, { 'true' }) + elseif op == 'fail' then + finish_return(block, { 'false' }) + elseif op == 'call' then + finish_return(block, expr) + else + assert(relops[op]) + finish_if(block, expr, kt, kf) + end + end + local start, accept, reject = add_block(), add_block(), add_block() + compile_bool(expr, start, accept, reject) + finish_return(accept, { 'true' }) + finish_return(reject, { 'false' }) + ssa.start = start.label + return ssa +end + +local function compute_use_counts(ssa) + local result = {} + local visited = {} + local function visit(label) + result[label] = result[label] + 1 + if not visited[label] then + visited[label] = true + local block = ssa.blocks[label] + if block.control[1] == 'if' then + visit(block.control[3]) + visit(block.control[4]) + elseif block.control[1] == 'goto' then + visit(block.control[2]) + else + assert(block.control[1] == 'return') + -- Nothing to do. + end + end + end + for label,_ in pairs(ssa.blocks) do result[label] = 0 end + visit(ssa.start) + return result +end + +local relop_inversions = { + ['<']='>=', ['<=']='>', ['=']='!=', ['!=']='=', ['>=']='<', ['>']='<=' +} + +local function invert_bool(expr) + if expr[1] == 'true' then return { 'false' } end + if expr[1] == 'false' then return { 'true' } end + assert(relop_inversions[expr[1]]) + return { relop_inversions[expr[1]], expr[2], expr[3] } +end + +local function is_simple_expr(expr) + -- Simple := return true | return false | goto Label + if expr[1] == 'return' then + return expr[2][1] == 'true' or expr[2][1] == 'false' + end + return expr[1] == 'goto' +end + +local function is_simple_block(block) + -- Simple := return true | return false | goto Label + if #block.bindings ~= 0 then return nil end + return is_simple_expr(block.control) +end + +local function simplify(ssa) + local result = { start=ssa.start, blocks={} } + local use_counts = compute_use_counts(ssa) + local function visit(label) + if result.blocks[label] then return result.blocks[label] end + local block = dup(ssa.blocks[label]) + if block.control[1] == 'if' then + local t, f = visit(block.control[3]), visit(block.control[4]) + if (is_simple_block(t) and is_simple_block(f) and + t.control[1] == 'return' and f.control[1] == 'return') then + local t_val, f_val = t.control[2][1], f.control[2][1] + if t_val == f_val then + -- if EXP then return true else return true end -> return true + -- + -- This is valid because EXP can have no side effects and + -- has no control effect. + block.control = t.control + elseif t_val == 'true' and f_val == 'false' then + -- if EXP then return true else return false -> return EXP + block.control = { 'return', block.control[2] } + else + assert(t_val == 'false' and f_val == 'true') + -- if EXP then return false else return true -> return not EXP + block.control = { 'return', invert_bool(block.control[2]) } + end + else + local control = { 'if', block.control[2], t.label, f.label } + if t.control[1] == 'goto' and #t.bindings == 0 then + control[3] = t.control[2] + end + if f.control[1] == 'goto' and #f.bindings == 0 then + control[4] = f.control[2] + end + block.control = control + end + elseif block.control[1] == 'goto' then + local k = visit(block.control[2]) + -- Inline blocks in cases where the inlining will not increase + -- code size, which is when the successor is simple (and thus + -- can be copied) or if the successor only has one predecessor. + if is_simple_block(k) or use_counts[block.control[2]] == 1 then + block.bindings = concat(block.bindings, k.bindings) + block.control = k.control + -- A subsequent iteration will remove the unused "k" block. + end + else + assert(block.control[1] == 'return') + -- Nothing to do. + end + result.blocks[label] = block + return block + end + visit(ssa.start) + return result +end + +local function optimize_ssa(ssa) + ssa = utils.fixpoint(simplify, ssa) + if verbose then pp(ssa) end + return ssa +end + +-- Compute a reverse-post-order sort of the blocks, which is a +-- topological sort. The result is an array of labels, from first to +-- last, which is set as the "order" property on the ssa. Each +-- block will also be given an "order" property. +local function order_blocks(ssa) + local tail = nil + local chain = {} -- label -> label | nil + local visited = {} -- label -> bool + local function visit(label) + if not visited[label] then visited[label] = true else return end + local block = ssa.blocks[label] + if block.control[1] == 'if' then + visit(block.control[4]) + visit(block.control[3]) + elseif block.control[1] == 'goto' then + visit(block.control[2]) + else + assert(block.control[1] == 'return') + end + chain[label] = tail + tail = label + end + visit(ssa.start) + local order = 1 + ssa.order = {} + while tail do + ssa.blocks[tail].order = order + ssa.order[order] = tail + tail = chain[tail] + order = order + 1 + end +end + +-- Add a "preds" property to all blocks, which is a list of labels of +-- predecessors. +local function add_predecessors(ssa) + local function visit(label, block) + local function add_predecessor(succ) + table.insert(ssa.blocks[succ].preds, label) + end + if block.control[1] == 'if' then + add_predecessor(block.control[3]) + add_predecessor(block.control[4]) + elseif block.control[1] == 'goto' then + add_predecessor(block.control[2]) + else + assert(block.control[1] == 'return') + end + end + for label,block in pairs(ssa.blocks) do block.preds = {} end + for label,block in pairs(ssa.blocks) do visit(label, block) end +end + +-- Add an "idom" property to all blocks, which is the label of the +-- immediate dominator. It's trivial as we have no loops. +local function compute_idoms(ssa) + local function dom(d1, d2) + if d1 == d2 then return d1 end + -- We exploit the fact that a reverse post-order is a topological + -- sort, and so the sort order of the idom of a node is always + -- numerically less than the node itself. + if ssa.blocks[d1].order < ssa.blocks[d2].order then + return dom(d1, ssa.blocks[d2].idom) + else + return dom(ssa.blocks[d1].idom, d2) + end + end + for order,label in ipairs(ssa.order) do + local preds = ssa.blocks[label].preds + if #preds == 0 then + assert(label == ssa.start) + -- No idom for the first block. + else + local idom = preds[1] + -- If there is just one predecessor, the idom is that + -- predecessor. Otherwise it's the common dominator of the + -- first predecessor and the other predecessors. + for j=2,#preds do + idom = dom(idom, preds[j]) + end + ssa.blocks[label].idom = idom + end + end +end + +local function compute_doms(ssa) + for order,label in ipairs(ssa.order) do + local block = ssa.blocks[label] + block.doms = {} + if block.idom then + table.insert(ssa.blocks[block.idom].doms, label) + end + end +end + +function convert_ssa(anf) + local ssa = optimize_ssa(lower(anf)) + order_blocks(ssa) + add_predecessors(ssa) + compute_idoms(ssa) + compute_doms(ssa) + if verbose then print_ssa(ssa) end + return ssa +end + +function selftest() + print("selftest: pf.ssa") + local parse = require('pf.parse').parse + local expand = require('pf.expand').expand + local optimize = require('pf.optimize').optimize + local convert_anf = require('pf.anf').convert_anf + + local function test(expr) + return convert_ssa(convert_anf(optimize(expand(parse(expr), "EN10MB")))) + end + + test("tcp port 80 or udp port 34") + + print("OK") +end diff --git a/src/pf/types.lua b/src/pf/types.lua new file mode 100644 index 0000000000..4ad1758864 --- /dev/null +++ b/src/pf/types.lua @@ -0,0 +1,61 @@ +module(...,package.seeall) + +local ffi = require("ffi") + +-- PCAP file format: http://wiki.wireshark.org/Development/LibpcapFileFormat/ +ffi.cdef[[ +struct pcap_file { + /* file header */ + uint32_t magic_number; /* magic number */ + uint16_t version_major; /* major version number */ + uint16_t version_minor; /* minor version number */ + int32_t thiszone; /* GMT to local correction */ + uint32_t sigfigs; /* accuracy of timestamps */ + uint32_t snaplen; /* max length of captured packets, in octets */ + uint32_t network; /* data link type */ +}; + +/* This is the header of a packet on disk. */ +struct pcap_record { + /* record header */ + uint32_t ts_sec; /* timestamp seconds */ + uint32_t ts_usec; /* timestamp microseconds */ + uint32_t incl_len; /* number of octets of packet saved in file */ + uint32_t orig_len; /* actual length of packet */ +}; + +/* This is the header of a packet as passed to pcap_offline_filter. */ +struct pcap_pkthdr { + /* record header */ + long ts_sec; /* timestamp seconds */ + long ts_usec; /* timestamp microseconds */ + uint32_t incl_len; /* number of octets of packet saved in file */ + uint32_t orig_len; /* actual length of packet */ +}; +]] + +-- BPF program format. Note: the bit module represents uint32_t values +-- with the high-bit set as negative int32_t values, so we do the same +-- for all of our 32-bit values including the "k" field in BPF +-- instructions. +ffi.cdef[[ +struct bpf_insn { uint16_t code; uint8_t jt, jf; int32_t k; }; +struct bpf_program { uint32_t bf_len; struct bpf_insn *bf_insns; }; +]] +local bpf_program_mt = { + __len = function (program) return program.bf_len end, + __index = function (program, idx) + assert(idx >= 0 and idx < #program) + return program.bf_insns[idx] + end +} + +bpf_insn = ffi.typeof("struct bpf_insn") +bpf_program = ffi.metatype("struct bpf_program", bpf_program_mt) +pcap_record = ffi.typeof("struct pcap_record") +pcap_pkthdr = ffi.typeof("struct pcap_pkthdr") + +function selftest () + print("selftest: ffi_types") + print("OK") +end diff --git a/src/pf/utils.lua b/src/pf/utils.lua new file mode 100644 index 0000000000..ddab58f75c --- /dev/null +++ b/src/pf/utils.lua @@ -0,0 +1,207 @@ +module(...,package.seeall) + +local ffi = require("ffi") +local C = ffi.C + +ffi.cdef[[ +struct pflua_timeval { + long tv_sec; /* seconds */ + long tv_usec; /* microseconds */ +}; +int gettimeofday(struct pflua_timeval *tv, struct timezone *tz); +]] + +-- now() returns the current time. The first time it is called, the +-- return value will be zero. This is to preserve precision, regardless +-- of what the current epoch is. +local zero_sec, zero_usec +function now() + local tv = ffi.new("struct pflua_timeval") + assert(C.gettimeofday(tv, nil) == 0) + if not zero_sec then + zero_sec = tv.tv_sec + zero_usec = tv.tv_usec + end + local secs = tonumber(tv.tv_sec - zero_sec) + secs = secs + tonumber(tv.tv_usec - zero_usec) * 1e-6 + return secs +end + +function gmtime() + local tv = ffi.new("struct pflua_timeval") + assert(C.gettimeofday(tv, nil) == 0) + local secs = tonumber(tv.tv_sec) + secs = secs + tonumber(tv.tv_usec) * 1e-6 + return secs +end + +function set(...) + local ret = {} + for k, v in pairs({...}) do ret[v] = true end + return ret +end + +function concat(a, b) + local ret = {} + for _, v in ipairs(a) do table.insert(ret, v) end + for _, v in ipairs(b) do table.insert(ret, v) end + return ret +end + +function dup(table) + local ret = {} + for k, v in pairs(table) do ret[k] = v end + return ret +end + +function equals(expected, actual) + if type(expected) ~= type(actual) then return false end + if type(expected) == 'table' then + for k, v in pairs(expected) do + if not equals(v, actual[k]) then return false end + end + for k, _ in pairs(actual) do + if expected[k] == nil then return false end + end + return true + else + return expected == actual + end +end + +function is_array(x) + if type(x) ~= 'table' then return false end + if #x == 0 then return false end + for k,v in pairs(x) do + if type(k) ~= 'number' then return false end + -- Restrict to unsigned 32-bit integer keys. + if k < 0 or k >= 2^32 then return false end + -- Array indices are integers. + if k - math.floor(k) ~= 0 then return false end + -- Negative zero is not a valid array index. + if 1 / k < 0 then return false end + end + return true +end + +function pp(expr, indent, suffix) + indent = indent or '' + suffix = suffix or '' + if type(expr) == 'number' then + print(indent..expr..suffix) + elseif type(expr) == 'string' then + print(indent..'"'..expr..'"'..suffix) + elseif type(expr) == 'boolean' then + print(indent..(expr and 'true' or 'false')..suffix) + elseif is_array(expr) then + assert(#expr > 0) + if #expr == 1 then + if type(expr[1]) == 'table' then + print(indent..'{') + pp(expr[1], indent..' ', ' }'..suffix) + else + print(indent..'{ "'..expr[1]..'" }'..suffix) + end + else + if type(expr[1]) == 'table' then + print(indent..'{') + pp(expr[1], indent..' ', ',') + else + print(indent..'{ "'..expr[1]..'",') + end + indent = indent..' ' + for i=2,#expr-1 do pp(expr[i], indent, ',') end + pp(expr[#expr], indent, ' }'..suffix) + end + elseif type(expr) == 'table' then + if #expr == 0 then + print(indent .. '{}') + else + error('unimplemented') + end + else + error("unsupported type "..type(expr)) + end + return expr +end + +function assert_equals(expected, actual) + if not equals(expected, actual) then + pp(expected) + pp(actual) + error('not equal') + end +end + +-- Construct uint32 from octets a, b, c, d; a is most significant. +function uint32(a, b, c, d) + return a * 2^24 + b * 2^16 + c * 2^8 + d +end + +-- Construct uint16 from octets a, b; a is most significant. +function uint16(a, b) + return a * 2^8 + b +end + +function ipv4_to_int(addr) + assert(addr[1] == 'ipv4', "Not an IPV4 address") + return uint32(addr[2], addr[3], addr[4], addr[5]) +end + +function ipv6_as_4x32(addr) + local function c(i, j) return addr[i] * 2^16 + addr[j] end + return { c(2,3), c(4,5), c(6,7), c(8,9) } +end + +function fixpoint(f, expr) + local prev + repeat expr, prev = f(expr), expr until equals(expr, prev) + return expr +end + +function choose(choices) + local idx = math.random(#choices) + return choices[idx] +end + +function choose_with_index(choices) + local idx = math.random(#choices) + return choices[idx], idx +end + +function parse_opts(opts, defaults) + local ret = {} + for k, v in pairs(opts) do + if defaults[k] == nil then error('unrecognized option ' .. k) end + ret[k] = v + end + for k, v in pairs(defaults) do + if ret[k] == nil then ret[k] = v end + end + return ret +end + +function table_values_all_equal(t) + local val + for _, v in pairs(t) do + if val == nil then val = v end + if v ~= val then return false end + end + return true, val +end + +function selftest () + print("selftest: pf.utils") + local tab = { 1, 2, 3 } + assert(tab ~= dup(tab)) + assert_equals(tab, dup(tab)) + assert_equals({ 1, 2, 3, 1, 2, 3 }, concat(tab, tab)) + assert_equals(set(3, 2, 1), set(1, 2, 3)) + if not zero_sec then assert_equals(now(), 0) end + assert(now() > 0) + assert_equals(ipv4_to_int({'ipv4', 255, 0, 0, 0}), 0xff000000) + local gu1 = gmtime() + local gu2 = gmtime() + assert(gu1, gu2) + print("OK") +end diff --git a/tcp_port_80_asm.md b/tcp_port_80_asm.md new file mode 100644 index 0000000000..979402cc1e --- /dev/null +++ b/tcp_port_80_asm.md @@ -0,0 +1,118 @@ +# Generated ASM code + +Currently the following filter: + +``` +tcp port 80 +``` + +yields the following assembly (x86) code: + +```asm +0bca9db1 mov dword [0x41dd94a0], 0x38 +0bca9dbc movsd xmm6, [0x418121d0] +0bca9dc5 cmp dword [rdx+0x4], -0x09 +0bca9dc9 jnz 0x0bca0010 ->0 +0bca9dcf cmp dword [rdx+0xc], -0x0b +0bca9dd3 jnz 0x0bca0010 ->0 +0bca9dd9 mov r14d, [rdx+0x8] +0bca9ddd cmp dword [rdx+0x14], 0xfffeffff +0bca9de4 jnb 0x0bca0010 ->0 +0bca9dea movsd xmm7, [rdx+0x10] +0bca9def cmp dword [rdx], 0x40a2c170 +0bca9df5 jnz 0x0bca0010 ->0 +0bca9dfb ucomisd xmm7, xmm6 +0bca9dff jb 0x0bca0014 ->1 +0bca9e05 mov ebp, [0x40a2c178] +0bca9e0c cmp dword [rbp+0x1c], +0x3f +0bca9e10 jnz 0x0bca0018 ->2 +0bca9e16 mov ebx, [rbp+0x14] +0bca9e19 mov rdi, 0xfffffffb41de0c70 +0bca9e23 cmp rdi, [rbx+0x320] +0bca9e2a jnz 0x0bca0018 ->2 +0bca9e30 cmp dword [rbx+0x31c], -0x0c +0bca9e37 jnz 0x0bca0018 ->2 +0bca9e3d mov ebp, [rbx+0x318] +0bca9e43 cmp dword [rbp+0x1c], +0x1f +0bca9e47 jnz 0x0bca0018 ->2 +0bca9e4d mov r15d, [rbp+0x14] +0bca9e51 mov rdi, 0xfffffffb41dea978 +0bca9e5b cmp rdi, [r15+0x98] +0bca9e62 jnz 0x0bca0018 ->2 +0bca9e68 cmp dword [r15+0x94], -0x09 +0bca9e70 jnz 0x0bca0018 ->2 +0bca9e76 movzx ebp, word [r14+0x6] +0bca9e7b cmp ebp, 0xac +0bca9e81 jnz 0x0bca0018 ->2 +0bca9e87 mov rbp, [r14+0x8] +0bca9e8b cmp dword [r15+0x90], 0x41dec420 +0bca9e96 jnz 0x0bca0018 ->2 +0bca9e9c movzx r13d, word [rbp+0xc] +0bca9ea1 cmp r13d, +0x08 +0bca9ea5 jnz 0x0bca001c ->3 +0bca9eab movzx r12d, byte [rbp+0x17] +0bca9eb0 cmp r12d, +0x06 +0bca9eb4 jnz 0x0bca0020 ->4 +0bca9eba movzx edi, word [rbp+0x14] +0bca9ebe mov rsi, 0xfffffffb41ddfd00 +0bca9ec8 cmp rsi, [rbx+0x398] +0bca9ecf jnz 0x0bca0024 ->5 +0bca9ed5 cmp dword [rbx+0x394], -0x0c +0bca9edc jnz 0x0bca0024 ->5 +0bca9ee2 mov ebx, [rbx+0x390] +0bca9ee8 cmp dword [rbx+0x1c], +0x0f +0bca9eec jnz 0x0bca0024 ->5 +0bca9ef2 mov ebx, [rbx+0x14] +0bca9ef5 mov rsi, 0xfffffffb41de0138 +0bca9eff cmp rsi, [rbx+0x170] +0bca9f06 jnz 0x0bca0024 ->5 +0bca9f0c cmp dword [rbx+0x16c], -0x09 +0bca9f13 jnz 0x0bca0024 ->5 +0bca9f19 cmp dword [rbx+0x168], 0x41de0110 +0bca9f23 jnz 0x0bca0024 ->5 +0bca9f29 mov esi, edi +0bca9f2b and esi, 0xff1f +0bca9f31 jnz 0x0bca0028 ->6 +0bca9f37 movzx ecx, byte [rbp+0xe] +0bca9f3b mov eax, ecx +0bca9f3d and eax, +0x0f +0bca9f40 mov r15, 0xfffffffb41ddffd0 +0bca9f4a cmp r15, [rbx+0x140] +0bca9f51 jnz 0x0bca002c ->7 +0bca9f57 cmp dword [rbx+0x13c], -0x09 +0bca9f5e jnz 0x0bca002c ->7 +0bca9f64 cmp dword [rbx+0x138], 0x41ddffa8 +0bca9f6e jnz 0x0bca002c ->7 +0bca9f74 mov r11d, ecx +0bca9f77 shl r11d, 0x02 +0bca9f7b and r11d, +0x3c +0bca9f7f mov ebx, r11d +0bca9f82 add ebx, +0x10 +0bca9f85 jo 0x0bca002c ->7 +0bca9f8b xorps xmm6, xmm6 +0bca9f8e cvtsi2sd xmm6, ebx +0bca9f92 ucomisd xmm7, xmm6 +0bca9f96 jb 0x0bca0030 ->8 +0bca9f9c mov r10d, r11d +0bca9f9f add r10d, +0x0e +0bca9fa3 jo 0x0bca0034 ->9 +0bca9fa9 movsxd r15, r10d +0bca9fac movzx r9d, word [r15+rbp] +0bca9fb1 cmp r9d, 0x5000 +0bca9fb8 jz 0x0bca0038 ->10 +0bca9fbe mov r15d, r11d +0bca9fc1 add r15d, +0x12 +0bca9fc5 jo 0x0bca003c ->11 +0bca9fcb xorps xmm6, xmm6 +0bca9fce cvtsi2sd xmm6, r15d +0bca9fd3 ucomisd xmm7, xmm6 +0bca9fd7 jb 0x0bca0040 ->12 +0bca9fdd movsxd rbx, ebx +0bca9fe0 movzx ebp, word [rbx+rbp] +0bca9fe4 cmp ebp, 0x5000 +0bca9fea jz 0x0bca0048 ->14 +0bca9ff0 xor eax, eax +0bca9ff2 mov ebx, 0x41df874c +0bca9ff7 mov r14d, 0x41dd9f78 +0bca9ffd jmp 0x0041e288 +``` diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000000..12f1db6fde --- /dev/null +++ b/tests/Makefile @@ -0,0 +1,51 @@ +TOP_SRCDIR:=.. +include $(TOP_SRCDIR)/common.mk + +# Tests where the pure-lua pipeline and/or bpf-lua pipeline diverged from libpcap. +PFLANG_REGRESSION_TESTS := $(wildcard pflang-reg/*) +IR_REGRESSION_TESTS := $(wildcard ir-reg/*.sh) + +TEST_SAVEFILE_URL='https://github.com/Igalia/pflua-test/blob/master/savefiles/wingolog.org.pcap?raw=true' +TEST_SAVEFILE=data/wingolog.pcap +PFLUA_QUICKCHECK=$(ABS_TOP_SRCDIR)/tools/pflua-quickcheck + +all: + @true + +check: + $(MAKE) $(TEST_SAVEFILE) + $(MAKE) regression quickcheck + ./test-matches data + # Make sure PF_VERBOSE isn't causing crashes + PF_VERBOSE=1 $(ABS_TOP_SRCDIR)/tools/pflua-compile "ip" >/dev/null 2>&1 + +$(TEST_SAVEFILE): + wget --output-document $@ $(TEST_SAVEFILE_URL) + +quickcheck: + $(MAKE) $(TEST_SAVEFILE) + $(PFLUA_QUICKCHECK) properties/repeatable_randomization + $(PFLUA_QUICKCHECK) properties/pflua_math_eq_libpcap_math + $(PFLUA_QUICKCHECK) --iterations=100 properties/pflua_pipelines_match $(TEST_SAVEFILE) + $(PFLUA_QUICKCHECK) properties/opt_eq_unopt $(TEST_SAVEFILE) + $(PFLUA_QUICKCHECK) properties/opt_eq_unopt $(TEST_SAVEFILE) test-filters + $(PFLUA_QUICKCHECK) properties/pipecmp_proto_or_proto $(TEST_SAVEFILE) + +regression: pflang_regression_tests ir_regression_tests + +ir_regression_tests: + $(MAKE) $(TEST_SAVEFILE) + $(MAKE) $(IR_REGRESSION_TESTS) + +.PHONY: $(IR_REGRESSION_TESTS) +$(IR_REGRESSION_TESTS): + ./$@ + +pflang_regression_tests: + $(MAKE) $(TEST_SAVEFILE) + $(MAKE) $(PFLANG_REGRESSION_TESTS) + +.PHONY: $(PFLANG_REGRESSION_TESTS) +$(PFLANG_REGRESSION_TESTS): + ./$@ +.SERIAL: all diff --git a/tests/data/COPYRIGHT b/tests/data/COPYRIGHT new file mode 100644 index 0000000000..9d0e908da1 --- /dev/null +++ b/tests/data/COPYRIGHT @@ -0,0 +1,14 @@ +The following packet captures are from +http://wiki.wireshark.org/SampleCaptures, and available under +http://wiki.wireshark.org/License: + + telnet-cooked.pcap + tftp_wrq.pcap + v4.pcap + v6.pcap + + +The following packet captures are from http://www.igalia.com, and are +available under the license of pflua itself: + + empty.pcap diff --git a/tests/data/arp.pcap b/tests/data/arp.pcap new file mode 100644 index 0000000000000000000000000000000000000000..e7326e19edfcdcb3207ae9de0ac5ed9db34f0735 GIT binary patch literal 158 zcmca|c+)~A1{MYcU}0bca&|}l4_-KjnZX9g2H}5yH~CL{*;274O3IOgje(Jafeok- l!UxJQLF7Sd5GKJ`AhXT_&C&u2fG|WCL>2)) != 0) diff --git a/tests/data/tcp-ack-66-bytes.pcap b/tests/data/tcp-ack-66-bytes.pcap new file mode 100644 index 0000000000000000000000000000000000000000..776adc82891cbaa4b1b34de6df05a4f05c15add9 GIT binary patch literal 106 zcmca|c+)~A1{MYcU}0bcauOo{2e)oyVsHYoLHOU_P5#qfwp6U)?RVs0aAjaH5iECL xuw>iHF?HRXSdiT14GaMa>kOmz2Tgm{AiyAhgpGlLk&%OIiMLAfkG($^0|4VsAj<#% literal 0 HcmV?d00001 diff --git a/tests/data/telnet-cooked.pcap b/tests/data/telnet-cooked.pcap new file mode 100644 index 0000000000000000000000000000000000000000..34515d415da7a543efe73bde6e50a22c4e43d1e5 GIT binary patch literal 9244 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 literal 0 HcmV?d00001 diff --git a/tests/data/telnet-cooked.pcap.test b/tests/data/telnet-cooked.pcap.test new file mode 100644 index 0000000000..cfe3a182ea --- /dev/null +++ b/tests/data/telnet-cooked.pcap.test @@ -0,0 +1,3 @@ +tcp dst port: 48: tcp dst port 23 +net 192.168.0.0 mask 255.255.255.0: 92: net 192.168.0.0 mask 255.255.255.0 +net 192.168.50.0 mask 255.255.255.0: 0: net 192.168.50.0 mask 255.255.255.0 diff --git a/tests/data/tftp_wrq.pcap b/tests/data/tftp_wrq.pcap new file mode 100644 index 0000000000000000000000000000000000000000..abb986aecbce6b0ecd32dfd18242b84a8258221c GIT binary patch literal 30839 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 literal 0 HcmV?d00001 diff --git a/tests/ir-reg/opt-bug120-rangecheck-opt.ir b/tests/ir-reg/opt-bug120-rangecheck-opt.ir new file mode 100644 index 0000000000..93c00ef3d7 --- /dev/null +++ b/tests/ir-reg/opt-bug120-rangecheck-opt.ir @@ -0,0 +1,6 @@ +{ "if", + { "!=", + "len", + 0 }, + { "false" }, + { "fail" } } diff --git a/tests/ir-reg/opt-bug120-rangecheck-unopt.ir b/tests/ir-reg/opt-bug120-rangecheck-unopt.ir new file mode 100644 index 0000000000..8d37f0857e --- /dev/null +++ b/tests/ir-reg/opt-bug120-rangecheck-unopt.ir @@ -0,0 +1,17 @@ +{ "if", + { "!=", + "len", + 0 }, + { ">", + 1, + { "uint32", + { "+", + { "uint32", + { "-", + 4294967295, + { "uint32", + { "/", + 1, + "len" } } } }, + 1 } } }, + { "fail" } } diff --git a/tests/ir-reg/opt-bug120-rangecheck.sh b/tests/ir-reg/opt-bug120-rangecheck.sh new file mode 100755 index 0000000000..41f6935b74 --- /dev/null +++ b/tests/ir-reg/opt-bug120-rangecheck.sh @@ -0,0 +1,7 @@ +#!/bin/bash +thisdir=$(dirname $0) +! "${thisdir}/../../env" pflua-pipelines-match --ir "${thisdir}/../data/wingolog.pcap" \ + "${thisdir}/opt-bug120-rangecheck-unopt.ir" "${thisdir}/opt-bug120-rangecheck-opt.ir" 13965 > /dev/null + +"${thisdir}/../../env" pflua-pipelines-match --ir --opt-ir "${thisdir}/../data/wingolog.pcap" \ + "${thisdir}/opt-bug120-rangecheck-unopt.ir" 13965 diff --git a/tests/ir-reg/opt-bug126-invalidopt.sh b/tests/ir-reg/opt-bug126-invalidopt.sh new file mode 100755 index 0000000000..28c688494e --- /dev/null +++ b/tests/ir-reg/opt-bug126-invalidopt.sh @@ -0,0 +1,6 @@ +#!/bin/bash +thisdir=$(dirname $0) +! "${thisdir}/../../env" pflua-pipelines-match --ir "${thisdir}/../data/wingolog.pcap" \ + "${thisdir}/opt-bug126-unopt.ir" "${thisdir}/opt-bug126-opt.ir" 13965 > /dev/null + +"${thisdir}/../../env" pflua-pipelines-match --ir --opt-ir "${thisdir}/../data/wingolog.pcap" "${thisdir}/opt-bug126-unopt.ir" 13965 diff --git a/tests/ir-reg/opt-bug126-opt.ir b/tests/ir-reg/opt-bug126-opt.ir new file mode 100644 index 0000000000..459643eaa3 --- /dev/null +++ b/tests/ir-reg/opt-bug126-opt.ir @@ -0,0 +1,90 @@ +{ "if", + { ">=", + "len", + 124 }, + { "if", + { "if", + { "<", + "len", + { "uint32", + { "+", + "len", + 4294967295 } } }, + { "if", + { ">=", + "len", + { "+", + "len", + 4 } }, + { "if", + { "!=", + { "ntohs", + { "[]", + "len", + 4 } }, + 0 }, + { "if", + { "!=", + { "ntohs", + { "[]", + 120, + 4 } }, + 0 }, + { "if", + { ">=", + "len", + { "+", + { "-", + { "/", + { "+", + { "+", + 2147483648, + "len" }, + { "ntohs", + { "[]", + 1, + 4 } } }, + { "ntohs", + { "[]", + "len", + 4 } } }, + { "/", + 1, + { "ntohs", + { "[]", + 120, + 4 } } } }, + 4 } }, + { "<", + { "ntohs", + { "[]", + { "-", + { "/", + { "+", + { "+", + 2147483648, + "len" }, + { "ntohs", + { "[]", + 1, + 4 } } }, + { "ntohs", + { "[]", + "len", + 4 } } }, + { "/", + 1, + { "ntohs", + { "[]", + 120, + 4 } } } }, + 4 } }, + 0 }, + { "fail" } }, + { "fail" } }, + { "fail" } }, + { "fail" } }, + { "false" } }, + { "fail" }, + { "true" } }, + { "fail" } } diff --git a/tests/ir-reg/opt-bug126-unopt.ir b/tests/ir-reg/opt-bug126-unopt.ir new file mode 100644 index 0000000000..ea4e1571d6 --- /dev/null +++ b/tests/ir-reg/opt-bug126-unopt.ir @@ -0,0 +1,238 @@ +{ "if", + { "if", + { "<", + "len", + { "uint32", + { "+", + "len", + 4294967295 } } }, + { "if", + { "if", + { "true" }, + { "true" }, + { "false" } }, + { "if", + { ">=", + "len", + { "+", + 1, + 4 } }, + { "if", + { ">=", + "len", + { "+", + "len", + 4 } }, + { "if", + { "!=", + { "uint32", + { "ntohs", + { "[]", + "len", + 4 } } }, + 0 }, + { "if", + { "!=", + 35575672, + 0 }, + { "if", + { ">=", + "len", + { "+", + { "uint32", + { "/", + 4294967295, + 35575672 } }, + 4 } }, + { "if", + { "!=", + { "uint32", + { "ntohs", + { "[]", + { "uint32", + { "/", + 4294967295, + 35575672 } }, + 4 } } }, + 0 }, + { "if", + { ">=", + "len", + { "+", + { "uint32", + { "-", + { "uint32", + { "/", + { "uint32", + { "+", + { "uint32", + { "+", + 2147483648, + "len" } }, + { "uint32", + { "ntohs", + { "[]", + 1, + 4 } } } } }, + { "uint32", + { "ntohs", + { "[]", + "len", + 4 } } } } }, + { "uint32", + { "/", + 1, + { "uint32", + { "ntohs", + { "[]", + { "uint32", + { "/", + 4294967295, + 35575672 } }, + 4 } } } } } } }, + 4 } }, + { "<", + { "uint32", + { "ntohs", + { "[]", + { "uint32", + { "-", + { "uint32", + { "/", + { "uint32", + { "+", + { "uint32", + { "+", + 2147483648, + "len" } }, + { "uint32", + { "ntohs", + { "[]", + 1, + 4 } } } } }, + { "uint32", + { "ntohs", + { "[]", + "len", + 4 } } } } }, + { "uint32", + { "/", + 1, + { "uint32", + { "ntohs", + { "[]", + { "uint32", + { "/", + 4294967295, + 35575672 } }, + 4 } } } } } } }, + 4 } } }, + 0 }, + { "fail" } }, + { "fail" } }, + { "fail" } }, + { "fail" } }, + { "fail" } }, + { "fail" } }, + { "fail" } }, + { "true" } }, + { "false" } }, + { "if", + { "if", + { "fail" }, + { "if", + { "true" }, + { "false" }, + { "fail" } }, + { "if", + { "if", + { ">=", + "len", + { "+", + 4294967295, + 4 } }, + { "<", + { "uint32", + { "-", + "len", + "len" } }, + { "uint32", + { "ntohs", + { "[]", + 4294967295, + 4 } } } }, + { "fail" } }, + { "fail" }, + { "true" } } }, + { "false" }, + { "if", + { "!=", + "len", + 0 }, + { "if", + { ">=", + "len", + { "+", + { "uint32", + { "+", + 1, + { "uint32", + { "/", + "len", + "len" } } } }, + 2 } }, + { "if", + { ">=", + "len", + { "+", + "len", + 4 } }, + { "if", + { ">=", + "len", + { "+", + { "uint32", + { "+", + { "ntohs", + { "[]", + { "uint32", + { "+", + 1, + { "uint32", + { "/", + "len", + "len" } } } }, + 2 } }, + { "uint32", + { "ntohs", + { "[]", + "len", + 4 } } } } }, + 2 } }, + { "<", + { "ntohs", + { "[]", + { "uint32", + { "+", + { "ntohs", + { "[]", + { "uint32", + { "+", + 1, + { "uint32", + { "/", + "len", + "len" } } } }, + 2 } }, + { "uint32", + { "ntohs", + { "[]", + "len", + 4 } } } } }, + 2 } }, + "len" }, + { "fail" } }, + { "fail" } }, + { "fail" } }, + { "fail" } } }, + { "true" } } diff --git a/tests/pflang-reg/pl-bug129-flipportrange.sh b/tests/pflang-reg/pl-bug129-flipportrange.sh new file mode 100755 index 0000000000..559e81518f --- /dev/null +++ b/tests/pflang-reg/pl-bug129-flipportrange.sh @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/wingolog.pcap" "portrange 49577-19673" 938 diff --git a/tests/pflang-reg/pl-bug130-allreject b/tests/pflang-reg/pl-bug130-allreject new file mode 100755 index 0000000000..f8d6307571 --- /dev/null +++ b/tests/pflang-reg/pl-bug130-allreject @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/arp.pcap" "portrange 1-2 and arp" 1 diff --git a/tests/pflang-reg/pl-bug131-notlen-igrp b/tests/pflang-reg/pl-bug131-notlen-igrp new file mode 100755 index 0000000000..ce61151449 --- /dev/null +++ b/tests/pflang-reg/pl-bug131-notlen-igrp @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/wingolog.pcap" "not igrp" 3839 diff --git a/tests/pflang-reg/pl-bug131-notlen-rarp b/tests/pflang-reg/pl-bug131-notlen-rarp new file mode 100755 index 0000000000..2edc1cc1c7 --- /dev/null +++ b/tests/pflang-reg/pl-bug131-notlen-rarp @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/wingolog.pcap" "not rarp[181] > 163" 15638 diff --git a/tests/pflang-reg/pl-bug131-notlen-tcpport b/tests/pflang-reg/pl-bug131-notlen-tcpport new file mode 100755 index 0000000000..b91b4e540d --- /dev/null +++ b/tests/pflang-reg/pl-bug131-notlen-tcpport @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/wingolog.pcap" "not tcp port 59054" 2852 diff --git a/tests/pflang-reg/pl-bug132-icmp6_or_ip b/tests/pflang-reg/pl-bug132-icmp6_or_ip new file mode 100755 index 0000000000..a2d0c9fe2f --- /dev/null +++ b/tests/pflang-reg/pl-bug132-icmp6_or_ip @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/wingolog.pcap" "icmp6 or ip" 2442 diff --git a/tests/pflang-reg/pl-bug132-icmp6_or_portrange b/tests/pflang-reg/pl-bug132-icmp6_or_portrange new file mode 100755 index 0000000000..ba1f2925d7 --- /dev/null +++ b/tests/pflang-reg/pl-bug132-icmp6_or_portrange @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/wingolog.pcap" "icmp6 or portrange 14682-50101" 14951 diff --git a/tests/pflang-reg/pl-bug132-not_icmp6 b/tests/pflang-reg/pl-bug132-not_icmp6 new file mode 100755 index 0000000000..21607b802e --- /dev/null +++ b/tests/pflang-reg/pl-bug132-not_icmp6 @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/wingolog.pcap" "not icmp6" 1 diff --git a/tests/pflang-reg/pl-bug139-multioctet.sh b/tests/pflang-reg/pl-bug139-multioctet.sh new file mode 100755 index 0000000000..bf0706c421 --- /dev/null +++ b/tests/pflang-reg/pl-bug139-multioctet.sh @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/wingolog.pcap" "ip[29:2] < 231" 16794 diff --git a/tests/pflang-reg/pl-bug171-arpindexing_or_tcp b/tests/pflang-reg/pl-bug171-arpindexing_or_tcp new file mode 100755 index 0000000000..e98a718e41 --- /dev/null +++ b/tests/pflang-reg/pl-bug171-arpindexing_or_tcp @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/wingolog.pcap" "arp[1] > 1 or tcp" 3584 diff --git a/tests/pflang-reg/pl-bug182-icmp_or_arp b/tests/pflang-reg/pl-bug182-icmp_or_arp new file mode 100755 index 0000000000..77c00423fc --- /dev/null +++ b/tests/pflang-reg/pl-bug182-icmp_or_arp @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/arp.pcap" "icmp or arp" 2 diff --git a/tests/pflang-reg/pl-bug205-greater1_or_less1 b/tests/pflang-reg/pl-bug205-greater1_or_less1 new file mode 100755 index 0000000000..70fcaf1702 --- /dev/null +++ b/tests/pflang-reg/pl-bug205-greater1_or_less1 @@ -0,0 +1,3 @@ +#!/bin/bash +thisdir=$(dirname $0) +"${thisdir}/../../env" pflua-pipelines-match "${thisdir}/../data/arp.pcap" "len >= 1 or len <= 1" 1 diff --git a/tests/pfquickcheck/README.md b/tests/pfquickcheck/README.md new file mode 100644 index 0000000000..315bd710da --- /dev/null +++ b/tests/pfquickcheck/README.md @@ -0,0 +1,30 @@ +# pflua-test +pflua-quickcheck [--seed=N] [--iterations=N] lua_property_file +[property-specific-args] + +Pflua-quickcheck is a tool inspired by Haskell's QuickCheck, for property-based +testing. + +It takes one mandatory argument, which generates examples for a property. This +argument is a file, in Lua. That file can optionally require more arguments, if +generating examples needs them. Examples: The simplest possible use: specify a +property file ./pflua-quickcheck properties/pfluamath_eq_libpcap_math + +This example's property file takes one mandatory and one optional argument: +./pflua-quickcheck properties/opt_eq_unopt ../data/wingolog.org.pcap test-filters +./pflua-quickcheck properties/opt_eq_unopt ../data/wingolog.org.pcap + +This example gives arguments to pflua-quickcheck, specifying the seed and number +of iterations. ./pflua-quickcheck --seed=379782615 --iterations=85 +properties/opt_eq_unopt ../data/wingolog.org.pcap + +About property files: A property file need to define a 'property' function, + which must return two values, which are expected to be equal if the + property is true. See the file properties/trivial.lua for an + example. Run it as: ./pflua-quickcheck properties/trivial + +A property file may also optionally define functions that parse extra arguments +(handle_prop_args), and/or that show more internal information if a property +failure occurs (print_extra_information). + +See also: https://github.com/Igalia/pflua diff --git a/tests/pfquickcheck/pfcompile.lua b/tests/pfquickcheck/pfcompile.lua new file mode 100644 index 0000000000..10a2047fef --- /dev/null +++ b/tests/pfquickcheck/pfcompile.lua @@ -0,0 +1,21 @@ +#!/usr/bin/env luajit +module(..., package.seeall) + +local backend = require("pf.backend") + +local function ast_to_ssa(ast) + local convert_anf = require('pf.anf').convert_anf + local convert_ssa = require('pf.ssa').convert_ssa + return convert_ssa(convert_anf(ast)) +end + +-- Compile_lua_ast and compile_ast are a stable API for tests +-- The idea is to have various compile_* helpers that take a particular +-- stage of IR and compile accordingly, even as pflua internals change. +function compile_lua_ast(ast) + return backend.emit_lua(ast_to_ssa(ast)) +end + +function compile_ast(ast, name) + return backend.emit_and_load(ast_to_ssa(ast, name)) +end diff --git a/tests/pfquickcheck/pflang.lua b/tests/pfquickcheck/pflang.lua new file mode 100644 index 0000000000..945f4335db --- /dev/null +++ b/tests/pfquickcheck/pflang.lua @@ -0,0 +1,356 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +-- This module generates (a subset of) pflang, libpcap's filter language + +-- Convention: initial uppercase letter => generates pflang expression +-- initial lowercase letter => aux helper + +-- Mutability discipline: +-- Any function may mutate results it calls into being. +-- No function may mutate its arguments; it must copy, +-- mutate the copy, and return instead. + +module(..., package.seeall) +local choose = require("pf.utils").choose +local utils = require("pf.utils") + +local verbose = os.getenv("PF_VERBOSE_PFLANG") + +local function Empty() return { "" } end + +local function uint8() return math.random(0, 2^8-1) end + +local function uint16() return math.random(0, 2^16-1) end + +local function tohex(n) return string.format("%x", n) end + +local function hexByte() return tohex(math.random(0, 0xff)) end + +local function hexWord() return tohex(math.random(0, 0xffff)) end + +-- Boundary numbers are often particularly interesting; test them often +local function uint32() + if math.random() < 0.2 + then return math.random(0, 2^32 - 1) + else + return choose({ 0, 1, 2^31-1, 2^31, 2^32-1 }) + end +end + +-- Given something like { 'host', '127.0.0.1' }, make it sometimes +-- start with src or dst. This should only be called on expressions +-- which can start with src or dst! +local function optionally_add_src_or_dst(expr) + local r = math.random() + local e = utils.dup(expr) + if r < 1/3 then table.insert(e, 1, "src") + elseif r < 2/3 then table.insert(e, 1, "dst") + end -- else: leave it unchanged + return e +end + +local function andSymbol() + local r = math.random() + if r < 1/2 then return "&&" else return "and" end +end + +local function orSymbol() + local r = math.random() + if r < 1/2 then return "||" else return "or" end +end + +local function notSymbol() + local r = math.random() + if r < 1/2 then return "!" else return "not" end +end + +local function optionally_not(expr) + local r = math.random() + local e = utils.dup(expr) + if r < 1/2 then + table.insert(e, 1, notSymbol()) end + return e +end + +local function IPProtocol() + return choose({"icmp", "igmp", "igrp", "pim", "ah", "esp", "vrrp", + "udp", "tcp", "sctp", "icmp6", "ip", "arp", "rarp", "ip6"}) +end + +local function ProtocolName() + return { IPProtocol() } +end + +-- TODO: add names? +local function portNumber() + return math.random(1, 2^16 - 1) +end + +local function Port() + return { "port", portNumber() } +end + +local function PortRange() + local port1, port2 = portNumber(), portNumber() + return { "portrange", port1 .. '-' .. port2 } +end + +local function ProtocolWithPort() + protocol = choose({ "tcp", "udp" }) + return { protocol, "port", portNumber() } +end + +-- TODO: generate other styles of ipv4 address +local function ipv4Addr() + return table.concat({ uint8(), uint8(), uint8(), uint8() }, '.') +end + +local function ipv4Netmask() return math.random(0, 32) end + +-- This function is overly conservative with zeroing octets. +-- TODO: zero more precisely? +local function ipv4Netspec() + local r = math.random() + local o1, o2, o3, o4 = uint8(), uint8(), uint8(), uint8() + + -- a bare number like '12' is interpreted as 12.0.0.0/8 + if r < 0.05 then return tostring(o1) + elseif r < 0.10 then return table.concat({o1, o2}, '.') + elseif r < 0.15 then return table.concat({o1, o2, o3}, '.') + else -- return a normal ipv4 netmask + local mask = ipv4Netmask() + if mask < 32 then o4 = 0 end + if mask < 24 then o3 = 0 end + if mask < 16 then o2 = 0 end + if mask < 8 then o1 = 0 end + local addr = table.concat({ o1, o2, o3, o4 }, '.') + return addr .. '/' .. mask + end +end + +local function abbreviate_ipv6(addrt) + local addrt = utils.dup(addrt) + local startgap = math.random(2, 7) + local gapbytes = math.random(1, 8 - startgap) + while gapbytes > 0 do + table.remove(addrt, startgap) + gapbytes = gapbytes - 1 + end + table.insert(addrt, startgap, '') + return addrt +end + +local function ipv6Chunks() + local o1, o2, o3, o4 = hexWord(), hexWord(), hexWord(), hexWord() + local o5, o6, o7, o8 = hexWord(), hexWord(), hexWord(), hexWord() + return {o1, o2, o3, o4, o5, o6, o7, o8} +end + +-- Sometimes, use abbreviated :: form addresses. +local function ipv6Addr() + local r = math.random() + local addrt = ipv6Chunks() + if r > 0.9 then addrt = abbreviate_ipv6(addrt) end + return table.concat(addrt, ':') +end + +local function ipv6Netspec() + local r = math.random() + local maskbytes = math.random(1, 8) + local maskbits = maskbytes * 16 + local addrt = ipv6Chunks() + while maskbytes <= 8 do + addrt[maskbytes] = 0 + maskbytes = maskbytes + 1 + end + if r > 0.9 then addrt = abbreviate_ipv6(addrt) end + return table.concat(addrt, ':') .. '/' .. maskbits +end + +local function ipAddr() + local r = math.random() + if r < 0.5 then return ipv6Addr() + else return ipv4Addr() + end +end + +-- A bare IP address is a valid netmask too, in this context. +local function ipv4Net() + local r = math.random() + if r < 0.9 then return ipv4Netspec() + else return ipv4Addr() + end +end + +local function ipv6Net() + local r = math.random() + if r < 0.9 then return ipv6Netspec() + else return ipv6Addr() + end +end + +local function ipNet() + local r = math.random() + if r < 0.5 then return ipv4Net() + else return ipv6Net() + end +end + +-- TODO: generate ipv6 addresses +local function Host() + return optionally_add_src_or_dst({ 'host', ipAddr() }) +end + +local function Net() + return optionally_add_src_or_dst({ 'net', ipNet() }) +end + +-- ^ intentionally omitted; 'len < 1 ^ 1' is not valid pflang +-- in older versions of libpcap +local function binaryMathOp() + return choose({ '+', '-', '/', '*', '|', '&' }) +end + +local function shiftOp() return choose({ '<<', '>>' }) end + +local function comparisonOp() + return choose({ '<', '>', '<=', ">=", '=', '!=', '==' }) +end + +-- Generate simple math expressions. +-- Don't recurse, to limit complexity; more complex math tests are elsewhere. +local function binMath(numberGen) + -- create numbers with the given function, or uint32 by default + if not numberGen then numberGen = uint32 end + local r, n1, n2, b = math.random() + if r < 0.2 then + n1, n2, b = numberGen(), math.random(0, 31), shiftOp() + else + n1, n2, b = numberGen(), numberGen(), binaryMathOp() + -- Don't divide by 0; that's tested elsewhere + if b == '/' then while n2 == 0 do n2 = numberGen() end end + end + return n1, n2, b +end + +-- Filters like 1+1=2 are legitimate pflang, as long as the result is right +local function Math() + local n1, n2, b = binMath() + local result + if b == '*' then + result = n1 * 1LL * n2 -- force non-floating point + result = tonumber(result % 2^32) -- Yes, this is necessary + elseif b == '/' then result = math.floor(n1 / n2) + elseif b == '-' then result = n1 - n2 + elseif b == '+' then result = n1 + n2 + elseif b == '|' then result = bit.bor(n1, n2) + elseif b == '&' then result = bit.band(n1, n2) + elseif b == '>>' then result = bit.rshift(n1, n2) + elseif b == '<<' then result = bit.lshift(n1, n2) + else error("Unhandled math operator " .. b) end + result = result % 2^32 -- doing this twice for * is fine + return { n1, b, n2, '=', result } +end + +-- Generate uint16s instead of uint32s to avoid triggering +-- libpcap bug 434. +local function LenWithMath() + local r = math.random() + local comparison = comparisonOp() + if r < 0.1 then + return { 'len', comparison, uint16() } + else + local n1, n2, b = binMath(uint16) + return { 'len', comparison, n1, b, n2 } + end +end + +-- TODO: use uint32 and ipv6 jumbo packets at some point? +local function packetAccessLocation() + local r1, r2 = math.random(), math.random() + local base + -- Usually generate small accesses - more likely to be in range + if r1 < 0.9 then + base = uint8() + else + base = uint16() + end + if r2 < 0.5 then + return tostring(base) + else + -- tcpdump only allows the following 3 numbers of bytes + local bytes = choose({1,2,4}) + return base .. ':' .. bytes + end +end + +local function PacketAccess() + local proto = ProtocolName()[1] + -- Avoid packet access on protocols where libpcap doesn't allow it + -- libpcap does not allow 'ah' and 'esp' packet access; not a pflua bug. + -- libpcap does not allow icmp6[x]: + -- "IPv6 upper-layer protocol is not supported by proto[x]" + local skip_protos = utils.set('ah', 'esp', 'icmp6') + while skip_protos[proto] do + proto = ProtocolName()[1] + end + local access = packetAccessLocation() + -- Hack around libpcap bug 430 + -- libpcap's match status depends on optimization levels if the access + -- is out of bounds. + -- Use len + 54 as a conservative bounds check; it gives room for + -- an ethernet header and an ipv6 fixed-length header. It's not ideal. + local header_guard = 54 -- ethernet + ipv6; most others are smaller + local access_loc = access:match("^%d+") + local guard = table.concat({'len >= ', access_loc, '+', header_guard}, ' ') + local comparison = table.concat({comparisonOp(), uint8()}, ' ') + local pkt_access = table.concat({proto, '[', access, '] '}) + return {'(', guard, 'and', pkt_access, comparisonOp(), uint8(), ')'} +end + +local function etherAddr() + local e1, e2, e3 = hexByte(), hexByte(), hexByte() + local e4, e5, e6 = hexByte(), hexByte(), hexByte() + return table.concat({e1, e2, e3, e4, e5, e6}, ':') +end + +local function Ether() + local qual = choose({'host', 'src', 'dst'}) + local addr = etherAddr() + return {'ether', qual, addr} +end + +local function PflangClause() + return choose({ProtocolName, Port, PortRange, ProtocolWithPort, + Host, Net, Math, LenWithMath, PacketAccess, Ether})() +end + +-- Add logical operators (or/not) +function PflangLogical() + local function PflangLogicalRec(depth, expr) + local r = math.random() + if depth <= 0 then return expr end + + if r < 0.9 then + local pclause2 = PflangClause() + local logicOp = orSymbol() + if r < 0.45 then logicOp = andSymbol() end + + table.insert(expr, logicOp) + for _,v in ipairs(pclause2) do table.insert(expr, v) end + return PflangLogicalRec(depth - 1, optionally_not(expr)) + else + return PflangLogicalRec(depth - 1, optionally_not(expr)) + end + end + + return PflangLogicalRec(math.random(1, 5), PflangClause()) +end + +function Pflang() + local r = math.random() + if r < 0.001 then return Empty() end + local expr = choose({ PflangClause, PflangLogical })() + if verbose then print(table.concat(expr, ' ')) end + return expr +end diff --git a/tests/pfquickcheck/pflang_math.lua b/tests/pfquickcheck/pflang_math.lua new file mode 100644 index 0000000000..ac1097fdfc --- /dev/null +++ b/tests/pfquickcheck/pflang_math.lua @@ -0,0 +1,70 @@ +#!/usr/bin/env luajit +module(..., package.seeall) + +local io = require("io") +local codegen = require("pf.backend") +local expand = require("pf.expand") +local parse = require("pf.parse") +local pfcompile = require("pfquickcheck.pfcompile") +local libpcap = require("pf.libpcap") +local bpf = require("pf.bpf") +local utils = require("pf.utils") + +-- Generate pflang arithmetic +local PflangNumber, PflangSmallNumber, PflangOp +function PflangNumber() return math.random(0, 2^32-1) end +function PflangOp() return utils.choose({ '+', '-', '*', '/' }) end +function PflangArithmetic() + return { PflangNumber(), PflangOp(), PflangNumber() } +end + +-- Evaluate math expressions with libpcap and pflang's IR + +-- Pflang allows arithmetic as part of larger expressions. +-- This tool uses len < arbitrary_arithmetic_here as a scaffold +function libpcap_eval(str_expr) + local expr = "len < " .. str_expr + local asm = libpcap.compile(expr, 'RAW') + local asm_str = bpf.disassemble(asm) + local template = "^000: A = length\ +001: if %(A >= (%d+)%) goto 2 else goto 3\ +002: return 0\ +003: return 65535\ +$" + local constant_str = asm_str:match(template) + if not constant_str then error ("unexpected bpf: "..asm_str) end + local constant = assert(tonumber(constant_str), constant_str) + assert(0 <= constant and constant < 2^32, constant) + return constant +end + +-- Here is an example of the pflua output that is parsed +--return function(P,length) +-- return length < ((519317859 + 63231) % 4294967296) +--end + +-- Old style: +-- return function(P,length) +-- local v1 = 3204555350 * 122882 +-- local v2 = v1 % 4294967296 +-- do return length < v2 end +-- end + +function pflua_eval(str_expr) + local expr = "len < " .. str_expr + local ir = expand.expand(parse.parse(expr)) + local filter = pfcompile.compile_lua_ast(ir, "Arithmetic check") + -- Old style: + -- local math_string = string.match(filter, "v1 = [%d-+/*()%a. ]*") + local math_str = string.match(filter, "return length < ([%d%a %%-+/*()]*)") + math_str = "v1 = " .. math_str + -- Loadstring has a different env, so floor doesn't resolve; use math.floor + math_str = math_str:gsub('floor', 'math.floor') + v1 = nil + loadstring(math_str)() -- v1 must not be local, or this approach will fail + -- v1 should always be within [0..2^32-1] + assert(v1 >= 0) + assert (v1 < 2^32) + assert(v1 == math.floor(v1)) + return v1 +end diff --git a/tests/pfquickcheck/pflua_ir.lua b/tests/pfquickcheck/pflua_ir.lua new file mode 100644 index 0000000000..7b7a81dd1f --- /dev/null +++ b/tests/pfquickcheck/pflua_ir.lua @@ -0,0 +1,56 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +-- This module generates (a subset of) pflua's IR, +-- for property-based tests of pflua internals. + +module(..., package.seeall) +local choose = require("pf.utils").choose + +local True, False, Fail, ComparisonOp, BinaryOp, Number, Len +local Binary, Arithmetic, Comparison, Conditional +-- Logical intentionally is not local; it is used elsewhere + +function True() return { 'true' } end +function False() return { 'false' } end +function Fail() return { 'fail' } end +function ComparisonOp() return choose({ '<', '>' }) end +function BinaryOp() return choose({ '+', '-', '/' }) end +-- Boundary numbers are often particularly interesting; test them often +function Number() + if math.random() < 0.2 + then return math.random(0, 2^32 - 1) + else + return choose({ 0, 1, 2^31-1, 2^31, 2^32-1 }) + end +end +function Len() return 'len' end +function Binary(db) + local op, lhs, rhs = BinaryOp(), Arithmetic(db), Arithmetic(db) + if op == '/' then table.insert(db, { '!=', rhs, 0 }) end + return { 'uint32', { op, lhs, rhs } } +end +function PacketAccess(db) + local pkt_access_size = choose({1, 2, 4}) + local position = Arithmetic(db) + table.insert(db, {'>=', 'len', {'+', position, pkt_access_size}}) + local access = { '[]', position, pkt_access_size } + if pkt_access_size == 1 then return access end + if pkt_access_size == 2 then return { 'ntohs', access } end + if pkt_access_size == 4 then return { 'uint32', { 'ntohs', access } } end + error('unreachable') +end +function Arithmetic(db) + return choose({ Binary, Number, Len, PacketAccess })(db) +end +function Comparison() + local asserts = {} + local expr = { ComparisonOp(), Arithmetic(asserts), Arithmetic(asserts) } + for i=#asserts,1,-1 do + expr = { 'if', asserts[i], expr, { 'fail' } } + end + return expr +end +function Conditional() return { 'if', Logical(), Logical(), Logical() } end +function Logical() + return choose({ Conditional, Comparison, True, False, Fail })() +end diff --git a/tests/properties/fail.lua b/tests/properties/fail.lua new file mode 100644 index 0000000000..b2ab42639b --- /dev/null +++ b/tests/properties/fail.lua @@ -0,0 +1,12 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +module(..., package.seeall) + +local function Number() return math.random(0, 2^32-1) end + +-- This is a trivial property file with a failing property, which is mainly +-- useful for testing pflua-quickcheck for obvious regressions +function property() + local n = Number() + return n, n + 1 +end diff --git a/tests/properties/opt_eq_unopt.lua b/tests/properties/opt_eq_unopt.lua new file mode 100644 index 0000000000..2a77179245 --- /dev/null +++ b/tests/properties/opt_eq_unopt.lua @@ -0,0 +1,79 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +module(..., package.seeall) +package.path = package.path .. ";../?.lua;../../src/?.lua" + +local ffi = require("ffi") +local parse = require("pf.parse") +local savefile = require("pf.savefile") +local expand = require("pf.expand") +local optimize = require("pf.optimize") +local codegen = require('pf.backend') +local utils = require('pf.utils') +local pp = utils.pp + +local pflua_ir = require('pfquickcheck.pflua_ir') +local pfcompile = require('pfquickcheck.pfcompile') + +local function load_filters(file) + local ret = {} + for line in io.lines(file) do table.insert(ret, line) end + return ret +end + +-- Several variables are non-local for print_extra_information() +function property(packets, filter_list) + local packet + -- Reset these every run, to minimize confusing output on crashes + optimized_pred, unoptimized_pred, expanded, optimized = nil, nil, nil, nil + packet, packet_idx = utils.choose_with_index(packets) + P, packet_len = packet.packet, packet.len + local F + if filters then + F = utils.choose(filters) + expanded = expand.expand(parse.parse(F), "EN10MB") + else + F = "generated expression" + expanded = pflua_ir.Logical() + end + optimized = optimize.optimize(expanded) + + unoptimized_pred = pfcompile.compile_ast(expanded, F) + optimized_pred = pfcompile.compile_ast(optimized, F) + return unoptimized_pred(P, packet_len), optimized_pred(P, packet_len) +end + +-- The test harness calls this on property failure. +function print_extra_information() + if expanded then + print("--- Expanded:") + pp(expanded) + else return -- Nothing else useful available to print + end + if optimized then + print("--- Optimized:") + pp(optimized) + else return -- Nothing else useful available to print + end + + print(("On packet %s: unoptimized was %s, optimized was %s"): + format(packet_idx, + unoptimized_pred(P, packet_len), + optimized_pred(P, packet_len))) +end + +function handle_prop_args(prop_args) + if #prop_args < 1 or #prop_args > 2 then + print("Usage: (pflua-quickcheck [args] properties/opt_eq_unopt) " .. + "PATH/TO/CAPTURE.PCAP [FILTER-LIST]") + os.exit(1) + end + + local capture, filter_list = prop_args[1], prop_args[2] + local packets = savefile.load_packets(capture) + local filters + if filter_list then + filters = load_filters(filter_list) + end + return packets, filter_list +end diff --git a/tests/properties/pflua_math_eq_libpcap_math.lua b/tests/properties/pflua_math_eq_libpcap_math.lua new file mode 100644 index 0000000000..4ceea95699 --- /dev/null +++ b/tests/properties/pflua_math_eq_libpcap_math.lua @@ -0,0 +1,16 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +module(..., package.seeall) +package.path = package.path .. ";../?.lua;../../src/?.lua" +local pflang_math = require("pfquickcheck.pflang_math") + +function property() + arithmetic_expr = table.concat(pflang_math.PflangArithmetic(), ' ') + local libpcap_result = pflang_math.libpcap_eval(arithmetic_expr) + local pflua_result = pflang_math.pflua_eval(arithmetic_expr) + return libpcap_result, pflua_result +end + +function print_extra_information() + print(("The arithmetic expression was %s"):format(arithmetic_expr)) +end diff --git a/tests/properties/pflua_pipelines_match.lua b/tests/properties/pflua_pipelines_match.lua new file mode 100644 index 0000000000..f300ef1eed --- /dev/null +++ b/tests/properties/pflua_pipelines_match.lua @@ -0,0 +1,45 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +module(..., package.seeall) +package.path = package.path .. ";../?.lua;../../src/?.lua" +-- Compare the results of the libpcap/bpf and pure-lua pflua pipelines. + +local pf = require("pf") +local savefile = require("pf.savefile") +local utils = require('pf.utils') + +local pflang = require('pfquickcheck.pflang') + +function property(packets) + --nil pkt_idx, pflang_expr, bpf_result, pflua_result to avoid + -- confusing debug information + pkt_idx, pflang_expr, bpf_result, pflua_result = nil + local pkt, P, pkt_len, libpcap_pred, pflua_pred + a = pflang.Pflang() + pflang_expr = table.concat(a, ' ') + pkt, pkt_idx = utils.choose_with_index(packets) + P, pkt_len = pkt.packet, pkt.len + libpcap_pred = pf.compile_filter(pflang_expr, { bpf = true }) + pflua_pred = pf.compile_filter(pflang_expr) + bpf_result = libpcap_pred(P, pkt_len) + pflua_result = pflua_pred(P, pkt_len) + return bpf_result, pflua_result +end + +function print_extra_information() + print(("The pflang expression was %s and the packet number %s"): + format(pflang_expr, pkt_idx)) + print(("BPF: %s, pure-lua: %s"):format(bpf_result, pflua_result)) +end + +function handle_prop_args(prop_args) + if #prop_args ~= 1 then + print("Usage: (pflua-quickcheck [args] properties/pflua_pipelines_match)" + .. " PATH/TO/CAPTURE.PCAP") + os.exit(1) + end + + local capture = prop_args[1] + return savefile.load_packets(capture) +end + diff --git a/tests/properties/pipecmp_proto_or_proto.lua b/tests/properties/pipecmp_proto_or_proto.lua new file mode 100644 index 0000000000..2e41ea93be --- /dev/null +++ b/tests/properties/pipecmp_proto_or_proto.lua @@ -0,0 +1,48 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +module(..., package.seeall) +package.path = package.path .. ";../?.lua;../../src/?.lua" +local pf = require("pf") +local savefile = require("pf.savefile") +local utils = require('pf.utils') + +local function choose_proto() + local protos = {"icmp", "igmp", "igrp", "pim", "ah", "esp", "vrrp", + "udp", "tcp", "sctp", "ip", "arp", "rarp", "ip6"} + return utils.choose(protos) +end + +function property(packets) + local expr = {choose_proto(), 'or', choose_proto()} + or_expr = table.concat(expr, ' ') -- Intentionally not local + + local pkt, pkt_idx = utils.choose_with_index(packets) + local P, pkt_len = pkt.packet, pkt.len + + local libpcap_pred = pf.compile_filter(or_expr, { bpf = true }) + local pflua_pred = pf.compile_filter(or_expr) + local bpf_result = libpcap_pred(P, pkt_len) + local pflua_result = pflua_pred(P, pkt_len) + + return bpf_result, pflua_result +end + +function print_extra_information() + print(("The arithmetic expression was %s"):format(or_expr)) +end + +function handle_prop_args(prop_args) + if #prop_args < 1 or #prop_args > 2 then + print("Usage: (pflua-quickcheck [args] " .. + "properties/pipecmp_proto_or_proto) PATH/TO/CAPTURE.PCAP") + os.exit(1) + end + + local capture, filter_list = prop_args[1], prop_args[2] + local packets = savefile.load_packets(capture) + local filters + if filter_list then + filters = load_filters(filter_list) + end + return packets, filter_list +end diff --git a/tests/properties/repeatable_randomization.lua b/tests/properties/repeatable_randomization.lua new file mode 100644 index 0000000000..a6fa00aa5d --- /dev/null +++ b/tests/properties/repeatable_randomization.lua @@ -0,0 +1,20 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +module(..., package.seeall) +package.path = package.path .. ";../?.lua;../../src/?.lua" + +local pflua_ir = require('pfquickcheck.pflua_ir') + +local function generate(seed) + math.randomseed(seed) + local res + -- Loop a few times so that we stress JIT compilation; see + -- https://github.com/Igalia/pflua/issues/77. + for i=1,100 do res = pflua_ir.Logical() end + return res +end + +function property(packets, filter_list) + local seed = math.random() + return generate(seed), generate(seed) +end diff --git a/tests/properties/trivial.lua b/tests/properties/trivial.lua new file mode 100644 index 0000000000..7d3aad6ffd --- /dev/null +++ b/tests/properties/trivial.lua @@ -0,0 +1,11 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +module(..., package.seeall) + +local function Number() return math.random(0, 2^32-1) end + +-- A number is always the same as itself plus 0 +function property() + local n = Number() + return n, n + 0 +end diff --git a/tests/test-filters b/tests/test-filters new file mode 100644 index 0000000000..93d111b2e1 --- /dev/null +++ b/tests/test-filters @@ -0,0 +1 @@ +ip diff --git a/tests/test-matches b/tests/test-matches new file mode 100755 index 0000000000..26d8a39ba2 --- /dev/null +++ b/tests/test-matches @@ -0,0 +1,90 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +package.path = package.path .. ';../src/?.lua' + +local pf = require('pf') +local ffi = require('ffi') +local savefile = require('pf.savefile') + +ffi.cdef[[ +struct DIR *opendir(const char *name); +typedef unsigned long ino_t; +struct dirent { + ino_t d_ino; /* inode number */ + off_t d_off; /* not an offset; see NOTES */ + unsigned short d_reclen; /* length of this record */ + unsigned char d_type; /* type of file; not supported + by all filesystem types */ + char d_name[256]; /* filename */ +}; +struct dirent *readdir(struct DIR *dirp); +]] + +function scandir(dirname) + if type(dirname) ~= 'string' then error("dirname not a string:", dirname) end + local dir = ffi.C.opendir(dirname) + if dir == nil then error("directory not found: "..dirname) end + local entries = {} + local dirent = ffi.C.readdir(dir) + while dirent ~= nil do + table.insert(entries, ffi.string(dirent.d_name)) + dirent = ffi.C.readdir(dir) + end + return entries +end + +function read_expectations(file) + local tests = {} + for line in io.lines(file) do + local description, count, filter = + line:match("^%s*([^:]+)%s*:%s*(%d+)%s*:%s*(.*)%s*$") + assert(filter, "failed to parse line "..line) + local test = { + description=description, + count=assert(tonumber(count)), + filter=filter, + } + table.insert(tests, test) + end + return tests +end + +function run_tests(pcap, tests) + local function write(...) + for _,str in ipairs({...}) do io.write(str) end + io.flush() + end + write('Running tests on ', pcap, ':\n') + local packets = savefile.load_packets(pcap) + for _,test in ipairs(tests) do + write(' ', test.description, ': ') + local pred = pf.compile_filter(test.filter) + local count = 0 + for _, packet in ipairs(packets) do + if pred(packet.packet, packet.len) then + count = count + 1 + end + end + write(count, ' matches: ') + if count == test.count then + write('PASS\n') + else + write('FAIL: expected ', test.count, ' matches.\n') + os.exit(1) + end + end + write('All pass.\n\n') +end + +function main(dir) + local entries = scandir(dir) + for _,x in ipairs(entries) do + local file = dir..'/'..x + if file:match("%.pcap%.test") then + local pcap = file:match('^(.*%.pcap)%.test$') + run_tests(pcap, read_expectations(file)) + end + end +end + +main(...) diff --git a/tools/Makefile b/tools/Makefile new file mode 100644 index 0000000000..6bfffe2c15 --- /dev/null +++ b/tools/Makefile @@ -0,0 +1,20 @@ +TOP_SRCDIR:=.. +include $(TOP_SRCDIR)/common.mk + +all: + +clean: + rm -f tmp.pcap + +# Very lightweight tests, to avoid low-hanging tool breakage +# during internal API changes +# TODO: add pflua-pipeline-match IR checks +tools_run: + ./pflua-pipelines-match ../tests/data/arp.pcap "arp" 1 + ./pflua-pipelines-match -O0 ../tests/data/arp.pcap "arp" 1 + ./pflua-pipelines-match --force-opt ../tests/data/arp.pcap "arp" 1 + +check: tools_run + ./pflua-filter ../tests/data/v4.pcap ./tmp.pcap "ip" + cmp ../tests/data/v4.pcap tmp.pcap + rm tmp.pcap diff --git a/tools/dump-markdown b/tools/dump-markdown new file mode 100755 index 0000000000..4721d7e636 --- /dev/null +++ b/tools/dump-markdown @@ -0,0 +1,24 @@ +#!/usr/bin/env luajit + +package.path = package.path .. ";../src/?.lua" + +local pf = require("pf") + +local filter = ... +assert(filter, "usage: dump-markdown FILTER") + +function out(...) print(string.format(...)) end + +function compile(opts) + local ok, result = pcall(pf.compile_filter, filter, opts) + if not ok then result = 'Filter failed to compile: '..result end + return result +end + +out("# %s\n\n", filter) +out("## BPF\n\n```\n%s```\n\n", + compile({libpcap=true, source=true})) +out("## BPF cross-compiled to Lua\n\n```\n%s\n```\n\n", + compile({bpf=true, source=true})) +out("## Direct pflang compilation\n\n```\n%s\n```\n", + compile({source=true})) diff --git a/tools/helpers/pflua_asm.lua b/tools/helpers/pflua_asm.lua new file mode 100644 index 0000000000..3aea18bb15 --- /dev/null +++ b/tools/helpers/pflua_asm.lua @@ -0,0 +1,69 @@ +--[[ +-- +-- This module is used to obtain the resulting jitted asm code for a pcap +-- expression using pflua. +-- +-- The file used for packet filtering is a 1GB file from pflua-bench, so it's +-- necessary to clone that repo, uncompress the file and create a symbolic link: +-- +-- $ git clone https://github.com/Igalia/pflua-bench.git +-- $ pflua-bench= +-- $ pflua= +-- $ unxz $pflua-bench/savefiles/one-gigabyte.pcap.xz +-- $ ln -fs $pflua-bench/savefiles/one-gigabyte.pcap \ +-- $pflua/tests/data/one-gigabyte.pcap +-- +--]] + +module("pflua_asm", package.seeall) + +package.path = package.path .. ";../../src/?.lua" + +local savefile = require("pf.savefile") +local libpcap = require("pf.libpcap") +local pf = require("pf") + +-- Counts number of packets within file +function filter_count(pred, file) + local total_pkt = 0 + local count = 0 + local records = savefile.records_mm(file) + + while true do + local pkt, hdr = records() + if not pkt then break end + + local length = hdr.incl_len + execute_pred_ensuring_trace(pred, pkt, length) + end + return count, total_pkt +end + +-- Executing pred within a function ensures a trace for this call +function execute_pred_ensuring_trace(pred, packet, length) + pred(packet, length) +end + +-- Calls func() during seconds +function call_during_seconds(seconds, func, pred, file) + local time = os.time + local finish = time() + seconds + while (true) do + func(pred, file) + if (time() > finish) then break end + end +end + +function selftest(filter) + print("selftest: pflua_asm") + + local file = "../tests/data/one-gigabyte.pcap" + if (filter == nil or filter == '') then + filter = "tcp port 80" + end + + local pred = pf.compile_filter(filter, {dlt="EN10MB"}) + call_during_seconds(1, filter_count, pred, file) + + print("OK") +end diff --git a/tools/pflua-allocchecker b/tools/pflua-allocchecker new file mode 100755 index 0000000000..e9ed0a2408 --- /dev/null +++ b/tools/pflua-allocchecker @@ -0,0 +1,98 @@ +#!/usr/bin/env luajit + +package.path = package.path .. ";../src/?.lua" + +local io = require("io") +local pf = require("pf") + +-- Given a command like "pflua-match wingolog.pcap tcp", get its trace. +-- Note: Extra argument can be passed to luajit thusly: +-- "-O-cse -other-arbitrary-luajit-argument pflua-match wingolog.pcap tcp" +function get_trace(...) + table.insert(arg, 1, "luajit") + table.insert(arg, 2, "-jdump=+rs") + cmdline = table.concat(arg, " ") + print("Running: " .. cmdline) + return io.popen(cmdline):read("*all") +end + +-- An 'interesting' start is currently one that's in a filter, not library code. +function find_first_interesting_start(raw_traces) + local i = 1 + local boring = {} + -- pf.* is library code. + for _,v in pairs(pf) do + if type(v) == "table" then -- turn pf.bpf into bpf, etc. + local pf_name = string.match(v._NAME, ".*%.(%a[_%w]*)") + if pf_name and pf_name ~= "" then boring[pf_name] = true end + end + end + + while true do + local tstart, tend = raw_traces:find("---- TRACE %d+ start ", i) + assert(tstart, "Failed to find an interesting trace!") + local tracing_in = raw_traces:match("[a-z]+", tend) + if not boring[tracing_in] then break end + i = tend + end + return i +end + +function filter_interesting_ir_traces(raw_traces) + local i = find_first_interesting_start(raw_traces) + local interesting_traces = {} + while true do + local interesting_start, _ = raw_traces:find("---- TRACE %d+ IR", i) + if not interesting_start then break end + local interesting_end, nexti = raw_traces:find("---- TRACE %d+ mcode", i) + assert(interesting_end, "The trace appears to be truncated.") + interesting_ir_trace = raw_traces:sub(interesting_start, interesting_end) + table.insert(interesting_traces, interesting_ir_trace) + i = nexti + end + return interesting_traces +end + +function find_unsunk_allocs_in(trace) + local unsunk_allocs = {} + local allocation_ops = {"SNEW", "XSNEW", "TNEW", "TDUP", "CNEW", "CNEWI"} + for _, alloc_op in ipairs(allocation_ops) do + local i = 1 + while true do + local astart, aend = trace:find("[^\n]*" .. alloc_op, i) + local alloc = trace:match("[^\n]*" .. alloc_op, astart) + if not astart then break end + local is_sunk = alloc:find("sink") + if not is_sunk then table.insert(unsunk_allocs, alloc) end + i = aend + end + end + return unsunk_allocs +end + +function main(...) + local raw_traces = get_trace(...) + local interesting_ir_traces = filter_interesting_ir_traces(raw_traces) + local unsunk_allocs = {} + local unsunk_alloc_traces = {} + for _,trace in ipairs(interesting_ir_traces) do + local uas = find_unsunk_allocs_in(trace) + if next(uas) then + for _,ua in ipairs(uas) do table.insert(unsunk_allocs, ua) end + table.insert(unsunk_alloc_traces, trace) + end + end + if next(unsunk_allocs) then + print(table.concat(unsunk_allocs, "\n")) + if os.getenv("PF_VERBOSE") then + print(table.concat(unsunk_alloc_traces, "\n")) + end + else + print("No unsunk allocations detected in the SSA IR.") + end +end + +assert(..., "usage: pflua-allocchecker program-to-run-with-its-args") + +main(...) + diff --git a/tools/pflua-asm b/tools/pflua-asm new file mode 100755 index 0000000000..ec76d8fd27 --- /dev/null +++ b/tools/pflua-asm @@ -0,0 +1,53 @@ +#!/bin/bash + +PRINT=0 + +usage() { +cat << EOF +Usage: $0 + + Prints out BPF asm code for a BPF expression. + +Options: + --help | -h Prints this help. + BPF expression to compile. DEFAULT: "tcp port 80". + +See: http://biot.com/capstats/bpf.html +EOF +} + +if [[ $1 == "--help" || $1 == "-h" ]]; then + usage + exit +fi + +filter="tcp port 80" +if [ $# == 1 ]; then + filter=$1 +fi + +# Name of the script that prints out asm for a pred expression +lua_script="helpers/pflua_asm.lua" + +# Linenumber where the 'execute_pred_ensuring_trace' function is defined +lineno=`grep -n 'function execute_pred_ensuring_trace' $lua_script | cut -d : -f 1` + +# Iterate through all asm output but print out only the fragment of asm code for the compiled pred expression +output=`luajit -jdump=m -l helpers/pflua_asm -e 'pflua_asm.selftest()'`; +while read -r line; do + # Was printing and found a blank line + if [ $PRINT -eq 1 ] && [ -z "$line" ]; then + break; + fi + # Is in printing mode + if [ $PRINT -eq 1 ]; then + echo "$line" + continue; + fi + # Found target TRACE, start printing + matches=$(echo $line | grep "$lua_script:$lineno") + if [ "$matches" ]; then + PRINT=1 + echo "$line" + fi +done <<< "$output" diff --git a/tools/pflua-compile b/tools/pflua-compile new file mode 100755 index 0000000000..a62aca43c9 --- /dev/null +++ b/tools/pflua-compile @@ -0,0 +1,62 @@ +#!/usr/bin/env luajit +-- -*- lua -*- + +package.path = package.path .. ";../src/?.lua" + +local pf = require("pf") +local bpf = require("pf.bpf") +local match = require("pf.match") +local utils = require("pf.utils") + +function usage() + local content = [=[ +Usage: pflua-compile [-O0] [--bpf-asm | --bpf-lua | --lua] + +Options: + --bpf-asm Print libpcap-generated BPF asm code for the pflang + --bpf-lua Print Lua code compiled from BPF for the pflang + --lua Print Lua code compiled directly for the pflang (DEFAULT) + --match Print Lua code compiled from the pfmatch + + -O0 Disable optimizations. (Optimizations are on by default) ]=] + print(content); + os.exit() +end + +-- Print help +if #arg == 0 then + usage() +end + +local flags = utils.set(...) + +-- Print help +if flags["--help"] or flags["-h"] then + usage() +end + +-- No code-generation flag defined +if (not(flags["--bpf-asm"] or flags["--bpf-lua"] or flags["--lua"] or flags['--match'])) then + -- Default action + flags["--lua"] = true +end + + +local optimize = true +if flags["-O0"] then optimize = false end + +local filter = arg[#arg] +if flags["--bpf-asm"] then + print(pf.compile_filter(filter, {libpcap=true, source=true, + optimize=optimize})) +end +if flags["--bpf-lua"] then + print(pf.compile_filter(filter, {bpf=true, source=true, + optimize=optimize})) +end +if flags["--lua"] then + print(pf.compile_filter(filter, {source=true, optimize=optimize})) +end +if flags["--match"] then + print(match.compile(filter, {source=true, optimize=optimize})) +end diff --git a/tools/pflua-expand b/tools/pflua-expand new file mode 100755 index 0000000000..7eeca877bc --- /dev/null +++ b/tools/pflua-expand @@ -0,0 +1,36 @@ +#!/usr/bin/env luajit +-- -*- lua -*- + +package.path = package.path .. ";../src/?.lua" + +local pf = require("pf") +local utils = require("pf.utils") + +local function usage() + local content = [=[ +Usage: pflua-expand [-O0] +Options: + -O0 Disable optimizations; optimizations are on by default.]=] + print(content); + os.exit() +end + +-- Print help +if #arg == 0 then + usage() +end + +local flags = utils.set(...) + +-- Print help +if flags["--help"] or flags["-h"] then + usage() +end + +local filter = arg[#arg] +local expanded = pf.expand.expand(pf.parse.parse(filter), "EN10MB") +if flags["-O0"] then + utils.pp(expanded) +else + utils.pp(pf.optimize.optimize(expanded)) +end diff --git a/tools/pflua-filter b/tools/pflua-filter new file mode 100755 index 0000000000..b4565b4d59 --- /dev/null +++ b/tools/pflua-filter @@ -0,0 +1,41 @@ +#!/usr/bin/env luajit + +package.path = package.path .. ";../src/?.lua" + +local ffi = require("ffi") +local pf = require("pf") +local savefile = require("pf.savefile") + +local function filter(ptr, ptr_end, out, pred) + local seen, written = 0, 0 + while ptr < ptr_end do + local record = ffi.cast("struct pcap_record *", ptr) + local packet = ffi.cast("unsigned char *", record + 1) + local ptr_next = packet + record.incl_len + if pred(packet, record.incl_len) then + out:write(ffi.string(ptr, ptr_next - ptr)) + written = written + 1 + end + seen = seen + 1 + ptr = ptr_next + end + out:flush() + return seen, written +end + +function main(in_file, out_file, filter_str) + local header, ptr, ptr_end = savefile.open_and_mmap(in_file) + local out = assert(io.open(out_file, 'w')) + out:setvbuf('full') + out:write(ffi.string(header, ffi.sizeof("struct pcap_file"))) + local pred = pf.compile_filter(filter_str) + local seen, written = filter(ptr, ptr_end, out, pred) + out:close() + print(string.format("Filtered %d/%d packets from %s to %s.", + written, seen, in_file, out_file)) +end + +local in_file, out_file, filter_str = ... +assert(filter_str, "usage: pflua-filter IN.PCAP OUT.PCAP FILTER") + +main(in_file, out_file, filter_str) diff --git a/tools/pflua-match b/tools/pflua-match new file mode 100755 index 0000000000..24149254c3 --- /dev/null +++ b/tools/pflua-match @@ -0,0 +1,84 @@ +#!/usr/bin/env luajit + +package.path = package.path .. ";../src/?.lua" + +local ffi = require("ffi") +local pf = require("pf") +local utils = require("pf.utils") +local savefile = require("pf.savefile") + +local function usage() + print([[ +Usage: pflua-match [--bpf] IN.PCAP FILTER + IN.PCAP Input file in .pcap format. + FILTER Filter to apply, as a string or file. + + --bpf Compile expression using libpcap. + ]]) + os.exit(false) +end + +local function filter(packets, pred) + local seen, matched = 0, 0 + for i = 1,#packets do + packet = packets[i] + seen = seen + 1 + if pred(packet.packet, packet.len) then matched = matched + 1 end + end + return seen, matched +end + +function get_predicate(filter_input, opts) + -- If the filter seems to be a filename, read the filter from the file. + -- Otherwise, compile it as a filter string. + local filter_file_pred = loadfile(filter_input) + if filter_file_pred then + if not getfenv(0).ffi then getfenv(0).ffi = require('ffi') end + return filter_file_pred() + else + return pf.compile_filter(filter_input, opts) + end +end + +local function run_filter(min_time, packets, pred) + local start = utils.now() + local finish = start + local seen, matched = 0 + local iterations = 0 + while finish - start < min_time do + seen, matched = filter(packets, pred) + finish = utils.now() + iterations = iterations + 1 + end + return seen, matched, (finish - start), iterations +end + +function main(in_file, filter_input, opts) + local packets = savefile.load_packets(in_file) + local pred = get_predicate(filter_input, opts) + -- Untimed warm up - this may involve disk access, etc. + filter(packets, pred) + -- Full warm-up, hopefully. 0.5s is a guess; most JIT will(?) occur. + local seen, matched = run_filter(0.5, packets, pred) + -- Very short timing runs are highly inaccurate. 0.002s is not ok. + -- By 1s, results are more consistent. + -- Seen and matched are the same for every run. + seen, matched, elapsed, iterations = run_filter(1, packets, pred) + print(string.format("Matched %d/%d packets in %s iterations: %s (%f MPPS).", + matched, seen, iterations, in_file, + (seen * iterations / elapsed) / 1e6)) +end + +-- Parse args +local opts = { } +for i=1, #arg do + if arg[i] == "--bpf" then + opts = { bpf = true } + table.remove(arg, i) + end +end + +local in_file, filter_input = arg[1], arg[2] +if not filter_input then usage() end + +main(in_file, filter_input, opts) diff --git a/tools/pflua-optimize b/tools/pflua-optimize new file mode 100755 index 0000000000..4a08d33188 --- /dev/null +++ b/tools/pflua-optimize @@ -0,0 +1,85 @@ +#!/usr/bin/env luajit +-- -*- lua -*- + +package.path = package.path .. ";../src/?.lua;../tests/?.lua" + +local optimize = require('pf.optimize') +local savefile = require('pf.savefile') +local utils = require('pf.utils') + +local pfcompile = require('pfquickcheck.pfcompile') + +local function usage() + local content = [=[ +Usage: pflua-optimize [--match-packet #packet file.pcap] [FILE] + +Takes an expanded AST expression, optimizes it, and prints out the +result. Useful when tracking down optimizer bugs. If FILE is given as +a command-line argument, we read the expression from the file; otherwise +it is read from standard input. + ]=] + print(content); +end + +local function run(input, options) + local expr = assert(loadstring('return '..input:read('*a')))() + + print('Optimizing:') + utils.pp(expr) + local optimized_expr = optimize.optimize(expr) + print('Result:') + utils.pp(optimized_expr) + + if options.pktfile then + local unopt_pred, opt_pred, packets, packet, P, len, unopt_res, opt_res + unopt_pred = pfcompile.compile_ast(expr, "Unopt") + opt_pred = pfcompile.compile_ast(optimized_expr, "Opt") + packets = savefile.load_packets(options.pktfile) + packet = packets[options.pktnum] + P, len = packet.packet, packet.len + + unopt_res = unopt_pred(P, len) + opt_res = opt_pred(P, len) + if unopt_res ~= opt_res then + print("Packet results did not match!") + print(("Unoptimized: %s, optimized: %s"):format(unopt_res, opt_res)) + os.exit(1) + else + print("Match status the same before and after optimization.") + end + end +end + +local function parse_command_line(args) + local input + local options = {} + while #args >= 1 and args[1]:match("^%-%-") do + local param_arg = table.remove(args, 1) + if param_arg == '--match-packet' then + options.pktnum = tonumber(table.remove(args, 1)) + options.pktfile = table.remove(args, 1) + print(options.pktnum, options.pktfile) + else error("Unknown argument: " .. arg) end + end + + if #args == 0 then + input = io.stdin + elseif #args == 1 then + if args[1] == '--help' or args[1] == '-h' then + usage() + os.exit(0) + end + input = assert(io.open(args[1])) + else + usage() + os.exit(1) + end + return input, options +end + +local function main(...) + local args = { ... } + run(parse_command_line(args)) +end + +main(...) diff --git a/tools/pflua-pipelines-match b/tools/pflua-pipelines-match new file mode 100755 index 0000000000..0e9e1e216f --- /dev/null +++ b/tools/pflua-pipelines-match @@ -0,0 +1,174 @@ +#!/usr/bin/env luajit +-- Do a 3-way compare between the pure-lua, bpf-lua and pure-libpcap pipelines, +-- given pflang, indicating whether or not all three match. +-- Input: pflang by default, or pflua IR if --ir is specified. +package.path = package.path .. ";../src/?.lua;../tests/?.lua" + +local pf = require("pf") +local savefile = require("pf.savefile") +local optimize = require('pf.optimize') +local utils = require("pf.utils") +local pfcompile = require('pfquickcheck.pfcompile') +local libpcap = require("pf.libpcap") + +local function usage() + print([[ +Usage: pflua-pipelines-match [-O0 | --force-opt] IN.PCAP FILTER PKT_NUMBER + pflua-pipelines-match --ir IN.PCAP IR_FILE IR_FILE PKT_NUMBER + pflua-pipelines-match --ir --opt-ir IN.PCAP IR_FILE PKT_NUMBER + + IN.PCAP Input file in .pcap format. + FILTER Filter to apply, as a string or file. + PKT_NUMBER Check if the pipelines match on the specified packet + IR_FILE filename containing IR + + --ir: the input is a file containing IR, not pflang + --opt-ir: instead of a 2nd IR file, optimize the first IR and use that + (valid if --ir specified) + + -O0: force optimizations to be disabled + --force-opt: only check pflang compiled with optimizations enabled. + Default: check with optimizations both enabled and disabled. + ]]) + os.exit(false) +end + +local OPT_FALSE, OPT_TRUE, OPT_BOTH = 0, 1, 2 + +local function filter(packets, preds, pkt_number) + local pkt = packets[pkt_number] + local results = {} + for d, pred in pairs(preds) do results[d] = pred(pkt.packet, pkt.len) end + + local results_match, res = utils.table_values_all_equal(results) + + if results_match then + local p = {} + for k,_ in pairs(results) do table.insert(p, k) end + local pipelines = table.concat(p, ' ') + local msg = "OK: %s concur: all were %s" + print(msg:format(pipelines, res)) + else + print("BUG: pipelines diverged.") + print(libpcap.pcap_version()) + local trues, falses = {}, {} + for k, v in pairs(results) do + if v then + table.insert(trues, k) + else + table.insert(falses, k) + end + end + print((" true: %s"):format(table.concat(trues, ', '))) + print((" false: %s"):format(table.concat(falses, ', '))) + end + return results_match +end + +local function create_preds(filter_input, opt) + local pflua_pred = pf.compile_filter(filter_input, {optimize=opt}) + local bpf_pred = pf.compile_filter(filter_input, {bpf=true, optimize=opt}) + local lpcap = pf.compile_filter(filter_input, {libpcap=true, optimize=opt}) + return pflua_pred, bpf_pred, lpcap +end + +local function main_pflang(pcap_file, filter_input, pkt_number, opt) + local packets = savefile.load_packets(pcap_file) + local preds = {} + if opt == OPT_FALSE or opt == OPT_TRUE then + local o = true + if opt == OPT_FALSE then o = false end + + local p, b, l = create_preds(filter_input, o) + preds["pure-lua"] = p + preds["bpf-lua"] = b + preds["libpcap"] = l + elseif opt == OPT_BOTH then + local p, b, l = create_preds(filter_input, false) + preds["pure-lua-unopt"] = p + preds["bpf-lua-unopt"] = b + preds["libpcap-unopt"] = l + p, b, l = create_preds(filter_input, true) + preds["pure-lua-opt"] = p + preds["bpf-lua-opt"] = b + preds["libpcap-opt"] = l + else + error("Invalid optimization value") + end + return filter(packets, preds, pkt_number) +end + +local function read_and_compile_ast(ir_file, optimize_ast) + local ir_in = assert(io.open(ir_file)) + local ir_str = ir_in:read('*a') + local ast = assert(loadstring('return ' .. ir_str))() + ir_in:close() + if optimize_ast then ast = optimize.optimize(ast) end + return pfcompile.compile_ast(ast, ir_file) +end + +local function main_ir(pcap_file, ir1, ir2, opt_ir, pkt_number) + local packets = savefile.load_packets(pcap_file) + local preds = {} + preds["ir1_pred"] = read_and_compile_ast(ir1, false) + -- It's more useful to check the *current* optimization rather than some + -- known good past one, for the sake of regression testing. + -- The flag opt_ir indicates that the only IR given should be optimized + -- should be optimized, instead of compared against a different given IR. + if opt_ir then + preds["ir2_pred"] = read_and_compile_ast(ir1, true) + else -- Leave given IR alone; do not modify it, do not optimize it + preds["ir2_pred"] = read_and_compile_ast(ir2, false) + end + return filter(packets, preds, pkt_number) +end + +local function get_nonflag_args(args) + local nf = {} + local idx = 1 + for _,v in pairs(args) do + if not v:match("^-") then + nf[idx] = v + idx = idx + 1 + end + end + return nf +end + +local function run_filters(...) + local opts = utils.set(...) + local arg = get_nonflag_args({...}) + + if opts['--ir'] then + local pcap_file, ir1, ir2, pkt_number + local opt_ir + if opts['--opt-ir'] then + opt_ir = true + pcap_file, ir1, ir2, pkt_number = arg[1], arg[2], nil, arg[3] + else + opt_ir = false + pcap_file, ir1, ir2, pkt_number = arg[1], arg[2], arg[3], arg[4] + end + + if not pkt_number then usage() end + os.exit(main_ir(pcap_file, ir1, ir2, opt_ir, tonumber(pkt_number))) + end + + -- Do a 3-way compare between the pure-lua, bpf-lua and pure-libpcap pipelines, + local optimize = OPT_BOTH + if opts['-O0'] and opts['--force-opts'] then + print("It's invalid to specify -O0 with --force-opts") + usage() + os.exit(1) + elseif opts['-O0'] then + optimize = OPT_FALSE + elseif opts['--force-opt'] then + optimize = OPT_TRUE + end + local pcap_file, filter_str, pkt_number = arg[1], arg[2], arg[3] + if not pkt_number then usage() end + os.exit(main_pflang(pcap_file, filter_str, tonumber(pkt_number), optimize)) +end + +-- Parse args and run everything. +run_filters(...) diff --git a/tools/pflua-quickcheck b/tools/pflua-quickcheck new file mode 100755 index 0000000000..f100b8b2fc --- /dev/null +++ b/tools/pflua-quickcheck @@ -0,0 +1,13 @@ +#!/usr/bin/env luajit +-- -*- lua -*- +package.path = package.path .. ";../src/?.lua" + +local quickcheck = require('pf.quickcheck') + +function main(...) + local args = { ... } + quickcheck.initialize_from_command_line(args) + quickcheck.run() +end + +main(...)