diff --git a/Kconfig b/Kconfig index a420ea18..6117b2a1 100644 --- a/Kconfig +++ b/Kconfig @@ -6,6 +6,9 @@ config HOMA tristate "The Homa transport protocol" depends on IPV6 +#ifndef __STRIP__ /* See strip.py */ + depends on NET_UDP_TUNNEL +#endif /* See strip.py */ help Homa is a network transport protocol for communication within diff --git a/dissector/homa.c b/dissector/homa.c index dc1d902f..145506a1 100644 --- a/dissector/homa.c +++ b/dissector/homa.c @@ -60,6 +60,13 @@ WS_DLL_PUBLIC_DEF const int plugin_want_minor = WIRESHARK_VERSION_MINOR; #define HOMA_PROTO 0xFD +/* Fixed UDP destination port used by Homa's optional UDP hijacking + * feature (see HOMA_UDP_HIJACK_PORT in homa_wire.h); packets encapsulated + * this way carry a Homa common header immediately after the UDP header, + * so the same dissector used for IPPROTO_HOMA also works for them. + */ +#define HOMA_UDP_HIJACK_PORT 54321 + #define HOMA_HEADER_TYPE_OFFSET 13 #define HOMA_DATA_PACKET 0x10 #define HOMA_GRANT_PACKET 0x11 @@ -396,6 +403,7 @@ static void proto_reg_handoff_homa(void) homa_handle = create_dissector_handle(dissect_homa, proto_homa); dissector_add_uint("ip.proto", HOMA_PROTO, homa_handle); + dissector_add_uint("udp.port", HOMA_UDP_HIJACK_PORT, homa_handle); } WS_DLL_PUBLIC void plugin_register(void) diff --git a/homa_devel.c b/homa_devel.c index edbe762e..59ee6c04 100644 --- a/homa_devel.c +++ b/homa_devel.c @@ -7,6 +7,9 @@ #include "homa_impl.h" #include "homa_devel.h" #include "homa_grant.h" +#ifndef __STRIP__ /* See strip.py */ +#include "homa_hijack.h" +#endif /* See strip.py */ #include "homa_peer.h" #include "homa_rpc.h" #ifndef __STRIP__ /* See strip.py */ @@ -122,6 +125,38 @@ char *homa_print_ipv6_addr(const struct in6_addr *addr) return buffer; } +/** + * homa_print_hdr_offset() - Determine how many bytes past + * skb_transport_header(skb) the packet's homa_common_hdr begins, for use + * by homa_print_packet() and homa_print_packet_short(). Those functions + * are invoked both on incoming packets (already positioned at the Homa + * header by the receive path) and on outgoing packets at the point they + * are handed to ip_queue_xmit()/ip6_xmit(), which is before any IP + * header has been added; homa_skb_inner_hdr() can't be used here because + * its UDP-encapsulation detection relies on an IP header (and + * skb->protocol) that doesn't exist yet at that point. Instead, this + * peeks directly at skb_transport_header(skb): if it looks like a real + * UDP header for the UDP-hijack port, the Homa header follows + * immediately after it; otherwise skb_transport_header(skb) already + * points at the Homa header. + * @skb: Packet to examine. + * Return: Number of bytes between skb_transport_header(skb) and the + * packet's homa_common_hdr. + */ +static int homa_print_hdr_offset(struct sk_buff *skb) +{ +#ifndef __STRIP__ /* See strip.py */ + struct udphdr *uh = (struct udphdr *)skb_transport_header(skb); + + if (skb->len >= (int)(skb_transport_offset(skb) + sizeof(*uh) + + sizeof(struct homa_common_hdr)) && + uh->dest == htons(HOMA_UDP_HIJACK_PORT) && + uh->source == htons(HOMA_UDP_HIJACK_PORT)) + return sizeof(*uh); +#endif /* See strip.py */ + return 0; +} + /** * homa_print_packet() - Print a human-readable string describing the * information in a Homa packet. @@ -137,6 +172,7 @@ char *homa_print_packet(struct sk_buff *skb, char *buffer, int buf_len) char header[HOMA_MAX_HEADER]; struct in6_addr saddr; int used = 0; + int offset; if (!skb) { snprintf(buffer, buf_len, "skb is NULL!"); @@ -144,7 +180,9 @@ char *homa_print_packet(struct sk_buff *skb, char *buffer, int buf_len) return buffer; } - skb_copy_bits(skb, 0, &header, min(sizeof(header), skb->len)); + offset = homa_print_hdr_offset(skb); + skb_copy_bits(skb, offset, &header, min(sizeof(header), + skb->len - offset)); common = (struct homa_common_hdr *)header; saddr = skb_canonical_ipv6_saddr(skb); used = homa_snprintf(buffer, buf_len, used, @@ -308,9 +346,12 @@ char *homa_print_packet_short(struct sk_buff *skb, char *buffer, int buf_len) { struct homa_common_hdr *common; char header[HOMA_MAX_HEADER]; + int offset; + offset = homa_print_hdr_offset(skb); common = (struct homa_common_hdr *)header; - skb_copy_bits(skb, 0, header, min(HOMA_MAX_HEADER, skb->len)); + skb_copy_bits(skb, offset, header, min(HOMA_MAX_HEADER, + skb->len - offset)); switch (common->type) { case DATA: { struct homa_data_hdr *h = (struct homa_data_hdr *)header; diff --git a/homa_hijack.c b/homa_hijack.c index 6f8fd84b..13e895dc 100644 --- a/homa_hijack.c +++ b/homa_hijack.c @@ -7,6 +7,11 @@ #include "homa_hijack.h" #include "homa_offload.h" #include "homa_peer.h" +#include "homa_rpc.h" +#ifndef __STRIP__ /* See strip.py */ +#include +#include +#endif /* See strip.py */ /* Pointers to TCP's net_offload structures. NULL means homa_hijack_init * hasn't been called yet. @@ -127,3 +132,572 @@ void homa_hijack_set_hdr(struct sk_buff *skb, struct homa_route *route, h->checksum = ~tcp_v4_check(skb->len, route->flow.u.ip4.saddr, route->flow.u.ip4.daddr, 0); } + +#ifndef __STRIP__ /* See strip.py */ +/* The remainder of this file implements UDP hijacking; see the comments + * in homa_hijack.h for an overview. + * + * The encap_rcv/encap_err_lookup/encap_err_rcv callbacks below implement + * the data-receive and ICMP-error paths for UDP-hijacked traffic. Both + * paths reuse Homa's normal dispatch machinery (homa_softirq_dispatch(), + * homa_dispatch_pkts(), homa_abort_rpcs()) so that header validation and + * RPC handling stay identical to the native/TCP-hijacked paths; the only + * addition is transport-isolation bookkeeping (enum homa_pkt_origin) so + * a UDP-hijacked socket can't be reached by native traffic or vice versa. + */ + +/** + * homa_hijack_prepend_udp() - Push a real UDP header onto an outgoing + * Homa packet that is already completely built (i.e. skb_transport_header() + * currently points at the packet's homa_common_hdr). This is the UDP + * hijacking counterpart of homa_hijack_set_hdr(): unlike TCP hijacking, + * which reuses fields already present in the Homa header, UDP hijacking + * must add an actual 8-byte header, so this function is only ever + * invoked for sockets where homa_sock_udp_hijacked() is true, and it is + * mutually exclusive with homa_hijack_set_hdr() (never call both for the + * same packet). + * @skb: Packet buffer to modify; must have enough headroom (guaranteed + * by HOMA_SKB_EXTRA) to hold an additional UDP header. + * @route: Contains source and destination addresses for the packet. + * @ipv6: True means the packet is going to be sent via IPv6; false means + * IPv4. + */ +void homa_hijack_prepend_udp(struct sk_buff *skb, struct homa_route *route, + bool ipv6) +{ + struct udphdr *uh; + + uh = skb_push(skb, sizeof(struct udphdr)); + skb_reset_transport_header(skb); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT); + uh->len = htons(skb->len); + uh->check = 0; + if (ipv6) + udp6_set_csum(false, skb, &route->flow.u.ip6.saddr, + &route->flow.u.ip6.daddr, skb->len); + else + udp_set_csum(false, skb, route->flow.u.ip4.saddr, + route->flow.u.ip4.daddr, skb->len); +} + +/** + * homa_hijack_udp_encap_rcv() - encap_rcv callback for the UDP hijack + * tunnel sockets: strips off the outer UDP header and hands the packet + * to Homa's normal receive dispatch, tagged as having arrived via UDP so + * that homa_dispatch_pkts() can enforce transport isolation. + * @sk: Tunnel socket that received the packet. + * @skb: The received packet, still including its outer UDP header. + * Return: 0 always: the packet is always consumed here (either + * dispatched to Homa or discarded). + */ +static int homa_hijack_udp_encap_rcv(struct sock *sk, struct sk_buff *skb) +{ + if (!pskb_may_pull(skb, sizeof(struct udphdr)) || + udp_hdr(skb)->dest != htons(HOMA_UDP_HIJACK_PORT)) { + kfree_skb(skb); + return 0; + } + skb_pull(skb, sizeof(struct udphdr)); + skb_reset_transport_header(skb); + homa_softirq_dispatch(skb, HOMA_PKT_UDP); + return 0; +} + +/** + * homa_hijack_udp_quoted_ip() - Find the IP header quoted by an ICMP error. + * @skb: Error skb whose transport header points at the quoted UDP header. + * @quoted_udp: Expected location of the quoted UDP header. + * Return: The quoted IP header, or NULL if its layout is not recognized. + * + * Some kernels leave the network header at the outer ICMP packet while + * skb->data points at the quoted IP header. Others position the network + * header at the quote. Validate both layouts against the quoted UDP header. + */ +static const u8 *homa_hijack_udp_quoted_ip(struct sk_buff *skb, + const u8 *quoted_udp) +{ + const u8 *candidates[] = {skb_network_header(skb), skb->data}; + int i; + + for (i = 0; i < ARRAY_SIZE(candidates); i++) { + const u8 *candidate = candidates[i]; + int version; + + if (candidate >= quoted_udp) + continue; + version = candidate[0] >> 4; + if (version == 4) { + const struct iphdr *iph = (const struct iphdr *)candidate; + + if (iph->ihl >= 5 && iph->protocol == IPPROTO_UDP && + candidate + iph->ihl * 4 == quoted_udp) + return candidate; + } else if (version == 6) { + const struct ipv6hdr *iph = + (const struct ipv6hdr *)candidate; + + if (iph->nexthdr == IPPROTO_UDP && + candidate + sizeof(*iph) == quoted_udp) + return candidate; + } + } + return NULL; +} + +/** + * homa_hijack_udp_encap_err_lookup() - Decides whether an ICMP error + * (already known to be addressed to one of Homa's UDP hijack tunnel + * sockets) actually quotes a well-formed Homa-over-UDP packet, so that + * the kernel will go on to invoke homa_hijack_udp_encap_err_rcv() for it. + * @sk: Tunnel socket that received the ICMP error. + * @skb: Packet describing the ICMP error; skb_network_header(skb) + * points at the quoted (original) IP/IPv6 header, and + * skb_transport_header(skb) points at the quoted UDP header. + * Return: 0 if the quoted packet is a Homa-over-UDP packet with room + * for at least a common Homa header (accept), 1 otherwise + * (reject; the kernel drops the error without calling back). + */ +static int homa_hijack_udp_encap_err_lookup(struct sock *sk, + struct sk_buff *skb) +{ + int hdr_end = skb_transport_offset(skb) + sizeof(struct udphdr) + + sizeof(struct homa_common_hdr); + + if (!pskb_may_pull(skb, hdr_end)) + return 1; + if (udp_hdr(skb)->dest != htons(HOMA_UDP_HIJACK_PORT) || + udp_hdr(skb)->source != htons(HOMA_UDP_HIJACK_PORT)) + return 1; + if (!homa_hijack_udp_quoted_ip(skb, skb_transport_header(skb))) + return 1; + return 0; +} + +/** + * homa_hijack_udp_encap_err_rcv() - ICMP error callback for the UDP + * hijack tunnel sockets, invoked only after + * homa_hijack_udp_encap_err_lookup() has already verified that @skb + * quotes a well-formed Homa-over-UDP packet with room for at least a + * common Homa header. + * + * Note: @err is classified by the generic IPv4/IPv6 ICMP handling code, + * which uses different conventions than Homa's native ICMP handlers + * (homa_err_handler_v4/v6): in particular, port-unreachable errors are + * reported here as ECONNREFUSED rather than Homa's native -ENOTCONN. That + * one case is translated below for consistency; all other error codes + * are passed through unchanged (this is actually more precise than the + * native handlers, which collapse several distinct ICMP codes together). + * @sk: Tunnel socket that received the ICMP error. + * @skb: Packet describing the ICMP error; skb_network_header(skb) + * points at the quoted (original) IP/IPv6 header. + * @err: Positive errno describing the error. + * @port: Destination port from the quoted UDP header (network byte + * order); always equal to htons(HOMA_UDP_HIJACK_PORT). + * @info: For EMSGSIZE errors, the new path MTU, in host byte order. + * Unused otherwise. + * @payload: Pointer to the quoted Homa header, immediately following the + * quoted UDP header (already verified to be within the linear + * part of @skb by homa_hijack_udp_encap_err_lookup()). + */ +static void homa_hijack_udp_encap_err_rcv(struct sock *sk, + struct sk_buff *skb, int err, + __be16 port, u32 info, u8 *payload) +{ + struct homa_common_hdr *h = (struct homa_common_hdr *)payload; + struct homa_net *hnet = rcu_dereference_sk_user_data(sk); + const u8 *quoted_ip = homa_hijack_udp_quoted_ip( + skb, skb_transport_header(skb)); + struct in6_addr daddr; + int hport = ntohs(h->dport); + int error; + + if (!quoted_ip) + quoted_ip = homa_hijack_udp_quoted_ip( + skb, payload - sizeof(struct udphdr)); + if (!quoted_ip) + return; + if (quoted_ip[0] >> 4 == 6) { + const struct ipv6hdr *iph = + (const struct ipv6hdr *)quoted_ip; + + daddr = iph->daddr; + } else { + const struct iphdr *iph = (const struct iphdr *)quoted_ip; + + ipv6_addr_set_v4mapped(iph->daddr, &daddr); + } + + if (err == ECONNREFUSED) { + error = -ENOTCONN; + } else { + error = -err; + } + + if (error == -EMSGSIZE) { + int old_network_offset = skb_network_offset(skb); + + /* Existing RPCs cannot change their message geometry. Future + * RPCs will pick up the reduced MTU through a fresh route. + */ + skb_set_network_header(skb, quoted_ip - skb->data); + homa_route_update_pmtu(hnet, skb, &daddr, info); + skb_set_network_header(skb, old_network_offset); + } + + homa_abort_rpcs(hnet->homa, &daddr, hport, error, IPPROTO_UDP); +} + + +/** + * homa_hijack_udp_release_pair() - Release the UDP tunnel sockets for a + * namespace, if they exist. Idempotent. + * @hnet: Namespace whose tunnel sockets should be released. Caller + * must hold @hnet->udp_mutex, unless this is being called during + * final namespace teardown (when no other thread can be + * accessing @hnet any more). + */ +static void homa_hijack_udp_release_pair(struct homa_net *hnet) +{ + if (hnet->udp_tun4) { + udp_tunnel_sock_release(hnet->udp_tun4); + hnet->udp_tun4 = NULL; + } + if (hnet->udp_tun6) { + udp_tunnel_sock_release(hnet->udp_tun6); + hnet->udp_tun6 = NULL; + } +} + +/** + * homa_hijack_udp_create_pair() - Create the pair of UDP tunnel sockets + * (IPv4 and IPv6) used for UDP hijacking in a namespace. On failure, no + * tunnel sockets are left behind (any partially created socket is + * released before returning). + * @hnet: Namespace that will own the new tunnel sockets. Caller must + * hold @hnet->udp_mutex. + * @net: The network namespace to create the sockets in; corresponds + * to @hnet. + * Return: 0 on success, otherwise a negative errno. + */ +static int homa_hijack_udp_create_pair(struct homa_net *hnet, + struct net *net) +{ + struct udp_tunnel_sock_cfg tunnel_cfg; + struct udp_port_cfg udp_cfg; + struct socket *sock4; + struct socket *sock6; + int err; + + memset(&udp_cfg, 0, sizeof(udp_cfg)); + udp_cfg.family = AF_INET; + udp_cfg.local_udp_port = htons(HOMA_UDP_HIJACK_PORT); + udp_cfg.use_udp_checksums = 1; + err = udp_sock_create(net, &udp_cfg, &sock4); + if (err) + return err; + + memset(&udp_cfg, 0, sizeof(udp_cfg)); + udp_cfg.family = AF_INET6; + udp_cfg.local_udp_port = htons(HOMA_UDP_HIJACK_PORT); + udp_cfg.use_udp_checksums = 1; + udp_cfg.use_udp6_tx_checksums = 1; + udp_cfg.use_udp6_rx_checksums = 1; + udp_cfg.ipv6_v6only = 1; + err = udp_sock_create(net, &udp_cfg, &sock6); + if (err) { + udp_tunnel_sock_release(sock4); + return err; + } + + memset(&tunnel_cfg, 0, sizeof(tunnel_cfg)); + tunnel_cfg.sk_user_data = hnet; + tunnel_cfg.encap_type = 1; + tunnel_cfg.encap_rcv = homa_hijack_udp_encap_rcv; + tunnel_cfg.encap_err_lookup = homa_hijack_udp_encap_err_lookup; + tunnel_cfg.encap_err_rcv = homa_hijack_udp_encap_err_rcv; + setup_udp_tunnel_sock(net, sock4, &tunnel_cfg); + setup_udp_tunnel_sock(net, sock6, &tunnel_cfg); + + hnet->udp_tun4 = sock4; + hnet->udp_tun6 = sock6; + return 0; +} + +/** + * homa_hijack_udp_release_work_fn() - Work function scheduled when UDP + * hijacking is disabled while RPCs are still using it; releases the + * tunnel sockets once the last such RPC has completed. + * @work: The &homa_net.udp_release_work embedded in the target + * &struct homa_net. + */ +static void homa_hijack_udp_release_work_fn(struct work_struct *work) +{ + struct homa_net *hnet = container_of(work, struct homa_net, + udp_release_work); + + mutex_lock(&hnet->udp_mutex); + if (hnet->udp_state == HOMA_UDP_DRAINING && + atomic_read(&hnet->udp_rpc_count) == 0) { + homa_hijack_udp_release_pair(hnet); + hnet->udp_state = HOMA_UDP_DISABLED; + } + mutex_unlock(&hnet->udp_mutex); +} + +/** + * homa_hijack_udp_net_init() - Initialize the UDP hijacking fields of a + * new &struct homa_net. Does not create tunnel sockets or register + * sysctl (see homa_hijack_udp_net_start() for that); safe to call even + * when no real "struct net" exists yet (e.g. in unit tests). + * @hnet: The (newly allocated) homa_net to initialize. + */ +void homa_hijack_udp_net_init(struct homa_net *hnet) +{ + mutex_init(&hnet->udp_mutex); + hnet->udp_state = HOMA_UDP_DISABLED; + hnet->udp_tun4 = NULL; + hnet->udp_tun6 = NULL; + atomic_set(&hnet->udp_rpc_count, 0); + hnet->udp_drain_deadline = 0; + INIT_WORK(&hnet->udp_release_work, homa_hijack_udp_release_work_fn); + hnet->udp_ctl_table = NULL; + hnet->udp_ctl_header = NULL; +} + +/** + * homa_hijack_udp_sysctl_handler() - proc_handler for the per-namespace + * net.homa.hijack_udp sysctl entry; parses the requested value and then + * drives the UDP hijacking enable/disable state machine. + * @table: Sysctl table entry being accessed (a per-namespace copy whose + * @data field points at the target homa_net's @hijack_udp). + * @write: Nonzero for a write access, zero for a read access. + * @buffer: User-space buffer for input or output. + * @lenp: Number of bytes in @buffer; modified to reflect number + * actually used. + * @ppos: File position; not used by this function. + * Return: 0 on success, otherwise a negative errno. + */ +static int homa_hijack_udp_sysctl_handler(const struct ctl_table *table, + int write, void *buffer, + size_t *lenp, loff_t *ppos) +{ + struct homa_net *hnet = container_of((int *)table->data, + struct homa_net, hijack_udp); + struct ctl_table tmp_table; + int value; + int err; + + value = READ_ONCE(hnet->hijack_udp); + tmp_table = *table; + tmp_table.data = &value; + tmp_table.extra1 = SYSCTL_ZERO; + tmp_table.extra2 = SYSCTL_ONE; + + err = proc_dointvec_minmax(&tmp_table, write, buffer, lenp, ppos); + if (err || !write) + return err; + + return homa_hijack_udp_set_enabled(hnet, current->nsproxy->net_ns, + value); +} + +/* Template used to build a per-namespace copy of the sysctl table that + * exposes net.homa.hijack_udp. A private copy is needed for each + * namespace because .data must point at that namespace's homa_net. + */ +static const struct ctl_table homa_udp_ctl_table_template[] = { + { + .procname = "hijack_udp", + .maxlen = sizeof(int), + .mode = 0644, + .proc_handler = homa_hijack_udp_sysctl_handler, + }, +}; + +/** + * homa_hijack_udp_net_start() - Register the per-namespace + * net.homa.hijack_udp sysctl entry. Invoked once for each namespace, + * after homa_hijack_udp_net_init() has already initialized @hnet. + * @hnet: Namespace to register the sysctl entry for. + * @net: The network namespace corresponding to @hnet. + * Return: 0 on success, otherwise a negative errno. + */ +int homa_hijack_udp_net_start(struct homa_net *hnet, struct net *net) +{ + hnet->udp_ctl_table = kmemdup(homa_udp_ctl_table_template, + sizeof(homa_udp_ctl_table_template), + GFP_KERNEL); + if (!hnet->udp_ctl_table) + return -ENOMEM; + hnet->udp_ctl_table[0].data = &hnet->hijack_udp; + + hnet->udp_ctl_header = register_net_sysctl_sz(net, "net/homa", + hnet->udp_ctl_table, + ARRAY_SIZE(homa_udp_ctl_table_template)); + if (!hnet->udp_ctl_header) { + kfree(hnet->udp_ctl_table); + hnet->udp_ctl_table = NULL; + return -ENOMEM; + } + return 0; +} + +/** + * homa_hijack_udp_net_exit_begin() - First half of UDP hijack cleanup for + * a namespace that is being destroyed: unregisters the sysctl entry (so + * no new enable/disable requests can arrive) and marks the namespace as + * being torn down. Must be called before the namespace's sockets and + * RPCs are torn down (i.e. before homa_net_destroy()). + * @hnet: Namespace being destroyed. + */ +void homa_hijack_udp_net_exit_begin(struct homa_net *hnet) +{ + if (hnet->udp_ctl_header) { + unregister_net_sysctl_table(hnet->udp_ctl_header); + hnet->udp_ctl_header = NULL; + } + mutex_lock(&hnet->udp_mutex); + hnet->udp_state = HOMA_UDP_TEARDOWN; + mutex_unlock(&hnet->udp_mutex); +} + +/** + * homa_hijack_udp_net_destroy() - Final, unconditional UDP hijack cleanup + * for a namespace that is being destroyed: cancels any pending release + * work, releases the tunnel sockets (if any), and frees the per-namespace + * sysctl table copy. Idempotent. Must be called after the namespace's + * sockets and RPCs have already been torn down (i.e. after + * homa_net_destroy(), or in test code that never created any). + * @hnet: Namespace being destroyed. + */ +void homa_hijack_udp_net_destroy(struct homa_net *hnet) +{ + cancel_work_sync(&hnet->udp_release_work); + mutex_lock(&hnet->udp_mutex); + homa_hijack_udp_release_pair(hnet); + mutex_unlock(&hnet->udp_mutex); + kfree(hnet->udp_ctl_table); + hnet->udp_ctl_table = NULL; +} + +/** + * homa_hijack_udp_set_enabled() - Implements the UDP hijacking enable/ + * disable state machine; invoked when net.homa.hijack_udp is written. + * @hnet: Namespace whose UDP hijacking state should change. + * @net: The network namespace corresponding to @hnet. + * @enable: Nonzero to enable UDP hijacking, zero to disable it. + * Return: 0 on success, otherwise a negative errno (only possible when + * enabling, if the tunnel sockets can't be created). + */ +int homa_hijack_udp_set_enabled(struct homa_net *hnet, struct net *net, + int enable) +{ + int err = 0; + + mutex_lock(&hnet->udp_mutex); + if (hnet->udp_state == HOMA_UDP_TEARDOWN) { + mutex_unlock(&hnet->udp_mutex); + return -ENETDOWN; + } + + if (enable) { + if (hnet->udp_state == HOMA_UDP_DISABLED) { + err = homa_hijack_udp_create_pair(hnet, net); + if (err) { + mutex_unlock(&hnet->udp_mutex); + return err; + } + } + /* HOMA_UDP_DRAINING -> HOMA_UDP_ENABLED reuses the tunnel + * sockets that are still being drained; HOMA_UDP_ENABLED + * is a no-op. + */ + hnet->udp_state = HOMA_UDP_ENABLED; + WRITE_ONCE(hnet->hijack_udp, 1); + } else { + WRITE_ONCE(hnet->hijack_udp, 0); + if (hnet->udp_state == HOMA_UDP_ENABLED) { + hnet->udp_state = HOMA_UDP_DRAINING; + + /* Full barrier so this state store is visible to + * homa_hijack_udp_admit() before udp_rpc_count is + * read below; pairs with the barrier in + * homa_hijack_udp_admit(). + */ + smp_mb(); + UNIT_HOOK("udp_disable_after_state"); + if (atomic_read(&hnet->udp_rpc_count) == 0) { + homa_hijack_udp_release_pair(hnet); + hnet->udp_state = HOMA_UDP_DISABLED; + } else { + hnet->udp_drain_deadline = + hnet->homa->timer_ticks + + hnet->homa->timeout_ticks; + } + } + /* HOMA_UDP_DISABLED and HOMA_UDP_DRAINING are unaffected + * by a repeated request to disable. + */ + } + mutex_unlock(&hnet->udp_mutex); + return err; +} + +/** + * homa_hijack_udp_admit() - Invoked when a new RPC is about to become + * visible, while its socket's bucket and socket locks are held; decides + * whether the RPC may be admitted as a UDP-hijacked RPC and, if so, + * accounts for it so that the tunnel sockets aren't released while it is + * still active. + * @rpc: The new RPC (not yet visible to other threads). + * Return: 0 if the RPC may proceed (whether or not it was admitted for + * UDP hijacking; check rpc->udp_admitted to tell which), or a + * negative errno if the RPC must be rejected because UDP + * hijacking has been disabled out from under it. + */ +int homa_hijack_udp_admit(struct homa_rpc *rpc) +{ + struct homa_net *hnet = rpc->hsk->hnet; + + if (!homa_sock_udp_hijacked(rpc->hsk)) + return 0; + + atomic_inc(&hnet->udp_rpc_count); + + /* Full barrier so the increment above is visible to + * homa_hijack_udp_set_enabled() before udp_state is read below; + * pairs with the barrier in homa_hijack_udp_set_enabled(). + */ + smp_mb(); + UNIT_HOOK("udp_admit_after_increment"); + if (READ_ONCE(hnet->udp_state) != HOMA_UDP_ENABLED) { + if (atomic_dec_and_test(&hnet->udp_rpc_count) && + READ_ONCE(hnet->udp_state) == HOMA_UDP_DRAINING) + schedule_work(&hnet->udp_release_work); + return -ENETDOWN; + } + rpc->udp_admitted = true; + return 0; +} + +/** + * homa_hijack_udp_end_rpc() - Invoked from homa_rpc_end() when an RPC + * makes its (idempotent) transition to RPC_DEAD; releases any UDP + * hijacking accounting held by the RPC. Softirq-safe: never sleeps and + * never acquires @hnet->udp_mutex. + * @rpc: The RPC that is being ended. + */ +void homa_hijack_udp_end_rpc(struct homa_rpc *rpc) +{ + struct homa_net *hnet; + + if (!rpc->udp_admitted) + return; + rpc->udp_admitted = false; + hnet = rpc->hsk->hnet; + if (atomic_dec_and_test(&hnet->udp_rpc_count) && + READ_ONCE(hnet->udp_state) == HOMA_UDP_DRAINING) + schedule_work(&hnet->udp_release_work); +} +#endif /* See strip.py */ diff --git a/homa_hijack.h b/homa_hijack.h index 178748b8..ae772851 100644 --- a/homa_hijack.h +++ b/homa_hijack.h @@ -25,6 +25,10 @@ #include "homa_sock.h" #include "homa_wire.h" +#ifndef __STRIP__ /* See strip.py */ +#include +#endif /* See strip.py */ + /* Special value stored in the flags field of TCP headers to indicate that * the packet is actually a Homa packet. It includes the SYN and RST flags * which TCP never uses together; must not include URG or FIN (TSO will turn @@ -82,4 +86,122 @@ void homa_hijack_init(void); void homa_hijack_set_hdr(struct sk_buff *skb, struct homa_route *route, bool ipv6); +/** + * homa_skb_inner_hdr() - Return a pointer to the inner Homa header of an + * outgoing packet, regardless of whether it is a native, TCP-hijacked, or + * UDP-hijacked packet. skb_transport_header(skb) always identifies the + * real (outermost) transport header for outgoing packets (see + * homa_hijack_prepend_udp()); this function looks past that header, and + * past any UDP encapsulation header, to find the inner Homa header, + * performing bounds checking along the way. + * @skb: Outgoing packet to examine. + * Return: Pointer to the packet's homa_common_hdr, or NULL if @skb isn't + * a well-formed outgoing Homa packet (wrong outer protocol or + * UDP destination port, too short to hold a Homa header, or an + * invalid packet type). + */ +static inline struct homa_common_hdr *homa_skb_inner_hdr(struct sk_buff *skb) +{ + struct homa_common_hdr *h; + int eth_prot, protocol; + int extra = 0; + + eth_prot = ntohs(skb_protocol(skb, true)); + if (eth_prot == ETH_P_IP) + protocol = ip_hdr(skb)->protocol; + else if (eth_prot == ETH_P_IPV6) + protocol = ipv6_hdr(skb)->nexthdr; + else + return NULL; + +#ifndef __STRIP__ /* See strip.py */ + if (protocol == IPPROTO_UDP) { + if (!pskb_may_pull(skb, skb_transport_offset(skb) + + sizeof(struct udphdr)) || + udp_hdr(skb)->dest != htons(HOMA_UDP_HIJACK_PORT)) + return NULL; + extra = sizeof(struct udphdr); + } else if (protocol != IPPROTO_HOMA && + !(protocol == IPPROTO_TCP && homa_skb_hijacked(skb))) { + return NULL; + } +#else /* See strip.py */ + if (protocol != IPPROTO_HOMA && + !(protocol == IPPROTO_TCP && homa_skb_hijacked(skb))) + return NULL; +#endif /* See strip.py */ + + if (!pskb_may_pull(skb, skb_transport_offset(skb) + extra + + sizeof(struct homa_common_hdr))) + return NULL; + h = (struct homa_common_hdr *)(skb_transport_header(skb) + extra); + if (h->type < DATA || h->type > MAX_OP) + return NULL; + return h; +} + +#ifndef __STRIP__ /* See strip.py */ +/* UDP hijacking: an optional mechanism in which Homa packets are + * encapsulated in UDP datagrams sent to/from a dedicated pair of kernel + * "tunnel" sockets (one for IPv4, one for IPv6), using the real Linux + * udp_tunnel socket infrastructure. Unlike TCP hijacking, this preserves + * a real UDP source port that NICs/switches can use for ECMP/RSS entropy. + * See homa_hijack.c for the full implementation. + */ + +/** + * homa_sock_udp_hijacked() - Returns true if outgoing packets on a socket + * should use UDP hijacking, false otherwise. + * @hsk: Socket to check. + */ +static inline bool homa_sock_udp_hijacked(struct homa_sock *hsk) +{ + return hsk->sock.sk_protocol == IPPROTO_UDP; +} + +/** + * homa_hijack_udp_sock_select() - Perform socket initialization related to + * UDP hijacking: if TCP hijacking hasn't already claimed the socket and + * UDP hijacking is currently enabled for this namespace, arrange for + * outgoing packets on the socket to use UDP. Must be called with the + * socket not yet visible to other threads (e.g. during homa_sock_init()). + * Leaves @hsk->hnet->udp_mutex locked; the caller must eventually invoke + * homa_hijack_udp_unlock(). + * @hsk: New socket to initialize. + */ +static inline void homa_hijack_udp_sock_select(struct homa_sock *hsk) + __acquires(&hsk->hnet->udp_mutex) +{ + struct homa_net *hnet = hsk->hnet; + + mutex_lock(&hnet->udp_mutex); + if (homa_sock_hijacked(hsk)) + return; + if (hnet->udp_state == HOMA_UDP_ENABLED) + hsk->sock.sk_protocol = IPPROTO_UDP; +} + +/** + * homa_hijack_udp_unlock() - Release the lock acquired by a matching call + * to homa_hijack_udp_sock_select(). + * @hnet: Namespace whose udp_mutex should be released. + */ +static inline void homa_hijack_udp_unlock(struct homa_net *hnet) + __releases(&hnet->udp_mutex) +{ + mutex_unlock(&hnet->udp_mutex); +} + +void homa_hijack_udp_net_init(struct homa_net *hnet); +int homa_hijack_udp_net_start(struct homa_net *hnet, struct net *net); +void homa_hijack_udp_net_exit_begin(struct homa_net *hnet); +void homa_hijack_udp_net_destroy(struct homa_net *hnet); +int homa_hijack_udp_set_enabled(struct homa_net *hnet, struct net *net, + int enable); +int homa_hijack_udp_admit(struct homa_rpc *rpc); +void homa_hijack_udp_end_rpc(struct homa_rpc *rpc); +void homa_hijack_prepend_udp(struct sk_buff *skb, struct homa_route *route, + bool ipv6); +#endif /* See strip.py */ + #endif /* _HOMA_HIJACK_H */ diff --git a/homa_impl.h b/homa_impl.h index d3cccf25..338ec993 100644 --- a/homa_impl.h +++ b/homa_impl.h @@ -41,6 +41,9 @@ #include #include #include +#ifndef __STRIP__ /* See strip.py */ +#include +#endif /* See strip.py */ #include #include #include @@ -502,6 +505,83 @@ struct homa_net { * for this namespace. Managed by homa_peer.c under the peertab lock. */ int num_routes; + +#ifndef __STRIP__ /* See strip.py */ + /** + * @hijack_udp: Non-zero means enable UDP hijacking: encapsulate + * outgoing Homa packets in UDP packets sent to a pair of dedicated + * kernel tunnel sockets, so that ECMP/RSS can use the outer UDP + * source port for entropy. Set externally via sysctl; the actual + * lifecycle state is tracked separately in @udp_state because + * disabling requires an asynchronous drain. See homa_hijack.c. + */ + int hijack_udp; + + /** + * @udp_state: Current lifecycle state of the UDP hijack tunnel + * sockets for this namespace. One of the HOMA_UDP_xxx values below. + * Protected by @udp_mutex. + */ + int udp_state; +#define HOMA_UDP_DISABLED 0 +#define HOMA_UDP_ENABLED 1 +#define HOMA_UDP_DRAINING 2 +#define HOMA_UDP_TEARDOWN 3 + + /** + * @udp_mutex: Protects @udp_state, @udp_tun4, @udp_tun6, and + * transitions between them. Sleeping lock: never acquired from + * softirq/RPC-completion context (see homa_hijack.c). + */ + struct mutex udp_mutex; + + /** @udp_tun4: IPv4 UDP tunnel socket used for hijacked traffic, + * or NULL if not currently created. Protected by @udp_mutex. + */ + struct socket *udp_tun4; + + /** @udp_tun6: IPv6 UDP tunnel socket used for hijacked traffic, + * or NULL if not currently created. Protected by @udp_mutex. + */ + struct socket *udp_tun6; + + /** + * @udp_rpc_count: Number of RPCs that were admitted for UDP + * hijacking and have not yet completed. Used to determine when it + * is safe to release @udp_tun4/@udp_tun6 after hijacking is + * disabled. Manipulated with atomic ops, not @udp_mutex (see + * homa_hijack.c for the synchronization argument). + */ + atomic_t udp_rpc_count; + + /** + * @udp_drain_deadline: Value of homa->timer_ticks after which the + * timer will forcibly abort any surviving UDP-hijacked RPCs so that + * the tunnel sockets can be released. Only meaningful when + * @udp_state is HOMA_UDP_DRAINING. + */ + u32 udp_drain_deadline; + + /** + * @udp_release_work: Deferred work that releases @udp_tun4/ + * @udp_tun6 once @udp_rpc_count reaches zero while draining. Runs + * in process context because releasing tunnel sockets can sleep. + */ + struct work_struct udp_release_work; + + /** + * @udp_ctl_table: Per-namespace copy of the sysctl table used to + * expose @hijack_udp as net.homa.hijack_udp for this namespace. + * Dynamically allocated; must be kfreed. + */ + struct ctl_table *udp_ctl_table; + + /** + * @udp_ctl_header: Handle returned by register_net_sysctl for + * @udp_ctl_table, needed to unregister it later. + */ + struct ctl_table_header *udp_ctl_header; +#endif /* See strip.py */ }; /** @@ -649,7 +729,26 @@ void homa_close(struct sock *sock, long timeout); int homa_copy_to_user(struct homa_rpc *rpc); void homa_data_pkt(struct sk_buff *skb, struct homa_rpc *rpc); void homa_destroy(struct homa *homa); -void homa_dispatch_pkts(struct sk_buff *skb); +/** + * enum homa_pkt_origin - Identifies how a batch of packets passed to + * homa_dispatch_pkts() arrived, so that Homa can enforce transport + * isolation between UDP-hijacked sockets and everything else. + */ +enum homa_pkt_origin { + /** + * @HOMA_PKT_NATIVE: The packets arrived as native (IPPROTO_HOMA) + * traffic, or were recovered from the TCP pipeline by TCP hijacking; + * both cases are treated identically for transport isolation. + */ + HOMA_PKT_NATIVE = 0, + + /** + * @HOMA_PKT_UDP: The packets were delivered via a UDP hijack tunnel + * socket's encap_rcv callback (see homa_hijack.c). + */ + HOMA_PKT_UDP = 1, +}; +void homa_dispatch_pkts(struct sk_buff *skb, int origin); int homa_err_handler_v4(struct sk_buff *skb, u32 info); int homa_err_handler_v6(struct sk_buff *skb, struct inet6_skb_parm *opt, u8 type, u8 code, @@ -663,7 +762,7 @@ int homa_init(struct homa *homa); int homa_ioc_info(struct socket *sock, unsigned long arg); int homa_ioctl(struct socket *sock, unsigned int cmd, unsigned long arg); int homa_load(void); -void homa_message_out_init(struct homa_rpc *rpc, int length); +int homa_message_out_init(struct homa_rpc *rpc, int length); void homa_need_ack_pkt(struct sk_buff *skb, struct homa_sock *hsk, struct homa_rpc *rpc); void homa_net_destroy(struct homa_net *hnet); @@ -687,6 +786,7 @@ int homa_shutdown(struct socket *sock, int how); struct sk_buff *__homa_skb_alloc(int length); int homa_socket(struct sock *sk); int homa_softirq(struct sk_buff *skb); +void homa_softirq_dispatch(struct sk_buff *skb, int origin); void homa_spin(int ns); void homa_timer(struct homa *homa); void homa_timer_check_rpc(struct homa_rpc *rpc); diff --git a/homa_incoming.c b/homa_incoming.c index db041f32..03605fc2 100644 --- a/homa_incoming.c +++ b/homa_incoming.c @@ -15,6 +15,7 @@ #ifndef __STRIP__ /* See strip.py */ #include "homa_grant.h" +#include "homa_hijack.h" #include "homa_offload.h" #endif /* See strip.py */ @@ -495,8 +496,12 @@ int homa_copy_to_user(struct homa_rpc *rpc) * @skb: First packet in the batch, linked through skb->next. Caller * must ensure that packets are long enough to cover the * type-specific Homa header. + * @origin: Either HOMA_PKT_NATIVE or HOMA_PKT_UDP (see enum + * homa_pkt_origin); identifies how @skb arrived, so that + * transport isolation can be enforced between UDP-hijacked + * sockets and everything else. */ -void homa_dispatch_pkts(struct sk_buff *skb) +void homa_dispatch_pkts(struct sk_buff *skb, int origin) { const struct in6_addr saddr = skb_canonical_ipv6_saddr(skb); struct homa_common_hdr *h = (struct homa_common_hdr *)skb->data; @@ -510,6 +515,26 @@ void homa_dispatch_pkts(struct sk_buff *skb) /* Find the appropriate socket.*/ hnet = homa_net(dev_net(skb->dev)); hsk = homa_sock_find(hnet, dport); +#ifndef __STRIP__ /* See strip.py */ + /* Enforce transport isolation: a socket that has been selected for + * UDP hijacking may only receive packets that arrived via the UDP + * tunnel, and vice versa. A mismatch here doesn't mean the port is + * unused (some other transport's socket owns it), so the packets + * are silently dropped without generating a port-unreachable ICMP. + */ + if (hsk && homa_sock_udp_hijacked(hsk) != (origin == HOMA_PKT_UDP)) { + INC_METRIC(unknown_packet_types, 1); + tt_record3("Discarding packet(s) for port %u, id %llu: transport mismatch (origin %d)", + dport, homa_local_id(h->sender_id), origin); + sock_put(&hsk->sock); + while (skb) { + next = skb->next; + kfree_skb(skb); + skb = next; + } + return; + } +#endif /* See strip.py */ if (!hsk || (!homa_is_client(id) && !hsk->is_server)) { if (skb_is_ipv6(skb)) icmp6_send(skb, ICMPV6_DEST_UNREACH, @@ -563,8 +588,11 @@ void homa_dispatch_pkts(struct sk_buff *skb) if (!rpc) { if (!homa_is_client(id)) { /* We are the server for this RPC. */ - if (h->type == DATA || - h->type == START_MSG) { + if (h->type == DATA +#ifndef __STRIP__ /* See strip.py */ + || h->type == START_MSG +#endif /* See strip.py */ + ) { /* Create a new RPC if one doesn't * already exist. */ diff --git a/homa_metrics.h b/homa_metrics.h index c8e696be..04cf459c 100644 --- a/homa_metrics.h +++ b/homa_metrics.h @@ -575,7 +575,6 @@ struct homa_metrics { * because it was too short to hold all the required information. */ u64 short_packets; - /** * @packet_discards: total number of times a normal (non-retransmitted) * packet was discarded because all its data had already been received. diff --git a/homa_outgoing.c b/homa_outgoing.c index a4031207..2b38dc13 100644 --- a/homa_outgoing.c +++ b/homa_outgoing.c @@ -11,6 +11,7 @@ #include "homa_wire.h" #ifndef __STRIP__ /* See strip.py */ +#include #include "homa_hijack.h" #include "homa_qdisc.h" #endif /* See strip.py */ @@ -23,8 +24,13 @@ * locked by caller. Fields in rpc->msgout should have been * zeroed by the caller. * @length: Number of bytes that will eventually be in rpc->msgout. + * Return: 0 for success, otherwise a negative errno (e.g. the route + * MTU is too small to hold a single byte of UDP-encapsulated + * Homa DATA for a UDP-hijacked RPC). This failure is fail-fast + * and non-retryable; it is unrelated to later transmit-time + * errors from ip_queue_xmit()/ip6_xmit(). */ -void homa_message_out_init(struct homa_rpc *rpc, int length) +int homa_message_out_init(struct homa_rpc *rpc, int length) __must_hold(rpc->bucket->lock) { struct dst_entry *dst; @@ -46,6 +52,27 @@ void homa_message_out_init(struct homa_rpc *rpc, int length) rcu_read_lock(); dst = rcu_dereference(rpc->route->dst); mtu = dst_mtu(dst); + +#ifndef __STRIP__ /* See strip.py */ + if (homa_sock_udp_hijacked(rpc->hsk)) { + int min_mtu = rpc->hsk->ip_header_length + + sizeof(struct udphdr) + + sizeof(struct homa_data_hdr) + 1; + + if (mtu < min_mtu) { + rcu_read_unlock(); + return -EMSGSIZE; + } + rpc->msgout.max_seg_data = mtu - rpc->hsk->ip_header_length - + sizeof(struct udphdr) - + sizeof(struct homa_data_hdr); + rpc->msgout.max_gso_segs = 1; + rpc->msgout.max_gso_data = rpc->msgout.max_seg_data; + rcu_read_unlock(); + return 0; + } +#endif /* See strip.py */ + rpc->msgout.max_seg_data = mtu - rpc->hsk->ip_header_length - sizeof(struct homa_data_hdr); max_segs = min_t(u32, rpc->hsk->homa->max_gso_size, @@ -60,6 +87,7 @@ void homa_message_out_init(struct homa_rpc *rpc, int length) rpc->msgout.max_gso_segs = max_segs; rpc->msgout.max_gso_data = max_segs * rpc->msgout.max_seg_data; rcu_read_unlock(); + return 0; } /** @@ -501,6 +529,23 @@ int __homa_xmit_control(void *contents, size_t length, struct homa_route *route, IF_NO_STRIP(int priority); +#ifndef __STRIP__ /* See strip.py */ + if (homa_sock_udp_hijacked(hsk)) { + int padded_length = length < HOMA_MIN_PKT_LENGTH ? + HOMA_MIN_PKT_LENGTH : length; + struct dst_entry *dst; + int mtu; + + rcu_read_lock(); + dst = rcu_dereference(route->dst); + mtu = dst_mtu(dst); + rcu_read_unlock(); + if (mtu < hsk->ip_header_length + sizeof(struct udphdr) + + padded_length) + return -EMSGSIZE; + } +#endif /* See strip.py */ + skb = __homa_skb_alloc(HOMA_MAX_HEADER); if (unlikely(!skb)) return -ENOBUFS; @@ -565,6 +610,7 @@ void homa_xmit_unknown(struct sk_buff *skb, struct homa_sock *hsk) } } +#ifndef __STRIP__ /* See strip.py */ /** * homa_xmit_start_msg() - Emit a START_MSG packet for a scheduled outgoing * message (this will trigger grant generation on the receiver). @@ -584,6 +630,7 @@ void homa_xmit_start_msg(struct homa_rpc *rpc, int length) rpc->msgout.length); homa_xmit_control(START_MSG, &h, sizeof(h), rpc); } +#endif /* See strip.py */ /** * homa_xmit_data() - If an RPC has outbound data packets that are permitted diff --git a/homa_peer.c b/homa_peer.c index c4e52cd9..33588e07 100644 --- a/homa_peer.c +++ b/homa_peer.c @@ -9,6 +9,8 @@ #include "homa_rpc.h" #include +#include +#include #ifdef __UNIT_TEST__ #undef rhashtable_init @@ -350,12 +352,20 @@ struct homa_route *homa_route_alloc(struct homa_sock *hsk, route->flow.flowi_secid = key->secid; if (ipv6_addr_v4mapped(&route->key.daddr)) { struct rtable *rt; + __be16 port = 0; + u8 proto = IPPROTO_HOMA; +#ifndef __STRIP__ /* See strip.py */ + if (key->sk_protocol == IPPROTO_UDP) { + proto = IPPROTO_UDP; + port = htons(HOMA_UDP_HIJACK_PORT); + } +#endif /* See strip.py */ flowi4_init_output(&route->flow.u.ip4, key->bound_dev_if, key->mark, 0, RT_SCOPE_UNIVERSE, - IPPROTO_HOMA, 0, + proto, 0, ipv6_to_ipv4(route->key.daddr), - ipv6_to_ipv4(route->key.saddr), 0, 0, + ipv6_to_ipv4(route->key.saddr), port, port, key->uid); rt = ip_route_output_flow(sock_net(&hsk->sock), &route->flow.u.ip4, &hsk->sock); @@ -365,15 +375,23 @@ struct homa_route *homa_route_alloc(struct homa_sock *hsk, } rcu_assign_pointer(route->dst, &rt->dst); } else { + __be16 port = 0; + /* This code is derived from code in tcp_v6_connect. */ route->flow.u.ip6.flowi6_proto = IPPROTO_HOMA; +#ifndef __STRIP__ /* See strip.py */ + if (key->sk_protocol == IPPROTO_UDP) { + route->flow.u.ip6.flowi6_proto = IPPROTO_UDP; + port = htons(HOMA_UDP_HIJACK_PORT); + } +#endif /* See strip.py */ route->flow.u.ip6.daddr = route->key.daddr; route->flow.u.ip6.saddr = route->key.saddr; route->flow.u.ip6.flowlabel = ip6_make_flowinfo(0, 0); route->flow.u.ip6.flowi6_oif = key->bound_dev_if; route->flow.u.ip6.flowi6_mark = key->mark; - route->flow.u.ip6.fl6_dport = 0; - route->flow.u.ip6.fl6_sport = 0; + route->flow.u.ip6.fl6_dport = port; + route->flow.u.ip6.fl6_sport = port; route->flow.u.ip6.flowi6_uid = key->uid; dst = ip6_dst_lookup_flow(sock_net(&hsk->sock), &hsk->sock, &route->flow.u.ip6, NULL); @@ -542,6 +560,77 @@ int homa_route_validate(struct homa_rpc *rpc) return 0; } +#ifndef __STRIP__ /* See strip.py */ +/** + * homa_route_update_pmtu() - Invoked when an ICMP error indicates that the + * effective path MTU to a destination has decreased (IPv4 "fragmentation + * needed" or IPv6 "packet too big"). Updates the kernel's PMTU exception + * cache for the destination (so that future route lookups will reflect the + * new MTU) and removes Homa's own cached route(s) to that destination from + * peertab->route_ht (so that RPCs currently using the route will look up a + * fresh one, picking up the reduced MTU via the generic dst_mtu() + * machinery). Only the table's own reference on each matching route is + * dropped; RPCs that are still using a route keep their own reference and + * are unaffected until they next call homa_route_validate() or complete. + * Idempotent: safe to call even if no matching routes remain. + * @hnet: Namespace that owns @skb. + * @skb: The ICMP error packet; its network header must be positioned + * at the quoted (original) IP/IPv6 header that triggered the + * error, and skb->dev must be the receiving device. This is + * guaranteed for the UDP hijack tunnel socket's encap_err_rcv + * callback. + * @daddr: Destination address (network order; IPv4 addresses are + * represented as IPv4-mapped IPv6 addresses) whose cached + * route(s) should be updated. + * @mtu: New path MTU, in host byte order. + */ +void homa_route_update_pmtu(struct homa_net *hnet, struct sk_buff *skb, + const struct in6_addr *daddr, u32 mtu) +{ + struct homa_peertab *peertab = hnet->homa->peertab; + struct net *net = dev_net(skb->dev); + bool is_ipv6 = skb_is_ipv6(skb); + struct rhashtable_iter iter; + struct homa_route *route; + + rhashtable_walk_enter(&peertab->route_ht, &iter); + rhashtable_walk_start(&iter); + while (1) { + route = rhashtable_walk_next(&iter); + if (!route) + break; + if (IS_ERR(route)) + continue; + if (route->key.hnet != hnet || + !ipv6_addr_equal(&route->key.daddr, daddr)) + continue; + + if (is_ipv6) + ip6_update_pmtu(skb, net, htonl(mtu), + route->key.bound_dev_if, + route->key.mark, route->key.uid); + else + ipv4_update_pmtu(skb, net, mtu, + route->key.bound_dev_if, + route->key.sk_protocol); + + spin_lock_bh(&peertab->lock); + if (rhashtable_remove_fast(&peertab->route_ht, + &route->ht_linkage, + route_ht_params) == 0) { + peertab->num_routes--; + hnet->num_routes--; + tt_record1("homa_route_update_pmtu removed route for 0x%x", + tt_addr(*daddr)); + homa_route_release(route); + } + spin_unlock_bh(&peertab->lock); + } + rhashtable_walk_stop(&iter); + rhashtable_walk_exit(&iter); +} +#endif /* See strip.py */ + /** * homa_route_gc() - This function is invoked by Homa at regular intervals; * its job is to ensure that the number of routes stays within limits. If diff --git a/homa_peer.h b/homa_peer.h index 6ae00836..9acfb380 100644 --- a/homa_peer.h +++ b/homa_peer.h @@ -265,6 +265,14 @@ struct homa_route_key { /** @bound_dev_if: sock->sk_bound_dev_if */ int bound_dev_if; + + /** + * @sk_protocol: sock->sk_protocol (IPPROTO_HOMA, IPPROTO_TCP, or + * IPPROTO_UDP). Separates cache entries by transport so that + * native, TCP-hijacked, and UDP-hijacked traffic to the same peer + * never share a dst_entry/flow. + */ + u8 sk_protocol; }; /** @@ -370,6 +378,11 @@ int homa_route_prefer_evict(struct homa_peertab *peertab, struct homa_route *route2); int homa_route_validate(struct homa_rpc *rpc); +#ifndef __STRIP__ /* See strip.py */ +void homa_route_update_pmtu(struct homa_net *hnet, struct sk_buff *skb, + const struct in6_addr *daddr, u32 mtu); +#endif /* See strip.py */ + extern const struct rhashtable_params peer_ht_params; extern const struct rhashtable_params route_ht_params; @@ -464,11 +477,21 @@ static inline int homa_route_xmit(struct sk_buff *skb, struct homa_sock *hsk, rcu_read_unlock(); IF_NO_STRIP(priority = hsk->homa->priority_map[priority]); if (ipv6_addr_v4mapped(&route->peer->addr)) { - IF_NO_STRIP(homa_hijack_set_hdr(skb, route, false)); +#ifndef __STRIP__ /* See strip.py */ + if (homa_sock_udp_hijacked(hsk)) + homa_hijack_prepend_udp(skb, route, false); + else + homa_hijack_set_hdr(skb, route, false); +#endif /* See strip.py */ hsk->inet.tos = priority << 5; return ip_queue_xmit(&hsk->inet.sk, skb, &route->flow); } - IF_NO_STRIP(homa_hijack_set_hdr(skb, route, true)); +#ifndef __STRIP__ /* See strip.py */ + if (homa_sock_udp_hijacked(hsk)) + homa_hijack_prepend_udp(skb, route, true); + else + homa_hijack_set_hdr(skb, route, true); +#endif /* See strip.py */ return ip6_xmit(&hsk->inet.sk, skb, &route->flow.u.ip6, 0, NULL, priority << 5, 0); } @@ -529,6 +552,7 @@ static inline void homa_route_key_init(struct homa_route_key *key, security_sk_classify_flow(&hsk->sock, &flowic); key->secid = flowic.flowic_secid; key->bound_dev_if = hsk->sock.sk_bound_dev_if; + key->sk_protocol = hsk->sock.sk_protocol; } #endif /* _HOMA_PEER_H */ diff --git a/homa_plumbing.c b/homa_plumbing.c index 43c799e0..b1c7d137 100644 --- a/homa_plumbing.c +++ b/homa_plumbing.c @@ -721,8 +721,19 @@ module_exit(homa_unload); */ int homa_net_start(struct net *net) { + struct homa_net *hnet = homa_net(net); + int err; + pr_notice("Homa attaching to net namespace\n"); - return homa_net_init(homa_net(net), &homa_data); + err = homa_net_init(hnet, &homa_data); +#ifndef __STRIP__ /* See strip.py */ + if (err) + return err; + err = homa_hijack_udp_net_start(hnet, net); + if (err) + homa_net_destroy(hnet); +#endif /* See strip.py */ + return err; } /** @@ -732,8 +743,11 @@ int homa_net_start(struct net *net) */ void homa_net_exit(struct net *net) { + struct homa_net *hnet = homa_net(net); + pr_notice("Homa detaching from net namespace\n"); - homa_net_destroy(homa_net(net)); + IF_NO_STRIP(homa_hijack_udp_net_exit_begin(hnet)); + homa_net_destroy(hnet); } /** @@ -1259,7 +1273,11 @@ int homa_sendmsg(struct sock *sk, struct msghdr *msg, size_t length) : tt_addr(addr->in6.sin6_addr), ntohs(addr->in6.sin6_port), rpc->id, length); rpc->completion_cookie = args.completion_cookie; - homa_message_out_init(rpc, msg->msg_iter.count); + result = homa_message_out_init(rpc, msg->msg_iter.count); + if (result) { + hsk->error_msg = "route MTU too small for message"; + goto error; + } #ifndef __STRIP__ /* See strip.py */ if (rpc->msgout.granted == 0) homa_xmit_start_msg(rpc, msg->msg_iter.count); @@ -1320,7 +1338,11 @@ int homa_sendmsg(struct sock *sk, struct msghdr *msg, size_t length) } rpc->state = RPC_OUTGOING; - homa_message_out_init(rpc, msg->msg_iter.count); + result = homa_message_out_init(rpc, msg->msg_iter.count); + if (result) { + hsk->error_msg = "route MTU too small for message"; + goto error; + } #ifndef __STRIP__ /* See strip.py */ if (rpc->msgout.granted == 0) homa_xmit_start_msg(rpc, msg->msg_iter.count); @@ -1578,12 +1600,18 @@ void homa_unhash(struct sock *sk) } /** - * homa_softirq() - This function is invoked at SoftIRQ level to handle - * incoming packets. - * @skb: The incoming packet. - * Return: Always 0 + * homa_softirq_dispatch() - Shared body for homa_softirq(): processes a + * batch of incoming packets, tagging them with @origin so that + * homa_dispatch_pkts() can enforce transport isolation. Used both by + * homa_softirq() itself (for native/TCP-hijacked packets) and by the UDP + * hijack tunnel's encap_rcv callback (for UDP-hijacked packets); see + * homa_hijack.c. + * @skb: The incoming packet (or, for GRO batches, the first packet + * in a list linked through skb_shinfo(skb)->frag_list). + * @origin: Either HOMA_PKT_NATIVE or HOMA_PKT_UDP (see enum + * homa_pkt_origin). */ -int homa_softirq(struct sk_buff *skb) +void homa_softirq_dispatch(struct sk_buff *skb, int origin) { struct sk_buff *packets, *other_pkts, *next; struct sk_buff **prev_link, **other_link; @@ -1680,7 +1708,7 @@ int homa_softirq(struct sk_buff *skb) h->type); *prev_link = skb->next; skb->next = NULL; - homa_dispatch_pkts(skb); + homa_dispatch_pkts(skb, origin); } else { prev_link = &skb->next; } @@ -1732,7 +1760,7 @@ int homa_softirq(struct sk_buff *skb) UNIT_LOG("", " %d", ntohl(h3->seg.offset)); } #endif /* __UNIT_TEST__ */ - homa_dispatch_pkts(packets); + homa_dispatch_pkts(packets, origin); packets = other_pkts; } @@ -1740,6 +1768,17 @@ int homa_softirq(struct sk_buff *skb) atomic_dec(&per_cpu(homa_offload_core, raw_smp_processor_id()).softirq_backlog); #endif /* See strip.py */ INC_METRIC(softirq_cycles, homa_clock() - start); +} + +/** + * homa_softirq() - This function is invoked at SoftIRQ level to handle + * incoming packets. + * @skb: The incoming packet. + * Return: Always 0 + */ +int homa_softirq(struct sk_buff *skb) +{ + homa_softirq_dispatch(skb, HOMA_PKT_NATIVE); return 0; } @@ -1782,7 +1821,7 @@ int homa_err_handler_v4(struct sk_buff *skb, u32 info) __func__, info, type, code); } if (error != 0) - homa_abort_rpcs(homa, &daddr, port, error); + homa_abort_rpcs(homa, &daddr, port, error, IPPROTO_HOMA); return 0; } @@ -1819,7 +1858,7 @@ int homa_err_handler_v6(struct sk_buff *skb, struct inet6_skb_parm *opt, error = -EPROTONOSUPPORT; } if (error != 0) - homa_abort_rpcs(homa, &iph->daddr, port, error); + homa_abort_rpcs(homa, &iph->daddr, port, error, IPPROTO_HOMA); return 0; } diff --git a/homa_pool.c b/homa_pool.c index 2e9cbc08..cff05bf7 100644 --- a/homa_pool.c +++ b/homa_pool.c @@ -583,7 +583,9 @@ void homa_pool_wakeup_rpc(struct homa_rpc *rpc) */ resend.offset = htonl(0); resend.length = htonl(rpc->msgin.length); +#ifndef __STRIP__ /* See strip.py */ resend.priority = homa_high_priority(rpc->hsk->homa); +#endif /* See strip.py */ homa_xmit_control(RESEND, &resend, sizeof(resend), rpc); } diff --git a/homa_qdisc.c b/homa_qdisc.c index c191df2d..1e42f376 100755 --- a/homa_qdisc.c +++ b/homa_qdisc.c @@ -163,8 +163,17 @@ static inline bool is_homa_pkt(struct sk_buff *skb) protocol = ipv6_hdr(skb)->nexthdr; else return false; - return protocol == IPPROTO_HOMA || - (protocol == IPPROTO_TCP && homa_skb_hijacked(skb)); + if (protocol == IPPROTO_HOMA || + (protocol == IPPROTO_TCP && homa_skb_hijacked(skb))) + return true; +#ifndef __STRIP__ /* See strip.py */ + if (protocol == IPPROTO_UDP) + return skb->len >= skb_transport_offset(skb) + + sizeof(struct udphdr) + + sizeof(struct homa_common_hdr) && + udp_hdr(skb)->dest == htons(HOMA_UDP_HIJACK_PORT); +#endif /* See strip.py */ + return false; } /** @@ -586,7 +595,12 @@ int homa_qdisc_enqueue(struct sk_buff *skb, struct Qdisc *sch, * issue any grants, even though the "incoming" data isn't going to * be transmitted anytime soon. */ - h = (struct homa_data_hdr *)skb_transport_header(skb); + h = (struct homa_data_hdr *)homa_skb_inner_hdr(skb); + if (!h) { + INC_METRIC(short_packets, 1); + homa_qdisc_update_link_idle(qdev, pkt_len, -1); + goto enqueue; + } offset = ntohl(h->seg.offset); if (h->common.type != DATA || ntohl(h->msg_length) < qshared->defer_min_bytes) { @@ -610,8 +624,8 @@ int homa_qdisc_enqueue(struct sk_buff *skb, struct Qdisc *sch, enqueue: if (is_homa_pkt(skb)) { - h = (struct homa_data_hdr *)skb_transport_header(skb); - if (h->common.type == DATA) { + h = (struct homa_data_hdr *)homa_skb_inner_hdr(skb); + if (h && h->common.type == DATA) { tt_record3("homa_qdisc_enqueue queuing homa data packet for id %d, offset %d on qid %d", be64_to_cpu(h->common.sender_id), offset, q->ix); @@ -750,8 +764,11 @@ void homa_qdisc_defer_homa(struct homa_qdisc_dev *qdev, struct sk_buff *skb) struct homa_data_hdr *h; int bytes_left; - h = (struct homa_data_hdr *)skb_transport_header(skb); - bytes_left = rpc->msgout.length - ntohl(h->seg.offset); + h = (struct homa_data_hdr *)homa_skb_inner_hdr(skb); + if (WARN_ON_ONCE(!h)) + bytes_left = rpc->qrpc.tx_left; + else + bytes_left = rpc->msgout.length - ntohl(h->seg.offset); if (bytes_left < rpc->qrpc.tx_left) rpc->qrpc.tx_left = bytes_left; rpc->qrpc.qdev = qdev; @@ -945,11 +962,13 @@ struct sk_buff *homa_qdisc_get_deferred_homa(struct homa_qdisc_dev *qdev) * it's position won't change because it is already highest priority). */ info = homa_get_skb_info(skb); - h = (struct homa_data_hdr *)skb_transport_header(skb); - bytes_left = rpc->msgout.length - (ntohl(h->seg.offset) + - info->data_bytes); - if (bytes_left < qrpc->tx_left) - qrpc->tx_left = bytes_left; + h = (struct homa_data_hdr *)homa_skb_inner_hdr(skb); + if (!WARN_ON_ONCE(!h)) { + bytes_left = rpc->msgout.length - (ntohl(h->seg.offset) + + info->data_bytes); + if (bytes_left < qrpc->tx_left) + qrpc->tx_left = bytes_left; + } if (fifo) { if (skb_queue_len(&qrpc->packets) > 0) { rb_erase_cached(node, &qdev->deferred_rpcs); @@ -991,9 +1010,10 @@ int homa_qdisc_xmit_deferred_homa(struct homa_qdisc_dev *qdev) pkt_len = qdisc_pkt_len(skb); homa_qdisc_update_link_idle(qdev, pkt_len, -1); - h = (struct homa_data_hdr *)skb_transport_header(skb); - tt_record2("homa_qdisc_pacer queuing homa data packet for id %d, offset %d", - be64_to_cpu(h->common.sender_id), ntohl(h->seg.offset)); + h = (struct homa_data_hdr *)homa_skb_inner_hdr(skb); + if (h) + tt_record2("homa_qdisc_pacer queuing homa data packet for id %d, offset %d", + be64_to_cpu(h->common.sender_id), ntohl(h->seg.offset)); /* Run the packet through dev_queue_xmit again to transmit it; * this means it will pass through homa_disc_enqueue again, but diff --git a/homa_rpc.c b/homa_rpc.c index 82e3f1fa..c032f47e 100644 --- a/homa_rpc.c +++ b/homa_rpc.c @@ -10,6 +10,7 @@ #ifndef __STRIP__ /* See strip.py */ #include "homa_grant.h" +#include "homa_hijack.h" #include "homa_qdisc.h" #endif /* See strip.py */ @@ -80,6 +81,15 @@ struct homa_rpc *homa_rpc_alloc_client(struct homa_sock *hsk, err = -ESHUTDOWN; goto error; } +#ifndef __STRIP__ /* See strip.py */ + err = homa_hijack_udp_admit(crpc); + if (err) { + homa_sock_unlock(hsk); + homa_rpc_unlock(crpc); + hsk->error_msg = "UDP hijacking has been disabled"; + goto error; + } +#endif /* See strip.py */ hlist_add_head(&crpc->hash_links, &bucket->rpcs); rcu_read_lock(); list_add_tail_rcu(&crpc->active_links, &hsk->active_rpcs); @@ -201,6 +211,13 @@ struct homa_rpc *homa_rpc_alloc_server(struct homa_sock *hsk, err = -ESHUTDOWN; goto error; } +#ifndef __STRIP__ /* See strip.py */ + err = homa_hijack_udp_admit(srpc); + if (err) { + homa_sock_unlock(hsk); + goto error; + } +#endif /* See strip.py */ hlist_add_head(&srpc->hash_links, &bucket->rpcs); list_add_tail_rcu(&srpc->active_links, &hsk->active_rpcs); homa_sock_unlock(hsk); @@ -310,6 +327,7 @@ void homa_rpc_end(struct homa_rpc *rpc) rpc->hsk->port); rpc->state = RPC_DEAD; rpc->error = -EINVAL; + IF_NO_STRIP(homa_hijack_udp_end_rpc(rpc)); #ifndef __STRIP__ /* See strip.py */ /* The following line must occur before the socket is locked. This is @@ -373,9 +391,10 @@ void homa_rpc_abort(struct homa_rpc *rpc, int error) * @port: If nonzero, then RPCs will only be aborted if they were * targeted at this server port. * @error: Negative errno value indicating the reason for the abort. + * @protocol: If nonzero, only sockets using this transport are scanned. */ void homa_abort_rpcs(struct homa *homa, const struct in6_addr *addr, - int port, int error) + int port, int error, int protocol) { struct homa_socktab_scan scan; struct homa_sock *hsk; @@ -383,6 +402,8 @@ void homa_abort_rpcs(struct homa *homa, const struct in6_addr *addr, for (hsk = homa_socktab_start_scan(homa->socktab, &scan); hsk; hsk = homa_socktab_next(&scan)) { + if (protocol && hsk->sock.sk_protocol != protocol) + continue; /* Skip the (expensive) lock acquisition if there's no * work to do. */ @@ -746,8 +767,22 @@ struct homa_rpc *homa_rpc_find_from_skb(struct sk_buff *skb, bool incoming) int port; u64 id; - /* Find the appropriate socket.*/ - h = (struct homa_common_hdr *)skb_transport_header(skb); + /* Find the appropriate socket. For outgoing packets, the inner Homa + * header may be preceded by a UDP encapsulation header, so use the + * bounds-checked accessor; incoming packets are already positioned + * at the Homa header by the receive path. + */ + if (incoming) { + h = (struct homa_common_hdr *)skb_transport_header(skb); + } else { +#ifndef __STRIP__ /* See strip.py */ + h = homa_skb_inner_hdr(skb); + if (!h) + return NULL; +#else /* See strip.py */ + h = (struct homa_common_hdr *)skb_transport_header(skb); +#endif /* See strip.py */ + } id = be64_to_cpu(h->sender_id); if (incoming) { port = ntohs(h->dport); diff --git a/homa_rpc.h b/homa_rpc.h index 48a6088b..7764967e 100644 --- a/homa_rpc.h +++ b/homa_rpc.h @@ -475,6 +475,16 @@ struct homa_rpc { */ u32 done_timer_ticks; +#ifndef __STRIP__ /* See strip.py */ + /** + * @udp_admitted: True means this RPC was admitted as a UDP-hijacked + * RPC (see homa_hijack_udp_admit()) and so holds a reference against + * its namespace's udp_rpc_count that must be released exactly once, + * via homa_hijack_udp_end_rpc(), when the RPC dies. + */ + bool udp_admitted; +#endif /* See strip.py */ + /** * @magic: when the RPC is alive, this holds a distinct value that * is unlikely to occur naturally. The value is cleared when the @@ -492,7 +502,7 @@ struct homa_rpc { }; void homa_abort_rpcs(struct homa *homa, const struct in6_addr *addr, - int port, int error); + int port, int error, int protocol); void homa_abort_sock_rpcs(struct homa_sock *hsk, int error); void homa_rpc_abort(struct homa_rpc *crpc, int error); void homa_rpc_ack(struct homa_sock *hsk, struct homa_rpc *rpc, diff --git a/homa_sock.c b/homa_sock.c index dda17cf4..f0a43910 100644 --- a/homa_sock.c +++ b/homa_sock.c @@ -239,6 +239,7 @@ int homa_sock_init(struct homa_sock *hsk) hsk->sock.sk_sndbuf = homa->wmem_max; sock_set_flag(&hsk->inet.sk, SOCK_RCU_FREE); IF_NO_STRIP(homa_hijack_sock_init(hsk)); + IF_NO_STRIP(homa_hijack_udp_sock_select(hsk)); /* This is needed to prevent blocking when allocating memory in * functions like ip_route_output_flow, which could be invoked @@ -271,10 +272,13 @@ int homa_sock_init(struct homa_sock *hsk) } result = homa_sock_link(hsk, hnet->prev_default_port); spin_unlock_bh(&socktab->write_lock); - if (result == 0) + if (result == 0) { + IF_NO_STRIP(homa_hijack_udp_unlock(hnet)); return result; + } error: + IF_NO_STRIP(homa_hijack_udp_unlock(hnet)); hsk->shutdown = true; hsk->homa = NULL; homa_pool_free(buffer_pool); diff --git a/homa_timer.c b/homa_timer.c index 8f689ab5..d240ef25 100644 --- a/homa_timer.c +++ b/homa_timer.c @@ -82,6 +82,19 @@ void homa_timer_check_rpc(struct homa_rpc *rpc) struct homa *homa = rpc->hsk->homa; int tx_end = homa_rpc_tx_end(rpc); +#ifndef __STRIP__ /* See strip.py */ + /* If UDP hijacking is being disabled for this RPC's namespace and + * this RPC hasn't finished by the drain deadline, abort it so that + * the namespace's UDP tunnel sockets can be released. + */ + if (rpc->udp_admitted && + READ_ONCE(rpc->hsk->hnet->udp_state) == HOMA_UDP_DRAINING && + (s32)(homa->timer_ticks - rpc->hsk->hnet->udp_drain_deadline) >= 0) { + homa_rpc_abort(rpc, -ENETDOWN); + return; + } +#endif /* See strip.py */ + /* See if we need to request an ack for this RPC. */ if (!homa_is_client(rpc->id) && rpc->state == RPC_OUTGOING && tx_end == rpc->msgout.length) { diff --git a/homa_utils.c b/homa_utils.c index f600d7e8..5e7584b7 100644 --- a/homa_utils.c +++ b/homa_utils.c @@ -11,6 +11,7 @@ #ifndef __STRIP__ /* See strip.py */ #include "homa_grant.h" +#include "homa_hijack.h" #include "homa_qdisc.h" #endif /* See strip.py */ @@ -158,6 +159,7 @@ int homa_net_init(struct homa_net *hnet, struct homa *homa) memset(hnet, 0, sizeof(*hnet)); hnet->homa = homa; hnet->prev_default_port = HOMA_MIN_DEFAULT_PORT - 1; + IF_NO_STRIP(homa_hijack_udp_net_init(hnet)); return 0; } @@ -170,6 +172,7 @@ void homa_net_destroy(struct homa_net *hnet) { homa_socktab_destroy(hnet->homa->socktab, hnet); homa_peer_free_net(hnet); + IF_NO_STRIP(homa_hijack_udp_net_destroy(hnet)); } #ifndef __STRIP__ /* See strip.py */ diff --git a/homa_wire.h b/homa_wire.h index 28af7c11..5ce296d4 100644 --- a/homa_wire.h +++ b/homa_wire.h @@ -168,6 +168,13 @@ struct homa_common_hdr { */ __be16 urgent; #define HOMA_HIJACK_URGENT 0xb97d + +/** + * HOMA_UDP_HIJACK_PORT - UDP port number (both source and destination) + * used for the pair of kernel tunnel sockets that implement UDP hijacking + * (see homa_hijack.c). Chosen from the dynamic/private port range. + */ +#define HOMA_UDP_HIJACK_PORT 54321 #else /* See strip.py */ /** @reserved2: Not used (corresponds to TCP urgent field). */ __be16 reserved2; diff --git a/man/homa.7 b/man/homa.7 index 10c7342f..37c28009 100644 --- a/man/homa.7 +++ b/man/homa.7 @@ -608,6 +608,31 @@ requires Homa to intercept all incoming TCP packets to see if they are actually Homa packets. Some might object to this interference with the rest of the Linux kernel. .TP +.IR hijack_udp +An integer value; if nonzero, Homa will transmit its packets encapsulated +in UDP packets (using a fixed, reserved destination port) instead of using +IPPROTO_HOMA directly. Like +.IR hijack_tcp , +this allows Homa to take advantage of NIC hardware support (such as RSS +and checksum offload) that isn't always available for a raw IPPROTO_HOMA +socket. The transport used by a given socket is selected once, when the +socket is created; +.IR hijack_tcp +takes precedence over +.IR hijack_udp +if both are enabled at that time. Enabling this parameter takes effect +immediately for new sockets; existing UDP-hijacked RPCs are drained (up +to a bounded time limit) before the underlying tunnel sockets are torn +down, rather than being switched over in place. Homa owns the reserved +UDP port outright: there is no fallback to plain UDP or coexistence with +other users of that port, so enabling this parameter will fail (leaving +hijacking off) if the port cannot be reserved. Because there is no +fallback, packets sent with UDP hijacking enabled always require correct +UDP checksums; this is a current (v1) performance limitation compared to +.IR hijack_tcp , +since checksum computation cannot be skipped or offloaded in every +configuration. +.TP .IR homa_share When there exist both Homa and TCP packets whose transmission has been deferred because the NIC queue is overloaded, this determines how the diff --git a/test/Makefile b/test/Makefile index 693b8640..eca9f2bb 100644 --- a/test/Makefile +++ b/test/Makefile @@ -22,7 +22,8 @@ CINCLUDES := \ -I. \ -I.. \ $(KERN_INCLUDES) \ - -include $(KDIR)/include/linux/kconfig.h + -include $(KDIR)/include/linux/kconfig.h \ + -include mock.h CCINCLUDES := \ -I. \ -I.. \ @@ -36,11 +37,12 @@ DEFS += -D__STRIP__ endif WARNS := -Wall -Wundef -Wno-trigraphs -Wno-sign-compare -Wuninitialized \ - -Wno-strict-aliasing -Wunused-but-set-variable -Werror -CFLAGS := $(WARNS) -Wstrict-prototypes -MD -no-pie -g $(CINCLUDES) $(DEFS) \ - -fsanitize=address -fno-omit-frame-pointer -CCFLAGS := -std=c++11 $(WARNS) -MD -g $(CCINCLUDES) $(DEFS) \ - -fsanitize=address -fno-omit-frame-pointer + -Wno-strict-aliasing -Wunused-but-set-variable -Wno-unused-variable \ + -Wno-pointer-sign -Werror +CFLAGS := $(WARNS) -Wstrict-prototypes -MD -no-pie -g -fcf-protection \ + -fsanitize=address -fno-omit-frame-pointer $(CINCLUDES) $(DEFS) +CCFLAGS := -std=c++11 $(WARNS) -MD -g -fcf-protection \ + -fsanitize=address -fno-omit-frame-pointer $(CCINCLUDES) $(DEFS) TEST_SRCS := unit_homa_incoming.c \ unit_homa_interest.c \ @@ -129,50 +131,18 @@ xxhash.o: xxhash.c $(CC) -c $(CFLAGS) -O2 -fno-sanitize=address $< -o $@ unit: $(OBJS) - $(CXX) $(CFLAGS) $^ -o $@ + $(CXX) $(CFLAGS) $^ -o $@ -lasan test: unit ./unit -# Additional definitions for running unit tests using stripped sources. - -S_HOMA_SRCS := $(patsubst %,stripped/%,$(filter-out timetrace.c, $(HOMA_SRCS))) -S_HOMA_OBJS := $(patsubst %.c,%.o,$(S_HOMA_SRCS)) -S_HOMA_HDRS := stripped/homa.h \ - stripped/homa_impl.h \ - stripped/homa_peer.h \ - stripped/homa_pool.h \ - stripped/homa_receiver.h \ - stripped/homa_rpc.h \ - stripped/homa_sock.h \ - stripped/homa_stub.h \ - stripped/homa_wire.h -stripped/%.c: ../%.c - ../util/strip.py --alt $< > $@ -stripped/%.h: ../%.h - ../util/strip.py --alt $< > $@ -S_TEST_OBJS := $(patsubst %,stripped/%,$(filter-out unit_timetrace.o, $(TEST_OBJS))) -S_OBJS := $(S_HOMA_OBJS) $(S_TEST_OBJS) $(patsubst %,stripped/%,$(OTHER_OBJS)) - -$(S_OBJS): | stripped $(S_HOMA_HDRS) - -stripped: - mkdir -p stripped - -stripped/%.o: stripped/%.c - $(CC) -c $(patsubst -I..,-Istripped,$(CFLAGS)) $< -o $@ -stripped/%.o: %.c - $(CC) -c $(patsubst -I..,-Istripped,$(CFLAGS)) $< -o $@ -stripped/%.o: %.cc - $(CXX) -c $(patsubst -I..,-Istripped,$(CCFLAGS)) $< -o $@ - -s_unit: $(S_OBJS) - $(CXX) $(CFLAGS) $^ -o $@ -lasan - -s_test: s_unit - ./s_unit - -CLEANS += s_unit +# Compile-time stripping keeps unit-test hooks while excluding code that is +# omitted from the upstream build. Use clean submakes so changed compiler +# definitions cannot reuse objects from a normal unit-test build. +.PHONY: s_test +s_test: + $(MAKE) clean + $(MAKE) __STRIP__=y test # The target below shouldn't be needed: theoretically, any code that is # sensitive to IPv4 vs. IPv6 should be tested explicitly, regardless of diff --git a/test/integration/README.md b/test/integration/README.md new file mode 100644 index 00000000..18a80ea4 --- /dev/null +++ b/test/integration/README.md @@ -0,0 +1,66 @@ +# UDP Tunnel Integration Tests + +The integration scripts exercise a loaded Homa module in temporary network +namespaces and leave captures and logs under `artifacts/`. + +Request/response callers use `homa_test --count 1 udp`, which sends 10 +warmups and 1 measured request over the namespace-selected Homa transport. +Each shell and Python script has a top-of-file description of its purpose. +Local helper tests, including mocked client count and error checks, run without +root privileges or live traffic: + +```bash +PYTHONDONTWRITEBYTECODE=1 python3 -B -m unittest discover -s test/integration -p 'test_*.py' -v +``` + +Run as root from the module tree: + +```bash +test/integration/run_udp_tunnel.sh +``` + +The harness verifies: + +- port-collision rollback and independent per-network-namespace controls; +- IPv4 and IPv6 request/response, including messages larger than the MTU; +- UDP source/destination port, length, checksum, and inner Homa header from + an ingress packet capture; +- operation with veth TX checksum offload disabled when supported; +- concurrent socket/RPC creation during disable, followed by re-enable; +- namespace deletion during active RPCs, immediate recreation, and reuse; +- native Homa fallback regression. + +Additional scripts: + +- `run_udp_pmtu.sh`: routed IPv4/IPv6 PMTU and tiny-MTU rejection. +- `run_udp_checksum.sh`: valid, invalid, and computed-zero checksums. +- `run_udp_isolation.sh`: live native/UDP mismatch rejection both ways. +- `run_udp_retransmit.sh`: one-shot drop, retransmission, and control classes. +- `run_udp_pacing.sh`: native baseline and 100 Mbit/s UDP pacing proof. + +`run_udp_pacing.sh` reports SRPT as incomplete on veth. Veth advertises a +fixed 10 Gbit/s rate, while Homa's qdisc has a 5% minimum; a rate-configurable +device is required for a valid constrained-link SRPT test. + +Requirements are root privileges, a loaded `homa.ko`, `ip`, `tcpdump`, +`ethtool`, `tc`, `iptables`, Python 3, `timeout`, and the normal utility build +dependencies. +The harness warns when the running kernel is not Linux 6.17; results from an +older kernel are useful regression evidence but do not satisfy final V1 target +validation. The checksum validator accepts classic Ethernet pcap files and +uses only the Python standard library. + +The race stress exercises real concurrent transitions but is not a formal +LKMM proof. Run the harness on a KASAN-enabled Linux 6.17 kernel to turn RCU +lifetime defects during namespace and socket teardown into detectable errors. + +## Current Pending Evidence + +- Linux 6.17 KASAN/lockdep/RCU execution and matching 6.17 headers. +- Physical-wire IPv6 offload validation; the physical hosts have no IPv6 + address. +- SRPT on a rate-configurable device running Homa's qdisc. +- Wireshark/TShark runtime dissector validation. + +All feasible Linux 6.12 UDP scenarios are complete. TCP-related testing is not +required. \ No newline at end of file diff --git a/test/integration/artifacts/.gitignore b/test/integration/artifacts/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/test/integration/artifacts/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/test/integration/inject_udp_checksum.py b/test/integration/inject_udp_checksum.py new file mode 100644 index 00000000..72a53121 --- /dev/null +++ b/test/integration/inject_udp_checksum.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# Construct and inject Homa-over-UDP packets with selected checksum cases +# for the live UDP checksum integration test. + +import argparse +import ipaddress +import socket +import struct + + +UDP_PORT = 54321 + + +def checksum_sum(data): + if len(data) % 2: + data += b"\0" + total = sum(struct.unpack("!%dH" % (len(data) // 2), data)) + while total >> 16: + total = (total & 0xFFFF) + (total >> 16) + return total + + +def internet_checksum(data): + checksum = (~checksum_sum(data)) & 0xFFFF + return checksum or 0xFFFF + + +def mac_bytes(address): + octets = address.split(":") + if len(octets) != 6: + raise ValueError("invalid MAC address: %s" % address) + return bytes(int(octet, 16) for octet in octets) + + +def homa_payload(data_packet=False, control_type=None, source_port=4000, + destination_port=4000): + payload_length = 156 if data_packet else 28 + if control_type == "resend": + payload_length = 37 + payload = bytearray(payload_length) + struct.pack_into("!HH", payload, 0, source_port, destination_port) + if data_packet: + payload[11] = 0x10 + payload[12] = 14 << 4 + struct.pack_into("!Q", payload, 20, 10000) + struct.pack_into("!I", payload, 28, 100) + struct.pack_into("!I", payload, 52, 0) + elif control_type == "resend": + payload[11] = 0x12 + struct.pack_into("!QII", payload, 20, 0xD00DFEED, 0, 100) + elif control_type == "need-ack": + payload[11] = 0x17 + struct.pack_into("!Q", payload, 20, 0xD00DFEED) + else: + payload[11] = 0x14 # BUSY: the shortest Homa header. + return bytes(payload) + + +def udp_datagram(source, destination, family, invalid, zero_checksum, + data_packet=False, control_type=None, source_port=4000, + destination_port=4000): + payload = bytearray(homa_payload(data_packet, control_type, source_port, + destination_port)) + udp_length = 8 + len(payload) + header = struct.pack("!HHHH", UDP_PORT, UDP_PORT, udp_length, 0) + if family == 4: + pseudo_header = source + destination + struct.pack( + "!BBH", 0, socket.IPPROTO_UDP, udp_length) + else: + pseudo_header = source + destination + struct.pack( + "!I3xB", udp_length, socket.IPPROTO_UDP) + if zero_checksum: + adjustment = 0xFFFF - checksum_sum(pseudo_header + header + payload) + struct.pack_into("!H", payload, len(payload) - 2, adjustment) + checksum = internet_checksum(pseudo_header + header + payload) + if invalid: + checksum ^= 1 + if checksum == 0: + checksum = 1 + return struct.pack("!HHHH", UDP_PORT, UDP_PORT, udp_length, + checksum) + payload + + +def build_frame(family, source_ip, destination_ip, source_mac, + destination_mac, invalid=False, zero_checksum=False, + data_packet=False, control_type=None, source_port=4000, + destination_port=4000): + source = ipaddress.ip_address(source_ip) + destination = ipaddress.ip_address(destination_ip) + if source.version != family or destination.version != family: + raise ValueError("IP address family does not match --family") + + source_packed = source.packed + destination_packed = destination.packed + datagram = udp_datagram(source_packed, destination_packed, family, + invalid, zero_checksum, data_packet, control_type, + source_port, destination_port) + if family == 4: + header = struct.pack("!BBHHHBBH4s4s", 0x45, 0, 20 + len(datagram), + 0, 0x4000, 64, socket.IPPROTO_UDP, 0, + source_packed, destination_packed) + header = header[:10] + struct.pack( + "!H", internet_checksum(header)) + header[12:] + ethertype = 0x0800 + else: + header = struct.pack("!IHBB16s16s", 6 << 28, len(datagram), + socket.IPPROTO_UDP, 64, source_packed, + destination_packed) + ethertype = 0x86DD + ethernet = (mac_bytes(destination_mac) + mac_bytes(source_mac) + + struct.pack("!H", ethertype)) + return ethernet + header + datagram + + +def main(): + parser = argparse.ArgumentParser( + description="Inject a valid or corrupted Homa UDP Ethernet frame") + parser.add_argument("--interface", required=True) + parser.add_argument("--family", required=True, type=int, + choices=(4, 6)) + parser.add_argument("--source-ip", required=True) + parser.add_argument("--destination-ip", required=True) + parser.add_argument("--source-mac", required=True) + parser.add_argument("--destination-mac", required=True) + checksum_mode = parser.add_mutually_exclusive_group() + checksum_mode.add_argument("--invalid", action="store_true") + checksum_mode.add_argument("--zero-checksum", action="store_true") + packet_type = parser.add_mutually_exclusive_group() + packet_type.add_argument("--data", action="store_true", + help="inject a complete 100-byte DATA request") + packet_type.add_argument("--resend", action="store_true", + help="inject RESEND for a nonexistent RPC") + packet_type.add_argument("--need-ack", action="store_true", + help="inject NEED_ACK for a nonexistent RPC") + parser.add_argument("--homa-source-port", type=int, default=4000) + parser.add_argument("--homa-destination-port", type=int, default=4000) + args = parser.parse_args() + + control_type = "resend" if args.resend else ( + "need-ack" if args.need_ack else None) + frame = build_frame(args.family, args.source_ip, args.destination_ip, + args.source_mac, args.destination_mac, args.invalid, + args.zero_checksum, args.data, control_type, + args.homa_source_port, args.homa_destination_port) + raw_socket = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, + socket.htons(0x0003)) + try: + raw_socket.bind((args.interface, 0)) + raw_socket.send(frame) + finally: + raw_socket.close() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/test/integration/run_udp_checksum.sh b/test/integration/run_udp_checksum.sh new file mode 100644 index 00000000..39b8853d --- /dev/null +++ b/test/integration/run_udp_checksum.sh @@ -0,0 +1,217 @@ +#!/usr/bin/env bash +# Inject valid, invalid, and computed-zero UDP checksums and verify +# Homa tunnel receive behavior using temporary namespaces and packet captures. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +ARTIFACT_DIR=${ARTIFACT_DIR:-"$SCRIPT_DIR/artifacts/checksum-$(date -u +%Y%m%dT%H%M%SZ)-$$"} +NS_CLIENT="homa-csum-client-$$" +NS_SERVER="homa-csum-server-$$" +CLIENT_IF="hcc$$" +SERVER_IF="hcs$$" +TRACE=/sys/kernel/tracing +BACKGROUND_PIDS=() +TRACE_OWNED=0 +LAST_PID="" +RESULTS_FILE="$ARTIFACT_DIR/results.tsv" + +fail() +{ + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +register_pid() +{ + BACKGROUND_PIDS+=("$1") +} + +stop_pid() +{ + local pid=$1 + local signal=${2:-TERM} + + if kill -0 "$pid" 2>/dev/null; then + kill -s "$signal" "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi +} + +cleanup() +{ + local pid + + for pid in "${BACKGROUND_PIDS[@]}"; do + stop_pid "$pid" + done + if (( TRACE_OWNED )); then + printf '0\n' > "$TRACE/tracing_on" + printf 'nop\n' > "$TRACE/current_tracer" + printf '\n' > "$TRACE/set_ftrace_filter" + printf '\n' > "$TRACE/trace" + printf '1\n' > "$TRACE/tracing_on" + fi + ip netns del "$NS_CLIENT" 2>/dev/null || true + ip netns del "$NS_SERVER" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +wait_for_pattern() +{ + local file=$1 + local pattern=$2 + local deadline=$((SECONDS + 10)) + + until grep -q "$pattern" "$file" 2>/dev/null; do + (( SECONDS < deadline )) || return 1 + done +} + +start_capture() +{ + local output=$1 + local log="$output.log" + + ip netns exec "$NS_SERVER" tcpdump --immediate-mode -Q in -U -n \ + -i "$SERVER_IF" -w "$output" "udp port 54321" \ + > "$log" 2>&1 & + LAST_PID=$! + register_pid "$LAST_PID" + wait_for_pattern "$log" "listening on" || fail "tcpdump did not start" +} + +wait_for_capture() +{ + local capture=$1 + local deadline=$((SECONDS + 10)) + + until tcpdump -n -c 1 -r "$capture" >/dev/null 2>&1; do + (( SECONDS < deadline )) || return 1 + done +} + +run_case() +{ + local family=$1 + local validity=$2 + local expected_callbacks=$3 + local source_ip destination_ip family_name checksum_arg="" + local capture="$ARTIFACT_DIR/ipv${family}-${validity}.pcap" + local capture_pid callback_count expected_valid + + if [[ "$family" == 4 ]]; then + source_ip=10.205.0.1 + destination_ip=10.205.0.2 + family_name=ipv4 + else + source_ip=fd00:205::1 + destination_ip=fd00:205::2 + family_name=ipv6 + fi + if [[ "$validity" == invalid ]]; then + checksum_arg=--invalid + expected_valid=0 + elif [[ "$validity" == zero ]]; then + checksum_arg=--zero-checksum + expected_valid=1 + else + expected_valid=1 + fi + + start_capture "$capture" + capture_pid=$LAST_PID + printf '0\n' > "$TRACE/tracing_on" + printf '\n' > "$TRACE/trace" + printf '1\n' > "$TRACE/tracing_on" + ip netns exec "$NS_CLIENT" python3 "$SCRIPT_DIR/inject_udp_checksum.py" \ + --interface "$CLIENT_IF" --family "$family" \ + --source-ip "$source_ip" --destination-ip "$destination_ip" \ + --source-mac "$CLIENT_MAC" --destination-mac "$SERVER_MAC" \ + $checksum_arg + wait_for_capture "$capture" || fail "$family_name $validity frame not captured" + printf '0\n' > "$TRACE/tracing_on" + stop_pid "$capture_pid" INT + callback_count=$(grep -c 'homa_hijack_udp_encap_rcv' "$TRACE/trace" || true) + [[ "$callback_count" == "$expected_callbacks" ]] || + fail "$family_name $validity frame invoked Homa $callback_count times; expected $expected_callbacks" + python3 - "$SCRIPT_DIR" "$capture" "$family_name" "$expected_valid" \ + "$validity" <<'PY' +import sys + +sys.path.insert(0, sys.argv[1]) +import verify_udp_pcap + +packets = [] +for _, frame in verify_udp_pcap.read_pcap(sys.argv[2]): + parsed = verify_udp_pcap.parse_udp(frame) + if parsed is not None: + packets.append(parsed) +if len(packets) != 1: + raise SystemExit("expected one captured UDP packet, found %d" % len(packets)) +packet = packets[0] +if packet["family"] != sys.argv[3]: + raise SystemExit("captured the wrong address family") +expected_valid = bool(int(sys.argv[4])) +if packet["checksum_valid"] != expected_valid: + raise SystemExit("captured checksum validity did not match the test case") +if packet["checksum"] == 0: + raise SystemExit("test packet used a zero UDP checksum") +if sys.argv[5] == "zero" and packet["checksum"] != 0xFFFF: + raise SystemExit("computed-zero checksum was not transmitted as 0xffff") +PY + printf '%s\tpass\tcaptured once; Homa callbacks=%s\n' \ + "$family_name-$validity" "$callback_count" >> "$RESULTS_FILE" +} + +[[ $EUID -eq 0 ]] || fail "run as root" +for command in ip sysctl tcpdump python3; do + command -v "$command" >/dev/null 2>&1 || + fail "required command not found: $command" +done +[[ -d /proc/sys/net/homa ]] || fail "the Homa module is not loaded" +[[ -w "$TRACE/current_tracer" ]] || fail "ftrace is unavailable" +[[ $(cat "$TRACE/current_tracer") == nop ]] || fail "ftrace is already in use" +[[ $(cat "$TRACE/function_profile_enabled") == 0 ]] || + fail "ftrace function profiling is already in use" +grep -q '^homa_hijack_udp_encap_rcv' "$TRACE/available_filter_functions" || + fail "homa_hijack_udp_encap_rcv is unavailable to ftrace" + +mkdir -p "$ARTIFACT_DIR" +: > "$RESULTS_FILE" +ip netns add "$NS_CLIENT" +ip netns add "$NS_SERVER" +ip link add "$CLIENT_IF" type veth peer name "$SERVER_IF" +ip link set "$CLIENT_IF" netns "$NS_CLIENT" +ip link set "$SERVER_IF" netns "$NS_SERVER" +ip -n "$NS_CLIENT" link set lo up +ip -n "$NS_SERVER" link set lo up +ip -n "$NS_CLIENT" addr add 10.205.0.1/24 dev "$CLIENT_IF" +ip -n "$NS_SERVER" addr add 10.205.0.2/24 dev "$SERVER_IF" +ip -n "$NS_CLIENT" addr add fd00:205::1/64 dev "$CLIENT_IF" nodad +ip -n "$NS_SERVER" addr add fd00:205::2/64 dev "$SERVER_IF" nodad +ip netns exec "$NS_CLIENT" sysctl -q -w \ + "net.ipv6.conf.$CLIENT_IF.accept_dad=0" +ip netns exec "$NS_SERVER" sysctl -q -w \ + "net.ipv6.conf.$SERVER_IF.accept_dad=0" +ip -n "$NS_CLIENT" link set "$CLIENT_IF" up +ip -n "$NS_SERVER" link set "$SERVER_IF" up +ip netns exec "$NS_SERVER" sysctl -q -w net.homa.hijack_udp=1 +CLIENT_MAC=$(ip netns exec "$NS_CLIENT" cat "/sys/class/net/$CLIENT_IF/address") +SERVER_MAC=$(ip netns exec "$NS_SERVER" cat "/sys/class/net/$SERVER_IF/address") + +printf '0\n' > "$TRACE/tracing_on" +printf '\n' > "$TRACE/trace" +printf 'homa_hijack_udp_encap_rcv\n' > "$TRACE/set_ftrace_filter" +printf 'function\n' > "$TRACE/current_tracer" +TRACE_OWNED=1 + +run_case 4 valid 1 +run_case 4 invalid 0 +run_case 4 zero 1 +run_case 6 valid 1 +run_case 6 invalid 0 +run_case 6 zero 1 + +printf 'PASS: invalid IPv4/IPv6 UDP checksums were dropped before Homa\n' +printf 'Artifacts: %s\n' "$ARTIFACT_DIR" \ No newline at end of file diff --git a/test/integration/run_udp_isolation.sh b/test/integration/run_udp_isolation.sh new file mode 100644 index 00000000..20a6ed2e --- /dev/null +++ b/test/integration/run_udp_isolation.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Verify native/UDP Homa transport isolation in both directions, +# including mismatch rejection and matching-transport baseline RPCs. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +ROOT_DIR=$(cd "$SCRIPT_DIR/../.." && pwd) +UTIL_DIR="$ROOT_DIR/util" +ARTIFACT_DIR=${ARTIFACT_DIR:-"$SCRIPT_DIR/artifacts/isolation-$(date -u +%Y%m%dT%H%M%SZ)-$$"} +NS_CLIENT="homa-iso-client-$$" +NS_SERVER="homa-iso-server-$$" +CLIENT_IF="hic$$" +SERVER_IF="his$$" +BACKGROUND_PIDS=() +LAST_PID="" + +fail() +{ + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +register_pid() +{ + BACKGROUND_PIDS+=("$1") +} + +stop_pid() +{ + local pid=$1 + local signal=${2:-TERM} + + if kill -0 "$pid" 2>/dev/null; then + kill -s "$signal" "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi +} + +cleanup() +{ + local pid + + for pid in "${BACKGROUND_PIDS[@]}"; do + stop_pid "$pid" + done + ip netns del "$NS_CLIENT" 2>/dev/null || true + ip netns del "$NS_SERVER" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +metric_value() +{ + awk '$1 == "unknown_packet_types" {sum += $2} END {print sum + 0}' \ + /proc/net/homa_metrics +} + +set_udp() +{ + ip netns exec "$1" sysctl -q -w "net.homa.hijack_udp=$2" +} + +start_server() +{ + local output=$1 + + ip netns exec "$NS_SERVER" "$UTIL_DIR/server" --port 4000 --verbose \ + > "$output" 2>&1 & + LAST_PID=$! + register_pid "$LAST_PID" +} + +invoke() +{ + local output=$1 + + timeout 20 ip netns exec "$NS_CLIENT" "$UTIL_DIR/homa_test" \ + 10.206.0.2:4000 --count 1 --length 100 udp > "$output" 2>&1 && + grep -q "Bandwidth at median" "$output" +} + +wait_one_second() +{ + local deadline=$((SECONDS + 1)) + + while (( SECONDS < deadline )); do + : + done +} + +[[ $EUID -eq 0 ]] || fail "run as root" +for command in ip sysctl python3 timeout make awk; do + command -v "$command" >/dev/null 2>&1 || + fail "required command not found: $command" +done +[[ -d /proc/sys/net/homa ]] || fail "the Homa module is not loaded" + +mkdir -p "$ARTIFACT_DIR" +make -C "$UTIL_DIR" homa_test server +ip netns add "$NS_CLIENT" +ip netns add "$NS_SERVER" +ip link add "$CLIENT_IF" type veth peer name "$SERVER_IF" +ip link set "$CLIENT_IF" netns "$NS_CLIENT" +ip link set "$SERVER_IF" netns "$NS_SERVER" +ip -n "$NS_CLIENT" link set lo up +ip -n "$NS_SERVER" link set lo up +ip -n "$NS_CLIENT" addr add 10.206.0.1/24 dev "$CLIENT_IF" +ip -n "$NS_SERVER" addr add 10.206.0.2/24 dev "$SERVER_IF" +ip -n "$NS_CLIENT" link set "$CLIENT_IF" up +ip -n "$NS_SERVER" link set "$SERVER_IF" up +set_udp "$NS_CLIENT" 0 +set_udp "$NS_SERVER" 0 + +# A socket selected while UDP is disabled must reject a valid packet that +# arrives later through the UDP tunnel. +start_server "$ARTIFACT_DIR/native-server.log" +native_server=$LAST_PID +invoke "$ARTIFACT_DIR/native-baseline.log" || + fail "native-selected server did not pass its baseline RPC" +set_udp "$NS_SERVER" 1 +before=$(metric_value) +server_lines=$(wc -l < "$ARTIFACT_DIR/native-server.log") +client_mac=$(ip netns exec "$NS_CLIENT" cat "/sys/class/net/$CLIENT_IF/address") +server_mac=$(ip netns exec "$NS_SERVER" cat "/sys/class/net/$SERVER_IF/address") +ip netns exec "$NS_CLIENT" python3 "$SCRIPT_DIR/inject_udp_checksum.py" \ + --interface "$CLIENT_IF" --family 4 \ + --source-ip 10.206.0.1 --destination-ip 10.206.0.2 \ + --source-mac "$client_mac" --destination-mac "$server_mac" --data +wait_one_second +after=$(metric_value) +[[ "$after" == $((before + 1)) ]] || + fail "UDP-to-native mismatch changed metric from $before to $after" +[[ $(wc -l < "$ARTIFACT_DIR/native-server.log") == "$server_lines" ]] || + fail "UDP-to-native mismatch reached the native application" +stop_pid "$native_server" + +# A socket selected while UDP is enabled must reject native Homa traffic. +set_udp "$NS_CLIENT" 1 +start_server "$ARTIFACT_DIR/udp-server.log" +udp_server=$LAST_PID +invoke "$ARTIFACT_DIR/udp-baseline.log" || + fail "UDP-selected server did not pass its baseline RPC" +set_udp "$NS_CLIENT" 0 +before=$(metric_value) +server_lines=$(wc -l < "$ARTIFACT_DIR/udp-server.log") +timeout 3 ip netns exec "$NS_CLIENT" "$UTIL_DIR/homa_test" \ + 10.206.0.2:4000 --count 1 --length 100 udp \ + > "$ARTIFACT_DIR/native-mismatch.log" 2>&1 || true +wait_one_second +after=$(metric_value) +(( after > before )) || + fail "native-to-UDP mismatch did not increment unknown_packet_types" +[[ $(wc -l < "$ARTIFACT_DIR/udp-server.log") == "$server_lines" ]] || + fail "native-to-UDP mismatch reached the UDP application" +if grep -q "Bandwidth at median" "$ARTIFACT_DIR/native-mismatch.log"; then + fail "native-to-UDP mismatch received a response" +fi +stop_pid "$udp_server" + +printf 'PASS: live native/UDP transport isolation in both directions\n' +printf 'Artifacts: %s\n' "$ARTIFACT_DIR" diff --git a/test/integration/run_udp_pacing.sh b/test/integration/run_udp_pacing.sh new file mode 100755 index 00000000..aa359c95 --- /dev/null +++ b/test/integration/run_udp_pacing.sh @@ -0,0 +1,161 @@ +#!/usr/bin/env bash +# Compare native Homa throughput with UDP tunnel traffic and verify +# pacing on a 100 Mbit/s link using packet captures and qdisc statistics. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +ROOT_DIR=$(cd "$SCRIPT_DIR/../.." && pwd) +UTIL_DIR="$ROOT_DIR/util" +ARTIFACT_DIR=${ARTIFACT_DIR:-"$SCRIPT_DIR/artifacts/pacing-$(date -u +%Y%m%dT%H%M%SZ)-$$"} +NS_CLIENT="homa-pace-client-$$" +NS_SERVER="homa-pace-server-$$" +CLIENT_IF="hpc$$" +SERVER_IF="hps$$" +BACKGROUND_PIDS=() +LAST_PID="" + +fail() +{ + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +register_pid() +{ + BACKGROUND_PIDS+=("$1") +} + +stop_pid() +{ + local pid=$1 + local signal=${2:-TERM} + + if kill -0 "$pid" 2>/dev/null; then + kill -s "$signal" "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi +} + +cleanup() +{ + local pid + + for pid in "${BACKGROUND_PIDS[@]}"; do + stop_pid "$pid" + done + ip netns del "$NS_CLIENT" 2>/dev/null || true + ip netns del "$NS_SERVER" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +wait_for_pattern() +{ + local file=$1 + local pattern=$2 + local deadline=$((SECONDS + 10)) + + until grep -q "$pattern" "$file" 2>/dev/null; do + (( SECONDS < deadline )) || return 1 + done +} + +start_server() +{ + local port=$1 + local log=$2 + + ip netns exec "$NS_SERVER" "$UTIL_DIR/server" --port "$port" \ + --validate --verbose > "$ARTIFACT_DIR/$log" 2>&1 & + LAST_PID=$! + register_pid "$LAST_PID" +} + +start_capture() +{ + local pcap=$1 + local filter=$2 + local log="${pcap%.pcap}.capture.log" + + ip netns exec "$NS_SERVER" tcpdump --immediate-mode -Q in -U -n \ + -i "$SERVER_IF" -w "$pcap" "$filter" > "$log" 2>&1 & + LAST_PID=$! + register_pid "$LAST_PID" + wait_for_pattern "$log" "listening on" || fail "tcpdump did not become ready" +} + +run_stream() +{ + local port=$1 + local log=$2 + + timeout 15 ip netns exec "$NS_CLIENT" "$UTIL_DIR/homa_test" \ + "10.208.0.2:$port" --count 1 --length 1000000 stream \ + > "$ARTIFACT_DIR/$log" 2>&1 || fail "stream workload failed" + grep -q "Homa throughput" "$ARTIFACT_DIR/$log" || + fail "stream workload did not report throughput" +} + +[[ $EUID -eq 0 ]] || fail "run as root" +for command in ip sysctl tcpdump ethtool tc python3 timeout make date; do + command -v "$command" >/dev/null 2>&1 || + fail "required command not found: $command" +done +[[ -d /proc/sys/net/homa ]] || fail "the Homa module is not loaded" + +mkdir -p "$ARTIFACT_DIR" +make -C "$UTIL_DIR" homa_test server +ip netns add "$NS_CLIENT" +ip netns add "$NS_SERVER" +ip link add "$CLIENT_IF" type veth peer name "$SERVER_IF" +ip link set "$CLIENT_IF" netns "$NS_CLIENT" +ip link set "$SERVER_IF" netns "$NS_SERVER" +ip -n "$NS_CLIENT" link set lo up +ip -n "$NS_SERVER" link set lo up +ip -n "$NS_CLIENT" addr add 10.208.0.1/24 dev "$CLIENT_IF" +ip -n "$NS_SERVER" addr add 10.208.0.2/24 dev "$SERVER_IF" +ip -n "$NS_CLIENT" link set "$CLIENT_IF" up +ip -n "$NS_SERVER" link set "$SERVER_IF" up +ip netns exec "$NS_CLIENT" ethtool -K "$CLIENT_IF" tx off \ + > "$ARTIFACT_DIR/client-offload.log" 2>&1 +ip netns exec "$NS_SERVER" ethtool -K "$SERVER_IF" tx off \ + > "$ARTIFACT_DIR/server-offload.log" 2>&1 +for endpoint in "$NS_CLIENT:$CLIENT_IF" "$NS_SERVER:$SERVER_IF"; do + namespace=${endpoint%%:*} + interface=${endpoint#*:} + ip netns exec "$namespace" tc qdisc add dev "$interface" root \ + netem limit 20000 rate 100mbit + ip netns exec "$namespace" tc -s qdisc show dev "$interface" +done > "$ARTIFACT_DIR/qdisc.txt" + +ip netns exec "$NS_CLIENT" sysctl -q -w net.homa.hijack_udp=0 +ip netns exec "$NS_SERVER" sysctl -q -w net.homa.hijack_udp=0 +start_server 4300 server-native.log +native_server=$LAST_PID +start_capture "$ARTIFACT_DIR/native-baseline.pcap" "ip proto 146" +native_capture=$LAST_PID +run_stream 4300 native-stream.log +stop_pid "$native_capture" INT +stop_pid "$native_server" +native_gbps=$(awk '/Homa throughput/ {print $(NF-1)}' \ + "$ARTIFACT_DIR/native-stream.log") +python3 -c 'import sys; sys.exit(0 if float(sys.argv[1]) >= 0.00875 else 1)' \ + "$native_gbps" || fail "native baseline was below 70 Mbit/s" + +ip netns exec "$NS_CLIENT" sysctl -q -w net.homa.hijack_udp=1 +ip netns exec "$NS_SERVER" sysctl -q -w net.homa.hijack_udp=1 +start_server 4301 server-udp-stream.log +udp_stream_server=$LAST_PID +start_capture "$ARTIFACT_DIR/udp-stream.pcap" "udp port 54321" +udp_capture=$LAST_PID +run_stream 4301 udp-stream.log +stop_pid "$udp_capture" INT +stop_pid "$udp_stream_server" +python3 "$SCRIPT_DIR/verify_udp_pacing.py" \ + "$ARTIFACT_DIR/udp-stream.pcap" | tee "$ARTIFACT_DIR/pacing.json" +cat > "$ARTIFACT_DIR/srpt.json" <<'EOF' +{"reason":"veth reports fixed 10 Gbit/s speed; Homa qdisc minimum is 5%, and its bulk RPC made no progress at that setting","status":"incomplete"} +EOF +printf 'PASS: UDP pacing checks completed\n' +printf 'INCOMPLETE: SRPT requires a rate-configurable Homa-qdisc device\n' +printf 'Artifacts: %s\n' "$ARTIFACT_DIR" diff --git a/test/integration/run_udp_pmtu.sh b/test/integration/run_udp_pmtu.sh new file mode 100644 index 00000000..ea5a9ddd --- /dev/null +++ b/test/integration/run_udp_pmtu.sh @@ -0,0 +1,260 @@ +#!/usr/bin/env bash +# Exercise routed IPv4/IPv6 path-MTU discovery and tiny-MTU rejection +# for Homa-over-UDP RPCs in temporary network namespaces. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +ROOT_DIR=$(cd "$SCRIPT_DIR/../.." && pwd) +UTIL_DIR="$ROOT_DIR/util" +ARTIFACT_DIR=${ARTIFACT_DIR:-"$SCRIPT_DIR/artifacts/pmtu-$(date -u +%Y%m%dT%H%M%SZ)-$$"} +NS_CLIENT="homa-pmtu-client-$$" +NS_ROUTER="homa-pmtu-router-$$" +NS_SERVER="homa-pmtu-server-$$" +CLIENT_IF="pmc$$" +ROUTER_CLIENT_IF="pmrc$$" +ROUTER_SERVER_IF="pmrs$$" +SERVER_IF="pms$$" +BACKGROUND_PIDS=() +LAST_PID="" + +log() +{ + printf '%s\n' "$*" +} + +fail() +{ + log "FAIL: $*" + exit 1 +} + +register_pid() +{ + BACKGROUND_PIDS+=("$1") +} + +stop_pid() +{ + local pid=$1 + local signal=${2:-TERM} + + if kill -0 "$pid" 2>/dev/null; then + kill -s "$signal" "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi +} + +cleanup() +{ + local pid + + for pid in "${BACKGROUND_PIDS[@]}"; do + stop_pid "$pid" + done + ip netns del "$NS_CLIENT" 2>/dev/null || true + ip netns del "$NS_ROUTER" 2>/dev/null || true + ip netns del "$NS_SERVER" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +wait_for_pattern() +{ + local file=$1 + local pattern=$2 + local deadline=$((SECONDS + 10)) + + until grep -q "$pattern" "$file" 2>/dev/null; do + (( SECONDS < deadline )) || return 1 + done +} + +start_capture() +{ + local namespace=$1 + local interface=$2 + local output=$3 + local filter=$4 + local capture_log="$output.log" + + ip netns exec "$namespace" tcpdump --immediate-mode -U -n -i "$interface" \ + -w "$output" "$filter" > "$capture_log" 2>&1 & + LAST_PID=$! + register_pid "$LAST_PID" + wait_for_pattern "$capture_log" "listening on" || + fail "tcpdump did not become ready on $namespace/$interface" +} + +run_followup() +{ + local family=$1 + local target=$2 + local port=$3 + local output=$4 + local family_args=() + local attempt + + if [[ "$family" == ipv6 ]]; then + family_args+=(--ipv6) + fi + for attempt in 1 2 3 4 5; do + if timeout 20 ip netns exec "$NS_CLIENT" "$UTIL_DIR/homa_test" \ + "$target:$port" "${family_args[@]}" --count 1 --length 8192 udp \ + > "$output" 2>&1 && grep -q "Bandwidth at median" "$output"; then + return 0 + fi + done + return 1 +} + +verify_maximum_length() +{ + local capture=$1 + local summary=$2 + + python3 "$SCRIPT_DIR/verify_udp_pcap.py" --json "$capture" > "$summary" + python3 - "$summary" <<'PY' +import json +import sys + +with open(sys.argv[1], encoding="utf-8") as input_file: + summary = json.load(input_file) +if summary["max_outer_length"] > 1280: + raise SystemExit("post-PMTU packet exceeded 1280 bytes: %d" % + summary["max_outer_length"]) +PY +} + +run_family() +{ + local family=$1 + local target=$2 + local port=$3 + local icmp_filter=$4 + local icmp_pattern=$5 + local family_args=() + local pre_capture="$ARTIFACT_DIR/$family-before-pmtu.pcap" + local post_capture="$ARTIFACT_DIR/$family-after-pmtu.pcap" + local first_log="$ARTIFACT_DIR/$family-first-rpc.log" + local followup_log="$ARTIFACT_DIR/$family-followup-rpc.log" + local summary="$ARTIFACT_DIR/$family-after-pmtu.json" + local pre_capture_pid + local post_capture_pid + + if [[ "$family" == ipv6 ]]; then + family_args+=(--ipv6) + fi + start_capture "$NS_CLIENT" "$CLIENT_IF" "$pre_capture" \ + "udp port 54321 or $icmp_filter" + pre_capture_pid=$LAST_PID + timeout 20 ip netns exec "$NS_CLIENT" "$UTIL_DIR/homa_test" \ + "$target:$port" "${family_args[@]}" --count 1 --length 8192 udp \ + > "$first_log" 2>&1 || true + stop_pid "$pre_capture_pid" INT + grep -qi "Message too long" "$first_log" || + fail "$family current RPC did not report EMSGSIZE" + tcpdump -nn -r "$pre_capture" "$icmp_filter" 2>/dev/null | + grep -qi "$icmp_pattern" || + fail "$family PMTU ICMP error was not captured" + + start_capture "$NS_SERVER" "$SERVER_IF" "$post_capture" \ + "udp port 54321" + post_capture_pid=$LAST_PID + run_followup "$family" "$target" "$port" "$followup_log" || + fail "$family follow-up RPC did not succeed after PMTU update" + stop_pid "$post_capture_pid" INT + verify_maximum_length "$post_capture" "$summary" + log "PASS: $family PMTU error, RPC abort, and reduced geometry" +} + +run_tiny_mtu() +{ + local capture="$ARTIFACT_DIR/ipv4-tiny-mtu.pcap" + local client_log="$ARTIFACT_DIR/ipv4-tiny-mtu.log" + local capture_pid + local packet_count + + ip -n "$NS_CLIENT" route add 10.204.2.3/32 via 10.204.1.1 mtu lock 80 + start_capture "$NS_SERVER" "$SERVER_IF" "$capture" "udp port 54321" + capture_pid=$LAST_PID + timeout 20 ip netns exec "$NS_CLIENT" "$UTIL_DIR/homa_test" \ + 10.204.2.3:4100 --count 1 --length 100 udp > "$client_log" 2>&1 || true + stop_pid "$capture_pid" INT + grep -qi "Message too long" "$client_log" || + fail "tiny-MTU IPv4 RPC did not report EMSGSIZE" + packet_count=$(tcpdump -nn -r "$capture" "udp port 54321" \ + 2>/dev/null | wc -l) + [[ "$packet_count" == 0 ]] || + fail "tiny-MTU IPv4 RPC emitted $packet_count UDP packets" + log "PASS: tiny IPv4 route MTU returned EMSGSIZE without transmission" +} + +[[ $EUID -eq 0 ]] || fail "run as root" +for command in ip sysctl tcpdump ethtool python3 timeout make; do + command -v "$command" >/dev/null 2>&1 || + fail "required command not found: $command" +done +[[ -d /proc/sys/net/homa ]] || fail "the Homa module is not loaded" +mkdir -p "$ARTIFACT_DIR" +make -C "$UTIL_DIR" homa_test server + +ip netns add "$NS_CLIENT" +ip netns add "$NS_ROUTER" +ip netns add "$NS_SERVER" +ip link add "$CLIENT_IF" type veth peer name "$ROUTER_CLIENT_IF" +ip link add "$ROUTER_SERVER_IF" type veth peer name "$SERVER_IF" +ip link set "$CLIENT_IF" netns "$NS_CLIENT" +ip link set "$ROUTER_CLIENT_IF" netns "$NS_ROUTER" +ip link set "$ROUTER_SERVER_IF" netns "$NS_ROUTER" +ip link set "$SERVER_IF" netns "$NS_SERVER" + +for namespace in "$NS_CLIENT" "$NS_ROUTER" "$NS_SERVER"; do + ip -n "$namespace" link set lo up +done +ip -n "$NS_CLIENT" addr add 10.204.1.2/24 dev "$CLIENT_IF" +ip -n "$NS_ROUTER" addr add 10.204.1.1/24 dev "$ROUTER_CLIENT_IF" +ip -n "$NS_ROUTER" addr add 10.204.2.1/24 dev "$ROUTER_SERVER_IF" +ip -n "$NS_SERVER" addr add 10.204.2.2/24 dev "$SERVER_IF" +ip -n "$NS_SERVER" addr add 10.204.2.3/24 dev "$SERVER_IF" +ip -n "$NS_CLIENT" addr add fd00:204:1::2/64 dev "$CLIENT_IF" nodad +ip -n "$NS_ROUTER" addr add fd00:204:1::1/64 dev "$ROUTER_CLIENT_IF" nodad +ip -n "$NS_ROUTER" addr add fd00:204:2::1/64 dev "$ROUTER_SERVER_IF" nodad +ip -n "$NS_SERVER" addr add fd00:204:2::2/64 dev "$SERVER_IF" nodad +ip netns exec "$NS_CLIENT" sysctl -q -w \ + "net.ipv6.conf.$CLIENT_IF.accept_dad=0" +ip netns exec "$NS_ROUTER" sysctl -q -w \ + "net.ipv6.conf.$ROUTER_CLIENT_IF.accept_dad=0" +ip netns exec "$NS_ROUTER" sysctl -q -w \ + "net.ipv6.conf.$ROUTER_SERVER_IF.accept_dad=0" +ip netns exec "$NS_SERVER" sysctl -q -w \ + "net.ipv6.conf.$SERVER_IF.accept_dad=0" +ip -n "$NS_CLIENT" link set "$CLIENT_IF" up +ip -n "$NS_ROUTER" link set "$ROUTER_CLIENT_IF" up +ip -n "$NS_ROUTER" link set "$ROUTER_SERVER_IF" mtu 1280 up +ip -n "$NS_SERVER" link set "$SERVER_IF" mtu 1280 up +ip netns exec "$NS_CLIENT" ethtool -K "$CLIENT_IF" tx off \ + > "$ARTIFACT_DIR/offload-disable-client.log" 2>&1 +ip netns exec "$NS_SERVER" ethtool -K "$SERVER_IF" tx off \ + > "$ARTIFACT_DIR/offload-disable-server.log" 2>&1 +ip -n "$NS_CLIENT" route add 10.204.2.0/24 via 10.204.1.1 +ip -n "$NS_SERVER" route add 10.204.1.0/24 via 10.204.2.1 +ip -n "$NS_CLIENT" -6 route add fd00:204:2::/64 via fd00:204:1::1 +ip -n "$NS_SERVER" -6 route add fd00:204:1::/64 via fd00:204:2::1 +ip netns exec "$NS_ROUTER" sysctl -q -w net.ipv4.ip_forward=1 +ip netns exec "$NS_ROUTER" sysctl -q -w net.ipv6.conf.all.forwarding=1 +ip netns exec "$NS_CLIENT" sysctl -q -w net.homa.hijack_udp=1 +ip netns exec "$NS_SERVER" sysctl -q -w net.homa.hijack_udp=1 + +ip netns exec "$NS_SERVER" "$UTIL_DIR/server" --port 4100 \ + --validate --verbose > "$ARTIFACT_DIR/server-v4.log" 2>&1 & +register_pid "$!" +ip netns exec "$NS_SERVER" "$UTIL_DIR/server" --ipv6 --port 4101 \ + --validate --verbose > "$ARTIFACT_DIR/server-v6.log" 2>&1 & +register_pid "$!" + +run_family ipv4 10.204.2.2 4100 icmp "need to frag" +run_family ipv6 "[fd00:204:2::2]" 4101 icmp6 "packet too big" +run_tiny_mtu + +log "PASS: UDP tunnel PMTU integration checks completed" +log "Artifacts: $ARTIFACT_DIR" diff --git a/test/integration/run_udp_retransmit.sh b/test/integration/run_udp_retransmit.sh new file mode 100755 index 00000000..08186020 --- /dev/null +++ b/test/integration/run_udp_retransmit.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# Drop a UDP tunnel packet once and verify RPC recovery, retransmitted +# DATA, and Homa control packet classes from captured traffic. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +ROOT_DIR=$(cd "$SCRIPT_DIR/../.." && pwd) +UTIL_DIR="$ROOT_DIR/util" +ARTIFACT_DIR=${ARTIFACT_DIR:-"$SCRIPT_DIR/artifacts/retransmit-$(date -u +%Y%m%dT%H%M%SZ)-$$"} +NS_CLIENT="homa-retx-client-$$" +NS_SERVER="homa-retx-server-$$" +CLIENT_IF="hrc$$" +SERVER_IF="hrs$$" +BACKGROUND_PIDS=() +LAST_PID="" +RULE_ACTIVE=0 + +fail() +{ + printf 'FAIL: %s\n' "$*" >&2 + exit 1 +} + +register_pid() +{ + BACKGROUND_PIDS+=("$1") +} + +stop_pid() +{ + local pid=$1 + local signal=${2:-TERM} + + if kill -0 "$pid" 2>/dev/null; then + kill -s "$signal" "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi +} + +remove_drop_rule() +{ + if (( RULE_ACTIVE )); then + ip netns exec "$NS_CLIENT" iptables -D OUTPUT -p udp \ + --dport 54321 -m statistic --mode nth --every 100000 \ + --packet 2 -j DROP 2>/dev/null || true + RULE_ACTIVE=0 + fi +} + +cleanup() +{ + local pid + + remove_drop_rule + for pid in "${BACKGROUND_PIDS[@]}"; do + stop_pid "$pid" + done + ip netns del "$NS_CLIENT" 2>/dev/null || true + ip netns del "$NS_SERVER" 2>/dev/null || true +} +trap cleanup EXIT INT TERM + +wait_for_pattern() +{ + local file=$1 + local pattern=$2 + local deadline=$((SECONDS + 10)) + + until grep -q "$pattern" "$file" 2>/dev/null; do + (( SECONDS < deadline )) || return 1 + done +} + +[[ $EUID -eq 0 ]] || fail "run as root" +for command in ip sysctl tcpdump ethtool iptables python3 timeout make; do + command -v "$command" >/dev/null 2>&1 || + fail "required command not found: $command" +done +[[ -d /proc/sys/net/homa ]] || fail "the Homa module is not loaded" + +mkdir -p "$ARTIFACT_DIR" +make -C "$UTIL_DIR" homa_test server +ip netns add "$NS_CLIENT" +ip netns add "$NS_SERVER" +ip link add "$CLIENT_IF" type veth peer name "$SERVER_IF" +ip link set "$CLIENT_IF" netns "$NS_CLIENT" +ip link set "$SERVER_IF" netns "$NS_SERVER" +ip -n "$NS_CLIENT" link set lo up +ip -n "$NS_SERVER" link set lo up +ip -n "$NS_CLIENT" addr add 10.207.0.1/24 dev "$CLIENT_IF" +ip -n "$NS_SERVER" addr add 10.207.0.2/24 dev "$SERVER_IF" +ip -n "$NS_CLIENT" link set "$CLIENT_IF" up +ip -n "$NS_SERVER" link set "$SERVER_IF" up +ip netns exec "$NS_CLIENT" ethtool -K "$CLIENT_IF" tx off \ + > "$ARTIFACT_DIR/client-offload.log" 2>&1 +ip netns exec "$NS_SERVER" ethtool -K "$SERVER_IF" tx off \ + > "$ARTIFACT_DIR/server-offload.log" 2>&1 +ip netns exec "$NS_CLIENT" sysctl -q -w net.homa.hijack_udp=1 +ip netns exec "$NS_SERVER" sysctl -q -w net.homa.hijack_udp=1 + +ip netns exec "$NS_SERVER" "$UTIL_DIR/server" --port 4200 \ + --validate --verbose > "$ARTIFACT_DIR/server.log" 2>&1 & +server_pid=$! +register_pid "$server_pid" + +ip netns exec "$NS_CLIENT" tcpdump --immediate-mode -U -n -i "$CLIENT_IF" -w \ + "$ARTIFACT_DIR/retransmit.pcap" "udp port 54321" \ + > "$ARTIFACT_DIR/tcpdump.log" 2>&1 & +capture_pid=$! +register_pid "$capture_pid" +wait_for_pattern "$ARTIFACT_DIR/tcpdump.log" "listening on" || + fail "tcpdump did not become ready" + +ip netns exec "$NS_CLIENT" iptables -I OUTPUT 1 -p udp --dport 54321 \ + -m statistic --mode nth --every 100000 --packet 2 -j DROP +RULE_ACTIVE=1 +timeout 30 ip netns exec "$NS_CLIENT" "$UTIL_DIR/homa_test" \ + 10.207.0.2:4200 --count 1 --length 65536 udp \ + > "$ARTIFACT_DIR/client.log" 2>&1 || fail "retransmission RPC failed" +grep -q "Bandwidth at median" "$ARTIFACT_DIR/client.log" || + fail "retransmission RPC did not complete" +ip netns exec "$NS_CLIENT" iptables -L OUTPUT 1 -v -n -x \ + > "$ARTIFACT_DIR/drop-rule.log" +dropped=$(awk 'NR == 1 {print $1}' "$ARTIFACT_DIR/drop-rule.log") +[[ "$dropped" == 1 ]] || fail "drop rule matched $dropped packets instead of 1" +remove_drop_rule + +client_mac=$(ip netns exec "$NS_CLIENT" \ + cat "/sys/class/net/$CLIENT_IF/address") +server_mac=$(ip netns exec "$NS_SERVER" \ + cat "/sys/class/net/$SERVER_IF/address") +timeout 10 ip netns exec "$NS_CLIENT" tcpdump --immediate-mode -U -n \ + -i "$CLIENT_IF" -c 2 \ + 'udp dst port 54321 and (udp[19] = 0x13 or udp[19] = 0x18)' \ + > "$ARTIFACT_DIR/control-responses.log" 2>&1 & +control_capture_pid=$! +register_pid "$control_capture_pid" +wait_for_pattern "$ARTIFACT_DIR/control-responses.log" "listening on" || + fail "control-response capture did not become ready" +for control in resend need-ack; do + ip netns exec "$NS_CLIENT" python3 "$SCRIPT_DIR/inject_udp_checksum.py" \ + --interface "$CLIENT_IF" --family 4 \ + --source-ip 10.207.0.1 --destination-ip 10.207.0.2 \ + --source-mac "$client_mac" --destination-mac "$server_mac" \ + --homa-source-port 4100 --homa-destination-port 4200 \ + "--$control" +done +wait "$control_capture_pid" || + fail "did not observe RPC_UNKNOWN and ACK control responses" + +stop_pid "$capture_pid" INT +stop_pid "$server_pid" + +python3 "$SCRIPT_DIR/verify_udp_retransmit.py" \ + "$ARTIFACT_DIR/retransmit.pcap" | tee "$ARTIFACT_DIR/retransmit.json" +printf 'PASS: one dropped DATA datagram was retransmitted successfully\n' +printf 'Artifacts: %s\n' "$ARTIFACT_DIR" diff --git a/test/integration/run_udp_tunnel.sh b/test/integration/run_udp_tunnel.sh new file mode 100644 index 00000000..12dc5d25 --- /dev/null +++ b/test/integration/run_udp_tunnel.sh @@ -0,0 +1,566 @@ +#!/usr/bin/env bash +# Exercise UDP tunnel setup, IPv4/IPv6 RPCs, checksum offload, lifecycle +# stress, namespace teardown, and native Homa fallback in temporary namespaces. + +set -Eeuo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +ROOT_DIR=$(cd "$SCRIPT_DIR/../.." && pwd) +UTIL_DIR="$ROOT_DIR/util" +ARTIFACT_BASE=${ARTIFACT_DIR:-"$SCRIPT_DIR/artifacts"} +RUN_ID=$(date -u +%Y%m%dT%H%M%SZ)-$$ +ARTIFACT_DIR="$ARTIFACT_BASE/$RUN_ID" +NS_CLIENT="homa-udp-client-$$" +NS_SERVER="homa-udp-server-$$" +VETH_CLIENT="huc$$" +VETH_SERVER="hus$$" +SERVER_BIN="$UTIL_DIR/server" +CLIENT_BIN="$UTIL_DIR/homa_test" +BACKGROUND_PIDS=() +LAST_PID="" +MODE=${MODE:-development} +STRESS_ITERATIONS=${STRESS_ITERATIONS:-1} +STRESS_WORKERS=${STRESS_WORKERS:-8} +RESULTS_FILE="$ARTIFACT_DIR/results.tsv" +RUN_FAILED=0 +CLEANUP_STARTED=0 +KERNEL_LOG_BASELINE_LINES=0 + +log() +{ + printf '%s\n' "$*" +} + +fail() +{ + record_result harness fail "$*" + log "FAIL: $*" + exit 1 +} + +record_result() +{ + local scenario=$1 + local status=$2 + local detail=$3 + + printf '%s\t%s\t%s\n' "$scenario" "$status" "$detail" \ + >> "$RESULTS_FILE" + if [[ "$status" == fail || + ("$MODE" == signoff && "$status" != pass) ]]; then + RUN_FAILED=1 + fi +} + +write_summary() +{ + python3 - "$RESULTS_FILE" "$ARTIFACT_DIR/results.json" \ + "$(uname -r)" "$MODE" "$ARTIFACT_DIR" <<'PY' +import json +import sys + +results_path, output_path, kernel, mode, artifacts = sys.argv[1:] +results = [] +with open(results_path, encoding="utf-8") as input_file: + for line in input_file: + scenario, status, detail = line.rstrip("\n").split("\t", 2) + results.append({ + "scenario": scenario, + "status": status, + "detail": detail, + }) +summary = { + "artifacts": artifacts, + "kernel": kernel, + "mode": mode, + "results": results, +} +with open(output_path, "w", encoding="utf-8") as output_file: + json.dump(summary, output_file, indent=2, sort_keys=True) + output_file.write("\n") +PY +} + +require_command() +{ + command -v "$1" >/dev/null 2>&1 || fail "required command not found: $1" +} + +register_pid() +{ + BACKGROUND_PIDS+=("$1") +} + +remove_pid() +{ + local target=$1 + local remaining=() + local pid + + for pid in "${BACKGROUND_PIDS[@]}"; do + if [[ "$pid" != "$target" ]]; then + remaining+=("$pid") + fi + done + BACKGROUND_PIDS=("${remaining[@]}") +} + +stop_pid() +{ + local pid=$1 + local signal=${2:-TERM} + + if kill -0 "$pid" 2>/dev/null; then + kill -s "$signal" "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + fi + remove_pid "$pid" +} + +cleanup() +{ + local pid + local diagnostics="$ARTIFACT_DIR/kernel-diagnostics.log" + local leaked_namespaces + + if (( CLEANUP_STARTED )); then + return + fi + CLEANUP_STARTED=1 + + for pid in "${BACKGROUND_PIDS[@]}"; do + if kill -0 "$pid" 2>/dev/null; then + kill "$pid" 2>/dev/null || true + fi + done + for pid in "${BACKGROUND_PIDS[@]}"; do + wait "$pid" 2>/dev/null || true + done + ip netns del "$NS_CLIENT" 2>/dev/null || true + ip netns del "$NS_SERVER" 2>/dev/null || true + leaked_namespaces=$(ip netns list 2>/dev/null | grep -Ec \ + "^($NS_CLIENT|$NS_SERVER)( |$)" || true) + if (( leaked_namespaces )); then + record_result cleanup fail "test namespaces remain after cleanup" + else + record_result cleanup pass "processes and test namespaces removed" + fi + + dmesg > "$ARTIFACT_DIR/kernel-after.log" 2>/dev/null || true + tail -n +$((KERNEL_LOG_BASELINE_LINES + 1)) \ + "$ARTIFACT_DIR/kernel-after.log" > "$ARTIFACT_DIR/kernel-new.log" + if grep -Ei 'BUG:|WARNING:|KASAN:|use-after-free|double-free|refcount|lockdep|RCU.*stall|soft lockup|hung task|kernel oops' \ + "$ARTIFACT_DIR/kernel-new.log" > "$diagnostics"; then + record_result kernel-diagnostics fail \ + "new kernel warning found; see kernel-diagnostics.log" + else + record_result kernel-diagnostics pass "no new kernel warnings" + fi + write_summary +} +trap cleanup EXIT INT TERM + +wait_for_pattern() +{ + local file=$1 + local pattern=$2 + local deadline=$((SECONDS + 10)) + + until grep -q "$pattern" "$file" 2>/dev/null; do + if (( SECONDS >= deadline )); then + return 1 + fi + done +} + +set_udp() +{ + local namespace=$1 + local value=$2 + + ip netns exec "$namespace" sysctl -q -w \ + net.homa.hijack_udp="$value" +} + +udp_value() +{ + ip netns exec "$1" cat /proc/sys/net/homa/hijack_udp +} + +wait_for_udp_port_pair() +{ + local namespace=$1 + local timeout_ms=$2 + + ip netns exec "$namespace" python3 - "$timeout_ms" <<'PY' +import socket +import sys +import time + +deadline = time.monotonic() + int(sys.argv[1]) / 1000 +while True: + sockets = [] + try: + sock4 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock4.bind(("0.0.0.0", 54321)) + sockets.append(sock4) + sock6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM) + sock6.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 1) + sock6.bind(("::", 54321)) + sockets.append(sock6) + break + except OSError: + for active_socket in sockets: + active_socket.close() + if time.monotonic() >= deadline: + raise + time.sleep(0.005) +for active_socket in sockets: + active_socket.close() +PY +} + +trace_release_disable() +{ + local namespace=$1 + local trace=/sys/kernel/tracing + local count + + if [[ ! -w "$trace/current_tracer" ]] || + ! grep -q '^udp_tunnel_sock_release' \ + "$trace/available_filter_functions" 2>/dev/null || + [[ $(cat "$trace/current_tracer") != nop ]] || + [[ $(cat "$trace/function_profile_enabled") != 0 ]] || + [[ -n $(grep -v '^####' "$trace/set_ftrace_filter" 2>/dev/null) ]]; then + record_result release-count incomplete \ + "ftrace is unavailable or already in use" + set_udp "$namespace" 0 + return + fi + + printf '0\n' > "$trace/tracing_on" + printf '\n' > "$trace/trace" + printf 'udp_tunnel_sock_release\n' > "$trace/set_ftrace_filter" + printf 'function\n' > "$trace/current_tracer" + printf '1\n' > "$trace/tracing_on" + set_udp "$namespace" 0 + printf '0\n' > "$trace/tracing_on" + count=$(grep -c 'udp_tunnel_sock_release' "$trace/trace" || true) + printf 'nop\n' > "$trace/current_tracer" + printf '\n' > "$trace/set_ftrace_filter" + printf '\n' > "$trace/trace" + printf '1\n' > "$trace/tracing_on" + + if [[ "$count" != 2 ]]; then + fail "expected two udp_tunnel_sock_release calls, observed $count" + fi + record_result release-count pass "two tunnel sockets released" +} + +start_server() +{ + local namespace=$1 + local log_name=$2 + shift 2 + + ip netns exec "$namespace" "$SERVER_BIN" "$@" \ + > "$ARTIFACT_DIR/$log_name" 2>&1 & + LAST_PID=$! + register_pid "$LAST_PID" +} + +run_invoke() +{ + local namespace=$1 + local target=$2 + local length=$3 + local log_name=$4 + shift 4 + local attempt + + for attempt in 1 2 3 4 5; do + if timeout 20 ip netns exec "$namespace" "$CLIENT_BIN" \ + "$target" "$@" --count 1 --length "$length" udp \ + > "$ARTIFACT_DIR/$log_name" 2>&1 && + grep -q "Bandwidth at median" "$ARTIFACT_DIR/$log_name"; then + return 0 + fi + done + return 1 +} + +start_capture() +{ + local namespace=$1 + local interface=$2 + local pcap=$3 + local filter=$4 + local capture_log="${pcap%.pcap}.capture.log" + + : > "$capture_log" + ip netns exec "$namespace" tcpdump --immediate-mode -Q in -U -n \ + -i "$interface" \ + -w "$pcap" "$filter" > "$capture_log" 2>&1 & + LAST_PID=$! + register_pid "$LAST_PID" + wait_for_pattern "$capture_log" "listening on" || + fail "tcpdump did not become ready" +} + +create_namespaces() +{ + ip netns add "$NS_CLIENT" + ip netns add "$NS_SERVER" + ip link add "$VETH_CLIENT" type veth peer name "$VETH_SERVER" + ip link set "$VETH_CLIENT" netns "$NS_CLIENT" + ip link set "$VETH_SERVER" netns "$NS_SERVER" + ip -n "$NS_CLIENT" link set lo up + ip -n "$NS_SERVER" link set lo up + ip -n "$NS_CLIENT" addr add 10.203.0.1/24 dev "$VETH_CLIENT" + ip -n "$NS_SERVER" addr add 10.203.0.2/24 dev "$VETH_SERVER" + ip -n "$NS_CLIENT" addr add fd00:203::1/64 dev "$VETH_CLIENT" + ip -n "$NS_SERVER" addr add fd00:203::2/64 dev "$VETH_SERVER" + ip -n "$NS_CLIENT" link set "$VETH_CLIENT" up + ip -n "$NS_SERVER" link set "$VETH_SERVER" up +} + +mkdir -p "$ARTIFACT_DIR" +: > "$RESULTS_FILE" + +[[ $EUID -eq 0 ]] || fail "run as root" +for command in ip sysctl tcpdump ethtool python3 timeout make tc; do + require_command "$command" +done +[[ -d /proc/sys/net/homa ]] || fail "the Homa module is not loaded" + +dmesg > "$ARTIFACT_DIR/kernel-before.log" 2>/dev/null || true +KERNEL_LOG_BASELINE_LINES=$(wc -l < "$ARTIFACT_DIR/kernel-before.log") +{ + printf 'run_id=%s\n' "$RUN_ID" + printf 'mode=%s\n' "$MODE" + printf 'uname=%s\n' "$(uname -a)" + printf 'module_srcversion=%s\n' \ + "$(cat /sys/module/homa/srcversion 2>/dev/null || echo unavailable)" + printf 'built_srcversion=%s\n' \ + "$(modinfo -F srcversion "$ROOT_DIR/homa.ko" 2>/dev/null || echo unavailable)" + printf 'built_sha256=%s\n' \ + "$(sha256sum "$ROOT_DIR/homa.ko" 2>/dev/null | awk '{print $1}' || echo unavailable)" + printf 'hijack_udp=%s\n' "$(cat /proc/sys/net/homa/hijack_udp)" + grep -E '^CONFIG_(INET|IPV6|NET_UDP_TUNNEL|KASAN|LOCKDEP)=' \ + "/boot/config-$(uname -r)" 2>/dev/null || true +} > "$ARTIFACT_DIR/environment.txt" + +loaded_srcversion=$(cat /sys/module/homa/srcversion 2>/dev/null || true) +built_srcversion=$(modinfo -F srcversion "$ROOT_DIR/homa.ko" 2>/dev/null || true) +if [[ -n "$loaded_srcversion" && "$loaded_srcversion" == "$built_srcversion" ]]; then + record_result module-identity pass \ + "loaded and built srcversion $loaded_srcversion" +else + fail "loaded module srcversion does not match $ROOT_DIR/homa.ko" +fi +make -C "$UTIL_DIR" homa_test server + +kernel_release=$(uname -r) +if [[ "$kernel_release" != 6.17* ]]; then + log "WARNING: running on $kernel_release; final V1 sign-off requires Linux 6.17" + record_result environment incomplete \ + "Linux $kernel_release is regression-only; sign-off requires 6.17" +else + record_result environment pass "Linux 6.17 target kernel" +fi + +create_namespaces + +[[ $(udp_value "$NS_CLIENT") == 0 ]] || fail "client namespace did not start disabled" +[[ $(udp_value "$NS_SERVER") == 0 ]] || fail "server namespace did not start disabled" + +ip netns exec "$NS_CLIENT" python3 -c \ + 'import signal, socket; s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM); s.bind(("0.0.0.0", 54321)); signal.pause()' & +port_holder=$! +register_pid "$port_holder" +deadline=$((SECONDS + 10)) +until ip netns exec "$NS_CLIENT" grep -q ':D431 ' /proc/net/udp; do + (( SECONDS < deadline )) || fail "UDP port-conflict process did not bind" +done +if set_udp "$NS_CLIENT" 1 2> "$ARTIFACT_DIR/port-collision.log"; then + fail "UDP hijack enabled despite a port 54321 collision" +fi +stop_pid "$port_holder" + +set_udp "$NS_CLIENT" 1 +[[ $(udp_value "$NS_SERVER") == 0 ]] || fail "UDP sysctl leaked across namespaces" +trace_release_disable "$NS_CLIENT" +wait_for_udp_port_pair "$NS_CLIENT" 0 || + fail "idle disable did not release IPv4/IPv6 port 54321 synchronously" +record_result idle-release pass "IPv4/IPv6 port 54321 reusable immediately" +set_udp "$NS_CLIENT" 1 +set_udp "$NS_SERVER" 1 + +start_server "$NS_SERVER" server-v4.log --port 4000 --validate --verbose +server_v4=$LAST_PID +if ip netns exec "$NS_CLIENT" ethtool -K "$VETH_CLIENT" tx off \ + > "$ARTIFACT_DIR/offload-disable-client.log" 2>&1 && + ip netns exec "$NS_SERVER" ethtool -K "$VETH_SERVER" tx off \ + > "$ARTIFACT_DIR/offload-disable-server.log" 2>&1; then + offload_off_pcap="$ARTIFACT_DIR/udp-ipv4-offload-off.pcap" + ip netns exec "$NS_CLIENT" ethtool -k "$VETH_CLIENT" \ + > "$ARTIFACT_DIR/offload-off-features.log" + start_capture "$NS_SERVER" "$VETH_SERVER" "$offload_off_pcap" \ + "udp port 54321" + offload_capture_pid=$LAST_PID + run_invoke "$NS_CLIENT" 10.203.0.2:4000 100 ipv4-small.log || + fail "IPv4 UDP request/response failed with TX offload disabled" + run_invoke "$NS_CLIENT" 10.203.0.2:4000 8192 ipv4-large.log || + fail "above-MTU IPv4 UDP request/response failed with TX offload disabled" + run_invoke "$NS_CLIENT" 10.203.0.2:4000 4096 offload-disabled.log || + fail "UDP request/response failed with TX offload disabled" + stop_pid "$offload_capture_pid" INT + python3 "$SCRIPT_DIR/verify_udp_pcap.py" "$offload_off_pcap" \ + > "$ARTIFACT_DIR/pcap-ipv4-offload-off.log" + python3 "$SCRIPT_DIR/verify_udp_pcap.py" --json "$offload_off_pcap" \ + > "$ARTIFACT_DIR/pcap-ipv4-offload-off.json" + record_result checksum-offload-off pass \ + "receiver-side IPv4 fallback checksums validated" +else + fail "veth TX checksum offload cannot be disabled" +fi + +udp_pcap="$ARTIFACT_DIR/udp-ipv4-offload-on.pcap" +ip netns exec "$NS_CLIENT" ethtool -K "$VETH_CLIENT" tx on \ + > "$ARTIFACT_DIR/offload-enable-client.log" 2>&1 || true +ip netns exec "$NS_SERVER" ethtool -K "$VETH_SERVER" tx on \ + > "$ARTIFACT_DIR/offload-enable-server.log" 2>&1 || true +ip netns exec "$NS_CLIENT" ethtool -k "$VETH_CLIENT" \ + > "$ARTIFACT_DIR/offload-on-features.log" +start_capture "$NS_SERVER" "$VETH_SERVER" "$udp_pcap" "udp port 54321" +capture_pid=$LAST_PID +run_invoke "$NS_CLIENT" 10.203.0.2:4000 4096 offload-enabled.log || + fail "IPv4 UDP request/response failed with TX offload enabled" +stop_pid "$capture_pid" INT +python3 "$SCRIPT_DIR/verify_udp_pcap.py" --allow-partial-checksum \ + "$udp_pcap" \ + > "$ARTIFACT_DIR/pcap-ipv4-offload-on.log" +python3 "$SCRIPT_DIR/verify_udp_pcap.py" --allow-partial-checksum --json \ + "$udp_pcap" \ + > "$ARTIFACT_DIR/pcap-ipv4-offload-on.json" +record_result checksum-offload-on incomplete \ + "veth preserved valid partial checksum seeds; physical wire completion is required" + +start_server "$NS_CLIENT" server-v6.log --ipv6 --port 4001 --validate --verbose +server_v6=$LAST_PID +ipv6_pcap="$ARTIFACT_DIR/udp-ipv6-loopback.pcap" +start_capture "$NS_CLIENT" lo "$ipv6_pcap" "udp port 54321" +ipv6_capture_pid=$LAST_PID +run_invoke "$NS_CLIENT" localhost:4001 8192 ipv6-large.log --ipv6 || + fail "IPv6 UDP request/response failed" +stop_pid "$ipv6_capture_pid" INT +python3 "$SCRIPT_DIR/verify_udp_pcap.py" --allow-partial-checksum \ + "$ipv6_pcap" \ + > "$ARTIFACT_DIR/pcap-ipv6.log" +python3 "$SCRIPT_DIR/verify_udp_pcap.py" --allow-partial-checksum --json \ + "$ipv6_pcap" \ + > "$ARTIFACT_DIR/pcap-ipv6.json" +python3 "$SCRIPT_DIR/verify_transport_pcap.py" --expect udp --json \ + "$udp_pcap" \ + > "$ARTIFACT_DIR/transport-udp-ipv4.json" +python3 "$SCRIPT_DIR/verify_transport_pcap.py" --expect udp --json \ + "$ipv6_pcap" \ + > "$ARTIFACT_DIR/transport-udp-ipv6.json" +record_result ipv6-traffic incomplete \ + "IPv6 loopback traffic passed with valid partial checksum seeds; physical wire completion is required" + +for iteration in $(seq 1 "$STRESS_ITERATIONS"); do + race_pids=() + for index in $(seq 1 "$STRESS_WORKERS"); do + timeout 20 ip netns exec "$NS_CLIENT" "$CLIENT_BIN" \ + 10.203.0.2:4000 --count 1 --length 65536 udp \ + > "$ARTIFACT_DIR/race-$iteration-$index.log" 2>&1 & + race_pids+=("$!") + done + set_udp "$NS_CLIENT" 0 + set_udp "$NS_SERVER" 0 + for pid in "${race_pids[@]}"; do + wait "$pid" 2>/dev/null || true + done + wait_for_udp_port_pair "$NS_CLIENT" 350 || + fail "client UDP tunnel sockets not released within 350 ms" + wait_for_udp_port_pair "$NS_SERVER" 350 || + fail "server UDP tunnel sockets not released within 350 ms" + set_udp "$NS_CLIENT" 1 + set_udp "$NS_SERVER" 1 +done +run_invoke "$NS_CLIENT" 10.203.0.2:4000 4096 reenabled.log || + fail "UDP request/response failed after disable/re-enable stress" +record_result lifecycle pass \ + "$STRESS_ITERATIONS iterations with $STRESS_WORKERS workers" + +stop_pid "$server_v4" +stop_pid "$server_v6" +set_udp "$NS_CLIENT" 0 +set_udp "$NS_SERVER" 0 +start_server "$NS_SERVER" server-native.log --port 4002 --validate --verbose +server_native=$LAST_PID +native_pcap="$ARTIFACT_DIR/native.pcap" +start_capture "$NS_SERVER" "$VETH_SERVER" "$native_pcap" \ + "ip proto 146 or ip6 proto 146" +native_capture_pid=$LAST_PID +run_invoke "$NS_CLIENT" 10.203.0.2:4002 4096 native.log || + fail "native Homa regression failed" +stop_pid "$native_capture_pid" INT +stop_pid "$server_native" +python3 "$SCRIPT_DIR/verify_transport_pcap.py" --expect native --json \ + "$native_pcap" \ + > "$ARTIFACT_DIR/transport-native.json" + +set_udp "$NS_CLIENT" 1 +set_udp "$NS_SERVER" 1 +ip netns exec "$NS_CLIENT" tc qdisc add dev "$VETH_CLIENT" root \ + netem delay 100ms +ip netns exec "$NS_SERVER" tc qdisc add dev "$VETH_SERVER" root \ + netem delay 100ms +start_server "$NS_SERVER" server-teardown.log --port 4003 --validate --verbose +teardown_server=$LAST_PID +teardown_pids=() +for index in $(seq 1 "$STRESS_WORKERS"); do + timeout 20 ip netns exec "$NS_CLIENT" "$CLIENT_BIN" \ + 10.203.0.2:4003 --count 1 --length 1000000 udp \ + > "$ARTIFACT_DIR/teardown-$index.log" 2>&1 & + teardown_pids+=("$!") + register_pid "$!" +done +deadline=$((SECONDS + 10)) +until ip netns exec "$NS_CLIENT" tc -s qdisc show dev "$VETH_CLIENT" | + grep -Eq 'Sent [1-9][0-9]* bytes [1-9][0-9]* pkt'; do + (( SECONDS < deadline )) || fail "teardown traffic did not reach the qdisc" +done +ip netns del "$NS_CLIENT" +ip netns del "$NS_SERVER" +for pid in "${teardown_pids[@]}"; do + stop_pid "$pid" +done +stop_pid "$teardown_server" +if ip netns list | grep -Eq "^($NS_CLIENT|$NS_SERVER)( |$)"; then + fail "namespace name remained after active teardown" +fi +create_namespaces +[[ $(udp_value "$NS_CLIENT") == 0 ]] || + fail "recreated client namespace inherited UDP enablement" +[[ $(udp_value "$NS_SERVER") == 0 ]] || + fail "recreated server namespace inherited UDP enablement" +set_udp "$NS_CLIENT" 1 +set_udp "$NS_SERVER" 1 +start_server "$NS_SERVER" server-after-teardown.log \ + --port 4004 --validate --verbose +server_after_teardown=$LAST_PID +run_invoke "$NS_CLIENT" 10.203.0.2:4004 4096 after-teardown.log || + fail "UDP request/response failed after active namespace teardown" +stop_pid "$server_after_teardown" +record_result namespace-teardown pass \ + "active namespaces deleted and recreated; UDP RPC succeeded after reuse" + +log "PASS: UDP tunnel integration checks completed" +log "Artifacts: $ARTIFACT_DIR" +record_result integration pass "UDP and native regression checks completed" +cleanup +trap - EXIT INT TERM +exit "$RUN_FAILED" \ No newline at end of file diff --git a/test/integration/test_homa_test_udp.py b/test/integration/test_homa_test_udp.py new file mode 100644 index 00000000..0df41992 --- /dev/null +++ b/test/integration/test_homa_test_udp.py @@ -0,0 +1,216 @@ +# Compile and test the UDP client and server with mocked socket I/O, +# checking request counts, errors, and startup transport labels without traffic. + +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +MOCK_SOURCE = r""" +#include +#include +#include +#include +#include +#include + +static int requests = 0; +static int responses = 0; +static size_t message_length = 0; + +extern "C" int __wrap_socket(int domain, int type, int protocol) +{ + return dup(STDERR_FILENO); +} + +extern "C" int __wrap_setsockopt(int descriptor, int level, int option, + const void *value, socklen_t value_length) +{ + return 0; +} + +extern "C" int __wrap_bind(int descriptor, const struct sockaddr *address, + socklen_t address_length) +{ + return 0; +} + +extern "C" int __wrap_getsockopt(int descriptor, int level, int option, + void *value, socklen_t *value_length) +{ + const char *protocol = getenv("HOMA_TEST_MOCK_PROTOCOL"); + if (!protocol || strcmp(protocol, "fail") == 0) { + errno = EOPNOTSUPP; + return -1; + } + if (level != SOL_SOCKET || option != SO_PROTOCOL || + *value_length != sizeof(int)) { + errno = EINVAL; + return -1; + } + *static_cast(value) = atoi(protocol); + *value_length = sizeof(int); + return 0; +} + +extern "C" ssize_t __wrap_sendmsg(int descriptor, + const struct msghdr *message, int flags) +{ + requests++; + message_length = message->msg_iov[0].iov_len; + const char *failure = getenv("HOMA_TEST_MOCK_FAIL"); + if (failure && strcmp(failure, "send") == 0) { + errno = EIO; + return -1; + } + return message_length; +} + +extern "C" ssize_t __wrap_recvmsg(int descriptor, + struct msghdr *message, int flags) +{ + responses++; + if (getenv("HOMA_TEST_MOCK_SERVER")) + exit(0); + const char *failure = getenv("HOMA_TEST_MOCK_FAIL"); + if (failure && strcmp(failure, "receive") == 0) { + errno = EIO; + return -1; + } + return message_length; +} + +__attribute__((destructor)) static void report_counts() +{ + fprintf(stderr, "MOCK requests=%d responses=%d length=%zu\n", + requests, responses, message_length); +} + +#ifdef main +#undef main +extern void homa_server(int port); +int main() +{ + homa_server(8000); + return 0; +} +#endif +""" + + +class HomaTestUdpTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + root = Path(__file__).resolve().parents[2] + cls.workspace = tempfile.TemporaryDirectory( + prefix="homa-test-udp-", dir=root / "test/integration/artifacts") + cls.addClassCleanup(cls.workspace.cleanup) + cls.client = Path(cls.workspace.name) / "homa_test" + cls.server = Path(cls.workspace.name) / "server" + for program in ("homa_test", "server"): + definitions = ["-Dmain=homa_server_main"] if program == "server" else [] + subprocess.run([ + "g++", "-std=c++17", "-O2", "-I" + str(root), + *definitions, + str(root / "util" / (program + ".cc")), + str(root / "util/test_utils.cc"), + str(root / "util/dist.cc"), + str(root / "util/time_trace.cc"), + str(root / "homa_receiver.cc"), "-x", "c++", "-", + "-Wl,--wrap=socket,--wrap=setsockopt,--wrap=sendmsg,--wrap=recvmsg", + "-Wl,--wrap=bind,--wrap=getsockopt", + "-lpthread", "-o", str(Path(cls.workspace.name) / program), + ], input=MOCK_SOURCE, text=True, check=True, capture_output=True) + + def run_client(self, *arguments, failure=None): + environment = os.environ.copy() + environment.pop("HOMA_TEST_MOCK_FAIL", None) + environment.pop("HOMA_TEST_MOCK_SERVER", None) + if failure: + environment["HOMA_TEST_MOCK_FAIL"] = failure + return subprocess.run( + [str(self.client), "127.0.0.1:8000", *arguments], + env=environment, text=True, capture_output=True, timeout=10) + + def run_server(self, protocol): + environment = os.environ.copy() + environment["HOMA_TEST_MOCK_SERVER"] = "1" + environment["HOMA_TEST_MOCK_PROTOCOL"] = protocol + return subprocess.run( + [str(self.server), "--port", "8000"], env=environment, + text=True, capture_output=True, timeout=10) + + def test_server_reports_udp_without_verbose(self): + result = self.run_server("17") + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("Server transport: Homa-over-UDP (port 8000)", result.stdout) + self.assertNotIn("Server transport: native Homa", result.stdout) + + def test_server_reports_homa_over_tcp_without_verbose(self): + result = self.run_server("6") + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("Server transport: Homa-over-TCP (port 8000)", result.stdout) + self.assertNotIn("Server transport: unknown", result.stdout) + + def test_server_reports_native_homa_without_verbose(self): + result = self.run_server("146") + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("Server transport: native Homa (port 8000)", result.stdout) + self.assertNotIn("Server transport: Homa-over-UDP", result.stdout) + + def test_server_query_failure_does_not_guess_or_stop(self): + result = self.run_server("fail") + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("Server transport: unknown (SO_PROTOCOL failed:", result.stdout) + self.assertIn("requests=0 responses=1", result.stderr) + self.assertNotIn("Server transport: native Homa", result.stdout) + self.assertNotIn("Server transport: Homa-over-UDP", result.stdout) + + def test_server_reports_unrecognized_protocol(self): + result = self.run_server("253") + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("Server transport: unknown (IP protocol 253, port 8000)", + result.stdout) + + def test_count_and_length_include_ten_warmups(self): + result = self.run_client( + "--count", "2", "--length", "1000", "--seed", "1", "udp") + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("requests=12 responses=12 length=1000", result.stderr) + self.assertIn("Bandwidth at median", result.stdout) + + def test_default_count_is_one_thousand(self): + result = self.run_client("--length", "1000", "udp") + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("requests=1010 responses=1010 length=1000", result.stderr) + + def test_rtt_keeps_its_request_count(self): + result = self.run_client("--count", "2", "rtt") + self.assertEqual(0, result.returncode, result.stdout + result.stderr) + self.assertIn("requests=12 responses=12", result.stderr) + + def test_udp_send_failure_exits_nonzero(self): + result = self.run_client("--count", "2", "udp", failure="send") + self.assertEqual(1, result.returncode) + self.assertIn("Error in sendmsg", result.stdout) + self.assertIn("requests=1 responses=0", result.stderr) + self.assertNotIn("Bandwidth at median", result.stdout) + + def test_udp_receive_failure_exits_nonzero(self): + result = self.run_client("--count", "2", "udp", failure="receive") + self.assertEqual(1, result.returncode) + self.assertIn("Error in recvmsg", result.stdout) + self.assertIn("requests=1 responses=1", result.stderr) + self.assertNotIn("Bandwidth at median", result.stdout) + + def test_invoke_operation_is_removed(self): + result = self.run_client("invoke") + self.assertEqual(1, result.returncode) + self.assertIn("Unknown operation 'invoke'", result.stdout) + self.assertIn("requests=0 responses=0", result.stderr) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/integration/test_verify_transport_pcap.py b/test/integration/test_verify_transport_pcap.py new file mode 100644 index 00000000..a06d2ccb --- /dev/null +++ b/test/integration/test_verify_transport_pcap.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# Unit-test native Homa and UDP tunnel capture classification +# using synthetic Ethernet/IP packets without live network traffic. + +import socket +import struct +import unittest + +import verify_transport_pcap + + +def ipv4_frame(protocol, source_port=0, destination_port=0): + payload = bytearray(28) + if protocol == socket.IPPROTO_UDP: + struct.pack_into("!HH", payload, 0, source_port, destination_port) + ip_header = bytearray(20) + ip_header[0] = 0x45 + struct.pack_into("!H", ip_header, 2, 20 + len(payload)) + ip_header[9] = protocol + return b"\0" * 12 + b"\x08\x00" + bytes(ip_header) + bytes(payload) + + +def ipv6_frame(protocol, source_port=0, destination_port=0): + payload = bytearray(28) + if protocol == socket.IPPROTO_UDP: + struct.pack_into("!HH", payload, 0, source_port, destination_port) + ip_header = struct.pack("!IHBB16s16s", 6 << 28, len(payload), protocol, + 64, b"\0" * 16, b"\0" * 16) + return b"\0" * 12 + b"\x86\xdd" + ip_header + bytes(payload) + + +class VerifyTransportPcapTest(unittest.TestCase): + def test_native_ipv4_and_ipv6(self): + self.assertEqual( + "native", verify_transport_pcap.classify_transport( + ipv4_frame(verify_transport_pcap.IPPROTO_HOMA))) + self.assertEqual( + "native", verify_transport_pcap.classify_transport( + ipv6_frame(verify_transport_pcap.IPPROTO_HOMA))) + + def test_udp_requires_fixed_tunnel_ports(self): + self.assertEqual( + "udp", verify_transport_pcap.classify_transport( + ipv4_frame(socket.IPPROTO_UDP, 54321, 54321))) + self.assertIsNone(verify_transport_pcap.classify_transport( + ipv6_frame(socket.IPPROTO_UDP, 54321, 4000))) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/integration/test_verify_udp_pacing.py b/test/integration/test_verify_udp_pacing.py new file mode 100755 index 00000000..e8599091 --- /dev/null +++ b/test/integration/test_verify_udp_pacing.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# Unit-test UDP pacing rate summaries and acceptance limits +# using synthetic timestamped traffic samples. + +import unittest + +import verify_udp_pacing + + +class VerifyUdpPacingTest(unittest.TestCase): + def test_rate_summary(self): + samples = [(index / 1000, 12500) for index in range(1000)] + + summary = verify_udp_pacing.rate_summary(samples) + + self.assertGreaterEqual(summary["steady_mbps"], 99) + self.assertLessEqual(summary["steady_mbps"], 101) + self.assertGreaterEqual(summary["max_100ms_mbps"], 99) + self.assertLessEqual(summary["max_100ms_mbps"], 101) + + def test_short_capture_rejected(self): + with self.assertRaisesRegex(ValueError, "too short"): + verify_udp_pacing.rate_summary([(0, 100), (0.1, 100)]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/integration/test_verify_udp_pcap.py b/test/integration/test_verify_udp_pcap.py new file mode 100644 index 00000000..34ae2e7b --- /dev/null +++ b/test/integration/test_verify_udp_pcap.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# Unit-test UDP tunnel packet validation and checksum construction +# using synthetic valid and malformed packets. + +import struct +import unittest + +import inject_udp_checksum +import verify_udp_pcap + + +def internet_checksum(data): + if len(data) % 2: + data += b"\0" + total = sum(struct.unpack("!%dH" % (len(data) // 2), data)) + while total >> 16: + total = (total & 0xFFFF) + (total >> 16) + checksum = (~total) & 0xFFFF + return checksum or 0xFFFF + + +def homa_data_payload(message_length=100, segment_offset=0, + segment_length=100): + payload = bytearray(56 + segment_length) + payload[11] = 0x10 + struct.pack_into("!I", payload, 28, message_length) + struct.pack_into("!I", payload, 52, segment_offset) + return bytes(payload) + + +def ipv4_udp_frame(payload, fragment=0, udp_length=None, partial=False): + source = b"\x0a\x00\x00\x01" + destination = b"\x0a\x00\x00\x02" + actual_udp_length = 8 + len(payload) + wire_udp_length = actual_udp_length if udp_length is None else udp_length + udp_header = struct.pack("!HHHH", verify_udp_pcap.UDP_PORT, + verify_udp_pcap.UDP_PORT, wire_udp_length, 0) + pseudo_header = source + destination + struct.pack( + "!BBH", 0, 17, actual_udp_length) + if partial: + checksum = verify_udp_pcap.checksum_sum(pseudo_header) + else: + checksum = internet_checksum(pseudo_header + udp_header + payload) + udp_header = struct.pack("!HHHH", verify_udp_pcap.UDP_PORT, + verify_udp_pcap.UDP_PORT, wire_udp_length, + checksum) + ip_header = bytearray(20) + ip_header[0] = 0x45 + struct.pack_into("!H", ip_header, 2, 20 + actual_udp_length) + struct.pack_into("!H", ip_header, 6, fragment) + ip_header[8] = 64 + ip_header[9] = 17 + ip_header[12:16] = source + ip_header[16:20] = destination + ethernet = b"\0" * 12 + b"\x08\x00" + return ethernet + bytes(ip_header) + udp_header + payload + + +class VerifyUdpPcapTest(unittest.TestCase): + def test_injected_ipv4_checksum(self): + frame = inject_udp_checksum.build_frame( + 4, "10.0.0.1", "10.0.0.2", "02:00:00:00:00:01", + "02:00:00:00:00:02") + parsed = verify_udp_pcap.parse_udp(frame) + + self.assertTrue(parsed["checksum_valid"]) + self.assertEqual(0x14, verify_udp_pcap.validate_homa_packet(parsed)) + + corrupted = inject_udp_checksum.build_frame( + 4, "10.0.0.1", "10.0.0.2", "02:00:00:00:00:01", + "02:00:00:00:00:02", invalid=True) + self.assertFalse( + verify_udp_pcap.parse_udp(corrupted)["checksum_valid"]) + + zero_checksum = inject_udp_checksum.build_frame( + 4, "10.0.0.1", "10.0.0.2", "02:00:00:00:00:01", + "02:00:00:00:00:02", zero_checksum=True) + parsed = verify_udp_pcap.parse_udp(zero_checksum) + self.assertTrue(parsed["checksum_valid"]) + self.assertEqual(0xFFFF, parsed["checksum"]) + + def test_injected_ipv6_checksum(self): + frame = inject_udp_checksum.build_frame( + 6, "fd00::1", "fd00::2", "02:00:00:00:00:01", + "02:00:00:00:00:02") + parsed = verify_udp_pcap.parse_udp(frame) + + self.assertTrue(parsed["checksum_valid"]) + self.assertEqual(0x14, verify_udp_pcap.validate_homa_packet(parsed)) + + corrupted = inject_udp_checksum.build_frame( + 6, "fd00::1", "fd00::2", "02:00:00:00:00:01", + "02:00:00:00:00:02", invalid=True) + self.assertFalse( + verify_udp_pcap.parse_udp(corrupted)["checksum_valid"]) + + zero_checksum = inject_udp_checksum.build_frame( + 6, "fd00::1", "fd00::2", "02:00:00:00:00:01", + "02:00:00:00:00:02", zero_checksum=True) + parsed = verify_udp_pcap.parse_udp(zero_checksum) + self.assertTrue(parsed["checksum_valid"]) + self.assertEqual(0xFFFF, parsed["checksum"]) + + def test_injected_data_packet(self): + frame = inject_udp_checksum.build_frame( + 4, "10.0.0.1", "10.0.0.2", "02:00:00:00:00:01", + "02:00:00:00:00:02", data_packet=True) + parsed = verify_udp_pcap.parse_udp(frame) + + self.assertTrue(parsed["checksum_valid"]) + self.assertEqual(0x10, verify_udp_pcap.validate_homa_packet(parsed)) + self.assertEqual(184, parsed["outer_length"]) + + def test_injected_control_packets(self): + for control_type, expected_type, expected_length in ( + ("resend", 0x12, 37), ("need-ack", 0x17, 28)): + frame = inject_udp_checksum.build_frame( + 4, "10.0.0.1", "10.0.0.2", "02:00:00:00:00:01", + "02:00:00:00:00:02", control_type=control_type, + source_port=4100, destination_port=4200) + parsed = verify_udp_pcap.parse_udp(frame) + + self.assertTrue(parsed["checksum_valid"]) + self.assertEqual(expected_type, + verify_udp_pcap.validate_homa_packet(parsed)) + self.assertEqual(expected_length, len(parsed["payload"])) + self.assertEqual((4100, 4200), struct.unpack_from( + "!HH", parsed["payload"], 0)) + + def test_valid_data_packet(self): + parsed = verify_udp_pcap.parse_udp( + ipv4_udp_frame(homa_data_payload())) + + self.assertEqual("ipv4", parsed["family"]) + self.assertEqual(184, parsed["outer_length"]) + self.assertTrue(parsed["checksum_valid"]) + self.assertEqual(0x10, verify_udp_pcap.validate_homa_packet(parsed)) + + def test_ipv4_fragment_rejected(self): + with self.assertRaisesRegex(ValueError, "fragmented IPv4"): + verify_udp_pcap.parse_udp( + ipv4_udp_frame(homa_data_payload(), fragment=0x2000)) + + def test_partial_checksum_requires_explicit_opt_in(self): + parsed = verify_udp_pcap.parse_udp( + ipv4_udp_frame(homa_data_payload(), partial=True)) + + self.assertTrue(parsed["checksum_partial"]) + self.assertFalse(parsed["checksum_valid"]) + with self.assertRaisesRegex(ValueError, "checksum is invalid"): + verify_udp_pcap.validate_homa_packet(parsed) + self.assertEqual( + 0x10, verify_udp_pcap.validate_homa_packet( + parsed, allow_partial_checksum=True)) + + def test_udp_length_mismatch_rejected(self): + with self.assertRaisesRegex(ValueError, "UDP length"): + verify_udp_pcap.parse_udp( + ipv4_udp_frame(homa_data_payload(), udp_length=20)) + + def test_short_type_specific_header_rejected(self): + parsed = verify_udp_pcap.parse_udp( + ipv4_udp_frame(homa_data_payload(segment_length=0)[:55])) + + with self.assertRaisesRegex(ValueError, "type-specific header"): + verify_udp_pcap.validate_homa_packet(parsed) + + def test_data_segment_outside_message_rejected(self): + parsed = verify_udp_pcap.parse_udp(ipv4_udp_frame( + homa_data_payload(message_length=100, segment_offset=80, + segment_length=40))) + + with self.assertRaisesRegex(ValueError, "exceeds message bounds"): + verify_udp_pcap.validate_homa_packet(parsed) + + +if __name__ == "__main__": + unittest.main() \ No newline at end of file diff --git a/test/integration/test_verify_udp_retransmit.py b/test/integration/test_verify_udp_retransmit.py new file mode 100755 index 00000000..cd88922e --- /dev/null +++ b/test/integration/test_verify_udp_retransmit.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# Unit-test retransmitted DATA identity and Homa control packet detection +# using synthetic UDP tunnel packets. + +import struct +import unittest + +import test_verify_udp_pcap +import verify_udp_pcap +import verify_udp_retransmit + + +class VerifyUdpRetransmitTest(unittest.TestCase): + def test_repeated_data_identity_and_packet_types(self): + data = bytearray(test_verify_udp_pcap.homa_data_payload()) + struct.pack_into("!Q", data, 20, 42) + data[48] = 1 + parsed_data = verify_udp_pcap.parse_udp( + test_verify_udp_pcap.ipv4_udp_frame(bytes(data))) + resend = bytearray(37) + resend[11] = verify_udp_retransmit.RESEND + struct.pack_into("!QII", resend, 20, 43, 0, 100) + parsed_resend = verify_udp_pcap.parse_udp( + test_verify_udp_pcap.ipv4_udp_frame(bytes(resend))) + rpc_unknown = bytearray(28) + rpc_unknown[11] = verify_udp_retransmit.RPC_UNKNOWN + ack = bytearray(80) + ack[11] = verify_udp_retransmit.ACK + + summary = verify_udp_retransmit.summarize( + [parsed_data, parsed_resend, + verify_udp_pcap.parse_udp( + test_verify_udp_pcap.ipv4_udp_frame(bytes(rpc_unknown))), + verify_udp_pcap.parse_udp( + test_verify_udp_pcap.ipv4_udp_frame(bytes(ack)))]) + + self.assertEqual({"42:0": 1}, summary["retransmitted_segments"]) + self.assertTrue(verify_udp_retransmit.has_matching_resend(summary)) + self.assertEqual(1, summary["packet_types"]["0x10"]) + self.assertEqual(1, summary["packet_types"]["0x12"]) + self.assertEqual(1, summary["packet_types"]["0x13"]) + self.assertEqual(1, summary["packet_types"]["0x18"]) + + def test_unique_data_is_not_retransmission(self): + first = bytearray(test_verify_udp_pcap.homa_data_payload()) + struct.pack_into("!Q", first, 20, 42) + + summary = verify_udp_retransmit.summarize([ + verify_udp_pcap.parse_udp( + test_verify_udp_pcap.ipv4_udp_frame(bytes(first))), + ]) + + self.assertEqual({}, summary["retransmitted_segments"]) + self.assertFalse(verify_udp_retransmit.has_matching_resend(summary)) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/integration/verify_transport_pcap.py b/test/integration/verify_transport_pcap.py new file mode 100644 index 00000000..12926282 --- /dev/null +++ b/test/integration/verify_transport_pcap.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +# Classify captured packets as native Homa or Homa-over-UDP and verify +# that the capture contains the expected transport. + +import argparse +from collections import Counter +import json +import socket +import struct +import sys + +import verify_udp_pcap + + +IPPROTO_HOMA = 146 +UDP_PORT = 54321 +TRANSPORTS = ("native", "udp") + + +def classify_transport(frame): + if len(frame) < 14: + return None + ethertype = struct.unpack_from("!H", frame, 12)[0] + offset = 14 + while ethertype in (0x8100, 0x88A8): + if len(frame) < offset + 4: + return None + ethertype = struct.unpack_from("!H", frame, offset + 2)[0] + offset += 4 + + if ethertype == 0x0800: + if len(frame) < offset + 20: + return None + header_length = (frame[offset] & 0x0F) * 4 + if header_length < 20 or len(frame) < offset + header_length: + return None + protocol = frame[offset + 9] + transport_offset = offset + header_length + elif ethertype == 0x86DD: + if len(frame) < offset + 40: + return None + payload_length = struct.unpack_from("!H", frame, offset + 4)[0] + packet_end = min(len(frame), offset + 40 + payload_length) + protocol, transport_offset = verify_udp_pcap.ipv6_transport( + frame, offset, packet_end) + else: + return None + + if protocol == IPPROTO_HOMA: + return "native" + if protocol == socket.IPPROTO_UDP and len(frame) >= transport_offset + 8: + source_port, destination_port = struct.unpack_from( + "!HH", frame, transport_offset) + if source_port == UDP_PORT and destination_port == UDP_PORT: + return "udp" + return None + + +def main(): + parser = argparse.ArgumentParser( + description="Verify the selected outer transport in a Homa pcap") + parser.add_argument("pcap") + parser.add_argument("--expect", required=True, choices=TRANSPORTS) + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + + counts = Counter() + for _, frame in verify_udp_pcap.read_pcap(args.pcap): + transport = classify_transport(frame) + if transport is not None: + counts[transport] += 1 + if counts[args.expect] == 0: + raise ValueError("capture contains no %s Homa packets" % args.expect) + unexpected = {name: counts[name] for name in TRANSPORTS + if name != args.expect and counts[name]} + if unexpected: + raise ValueError("capture contains unexpected transports: %s" % + ", ".join("%s=%d" % item + for item in sorted(unexpected.items()))) + + summary = {name: counts[name] for name in TRANSPORTS} + if args.json: + print(json.dumps(summary, sort_keys=True)) + else: + print("validated %s transport: %d packets" % + (args.expect, counts[args.expect])) + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError, struct.error) as error: + print("transport validation failed: %s" % error, file=sys.stderr) + sys.exit(1) \ No newline at end of file diff --git a/test/integration/verify_udp_pacing.py b/test/integration/verify_udp_pacing.py new file mode 100755 index 00000000..9c1f9eff --- /dev/null +++ b/test/integration/verify_udp_pacing.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# Measure captured UDP tunnel traffic rates and verify sustained pacing +# and burst limits for the 100 Mbit/s integration scenario. + +import argparse +from collections import deque +import json +import sys + +import verify_udp_pcap + + +WINDOW_SECONDS = 0.1 +RATE_BITS_PER_SECOND = 100_000_000 +MAX_RATE_BITS_PER_SECOND = RATE_BITS_PER_SECOND * 1.2 +MIN_RATE_BITS_PER_SECOND = RATE_BITS_PER_SECOND * 0.7 + + +def rate_summary(samples): + if len(samples) < 2: + raise ValueError("capture has too few packets for a rate measurement") + active = deque() + active_bytes = 0 + max_window_bytes = 0 + for timestamp, packet_bytes in samples: + active.append((timestamp, packet_bytes)) + active_bytes += packet_bytes + while active and timestamp - active[0][0] >= WINDOW_SECONDS: + active_bytes -= active.popleft()[1] + max_window_bytes = max(max_window_bytes, active_bytes) + + steady_start = samples[0][0] + WINDOW_SECONDS + steady_end = samples[-1][0] - WINDOW_SECONDS + if steady_end <= steady_start: + raise ValueError("capture is too short for steady-state measurement") + steady_bytes = sum(packet_bytes for timestamp, packet_bytes in samples + if steady_start <= timestamp <= steady_end) + steady_rate = steady_bytes * 8 / (steady_end - steady_start) + max_window_rate = max_window_bytes * 8 / WINDOW_SECONDS + return { + "max_100ms_mbps": round(max_window_rate / 1_000_000, 3), + "steady_mbps": round(steady_rate / 1_000_000, 3), + } + + +def main(): + parser = argparse.ArgumentParser( + description="Validate Homa-over-UDP pacing from an ingress capture") + parser.add_argument("pcap") + args = parser.parse_args() + + samples = [] + for timestamp, frame in verify_udp_pcap.read_pcap(args.pcap): + parsed = verify_udp_pcap.parse_udp(frame) + if parsed is None: + continue + verify_udp_pcap.validate_homa_packet(parsed) + samples.append((timestamp, parsed["outer_length"] + 14)) + summary = rate_summary(samples) + if summary["max_100ms_mbps"] * 1_000_000 > MAX_RATE_BITS_PER_SECOND: + raise ValueError("100 ms traffic window exceeded 120 Mbit/s") + if summary["steady_mbps"] * 1_000_000 < MIN_RATE_BITS_PER_SECOND: + raise ValueError("steady traffic rate was below 70 Mbit/s") + summary["packet_count"] = len(samples) + print(json.dumps(summary, sort_keys=True)) + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError) as error: + print("pacing validation failed: %s" % error, file=sys.stderr) + sys.exit(1) diff --git a/test/integration/verify_udp_pcap.py b/test/integration/verify_udp_pcap.py new file mode 100644 index 00000000..5d971256 --- /dev/null +++ b/test/integration/verify_udp_pcap.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +# Validate captured Homa-over-UDP packets, including tunnel ports, +# lengths, checksums, and inner Homa headers. + +import argparse +from collections import Counter +import json +import socket +import struct +import sys + + +UDP_PORT = 54321 +HOMA_MIN_TYPE = 0x10 +HOMA_MAX_TYPE = 0x19 +HOMA_HEADER_LENGTHS = { + 0x10: 56, # DATA + 0x11: 33, # GRANT + 0x12: 37, # RESEND + 0x13: 28, # RPC_UNKNOWN + 0x14: 28, # BUSY + 0x15: 62, # CUTOFFS + 0x16: 28, # FREEZE + 0x17: 28, # NEED_ACK + 0x18: 80, # ACK + 0x19: 32, # START_MSG +} + + +def checksum_sum(data): + if len(data) % 2: + data += b"\0" + total = sum(struct.unpack("!%dH" % (len(data) // 2), data)) + while total >> 16: + total = (total & 0xFFFF) + (total >> 16) + return total + + +def checksum_valid(data): + return checksum_sum(data) == 0xFFFF + + +def ipv6_transport(packet, offset, packet_end): + next_header = packet[offset + 6] + cursor = offset + 40 + while next_header in (0, 43, 44, 51, 60): + if cursor + 2 > packet_end: + raise ValueError("truncated IPv6 extension header") + if next_header == 44: + raise ValueError("fragmented IPv6 packet") + elif next_header == 51: + header_length = (packet[cursor + 1] + 2) * 4 + else: + header_length = (packet[cursor + 1] + 1) * 8 + if cursor + header_length > packet_end: + raise ValueError("truncated IPv6 extension header") + next_header = packet[cursor] + cursor += header_length + return next_header, cursor + + +def parse_udp(frame): + if len(frame) < 14: + return None + ethertype = struct.unpack_from("!H", frame, 12)[0] + offset = 14 + while ethertype in (0x8100, 0x88A8): + ethertype = struct.unpack_from("!H", frame, offset + 2)[0] + offset += 4 + + if ethertype == 0x0800: + if len(frame) < offset + 20: + return None + header_length = (frame[offset] & 0x0F) * 4 + total_length = struct.unpack_from("!H", frame, offset + 2)[0] + fragment = struct.unpack_from("!H", frame, offset + 6)[0] + if header_length < 20 or total_length < header_length: + raise ValueError("invalid IPv4 length") + if fragment & 0x3FFF: + raise ValueError("fragmented IPv4 packet") + packet_end = offset + total_length + if len(frame) < packet_end: + raise ValueError("truncated IPv4 packet") + if frame[offset + 9] != socket.IPPROTO_UDP: + return None + source = frame[offset + 12:offset + 16] + destination = frame[offset + 16:offset + 20] + udp_offset = offset + header_length + family = "ipv4" + outer_length = total_length + elif ethertype == 0x86DD: + if len(frame) < offset + 40: + return None + payload_length = struct.unpack_from("!H", frame, offset + 4)[0] + packet_end = offset + 40 + payload_length + if len(frame) < packet_end: + raise ValueError("truncated IPv6 packet") + protocol, udp_offset = ipv6_transport(frame, offset, packet_end) + if protocol != socket.IPPROTO_UDP: + return None + source = frame[offset + 8:offset + 24] + destination = frame[offset + 24:offset + 40] + family = "ipv6" + outer_length = 40 + payload_length + else: + return None + + if len(frame) < udp_offset + 8: + raise ValueError("truncated UDP header") + source_port, destination_port, udp_length, checksum = struct.unpack_from( + "!HHHH", frame, udp_offset) + if udp_length < 8: + raise ValueError("invalid UDP length") + if udp_offset + udp_length != packet_end: + raise ValueError("UDP length does not match IP payload") + if len(frame) < udp_offset + udp_length: + raise ValueError("truncated UDP datagram") + datagram = frame[udp_offset:udp_offset + udp_length] + payload = datagram[8:] + if family == "ipv4": + pseudo_header = source + destination + struct.pack( + "!BBH", 0, socket.IPPROTO_UDP, udp_length) + else: + pseudo_header = source + destination + struct.pack( + "!I3xB", udp_length, socket.IPPROTO_UDP) + return { + "checksum": checksum, + "checksum_partial": checksum == checksum_sum(pseudo_header), + "checksum_valid": checksum_valid(pseudo_header + datagram), + "destination_port": destination_port, + "family": family, + "homa_offset": udp_offset + 8, + "outer_length": outer_length, + "payload": payload, + "source_port": source_port, + "udp_length": udp_length, + } + + +def validate_homa_packet(parsed, allow_partial_checksum=False): + if (parsed["source_port"] != UDP_PORT or + parsed["destination_port"] != UDP_PORT): + raise ValueError("UDP tunnel packet used an unexpected port") + payload = parsed["payload"] + if parsed["udp_length"] != len(payload) + 8: + raise ValueError("UDP length does not match captured payload") + if parsed["checksum"] == 0 and not ( + allow_partial_checksum and parsed["checksum_partial"]): + raise ValueError("UDP checksum is disabled") + if not parsed["checksum_valid"] and not ( + allow_partial_checksum and parsed["checksum_partial"]): + raise ValueError("UDP checksum is invalid") + if len(payload) < 28: + raise ValueError("UDP payload is shorter than a Homa common header") + packet_type = payload[11] + if not HOMA_MIN_TYPE <= packet_type <= HOMA_MAX_TYPE: + raise ValueError("UDP payload does not start with a Homa header") + header_length = HOMA_HEADER_LENGTHS[packet_type] + if len(payload) < header_length: + raise ValueError("Homa packet is shorter than its type-specific header") + if packet_type == 0x10: + message_length = struct.unpack_from("!I", payload, 28)[0] + segment_offset = struct.unpack_from("!I", payload, 52)[0] + segment_length = len(payload) - header_length + if segment_offset > message_length or ( + segment_length > message_length - segment_offset): + raise ValueError("Homa DATA segment exceeds message bounds") + return packet_type + + +def read_pcap(path): + with open(path, "rb") as capture: + header = capture.read(24) + if len(header) != 24: + raise ValueError("pcap header is missing or truncated") + magic = header[:4] + if magic == b"\xd4\xc3\xb2\xa1": + byte_order = "<" + timestamp_scale = 1_000_000 + elif magic == b"\x4d\x3c\xb2\xa1": + byte_order = "<" + timestamp_scale = 1_000_000_000 + elif magic == b"\xa1\xb2\xc3\xd4": + byte_order = ">" + timestamp_scale = 1_000_000 + elif magic == b"\xa1\xb2\x3c\x4d": + byte_order = ">" + timestamp_scale = 1_000_000_000 + else: + raise ValueError("unsupported pcap magic") + link_type = struct.unpack_from(byte_order + "I", header, 20)[0] + if link_type != 1: + raise ValueError("expected Ethernet pcap link type") + while True: + packet_header = capture.read(16) + if not packet_header: + return + if len(packet_header) != 16: + raise ValueError("truncated pcap packet header") + seconds, fraction, captured_length, _ = struct.unpack_from( + byte_order + "IIII", packet_header) + frame = capture.read(captured_length) + if len(frame) != captured_length: + raise ValueError("truncated pcap packet") + yield seconds + fraction / timestamp_scale, frame + + +def main(): + parser = argparse.ArgumentParser( + description="Validate Homa UDP tunnel packets in a classic pcap") + parser.add_argument("pcap") + parser.add_argument("--json", action="store_true", + help="write the validation summary as JSON") + parser.add_argument( + "--allow-partial-checksum", action="store_true", + help="accept a valid CHECKSUM_PARTIAL pseudo-header seed") + args = parser.parse_args() + + packet_count = 0 + checksum_ffff = 0 + partial_checksums = 0 + families = Counter() + homa_offsets = set() + max_outer_length = 0 + packet_types = Counter() + timestamps = [] + for timestamp, frame in read_pcap(args.pcap): + parsed = parse_udp(frame) + if parsed is None: + continue + packet_type = validate_homa_packet( + parsed, allow_partial_checksum=args.allow_partial_checksum) + + checksum_ffff += parsed["checksum"] == 0xFFFF + partial_checksums += (not parsed["checksum_valid"] and + parsed["checksum_partial"]) + families[parsed["family"]] += 1 + homa_offsets.add(parsed["homa_offset"]) + max_outer_length = max(max_outer_length, parsed["outer_length"]) + packet_types[packet_type] += 1 + timestamps.append(timestamp) + packet_count += 1 + + if packet_count == 0: + raise ValueError("capture contains no Homa UDP tunnel packets") + gaps = [later - earlier for earlier, later in zip(timestamps, timestamps[1:])] + summary = { + "checksum_ffff": checksum_ffff, + "families": dict(sorted(families.items())), + "homa_header_offsets": sorted(homa_offsets), + "max_outer_length": max_outer_length, + "max_timestamp_gap_us": round(max(gaps, default=0) * 1_000_000, 3), + "packet_count": packet_count, + "packet_types": { + "0x%02x" % packet_type: count + for packet_type, count in sorted(packet_types.items()) + }, + "partial_checksums": partial_checksums, + } + if args.json: + print(json.dumps(summary, sort_keys=True)) + else: + print("validated %d Homa UDP packets" % packet_count) + print("families: %s" % ", ".join( + "%s=%d" % item for item in sorted(families.items()))) + print("types: %s" % ", ".join( + "0x%02x=%d" % item for item in sorted(packet_types.items()))) + print("maximum outer packet length: %d" % max_outer_length) + print("wire checksums equal to 0xffff: %d" % checksum_ffff) + print("partial checksum seeds: %d" % partial_checksums) + print("Homa header offsets: %s" % ", ".join( + str(offset) for offset in sorted(homa_offsets))) + print("maximum timestamp gap: %.3f us" % summary["max_timestamp_gap_us"]) + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError, struct.error) as error: + print("pcap validation failed: %s" % error, file=sys.stderr) + sys.exit(1) \ No newline at end of file diff --git a/test/integration/verify_udp_retransmit.py b/test/integration/verify_udp_retransmit.py new file mode 100755 index 00000000..94e9744b --- /dev/null +++ b/test/integration/verify_udp_retransmit.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# Verify repeated Homa DATA identities and required control packet classes +# in a UDP tunnel retransmission capture. + +import argparse +from collections import Counter +import json +import struct +import sys + +import verify_udp_pcap + + +DATA = 0x10 +RESEND = 0x12 +RPC_UNKNOWN = 0x13 +ACK = 0x18 + + +def data_identity(parsed): + payload = parsed["payload"] + if len(payload) < verify_udp_pcap.HOMA_HEADER_LENGTHS[DATA] or \ + payload[11] != DATA: + return None + sender_id = struct.unpack_from("!Q", payload, 20)[0] + segment_offset = struct.unpack_from("!I", payload, 52)[0] + return sender_id, segment_offset + + +def summarize(parsed_packets): + packet_types = Counter() + retransmitted_segments = Counter() + resend_requests = [] + for parsed in parsed_packets: + packet_type = verify_udp_pcap.validate_homa_packet(parsed) + packet_types[packet_type] += 1 + identity = data_identity(parsed) + if identity is not None and parsed["payload"][48] != 0: + retransmitted_segments[identity] += 1 + if packet_type == RESEND: + payload = parsed["payload"] + sender_id = struct.unpack_from("!Q", payload, 20)[0] + offset, length = struct.unpack_from("!II", payload, 28) + resend_requests.append((sender_id, offset, length)) + retransmissions = { + "%d:%d" % identity: count + for identity, count in sorted(retransmitted_segments.items()) + } + return { + "packet_types": { + "0x%02x" % packet_type: count + for packet_type, count in sorted(packet_types.items()) + }, + "resend_requests": [ + "%d:%d:%d" % request for request in resend_requests + ], + "retransmitted_segments": retransmissions, + } + + +def has_matching_resend(summary): + for identity in summary["retransmitted_segments"]: + sender_id, segment_offset = (int(value) + for value in identity.split(":")) + for request in summary["resend_requests"]: + resend_id, offset, length = (int(value) + for value in request.split(":")) + if resend_id != sender_id ^ 1: + continue + if length == 0xFFFFFFFF or ( + offset <= segment_offset < offset + length): + return True + return False + + +def main(): + parser = argparse.ArgumentParser( + description="Verify deterministic Homa-over-UDP retransmission") + parser.add_argument("pcap") + args = parser.parse_args() + + packets = [] + for _, frame in verify_udp_pcap.read_pcap(args.pcap): + parsed = verify_udp_pcap.parse_udp(frame) + if parsed is not None: + packets.append(parsed) + summary = summarize(packets) + if not summary["retransmitted_segments"]: + raise ValueError("capture contains no retransmit-marked DATA segment") + if not has_matching_resend(summary): + raise ValueError("no RESEND request matches a retransmitted DATA segment") + for packet_type, name in ((RPC_UNKNOWN, "RPC_UNKNOWN"), (ACK, "ACK")): + if summary["packet_types"].get("0x%02x" % packet_type, 0) == 0: + raise ValueError("capture contains no %s packet" % name) + print(json.dumps(summary, sort_keys=True)) + + +if __name__ == "__main__": + try: + main() + except (OSError, ValueError, struct.error) as error: + print("retransmission validation failed: %s" % error, + file=sys.stderr) + sys.exit(1) diff --git a/test/main.c b/test/main.c index 9c95ea58..e7f83eef 100644 --- a/test/main.c +++ b/test/main.c @@ -45,5 +45,6 @@ int main(int argc, char **argv) } else break; } - test_harness_run(argc-i, argv+i, verbose); + mock_ipv6 = mock_ipv6_default; + return test_harness_run(argc-i, argv+i, verbose); } diff --git a/test/mock.c b/test/mock.c index 50f269f5..eb470f6f 100644 --- a/test/mock.c +++ b/test/mock.c @@ -9,6 +9,7 @@ #include "homa_pool.h" #include "homa_tx_pool.h" #ifndef __STRIP__ /* See strip.py */ +#include "homa_hijack.h" #include "homa_qdisc.h" #endif /* See strip.py */ #include "ccutils.h" @@ -53,10 +54,14 @@ int mock_prepare_to_wait_errors; int mock_register_protosw_errors; int mock_register_qdisc_errors; int mock_register_sysctl_errors; +size_t mock_register_sysctl_size; int mock_rht_init_errors; int mock_rht_insert_errors; int mock_route_errors; int mock_trylock_errors; +int mock_udp_sock_create_errors; +int mock_udp_tunnel_release_count; +struct udp_tunnel_sock_cfg mock_udp_tunnel_cfg; int mock_vmalloc_errors; int mock_wait_intr_irq_errors; @@ -81,6 +86,11 @@ int mock_xmit_log_verbose; */ int mock_xmit_log_hijack; +/* If a test sets this variable to nonzero, ip*xmit will log the outer + * UDP header of outgoing UDP-hijacked packets. + */ +int mock_xmit_log_udp_hijack; + /* If a test sets this variable to nonzero, calls to wake_up and * wake_up_all will be logged. */ @@ -346,6 +356,14 @@ kmem_buckets kmalloc_caches[NR_KMALLOC_TYPES]; #endif int __preempt_count; int cpu_number = 1; +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0) +/* Storage for the kernel's per-cpu "hot" fields (task, preempt count, + * cpu number, etc.); real per-cpu semantics don't matter for unit tests, + * which are single-threaded, but the symbol must exist to satisfy the + * linker for inline kernel functions that reference it directly. + */ +struct pcpu_hot pcpu_hot; +#endif char sock_flow_table[RPS_SOCK_FLOW_TABLE_SIZE(1024)]; struct net_hotdata net_hotdata = { .rps_cpu_mask = 0x1f, @@ -880,11 +898,28 @@ int ip6_xmit(const struct sock *sk, struct sk_buff *skb, struct flowi6 *fl6, unit_log_printf("; ", "hijack checksum %d, flags 0x%x", h->checksum, h->flags); } + if (mock_xmit_log_udp_hijack) { + struct udphdr *uh; + + uh = (struct udphdr *)skb_transport_header(skb); + unit_log_printf("; ", + "udp hijack sport %d, dport %d, len %d, checksum %d", + ntohs(uh->source), ntohs(uh->dest), + ntohs(uh->len), uh->check); + } #endif /* See strip.py */ kfree_skb(skb); return 0; } +#ifndef __STRIP__ /* See strip.py */ +void ip6_update_pmtu(struct sk_buff *skb, struct net *net, __be32 mtu, + int oif, u32 mark, kuid_t uid) +{ + unit_log_printf("; ", "ip6_update_pmtu mtu %d", ntohl(mtu)); +} +#endif /* See strip.py */ + int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl) { const char *prefix = " "; @@ -919,6 +954,15 @@ int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl) unit_log_printf("; ", "hijack checksum %d, flags 0x%x", h->checksum, h->flags); } + if (mock_xmit_log_udp_hijack) { + struct udphdr *uh; + + uh = (struct udphdr *)skb_transport_header(skb); + unit_log_printf("; ", + "udp hijack sport %d, dport %d, len %d, checksum %d", + ntohs(uh->source), ntohs(uh->dest), + ntohs(uh->len), uh->check); + } #endif /* See strip.py */ kfree_skb(skb); return 0; @@ -936,6 +980,14 @@ unsigned int ipv4_mtu(const struct dst_entry *dst) return mock_mtu; } +#ifndef __STRIP__ /* See strip.py */ +void ipv4_update_pmtu(struct sk_buff *skb, struct net *net, u32 mtu, + int oif, u8 protocol) +{ + unit_log_printf("; ", "ipv4_update_pmtu mtu %d", mtu); +} +#endif /* See strip.py */ + struct rtable *ip_route_output_flow(struct net *net, struct flowi4 *flp4, const struct sock *sk) { @@ -1117,6 +1169,15 @@ void *__kmalloc_noprof(size_t size, gfp_t flags) return mock_kmalloc(size, flags); } +void *kmemdup_noprof(const void *src, size_t len, gfp_t flags) +{ + void *block = mock_kmalloc(len, flags); + + if (block) + memcpy(block, src, len); + return block; +} + void kvfree(const void *addr) { kfree(addr); @@ -1148,27 +1209,17 @@ int kthread_stop(struct task_struct *k) return 0; } -#ifdef CONFIG_DEBUG_LIST -bool __list_add_valid(struct list_head *new, struct list_head *prev, - struct list_head *next) -{ - return true; -} -#endif - +/* __list_add_valid/__list_del_entry_valid are now provided by the + * kernel's own linux/list.h (unconditionally, regardless of + * CONFIG_DEBUG_LIST) - only the _or_report reporting hooks still need + * a definition here. + */ bool __list_add_valid_or_report(struct list_head *new, struct list_head *prev, struct list_head *next) { return true; } -#ifdef CONFIG_DEBUG_LIST -bool __list_del_entry_valid(struct list_head *entry) -{ - return true; -} -#endif - bool __list_del_entry_valid_or_report(struct list_head *entry) { return true; @@ -1202,7 +1253,7 @@ void lock_sock_nested(struct sock *sk, int subclass) sk->sk_lock.owned = 1; } -ssize_t __modver_version_show(const struct module_attribute *a, +ssize_t __modver_version_show(struct module_attribute *a, struct module_kobject *b, char *c) { return 0; @@ -1245,20 +1296,21 @@ int netif_receive_skb(struct sk_buff *skb) void __netif_schedule(struct Qdisc *q) {} -void preempt_count_add(int val) +void mock_preempt_count_add(int val) { - int i; - - for (i = 0; i < val; i++) - preempt_disable(); + /* Just adjust the mock backing-store counter directly; don't + * route through mock_preempt_disable(), since preempt_count_add() + * is used by callers (e.g. local_bh_disable()) that don't pair up + * 1-for-1 with preempt_disable()/preempt_enable(), so looping + * through the disable/enable leak-detector here would produce + * false "preempt_disables still active" failures. + */ + __preempt_count += val; } -void preempt_count_sub(int val) +void mock_preempt_count_sub(int val) { - int i; - - for (i = 0; i < val; i++) - preempt_enable(); + __preempt_count -= val; } long prepare_to_wait_event(struct wait_queue_head *wq_head, @@ -1328,6 +1380,22 @@ int proc_dointvec(const struct ctl_table *table, int write, return 0; } +/* Backs the SYSCTL_ZERO/SYSCTL_ONE/etc. macros in linux/sysctl.h (the real + * array lives in kernel/sysctl.c, which isn't linked into the unit tests). + */ +const int sysctl_vals[] = {0, 1, 2, 3, 4, 100, 200, 1000, 3000, INT_MAX}; + +#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 12, 0) +int proc_dointvec_minmax(struct ctl_table *table, int write, + void __user *buffer, size_t *lenp, loff_t *ppos) +#else +int proc_dointvec_minmax(const struct ctl_table *table, int write, + void __user *buffer, size_t *lenp, loff_t *ppos) +#endif +{ + return 0; +} + void proc_remove(struct proc_dir_entry *de) { if (!de) @@ -1354,7 +1422,40 @@ void proto_unregister(struct proto *prot) void *__pskb_pull_tail(struct sk_buff *skb, int delta) { - return NULL; + struct skb_shared_info *shinfo = skb_shinfo(skb); + unsigned char *destination; + int copied = 0; + + if (delta < 0 || delta > skb->data_len || + delta > skb_end_pointer(skb) - skb_tail_pointer(skb)) + return NULL; + destination = skb_put(skb, delta); + skb->len -= delta; + while (copied < delta) { + skb_frag_t *frag; + struct page *page; + int bytes; + + if (shinfo->nr_frags == 0) + return NULL; + frag = &shinfo->frags[0]; + bytes = min(delta - copied, (int)skb_frag_size(frag)); + memcpy(destination + copied, + page_address(skb_frag_page(frag)) + skb_frag_off(frag), + bytes); + copied += bytes; + frag->offset += bytes; + skb_frag_size_set(frag, skb_frag_size(frag) - bytes); + if (skb_frag_size(frag) != 0) + continue; + page = skb_frag_page(frag); + shinfo->nr_frags--; + memmove(frag, frag + 1, + shinfo->nr_frags * sizeof(shinfo->frags[0])); + put_page(page); + } + skb->data_len -= delta; + return skb_tail_pointer(skb); } bool queue_work_on(int cpu, struct workqueue_struct *wq, @@ -1479,9 +1580,9 @@ bool rcuref_get_slowpath(rcuref_t *ref) return true; } -bool rcuref_put_slowpath(rcuref_t *ref, unsigned int cnt) +bool rcuref_put_slowpath(rcuref_t *ref) { - return cnt == RCUREF_NOREF; + return true; } void refcount_warn_saturate(refcount_t *r, enum refcount_saturation_type t) {} @@ -2265,6 +2366,7 @@ struct sk_buff *mock_raw_skb(struct in6_addr *saddr, struct in6_addr *daddr, skb->protocol = htons(ETH_P_IPV6); } else { ip_hdr(skb)->version = 4; + ip_hdr(skb)->ihl = 5; ip_hdr(skb)->saddr = saddr->in6_u.u6_addr32[3]; ip_hdr(skb)->daddr = daddr->in6_u.u6_addr32[3]; ip_hdr(skb)->protocol = protocol; @@ -2383,6 +2485,120 @@ struct ctl_table_header *mock_register_net_sysctl(struct net *net, return (struct ctl_table_header *)11111; } +/** + * mock_register_net_sysctl_sz() - Called instead of register_net_sysctl_sz + * when Homa is compiled for unit testing. + */ +struct ctl_table_header *mock_register_net_sysctl_sz(struct net *net, + const char *path, struct ctl_table *table, size_t table_size) +{ + mock_register_sysctl_size = table_size; + return mock_register_net_sysctl(net, path, table); +} + +/** + * mock_schedule_work() - Called instead of schedule_work when Homa is + * compiled for unit testing. Runs the work function immediately and + * synchronously, since there is no real kernel workqueue thread in the + * unit test harness. + * @work: Work item to run. + * Return: Always true (the work "was scheduled"). + */ +bool mock_schedule_work(struct work_struct *work) +{ + work->func(work); + return true; +} + +#ifndef __STRIP__ /* See strip.py */ +/** + * mock_setup_udp_tunnel_sock() - Called instead of setup_udp_tunnel_sock + * when Homa is compiled for unit testing. Just records the requested + * configuration in the (fake) socket, and in mock_udp_tunnel_cfg, so + * tests can inspect it (and invoke the registered encap_rcv/ + * encap_err_lookup/encap_err_rcv callbacks directly). + * @net: Unused. + * @sock: Tunnel socket previously returned by mock_udp_sock_create4() + * or mock_udp_sock_create6(). + * @cfg: Configuration to associate with @sock. + */ +void mock_setup_udp_tunnel_sock(struct net *net, struct socket *sock, + struct udp_tunnel_sock_cfg *cfg) +{ + sock->sk->sk_user_data = cfg->sk_user_data; + mock_udp_tunnel_cfg = *cfg; +} + +/** + * mock_udp_sock_create4() - Called instead of udp_sock_create4 when Homa + * is compiled for unit testing. Allocates a fake socket/sock pair instead + * of creating a real kernel UDP socket. + * @net: Unused. + * @cfg: Configuration for the new socket; @cfg->local_udp_port is + * recorded in the fake sock's sk_num field. + * @sockp: Modified to point at the new fake socket. + * Return: 0 on success, or -EADDRINUSE if + * mock_udp_sock_create_errors indicates this call should fail. + */ +int mock_udp_sock_create4(struct net *net, struct udp_port_cfg *cfg, + struct socket **sockp) +{ + struct socket *sock; + + if (mock_check_error(&mock_udp_sock_create_errors)) + return -EADDRINUSE; + sock = malloc(sizeof(*sock)); + memset(sock, 0, sizeof(*sock)); + sock->sk = malloc(sizeof(*sock->sk)); + memset(sock->sk, 0, sizeof(*sock->sk)); + sock->sk->sk_family = AF_INET; + sock->sk->sk_num = ntohs(cfg->local_udp_port); + *sockp = sock; + return 0; +} + +/** + * mock_udp_sock_create6() - Called instead of udp_sock_create6 when Homa + * is compiled for unit testing. See mock_udp_sock_create4(). + * @net: Unused. + * @cfg: Configuration for the new socket. + * @sockp: Modified to point at the new fake socket. + * Return: 0 on success, or -EADDRINUSE if + * mock_udp_sock_create_errors indicates this call should fail. + */ +int mock_udp_sock_create6(struct net *net, struct udp_port_cfg *cfg, + struct socket **sockp) +{ + struct socket *sock; + + if (mock_check_error(&mock_udp_sock_create_errors)) + return -EADDRINUSE; + sock = malloc(sizeof(*sock)); + memset(sock, 0, sizeof(*sock)); + sock->sk = malloc(sizeof(*sock->sk)); + memset(sock->sk, 0, sizeof(*sock->sk)); + sock->sk->sk_family = AF_INET6; + sock->sk->sk_num = ntohs(cfg->local_udp_port); + *sockp = sock; + return 0; +} + +/** + * mock_udp_tunnel_sock_release() - Called instead of + * udp_tunnel_sock_release when Homa is compiled for unit testing. Frees + * the fake socket/sock pair allocated by mock_udp_sock_create4()/6(). + * @sock: Fake socket to release. + */ +void mock_udp_tunnel_sock_release(struct socket *sock) +{ + mock_udp_tunnel_release_count++; + UNIT_HOOK("udp_tunnel_release"); + sock->sk->sk_user_data = NULL; + free(sock->sk); + free(sock); +} +#endif /* See strip.py */ + int mock_rht_init(struct rhashtable *ht, const struct rhashtable_params *params) { @@ -2761,8 +2977,12 @@ void mock_teardown(void) mock_register_protosw_errors = 0; mock_register_qdisc_errors = 0; mock_register_sysctl_errors = 0; + mock_register_sysctl_size = 0; mock_rht_init_errors = 0; mock_rht_insert_errors = 0; + mock_udp_sock_create_errors = 0; + mock_udp_tunnel_release_count = 0; + memset(&mock_udp_tunnel_cfg, 0, sizeof(mock_udp_tunnel_cfg)); mock_wait_intr_irq_errors = 0; mock_copy_from_iter_no_log = false; mock_copy_to_user_dont_copy = 0; @@ -2780,6 +3000,7 @@ void mock_teardown(void) mock_signal_pending = 0; mock_xmit_log_verbose = 0; mock_xmit_log_hijack = 0; + mock_xmit_log_udp_hijack = 0; mock_log_wakeups = 0; mock_mtu = 0; mock_max_skb_frags = 10; @@ -2793,6 +3014,9 @@ void mock_teardown(void) homa_net_id = 0; for (i = 0; i < MOCK_MAX_NETS; i++) { if (mock_hnets[i]) { +#ifndef __STRIP__ /* See strip.py */ + homa_hijack_udp_net_destroy(mock_hnets[i]); +#endif /* See strip.py */ free(mock_hnets[i]); mock_hnets[i] = NULL; } diff --git a/test/mock.h b/test/mock.h index f7c7e524..6af72a22 100644 --- a/test/mock.h +++ b/test/mock.h @@ -4,8 +4,108 @@ #ifndef _HOMA_MOCK_H #define _HOMA_MOCK_H +/* This file must be the very first #include for every compiled .c file + * (forced via -include mock.h in the test Makefile, before homa_impl.h and + * before any real kernel header). -> + * and -> , dragged in transitively by many + * kernel headers homa_impl.h includes (skbuff.h, kthread.h, completion.h, + * sched/signal.h, proc_fs.h, etc.), use preempt_disable()/preempt_enable()/ + * smp_processor_id()/raw_smp_processor_id()/WARN_ON_ONCE() in inline + * functions; those inline functions bake in whichever macro definition is + * active when the header is first parsed. Getting the mocked versions + * active before any of that runs requires mock.h itself to be included + * first, standalone. homa_impl.h's own later #include "mock.h" is then a + * no-op (include guard), but these overrides are already in effect. + */ +#include + +#undef WARN +#define WARN(...) + +#undef WARN_ON +#define WARN_ON(condition) ({ \ + int __ret_warn_on = !!(condition); \ + unlikely(__ret_warn_on); \ +}) + +#undef WARN_ON_ONCE +#define WARN_ON_ONCE(condition) WARN_ON(condition) + +#undef WARN_ONCE +#define WARN_ONCE(cond, ...) ({ bool __c = (cond); (void)__c; __c; }) + +/* Pulling in the real / here first (before + * the undef/define below) sets their include guards so later transitive + * re-inclusion is a no-op and can't clobber these overrides. This must + * come after the WARN overrides above, since preempt.h drags in + * (via linkage.h), whose inline functions use + * WARN_ON_ONCE() and must see the mocked (no-op) version. + */ +#include +#include + +/* Forward declarations needed because the overrides below are used by + * headers included further down in this file, before mock.c's own + * declarations (later in this file) would otherwise be visible. + */ +void mock_preempt_disable(void); +void mock_preempt_enable(void); +int mock_processor_id(void); + +#undef preempt_disable +#define preempt_disable() mock_preempt_disable() + +#undef preempt_enable +#define preempt_enable() mock_preempt_enable() + +#undef smp_processor_id +#define smp_processor_id() mock_processor_id() + +#undef raw_smp_processor_id +#define raw_smp_processor_id() mock_processor_id() + +/* (tcp_v4_check/tcp_v6_check), (this_cpu_ptr, + * via ), (icmp_send), + * (rt6_get_cookie, via ) and + * (net_generic) all declare real inline functions/objects whose names this + * file redirects below via object-style macros; those macros corrupt the + * real declarations if the real headers are parsed afterward (their + * include guards would otherwise make a later re-inclusion elsewhere a + * silent no-op with the corrupted macro baked in). Pulling them in here + * first, before any of the redirects below, avoids that. homa.h is + * included here too so the HOMA_BPAGE_SIZE/HOMA_MIN_DEFAULT_PORT/etc. + * overrides below apply after the real values are already defined, instead + * of being silently clobbered by a later #include of homa.h from + * homa_impl.h. + */ +#include +#include +#include +#include +#include +#include +#include "homa.h" +#include "homa_wire.h" + #include +/* net/udp_tunnel.h's udp_sock_create() is a static inline that calls + * udp_sock_create4()/udp_sock_create6() directly, so those two must be + * mocked (and prototyped, since struct udp_port_cfg isn't defined until + * the include below) before udp_tunnel.h is parsed - otherwise the inline + * body bakes in calls to the real, unlinked kernel functions. + */ +struct udp_port_cfg; +int mock_udp_sock_create4(struct net *net, struct udp_port_cfg *cfg, + struct socket **sockp); +int mock_udp_sock_create6(struct net *net, struct udp_port_cfg *cfg, + struct socket **sockp); +#undef udp_sock_create4 +#define udp_sock_create4 mock_udp_sock_create4 +#undef udp_sock_create6 +#define udp_sock_create6 mock_udp_sock_create6 +#include + /* Replace various Linux variables and functions with mocked ones. */ #undef alloc_pages #define alloc_pages mock_alloc_pages @@ -104,6 +204,12 @@ #undef preempt_enable #define preempt_enable() mock_preempt_enable() +#undef preempt_count_add +#define preempt_count_add(val) mock_preempt_count_add(val) + +#undef preempt_count_sub +#define preempt_count_sub(val) mock_preempt_count_sub(val) + #define put_page mock_put_page #define rcu_read_lock mock_rcu_read_lock @@ -119,11 +225,22 @@ #undef register_net_sysctl #define register_net_sysctl mock_register_net_sysctl +#undef register_net_sysctl_sz +#define register_net_sysctl_sz mock_register_net_sysctl_sz + +#define rt6_get_cookie(...) 999 + +#undef schedule_work +#define schedule_work(work) mock_schedule_work(work) + #define signal_pending(...) mock_signal_pending #undef set_active_memcg #define set_active_memcg mock_set_active_memcg +#undef setup_udp_tunnel_sock +#define setup_udp_tunnel_sock mock_setup_udp_tunnel_sock + /* Must redefine skb_frag_foreach_page because page pointers are different * when unit testing (a page point points to an actual page, rather than * a descriptor) @@ -138,9 +255,6 @@ copied += p_len, p++, p_off = 0, \ p_len = f_len - copied) \ -#undef smp_processor_id -#define smp_processor_id() mock_processor_id() - #define sock_hold(sock) mock_sock_hold(sock) #define sock_put(sock) mock_sock_put(sock) @@ -153,12 +267,35 @@ #undef tcp_v6_check #define tcp_v6_check(...) (~(__force __sum16)666U) +/* udp_set_csum()/udp6_set_csum() perform real checksum computation using + * kernel internals (skb_is_gso, skb_dst, NETIF_F_IP_CSUM, ...) that aren't + * available in the unit test environment, so (like tcp_v4_check/ + * tcp_v6_check above) they are replaced here with simple stand-ins that + * just store a fixed, distinguishable value in the packet's checksum + * field. + */ +#undef udp_set_csum +#define udp_set_csum(nocheck, skb, saddr, daddr, len) \ + (udp_hdr(skb)->check = (__force __sum16)555U) + +#undef udp6_set_csum +#define udp6_set_csum(nocheck, skb, saddr, daddr, len) \ + (udp_hdr(skb)->check = (__force __sum16)777U) + #undef this_cpu_ptr #define this_cpu_ptr(name) (&name[cpu_number]) #undef __this_cpu_read #define __this_cpu_read(name) (name) +#undef udp_tunnel_sock_release +#define udp_tunnel_sock_release mock_udp_tunnel_sock_release + +/* udp_sock_create4/6 are mocked earlier, right before the + * #include near the top of this file - see the + * comment there. + */ + #undef vmalloc #define vmalloc mock_vmalloc @@ -185,6 +322,7 @@ extern int mock_copy_to_frags_errors; extern int mock_copy_to_user_dont_copy; extern int mock_copy_to_user_errors; extern int mock_cpu_idle; +extern int cpu_number; extern struct net_device mock_devices[]; extern enum skb_drop_reason @@ -223,6 +361,7 @@ extern int mock_queue_index; extern int mock_register_protosw_errors; extern int mock_register_qdisc_errors; extern int mock_register_sysctl_errors; +extern size_t mock_register_sysctl_size; extern int mock_rht_init_errors; extern int mock_rht_insert_errors; extern void **mock_rht_walk_results; @@ -235,14 +374,22 @@ extern struct task_struct extern int mock_total_spin_locks; extern int mock_trylock_errors; extern u64 mock_tt_cycles; +extern int mock_udp_sock_create_errors; +extern int mock_udp_tunnel_release_count; +extern struct udp_tunnel_sock_cfg + mock_udp_tunnel_cfg; extern int mock_vmalloc_errors; extern int mock_wait_intr_irq_errors; extern int mock_xmit_log_verbose; extern int mock_xmit_log_hijack; +extern int mock_xmit_log_udp_hijack; extern char mock_xmit_prios[]; extern struct task_struct *current_task; +void hrtimer_setup(struct hrtimer *timer, + enum hrtimer_restart (*function)(struct hrtimer *), + clockid_t clock_id, enum hrtimer_mode mode); struct page * mock_alloc_pages(gfp_t gfp, unsigned order); struct Qdisc @@ -278,6 +425,8 @@ int mock_page_refs(struct page *page); int mock_page_to_nid(struct page *page); void mock_preempt_disable(void); void mock_preempt_enable(void); +void mock_preempt_count_add(int val); +void mock_preempt_count_sub(int val); int mock_processor_id(void); void mock_put_page(struct page *page); struct sk_buff * @@ -293,6 +442,11 @@ struct ctl_table_header * mock_register_net_sysctl(struct net *net, const char *path, struct ctl_table *table); +struct ctl_table_header * + mock_register_net_sysctl_sz(struct net *net, + const char *path, + struct ctl_table *table, + size_t table_size); int mock_rht_init(struct rhashtable *ht, const struct rhashtable_params *params); void *mock_rht_lookup_get_insert_fast(struct rhashtable *ht, @@ -307,6 +461,7 @@ int mock_rht_lookup_insert_fast(struct rhashtable *ht, void *mock_rht_walk_next(struct rhashtable_iter *iter); void mock_rpc_hold(struct homa_rpc *rpc); void mock_rpc_put(struct homa_rpc *rpc); +bool mock_schedule_work(struct work_struct *work); struct mem_cgroup * mock_set_active_memcg(struct mem_cgroup *memcg); void mock_set_clock_vals(u64 t, ...); @@ -325,10 +480,17 @@ int mock_sock_init(struct homa_sock *hsk, struct homa_net *hnet, void mock_sock_put(struct sock *sk); void mock_spin_lock(spinlock_t *lock); void mock_spin_unlock(spinlock_t *lock); +void mock_setup_udp_tunnel_sock(struct net *net, struct socket *sock, + struct udp_tunnel_sock_cfg *cfg); struct sk_buff * mock_tcp_skb(struct in6_addr *saddr, struct in6_addr *daddr, int sequence, int extra_bytes); void mock_teardown(void); +int mock_udp_sock_create4(struct net *net, struct udp_port_cfg *cfg, + struct socket **sockp); +int mock_udp_sock_create6(struct net *net, struct udp_port_cfg *cfg, + struct socket **sockp); +void mock_udp_tunnel_sock_release(struct socket *sock); void *mock_vmalloc(size_t size); #endif /* _HOMA_MOCK_H */ diff --git a/test/unit_homa_hijack.c b/test/unit_homa_hijack.c index 0df6103a..a2054d1f 100644 --- a/test/unit_homa_hijack.c +++ b/test/unit_homa_hijack.c @@ -26,6 +26,48 @@ static struct sk_buff *unit_tcp6_gro_receive(struct list_head *held_list, return NULL; } +static struct homa_net *udp_race_hnet; +static struct net *udp_race_net; +static struct homa_rpc *udp_race_rpc; +static int udp_race_admit_result; +static struct sk_buff *udp_release_skb; +static struct homa_common_hdr *udp_release_hdr; +static int udp_release_callback_count; + +static void udp_disable_during_admit_hook(char *id) +{ + if (strcmp(id, "udp_admit_after_increment") != 0) + return; + homa_hijack_udp_set_enabled(udp_race_hnet, udp_race_net, 0); +} + +static void udp_admit_during_disable_hook(char *id) +{ + if (strcmp(id, "udp_disable_after_state") != 0) + return; + udp_race_admit_result = homa_hijack_udp_admit(udp_race_rpc); +} + +static void udp_callback_during_release_hook(char *id) +{ + struct sock *sk; + + if (strcmp(id, "udp_tunnel_release") != 0 || + udp_release_callback_count != 0) + return; + sk = udp_race_hnet->udp_tun4->sk; + if (rcu_dereference_sk_user_data(sk) != udp_race_hnet) { + udp_release_callback_count = -1; + return; + } + rcu_read_lock(); + mock_udp_tunnel_cfg.encap_err_rcv(sk, udp_release_skb, + EHOSTUNREACH, htons(HOMA_UDP_HIJACK_PORT), 0, + (u8 *)udp_release_hdr); + rcu_read_unlock(); + udp_release_callback_count = 1; +} + FIXTURE(homa_hijack) { struct homa homa; @@ -230,4 +272,813 @@ TEST_F(homa_hijack, homa_skb_hijacked) homa_route_release(route); kfree_skb(skb); -} \ No newline at end of file +} + +TEST_F(homa_hijack, homa_skb_inner_hdr__native) +{ + struct homa_common_hdr *h; + struct sk_buff *skb; + + skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, + &self->header.common, 1400, 0); + h = homa_skb_inner_hdr(skb); + ASSERT_NE(NULL, h); + EXPECT_EQ((unsigned char *)h, skb_transport_header(skb)); + EXPECT_EQ(self->header.common.sender_id, h->sender_id); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_skb_inner_hdr__tcp_hijacked) +{ + struct homa_route *route = homa_route_get(&self->hsk, &self->src_ip); + struct homa_common_hdr *h; + struct sk_buff *skb; + + mock_ipv6 = false; + skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, + &self->header.common, 1400, 0); + homa_hijack_set_hdr(skb, route, false); + ip_hdr(skb)->protocol = IPPROTO_TCP; + h = homa_skb_inner_hdr(skb); + ASSERT_NE(NULL, h); + EXPECT_EQ((unsigned char *)h, skb_transport_header(skb)); + EXPECT_EQ(self->header.common.sender_id, h->sender_id); + + homa_route_release(route); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_skb_inner_hdr__not_hijacked_tcp) +{ + struct sk_buff *skb; + + /* Real (non-hijacked) TCP packets must not be mistaken for Homa + * packets, even if the IP protocol matches. + */ + mock_ipv6 = false; + skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, + &self->header.common, 1400, 0); + ip_hdr(skb)->protocol = IPPROTO_TCP; + EXPECT_EQ(NULL, homa_skb_inner_hdr(skb)); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_skb_inner_hdr__unknown_eth_type) +{ + struct sk_buff *skb; + + skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, + &self->header.common, 1400, 0); + skb->protocol = htons(ETH_P_ARP); + EXPECT_EQ(NULL, homa_skb_inner_hdr(skb)); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_skb_inner_hdr__invalid_type) +{ + struct homa_common_hdr *h; + struct sk_buff *skb; + + skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, + &self->header.common, 1400, 0); + h = (struct homa_common_hdr *)skb_transport_header(skb); + h->type = MAX_OP + 1; + EXPECT_EQ(NULL, homa_skb_inner_hdr(skb)); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_skb_inner_hdr__too_short) +{ + struct sk_buff *skb; + + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_HOMA, + sizeof(struct homa_common_hdr) - 1); + skb_put(skb, sizeof(struct homa_common_hdr) - 1); + EXPECT_EQ(NULL, homa_skb_inner_hdr(skb)); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_skb_inner_hdr__udp_header_in_nonlinear_data) +{ + struct skb_shared_info *shinfo; + struct homa_common_hdr *h; + struct sk_buff *skb; + struct udphdr *uh; + + mock_ipv6 = false; + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, + sizeof(*uh) + sizeof(self->header.common)); + uh = skb_put(skb, sizeof(*uh)); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT); + shinfo = skb_shinfo(skb); + unit_alloc_frags(1, shinfo->frags, 0, + sizeof(self->header.common)); + memcpy(unit_frag_first_byte(&shinfo->frags[0]), &self->header.common, + sizeof(self->header.common)); + shinfo->nr_frags = 1; + skb->data_len = sizeof(self->header.common); + skb->len += skb->data_len; + + h = homa_skb_inner_hdr(skb); + ASSERT_NE(NULL, h); + EXPECT_EQ(self->header.common.sender_id, h->sender_id); + EXPECT_EQ(0, skb->data_len); + EXPECT_EQ(0, shinfo->nr_frags); + kfree_skb(skb); +} + +/* Tests for UDP hijacking (homa_hijack.c/h). */ +#ifndef __STRIP__ /* See strip.py */ + +TEST_F(homa_hijack, homa_sock_udp_hijacked) +{ + EXPECT_EQ(0, homa_sock_udp_hijacked(&self->hsk)); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + EXPECT_EQ(1, homa_sock_udp_hijacked(&self->hsk)); + self->hsk.sock.sk_protocol = IPPROTO_HOMA; +} + +TEST_F(homa_hijack, homa_hijack_udp_sock_select__tcp_hijack_wins) +{ + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->homa.hijack_tcp = 1; + homa_hijack_sock_init(&self->hsk); + homa_hijack_udp_sock_select(&self->hsk); + homa_hijack_udp_unlock(self->hnet); + EXPECT_EQ(IPPROTO_TCP, self->hsk.sock.sk_protocol); +} + +TEST_F(homa_hijack, homa_hijack_udp_sock_select__enabled) +{ + self->hnet->udp_state = HOMA_UDP_ENABLED; + homa_hijack_udp_sock_select(&self->hsk); + homa_hijack_udp_unlock(self->hnet); + EXPECT_EQ(IPPROTO_UDP, self->hsk.sock.sk_protocol); +} + +TEST_F(homa_hijack, homa_hijack_udp_sock_select__disabled) +{ + homa_hijack_udp_sock_select(&self->hsk); + homa_hijack_udp_unlock(self->hnet); + EXPECT_EQ(IPPROTO_HOMA, self->hsk.sock.sk_protocol); +} + +TEST_F(homa_hijack, homa_hijack_udp_net_init) +{ + struct homa_net hnet2; + + memset(&hnet2, 0xab, sizeof(hnet2)); + homa_hijack_udp_net_init(&hnet2); + EXPECT_EQ(HOMA_UDP_DISABLED, hnet2.udp_state); + EXPECT_EQ(NULL, hnet2.udp_tun4); + EXPECT_EQ(NULL, hnet2.udp_tun6); + EXPECT_EQ(0, atomic_read(&hnet2.udp_rpc_count)); + EXPECT_EQ(NULL, hnet2.udp_ctl_table); + EXPECT_EQ(NULL, hnet2.udp_ctl_header); +} + +TEST_F(homa_hijack, homa_hijack_udp_net_start__basics) +{ + struct net *net = mock_net_for_hnet(self->hnet); + + EXPECT_EQ(0, homa_hijack_udp_net_start(self->hnet, net)); + EXPECT_NE(NULL, self->hnet->udp_ctl_table); + EXPECT_NE(NULL, self->hnet->udp_ctl_header); + EXPECT_EQ(&self->hnet->hijack_udp, self->hnet->udp_ctl_table[0].data); + EXPECT_EQ(1, mock_register_sysctl_size); + + homa_hijack_udp_net_exit_begin(self->hnet); + EXPECT_EQ(NULL, self->hnet->udp_ctl_header); + EXPECT_EQ(HOMA_UDP_TEARDOWN, self->hnet->udp_state); + EXPECT_STREQ("unregister_net_sysctl_table", unit_log_get()); + + homa_hijack_udp_net_destroy(self->hnet); + EXPECT_EQ(NULL, self->hnet->udp_ctl_table); +} + +TEST_F(homa_hijack, homa_hijack_udp_net_start__kmalloc_error) +{ + struct net *net = mock_net_for_hnet(self->hnet); + + mock_kmalloc_errors = 1; + EXPECT_EQ(ENOMEM, -homa_hijack_udp_net_start(self->hnet, net)); + EXPECT_EQ(NULL, self->hnet->udp_ctl_table); +} + +TEST_F(homa_hijack, homa_hijack_udp_net_start__register_sysctl_error) +{ + struct net *net = mock_net_for_hnet(self->hnet); + + mock_register_sysctl_errors = 1; + EXPECT_EQ(ENOMEM, -homa_hijack_udp_net_start(self->hnet, net)); + EXPECT_EQ(NULL, self->hnet->udp_ctl_table); +} + +TEST_F(homa_hijack, homa_hijack_udp_set_enabled__enable_creates_pair) +{ + struct net *net = mock_net_for_hnet(self->hnet); + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + EXPECT_EQ(HOMA_UDP_ENABLED, self->hnet->udp_state); + EXPECT_EQ(1, self->hnet->hijack_udp); + EXPECT_NE(NULL, self->hnet->udp_tun4); + EXPECT_NE(NULL, self->hnet->udp_tun6); + + /* Enabling again is a no-op (doesn't recreate the pair). */ + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + EXPECT_EQ(HOMA_UDP_ENABLED, self->hnet->udp_state); +} + +TEST_F(homa_hijack, homa_hijack_udp_set_enabled__create_pair_v4_error) +{ + struct net *net = mock_net_for_hnet(self->hnet); + + mock_udp_sock_create_errors = 1; + EXPECT_EQ(EADDRINUSE, -homa_hijack_udp_set_enabled(self->hnet, net, + 1)); + EXPECT_EQ(HOMA_UDP_DISABLED, self->hnet->udp_state); + EXPECT_EQ(NULL, self->hnet->udp_tun4); + EXPECT_EQ(NULL, self->hnet->udp_tun6); +} + +TEST_F(homa_hijack, homa_hijack_udp_set_enabled__create_pair_v6_error) +{ + struct net *net = mock_net_for_hnet(self->hnet); + + mock_udp_sock_create_errors = 2; + EXPECT_EQ(EADDRINUSE, -homa_hijack_udp_set_enabled(self->hnet, net, + 1)); + EXPECT_EQ(HOMA_UDP_DISABLED, self->hnet->udp_state); + EXPECT_EQ(NULL, self->hnet->udp_tun4); + EXPECT_EQ(NULL, self->hnet->udp_tun6); + EXPECT_EQ(1, mock_udp_tunnel_release_count); +} + +TEST_F(homa_hijack, homa_hijack_udp_set_enabled__disable_no_rpcs) +{ + struct net *net = mock_net_for_hnet(self->hnet); + + homa_hijack_udp_set_enabled(self->hnet, net, 1); + mock_udp_tunnel_release_count = 0; + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 0)); + EXPECT_EQ(HOMA_UDP_DISABLED, self->hnet->udp_state); + EXPECT_EQ(0, self->hnet->hijack_udp); + EXPECT_EQ(NULL, self->hnet->udp_tun4); + EXPECT_EQ(NULL, self->hnet->udp_tun6); + EXPECT_EQ(2, mock_udp_tunnel_release_count); +} + +TEST_F(homa_hijack, homa_hijack_udp_set_enabled__disable_with_active_rpc_drains) +{ + struct net *net = mock_net_for_hnet(self->hnet); + + homa_hijack_udp_set_enabled(self->hnet, net, 1); + atomic_set(&self->hnet->udp_rpc_count, 1); + self->homa.timer_ticks = 500; + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 0)); + EXPECT_EQ(HOMA_UDP_DRAINING, self->hnet->udp_state); + EXPECT_EQ(0, self->hnet->hijack_udp); + EXPECT_NE(NULL, self->hnet->udp_tun4); + EXPECT_NE(NULL, self->hnet->udp_tun6); + EXPECT_EQ(500 + self->homa.timeout_ticks, + self->hnet->udp_drain_deadline); + + /* Repeated disable requests while draining are no-ops. */ + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 0)); + EXPECT_EQ(HOMA_UDP_DRAINING, self->hnet->udp_state); +} + +TEST_F(homa_hijack, homa_hijack_udp_set_enabled__teardown_rejects) +{ + struct net *net = mock_net_for_hnet(self->hnet); + + self->hnet->udp_state = HOMA_UDP_TEARDOWN; + EXPECT_EQ(ENETDOWN, -homa_hijack_udp_set_enabled(self->hnet, net, 1)); + EXPECT_EQ(ENETDOWN, -homa_hijack_udp_set_enabled(self->hnet, net, 0)); +} + +TEST_F(homa_hijack, homa_hijack_udp_admit__socket_not_hijacked) +{ + struct homa_rpc rpc; + + memset(&rpc, 0, sizeof(rpc)); + rpc.hsk = &self->hsk; + self->hsk.sock.sk_protocol = IPPROTO_HOMA; + EXPECT_EQ(0, homa_hijack_udp_admit(&rpc)); + EXPECT_EQ(0, rpc.udp_admitted); + EXPECT_EQ(0, atomic_read(&self->hnet->udp_rpc_count)); +} + +TEST_F(homa_hijack, homa_hijack_udp_admit__enabled) +{ + struct homa_rpc rpc; + + memset(&rpc, 0, sizeof(rpc)); + rpc.hsk = &self->hsk; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + self->hnet->udp_state = HOMA_UDP_ENABLED; + EXPECT_EQ(0, homa_hijack_udp_admit(&rpc)); + EXPECT_EQ(1, rpc.udp_admitted); + EXPECT_EQ(1, atomic_read(&self->hnet->udp_rpc_count)); +} + +TEST_F(homa_hijack, homa_hijack_udp_admit__disabled_rejects) +{ + struct homa_rpc rpc; + + memset(&rpc, 0, sizeof(rpc)); + rpc.hsk = &self->hsk; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + self->hnet->udp_state = HOMA_UDP_DRAINING; + EXPECT_EQ(ENETDOWN, -homa_hijack_udp_admit(&rpc)); + EXPECT_EQ(0, rpc.udp_admitted); + EXPECT_EQ(0, atomic_read(&self->hnet->udp_rpc_count)); +} + +TEST_F(homa_hijack, homa_hijack_udp_admit__disable_race_releases_pair) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_rpc rpc; + + memset(&rpc, 0, sizeof(rpc)); + rpc.hsk = &self->hsk; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + ASSERT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + udp_race_hnet = self->hnet; + udp_race_net = net; + unit_hook_register(udp_disable_during_admit_hook); + + EXPECT_EQ(ENETDOWN, -homa_hijack_udp_admit(&rpc)); + EXPECT_EQ(0, rpc.udp_admitted); + EXPECT_EQ(0, atomic_read(&self->hnet->udp_rpc_count)); + EXPECT_EQ(HOMA_UDP_DISABLED, self->hnet->udp_state); + EXPECT_EQ(NULL, self->hnet->udp_tun4); + EXPECT_EQ(NULL, self->hnet->udp_tun6); +} + +TEST_F(homa_hijack, homa_hijack_udp_admit__disable_wins_race) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_rpc rpc; + + memset(&rpc, 0, sizeof(rpc)); + rpc.hsk = &self->hsk; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + ASSERT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + udp_race_rpc = &rpc; + udp_race_admit_result = 0; + unit_hook_register(udp_admit_during_disable_hook); + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 0)); + EXPECT_EQ(ENETDOWN, -udp_race_admit_result); + EXPECT_EQ(0, rpc.udp_admitted); + EXPECT_EQ(0, atomic_read(&self->hnet->udp_rpc_count)); + EXPECT_EQ(HOMA_UDP_DISABLED, self->hnet->udp_state); + EXPECT_EQ(NULL, self->hnet->udp_tun4); + EXPECT_EQ(NULL, self->hnet->udp_tun6); +} + +TEST_F(homa_hijack, homa_hijack_udp_release__active_callback_completes) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_common_hdr hdr; + + ASSERT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + memset(&hdr, 0, sizeof(hdr)); + hdr.dport = htons(self->hsk.port); + udp_race_hnet = self->hnet; + udp_release_skb = mock_raw_skb(&self->src_ip, &self->dst_ip, + IPPROTO_UDP, 0); + udp_release_hdr = &hdr; + udp_release_callback_count = 0; + unit_hook_register(udp_callback_during_release_hook); + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 0)); + EXPECT_EQ(1, udp_release_callback_count); + EXPECT_EQ(HOMA_UDP_DISABLED, self->hnet->udp_state); + EXPECT_EQ(2, mock_udp_tunnel_release_count); + kfree_skb(udp_release_skb); +} + +TEST_F(homa_hijack, homa_hijack_udp_end_rpc__triggers_release_when_draining) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_rpc rpc; + + memset(&rpc, 0, sizeof(rpc)); + rpc.hsk = &self->hsk; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + homa_hijack_udp_set_enabled(self->hnet, net, 1); + EXPECT_EQ(0, homa_hijack_udp_admit(&rpc)); + homa_hijack_udp_set_enabled(self->hnet, net, 0); + EXPECT_EQ(HOMA_UDP_DRAINING, self->hnet->udp_state); + EXPECT_NE(NULL, self->hnet->udp_tun4); + + homa_hijack_udp_end_rpc(&rpc); + EXPECT_EQ(0, rpc.udp_admitted); + EXPECT_EQ(HOMA_UDP_DISABLED, self->hnet->udp_state); + EXPECT_EQ(NULL, self->hnet->udp_tun4); + EXPECT_EQ(NULL, self->hnet->udp_tun6); + + /* Calling again is idempotent (no double-decrement or crash). */ + homa_hijack_udp_end_rpc(&rpc); +} + +/* The tests below exercise the encap_rcv/encap_err_lookup/encap_err_rcv + * callbacks registered with the UDP tunnel sockets (see homa_hijack.c). + * Those callbacks are static, so they're reached indirectly: enabling + * UDP hijacking populates mock_udp_tunnel_cfg (see + * mock_setup_udp_tunnel_sock()) with the same function pointers that + * were passed to setup_udp_tunnel_sock(), and the tests invoke them + * through that struct. + */ +TEST_F(homa_hijack, homa_hijack_udp_encap_rcv__dispatches_udp_packet) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_resend_hdr h = {{.sport = htons(40000), + .dport = htons(self->hsk.port), + .sender_id = cpu_to_be64(1234), + .type = RESEND}, + .offset = htonl(0), .length = htonl(100)}; + struct sk_buff *skb; + struct udphdr *uh; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, + sizeof(*uh) + sizeof(h)); + uh = skb_put(skb, sizeof(*uh)); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT); + memcpy(skb_put(skb, sizeof(h)), &h, sizeof(h)); + + unit_log_clear(); + EXPECT_EQ(0, mock_udp_tunnel_cfg.encap_rcv(&self->hsk.sock, skb)); + EXPECT_SUBSTR("xmit RPC_UNKNOWN", unit_log_get()); +} +TEST_F(homa_hijack, homa_hijack_udp_encap_rcv__ipv6_extension_header) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_resend_hdr h = {{.sport = htons(40000), + .dport = htons(self->hsk.port), + .sender_id = cpu_to_be64(1234), + .type = RESEND}, + .offset = htonl(0), .length = htonl(100)}; + unsigned char *extension; + struct sk_buff *skb; + struct udphdr *uh; + + mock_ipv6 = true; + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, + 8 + sizeof(*uh) + sizeof(h)); + ipv6_hdr(skb)->nexthdr = NEXTHDR_DEST; + extension = skb_put(skb, 8); + extension[0] = IPPROTO_UDP; + extension[1] = 0; + uh = skb_put(skb, sizeof(*uh)); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT); + memcpy(skb_put(skb, sizeof(h)), &h, sizeof(h)); + skb_pull(skb, 8); + skb_reset_transport_header(skb); + + unit_log_clear(); + EXPECT_EQ(0, mock_udp_tunnel_cfg.encap_rcv(&self->hsk.sock, skb)); + EXPECT_SUBSTR("xmit RPC_UNKNOWN", unit_log_get()); +} +TEST_F(homa_hijack, homa_hijack_udp_encap_rcv__nonlinear_homa_header) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_resend_hdr h = {{.sport = htons(40000), + .dport = htons(self->hsk.port), + .sender_id = cpu_to_be64(1234), + .type = RESEND}, + .offset = htonl(0), .length = htonl(100)}; + struct skb_shared_info *shinfo; + struct sk_buff *skb; + struct udphdr *uh; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, + sizeof(*uh) + sizeof(h)); + uh = skb_put(skb, sizeof(*uh)); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT); + shinfo = skb_shinfo(skb); + unit_alloc_frags(1, shinfo->frags, 0, sizeof(h)); + memcpy(unit_frag_first_byte(&shinfo->frags[0]), &h, sizeof(h)); + shinfo->nr_frags = 1; + skb->data_len = sizeof(h); + skb->len += skb->data_len; + + unit_log_clear(); + EXPECT_EQ(0, mock_udp_tunnel_cfg.encap_rcv(&self->hsk.sock, skb)); + EXPECT_SUBSTR("xmit RPC_UNKNOWN", unit_log_get()); + EXPECT_NOSUBSTR("pskb discard", unit_log_get()); +} +TEST_F(homa_hijack, homa_hijack_udp_encap_rcv__wrong_port_dropped) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct sk_buff *skb; + struct udphdr *uh; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, + sizeof(*uh)); + uh = skb_put(skb, sizeof(*uh)); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT + 1); + + unit_log_clear(); + EXPECT_EQ(0, mock_udp_tunnel_cfg.encap_rcv(&self->hsk.sock, skb)); + EXPECT_STREQ("", unit_log_get()); +} +TEST_F(homa_hijack, homa_hijack_udp_encap_rcv__too_short_dropped) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct sk_buff *skb; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, 2); + skb_put(skb, 2); + + unit_log_clear(); + EXPECT_EQ(0, mock_udp_tunnel_cfg.encap_rcv(&self->hsk.sock, skb)); + EXPECT_STREQ("", unit_log_get()); +} + +TEST_F(homa_hijack, homa_hijack_udp_encap_err_lookup__valid_accepted) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_common_hdr h; + struct sk_buff *skb; + struct udphdr *uh; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + + memset(&h, 0, sizeof(h)); + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, + sizeof(*uh) + sizeof(h)); + uh = skb_put(skb, sizeof(*uh)); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT); + memcpy(skb_put(skb, sizeof(h)), &h, sizeof(h)); + + EXPECT_EQ(0, mock_udp_tunnel_cfg.encap_err_lookup(&self->hsk.sock, + skb)); + kfree_skb(skb); +} +TEST_F(homa_hijack, homa_hijack_udp_encap_err_lookup__wrong_port_rejected) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_common_hdr h; + struct sk_buff *skb; + struct udphdr *uh; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + + memset(&h, 0, sizeof(h)); + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, + sizeof(*uh) + sizeof(h)); + uh = skb_put(skb, sizeof(*uh)); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT + 1); + memcpy(skb_put(skb, sizeof(h)), &h, sizeof(h)); + + EXPECT_EQ(1, mock_udp_tunnel_cfg.encap_err_lookup(&self->hsk.sock, + skb)); + kfree_skb(skb); +} +TEST_F(homa_hijack, homa_hijack_udp_encap_err_lookup__too_short_rejected) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct sk_buff *skb; + struct udphdr *uh; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, + sizeof(*uh)); + uh = skb_put(skb, sizeof(*uh)); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT); + + EXPECT_EQ(1, mock_udp_tunnel_cfg.encap_err_lookup(&self->hsk.sock, + skb)); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_hijack_udp_encap_err_rcv__econnrefused_translated) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_rpc *crpc; + struct homa_common_hdr h; + struct sk_buff *skb; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + crpc = unit_client_rpc(&self->hsk, UNIT_OUTGOING, &self->src_ip, + &self->dst_ip, 99, 1000, 100, 100); + ASSERT_NE(NULL, crpc); + + memset(&h, 0, sizeof(h)); + h.dport = htons(99); + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, 0); + + mock_udp_tunnel_cfg.encap_err_rcv(self->hnet->udp_tun4->sk, skb, + ECONNREFUSED, + htons(HOMA_UDP_HIJACK_PORT), 0, + (u8 *)&h); + EXPECT_EQ(ENOTCONN, -crpc->error); + + kfree_skb(skb); +} +TEST_F(homa_hijack, homa_hijack_udp_encap_err_rcv__other_errno_uses_port_filter) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_rpc *crpc; + struct homa_common_hdr h; + struct sk_buff *skb; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + crpc = unit_client_rpc(&self->hsk, UNIT_OUTGOING, &self->src_ip, + &self->dst_ip, 99, 1000, 100, 100); + ASSERT_NE(NULL, crpc); + + memset(&h, 0, sizeof(h)); + h.dport = htons(12345); + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, 0); + + mock_udp_tunnel_cfg.encap_err_rcv(self->hnet->udp_tun4->sk, skb, + EHOSTUNREACH, + htons(HOMA_UDP_HIJACK_PORT), 0, + (u8 *)&h); + EXPECT_EQ(0, crpc->error); + + h.dport = htons(99); + mock_udp_tunnel_cfg.encap_err_rcv(self->hnet->udp_tun4->sk, skb, + EHOSTUNREACH, + htons(HOMA_UDP_HIJACK_PORT), 0, + (u8 *)&h); + EXPECT_EQ(EHOSTUNREACH, -crpc->error); + + kfree_skb(skb); +} +TEST_F(homa_hijack, homa_hijack_udp_encap_err_rcv__emsgsize_updates_pmtu_and_aborts) +{ + struct net *net = mock_net_for_hnet(self->hnet); + struct homa_rpc *crpc; + struct homa_route *route; + struct homa_common_hdr h; + struct sk_buff *skb; + u64 route_allocs; + + EXPECT_EQ(0, homa_hijack_udp_set_enabled(self->hnet, net, 1)); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + mock_ipv6 = false; + crpc = unit_client_rpc(&self->hsk, UNIT_OUTGOING, &self->src_ip, + &self->dst_ip, 99, 1000, 100, 100); + ASSERT_NE(NULL, crpc); + EXPECT_EQ(1, self->hnet->num_routes); + route_allocs = homa_metrics_per_cpu()->route_allocs; + + memset(&h, 0, sizeof(h)); + h.dport = htons(99); + skb = mock_raw_skb(&self->src_ip, &self->dst_ip, IPPROTO_UDP, 64); + skb_put(skb, 64); + skb_push(skb, sizeof(struct iphdr)); + ((struct iphdr *)skb->data)->ihl = 5; + skb_set_network_header(skb, 40); + + unit_log_clear(); + mock_udp_tunnel_cfg.encap_err_rcv(self->hnet->udp_tun4->sk, skb, + EMSGSIZE, + htons(HOMA_UDP_HIJACK_PORT), 500, + (u8 *)&h); + EXPECT_SUBSTR("update_pmtu mtu 500", unit_log_get()); + EXPECT_EQ(EMSGSIZE, -crpc->error); + EXPECT_EQ(0, self->hnet->num_routes); + + route = homa_route_get(&self->hsk, &self->dst_ip); + ASSERT_FALSE(IS_ERR(route)); + EXPECT_EQ(route_allocs + 1, homa_metrics_per_cpu()->route_allocs); + EXPECT_EQ(1, self->hnet->num_routes); + homa_route_release(route); + + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_hijack_prepend_udp__ipv4) +{ + struct homa_route *route = homa_route_get(&self->hsk, &self->src_ip); + struct homa_common_hdr *h; + struct udphdr *uh; + struct sk_buff *skb; + int orig_len; + + skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, + &self->header.common, 1400, 0); + orig_len = skb->len; + + homa_hijack_prepend_udp(skb, route, false); + + EXPECT_EQ(orig_len + sizeof(struct udphdr), skb->len); + uh = (struct udphdr *)skb_transport_header(skb); + EXPECT_EQ(HOMA_UDP_HIJACK_PORT, ntohs(uh->source)); + EXPECT_EQ(HOMA_UDP_HIJACK_PORT, ntohs(uh->dest)); + EXPECT_EQ(orig_len + sizeof(struct udphdr), ntohs(uh->len)); + EXPECT_EQ(555U, uh->check); + + /* The original Homa header must still be intact immediately after + * the new UDP header. + */ + h = (struct homa_common_hdr *)(uh + 1); + EXPECT_EQ(self->header.common.type, h->type); + EXPECT_EQ(self->header.common.sender_id, h->sender_id); + + homa_route_release(route); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_hijack_prepend_udp__ipv6) +{ + struct homa_route *route = homa_route_get(&self->hsk, &self->src_ip); + struct homa_common_hdr *h; + struct udphdr *uh; + struct sk_buff *skb; + int orig_len; + + skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, + &self->header.common, 1400, 0); + orig_len = skb->len; + + homa_hijack_prepend_udp(skb, route, true); + + EXPECT_EQ(orig_len + sizeof(struct udphdr), skb->len); + uh = (struct udphdr *)skb_transport_header(skb); + EXPECT_EQ(HOMA_UDP_HIJACK_PORT, ntohs(uh->source)); + EXPECT_EQ(HOMA_UDP_HIJACK_PORT, ntohs(uh->dest)); + EXPECT_EQ(orig_len + sizeof(struct udphdr), ntohs(uh->len)); + EXPECT_EQ(777U, uh->check); + + h = (struct homa_common_hdr *)(uh + 1); + EXPECT_EQ(self->header.common.type, h->type); + EXPECT_EQ(self->header.common.sender_id, h->sender_id); + + homa_route_release(route); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_skb_inner_hdr__udp_hijacked) +{ + struct homa_route *route = homa_route_get(&self->hsk, &self->src_ip); + struct homa_common_hdr *h; + struct sk_buff *skb; + + mock_ipv6 = false; + skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, + &self->header.common, 1400, 0); + homa_hijack_prepend_udp(skb, route, false); + ip_hdr(skb)->protocol = IPPROTO_UDP; + + h = homa_skb_inner_hdr(skb); + ASSERT_NE(NULL, h); + EXPECT_EQ((unsigned char *)h, + skb_transport_header(skb) + sizeof(struct udphdr)); + EXPECT_EQ(self->header.common.sender_id, h->sender_id); + + homa_route_release(route); + kfree_skb(skb); +} + +TEST_F(homa_hijack, homa_skb_inner_hdr__udp_wrong_port) +{ + struct homa_route *route = homa_route_get(&self->hsk, &self->src_ip); + struct udphdr *uh; + struct sk_buff *skb; + + /* A UDP packet that isn't using the reserved hijack port must not + * be mistaken for a Homa-over-UDP packet. + */ + mock_ipv6 = false; + skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, + &self->header.common, 1400, 0); + homa_hijack_prepend_udp(skb, route, false); + ip_hdr(skb)->protocol = IPPROTO_UDP; + uh = (struct udphdr *)skb_transport_header(skb); + uh->dest = htons(HOMA_UDP_HIJACK_PORT + 1); + + EXPECT_EQ(NULL, homa_skb_inner_hdr(skb)); + + homa_route_release(route); + kfree_skb(skb); +} +#endif /* See strip.py */ \ No newline at end of file diff --git a/test/unit_homa_incoming.c b/test/unit_homa_incoming.c index 90b19ce7..b1e7eae2 100644 --- a/test/unit_homa_incoming.c +++ b/test/unit_homa_incoming.c @@ -87,7 +87,9 @@ FIXTURE(homa_incoming) { struct homa_sock hsk; struct homa_sock hsk2; struct homa_data_hdr data; +#ifndef __STRIP__ /* See strip.py */ struct homa_start_msg_hdr start; +#endif /* See strip.py */ }; FIXTURE_SETUP(homa_incoming) { @@ -126,6 +128,7 @@ FIXTURE_SETUP(homa_incoming) }; self->data.msg_length = htonl(10000); +#ifndef __STRIP__ /* See strip.py */ memset(&self->start, 0, sizeof(self->start)); self->start.common = (struct homa_common_hdr){ .sport = htons(self->client_port), @@ -134,6 +137,7 @@ FIXTURE_SETUP(homa_incoming) .sender_id = cpu_to_be64(self->client_id) }; self->start.msg_length = htonl(10000); +#endif /* See strip.py */ unit_log_clear(); delete_count = 0; @@ -1206,7 +1210,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__unknown_socket_ipv4) skb = mock_skb_alloc(self->client_ip, self->server_ip, &self->data.common, 1400, 1400); unit_log_clear(); - homa_dispatch_pkts(skb); + homa_dispatch_pkts(skb, HOMA_PKT_NATIVE); EXPECT_EQ(0, unit_list_length(&self->hsk.active_rpcs)); EXPECT_STREQ("icmp_send type 3, code 3", unit_log_get()); } @@ -1224,7 +1228,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__unknown_socket_ipv6) skb = mock_skb_alloc(self->client_ip, self->server_ip, &self->data.common, 1400, 1400); unit_log_clear(); - homa_dispatch_pkts(skb); + homa_dispatch_pkts(skb, HOMA_PKT_NATIVE); EXPECT_EQ(0, unit_list_length(&self->hsk.active_rpcs)); EXPECT_STREQ("icmp6_send type 1, code 4", unit_log_get()); } @@ -1243,7 +1247,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__server_not_enabled) skb = mock_skb_alloc(self->client_ip, self->server_ip, &self->data.common, 1400, 1400); unit_log_clear(); - homa_dispatch_pkts(skb); + homa_dispatch_pkts(skb, HOMA_PKT_NATIVE); EXPECT_EQ(0, unit_list_length(&self->hsk.active_rpcs)); EXPECT_STREQ("icmp_send type 3, code 3", unit_log_get()); } @@ -1267,14 +1271,56 @@ TEST_F(homa_incoming, homa_dispatch_pkts__unknown_socket_free_many_packets) skb->next = skb2; skb2->next = skb3; unit_log_clear(); - homa_dispatch_pkts(skb); + homa_dispatch_pkts(skb, HOMA_PKT_NATIVE); EXPECT_EQ(0, unit_list_length(&self->hsk.active_rpcs)); EXPECT_STREQ("icmp6_send type 1, code 4", unit_log_get()); } +#ifndef __STRIP__ /* See strip.py */ +TEST_F(homa_incoming, homa_dispatch_pkts__delayed_udp_after_native_port_reuse) +{ + struct sk_buff *delayed; + + self->hsk2.sock.sk_protocol = IPPROTO_UDP; + delayed = mock_skb_alloc(self->client_ip, self->server_ip, + &self->data.common, 1400, 0); + unit_sock_destroy(&self->hsk2); + mock_sock_init(&self->hsk2, self->hnet, self->server_port); + + unit_log_clear(); + homa_dispatch_pkts(delayed, HOMA_PKT_UDP); + EXPECT_STREQ("", unit_log_get()); + EXPECT_EQ(1, homa_metrics_per_cpu()->unknown_packet_types); + EXPECT_EQ(0, unit_list_length(&self->hsk2.active_rpcs)); +} +TEST_F(homa_incoming, homa_dispatch_pkts__native_origin_mismatch_dropped_silently) +{ + self->hsk2.sock.sk_protocol = IPPROTO_UDP; + + unit_log_clear(); + homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, + &self->data.common, 1400, 0), + HOMA_PKT_NATIVE); + EXPECT_STREQ("", unit_log_get()); + EXPECT_EQ(1, homa_metrics_per_cpu()->unknown_packet_types); + EXPECT_EQ(0, unit_list_length(&self->hsk2.active_rpcs)); +} +TEST_F(homa_incoming, homa_dispatch_pkts__native_origin_matches_non_hijacked_sock) +{ + /* Sanity check: native-origin packets for a non-UDP-hijacked socket + * are dispatched normally (no transport-isolation drop). + */ + unit_log_clear(); + homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, + &self->data.common, 1400, 0), + HOMA_PKT_NATIVE); + EXPECT_EQ(0, homa_metrics_per_cpu()->unknown_packet_types); + EXPECT_EQ(1, unit_list_length(&self->hsk2.active_rpcs)); +} +#endif /* See strip.py */ TEST_F(homa_incoming, homa_dispatch_pkts__new_server_rpc) { homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_EQ(1, unit_list_length(&self->hsk2.active_rpcs)); EXPECT_EQ(1, mock_skb_count()); } @@ -1282,7 +1328,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__cant_create_server_rpc) { mock_kmalloc_errors = 1; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_EQ(0, unit_list_length(&self->hsk.active_rpcs)); EXPECT_EQ(0, mock_skb_count()); #ifndef __STRIP__ /* See strip.py */ @@ -1300,7 +1346,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__existing_server_rpc) self->data.seg.offset = htonl(1400); self->data.common.sender_id = cpu_to_be64(self->client_id); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_EQ(7200, srpc->msgin.bytes_remaining); } TEST_F(homa_incoming, homa_dispatch_pkts__non_data_packet_for_existing_server_rpc) @@ -1324,7 +1370,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__non_data_packet_for_existing_server_rp ASSERT_NE(NULL, srpc); unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &resend.common, 0, 0)); + &resend.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit BUSY", unit_log_get()); } #ifndef __STRIP__ /* See strip.py */ @@ -1354,7 +1400,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__unknown_client_rpc) mock_xmit_log_verbose = 1; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(1, homa_metrics_per_cpu()->unknown_rpcs); } TEST_F(homa_incoming, homa_dispatch_pkts__unknown_server_rpc) @@ -1366,7 +1412,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__unknown_server_rpc) mock_xmit_log_verbose = 1; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(0, homa_metrics_per_cpu()->unknown_rpcs); } TEST_F(homa_incoming, homa_dispatch_pkts__cutoffs_for_unknown_client_rpc) @@ -1382,7 +1428,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__cutoffs_for_unknown_client_rpc) struct homa_route *route; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); route = homa_route_get(&self->hsk, self->server_ip); ASSERT_FALSE(IS_ERR(route)); EXPECT_EQ(400, route->peer->cutoff_version); @@ -1404,7 +1450,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__resend_for_unknown_server_rpc) #endif /* See strip.py */ homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit RPC_UNKNOWN", unit_log_get()); } #ifndef __STRIP__ /* See strip.py */ @@ -1428,13 +1474,13 @@ TEST_F(homa_incoming, homa_dispatch_pkts__reset_counters) unit_log_clear(); crpc->silent_ticks = 5; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(0, crpc->silent_ticks); /* Don't reset silent_ticks for some packet types. */ crpc->silent_ticks = 5; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &cutoffs.common, 0, 0)); + &cutoffs.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(5, crpc->silent_ticks); } #endif /* See strip.py */ @@ -1459,7 +1505,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__dont_reset_silent_ticks_on_NEED_ACK) unit_log_clear(); crpc->silent_ticks = 2; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(2, crpc->silent_ticks); } TEST_F(homa_incoming, homa_dispatch_pkts__multiple_ack_packets) @@ -1487,7 +1533,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__multiple_ack_packets) skb2->next = skb3; unit_log_clear(); - homa_dispatch_pkts(skb); + homa_dispatch_pkts(skb, HOMA_PKT_NATIVE); EXPECT_SUBSTR("ack 1239", unit_log_get()); } #ifndef __STRIP__ /* See strip.py */ @@ -1502,7 +1548,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__start_msg_creates_new_server_rpc) h.common.type = START_MSG; h.msg_length = htonl(1000); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(1, homa_metrics_per_cpu()->packets_received[START_MSG - DATA]); EXPECT_EQ(1, unit_list_length(&self->hsk2.active_rpcs)); @@ -1524,7 +1570,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__unknown_type) .dport = htons(self->hsk.port), .sender_id = cpu_to_be64(self->server_id), .type = 99}; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h, 0, 0)); + &h, 0, 0), HOMA_PKT_NATIVE); #ifndef __STRIP__ /* See strip.py */ EXPECT_EQ(1, homa_metrics_per_cpu()->unknown_packet_types); #endif /* See strip.py */ @@ -1542,7 +1588,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__handle_ack) self->data.common.sender_id = cpu_to_be64(self->client_id+10); unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("DEAD", homa_symbol_for_state(srpc)); EXPECT_SUBSTR("ack 1235", unit_log_get()); } @@ -1551,7 +1597,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__invoke_homa_grant_check_rpc) { self->data.msg_length = htonl(20000); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &self->start.common, 0, 0)); + &self->start.common, 0, 0), HOMA_PKT_NATIVE); unit_log_clear(); unit_log_grantables(&self->homa); EXPECT_SUBSTR("id 1235", unit_log_get()); @@ -1588,7 +1634,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__forced_reap) self->data.common.dport = htons(self->hsk.port); unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_EQ(20, self->hsk.dead_frags); #ifndef __STRIP__ /* See strip.py */ EXPECT_EQ(0, homa_metrics_per_cpu()->dispatch_pkt_reap_cycles); @@ -1600,7 +1646,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__forced_reap) self->homa.dead_frags_limit = 6; unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_EQ(4, self->hsk.dead_frags); EXPECT_STREQ("reaped 1234", unit_log_get()); #ifndef __STRIP__ /* See strip.py */ @@ -1614,7 +1660,7 @@ TEST_F(homa_incoming, homa_dispatch_pkts__forced_reap) unit_log_clear(); set_bit(HOMA_SOCK_NOSPACE, &self->hsk.flags); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("reaped 1236", unit_log_get()); EXPECT_EQ(2, self->hsk.dead_frags); homa_rpc_unlock(dead3); @@ -1739,7 +1785,7 @@ TEST_F(homa_incoming, homa_data_pkt__send_cutoffs) self->data.msg_length = htonl(5000); mock_xmit_log_verbose = 1; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_SUBSTR("cutoffs 19 18 17 16 15 14 13 12, version 2", unit_log_get()); @@ -1750,7 +1796,7 @@ TEST_F(homa_incoming, homa_data_pkt__send_cutoffs) self->homa.cutoff_version = 3; self->data.seg.offset = 1400; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("", unit_log_get()); } TEST_F(homa_incoming, homa_data_pkt__cutoffs_up_to_date) @@ -1758,7 +1804,7 @@ TEST_F(homa_incoming, homa_data_pkt__cutoffs_up_to_date) self->homa.cutoff_version = 123; self->data.cutoff_version = htons(123); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1400, 0)); + &self->data.common, 1400, 0), HOMA_PKT_NATIVE); EXPECT_NOSUBSTR("cutoffs", unit_log_get()); } TEST_F(homa_incoming, homa_data_pkt__homa_add_packet_returns_error) @@ -1882,7 +1928,7 @@ TEST_F(homa_incoming, homa_grant_pkt__basics) unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(3000, srpc->msgout.granted); EXPECT_STREQ("xmit DATA 1400@0; " "xmit DATA 1400@1400; " @@ -1892,7 +1938,7 @@ TEST_F(homa_incoming, homa_grant_pkt__basics) h.offset = htonl(900); unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(3000, srpc->msgout.granted); EXPECT_STREQ("", unit_log_get()); @@ -1901,7 +1947,7 @@ TEST_F(homa_incoming, homa_grant_pkt__basics) srpc->state = RPC_INCOMING; unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(3000, srpc->msgout.granted); EXPECT_STREQ("", unit_log_get()); @@ -1923,7 +1969,7 @@ TEST_F(homa_incoming, homa_grant_pkt__grant_past_end_of_message) ASSERT_NE(NULL, crpc); unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(20000, crpc->msgout.granted); } TEST_F(homa_incoming, homa_grant_pkt__offset_has_sign_bit_set) @@ -1946,7 +1992,7 @@ TEST_F(homa_incoming, homa_grant_pkt__offset_has_sign_bit_set) unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(20000, crpc->msgout.granted); } #endif /* See strip.py */ @@ -1961,7 +2007,7 @@ TEST_F(homa_incoming, homa_resend_pkt__unknown_rpc) .length = htonl(200)}; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit RPC_UNKNOWN", unit_log_get()); } TEST_F(homa_incoming, homa_resend_pkt__response_not_ready_server_sends_busy) @@ -1980,9 +2026,10 @@ TEST_F(homa_incoming, homa_resend_pkt__response_not_ready_server_sends_busy) unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit BUSY", unit_log_get()); } +#ifndef __STRIP__ /* See strip.py */ TEST_F(homa_incoming, homa_resend_pkt__negative_length_in_resend_send_start_msg) { struct homa_resend_hdr h = {{.sport = htons(self->client_port), @@ -2003,7 +2050,7 @@ TEST_F(homa_incoming, homa_resend_pkt__negative_length_in_resend_send_start_msg) unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit START_MSG 20000", unit_log_get()); } TEST_F(homa_incoming, homa_resend_pkt__negative_length_in_resend_send_data) @@ -2024,10 +2071,11 @@ TEST_F(homa_incoming, homa_resend_pkt__negative_length_in_resend_send_data) srpc->msgout.granted = 3000; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit DATA retrans 1400@0; " "xmit DATA retrans 1400@1400", unit_log_get()); } +#endif /* See strip.py */ TEST_F(homa_incoming, homa_resend_pkt__client_not_outgoing) { /* Important to respond to resends even if client thinks the @@ -2047,7 +2095,7 @@ TEST_F(homa_incoming, homa_resend_pkt__client_not_outgoing) unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit DATA retrans 1400@0", unit_log_get()); } TEST_F(homa_incoming, homa_resend_pkt__clip_range_to_tx_end) @@ -2068,7 +2116,7 @@ TEST_F(homa_incoming, homa_resend_pkt__clip_range_to_tx_end) IF_NO_STRIP(crpc->msgout.granted = 5000); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit DATA retrans 1400@0", unit_log_get()); } TEST_F(homa_incoming, homa_resend_pkt__no_need_to_clip_range) @@ -2089,7 +2137,7 @@ TEST_F(homa_incoming, homa_resend_pkt__no_need_to_clip_range) crpc->msgout.next_xmit_offset = 2800; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit DATA retrans 1400@0", unit_log_get()); } #ifndef __STRIP__ /* See strip.py */ @@ -2111,7 +2159,7 @@ TEST_F(homa_incoming, homa_resend_pkt__set_priority) crpc->msgout.next_xmit_offset = 2800; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit DATA retrans 1400@0", unit_log_get()); EXPECT_STREQ("5", mock_xmit_prios); } @@ -2136,7 +2184,7 @@ TEST_F(homa_incoming, homa_resend_pkt__update_granted_and_xmit) EXPECT_EQ(1400, crpc->msgout.next_xmit_offset); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(3400, crpc->msgout.granted); EXPECT_EQ(4200, crpc->msgout.next_xmit_offset); } @@ -2155,7 +2203,7 @@ TEST_F(homa_incoming, homa_resend_pkt__clip_granted_to_message_length) ASSERT_NE(NULL, crpc); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(5000, crpc->msgout.granted); } #endif /* See strip.py */ @@ -2176,10 +2224,11 @@ TEST_F(homa_incoming, homa_resend_pkt__requested_data_hasnt_been_sent_yet) unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_SUBSTR("xmit BUSY", unit_log_get()); } +#ifndef __STRIP__ /* See strip.py */ TEST_F(homa_incoming, homa_unknown_pkt__client_resend_start_msg) { struct homa_rpc_unknown_hdr h = {{.sport = htons(self->server_port), @@ -2197,9 +2246,10 @@ TEST_F(homa_incoming, homa_unknown_pkt__client_resend_start_msg) unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit START_MSG 10000", unit_log_get()); } +#endif /* See strip.py */ TEST_F(homa_incoming, homa_unknown_pkt__client_resend_all_data) { struct homa_rpc_unknown_hdr h = {{.sport = htons(self->server_port), @@ -2219,7 +2269,7 @@ TEST_F(homa_incoming, homa_unknown_pkt__client_resend_all_data) IF_NO_STRIP(self->homa.num_priorities = 8); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit DATA retrans 1400@0; xmit DATA retrans 600@1400", unit_log_get()); IF_NO_STRIP(EXPECT_STREQ("6 6", mock_xmit_prios)); @@ -2242,7 +2292,7 @@ TEST_F(homa_incoming, homa_unknown_pkt__client_resend_part) unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit DATA retrans 1400@0; xmit DATA retrans 600@1400", unit_log_get()); EXPECT_EQ(-1, crpc->msgin.length); @@ -2261,7 +2311,7 @@ TEST_F(homa_incoming, homa_unknown_pkt__free_server_rpc) unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("DEAD", homa_symbol_for_state(srpc)); } @@ -2282,7 +2332,7 @@ TEST_F(homa_incoming, homa_cutoffs_pkt_basics) ASSERT_NE(NULL, crpc); unit_log_clear(); - homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, &h.common, 0, 0)); + homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(400, crpc->route->peer->cutoff_version); EXPECT_EQ(9, crpc->route->peer->unsched_cutoffs[1]); EXPECT_EQ(3, crpc->route->peer->unsched_cutoffs[7]); @@ -2325,7 +2375,7 @@ TEST_F(homa_incoming, homa_need_ack_pkt__rpc_response_fully_received) unit_log_clear(); mock_xmit_log_verbose = 1; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit ACK from 0.0.0.0:32768, dport 99, id 1234, acks", unit_log_get()); #ifndef __STRIP__ /* See strip.py */ @@ -2347,7 +2397,7 @@ TEST_F(homa_incoming, homa_need_ack_pkt__rpc_response_not_fully_received) ASSERT_NE(NULL, crpc); unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); #ifndef __STRIP__ /* See strip.py */ EXPECT_STREQ("xmit RESEND 1400, 1600 @0", unit_log_get()); EXPECT_EQ(1, homa_metrics_per_cpu()->packets_received[ @@ -2370,7 +2420,7 @@ TEST_F(homa_incoming, homa_need_ack_pkt__rpc_not_incoming) ASSERT_NE(NULL, crpc); unit_log_clear(); homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); #ifndef __STRIP__ /* See strip.py */ EXPECT_STREQ("xmit RESEND 0, -1 @0", unit_log_get()); EXPECT_EQ(1, homa_metrics_per_cpu()->packets_received[ @@ -2393,7 +2443,7 @@ TEST_F(homa_incoming, homa_need_ack_pkt__rpc_doesnt_exist) route->peer->num_acks = 1; mock_xmit_log_verbose = 1; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_STREQ("xmit ACK from 0.0.0.0:32768, dport 99, id 1234, acks [sp 99, id 1236]", unit_log_get()); homa_route_release(route); @@ -2416,7 +2466,7 @@ TEST_F(homa_incoming, homa_ack_pkt__target_rpc_exists_no_extras) unit_log_clear(); mock_xmit_log_verbose = 1; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(0, unit_list_length(&self->hsk2.active_rpcs)); #ifndef __STRIP__ /* See strip.py */ EXPECT_EQ(1, homa_metrics_per_cpu()->packets_received[ACK - DATA]); @@ -2451,7 +2501,7 @@ TEST_F(homa_incoming, homa_ack_pkt__target_rpc_exists_plus_extras) h.acks[1] = (struct homa_ack) {.server_port = htons(self->server_port), .client_id = cpu_to_be64(self->server_id+3)}; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(0, unit_list_length(&self->hsk2.active_rpcs)); EXPECT_STREQ("DEAD", homa_symbol_for_state(srpc1)); EXPECT_STREQ("DEAD", homa_symbol_for_state(srpc2)); @@ -2494,7 +2544,7 @@ TEST_F(homa_incoming, homa_ack_pkt__reduce_oversize_count) unit_log_clear(); mock_xmit_log_verbose = 1; - homa_dispatch_pkts(skb); + homa_dispatch_pkts(skb, HOMA_PKT_NATIVE); EXPECT_EQ(1, unit_list_length(&self->hsk2.active_rpcs)); EXPECT_STREQ("DEAD", homa_symbol_for_state(srpc1)); EXPECT_STREQ("OUTGOING", homa_symbol_for_state(srpc2)); @@ -2524,7 +2574,7 @@ TEST_F(homa_incoming, homa_ack_pkt__target_rpc_doesnt_exist) h.acks[1] = (struct homa_ack) {.server_port = htons(self->server_port), .client_id = cpu_to_be64(self->server_id+1)}; homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(1, unit_list_length(&self->hsk2.active_rpcs)); EXPECT_STREQ("OUTGOING", homa_symbol_for_state(srpc1)); EXPECT_STREQ("DEAD", homa_symbol_for_state(srpc2)); @@ -2550,13 +2600,13 @@ TEST_F(homa_incoming, homa_start_msg_pkt) /* First attempt: RPC is in wrong state. */ crpc->state = RPC_INCOMING; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(-1, crpc->msgin.length); /* Second attempt: RPC is in correct state. */ crpc->state = RPC_OUTGOING; homa_dispatch_pkts(mock_skb_alloc(self->server_ip, self->client_ip, - &h.common, 0, 0)); + &h.common, 0, 0), HOMA_PKT_NATIVE); EXPECT_EQ(1000, crpc->msgin.length); EXPECT_EQ(RPC_INCOMING, crpc->state); } @@ -2606,7 +2656,8 @@ TEST_F(homa_incoming, homa_abort_rpcs__basics) ASSERT_NE(NULL, crpc2); ASSERT_NE(NULL, crpc3); unit_log_clear(); - homa_abort_rpcs(&self->homa, self->server_ip, 0, -EPROTONOSUPPORT); + homa_abort_rpcs(&self->homa, self->server_ip, 0, -EPROTONOSUPPORT, + IPPROTO_HOMA); EXPECT_EQ(2, unit_list_length(&self->hsk.ready_rpcs)); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(EPROTONOSUPPORT, -crpc1->error); @@ -2631,7 +2682,8 @@ TEST_F(homa_incoming, homa_abort_rpcs__multiple_sockets) ASSERT_NE(NULL, crpc2); ASSERT_NE(NULL, crpc3); unit_log_clear(); - homa_abort_rpcs(&self->homa, self->server_ip, 0, -EPROTONOSUPPORT); + homa_abort_rpcs(&self->homa, self->server_ip, 0, -EPROTONOSUPPORT, + IPPROTO_HOMA); EXPECT_EQ(1, unit_list_length(&self->hsk.ready_rpcs)); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(EPROTONOSUPPORT, -crpc1->error); @@ -2658,7 +2710,7 @@ TEST_F(homa_incoming, homa_abort_rpcs__select_addr) ASSERT_NE(NULL, crpc3); unit_log_clear(); homa_abort_rpcs(&self->homa, self->server_ip, self->server_port, - -ENOTCONN); + -ENOTCONN, IPPROTO_HOMA); EXPECT_EQ(1, unit_list_length(&self->hsk.ready_rpcs)); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(RPC_OUTGOING, crpc2->state); @@ -2681,7 +2733,7 @@ TEST_F(homa_incoming, homa_abort_rpcs__select_port) ASSERT_NE(NULL, crpc3); unit_log_clear(); homa_abort_rpcs(&self->homa, self->server_ip, self->server_port, - -ENOTCONN); + -ENOTCONN, IPPROTO_HOMA); EXPECT_EQ(2, unit_list_length(&self->hsk.ready_rpcs)); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(ENOTCONN, -crpc1->error); @@ -2705,7 +2757,8 @@ TEST_F(homa_incoming, homa_abort_rpcs__any_port) ASSERT_NE(NULL, crpc2); ASSERT_NE(NULL, crpc3); unit_log_clear(); - homa_abort_rpcs(&self->homa, self->server_ip, 0, -ENOTCONN); + homa_abort_rpcs(&self->homa, self->server_ip, 0, -ENOTCONN, + IPPROTO_HOMA); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(0, list_empty(&crpc2->ready_links)); EXPECT_EQ(0, list_empty(&crpc3->ready_links)); @@ -2720,7 +2773,8 @@ TEST_F(homa_incoming, homa_abort_rpcs__ignore_dead_rpcs) homa_rpc_end(crpc); EXPECT_EQ(RPC_DEAD, crpc->state); unit_log_clear(); - homa_abort_rpcs(&self->homa, self->server_ip, 0, -ENOTCONN); + homa_abort_rpcs(&self->homa, self->server_ip, 0, -ENOTCONN, + IPPROTO_HOMA); EXPECT_EQ(-EINVAL, crpc->error); } TEST_F(homa_incoming, homa_abort_rpcs__free_server_rpc) @@ -2731,7 +2785,7 @@ TEST_F(homa_incoming, homa_abort_rpcs__free_server_rpc) ASSERT_NE(NULL, srpc); unit_log_clear(); - homa_abort_rpcs(&self->homa, self->client_ip, 0, 0); + homa_abort_rpcs(&self->homa, self->client_ip, 0, 0, IPPROTO_HOMA); EXPECT_EQ(RPC_DEAD, srpc->state); } diff --git a/test/unit_homa_offload.c b/test/unit_homa_offload.c index e99ff632..90b2a50d 100644 --- a/test/unit_homa_offload.c +++ b/test/unit_homa_offload.c @@ -57,10 +57,10 @@ FIXTURE_SETUP(homa_offload) self->header.msg_length = htonl(10000); self->header.seg.offset = htonl(2000); for (i = 0; i < GRO_HASH_BUCKETS; i++) { - INIT_LIST_HEAD(&self->napi.gro.hash[i].list); - self->napi.gro.hash[i].count = 0; + INIT_LIST_HEAD(&self->napi.gro_hash[i].list); + self->napi.gro_hash[i].count = 0; } - self->napi.gro.bitmask = 0; + self->napi.gro_bitmask = 0; self->skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, &self->header.common, 1400, 2000); @@ -75,10 +75,10 @@ FIXTURE_SETUP(homa_offload) NAPI_GRO_CB(self->skb2)->same_flow = 0; NAPI_GRO_CB(self->skb2)->last = self->skb2; NAPI_GRO_CB(self->skb2)->count = 1; - self->napi.gro.bitmask = 6; - self->napi.gro.hash[2].count = 2; - list_add_tail(&self->skb->list, &self->napi.gro.hash[2].list); - list_add_tail(&self->skb2->list, &self->napi.gro.hash[2].list); + self->napi.gro_bitmask = 6; + self->napi.gro_hash[2].count = 2; + list_add_tail(&self->skb->list, &self->napi.gro_hash[2].list); + list_add_tail(&self->skb2->list, &self->napi.gro_hash[2].list); INIT_LIST_HEAD(&self->empty_list); self->tcp_offloads.callbacks.gro_receive = test_tcp_gro_receive; inet_offloads[IPPROTO_TCP] = &self->tcp_offloads; @@ -98,7 +98,7 @@ FIXTURE_TEARDOWN(homa_offload) struct sk_buff *skb, *tmp; homa_offload_end(); - list_for_each_entry_safe(skb, tmp, &self->napi.gro.hash[2].list, list) + list_for_each_entry_safe(skb, tmp, &self->napi.gro_hash[2].list, list) kfree_skb(skb); homa_destroy(&self->homa); unit_teardown(); @@ -255,7 +255,7 @@ TEST_F(homa_offload, homa_gro_receive__no_held_skb) NAPI_GRO_CB(skb)->same_flow = 0; cur_offload_core->held_skb = NULL; cur_offload_core->held_bucket = 2; - EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro.hash[2].list, skb)); + EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro_hash[2].list, skb)); same_flow = NAPI_GRO_CB(skb)->same_flow; EXPECT_EQ(0, same_flow); EXPECT_EQ(skb, cur_offload_core->held_skb); @@ -274,7 +274,7 @@ TEST_F(homa_offload, homa_gro_receive__empty_merge_list) NAPI_GRO_CB(skb)->same_flow = 0; cur_offload_core->held_skb = self->skb; cur_offload_core->held_bucket = 3; - EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro.hash[2].list, skb)); + EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro_hash[2].list, skb)); same_flow = NAPI_GRO_CB(skb)->same_flow; EXPECT_EQ(0, same_flow); EXPECT_EQ(skb, cur_offload_core->held_skb); @@ -292,7 +292,7 @@ TEST_F(homa_offload, homa_gro_receive__held_skb_not_in_merge_list) NAPI_GRO_CB(skb)->same_flow = 0; cur_offload_core->held_skb = skb; cur_offload_core->held_bucket = 2; - EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro.hash[3].list, skb)); + EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro_hash[3].list, skb)); same_flow = NAPI_GRO_CB(skb)->same_flow; EXPECT_EQ(0, same_flow); EXPECT_EQ(skb, cur_offload_core->held_skb); @@ -315,7 +315,7 @@ TEST_F(homa_offload, homa_gro_receive__held_skb__in_merge_list_but_wrong_proto) else ip_hdr(self->skb)->protocol = IPPROTO_TCP; cur_offload_core->held_bucket = 2; - EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro.hash[3].list, skb)); + EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro_hash[3].list, skb)); same_flow = NAPI_GRO_CB(skb)->same_flow; EXPECT_EQ(0, same_flow); EXPECT_EQ(skb, cur_offload_core->held_skb); @@ -335,7 +335,7 @@ TEST_F(homa_offload, homa_gro_receive__merge) skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, &self->header.common, 1400, 0); NAPI_GRO_CB(skb)->same_flow = 0; - EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro.hash[3].list, skb)); + EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro_hash[3].list, skb)); same_flow = NAPI_GRO_CB(skb)->same_flow; EXPECT_EQ(1, same_flow); EXPECT_EQ(2, NAPI_GRO_CB(self->skb2)->count); @@ -345,7 +345,7 @@ TEST_F(homa_offload, homa_gro_receive__merge) skb2 = mock_skb_alloc(&self->src_ip, &self->dst_ip, &self->header.common, 1400, 0); NAPI_GRO_CB(skb2)->same_flow = 0; - EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro.hash[3].list, skb2)); + EXPECT_EQ(NULL, homa_gro_receive(&self->napi.gro_hash[3].list, skb2)); same_flow = NAPI_GRO_CB(skb)->same_flow; EXPECT_EQ(1, same_flow); EXPECT_EQ(3, NAPI_GRO_CB(self->skb2)->count); @@ -366,9 +366,9 @@ TEST_F(homa_offload, homa_gro_receive__max_gro_skbs) self->header.seg.offset = htonl(6000); skb = mock_skb_alloc(&self->src_ip, &self->dst_ip, &self->header.common, 1400, 0); - homa_gro_receive(&self->napi.gro.hash[3].list, skb); + homa_gro_receive(&self->napi.gro_hash[3].list, skb); EXPECT_EQ(2, NAPI_GRO_CB(self->skb2)->count); - EXPECT_EQ(2, self->napi.gro.hash[2].count); + EXPECT_EQ(2, self->napi.gro_hash[2].count); // Second packet hits the limit. self->header.common.sport = htons(40001); @@ -376,14 +376,14 @@ TEST_F(homa_offload, homa_gro_receive__max_gro_skbs) 1400, 0); unit_log_clear(); EXPECT_EQ(EINPROGRESS, -PTR_ERR(homa_gro_receive( - &self->napi.gro.hash[3].list, skb))); + &self->napi.gro_hash[3].list, skb))); EXPECT_EQ(3, NAPI_GRO_CB(self->skb2)->count); - EXPECT_EQ(1, self->napi.gro.hash[2].count); + EXPECT_EQ(1, self->napi.gro_hash[2].count); EXPECT_STREQ("netif_receive_skb, id 1002, offset 4000", unit_log_get()); kfree_skb(self->skb2); - EXPECT_EQ(1, self->napi.gro.hash[2].count); - EXPECT_EQ(6, self->napi.gro.bitmask); + EXPECT_EQ(1, self->napi.gro_hash[2].count); + EXPECT_EQ(6, self->napi.gro_bitmask); // Third packet also hits the limit for skb, causing the bucket // to become empty. @@ -393,10 +393,10 @@ TEST_F(homa_offload, homa_gro_receive__max_gro_skbs) 1400, 0); unit_log_clear(); EXPECT_EQ(EINPROGRESS, -PTR_ERR(homa_gro_receive( - &self->napi.gro.hash[3].list, skb))); + &self->napi.gro_hash[3].list, skb))); EXPECT_EQ(2, NAPI_GRO_CB(self->skb)->count); - EXPECT_EQ(0, self->napi.gro.hash[2].count); - EXPECT_EQ(2, self->napi.gro.bitmask); + EXPECT_EQ(0, self->napi.gro_hash[2].count); + EXPECT_EQ(2, self->napi.gro_bitmask); EXPECT_STREQ("netif_receive_skb, id 1000, offset 2000", unit_log_get()); kfree_skb(self->skb); diff --git a/test/unit_homa_outgoing.c b/test/unit_homa_outgoing.c index c01a3f8d..6279a354 100644 --- a/test/unit_homa_outgoing.c +++ b/test/unit_homa_outgoing.c @@ -180,6 +180,43 @@ TEST_F(homa_outgoing, homa_message_out_init__max_gso_segs) homa_rpc_unlock(crpc); } +#ifndef __STRIP__ /* See strip.py */ +TEST_F(homa_outgoing, homa_message_out_init__udp_hijacked_geometry) +{ + struct homa_rpc *crpc; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + crpc = homa_rpc_alloc_client(&self->hsk, &self->server_addr); + ASSERT_NE(NULL, crpc); + ASSERT_FALSE(IS_ERR(crpc)); + + EXPECT_EQ(0, homa_message_out_init(crpc, 10000)); + EXPECT_EQ(1, crpc->msgout.max_gso_segs); + EXPECT_EQ(crpc->msgout.max_seg_data, crpc->msgout.max_gso_data); + EXPECT_EQ(mock_mtu - self->hsk.ip_header_length - + sizeof(struct udphdr) - sizeof(struct homa_data_hdr), + crpc->msgout.max_seg_data); + + homa_rpc_unlock(crpc); +} +TEST_F(homa_outgoing, homa_message_out_init__udp_hijacked_mtu_too_small) +{ + struct homa_rpc *crpc; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + crpc = homa_rpc_alloc_client(&self->hsk, &self->server_addr); + ASSERT_NE(NULL, crpc); + ASSERT_FALSE(IS_ERR(crpc)); + mock_mtu = self->hsk.ip_header_length + sizeof(struct udphdr) + + sizeof(struct homa_data_hdr); + + EXPECT_EQ(EMSGSIZE, -homa_message_out_init(crpc, 10000)); + + homa_rpc_unlock(crpc); +} +#endif /* See strip.py */ TEST_F(homa_outgoing, homa_tx_copy_from_user__basics) { @@ -661,7 +698,8 @@ TEST_F(homa_outgoing, homa_tx_skb_alloc__shinfo_gso_fields) shinfo = skb_shinfo(skb); EXPECT_EQ(3, shinfo->gso_segs); EXPECT_EQ(1400 + sizeof(struct homa_seg_hdr), shinfo->gso_size); - EXPECT_EQ(SKB_GSO_TCPV6, shinfo->gso_type); + EXPECT_EQ(mock_ipv6 ? SKB_GSO_TCPV6 : SKB_GSO_TCPV4, + shinfo->gso_type); kfree_skb(skb); } TEST_F(homa_outgoing, homa_tx_skb_alloc__homa_info_fields) @@ -880,6 +918,81 @@ TEST_F(homa_outgoing, homa_tx_skb_send__ipv6_transmit_error) IF_NO_STRIP(EXPECT_EQ(1, homa_metrics_per_cpu()->data_xmit_errors)); homa_rpc_unlock(crpc); } +TEST_F(homa_outgoing, homa_tx_skb_send__ipv4_call_homa_hijack_prepend_udp) +{ + struct homa_rpc *crpc; + u32 end; + + // Make sure the test uses IPv4. + mock_ipv6 = false; + unit_sock_destroy(&self->hsk); + mock_sock_init(&self->hsk, self->hnet, self->client_port); + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + + crpc = unit_client_rpc(&self->hsk, UNIT_OUTGOING, self->client_ip, + self->server_ip, self->server_port, + self->client_id, 1000, 100); + ASSERT_NE(NULL, crpc); + EXPECT_EQ(1, crpc->msgout.max_gso_segs); + mock_xmit_log_udp_hijack = 1; + unit_log_clear(); + + homa_rpc_lock(crpc); + end = 1000; + EXPECT_EQ(0, homa_tx_skb_send(crpc, 0, &end)); + EXPECT_SUBSTR("udp hijack sport 54321, dport 54321,", unit_log_get()); + EXPECT_SUBSTR("checksum 555", unit_log_get()); + homa_rpc_unlock(crpc); +} +TEST_F(homa_outgoing, homa_tx_skb_send__ipv6_call_homa_hijack_prepend_udp) +{ + struct homa_rpc *crpc; + u32 end; + struct in6_addr addr; + + ASSERT_EQ(1, inet_pton(AF_INET6, "2001:44::1", &addr)); + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + + crpc = unit_client_rpc(&self->hsk, UNIT_OUTGOING, self->client_ip, + &addr, self->server_port, + self->client_id, 1000, 100); + ASSERT_NE(NULL, crpc); + EXPECT_EQ(1, crpc->msgout.max_gso_segs); + mock_xmit_log_udp_hijack = 1; + unit_log_clear(); + + homa_rpc_lock(crpc); + end = 1000; + EXPECT_EQ(0, homa_tx_skb_send(crpc, 0, &end)); + EXPECT_SUBSTR("udp hijack sport 54321, dport 54321,", unit_log_get()); + EXPECT_SUBSTR("checksum 777", unit_log_get()); + homa_rpc_unlock(crpc); +} +TEST_F(homa_outgoing, homa_tx_skb_send__udp_retransmit) +{ + struct homa_rpc *crpc; + u32 end; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + crpc = unit_client_rpc(&self->hsk, UNIT_OUTGOING, self->client_ip, + self->server_ip, self->server_port, + self->client_id, 3000, 100); + ASSERT_NE(NULL, crpc); + mock_xmit_log_verbose = 1; + mock_xmit_log_udp_hijack = 1; + crpc->msgout.next_xmit_offset = 1400; + + homa_rpc_lock(crpc); + end = 1400; + EXPECT_EQ(0, homa_tx_skb_send(crpc, 0, &end)); + EXPECT_SUBSTR("RETRANSMIT", unit_log_get()); + EXPECT_SUBSTR("udp hijack sport 54321, dport 54321", unit_log_get()); + EXPECT_EQ(1392, end); + homa_rpc_unlock(crpc); +} #endif /* See strip.py */ TEST_F(homa_outgoing, homa_xmit_control__busy_from_server_request) @@ -1054,6 +1167,88 @@ TEST_F(homa_outgoing, __homa_xmit_control__ipv4_error) IF_NO_STRIP(EXPECT_EQ(1, homa_metrics_per_cpu()->control_xmit_errors)); homa_rpc_unlock(srpc); } +TEST_F(homa_outgoing, __homa_xmit_control__udp_hijacked_mtu_too_small) +{ + struct homa_busy_hdr h; + struct homa_rpc *srpc; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + srpc = unit_server_rpc(&self->hsk, UNIT_RCVD_ONE_PKT, self->client_ip, + self->server_ip, self->client_port, 1111, 10000, 10000); + ASSERT_NE(NULL, srpc); + unit_log_clear(); + + h.common.type = BUSY; + mock_mtu = self->hsk.ip_header_length + sizeof(struct udphdr) + + HOMA_MIN_PKT_LENGTH - 1; + EXPECT_EQ(EMSGSIZE, -__homa_xmit_control(&h, sizeof(h), srpc->route, + &self->hsk)); + EXPECT_STREQ("", unit_log_get()); +} +TEST_F(homa_outgoing, __homa_xmit_control__udp_hijacked_ok) +{ + struct homa_grant_hdr h; + struct homa_rpc *srpc; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + srpc = unit_server_rpc(&self->hsk, UNIT_RCVD_ONE_PKT, self->client_ip, + self->server_ip, self->client_port, 1111, 10000, 10000); + ASSERT_NE(NULL, srpc); + unit_log_clear(); + + h.offset = htonl(12345); + h.priority = 4; + mock_xmit_log_udp_hijack = 1; + homa_rpc_lock(srpc); + EXPECT_EQ(0, -homa_xmit_control(GRANT, &h, sizeof(h), srpc)); + EXPECT_SUBSTR("udp hijack sport 54321, dport 54321,", unit_log_get()); + homa_rpc_unlock(srpc); +} + +TEST_F(homa_outgoing, __homa_xmit_control__udp_packet_class_matrix) +{ + struct { + enum homa_packet_type type; + size_t length; + } cases[] = { + {GRANT, sizeof(struct homa_grant_hdr)}, + {RESEND, sizeof(struct homa_resend_hdr)}, + {RPC_UNKNOWN, sizeof(struct homa_rpc_unknown_hdr)}, + {BUSY, sizeof(struct homa_busy_hdr)}, + {CUTOFFS, sizeof(struct homa_cutoffs_hdr)}, + {FREEZE, sizeof(struct homa_freeze_hdr)}, + {NEED_ACK, sizeof(struct homa_need_ack_hdr)}, + {ACK, sizeof(struct homa_ack_hdr)}, + {START_MSG, sizeof(struct homa_start_msg_hdr)}, + }; + unsigned char contents[HOMA_MAX_HEADER]; + struct homa_common_hdr *h = (struct homa_common_hdr *)contents; + struct homa_rpc *srpc; + int i; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + srpc = unit_server_rpc(&self->hsk, UNIT_RCVD_ONE_PKT, self->client_ip, + self->server_ip, self->client_port, 1111, + 10000, 10000); + ASSERT_NE(NULL, srpc); + mock_xmit_log_udp_hijack = 1; + + for (i = 0; i < ARRAY_SIZE(cases); i++) { + memset(contents, 0, sizeof(contents)); + h->sport = htons(self->server_port); + h->dport = htons(self->client_port); + h->type = cases[i].type; + h->sender_id = cpu_to_be64(1111); + unit_log_clear(); + EXPECT_EQ(0, __homa_xmit_control(contents, cases[i].length, + srpc->route, &self->hsk)); + EXPECT_SUBSTR("udp hijack sport 54321, dport 54321", + unit_log_get()); + } +} TEST_F(homa_outgoing, homa_xmit_unknown__basics) { @@ -1071,6 +1266,25 @@ TEST_F(homa_outgoing, homa_xmit_unknown__basics) unit_log_get()); kfree_skb(skb); } +TEST_F(homa_outgoing, homa_xmit_unknown__udp_hijacked) +{ + struct homa_grant_hdr h = {{.sport = htons(self->client_port), + .dport = htons(self->server_port), + .sender_id = cpu_to_be64(99990), + .type = GRANT}, + .offset = htonl(11200)}; + struct sk_buff *skb; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + mock_xmit_log_verbose = 1; + mock_xmit_log_udp_hijack = 1; + skb = mock_skb_alloc(self->client_ip, self->server_ip, &h.common, 0, 0); + homa_xmit_unknown(skb, &self->hsk); + EXPECT_SUBSTR("xmit RPC_UNKNOWN", unit_log_get()); + EXPECT_SUBSTR("udp hijack sport 54321, dport 54321", unit_log_get()); + kfree_skb(skb); +} TEST_F(homa_outgoing, homa_xmit_unknown__cant_find_peer) { struct homa_grant_hdr h = {{.sport = htons(self->client_port), diff --git a/test/unit_homa_peer.c b/test/unit_homa_peer.c index df352a80..91dd4226 100644 --- a/test/unit_homa_peer.c +++ b/test/unit_homa_peer.c @@ -361,6 +361,71 @@ TEST_F(homa_peer, homa_route_alloc__route_error_ipv6) #endif /* See strip.py */ } +TEST_F(homa_peer, homa_route_key_init__sk_protocol) +{ + struct homa_route_key key; + + self->hsk.sock.sk_protocol = IPPROTO_HOMA; + homa_route_key_init(&key, &self->hsk, ip1111); + EXPECT_EQ(IPPROTO_HOMA, key.sk_protocol); + + self->hsk.sock.sk_protocol = IPPROTO_UDP; + homa_route_key_init(&key, &self->hsk, ip1111); + EXPECT_EQ(IPPROTO_UDP, key.sk_protocol); +} + +#ifndef __STRIP__ /* See strip.py */ +TEST_F(homa_peer, homa_route_alloc__udp_hijack_proto_and_ports_ipv4) +{ + struct homa_route_key key; + struct homa_route *route; + + mock_ipv6 = false; + unit_sock_destroy(&self->hsk); + mock_sock_init(&self->hsk, self->hnet, 0); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + + homa_route_key_init(&key, &self->hsk, &self->client_ip[0]); + route = homa_route_alloc(&self->hsk, &key); + ASSERT_FALSE(IS_ERR(route)); + EXPECT_EQ(IPPROTO_UDP, route->flow.u.ip4.flowi4_proto); + EXPECT_EQ(htons(HOMA_UDP_HIJACK_PORT), route->flow.u.ip4.fl4_dport); + EXPECT_EQ(htons(HOMA_UDP_HIJACK_PORT), route->flow.u.ip4.fl4_sport); + homa_route_free(&route->rcu_head); +} +TEST_F(homa_peer, homa_route_alloc__udp_hijack_proto_and_ports_ipv6) +{ + struct homa_route_key key; + struct homa_route *route; + + mock_ipv6 = true; + unit_sock_destroy(&self->hsk); + mock_sock_init(&self->hsk, self->hnet, 0); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + + homa_route_key_init(&key, &self->hsk, ip3333); + route = homa_route_alloc(&self->hsk, &key); + ASSERT_FALSE(IS_ERR(route)); + EXPECT_EQ(IPPROTO_UDP, route->flow.u.ip6.flowi6_proto); + EXPECT_EQ(htons(HOMA_UDP_HIJACK_PORT), route->flow.u.ip6.fl6_dport); + EXPECT_EQ(htons(HOMA_UDP_HIJACK_PORT), route->flow.u.ip6.fl6_sport); + homa_route_free(&route->rcu_head); +} +#endif /* See strip.py */ +TEST_F(homa_peer, homa_route_alloc__native_proto_and_zero_ports) +{ + struct homa_route_key key; + struct homa_route *route; + + homa_route_key_init(&key, &self->hsk, ip1111); + route = homa_route_alloc(&self->hsk, &key); + ASSERT_FALSE(IS_ERR(route)); + EXPECT_EQ(IPPROTO_HOMA, route->flow.u.ip6.flowi6_proto); + EXPECT_EQ(0, route->flow.u.ip6.fl6_dport); + EXPECT_EQ(0, route->flow.u.ip6.fl6_sport); + homa_route_free(&route->rcu_head); +} + TEST_F(homa_peer, homa_route_free) { struct homa_route_key key; diff --git a/test/unit_homa_plumbing.c b/test/unit_homa_plumbing.c index 914d23be..e5bed945 100644 --- a/test/unit_homa_plumbing.c +++ b/test/unit_homa_plumbing.c @@ -962,6 +962,7 @@ TEST_F(homa_plumbing, homa_sendmsg__request_sent_successfully) EXPECT_EQ(88888, crpc->completion_cookie); homa_rpc_unlock(crpc); } +#ifndef __STRIP__ /* See strip.py */ TEST_F(homa_plumbing, homa_sendmsg__send_start_msg_packet_for_scheduled_msg) { self->homa.unsched_bytes = 100; @@ -970,7 +971,6 @@ TEST_F(homa_plumbing, homa_sendmsg__send_start_msg_packet_for_scheduled_msg) EXPECT_SUBSTR("xmit START_MSG 200", unit_log_get()); ASSERT_EQ(1, unit_list_length(&self->hsk.active_rpcs)); } -#ifndef __STRIP__ /* See strip.py */ TEST_F(homa_plumbing, homa_sendmsg__request_metrics) { EXPECT_EQ(0, -homa_sendmsg(&self->hsk.inet.sk, @@ -1035,6 +1035,7 @@ TEST_F(homa_plumbing, homa_sendmsg__response_wrong_state) EXPECT_EQ(RPC_INCOMING, srpc->state); EXPECT_EQ(1, unit_list_length(&self->hsk.active_rpcs)); } +#ifndef __STRIP__ /* See strip.py */ TEST_F(homa_plumbing, homa_sendmsg__send_start_msg_for_scheduled_response) { unit_server_rpc(&self->hsk, UNIT_IN_SERVICE, self->client_ip, @@ -1047,6 +1048,7 @@ TEST_F(homa_plumbing, homa_sendmsg__send_start_msg_for_scheduled_response) EXPECT_SUBSTR("xmit START_MSG 200", unit_log_get()); EXPECT_EQ(1, unit_list_length(&self->hsk.active_rpcs)); } +#endif /* See strip.py */ TEST_F(homa_plumbing, homa_sendmsg__homa_tx_copy_from_user_returns_error) { struct homa_rpc *srpc = unit_server_rpc(&self->hsk, UNIT_IN_SERVICE, diff --git a/test/unit_homa_pool.c b/test/unit_homa_pool.c index f56bc8ce..2dc53f61 100644 --- a/test/unit_homa_pool.c +++ b/test/unit_homa_pool.c @@ -762,6 +762,7 @@ TEST_F(homa_pool, homa_pool_check_waiting__reset_bpages_needed) EXPECT_EQ(0, crpc2->msgin.num_bpages); EXPECT_EQ(2, pool->bpages_needed); } +#ifndef __STRIP__ /* See strip.py */ TEST_F(homa_pool, homa_pool_check_waiting__wake_up_waiting_rpc_unscheduled) { struct homa_pool *pool = self->hsk.buffer_pool; @@ -810,6 +811,7 @@ TEST_F(homa_pool, homa_pool_check_waiting__wake_up_waiting_rpc_scheduled) EXPECT_EQ(2, crpc->msgin.num_bpages); EXPECT_SUBSTR("xmit GRANT 50000@0", unit_log_get()); } +#endif /* See strip.py */ TEST_F(homa_pool, homa_pool_check_waiting__reallocation_fails) { struct homa_pool *pool = self->hsk.buffer_pool; @@ -833,6 +835,7 @@ TEST_F(homa_pool, homa_pool_check_waiting__reallocation_fails) EXPECT_EQ(4, pool->bpages_needed); } +#ifndef __STRIP__ /* See strip.py */ TEST_F(homa_pool, homa_pool_wakeup_rpc__scheduled) { struct homa_rpc *srpc; @@ -848,6 +851,7 @@ TEST_F(homa_pool, homa_pool_wakeup_rpc__scheduled) EXPECT_STREQ("xmit GRANT 20000@0", unit_log_get()); homa_rpc_unlock(srpc); } +#endif /* See strip.py */ TEST_F(homa_pool, homa_pool_wakeup_rpc__unscheduled) { struct homa_rpc *srpc; @@ -859,7 +863,11 @@ TEST_F(homa_pool, homa_pool_wakeup_rpc__unscheduled) unit_log_clear(); homa_rpc_lock(srpc); homa_pool_wakeup_rpc(srpc); +#ifndef __STRIP__ /* See strip.py */ EXPECT_STREQ("xmit RESEND 0, 200 @6", unit_log_get()); +#else /* See strip.py */ + EXPECT_STREQ("xmit RESEND 0, 200", unit_log_get()); +#endif /* See strip.py */ homa_rpc_unlock(srpc); } diff --git a/test/unit_homa_qdisc.c b/test/unit_homa_qdisc.c index a4f98c6e..d1d1c8a3 100644 --- a/test/unit_homa_qdisc.c +++ b/test/unit_homa_qdisc.c @@ -723,6 +723,80 @@ TEST_F(homa_qdisc, homa_qdisc_enqueue__defer_homa_packet_because_of_nic_queue_co EXPECT_TRUE(homa_qdisc_any_deferred(q->qdev)); EXPECT_STREQ("[id 1234, offsets 0]", unit_log_deferred(q->qdev)); } +#ifndef __STRIP__ /* See strip.py */ +TEST_F(homa_qdisc, homa_qdisc_enqueue__defer_udp_homa_packet) +{ + struct homa_qdisc *q = init_qdisc(self->qdiscs[3]); + struct sk_buff *skb, *to_free; + struct homa_rpc *crpc; + struct udphdr *uh; + + crpc = unit_client_rpc(&self->hsk, UNIT_OUTGOING, &self->client_ip, + &self->server_ip, self->server_port, + self->client_id, 7100, 100); + ASSERT_NE(NULL, crpc); + mock_ipv6 = false; + skb = new_test_skb(crpc, &self->addr, &self->addr2, 1400, 1500); + uh = skb_push(skb, sizeof(*uh)); + skb_reset_transport_header(skb); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT); + uh->len = htons(skb->len); + ip_hdr(skb)->protocol = IPPROTO_UDP; + to_free = NULL; + mock_log_wakeups = 1; + atomic_set(&q->qdev->total_nic_queue, 1500); + q->qdev->max_nic_queue_bytes = 1499; + + unit_log_clear(); + EXPECT_EQ(NET_XMIT_SUCCESS, + homa_qdisc_enqueue(skb, q->qdisc, &to_free)); + EXPECT_EQ(NULL, to_free); + EXPECT_TRUE(homa_qdisc_any_deferred(q->qdev)); + EXPECT_STREQ("[id 1234, offsets 1400]", unit_log_deferred(q->qdev)); +} +TEST_F(homa_qdisc, homa_qdisc_enqueue__defer_nonlinear_udp_homa_packet) +{ + struct homa_qdisc *q = init_qdisc(self->qdiscs[3]); + struct skb_shared_info *shinfo; + struct sk_buff *skb, *to_free; + struct homa_rpc *crpc; + struct udphdr *uh; + int inner_length; + + crpc = unit_client_rpc(&self->hsk, UNIT_OUTGOING, &self->client_ip, + &self->server_ip, self->server_port, + self->client_id, 7100, 100); + ASSERT_NE(NULL, crpc); + mock_ipv6 = false; + skb = new_test_skb(crpc, &self->addr, &self->addr2, 1400, 1500); + inner_length = skb->len; + shinfo = skb_shinfo(skb); + unit_alloc_frags(1, shinfo->frags, 0, inner_length); + memcpy(unit_frag_first_byte(&shinfo->frags[0]), skb->data, + inner_length); + shinfo->nr_frags = 1; + uh = skb_push(skb, sizeof(*uh)); + skb_reset_transport_header(skb); + uh->source = htons(HOMA_UDP_HIJACK_PORT); + uh->dest = htons(HOMA_UDP_HIJACK_PORT); + uh->len = htons(skb->len); + ip_hdr(skb)->protocol = IPPROTO_UDP; + skb_set_tail_pointer(skb, sizeof(*uh)); + skb->data_len = inner_length; + to_free = NULL; + mock_log_wakeups = 1; + atomic_set(&q->qdev->total_nic_queue, 1500); + q->qdev->max_nic_queue_bytes = 1499; + + unit_log_clear(); + EXPECT_EQ(NET_XMIT_SUCCESS, + homa_qdisc_enqueue(skb, q->qdisc, &to_free)); + EXPECT_EQ(NULL, to_free); + EXPECT_TRUE(homa_qdisc_any_deferred(q->qdev)); + EXPECT_STREQ("[id 1234, offsets 1400]", unit_log_deferred(q->qdev)); +} +#endif /* See strip.py */ TEST_F(homa_qdisc, homa_qdisc_enqueue__defer_homa_packet_other_packets_deferred) { struct homa_qdisc *q = init_qdisc(self->qdiscs[3]); diff --git a/test/unit_homa_rpc.c b/test/unit_homa_rpc.c index 18210126..58de78cb 100644 --- a/test/unit_homa_rpc.c +++ b/test/unit_homa_rpc.c @@ -129,6 +129,50 @@ TEST_F(homa_rpc, homa_rpc_alloc_client__socket_shutdown) EXPECT_STREQ("socket has been shut down", self->hsk.error_msg); self->hsk.shutdown = 0; } +#ifndef __STRIP__ /* See strip.py */ +TEST_F(homa_rpc, homa_rpc_alloc_client__udp_hijack_admitted) +{ + struct homa_rpc *crpc; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + crpc = homa_rpc_alloc_client(&self->hsk, &self->server_addr); + ASSERT_FALSE(IS_ERR(crpc)); + EXPECT_EQ(1, crpc->udp_admitted); + EXPECT_EQ(1, atomic_read(&self->hnet->udp_rpc_count)); + homa_rpc_end(crpc); + homa_rpc_unlock(crpc); + EXPECT_EQ(0, crpc->udp_admitted); + EXPECT_EQ(0, atomic_read(&self->hnet->udp_rpc_count)); +} +TEST_F(homa_rpc, homa_rpc_alloc_client__udp_hijack_disabled_rejects) +{ + struct homa_rpc *crpc; + + self->hnet->udp_state = HOMA_UDP_DRAINING; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + crpc = homa_rpc_alloc_client(&self->hsk, &self->server_addr); + EXPECT_TRUE(IS_ERR(crpc)); + EXPECT_EQ(ENETDOWN, -PTR_ERR(crpc)); + EXPECT_STREQ("UDP hijacking has been disabled", self->hsk.error_msg); + EXPECT_EQ(0, atomic_read(&self->hnet->udp_rpc_count)); +} +TEST_F(homa_rpc, homa_rpc_alloc_server__udp_hijack_admitted) +{ + struct homa_rpc *srpc; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + self->hsk.sock.sk_protocol = IPPROTO_UDP; + srpc = homa_rpc_alloc_server(&self->hsk, self->client_ip, + &self->data.common); + ASSERT_FALSE(IS_ERR(srpc)); + EXPECT_EQ(1, srpc->udp_admitted); + EXPECT_EQ(1, atomic_read(&self->hnet->udp_rpc_count)); + homa_rpc_unlock(srpc); + homa_rpc_end(srpc); + EXPECT_EQ(0, atomic_read(&self->hnet->udp_rpc_count)); +} +#endif /* See strip.py */ TEST_F(homa_rpc, homa_rpc_alloc_server__basics_from_data_packet) { @@ -140,11 +184,12 @@ TEST_F(homa_rpc, homa_rpc_alloc_server__basics_from_data_packet) homa_rpc_unlock(srpc); EXPECT_EQ(RPC_INCOMING, srpc->state); EXPECT_EQ(10000, srpc->msgin.length); - EXPECT_EQ(10000, srpc->msgin.granted); + IF_NO_STRIP(EXPECT_EQ(10000, srpc->msgin.granted)); EXPECT_EQ(RPC_INCOMING, srpc->state); EXPECT_EQ(1, unit_list_length(&self->hsk.active_rpcs)); homa_rpc_end(srpc); } +#ifndef __STRIP__ /* See strip.py */ TEST_F(homa_rpc, homa_rpc_alloc_server__basics_from_start_msg_packet) { struct homa_start_msg_hdr h; @@ -166,6 +211,7 @@ TEST_F(homa_rpc, homa_rpc_alloc_server__basics_from_start_msg_packet) EXPECT_EQ(1, unit_list_length(&self->hsk.active_rpcs)); homa_rpc_end(srpc); } +#endif /* See strip.py */ TEST_F(homa_rpc, homa_rpc_alloc_server__no_buffer_pool) { struct homa_rpc *srpc; @@ -287,6 +333,7 @@ TEST_F(homa_rpc, homa_rpc_alloc_server__dont_handoff_no_buffers) EXPECT_EQ(0, unit_list_length(&self->hsk.ready_rpcs)); homa_rpc_end(srpc); } +#ifndef __STRIP__ /* See strip.py */ TEST_F(homa_rpc, homa_rpc_alloc_server__dont_handoff_rpc_start_msg) { struct homa_start_msg_hdr h; @@ -305,6 +352,7 @@ TEST_F(homa_rpc, homa_rpc_alloc_server__dont_handoff_rpc_start_msg) EXPECT_EQ(0, unit_list_length(&self->hsk.ready_rpcs)); homa_rpc_end(srpc); } +#endif /* See strip.py */ #ifndef __STRIP__ /* See strip.py */ TEST_F(homa_rpc, homa_bucket_lock_slow) @@ -440,7 +488,11 @@ TEST_F(homa_rpc, homa_rpc_ack__cant_find_rpc) TEST_F(homa_rpc, homa_rpc_end__basics) { struct homa_rpc *crpc = unit_client_rpc(&self->hsk, +#ifndef __STRIP__ /* See strip.py */ UNIT_RCVD_START_MSG, self->client_ip, self->server_ip, +#else /* See strip.py */ + UNIT_RCVD_ONE_PKT, self->client_ip, self->server_ip, +#endif /* See strip.py */ self->server_port, self->client_id, 1000, 20000); #ifndef __STRIP__ /* See strip.py */ @@ -604,7 +656,8 @@ TEST_F(homa_rpc, homa_abort_rpcs__basics) ASSERT_NE(NULL, crpc2); ASSERT_NE(NULL, crpc3); unit_log_clear(); - homa_abort_rpcs(&self->homa, self->server_ip, 0, -EPROTONOSUPPORT); + homa_abort_rpcs(&self->homa, self->server_ip, 0, -EPROTONOSUPPORT, + IPPROTO_HOMA); EXPECT_EQ(2, unit_list_length(&self->hsk.ready_rpcs)); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(EPROTONOSUPPORT, -crpc1->error); @@ -631,7 +684,8 @@ TEST_F(homa_rpc, homa_abort_rpcs__multiple_sockets) ASSERT_NE(NULL, crpc2); ASSERT_NE(NULL, crpc3); unit_log_clear(); - homa_abort_rpcs(&self->homa, self->server_ip, 0, -EPROTONOSUPPORT); + homa_abort_rpcs(&self->homa, self->server_ip, 0, -EPROTONOSUPPORT, + IPPROTO_HOMA); EXPECT_EQ(1, unit_list_length(&self->hsk.ready_rpcs)); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(EPROTONOSUPPORT, -crpc1->error); @@ -642,6 +696,37 @@ TEST_F(homa_rpc, homa_abort_rpcs__multiple_sockets) EXPECT_EQ(2, unit_list_length(&hsk.ready_rpcs)); unit_sock_destroy(&hsk); } +#ifndef __STRIP__ /* See strip.py */ +TEST_F(homa_rpc, homa_abort_rpcs__select_protocol) +{ + struct homa_rpc *native_rpc, *udp_rpc; + struct homa_sock udp_hsk; + + mock_sock_init(&udp_hsk, self->hnet, self->server_port + 1); + udp_hsk.sock.sk_protocol = IPPROTO_UDP; + self->hnet->udp_state = HOMA_UDP_ENABLED; + native_rpc = unit_client_rpc(&self->hsk, UNIT_OUTGOING, + self->client_ip, self->server_ip, self->server_port, + self->client_id, 5000, 1600); + udp_rpc = unit_client_rpc(&udp_hsk, UNIT_OUTGOING, self->client_ip, + self->server_ip, self->server_port, self->client_id + 2, + 5000, 1600); + ASSERT_NE(NULL, native_rpc); + ASSERT_NE(NULL, udp_rpc); + + homa_abort_rpcs(&self->homa, self->server_ip, 0, -EMSGSIZE, + IPPROTO_UDP); + EXPECT_EQ(RPC_OUTGOING, native_rpc->state); + EXPECT_EQ(0, native_rpc->error); + EXPECT_EQ(EMSGSIZE, -udp_rpc->error); + + homa_abort_rpcs(&self->homa, self->server_ip, 0, -EHOSTUNREACH, + IPPROTO_HOMA); + EXPECT_EQ(EHOSTUNREACH, -native_rpc->error); + EXPECT_EQ(EMSGSIZE, -udp_rpc->error); + unit_sock_destroy(&udp_hsk); +} +#endif /* See strip.py */ TEST_F(homa_rpc, homa_abort_rpcs__select_addr) { struct homa_rpc *crpc1 = unit_client_rpc(&self->hsk, @@ -659,7 +744,7 @@ TEST_F(homa_rpc, homa_abort_rpcs__select_addr) ASSERT_NE(NULL, crpc3); unit_log_clear(); homa_abort_rpcs(&self->homa, self->server_ip, self->server_port, - -ENOTCONN); + -ENOTCONN, IPPROTO_HOMA); EXPECT_EQ(1, unit_list_length(&self->hsk.ready_rpcs)); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(RPC_OUTGOING, crpc2->state); @@ -682,7 +767,7 @@ TEST_F(homa_rpc, homa_abort_rpcs__select_port) ASSERT_NE(NULL, crpc3); unit_log_clear(); homa_abort_rpcs(&self->homa, self->server_ip, self->server_port, - -ENOTCONN); + -ENOTCONN, IPPROTO_HOMA); EXPECT_EQ(2, unit_list_length(&self->hsk.ready_rpcs)); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(ENOTCONN, -crpc1->error); @@ -706,7 +791,8 @@ TEST_F(homa_rpc, homa_abort_rpcs__any_port) ASSERT_NE(NULL, crpc2); ASSERT_NE(NULL, crpc3); unit_log_clear(); - homa_abort_rpcs(&self->homa, self->server_ip, 0, -ENOTCONN); + homa_abort_rpcs(&self->homa, self->server_ip, 0, -ENOTCONN, + IPPROTO_HOMA); EXPECT_EQ(0, list_empty(&crpc1->ready_links)); EXPECT_EQ(0, list_empty(&crpc2->ready_links)); EXPECT_EQ(0, list_empty(&crpc3->ready_links)); @@ -720,7 +806,8 @@ TEST_F(homa_rpc, homa_abort_rpcs__ignore_dead_rpcs) ASSERT_NE(NULL, crpc); crpc->state = RPC_DEAD; unit_log_clear(); - homa_abort_rpcs(&self->homa, self->server_ip, 0, -ENOTCONN); + homa_abort_rpcs(&self->homa, self->server_ip, 0, -ENOTCONN, + IPPROTO_HOMA); EXPECT_EQ(0, unit_list_length(&self->hsk.ready_rpcs)); crpc->state = RPC_OUTGOING; } @@ -732,7 +819,7 @@ TEST_F(homa_rpc, homa_abort_rpcs__free_server_rpc) ASSERT_NE(NULL, srpc); unit_log_clear(); - homa_abort_rpcs(&self->homa, self->client_ip, 0, 0); + homa_abort_rpcs(&self->homa, self->client_ip, 0, 0, IPPROTO_HOMA); EXPECT_EQ(RPC_DEAD, srpc->state); } @@ -1418,20 +1505,32 @@ TEST_F(homa_rpc, homa_rpc_find_from_skb__server_ipv4_outgoing) TEST_F(homa_rpc, homa_rpc_get_info__basics) { struct homa_rpc *crpc = unit_client_rpc(&self->hsk, +#ifndef __STRIP__ /* See strip.py */ UNIT_RCVD_START_MSG, self->client_ip, self->server_ip, +#else /* See strip.py */ + UNIT_RCVD_ONE_PKT, self->client_ip, self->server_ip, +#endif /* See strip.py */ self->server_port, self->client_id, 1000, 20000); struct homa_rpc_info info; crpc->completion_cookie = 1111; - crpc->msgout.priority = 4; + IF_NO_STRIP(crpc->msgout.priority = 4); homa_rpc_get_info(crpc, &info); - EXPECT_EQ(AF_INET6, info.peer.in6.sin6_family); - EXPECT_EQ(0, info.peer.in6.sin6_addr.in6_u.u6_addr32[0]); - EXPECT_EQ(0, info.peer.in6.sin6_addr.in6_u.u6_addr32[1]); - EXPECT_EQ(htonl(0x0000ffff), info.peer.in6.sin6_addr.in6_u.u6_addr32[2]); - EXPECT_EQ(0x04030201, info.peer.in6.sin6_addr.in6_u.u6_addr32[3]); - EXPECT_EQ(99, ntohs(info.peer.in6.sin6_port)); + if (mock_ipv6) { + EXPECT_EQ(AF_INET6, info.peer.in6.sin6_family); + EXPECT_EQ(0, info.peer.in6.sin6_addr.in6_u.u6_addr32[0]); + EXPECT_EQ(0, info.peer.in6.sin6_addr.in6_u.u6_addr32[1]); + EXPECT_EQ(htonl(0x0000ffff), + info.peer.in6.sin6_addr.in6_u.u6_addr32[2]); + EXPECT_EQ(htonl(0x01020304), + info.peer.in6.sin6_addr.in6_u.u6_addr32[3]); + EXPECT_EQ(99, ntohs(info.peer.in6.sin6_port)); + } else { + EXPECT_EQ(AF_INET, info.peer.in4.sin_family); + EXPECT_EQ(htonl(0x01020304), info.peer.in4.sin_addr.s_addr); + EXPECT_EQ(99, ntohs(info.peer.in4.sin_port)); + } EXPECT_EQ(1234, info.id); EXPECT_EQ(1111, info.completion_cookie); EXPECT_EQ(1000, info.tx_length); @@ -1439,7 +1538,11 @@ TEST_F(homa_rpc, homa_rpc_get_info__basics) EXPECT_EQ(1000, info.tx_granted); IF_NO_STRIP(EXPECT_EQ(4, info.tx_prio)); EXPECT_EQ(20000, info.rx_length); +#ifndef __STRIP__ /* See strip.py */ EXPECT_EQ(20000, info.rx_remaining); +#else /* See strip.py */ + EXPECT_EQ(20000 - UNIT_TEST_DATA_PER_PACKET, info.rx_remaining); +#endif /* See strip.py */ EXPECT_EQ(0, info.rx_gaps); EXPECT_EQ(0, info.rx_gap_bytes); IF_NO_STRIP(EXPECT_EQ(10000, info.rx_granted)); @@ -1546,7 +1649,7 @@ TEST_F(homa_rpc, homa_rpc_get_info__HOMA_RPC_RX_READY_and_HOMA_RPC_RX_COPY) /* Second call: all bytes received, but haven't been copied out. */ self->data.seg.offset = htonl(1400); homa_dispatch_pkts(mock_skb_alloc(self->client_ip, self->server_ip, - &self->data.common, 1000, 0)); + &self->data.common, 1000, 0), HOMA_PKT_NATIVE); homa_rpc_get_info(srpc, &info); EXPECT_EQ(0, info.rx_remaining); EXPECT_EQ(2, skb_queue_len(&srpc->msgin.packets)); diff --git a/test/unit_homa_sock.c b/test/unit_homa_sock.c index be401480..80edaa28 100644 --- a/test/unit_homa_sock.c +++ b/test/unit_homa_sock.c @@ -383,6 +383,27 @@ TEST_F(homa_sock, homa_sock_init__hijack_tcp) unit_sock_destroy(&hijack); unit_sock_destroy(&no_hijack); } +TEST_F(homa_sock, homa_sock_init__hijack_udp) +{ + struct homa_sock hijack_udp, hijack_tcp, neither; + + self->hnet->udp_state = HOMA_UDP_ENABLED; + mock_sock_init(&hijack_udp, self->hnet, 0); + EXPECT_EQ(IPPROTO_UDP, hijack_udp.sock.sk_protocol); + + self->homa.hijack_tcp = 1; + mock_sock_init(&hijack_tcp, self->hnet, 0); + EXPECT_EQ(IPPROTO_TCP, hijack_tcp.sock.sk_protocol); + self->homa.hijack_tcp = 0; + + self->hnet->udp_state = HOMA_UDP_DISABLED; + mock_sock_init(&neither, self->hnet, 0); + EXPECT_EQ(IPPROTO_HOMA, neither.sock.sk_protocol); + + unit_sock_destroy(&hijack_udp); + unit_sock_destroy(&hijack_tcp); + unit_sock_destroy(&neither); +} #endif /* See strip.py */ TEST_F(homa_sock, homa_sock_link__basics) diff --git a/test/unit_homa_timer.c b/test/unit_homa_timer.c index e7cfac1a..698ef3a7 100644 --- a/test/unit_homa_timer.c +++ b/test/unit_homa_timer.c @@ -139,6 +139,40 @@ TEST_F(homa_timer, homa_timer_check_rpc__server_has_received_request) EXPECT_EQ(0, srpc->silent_ticks); EXPECT_STREQ("", unit_log_get()); } +#ifndef __STRIP__ /* See strip.py */ +TEST_F(homa_timer, homa_timer_check_rpc__udp_drain_deadline) +{ + struct homa_rpc *srpc = unit_server_rpc(&self->hsk, UNIT_IN_SERVICE, + self->client_ip, self->server_ip, self->client_port, + self->server_id, 100, 100); + + ASSERT_NE(NULL, srpc); + self->hsk.sock.sk_protocol = IPPROTO_UDP; + srpc->udp_admitted = true; + atomic_set(&self->hnet->udp_rpc_count, 1); + self->hnet->udp_state = HOMA_UDP_DRAINING; + self->homa.timer_ticks = UINT_MAX - 2; + self->hnet->udp_drain_deadline = self->homa.timer_ticks + 5; + + homa_rpc_lock(srpc); + homa_timer_check_rpc(srpc); + EXPECT_EQ(RPC_IN_SERVICE, srpc->state); + + self->homa.timer_ticks = 1; + homa_timer_check_rpc(srpc); + EXPECT_EQ(RPC_IN_SERVICE, srpc->state); + + self->homa.timer_ticks = self->hnet->udp_drain_deadline; + homa_timer_check_rpc(srpc); + homa_rpc_unlock(srpc); + EXPECT_EQ(RPC_DEAD, srpc->state); + /* Server RPCs ignore the abort error and are just freed by + * homa_rpc_end, which always sets rpc->error to -EINVAL. + */ + EXPECT_EQ(EINVAL, -srpc->error); + EXPECT_EQ(0, atomic_read(&self->hnet->udp_rpc_count)); +} +#endif /* See strip.py */ TEST_F(homa_timer, homa_timer_check_rpc__granted_bytes_not_sent) { struct homa_rpc *crpc = unit_client_rpc(&self->hsk, diff --git a/test/utils.c b/test/utils.c index 2ba372b1..26dbc0ea 100644 --- a/test/utils.c +++ b/test/utils.c @@ -15,6 +15,7 @@ #include "utils.h" #ifndef __STRIP__ /* See strip.py */ +#include "homa_hijack.h" #include "homa_qdisc.h" #endif /* See strip.py */ @@ -69,6 +70,7 @@ struct homa_rpc *unit_client_rpc(struct homa_sock *hsk, return crpc; crpc->msgout.next_xmit_offset = crpc->msgout.length; +#ifndef __STRIP__ /* See strip.py */ if (state == UNIT_RCVD_START_MSG) { struct homa_start_msg_hdr start; memset(&start, 0, sizeof(start)); @@ -81,9 +83,10 @@ struct homa_rpc *unit_client_rpc(struct homa_sock *hsk, start.msg_length = htonl(resp_length); homa_dispatch_pkts(mock_skb_alloc(server_ip, client_ip, - &start.common, 0, 0)); + &start.common, 0, 0), HOMA_PKT_NATIVE); return crpc; } +#endif /* See strip.py */ struct homa_data_hdr h; memset(&h, 0, sizeof(h)); @@ -98,7 +101,7 @@ struct homa_rpc *unit_client_rpc(struct homa_sock *hsk, this_size = (resp_length > UNIT_TEST_DATA_PER_PACKET) ? UNIT_TEST_DATA_PER_PACKET : resp_length; homa_dispatch_pkts(mock_skb_alloc(server_ip, client_ip, &h.common, - this_size, 0)); + this_size, 0), HOMA_PKT_NATIVE); if (state == UNIT_RCVD_ONE_PKT) return crpc; for (bytes_received = UNIT_TEST_DATA_PER_PACKET; @@ -109,7 +112,7 @@ struct homa_rpc *unit_client_rpc(struct homa_sock *hsk, this_size = UNIT_TEST_DATA_PER_PACKET; h.seg.offset = htonl(bytes_received); homa_dispatch_pkts(mock_skb_alloc(server_ip, client_ip, - &h.common, this_size, 0)); + &h.common, this_size, 0), HOMA_PKT_NATIVE); } if (state == UNIT_RCVD_MSG) return crpc; @@ -327,6 +330,7 @@ struct homa_rpc *unit_server_rpc(struct homa_sock *hsk, int bytes_received; int status; +#ifndef __STRIP__ /* See strip.py */ if (state == UNIT_RCVD_START_MSG) { struct homa_start_msg_hdr start; memset(&start, 0, sizeof(start)); @@ -339,12 +343,13 @@ struct homa_rpc *unit_server_rpc(struct homa_sock *hsk, start.msg_length = htonl(req_length); homa_dispatch_pkts(mock_skb_alloc(client_ip, server_ip, - &start.common, 0, 0)); + &start.common, 0, 0), HOMA_PKT_NATIVE); srpc = homa_rpc_find_server(hsk, client_ip, id); if (srpc) homa_rpc_unlock(srpc); return srpc; } +#endif /* See strip.py */ memset(&h, 0, sizeof(h)); h.common = (struct homa_common_hdr){ @@ -362,7 +367,7 @@ struct homa_rpc *unit_server_rpc(struct homa_sock *hsk, homa_rpc_unlock(srpc); homa_dispatch_pkts(mock_skb_alloc(client_ip, server_ip, &h.common, (req_length > UNIT_TEST_DATA_PER_PACKET) - ? UNIT_TEST_DATA_PER_PACKET : req_length, 0)); + ? UNIT_TEST_DATA_PER_PACKET : req_length, 0), HOMA_PKT_NATIVE); if (state == UNIT_RCVD_ONE_PKT) return srpc; for (bytes_received = UNIT_TEST_DATA_PER_PACKET; @@ -374,7 +379,7 @@ struct homa_rpc *unit_server_rpc(struct homa_sock *hsk, this_size = UNIT_TEST_DATA_PER_PACKET; h.seg.offset = htonl(bytes_received); homa_dispatch_pkts(mock_skb_alloc(client_ip, server_ip, - &h.common, this_size, 0)); + &h.common, this_size, 0), HOMA_PKT_NATIVE); } if (state == UNIT_RCVD_MSG) return srpc; @@ -523,7 +528,7 @@ const char *unit_log_deferred(struct homa_qdisc_dev *qdev) rpc = container_of(node, struct homa_rpc, qrpc.rb_node); unit_log_printf("; ", "[id %llu, offsets", rpc->id); skb_queue_walk(&rpc->qrpc.packets, skb) { - h = (struct homa_data_hdr *)skb_transport_header(skb); + h = (struct homa_data_hdr *)homa_skb_inner_hdr(skb); unit_log_printf(" ", "%d", ntohl(h->seg.offset)); } unit_log_printf("", "]"); diff --git a/util/README.md b/util/README.md index d5023130..8b58ef71 100644 --- a/util/README.md +++ b/util/README.md @@ -3,6 +3,33 @@ analyzing Homa. Here is a summary of some of the programs in this directory; for more information, run any program with the "--help" option, or look at its source code. +### UDP Request/Response Tests + +The `udp` operation replaces `invoke` and uses the same sequential +request/response timing loop as `rtt`: + +```bash +./homa_test 10.157.149.212:8000 --count 2 --length 1000 --seed 1 udp +``` + +This sends 10 warmup requests followed by 2 measured requests and prints +RTT statistics and median bandwidth. The default measured count is 1000. +Send or receive errors make the `udp` operation exit with a nonzero status. + +Enable `net.homa.hijack_udp=1` in both endpoint network namespaces before +starting their Homa client and server sockets. The utility does not change +the sysctl or force a transport: with UDP disabled, it uses native Homa. +The server must listen on the specified Homa port. For IPv6, use a bracketed +destination and `--ipv6`, for example `[fd00::2]:8000 --ipv6 --count 2 udp`. + +At startup, the Homa server queries its socket's `SO_PROTOCOL` and prints +`Server transport: Homa-over-UDP (port 8000)`, +`Server transport: Homa-over-TCP (port 8000)`, or +`Server transport: native Homa (port 8000)`, even without `--verbose`. +Failed queries and unrecognized protocols are reported as `unknown` without +stopping the receive loop. The startup line is flushed for redirected logs. +This reports the socket's selected transport, not the current sysctl value. + ### Cluster Performance Tests **cp_node**: a program that runs on an individual node as part of cluster diff --git a/util/homa_test.cc b/util/homa_test.cc index fd9fbfea..0aa21be1 100644 --- a/util/homa_test.cc +++ b/util/homa_test.cc @@ -125,7 +125,10 @@ void print_help(const char *name) printf("Usage: %s host:port [options] op op ...\n\n" "host:port describes a server to communicate with, and each op\n" "selects a particular test to run (see the code for available\n" - "tests). The following options are supported:\n\n" + "tests). udp runs an RTT test over the selected Homa transport;\n" + "enable net.homa.hijack_udp before starting it to use UDP.\n" + "rtt and udp send 10 warmups before the measured requests.\n" + "The following options are supported:\n\n" "--count Number of times to repeat a test (default: 1000)\n" "--ipv6 Use IPv6 instead of IPv4 (default: IPv4)\n" "--length Size of messages, in bytes (default: 100)\n" @@ -220,46 +223,6 @@ void test_fill_memory(int fd, const sockaddr_in_union *dest, char *request) completed, count, tput*1e-06, timePer*1e06); } -/** - * test_invoke() - Send a request and wait for response. - * @fd: Homa socket. - * @dest: Where to send the request - * @request: Request message. - */ -void test_invoke(int fd, const sockaddr_in_union *dest, char *request) -{ - struct homa_sendmsg_args homa_args; - struct msghdr msghdr; - ssize_t resp_length; - struct iovec iov; - int status; - - iov.iov_base = request; - iov.iov_len = length; - init_sendmsg_hdrs(&msghdr, &homa_args, &iov, 1, &dest->sa, - sockaddr_size(&dest->sa)); - status = sendmsg(fd, &msghdr, 0); - if (status < 0) { - printf("Error in sendmsg: %s\n", strerror(errno)); - return; - } else { - printf("sendmsg succeeded, id %llu\n", homa_args.id); - } - recv_args.id = 0; - recv_hdr.msg_controllen = sizeof(recv_args); - resp_length = recvmsg(fd, &recv_hdr, 0); - if (resp_length < 0) { - printf("Error in recvmsg: %s\n", strerror(errno)); - return; - } - int seed = check_message(&recv_args, buf_region, resp_length, - 2*sizeof32(int)); - printf("Received message from %s with %lu bytes, " - "seed %d, id %llu\n", - print_address(&source_addr), resp_length, seed, - recv_args.id); -} - /** * test_ioctl() - Measure round-trip time for an ioctl kernel call that * does nothing but return an error. @@ -431,7 +394,7 @@ void test_read(int fd, int count) * @dest: Where to send requests. * @request: Request message. */ -void test_rtt(int fd, const sockaddr_in_union *dest, char *request) +int test_rtt(int fd, const sockaddr_in_union *dest, char *request) { uint64_t *times = new uint64_t[count]; struct homa_sendmsg_args homa_args; @@ -450,7 +413,8 @@ void test_rtt(int fd, const sockaddr_in_union *dest, char *request) status = sendmsg(fd, &msghdr, 0); if (status < 0) { printf("Error in sendmsg: %s\n", strerror(errno)); - return; + delete[] times; + return 1; } recv_args.id = 0; recv_hdr.msg_controllen = sizeof(recv_args); @@ -459,7 +423,8 @@ void test_rtt(int fd, const sockaddr_in_union *dest, char *request) times[i] = rdtsc() - start; if (resp_length < 0) { printf("Error in recvmsg: %s\n", strerror(errno)); - return; + delete[] times; + return 1; } if (resp_length != length) printf("Expected %d bytes in response, received %ld\n", @@ -469,6 +434,7 @@ void test_rtt(int fd, const sockaddr_in_union *dest, char *request) printf("Bandwidth at median: %.1f MB/sec\n", 2.0*((double) length)/(to_seconds(times[count/2])*1e06)); delete[] times; + return 0; } /** @@ -1020,13 +986,28 @@ int main(int argc, char** argv) exit(1); } host = argv[1]; - port_name = strchr(argv[1], ':'); + if (host[0] == '[') { + char *closing_bracket = strchr(host, ']'); + + if ((closing_bracket == NULL) || (closing_bracket[1] != ':')) { + printf("Bad server spec %s: bracketed IPv6 must be '[host]:port'\n", + argv[1]); + exit(1); + } + *closing_bracket = 0; + host++; + port_name = closing_bracket + 2; + } else { + port_name = strchr(host, ':'); + } if (port_name == NULL) { printf("Bad server spec %s: must be 'host:port'\n", argv[1]); exit(1); } - *port_name = 0; - port_name++; + if (host == argv[1]) { + *port_name = 0; + port_name++; + } port = get_int(port_name, "Bad port number %s; must be positive integer\n"); for (next_arg = 2; (next_arg < argc) && (*argv[next_arg] == '-'); @@ -1132,8 +1113,9 @@ int main(int argc, char** argv) test_close(); } else if (strcmp(argv[next_arg], "fill_memory") == 0) { test_fill_memory(fd, &dest, buffer); - } else if (strcmp(argv[next_arg], "invoke") == 0) { - test_invoke(fd, &dest, buffer); + } else if (strcmp(argv[next_arg], "udp") == 0) { + if (test_rtt(fd, &dest, buffer) != 0) + exit(1); } else if (strcmp(argv[next_arg], "ioctl") == 0) { test_ioctl(fd, count); } else if (strcmp(argv[next_arg], "poll") == 0) { diff --git a/util/server.cc b/util/server.cc index 821149f3..b76575f7 100644 --- a/util/server.cc +++ b/util/server.cc @@ -104,6 +104,24 @@ void homa_server(int port) return; } + int protocol; + socklen_t protocol_length = sizeof(protocol); + if (getsockopt(fd, SOL_SOCKET, SO_PROTOCOL, &protocol, + &protocol_length) < 0) { + printf("Server transport: unknown (SO_PROTOCOL failed: %s)\n", + strerror(errno)); + } else if (protocol == IPPROTO_UDP) { + printf("Server transport: Homa-over-UDP (port %d)\n", port); + } else if (protocol == IPPROTO_TCP) { + printf("Server transport: Homa-over-TCP (port %d)\n", port); + } else if (protocol == IPPROTO_HOMA) { + printf("Server transport: native Homa (port %d)\n", port); + } else { + printf("Server transport: unknown (IP protocol %d, port %d)\n", + protocol, port); + } + fflush(stdout); + memset(&recv_args, 0, sizeof(recv_args)); hdr.msg_name = &source; hdr.msg_namelen = sizeof32(source); diff --git a/util/strip.py b/util/strip.py index 0314ef99..b9998280 100755 --- a/util/strip.py +++ b/util/strip.py @@ -60,6 +60,7 @@ #else /* See strip.py */ ... #endif /* See strip.py */ + """ from __future__ import annotations diff --git a/util/strip_decl.py b/util/strip_decl.py index 0e156169..2062757a 100755 --- a/util/strip_decl.py +++ b/util/strip_decl.py @@ -41,7 +41,7 @@ 'int homa_tx_copy_from_user(', 'struct sk_buff *homa_tx_skb_alloc(', 'int homa_tx_skb_send(', - 'void homa_message_out_init(', + 'int homa_message_out_init(', 'int homa_resend_data(', 'int homa_rpc_tx_end(', 'struct sk_buff *__homa_skb_alloc(',