~ubuntu-branches/debian/stretch/waagent/stretch

« back to all changes in this revision

Viewing changes to azurelinuxagent/common/dhcp.py

  • Committer: Package Import Robot
  • Author(s): Bastian Blank
  • Date: 2016-08-24 16:48:22 UTC
  • mfrom: (1.2.5)
  • Revision ID: package-import@ubuntu.com-20160824164822-vdf8m5xy5gycm1cz
Tags: 2.1.6-1
New upstream version.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014 Microsoft Corporation
 
2
#
 
3
# Licensed under the Apache License, Version 2.0 (the "License");
 
4
# you may not use this file except in compliance with the License.
 
5
# You may obtain a copy of the License at
 
6
#
 
7
#     http://www.apache.org/licenses/LICENSE-2.0
 
8
#
 
9
# Unless required by applicable law or agreed to in writing, software
 
10
# distributed under the License is distributed on an "AS IS" BASIS,
 
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
12
# See the License for the specific language governing permissions and
 
13
# limitations under the License.
 
14
#
 
15
# Requires Python 2.4+ and Openssl 1.0+
 
16
 
 
17
import os
 
18
import socket
 
19
import array
 
20
import time
 
21
import azurelinuxagent.common.logger as logger
 
22
import azurelinuxagent.common.utils.shellutil as shellutil
 
23
from azurelinuxagent.common.utils import fileutil
 
24
from azurelinuxagent.common.utils.textutil import hex_dump, hex_dump2, \
 
25
    hex_dump3, \
 
26
    compare_bytes, str_to_ord, \
 
27
    unpack_big_endian, \
 
28
    int_to_ip4_addr
 
29
from azurelinuxagent.common.exception import DhcpError
 
30
from azurelinuxagent.common.osutil import get_osutil
 
31
 
 
32
# the kernel routing table representation of 168.63.129.16
 
33
KNOWN_WIRESERVER_IP_ENTRY = '10813FA8'
 
34
KNOWN_WIRESERVER_IP = '168.63.129.16'
 
35
 
 
36
 
 
37
def get_dhcp_handler():
 
38
    return DhcpHandler()
 
39
 
 
40
 
 
41
class DhcpHandler(object):
 
42
    """
 
43
    Azure use DHCP option 245 to pass endpoint ip to VMs.
 
44
    """
 
45
 
 
46
    def __init__(self):
 
47
        self.osutil = get_osutil()
 
48
        self.endpoint = None
 
49
        self.gateway = None
 
50
        self.routes = None
 
51
        self._request_broadcast = False
 
52
        self.skip_cache = False
 
53
 
 
54
    def run(self):
 
55
        """
 
56
        Send dhcp request
 
57
        Configure default gateway and routes
 
58
        Save wire server endpoint if found
 
59
        """
 
60
        if self.wireserver_route_exists or self.dhcp_cache_exists:
 
61
            return
 
62
 
 
63
        self.send_dhcp_req()
 
64
        self.conf_routes()
 
65
 
 
66
    def wait_for_network(self):
 
67
        """
 
68
        Wait for network stack to be initialized.
 
69
        """
 
70
        ipv4 = self.osutil.get_ip4_addr()
 
71
        while ipv4 == '' or ipv4 == '0.0.0.0':
 
72
            logger.info("Waiting for network.")
 
73
            time.sleep(10)
 
74
            logger.info("Try to start network interface.")
 
75
            self.osutil.start_network()
 
76
            ipv4 = self.osutil.get_ip4_addr()
 
77
 
 
78
    @property
 
79
    def wireserver_route_exists(self):
 
80
        """
 
81
        Determine whether a route to the known wireserver
 
82
        ip already exists, and if so use that as the endpoint.
 
83
        This is true when running in a virtual network.
 
84
        :return: True if a route to KNOWN_WIRESERVER_IP exists.
 
85
        """
 
86
        route_exists = False
 
87
        logger.info("test for route to {0}".format(KNOWN_WIRESERVER_IP))
 
88
        try:
 
89
            route_file = '/proc/net/route'
 
90
            if os.path.exists(route_file) and \
 
91
                    KNOWN_WIRESERVER_IP_ENTRY in open(route_file).read():
 
92
                # reset self.gateway and self.routes
 
93
                # we do not need to alter the routing table
 
94
                self.endpoint = KNOWN_WIRESERVER_IP
 
95
                self.gateway = None
 
96
                self.routes = None
 
97
                route_exists = True
 
98
                logger.info("route to {0} exists".format(KNOWN_WIRESERVER_IP))
 
99
            else:
 
100
                logger.warn(
 
101
                    "no route exists to {0}".format(KNOWN_WIRESERVER_IP))
 
102
        except Exception as e:
 
