Hi.

To confirm, are you trying to implement a (permutation of?) https://www.rfc-editor.org/rfc/rfc3046.html here? If so, the feature should probably be renamed into DHCP Relay Agent support. Please clarify if that's your intent here.

Thanks,

Ihar

On 8/22/23 10:36 PM, naveen.yerramneni wrote:
This patch contains changes to enable DHCP Proxy Agent support for overlay 
subnets.

NOTE:
-----
   - This patch is not complete, sending this to get the initial feedback about 
the approach taken.

USE CASE:
----------
   - Enable IP address assignment for overlay subnets from the centralized DHCP 
server present in the underlay network.


PREREQUISITES
--------------
   - Logical Router Port IP should be assigned (statically) from the same 
overlay subnet which is managed by DHCP server.
   - LRP IP is used for GIADRR field when proxying the DHCP packets and also 
same IP needs to be configured as default gateway for the overlay subnet.
   - Overlay subnets managed by external DHCP server are expected to be 
directly reachable from the underlay network.

EXPECTED PACKET FLOW:
----------------------
Following is the expected packet flow inorder to support DHCP Proxy 
functionality in OVN.
   1. VM originates DHCP discovery (broadcast).
   2. DHCP Proxy (running on the OVN) receives the broadcast and forwards the 
packet to the DHCP server by converting it to unicast. While forwarding the 
packet, it updates the GIADDR in DHCP header to its
      interface IP on which DHCP packet is received.
   3. DHCP server uses GIADDR field to decide the IP address pool from which IP 
has to be assigned and DHCP offer is sent to the same IP (GIADDR).
   4. DHCP proxy resets the GIADDR field when forwarding the offer to the 
client and it also replaces DHCP server ID option (54) with its IP address.
   5. VM/Host sends DHCP request (broadcast) packet.
   6. DHCP Proxy (running on the OVN) receives the broadcast and forwards the 
packet to the DHCP server by converting it to unicast. While forwarding the 
packet, it updates the GIADDR in DHCP header to its
      interface IP on which DHCP packet is received and also replaces the DHCP 
server ID option (54) with DHCP server IP address.
   7. DHCP Server sends the ACK packet.
   8. DHCP Proxy resets the GIADDR field when forwarding the ACK to the client 
and it also replaces DHCP server ID option (54) with its IP address.
   9. By setting DHCP server ID option (54) to its IP address, DHCP Proxy 
completely hides the DHCP server from the end DHCP clients and all the future 
renew/release packets come to DHCP proxy.

OVN DHCP PROXY PACKET FLOW:
----------------------------
To add DHCP Proxy support on OVN, we need to replicate all the behavior 
described above using distributed logical switch and logical router.
At a highlevel, packet flow is distributed among Logical Switch and Logical 
Router on source node (where VM is deployed) and redirect chassis(RC) node.
   1. Request packet gets processed on the source node. Required info is 
extracted from the DHCP payload and stored in the in-memory hash map (key: MAC 
+ DHCP_xid) of the OVN controller.
   2. Response packet is first processed on RC node (which first receives the 
packet from underlay network). RC node forwards the packet to the right node by 
filling in the dest MAC and IP.
   3. Response packet is processed on the source node which originated the DHCP 
request. Hash map lookup is done to find the associative DHCP session and 
stateful checks are performed to validate the response.

OVN Packet flow with DHCP Proxy is explained below.
   1. VM sends the DHCP discover packet (broadcast).
   2. Logical switch converts the packet to L2 unicast by setting the 
destination MAC to LRP's MAC
   3. Logical Router receives the packet and redirects it to the OVN controller.
   4. OVN controller updates the required information in the DHCP payload 
(GIADDR, SERVER ID) after doing the required checks. It stores required 
information from the packet to do some stateful checks on
      response packets and reinjects the packet back to the datapath provided 
if all sanity checks are passed.
   5. Logical Router converts the packet to L3 unicast and forwards it to the 
server. This packet gets routed like any other packet (via RC node).
   6. Server replies with DHCP offer.
   7. RC node processes the DHCP offer and forwards it to the OVN controller.
   8. OVN controller updates the destination MAC (available in DHCP header) and 
destination IP (available in DHCP header) and reinjects the packet to datapath.
   9. Logical router outputs  the packet to the logical switch.
   10.Logical switch processes the packet and redirects it to the OVN 
controller.
   11.OVN controller does the required checks on the packet, updates DHCP 
payload (GIADDR, Server ID)and reinjects the packet back to the datapath if all 
stateful checks on the packet are passed.
   12.Logical switch outputs the packet to VM by updating the required fields 
in IP and UDP header.
   13. Similar steps are performed for further packets.

NEW OVN ACTIONS
---------------

   1. dhcp_relay_req(<proxy-ip>, <server-ip>)
       - This action executes on the source node on which the DHCP request 
originated.
       - This action proxies the DHCP request coming from client to the server 
and stores required info about the DHCP txn in the memory. Proxy-ip is used to 
update GIADDR in the DHCP header.
   2. dhcp_relay_resp_fwd()
       - This action executes on the first node (RC node) which processes the 
DHCP response from the server.
       - This action updates  the destination MAC and destination IP so that 
the response can be forwarded to the appropriate node from which request was 
originated.
   3. dhcp_relay_resp(<proxy-ip>, <server-ip>)
       - This action executes on the node where the VM (to which DHCP packet is 
destined) is running.
       - This action does all the stateful packet checks and updates all the 
required info in the DHCP payload.
       - Proxy-ip, server-ip are used to update/validate GIADDR and SERVER ID 
in the DHCP payload.

FLOWS
-----
Following are the new flows required to support DHCP functionality. Following 
flows enable DHCP Proxy support for one VM port.
For each DHCP subnet, four flows are requried to enable the DHCP proxy.
For each VM port, two flows are requried to enable the DHCP proxy support in 
the given subnet.

   1. table=27(ls_in_l2_lkup      ), priority=100  , match=(inport == <vm_port> && eth.src == <vm_mac> && ip4.src 
== 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67), 
action=(eth.dst=<lrp_mac>;outport=<lrp-port>;next;/* DHCP_PROXY_REQ */)
   2. table=9 (ls_out_dhcp_proxy_resp), priority=100  , match=(inport == <lrp_port> && eth.src == <lrp_mac> && outport == 
<vm_port> &&ip4.src == <dhcp_server_ip> && udp.src == 67 && udp.dst == 67), 
action=(dhcp_proxy_resp(<lrp_ip>,<dhcp_server_ip>);ip4.src=<lrp_ip>;udp.dst=68;next;/* DHCP_PROXY_RESP */)
   3. table=3 (lr_in_ip_input     ), priority=110  , match=(inport == <lrp_port> && ip4.dst == <lrp_ip> && 
udp.src == 68 && udp.dst == 67), 
action=(dhcp_proxy_req(<lrp_ip>,<dhcp_server_ip>);ip4.src=<lrp_ip>;ip4.dst=<dhcp_server_ip>;udp.src=67;next; /* 
DHCP_PROXY_REQ */)
   4. table=3 (lr_in_ip_input     ), priority=110  , match=(inport == <lrp_port> && ip4.src == 0.0.0.0 && ip4.dst == 
255.255.255.255 && udp.src == 68 && udp.dst == 67), 
action=(dhcp_proxy_req(<lrp_ip>,<dhcp_server_ip>);ip4.src=<lrp_ip>;ip4.dst=<dhcp_server_ip>;udp.src=67;next; /* 
DHCP_PROXY_REQ */)
   5. table=3 (lr_in_ip_input     ), priority=110  , match=(ip4.src == <dhcp_server_ip> && ip4.dst 
==<lrp_ip> && udp.src == 67 && udp.dst == 67), action=(next;/* DHCP_PROXY_RESP */)
   6. table=17(lr_in_dhcp_proxy_resp_fwd), priority=110  , match=(ip4.src == <dhcp_server_ip> && ip4.dst == 
<lrp_ip> && udp.src == 67 && udp.dst == 67), 
action=(dhcp_proxy_resp_fwd();outport=<lrp_port>;output; /* DHCP_PROXY_RESP */)

NEW PIPELINE STAGES
-------------------
Following are new stages are added for DHCP proxy feature. Some of the flows 
are fitted into the existing pipeline tages.
   1. lr_in_dhcp_proxy_resp_fwd
       - Forward teh DHCP response to the appropriate node
   2. ls_out_dhcp_proxy_resp
       - Process DHCP response on the source which originated the request.

NB SCHEMA CHANGES
----------------
   1. New DHCP_Proxy table
       "DHCP_Proxy": {
             "columns": {
         "name": {"type": "string"},
                 "servers": {"type": {"key": "string",
                                        "min": 0,
                                        "max": 1}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                             "min": 0, "max": "unlimited"}}},
             "isRoot": true},
   2. New column to Logical_Router_Port table
       "dhcp_proxy": {"type": {"key": {"type": "uuid",
                             "refTable": "DHCP_Proxy",
                             "refType": "weak"},
                             "min": 0,
                             "max": 1}},
   3. New column to Logical_Switch_table
       "dhcp_proxy_port": {"type": {"key": {"type": "uuid",
                                     "refTable": "Logical_Router_Port",
                                     "refType": "weak"},
                                      "min": 0,
                                      "max": 1}}},
Commands to enable the feature.
   - ovn-nbctl create DHCP_Proxy servers=<ip>
   - ovn-nbctl set Logical_Router_port <lrp_uuid> dhcp_proxy=<dhcp_proxy_uuid>
   - ovn-nbctl set Logical_Switch <ls_uuid> dhcp_proxy_port=<lrp_uuid>

POST RFC REVIEW
----------------
   1. Address review comments/suggestions
   2. Address TODOs and pending changes
   3. Unit tests
   4. Complete testing
   5. IP Discovery support in future

Signed-off-by: naveen.yerramneni <naveen.yerramn...@nutanix.com>
Co-authored-by: Huzaifa Calcuttawala <huzaif...@nutanix.com>
Signed-off-by: Huzaifa Calcuttawala <huzaif...@nutanix.com>
CC: Mary Manohar <mary.mano...@nutanix.com>
CC: Abhiram Sangana <sangana.abhi...@nutanix.com>
---
  controller/pinctrl.c  | 766 ++++++++++++++++++++++++++++++++++++++++++
  include/ovn/actions.h |  33 ++
  lib/actions.c         | 143 ++++++++
  lib/ovn-l7.h          |   1 +
  northd/northd.c       | 218 +++++++++++-
  ovn-nb.ovsschema      |  23 +-
  ovn-nb.xml            |  27 ++
  tests/ovn-northd.at   |  32 +-
  tests/ovn.at          |  12 +-
  utilities/ovn-trace.c |  12 +
  10 files changed, 1237 insertions(+), 30 deletions(-)

diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index bed90fe0b..11f5839e3 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -377,6 +377,9 @@ static void wait_put_fdbs(struct ovsdb_idl_txn 
*ovnsb_idl_txn);
  static void pinctrl_handle_put_fdb(const struct flow *md,
                                     const struct flow *headers)
                                     OVS_REQUIRES(pinctrl_mutex);
+static struct dhcp_proxy_info *dhcp_proxy_get(void);
+static void dhcp_proxy_init(struct dhcp_proxy_info *dhcp_proxy);
+
COVERAGE_DEFINE(pinctrl_drop_put_mac_binding);
  COVERAGE_DEFINE(pinctrl_drop_buffered_packets_map);
@@ -547,6 +550,7 @@ pinctrl_init(void)
      init_svc_monitors();
      bfd_monitor_init();
      init_fdb_entries();
+    dhcp_proxy_init(dhcp_proxy_get());
      pinctrl.br_int_name = NULL;
      pinctrl.mac_binding_can_timestamp = false;
      pinctrl_handler_seq = seq_create();
@@ -1883,6 +1887,753 @@ is_dhcp_flags_broadcast(ovs_be16 flags)
      return flags & htons(DHCP_BROADCAST_FLAG);
  }
+
+enum dhcp_proxy_txn_state
+{
+    DHCP_PROXY_TXN_STATE_INIT=0,
+    DHCP_PROXY_TXN_STATE_DISCOVER,
+    DHCP_PROXY_TXN_STATE_OFFER,
+    DHCP_PROXY_TXN_STATE_REQUEST,
+    DHCP_PROXY_TXN_STATE_ASSIGNED,
+    DHCP_PROXY_TXN_STATE_INVALID,
+    DHCP_PROXY_TXN_STATE_MAX=DHCP_PROXY_TXN_STATE_INVALID
+};
+
+struct dhcp_proxy_node
+{
+    struct hmap_node hmap_node;
+    struct eth_addr client_mac;
+    uint64_t epoch_ms;
+    uint64_t lease_time_ms;
+    enum dhcp_proxy_txn_state txn_state;
+    ovs_be32 txn_id;
+    ovs_be32 client_ip;
+    ovs_be32 proxy_ip;
+    ovs_be32 server_ip;
+};
+
+#define DHCP_MSG_TYPE_MAX (OVN_DHCP_MSG_RELEASE+1)
+struct dhcp_proxy_info
+{
+    struct hmap cache_hmap;
+    uint32_t secret;
+    enum dhcp_proxy_txn_state sm[DHCP_PROXY_TXN_STATE_MAX][DHCP_MSG_TYPE_MAX];
+};
+
+static struct dhcp_proxy_info g_dhcp_proxy;
+
+static struct dhcp_proxy_info*
+dhcp_proxy_get(void)
+{
+    return &g_dhcp_proxy;
+}
+
+static void
+dhcp_proxy_init(struct dhcp_proxy_info *dhcp_proxy)
+{
+    hmap_init(&dhcp_proxy->cache_hmap);
+    dhcp_proxy->secret = random_uint32();
+
+    for (int i=DHCP_PROXY_TXN_STATE_INIT; i<DHCP_PROXY_TXN_STATE_MAX; i++)
+    {
+        for (int j=0; j<DHCP_MSG_TYPE_MAX; j++)
+        {
+            dhcp_proxy->sm[i][j] = DHCP_PROXY_TXN_STATE_INVALID;
+        }
+    }
+
+    //TODO: Validate state machine for all possible cases
+    enum dhcp_proxy_txn_state *dhcp_sm_state =
+                                  dhcp_proxy->sm[DHCP_PROXY_TXN_STATE_INIT];
+    dhcp_sm_state[DHCP_MSG_DISCOVER] = DHCP_PROXY_TXN_STATE_DISCOVER;
+    dhcp_sm_state[DHCP_MSG_OFFER] = DHCP_PROXY_TXN_STATE_INIT;
+    dhcp_sm_state[DHCP_MSG_REQUEST] = DHCP_PROXY_TXN_STATE_REQUEST;
+    dhcp_sm_state[OVN_DHCP_MSG_DECLINE] = DHCP_PROXY_TXN_STATE_INIT;
+    dhcp_sm_state[DHCP_MSG_ACK] = DHCP_PROXY_TXN_STATE_INIT;
+    dhcp_sm_state[DHCP_MSG_NAK] = DHCP_PROXY_TXN_STATE_INIT;
+    dhcp_sm_state[OVN_DHCP_MSG_RELEASE] = DHCP_PROXY_TXN_STATE_INIT;
+
+    dhcp_sm_state = dhcp_proxy->sm[DHCP_PROXY_TXN_STATE_DISCOVER];
+    dhcp_sm_state[DHCP_MSG_DISCOVER] = DHCP_PROXY_TXN_STATE_DISCOVER;
+    dhcp_sm_state[DHCP_MSG_OFFER] = DHCP_PROXY_TXN_STATE_OFFER;
+
+    dhcp_sm_state = dhcp_proxy->sm[DHCP_PROXY_TXN_STATE_OFFER];
+    dhcp_sm_state[DHCP_MSG_DISCOVER] = DHCP_PROXY_TXN_STATE_DISCOVER;
+    dhcp_sm_state[DHCP_MSG_REQUEST] = DHCP_PROXY_TXN_STATE_REQUEST;
+    dhcp_sm_state[OVN_DHCP_MSG_DECLINE] = DHCP_PROXY_TXN_STATE_INIT;
+    dhcp_sm_state[DHCP_MSG_OFFER] = DHCP_PROXY_TXN_STATE_OFFER;
+
+    dhcp_sm_state = dhcp_proxy->sm[DHCP_PROXY_TXN_STATE_REQUEST];
+    dhcp_sm_state[DHCP_MSG_DISCOVER] = DHCP_PROXY_TXN_STATE_DISCOVER;
+    dhcp_sm_state[DHCP_MSG_REQUEST] = DHCP_PROXY_TXN_STATE_REQUEST;
+    dhcp_sm_state[DHCP_MSG_OFFER] = DHCP_PROXY_TXN_STATE_OFFER;
+    dhcp_sm_state[DHCP_MSG_ACK] = DHCP_PROXY_TXN_STATE_ASSIGNED;
+    dhcp_sm_state[DHCP_MSG_NAK] = DHCP_PROXY_TXN_STATE_INIT;
+
+    dhcp_sm_state = dhcp_proxy->sm[DHCP_PROXY_TXN_STATE_ASSIGNED];
+    dhcp_sm_state[DHCP_MSG_DISCOVER] = DHCP_PROXY_TXN_STATE_DISCOVER;
+    dhcp_sm_state[DHCP_MSG_REQUEST] = DHCP_PROXY_TXN_STATE_REQUEST;
+    dhcp_sm_state[OVN_DHCP_MSG_RELEASE] = DHCP_PROXY_TXN_STATE_INIT;
+}
+
+static bool
+dhcp_proxy_is_msg_type_supported(uint8_t msg_type)
+{
+    return (msg_type >= DHCP_MSG_DISCOVER && msg_type <= OVN_DHCP_MSG_RELEASE);
+}
+
+static bool
+dhcp_proxy_is_txn_state_valid(enum dhcp_proxy_txn_state state)
+{
+    return (state >= DHCP_PROXY_TXN_STATE_INIT
+            && state < DHCP_PROXY_TXN_STATE_INVALID);
+}
+
+static enum dhcp_proxy_txn_state
+dhcp_proxy_next_txn_state(enum dhcp_proxy_txn_state sm[][DHCP_MSG_TYPE_MAX],
+        enum dhcp_proxy_txn_state curr_state, uint8_t msg_type)
+{
+    if (!(dhcp_proxy_is_msg_type_supported(msg_type)
+          && dhcp_proxy_is_txn_state_valid(curr_state)))
+    {
+        return DHCP_PROXY_TXN_STATE_INVALID;
+    }
+    return sm[curr_state][msg_type];
+}
+
+static uint64_t
+dhcp_proxy_hash(struct eth_addr ea, uint32_t basis)
+{
+    return hash_uint64_basis(eth_addr_to_uint64(ea), basis);
+}
+
+static void
+dhcp_proxy_insert(struct dhcp_proxy_info *dhcp_proxy,
+                  struct dhcp_proxy_node *node, uint64_t hash)
+{
+    hmap_insert(&dhcp_proxy->cache_hmap, &node->hmap_node, hash);
+}
+
+static struct dhcp_proxy_node*
+dhcp_proxy_lookup(struct dhcp_proxy_info *dhcp_proxy, struct eth_addr ea)
+{
+    struct dhcp_proxy_node *node;
+    HMAP_FOR_EACH_WITH_HASH(node, hmap_node,
+            dhcp_proxy_hash(ea, dhcp_proxy->secret), &dhcp_proxy->cache_hmap)
+    {
+        if (eth_addr_equals(node->client_mac, ea))
+        {
+            return node;
+        }
+    }
+    return NULL;
+}
+
+static struct dhcp_proxy_node*
+dhcp_proxy_lookup_match_xid(struct dhcp_proxy_info *dhcp_proxy, struct 
eth_addr ea,
+                  uint32_t dhcp_txn_id)
+{
+    struct dhcp_proxy_node *node = dhcp_proxy_lookup(dhcp_proxy, ea);
+    if (node && (node->txn_id == dhcp_txn_id))
+    {
+        return node;
+    }
+    return NULL;
+}
+
+//TODO: yet to implement when and how nodes are deleted
+static void
+dhcp_proxy_delete_node(struct dhcp_proxy_info *dhcp_proxy OVS_UNUSED,
+                      struct eth_addr ea OVS_UNUSED)
+{
+}
+
+//time_wall_msec();
+
+/* Called with in the pinctrl_handler thread context. */
+static void
+pinctrl_handle_dhcp_proxy_req(
+    struct rconn *swconn,
+    struct dp_packet *pkt_in, struct ofputil_packet_in *pin,
+    struct ofpbuf *userdata,
+    struct ofpbuf *continuation)
+{
+    enum ofp_version version = rconn_get_version(swconn);
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+    struct dp_packet *pkt_out_ptr = NULL;
+
+    /* Parse relay IP and server IP. */
+    ovs_be32 *proxy_ip = ofpbuf_try_pull(userdata, sizeof *proxy_ip);
+    ovs_be32 *server_ip = ofpbuf_try_pull(userdata, sizeof *server_ip);
+    if (!proxy_ip || !server_ip) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: proxy ip or server ip not present in the 
userdata");
+        return;
+    }
+
+    /* Validate the DHCP request packet.
+     * Format of the DHCP packet is
+     * ------------------------------------------------------------------------
+     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var 
len)|
+     * ------------------------------------------------------------------------
+     */
+
+    size_t in_l4_size = dp_packet_l4_size(pkt_in);
+    const char *end = (char *)dp_packet_l4(pkt_in) + in_l4_size;
+    const char *in_dhcp_ptr = dp_packet_get_udp_payload(pkt_in);
+    if (!in_dhcp_ptr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: invalid or incomplete DHCP packet 
received");
+        return;
+    }
+
+    const struct dhcp_header *in_dhcp_data
+        = (const struct dhcp_header *) in_dhcp_ptr;
+    in_dhcp_ptr += sizeof *in_dhcp_data;
+    if (in_dhcp_ptr > end) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: invalid or incomplete DHCP packet 
received, "
+                     "bad data length");
+        return;
+    }
+    if (in_dhcp_data->op != DHCP_OP_REQUEST) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: invalid opcode in the DHCP packet: 
%d",
+                     in_dhcp_data->op);
+        return;
+    }
+
+    /* DHCP options follow the DHCP header. The first 4 bytes of the DHCP
+     * options is the DHCP magic cookie followed by the actual DHCP options.
+     */
+    ovs_be32 magic_cookie = htonl(DHCP_MAGIC_COOKIE);
+    if (in_dhcp_ptr + sizeof magic_cookie > end ||
+        get_unaligned_be32((const void *) in_dhcp_ptr) != magic_cookie) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: magic cookie not present in the 
packet");
+        return;
+    }
+
+    if (in_dhcp_data->giaddr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: giaddr is already set");
+        return;
+    }
+
+    if (in_dhcp_data->htype != 0x1) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: packet is recieved with unsupported 
hardware type");
+        return;
+    }
+
+    ovs_be32 *server_id_ptr = NULL;
+    const uint8_t *in_dhcp_msg_type = NULL;
+
+    in_dhcp_ptr += sizeof magic_cookie;
+    while (in_dhcp_ptr < end) {
+        const struct dhcp_opt_header *in_dhcp_opt =
+            (const struct dhcp_opt_header *)in_dhcp_ptr;
+        if (in_dhcp_opt->code == DHCP_OPT_END) {
+            break;
+        }
+        if (in_dhcp_opt->code == DHCP_OPT_PAD) {
+            in_dhcp_ptr += 1;
+            continue;
+        }
+        in_dhcp_ptr += sizeof *in_dhcp_opt;
+        if (in_dhcp_ptr > end) {
+            break;
+        }
+        in_dhcp_ptr += in_dhcp_opt->len;
+        if (in_dhcp_ptr > end) {
+            break;
+        }
+
+        switch (in_dhcp_opt->code) {
+        case DHCP_OPT_MSG_TYPE:
+            if (in_dhcp_opt->len == 1) {
+                in_dhcp_msg_type = DHCP_OPT_PAYLOAD(in_dhcp_opt);
+            }
+            break;
+        case OVN_DHCP_OPT_CODE_SERVER_ID: //Server Identifier
+            if (in_dhcp_opt->len == 4) {
+                server_id_ptr = DHCP_OPT_PAYLOAD(in_dhcp_opt);
+            }
+            break;
+        default:
+            break;
+        }
+    }
+
+    /* Check whether the DHCP Message Type (opt 53) is present or not */
+    if (!in_dhcp_msg_type) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: missing message type");
+        return;
+    }
+
+    struct eth_addr ea;
+    struct dhcp_proxy_node *node;
+    struct dhcp_proxy_info *dhcp_proxy = dhcp_proxy_get();
+    memcpy(ea.ea, in_dhcp_data->chaddr, sizeof(ea));
+
+    if((node = dhcp_proxy_lookup_match_xid(dhcp_proxy, ea, in_dhcp_data->xid))
+                != NULL) {
+        enum dhcp_proxy_txn_state nxt_state = dhcp_proxy_next_txn_state(
+                                              dhcp_proxy->sm,
+                                              node->txn_state,
+                                              *in_dhcp_msg_type);
+        if (nxt_state < DHCP_PROXY_TXN_STATE_INVALID) {
+            if (server_id_ptr && *server_id_ptr != *proxy_ip) {
+                static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+                VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: server identifier 
mismatch");
+                return;
+            }
+            node->txn_state = nxt_state;
+            node->epoch_ms = time_wall_msec();
+        }
+        else {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: invalid state transistion"
+                              " for existing session");
+            return;
+        }
+    }
+    else {
+        enum dhcp_proxy_txn_state nxt_state = dhcp_proxy_next_txn_state(
+                                              dhcp_proxy->sm,
+                                              DHCP_PROXY_TXN_STATE_INIT,
+                                              *in_dhcp_msg_type);
+        if (nxt_state < DHCP_PROXY_TXN_STATE_INVALID) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: New session");
+            node = xmalloc(sizeof(*node));
+            node->client_mac = ea;
+            node->client_ip = in_dhcp_data->ciaddr;
+            node->txn_id = in_dhcp_data->xid;
+            node->txn_state = nxt_state;
+            node->epoch_ms = time_wall_msec();
+            node->server_ip = *server_ip;
+            node->proxy_ip = *proxy_ip;
+            dhcp_proxy_insert(dhcp_proxy, node,
+                    dhcp_proxy_hash(ea, dhcp_proxy->secret));
+        }
+        else {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_WARN_RL(&rl, "DHCP_PROXY_REQ: invalid state transistion"
+                              " for new session");
+            return;
+        }
+    }
+
+    /* Proxy the DHCP request packet */
+
+    /* Replace Server Identifier Option */
+    if (server_id_ptr) {
+        put_unaligned_be32(server_id_ptr, *server_ip);
+    }
+
+    uint16_t new_l4_size = in_l4_size;
+    size_t new_packet_size = pkt_in->l4_ofs + new_l4_size;
+
+    struct dp_packet pkt_out;
+    dp_packet_init(&pkt_out, new_packet_size);
+    dp_packet_clear(&pkt_out);
+    dp_packet_prealloc_tailroom(&pkt_out, new_packet_size);
+    pkt_out_ptr = &pkt_out;
+
+    /* Copy the L2 and L3 headers from the pkt_in as they would remain same*/
+    dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, pkt_in->l4_ofs), pkt_in->l4_ofs);
+
+    pkt_out.l2_5_ofs = pkt_in->l2_5_ofs;
+    pkt_out.l2_pad_size = pkt_in->l2_pad_size;
+    pkt_out.l3_ofs = pkt_in->l3_ofs;
+    pkt_out.l4_ofs = pkt_in->l4_ofs;
+
+    struct ip_header *out_ip = dp_packet_l3(&pkt_out);
+
+    struct udp_header *udp = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, UDP_HEADER_LEN), UDP_HEADER_LEN);
+
+    struct dhcp_header *dhcp_data = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, new_l4_size-UDP_HEADER_LEN), 
new_l4_size-UDP_HEADER_LEN);
+    dhcp_data->giaddr = *proxy_ip;
+    //TODO: incremental checkcum
+    if (udp->udp_csum) {
+        udp->udp_csum = 0;
+        uint32_t p_csum = packet_csum_pseudoheader(out_ip);
+        udp->udp_csum = csum_finish(csum_continue(p_csum, udp, new_l4_size));
+    }
+    pin->packet = dp_packet_data(&pkt_out);
+    pin->packet_len = dp_packet_size(&pkt_out);
+
+    queue_msg(swconn, ofputil_encode_resume(pin, continuation, proto));
+    if (pkt_out_ptr) {
+        dp_packet_uninit(pkt_out_ptr);
+    }
+}
+
+
+/* Called with in the pinctrl_handler thread context. */
+static void
+pinctrl_handle_dhcp_proxy_resp(
+    struct rconn *swconn,
+    struct dp_packet *pkt_in, struct ofputil_packet_in *pin,
+    struct ofpbuf *userdata,
+    struct ofpbuf *continuation)
+{
+    enum ofp_version version = rconn_get_version(swconn);
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+    struct dp_packet *pkt_out_ptr = NULL;
+
+    /* Parse relay IP and server IP. */
+    ovs_be32 *proxy_ip = ofpbuf_try_pull(userdata, sizeof *proxy_ip);
+    ovs_be32 *server_ip = ofpbuf_try_pull(userdata, sizeof *server_ip);
+    if (!proxy_ip || !server_ip) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: proxy ip or server ip not present in 
the userdata");
+        return;
+    }
+
+    /* Validate the DHCP request packet.
+     * Format of the DHCP packet is
+     * ------------------------------------------------------------------------
+     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var 
len)|
+     * ------------------------------------------------------------------------
+     */
+
+    size_t in_l4_size = dp_packet_l4_size(pkt_in);
+    const char *end = (char *)dp_packet_l4(pkt_in) + in_l4_size;
+    const char *in_dhcp_ptr = dp_packet_get_udp_payload(pkt_in);
+    if (!in_dhcp_ptr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: invalid or incomplete packet 
received");
+        return;
+    }
+
+    const struct dhcp_header *in_dhcp_data
+        = (const struct dhcp_header *) in_dhcp_ptr;
+    in_dhcp_ptr += sizeof *in_dhcp_data;
+    if (in_dhcp_ptr > end) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: invalid or incomplete packet received, 
"
+                     "bad data length");
+        return;
+    }
+    if (in_dhcp_data->op != DHCP_OP_REPLY) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP:: invalid opcode in the packet: %d",
+                     in_dhcp_data->op);
+        return;
+    }
+
+    /* DHCP options follow the DHCP header. The first 4 bytes of the DHCP
+     * options is the DHCP magic cookie followed by the actual DHCP options.
+     */
+    ovs_be32 magic_cookie = htonl(DHCP_MAGIC_COOKIE);
+    if (in_dhcp_ptr + sizeof magic_cookie > end ||
+        get_unaligned_be32((const void *) in_dhcp_ptr) != magic_cookie) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP:: magic cookie not present in the 
packet");
+        return;
+    }
+
+    if (!in_dhcp_data->giaddr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: giaddr is not set in response");
+        return;
+    }
+
+    ovs_be32 *server_id_ptr = NULL;
+    ovs_be32 lease_time = 0;
+    ovs_be32 giaddr = in_dhcp_data->giaddr;
+    const uint8_t *in_dhcp_msg_type = NULL;
+
+    in_dhcp_ptr += sizeof magic_cookie;
+    while (in_dhcp_ptr < end) {
+        const struct dhcp_opt_header *in_dhcp_opt =
+            (const struct dhcp_opt_header *)in_dhcp_ptr;
+        if (in_dhcp_opt->code == DHCP_OPT_END) {
+            break;
+        }
+        if (in_dhcp_opt->code == DHCP_OPT_PAD) {
+            in_dhcp_ptr += 1;
+            continue;
+        }
+        in_dhcp_ptr += sizeof *in_dhcp_opt;
+        if (in_dhcp_ptr > end) {
+            break;
+        }
+        in_dhcp_ptr += in_dhcp_opt->len;
+        if (in_dhcp_ptr > end) {
+            break;
+        }
+
+        switch (in_dhcp_opt->code) {
+        case DHCP_OPT_MSG_TYPE:
+            if (in_dhcp_opt->len == 1) {
+                in_dhcp_msg_type = DHCP_OPT_PAYLOAD(in_dhcp_opt);
+            }
+            break;
+        case OVN_DHCP_OPT_CODE_SERVER_ID: //Server Identifier
+            if (in_dhcp_opt->len == 4) {
+                server_id_ptr = DHCP_OPT_PAYLOAD(in_dhcp_opt);
+            }
+            break;
+        case OVN_DHCP_OPT_CODE_LEASE_TIME:
+            if (in_dhcp_opt->len == 4) {
+                lease_time = get_unaligned_be32(DHCP_OPT_PAYLOAD(in_dhcp_opt));
+            }
+            break;
+        default:
+            break;
+        }
+    }
+
+    /* Check whether the DHCP Message Type (opt 53) is present or not */
+    if (!in_dhcp_msg_type) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: missing message type");
+        return;
+    }
+
+    if (!server_id_ptr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: missing server identifier");
+        return;
+    }
+
+    if (*server_id_ptr != *server_ip) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: server identifier mismatch");
+        return;
+    }
+
+    if (giaddr != *proxy_ip) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: giaddr mismatch");
+        return;
+    }
+
+    struct eth_addr ea;
+    struct dhcp_proxy_node *node;
+    struct dhcp_proxy_info *dhcp_proxy = dhcp_proxy_get();
+    memcpy(ea.ea, in_dhcp_data->chaddr, sizeof(ea));
+
+    if((node = dhcp_proxy_lookup_match_xid(dhcp_proxy, ea, in_dhcp_data->xid))
+                != NULL) {
+        enum dhcp_proxy_txn_state nxt_state = dhcp_proxy_next_txn_state(
+                                              dhcp_proxy->sm,
+                                              node->txn_state,
+                                              *in_dhcp_msg_type);
+        if (nxt_state < DHCP_PROXY_TXN_STATE_INVALID) {
+            //TODO: Check IP assigned by server is falling in giaddr subnet
+            if (nxt_state == DHCP_PROXY_TXN_STATE_ASSIGNED) {
+                node->client_ip = in_dhcp_data->yiaddr;
+            }
+            node->txn_state = nxt_state;
+            node->epoch_ms = time_wall_msec();
+            node->lease_time_ms = node->epoch_ms + ntohl(lease_time)*60*1000;
+        }
+        else {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: invalid state transistion"
+                              " for existing session");
+            return;
+        }
+    }
+    else {
+          static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+          VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP: transaction not found");
+          return;
+    }
+
+    /* Proxy the DHCP response packet */
+
+    /* Replace Server Identifier Option */
+    if (server_id_ptr) {
+        put_unaligned_be32(server_id_ptr, *proxy_ip);
+    }
+
+    uint16_t new_l4_size = in_l4_size;
+    size_t new_packet_size = pkt_in->l4_ofs + new_l4_size;
+
+    struct dp_packet pkt_out;
+    dp_packet_init(&pkt_out, new_packet_size);
+    dp_packet_clear(&pkt_out);
+    dp_packet_prealloc_tailroom(&pkt_out, new_packet_size);
+    pkt_out_ptr = &pkt_out;
+
+    /* Copy the L2 and L3 headers from the pkt_in as they would remain same*/
+    struct eth_header *eth = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, pkt_in->l4_ofs), pkt_in->l4_ofs);
+
+    pkt_out.l2_5_ofs = pkt_in->l2_5_ofs;
+    pkt_out.l2_pad_size = pkt_in->l2_pad_size;
+    pkt_out.l3_ofs = pkt_in->l3_ofs;
+    pkt_out.l4_ofs = pkt_in->l4_ofs;
+
+    struct udp_header *udp = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, UDP_HEADER_LEN), UDP_HEADER_LEN);
+
+    struct dhcp_header *dhcp_data = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, new_l4_size-UDP_HEADER_LEN), 
new_l4_size-UDP_HEADER_LEN);
+    dhcp_data->giaddr = htonl(0x0);
+    memcpy(&eth->eth_dst, dhcp_data->chaddr, sizeof(eth->eth_dst));
+
+
+    /* Send a broadcast IP frame when BROADCAST flag is set. */
+    struct ip_header *out_ip = dp_packet_l3(&pkt_out);
+
+    ovs_be32 ip_dst_orig, ip_dst;
+    if (!is_dhcp_flags_broadcast(dhcp_data->flags)) {
+        ip_dst = dhcp_data->yiaddr;
+    } else {
+        ip_dst = htonl(0xffffffff);
+    }
+    ip_dst_orig = get_16aligned_be32(&out_ip->ip_dst);
+    if (ip_dst_orig != ip_dst)
+    {
+        put_16aligned_be32(&out_ip->ip_dst, ip_dst);
+        out_ip->ip_csum = recalc_csum32(out_ip->ip_csum,
+            ip_dst_orig, ip_dst);
+        if (udp->udp_csum) {
+            udp->udp_csum = recalc_csum32(udp->udp_csum,
+                ip_dst_orig, ip_dst);
+        }
+    }
+    //TODO: incremental checkcum
+    if (udp->udp_csum) {
+        udp->udp_csum = 0;
+        uint32_t p_csum = packet_csum_pseudoheader(out_ip);
+        udp->udp_csum = csum_finish(csum_continue(p_csum, udp, new_l4_size));
+    }
+
+    pin->packet = dp_packet_data(&pkt_out);
+    pin->packet_len = dp_packet_size(&pkt_out);
+
+    queue_msg(swconn, ofputil_encode_resume(pin, continuation, proto));
+    if (pkt_out_ptr) {
+        dp_packet_uninit(pkt_out_ptr);
+    }
+}
+
+/* Called with in the pinctrl_handler thread context. */
+static void
+pinctrl_handle_dhcp_proxy_resp_fwd(
+    struct rconn *swconn,
+    struct dp_packet *pkt_in, struct ofputil_packet_in *pin,
+    struct ofpbuf *continuation)
+{
+    enum ofp_version version = rconn_get_version(swconn);
+    enum ofputil_protocol proto = ofputil_protocol_from_ofp_version(version);
+    struct dp_packet *pkt_out_ptr = NULL;
+
+    /* Validate the DHCP request packet.
+     * Format of the DHCP packet is
+     * ------------------------------------------------------------------------
+     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var 
len)|
+     * ------------------------------------------------------------------------
+     */
+
+    size_t in_l4_size = dp_packet_l4_size(pkt_in);
+    const char *end = (char *)dp_packet_l4(pkt_in) + in_l4_size;
+    const char *in_dhcp_ptr = dp_packet_get_udp_payload(pkt_in);
+    if (!in_dhcp_ptr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP_FWD: invalid or incomplete packet 
received");
+        return;
+    }
+
+    const struct dhcp_header *in_dhcp_data
+        = (const struct dhcp_header *) in_dhcp_ptr;
+    in_dhcp_ptr += sizeof *in_dhcp_data;
+    if (in_dhcp_ptr > end) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP_FWD: invalid or incomplete packet 
received, "
+                     "bad data length");
+        return;
+    }
+    if (in_dhcp_data->op != DHCP_OP_REPLY) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP_FWD: invalid opcode in the packet: 
%d",
+                     in_dhcp_data->op);
+        return;
+    }
+
+    /* DHCP options follow the DHCP header. The first 4 bytes of the DHCP
+     * options is the DHCP magic cookie followed by the actual DHCP options.
+     */
+    ovs_be32 magic_cookie = htonl(DHCP_MAGIC_COOKIE);
+    if (in_dhcp_ptr + sizeof magic_cookie > end ||
+        get_unaligned_be32((const void *) in_dhcp_ptr) != magic_cookie) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP_FWD: magic cookie not present in the 
packet");
+        return;
+    }
+
+    if (!in_dhcp_data->giaddr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_PROXY_RESP_FWD: giaddr is not set in request");
+        return;
+    }
+
+    /* Update destination MAC & IP so that the packet is forward to the
+     * right destination node.
+     */
+    uint16_t new_l4_size = in_l4_size;
+    size_t new_packet_size = pkt_in->l4_ofs + new_l4_size;
+
+    struct dp_packet pkt_out;
+    dp_packet_init(&pkt_out, new_packet_size);
+    dp_packet_clear(&pkt_out);
+    dp_packet_prealloc_tailroom(&pkt_out, new_packet_size);
+    pkt_out_ptr = &pkt_out;
+
+    /* Copy the L2 and L3 headers from the pkt_in as they would remain same*/
+    struct eth_header *eth = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, pkt_in->l4_ofs), pkt_in->l4_ofs);
+
+    pkt_out.l2_5_ofs = pkt_in->l2_5_ofs;
+    pkt_out.l2_pad_size = pkt_in->l2_pad_size;
+    pkt_out.l3_ofs = pkt_in->l3_ofs;
+    pkt_out.l4_ofs = pkt_in->l4_ofs;
+
+    struct udp_header *udp = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, UDP_HEADER_LEN), UDP_HEADER_LEN);
+
+    struct dhcp_header *dhcp_data = dp_packet_put(
+        &pkt_out, dp_packet_pull(pkt_in, new_l4_size-UDP_HEADER_LEN), 
new_l4_size-UDP_HEADER_LEN);
+    memcpy(&eth->eth_dst, dhcp_data->chaddr, sizeof(eth->eth_dst));
+
+
+    /* Send a broadcast IP frame when BROADCAST flag is set. */
+    struct ip_header *out_ip = dp_packet_l3(&pkt_out);
+    ovs_be32 ip_dst;
+    ovs_be32 ip_dst_orig = get_16aligned_be32(&out_ip->ip_dst);
+    if (!is_dhcp_flags_broadcast(dhcp_data->flags)) {
+        ip_dst = dhcp_data->yiaddr;
+    } else {
+        ip_dst = htonl(0xffffffff);
+    }
+    put_16aligned_be32(&out_ip->ip_dst, ip_dst);
+    out_ip->ip_csum = recalc_csum32(out_ip->ip_csum,
+              ip_dst_orig, ip_dst);
+    if (udp->udp_csum)
+    {
+        udp->udp_csum = recalc_csum32(udp->udp_csum,
+            ip_dst_orig, ip_dst);
+    }
+    pin->packet = dp_packet_data(&pkt_out);
+    pin->packet_len = dp_packet_size(&pkt_out);
+
+    queue_msg(swconn, ofputil_encode_resume(pin, continuation, proto));
+    if (pkt_out_ptr) {
+        dp_packet_uninit(pkt_out_ptr);
+    }
+}
+
  /* Called with in the pinctrl_handler thread context. */
  static void
  pinctrl_handle_put_dhcp_opts(
@@ -3105,6 +3856,21 @@ process_packet_in(struct rconn *swconn, const struct 
ofp_header *msg)
          ovs_mutex_unlock(&pinctrl_mutex);
          break;
+ case ACTION_OPCODE_DHCP_PROXY_REQ:
+        pinctrl_handle_dhcp_proxy_req(swconn, &packet, &pin,
+                                     &userdata, &continuation);
+        break;
+
+    case ACTION_OPCODE_DHCP_PROXY_RESP:
+        pinctrl_handle_dhcp_proxy_resp(swconn, &packet, &pin,
+                                     &userdata, &continuation);
+        break;
+
+    case ACTION_OPCODE_DHCP_PROXY_RESP_FWD:
+        pinctrl_handle_dhcp_proxy_resp_fwd(swconn, &packet, &pin,
+                                     &continuation);
+        break;
+
      case ACTION_OPCODE_PUT_DHCP_OPTS:
          pinctrl_handle_put_dhcp_opts(swconn, &packet, &pin, &headers,
                                       &userdata, &continuation);
diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index 23a919049..97b074b3e 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -95,6 +95,9 @@ struct collector_set_ids;
      OVNACT(LOOKUP_ND_IP,      ovnact_lookup_mac_bind_ip) \
      OVNACT(PUT_DHCPV4_OPTS,   ovnact_put_opts)        \
      OVNACT(PUT_DHCPV6_OPTS,   ovnact_put_opts)        \
+    OVNACT(DHCPV4_PROXY_REQ,  ovnact_dhcp_proxy)      \
+    OVNACT(DHCPV4_PROXY_RESP, ovnact_dhcp_proxy)      \
+    OVNACT(DHCPV4_PROXY_RESP_FWD, ovnact_dhcp_proxy)      \
      OVNACT(SET_QUEUE,         ovnact_set_queue)       \
      OVNACT(DNS_LOOKUP,        ovnact_result)          \
      OVNACT(LOG,               ovnact_log)             \
@@ -386,6 +389,14 @@ struct ovnact_put_opts {
      size_t n_options;
  };
+/* OVNACT_DHCP_PROXY. */
+struct ovnact_dhcp_proxy {
+    struct ovnact ovnact;
+    int family;
+    ovs_be32 proxy_ipv4;
+    ovs_be32 server_ipv4;
+};
+
  /* Valid arguments to SET_QUEUE action.
   *
   * QDISC_MIN_QUEUE_ID is the default queue, so user-defined queues should
@@ -746,6 +757,28 @@ enum action_opcode {
/* activation_strategy_rarp() */
      ACTION_OPCODE_ACTIVATION_STRATEGY_RARP,
+
+    /* "result = dhcp_proxy_req(proxy_ip, server_ip)".
+     *
+     * Arguments follow the action_header, in this format:
+     *   - The 32-bit DHCP proxy IP.
+     *   - The 32-bit DHCP server IP.
+     */
+    ACTION_OPCODE_DHCP_PROXY_REQ,
+
+    /* "result = dhcp_proxy_resp(proxy_ip, server_ip)".
+     *
+     * Arguments follow the action_header, in this format:
+     *   - The 32-bit DHCP proxy IP.
+     */
+    ACTION_OPCODE_DHCP_PROXY_RESP,
+
+    /* "result = dhcp_proxy_resp_fwd()".
+     *
+     * Arguments follow the action_header, in this format:
+     *   - The 32-bit DHCP proxy IP.
+     */
+    ACTION_OPCODE_DHCP_PROXY_RESP_FWD,
  };
