OVN's native DNS responder answered every A/AAAA/ANY/PTR query with a hardcoded resource-record TTL of 3600 seconds. In dynamic environments caching a DNS response for 1h is probably not desirable, in any case it's better if the TTL is operator configurable.
Add a per-row "options:ttl" key on the NB DNS table. ovn-northd validates the value in sync_dns_entries() and, like the existing "ovn-owned" option, propagates it to the SB DNS table. ovn-controller reads the option back and uses it when building DNS replies for that record. The default remains 3600 seconds. Reported-at: https://redhat.atlassian.net/browse/FDP-2956 Assisted-by: Qwen3.8-27B-FP8, opencode Signed-off-by: Dumitru Ceara <[email protected]> --- NEWS | 2 + controller/ovn-dns.c | 7 ++- controller/ovn-dns.h | 2 +- controller/pinctrl.c | 27 +++++---- lib/ovn-l7.h | 1 + northd/northd.c | 22 +++++++ ovn-nb.xml | 14 +++++ ovn-sb.xml | 9 +++ tests/ovn-northd.at | 60 +++++++++++++++++++ tests/ovn.at | 138 +++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 268 insertions(+), 14 deletions(-) diff --git a/NEWS b/NEWS index aadf6416d2..f1c56dd147 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,7 @@ Post v26.09.0 -------------- + - Added a new "options:ttl" key on the NB DNS table to make the TTL of + DNS replies from OVN's native DNS resolver configurable per row. - Removed implementations of the commit_ecmp_nh, chk_ecmp_nh, and chk_ecmp_nh_mac actions from the code. - Mark tunnel ports as transient (other_config:transient=true) when the diff --git a/controller/ovn-dns.c b/controller/ovn-dns.c index bfeef5a747..416f399d78 100644 --- a/controller/ovn-dns.c +++ b/controller/ovn-dns.c @@ -22,6 +22,7 @@ #include "openvswitch/vlog.h" /* OVN includes. */ +#include "lib/ovn-l7.h" #include "lib/ovn-sb-idl.h" #include "ovn-dns.h" @@ -112,12 +113,14 @@ ovn_dns_update_cache(const struct sbrec_dns_table *dns_table) } const char * -ovn_dns_lookup(const char *query_name, uint64_t dp_key, bool *ovn_owned) +ovn_dns_lookup(const char *query_name, uint64_t dp_key, bool *ovn_owned, + uint32_t *ttl) { const char *answer_data = NULL; struct dns_data *dns_data; *ovn_owned = false; + *ttl = DNS_DEFAULT_RR_TTL; CMAP_FOR_EACH (dns_data, cmap_node, &dns_cache_) { for (size_t i = 0; i < dns_data->n_dps; i++) { @@ -131,6 +134,8 @@ ovn_dns_lookup(const char *query_name, uint64_t dp_key, bool *ovn_owned) if (answer_data) { *ovn_owned = smap_get_bool(&dns_data->options, "ovn-owned", false); + *ttl = smap_get_uint(&dns_data->options, "ttl", + DNS_DEFAULT_RR_TTL); break; } } diff --git a/controller/ovn-dns.h b/controller/ovn-dns.h index 8eca6ad0eb..6c86ccc858 100644 --- a/controller/ovn-dns.h +++ b/controller/ovn-dns.h @@ -24,6 +24,6 @@ void ovn_dns_cache_destroy(void); void ovn_dns_sync_cache(const struct sbrec_dns_table *); void ovn_dns_update_cache(const struct sbrec_dns_table *); const char *ovn_dns_lookup(const char *query_name, uint64_t dp_key, - bool *ovn_owned); + bool *ovn_owned, uint32_t *ttl); #endif /* OVN_DNS_H */ diff --git a/controller/pinctrl.c b/controller/pinctrl.c index 333dcedb6e..e100912510 100644 --- a/controller/pinctrl.c +++ b/controller/pinctrl.c @@ -3394,22 +3394,22 @@ put_be32(struct ofpbuf *buf, ovs_be32 x) static void dns_build_base_answer( struct ofpbuf *dns_answer, const uint8_t *in_queryname, - uint16_t query_length, int query_type) + uint16_t query_length, int query_type, uint32_t ttl) { ofpbuf_put(dns_answer, in_queryname, query_length); put_be16(dns_answer, htons(query_type)); put_be16(dns_answer, htons(DNS_CLASS_IN)); - put_be32(dns_answer, htonl(DNS_DEFAULT_RR_TTL)); + put_be32(dns_answer, htonl(ttl)); } /* Populates dns_answer struct with a TYPE A answer. */ static void dns_build_a_answer( struct ofpbuf *dns_answer, const uint8_t *in_queryname, - uint16_t query_length, const ovs_be32 addr) + uint16_t query_length, const ovs_be32 addr, uint32_t ttl) { dns_build_base_answer(dns_answer, in_queryname, query_length, - DNS_QUERY_TYPE_A); + DNS_QUERY_TYPE_A, ttl); put_be16(dns_answer, htons(sizeof(ovs_be32))); put_be32(dns_answer, addr); } @@ -3418,10 +3418,10 @@ dns_build_a_answer( static void dns_build_aaaa_answer( struct ofpbuf *dns_answer, const uint8_t *in_queryname, - uint16_t query_length, const struct in6_addr *addr) + uint16_t query_length, const struct in6_addr *addr, uint32_t ttl) { dns_build_base_answer(dns_answer, in_queryname, query_length, - DNS_QUERY_TYPE_AAAA); + DNS_QUERY_TYPE_AAAA, ttl); put_be16(dns_answer, htons(sizeof(*addr))); ofpbuf_put(dns_answer, addr, sizeof(*addr)); } @@ -3430,10 +3430,10 @@ dns_build_aaaa_answer( static void dns_build_ptr_answer( struct ofpbuf *dns_answer, const uint8_t *in_queryname, - uint16_t query_length, const char *answer_data) + uint16_t query_length, const char *answer_data, uint32_t ttl) { dns_build_base_answer(dns_answer, in_queryname, query_length, - DNS_QUERY_TYPE_PTR); + DNS_QUERY_TYPE_PTR, ttl); size_t encoded_len = 0; char *encoded = encode_fqdn_string(answer_data, &encoded_len); @@ -3586,8 +3586,9 @@ pinctrl_handle_dns_lookup( uint64_t dp_key = ntohll(pin->flow_metadata.flow.metadata); bool ovn_owned = false; + uint32_t ttl = DNS_DEFAULT_RR_TTL; const char *answer_data = ovn_dns_lookup(ds_cstr(&query_name), dp_key, - &ovn_owned); + &ovn_owned, &ttl); ds_destroy(&query_name); if (!answer_data) { COVERAGE_INC(dns_cache_miss); @@ -3601,7 +3602,8 @@ pinctrl_handle_dns_lookup( struct ofpbuf dns_answer = OFPBUF_STUB_INITIALIZER(dns_ans_stub); if (query_type == DNS_QUERY_TYPE_PTR) { - dns_build_ptr_answer(&dns_answer, in_queryname, idx, answer_data); + dns_build_ptr_answer(&dns_answer, in_queryname, idx, answer_data, + ttl); ancount++; } else { struct lport_addresses ip_addrs; @@ -3617,7 +3619,7 @@ pinctrl_handle_dns_lookup( query_type == DNS_QUERY_TYPE_ANY) { for (size_t i = 0; i < ip_addrs.n_ipv4_addrs; i++) { ovs_be32 addr = ip_addrs.ipv4_addrs[ipv4_order[i]].addr; - dns_build_a_answer(&dns_answer, in_queryname, idx, addr); + dns_build_a_answer(&dns_answer, in_queryname, idx, addr, ttl); ancount++; } } @@ -3627,7 +3629,8 @@ pinctrl_handle_dns_lookup( for (size_t i = 0; i < ip_addrs.n_ipv6_addrs; i++) { struct in6_addr *addr = &ip_addrs.ipv6_addrs[ipv6_order[i]].addr; - dns_build_aaaa_answer(&dns_answer, in_queryname, idx, addr); + dns_build_aaaa_answer(&dns_answer, in_queryname, idx, addr, + ttl); ancount++; } } diff --git a/lib/ovn-l7.h b/lib/ovn-l7.h index 5f52e1791f..58b8a4a1c3 100644 --- a/lib/ovn-l7.h +++ b/lib/ovn-l7.h @@ -52,6 +52,7 @@ BUILD_ASSERT_DECL(BFD_PACKET_LEN == sizeof(struct bfd_msg)); #define DNS_CLASS_IN 0x01 #define DNS_DEFAULT_RR_TTL 3600 +#define DNS_MAX_RR_TTL 2147483647U /* 2^31-1, per RFC 2181. */ /* Generic options map which is used to store dhcpv4 opts and dhcpv6 opts. */ struct gen_opts_map { diff --git a/northd/northd.c b/northd/northd.c index 1c9e5d0703..a0b90646f0 100644 --- a/northd/northd.c +++ b/northd/northd.c @@ -21618,6 +21618,28 @@ sync_dns_entries(struct ovsdb_idl_txn *ovnsb_txn, "ovn-owned", false); smap_replace(&options, "ovn-owned", ovn_owned? "true" : "false"); + + /* Sync the per-row TTL option to SB. Only values in the range + * 0..DNS_MAX_RR_TTL are valid. + */ + static struct vlog_rate_limit rl = VLOG_RATE_LIMIT_INIT(5, 1); + const char *ttl = smap_get(&dns_info->nb_dns->options, "ttl"); + if (ttl) { + unsigned int ttl_val; + if (str_to_uint(ttl, 10, &ttl_val) && ttl_val <= DNS_MAX_RR_TTL) { + smap_replace(&options, "ttl", ttl); + } else { + VLOG_WARN_RL(&rl, "Ignoring invalid \"options:ttl\" %s " + "(expected 0-%"PRIu32") for DNS record " + UUID_FMT, ttl, DNS_MAX_RR_TTL, + UUID_ARGS(&dns_info->nb_dns->header_.uuid)); + smap_remove(&options, "ttl"); + } + } else { + /* Remove any stale value so the default TTL is used. */ + smap_remove(&options, "ttl"); + } + sbrec_dns_set_options(dns_info->sb_dns, &options); smap_destroy(&options); diff --git a/ovn-nb.xml b/ovn-nb.xml index 57b81d4b44..078bd60687 100644 --- a/ovn-nb.xml +++ b/ovn-nb.xml @@ -6464,6 +6464,20 @@ or </p> </column> + <column name="options" key="ttl"> + The time-to-live (in seconds) set in the TTL field of DNS replies + generated by OVN for the records in this row. + + <p> + The default is <code>3600</code> seconds. Set this to a smaller + value in dynamic environments (e.g. VM fail-over) so that + clients do not cache stale records for long. Valid values are 0 + to 2147483647 (inclusive). An invalid or out-of-range value is + rejected with a warning by ovn-northd and the default of 3600 + seconds is used instead. + </p> + </column> + <column name="external_ids"> See <em>External IDs</em> at the beginning of this document. </column> diff --git a/ovn-sb.xml b/ovn-sb.xml index 808694fe0a..2096fc3e3c 100644 --- a/ovn-sb.xml +++ b/ovn-sb.xml @@ -5015,6 +5015,15 @@ tcp.flags = RST; will be answered locally by either an IP address or <code>DNS rejection</code>. </column> + + <column name="options" key="ttl"> + The time-to-live (in seconds) set in the TTL field of DNS replies for + the records in this row. This is synced automatically by ovn-northd + from the corresponding + <ref table="DNS" column="options" key="ttl" db="OVN_Northbound"/> + option and should not be set directly. + </column> + <group title="Common Columns"> <column name="external_ids"> See <em>External IDs</em> at the beginning of this document. diff --git a/tests/ovn-northd.at b/tests/ovn-northd.at index af231fb871..7d9bd68541 100644 --- a/tests/ovn-northd.at +++ b/tests/ovn-northd.at @@ -12354,6 +12354,66 @@ wait_row_count sb:DNS 0 AT_CLEANUP ]) +OVN_FOR_EACH_NORTHD_NO_HV([ +AT_SETUP([DNS options:ttl synchronization to SB]) +ovn_start + +check ovn-nbctl ls-add sw0 +dns_uuid=$(ovn-nbctl create DNS records={}) +check ovn-nbctl set DNS $dns_uuid records:vm1.ovn.org="10.0.0.4" +check ovn-nbctl set Logical_Switch sw0 dns_records="$dns_uuid" +check ovn-nbctl --wait=sb sync + +wait_row_count sb:DNS 1 +sb_dns_uuid=$(fetch_column sb:DNS _uuid) + +# Without "options:ttl", the key is not present in SB. +AT_CHECK([test -z "$(ovn-sbctl --if-exists get DNS $sb_dns_uuid options:ttl)"]) + +# Valid values are synced to SB, including the boundary values 0 and the +# RFC 2181 maximum of 2147483647. +check ovn-nbctl --wait=sb set DNS $dns_uuid options:ttl=30 +AT_CHECK([ovn-sbctl get DNS $sb_dns_uuid options:ttl], [0], [dnl +"30" +]) + +check ovn-nbctl --wait=sb set DNS $dns_uuid options:ttl=0 +AT_CHECK([ovn-sbctl get DNS $sb_dns_uuid options:ttl], [0], [dnl +"0" +]) + +check ovn-nbctl --wait=sb set DNS $dns_uuid options:ttl=2147483647 +AT_CHECK([ovn-sbctl get DNS $sb_dns_uuid options:ttl], [0], [dnl +"2147483647" +]) + +# Invalid values are rejected and the key is removed from SB. +check ovn-nbctl --wait=sb set DNS $dns_uuid options:ttl=garbage +AT_CHECK([test -z "$(ovn-sbctl --if-exists get DNS $sb_dns_uuid options:ttl)"]) + +check ovn-nbctl --wait=sb set DNS $dns_uuid options:ttl=-5 +AT_CHECK([test -z "$(ovn-sbctl --if-exists get DNS $sb_dns_uuid options:ttl)"]) + +# A value above the 2^31-1 cap is out of range and rejected. +check ovn-nbctl --wait=sb set DNS $dns_uuid options:ttl=2147483648 +AT_CHECK([test -z "$(ovn-sbctl --if-exists get DNS $sb_dns_uuid options:ttl)"]) + +# A value that overflows the 32-bit unsigned range is rejected as well. +check ovn-nbctl --wait=sb set DNS $dns_uuid options:ttl=99999999999 +AT_CHECK([test -z "$(ovn-sbctl --if-exists get DNS $sb_dns_uuid options:ttl)"]) + +# Removing the option clears any previously synced value. +check ovn-nbctl --wait=sb set DNS $dns_uuid options:ttl=30 +AT_CHECK([ovn-sbctl get DNS $sb_dns_uuid options:ttl], [0], [dnl +"30" +]) +check ovn-nbctl --wait=sb remove DNS $dns_uuid options ttl +AT_CHECK([test -z "$(ovn-sbctl --if-exists get DNS $sb_dns_uuid options:ttl)"]) + +OVN_CLEANUP_NORTHD +AT_CLEANUP +]) + OVN_FOR_EACH_NORTHD_NO_HV([ AT_SETUP([ACL/Meter incremental processing - no northd recompute]) ovn_start diff --git a/tests/ovn.at b/tests/ovn.at index 8acfecb7bc..7f823de1da 100644 --- a/tests/ovn.at +++ b/tests/ovn.at @@ -12677,6 +12677,144 @@ OVN_CLEANUP([hv1]) AT_CLEANUP ]) +OVN_FOR_EACH_NORTHD([ +AT_SETUP([dns lookup : configurable TTL]) +CHECK_SCAPY +ovn_start + +check ovn-nbctl ls-add ls \ + -- lsp-add ls lsp \ + -- lsp-set-addresses lsp "00:00:00:00:00:01 10.0.0.1" + +d=$(ovn-nbctl create DNS records={}) +check ovn-nbctl set DNS $d records:foo.ovn.org="10.0.0.42 aef0::42" +check ovn-nbctl set DNS $d records:42.0.0.10.in-addr.arpa="foo.ovn.org" +check ovn-nbctl set Logical_switch ls dns_records="$d" + +net_add n1 +sim_add hv1 + +as hv1 +ovs-vsctl add-br br-phys +ovn_attach n1 br-phys 192.168.0.1 +check ovs-vsctl add-port br-int hv1-vif1 -- \ + set interface hv1-vif1 external-ids:iface-id=lsp \ + options:tx_pcap=hv1/vif1-tx.pcap \ + options:rxq_pcap=hv1/vif1-rx.pcap + +OVN_POPULATE_ARP +wait_for_ports_up +check ovn-nbctl --wait=hv sync + +dns_req_a=$(fmt_pkt "Ether(dst='00:00:00:00:00:02', src='00:00:00:00:00:01') / \ + IP(dst='10.0.0.254', src='10.0.0.1') / \ + UDP(sport=42424, dport=53) / \ + DNS(rd=1, qd=DNSQR(qname='foo.ovn.org', qtype='A'))") +dns_req_aaaa=$(fmt_pkt "Ether(dst='00:00:00:00:00:02', src='00:00:00:00:00:01') / \ + IP(dst='10.0.0.254', src='10.0.0.1') / \ + UDP(sport=42424, dport=53) / \ + DNS(rd=1, qd=DNSQR(qname='foo.ovn.org', qtype='AAAA'))") +dns_req_ptr=$(fmt_pkt "Ether(dst='00:00:00:00:00:02', src='00:00:00:00:00:01') / \ + IP(dst='10.0.0.254', src='10.0.0.1') / \ + UDP(sport=42424, dport=53) / \ + DNS(rd=1, qd=DNSQR(qname='42.0.0.10.in-addr.arpa', qtype='PTR'))") +dns_req_any=$(fmt_pkt "Ether(dst='00:00:00:00:00:02', src='00:00:00:00:00:01') / \ + IP(dst='10.0.0.254', src='10.0.0.1') / \ + UDP(sport=42424, dport=53) / \ + DNS(rd=1, qd=DNSQR(qname='foo.ovn.org', qtype='ALL'))") + +# Without "options:ttl", the reply uses the default TTL of 3600. +dns_reply=$(fmt_pkt "Ether(dst='00:00:00:00:00:01', src='00:00:00:00:00:02') / \ + IP(dst='10.0.0.1', src='10.0.0.254') / \ + UDP(sport=53, dport=42424, chksum=0) / \ + DNS(qr=1, qd=DNSQR(qname='foo.ovn.org', qtype='A'), \ + an=DNSRR(rrname='foo.ovn.org', type='A', ttl=3600, \ + rdata='10.0.0.42'))") +echo ${dns_reply} > expected +as hv1 reset_pcap_file hv1-vif1 hv1/vif1 +as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 ${dns_req_a} +OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [expected]) + +# options:ttl=30 is applied to the A, AAAA and PTR replies alike. +check ovn-nbctl --wait=hv set DNS $d options:ttl=30 + +dns_reply=$(fmt_pkt "Ether(dst='00:00:00:00:00:01', src='00:00:00:00:00:02') / \ + IP(dst='10.0.0.1', src='10.0.0.254') / \ + UDP(sport=53, dport=42424, chksum=0) / \ + DNS(qr=1, qd=DNSQR(qname='foo.ovn.org', qtype='A'), \ + an=DNSRR(rrname='foo.ovn.org', type='A', ttl=30, \ + rdata='10.0.0.42'))") +echo ${dns_reply} > expected +as hv1 reset_pcap_file hv1-vif1 hv1/vif1 +as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 ${dns_req_a} +OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [expected]) + +dns_reply=$(fmt_pkt "Ether(dst='00:00:00:00:00:01', src='00:00:00:00:00:02') / \ + IP(dst='10.0.0.1', src='10.0.0.254') / \ + UDP(sport=53, dport=42424, chksum=0) / \ + DNS(qr=1, qd=DNSQR(qname='foo.ovn.org', qtype='AAAA'), \ + an=DNSRR(rrname='foo.ovn.org', type='AAAA', ttl=30, \ + rdata='aef0::42'))") +echo ${dns_reply} > expected +as hv1 reset_pcap_file hv1-vif1 hv1/vif1 +as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 ${dns_req_aaaa} +OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [expected]) + +dns_reply=$(fmt_pkt "Ether(dst='00:00:00:00:00:01', src='00:00:00:00:00:02') / \ + IP(dst='10.0.0.1', src='10.0.0.254') / \ + UDP(sport=53, dport=42424, chksum=0) / \ + DNS(qr=1, qd=DNSQR(qname='42.0.0.10.in-addr.arpa', qtype='PTR'), \ + an=DNSRR(rrname='42.0.0.10.in-addr.arpa', type='PTR', \ + ttl=30, rdata='foo.ovn.org'))") +echo ${dns_reply} > expected +as hv1 reset_pcap_file hv1-vif1 hv1/vif1 +as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 ${dns_req_ptr} +OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [expected]) + +# The ANY reply carries the A and AAAA RRs, each with the per-row TTL. +dns_reply=$(fmt_pkt "Ether(dst='00:00:00:00:00:01', src='00:00:00:00:00:02') / \ + IP(dst='10.0.0.1', src='10.0.0.254') / \ + UDP(sport=53, dport=42424, chksum=0) / \ + DNS(qr=1, qd=DNSQR(qname='foo.ovn.org', qtype='ALL'), \ + an=[[DNSRR(rrname='foo.ovn.org', type='A', \ + ttl=30, rdata='10.0.0.42'), \ + DNSRR(rrname='foo.ovn.org', type='AAAA', \ + ttl=30, rdata='aef0::42')]])") +echo ${dns_reply} > expected +as hv1 reset_pcap_file hv1-vif1 hv1/vif1 +as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 ${dns_req_any} +OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [expected]) + +# Removing the option restores the default TTL of 3600. +check ovn-nbctl --wait=hv remove DNS $d options ttl +dns_reply=$(fmt_pkt "Ether(dst='00:00:00:00:00:01', src='00:00:00:00:00:02') / \ + IP(dst='10.0.0.1', src='10.0.0.254') / \ + UDP(sport=53, dport=42424, chksum=0) / \ + DNS(qr=1, qd=DNSQR(qname='foo.ovn.org', qtype='A'), \ + an=DNSRR(rrname='foo.ovn.org', type='A', ttl=3600, \ + rdata='10.0.0.42'))") +echo ${dns_reply} > expected +as hv1 reset_pcap_file hv1-vif1 hv1/vif1 +as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 ${dns_req_a} +OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [expected]) + +# options:ttl=0 is honored (a reply TTL of 0). +check ovn-nbctl --wait=hv set DNS $d options:ttl=0 +dns_reply=$(fmt_pkt "Ether(dst='00:00:00:00:00:01', src='00:00:00:00:00:02') / \ + IP(dst='10.0.0.1', src='10.0.0.254') / \ + UDP(sport=53, dport=42424, chksum=0) / \ + DNS(qr=1, qd=DNSQR(qname='foo.ovn.org', qtype='A'), \ + an=DNSRR(rrname='foo.ovn.org', type='A', ttl=0, \ + rdata='10.0.0.42'))") +echo ${dns_reply} > expected +as hv1 reset_pcap_file hv1-vif1 hv1/vif1 +as hv1 ovs-appctl netdev-dummy/receive hv1-vif1 ${dns_req_a} +OVN_CHECK_PACKETS_REMOVE_BROADCAST([hv1/vif1-tx.pcap], [expected]) + +OVN_CLEANUP([hv1]) +AT_CLEANUP +]) + OVN_FOR_EACH_NORTHD([ AT_SETUP([4 HV, 1 LS, 1 LR, packet test with HA distributed router gateway port]) ovn_start -- 2.55.0 _______________________________________________ dev mailing list [email protected] https://mail.openvswitch.org/mailman/listinfo/ovs-dev
