xref: /aosp_15_r20/external/autotest/client/cros/dhcp_test_server.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li# Lint as: python2, python3
2*9c5db199SXin Li# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
4*9c5db199SXin Li# found in the LICENSE file.
5*9c5db199SXin Li
6*9c5db199SXin Li"""
7*9c5db199SXin LiProgrammable testing DHCP server.
8*9c5db199SXin Li
9*9c5db199SXin LiSimple DHCP server you can program with expectations of future packets and
10*9c5db199SXin Liresponses to those packets.  The server is basically a thin wrapper around a
11*9c5db199SXin Liserver socket with some utility logic to make setting up tests easier.  To write
12*9c5db199SXin Lia test, you start a server, construct a sequence of handling rules.
13*9c5db199SXin Li
14*9c5db199SXin LiHandling rules let you set up expectations of future packets of certain types.
15*9c5db199SXin LiHandling rules are processed in order, and only the first remaining handler
16*9c5db199SXin Lihandles a given packet.  In theory you could write the entire test into a single
17*9c5db199SXin Lihandling rule and keep an internal state machine for how far that handler has
18*9c5db199SXin Ligotten through the test.  This would be poor style however.  Correct style is to
19*9c5db199SXin Liwrite (or reuse) a handler for each packet the server should see, leading us to
20*9c5db199SXin Lia happy land where any conceivable packet handler has already been written for
21*9c5db199SXin Lius.
22*9c5db199SXin Li
23*9c5db199SXin LiExample usage:
24*9c5db199SXin Li
25*9c5db199SXin Li# Start up the DHCP server, which will ignore packets until a test is started
26*9c5db199SXin Liserver = DhcpTestServer(interface=interface_name)
27*9c5db199SXin Liserver.start()
28*9c5db199SXin Li
29*9c5db199SXin Li# Given a list of handling rules, start a test with a 30 sec timeout.
30*9c5db199SXin Lihandling_rules = []
31*9c5db199SXin Lihandling_rules.append(DhcpHandlingRule_RespondToDiscovery(intended_ip,
32*9c5db199SXin Li                                                          intended_subnet_mask,
33*9c5db199SXin Li                                                          dhcp_server_ip,
34*9c5db199SXin Li                                                          lease_time_seconds)
35*9c5db199SXin Liserver.start_test(handling_rules, 30.0)
36*9c5db199SXin Li
37*9c5db199SXin Li# Trigger DHCP clients to do various test related actions
38*9c5db199SXin Li...
39*9c5db199SXin Li
40*9c5db199SXin Li# Get results
41*9c5db199SXin Liserver.wait_for_test_to_finish()
42*9c5db199SXin Liif (server.last_test_passed):
43*9c5db199SXin Li    ...
44*9c5db199SXin Lielse:
45*9c5db199SXin Li    ...
46*9c5db199SXin Li
47*9c5db199SXin Li
48*9c5db199SXin LiNote that if you make changes, make sure that the tests in dhcp_unittest.py
49*9c5db199SXin Listill pass.
50*9c5db199SXin Li"""
51*9c5db199SXin Li
52*9c5db199SXin Lifrom __future__ import absolute_import
53*9c5db199SXin Lifrom __future__ import division
54*9c5db199SXin Lifrom __future__ import print_function
55*9c5db199SXin Li
56*9c5db199SXin Liimport logging
57*9c5db199SXin Liimport six
58*9c5db199SXin Lifrom six.moves import range
59*9c5db199SXin Liimport socket
60*9c5db199SXin Liimport threading
61*9c5db199SXin Liimport time
62*9c5db199SXin Liimport traceback
63*9c5db199SXin Li
64*9c5db199SXin Lifrom autotest_lib.client.cros import dhcp_packet
65*9c5db199SXin Lifrom autotest_lib.client.cros import dhcp_handling_rule
66*9c5db199SXin Li
67*9c5db199SXin Li# From socket.h
68*9c5db199SXin LiSO_BINDTODEVICE = 25
69*9c5db199SXin Li
70*9c5db199SXin Li# These imports are purely for handling of namespaces
71*9c5db199SXin Liimport os
72*9c5db199SXin Liimport subprocess
73*9c5db199SXin Lifrom ctypes import CDLL, get_errno
74*9c5db199SXin Lifrom ctypes.util import find_library
75*9c5db199SXin Li
76*9c5db199SXin Li
77*9c5db199SXin Li# Let's throw an exception (with formatted error message) in case of
78*9c5db199SXin Li# 'setns' failure instead of returning an error code
79*9c5db199SXin Lidef errcheck(ret, func, args):
80*9c5db199SXin Li    if ret == -1:
81*9c5db199SXin Li        e = get_errno()
82*9c5db199SXin Li        raise OSError(e, os.strerror(e))
83*9c5db199SXin Li
84*9c5db199SXin Li
85*9c5db199SXin Lilibc = CDLL(find_library('c'))
86*9c5db199SXin Lilibc.setns.errcheck = errcheck
87*9c5db199SXin LiCLONE_NEWNET = 0x40000000
88*9c5db199SXin Li
89*9c5db199SXin Li
90*9c5db199SXin Liclass DhcpTestServer(threading.Thread):
91*9c5db199SXin Li    def __init__(self,
92*9c5db199SXin Li                 interface=None,
93*9c5db199SXin Li                 ingress_address="<broadcast>",
94*9c5db199SXin Li                 ingress_port=67,
95*9c5db199SXin Li                 broadcast_address="255.255.255.255",
96*9c5db199SXin Li                 broadcast_port=68,
97*9c5db199SXin Li                 namespace=None):
98*9c5db199SXin Li        super(DhcpTestServer, self).__init__()
99*9c5db199SXin Li        self._mutex = threading.Lock()
100*9c5db199SXin Li        self._ingress_address = ingress_address
101*9c5db199SXin Li        self._ingress_port = ingress_port
102*9c5db199SXin Li        self._broadcast_port = broadcast_port
103*9c5db199SXin Li        self._broadcast_address = broadcast_address
104*9c5db199SXin Li        self._socket = None
105*9c5db199SXin Li        self._interface = interface
106*9c5db199SXin Li        self._namespace = namespace
107*9c5db199SXin Li        self._stopped = False
108*9c5db199SXin Li        self._test_in_progress = False
109*9c5db199SXin Li        self._last_test_passed = False
110*9c5db199SXin Li        self._test_timeout = 0
111*9c5db199SXin Li        self._handling_rules = []
112*9c5db199SXin Li        self._logger = logging.getLogger("dhcp.test_server")
113*9c5db199SXin Li        self._exception = None
114*9c5db199SXin Li        self.daemon = False
115*9c5db199SXin Li
116*9c5db199SXin Li    @property
117*9c5db199SXin Li    def stopped(self):
118*9c5db199SXin Li        with self._mutex:
119*9c5db199SXin Li            return self._stopped
120*9c5db199SXin Li
121*9c5db199SXin Li    @property
122*9c5db199SXin Li    def is_healthy(self):
123*9c5db199SXin Li        with self._mutex:
124*9c5db199SXin Li            return self._socket is not None
125*9c5db199SXin Li
126*9c5db199SXin Li    @property
127*9c5db199SXin Li    def test_in_progress(self):
128*9c5db199SXin Li        with self._mutex:
129*9c5db199SXin Li            return self._test_in_progress
130*9c5db199SXin Li
131*9c5db199SXin Li    @property
132*9c5db199SXin Li    def last_test_passed(self):
133*9c5db199SXin Li        with self._mutex:
134*9c5db199SXin Li            return self._last_test_passed
135*9c5db199SXin Li
136*9c5db199SXin Li    @property
137*9c5db199SXin Li    def current_rule(self):
138*9c5db199SXin Li        """
139*9c5db199SXin Li        Return the currently active DhcpHandlingRule.
140*9c5db199SXin Li        """
141*9c5db199SXin Li        with self._mutex:
142*9c5db199SXin Li            return self._handling_rules[0]
143*9c5db199SXin Li
144*9c5db199SXin Li    def start(self):
145*9c5db199SXin Li        """
146*9c5db199SXin Li        Start the DHCP server.  Only call this once.
147*9c5db199SXin Li        """
148*9c5db199SXin Li        if self.is_alive():
149*9c5db199SXin Li            return False
150*9c5db199SXin Li        self._logger.info("DhcpTestServer started; opening sockets.")
151*9c5db199SXin Li        try:
152*9c5db199SXin Li            if self._namespace:
153*9c5db199SXin Li                self._logger.info("Moving to namespace %s.", self._namespace)
154*9c5db199SXin Li                # Figure out where the mount bind is - ChromeOS does not
155*9c5db199SXin Li                # follow standard /var/run/netns path so lets try to be more
156*9c5db199SXin Li                # generic and get it from runtime
157*9c5db199SXin Li                tgtpath = subprocess.check_output('mount | grep "netns/%s"' %
158*9c5db199SXin Li                                                  self._namespace,
159*9c5db199SXin Li                                                  shell=True).split()[2]
160*9c5db199SXin Li                self._tgtns = open(tgtpath)
161*9c5db199SXin Li                self._myns = open('/proc/self/ns/net')
162*9c5db199SXin Li                libc.setns(self._tgtns.fileno(), CLONE_NEWNET)
163*9c5db199SXin Li            self._logger.info("Opening socket on '%s' port %d.",
164*9c5db199SXin Li                              self._ingress_address, self._ingress_port)
165*9c5db199SXin Li            self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
166*9c5db199SXin Li            self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
167*9c5db199SXin Li            self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
168*9c5db199SXin Li            if self._interface is not None:
169*9c5db199SXin Li                self._logger.info("Binding to %s", self._interface)
170*9c5db199SXin Li                if six.PY2:
171*9c5db199SXin Li                    self._socket.setsockopt(socket.SOL_SOCKET, SO_BINDTODEVICE,
172*9c5db199SXin Li                                            self._interface)
173*9c5db199SXin Li                else:
174*9c5db199SXin Li                    self._socket.setsockopt(
175*9c5db199SXin Li                            socket.SOL_SOCKET, SO_BINDTODEVICE,
176*9c5db199SXin Li                            self._interface.encode('ISO-8859-1'))
177*9c5db199SXin Li            self._socket.bind((self._ingress_address, self._ingress_port))
178*9c5db199SXin Li            # Wait 100 ms for a packet, then return, thus keeping the thread
179*9c5db199SXin Li            # active but mostly idle.
180*9c5db199SXin Li            self._socket.settimeout(0.1)
181*9c5db199SXin Li        except socket.error as socket_error:
182*9c5db199SXin Li            self._logger.error("Socket error: %s.", str(socket_error))
183*9c5db199SXin Li            self._logger.error(traceback.format_exc())
184*9c5db199SXin Li            if not self._socket is None:
185*9c5db199SXin Li                self._socket.close()
186*9c5db199SXin Li            self._socket = None
187*9c5db199SXin Li            self._logger.error("Failed to open server socket.  Aborting.")
188*9c5db199SXin Li            return
189*9c5db199SXin Li        except OSError as os_err:
190*9c5db199SXin Li            self._logger.error("System error: %s.", str(os_err))
191*9c5db199SXin Li            self._logger.error(traceback.format_exc())
192*9c5db199SXin Li            self._logger.error("Failed to change namespace.  Aborting.")
193*9c5db199SXin Li            return
194*9c5db199SXin Li        finally:
195*9c5db199SXin Li            if self._namespace:
196*9c5db199SXin Li                self._tgtns.close()
197*9c5db199SXin Li                libc.setns(self._myns.fileno(), CLONE_NEWNET)
198*9c5db199SXin Li                self._myns.close()
199*9c5db199SXin Li        super(DhcpTestServer, self).start()
200*9c5db199SXin Li
201*9c5db199SXin Li    def stop(self):
202*9c5db199SXin Li        """
203*9c5db199SXin Li        Stop the DHCP server and free its socket.
204*9c5db199SXin Li        """
205*9c5db199SXin Li        with self._mutex:
206*9c5db199SXin Li            self._stopped = True
207*9c5db199SXin Li
208*9c5db199SXin Li    def start_test(self, handling_rules, test_timeout_seconds):
209*9c5db199SXin Li        """
210*9c5db199SXin Li        Start a new test using |handling_rules|.  The server will call the
211*9c5db199SXin Li        test successfull if it receives a RESPONSE_IGNORE_SUCCESS (or
212*9c5db199SXin Li        RESPONSE_RESPOND_SUCCESS) from a handling_rule before
213*9c5db199SXin Li        |test_timeout_seconds| passes.  If the timeout passes without that
214*9c5db199SXin Li        message, the server runs out of handling rules, or a handling rule
215*9c5db199SXin Li        return RESPONSE_FAIL, the test is ended and marked as not passed.
216*9c5db199SXin Li
217*9c5db199SXin Li        All packets received before start_test() is called are received and
218*9c5db199SXin Li        ignored.
219*9c5db199SXin Li        """
220*9c5db199SXin Li        with self._mutex:
221*9c5db199SXin Li            self._test_timeout = time.time() + test_timeout_seconds
222*9c5db199SXin Li            self._handling_rules = handling_rules
223*9c5db199SXin Li            self._test_in_progress = True
224*9c5db199SXin Li            self._last_test_passed = False
225*9c5db199SXin Li            self._exception = None
226*9c5db199SXin Li
227*9c5db199SXin Li    def wait_for_test_to_finish(self):
228*9c5db199SXin Li        """
229*9c5db199SXin Li        Block on the test finishing in a CPU friendly way.  Timeouts, successes,
230*9c5db199SXin Li        and failures count as finishes.
231*9c5db199SXin Li        """
232*9c5db199SXin Li        while self.test_in_progress:
233*9c5db199SXin Li            time.sleep(0.1)
234*9c5db199SXin Li        if self._exception:
235*9c5db199SXin Li            raise self._exception
236*9c5db199SXin Li
237*9c5db199SXin Li    def abort_test(self):
238*9c5db199SXin Li        """
239*9c5db199SXin Li        Abort a test prematurely, counting the test as a failure.
240*9c5db199SXin Li        """
241*9c5db199SXin Li        with self._mutex:
242*9c5db199SXin Li            self._logger.info("Manually aborting test.")
243*9c5db199SXin Li            self._end_test_unsafe(False)
244*9c5db199SXin Li
245*9c5db199SXin Li    def _teardown(self):
246*9c5db199SXin Li        with self._mutex:
247*9c5db199SXin Li            self._socket.close()
248*9c5db199SXin Li            self._socket = None
249*9c5db199SXin Li
250*9c5db199SXin Li    def _end_test_unsafe(self, passed):
251*9c5db199SXin Li        if not self._test_in_progress:
252*9c5db199SXin Li            return
253*9c5db199SXin Li        if passed:
254*9c5db199SXin Li            self._logger.info("DHCP server says test passed.")
255*9c5db199SXin Li        else:
256*9c5db199SXin Li            self._logger.info("DHCP server says test failed.")
257*9c5db199SXin Li        self._test_in_progress = False
258*9c5db199SXin Li        self._last_test_passed = passed
259*9c5db199SXin Li
260*9c5db199SXin Li    def _send_response_unsafe(self, packet):
261*9c5db199SXin Li        if packet is None:
262*9c5db199SXin Li            self._logger.error("Handling rule failed to return a packet.")
263*9c5db199SXin Li            return False
264*9c5db199SXin Li        self._logger.debug("Sending response: %s", packet)
265*9c5db199SXin Li        binary_string = packet.to_binary_string()
266*9c5db199SXin Li        if binary_string is None or len(binary_string) < 1:
267*9c5db199SXin Li            self._logger.error("Packet failed to serialize to binary string.")
268*9c5db199SXin Li            return False
269*9c5db199SXin Li
270*9c5db199SXin Li        self._socket.sendto(binary_string,
271*9c5db199SXin Li                            (self._broadcast_address, self._broadcast_port))
272*9c5db199SXin Li        return True
273*9c5db199SXin Li
274*9c5db199SXin Li    def _loop_body(self):
275*9c5db199SXin Li        with self._mutex:
276*9c5db199SXin Li            if self._test_in_progress and self._test_timeout < time.time():
277*9c5db199SXin Li                # The test has timed out, so we abort it.  However, we should
278*9c5db199SXin Li                # continue to accept packets, so we fall through.
279*9c5db199SXin Li                self._logger.error("Test in progress has timed out.")
280*9c5db199SXin Li                self._end_test_unsafe(False)
281*9c5db199SXin Li            try:
282*9c5db199SXin Li                data, _ = self._socket.recvfrom(1024)
283*9c5db199SXin Li                self._logger.info("Server received packet of length %d.",
284*9c5db199SXin Li                                  len(data))
285*9c5db199SXin Li            except socket.timeout:
286*9c5db199SXin Li                # No packets available, lets return and see if the server has
287*9c5db199SXin Li                # been shut down in the meantime.
288*9c5db199SXin Li                return
289*9c5db199SXin Li
290*9c5db199SXin Li            # Receive packets when no test is in progress, just don't process
291*9c5db199SXin Li            # them.
292*9c5db199SXin Li            if not self._test_in_progress:
293*9c5db199SXin Li                return
294*9c5db199SXin Li
295*9c5db199SXin Li            packet = dhcp_packet.DhcpPacket(byte_str=data)
296*9c5db199SXin Li            if not packet.is_valid:
297*9c5db199SXin Li                self._logger.warning("Server received an invalid packet over a "
298*9c5db199SXin Li                                     "DHCP port?")
299*9c5db199SXin Li                return
300*9c5db199SXin Li
301*9c5db199SXin Li            logging.debug("Server received a DHCP packet: %s.", packet)
302*9c5db199SXin Li            if len(self._handling_rules) < 1:
303*9c5db199SXin Li                self._logger.info("No handling rule for packet: %s.",
304*9c5db199SXin Li                                  str(packet))
305*9c5db199SXin Li                self._end_test_unsafe(False)
306*9c5db199SXin Li                return
307*9c5db199SXin Li
308*9c5db199SXin Li            handling_rule = self._handling_rules[0]
309*9c5db199SXin Li            response_code = handling_rule.handle(packet)
310*9c5db199SXin Li            logging.info("Handler gave response: %d", response_code)
311*9c5db199SXin Li            if response_code & dhcp_handling_rule.RESPONSE_POP_HANDLER:
312*9c5db199SXin Li                self._handling_rules.pop(0)
313*9c5db199SXin Li
314*9c5db199SXin Li            if response_code & dhcp_handling_rule.RESPONSE_HAVE_RESPONSE:
315*9c5db199SXin Li                for response_instance in range(
316*9c5db199SXin Li                        handling_rule.response_packet_count):
317*9c5db199SXin Li                    response = handling_rule.respond(packet)
318*9c5db199SXin Li                    if not self._send_response_unsafe(response):
319*9c5db199SXin Li                        self._logger.error(
320*9c5db199SXin Li                                "Failed to send packet, ending test.")
321*9c5db199SXin Li                        self._end_test_unsafe(False)
322*9c5db199SXin Li                        return
323*9c5db199SXin Li
324*9c5db199SXin Li            if response_code & dhcp_handling_rule.RESPONSE_TEST_FAILED:
325*9c5db199SXin Li                self._logger.info("Handling rule %s rejected packet %s.",
326*9c5db199SXin Li                                  (handling_rule, packet))
327*9c5db199SXin Li                self._end_test_unsafe(False)
328*9c5db199SXin Li                return
329*9c5db199SXin Li
330*9c5db199SXin Li            if response_code & dhcp_handling_rule.RESPONSE_TEST_SUCCEEDED:
331*9c5db199SXin Li                self._end_test_unsafe(True)
332*9c5db199SXin Li                return
333*9c5db199SXin Li
334*9c5db199SXin Li    def run(self):
335*9c5db199SXin Li        """
336*9c5db199SXin Li        Main method of the thread.  Never call this directly, since it assumes
337*9c5db199SXin Li        some setup done in start().
338*9c5db199SXin Li        """
339*9c5db199SXin Li        with self._mutex:
340*9c5db199SXin Li            if self._socket is None:
341*9c5db199SXin Li                self._logger.error("Failed to create server socket, exiting.")
342*9c5db199SXin Li                return
343*9c5db199SXin Li
344*9c5db199SXin Li        self._logger.info("DhcpTestServer entering handling loop.")
345*9c5db199SXin Li        while not self.stopped:
346*9c5db199SXin Li            try:
347*9c5db199SXin Li                self._loop_body()
348*9c5db199SXin Li                # Python does not have waiting queues on Lock objects.
349*9c5db199SXin Li                # Give other threads a change to hold the mutex by
350*9c5db199SXin Li                # forcibly releasing the GIL while we sleep.
351*9c5db199SXin Li                time.sleep(0.01)
352*9c5db199SXin Li            except Exception as e:
353*9c5db199SXin Li                with self._mutex:
354*9c5db199SXin Li                    self._end_test_unsafe(False)
355*9c5db199SXin Li                    self._exception = e
356*9c5db199SXin Li        with self._mutex:
357*9c5db199SXin Li            self._end_test_unsafe(False)
358*9c5db199SXin Li        self._logger.info("DhcpTestServer closing sockets.")
359*9c5db199SXin Li        self._teardown()
360*9c5db199SXin Li        self._logger.info("DhcpTestServer exiting.")
361