/* Header. */
diff --git a/lib/actions.c b/lib/actions.c
index f89ccc6bf..402d6cbde 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -2629,6 +2629,143 @@ ovnact_controller_event_free(struct 
ovnact_controller_event *event)
      free_gen_options(event->options, event->n_options);
  }
+static void
+format_DHCPV4_PROXY_REQ(const struct ovnact_dhcp_proxy *dhcp_proxy, struct ds 
*s)
+{
+    ds_put_format(s, "dhcp_proxy_req("IP_FMT","IP_FMT");",
+                  IP_ARGS(dhcp_proxy->proxy_ipv4),
+                  IP_ARGS(dhcp_proxy->server_ipv4));
+}
+
+static void
+parse_dhcp_proxy_req(struct action_context *ctx,
+               struct ovnact_dhcp_proxy *dhcp_proxy)
+{
+    //lexer_get(ctx->lexer); /* Skip dhcp_proxy_req. */
+    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+
+    /* Parse proxy ip and server ip. */
+    if (ctx->lexer->token.format == LEX_F_IPV4) {
+        dhcp_proxy->family = AF_INET;
+        dhcp_proxy->proxy_ipv4 = ctx->lexer->token.value.ipv4;
+        lexer_get(ctx->lexer);
+        lexer_match(ctx->lexer, LEX_T_COMMA);
+        if (ctx->lexer->token.format == LEX_F_IPV4) {
+            dhcp_proxy->family = AF_INET;
+            dhcp_proxy->server_ipv4 = ctx->lexer->token.value.ipv4;
+            lexer_get(ctx->lexer);
+        }
+        else
+        {
+            lexer_syntax_error(ctx->lexer, "expecting IPv4 dhcp server ip");
+            return;
+        }
+    }
+    else
+    {
+          lexer_syntax_error(ctx->lexer, "expecting IPv4 dhcp proxy and server 
ips");
+          return;
+    }
+    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+}
+
+static void
+encode_DHCPV4_PROXY_REQ(const struct ovnact_dhcp_proxy *dhcp_proxy,
+                    const struct ovnact_encode_params *ep,
+                    struct ofpbuf *ofpacts)
+{
+    size_t oc_offset = encode_start_controller_op(ACTION_OPCODE_DHCP_PROXY_REQ,
+                                                  true, ep->ctrl_meter_id,
+                                                  ofpacts);
+    ofpbuf_put(ofpacts, &dhcp_proxy->proxy_ipv4, 
sizeof(dhcp_proxy->proxy_ipv4));
+    ofpbuf_put(ofpacts, &dhcp_proxy->server_ipv4, 
sizeof(dhcp_proxy->server_ipv4));
+    encode_finish_controller_op(oc_offset, ofpacts);
+}
+
+static void
+format_DHCPV4_PROXY_RESP(const struct ovnact_dhcp_proxy *dhcp_proxy, struct ds 
*s)
+{
+    ds_put_format(s, "dhcp_proxy_resp("IP_FMT");",
+                  IP_ARGS(dhcp_proxy->proxy_ipv4));
+}
+
+static void
+parse_dhcp_proxy_resp(struct action_context *ctx,
+               struct ovnact_dhcp_proxy *dhcp_proxy)
+{
+    //lexer_get(ctx->lexer); /* Skip dhcp_proxy_resp. */
+    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+
+    /* Parse proxy ip and server ip. */
+    if (ctx->lexer->token.format == LEX_F_IPV4) {
+        dhcp_proxy->family = AF_INET;
+        dhcp_proxy->proxy_ipv4 = ctx->lexer->token.value.ipv4;
+        lexer_get(ctx->lexer);
+        lexer_match(ctx->lexer, LEX_T_COMMA);
+        if (ctx->lexer->token.format == LEX_F_IPV4) {
+            dhcp_proxy->family = AF_INET;
+            dhcp_proxy->server_ipv4 = ctx->lexer->token.value.ipv4;
+            lexer_get(ctx->lexer);
+        }
+        else
+        {
+            lexer_syntax_error(ctx->lexer, "expecting IPv4 dhcp server ip");
+            return;
+        }
+    }
+    else
+    {
+          lexer_syntax_error(ctx->lexer, "expecting IPv4 dhcp proxy and server 
ips");
+          return;
+    }
+    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+}
+
+static void
+encode_DHCPV4_PROXY_RESP(const struct ovnact_dhcp_proxy *dhcp_proxy,
+                    const struct ovnact_encode_params *ep,
+                    struct ofpbuf *ofpacts)
+{
+    size_t oc_offset = 
encode_start_controller_op(ACTION_OPCODE_DHCP_PROXY_RESP,
+                                                  true, ep->ctrl_meter_id,
+                                                  ofpacts);
+    ofpbuf_put(ofpacts, &dhcp_proxy->proxy_ipv4, 
sizeof(dhcp_proxy->proxy_ipv4));
+    ofpbuf_put(ofpacts, &dhcp_proxy->server_ipv4, 
sizeof(dhcp_proxy->server_ipv4));
+    encode_finish_controller_op(oc_offset, ofpacts);
+}
+
+static void
+format_DHCPV4_PROXY_RESP_FWD(const struct ovnact_dhcp_proxy *dhcp_proxy, 
struct ds *s)
+{
+    ds_put_format(s, "dhcp_proxy_resp_fwd("IP_FMT");",
+                  IP_ARGS(dhcp_proxy->proxy_ipv4));
+}
+
+static void
+parse_dhcp_proxy_resp_fwd(struct action_context *ctx,
+               struct ovnact_dhcp_proxy *dhcp_proxy OVS_UNUSED)
+{
+    //lexer_get(ctx->lexer); /* Skip dhcp_proxy_resp_fwd. */
+    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+}
+
+static void
+encode_DHCPV4_PROXY_RESP_FWD(
+                    const struct ovnact_dhcp_proxy *dhcp_proxy OVS_UNUSED,
+                    const struct ovnact_encode_params *ep,
+                    struct ofpbuf *ofpacts)
+{
+    size_t oc_offset = 
encode_start_controller_op(ACTION_OPCODE_DHCP_PROXY_RESP_FWD,
+                                                  true, ep->ctrl_meter_id,
+                                                  ofpacts);
+    encode_finish_controller_op(oc_offset, ofpacts);
+}
+
+static void ovnact_dhcp_proxy_free(struct ovnact_dhcp_proxy *dhcp_proxy 
OVS_UNUSED)
+{
+}
+
  static void
  parse_put_opts(struct action_context *ctx, const struct expr_field *dst,
                 struct ovnact_put_opts *po, const struct hmap *gen_opts,
@@ -5435,6 +5572,12 @@ parse_action(struct action_context *ctx)
          parse_commit_lb_aff(ctx, ovnact_put_COMMIT_LB_AFF(ctx->ovnacts));
      } else if (lexer_match_id(ctx->lexer, "sample")) {
          parse_sample(ctx);
+    } else if (lexer_match_id(ctx->lexer, "dhcp_proxy_req")) {
+        parse_dhcp_proxy_req(ctx, ovnact_put_DHCPV4_PROXY_REQ(ctx->ovnacts));
+    } else if (lexer_match_id(ctx->lexer, "dhcp_proxy_resp")) {
+        parse_dhcp_proxy_resp(ctx, ovnact_put_DHCPV4_PROXY_RESP(ctx->ovnacts));
+    } else if (lexer_match_id(ctx->lexer, "dhcp_proxy_resp_fwd")) {
+        parse_dhcp_proxy_resp_fwd(ctx, 
ovnact_put_DHCPV4_PROXY_RESP_FWD(ctx->ovnacts));
      } else {
          lexer_syntax_error(ctx->lexer, "expecting action");
      }
diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h
index 9dc331421..94c4955ff 100644
--- a/lib/ovn-l7.h
+++ b/lib/ovn-l7.h
@@ -69,6 +69,7 @@ struct gen_opts_map {
   */
  #define OVN_DHCP_OPT_CODE_NETMASK      1
  #define OVN_DHCP_OPT_CODE_LEASE_TIME   51
+#define OVN_DHCP_OPT_CODE_SERVER_ID    54
  #define OVN_DHCP_OPT_CODE_T1           58
  #define OVN_DHCP_OPT_CODE_T2           59
diff --git a/northd/northd.c b/northd/northd.c
index 9a12a94ae..7e2fa1d22 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -157,8 +157,9 @@ enum ovn_stage {
      PIPELINE_STAGE(SWITCH, OUT, QOS_MARK,     6, "ls_out_qos_mark")       \
      PIPELINE_STAGE(SWITCH, OUT, QOS_METER,    7, "ls_out_qos_meter")      \
      PIPELINE_STAGE(SWITCH, OUT, STATEFUL,     8, "ls_out_stateful")       \
-    PIPELINE_STAGE(SWITCH, OUT, CHECK_PORT_SEC,  9, "ls_out_check_port_sec") \
-    PIPELINE_STAGE(SWITCH, OUT, APPLY_PORT_SEC, 10, "ls_out_apply_port_sec") \
+    PIPELINE_STAGE(SWITCH, OUT, DHCP_PROXY_RESP, 9, "ls_out_dhcp_proxy_resp")  
     \
+    PIPELINE_STAGE(SWITCH, OUT, CHECK_PORT_SEC,  10, "ls_out_check_port_sec") \
+    PIPELINE_STAGE(SWITCH, OUT, APPLY_PORT_SEC, 11, "ls_out_apply_port_sec") \
                                                                        \
      /* Logical router ingress stages. */                              \
      PIPELINE_STAGE(ROUTER, IN,  ADMISSION,       0, "lr_in_admission")    \
@@ -178,11 +179,12 @@ enum ovn_stage {
      PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 14, "lr_in_ip_routing_ecmp") 
\
      PIPELINE_STAGE(ROUTER, IN,  POLICY,          15, "lr_in_policy")          
\
      PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     16, "lr_in_policy_ecmp")     
\
-    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     17, "lr_in_arp_resolve")     \
-    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     18, "lr_in_chk_pkt_len")     \
-    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     19, "lr_in_larger_pkts")     \
-    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     20, "lr_in_gw_redirect")     \
-    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     21, "lr_in_arp_request")     \
+    PIPELINE_STAGE(ROUTER, IN,  DHCP_PROXY_RESP_FWD, 17, 
"lr_in_dhcp_proxy_resp_fwd") \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     18, "lr_in_arp_resolve")     \
+    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     19, "lr_in_chk_pkt_len")     \
+    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     20, "lr_in_larger_pkts")     \
+    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     21, "lr_in_gw_redirect")     \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     22, "lr_in_arp_request")     \
                                                                        \
      /* Logical router egress stages. */                               \
      PIPELINE_STAGE(ROUTER, OUT, CHECK_DNAT_LOCAL,   0,                       \
@@ -9110,6 +9112,103 @@ build_dhcpv6_options_flows(struct ovn_port *op,
      ds_destroy(&match);
  }
+static void
+build_lswitch_dhcp_proxy_flows(struct ovn_port *op,
+                           const struct hmap *lr_ports,
+                           const struct hmap *lflows,
+                           const struct shash *meter_groups OVS_UNUSED)
+{
+    if (op->nbrp || !op->nbsp) {
+        return;
+    }
+    //consider only ports attached to VMs
+    if (strcmp(op->nbsp->type, "")) {
+        return;
+    }
+
+    if (!op->od || !op->od->n_router_ports ||
+        !op->od->nbs || !op->od->nbs->dhcp_proxy_port) {
+        return;
+    }
+
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds action = DS_EMPTY_INITIALIZER;
+    struct nbrec_logical_router_port *lrp = op->od->nbs->dhcp_proxy_port;
+    struct ovn_port *rp = ovn_port_find(lr_ports, lrp->name);
+
+    if (!rp || !rp->nbrp || !rp->nbrp->dhcp_proxy) {
+        return;
+    }
+
+    struct ovn_port *sp = NULL;
+    struct nbrec_dhcp_proxy *dhcp_proxy = rp->nbrp->dhcp_proxy;
+
+    for (int i=0; i<op->od->n_router_ports; i++) {
+        struct ovn_port *sp_tmp = op->od->router_ports[i];
+        if (sp_tmp->peer == rp) {
+            sp = sp_tmp;
+            break;
+        }
+    }
+    if (!sp) {
+      return;
+    }
+
+    char *server_ip_str = NULL;
+    uint16_t port;
+    int addr_family;
+    struct in6_addr server_ip;
+
+    if (!ip_address_and_port_from_lb_key(dhcp_proxy->servers, &server_ip_str,
+                                         &server_ip, &port, &addr_family)) {
+        return;
+    }
+
+    if (server_ip_str == NULL) {
+        return;
+    }
+
+    ds_put_format(
+        &match, "inport == %s && eth.src == %s && "
+        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
+        "udp.src == 68 && udp.dst == 67",
+        op->json_key, op->lsp_addrs[0].ea_s);
+    ds_put_format(&action,
+                  "eth.dst=%s;outport=%s;next;/* DHCP_PROXY_REQ */",
+                  rp->lrp_networks.ea_s,sp->json_key);
+    ovn_lflow_add_with_hint__(lflows, op->od,
+                              S_SWITCH_IN_L2_LKUP, 100,
+                              ds_cstr(&match),
+                              ds_cstr(&action),
+                              op->key,
+                              NULL,
+                              &lrp->header_);
+    ds_clear(&match);
+    ds_clear(&action);
+
+    ds_put_format(
+        &match, "inport == %s && eth.src == %s && outport == %s &&"
+        "ip4.src == %s && "
+        "udp.src == 67 && udp.dst == 67",
+        sp->json_key, rp->lrp_networks.ea_s, op->json_key, server_ip_str);
+    ds_put_format(&action,
+                  "dhcp_proxy_resp(%s,%s);ip4.src=%s;udp.dst=68;next;"
+                  "/* DHCP_PROXY_RESP */",
+                  rp->lrp_networks.ipv4_addrs[0].addr_s,
+                  server_ip_str,
+                  rp->lrp_networks.ipv4_addrs[0].addr_s);
+    ovn_lflow_add_with_hint__(lflows, op->od,
+                              S_SWITCH_OUT_DHCP_PROXY_RESP, 100,
+                              ds_cstr(&match),
+                              ds_cstr(&action),
+                              op->key,
+                              NULL,
+                              &lrp->header_);
+    ds_clear(&match);
+    ds_clear(&action);
+    free(server_ip_str);
+}
+
  static void
  build_drop_arp_nd_flows_for_unbound_router_ports(struct ovn_port *op,
                                                   const struct ovn_port *port,
@@ -9672,6 +9771,13 @@ build_lswitch_dhcp_options_and_response(struct ovn_port 
*op,
          return;
      }
+ if (op->od && op->od->nbs
+        && op->od->nbs->dhcp_proxy_port) {
+        /* Don't add the DHCP server flows if DHCP Proxy is enabled on the
+         * logical switch. */
+        return;
+    }
+
      bool is_external = lsp_is_external(op->nbsp);
      if (is_external && (!op->od->n_localnet_ports ||
                          !op->nbsp->ha_chassis_group)) {
@@ -9719,6 +9825,7 @@ build_lswitch_dhcp_and_dns_defaults(struct ovn_datapath 
*od,
      ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_LOOKUP, 0, "1", "next;");
      ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 0, "1", "next;");
      ovn_lflow_add(lflows, od, S_SWITCH_IN_EXTERNAL_PORT, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_SWITCH_OUT_DHCP_PROXY_RESP, 0, "1", "next;");
  }
/* Logical switch ingress table 22 and 23: DNS lookup and response
@@ -13918,6 +14025,100 @@ build_dhcpv6_reply_flows_for_lrouter_port(
      }
  }
+static void
+build_dhcp_proxy_flows_for_lrouter_port(
+        struct ovn_port *op, struct hmap *lflows,
+        struct ds *match)
+{
+    if (!op->nbrp || !op->nbrp->dhcp_proxy) {
+        return;
+    }
+    struct nbrec_dhcp_proxy *dhcp_proxy = op->nbrp->dhcp_proxy;
+    if (!dhcp_proxy->servers) {
+        return;
+    }
+
+    int addr_family;
+    uint16_t port;
+    char *server_ip_str = NULL;
+    struct in6_addr server_ip;
+
+    if (!ip_address_and_port_from_lb_key(dhcp_proxy->servers, &server_ip_str,
+                                         &server_ip, &port, &addr_family)) {
+        return;
+    }
+
+    if (server_ip_str == NULL) {
+        return;
+    }
+
+    struct ds dhcp_action = DS_EMPTY_INITIALIZER;
+    ds_clear(match);
+    ds_put_format(
+        match, "inport == %s && "
+        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
+        "udp.src == 68 && udp.dst == 67",
+        op->json_key);
+    ds_put_format(&dhcp_action,
+                "dhcp_proxy_req(%s,%s);"
+                "ip4.src=%s;ip4.dst=%s;udp.src=67;next; /* DHCP_PROXY_REQ */",
+                op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str,
+                op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str);
+
+    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 110,
+                            ds_cstr(match), ds_cstr(&dhcp_action),
+                            &op->nbrp->header_);
+
+    ds_clear(match);
+    ds_clear(&dhcp_action);
+
+    ds_put_format(
+        match, "inport == %s && "
+        "ip4.dst == %s && "
+        "udp.src == 68 && udp.dst == 67",
+        op->json_key, op->lrp_networks.ipv4_addrs[0].addr_s);
+
+    ds_put_format(&dhcp_action,
+                "dhcp_proxy_req(%s,%s);"
+                "ip4.src=%s;ip4.dst=%s;udp.src=67;next; /* DHCP_PROXY_REQ */",
+                op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str,
+                op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str);
+    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 110,
+                            ds_cstr(match), ds_cstr(&dhcp_action),
+                            &op->nbrp->header_);
+    ds_clear(match);
+    ds_clear(&dhcp_action);
+
+    ds_put_format(
+        match, "ip4.src == %s && ip4.dst == %s && "
+        "udp.src == 67 && udp.dst == 67",
+        server_ip_str, op->lrp_networks.ipv4_addrs[0].addr_s);
+    ds_put_format(&dhcp_action, "next;/* DHCP_PROXY_RESP */");
+    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_IP_INPUT, 110,
+                            ds_cstr(match), ds_cstr(&dhcp_action),
+                            &op->nbrp->header_);
+
+    ds_clear(match);
+    ds_clear(&dhcp_action);
+
+    ds_put_format(
+        match, "ip4.src == %s && ip4.dst == %s && "
+        "udp.src == 67 && udp.dst == 67",
+        server_ip_str, op->lrp_networks.ipv4_addrs[0].addr_s);
+    ds_put_format(&dhcp_action,
+          "dhcp_proxy_resp_fwd();outport=%s;output; /* DHCP_PROXY_RESP */",
+          op->json_key);
+    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_PROXY_RESP_FWD,
+                            110,
+                            ds_cstr(match), ds_cstr(&dhcp_action),
+                            &op->nbrp->header_);
+
+    ds_clear(match);
+    ds_clear(&dhcp_action);
+
+    free(server_ip_str);
+}
+
  static void
  build_ipv6_input_flows_for_lrouter_port(
          struct ovn_port *op, struct hmap *lflows,
@@ -15134,6 +15335,7 @@ build_lrouter_nat_defrag_and_lb(struct ovn_datapath 
*od, struct hmap *lflows,
      ovn_lflow_add(lflows, od, S_ROUTER_OUT_POST_SNAT, 0, "1", "next;");
      ovn_lflow_add(lflows, od, S_ROUTER_OUT_EGR_LOOP, 0, "1", "next;");
      ovn_lflow_add(lflows, od, S_ROUTER_IN_ECMP_STATEFUL, 0, "1", "next;");
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_PROXY_RESP_FWD, 0, "1", 
"next;");
const char *ct_flag_reg = features->ct_no_masked_label
                                ? "ct_mark"
@@ -15614,6 +15816,7 @@ build_lswitch_and_lrouter_iterate_by_lsp(struct 
ovn_port *op,
      build_lswitch_dhcp_options_and_response(op, lflows, meter_groups);
      build_lswitch_external_port(op, lflows);
      build_lswitch_ip_unicast_lookup(op, lflows, actions, match);
+    build_lswitch_dhcp_proxy_flows(op, lr_ports, lflows, meter_groups);
/* Build Logical Router Flows. */
      build_ip_routing_flows_for_router_type_lsp(op, lr_ports, lflows);
@@ -15643,6 +15846,7 @@ build_lswitch_and_lrouter_iterate_by_lrp(struct 
ovn_port *op,
      build_egress_delivery_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
                                                   &lsi->actions);
      build_dhcpv6_reply_flows_for_lrouter_port(op, lsi->lflows, &lsi->match);
+    build_dhcp_proxy_flows_for_lrouter_port(op, lsi->lflows, &lsi->match);
      build_ipv6_input_flows_for_lrouter_port(op, lsi->lflows,
                                              &lsi->match, &lsi->actions,
                                              lsi->meter_groups);
diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
index f8bac5302..d448e2bfe 100644
--- a/ovn-nb.ovsschema
+++ b/ovn-nb.ovsschema
@@ -1,7 +1,7 @@
  {
      "name": "OVN_Northbound",
      "version": "7.0.4",
-    "cksum": "1676649201 33795",
+    "cksum": "2503700777 34818",
      "tables": {
          "NB_Global": {
              "columns": {
@@ -89,7 +89,12 @@
                      "type": {"key": {"type": "uuid",
                                       "refTable": "Forwarding_Group",
                                       "refType": "strong"},
-                                     "min": 0, "max": "unlimited"}}},
+                                     "min": 0, "max": "unlimited"}},
+                "dhcp_proxy_port": {"type": {"key": {"type": "uuid",
+                                            "refTable": "Logical_Router_Port",
+                                            "refType": "weak"},
+                                            "min": 0,
+                                            "max": 1}}},
              "isRoot": true},
          "Logical_Switch_Port": {
              "columns": {
@@ -436,6 +441,11 @@
                  "ipv6_prefix": {"type": {"key": "string",
                                        "min": 0,
                                        "max": "unlimited"}},
+                "dhcp_proxy": {"type": {"key": {"type": "uuid",
+                                            "refTable": "DHCP_Proxy",
+                                            "refType": "weak"},
+                                            "min": 0,
+                                            "max": 1}},
                  "external_ids": {
                      "type": {"key": "string", "value": "string",
                               "min": 0, "max": "unlimited"}}},
@@ -526,6 +536,15 @@
                      "type": {"key": "string", "value": "string",
                               "min": 0, "max": "unlimited"}}},
              "isRoot": true},
