Add secure transport and OAM router runtime implementations
- Implement secure transport mechanisms in `gateway_knx_secure_transport.cpp` for handling secure sessions, including AES encryption, session key generation, and secure packet wrapping and unwrapping. - Introduce `OamRouterRuntime` in `oam_router_runtime.cpp` to manage OAM router identity, individual addresses, and tunnel frame handling. - Enhance secure session management with functions for session allocation, authentication, and secure packet processing. - Ensure compatibility with existing KNXnet/IP protocols while adding support for secure communications. Signed-off-by: Tony <tonylu@tony-cloud.com>
This commit is contained in:
@@ -4,6 +4,9 @@
|
||||
#include "gateway_controller.hpp"
|
||||
#include "gateway_runtime.hpp"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include "host/ble_gap.h"
|
||||
@@ -29,6 +32,8 @@ constexpr uint16_t kChannel2Uuid = 0xFFF2;
|
||||
constexpr uint16_t kGatewayUuid = 0xFFF3;
|
||||
constexpr int64_t kGenericDedupeWindowUs = 120000;
|
||||
constexpr size_t kGatewayCharacteristicIndex = 2;
|
||||
constexpr int kGatewayNotifyAllocationAttempts = 6;
|
||||
constexpr TickType_t kGatewayNotifyRetryDelayTicks = pdMS_TO_TICKS(20);
|
||||
|
||||
gateway::GatewayBleBridge* s_active_bridge = nullptr;
|
||||
uint16_t s_value_handles[3] = {0, 0, 0};
|
||||
@@ -348,9 +353,25 @@ void GatewayBleBridge::notifyCharacteristic(size_t index, const std::vector<uint
|
||||
}
|
||||
|
||||
characteristic_values_[index] = payload;
|
||||
struct os_mbuf* buffer = ble_hs_mbuf_from_flat(payload.data(), payload.size());
|
||||
const int allocation_attempts =
|
||||
index == kGatewayCharacteristicIndex ? kGatewayNotifyAllocationAttempts : 1;
|
||||
struct os_mbuf* buffer = nullptr;
|
||||
for (int attempt = 0; attempt < allocation_attempts; ++attempt) {
|
||||
if (conn_handle_ == kInvalidConnectionHandle || !notify_enabled_[index]) {
|
||||
return;
|
||||
}
|
||||
buffer = ble_hs_mbuf_from_flat(payload.data(), payload.size());
|
||||
if (buffer != nullptr) {
|
||||
break;
|
||||
}
|
||||
if (attempt + 1 < allocation_attempts) {
|
||||
vTaskDelay(kGatewayNotifyRetryDelayTicks);
|
||||
}
|
||||
}
|
||||
if (buffer == nullptr) {
|
||||
ESP_LOGW(kTag, "failed to allocate notify mbuf idx=%u", static_cast<unsigned>(index));
|
||||
ESP_LOGW(kTag, "failed to allocate notify mbuf idx=%u attempts=%d len=%u",
|
||||
static_cast<unsigned>(index), allocation_attempts,
|
||||
static_cast<unsigned>(payload.size()));
|
||||
return;
|
||||
}
|
||||
const int rc = ble_gatts_notify_custom(conn_handle_, s_value_handles[index], buffer);
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "gateway_knx.hpp"
|
||||
|
||||
namespace gateway::openknx {
|
||||
|
||||
@@ -27,6 +30,14 @@ struct FactoryCertificatePayload {
|
||||
std::string checksum;
|
||||
};
|
||||
|
||||
struct IpSecureCredentialStatus {
|
||||
bool activated{false};
|
||||
bool backboneKeyAvailable{false};
|
||||
bool deviceAuthenticationKeyAvailable{false};
|
||||
uint8_t tunnelUserCount{0};
|
||||
uint64_t routingSequence{0};
|
||||
};
|
||||
|
||||
bool LoadFactoryFdsk(uint8_t* data, size_t len);
|
||||
FactoryFdskInfo LoadFactoryFdskInfo();
|
||||
bool GenerateFactoryFdsk(FactoryFdskInfo* info = nullptr);
|
||||
@@ -34,4 +45,21 @@ bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info = nul
|
||||
bool ResetFactoryFdskCache(FactoryFdskInfo* info = nullptr);
|
||||
FactoryCertificatePayload BuildFactoryCertificatePayload();
|
||||
|
||||
bool LoadOamFactoryFdsk(uint8_t* data, size_t len);
|
||||
FactoryFdskInfo LoadOamFactoryFdskInfo();
|
||||
bool GenerateOamFactoryFdsk(FactoryFdskInfo* info = nullptr);
|
||||
bool WriteOamFactoryFdskHex(const std::string& hex_key,
|
||||
FactoryFdskInfo* info = nullptr);
|
||||
bool ResetOamFactoryFdskCache(FactoryFdskInfo* info = nullptr);
|
||||
FactoryCertificatePayload BuildOamFactoryCertificatePayload();
|
||||
|
||||
IpSecureCredentialStatus LoadOamIpSecureCredentialStatus();
|
||||
::gateway::GatewayKnxIpSecureCredentialMaterial LoadOamIpSecureCredentialMaterial();
|
||||
bool WriteOamIpSecureKeyringHex(const std::string& backbone_key_hex,
|
||||
const std::vector<std::string>& tunnel_user_key_hex,
|
||||
const std::string& device_auth_key_hex,
|
||||
bool activated);
|
||||
bool StoreOamIpSecureRoutingSequence(uint64_t sequence);
|
||||
bool ClearOamIpSecureKeyring();
|
||||
|
||||
} // namespace gateway::openknx
|
||||
|
||||
@@ -199,6 +199,22 @@ cJSON* FactoryCertificateToCjson(const openknx::FactoryCertificatePayload& certi
|
||||
return root;
|
||||
}
|
||||
|
||||
cJSON* IpSecureCredentialStatusToCjson(
|
||||
const openknx::IpSecureCredentialStatus& status) {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (root == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
cJSON_AddBoolToObject(root, "activated", status.activated);
|
||||
cJSON_AddBoolToObject(root, "backboneKeyAvailable", status.backboneKeyAvailable);
|
||||
cJSON_AddBoolToObject(root, "deviceAuthenticationKeyAvailable",
|
||||
status.deviceAuthenticationKeyAvailable);
|
||||
cJSON_AddNumberToObject(root, "tunnelUserCount", status.tunnelUserCount);
|
||||
cJSON_AddNumberToObject(root, "routingSequence",
|
||||
static_cast<double>(status.routingSequence));
|
||||
return root;
|
||||
}
|
||||
|
||||
cJSON* SecurityFailuresToCjson() {
|
||||
cJSON* root = cJSON_CreateObject();
|
||||
if (root == nullptr) {
|
||||
@@ -1522,6 +1538,10 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
[this](uint16_t group_object_number, const uint8_t* data, size_t len) {
|
||||
return service.routeKnxGroupObjectWrite(group_object_number, data, len);
|
||||
});
|
||||
knx_router->setOamIpSecureRoutingSequenceStoreHandler([](uint64_t sequence) {
|
||||
openknx::StoreOamIpSecureRoutingSequence(sequence);
|
||||
});
|
||||
knx_router->setOamIpSecureCredentials(openknx::LoadOamIpSecureCredentialMaterial());
|
||||
if (const auto active_knx = activeKnxConfigLocked(); active_knx.has_value()) {
|
||||
knx->setConfig(active_knx.value());
|
||||
knx_router->setConfig(active_knx.value());
|
||||
@@ -2154,6 +2174,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
runtime_config.individual_address);
|
||||
knx->setConfig(runtime_config);
|
||||
knx_router->setConfig(runtime_config);
|
||||
knx_router->setOamIpSecureCredentials(openknx::LoadOamIpSecureCredentialMaterial());
|
||||
knx_router->setCommissioningOnly(commissioning_only);
|
||||
if (!runtime_config.ip_router_enabled) {
|
||||
knx_started = false;
|
||||
@@ -2218,6 +2239,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
endpoint_runtime = const_cast<GatewayBridgeService&>(service).selectKnxEndpointRuntime();
|
||||
}
|
||||
bool programming_mode = false;
|
||||
bool oam_programming_mode = false;
|
||||
bool programming_control_available = false;
|
||||
int endpoint_owner_gateway_id = -1;
|
||||
if (endpoint_runtime != nullptr) {
|
||||
@@ -2227,6 +2249,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
endpoint_runtime->knx_router->started();
|
||||
if (programming_control_available) {
|
||||
programming_mode = endpoint_runtime->knx_router->programmingMode();
|
||||
oam_programming_mode = endpoint_runtime->knx_router->oamProgrammingMode();
|
||||
}
|
||||
}
|
||||
const auto effective_knx =
|
||||
@@ -2236,6 +2259,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
cJSON_AddBoolToObject(knx_json, "started", knx_started);
|
||||
cJSON_AddBoolToObject(knx_json, "routerReady", knx_router != nullptr && knx_router->started());
|
||||
cJSON_AddBoolToObject(knx_json, "programmingMode", programming_mode);
|
||||
cJSON_AddBoolToObject(knx_json, "oamProgrammingMode", oam_programming_mode);
|
||||
cJSON_AddBoolToObject(knx_json, "programmingControlAvailable",
|
||||
programming_control_available);
|
||||
cJSON_AddBoolToObject(knx_json, "endpointOwner",
|
||||
@@ -2265,7 +2289,11 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
#else
|
||||
cJSON_AddBoolToObject(security_json, "knxnetIpSecureServicesRecognized", false);
|
||||
#endif
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", true);
|
||||
#else
|
||||
cJSON_AddBoolToObject(security_json, "knxnetIpSecureImplemented", false);
|
||||
#endif
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
|
||||
cJSON_AddBoolToObject(security_json, "developmentEndpointsEnabled", true);
|
||||
#else
|
||||
@@ -2294,6 +2322,27 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
cJSON_AddItemToObject(security_json, "failures", failures_json);
|
||||
}
|
||||
#endif
|
||||
cJSON* oam_security_json = cJSON_CreateObject();
|
||||
if (oam_security_json != nullptr) {
|
||||
cJSON* oam_fdsk_json = FactoryFdskInfoToCjson(
|
||||
openknx::LoadOamFactoryFdskInfo(), false);
|
||||
if (oam_fdsk_json != nullptr) {
|
||||
cJSON_AddItemToObject(oam_security_json, "factorySetupKey", oam_fdsk_json);
|
||||
}
|
||||
cJSON* oam_certificate_json = FactoryCertificateToCjson(
|
||||
openknx::BuildOamFactoryCertificatePayload(), false);
|
||||
if (oam_certificate_json != nullptr) {
|
||||
cJSON_AddItemToObject(oam_security_json, "factoryCertificate",
|
||||
oam_certificate_json);
|
||||
}
|
||||
cJSON* oam_credentials_json = IpSecureCredentialStatusToCjson(
|
||||
openknx::LoadOamIpSecureCredentialStatus());
|
||||
if (oam_credentials_json != nullptr) {
|
||||
cJSON_AddItemToObject(oam_security_json, "ipSecureCredentials",
|
||||
oam_credentials_json);
|
||||
}
|
||||
cJSON_AddItemToObject(security_json, "oamRouter", oam_security_json);
|
||||
}
|
||||
cJSON_AddItemToObject(knx_json, "security", security_json);
|
||||
}
|
||||
if (effective_knx.has_value()) {
|
||||
@@ -2314,6 +2363,29 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
effective_knx->ip_interface_individual_address);
|
||||
cJSON_AddNumberToObject(knx_json, "individualAddress",
|
||||
effective_knx->individual_address);
|
||||
cJSON* oam_json = ToCjson(
|
||||
GatewayKnxOamRouterConfigToValue(effective_knx->oam_router));
|
||||
if (oam_json != nullptr) {
|
||||
cJSON_AddItemToObject(knx_json, "oamRouter", oam_json);
|
||||
}
|
||||
cJSON* cloud_remote_json = cJSON_CreateObject();
|
||||
if (cloud_remote_json != nullptr) {
|
||||
const auto& cloud_remote = effective_knx->oam_router.cloud_remote;
|
||||
cJSON_AddBoolToObject(cloud_remote_json, "prepared", true);
|
||||
cJSON_AddBoolToObject(cloud_remote_json, "enabled", cloud_remote.enabled);
|
||||
cJSON_AddStringToObject(cloud_remote_json, "mode", cloud_remote.mode.c_str());
|
||||
cJSON_AddBoolToObject(cloud_remote_json, "requireSecureTunnel",
|
||||
cloud_remote.require_secure_tunnel);
|
||||
cJSON_AddBoolToObject(cloud_remote_json, "udpPunchEnabled",
|
||||
cloud_remote.udp_punch_enabled);
|
||||
cJSON_AddBoolToObject(cloud_remote_json, "relayEndpointConfigured",
|
||||
!cloud_remote.relay_endpoint.empty());
|
||||
cJSON_AddBoolToObject(cloud_remote_json, "mqttTopicPrefixConfigured",
|
||||
!cloud_remote.mqtt_topic_prefix.empty());
|
||||
cJSON_AddBoolToObject(cloud_remote_json, "authTokenRefConfigured",
|
||||
!cloud_remote.auth_token_ref.empty());
|
||||
cJSON_AddItemToObject(knx_json, "cloudKnxRemoteAccess", cloud_remote_json);
|
||||
}
|
||||
cJSON* serial_json = cJSON_CreateObject();
|
||||
if (serial_json != nullptr) {
|
||||
cJSON_AddNumberToObject(serial_json, "uartPort", effective_knx->tp_uart.uart_port);
|
||||
@@ -3220,6 +3292,46 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
}
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (config.oam_router.enabled) {
|
||||
if (config.oam_router.individual_address == 0 ||
|
||||
config.oam_router.individual_address == 0xffff) {
|
||||
if (error_message != nullptr) {
|
||||
*error_message = "OAM KNX/IP router individual address must be a configured address";
|
||||
}
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (config.oam_router.tunnel_address_base == 0 ||
|
||||
config.oam_router.tunnel_address_base == 0xffff ||
|
||||
config.oam_router.tunnel_address_base > 0xffef) {
|
||||
if (error_message != nullptr) {
|
||||
*error_message = "OAM KNX/IP router tunnel address base must leave room for 16 tunnels";
|
||||
}
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (config.oam_router.individual_address == config.individual_address ||
|
||||
config.oam_router.individual_address == config.ip_interface_individual_address ||
|
||||
config.oam_router.tunnel_address_base == config.individual_address ||
|
||||
config.oam_router.tunnel_address_base == config.ip_interface_individual_address) {
|
||||
if (error_message != nullptr) {
|
||||
*error_message = "OAM KNX/IP router addresses must differ from the shared IP interface and KNX-DALI gateway addresses";
|
||||
}
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (config.programming_button_gpio >= 0 &&
|
||||
config.programming_button_gpio == config.oam_router.programming_button_gpio) {
|
||||
if (error_message != nullptr) {
|
||||
*error_message = "OAM and KNX-DALI programming buttons must use separate GPIOs";
|
||||
}
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
if (config.programming_led_gpio >= 0 &&
|
||||
config.programming_led_gpio == config.oam_router.programming_led_gpio) {
|
||||
if (error_message != nullptr) {
|
||||
*error_message = "OAM and KNX-DALI programming LEDs must use separate GPIOs";
|
||||
}
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
}
|
||||
if (!config.ip_router_enabled || !GatewayKnxConfigUsesTpUart(config)) {
|
||||
return ESP_OK;
|
||||
}
|
||||
@@ -3360,6 +3472,7 @@ struct GatewayBridgeService::ChannelRuntime {
|
||||
}
|
||||
if (knx_router != nullptr) {
|
||||
knx_router->setConfig(merged_config);
|
||||
knx_router->setOamIpSecureCredentials(openknx::LoadOamIpSecureCredentialMaterial());
|
||||
knx_router->setCommissioningOnly(false);
|
||||
}
|
||||
if (restart_router) {
|
||||
@@ -4615,6 +4728,40 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
|
||||
}
|
||||
return handleGet("status", gateway_id.value());
|
||||
}
|
||||
if (action == "knx_oam_programming_mode") {
|
||||
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
|
||||
if (body_root == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "OAM KNX programming mode JSON is required");
|
||||
}
|
||||
const cJSON* enabled_item = cJSON_GetObjectItemCaseSensitive(body_root, "enabled");
|
||||
if (!cJSON_IsBool(enabled_item)) {
|
||||
cJSON_Delete(body_root);
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "boolean enabled field is required");
|
||||
}
|
||||
const bool enabled = cJSON_IsTrue(enabled_item);
|
||||
cJSON_Delete(body_root);
|
||||
|
||||
ChannelRuntime* owner = selectKnxEndpointRuntime();
|
||||
if (owner == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NOT_FOUND, "no KNX/IP endpoint owner is configured");
|
||||
}
|
||||
|
||||
esp_err_t err = ESP_ERR_INVALID_STATE;
|
||||
std::string detail = "KNX/IP router is unavailable";
|
||||
{
|
||||
LockGuard guard(owner->lock);
|
||||
if (owner->knx_router != nullptr) {
|
||||
err = owner->knx_router->setOamProgrammingMode(enabled);
|
||||
detail = owner->knx_router->lastError();
|
||||
}
|
||||
owner->knx_last_error = err == ESP_OK ? std::string() : detail;
|
||||
}
|
||||
if (err != ESP_OK) {
|
||||
return ErrorResponse(err, detail.empty() ? "failed to change OAM KNX programming mode"
|
||||
: detail.c_str());
|
||||
}
|
||||
return handleGet("status", gateway_id.value());
|
||||
}
|
||||
if (action == "knx_security_read_factory_key") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
|
||||
defined(CONFIG_GATEWAY_KNX_DATA_SECURE_SUPPORTED)
|
||||
@@ -4807,6 +4954,225 @@ GatewayBridgeHttpResponse GatewayBridgeService::handlePost(
|
||||
#else
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
|
||||
"KNX security development endpoints are disabled");
|
||||
#endif
|
||||
}
|
||||
if (action == "knx_oam_security_read_factory_key") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
|
||||
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
|
||||
if (body_root == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
|
||||
}
|
||||
const char* confirm = JsonString(body_root, "confirm");
|
||||
const bool confirmed = confirm != nullptr &&
|
||||
std::string_view(confirm) == "read-oam-factory-setup-key";
|
||||
cJSON_Delete(body_root);
|
||||
if (!confirmed) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG,
|
||||
"OAM factory setup key read confirmation is required");
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response");
|
||||
}
|
||||
cJSON* fdsk_json = FactoryFdskInfoToCjson(openknx::LoadOamFactoryFdskInfo(), true);
|
||||
if (fdsk_json != nullptr) {
|
||||
cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json);
|
||||
}
|
||||
return JsonOk(response);
|
||||
#else
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
|
||||
"KNX security development endpoints are disabled");
|
||||
#endif
|
||||
}
|
||||
if (action == "knx_oam_security_generate_factory_key") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
|
||||
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
|
||||
if (body_root == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
|
||||
}
|
||||
const char* confirm = JsonString(body_root, "confirm");
|
||||
const bool include_secret = JsonBool(body_root, "includeSecret", false);
|
||||
const bool confirmed = confirm != nullptr &&
|
||||
std::string_view(confirm) == "generate-oam-factory-setup-key";
|
||||
cJSON_Delete(body_root);
|
||||
if (!confirmed) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG,
|
||||
"OAM factory setup key generation confirmation is required");
|
||||
}
|
||||
openknx::FactoryFdskInfo info;
|
||||
if (!openknx::GenerateOamFactoryFdsk(&info)) {
|
||||
return ErrorResponse(ESP_FAIL, "failed to generate OAM factory setup key");
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response");
|
||||
}
|
||||
cJSON* fdsk_json = FactoryFdskInfoToCjson(info, include_secret);
|
||||
if (fdsk_json != nullptr) {
|
||||
cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json);
|
||||
}
|
||||
return JsonOk(response);
|
||||
#else
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
|
||||
"KNX security development endpoints are disabled");
|
||||
#endif
|
||||
}
|
||||
if (action == "knx_oam_security_reset_factory_key") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
|
||||
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
|
||||
if (body_root == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
|
||||
}
|
||||
const char* confirm = JsonString(body_root, "confirm");
|
||||
const bool confirmed = confirm != nullptr &&
|
||||
std::string_view(confirm) == "reset-oam-factory-setup-key";
|
||||
cJSON_Delete(body_root);
|
||||
if (!confirmed) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG,
|
||||
"OAM factory setup key reset confirmation is required");
|
||||
}
|
||||
openknx::FactoryFdskInfo info;
|
||||
if (!openknx::ResetOamFactoryFdskCache(&info)) {
|
||||
return ErrorResponse(ESP_FAIL, "failed to reload OAM factory setup key");
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response");
|
||||
}
|
||||
cJSON* fdsk_json = FactoryFdskInfoToCjson(info, false);
|
||||
if (fdsk_json != nullptr) {
|
||||
cJSON_AddItemToObject(response, "factorySetupKey", fdsk_json);
|
||||
}
|
||||
return JsonOk(response);
|
||||
#else
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
|
||||
"KNX security development endpoints are disabled");
|
||||
#endif
|
||||
}
|
||||
if (action == "knx_oam_security_export_factory_certificate") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS)
|
||||
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
|
||||
if (body_root == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
|
||||
}
|
||||
const char* confirm = JsonString(body_root, "confirm");
|
||||
const bool confirmed = confirm != nullptr &&
|
||||
std::string_view(confirm) == "export-oam-factory-certificate";
|
||||
cJSON_Delete(body_root);
|
||||
if (!confirmed) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG,
|
||||
"OAM factory certificate export confirmation is required");
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM security response");
|
||||
}
|
||||
cJSON* certificate_json = FactoryCertificateToCjson(
|
||||
openknx::BuildOamFactoryCertificatePayload(), true);
|
||||
if (certificate_json != nullptr) {
|
||||
cJSON_AddItemToObject(response, "factoryCertificate", certificate_json);
|
||||
}
|
||||
return JsonOk(response);
|
||||
#else
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
|
||||
"KNX security development endpoints are disabled");
|
||||
#endif
|
||||
}
|
||||
if (action == "knx_oam_security_upload_keyring") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
|
||||
defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
|
||||
if (body_root == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "OAM keyring JSON is required");
|
||||
}
|
||||
const char* confirm = JsonString(body_root, "confirm");
|
||||
const char* backbone_key = JsonString(body_root, "backboneKeyHex");
|
||||
const char* device_auth_key = JsonString(body_root, "deviceAuthenticationKeyHex");
|
||||
const bool activated = JsonBool(body_root, "activated", true);
|
||||
const bool confirmed = confirm != nullptr &&
|
||||
std::string_view(confirm) == "upload-oam-ip-secure-keyring";
|
||||
std::vector<std::string> tunnel_keys;
|
||||
const cJSON* tunnel_array = cJSON_GetObjectItemCaseSensitive(body_root, "tunnelUserKeysHex");
|
||||
if (tunnel_array == nullptr) {
|
||||
tunnel_array = cJSON_GetObjectItemCaseSensitive(body_root, "tunnelUserKeyHex");
|
||||
}
|
||||
if (cJSON_IsArray(tunnel_array)) {
|
||||
const cJSON* item = nullptr;
|
||||
cJSON_ArrayForEach(item, tunnel_array) {
|
||||
if (cJSON_IsString(item) && item->valuestring != nullptr) {
|
||||
tunnel_keys.emplace_back(item->valuestring);
|
||||
}
|
||||
}
|
||||
} else if (cJSON_IsString(tunnel_array) && tunnel_array->valuestring != nullptr) {
|
||||
tunnel_keys.emplace_back(tunnel_array->valuestring);
|
||||
}
|
||||
if (!confirmed || backbone_key == nullptr) {
|
||||
cJSON_Delete(body_root);
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG,
|
||||
"OAM keyring upload confirmation and backboneKeyHex are required");
|
||||
}
|
||||
const std::string backbone_key_value(backbone_key);
|
||||
const std::string device_auth_key_value(device_auth_key == nullptr ? "" : device_auth_key);
|
||||
cJSON_Delete(body_root);
|
||||
if (!openknx::WriteOamIpSecureKeyringHex(backbone_key_value, tunnel_keys,
|
||||
device_auth_key_value, activated)) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG,
|
||||
"invalid OAM IP Secure keyring material");
|
||||
}
|
||||
if (runtime->knx_router != nullptr) {
|
||||
runtime->knx_router->setOamIpSecureCredentials(
|
||||
openknx::LoadOamIpSecureCredentialMaterial());
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM keyring response");
|
||||
}
|
||||
cJSON* credentials_json = IpSecureCredentialStatusToCjson(
|
||||
openknx::LoadOamIpSecureCredentialStatus());
|
||||
if (credentials_json != nullptr) {
|
||||
cJSON_AddItemToObject(response, "ipSecureCredentials", credentials_json);
|
||||
}
|
||||
return JsonOk(response);
|
||||
#else
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
|
||||
"KNXnet/IP Secure development endpoints are disabled");
|
||||
#endif
|
||||
}
|
||||
if (action == "knx_oam_security_clear_keyring") {
|
||||
#if defined(CONFIG_GATEWAY_KNX_SECURITY_DEV_ENDPOINTS) && \
|
||||
defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
cJSON* body_root = body.empty() ? nullptr : cJSON_ParseWithLength(body.data(), body.size());
|
||||
if (body_root == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG, "confirmation JSON is required");
|
||||
}
|
||||
const char* confirm = JsonString(body_root, "confirm");
|
||||
const bool confirmed = confirm != nullptr &&
|
||||
std::string_view(confirm) == "clear-oam-ip-secure-keyring";
|
||||
cJSON_Delete(body_root);
|
||||
if (!confirmed) {
|
||||
return ErrorResponse(ESP_ERR_INVALID_ARG,
|
||||
"OAM keyring clear confirmation is required");
|
||||
}
|
||||
if (!openknx::ClearOamIpSecureKeyring()) {
|
||||
return ErrorResponse(ESP_FAIL, "failed to clear OAM IP Secure keyring");
|
||||
}
|
||||
if (runtime->knx_router != nullptr) {
|
||||
runtime->knx_router->setOamIpSecureCredentials(
|
||||
openknx::LoadOamIpSecureCredentialMaterial());
|
||||
}
|
||||
cJSON* response = cJSON_CreateObject();
|
||||
if (response == nullptr) {
|
||||
return ErrorResponse(ESP_ERR_NO_MEM, "failed to allocate OAM keyring response");
|
||||
}
|
||||
cJSON* credentials_json = IpSecureCredentialStatusToCjson(
|
||||
openknx::LoadOamIpSecureCredentialStatus());
|
||||
if (credentials_json != nullptr) {
|
||||
cJSON_AddItemToObject(response, "ipSecureCredentials", credentials_json);
|
||||
}
|
||||
return JsonOk(response);
|
||||
#else
|
||||
return ErrorResponse(ESP_ERR_NOT_SUPPORTED,
|
||||
"KNXnet/IP Secure development endpoints are disabled");
|
||||
#endif
|
||||
}
|
||||
if (action == "bacnet_start") {
|
||||
|
||||
@@ -22,13 +22,21 @@ namespace {
|
||||
|
||||
constexpr const char* kTag = "openknx_sec";
|
||||
constexpr const char* kNamespace = "knx_sec";
|
||||
constexpr const char* kOamNamespace = "knx_oam_sec";
|
||||
constexpr const char* kFactoryFdskKey = "factory_fdsk";
|
||||
constexpr const char* kIpSecureBackboneKey = "ipsec_backbone";
|
||||
constexpr const char* kIpSecureDeviceAuthKey = "ipsec_dev_auth";
|
||||
constexpr const char* kIpSecureTunnelCountKey = "ipsec_tunnels";
|
||||
constexpr const char* kIpSecureActivatedKey = "ipsec_active";
|
||||
constexpr const char* kIpSecureRoutingSeqKey = "ipsec_route_seq";
|
||||
constexpr size_t kFdskSize = 16;
|
||||
constexpr size_t kSerialSize = 6;
|
||||
constexpr size_t kFdskQrSize = 36;
|
||||
constexpr const char* kProductIdentity = "REG1-Dali";
|
||||
constexpr const char* kOamProductIdentity = "OpenKNX-IP-Router";
|
||||
constexpr const char* kDevelopmentStorage = "base_mac_derived_plain_nvs_development";
|
||||
constexpr char kFdskDerivationLabel[] = "DaliMaster REG1-Dali deterministic FDSK v1";
|
||||
constexpr char kOamFdskDerivationLabel[] = "DaliMaster OAM-IP-Router deterministic FDSK v1";
|
||||
constexpr uint8_t kCrc4Tab[16] = {
|
||||
0x0, 0x3, 0x6, 0x5, 0xc, 0xf, 0xa, 0x9,
|
||||
0xb, 0x8, 0xd, 0xe, 0x7, 0x4, 0x1, 0x2,
|
||||
@@ -36,6 +44,37 @@ constexpr uint8_t kCrc4Tab[16] = {
|
||||
constexpr char kBase32Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
constexpr char kHexAlphabet[] = "0123456789ABCDEF";
|
||||
|
||||
struct FactoryFdskContext {
|
||||
const char* nvsNamespace;
|
||||
const char* productIdentity;
|
||||
const char* derivationLabel;
|
||||
uint16_t manufacturerId;
|
||||
uint16_t applicationNumber;
|
||||
uint8_t applicationVersion;
|
||||
uint32_t serialMacIncrement;
|
||||
bool clearOpenKnxCache;
|
||||
};
|
||||
|
||||
constexpr FactoryFdskContext kReg1Context{
|
||||
kNamespace,
|
||||
kProductIdentity,
|
||||
kFdskDerivationLabel,
|
||||
gateway::knx_internal::kReg1DaliManufacturerId,
|
||||
gateway::knx_internal::kReg1DaliApplicationNumber,
|
||||
gateway::knx_internal::kReg1DaliApplicationVersion,
|
||||
gateway::knx_internal::kReg1DaliSerialMacIncrement,
|
||||
true};
|
||||
|
||||
constexpr FactoryFdskContext kOamContext{
|
||||
kOamNamespace,
|
||||
kOamProductIdentity,
|
||||
kOamFdskDerivationLabel,
|
||||
gateway::knx_internal::kOamRouterManufacturerId,
|
||||
gateway::knx_internal::kOamRouterApplicationNumber,
|
||||
gateway::knx_internal::kOamRouterApplicationVersion,
|
||||
gateway::knx_internal::kOamRouterSerialMacIncrement,
|
||||
false};
|
||||
|
||||
extern "C" void knx_platform_clear_cached_fdsk() __attribute__((weak));
|
||||
|
||||
std::string hexValue(uint32_t value, int width) {
|
||||
@@ -120,7 +159,7 @@ bool parseHexKey(const std::string& value, uint8_t* out) {
|
||||
return plausibleKey(out);
|
||||
}
|
||||
|
||||
bool loadKnxSerialNumber(uint8_t* serial) {
|
||||
bool loadKnxSerialNumber(const FactoryFdskContext& context, uint8_t* serial) {
|
||||
if (serial == nullptr) {
|
||||
return false;
|
||||
}
|
||||
@@ -129,22 +168,30 @@ bool loadKnxSerialNumber(uint8_t* serial) {
|
||||
return false;
|
||||
}
|
||||
|
||||
serial[0] = static_cast<uint8_t>(
|
||||
(gateway::knx_internal::kReg1DaliManufacturerId >> 8) & 0xff);
|
||||
serial[1] = static_cast<uint8_t>(
|
||||
gateway::knx_internal::kReg1DaliManufacturerId & 0xff);
|
||||
std::copy(mac.begin() + 2, mac.end(), serial + 2);
|
||||
uint32_t suffix = (static_cast<uint32_t>(mac[2]) << 24) |
|
||||
(static_cast<uint32_t>(mac[3]) << 16) |
|
||||
(static_cast<uint32_t>(mac[4]) << 8) |
|
||||
static_cast<uint32_t>(mac[5]);
|
||||
suffix += context.serialMacIncrement;
|
||||
|
||||
serial[0] = static_cast<uint8_t>((context.manufacturerId >> 8) & 0xff);
|
||||
serial[1] = static_cast<uint8_t>(context.manufacturerId & 0xff);
|
||||
serial[2] = static_cast<uint8_t>((suffix >> 24) & 0xff);
|
||||
serial[3] = static_cast<uint8_t>((suffix >> 16) & 0xff);
|
||||
serial[4] = static_cast<uint8_t>((suffix >> 8) & 0xff);
|
||||
serial[5] = static_cast<uint8_t>(suffix & 0xff);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) {
|
||||
bool deriveFactoryFdskFromSerial(const FactoryFdskContext& context,
|
||||
const uint8_t* serial, uint8_t* key) {
|
||||
if (serial == nullptr || key == nullptr) {
|
||||
return false;
|
||||
}
|
||||
std::array<uint8_t, sizeof(kFdskDerivationLabel) - 1 + kSerialSize> material{};
|
||||
std::copy(kFdskDerivationLabel, kFdskDerivationLabel + sizeof(kFdskDerivationLabel) - 1,
|
||||
material.begin());
|
||||
std::copy(serial, serial + kSerialSize, material.begin() + sizeof(kFdskDerivationLabel) - 1);
|
||||
const size_t label_len = std::strlen(context.derivationLabel);
|
||||
std::vector<uint8_t> material(label_len + kSerialSize);
|
||||
std::copy(context.derivationLabel, context.derivationLabel + label_len, material.begin());
|
||||
std::copy(serial, serial + kSerialSize, material.begin() + label_len);
|
||||
|
||||
std::array<uint8_t, 32> digest{};
|
||||
if (mbedtls_sha256(material.data(), material.size(), digest.data(), 0) != 0) {
|
||||
@@ -157,7 +204,7 @@ bool deriveFactoryFdskFromSerial(const uint8_t* serial, uint8_t* key) {
|
||||
return plausibleKey(key);
|
||||
}
|
||||
|
||||
void syncFactoryFdskToNvs(const uint8_t* data) {
|
||||
void syncFactoryFdskToNvs(const FactoryFdskContext& context, const uint8_t* data) {
|
||||
if (data == nullptr || !plausibleKey(data) || !ensureNvsReady()) {
|
||||
return;
|
||||
}
|
||||
@@ -166,7 +213,7 @@ void syncFactoryFdskToNvs(const uint8_t* data) {
|
||||
size_t stored_size = stored.size();
|
||||
|
||||
nvs_handle_t handle = 0;
|
||||
esp_err_t err = nvs_open(kNamespace, NVS_READWRITE, &handle);
|
||||
esp_err_t err = nvs_open(context.nvsNamespace, NVS_READWRITE, &handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(kTag, "failed to open KNX security NVS namespace: %s", esp_err_to_name(err));
|
||||
return;
|
||||
@@ -186,7 +233,9 @@ void syncFactoryFdskToNvs(const uint8_t* data) {
|
||||
ESP_LOGW(kTag, "failed to mirror deterministic KNX factory FDSK: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
clearOpenKnxFdskCache();
|
||||
if (context.clearOpenKnxCache) {
|
||||
clearOpenKnxFdskCache();
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t crc4Array(const uint8_t* data, size_t len) {
|
||||
@@ -275,27 +324,28 @@ std::string fnv1aHex(const std::string& value) {
|
||||
|
||||
namespace gateway::openknx {
|
||||
|
||||
bool LoadFactoryFdsk(uint8_t* data, size_t len) {
|
||||
bool LoadFactoryFdskForContext(const FactoryFdskContext& context, uint8_t* data, size_t len) {
|
||||
if (data == nullptr || len < kFdskSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<uint8_t, kSerialSize> serial{};
|
||||
std::array<uint8_t, kFdskSize> key{};
|
||||
if (!loadKnxSerialNumber(serial.data()) ||
|
||||
!deriveFactoryFdskFromSerial(serial.data(), key.data())) {
|
||||
if (!loadKnxSerialNumber(context, serial.data()) ||
|
||||
!deriveFactoryFdskFromSerial(context, serial.data(), key.data())) {
|
||||
return false;
|
||||
}
|
||||
std::memcpy(data, key.data(), kFdskSize);
|
||||
syncFactoryFdskToNvs(key.data());
|
||||
syncFactoryFdskToNvs(context, key.data());
|
||||
return true;
|
||||
}
|
||||
|
||||
FactoryFdskInfo LoadFactoryFdskInfo() {
|
||||
FactoryFdskInfo LoadFactoryFdskInfoForContext(const FactoryFdskContext& context) {
|
||||
FactoryFdskInfo info;
|
||||
std::array<uint8_t, kFdskSize> key{};
|
||||
std::array<uint8_t, kSerialSize> serial{};
|
||||
if (!loadKnxSerialNumber(serial.data()) || !LoadFactoryFdsk(key.data(), key.size())) {
|
||||
if (!loadKnxSerialNumber(context, serial.data()) ||
|
||||
!LoadFactoryFdskForContext(context, key.data(), key.size())) {
|
||||
return info;
|
||||
}
|
||||
|
||||
@@ -306,31 +356,34 @@ FactoryFdskInfo LoadFactoryFdskInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
bool GenerateFactoryFdsk(FactoryFdskInfo* info) {
|
||||
bool GenerateFactoryFdskForContext(const FactoryFdskContext& context,
|
||||
FactoryFdskInfo* info) {
|
||||
std::array<uint8_t, kFdskSize> key{};
|
||||
const bool stored = LoadFactoryFdsk(key.data(), key.size());
|
||||
const bool stored = LoadFactoryFdskForContext(context, key.data(), key.size());
|
||||
std::fill(key.begin(), key.end(), 0);
|
||||
if (!stored) {
|
||||
return false;
|
||||
}
|
||||
if (info != nullptr) {
|
||||
*info = LoadFactoryFdskInfo();
|
||||
*info = LoadFactoryFdskInfoForContext(context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
|
||||
bool WriteFactoryFdskHexForContext(const FactoryFdskContext& context,
|
||||
const std::string& hex_key,
|
||||
FactoryFdskInfo* info) {
|
||||
std::array<uint8_t, kFdskSize> key{};
|
||||
if (!parseHexKey(hex_key, key.data())) {
|
||||
return false;
|
||||
}
|
||||
std::array<uint8_t, kSerialSize> serial{};
|
||||
std::array<uint8_t, kFdskSize> derived{};
|
||||
const bool stored = loadKnxSerialNumber(serial.data()) &&
|
||||
deriveFactoryFdskFromSerial(serial.data(), derived.data()) &&
|
||||
const bool stored = loadKnxSerialNumber(context, serial.data()) &&
|
||||
deriveFactoryFdskFromSerial(context, serial.data(), derived.data()) &&
|
||||
std::equal(key.begin(), key.end(), derived.begin());
|
||||
if (stored) {
|
||||
syncFactoryFdskToNvs(derived.data());
|
||||
syncFactoryFdskToNvs(context, derived.data());
|
||||
}
|
||||
std::fill(key.begin(), key.end(), 0);
|
||||
std::fill(derived.begin(), derived.end(), 0);
|
||||
@@ -338,33 +391,36 @@ bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
|
||||
return false;
|
||||
}
|
||||
if (info != nullptr) {
|
||||
*info = LoadFactoryFdskInfo();
|
||||
*info = LoadFactoryFdskInfoForContext(context);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResetFactoryFdskCache(FactoryFdskInfo* info) {
|
||||
clearOpenKnxFdskCache();
|
||||
const auto loaded = LoadFactoryFdskInfo();
|
||||
bool ResetFactoryFdskCacheForContext(const FactoryFdskContext& context,
|
||||
FactoryFdskInfo* info) {
|
||||
if (context.clearOpenKnxCache) {
|
||||
clearOpenKnxFdskCache();
|
||||
}
|
||||
const auto loaded = LoadFactoryFdskInfoForContext(context);
|
||||
if (info != nullptr) {
|
||||
*info = loaded;
|
||||
}
|
||||
return loaded.available;
|
||||
}
|
||||
|
||||
FactoryCertificatePayload BuildFactoryCertificatePayload() {
|
||||
FactoryCertificatePayload BuildFactoryCertificatePayloadForContext(
|
||||
const FactoryFdskContext& context) {
|
||||
FactoryCertificatePayload payload;
|
||||
const auto info = LoadFactoryFdskInfo();
|
||||
const auto info = LoadFactoryFdskInfoForContext(context);
|
||||
if (!info.available) {
|
||||
return payload;
|
||||
}
|
||||
payload.available = true;
|
||||
payload.productIdentity = kProductIdentity;
|
||||
payload.manufacturerId = hexValue(gateway::knx_internal::kReg1DaliManufacturerId, 4);
|
||||
payload.applicationNumber = hexValue(
|
||||
gateway::knx_internal::kReg1DaliApplicationNumber, 2);
|
||||
payload.applicationVersion = hexValue(
|
||||
gateway::knx_internal::kReg1DaliApplicationVersion, 2);
|
||||
payload.productIdentity = context.productIdentity;
|
||||
payload.manufacturerId = hexValue(context.manufacturerId, 4);
|
||||
payload.applicationNumber = hexValue(context.applicationNumber,
|
||||
context.applicationNumber <= 0xff ? 2 : 4);
|
||||
payload.applicationVersion = hexValue(context.applicationVersion, 2);
|
||||
payload.serialNumber = info.serialNumber;
|
||||
payload.fdskLabel = info.label;
|
||||
payload.fdskQrCode = info.qrCode;
|
||||
@@ -377,6 +433,241 @@ FactoryCertificatePayload BuildFactoryCertificatePayload() {
|
||||
return payload;
|
||||
}
|
||||
|
||||
std::string TunnelUserKeyName(uint8_t index) {
|
||||
std::array<char, 18> buffer{};
|
||||
std::snprintf(buffer.data(), buffer.size(), "ipsec_tunnel_%02u",
|
||||
static_cast<unsigned>(index));
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
bool BlobKeyAvailable(nvs_handle_t handle, const char* key) {
|
||||
std::array<uint8_t, kFdskSize> data{};
|
||||
size_t len = data.size();
|
||||
return nvs_get_blob(handle, key, data.data(), &len) == ESP_OK && len == data.size() &&
|
||||
plausibleKey(data.data());
|
||||
}
|
||||
|
||||
bool SetOptionalHexBlob(nvs_handle_t handle, const char* key, const std::string& hex) {
|
||||
if (hex.empty()) {
|
||||
nvs_erase_key(handle, key);
|
||||
return true;
|
||||
}
|
||||
std::array<uint8_t, kFdskSize> data{};
|
||||
if (!parseHexKey(hex, data.data())) {
|
||||
return false;
|
||||
}
|
||||
const esp_err_t err = nvs_set_blob(handle, key, data.data(), data.size());
|
||||
std::fill(data.begin(), data.end(), 0);
|
||||
return err == ESP_OK;
|
||||
}
|
||||
|
||||
bool LoadOptionalKey(nvs_handle_t handle, const char* key, std::array<uint8_t, kFdskSize>* out) {
|
||||
if (out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
std::array<uint8_t, kFdskSize> data{};
|
||||
size_t len = data.size();
|
||||
if (nvs_get_blob(handle, key, data.data(), &len) != ESP_OK || len != data.size() ||
|
||||
!plausibleKey(data.data())) {
|
||||
return false;
|
||||
}
|
||||
*out = data;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LoadFactoryFdsk(uint8_t* data, size_t len) {
|
||||
return LoadFactoryFdskForContext(kReg1Context, data, len);
|
||||
}
|
||||
|
||||
FactoryFdskInfo LoadFactoryFdskInfo() {
|
||||
return LoadFactoryFdskInfoForContext(kReg1Context);
|
||||
}
|
||||
|
||||
bool GenerateFactoryFdsk(FactoryFdskInfo* info) {
|
||||
return GenerateFactoryFdskForContext(kReg1Context, info);
|
||||
}
|
||||
|
||||
bool WriteFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
|
||||
return WriteFactoryFdskHexForContext(kReg1Context, hex_key, info);
|
||||
}
|
||||
|
||||
bool ResetFactoryFdskCache(FactoryFdskInfo* info) {
|
||||
return ResetFactoryFdskCacheForContext(kReg1Context, info);
|
||||
}
|
||||
|
||||
FactoryCertificatePayload BuildFactoryCertificatePayload() {
|
||||
return BuildFactoryCertificatePayloadForContext(kReg1Context);
|
||||
}
|
||||
|
||||
bool LoadOamFactoryFdsk(uint8_t* data, size_t len) {
|
||||
return LoadFactoryFdskForContext(kOamContext, data, len);
|
||||
}
|
||||
|
||||
FactoryFdskInfo LoadOamFactoryFdskInfo() {
|
||||
return LoadFactoryFdskInfoForContext(kOamContext);
|
||||
}
|
||||
|
||||
bool GenerateOamFactoryFdsk(FactoryFdskInfo* info) {
|
||||
return GenerateFactoryFdskForContext(kOamContext, info);
|
||||
}
|
||||
|
||||
bool WriteOamFactoryFdskHex(const std::string& hex_key, FactoryFdskInfo* info) {
|
||||
return WriteFactoryFdskHexForContext(kOamContext, hex_key, info);
|
||||
}
|
||||
|
||||
bool ResetOamFactoryFdskCache(FactoryFdskInfo* info) {
|
||||
return ResetFactoryFdskCacheForContext(kOamContext, info);
|
||||
}
|
||||
|
||||
FactoryCertificatePayload BuildOamFactoryCertificatePayload() {
|
||||
return BuildFactoryCertificatePayloadForContext(kOamContext);
|
||||
}
|
||||
|
||||
IpSecureCredentialStatus LoadOamIpSecureCredentialStatus() {
|
||||
IpSecureCredentialStatus status;
|
||||
if (!ensureNvsReady()) {
|
||||
return status;
|
||||
}
|
||||
nvs_handle_t handle = 0;
|
||||
if (nvs_open(kOamNamespace, NVS_READONLY, &handle) != ESP_OK) {
|
||||
return status;
|
||||
}
|
||||
uint8_t activated = 0;
|
||||
if (nvs_get_u8(handle, kIpSecureActivatedKey, &activated) == ESP_OK) {
|
||||
status.activated = activated != 0;
|
||||
}
|
||||
uint8_t tunnel_count = 0;
|
||||
if (nvs_get_u8(handle, kIpSecureTunnelCountKey, &tunnel_count) == ESP_OK) {
|
||||
status.tunnelUserCount = tunnel_count;
|
||||
}
|
||||
uint64_t routing_sequence = 0;
|
||||
if (nvs_get_u64(handle, kIpSecureRoutingSeqKey, &routing_sequence) == ESP_OK) {
|
||||
status.routingSequence = routing_sequence;
|
||||
}
|
||||
status.backboneKeyAvailable = BlobKeyAvailable(handle, kIpSecureBackboneKey);
|
||||
status.deviceAuthenticationKeyAvailable = BlobKeyAvailable(handle, kIpSecureDeviceAuthKey);
|
||||
nvs_close(handle);
|
||||
return status;
|
||||
}
|
||||
|
||||
::gateway::GatewayKnxIpSecureCredentialMaterial LoadOamIpSecureCredentialMaterial() {
|
||||
::gateway::GatewayKnxIpSecureCredentialMaterial material;
|
||||
if (!ensureNvsReady()) {
|
||||
return material;
|
||||
}
|
||||
nvs_handle_t handle = 0;
|
||||
if (nvs_open(kOamNamespace, NVS_READONLY, &handle) != ESP_OK) {
|
||||
return material;
|
||||
}
|
||||
uint8_t activated = 0;
|
||||
if (nvs_get_u8(handle, kIpSecureActivatedKey, &activated) == ESP_OK) {
|
||||
material.activated = activated != 0;
|
||||
}
|
||||
material.backbone_key_available = LoadOptionalKey(handle, kIpSecureBackboneKey,
|
||||
&material.backbone_key);
|
||||
material.device_authentication_key_available =
|
||||
LoadOptionalKey(handle, kIpSecureDeviceAuthKey,
|
||||
&material.device_authentication_key);
|
||||
uint8_t tunnel_count = 0;
|
||||
if (nvs_get_u8(handle, kIpSecureTunnelCountKey, &tunnel_count) == ESP_OK) {
|
||||
tunnel_count = std::min<uint8_t>(tunnel_count, 16);
|
||||
material.tunnel_user_keys.reserve(tunnel_count);
|
||||
for (uint8_t index = 0; index < tunnel_count; ++index) {
|
||||
std::array<uint8_t, kFdskSize> key{};
|
||||
const std::string key_name = TunnelUserKeyName(index);
|
||||
if (LoadOptionalKey(handle, key_name.c_str(), &key)) {
|
||||
material.tunnel_user_keys.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
uint64_t routing_sequence = 0;
|
||||
if (nvs_get_u64(handle, kIpSecureRoutingSeqKey, &routing_sequence) == ESP_OK) {
|
||||
material.routing_sequence = routing_sequence;
|
||||
}
|
||||
nvs_close(handle);
|
||||
return material;
|
||||
}
|
||||
|
||||
bool WriteOamIpSecureKeyringHex(const std::string& backbone_key_hex,
|
||||
const std::vector<std::string>& tunnel_user_key_hex,
|
||||
const std::string& device_auth_key_hex,
|
||||
bool activated) {
|
||||
if (tunnel_user_key_hex.size() > 16 || !ensureNvsReady()) {
|
||||
return false;
|
||||
}
|
||||
nvs_handle_t handle = 0;
|
||||
if (nvs_open(kOamNamespace, NVS_READWRITE, &handle) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
bool ok = SetOptionalHexBlob(handle, kIpSecureBackboneKey, backbone_key_hex) &&
|
||||
SetOptionalHexBlob(handle, kIpSecureDeviceAuthKey, device_auth_key_hex);
|
||||
for (uint8_t index = 0; index < 16; ++index) {
|
||||
const std::string key = TunnelUserKeyName(index);
|
||||
nvs_erase_key(handle, key.c_str());
|
||||
}
|
||||
if (ok) {
|
||||
for (size_t index = 0; index < tunnel_user_key_hex.size(); ++index) {
|
||||
const std::string key = TunnelUserKeyName(static_cast<uint8_t>(index));
|
||||
ok = SetOptionalHexBlob(handle, key.c_str(), tunnel_user_key_hex[index]);
|
||||
if (!ok) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
ok = nvs_set_u8(handle, kIpSecureTunnelCountKey,
|
||||
static_cast<uint8_t>(tunnel_user_key_hex.size())) == ESP_OK &&
|
||||
nvs_set_u8(handle, kIpSecureActivatedKey, activated ? 1 : 0) == ESP_OK;
|
||||
}
|
||||
if (ok) {
|
||||
uint64_t existing_sequence = 0;
|
||||
if (nvs_get_u64(handle, kIpSecureRoutingSeqKey, &existing_sequence) != ESP_OK) {
|
||||
ok = nvs_set_u64(handle, kIpSecureRoutingSeqKey, 0) == ESP_OK;
|
||||
}
|
||||
}
|
||||
if (ok) {
|
||||
ok = nvs_commit(handle) == ESP_OK;
|
||||
}
|
||||
nvs_close(handle);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool StoreOamIpSecureRoutingSequence(uint64_t sequence) {
|
||||
if (!ensureNvsReady()) {
|
||||
return false;
|
||||
}
|
||||
nvs_handle_t handle = 0;
|
||||
if (nvs_open(kOamNamespace, NVS_READWRITE, &handle) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
const bool ok = nvs_set_u64(handle, kIpSecureRoutingSeqKey, sequence) == ESP_OK &&
|
||||
nvs_commit(handle) == ESP_OK;
|
||||
nvs_close(handle);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool ClearOamIpSecureKeyring() {
|
||||
if (!ensureNvsReady()) {
|
||||
return false;
|
||||
}
|
||||
nvs_handle_t handle = 0;
|
||||
if (nvs_open(kOamNamespace, NVS_READWRITE, &handle) != ESP_OK) {
|
||||
return false;
|
||||
}
|
||||
nvs_erase_key(handle, kIpSecureBackboneKey);
|
||||
nvs_erase_key(handle, kIpSecureDeviceAuthKey);
|
||||
nvs_erase_key(handle, kIpSecureTunnelCountKey);
|
||||
nvs_erase_key(handle, kIpSecureActivatedKey);
|
||||
nvs_erase_key(handle, kIpSecureRoutingSeqKey);
|
||||
for (uint8_t index = 0; index < 16; ++index) {
|
||||
const std::string key = TunnelUserKeyName(index);
|
||||
nvs_erase_key(handle, key.c_str());
|
||||
}
|
||||
const bool ok = nvs_commit(handle) == ESP_OK;
|
||||
nvs_close(handle);
|
||||
return ok;
|
||||
}
|
||||
|
||||
} // namespace gateway::openknx
|
||||
|
||||
extern "C" bool knx_platform_get_fdsk(uint8_t* data, size_t len) {
|
||||
|
||||
@@ -6,10 +6,12 @@ idf_component_register(
|
||||
"src/gateway_knx_router_openknx.cpp"
|
||||
"src/gateway_knx_router_packets.cpp"
|
||||
"src/gateway_knx_router_services.cpp"
|
||||
"src/gateway_knx_secure_transport.cpp"
|
||||
"src/oam_router_runtime.cpp"
|
||||
"src/ets_device_runtime.cpp"
|
||||
"src/ets_memory_loader.cpp"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip knx
|
||||
REQUIRES dali_cpp esp_driver_gpio esp_driver_uart esp_hw_support esp_netif freertos log lwip knx mbedtls
|
||||
)
|
||||
|
||||
set_property(TARGET ${COMPONENT_LIB} PROPERTY CXX_STANDARD 17)
|
||||
@@ -27,6 +27,7 @@ namespace gateway {
|
||||
|
||||
namespace openknx {
|
||||
class EtsDeviceRuntime;
|
||||
class OamRouterRuntime;
|
||||
class TpuartUartInterface;
|
||||
}
|
||||
|
||||
@@ -59,6 +60,40 @@ struct GatewayKnxEtsAssociation {
|
||||
uint16_t group_object_number{0};
|
||||
};
|
||||
|
||||
struct GatewayKnxCloudRemoteConfig {
|
||||
bool enabled{false};
|
||||
std::string mode{"mqtt"};
|
||||
std::string relay_endpoint;
|
||||
std::string mqtt_topic_prefix;
|
||||
std::string auth_token_ref;
|
||||
bool require_secure_tunnel{true};
|
||||
bool udp_punch_enabled{false};
|
||||
};
|
||||
|
||||
struct GatewayKnxOamRouterConfig {
|
||||
bool enabled{false};
|
||||
bool ets_database_enabled{true};
|
||||
bool secure_tunnel_enabled{true};
|
||||
bool secure_routing_enabled{true};
|
||||
uint16_t individual_address{0xff02};
|
||||
uint16_t tunnel_address_base{0xff10};
|
||||
int programming_button_gpio{-1};
|
||||
bool programming_button_active_low{true};
|
||||
int programming_led_gpio{-1};
|
||||
bool programming_led_active_high{true};
|
||||
GatewayKnxCloudRemoteConfig cloud_remote;
|
||||
};
|
||||
|
||||
struct GatewayKnxIpSecureCredentialMaterial {
|
||||
bool activated{false};
|
||||
bool backbone_key_available{false};
|
||||
std::array<uint8_t, 16> backbone_key{};
|
||||
bool device_authentication_key_available{false};
|
||||
std::array<uint8_t, 16> device_authentication_key{};
|
||||
std::vector<std::array<uint8_t, 16>> tunnel_user_keys;
|
||||
uint64_t routing_sequence{0};
|
||||
};
|
||||
|
||||
struct GatewayKnxConfig {
|
||||
bool dali_router_enabled{true};
|
||||
bool ip_router_enabled{false};
|
||||
@@ -76,6 +111,7 @@ struct GatewayKnxConfig {
|
||||
bool programming_button_active_low{true};
|
||||
int programming_led_gpio{-1};
|
||||
bool programming_led_active_high{true};
|
||||
GatewayKnxOamRouterConfig oam_router;
|
||||
std::vector<GatewayKnxEtsAssociation> ets_associations;
|
||||
GatewayKnxTpUartConfig tp_uart;
|
||||
};
|
||||
@@ -134,6 +170,9 @@ struct GatewayKnxReg1ScanOptions {
|
||||
|
||||
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value);
|
||||
DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config);
|
||||
std::optional<GatewayKnxOamRouterConfig> GatewayKnxOamRouterConfigFromValue(
|
||||
const DaliValue* value);
|
||||
DaliValue GatewayKnxOamRouterConfigToValue(const GatewayKnxOamRouterConfig& config);
|
||||
bool GatewayKnxConfigUsesTpUart(const GatewayKnxConfig& config);
|
||||
|
||||
const char* GatewayKnxMappingModeToString(GatewayKnxMappingMode mode);
|
||||
@@ -228,6 +267,7 @@ class GatewayKnxTpIpRouter {
|
||||
using GroupObjectWriteHandler = std::function<DaliBridgeResult(uint16_t group_object_number,
|
||||
const uint8_t* data,
|
||||
size_t len)>;
|
||||
using RoutingSequenceStoreHandler = std::function<void(uint64_t sequence)>;
|
||||
|
||||
GatewayKnxTpIpRouter(GatewayKnxBridge& bridge,
|
||||
std::string openknx_namespace = "openknx");
|
||||
@@ -237,11 +277,16 @@ class GatewayKnxTpIpRouter {
|
||||
void setCommissioningOnly(bool enabled);
|
||||
void setGroupWriteHandler(GroupWriteHandler handler);
|
||||
void setGroupObjectWriteHandler(GroupObjectWriteHandler handler);
|
||||
void setOamIpSecureCredentials(const GatewayKnxIpSecureCredentialMaterial& credentials);
|
||||
void setOamIpSecureRoutingSequenceStoreHandler(RoutingSequenceStoreHandler handler);
|
||||
const GatewayKnxConfig& config() const;
|
||||
bool tpUartOnline() const;
|
||||
bool programmingMode();
|
||||
esp_err_t setProgrammingMode(bool enabled);
|
||||
esp_err_t toggleProgrammingMode();
|
||||
bool oamProgrammingMode();
|
||||
esp_err_t setOamProgrammingMode(bool enabled);
|
||||
esp_err_t toggleOamProgrammingMode();
|
||||
|
||||
esp_err_t start(uint32_t task_stack_size, UBaseType_t task_priority);
|
||||
esp_err_t stop();
|
||||
@@ -285,6 +330,24 @@ class GatewayKnxTpIpRouter {
|
||||
::sockaddr_in data_remote{};
|
||||
std::vector<uint8_t> last_received_cemi;
|
||||
std::vector<uint8_t> last_tunnel_confirmation_packet;
|
||||
uint16_t secure_session_id{0};
|
||||
bool oam_router_persona{false};
|
||||
};
|
||||
|
||||
struct SecureSession {
|
||||
bool active{false};
|
||||
bool authenticated{false};
|
||||
uint16_t session_id{0};
|
||||
int tcp_sock{-1};
|
||||
::sockaddr_in remote{};
|
||||
std::array<uint8_t, 32> client_public_key{};
|
||||
std::array<uint8_t, 32> server_public_key{};
|
||||
std::array<uint8_t, 32> shared_secret{};
|
||||
std::array<uint8_t, 16> session_key{};
|
||||
uint64_t send_sequence{0};
|
||||
uint64_t receive_sequence{0};
|
||||
uint8_t user_id{0};
|
||||
TickType_t last_activity_tick{0};
|
||||
};
|
||||
|
||||
static void TaskEntry(void* arg);
|
||||
@@ -297,6 +360,7 @@ class GatewayKnxTpIpRouter {
|
||||
void handleTcpAccept();
|
||||
void handleTcpClient(TcpClient& client);
|
||||
void closeTcpClient(TcpClient& client);
|
||||
void closeSecureSessionsForTcp(int sock);
|
||||
std::unique_ptr<openknx::TpuartUartInterface> createOpenKnxTpUartInterface();
|
||||
bool configureTpUart();
|
||||
bool configureProgrammingGpio();
|
||||
@@ -316,6 +380,12 @@ class GatewayKnxTpIpRouter {
|
||||
void handleDisconnectRequest(const uint8_t* packet_data, size_t len, const ::sockaddr_in& remote);
|
||||
void handleSecureService(uint16_t service, const uint8_t* body, size_t len,
|
||||
const ::sockaddr_in& remote);
|
||||
void handleSecureSessionRequest(const uint8_t* body, size_t len,
|
||||
const ::sockaddr_in& remote);
|
||||
void handleSecureWrapper(const uint8_t* body, size_t len,
|
||||
const ::sockaddr_in& remote);
|
||||
void handleSecureGroupSync(const uint8_t* body, size_t len,
|
||||
const ::sockaddr_in& remote);
|
||||
void sendTunnellingAck(uint8_t channel_id, uint8_t sequence, uint8_t status,
|
||||
const ::sockaddr_in& remote);
|
||||
void sendDeviceConfigurationAck(uint8_t channel_id, uint8_t sequence, uint8_t status,
|
||||
@@ -335,9 +405,9 @@ class GatewayKnxTpIpRouter {
|
||||
const ::sockaddr_in& remote, uint8_t connection_type,
|
||||
uint16_t tunnel_address);
|
||||
void sendRoutingIndication(const uint8_t* data, size_t len);
|
||||
bool sendPacket(const std::vector<uint8_t>& packet, const ::sockaddr_in& remote) const;
|
||||
bool sendPacket(const std::vector<uint8_t>& packet, const ::sockaddr_in& remote);
|
||||
bool sendPacketToTunnelClient(const TunnelClient& client,
|
||||
const std::vector<uint8_t>& packet) const;
|
||||
const std::vector<uint8_t>& packet);
|
||||
bool currentTransportAllowsTcpHpai() const;
|
||||
std::optional<std::array<uint8_t, 8>> localHpaiForRemote(const ::sockaddr_in& remote,
|
||||
bool tcp = false) const;
|
||||
@@ -359,6 +429,22 @@ class GatewayKnxTpIpRouter {
|
||||
const ::sockaddr_in& data_remote,
|
||||
uint8_t connection_type);
|
||||
void resetTunnelClient(TunnelClient& client);
|
||||
SecureSession* findSecureSession(uint16_t session_id, const ::sockaddr_in& remote);
|
||||
const SecureSession* findSecureSession(uint16_t session_id,
|
||||
const ::sockaddr_in& remote) const;
|
||||
SecureSession* allocateSecureSession(const ::sockaddr_in& remote);
|
||||
SecureSession* activeSecureSession();
|
||||
bool wrapSecurePacket(SecureSession& session, const std::vector<uint8_t>& inner,
|
||||
std::vector<uint8_t>* wrapped);
|
||||
bool wrapSecureRoutingPacket(const std::vector<uint8_t>& inner,
|
||||
std::vector<uint8_t>* wrapped);
|
||||
bool decryptSecureWrapper(SecureSession& session, const uint8_t* body, size_t len,
|
||||
std::vector<uint8_t>* inner);
|
||||
bool decryptSecureRoutingWrapper(const uint8_t* body, size_t len,
|
||||
std::vector<uint8_t>* inner);
|
||||
bool verifySecureSessionAuth(SecureSession& session, const uint8_t* packet,
|
||||
size_t len, uint8_t* status);
|
||||
bool secureCredentialsReady() const;
|
||||
uint8_t nextTunnelChannelId() const;
|
||||
uint16_t effectiveTunnelAddressForSlot(size_t slot) const;
|
||||
void pruneStaleTunnelClients();
|
||||
@@ -366,6 +452,10 @@ class GatewayKnxTpIpRouter {
|
||||
TunnelClient* response_client, uint16_t response_service,
|
||||
const uint8_t* suppress_routing_echo = nullptr,
|
||||
size_t suppress_routing_echo_len = 0);
|
||||
bool handleOamRouterTunnelFrame(const uint8_t* data, size_t len,
|
||||
TunnelClient* response_client, uint16_t response_service,
|
||||
const uint8_t* suppress_routing_echo = nullptr,
|
||||
size_t suppress_routing_echo_len = 0);
|
||||
bool handleOpenKnxBusFrame(const uint8_t* data, size_t len);
|
||||
bool transmitOpenKnxTpFrame(const uint8_t* data, size_t len);
|
||||
void selectOpenKnxNetworkInterface(const ::sockaddr_in& remote);
|
||||
@@ -380,13 +470,16 @@ class GatewayKnxTpIpRouter {
|
||||
void pollProgrammingButton();
|
||||
void updateProgrammingLed();
|
||||
void setProgrammingLed(bool on);
|
||||
void setOamProgrammingLed(bool on);
|
||||
|
||||
GatewayKnxBridge& bridge_;
|
||||
GroupWriteHandler group_write_handler_;
|
||||
GroupObjectWriteHandler group_object_write_handler_;
|
||||
RoutingSequenceStoreHandler routing_sequence_store_handler_;
|
||||
std::string openknx_namespace_;
|
||||
GatewayKnxConfig config_;
|
||||
std::unique_ptr<openknx::EtsDeviceRuntime> ets_device_;
|
||||
std::unique_ptr<openknx::OamRouterRuntime> oam_router_;
|
||||
TaskHandle_t task_handle_{nullptr};
|
||||
SemaphoreHandle_t openknx_lock_{nullptr};
|
||||
SemaphoreHandle_t startup_semaphore_{nullptr};
|
||||
@@ -403,14 +496,22 @@ class GatewayKnxTpIpRouter {
|
||||
TickType_t network_refresh_tick_{0};
|
||||
std::array<TcpClient, kMaxTcpClients> tcp_clients_{};
|
||||
std::array<TunnelClient, kMaxTunnelClients> tunnel_clients_{};
|
||||
std::array<SecureSession, kMaxTcpClients> secure_sessions_{};
|
||||
std::unique_ptr<IpParameterObject> knx_ip_parameters_;
|
||||
uint8_t last_tunnel_channel_id_{0};
|
||||
uint16_t last_secure_session_id_{0};
|
||||
uint16_t active_secure_session_id_{0};
|
||||
GatewayKnxIpSecureCredentialMaterial oam_ip_secure_credentials_{};
|
||||
bool tp_uart_online_{false};
|
||||
bool commissioning_only_{false};
|
||||
std::atomic_bool openknx_configured_{false};
|
||||
bool programming_button_last_pressed_{false};
|
||||
bool programming_led_state_{false};
|
||||
TickType_t programming_button_last_toggle_tick_{0};
|
||||
bool oam_programming_mode_{false};
|
||||
bool oam_programming_button_last_pressed_{false};
|
||||
bool oam_programming_led_state_{false};
|
||||
TickType_t oam_programming_button_last_toggle_tick_{0};
|
||||
std::string last_error_;
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,22 @@ constexpr const char* kTag = "gateway_knx";
|
||||
#define CONFIG_GATEWAY_KNX_OEM_HARDWARE_ID 0xA401
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID
|
||||
#define CONFIG_GATEWAY_KNX_OAM_ROUTER_OEM_MANUFACTURER_ID 0x00FA
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER
|
||||
#define CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_NUMBER 0xA11F
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION
|
||||
#define CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION 0x07
|
||||
#endif
|
||||
|
||||
#ifndef CONFIG_GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID
|
||||
#define CONFIG_GATEWAY_KNX_OAM_ROUTER_HARDWARE_ID 0x0001
|
||||
#endif
|
||||
|
||||
inline constexpr uint16_t kReg1DaliManufacturerId =
|
||||
static_cast<uint16_t>(CONFIG_GATEWAY_KNX_OEM_MANUFACTURER_ID);
|
||||
inline constexpr uint16_t kReg1DaliHardwareId =
|
||||
@@ -56,6 +72,33 @@ inline constexpr uint8_t kReg1DaliProgramVersion[5] = {
|
||||
static_cast<uint8_t>(kReg1DaliApplicationNumber & 0xff),
|
||||
kReg1DaliApplicationVersion};
|
||||
|
||||
inline constexpr uint32_t kReg1DaliSerialMacIncrement = 0;
|
||||
inline constexpr uint32_t kOamRouterSerialMacIncrement = 1;
|
||||
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 =
|
||||
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);
|
||||
inline constexpr uint8_t kOamRouterApplicationVersion =
|
||||
static_cast<uint8_t>(CONFIG_GATEWAY_KNX_OAM_ROUTER_APPLICATION_VERSION);
|
||||
inline constexpr uint8_t kOamRouterHardwareType[6] = {
|
||||
0x00,
|
||||
0x00,
|
||||
static_cast<uint8_t>((kOamRouterHardwareId >> 8) & 0xff),
|
||||
static_cast<uint8_t>(kOamRouterHardwareId & 0xff),
|
||||
kOamRouterApplicationVersion,
|
||||
0x00};
|
||||
inline constexpr uint8_t kOamRouterOrderNumber[10] = {
|
||||
'I', 'P', '-', 'R', 'o', 'u', 't', 'e', 'r', 0};
|
||||
inline constexpr uint8_t kOamRouterProgramVersion[5] = {
|
||||
static_cast<uint8_t>((kOamRouterManufacturerId >> 8) & 0xff),
|
||||
static_cast<uint8_t>(kOamRouterManufacturerId & 0xff),
|
||||
static_cast<uint8_t>((kOamRouterApplicationNumber >> 8) & 0xff),
|
||||
static_cast<uint8_t>(kOamRouterApplicationNumber & 0xff),
|
||||
kOamRouterApplicationVersion};
|
||||
|
||||
// RAII semaphore guard.
|
||||
class SemaphoreGuard {
|
||||
public:
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_idf_platform.h"
|
||||
#include "ets_memory_loader.h"
|
||||
|
||||
#include "esp_netif.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "knx/cemi_frame.h"
|
||||
#include "knx/device_object.h"
|
||||
#include "knx/platform.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
#include "knx/bau091A.h"
|
||||
#endif
|
||||
|
||||
namespace gateway::openknx {
|
||||
|
||||
class OamRouterRuntime {
|
||||
public:
|
||||
using CemiFrameSender = std::function<void(const uint8_t* data, size_t len)>;
|
||||
|
||||
OamRouterRuntime(std::string nvs_namespace,
|
||||
uint16_t fallback_individual_address,
|
||||
uint16_t tunnel_client_address = 0);
|
||||
~OamRouterRuntime();
|
||||
|
||||
bool available() const;
|
||||
uint16_t individualAddress() const;
|
||||
uint16_t tunnelClientAddress() const;
|
||||
bool configured() const;
|
||||
bool programmingMode() const;
|
||||
void setProgrammingMode(bool enabled);
|
||||
void toggleProgrammingMode();
|
||||
EtsMemorySnapshot snapshot() const;
|
||||
|
||||
DeviceObject* deviceObject();
|
||||
Platform* platform();
|
||||
void setNetworkInterface(esp_netif_t* netif);
|
||||
bool handleTunnelFrame(const uint8_t* data, size_t len, CemiFrameSender sender);
|
||||
void loop();
|
||||
|
||||
private:
|
||||
static bool HandleOutboundCemiFrame(CemiFrame& frame, void* context);
|
||||
static void EmitTunnelFrame(CemiFrame& frame, void* context);
|
||||
static uint16_t DefaultTunnelClientAddress(uint16_t individual_address);
|
||||
bool shouldConsumeTunnelFrame(CemiFrame& frame) const;
|
||||
|
||||
std::string nvs_namespace_;
|
||||
CemiFrameSender sender_;
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
EspIdfPlatform platform_;
|
||||
Bau091A device_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace gateway::openknx
|
||||
@@ -2,6 +2,115 @@
|
||||
|
||||
namespace gateway {
|
||||
|
||||
namespace {
|
||||
|
||||
GatewayKnxCloudRemoteConfig GatewayKnxCloudRemoteConfigFromValue(const DaliValue* value) {
|
||||
GatewayKnxCloudRemoteConfig config;
|
||||
if (value == nullptr || value->asObject() == nullptr) {
|
||||
return config;
|
||||
}
|
||||
const auto& object = *value->asObject();
|
||||
config.enabled = ObjectBoolAny(object, {"enabled", "cloudRemoteEnabled",
|
||||
"cloud_remote_enabled"})
|
||||
.value_or(config.enabled);
|
||||
config.mode = ObjectStringAny(object, {"mode", "remoteMode", "remote_mode"})
|
||||
.value_or(config.mode);
|
||||
config.relay_endpoint = ObjectStringAny(object, {"relayEndpoint", "relay_endpoint",
|
||||
"endpoint"})
|
||||
.value_or(config.relay_endpoint);
|
||||
config.mqtt_topic_prefix = ObjectStringAny(object, {"mqttTopicPrefix", "mqtt_topic_prefix",
|
||||
"topicPrefix", "topic_prefix"})
|
||||
.value_or(config.mqtt_topic_prefix);
|
||||
config.auth_token_ref = ObjectStringAny(object, {"authTokenRef", "auth_token_ref",
|
||||
"tokenRef", "token_ref"})
|
||||
.value_or(config.auth_token_ref);
|
||||
config.require_secure_tunnel = ObjectBoolAny(object, {"requireSecureTunnel",
|
||||
"require_secure_tunnel"})
|
||||
.value_or(config.require_secure_tunnel);
|
||||
config.udp_punch_enabled = ObjectBoolAny(object, {"udpPunchEnabled", "udp_punch_enabled"})
|
||||
.value_or(config.udp_punch_enabled);
|
||||
return config;
|
||||
}
|
||||
|
||||
DaliValue GatewayKnxCloudRemoteConfigToValue(const GatewayKnxCloudRemoteConfig& config) {
|
||||
DaliValue::Object out;
|
||||
out["enabled"] = config.enabled;
|
||||
out["mode"] = config.mode;
|
||||
out["relayEndpoint"] = config.relay_endpoint;
|
||||
out["mqttTopicPrefix"] = config.mqtt_topic_prefix;
|
||||
out["authTokenRef"] = config.auth_token_ref;
|
||||
out["requireSecureTunnel"] = config.require_secure_tunnel;
|
||||
out["udpPunchEnabled"] = config.udp_punch_enabled;
|
||||
return DaliValue(std::move(out));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<GatewayKnxOamRouterConfig> GatewayKnxOamRouterConfigFromValue(
|
||||
const DaliValue* value) {
|
||||
if (value == nullptr || value->asObject() == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto& object = *value->asObject();
|
||||
GatewayKnxOamRouterConfig config;
|
||||
config.enabled = ObjectBoolAny(object, {"enabled", "oamRouterEnabled",
|
||||
"oam_router_enabled"})
|
||||
.value_or(config.enabled);
|
||||
config.ets_database_enabled = ObjectBoolAny(object, {"etsDatabaseEnabled",
|
||||
"ets_database_enabled"})
|
||||
.value_or(config.ets_database_enabled);
|
||||
config.secure_tunnel_enabled = ObjectBoolAny(object, {"secureTunnelEnabled",
|
||||
"secure_tunnel_enabled"})
|
||||
.value_or(config.secure_tunnel_enabled);
|
||||
config.secure_routing_enabled = ObjectBoolAny(object, {"secureRoutingEnabled",
|
||||
"secure_routing_enabled"})
|
||||
.value_or(config.secure_routing_enabled);
|
||||
config.individual_address = static_cast<uint16_t>(std::clamp(
|
||||
ObjectIntAny(object, {"individualAddress", "individual_address", "routerAddress",
|
||||
"router_address"})
|
||||
.value_or(config.individual_address),
|
||||
0, 0xffff));
|
||||
config.tunnel_address_base = static_cast<uint16_t>(std::clamp(
|
||||
ObjectIntAny(object, {"tunnelAddressBase", "tunnel_address_base",
|
||||
"tunnelBaseAddress", "tunnel_base_address"})
|
||||
.value_or(config.tunnel_address_base),
|
||||
0, 0xffff));
|
||||
config.programming_button_gpio = std::clamp(
|
||||
ObjectIntAny(object, {"programmingButtonGpio", "programming_button_gpio"})
|
||||
.value_or(config.programming_button_gpio),
|
||||
-1, 48);
|
||||
config.programming_button_active_low =
|
||||
ObjectBoolAny(object, {"programmingButtonActiveLow", "programming_button_active_low"})
|
||||
.value_or(config.programming_button_active_low);
|
||||
config.programming_led_gpio = std::clamp(
|
||||
ObjectIntAny(object, {"programmingLedGpio", "programming_led_gpio"})
|
||||
.value_or(config.programming_led_gpio),
|
||||
-1, 48);
|
||||
config.programming_led_active_high =
|
||||
ObjectBoolAny(object, {"programmingLedActiveHigh", "programming_led_active_high"})
|
||||
.value_or(config.programming_led_active_high);
|
||||
config.cloud_remote = GatewayKnxCloudRemoteConfigFromValue(
|
||||
ObjectValueAny(object, {"cloudRemote", "cloud_remote", "remoteAccess",
|
||||
"remote_access"}));
|
||||
return config;
|
||||
}
|
||||
|
||||
DaliValue GatewayKnxOamRouterConfigToValue(const GatewayKnxOamRouterConfig& config) {
|
||||
DaliValue::Object out;
|
||||
out["enabled"] = config.enabled;
|
||||
out["etsDatabaseEnabled"] = config.ets_database_enabled;
|
||||
out["secureTunnelEnabled"] = config.secure_tunnel_enabled;
|
||||
out["secureRoutingEnabled"] = config.secure_routing_enabled;
|
||||
out["individualAddress"] = static_cast<int>(config.individual_address);
|
||||
out["tunnelAddressBase"] = static_cast<int>(config.tunnel_address_base);
|
||||
out["programmingButtonGpio"] = config.programming_button_gpio;
|
||||
out["programmingButtonActiveLow"] = config.programming_button_active_low;
|
||||
out["programmingLedGpio"] = config.programming_led_gpio;
|
||||
out["programmingLedActiveHigh"] = config.programming_led_active_high;
|
||||
out["cloudRemote"] = GatewayKnxCloudRemoteConfigToValue(config.cloud_remote);
|
||||
return DaliValue(std::move(out));
|
||||
}
|
||||
|
||||
std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value) {
|
||||
if (value == nullptr || value->asObject() == nullptr) {
|
||||
return std::nullopt;
|
||||
@@ -65,6 +174,15 @@ std::optional<GatewayKnxConfig> GatewayKnxConfigFromValue(const DaliValue* value
|
||||
config.programming_led_active_high =
|
||||
ObjectBoolAny(object, {"programmingLedActiveHigh", "programming_led_active_high"})
|
||||
.value_or(config.programming_led_active_high);
|
||||
if (const auto oam_router = GatewayKnxOamRouterConfigFromValue(
|
||||
ObjectValueAny(object, {"oamRouter", "oam_router", "routerApplication",
|
||||
"router_application"}))) {
|
||||
config.oam_router = oam_router.value();
|
||||
} else {
|
||||
config.oam_router.enabled = ObjectBoolAny(object, {"oamRouterEnabled",
|
||||
"oam_router_enabled"})
|
||||
.value_or(config.oam_router.enabled);
|
||||
}
|
||||
|
||||
const auto* tp_uart = getObjectValue(object, "tpUart");
|
||||
if (tp_uart == nullptr) {
|
||||
@@ -117,6 +235,7 @@ DaliValue GatewayKnxConfigToValue(const GatewayKnxConfig& config) {
|
||||
out["programmingButtonActiveLow"] = config.programming_button_active_low;
|
||||
out["programmingLedGpio"] = config.programming_led_gpio;
|
||||
out["programmingLedActiveHigh"] = config.programming_led_active_high;
|
||||
out["oamRouter"] = GatewayKnxOamRouterConfigToValue(config.oam_router);
|
||||
DaliValue::Object serial;
|
||||
serial["uartPort"] = config.tp_uart.uart_port;
|
||||
serial["txPin"] = config.tp_uart.tx_pin;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "lwip/sockets.h"
|
||||
#include "ets_device_runtime.h"
|
||||
#include "gateway_knx_internal.h"
|
||||
#include "oam_router_runtime.h"
|
||||
#include "soc/uart_periph.h"
|
||||
#include "tpuart_uart_interface.h"
|
||||
|
||||
@@ -105,6 +106,7 @@ constexpr uint8_t kKnxServiceFamilyCore = 0x02;
|
||||
constexpr uint8_t kKnxServiceFamilyDeviceManagement = 0x03;
|
||||
constexpr uint8_t kKnxServiceFamilyTunnelling = 0x04;
|
||||
constexpr uint8_t kKnxServiceFamilyRouting = 0x05;
|
||||
constexpr uint8_t kKnxServiceFamilySecurity = 0x06;
|
||||
constexpr uint16_t kKnxIpOnlyDeviceDescriptor = 0x57b0;
|
||||
constexpr uint16_t kKnxTpIpInterfaceDeviceDescriptor = 0x091a;
|
||||
constexpr uint8_t kKnxIpAssignmentManual = 0x01;
|
||||
@@ -848,6 +850,18 @@ bool MatchesOpenKnxLocalIndividualAddress(const CemiFrame& frame,
|
||||
return dest == own_address || dest == client_address || (commissioning && dest == 0xffff);
|
||||
}
|
||||
|
||||
bool MatchesOamRouterLocalIndividualAddress(const CemiFrame& frame,
|
||||
const openknx::OamRouterRuntime& oam_router) {
|
||||
if (frame.addressType() != IndividualAddress) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t dest = frame.destinationAddress();
|
||||
const uint16_t own_address = oam_router.individualAddress();
|
||||
const uint16_t client_address = oam_router.tunnelClientAddress();
|
||||
const bool commissioning = !oam_router.configured() || oam_router.programmingMode();
|
||||
return dest == own_address || dest == client_address || (commissioning && dest == 0xffff);
|
||||
}
|
||||
|
||||
bool BuildLocalRoutingTunnelFrame(const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* local_frame) {
|
||||
if (data == nullptr || local_frame == nullptr || len < 2) {
|
||||
|
||||
@@ -36,6 +36,16 @@ void GatewayKnxTpIpRouter::setGroupObjectWriteHandler(GroupObjectWriteHandler ha
|
||||
group_object_write_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::setOamIpSecureCredentials(
|
||||
const GatewayKnxIpSecureCredentialMaterial& credentials) {
|
||||
oam_ip_secure_credentials_ = credentials;
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::setOamIpSecureRoutingSequenceStoreHandler(
|
||||
RoutingSequenceStoreHandler handler) {
|
||||
routing_sequence_store_handler_ = std::move(handler);
|
||||
}
|
||||
|
||||
const GatewayKnxConfig& GatewayKnxTpIpRouter::config() const { return config_; }
|
||||
|
||||
bool GatewayKnxTpIpRouter::tpUartOnline() const { return tp_uart_online_; }
|
||||
@@ -69,6 +79,38 @@ esp_err_t GatewayKnxTpIpRouter::toggleProgrammingMode() {
|
||||
return setProgrammingMode(!programmingMode());
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::oamProgrammingMode() {
|
||||
if (openknx_lock_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
SemaphoreGuard guard(openknx_lock_);
|
||||
return oam_router_ != nullptr ? oam_router_->programmingMode() : oam_programming_mode_;
|
||||
}
|
||||
|
||||
esp_err_t GatewayKnxTpIpRouter::setOamProgrammingMode(bool enabled) {
|
||||
if (openknx_lock_ == nullptr) {
|
||||
last_error_ = "KNX runtime lock is unavailable";
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
if (!config_.oam_router.enabled) {
|
||||
last_error_ = "OAM KNX/IP router persona is disabled";
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
}
|
||||
SemaphoreGuard guard(openknx_lock_);
|
||||
oam_programming_mode_ = enabled;
|
||||
if (oam_router_ != nullptr) {
|
||||
oam_router_->setProgrammingMode(enabled);
|
||||
}
|
||||
setOamProgrammingLed(enabled);
|
||||
ESP_LOGI(kTag, "OAM KNX/IP router programming mode %s namespace=%s",
|
||||
enabled ? "enabled" : "disabled", openknx_namespace_.c_str());
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t GatewayKnxTpIpRouter::toggleOamProgrammingMode() {
|
||||
return setOamProgrammingMode(!oamProgrammingMode());
|
||||
}
|
||||
|
||||
esp_err_t GatewayKnxTpIpRouter::start(uint32_t task_stack_size, UBaseType_t task_priority) {
|
||||
if (started_ || task_handle_ != nullptr) {
|
||||
return ESP_OK;
|
||||
@@ -193,6 +235,19 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
|
||||
bridge_.setRuntimeContext(ets_device_.get());
|
||||
knx_ip_parameters_ = std::make_unique<IpParameterObject>(
|
||||
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,
|
||||
config_.oam_router.tunnel_address_base);
|
||||
if (oam_router_->available()) {
|
||||
oam_router_->setProgrammingMode(oam_programming_mode_);
|
||||
} else {
|
||||
ESP_LOGW(kTag, "OAM router persona requested but BAU091A support is not compiled in");
|
||||
oam_router_.reset();
|
||||
}
|
||||
} else {
|
||||
oam_router_.reset();
|
||||
}
|
||||
openknx_configured_.store(ets_device_->configured());
|
||||
ESP_LOGI(kTag,
|
||||
"OpenKNX runtime namespace=%s configured=%d ipInterface=0x%04x "
|
||||
@@ -200,6 +255,14 @@ esp_err_t GatewayKnxTpIpRouter::initializeRuntime() {
|
||||
openknx_namespace_.c_str(), ets_device_->configured(),
|
||||
effectiveIpInterfaceIndividualAddress(), ets_device_->individualAddress(),
|
||||
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_->individualAddress(), oam_router_->tunnelClientAddress(),
|
||||
config_.oam_router.secure_tunnel_enabled,
|
||||
config_.oam_router.secure_routing_enabled);
|
||||
}
|
||||
ets_device_->setFunctionPropertyHandlers(
|
||||
[this](uint8_t object_index, uint8_t property_id, const uint8_t* data, size_t len,
|
||||
std::vector<uint8_t>* response) {
|
||||
@@ -300,6 +363,10 @@ void GatewayKnxTpIpRouter::taskLoop() {
|
||||
if (ets_device_ != nullptr) {
|
||||
pollProgrammingButton();
|
||||
ets_device_->loop();
|
||||
if (oam_router_ != nullptr) {
|
||||
oam_router_->loop();
|
||||
oam_programming_mode_ = oam_router_->programmingMode();
|
||||
}
|
||||
tp_uart_online_ = ets_device_->tpUartOnline();
|
||||
updateProgrammingLed();
|
||||
}
|
||||
@@ -376,8 +443,11 @@ void GatewayKnxTpIpRouter::finishTask() {
|
||||
{
|
||||
SemaphoreGuard guard(openknx_lock_);
|
||||
setProgrammingLed(false);
|
||||
setOamProgrammingLed(false);
|
||||
oam_programming_mode_ = false;
|
||||
knx_ip_parameters_.reset();
|
||||
bridge_.setRuntimeContext(nullptr);
|
||||
oam_router_.reset();
|
||||
ets_device_.reset();
|
||||
openknx_configured_.store(false);
|
||||
}
|
||||
@@ -387,32 +457,57 @@ void GatewayKnxTpIpRouter::finishTask() {
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::pollProgrammingButton() {
|
||||
if (config_.programming_button_gpio < 0 || ets_device_ == nullptr) {
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
|
||||
if (config_.programming_button_gpio >= 0 && ets_device_ != nullptr) {
|
||||
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.programming_button_gpio));
|
||||
const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0;
|
||||
if (pressed && !programming_button_last_pressed_ &&
|
||||
now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) {
|
||||
ets_device_->toggleProgrammingMode();
|
||||
ESP_LOGI(kTag, "KNX programming mode %s namespace=%s",
|
||||
ets_device_->programmingMode() ? "enabled" : "disabled",
|
||||
openknx_namespace_.c_str());
|
||||
programming_button_last_toggle_tick_ = now;
|
||||
}
|
||||
programming_button_last_pressed_ = pressed;
|
||||
}
|
||||
|
||||
if (!config_.oam_router.enabled || config_.oam_router.programming_button_gpio < 0) {
|
||||
return;
|
||||
}
|
||||
const int level = gpio_get_level(static_cast<gpio_num_t>(config_.programming_button_gpio));
|
||||
const bool pressed = config_.programming_button_active_low ? level == 0 : level != 0;
|
||||
const TickType_t now = xTaskGetTickCount();
|
||||
if (pressed && !programming_button_last_pressed_ &&
|
||||
now - programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) {
|
||||
ets_device_->toggleProgrammingMode();
|
||||
ESP_LOGI(kTag, "KNX programming mode %s namespace=%s",
|
||||
ets_device_->programmingMode() ? "enabled" : "disabled",
|
||||
const int oam_level = gpio_get_level(
|
||||
static_cast<gpio_num_t>(config_.oam_router.programming_button_gpio));
|
||||
const bool oam_pressed = config_.oam_router.programming_button_active_low
|
||||
? oam_level == 0
|
||||
: oam_level != 0;
|
||||
if (oam_pressed && !oam_programming_button_last_pressed_ &&
|
||||
now - oam_programming_button_last_toggle_tick_ >= pdMS_TO_TICKS(200)) {
|
||||
oam_programming_mode_ = !oam_programming_mode_;
|
||||
if (oam_router_ != nullptr) {
|
||||
oam_router_->setProgrammingMode(oam_programming_mode_);
|
||||
}
|
||||
setOamProgrammingLed(oam_programming_mode_);
|
||||
ESP_LOGI(kTag, "OAM KNX/IP router programming mode %s namespace=%s",
|
||||
oam_programming_mode_ ? "enabled" : "disabled",
|
||||
openknx_namespace_.c_str());
|
||||
programming_button_last_toggle_tick_ = now;
|
||||
oam_programming_button_last_toggle_tick_ = now;
|
||||
}
|
||||
programming_button_last_pressed_ = pressed;
|
||||
oam_programming_button_last_pressed_ = oam_pressed;
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::updateProgrammingLed() {
|
||||
if (config_.programming_led_gpio < 0 || ets_device_ == nullptr) {
|
||||
return;
|
||||
if (config_.programming_led_gpio >= 0 && ets_device_ != nullptr) {
|
||||
const bool programming_mode = ets_device_->programmingMode();
|
||||
if (programming_mode != programming_led_state_) {
|
||||
setProgrammingLed(programming_mode);
|
||||
}
|
||||
}
|
||||
const bool programming_mode = ets_device_->programmingMode();
|
||||
if (programming_mode == programming_led_state_) {
|
||||
return;
|
||||
|
||||
if (config_.oam_router.enabled && config_.oam_router.programming_led_gpio >= 0 &&
|
||||
oam_programming_mode_ != oam_programming_led_state_) {
|
||||
setOamProgrammingLed(oam_programming_mode_);
|
||||
}
|
||||
setProgrammingLed(programming_mode);
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::setProgrammingLed(bool on) {
|
||||
@@ -425,6 +520,17 @@ void GatewayKnxTpIpRouter::setProgrammingLed(bool on) {
|
||||
programming_led_state_ = on;
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::setOamProgrammingLed(bool on) {
|
||||
if (config_.oam_router.programming_led_gpio < 0) {
|
||||
oam_programming_led_state_ = on;
|
||||
return;
|
||||
}
|
||||
const bool level = config_.oam_router.programming_led_active_high ? on : !on;
|
||||
gpio_set_level(static_cast<gpio_num_t>(config_.oam_router.programming_led_gpio),
|
||||
level ? 1 : 0);
|
||||
oam_programming_led_state_ = on;
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::closeSockets() {
|
||||
if (udp_sock_ >= 0) {
|
||||
shutdown(udp_sock_, SHUT_RDWR);
|
||||
@@ -609,6 +715,7 @@ void GatewayKnxTpIpRouter::closeTcpClient(TcpClient& client) {
|
||||
resetTunnelClient(tunnel);
|
||||
}
|
||||
}
|
||||
closeSecureSessionsForTcp(sock);
|
||||
if (active_tcp_sock_ == sock) {
|
||||
active_tcp_sock_ = -1;
|
||||
}
|
||||
@@ -754,6 +861,9 @@ bool GatewayKnxTpIpRouter::configureProgrammingGpio() {
|
||||
programming_button_last_pressed_ = false;
|
||||
programming_button_last_toggle_tick_ = 0;
|
||||
programming_led_state_ = false;
|
||||
oam_programming_button_last_pressed_ = false;
|
||||
oam_programming_button_last_toggle_tick_ = 0;
|
||||
oam_programming_led_state_ = false;
|
||||
|
||||
if (config_.programming_button_gpio >= 0) {
|
||||
gpio_config_t button_config{};
|
||||
@@ -792,6 +902,47 @@ bool GatewayKnxTpIpRouter::configureProgrammingGpio() {
|
||||
setProgrammingLed(false);
|
||||
}
|
||||
|
||||
if (config_.oam_router.enabled && config_.oam_router.programming_button_gpio >= 0) {
|
||||
gpio_config_t button_config{};
|
||||
button_config.pin_bit_mask =
|
||||
1ULL << static_cast<uint32_t>(config_.oam_router.programming_button_gpio);
|
||||
button_config.mode = GPIO_MODE_INPUT;
|
||||
button_config.pull_up_en = config_.oam_router.programming_button_active_low
|
||||
? GPIO_PULLUP_ENABLE
|
||||
: GPIO_PULLUP_DISABLE;
|
||||
button_config.pull_down_en = config_.oam_router.programming_button_active_low
|
||||
? GPIO_PULLDOWN_DISABLE
|
||||
: GPIO_PULLDOWN_ENABLE;
|
||||
button_config.intr_type = GPIO_INTR_DISABLE;
|
||||
const esp_err_t err = gpio_config(&button_config);
|
||||
if (err != ESP_OK) {
|
||||
last_error_ = EspErrDetail("failed to configure OAM KNX programming button GPIO" +
|
||||
std::to_string(config_.oam_router.programming_button_gpio),
|
||||
err);
|
||||
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (config_.oam_router.enabled && config_.oam_router.programming_led_gpio >= 0) {
|
||||
gpio_config_t led_config{};
|
||||
led_config.pin_bit_mask =
|
||||
1ULL << static_cast<uint32_t>(config_.oam_router.programming_led_gpio);
|
||||
led_config.mode = GPIO_MODE_OUTPUT;
|
||||
led_config.pull_up_en = GPIO_PULLUP_DISABLE;
|
||||
led_config.pull_down_en = GPIO_PULLDOWN_DISABLE;
|
||||
led_config.intr_type = GPIO_INTR_DISABLE;
|
||||
const esp_err_t err = gpio_config(&led_config);
|
||||
if (err != ESP_OK) {
|
||||
last_error_ = EspErrDetail("failed to configure OAM KNX programming LED GPIO" +
|
||||
std::to_string(config_.oam_router.programming_led_gpio),
|
||||
err);
|
||||
ESP_LOGE(kTag, "%s", last_error_.c_str());
|
||||
return false;
|
||||
}
|
||||
setOamProgrammingLed(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ void GatewayKnxTpIpRouter::selectOpenKnxNetworkInterface(const sockaddr_in& remo
|
||||
if (ets_device_ != nullptr) {
|
||||
ets_device_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr);
|
||||
}
|
||||
if (oam_router_ != nullptr) {
|
||||
oam_router_->setNetworkInterface(netif.has_value() ? netif->netif : nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t len,
|
||||
@@ -15,6 +18,20 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
|
||||
uint16_t response_service,
|
||||
const uint8_t* suppress_routing_echo,
|
||||
size_t suppress_routing_echo_len) {
|
||||
bool route_to_oam = response_client != nullptr && response_client->oam_router_persona;
|
||||
if (!route_to_oam && data != nullptr && len >= 2) {
|
||||
std::vector<uint8_t> frame_data(data, data + len);
|
||||
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
|
||||
if (frame.valid() && oam_router_ != nullptr &&
|
||||
MatchesOamRouterLocalIndividualAddress(frame, *oam_router_)) {
|
||||
route_to_oam = true;
|
||||
}
|
||||
}
|
||||
if (route_to_oam) {
|
||||
return handleOamRouterTunnelFrame(data, len, response_client, response_service,
|
||||
suppress_routing_echo, suppress_routing_echo_len);
|
||||
}
|
||||
|
||||
SemaphoreGuard guard(openknx_lock_);
|
||||
if (ets_device_ == nullptr) {
|
||||
return false;
|
||||
@@ -87,6 +104,82 @@ bool GatewayKnxTpIpRouter::handleOpenKnxTunnelFrame(const uint8_t* data, size_t
|
||||
return consumed;
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::handleOamRouterTunnelFrame(const uint8_t* data, size_t len,
|
||||
TunnelClient* response_client,
|
||||
uint16_t response_service,
|
||||
const uint8_t* suppress_routing_echo,
|
||||
size_t suppress_routing_echo_len) {
|
||||
SemaphoreGuard guard(openknx_lock_);
|
||||
if (oam_router_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> tunnel_confirmation;
|
||||
const bool needs_tunnel_confirmation =
|
||||
response_client != nullptr && response_client->connected &&
|
||||
response_service == kServiceTunnellingRequest &&
|
||||
BuildTunnelConfirmationFrame(data, len, &tunnel_confirmation);
|
||||
bool sent_tunnel_confirmation = false;
|
||||
const bool consumed = oam_router_->handleTunnelFrame(
|
||||
data, len,
|
||||
[this, response_client, response_service, needs_tunnel_confirmation,
|
||||
&tunnel_confirmation, &sent_tunnel_confirmation,
|
||||
suppress_routing_echo, suppress_routing_echo_len](const uint8_t* response,
|
||||
size_t response_len) {
|
||||
if (response == nullptr || response_len == 0) {
|
||||
return;
|
||||
}
|
||||
const bool routing_context =
|
||||
response_client == nullptr && response_service == kServiceRoutingIndication;
|
||||
const auto message_code = CemiMessageCode(response, response_len);
|
||||
if (routing_context && suppress_routing_echo != nullptr &&
|
||||
IsLocalRoutingEchoIndication(response, response_len, suppress_routing_echo,
|
||||
suppress_routing_echo_len)) {
|
||||
return;
|
||||
}
|
||||
if (needs_tunnel_confirmation && !sent_tunnel_confirmation &&
|
||||
message_code.has_value() && message_code.value() != L_data_con) {
|
||||
sent_tunnel_confirmation = sendCemiFrameToClient(
|
||||
*response_client, kServiceTunnellingRequest,
|
||||
tunnel_confirmation.data(), tunnel_confirmation.size());
|
||||
}
|
||||
|
||||
const uint16_t service = KnxIpServiceForCemi(response, response_len, response_service);
|
||||
if (service == kServiceDeviceConfigurationRequest) {
|
||||
if (response_client != nullptr && response_client->connected) {
|
||||
sendCemiFrameToClient(*response_client, service, response, response_len);
|
||||
} else if (routing_context) {
|
||||
sendRoutingIndication(response, response_len);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (message_code.has_value() && message_code.value() == L_data_con) {
|
||||
if (routing_context) {
|
||||
return;
|
||||
}
|
||||
if (response_client != nullptr && response_client->connected) {
|
||||
sent_tunnel_confirmation =
|
||||
sendCemiFrameToClient(*response_client, service, response, response_len) ||
|
||||
sent_tunnel_confirmation;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (routing_context) {
|
||||
sendRoutingIndication(response, response_len);
|
||||
return;
|
||||
}
|
||||
if (response_client != nullptr && response_client->connected) {
|
||||
sendCemiFrameToClient(*response_client, service, response, response_len);
|
||||
return;
|
||||
}
|
||||
sendTunnelIndication(response, response_len);
|
||||
});
|
||||
if (needs_tunnel_confirmation && consumed && !sent_tunnel_confirmation) {
|
||||
sendCemiFrameToClient(*response_client, kServiceTunnellingRequest,
|
||||
tunnel_confirmation.data(), tunnel_confirmation.size());
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::transmitOpenKnxTpFrame(const uint8_t* data, size_t len) {
|
||||
SemaphoreGuard guard(openknx_lock_);
|
||||
if (ets_device_ == nullptr) {
|
||||
|
||||
@@ -33,10 +33,20 @@ void GatewayKnxTpIpRouter::sendSecureSessionStatus(uint8_t status, const sockadd
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::sendPacket(const std::vector<uint8_t>& packet,
|
||||
const sockaddr_in& remote) const {
|
||||
const sockaddr_in& remote) {
|
||||
if (packet.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (SecureSession* session = activeSecureSession(); session != nullptr) {
|
||||
std::vector<uint8_t> wrapped;
|
||||
if (wrapSecurePacket(*session, packet, &wrapped)) {
|
||||
if (active_tcp_sock_ >= 0) {
|
||||
return SendStream(active_tcp_sock_, wrapped.data(), wrapped.size());
|
||||
}
|
||||
return udp_sock_ >= 0 && SendAll(udp_sock_, wrapped.data(), wrapped.size(), remote);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (active_tcp_sock_ >= 0) {
|
||||
return SendStream(active_tcp_sock_, packet.data(), packet.size());
|
||||
}
|
||||
@@ -44,10 +54,32 @@ bool GatewayKnxTpIpRouter::sendPacket(const std::vector<uint8_t>& packet,
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::sendPacketToTunnelClient(
|
||||
const TunnelClient& client, const std::vector<uint8_t>& packet) const {
|
||||
const TunnelClient& client, const std::vector<uint8_t>& packet) {
|
||||
if (packet.empty()) {
|
||||
return false;
|
||||
}
|
||||
if (client.secure_session_id != 0) {
|
||||
SecureSession* session = nullptr;
|
||||
for (auto& candidate : secure_sessions_) {
|
||||
if (candidate.active && candidate.authenticated &&
|
||||
candidate.session_id == client.secure_session_id) {
|
||||
session = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (session != nullptr) {
|
||||
std::vector<uint8_t> wrapped;
|
||||
if (wrapSecurePacket(*session, packet, &wrapped)) {
|
||||
if (client.tcp_sock >= 0) {
|
||||
return SendStream(client.tcp_sock, wrapped.data(), wrapped.size());
|
||||
}
|
||||
return udp_sock_ >= 0 && SendAll(udp_sock_, wrapped.data(), wrapped.size(),
|
||||
client.data_remote);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (client.tcp_sock >= 0) {
|
||||
return SendStream(client.tcp_sock, packet.data(), packet.size());
|
||||
}
|
||||
@@ -211,6 +243,15 @@ std::vector<uint8_t> GatewayKnxTpIpRouter::buildSupportedServiceDib() const {
|
||||
if (config_.multicast_enabled) {
|
||||
services.emplace_back(kKnxServiceFamilyRouting, 1);
|
||||
}
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
const bool secure_routing_ready =
|
||||
config_.oam_router.enabled && config_.oam_router.secure_routing_enabled &&
|
||||
oam_ip_secure_credentials_.activated &&
|
||||
oam_ip_secure_credentials_.backbone_key_available;
|
||||
if (secureCredentialsReady() || secure_routing_ready) {
|
||||
services.emplace_back(kKnxServiceFamilySecurity, 1);
|
||||
}
|
||||
#endif
|
||||
std::vector<uint8_t> dib(2 + services.size() * 2U, 0);
|
||||
dib[0] = static_cast<uint8_t>(dib.size());
|
||||
dib[1] = kKnxDibSupportedServices;
|
||||
@@ -399,7 +440,19 @@ void GatewayKnxTpIpRouter::sendRoutingIndication(const uint8_t* data, size_t len
|
||||
return;
|
||||
}
|
||||
KnxIpRoutingIndication routing(frame);
|
||||
const std::vector<uint8_t> packet(routing.data(), routing.data() + routing.totalLength());
|
||||
std::vector<uint8_t> packet(routing.data(), routing.data() + routing.totalLength());
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
if (config_.oam_router.enabled && config_.oam_router.secure_routing_enabled) {
|
||||
std::vector<uint8_t> secure_packet;
|
||||
if (wrapSecureRoutingPacket(packet, &secure_packet)) {
|
||||
packet = std::move(secure_packet);
|
||||
} else if (oam_ip_secure_credentials_.activated &&
|
||||
oam_ip_secure_credentials_.backbone_key_available) {
|
||||
ESP_LOGW(kTag, "failed to wrap KNXnet/IP Secure routing indication");
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const auto netifs = ActiveKnxNetifs();
|
||||
if (netifs.empty()) {
|
||||
SendAll(udp_sock_, packet.data(), packet.size(), remote);
|
||||
|
||||
@@ -184,7 +184,11 @@ void GatewayKnxTpIpRouter::handleRoutingIndication(const uint8_t* packet_data, s
|
||||
const uint8_t* cemi = frame.data();
|
||||
const size_t cemi_len = frame.dataLength();
|
||||
bool consumed_by_local_application = false;
|
||||
if (ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_)) {
|
||||
const bool addressed_to_oam =
|
||||
oam_router_ != nullptr && MatchesOamRouterLocalIndividualAddress(frame, *oam_router_);
|
||||
const bool addressed_to_reg1 =
|
||||
ets_device_ != nullptr && MatchesOpenKnxLocalIndividualAddress(frame, *ets_device_);
|
||||
if (addressed_to_oam || addressed_to_reg1) {
|
||||
std::vector<uint8_t> local_tunnel_frame;
|
||||
if (BuildLocalRoutingTunnelFrame(cemi, cemi_len, &local_tunnel_frame)) {
|
||||
consumed_by_local_application = handleOpenKnxTunnelFrame(
|
||||
@@ -300,11 +304,26 @@ GatewayKnxTpIpRouter::TunnelClient* GatewayKnxTpIpRouter::allocateTunnelClient(
|
||||
free_client->connection_type = connection_type;
|
||||
free_client->received_sequence = 255;
|
||||
free_client->send_sequence = 0;
|
||||
free_client->individual_address = effectiveTunnelAddressForSlot(free_index);
|
||||
const bool oam_router_persona =
|
||||
config_.oam_router.enabled && config_.oam_router.secure_tunnel_enabled &&
|
||||
active_secure_session_id_ != 0;
|
||||
if (oam_router_persona) {
|
||||
const uint16_t first = config_.oam_router.tunnel_address_base;
|
||||
const uint16_t line = first & 0xff00;
|
||||
uint16_t device = static_cast<uint16_t>((first & 0x00ff) + free_index);
|
||||
if (device == 0 || device > 0xff) {
|
||||
device = static_cast<uint16_t>(1 + free_index);
|
||||
}
|
||||
free_client->individual_address = static_cast<uint16_t>(line | (device & 0x00ff));
|
||||
} else {
|
||||
free_client->individual_address = effectiveTunnelAddressForSlot(free_index);
|
||||
}
|
||||
free_client->last_activity_tick = xTaskGetTickCount();
|
||||
free_client->control_remote = control_remote;
|
||||
free_client->data_remote = data_remote;
|
||||
free_client->tcp_sock = active_tcp_sock_;
|
||||
free_client->secure_session_id = active_secure_session_id_;
|
||||
free_client->oam_router_persona = oam_router_persona;
|
||||
last_tunnel_channel_id_ = channel_id;
|
||||
return free_client;
|
||||
}
|
||||
@@ -636,16 +655,18 @@ void GatewayKnxTpIpRouter::handleSecureService(uint16_t service, const uint8_t*
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
switch (service) {
|
||||
case kServiceSecureSessionRequest:
|
||||
handleSecureSessionRequest(body, len, remote);
|
||||
break;
|
||||
case kServiceSecureSessionAuth:
|
||||
ESP_LOGW(kTag, "KNXnet/IP Secure service 0x%04x rejected: secure sessions are not provisioned", service);
|
||||
ESP_LOGW(kTag, "KNXnet/IP Secure auth from %s rejected outside a secure wrapper",
|
||||
EndpointString(remote).c_str());
|
||||
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
|
||||
break;
|
||||
case kServiceSecureWrapper:
|
||||
ESP_LOGW(kTag, "KNXnet/IP Secure wrapper rejected: no authenticated secure session");
|
||||
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
|
||||
handleSecureWrapper(body, len, remote);
|
||||
break;
|
||||
case kServiceSecureGroupSync:
|
||||
ESP_LOGD(kTag, "KNXnet/IP Secure group sync ignored until secure routing is provisioned");
|
||||
handleSecureGroupSync(body, len, remote);
|
||||
break;
|
||||
default:
|
||||
ESP_LOGD(kTag, "KNXnet/IP Secure service 0x%04x ignored", service);
|
||||
|
||||
@@ -0,0 +1,780 @@
|
||||
#include "gateway_knx_private.hpp"
|
||||
|
||||
#include "esp_random.h"
|
||||
#include "mbedtls/aes.h"
|
||||
#include "mbedtls/ecp.h"
|
||||
#include "mbedtls/sha256.h"
|
||||
|
||||
namespace gateway {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kSecurePublicKeyLen = 32;
|
||||
constexpr size_t kSecureKeyLen = 16;
|
||||
constexpr size_t kSecureSeqLen = 6;
|
||||
constexpr size_t kSecureSerialLen = 6;
|
||||
constexpr size_t kSecureMacLen = 16;
|
||||
constexpr size_t kSecureWrapperBodyOverhead = 2 + kSecureSeqLen + kSecureSerialLen + 2 + kSecureMacLen;
|
||||
constexpr std::array<uint8_t, 16> kAuthCtrIv{0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0xff, 0x00};
|
||||
|
||||
int SecureRandom(void*, unsigned char* output, size_t len) {
|
||||
esp_fill_random(output, len);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void WriteSeq48(uint8_t* data, uint64_t value) {
|
||||
data[0] = static_cast<uint8_t>((value >> 40) & 0xff);
|
||||
data[1] = static_cast<uint8_t>((value >> 32) & 0xff);
|
||||
data[2] = static_cast<uint8_t>((value >> 24) & 0xff);
|
||||
data[3] = static_cast<uint8_t>((value >> 16) & 0xff);
|
||||
data[4] = static_cast<uint8_t>((value >> 8) & 0xff);
|
||||
data[5] = static_cast<uint8_t>(value & 0xff);
|
||||
}
|
||||
|
||||
uint64_t ReadSeq48(const uint8_t* data) {
|
||||
uint64_t value = 0;
|
||||
for (size_t index = 0; index < kSecureSeqLen; ++index) {
|
||||
value = (value << 8) | data[index];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::array<uint8_t, kSecureSerialLen> OamRouterSerial() {
|
||||
std::array<uint8_t, kSecureSerialLen> serial{};
|
||||
uint8_t mac[6]{};
|
||||
if (!ReadBaseMac(mac)) {
|
||||
return serial;
|
||||
}
|
||||
uint32_t suffix = (static_cast<uint32_t>(mac[2]) << 24) |
|
||||
(static_cast<uint32_t>(mac[3]) << 16) |
|
||||
(static_cast<uint32_t>(mac[4]) << 8) |
|
||||
static_cast<uint32_t>(mac[5]);
|
||||
suffix += knx_internal::kOamRouterSerialMacIncrement;
|
||||
serial[0] = static_cast<uint8_t>((knx_internal::kOamRouterManufacturerId >> 8) & 0xff);
|
||||
serial[1] = static_cast<uint8_t>(knx_internal::kOamRouterManufacturerId & 0xff);
|
||||
serial[2] = static_cast<uint8_t>((suffix >> 24) & 0xff);
|
||||
serial[3] = static_cast<uint8_t>((suffix >> 16) & 0xff);
|
||||
serial[4] = static_cast<uint8_t>((suffix >> 8) & 0xff);
|
||||
serial[5] = static_cast<uint8_t>(suffix & 0xff);
|
||||
return serial;
|
||||
}
|
||||
|
||||
bool AesCbcMac(const std::array<uint8_t, kSecureKeyLen>& key,
|
||||
const std::vector<uint8_t>& additional_data,
|
||||
const std::vector<uint8_t>& payload,
|
||||
const std::array<uint8_t, kSecureMacLen>& block0,
|
||||
std::array<uint8_t, kSecureMacLen>* mac) {
|
||||
if (mac == nullptr || additional_data.size() > 0xffff) {
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> blocks;
|
||||
blocks.reserve(block0.size() + 2 + additional_data.size() + payload.size() + kSecureMacLen);
|
||||
blocks.insert(blocks.end(), block0.begin(), block0.end());
|
||||
blocks.push_back(static_cast<uint8_t>((additional_data.size() >> 8) & 0xff));
|
||||
blocks.push_back(static_cast<uint8_t>(additional_data.size() & 0xff));
|
||||
blocks.insert(blocks.end(), additional_data.begin(), additional_data.end());
|
||||
blocks.insert(blocks.end(), payload.begin(), payload.end());
|
||||
const size_t remainder = blocks.size() % kSecureMacLen;
|
||||
if (remainder != 0) {
|
||||
blocks.insert(blocks.end(), kSecureMacLen - remainder, 0x00);
|
||||
}
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> iv{};
|
||||
mbedtls_aes_context aes;
|
||||
mbedtls_aes_init(&aes);
|
||||
const int set_key = mbedtls_aes_setkey_enc(&aes, key.data(), 128);
|
||||
int crypt = 0;
|
||||
if (set_key == 0) {
|
||||
crypt = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_ENCRYPT, blocks.size(),
|
||||
iv.data(), blocks.data(), blocks.data());
|
||||
}
|
||||
mbedtls_aes_free(&aes);
|
||||
if (set_key != 0 || crypt != 0) {
|
||||
return false;
|
||||
}
|
||||
std::copy(blocks.end() - kSecureMacLen, blocks.end(), mac->begin());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AesCtrTransform(const std::array<uint8_t, kSecureKeyLen>& key,
|
||||
const std::array<uint8_t, kSecureMacLen>& counter,
|
||||
const uint8_t* input, size_t len,
|
||||
std::vector<uint8_t>* output) {
|
||||
if (output == nullptr || (input == nullptr && len != 0)) {
|
||||
return false;
|
||||
}
|
||||
output->assign(len, 0);
|
||||
std::array<uint8_t, kSecureMacLen> nonce_counter = counter;
|
||||
std::array<uint8_t, kSecureMacLen> stream_block{};
|
||||
size_t nc_off = 0;
|
||||
mbedtls_aes_context aes;
|
||||
mbedtls_aes_init(&aes);
|
||||
const int set_key = mbedtls_aes_setkey_enc(&aes, key.data(), 128);
|
||||
int crypt = 0;
|
||||
if (set_key == 0 && len > 0) {
|
||||
crypt = mbedtls_aes_crypt_ctr(&aes, len, &nc_off, nonce_counter.data(),
|
||||
stream_block.data(), input, output->data());
|
||||
}
|
||||
mbedtls_aes_free(&aes);
|
||||
return set_key == 0 && crypt == 0;
|
||||
}
|
||||
|
||||
bool GenerateSessionKey(const std::array<uint8_t, kSecurePublicKeyLen>& client_public,
|
||||
std::array<uint8_t, kSecurePublicKeyLen>* server_public,
|
||||
std::array<uint8_t, kSecurePublicKeyLen>* shared_secret,
|
||||
std::array<uint8_t, kSecureKeyLen>* session_key) {
|
||||
if (server_public == nullptr || shared_secret == nullptr || session_key == nullptr) {
|
||||
return false;
|
||||
}
|
||||
mbedtls_ecp_group group;
|
||||
mbedtls_mpi private_key;
|
||||
mbedtls_ecp_point public_key;
|
||||
mbedtls_ecp_point peer_public_key;
|
||||
mbedtls_ecp_point shared_point;
|
||||
mbedtls_ecp_group_init(&group);
|
||||
mbedtls_mpi_init(&private_key);
|
||||
mbedtls_ecp_point_init(&public_key);
|
||||
mbedtls_ecp_point_init(&peer_public_key);
|
||||
mbedtls_ecp_point_init(&shared_point);
|
||||
|
||||
int ret = mbedtls_ecp_group_load(&group, MBEDTLS_ECP_DP_CURVE25519);
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecp_gen_privkey(&group, &private_key, SecureRandom, nullptr);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecp_mul(&group, &public_key, &private_key, &group.G, SecureRandom, nullptr);
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecp_point_read_binary(&group, &peer_public_key, client_public.data(),
|
||||
client_public.size());
|
||||
}
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecp_mul(&group, &shared_point, &private_key, &peer_public_key,
|
||||
SecureRandom, nullptr);
|
||||
}
|
||||
size_t public_len = 0;
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecp_point_write_binary(&group, &public_key, MBEDTLS_ECP_PF_UNCOMPRESSED,
|
||||
&public_len, server_public->data(),
|
||||
server_public->size());
|
||||
}
|
||||
size_t secret_len = 0;
|
||||
if (ret == 0) {
|
||||
ret = mbedtls_ecp_point_write_binary(&group, &shared_point, MBEDTLS_ECP_PF_UNCOMPRESSED,
|
||||
&secret_len, shared_secret->data(),
|
||||
shared_secret->size());
|
||||
}
|
||||
mbedtls_ecp_point_free(&shared_point);
|
||||
mbedtls_ecp_point_free(&peer_public_key);
|
||||
mbedtls_ecp_point_free(&public_key);
|
||||
mbedtls_mpi_free(&private_key);
|
||||
mbedtls_ecp_group_free(&group);
|
||||
if (ret != 0 || public_len != server_public->size() || secret_len != shared_secret->size()) {
|
||||
return false;
|
||||
}
|
||||
std::array<uint8_t, 32> digest{};
|
||||
if (mbedtls_sha256(shared_secret->data(), shared_secret->size(), digest.data(), 0) != 0) {
|
||||
return false;
|
||||
}
|
||||
std::copy(digest.begin(), digest.begin() + session_key->size(), session_key->begin());
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool GatewayKnxTpIpRouter::secureCredentialsReady() const {
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
return config_.oam_router.enabled && config_.oam_router.secure_tunnel_enabled &&
|
||||
oam_ip_secure_credentials_.activated &&
|
||||
!oam_ip_secure_credentials_.tunnel_user_keys.empty();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::findSecureSession(
|
||||
uint16_t session_id, const sockaddr_in& remote) {
|
||||
if (session_id == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
for (auto& session : secure_sessions_) {
|
||||
if (session.active && session.session_id == session_id &&
|
||||
EndpointEquals(session.remote, remote)) {
|
||||
return &session;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::findSecureSession(
|
||||
uint16_t session_id, const sockaddr_in& remote) const {
|
||||
if (session_id == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
for (const auto& session : secure_sessions_) {
|
||||
if (session.active && session.session_id == session_id &&
|
||||
EndpointEquals(session.remote, remote)) {
|
||||
return &session;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::allocateSecureSession(
|
||||
const sockaddr_in& remote) {
|
||||
SecureSession* slot = nullptr;
|
||||
for (auto& session : secure_sessions_) {
|
||||
if (session.active && EndpointEquals(session.remote, remote)) {
|
||||
slot = &session;
|
||||
break;
|
||||
}
|
||||
if (!session.active && slot == nullptr) {
|
||||
slot = &session;
|
||||
}
|
||||
}
|
||||
if (slot == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
*slot = SecureSession{};
|
||||
slot->active = true;
|
||||
slot->remote = remote;
|
||||
slot->tcp_sock = active_tcp_sock_;
|
||||
for (uint32_t attempt = 0; attempt < 0xffff; ++attempt) {
|
||||
last_secure_session_id_ = static_cast<uint16_t>(last_secure_session_id_ + 1);
|
||||
if (last_secure_session_id_ == 0) {
|
||||
last_secure_session_id_ = 1;
|
||||
}
|
||||
if (findSecureSession(last_secure_session_id_, remote) == nullptr) {
|
||||
slot->session_id = last_secure_session_id_;
|
||||
break;
|
||||
}
|
||||
}
|
||||
slot->last_activity_tick = xTaskGetTickCount();
|
||||
return slot->session_id == 0 ? nullptr : slot;
|
||||
}
|
||||
|
||||
GatewayKnxTpIpRouter::SecureSession* GatewayKnxTpIpRouter::activeSecureSession() {
|
||||
if (active_secure_session_id_ == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
for (auto& session : secure_sessions_) {
|
||||
if (session.active && session.session_id == active_secure_session_id_) {
|
||||
return &session;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::closeSecureSessionsForTcp(int sock) {
|
||||
if (sock < 0) {
|
||||
return;
|
||||
}
|
||||
for (auto& session : secure_sessions_) {
|
||||
if (session.active && session.tcp_sock == sock) {
|
||||
session = SecureSession{};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::wrapSecurePacket(SecureSession& session,
|
||||
const std::vector<uint8_t>& inner,
|
||||
std::vector<uint8_t>* wrapped) {
|
||||
if (wrapped == nullptr || inner.empty() || inner.size() > 0xffff) {
|
||||
return false;
|
||||
}
|
||||
const size_t total_len = kKnxNetIpHeaderSize + kSecureWrapperBodyOverhead + inner.size();
|
||||
if (total_len > 0xffff) {
|
||||
return false;
|
||||
}
|
||||
const auto serial = OamRouterSerial();
|
||||
std::array<uint8_t, kSecureSeqLen> sequence{};
|
||||
WriteSeq48(sequence.data(), session.send_sequence++ & 0xffffffffffffULL);
|
||||
std::array<uint8_t, 2> tag{0x00, 0x00};
|
||||
|
||||
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
|
||||
header[0] = kKnxNetIpHeaderSize;
|
||||
header[1] = kKnxNetIpVersion10;
|
||||
WriteBe16(header.data() + 2, kServiceSecureWrapper);
|
||||
WriteBe16(header.data() + 4, static_cast<uint16_t>(total_len));
|
||||
|
||||
std::vector<uint8_t> additional = header;
|
||||
additional.push_back(static_cast<uint8_t>((session.session_id >> 8) & 0xff));
|
||||
additional.push_back(static_cast<uint8_t>(session.session_id & 0xff));
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> block0{};
|
||||
std::copy(sequence.begin(), sequence.end(), block0.begin());
|
||||
std::copy(serial.begin(), serial.end(), block0.begin() + kSecureSeqLen);
|
||||
block0[12] = tag[0];
|
||||
block0[13] = tag[1];
|
||||
WriteBe16(block0.data() + 14, static_cast<uint16_t>(inner.size()));
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> mac_cbc{};
|
||||
if (!AesCbcMac(session.session_key, additional, inner, block0, &mac_cbc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> counter{};
|
||||
std::copy(sequence.begin(), sequence.end(), counter.begin());
|
||||
std::copy(serial.begin(), serial.end(), counter.begin() + kSecureSeqLen);
|
||||
counter[12] = tag[0];
|
||||
counter[13] = tag[1];
|
||||
counter[14] = 0xff;
|
||||
counter[15] = 0x00;
|
||||
|
||||
std::vector<uint8_t> enc_mac;
|
||||
if (!AesCtrTransform(session.session_key, counter, mac_cbc.data(), mac_cbc.size(),
|
||||
&enc_mac)) {
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> enc_payload;
|
||||
if (!AesCtrTransform(session.session_key, counter, inner.data(), inner.size(),
|
||||
&enc_payload)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wrapped->clear();
|
||||
wrapped->reserve(total_len);
|
||||
wrapped->insert(wrapped->end(), header.begin(), header.end());
|
||||
wrapped->push_back(static_cast<uint8_t>((session.session_id >> 8) & 0xff));
|
||||
wrapped->push_back(static_cast<uint8_t>(session.session_id & 0xff));
|
||||
wrapped->insert(wrapped->end(), sequence.begin(), sequence.end());
|
||||
wrapped->insert(wrapped->end(), serial.begin(), serial.end());
|
||||
wrapped->insert(wrapped->end(), tag.begin(), tag.end());
|
||||
wrapped->insert(wrapped->end(), enc_payload.begin(), enc_payload.end());
|
||||
wrapped->insert(wrapped->end(), enc_mac.begin(), enc_mac.end());
|
||||
session.last_activity_tick = xTaskGetTickCount();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::wrapSecureRoutingPacket(const std::vector<uint8_t>& inner,
|
||||
std::vector<uint8_t>* wrapped) {
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
if (wrapped == nullptr || inner.empty() || inner.size() > 0xffff ||
|
||||
!config_.oam_router.enabled || !config_.oam_router.secure_routing_enabled ||
|
||||
!oam_ip_secure_credentials_.activated ||
|
||||
!oam_ip_secure_credentials_.backbone_key_available) {
|
||||
return false;
|
||||
}
|
||||
const size_t total_len = kKnxNetIpHeaderSize + kSecureWrapperBodyOverhead + inner.size();
|
||||
if (total_len > 0xffff) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t routing_session_id = 0;
|
||||
const auto serial = OamRouterSerial();
|
||||
const uint64_t sequence_value = oam_ip_secure_credentials_.routing_sequence;
|
||||
std::array<uint8_t, kSecureSeqLen> sequence{};
|
||||
WriteSeq48(sequence.data(), sequence_value & 0xffffffffffffULL);
|
||||
std::array<uint8_t, 2> tag{0x00, 0x00};
|
||||
|
||||
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
|
||||
header[0] = kKnxNetIpHeaderSize;
|
||||
header[1] = kKnxNetIpVersion10;
|
||||
WriteBe16(header.data() + 2, kServiceSecureWrapper);
|
||||
WriteBe16(header.data() + 4, static_cast<uint16_t>(total_len));
|
||||
|
||||
std::vector<uint8_t> additional = header;
|
||||
additional.push_back(0x00);
|
||||
additional.push_back(0x00);
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> block0{};
|
||||
std::copy(sequence.begin(), sequence.end(), block0.begin());
|
||||
std::copy(serial.begin(), serial.end(), block0.begin() + kSecureSeqLen);
|
||||
block0[12] = tag[0];
|
||||
block0[13] = tag[1];
|
||||
WriteBe16(block0.data() + 14, static_cast<uint16_t>(inner.size()));
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> mac_cbc{};
|
||||
if (!AesCbcMac(oam_ip_secure_credentials_.backbone_key, additional, inner, block0,
|
||||
&mac_cbc)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> counter{};
|
||||
std::copy(sequence.begin(), sequence.end(), counter.begin());
|
||||
std::copy(serial.begin(), serial.end(), counter.begin() + kSecureSeqLen);
|
||||
counter[12] = tag[0];
|
||||
counter[13] = tag[1];
|
||||
counter[14] = 0xff;
|
||||
counter[15] = 0x00;
|
||||
|
||||
std::vector<uint8_t> enc_mac;
|
||||
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter,
|
||||
mac_cbc.data(), mac_cbc.size(), &enc_mac)) {
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> enc_payload;
|
||||
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter,
|
||||
inner.data(), inner.size(), &enc_payload)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
wrapped->clear();
|
||||
wrapped->reserve(total_len);
|
||||
wrapped->insert(wrapped->end(), header.begin(), header.end());
|
||||
wrapped->push_back(static_cast<uint8_t>((routing_session_id >> 8) & 0xff));
|
||||
wrapped->push_back(static_cast<uint8_t>(routing_session_id & 0xff));
|
||||
wrapped->insert(wrapped->end(), sequence.begin(), sequence.end());
|
||||
wrapped->insert(wrapped->end(), serial.begin(), serial.end());
|
||||
wrapped->insert(wrapped->end(), tag.begin(), tag.end());
|
||||
wrapped->insert(wrapped->end(), enc_payload.begin(), enc_payload.end());
|
||||
wrapped->insert(wrapped->end(), enc_mac.begin(), enc_mac.end());
|
||||
oam_ip_secure_credentials_.routing_sequence = sequence_value + 1;
|
||||
if (routing_sequence_store_handler_) {
|
||||
routing_sequence_store_handler_(oam_ip_secure_credentials_.routing_sequence);
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
(void)inner;
|
||||
(void)wrapped;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::decryptSecureWrapper(SecureSession& session,
|
||||
const uint8_t* body, size_t len,
|
||||
std::vector<uint8_t>* inner) {
|
||||
if (body == nullptr || inner == nullptr || len < kSecureWrapperBodyOverhead) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t session_id = ReadBe16(body);
|
||||
if (session_id != session.session_id) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t* sequence = body + 2;
|
||||
const uint8_t* serial = body + 8;
|
||||
const uint8_t* tag = body + 14;
|
||||
const uint8_t* encrypted_payload = body + 16;
|
||||
const size_t encrypted_payload_len = len - kSecureWrapperBodyOverhead;
|
||||
const uint8_t* encrypted_mac = body + len - kSecureMacLen;
|
||||
|
||||
const uint64_t incoming_sequence = ReadSeq48(sequence);
|
||||
if (incoming_sequence < session.receive_sequence) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> counter{};
|
||||
std::copy(sequence, sequence + kSecureSeqLen, counter.begin());
|
||||
std::copy(serial, serial + kSecureSerialLen, counter.begin() + kSecureSeqLen);
|
||||
counter[12] = tag[0];
|
||||
counter[13] = tag[1];
|
||||
counter[14] = 0xff;
|
||||
counter[15] = 0x00;
|
||||
|
||||
std::vector<uint8_t> mac_tr;
|
||||
if (!AesCtrTransform(session.session_key, counter, encrypted_mac, kSecureMacLen, &mac_tr)) {
|
||||
return false;
|
||||
}
|
||||
if (!AesCtrTransform(session.session_key, counter, encrypted_payload,
|
||||
encrypted_payload_len, inner)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
|
||||
header[0] = kKnxNetIpHeaderSize;
|
||||
header[1] = kKnxNetIpVersion10;
|
||||
WriteBe16(header.data() + 2, kServiceSecureWrapper);
|
||||
WriteBe16(header.data() + 4, static_cast<uint16_t>(kKnxNetIpHeaderSize + len));
|
||||
std::vector<uint8_t> additional = header;
|
||||
additional.push_back(static_cast<uint8_t>((session_id >> 8) & 0xff));
|
||||
additional.push_back(static_cast<uint8_t>(session_id & 0xff));
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> block0{};
|
||||
std::copy(sequence, sequence + kSecureSeqLen, block0.begin());
|
||||
std::copy(serial, serial + kSecureSerialLen, block0.begin() + kSecureSeqLen);
|
||||
block0[12] = tag[0];
|
||||
block0[13] = tag[1];
|
||||
WriteBe16(block0.data() + 14, static_cast<uint16_t>(inner->size()));
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> mac_cbc{};
|
||||
if (!AesCbcMac(session.session_key, additional, *inner, block0, &mac_cbc) ||
|
||||
mac_tr.size() != mac_cbc.size() ||
|
||||
!std::equal(mac_cbc.begin(), mac_cbc.end(), mac_tr.begin())) {
|
||||
return false;
|
||||
}
|
||||
session.receive_sequence = incoming_sequence + 1;
|
||||
session.last_activity_tick = xTaskGetTickCount();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::decryptSecureRoutingWrapper(const uint8_t* body,
|
||||
size_t len,
|
||||
std::vector<uint8_t>* inner) {
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
if (body == nullptr || inner == nullptr || len < kSecureWrapperBodyOverhead ||
|
||||
!config_.oam_router.enabled || !config_.oam_router.secure_routing_enabled ||
|
||||
!oam_ip_secure_credentials_.activated ||
|
||||
!oam_ip_secure_credentials_.backbone_key_available) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t session_id = ReadBe16(body);
|
||||
if (session_id != 0) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t* sequence = body + 2;
|
||||
const uint8_t* serial = body + 8;
|
||||
const uint8_t* tag = body + 14;
|
||||
const uint8_t* encrypted_payload = body + 16;
|
||||
const size_t encrypted_payload_len = len - kSecureWrapperBodyOverhead;
|
||||
const uint8_t* encrypted_mac = body + len - kSecureMacLen;
|
||||
const uint64_t incoming_sequence = ReadSeq48(sequence);
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> counter{};
|
||||
std::copy(sequence, sequence + kSecureSeqLen, counter.begin());
|
||||
std::copy(serial, serial + kSecureSerialLen, counter.begin() + kSecureSeqLen);
|
||||
counter[12] = tag[0];
|
||||
counter[13] = tag[1];
|
||||
counter[14] = 0xff;
|
||||
counter[15] = 0x00;
|
||||
|
||||
std::vector<uint8_t> mac_tr;
|
||||
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter,
|
||||
encrypted_mac, kSecureMacLen, &mac_tr)) {
|
||||
return false;
|
||||
}
|
||||
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter,
|
||||
encrypted_payload, encrypted_payload_len, inner)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
|
||||
header[0] = kKnxNetIpHeaderSize;
|
||||
header[1] = kKnxNetIpVersion10;
|
||||
WriteBe16(header.data() + 2, kServiceSecureWrapper);
|
||||
WriteBe16(header.data() + 4, static_cast<uint16_t>(kKnxNetIpHeaderSize + len));
|
||||
std::vector<uint8_t> additional = header;
|
||||
additional.push_back(0x00);
|
||||
additional.push_back(0x00);
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> block0{};
|
||||
std::copy(sequence, sequence + kSecureSeqLen, block0.begin());
|
||||
std::copy(serial, serial + kSecureSerialLen, block0.begin() + kSecureSeqLen);
|
||||
block0[12] = tag[0];
|
||||
block0[13] = tag[1];
|
||||
WriteBe16(block0.data() + 14, static_cast<uint16_t>(inner->size()));
|
||||
|
||||
std::array<uint8_t, kSecureMacLen> mac_cbc{};
|
||||
if (!AesCbcMac(oam_ip_secure_credentials_.backbone_key, additional, *inner, block0,
|
||||
&mac_cbc) ||
|
||||
mac_tr.size() != mac_cbc.size() ||
|
||||
!std::equal(mac_cbc.begin(), mac_cbc.end(), mac_tr.begin())) {
|
||||
return false;
|
||||
}
|
||||
oam_ip_secure_credentials_.routing_sequence =
|
||||
std::max(oam_ip_secure_credentials_.routing_sequence, incoming_sequence + 1);
|
||||
if (routing_sequence_store_handler_) {
|
||||
routing_sequence_store_handler_(oam_ip_secure_credentials_.routing_sequence);
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
(void)body;
|
||||
(void)len;
|
||||
(void)inner;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool GatewayKnxTpIpRouter::verifySecureSessionAuth(SecureSession& session,
|
||||
const uint8_t* packet,
|
||||
size_t len, uint8_t* status) {
|
||||
if (status != nullptr) {
|
||||
*status = kKnxSecureStatusAuthFailed;
|
||||
}
|
||||
uint16_t service = 0;
|
||||
uint16_t total_len = 0;
|
||||
if (!ParseKnxNetIpHeader(packet, len, &service, &total_len) ||
|
||||
service != kServiceSecureSessionAuth || total_len < kKnxNetIpHeaderSize + 18) {
|
||||
return false;
|
||||
}
|
||||
const uint8_t user_id = packet[7];
|
||||
const uint8_t* received_mac = packet + 8;
|
||||
const std::array<uint8_t, kSecureKeyLen>* user_key = nullptr;
|
||||
if (user_id > 0 && user_id - 1 < oam_ip_secure_credentials_.tunnel_user_keys.size()) {
|
||||
user_key = &oam_ip_secure_credentials_.tunnel_user_keys[user_id - 1];
|
||||
} else if (oam_ip_secure_credentials_.tunnel_user_keys.size() == 1) {
|
||||
user_key = &oam_ip_secure_credentials_.tunnel_user_keys.front();
|
||||
}
|
||||
if (user_key == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> additional;
|
||||
additional.reserve(6 + 2 + kSecurePublicKeyLen);
|
||||
additional.insert(additional.end(), packet, packet + 6);
|
||||
additional.push_back(packet[6]);
|
||||
additional.push_back(user_id);
|
||||
for (size_t index = 0; index < kSecurePublicKeyLen; ++index) {
|
||||
additional.push_back(session.client_public_key[index] ^ session.server_public_key[index]);
|
||||
}
|
||||
std::array<uint8_t, kSecureMacLen> block0{};
|
||||
std::array<uint8_t, kSecureMacLen> mac_cbc{};
|
||||
if (!AesCbcMac(*user_key, additional, {}, block0, &mac_cbc)) {
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> transformed_mac;
|
||||
if (!AesCtrTransform(*user_key, kAuthCtrIv, mac_cbc.data(), mac_cbc.size(),
|
||||
&transformed_mac) ||
|
||||
transformed_mac.size() != kSecureMacLen ||
|
||||
!std::equal(transformed_mac.begin(), transformed_mac.end(), received_mac)) {
|
||||
return false;
|
||||
}
|
||||
session.authenticated = true;
|
||||
session.user_id = user_id;
|
||||
if (status != nullptr) {
|
||||
*status = kKnxNoError;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::handleSecureSessionRequest(const uint8_t* body,
|
||||
size_t len,
|
||||
const sockaddr_in& remote) {
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
if (!secureCredentialsReady()) {
|
||||
ESP_LOGW(kTag, "reject KNXnet/IP Secure session from %s: OAM IP Secure credentials are not active",
|
||||
EndpointString(remote).c_str());
|
||||
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
|
||||
return;
|
||||
}
|
||||
if (body == nullptr || len < 8 + kSecurePublicKeyLen) {
|
||||
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
|
||||
return;
|
||||
}
|
||||
SecureSession* session = allocateSecureSession(remote);
|
||||
if (session == nullptr) {
|
||||
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
|
||||
return;
|
||||
}
|
||||
std::copy(body + 8, body + 8 + kSecurePublicKeyLen, session->client_public_key.begin());
|
||||
if (!GenerateSessionKey(session->client_public_key, &session->server_public_key,
|
||||
&session->shared_secret, &session->session_key)) {
|
||||
*session = SecureSession{};
|
||||
sendSecureSessionStatus(kKnxSecureStatusAuthFailed, remote);
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> response_body;
|
||||
response_body.reserve(2 + kSecurePublicKeyLen);
|
||||
response_body.push_back(static_cast<uint8_t>((session->session_id >> 8) & 0xff));
|
||||
response_body.push_back(static_cast<uint8_t>(session->session_id & 0xff));
|
||||
response_body.insert(response_body.end(), session->server_public_key.begin(),
|
||||
session->server_public_key.end());
|
||||
sendPacket(OpenKnxIpPacket(kServiceSecureSessionResponse, response_body), remote);
|
||||
ESP_LOGI(kTag, "accepted KNXnet/IP Secure session request sid=%u from %s",
|
||||
static_cast<unsigned>(session->session_id), EndpointString(remote).c_str());
|
||||
#else
|
||||
(void)body;
|
||||
(void)len;
|
||||
(void)remote;
|
||||
#endif
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::handleSecureWrapper(const uint8_t* body, size_t len,
|
||||
const sockaddr_in& remote) {
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
if (body == nullptr || len < kSecureWrapperBodyOverhead) {
|
||||
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
|
||||
return;
|
||||
}
|
||||
const uint16_t session_id = ReadBe16(body);
|
||||
if (session_id == 0) {
|
||||
std::vector<uint8_t> inner;
|
||||
if (!decryptSecureRoutingWrapper(body, len, &inner)) {
|
||||
return;
|
||||
}
|
||||
handleUdpDatagram(inner.data(), inner.size(), remote);
|
||||
return;
|
||||
}
|
||||
SecureSession* session = findSecureSession(session_id, remote);
|
||||
if (session == nullptr) {
|
||||
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> inner;
|
||||
if (!decryptSecureWrapper(*session, body, len, &inner)) {
|
||||
sendSecureSessionStatus(0x04, remote);
|
||||
return;
|
||||
}
|
||||
uint16_t service = 0;
|
||||
uint16_t total_len = 0;
|
||||
if (!ParseKnxNetIpHeader(inner.data(), inner.size(), &service, &total_len)) {
|
||||
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
|
||||
return;
|
||||
}
|
||||
const uint16_t previous_session = active_secure_session_id_;
|
||||
active_secure_session_id_ = session->session_id;
|
||||
if (service == kServiceSecureSessionAuth) {
|
||||
uint8_t status = kKnxSecureStatusAuthFailed;
|
||||
verifySecureSessionAuth(*session, inner.data(), inner.size(), &status);
|
||||
sendSecureSessionStatus(status, remote);
|
||||
active_secure_session_id_ = previous_session;
|
||||
return;
|
||||
}
|
||||
if (!session->authenticated) {
|
||||
sendSecureSessionStatus(kKnxSecureStatusUnauthenticated, remote);
|
||||
active_secure_session_id_ = previous_session;
|
||||
return;
|
||||
}
|
||||
handleUdpDatagram(inner.data(), inner.size(), remote);
|
||||
active_secure_session_id_ = previous_session;
|
||||
#else
|
||||
(void)body;
|
||||
(void)len;
|
||||
(void)remote;
|
||||
#endif
|
||||
}
|
||||
|
||||
void GatewayKnxTpIpRouter::handleSecureGroupSync(const uint8_t* body, size_t len,
|
||||
const sockaddr_in& remote) {
|
||||
#if defined(CONFIG_GATEWAY_KNX_IP_SECURE_SUPPORTED)
|
||||
if (body == nullptr || len < kSecureSeqLen + kSecureSerialLen + 2 + kSecureMacLen ||
|
||||
!oam_ip_secure_credentials_.activated ||
|
||||
!oam_ip_secure_credentials_.backbone_key_available) {
|
||||
return;
|
||||
}
|
||||
const uint8_t* sequence = body;
|
||||
const uint8_t* serial = body + kSecureSeqLen;
|
||||
const uint8_t* tag = body + kSecureSeqLen + kSecureSerialLen;
|
||||
const uint8_t* encrypted_mac = tag + 2;
|
||||
std::array<uint8_t, kSecureMacLen> counter{};
|
||||
std::copy(sequence, sequence + kSecureSeqLen, counter.begin());
|
||||
std::copy(serial, serial + kSecureSerialLen, counter.begin() + kSecureSeqLen);
|
||||
counter[12] = tag[0];
|
||||
counter[13] = tag[1];
|
||||
counter[14] = 0xff;
|
||||
counter[15] = 0x00;
|
||||
std::vector<uint8_t> mac_tr;
|
||||
if (!AesCtrTransform(oam_ip_secure_credentials_.backbone_key, counter, encrypted_mac,
|
||||
kSecureMacLen, &mac_tr)) {
|
||||
return;
|
||||
}
|
||||
std::vector<uint8_t> header(kKnxNetIpHeaderSize, 0);
|
||||
header[0] = kKnxNetIpHeaderSize;
|
||||
header[1] = kKnxNetIpVersion10;
|
||||
WriteBe16(header.data() + 2, kServiceSecureGroupSync);
|
||||
WriteBe16(header.data() + 4, static_cast<uint16_t>(kKnxNetIpHeaderSize + len));
|
||||
std::array<uint8_t, kSecureMacLen> block0{};
|
||||
std::copy(sequence, sequence + kSecureSeqLen, block0.begin());
|
||||
std::copy(serial, serial + kSecureSerialLen, block0.begin() + kSecureSeqLen);
|
||||
block0[12] = tag[0];
|
||||
block0[13] = tag[1];
|
||||
std::array<uint8_t, kSecureMacLen> mac_cbc{};
|
||||
if (AesCbcMac(oam_ip_secure_credentials_.backbone_key, header, {}, block0, &mac_cbc) &&
|
||||
mac_tr.size() == mac_cbc.size() &&
|
||||
std::equal(mac_cbc.begin(), mac_cbc.end(), mac_tr.begin())) {
|
||||
oam_ip_secure_credentials_.routing_sequence =
|
||||
std::max(oam_ip_secure_credentials_.routing_sequence, ReadSeq48(sequence) + 1);
|
||||
if (routing_sequence_store_handler_) {
|
||||
routing_sequence_store_handler_(oam_ip_secure_credentials_.routing_sequence);
|
||||
}
|
||||
ESP_LOGD(kTag, "authenticated KNXnet/IP Secure group sync from %s",
|
||||
EndpointString(remote).c_str());
|
||||
}
|
||||
#else
|
||||
(void)body;
|
||||
(void)len;
|
||||
(void)remote;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace gateway
|
||||
@@ -0,0 +1,278 @@
|
||||
#include "oam_router_runtime.h"
|
||||
|
||||
#include "gateway_knx_internal.h"
|
||||
|
||||
#include "esp_log.h"
|
||||
#include "esp_mac.h"
|
||||
#include "knx/cemi_server.h"
|
||||
#include "knx/property.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <utility>
|
||||
|
||||
namespace gateway::openknx {
|
||||
namespace {
|
||||
|
||||
constexpr uint16_t kInvalidIndividualAddress = 0xffff;
|
||||
constexpr uint16_t kKnxUnconfiguredBroadcastAddress = 0xffff;
|
||||
|
||||
bool IsUsableIndividualAddress(uint16_t address) {
|
||||
return address != 0 && address != kInvalidIndividualAddress;
|
||||
}
|
||||
|
||||
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) {
|
||||
return 0;
|
||||
}
|
||||
uint32_t suffix = (static_cast<uint32_t>(mac[2]) << 24) |
|
||||
(static_cast<uint32_t>(mac[3]) << 16) |
|
||||
(static_cast<uint32_t>(mac[4]) << 8) |
|
||||
static_cast<uint32_t>(mac[5]);
|
||||
return suffix + knx_internal::kOamRouterSerialMacIncrement;
|
||||
}
|
||||
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
void ApplyOamRouterIdentity(Bau091A& device) {
|
||||
device.deviceObject().manufacturerId(knx_internal::kOamRouterManufacturerId);
|
||||
device.deviceObject().bauNumber(OamBauNumberFromBaseMac());
|
||||
device.deviceObject().hardwareType(knx_internal::kOamRouterHardwareType);
|
||||
device.deviceObject().orderNumber(knx_internal::kOamRouterOrderNumber);
|
||||
if (auto* property = device.parameters().property(PID_PROG_VERSION); property != nullptr) {
|
||||
property->write(knx_internal::kOamRouterProgramVersion);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
OamRouterRuntime::OamRouterRuntime(std::string nvs_namespace,
|
||||
uint16_t fallback_individual_address,
|
||||
uint16_t tunnel_client_address)
|
||||
: nvs_namespace_(std::move(nvs_namespace))
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
,
|
||||
platform_(nullptr, nvs_namespace_.c_str()),
|
||||
device_(platform_)
|
||||
#endif
|
||||
{
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
platform_.outboundCemiFrameCallback(&OamRouterRuntime::HandleOutboundCemiFrame, this);
|
||||
ApplyOamRouterIdentity(device_);
|
||||
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();
|
||||
ApplyOamRouterIdentity(device_);
|
||||
if (!IsUsableIndividualAddress(device_.deviceObject().individualAddress()) &&
|
||||
IsUsableIndividualAddress(fallback_individual_address)) {
|
||||
device_.deviceObject().individualAddress(fallback_individual_address);
|
||||
}
|
||||
#ifdef USE_CEMI_SERVER
|
||||
if (auto* server = device_.getCemiServer()) {
|
||||
server->clientAddress(IsUsableIndividualAddress(tunnel_client_address)
|
||||
? tunnel_client_address
|
||||
: DefaultTunnelClientAddress(
|
||||
device_.deviceObject().individualAddress()));
|
||||
server->deviceAddressPropertiesTargetClient(false);
|
||||
server->tunnelFrameCallback(&OamRouterRuntime::EmitTunnelFrame, this);
|
||||
}
|
||||
#endif
|
||||
uint8_t program_version[5]{};
|
||||
if (auto* property = device_.parameters().property(PID_PROG_VERSION); property != nullptr) {
|
||||
property->read(program_version);
|
||||
}
|
||||
ESP_LOGI("gateway_knx",
|
||||
"OAM router runtime namespace=%s configured=%d manufacturer=0x%04x mask=0x%04x device=0x%04x tunnelClient=0x%04x progVersion=%02X %02X %02X %02X %02X",
|
||||
nvs_namespace_.c_str(), device_.configured(), device_.deviceObject().manufacturerId(),
|
||||
device_.deviceObject().maskVersion(), device_.deviceObject().individualAddress(),
|
||||
tunnelClientAddress(), program_version[0], program_version[1], program_version[2],
|
||||
program_version[3], program_version[4]);
|
||||
#else
|
||||
(void)fallback_individual_address;
|
||||
(void)tunnel_client_address;
|
||||
#endif
|
||||
}
|
||||
|
||||
OamRouterRuntime::~OamRouterRuntime() {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
platform_.outboundCemiFrameCallback(nullptr, nullptr);
|
||||
#ifdef USE_CEMI_SERVER
|
||||
if (auto* server = device_.getCemiServer()) {
|
||||
server->tunnelFrameCallback(nullptr, nullptr);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OamRouterRuntime::available() const {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t OamRouterRuntime::individualAddress() const {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
return const_cast<Bau091A&>(device_).deviceObject().individualAddress();
|
||||
#else
|
||||
return 0xffff;
|
||||
#endif
|
||||
}
|
||||
|
||||
uint16_t OamRouterRuntime::tunnelClientAddress() const {
|
||||
#if defined(ENABLE_BAU091A_PERSONA) && defined(USE_CEMI_SERVER)
|
||||
if (auto* server = const_cast<Bau091A&>(device_).getCemiServer()) {
|
||||
return server->clientAddress();
|
||||
}
|
||||
#endif
|
||||
return DefaultTunnelClientAddress(individualAddress());
|
||||
}
|
||||
|
||||
bool OamRouterRuntime::configured() const {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
return const_cast<Bau091A&>(device_).configured();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OamRouterRuntime::programmingMode() const {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
return const_cast<Bau091A&>(device_).deviceObject().progMode();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void OamRouterRuntime::setProgrammingMode(bool enabled) {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
device_.deviceObject().progMode(enabled);
|
||||
#else
|
||||
(void)enabled;
|
||||
#endif
|
||||
}
|
||||
|
||||
void OamRouterRuntime::toggleProgrammingMode() { setProgrammingMode(!programmingMode()); }
|
||||
|
||||
EtsMemorySnapshot OamRouterRuntime::snapshot() const {
|
||||
EtsMemorySnapshot out;
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
auto& device = const_cast<Bau091A&>(device_);
|
||||
out.configured = device.configured();
|
||||
out.individual_address = device.deviceObject().individualAddress();
|
||||
#endif
|
||||
return out;
|
||||
}
|
||||
|
||||
DeviceObject* OamRouterRuntime::deviceObject() {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
return &device_.deviceObject();
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
Platform* OamRouterRuntime::platform() {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
return &platform_;
|
||||
#else
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void OamRouterRuntime::setNetworkInterface(esp_netif_t* netif) {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
platform_.networkInterface(netif);
|
||||
#else
|
||||
(void)netif;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OamRouterRuntime::handleTunnelFrame(const uint8_t* data, size_t len,
|
||||
CemiFrameSender sender) {
|
||||
#if defined(ENABLE_BAU091A_PERSONA) && defined(USE_CEMI_SERVER)
|
||||
auto* server = device_.getCemiServer();
|
||||
if (server == nullptr || data == nullptr || len < 2) {
|
||||
return false;
|
||||
}
|
||||
std::vector<uint8_t> frame_data(data, data + len);
|
||||
CemiFrame frame(frame_data.data(), static_cast<uint16_t>(frame_data.size()));
|
||||
if (!shouldConsumeTunnelFrame(frame)) {
|
||||
return false;
|
||||
}
|
||||
sender_ = std::move(sender);
|
||||
server->frameReceived(frame);
|
||||
loop();
|
||||
sender_ = nullptr;
|
||||
return true;
|
||||
#else
|
||||
(void)data;
|
||||
(void)len;
|
||||
(void)sender;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void OamRouterRuntime::loop() {
|
||||
#if defined(ENABLE_BAU091A_PERSONA)
|
||||
device_.loop();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OamRouterRuntime::HandleOutboundCemiFrame(CemiFrame& frame, void* context) {
|
||||
auto* self = static_cast<OamRouterRuntime*>(context);
|
||||
if (self == nullptr || !self->sender_) {
|
||||
return false;
|
||||
}
|
||||
self->sender_(frame.data(), frame.dataLength());
|
||||
return true;
|
||||
}
|
||||
|
||||
void OamRouterRuntime::EmitTunnelFrame(CemiFrame& frame, void* context) {
|
||||
auto* self = static_cast<OamRouterRuntime*>(context);
|
||||
if (self == nullptr || !self->sender_) {
|
||||
return;
|
||||
}
|
||||
self->sender_(frame.data(), frame.dataLength());
|
||||
}
|
||||
|
||||
uint16_t OamRouterRuntime::DefaultTunnelClientAddress(uint16_t individual_address) {
|
||||
if (!IsUsableIndividualAddress(individual_address)) {
|
||||
return 0x1102;
|
||||
}
|
||||
const uint16_t line_base = individual_address & 0xff00;
|
||||
uint16_t device = static_cast<uint16_t>((individual_address & 0x00ff) + 1);
|
||||
if (device == 0 || device > 0xff) {
|
||||
device = 2;
|
||||
}
|
||||
return static_cast<uint16_t>(line_base | device);
|
||||
}
|
||||
|
||||
bool OamRouterRuntime::shouldConsumeTunnelFrame(CemiFrame& frame) const {
|
||||
switch (frame.messageCode()) {
|
||||
case M_PropRead_req:
|
||||
case M_PropWrite_req:
|
||||
case M_Reset_req:
|
||||
case M_FuncPropCommand_req:
|
||||
case M_FuncPropStateRead_req:
|
||||
return true;
|
||||
case L_data_req: {
|
||||
const uint16_t dest = frame.destinationAddress();
|
||||
const bool commissioning = !configured() || programmingMode();
|
||||
if (frame.addressType() == IndividualAddress) {
|
||||
return dest == individualAddress() || dest == tunnelClientAddress() ||
|
||||
(commissioning && dest == kKnxUnconfiguredBroadcastAddress);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gateway::openknx
|
||||
Reference in New Issue
Block a user