Skip to content

DHCP state machine error handling #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jan 9, 2023
145 changes: 78 additions & 67 deletions adafruit_wiznet5k/adafruit_wiznet5k_dhcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
DHCP_HLENETHERNET = const(0x06)
DHCP_HOPS = const(0x00)

MAGIC_COOKIE = const(0x63825363)
MAGIC_COOKIE = b"c\x82Sc" # Four bytes 99.130.83.99
MAX_DHCP_OPT = const(0x10)

# Default DHCP Server port
Expand Down Expand Up @@ -179,7 +179,7 @@ def send_dhcp_message(
# Transaction ID (xid)
self._initial_xid = htonl(self._transaction_id)
self._initial_xid = self._initial_xid.to_bytes(4, "big")
_BUFF[4:7] = self._initial_xid
_BUFF[4:8] = self._initial_xid

# seconds elapsed
_BUFF[8] = (int(time_elapsed) & 0xFF00) >> 8
Expand All @@ -195,18 +195,15 @@ def send_dhcp_message(
# as they're already set to 0.0.0.0
# Except when renewing, then fill in ciaddr
if renew:
_BUFF[12:15] = bytes(self.local_ip)
_BUFF[12:16] = bytes(self.local_ip)

# chaddr
_BUFF[28:34] = self._mac_address

# NOTE: 192 octets of 0's, BOOTP legacy

# Magic Cookie
_BUFF[236] = (MAGIC_COOKIE >> 24) & 0xFF
_BUFF[237] = (MAGIC_COOKIE >> 16) & 0xFF
_BUFF[238] = (MAGIC_COOKIE >> 8) & 0xFF
_BUFF[239] = MAGIC_COOKIE & 0xFF
_BUFF[236:240] = MAGIC_COOKIE

# Option - DHCP Message Type
_BUFF[240] = 53
Expand Down Expand Up @@ -262,10 +259,10 @@ def send_dhcp_message(
# pylint: disable=too-many-branches, too-many-statements
def parse_dhcp_response(
self,
) -> Union[Tuple[int, bytes], Tuple[int, int]]:
) -> Tuple[int, bytearray]:
"""Parse DHCP response from DHCP server.

:return Union[Tuple[int, bytes], Tuple[int, int]]: DHCP packet type.
:return Tuple[int, bytearray]: DHCP packet type and ID.
"""
# store packet in buffer
_BUFF = self._sock.recv()
Expand All @@ -281,16 +278,16 @@ def parse_dhcp_response(
)

xid = _BUFF[4:8]
if bytes(xid) < self._initial_xid:
print("f")
return 0, 0
if bytes(xid) != self._initial_xid:
raise ValueError("DHCP response ID mismatch.")

self.local_ip = tuple(_BUFF[16:20])
if _BUFF[28:34] == 0:
return 0, 0
# Check that there is a server ID.
if _BUFF[28:34] == b"\x00\x00\x00\x00\x00\x00":
raise ValueError("No DHCP server ID in the response.")

if int.from_bytes(_BUFF[235:240], "big") != MAGIC_COOKIE:
return 0, 0
if _BUFF[236:240] != MAGIC_COOKIE:
raise ValueError("No DHCP Magic Cookie in the response.")

# -- Parse Packet, VARIABLE -- #
ptr = 240
Expand Down Expand Up @@ -323,8 +320,8 @@ def parse_dhcp_response(
ptr += 1
opt_len = _BUFF[ptr]
ptr += 1
self.gateway_ip = tuple(_BUFF[ptr : ptr + opt_len])
ptr += opt_len
self.gateway_ip = tuple(_BUFF[ptr : ptr + 4])
ptr += opt_len # still increment even though we only read 1 addr.
elif _BUFF[ptr] == DNS_SERVERS:
ptr += 1
opt_len = _BUFF[ptr]
Expand Down Expand Up @@ -427,65 +424,79 @@ def _dhcp_state_machine(self) -> None:
if self._sock.available():
if self._debug:
print("* DHCP: Parsing OFFER")
msg_type, xid = self.parse_dhcp_response()
if msg_type == DHCP_OFFER:
# Check if transaction ID matches, otherwise it may be an offer
# for another device
if htonl(self._transaction_id) == int.from_bytes(xid, "big"):
if self._debug:
print(
"* DHCP: Send request to {}".format(self.dhcp_server_ip)
try:
msg_type, xid = self.parse_dhcp_response()
except ValueError as error:
if self._debug:
print(error)
else:
if msg_type == DHCP_OFFER:
# Check if transaction ID matches, otherwise it may be an offer
# for another device
if htonl(self._transaction_id) == int.from_bytes(xid, "big"):
if self._debug:
print(
"* DHCP: Send request to {}".format(
self.dhcp_server_ip
)
)
self._transaction_id = (
self._transaction_id + 1
) & 0x7FFFFFFF
self.send_dhcp_message(
DHCP_REQUEST, (time.monotonic() - self._start_time)
)
self._transaction_id = (self._transaction_id + 1) & 0x7FFFFFFF
self.send_dhcp_message(
DHCP_REQUEST, (time.monotonic() - self._start_time)
)
self._dhcp_state = STATE_DHCP_REQUEST
self._dhcp_state = STATE_DHCP_REQUEST
else:
if self._debug:
print("* DHCP: Received OFFER with non-matching xid")
else:
if self._debug:
print("* DHCP: Received OFFER with non-matching xid")
else:
if self._debug:
print("* DHCP: Received DHCP Message is not OFFER")
print("* DHCP: Received DHCP Message is not OFFER")

elif self._dhcp_state == STATE_DHCP_REQUEST:
if self._sock.available():
if self._debug:
print("* DHCP: Parsing ACK")
msg_type, xid = self.parse_dhcp_response()
# Check if transaction ID matches, otherwise it may be
# for another device
if htonl(self._transaction_id) == int.from_bytes(xid, "big"):
if msg_type == DHCP_ACK:
if self._debug:
print("* DHCP: Successful lease")
self._sock.close()
self._sock = None
self._dhcp_state = STATE_DHCP_LEASED
self._last_lease_time = self._start_time
if self._lease_time == 0:
self._lease_time = DEFAULT_LEASE_TIME
if self._t1 == 0:
# T1 is 50% of _lease_time
self._t1 = self._lease_time >> 1
if self._t2 == 0:
# T2 is 87.5% of _lease_time
self._t2 = self._lease_time - (self._lease_time >> 3)
self._renew_in_sec = self._t1
self._rebind_in_sec = self._t2
self._eth.ifconfig = (
self.local_ip,
self.subnet_mask,
self.gateway_ip,
self.dns_server_ip,
)
gc.collect()
try:
msg_type, xid = self.parse_dhcp_response()
except ValueError as error:
if self._debug:
print(error)
else:
# Check if transaction ID matches, otherwise it may be
# for another device
if htonl(self._transaction_id) == int.from_bytes(xid, "big"):
if msg_type == DHCP_ACK:
if self._debug:
print("* DHCP: Successful lease")
self._sock.close()
self._sock = None
self._dhcp_state = STATE_DHCP_LEASED
self._last_lease_time = self._start_time
if self._lease_time == 0:
self._lease_time = DEFAULT_LEASE_TIME
if self._t1 == 0:
# T1 is 50% of _lease_time
self._t1 = self._lease_time >> 1
if self._t2 == 0:
# T2 is 87.5% of _lease_time
self._t2 = self._lease_time - (self._lease_time >> 3)
self._renew_in_sec = self._t1
self._rebind_in_sec = self._t2
self._eth.ifconfig = (
self.local_ip,
self.subnet_mask,
self.gateway_ip,
self.dns_server_ip,
)
gc.collect()
else:
if self._debug:
print("* DHCP: Received DHCP Message is not ACK")
else:
if self._debug:
print("* DHCP: Received DHCP Message is not ACK")
else:
if self._debug:
print("* DHCP: Received non-matching xid")
print("* DHCP: Received non-matching xid")

elif self._dhcp_state == STATE_DHCP_WAIT:
if time.monotonic() > (self._start_time + DHCP_WAIT_TIME):
Expand Down
138 changes: 138 additions & 0 deletions examples/wiznet5k_wsgiservertest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# SPDX-FileCopyrightText: 2021 Patrick Van Oosterwijck @ Silicognition LLC
#
# SPDX-License-Identifier: MIT
#
# This demo was tested with the PoE-FeatherWing, which contains a 24AA02E48
# chip to provide a globally unique MAC address, but can also work without
# this chip for testing purposes by using a hard coded MAC.
#
# It also contains a `get_static_file` function that demonstrates how to
# use a generator to serve large static files without using up too much
# memory. To avoid having to put extra files in the repo, it just serves
# `code.py` which isn't very large, but to properly test it, adjust the code
# to serve an image of several 100 kB to see how it works.
#
# There's also an endpoint that demonstrates that `requests` can be used to
# get data from another socket and serve it.
#

import board
import busio
import digitalio
import neopixel

import adafruit_requests as requests
from adafruit_wsgi.wsgi_app import WSGIApp
from adafruit_wiznet5k.adafruit_wiznet5k import WIZNET5K
import adafruit_wiznet5k.adafruit_wiznet5k_socket as socket
import adafruit_wiznet5k.adafruit_wiznet5k_wsgiserver as server


print("Wiznet5k Web Server Test")


def get_mac(i2c_obj):
"Read MAC from 24AA02E48 chip and return it"
mac_addr = bytearray(6)
while not i2c_obj.try_lock():
pass
i2c_obj.writeto(0x50, bytearray((0xFA,)))
i2c_obj.readfrom_into(0x50, mac_addr, start=0, end=6)
i2c_obj.unlock()
return mac_addr


def get_static_file(filename):
"Static file generator"
with open(filename, "rb") as f:
b = None
while b is None or len(b) == 2048:
b = f.read(2048)
yield b


# Status LED
led = neopixel.NeoPixel(board.NEOPIXEL, 1)
led.brightness = 0.3
led[0] = (255, 0, 0)

# Chip Select for PoE-FeatherWing and Adafruit Ethernet FeatherWing
cs = digitalio.DigitalInOut(board.D10)
# Chip Select for Particle Ethernet FeatherWing
# cs = digitalio.DigitalInOut(board.D5)

# Initialize SPI bus
spi_bus = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)

try:
# Initialize the I2C bus to read the MAC
i2c = busio.I2C(board.SCL, board.SDA)
# Read the MAC from the 24AA02E48 chip
mac = get_mac(i2c)
except (RuntimeError, OSError):
# Hard coded MAC if there is no 24AA02E48
mac = b"\xFE\xED\xDE\xAD\xBE\xEF"

# Initialize Ethernet interface with DHCP
eth = WIZNET5K(spi_bus, cs, mac=mac)

# Initialize a requests object with a socket and ethernet interface
requests.set_socket(socket, eth)


# Here we create our application, registering the
# following functions to be called on specific HTTP GET requests routes

web_app = WSGIApp()


@web_app.route("/led/<r>/<g>/<b>")
def led_on(request, r, g, b): # pylint: disable=unused-argument
print("LED handler")
led.fill((int(r), int(g), int(b)))
return ("200 OK", [], ["LED set!"])


@web_app.route("/")
def root(request): # pylint: disable=unused-argument
print("Root WSGI handler")
return ("200 OK", [], ["Root document"])


@web_app.route("/large")
def large(request): # pylint: disable=unused-argument
print("Large pattern handler")
return ("200 OK", [], ["*-.-" * 2000])


@web_app.route("/code")
def code(request): # pylint: disable=unused-argument
print("Static file code.py handler")
return ("200 OK", [], get_static_file("code.py"))


@web_app.route("/btc")
def btc(request): # pylint: disable=unused-argument
print("BTC handler")
r = requests.get("http://api.coindesk.com/v1/bpi/currentprice/USD.json")
result = r.text
r.close()
return ("200 OK", [], [result])


# Here we setup our server, passing in our web_app as the application
server.set_interface(eth)
wsgiServer = server.WSGIServer(80, application=web_app)

print("Open this IP in your browser: ", eth.pretty_ip(eth.ip_address))

# Start the server
wsgiServer.start()
led[0] = (0, 0, 255)

while True:
# Our main loop where we have the server poll for incoming requests
wsgiServer.update_poll()
# Maintain DHCP lease
eth.maintain_dhcp_lease()
# Could do any other background tasks here, like reading sensors
1 change: 1 addition & 0 deletions optional_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# SPDX-FileCopyrightText: 2022 Alec Delaney, for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
pytest
Loading