scapy icon indicating copy to clipboard operation
scapy copied to clipboard

Added CoAP socket

Open eHonnef opened this issue 1 year ago • 7 comments

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.

eHonnef avatar Mar 23 '24 20:03 eHonnef

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%> (ø)

... and 141 files with indirect coverage changes

codecov[bot] avatar Mar 23 '24 20:03 codecov[bot]

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.

eHonnef avatar Mar 24 '24 09:03 eHonnef

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 avatar Mar 24 '24 13:03 gpotter2

@gpotter2 Thanks for the insights, it is ready for reviews :D

eHonnef avatar Mar 24 '24 14:03 eHonnef

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.

polybassa avatar Apr 14 '24 07:04 polybassa

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 in SndRcvHandler::_sndrcv_snd, line 246, it puts the package in the hash by calling the hashret function of IP, then UDP, then CoAP.
  • 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 answer function, where I have to set the sport to return the proper value.

Is there a better way of doing this?

Thanks so far :)

eHonnef avatar May 16 '24 19:05 eHonnef

Thanks for the update. I'll review soon.

polybassa avatar Jul 12 '24 20:07 polybassa