Skip to content

Commit

Permalink
Merge pull request #80 from BiffoBear/dhcp_error_handling
Browse files Browse the repository at this point in the history
DHCP state machine error handling
  • Loading branch information
FoamyGuy authored Jan 9, 2023
2 parents f97e78c + 6bc9f63 commit 1b8ff4e
Show file tree
Hide file tree
Showing 4 changed files with 729 additions and 67 deletions.
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

0 comments on commit 1b8ff4e

Please sign in to comment.