+        "DHCP_Proxy": {
+            "columns": {
+                "servers": {"type": {"key": "string",
+                                       "min": 0,
+                                       "max": 1}},
+                "external_ids": {
+                    "type": {"key": "string", "value": "string",
+                             "min": 0, "max": "unlimited"}}},
+            "isRoot": true},
          "Connection": {
              "columns": {
                  "target": {"type": "string"},
diff --git a/ovn-nb.xml b/ovn-nb.xml
index 4fbf4f7e5..00e4c41fc 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -572,6 +572,11 @@
        Please see the <ref table="DNS"/> table.
      </column>
+ <column name="dhcp_proxy_port">
+      This column defines the <ref table="Logical_Router_Port"/> on which
+      DHCP proxy is enabled.
+    </column>
+
      <column name="forwarding_groups">
        Groups a set of logical port endpoints for traffic going out of the
        logical switch.
@@ -2944,6 +2949,10 @@ or
        port has all ingress and egress traffic dropped.
      </column>
+ <column name="dhcp_proxy">
+      This column is used to enabled DHCP Proxy. Please refer to <ref 
table="DHCP_Proxy"/> table.
+    </column>
+
      <group title="Distributed Gateway Ports">
        <p>
          Gateways, as documented under <code>Gateways</code> in the OVN
@@ -4231,6 +4240,24 @@ or
      </group>
    </table>
+ <table name="DHCP_Proxy" title="DHCP Proxy">
+    <p>
+      OVN implements native DHCPv4 proxy support which caters to the common
+      use case of proxying the DHCP requests to external DHCP server.
+    </p>
+
+    <column name="servers">
+      <p>
+        The DHCPv4 server IP address.
+      </p>
+    </column>
+    <group title="Common Columns">
+      <column name="external_ids">
+        See <em>External IDs</em> at the beginning of this document.
+      </column>
+    </group>
+  </table>
+
    <table name="Connection" title="OVSDB client connections.">
      <p>
        Configuration for a database connection to an Open vSwitch database
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index d5be3be75..ace0c5c34 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -8206,6 +8206,8 @@ check ovn-nbctl --wait=sb ls-add sw0
ovn-sbctl dump-flows sw0 > sw0flows
  AT_CAPTURE_FILE([sw0flows])
+echo "FLOWS"
+cat sw0flows | grep -e port_sec -e ls_in_l2_lkup -e ls_in_l2_unknown | sort | 
sed 's/table=../table=??/'
AT_CHECK([cat sw0flows | grep -e port_sec -e ls_in_l2_lkup -e ls_in_l2_unknown | \
  sort | sed 's/table=../table=??/' ], [0], [dnl
@@ -8214,6 +8216,8 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_check_port_sec), priority=50   , match=(1), 
action=(reg0[[15]] = check_in_port_sec(); next;)
    table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
    table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
+  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
+  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
    table=??(ls_out_apply_port_sec), priority=0    , match=(1), action=(output;)
    table=??(ls_out_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
    table=??(ls_in_l2_lkup      ), priority=0    , match=(1), action=(outport = 
get_fdb(eth.dst); next;)
@@ -8221,8 +8225,6 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_l2_lkup      ), priority=70   , match=(eth.mcast), action=(outport = 
"_MC_flood"; output;)
    table=??(ls_in_l2_unknown   ), priority=0    , match=(1), action=(output;)
    table=??(ls_in_l2_unknown   ), priority=50   , match=(outport == "none"), 
action=(drop;)
-  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
-  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
  ])
check ovn-nbctl lsp-add sw0 sw0p1 -- lsp-set-addresses sw0p1 "00:00:00:00:00:01"
@@ -8239,6 +8241,8 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_check_port_sec), priority=50   , match=(1), 
action=(reg0[[15]] = check_in_port_sec(); next;)
    table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
    table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
