/* * Copyright (c) 2018-2021, Andreas Kling * * SPDX-License-Identifier: BSD-2-Clause */ #include "LookupServer.h" #include "ClientConnection.h" #include "DNSPacket.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace LookupServer { static LookupServer* s_the; // NOTE: This is the TTL we return for the hostname or answers from /etc/hosts. static constexpr u32 s_static_ttl = 86400; LookupServer& LookupServer::the() { VERIFY(s_the); return *s_the; } LookupServer::LookupServer() { VERIFY(s_the == nullptr); s_the = this; auto config = Core::ConfigFile::get_for_system("LookupServer"); dbgln("Using network config file at {}", config->filename()); m_nameservers = config->read_entry("DNS", "Nameservers", "1.1.1.1,1.0.0.1").split(','); load_etc_hosts(); auto maybe_file_watcher = Core::FileWatcher::create(); // NOTE: If this happens during startup, something is very wrong. if (maybe_file_watcher.is_error()) { dbgln("Core::FileWatcher::create(): {}", maybe_file_watcher.error()); VERIFY_NOT_REACHED(); } m_file_watcher = maybe_file_watcher.release_value(); m_file_watcher->on_change = [this](auto&) { dbgln("Reloading '/etc/hosts' because it was changed."); load_etc_hosts(); }; auto result = m_file_watcher->add_watch("/etc/hosts", Core::FileWatcherEvent::Type::ContentModified | Core::FileWatcherEvent::Type::Deleted); // NOTE: If this happens during startup, something is very wrong. if (result.is_error()) { dbgln("Core::FileWatcher::add_watch(): {}", result.error()); VERIFY_NOT_REACHED(); } else if (!result.value()) { dbgln("Core::FileWatcher::add_watch(): {}", result.value()); VERIFY_NOT_REACHED(); } if (config->read_bool_entry("DNS", "EnableServer")) { m_dns_server = DNSServer::construct(this); // TODO: drop root privileges here. } m_mdns = MulticastDNS::construct(this); m_local_server = Core::LocalServer::construct(this); m_local_server->on_ready_to_accept = [this]() { auto socket = m_local_server->accept(); if (!socket) { dbgln("Failed to accept a client connection"); return; } static int s_next_client_id = 0; int client_id = ++s_next_client_id; IPC::new_client_connection(socket.release_nonnull(), client_id); }; bool ok = m_local_server->take_over_from_system_server(); VERIFY(ok); } void LookupServer::load_etc_hosts() { m_etc_hosts.clear(); auto add_answer = [this](const DNSName& name, DNSRecordType record_type, String data) { auto it = m_etc_hosts.find(name); if (it == m_etc_hosts.end()) { m_etc_hosts.set(name, {}); it = m_etc_hosts.find(name); } it->value.empend(name, record_type, DNSRecordClass::IN, s_static_ttl, data, false); }; auto file = Core::File::construct("/etc/hosts"); if (!file->open(Core::OpenMode::ReadOnly)) { dbgln("Failed to open '/etc/hosts'"); return; } u32 line_number = 0; while (!file->eof()) { auto original_line = file->read_line(1024); ++line_number; if (original_line.is_empty()) break; auto trimmed_line = original_line.view().trim_whitespace(); auto fields = trimmed_line.split_view('\t', false); if (fields.size() < 2) { dbgln("Failed to parse line {} from '/etc/hosts': '{}'", line_number, original_line); continue; } if (fields.size() > 2) dbgln("Line {} from '/etc/hosts' ('{}') has more than two parts, only the first two are used.", line_number, original_line); auto maybe_address = IPv4Address::from_string(fields[0]); if (!maybe_address.has_value()) { dbgln("Failed to parse line {} from '/etc/hosts': '{}'", line_number, original_line); continue; } auto raw_addr = maybe_address->to_in_addr_t(); DNSName name { fields[1] }; add_answer(name, DNSRecordType::A, String { (const char*)&raw_addr, sizeof(raw_addr) }); StringBuilder builder; builder.append(maybe_address->to_string_reversed()); builder.append(".in-addr.arpa"); add_answer(builder.to_string(), DNSRecordType::PTR, name.as_string()); } } static String get_hostname() { char buffer[HOST_NAME_MAX]; VERIFY(gethostname(buffer, sizeof(buffer)) == 0); return buffer; } Vector LookupServer::lookup(const DNSName& name, DNSRecordType record_type) { dbgln_if(LOOKUPSERVER_DEBUG, "Got request for '{}'", name.as_string()); Vector answers; auto add_answer = [&](const DNSAnswer& answer) { DNSAnswer answer_with_original_case { name, answer.type(), answer.class_code(), answer.ttl(), answer.record_data(), answer.mdns_cache_flush(), }; answers.append(answer_with_original_case); }; // First, try /etc/hosts. if (auto local_answers = m_etc_hosts.get(name); local_answers.has_value()) { for (auto& answer : local_answers.value()) { if (answer.type() == record_type) add_answer(answer); } if (!answers.is_empty()) return answers; } // Second, try the hostname. // NOTE: We don't cache the hostname since it could change during runtime. if (record_type == DNSRecordType::A && get_hostname() == name) { IPv4Address address = { 127, 0, 0, 1 }; auto raw_address = address.to_in_addr_t(); DNSAnswer answer { name, DNSRecordType::A, DNSRecordClass::IN, s_static_ttl, String { (const char*)&raw_address, sizeof(raw_address) }, false }; answers.append(move(answer)); return answers; } // Third, try our cache. if (auto cached_answers = m_lookup_cache.get(name); cached_answers.has_value()) { for (auto& answer : cached_answers.value()) { // TODO: Actually remove expired answers from the cache. if (answer.type() == record_type && !answer.has_expired()) { dbgln_if(LOOKUPSERVER_DEBUG, "Cache hit: {} -> {}", name.as_string(), answer.record_data()); add_answer(answer); } } if (!answers.is_empty()) return answers; } // Fourth, look up .local names using mDNS instead of DNS nameservers. if (name.as_string().ends_with(".local")) { answers = m_mdns->lookup(name, record_type); for (auto& answer : answers) put_in_cache(answer); return answers; } // Fifth, ask the upstream nameservers. for (auto& nameserver : m_nameservers) { dbgln_if(LOOKUPSERVER_DEBUG, "Doing lookup using nameserver '{}'", nameserver); bool did_get_response = false; int retries = 3; Vector upstream_answers; do { upstream_answers = lookup(name, nameserver, did_get_response, record_type); if (did_get_response) break; } while (--retries); if (!upstream_answers.is_empty()) { for (auto& answer : upstream_answers) add_answer(answer); break; } else { if (!did_get_response) dbgln("Never got a response from '{}', trying next nameserver", nameserver); else dbgln("Received response from '{}' but no result(s), trying next nameserver", nameserver); } } // Sixth, fail. if (answers.is_empty()) { dbgln("Tried all nameservers but never got a response :("); return {}; } return answers; } Vector LookupServer::lookup(const DNSName& name, const String& nameserver, bool& did_get_response, DNSRecordType record_type, ShouldRandomizeCase should_randomize_case) { DNSPacket request; request.set_is_query(); request.set_id(get_random_uniform(UINT16_MAX)); DNSName name_in_question = name; if (should_randomize_case == ShouldRandomizeCase::Yes) name_in_question.randomize_case(); request.add_question({ name_in_question, record_type, DNSRecordClass::IN, false }); auto buffer = request.to_byte_buffer(); auto udp_socket = Core::UDPSocket::construct(); udp_socket->set_blocking(true); struct timeval timeout { 1, 0 }; int rc = setsockopt(udp_socket->fd(), SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); if (rc < 0) { perror("setsockopt(SOL_SOCKET, SO_RCVTIMEO)"); return {}; } if (!udp_socket->connect(nameserver, 53)) return {}; if (!udp_socket->write(buffer)) return {}; u8 response_buffer[4096]; int nrecv = udp_socket->read(response_buffer, sizeof(response_buffer)); if (nrecv == 0) return {}; did_get_response = true; auto o_response = DNSPacket::from_raw_packet(response_buffer, nrecv); if (!o_response.has_value()) return {}; auto& response = o_response.value(); if (response.id() != request.id()) { dbgln("LookupServer: ID mismatch ({} vs {}) :(", response.id(), request.id()); return {}; } if (response.code() == DNSPacket::Code::REFUSED) { if (should_randomize_case == ShouldRandomizeCase::Yes) { // Retry with 0x20 case randomization turned off. return lookup(name, nameserver, did_get_response, record_type, ShouldRandomizeCase::No); } return {}; } if (response.question_count() != request.question_count()) { dbgln("LookupServer: Question count ({} vs {}) :(", response.question_count(), request.question_count()); return {}; } // Verify the questions in our request and in their response match exactly, including case. for (size_t i = 0; i < request.question_count(); ++i) { auto& request_question = request.questions()[i]; auto& response_question = response.questions()[i]; bool exact_match = request_question.class_code() == response_question.class_code() && request_question.record_type() == response_question.record_type() && request_question.name().as_string() == response_question.name().as_string(); if (!exact_match) { dbgln("Request and response questions do not match"); dbgln(" Request: name=_{}_, type={}, class={}", request_question.name().as_string(), response_question.record_type(), response_question.class_code()); dbgln(" Response: name=_{}_, type={}, class={}", response_question.name().as_string(), response_question.record_type(), response_question.class_code()); return {}; } } if (response.answer_count() < 1) { dbgln("LookupServer: No answers :("); return {}; } Vector answers; for (auto& answer : response.answers()) { put_in_cache(answer); if (answer.type() != record_type) continue; answers.append(answer); } return answers; } void LookupServer::put_in_cache(const DNSAnswer& answer) { if (answer.has_expired()) return; // Prevent the cache from growing too big. // TODO: Evict least used entries. if (m_lookup_cache.size() >= 256) m_lookup_cache.remove(m_lookup_cache.begin()); auto it = m_lookup_cache.find(answer.name()); if (it == m_lookup_cache.end()) m_lookup_cache.set(answer.name(), { answer }); else { if (answer.mdns_cache_flush()) { auto now = time(nullptr); it->value.remove_all_matching([&](DNSAnswer const& other_answer) { if (other_answer.type() != answer.type() || other_answer.class_code() != answer.class_code()) return false; if (other_answer.received_time() >= now - 1) return false; dbgln_if(LOOKUPSERVER_DEBUG, "Removing cache entry: {}", other_answer.name()); return true; }); } it->value.append(answer); } } }