diff mbox series

[ovs-dev,v2] DHCP Relay Agent support for overlay subnets.

Message ID 20240304041958.24204-1-naveen.yerramneni@nutanix.com
State Superseded
Delegated to: Numan Siddique
Headers show
Series [ovs-dev,v2] DHCP Relay Agent support for overlay subnets. | expand

Checks

Context Check Description
ovsrobot/apply-robot warning apply and check: warning
ovsrobot/github-robot-_Build_and_Test fail github build: failed
ovsrobot/github-robot-_ovn-kubernetes success github build: passed

Commit Message

Naveen Yerramneni March 4, 2024, 4:19 a.m. UTC
This patch contains changes to enable DHCP Relay Agent support for overlay subnets.

    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 relaying 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 rleay functionality in OVN.
      1. DHCP client originates DHCP discovery (broadcast).
      2. DHCP relay (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 increments hop count.
      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 relay agent forwards the offer to the client.
      5. DHCP client sends DHCP request (broadcast) packet.
      6. DHCP relay (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.
      7. DHCP Server sends the ACK packet.
      8. DHCP relay agent forwards the ACK packet to the client.
      9. All the future renew/release packets are directly exchanged between DHCP client and DHCP server.

    OVN DHCP RELAY PACKET FLOW:
    ----------------------------
    To add DHCP Relay support on OVN, we need to replicate all the behavior described above using distributed logical switch and logical router.
    At, 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 where VM is deployed and relays the packet to DHCP server.
      2. Response packet is first processed on RC node (which first recieves the packet from underlay network). RC node forwards the packet to the right node by filling in the dest MAC and IP.

    OVN Packet flow with DHCP relay is explained below.
      1. DHCP client (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(GIADDR, HOP count) in the DHCP payload after doing the required checks. If any check fails, packet is dropped.
      5. Logical Router converts the packet to L3 unicast and forwards it to the server. This packets 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 does sanity checks and  updates the destination MAC (available in DHCP header), destination IP (available in DHCP header) and reinjects the packet to datapath.
         If any check fails, packet is dropped.
      9. Logical router updates the source IP and port and forwards the packet to logical switch.
      10. Logical switch delivers the packet to the DHCP client.
      11. Similar steps are performed for Request and Ack packets.
      12. All the future renew/release packets are directly exchanged between DHCP client and DHCP server

    NEW OVN ACTIONS
    ---------------
      1. dhcp_relay_req_chk(<relay-ip>, <server-ip>)
          - This action executes on the source node on which the DHCP request originated.
          - This action relays the DHCP request coming from client to the server. Relay-ip is used to update GIADDR in the DHCP header.
      2. dhcp_relay_resp_chk(<relay-ip>, <server-ip>)
          - 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.
          - Relay-ip, server-ip are used to validate GIADDR and SERVER ID in the DHCP payload.

    FLOWS
    -----
    Following are the flows added when DHCP Relay is configured on one overlay subnet, one additonal flow is added in ls_in_l2_lkup table for each VM part of the 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>;next;/* DHCP_RELAY_REQ */)
      2. table=3 (lr_in_ip_input     ), priority=110  , match=(inport == <lrp> && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67),
         action=(reg9[7] = dhcp_relay_req_chk(<lrp_ip>, <dhcp_server_ip>);next; /* DHCP_RELAY_REQ */)
      3. table=3 (lr_in_ip_input     ), priority=110  , match=(ip4.src == <dhcp_server> && ip4.dst == <lrp> && udp.src == 67 && udp.dst == 67), action=(next;/* DHCP_RELAY_RESP */)
      4. table=4 (lr_in_dhcp_relay_req), priority=100  , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[7]),
         action=(ip4.src=<lrp>;ip4.dst=<dhcp_server>;udp.src=67;next; /* DHCP_RELAY_REQ */)
      5. table=4 (lr_in_dhcp_relay_req), priority=1    , match=(inport == <lrp> && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[7] == 0),
         action=(drop; /* DHCP_RELAY_REQ */)
      6. table=18(lr_in_dhcp_relay_resp_chk), priority=100  , match=(ip4.src == <dhcp_server> && ip4.dst == <lrp> && udp.src == 67 && udp.dst == 67),
         action=(reg2 = ip4.dst;reg9[8] = dhcp_relay_resp_chk(<lrp_ip>, <dhcp_server_ip>);next;/* DHCP_RELAY_RESP */)
      7. table=19(lr_in_dhcp_relay_resp), priority=100  , match=(ip4.src == <dhcp_server> && reg2 == <lrp_ip> && udp.src == 67 && udp.dst == 67 && reg9[8]),
         action=(ip4.src=<lrp>;udp.dst=68;outport=<lrp>;output; /* DHCP_RELAY_RESP */)
      8. table=19(lr_in_dhcp_relay_resp), priority=1    , match=(ip4.src == <dhcp_server> && reg2 == <lrp_ip> && udp.src == 67 && udp.dst == 67 && reg9[8] == 0), action=(drop; /* DHCP_RELAY_RESP */)

    NEW PIPELINE STAGES
    -------------------
    Following stage is added for DHCP relay feature. Some of the flows are fitted into the existing pipeline tages.
      1. lr_in_dhcp_relay_req
          - This stage process the DHCP request packets coming from DHCP clients.
          - DHCP request packets for which dhcp_relay_req_chk action (which gets applied in ip input stage) is successful are forwarded to DHCP server.
          - DHCP request packets for which dhcp_relay_req_chk action is unsuccessful gets dropped.
      2. lr_in_dhcp_relay_resp_chk
          - This stage applied the dhcp_relay_resp_chk action for  DHCP response packets coming from the DHCP server.
      3. lr_in_dhcp_relay_resp
          - DHCP response packets for which dhcp_relay_resp_chk is sucessful are forwarded to the DHCP clients.
          - DHCP response packets for which dhcp_relay_resp_chk is unsucessful gets dropped.

    REGISTRY USAGE
    ---------------
      - reg9[7] : To store the result of dhcp_relay_req_chk action.
      - reg9[8] : To store the result of dhcp_relay_resp_chk action.
      - reg2 : To store the original dest ip for DHCP response packets.
               This is required to properly match the packets in
               lr_in_dhcp_relay_resp stage since dhcp_relay_resp_chk action
               changes the dest ip.

    NB SCHEMA CHANGES
    ----------------
      1. New DHCP_Relay table
          "DHCP_Relay": {
                "columns": {
                    "name": {"type": "string"},
                    "servers": {"type": {"key": "string",
                                           "min": 0,
                                           "max": 1}},
                    "external_ids": {
                        "type": {"key": "string", "value": "string",
                                "min": 0, "max": "unlimited"}}},
                    "options": {"type": {"key": "string", "value": "string",
                                "min": 0, "max": "unlimited"}},
                "isRoot": true},
      2. New column to Logical_Router_Port table
          "dhcp_relay": {"type": {"key": {"type": "uuid",
                                "refTable": "DHCP_Relay",
                                "refType": "strong"},
                                "min": 0,
                                "max": 1}},

    Commands to enable the feature:
    ------------------------------
     ovn-nbctl create DHCP_Relay name=<name> servers=<dhcp_server_ip>
     ovn-nbctl set Logical_Router_port <lrp> dhcp_relay=<relay_uuid>
     ovn-nbctl set Logical_Switch <ls> other_config:dhcp_relay_port=<router_patch_port>

    Example:
    -------
     ovn-nbctl ls-add ls0
     ovn-nbctl lsp-add ls0 vif0
     ovn-nbctl lsp-set-addresses vif0 <MAC> #Only MAC address has to be specified when logical ports are created.
     ovn-nbctl lsp-add ls0 lrp1-attachment
     ovn-nbctl lsp-set-type lrp1-attachment router
     ovn-nbctl lsp-set-addresses lrp1-attachment
     ovn-nbctl lsp-set-options lrp1-attachment router-port=lrp1
     ovn-nbctl lr-add lr0
     ovn-nbctl lrp-add lr0 lrp1 <MAC> <GATEWAY_IP/Prefix> #GATEWAY IP is set in GIADDR field when relaying the DHCP requests to server.
     ovn-nbctl lrp-add lr0 lrp-ext <MAC> <GATEWAY_IP/Prefix>
     ovn-nbctl ls-add ls-ext
     ovn-nbctl lsp-add ls-ext lrp-ext-attachment
     ovn-nbctl lsp-set-type lrp-ext-attachment router
     ovn-nbctl lsp-set-addresses lrp-ext-attachment
     ovn-nbctl lsp-set-options lrp-ext-attachment router-port=lrp-ext
     ovn-nbctl lsp-add ls-ext ln_port
     ovn-nbctl lsp-set-addresses ln_port unknown
     ovn-nbctl lsp-set-type ln_port localnet
     ovn-nbctl lsp-set-options ln_port network_name=physnet1
     # Enable DHCP Relay feature
     ovn-nbctl create DHCP_Relay name=dhcp_relay_test servers=<dhcp_server_ip>
     ovn-nbctl set Logical_Router_port lrp1 dhcp_relay=<relay_uuid>
     ovn-nbctl set Logical_Switch ls0 other_config:dhcp_relay_port=lrp1-attachment

    Limitations:
    ------------
      - All OVN features that needs IP address to be configured on logical port (like proxy arp, etc) will not be supported for overlay subnets on which DHCP relay is enabled.

    References:
    ----------
      - rfc1541, rfc1542, rfc2131

Signed-off-by: Naveen Yerramneni <naveen.yerramneni@nutanix.com>
Co-authored-by: Huzaifa Calcuttawala <huzaifa.c@nutanix.com>
Signed-off-by: Huzaifa Calcuttawala <huzaifa.c@nutanix.com>
CC: Mary Manohar <mary.manohar@nutanix.com>
---
V2:
  Addressed review comments from Numan. 
---
 controller/pinctrl.c  | 596 +++++++++++++++++++++++++++++++++++++-----
 include/ovn/actions.h |  27 ++
 lib/actions.c         | 149 +++++++++++
 lib/ovn-l7.h          |   2 +
 northd/northd.c       | 265 ++++++++++++++++++-
 northd/northd.h       |  41 +--
 ovn-nb.ovsschema      |  19 +-
 ovn-nb.xml            |  39 +++
 tests/atlocal.in      |   3 +
 tests/ovn-northd.at   |  38 +++
 tests/ovn.at          | 293 +++++++++++++++++++--
 tests/system-ovn.at   | 148 +++++++++++
 utilities/ovn-trace.c |  67 +++++
 13 files changed, 1576 insertions(+), 111 deletions(-)

Comments

0-day Robot March 4, 2024, 4:39 a.m. UTC | #1
References:  <20240304041958.24204-1-naveen.yerramneni@nutanix.com>
 

Bleep bloop.  Greetings Naveen Yerramneni, I am a robot and I have tried out your patch.
Thanks for your contribution.

I encountered some error that I wasn't expecting.  See the details below.


checkpatch:
WARNING: Line is 80 characters long (recommended limit is 79)
#231 FILE: controller/pinctrl.c:1947:
     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var len)|

WARNING: Line is 101 characters long (recommended limit is 79)
#1097 FILE: northd/northd.c:237:
 * | R2     REG_DHCP_RELAY_DIP_IPV4  | X |                 | 0 |                                    |

Lines checked: 2295, Warnings: 2, Errors: 0


Please check this out.  If you feel there has been an error, please email aconole@redhat.com

