mirror of
https://github.com/ipxe/ipxe
synced 2026-01-01 17:34:42 +03:00
[ipv6] Replace IPv6 stack
Replace the existing partially-implemented IPv6 stack with a fresh implementation. This implementation is not yet complete. The IPv6 transmit and receive datapaths are functional (including fragment reassembly and parsing of arbitrary extension headers). NDP neighbour solicitations and advertisements are supported. ICMPv6 echo is supported. At present, only link-local addresses may be used, and there is no way to specify an IPv6 address as part of a URI (either directly or via a DNS lookup). Signed-off-by: Michael Brown <mcb30@ipxe.org>
This commit is contained in:
484
src/net/ndp.c
484
src/net/ndp.c
@@ -1,180 +1,370 @@
|
||||
#include <stdint.h>
|
||||
/*
|
||||
* Copyright (C) 2013 Michael Brown <mbrown@fensystems.co.uk>.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License as
|
||||
* published by the Free Software Foundation; either version 2 of the
|
||||
* License, or any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
* General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
||||
* 02110-1301, USA.
|
||||
*/
|
||||
|
||||
FILE_LICENCE ( GPL2_OR_LATER );
|
||||
|
||||
#include <string.h>
|
||||
#include <byteswap.h>
|
||||
#include <errno.h>
|
||||
#include <ipxe/if_ether.h>
|
||||
#include <byteswap.h>
|
||||
#include <ipxe/in.h>
|
||||
#include <ipxe/iobuf.h>
|
||||
#include <ipxe/tcpip.h>
|
||||
#include <ipxe/ipv6.h>
|
||||
#include <ipxe/icmpv6.h>
|
||||
#include <ipxe/neighbour.h>
|
||||
#include <ipxe/ndp.h>
|
||||
#include <ipxe/icmp6.h>
|
||||
#include <ipxe/ip6.h>
|
||||
#include <ipxe/netdevice.h>
|
||||
|
||||
/** @file
|
||||
*
|
||||
* Neighbour Discovery Protocol
|
||||
* IPv6 neighbour discovery protocol
|
||||
*
|
||||
* This file implements address resolution as specified by the neighbour
|
||||
* discovery protocol in RFC2461. This protocol is part of the IPv6 protocol
|
||||
* family.
|
||||
*/
|
||||
|
||||
/* A neighbour entry */
|
||||
struct ndp_entry {
|
||||
/** Target IP6 address */
|
||||
struct in6_addr in6;
|
||||
/** Link layer protocol */
|
||||
struct ll_protocol *ll_protocol;
|
||||
/** Link-layer address */
|
||||
uint8_t ll_addr[MAX_LL_ADDR_LEN];
|
||||
/** State of the neighbour entry */
|
||||
int state;
|
||||
};
|
||||
|
||||
/** Number of entries in the neighbour cache table */
|
||||
#define NUM_NDP_ENTRIES 4
|
||||
|
||||
/** The neighbour cache table */
|
||||
static struct ndp_entry ndp_table[NUM_NDP_ENTRIES];
|
||||
#define ndp_table_end &ndp_table[NUM_NDP_ENTRIES]
|
||||
|
||||
static unsigned int next_new_ndp_entry = 0;
|
||||
|
||||
/**
|
||||
* Find entry in the neighbour cache
|
||||
*
|
||||
* @v in6 IP6 address
|
||||
*/
|
||||
static struct ndp_entry *
|
||||
ndp_find_entry ( struct in6_addr *in6 ) {
|
||||
struct ndp_entry *ndp;
|
||||
|
||||
for ( ndp = ndp_table ; ndp < ndp_table_end ; ndp++ ) {
|
||||
if ( IP6_EQUAL ( ( *in6 ), ndp->in6 ) &&
|
||||
( ndp->state != NDP_STATE_INVALID ) ) {
|
||||
return ndp;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add NDP entry
|
||||
*
|
||||
* @v netdev Network device
|
||||
* @v in6 IP6 address
|
||||
* @v ll_addr Link-layer address
|
||||
* @v state State of the entry - one of the NDP_STATE_XXX values
|
||||
*/
|
||||
static void
|
||||
add_ndp_entry ( struct net_device *netdev, struct in6_addr *in6,
|
||||
void *ll_addr, int state ) {
|
||||
struct ndp_entry *ndp;
|
||||
ndp = &ndp_table[next_new_ndp_entry++ % NUM_NDP_ENTRIES];
|
||||
|
||||
/* Fill up entry */
|
||||
ndp->ll_protocol = netdev->ll_protocol;
|
||||
memcpy ( &ndp->in6, &( *in6 ), sizeof ( *in6 ) );
|
||||
if ( ll_addr ) {
|
||||
memcpy ( ndp->ll_addr, ll_addr, netdev->ll_protocol->ll_addr_len );
|
||||
} else {
|
||||
memset ( ndp->ll_addr, 0, netdev->ll_protocol->ll_addr_len );
|
||||
}
|
||||
ndp->state = state;
|
||||
DBG ( "New neighbour cache entry: IP6 %s => %s %s\n",
|
||||
inet6_ntoa ( ndp->in6 ), netdev->ll_protocol->name,
|
||||
netdev->ll_protocol->ntoa ( ndp->ll_addr ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the link-layer address
|
||||
* Transmit NDP neighbour solicitation/advertisement packet
|
||||
*
|
||||
* @v netdev Network device
|
||||
* @v dest Destination address
|
||||
* @v src Source address
|
||||
* @ret dest_ll_addr Destination link-layer address or NULL
|
||||
* @ret rc Status
|
||||
*
|
||||
* This function looks up the neighbour cache for an entry corresponding to the
|
||||
* destination address. If it finds a valid entry, it fills up dest_ll_addr and
|
||||
* returns 0. Otherwise it sends a neighbour solicitation to the solicited
|
||||
* multicast address.
|
||||
* @v sin6_src Source socket address
|
||||
* @v sin6_dest Destination socket address
|
||||
* @v target Neighbour target address
|
||||
* @v icmp_type ICMPv6 type
|
||||
* @v flags NDP flags
|
||||
* @v option_type NDP option type
|
||||
* @ret rc Return status code
|
||||
*/
|
||||
int ndp_resolve ( struct net_device *netdev, struct in6_addr *dest,
|
||||
struct in6_addr *src, void *dest_ll_addr ) {
|
||||
static int ndp_tx_neighbour ( struct net_device *netdev,
|
||||
struct sockaddr_in6 *sin6_src,
|
||||
struct sockaddr_in6 *sin6_dest,
|
||||
const struct in6_addr *target,
|
||||
unsigned int icmp_type,
|
||||
unsigned int flags,
|
||||
unsigned int option_type ) {
|
||||
struct sockaddr_tcpip *st_src =
|
||||
( ( struct sockaddr_tcpip * ) sin6_src );
|
||||
struct sockaddr_tcpip *st_dest =
|
||||
( ( struct sockaddr_tcpip * ) sin6_dest );
|
||||
struct ll_protocol *ll_protocol = netdev->ll_protocol;
|
||||
struct ndp_entry *ndp;
|
||||
struct io_buffer *iobuf;
|
||||
struct ndp_header *ndp;
|
||||
size_t option_len;
|
||||
size_t len;
|
||||
int rc;
|
||||
|
||||
ndp = ndp_find_entry ( dest );
|
||||
/* Check if the entry is valid */
|
||||
if ( ndp && ndp->state == NDP_STATE_REACHABLE ) {
|
||||
DBG ( "Neighbour cache hit: IP6 %s => %s %s\n",
|
||||
inet6_ntoa ( *dest ), ll_protocol->name,
|
||||
ll_protocol->ntoa ( ndp->ll_addr ) );
|
||||
memcpy ( dest_ll_addr, ndp->ll_addr, ll_protocol->ll_addr_len );
|
||||
return 0;
|
||||
}
|
||||
/* Allocate and populate buffer */
|
||||
option_len = ( ( sizeof ( ndp->option[0] ) + ll_protocol->ll_addr_len +
|
||||
NDP_OPTION_BLKSZ - 1 ) &
|
||||
~( NDP_OPTION_BLKSZ - 1 ) );
|
||||
len = ( sizeof ( *ndp ) + option_len );
|
||||
iobuf = alloc_iob ( MAX_LL_NET_HEADER_LEN + len );
|
||||
if ( ! iobuf )
|
||||
return -ENOMEM;
|
||||
iob_reserve ( iobuf, MAX_LL_NET_HEADER_LEN );
|
||||
ndp = iob_put ( iobuf, len );
|
||||
memset ( ndp, 0, len );
|
||||
ndp->icmp.type = icmp_type;
|
||||
ndp->flags = flags;
|
||||
memcpy ( &ndp->target, target, sizeof ( ndp->target ) );
|
||||
ndp->option[0].type = option_type;
|
||||
ndp->option[0].blocks = ( option_len / NDP_OPTION_BLKSZ );
|
||||
memcpy ( ndp->option[0].value, netdev->ll_addr,
|
||||
ll_protocol->ll_addr_len );
|
||||
ndp->icmp.chksum = tcpip_chksum ( ndp, len );
|
||||
|
||||
/* Check if the entry was already created */
|
||||
if ( ndp ) {
|
||||
DBG ( "Awaiting neighbour advertisement\n" );
|
||||
/* For test */
|
||||
// ndp->state = NDP_STATE_REACHABLE;
|
||||
// memcpy ( ndp->ll_addr, netdev->ll_addr, 6 );
|
||||
// assert ( ndp->ll_protocol->ll_addr_len == 6 );
|
||||
// icmp6_test_nadvert ( netdev, dest, ndp->ll_addr );
|
||||
// assert ( ndp->state == NDP_STATE_REACHABLE );
|
||||
/* Take it out till here */
|
||||
return -ENOENT;
|
||||
}
|
||||
DBG ( "Neighbour cache miss: IP6 %s\n", inet6_ntoa ( *dest ) );
|
||||
|
||||
/* Add entry in the neighbour cache */
|
||||
add_ndp_entry ( netdev, dest, NULL, NDP_STATE_INCOMPLETE );
|
||||
|
||||
/* Send neighbour solicitation */
|
||||
if ( ( rc = icmp6_send_solicit ( netdev, src, dest ) ) != 0 ) {
|
||||
/* Transmit packet */
|
||||
if ( ( rc = tcpip_tx ( iobuf, &icmpv6_protocol, st_src, st_dest,
|
||||
netdev, &ndp->icmp.chksum ) ) != 0 ) {
|
||||
DBGC ( netdev, "NDP could not transmit packet: %s\n",
|
||||
strerror ( rc ) );
|
||||
return rc;
|
||||
}
|
||||
return -ENOENT;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process neighbour advertisement
|
||||
* Transmit NDP neighbour discovery request
|
||||
*
|
||||
* @v iobuf I/O buffer
|
||||
* @v st_src Source address
|
||||
* @v st_dest Destination address
|
||||
* @v netdev Network device
|
||||
* @v net_protocol Network-layer protocol
|
||||
* @v net_dest Destination network-layer address
|
||||
* @v net_source Source network-layer address
|
||||
* @ret rc Return status code
|
||||
*/
|
||||
int ndp_process_advert ( struct io_buffer *iobuf, struct sockaddr_tcpip *st_src __unused,
|
||||
struct sockaddr_tcpip *st_dest __unused ) {
|
||||
struct neighbour_advert *nadvert = iobuf->data;
|
||||
struct ndp_entry *ndp;
|
||||
static int ndp_tx_request ( struct net_device *netdev,
|
||||
struct net_protocol *net_protocol __unused,
|
||||
const void *net_dest, const void *net_source ) {
|
||||
struct sockaddr_in6 sin6_src;
|
||||
struct sockaddr_in6 sin6_dest;
|
||||
|
||||
/* Construct source address */
|
||||
memset ( &sin6_src, 0, sizeof ( sin6_src ) );
|
||||
sin6_src.sin6_family = AF_INET6;
|
||||
memcpy ( &sin6_src.sin6_addr, net_source,
|
||||
sizeof ( sin6_src.sin6_addr ) );
|
||||
sin6_src.sin6_scope_id = htons ( netdev->index );
|
||||
|
||||
/* Construct multicast destination address */
|
||||
memset ( &sin6_dest, 0, sizeof ( sin6_dest ) );
|
||||
sin6_dest.sin6_family = AF_INET6;
|
||||
sin6_dest.sin6_scope_id = htons ( netdev->index );
|
||||
ipv6_solicited_node ( &sin6_dest.sin6_addr, net_dest );
|
||||
|
||||
/* Transmit neighbour discovery packet */
|
||||
return ndp_tx_neighbour ( netdev, &sin6_src, &sin6_dest, net_dest,
|
||||
ICMPV6_NDP_NEIGHBOUR_SOLICITATION, 0,
|
||||
NDP_OPT_LL_SOURCE );
|
||||
}
|
||||
|
||||
/** NDP neighbour discovery protocol */
|
||||
struct neighbour_discovery ndp_discovery = {
|
||||
.name = "NDP",
|
||||
.tx_request = ndp_tx_request,
|
||||
};
|
||||
|
||||
/**
|
||||
* Process NDP neighbour solicitation source link-layer address option
|
||||
*
|
||||
* @v netdev Network device
|
||||
* @v sin6_src Source socket address
|
||||
* @v ndp NDP packet
|
||||
* @v ll_addr Source link-layer address
|
||||
* @v ll_addr_len Source link-layer address length
|
||||
* @ret rc Return status code
|
||||
*/
|
||||
static int ndp_rx_neighbour_solicitation ( struct net_device *netdev,
|
||||
struct sockaddr_in6 *sin6_src,
|
||||
struct ndp_header *ndp __unused,
|
||||
const void *ll_addr,
|
||||
size_t ll_addr_len ) {
|
||||
struct ll_protocol *ll_protocol = netdev->ll_protocol;
|
||||
int rc;
|
||||
|
||||
/* Silently ignore neighbour solicitations for addresses we do
|
||||
* not own.
|
||||
*/
|
||||
if ( ! ipv6_has_addr ( netdev, &ndp->target ) )
|
||||
return 0;
|
||||
|
||||
/* Sanity check */
|
||||
if ( iob_len ( iobuf ) < sizeof ( *nadvert ) ) {
|
||||
DBG ( "Packet too short (%zd bytes)\n", iob_len ( iobuf ) );
|
||||
if ( ll_addr_len < ll_protocol->ll_addr_len ) {
|
||||
DBGC ( netdev, "NDP neighbour solicitation link-layer address "
|
||||
"too short at %zd bytes (min %d bytes)\n",
|
||||
ll_addr_len, ll_protocol->ll_addr_len );
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
assert ( nadvert->code == 0 );
|
||||
assert ( nadvert->flags & ICMP6_FLAGS_SOLICITED );
|
||||
assert ( nadvert->opt_type == 2 );
|
||||
|
||||
/* Update the neighbour cache, if entry is present */
|
||||
ndp = ndp_find_entry ( &nadvert->target );
|
||||
if ( ndp ) {
|
||||
|
||||
assert ( nadvert->opt_len ==
|
||||
( ( 2 + ndp->ll_protocol->ll_addr_len ) / 8 ) );
|
||||
|
||||
if ( IP6_EQUAL ( ndp->in6, nadvert->target ) ) {
|
||||
memcpy ( ndp->ll_addr, nadvert->opt_ll_addr,
|
||||
ndp->ll_protocol->ll_addr_len );
|
||||
ndp->state = NDP_STATE_REACHABLE;
|
||||
return 0;
|
||||
}
|
||||
/* Create or update neighbour cache entry */
|
||||
if ( ( rc = neighbour_define ( netdev, &ipv6_protocol,
|
||||
&sin6_src->sin6_addr,
|
||||
ll_addr ) ) != 0 ) {
|
||||
DBGC ( netdev, "NDP could not define %s => %s: %s\n",
|
||||
inet6_ntoa ( &sin6_src->sin6_addr ),
|
||||
ll_protocol->ntoa ( ll_addr ), strerror ( rc ) );
|
||||
return rc;
|
||||
}
|
||||
DBG ( "Unsolicited advertisement (dropping packet)\n" );
|
||||
|
||||
/* Send neighbour advertisement */
|
||||
if ( ( rc = ndp_tx_neighbour ( netdev, NULL, sin6_src, &ndp->target,
|
||||
ICMPV6_NDP_NEIGHBOUR_ADVERTISEMENT,
|
||||
( NDP_SOLICITED | NDP_OVERRIDE ),
|
||||
NDP_OPT_LL_TARGET ) ) != 0 ) {
|
||||
return rc;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process NDP neighbour advertisement target link-layer address option
|
||||
*
|
||||
* @v netdev Network device
|
||||
* @v sin6_src Source socket address
|
||||
* @v ndp NDP packet
|
||||
* @v ll_addr Target link-layer address
|
||||
* @v ll_addr_len Target link-layer address length
|
||||
* @ret rc Return status code
|
||||
*/
|
||||
static int
|
||||
ndp_rx_neighbour_advertisement ( struct net_device *netdev,
|
||||
struct sockaddr_in6 *sin6_src __unused,
|
||||
struct ndp_header *ndp, const void *ll_addr,
|
||||
size_t ll_addr_len ) {
|
||||
struct ll_protocol *ll_protocol = netdev->ll_protocol;
|
||||
int rc;
|
||||
|
||||
/* Sanity check */
|
||||
if ( ll_addr_len < ll_protocol->ll_addr_len ) {
|
||||
DBGC ( netdev, "NDP neighbour advertisement link-layer address "
|
||||
"too short at %zd bytes (min %d bytes)\n",
|
||||
ll_addr_len, ll_protocol->ll_addr_len );
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
/* Update neighbour cache entry, if any */
|
||||
if ( ( rc = neighbour_update ( netdev, &ipv6_protocol, &ndp->target,
|
||||
ll_addr ) ) != 0 ) {
|
||||
DBGC ( netdev, "NDP could not update %s => %s: %s\n",
|
||||
inet6_ntoa ( &ndp->target ),
|
||||
ll_protocol->ntoa ( ll_addr ), strerror ( rc ) );
|
||||
return rc;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** An NDP option handler */
|
||||
struct ndp_option_handler {
|
||||
/** ICMPv6 type */
|
||||
uint8_t icmp_type;
|
||||
/** Option type */
|
||||
uint8_t option_type;
|
||||
/**
|
||||
* Handle received option
|
||||
*
|
||||
* @v netdev Network device
|
||||
* @v sin6_src Source socket address
|
||||
* @v ndp NDP packet
|
||||
* @v value Option value
|
||||
* @v len Option length
|
||||
* @ret rc Return status code
|
||||
*/
|
||||
int ( * rx ) ( struct net_device *netdev, struct sockaddr_in6 *sin6_src,
|
||||
struct ndp_header *ndp, const void *value, size_t len );
|
||||
};
|
||||
|
||||
/** NDP option handlers */
|
||||
static struct ndp_option_handler ndp_option_handlers[] = {
|
||||
{
|
||||
.icmp_type = ICMPV6_NDP_NEIGHBOUR_SOLICITATION,
|
||||
.option_type = NDP_OPT_LL_SOURCE,
|
||||
.rx = ndp_rx_neighbour_solicitation,
|
||||
},
|
||||
{
|
||||
.icmp_type = ICMPV6_NDP_NEIGHBOUR_ADVERTISEMENT,
|
||||
.option_type = NDP_OPT_LL_TARGET,
|
||||
.rx = ndp_rx_neighbour_advertisement,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Process received NDP option
|
||||
*
|
||||
* @v netdev Network device
|
||||
* @v sin6_src Source socket address
|
||||
* @v ndp NDP packet
|
||||
* @v type Option type
|
||||
* @v value Option value
|
||||
* @v len Option length
|
||||
* @ret rc Return status code
|
||||
*/
|
||||
static int ndp_rx_option ( struct net_device *netdev,
|
||||
struct sockaddr_in6 *sin6_src,
|
||||
struct ndp_header *ndp, unsigned int type,
|
||||
const void *value, size_t len ) {
|
||||
struct ndp_option_handler *handler;
|
||||
unsigned int i;
|
||||
|
||||
/* Locate a suitable option handler, if any */
|
||||
for ( i = 0 ; i < ( sizeof ( ndp_option_handlers ) /
|
||||
sizeof ( ndp_option_handlers[0] ) ) ; i++ ) {
|
||||
handler = &ndp_option_handlers[i];
|
||||
if ( ( handler->icmp_type == ndp->icmp.type ) &&
|
||||
( handler->option_type == type ) ) {
|
||||
return handler->rx ( netdev, sin6_src, ndp,
|
||||
value, len );
|
||||
}
|
||||
}
|
||||
|
||||
/* Silently ignore unknown options as per RFC 4861 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process received NDP packet
|
||||
*
|
||||
* @v iobuf I/O buffer
|
||||
* @v netdev Network device
|
||||
* @v sin6_src Source socket address
|
||||
* @v sin6_dest Destination socket address
|
||||
* @ret rc Return status code
|
||||
*/
|
||||
static int ndp_rx ( struct io_buffer *iobuf,
|
||||
struct net_device *netdev,
|
||||
struct sockaddr_in6 *sin6_src,
|
||||
struct sockaddr_in6 *sin6_dest __unused ) {
|
||||
struct ndp_header *ndp = iobuf->data;
|
||||
struct ndp_option *option;
|
||||
size_t remaining;
|
||||
size_t option_len;
|
||||
size_t option_value_len;
|
||||
int rc;
|
||||
|
||||
/* Sanity check */
|
||||
if ( iob_len ( iobuf ) < sizeof ( *ndp ) ) {
|
||||
DBGC ( netdev, "NDP packet too short at %zd bytes (min %zd "
|
||||
"bytes)\n", iob_len ( iobuf ), sizeof ( *ndp ) );
|
||||
rc = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Search for option */
|
||||
option = ndp->option;
|
||||
remaining = ( iob_len ( iobuf ) - offsetof ( typeof ( *ndp ), option ));
|
||||
while ( remaining ) {
|
||||
|
||||
/* Sanity check */
|
||||
if ( ( remaining < sizeof ( *option ) ) ||
|
||||
( option->blocks == 0 ) ||
|
||||
( remaining < ( option->blocks * NDP_OPTION_BLKSZ ) ) ) {
|
||||
DBGC ( netdev, "NDP bad option length:\n" );
|
||||
DBGC_HDA ( netdev, 0, option, remaining );
|
||||
rc = -EINVAL;
|
||||
goto done;
|
||||
}
|
||||
option_len = ( option->blocks * NDP_OPTION_BLKSZ );
|
||||
option_value_len = ( option_len - sizeof ( *option ) );
|
||||
|
||||
/* Handle option */
|
||||
if ( ( rc = ndp_rx_option ( netdev, sin6_src, ndp,
|
||||
option->type, option->value,
|
||||
option_value_len ) ) != 0 ) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* Move to next option */
|
||||
option = ( ( ( void * ) option ) + option_len );
|
||||
remaining -= option_len;
|
||||
}
|
||||
|
||||
done:
|
||||
free_iob ( iobuf );
|
||||
return rc;
|
||||
}
|
||||
|
||||
/** NDP ICMPv6 handlers */
|
||||
struct icmpv6_handler ndp_handlers[] __icmpv6_handler = {
|
||||
{
|
||||
.type = ICMPV6_NDP_NEIGHBOUR_SOLICITATION,
|
||||
.rx = ndp_rx,
|
||||
},
|
||||
{
|
||||
.type = ICMPV6_NDP_NEIGHBOUR_ADVERTISEMENT,
|
||||
.rx = ndp_rx,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user