X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=blobdiff_plain;f=src%2Flibsystemd-network%2Fsd-dhcp-server.c;h=e170cfabf19335df55a055a3e193d0e0f8d9e743;hb=2dead8129f7b6fe644e17e1dc1739bebacfe1364;hp=ecdc15dff733c1789a7ede402948691941c6b197;hpb=8de4a226c71ef43e652274b33b5d19211a44ac7b;p=elogind.git diff --git a/src/libsystemd-network/sd-dhcp-server.c b/src/libsystemd-network/sd-dhcp-server.c index ecdc15dff..e170cfabf 100644 --- a/src/libsystemd-network/sd-dhcp-server.c +++ b/src/libsystemd-network/sd-dhcp-server.c @@ -27,6 +27,34 @@ #include "dhcp-server-internal.h" #include "dhcp-internal.h" +#define DHCP_DEFAULT_LEASE_TIME 60 + +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); + + 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) { + assert_return(server, -EINVAL); + assert_return(address, -EINVAL); + assert_return(address->s_addr, -EINVAL); + assert_return(server->address == htobe32(INADDR_ANY), -EBUSY); + + server->address = address->s_addr; + + return 0; +} + sd_dhcp_server *sd_dhcp_server_ref(sd_dhcp_server *server) { if (server) assert_se(REFCNT_INC(server->n_ref) >= 2); @@ -60,6 +88,7 @@ int sd_dhcp_server_new(sd_dhcp_server **ret, int ifindex) { server->n_ref = REFCNT_INIT; server->fd_raw = -1; server->fd = -1; + server->address = htobe32(INADDR_ANY); server->index = ifindex; *ret = server; @@ -115,6 +144,239 @@ int sd_dhcp_server_stop(sd_dhcp_server *server) { 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 || requested_broadcast(req) || type == DHCP_NAK) + return dhcp_server_send_udp(server, destination, &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; + int r; + + assert(server); + assert(ret); + assert(_optoffset); + assert(IN_SET(type, DHCP_OFFER, DHCP_ACK)); + + packet = malloc0(sizeof(DHCPPacket) + req->max_optlen); + if (!packet) + return -ENOMEM; + + r = dhcp_message_init(&packet->dhcp, BOOTREPLY, be32toh(req->message->xid), + type, 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; + + /* for one minute */ + lease_time = htobe32(DHCP_DEFAULT_LEASE_TIME); + 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_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; + + /* for ten seconds */ + lease_time = htobe32(DHCP_DEFAULT_LEASE_TIME); + 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_server_send_packet(server, req, packet, DHCP_ACK, offset); + 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; @@ -122,6 +384,11 @@ static int parse_request(uint8_t code, uint8_t len, const uint8_t *option, assert(req); switch(code) { + 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; @@ -215,9 +482,108 @@ int dhcp_server_handle_message(sd_dhcp_server *server, DHCPMessage *message, /* this only fails on critical errors */ return r; - log_dhcp_server(server, "received message of type %d", type); + switch(type) { + case DHCP_DISCOVER: + { + be32_t address; + + 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 address from the pool */ + address = htobe32(be32toh(server->pool_start) + + (random_u32() % server->pool_size)); + + 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_REQUEST: + { + be32_t address; + + /* 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 if requested IP is correct, NAK if not */ + address = req->requested_ip; + } 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; + } - return 1; + /* for now we just verify that the address is from the pool, not + whether or not it is taken */ + if (htobe32(req->requested_ip) >= htobe32(server->pool_start) && + htobe32(req->requested_ip) < htobe32(server->pool_start) + + + server->pool_size) { + 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)); + return r; + } else { + log_dhcp_server(server, "ACK (0x%x)", + be32toh(req->message->xid)); + return DHCP_ACK; + } + } else + return 0; + + break; + } + } + + return 0; } static int server_receive_message(sd_event_source *s, int fd, @@ -281,6 +647,7 @@ int sd_dhcp_server_start(sd_dhcp_server *server) { 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) {