+  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
+  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
    table=??(ls_out_apply_port_sec), priority=0    , match=(1), action=(output;)
    table=??(ls_out_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
    table=??(ls_in_l2_lkup      ), priority=0    , match=(1), action=(outport = 
get_fdb(eth.dst); next;)
@@ -8248,8 +8252,6 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_l2_lkup      ), priority=70   , match=(eth.mcast), action=(outport = 
"_MC_flood"; output;)
    table=??(ls_in_l2_unknown   ), priority=0    , match=(1), action=(output;)
    table=??(ls_in_l2_unknown   ), priority=50   , match=(outport == "none"), 
action=(drop;)
-  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
-  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
  ])
check ovn-nbctl lsp-set-port-security sw0p1 "00:00:00:00:00:01 10.0.0.3 1000::3"
@@ -8265,6 +8267,8 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_check_port_sec), priority=50   , match=(1), 
action=(reg0[[15]] = check_in_port_sec(); next;)
    table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
    table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
+  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
+  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
    table=??(ls_out_apply_port_sec), priority=0    , match=(1), action=(output;)
    table=??(ls_out_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
    table=??(ls_in_l2_lkup      ), priority=0    , match=(1), action=(outport = 
get_fdb(eth.dst); next;)
@@ -8274,8 +8278,6 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_l2_lkup      ), priority=70   , match=(eth.mcast), action=(outport = 
"_MC_flood"; output;)
    table=??(ls_in_l2_unknown   ), priority=0    , match=(1), action=(output;)
    table=??(ls_in_l2_unknown   ), priority=50   , match=(outport == "none"), 
