Skip to content
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

Added CoAP socket #4334

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open

Added CoAP socket #4334

wants to merge 4 commits into from

Conversation

eHonnef
Copy link
Contributor

@eHonnef eHonnef commented Mar 23, 2024

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.

if sock is None:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((self.ip, self.port))

Check warning

Code scanning / CodeQL

Binding a socket to all network interfaces Medium

'' binds a socket to all interfaces.
Copy link

codecov bot commented Mar 23, 2024

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%) ⬆️
scapy/contrib/coap_socket.py 88.25% <88.25%> (ø)

... and 141 files with indirect coverage changes

@eHonnef eHonnef force-pushed the coap-socket branch 2 times, most recently from 245e388 to 7ee64b2 Compare March 23, 2024 22:10
@eHonnef
Copy link
Contributor Author

eHonnef commented Mar 24, 2024

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.

@gpotter2
Copy link
Member

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.

Fixing response payload

Some docstring and bug fixes.

Finished CoAP server logic implementation

Added client interaction

Client/Server done.

Added delayed response handling

Fixing small problems

Unit tests

Documentation
@eHonnef
Copy link
Contributor Author

eHonnef commented Mar 24, 2024

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

scapy/contrib/coap_socket.py Outdated Show resolved Hide resolved
scapy/contrib/coap_socket.py Show resolved Hide resolved
scapy/contrib/coap_socket.py Outdated Show resolved Hide resolved
- Moved the defines/enumerators to coap.py
- Changed the send() function to match the SuperSocket declaration
- Updated unit tests
@polybassa
Copy link
Contributor

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.

@eHonnef eHonnef changed the title Added CoAP socket Draft: Added CoAP socket May 16, 2024
@eHonnef eHonnef changed the title Draft: Added CoAP socket Added CoAP socket May 16, 2024
@eHonnef
Copy link
Contributor Author

eHonnef commented May 16, 2024

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 :)

@polybassa
Copy link
Contributor

Thanks for the update. I'll review soon.

def hashret(self):
return struct.pack('I', self.msg_id) + self.token

def answers(self, other):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make sense to implement the answers function based on coap_codes / the field "code"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a simple verification where any kind of answer is accepted (within the response codes in the RFC's section 5.9), except if you are trying to answer with another request.

I did this way because I don't want to put too much constraints for the user.

return len(x)

def sr(self, *args, **kargs):
args[0].sport = self.impl.port
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since you want to operate on the packet you get here, I suggest to change the function signature:

def sr(pkt, *args, **kargs):
    pkt[UDP].sport = ...
    ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully I got this correct, did you mean:

def sr(self, pkt, *args, **kargs):
    pkt[UDP] ...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Unfortunately I’m not fully done with the review. I try to find some time as soon as possible.

- Implemented coap.answers function
- Fixed some types
- Remove unnecessary override
- Changed sr and sr1 functions signatures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants