Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/arstack_studio/src/DeviceController.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

#include "DeviceIoWorker.hpp"

#include "ariec61850/ethernet/ethernet.hpp"

#include <QDateTime>
#include <QDebug>
#include <QMetaObject>
Expand Down Expand Up @@ -669,7 +671,9 @@ bool DeviceController::deployProfile(const QVariantMap& profile) {
const QString idHex = utf8Hex(svId);
const QString dataSetHex = includeDataSet ? utf8Hex(dataSet) : QStringLiteral("-");
if (svId.isEmpty() || idHex.isEmpty() || idHex.size() > 180 || dataSetHex.size() > 170 ||
mac.size() != 12 || appId == 0U || appId > 65535U || vlanId > 4095U || pcp > 7U ||
mac.size() != 12 || appId == 0U || appId > 65535U ||
vlanId > ar::iec61850::ethernet::maximum_vlan_id ||
pcp > ar::iec61850::ethernet::maximum_vlan_priority ||
rate == 0U || rate > 65535U || modulus == 0U || modulus > 65535U || noAsdu != 1U) {
setError(QStringLiteral("Compiled profile exceeds the current bounded device bridge."));
return false;
Expand Down
8 changes: 6 additions & 2 deletions embedded/esp32p4_smv_injector/main/profile_control.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "runtime_profile.hpp"
#include "smp_synch_lab.hpp"

#include "ariec61850/ethernet/ethernet.hpp"

#include "esp_log.h"
#include "sdkconfig.h"

Expand Down Expand Up @@ -311,8 +313,10 @@ void handle_profile_command(char* arguments) noexcept {
if (!parse_u32(appid_text, appid) || appid == 0U || appid > 65535U ||
!decode_mac(mac_text, mac) ||
!parse_u32(vlan_present_text, vlan_present) || vlan_present > 1U ||
!parse_u32(vlan_id_text, vlan_id) || vlan_id > 4095U ||
!parse_u32(pcp_text, pcp) || pcp > 7U || !no_extra(&save)) {
!parse_u32(vlan_id_text, vlan_id) ||
vlan_id > ar::iec61850::ethernet::maximum_vlan_id ||
!parse_u32(pcp_text, pcp) ||
pcp > ar::iec61850::ethernet::maximum_vlan_priority || !no_extra(&save)) {
ESP_LOGE(kTag, "Invalid PROFILE L2 payload");
return;
}
Expand Down
6 changes: 5 additions & 1 deletion embedded/esp32p4_smv_injector/main/runtime_profile.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include "runtime_profile.hpp"

#include "ariec61850/ethernet/ethernet.hpp"

#include "freertos/FreeRTOS.h"

#include <algorithm>
Expand Down Expand Up @@ -46,7 +48,9 @@ bool runtime_profile_validate(const RuntimePublisherProfile& profile) noexcept {
profile.sv_id[0] == '\0') {
return false;
}
if (profile.vlan_present && (profile.vlan_id > 4095U || profile.vlan_priority > 7U)) {
if (profile.vlan_present &&
(!ar::iec61850::ethernet::is_valid_vlan_id(profile.vlan_id) ||
!ar::iec61850::ethernet::is_valid_vlan_priority(profile.vlan_priority))) {
return false;
}
if (profile.include_data_set && profile.data_set_reference[0] == '\0') {
Expand Down
14 changes: 14 additions & 0 deletions include/ariec61850/ethernet/ethernet.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ inline constexpr std::uint16_t goose_ethertype = 0x88B8;
inline constexpr std::uint16_t sampled_values_ethertype = 0x88BA;
inline constexpr std::uint16_t ptp_ethertype = 0x88F7;

// IEEE 802.1Q VID 4095 is reserved. VID 0 remains valid at the generic
// Ethernet layer for priority-tagged frames; profile-specific users may impose
// a narrower policy without changing the wire invariant.
inline constexpr std::uint16_t maximum_vlan_id = 4094U;
inline constexpr std::uint8_t maximum_vlan_priority = 7U;

[[nodiscard]] constexpr bool is_valid_vlan_id(const std::uint16_t vlan_id) noexcept {
return vlan_id <= maximum_vlan_id;
}

[[nodiscard]] constexpr bool is_valid_vlan_priority(const std::uint8_t priority) noexcept {
return priority <= maximum_vlan_priority;
}

class MacAddress final {
public:
MacAddress() = default;
Expand Down
7 changes: 4 additions & 3 deletions include/ariec61850/sampled_values/publisher_profile.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#pragma once

#include "ariec61850/ethernet/ethernet.hpp"
#include "ariec61850/scl/model.hpp"

#include <array>
Expand Down Expand Up @@ -134,10 +135,10 @@ class SvPublisherProfileCompiler final {
profile.vlan_present = true;
profile.vlan_id = *stream.address.vlan_id;
profile.vlan_priority = *stream.address.vlan_priority;
if (profile.vlan_id > 4095U) {
result.errors.push_back("SV VLAN ID exceeds the 12-bit Ethernet VLAN range.");
if (!ethernet::is_valid_vlan_id(profile.vlan_id)) {
result.errors.push_back("SV VLAN ID must be 0..4094; VID 4095 is reserved.");
}
if (profile.vlan_priority > 7U) {
if (!ethernet::is_valid_vlan_priority(profile.vlan_priority)) {
result.errors.push_back("SV VLAN priority exceeds the 3-bit Ethernet PCP range.");
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/ethernet/ethernet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@ std::string MacAddress::to_string() const {
}

std::uint16_t VlanTag::to_tag_control_information() const {
if (priority_code_point > 7U) {
if (!is_valid_vlan_priority(priority_code_point)) {
throw std::out_of_range("VLAN priority must be 0..7.");
}
if (vlan_id > 4094U) {
if (!is_valid_vlan_id(vlan_id)) {
throw std::out_of_range("VLAN ID must be 0..4094.");
}

Expand Down Expand Up @@ -182,7 +182,7 @@ bool EthernetFrameCodec::try_decode(const std::span<const std::uint8_t> bytes,
VlanTag::from_tag_control_information(read_u16_be(bytes, 14U));
// IEEE 802.1Q reserves VID 4095. Keep try_decode closed under encode:
// every successfully decoded VlanTag must be valid for re-encoding.
if (decoded_vlan.vlan_id > 4094U) {
if (!is_valid_vlan_id(decoded_vlan.vlan_id)) {
return false;
}
vlan = decoded_vlan;
Expand Down
3 changes: 2 additions & 1 deletion src/sampled_values/frame_codec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ void write_u16_be(

[[nodiscard]] std::optional<std::uint16_t> vlan_tci(
const ethernet::VlanTag& vlan) noexcept {
if (vlan.priority_code_point > 7U || vlan.vlan_id > 4094U) {
if (!ethernet::is_valid_vlan_priority(vlan.priority_code_point) ||
!ethernet::is_valid_vlan_id(vlan.vlan_id)) {
return std::nullopt;
}
const auto value =
Expand Down
3 changes: 2 additions & 1 deletion src/time_sync/ptp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ std::vector<std::uint8_t> PtpCodec::build_ethernet_frame(
const std::span<const std::uint8_t> ptp_message,
const std::optional<std::uint16_t> vlan_id,
const std::uint8_t vlan_priority) {
if (vlan_priority > 7U || (vlan_id.has_value() && *vlan_id > 4095U)) {
if (!ethernet::is_valid_vlan_priority(vlan_priority) ||
(vlan_id.has_value() && !ethernet::is_valid_vlan_id(*vlan_id))) {
return {};
}

Expand Down
16 changes: 16 additions & 0 deletions tests/test_ptp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,21 @@ void transport_specific_change_is_visible_as_health_warning() {
CHECK(resolve_smp_synch(report, true) == SmpSynchValue::local_synchronized);
}

void ethernet_builder_enforces_canonical_vlan_boundaries() {
using namespace ar::iec61850::time_sync;
const std::array<std::uint8_t, 6> source{0x02U, 0x00U, 0x00U, 0x00U, 0x00U, 0x01U};
const auto message = PtpCodec::build_sync(make_options());

CHECK(!PtpCodec::build_ethernet_frame(
ptp_general_multicast_mac, source, message, std::uint16_t{0U}, std::uint8_t{4U}).empty());
CHECK(!PtpCodec::build_ethernet_frame(
ptp_general_multicast_mac, source, message, std::uint16_t{4094U}, std::uint8_t{7U}).empty());
CHECK(PtpCodec::build_ethernet_frame(
ptp_general_multicast_mac, source, message, std::uint16_t{4095U}, std::uint8_t{4U}).empty());
CHECK(PtpCodec::build_ethernet_frame(
ptp_general_multicast_mac, source, message, std::uint16_t{100U}, std::uint8_t{8U}).empty());
}

void malformed_or_non_ptp_frames_are_rejected() {
using namespace ar::iec61850::time_sync;
PtpFrame frame;
Expand All @@ -308,6 +323,7 @@ int main() {
{"PTP VLAN/QinQ/transportSpecific analyzer parser", ethernet_vlan_qinq_and_transport_specific_are_parsed_for_analyzer_use},
{"PTP monitor health and smpSynch", passive_monitor_health_drives_conservative_smp_synch_policy},
{"PTP transportSpecific stability", transport_specific_change_is_visible_as_health_warning},
{"PTP Ethernet VLAN boundaries", ethernet_builder_enforces_canonical_vlan_boundaries},
{"PTP malformed input", malformed_or_non_ptp_frames_are_rejected},
};

Expand Down
19 changes: 19 additions & 0 deletions tests/test_sampled_values.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,25 @@ void sampled_values_frame_round_trips_vlan_process_bus_header() {
CHECK(SampledValuesFrameCodec::try_decode(encoded, decoded));
CHECK(decoded == frame);

auto priority_tagged = frame;
priority_tagged.vlan = ethernet::VlanTag{4U, 0U};
CHECK(SampledValuesFrameCodec::encoded_size(priority_tagged).has_value());

auto maximum_vlan = frame;
maximum_vlan.vlan = ethernet::VlanTag{7U, 4094U};
CHECK(SampledValuesFrameCodec::encoded_size(maximum_vlan).has_value());

auto reserved_vlan = frame;
reserved_vlan.vlan = ethernet::VlanTag{4U, 4095U};
CHECK(!SampledValuesFrameCodec::encoded_size(reserved_vlan).has_value());
bool reserved_vlan_rejected = false;
try {
static_cast<void>(SampledValuesFrameCodec::encode(reserved_vlan));
} catch (const std::out_of_range&) {
reserved_vlan_rejected = true;
}
CHECK(reserved_vlan_rejected);

auto wrong_ethertype = encoded;
wrong_ethertype[16] = 0x88U;
wrong_ethertype[17] = 0xB8U;
Expand Down
18 changes: 18 additions & 0 deletions tests/test_scl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,24 @@ void parser_compiles_structured_4800_sv_profile_without_drift() {
CHECK(profile.payload_size_bytes == 64U);
CHECK(profile.asdu_options.element_present);

auto maximum_vlan_stream = stream;
maximum_vlan_stream.address.vlan_id = static_cast<std::uint16_t>(4094U);
const auto maximum_vlan = SvPublisherProfileCompiler::compile(maximum_vlan_stream, context);
CHECK(maximum_vlan.ok());
CHECK(maximum_vlan.profile.has_value());
CHECK(maximum_vlan.profile->vlan_id == 4094U);

auto reserved_vlan_stream = stream;
reserved_vlan_stream.address.vlan_id = static_cast<std::uint16_t>(4095U);
const auto reserved_vlan = SvPublisherProfileCompiler::compile(reserved_vlan_stream, context);
CHECK(!reserved_vlan.ok());
CHECK(!reserved_vlan.profile.has_value());
CHECK(std::any_of(
reserved_vlan.errors.begin(), reserved_vlan.errors.end(),
[](const std::string& error) {
return error.find("0..4094") != std::string::npos;
}));

RationalTickSchedule schedule_4800{1'000'000U, 4800U};
std::uint64_t total_ticks{};
std::uint32_t intervals_208{};
Expand Down
Loading