#include <linux/udp.h>
 #include <linux/if_vlan.h>
 
-/* This is the offset of the options field in a dhcp packet starting at
- * the beginning of the dhcp header
+/* These are the offsets of the "hw type" and "hw address length" in the dhcp
+ * packet starting at the beginning of the dhcp header
  */
-#define BATADV_DHCP_OPTIONS_OFFSET 240
-#define BATADV_DHCP_REQUEST 3
+#define BATADV_DHCP_HTYPE_OFFSET       1
+#define BATADV_DHCP_HLEN_OFFSET                2
+/* Value of htype representing Ethernet */
+#define BATADV_DHCP_HTYPE_ETHERNET     0x01
+/* This is the offset of the "chaddr" field in the dhcp packet starting at the
+ * beginning of the dhcp header
+ */
+#define BATADV_DHCP_CHADDR_OFFSET      28
 
 static void batadv_gw_node_free_ref(struct batadv_gw_node *gw_node)
 {
        return 0;
 }
 
-/* this call might reallocate skb data */
-static bool batadv_is_type_dhcprequest(struct sk_buff *skb, int header_len)
-{
-       int ret = false;
-       unsigned char *p;
-       int pkt_len;
-
-       if (skb_linearize(skb) < 0)
-               goto out;
-
-       pkt_len = skb_headlen(skb);
-
-       if (pkt_len < header_len + BATADV_DHCP_OPTIONS_OFFSET + 1)
-               goto out;
-
-       p = skb->data + header_len + BATADV_DHCP_OPTIONS_OFFSET;
-       pkt_len -= header_len + BATADV_DHCP_OPTIONS_OFFSET + 1;
-
-       /* Access the dhcp option lists. Each entry is made up by:
-        * - octet 1: option type
-        * - octet 2: option data len (only if type != 255 and 0)
-        * - octet 3: option data
-        */
-       while (*p != 255 && !ret) {
-               /* p now points to the first octet: option type */
-               if (*p == 53) {
-                       /* type 53 is the message type option.
-                        * Jump the len octet and go to the data octet
-                        */
-                       if (pkt_len < 2)
-                               goto out;
-                       p += 2;
-
-                       /* check if the message type is what we need */
-                       if (*p == BATADV_DHCP_REQUEST)
-                               ret = true;
-                       break;
-               } else if (*p == 0) {
-                       /* option type 0 (padding), just go forward */
-                       if (pkt_len < 1)
-                               goto out;
-                       pkt_len--;
-                       p++;
-               } else {
-                       /* This is any other option. So we get the length... */
-                       if (pkt_len < 1)
-                               goto out;
-                       pkt_len--;
-                       p++;
-
-                       /* ...and then we jump over the data */
-                       if (pkt_len < 1 + (*p))
-                               goto out;
-                       pkt_len -= 1 + (*p);
-                       p += 1 + (*p);
-               }
-       }
-out:
-       return ret;
-}
-
-/* this call might reallocate skb data */
-bool batadv_gw_is_dhcp_target(struct sk_buff *skb, unsigned int *header_len)
+/**
+ * batadv_gw_dhcp_recipient_get - check if a packet is a DHCP message
+ * @skb: the packet to check
+ * @header_len: a pointer to the batman-adv header size
+ * @chaddr: buffer where the client address will be stored. Valid
+ *  only if the function returns BATADV_DHCP_TO_CLIENT
+ *
+ * Returns:
+ * - BATADV_DHCP_NO if the packet is not a dhcp message or if there was an error
+ *   while parsing it
+ * - BATADV_DHCP_TO_SERVER if this is a message going to the DHCP server
+ * - BATADV_DHCP_TO_CLIENT if this is a message going to a DHCP client
+ *
+ * This function may re-allocate the data buffer of the skb passed as argument.
+ */
+enum batadv_dhcp_recipient
+batadv_gw_dhcp_recipient_get(struct sk_buff *skb, unsigned int *header_len,
+                            uint8_t *chaddr)
 {
+       enum batadv_dhcp_recipient ret = BATADV_DHCP_NO;
        struct ethhdr *ethhdr;
        struct iphdr *iphdr;
        struct ipv6hdr *ipv6hdr;
        struct udphdr *udphdr;
        struct vlan_ethhdr *vhdr;
+       int chaddr_offset;
        __be16 proto;
+       uint8_t *p;
 
        /* check for ethernet header */
        if (!pskb_may_pull(skb, *header_len + ETH_HLEN))
-               return false;
+               return BATADV_DHCP_NO;
+
        ethhdr = (struct ethhdr *)skb->data;
        proto = ethhdr->h_proto;
        *header_len += ETH_HLEN;
        /* check for initial vlan header */
        if (proto == htons(ETH_P_8021Q)) {
                if (!pskb_may_pull(skb, *header_len + VLAN_HLEN))
-                       return false;
+                       return BATADV_DHCP_NO;
 
                vhdr = (struct vlan_ethhdr *)skb->data;
                proto = vhdr->h_vlan_encapsulated_proto;
        switch (proto) {
        case htons(ETH_P_IP):
                if (!pskb_may_pull(skb, *header_len + sizeof(*iphdr)))
-                       return false;
+                       return BATADV_DHCP_NO;
+
                iphdr = (struct iphdr *)(skb->data + *header_len);
                *header_len += iphdr->ihl * 4;
 
                /* check for udp header */
                if (iphdr->protocol != IPPROTO_UDP)
-                       return false;
+                       return BATADV_DHCP_NO;
 
                break;
        case htons(ETH_P_IPV6):
                if (!pskb_may_pull(skb, *header_len + sizeof(*ipv6hdr)))
-                       return false;
+                       return BATADV_DHCP_NO;
+
                ipv6hdr = (struct ipv6hdr *)(skb->data + *header_len);
                *header_len += sizeof(*ipv6hdr);
 
                /* check for udp header */
                if (ipv6hdr->nexthdr != IPPROTO_UDP)
-                       return false;
+                       return BATADV_DHCP_NO;
 
                break;
        default:
-               return false;
+               return BATADV_DHCP_NO;
        }
 
        if (!pskb_may_pull(skb, *header_len + sizeof(*udphdr)))
-               return false;
+               return BATADV_DHCP_NO;
 
        /* skb->data might have been reallocated by pskb_may_pull() */
        ethhdr = (struct ethhdr *)skb->data;
        *header_len += sizeof(*udphdr);
 
        /* check for bootp port */
-       if ((proto == htons(ETH_P_IP)) &&
-           (udphdr->dest != htons(67)))
-               return false;
+       switch (proto) {
+       case htons(ETH_P_IP):
+               if (udphdr->dest == htons(67))
+                       ret = BATADV_DHCP_TO_SERVER;
+               else if (udphdr->source == htons(67))
+                       ret = BATADV_DHCP_TO_CLIENT;
+               break;
+       case htons(ETH_P_IPV6):
+               if (udphdr->dest == htons(547))
+                       ret = BATADV_DHCP_TO_SERVER;
+               else if (udphdr->source == htons(547))
+                       ret = BATADV_DHCP_TO_CLIENT;
+               break;
+       }
 
-       if ((proto == htons(ETH_P_IPV6)) &&
-           (udphdr->dest != htons(547)))
-               return false;
+       chaddr_offset = *header_len + BATADV_DHCP_CHADDR_OFFSET;
+       /* store the client address if the message is going to a client */
+       if (ret == BATADV_DHCP_TO_CLIENT &&
+           pskb_may_pull(skb, chaddr_offset + ETH_ALEN)) {
+               /* check if the DHCP packet carries an Ethernet DHCP */
+               p = skb->data + *header_len + BATADV_DHCP_HTYPE_OFFSET;
+               if (*p != BATADV_DHCP_HTYPE_ETHERNET)
+                       return BATADV_DHCP_NO;
+
+               /* check if the DHCP packet carries a valid Ethernet address */
+               p = skb->data + *header_len + BATADV_DHCP_HLEN_OFFSET;
+               if (*p != ETH_ALEN)
+                       return BATADV_DHCP_NO;
+
+               memcpy(chaddr, skb->data + chaddr_offset, ETH_ALEN);
+       }
 
-       return true;
+       return ret;
 }
-
 /**
  * batadv_gw_out_of_range - check if the dhcp request destination is the best gw
  * @bat_priv: the bat priv with all the soft interface information
  * false otherwise.
  *
  * This call might reallocate skb data.
+ * Must be invoked only when the DHCP packet is going TO a DHCP SERVER.
  */
 bool batadv_gw_out_of_range(struct batadv_priv *bat_priv,
                            struct sk_buff *skb)
        struct batadv_neigh_node *neigh_curr = NULL, *neigh_old = NULL;
        struct batadv_orig_node *orig_dst_node = NULL;
        struct batadv_gw_node *gw_node = NULL, *curr_gw = NULL;
-       struct ethhdr *ethhdr;
-       bool ret, out_of_range = false;
-       unsigned int header_len = 0;
+       struct ethhdr *ethhdr = (struct ethhdr *)skb->data;
+       bool out_of_range = false;
        uint8_t curr_tq_avg;
        unsigned short vid;
 
        vid = batadv_get_vid(skb, 0);
 
-       ret = batadv_gw_is_dhcp_target(skb, &header_len);
-       if (!ret)
-               goto out;
-
-       ethhdr = (struct ethhdr *)skb->data;
        orig_dst_node = batadv_transtable_search(bat_priv, ethhdr->h_source,
                                                 ethhdr->h_dest, vid);
        if (!orig_dst_node)
        if (!gw_node->bandwidth_down == 0)
                goto out;
 
-       ret = batadv_is_type_dhcprequest(skb, header_len);
-       if (!ret)
-               goto out;
-
        switch (atomic_read(&bat_priv->gw_mode)) {
        case BATADV_GW_MODE_SERVER:
                /* If we are a GW then we are our best GW. We can artificially
 
                                                   0x00, 0x00};
        static const uint8_t ectp_addr[ETH_ALEN] = {0xCF, 0x00, 0x00, 0x00,
                                                    0x00, 0x00};
+       enum batadv_dhcp_recipient dhcp_rcp = BATADV_DHCP_NO;
+       uint8_t *dst_hint = NULL, chaddr[ETH_ALEN];
        struct vlan_ethhdr *vhdr;
        unsigned int header_len = 0;
        int data_len = skb->len, ret;
        bool do_bcast = false, client_added;
        unsigned short vid;
        uint32_t seqno;
+       int gw_mode;
 
        if (atomic_read(&bat_priv->mesh_state) != BATADV_MESH_ACTIVE)
                goto dropped;
        if (batadv_compare_eth(ethhdr->h_dest, ectp_addr))
                goto dropped;
 
+       gw_mode = atomic_read(&bat_priv->gw_mode);
        if (is_multicast_ether_addr(ethhdr->h_dest)) {
-               do_bcast = true;
-
-               switch (atomic_read(&bat_priv->gw_mode)) {
-               case BATADV_GW_MODE_SERVER:
-                       /* gateway servers should not send dhcp
-                        * requests into the mesh
-                        */
-                       ret = batadv_gw_is_dhcp_target(skb, &header_len);
-                       if (ret)
-                               goto dropped;
-                       break;
-               case BATADV_GW_MODE_CLIENT:
-                       /* gateway clients should send dhcp requests
-                        * via unicast to their gateway
-                        */
-                       ret = batadv_gw_is_dhcp_target(skb, &header_len);
-                       if (ret)
-                               do_bcast = false;
-                       break;
-               case BATADV_GW_MODE_OFF:
-               default:
-                       break;
+               /* if gw mode is off, broadcast every packet */
+               if (gw_mode == BATADV_GW_MODE_OFF) {
+                       do_bcast = true;
+                       goto send;
                }
 
-               /* reminder: ethhdr might have become unusable from here on
-                * (batadv_gw_is_dhcp_target() might have reallocated skb data)
+               dhcp_rcp = batadv_gw_dhcp_recipient_get(skb, &header_len,
+                                                       chaddr);
+               /* skb->data may have been modified by
+                * batadv_gw_dhcp_recipient_get()
                 */
+               ethhdr = (struct ethhdr *)skb->data;
+               /* if gw_mode is on, broadcast any non-DHCP message.
+                * All the DHCP packets are going to be sent as unicast
+                */
+               if (dhcp_rcp == BATADV_DHCP_NO) {
+                       do_bcast = true;
+                       goto send;
+               }
+
+               if (dhcp_rcp == BATADV_DHCP_TO_CLIENT)
+                       dst_hint = chaddr;
+               else if ((gw_mode == BATADV_GW_MODE_SERVER) &&
+                        (dhcp_rcp == BATADV_DHCP_TO_SERVER))
+                       /* gateways should not forward any DHCP message if
+                        * directed to a DHCP server
+                        */
+                       goto dropped;
        }
 
+send:
        batadv_skb_set_priority(skb, 0);
 
        /* ethernet packet should be broadcasted */
 
        /* unicast packet */
        } else {
-               if (atomic_read(&bat_priv->gw_mode) != BATADV_GW_MODE_OFF) {
+               /* DHCP packets going to a server will use the GW feature */
+               if (dhcp_rcp == BATADV_DHCP_TO_SERVER) {
                        ret = batadv_gw_out_of_range(bat_priv, skb);
                        if (ret)
                                goto dropped;
-               }
-
-               if (batadv_dat_snoop_outgoing_arp_request(bat_priv, skb))
-                       goto dropped;
-
-               batadv_dat_snoop_outgoing_arp_reply(bat_priv, skb);
-
-               if (is_multicast_ether_addr(ethhdr->h_dest))
                        ret = batadv_send_skb_via_gw(bat_priv, skb, vid);
-               else
-                       ret = batadv_send_skb_via_tt(bat_priv, skb, vid);
+               } else {
+                       if (batadv_dat_snoop_outgoing_arp_request(bat_priv,
+                                                                 skb))
+                               goto dropped;
 
+                       batadv_dat_snoop_outgoing_arp_reply(bat_priv, skb);
+
+                       ret = batadv_send_skb_via_tt(bat_priv, skb, dst_hint,
+                                                    vid);
+               }
                if (ret == NET_XMIT_DROP)
                        goto dropped_freed;
        }