Move command registry from components/command/ into components/core/. New modules: mod_canbus, mod_honeypot, mod_fallback, mod_redteam, mod_ota. Replace mod_proxy with tun_core (multiplexed SOCKS5 tunnel). Kconfig extended with per-module settings and async worker config.
417 lines
12 KiB
C
417 lines
12 KiB
C
/*
|
|
* canbus_isotp.c
|
|
* ISO-TP (ISO 15765-2) transport layer implementation.
|
|
*
|
|
* Frame types:
|
|
* Single Frame (SF): [0x0N | data...] N = length (1-7)
|
|
* First Frame (FF): [0x1H 0xLL | 6 bytes] H:L = total length (up to 4095)
|
|
* Consecutive Frame (CF): [0x2N | 7 bytes] N = sequence (0-F, wrapping)
|
|
* Flow Control (FC): [0x30 BS ST] BS=block size, ST=separation time
|
|
*/
|
|
#include "sdkconfig.h"
|
|
|
|
#if defined(CONFIG_MODULE_CANBUS) && defined(CONFIG_CANBUS_ISO_TP)
|
|
|
|
#include <string.h>
|
|
#include "freertos/FreeRTOS.h"
|
|
#include "freertos/semphr.h"
|
|
#include "esp_timer.h"
|
|
#include "esp_log.h"
|
|
|
|
#include "canbus_isotp.h"
|
|
#include "canbus_driver.h"
|
|
|
|
#define TAG "CAN_ISOTP"
|
|
|
|
/* Max ISO-TP payload (12-bit length field) */
|
|
#define ISOTP_MAX_LEN 4095
|
|
|
|
/* Reassembly buffer (static — single concurrent transfer) */
|
|
static uint8_t s_reassembly[ISOTP_MAX_LEN];
|
|
|
|
/* Synchronization: RX callback puts frame here, isotp functions wait on semaphore */
|
|
static SemaphoreHandle_t s_rx_sem = NULL;
|
|
static can_frame_t s_rx_frame;
|
|
static volatile uint32_t s_listen_id = 0;
|
|
static volatile bool s_listening = false;
|
|
|
|
/* Previous RX callback to chain */
|
|
static can_rx_callback_t s_prev_cb = NULL;
|
|
static void *s_prev_ctx = NULL;
|
|
|
|
/* ============================================================
|
|
* Internal RX callback for ISO-TP framing
|
|
* ============================================================ */
|
|
|
|
static void isotp_rx_callback(const can_frame_t *frame, void *ctx)
|
|
{
|
|
(void)ctx;
|
|
|
|
/* If we're listening for a specific ID, capture it */
|
|
if (s_listening && frame->id == s_listen_id) {
|
|
s_rx_frame = *frame;
|
|
if (s_rx_sem) xSemaphoreGive(s_rx_sem);
|
|
}
|
|
|
|
/* Chain to previous callback (sniff/record) */
|
|
if (s_prev_cb) s_prev_cb(frame, s_prev_ctx);
|
|
}
|
|
|
|
/* ============================================================
|
|
* Helpers
|
|
* ============================================================ */
|
|
|
|
static void isotp_init_once(void)
|
|
{
|
|
if (!s_rx_sem) {
|
|
s_rx_sem = xSemaphoreCreateBinary();
|
|
}
|
|
}
|
|
|
|
/* Hook our callback, saving the previous one */
|
|
static void isotp_hook_rx(uint32_t listen_id)
|
|
{
|
|
isotp_init_once();
|
|
|
|
s_listen_id = listen_id;
|
|
s_listening = true;
|
|
|
|
/* Clear any pending semaphore */
|
|
xSemaphoreTake(s_rx_sem, 0);
|
|
}
|
|
|
|
static void isotp_unhook_rx(void)
|
|
{
|
|
s_listening = false;
|
|
s_listen_id = 0;
|
|
}
|
|
|
|
/* Wait for a frame with the target ID, timeout in ms */
|
|
static bool wait_frame(can_frame_t *out, int timeout_ms)
|
|
{
|
|
if (xSemaphoreTake(s_rx_sem, pdMS_TO_TICKS(timeout_ms)) == pdTRUE) {
|
|
*out = s_rx_frame;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* Send a single CAN frame (helper) */
|
|
static bool send_frame(uint32_t id, const uint8_t *data, uint8_t dlc)
|
|
{
|
|
can_frame_t f = {
|
|
.id = id,
|
|
.dlc = dlc,
|
|
.extended = (id > 0x7FF),
|
|
.rtr = false,
|
|
.timestamp_us = 0,
|
|
};
|
|
memcpy(f.data, data, dlc);
|
|
return can_driver_send(&f);
|
|
}
|
|
|
|
/* Send Flow Control frame: CTS (continue to send) */
|
|
static bool send_fc(uint32_t tx_id, uint8_t block_size, uint8_t st_min)
|
|
{
|
|
uint8_t fc[8] = { 0x30, block_size, st_min, 0, 0, 0, 0, 0 };
|
|
return send_frame(tx_id, fc, 8);
|
|
}
|
|
|
|
/* ============================================================
|
|
* isotp_send — Send ISO-TP message
|
|
* ============================================================ */
|
|
|
|
isotp_status_t isotp_send(uint32_t tx_id, uint32_t rx_id,
|
|
const uint8_t *data, size_t len,
|
|
int timeout_ms)
|
|
{
|
|
if (!data || len == 0 || len > ISOTP_MAX_LEN) return ISOTP_ERROR;
|
|
|
|
isotp_init_once();
|
|
|
|
/* Single Frame: len <= 7 */
|
|
if (len <= 7) {
|
|
uint8_t sf[8] = { 0 };
|
|
sf[0] = (uint8_t)(len & 0x0F); /* PCI: 0x0N */
|
|
memcpy(&sf[1], data, len);
|
|
if (!send_frame(tx_id, sf, 8)) return ISOTP_ERROR;
|
|
return ISOTP_OK;
|
|
}
|
|
|
|
/* Multi-frame: First Frame + wait FC + Consecutive Frames */
|
|
|
|
/* Send First Frame */
|
|
uint8_t ff[8] = { 0 };
|
|
ff[0] = 0x10 | (uint8_t)((len >> 8) & 0x0F);
|
|
ff[1] = (uint8_t)(len & 0xFF);
|
|
memcpy(&ff[2], data, 6);
|
|
if (!send_frame(tx_id, ff, 8)) return ISOTP_ERROR;
|
|
|
|
/* Wait for Flow Control */
|
|
isotp_hook_rx(rx_id);
|
|
|
|
can_frame_t fc;
|
|
if (!wait_frame(&fc, timeout_ms)) {
|
|
isotp_unhook_rx();
|
|
ESP_LOGW(TAG, "FC timeout from 0x%03lX", (unsigned long)rx_id);
|
|
return ISOTP_TIMEOUT;
|
|
}
|
|
|
|
isotp_unhook_rx();
|
|
|
|
/* Parse FC */
|
|
if ((fc.data[0] & 0xF0) != 0x30) {
|
|
ESP_LOGW(TAG, "Expected FC, got PCI 0x%02X", fc.data[0]);
|
|
return ISOTP_ERROR;
|
|
}
|
|
|
|
uint8_t block_size = fc.data[1]; /* 0 = no limit */
|
|
uint8_t st_min = fc.data[2]; /* Separation time in ms */
|
|
|
|
/* Send Consecutive Frames */
|
|
size_t offset = 6; /* First 6 bytes already sent in FF */
|
|
uint8_t seq = 1;
|
|
uint8_t blocks_sent = 0;
|
|
|
|
while (offset < len) {
|
|
uint8_t cf[8] = { 0 };
|
|
cf[0] = 0x20 | (seq & 0x0F);
|
|
|
|
size_t chunk = len - offset;
|
|
if (chunk > 7) chunk = 7;
|
|
memcpy(&cf[1], &data[offset], chunk);
|
|
|
|
if (!send_frame(tx_id, cf, 8)) return ISOTP_ERROR;
|
|
|
|
offset += chunk;
|
|
seq = (seq + 1) & 0x0F;
|
|
blocks_sent++;
|
|
|
|
/* Respect separation time */
|
|
if (st_min > 0 && st_min <= 127) {
|
|
vTaskDelay(pdMS_TO_TICKS(st_min));
|
|
}
|
|
|
|
/* Block size flow control */
|
|
if (block_size > 0 && blocks_sent >= block_size && offset < len) {
|
|
blocks_sent = 0;
|
|
isotp_hook_rx(rx_id);
|
|
if (!wait_frame(&fc, timeout_ms)) {
|
|
isotp_unhook_rx();
|
|
return ISOTP_TIMEOUT;
|
|
}
|
|
isotp_unhook_rx();
|
|
if ((fc.data[0] & 0xF0) != 0x30) return ISOTP_ERROR;
|
|
block_size = fc.data[1];
|
|
st_min = fc.data[2];
|
|
}
|
|
}
|
|
|
|
return ISOTP_OK;
|
|
}
|
|
|
|
/* ============================================================
|
|
* isotp_recv — Receive ISO-TP message
|
|
* ============================================================ */
|
|
|
|
isotp_status_t isotp_recv(uint32_t rx_id,
|
|
uint8_t *buf, size_t buf_cap, size_t *out_len,
|
|
int timeout_ms)
|
|
{
|
|
if (!buf || buf_cap == 0 || !out_len) return ISOTP_ERROR;
|
|
*out_len = 0;
|
|
|
|
isotp_hook_rx(rx_id);
|
|
|
|
can_frame_t frame;
|
|
if (!wait_frame(&frame, timeout_ms)) {
|
|
isotp_unhook_rx();
|
|
return ISOTP_TIMEOUT;
|
|
}
|
|
|
|
uint8_t pci_type = frame.data[0] & 0xF0;
|
|
|
|
/* Single Frame */
|
|
if (pci_type == 0x00) {
|
|
isotp_unhook_rx();
|
|
size_t sf_len = frame.data[0] & 0x0F;
|
|
if (sf_len == 0 || sf_len > 7 || sf_len > buf_cap) return ISOTP_ERROR;
|
|
memcpy(buf, &frame.data[1], sf_len);
|
|
*out_len = sf_len;
|
|
return ISOTP_OK;
|
|
}
|
|
|
|
/* First Frame */
|
|
if (pci_type != 0x10) {
|
|
isotp_unhook_rx();
|
|
ESP_LOGW(TAG, "Expected SF/FF, got PCI 0x%02X", frame.data[0]);
|
|
return ISOTP_ERROR;
|
|
}
|
|
|
|
size_t total_len = ((size_t)(frame.data[0] & 0x0F) << 8) | frame.data[1];
|
|
if (total_len > buf_cap || total_len > ISOTP_MAX_LEN) {
|
|
isotp_unhook_rx();
|
|
return ISOTP_OVERFLOW;
|
|
}
|
|
|
|
/* Copy first 6 data bytes from FF */
|
|
size_t received = (total_len < 6) ? total_len : 6;
|
|
memcpy(buf, &frame.data[2], received);
|
|
|
|
/* We need to figure out the TX ID to send FC back.
|
|
* Convention: if rx_id is in 0x7E8-0x7EF range, tx_id = rx_id - 8.
|
|
* For functional requests, FC goes to rx_id - 8.
|
|
* Caller should use isotp_request() for proper bidirectional comms. */
|
|
uint32_t fc_tx_id = (rx_id >= 0x7E8 && rx_id <= 0x7EF)
|
|
? (rx_id - 8)
|
|
: (rx_id - 1);
|
|
|
|
/* Send Flow Control: continue, no block limit, 0ms separation */
|
|
send_fc(fc_tx_id, 0, 0);
|
|
|
|
/* Receive Consecutive Frames */
|
|
uint8_t expected_seq = 1;
|
|
while (received < total_len) {
|
|
if (!wait_frame(&frame, timeout_ms)) {
|
|
isotp_unhook_rx();
|
|
return ISOTP_TIMEOUT;
|
|
}
|
|
|
|
if ((frame.data[0] & 0xF0) != 0x20) {
|
|
isotp_unhook_rx();
|
|
ESP_LOGW(TAG, "Expected CF, got PCI 0x%02X", frame.data[0]);
|
|
return ISOTP_ERROR;
|
|
}
|
|
|
|
uint8_t seq = frame.data[0] & 0x0F;
|
|
if (seq != (expected_seq & 0x0F)) {
|
|
ESP_LOGW(TAG, "CF seq mismatch: expected %u, got %u",
|
|
expected_seq & 0x0F, seq);
|
|
}
|
|
expected_seq++;
|
|
|
|
size_t chunk = total_len - received;
|
|
if (chunk > 7) chunk = 7;
|
|
memcpy(&buf[received], &frame.data[1], chunk);
|
|
received += chunk;
|
|
}
|
|
|
|
isotp_unhook_rx();
|
|
*out_len = total_len;
|
|
return ISOTP_OK;
|
|
}
|
|
|
|
/* ============================================================
|
|
* isotp_request — Send + Receive (UDS request-response pattern)
|
|
* ============================================================ */
|
|
|
|
isotp_status_t isotp_request(uint32_t tx_id, uint32_t rx_id,
|
|
const uint8_t *req, size_t req_len,
|
|
uint8_t *resp, size_t resp_cap, size_t *resp_len,
|
|
int timeout_ms)
|
|
{
|
|
if (!resp || !resp_len) return ISOTP_ERROR;
|
|
*resp_len = 0;
|
|
|
|
isotp_init_once();
|
|
|
|
/* For request-response, we need to listen before sending
|
|
* (the response may come very quickly after the request) */
|
|
isotp_hook_rx(rx_id);
|
|
|
|
/* Send request */
|
|
isotp_status_t st;
|
|
|
|
if (req_len <= 7) {
|
|
/* Single frame — send directly and wait for response */
|
|
uint8_t sf[8] = { 0 };
|
|
sf[0] = (uint8_t)(req_len & 0x0F);
|
|
memcpy(&sf[1], req, req_len);
|
|
if (!send_frame(tx_id, sf, 8)) {
|
|
isotp_unhook_rx();
|
|
return ISOTP_ERROR;
|
|
}
|
|
} else {
|
|
/* Multi-frame send — unhook first since isotp_send hooks itself */
|
|
isotp_unhook_rx();
|
|
st = isotp_send(tx_id, rx_id, req, req_len, timeout_ms);
|
|
if (st != ISOTP_OK) return st;
|
|
isotp_hook_rx(rx_id);
|
|
}
|
|
|
|
/* Wait for response (may be SF or FF+CF) */
|
|
can_frame_t frame;
|
|
if (!wait_frame(&frame, timeout_ms)) {
|
|
isotp_unhook_rx();
|
|
return ISOTP_TIMEOUT;
|
|
}
|
|
|
|
uint8_t pci_type = frame.data[0] & 0xF0;
|
|
|
|
/* Single Frame response */
|
|
if (pci_type == 0x00) {
|
|
isotp_unhook_rx();
|
|
size_t sf_len = frame.data[0] & 0x0F;
|
|
if (sf_len == 0 || sf_len > 7 || sf_len > resp_cap) return ISOTP_ERROR;
|
|
memcpy(resp, &frame.data[1], sf_len);
|
|
*resp_len = sf_len;
|
|
return ISOTP_OK;
|
|
}
|
|
|
|
/* First Frame response */
|
|
if (pci_type == 0x10) {
|
|
size_t total_len = ((size_t)(frame.data[0] & 0x0F) << 8) | frame.data[1];
|
|
if (total_len > resp_cap || total_len > ISOTP_MAX_LEN) {
|
|
isotp_unhook_rx();
|
|
return ISOTP_OVERFLOW;
|
|
}
|
|
|
|
size_t received = (total_len < 6) ? total_len : 6;
|
|
memcpy(resp, &frame.data[2], received);
|
|
|
|
/* Send FC */
|
|
send_fc(tx_id, 0, 0);
|
|
|
|
/* Receive CFs */
|
|
uint8_t expected_seq = 1;
|
|
while (received < total_len) {
|
|
if (!wait_frame(&frame, timeout_ms)) {
|
|
isotp_unhook_rx();
|
|
return ISOTP_TIMEOUT;
|
|
}
|
|
if ((frame.data[0] & 0xF0) != 0x20) {
|
|
isotp_unhook_rx();
|
|
return ISOTP_ERROR;
|
|
}
|
|
expected_seq++;
|
|
|
|
size_t chunk = total_len - received;
|
|
if (chunk > 7) chunk = 7;
|
|
memcpy(&resp[received], &frame.data[1], chunk);
|
|
received += chunk;
|
|
}
|
|
|
|
isotp_unhook_rx();
|
|
*resp_len = total_len;
|
|
return ISOTP_OK;
|
|
}
|
|
|
|
isotp_unhook_rx();
|
|
ESP_LOGW(TAG, "Unexpected PCI type 0x%02X in response", frame.data[0]);
|
|
return ISOTP_ERROR;
|
|
}
|
|
|
|
/* ============================================================
|
|
* Install ISO-TP RX hook into the CAN driver
|
|
* ============================================================ */
|
|
|
|
void isotp_install_hook(void)
|
|
{
|
|
isotp_init_once();
|
|
/* Save the current callback so we can chain to it */
|
|
can_driver_get_rx_callback(&s_prev_cb, &s_prev_ctx);
|
|
can_driver_set_rx_callback(isotp_rx_callback, NULL);
|
|
}
|
|
|
|
#endif /* CONFIG_MODULE_CANBUS && CONFIG_CANBUS_ISO_TP */
|