| /*-*- Mode: C; c-basic-offset: 8; indent-tabs-mode: nil -*-*/ |
| |
| /*** |
| This file is part of systemd. |
| |
| Copyright (C) 2013 Intel Corporation. All rights reserved. |
| Copyright (C) 2014 Tom Gundersen |
| |
| 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 <http://www.gnu.org/licenses/>. |
| ***/ |
| |
| #include <sys/ioctl.h> |
| |
| #include "siphash24.h" |
| |
| #include "sd-dhcp-server.h" |
| #include "dhcp-server-internal.h" |
| #include "dhcp-internal.h" |
| |
| #define DHCP_DEFAULT_LEASE_TIME 3600 /* one hour */ |
| |
| int sd_dhcp_server_set_lease_pool(sd_dhcp_server *server, |
| struct in_addr *address, |
| size_t size) { |
| assert_return(server, -EINVAL); |
| assert_return(address, -EINVAL); |
| assert_return(address->s_addr, -EINVAL); |
| assert_return(size, -EINVAL); |
| assert_return(server->pool_start == htobe32(INADDR_ANY), -EBUSY); |
| assert_return(!server->pool_size, -EBUSY); |
| assert_return(!server->bound_leases, -EBUSY); |
| |
| server->bound_leases = new0(DHCPLease*, size); |
| if (!server->bound_leases) |
| return -ENOMEM; |
| |
| server->pool_start = address->s_addr; |
| server->pool_size = size; |
| |
| return 0; |
| } |
| |
| int sd_dhcp_server_set_address(sd_dhcp_server *server, struct in_addr *address, |
| unsigned char prefixlen) { |
| assert_return(server, -EINVAL); |
| assert_return(address, -EINVAL); |
| assert_return(address->s_addr, -EINVAL); |
| assert_return(prefixlen <= 32, -ERANGE); |
| assert_return(server->address == htobe32(INADDR_ANY), -EBUSY); |
| assert_return(server->netmask == htobe32(INADDR_ANY), -EBUSY); |
| |
| server->address = address->s_addr; |
| server->netmask = htobe32(0xfffffffflu << (32 - prefixlen)); |
| |
| return 0; |
| } |
| |
| bool sd_dhcp_server_is_running(sd_dhcp_server *server) { |
| assert_return(server, -EINVAL); |
| |
| return !!server->receive_message; |
| } |
| |
| sd_dhcp_server *sd_dhcp_server_ref(sd_dhcp_server *server) { |
| if (server) |
| assert_se(REFCNT_INC(server->n_ref) >= 2); |
| |
| return server; |
| } |
| |
| unsigned long client_id_hash_func(const void *p, |
| const uint8_t hash_key[HASH_KEY_SIZE]) { |
| uint64_t u; |
| const DHCPClientId *id = p; |
| |
| assert(id); |
| assert(id->length); |
| assert(id->data); |
| |
| siphash24((uint8_t*) &u, id->data, id->length, hash_key); |
| |
| return (unsigned long) u; |
| } |
| |
| int client_id_compare_func(const void *_a, const void *_b) { |
| const DHCPClientId *a, *b; |
| |
| a = _a; |
| b = _b; |
| |
| assert(!a->length || a->data); |
| assert(!b->length || b->data); |
| |
| if (a->length != b->length) |
| return a->length < b->length ? -1 : 1; |
| |
| return memcmp(a->data, b->data, a->length); |
| } |
| |
| static const struct hash_ops client_id_hash_ops = { |
| .hash = client_id_hash_func, |
| .compare = client_id_compare_func |
| }; |
| |
| static void dhcp_lease_free(DHCPLease *lease) { |
| if (!lease) |
| return; |
| |
| free(lease->client_id.data); |
| free(lease); |
| } |
| |
| sd_dhcp_server *sd_dhcp_server_unref(sd_dhcp_server *server) { |
| DHCPLease *lease; |
| |
| if (!server) |
| return NULL; |
| |
| if (REFCNT_DEC(server->n_ref) > 0) |
| return NULL; |
| |
| log_dhcp_server(server, "UNREF"); |
| |
| sd_dhcp_server_stop(server); |
| |
| sd_event_unref(server->event); |
| |
| while ((lease = hashmap_steal_first(server->leases_by_client_id))) |
| dhcp_lease_free(lease); |
| hashmap_free(server->leases_by_client_id); |
| |
| free(server->bound_leases); |
| free(server); |
| |
| return NULL; |
| } |
| |
| int sd_dhcp_server_new(sd_dhcp_server **ret, int ifindex) { |
| _cleanup_dhcp_server_unref_ sd_dhcp_server *server = NULL; |
| |
| assert_return(ret, -EINVAL); |
| assert_return(ifindex > 0, -EINVAL); |
| |
| server = new0(sd_dhcp_server, 1); |
| if (!server) |
| return -ENOMEM; |
| |
| server->n_ref = REFCNT_INIT; |
| server->fd_raw = -1; |
| server->fd = -1; |
| server->address = htobe32(INADDR_ANY); |
| server->netmask = htobe32(INADDR_ANY); |
| server->index = ifindex; |
| server->leases_by_client_id = hashmap_new(&client_id_hash_ops); |
| |
| *ret = server; |
| server = NULL; |
| |
| return 0; |
| } |
| |
| int sd_dhcp_server_attach_event(sd_dhcp_server *server, sd_event *event, |
| int priority) { |
| int r; |
| |
| assert_return(server, -EINVAL); |
| assert_return(!server->event, -EBUSY); |
| |
| if (event) |
| server->event = sd_event_ref(event); |
| else { |
| r = sd_event_default(&server->event); |
| if (r < 0) |
| return r; |
| } |
| |
| server->event_priority = priority; |
| |
| return 0; |
| } |
| |
| int sd_dhcp_server_detach_event(sd_dhcp_server *server) { |
| assert_return(server, -EINVAL); |
| |
| server->event = sd_event_unref(server->event); |
| |
| return 0; |
| } |
| |
| sd_event *sd_dhcp_server_get_event(sd_dhcp_server *server) { |
| assert_return(server, NULL); |
| |
| return server->event; |
| } |
| |
| int sd_dhcp_server_stop(sd_dhcp_server *server) { |
| assert_return(server, -EINVAL); |
| |
| server->receive_message = |
| sd_event_source_unref(server->receive_message); |
| |
| server->fd_raw = safe_close(server->fd_raw); |
| server->fd = safe_close(server->fd); |
| |
| log_dhcp_server(server, "STOPPED"); |
| |
| return 0; |
| } |
| |
| static int dhcp_server_send_unicast_raw(sd_dhcp_server *server, |
| DHCPPacket *packet, size_t len) { |
| union sockaddr_union link = { |
| .ll.sll_family = AF_PACKET, |
| .ll.sll_protocol = htons(ETH_P_IP), |
| .ll.sll_ifindex = server->index, |
| .ll.sll_halen = ETH_ALEN, |
| }; |
| int r; |
| |
| assert(server); |
| assert(server->index > 0); |
| assert(server->address); |
| assert(packet); |
| assert(len > sizeof(DHCPPacket)); |
| |
| memcpy(&link.ll.sll_addr, &packet->dhcp.chaddr, ETH_ALEN); |
| |
| dhcp_packet_append_ip_headers(packet, server->address, DHCP_PORT_SERVER, |
| packet->dhcp.yiaddr, |
| DHCP_PORT_CLIENT, len); |
| |
| r = dhcp_network_send_raw_socket(server->fd_raw, &link, packet, len); |
| if (r < 0) |
| return r; |
| |
| return 0; |
| } |
| |
| static int dhcp_server_send_udp(sd_dhcp_server *server, be32_t destination, |
| DHCPMessage *message, size_t len) { |
| union sockaddr_union dest = { |
| .in.sin_family = AF_INET, |
| .in.sin_port = htobe16(DHCP_PORT_CLIENT), |
| .in.sin_addr.s_addr = destination, |
| }; |
| struct iovec iov = { |
| .iov_base = message, |
| .iov_len = len, |
| }; |
| uint8_t cmsgbuf[CMSG_LEN(sizeof(struct in_pktinfo))] = {}; |
| struct msghdr msg = { |
| .msg_name = &dest, |
| .msg_namelen = sizeof(dest.in), |
| .msg_iov = &iov, |
| .msg_iovlen = 1, |
| .msg_control = cmsgbuf, |
| .msg_controllen = sizeof(cmsgbuf), |
| }; |
| struct cmsghdr *cmsg; |
| struct in_pktinfo *pktinfo; |
| int r; |
| |
| assert(server); |
| assert(server->fd > 0); |
| assert(message); |
| assert(len > sizeof(DHCPMessage)); |
| |
| cmsg = CMSG_FIRSTHDR(&msg); |
| assert(cmsg); |
| |
| cmsg->cmsg_level = IPPROTO_IP; |
| cmsg->cmsg_type = IP_PKTINFO; |
| cmsg->cmsg_len = CMSG_LEN(sizeof(struct in_pktinfo)); |
| |
| /* we attach source interface and address info to the message |
| rather than binding the socket. This will be mostly useful |
| when we gain support for arbitrary number of server addresses |
| */ |
| pktinfo = (struct in_pktinfo*) CMSG_DATA(cmsg); |
| assert(pktinfo); |
| |
| pktinfo->ipi_ifindex = server->index; |
| pktinfo->ipi_spec_dst.s_addr = server->address; |
| |
| r = sendmsg(server->fd, &msg, 0); |
| if (r < 0) |
| return -errno; |
| |
| return 0; |
| } |
| |
| static bool requested_broadcast(DHCPRequest *req) { |
| assert(req); |
| |
| return req->message->flags & htobe16(0x8000); |
| } |
| |
| int dhcp_server_send_packet(sd_dhcp_server *server, |
| DHCPRequest *req, DHCPPacket *packet, |
| int type, size_t optoffset) { |
| be32_t destination = INADDR_ANY; |
| int r; |
| |
| assert(server); |
| assert(req); |
| assert(req->max_optlen); |
| assert(optoffset <= req->max_optlen); |
| assert(packet); |
| |
| r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0, |
| DHCP_OPTION_SERVER_IDENTIFIER, |
| 4, &server->address); |
| if (r < 0) |
| return r; |
| |
| r = dhcp_option_append(&packet->dhcp, req->max_optlen, &optoffset, 0, |
| DHCP_OPTION_END, 0, NULL); |
| if (r < 0) |
| return r; |
| |
| /* RFC 2131 Section 4.1 |
| |
| If the ’giaddr’ field in a DHCP message from a client is non-zero, |
| the server sends any return messages to the ’DHCP server’ port on the |
| BOOTP relay agent whose address appears in ’giaddr’. If the ’giaddr’ |
| field is zero and the ’ciaddr’ field is nonzero, then the server |
| unicasts DHCPOFFER and DHCPACK messages to the address in ’ciaddr’. |
| If ’giaddr’ is zero and ’ciaddr’ is zero, and the broadcast bit is |
| set, then the server broadcasts DHCPOFFER and DHCPACK messages to |
| 0xffffffff. If the broadcast bit is not set and ’giaddr’ is zero and |
| ’ciaddr’ is zero, then the server unicasts DHCPOFFER and DHCPACK |
| messages to the client’s hardware address and ’yiaddr’ address. In |
| all cases, when ’giaddr’ is zero, the server broadcasts any DHCPNAK |
| messages to 0xffffffff. |
| |
| Section 4.3.2 |
| |
| If ’giaddr’ is set in the DHCPREQUEST message, the client is on a |
| different subnet. The server MUST set the broadcast bit in the |
| DHCPNAK, so that the relay agent will broadcast the DHCPNAK to the |
| client, because the client may not have a correct network address |
| or subnet mask, and the client may not be answering ARP requests. |
| */ |
| if (req->message->giaddr) { |
| destination = req->message->giaddr; |
| if (type == DHCP_NAK) |
| packet->dhcp.flags = htobe16(0x8000); |
| } else if (req->message->ciaddr && type != DHCP_NAK) |
| destination = req->message->ciaddr; |
| |
| if (destination != INADDR_ANY) |
| return dhcp_server_send_udp(server, destination, &packet->dhcp, |
| sizeof(DHCPMessage) + optoffset); |
| else if (requested_broadcast(req) || type == DHCP_NAK) |
| return dhcp_server_send_udp(server, INADDR_BROADCAST, |
| &packet->dhcp, |
| sizeof(DHCPMessage) + optoffset); |
| else |
| /* we cannot send UDP packet to specific MAC address when the |
| address is not yet configured, so must fall back to raw |
| packets */ |
| return dhcp_server_send_unicast_raw(server, packet, |
| sizeof(DHCPPacket) + optoffset); |
| } |
| |
| static int server_message_init(sd_dhcp_server *server, DHCPPacket **ret, |
| uint8_t type, size_t *_optoffset, |
| DHCPRequest *req) { |
| _cleanup_free_ DHCPPacket *packet = NULL; |
| size_t optoffset = 0; |
| int r; |
| |
| assert(server); |
| assert(ret); |
| assert(_optoffset); |
| assert(IN_SET(type, DHCP_OFFER, DHCP_ACK, DHCP_NAK)); |
| |
| packet = malloc0(sizeof(DHCPPacket) + req->max_optlen); |
| if (!packet) |
| return -ENOMEM; |
| |
| r = dhcp_message_init(&packet->dhcp, BOOTREPLY, |
| be32toh(req->message->xid), type, ARPHRD_ETHER, |
| req->max_optlen, &optoffset); |
| if (r < 0) |
| return r; |
| |
| packet->dhcp.flags = req->message->flags; |
| packet->dhcp.giaddr = req->message->giaddr; |
| memcpy(&packet->dhcp.chaddr, &req->message->chaddr, ETH_ALEN); |
| |
| *_optoffset = optoffset; |
| *ret = packet; |
| packet = NULL; |
| |
| return 0; |
| } |
| |
| static int server_send_offer(sd_dhcp_server *server, DHCPRequest *req, |
| be32_t address) { |
| _cleanup_free_ DHCPPacket *packet = NULL; |
| size_t offset; |
| be32_t lease_time; |
| int r; |
| |
| r = server_message_init(server, &packet, DHCP_OFFER, &offset, req); |
| if (r < 0) |
| return r; |
| |
| packet->dhcp.yiaddr = address; |
| |
| lease_time = htobe32(req->lifetime); |
| r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, |
| DHCP_OPTION_IP_ADDRESS_LEASE_TIME, 4, |
| &lease_time); |
| if (r < 0) |
| return r; |
| |
| r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, |
| DHCP_OPTION_SUBNET_MASK, 4, &server->netmask); |
| if (r < 0) |
| return r; |
| |
| r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, |
| DHCP_OPTION_ROUTER, 4, &server->address); |
| if (r < 0) |
| return r; |
| |
| r = dhcp_server_send_packet(server, req, packet, DHCP_OFFER, offset); |
| if (r < 0) |
| return r; |
| |
| return 0; |
| } |
| |
| static int server_send_ack(sd_dhcp_server *server, DHCPRequest *req, |
| be32_t address) { |
| _cleanup_free_ DHCPPacket *packet = NULL; |
| size_t offset; |
| be32_t lease_time; |
| int r; |
| |
| r = server_message_init(server, &packet, DHCP_ACK, &offset, req); |
| if (r < 0) |
| return r; |
| |
| packet->dhcp.yiaddr = address; |
| |
| lease_time = htobe32(req->lifetime); |
| r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, |
| DHCP_OPTION_IP_ADDRESS_LEASE_TIME, 4, |
| &lease_time); |
| if (r < 0) |
| return r; |
| |
| r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, |
| DHCP_OPTION_SUBNET_MASK, 4, &server->netmask); |
| if (r < 0) |
| return r; |
| |
| r = dhcp_option_append(&packet->dhcp, req->max_optlen, &offset, 0, |
| DHCP_OPTION_ROUTER, 4, &server->address); |
| if (r < 0) |
| return r; |
| |
| r = dhcp_server_send_packet(server, req, packet, DHCP_ACK, offset); |
| if (r < 0) |
| return r; |
| |
| return 0; |
| } |
| |
| static int server_send_nak(sd_dhcp_server *server, DHCPRequest *req) { |
| _cleanup_free_ DHCPPacket *packet = NULL; |
| size_t offset; |
| int r; |
| |
| r = server_message_init(server, &packet, DHCP_NAK, &offset, req); |
| if (r < 0) |
| return r; |
| |
| r = dhcp_server_send_packet(server, req, packet, DHCP_NAK, offset); |
| if (r < 0) |
| return r; |
| |
| return 0; |
| } |
| |
| static int server_send_forcerenew(sd_dhcp_server *server, be32_t address, |
| be32_t gateway, uint8_t chaddr[]) { |
| _cleanup_free_ DHCPPacket *packet = NULL; |
| size_t optoffset = 0; |
| int r; |
| |
| assert(server); |
| assert(address != INADDR_ANY); |
| assert(chaddr); |
| |
| packet = malloc0(sizeof(DHCPPacket) + DHCP_MIN_OPTIONS_SIZE); |
| if (!packet) |
| return -ENOMEM; |
| |
| r = dhcp_message_init(&packet->dhcp, BOOTREPLY, 0, |
| DHCP_FORCERENEW, ARPHRD_ETHER, |
| DHCP_MIN_OPTIONS_SIZE, &optoffset); |
| if (r < 0) |
| return r; |
| |
| r = dhcp_option_append(&packet->dhcp, DHCP_MIN_OPTIONS_SIZE, |
| &optoffset, 0, DHCP_OPTION_END, 0, NULL); |
| if (r < 0) |
| return r; |
| |
| memcpy(&packet->dhcp.chaddr, chaddr, ETH_ALEN); |
| |
| r = dhcp_server_send_udp(server, address, &packet->dhcp, |
| sizeof(DHCPMessage) + optoffset); |
| if (r < 0) |
| return r; |
| |
| return 0; |
| } |
| |
| static int parse_request(uint8_t code, uint8_t len, const uint8_t *option, |
| void *user_data) { |
| DHCPRequest *req = user_data; |
| |
| assert(req); |
| |
| switch(code) { |
| case DHCP_OPTION_IP_ADDRESS_LEASE_TIME: |
| if (len == 4) |
| req->lifetime = be32toh(*(be32_t*)option); |
| |
| break; |
| case DHCP_OPTION_REQUESTED_IP_ADDRESS: |
| if (len == 4) |
| req->requested_ip = *(be32_t*)option; |
| |
| break; |
| case DHCP_OPTION_SERVER_IDENTIFIER: |
| if (len == 4) |
| req->server_id = *(be32_t*)option; |
| |
| break; |
| case DHCP_OPTION_CLIENT_IDENTIFIER: |
| if (len >= 2) { |
| uint8_t *data; |
| |
| data = memdup(option, len); |
| if (!data) |
| return -ENOMEM; |
| |
| free(req->client_id.data); |
| req->client_id.data = data; |
| req->client_id.length = len; |
| } |
| |
| break; |
| case DHCP_OPTION_MAXIMUM_MESSAGE_SIZE: |
| if (len == 2) |
| req->max_optlen = be16toh(*(be16_t*)option) - |
| - sizeof(DHCPPacket); |
| |
| break; |
| } |
| |
| return 0; |
| } |
| |
| static void dhcp_request_free(DHCPRequest *req) { |
| if (!req) |
| return; |
| |
| free(req->client_id.data); |
| free(req); |
| } |
| |
| DEFINE_TRIVIAL_CLEANUP_FUNC(DHCPRequest*, dhcp_request_free); |
| #define _cleanup_dhcp_request_free_ _cleanup_(dhcp_request_freep) |
| |
| static int ensure_sane_request(DHCPRequest *req, DHCPMessage *message) { |
| assert(req); |
| assert(message); |
| |
| req->message = message; |
| |
| /* set client id based on MAC address if client did not send an explicit |
| one */ |
| if (!req->client_id.data) { |
| uint8_t *data; |
| |
| data = new0(uint8_t, ETH_ALEN + 1); |
| if (!data) |
| return -ENOMEM; |
| |
| req->client_id.length = ETH_ALEN + 1; |
| req->client_id.data = data; |
| req->client_id.data[0] = 0x01; |
| memcpy(&req->client_id.data[1], &message->chaddr, ETH_ALEN); |
| } |
| |
| if (req->max_optlen < DHCP_MIN_OPTIONS_SIZE) |
| req->max_optlen = DHCP_MIN_OPTIONS_SIZE; |
| |
| if (!req->lifetime) |
| req->lifetime = DHCP_DEFAULT_LEASE_TIME; |
| |
| return 0; |
| } |
| |
| static int get_pool_offset(sd_dhcp_server *server, be32_t requested_ip) { |
| assert(server); |
| |
| if (!server->pool_size) |
| return -EINVAL; |
| |
| if (be32toh(requested_ip) < be32toh(server->pool_start) || |
| be32toh(requested_ip) >= be32toh(server->pool_start) + |
| + server->pool_size) |
| return -EINVAL; |
| |
| return be32toh(requested_ip) - be32toh(server->pool_start); |
| } |
| |
| int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, |
| size_t length) { |
| _cleanup_dhcp_request_free_ DHCPRequest *req = NULL; |
| DHCPLease *existing_lease; |
| int type, r; |
| |
| assert(server); |
| assert(message); |
| |
| if (message->op != BOOTREQUEST || |
| message->htype != ARPHRD_ETHER || |
| message->hlen != ETHER_ADDR_LEN) |
| return 0; |
| |
| req = new0(DHCPRequest, 1); |
| if (!req) |
| return -ENOMEM; |
| |
| type = dhcp_option_parse(message, length, parse_request, req); |
| if (type < 0) |
| return 0; |
| |
| r = ensure_sane_request(req, message); |
| if (r < 0) |
| /* this only fails on critical errors */ |
| return r; |
| |
| existing_lease = hashmap_get(server->leases_by_client_id, |
| &req->client_id); |
| |
| switch(type) { |
| case DHCP_DISCOVER: |
| { |
| be32_t address = INADDR_ANY; |
| unsigned i; |
| |
| log_dhcp_server(server, "DISCOVER (0x%x)", |
| be32toh(req->message->xid)); |
| |
| if (!server->pool_size) |
| /* no pool allocated */ |
| return 0; |
| |
| /* for now pick a random free address from the pool */ |
| if (existing_lease) |
| address = existing_lease->address; |
| else { |
| for (i = 0; i < server->pool_size; i++) { |
| if (!server->bound_leases[server->next_offer]) { |
| address = htobe32(be32toh(server->pool_start) + server->next_offer); |
| break; |
| } else |
| server->next_offer = (server->next_offer + 1) % server->pool_size; |
| } |
| } |
| |
| if (address == INADDR_ANY) |
| /* no free addresses left */ |
| return 0; |
| |
| r = server_send_offer(server, req, address); |
| if (r < 0) { |
| /* this only fails on critical errors */ |
| log_dhcp_server(server, "could not send offer: %s", |
| strerror(-r)); |
| return r; |
| } else { |
| log_dhcp_server(server, "OFFER (0x%x)", |
| be32toh(req->message->xid)); |
| return DHCP_OFFER; |
| } |
| |
| break; |
| } |
| case DHCP_DECLINE: |
| log_dhcp_server(server, "DECLINE (0x%x)", |
| be32toh(req->message->xid)); |
| |
| /* TODO: make sure we don't offer this address again */ |
| |
| return 1; |
| |
| break; |
| case DHCP_REQUEST: |
| { |
| be32_t address; |
| bool init_reboot = false; |
| int pool_offset; |
| |
| /* see RFC 2131, section 4.3.2 */ |
| |
| if (req->server_id) { |
| log_dhcp_server(server, "REQUEST (selecting) (0x%x)", |
| be32toh(req->message->xid)); |
| |
| /* SELECTING */ |
| if (req->server_id != server->address) |
| /* client did not pick us */ |
| return 0; |
| |
| if (req->message->ciaddr) |
| /* this MUST be zero */ |
| return 0; |
| |
| if (!req->requested_ip) |
| /* this must be filled in with the yiaddr |
| from the chosen OFFER */ |
| return 0; |
| |
| address = req->requested_ip; |
| } else if (req->requested_ip) { |
| log_dhcp_server(server, "REQUEST (init-reboot) (0x%x)", |
| be32toh(req->message->xid)); |
| |
| /* INIT-REBOOT */ |
| if (req->message->ciaddr) |
| /* this MUST be zero */ |
| return 0; |
| |
| /* TODO: check more carefully if IP is correct */ |
| address = req->requested_ip; |
| init_reboot = true; |
| } else { |
| log_dhcp_server(server, "REQUEST (rebinding/renewing) (0x%x)", |
| be32toh(req->message->xid)); |
| |
| /* REBINDING / RENEWING */ |
| if (!req->message->ciaddr) |
| /* this MUST be filled in with clients IP address */ |
| return 0; |
| |
| address = req->message->ciaddr; |
| } |
| |
| pool_offset = get_pool_offset(server, address); |
| |
| /* verify that the requested address is from the pool, and either |
| owned by the current client or free */ |
| if (pool_offset >= 0 && |
| server->bound_leases[pool_offset] == existing_lease) { |
| DHCPLease *lease; |
| usec_t time_now = 0; |
| |
| if (!existing_lease) { |
| lease = new0(DHCPLease, 1); |
| lease->address = req->requested_ip; |
| lease->client_id.data = memdup(req->client_id.data, |
| req->client_id.length); |
| if (!lease->client_id.data) { |
| free(lease); |
| return -ENOMEM; |
| } |
| lease->client_id.length = req->client_id.length; |
| memcpy(&lease->chaddr, &req->message->chaddr, |
| ETH_ALEN); |
| lease->gateway = req->message->giaddr; |
| } else |
| lease = existing_lease; |
| |
| r = sd_event_now(server->event, |
| clock_boottime_or_monotonic(), |
| &time_now); |
| if (r < 0) |
| time_now = now(clock_boottime_or_monotonic()); |
| lease->expiration = req->lifetime * USEC_PER_SEC + time_now; |
| |
| r = server_send_ack(server, req, address); |
| if (r < 0) { |
| /* this only fails on critical errors */ |
| log_dhcp_server(server, "could not send ack: %s", |
| strerror(-r)); |
| |
| if (!existing_lease) |
| dhcp_lease_free(lease); |
| |
| return r; |
| } else { |
| log_dhcp_server(server, "ACK (0x%x)", |
| be32toh(req->message->xid)); |
| |
| server->bound_leases[pool_offset] = lease; |
| hashmap_put(server->leases_by_client_id, |
| &lease->client_id, lease); |
| |
| return DHCP_ACK; |
| } |
| } else if (init_reboot) { |
| r = server_send_nak(server, req); |
| if (r < 0) { |
| /* this only fails on critical errors */ |
| log_dhcp_server(server, "could not send nak: %s", |
| strerror(-r)); |
| return r; |
| } else { |
| log_dhcp_server(server, "NAK (0x%x)", |
| be32toh(req->message->xid)); |
| return DHCP_NAK; |
| } |
| } |
| |
| break; |
| } |
| case DHCP_RELEASE: { |
| int pool_offset; |
| |
| log_dhcp_server(server, "RELEASE (0x%x)", |
| be32toh(req->message->xid)); |
| |
| if (!existing_lease) |
| return 0; |
| |
| if (existing_lease->address != req->message->ciaddr) |
| return 0; |
| |
| pool_offset = get_pool_offset(server, req->message->ciaddr); |
| if (pool_offset < 0) |
| return 0; |
| |
| if (server->bound_leases[pool_offset] == existing_lease) { |
| server->bound_leases[pool_offset] = NULL; |
| hashmap_remove(server->leases_by_client_id, existing_lease); |
| dhcp_lease_free(existing_lease); |
| |
| return 1; |
| } else |
| return 0; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int server_receive_message(sd_event_source *s, int fd, |
| uint32_t revents, void *userdata) { |
| _cleanup_free_ DHCPMessage *message = NULL; |
| uint8_t cmsgbuf[CMSG_LEN(sizeof(struct in_pktinfo))]; |
| sd_dhcp_server *server = userdata; |
| struct iovec iov = {}; |
| struct msghdr msg = { |
| .msg_iov = &iov, |
| .msg_iovlen = 1, |
| .msg_control = cmsgbuf, |
| .msg_controllen = sizeof(cmsgbuf), |
| }; |
| struct cmsghdr *cmsg; |
| int buflen = 0, len, r; |
| |
| assert(server); |
| |
| r = ioctl(fd, FIONREAD, &buflen); |
| if (r < 0) |
| return r; |
| if (buflen < 0) |
| return -EIO; |
| |
| message = malloc0(buflen); |
| if (!message) |
| return -ENOMEM; |
| |
| iov.iov_base = message; |
| iov.iov_len = buflen; |
| |
| len = recvmsg(fd, &msg, 0); |
| if (len < buflen) |
| return 0; |
| else if ((size_t)len < sizeof(DHCPMessage)) |
| return 0; |
| |
| CMSG_FOREACH(cmsg, &msg) { |
| if (cmsg->cmsg_level == IPPROTO_IP && |
| cmsg->cmsg_type == IP_PKTINFO && |
| cmsg->cmsg_len == CMSG_LEN(sizeof(struct in_pktinfo))) { |
| struct in_pktinfo *info = (struct in_pktinfo*)CMSG_DATA(cmsg); |
| |
| /* TODO figure out if this can be done as a filter on |
| * the socket, like for IPv6 */ |
| if (server->index != info->ipi_ifindex) |
| return 0; |
| |
| break; |
| } |
| } |
| |
| return dhcp_server_handle_message(server, message, (size_t)len); |
| } |
| |
| int sd_dhcp_server_start(sd_dhcp_server *server) { |
| int r; |
| |
| assert_return(server, -EINVAL); |
| assert_return(server->event, -EINVAL); |
| assert_return(!server->receive_message, -EBUSY); |
| assert_return(server->fd_raw == -1, -EBUSY); |
| assert_return(server->fd == -1, -EBUSY); |
| assert_return(server->address != htobe32(INADDR_ANY), -EUNATCH); |
| |
| r = socket(AF_PACKET, SOCK_DGRAM | SOCK_NONBLOCK, 0); |
| if (r < 0) { |
| r = -errno; |
| sd_dhcp_server_stop(server); |
| return r; |
| } |
| server->fd_raw = r; |
| |
| r = dhcp_network_bind_udp_socket(INADDR_ANY, DHCP_PORT_SERVER); |
| if (r < 0) { |
| sd_dhcp_server_stop(server); |
| return r; |
| } |
| server->fd = r; |
| |
| r = sd_event_add_io(server->event, &server->receive_message, |
| server->fd, EPOLLIN, |
| server_receive_message, server); |
| if (r < 0) { |
| sd_dhcp_server_stop(server); |
| return r; |
| } |
| |
| r = sd_event_source_set_priority(server->receive_message, |
| server->event_priority); |
| if (r < 0) { |
| sd_dhcp_server_stop(server); |
| return r; |
| } |
| |
| log_dhcp_server(server, "STARTED"); |
| |
| return 0; |
| } |
| |
| int sd_dhcp_server_forcerenew(sd_dhcp_server *server) { |
| unsigned i; |
| int r = 0; |
| |
| assert_return(server, -EINVAL); |
| assert(server->bound_leases); |
| |
| for (i = 0; i < server->pool_size; i++) { |
| DHCPLease *lease = server->bound_leases[i]; |
| |
| if (!lease) |
| continue; |
| |
| r = server_send_forcerenew(server, lease->address, |
| lease->gateway, |
| lease->chaddr); |
| if (r < 0) |
| return r; |
| else |
| log_dhcp_server(server, "FORCERENEW"); |
| } |
| |
| return r; |
| } |