Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 42 additions & 12 deletions northd/northd.c
Original file line number Diff line number Diff line change
Expand Up @@ -10932,23 +10932,53 @@ build_lswitch_dns_lookup_and_response(struct ovn_datapath *od,
if (!ls_has_dns_records(od->nbs)) {
return;
}

const char *dns_server_ip =
smap_get(&od->nbs->other_config, "dns_server_ip");
const char *copp_meter = copp_meter_get(COPP_DNS, od->nbs->copp,
meter_groups);

struct ds match = DS_EMPTY_INITIALIZER;

/* IPv4 DNS lookup. */
if (dns_server_ip) {
ds_put_format(&match, "udp.dst == 53 && ip4.dst == %s", dns_server_ip);
} else {
ds_put_cstr(&match, "udp.dst == 53 && ip4");
}
ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_LOOKUP, 100, ds_cstr(&match),
REGBIT_DNS_LOOKUP_RESULT" = dns_lookup(); next;",
lflow_ref, WITH_CTRL_METER(copp_meter));
ds_clear(&match);

/* IPv6 DNS lookup. */
ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_LOOKUP, 100,
"udp.dst == 53",
"udp.dst == 53 && ip6",
REGBIT_DNS_LOOKUP_RESULT" = dns_lookup(); next;",
lflow_ref, WITH_CTRL_METER(copp_meter_get(COPP_DNS,
od->nbs->copp,
meter_groups)));
const char *dns_action = "eth.dst <-> eth.src; ip4.src <-> ip4.dst; "
lflow_ref, WITH_CTRL_METER(copp_meter));

/* IPv4 DNS response. */
if (dns_server_ip) {
ds_put_format(&match, "udp.dst == 53 && ip4.dst == %s && "
REGBIT_DNS_LOOKUP_RESULT, dns_server_ip);
} else {
ds_put_cstr(&match, "udp.dst == 53 && ip4 && "
REGBIT_DNS_LOOKUP_RESULT);
}
ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 100, ds_cstr(&match),
"eth.dst <-> eth.src; ip4.src <-> ip4.dst; "
"udp.dst = udp.src; udp.src = 53; outport = inport; "
"flags.loopback = 1; output;";
const char *dns_match = "udp.dst == 53 && "REGBIT_DNS_LOOKUP_RESULT;
"flags.loopback = 1; output;", lflow_ref);
ds_clear(&match);

/* IPv6 DNS response. */
ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 100,
dns_match, dns_action, lflow_ref);
dns_action = "eth.dst <-> eth.src; ip6.src <-> ip6.dst; "
"udp.dst == 53 && ip6 && " REGBIT_DNS_LOOKUP_RESULT,
"eth.dst <-> eth.src; ip6.src <-> ip6.dst; "
"udp.dst = udp.src; udp.src = 53; outport = inport; "
"flags.loopback = 1; output;";
ovn_lflow_add(lflows, od, S_SWITCH_IN_DNS_RESPONSE, 100,
dns_match, dns_action, lflow_ref);
"flags.loopback = 1; output;", lflow_ref);

ds_destroy(&match);
}

