scapy
scapy copied to clipboard
Added CoAP socket
Description
This PR implements a CoAP socket, pretty similar on how ISOTPSoftSocket works. I implemented the basic message exchange, mostly based on the RFC-7252.
- Congestion control
- Retransmission mechanism
- Separate responses
- Message duplication detection
Known-limitations
- No POST and DELETE methods
- No DTLS
- No discovery via multicast/broadcast, although you can still bind to one of these interfaces
- No observer
- The SR/SR1 functions cannot handle separate responses.
General comments
It has a dependency for from scapy.contrib.isotp.isotp_soft_socket import TimeoutScheduler, I found nice how this is implemented, so I just used it, I didn't want to copy/paste again.
Also I added some unit tests for the basic cases.
Quick usage
Client example:
>>> with CoAPSocket("127.0.0.1", 1234) as coap_client:
>>> req = CoAPSocket.make_coap_req_packet(method=GET, uri="endpoint-uri", payload=b"")
>>> coap_client.send("127.0.0.1", 5683, req)
>>> res = coap_client.recv() # Careful, this will block until the coap_client receives something
Server without specifying resources:
>>> with CoAPSocket("127.0.0.1", 5683) as coap_server:
>>> while True:
>>> pkg = coap_server.recv()
>>> handle_package(pkg)
Server with custom resources:
>>> class DummyResource(CoAPResource):
>>> def get(self, payload, options, token, sa_ll):
>>> return {"type": ACK, "code": CONTENT_205, "options": [(CONTENT_FORMAT, CF_TEXT_PLAIN)], "payload": b'dummy response'}
>>>
>>> class DelayedResource(CoAPResource):
>>> def __init__(self, url):
>>> CoAPResource.__init__(self, url=url)
>>> self.delayed_tokens = []
>>>
>>> def delayed_message(self):
>>> token, address = self.delayed_tokens.pop(0)
>>> pkt = CoAPSocket.make_delayed_resp_packet(token, [(CONTENT_FORMAT, CF_TEXT_PLAIN)], b"delayed payload")
>>> self._send_separate_response(pkt, address)
>>>
>>> def get(self, payload, options, token, sa_ll):
>>> # We know that this can take a while, so we return an empty ACK now and wait for whatever resource to be available.
>>> TimeoutScheduler.schedule(1, self.delayed_message)
>>> self.delayed_tokens.append((token, sa_ll))
>>> return CoAPSocket.empty_ack_params()
>>>
>>> # Doesn't matter if it starts with "/dummy" or "dummy", but it is an error if it is in the end
>>> lst_resources = [DummyResource("dummy"), DelayedResource("/delayed")].
>>> with CoAPSocket("127.0.0.1", 5683, lst_resources=lst_resources) as coap_socket:
>>> while True:
>>> pkg = coap_socket.recv()
>>> # You can handle the packages inside your resources, here will only be the "unhandled" ones.
Codecov Report
Attention: Patch coverage is 89.01602% with 48 lines in your changes missing coverage. Please review.
Project coverage is 81.53%. Comparing base (
6294c6e) to head (73ce0b0). Report is 193 commits behind head on master.
Additional details and impacted files
@@ Coverage Diff @@
## master #4334 +/- ##
==========================================
- Coverage 81.90% 81.53% -0.38%
==========================================
Files 330 356 +26
Lines 76380 85140 +8760
==========================================
+ Hits 62561 69416 +6855
- Misses 13819 15724 +1905
| Files | Coverage Δ | |
|---|---|---|
| scapy/contrib/coap.py | 95.91% <97.29%> (+0.42%) |
:arrow_up: |
| scapy/contrib/coap_socket.py | 88.25% <88.25%> (ø) |
Any idea on how I can fix these failed tests? If I try to run on my machine without root, it works, so I don't know really how to debug this.
Tests are failing on versions older than 3.9. You're using 3.9+ specific features. Scapy should work with anything starting from 3.7, so this probably needs to be updated.
@gpotter2 Thanks for the insights, it is ready for reviews :D
Could you please implement a select function for your socket, so that sr, sr1 and sniff will work. Could you please also provide unit tests for these functions.
Hello, thanks for the review, I updated the code to include a select function, I'm not sure about the error in the macos tests.
About the changes, I'm not sure if those can be considered "correct", it feels like workarounds. Because:
- To send a package you have to assemble as
IP / UDP / CoAP, therefore inSndRcvHandler::_sndrcv_snd, line 246, it puts the package in the hash by calling thehashretfunction ofIP, thenUDP, thenCoAP. - When I receive the response, it will not receive the full
IP / UDP / CoAP, I have to "manually" assemble this packet, so the hashes will match. - The same logic for the
answerfunction, where I have to set thesportto return the proper value.
Is there a better way of doing this?
Thanks so far :)
Thanks for the update. I'll review soon.