Stumped on IPv6 ping in c language with PF_PACKET/SOCK_DGRAM
ProgrammingThis forum is for all programming questions.
The question does not have to be directly related to Linux and any language is fair game.
Notices
Welcome to LinuxQuestions.org, a friendly and active Linux Community.
You are currently viewing LQ as a guest. By joining our community you will have the ability to post topics, receive our newsletter, use the advanced search, subscribe to threads and access many other special features. Registration is quick, simple and absolutely free. Join our community today!
Note that registered members see fewer ads, and ContentLink is completely disabled once you log in.
If you have any problems with the registration process or your account login, please contact us. If you need to reset your password, click here.
Having a problem logging in? Please visit this page to clear all LQ-related cookies.
Get a virtual cloud desktop with the Linux distro that you want in less than five minutes with Shells! With over 10 pre-installed distros to choose from, the worry-free installation life is here! Whether you are a digital nomad or just looking for flexibility, Shells can put your Linux machine on the device that you want to use.
Exclusive for LQ members, get up to 45% off per month. Click here for more info.
Stumped on IPv6 ping in c language with PF_PACKET/SOCK_DGRAM
Hi guys,
I'm trying to write a c program on Ubuntu to send an ICMP echo request over IPv6. I'm using 6to4/sit0 to do IPv6 as my ISP doesn't support it yet. I'm monitoring sit0 with Wireshark.
I can use the ping6 which comes with Ubuntu and successfully ping ipv6.google.com and on Wireshark see the IPv6 echo request and reply on sit0.
I've used PF_PACKET and SOCK_DGRAM because I don't want to have to specify the MAC addresses. I do want to specify source and destination IP addresses. I understand that SOCK_PACKET is "strongly" deprecated, so I'm trying not to use it.
My program compiles and runs without errors, but nothing shows up on Wireshark on sit0. In fact, if I ask Wireshark to monitor all interfaces, I still see no IPv6 traffic.
From my investigations, I believe you cannot send PF_PACKET/SOCK_DGRAM packets without knowledge of the target node's link-layer (MAC) address.
If that is correct, you need to do neighbor solicitation, receive the elicited neighbor advertisement, and extract the MAC address. Then you can compose the raw sockets using SOCK_RAW.
The neighbor solicitation is this:
Code:
// Sends a valid IPv6 ICMP neighbor solicitation packet,
// changes hoplimit and specifies interface using ancillary
// data method.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // close()
#include <netinet/icmp6.h> // struct nd_neighbor_solicit, which
contains icmp6_hdr
#include <arpa/inet.h> // inet_pton() and inet_ntop()
#include <netdb.h> // struct addrinfo
#include <sys/ioctl.h> // macro ioctl is defined
#include <bits/ioctls.h> // defines values for argument "request"
of ioctl. Here, we need SIOCGIFHWADDR
#include <bits/socket.h> // structs msghdr and cmsghdr
#include <net/if.h> // struct ifreq
// Taken from <linux/ipv6.h>, also in <netinet/in.h>
struct in6_pktinfo {
struct in6_addr ipi6_addr;
int ipi6_ifindex;
};
// Function prototypes
unsigned short int checksum (unsigned short int *, int);
int
main (int argc, char **argv)
{
int MAXPACKETLEN = 131072;
int NSHDRLEN = sizeof (struct nd_neighbor_solicit); // Length of NS
message header
int OPTLEN = 8; // Option Type (1 byte) + Length (1 byte) + Length
of MAC address (6 bytes)
int PSDHDRLEN = 16 + 16 + 4 + 3 + 1 + NSHDRLEN + OPTLEN; // Section
8.1 of RFC 2460
int i, sd, status, ifindex, cmsglen, hoplimit;
struct addrinfo hints;
struct addrinfo *res;
struct sockaddr_in6 dst;
struct sockaddr_in6 src;
struct sockaddr_in6 dstsnmc;
struct nd_neighbor_solicit *ns;
socklen_t srclen;
unsigned char *outpack, *options, *psdhdr;
unsigned char temp[16];
struct msghdr msghdr;
struct ifreq ifr;
struct cmsghdr *cmsghdr1, *cmsghdr2;
struct in6_pktinfo *pktinfo;
struct iovec iov[2];
char *target, *source, *interface;
void *tmp;
// Allocate memory for various arrays.
tmp = (char *) malloc (20 * sizeof (char));
if (tmp != NULL) {
interface = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array
'interface'.\n");
exit (EXIT_FAILURE);
}
memset (interface, 0, sizeof (interface));
strcpy (interface, "wlan0");
tmp = (char *) malloc (40 * sizeof (char));
if (tmp != NULL) {
target = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array 'target'.
\n");
exit (EXIT_FAILURE);
}
memset (target, 0, 40 * sizeof (char));
tmp = (char *) malloc (40 * sizeof (char));
if (tmp != NULL) {
source = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array 'source'.
\n");
exit (EXIT_FAILURE);
}
memset (source, 0, 40 * sizeof (char));
tmp = (unsigned char *) malloc (MAXPACKETLEN * sizeof (unsigned
char));
if (tmp != NULL) {
outpack = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array
'outpack'.\n");
exit (EXIT_FAILURE);
}
memset (outpack, 0, MAXPACKETLEN * sizeof (unsigned char));
tmp = (unsigned char *) malloc (OPTLEN * sizeof (unsigned char));
if (tmp != NULL) {
options = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array
'options'.\n");
exit (EXIT_FAILURE);
}
memset (options, 0, OPTLEN * sizeof (unsigned char));
tmp = (unsigned char *) malloc (PSDHDRLEN * sizeof (unsigned char));
if (tmp != NULL) {
psdhdr = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array 'psdhdr'.
\n");
exit (EXIT_FAILURE);
}
memset (psdhdr, 0, PSDHDRLEN * sizeof (unsigned char));
// Hard-code target address
strcpy (target, "ipv6.google.com");
// Hard-code source address
strcpy (source, "xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx");
// Note that you can't bind to link-local address unless you add
scope)
// Fill out hints, which is a struct addrinfo to be used by
getaddrinfo.
memset (&hints, 0, sizeof (struct addrinfo));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = AF_INET6;
hints.ai_socktype = SOCK_RAW;
hints.ai_protocol = IPPROTO_ICMPV6;
// Turn source URL or IPv6 address into sockaddr_in6 form and copy to
src, and
// copy source address to checksum pseudo-header (RFC 2460).
if ((status = getaddrinfo (source, NULL, &hints, &res)) != 0) {
fprintf (stderr, "getaddrinfo in target: %s\n", gai_strerror
(status));
return (EXIT_FAILURE);
}
memcpy (&src, res->ai_addr, res->ai_addrlen);
srclen = res->ai_addrlen;
inet_pton (AF_INET6, source, psdhdr);
// Turn target URL or IPv6 address into sockaddr_in6 form and copy to
dst, and
// copy destination address to checksum pseudo-header (RFC 2460).
if ((status = getaddrinfo (target, NULL, &hints, &res)) != 0) {
fprintf (stderr, "getaddrinfo in target: %s\n", gai_strerror
(status));
return (EXIT_FAILURE);
}
memcpy (&dstsnmc, res->ai_addr, res->ai_addrlen);
memcpy (&dst, res->ai_addr, res->ai_addrlen);
tmp = &((struct sockaddr_in6 *) res->ai_addr)->sin6_addr;
// Convert target's IPv6 unicast address to solicited-node multicast
address.
// Section 2.7.1 of RFC 4291.
memset (temp, 0, 16);
memcpy (temp, tmp, 16);
printf ("Target unicast IPv6 address: ");
for (i=0; i<16; i++) {
printf ("%02x ", temp[i]);
}
printf("\n");
temp[0]= 255;
temp[1]=2;
for (i=2; i<11; i++) {
temp[i] = 0;
}
temp[11]=1;
temp[12]=255;
printf ("Target solicited-node multicast address: ");
for (i=0; i<16; i++) {
printf ("%02x ", temp[i]);
}
printf("\n");
memcpy (tmp, temp, 16);
memcpy (&dstsnmc, res->ai_addr, res->ai_addrlen);
memcpy (psdhdr + 16, temp, 16);
// Request a socket descriptor sd.
if ((sd = socket (res->ai_family, res->ai_socktype, res-
>ai_protocol)) < 0) {
perror ("Failed to get socket descriptor ");
exit (EXIT_FAILURE);
}
// Obtain source MAC address.
memset (&ifr, 0, sizeof (ifr));
snprintf (ifr.ifr_name, sizeof (ifr.ifr_name), "%s", interface);
if (ioctl (sd, SIOCGIFHWADDR, &ifr) < 0) {
perror ("ioctl() failed to get source MAC address ");
return (EXIT_FAILURE);
}
// Copy source MAC address into options buffer.
options[0] = 1; // Option Type - "source link layer
address" (Section 4.6 of RFC 4861)
options[1] = OPTLEN / 8; // Option Length - units of 8 octets (RFC
4861)
for (i=0; i<6; i++) {
options[i+2] = (unsigned char) ifr.ifr_addr.sa_data[i];
}
// Report source MAC address to stdout.
printf ("MAC address for interface %s is ", interface);
for (i=0; i<5; i++) {
printf ("%02x:", options[i+2]);
}
printf ("%02x\n", options[5+2]);
// Bind the socket descriptor to the source address if not site-local
or link-local.
if (!(psdhdr[0] == 0xfe)) {
if (bind (sd, (struct sockaddr *) &src, srclen) < 0) {
perror ("Failed to bind the socket descriptor to the source
address ");
exit (EXIT_FAILURE);
}
}
// Retrieve source interface index.
if ((ifindex = if_nametoindex (interface)) == 0) {
perror ("if_nametoindex() failed to obtain interface index ");
exit (EXIT_FAILURE);
}
printf ("Index for interface %s is %i\n", interface, ifindex);
// Define first part of buffer outpack to be a neighbor solicit
struct.
ns = (struct nd_neighbor_solicit *) outpack;
memset (ns, 0, sizeof (*ns));
// Populate icmp6_hdr portion of neighbor solicit struct.
ns->nd_ns_hdr.icmp6_type = ND_NEIGHBOR_SOLICIT; // 135 (RFC 4861)
ns->nd_ns_hdr.icmp6_code = 0; // zero for neighbor
solicitation (RFC 4861)
ns->nd_ns_hdr.icmp6_cksum = htons(0); // zero when calculating
checksum
ns->nd_ns_hdr.icmp6_data32[0] = htonl(0); // Reserved - must be set
to zero (RFC 4861)
ns->nd_ns_target = dst.sin6_addr; // Target address (NOT
MULTICAST) (as type in6_addr)
// Append options to end of neighbor solicit struct.
memcpy (outpack + NSHDRLEN, options, OPTLEN);
// Prepare msghdr for sendmsg().
memset (&msghdr, 0, sizeof (msghdr));
msghdr.msg_name = &dstsnmc; // Destination IPv6 address (solicited
node multicast) (as struct sockaddr_in6)
msghdr.msg_namelen = sizeof (dstsnmc);
memset (&iov, 0, sizeof (iov));
iov[0].iov_base = (unsigned char *) outpack; // Point msghdr to
buffer outpack
iov[0].iov_len = NSHDRLEN + OPTLEN;
msghdr.msg_iov = iov; // scatter/gather array
msghdr.msg_iovlen = 1; // number of elements in
scatter/gather array
// Tell msghdr we're adding cmsghdr data to change hop limit and
specify interface.
// Allocate some memory for our cmsghdr data.
cmsglen = CMSG_SPACE (sizeof (int)) + CMSG_SPACE (sizeof (struct
in6_pktinfo));
tmp = (unsigned char *) malloc (cmsglen * sizeof (unsigned char));
if (tmp != NULL) {
msghdr.msg_control = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array
'msghdr.msg_control'.\n");
exit (EXIT_FAILURE);
}
memset (msghdr.msg_control, 0, cmsglen);
msghdr.msg_controllen = cmsglen;
// Change hop limit to 255 as required for neighbor solicitation (RFC
4861).
hoplimit = 255;
cmsghdr1 = CMSG_FIRSTHDR (&msghdr);
cmsghdr1->cmsg_level = IPPROTO_IPV6;
cmsghdr1->cmsg_type = IPV6_HOPLIMIT; // We want to change hop limit
cmsghdr1->cmsg_len = CMSG_LEN (sizeof (int));
*((int *) CMSG_DATA (cmsghdr1)) = hoplimit;
// Specify source interface index for this packet via cmsghdr data.
cmsghdr2 = CMSG_NXTHDR (&msghdr, cmsghdr1);
cmsghdr2->cmsg_level = IPPROTO_IPV6;
cmsghdr2->cmsg_type = IPV6_PKTINFO; // We want to specify interface
here
cmsghdr2->cmsg_len = CMSG_LEN (sizeof (struct in6_pktinfo));
pktinfo = (struct in6_pktinfo *) CMSG_DATA (cmsghdr2);
pktinfo->ipi6_ifindex = ifindex;
// Compute ICMPv6 checksum (RFC 2460).
// psdhdr[0 to 15] = source IPv6 address, set earlier.
// psdhdr[16 to 31] = destination IPv6 address, set earlier.
psdhdr[32] = 0; // Length should not be greater than 65535 (i.e., 2
bytes)
psdhdr[33] = 0; // Length should not be greater than 65535 (i.e., 2
bytes)
psdhdr[34] = (NSHDRLEN + OPTLEN) / 256; // Upper layer packet
length
psdhdr[35] = (NSHDRLEN + OPTLEN) % 256; // Upper layer packet
length
psdhdr[36] = 0; // Must be zero
psdhdr[37] = 0; // Must be zero
psdhdr[38] = 0; // Must be zero
psdhdr[39] = IPPROTO_ICMPV6;
memcpy (psdhdr + 40, outpack, NSHDRLEN + OPTLEN);
ns->nd_ns_hdr.icmp6_cksum = checksum ((unsigned short int *) psdhdr,
PSDHDRLEN);
printf ("Checksum: %x\n", ntohs (ns->nd_ns_hdr.icmp6_cksum));
// Send packet.
if (sendmsg (sd, &msghdr, 0) < 0) {
perror ("sendmsg() failed ");
exit (EXIT_FAILURE);
}
close (sd);
// Free allocated memory.
free (interface);
free (target);
free (source);
free (outpack);
free (options);
free (psdhdr);
free (msghdr.msg_control);
return (EXIT_SUCCESS);
}
// Checksum function
unsigned short int
checksum (unsigned short int *addr, int len)
{
int nleft = len;
int sum = 0;
unsigned short int *w = addr;
unsigned short int answer = 0;
while (nleft > 1) {
sum += *w++;
nleft -= sizeof (unsigned short int);
}
if (nleft == 1) {
*(unsigned char *) (&answer) = *(unsigned char *) w;
sum += answer;
}
sum = (sum >> 16) + (sum & 0xFFFF);
sum += (sum >> 16);
answer = ~sum;
return (answer);
}
... and the code to receive the neighbor advertisement and extract the link-layer address is this:
Code:
// Receives a neighbor advertisement and extracts hop limit,
// destination address and interface index from ancillary
// data, and advertising link-layer address (i.e., MAC)
// from options data.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // close()
#include <netinet/icmp6.h> // struct nd_neighbor_solicit/advert,
which contains icmp6_hdr and ND_NEIGHBOR_ADVERT
#include <netinet/ip.h> // IP_MAXPACKET (65535)
#include <arpa/inet.h> // inet_pton() and inet_ntop()
#include <netdb.h> // struct addrinfo
#include <sys/ioctl.h> // macro ioctl is defined
#include <bits/ioctls.h> // defines values for argument "request"
of ioctl. Here, we need SIOCGIFHWADDR
#include <bits/socket.h> // structs msghdr and cmsghdr
#include <net/if.h> // struct ifreq
// Taken from <linux/ipv6.h>, also in <netinet/in.h>
struct in6_pktinfo {
struct in6_addr ipi6_addr;
int ipi6_ifindex;
};
// Function prototypes
static void *find_ancillary (struct msghdr *, int);
int
main (int argc, char **argv)
{
int i, status, sd, on, ifindex, hoplimit;
struct nd_neighbor_advert *na;
unsigned char *inpack;
int len;
struct msghdr msghdr;
struct iovec iov[2];
unsigned char *opt, *pkt;
char *interface, *target, *destination;
struct in6_addr dst;
int rcv_ifindex;
struct ifreq ifr;
void *tmp;
// Allocate memory for various arrays.
tmp = (unsigned char *) malloc (IP_MAXPACKET * sizeof (unsigned
char));
if (tmp != NULL) {
inpack = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array 'inpack'.
\n");
exit (EXIT_FAILURE);
}
memset (inpack, 0, IP_MAXPACKET * sizeof (unsigned char));
tmp = (char *) malloc (40 * sizeof (char));
if (tmp != NULL) {
target = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array 'target'.
\n");
exit (EXIT_FAILURE);
}
memset (target, 0, 40 * sizeof (char));
tmp = (char *) malloc (20 * sizeof (char));
if (tmp != NULL) {
interface = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array
'interface'.\n");
exit (EXIT_FAILURE);
}
memset (interface, 0, sizeof (interface));
strcpy (interface, "wlan0");
tmp = (char *) malloc (40 * sizeof (char));
if (tmp != NULL) {
destination = tmp;}
else {
fprintf (stderr, "ERROR: Cannot allocate memory for array
'destination'.\n");
exit (EXIT_FAILURE);
}
memset (destination, 0, sizeof (destination));
// Prepare msghdr for recvmsg().
memset (&msghdr, 0, sizeof (msghdr));
msghdr.msg_name = NULL;
msghdr.msg_namelen = 0;
memset (&iov, 0, sizeof (iov));
iov[0].iov_base = (unsigned char *) inpack;
iov[0].iov_len = IP_MAXPACKET;
msghdr.msg_iov = iov;
msghdr.msg_iovlen = 1;
msghdr.msg_control = (unsigned char *) malloc (IP_MAXPACKET * sizeof
(unsigned char));
msghdr.msg_controllen = IP_MAXPACKET * sizeof (unsigned char);
// Request a socket descriptor sd.
if ((sd = socket (AF_INET6, SOCK_RAW, IPPROTO_ICMPV6)) < 0) {
perror ("Failed to get socket descriptor ");
exit (EXIT_FAILURE);
}
// Set flag so we receive hop limit from recvmsg.
on = 1;
if ((status = setsockopt (sd, IPPROTO_IPV6, IPV6_RECVHOPLIMIT, &on,
sizeof (on))) < 0) {
perror ("setsockopt to IPV6_RECVHOPLIMIT failed ");
exit (EXIT_FAILURE);
}
// Set flag so we receive destination address from recvmsg.
on = 1;
if ((status = setsockopt (sd, IPPROTO_IPV6, IPV6_RECVPKTINFO, &on,
sizeof (on))) < 0) {
perror ("setsockopt to IPV6_RECVPKTINFO failed ");
exit (EXIT_FAILURE);
}
// Obtain MAC address of this node.
memset (&ifr, 0, sizeof (ifr));
snprintf (ifr.ifr_name, sizeof (ifr.ifr_name), "%s", interface);
if (ioctl (sd, SIOCGIFHWADDR, &ifr) < 0) {
perror ("ioctl() failed to get source MAC address ");
return (EXIT_FAILURE);
}
// Retrieve interface index of this node.
if ((ifindex = if_nametoindex (interface)) == 0) {
perror ("if_nametoindex() failed to obtain interface index ");
exit (EXIT_FAILURE);
}
printf ("\nOn this node, index for interface %s is %i\n", interface,
ifindex);
// Bind socket to interface of this node.
if (setsockopt (sd, SOL_SOCKET, SO_BINDTODEVICE, (void *) &ifr,
sizeof (ifr)) < 0) {
perror ("SO_BINDTODEVICE failed");
exit (EXIT_FAILURE);
}
// Grab incoming message from socket sd.
// Keep at it until we get a neighbor advertisement.
na = (struct nd_neighbor_advert *) inpack;
while (na->nd_na_hdr.icmp6_type != ND_NEIGHBOR_ADVERT) {
if ((len = recvmsg (sd, &msghdr, 0)) < 0) {
perror ("recvmsg failed ");
return (EXIT_FAILURE);
}
}
// Ancillary data
printf ("\nIPv6 header data:\n");
opt = find_ancillary (&msghdr, IPV6_HOPLIMIT);
if (opt == NULL) {
fprintf (stderr, "Unknown hop limit\n");
exit (EXIT_FAILURE);
}
hoplimit = *(unsigned int *) opt;
printf ("Hop limit: %u\n", hoplimit);
opt = find_ancillary (&msghdr, IPV6_PKTINFO);
if (opt == NULL) {
fprintf (stderr, "Unkown destination address\n");
exit (EXIT_FAILURE);
}
dst = ((struct in6_pktinfo *) opt)->ipi6_addr;
inet_ntop (AF_INET6, &dst, destination, 40);
printf ("Destination address: %s\n", destination);
rcv_ifindex = ((struct in6_pktinfo *) opt)->ipi6_ifindex;
printf ("Destination interface index: %i\n", rcv_ifindex);
// ICMPv6 header and options data
printf ("\nICMPv6 header data:\n");
printf ("Type: %u\n", na->nd_na_hdr.icmp6_type);
printf ("Code: %u\n", na->nd_na_hdr.icmp6_code);
printf ("Checksum: %x\n", ntohs (na->nd_na_hdr.icmp6_cksum));
printf ("Router flag: %u\n", ntohl (na->nd_na_hdr.icmp6_data32[0])
>> 31);
printf ("Solicited flag: %u\n", ((ntohl (na-
>nd_na_hdr.icmp6_data32[0])) >> 30) & 1);
printf ("Override flag: %u\n", ((ntohl (na-
>nd_na_hdr.icmp6_data32[0])) >> 29) & 1);
printf ("Reserved: %i\n", (ntohl (na->nd_na_hdr.icmp6_data32[0])) &
536870911u);
inet_ntop (AF_INET6, &(na->nd_na_target), target, 40);
printf ("Target address of neighbor solicitation: %s\n", target);
printf ("\nOptions:\n");
pkt = (unsigned char *) inpack;
printf ("Type: %u\n", pkt[sizeof (struct nd_neighbor_advert)]);
printf ("Length: %u (units of 8 octets)\n", pkt[sizeof (struct
nd_neighbor_advert) + 1]);
printf ("MAC address: ");
for (i=2; i<7; i++) {
printf ("%02x:", pkt[sizeof (struct nd_neighbor_advert) + i]);
}
printf ("%02x\n", pkt[sizeof (struct nd_neighbor_advert) + 7]);
close (sd);
free (inpack);
free (target);
free (interface);
free (destination);
free (msghdr.msg_control);
return (EXIT_SUCCESS);
}
static void *
find_ancillary (struct msghdr *msg, int cmsg_type)
{
struct cmsghdr *cmsg = NULL;
for (cmsg = CMSG_FIRSTHDR (msg); cmsg != NULL; cmsg = CMSG_NXTHDR
(msg, cmsg)) {
if ((cmsg->cmsg_level == IPPROTO_IPV6) && (cmsg->cmsg_type ==
cmsg_type)) {
return (CMSG_DATA (cmsg));
}
}
return (NULL);
To be practical, it should probably have a timer on the recvmsg loop.
Also, it would normally be in the same program as that which sent the
solicitation.
I also note that since I'm using 6to4 because my ISP does not support IPv6, the code above won't work for addresses outside my network. I need to create a IPv4 tunnel and send through interface sit0. I still have to investigate how to do that.
You should be using PF_INET6/AF_INET6 with SOCK_RAW. PF_PACKET is for Ethernet, and whilst you could construct an IPv4 header to put before your IPv6 packet, it would be silly to do so; as your program would not work on other types of connection.
LinuxQuestions.org is looking for people interested in writing
Editorials, Articles, Reviews, and more. If you'd like to contribute
content, let us know.