/* -*- mode: c; c-file-style: "openbsd" -*- */
/*
 * Copyright (c) 2008 Vincent Bernat <bernat@luffy.cx>
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "../lldpd.h"
#include "../frame.h"

#ifdef ENABLE_EDP

#  include <stdio.h>
#  include <unistd.h>
#  include <errno.h>
#  include <arpa/inet.h>
#  include <fnmatch.h>
#  include <sys/param.h>

static int seq = 0;

int
edp_send(struct lldpd *global, struct lldpd_hardware *hardware)
{
	const u_int8_t mcastaddr[] = EDP_MULTICAST_ADDR;
	const u_int8_t llcorg[] = LLC_ORG_EXTREME;
	struct lldpd_chassis *chassis;
	int length, i, v;
	u_int8_t *packet, *pos, *pos_llc, *pos_len_eh, *pos_len_edp, *pos_edp, *tlv,
	    *end;
	u_int16_t checksum;
#  ifdef ENABLE_DOT1
	struct lldpd_vlan *vlan;
	unsigned int state = 0;
#  endif
	u_int8_t edp_fakeversion[] = { 7, 6, 4, 99 };
	/* Subsequent XXX can be replaced by other values. We place
	   them here to ensure the position of "" to be a bit
	   invariant with version changes. */
	const char *deviceslot[] = { "eth", "veth", "XXX", "XXX", "XXX", "XXX", "XXX",
		"XXX", "", NULL };

	log_debug("edp", "send EDP frame on port %s", hardware->h_ifname);

	chassis = hardware->h_lport.p_chassis;
