| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Copyright (c) 2019 Synopsys, Inc. and/or its affiliates. | |
| * stmmac Selftests Support | |
| * | |
| * Author: Jose Abreu <joabreu@synopsys.com> | |
| * | |
| * Ported from stmmac by: | |
| * Copyright (C) 2021 Oleksij Rempel <o.rempel@pengutronix.de> | |
| */ | |
| #include <linux/phy.h> | |
| #include <net/selftests.h> | |
| #include <net/tcp.h> | |
| #include <net/udp.h> | |
| static u8 net_test_next_id; | |
| struct sk_buff *net_test_get_skb(struct net_device *ndev, u8 id, | |
| struct net_packet_attrs *attr) | |
| { | |
| struct sk_buff *skb = NULL; | |
| struct udphdr *uhdr = NULL; | |
| struct tcphdr *thdr = NULL; | |
| struct netsfhdr *shdr; | |
| struct ethhdr *ehdr; | |
| struct iphdr *ihdr; | |
| int iplen, size; | |
| size = attr->size + NET_TEST_PKT_SIZE; | |
| if (attr->tcp) | |
| size += sizeof(struct tcphdr); | |
| else | |
| size += sizeof(struct udphdr); | |
| if (attr->max_size && attr->max_size > size) | |
| size = attr->max_size; | |
| skb = netdev_alloc_skb(ndev, size); | |
| if (!skb) | |
| return NULL; | |
| prefetchw(skb->data); | |
| ehdr = skb_push(skb, ETH_HLEN); | |
| skb_reset_mac_header(skb); | |
| skb_set_network_header(skb, skb->len); | |
| ihdr = skb_put(skb, sizeof(*ihdr)); | |
| skb_set_transport_header(skb, skb->len); | |
| if (attr->tcp) | |
| thdr = skb_put(skb, sizeof(*thdr)); | |
| else | |
| uhdr = skb_put(skb, sizeof(*uhdr)); | |
| eth_zero_addr(ehdr->h_dest); | |
| if (attr->src) | |
| ether_addr_copy(ehdr->h_source, attr->src); | |
| if (attr->dst) | |
| ether_addr_copy(ehdr->h_dest, attr->dst); | |
| ehdr->h_proto = htons(ETH_P_IP); | |
| if (attr->tcp) { | |
| memset(thdr, 0, sizeof(*thdr)); | |
| thdr->source = htons(attr->sport); | |
| thdr->dest = htons(attr->dport); | |
| thdr->doff = sizeof(struct tcphdr) / 4; | |
| } else { | |
| uhdr->source = htons(attr->sport); | |
| uhdr->dest = htons(attr->dport); | |
| udp_set_len_short(uhdr, sizeof(*shdr) + sizeof(*uhdr) + attr->size); | |
| if (attr->max_size) | |
| udp_set_len_short(uhdr, attr->max_size - | |
| (sizeof(*ihdr) + sizeof(*ehdr))); | |
| uhdr->check = 0; | |
| } | |
| ihdr->ihl = 5; | |
| ihdr->ttl = 32; | |
| ihdr->version = 4; | |
| if (attr->tcp) | |
| ihdr->protocol = IPPROTO_TCP; | |
| else | |
| ihdr->protocol = IPPROTO_UDP; | |
| iplen = sizeof(*ihdr) + sizeof(*shdr) + attr->size; | |
| if (attr->tcp) | |
| iplen += sizeof(*thdr); | |
| else | |
| iplen += sizeof(*uhdr); | |
| if (attr->max_size) | |
| iplen = attr->max_size - sizeof(*ehdr); | |
| ihdr->tot_len = htons(iplen); | |
| ihdr->frag_off = 0; | |
| ihdr->saddr = htonl(attr->ip_src); | |
| ihdr->daddr = htonl(attr->ip_dst); | |
| ihdr->tos = 0; | |
| ihdr->id = 0; | |
| ip_send_check(ihdr); | |
| shdr = skb_put(skb, sizeof(*shdr)); | |
| shdr->version = 0; | |
| shdr->magic = cpu_to_be64(NET_TEST_PKT_MAGIC); | |
| attr->id = id; | |
| shdr->id = id; | |
| if (attr->size) { | |
| void *payload = skb_put(skb, attr->size); | |
| memset(payload, 0, attr->size); | |
| } | |
| if (attr->max_size && attr->max_size > skb->len) { | |
| size_t pad_len = attr->max_size - skb->len; | |
| void *pad = skb_put(skb, pad_len); | |
| memset(pad, 0, pad_len); | |
| } | |
| skb->csum = 0; | |
| skb->ip_summed = CHECKSUM_PARTIAL; | |
| if (attr->tcp) { | |
| int l4len = skb->len - skb_transport_offset(skb); | |
| thdr->check = ~tcp_v4_check(l4len, ihdr->saddr, ihdr->daddr, 0); | |
| skb->csum_start = skb_transport_header(skb) - skb->head; | |
| skb->csum_offset = offsetof(struct tcphdr, check); | |
| if (attr->bad_csum) { | |
| /* Force mangled checksum */ | |
| if (skb_checksum_help(skb)) { | |
| kfree_skb(skb); | |
| return NULL; | |
| } | |
| if (thdr->check != CSUM_MANGLED_0) | |
| thdr->check = CSUM_MANGLED_0; | |
| else | |
| thdr->check = csum16_sub(thdr->check, | |
| cpu_to_be16(1)); | |
| } | |
| } else { | |
| udp4_hwcsum(skb, ihdr->saddr, ihdr->daddr); | |
| } | |
| skb->protocol = htons(ETH_P_IP); | |
| skb->pkt_type = PACKET_HOST; | |
| skb->dev = ndev; | |
| return skb; | |
| } | |
| EXPORT_SYMBOL_GPL(net_test_get_skb); | |
| static int net_test_loopback_validate(struct sk_buff *skb, | |
| struct net_device *ndev, | |
| struct packet_type *pt, | |
| struct net_device *orig_ndev) | |
| { | |
| struct net_test_priv *tpriv = pt->af_packet_priv; | |
| const unsigned char *src = tpriv->packet->src; | |
| const unsigned char *dst = tpriv->packet->dst; | |
| struct netsfhdr *shdr; | |
| struct ethhdr *ehdr; | |
| struct udphdr *uhdr; | |
| struct tcphdr *thdr; | |
| struct iphdr *ihdr; | |
| skb = skb_unshare(skb, GFP_ATOMIC); | |
| if (!skb) | |
| goto out; | |
| if (skb_linearize(skb)) | |
| goto out; | |
| if (skb_headlen(skb) < (NET_TEST_PKT_SIZE - ETH_HLEN)) | |
| goto out; | |
| ehdr = (struct ethhdr *)skb_mac_header(skb); | |
| if (dst) { | |
| if (!ether_addr_equal_unaligned(ehdr->h_dest, dst)) | |
| goto out; | |
| } | |
| if (src) { | |
| if (!ether_addr_equal_unaligned(ehdr->h_source, src)) | |
| goto out; | |
| } | |
| ihdr = ip_hdr(skb); | |
| if (tpriv->double_vlan) | |
| ihdr = (struct iphdr *)(skb_network_header(skb) + 4); | |
| if (tpriv->packet->tcp) { | |
| if (ihdr->protocol != IPPROTO_TCP) | |
| goto out; | |
| thdr = (struct tcphdr *)((u8 *)ihdr + 4 * ihdr->ihl); | |
| if (thdr->dest != htons(tpriv->packet->dport)) | |
| goto out; | |
| shdr = (struct netsfhdr *)((u8 *)thdr + sizeof(*thdr)); | |
| } else { | |
| if (ihdr->protocol != IPPROTO_UDP) | |
| goto out; | |
| uhdr = (struct udphdr *)((u8 *)ihdr + 4 * ihdr->ihl); | |
| if (uhdr->dest != htons(tpriv->packet->dport)) | |
| goto out; | |
| shdr = (struct netsfhdr *)((u8 *)uhdr + sizeof(*uhdr)); | |
| } | |
| if (shdr->magic != cpu_to_be64(NET_TEST_PKT_MAGIC)) | |
| goto out; | |
| if (tpriv->packet->id != shdr->id) | |
| goto out; | |
| if (tpriv->packet->bad_csum && skb->ip_summed == CHECKSUM_UNNECESSARY) | |
| tpriv->ok = -EIO; | |
| else | |
| tpriv->ok = true; | |
| complete(&tpriv->comp); | |
| out: | |
| kfree_skb(skb); | |
| return 0; | |
| } | |
| static int __net_test_loopback(struct net_device *ndev, | |
| struct net_packet_attrs *attr) | |
| { | |
| struct net_test_priv *tpriv; | |
| struct sk_buff *skb = NULL; | |
| int ret = 0; | |
| tpriv = kzalloc_obj(*tpriv); | |
| if (!tpriv) | |
| return -ENOMEM; | |
| tpriv->ok = false; | |
| init_completion(&tpriv->comp); | |
| tpriv->pt.type = htons(ETH_P_IP); | |
| tpriv->pt.func = net_test_loopback_validate; | |
| tpriv->pt.dev = ndev; | |
| tpriv->pt.af_packet_priv = tpriv; | |
| tpriv->packet = attr; | |
| dev_add_pack(&tpriv->pt); | |
| skb = net_test_get_skb(ndev, net_test_next_id, attr); | |
| if (!skb) { | |
| ret = -ENOMEM; | |
| goto cleanup; | |
| } | |
| net_test_next_id++; | |
| ret = dev_direct_xmit(skb, attr->queue_mapping); | |
| if (ret < 0) { | |
| goto cleanup; | |
| } else if (ret > 0) { | |
| ret = -ENETUNREACH; | |
| goto cleanup; | |
| } | |
| if (!attr->timeout) | |
| attr->timeout = NET_LB_TIMEOUT; | |
| wait_for_completion_timeout(&tpriv->comp, attr->timeout); | |
| if (tpriv->ok < 0) | |
| ret = tpriv->ok; | |
| else if (!tpriv->ok) | |
| ret = -ETIMEDOUT; | |
| else | |
| ret = 0; | |
| cleanup: | |
| dev_remove_pack(&tpriv->pt); | |
| kfree(tpriv); | |
| return ret; | |
| } | |
| static int net_test_netif_carrier(struct net_device *ndev) | |
| { | |
| return netif_carrier_ok(ndev) ? 0 : -ENOLINK; | |
| } | |
| static int net_test_phy_phydev(struct net_device *ndev) | |
| { | |
| return ndev->phydev ? 0 : -EOPNOTSUPP; | |
| } | |
| static int net_test_phy_loopback_enable(struct net_device *ndev) | |
| { | |
| if (!ndev->phydev) | |
| return -EOPNOTSUPP; | |
| return phy_loopback(ndev->phydev, true, 0); | |
| } | |
| static int net_test_phy_loopback_disable(struct net_device *ndev) | |
| { | |
| if (!ndev->phydev) | |
| return -EOPNOTSUPP; | |
| return phy_loopback(ndev->phydev, false, 0); | |
| } | |
| static int net_test_phy_loopback_udp(struct net_device *ndev) | |
| { | |
| struct net_packet_attrs attr = { }; | |
| attr.dst = ndev->dev_addr; | |
| return __net_test_loopback(ndev, &attr); | |
| } | |
| static int net_test_phy_loopback_udp_mtu(struct net_device *ndev) | |
| { | |
| struct net_packet_attrs attr = { }; | |
| attr.dst = ndev->dev_addr; | |
| attr.max_size = ndev->mtu; | |
| return __net_test_loopback(ndev, &attr); | |
| } | |
| static int net_test_phy_loopback_tcp(struct net_device *ndev) | |
| { | |
| struct net_packet_attrs attr = { }; | |
| attr.dst = ndev->dev_addr; | |
| attr.tcp = true; | |
| return __net_test_loopback(ndev, &attr); | |
| } | |
| /** | |
| * net_test_phy_loopback_tcp_bad_csum - PHY loopback test with a deliberately | |
| * corrupted TCP checksum | |
| * @ndev: the network device to test | |
| * | |
| * Builds the same minimal Ethernet/IPv4/TCP frame as | |
| * net_test_phy_loopback_tcp(), then flips the least-significant bit of the TCP | |
| * checksum so the resulting value is provably invalid (neither 0 nor 0xFFFF). | |
| * The frame is transmitted through the device’s internal PHY loopback path: | |
| * | |
| * test code -> MAC driver -> MAC HW -> xMII -> PHY -> | |
| * internal PHY loopback -> xMII -> MAC HW -> MAC driver -> test code | |
| * | |
| * Result interpretation | |
| * --------------------- | |
| * 0 The frame is delivered to the stack and the driver reports | |
| * ip_summed as CHECKSUM_NONE or CHECKSUM_COMPLETE - both are | |
| * valid ways to indicate “bad checksum, let the stack verify.” | |
| * -ETIMEDOUT The MAC/PHY silently dropped the frame; hardware checksum | |
| * verification filtered it out before the driver saw it. | |
| * -EIO The driver returned the frame with ip_summed == | |
| * CHECKSUM_UNNECESSARY, falsely claiming a valid checksum and | |
| * indicating a serious RX-path defect. | |
| * | |
| * Return: 0 on success or a negative error code on failure. | |
| */ | |
| static int net_test_phy_loopback_tcp_bad_csum(struct net_device *ndev) | |
| { | |
| struct net_packet_attrs attr = { }; | |
| attr.dst = ndev->dev_addr; | |
| attr.tcp = true; | |
| attr.bad_csum = true; | |
| return __net_test_loopback(ndev, &attr); | |
| } | |
| static const struct net_test { | |
| char name[ETH_GSTRING_LEN]; | |
| int (*fn)(struct net_device *ndev); | |
| } net_selftests[] = { | |
| { | |
| .name = "Carrier ", | |
| .fn = net_test_netif_carrier, | |
| }, { | |
| .name = "PHY dev is present ", | |
| .fn = net_test_phy_phydev, | |
| }, { | |
| /* This test should be done before all PHY loopback test */ | |
| .name = "PHY internal loopback, enable ", | |
| .fn = net_test_phy_loopback_enable, | |
| }, { | |
| .name = "PHY internal loopback, UDP ", | |
| .fn = net_test_phy_loopback_udp, | |
| }, { | |
| .name = "PHY internal loopback, MTU ", | |
| .fn = net_test_phy_loopback_udp_mtu, | |
| }, { | |
| .name = "PHY internal loopback, TCP ", | |
| .fn = net_test_phy_loopback_tcp, | |
| }, { | |
| .name = "PHY loopback, bad TCP csum ", | |
| .fn = net_test_phy_loopback_tcp_bad_csum, | |
| }, { | |
| /* This test should be done after all PHY loopback test */ | |
| .name = "PHY internal loopback, disable", | |
| .fn = net_test_phy_loopback_disable, | |
| }, | |
| }; | |
| void net_selftest(struct net_device *ndev, struct ethtool_test *etest, u64 *buf) | |
| { | |
| int count = net_selftest_get_count(); | |
| int i; | |
| memset(buf, 0, sizeof(*buf) * count); | |
| net_test_next_id = 0; | |
| if (etest->flags != ETH_TEST_FL_OFFLINE) { | |
| netdev_err(ndev, "Only offline tests are supported\n"); | |
| etest->flags |= ETH_TEST_FL_FAILED; | |
| return; | |
| } | |
| for (i = 0; i < count; i++) { | |
| buf[i] = net_selftests[i].fn(ndev); | |
| if (buf[i] && (buf[i] != -EOPNOTSUPP)) | |
| etest->flags |= ETH_TEST_FL_FAILED; | |
| } | |
| } | |
| EXPORT_SYMBOL_GPL(net_selftest); | |
| int net_selftest_get_count(void) | |
| { | |
| return ARRAY_SIZE(net_selftests); | |
| } | |
| EXPORT_SYMBOL_GPL(net_selftest_get_count); | |
| void net_selftest_get_strings(u8 *data) | |
| { | |
| int i; | |
| for (i = 0; i < net_selftest_get_count(); i++) | |
| ethtool_sprintf(&data, "%2d. %s", i + 1, | |
| net_selftests[i].name); | |
| } | |
| EXPORT_SYMBOL_GPL(net_selftest_get_strings); | |
| MODULE_DESCRIPTION("Common library for generic PHY ethtool selftests"); | |
| MODULE_LICENSE("GPL v2"); | |
| MODULE_AUTHOR("Oleksij Rempel <o.rempel@pengutronix.de>"); |