Thanks,
0-day Robot
Numan Siddique March 14, 2024, 2:21 p.m. UTC | #2
On Sun, Mar 3, 2024 at 11:20 PM Naveen Yerramneni
<naveen.yerramneni@nutanix.com> wrote:
>
>     This patch contains changes to enable DHCP Relay Agent support for overlay subnets.
>
>     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 relaying 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 rleay functionality in OVN.
>       1. DHCP client originates DHCP discovery (broadcast).
>       2. DHCP relay (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 increments hop count.
>       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 relay agent forwards the offer to the client.
>       5. DHCP client sends DHCP request (broadcast) packet.
>       6. DHCP relay (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.
>       7. DHCP Server sends the ACK packet.
>       8. DHCP relay agent forwards the ACK packet to the client.
>       9. All the future renew/release packets are directly exchanged between DHCP client and DHCP server.
>
>     OVN DHCP RELAY PACKET FLOW:
>     ----------------------------
>     To add DHCP Relay support on OVN, we need to replicate all the behavior described above using distributed logical switch and logical router.
>     At, 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 where VM is deployed and relays the packet to DHCP server.
>       2. Response packet is first processed on RC node (which first recieves the packet from underlay network). RC node forwards the packet to the right node by filling in the dest MAC and IP.
>
>     OVN Packet flow with DHCP relay is explained below.
>       1. DHCP client (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(GIADDR, HOP count) in the DHCP payload after doing the required checks. If any check fails, packet is dropped.
>       5. Logical Router converts the packet to L3 unicast and forwards it to the server. This packets 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 does sanity checks and  updates the destination MAC (available in DHCP header), destination IP (available in DHCP header) and reinjects the packet to datapath.
>          If any check fails, packet is dropped.
>       9. Logical router updates the source IP and port and forwards the packet to logical switch.
>       10. Logical switch delivers the packet to the DHCP client.
>       11. Similar steps are performed for Request and Ack packets.
>       12. All the future renew/release packets are directly exchanged between DHCP client and DHCP server
>
>     NEW OVN ACTIONS
>     ---------------
>       1. dhcp_relay_req_chk(<relay-ip>, <server-ip>)
>           - This action executes on the source node on which the DHCP request originated.
>           - This action relays the DHCP request coming from client to the server. Relay-ip is used to update GIADDR in the DHCP header.
>       2. dhcp_relay_resp_chk(<relay-ip>, <server-ip>)
>           - 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.
>           - Relay-ip, server-ip are used to validate GIADDR and SERVER ID in the DHCP payload.
>
>     FLOWS
>     -----
>     Following are the flows added when DHCP Relay is configured on one overlay subnet, one additonal flow is added in ls_in_l2_lkup table for each VM part of the 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>;next;/* DHCP_RELAY_REQ */)
>       2. table=3 (lr_in_ip_input     ), priority=110  , match=(inport == <lrp> && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67),
>          action=(reg9[7] = dhcp_relay_req_chk(<lrp_ip>, <dhcp_server_ip>);next; /* DHCP_RELAY_REQ */)
>       3. table=3 (lr_in_ip_input     ), priority=110  , match=(ip4.src == <dhcp_server> && ip4.dst == <lrp> && udp.src == 67 && udp.dst == 67), action=(next;/* DHCP_RELAY_RESP */)
>       4. table=4 (lr_in_dhcp_relay_req), priority=100  , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[7]),
>          action=(ip4.src=<lrp>;ip4.dst=<dhcp_server>;udp.src=67;next; /* DHCP_RELAY_REQ */)
>       5. table=4 (lr_in_dhcp_relay_req), priority=1    , match=(inport == <lrp> && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[7] == 0),
>          action=(drop; /* DHCP_RELAY_REQ */)
>       6. table=18(lr_in_dhcp_relay_resp_chk), priority=100  , match=(ip4.src == <dhcp_server> && ip4.dst == <lrp> && udp.src == 67 && udp.dst == 67),
>          action=(reg2 = ip4.dst;reg9[8] = dhcp_relay_resp_chk(<lrp_ip>, <dhcp_server_ip>);next;/* DHCP_RELAY_RESP */)
>       7. table=19(lr_in_dhcp_relay_resp), priority=100  , match=(ip4.src == <dhcp_server> && reg2 == <lrp_ip> && udp.src == 67 && udp.dst == 67 && reg9[8]),
>          action=(ip4.src=<lrp>;udp.dst=68;outport=<lrp>;output; /* DHCP_RELAY_RESP */)
>       8. table=19(lr_in_dhcp_relay_resp), priority=1    , match=(ip4.src == <dhcp_server> && reg2 == <lrp_ip> && udp.src == 67 && udp.dst == 67 && reg9[8] == 0), action=(drop; /* DHCP_RELAY_RESP */)
>
>     NEW PIPELINE STAGES
>     -------------------
>     Following stage is added for DHCP relay feature. Some of the flows are fitted into the existing pipeline tages.
>       1. lr_in_dhcp_relay_req
>           - This stage process the DHCP request packets coming from DHCP clients.
>           - DHCP request packets for which dhcp_relay_req_chk action (which gets applied in ip input stage) is successful are forwarded to DHCP server.
>           - DHCP request packets for which dhcp_relay_req_chk action is unsuccessful gets dropped.
>       2. lr_in_dhcp_relay_resp_chk
>           - This stage applied the dhcp_relay_resp_chk action for  DHCP response packets coming from the DHCP server.
>       3. lr_in_dhcp_relay_resp
>           - DHCP response packets for which dhcp_relay_resp_chk is sucessful are forwarded to the DHCP clients.
>           - DHCP response packets for which dhcp_relay_resp_chk is unsucessful gets dropped.
>
>     REGISTRY USAGE
>     ---------------
>       - reg9[7] : To store the result of dhcp_relay_req_chk action.
>       - reg9[8] : To store the result of dhcp_relay_resp_chk action.
>       - reg2 : To store the original dest ip for DHCP response packets.
>                This is required to properly match the packets in
>                lr_in_dhcp_relay_resp stage since dhcp_relay_resp_chk action
>                changes the dest ip.
>
>     NB SCHEMA CHANGES
>     ----------------
>       1. New DHCP_Relay table
>           "DHCP_Relay": {
>                 "columns": {
>                     "name": {"type": "string"},
>                     "servers": {"type": {"key": "string",
>                                            "min": 0,
>                                            "max": 1}},
>                     "external_ids": {
>                         "type": {"key": "string", "value": "string",
>                                 "min": 0, "max": "unlimited"}}},
>                     "options": {"type": {"key": "string", "value": "string",
>                                 "min": 0, "max": "unlimited"}},
>                 "isRoot": true},
>       2. New column to Logical_Router_Port table
>           "dhcp_relay": {"type": {"key": {"type": "uuid",
>                                 "refTable": "DHCP_Relay",
>                                 "refType": "strong"},
>                                 "min": 0,
>                                 "max": 1}},
>
>     Commands to enable the feature:
>     ------------------------------
>      ovn-nbctl create DHCP_Relay name=<name> servers=<dhcp_server_ip>
>      ovn-nbctl set Logical_Router_port <lrp> dhcp_relay=<relay_uuid>
>      ovn-nbctl set Logical_Switch <ls> other_config:dhcp_relay_port=<router_patch_port>
>
>     Example:
>     -------
>      ovn-nbctl ls-add ls0
>      ovn-nbctl lsp-add ls0 vif0
>      ovn-nbctl lsp-set-addresses vif0 <MAC> #Only MAC address has to be specified when logical ports are created.
>      ovn-nbctl lsp-add ls0 lrp1-attachment
>      ovn-nbctl lsp-set-type lrp1-attachment router
>      ovn-nbctl lsp-set-addresses lrp1-attachment
>      ovn-nbctl lsp-set-options lrp1-attachment router-port=lrp1
>      ovn-nbctl lr-add lr0
>      ovn-nbctl lrp-add lr0 lrp1 <MAC> <GATEWAY_IP/Prefix> #GATEWAY IP is set in GIADDR field when relaying the DHCP requests to server.
>      ovn-nbctl lrp-add lr0 lrp-ext <MAC> <GATEWAY_IP/Prefix>
>      ovn-nbctl ls-add ls-ext
>      ovn-nbctl lsp-add ls-ext lrp-ext-attachment
>      ovn-nbctl lsp-set-type lrp-ext-attachment router
>      ovn-nbctl lsp-set-addresses lrp-ext-attachment
>      ovn-nbctl lsp-set-options lrp-ext-attachment router-port=lrp-ext
>      ovn-nbctl lsp-add ls-ext ln_port
>      ovn-nbctl lsp-set-addresses ln_port unknown
>      ovn-nbctl lsp-set-type ln_port localnet
>      ovn-nbctl lsp-set-options ln_port network_name=physnet1
>      # Enable DHCP Relay feature
>      ovn-nbctl create DHCP_Relay name=dhcp_relay_test servers=<dhcp_server_ip>
>      ovn-nbctl set Logical_Router_port lrp1 dhcp_relay=<relay_uuid>
>      ovn-nbctl set Logical_Switch ls0 other_config:dhcp_relay_port=lrp1-attachment
>
>     Limitations:
>     ------------
>       - All OVN features that needs IP address to be configured on logical port (like proxy arp, etc) will not be supported for overlay subnets on which DHCP relay is enabled.
>
>     References:
>     ----------
>       - rfc1541, rfc1542, rfc2131
>
> Signed-off-by: Naveen Yerramneni <naveen.yerramneni@nutanix.com>
> Co-authored-by: Huzaifa Calcuttawala <huzaifa.c@nutanix.com>
> Signed-off-by: Huzaifa Calcuttawala <huzaifa.c@nutanix.com>
> CC: Mary Manohar <mary.manohar@nutanix.com>
> ---
> V2:
>   Addressed review comments from Numan.

Thanks for the v2.  This patch is failing compilation with gcc and
address sanitizer configured (./configure --enable-Werror
--enable-sparse 'CFLAGS=-g -fsanitize=address')

---------------------------

../utilities/ovn-trace.c:2368:19: error: incorrect type in assignment
(different base types)
../utilities/ovn-trace.c:2368:19:    expected restricted ovs_be32
[usertype] nw_dst
../utilities/ovn-trace.c:2368:19:    got unsigned int
depbase=`echo utilities/ovn-appctl.o | sed 's|[^/]*$|.deps/&|;s|\.o$||'`;\

--

../controller/pinctrl.c:2174:14: error: incorrect type in argument 2
(different base types)
../controller/pinctrl.c:2174:14:    expected restricted ovs_be16
[usertype] old_u16
../controller/pinctrl.c:2174:14:    got unsigned short [usertype]
../controller/pinctrl.c:2174:38: error: incorrect type in argument 3
(different base types)
../controller/pinctrl.c:2174:38:    expected restricted ovs_be16
[usertype] new_u16
../controller/pinctrl.c:2174:38:    got unsigned short [usertype]
make[1]: *** [Makefile:2371: controller/pinctrl.o] Error 1

--------------------------------


I'd suggest split this patch into a 3 patch series.

In patch 1 - pinctrl.c changes for the action opcodes
ACTION_OPCODE_DHCP_RELAY_REQ_CHK and ACTION_OPCODE_DHCP_RELAY_RESP_CHK
In patch 2 - Add the OVN logical actions - dhcp_relay_req_chk and
dhcp_relay_resp_chk and the corresponding tests.
and finally northd changes and test cases in patch 3.

There are indentation issues with the patch.  Please correct me as per
the present code/coding guidelines.

Please see below for a few comments.

Thanks
Numan


> ---
>  controller/pinctrl.c  | 596 +++++++++++++++++++++++++++++++++++++-----
>  include/ovn/actions.h |  27 ++
>  lib/actions.c         | 149 +++++++++++
>  lib/ovn-l7.h          |   2 +
>  northd/northd.c       | 265 ++++++++++++++++++-
>  northd/northd.h       |  41 +--
>  ovn-nb.ovsschema      |  19 +-
>  ovn-nb.xml            |  39 +++
>  tests/atlocal.in      |   3 +
>  tests/ovn-northd.at   |  38 +++
>  tests/ovn.at          | 293 +++++++++++++++++++--
>  tests/system-ovn.at   | 148 +++++++++++
>  utilities/ovn-trace.c |  67 +++++
>  13 files changed, 1576 insertions(+), 111 deletions(-)
>
> diff --git a/controller/pinctrl.c b/controller/pinctrl.c
> index 98b29de9f..a776ac7c5 100644
> --- a/controller/pinctrl.c
> +++ b/controller/pinctrl.c
> @@ -1909,6 +1909,514 @@ is_dhcp_flags_broadcast(ovs_be16 flags)
>      return flags & htons(DHCP_BROADCAST_FLAG);
>  }
>
> +static const char *dhcp_msg_str[] = {
> +[0] = "INVALID",
> +[DHCP_MSG_DISCOVER] = "DISCOVER",
> +[DHCP_MSG_OFFER] = "OFFER",
> +[DHCP_MSG_REQUEST] = "REQUEST",
> +[OVN_DHCP_MSG_DECLINE] = "DECLINE",
> +[DHCP_MSG_ACK] = "ACK",
> +[DHCP_MSG_NAK] = "NAK",
> +[OVN_DHCP_MSG_RELEASE] = "RELEASE",
> +[OVN_DHCP_MSG_INFORM] = "INFORM"
> +};
> +
> +static bool
> +dhcp_relay_is_msg_type_supported(uint8_t msg_type)
> +{
> +    return (msg_type >= DHCP_MSG_DISCOVER && msg_type <= OVN_DHCP_MSG_RELEASE);
> +}
> +
> +static const char *dhcp_msg_str_get(uint8_t msg_type)
> +{
> +    if (!dhcp_relay_is_msg_type_supported(msg_type)) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "Unknown DHCP msg type: %u", msg_type);
> +        return "UNKNOWN";
> +    }
> +    return dhcp_msg_str[msg_type];
> +}
> +
> +static const struct dhcp_header *
> +dhcp_get_hdr_from_pkt(struct dp_packet *pkt_in, const char **in_dhcp_pptr,
> +                  const char *end)
> +{
> +    /* Validate the DHCP request packet.
> +     * Format of the DHCP packet is
> +     * ------------------------------------------------------------------------
> +     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var len)|
> +     * ------------------------------------------------------------------------
> +     */
> +
> +    *in_dhcp_pptr = dp_packet_get_udp_payload(pkt_in);
> +    if (*in_dhcp_pptr == NULL) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP: Invalid or incomplete DHCP packet received");
> +        return NULL;
> +    }
> +
> +    const struct dhcp_header *dhcp_hdr
> +        = (const struct dhcp_header *) *in_dhcp_pptr;
> +    (*in_dhcp_pptr) += sizeof *dhcp_hdr;
> +    if (*in_dhcp_pptr > end) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP: Invalid or incomplete DHCP packet received, "
> +                     "bad data length");
> +        return NULL;
> +    }
> +
> +    if (dhcp_hdr->htype != 0x1) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP: Packet is recieved with "
> +                "unsupported hardware type");
> +        return NULL;
> +    }
> +
> +    if (dhcp_hdr->hlen != 0x6) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP: Packet is recieved with "
> +                "unsupported hardware length");
> +        return NULL;
> +    }
> +
> +    /* 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_pptr) + sizeof magic_cookie > end ||
> +        get_unaligned_be32((const void *) (*in_dhcp_pptr)) != magic_cookie) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP: Magic cookie not present in the DHCP packet");
> +        return NULL;
> +    }
> +
> +    (*in_dhcp_pptr) += sizeof magic_cookie;
> +
> +    return dhcp_hdr;
> +}
> +
> +static void
> +dhcp_parse_options(const char **in_dhcp_pptr, const char *end,
> +          const uint8_t **dhcp_msg_type_pptr, ovs_be32 *request_ip_ptr,
> +          bool *ipxe_req_ptr, ovs_be32 *server_id_ptr,
> +          ovs_be32 *netmask_ptr, ovs_be32 *router_ip_ptr)
> +{
> +    while ((*in_dhcp_pptr) < end) {
> +        const struct dhcp_opt_header *in_dhcp_opt =
> +            (const struct dhcp_opt_header *) *in_dhcp_pptr;
> +        if (in_dhcp_opt->code == DHCP_OPT_END) {
> +            break;
> +        }
> +        if (in_dhcp_opt->code == DHCP_OPT_PAD) {
> +            (*in_dhcp_pptr) += 1;
> +            continue;
> +        }
> +        (*in_dhcp_pptr) += sizeof *in_dhcp_opt;
> +        if ((*in_dhcp_pptr) > end) {
> +            break;
> +        }
> +        (*in_dhcp_pptr) += in_dhcp_opt->len;
> +        if ((*in_dhcp_pptr) > end) {
> +            break;
> +        }
> +
> +        switch (in_dhcp_opt->code) {
> +        case DHCP_OPT_MSG_TYPE:
> +            if (dhcp_msg_type_pptr && in_dhcp_opt->len == 1) {
> +                *dhcp_msg_type_pptr = DHCP_OPT_PAYLOAD(in_dhcp_opt);
> +            }
> +            break;
> +        case DHCP_OPT_REQ_IP:
> +            if (request_ip_ptr && in_dhcp_opt->len == 4) {
> +                *request_ip_ptr = get_unaligned_be32(
> +                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
> +            }
> +            break;
> +        case OVN_DHCP_OPT_CODE_SERVER_ID:
> +            if (server_id_ptr && in_dhcp_opt->len == 4) {
> +                *server_id_ptr = get_unaligned_be32(
> +                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
> +            }
> +            break;
> +        case OVN_DHCP_OPT_CODE_NETMASK:
> +            if (netmask_ptr && in_dhcp_opt->len == 4) {
> +                *netmask_ptr = get_unaligned_be32(
> +                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
> +            }
> +            break;
> +        case OVN_DHCP_OPT_CODE_ROUTER_IP:
> +            if (router_ip_ptr && in_dhcp_opt->len == 4) {
> +                *router_ip_ptr = get_unaligned_be32(
> +                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
> +            }
> +            break;
> +        case DHCP_OPT_ETHERBOOT:
> +            if (ipxe_req_ptr) {
> +                *ipxe_req_ptr = true;
> +            }
> +            break;
> +        default:
> +            break;
> +        }
> +    }
> +}
> +
> +/* Called with in the pinctrl_handler thread context. */
> +static void
> +pinctrl_handle_dhcp_relay_req_chk(
> +    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;
> +    uint32_t success = 0;
> +
> +    /* Parse result field. */
> +    const struct mf_field *f;
> +    enum ofperr ofperr = nx_pull_header(userdata, NULL, &f, NULL);
> +    if (ofperr) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: bad result OXM (%s)",
> +                        ofperr_to_string(ofperr));
> +        goto exit;
> +    }
> +    ovs_be32 *ofsp = ofpbuf_try_pull(userdata, sizeof *ofsp);
> +    /* Check that the result is valid and writable. */
> +    struct mf_subfield dst = { .field = f, .ofs = ntohl(*ofsp), .n_bits = 1 };
> +    ofperr = mf_check_dst(&dst, NULL);
> +    if (ofperr) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: bad result bit (%s)",
> +                        ofperr_to_string(ofperr));
> +        goto exit;
> +    }
> +
> +    /* Parse relay IP and server IP. */
> +    ovs_be32 *relay_ip = ofpbuf_try_pull(userdata, sizeof *relay_ip);
> +    ovs_be32 *server_ip = ofpbuf_try_pull(userdata, sizeof *server_ip);
> +    if (!relay_ip || !server_ip) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: relay ip or server ip "
> +                  "not present in the userdata");
> +        goto exit;
> +    }
> +
> +    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 = NULL;
> +    const struct dhcp_header *in_dhcp_data
> +                          = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
> +
> +    if (!in_dhcp_data) {
> +        goto exit;
> +    }
> +    ovs_assert(in_dhcp_ptr);
> +
> +    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_RELAY_REQ_CHK: invalid opcode in the "
> +                "DHCP packet: %d", in_dhcp_data->op);
> +        goto exit;
> +    }
> +
> +    if (in_dhcp_data->giaddr) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: giaddr is already set");
> +        goto exit;
> +    }
> +
> +    const uint8_t *in_dhcp_msg_type = NULL;
> +    ovs_be32 request_ip = in_dhcp_data->ciaddr;
> +
> +    dhcp_parse_options(&in_dhcp_ptr, end,
> +          &in_dhcp_msg_type, &request_ip, NULL, NULL, NULL, NULL);
> +
> +    /* 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_RELAY_REQ_CHK: missing message type");
> +        goto exit;
> +    }
> +
> +    /* Relay the DHCP request packet */
> +    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 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);
> +
> +    uint8_t hops = dhcp_data->hops;
> +    dhcp_data->hops = hops + 1;
> +    if (udp->udp_csum) {
> +        udp->udp_csum = recalc_csum16(udp->udp_csum,
> +            (uint16_t) (hops << 8), (uint16_t) (dhcp_data->hops << 8));

recalc_csum16() expects all the arguments of type 'ovs_be16'  but
you're casting to uint16_t.
That's why the compilation is failing.


> +    }
> +
> +    dhcp_data->giaddr = *relay_ip;
> +    if (udp->udp_csum) {
> +        udp->udp_csum = recalc_csum32(udp->udp_csum,
> +            0, dhcp_data->giaddr);
> +    }
> +    pin->packet = dp_packet_data(&pkt_out);
> +    pin->packet_len = dp_packet_size(&pkt_out);
> +
> +    /* Log the DHCP message. */
> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(20, 40);
> +    VLOG_INFO_RL(&rl, "DHCP_RELAY_REQ_CHK:: MSG_TYPE:%s MAC:"ETH_ADDR_FMT
> +                " XID:%u"
> +                " REQ_IP:"IP_FMT
> +                " GIADDR:"IP_FMT
> +                " SERVER_ADDR:"IP_FMT,
> +                dhcp_msg_str_get(*in_dhcp_msg_type),
> +                ETH_ADDR_BYTES_ARGS(dhcp_data->chaddr), ntohl(dhcp_data->xid),
> +                IP_ARGS(request_ip), IP_ARGS(dhcp_data->giaddr),
> +                IP_ARGS(*server_ip));
> +    success = 1;
> +exit:
> +    if (!ofperr) {
> +        union mf_subvalue sv;
> +        sv.u8_val = success;
> +        mf_write_subfield(&dst, &sv, &pin->flow_metadata);
> +    }
> +    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_relay_resp_chk(
> +    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;
> +    uint32_t success = 0;
> +
> +    /* Parse result field. */
> +    const struct mf_field *f;
> +    enum ofperr ofperr = nx_pull_header(userdata, NULL, &f, NULL);
> +    if (ofperr) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: bad result OXM (%s)",
> +                        ofperr_to_string(ofperr));
> +        goto exit;
> +    }
> +    ovs_be32 *ofsp = ofpbuf_try_pull(userdata, sizeof *ofsp);
> +    /* Check that the result is valid and writable. */
> +    struct mf_subfield dst = { .field = f, .ofs = ntohl(*ofsp), .n_bits = 1 };
> +    ofperr = mf_check_dst(&dst, NULL);
> +    if (ofperr) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: bad result bit (%s)",
> +                        ofperr_to_string(ofperr));
> +        goto exit;
> +    }
> +
> +    /* Parse relay IP and server IP. */
> +    ovs_be32 *relay_ip = ofpbuf_try_pull(userdata, sizeof *relay_ip);
> +    ovs_be32 *server_ip = ofpbuf_try_pull(userdata, sizeof *server_ip);
> +    if (!relay_ip || !server_ip) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: relay ip or server ip "
> +                "not present in the userdata");
> +        goto exit;
> +    }
> +
> +    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 = NULL;
> +    const struct dhcp_header *in_dhcp_data
> +                          = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
> +
> +    if (!in_dhcp_data) {
> +        goto exit;
> +    }
> +    ovs_assert(in_dhcp_ptr);
> +
> +    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_RELAY_RESP_CHK: invalid opcode "
> +                "in the packet: %d", in_dhcp_data->op);
> +        goto exit;
> +    }
> +
> +    if (!in_dhcp_data->giaddr) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: giaddr is "
> +                    "not set in request");
> +        goto exit;
> +    }
> +
> +    ovs_be32 giaddr = in_dhcp_data->giaddr;
> +    ovs_be32 yiaddr = in_dhcp_data->yiaddr;
> +    ovs_be32 server_id = 0, netmask = 0, router_ip = 0;
> +    const uint8_t *in_dhcp_msg_type = NULL;
> +
> +    dhcp_parse_options(&in_dhcp_ptr, end,
> +          &in_dhcp_msg_type, NULL, NULL, &server_id, &netmask, &router_ip);
> +
> +    /* 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_RELAY_RESP: missing message type");
> +        goto exit;
> +    }
> +
> +    if (!server_id) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP: missing server identifier");
> +        goto exit;
> +    }
> +
> +    if (server_id != *server_ip) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP: server identifier mismatch");
> +        goto exit;
> +    }
> +
> +    if (giaddr != *relay_ip) {
> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP: giaddr mismatch");
> +        goto exit;
> +    }
> +
> +    if (*in_dhcp_msg_type == DHCP_MSG_OFFER ||
> +        *in_dhcp_msg_type == DHCP_MSG_ACK) {
> +        if ((yiaddr & netmask) != (giaddr & netmask)) {
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +            VLOG_INFO_RL(&rl, "DHCP_RELAY_RESP_CHK::"
> +                     " Allocated ip adress and giaddr are not in same subnet."
> +                     " MSG_TYPE:%s MAC:"ETH_ADDR_FMT
> +                     " XID:%u"
> +                     " YIADDR:"IP_FMT
> +                     " GIADDR:"IP_FMT
> +                     " SERVER_ADDR:"IP_FMT,
> +                     dhcp_msg_str_get(*in_dhcp_msg_type),
> +                     ETH_ADDR_BYTES_ARGS(in_dhcp_data->chaddr),
> +                     ntohl(in_dhcp_data->xid),
> +                     IP_ARGS(yiaddr),
> +                     IP_ARGS(giaddr), IP_ARGS(server_id));
> +            goto exit;
> +        }
> +
> +        if (router_ip && router_ip != giaddr) {
> +            /* Log the default gateway mismatch and
> +             * continue with rest of the processing */
> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
> +            VLOG_INFO_RL(&rl, "DHCP_RELAY_RESP_CHK::"
> +                     " Router ip adress and giaddr are not same."
> +                     " MSG_TYPE:%s MAC:"ETH_ADDR_FMT
> +                     " XID:%u"
> +                     " YIADDR:"IP_FMT
> +                     " GIADDR:"IP_FMT
> +                     " SERVER_ADDR:"IP_FMT,
> +                     dhcp_msg_str_get(*in_dhcp_msg_type),
> +                     ETH_ADDR_BYTES_ARGS(in_dhcp_data->chaddr),
> +                     ntohl(in_dhcp_data->xid),
> +                     IP_ARGS(yiaddr),
> +                     IP_ARGS(giaddr), IP_ARGS(server_id));
> +        }
> +    }
> +
> +    /* 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);
> +
> +    /* Log the DHCP message. */
> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(20, 40);
> +    VLOG_INFO_RL(&rl, "DHCP_RELAY_RESP_CHK:: MSG_TYPE:%s MAC:"ETH_ADDR_FMT
> +             " XID:%u"
> +             " YIADDR:"IP_FMT
> +             " GIADDR:"IP_FMT
> +             " SERVER_ADDR:"IP_FMT,
> +             dhcp_msg_str_get(*in_dhcp_msg_type),
> +             ETH_ADDR_BYTES_ARGS(dhcp_data->chaddr), ntohl(dhcp_data->xid),
> +             IP_ARGS(yiaddr),
> +             IP_ARGS(giaddr), IP_ARGS(server_id));
> +    success = 1;
> +exit:
> +    if (!ofperr) {
> +        union mf_subvalue sv;
> +        sv.u8_val = success;
> +        mf_write_subfield(&dst, &sv, &pin->flow_metadata);
> +    }
> +    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(
> @@ -1956,30 +2464,16 @@ pinctrl_handle_put_dhcp_opts(
>          goto exit;
>      }
>
> -    /* Validate the DHCP request packet.
> -     * Format of the DHCP packet is
> -     * ------------------------------------------------------------------------
> -     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var len)|
> -     * ------------------------------------------------------------------------
> -     */
> -
>      const char *end = (char *)dp_packet_l4(pkt_in) + dp_packet_l4_size(pkt_in);
> -    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, "Invalid or incomplete DHCP packet received");
> -        goto exit;
> -    }
> -
> +    const char *in_dhcp_ptr = NULL;
>      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, "Invalid or incomplete DHCP packet received, "
> -                     "bad data length");
> +                          = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
> +
> +    if (!in_dhcp_data) {
>          goto exit;
>      }
> +    ovs_assert(in_dhcp_ptr);
> +
>      if (in_dhcp_data->op != DHCP_OP_REQUEST) {
>          static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>          VLOG_WARN_RL(&rl, "Invalid opcode in the DHCP packet: %d",
> @@ -1987,58 +2481,11 @@ pinctrl_handle_put_dhcp_opts(
>          goto exit;
>      }
>
> -    /* 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 magic cookie not present in the DHCP packet");
> -        goto exit;
> -    }
> -    in_dhcp_ptr += sizeof magic_cookie;
> -
>      bool ipxe_req = false;
>      const uint8_t *in_dhcp_msg_type = NULL;
>      ovs_be32 request_ip = in_dhcp_data->ciaddr;
> -    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 DHCP_OPT_REQ_IP:
> -            if (in_dhcp_opt->len == 4) {
> -                request_ip = get_unaligned_be32(DHCP_OPT_PAYLOAD(in_dhcp_opt));
> -            }
> -            break;
> -        case DHCP_OPT_ETHERBOOT:
> -            ipxe_req = true;
> -            break;
> -        default:
> -            break;
> -        }
> -    }
> +    dhcp_parse_options(&in_dhcp_ptr, end,
> +          &in_dhcp_msg_type, &request_ip, &ipxe_req, NULL, NULL, NULL);
>
>      /* Check that the DHCP Message Type (opt 53) is present or not with
>       * valid values - DHCP_MSG_DISCOVER or DHCP_MSG_REQUEST.
> @@ -2245,6 +2692,7 @@ pinctrl_handle_put_dhcp_opts(
>          dhcp_data->yiaddr = 0;
>      }
>
> +    ovs_be32 magic_cookie = htonl(DHCP_MAGIC_COOKIE);
>      dp_packet_put(&pkt_out, &magic_cookie, sizeof(ovs_be32));
>
>      uint16_t out_dhcp_opts_size = 12;
> @@ -3215,6 +3663,16 @@ process_packet_in(struct rconn *swconn, const struct ofp_header *msg)
>          ovs_mutex_unlock(&pinctrl_mutex);
>          break;
>
> +    case ACTION_OPCODE_DHCP_RELAY_REQ_CHK:
> +        pinctrl_handle_dhcp_relay_req_chk(swconn, &packet, &pin,
> +                                     &userdata, &continuation);
> +        break;
> +
> +    case ACTION_OPCODE_DHCP_RELAY_RESP_CHK:
> +        pinctrl_handle_dhcp_relay_resp_chk(swconn, &packet, &pin,
> +                                     &userdata, &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 49fb96fc6..a8e4393ed 100644
> --- a/include/ovn/actions.h
> +++ b/include/ovn/actions.h
> @@ -95,6 +95,8 @@ 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_RELAY_REQ_CHK,  ovnact_dhcp_relay)      \
> +    OVNACT(DHCPV4_RELAY_RESP_CHK, ovnact_dhcp_relay)      \
>      OVNACT(SET_QUEUE,         ovnact_set_queue)       \
>      OVNACT(DNS_LOOKUP,        ovnact_result)          \
>      OVNACT(LOG,               ovnact_log)             \
> @@ -395,6 +397,15 @@ struct ovnact_put_opts {
>      size_t n_options;
>  };
>
> +/* OVNACT_DHCP_RELAY. */
> +struct ovnact_dhcp_relay {
> +    struct ovnact ovnact;
> +    int family;
> +    struct expr_field dst;      /* 1-bit destination field. */
> +    ovs_be32 relay_ipv4;
> +    ovs_be32 server_ipv4;
> +};
> +
>  /* Valid arguments to SET_QUEUE action.
>   *
>   * QDISC_MIN_QUEUE_ID is the default queue, so user-defined queues should
> @@ -758,6 +769,22 @@ enum action_opcode {
>
>      /* multicast group split buffer action. */
>      ACTION_OPCODE_MG_SPLIT_BUF,
> +
> +    /* "dhcp_relay_req_chk(relay_ip, server_ip)".
> +     *
> +     * Arguments follow the action_header, in this format:
> +     *   - The 32-bit DHCP relay IP.
> +     *   - The 32-bit DHCP server IP.
> +     */
> +    ACTION_OPCODE_DHCP_RELAY_REQ_CHK,
> +
> +    /* "dhcp_relay_resp_chk(relay_ip, server_ip)".
> +     *
> +     * Arguments follow the action_header, in this format:
> +     *   - The 32-bit DHCP relay IP.
> +     *   - The 32-bit DHCP server IP.
> +     */
> +    ACTION_OPCODE_DHCP_RELAY_RESP_CHK,
>  };
>
>  /* Header. */
> diff --git a/lib/actions.c b/lib/actions.c
> index a45874dfb..d55b5153f 100644
> --- a/lib/actions.c
> +++ b/lib/actions.c
> @@ -2680,6 +2680,149 @@ ovnact_controller_event_free(struct ovnact_controller_event *event)
>      free_gen_options(event->options, event->n_options);
>  }
>
> +static void
> +format_DHCPV4_RELAY_REQ_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
> +                struct ds *s)
> +{
> +    expr_field_format(&dhcp_relay->dst, s);
> +    ds_put_format(s, " = dhcp_relay_req_chk("IP_FMT", "IP_FMT");",
> +                  IP_ARGS(dhcp_relay->relay_ipv4),
> +                  IP_ARGS(dhcp_relay->server_ipv4));
> +}
> +
> +static void
> +parse_dhcp_relay_req_chk(struct action_context *ctx,
> +               const struct expr_field *dst,
> +               struct ovnact_dhcp_relay *dhcp_relay)
> +{
> +    /* Skip dhcp_relay_req_chk( */
> +    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
> +
> +    /* Validate that the destination is a 1-bit, modifiable field. */
> +    char *error = expr_type_check(dst, 1, true, ctx->scope);
> +    if (error) {
> +        lexer_error(ctx->lexer, "%s", error);
> +        free(error);
> +        return;
> +    }
> +    dhcp_relay->dst = *dst;
> +
> +    /* Parse relay ip and server ip. */
> +    if (ctx->lexer->token.format == LEX_F_IPV4) {
> +        dhcp_relay->family = AF_INET;
> +        dhcp_relay->relay_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_relay->family = AF_INET;
> +            dhcp_relay->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 relay "
> +                          "and server ips");
> +          return;
> +    }
> +    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
> +}
> +
> +static void
> +encode_DHCPV4_RELAY_REQ_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
> +                    const struct ovnact_encode_params *ep,
> +                    struct ofpbuf *ofpacts)
> +{
> +    struct mf_subfield dst = expr_resolve_field(&dhcp_relay->dst);
> +    size_t oc_offset = encode_start_controller_op(
> +                                            ACTION_OPCODE_DHCP_RELAY_REQ_CHK,
> +                                            true, ep->ctrl_meter_id,
> +                                            ofpacts);
> +    nx_put_header(ofpacts, dst.field->id, OFP15_VERSION, false);
> +    ovs_be32 ofs = htonl(dst.ofs);
> +    ofpbuf_put(ofpacts, &ofs, sizeof ofs);
> +    ofpbuf_put(ofpacts, &dhcp_relay->relay_ipv4,
> +            sizeof(dhcp_relay->relay_ipv4));
> +    ofpbuf_put(ofpacts, &dhcp_relay->server_ipv4,
> +            sizeof(dhcp_relay->server_ipv4));
> +    encode_finish_controller_op(oc_offset, ofpacts);
> +}
> +
> +static void
> +format_DHCPV4_RELAY_RESP_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
> +                    struct ds *s)
> +{
> +    expr_field_format(&dhcp_relay->dst, s);
> +    ds_put_format(s, " = dhcp_relay_resp_chk("IP_FMT", "IP_FMT");",
> +                  IP_ARGS(dhcp_relay->relay_ipv4),
> +                  IP_ARGS(dhcp_relay->server_ipv4));
> +}
> +
> +static void
> +parse_dhcp_relay_resp_chk(struct action_context *ctx,
> +               const struct expr_field *dst,
> +               struct ovnact_dhcp_relay *dhcp_relay)
> +{
> +    /* Skip dhcp_relay_resp_chk( */
> +    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
> +
> +    /* Validate that the destination is a 1-bit, modifiable field. */
> +    char *error = expr_type_check(dst, 1, true, ctx->scope);
> +    if (error) {
> +        lexer_error(ctx->lexer, "%s", error);
> +        free(error);
> +        return;
> +    }
> +    dhcp_relay->dst = *dst;
> +
> +    /* Parse relay ip and server ip. */
> +    if (ctx->lexer->token.format == LEX_F_IPV4) {
> +        dhcp_relay->family = AF_INET;
> +        dhcp_relay->relay_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_relay->family = AF_INET;
> +            dhcp_relay->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 relay and "
> +                          "server ips");
> +          return;
> +    }
> +    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
> +}
> +
> +static void
> +encode_DHCPV4_RELAY_RESP_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
> +                    const struct ovnact_encode_params *ep,
> +                    struct ofpbuf *ofpacts)
> +{
> +    struct mf_subfield dst = expr_resolve_field(&dhcp_relay->dst);
> +    size_t oc_offset = encode_start_controller_op(
> +                                ACTION_OPCODE_DHCP_RELAY_RESP_CHK,
> +                                true, ep->ctrl_meter_id,
> +                                ofpacts);
> +    nx_put_header(ofpacts, dst.field->id, OFP15_VERSION, false);
> +    ovs_be32 ofs = htonl(dst.ofs);
> +    ofpbuf_put(ofpacts, &ofs, sizeof ofs);
> +    ofpbuf_put(ofpacts, &dhcp_relay->relay_ipv4,
> +                  sizeof(dhcp_relay->relay_ipv4));
> +    ofpbuf_put(ofpacts, &dhcp_relay->server_ipv4,
> +                  sizeof(dhcp_relay->server_ipv4));
> +    encode_finish_controller_op(oc_offset, ofpacts);
> +}
> +
> +static void ovnact_dhcp_relay_free(
> +          struct ovnact_dhcp_relay *dhcp_relay 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,
> @@ -5384,6 +5527,12 @@ parse_set_action(struct action_context *ctx)
>                     lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
>              parse_chk_lb_aff(ctx, &lhs,
>                      ovnact_put_CHK_LB_AFF(ctx->ovnacts));
> +        } else if (lexer_match_id(ctx->lexer, "dhcp_relay_req_chk")) {
> +            parse_dhcp_relay_req_chk(ctx, &lhs,
> +                    ovnact_put_DHCPV4_RELAY_REQ_CHK(ctx->ovnacts));
> +        } else if (lexer_match_id(ctx->lexer, "dhcp_relay_resp_chk")) {
> +            parse_dhcp_relay_resp_chk(ctx, &lhs,
> +                    ovnact_put_DHCPV4_RELAY_RESP_CHK(ctx->ovnacts));
>          } else {
>              parse_assignment_action(ctx, false, &lhs);
>          }
> diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h
> index ad514a922..5eeb7c1a0 100644
> --- a/lib/ovn-l7.h
> +++ b/lib/ovn-l7.h
> @@ -68,7 +68,9 @@ struct gen_opts_map {
>   * OVN_DHCP_OPT_CODE_<opt_name>.
>   */
>  #define OVN_DHCP_OPT_CODE_NETMASK      1
> +#define OVN_DHCP_OPT_CODE_ROUTER_IP    3
>  #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 2c3560ce2..1f7a62982 100644
> --- a/northd/northd.c
> +++ b/northd/northd.c
> @@ -91,7 +91,6 @@ static bool use_ct_inv_match = true;
>  static bool default_acl_drop;
>
>  #define MAX_OVN_TAGS 4096
> -
>
>  /* Due to various hard-coded priorities need to implement ACLs, the
>   * northbound database supports a smaller range of ACL priorities than
> @@ -153,6 +152,8 @@ static bool default_acl_drop;
>  #define REGBIT_LOOKUP_NEIGHBOR_IP_RESULT "reg9[3]"
>  #define REGBIT_DST_NAT_IP_LOCAL "reg9[4]"
>  #define REGBIT_KNOWN_LB_SESSION "reg9[6]"
> +#define REGBIT_DHCP_RELAY_REQ_CHK "reg9[7]"
> +#define REGBIT_DHCP_RELAY_RESP_CHK "reg9[8]"
>
>  /* Register to store the eth address associated to a router port for packets
>   * received in S_ROUTER_IN_ADMISSION.
> @@ -168,6 +169,7 @@ static bool default_acl_drop;
>  #define REG_NEXT_HOP_IPV6 "xxreg0"
>  #define REG_SRC_IPV4 "reg1"
>  #define REG_SRC_IPV6 "xxreg1"
> +#define REG_DHCP_RELAY_DIP_IPV4 "reg2"
>  #define REG_ROUTE_TABLE_ID "reg7"
>
>  /* Register used to store backend ipv6 address
> @@ -232,7 +234,7 @@ static bool default_acl_drop;
>   * | R1  |   SRC_IPV4 for ARP-REQ    | 0 |                 | R |                                    |
>   * |     |      (>= IP_INPUT)        |   |                 | E |     NEXT_HOP_IPV6 (>= DEFRAG )     |
>   * +-----+---------------------------+---+-----------------+ G |                                    |
> - * | R2  |        UNUSED             | X |                 | 0 |                                    |
> + * | R2     REG_DHCP_RELAY_DIP_IPV4  | X |                 | 0 |                                    |
>   * |     |                           | R |                 |   |                                    |
>   * +-----+---------------------------+ E |     UNUSED      |   |                                    |
>   * | R3  |        UNUSED             | G |                 |   |                                    |
> @@ -259,7 +261,9 @@ static bool default_acl_drop;
>   * |     |   EGRESS_LOOPBACK/        | G |     UNUSED      |
>   * | R9  |   PKT_LARGER/             | 4 |                 |
>   * |     |   LOOKUP_NEIGHBOR_RESULT/ |   |                 |
> - * |     |   SKIP_LOOKUP_NEIGHBOR}   |   |                 |
> + * |     |   SKIP_LOOKUP_NEIGHBOR/   |   |                 |
> + * |     |REGBIT_DHCP_RELAY_REQ_CHK/ |   |                 |
> + * |     |REGBIT_DHCP_RELAY_RESP_CHK}|   |                 |
>   * |     |                           |   |                 |
>   * |     | REG_ORIG_TP_DPORT_ROUTER  |   |                 |
>   * |     |                           |   |                 |
> @@ -8555,6 +8559,85 @@ build_dhcpv6_options_flows(struct ovn_port *op,
>      ds_destroy(&match);
>  }
>
> +static const char *
> +ls_dhcp_relay_port(const struct ovn_datapath *od)
> +{
> +    return smap_get(&od->nbs->other_config, "dhcp_relay_port");
> +}
> +
> +static void
> +build_lswitch_dhcp_relay_flows(struct ovn_port *op,
> +                           const struct hmap *ls_ports,
> +                           struct lflow_table *lflows)
> +{
> +    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) {
> +        return;
> +    }
> +
> +    /* configure dhcp relay flows only when peer router  has
> +     * relay config enabled */
> +    const char *dhcp_relay_port = ls_dhcp_relay_port(op->od);
> +    if (!dhcp_relay_port) {
> +        return;
> +    }
> +
> +    struct ds match = DS_EMPTY_INITIALIZER;
> +    struct ds action = DS_EMPTY_INITIALIZER;
> +    struct ovn_port *sp = ovn_port_find(ls_ports, dhcp_relay_port);
> +
> +    if (!sp || !sp->nbsp || !sp->peer) {
> +        return;
> +    }
> +
> +    struct ovn_port *rp = sp->peer;
> +    if (!rp || !rp->nbrp || !rp->nbrp->dhcp_relay || rp->peer != sp) {
> +        return;
> +    }
> +
> +    char *server_ip_str = NULL;
> +    uint16_t port;
> +    int addr_family;
> +    struct in6_addr server_ip;
> +    struct nbrec_dhcp_relay *dhcp_relay = rp->nbrp->dhcp_relay;
> +
> +    if (!ip_address_and_port_from_lb_key(dhcp_relay->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_RELAY_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,
> +                              &op->nbsp->header_,
> +                              op->lflow_ref);
> +    free(server_ip_str);
> +}
> +
>  static void
>  build_drop_arp_nd_flows_for_unbound_router_ports(struct ovn_port *op,
>                                                   const struct ovn_port *port,
> @@ -9156,6 +9239,13 @@ build_lswitch_dhcp_options_and_response(struct ovn_port *op,
>          return;
>      }
>
> +    if (op->od && op->od->nbs
> +        && ls_dhcp_relay_port(op->od)) {
> +        /* Don't add the DHCP server flows if DHCP Relay 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)) {
> @@ -13636,6 +13726,165 @@ build_dhcpv6_reply_flows_for_lrouter_port(
>      }
>  }
>
> +static void
> +build_dhcp_relay_flows_for_lrouter_port(
> +        struct ovn_port *op, struct lflow_table *lflows,
> +        struct ds *match, struct lflow_ref *lflow_ref)
> +{
> +    if (!op->nbrp || !op->nbrp->dhcp_relay) {
> +        return;
> +
> +    }
> +
> +    /* configure dhcp relay flows only when peer switch has
> +     * relay config enabled */
> +    struct ovn_port *sp = op->peer;
> +    if (!sp || !sp->nbsp || sp->peer != op ||
> +        !sp->od || !ls_dhcp_relay_port(sp->od)) {
> +        return;
> +    }
> +
> +    struct nbrec_dhcp_relay *dhcp_relay = op->nbrp->dhcp_relay;
> +    if (!dhcp_relay->servers) {
> +        return;
> +    }
> +
> +    int addr_family;
> +    /* currently not supporting custom port,
> +     * dhcp server port is always set to 67 when installing flows */
> +    uint16_t port;
> +    char *server_ip_str = NULL;
> +    struct in6_addr server_ip;
> +
> +    if (!ip_address_and_port_from_lb_key(dhcp_relay->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,
> +                REGBIT_DHCP_RELAY_REQ_CHK
> +                " = dhcp_relay_req_chk(%s, %s);"
> +                "next; /* DHCP_RELAY_REQ */",
> +                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_, lflow_ref);
> +
> +    ds_clear(match);
> +    ds_clear(&dhcp_action);
> +
> +    ds_put_format(
> +        match, "inport == %s && "
> +        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
> +        "udp.src == 68 && udp.dst == 67 && "
> +        REGBIT_DHCP_RELAY_REQ_CHK,
> +        op->json_key);
> +    ds_put_format(&dhcp_action,
> +                "ip4.src=%s;ip4.dst=%s;udp.src=67;next; /* DHCP_RELAY_REQ */",
> +                op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str);
> +
> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_REQ, 100,
> +                            ds_cstr(match), ds_cstr(&dhcp_action),
> +                            &op->nbrp->header_, lflow_ref);
> +
> +    ds_clear(match);
> +    ds_clear(&dhcp_action);
> +
> +    ds_put_format(
> +        match, "inport == %s && "
> +        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
> +        "udp.src == 68 && udp.dst == 67 && "
> +        REGBIT_DHCP_RELAY_REQ_CHK" == 0",
> +        op->json_key);
> +    ds_put_format(&dhcp_action,
> +                "drop; /* DHCP_RELAY_REQ */");
> +
> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_REQ, 1,
> +                            ds_cstr(match), ds_cstr(&dhcp_action),
> +                            &op->nbrp->header_, lflow_ref);
> +
> +    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_RELAY_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_, lflow_ref);
> +
> +    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,
> +          REG_DHCP_RELAY_DIP_IPV4" = ip4.dst;"
> +          REGBIT_DHCP_RELAY_RESP_CHK
> +          " = dhcp_relay_resp_chk(%s, %s);next;/* DHCP_RELAY_RESP */",
> +          op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str);
> +
> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_RESP_CHK,
> +                            100,
> +                            ds_cstr(match), ds_cstr(&dhcp_action),
> +                            &op->nbrp->header_, lflow_ref);
> +
> +
> +    ds_clear(match);
> +    ds_clear(&dhcp_action);
> +
> +    ds_put_format(
> +        match, "ip4.src == %s && "
> +        REG_DHCP_RELAY_DIP_IPV4" == %s && "
> +        "udp.src == 67 && udp.dst == 67 && "
> +        REGBIT_DHCP_RELAY_RESP_CHK,
> +        server_ip_str, op->lrp_networks.ipv4_addrs[0].addr_s);
> +    ds_put_format(&dhcp_action,
> +          "ip4.src=%s;udp.dst=68;"
> +          "outport=%s;output; /* DHCP_RELAY_RESP */",
> +          op->lrp_networks.ipv4_addrs[0].addr_s, op->json_key);
> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_RESP,
> +                            100,
> +                            ds_cstr(match), ds_cstr(&dhcp_action),
> +                            &op->nbrp->header_, lflow_ref);
> +
> +    ds_clear(match);
> +    ds_clear(&dhcp_action);
> +
> +    ds_put_format(
> +        match, "ip4.src == %s && "
> +        REG_DHCP_RELAY_DIP_IPV4" == %s && "
> +        "udp.src == 67 && udp.dst == 67 && "
> +        REGBIT_DHCP_RELAY_RESP_CHK" == 0",
> +        server_ip_str, op->lrp_networks.ipv4_addrs[0].addr_s);
> +    ds_put_format(&dhcp_action,
> +          "drop; /* DHCP_RELAY_RESP */");
> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_RESP,
> +                            1,
> +                            ds_cstr(match), ds_cstr(&dhcp_action),
> +                            &op->nbrp->header_, lflow_ref);
> +    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 lflow_table *lflows,
> @@ -14906,6 +15155,13 @@ static void build_lr_nat_defrag_and_lb_default_flows(
>                    lflow_ref);
>      ovn_lflow_add(lflows, od, S_ROUTER_IN_ECMP_STATEFUL, 0, "1", "next;",
>                    lflow_ref);
> +    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_RELAY_REQ, 0, "1",
> +                  "next;", lflow_ref);
> +    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_RELAY_RESP_CHK, 0, "1",
> +                  "next;", lflow_ref);
> +    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_RELAY_RESP, 0, "1",
> +                  "next;", lflow_ref);
> +
>
>      /* Send the IPv6 NS packets to next table. When ovn-controller
>       * generates IPv6 NS (for the action - nd_ns{}), the injected
> @@ -15670,6 +15926,7 @@ build_lswitch_and_lrouter_iterate_by_lsp(struct ovn_port *op,
>      build_lswitch_icmp_packet_toobig_admin_flows(op, lflows, match, actions);
>      build_lswitch_ip_unicast_lookup(op, lflows, actions,
>                                      match);
> +    build_lswitch_dhcp_relay_flows(op, ls_ports, lflows);
>
>      /* Build Logical Router Flows. */
>      build_arp_resolve_flows_for_lsp(op, lflows, lr_ports, match, actions);
> @@ -15699,6 +15956,8 @@ build_lswitch_and_lrouter_iterate_by_lrp(struct ovn_port *op,
>                                                   op->lflow_ref);
>      build_dhcpv6_reply_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
>                                                op->lflow_ref);
> +    build_dhcp_relay_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
> +                                              op->lflow_ref);
>      build_ipv6_input_flows_for_lrouter_port(op, lsi->lflows,
>                                              &lsi->match, &lsi->actions,
>                                              lsi->meter_groups,
> diff --git a/northd/northd.h b/northd/northd.h
> index 3f1cd8341..0a349c8f7 100644
> --- a/northd/northd.h
> +++ b/northd/northd.h
> @@ -437,24 +437,29 @@ enum ovn_stage {
>      PIPELINE_STAGE(ROUTER, IN,  LOOKUP_NEIGHBOR, 1, "lr_in_lookup_neighbor") \
>      PIPELINE_STAGE(ROUTER, IN,  LEARN_NEIGHBOR,  2, "lr_in_learn_neighbor") \
>      PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,        3, "lr_in_ip_input")     \
> -    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          4, "lr_in_unsnat")       \
> -    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          5, "lr_in_defrag")       \
> -    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    6, "lr_in_lb_aff_check") \
> -    PIPELINE_STAGE(ROUTER, IN,  DNAT,            7, "lr_in_dnat")         \
> -    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    8, "lr_in_lb_aff_learn") \
> -    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   9, "lr_in_ecmp_stateful") \
> -    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   10, "lr_in_nd_ra_options") \
> -    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  11, "lr_in_nd_ra_response") \
> -    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  12, "lr_in_ip_routing_pre")  \
> -    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      13, "lr_in_ip_routing")      \
> -    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_RELAY_REQ,  4, "lr_in_dhcp_relay_req") \
> +    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          5, "lr_in_unsnat")       \
> +    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          6, "lr_in_defrag")       \
> +    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    7, "lr_in_lb_aff_check") \
> +    PIPELINE_STAGE(ROUTER, IN,  DNAT,            8, "lr_in_dnat")         \
> +    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    9, "lr_in_lb_aff_learn") \
> +    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   10, "lr_in_ecmp_stateful") \
> +    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   11, "lr_in_nd_ra_options") \
> +    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  12, "lr_in_nd_ra_response") \
> +    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  13, "lr_in_ip_routing_pre")  \
> +    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      14, "lr_in_ip_routing")      \
> +    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 15, "lr_in_ip_routing_ecmp") \
> +    PIPELINE_STAGE(ROUTER, IN,  POLICY,          16, "lr_in_policy")          \
> +    PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     17, "lr_in_policy_ecmp")     \
> +    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP_CHK, 18,                      \
> +                  "lr_in_dhcp_relay_resp_chk")                                \
> +    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP, 19,                          \
> +                  "lr_in_dhcp_relay_resp")                                    \
> +    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     20, "lr_in_arp_resolve")     \
> +    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     21, "lr_in_chk_pkt_len")     \
> +    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     22, "lr_in_larger_pkts")     \
> +    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     23, "lr_in_gw_redirect")     \
> +    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     24, "lr_in_arp_request")     \
>                                                                        \
>      /* Logical router egress stages. */                               \
>      PIPELINE_STAGE(ROUTER, OUT, CHECK_DNAT_LOCAL,   0,                       \
> diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
> index a9c5b7af5..a616dbd6b 100644
> --- a/ovn-nb.ovsschema
> +++ b/ovn-nb.ovsschema
> @@ -1,7 +1,7 @@
>  {
>      "name": "OVN_Northbound",
>      "version": "7.3.0",
> -    "cksum": "3546526738 34483",
> +    "cksum": "3858903371 35372",
>      "tables": {
>          "NB_Global": {
>              "columns": {
> @@ -436,6 +436,11 @@
>                  "ipv6_prefix": {"type": {"key": "string",
>                                        "min": 0,
>                                        "max": "unlimited"}},
> +                "dhcp_relay": {"type": {"key": {"type": "uuid",
> +                                            "refTable": "DHCP_Relay",
> +                                            "refType": "strong"},
> +                                            "min": 0,
> +                                            "max": 1}},
>                  "external_ids": {
>                      "type": {"key": "string", "value": "string",
>                               "min": 0, "max": "unlimited"}},
> @@ -534,6 +539,18 @@
>                      "type": {"key": "string", "value": "string",
>                               "min": 0, "max": "unlimited"}}},
>              "isRoot": true},
> +        "DHCP_Relay": {
> +            "columns": {
> +                "name": {"type": "string"},
> +                "servers": {"type": {"key": "string",
> +                                       "min": 0,
> +                                       "max": 1}},
> +                "options": {"type": {"key": "string", "value": "string",
> +                                     "min": 0, "max": "unlimited"}},
> +                "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 e0b983ed6..6f0210325 100644
> --- a/ovn-nb.xml
> +++ b/ovn-nb.xml
> @@ -703,6 +703,13 @@
>          </ul>
>        </column>
>
> +      <column name="other_config" key="dhcp_relay_port">
> +        If set to the name of logical switch port of type <code>router</code>
> +        then, DHCP Relay is enabled for this logical switch provided the
> +        corresponding <ref table="Logical_Router_Port"/> has DHCP Relay
> +        configured.
> +      </column>
> +
>        <column name="other_config" key="mac_only" type='{"type": "boolean"}'>
>          Value used to request to assign L2 address only if neither subnet
>          nor ipv6_prefix are specified
> @@ -3059,6 +3066,11 @@ or
>        port has all ingress and egress traffic dropped.
>      </column>
>
> +    <column name="dhcp_relay">
> +      This column is used to enabled DHCP Relay. Please refer
> +      to <ref table="DHCP_Relay"/> table.
> +    </column>
> +
>      <group title="Distributed Gateway Ports">
>        <p>
>          Gateways, as documented under <code>Gateways</code> in the OVN
> @@ -4372,6 +4384,33 @@ or
>      </group>
>    </table>
>
> +  <table name="DHCP_Relay" title="DHCP Relay">
> +    <p>
> +      OVN implements native DHCPv4 relay support which caters to the common
> +      use case of relaying the DHCP requests to external DHCP server.
> +    </p>
> +    <column name="name">
> +      <p>
> +        A name for the DHCP Relay.
> +      </p>
> +    </column>
> +    <column name="servers">
> +      <p>
> +        The DHCPv4 server IP address.
> +      </p>
> +    </column>
> +    <column name="options">
> +      <p>
> +        Future purpose.
> +      </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/atlocal.in b/tests/atlocal.in
> index 63d891b89..32d1c374e 100644
> --- a/tests/atlocal.in
> +++ b/tests/atlocal.in
> @@ -187,6 +187,9 @@ fi
>  # Set HAVE_DHCPD
>  find_command dhcpd
>
> +# Set HAVE_DHCLIENT
> +find_command dhclient
> +
>  # Set HAVE_BFDD_BEACON
>  find_command bfdd-beacon
>
> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
> index c189dcccc..c8d551f2d 100644
> --- a/tests/ovn-northd.at
> +++ b/tests/ovn-northd.at
> @@ -12138,6 +12138,44 @@ check_row_count nb:QoS 0
>  AT_CLEANUP
>  ])
>
> +OVN_FOR_EACH_NORTHD_NO_HV([
> +AT_SETUP([check DHCP RELAY])
> +ovn_start NORTHD_TYPE
> +
> +check ovn-nbctl ls-add ls0
> +check ovn-nbctl lsp-add ls0 ls0-port1
> +check ovn-nbctl lsp-set-addresses ls0-port1 02:00:00:00:00:10
> +check ovn-nbctl lr-add lr0
> +check ovn-nbctl lrp-add lr0 lrp1 02:00:00:00:00:01 192.168.1.1/24
> +check ovn-nbctl lsp-add ls0 lrp1-attachment
> +check ovn-nbctl lsp-set-type lrp1-attachment router
> +check ovn-nbctl lsp-set-addresses lrp1-attachment 00:00:00:00:ff:02
> +check ovn-nbctl lsp-set-options lrp1-attachment router-port=lrp1
> +check ovn-nbctl lrp-add lr0 lrp-ext 02:00:00:00:00:02 192.168.2.1/24
> +
> +dhcp_relay=$(ovn-nbctl create DHCP_Relay servers=172.16.1.1)
> +check ovn-nbctl set Logical_Router_port lrp1 dhcp_relay=$dhcp_relay
> +check ovn-nbctl set Logical_Switch ls0 other_config:dhcp_relay_port=lrp1-attachment
> +
> +check ovn-nbctl --wait=sb sync
> +
> +ovn-sbctl lflow-list > lflows
> +AT_CAPTURE_FILE([lflows])
> +
> +AT_CHECK([grep -e "DHCP_RELAY_" lflows | sed 's/table=../table=??/'], [0], [dnl
> +  table=??(lr_in_ip_input     ), priority=110  , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67), action=(reg9[[7]] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);next; /* DHCP_RELAY_REQ */)
> +  table=??(lr_in_ip_input     ), priority=110  , match=(ip4.src == 172.16.1.1 && ip4.dst == 192.168.1.1 && udp.src == 67 && udp.dst == 67), action=(next;/* DHCP_RELAY_RESP */)
> +  table=??(lr_in_dhcp_relay_req), priority=100  , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[[7]]), action=(ip4.src=192.168.1.1;ip4.dst=172.16.1.1;udp.src=67;next; /* DHCP_RELAY_REQ */)
> +  table=??(lr_in_dhcp_relay_req), priority=1    , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[[7]] == 0), action=(drop; /* DHCP_RELAY_REQ */)
> +  table=??(lr_in_dhcp_relay_resp_chk), priority=100  , match=(ip4.src == 172.16.1.1 && ip4.dst == 192.168.1.1 && udp.src == 67 && udp.dst == 67), action=(reg2 = ip4.dst;reg9[[8]] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);next;/* DHCP_RELAY_RESP */)
> +  table=??(lr_in_dhcp_relay_resp), priority=100  , match=(ip4.src == 172.16.1.1 && reg2 == 192.168.1.1 && udp.src == 67 && udp.dst == 67 && reg9[[8]]), action=(ip4.src=192.168.1.1;udp.dst=68;outport="lrp1";output; /* DHCP_RELAY_RESP */)
> +  table=??(lr_in_dhcp_relay_resp), priority=1    , match=(ip4.src == 172.16.1.1 && reg2 == 192.168.1.1 && udp.src == 67 && udp.dst == 67 && reg9[[8]] == 0), action=(drop; /* DHCP_RELAY_RESP */)
> +  table=??(ls_in_l2_lkup      ), priority=100  , match=(inport == "ls0-port1" && eth.src == 02:00:00:00:00:10 && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67), action=(eth.dst=02:00:00:00:00:01;outport="lrp1-attachment";next;/* DHCP_RELAY_REQ */)
> +])
> +
> +AT_CLEANUP
> +])
> +
>  AT_SETUP([NB_Global and SB_Global incremental processing])
>
>  ovn_start
> diff --git a/tests/ovn.at b/tests/ovn.at
> index 902dd3793..109e19550 100644
> --- a/tests/ovn.at
> +++ b/tests/ovn.at
> @@ -1661,6 +1661,40 @@ reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain_name=1.2.3.4);
>  reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain_search_list=1.2.3.4);
>      DHCPv4 option domain_search_list requires string value.
>
> +#dhcp_relay_req_chk
> +reg9[7] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);
> +    encodes as controller(userdata=00.00.00.1c.00.00.00.00.80.01.08.08.00.00.00.07.c0.a8.01.01.ac.10.01.01,pause)
> +
> +reg9[7] = dhcp_relay_req_chk(192.168.1.1,172.16.1.1);
> +    formats as reg9[7] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);
> +    encodes as controller(userdata=00.00.00.1c.00.00.00.00.80.01.08.08.00.00.00.07.c0.a8.01.01.ac.10.01.01,pause)
> +
> +reg9[7..8] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);
> +    Cannot use 2-bit field reg9[7..8] where 1-bit field is required.
> +
> +reg9[7] = dhcp_relay_req_chk("192.168.1.1", "172.16.1.1");
> +    Syntax error at `"192.168.1.1"' expecting IPv4 dhcp relay and server ips.
> +
> +reg9[7] = dhcp_relay_req_chk(192.168.1, 172.16.1.1);
> +    Invalid numeric constant.
> +
> +#dhcp_relay_resp_chk
> +reg9[8] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);
> +    encodes as controller(userdata=00.00.00.1d.00.00.00.00.80.01.08.08.00.00.00.08.c0.a8.01.01.ac.10.01.01,pause)
> +
> +reg9[8] = dhcp_relay_resp_chk(192.168.1.1,172.16.1.1);
> +    formats as reg9[8] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);
> +    encodes as controller(userdata=00.00.00.1d.00.00.00.00.80.01.08.08.00.00.00.08.c0.a8.01.01.ac.10.01.01,pause)
> +
> +reg9[7..8] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);
> +    Cannot use 2-bit field reg9[7..8] where 1-bit field is required.
> +
> +reg9[8] = dhcp_relay_resp_chk("192.168.1.1", "172.16.1.1");
> +    Syntax error at `"192.168.1.1"' expecting IPv4 dhcp relay and server ips.
> +
> +reg9[8] = dhcp_relay_resp_chk(192.168.1, 172.16.1.1);
> +    Invalid numeric constant.
> +
>  # nd_ns
>  nd_ns { nd.target = xxreg0; output; };
>      encodes as controller(userdata=00.00.00.09.00.00.00.00.00.1c.00.18.00.80.00.00.00.00.00.00.00.01.de.10.80.00.3e.10.00.00.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00)
> @@ -21996,7 +22030,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_UNQUOTED([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_UNQUOTED([as hv1 ovs-ofctl dump-flows br-int metadata=0x$lr0_dp_key | awk '/table=31, n_packets=1, n_bytes=45/{print $7" "$8}'],[0],[dnl
>  priority=80,ip,reg15=0x$lr0_public_dp_key,metadata=0x$lr0_dp_key,nw_src=10.0.0.10 actions=drop
>  ])
>
> @@ -28221,7 +28255,7 @@ ovn-sbctl dump-flows > sbflows
>  AT_CAPTURE_FILE([sbflows])
>  AT_CAPTURE_FILE([offlows])
>  OVS_WAIT_UNTIL([
> -    as hv1 ovs-ofctl dump-flows br-int table=23 > offlows
> +    as hv1 ovs-ofctl dump-flows br-int table=24 > offlows
>      test $(grep -c "load:0x64->NXM_NX_PKT_MARK" offlows) = 1 && \
>      test $(grep -c "load:0x3->NXM_NX_PKT_MARK" offlows) = 1 && \
>      test $(grep -c "load:0x4->NXM_NX_PKT_MARK" offlows) = 1 && \
> @@ -28319,12 +28353,12 @@ send_ipv4_pkt hv1 hv1-vif1 505400000003 00000000ff01 \
>      c3ad 83dc
>
>  OVS_WAIT_UNTIL([
> -    test 1 -eq $(as hv1 ovs-ofctl dump-flows br-int table=23 | \
> +    test 1 -eq $(as hv1 ovs-ofctl dump-flows br-int table=24 | \
>      grep "load:0x2->NXM_NX_PKT_MARK" -c)
>  ])
>
>  AT_CHECK([
> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=23 | \
> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=24 | \
>      grep "load:0x64->NXM_NX_PKT_MARK" -c)
>  ])
>
> @@ -29017,23 +29051,23 @@ check ovn-nbctl --wait=hv sync
>
>  # Ensure ECMP symmetric reply flows are not present on any hypervisor.
>  AT_CHECK([
> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=17 | \
> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=18 | \
>      grep "priority=100" | \
>      grep "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_LABEL\\[[80..95\\]]))" -c)
>  ])
>  AT_CHECK([
> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=25 | \
> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=26 | \
>      grep "priority=200" | \
>      grep "actions=move:NXM_NX_CT_LABEL\\[[32..79\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
>  ])
>
>  AT_CHECK([
> -    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=17 | \
> +    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=18 | \
>      grep "priority=100" | \
>      grep "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_LABEL\\[[80..95\\]]))" -c)
>  ])
>  AT_CHECK([
> -    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=25 | \
> +    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=26 | \
>      grep "priority=200" | \
>      grep "actions=move:NXM_NX_CT_LABEL\\[[32..79\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
>  ])
> @@ -29051,11 +29085,11 @@ AT_CAPTURE_FILE([hv2flows])
>
>  AT_CHECK([
>      for hv in 1 2; do
> -        grep table=17 hv${hv}flows | \
> +        grep table=18 hv${hv}flows | \
>          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=28 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
> @@ -29143,23 +29177,23 @@ check ovn-nbctl --wait=hv sync
>
>  # Ensure ECMP symmetric reply flows are not present on any hypervisor.
>  AT_CHECK([
> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=17 | \
> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=18 | \
>      grep "priority=100" | \
>      grep "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_LABEL\\[[80..95\\]]))" -c)
>  ])
>  AT_CHECK([
> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=25 | \
> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=28 | \
>      grep "priority=200" | \
>      grep "actions=move:NXM_NX_CT_LABEL\\[[32..79\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
>  ])
>
>  AT_CHECK([
> -    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=17 | \
> +    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=18 | \
>      grep "priority=100" | \
>      grep "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\\]]))" -c)
>  ])
>  AT_CHECK([
> -    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=25 | \
> +    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=28 | \
>      grep "priority=200" | \
>      grep "actions=move:NXM_NX_CT_LABEL\\[[\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
>  ])
> @@ -29176,11 +29210,11 @@ AT_CAPTURE_FILE([hv2flows])
>
>  AT_CHECK([
>      for hv in 1 2; do
> -        grep table=17 hv${hv}flows | \
> +        grep table=18 hv${hv}flows | \
>          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=28 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
> @@ -29677,7 +29711,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=28, n_packets=1,.* priority=$prio,ip,$inport.*$outport.*metadata=0x${sw_key},nw_dst=10.0.1.1 actions=drop" -c], [0], [dnl
>  1
>  ])
>
> @@ -29696,13 +29730,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=28, 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=28, 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])
> @@ -34645,7 +34679,7 @@ check ovn-nbctl set nb_global . options:use_common_zone="true"
>  check ovn-nbctl --wait=hv sync
>  # Use constants so that if tables or registers change, this test can
>  # be updated easily.
> -DNAT_TABLE=15
> +DNAT_TABLE=16
>  SNAT_TABLE=45
>  DNAT_ZONE_REG="NXM_NX_REG11[[0..15]]"
>  SNAT_ZONE_REG="NXM_NX_REG12[[0..15]]"
> @@ -38088,3 +38122,222 @@ OVS_WAIT_UNTIL([test 1 = $(as hv ovs-ofctl dump-flows br-int | grep -E "pkt_mark
>  OVN_CLEANUP([hv])
>  AT_CLEANUP
>  ])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([DHCP RELAY])
> +ovn_start
> +net_add n1
> +
> +AT_CHECK([ovn-nbctl ls-add ls0])
> +AT_CHECK([ovn-nbctl lsp-add ls0 vif0])
> +AT_CHECK([ovn-nbctl lsp-set-addresses vif0 "50:54:00:00:00:10"])
> +AT_CHECK([ovn-nbctl lsp-add ls0 lrp1-attachment])
> +AT_CHECK([ovn-nbctl lsp-set-type lrp1-attachment router])
> +AT_CHECK([ovn-nbctl lsp-set-addresses lrp1-attachment 50:54:00:00:00:01])
> +AT_CHECK([ovn-nbctl lsp-set-options lrp1-attachment router-port=lrp1])
> +
> +AT_CHECK([ovn-nbctl lr-add lr0])
> +AT_CHECK([ovn-nbctl lrp-add lr0 lrp1 50:54:00:00:00:01 192.168.1.1/24])
> +AT_CHECK([ovn-nbctl lrp-add lr0 lrp2 50:54:00:00:00:02 172.16.1.254/24])
> +
> +AT_CHECK([ovn-nbctl ls-add ls-ext])
> +AT_CHECK([ovn-nbctl lsp-add ls-ext lrp2-attachment])
> +AT_CHECK([ovn-nbctl lsp-set-type lrp2-attachment router])
> +AT_CHECK([ovn-nbctl lsp-set-addresses lrp2-attachment 50:54:00:00:00:02])
> +AT_CHECK([ovn-nbctl lsp-set-options lrp2-attachment router-port=lrp2])
> +AT_CHECK([ovn-nbctl lsp-add ls-ext ln_port])
> +AT_CHECK([ovn-nbctl lsp-set-addresses ln_port unknown])
> +AT_CHECK([ovn-nbctl lsp-set-type ln_port localnet])
> +AT_CHECK([ovn-nbctl lsp-set-options ln_port network_name=physnet1])
> +
> +dhcp_relay=$(ovn-nbctl create DHCP_Relay servers=172.16.1.1)
> +AT_CHECK([ovn-nbctl set Logical_Router_port lrp1 dhcp_relay=$dhcp_relay])
> +AT_CHECK([ovn-nbctl set Logical_Switch ls0 other_config:dhcp_relay_port=lrp1-attachment])
> +
> +sim_add hv1
> +as hv1
> +ovs-vsctl add-br br-phys
> +ovn_attach n1 br-phys 192.168.0.1
> +ovs-vsctl -- add-port br-int vif0 -- \
> +    set interface vif0 external-ids:iface-id=vif0 \
> +    options:tx_pcap=hv1/vif0-tx.pcap \
> +    options:rxq_pcap=hv1/vif0-rx.pcap \
> +    ofport-request=1
> +ovs-vsctl -- add-port br-phys ext0 -- \
> +    set interface ext0 \
> +    options:tx_pcap=hv1/ext0-tx.pcap \
> +    options:rxq_pcap=hv1/ext0-rx.pcap \
> +    ofport-request=2
> +
> +ovs-vsctl set open . external_ids:ovn-bridge-mappings=physnet1:br-phys
> +
> +wait_for_ports_up
> +AT_CHECK([ovn-nbctl --wait=hv sync])
> +
> +send_dhcp_packet() {
> +    src_mac=${1}
> +    src_ip=${2}
> +    dst_mac=${3}
> +    dst_ip=${4}
> +    op_code=${5}
> +    msg_type=${6}
> +    yiaddr=$7
> +    giaddr=${8}
> +    sid=${9}
> +    iface=${10}
> +    #echo "ARGS: ${1} ${2} ${3} ${4} ${5} ${6} ${7} ${8} ${9} ${10}"
> +    echo "ARGS: $@"
> +    if [[ $op_code == "01" ]]; then
> +        ip_len=0111
> +        udp_len=00fd
> +        src_port=0044
> +    else
> +        ip_len=011d
> +        udp_len=0109
> +        src_port=0043
> +    fi
> +    flags=0000
> +
> +    local pkt=${dst_mac}${src_mac}08004510${ip_len}0000000080110000${src_ip}${dst_ip}
> +    # udp header and dhcp header
> +    pkt=${pkt}${src_port}0043${udp_len}0000
> +    pkt=${pkt}${op_code}0106006359aa760000${flags}00000000${yiaddr}00000000${giaddr}${src_mac}
> +    # client hardware padding
> +    pkt=${pkt}00000000000000000000
> +    # server hostname
> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
> +    # boot file name
> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
> +    # dhcp magic cookie
> +    pkt=${pkt}63825363
> +    # dhcp message type
> +    pkt=${pkt}3501${msg_type}
> +    # dhcp server identifier and subnet mask options
> +    if  [[ $op_code == "02" ]]; then
> +        pkt=${pkt}3604${sid}
> +        pkt=${pkt}0104ffffff00
> +    fi
> +    # dhcp pad option
> +    pkt=${pkt}00
> +    # dhcp end option
> +    pkt=${pkt}ff
> +
> +    tcpdump_hex "-- sending DHCP pkt on hv1-$iface" $pkt
> +
> +    ovs-appctl netdev-dummy/receive $iface $pkt
> +}
> +
> +
> +# Check that there is commit_fdb_local_fdb() flow added by ovn-northd for vif0 and localnet
> +ovn-sbctl dump-flows > lflows
> +AT_CAPTURE_FILE([lflows])
> +
> +# ====================================================
> +# Send DHCP valid discovery
> +src_mac="505400000010"
> +src_ip=`ip_to_hex 0.0.0.0`
> +dst_mac="ffffffffffff"
> +dst_ip=`ip_to_hex 255.255.255.255`
> +yiaddr=`ip_to_hex 0.0.0.0`
> +giaddr=`ip_to_hex 0.0.0.0`
> +sid=$src_ip
> +# send packet
> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 01 01 $yiaddr $giaddr $sid vif0
> +
> +ovs-ofctl dump-flows br-int table=12 > pflows1_dhcp_relay_req
> +AT_CAPTURE_FILE([pflows1_dhcp_relay_req])
> +
> +AT_CHECK([cat pflows1_dhcp_relay_req | grep -v NXST | grep 255.255.255.255 | grep resubmit |
> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
> +n_packets=1
> +])
> +
> +# ====================================================
> +# Send DHCP discovery with giaddr set
> +giaddr=`ip_to_hex 192.168.1.1`
> +# send packet
> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 01 01 $yiaddr $giaddr $sid vif0
> +
> +ovs-ofctl dump-flows br-int table=12 > pflows2_dhcp_relay_req
> +AT_CAPTURE_FILE([pflows2_dhcp_relay_req])
> +
> +AT_CHECK([cat pflows2_dhcp_relay_req | grep -v NXST | grep 255.255.255.255 | grep drop |
> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
> +n_packets=1
> +])
> +
> +# ====================================================
> +# Send DHCP valid offer
> +src_mac="50540000001f"
> +src_ip=`ip_to_hex 172.16.1.1`
> +dst_mac="505400000002"
> +dst_ip=`ip_to_hex 192.168.1.1`
> +yiaddr=`ip_to_hex 192.168.1.10`
> +giaddr=`ip_to_hex 192.168.1.1`
> +sid=$src_ip
> +# send packet
> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
> +
> +ovs-ofctl dump-flows br-int table=27 > pflows1_dhcp_relay_resp
> +AT_CAPTURE_FILE([pflows1_dhcp_relay_resp])
> +
> +AT_CHECK([cat pflows1_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep resubmit |
> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
> +n_packets=1
> +])
> +# ====================================================
> +# Send DHCP offer with incorrect giaddr
> +giaddr=`ip_to_hex 192.168.1.10`
> +# send packet
> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
> +
> +ovs-ofctl dump-flows br-int table=27 > pflows2_dhcp_relay_resp
> +AT_CAPTURE_FILE([pflows2_dhcp_relay_resp])
> +
> +AT_CHECK([cat pflows2_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep drop |
> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
> +n_packets=1
> +])
> +
> +giaddr=`ip_to_hex 192.168.1.1`
> +
> +# ====================================================
> +# Send DHCP offer with yiaddr outside of the subnet
> +yiaddr=`ip_to_hex 192.168.2.10`
> +# send packet
> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
> +
> +ovs-ofctl dump-flows br-int table=27 > pflows2_dhcp_relay_resp
> +AT_CAPTURE_FILE([pflows2_dhcp_relay_resp])
> +
> +AT_CHECK([cat pflows2_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep drop |
> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
> +n_packets=2
> +])
> +
> +yiaddr=`ip_to_hex 192.168.1.10`
> +
> +# ====================================================
> +# Send DHCP offer with differnt server identifier
> +sid=`ip_to_hex 172.16.1.100`
> +# send packet
> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
> +
> +ovs-ofctl dump-flows br-int table=27 > pflows2_dhcp_relay_resp
> +AT_CAPTURE_FILE([pflows2_dhcp_relay_resp])
> +
> +AT_CHECK([cat pflows2_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep drop |
> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
> +n_packets=3
> +])
> +
> +sid=`ip_to_hex 172.16.1.1`
> +
> +OVN_CLEANUP([hv1])
> +AT_CLEANUP
> +])
> +
> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
> index c22c7882f..e7fa79727 100644
> --- a/tests/system-ovn.at
> +++ b/tests/system-ovn.at
> @@ -12184,3 +12184,151 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
>  /connection dropped.*/d"])
>  AT_CLEANUP
>  ])
> +
> +OVN_FOR_EACH_NORTHD([
> +AT_SETUP([DHCP RELAY])
> +AT_SKIP_IF([test $HAVE_DHCPD = no])
> +AT_SKIP_IF([test $HAVE_DHCLIENT = no])
> +AT_SKIP_IF([test $HAVE_TCPDUMP = no])
> +ovn_start
> +OVS_TRAFFIC_VSWITCHD_START()
> +
> +ADD_BR([br-int])
> +ADD_BR([br-ext])
> +
> +ovs-ofctl add-flow br-ext action=normal
> +# Set external-ids in br-int needed for ovn-controller
> +ovs-vsctl \
> +        -- set Open_vSwitch . external-ids:system-id=hv1 \
> +        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
> +        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
> +        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
> +        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
> +
> +# Start ovn-controller
> +start_daemon ovn-controller
> +
> +ADD_NAMESPACES(sw01)
> +ADD_VETH(sw01, sw01, br-int, "0", "f0:00:00:01:02:03")
> +ADD_NAMESPACES(sw11)
> +ADD_VETH(sw11, sw11, br-int, "0", "f0:00:00:02:02:03")
> +ADD_NAMESPACES(server)
> +ADD_VETH(s1, server, br-ext, "172.16.1.1/24", "f0:00:00:01:02:05", \
> +         "172.16.1.254")
> +
> +check ovn-nbctl lr-add R1
> +
> +check ovn-nbctl ls-add sw0
> +check ovn-nbctl ls-add sw1
> +check ovn-nbctl ls-add sw-ext
> +
> +check ovn-nbctl lrp-add R1 rp-sw0 00:00:01:01:02:03 192.168.1.1/24
> +check ovn-nbctl lrp-add R1 rp-sw1 00:00:03:01:02:03 192.168.2.1/24
> +check ovn-nbctl lrp-add R1 rp-ext 00:00:02:01:02:03 172.16.1.254/24
> +
> +dhcp_relay=$(ovn-nbctl create DHCP_Relay servers=172.16.1.1)
> +check ovn-nbctl set Logical_Router_port rp-sw0 dhcp_relay=$dhcp_relay
> +check ovn-nbctl set Logical_Router_port rp-sw1 dhcp_relay=$dhcp_relay
> +check ovn-nbctl lrp-set-gateway-chassis rp-ext hv1
> +
> +check ovn-nbctl lsp-add sw0 sw0-rp -- set Logical_Switch_Port sw0-rp \
> +    type=router options:router-port=rp-sw0 \
> +    -- lsp-set-addresses sw0-rp router
> +check ovn-nbctl lsp-add sw1 sw1-rp -- set Logical_Switch_Port sw1-rp \
> +    type=router options:router-port=rp-sw1 \
> +    -- lsp-set-addresses sw1-rp router
> +
> +check ovn-nbctl set Logical_Switch sw0 other_config:dhcp_relay_port=sw0-rp
> +check ovn-nbctl set Logical_Switch sw1 other_config:dhcp_relay_port=sw1-rp
> +
> +check ovn-nbctl lsp-add sw-ext ext-rp -- set Logical_Switch_Port ext-rp \
> +    type=router options:router-port=rp-ext \
> +    -- lsp-set-addresses ext-rp router
> +check ovn-nbctl lsp-add sw-ext lnet \
> +        -- lsp-set-addresses lnet unknown \
> +        -- lsp-set-type lnet localnet \
> +        -- lsp-set-options lnet network_name=phynet
> +
> +check ovn-nbctl lsp-add sw0 sw01 \
> +    -- lsp-set-addresses sw01 "f0:00:00:01:02:03"
> +
> +check ovn-nbctl lsp-add sw1 sw11 \
> +    -- lsp-set-addresses sw11 "f0:00:00:02:02:03"
> +
> +AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext])
> +
> +OVN_POPULATE_ARP
> +
> +check ovn-nbctl --wait=hv sync
> +
> +DHCP_TEST_DIR="/tmp/dhcp-test"
> +rm -rf $DHCP_TEST_DIR
> +mkdir $DHCP_TEST_DIR
> +cat > $DHCP_TEST_DIR/dhcpd.conf <<EOF
> +subnet 172.16.1.0 netmask 255.255.255.0 {
> +}
> +subnet 192.168.1.0 netmask 255.255.255.0 {
> +  range 192.168.1.10 192.168.1.10;
> +  option routers 192.168.1.1;
> +  option broadcast-address 192.168.1.255;
> +  default-lease-time 60;
> +  max-lease-time 120;
> +}
> +subnet 192.168.2.0 netmask 255.255.255.0 {
> +  range 192.168.2.10 192.168.2.10;
> +  option routers 192.168.2.1;
> +  option broadcast-address 192.168.2.255;
> +  default-lease-time 60;
> +  max-lease-time 120;
> +}
> +EOF
> +cat > $DHCP_TEST_DIR/dhclien.conf <<EOF
> +timeout 2
> +EOF
> +
> +touch $DHCP_TEST_DIR/dhcpd.leases
> +chown root:dhcpd $DHCP_TEST_DIR $DHCP_TEST_DIR/dhcpd.leases
> +chmod 775 $DHCP_TEST_DIR
> +chmod 664 $DHCP_TEST_DIR/dhcpd.leases
> +
> +
> +NETNS_DAEMONIZE([server], [dhcpd -4 -f -cf $DHCP_TEST_DIR/dhcpd.conf s1 > dhcpd.log 2>&1], [dhcpd.pid])
> +
> +NS_CHECK_EXEC([server], [tcpdump -l -nvv -i s1  udp > pkt.pcap 2>tcpdump_err &])
> +OVS_WAIT_UNTIL([grep "listening" tcpdump_err])
> +on_exit 'kill $(pidof tcpdump)'
> +
> +NS_CHECK_EXEC([sw01], [dhclient -1 -q -lf $DHCP_TEST_DIR/dhclient-sw01.lease -pf $DHCP_TEST_DIR/dhclient-sw01.pid -cf $DHCP_TEST_DIR/dhclien.conf sw01])
> +NS_CHECK_EXEC([sw11], [dhclient -1 -q -lf $DHCP_TEST_DIR/dhclient-sw11.lease -pf $DHCP_TEST_DIR/dhclient-sw11.pid -cf $DHCP_TEST_DIR/dhclien.conf sw11])
> +
> +OVS_WAIT_UNTIL([
> +    total_pkts=$(cat pkt.pcap | wc -l)
> +    test ${total_pkts} -ge 8
> +])
> +
> +on_exit 'kill `cat $DHCP_TEST_DIR/dhclient-sw01.pid` &&
> +kill `cat $DHCP_TEST_DIR/dhclient-sw11.pid` && rm -rf $DHCP_TEST_DIR'
> +
> +NS_CHECK_EXEC([sw01], [ip addr show sw01 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'], [0], [dnl
> +192.168.1.10
> +])
> +NS_CHECK_EXEC([sw11], [ip addr show sw11 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'], [0], [dnl
> +192.168.2.10
> +])
> +OVS_APP_EXIT_AND_WAIT([ovn-controller])
> +
> +as ovn-sb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as ovn-nb
> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
> +
> +as northd
> +OVS_APP_EXIT_AND_WAIT([ovn-northd])
> +
> +as
> +OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
> +/failed to query port patch-.*/d
> +/.*terminating with signal 15.*/d"])
> +AT_CLEANUP
> +])
> diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
> index e0f1c3ec9..903457d45 100644
> --- a/utilities/ovn-trace.c
> +++ b/utilities/ovn-trace.c
> @@ -2329,6 +2329,63 @@ execute_put_dhcp_opts(const struct ovnact_put_opts *pdo,
>      execute_put_opts(pdo, name, uflow, super);
>  }
>
> +static void
> +execute_dhcpv4_relay_req_chk(const struct ovnact_dhcp_relay *dr,
> +                             struct flow *uflow, struct ovs_list *super)
> +{
> +    ovntrace_node_append(
> +        super, OVNTRACE_NODE_ERROR,
> +        "/* We assume that this packet is DHCPDISCOVER or DHCPREQUEST. */");
> +
> +    struct ds s = DS_EMPTY_INITIALIZER;
> +    struct mf_subfield dst = expr_resolve_field(&dr->dst);
> +    if (!mf_is_register(dst.field->id)) {
> +        /* Format assignment. */
> +        ds_clear(&s);
> +        expr_field_format(&dr->dst, &s);
> +        ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
> +                             "%s = 1", ds_cstr(&s));
> +    }
> +    ds_destroy(&s);
> +
> +    struct mf_subfield sf = expr_resolve_field(&dr->dst);
> +    union mf_subvalue sv = { .u8_val = 1 };
> +    mf_write_subfield_flow(&sf, &sv, uflow);
> +}
> +
> +static void
> +execute_dhcpv4_relay_resp_chk(const struct ovnact_dhcp_relay *dr,
> +                              struct flow *uflow, struct ovs_list *super)
> +{
> +    ovntrace_node_append(
> +        super, OVNTRACE_NODE_ERROR,
> +        "/* We assume that this packet is DHCPOFFER or DHCPACK and "
> +            "DHCP broadcast flag is set. Dest IP is set to broadcast. "
> +            "Dest MAC is set to broadcast but in real network this is unicast "
> +            "which is extracted from DHCP header. */");
> +
> +    /* Assume DHCP broadcast flag is set */
> +    uflow->nw_dst = 0xFFFFFFFF;
> +    /* Dest MAC is set to broadcast but in real network this is unicast */
> +    struct eth_addr bcast_mac = { .ea = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
> +    uflow->dl_dst = bcast_mac;
> +
> +    struct ds s = DS_EMPTY_INITIALIZER;
> +    struct mf_subfield dst = expr_resolve_field(&dr->dst);
> +    if (!mf_is_register(dst.field->id)) {
> +        /* Format assignment. */
> +        ds_clear(&s);
> +        expr_field_format(&dr->dst, &s);
> +        ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
> +                             "%s = 1", ds_cstr(&s));
> +    }
> +    ds_destroy(&s);
> +
> +    struct mf_subfield sf = expr_resolve_field(&dr->dst);
> +    union mf_subvalue sv = { .u8_val = 1 };
> +    mf_write_subfield_flow(&sf, &sv, uflow);
> +}
> +
>  static void
>  execute_put_nd_ra_opts(const struct ovnact_put_opts *pdo,
>                         const char *name, struct flow *uflow,
> @@ -3216,6 +3273,16 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
>                                    "put_dhcpv6_opts", uflow, super);
>              break;
>
> +        case OVNACT_DHCPV4_RELAY_REQ_CHK:
> +            execute_dhcpv4_relay_req_chk(ovnact_get_DHCPV4_RELAY_REQ_CHK(a),
> +                                    uflow, super);
> +            break;
> +
> +        case OVNACT_DHCPV4_RELAY_RESP_CHK:
> +            execute_dhcpv4_relay_resp_chk(ovnact_get_DHCPV4_RELAY_RESP_CHK(a),
> +                                    uflow, super);
> +            break;
> +
>          case OVNACT_PUT_ND_RA_OPTS:
>              execute_put_nd_ra_opts(ovnact_get_PUT_DHCPV6_OPTS(a),
>                                     "put_nd_ra_opts", uflow, super);
> --
> 2.36.6
>
> _______________________________________________
> dev mailing list
> dev@openvswitch.org
> https://mail.openvswitch.org/mailman/listinfo/ovs-dev
>
Naveen Yerramneni March 20, 2024, 4:47 p.m. UTC | #3
> On 14-Mar-2024, at 7:51 PM, Numan Siddique <numans@ovn.org> wrote:
> 
> On Sun, Mar 3, 2024 at 11:20 PM Naveen Yerramneni
> <naveen.yerramneni@nutanix.com> wrote:
>> 
>>    This patch contains changes to enable DHCP Relay Agent support for overlay subnets.
>> 
>>    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 relaying 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 rleay functionality in OVN.
>>      1. DHCP client originates DHCP discovery (broadcast).
>>      2. DHCP relay (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 increments hop count.
>>      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 relay agent forwards the offer to the client.
>>      5. DHCP client sends DHCP request (broadcast) packet.
>>      6. DHCP relay (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.
>>      7. DHCP Server sends the ACK packet.
>>      8. DHCP relay agent forwards the ACK packet to the client.
>>      9. All the future renew/release packets are directly exchanged between DHCP client and DHCP server.
>> 
>>    OVN DHCP RELAY PACKET FLOW:
>>    ----------------------------
>>    To add DHCP Relay support on OVN, we need to replicate all the behavior described above using distributed logical switch and logical router.
>>    At, 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 where VM is deployed and relays the packet to DHCP server.
>>      2. Response packet is first processed on RC node (which first recieves the packet from underlay network). RC node forwards the packet to the right node by filling in the dest MAC and IP.
>> 
>>    OVN Packet flow with DHCP relay is explained below.
>>      1. DHCP client (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(GIADDR, HOP count) in the DHCP payload after doing the required checks. If any check fails, packet is dropped.
>>      5. Logical Router converts the packet to L3 unicast and forwards it to the server. This packets 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 does sanity checks and  updates the destination MAC (available in DHCP header), destination IP (available in DHCP header) and reinjects the packet to datapath.
>>         If any check fails, packet is dropped.
>>      9. Logical router updates the source IP and port and forwards the packet to logical switch.
>>      10. Logical switch delivers the packet to the DHCP client.
>>      11. Similar steps are performed for Request and Ack packets.
>>      12. All the future renew/release packets are directly exchanged between DHCP client and DHCP server
>> 
>>    NEW OVN ACTIONS
>>    ---------------
>>      1. dhcp_relay_req_chk(<relay-ip>, <server-ip>)
>>          - This action executes on the source node on which the DHCP request originated.
>>          - This action relays the DHCP request coming from client to the server. Relay-ip is used to update GIADDR in the DHCP header.
>>      2. dhcp_relay_resp_chk(<relay-ip>, <server-ip>)
>>          - 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.
>>          - Relay-ip, server-ip are used to validate GIADDR and SERVER ID in the DHCP payload.
>> 
>>    FLOWS
>>    -----
>>    Following are the flows added when DHCP Relay is configured on one overlay subnet, one additonal flow is added in ls_in_l2_lkup table for each VM part of the 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>;next;/* DHCP_RELAY_REQ */)
>>      2. table=3 (lr_in_ip_input     ), priority=110  , match=(inport == <lrp> && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67),
>>         action=(reg9[7] = dhcp_relay_req_chk(<lrp_ip>, <dhcp_server_ip>);next; /* DHCP_RELAY_REQ */)
>>      3. table=3 (lr_in_ip_input     ), priority=110  , match=(ip4.src == <dhcp_server> && ip4.dst == <lrp> && udp.src == 67 && udp.dst == 67), action=(next;/* DHCP_RELAY_RESP */)
>>      4. table=4 (lr_in_dhcp_relay_req), priority=100  , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[7]),
>>         action=(ip4.src=<lrp>;ip4.dst=<dhcp_server>;udp.src=67;next; /* DHCP_RELAY_REQ */)
>>      5. table=4 (lr_in_dhcp_relay_req), priority=1    , match=(inport == <lrp> && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[7] == 0),
>>         action=(drop; /* DHCP_RELAY_REQ */)
>>      6. table=18(lr_in_dhcp_relay_resp_chk), priority=100  , match=(ip4.src == <dhcp_server> && ip4.dst == <lrp> && udp.src == 67 && udp.dst == 67),
>>         action=(reg2 = ip4.dst;reg9[8] = dhcp_relay_resp_chk(<lrp_ip>, <dhcp_server_ip>);next;/* DHCP_RELAY_RESP */)
>>      7. table=19(lr_in_dhcp_relay_resp), priority=100  , match=(ip4.src == <dhcp_server> && reg2 == <lrp_ip> && udp.src == 67 && udp.dst == 67 && reg9[8]),
>>         action=(ip4.src=<lrp>;udp.dst=68;outport=<lrp>;output; /* DHCP_RELAY_RESP */)
>>      8. table=19(lr_in_dhcp_relay_resp), priority=1    , match=(ip4.src == <dhcp_server> && reg2 == <lrp_ip> && udp.src == 67 && udp.dst == 67 && reg9[8] == 0), action=(drop; /* DHCP_RELAY_RESP */)
>> 
>>    NEW PIPELINE STAGES
>>    -------------------
>>    Following stage is added for DHCP relay feature. Some of the flows are fitted into the existing pipeline tages.
>>      1. lr_in_dhcp_relay_req
>>          - This stage process the DHCP request packets coming from DHCP clients.
>>          - DHCP request packets for which dhcp_relay_req_chk action (which gets applied in ip input stage) is successful are forwarded to DHCP server.
>>          - DHCP request packets for which dhcp_relay_req_chk action is unsuccessful gets dropped.
>>      2. lr_in_dhcp_relay_resp_chk
>>          - This stage applied the dhcp_relay_resp_chk action for  DHCP response packets coming from the DHCP server.
>>      3. lr_in_dhcp_relay_resp
>>          - DHCP response packets for which dhcp_relay_resp_chk is sucessful are forwarded to the DHCP clients.
>>          - DHCP response packets for which dhcp_relay_resp_chk is unsucessful gets dropped.
>> 
>>    REGISTRY USAGE
>>    ---------------
>>      - reg9[7] : To store the result of dhcp_relay_req_chk action.
>>      - reg9[8] : To store the result of dhcp_relay_resp_chk action.
>>      - reg2 : To store the original dest ip for DHCP response packets.
>>               This is required to properly match the packets in
>>               lr_in_dhcp_relay_resp stage since dhcp_relay_resp_chk action
>>               changes the dest ip.
>> 
>>    NB SCHEMA CHANGES
>>    ----------------
>>      1. New DHCP_Relay table
>>          "DHCP_Relay": {
>>                "columns": {
>>                    "name": {"type": "string"},
>>                    "servers": {"type": {"key": "string",
>>                                           "min": 0,
>>                                           "max": 1}},
>>                    "external_ids": {
>>                        "type": {"key": "string", "value": "string",
>>                                "min": 0, "max": "unlimited"}}},
>>                    "options": {"type": {"key": "string", "value": "string",
>>                                "min": 0, "max": "unlimited"}},
>>                "isRoot": true},
>>      2. New column to Logical_Router_Port table
>>          "dhcp_relay": {"type": {"key": {"type": "uuid",
>>                                "refTable": "DHCP_Relay",
>>                                "refType": "strong"},
>>                                "min": 0,
>>                                "max": 1}},
>> 
>>    Commands to enable the feature:
>>    ------------------------------
>>     ovn-nbctl create DHCP_Relay name=<name> servers=<dhcp_server_ip>
>>     ovn-nbctl set Logical_Router_port <lrp> dhcp_relay=<relay_uuid>
>>     ovn-nbctl set Logical_Switch <ls> other_config:dhcp_relay_port=<router_patch_port>
>> 
>>    Example:
>>    -------
>>     ovn-nbctl ls-add ls0
>>     ovn-nbctl lsp-add ls0 vif0
>>     ovn-nbctl lsp-set-addresses vif0 <MAC> #Only MAC address has to be specified when logical ports are created.
>>     ovn-nbctl lsp-add ls0 lrp1-attachment
>>     ovn-nbctl lsp-set-type lrp1-attachment router
>>     ovn-nbctl lsp-set-addresses lrp1-attachment
>>     ovn-nbctl lsp-set-options lrp1-attachment router-port=lrp1
>>     ovn-nbctl lr-add lr0
>>     ovn-nbctl lrp-add lr0 lrp1 <MAC> <GATEWAY_IP/Prefix> #GATEWAY IP is set in GIADDR field when relaying the DHCP requests to server.
>>     ovn-nbctl lrp-add lr0 lrp-ext <MAC> <GATEWAY_IP/Prefix>
>>     ovn-nbctl ls-add ls-ext
>>     ovn-nbctl lsp-add ls-ext lrp-ext-attachment
>>     ovn-nbctl lsp-set-type lrp-ext-attachment router
>>     ovn-nbctl lsp-set-addresses lrp-ext-attachment
>>     ovn-nbctl lsp-set-options lrp-ext-attachment router-port=lrp-ext
>>     ovn-nbctl lsp-add ls-ext ln_port
>>     ovn-nbctl lsp-set-addresses ln_port unknown
>>     ovn-nbctl lsp-set-type ln_port localnet
>>     ovn-nbctl lsp-set-options ln_port network_name=physnet1
>>     # Enable DHCP Relay feature
>>     ovn-nbctl create DHCP_Relay name=dhcp_relay_test servers=<dhcp_server_ip>
>>     ovn-nbctl set Logical_Router_port lrp1 dhcp_relay=<relay_uuid>
>>     ovn-nbctl set Logical_Switch ls0 other_config:dhcp_relay_port=lrp1-attachment
>> 
>>    Limitations:
>>    ------------
>>      - All OVN features that needs IP address to be configured on logical port (like proxy arp, etc) will not be supported for overlay subnets on which DHCP relay is enabled.
>> 
>>    References:
>>    ----------
>>      - rfc1541, rfc1542, rfc2131
>> 
>> Signed-off-by: Naveen Yerramneni <naveen.yerramneni@nutanix.com>
>> Co-authored-by: Huzaifa Calcuttawala <huzaifa.c@nutanix.com>
>> Signed-off-by: Huzaifa Calcuttawala <huzaifa.c@nutanix.com>
>> CC: Mary Manohar <mary.manohar@nutanix.com>
>> ---
>> V2:
>>  Addressed review comments from Numan.
> 
> Thanks for the v2.  This patch is failing compilation with gcc and
> address sanitizer configured (./configure --enable-Werror
> --enable-sparse 'CFLAGS=-g -fsanitize=address')
> 
> ---------------------------
> 
> ../utilities/ovn-trace.c:2368:19: error: incorrect type in assignment
> (different base types)
> ../utilities/ovn-trace.c:2368:19:    expected restricted ovs_be32
> [usertype] nw_dst
> ../utilities/ovn-trace.c:2368:19:    got unsigned int
> depbase=`echo utilities/ovn-appctl.o | sed 's|[^/]*$|.deps/&|;s|\.o$||'`;\
> 
> --
> 
> ../controller/pinctrl.c:2174:14: error: incorrect type in argument 2
> (different base types)
> ../controller/pinctrl.c:2174:14:    expected restricted ovs_be16
> [usertype] old_u16
> ../controller/pinctrl.c:2174:14:    got unsigned short [usertype]
> ../controller/pinctrl.c:2174:38: error: incorrect type in argument 3
> (different base types)
> ../controller/pinctrl.c:2174:38:    expected restricted ovs_be16
> [usertype] new_u16
> ../controller/pinctrl.c:2174:38:    got unsigned short [usertype]
> make[1]: *** [Makefile:2371: controller/pinctrl.o] Error 1
> 
> --------------------------------
> 
> 
> I'd suggest split this patch into a 3 patch series.
> 
> In patch 1 - pinctrl.c changes for the action opcodes
> ACTION_OPCODE_DHCP_RELAY_REQ_CHK and ACTION_OPCODE_DHCP_RELAY_RESP_CHK
> In patch 2 - Add the OVN logical actions - dhcp_relay_req_chk and
> dhcp_relay_resp_chk and the corresponding tests.
> and finally northd changes and test cases in patch 3.
> 
> There are indentation issues with the patch.  Please correct me as per
> the present code/coding guidelines.
> 
> Please see below for a few comments.

Hi Numan,

Thanks for the review.
I split the patch into series and addressed the test failures. Please review v5.

Thanks,
Naveen

> 
> Thanks
> Numan
> 
> 
>> ---
>> controller/pinctrl.c  | 596 +++++++++++++++++++++++++++++++++++++-----
>> include/ovn/actions.h |  27 ++
>> lib/actions.c         | 149 +++++++++++
>> lib/ovn-l7.h          |   2 +
>> northd/northd.c       | 265 ++++++++++++++++++-
>> northd/northd.h       |  41 +--
>> ovn-nb.ovsschema      |  19 +-
>> ovn-nb.xml            |  39 +++
>> tests/atlocal.in      |   3 +
>> tests/ovn-northd.at   |  38 +++
>> tests/ovn.at          | 293 +++++++++++++++++++--
>> tests/system-ovn.at   | 148 +++++++++++
>> utilities/ovn-trace.c |  67 +++++
>> 13 files changed, 1576 insertions(+), 111 deletions(-)
>> 
>> diff --git a/controller/pinctrl.c b/controller/pinctrl.c
>> index 98b29de9f..a776ac7c5 100644
>> --- a/controller/pinctrl.c
>> +++ b/controller/pinctrl.c
>> @@ -1909,6 +1909,514 @@ is_dhcp_flags_broadcast(ovs_be16 flags)
>>     return flags & htons(DHCP_BROADCAST_FLAG);
>> }
>> 
>> +static const char *dhcp_msg_str[] = {
>> +[0] = "INVALID",
>> +[DHCP_MSG_DISCOVER] = "DISCOVER",
>> +[DHCP_MSG_OFFER] = "OFFER",
>> +[DHCP_MSG_REQUEST] = "REQUEST",
>> +[OVN_DHCP_MSG_DECLINE] = "DECLINE",
>> +[DHCP_MSG_ACK] = "ACK",
>> +[DHCP_MSG_NAK] = "NAK",
>> +[OVN_DHCP_MSG_RELEASE] = "RELEASE",
>> +[OVN_DHCP_MSG_INFORM] = "INFORM"
>> +};
>> +
>> +static bool
>> +dhcp_relay_is_msg_type_supported(uint8_t msg_type)
>> +{
>> +    return (msg_type >= DHCP_MSG_DISCOVER && msg_type <= OVN_DHCP_MSG_RELEASE);
>> +}
>> +
>> +static const char *dhcp_msg_str_get(uint8_t msg_type)
>> +{
>> +    if (!dhcp_relay_is_msg_type_supported(msg_type)) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "Unknown DHCP msg type: %u", msg_type);
>> +        return "UNKNOWN";
>> +    }
>> +    return dhcp_msg_str[msg_type];
>> +}
>> +
>> +static const struct dhcp_header *
>> +dhcp_get_hdr_from_pkt(struct dp_packet *pkt_in, const char **in_dhcp_pptr,
>> +                  const char *end)
>> +{
>> +    /* Validate the DHCP request packet.
>> +     * Format of the DHCP packet is
>> +     * ------------------------------------------------------------------------
>> +     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var len)|
>> +     * ------------------------------------------------------------------------
>> +     */
>> +
>> +    *in_dhcp_pptr = dp_packet_get_udp_payload(pkt_in);
>> +    if (*in_dhcp_pptr == NULL) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP: Invalid or incomplete DHCP packet received");
>> +        return NULL;
>> +    }
>> +
>> +    const struct dhcp_header *dhcp_hdr
>> +        = (const struct dhcp_header *) *in_dhcp_pptr;
>> +    (*in_dhcp_pptr) += sizeof *dhcp_hdr;
>> +    if (*in_dhcp_pptr > end) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP: Invalid or incomplete DHCP packet received, "
>> +                     "bad data length");
>> +        return NULL;
>> +    }
>> +
>> +    if (dhcp_hdr->htype != 0x1) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP: Packet is recieved with "
>> +                "unsupported hardware type");
>> +        return NULL;
>> +    }
>> +
>> +    if (dhcp_hdr->hlen != 0x6) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP: Packet is recieved with "
>> +                "unsupported hardware length");
>> +        return NULL;
>> +    }
>> +
>> +    /* 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_pptr) + sizeof magic_cookie > end ||
>> +        get_unaligned_be32((const void *) (*in_dhcp_pptr)) != magic_cookie) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP: Magic cookie not present in the DHCP packet");
>> +        return NULL;
>> +    }
>> +
>> +    (*in_dhcp_pptr) += sizeof magic_cookie;
>> +
>> +    return dhcp_hdr;
>> +}
>> +
>> +static void
>> +dhcp_parse_options(const char **in_dhcp_pptr, const char *end,
>> +          const uint8_t **dhcp_msg_type_pptr, ovs_be32 *request_ip_ptr,
>> +          bool *ipxe_req_ptr, ovs_be32 *server_id_ptr,
>> +          ovs_be32 *netmask_ptr, ovs_be32 *router_ip_ptr)
>> +{
>> +    while ((*in_dhcp_pptr) < end) {
>> +        const struct dhcp_opt_header *in_dhcp_opt =
>> +            (const struct dhcp_opt_header *) *in_dhcp_pptr;
>> +        if (in_dhcp_opt->code == DHCP_OPT_END) {
>> +            break;
>> +        }
>> +        if (in_dhcp_opt->code == DHCP_OPT_PAD) {
>> +            (*in_dhcp_pptr) += 1;
>> +            continue;
>> +        }
>> +        (*in_dhcp_pptr) += sizeof *in_dhcp_opt;
>> +        if ((*in_dhcp_pptr) > end) {
>> +            break;
>> +        }
>> +        (*in_dhcp_pptr) += in_dhcp_opt->len;
>> +        if ((*in_dhcp_pptr) > end) {
>> +            break;
>> +        }
>> +
>> +        switch (in_dhcp_opt->code) {
>> +        case DHCP_OPT_MSG_TYPE:
>> +            if (dhcp_msg_type_pptr && in_dhcp_opt->len == 1) {
>> +                *dhcp_msg_type_pptr = DHCP_OPT_PAYLOAD(in_dhcp_opt);
>> +            }
>> +            break;
>> +        case DHCP_OPT_REQ_IP:
>> +            if (request_ip_ptr && in_dhcp_opt->len == 4) {
>> +                *request_ip_ptr = get_unaligned_be32(
>> +                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
>> +            }
>> +            break;
>> +        case OVN_DHCP_OPT_CODE_SERVER_ID:
>> +            if (server_id_ptr && in_dhcp_opt->len == 4) {
>> +                *server_id_ptr = get_unaligned_be32(
>> +                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
>> +            }
>> +            break;
>> +        case OVN_DHCP_OPT_CODE_NETMASK:
>> +            if (netmask_ptr && in_dhcp_opt->len == 4) {
>> +                *netmask_ptr = get_unaligned_be32(
>> +                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
>> +            }
>> +            break;
>> +        case OVN_DHCP_OPT_CODE_ROUTER_IP:
>> +            if (router_ip_ptr && in_dhcp_opt->len == 4) {
>> +                *router_ip_ptr = get_unaligned_be32(
>> +                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
>> +            }
>> +            break;
>> +        case DHCP_OPT_ETHERBOOT:
>> +            if (ipxe_req_ptr) {
>> +                *ipxe_req_ptr = true;
>> +            }
>> +            break;
>> +        default:
>> +            break;
>> +        }
>> +    }
>> +}
>> +
>> +/* Called with in the pinctrl_handler thread context. */
>> +static void
>> +pinctrl_handle_dhcp_relay_req_chk(
>> +    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;
>> +    uint32_t success = 0;
>> +
>> +    /* Parse result field. */
>> +    const struct mf_field *f;
>> +    enum ofperr ofperr = nx_pull_header(userdata, NULL, &f, NULL);
>> +    if (ofperr) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: bad result OXM (%s)",
>> +                        ofperr_to_string(ofperr));
>> +        goto exit;
>> +    }
>> +    ovs_be32 *ofsp = ofpbuf_try_pull(userdata, sizeof *ofsp);
>> +    /* Check that the result is valid and writable. */
>> +    struct mf_subfield dst = { .field = f, .ofs = ntohl(*ofsp), .n_bits = 1 };
>> +    ofperr = mf_check_dst(&dst, NULL);
>> +    if (ofperr) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: bad result bit (%s)",
>> +                        ofperr_to_string(ofperr));
>> +        goto exit;
>> +    }
>> +
>> +    /* Parse relay IP and server IP. */
>> +    ovs_be32 *relay_ip = ofpbuf_try_pull(userdata, sizeof *relay_ip);
>> +    ovs_be32 *server_ip = ofpbuf_try_pull(userdata, sizeof *server_ip);
>> +    if (!relay_ip || !server_ip) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: relay ip or server ip "
>> +                  "not present in the userdata");
>> +        goto exit;
>> +    }
>> +
>> +    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 = NULL;
>> +    const struct dhcp_header *in_dhcp_data
>> +                          = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
>> +
>> +    if (!in_dhcp_data) {
>> +        goto exit;
>> +    }
>> +    ovs_assert(in_dhcp_ptr);
>> +
>> +    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_RELAY_REQ_CHK: invalid opcode in the "
>> +                "DHCP packet: %d", in_dhcp_data->op);
>> +        goto exit;
>> +    }
>> +
>> +    if (in_dhcp_data->giaddr) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: giaddr is already set");
>> +        goto exit;
>> +    }
>> +
>> +    const uint8_t *in_dhcp_msg_type = NULL;
>> +    ovs_be32 request_ip = in_dhcp_data->ciaddr;
>> +
>> +    dhcp_parse_options(&in_dhcp_ptr, end,
>> +          &in_dhcp_msg_type, &request_ip, NULL, NULL, NULL, NULL);
>> +
>> +    /* 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_RELAY_REQ_CHK: missing message type");
>> +        goto exit;
>> +    }
>> +
>> +    /* Relay the DHCP request packet */
>> +    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 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);
>> +
>> +    uint8_t hops = dhcp_data->hops;
>> +    dhcp_data->hops = hops + 1;
>> +    if (udp->udp_csum) {
>> +        udp->udp_csum = recalc_csum16(udp->udp_csum,
>> +            (uint16_t) (hops << 8), (uint16_t) (dhcp_data->hops << 8));
> 
> recalc_csum16() expects all the arguments of type 'ovs_be16'  but
> you're casting to uint16_t.
> That's why the compilation is failing.
> 
> 
>> +    }
>> +
>> +    dhcp_data->giaddr = *relay_ip;
>> +    if (udp->udp_csum) {
>> +        udp->udp_csum = recalc_csum32(udp->udp_csum,
>> +            0, dhcp_data->giaddr);
>> +    }
>> +    pin->packet = dp_packet_data(&pkt_out);
>> +    pin->packet_len = dp_packet_size(&pkt_out);
>> +
>> +    /* Log the DHCP message. */
>> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(20, 40);
>> +    VLOG_INFO_RL(&rl, "DHCP_RELAY_REQ_CHK:: MSG_TYPE:%s MAC:"ETH_ADDR_FMT
>> +                " XID:%u"
>> +                " REQ_IP:"IP_FMT
>> +                " GIADDR:"IP_FMT
>> +                " SERVER_ADDR:"IP_FMT,
>> +                dhcp_msg_str_get(*in_dhcp_msg_type),
>> +                ETH_ADDR_BYTES_ARGS(dhcp_data->chaddr), ntohl(dhcp_data->xid),
>> +                IP_ARGS(request_ip), IP_ARGS(dhcp_data->giaddr),
>> +                IP_ARGS(*server_ip));
>> +    success = 1;
>> +exit:
>> +    if (!ofperr) {
>> +        union mf_subvalue sv;
>> +        sv.u8_val = success;
>> +        mf_write_subfield(&dst, &sv, &pin->flow_metadata);
>> +    }
>> +    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_relay_resp_chk(
>> +    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;
>> +    uint32_t success = 0;
>> +
>> +    /* Parse result field. */
>> +    const struct mf_field *f;
>> +    enum ofperr ofperr = nx_pull_header(userdata, NULL, &f, NULL);
>> +    if (ofperr) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: bad result OXM (%s)",
>> +                        ofperr_to_string(ofperr));
>> +        goto exit;
>> +    }
>> +    ovs_be32 *ofsp = ofpbuf_try_pull(userdata, sizeof *ofsp);
>> +    /* Check that the result is valid and writable. */
>> +    struct mf_subfield dst = { .field = f, .ofs = ntohl(*ofsp), .n_bits = 1 };
>> +    ofperr = mf_check_dst(&dst, NULL);
>> +    if (ofperr) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: bad result bit (%s)",
>> +                        ofperr_to_string(ofperr));
>> +        goto exit;
>> +    }
>> +
>> +    /* Parse relay IP and server IP. */
>> +    ovs_be32 *relay_ip = ofpbuf_try_pull(userdata, sizeof *relay_ip);
>> +    ovs_be32 *server_ip = ofpbuf_try_pull(userdata, sizeof *server_ip);
>> +    if (!relay_ip || !server_ip) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: relay ip or server ip "
>> +                "not present in the userdata");
>> +        goto exit;
>> +    }
>> +
>> +    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 = NULL;
>> +    const struct dhcp_header *in_dhcp_data
>> +                          = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
>> +
>> +    if (!in_dhcp_data) {
>> +        goto exit;
>> +    }
>> +    ovs_assert(in_dhcp_ptr);
>> +
>> +    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_RELAY_RESP_CHK: invalid opcode "
>> +                "in the packet: %d", in_dhcp_data->op);
>> +        goto exit;
>> +    }
>> +
>> +    if (!in_dhcp_data->giaddr) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: giaddr is "
>> +                    "not set in request");
>> +        goto exit;
>> +    }
>> +
>> +    ovs_be32 giaddr = in_dhcp_data->giaddr;
>> +    ovs_be32 yiaddr = in_dhcp_data->yiaddr;
>> +    ovs_be32 server_id = 0, netmask = 0, router_ip = 0;
>> +    const uint8_t *in_dhcp_msg_type = NULL;
>> +
>> +    dhcp_parse_options(&in_dhcp_ptr, end,
>> +          &in_dhcp_msg_type, NULL, NULL, &server_id, &netmask, &router_ip);
>> +
>> +    /* 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_RELAY_RESP: missing message type");
>> +        goto exit;
>> +    }
>> +
>> +    if (!server_id) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP: missing server identifier");
>> +        goto exit;
>> +    }
>> +
>> +    if (server_id != *server_ip) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP: server identifier mismatch");
>> +        goto exit;
>> +    }
>> +
>> +    if (giaddr != *relay_ip) {
>> +        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP: giaddr mismatch");
>> +        goto exit;
>> +    }
>> +
>> +    if (*in_dhcp_msg_type == DHCP_MSG_OFFER ||
>> +        *in_dhcp_msg_type == DHCP_MSG_ACK) {
>> +        if ((yiaddr & netmask) != (giaddr & netmask)) {
>> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +            VLOG_INFO_RL(&rl, "DHCP_RELAY_RESP_CHK::"
>> +                     " Allocated ip adress and giaddr are not in same subnet."
>> +                     " MSG_TYPE:%s MAC:"ETH_ADDR_FMT
>> +                     " XID:%u"
>> +                     " YIADDR:"IP_FMT
>> +                     " GIADDR:"IP_FMT
>> +                     " SERVER_ADDR:"IP_FMT,
>> +                     dhcp_msg_str_get(*in_dhcp_msg_type),
>> +                     ETH_ADDR_BYTES_ARGS(in_dhcp_data->chaddr),
>> +                     ntohl(in_dhcp_data->xid),
>> +                     IP_ARGS(yiaddr),
>> +                     IP_ARGS(giaddr), IP_ARGS(server_id));
>> +            goto exit;
>> +        }
>> +
>> +        if (router_ip && router_ip != giaddr) {
>> +            /* Log the default gateway mismatch and
>> +             * continue with rest of the processing */
>> +            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>> +            VLOG_INFO_RL(&rl, "DHCP_RELAY_RESP_CHK::"
>> +                     " Router ip adress and giaddr are not same."
>> +                     " MSG_TYPE:%s MAC:"ETH_ADDR_FMT
>> +                     " XID:%u"
>> +                     " YIADDR:"IP_FMT
>> +                     " GIADDR:"IP_FMT
>> +                     " SERVER_ADDR:"IP_FMT,
>> +                     dhcp_msg_str_get(*in_dhcp_msg_type),
>> +                     ETH_ADDR_BYTES_ARGS(in_dhcp_data->chaddr),
>> +                     ntohl(in_dhcp_data->xid),
>> +                     IP_ARGS(yiaddr),
>> +                     IP_ARGS(giaddr), IP_ARGS(server_id));
>> +        }
>> +    }
>> +
>> +    /* 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);
>> +
>> +    /* Log the DHCP message. */
>> +    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(20, 40);
>> +    VLOG_INFO_RL(&rl, "DHCP_RELAY_RESP_CHK:: MSG_TYPE:%s MAC:"ETH_ADDR_FMT
>> +             " XID:%u"
>> +             " YIADDR:"IP_FMT
>> +             " GIADDR:"IP_FMT
>> +             " SERVER_ADDR:"IP_FMT,
>> +             dhcp_msg_str_get(*in_dhcp_msg_type),
>> +             ETH_ADDR_BYTES_ARGS(dhcp_data->chaddr), ntohl(dhcp_data->xid),
>> +             IP_ARGS(yiaddr),
>> +             IP_ARGS(giaddr), IP_ARGS(server_id));
>> +    success = 1;
>> +exit:
>> +    if (!ofperr) {
>> +        union mf_subvalue sv;
>> +        sv.u8_val = success;
>> +        mf_write_subfield(&dst, &sv, &pin->flow_metadata);
>> +    }
>> +    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(
>> @@ -1956,30 +2464,16 @@ pinctrl_handle_put_dhcp_opts(
>>         goto exit;
>>     }
>> 
>> -    /* Validate the DHCP request packet.
>> -     * Format of the DHCP packet is
>> -     * ------------------------------------------------------------------------
>> -     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var len)|
>> -     * ------------------------------------------------------------------------
>> -     */
>> -
>>     const char *end = (char *)dp_packet_l4(pkt_in) + dp_packet_l4_size(pkt_in);
>> -    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, "Invalid or incomplete DHCP packet received");
>> -        goto exit;
>> -    }
>> -
>> +    const char *in_dhcp_ptr = NULL;
>>     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, "Invalid or incomplete DHCP packet received, "
>> -                     "bad data length");
>> +                          = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
>> +
>> +    if (!in_dhcp_data) {
>>         goto exit;
>>     }
>> +    ovs_assert(in_dhcp_ptr);
>> +
>>     if (in_dhcp_data->op != DHCP_OP_REQUEST) {
>>         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
>>         VLOG_WARN_RL(&rl, "Invalid opcode in the DHCP packet: %d",
>> @@ -1987,58 +2481,11 @@ pinctrl_handle_put_dhcp_opts(
>>         goto exit;
>>     }
>> 
>> -    /* 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 magic cookie not present in the DHCP packet");
>> -        goto exit;
>> -    }
>> -    in_dhcp_ptr += sizeof magic_cookie;
>> -
>>     bool ipxe_req = false;
>>     const uint8_t *in_dhcp_msg_type = NULL;
>>     ovs_be32 request_ip = in_dhcp_data->ciaddr;
>> -    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 DHCP_OPT_REQ_IP:
>> -            if (in_dhcp_opt->len == 4) {
>> -                request_ip = get_unaligned_be32(DHCP_OPT_PAYLOAD(in_dhcp_opt));
>> -            }
>> -            break;
>> -        case DHCP_OPT_ETHERBOOT:
>> -            ipxe_req = true;
>> -            break;
>> -        default:
>> -            break;
>> -        }
>> -    }
>> +    dhcp_parse_options(&in_dhcp_ptr, end,
>> +          &in_dhcp_msg_type, &request_ip, &ipxe_req, NULL, NULL, NULL);
>> 
>>     /* Check that the DHCP Message Type (opt 53) is present or not with
>>      * valid values - DHCP_MSG_DISCOVER or DHCP_MSG_REQUEST.
>> @@ -2245,6 +2692,7 @@ pinctrl_handle_put_dhcp_opts(
>>         dhcp_data->yiaddr = 0;
>>     }
>> 
>> +    ovs_be32 magic_cookie = htonl(DHCP_MAGIC_COOKIE);
>>     dp_packet_put(&pkt_out, &magic_cookie, sizeof(ovs_be32));
>> 
>>     uint16_t out_dhcp_opts_size = 12;
>> @@ -3215,6 +3663,16 @@ process_packet_in(struct rconn *swconn, const struct ofp_header *msg)
>>         ovs_mutex_unlock(&pinctrl_mutex);
>>         break;
>> 
>> +    case ACTION_OPCODE_DHCP_RELAY_REQ_CHK:
>> +        pinctrl_handle_dhcp_relay_req_chk(swconn, &packet, &pin,
>> +                                     &userdata, &continuation);
>> +        break;
>> +
>> +    case ACTION_OPCODE_DHCP_RELAY_RESP_CHK:
>> +        pinctrl_handle_dhcp_relay_resp_chk(swconn, &packet, &pin,
>> +                                     &userdata, &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 49fb96fc6..a8e4393ed 100644
>> --- a/include/ovn/actions.h
>> +++ b/include/ovn/actions.h
>> @@ -95,6 +95,8 @@ 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_RELAY_REQ_CHK,  ovnact_dhcp_relay)      \
>> +    OVNACT(DHCPV4_RELAY_RESP_CHK, ovnact_dhcp_relay)      \
>>     OVNACT(SET_QUEUE,         ovnact_set_queue)       \
>>     OVNACT(DNS_LOOKUP,        ovnact_result)          \
>>     OVNACT(LOG,               ovnact_log)             \
>> @@ -395,6 +397,15 @@ struct ovnact_put_opts {
>>     size_t n_options;
>> };
>> 
>> +/* OVNACT_DHCP_RELAY. */
>> +struct ovnact_dhcp_relay {
>> +    struct ovnact ovnact;
>> +    int family;
>> +    struct expr_field dst;      /* 1-bit destination field. */
>> +    ovs_be32 relay_ipv4;
>> +    ovs_be32 server_ipv4;
>> +};
>> +
>> /* Valid arguments to SET_QUEUE action.
>>  *
>>  * QDISC_MIN_QUEUE_ID is the default queue, so user-defined queues should
>> @@ -758,6 +769,22 @@ enum action_opcode {
>> 
>>     /* multicast group split buffer action. */
>>     ACTION_OPCODE_MG_SPLIT_BUF,
>> +
>> +    /* "dhcp_relay_req_chk(relay_ip, server_ip)".
>> +     *
>> +     * Arguments follow the action_header, in this format:
>> +     *   - The 32-bit DHCP relay IP.
>> +     *   - The 32-bit DHCP server IP.
>> +     */
>> +    ACTION_OPCODE_DHCP_RELAY_REQ_CHK,
>> +
>> +    /* "dhcp_relay_resp_chk(relay_ip, server_ip)".
>> +     *
>> +     * Arguments follow the action_header, in this format:
>> +     *   - The 32-bit DHCP relay IP.
>> +     *   - The 32-bit DHCP server IP.
>> +     */
>> +    ACTION_OPCODE_DHCP_RELAY_RESP_CHK,
>> };
>> 
>> /* Header. */
>> diff --git a/lib/actions.c b/lib/actions.c
>> index a45874dfb..d55b5153f 100644
>> --- a/lib/actions.c
>> +++ b/lib/actions.c
>> @@ -2680,6 +2680,149 @@ ovnact_controller_event_free(struct ovnact_controller_event *event)
>>     free_gen_options(event->options, event->n_options);
>> }
>> 
>> +static void
>> +format_DHCPV4_RELAY_REQ_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
>> +                struct ds *s)
>> +{
>> +    expr_field_format(&dhcp_relay->dst, s);
>> +    ds_put_format(s, " = dhcp_relay_req_chk("IP_FMT", "IP_FMT");",
>> +                  IP_ARGS(dhcp_relay->relay_ipv4),
>> +                  IP_ARGS(dhcp_relay->server_ipv4));
>> +}
>> +
>> +static void
>> +parse_dhcp_relay_req_chk(struct action_context *ctx,
>> +               const struct expr_field *dst,
>> +               struct ovnact_dhcp_relay *dhcp_relay)
>> +{
>> +    /* Skip dhcp_relay_req_chk( */
>> +    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
>> +
>> +    /* Validate that the destination is a 1-bit, modifiable field. */
>> +    char *error = expr_type_check(dst, 1, true, ctx->scope);
>> +    if (error) {
>> +        lexer_error(ctx->lexer, "%s", error);
>> +        free(error);
>> +        return;
>> +    }
>> +    dhcp_relay->dst = *dst;
>> +
>> +    /* Parse relay ip and server ip. */
>> +    if (ctx->lexer->token.format == LEX_F_IPV4) {
>> +        dhcp_relay->family = AF_INET;
>> +        dhcp_relay->relay_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_relay->family = AF_INET;
>> +            dhcp_relay->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 relay "
>> +                          "and server ips");
>> +          return;
>> +    }
>> +    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
>> +}
>> +
>> +static void
>> +encode_DHCPV4_RELAY_REQ_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
>> +                    const struct ovnact_encode_params *ep,
>> +                    struct ofpbuf *ofpacts)
>> +{
>> +    struct mf_subfield dst = expr_resolve_field(&dhcp_relay->dst);
>> +    size_t oc_offset = encode_start_controller_op(
>> +                                            ACTION_OPCODE_DHCP_RELAY_REQ_CHK,
>> +                                            true, ep->ctrl_meter_id,
>> +                                            ofpacts);
>> +    nx_put_header(ofpacts, dst.field->id, OFP15_VERSION, false);
>> +    ovs_be32 ofs = htonl(dst.ofs);
>> +    ofpbuf_put(ofpacts, &ofs, sizeof ofs);
>> +    ofpbuf_put(ofpacts, &dhcp_relay->relay_ipv4,
>> +            sizeof(dhcp_relay->relay_ipv4));
>> +    ofpbuf_put(ofpacts, &dhcp_relay->server_ipv4,
>> +            sizeof(dhcp_relay->server_ipv4));
>> +    encode_finish_controller_op(oc_offset, ofpacts);
>> +}
>> +
>> +static void
>> +format_DHCPV4_RELAY_RESP_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
>> +                    struct ds *s)
>> +{
>> +    expr_field_format(&dhcp_relay->dst, s);
>> +    ds_put_format(s, " = dhcp_relay_resp_chk("IP_FMT", "IP_FMT");",
>> +                  IP_ARGS(dhcp_relay->relay_ipv4),
>> +                  IP_ARGS(dhcp_relay->server_ipv4));
>> +}
>> +
>> +static void
>> +parse_dhcp_relay_resp_chk(struct action_context *ctx,
>> +               const struct expr_field *dst,
>> +               struct ovnact_dhcp_relay *dhcp_relay)
>> +{
>> +    /* Skip dhcp_relay_resp_chk( */
>> +    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
>> +
>> +    /* Validate that the destination is a 1-bit, modifiable field. */
>> +    char *error = expr_type_check(dst, 1, true, ctx->scope);
>> +    if (error) {
>> +        lexer_error(ctx->lexer, "%s", error);
>> +        free(error);
>> +        return;
>> +    }
>> +    dhcp_relay->dst = *dst;
>> +
>> +    /* Parse relay ip and server ip. */
>> +    if (ctx->lexer->token.format == LEX_F_IPV4) {
>> +        dhcp_relay->family = AF_INET;
>> +        dhcp_relay->relay_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_relay->family = AF_INET;
>> +            dhcp_relay->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 relay and "
>> +                          "server ips");
>> +          return;
>> +    }
>> +    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
>> +}
>> +
>> +static void
>> +encode_DHCPV4_RELAY_RESP_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
>> +                    const struct ovnact_encode_params *ep,
>> +                    struct ofpbuf *ofpacts)
>> +{
>> +    struct mf_subfield dst = expr_resolve_field(&dhcp_relay->dst);
>> +    size_t oc_offset = encode_start_controller_op(
>> +                                ACTION_OPCODE_DHCP_RELAY_RESP_CHK,
>> +                                true, ep->ctrl_meter_id,
>> +                                ofpacts);
>> +    nx_put_header(ofpacts, dst.field->id, OFP15_VERSION, false);
>> +    ovs_be32 ofs = htonl(dst.ofs);
>> +    ofpbuf_put(ofpacts, &ofs, sizeof ofs);
>> +    ofpbuf_put(ofpacts, &dhcp_relay->relay_ipv4,
>> +                  sizeof(dhcp_relay->relay_ipv4));
>> +    ofpbuf_put(ofpacts, &dhcp_relay->server_ipv4,
>> +                  sizeof(dhcp_relay->server_ipv4));
>> +    encode_finish_controller_op(oc_offset, ofpacts);
>> +}
>> +
>> +static void ovnact_dhcp_relay_free(
>> +          struct ovnact_dhcp_relay *dhcp_relay 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,
>> @@ -5384,6 +5527,12 @@ parse_set_action(struct action_context *ctx)
>>                    lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
>>             parse_chk_lb_aff(ctx, &lhs,
>>                     ovnact_put_CHK_LB_AFF(ctx->ovnacts));
>> +        } else if (lexer_match_id(ctx->lexer, "dhcp_relay_req_chk")) {
>> +            parse_dhcp_relay_req_chk(ctx, &lhs,
>> +                    ovnact_put_DHCPV4_RELAY_REQ_CHK(ctx->ovnacts));
>> +        } else if (lexer_match_id(ctx->lexer, "dhcp_relay_resp_chk")) {
>> +            parse_dhcp_relay_resp_chk(ctx, &lhs,
>> +                    ovnact_put_DHCPV4_RELAY_RESP_CHK(ctx->ovnacts));
>>         } else {
>>             parse_assignment_action(ctx, false, &lhs);
>>         }
>> diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h
>> index ad514a922..5eeb7c1a0 100644
>> --- a/lib/ovn-l7.h
>> +++ b/lib/ovn-l7.h
>> @@ -68,7 +68,9 @@ struct gen_opts_map {
>>  * OVN_DHCP_OPT_CODE_<opt_name>.
>>  */
>> #define OVN_DHCP_OPT_CODE_NETMASK      1
>> +#define OVN_DHCP_OPT_CODE_ROUTER_IP    3
>> #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 2c3560ce2..1f7a62982 100644
>> --- a/northd/northd.c
>> +++ b/northd/northd.c
>> @@ -91,7 +91,6 @@ static bool use_ct_inv_match = true;
>> static bool default_acl_drop;
>> 
>> #define MAX_OVN_TAGS 4096
>> -
>> 
>> /* Due to various hard-coded priorities need to implement ACLs, the
>>  * northbound database supports a smaller range of ACL priorities than
>> @@ -153,6 +152,8 @@ static bool default_acl_drop;
>> #define REGBIT_LOOKUP_NEIGHBOR_IP_RESULT "reg9[3]"
>> #define REGBIT_DST_NAT_IP_LOCAL "reg9[4]"
>> #define REGBIT_KNOWN_LB_SESSION "reg9[6]"
>> +#define REGBIT_DHCP_RELAY_REQ_CHK "reg9[7]"
>> +#define REGBIT_DHCP_RELAY_RESP_CHK "reg9[8]"
>> 
>> /* Register to store the eth address associated to a router port for packets
>>  * received in S_ROUTER_IN_ADMISSION.
>> @@ -168,6 +169,7 @@ static bool default_acl_drop;
>> #define REG_NEXT_HOP_IPV6 "xxreg0"
>> #define REG_SRC_IPV4 "reg1"
>> #define REG_SRC_IPV6 "xxreg1"
>> +#define REG_DHCP_RELAY_DIP_IPV4 "reg2"
>> #define REG_ROUTE_TABLE_ID "reg7"
>> 
>> /* Register used to store backend ipv6 address
>> @@ -232,7 +234,7 @@ static bool default_acl_drop;
>>  * | R1  |   SRC_IPV4 for ARP-REQ    | 0 |                 | R |                                    |
>>  * |     |      (>= IP_INPUT)        |   |                 | E |     NEXT_HOP_IPV6 (>= DEFRAG )     |
>>  * +-----+---------------------------+---+-----------------+ G |                                    |
>> - * | R2  |        UNUSED             | X |                 | 0 |                                    |
>> + * | R2     REG_DHCP_RELAY_DIP_IPV4  | X |                 | 0 |                                    |
>>  * |     |                           | R |                 |   |                                    |
>>  * +-----+---------------------------+ E |     UNUSED      |   |                                    |
>>  * | R3  |        UNUSED             | G |                 |   |                                    |
>> @@ -259,7 +261,9 @@ static bool default_acl_drop;
>>  * |     |   EGRESS_LOOPBACK/        | G |     UNUSED      |
>>  * | R9  |   PKT_LARGER/             | 4 |                 |
>>  * |     |   LOOKUP_NEIGHBOR_RESULT/ |   |                 |
>> - * |     |   SKIP_LOOKUP_NEIGHBOR}   |   |                 |
>> + * |     |   SKIP_LOOKUP_NEIGHBOR/   |   |                 |
>> + * |     |REGBIT_DHCP_RELAY_REQ_CHK/ |   |                 |
>> + * |     |REGBIT_DHCP_RELAY_RESP_CHK}|   |                 |
>>  * |     |                           |   |                 |
>>  * |     | REG_ORIG_TP_DPORT_ROUTER  |   |                 |
>>  * |     |                           |   |                 |
>> @@ -8555,6 +8559,85 @@ build_dhcpv6_options_flows(struct ovn_port *op,
>>     ds_destroy(&match);
>> }
>> 
>> +static const char *
>> +ls_dhcp_relay_port(const struct ovn_datapath *od)
>> +{
>> +    return smap_get(&od->nbs->other_config, "dhcp_relay_port");
>> +}
>> +
>> +static void
>> +build_lswitch_dhcp_relay_flows(struct ovn_port *op,
>> +                           const struct hmap *ls_ports,
>> +                           struct lflow_table *lflows)
>> +{
>> +    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) {
>> +        return;
>> +    }
>> +
>> +    /* configure dhcp relay flows only when peer router  has
>> +     * relay config enabled */
>> +    const char *dhcp_relay_port = ls_dhcp_relay_port(op->od);
>> +    if (!dhcp_relay_port) {
>> +        return;
>> +    }
>> +
>> +    struct ds match = DS_EMPTY_INITIALIZER;
>> +    struct ds action = DS_EMPTY_INITIALIZER;
>> +    struct ovn_port *sp = ovn_port_find(ls_ports, dhcp_relay_port);
>> +
>> +    if (!sp || !sp->nbsp || !sp->peer) {
>> +        return;
>> +    }
>> +
>> +    struct ovn_port *rp = sp->peer;
>> +    if (!rp || !rp->nbrp || !rp->nbrp->dhcp_relay || rp->peer != sp) {
>> +        return;
>> +    }
>> +
>> +    char *server_ip_str = NULL;
>> +    uint16_t port;
>> +    int addr_family;
>> +    struct in6_addr server_ip;
>> +    struct nbrec_dhcp_relay *dhcp_relay = rp->nbrp->dhcp_relay;
>> +
>> +    if (!ip_address_and_port_from_lb_key(dhcp_relay->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_RELAY_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,
>> +                              &op->nbsp->header_,
>> +                              op->lflow_ref);
>> +    free(server_ip_str);
>> +}
>> +
>> static void
>> build_drop_arp_nd_flows_for_unbound_router_ports(struct ovn_port *op,
>>                                                  const struct ovn_port *port,
>> @@ -9156,6 +9239,13 @@ build_lswitch_dhcp_options_and_response(struct ovn_port *op,
>>         return;
>>     }
>> 
>> +    if (op->od && op->od->nbs
>> +        && ls_dhcp_relay_port(op->od)) {
>> +        /* Don't add the DHCP server flows if DHCP Relay 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)) {
>> @@ -13636,6 +13726,165 @@ build_dhcpv6_reply_flows_for_lrouter_port(
>>     }
>> }
>> 
>> +static void
>> +build_dhcp_relay_flows_for_lrouter_port(
>> +        struct ovn_port *op, struct lflow_table *lflows,
>> +        struct ds *match, struct lflow_ref *lflow_ref)
>> +{
>> +    if (!op->nbrp || !op->nbrp->dhcp_relay) {
>> +        return;
>> +
>> +    }
>> +
>> +    /* configure dhcp relay flows only when peer switch has
>> +     * relay config enabled */
>> +    struct ovn_port *sp = op->peer;
>> +    if (!sp || !sp->nbsp || sp->peer != op ||
>> +        !sp->od || !ls_dhcp_relay_port(sp->od)) {
>> +        return;
>> +    }
>> +
>> +    struct nbrec_dhcp_relay *dhcp_relay = op->nbrp->dhcp_relay;
>> +    if (!dhcp_relay->servers) {
>> +        return;
>> +    }
>> +
>> +    int addr_family;
>> +    /* currently not supporting custom port,
>> +     * dhcp server port is always set to 67 when installing flows */
>> +    uint16_t port;
>> +    char *server_ip_str = NULL;
>> +    struct in6_addr server_ip;
>> +
>> +    if (!ip_address_and_port_from_lb_key(dhcp_relay->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,
>> +                REGBIT_DHCP_RELAY_REQ_CHK
>> +                " = dhcp_relay_req_chk(%s, %s);"
>> +                "next; /* DHCP_RELAY_REQ */",
>> +                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_, lflow_ref);
>> +
>> +    ds_clear(match);
>> +    ds_clear(&dhcp_action);
>> +
>> +    ds_put_format(
>> +        match, "inport == %s && "
>> +        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
>> +        "udp.src == 68 && udp.dst == 67 && "
>> +        REGBIT_DHCP_RELAY_REQ_CHK,
>> +        op->json_key);
>> +    ds_put_format(&dhcp_action,
>> +                "ip4.src=%s;ip4.dst=%s;udp.src=67;next; /* DHCP_RELAY_REQ */",
>> +                op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str);
>> +
>> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_REQ, 100,
>> +                            ds_cstr(match), ds_cstr(&dhcp_action),
>> +                            &op->nbrp->header_, lflow_ref);
>> +
>> +    ds_clear(match);
>> +    ds_clear(&dhcp_action);
>> +
>> +    ds_put_format(
>> +        match, "inport == %s && "
>> +        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
>> +        "udp.src == 68 && udp.dst == 67 && "
>> +        REGBIT_DHCP_RELAY_REQ_CHK" == 0",
>> +        op->json_key);
>> +    ds_put_format(&dhcp_action,
>> +                "drop; /* DHCP_RELAY_REQ */");
>> +
>> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_REQ, 1,
>> +                            ds_cstr(match), ds_cstr(&dhcp_action),
>> +                            &op->nbrp->header_, lflow_ref);
>> +
>> +    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_RELAY_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_, lflow_ref);
>> +
>> +    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,
>> +          REG_DHCP_RELAY_DIP_IPV4" = ip4.dst;"
>> +          REGBIT_DHCP_RELAY_RESP_CHK
>> +          " = dhcp_relay_resp_chk(%s, %s);next;/* DHCP_RELAY_RESP */",
>> +          op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str);
>> +
>> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_RESP_CHK,
>> +                            100,
>> +                            ds_cstr(match), ds_cstr(&dhcp_action),
>> +                            &op->nbrp->header_, lflow_ref);
>> +
>> +
>> +    ds_clear(match);
>> +    ds_clear(&dhcp_action);
>> +
>> +    ds_put_format(
>> +        match, "ip4.src == %s && "
>> +        REG_DHCP_RELAY_DIP_IPV4" == %s && "
>> +        "udp.src == 67 && udp.dst == 67 && "
>> +        REGBIT_DHCP_RELAY_RESP_CHK,
>> +        server_ip_str, op->lrp_networks.ipv4_addrs[0].addr_s);
>> +    ds_put_format(&dhcp_action,
>> +          "ip4.src=%s;udp.dst=68;"
>> +          "outport=%s;output; /* DHCP_RELAY_RESP */",
>> +          op->lrp_networks.ipv4_addrs[0].addr_s, op->json_key);
>> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_RESP,
>> +                            100,
>> +                            ds_cstr(match), ds_cstr(&dhcp_action),
>> +                            &op->nbrp->header_, lflow_ref);
>> +
>> +    ds_clear(match);
>> +    ds_clear(&dhcp_action);
>> +
>> +    ds_put_format(
>> +        match, "ip4.src == %s && "
>> +        REG_DHCP_RELAY_DIP_IPV4" == %s && "
>> +        "udp.src == 67 && udp.dst == 67 && "
>> +        REGBIT_DHCP_RELAY_RESP_CHK" == 0",
>> +        server_ip_str, op->lrp_networks.ipv4_addrs[0].addr_s);
>> +    ds_put_format(&dhcp_action,
>> +          "drop; /* DHCP_RELAY_RESP */");
>> +    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_RESP,
>> +                            1,
>> +                            ds_cstr(match), ds_cstr(&dhcp_action),
>> +                            &op->nbrp->header_, lflow_ref);
>> +    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 lflow_table *lflows,
>> @@ -14906,6 +15155,13 @@ static void build_lr_nat_defrag_and_lb_default_flows(
>>                   lflow_ref);
>>     ovn_lflow_add(lflows, od, S_ROUTER_IN_ECMP_STATEFUL, 0, "1", "next;",
>>                   lflow_ref);
>> +    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_RELAY_REQ, 0, "1",
>> +                  "next;", lflow_ref);
>> +    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_RELAY_RESP_CHK, 0, "1",
>> +                  "next;", lflow_ref);
>> +    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_RELAY_RESP, 0, "1",
>> +                  "next;", lflow_ref);
>> +
>> 
>>     /* Send the IPv6 NS packets to next table. When ovn-controller
>>      * generates IPv6 NS (for the action - nd_ns{}), the injected
>> @@ -15670,6 +15926,7 @@ build_lswitch_and_lrouter_iterate_by_lsp(struct ovn_port *op,
>>     build_lswitch_icmp_packet_toobig_admin_flows(op, lflows, match, actions);
>>     build_lswitch_ip_unicast_lookup(op, lflows, actions,
>>                                     match);
>> +    build_lswitch_dhcp_relay_flows(op, ls_ports, lflows);
>> 
>>     /* Build Logical Router Flows. */
>>     build_arp_resolve_flows_for_lsp(op, lflows, lr_ports, match, actions);
>> @@ -15699,6 +15956,8 @@ build_lswitch_and_lrouter_iterate_by_lrp(struct ovn_port *op,
>>                                                  op->lflow_ref);
>>     build_dhcpv6_reply_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
>>                                               op->lflow_ref);
>> +    build_dhcp_relay_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
>> +                                              op->lflow_ref);
>>     build_ipv6_input_flows_for_lrouter_port(op, lsi->lflows,
>>                                             &lsi->match, &lsi->actions,
>>                                             lsi->meter_groups,
>> diff --git a/northd/northd.h b/northd/northd.h
>> index 3f1cd8341..0a349c8f7 100644
>> --- a/northd/northd.h
>> +++ b/northd/northd.h
>> @@ -437,24 +437,29 @@ enum ovn_stage {
>>     PIPELINE_STAGE(ROUTER, IN,  LOOKUP_NEIGHBOR, 1, "lr_in_lookup_neighbor") \
>>     PIPELINE_STAGE(ROUTER, IN,  LEARN_NEIGHBOR,  2, "lr_in_learn_neighbor") \
>>     PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,        3, "lr_in_ip_input")     \
>> -    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          4, "lr_in_unsnat")       \
>> -    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          5, "lr_in_defrag")       \
>> -    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    6, "lr_in_lb_aff_check") \
>> -    PIPELINE_STAGE(ROUTER, IN,  DNAT,            7, "lr_in_dnat")         \
>> -    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    8, "lr_in_lb_aff_learn") \
>> -    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   9, "lr_in_ecmp_stateful") \
>> -    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   10, "lr_in_nd_ra_options") \
>> -    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  11, "lr_in_nd_ra_response") \
>> -    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  12, "lr_in_ip_routing_pre")  \
>> -    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      13, "lr_in_ip_routing")      \
>> -    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_RELAY_REQ,  4, "lr_in_dhcp_relay_req") \
>> +    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          5, "lr_in_unsnat")       \
>> +    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          6, "lr_in_defrag")       \
>> +    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    7, "lr_in_lb_aff_check") \
>> +    PIPELINE_STAGE(ROUTER, IN,  DNAT,            8, "lr_in_dnat")         \
>> +    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    9, "lr_in_lb_aff_learn") \
>> +    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   10, "lr_in_ecmp_stateful") \
>> +    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   11, "lr_in_nd_ra_options") \
>> +    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  12, "lr_in_nd_ra_response") \
>> +    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  13, "lr_in_ip_routing_pre")  \
>> +    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      14, "lr_in_ip_routing")      \
>> +    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 15, "lr_in_ip_routing_ecmp") \
>> +    PIPELINE_STAGE(ROUTER, IN,  POLICY,          16, "lr_in_policy")          \
>> +    PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     17, "lr_in_policy_ecmp")     \
>> +    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP_CHK, 18,                      \
>> +                  "lr_in_dhcp_relay_resp_chk")                                \
>> +    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP, 19,                          \
>> +                  "lr_in_dhcp_relay_resp")                                    \
>> +    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     20, "lr_in_arp_resolve")     \
>> +    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     21, "lr_in_chk_pkt_len")     \
>> +    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     22, "lr_in_larger_pkts")     \
>> +    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     23, "lr_in_gw_redirect")     \
>> +    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     24, "lr_in_arp_request")     \
>>                                                                       \
>>     /* Logical router egress stages. */                               \
>>     PIPELINE_STAGE(ROUTER, OUT, CHECK_DNAT_LOCAL,   0,                       \
>> diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
>> index a9c5b7af5..a616dbd6b 100644
>> --- a/ovn-nb.ovsschema
>> +++ b/ovn-nb.ovsschema
>> @@ -1,7 +1,7 @@
>> {
>>     "name": "OVN_Northbound",
>>     "version": "7.3.0",
>> -    "cksum": "3546526738 34483",
>> +    "cksum": "3858903371 35372",
>>     "tables": {
>>         "NB_Global": {
>>             "columns": {
>> @@ -436,6 +436,11 @@
>>                 "ipv6_prefix": {"type": {"key": "string",
>>                                       "min": 0,
>>                                       "max": "unlimited"}},
>> +                "dhcp_relay": {"type": {"key": {"type": "uuid",
>> +                                            "refTable": "DHCP_Relay",
>> +                                            "refType": "strong"},
>> +                                            "min": 0,
>> +                                            "max": 1}},
>>                 "external_ids": {
>>                     "type": {"key": "string", "value": "string",
>>                              "min": 0, "max": "unlimited"}},
>> @@ -534,6 +539,18 @@
>>                     "type": {"key": "string", "value": "string",
>>                              "min": 0, "max": "unlimited"}}},
>>             "isRoot": true},
>> +        "DHCP_Relay": {
>> +            "columns": {
>> +                "name": {"type": "string"},
>> +                "servers": {"type": {"key": "string",
>> +                                       "min": 0,
>> +                                       "max": 1}},
>> +                "options": {"type": {"key": "string", "value": "string",
>> +                                     "min": 0, "max": "unlimited"}},
>> +                "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 e0b983ed6..6f0210325 100644
>> --- a/ovn-nb.xml
>> +++ b/ovn-nb.xml
>> @@ -703,6 +703,13 @@
>>         </ul>
>>       </column>
>> 
>> +      <column name="other_config" key="dhcp_relay_port">
>> +        If set to the name of logical switch port of type <code>router</code>
>> +        then, DHCP Relay is enabled for this logical switch provided the
>> +        corresponding <ref table="Logical_Router_Port"/> has DHCP Relay
>> +        configured.
>> +      </column>
>> +
>>       <column name="other_config" key="mac_only" type='{"type": "boolean"}'>
>>         Value used to request to assign L2 address only if neither subnet
>>         nor ipv6_prefix are specified
>> @@ -3059,6 +3066,11 @@ or
>>       port has all ingress and egress traffic dropped.
>>     </column>
>> 
>> +    <column name="dhcp_relay">
>> +      This column is used to enabled DHCP Relay. Please refer
>> +      to <ref table="DHCP_Relay"/> table.
>> +    </column>
>> +
>>     <group title="Distributed Gateway Ports">
>>       <p>
>>         Gateways, as documented under <code>Gateways</code> in the OVN
>> @@ -4372,6 +4384,33 @@ or
>>     </group>
>>   </table>
>> 
>> +  <table name="DHCP_Relay" title="DHCP Relay">
>> +    <p>
>> +      OVN implements native DHCPv4 relay support which caters to the common
>> +      use case of relaying the DHCP requests to external DHCP server.
>> +    </p>
>> +    <column name="name">
>> +      <p>
>> +        A name for the DHCP Relay.
>> +      </p>
>> +    </column>
>> +    <column name="servers">
>> +      <p>
>> +        The DHCPv4 server IP address.
>> +      </p>
>> +    </column>
>> +    <column name="options">
>> +      <p>
>> +        Future purpose.
>> +      </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/atlocal.in b/tests/atlocal.in
>> index 63d891b89..32d1c374e 100644
>> --- a/tests/atlocal.in
>> +++ b/tests/atlocal.in
>> @@ -187,6 +187,9 @@ fi
>> # Set HAVE_DHCPD
>> find_command dhcpd
>> 
>> +# Set HAVE_DHCLIENT
>> +find_command dhclient
>> +
>> # Set HAVE_BFDD_BEACON
>> find_command bfdd-beacon
>> 
>> diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
>> index c189dcccc..c8d551f2d 100644
>> --- a/tests/ovn-northd.at
>> +++ b/tests/ovn-northd.at
>> @@ -12138,6 +12138,44 @@ check_row_count nb:QoS 0
>> AT_CLEANUP
>> ])
>> 
>> +OVN_FOR_EACH_NORTHD_NO_HV([
>> +AT_SETUP([check DHCP RELAY])
>> +ovn_start NORTHD_TYPE
>> +
>> +check ovn-nbctl ls-add ls0
>> +check ovn-nbctl lsp-add ls0 ls0-port1
>> +check ovn-nbctl lsp-set-addresses ls0-port1 02:00:00:00:00:10
>> +check ovn-nbctl lr-add lr0
>> +check ovn-nbctl lrp-add lr0 lrp1 02:00:00:00:00:01 192.168.1.1/24
>> +check ovn-nbctl lsp-add ls0 lrp1-attachment
>> +check ovn-nbctl lsp-set-type lrp1-attachment router
>> +check ovn-nbctl lsp-set-addresses lrp1-attachment 00:00:00:00:ff:02
>> +check ovn-nbctl lsp-set-options lrp1-attachment router-port=lrp1
>> +check ovn-nbctl lrp-add lr0 lrp-ext 02:00:00:00:00:02 192.168.2.1/24
>> +
>> +dhcp_relay=$(ovn-nbctl create DHCP_Relay servers=172.16.1.1)
>> +check ovn-nbctl set Logical_Router_port lrp1 dhcp_relay=$dhcp_relay
>> +check ovn-nbctl set Logical_Switch ls0 other_config:dhcp_relay_port=lrp1-attachment
>> +
>> +check ovn-nbctl --wait=sb sync
>> +
>> +ovn-sbctl lflow-list > lflows
>> +AT_CAPTURE_FILE([lflows])
>> +
>> +AT_CHECK([grep -e "DHCP_RELAY_" lflows | sed 's/table=../table=??/'], [0], [dnl
>> +  table=??(lr_in_ip_input     ), priority=110  , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67), action=(reg9[[7]] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);next; /* DHCP_RELAY_REQ */)
>> +  table=??(lr_in_ip_input     ), priority=110  , match=(ip4.src == 172.16.1.1 && ip4.dst == 192.168.1.1 && udp.src == 67 && udp.dst == 67), action=(next;/* DHCP_RELAY_RESP */)
>> +  table=??(lr_in_dhcp_relay_req), priority=100  , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[[7]]), action=(ip4.src=192.168.1.1;ip4.dst=172.16.1.1;udp.src=67;next; /* DHCP_RELAY_REQ */)
>> +  table=??(lr_in_dhcp_relay_req), priority=1    , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[[7]] == 0), action=(drop; /* DHCP_RELAY_REQ */)
>> +  table=??(lr_in_dhcp_relay_resp_chk), priority=100  , match=(ip4.src == 172.16.1.1 && ip4.dst == 192.168.1.1 && udp.src == 67 && udp.dst == 67), action=(reg2 = ip4.dst;reg9[[8]] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);next;/* DHCP_RELAY_RESP */)
>> +  table=??(lr_in_dhcp_relay_resp), priority=100  , match=(ip4.src == 172.16.1.1 && reg2 == 192.168.1.1 && udp.src == 67 && udp.dst == 67 && reg9[[8]]), action=(ip4.src=192.168.1.1;udp.dst=68;outport="lrp1";output; /* DHCP_RELAY_RESP */)
>> +  table=??(lr_in_dhcp_relay_resp), priority=1    , match=(ip4.src == 172.16.1.1 && reg2 == 192.168.1.1 && udp.src == 67 && udp.dst == 67 && reg9[[8]] == 0), action=(drop; /* DHCP_RELAY_RESP */)
>> +  table=??(ls_in_l2_lkup      ), priority=100  , match=(inport == "ls0-port1" && eth.src == 02:00:00:00:00:10 && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67), action=(eth.dst=02:00:00:00:00:01;outport="lrp1-attachment";next;/* DHCP_RELAY_REQ */)
>> +])
>> +
>> +AT_CLEANUP
>> +])
>> +
>> AT_SETUP([NB_Global and SB_Global incremental processing])
>> 
>> ovn_start
>> diff --git a/tests/ovn.at b/tests/ovn.at
>> index 902dd3793..109e19550 100644
>> --- a/tests/ovn.at
>> +++ b/tests/ovn.at
>> @@ -1661,6 +1661,40 @@ reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain_name=1.2.3.4);
>> reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain_search_list=1.2.3.4);
>>     DHCPv4 option domain_search_list requires string value.
>> 
>> +#dhcp_relay_req_chk
>> +reg9[7] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);
>> +    encodes as controller(userdata=00.00.00.1c.00.00.00.00.80.01.08.08.00.00.00.07.c0.a8.01.01.ac.10.01.01,pause)
>> +
>> +reg9[7] = dhcp_relay_req_chk(192.168.1.1,172.16.1.1);
>> +    formats as reg9[7] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);
>> +    encodes as controller(userdata=00.00.00.1c.00.00.00.00.80.01.08.08.00.00.00.07.c0.a8.01.01.ac.10.01.01,pause)
>> +
>> +reg9[7..8] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);
>> +    Cannot use 2-bit field reg9[7..8] where 1-bit field is required.
>> +
>> +reg9[7] = dhcp_relay_req_chk("192.168.1.1", "172.16.1.1");
>> +    Syntax error at `"192.168.1.1"' expecting IPv4 dhcp relay and server ips.
>> +
>> +reg9[7] = dhcp_relay_req_chk(192.168.1, 172.16.1.1);
>> +    Invalid numeric constant.
>> +
>> +#dhcp_relay_resp_chk
>> +reg9[8] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);
>> +    encodes as controller(userdata=00.00.00.1d.00.00.00.00.80.01.08.08.00.00.00.08.c0.a8.01.01.ac.10.01.01,pause)
>> +
>> +reg9[8] = dhcp_relay_resp_chk(192.168.1.1,172.16.1.1);
>> +    formats as reg9[8] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);
>> +    encodes as controller(userdata=00.00.00.1d.00.00.00.00.80.01.08.08.00.00.00.08.c0.a8.01.01.ac.10.01.01,pause)
>> +
>> +reg9[7..8] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);
>> +    Cannot use 2-bit field reg9[7..8] where 1-bit field is required.
>> +
>> +reg9[8] = dhcp_relay_resp_chk("192.168.1.1", "172.16.1.1");
>> +    Syntax error at `"192.168.1.1"' expecting IPv4 dhcp relay and server ips.
>> +
>> +reg9[8] = dhcp_relay_resp_chk(192.168.1, 172.16.1.1);
>> +    Invalid numeric constant.
>> +
>> # nd_ns
>> nd_ns { nd.target = xxreg0; output; };
>>     encodes as controller(userdata=00.00.00.09.00.00.00.00.00.1c.00.18.00.80.00.00.00.00.00.00.00.01.de.10.80.00.3e.10.00.00.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00)
>> @@ -21996,7 +22030,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_UNQUOTED([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_UNQUOTED([as hv1 ovs-ofctl dump-flows br-int metadata=0x$lr0_dp_key | awk '/table=31, n_packets=1, n_bytes=45/{print $7" "$8}'],[0],[dnl
>> priority=80,ip,reg15=0x$lr0_public_dp_key,metadata=0x$lr0_dp_key,nw_src=10.0.0.10 actions=drop
>> ])
>> 
>> @@ -28221,7 +28255,7 @@ ovn-sbctl dump-flows > sbflows
>> AT_CAPTURE_FILE([sbflows])
>> AT_CAPTURE_FILE([offlows])
>> OVS_WAIT_UNTIL([
>> -    as hv1 ovs-ofctl dump-flows br-int table=23 > offlows
>> +    as hv1 ovs-ofctl dump-flows br-int table=24 > offlows
>>     test $(grep -c "load:0x64->NXM_NX_PKT_MARK" offlows) = 1 && \
>>     test $(grep -c "load:0x3->NXM_NX_PKT_MARK" offlows) = 1 && \
>>     test $(grep -c "load:0x4->NXM_NX_PKT_MARK" offlows) = 1 && \
>> @@ -28319,12 +28353,12 @@ send_ipv4_pkt hv1 hv1-vif1 505400000003 00000000ff01 \
>>     c3ad 83dc
>> 
>> OVS_WAIT_UNTIL([
>> -    test 1 -eq $(as hv1 ovs-ofctl dump-flows br-int table=23 | \
>> +    test 1 -eq $(as hv1 ovs-ofctl dump-flows br-int table=24 | \
>>     grep "load:0x2->NXM_NX_PKT_MARK" -c)
>> ])
>> 
>> AT_CHECK([
>> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=23 | \
>> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=24 | \
>>     grep "load:0x64->NXM_NX_PKT_MARK" -c)
>> ])
>> 
>> @@ -29017,23 +29051,23 @@ check ovn-nbctl --wait=hv sync
>> 
>> # Ensure ECMP symmetric reply flows are not present on any hypervisor.
>> AT_CHECK([
>> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=17 | \
>> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=18 | \
>>     grep "priority=100" | \
>>     grep "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_LABEL\\[[80..95\\]]))" -c)
>> ])
>> AT_CHECK([
>> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=25 | \
>> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=26 | \
>>     grep "priority=200" | \
>>     grep "actions=move:NXM_NX_CT_LABEL\\[[32..79\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
>> ])
>> 
>> AT_CHECK([
>> -    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=17 | \
>> +    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=18 | \
>>     grep "priority=100" | \
>>     grep "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_LABEL\\[[80..95\\]]))" -c)
>> ])
>> AT_CHECK([
>> -    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=25 | \
>> +    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=26 | \
>>     grep "priority=200" | \
>>     grep "actions=move:NXM_NX_CT_LABEL\\[[32..79\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
>> ])
>> @@ -29051,11 +29085,11 @@ AT_CAPTURE_FILE([hv2flows])
>> 
>> AT_CHECK([
>>     for hv in 1 2; do
>> -        grep table=17 hv${hv}flows | \
>> +        grep table=18 hv${hv}flows | \
>>         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=28 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
>> @@ -29143,23 +29177,23 @@ check ovn-nbctl --wait=hv sync
>> 
>> # Ensure ECMP symmetric reply flows are not present on any hypervisor.
>> AT_CHECK([
>> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=17 | \
>> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=18 | \
>>     grep "priority=100" | \
>>     grep "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_LABEL\\[[80..95\\]]))" -c)
>> ])
>> AT_CHECK([
>> -    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=25 | \
>> +    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=28 | \
>>     grep "priority=200" | \
>>     grep "actions=move:NXM_NX_CT_LABEL\\[[32..79\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
>> ])
>> 
>> AT_CHECK([
>> -    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=17 | \
>> +    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=18 | \
>>     grep "priority=100" | \
>>     grep "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\\]]))" -c)
>> ])
>> AT_CHECK([
>> -    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=25 | \
>> +    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=28 | \
>>     grep "priority=200" | \
>>     grep "actions=move:NXM_NX_CT_LABEL\\[[\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
>> ])
>> @@ -29176,11 +29210,11 @@ AT_CAPTURE_FILE([hv2flows])
>> 
>> AT_CHECK([
>>     for hv in 1 2; do
>> -        grep table=17 hv${hv}flows | \
>> +        grep table=18 hv${hv}flows | \
>>         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=28 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
>> @@ -29677,7 +29711,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=28, n_packets=1,.* priority=$prio,ip,$inport.*$outport.*metadata=0x${sw_key},nw_dst=10.0.1.1 actions=drop" -c], [0], [dnl
>> 1
>> ])
>> 
>> @@ -29696,13 +29730,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=28, 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=28, 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])
>> @@ -34645,7 +34679,7 @@ check ovn-nbctl set nb_global . options:use_common_zone="true"
>> check ovn-nbctl --wait=hv sync
>> # Use constants so that if tables or registers change, this test can
>> # be updated easily.
>> -DNAT_TABLE=15
>> +DNAT_TABLE=16
>> SNAT_TABLE=45
>> DNAT_ZONE_REG="NXM_NX_REG11[[0..15]]"
>> SNAT_ZONE_REG="NXM_NX_REG12[[0..15]]"
>> @@ -38088,3 +38122,222 @@ OVS_WAIT_UNTIL([test 1 = $(as hv ovs-ofctl dump-flows br-int | grep -E "pkt_mark
>> OVN_CLEANUP([hv])
>> AT_CLEANUP
>> ])
>> +
>> +OVN_FOR_EACH_NORTHD([
>> +AT_SETUP([DHCP RELAY])
>> +ovn_start
>> +net_add n1
>> +
>> +AT_CHECK([ovn-nbctl ls-add ls0])
>> +AT_CHECK([ovn-nbctl lsp-add ls0 vif0])
>> +AT_CHECK([ovn-nbctl lsp-set-addresses vif0 "50:54:00:00:00:10"])
>> +AT_CHECK([ovn-nbctl lsp-add ls0 lrp1-attachment])
>> +AT_CHECK([ovn-nbctl lsp-set-type lrp1-attachment router])
>> +AT_CHECK([ovn-nbctl lsp-set-addresses lrp1-attachment 50:54:00:00:00:01])
>> +AT_CHECK([ovn-nbctl lsp-set-options lrp1-attachment router-port=lrp1])
>> +
>> +AT_CHECK([ovn-nbctl lr-add lr0])
>> +AT_CHECK([ovn-nbctl lrp-add lr0 lrp1 50:54:00:00:00:01 192.168.1.1/24])
>> +AT_CHECK([ovn-nbctl lrp-add lr0 lrp2 50:54:00:00:00:02 172.16.1.254/24])
>> +
>> +AT_CHECK([ovn-nbctl ls-add ls-ext])
>> +AT_CHECK([ovn-nbctl lsp-add ls-ext lrp2-attachment])
>> +AT_CHECK([ovn-nbctl lsp-set-type lrp2-attachment router])
>> +AT_CHECK([ovn-nbctl lsp-set-addresses lrp2-attachment 50:54:00:00:00:02])
>> +AT_CHECK([ovn-nbctl lsp-set-options lrp2-attachment router-port=lrp2])
>> +AT_CHECK([ovn-nbctl lsp-add ls-ext ln_port])
>> +AT_CHECK([ovn-nbctl lsp-set-addresses ln_port unknown])
>> +AT_CHECK([ovn-nbctl lsp-set-type ln_port localnet])
>> +AT_CHECK([ovn-nbctl lsp-set-options ln_port network_name=physnet1])
>> +
>> +dhcp_relay=$(ovn-nbctl create DHCP_Relay servers=172.16.1.1)
>> +AT_CHECK([ovn-nbctl set Logical_Router_port lrp1 dhcp_relay=$dhcp_relay])
>> +AT_CHECK([ovn-nbctl set Logical_Switch ls0 other_config:dhcp_relay_port=lrp1-attachment])
>> +
>> +sim_add hv1
>> +as hv1
>> +ovs-vsctl add-br br-phys
>> +ovn_attach n1 br-phys 192.168.0.1
>> +ovs-vsctl -- add-port br-int vif0 -- \
>> +    set interface vif0 external-ids:iface-id=vif0 \
>> +    options:tx_pcap=hv1/vif0-tx.pcap \
>> +    options:rxq_pcap=hv1/vif0-rx.pcap \
>> +    ofport-request=1
>> +ovs-vsctl -- add-port br-phys ext0 -- \
>> +    set interface ext0 \
>> +    options:tx_pcap=hv1/ext0-tx.pcap \
>> +    options:rxq_pcap=hv1/ext0-rx.pcap \
>> +    ofport-request=2
>> +
>> +ovs-vsctl set open . external_ids:ovn-bridge-mappings=physnet1:br-phys
>> +
>> +wait_for_ports_up
>> +AT_CHECK([ovn-nbctl --wait=hv sync])
>> +
>> +send_dhcp_packet() {
>> +    src_mac=${1}
>> +    src_ip=${2}
>> +    dst_mac=${3}
>> +    dst_ip=${4}
>> +    op_code=${5}
>> +    msg_type=${6}
>> +    yiaddr=$7
>> +    giaddr=${8}
>> +    sid=${9}
>> +    iface=${10}
>> +    #echo "ARGS: ${1} ${2} ${3} ${4} ${5} ${6} ${7} ${8} ${9} ${10}"
>> +    echo "ARGS: $@"
>> +    if [[ $op_code == "01" ]]; then
>> +        ip_len=0111
>> +        udp_len=00fd
>> +        src_port=0044
>> +    else
>> +        ip_len=011d
>> +        udp_len=0109
>> +        src_port=0043
>> +    fi
>> +    flags=0000
>> +
>> +    local pkt=${dst_mac}${src_mac}08004510${ip_len}0000000080110000${src_ip}${dst_ip}
>> +    # udp header and dhcp header
>> +    pkt=${pkt}${src_port}0043${udp_len}0000
>> +    pkt=${pkt}${op_code}0106006359aa760000${flags}00000000${yiaddr}00000000${giaddr}${src_mac}
>> +    # client hardware padding
>> +    pkt=${pkt}00000000000000000000
>> +    # server hostname
>> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
>> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
>> +    # boot file name
>> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
>> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
>> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
>> +    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
>> +    # dhcp magic cookie
>> +    pkt=${pkt}63825363
>> +    # dhcp message type
>> +    pkt=${pkt}3501${msg_type}
>> +    # dhcp server identifier and subnet mask options
>> +    if  [[ $op_code == "02" ]]; then
>> +        pkt=${pkt}3604${sid}
>> +        pkt=${pkt}0104ffffff00
>> +    fi
>> +    # dhcp pad option
>> +    pkt=${pkt}00
>> +    # dhcp end option
>> +    pkt=${pkt}ff
>> +
>> +    tcpdump_hex "-- sending DHCP pkt on hv1-$iface" $pkt
>> +
>> +    ovs-appctl netdev-dummy/receive $iface $pkt
>> +}
>> +
>> +
>> +# Check that there is commit_fdb_local_fdb() flow added by ovn-northd for vif0 and localnet
>> +ovn-sbctl dump-flows > lflows
>> +AT_CAPTURE_FILE([lflows])
>> +
>> +# ====================================================
>> +# Send DHCP valid discovery
>> +src_mac="505400000010"
>> +src_ip=`ip_to_hex 0.0.0.0`
>> +dst_mac="ffffffffffff"
>> +dst_ip=`ip_to_hex 255.255.255.255`
>> +yiaddr=`ip_to_hex 0.0.0.0`
>> +giaddr=`ip_to_hex 0.0.0.0`
>> +sid=$src_ip
>> +# send packet
>> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 01 01 $yiaddr $giaddr $sid vif0
>> +
>> +ovs-ofctl dump-flows br-int table=12 > pflows1_dhcp_relay_req
>> +AT_CAPTURE_FILE([pflows1_dhcp_relay_req])
>> +
>> +AT_CHECK([cat pflows1_dhcp_relay_req | grep -v NXST | grep 255.255.255.255 | grep resubmit |
>> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
>> +n_packets=1
>> +])
>> +
>> +# ====================================================
>> +# Send DHCP discovery with giaddr set
>> +giaddr=`ip_to_hex 192.168.1.1`
>> +# send packet
>> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 01 01 $yiaddr $giaddr $sid vif0
>> +
>> +ovs-ofctl dump-flows br-int table=12 > pflows2_dhcp_relay_req
>> +AT_CAPTURE_FILE([pflows2_dhcp_relay_req])
>> +
>> +AT_CHECK([cat pflows2_dhcp_relay_req | grep -v NXST | grep 255.255.255.255 | grep drop |
>> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
>> +n_packets=1
>> +])
>> +
>> +# ====================================================
>> +# Send DHCP valid offer
>> +src_mac="50540000001f"
>> +src_ip=`ip_to_hex 172.16.1.1`
>> +dst_mac="505400000002"
>> +dst_ip=`ip_to_hex 192.168.1.1`
>> +yiaddr=`ip_to_hex 192.168.1.10`
>> +giaddr=`ip_to_hex 192.168.1.1`
>> +sid=$src_ip
>> +# send packet
>> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
>> +
>> +ovs-ofctl dump-flows br-int table=27 > pflows1_dhcp_relay_resp
>> +AT_CAPTURE_FILE([pflows1_dhcp_relay_resp])
>> +
>> +AT_CHECK([cat pflows1_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep resubmit |
>> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
>> +n_packets=1
>> +])
>> +# ====================================================
>> +# Send DHCP offer with incorrect giaddr
>> +giaddr=`ip_to_hex 192.168.1.10`
>> +# send packet
>> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
>> +
>> +ovs-ofctl dump-flows br-int table=27 > pflows2_dhcp_relay_resp
>> +AT_CAPTURE_FILE([pflows2_dhcp_relay_resp])
>> +
>> +AT_CHECK([cat pflows2_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep drop |
>> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
>> +n_packets=1
>> +])
>> +
>> +giaddr=`ip_to_hex 192.168.1.1`
>> +
>> +# ====================================================
>> +# Send DHCP offer with yiaddr outside of the subnet
>> +yiaddr=`ip_to_hex 192.168.2.10`
>> +# send packet
>> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
>> +
>> +ovs-ofctl dump-flows br-int table=27 > pflows2_dhcp_relay_resp
>> +AT_CAPTURE_FILE([pflows2_dhcp_relay_resp])
>> +
>> +AT_CHECK([cat pflows2_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep drop |
>> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
>> +n_packets=2
>> +])
>> +
>> +yiaddr=`ip_to_hex 192.168.1.10`
>> +
>> +# ====================================================
>> +# Send DHCP offer with differnt server identifier
>> +sid=`ip_to_hex 172.16.1.100`
>> +# send packet
>> +send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
>> +
>> +ovs-ofctl dump-flows br-int table=27 > pflows2_dhcp_relay_resp
>> +AT_CAPTURE_FILE([pflows2_dhcp_relay_resp])
>> +
>> +AT_CHECK([cat pflows2_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep drop |
>> +cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
>> +n_packets=3
>> +])
>> +
>> +sid=`ip_to_hex 172.16.1.1`
>> +
>> +OVN_CLEANUP([hv1])
>> +AT_CLEANUP
>> +])
>> +
>> diff --git a/tests/system-ovn.at b/tests/system-ovn.at
>> index c22c7882f..e7fa79727 100644
>> --- a/tests/system-ovn.at
>> +++ b/tests/system-ovn.at
>> @@ -12184,3 +12184,151 @@ OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
>> /connection dropped.*/d"])
>> AT_CLEANUP
>> ])
>> +
>> +OVN_FOR_EACH_NORTHD([
>> +AT_SETUP([DHCP RELAY])
>> +AT_SKIP_IF([test $HAVE_DHCPD = no])
>> +AT_SKIP_IF([test $HAVE_DHCLIENT = no])
>> +AT_SKIP_IF([test $HAVE_TCPDUMP = no])
>> +ovn_start
>> +OVS_TRAFFIC_VSWITCHD_START()
>> +
>> +ADD_BR([br-int])
>> +ADD_BR([br-ext])
>> +
>> +ovs-ofctl add-flow br-ext action=normal
>> +# Set external-ids in br-int needed for ovn-controller
>> +ovs-vsctl \
>> +        -- set Open_vSwitch . external-ids:system-id=hv1 \
>> +        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
>> +        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
>> +        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
>> +        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
>> +
>> +# Start ovn-controller
>> +start_daemon ovn-controller
>> +
>> +ADD_NAMESPACES(sw01)
>> +ADD_VETH(sw01, sw01, br-int, "0", "f0:00:00:01:02:03")
>> +ADD_NAMESPACES(sw11)
>> +ADD_VETH(sw11, sw11, br-int, "0", "f0:00:00:02:02:03")
>> +ADD_NAMESPACES(server)
>> +ADD_VETH(s1, server, br-ext, "172.16.1.1/24", "f0:00:00:01:02:05", \
>> +         "172.16.1.254")
>> +
>> +check ovn-nbctl lr-add R1
>> +
>> +check ovn-nbctl ls-add sw0
>> +check ovn-nbctl ls-add sw1
>> +check ovn-nbctl ls-add sw-ext
>> +
>> +check ovn-nbctl lrp-add R1 rp-sw0 00:00:01:01:02:03 192.168.1.1/24
>> +check ovn-nbctl lrp-add R1 rp-sw1 00:00:03:01:02:03 192.168.2.1/24
>> +check ovn-nbctl lrp-add R1 rp-ext 00:00:02:01:02:03 172.16.1.254/24
>> +
>> +dhcp_relay=$(ovn-nbctl create DHCP_Relay servers=172.16.1.1)
>> +check ovn-nbctl set Logical_Router_port rp-sw0 dhcp_relay=$dhcp_relay
>> +check ovn-nbctl set Logical_Router_port rp-sw1 dhcp_relay=$dhcp_relay
>> +check ovn-nbctl lrp-set-gateway-chassis rp-ext hv1
>> +
>> +check ovn-nbctl lsp-add sw0 sw0-rp -- set Logical_Switch_Port sw0-rp \
>> +    type=router options:router-port=rp-sw0 \
>> +    -- lsp-set-addresses sw0-rp router
>> +check ovn-nbctl lsp-add sw1 sw1-rp -- set Logical_Switch_Port sw1-rp \
>> +    type=router options:router-port=rp-sw1 \
>> +    -- lsp-set-addresses sw1-rp router
>> +
>> +check ovn-nbctl set Logical_Switch sw0 other_config:dhcp_relay_port=sw0-rp
>> +check ovn-nbctl set Logical_Switch sw1 other_config:dhcp_relay_port=sw1-rp
>> +
>> +check ovn-nbctl lsp-add sw-ext ext-rp -- set Logical_Switch_Port ext-rp \
>> +    type=router options:router-port=rp-ext \
>> +    -- lsp-set-addresses ext-rp router
>> +check ovn-nbctl lsp-add sw-ext lnet \
>> +        -- lsp-set-addresses lnet unknown \
>> +        -- lsp-set-type lnet localnet \
>> +        -- lsp-set-options lnet network_name=phynet
>> +
>> +check ovn-nbctl lsp-add sw0 sw01 \
>> +    -- lsp-set-addresses sw01 "f0:00:00:01:02:03"
>> +
>> +check ovn-nbctl lsp-add sw1 sw11 \
>> +    -- lsp-set-addresses sw11 "f0:00:00:02:02:03"
>> +
>> +AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext])
>> +
>> +OVN_POPULATE_ARP
>> +
>> +check ovn-nbctl --wait=hv sync
>> +
>> +DHCP_TEST_DIR="/tmp/dhcp-test"
>> +rm -rf $DHCP_TEST_DIR
>> +mkdir $DHCP_TEST_DIR
>> +cat > $DHCP_TEST_DIR/dhcpd.conf <<EOF
>> +subnet 172.16.1.0 netmask 255.255.255.0 {
>> +}
>> +subnet 192.168.1.0 netmask 255.255.255.0 {
>> +  range 192.168.1.10 192.168.1.10;
>> +  option routers 192.168.1.1;
>> +  option broadcast-address 192.168.1.255;
>> +  default-lease-time 60;
>> +  max-lease-time 120;
>> +}
>> +subnet 192.168.2.0 netmask 255.255.255.0 {
>> +  range 192.168.2.10 192.168.2.10;
>> +  option routers 192.168.2.1;
>> +  option broadcast-address 192.168.2.255;
>> +  default-lease-time 60;
>> +  max-lease-time 120;
>> +}
>> +EOF
>> +cat > $DHCP_TEST_DIR/dhclien.conf <<EOF
>> +timeout 2
>> +EOF
>> +
>> +touch $DHCP_TEST_DIR/dhcpd.leases
>> +chown root:dhcpd $DHCP_TEST_DIR $DHCP_TEST_DIR/dhcpd.leases
>> +chmod 775 $DHCP_TEST_DIR
>> +chmod 664 $DHCP_TEST_DIR/dhcpd.leases
>> +
>> +
>> +NETNS_DAEMONIZE([server], [dhcpd -4 -f -cf $DHCP_TEST_DIR/dhcpd.conf s1 > dhcpd.log 2>&1], [dhcpd.pid])
>> +
>> +NS_CHECK_EXEC([server], [tcpdump -l -nvv -i s1  udp > pkt.pcap 2>tcpdump_err &])
>> +OVS_WAIT_UNTIL([grep "listening" tcpdump_err])
>> +on_exit 'kill $(pidof tcpdump)'
>> +
>> +NS_CHECK_EXEC([sw01], [dhclient -1 -q -lf $DHCP_TEST_DIR/dhclient-sw01.lease -pf $DHCP_TEST_DIR/dhclient-sw01.pid -cf $DHCP_TEST_DIR/dhclien.conf sw01])
>> +NS_CHECK_EXEC([sw11], [dhclient -1 -q -lf $DHCP_TEST_DIR/dhclient-sw11.lease -pf $DHCP_TEST_DIR/dhclient-sw11.pid -cf $DHCP_TEST_DIR/dhclien.conf sw11])
>> +
>> +OVS_WAIT_UNTIL([
>> +    total_pkts=$(cat pkt.pcap | wc -l)
>> +    test ${total_pkts} -ge 8
>> +])
>> +
>> +on_exit 'kill `cat $DHCP_TEST_DIR/dhclient-sw01.pid` &&
>> +kill `cat $DHCP_TEST_DIR/dhclient-sw11.pid` && rm -rf $DHCP_TEST_DIR'
>> +
>> +NS_CHECK_EXEC([sw01], [ip addr show sw01 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'], [0], [dnl
>> +192.168.1.10
>> +])
>> +NS_CHECK_EXEC([sw11], [ip addr show sw11 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'], [0], [dnl
>> +192.168.2.10
>> +])
>> +OVS_APP_EXIT_AND_WAIT([ovn-controller])
>> +
>> +as ovn-sb
>> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
>> +
>> +as ovn-nb
>> +OVS_APP_EXIT_AND_WAIT([ovsdb-server])
>> +
>> +as northd
>> +OVS_APP_EXIT_AND_WAIT([ovn-northd])
>> +
>> +as
>> +OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
>> +/failed to query port patch-.*/d
>> +/.*terminating with signal 15.*/d"])
>> +AT_CLEANUP
>> +])
>> diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
>> index e0f1c3ec9..903457d45 100644
>> --- a/utilities/ovn-trace.c
>> +++ b/utilities/ovn-trace.c
>> @@ -2329,6 +2329,63 @@ execute_put_dhcp_opts(const struct ovnact_put_opts *pdo,
>>     execute_put_opts(pdo, name, uflow, super);
>> }
>> 
>> +static void
>> +execute_dhcpv4_relay_req_chk(const struct ovnact_dhcp_relay *dr,
>> +                             struct flow *uflow, struct ovs_list *super)
>> +{
>> +    ovntrace_node_append(
>> +        super, OVNTRACE_NODE_ERROR,
>> +        "/* We assume that this packet is DHCPDISCOVER or DHCPREQUEST. */");
>> +
>> +    struct ds s = DS_EMPTY_INITIALIZER;
>> +    struct mf_subfield dst = expr_resolve_field(&dr->dst);
>> +    if (!mf_is_register(dst.field->id)) {
>> +        /* Format assignment. */
>> +        ds_clear(&s);
>> +        expr_field_format(&dr->dst, &s);
>> +        ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
>> +                             "%s = 1", ds_cstr(&s));
>> +    }
>> +    ds_destroy(&s);
>> +
>> +    struct mf_subfield sf = expr_resolve_field(&dr->dst);
>> +    union mf_subvalue sv = { .u8_val = 1 };
>> +    mf_write_subfield_flow(&sf, &sv, uflow);
>> +}
>> +
>> +static void
>> +execute_dhcpv4_relay_resp_chk(const struct ovnact_dhcp_relay *dr,
>> +                              struct flow *uflow, struct ovs_list *super)
>> +{
>> +    ovntrace_node_append(
>> +        super, OVNTRACE_NODE_ERROR,
>> +        "/* We assume that this packet is DHCPOFFER or DHCPACK and "
>> +            "DHCP broadcast flag is set. Dest IP is set to broadcast. "
>> +            "Dest MAC is set to broadcast but in real network this is unicast "
>> +            "which is extracted from DHCP header. */");
>> +
>> +    /* Assume DHCP broadcast flag is set */
>> +    uflow->nw_dst = 0xFFFFFFFF;
>> +    /* Dest MAC is set to broadcast but in real network this is unicast */
>> +    struct eth_addr bcast_mac = { .ea = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
>> +    uflow->dl_dst = bcast_mac;
>> +
>> +    struct ds s = DS_EMPTY_INITIALIZER;
>> +    struct mf_subfield dst = expr_resolve_field(&dr->dst);
>> +    if (!mf_is_register(dst.field->id)) {
>> +        /* Format assignment. */
>> +        ds_clear(&s);
>> +        expr_field_format(&dr->dst, &s);
>> +        ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
>> +                             "%s = 1", ds_cstr(&s));
>> +    }
>> +    ds_destroy(&s);
>> +
>> +    struct mf_subfield sf = expr_resolve_field(&dr->dst);
>> +    union mf_subvalue sv = { .u8_val = 1 };
>> +    mf_write_subfield_flow(&sf, &sv, uflow);
>> +}
>> +
>> static void
>> execute_put_nd_ra_opts(const struct ovnact_put_opts *pdo,
>>                        const char *name, struct flow *uflow,
>> @@ -3216,6 +3273,16 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
>>                                   "put_dhcpv6_opts", uflow, super);
>>             break;
>> 
>> +        case OVNACT_DHCPV4_RELAY_REQ_CHK:
>> +            execute_dhcpv4_relay_req_chk(ovnact_get_DHCPV4_RELAY_REQ_CHK(a),
>> +                                    uflow, super);
>> +            break;
>> +
>> +        case OVNACT_DHCPV4_RELAY_RESP_CHK:
>> +            execute_dhcpv4_relay_resp_chk(ovnact_get_DHCPV4_RELAY_RESP_CHK(a),
>> +                                    uflow, super);
>> +            break;
>> +
>>         case OVNACT_PUT_ND_RA_OPTS:
>>             execute_put_nd_ra_opts(ovnact_get_PUT_DHCPV6_OPTS(a),
>>                                    "put_nd_ra_opts", uflow, super);
>> --
>> 2.36.6
>> 
>> _______________________________________________
>> dev mailing list
>> dev@openvswitch.org
>> https://urldefense.proofpoint.com/v2/url?u=https-3A__mail.openvswitch.org_mailman_listinfo_ovs-2Ddev&d=DwIFaQ&c=s883GpUCOChKOHiocYtGcg&r=2PQjSDR7A28z1kXE1ptSm6X36oL_nCq1XxeEt7FkLmA&m=1fvmfSC4tEXMfJOktAtv3fWxnnwNdu9mfHYf31u8044u5ShtRTd0dIRyz3VTyIyR&s=HCUlFDHK1FJ9m6exzM3_Ba2fFqS36fz6rrvb35phnP4&e=
diff mbox series