/* Table 24: External port. Drop ARP request for router ips from
Expand Down
25 changes: 25 additions & 0 deletions ovn-nb.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1157,6 +1157,31 @@
forwarded to all routers and therefor the mac bindings of the routers
are no longer updated.
</column>

<column name="other_config" key="dns_server_ip">
<p>
IPv4 address of OVN's virtual DNS server for this logical switch.
When set, <code>ovn-northd</code> generates DNS interception flows
that match only packets whose destination IP equals this address.
This prevents OVN from sending a packet-in to
<code>ovn-controller</code> for DNS queries directed at external
resolvers (e.g. <code>8.8.8.8</code>), reducing unnecessary CPU
overhead.
</p>

<p>
The value must be a valid IPv4 address. VMs must be configured
(e.g. via the <ref column="options" key="dns_server"
table="DHCP_Options"/> DHCP option) to use this address as their
DNS resolver so that OVN can intercept and answer their queries.
</p>

<p>
When this option is not set, OVN falls back to the default behavior
of intercepting all UDP port-53 traffic on the logical switch.
</p>
</column>

</group>

<group title="Common Columns">
Expand Down
112 changes: 112 additions & 0 deletions tests/ovn.at
Original file line number Diff line number Diff line change
Expand Up @@ -12532,6 +12532,118 @@ OVN_CLEANUP([hv1])
AT_CLEANUP
])

OVN_FOR_EACH_NORTHD([
AT_SETUP([dns lookup : dns_server_ip])
ovn_start

check ovn-nbctl ls-add ls1

check ovn-nbctl lsp-add ls1 ls1-lp1 \
-- lsp-set-addresses ls1-lp1 "f0:00:00:00:00:01 10.0.0.4"
check ovn-nbctl lsp-set-port-security ls1-lp1 "f0:00:00:00:00:01 10.0.0.4"

DNS1=`ovn-nbctl create DNS records={}`
check ovn-nbctl set DNS $DNS1 records:vm1.ovn.org="10.0.0.4"
check ovn-nbctl set Logical_switch ls1 dns_records="$DNS1"

# Only intercept DNS queries sent to this specific IP, not all UDP port-53 traffic.
check ovn-nbctl set Logical_switch ls1 other_config:dns_server_ip=10.0.0.53

net_add n1
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 hv1-vif1 -- \
set interface hv1-vif1 external-ids:iface-id=ls1-lp1 \
options:tx_pcap=hv1/vif1-tx.pcap \
options:rxq_pcap=hv1/vif1-rx.pcap \
ofport-request=1

OVN_POPULATE_ARP
wait_for_ports_up
check ovn-nbctl --wait=hv sync

ovn-sbctl dump-flows > sbflows
AT_CAPTURE_FILE([sbflows])

# DNS lookup/response flows must match only the configured dns_server_ip.
AT_CHECK([grep "ls_in_dns_lookup" sbflows | grep "ip4.dst == 10.0.0.53"], [0], [ignore])
AT_CHECK([grep "ls_in_dns_response" sbflows | grep "ip4.dst == 10.0.0.53"], [0], [ignore])
# There must be no catch-all IPv4 DNS flow.
AT_CHECK([grep "ls_in_dns_lookup" sbflows | grep -v "ip4.dst" | grep " ip4"], [1], [ignore])

# vm1.ovn.org A-record query/response data (DNS label encoding).
query_name=03766d31036f766e036f726700
dns_req_data=010201200001000000000000${query_name}00010001
dns_resp_data=010281200001000100000000${query_name}00010001${query_name}0001000100000e1000040a000004

# Build and inject a DNS/UDP/IPv4 packet. When expect_reply=1 the expected
# response is appended to $inport.expected for later OVN_CHECK_PACKETS.
test_dns_server_ip() {
local inport=$1 src_mac=$2 dst_mac=$3 src_ip=$4 dst_ip=$5 expect_reply=$6
local query_data=$7
shift 7

local ip_len udp_len
ip_len=`expr 28 + ${#query_data} / 2`
udp_len=`expr $ip_len - 20`
ip_len=$(printf "%x" $ip_len)
udp_len=$(printf "%x" $udp_len)

local request=${dst_mac}${src_mac}0800450000${ip_len}0000000080110000
request=${request}${src_ip}${dst_ip}9234003500${udp_len}0000${query_data}

if test "$expect_reply" = "1"; then
local resp_data=$1
ip_len=`expr 28 + ${#resp_data} / 2`
udp_len=`expr $ip_len - 20`
ip_len=$(printf "%x" $ip_len)
udp_len=$(printf "%x" $udp_len)
local reply=${src_mac}${dst_mac}0800450000${ip_len}0000000080110000
reply=${reply}${dst_ip}${src_ip}0035923400${udp_len}0000${resp_data}
echo $reply >> ${inport}.expected
fi
as hv1 ovs-appctl netdev-dummy/receive hv1-vif${inport} $request
}

AT_CAPTURE_FILE([ofctl_monitor0.log])
as hv1 ovs-ofctl -t 300 monitor br-int resume --detach --no-chdir \
--pidfile=ovs-ofctl0.pid 2> ofctl_monitor0.log

AS_BOX([Query to dns_server_ip should be intercepted and answered by OVN])
src_ip=`ip_to_hex 10 0 0 4`
dst_ip=`ip_to_hex 10 0 0 53`
test_dns_server_ip 1 f00000000001 f000000000d0 $src_ip $dst_ip 1 \
$dns_req_data $dns_resp_data

# NXT_RESUMEs should be 1.
OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])

OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c -48"])
# Skip the IPv4 checksum.
OVN_CHECK_PACKETS([hv1/vif1-tx.pcap], [1.expected], ["cut -c 53-"])

reset_pcap_file hv1-vif1 hv1/vif1
rm -f 1.expected

AS_BOX([Query to a different IP should not be intercepted])
src_ip=`ip_to_hex 10 0 0 4`
dst_ip=`ip_to_hex 8 8 8 8`
test_dns_server_ip 1 f00000000001 f000000000d0 $src_ip $dst_ip 0 $dns_req_data

# NXT_RESUME count must remain 1 (packet was not intercepted by OVN DNS).
OVS_WAIT_UNTIL([test 1 = `cat ofctl_monitor*.log | grep -c NXT_RESUME`])

$PYTHON "$ovs_srcdir/utilities/ovs-pcap.in" hv1/vif1-tx.pcap > 1.packets
AT_CHECK([cat 1.packets], [0], [])

OVN_CLEANUP([hv1])

AT_CLEANUP
])

OVN_FOR_EACH_NORTHD([
AT_SETUP([dns lookup : EDNS])
OVN_CHECK_SCAPY_EDNS_CLIENT_SUBNET_SUPPORT()
Expand Down