mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 11:58:12 +00:00

Previously we'd only only send one DHCP request for network interfaces which were up when DHCPClient started. If that packet was lost we'd never send another request for those interfaces. Also, if an interface were to appear after DHCPClient started (not that that is possible at the moment) we wouldn't send requests for that interface either.
378 lines
13 KiB
C++
378 lines
13 KiB
C++
/*
|
|
* Copyright (c) 2020, the SerenityOS developers.
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include "DHCPv4Client.h"
|
|
#include <AK/ByteBuffer.h>
|
|
#include <AK/Debug.h>
|
|
#include <AK/Endian.h>
|
|
#include <AK/Function.h>
|
|
#include <AK/JsonArray.h>
|
|
#include <AK/JsonObject.h>
|
|
#include <AK/JsonParser.h>
|
|
#include <AK/Random.h>
|
|
#include <LibCore/File.h>
|
|
#include <LibCore/SocketAddress.h>
|
|
#include <LibCore/Timer.h>
|
|
#include <stdio.h>
|
|
|
|
static u8 mac_part(const Vector<String>& parts, size_t index)
|
|
{
|
|
auto result = AK::StringUtils::convert_to_uint_from_hex(parts.at(index));
|
|
VERIFY(result.has_value());
|
|
return result.value();
|
|
}
|
|
|
|
static MACAddress mac_from_string(const String& str)
|
|
{
|
|
auto chunks = str.split(':');
|
|
VERIFY(chunks.size() == 6); // should we...worry about this?
|
|
return {
|
|
mac_part(chunks, 0), mac_part(chunks, 1), mac_part(chunks, 2),
|
|
mac_part(chunks, 3), mac_part(chunks, 4), mac_part(chunks, 5)
|
|
};
|
|
}
|
|
|
|
static bool send(const InterfaceDescriptor& iface, const DHCPv4Packet& packet, Core::Object*)
|
|
{
|
|
int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
|
if (fd < 0) {
|
|
dbgln("ERROR: socket :: {}", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
if (setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, iface.m_ifname.characters(), IFNAMSIZ) < 0) {
|
|
dbgln("ERROR: setsockopt(SO_BINDTODEVICE) :: {}", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
sockaddr_in dst;
|
|
memset(&dst, 0, sizeof(dst));
|
|
dst.sin_family = AF_INET;
|
|
dst.sin_port = htons(67);
|
|
dst.sin_addr.s_addr = IPv4Address { 255, 255, 255, 255 }.to_u32();
|
|
memset(&dst.sin_zero, 0, sizeof(dst.sin_zero));
|
|
|
|
dbgln_if(DHCPV4CLIENT_DEBUG, "sendto({} bound to {}, ..., {} at {}) = ...?", fd, iface.m_ifname, dst.sin_addr.s_addr, dst.sin_port);
|
|
auto rc = sendto(fd, &packet, sizeof(packet), 0, (sockaddr*)&dst, sizeof(dst));
|
|
dbgln_if(DHCPV4CLIENT_DEBUG, "sendto({}) = {}", fd, rc);
|
|
if (rc < 0) {
|
|
dbgln("sendto failed with {}", strerror(errno));
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
static void set_params(const InterfaceDescriptor& iface, const IPv4Address& ipv4_addr, const IPv4Address& netmask, const IPv4Address& gateway)
|
|
{
|
|
int fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
|
if (fd < 0) {
|
|
dbgln("ERROR: socket :: {}", strerror(errno));
|
|
return;
|
|
}
|
|
|
|
struct ifreq ifr;
|
|
memset(&ifr, 0, sizeof(ifr));
|
|
|
|
bool fits = iface.m_ifname.copy_characters_to_buffer(ifr.ifr_name, IFNAMSIZ);
|
|
if (!fits) {
|
|
dbgln("Interface name doesn't fit into IFNAMSIZ!");
|
|
return;
|
|
}
|
|
|
|
// set the IP address
|
|
ifr.ifr_addr.sa_family = AF_INET;
|
|
((sockaddr_in&)ifr.ifr_addr).sin_addr.s_addr = ipv4_addr.to_in_addr_t();
|
|
|
|
if (ioctl(fd, SIOCSIFADDR, &ifr) < 0) {
|
|
dbgln("ERROR: ioctl(SIOCSIFADDR) :: {}", strerror(errno));
|
|
}
|
|
|
|
// set the network mask
|
|
((sockaddr_in&)ifr.ifr_netmask).sin_addr.s_addr = netmask.to_in_addr_t();
|
|
|
|
if (ioctl(fd, SIOCSIFNETMASK, &ifr) < 0) {
|
|
dbgln("ERROR: ioctl(SIOCSIFNETMASK) :: {}", strerror(errno));
|
|
}
|
|
|
|
// set the default gateway
|
|
struct rtentry rt;
|
|
memset(&rt, 0, sizeof(rt));
|
|
|
|
rt.rt_dev = const_cast<char*>(iface.m_ifname.characters());
|
|
rt.rt_gateway.sa_family = AF_INET;
|
|
((sockaddr_in&)rt.rt_gateway).sin_addr.s_addr = gateway.to_in_addr_t();
|
|
rt.rt_flags = RTF_UP | RTF_GATEWAY;
|
|
|
|
if (ioctl(fd, SIOCADDRT, &rt) < 0) {
|
|
dbgln("Error: ioctl(SIOCADDRT) :: {}", strerror(errno));
|
|
}
|
|
}
|
|
|
|
DHCPv4Client::DHCPv4Client()
|
|
{
|
|
m_server = Core::UDPServer::construct(this);
|
|
m_server->on_ready_to_receive = [this] {
|
|
auto buffer = m_server->receive(sizeof(DHCPv4Packet));
|
|
dbgln_if(DHCPV4CLIENT_DEBUG, "Received {} bytes", buffer.size());
|
|
if (buffer.size() < sizeof(DHCPv4Packet) - DHCPV4_OPTION_FIELD_MAX_LENGTH + 1 || buffer.size() > sizeof(DHCPv4Packet)) {
|
|
dbgln("we expected {}-{} bytes, this is a bad packet", sizeof(DHCPv4Packet) - DHCPV4_OPTION_FIELD_MAX_LENGTH + 1, sizeof(DHCPv4Packet));
|
|
return;
|
|
}
|
|
auto& packet = *(DHCPv4Packet*)buffer.data();
|
|
process_incoming(packet);
|
|
};
|
|
|
|
if (!m_server->bind({}, 68)) {
|
|
dbgln("The server we just created somehow came already bound, refusing to continue");
|
|
VERIFY_NOT_REACHED();
|
|
}
|
|
|
|
m_check_timer = Core::Timer::create_repeating(
|
|
1000, [this] { try_discover_ifs(); }, this);
|
|
|
|
m_check_timer->start();
|
|
|
|
try_discover_ifs();
|
|
}
|
|
|
|
void DHCPv4Client::try_discover_ifs()
|
|
{
|
|
auto ifs_result = get_discoverable_interfaces();
|
|
if (ifs_result.is_error())
|
|
return;
|
|
|
|
bool sent_discover_request = false;
|
|
Interfaces& ifs = ifs_result.value();
|
|
for (auto& iface : ifs.ready) {
|
|
if (iface.m_current_ip_address != IPv4Address { 0, 0, 0, 0 })
|
|
continue;
|
|
|
|
dhcp_discover(iface);
|
|
sent_discover_request = true;
|
|
}
|
|
|
|
if (sent_discover_request) {
|
|
auto current_interval = m_check_timer->interval();
|
|
if (current_interval < m_max_timer_backoff_interval)
|
|
current_interval *= 1.9f;
|
|
m_check_timer->set_interval(current_interval);
|
|
} else {
|
|
m_check_timer->set_interval(1000);
|
|
}
|
|
}
|
|
|
|
Result<DHCPv4Client::Interfaces, String> DHCPv4Client::get_discoverable_interfaces()
|
|
{
|
|
auto file = Core::File::construct("/proc/net/adapters");
|
|
if (!file->open(Core::OpenMode::ReadOnly)) {
|
|
dbgln("Error: Failed to open /proc/net/adapters: {}", file->error_string());
|
|
return String { file->error_string() };
|
|
}
|
|
|
|
auto file_contents = file->read_all();
|
|
auto json = JsonValue::from_string(file_contents);
|
|
|
|
if (!json.has_value() || !json.value().is_array()) {
|
|
dbgln("Error: No network adapters available");
|
|
return String { "No network adapters available" };
|
|
}
|
|
|
|
Vector<InterfaceDescriptor> ifnames_to_immediately_discover, ifnames_to_attempt_later;
|
|
json.value().as_array().for_each([&ifnames_to_immediately_discover, &ifnames_to_attempt_later](auto& value) {
|
|
auto if_object = value.as_object();
|
|
|
|
if (if_object.get("class_name").to_string() == "LoopbackAdapter")
|
|
return;
|
|
|
|
auto name = if_object.get("name").to_string();
|
|
auto mac = if_object.get("mac_address").to_string();
|
|
auto is_up = if_object.get("link_up").to_bool();
|
|
auto ipv4_addr_maybe = IPv4Address::from_string(if_object.get("ipv4_address").to_string());
|
|
auto ipv4_addr = ipv4_addr_maybe.has_value() ? ipv4_addr_maybe.value() : IPv4Address { 0, 0, 0, 0 };
|
|
if (is_up) {
|
|
dbgln_if(DHCPV4_DEBUG, "Found adapter '{}' with mac {}, and it was up!", name, mac);
|
|
ifnames_to_immediately_discover.empend(name, mac_from_string(mac), ipv4_addr);
|
|
} else {
|
|
dbgln_if(DHCPV4_DEBUG, "Found adapter '{}' with mac {}, but it was down", name, mac);
|
|
ifnames_to_attempt_later.empend(name, mac_from_string(mac), ipv4_addr);
|
|
}
|
|
});
|
|
|
|
return Interfaces {
|
|
move(ifnames_to_immediately_discover),
|
|
move(ifnames_to_attempt_later)
|
|
};
|
|
}
|
|
|
|
DHCPv4Client::~DHCPv4Client()
|
|
{
|
|
}
|
|
|
|
void DHCPv4Client::handle_offer(const DHCPv4Packet& packet, const ParsedDHCPv4Options& options)
|
|
{
|
|
dbgln("We were offered {} for {}", packet.yiaddr().to_string(), options.get<u32>(DHCPOption::IPAddressLeaseTime).value_or(0));
|
|
auto* transaction = const_cast<DHCPv4Transaction*>(m_ongoing_transactions.get(packet.xid()).value_or(nullptr));
|
|
if (!transaction) {
|
|
dbgln("we're not looking for {}", packet.xid());
|
|
return;
|
|
}
|
|
if (transaction->has_ip)
|
|
return;
|
|
if (transaction->accepted_offer) {
|
|
// we've accepted someone's offer, but they haven't given us an ack
|
|
// TODO: maybe record this offer?
|
|
return;
|
|
}
|
|
// TAKE IT...
|
|
transaction->offered_lease_time = options.get<u32>(DHCPOption::IPAddressLeaseTime).value();
|
|
dhcp_request(*transaction, packet);
|
|
}
|
|
|
|
void DHCPv4Client::handle_ack(const DHCPv4Packet& packet, const ParsedDHCPv4Options& options)
|
|
{
|
|
if constexpr (DHCPV4CLIENT_DEBUG) {
|
|
dbgln("The DHCP server handed us {}", packet.yiaddr().to_string());
|
|
dbgln("Here are the options: {}", options.to_string());
|
|
}
|
|
|
|
auto* transaction = const_cast<DHCPv4Transaction*>(m_ongoing_transactions.get(packet.xid()).value_or(nullptr));
|
|
if (!transaction) {
|
|
dbgln("we're not looking for {}", packet.xid());
|
|
return;
|
|
}
|
|
transaction->has_ip = true;
|
|
auto& interface = transaction->interface;
|
|
auto new_ip = packet.yiaddr();
|
|
interface.m_current_ip_address = new_ip;
|
|
auto lease_time = AK::convert_between_host_and_network_endian(options.get<u32>(DHCPOption::IPAddressLeaseTime).value_or(transaction->offered_lease_time));
|
|
// set a timer for the duration of the lease, we shall renew if needed
|
|
Core::Timer::create_single_shot(
|
|
lease_time * 1000,
|
|
[this, transaction, interface = InterfaceDescriptor { interface }] {
|
|
transaction->accepted_offer = false;
|
|
transaction->has_ip = false;
|
|
dhcp_discover(interface);
|
|
},
|
|
this);
|
|
set_params(transaction->interface, new_ip, options.get<IPv4Address>(DHCPOption::SubnetMask).value(), options.get_many<IPv4Address>(DHCPOption::Router, 1).first());
|
|
}
|
|
|
|
void DHCPv4Client::handle_nak(const DHCPv4Packet& packet, const ParsedDHCPv4Options& options)
|
|
{
|
|
dbgln("The DHCP server told us to go chase our own tail about {}", packet.yiaddr().to_string());
|
|
dbgln("Here are the options: {}", options.to_string());
|
|
// make another request a bit later :shrug:
|
|
auto* transaction = const_cast<DHCPv4Transaction*>(m_ongoing_transactions.get(packet.xid()).value_or(nullptr));
|
|
if (!transaction) {
|
|
dbgln("we're not looking for {}", packet.xid());
|
|
return;
|
|
}
|
|
transaction->accepted_offer = false;
|
|
transaction->has_ip = false;
|
|
auto& iface = transaction->interface;
|
|
Core::Timer::create_single_shot(
|
|
10000,
|
|
[this, iface = InterfaceDescriptor { iface }] {
|
|
dhcp_discover(iface);
|
|
},
|
|
this);
|
|
}
|
|
|
|
void DHCPv4Client::process_incoming(const DHCPv4Packet& packet)
|
|
{
|
|
auto options = packet.parse_options();
|
|
|
|
dbgln_if(DHCPV4CLIENT_DEBUG, "Here are the options: {}", options.to_string());
|
|
|
|
auto value_or_error = options.get<DHCPMessageType>(DHCPOption::DHCPMessageType);
|
|
if (!value_or_error.has_value())
|
|
return;
|
|
|
|
auto value = value_or_error.value();
|
|
switch (value) {
|
|
case DHCPMessageType::DHCPOffer:
|
|
handle_offer(packet, options);
|
|
break;
|
|
case DHCPMessageType::DHCPAck:
|
|
handle_ack(packet, options);
|
|
break;
|
|
case DHCPMessageType::DHCPNak:
|
|
handle_nak(packet, options);
|
|
break;
|
|
case DHCPMessageType::DHCPDiscover:
|
|
case DHCPMessageType::DHCPRequest:
|
|
case DHCPMessageType::DHCPRelease:
|
|
// These are not for us
|
|
// we're just getting them because there are other people on our subnet
|
|
// broadcasting stuff
|
|
break;
|
|
case DHCPMessageType::DHCPDecline:
|
|
default:
|
|
dbgln("I dunno what to do with this {}", (u8)value);
|
|
VERIFY_NOT_REACHED();
|
|
break;
|
|
}
|
|
}
|
|
|
|
void DHCPv4Client::dhcp_discover(const InterfaceDescriptor& iface)
|
|
{
|
|
auto transaction_id = get_random<u32>();
|
|
|
|
if constexpr (DHCPV4CLIENT_DEBUG) {
|
|
dbgln("Trying to lease an IP for {} with ID {}", iface.m_ifname, transaction_id);
|
|
if (!iface.m_current_ip_address.is_zero())
|
|
dbgln("going to request the server to hand us {}", iface.m_current_ip_address.to_string());
|
|
}
|
|
|
|
DHCPv4PacketBuilder builder;
|
|
|
|
DHCPv4Packet& packet = builder.peek();
|
|
packet.set_op(DHCPv4Op::BootRequest);
|
|
packet.set_htype(1); // 10mb ethernet
|
|
packet.set_hlen(sizeof(MACAddress));
|
|
packet.set_xid(transaction_id);
|
|
packet.set_flags(DHCPv4Flags::Broadcast);
|
|
packet.ciaddr() = iface.m_current_ip_address;
|
|
packet.set_chaddr(iface.m_mac_address);
|
|
packet.set_secs(65535); // we lie
|
|
|
|
// set packet options
|
|
builder.set_message_type(DHCPMessageType::DHCPDiscover);
|
|
auto& dhcp_packet = builder.build();
|
|
|
|
// broadcast the discover request
|
|
if (!send(iface, dhcp_packet, this))
|
|
return;
|
|
m_ongoing_transactions.set(transaction_id, make<DHCPv4Transaction>(iface));
|
|
}
|
|
|
|
void DHCPv4Client::dhcp_request(DHCPv4Transaction& transaction, const DHCPv4Packet& offer)
|
|
{
|
|
auto& iface = transaction.interface;
|
|
dbgln("Leasing the IP {} for adapter {}", offer.yiaddr().to_string(), iface.m_ifname);
|
|
DHCPv4PacketBuilder builder;
|
|
|
|
DHCPv4Packet& packet = builder.peek();
|
|
packet.set_op(DHCPv4Op::BootRequest);
|
|
packet.ciaddr() = iface.m_current_ip_address;
|
|
packet.set_htype(1); // 10mb ethernet
|
|
packet.set_hlen(sizeof(MACAddress));
|
|
packet.set_xid(offer.xid());
|
|
packet.set_flags(DHCPv4Flags::Broadcast);
|
|
packet.set_chaddr(iface.m_mac_address);
|
|
packet.set_secs(65535); // we lie
|
|
|
|
// set packet options
|
|
builder.set_message_type(DHCPMessageType::DHCPRequest);
|
|
builder.add_option(DHCPOption::RequestedIPAddress, sizeof(IPv4Address), &offer.yiaddr());
|
|
auto& dhcp_packet = builder.build();
|
|
|
|
// broadcast the "request" request
|
|
if (!send(iface, dhcp_packet, this))
|
|
return;
|
|
transaction.accepted_offer = true;
|
|
}
|