#  ifdef ENABLE_DOT1
	while (state != 2) {
#  endif
		length = hardware->h_mtu;
		if ((packet = (u_int8_t *)calloc(1, length)) == NULL) return ENOMEM;
		pos = packet;
		v = 0;

		/* Ethernet header */
		if (!(POKE_BYTES(mcastaddr, sizeof(mcastaddr)) &&
			POKE_BYTES(&hardware->h_lladdr, ETHER_ADDR_LEN) &&
			POKE_SAVE(pos_len_eh) && /* We compute the len later */
			POKE_UINT16(0)))
			goto toobig;

		/* LLC */
		if (!(POKE_SAVE(pos_llc) && /* We need to save our
					       current position to
					       compute ethernet len */
			/* SSAP and DSAP */
			POKE_UINT8(0xaa) && POKE_UINT8(0xaa) &&
			/* Control field */
			POKE_UINT8(0x03) &&
			/* ORG */
			POKE_BYTES(llcorg, sizeof(llcorg)) && POKE_UINT16(LLC_PID_EDP)))
			goto toobig;

		/* EDP header */
		if ((chassis->c_id_len != ETHER_ADDR_LEN) ||
		    (chassis->c_id_subtype != LLDP_CHASSISID_SUBTYPE_LLADDR)) {
			log_warnx("edp",
			    "local chassis does not use MAC address as chassis ID!?");
			free(packet);
			return EINVAL;
		}
		if (!(POKE_SAVE(pos_edp) && /* Save the start of EDP frame */
			POKE_UINT8(1) && POKE_UINT8(0) &&
			POKE_SAVE(pos_len_edp) && /* We compute the len
						     and the checksum
						     later */
			POKE_UINT32(0) &&	  /* Len + Checksum */
			POKE_UINT16(seq) && POKE_UINT16(0) &&
			POKE_BYTES(chassis->c_id, ETHER_ADDR_LEN)))
			goto toobig;
		seq++;

#  ifdef ENABLE_DOT1
		switch (state) {
		case 0:
#  endif
			/* Display TLV */
			if (!(POKE_START_EDP_TLV(EDP_TLV_DISPLAY) &&
				POKE_BYTES(chassis->c_name, strlen(chassis->c_name)) &&
				POKE_UINT8(0) && /* Add a NULL character
						    for better
						    compatibility */
				POKE_END_EDP_TLV))
				goto toobig;

			/* Info TLV */
			if (!(POKE_START_EDP_TLV(EDP_TLV_INFO))) goto toobig;
			/* We try to emulate the slot thing */
			for (i = 0; deviceslot[i] != NULL; i++) {
				if (strncmp(hardware->h_ifname, deviceslot[i],
					strlen(deviceslot[i])) == 0) {
					if (!(POKE_UINT16(i) &&
						POKE_UINT16(atoi(hardware->h_ifname +
						    strlen(deviceslot[i])))))
						goto toobig;
					break;
				}
			}
			/* If we don't find a "slot", we say that the
			   interface is in slot 8 */
			if (deviceslot[i] == NULL) {
				if (!(POKE_UINT16(8) &&
					POKE_UINT16(hardware->h_ifindex)))
					goto toobig;
			}
			if (!(POKE_UINT16(0) &&			    /* vchassis */
				POKE_UINT32(0) && POKE_UINT16(0) && /* Reserved */
				/* Version */
				POKE_BYTES(edp_fakeversion, sizeof(edp_fakeversion)) &&
				/* Connections, we say that we won't
				   have more interfaces than this
				   mask. */
				POKE_UINT32(0xffffffff) && POKE_UINT32(0) &&
				POKE_UINT32(0) && POKE_UINT32(0) && POKE_END_EDP_TLV))
				goto toobig;

#  ifdef ENABLE_DOT1
			break;
		case 1:
			TAILQ_FOREACH (vlan, &hardware->h_lport.p_vlans, v_entries) {
				v++;
				if (!(POKE_START_EDP_TLV(EDP_TLV_VLAN) &&
					POKE_UINT8(0) && /* Flags: no IP address */
					POKE_UINT8(0) && /* Reserved */
					POKE_UINT16(vlan->v_vid) &&
					POKE_UINT32(0) && /* Reserved */
					POKE_UINT32(0) && /* IP address */
					/* VLAN name */
					POKE_BYTES(vlan->v_name,
					    strlen(vlan->v_name)) &&
					POKE_UINT8(0) && POKE_END_EDP_TLV))
					goto toobig;
			}
			break;
		}

		if ((state == 1) && (v == 0)) {
			/* No VLAN, no need to send another TLV */
			free(packet);
			break;
		}
#  endif

		/* Null TLV */
		if (!(POKE_START_EDP_TLV(EDP_TLV_NULL) && POKE_END_EDP_TLV &&
			POKE_SAVE(end)))
			goto toobig;

		/* Compute len and checksum */
		i = end - pos_llc; /* Ethernet length */
		v = end - pos_edp; /* EDP length */
		POKE_RESTORE(pos_len_eh);
		if (!(POKE_UINT16(i))) goto toobig;
		POKE_RESTORE(pos_len_edp);
		if (!(POKE_UINT16(v))) goto toobig;
		checksum = frame_checksum(pos_edp, v, 0);
		if (!(POKE_UINT16(checksum))) goto toobig;

		if (interfaces_send_helper(global, hardware, (char *)packet,
			end - packet) == -1) {
			log_warn("edp", "unable to send packet on real device for %s",
			    hardware->h_ifname);
			free(packet);
			return ENETDOWN;
		}
		free(packet);

#  ifdef ENABLE_DOT1
		state++;
	}
#  endif

	hardware->h_tx_cnt++;
	return 0;
toobig:
	free(packet);
	return E2BIG;
}

#  define CHECK_TLV_SIZE(x, name)                                  \
    do {                                                           \
      if (tlv_len < (x)) {                                         \
	log_warnx("edp", name " EDP TLV too short received on %s", \
	    hardware->h_ifname);                                   \
	goto malformed;                                            \
      }                                                            \
    } while (0)