action=(drop;)
-  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
-  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
  ])
# Disable sw0p1
@@ -8292,6 +8294,8 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_check_port_sec), priority=50   , match=(1), 
action=(reg0[[15]] = check_in_port_sec(); next;)
    table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
    table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
+  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
+  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
    table=??(ls_out_apply_port_sec), priority=0    , match=(1), action=(output;)
    table=??(ls_out_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
    table=??(ls_in_l2_lkup      ), priority=0    , match=(1), action=(outport = 
get_fdb(eth.dst); next;)
@@ -8302,8 +8306,6 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_l2_unknown   ), priority=0    , match=(1), action=(output;)
    table=??(ls_in_l2_unknown   ), priority=50   , match=(outport == "none"), 
action=(drop;)
    table=??(ls_in_l2_unknown   ), priority=50   , match=(outport == "sw0p1"), 
action=(drop;)
-  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
-  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
  ])
check ovn-nbctl --wait=sb lsp-set-options sw0p2 qdisc_queue_id=10
@@ -8319,6 +8321,8 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_check_port_sec), priority=70   , match=(inport == "sw0p2"), 
action=(set_queue(10); reg0[[15]] = check_in_port_sec(); next;)
    table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
    table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
+  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
+  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
    table=??(ls_out_apply_port_sec), priority=0    , match=(1), action=(output;)
    table=??(ls_out_apply_port_sec), priority=110  , match=(outport == "localnetport" && 
inport == "sw0p2"), action=(set_queue(10); output;)
    table=??(ls_out_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
@@ -8330,8 +8334,6 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_l2_unknown   ), priority=0    , match=(1), action=(output;)
    table=??(ls_in_l2_unknown   ), priority=50   , match=(outport == "none"), 
