fix: enhance KNX OAM router functionality and security features

Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
Tony
2026-05-29 01:31:58 +08:00
parent bb0fb01c00
commit f39ae6f0c6
13 changed files with 375 additions and 35 deletions
@@ -23,6 +23,7 @@ class EtsDeviceRuntime {
public:
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
using CemiFrameReceiver = std::function<bool(const uint8_t* data, size_t len)>;
using TpAckHandler = std::function<TPAckType(uint16_t destination, bool is_group_address)>;
using GroupWriteHandler = std::function<void(uint16_t group_address, const uint8_t* data,
size_t len)>;
using GroupObjectWriteHandler = std::function<void(uint16_t group_object_number,
@@ -64,6 +65,7 @@ class EtsDeviceRuntime {
void setGroupObjectWriteHandler(GroupObjectWriteHandler handler);
void setBusFrameSender(CemiFrameSender sender);
void setTpFrameReceiver(CemiFrameReceiver receiver);
void setTpAckHandler(TpAckHandler handler);
void setNetworkInterface(esp_netif_t* netif);
bool hasTpUart() const;
bool enableTpUart(bool enabled = true);
@@ -124,6 +126,7 @@ class EtsDeviceRuntime {
CemiFrameSender sender_;
CemiFrameSender bus_frame_sender_;
CemiFrameReceiver tp_frame_receiver_;
TpAckHandler tp_ack_handler_;
GroupWriteHandler group_write_handler_;
GroupObjectWriteHandler group_object_write_handler_;
FunctionPropertyHandler command_handler_;
@@ -33,6 +33,7 @@ class TpuartUartInterface;
constexpr uint16_t kGatewayKnxDefaultUdpPort = 3671;
constexpr const char* kGatewayKnxDefaultMulticastAddress = "224.0.23.12";
constexpr const char* kGatewayKnxOamOpenKnxNamespace = "openknx_oam";
constexpr uint32_t kGatewayKnxDefaultTpBaudrate = 19200;
constexpr uint32_t kGatewayKnxDefaultTpStartupTimeoutMs = 2000;
@@ -78,7 +78,7 @@ inline constexpr uint32_t kOamRouterSerialMacIncrement = kDaliMaxKnxInstanceCoun
inline constexpr uint16_t kOamRouterDeviceDescriptor = 0x091A;
inline constexpr uint16_t kOamRouterManufacturerId =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID);
inline constexpr uint16_t kOamRouterHardwareId =
inline constexpr uint16_t kOamRouterLegacyHardwareId =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID);
inline constexpr uint16_t kOamRouterApplicationNumber =
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER);
@@ -87,8 +87,15 @@ inline constexpr uint8_t kOamRouterApplicationVersion =
inline constexpr uint8_t kOamRouterHardwareType[6] = {
0x00,
0x00,
static_cast<uint8_t>((kOamRouterHardwareId >> 8) & 0xff),
static_cast<uint8_t>(kOamRouterHardwareId & 0xff),
static_cast<uint8_t>((kOamRouterApplicationNumber >> 8) & 0xff),
static_cast<uint8_t>(kOamRouterApplicationNumber & 0xff),
kOamRouterApplicationVersion,
0x00};
inline constexpr uint8_t kOamRouterLegacyHardwareType[6] = {
0x00,
0x00,
static_cast<uint8_t>((kOamRouterLegacyHardwareId >> 8) & 0xff),
static_cast<uint8_t>(kOamRouterLegacyHardwareId & 0xff),
kOamRouterApplicationVersion,
0x00};
inline constexpr uint8_t kOamRouterOrderNumber[10] = {
@@ -38,6 +38,8 @@ class OamRouterRuntime {
bool programmingMode() const;
void setProgrammingMode(bool enabled);
void toggleProgrammingMode();
bool matchesSecureSyncSerial(CemiFrame& frame) const;
bool matchesRecentSecureToolAccess(CemiFrame& frame) const;
EtsMemorySnapshot snapshot() const;
DeviceObject* deviceObject();
@@ -60,6 +62,8 @@ class OamRouterRuntime {
std::string nvs_namespace_;
CemiFrameSender sender_;
CemiFrameSender bus_frame_sender_;
mutable uint16_t recent_secure_tool_source_{0xffff};
mutable int64_t recent_secure_tool_sync_us_{0};
#if defined(ENABLE_BAU091A_PERSONA)
EspIdfPlatform platform_;
Bau091A device_;
@@ -301,6 +301,16 @@ void EtsDeviceRuntime::setTpFrameReceiver(CemiFrameReceiver receiver) {
}
}
void EtsDeviceRuntime::setTpAckHandler(TpAckHandler handler) {
tp_ack_handler_ = std::move(handler);
if (auto* data_link_layer = device_.getDataLinkLayer()) {
data_link_layer->acknowledgeHandler([this](uint16_t destination, bool is_group_address) {
return tp_ack_handler_ ? tp_ack_handler_(destination, is_group_address)
: TPAckType::AckReqNone;
});
}
}
void EtsDeviceRuntime::setNetworkInterface(esp_netif_t* netif) {
platform_.networkInterface(netif);
}
@@ -256,7 +256,7 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
ets_device_->deviceObject(), ets_device_->platform());
if (config_.oam_router.enabled) {
oam_router_ = std::make_unique<openknx::OamRouterRuntime>(
openknx_namespace_ + "_oam", config_.oam_router.individual_address,
kGatewayKnxOamOpenKnxNamespace, config_.oam_router.individual_address,
config_.oam_router.tunnel_address_base);
if (oam_router_->available()) {
oam_router_->setProgrammingMode(oam_programming_mode_);
@@ -276,8 +276,8 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
ets_device_->tunnelClientAddress(), commissioning_only_);
if (oam_router_ != nullptr) {
ESP_LOGI(kTag,
"OAM router persona namespace=%s_oam configured=%d device=0x%04x tunnelClient=0x%04x secureTunnel=%d secureRouting=%d",
openknx_namespace_.c_str(), oam_router_->configured(),
"OAM router persona namespace=%s configured=%d device=0x%04x tunnelClient=0x%04x secureTunnel=%d secureRouting=%d",
kGatewayKnxOamOpenKnxNamespace, oam_router_->configured(),
oam_router_->individualAddress(), oam_router_->tunnelClientAddress(),
config_.oam_router.secure_tunnel_enabled,
config_.oam_router.secure_routing_enabled);
@@ -354,6 +354,17 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
ets_device_->setTpFrameReceiver([this](const uint8_t* data, size_t len) {
return handleOpenKnxTpIngressFrame(data, len);
});
ets_device_->setTpAckHandler([this](uint16_t destination, bool is_group_address) {
if (is_group_address || oam_router_ == nullptr) {
return TPAckType::AckReqNone;
}
const bool commissioning = !oam_router_->configured() || oam_router_->programmingMode();
return destination == oam_router_->individualAddress() ||
destination == oam_router_->tunnelClientAddress() ||
(commissioning && destination == 0xffff)
? TPAckType::AckReqAck
: TPAckType::AckReqNone;
});
if (oam_router_ != nullptr) {
oam_router_->setBusFrameSender([this](const uint8_t* data, size_t len) {
publishCloudCemiFrame(data, len);
@@ -34,7 +34,9 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
if (frame.valid()) {
route_to_all_internal_instances = IsKnxBroadcastManagementRequest(frame);
if (!route_to_oam && oam_router_ != nullptr &&
MatchesOamRouterLocalIndividualAddress(frame, *oam_router_)) {
(MatchesOamRouterLocalIndividualAddress(frame, *oam_router_) ||
oam_router_->matchesSecureSyncSerial(frame) ||
oam_router_->matchesRecentSecureToolAccess(frame))) {
route_to_oam = true;
}
}
@@ -326,7 +328,10 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTpIngressFrame(const uint8_t* data, size
const bool broadcast_management = IsKnxBroadcastManagementRequest(frame);
const bool addressed_to_oam =
oam_router_ != nullptr && MatchesOamRouterLocalIndividualAddress(frame, *oam_router_);
oam_router_ != nullptr &&
(MatchesOamRouterLocalIndividualAddress(frame, *oam_router_) ||
oam_router_->matchesSecureSyncSerial(frame) ||
oam_router_->matchesRecentSecureToolAccess(frame));
if (!broadcast_management && !addressed_to_oam) {
return false;
}
@@ -186,7 +186,10 @@ void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, s
const size_t cemi_len = frame.dataLength();
bool consumed_by_local_application = false;
const bool addressed_to_oam =
oam_router_ != nullptr && MatchesOamRouterLocalIndividualAddress(frame, *oam_router_);
oam_router_ != nullptr &&
(MatchesOamRouterLocalIndividualAddress(frame, *oam_router_) ||
oam_router_->matchesSecureSyncSerial(frame) ||
oam_router_->matchesRecentSecureToolAccess(frame));
const bool addressed_to_reg1 =
ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_);
std::vector<uint8_t> local_tunnel_frame;
@@ -4,19 +4,52 @@
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_timer.h"
#include "knx/cemi_server.h"
#include "knx/property.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <utility>
extern "C" bool knx_platform_get_fdsk_for_namespace(const char* nvs_namespace,
uint8_t* data,
size_t len) __attribute__((weak));
namespace gateway::openknx {
namespace {
constexpr uint16_t kInvalidIndividualAddress = 0xffff;
constexpr uint16_t kKnxUnconfiguredBroadcastAddress = 0xffff;
constexpr uint8_t kSecureDataPdu = 0;
constexpr uint8_t kSecureSyncRequest = 2;
constexpr uint8_t kSecureToolAccessFlag = 0x80;
constexpr size_t kKnxSerialLength = 6;
constexpr size_t kSecureApduScfOffset = 1;
constexpr size_t kSecureApduSerialOffset = 8;
constexpr size_t kSecureApduMinimumSyncRequestLength =
kSecureApduSerialOffset + kKnxSerialLength;
constexpr int64_t kSecureToolAccessRouteWindowUs = 120LL * 1000LL * 1000LL;
constexpr uint16_t kOamDeviceObjectVersion = 3;
bool HardwareTypeEquals(const uint8_t* actual, const uint8_t* expected) {
return actual != nullptr && expected != nullptr &&
std::memcmp(actual, expected, sizeof(knx_internal::kOamRouterHardwareType)) == 0;
}
VersionCheckResult OamRouterVersionCheck(uint16_t manufacturer_id,
uint8_t* hardware_type,
uint16_t version) {
if (manufacturer_id != knx_internal::kOamRouterManufacturerId ||
(!HardwareTypeEquals(hardware_type, knx_internal::kOamRouterHardwareType) &&
!HardwareTypeEquals(hardware_type, knx_internal::kOamRouterLegacyHardwareType))) {
return FlashAllInvalid;
}
return version == kOamDeviceObjectVersion ? FlashValid : FlashTablesInvalid;
}
bool IsUsableIndividualAddress(uint16_t address) {
return address != 0 && address != kInvalidIndividualAddress;
@@ -37,6 +70,21 @@ bool IsBroadcastManagementRequest(CemiFrame& frame) {
}
}
bool IsGroupBroadcastSecureToolAccess(CemiFrame& frame, uint8_t* service) {
if (frame.addressType() != GroupAddress || frame.destinationAddress() != 0x0000 ||
frame.apdu().type() != SecureService || frame.apdu().length() <= kSecureApduScfOffset) {
return false;
}
const uint8_t scf = frame.apdu().data()[kSecureApduScfOffset];
if ((scf & kSecureToolAccessFlag) == 0) {
return false;
}
if (service != nullptr) {
*service = scf & 0x07;
}
return true;
}
uint32_t OamBauNumberFromBaseMac() {
uint8_t mac[6]{};
if (esp_efuse_mac_get_default(mac) != ESP_OK && esp_read_mac(mac, ESP_MAC_WIFI_STA) != ESP_OK) {
@@ -59,6 +107,22 @@ void ApplyOamRouterIdentity(Bau091A& device) {
property->write(knx_internal::kOamRouterProgramVersion);
}
}
void ApplyOamFactoryFdsk(Bau091A& device, const std::string& nvs_namespace) {
#if defined(USE_DATASECURE)
if (knx_platform_get_fdsk_for_namespace == nullptr) {
return;
}
std::array<uint8_t, 16> fdsk{};
if (knx_platform_get_fdsk_for_namespace(nvs_namespace.c_str(), fdsk.data(), fdsk.size())) {
device.factoryFdsk(fdsk.data(), fdsk.size());
}
std::fill(fdsk.begin(), fdsk.end(), 0);
#else
(void)device;
(void)nvs_namespace;
#endif
}
#endif
} // namespace
@@ -75,12 +139,15 @@ OamRouterRuntime::OamRouterRuntime(std::string nvs_namespace,
{
#if defined(ENABLE_BAU091A_PERSONA)
platform_.outboundCemiFrameCallback(&OamRouterRuntime::HandleOutboundCemiFrame, this);
ApplyOamFactoryFdsk(device_, nvs_namespace_);
ApplyOamRouterIdentity(device_);
device_.versionCheckCallback(&OamRouterVersionCheck);
if (IsUsableIndividualAddress(fallback_individual_address)) {
device_.deviceObject().individualAddress(fallback_individual_address);
}
ESP_LOGI("gateway_knx", "OAM OpenKNX loading memory namespace=%s", nvs_namespace_.c_str());
device_.readMemory();
ApplyOamFactoryFdsk(device_, nvs_namespace_);
ApplyOamRouterIdentity(device_);
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
IsUsableIndividualAddress(fallback_individual_address)) {
@@ -174,6 +241,55 @@ void OamRouterRuntime::setProgrammingMode(bool enabled) {
void OamRouterRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); }
bool OamRouterRuntime::matchesSecureSyncSerial(CemiFrame& frame) const {
#if defined(ENABLE_BAU091A_PERSONA) && defined(USE_DATASECURE)
uint8_t service = 0;
if (!IsGroupBroadcastSecureToolAccess(frame, &service) ||
frame.apdu().length() < kSecureApduMinimumSyncRequestLength) {
return false;
}
if (service != kSecureSyncRequest) {
return false;
}
const uint8_t* apdu_data = frame.apdu().data();
const uint8_t* serial = apdu_data + kSecureApduSerialOffset;
const uint8_t* local_serial =
const_cast<Bau091A&>(device_).deviceObject().propertyData(PID_SERIAL_NUMBER);
const bool matches = local_serial != nullptr &&
std::memcmp(serial, local_serial, kKnxSerialLength) == 0;
if (matches) {
recent_secure_tool_source_ = frame.sourceAddress();
recent_secure_tool_sync_us_ = esp_timer_get_time();
}
return matches;
#else
(void)frame;
return false;
#endif
}
bool OamRouterRuntime::matchesRecentSecureToolAccess(CemiFrame& frame) const {
#if defined(ENABLE_BAU091A_PERSONA) && defined(USE_DATASECURE)
uint8_t service = 0;
if (!IsGroupBroadcastSecureToolAccess(frame, &service) || service != kSecureDataPdu ||
recent_secure_tool_sync_us_ <= 0 || frame.sourceAddress() != recent_secure_tool_source_) {
return false;
}
const int64_t now = esp_timer_get_time();
if (now < recent_secure_tool_sync_us_ ||
now - recent_secure_tool_sync_us_ > kSecureToolAccessRouteWindowUs) {
return false;
}
recent_secure_tool_sync_us_ = now;
return true;
#else
(void)frame;
return false;
#endif
}
EtsMemorySnapshot OamRouterRuntime::snapshot() const {
EtsMemorySnapshot out;
#if defined(ENABLE_BAU091A_PERSONA)
@@ -300,11 +416,18 @@ void OamRouterRuntime::loop() {
bool OamRouterRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) {
auto* self = static_cast<OamRouterRuntime*>(context);
if (self == nullptr || !self->sender_) {
if (self == nullptr) {
return false;
}
self->sender_(frame.data(), frame.dataLength());
return true;
if (self->sender_) {
self->sender_(frame.data(), frame.dataLength());
return true;
}
if (self->bus_frame_sender_) {
self->bus_frame_sender_(frame.data(), frame.dataLength());
return true;
}
return false;
}
void OamRouterRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
@@ -348,6 +471,12 @@ bool OamRouterRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
return dest == individualAddress() || dest == tunnelClientAddress() ||
(commissioning && dest == kKnxUnconfiguredBroadcastAddress);
}
if (matchesSecureSyncSerial(frame)) {
return true;
}
if (matchesRecentSecureToolAccess(frame)) {
return true;
}
if (IsBroadcastManagementRequest(frame)) {
return true;
}
@@ -365,6 +494,12 @@ bool OamRouterRuntime::shouldConsumeBusFrame(CemiFrame& frame) const {
if (IsBroadcastManagementRequest(frame)) {
return true;
}
if (matchesSecureSyncSerial(frame)) {
return true;
}
if (matchesRecentSecureToolAccess(frame)) {
return true;
}
if (frame.addressType() != IndividualAddress) {
return false;
}