/*
 * Copyright(c) 2010 Intel Corporation. All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Maintained at www.Open-FCoE.org
 */

/* Routines for automatic FIP VLAN discovery and creation */
/* Shared by fcoemon and fipvlan */

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <errno.h>
#include <getopt.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <net/if.h>
#include <net/if_arp.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/if_packet.h>
#include "fip.h"
#include "fcoemon_utils.h"
#include "rtnetlink.h"

#define FIP_LOG(...)			sa_log(__VA_ARGS__)
#define FIP_LOG_ERR(error, ...)		sa_log_err(error, __func__, __VA_ARGS__)
#define FIP_LOG_ERRNO(...)		sa_log_err(errno, __func__, __VA_ARGS__)
#define FIP_LOG_DBG(...)		sa_log_debug(__VA_ARGS__)

static int fip_mac_is_valid(unsigned char *mac)
{
	if (0x01 & mac[0])
		return 0;
	return !!(mac[0] | mac[1] | mac[2] | mac[3] | mac[4] | mac[5]);
}

/**
 * fip_get_sanmac - get SAN MAC through dcbnl interface
 * @ifindex: network interface index to send on
 * @addr: output buffer to the SAN MAC address
 *
 * Returns 0 for success, none 0 for failure
 */
static int fip_get_sanmac(int ifindex, unsigned char *addr)
{
	int s;
	int rc = -EIO;
	struct ifreq ifr;

	memset(addr, 0, ETHER_ADDR_LEN);
	s = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
	if (s < 0)
		return s;

	memset(&ifr, 0, sizeof(ifr));
	ifr.ifr_ifindex = ifindex;
	rc = ioctl(s, SIOCGIFNAME, &ifr);
	close(s);
	if (rc)
		return rc;

	rc = rtnl_get_sanmac(ifr.ifr_name, addr);
	if (rc)
		return rc;

	return !fip_mac_is_valid(addr);
}

/**
 * fip_socket_add_addr - add a MAC address to the input socket
 * @s: ETH_P_FIP packet socket to setsockopt on
 * @ifindex: network interface index to send on
 * @add: true to add false to del
 * @mac: MAC address to add or delete
 * @multi: false if unicast, true if multicast address
 */
static int
fip_socket_add_addr(int s, int ifindex, bool add, const __u8 *mac, bool multi)
{
	struct packet_mreq mr;
	int rc = 0;

	memset(&mr, 0, sizeof(mr));
	mr.mr_ifindex = ifindex;
	mr.mr_type = multi ? PACKET_MR_MULTICAST : PACKET_MR_UNICAST;
	mr.mr_alen = ETHER_ADDR_LEN;
	memcpy(mr.mr_address, mac, ETHER_ADDR_LEN);
	if (setsockopt(s, SOL_PACKET,
		       add ? PACKET_ADD_MEMBERSHIP : PACKET_DROP_MEMBERSHIP,
		       &mr, sizeof(mr)) < 0) {
		FIP_LOG_DBG("PACKET_%s_MEMBERSHIP:failed\n",
			    add ? "ADD" : "DROP");
		rc = -errno;
	}
	return rc;
}

/**
 * fip_socket_sanmac - add SAN MAC to the unicast list for input socket
 * @s: ETH_P_FIP packet socket to setsockopt on
 * @ifindex: network interface index to send on
 * @add: 1 to add 0 to del
 */
static int fip_socket_sanmac(int s, int ifindex, unsigned char *mac, int add)
{
	unsigned char smac[ETHER_ADDR_LEN];

	if (fip_get_sanmac(ifindex, smac)) {
		FIP_LOG_DBG("%s: no sanmac, ifindex %d\n", __func__, ifindex);
		memcpy(smac, mac, ETHER_ADDR_LEN);
	}

	return fip_socket_add_addr(s, ifindex, add, smac, false);
}

/**
 * fip_socket_multi - add multicast MAC address to the input socket
 * @s: ETH_P_FIP packet socket to setsockopt on
 * @ifindex: network interface index to send on
 * @add: true to add, false to del
 * @multi: Multicast destination
 */
static void
fip_socket_multi(int s, int ifindex, bool add, enum fip_multi multi)
{
	__u8 smac[ETHER_ADDR_LEN] = FIP_ALL_FCOE_MACS;

	smac[ETHER_ADDR_LEN - 1] = multi;
	fip_socket_add_addr(s, ifindex, add, smac, true);
}

/**
 * fip_ethhdr - fills up the ethhdr for FIP
 * @ifindex: network interface index to send on
 * @mac: mac address of the sending network interface
 * @eh: buffer for ether header
 * @dest: destination selector
 *
 * Note: assuming no VLAN
 */
