From 3818b2d003f3fca49afce07583ee41336ccfecad Mon Sep 17 00:00:00 2001 From: Francis Dupont Date: Thu, 21 Aug 2025 01:00:46 +0200 Subject: [PATCH] [#3860] Checkpoint: doing vsa --- src/hooks/dhcp/radius/client_attribute.cc | 114 ++++++++++++- src/hooks/dhcp/radius/client_attribute.h | 157 +++++++++++++++++- src/hooks/dhcp/radius/client_dictionary.cc | 8 + src/hooks/dhcp/radius/client_dictionary.h | 3 +- .../dhcp/radius/tests/attribute_unittests.cc | 29 +++- .../dhcp/radius/tests/dictionary_unittests.cc | 13 ++ 6 files changed, 310 insertions(+), 14 deletions(-) diff --git a/src/hooks/dhcp/radius/client_attribute.cc b/src/hooks/dhcp/radius/client_attribute.cc index 403f283d16..41aac45726 100644 --- a/src/hooks/dhcp/radius/client_attribute.cc +++ b/src/hooks/dhcp/radius/client_attribute.cc @@ -80,6 +80,8 @@ Attribute::fromText(const AttrDefPtr& def, const string& value) { return (AttrIpv6Addr::fromText(def->type_, value)); case PW_TYPE_IPV6PREFIX: return (AttrIpv6Prefix::fromText(def->type_, value)); + case PW_TYPE_VSA: + return (AttrVSA::fromText(def->type_, value)); default: // Impossible case. isc_throw(OutOfRange, "unknown value type " @@ -131,6 +133,8 @@ Attribute::fromBytes(const AttrDefPtr& def, const vector& value) { return (AttrIpv6Addr::fromBytes(def->type_, value)); case PW_TYPE_IPV6PREFIX: return (AttrIpv6Prefix::fromBytes(def->type_, value)); + case PW_TYPE_VSA: + return (AttrVSA::fromBytes(def->type_, value)); default: // Impossible case. isc_throw(OutOfRange, "unknown value type " @@ -177,13 +181,13 @@ Attribute::fromIpv6Prefix(const uint8_t type, const uint8_t len, string Attribute::toString() const { - isc_throw(TypeError, "the attribute value type must be string, not " + isc_throw(TypeError, "the attribute value type must be string or vsa, not " << attrValueTypeToText(getValueType())); } vector Attribute::toBinary() const { - isc_throw(TypeError, "the attribute value type must be string, not " + isc_throw(TypeError, "the attribute value type must be string or vsa, not " << attrValueTypeToText(getValueType())); } @@ -217,6 +221,18 @@ Attribute::toIpv6PrefixLen() const { << attrValueTypeToText(getValueType())); } +uint32_t +Attribute::toVendorId() const { + isc_throw(TypeError, "the attribute value type must be vsa, not " + << attrValueTypeToText(getValueType())); +} + +void +Attribute::setVendorId(const uint32_t vendor) { + isc_throw(TypeError, "the attribute value type must be vsa, not " + << attrValueTypeToText(getValueType())); +} + AttrString::AttrString(const uint8_t type, const vector& value) : Attribute(type), value_() { if (value.empty()) { @@ -612,6 +628,100 @@ AttrIpv6Prefix::toElement() const { return (output); } +AttrVSA::AttrVSA(const uint8_t type, const int32_t vendor, + const vector& value) + : Attribute(type), vendor_(vendor), value_() { + if (value.empty()) { + isc_throw(BadValue, "value is empty"); + } + if (value.size() > MAX_VSA_DATA_LEN) { + isc_throw(BadValue, "value is too large " << value.size() + << " > " << MAX_VSA_DATA_LEN); + } + value_.resize(value.size()); + memmove(&value_[0], &value[0], value_.size()); +} + +AttributePtr +AttrVSA::fromText(const uint8_t type, const string& repr) { + isc_throw(NotImplemented, "Can't decode VSA from text"); +} + +AttributePtr +AttrVSA::fromBytes(const uint8_t type, const vector& bytes) { + if (bytes.empty()) { + isc_throw(BadValue, "empty attribute value"); + } + if (bytes.size() < 5) { + isc_throw(BadValue, "value is too small " << bytes.size() << " < 5"); + } else if (bytes.size() > MAX_STRING_LEN) { + isc_throw(BadValue, "value is too large " << bytes.size() + << " > " << MAX_STRING_LEN); + } + uint32_t vendor = bytes[0] << 24; + vendor |= bytes[1] << 16; + vendor |= bytes[2] << 8; + vendor |= bytes[3]; + vector value; + value.resize(bytes.size() - 4); + if (value.size() > 0) { + memmove(&value[0], &bytes[4], value.size()); + } + return (AttributePtr(new AttrVSA(type, vendor, value))); +} + +string +AttrVSA::toText(size_t indent) const { + isc_throw(NotImplemented, "Can't encode VSA into text"); +} + +std::vector +AttrVSA::toBytes() const { + vector output; + output.resize(2 + getValueLen()); + output[0] = getType(); + output[1] = 2 + getValueLen(); + output[2] = (vendor_ & 0xff000000U) >> 24; + output[3] = (vendor_ & 0xff0000U) >> 16; + output[4] = (vendor_ & 0xff00U) >> 8; + output[5] = vendor_ & 0xffU; + if (output.size() > 6) { + memmove(&output[6], &value_[0], output.size() - 6); + } + return (output); +} + +std::vector +AttrVSA::toBinary() const { + vector binary; + binary.resize(getValueLen() - 4); + if (binary.size() > 0) { + memmove(&binary[0], &value_[0], binary.size()); + } + return (binary); +} + +ElementPtr +AttrVSA::toElement() const { + ElementPtr output = Element::createMap(); + AttrDefPtr def = AttrDefs::instance().getByType(getType()); + if (def) { + output->set("name", Element::create(def->name_)); + } + output->set("type", Element::create(static_cast(getType()))); + ostringstream vendor; + vendor << vendor_; + output->set("vendor", Element::create(vendor.str())); + vector binary; + binary.resize(value_.size()); + if (binary.size() > 0) { + memmove(&binary[0], value_.c_str(), binary.size()); + } + string raw = encode::encodeHex(binary); + output->set("vsa-raw", Element::create(raw)); + return (output); +} + void Attributes::add(const ConstAttributePtr& attr) { if (!attr) { diff --git a/src/hooks/dhcp/radius/client_attribute.h b/src/hooks/dhcp/radius/client_attribute.h index 5e7d642f23..ac13cfbd79 100644 --- a/src/hooks/dhcp/radius/client_attribute.h +++ b/src/hooks/dhcp/radius/client_attribute.h @@ -28,6 +28,9 @@ namespace radius { /// @brief Maximum string size. static constexpr size_t MAX_STRING_LEN = 253; +/// @brief Maximum vsa data size. +static constexpr size_t MAX_VSA_DATA_LEN = MAX_STRING_LEN - 4; + /// @brief Type error. using isc::data::TypeError; @@ -159,6 +162,28 @@ public: const uint8_t len, const asiolink::IOAddress& value); + /// @brief From Vendor ID and string data with type. + /// + /// @note Requires the type to be of the Vendor Specific attribute (26). + /// + /// @param type type of attribute. + /// @param vendor vendor id. + /// @param value vsa data. + static AttributePtr fromVSA(const uint8_t type, + const uint32_t vendor, + const std::string& value); + + /// @brief From Vendor ID and binary data with type. + /// + /// @note Requires the type to be of the Vendor Specific attribute (26). + /// + /// @param type type of attribute. + /// @param vendor vendor id. + /// @param value vsa data. + static AttributePtr fromVSA(const uint8_t type, + const uint32_t vendor, + const std::vector& value); + /// Generic get methods. /// @brief Value length. @@ -184,13 +209,13 @@ public: /// @brief To string. /// /// @return the string value. - /// @throw TypeError if the attribute is not a string one. + /// @throw TypeError if the attribute is not a string or vsa one. virtual std::string toString() const; /// @brief To binary. /// /// @return the string value as a binary. - /// @throw TypeError if the attribute is not a string one. + /// @throw TypeError if the attribute is not a string or vsa one. virtual std::vector toBinary() const; /// @brief To integer. @@ -223,6 +248,20 @@ public: /// @throw TypeError if the attribute is not an ipv6prefix one. virtual uint8_t toIpv6PrefixLen() const; + /// @brief To vendor id. + /// + /// @return the vendor id. + /// @throw TypeError if the attribute is not a vsa one. + virtual uint32_t toVendorId() const; + + /// Generic set methods. + + /// @brief Set vendor id. + /// + /// @param vendor vendor id. + /// @throw TypeError if the attribute is not a vsa one. + virtual void setVendorId(const uint32_t vendor); + /// @brief Type. const uint8_t type_; }; @@ -233,6 +272,7 @@ public: /// @brief RADIUS attribute holding strings. class AttrString : public Attribute { protected: + /// @brief Constructor. /// /// @param type attribute type. @@ -648,6 +688,119 @@ private: asiolink::IOAddress value_; }; +/// @brief RADIUS attribute holding vsa. +class AttrVSA : public Attribute { +protected: + + /// @brief Constructor. + /// + /// @param type attribute type. + /// @param vendor vendor id. + /// @param value string vsa data. + AttrVSA(const uint8_t type, const int32_t vendor, const std::string& value) + : Attribute(type), vendor_(vendor), value_(value) { + if (value.empty()) { + isc_throw(BadValue, "value is empty"); + } + if (value.size() > MAX_VSA_DATA_LEN) { + isc_throw(BadValue, "value is too large " << value.size() + << " > " << MAX_VSA_DATA_LEN); + } + } + + /// @brief Constructor. + /// + /// @param type attribute type. + /// @param vendor vendor id. + /// @param value binary vsa data. + AttrVSA(const uint8_t type, const int32_t vendor, + const std::vector& value); + + /// @brief From text. + /// + /// @param type attribute type. + /// @param repr value representation. + /// @return pointer to the attribute or null. + static AttributePtr fromText(const uint8_t type, const std::string& repr); + + /// @brief From bytes. + /// + /// @param type attribute type. + /// @param bytes binary value. + /// @return pointer to the attribute or null. + static AttributePtr fromBytes(const uint8_t type, + const std::vector& bytes); + + /// Make Attribute a friend class. + friend class Attribute; + +public: + + /// @brief Get value type. + /// + /// @return the value type. + virtual AttrValueType getValueType() const override { + return (PW_TYPE_VSA); + } + + /// @brief Value length. + /// + /// @return Value length. + virtual size_t getValueLen() const override { + return (4 + value_.size()); + } + + /// @brief Returns text representation of the attribute. + /// + /// @param indent number of spaces before printing text. + /// @return string with text representation. + virtual std::string toText(size_t indent = 0) const override; + + /// @brief To bytes. + /// + /// @return binary representation. + virtual std::vector toBytes() const override; + + /// @brief To string. + /// + /// @return the string value. + virtual std::string toString() const override { + return (value_); + } + + /// @brief To binary. + /// + /// @return the string value as a binary. + virtual std::vector toBinary() const override; + + /// @brief To vendor id. + /// + /// @return the vendor id. + virtual uint32_t toVendorId() const override { + return (vendor_); + } + + /// @brief Set vendor id. + /// + /// @param vendor vendor id. + virtual void setVendorId(const uint32_t vendor) override { + vendor_ = vendor; + } + + /// @brief Unparse attribute. + /// + /// @return a pointer to unparsed attribute. + virtual data::ElementPtr toElement() const override; + +private: + /// @brief Vendor id. + uint32_t vendor_; + + /// @brief Value. + std::string value_; +}; + + /// @brief Collection of attributes. class Attributes : public data::CfgToElement { public: diff --git a/src/hooks/dhcp/radius/client_dictionary.cc b/src/hooks/dhcp/radius/client_dictionary.cc index 01f7b9b641..988ab2f2ff 100644 --- a/src/hooks/dhcp/radius/client_dictionary.cc +++ b/src/hooks/dhcp/radius/client_dictionary.cc @@ -34,6 +34,8 @@ attrValueTypeToText(const AttrValueType value) { return ("ipv6addr"); case PW_TYPE_IPV6PREFIX: return ("ipv6prefix"); + case PW_TYPE_VSA: + return ("vsa"); default: // Impossible case. return ("unknown?"); @@ -52,6 +54,8 @@ textToAttrValueType(const string& name) { return (PW_TYPE_IPV6ADDR); } else if (name == "ipv6prefix") { return (PW_TYPE_IPV6PREFIX); + } else if (name == "vsa") { + return (PW_TYPE_VSA); } else { isc_throw(OutOfRange, "unknown AttrValueType name " << name); } @@ -230,6 +234,10 @@ AttrDefs::parseLine(const string& line, unsigned int depth) { isc_throw(Unexpected, "can't parse attribute type " << type_str); } AttrValueType value_type = textToAttrValueType(tokens[3]); + if ((value_type == PW_TYPE_VSA) && (type != PW_VENDOR_SPECIFIC)) { + isc_throw(BadValue, "only Vendor-Specific (26) attribute can " + << "have the vsa data type"); + } AttrDefPtr def(new AttrDef(type, name, value_type)); add(def); return; diff --git a/src/hooks/dhcp/radius/client_dictionary.h b/src/hooks/dhcp/radius/client_dictionary.h index 21ee11b9b1..4b6824faee 100644 --- a/src/hooks/dhcp/radius/client_dictionary.h +++ b/src/hooks/dhcp/radius/client_dictionary.h @@ -31,7 +31,8 @@ enum AttrValueType { PW_TYPE_INTEGER, PW_TYPE_IPADDR, PW_TYPE_IPV6ADDR, - PW_TYPE_IPV6PREFIX + PW_TYPE_IPV6PREFIX, + PW_TYPE_VSA }; /// @brief AttrValueType value -> name function. diff --git a/src/hooks/dhcp/radius/tests/attribute_unittests.cc b/src/hooks/dhcp/radius/tests/attribute_unittests.cc index 2d72b05c51..1bc3e32c90 100644 --- a/src/hooks/dhcp/radius/tests/attribute_unittests.cc +++ b/src/hooks/dhcp/radius/tests/attribute_unittests.cc @@ -175,6 +175,8 @@ TEST_F(AttributeTest, attrString) { "the attribute value type must be ipv6prefix, not string"); EXPECT_THROW_MSG(attr->toIpv6PrefixLen(), TypeError, "the attribute value type must be ipv6prefix, not string"); + EXPECT_THROW_MSG(attr->toVendorId(), TypeError, + "the attribute value type must be vsa, not string"); } // Verifies raw string attribute. @@ -268,9 +270,9 @@ TEST_F(AttributeTest, attrInt) { << def_bytes->toText() << " != " << attr->toText(); EXPECT_THROW_MSG(attr->toString(), TypeError, - "the attribute value type must be string, not integer"); + "the attribute value type must be string or vsa, not integer"); EXPECT_THROW_MSG(attr->toBinary(), TypeError, - "the attribute value type must be string, not integer"); + "the attribute value type must be string or vsa, not integer"); EXPECT_THROW_MSG(attr->toIpAddr(), TypeError, "the attribute value type must be ipaddr, not integer"); EXPECT_THROW_MSG(attr->toIpv6Addr(), TypeError, @@ -278,7 +280,10 @@ TEST_F(AttributeTest, attrInt) { EXPECT_THROW_MSG(attr->toIpv6Prefix(), TypeError, "the attribute value type must be ipv6prefix, not integer"); EXPECT_THROW_MSG(attr->toIpv6PrefixLen(), TypeError, - "the attribute value type must be ipv6prefix, not integer"); + "the attribute value type must be ipv6prefix, not integer" +); + EXPECT_THROW_MSG(attr->toVendorId(), TypeError, + "the attribute value type must be vsa, not integer"); } // Verifies IP address attribute. @@ -338,9 +343,9 @@ TEST_F(AttributeTest, attrIpAddr) { << def_bytes->toText() << " != " << attr->toText(); EXPECT_THROW_MSG(attr->toString(), TypeError, - "the attribute value type must be string, not ipaddr"); + "the attribute value type must be string or vsa, not ipaddr"); EXPECT_THROW_MSG(attr->toBinary(), TypeError, - "the attribute value type must be string, not ipaddr"); + "the attribute value type must be string or vsa, not ipaddr"); EXPECT_THROW_MSG(attr->toInt(), TypeError, "the attribute value type must be integer, not ipaddr"); EXPECT_THROW_MSG(attr->toIpv6Addr(), TypeError, @@ -349,6 +354,8 @@ TEST_F(AttributeTest, attrIpAddr) { "the attribute value type must be ipv6prefix, not ipaddr"); EXPECT_THROW_MSG(attr->toIpv6PrefixLen(), TypeError, "the attribute value type must be ipv6prefix, not ipaddr"); + EXPECT_THROW_MSG(attr->toVendorId(), TypeError, + "the attribute value type must be vsa, not ipaddr"); } // Verifies IPv6 address attribute. @@ -415,9 +422,9 @@ TEST_F(AttributeTest, attrIpv6Addr) { << def_bytes->toText() << " != " << attr->toText(); EXPECT_THROW_MSG(attr->toString(), TypeError, - "the attribute value type must be string, not ipv6addr"); + "the attribute value type must be string or vsa, not ipv6addr"); EXPECT_THROW_MSG(attr->toBinary(), TypeError, - "the attribute value type must be string, not ipv6addr"); + "the attribute value type must be string or vsa, not ipv6addr"); EXPECT_THROW_MSG(attr->toInt(), TypeError, "the attribute value type must be integer, not ipv6addr"); EXPECT_THROW_MSG(attr->toIpAddr(), TypeError, @@ -426,6 +433,8 @@ TEST_F(AttributeTest, attrIpv6Addr) { "the attribute value type must be ipv6prefix, not ipv6addr"); EXPECT_THROW_MSG(attr->toIpv6PrefixLen(), TypeError, "the attribute value type must be ipv6prefix, not ipv6addr"); + EXPECT_THROW_MSG(attr->toVendorId(), TypeError, + "the attribute value type must be vsa, not ipv6addr"); } // Verifies IPv6 prefix attribute. @@ -524,15 +533,17 @@ TEST_F(AttributeTest, attrIpv6Prefix) { << def_bytes->toText() << " != " << attr->toText(); EXPECT_THROW_MSG(attr->toString(), TypeError, - "the attribute value type must be string, not ipv6prefix"); + "the attribute value type must be string or vsa, not ipv6prefix"); EXPECT_THROW_MSG(attr->toBinary(), TypeError, - "the attribute value type must be string, not ipv6prefix"); + "the attribute value type must be string or vsa, not ipv6prefix"); EXPECT_THROW_MSG(attr->toInt(), TypeError, "the attribute value type must be integer, not ipv6prefix"); EXPECT_THROW_MSG(attr->toIpAddr(), TypeError, "the attribute value type must be ipaddr, not ipv6prefix"); EXPECT_THROW_MSG(attr->toIpv6Addr(), TypeError, "the attribute value type must be ipv6addr, not ipv6prefix"); + EXPECT_THROW_MSG(attr->toVendorId(), TypeError, + "the attribute value type must be vsa, not ipv6prefix"); } // Verifies basic methods for attribute collection. diff --git a/src/hooks/dhcp/radius/tests/dictionary_unittests.cc b/src/hooks/dhcp/radius/tests/dictionary_unittests.cc index 2fbd014bc9..a0589478d3 100644 --- a/src/hooks/dhcp/radius/tests/dictionary_unittests.cc +++ b/src/hooks/dhcp/radius/tests/dictionary_unittests.cc @@ -180,6 +180,19 @@ TEST_F(DictionaryTest, parseLines) { expected = "Illegal attribute redefinition of 'Service-Type' "; expected += "type 6 value type integer by 6 string at line 2"; EXPECT_THROW_MSG(parseLines(new_value_type), BadValue, expected); + + // Only the attribute 26 (Vendor-Specific) can have the vsa data type. + list bad_vsa = { + "ATTRIBUTE Attr126 126 vsa" + }; + expected = "only Vendor-Specific (26) attribute can have "; + expected += "the vsa data type at line 1"; + EXPECT_THROW_MSG(parseLines(bad_vsa), BadValue, expected); + + list vsa = { + "ATTRIBUTE Attr26 26 vsa" + }; + EXPECT_NO_THROW_LOG(parseLines(vsa)); } // Verifies integer constant definitions. -- 2.47.3