action=(drop;)
    table=??(ls_in_l2_unknown   ), priority=50   , match=(outport == "sw0p1"), 
action=(drop;)
-  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
-  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
  ])
check ovn-nbctl set logical_switch_port sw0p1 enabled=true
@@ -8350,6 +8352,8 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_check_port_sec), priority=70   , match=(inport == "sw0p2"), 
action=(set_queue(10); reg0[[15]] = check_in_port_sec(); next;)
    table=??(ls_in_apply_port_sec), priority=0    , match=(1), action=(next;)
    table=??(ls_in_apply_port_sec), priority=50   , match=(reg0[[15]] == 1), 
action=(drop;)
+  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
+  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
    table=??(ls_out_apply_port_sec), priority=0    , match=(1), action=(output;)
    table=??(ls_out_apply_port_sec), priority=100  , match=(outport == 
"localnetport"), action=(set_queue(10); output;)
    table=??(ls_out_apply_port_sec), priority=110  , match=(outport == "localnetport" && 
inport == "sw0p2"), action=(set_queue(10); output;)
@@ -8361,8 +8365,6 @@ sort | sed 's/table=../table=??/' ], [0], [dnl
    table=??(ls_in_l2_lkup      ), priority=70   , match=(eth.mcast), action=(outport = 
"_MC_flood"; output;)
    table=??(ls_in_l2_unknown   ), priority=0    , match=(1), action=(output;)
    table=??(ls_in_l2_unknown   ), priority=50   , match=(outport == "none"), 
action=(drop;)
-  table=??(ls_out_check_port_sec), priority=0    , match=(1), 
action=(reg0[[15]] = check_out_port_sec(); next;)
-  table=??(ls_out_check_port_sec), priority=100  , match=(eth.mcast), 
action=(reg0[[15]] = 0; next;)
  ])
