/* Minimal ICMPv6 code for generating router advertisements as required by
 * relevant 3GPP specs for a GGSN with IPv6 PDP contexts */

/* (C) 2017 by Harald Welte <laforge@gnumonks.org>
 * All Rights Reserved.
 *
 * SPDX-License-Identifier: GPL-2.0+
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <netinet/in.h>
#if defined(__FreeBSD__)
#include <sys/types.h>	/* FreeBSD 10.x needs this before ip6.h */
#include <sys/endian.h>
#endif
#include <netinet/ip6.h>

#include <osmocom/core/msgb.h>
#include <osmocom/core/utils.h>

#include <osmocom/netif/ip_checksum.h>
#include <osmocom/netif/icmpv6.h>

/* 29.061 11.2.1.3.4 IPv6 Router Configuration Variables in GGSN */
#define GGSN_MaxRtrAdvInterval	21600		/* 6 hours */
#define GGSN_MinRtrAdvInterval 16200		/* 4.5 hours */
#define GGSN_AdvValidLifetime	0xffffffff	/* infinite */
#define GGSN_AdvPreferredLifetime 0xffffffff	/* infinite */

/* RFC3307 link-local scope multicast address */
const struct in6_addr osmo_icmpv6_all_router_mcast_addr = {
	.s6_addr = { 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 }
};


/* RFC4291  link-local solicited-node multicast address, FF02:0:0:0:0:1:FF, 104 bits = 13 bytes */
const uint8_t osmo_icmpv6_solicited_node_mcast_addr_prefix[13] = {
	0xff, 0x02, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x00,
	0x00, 0x00, 0x00, 0x01,
	0xFF
};

/* Prepends the ipv6 header and returns checksum content */
uint16_t osmo_icmpv6_prepend_ip6hdr(struct msgb *msg, const struct in6_addr *saddr,
				    const struct in6_addr *daddr)
{
	uint32_t len;
	uint16_t skb_csum;
	struct ip6_hdr *i6h;

	/* checksum */
	skb_csum = osmo_ip_checksum_csum_partial(msgb_data(msg), msgb_length(msg), 0);
	len = msgb_length(msg);
	skb_csum = osmo_ip_checksum_csum_ipv6_magic(saddr, daddr, len, IPPROTO_ICMPV6, skb_csum);

	/* Push IPv6 header in front of ICMPv6 packet */
	i6h = (struct ip6_hdr *) msgb_push(msg, sizeof(*i6h));
	/* 4 bits version, 8 bits TC, 20 bits flow-ID */
	i6h->ip6_ctlun.ip6_un1.ip6_un1_flow = htonl(0x60000000);
	i6h->ip6_ctlun.ip6_un1.ip6_un1_plen = htons(len);
	i6h->ip6_ctlun.ip6_un1.ip6_un1_nxt = IPPROTO_ICMPV6;
	i6h->ip6_ctlun.ip6_un1.ip6_un1_hlim = 255;
	i6h->ip6_src = *saddr;
	i6h->ip6_dst = *daddr;
	return skb_csum;
}

/*! construct a RFC4861 compliant ICMPv6 router soliciation
 *  \param[in] saddr Source IPv6 address for router advertisement
 *  \param[in] daddr Destination IPv6 address for router advertisement IPv6 header
 *  \param[in] prefix The single prefix to be advertised (/64 implied!)
 *  \returns callee-allocated message buffer containing router advertisement */
struct msgb *osmo_icmpv6_construct_rs(const struct in6_addr *saddr)
{
	struct msgb *msg = msgb_alloc_headroom(512, 128, "IPv6 RS");
	struct osmo_icmpv6_rsol_hdr *rs;
	OSMO_ASSERT(msg);
	rs = (struct osmo_icmpv6_rsol_hdr *) msgb_put(msg, sizeof(*rs));
	rs->hdr.type = 133;	/* see RFC4861 4.1 */
	rs->hdr.code = 0;	/* see RFC4861 4.1 */
	rs->hdr.csum = 0;	/* updated below */
	rs->reserved = 0;	/* see RFC4861 4.1 */

	rs->hdr.csum = osmo_icmpv6_prepend_ip6hdr(msg, saddr, &osmo_icmpv6_all_router_mcast_addr);

	return msg;
}
/*! construct a 3GPP 29.061 compliant router advertisement for a given prefix
 *  \param[in] saddr Source IPv6 address for router advertisement
 *  \param[in] daddr Destination IPv6 address for router advertisement IPv6 header
 *  \param[in] prefix The single prefix to be advertised (/64 implied!)
 *  \returns callee-allocated message buffer containing router advertisement */
