diff options
Diffstat (limited to 'azurelinuxagent/common/dhcp.py')
-rw-r--r-- | azurelinuxagent/common/dhcp.py | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/azurelinuxagent/common/dhcp.py b/azurelinuxagent/common/dhcp.py new file mode 100644 index 0000000..d5c90cb --- /dev/null +++ b/azurelinuxagent/common/dhcp.py @@ -0,0 +1,400 @@ +# Copyright 2014 Microsoft Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Requires Python 2.4+ and Openssl 1.0+ + +import os +import socket +import array +import time +import azurelinuxagent.common.logger as logger +import azurelinuxagent.common.utils.shellutil as shellutil +from azurelinuxagent.common.utils import fileutil +from azurelinuxagent.common.utils.textutil import hex_dump, hex_dump2, \ + hex_dump3, \ + compare_bytes, str_to_ord, \ + unpack_big_endian, \ + int_to_ip4_addr +from azurelinuxagent.common.exception import DhcpError +from azurelinuxagent.common.osutil import get_osutil + +# the kernel routing table representation of 168.63.129.16 +KNOWN_WIRESERVER_IP_ENTRY = '10813FA8' +KNOWN_WIRESERVER_IP = '168.63.129.16' + + +def get_dhcp_handler(): + return DhcpHandler() + + +class DhcpHandler(object): + """ + Azure use DHCP option 245 to pass endpoint ip to VMs. + """ + + def __init__(self): + self.osutil = get_osutil() + self.endpoint = None + self.gateway = None + self.routes = None + self._request_broadcast = False + self.skip_cache = False + + def run(self): + """ + Send dhcp request + Configure default gateway and routes + Save wire server endpoint if found + """ + if self.wireserver_route_exists or self.dhcp_cache_exists: + return + + self.send_dhcp_req() + self.conf_routes() + + def wait_for_network(self): + """ + Wait for network stack to be initialized. + """ + ipv4 = self.osutil.get_ip4_addr() + while ipv4 == '' or ipv4 == '0.0.0.0': + logger.info("Waiting for network.") + time.sleep(10) + logger.info("Try to start network interface.") + self.osutil.start_network() + ipv4 = self.osutil.get_ip4_addr() + + @property + def wireserver_route_exists(self): + """ + Determine whether a route to the known wireserver + ip already exists, and if so use that as the endpoint. + This is true when running in a virtual network. + :return: True if a route to KNOWN_WIRESERVER_IP exists. + """ + route_exists = False + logger.info("test for route to {0}".format(KNOWN_WIRESERVER_IP)) + try: + route_file = '/proc/net/route' + if os.path.exists(route_file) and \ + KNOWN_WIRESERVER_IP_ENTRY in open(route_file).read(): + # reset self.gateway and self.routes + # we do not need to alter the routing table + self.endpoint = KNOWN_WIRESERVER_IP + self.gateway = None + self.routes = None + route_exists = True + logger.info("route to {0} exists".format(KNOWN_WIRESERVER_IP)) + else: + logger.warn( + "no route exists to {0}".format(KNOWN_WIRESERVER_IP)) + except Exception as e: + logger.error( + "could not determine whether route exists to {0}: {1}".format( + KNOWN_WIRESERVER_IP, e)) + + return route_exists + + @property + def dhcp_cache_exists(self): + """ + Check whether the dhcp options cache exists and contains the + wireserver endpoint, unless skip_cache is True. + :return: True if the cached endpoint was found in the dhcp lease + """ + if self.skip_cache: + return False + + exists = False + + logger.info("checking for dhcp lease cache") + cached_endpoint = self.osutil.get_dhcp_lease_endpoint() + if cached_endpoint is not None: + self.endpoint = cached_endpoint + exists = True + logger.info("cache exists [{0}]".format(exists)) + return exists + + def conf_routes(self): + logger.info("Configure routes") + logger.info("Gateway:{0}", self.gateway) + logger.info("Routes:{0}", self.routes) + # Add default gateway + if self.gateway is not None: + self.osutil.route_add(0, 0, self.gateway) + if self.routes is not None: + for route in self.routes: + self.osutil.route_add(route[0], route[1], route[2]) + + def _send_dhcp_req(self, request): + __waiting_duration__ = [0, 10, 30, 60, 60] + for duration in __waiting_duration__: + try: + self.osutil.allow_dhcp_broadcast() + response = socket_send(request) + validate_dhcp_resp(request, response) + return response + except DhcpError as e: + logger.warn("Failed to send DHCP request: {0}", e) + time.sleep(duration) + return None + + def send_dhcp_req(self): + """ + Build dhcp request with mac addr + Configure route to allow dhcp traffic + Stop dhcp service if necessary + """ + logger.info("Send dhcp request") + mac_addr = self.osutil.get_mac_addr() + + # Do unicast first, then fallback to broadcast if fails. + req = build_dhcp_request(mac_addr, self._request_broadcast) + if not self._request_broadcast: + self._request_broadcast = True + + # Temporary allow broadcast for dhcp. Remove the route when done. + missing_default_route = self.osutil.is_missing_default_route() + ifname = self.osutil.get_if_name() + if missing_default_route: + self.osutil.set_route_for_dhcp_broadcast(ifname) + + # In some distros, dhcp service needs to be shutdown before agent probe + # endpoint through dhcp. + if self.osutil.is_dhcp_enabled(): + self.osutil.stop_dhcp_service() + + resp = self._send_dhcp_req(req) + + if self.osutil.is_dhcp_enabled(): + self.osutil.start_dhcp_service() + + if missing_default_route: + self.osutil.remove_route_for_dhcp_broadcast(ifname) + + if resp is None: + raise DhcpError("Failed to receive dhcp response.") + self.endpoint, self.gateway, self.routes = parse_dhcp_resp(resp) + + +def validate_dhcp_resp(request, response): + bytes_recv = len(response) + if bytes_recv < 0xF6: + logger.error("HandleDhcpResponse: Too few bytes received:{0}", + bytes_recv) + return False + + logger.verbose("BytesReceived:{0}", hex(bytes_recv)) + logger.verbose("DHCP response:{0}", hex_dump(response, bytes_recv)) + + # check transactionId, cookie, MAC address cookie should never mismatch + # transactionId and MAC address may mismatch if we see a response + # meant from another machine + if not compare_bytes(request, response, 0xEC, 4): + logger.verbose("Cookie not match:\nsend={0},\nreceive={1}", + hex_dump3(request, 0xEC, 4), + hex_dump3(response, 0xEC, 4)) + raise DhcpError("Cookie in dhcp respones doesn't match the request") + + if not compare_bytes(request, response, 4, 4): + logger.verbose("TransactionID not match:\nsend={0},\nreceive={1}", + hex_dump3(request, 4, 4), + hex_dump3(response, 4, 4)) + raise DhcpError("TransactionID in dhcp respones " + "doesn't match the request") + + if not compare_bytes(request, response, 0x1C, 6): + logger.verbose("Mac Address not match:\nsend={0},\nreceive={1}", + hex_dump3(request, 0x1C, 6), + hex_dump3(response, 0x1C, 6)) + raise DhcpError("Mac Addr in dhcp respones " + "doesn't match the request") + + +def parse_route(response, option, i, length, bytes_recv): + # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx + logger.verbose("Routes at offset: {0} with length:{1}", hex(i), + hex(length)) + routes = [] + if length < 5: + logger.error("Data too small for option:{0}", option) + j = i + 2 + while j < (i + length + 2): + mask_len_bits = str_to_ord(response[j]) + mask_len_bytes = (((mask_len_bits + 7) & ~7) >> 3) + mask = 0xFFFFFFFF & (0xFFFFFFFF << (32 - mask_len_bits)) + j += 1 + net = unpack_big_endian(response, j, mask_len_bytes) + net <<= (32 - mask_len_bytes * 8) + net &= mask + j += mask_len_bytes + gateway = unpack_big_endian(response, j, 4) + j += 4 + routes.append((net, mask, gateway)) + if j != (i + length + 2): + logger.error("Unable to parse routes") + return routes + + +def parse_ip_addr(response, option, i, length, bytes_recv): + if i + 5 < bytes_recv: + if length != 4: + logger.error("Endpoint or Default Gateway not 4 bytes") + return None + addr = unpack_big_endian(response, i + 2, 4) + ip_addr = int_to_ip4_addr(addr) + return ip_addr + else: + logger.error("Data too small for option:{0}", option) + return None + + +def parse_dhcp_resp(response): + """ + Parse DHCP response: + Returns endpoint server or None on error. + """ + logger.verbose("parse Dhcp Response") + bytes_recv = len(response) + endpoint = None + gateway = None + routes = None + + # Walk all the returned options, parsing out what we need, ignoring the + # others. We need the custom option 245 to find the the endpoint we talk to + # as well as to handle some Linux DHCP client incompatibilities; + # options 3 for default gateway and 249 for routes; 255 is end. + + i = 0xF0 # offset to first option + while i < bytes_recv: + option = str_to_ord(response[i]) + length = 0 + if (i + 1) < bytes_recv: + length = str_to_ord(response[i + 1]) + logger.verbose("DHCP option {0} at offset:{1} with length:{2}", + hex(option), hex(i), hex(length)) + if option == 255: + logger.verbose("DHCP packet ended at offset:{0}", hex(i)) + break + elif option == 249: + routes = parse_route(response, option, i, length, bytes_recv) + elif option == 3: + gateway = parse_ip_addr(response, option, i, length, bytes_recv) + logger.verbose("Default gateway:{0}, at {1}", gateway, hex(i)) + elif option == 245: + endpoint = parse_ip_addr(response, option, i, length, bytes_recv) + logger.verbose("Azure wire protocol endpoint:{0}, at {1}", + endpoint, + hex(i)) + else: + logger.verbose("Skipping DHCP option:{0} at {1} with length {2}", + hex(option), hex(i), hex(length)) + i += length + 2 + return endpoint, gateway, routes + + +def socket_send(request): + sock = None + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, + socket.IPPROTO_UDP) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind(("0.0.0.0", 68)) + sock.sendto(request, ("<broadcast>", 67)) + sock.settimeout(10) + logger.verbose("Send DHCP request: Setting socket.timeout=10, " + "entering recv") + response = sock.recv(1024) + return response + except IOError as e: + raise DhcpError("{0}".format(e)) + finally: + if sock is not None: + sock.close() + + +def build_dhcp_request(mac_addr, request_broadcast): + """ + Build DHCP request string. + """ + # + # typedef struct _DHCP { + # UINT8 Opcode; /* op: BOOTREQUEST or BOOTREPLY */ + # UINT8 HardwareAddressType; /* htype: ethernet */ + # UINT8 HardwareAddressLength; /* hlen: 6 (48 bit mac address) */ + # UINT8 Hops; /* hops: 0 */ + # UINT8 TransactionID[4]; /* xid: random */ + # UINT8 Seconds[2]; /* secs: 0 */ + # UINT8 Flags[2]; /* flags: 0 or 0x8000 for broadcast*/ + # UINT8 ClientIpAddress[4]; /* ciaddr: 0 */ + # UINT8 YourIpAddress[4]; /* yiaddr: 0 */ + # UINT8 ServerIpAddress[4]; /* siaddr: 0 */ + # UINT8 RelayAgentIpAddress[4]; /* giaddr: 0 */ + # UINT8 ClientHardwareAddress[16]; /* chaddr: 6 byte eth MAC address */ + # UINT8 ServerName[64]; /* sname: 0 */ + # UINT8 BootFileName[128]; /* file: 0 */ + # UINT8 MagicCookie[4]; /* 99 130 83 99 */ + # /* 0x63 0x82 0x53 0x63 */ + # /* options -- hard code ours */ + # + # UINT8 MessageTypeCode; /* 53 */ + # UINT8 MessageTypeLength; /* 1 */ + # UINT8 MessageType; /* 1 for DISCOVER */ + # UINT8 End; /* 255 */ + # } DHCP; + # + + # tuple of 244 zeros + # (struct.pack_into would be good here, but requires Python 2.5) + request = [0] * 244 + + trans_id = gen_trans_id() + + # Opcode = 1 + # HardwareAddressType = 1 (ethernet/MAC) + # HardwareAddressLength = 6 (ethernet/MAC/48 bits) + for a in range(0, 3): + request[a] = [1, 1, 6][a] + + # fill in transaction id (random number to ensure response matches request) + for a in range(0, 4): + request[4 + a] = str_to_ord(trans_id[a]) + + logger.verbose("BuildDhcpRequest: transactionId:%s,%04X" % ( + hex_dump2(trans_id), + unpack_big_endian(request, 4, 4))) + + if request_broadcast: + # set broadcast flag to true to request the dhcp sever + # to respond to a boradcast address, + # this is useful when user dhclient fails. + request[0x0A] = 0x80; + + # fill in ClientHardwareAddress + for a in range(0, 6): + request[0x1C + a] = str_to_ord(mac_addr[a]) + + # DHCP Magic Cookie: 99, 130, 83, 99 + # MessageTypeCode = 53 DHCP Message Type + # MessageTypeLength = 1 + # MessageType = DHCPDISCOVER + # End = 255 DHCP_END + for a in range(0, 8): + request[0xEC + a] = [99, 130, 83, 99, 53, 1, 1, 255][a] + return array.array("B", request) + + +def gen_trans_id(): + return os.urandom(4) |