103
            logger.error(
 
104
                "could not determine whether route exists to {0}: {1}".format(
 
105
                    KNOWN_WIRESERVER_IP, e))
 
106
 
 
107
        return route_exists
 
108
 
 
109
    @property
 
110
    def dhcp_cache_exists(self):
 
111
        """
 
112
        Check whether the dhcp options cache exists and contains the
 
113
        wireserver endpoint, unless skip_cache is True.
 
114
        :return: True if the cached endpoint was found in the dhcp lease
 
115
        """
 
116
        if self.skip_cache:
 
117
            return False
 
118
 
 
119
        exists = False
 
120
 
 
121
        logger.info("checking for dhcp lease cache")
 
122
        cached_endpoint = self.osutil.get_dhcp_lease_endpoint()
 
123
        if cached_endpoint is not None:
 
124
            self.endpoint = cached_endpoint
 
125
            exists = True
 
126
        logger.info("cache exists [{0}]".format(exists))
 
127
        return exists
 
128
 
 
129
    def conf_routes(self):
 
130
        logger.info("Configure routes")
 
131
        logger.info("Gateway:{0}", self.gateway)
 
132
        logger.info("Routes:{0}", self.routes)
 
133
        # Add default gateway
 
134
        if self.gateway is not None:
 
135
            self.osutil.route_add(0, 0, self.gateway)
 
136
        if self.routes is not None:
 
137
            for route in self.routes:
 
138
                self.osutil.route_add(route[0], route[1], route[2])
 
139
 
 
140
    def _send_dhcp_req(self, request):
 
141
        __waiting_duration__ = [0, 10, 30, 60, 60]
 
142
        for duration in __waiting_duration__:
 
143
            try:
 
144
                self.osutil.allow_dhcp_broadcast()
 
145
                response = socket_send(request)
 
146
                validate_dhcp_resp(request, response)
 
147
                return response
 
148
            except DhcpError as e:
 
149
                logger.warn("Failed to send DHCP request: {0}", e)
 
150
            time.sleep(duration)
 
151
        return None
 
152
 
 
153
    def send_dhcp_req(self):
 
154
        """
 
155
        Build dhcp request with mac addr
 
156
        Configure route to allow dhcp traffic
 
157
        Stop dhcp service if necessary
 
158
        """
 
159
        logger.info("Send dhcp request")
 
160
        mac_addr = self.osutil.get_mac_addr()
 
161
 
 
162
        # Do unicast first, then fallback to broadcast if fails.
 
163
        req = build_dhcp_request(mac_addr, self._request_broadcast)
 
164
        if not self._request_broadcast:
 
165
            self._request_broadcast = True
 
166
 
 
167
        # Temporary allow broadcast for dhcp. Remove the route when done.
 
168
        missing_default_route = self.osutil.is_missing_default_route()
 
169
        ifname = self.osutil.get_if_name()
 
170
        if missing_default_route:
 
171
            self.osutil.set_route_for_dhcp_broadcast(ifname)
 
172
 
 
173
        # In some distros, dhcp service needs to be shutdown before agent probe
 
174
        # endpoint through dhcp.
 
175
        if self.osutil.is_dhcp_enabled():
 
176
            self.osutil.stop_dhcp_service()
 
177
 
 
178
        resp = self._send_dhcp_req(req)
 
179
 
 
180
        if self.osutil.is_dhcp_enabled():
 
181
            self.osutil.start_dhcp_service()
 
182
 
 
183
        if missing_default_route:
 
184
            self.osutil.remove_route_for_dhcp_broadcast(ifname)
 
185
 
 
186
        if resp is None:
 
187
            raise DhcpError("Failed to receive dhcp response.")
 
188
        self.endpoint, self.gateway, self.routes = parse_dhcp_resp(resp)
 
189
 
 
190
 
 
191
def validate_dhcp_resp(request, response):
 
192
    bytes_recv = len(response)
 
193
    if bytes_recv < 0xF6:
 
194
        logger.error("HandleDhcpResponse: Too few bytes received:{0}",
 
195
                     bytes_recv)
 
196
        return False
 
197
 
 
198
    logger.verbose("BytesReceived:{0}", hex(bytes_recv))
 
199
    logger.verbose("DHCP response:{0}", hex_dump(response, bytes_recv))
 
200
 
 
201
    # check transactionId, cookie, MAC address cookie should never mismatch
 
202
    # transactionId and MAC address may mismatch if we see a response
 
203
    # meant from another machine
 
204
    if not compare_bytes(request, response, 0xEC, 4):
 
205
        logger.verbose("Cookie not match:\nsend={0},\nreceive={1}",
 
206
                       hex_dump3(request, 0xEC, 4),
 
207
                       hex_dump3(response, 0xEC, 4))
 
208
        raise DhcpError("Cookie in dhcp respones doesn't match the request")
 