static void fip_ethhdr(int ifindex, const unsigned char *mac, struct ethhdr *eh,
		       enum fip_multi multi)
{
	unsigned char smac[ETHER_ADDR_LEN];
	unsigned char dmac[ETHER_ADDR_LEN] = FIP_ALL_FCF_MACS;
	if (fip_get_sanmac(ifindex, smac))
		memcpy(smac, mac, ETHER_ADDR_LEN);

	dmac[ETHER_ADDR_LEN - 1] = multi;
	eh->h_proto = htons(ETH_P_FIP);
	memcpy(eh->h_source, smac, ETHER_ADDR_LEN);
	memcpy(eh->h_dest, dmac, ETHER_ADDR_LEN);
}

/**
 * drain_socket - Discard receive packets on a socket
 */
static void drain_socket(int s)
{
	char buf[4096];
	struct sockaddr_ll sa;
	struct iovec iov[] = {
		{ .iov_base = buf, .iov_len = sizeof(buf), },
	};
	struct msghdr msg = {
		.msg_name = &sa,
		.msg_namelen = sizeof(sa),
		.msg_iov = iov,
		.msg_iovlen = ARRAY_SIZE(iov),
	};

	while (recvmsg(s, &msg, MSG_DONTWAIT) > 0) {
		/* Drop the packet */
	}
}

/**
 * fip_socket - create and bind a packet socket for FIP
 * @ifindex: ifindex of netdevice to bind to
 * @multi: Indication of any multicast address to bind to
 */
int fip_socket(int ifindex, unsigned char *mac, enum fip_multi multi)
{
	struct sockaddr_ll sa = {
		.sll_family = AF_PACKET,
		.sll_protocol = htons(ETH_P_FIP),
		.sll_ifindex = ifindex,
	};
	int s;
	int rc;

	s = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_FIP));
	if (s < 0) {
		FIP_LOG_ERR(errno, "Failed to open FIP socket.\n");
		return s;
	}

	rc = fip_socket_sanmac(s, ifindex, mac, 1);
	if (rc < 0 && rc != -ENXIO) {
		FIP_LOG_ERR(errno, "Failed to open SANMAC socket.\n");
		close(s);
		return rc;
	}
	if (multi != FIP_NONE)
		fip_socket_multi(s, ifindex, true, multi);

	rc = bind(s, (struct sockaddr *) &sa, sizeof(sa));
	if (rc < 0) {
		FIP_LOG_ERR(errno, "Bind failed.\n");
		close(s);
		return rc;
	}

	/*
	 * Drain the packets that were received between socket and bind. We
	 * could've received packets not meant for our interface. This can
	 * interfere with vlan discovery
	 */
	drain_socket(s);

	return s;
}


/**
 * fip_send_vlan_request - send a FIP VLAN request
 * @s: ETH_P_FIP packet socket to send on
 * @ifindex: network interface index to send on
 * @mac: mac address of the sending network interface
 * @dest: destination selector
 *
 * Note: sends to address selected by @dest
 */
ssize_t fip_send_vlan_request(int s, int ifindex, const unsigned char *mac,
			      enum fip_multi dest)
{
	struct sockaddr_ll sa = {
		.sll_family = AF_PACKET,
		.sll_protocol = htons(ETH_P_FIP),
		.sll_ifindex = ifindex,
		.sll_hatype = ARPHRD_ETHER,
		.sll_pkttype = PACKET_MULTICAST,
		.sll_halen = ETHER_ADDR_LEN,
		.sll_addr = FIP_ALL_FCF_MACS,
	};
	struct fiphdr fh = {
		.fip_version = FIP_VERSION(1),
		.fip_proto = htons(FIP_PROTO_VLAN),
		.fip_subcode = FIP_VLAN_REQ,
		.fip_desc_len = htons(2),
		.fip_flags = 0,
	};
	struct {
		struct fip_tlv_mac_addr mac;
	} tlvs = {
		.mac = {
			.hdr = {
				.tlv_type = FIP_TLV_MAC_ADDR,
				.tlv_len = 2,
			},
		},
	};
	struct ethhdr eh;
	struct iovec iov[] = {
		{ .iov_base = &eh, .iov_len = sizeof(eh), },
		{ .iov_base = &fh, .iov_len = sizeof(fh), },
		{ .iov_base = &tlvs, .iov_len = sizeof(tlvs), },
	};
	struct msghdr msg = {
		.msg_name = &sa,
		.msg_namelen = sizeof(sa),
		.msg_iov = iov,
		.msg_iovlen = ARRAY_SIZE(iov),
	};
	int rc;

	fip_ethhdr(ifindex, mac, &eh, dest);
	sa.sll_addr[ETHER_ADDR_LEN - 1] = dest;
	memcpy(tlvs.mac.mac_addr, eh.h_source, ETHER_ADDR_LEN);
	FIP_LOG_DBG("sending FIP VLAN request");
	rc = sendmsg(s, &msg, 0);
	if (rc < 0) {
		rc = -errno;
		FIP_LOG_ERRNO("sendmsg error");
	}
	return rc;
}