int
edp_decode(struct lldpd *cfg, char *frame, int s, struct lldpd_hardware *hardware,
    struct lldpd_chassis **newchassis, struct lldpd_port **newport)
{
	struct lldpd_chassis *chassis;
	struct lldpd_port *port;
#  ifdef ENABLE_DOT1
	struct lldpd_mgmt *mgmt, *mgmt_next, *m;
	struct lldpd_vlan *lvlan = NULL, *lvlan_next;
#  endif
	const unsigned char edpaddr[] = EDP_MULTICAST_ADDR;
	int length, gotend = 0, gotvlans = 0, edp_len, tlv_len, tlv_type;
	int edp_port, edp_slot;
	u_int8_t *pos, *pos_edp, *tlv;
	u_int8_t version[4];
#  ifdef ENABLE_DOT1
	struct in_addr address;
	struct lldpd_port *oport;
#  endif

	log_debug("edp", "decode EDP frame on port %s", hardware->h_ifname);

	if ((chassis = calloc(1, sizeof(struct lldpd_chassis))) == NULL) {
		log_warn("edp", "failed to allocate remote chassis");
		return -1;
	}
	TAILQ_INIT(&chassis->c_mgmt);
	if ((port = calloc(1, sizeof(struct lldpd_port))) == NULL) {
		log_warn("edp", "failed to allocate remote port");
		free(chassis);
		return -1;
	}
#  ifdef ENABLE_DOT1
	TAILQ_INIT(&port->p_vlans);
#  endif

	length = s;
	pos = (u_int8_t *)frame;

	if (length < 2 * ETHER_ADDR_LEN + sizeof(u_int16_t) + 8 /* LLC */ + 10 +
		ETHER_ADDR_LEN /* EDP header */) {
		log_warnx("edp", "too short EDP frame received on %s",
		    hardware->h_ifname);
		goto malformed;
	}

	if (PEEK_CMP(edpaddr, sizeof(edpaddr)) != 0) {
		log_info("edp",
		    "frame not targeted at EDP multicast address received on %s",
		    hardware->h_ifname);
		goto malformed;
	}
	PEEK_DISCARD(ETHER_ADDR_LEN);
	PEEK_DISCARD_UINT16;
	PEEK_DISCARD(6); /* LLC: DSAP + SSAP + control + org */
	if (PEEK_UINT16 != LLC_PID_EDP) {
		log_debug("edp", "incorrect LLC protocol ID received on %s",
		    hardware->h_ifname);
		goto malformed;
	}

	(void)PEEK_SAVE(pos_edp); /* Save the start of EDP packet */
	if (PEEK_UINT8 != 1) {
		log_warnx("edp", "incorrect EDP version for frame received on %s",
		    hardware->h_ifname);
		goto malformed;
	}
	PEEK_DISCARD_UINT8; /* Reserved */
	edp_len = PEEK_UINT16;
	PEEK_DISCARD_UINT16;	/* Checksum */
	PEEK_DISCARD_UINT16;	/* Sequence */
	if (PEEK_UINT16 != 0) { /* ID Type = 0 = MAC */
		log_warnx("edp", "incorrect device id type for frame received on %s",
		    hardware->h_ifname);
		goto malformed;
	}
	if (edp_len > length + 10) {
		log_warnx("edp", "incorrect size for EDP frame received on %s",
		    hardware->h_ifname);
		goto malformed;
	}
	port->p_ttl = cfg ? cfg->g_config.c_tx_interval * cfg->g_config.c_tx_hold : 0;
	port->p_ttl = MIN((port->p_ttl + 999) / 1000, 65535);
	chassis->c_id_subtype = LLDP_CHASSISID_SUBTYPE_LLADDR;
	chassis->c_id_len = ETHER_ADDR_LEN;
	if ((chassis->c_id = (char *)malloc(ETHER_ADDR_LEN)) == NULL) {
		log_warn("edp", "unable to allocate memory for chassis ID");
		goto malformed;
	}
	PEEK_BYTES(chassis->c_id, ETHER_ADDR_LEN);

#  ifndef FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION
	/* Let's check checksum */
	if (frame_checksum(pos_edp, edp_len, 0) != 0) {
		log_warnx("edp", "incorrect EDP checksum for frame received on %s",
		    hardware->h_ifname);
		goto malformed;
	}
#  endif

