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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions core/src/drivers/plugins/native/ethercat/ethercat_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -909,9 +909,9 @@ int ecat_config_parse_all(const char *config_path,
cJSON_Delete(root);

/* Refuse configs where two masters share the same network interface.
* Per-iface external state (NIC tuning + iptables + ipv6) in
* ethercat_iface_state.c is single-owner; two masters on the same iface
* produce corrupted persistence on crash recovery. */
* Per-iface NIC tuning state in ethercat_iface_state.c is
* single-owner; two masters on the same iface produce corrupted
* persistence on crash recovery. */
for (int i = 0; i < count; i++) {
for (int j = i + 1; j < count; j++) {
if (strcmp(instances[i].config.master.interface,
Expand Down
23 changes: 10 additions & 13 deletions core/src/drivers/plugins/native/ethercat/ethercat_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,10 @@ void ecat_config_set_logger(plugin_logger_t *logger);
/**
* @brief Validation mode for interface names.
*
* Different callers need different rules: NIC tuning / iptables paths must
* receive Linux-only names safe for /proc and external binaries; the scan
* and test commands accept any name the underlying socket layer accepts,
* including Windows NPF device paths like "\Device\NPF_{GUID}".
* Different callers need different rules: NIC tuning paths must receive
* Linux-only names safe for /proc and external binaries (ethtool); the
* scan and test commands accept any name the underlying socket layer
* accepts, including Windows NPF device paths like "\Device\NPF_{GUID}".
*/
typedef enum {
ECAT_IFACE_LINUX_STRICT, /* alfanum + '_' '-', starts alpha, len 1..15 */
Expand Down Expand Up @@ -429,20 +429,17 @@ typedef struct {
#define ECAT_AVG_EWMA_SHIFT 5

/**
* @brief Per-interface external state captured by ecat_iface_state_apply().
* @brief Per-interface NIC tuning state captured by ecat_iface_state_apply().
*
* Combines NIC-tuning save/restore (ethtool coalescing + offloads) and
* IP-stack isolation (iptables INPUT DROP, IPv6 sysctl) into a single
* struct, embedded in each ecat_master_instance_t. See
* ethercat_iface_state.h for the apply/revert API.
* Holds the pre-EtherCAT NIC settings (ethtool coalescing + offloads) so
* `ecat_iface_state_revert()` can roll them back on graceful shutdown,
* and so the next runtime start can recover from a crash. Embedded in
* each `ecat_master_instance_t`. See ethercat_iface_state.h for the
* apply/revert API.
*/
typedef struct {
char iface[ECAT_IFNAME_MAX];

/* IP-stack isolation */
bool ipv6_disabled_by_us;
bool iptables_added;

/* NIC tuning -- ethtool -C (coalescing) */
bool coalescing_saved;
int rx_usecs;
Expand Down
236 changes: 17 additions & 219 deletions core/src/drivers/plugins/native/ethercat/ethercat_iface_state.c
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
/**
* @file ethercat_iface_state.c
* @brief Implementation of ethercat_iface_state.h (consolidated NIC tuning
* + IP-stack isolation with crash-recovery persistence).
* @brief Implementation of ethercat_iface_state.h.
*
* On Linux this file owns three external mechanisms:
* - ethtool -C / -K (NIC coalescing and offloads)
* - iptables -I/-D (drop IP traffic on the EtherCAT NIC)
* - sysctl IPv6 (disable_ipv6 on the NIC)
* This module owns the NIC tuning the EtherCAT bus thread depends on:
* - ethtool -C (interrupt coalescing: rx-usecs / tx-usecs = 0)
* - ethtool -K (offload aggregation: GRO / GSO / TSO off)
*
* Both are runtime-only NIC settings (not persisted across reboots by
* the OS), so the only correctness concern is making sure a graceful
* shutdown — and a *crashed* runtime's next start — both restore them
* to the values that were in effect before the master was brought up.
* That's what the `/run/runtime/ecat_iface_<iface>.state` file is for:
* we write the captured "before" values there, and the next apply call
* checks for a leftover file and rolls back before re-applying.
*
* On non-Linux platforms apply/revert are no-ops; the SOEM raw socket
* is the only thing that interacts with the NIC there.
Expand All @@ -30,60 +36,6 @@
#define ECAT_IFACE_STATE_DIR "/run/runtime"
#define ECAT_IFACE_STATE_FMT ECAT_IFACE_STATE_DIR "/ecat_iface_%s.state"

/* Legacy persistence files from the pre-consolidation versions. Detected
* on apply, reverted, and removed before we proceed with the unified flow. */
#define ECAT_LEGACY_NIC_FMT ECAT_IFACE_STATE_DIR "/ecat_nic_saved_%s.conf"
#define ECAT_LEGACY_ISO_FMT ECAT_IFACE_STATE_DIR "/ecat_iface_iso_%s.state"

/* ------------------------------------------------------------------ */
/* ipv6 sysctl */
/* ------------------------------------------------------------------ */

static int read_ipv6_disabled(const char *iface)
{
char path[160];
snprintf(path, sizeof(path),
"/proc/sys/net/ipv6/conf/%s/disable_ipv6", iface);
FILE *f = fopen(path, "r");
if (!f) return -1;
int v = -1;
if (fscanf(f, "%d", &v) != 1) v = -1;
fclose(f);
return v;
}

static int write_ipv6_disabled(const char *iface, int value)
{
char path[160];
snprintf(path, sizeof(path),
"/proc/sys/net/ipv6/conf/%s/disable_ipv6", iface);
FILE *f = fopen(path, "w");
if (!f) return -1;
int rc = (fprintf(f, "%d\n", value) > 0) ? 0 : -1;
fclose(f);
return rc;
}

/* ------------------------------------------------------------------ */
/* iptables */
/* ------------------------------------------------------------------ */

static int iptables_delete(const char *iface)
{
char *argv[] = {
"iptables", "-D", "INPUT", "-i", (char *)iface, "-j", "DROP", NULL
};
return ecat_run_argv("iptables", argv, NULL, 0);
}

static int iptables_insert(const char *iface)
{
char *argv[] = {
"iptables", "-I", "INPUT", "1", "-i", (char *)iface, "-j", "DROP", NULL
};
return ecat_run_argv("iptables", argv, NULL, 0);
}

/* ------------------------------------------------------------------ */
/* ethtool output parsing */
/* ------------------------------------------------------------------ */
Expand Down Expand Up @@ -126,7 +78,7 @@ static int parse_ethtool_bool(const char *output, const char *key, int *value)
}

/* ------------------------------------------------------------------ */
/* NIC capture / apply */
/* NIC capture / apply / restore */
/* ------------------------------------------------------------------ */

static void capture_nic_settings(ecat_iface_state_t *s, plugin_logger_t *logger)
Expand Down Expand Up @@ -276,8 +228,6 @@ static void persist_state(const ecat_iface_state_t *s, plugin_logger_t *logger)
}

fprintf(fp, "iface=%s\n", s->iface);
fprintf(fp, "iptables_added=%d\n", s->iptables_added ? 1 : 0);
fprintf(fp, "ipv6_disabled_by_us=%d\n", s->ipv6_disabled_by_us ? 1 : 0);
if (s->coalescing_saved) {
fprintf(fp, "rx_usecs=%d\n", s->rx_usecs);
fprintf(fp, "tx_usecs=%d\n", s->tx_usecs);
Expand Down Expand Up @@ -331,11 +281,7 @@ static bool load_state(const char *iface, ecat_iface_state_t *s)
*eq = '\0';
const char *key = line;
const char *val = eq + 1;
if (strcmp(key, "iptables_added") == 0) {
s->iptables_added = (atoi(val) != 0);
} else if (strcmp(key, "ipv6_disabled_by_us") == 0) {
s->ipv6_disabled_by_us = (atoi(val) != 0);
} else if (strcmp(key, "rx_usecs") == 0) {
if (strcmp(key, "rx_usecs") == 0) {
s->rx_usecs = atoi(val);
s->coalescing_saved = true;
} else if (strcmp(key, "tx_usecs") == 0) {
Expand All @@ -356,106 +302,6 @@ static bool load_state(const char *iface, ecat_iface_state_t *s)
return true;
}

/* ------------------------------------------------------------------ */
/* Legacy file migration */
/* ------------------------------------------------------------------ */

/*
* If a NIC settings file from the pre-consolidation version exists,
* parse it, restore the NIC, and remove the file.
*/
static void migrate_legacy_nic(const char *iface, plugin_logger_t *logger)
{
char path[160];
snprintf(path, sizeof(path), ECAT_LEGACY_NIC_FMT, iface);
FILE *fp = fopen(path, "r");
if (!fp)
return;

plugin_logger_warn(logger,
"Found legacy NIC state file %s - reverting and migrating", path);

ecat_iface_state_t legacy;
memset(&legacy, 0, sizeof(legacy));
strncpy(legacy.iface, iface, sizeof(legacy.iface) - 1);
legacy.iface[sizeof(legacy.iface) - 1] = '\0';

char line[128];
while (fgets(line, sizeof(line), fp)) {
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n')
line[len - 1] = '\0';
char *eq = strchr(line, '=');
if (!eq)
continue;
*eq = '\0';
const char *key = line;
const char *val = eq + 1;
if (strcmp(key, "rx_usecs") == 0) {
legacy.rx_usecs = atoi(val);
legacy.coalescing_saved = true;
} else if (strcmp(key, "tx_usecs") == 0) {
legacy.tx_usecs = atoi(val);
legacy.coalescing_saved = true;
} else if (strcmp(key, "gro") == 0) {
legacy.gro = (strcmp(val, "on") == 0);
legacy.offloads_saved = true;
} else if (strcmp(key, "gso") == 0) {
legacy.gso = (strcmp(val, "on") == 0);
legacy.offloads_saved = true;
} else if (strcmp(key, "tso") == 0) {
legacy.tso = (strcmp(val, "on") == 0);
legacy.offloads_saved = true;
}
}
fclose(fp);

restore_nic_settings(&legacy, logger);
unlink(path);
}

/*
* If an iface-isolation file from the pre-consolidation version exists,
* parse it, undo iptables / ipv6, and remove the file.
*/
static void migrate_legacy_iso(const char *iface, plugin_logger_t *logger)
{
char path[160];
snprintf(path, sizeof(path), ECAT_LEGACY_ISO_FMT, iface);
FILE *fp = fopen(path, "r");
if (!fp)
return;

plugin_logger_warn(logger,
"Found legacy iface-isolation file %s - reverting and migrating", path);

bool ipt = false, ipv6 = false;
char line[128];
while (fgets(line, sizeof(line), fp)) {
size_t len = strlen(line);
if (len > 0 && line[len - 1] == '\n')
line[len - 1] = '\0';
char *eq = strchr(line, '=');
if (!eq)
continue;
*eq = '\0';
const char *key = line;
const char *val = eq + 1;
if (strcmp(key, "iptables_added") == 0) {
ipt = (atoi(val) != 0);
} else if (strcmp(key, "ipv6_disabled_by_us") == 0) {
ipv6 = (atoi(val) != 0);
}
}
fclose(fp);

if (ipt)
iptables_delete(iface);
if (ipv6)
write_ipv6_disabled(iface, 0);
unlink(path);
}

/* ------------------------------------------------------------------ */
/* Crash recovery from the unified state file */
/* ------------------------------------------------------------------ */
Expand All @@ -468,18 +314,12 @@ static void recover_from_crash(const char *iface, plugin_logger_t *logger)

plugin_logger_warn(logger,
"Found stale iface state for %s "
"(iptables=%d, ipv6=%d, coalescing_saved=%d, offloads_saved=%d) - "
"(coalescing_saved=%d, offloads_saved=%d) - "
"previous process likely crashed. Reverting before re-applying.",
iface,
(int)prev.iptables_added, (int)prev.ipv6_disabled_by_us,
(int)prev.coalescing_saved, (int)prev.offloads_saved);

if (prev.iptables_added)
iptables_delete(iface);
if (prev.ipv6_disabled_by_us)
write_ipv6_disabled(iface, 0);
restore_nic_settings(&prev, logger);

remove_state_file(iface);
}

Expand All @@ -498,52 +338,21 @@ void ecat_iface_state_apply(ecat_iface_state_t *state, const char *iface,
if (!ecat_iface_validate(iface, ECAT_IFACE_LINUX_STRICT)) {
plugin_logger_warn(logger,
"iface '%s' not a valid Linux interface name -- "
"NIC tuning (ethtool) and IP-stack isolation (iptables, IPv6) "
"are disabled for this master. Jitter may be higher.",
"NIC tuning (ethtool) is disabled for this master. "
"Jitter may be higher.",
iface);
return;
}

strncpy(state->iface, iface, sizeof(state->iface) - 1);
state->iface[sizeof(state->iface) - 1] = '\0';

/* Migrate any leftover state from prior versions (best-effort) */
migrate_legacy_nic(iface, logger);
migrate_legacy_iso(iface, logger);

/* Recover from a crash of the current-format file */
recover_from_crash(iface, logger);

/* Capture the live NIC settings before we change them */
capture_nic_settings(state, logger);

/* IPv6: only flip if currently enabled, so we never undo an operator
* setting that was already disabled. */
int ipv6 = read_ipv6_disabled(iface);
if (ipv6 == 0) {
if (write_ipv6_disabled(iface, 1) == 0) {
state->ipv6_disabled_by_us = true;
plugin_logger_info(logger,
"%s: disabled IPv6 (jitter isolation)", iface);
} else {
plugin_logger_warn(logger,
"%s: could not disable IPv6", iface);
}
} else if (ipv6 == 1) {
plugin_logger_debug(logger, "%s: IPv6 already disabled", iface);
}

/* Delete-then-insert keeps the rule unique across re-runs */
iptables_delete(iface);
if (iptables_insert(iface) == 0) {
state->iptables_added = true;
plugin_logger_info(logger,
"%s: inserted iptables INPUT DROP (jitter isolation)", iface);
} else {
plugin_logger_warn(logger,
"%s: iptables -I INPUT failed -- isolation skipped", iface);
}

/* Apply low-latency NIC tuning */
apply_low_latency_nic(iface, logger);

Expand All @@ -557,17 +366,6 @@ void ecat_iface_state_revert(ecat_iface_state_t *state, plugin_logger_t *logger)
return;

/* Only undo what we applied, in reverse order */
if (state->iptables_added) {
iptables_delete(state->iface);
plugin_logger_info(logger,
"%s: removed iptables INPUT DROP", state->iface);
state->iptables_added = false;
}
if (state->ipv6_disabled_by_us) {
write_ipv6_disabled(state->iface, 0);
plugin_logger_info(logger, "%s: re-enabled IPv6", state->iface);
state->ipv6_disabled_by_us = false;
}
restore_nic_settings(state, logger);
state->coalescing_saved = false;
state->offloads_saved = false;
Expand Down
Loading