Patch

diff --git a/controller/pinctrl.c b/controller/pinctrl.c
index 98b29de9f..a776ac7c5 100644
--- a/controller/pinctrl.c
+++ b/controller/pinctrl.c
@@ -1909,6 +1909,514 @@  is_dhcp_flags_broadcast(ovs_be16 flags)
     return flags & htons(DHCP_BROADCAST_FLAG);
 }
 
+static const char *dhcp_msg_str[] = {
+[0] = "INVALID",
+[DHCP_MSG_DISCOVER] = "DISCOVER",
+[DHCP_MSG_OFFER] = "OFFER",
+[DHCP_MSG_REQUEST] = "REQUEST",
+[OVN_DHCP_MSG_DECLINE] = "DECLINE",
+[DHCP_MSG_ACK] = "ACK",
+[DHCP_MSG_NAK] = "NAK",
+[OVN_DHCP_MSG_RELEASE] = "RELEASE",
+[OVN_DHCP_MSG_INFORM] = "INFORM"
+};
+
+static bool
+dhcp_relay_is_msg_type_supported(uint8_t msg_type)
+{
+    return (msg_type >= DHCP_MSG_DISCOVER && msg_type <= OVN_DHCP_MSG_RELEASE);
+}
+
+static const char *dhcp_msg_str_get(uint8_t msg_type)
+{
+    if (!dhcp_relay_is_msg_type_supported(msg_type)) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "Unknown DHCP msg type: %u", msg_type);
+        return "UNKNOWN";
+    }
+    return dhcp_msg_str[msg_type];
+}
+
+static const struct dhcp_header *
+dhcp_get_hdr_from_pkt(struct dp_packet *pkt_in, const char **in_dhcp_pptr,
+                  const char *end)
+{
+    /* Validate the DHCP request packet.
+     * Format of the DHCP packet is
+     * ------------------------------------------------------------------------
+     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var len)|
+     * ------------------------------------------------------------------------
+     */
+
+    *in_dhcp_pptr = dp_packet_get_udp_payload(pkt_in);
+    if (*in_dhcp_pptr == NULL) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP: Invalid or incomplete DHCP packet received");
+        return NULL;
+    }
+
+    const struct dhcp_header *dhcp_hdr
+        = (const struct dhcp_header *) *in_dhcp_pptr;
+    (*in_dhcp_pptr) += sizeof *dhcp_hdr;
+    if (*in_dhcp_pptr > end) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP: Invalid or incomplete DHCP packet received, "
+                     "bad data length");
+        return NULL;
+    }
+
+    if (dhcp_hdr->htype != 0x1) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP: Packet is recieved with "
+                "unsupported hardware type");
+        return NULL;
+    }
+
+    if (dhcp_hdr->hlen != 0x6) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP: Packet is recieved with "
+                "unsupported hardware length");
+        return NULL;
+    }
+
+    /* 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_pptr) + sizeof magic_cookie > end ||
+        get_unaligned_be32((const void *) (*in_dhcp_pptr)) != magic_cookie) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP: Magic cookie not present in the DHCP packet");
+        return NULL;
+    }
+
+    (*in_dhcp_pptr) += sizeof magic_cookie;
+
+    return dhcp_hdr;
+}
+
+static void
+dhcp_parse_options(const char **in_dhcp_pptr, const char *end,
+          const uint8_t **dhcp_msg_type_pptr, ovs_be32 *request_ip_ptr,
+          bool *ipxe_req_ptr, ovs_be32 *server_id_ptr,
+          ovs_be32 *netmask_ptr, ovs_be32 *router_ip_ptr)
+{
+    while ((*in_dhcp_pptr) < end) {
+        const struct dhcp_opt_header *in_dhcp_opt =
+            (const struct dhcp_opt_header *) *in_dhcp_pptr;
+        if (in_dhcp_opt->code == DHCP_OPT_END) {
+            break;
+        }
+        if (in_dhcp_opt->code == DHCP_OPT_PAD) {
+            (*in_dhcp_pptr) += 1;
+            continue;
+        }
+        (*in_dhcp_pptr) += sizeof *in_dhcp_opt;
+        if ((*in_dhcp_pptr) > end) {
+            break;
+        }
+        (*in_dhcp_pptr) += in_dhcp_opt->len;
+        if ((*in_dhcp_pptr) > end) {
+            break;
+        }
+
+        switch (in_dhcp_opt->code) {
+        case DHCP_OPT_MSG_TYPE:
+            if (dhcp_msg_type_pptr && in_dhcp_opt->len == 1) {
+                *dhcp_msg_type_pptr = DHCP_OPT_PAYLOAD(in_dhcp_opt);
+            }
+            break;
+        case DHCP_OPT_REQ_IP:
+            if (request_ip_ptr && in_dhcp_opt->len == 4) {
+                *request_ip_ptr = get_unaligned_be32(
+                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
+            }
+            break;
+        case OVN_DHCP_OPT_CODE_SERVER_ID:
+            if (server_id_ptr && in_dhcp_opt->len == 4) {
+                *server_id_ptr = get_unaligned_be32(
+                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
+            }
+            break;
+        case OVN_DHCP_OPT_CODE_NETMASK:
+            if (netmask_ptr && in_dhcp_opt->len == 4) {
+                *netmask_ptr = get_unaligned_be32(
+                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
+            }
+            break;
+        case OVN_DHCP_OPT_CODE_ROUTER_IP:
+            if (router_ip_ptr && in_dhcp_opt->len == 4) {
+                *router_ip_ptr = get_unaligned_be32(
+                                  DHCP_OPT_PAYLOAD(in_dhcp_opt));
+            }
+            break;
+        case DHCP_OPT_ETHERBOOT:
+            if (ipxe_req_ptr) {
+                *ipxe_req_ptr = true;
+            }
+            break;
+        default:
+            break;
+        }
+    }
+}
+
+/* Called with in the pinctrl_handler thread context. */
+static void
+pinctrl_handle_dhcp_relay_req_chk(
+    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;
+    uint32_t success = 0;
+
+    /* Parse result field. */
+    const struct mf_field *f;
+    enum ofperr ofperr = nx_pull_header(userdata, NULL, &f, NULL);
+    if (ofperr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: bad result OXM (%s)",
+                        ofperr_to_string(ofperr));
+        goto exit;
+    }
+    ovs_be32 *ofsp = ofpbuf_try_pull(userdata, sizeof *ofsp);
+    /* Check that the result is valid and writable. */
+    struct mf_subfield dst = { .field = f, .ofs = ntohl(*ofsp), .n_bits = 1 };
+    ofperr = mf_check_dst(&dst, NULL);
+    if (ofperr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: bad result bit (%s)",
+                        ofperr_to_string(ofperr));
+        goto exit;
+    }
+
+    /* Parse relay IP and server IP. */
+    ovs_be32 *relay_ip = ofpbuf_try_pull(userdata, sizeof *relay_ip);
+    ovs_be32 *server_ip = ofpbuf_try_pull(userdata, sizeof *server_ip);
+    if (!relay_ip || !server_ip) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: relay ip or server ip "
+                  "not present in the userdata");
+        goto exit;
+    }
+
+    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 = NULL;
+    const struct dhcp_header *in_dhcp_data
+                          = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
+
+    if (!in_dhcp_data) {
+        goto exit;
+    }
+    ovs_assert(in_dhcp_ptr);
+
+    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_RELAY_REQ_CHK: invalid opcode in the "
+                "DHCP packet: %d", in_dhcp_data->op);
+        goto exit;
+    }
+
+    if (in_dhcp_data->giaddr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_REQ_CHK: giaddr is already set");
+        goto exit;
+    }
+
+    const uint8_t *in_dhcp_msg_type = NULL;
+    ovs_be32 request_ip = in_dhcp_data->ciaddr;
+
+    dhcp_parse_options(&in_dhcp_ptr, end,
+          &in_dhcp_msg_type, &request_ip, NULL, NULL, NULL, NULL);
+
+    /* 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_RELAY_REQ_CHK: missing message type");
+        goto exit;
+    }
+
+    /* Relay the DHCP request packet */
+    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 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);
+
+    uint8_t hops = dhcp_data->hops;
+    dhcp_data->hops = hops + 1;
+    if (udp->udp_csum) {
+        udp->udp_csum = recalc_csum16(udp->udp_csum,
+            (uint16_t) (hops << 8), (uint16_t) (dhcp_data->hops << 8));
+    }
+
+    dhcp_data->giaddr = *relay_ip;
+    if (udp->udp_csum) {
+        udp->udp_csum = recalc_csum32(udp->udp_csum,
+            0, dhcp_data->giaddr);
+    }
+    pin->packet = dp_packet_data(&pkt_out);
+    pin->packet_len = dp_packet_size(&pkt_out);
+
+    /* Log the DHCP message. */
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(20, 40);
+    VLOG_INFO_RL(&rl, "DHCP_RELAY_REQ_CHK:: MSG_TYPE:%s MAC:"ETH_ADDR_FMT
+                " XID:%u"
+                " REQ_IP:"IP_FMT
+                " GIADDR:"IP_FMT
+                " SERVER_ADDR:"IP_FMT,
+                dhcp_msg_str_get(*in_dhcp_msg_type),
+                ETH_ADDR_BYTES_ARGS(dhcp_data->chaddr), ntohl(dhcp_data->xid),
+                IP_ARGS(request_ip), IP_ARGS(dhcp_data->giaddr),
+                IP_ARGS(*server_ip));
+    success = 1;
+exit:
+    if (!ofperr) {
+        union mf_subvalue sv;
+        sv.u8_val = success;
+        mf_write_subfield(&dst, &sv, &pin->flow_metadata);
+    }
+    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_relay_resp_chk(
+    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;
+    uint32_t success = 0;
+
+    /* Parse result field. */
+    const struct mf_field *f;
+    enum ofperr ofperr = nx_pull_header(userdata, NULL, &f, NULL);
+    if (ofperr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: bad result OXM (%s)",
+                        ofperr_to_string(ofperr));
+        goto exit;
+    }
+    ovs_be32 *ofsp = ofpbuf_try_pull(userdata, sizeof *ofsp);
+    /* Check that the result is valid and writable. */
+    struct mf_subfield dst = { .field = f, .ofs = ntohl(*ofsp), .n_bits = 1 };
+    ofperr = mf_check_dst(&dst, NULL);
+    if (ofperr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: bad result bit (%s)",
+                        ofperr_to_string(ofperr));
+        goto exit;
+    }
+
+    /* Parse relay IP and server IP. */
+    ovs_be32 *relay_ip = ofpbuf_try_pull(userdata, sizeof *relay_ip);
+    ovs_be32 *server_ip = ofpbuf_try_pull(userdata, sizeof *server_ip);
+    if (!relay_ip || !server_ip) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: relay ip or server ip "
+                "not present in the userdata");
+        goto exit;
+    }
+
+    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 = NULL;
+    const struct dhcp_header *in_dhcp_data
+                          = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
+
+    if (!in_dhcp_data) {
+        goto exit;
+    }
+    ovs_assert(in_dhcp_ptr);
+
+    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_RELAY_RESP_CHK: invalid opcode "
+                "in the packet: %d", in_dhcp_data->op);
+        goto exit;
+    }
+
+    if (!in_dhcp_data->giaddr) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP_CHK: giaddr is "
+                    "not set in request");
+        goto exit;
+    }
+
+    ovs_be32 giaddr = in_dhcp_data->giaddr;
+    ovs_be32 yiaddr = in_dhcp_data->yiaddr;
+    ovs_be32 server_id = 0, netmask = 0, router_ip = 0;
+    const uint8_t *in_dhcp_msg_type = NULL;
+
+    dhcp_parse_options(&in_dhcp_ptr, end,
+          &in_dhcp_msg_type, NULL, NULL, &server_id, &netmask, &router_ip);
+
+    /* 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_RELAY_RESP: missing message type");
+        goto exit;
+    }
+
+    if (!server_id) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP: missing server identifier");
+        goto exit;
+    }
+
+    if (server_id != *server_ip) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP: server identifier mismatch");
+        goto exit;
+    }
+
+    if (giaddr != *relay_ip) {
+        static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+        VLOG_WARN_RL(&rl, "DHCP_RELAY_RESP: giaddr mismatch");
+        goto exit;
+    }
+
+    if (*in_dhcp_msg_type == DHCP_MSG_OFFER ||
+        *in_dhcp_msg_type == DHCP_MSG_ACK) {
+        if ((yiaddr & netmask) != (giaddr & netmask)) {
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_INFO_RL(&rl, "DHCP_RELAY_RESP_CHK::"
+                     " Allocated ip adress and giaddr are not in same subnet."
+                     " MSG_TYPE:%s MAC:"ETH_ADDR_FMT
+                     " XID:%u"
+                     " YIADDR:"IP_FMT
+                     " GIADDR:"IP_FMT
+                     " SERVER_ADDR:"IP_FMT,
+                     dhcp_msg_str_get(*in_dhcp_msg_type),
+                     ETH_ADDR_BYTES_ARGS(in_dhcp_data->chaddr),
+                     ntohl(in_dhcp_data->xid),
+                     IP_ARGS(yiaddr),
+                     IP_ARGS(giaddr), IP_ARGS(server_id));
+            goto exit;
+        }
+
+        if (router_ip && router_ip != giaddr) {
+            /* Log the default gateway mismatch and
+             * continue with rest of the processing */
+            static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
+            VLOG_INFO_RL(&rl, "DHCP_RELAY_RESP_CHK::"
+                     " Router ip adress and giaddr are not same."
+                     " MSG_TYPE:%s MAC:"ETH_ADDR_FMT
+                     " XID:%u"
+                     " YIADDR:"IP_FMT
+                     " GIADDR:"IP_FMT
+                     " SERVER_ADDR:"IP_FMT,
+                     dhcp_msg_str_get(*in_dhcp_msg_type),
+                     ETH_ADDR_BYTES_ARGS(in_dhcp_data->chaddr),
+                     ntohl(in_dhcp_data->xid),
+                     IP_ARGS(yiaddr),
+                     IP_ARGS(giaddr), IP_ARGS(server_id));
+        }
+    }
+
+    /* 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);
+
+    /* Log the DHCP message. */
+    static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(20, 40);
+    VLOG_INFO_RL(&rl, "DHCP_RELAY_RESP_CHK:: MSG_TYPE:%s MAC:"ETH_ADDR_FMT
+             " XID:%u"
+             " YIADDR:"IP_FMT
+             " GIADDR:"IP_FMT
+             " SERVER_ADDR:"IP_FMT,
+             dhcp_msg_str_get(*in_dhcp_msg_type),
+             ETH_ADDR_BYTES_ARGS(dhcp_data->chaddr), ntohl(dhcp_data->xid),
+             IP_ARGS(yiaddr),
+             IP_ARGS(giaddr), IP_ARGS(server_id));
+    success = 1;
+exit:
+    if (!ofperr) {
+        union mf_subvalue sv;
+        sv.u8_val = success;
+        mf_write_subfield(&dst, &sv, &pin->flow_metadata);
+    }
+    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(
@@ -1956,30 +2464,16 @@  pinctrl_handle_put_dhcp_opts(
         goto exit;
     }
 
-    /* Validate the DHCP request packet.
-     * Format of the DHCP packet is
-     * ------------------------------------------------------------------------
-     *| UDP HEADER  | DHCP HEADER  | 4 Byte DHCP Cookie | DHCP OPTIONS(var len)|
-     * ------------------------------------------------------------------------
-     */
-
     const char *end = (char *)dp_packet_l4(pkt_in) + dp_packet_l4_size(pkt_in);
-    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, "Invalid or incomplete DHCP packet received");
-        goto exit;
-    }
-
+    const char *in_dhcp_ptr = NULL;
     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, "Invalid or incomplete DHCP packet received, "
