diff --git a/man/rules/meson.build b/man/rules/meson.build index 7385658d6a..499fe6d19e 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -662,6 +662,7 @@ manpages = [ ['systemd.link', '5', [], ''], ['systemd.mount', '5', [], ''], ['systemd.netdev', '5', [], 'ENABLE_NETWORKD'], + ['systemd.dnssd', '5', [], 'ENABLE_RESOLVE'], ['systemd.network', '5', [], 'ENABLE_NETWORKD'], ['systemd.nspawn', '5', [], ''], ['systemd.offline-updates', '7', [], ''], diff --git a/man/systemd-resolve.xml b/man/systemd-resolve.xml index 835521aa74..c4af2d8662 100644 --- a/man/systemd-resolve.xml +++ b/man/systemd-resolve.xml @@ -404,6 +404,7 @@ _443._tcp.fedoraproject.org IN TLSA 0 0 1 19400be5b7a31fb733917700789d2f0a2471c0 systemd1, systemd-resolved.service8 + systemd.dnssd5 diff --git a/man/systemd.dnssd.xml b/man/systemd.dnssd.xml new file mode 100644 index 0000000000..1270e08cd2 --- /dev/null +++ b/man/systemd.dnssd.xml @@ -0,0 +1,258 @@ + + + + + + + + + systemd.dnssd + systemd + + + + Developer + Dmitry + Rozhkov + dmitry.rozhkov@intel.com + + + + + + systemd.dnssd + 5 + + + + systemd.dnssd + DNS-SD configuration + + + + network_service.dnssd + + + + Description + + DNS-SD setup is performed by + systemd-resolved8. + + + The main network service file must have the extension .dnssd; other + extensions are ignored. + + The .dnssd files are read from the files located in the system + network directory /usr/lib/systemd/dnssd, the volatile runtime network + directory /run/systemd/dnssd and the local administration network + directory /etc/systemd/dnssd. All configuration files are collectively + sorted and processed in lexical order, regardless of the directories in which they live. + However, files with identical filenames replace each other. Files in /etc + have the highest priority, files in /run take precedence over files with + the same name in /usr/lib. This can be used to override a system-supplied + configuration file with a local file if needed. + + Along with the network service file foo.dnssd, a "drop-in" directory + foo.dnssd.d/ may exist. All files with the suffix + .conf from this directory will be parsed after the file itself is + parsed. This is useful to alter or add configuration settings, without having to modify the main + configuration file. Each drop-in file must have appropriate section headers. + + In addition to /etc/systemd/dnssd, drop-in .d + directories can be placed in /usr/lib/systemd/dnssd or + /run/systemd/dnssd directories. Drop-in files in + /etc take precedence over those in /run which in turn + take precedence over those in /usr/lib. Drop-in files under any of these + directories take precedence over the main network service file wherever located. (Of course, since + /run is temporary and /usr/lib is for vendors, it is + unlikely drop-ins should be used in either of those places.) + + + + [Service] Section Options + + The network service file contains a [Service] + section, which specifies a discoverable network service announced in a + local network with Multicast DNS broadcasts. + + + + Name= + + An instance name of the network service as defined in the section 4.1.1 of RFC 6763, e.g. webserver. + The option supports simple specifier expansion. The following expansions are understood: + + Specifiers available + + + + + + + Specifier + Meaning + Details + + + + + %m + Machine ID + The machine ID of the running system, formatted as string. See machine-id5 for more information. + + + %b + Boot ID + The boot ID of the running system, formatted as string. See random4 for more information. + + + %H + Host name + The hostname of the running system. + + + %v + Kernel release + Identical to uname -r output. + + + +
+
+
+ + Type= + + A type of the network service as defined in the section 4.1.2 of RFC 6763, e.g. _http._tcp. + + + + + Port= + + An IP port number of the network service. + + + + Priority= + + A priority number set in SRV resource records corresponding to the network service. + + + + Weight= + + A weight number set in SRV resource records corresponding to the network service. + + + + TxtText= + + A whitespace-separated list of arbitrary key/value pairs + conveying additional information about the named service in the corresponding TXT resource record, + e.g. path=/portal/index.html. Keys and values can contain C-style escape + sequences which get translated upon reading configuration files. + + This option together with TxtData= may be specified more than once, in which + case multiple TXT resource records will be created for the service. If the empty string is assigned to + this option, the list is reset and all prior assignments will have no effect. + + + + + TxtData= + + A whitespace-separated list of arbitrary key/value pairs + conveying additional information about the named service in the corresponding TXT resource record + where values are base64-encoded string representing any binary data, + e.g. data=YW55IGJpbmFyeSBkYXRhCg==. Keys can contain C-style escape + sequences which get translated upon reading configuration files. + + This option together with TxtText= may be specified more than once, in which + case multiple TXT resource records will be created for the service. If the empty string is assigned to + this option, the list is reset and all prior assignments will have no effect. + + + +
+ +
+ + + Examples + + HTTP service + + # /etc/systemd/dnssd/http.dnssd +[Service] +Name=%h +Type=_http._tcp +Port=80 +TxtText=path=/stats/index.html t=temperature_sensor + + This makes the http server running on the host discoverable in the local network + given MulticastDNS is enabled on the network interface. + + Now the utility systemd-resolve should be able to resolve the + service to the host's name: + + $ systemd-resolve --service meteo._http._tcp.local +meteo._http._tcp.local: meteo.local:80 [priority=0, weight=0] + 169.254.208.106%senp0s21f0u2u4 + fe80::213:3bff:fe49:8aa%senp0s21f0u2u4 + path=/stats/index.html + t=temperature_sensor + (meteo/_http._tcp/local) + +-- Information acquired via protocol mDNS/IPv6 in 4.0ms. +-- Data is authenticated: yes + + Avahi running on a different host in the same local network should see the service as well: + + $ avahi-browse -a -r ++ enp3s0 IPv6 meteo Web Site local ++ enp3s0 IPv4 meteo Web Site local += enp3s0 IPv6 meteo Web Site local + hostname = [meteo.local] + address = [fe80::213:3bff:fe49:8aa] + port = [80] + txt = ["path=/stats/index.html" "t=temperature_sensor"] += enp3s0 IPv4 meteo Web Site local + hostname = [meteo.local] + address = [169.254.208.106] + port = [80] + txt = ["path=/stats/index.html" "t=temperature_sensor"] + + + + + + See Also + + systemd1, + systemd-resolved.service8 + + + +
diff --git a/src/libsystemd/sd-bus/bus-common-errors.h b/src/libsystemd/sd-bus/bus-common-errors.h index 7317b174b3..42c0f528f0 100644 --- a/src/libsystemd/sd-bus/bus-common-errors.h +++ b/src/libsystemd/sd-bus/bus-common-errors.h @@ -83,6 +83,8 @@ #define BUS_ERROR_NO_SUCH_LINK "org.freedesktop.resolve1.NoSuchLink" #define BUS_ERROR_LINK_BUSY "org.freedesktop.resolve1.LinkBusy" #define BUS_ERROR_NETWORK_DOWN "org.freedesktop.resolve1.NetworkDown" +#define BUS_ERROR_NO_SUCH_DNSSD_SERVICE "org.freedesktop.resolve1.NoSuchDnssdService" +#define BUS_ERROR_DNSSD_SERVICE_EXISTS "org.freedesktop.resolve1.DnssdServiceExists" #define _BUS_ERROR_DNS "org.freedesktop.resolve1.DnsError." #define BUS_ERROR_NO_SUCH_TRANSFER "org.freedesktop.import1.NoSuchTransfer" diff --git a/src/resolve/meson.build b/src/resolve/meson.build index dbc63f15c2..ee1acb5166 100644 --- a/src/resolve/meson.build +++ b/src/resolve/meson.build @@ -35,6 +35,10 @@ systemd_resolved_only_sources = files(''' resolved.c resolved-manager.c resolved-manager.h + resolved-dnssd.c + resolved-dnssd.h + resolved-dnssd-bus.c + resolved-dnssd-bus.h resolved-conf.c resolved-conf.h resolved-resolv-conf.c @@ -131,8 +135,14 @@ resolved_gperf_c = custom_target( output : 'resolved-gperf.c', command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) +resolved_dnssd_gperf_c = custom_target( + 'resolved_dnssd_gperf.c', + input : 'resolved-dnssd-gperf.gperf', + output : 'resolved-dnssd-gperf.c', + command : [gperf, '@INPUT@', '--output-file', '@OUTPUT@']) + systemd_resolved_sources = (basic_dns_sources + - [resolved_gperf_c] + + [resolved_gperf_c, resolved_dnssd_gperf_c] + systemd_resolved_only_sources + dns_type_headers) @@ -155,6 +165,15 @@ if conf.get('ENABLE_RESOLVE') == 1 install_data('resolv.conf', install_dir : rootlibexecdir) + + i18n.merge_file( + 'org.freedesktop.resolve1.policy', + input : 'org.freedesktop.resolve1.policy.in', + output : 'org.freedesktop.resolve1.policy', + po_dir : po_dir, + data_dirs : po_dir, + install : install_polkit, + install_dir : polkitpolicydir) endif tests += [ diff --git a/src/resolve/org.freedesktop.resolve1.policy.in b/src/resolve/org.freedesktop.resolve1.policy.in new file mode 100644 index 0000000000..da948eb0b7 --- /dev/null +++ b/src/resolve/org.freedesktop.resolve1.policy.in @@ -0,0 +1,43 @@ + + + + + + + + The systemd Project + http://www.freedesktop.org/wiki/Software/systemd + + + Register a DNS-SD service + Authentication is required to register a DNS-SD service + + auth_admin + auth_admin + auth_admin_keep + + unix-user:systemd-resolve + + + + Unregister a DNS-SD service + Authentication is required to unregister a DNS-SD service + + auth_admin + auth_admin + auth_admin_keep + + unix-user:systemd-resolve + + + diff --git a/src/resolve/resolved-bus.c b/src/resolve/resolved-bus.c index 700a7d5ee0..9157d9ea68 100644 --- a/src/resolve/resolved-bus.c +++ b/src/resolve/resolved-bus.c @@ -25,7 +25,11 @@ #include "resolved-bus.h" #include "resolved-def.h" #include "resolved-dns-synthesize.h" +#include "resolved-dnssd.h" +#include "resolved-dnssd-bus.h" #include "resolved-link-bus.h" +#include "user-util.h" +#include "utf8.h" static int reply_query_state(DnsQuery *q) { @@ -1581,6 +1585,232 @@ static int bus_method_reset_server_features(sd_bus_message *message, void *userd return sd_bus_reply_method_return(message, NULL); } +static int on_bus_track(sd_bus_track *t, void *userdata) { + DnssdService *s = userdata; + + assert(t); + assert(s); + + log_debug("Client of active request vanished, destroying DNS-SD service."); + dnssd_service_free(s); + + return 0; +} + +static int bus_method_register_service(sd_bus_message *message, void *userdata, sd_bus_error *error) { + _cleanup_(sd_bus_creds_unrefp) sd_bus_creds *creds = NULL; + _cleanup_(dnssd_service_freep) DnssdService *service = NULL; + _cleanup_(sd_bus_track_unrefp) sd_bus_track *bus_track = NULL; + _cleanup_free_ char *path = NULL; + _cleanup_free_ char *instance_name = NULL; + Manager *m = userdata; + DnssdService *s = NULL; + const char *name; + const char *name_template; + const char *type; + uid_t euid; + int r; + + assert(message); + assert(m); + + if (m->mdns_support != RESOLVE_SUPPORT_YES) + return sd_bus_error_setf(error, SD_BUS_ERROR_NOT_SUPPORTED, "Support for MulticastDNS is disabled"); + + r = bus_verify_polkit_async(message, CAP_SYS_ADMIN, + "org.freedesktop.resolve1.register-service", + NULL, false, UID_INVALID, + &m->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + service = new0(DnssdService, 1); + if (!service) + return log_oom(); + + r = sd_bus_query_sender_creds(message, SD_BUS_CREDS_EUID, &creds); + if (r < 0) + return r; + + r = sd_bus_creds_get_euid(creds, &euid); + if (r < 0) + return r; + service->originator = euid; + + r = sd_bus_message_read(message, "sssqqq", &name, &name_template, &type, + &service->port, &service->priority, + &service->weight); + if (r < 0) + return r; + + s = hashmap_get(m->dnssd_services, name); + if (s) + return sd_bus_error_setf(error, BUS_ERROR_DNSSD_SERVICE_EXISTS, "DNS-SD service '%s' exists already", name); + + if (!dnssd_srv_type_is_valid(type)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "DNS-SD service type '%s' is invalid", type); + + service->name = strdup(name); + if (!service->name) + return log_oom(); + + service->name_template = strdup(name_template); + if (!service->name_template) + return log_oom(); + + service->type = strdup(type); + if (!service->type) + return log_oom(); + + r = dnssd_render_instance_name(service, &instance_name); + if (r < 0) + return r; + + r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "a{say}"); + if (r < 0) + return r; + + while ((r = sd_bus_message_enter_container(message, SD_BUS_TYPE_ARRAY, "{say}")) > 0) { + _cleanup_(dnssd_txtdata_freep) DnssdTxtData *txt_data = NULL; + DnsTxtItem *last = NULL; + + txt_data = new0(DnssdTxtData, 1); + if (!txt_data) + return log_oom(); + + while ((r = sd_bus_message_enter_container(message, SD_BUS_TYPE_DICT_ENTRY, "say")) > 0) { + const char *key; + const void *value; + size_t size; + DnsTxtItem *i; + + r = sd_bus_message_read(message, "s", &key); + if (r < 0) + return r; + + if (isempty(key)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "Keys in DNS-SD TXT RRs can't be empty"); + + if (!ascii_is_valid(key)) + return sd_bus_error_setf(error, SD_BUS_ERROR_INVALID_ARGS, "TXT key '%s' contains non-ASCII symbols", key); + + r = sd_bus_message_read_array(message, 'y', &value, &size); + if (r < 0) + return r; + + r = dnssd_txt_item_new_from_data(key, value, size, &i); + if (r < 0) + return r; + + LIST_INSERT_AFTER(items, txt_data->txt, last, i); + last = i; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + } + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (txt_data->txt) { + LIST_PREPEND(items, service->txt_data_items, txt_data); + txt_data = NULL; + } + } + if (r < 0) + return r; + + r = sd_bus_message_exit_container(message); + if (r < 0) + return r; + + if (!service->txt_data_items) { + _cleanup_(dnssd_txtdata_freep) DnssdTxtData *txt_data = NULL; + + txt_data = new0(DnssdTxtData, 1); + if (!txt_data) + return log_oom(); + + r = dns_txt_item_new_empty(&txt_data->txt); + if (r < 0) + return r; + + LIST_PREPEND(items, service->txt_data_items, txt_data); + txt_data = NULL; + } + + r = sd_bus_path_encode("/org/freedesktop/resolve1/dnssd", service->name, &path); + if (r < 0) + return r; + + r = hashmap_ensure_allocated(&m->dnssd_services, &string_hash_ops); + if (r < 0) + return r; + + r = hashmap_put(m->dnssd_services, service->name, service); + if (r < 0) + return r; + + r = sd_bus_track_new(sd_bus_message_get_bus(message), &bus_track, on_bus_track, service); + if (r < 0) + return r; + + r = sd_bus_track_add_sender(bus_track, message); + if (r < 0) + return r; + + service->manager = m; + + service = NULL; + + manager_refresh_rrs(m); + + return sd_bus_reply_method_return(message, "o", path); +} + +static int call_dnssd_method(Manager *m, sd_bus_message *message, sd_bus_message_handler_t handler, sd_bus_error *error) { + _cleanup_free_ char *name = NULL; + DnssdService *s = NULL; + const char *path; + int r; + + assert(m); + assert(message); + assert(handler); + + r = sd_bus_message_read(message, "o", &path); + if (r < 0) + return r; + + r = sd_bus_path_decode(path, "/org/freedesktop/resolve1/dnssd", &name); + if (r == 0) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_DNSSD_SERVICE, "DNS-SD service with object path '%s' does not exist", path); + if (r < 0) + return r; + + s = hashmap_get(m->dnssd_services, name); + if (!s) + return sd_bus_error_setf(error, BUS_ERROR_NO_SUCH_DNSSD_SERVICE, "DNS-SD service '%s' not known", name); + + return handler(message, s, error); +} + +static int bus_method_unregister_service(sd_bus_message *message, void *userdata, sd_bus_error *error) { + Manager *m = userdata; + + assert(message); + assert(m); + + return call_dnssd_method(m, message, bus_dnssd_method_unregister, error); +} + static const sd_bus_vtable resolve_vtable[] = { SD_BUS_VTABLE_START(0), SD_BUS_PROPERTY("LLMNRHostname", "s", NULL, offsetof(Manager, llmnr_hostname), 0), @@ -1609,6 +1839,8 @@ static const sd_bus_vtable resolve_vtable[] = { SD_BUS_METHOD("SetLinkDNSSECNegativeTrustAnchors", "ias", NULL, bus_method_set_link_dnssec_negative_trust_anchors, 0), SD_BUS_METHOD("RevertLink", "i", NULL, bus_method_revert_link, 0), + SD_BUS_METHOD("RegisterService", "sssqqqaa{say}", "o", bus_method_register_service, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_METHOD("UnregisterService", "o", NULL, bus_method_unregister_service, SD_BUS_VTABLE_UNPRIVILEGED), SD_BUS_VTABLE_END, }; @@ -1681,6 +1913,14 @@ int manager_connect_bus(Manager *m) { if (r < 0) return log_error_errno(r, "Failed to register link enumerator: %m"); + r = sd_bus_add_fallback_vtable(m->bus, NULL, "/org/freedesktop/resolve1/dnssd", "org.freedesktop.resolve1.DnssdService", dnssd_vtable, dnssd_object_find, m); + if (r < 0) + return log_error_errno(r, "Failed to register dnssd objects: %m"); + + r = sd_bus_add_node_enumerator(m->bus, NULL, "/org/freedesktop/resolve1/dnssd", dnssd_node_enumerator, m); + if (r < 0) + return log_error_errno(r, "Failed to register dnssd enumerator: %m"); + r = sd_bus_request_name(m->bus, "org.freedesktop.resolve1", 0); if (r < 0) return log_error_errno(r, "Failed to register name: %m"); diff --git a/src/resolve/resolved-conf.c b/src/resolve/resolved-conf.c index 00135b107c..ca69d70e3c 100644 --- a/src/resolve/resolved-conf.c +++ b/src/resolve/resolved-conf.c @@ -22,10 +22,14 @@ #include "conf-parser.h" #include "def.h" #include "extract-word.h" +#include "hexdecoct.h" #include "parse-util.h" #include "resolved-conf.h" +#include "resolved-dnssd.h" +#include "specifier.h" #include "string-table.h" #include "string-util.h" +#include "utf8.h" DEFINE_CONFIG_PARSE_ENUM(config_parse_dns_stub_listener_mode, dns_stub_listener_mode, DnsStubListenerMode, "Failed to parse DNS stub listener mode setting"); @@ -228,6 +232,160 @@ int config_parse_search_domains( return 0; } +int config_parse_dnssd_service_name(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata) { + static const Specifier specifier_table[] = { + { 'b', specifier_boot_id, NULL }, + { 'H', specifier_host_name, NULL }, + { 'm', specifier_machine_id, NULL }, + { 'v', specifier_kernel_release, NULL }, + {} + }; + DnssdService *s = userdata; + _cleanup_free_ char *name = NULL; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(s); + + if (isempty(rvalue)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Service instance name can't be empty. Ignoring."); + return -EINVAL; + } + + r = free_and_strdup(&s->name_template, rvalue); + if (r < 0) + return log_oom(); + + r = specifier_printf(s->name_template, specifier_table, NULL, &name); + if (r < 0) + return log_debug_errno(r, "Failed to replace specifiers: %m"); + + if (!dns_service_name_is_valid(name)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Service instance name template renders to invalid name '%s'. Ignoring.", name); + return -EINVAL; + } + + return 0; +} + +int config_parse_dnssd_service_type(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata) { + DnssdService *s = userdata; + int r; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(s); + + if (isempty(rvalue)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Service type can't be empty. Ignoring."); + return -EINVAL; + } + + if (!dnssd_srv_type_is_valid(rvalue)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Service type is invalid. Ignoring."); + return -EINVAL; + } + + r = free_and_strdup(&s->type, rvalue); + if (r < 0) + return log_oom(); + + return 0; +} + +int config_parse_dnssd_txt(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata) { + _cleanup_(dnssd_txtdata_freep) DnssdTxtData *txt_data = NULL; + DnssdService *s = userdata; + DnsTxtItem *last = NULL; + + assert(filename); + assert(lvalue); + assert(rvalue); + assert(s); + + if (isempty(rvalue)) { + /* Flush out collected items */ + s->txt_data_items = dnssd_txtdata_free_all(s->txt_data_items); + return 0; + } + + txt_data = new0(DnssdTxtData, 1); + if (!txt_data) + return log_oom(); + + for (;;) { + _cleanup_free_ char *word = NULL; + _cleanup_free_ char *key = NULL; + _cleanup_free_ char *value = NULL; + _cleanup_free_ void *decoded = NULL; + size_t length = 0; + DnsTxtItem *i; + int r; + + r = extract_first_word(&rvalue, &word, NULL, + EXTRACT_QUOTES|EXTRACT_CUNESCAPE|EXTRACT_CUNESCAPE_RELAX); + if (r == 0) + break; + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_syntax(unit, LOG_ERR, filename, line, r, "Invalid syntax, ignoring: %s", rvalue); + + r = split_pair(word, "=", &key, &value); + if (r == -ENOMEM) + return log_oom(); + if (r == -EINVAL) { + key = word; + word = NULL; + } + + if (!ascii_is_valid(key)) { + log_syntax(unit, LOG_ERR, filename, line, 0, "Invalid syntax, ignoring: %s", key); + return -EINVAL; + } + + switch (ltype) { + + case DNS_TXT_ITEM_DATA: + if (value) { + r = unbase64mem(value, strlen(value), &decoded, &length); + if (r == -ENOMEM) + return log_oom(); + if (r < 0) + return log_syntax(unit, LOG_ERR, filename, line, r, + "Invalid base64 encoding, ignoring: %s", value); + } + + r = dnssd_txt_item_new_from_data(key, decoded, length, &i); + if (r < 0) + return log_oom(); + break; + + case DNS_TXT_ITEM_TEXT: + r = dnssd_txt_item_new_from_string(key, value, &i); + if (r < 0) + return log_oom(); + break; + + default: + assert_not_reached("Unknown type of Txt config"); + } + + LIST_INSERT_AFTER(items, txt_data->txt, last, i); + last = i; + } + + if (!LIST_IS_EMPTY(txt_data->txt)) { + LIST_PREPEND(items, s->txt_data_items, txt_data); + txt_data = NULL; + } + + return 0; +} + int manager_parse_config_file(Manager *m) { int r; diff --git a/src/resolve/resolved-conf.h b/src/resolve/resolved-conf.h index 9b0ffb6483..4ebb45f3aa 100644 --- a/src/resolve/resolved-conf.h +++ b/src/resolve/resolved-conf.h @@ -44,9 +44,14 @@ int manager_parse_dns_server_string_and_warn(Manager *m, DnsServerType type, con const struct ConfigPerfItem* resolved_gperf_lookup(const char *key, GPERF_LEN_TYPE length); +const struct ConfigPerfItem* resolved_dnssd_gperf_lookup(const char *key, GPERF_LEN_TYPE length); + int config_parse_dns_servers(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); int config_parse_search_domains(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); int config_parse_dns_stub_listener_mode(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); +int config_parse_dnssd_service_name(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); +int config_parse_dnssd_service_type(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); +int config_parse_dnssd_txt(const char *unit, const char *filename, unsigned line, const char *section, unsigned section_line, const char *lvalue, int ltype, const char *rvalue, void *data, void *userdata); const char* dns_stub_listener_mode_to_string(DnsStubListenerMode p) _const_; DnsStubListenerMode dns_stub_listener_mode_from_string(const char *s) _pure_; diff --git a/src/resolve/resolved-dns-packet.c b/src/resolve/resolved-dns-packet.c index 40f35475fd..d7a839a823 100644 --- a/src/resolve/resolved-dns-packet.c +++ b/src/resolve/resolved-dns-packet.c @@ -1698,16 +1698,9 @@ int dns_packet_read_rr(DnsPacket *p, DnsResourceRecord **ret, bool *ret_cache_fl case DNS_TYPE_SPF: /* exactly the same as TXT */ case DNS_TYPE_TXT: if (rdlength <= 0) { - DnsTxtItem *i; - /* RFC 6763, section 6.1 suggests to treat - * empty TXT RRs as equivalent to a TXT record - * with a single empty string. */ - - i = malloc0(offsetof(DnsTxtItem, data) + 1); /* for safety reasons we add an extra NUL byte */ - if (!i) - return -ENOMEM; - - rr->txt.items = i; + r = dns_txt_item_new_empty(&rr->txt.items); + if (r < 0) + return r; } else { DnsTxtItem *last = NULL; diff --git a/src/resolve/resolved-dns-rr.c b/src/resolve/resolved-dns-rr.c index 898d5b3c36..4a0c1d6a35 100644 --- a/src/resolve/resolved-dns-rr.c +++ b/src/resolve/resolved-dns-rr.c @@ -182,6 +182,19 @@ bool dns_resource_key_is_address(const DnsResourceKey *key) { return key->class == DNS_CLASS_IN && IN_SET(key->type, DNS_TYPE_A, DNS_TYPE_AAAA); } +bool dns_resource_key_is_dnssd_ptr(const DnsResourceKey *key) { + assert(key); + + /* Check if this is a PTR resource key used in + Service Instance Enumeration as described in RFC6763 p4.1. */ + + if (key->type != DNS_TYPE_PTR) + return false; + + return dns_name_endswith(dns_resource_key_name(key), "_tcp.local") || + dns_name_endswith(dns_resource_key_name(key), "_udp.local"); +} + int dns_resource_key_equal(const DnsResourceKey *a, const DnsResourceKey *b) { int r; @@ -1806,6 +1819,22 @@ DnsTxtItem *dns_txt_item_copy(DnsTxtItem *first) { return copy; } +int dns_txt_item_new_empty(DnsTxtItem **ret) { + DnsTxtItem *i; + + /* RFC 6763, section 6.1 suggests to treat + * empty TXT RRs as equivalent to a TXT record + * with a single empty string. */ + + i = malloc0(offsetof(DnsTxtItem, data) + 1); /* for safety reasons we add an extra NUL byte */ + if (!i) + return -ENOMEM; + + *ret = i; + + return 0; +} + static const char* const dnssec_algorithm_table[_DNSSEC_ALGORITHM_MAX_DEFINED] = { /* Mnemonics as listed on https://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml */ [DNSSEC_ALGORITHM_RSAMD5] = "RSAMD5", diff --git a/src/resolve/resolved-dns-rr.h b/src/resolve/resolved-dns-rr.h index 9faf4debe7..3f3d46e6b3 100644 --- a/src/resolve/resolved-dns-rr.h +++ b/src/resolve/resolved-dns-rr.h @@ -298,6 +298,7 @@ DnsResourceKey* dns_resource_key_ref(DnsResourceKey *key); DnsResourceKey* dns_resource_key_unref(DnsResourceKey *key); const char* dns_resource_key_name(const DnsResourceKey *key); bool dns_resource_key_is_address(const DnsResourceKey *key); +bool dns_resource_key_is_dnssd_ptr(const DnsResourceKey *key); int dns_resource_key_equal(const DnsResourceKey *a, const DnsResourceKey *b); int dns_resource_key_match_rr(const DnsResourceKey *key, DnsResourceRecord *rr, const char *search_domain); int dns_resource_key_match_cname_or_dname(const DnsResourceKey *key, const DnsResourceKey *cname, const char *search_domain); @@ -341,6 +342,7 @@ int dns_resource_record_clamp_ttl(DnsResourceRecord **rr, uint32_t max_ttl); DnsTxtItem *dns_txt_item_free_all(DnsTxtItem *i); bool dns_txt_item_equal(DnsTxtItem *a, DnsTxtItem *b); DnsTxtItem *dns_txt_item_copy(DnsTxtItem *i); +int dns_txt_item_new_empty(DnsTxtItem **ret); void dns_resource_record_hash_func(const void *i, struct siphash *state); diff --git a/src/resolve/resolved-dns-scope.c b/src/resolve/resolved-dns-scope.c index a9071ee73e..0a644f4252 100644 --- a/src/resolve/resolved-dns-scope.c +++ b/src/resolve/resolved-dns-scope.c @@ -27,7 +27,9 @@ #include "hostname-util.h" #include "missing.h" #include "random-util.h" +#include "resolved-dnssd.h" #include "resolved-dns-scope.h" +#include "resolved-dns-zone.h" #include "resolved-llmnr.h" #include "resolved-mdns.h" #include "socket-util.h" @@ -1064,7 +1066,12 @@ static int on_announcement_timeout(sd_event_source *s, usec_t usec, void *userda int dns_scope_announce(DnsScope *scope, bool goodbye) { _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; - LinkAddress *a; + _cleanup_set_free_ Set *types = NULL; + DnsTransaction *t; + DnsZoneItem *z, *i; + unsigned size = 0; + Iterator iterator; + char *service_type; int r; if (!scope) @@ -1073,18 +1080,85 @@ int dns_scope_announce(DnsScope *scope, bool goodbye) { if (scope->protocol != DNS_PROTOCOL_MDNS) return 0; - answer = dns_answer_new(scope->link->n_addresses * 2); + /* Check if we're done with probing. */ + LIST_FOREACH(transactions_by_scope, t, scope->transactions) + if (DNS_TRANSACTION_IS_LIVE(t->state)) + return 0; + + /* Check if there're services pending conflict resolution. */ + if (manager_next_dnssd_names(scope->manager)) + return 0; /* we reach this point only if changing hostname didn't help */ + + /* Calculate answer's size. */ + HASHMAP_FOREACH(z, scope->zone.by_key, iterator) { + if (z->state != DNS_ZONE_ITEM_ESTABLISHED) + continue; + + if (z->rr->key->type == DNS_TYPE_PTR && + !dns_zone_contains_name(&scope->zone, z->rr->ptr.name)) { + char key_str[DNS_RESOURCE_KEY_STRING_MAX]; + + log_debug("Skip PTR RR <%s> since its counterparts seem to be withdrawn", dns_resource_key_to_string(z->rr->key, key_str, sizeof key_str)); + z->state = DNS_ZONE_ITEM_WITHDRAWN; + continue; + } + + /* Collect service types for _services._dns-sd._udp.local RRs in a set */ + if (!scope->announced && + dns_resource_key_is_dnssd_ptr(z->rr->key)) { + if (!set_contains(types, dns_resource_key_name(z->rr->key))) { + r = set_ensure_allocated(&types, &dns_name_hash_ops); + if (r < 0) + return log_debug_errno(r, "Failed to allocate set: %m"); + + r = set_put(types, dns_resource_key_name(z->rr->key)); + if (r < 0) + return log_debug_errno(r, "Failed to add item to set: %m"); + } + } + + LIST_FOREACH(by_key, i, z) + size++; + } + + answer = dns_answer_new(size + set_size(types)); if (!answer) return log_oom(); - LIST_FOREACH(addresses, a, scope->link->addresses) { - r = dns_answer_add(answer, a->mdns_address_rr, 0, goodbye ? DNS_ANSWER_GOODBYE : DNS_ANSWER_CACHE_FLUSH); - if (r < 0) - return log_debug_errno(r, "Failed to add address RR to answer: %m"); + /* Second iteration, actually add RRs to the answer. */ + HASHMAP_FOREACH(z, scope->zone.by_key, iterator) + LIST_FOREACH (by_key, i, z) { + DnsAnswerFlags flags; - r = dns_answer_add(answer, a->mdns_ptr_rr, 0, goodbye ? DNS_ANSWER_GOODBYE : DNS_ANSWER_CACHE_FLUSH); + if (i->state != DNS_ZONE_ITEM_ESTABLISHED) + continue; + + if (dns_resource_key_is_dnssd_ptr(i->rr->key)) + flags = goodbye ? DNS_ANSWER_GOODBYE : 0; + else + flags = goodbye ? (DNS_ANSWER_GOODBYE|DNS_ANSWER_CACHE_FLUSH) : DNS_ANSWER_CACHE_FLUSH; + + r = dns_answer_add(answer, i->rr, 0 , flags); + if (r < 0) + return log_debug_errno(r, "Failed to add RR to announce: %m"); + } + + /* Since all the active services are in the zone make them discoverable now. */ + SET_FOREACH(service_type, types, iterator) { + _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr; + + rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_PTR, + "_services._dns-sd._udp.local"); + rr->ptr.name = strdup(service_type); + rr->ttl = MDNS_DEFAULT_TTL; + + r = dns_zone_put(&scope->zone, scope, rr, false); if (r < 0) - return log_debug_errno(r, "Failed to add PTR RR to answer: %m"); + log_warning_errno(r, "Failed to add DNS-SD PTR record to MDNS zone: %m"); + + r = dns_answer_add(answer, rr, 0 , 0); + if (r < 0) + return log_debug_errno(r, "Failed to add RR to announce: %m"); } if (dns_answer_isempty(answer)) @@ -1123,3 +1197,65 @@ int dns_scope_announce(DnsScope *scope, bool goodbye) { return 0; } + +int dns_scope_add_dnssd_services(DnsScope *scope) { + Iterator i; + DnssdService *service; + DnssdTxtData *txt_data; + int r; + + assert(scope); + + if (hashmap_size(scope->manager->dnssd_services) == 0) + return 0; + + scope->announced = false; + + HASHMAP_FOREACH(service, scope->manager->dnssd_services, i) { + service->withdrawn = false; + + r = dns_zone_put(&scope->zone, scope, service->ptr_rr, false); + if (r < 0) + log_warning_errno(r, "Failed to add PTR record to MDNS zone: %m"); + + r = dns_zone_put(&scope->zone, scope, service->srv_rr, true); + if (r < 0) + log_warning_errno(r, "Failed to add SRV record to MDNS zone: %m"); + + LIST_FOREACH(items, txt_data, service->txt_data_items) { + r = dns_zone_put(&scope->zone, scope, txt_data->rr, true); + if (r < 0) + log_warning_errno(r, "Failed to add TXT record to MDNS zone: %m"); + } + } + + return 0; +} + +int dns_scope_remove_dnssd_services(DnsScope *scope) { + _cleanup_(dns_resource_key_unrefp) DnsResourceKey *key = NULL; + Iterator i; + DnssdService *service; + DnssdTxtData *txt_data; + int r; + + assert(scope); + + key = dns_resource_key_new(DNS_CLASS_IN, DNS_TYPE_PTR, + "_services._dns-sd._udp.local"); + if (!key) + return log_oom(); + + r = dns_zone_remove_rrs_by_key(&scope->zone, key); + if (r < 0) + return r; + + HASHMAP_FOREACH(service, scope->manager->dnssd_services, i) { + dns_zone_remove_rr(&scope->zone, service->ptr_rr); + dns_zone_remove_rr(&scope->zone, service->srv_rr); + LIST_FOREACH(items, txt_data, service->txt_data_items) + dns_zone_remove_rr(&scope->zone, txt_data->rr); + } + + return 0; +} diff --git a/src/resolve/resolved-dns-scope.h b/src/resolve/resolved-dns-scope.h index d3da8e54ea..d46ccd884c 100644 --- a/src/resolve/resolved-dns-scope.h +++ b/src/resolve/resolved-dns-scope.h @@ -119,3 +119,7 @@ bool dns_scope_network_good(DnsScope *s); int dns_scope_ifindex(DnsScope *s); int dns_scope_announce(DnsScope *scope, bool goodbye); + +int dns_scope_add_dnssd_services(DnsScope *scope); + +int dns_scope_remove_dnssd_services(DnsScope *scope); diff --git a/src/resolve/resolved-dns-transaction.c b/src/resolve/resolved-dns-transaction.c index a66d605405..792a16d693 100644 --- a/src/resolve/resolved-dns-transaction.c +++ b/src/resolve/resolved-dns-transaction.c @@ -365,7 +365,7 @@ void dns_transaction_complete(DnsTransaction *t, DnsTransactionState state) { SET_FOREACH_MOVE(z, t->notify_zone_items_done, t->notify_zone_items) dns_zone_item_notify(z); SWAP_TWO(t->notify_zone_items, t->notify_zone_items_done); - if (t->probing) + if (t->probing && t->state == DNS_TRANSACTION_ATTEMPTS_MAX_REACHED) (void) dns_scope_announce(t->scope, false); SET_FOREACH_MOVE(d, t->notify_transactions_done, t->notify_transactions) @@ -1377,7 +1377,11 @@ static int dns_transaction_make_packet_mdns(DnsTransaction *t) { _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL; bool add_known_answers = false; DnsTransaction *other; + Iterator i; + DnsResourceKey *tkey; + _cleanup_set_free_ Set *keys = NULL; unsigned qdcount; + unsigned nscount = 0; usec_t ts; int r; @@ -1400,6 +1404,16 @@ static int dns_transaction_make_packet_mdns(DnsTransaction *t) { if (dns_key_is_shared(t->key)) add_known_answers = true; + if (t->key->type == DNS_TYPE_ANY) { + r = set_ensure_allocated(&keys, &dns_resource_key_hash_ops); + if (r < 0) + return r; + + r = set_put(keys, t->key); + if (r < 0) + return r; + } + /* * For mDNS, we want to coalesce as many open queries in pending transactions into one single * query packet on the wire as possible. To achieve that, we iterate through all pending transactions @@ -1459,6 +1473,16 @@ static int dns_transaction_make_packet_mdns(DnsTransaction *t) { if (dns_key_is_shared(other->key)) add_known_answers = true; + + if (other->key->type == DNS_TYPE_ANY) { + r = set_ensure_allocated(&keys, &dns_resource_key_hash_ops); + if (r < 0) + return r; + + r = set_put(keys, other->key); + if (r < 0) + return r; + } } DNS_PACKET_HEADER(p)->qdcount = htobe16(qdcount); @@ -1470,6 +1494,22 @@ static int dns_transaction_make_packet_mdns(DnsTransaction *t) { return r; } + SET_FOREACH(tkey, keys, i) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL; + bool tentative; + + r = dns_zone_lookup(&t->scope->zone, tkey, t->scope->link->ifindex, &answer, NULL, &tentative); + if (r < 0) + return r; + + r = dns_packet_append_answer(p, answer); + if (r < 0) + return r; + + nscount += dns_answer_size(answer); + } + DNS_PACKET_HEADER(p)->nscount = htobe16(nscount); + t->sent = p; p = NULL; diff --git a/src/resolve/resolved-dns-zone.c b/src/resolve/resolved-dns-zone.c index 2c32bb4d4f..dcb9702e57 100644 --- a/src/resolve/resolved-dns-zone.c +++ b/src/resolve/resolved-dns-zone.c @@ -23,6 +23,7 @@ #include "list.h" #include "resolved-dns-packet.h" #include "resolved-dns-zone.h" +#include "resolved-dnssd.h" #include "string-util.h" /* Never allow more than 1K entries */ @@ -95,7 +96,7 @@ void dns_zone_flush(DnsZone *z) { z->by_name = hashmap_free(z->by_name); } -static DnsZoneItem* dns_zone_get(DnsZone *z, DnsResourceRecord *rr) { +DnsZoneItem* dns_zone_get(DnsZone *z, DnsResourceRecord *rr) { DnsZoneItem *i; assert(z); @@ -119,6 +120,22 @@ void dns_zone_remove_rr(DnsZone *z, DnsResourceRecord *rr) { dns_zone_item_remove_and_free(z, i); } +int dns_zone_remove_rrs_by_key(DnsZone *z, DnsResourceKey *key) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL; + DnsResourceRecord *rr; + bool tentative; + int r; + + r = dns_zone_lookup(z, key, 0, &answer, &soa, &tentative); + if (r < 0) + return r; + + DNS_ANSWER_FOREACH(rr, answer) + dns_zone_remove_rr(z, rr); + + return 0; +} + static int dns_zone_init(DnsZone *z) { int r; @@ -289,6 +306,24 @@ int dns_zone_put(DnsZone *z, DnsScope *s, DnsResourceRecord *rr, bool probe) { return 0; } +static int dns_zone_add_authenticated_answer(DnsAnswer *a, DnsZoneItem *i, int ifindex) { + DnsAnswerFlags flags; + + /* From RFC 6762, Section 10.2 + * "They (the rules about when to set the cache-flush bit) apply to + * startup announcements as described in Section 8.3, "Announcing", + * and to responses generated as a result of receiving query messages." + * So, set the cache-flush bit for mDNS answers except for DNS-SD + * service enumeration PTRs described in RFC 6763, Section 4.1. */ + if (i->scope->protocol == DNS_PROTOCOL_MDNS && + !dns_resource_key_is_dnssd_ptr(i->rr->key)) + flags = DNS_ANSWER_AUTHENTICATED|DNS_ANSWER_CACHE_FLUSH; + else + flags = DNS_ANSWER_AUTHENTICATED; + + return dns_answer_add(a, i->rr, ifindex, flags); +} + int dns_zone_lookup(DnsZone *z, DnsResourceKey *key, int ifindex, DnsAnswer **ret_answer, DnsAnswer **ret_soa, bool *ret_tentative) { _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL; unsigned n_answer = 0; @@ -394,7 +429,7 @@ int dns_zone_lookup(DnsZone *z, DnsResourceKey *key, int ifindex, DnsAnswer **re if (k < 0) return k; if (k > 0) { - r = dns_answer_add(answer, j->rr, ifindex, DNS_ANSWER_AUTHENTICATED); + r = dns_zone_add_authenticated_answer(answer, j, ifindex); if (r < 0) return r; @@ -420,7 +455,7 @@ int dns_zone_lookup(DnsZone *z, DnsResourceKey *key, int ifindex, DnsAnswer **re if (j->state != DNS_ZONE_ITEM_PROBING) tentative = false; - r = dns_answer_add(answer, j->rr, ifindex, DNS_ANSWER_AUTHENTICATED); + r = dns_zone_add_authenticated_answer(answer, j, ifindex); if (r < 0) return r; } @@ -491,6 +526,8 @@ void dns_zone_item_conflict(DnsZoneItem *i) { /* Withdraw the conflict item */ i->state = DNS_ZONE_ITEM_WITHDRAWN; + dnssd_signal_conflict(i->scope->manager, dns_resource_key_name(i->rr->key)); + /* Maybe change the hostname */ if (manager_is_own_hostname(i->scope->manager, dns_resource_key_name(i->rr->key)) > 0) manager_next_hostname(i->scope->manager); @@ -510,7 +547,9 @@ void dns_zone_item_notify(DnsZoneItem *i) { bool we_lost = false; /* The probe got a successful reply. If we so far - * weren't established we just give up. If we already + * weren't established we just give up. + * + * In LLMNR case if we already * were established, and the peer has the * lexicographically larger IP address we continue * and defend it. */ @@ -518,7 +557,7 @@ void dns_zone_item_notify(DnsZoneItem *i) { if (!IN_SET(i->state, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) { log_debug("Got a successful probe for not yet established RR, we lost."); we_lost = true; - } else { + } else if (i->probe_transaction->scope->protocol == DNS_PROTOCOL_LLMNR) { assert(i->probe_transaction->received); we_lost = memcmp(&i->probe_transaction->received->sender, &i->probe_transaction->received->destination, FAMILY_ADDRESS_SIZE(i->probe_transaction->received->family)) < 0; if (we_lost) @@ -580,6 +619,10 @@ int dns_zone_check_conflicts(DnsZone *zone, DnsResourceRecord *rr) { if (dns_zone_get(zone, rr)) return 0; + /* No conflict if it is DNS-SD RR used for service enumeration. */ + if (dns_resource_key_is_dnssd_ptr(rr->key)) + return 0; + /* OK, somebody else has RRs for the same name. Yuck! Let's * start probing again */ @@ -664,3 +707,20 @@ bool dns_zone_is_empty(DnsZone *zone) { return hashmap_isempty(zone->by_key); } + +bool dns_zone_contains_name(DnsZone *z, const char *name) { + DnsZoneItem *i, *first; + + first = hashmap_get(z->by_name, name); + if (!first) + return false; + + LIST_FOREACH(by_name, i, first) { + if (!IN_SET(i->state, DNS_ZONE_ITEM_PROBING, DNS_ZONE_ITEM_ESTABLISHED, DNS_ZONE_ITEM_VERIFYING)) + continue; + + return true; + } + + return false; +} diff --git a/src/resolve/resolved-dns-zone.h b/src/resolve/resolved-dns-zone.h index f654537b45..c9c7440351 100644 --- a/src/resolve/resolved-dns-zone.h +++ b/src/resolve/resolved-dns-zone.h @@ -67,7 +67,9 @@ struct DnsZoneItem { void dns_zone_flush(DnsZone *z); int dns_zone_put(DnsZone *z, DnsScope *s, DnsResourceRecord *rr, bool probe); +DnsZoneItem* dns_zone_get(DnsZone *z, DnsResourceRecord *rr); void dns_zone_remove_rr(DnsZone *z, DnsResourceRecord *rr); +int dns_zone_remove_rrs_by_key(DnsZone *z, DnsResourceKey *key); int dns_zone_lookup(DnsZone *z, DnsResourceKey *key, int ifindex, DnsAnswer **answer, DnsAnswer **soa, bool *tentative); @@ -83,3 +85,4 @@ void dns_zone_item_probe_stop(DnsZoneItem *i); void dns_zone_dump(DnsZone *zone, FILE *f); bool dns_zone_is_empty(DnsZone *zone); +bool dns_zone_contains_name(DnsZone *z, const char *name); diff --git a/src/resolve/resolved-dnssd-bus.c b/src/resolve/resolved-dnssd-bus.c new file mode 100644 index 0000000000..c914e8f8d2 --- /dev/null +++ b/src/resolve/resolved-dnssd-bus.c @@ -0,0 +1,146 @@ +/*** + This file is part of systemd. + + Copyright 2017 Dmitry Rozhkov + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include "alloc-util.h" +#include "bus-util.h" +#include "resolved-dnssd.h" +#include "resolved-dnssd-bus.h" +#include "resolved-link.h" +#include "strv.h" +#include "user-util.h" + +int bus_dnssd_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error) { + DnssdService *s = userdata; + DnssdTxtData *txt_data; + Manager *m; + Iterator i; + Link *l; + int r; + + assert(message); + assert(s); + + m = s->manager; + + r = bus_verify_polkit_async(message, CAP_SYS_ADMIN, + "org.freedesktop.resolve1.unregister-service", + NULL, false, s->originator, + &m->polkit_registry, error); + if (r < 0) + return r; + if (r == 0) + return 1; /* Polkit will call us back */ + + HASHMAP_FOREACH(l, m->links, i) { + if (l->mdns_ipv4_scope) { + r = dns_scope_announce(l->mdns_ipv4_scope, true); + if (r < 0) + log_warning_errno(r, "Failed to send goodbye messages in IPv4 scope: %m"); + + dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, s->ptr_rr); + dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, s->srv_rr); + LIST_FOREACH(items, txt_data, s->txt_data_items) + dns_zone_remove_rr(&l->mdns_ipv4_scope->zone, txt_data->rr); + } + + if (l->mdns_ipv6_scope) { + r = dns_scope_announce(l->mdns_ipv6_scope, true); + if (r < 0) + log_warning_errno(r, "Failed to send goodbye messages in IPv6 scope: %m"); + + dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, s->ptr_rr); + dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, s->srv_rr); + LIST_FOREACH(items, txt_data, s->txt_data_items) + dns_zone_remove_rr(&l->mdns_ipv6_scope->zone, txt_data->rr); + } + } + + dnssd_service_free(s); + + manager_refresh_rrs(m); + + return sd_bus_reply_method_return(message, NULL); +} + +const sd_bus_vtable dnssd_vtable[] = { + SD_BUS_VTABLE_START(0), + + SD_BUS_METHOD("Unregister", NULL, NULL, bus_dnssd_method_unregister, SD_BUS_VTABLE_UNPRIVILEGED), + SD_BUS_SIGNAL("Conflicted", NULL, 0), + + SD_BUS_VTABLE_END +}; + +int dnssd_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error) { + _cleanup_free_ char *name = NULL; + Manager *m = userdata; + DnssdService *service; + int r; + + assert(bus); + assert(path); + assert(interface); + assert(found); + assert(m); + + r = sd_bus_path_decode(path, "/org/freedesktop/resolve1/dnssd", &name); + if (r <= 0) + return 0; + + service = hashmap_get(m->dnssd_services, name); + if (!service) + return 0; + + *found = service; + return 1; +} + +int dnssd_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error) { + _cleanup_strv_free_ char **l = NULL; + Manager *m = userdata; + DnssdService *service; + Iterator i; + unsigned c = 0; + int r; + + assert(bus); + assert(path); + assert(m); + assert(nodes); + + l = new0(char*, hashmap_size(m->dnssd_services) + 1); + if (!l) + return -ENOMEM; + + HASHMAP_FOREACH(service, m->dnssd_services, i) { + char *p; + + r = sd_bus_path_encode("/org/freedesktop/resolve1/dnssd", service->name, &p); + if (r < 0) + return r; + + l[c++] = p; + } + + l[c] = NULL; + *nodes = l; + l = NULL; + + return 1; +} diff --git a/src/resolve/resolved-dnssd-bus.h b/src/resolve/resolved-dnssd-bus.h new file mode 100644 index 0000000000..ab915c86f9 --- /dev/null +++ b/src/resolve/resolved-dnssd-bus.h @@ -0,0 +1,29 @@ +#pragma once + +/*** + This file is part of systemd. + + Copyright 2017 Dmitry Rozhkov + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include "sd-bus.h" + +extern const sd_bus_vtable dnssd_vtable[]; + +int dnssd_object_find(sd_bus *bus, const char *path, const char *interface, void *userdata, void **found, sd_bus_error *error); +int dnssd_node_enumerator(sd_bus *bus, const char *path, void *userdata, char ***nodes, sd_bus_error *error); + +int bus_dnssd_method_unregister(sd_bus_message *message, void *userdata, sd_bus_error *error); diff --git a/src/resolve/resolved-dnssd-gperf.gperf b/src/resolve/resolved-dnssd-gperf.gperf new file mode 100644 index 0000000000..2780b856bf --- /dev/null +++ b/src/resolve/resolved-dnssd-gperf.gperf @@ -0,0 +1,24 @@ +%{ +#include +#include "conf-parser.h" +#include "resolved-conf.h" +#include "resolved-dnssd.h" +%} +struct ConfigPerfItem; +%null_strings +%language=ANSI-C +%define slot-name section_and_lvalue +%define hash-function-name resolved_dnssd_gperf_hash +%define lookup-function-name resolved_dnssd_gperf_lookup +%readonly-tables +%omit-struct-type +%struct-type +%includes +%% +Service.Name, config_parse_dnssd_service_name, 0, 0 +Service.Type, config_parse_dnssd_service_type, 0, 0 +Service.Port, config_parse_ip_port, 0, offsetof(DnssdService, port) +Service.Priority, config_parse_uint16, 0, offsetof(DnssdService, priority) +Service.Weight, config_parse_uint16, 0, offsetof(DnssdService, weight) +Service.TxtText, config_parse_dnssd_txt, DNS_TXT_ITEM_TEXT, 0 +Service.TxtData, config_parse_dnssd_txt, DNS_TXT_ITEM_DATA, 0 diff --git a/src/resolve/resolved-dnssd.c b/src/resolve/resolved-dnssd.c new file mode 100644 index 0000000000..db589f4436 --- /dev/null +++ b/src/resolve/resolved-dnssd.c @@ -0,0 +1,387 @@ +/*** + This file is part of systemd. + + Copyright 2017 Dmitry Rozhkov + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include "conf-files.h" +#include "conf-parser.h" +#include "resolved-dnssd.h" +#include "resolved-dns-rr.h" +#include "resolved-manager.h" +#include "specifier.h" +#include "strv.h" + +const char* const dnssd_service_dirs[] = { + "/etc/systemd/dnssd", + "/run/systemd/dnssd", + "/usr/lib/systemd/dnssd", +#ifdef HAVE_SPLIT_USR + "/lib/systemd/dnssd", +#endif + NULL +}; + +DnssdTxtData *dnssd_txtdata_free(DnssdTxtData *txt_data) { + if (!txt_data) + return NULL; + + dns_resource_record_unref(txt_data->rr); + dns_txt_item_free_all(txt_data->txt); + + return mfree(txt_data); +} + +DnssdTxtData *dnssd_txtdata_free_all(DnssdTxtData *txt_data) { + DnssdTxtData *next; + + if (!txt_data) + return NULL; + + next = txt_data->items_next; + + dnssd_txtdata_free(txt_data); + + return dnssd_txtdata_free_all(next); +} + +DnssdService *dnssd_service_free(DnssdService *service) { + if (!service) + return NULL; + + if (service->manager) + hashmap_remove(service->manager->dnssd_services, service->name); + + dns_resource_record_unref(service->ptr_rr); + dns_resource_record_unref(service->srv_rr); + + dnssd_txtdata_free_all(service->txt_data_items); + + free(service->filename); + free(service->name); + free(service->type); + free(service->name_template); + + return mfree(service); +} + +static int dnssd_service_load(Manager *manager, const char *filename) { + _cleanup_(dnssd_service_freep) DnssdService *service = NULL; + _cleanup_(dnssd_txtdata_freep) DnssdTxtData *txt_data = NULL; + char *d; + const char *dropin_dirname; + int r; + + assert(manager); + assert(filename); + + service = new0(DnssdService, 1); + if (!service) + return log_oom(); + + service->filename = strdup(filename); + if (!service->filename) + return log_oom(); + + service->name = strdup(basename(filename)); + if (!service->name) + return log_oom(); + + d = endswith(service->name, ".dnssd"); + if (!d) + return -EINVAL; + + assert(streq(d, ".dnssd")); + + *d = '\0'; + + dropin_dirname = strjoina(service->name, ".dnssd.d"); + + r = config_parse_many(filename, dnssd_service_dirs, dropin_dirname, + "Service\0", + config_item_perf_lookup, resolved_dnssd_gperf_lookup, + false, service); + if (r < 0) + return r; + + if (!service->name_template) { + log_error("%s doesn't define service instance name", service->name); + return -EINVAL; + } + + if (!service->type) { + log_error("%s doesn't define service type", service->name); + return -EINVAL; + } + + if (LIST_IS_EMPTY(service->txt_data_items)) { + txt_data = new0(DnssdTxtData, 1); + if (!txt_data) + return log_oom(); + + r = dns_txt_item_new_empty(&txt_data->txt); + if (r < 0) + return r; + + LIST_PREPEND(items, service->txt_data_items, txt_data); + txt_data = NULL; + } + + r = hashmap_ensure_allocated(&manager->dnssd_services, &string_hash_ops); + if (r < 0) + return r; + + r = hashmap_put(manager->dnssd_services, service->name, service); + if (r < 0) + return r; + + service->manager = manager; + + r = dnssd_update_rrs(service); + if (r < 0) + return r; + + service = NULL; + + return 0; +} + +static int specifier_dnssd_host_name(char specifier, void *data, void *userdata, char **ret) { + DnssdService *s = (DnssdService *) userdata; + char *n; + + assert(s); + assert(s->manager); + assert(s->manager->llmnr_hostname); + + n = strdup(s->manager->llmnr_hostname); + if (!n) + return -ENOMEM; + + *ret = n; + return 0; +} + +int dnssd_render_instance_name(DnssdService *s, char **ret_name) { + static const Specifier specifier_table[] = { + { 'b', specifier_boot_id, NULL }, + { 'H', specifier_dnssd_host_name, NULL }, + { 'm', specifier_machine_id, NULL }, + { 'v', specifier_kernel_release, NULL }, + {} + }; + _cleanup_free_ char *name = NULL; + int r; + + assert(s); + assert(s->name_template); + + r = specifier_printf(s->name_template, specifier_table, s, &name); + if (r < 0) + return log_debug_errno(r, "Failed to replace specifiers: %m"); + + if (!dns_service_name_is_valid(name)) { + log_debug("Service instance name '%s' is invalid.", name); + return -EINVAL; + } + + *ret_name = name; + name = NULL; + + return 0; +} + +int dnssd_load(Manager *manager) { + _cleanup_strv_free_ char **files = NULL; + char **f; + int r; + + assert(manager); + + if (manager->mdns_support != RESOLVE_SUPPORT_YES) + return 0; + + r = conf_files_list_strv(&files, ".dnssd", NULL, 0, dnssd_service_dirs); + if (r < 0) + return log_error_errno(r, "Failed to enumerate .dnssd files: %m"); + + STRV_FOREACH_BACKWARDS(f, files) { + r = dnssd_service_load(manager, *f); + if (r < 0) + log_warning_errno(r, "Failed to load '%s': %m", *f);; + } + + return 0; +} + +int dnssd_update_rrs(DnssdService *s) { + _cleanup_free_ char *n = NULL; + _cleanup_free_ char *service_name = NULL; + _cleanup_free_ char *full_name = NULL; + DnssdTxtData *txt_data; + int r; + + assert(s); + assert(s->txt_data_items); + assert(s->manager); + + s->ptr_rr = dns_resource_record_unref(s->ptr_rr); + s->srv_rr = dns_resource_record_unref(s->srv_rr); + LIST_FOREACH(items, txt_data, s->txt_data_items) + txt_data->rr = dns_resource_record_unref(txt_data->rr); + + r = dnssd_render_instance_name(s, &n); + if (r < 0) + return r; + + r = dns_name_concat(s->type, "local", &service_name); + if (r < 0) + return r; + r = dns_name_concat(n, service_name, &full_name); + if (r < 0) + return r; + + LIST_FOREACH(items, txt_data, s->txt_data_items) { + txt_data->rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_TXT, + full_name); + if (!txt_data->rr) + goto oom; + + txt_data->rr->ttl = MDNS_DEFAULT_TTL; + txt_data->rr->txt.items = dns_txt_item_copy(txt_data->txt); + if (!txt_data->rr->txt.items) + goto oom; + } + + s->ptr_rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_PTR, + service_name); + if (!s->ptr_rr) + goto oom; + + s->ptr_rr->ttl = MDNS_DEFAULT_TTL; + s->ptr_rr->ptr.name = strdup(full_name); + if (!s->ptr_rr->ptr.name) + goto oom; + + s->srv_rr = dns_resource_record_new_full(DNS_CLASS_IN, DNS_TYPE_SRV, + full_name); + if (!s->srv_rr) + goto oom; + + s->srv_rr->ttl = MDNS_DEFAULT_TTL; + s->srv_rr->srv.priority = s->priority; + s->srv_rr->srv.weight = s->weight; + s->srv_rr->srv.port = s->port; + s->srv_rr->srv.name = strdup(s->manager->mdns_hostname); + if (!s->srv_rr->srv.name) + goto oom; + + return 0; + +oom: + LIST_FOREACH(items, txt_data, s->txt_data_items) + txt_data->rr = dns_resource_record_unref(txt_data->rr); + s->ptr_rr = dns_resource_record_unref(s->ptr_rr); + s->srv_rr = dns_resource_record_unref(s->srv_rr); + return -ENOMEM; +} + +int dnssd_txt_item_new_from_string(const char *key, const char *value, DnsTxtItem **ret_item) { + size_t length; + DnsTxtItem *i; + + length = strlen(key); + + if (!isempty(value)) + length += strlen(value) + 1; /* length of value plus '=' */ + + i = malloc0(offsetof(DnsTxtItem, data) + length + 1); /* for safety reasons we add an extra NUL byte */ + if (!i) + return -ENOMEM; + + memcpy(i->data, key, strlen(key)); + if (!isempty(value)) { + memcpy(i->data + strlen(key), "=", 1); + memcpy(i->data + strlen(key) + 1, value, strlen(value)); + } + i->length = length; + + *ret_item = i; + i = NULL; + + return 0; +} + +int dnssd_txt_item_new_from_data(const char *key, const void *data, const size_t size, DnsTxtItem **ret_item) { + size_t length; + DnsTxtItem *i; + + length = strlen(key); + + if (size > 0) + length += size + 1; /* size of date plus '=' */ + + i = malloc0(offsetof(DnsTxtItem, data) + length + 1); /* for safety reasons we add an extra NUL byte */ + if (!i) + return -ENOMEM; + + memcpy(i->data, key, strlen(key)); + if (size > 0) { + memcpy(i->data + strlen(key), "=", 1); + memcpy(i->data + strlen(key) + 1, data, size); + } + i->length = length; + + *ret_item = i; + i = NULL; + + return 0; +} + +void dnssd_signal_conflict(Manager *manager, const char *name) { + Iterator i; + DnssdService *s; + int r; + + HASHMAP_FOREACH(s, manager->dnssd_services, i) { + if (s->withdrawn) + continue; + + if (dns_name_equal(dns_resource_key_name(s->srv_rr->key), name)) { + _cleanup_free_ char *path = NULL; + + s->withdrawn = true; + + r = sd_bus_path_encode("/org/freedesktop/resolve1/dnssd", s->name, &path); + if (r < 0) { + log_error_errno(r, "Can't get D-BUS object path: %m"); + return; + } + + r = sd_bus_emit_signal(manager->bus, + path, + "org.freedesktop.resolve1.DnssdService", + "Conflicted", + NULL); + if (r < 0) { + log_error_errno(r, "Cannot emit signal: %m"); + return; + } + + break; + } + } +} diff --git a/src/resolve/resolved-dnssd.h b/src/resolve/resolved-dnssd.h new file mode 100644 index 0000000000..fbc043d673 --- /dev/null +++ b/src/resolve/resolved-dnssd.h @@ -0,0 +1,78 @@ +#pragma once + +/*** + This file is part of systemd. + + Copyright 2017 Dmitry Rozhkov + + systemd is free software; you can redistribute it and/or modify it + under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation; either version 2.1 of the License, or + (at your option) any later version. + + systemd is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with systemd; If not, see . +***/ + +#include "list.h" + +typedef struct DnssdService DnssdService; +typedef struct DnssdTxtData DnssdTxtData; + +typedef struct Manager Manager; +typedef struct DnsResourceRecord DnsResourceRecord; +typedef struct DnsTxtItem DnsTxtItem; + +enum { + DNS_TXT_ITEM_TEXT, + DNS_TXT_ITEM_DATA +}; + +struct DnssdTxtData { + DnsResourceRecord *rr; + + LIST_HEAD(DnsTxtItem, txt); + + LIST_FIELDS(DnssdTxtData, items); +}; + +struct DnssdService { + char *filename; + char *name; + char *name_template; + char *type; + uint16_t port; + uint16_t priority; + uint16_t weight; + + DnsResourceRecord *ptr_rr; + DnsResourceRecord *srv_rr; + + /* Section 6.8 of RFC 6763 allows having service + * instances with multiple TXT resource records. */ + LIST_HEAD(DnssdTxtData, txt_data_items); + + Manager *manager; + + bool withdrawn:1; + uid_t originator; +}; + +DnssdService *dnssd_service_free(DnssdService *service); +DnssdTxtData *dnssd_txtdata_free(DnssdTxtData *txt_data); +DnssdTxtData *dnssd_txtdata_free_all(DnssdTxtData *txt_data); + +DEFINE_TRIVIAL_CLEANUP_FUNC(DnssdService*, dnssd_service_free); +DEFINE_TRIVIAL_CLEANUP_FUNC(DnssdTxtData*, dnssd_txtdata_free); + +int dnssd_render_instance_name(DnssdService *s, char **ret_name); +int dnssd_load(Manager *manager); +int dnssd_txt_item_new_from_string(const char *key, const char *value, DnsTxtItem **ret_item); +int dnssd_txt_item_new_from_data(const char *key, const void *value, const size_t size, DnsTxtItem **ret_item); +int dnssd_update_rrs(DnssdService *s); +void dnssd_signal_conflict(Manager *manager, const char *name); diff --git a/src/resolve/resolved-link.c b/src/resolve/resolved-link.c index a0128aace0..ed7bdc848a 100644 --- a/src/resolve/resolved-link.c +++ b/src/resolve/resolved-link.c @@ -191,9 +191,41 @@ void link_allocate_scopes(Link *l) { void link_add_rrs(Link *l, bool force_remove) { LinkAddress *a; + int r; LIST_FOREACH(addresses, a, l->addresses) link_address_add_rrs(a, force_remove); + + if (!force_remove && + l->mdns_support == RESOLVE_SUPPORT_YES && + l->manager->mdns_support == RESOLVE_SUPPORT_YES) { + + if (l->mdns_ipv4_scope) { + r = dns_scope_add_dnssd_services(l->mdns_ipv4_scope); + if (r < 0) + log_warning_errno(r, "Failed to add IPv4 DNS-SD services: %m"); + } + + if (l->mdns_ipv6_scope) { + r = dns_scope_add_dnssd_services(l->mdns_ipv6_scope); + if (r < 0) + log_warning_errno(r, "Failed to add IPv6 DNS-SD services: %m"); + } + + } else { + + if (l->mdns_ipv4_scope) { + r = dns_scope_remove_dnssd_services(l->mdns_ipv4_scope); + if (r < 0) + log_warning_errno(r, "Failed to remove IPv4 DNS-SD services: %m"); + } + + if (l->mdns_ipv6_scope) { + r = dns_scope_remove_dnssd_services(l->mdns_ipv6_scope); + if (r < 0) + log_warning_errno(r, "Failed to remove IPv6 DNS-SD services: %m"); + } + } } int link_process_rtnl(Link *l, sd_netlink_message *m) { diff --git a/src/resolve/resolved-manager.c b/src/resolve/resolved-manager.c index 4292383d21..983e6c091a 100644 --- a/src/resolve/resolved-manager.c +++ b/src/resolve/resolved-manager.c @@ -41,6 +41,7 @@ #include "random-util.h" #include "resolved-bus.h" #include "resolved-conf.h" +#include "resolved-dnssd.h" #include "resolved-dns-stub.h" #include "resolved-etc-hosts.h" #include "resolved-llmnr.h" @@ -621,6 +622,10 @@ int manager_new(Manager **ret) { if (r < 0) return r; + r = dnssd_load(m); + if (r < 0) + log_warning_errno(r, "Failed to load DNS-SD configuration files: %m"); + r = dns_scope_new(m, &m->unicast_scope, NULL, DNS_PROTOCOL_DNS, AF_UNSPEC); if (r < 0) return r; @@ -663,6 +668,7 @@ int manager_start(Manager *m) { Manager *manager_free(Manager *m) { Link *l; + DnssdService *s; if (!m) return NULL; @@ -719,6 +725,10 @@ Manager *manager_free(Manager *m) { free(m->llmnr_hostname); free(m->mdns_hostname); + while ((s = hashmap_first(m->dnssd_services))) + dnssd_service_free(s); + hashmap_free(m->dnssd_services); + dns_trust_anchor_flush(&m->trust_anchor); manager_etc_hosts_flush(m); @@ -1094,6 +1104,7 @@ int manager_find_ifindex(Manager *m, int family, const union in_addr_union *in_a void manager_refresh_rrs(Manager *m) { Iterator i; Link *l; + DnssdService *s; assert(m); @@ -1102,25 +1113,27 @@ void manager_refresh_rrs(Manager *m) { m->mdns_host_ipv4_key = dns_resource_key_unref(m->mdns_host_ipv4_key); m->mdns_host_ipv6_key = dns_resource_key_unref(m->mdns_host_ipv6_key); + if (m->mdns_support == RESOLVE_SUPPORT_YES) + HASHMAP_FOREACH(s, m->dnssd_services, i) + if (dnssd_update_rrs(s) < 0) + log_warning("Failed to refresh DNS-SD service '%s'", s->name); + HASHMAP_FOREACH(l, m->links, i) { link_add_rrs(l, true); link_add_rrs(l, false); } } -int manager_next_hostname(Manager *m) { +static int manager_next_random_name(const char *old, char **ret_new) { const char *p; uint64_t u, a; - char *h, *k; - int r; + char *n; - assert(m); - - p = strchr(m->llmnr_hostname, 0); + p = strchr(old, 0); assert(p); - while (p > m->llmnr_hostname) { - if (!strchr("0123456789", p[-1])) + while (p > old) { + if (!strchr(DIGITS, p[-1])) break; p--; @@ -1139,22 +1152,32 @@ int manager_next_hostname(Manager *m) { random_bytes(&a, sizeof(a)); u += 1 + a % 10; - if (asprintf(&h, "%.*s%" PRIu64, (int) (p - m->llmnr_hostname), m->llmnr_hostname, u) < 0) + if (asprintf(&n, "%.*s%" PRIu64, (int) (p - old), old, u) < 0) return -ENOMEM; + *ret_new = n; + + return 0; +} + +int manager_next_hostname(Manager *m) { + _cleanup_free_ char *h = NULL, *k = NULL; + int r; + + assert(m); + + r = manager_next_random_name(m->llmnr_hostname, &h); + if (r < 0) + return r; + r = dns_name_concat(h, "local", &k); - if (r < 0) { - free(h); + if (r < 0) return r; - } log_info("Hostname conflict, changing published hostname from '%s' to '%s'.", m->llmnr_hostname, h); - free(m->llmnr_hostname); - m->llmnr_hostname = h; - - free(m->mdns_hostname); - m->mdns_hostname = k; + free_and_replace(m->llmnr_hostname, h); + free_and_replace(m->mdns_hostname, k); manager_refresh_rrs(m); @@ -1488,3 +1511,36 @@ void manager_cleanup_saved_user(Manager *m) { (void) unlink(p); } } + +bool manager_next_dnssd_names(Manager *m) { + Iterator i; + DnssdService *s; + bool tried = false; + int r; + + assert(m); + + HASHMAP_FOREACH(s, m->dnssd_services, i) { + _cleanup_free_ char * new_name = NULL; + + if (!s->withdrawn) + continue; + + r = manager_next_random_name(s->name_template, &new_name); + if (r < 0) { + log_warning_errno(r, "Failed to get new name for service '%s': %m", s->name); + continue; + } + + free_and_replace(s->name_template, new_name); + + s->withdrawn = false; + + tried = true; + } + + if (tried) + manager_refresh_rrs(m); + + return tried; +} diff --git a/src/resolve/resolved-manager.h b/src/resolve/resolved-manager.h index c9ff788510..5c1a6670ef 100644 --- a/src/resolve/resolved-manager.h +++ b/src/resolve/resolved-manager.h @@ -102,6 +102,9 @@ struct Manager { int mdns_ipv4_fd; int mdns_ipv6_fd; + /* DNS-SD */ + Hashmap *dnssd_services; + sd_event_source *mdns_ipv4_event_source; sd_event_source *mdns_ipv6_event_source; @@ -143,6 +146,8 @@ struct Manager { sd_event_source *dns_stub_udp_event_source; sd_event_source *dns_stub_tcp_event_source; + + Hashmap *polkit_registry; }; /* Manager */ @@ -189,3 +194,5 @@ void manager_flush_caches(Manager *m); void manager_reset_server_features(Manager *m); void manager_cleanup_saved_user(Manager *m); + +bool manager_next_dnssd_names(Manager *m); diff --git a/src/resolve/resolved-mdns.c b/src/resolve/resolved-mdns.c index a492937176..38e2c54227 100644 --- a/src/resolve/resolved-mdns.c +++ b/src/resolve/resolved-mdns.c @@ -22,10 +22,13 @@ #include #include +#include "alloc-util.h" #include "fd-util.h" #include "resolved-manager.h" #include "resolved-mdns.h" +#define CLEAR_CACHE_FLUSH(x) (~MDNS_RR_CACHE_FLUSH & (x)) + void manager_mdns_stop(Manager *m) { assert(m); @@ -68,10 +71,145 @@ eaddrinuse: return 0; } +static int mdns_rr_compare(const void *a, const void *b) { + DnsResourceRecord **x = (DnsResourceRecord**) a, **y = (DnsResourceRecord**) b; + size_t m; + int r; + + assert(x); + assert(*x); + assert(y); + assert(*y); + + if (CLEAR_CACHE_FLUSH((*x)->key->class) < CLEAR_CACHE_FLUSH((*y)->key->class)) + return -1; + else if (CLEAR_CACHE_FLUSH((*x)->key->class) > CLEAR_CACHE_FLUSH((*y)->key->class)) + return 1; + + if ((*x)->key->type < (*y)->key->type) + return -1; + else if ((*x)->key->type > (*y)->key->type) + return 1; + + r = dns_resource_record_to_wire_format(*x, false); + if (r < 0) { + log_warning_errno(r, "Can't wire-format RR: %m"); + return 0; + } + + r = dns_resource_record_to_wire_format(*y, false); + if (r < 0) { + log_warning_errno(r, "Can't wire-format RR: %m"); + return 0; + } + + m = MIN(DNS_RESOURCE_RECORD_RDATA_SIZE(*x), DNS_RESOURCE_RECORD_RDATA_SIZE(*y)); + + r = memcmp(DNS_RESOURCE_RECORD_RDATA(*x), DNS_RESOURCE_RECORD_RDATA(*y), m); + if (r != 0) + return r; + + if (DNS_RESOURCE_RECORD_RDATA_SIZE(*x) < DNS_RESOURCE_RECORD_RDATA_SIZE(*y)) + return -1; + else if (DNS_RESOURCE_RECORD_RDATA_SIZE(*x) > DNS_RESOURCE_RECORD_RDATA_SIZE(*y)) + return 1; + + return 0; +} + +static int proposed_rrs_cmp(DnsResourceRecord **x, unsigned x_size, DnsResourceRecord **y, unsigned y_size) { + unsigned m; + int r; + + m = MIN(x_size, y_size); + for (unsigned i = 0; i < m; i++) { + r = mdns_rr_compare(&x[i], &y[i]); + if (r != 0) + return r; + } + + if (x_size < y_size) + return -1; + if (x_size > y_size) + return 1; + + return 0; +} + +static int mdns_packet_extract_matching_rrs(DnsPacket *p, DnsResourceKey *key, DnsResourceRecord ***ret_rrs) { + _cleanup_free_ DnsResourceRecord **list = NULL; + unsigned n = 0, size = 0; + int r; + + assert(p); + assert(key); + assert(ret_rrs); + assert_return(DNS_PACKET_NSCOUNT(p) > 0, -EINVAL); + + for (unsigned i = DNS_PACKET_ANCOUNT(p); i < (DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p)); i++) { + r = dns_resource_key_match_rr(key, p->answer->items[i].rr, NULL); + if (r < 0) + return r; + if (r > 0) + size++; + } + + if (size == 0) + return 0; + + list = new(DnsResourceRecord *, size); + if (!list) + return -ENOMEM; + + for (unsigned i = DNS_PACKET_ANCOUNT(p); i < (DNS_PACKET_ANCOUNT(p) + DNS_PACKET_NSCOUNT(p)); i++) { + r = dns_resource_key_match_rr(key, p->answer->items[i].rr, NULL); + if (r < 0) + return r; + if (r > 0) + list[n++] = p->answer->items[i].rr; + } + assert(n == size); + qsort_safe(list, size, sizeof(DnsResourceRecord*), mdns_rr_compare); + + *ret_rrs = list; + list = NULL; + + return size; +} + +static int mdns_do_tiebreak(DnsResourceKey *key, DnsAnswer *answer, DnsPacket *p) { + _cleanup_free_ DnsResourceRecord **our = NULL, **remote = NULL; + DnsResourceRecord *rr; + unsigned i = 0; + unsigned size; + int r; + + size = dns_answer_size(answer); + our = new(DnsResourceRecord *, size); + if (!our) + return -ENOMEM; + + DNS_ANSWER_FOREACH(rr, answer) + our[i++] = rr; + qsort_safe(our, size, sizeof(DnsResourceRecord*), mdns_rr_compare); + + r = mdns_packet_extract_matching_rrs(p, key, &remote); + if (r < 0) + return r; + + assert(r > 0); + + if (proposed_rrs_cmp(remote, r, our, size) > 0) + return 1; + + return 0; +} + static int mdns_scope_process_query(DnsScope *s, DnsPacket *p) { - _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL; + _cleanup_(dns_answer_unrefp) DnsAnswer *full_answer = NULL; _cleanup_(dns_packet_unrefp) DnsPacket *reply = NULL; DnsResourceKey *key = NULL; + DnsResourceRecord *rr; bool tentative = false; int r; @@ -82,17 +220,49 @@ static int mdns_scope_process_query(DnsScope *s, DnsPacket *p) { if (r < 0) return log_debug_errno(r, "Failed to extract resource records from incoming packet: %m"); - /* TODO: there might be more than one question in mDNS queries. */ assert_return((dns_question_size(p->question) > 0), -EINVAL); - key = p->question->keys[0]; - r = dns_zone_lookup(&s->zone, key, 0, &answer, &soa, &tentative); - if (r < 0) - return log_debug_errno(r, "Failed to lookup key: %m"); - if (r == 0) + DNS_QUESTION_FOREACH(key, p->question) { + _cleanup_(dns_answer_unrefp) DnsAnswer *answer = NULL, *soa = NULL; + + r = dns_zone_lookup(&s->zone, key, 0, &answer, &soa, &tentative); + if (r < 0) + return log_debug_errno(r, "Failed to lookup key: %m"); + + if (tentative && DNS_PACKET_NSCOUNT(p) > 0) { + /* + * A race condition detected with the probe packet from + * a remote host. + * Do simultaneous probe tiebreaking as described in + * RFC 6762, Section 8.2. In case we lost don't reply + * the question and withdraw conflicting RRs. + */ + r = mdns_do_tiebreak(key, answer, p); + if (r < 0) + return log_debug_errno(r, "Failed to do tiebreaking"); + + if (r > 0) { /* we lost */ + DNS_ANSWER_FOREACH(rr, answer) { + DnsZoneItem *i; + + i = dns_zone_get(&s->zone, rr); + if (i) + dns_zone_item_conflict(i); + } + + continue; + } + } + + r = dns_answer_extend(&full_answer, answer); + if (r < 0) + return log_debug_errno(r, "Failed to extend answer: %m"); + } + + if (dns_answer_isempty(full_answer)) return 0; - r = dns_scope_make_reply_packet(s, DNS_PACKET_ID(p), DNS_RCODE_SUCCESS, NULL, answer, NULL, false, &reply); + r = dns_scope_make_reply_packet(s, DNS_PACKET_ID(p), DNS_RCODE_SUCCESS, NULL, full_answer, NULL, false, &reply); if (r < 0) return log_debug_errno(r, "Failed to build reply packet: %m"); diff --git a/src/shared/dns-domain.c b/src/shared/dns-domain.c index 670aa48800..bdef67dc4f 100644 --- a/src/shared/dns-domain.c +++ b/src/shared/dns-domain.c @@ -959,6 +959,12 @@ bool dns_srv_type_is_valid(const char *name) { return c == 2; /* exactly two labels */ } +bool dnssd_srv_type_is_valid(const char *name) { + return dns_srv_type_is_valid(name) && + ((dns_name_endswith(name, "_tcp") > 0) || + (dns_name_endswith(name, "_udp") > 0)); /* Specific to DNS-SD. RFC 6763, Section 7 */ +} + bool dns_service_name_is_valid(const char *name) { size_t l; diff --git a/src/shared/dns-domain.h b/src/shared/dns-domain.h index af74b97484..cd3dbb7a89 100644 --- a/src/shared/dns-domain.h +++ b/src/shared/dns-domain.h @@ -96,6 +96,7 @@ bool dns_name_is_single_label(const char *name); int dns_name_to_wire_format(const char *domain, uint8_t *buffer, size_t len, bool canonical); bool dns_srv_type_is_valid(const char *name); +bool dnssd_srv_type_is_valid(const char *name); bool dns_service_name_is_valid(const char *name); int dns_service_join(const char *name, const char *type, const char *domain, char **ret); diff --git a/src/test/test-dns-domain.c b/src/test/test-dns-domain.c index 5f669df051..3105c14ee6 100644 --- a/src/test/test-dns-domain.c +++ b/src/test/test-dns-domain.c @@ -427,6 +427,30 @@ static void test_dns_srv_type_is_valid(void) { assert_se(!dns_srv_type_is_valid("_piep._foo._udp")); } +static void test_dnssd_srv_type_is_valid(void) { + + assert_se(dnssd_srv_type_is_valid("_http._tcp")); + assert_se(dnssd_srv_type_is_valid("_foo-bar._tcp")); + assert_se(dnssd_srv_type_is_valid("_w._udp")); + assert_se(dnssd_srv_type_is_valid("_a800._tcp")); + assert_se(dnssd_srv_type_is_valid("_a-800._tcp")); + + assert_se(!dnssd_srv_type_is_valid(NULL)); + assert_se(!dnssd_srv_type_is_valid("")); + assert_se(!dnssd_srv_type_is_valid("x")); + assert_se(!dnssd_srv_type_is_valid("_foo")); + assert_se(!dnssd_srv_type_is_valid("_tcp")); + assert_se(!dnssd_srv_type_is_valid("_")); + assert_se(!dnssd_srv_type_is_valid("_foo.")); + assert_se(!dnssd_srv_type_is_valid("_föo._tcp")); + assert_se(!dnssd_srv_type_is_valid("_f\no._tcp")); + assert_se(!dnssd_srv_type_is_valid("_800._tcp")); + assert_se(!dnssd_srv_type_is_valid("_-800._tcp")); + assert_se(!dnssd_srv_type_is_valid("_-foo._tcp")); + assert_se(!dnssd_srv_type_is_valid("_piep._foo._udp")); + assert_se(!dnssd_srv_type_is_valid("_foo._unknown")); +} + static void test_dns_service_join_one(const char *a, const char *b, const char *c, int r, const char *d) { _cleanup_free_ char *x = NULL, *y = NULL, *z = NULL, *t = NULL; @@ -699,6 +723,7 @@ int main(int argc, char *argv[]) { test_dns_name_to_wire_format(); test_dns_service_name_is_valid(); test_dns_srv_type_is_valid(); + test_dnssd_srv_type_is_valid(); test_dns_service_join(); test_dns_service_split(); test_dns_name_change_suffix();