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

« back to all changes in this revision

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