From d7d6c2984138ed257d98485115a9ab9306b0197d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Osi=C5=84ski?= Date: Tue, 26 Apr 2022 22:28:27 +0200 Subject: [PATCH] Add support for Random extern to PSA/eBPF backend (#3251) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mateusz Kossakowski Co-authored-by: Jan PalimÄ…ka --- backends/ebpf/CMakeLists.txt | 2 + backends/ebpf/psa/README.md | 6 ++ backends/ebpf/psa/ebpfPsaControl.cpp | 4 + backends/ebpf/psa/ebpfPsaControl.h | 8 ++ backends/ebpf/psa/ebpfPsaGen.cpp | 4 + backends/ebpf/psa/externs/ebpfPsaRandom.cpp | 104 ++++++++++++++++++++ backends/ebpf/psa/externs/ebpfPsaRandom.h | 34 +++++++ backends/ebpf/tests/p4testdata/random.p4 | 96 ++++++++++++++++++ backends/ebpf/tests/ptf/test.py | 52 ++++++++++ 9 files changed, 310 insertions(+) create mode 100644 backends/ebpf/psa/externs/ebpfPsaRandom.cpp create mode 100644 backends/ebpf/psa/externs/ebpfPsaRandom.h create mode 100644 backends/ebpf/tests/p4testdata/random.p4 diff --git a/backends/ebpf/CMakeLists.txt b/backends/ebpf/CMakeLists.txt index fc0c4248aca..75c98cc8410 100644 --- a/backends/ebpf/CMakeLists.txt +++ b/backends/ebpf/CMakeLists.txt @@ -48,6 +48,7 @@ set (P4C_EBPF_SRCS psa/externs/ebpfPsaChecksum.cpp psa/externs/ebpfPsaHashAlgorithm.cpp psa/externs/ebpfPsaTableImplementation.cpp + psa/externs/ebpfPsaRandom.cpp psa/externs/ebpfPsaRegister.cpp psa/externs/ebpfPsaMeter.cpp ) @@ -83,6 +84,7 @@ set (P4C_EBPF_HDRS psa/externs/ebpfPsaHashAlgorithm.h psa/externs/ebpfPsaTableImplementation.h psa/externs/ebpfPsaRegister.h + psa/externs/ebpfPsaRandom.h psa/externs/ebpfPsaMeter.h ) diff --git a/backends/ebpf/psa/README.md b/backends/ebpf/psa/README.md index 7b7f9bca13e..300722a0b43 100644 --- a/backends/ebpf/psa/README.md +++ b/backends/ebpf/psa/README.md @@ -299,6 +299,12 @@ for a given key exists. A value of the BPF map is ignored. **Note:** As of April 2022, support for value_set in `psabpf-ctl` CLI/API is not implemented yet. As a workaround you can use the `bpftool` command. +### Random + +The [Random](https://p4.org/p4-spec/docs/PSA.html#sec-random) extern is a mean to retrieve a pseudo-random number in a specified range within a P4 program. +The PSA-eBPF compiler uses the `bpf_get_prandom_u32()` BPF helper to get a pseudo-random number. +Each `read()` operation on the Random extern in a P4 program is translated into a call to the BPF helper. + # Getting started ## Installation diff --git a/backends/ebpf/psa/ebpfPsaControl.cpp b/backends/ebpf/psa/ebpfPsaControl.cpp index 9a355783bf6..a26b7dec828 100644 --- a/backends/ebpf/psa/ebpfPsaControl.cpp +++ b/backends/ebpf/psa/ebpfPsaControl.cpp @@ -75,6 +75,10 @@ void ControlBodyTranslatorPSA::processMethod(const P4::ExternMethod* method) { auto hash = control->to()->getHash(name); hash->processMethod(builder, method->method->name.name, method->expr, this); return; + } else if (declType->name.name == "Random") { + auto rand = control->to()->getRandomExt(name); + rand->processMethod(builder, method); + return; } else if (declType->name.name == "Register") { auto reg = control->to()->getRegister(name); if (method->method->type->name == "write") { diff --git a/backends/ebpf/psa/ebpfPsaControl.h b/backends/ebpf/psa/ebpfPsaControl.h index e19da296e9e..7e01687ae52 100644 --- a/backends/ebpf/psa/ebpfPsaControl.h +++ b/backends/ebpf/psa/ebpfPsaControl.h @@ -20,6 +20,7 @@ limitations under the License. #include "ebpfPsaTable.h" #include "backends/ebpf/ebpfControl.h" #include "backends/ebpf/psa/externs/ebpfPsaChecksum.h" +#include "backends/ebpf/psa/externs/ebpfPsaRandom.h" #include "backends/ebpf/psa/externs/ebpfPsaRegister.h" namespace EBPF { @@ -64,6 +65,7 @@ class EBPFControlPSA : public EBPFControl { const IR::Parameter* outputStandardMetadata; std::map hashes; + std::map randoms; std::map registers; std::map meters; @@ -76,6 +78,12 @@ class EBPFControlPSA : public EBPFControl { void emitTableInstances(CodeBuilder* builder) override; void emitTableInitializers(CodeBuilder* builder) override; + EBPFRandomPSA* getRandomExt(cstring name) const { + auto result = ::get(randoms, name); + BUG_CHECK(result != nullptr, "No random generator named %1%", name); + return result; + } + EBPFRegisterPSA* getRegister(cstring name) const { auto result = ::get(registers, name); BUG_CHECK(result != nullptr, "No register named %1%", name); diff --git a/backends/ebpf/psa/ebpfPsaGen.cpp b/backends/ebpf/psa/ebpfPsaGen.cpp index 544ad78b310..75ca1987ab3 100644 --- a/backends/ebpf/psa/ebpfPsaGen.cpp +++ b/backends/ebpf/psa/ebpfPsaGen.cpp @@ -23,6 +23,7 @@ limitations under the License. #include "externs/ebpfPsaCounter.h" #include "externs/ebpfPsaHashAlgorithm.h" #include "externs/ebpfPsaTableImplementation.h" +#include "externs/ebpfPsaRandom.h" #include "externs/ebpfPsaMeter.h" namespace EBPF { @@ -693,6 +694,9 @@ bool ConvertToEBPFControlPSA::preorder(const IR::ExternBlock* instance) { } else if (typeName == "Counter") { auto ctr = new EBPFCounterPSA(program, di, name, control->codeGen); control->counters.emplace(name, ctr); + } else if (typeName == "Random") { + auto rand = new EBPFRandomPSA(di); + control->randoms.emplace(name, rand); } else if (typeName == "Register") { auto reg = new EBPFRegisterPSA(program, name, di, control->codeGen); control->registers.emplace(name, reg); diff --git a/backends/ebpf/psa/externs/ebpfPsaRandom.cpp b/backends/ebpf/psa/externs/ebpfPsaRandom.cpp new file mode 100644 index 00000000000..4dce083d28c --- /dev/null +++ b/backends/ebpf/psa/externs/ebpfPsaRandom.cpp @@ -0,0 +1,104 @@ +/* +Copyright 2022-present Orange +Copyright 2022-present Open Networking Foundation +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. +*/ +#include "ebpfPsaRandom.h" + +namespace EBPF { + +EBPFRandomPSA::EBPFRandomPSA(const IR::Declaration_Instance* di) : + minValue(0), maxValue(0), range(0) { + CHECK_NULL(di); + + // verify type + if (!di->type->is()) { + ::error(ErrorType::ERR_MODEL, "Missing specialization: %1%", di); + return; + } + auto ts = di->type->to(); + BUG_CHECK(ts->arguments->size() == 1, "%1%, Lack of specialization argument", ts); + auto type = ts->arguments->at(0); + if (!type->is()) { + ::error(ErrorType::ERR_UNSUPPORTED, "Must be bit or int type: %1%", ts); + return; + } + if (type->width_bits() > 32) { + ::error(ErrorType::ERR_UNSUPPORTED, "%1%: up to 32 bits width is supported", ts); + } + + if (di->arguments->size() != 2) { + ::error(ErrorType::ERR_MODEL, "Expected 2 arguments to: %1%", di); + return; + } + + unsigned tmp[2] = {0}; + for (int i = 0; i < 2; ++i) { + auto expr = di->arguments->at(i)->expression->to(); + if (expr != nullptr) { + if (expr->fitsUint()) { + tmp[i] = expr->asUnsigned(); + } else { + ::error(ErrorType::ERR_OVERLIMIT, "%1%: size too large", expr); + } + } else { + ::error(ErrorType::ERR_UNSUPPORTED, "Must be constant value: %1%", + di->arguments->at(i)->expression); + } + } + + minValue = tmp[0]; + maxValue = tmp[1]; + range = (long) maxValue - minValue + 1; + + // verify constructor parameters + if (minValue > maxValue) { + ::error(ErrorType::ERR_INVALID, "%1%: Max value lower than min value", di); + } + if (minValue == maxValue) { + ::warning(ErrorType::WARN_IGNORE, + "%1%: No randomness, will always return the same value " + "due to that the min value is equal to the max value", di); + } +} + +void EBPFRandomPSA::processMethod(CodeBuilder* builder, const P4::ExternMethod* method) const { + if (method->method->type->name == "read") { + emitRead(builder); + } else { + ::error(ErrorType::ERR_UNSUPPORTED, "%1%: Method not implemented yet", method->expr); + } +} + +void EBPFRandomPSA::emitRead(CodeBuilder* builder) const { + if (minValue == maxValue || range == 0) { + builder->append(minValue); + return; + } + + bool rangeIsPowerOf2 = (range & (range - 1)) == 0; + + if (minValue != 0) + builder->appendFormat("(%uu + ", minValue); + + builder->append("(bpf_get_prandom_u32() "); + if (rangeIsPowerOf2) { + builder->appendFormat("& 0x%llxu", range - 1); + } else { + builder->appendFormat("%% %lluu", range); + } + builder->append(")"); + + if (minValue != 0) + builder->append(")"); +} + +} // namespace EBPF diff --git a/backends/ebpf/psa/externs/ebpfPsaRandom.h b/backends/ebpf/psa/externs/ebpfPsaRandom.h new file mode 100644 index 00000000000..f8cfc6bbc7c --- /dev/null +++ b/backends/ebpf/psa/externs/ebpfPsaRandom.h @@ -0,0 +1,34 @@ +/* +Copyright 2022-present Orange +Copyright 2022-present Open Networking Foundation +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. +*/ +#ifndef BACKENDS_EBPF_PSA_EXTERNS_EBPFPSARANDOM_H_ +#define BACKENDS_EBPF_PSA_EXTERNS_EBPFPSARANDOM_H_ + +#include "frontends/p4/methodInstance.h" +#include "backends/ebpf/ebpfObject.h" + +namespace EBPF { + +class EBPFRandomPSA : public EBPFObject { + unsigned int minValue, maxValue; + long range; + public: + explicit EBPFRandomPSA(const IR::Declaration_Instance* di); + + void processMethod(CodeBuilder* builder, const P4::ExternMethod* method) const; + void emitRead(CodeBuilder* builder) const; +}; + +} // namespace EBPF + +#endif // BACKENDS_EBPF_PSA_EXTERNS_EBPFPSARANDOM_H_ diff --git a/backends/ebpf/tests/p4testdata/random.p4 b/backends/ebpf/tests/p4testdata/random.p4 new file mode 100644 index 00000000000..761e2e643d5 --- /dev/null +++ b/backends/ebpf/tests/p4testdata/random.p4 @@ -0,0 +1,96 @@ +/* +Copyright 2022-present Orange +Copyright 2022-present Open Networking Foundation + +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. +*/ +#include +#include +#include "common_headers.p4" + +header random_t { + bit<32> f1; + bit<16> f2; + bit<16> f3; +} + +struct headers { + ethernet_t eth; + random_t rand; +} + +parser MyIP(packet_in buffer, out headers hdr, inout empty_t bp, + in psa_ingress_parser_input_metadata_t c, in empty_t d, in empty_t e) { + state start { + buffer.extract(hdr.eth); + buffer.extract(hdr.rand); + transition accept; + } +} + +parser MyEP(packet_in buffer, out empty_t a, inout empty_t b, + in psa_egress_parser_input_metadata_t c, in empty_t d, in empty_t e, in empty_t f) { + state start { + transition accept; + } +} + +control MyIC(inout headers a, inout empty_t bc, + in psa_ingress_input_metadata_t istd, inout psa_ingress_output_metadata_t ostd) { + Random(16w0, 16w127) r1; + Random>(0x80_00_00_01, 0x80_00_00_05) r2; + Random(16w256, 16w259) r3; + + action do_forward(PortId_t egress_port) { + a.rand.f3 = r3.read(); + send_to_port(ostd, egress_port); + } + + table tbl_fwd { + key = { + istd.ingress_port : exact; + } + actions = { do_forward; NoAction; } + default_action = do_forward((PortId_t) 5); + size = 100; + } + + apply { + a.rand.f1 = r2.read(); + a.rand.f2 = r1.read(); + tbl_fwd.apply(); + } +} + +control MyEC(inout empty_t a, inout empty_t b, + in psa_egress_input_metadata_t c, inout psa_egress_output_metadata_t d) { + apply { } +} + +control MyID(packet_out buffer, out empty_t a, out empty_t b, out empty_t c, + inout headers d, in empty_t e, in psa_ingress_output_metadata_t f) { + apply { + buffer.emit(d.eth); + buffer.emit(d.rand); + } +} + +control MyED(packet_out buffer, out empty_t a, out empty_t b, inout empty_t c, in empty_t d, + in psa_egress_output_metadata_t e, in psa_egress_deparser_input_metadata_t f) { + apply { } +} + +IngressPipeline(MyIP(), MyIC(), MyID()) ip; +EgressPipeline(MyEP(), MyEC(), MyED()) ep; + +PSA_Switch(ip, PacketReplicationEngine(), ep, BufferingQueueingEngine()) main; \ No newline at end of file diff --git a/backends/ebpf/tests/ptf/test.py b/backends/ebpf/tests/ptf/test.py index d81b80a881c..512e2c1ac02 100644 --- a/backends/ebpf/tests/ptf/test.py +++ b/backends/ebpf/tests/ptf/test.py @@ -432,6 +432,58 @@ def runTest(self): testutils.verify_no_other_packets(self) +# xdp2tc=head is not supported because pkt_len of test packet (Ethernet+RandomHeader) < 34 B +@xdp2tc_head_not_supported +class RandomPSATest(P4EbpfTest): + """ + Read random data generated by data plane. + Verify that random values are in the expected range. + """ + p4_file_path = "p4testdata/random.p4" + + class RandomHeader(Packet): + name = "random" + fields_desc = [ + IntField("f1", 0), + ShortField("f2", 0), + ShortField("f3", 0) + ] + + def setUp(self): + super(RandomPSATest, self).setUp() + bind_layers(Ether, self.RandomHeader, type=0x801) + + def tearDown(self): + split_layers(Ether, self.RandomHeader, type=0x801) + super(RandomPSATest, self).tearDown() + + def verify_range(self, value, min_value, max_value): + if value < min_value or value > max_value: + self.fail("Value {} out of range [{}, {}]".format(value, min_value, max_value)) + + def runTest(self): + self.table_add(table="MyIC_tbl_fwd", keys=[4], action=1, data=[5]) + pkt = Ether() / self.RandomHeader() + mask = Mask(pkt) + mask.set_do_not_care_scapy(self.RandomHeader, "f1") + mask.set_do_not_care_scapy(self.RandomHeader, "f2") + mask.set_do_not_care_scapy(self.RandomHeader, "f3") + sequence = [[], [], []] + for _ in range(10): + testutils.send_packet(self, PORT0, pkt) + (_, recv_pkt) = testutils.verify_packet_any_port(self, mask, ALL_PORTS) + recv_pkt = Ether(recv_pkt) + self.verify_range(value=recv_pkt[self.RandomHeader].f1, min_value=0x80_00_00_01, max_value=0x80_00_00_05) + sequence[0].append(recv_pkt[self.RandomHeader].f1) + self.verify_range(value=recv_pkt[self.RandomHeader].f2, min_value=0, max_value=127) + sequence[1].append(recv_pkt[self.RandomHeader].f2) + self.verify_range(value=recv_pkt[self.RandomHeader].f3, min_value=256, max_value=259) + sequence[2].append(recv_pkt[self.RandomHeader].f3) + logger.info("f1 sequence: {}".format(sequence[0])) + logger.info("f2 sequence: {}".format(sequence[1])) + logger.info("f3 sequence: {}".format(sequence[2])) + + class VerifyPSATest(P4EbpfTest): p4_file_path = "p4testdata/verify.p4"