1
# Microsoft Azure Linux Agent
3
# Copyright 2014 Microsoft Corporation
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
9
# http://www.apache.org/licenses/LICENSE-2.0
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.
17
# Requires Python 2.4+ and Openssl 1.0+
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, \
30
unpack_little_endian, \
32
from azurelinuxagent.exception import DhcpError
35
class DhcpHandler(object):
37
Azure use DHCP option 245 to pass endpoint ip to VMs.
39
def __init__(self, distro):
44
self._request_broadcast = False
49
Configure default gateway and routes
50
Save wire server endpoint if found
55
def wait_for_network(self):
57
Wait for network stack to be initialized.
59
ipv4 = self.distro.osutil.get_ip4_addr()
60
while ipv4 == '' or ipv4 == '0.0.0.0':
61
logger.info("Waiting for network.")
63
logger.info("Try to start network interface.")
64
self.distro.osutil.start_network()
65
ipv4 = self.distro.osutil.get_ip4_addr()
67
def conf_routes(self):
68
logger.info("Configure routes")
69
logger.info("Gateway:{0}", self.gateway)
70
logger.info("Routes:{0}", self.routes)
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])
78
def _send_dhcp_req(self, request):
79
__waiting_duration__ = [0, 10, 30, 60, 60]
80
for duration in __waiting_duration__:
82
self.distro.osutil.allow_dhcp_broadcast()
83
response = socket_send(request)
84
validate_dhcp_resp(request, response)
86
except DhcpError as e:
87
logger.warn("Failed to send DHCP request: {0}", e)
91
def send_dhcp_req(self):
93
Build dhcp request with mac addr
94
Configure route to allow dhcp traffic
95
Stop dhcp service if necessary
97
logger.info("Send dhcp request")
98
mac_addr = self.distro.osutil.get_mac_addr()
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
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)
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()
116
resp = self._send_dhcp_req(req)
118
if self.distro.osutil.is_dhcp_enabled():
119
self.distro.osutil.start_dhcp_service()
121
if missing_default_route:
122
self.distro.osutil.remove_route_for_dhcp_broadcast(ifname)
125
raise DhcpError("Failed to receive dhcp response.")
126
self.endpoint, self.gateway, self.routes = parse_dhcp_resp(resp)
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}",
135
logger.verb("BytesReceived:{0}", hex(bytes_recv))
136
logger.verb("DHCP response:{0}", hex_dump(response, bytes_recv))
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")
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")
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")
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))
166
logger.error("Data too small for option:{0}", option)
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))
173
net = unpack_big_endian(response, j, mask_len_bytes)
174
net <<= (32 - mask_len_bytes * 8)
177
gateway = unpack_big_endian(response, j, 4)
179
routes.append((net, mask, gateway))
180
if j != (i + length + 2):
181
logger.error("Unable to parse routes")
184
def parse_ip_addr(response, option, i, length, bytes_recv):
185
if i + 5 < bytes_recv:
187
logger.error("Endpoint or Default Gateway not 4 bytes")
189
addr = unpack_big_endian(response, i + 2, 4)
190
ip_addr = int_to_ip4_addr(addr)
193
logger.error("Data too small for option:{0}", option)
196
def parse_dhcp_resp(response):
199
Returns endpoint server or None on error.
201
logger.verb("parse Dhcp Response")
202
bytes_recv = len(response)
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.
212
i = 0xF0 # offset to first option
213
while i < bytes_recv:
214
option = str_to_ord(response[i])
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))
221
logger.verb("DHCP packet ended at offset:{0}", hex(i))
224
routes = parse_route(response, option, i, length, bytes_recv)
226
gateway = parse_ip_addr(response, option, i, length, bytes_recv)
227
logger.verb("Default gateway:{0}, at {1}", gateway, hex(i))
229
endpoint = parse_ip_addr(response, option, i, length, bytes_recv)
230
logger.verb("Azure wire protocol endpoint:{0}, at {1}", endpoint,
233
logger.verb("Skipping DHCP option:{0} at {1} with length {2}",
234
hex(option), hex(i), hex(length))
236
return endpoint, gateway, routes
238
def socket_send(request):
241
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
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))
248
logger.verb("Send DHCP request: Setting socket.timeout=10, "
250
response = sock.recv(1024)
253
raise DhcpError("{0}".format(e))
258
def build_dhcp_request(mac_addr, request_broadcast):
260
Build DHCP request string.
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 */
282
# UINT8 MessageTypeCode; /* 53 */
283
# UINT8 MessageTypeLength; /* 1 */
284
# UINT8 MessageType; /* 1 for DISCOVER */
285
# UINT8 End; /* 255 */
290
# (struct.pack_into would be good here, but requires Python 2.5)
293
trans_id = gen_trans_id()
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]
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])
305
logger.verb("BuildDhcpRequest: transactionId:%s,%04X" % (
307
unpack_big_endian(request, 4, 4)))
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;
314
# fill in ClientHardwareAddress
315
for a in range(0, 6):
316
request[0x1C + a] = str_to_ord(mac_addr[a])
318
# DHCP Magic Cookie: 99, 130, 83, 99
319
# MessageTypeCode = 53 DHCP Message Type
320
# MessageTypeLength = 1
321
# MessageType = DHCPDISCOVER
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)