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..042b26c41 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 && ip.frag == 0 && 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 && ip.frag == 0 && 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
+])