/**
 * fip_send_vlan_notification - send a FIP VLAN notification
 * @s: ETH_P_FIP packet socket to send on
 * @ifindex: network interface index to send on
 * @mac: mac address of the sending network interface
 * @dest: destination mac address
 * @tlvs: pointer to vlan tlvs to send
 * @tlv_count: number of vlan tlvs to send
 */
int
fip_send_vlan_notification(int s, int ifindex, const __u8 *mac,
			   const __u8 *dest, struct fip_tlv_vlan *vtlvs,
			   int vlan_count)
{
	struct sockaddr_ll dsa = {
		.sll_family = AF_PACKET,
		.sll_protocol = htons(ETH_P_FIP),
		.sll_ifindex = ifindex,
		.sll_hatype = ARPHRD_ETHER,
		.sll_pkttype = PACKET_OTHERHOST,
		.sll_halen = ETHER_ADDR_LEN,
	};
	struct fiphdr sfh = {
		.fip_version = FIP_VERSION(1),
		.fip_proto = htons(FIP_PROTO_VLAN),
		.fip_subcode = FIP_VLAN_NOTE_VN2VN,
		.fip_desc_len = htons(2),
		.fip_flags = 0,
	};
	struct {
		struct fip_tlv_mac_addr mac;
	} tlvs = {
		.mac = {
			.hdr = {
				.tlv_type = FIP_TLV_MAC_ADDR,
				.tlv_len = 2,
			},
		},
	};
	struct ethhdr eh;
	struct iovec iov[] = {
		{ .iov_base = &eh, .iov_len = sizeof(eh), },
		{ .iov_base = &sfh, .iov_len = sizeof(sfh), },
		{ .iov_base = &tlvs, .iov_len = sizeof(tlvs), },
		{ .iov_base = NULL, .iov_len = 0, },
	};
	struct msghdr msg = {
		.msg_name = &dsa,
		.msg_namelen = sizeof(dsa),
		.msg_iov = iov,
		.msg_iovlen = ARRAY_SIZE(iov) - 1,
	};
	int rc;

	if (vlan_count) {
		sfh.fip_desc_len = htons(3);
		iov[3].iov_base = vtlvs;
		iov[3].iov_len = vlan_count * sizeof(*vtlvs);
		msg.msg_iovlen = ARRAY_SIZE(iov);
	}

	if (fip_get_sanmac(ifindex, eh.h_source))
		memcpy(eh.h_source, mac, ETHER_ADDR_LEN);
	eh.h_proto = htons(ETH_P_FIP);
	memcpy(dsa.sll_addr, dest, ETHER_ADDR_LEN);
	memcpy(eh.h_dest, dest, ETHER_ADDR_LEN);
	memcpy(tlvs.mac.mac_addr, eh.h_source, ETHER_ADDR_LEN);
	rc = sendmsg(s, &msg, 0);
	if (rc < 0) {
		rc = -errno;
		FIP_LOG_ERR(errno, "%s:sendmsg error\n", __func__);
	} else {
		FIP_LOG_DBG("%s: sent %d bytes to ifindex %d\n",
			    __func__, rc, ifindex);
	}
	return rc;
}

/**
 * fip_recv - receive from a FIP packet socket
 * @s: packet socket with data ready to be received
 * @fn: FIP receive callback to process the payload
 * @arg: argument to pass through to @fn
 */
int fip_recv(int s, fip_handler *fn, void *arg)
{
	char buf[4096];
	struct sockaddr_ll sa;
	struct iovec iov[] = {
		{ .iov_base = buf, .iov_len = sizeof(buf), },
	};
	struct msghdr msg = {
		.msg_name = &sa,
		.msg_namelen = sizeof(sa),
		.msg_iov = iov,
		.msg_iovlen = ARRAY_SIZE(iov),
	};
	struct fiphdr *fh;
	size_t len, desc_len;
	int rc;
	struct ethhdr *eth = (struct ethhdr *)buf;

	rc = recvmsg(s, &msg, MSG_DONTWAIT);
	if (rc < 0) {
		FIP_LOG_ERRNO("packet socket recv error");
		return rc;
	}

	len = rc;
	if (len < sizeof(*fh)) {
		FIP_LOG_ERR(EINVAL, "received packed smaller that FIP header");
		return -1;
	}

	if (eth->h_proto == htons(ETH_P_8021Q))
		fh = (struct fiphdr *) (buf + sizeof(struct ethhdr) + VLAN_HLEN);
	else
		fh = (struct fiphdr *) (buf + sizeof(struct ethhdr));

	desc_len = ntohs(fh->fip_desc_len);
	if (len < (sizeof(*fh) + (desc_len << 2))) {
		FIP_LOG_ERR(EINVAL, "received data less that FIP descriptor");
		return -1;
	}

	if (fn)
		return fn(fh, &sa, arg);
	return 0;
}
