1
# Copyright 2014 Microsoft Corporation
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
7
# http://www.apache.org/licenses/LICENSE-2.0
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.
15
# Requires Python 2.4+ and Openssl 1.0+
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, \
26
compare_bytes, str_to_ord, \
29
from azurelinuxagent.common.exception import DhcpError
30
from azurelinuxagent.common.osutil import get_osutil
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'
37
def get_dhcp_handler():
41
class DhcpHandler(object):
43
Azure use DHCP option 245 to pass endpoint ip to VMs.
47
self.osutil = get_osutil()
51
self._request_broadcast = False
52
self.skip_cache = False
57
Configure default gateway and routes
58
Save wire server endpoint if found
60
if self.wireserver_route_exists or self.dhcp_cache_exists:
66
def wait_for_network(self):
68
Wait for network stack to be initialized.
70
ipv4 = self.osutil.get_ip4_addr()
71
while ipv4 == '' or ipv4 == '0.0.0.0':
72
logger.info("Waiting for network.")
74
logger.info("Try to start network interface.")
75
self.osutil.start_network()
76
ipv4 = self.osutil.get_ip4_addr()
79
def wireserver_route_exists(self):
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.
87
logger.info("test for route to {0}".format(KNOWN_WIRESERVER_IP))
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
98
logger.info("route to {0} exists".format(KNOWN_WIRESERVER_IP))
101
"no route exists to {0}".format(KNOWN_WIRESERVER_IP))
102
except Exception as e:
104
"could not determine whether route exists to {0}: {1}".format(
105
KNOWN_WIRESERVER_IP, e))
110
def dhcp_cache_exists(self):
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
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
126
logger.info("cache exists [{0}]".format(exists))
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])
140
def _send_dhcp_req(self, request):
141
__waiting_duration__ = [0, 10, 30, 60, 60]
142
for duration in __waiting_duration__:
144
self.osutil.allow_dhcp_broadcast()
145
response = socket_send(request)
146
validate_dhcp_resp(request, response)
148
except DhcpError as e:
149
logger.warn("Failed to send DHCP request: {0}", e)
153
def send_dhcp_req(self):
155
Build dhcp request with mac addr
156
Configure route to allow dhcp traffic
157
Stop dhcp service if necessary
159
logger.info("Send dhcp request")
160
mac_addr = self.osutil.get_mac_addr()
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
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)
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()
178
resp = self._send_dhcp_req(req)
180
if self.osutil.is_dhcp_enabled():
181
self.osutil.start_dhcp_service()
183
if missing_default_route:
184
self.osutil.remove_route_for_dhcp_broadcast(ifname)
187
raise DhcpError("Failed to receive dhcp response.")
188
self.endpoint, self.gateway, self.routes = parse_dhcp_resp(resp)
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}",
198
logger.verbose("BytesReceived:{0}", hex(bytes_recv))
199
logger.verbose("DHCP response:{0}", hex_dump(response, bytes_recv))
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")
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")
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")
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),
231
logger.error("Data too small for option:{0}", option)
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))
238
net = unpack_big_endian(response, j, mask_len_bytes)
239
net <<= (32 - mask_len_bytes * 8)
242
gateway = unpack_big_endian(response, j, 4)
244
routes.append((net, mask, gateway))
245
if j != (i + length + 2):
246
logger.error("Unable to parse routes")
250
def parse_ip_addr(response, option, i, length, bytes_recv):
251
if i + 5 < bytes_recv:
253
logger.error("Endpoint or Default Gateway not 4 bytes")
255
addr = unpack_big_endian(response, i + 2, 4)
256
ip_addr = int_to_ip4_addr(addr)
259
logger.error("Data too small for option:{0}", option)
263
def parse_dhcp_resp(response):
266
Returns endpoint server or None on error.
268
logger.verbose("parse Dhcp Response")
269
bytes_recv = len(response)
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.
279
i = 0xF0 # offset to first option
280
while i < bytes_recv:
281
option = str_to_ord(response[i])
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))
288
logger.verbose("DHCP packet ended at offset:{0}", hex(i))
291
routes = parse_route(response, option, i, length, bytes_recv)
293
gateway = parse_ip_addr(response, option, i, length, bytes_recv)
294
logger.verbose("Default gateway:{0}, at {1}", gateway, hex(i))
296
endpoint = parse_ip_addr(response, option, i, length, bytes_recv)
297
logger.verbose("Azure wire protocol endpoint:{0}, at {1}",
301
logger.verbose("Skipping DHCP option:{0} at {1} with length {2}",
302
hex(option), hex(i), hex(length))
304
return endpoint, gateway, routes
307
def socket_send(request):
310
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
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))
317
logger.verbose("Send DHCP request: Setting socket.timeout=10, "
319
response = sock.recv(1024)
322
raise DhcpError("{0}".format(e))
328
def build_dhcp_request(mac_addr, request_broadcast):
330
Build DHCP request string.
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 */
352
# UINT8 MessageTypeCode; /* 53 */
353
# UINT8 MessageTypeLength; /* 1 */
354
# UINT8 MessageType; /* 1 for DISCOVER */
355
# UINT8 End; /* 255 */
360
# (struct.pack_into would be good here, but requires Python 2.5)
363
trans_id = gen_trans_id()
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]
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])
375
logger.verbose("BuildDhcpRequest: transactionId:%s,%04X" % (
377
unpack_big_endian(request, 4, 4)))
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;
385
# fill in ClientHardwareAddress
386
for a in range(0, 6):
387
request[0x1C + a] = str_to_ord(mac_addr[a])
389
# DHCP Magic Cookie: 99, 130, 83, 99
390
# MessageTypeCode = 53 DHCP Message Type
391
# MessageTypeLength = 1
392
# MessageType = DHCPDISCOVER
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)