-                     "bad data length");
+                          = dhcp_get_hdr_from_pkt(pkt_in, &in_dhcp_ptr, end);
+
+    if (!in_dhcp_data) {
         goto exit;
     }
+    ovs_assert(in_dhcp_ptr);
+
     if (in_dhcp_data->op != DHCP_OP_REQUEST) {
         static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(1, 5);
         VLOG_WARN_RL(&rl, "Invalid opcode in the DHCP packet: %d",
@@ -1987,58 +2481,11 @@  pinctrl_handle_put_dhcp_opts(
         goto exit;
     }
 
-    /* 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 magic cookie not present in the DHCP packet");
-        goto exit;
-    }
-    in_dhcp_ptr += sizeof magic_cookie;
-
     bool ipxe_req = false;
     const uint8_t *in_dhcp_msg_type = NULL;
     ovs_be32 request_ip = in_dhcp_data->ciaddr;
-    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 DHCP_OPT_REQ_IP:
-            if (in_dhcp_opt->len == 4) {
-                request_ip = get_unaligned_be32(DHCP_OPT_PAYLOAD(in_dhcp_opt));
-            }
-            break;
-        case DHCP_OPT_ETHERBOOT:
-            ipxe_req = true;
-            break;
-        default:
-            break;
-        }
-    }
+    dhcp_parse_options(&in_dhcp_ptr, end,
+          &in_dhcp_msg_type, &request_ip, &ipxe_req, NULL, NULL, NULL);
 
     /* Check that the DHCP Message Type (opt 53) is present or not with
      * valid values - DHCP_MSG_DISCOVER or DHCP_MSG_REQUEST.
@@ -2245,6 +2692,7 @@  pinctrl_handle_put_dhcp_opts(
         dhcp_data->yiaddr = 0;
     }
 
+    ovs_be32 magic_cookie = htonl(DHCP_MAGIC_COOKIE);
     dp_packet_put(&pkt_out, &magic_cookie, sizeof(ovs_be32));
 
     uint16_t out_dhcp_opts_size = 12;
@@ -3215,6 +3663,16 @@  process_packet_in(struct rconn *swconn, const struct ofp_header *msg)
         ovs_mutex_unlock(&pinctrl_mutex);
         break;
 
+    case ACTION_OPCODE_DHCP_RELAY_REQ_CHK:
+        pinctrl_handle_dhcp_relay_req_chk(swconn, &packet, &pin,
+                                     &userdata, &continuation);
+        break;
+
+    case ACTION_OPCODE_DHCP_RELAY_RESP_CHK:
+        pinctrl_handle_dhcp_relay_resp_chk(swconn, &packet, &pin,
+                                     &userdata, &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 49fb96fc6..a8e4393ed 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -95,6 +95,8 @@  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_RELAY_REQ_CHK,  ovnact_dhcp_relay)      \
+    OVNACT(DHCPV4_RELAY_RESP_CHK, ovnact_dhcp_relay)      \
     OVNACT(SET_QUEUE,         ovnact_set_queue)       \
     OVNACT(DNS_LOOKUP,        ovnact_result)          \
     OVNACT(LOG,               ovnact_log)             \
@@ -395,6 +397,15 @@  struct ovnact_put_opts {
     size_t n_options;
 };
 
+/* OVNACT_DHCP_RELAY. */
+struct ovnact_dhcp_relay {
+    struct ovnact ovnact;
+    int family;
+    struct expr_field dst;      /* 1-bit destination field. */
+    ovs_be32 relay_ipv4;
+    ovs_be32 server_ipv4;
+};
+
 /* Valid arguments to SET_QUEUE action.
  *
  * QDISC_MIN_QUEUE_ID is the default queue, so user-defined queues should
@@ -758,6 +769,22 @@  enum action_opcode {
 
     /* multicast group split buffer action. */
     ACTION_OPCODE_MG_SPLIT_BUF,
+
+    /* "dhcp_relay_req_chk(relay_ip, server_ip)".
+     *
+     * Arguments follow the action_header, in this format:
+     *   - The 32-bit DHCP relay IP.
+     *   - The 32-bit DHCP server IP.
+     */
+    ACTION_OPCODE_DHCP_RELAY_REQ_CHK,
+
+    /* "dhcp_relay_resp_chk(relay_ip, server_ip)".
+     *
+     * Arguments follow the action_header, in this format:
+     *   - The 32-bit DHCP relay IP.
+     *   - The 32-bit DHCP server IP.
+     */
+    ACTION_OPCODE_DHCP_RELAY_RESP_CHK,
 };
 
 /* Header. */
diff --git a/lib/actions.c b/lib/actions.c
index a45874dfb..d55b5153f 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -2680,6 +2680,149 @@  ovnact_controller_event_free(struct ovnact_controller_event *event)
     free_gen_options(event->options, event->n_options);
 }
 