209
 
 
210
    if not compare_bytes(request, response, 4, 4):
 
211
        logger.verbose("TransactionID not match:\nsend={0},\nreceive={1}",
 
212
                       hex_dump3(request, 4, 4),
 
213
                       hex_dump3(response, 4, 4))
 
214
        raise DhcpError("TransactionID in dhcp respones "
 
215
                        "doesn't match the request")
 
216
 
 
217
    if not compare_bytes(request, response, 0x1C, 6):
 
218
        logger.verbose("Mac Address not match:\nsend={0},\nreceive={1}",
 
219
                       hex_dump3(request, 0x1C, 6),
 
220
                       hex_dump3(response, 0x1C, 6))
 
221
        raise DhcpError("Mac Addr in dhcp respones "
 
222
                        "doesn't match the request")
 
223
 
 
224
 
 
225
def parse_route(response, option, i, length, bytes_recv):
 
226
    # http://msdn.microsoft.com/en-us/library/cc227282%28PROT.10%29.aspx
 
227
    logger.verbose("Routes at offset: {0} with length:{1}", hex(i),
 
228
                   hex(length))
 
229
    routes = []
 
230
    if length < 5:
 
231
        logger.error("Data too small for option:{0}", option)
 
232
    j = i + 2
 
233
    while j < (i + length + 2):
 
234
        mask_len_bits = str_to_ord(response[j])
 
235
        mask_len_bytes = (((mask_len_bits + 7) & ~7) >> 3)
 
236
        mask = 0xFFFFFFFF & (0xFFFFFFFF << (32 - mask_len_bits))
 
237
        j += 1
 
238
        net = unpack_big_endian(response, j, mask_len_bytes)
 
239
        net <<= (32 - mask_len_bytes * 8)
 
240
        net &= mask
 
241
        j += mask_len_bytes
 
242
        gateway = unpack_big_endian(response, j, 4)
 
243
        j += 4
 
244
        routes.append((net, mask, gateway))
 
245
    if j != (i + length + 2):
 
246
        logger.error("Unable to parse routes")
 
247
    return routes
 
248
 
 
249
 
 
250
def parse_ip_addr(response, option, i, length, bytes_recv):
 
251
    if i + 5 < bytes_recv:
 
252
        if length != 4:
 
253
            logger.error("Endpoint or Default Gateway not 4 bytes")
 
254
            return None
 
255
        addr = unpack_big_endian(response, i + 2, 4)
 
256
        ip_addr = int_to_ip4_addr(addr)
 
257
        return ip_addr
 
258
    else:
 
259
        logger.error("Data too small for option:{0}", option)
 
260
    return None
 
261
 
 
262
 
 
263
def parse_dhcp_resp(response):
 
264
    """
 
265
    Parse DHCP response:
 
266
    Returns endpoint server or None on error.
 
267
    """
 
268
    logger.verbose("parse Dhcp Response")
 
269
    bytes_recv = len(response)
 
270
    endpoint = None
 
271
    gateway = None
 
272
    routes = None
 
273
 
 
274
    # Walk all the returned options, parsing out what we need, ignoring the
 
275
    # others. We need the custom option 245 to find the the endpoint we talk to
 
276
    # as well as to handle some Linux DHCP client incompatibilities;
 
277
    # options 3 for default gateway and 249 for routes; 255 is end.
 
278
 
 
279
    i = 0xF0  # offset to first option
 
280
    while i < bytes_recv:
 
281
        option = str_to_ord(response[i])
 
282
        length = 0
 
283
        if (i + 1) < bytes_recv:
 
284
            length = str_to_ord(response[i + 1])
 
285
        logger.verbose("DHCP option {0} at offset:{1} with length:{2}",
 
286
                       hex(option), hex(i), hex(length))
 
287
        if option == 255:
 
288
            logger.verbose("DHCP packet ended at offset:{0}", hex(i))
 
289
            break
 
290
        elif option == 249:
 
291
            routes = parse_route(response, option, i, length, bytes_recv)
 
292
        elif option == 3:
 
293
            gateway = parse_ip_addr(response, option, i, length, bytes_recv)
 
294
            logger.verbose("Default gateway:{0}, at {1}", gateway, hex(i))
 
295
        elif option == 245:
 
296
            endpoint = parse_ip_addr(response, option, i, length, bytes_recv)
 
297
            logger.verbose("Azure wire protocol endpoint:{0}, at {1}",
 
298
                           endpoint,
 
299
                           hex(i))
 
300
        else:
 
301
            logger.verbose("Skipping DHCP option:{0} at {1} with length {2}",
 
302
                           hex(option), hex(i), hex(length))
 
303
        i += length + 2
 
304
    return endpoint, gateway, routes
 
