zephyr/subsys/net/ip/ipv4_acd.c
Andreas Huber 27d93f8b6c net: ipv4: Fix ARP probe check in address conflict detection
The second condition needs to check ARP probes only

The ACD is not properly implemented as described in RFC5227 ch. 2.1.1
The implementation incorrectly detects an IP conflict, if an ARP request
is received for the target IP.
The reason is that the current implementation checks for ARP requests
instead of ARP probes.

Signed-off-by: Andreas Huber <andreas.huber@ch.sauter-bc.com>
2024-10-29 07:07:59 -05:00

420 lines
10 KiB
C

/*
* Copyright (c) 2017 Matthias Boesl
* Copyright (c) 2018 Intel Corporation
* Copyright (c) 2024 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
/** @file
* @brief IPv4 address conflict detection
*/
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(net_ipv4_acd, CONFIG_NET_IPV4_ACD_LOG_LEVEL);
#include <zephyr/net/ethernet.h>
#include <zephyr/net/net_if.h>
#include <zephyr/net/net_l2.h>
#include <zephyr/net/net_mgmt.h>
#include <zephyr/net/net_pkt.h>
#include <zephyr/random/random.h>
#include <zephyr/sys/slist.h>
#include "ipv4.h"
#include "net_private.h"
#include "../l2/ethernet/arp.h"
static K_MUTEX_DEFINE(lock);
/* Address conflict detection timer. */
static struct k_work_delayable ipv4_acd_timer;
/* List of IPv4 addresses under an active conflict detection. */
static sys_slist_t active_acd_timers;
#define BUF_ALLOC_TIMEOUT K_MSEC(100)
/* Initial random delay*/
#define IPV4_ACD_PROBE_WAIT 1
/* Number of probe packets */
#define IPV4_ACD_PROBE_NUM 3
/* Minimum delay till repeated probe */
#define IPV4_ACD_PROBE_MIN 1
/* Maximum delay till repeated probe */
#define IPV4_ACD_PROBE_MAX 2
/* Delay before announcing */
#define IPV4_ACD_ANNOUNCE_WAIT 2
/* Number of announcement packets */
#define IPV4_ACD_ANNOUNCE_NUM 2
/* Time between announcement packets */
#define IPV4_ACD_ANNOUNCE_INTERVAL 2
/* Max conflicts before rate limiting */
#define IPV4_ACD_MAX_CONFLICTS 10
/* Delay between successive attempts */
#define IPV4_ACD_RATE_LIMIT_INTERVAL 60
/* Minimum interval between defensive ARPs */
#define IPV4_ACD_DEFEND_INTERVAL 10
enum ipv4_acd_state {
IPV4_ACD_PROBE, /* Probing state */
IPV4_ACD_ANNOUNCE, /* Announce state */
};
static struct net_pkt *ipv4_acd_prepare_arp(struct net_if *iface,
struct in_addr *sender_ip,
struct in_addr *target_ip)
{
struct net_pkt *pkt;
/* We provide AF_UNSPEC to the allocator: this packet does not
* need space for any IPv4 header.
*/
pkt = net_pkt_alloc_with_buffer(iface, sizeof(struct net_arp_hdr),
AF_UNSPEC, 0, BUF_ALLOC_TIMEOUT);
if (!pkt) {
return NULL;
}
net_pkt_set_family(pkt, AF_INET);
net_pkt_set_ipv4_acd(pkt, true);
return net_arp_prepare(pkt, target_ip, sender_ip);
}
static void ipv4_acd_send_probe(struct net_if_addr *ifaddr)
{
struct net_if *iface = net_if_get_by_index(ifaddr->ifindex);
struct in_addr unspecified = { 0 };
struct net_pkt *pkt;
pkt = ipv4_acd_prepare_arp(iface, &unspecified, &ifaddr->address.in_addr);
if (!pkt) {
NET_DBG("Failed to prepare probe %p", iface);
return;
}
if (net_if_send_data(iface, pkt) == NET_DROP) {
net_pkt_unref(pkt);
}
}
static void ipv4_acd_send_announcement(struct net_if_addr *ifaddr)
{
struct net_if *iface = net_if_get_by_index(ifaddr->ifindex);
struct net_pkt *pkt;
pkt = ipv4_acd_prepare_arp(iface, &ifaddr->address.in_addr,
&ifaddr->address.in_addr);
if (!pkt) {
NET_DBG("Failed to prepare announcement %p", iface);
return;
}
if (net_if_send_data(iface, pkt) == NET_DROP) {
net_pkt_unref(pkt);
}
}
static void acd_timer_reschedule(void)
{
k_timepoint_t expiry = sys_timepoint_calc(K_FOREVER);
k_timeout_t timeout;
sys_snode_t *node;
SYS_SLIST_FOR_EACH_NODE(&active_acd_timers, node) {
struct net_if_addr *ifaddr =
CONTAINER_OF(node, struct net_if_addr, acd_node);
if (sys_timepoint_cmp(ifaddr->acd_timeout, expiry) < 0) {
expiry = ifaddr->acd_timeout;
}
}
timeout = sys_timepoint_timeout(expiry);
if (K_TIMEOUT_EQ(timeout, K_FOREVER)) {
k_work_cancel_delayable(&ipv4_acd_timer);
return;
}
k_work_reschedule(&ipv4_acd_timer, timeout);
}
static void ipv4_acd_manage_timeout(struct net_if_addr *ifaddr)
{
switch (ifaddr->acd_state) {
case IPV4_ACD_PROBE:
if (ifaddr->acd_count < IPV4_ACD_PROBE_NUM) {
uint32_t delay;
NET_DBG("Sending probe for %s",
net_sprint_ipv4_addr(&ifaddr->address.in_addr));
ipv4_acd_send_probe(ifaddr);
ifaddr->acd_count++;
if (ifaddr->acd_count < IPV4_ACD_PROBE_NUM) {
delay = sys_rand32_get();
delay %= MSEC_PER_SEC * (IPV4_ACD_PROBE_MAX - IPV4_ACD_PROBE_MIN);
delay += MSEC_PER_SEC * IPV4_ACD_PROBE_MIN;
} else {
delay = MSEC_PER_SEC * IPV4_ACD_ANNOUNCE_WAIT;
}
ifaddr->acd_timeout = sys_timepoint_calc(K_MSEC(delay));
break;
}
net_if_ipv4_acd_succeeded(net_if_get_by_index(ifaddr->ifindex),
ifaddr);
ifaddr->acd_state = IPV4_ACD_ANNOUNCE;
ifaddr->acd_count = 0;
__fallthrough;
case IPV4_ACD_ANNOUNCE:
if (ifaddr->acd_count < IPV4_ACD_ANNOUNCE_NUM) {
NET_DBG("Sending announcement for %s",
net_sprint_ipv4_addr(&ifaddr->address.in_addr));
ipv4_acd_send_announcement(ifaddr);
ifaddr->acd_count++;
ifaddr->acd_timeout = sys_timepoint_calc(
K_SECONDS(IPV4_ACD_ANNOUNCE_INTERVAL));
break;
}
NET_DBG("IPv4 conflict detection done for %s",
net_sprint_ipv4_addr(&ifaddr->address.in_addr));
/* Timeout will be used to determine whether DEFEND_INTERVAL
* has expired in case of conflicts.
*/
ifaddr->acd_timeout = sys_timepoint_calc(K_NO_WAIT);
sys_slist_find_and_remove(&active_acd_timers, &ifaddr->acd_node);
break;
default:
break;
}
}
static void ipv4_acd_timeout(struct k_work *work)
{
sys_snode_t *current, *next;
ARG_UNUSED(work);
k_mutex_lock(&lock, K_FOREVER);
SYS_SLIST_FOR_EACH_NODE_SAFE(&active_acd_timers, current, next) {
struct net_if_addr *ifaddr =
CONTAINER_OF(current, struct net_if_addr, acd_node);
if (sys_timepoint_expired(ifaddr->acd_timeout)) {
ipv4_acd_manage_timeout(ifaddr);
}
}
acd_timer_reschedule();
k_mutex_unlock(&lock);
}
static void acd_start_timer(struct net_if *iface, struct net_if_addr *ifaddr)
{
uint32_t delay;
sys_slist_find_and_remove(&active_acd_timers, &ifaddr->acd_node);
sys_slist_append(&active_acd_timers, &ifaddr->acd_node);
if (iface->config.ip.ipv4->conflict_cnt >= IPV4_ACD_MAX_CONFLICTS) {
NET_DBG("Rate limiting");
delay = MSEC_PER_SEC * IPV4_ACD_RATE_LIMIT_INTERVAL;
} else {
/* Initial probe should be delayed by a random time interval
* between 0 and PROBE_WAIT.
*/
delay = sys_rand32_get() % (MSEC_PER_SEC * IPV4_ACD_PROBE_WAIT);
}
ifaddr->acd_timeout = sys_timepoint_calc(K_MSEC(delay));
acd_timer_reschedule();
}
enum net_verdict net_ipv4_acd_input(struct net_if *iface, struct net_pkt *pkt)
{
sys_snode_t *current, *next;
struct net_arp_hdr *arp_hdr;
struct net_if_ipv4 *ipv4;
if (net_pkt_get_len(pkt) < sizeof(struct net_arp_hdr)) {
NET_DBG("Invalid ARP header (len %zu, min %zu bytes)",
net_pkt_get_len(pkt), sizeof(struct net_arp_hdr));
return NET_DROP;
}
arp_hdr = NET_ARP_HDR(pkt);
k_mutex_lock(&lock, K_FOREVER);
SYS_SLIST_FOR_EACH_NODE_SAFE(&active_acd_timers, current, next) {
struct net_if_addr *ifaddr =
CONTAINER_OF(current, struct net_if_addr, acd_node);
struct net_if *addr_iface = net_if_get_by_index(ifaddr->ifindex);
struct net_linkaddr *ll_addr;
if (iface != addr_iface) {
continue;
}
if (ifaddr->acd_state != IPV4_ACD_PROBE) {
continue;
}
ll_addr = net_if_get_link_addr(addr_iface);
/* RFC 5227, ch. 2.1.1 Probe Details:
* - ARP Request/Reply with Sender IP address match OR,
* - ARP Probe where Target IP address match with different sender HW address,
* indicate a conflict.
* ARP Probe has an all-zero sender IP address
*/
if (net_ipv4_addr_cmp_raw(arp_hdr->src_ipaddr,
(uint8_t *)&ifaddr->address.in_addr) ||
(net_ipv4_addr_cmp_raw(arp_hdr->dst_ipaddr,
(uint8_t *)&ifaddr->address.in_addr) &&
(memcmp(&arp_hdr->src_hwaddr, ll_addr->addr, ll_addr->len) != 0) &&
(net_ipv4_addr_cmp_raw(arp_hdr->src_ipaddr,
(uint8_t *)&(struct in_addr)INADDR_ANY_INIT)))) {
NET_DBG("Conflict detected from %s for %s",
net_sprint_ll_addr((uint8_t *)&arp_hdr->src_hwaddr,
arp_hdr->hwlen),
net_sprint_ipv4_addr(&ifaddr->address.in_addr));
iface->config.ip.ipv4->conflict_cnt++;
net_if_ipv4_acd_failed(addr_iface, ifaddr);
k_mutex_unlock(&lock);
return NET_DROP;
}
}
k_mutex_unlock(&lock);
ipv4 = iface->config.ip.ipv4;
if (ipv4 == NULL) {
goto out;
}
/* Passive conflict detection - try to defend already confirmed
* addresses.
*/
ARRAY_FOR_EACH(ipv4->unicast, i) {
struct net_if_addr *ifaddr = &ipv4->unicast[i].ipv4;
struct net_linkaddr *ll_addr = net_if_get_link_addr(iface);
if (!ifaddr->is_used) {
continue;
}
if (net_ipv4_addr_cmp_raw(arp_hdr->src_ipaddr,
(uint8_t *)&ifaddr->address.in_addr) &&
memcmp(&arp_hdr->src_hwaddr, ll_addr->addr, ll_addr->len) != 0) {
NET_DBG("Conflict detected from %s for %s",
net_sprint_ll_addr((uint8_t *)&arp_hdr->src_hwaddr,
arp_hdr->hwlen),
net_sprint_ipv4_addr(&ifaddr->address.in_addr));
ipv4->conflict_cnt++;
/* In case timer has expired, we're past DEFEND_INTERVAL
* and can try to defend again
*/
if (sys_timepoint_expired(ifaddr->acd_timeout)) {
NET_DBG("Defending address %s",
net_sprint_ipv4_addr(&ifaddr->address.in_addr));
ipv4_acd_send_announcement(ifaddr);
ifaddr->acd_timeout = sys_timepoint_calc(
K_SECONDS(IPV4_ACD_DEFEND_INTERVAL));
} else {
NET_DBG("Reporting conflict on %s",
net_sprint_ipv4_addr(&ifaddr->address.in_addr));
/* Otherwise report the conflict and let the
* application decide.
*/
net_mgmt_event_notify_with_info(
NET_EVENT_IPV4_ACD_CONFLICT, iface,
&ifaddr->address.in_addr,
sizeof(struct in_addr));
}
break;
}
}
out:
return NET_CONTINUE;
}
void net_ipv4_acd_init(void)
{
k_work_init_delayable(&ipv4_acd_timer, ipv4_acd_timeout);
}
int net_ipv4_acd_start(struct net_if *iface, struct net_if_addr *ifaddr)
{
/* Address conflict detection is based on ARP, so can only be done on
* supporting interfaces.
*/
if (!(net_if_l2(iface) == &NET_L2_GET_NAME(ETHERNET) ||
net_eth_is_vlan_interface(iface))) {
net_if_ipv4_acd_succeeded(iface, ifaddr);
return 0;
}
k_mutex_lock(&lock, K_FOREVER);
ifaddr->ifindex = net_if_get_by_iface(iface);
ifaddr->acd_state = IPV4_ACD_PROBE;
ifaddr->acd_count = 0;
acd_start_timer(iface, ifaddr);
k_mutex_unlock(&lock);
return 0;
}
void net_ipv4_acd_cancel(struct net_if *iface, struct net_if_addr *ifaddr)
{
/* Address conflict detection is based on ARP, so can only be done on
* supporting interfaces.
*/
if (!(net_if_l2(iface) == &NET_L2_GET_NAME(ETHERNET) ||
net_eth_is_vlan_interface(iface))) {
return;
}
k_mutex_lock(&lock, K_FOREVER);
sys_slist_find_and_remove(&active_acd_timers, &ifaddr->acd_node);
acd_timer_reschedule();
k_mutex_unlock(&lock);
}