+static void
+format_DHCPV4_RELAY_REQ_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
+                struct ds *s)
+{
+    expr_field_format(&dhcp_relay->dst, s);
+    ds_put_format(s, " = dhcp_relay_req_chk("IP_FMT", "IP_FMT");",
+                  IP_ARGS(dhcp_relay->relay_ipv4),
+                  IP_ARGS(dhcp_relay->server_ipv4));
+}
+
+static void
+parse_dhcp_relay_req_chk(struct action_context *ctx,
+               const struct expr_field *dst,
+               struct ovnact_dhcp_relay *dhcp_relay)
+{
+    /* Skip dhcp_relay_req_chk( */
+    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+
+    /* Validate that the destination is a 1-bit, modifiable field. */
+    char *error = expr_type_check(dst, 1, true, ctx->scope);
+    if (error) {
+        lexer_error(ctx->lexer, "%s", error);
+        free(error);
+        return;
+    }
+    dhcp_relay->dst = *dst;
+
+    /* Parse relay ip and server ip. */
+    if (ctx->lexer->token.format == LEX_F_IPV4) {
+        dhcp_relay->family = AF_INET;
+        dhcp_relay->relay_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_relay->family = AF_INET;
+            dhcp_relay->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 relay "
+                          "and server ips");
+          return;
+    }
+    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+}
+
+static void
+encode_DHCPV4_RELAY_REQ_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
+                    const struct ovnact_encode_params *ep,
+                    struct ofpbuf *ofpacts)
+{
+    struct mf_subfield dst = expr_resolve_field(&dhcp_relay->dst);
+    size_t oc_offset = encode_start_controller_op(
+                                            ACTION_OPCODE_DHCP_RELAY_REQ_CHK,
+                                            true, ep->ctrl_meter_id,
+                                            ofpacts);
+    nx_put_header(ofpacts, dst.field->id, OFP15_VERSION, false);
+    ovs_be32 ofs = htonl(dst.ofs);
+    ofpbuf_put(ofpacts, &ofs, sizeof ofs);
+    ofpbuf_put(ofpacts, &dhcp_relay->relay_ipv4,
+            sizeof(dhcp_relay->relay_ipv4));
+    ofpbuf_put(ofpacts, &dhcp_relay->server_ipv4,
+            sizeof(dhcp_relay->server_ipv4));
+    encode_finish_controller_op(oc_offset, ofpacts);
+}
+
+static void
+format_DHCPV4_RELAY_RESP_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
+                    struct ds *s)
+{
+    expr_field_format(&dhcp_relay->dst, s);
+    ds_put_format(s, " = dhcp_relay_resp_chk("IP_FMT", "IP_FMT");",
+                  IP_ARGS(dhcp_relay->relay_ipv4),
+                  IP_ARGS(dhcp_relay->server_ipv4));
+}
+
+static void
+parse_dhcp_relay_resp_chk(struct action_context *ctx,
+               const struct expr_field *dst,
+               struct ovnact_dhcp_relay *dhcp_relay)
+{
+    /* Skip dhcp_relay_resp_chk( */
+    lexer_force_match(ctx->lexer, LEX_T_LPAREN);
+
+    /* Validate that the destination is a 1-bit, modifiable field. */
+    char *error = expr_type_check(dst, 1, true, ctx->scope);
+    if (error) {
+        lexer_error(ctx->lexer, "%s", error);
+        free(error);
+        return;
+    }
+    dhcp_relay->dst = *dst;
+
+    /* Parse relay ip and server ip. */
+    if (ctx->lexer->token.format == LEX_F_IPV4) {
+        dhcp_relay->family = AF_INET;
+        dhcp_relay->relay_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_relay->family = AF_INET;
+            dhcp_relay->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 relay and "
+                          "server ips");
+          return;
+    }
+    lexer_force_match(ctx->lexer, LEX_T_RPAREN);
+}
+
+static void
+encode_DHCPV4_RELAY_RESP_CHK(const struct ovnact_dhcp_relay *dhcp_relay,
+                    const struct ovnact_encode_params *ep,
+                    struct ofpbuf *ofpacts)
+{
+    struct mf_subfield dst = expr_resolve_field(&dhcp_relay->dst);
+    size_t oc_offset = encode_start_controller_op(
+                                ACTION_OPCODE_DHCP_RELAY_RESP_CHK,
+                                true, ep->ctrl_meter_id,
+                                ofpacts);
+    nx_put_header(ofpacts, dst.field->id, OFP15_VERSION, false);
+    ovs_be32 ofs = htonl(dst.ofs);
+    ofpbuf_put(ofpacts, &ofs, sizeof ofs);
+    ofpbuf_put(ofpacts, &dhcp_relay->relay_ipv4,
+                  sizeof(dhcp_relay->relay_ipv4));
+    ofpbuf_put(ofpacts, &dhcp_relay->server_ipv4,
+                  sizeof(dhcp_relay->server_ipv4));
+    encode_finish_controller_op(oc_offset, ofpacts);
+}
+
+static void ovnact_dhcp_relay_free(
+          struct ovnact_dhcp_relay *dhcp_relay 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,
@@ -5384,6 +5527,12 @@  parse_set_action(struct action_context *ctx)
                    lexer_lookahead(ctx->lexer) == LEX_T_LPAREN) {
             parse_chk_lb_aff(ctx, &lhs,
                     ovnact_put_CHK_LB_AFF(ctx->ovnacts));
+        } else if (lexer_match_id(ctx->lexer, "dhcp_relay_req_chk")) {
+            parse_dhcp_relay_req_chk(ctx, &lhs,
+                    ovnact_put_DHCPV4_RELAY_REQ_CHK(ctx->ovnacts));
+        } else if (lexer_match_id(ctx->lexer, "dhcp_relay_resp_chk")) {
+            parse_dhcp_relay_resp_chk(ctx, &lhs,
+                    ovnact_put_DHCPV4_RELAY_RESP_CHK(ctx->ovnacts));
         } else {
             parse_assignment_action(ctx, false, &lhs);
         }
diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h
index ad514a922..5eeb7c1a0 100644
--- a/lib/ovn-l7.h
+++ b/lib/ovn-l7.h
@@ -68,7 +68,9 @@  struct gen_opts_map {
  * OVN_DHCP_OPT_CODE_<opt_name>.
  */
 #define OVN_DHCP_OPT_CODE_NETMASK      1
+#define OVN_DHCP_OPT_CODE_ROUTER_IP    3
 #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 2c3560ce2..1f7a62982 100644
--- a/northd/northd.c
+++ b/northd/northd.c
@@ -91,7 +91,6 @@  static bool use_ct_inv_match = true;
 static bool default_acl_drop;
 
 #define MAX_OVN_TAGS 4096
-
 
 /* Due to various hard-coded priorities need to implement ACLs, the
  * northbound database supports a smaller range of ACL priorities than
@@ -153,6 +152,8 @@  static bool default_acl_drop;
 #define REGBIT_LOOKUP_NEIGHBOR_IP_RESULT "reg9[3]"
 #define REGBIT_DST_NAT_IP_LOCAL "reg9[4]"
 #define REGBIT_KNOWN_LB_SESSION "reg9[6]"
+#define REGBIT_DHCP_RELAY_REQ_CHK "reg9[7]"
+#define REGBIT_DHCP_RELAY_RESP_CHK "reg9[8]"
 
 /* Register to store the eth address associated to a router port for packets
  * received in S_ROUTER_IN_ADMISSION.
@@ -168,6 +169,7 @@  static bool default_acl_drop;
 #define REG_NEXT_HOP_IPV6 "xxreg0"
 #define REG_SRC_IPV4 "reg1"
 #define REG_SRC_IPV6 "xxreg1"
+#define REG_DHCP_RELAY_DIP_IPV4 "reg2"
 #define REG_ROUTE_TABLE_ID "reg7"
 
 /* Register used to store backend ipv6 address
@@ -232,7 +234,7 @@  static bool default_acl_drop;
  * | R1  |   SRC_IPV4 for ARP-REQ    | 0 |                 | R |                                    |
  * |     |      (>= IP_INPUT)        |   |                 | E |     NEXT_HOP_IPV6 (>= DEFRAG )     |
  * +-----+---------------------------+---+-----------------+ G |                                    |
- * | R2  |        UNUSED             | X |                 | 0 |                                    |
+ * | R2     REG_DHCP_RELAY_DIP_IPV4  | X |                 | 0 |                                    |
  * |     |                           | R |                 |   |                                    |
  * +-----+---------------------------+ E |     UNUSED      |   |                                    |
  * | R3  |        UNUSED             | G |                 |   |                                    |
@@ -259,7 +261,9 @@  static bool default_acl_drop;
  * |     |   EGRESS_LOOPBACK/        | G |     UNUSED      |
  * | R9  |   PKT_LARGER/             | 4 |                 |
  * |     |   LOOKUP_NEIGHBOR_RESULT/ |   |                 |
- * |     |   SKIP_LOOKUP_NEIGHBOR}   |   |                 |
+ * |     |   SKIP_LOOKUP_NEIGHBOR/   |   |                 |
+ * |     |REGBIT_DHCP_RELAY_REQ_CHK/ |   |                 |
+ * |     |REGBIT_DHCP_RELAY_RESP_CHK}|   |                 |
  * |     |                           |   |                 |
  * |     | REG_ORIG_TP_DPORT_ROUTER  |   |                 |
  * |     |                           |   |                 |
@@ -8555,6 +8559,85 @@  build_dhcpv6_options_flows(struct ovn_port *op,
     ds_destroy(&match);
 }
 
+static const char *
+ls_dhcp_relay_port(const struct ovn_datapath *od)
+{
+    return smap_get(&od->nbs->other_config, "dhcp_relay_port");
+}
+
+static void
+build_lswitch_dhcp_relay_flows(struct ovn_port *op,
+                           const struct hmap *ls_ports,
+                           struct lflow_table *lflows)
+{
+    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) {
+        return;
+    }
+
+    /* configure dhcp relay flows only when peer router  has
+     * relay config enabled */
+    const char *dhcp_relay_port = ls_dhcp_relay_port(op->od);
+    if (!dhcp_relay_port) {
+        return;
+    }
+
+    struct ds match = DS_EMPTY_INITIALIZER;
+    struct ds action = DS_EMPTY_INITIALIZER;
+    struct ovn_port *sp = ovn_port_find(ls_ports, dhcp_relay_port);
+
+    if (!sp || !sp->nbsp || !sp->peer) {
+        return;
+    }
+
+    struct ovn_port *rp = sp->peer;
+    if (!rp || !rp->nbrp || !rp->nbrp->dhcp_relay || rp->peer != sp) {
+        return;
+    }
+
+    char *server_ip_str = NULL;
+    uint16_t port;
+    int addr_family;
+    struct in6_addr server_ip;
+    struct nbrec_dhcp_relay *dhcp_relay = rp->nbrp->dhcp_relay;
+
+    if (!ip_address_and_port_from_lb_key(dhcp_relay->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_RELAY_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,
+                              &op->nbsp->header_,
+                              op->lflow_ref);
+    free(server_ip_str);
+}
+
 static void
 build_drop_arp_nd_flows_for_unbound_router_ports(struct ovn_port *op,
                                                  const struct ovn_port *port,
@@ -9156,6 +9239,13 @@  build_lswitch_dhcp_options_and_response(struct ovn_port *op,
         return;
     }
 
+    if (op->od && op->od->nbs
+        && ls_dhcp_relay_port(op->od)) {
+        /* Don't add the DHCP server flows if DHCP Relay 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)) {
@@ -13636,6 +13726,165 @@  build_dhcpv6_reply_flows_for_lrouter_port(
     }
 }
 
+static void
+build_dhcp_relay_flows_for_lrouter_port(
+        struct ovn_port *op, struct lflow_table *lflows,
+        struct ds *match, struct lflow_ref *lflow_ref)
+{
+    if (!op->nbrp || !op->nbrp->dhcp_relay) {
+        return;
+
+    }
+
+    /* configure dhcp relay flows only when peer switch has
+     * relay config enabled */
+    struct ovn_port *sp = op->peer;
+    if (!sp || !sp->nbsp || sp->peer != op ||
+        !sp->od || !ls_dhcp_relay_port(sp->od)) {
+        return;
+    }
+
+    struct nbrec_dhcp_relay *dhcp_relay = op->nbrp->dhcp_relay;
+    if (!dhcp_relay->servers) {
+        return;
+    }
+
+    int addr_family;
+    /* currently not supporting custom port,
+     * dhcp server port is always set to 67 when installing flows */
+    uint16_t port;
+    char *server_ip_str = NULL;
+    struct in6_addr server_ip;
+
+    if (!ip_address_and_port_from_lb_key(dhcp_relay->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,
+                REGBIT_DHCP_RELAY_REQ_CHK
+                " = dhcp_relay_req_chk(%s, %s);"
+                "next; /* DHCP_RELAY_REQ */",
+                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_, lflow_ref);
+
+    ds_clear(match);
+    ds_clear(&dhcp_action);
+
+    ds_put_format(
+        match, "inport == %s && "
+        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
+        "udp.src == 68 && udp.dst == 67 && "
+        REGBIT_DHCP_RELAY_REQ_CHK,
+        op->json_key);
+    ds_put_format(&dhcp_action,
+                "ip4.src=%s;ip4.dst=%s;udp.src=67;next; /* DHCP_RELAY_REQ */",
+                op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str);
+
+    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_REQ, 100,
+                            ds_cstr(match), ds_cstr(&dhcp_action),
+                            &op->nbrp->header_, lflow_ref);
+
+    ds_clear(match);
+    ds_clear(&dhcp_action);
+
+    ds_put_format(
+        match, "inport == %s && "
+        "ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && "
+        "udp.src == 68 && udp.dst == 67 && "
+        REGBIT_DHCP_RELAY_REQ_CHK" == 0",
+        op->json_key);
+    ds_put_format(&dhcp_action,
+                "drop; /* DHCP_RELAY_REQ */");
+
+    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_REQ, 1,
+                            ds_cstr(match), ds_cstr(&dhcp_action),
+                            &op->nbrp->header_, lflow_ref);
+
+    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_RELAY_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_, lflow_ref);
+
+    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,
+          REG_DHCP_RELAY_DIP_IPV4" = ip4.dst;"
+          REGBIT_DHCP_RELAY_RESP_CHK
+          " = dhcp_relay_resp_chk(%s, %s);next;/* DHCP_RELAY_RESP */",
+          op->lrp_networks.ipv4_addrs[0].addr_s, server_ip_str);
+
+    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_RESP_CHK,
+                            100,
+                            ds_cstr(match), ds_cstr(&dhcp_action),
+                            &op->nbrp->header_, lflow_ref);
+
+
+    ds_clear(match);
+    ds_clear(&dhcp_action);
+
+    ds_put_format(
+        match, "ip4.src == %s && "
+        REG_DHCP_RELAY_DIP_IPV4" == %s && "
+        "udp.src == 67 && udp.dst == 67 && "
+        REGBIT_DHCP_RELAY_RESP_CHK,
+        server_ip_str, op->lrp_networks.ipv4_addrs[0].addr_s);
+    ds_put_format(&dhcp_action,
+          "ip4.src=%s;udp.dst=68;"
+          "outport=%s;output; /* DHCP_RELAY_RESP */",
+          op->lrp_networks.ipv4_addrs[0].addr_s, op->json_key);
+    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_RESP,
+                            100,
+                            ds_cstr(match), ds_cstr(&dhcp_action),
+                            &op->nbrp->header_, lflow_ref);
+
+    ds_clear(match);
+    ds_clear(&dhcp_action);
+
+    ds_put_format(
+        match, "ip4.src == %s && "
+        REG_DHCP_RELAY_DIP_IPV4" == %s && "
+        "udp.src == 67 && udp.dst == 67 && "
+        REGBIT_DHCP_RELAY_RESP_CHK" == 0",
+        server_ip_str, op->lrp_networks.ipv4_addrs[0].addr_s);
+    ds_put_format(&dhcp_action,
+          "drop; /* DHCP_RELAY_RESP */");
+    ovn_lflow_add_with_hint(lflows, op->od, S_ROUTER_IN_DHCP_RELAY_RESP,
+                            1,
+                            ds_cstr(match), ds_cstr(&dhcp_action),
+                            &op->nbrp->header_, lflow_ref);
+    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 lflow_table *lflows,
@@ -14906,6 +15155,13 @@  static void build_lr_nat_defrag_and_lb_default_flows(
                   lflow_ref);
     ovn_lflow_add(lflows, od, S_ROUTER_IN_ECMP_STATEFUL, 0, "1", "next;",
                   lflow_ref);
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_RELAY_REQ, 0, "1",
+                  "next;", lflow_ref);
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_RELAY_RESP_CHK, 0, "1",
+                  "next;", lflow_ref);
+    ovn_lflow_add(lflows, od, S_ROUTER_IN_DHCP_RELAY_RESP, 0, "1",
+                  "next;", lflow_ref);
+
 
     /* Send the IPv6 NS packets to next table. When ovn-controller
      * generates IPv6 NS (for the action - nd_ns{}), the injected
@@ -15670,6 +15926,7 @@  build_lswitch_and_lrouter_iterate_by_lsp(struct ovn_port *op,
     build_lswitch_icmp_packet_toobig_admin_flows(op, lflows, match, actions);
     build_lswitch_ip_unicast_lookup(op, lflows, actions,
                                     match);
+    build_lswitch_dhcp_relay_flows(op, ls_ports, lflows);
 
     /* Build Logical Router Flows. */
     build_arp_resolve_flows_for_lsp(op, lflows, lr_ports, match, actions);