AT_CLEANUP
@@ -8684,9 +8686,9 @@ ovn-nbctl --wait=sb set logical_router_port R1-PUB 
options:redirect-type=bridged
  ovn-sbctl dump-flows R1 > R1flows
  AT_CAPTURE_FILE([R1flows])
-AT_CHECK([grep "lr_in_arp_resolve" R1flows | grep priority=90 | sort], [0], [dnl
-  table=17(lr_in_arp_resolve  ), priority=90   , match=(outport == "R1-PUB" && ip4.src == 
10.0.0.3 && is_chassis_resident("S0-P0")), action=(get_arp(outport, reg0); next;)
-  table=17(lr_in_arp_resolve  ), priority=90   , match=(outport == "R1-PUB" && ip6.src == 
1000::3 && is_chassis_resident("S0-P0")), action=(get_nd(outport, xxreg0); next;)
+AT_CHECK([grep "lr_in_arp_resolve" R1flows | grep priority=90 | sed 
's/table=../table=??/' | sort], [0], [dnl
+  table=??(lr_in_arp_resolve  ), priority=90   , match=(outport == "R1-PUB" && ip4.src == 
10.0.0.3 && is_chassis_resident("S0-P0")), action=(get_arp(outport, reg0); next;)
+  table=??(lr_in_arp_resolve  ), priority=90   , match=(outport == "R1-PUB" && ip6.src == 
1000::3 && is_chassis_resident("S0-P0")), action=(get_nd(outport, xxreg0); next;)
  ])
AT_CLEANUP
diff --git a/tests/ovn.at b/tests/ovn.at
index 2cf5d5169..03e4d77e5 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -21871,7 +21871,7 @@ eth_dst=00000000ff01
  ip_src=$(ip_to_hex 10 0 0 10)
  ip_dst=$(ip_to_hex 172 168 0 101)
  send_icmp_packet 1 1 $eth_src $eth_dst $ip_src $ip_dst c4c9 
0000000000000000000000
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int metadata=0x$lr0_dp_key | awk '/table=28, 
n_packets=1, n_bytes=45/{print $7" "$8}'],[0],[dnl
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int metadata=0x$lr0_dp_key | awk '/table=29, 
n_packets=1, n_bytes=45/{print $7" "$8}'],[0],[dnl
  priority=80,ip,reg15=0x3,metadata=0x3,nw_src=10.0.0.10 actions=drop
  ])
@@ -28888,7 +28888,7 @@ AT_CHECK([
          grep "priority=100" | \
          grep -c 
"ct(commit,zone=NXM_NX_REG11\\[[0..15\\]],.*exec(move:NXM_OF_ETH_SRC\\[[\\]]->NXM_NX_CT_LABEL\\[[32..79\\]],load:0x[[0-9]]->NXM_NX_CT_MARK\\[[16..31\\]]))"
- grep table=25 hv${hv}flows | \
+        grep table=26 hv${hv}flows | \
          grep "priority=200" | \
          grep -c 
"move:NXM_NX_CT_LABEL\\[[\\]]->NXM_NX_XXREG1\\[[\\]],move:NXM_NX_XXREG1\\[[32..79\\]]->NXM_OF_ETH_DST"
      done; :], [0], [dnl
@@ -29013,7 +29013,7 @@ AT_CHECK([
          grep "priority=100" | \
          grep -c 
"ct(commit,zone=NXM_NX_REG11\\[[0..15\\]],.*exec(move:NXM_OF_ETH_SRC\\[[\\]]->NXM_NX_CT_LABEL\\[[32..79\\]],load:0x[[0-9]]->NXM_NX_CT_MARK\\[[16..31\\]]))"
- grep table=25 hv${hv}flows | \
+        grep table=26 hv${hv}flows | \
          grep "priority=200" | \
          grep -c 
"move:NXM_NX_CT_LABEL\\[[\\]]->NXM_NX_XXREG1\\[[\\]],move:NXM_NX_XXREG1\\[[32..79\\]]->NXM_OF_ETH_DST"
      done; :], [0], [dnl
@@ -29510,7 +29510,7 @@ if test X"$1" = X"DGP"; then
  else
      prio=2
  fi
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=25, n_packets=1,.* 
priority=$prio,ip,$inport.*$outport.*metadata=0x${sw_key},nw_dst=10.0.1.1 
actions=drop" -c], [0], [dnl
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=26, n_packets=1,.* 
priority=$prio,ip,$inport.*$outport.*metadata=0x${sw_key},nw_dst=10.0.1.1 
actions=drop" -c], [0], [dnl
  1
  ])
@@ -29529,13 +29529,13 @@ AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep "actions=controller" | grep if test X"$1" = X"DGP"; then
      # The packet dst should be resolved once for E/W centralized NAT purpose.
-    AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=25, 
n_packets=1,.* priority=100,reg0=0xa000101,reg15=.*metadata=0x${sw_key} 
actions=mod_dl_dst:00:00:00:00:01:01,resubmit" -c], [0], [dnl
+    AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=26, 
n_packets=1,.* priority=100,reg0=0xa000101,reg15=.*metadata=0x${sw_key} 
actions=mod_dl_dst:00:00:00:00:01:01,resubmit" -c], [0], [dnl
  1
  ])
  fi
# The packet should've been finally dropped in the lr_in_arp_resolve stage.
-AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=25, n_packets=2,.* 
priority=$prio,ip,$inport.*$outport.*metadata=0x${sw_key},nw_dst=10.0.1.1 
actions=drop" -c], [0], [dnl
+AT_CHECK([as hv1 ovs-ofctl dump-flows br-int | grep -E "table=26, n_packets=2,.* 
priority=$prio,ip,$inport.*$outport.*metadata=0x${sw_key},nw_dst=10.0.1.1 
actions=drop" -c], [0], [dnl
  1
  ])
  OVN_CLEANUP([hv1])
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index fb54ed060..9a0cbeafe 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -3205,6 +3205,18 @@ trace_actions(const struct ovnact *ovnacts, size_t 
ovnacts_len,
                                         super);
              break;
+ case OVNACT_DHCPV4_PROXY_REQ:
+            /* TODO. */
+            break;
+
+        case OVNACT_DHCPV4_PROXY_RESP:
+            /* TODO. */
+            break;
+
+        case OVNACT_DHCPV4_PROXY_RESP_FWD:
+            /* TODO. */
+            break;
+
          case OVNACT_PUT_DHCPV4_OPTS:
              execute_put_dhcp_opts(ovnact_get_PUT_DHCPV4_OPTS(a),
                                    "put_dhcp_opts", uflow, super);

_______________________________________________
dev mailing list
d...@openvswitch.org
https://mail.openvswitch.org/mailman/listinfo/ovs-dev

Reply via email to