	while (length && !gotend) {
		if (length < 4) {
			log_warnx("edp",
			    "EDP TLV header is too large for "
			    "frame received on %s",
			    hardware->h_ifname);
			goto malformed;
		}
		if (PEEK_UINT8 != EDP_TLV_MARKER) {
			log_warnx("edp",
			    "incorrect marker starting EDP TLV header for frame "
			    "received on %s",
			    hardware->h_ifname);
			goto malformed;
		}
		tlv_type = PEEK_UINT8;
		tlv_len = PEEK_UINT16 - 4;
		(void)PEEK_SAVE(tlv);
		if ((tlv_len < 0) || (tlv_len > length)) {
			log_debug("edp",
			    "incorrect size in EDP TLV header for frame "
			    "received on %s",
			    hardware->h_ifname);
			/* Some poor old Extreme Summit are quite bogus */
			gotend = 1;
			break;
		}
		switch (tlv_type) {
		case EDP_TLV_INFO:
			CHECK_TLV_SIZE(32, "Info");
			port->p_id_subtype = LLDP_PORTID_SUBTYPE_IFNAME;
			edp_slot = PEEK_UINT16;
			edp_port = PEEK_UINT16;
			free(port->p_id);
			port->p_id_len =
			    asprintf(&port->p_id, "%d/%d", edp_slot + 1, edp_port + 1);
			if (port->p_id_len == -1) {
				log_warn("edp",
				    "unable to allocate memory for "
				    "port ID");
				goto malformed;
			}
			free(port->p_descr);
			if (asprintf(&port->p_descr, "Slot %d / Port %d", edp_slot + 1,
				edp_port + 1) == -1) {
				log_warn("edp",
				    "unable to allocate memory for "
				    "port description");
				goto malformed;
			}
			PEEK_DISCARD_UINT16; /* vchassis */
			PEEK_DISCARD(6);     /* Reserved */
			PEEK_BYTES(version, 4);
			free(chassis->c_descr);
			if (asprintf(&chassis->c_descr,
				"EDP enabled device, version %d.%d.%d.%d", version[0],
				version[1], version[2], version[3]) == -1) {
				log_warn("edp",
				    "unable to allocate memory for "
				    "chassis description");
				goto malformed;
			}
			break;
		case EDP_TLV_DISPLAY:
			free(chassis->c_name);
			if ((chassis->c_name = (char *)calloc(1, tlv_len + 1)) ==
			    NULL) {
				log_warn("edp",
				    "unable to allocate memory for chassis "
				    "name");
				goto malformed;
			}
			/* TLV display contains a lot of garbage */
			PEEK_BYTES(chassis->c_name, tlv_len);
			break;
		case EDP_TLV_NULL:
			if (tlv_len != 0) {
				log_warnx("edp",
				    "null tlv with incorrect size in frame "
				    "received on %s",
				    hardware->h_ifname);
				goto malformed;
			}
			if (length)
				log_debug("edp", "extra data after edp frame on %s",
				    hardware->h_ifname);
			gotend = 1;
			break;
		case EDP_TLV_VLAN:
#  ifdef ENABLE_DOT1
			CHECK_TLV_SIZE(12, "VLAN");
			if ((lvlan = (struct lldpd_vlan *)calloc(1,
				 sizeof(struct lldpd_vlan))) == NULL) {
				log_warn("edp", "unable to allocate vlan");
				goto malformed;
			}
			PEEK_DISCARD_UINT16;	    /* Flags + reserved */
			lvlan->v_vid = PEEK_UINT16; /* VID */
			PEEK_DISCARD(4);	    /* Reserved */
			PEEK_BYTES(&address, sizeof(address));

			if (address.s_addr != INADDR_ANY) {
				mgmt = lldpd_alloc_mgmt(LLDPD_AF_IPV4, &address,
				    sizeof(struct in_addr), 0);
				if (mgmt == NULL) {
					log_warn("edp", "Out of memory");
					goto malformed;
				}
				TAILQ_INSERT_TAIL(&chassis->c_mgmt, mgmt, m_entries);
			}

			if ((lvlan->v_name = (char *)calloc(1, tlv_len + 1 - 12)) ==
			    NULL) {
				log_warn("edp", "unable to allocate vlan name");
				goto malformed;
			}
			PEEK_BYTES(lvlan->v_name, tlv_len - 12);

			TAILQ_INSERT_TAIL(&port->p_vlans, lvlan, v_entries);
			lvlan = NULL;
#  endif
			gotvlans = 1;
			break;
		default:
			log_debug("edp", "unknown EDP TLV type (%d) received on %s",
			    tlv_type, hardware->h_ifname);
			hardware->h_rx_unrecognized_cnt++;
		}
		PEEK_DISCARD(tlv + tlv_len - pos);
	}
	if ((chassis->c_id == NULL) || (port->p_id == NULL) ||
	    (chassis->c_name == NULL) || (chassis->c_descr == NULL) ||
	    (port->p_descr == NULL) || (gotend == 0)) {
#  ifdef ENABLE_DOT1
		if (gotvlans && gotend) {
			/* VLAN can be sent in a separate frames. We need to add
			 * those vlans to an existing port */
			TAILQ_FOREACH (oport, &hardware->h_rports, p_entries) {
				if (!((oport->p_protocol == LLDPD_MODE_EDP) &&
					(oport->p_chassis->c_id_subtype ==
					    chassis->c_id_subtype) &&
					(oport->p_chassis->c_id_len ==
					    chassis->c_id_len) &&
					(memcmp(oport->p_chassis->c_id, chassis->c_id,
					     chassis->c_id_len) == 0)))
					continue;
				/* We attach the VLANs to the found port */
				lldpd_vlan_cleanup(oport);
				for (lvlan = TAILQ_FIRST(&port->p_vlans); lvlan != NULL;
				     lvlan = lvlan_next) {
					lvlan_next = TAILQ_NEXT(lvlan, v_entries);
					TAILQ_REMOVE(&port->p_vlans, lvlan, v_entries);
					TAILQ_INSERT_TAIL(&oport->p_vlans, lvlan,
					    v_entries);
				}
				/* And the IP addresses */
				for (mgmt = TAILQ_FIRST(&chassis->c_mgmt); mgmt != NULL;
				     mgmt = mgmt_next) {
					mgmt_next = TAILQ_NEXT(mgmt, m_entries);
					TAILQ_REMOVE(&chassis->c_mgmt, mgmt, m_entries);
					/* Don't add an address that already exists! */
					TAILQ_FOREACH (m, &chassis->c_mgmt, m_entries)
						if (m->m_family == mgmt->m_family &&
						    !memcmp(&m->m_addr, &mgmt->m_addr,
							sizeof(m->m_addr)))
							break;
					if (m == NULL)
						TAILQ_INSERT_TAIL(
						    &oport->p_chassis->c_mgmt, mgmt,
						    m_entries);
				}
			}
			/* We discard the remaining frame */
			goto malformed;
		}
#  else
		if (gotvlans) goto malformed;
#  endif
		log_warnx("edp",
		    "some mandatory tlv are missing for frame received on %s",
		    hardware->h_ifname);
		goto malformed;
	}
	*newchassis = chassis;
	*newport = port;
	return 1;

malformed:
#  ifdef ENABLE_DOT1
	free(lvlan);
#  endif
	lldpd_chassis_cleanup(chassis, 1);
	lldpd_port_cleanup(port, 1);
	free(port);
	return -1;
}

#endif /* ENABLE_EDP */