@@ -15699,6 +15956,8 @@  build_lswitch_and_lrouter_iterate_by_lrp(struct ovn_port *op,
                                                  op->lflow_ref);
     build_dhcpv6_reply_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
                                               op->lflow_ref);
+    build_dhcp_relay_flows_for_lrouter_port(op, lsi->lflows, &lsi->match,
+                                              op->lflow_ref);
     build_ipv6_input_flows_for_lrouter_port(op, lsi->lflows,
                                             &lsi->match, &lsi->actions,
                                             lsi->meter_groups,
diff --git a/northd/northd.h b/northd/northd.h
index 3f1cd8341..0a349c8f7 100644
--- a/northd/northd.h
+++ b/northd/northd.h
@@ -437,24 +437,29 @@  enum ovn_stage {
     PIPELINE_STAGE(ROUTER, IN,  LOOKUP_NEIGHBOR, 1, "lr_in_lookup_neighbor") \
     PIPELINE_STAGE(ROUTER, IN,  LEARN_NEIGHBOR,  2, "lr_in_learn_neighbor") \
     PIPELINE_STAGE(ROUTER, IN,  IP_INPUT,        3, "lr_in_ip_input")     \
-    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          4, "lr_in_unsnat")       \
-    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          5, "lr_in_defrag")       \
-    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    6, "lr_in_lb_aff_check") \
-    PIPELINE_STAGE(ROUTER, IN,  DNAT,            7, "lr_in_dnat")         \
-    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    8, "lr_in_lb_aff_learn") \
-    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   9, "lr_in_ecmp_stateful") \
-    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   10, "lr_in_nd_ra_options") \
-    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  11, "lr_in_nd_ra_response") \
-    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  12, "lr_in_ip_routing_pre")  \
-    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      13, "lr_in_ip_routing")      \
-    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_RELAY_REQ,  4, "lr_in_dhcp_relay_req") \
+    PIPELINE_STAGE(ROUTER, IN,  UNSNAT,          5, "lr_in_unsnat")       \
+    PIPELINE_STAGE(ROUTER, IN,  DEFRAG,          6, "lr_in_defrag")       \
+    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_CHECK,    7, "lr_in_lb_aff_check") \
+    PIPELINE_STAGE(ROUTER, IN,  DNAT,            8, "lr_in_dnat")         \
+    PIPELINE_STAGE(ROUTER, IN,  LB_AFF_LEARN,    9, "lr_in_lb_aff_learn") \
+    PIPELINE_STAGE(ROUTER, IN,  ECMP_STATEFUL,   10, "lr_in_ecmp_stateful") \
+    PIPELINE_STAGE(ROUTER, IN,  ND_RA_OPTIONS,   11, "lr_in_nd_ra_options") \
+    PIPELINE_STAGE(ROUTER, IN,  ND_RA_RESPONSE,  12, "lr_in_nd_ra_response") \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_PRE,  13, "lr_in_ip_routing_pre")  \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING,      14, "lr_in_ip_routing")      \
+    PIPELINE_STAGE(ROUTER, IN,  IP_ROUTING_ECMP, 15, "lr_in_ip_routing_ecmp") \
+    PIPELINE_STAGE(ROUTER, IN,  POLICY,          16, "lr_in_policy")          \
+    PIPELINE_STAGE(ROUTER, IN,  POLICY_ECMP,     17, "lr_in_policy_ecmp")     \
+    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP_CHK, 18,                      \
+                  "lr_in_dhcp_relay_resp_chk")                                \
+    PIPELINE_STAGE(ROUTER, IN,  DHCP_RELAY_RESP, 19,                          \
+                  "lr_in_dhcp_relay_resp")                                    \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_RESOLVE,     20, "lr_in_arp_resolve")     \
+    PIPELINE_STAGE(ROUTER, IN,  CHK_PKT_LEN,     21, "lr_in_chk_pkt_len")     \
+    PIPELINE_STAGE(ROUTER, IN,  LARGER_PKTS,     22, "lr_in_larger_pkts")     \
+    PIPELINE_STAGE(ROUTER, IN,  GW_REDIRECT,     23, "lr_in_gw_redirect")     \
+    PIPELINE_STAGE(ROUTER, IN,  ARP_REQUEST,     24, "lr_in_arp_request")     \
                                                                       \
     /* Logical router egress stages. */                               \
     PIPELINE_STAGE(ROUTER, OUT, CHECK_DNAT_LOCAL,   0,                       \
diff --git a/ovn-nb.ovsschema b/ovn-nb.ovsschema
index a9c5b7af5..a616dbd6b 100644
--- a/ovn-nb.ovsschema
+++ b/ovn-nb.ovsschema
@@ -1,7 +1,7 @@ 
 {
     "name": "OVN_Northbound",
     "version": "7.3.0",
-    "cksum": "3546526738 34483",
+    "cksum": "3858903371 35372",
     "tables": {
         "NB_Global": {
             "columns": {
@@ -436,6 +436,11 @@ 
                 "ipv6_prefix": {"type": {"key": "string",
                                       "min": 0,
                                       "max": "unlimited"}},
+                "dhcp_relay": {"type": {"key": {"type": "uuid",
+                                            "refTable": "DHCP_Relay",
+                                            "refType": "strong"},
+                                            "min": 0,
+                                            "max": 1}},
                 "external_ids": {
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}},
@@ -534,6 +539,18 @@ 
                     "type": {"key": "string", "value": "string",
                              "min": 0, "max": "unlimited"}}},
             "isRoot": true},
+        "DHCP_Relay": {
+            "columns": {
+                "name": {"type": "string"},
+                "servers": {"type": {"key": "string",
+                                       "min": 0,
+                                       "max": 1}},
+                "options": {"type": {"key": "string", "value": "string",
+                                     "min": 0, "max": "unlimited"}},
+                "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 e0b983ed6..6f0210325 100644
--- a/ovn-nb.xml
+++ b/ovn-nb.xml
@@ -703,6 +703,13 @@ 
         </ul>
       </column>
 
+      <column name="other_config" key="dhcp_relay_port">
+        If set to the name of logical switch port of type <code>router</code>
+        then, DHCP Relay is enabled for this logical switch provided the
+        corresponding <ref table="Logical_Router_Port"/> has DHCP Relay
+        configured.
+      </column>
+
       <column name="other_config" key="mac_only" type='{"type": "boolean"}'>
         Value used to request to assign L2 address only if neither subnet
         nor ipv6_prefix are specified
@@ -3059,6 +3066,11 @@  or
       port has all ingress and egress traffic dropped.
     </column>
 
+    <column name="dhcp_relay">
+      This column is used to enabled DHCP Relay. Please refer
+      to <ref table="DHCP_Relay"/> table.
+    </column>
+
     <group title="Distributed Gateway Ports">
       <p>
         Gateways, as documented under <code>Gateways</code> in the OVN
@@ -4372,6 +4384,33 @@  or
     </group>
   </table>
 
+  <table name="DHCP_Relay" title="DHCP Relay">
+    <p>
+      OVN implements native DHCPv4 relay support which caters to the common
+      use case of relaying the DHCP requests to external DHCP server.
+    </p>
+    <column name="name">
+      <p>
+        A name for the DHCP Relay.
+      </p>
+    </column>
+    <column name="servers">
+      <p>
+        The DHCPv4 server IP address.
+      </p>
+    </column>
+    <column name="options">
+      <p>
+        Future purpose.
+      </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/atlocal.in b/tests/atlocal.in
index 63d891b89..32d1c374e 100644
--- a/tests/atlocal.in
+++ b/tests/atlocal.in
@@ -187,6 +187,9 @@  fi
 # Set HAVE_DHCPD
 find_command dhcpd
 
+# Set HAVE_DHCLIENT
+find_command dhclient
+
 # Set HAVE_BFDD_BEACON
 find_command bfdd-beacon
 
diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at
index c189dcccc..c8d551f2d 100644
--- a/tests/ovn-northd.at
+++ b/tests/ovn-northd.at
@@ -12138,6 +12138,44 @@  check_row_count nb:QoS 0
 AT_CLEANUP
 ])
 
+OVN_FOR_EACH_NORTHD_NO_HV([
+AT_SETUP([check DHCP RELAY])
+ovn_start NORTHD_TYPE
+
+check ovn-nbctl ls-add ls0
+check ovn-nbctl lsp-add ls0 ls0-port1
+check ovn-nbctl lsp-set-addresses ls0-port1 02:00:00:00:00:10
+check ovn-nbctl lr-add lr0
+check ovn-nbctl lrp-add lr0 lrp1 02:00:00:00:00:01 192.168.1.1/24
+check ovn-nbctl lsp-add ls0 lrp1-attachment
+check ovn-nbctl lsp-set-type lrp1-attachment router
+check ovn-nbctl lsp-set-addresses lrp1-attachment 00:00:00:00:ff:02
+check ovn-nbctl lsp-set-options lrp1-attachment router-port=lrp1
+check ovn-nbctl lrp-add lr0 lrp-ext 02:00:00:00:00:02 192.168.2.1/24
+
+dhcp_relay=$(ovn-nbctl create DHCP_Relay servers=172.16.1.1)
+check ovn-nbctl set Logical_Router_port lrp1 dhcp_relay=$dhcp_relay
+check ovn-nbctl set Logical_Switch ls0 other_config:dhcp_relay_port=lrp1-attachment
+
+check ovn-nbctl --wait=sb sync
+
+ovn-sbctl lflow-list > lflows
+AT_CAPTURE_FILE([lflows])
+
+AT_CHECK([grep -e "DHCP_RELAY_" lflows | sed 's/table=../table=??/'], [0], [dnl
+  table=??(lr_in_ip_input     ), priority=110  , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67), action=(reg9[[7]] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);next; /* DHCP_RELAY_REQ */)
+  table=??(lr_in_ip_input     ), priority=110  , match=(ip4.src == 172.16.1.1 && ip4.dst == 192.168.1.1 && udp.src == 67 && udp.dst == 67), action=(next;/* DHCP_RELAY_RESP */)
+  table=??(lr_in_dhcp_relay_req), priority=100  , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[[7]]), action=(ip4.src=192.168.1.1;ip4.dst=172.16.1.1;udp.src=67;next; /* DHCP_RELAY_REQ */)
+  table=??(lr_in_dhcp_relay_req), priority=1    , match=(inport == "lrp1" && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67 && reg9[[7]] == 0), action=(drop; /* DHCP_RELAY_REQ */)
+  table=??(lr_in_dhcp_relay_resp_chk), priority=100  , match=(ip4.src == 172.16.1.1 && ip4.dst == 192.168.1.1 && udp.src == 67 && udp.dst == 67), action=(reg2 = ip4.dst;reg9[[8]] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);next;/* DHCP_RELAY_RESP */)
+  table=??(lr_in_dhcp_relay_resp), priority=100  , match=(ip4.src == 172.16.1.1 && reg2 == 192.168.1.1 && udp.src == 67 && udp.dst == 67 && reg9[[8]]), action=(ip4.src=192.168.1.1;udp.dst=68;outport="lrp1";output; /* DHCP_RELAY_RESP */)
+  table=??(lr_in_dhcp_relay_resp), priority=1    , match=(ip4.src == 172.16.1.1 && reg2 == 192.168.1.1 && udp.src == 67 && udp.dst == 67 && reg9[[8]] == 0), action=(drop; /* DHCP_RELAY_RESP */)
+  table=??(ls_in_l2_lkup      ), priority=100  , match=(inport == "ls0-port1" && eth.src == 02:00:00:00:00:10 && ip4.src == 0.0.0.0 && ip4.dst == 255.255.255.255 && udp.src == 68 && udp.dst == 67), action=(eth.dst=02:00:00:00:00:01;outport="lrp1-attachment";next;/* DHCP_RELAY_REQ */)
+])
+
+AT_CLEANUP
+])
+
 AT_SETUP([NB_Global and SB_Global incremental processing])
 
 ovn_start
