]> git.ipfire.org Git - thirdparty/systemd.git/blobdiff - src/resolve/resolved-dns-transaction.c
Merge pull request #30284 from YHNdnzj/fstab-wantedby-defaultdeps
[thirdparty/systemd.git] / src / resolve / resolved-dns-transaction.c
index 0ef0782bbbf14fe138c0eec391eed5af5f2f19e5..fe88e502e7c11c3ad24264ff47ace13279113a36 100644 (file)
@@ -69,7 +69,7 @@ static void dns_transaction_close_connection(
 
         t->dns_udp_event_source = sd_event_source_disable_unref(t->dns_udp_event_source);
 
-        /* If we have an UDP socket where we sent a packet, but never received one, then add it to the socket
+        /* If we have a UDP socket where we sent a packet, but never received one, then add it to the socket
          * graveyard, instead of closing it right away. That way it will stick around for a moment longer,
          * and the reply we might still get from the server will be eaten up instead of resulting in an ICMP
          * port unreachable error message. */
@@ -273,7 +273,7 @@ int dns_transaction_new(
                 return -ENOMEM;
 
         *t = (DnsTransaction) {
-                .dns_udp_fd = -1,
+                .dns_udp_fd = -EBADF,
                 .answer_source = _DNS_TRANSACTION_SOURCE_INVALID,
                 .answer_dnssec_result = _DNSSEC_RESULT_INVALID,
                 .answer_nsec_ttl = UINT32_MAX,
@@ -282,7 +282,6 @@ int dns_transaction_new(
                 .bypass = dns_packet_ref(bypass),
                 .current_feature_level = _DNS_SERVER_FEATURE_LEVEL_INVALID,
                 .clamp_feature_level_servfail = _DNS_SERVER_FEATURE_LEVEL_INVALID,
-                .clamp_feature_level_nxdomain = _DNS_SERVER_FEATURE_LEVEL_INVALID,
                 .id = pick_new_id(s->manager),
         };
 
@@ -472,10 +471,8 @@ static int dns_transaction_pick_server(DnsTransaction *t) {
 
         /* If we changed the server invalidate the feature level clamping, as the new server might have completely
          * different properties. */
-        if (server != t->server) {
+        if (server != t->server)
                 t->clamp_feature_level_servfail = _DNS_SERVER_FEATURE_LEVEL_INVALID;
-                t->clamp_feature_level_nxdomain = _DNS_SERVER_FEATURE_LEVEL_INVALID;
-        }
 
         t->current_feature_level = dns_server_possible_feature_level(server);
 
@@ -483,9 +480,6 @@ static int dns_transaction_pick_server(DnsTransaction *t) {
         if (t->clamp_feature_level_servfail != _DNS_SERVER_FEATURE_LEVEL_INVALID &&
             t->current_feature_level > t->clamp_feature_level_servfail)
                 t->current_feature_level = t->clamp_feature_level_servfail;
-        if (t->clamp_feature_level_nxdomain != _DNS_SERVER_FEATURE_LEVEL_INVALID &&
-            t->current_feature_level > t->clamp_feature_level_nxdomain)
-                t->current_feature_level = t->clamp_feature_level_nxdomain;
 
         log_debug("Using feature level %s for transaction %u.", dns_server_feature_level_to_string(t->current_feature_level), t->id);
 
@@ -669,7 +663,7 @@ static uint16_t dns_transaction_port(DnsTransaction *t) {
 static int dns_transaction_emit_tcp(DnsTransaction *t) {
         usec_t stream_timeout_usec = DNS_STREAM_DEFAULT_TIMEOUT_USEC;
         _cleanup_(dns_stream_unrefp) DnsStream *s = NULL;
-        _cleanup_close_ int fd = -1;
+        _cleanup_close_ int fd = -EBADF;
         union sockaddr_union sa;
         DnsStreamType type;
         int r;
@@ -753,7 +747,7 @@ static int dns_transaction_emit_tcp(DnsTransaction *t) {
                 if (r < 0)
                         return r;
 
-                fd = -1;
+                fd = -EBADF;
 
 #if ENABLE_DNS_OVER_TLS
                 if (t->scope->protocol == DNS_PROTOCOL_DNS &&
@@ -817,18 +811,20 @@ static void dns_transaction_cache_answer(DnsTransaction *t) {
 
         dns_cache_put(&t->scope->cache,
                       t->scope->manager->enable_cache,
+                      t->scope->protocol,
                       dns_transaction_key(t),
                       t->answer_rcode,
                       t->answer,
                       DNS_PACKET_CD(t->received) ? t->received : NULL, /* only cache full packets with CD on,
-                                                                        * since our usecase for caching them
+                                                                        * since our use case for caching them
                                                                         * is "bypass" mode which is only
                                                                         * enabled for CD packets. */
                       t->answer_query_flags,
                       t->answer_dnssec_result,
                       t->answer_nsec_ttl,
                       t->received->family,
-                      &t->received->sender);
+                      &t->received->sender,
+                      t->scope->manager->stale_retention_usec);
 }
 
 static bool dns_transaction_dnssec_is_live(DnsTransaction *t) {
@@ -1043,6 +1039,13 @@ void dns_transaction_process_reply(DnsTransaction *t, DnsPacket *p, bool encrypt
         if (t->state != DNS_TRANSACTION_PENDING)
                 return;
 
+        /* Increment the total failure counter only when it is the first attempt at querying and the upstream
+         * server returns a failure response code. This ensures a more accurate count of the number of queries
+         * that received a failure response code, as it doesn't consider retries. */
+
+        if (t->n_attempts == 1 && !IN_SET(DNS_PACKET_RCODE(p), DNS_RCODE_SUCCESS, DNS_RCODE_NXDOMAIN))
+                t->scope->manager->n_failure_responses_total++;
+
         /* Note that this call might invalidate the query. Callers
          * should hence not attempt to access the query or transaction
          * after calling this function. */
@@ -1276,45 +1279,6 @@ void dns_transaction_process_reply(DnsTransaction *t, DnsPacket *p, bool encrypt
                 return;
         }
 
-        if (t->scope->protocol == DNS_PROTOCOL_DNS &&
-            !t->bypass &&
-            DNS_PACKET_RCODE(p) == DNS_RCODE_NXDOMAIN &&
-            p->opt && !DNS_PACKET_DO(p) &&
-            DNS_SERVER_FEATURE_LEVEL_IS_EDNS0(t->current_feature_level) &&
-            DNS_SERVER_FEATURE_LEVEL_IS_UDP(t->current_feature_level) &&
-            t->scope->dnssec_mode != DNSSEC_YES) {
-
-                /* Some captive portals are special in that the Aruba/Datavalet hardware will miss
-                 * replacing the packets with the local server IP to point to the authenticated side
-                 * of the network if EDNS0 is enabled. Instead they return NXDOMAIN, with DO bit set
-                 * to zero... nothing to see here, yet respond with the captive portal IP, when using
-                 * the more simple UDP level.
-                 *
-                 * Common portal names that fail like so are:
-                 *     secure.datavalet.io
-                 *     securelogin.arubanetworks.com
-                 *     securelogin.networks.mycompany.com
-                 *
-                 * Thus retry NXDOMAIN RCODES with a lower feature level.
-                 *
-                 * Do not lower the server's tracked feature level, as the captive portal should not
-                 * be lying for the wider internet (e.g. _other_ queries were observed fine with
-                 * EDNS0 on these networks, post auth), i.e. let's just lower the level transaction's
-                 * feature level.
-                 *
-                 * This is reported as https://github.com/dns-violations/dns-violations/blob/master/2018/DVE-2018-0001.md
-                 */
-
-                t->clamp_feature_level_nxdomain = DNS_SERVER_FEATURE_LEVEL_UDP;
-
-                log_debug("Server returned error %s in EDNS0 mode, retrying transaction with reduced feature level %s (DVE-2018-0001 mitigation)",
-                          FORMAT_DNS_RCODE(DNS_PACKET_RCODE(p)),
-                          dns_server_feature_level_to_string(t->clamp_feature_level_nxdomain));
-
-                dns_transaction_retry(t, false /* use the same server */);
-                return;
-        }
-
         if (t->server) {
                 /* Report that we successfully received a valid packet with a good rcode after we initially got a bad
                  * rcode and subsequently downgraded the protocol */
@@ -1410,25 +1374,22 @@ static int on_dns_packet(sd_event_source *s, int fd, uint32_t revents, void *use
         assert(t->scope);
 
         r = manager_recv(t->scope->manager, fd, DNS_PROTOCOL_DNS, &p);
-        if (ERRNO_IS_DISCONNECT(r)) {
-                usec_t usec;
+        if (r < 0) {
+                if (ERRNO_IS_DISCONNECT(r)) {
+                        usec_t usec;
 
-                /* UDP connection failures get reported via ICMP and then are possibly delivered to us on the
-                 * next recvmsg(). Treat this like a lost packet. */
+                        /* UDP connection failures get reported via ICMP and then are possibly delivered to us on the
+                         * next recvmsg(). Treat this like a lost packet. */
 
-                log_debug_errno(r, "Connection failure for DNS UDP packet: %m");
-                assert_se(sd_event_now(t->scope->manager->event, CLOCK_BOOTTIME, &usec) >= 0);
-                dns_server_packet_lost(t->server, IPPROTO_UDP, t->current_feature_level);
+                        log_debug_errno(r, "Connection failure for DNS UDP packet: %m");
+                        assert_se(sd_event_now(t->scope->manager->event, CLOCK_BOOTTIME, &usec) >= 0);
+                        dns_server_packet_lost(t->server, IPPROTO_UDP, t->current_feature_level);
 
-                dns_transaction_close_connection(t, /* use_graveyard = */ false);
+                        dns_transaction_close_connection(t, /* use_graveyard = */ false);
 
-                if (dns_transaction_limited_retry(t)) /* Try a different server */
-                        return 0;
-
-                dns_transaction_complete_errno(t, r);
-                return 0;
-        }
-        if (r < 0) {
+                        if (dns_transaction_limited_retry(t)) /* Try a different server */
+                                return 0;
+                }
                 dns_transaction_complete_errno(t, r);
                 return 0;
         }
@@ -1519,6 +1480,8 @@ static int on_transaction_timeout(sd_event_source *s, usec_t usec, void *userdat
 
         assert(s);
 
+        t->seen_timeout = true;
+
         if (t->initial_jitter_scheduled && !t->initial_jitter_elapsed) {
                 log_debug("Initial jitter phase for transaction %" PRIu16 " elapsed.", t->id);
                 t->initial_jitter_elapsed = true;
@@ -1643,6 +1606,9 @@ static int dns_transaction_prepare(DnsTransaction *t, usec_t ts) {
 
         dns_transaction_stop_timeout(t);
 
+        if (t->n_attempts == 1 && t->seen_timeout)
+                t->scope->manager->n_timeouts_total++;
+
         if (!dns_scope_network_good(t->scope)) {
                 dns_transaction_complete(t, DNS_TRANSACTION_NETWORK_DOWN);
                 return 0;
@@ -1741,10 +1707,18 @@ static int dns_transaction_prepare(DnsTransaction *t, usec_t ts) {
                 /* Let's then prune all outdated entries */
                 dns_cache_prune(&t->scope->cache);
 
+                /* For the initial attempt or when no stale data is requested, disable serve stale
+                 * and answer the question from the cache (honors ttl property).
+                 * On the second attempt, if StaleRetentionSec is greater than zero,
+                 * try to answer the question using stale date (honors until property) */
+                uint64_t query_flags = t->query_flags;
+                if (t->n_attempts == 1 || t->scope->manager->stale_retention_usec == 0)
+                        query_flags |= SD_RESOLVED_NO_STALE;
+
                 r = dns_cache_lookup(
                                 &t->scope->cache,
                                 dns_transaction_key(t),
-                                t->query_flags,
+                                query_flags,
                                 &t->answer_rcode,
                                 &t->answer,
                                 &t->received,
@@ -1760,6 +1734,21 @@ static int dns_transaction_prepare(DnsTransaction *t, usec_t ts) {
                                  * packet. */
                                 dns_transaction_reset_answer(t);
                         else {
+                                if (t->n_attempts > 1 && !FLAGS_SET(query_flags, SD_RESOLVED_NO_STALE)) {
+
+                                        if (t->answer_rcode == DNS_RCODE_SUCCESS) {
+                                                if (t->seen_timeout)
+                                                        t->scope->manager->n_timeouts_served_stale_total++;
+                                                else
+                                                        t->scope->manager->n_failure_responses_served_stale_total++;
+                                        }
+
+                                        char key_str[DNS_RESOURCE_KEY_STRING_MAX];
+                                        log_debug("Serve Stale response rcode=%s for %s",
+                                                FORMAT_DNS_RCODE(t->answer_rcode),
+                                                dns_resource_key_to_string(dns_transaction_key(t), key_str, sizeof key_str));
+                                }
+
                                 t->answer_source = DNS_TRANSACTION_CACHE;
                                 if (t->answer_rcode == DNS_RCODE_SUCCESS)
                                         dns_transaction_complete(t, DNS_TRANSACTION_SUCCESS);
@@ -1797,21 +1786,20 @@ static int dns_packet_append_zone(DnsPacket *p, DnsTransaction *t, DnsResourceKe
         return dns_packet_append_answer(p, answer, nscount);
 }
 
-static int dns_transaction_make_packet_mdns(DnsTransaction *t) {
+static int mdns_make_dummy_packet(DnsTransaction *t, DnsPacket **ret_packet, Set **ret_keys) {
         _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL;
         _cleanup_set_free_ Set *keys = NULL;
-        unsigned qdcount, ancount = 0 /* avoid false maybe-uninitialized warning */, nscount;
         bool add_known_answers = false;
+        unsigned qdcount;
         usec_t ts;
         int r;
 
         assert(t);
+        assert(t->scope);
         assert(t->scope->protocol == DNS_PROTOCOL_MDNS);
+        assert(ret_packet);
+        assert(ret_keys);
 
-        /* Discard any previously prepared packet, so we can start over and coalesce again */
-        t->sent = dns_packet_unref(t->sent);
-
-        /* First, create a dummy packet to calculate packet size. */
         r = dns_packet_new_query(&p, t->scope->protocol, 0, false);
         if (r < 0)
                 return r;
@@ -1834,120 +1822,139 @@ static int dns_transaction_make_packet_mdns(DnsTransaction *t) {
         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
-         * in our current scope, and see whether their timing constraints allow them to be sent.
-         */
-
         assert_se(sd_event_now(t->scope->manager->event, CLOCK_BOOTTIME, &ts) >= 0);
 
-        for (bool restart = true; restart;) {
-                restart = false;
-                LIST_FOREACH(transactions_by_scope, other, t->scope->transactions) {
-                        size_t saved_packet_size;
-                        bool append = false;
-
-                        /* Skip ourselves */
-                        if (other == t)
-                                continue;
-
-                        if (other->state != DNS_TRANSACTION_PENDING)
-                                continue;
+        LIST_FOREACH(transactions_by_scope, other, t->scope->transactions) {
 
-                        if (other->next_attempt_after > ts)
-                                continue;
+                /* Skip ourselves */
+                if (other == t)
+                        continue;
 
-                        if (!set_contains(keys, dns_transaction_key(other))) {
-                                r = dns_packet_append_key(p, dns_transaction_key(other), 0, &saved_packet_size);
-                                /* If we can't stuff more questions into the packet, just give up.
-                                 * One of the 'other' transactions will fire later and take care of the rest. */
-                                if (r == -EMSGSIZE)
-                                        break;
-                                if (r < 0)
-                                        return r;
+                if (other->state != DNS_TRANSACTION_PENDING)
+                        continue;
 
-                                r = dns_packet_append_zone(p, t, dns_transaction_key(other), NULL);
-                                if (r == -EMSGSIZE)
-                                        break;
-                                if (r < 0)
-                                        return r;
+                if (other->next_attempt_after > ts)
+                        continue;
 
-                                append = true;
-                        }
+                if (!set_contains(keys, dns_transaction_key(other))) {
+                        size_t saved_packet_size;
 
-                        r = dns_transaction_prepare(other, ts);
+                        r = dns_packet_append_key(p, dns_transaction_key(other), 0, &saved_packet_size);
+                        /* If we can't stuff more questions into the packet, just give up.
+                         * One of the 'other' transactions will fire later and take care of the rest. */
+                        if (r == -EMSGSIZE)
+                                break;
                         if (r < 0)
                                 return r;
-                        if (r == 0) {
-                                if (append)
-                                        dns_packet_truncate(p, saved_packet_size);
 
-                                /* In this case, not only this transaction, but multiple transactions may be
-                                 * freed. Hence, we need to restart the loop. */
-                                restart = true;
+                        r = dns_packet_append_zone(p, t, dns_transaction_key(other), NULL);
+                        if (r == -EMSGSIZE) {
+                                dns_packet_truncate(p, saved_packet_size);
                                 break;
                         }
+                        if (r < 0)
+                                return r;
 
-                        usec_t timeout = transaction_get_resend_timeout(other);
-                        r = dns_transaction_setup_timeout(other, timeout, usec_add(ts, timeout));
+                        r = set_ensure_put(&keys, &dns_resource_key_hash_ops, dns_transaction_key(other));
                         if (r < 0)
                                 return r;
+                }
 
-                        if (dns_key_is_shared(dns_transaction_key(other)))
-                                add_known_answers = true;
+                r = dns_transaction_prepare(other, ts);
+                if (r < 0)
+                        return r;
+                if (r == 0)
+                        /* In this case, not only this transaction, but multiple transactions may be
+                         * freed. Hence, we need to restart the loop. */
+                        return -EAGAIN;
 
-                        if (append) {
-                                r = set_ensure_put(&keys, &dns_resource_key_hash_ops, dns_transaction_key(other));
-                                if (r < 0)
-                                        return r;
-                        }
+                usec_t timeout = transaction_get_resend_timeout(other);
+                r = dns_transaction_setup_timeout(other, timeout, usec_add(ts, timeout));
+                if (r < 0)
+                        return r;
 
-                        qdcount++;
-                        if (qdcount >= UINT16_MAX)
-                                break;
-                }
+                if (dns_key_is_shared(dns_transaction_key(other)))
+                        add_known_answers = true;
+
+                qdcount++;
+                if (qdcount >= UINT16_MAX)
+                        break;
         }
 
-        /* Append known answer section if we're asking for any shared record */
+        DNS_PACKET_HEADER(p)->qdcount = htobe16(qdcount);
+
+        /* Append known answers section if we're asking for any shared record */
         if (add_known_answers) {
                 r = dns_cache_export_shared_to_packet(&t->scope->cache, p, ts, 0);
                 if (r < 0)
                         return r;
+        }
+
+        *ret_packet = TAKE_PTR(p);
+        *ret_keys = TAKE_PTR(keys);
+        return add_known_answers;
+}
 
-                ancount = be16toh(DNS_PACKET_HEADER(p)->ancount);
+static int dns_transaction_make_packet_mdns(DnsTransaction *t) {
+        _cleanup_(dns_packet_unrefp) DnsPacket *p = NULL, *dummy = NULL;
+        _cleanup_set_free_ Set *keys = NULL;
+        bool add_known_answers;
+        DnsResourceKey *k;
+        unsigned c;
+        int r;
+
+        assert(t);
+        assert(t->scope->protocol == DNS_PROTOCOL_MDNS);
+
+        /* Discard any previously prepared packet, so we can start over and coalesce again */
+        t->sent = dns_packet_unref(t->sent);
+
+        /* First, create a dummy packet to calculate the number of known answers to be appended in the first packet. */
+        for (;;) {
+                r = mdns_make_dummy_packet(t, &dummy, &keys);
+                if (r == -EAGAIN)
+                        continue;
+                if (r < 0)
+                        return r;
+
+                add_known_answers = r;
+                break;
         }
 
         /* Then, create actual packet. */
-        p = dns_packet_unref(p);
         r = dns_packet_new_query(&p, t->scope->protocol, 0, false);
         if (r < 0)
                 return r;
 
         /* Questions */
-        DnsResourceKey *k;
+        c = 0;
         SET_FOREACH(k, keys) {
                 r = dns_packet_append_key(p, k, 0, NULL);
                 if (r < 0)
                         return r;
+                c++;
         }
-        DNS_PACKET_HEADER(p)->qdcount = htobe16(qdcount);
+        DNS_PACKET_HEADER(p)->qdcount = htobe16(c);
 
         /* Known answers */
         if (add_known_answers) {
-                r = dns_cache_export_shared_to_packet(&t->scope->cache, p, ts, ancount);
+                usec_t ts;
+
+                assert_se(sd_event_now(t->scope->manager->event, CLOCK_BOOTTIME, &ts) >= 0);
+
+                r = dns_cache_export_shared_to_packet(&t->scope->cache, p, ts, be16toh(DNS_PACKET_HEADER(dummy)->ancount));
                 if (r < 0)
                         return r;
         }
 
         /* Authorities */
-        nscount = 0;
+        c = 0;
         SET_FOREACH(k, keys) {
-                r = dns_packet_append_zone(p, t, k, &nscount);
+                r = dns_packet_append_zone(p, t, k, &c);
                 if (r < 0)
                         return r;
         }
-        DNS_PACKET_HEADER(p)->nscount = htobe16(nscount);
+        DNS_PACKET_HEADER(p)->nscount = htobe16(c);
 
         t->sent = TAKE_PTR(p);
         return 0;
@@ -2078,7 +2085,9 @@ int dns_transaction_go(DnsTransaction *t) {
                         log_debug("Sending query via TCP since it is too large.");
                 else if (r == -EAGAIN)
                         log_debug("Sending query via TCP since UDP isn't supported or DNS-over-TLS is selected.");
-                if (IN_SET(r, -EMSGSIZE, -EAGAIN))
+                else if (r == -EPERM)
+                        log_debug("Sending query via TCP since UDP is blocked.");
+                if (IN_SET(r, -EMSGSIZE, -EAGAIN, -EPERM))
                         r = dns_transaction_emit_tcp(t);
         }
         if (r == -ELOOP) {
@@ -2108,7 +2117,7 @@ int dns_transaction_go(DnsTransaction *t) {
                 dns_transaction_complete(t, DNS_TRANSACTION_RR_TYPE_UNSUPPORTED);
                 return 0;
         }
-        if (t->scope->protocol == DNS_PROTOCOL_LLMNR && ERRNO_IS_DISCONNECT(r)) {
+        if (t->scope->protocol == DNS_PROTOCOL_LLMNR && ERRNO_IS_NEG_DISCONNECT(r)) {
                 /* On LLMNR, if we cannot connect to a host via TCP when doing reverse lookups. This means we cannot
                  * answer this request with this protocol. */
                 dns_transaction_complete(t, DNS_TRANSACTION_NOT_FOUND);
@@ -2799,7 +2808,7 @@ static int dns_transaction_requires_rrsig(DnsTransaction *t, DnsResourceRecord *
                         if (r == 0)
                                 continue;
 
-                        return FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED);
+                        return FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED);
                 }
 
                 return true;
@@ -2826,7 +2835,7 @@ static int dns_transaction_requires_rrsig(DnsTransaction *t, DnsResourceRecord *
                         /* We found the transaction that was supposed to find the SOA RR for us. It was
                          * successful, but found no RR for us. This means we are not at a zone cut. In this
                          * case, we require authentication if the SOA lookup was authenticated too. */
-                        return FLAGS_SET(t->answer_query_flags, SD_RESOLVED_AUTHENTICATED);
+                        return FLAGS_SET(dt->answer_query_flags, SD_RESOLVED_AUTHENTICATED);
                 }
 
                 return true;
@@ -3331,10 +3340,19 @@ static int dnssec_validate_records(
                         }
                 }
 
+                /* https://datatracker.ietf.org/doc/html/rfc6840#section-5.2 */
+                if (result == DNSSEC_UNSUPPORTED_ALGORITHM) {
+                        r = dns_answer_move_by_key(validated, &t->answer, rr->key, 0, NULL);
+                        if (r < 0)
+                                return r;
+
+                        manager_dnssec_verdict(t->scope->manager, DNSSEC_INSECURE, rr->key);
+                        return 1;
+                }
+
                 if (IN_SET(result,
                            DNSSEC_MISSING_KEY,
-                           DNSSEC_SIGNATURE_EXPIRED,
-                           DNSSEC_UNSUPPORTED_ALGORITHM)) {
+                           DNSSEC_SIGNATURE_EXPIRED)) {
 
                         r = dns_transaction_dnskey_authenticated(t, rr);
                         if (r < 0 && r != -ENXIO)