struct msgb *osmo_icmpv6_construct_ra(const struct in6_addr *saddr,
				      const struct in6_addr *daddr,
				      const struct in6_addr *prefix,
				      uint32_t mtu)
{
	struct msgb *msg = msgb_alloc_headroom(512, 128, "IPv6 RA");
	struct osmo_icmpv6_radv_hdr *ra;
	struct osmo_icmpv6_opt_prefix *ra_opt_pref;
	struct osmo_icmpv6_opt_mtu *ra_opt_mtu;

	OSMO_ASSERT(msg);

	ra = (struct osmo_icmpv6_radv_hdr *) msgb_put(msg, sizeof(*ra));
	ra->hdr.type = 134;	/* see RFC4861 4.2 */
	ra->hdr.code = 0;	/* see RFC4861 4.2 */
	ra->hdr.csum = 0;	/* updated below */
	ra->cur_ho_limit = 64;	/* seems reasonable? */
	/* the GGSN shall leave the M-flag cleared in the Router
	 * Advertisement messages */
	ra->m = 0;
	/* The GGSN may set the O-flag if there are additional
	 * configuration parameters that need to be fetched by the MS */
	ra->o = 0;		/* no DHCPv6 */
	ra->res = 0;
	/* RFC4861 Default: 3 * MaxRtrAdvInterval */
	ra->router_lifetime = htons(3*GGSN_MaxRtrAdvInterval);
	ra->reachable_time = 0;	/* Unspecified */

	/* RFC4861 Section 4.6.2 */
	ra_opt_pref = (struct osmo_icmpv6_opt_prefix *) msgb_put(msg, sizeof(*ra_opt_pref));
	ra_opt_pref->hdr.type = 3;	/* RFC4861 4.6.2 */
	ra_opt_pref->hdr.len = 4;	/* RFC4861 4.6.2 */
	ra_opt_pref->prefix_len = 64;	/* only prefix length as per 3GPP */
	/* The Prefix is contained in the Prefix Information Option of
	 * the Router Advertisements and shall have the A-flag set
	 * and the L-flag cleared */
	ra_opt_pref->a = 1;
	ra_opt_pref->l = 0;
	ra_opt_pref->res = 0;
	/*  The lifetime of the prefix shall be set to infinity */
	ra_opt_pref->valid_lifetime = htonl(GGSN_AdvValidLifetime);
	ra_opt_pref->preferred_lifetime = htonl(GGSN_AdvPreferredLifetime);
	ra_opt_pref->res2 = 0;
	memcpy(ra_opt_pref->prefix, prefix, sizeof(ra_opt_pref->prefix));

	/* RFC4861 Section 4.6.4, MTU */
	ra_opt_mtu = (struct osmo_icmpv6_opt_mtu *) msgb_put(msg, sizeof(*ra_opt_mtu));
	ra_opt_mtu->hdr.type = 5;	/* RFC4861 4.6.4 */
	ra_opt_mtu->hdr.len = 1;	/* RFC4861 4.6.4 */
	ra_opt_mtu->reserved = 0;
	ra_opt_mtu->mtu = htonl(mtu);

	/* The Prefix is contained in the Prefix Information Option of
	 * the Router Advertisements and shall have the A-flag set
	 * and the L-flag cleared */
	ra_opt_pref->a = 1;
	ra_opt_pref->l = 0;
	ra_opt_pref->res = 0;
	/*  The lifetime of the prefix shall be set to infinity */
	ra_opt_pref->valid_lifetime = htonl(GGSN_AdvValidLifetime);
	ra_opt_pref->preferred_lifetime = htonl(GGSN_AdvPreferredLifetime);
	ra_opt_pref->res2 = 0;
	memcpy(ra_opt_pref->prefix, prefix, sizeof(ra_opt_pref->prefix));

	/* checksum */
	ra->hdr.csum = osmo_icmpv6_prepend_ip6hdr(msg, saddr, daddr);

	return msg;
}

/* Validate an ICMPv6 router solicitation according to RFC4861 6.1.1 */
bool osmo_icmpv6_validate_router_solicit(const uint8_t *pack, unsigned len)
{
	const struct ip6_hdr *ip6h = (struct ip6_hdr *)pack;
	//const struct icmpv6_hdr *ic6h = (struct icmpv6_hdr *) (pack + sizeof(*ip6h));

	/* Hop limit field must have 255 */
	if (ip6h->ip6_ctlun.ip6_un1.ip6_un1_hlim != 255)
		return false;
	/* FIXME: ICMP checksum is valid */
	/* ICMP length (derived from IP length) is 8 or more octets */
	if (ip6h->ip6_ctlun.ip6_un1.ip6_un1_plen < 8)
		return false;
	/* FIXME: All included options have a length > 0 */
	/* FIXME: If IP source is unspecified, no source link-layer addr option */
	return true;
}

/* Validate an ICMPv6 router advertisement according to RFC4861 6.1.2.
   Returns pointer packet header on success, NULL otherwise. */
struct osmo_icmpv6_radv_hdr *osmo_icmpv6_validate_router_adv(const uint8_t *pack, unsigned len)
{
	const struct ip6_hdr *ip6h = (struct ip6_hdr *)pack;
	const struct osmo_icmpv6_hdr *ic6h = (struct osmo_icmpv6_hdr *) (pack + sizeof(*ip6h));

	/* ICMP length (derived from IP length) is 16 or more octets */
	if (len < sizeof(*ip6h) + 16)
		return NULL;

	if (ic6h->type != 134) /* router advertismenet type */
		return NULL;

	/*Routers must use their link-local address */
	if (!IN6_IS_ADDR_LINKLOCAL(&ip6h->ip6_src))
		return NULL;
	/* Hop limit field must have 255 */
	if (ip6h->ip6_ctlun.ip6_un1.ip6_un1_hlim != 255)
		return NULL;
	/* ICMP Code is 0 */
	if (ic6h->code != 0)
		return NULL;
	/* ICMP length (derived from IP length) is 16 or more octets */
	if (ip6h->ip6_ctlun.ip6_un1.ip6_un1_plen < 16)
		return NULL;
	/* FIXME: All included options have a length > 0 */
	/* FIXME: If IP source is unspecified, no source link-layer addr option */
	return (struct osmo_icmpv6_radv_hdr *)ic6h;
}