diff --git a/tests/ovn.at b/tests/ovn.at
index 902dd3793..109e19550 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1661,6 +1661,40 @@  reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain_name=1.2.3.4);
 reg1[0] = put_dhcp_opts(offerip=1.2.3.4, domain_search_list=1.2.3.4);
     DHCPv4 option domain_search_list requires string value.
 
+#dhcp_relay_req_chk
+reg9[7] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);
+    encodes as controller(userdata=00.00.00.1c.00.00.00.00.80.01.08.08.00.00.00.07.c0.a8.01.01.ac.10.01.01,pause)
+
+reg9[7] = dhcp_relay_req_chk(192.168.1.1,172.16.1.1);
+    formats as reg9[7] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);
+    encodes as controller(userdata=00.00.00.1c.00.00.00.00.80.01.08.08.00.00.00.07.c0.a8.01.01.ac.10.01.01,pause)
+
+reg9[7..8] = dhcp_relay_req_chk(192.168.1.1, 172.16.1.1);
+    Cannot use 2-bit field reg9[7..8] where 1-bit field is required.
+
+reg9[7] = dhcp_relay_req_chk("192.168.1.1", "172.16.1.1");
+    Syntax error at `"192.168.1.1"' expecting IPv4 dhcp relay and server ips.
+
+reg9[7] = dhcp_relay_req_chk(192.168.1, 172.16.1.1);
+    Invalid numeric constant.
+
+#dhcp_relay_resp_chk
+reg9[8] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);
+    encodes as controller(userdata=00.00.00.1d.00.00.00.00.80.01.08.08.00.00.00.08.c0.a8.01.01.ac.10.01.01,pause)
+
+reg9[8] = dhcp_relay_resp_chk(192.168.1.1,172.16.1.1);
+    formats as reg9[8] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);
+    encodes as controller(userdata=00.00.00.1d.00.00.00.00.80.01.08.08.00.00.00.08.c0.a8.01.01.ac.10.01.01,pause)
+
+reg9[7..8] = dhcp_relay_resp_chk(192.168.1.1, 172.16.1.1);
+    Cannot use 2-bit field reg9[7..8] where 1-bit field is required.
+
+reg9[8] = dhcp_relay_resp_chk("192.168.1.1", "172.16.1.1");
+    Syntax error at `"192.168.1.1"' expecting IPv4 dhcp relay and server ips.
+
+reg9[8] = dhcp_relay_resp_chk(192.168.1, 172.16.1.1);
+    Invalid numeric constant.
+
 # nd_ns
 nd_ns { nd.target = xxreg0; output; };
     encodes as controller(userdata=00.00.00.09.00.00.00.00.00.1c.00.18.00.80.00.00.00.00.00.00.00.01.de.10.80.00.3e.10.00.00.00.00.ff.ff.00.10.00.00.23.20.00.0e.ff.f8.40.00.00.00)
@@ -21996,7 +22030,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_UNQUOTED([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_UNQUOTED([as hv1 ovs-ofctl dump-flows br-int metadata=0x$lr0_dp_key | awk '/table=31, n_packets=1, n_bytes=45/{print $7" "$8}'],[0],[dnl
 priority=80,ip,reg15=0x$lr0_public_dp_key,metadata=0x$lr0_dp_key,nw_src=10.0.0.10 actions=drop
 ])
 
@@ -28221,7 +28255,7 @@  ovn-sbctl dump-flows > sbflows
 AT_CAPTURE_FILE([sbflows])
 AT_CAPTURE_FILE([offlows])
 OVS_WAIT_UNTIL([
-    as hv1 ovs-ofctl dump-flows br-int table=23 > offlows
+    as hv1 ovs-ofctl dump-flows br-int table=24 > offlows
     test $(grep -c "load:0x64->NXM_NX_PKT_MARK" offlows) = 1 && \
     test $(grep -c "load:0x3->NXM_NX_PKT_MARK" offlows) = 1 && \
     test $(grep -c "load:0x4->NXM_NX_PKT_MARK" offlows) = 1 && \
@@ -28319,12 +28353,12 @@  send_ipv4_pkt hv1 hv1-vif1 505400000003 00000000ff01 \
     c3ad 83dc
 
 OVS_WAIT_UNTIL([
-    test 1 -eq $(as hv1 ovs-ofctl dump-flows br-int table=23 | \
+    test 1 -eq $(as hv1 ovs-ofctl dump-flows br-int table=24 | \
     grep "load:0x2->NXM_NX_PKT_MARK" -c)
 ])
 
 AT_CHECK([
-    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=23 | \
+    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=24 | \
     grep "load:0x64->NXM_NX_PKT_MARK" -c)
 ])
 
@@ -29017,23 +29051,23 @@  check ovn-nbctl --wait=hv sync
 
 # Ensure ECMP symmetric reply flows are not present on any hypervisor.
 AT_CHECK([
-    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=17 | \
+    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=18 | \
     grep "priority=100" | \
     grep "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_LABEL\\[[80..95\\]]))" -c)
 ])
 AT_CHECK([
-    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=25 | \
+    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=26 | \
     grep "priority=200" | \
     grep "actions=move:NXM_NX_CT_LABEL\\[[32..79\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
 ])
 
 AT_CHECK([
-    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=17 | \
+    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=18 | \
     grep "priority=100" | \
     grep "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_LABEL\\[[80..95\\]]))" -c)
 ])
 AT_CHECK([
-    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=25 | \
+    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=26 | \
     grep "priority=200" | \
     grep "actions=move:NXM_NX_CT_LABEL\\[[32..79\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
 ])
@@ -29051,11 +29085,11 @@  AT_CAPTURE_FILE([hv2flows])
 
 AT_CHECK([
     for hv in 1 2; do
-        grep table=17 hv${hv}flows | \
+        grep table=18 hv${hv}flows | \
         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=28 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
@@ -29143,23 +29177,23 @@  check ovn-nbctl --wait=hv sync
 
 # Ensure ECMP symmetric reply flows are not present on any hypervisor.
 AT_CHECK([
-    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=17 | \
+    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=18 | \
     grep "priority=100" | \
     grep "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_LABEL\\[[80..95\\]]))" -c)
 ])
 AT_CHECK([
-    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=25 | \
+    test 0 -eq $(as hv1 ovs-ofctl dump-flows br-int table=28 | \
     grep "priority=200" | \
     grep "actions=move:NXM_NX_CT_LABEL\\[[32..79\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
 ])
 
 AT_CHECK([
-    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=17 | \
+    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=18 | \
     grep "priority=100" | \
     grep "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\\]]))" -c)
 ])
 AT_CHECK([
-    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=25 | \
+    test 0 -eq $(as hv2 ovs-ofctl dump-flows br-int table=28 | \
     grep "priority=200" | \
     grep "actions=move:NXM_NX_CT_LABEL\\[[\\]]->NXM_OF_ETH_DST\\[[\\]]" -c)
 ])
@@ -29176,11 +29210,11 @@  AT_CAPTURE_FILE([hv2flows])
 
 AT_CHECK([
     for hv in 1 2; do
-        grep table=17 hv${hv}flows | \
+        grep table=18 hv${hv}flows | \
         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=28 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
@@ -29677,7 +29711,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=28, n_packets=1,.* priority=$prio,ip,$inport.*$outport.*metadata=0x${sw_key},nw_dst=10.0.1.1 actions=drop" -c], [0], [dnl
 1
 ])
 
@@ -29696,13 +29730,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=28, 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=28, 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])
@@ -34645,7 +34679,7 @@  check ovn-nbctl set nb_global . options:use_common_zone="true"
 check ovn-nbctl --wait=hv sync
 # Use constants so that if tables or registers change, this test can
 # be updated easily.
-DNAT_TABLE=15
+DNAT_TABLE=16
 SNAT_TABLE=45
 DNAT_ZONE_REG="NXM_NX_REG11[[0..15]]"
 SNAT_ZONE_REG="NXM_NX_REG12[[0..15]]"
@@ -38088,3 +38122,222 @@  OVS_WAIT_UNTIL([test 1 = $(as hv ovs-ofctl dump-flows br-int | grep -E "pkt_mark
 OVN_CLEANUP([hv])
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([DHCP RELAY])
+ovn_start
+net_add n1
+
+AT_CHECK([ovn-nbctl ls-add ls0])
+AT_CHECK([ovn-nbctl lsp-add ls0 vif0])
+AT_CHECK([ovn-nbctl lsp-set-addresses vif0 "50:54:00:00:00:10"])
+AT_CHECK([ovn-nbctl lsp-add ls0 lrp1-attachment])
+AT_CHECK([ovn-nbctl lsp-set-type lrp1-attachment router])
+AT_CHECK([ovn-nbctl lsp-set-addresses lrp1-attachment 50:54:00:00:00:01])
+AT_CHECK([ovn-nbctl lsp-set-options lrp1-attachment router-port=lrp1])
+
+AT_CHECK([ovn-nbctl lr-add lr0])
+AT_CHECK([ovn-nbctl lrp-add lr0 lrp1 50:54:00:00:00:01 192.168.1.1/24])
+AT_CHECK([ovn-nbctl lrp-add lr0 lrp2 50:54:00:00:00:02 172.16.1.254/24])
+
+AT_CHECK([ovn-nbctl ls-add ls-ext])
+AT_CHECK([ovn-nbctl lsp-add ls-ext lrp2-attachment])
+AT_CHECK([ovn-nbctl lsp-set-type lrp2-attachment router])
+AT_CHECK([ovn-nbctl lsp-set-addresses lrp2-attachment 50:54:00:00:00:02])
+AT_CHECK([ovn-nbctl lsp-set-options lrp2-attachment router-port=lrp2])
+AT_CHECK([ovn-nbctl lsp-add ls-ext ln_port])
+AT_CHECK([ovn-nbctl lsp-set-addresses ln_port unknown])
+AT_CHECK([ovn-nbctl lsp-set-type ln_port localnet])
+AT_CHECK([ovn-nbctl lsp-set-options ln_port network_name=physnet1])
+
+dhcp_relay=$(ovn-nbctl create DHCP_Relay servers=172.16.1.1)
+AT_CHECK([ovn-nbctl set Logical_Router_port lrp1 dhcp_relay=$dhcp_relay])
+AT_CHECK([ovn-nbctl set Logical_Switch ls0 other_config:dhcp_relay_port=lrp1-attachment])
+
+sim_add hv1
+as hv1
+ovs-vsctl add-br br-phys
+ovn_attach n1 br-phys 192.168.0.1
+ovs-vsctl -- add-port br-int vif0 -- \
+    set interface vif0 external-ids:iface-id=vif0 \
+    options:tx_pcap=hv1/vif0-tx.pcap \
+    options:rxq_pcap=hv1/vif0-rx.pcap \
+    ofport-request=1
+ovs-vsctl -- add-port br-phys ext0 -- \
+    set interface ext0 \
+    options:tx_pcap=hv1/ext0-tx.pcap \
+    options:rxq_pcap=hv1/ext0-rx.pcap \
+    ofport-request=2
+
+ovs-vsctl set open . external_ids:ovn-bridge-mappings=physnet1:br-phys
+
+wait_for_ports_up
+AT_CHECK([ovn-nbctl --wait=hv sync])
+
+send_dhcp_packet() {
+    src_mac=${1}
+    src_ip=${2}
+    dst_mac=${3}
+    dst_ip=${4}
+    op_code=${5}
+    msg_type=${6}
+    yiaddr=$7
+    giaddr=${8}
+    sid=${9}
+    iface=${10}
+    #echo "ARGS: ${1} ${2} ${3} ${4} ${5} ${6} ${7} ${8} ${9} ${10}"
+    echo "ARGS: $@"
+    if [[ $op_code == "01" ]]; then
+        ip_len=0111
+        udp_len=00fd
+        src_port=0044
+    else
+        ip_len=011d
+        udp_len=0109
+        src_port=0043
+    fi
+    flags=0000
+
+    local pkt=${dst_mac}${src_mac}08004510${ip_len}0000000080110000${src_ip}${dst_ip}
+    # udp header and dhcp header
+    pkt=${pkt}${src_port}0043${udp_len}0000
+    pkt=${pkt}${op_code}0106006359aa760000${flags}00000000${yiaddr}00000000${giaddr}${src_mac}
+    # client hardware padding
+    pkt=${pkt}00000000000000000000
+    # server hostname
+    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
+    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
+    # boot file name
+    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
+    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
+    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
+    pkt=${pkt}0000000000000000000000000000000000000000000000000000000000000000
+    # dhcp magic cookie
+    pkt=${pkt}63825363
+    # dhcp message type
+    pkt=${pkt}3501${msg_type}
+    # dhcp server identifier and subnet mask options
+    if  [[ $op_code == "02" ]]; then
+        pkt=${pkt}3604${sid}
+        pkt=${pkt}0104ffffff00
+    fi
+    # dhcp pad option
+    pkt=${pkt}00
+    # dhcp end option
+    pkt=${pkt}ff
+
+    tcpdump_hex "-- sending DHCP pkt on hv1-$iface" $pkt
+
+    ovs-appctl netdev-dummy/receive $iface $pkt
+}
+
+
+# Check that there is commit_fdb_local_fdb() flow added by ovn-northd for vif0 and localnet
+ovn-sbctl dump-flows > lflows
+AT_CAPTURE_FILE([lflows])
+
+# ====================================================
+# Send DHCP valid discovery
+src_mac="505400000010"
+src_ip=`ip_to_hex 0.0.0.0`
+dst_mac="ffffffffffff"
+dst_ip=`ip_to_hex 255.255.255.255`
+yiaddr=`ip_to_hex 0.0.0.0`
+giaddr=`ip_to_hex 0.0.0.0`
+sid=$src_ip
+# send packet
+send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 01 01 $yiaddr $giaddr $sid vif0
+
+ovs-ofctl dump-flows br-int table=12 > pflows1_dhcp_relay_req
+AT_CAPTURE_FILE([pflows1_dhcp_relay_req])
+
+AT_CHECK([cat pflows1_dhcp_relay_req | grep -v NXST | grep 255.255.255.255 | grep resubmit |
+cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
+n_packets=1
+])
+
+# ====================================================
+# Send DHCP discovery with giaddr set
+giaddr=`ip_to_hex 192.168.1.1`
+# send packet
+send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 01 01 $yiaddr $giaddr $sid vif0
+
+ovs-ofctl dump-flows br-int table=12 > pflows2_dhcp_relay_req
+AT_CAPTURE_FILE([pflows2_dhcp_relay_req])
+
+AT_CHECK([cat pflows2_dhcp_relay_req | grep -v NXST | grep 255.255.255.255 | grep drop |
+cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
+n_packets=1
+])
+
+# ====================================================
+# Send DHCP valid offer
+src_mac="50540000001f"
+src_ip=`ip_to_hex 172.16.1.1`
+dst_mac="505400000002"
+dst_ip=`ip_to_hex 192.168.1.1`
+yiaddr=`ip_to_hex 192.168.1.10`
+giaddr=`ip_to_hex 192.168.1.1`
+sid=$src_ip
+# send packet
+send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
+
+ovs-ofctl dump-flows br-int table=27 > pflows1_dhcp_relay_resp
+AT_CAPTURE_FILE([pflows1_dhcp_relay_resp])
+
+AT_CHECK([cat pflows1_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep resubmit |
+cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
+n_packets=1
+])
+# ====================================================
+# Send DHCP offer with incorrect giaddr
+giaddr=`ip_to_hex 192.168.1.10`
+# send packet
+send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
+
+ovs-ofctl dump-flows br-int table=27 > pflows2_dhcp_relay_resp
+AT_CAPTURE_FILE([pflows2_dhcp_relay_resp])
+
+AT_CHECK([cat pflows2_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep drop |
+cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
+n_packets=1
+])
+
+giaddr=`ip_to_hex 192.168.1.1`
+
+# ====================================================
+# Send DHCP offer with yiaddr outside of the subnet
+yiaddr=`ip_to_hex 192.168.2.10`
+# send packet
+send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
+
+ovs-ofctl dump-flows br-int table=27 > pflows2_dhcp_relay_resp
+AT_CAPTURE_FILE([pflows2_dhcp_relay_resp])
+
+AT_CHECK([cat pflows2_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep drop |
+cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
+n_packets=2
+])
+
+yiaddr=`ip_to_hex 192.168.1.10`
+
+# ====================================================
+# Send DHCP offer with differnt server identifier
+sid=`ip_to_hex 172.16.1.100`
+# send packet
+send_dhcp_packet $src_mac $src_ip $dst_mac $dst_ip 02 02 $yiaddr $giaddr $sid ext0
+
+ovs-ofctl dump-flows br-int table=27 > pflows2_dhcp_relay_resp
+AT_CAPTURE_FILE([pflows2_dhcp_relay_resp])
+
+AT_CHECK([cat pflows2_dhcp_relay_resp | grep -v NXST | grep 172.16.1.1 | grep drop |
+cut -d ' ' -f5-5 | sed "s/,//"], [0], [dnl
+n_packets=3
+])
+
+sid=`ip_to_hex 172.16.1.1`
+
+OVN_CLEANUP([hv1])
+AT_CLEANUP
+])
+
diff --git a/tests/system-ovn.at b/tests/system-ovn.at
index c22c7882f..e7fa79727 100644
--- a/tests/system-ovn.at
+++ b/tests/system-ovn.at
@@ -12184,3 +12184,151 @@  OVS_TRAFFIC_VSWITCHD_STOP(["/failed to query port patch-.*/d
 /connection dropped.*/d"])
 AT_CLEANUP
 ])
+
+OVN_FOR_EACH_NORTHD([
+AT_SETUP([DHCP RELAY])
+AT_SKIP_IF([test $HAVE_DHCPD = no])
+AT_SKIP_IF([test $HAVE_DHCLIENT = no])
+AT_SKIP_IF([test $HAVE_TCPDUMP = no])
+ovn_start
+OVS_TRAFFIC_VSWITCHD_START()
+
+ADD_BR([br-int])
+ADD_BR([br-ext])
+
+ovs-ofctl add-flow br-ext action=normal
+# Set external-ids in br-int needed for ovn-controller
+ovs-vsctl \
+        -- set Open_vSwitch . external-ids:system-id=hv1 \
+        -- set Open_vSwitch . external-ids:ovn-remote=unix:$ovs_base/ovn-sb/ovn-sb.sock \
+        -- set Open_vSwitch . external-ids:ovn-encap-type=geneve \
+        -- set Open_vSwitch . external-ids:ovn-encap-ip=169.0.0.1 \
+        -- set bridge br-int fail-mode=secure other-config:disable-in-band=true
+
+# Start ovn-controller
+start_daemon ovn-controller
+
+ADD_NAMESPACES(sw01)
+ADD_VETH(sw01, sw01, br-int, "0", "f0:00:00:01:02:03")
+ADD_NAMESPACES(sw11)
+ADD_VETH(sw11, sw11, br-int, "0", "f0:00:00:02:02:03")
+ADD_NAMESPACES(server)
+ADD_VETH(s1, server, br-ext, "172.16.1.1/24", "f0:00:00:01:02:05", \
+         "172.16.1.254")
+
+check ovn-nbctl lr-add R1
+
+check ovn-nbctl ls-add sw0
+check ovn-nbctl ls-add sw1
+check ovn-nbctl ls-add sw-ext
+
+check ovn-nbctl lrp-add R1 rp-sw0 00:00:01:01:02:03 192.168.1.1/24
+check ovn-nbctl lrp-add R1 rp-sw1 00:00:03:01:02:03 192.168.2.1/24
+check ovn-nbctl lrp-add R1 rp-ext 00:00:02:01:02:03 172.16.1.254/24
+
+dhcp_relay=$(ovn-nbctl create DHCP_Relay servers=172.16.1.1)
+check ovn-nbctl set Logical_Router_port rp-sw0 dhcp_relay=$dhcp_relay
+check ovn-nbctl set Logical_Router_port rp-sw1 dhcp_relay=$dhcp_relay
+check ovn-nbctl lrp-set-gateway-chassis rp-ext hv1
+
+check ovn-nbctl lsp-add sw0 sw0-rp -- set Logical_Switch_Port sw0-rp \
+    type=router options:router-port=rp-sw0 \
+    -- lsp-set-addresses sw0-rp router
+check ovn-nbctl lsp-add sw1 sw1-rp -- set Logical_Switch_Port sw1-rp \
+    type=router options:router-port=rp-sw1 \
+    -- lsp-set-addresses sw1-rp router
+
+check ovn-nbctl set Logical_Switch sw0 other_config:dhcp_relay_port=sw0-rp
+check ovn-nbctl set Logical_Switch sw1 other_config:dhcp_relay_port=sw1-rp
+
+check ovn-nbctl lsp-add sw-ext ext-rp -- set Logical_Switch_Port ext-rp \
+    type=router options:router-port=rp-ext \
+    -- lsp-set-addresses ext-rp router
+check ovn-nbctl lsp-add sw-ext lnet \
+        -- lsp-set-addresses lnet unknown \
+        -- lsp-set-type lnet localnet \
+        -- lsp-set-options lnet network_name=phynet
+
+check ovn-nbctl lsp-add sw0 sw01 \
+    -- lsp-set-addresses sw01 "f0:00:00:01:02:03"
+
+check ovn-nbctl lsp-add sw1 sw11 \
+    -- lsp-set-addresses sw11 "f0:00:00:02:02:03"
+
+AT_CHECK([ovs-vsctl set Open_vSwitch . external-ids:ovn-bridge-mappings=phynet:br-ext])
+
+OVN_POPULATE_ARP
+
+check ovn-nbctl --wait=hv sync
+
+DHCP_TEST_DIR="/tmp/dhcp-test"
+rm -rf $DHCP_TEST_DIR
+mkdir $DHCP_TEST_DIR
+cat > $DHCP_TEST_DIR/dhcpd.conf <<EOF
+subnet 172.16.1.0 netmask 255.255.255.0 {
+}
+subnet 192.168.1.0 netmask 255.255.255.0 {
+  range 192.168.1.10 192.168.1.10;
+  option routers 192.168.1.1;
+  option broadcast-address 192.168.1.255;
+  default-lease-time 60;
+  max-lease-time 120;
+}
+subnet 192.168.2.0 netmask 255.255.255.0 {
+  range 192.168.2.10 192.168.2.10;
+  option routers 192.168.2.1;
+  option broadcast-address 192.168.2.255;
+  default-lease-time 60;
+  max-lease-time 120;
+}
+EOF
+cat > $DHCP_TEST_DIR/dhclien.conf <<EOF
+timeout 2
+EOF
+
+touch $DHCP_TEST_DIR/dhcpd.leases
+chown root:dhcpd $DHCP_TEST_DIR $DHCP_TEST_DIR/dhcpd.leases
+chmod 775 $DHCP_TEST_DIR
+chmod 664 $DHCP_TEST_DIR/dhcpd.leases
+
+
+NETNS_DAEMONIZE([server], [dhcpd -4 -f -cf $DHCP_TEST_DIR/dhcpd.conf s1 > dhcpd.log 2>&1], [dhcpd.pid])
+
+NS_CHECK_EXEC([server], [tcpdump -l -nvv -i s1  udp > pkt.pcap 2>tcpdump_err &])
+OVS_WAIT_UNTIL([grep "listening" tcpdump_err])
+on_exit 'kill $(pidof tcpdump)'
+
+NS_CHECK_EXEC([sw01], [dhclient -1 -q -lf $DHCP_TEST_DIR/dhclient-sw01.lease -pf $DHCP_TEST_DIR/dhclient-sw01.pid -cf $DHCP_TEST_DIR/dhclien.conf sw01])
+NS_CHECK_EXEC([sw11], [dhclient -1 -q -lf $DHCP_TEST_DIR/dhclient-sw11.lease -pf $DHCP_TEST_DIR/dhclient-sw11.pid -cf $DHCP_TEST_DIR/dhclien.conf sw11])
+
+OVS_WAIT_UNTIL([
+    total_pkts=$(cat pkt.pcap | wc -l)
+    test ${total_pkts} -ge 8
+])
+
+on_exit 'kill `cat $DHCP_TEST_DIR/dhclient-sw01.pid` &&
+kill `cat $DHCP_TEST_DIR/dhclient-sw11.pid` && rm -rf $DHCP_TEST_DIR'
+
+NS_CHECK_EXEC([sw01], [ip addr show sw01 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'], [0], [dnl
+192.168.1.10
+])
+NS_CHECK_EXEC([sw11], [ip addr show sw11 | grep -oP '(?<=inet\s)\d+(\.\d+){3}'], [0], [dnl
+192.168.2.10
+])
+OVS_APP_EXIT_AND_WAIT([ovn-controller])
+
+as ovn-sb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as ovn-nb
+OVS_APP_EXIT_AND_WAIT([ovsdb-server])
+
+as northd
+OVS_APP_EXIT_AND_WAIT([ovn-northd])
+
+as
+OVS_TRAFFIC_VSWITCHD_STOP(["/.*error receiving.*/d
+/failed to query port patch-.*/d
+/.*terminating with signal 15.*/d"])
+AT_CLEANUP
+])
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index e0f1c3ec9..903457d45 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -2329,6 +2329,63 @@  execute_put_dhcp_opts(const struct ovnact_put_opts *pdo,
     execute_put_opts(pdo, name, uflow, super);
 }
 
+static void
+execute_dhcpv4_relay_req_chk(const struct ovnact_dhcp_relay *dr,
+                             struct flow *uflow, struct ovs_list *super)
+{
+    ovntrace_node_append(
+        super, OVNTRACE_NODE_ERROR,
+        "/* We assume that this packet is DHCPDISCOVER or DHCPREQUEST. */");
+
+    struct ds s = DS_EMPTY_INITIALIZER;
+    struct mf_subfield dst = expr_resolve_field(&dr->dst);
+    if (!mf_is_register(dst.field->id)) {
+        /* Format assignment. */
+        ds_clear(&s);
+        expr_field_format(&dr->dst, &s);
+        ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
+                             "%s = 1", ds_cstr(&s));
+    }
+    ds_destroy(&s);
+
+    struct mf_subfield sf = expr_resolve_field(&dr->dst);
+    union mf_subvalue sv = { .u8_val = 1 };
+    mf_write_subfield_flow(&sf, &sv, uflow);
+}
+
+static void
+execute_dhcpv4_relay_resp_chk(const struct ovnact_dhcp_relay *dr,
+                              struct flow *uflow, struct ovs_list *super)
+{
+    ovntrace_node_append(
+        super, OVNTRACE_NODE_ERROR,
+        "/* We assume that this packet is DHCPOFFER or DHCPACK and "
+            "DHCP broadcast flag is set. Dest IP is set to broadcast. "
+            "Dest MAC is set to broadcast but in real network this is unicast "
+            "which is extracted from DHCP header. */");
+
+    /* Assume DHCP broadcast flag is set */
+    uflow->nw_dst = 0xFFFFFFFF;
+    /* Dest MAC is set to broadcast but in real network this is unicast */
+    struct eth_addr bcast_mac = { .ea = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
+    uflow->dl_dst = bcast_mac;
+
+    struct ds s = DS_EMPTY_INITIALIZER;
+    struct mf_subfield dst = expr_resolve_field(&dr->dst);
+    if (!mf_is_register(dst.field->id)) {
+        /* Format assignment. */
+        ds_clear(&s);
+        expr_field_format(&dr->dst, &s);
+        ovntrace_node_append(super, OVNTRACE_NODE_MODIFY,
+                             "%s = 1", ds_cstr(&s));
+    }
+    ds_destroy(&s);
+
+    struct mf_subfield sf = expr_resolve_field(&dr->dst);
+    union mf_subvalue sv = { .u8_val = 1 };
+    mf_write_subfield_flow(&sf, &sv, uflow);
+}
+
 static void
 execute_put_nd_ra_opts(const struct ovnact_put_opts *pdo,
                        const char *name, struct flow *uflow,
@@ -3216,6 +3273,16 @@  trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
                                   "put_dhcpv6_opts", uflow, super);
             break;
 
+        case OVNACT_DHCPV4_RELAY_REQ_CHK:
+            execute_dhcpv4_relay_req_chk(ovnact_get_DHCPV4_RELAY_REQ_CHK(a),
+                                    uflow, super);
+            break;
+
+        case OVNACT_DHCPV4_RELAY_RESP_CHK:
+            execute_dhcpv4_relay_resp_chk(ovnact_get_DHCPV4_RELAY_RESP_CHK(a),
+                                    uflow, super);
+            break;
+
         case OVNACT_PUT_ND_RA_OPTS:
             execute_put_nd_ra_opts(ovnact_get_PUT_DHCPV6_OPTS(a),
                                    "put_nd_ra_opts", uflow, super);