305
 
 
306
 
 
307
def socket_send(request):
 
308
    sock = None
 
309
    try:
 
310
        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
 
311
                             socket.IPPROTO_UDP)
 
312
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
 
313
        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 
314
        sock.bind(("0.0.0.0", 68))
 
315
        sock.sendto(request, ("<broadcast>", 67))
 
316
        sock.settimeout(10)
 
317
        logger.verbose("Send DHCP request: Setting socket.timeout=10, "
 
318
                       "entering recv")
 
319
        response = sock.recv(1024)
 
320
        return response
 
321
    except IOError as e:
 
322
        raise DhcpError("{0}".format(e))
 
323
    finally:
 
324
        if sock is not None:
 
325
            sock.close()
 
326
 
 
327
 
 
328
def build_dhcp_request(mac_addr, request_broadcast):
 
329
    """
 
330
    Build DHCP request string.
 
331
    """
 
332
    #
 
333
    # typedef struct _DHCP {
 
334
    #  UINT8   Opcode;                    /* op:    BOOTREQUEST or BOOTREPLY */
 
335
    #  UINT8   HardwareAddressType;       /* htype: ethernet */
 
336
    #  UINT8   HardwareAddressLength;     /* hlen:  6 (48 bit mac address) */
 
337
    #  UINT8   Hops;                      /* hops:  0 */
 
338
    #  UINT8   TransactionID[4];          /* xid:   random */
 
339
    #  UINT8   Seconds[2];                /* secs:  0 */
 
340
    #  UINT8   Flags[2];                  /* flags: 0 or 0x8000 for broadcast*/
 
341
    #  UINT8   ClientIpAddress[4];        /* ciaddr: 0 */
 
342
    #  UINT8   YourIpAddress[4];          /* yiaddr: 0 */
 
343
    #  UINT8   ServerIpAddress[4];        /* siaddr: 0 */
 
344
    #  UINT8   RelayAgentIpAddress[4];    /* giaddr: 0 */
 
345
    #  UINT8   ClientHardwareAddress[16]; /* chaddr: 6 byte eth MAC address */
 
346
    #  UINT8   ServerName[64];            /* sname:  0 */
 
347
    #  UINT8   BootFileName[128];         /* file:   0  */
 
348
    #  UINT8   MagicCookie[4];            /*   99  130   83   99 */
 
349
    #                                        /* 0x63 0x82 0x53 0x63 */
 
350
    #     /* options -- hard code ours */
 
351
    #
 
352
    #     UINT8 MessageTypeCode;              /* 53 */
 
353
    #     UINT8 MessageTypeLength;            /* 1 */
 
354
    #     UINT8 MessageType;                  /* 1 for DISCOVER */
 
355
    #     UINT8 End;                          /* 255 */
 
356
    # } DHCP;
 
357
    #
 
358
 
 
359
    # tuple of 244 zeros
 
360
    # (struct.pack_into would be good here, but requires Python 2.5)
 
361
    request = [0] * 244
 
362
 
 
363
    trans_id = gen_trans_id()
 
364
 
 
365
    # Opcode = 1
 
366
    # HardwareAddressType = 1 (ethernet/MAC)
 
367
    # HardwareAddressLength = 6 (ethernet/MAC/48 bits)
 
368
    for a in range(0, 3):
 
369
        request[a] = [1, 1, 6][a]
 
370
 
 
371
    # fill in transaction id (random number to ensure response matches request)
 
372
    for a in range(0, 4):
 
373
        request[4 + a] = str_to_ord(trans_id[a])
 
374
 
 
375
    logger.verbose("BuildDhcpRequest: transactionId:%s,%04X" % (
 
376
        hex_dump2(trans_id),
 
377
        unpack_big_endian(request, 4, 4)))
 
378
 
 
379
    if request_broadcast:
 
380
        # set broadcast flag to true to request the dhcp sever
 
381
        # to respond to a boradcast address,
 
382
        # this is useful when user dhclient fails.
 
383
        request[0x0A] = 0x80;
 
384
 
 
385
    # fill in ClientHardwareAddress
 
386
    for a in range(0, 6):
 
387
        request[0x1C + a] = str_to_ord(mac_addr[a])
 
388
 
 
389
    # DHCP Magic Cookie: 99, 130, 83, 99
 
390
    # MessageTypeCode = 53 DHCP Message Type
 
391
    # MessageTypeLength = 1
 
392
    # MessageType = DHCPDISCOVER
 
393
    # End = 255 DHCP_END
 
394
    for a in range(0, 8):
 
395
        request[0xEC + a] = [99, 130, 83, 99, 53, 1, 1, 255][a]
 
396
    return array.array("B", request)
 
397
 
 
398
 
 
399
def gen_trans_id():
 
400
    return os.urandom(4)