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

Persisting observers #1012

Closed
mkovatsc opened this issue Jan 18, 2023 · 13 comments
Closed

Persisting observers #1012

mkovatsc opened this issue Jan 18, 2023 · 13 comments

Comments

@mkovatsc
Copy link

Some applications including IoT standards such as KNX IoT require that observe relationships can survive reboots of the device. Examples for uses cases are availability notifications (device is up, connected, and operational again) or state notifications over a firmware update cycle (which naturally requires a reboot).

Persisting observe relationships is also related to "unsolicited notifications": Setting up an observe relationship without a request ever arriving at the server is conceptionally similar to recovering previous observe relationships from non-volatile storage / flash during boot.

After looking into this issue, it looks to me like libcoap currently does not provide an API to do this and the dependency on context/session structs makes it hard to implement this from the application side.

The only way to realize persisting observers I have seen so far is to fully replace the observe support of libcoap with a custom one. This approach makes it easy to store and recover (or set up) observe relationships locally, but it must reimplement the resource state change and notification mechanisms.

Is there a way to persist observers or at least to register them locally without an incoming request with its context/session structs? Any pointers that could be given?

The ideal solution would be to add this support (also covering "unsolicited notifications") through the libcoap API.

@mrdeep1
Copy link
Collaborator

mrdeep1 commented Jan 19, 2023

Thanks for raising this.

A CoAP application (client or server) needs to know where to send a packet (response or request) and it gets this information for the coap_session_t structure. Both the IP address and port of the destination need to be known - in particular the port needs to be known for any application running on the destination device.

So in the case of sending off a "unsolicited response - i.e availability/state notifications" libcoap will require a coap_session_t which will have the remote IP/port defined. The API could be extended to do this I guess.

However, once an IP tuple is established (or configured) between two CoAP endpoints, there is nothing that says only one end can be the client and the other the server. Both ends can act as a client/server in their own rights - the client/server relationship between the 2 ends can swap over at any time (even running concurrently with multiplexed requests/responses) - either end can send a request at any time with the other end responding.

So with 1 of the 2 endpoints (Endpoint A - who is your rebooting device) being able to act as both a CoAP client and a CoAP server uses the CoAP client request functionality sends out a request message to the other endpoint (Endpoint B) acting as a CoAP server on a defined port who receives (and potentially acknowledges) the request. At this point, Endpoint B can elect to run as a client and send off a request to get whatever information is normally provided by Endpoint A using the same coap_session_t session.

If Endpoint A uses PUT or POST for the "availability/state notifications" (as a client request), the appropriate data can be easily conveyed for Endpoint B to log/act upon etc.

Endpoint A can also send out the "availability/state notifications" to a multicast address giving even more configuration flexibility.

Doing things this way requires no changes to the libcoap API. I hope this helps.

@mkovatsc
Copy link
Author

Dear Jon, thanks for your reply!

I am aware of the duality of CoAP endpoints. Just changing the the notification to a request to convey the information is not an option. My challenge is that I do not have enough time to do the coding myself, but the developers cannot find it out by themselves. So I was hoping for a good initial pointer from the maintainers to derive a rough "HOWTO".

So what I understand is that it should be enough to allocate a coap_session_t struct and initialize it with just the remote IP address and port, and the stack should be able to send out messages. However, I say to for sure where to hook in to persist information on reception of an Observe request and where to restore the information on boot...

  • To persist observers, is it enough to copy the necessary information (IP addr + port from session, token from the PDU, and request PDU itself) in the COAP_REQUEST_GET handler function? Or is there a challenge with a "deep copy" because of the many pointers? Copying body_offset bytes from *data should be enough to get the headers, no?
  • To restore, could I just call coap_add_observer() from my application code with the persisted information?

@mrdeep1
Copy link
Collaborator

mrdeep1 commented Jan 24, 2023

To set up the coap_session_t, I would use coap_new_client_session() which will fill in all of the necessary information as well as handle any subsequent internal reworking of code. There is nothing to stop using this coap_session_t to send a response. So here I think you will just need to keep the 5-tuple and CoAP protocol from the coap_session_t over a reboot to recreate the coap_session_t.

Server coap_session_t are added to coap_context_t->appropriate_endpoint->sessions with a reference count of 0 and type COAP_SESSION_TYPE_SERVER, where as Client coap_session_t are added to coap_context_t->sessions with a reference count of 1 and type COAP_SESSION_TYPE_CLIENT. Server coap_session_t usually only 'least recently used go away' if memory limits are imposed. Client coap_session_t go away when reference count drops to 0.

There is a coap_session_set_type_client() which converts server type 'coap_session_t' to client type. With care a coap_session_set_type_server() could be written.

coap_add_observer() is not exposed in libcoap as it is defined in coap_subscribe_internal.h. You will need to move the definition into coap_subscribe.h if you want to use it in the application. The parameter list of this function will change over time (#938 for example). It is possible that the token parameter (which can be obtained from the request PDU) may get dropped.

Assuming that you only intend to observe a GET, you will need a coap_session_t set up, and a re-created PDU for the call to coap_add_observer(). For this PDU, you will need to have saved the information from pdu->token (not pdu->data which if set is used for the payload) with length pdu->used_size. As coap_pdu_t is not accessible from an application, then it may be worth writing a wrapper function as a part of libcoap which then invokes coap_add_observe(). I suggest you look at coap_pdu_duplicate() as a starting point (drop_options == NULL path).

@mkovatsc
Copy link
Author

Cool, thanks a lot for this run-through! I will check it against the code and try to hand it over. I will try to come back with results/observations.

@mrdeep1
Copy link
Collaborator

mrdeep1 commented Jan 25, 2023

I've been having a further think about this. This would only work if (D)TLS and TCP are not being used.

If the raw GET observe subscription packet (as seen on the wire) along with the 5-tuple of the request is kept, then this can be used to set up the server coap_session_t and resource (if it does not exist) after the server reboot.

A secondary issue is that the unsolicited response Observe value should continue to be incremented even over a reboot. RFC7641 4.4 Reordering.

In coap_add_observer() add in an optional callback handler typedef int (*coap_observe_added)(coap_session_t *session, coap_subscription_t* observe_key, coap_addr_tuple_t *addr_info, coap_bin_const_t *packet).

In coap_delete_observer() add in an optional callback handler typedef int (*coap_observe_deleted)(coap_session_t *session, coap_subscription_t* observe_key).

Create a new function to set up the above callback handlers (need to be stored in coap_context_t).

Create a new callback handler for saving every n'th unsolicited observe value typedef int (*coap_save_observe_value)(coap_session_t *session, coap_subscription_t *observe_key, uint32_t observe_num).

Create a new function coap_session_t *coap_persist_observe(coap_endpoint_t *ep, coap_addr_tuple_t *addr_info, coap_bin_const_t *packet, coap_method_handler_t get_handler, coap_method_handler_t fetch_handler, int resource_flags, coap_save_observe_value save_callback, uint32_t start_seq_no, uint32_t save_seq_freq). start_seq_no must be set to the last possible seq no that could have been used prior to the reboot (i.e last recorded seq no + save_seq_freq - 1).

This function would parse packet (coap_pdu_parse()), determine the full resource path (coap_get_uri_path(()), add in the resource if it does not exist with appropriate handlers/flags, get (or create if it does not exist) coap_session_t based on addr_info (coap_endpoint_get_session()) and add in the observe request (coap_add_observe()).

If this works for your requirements, it should not take me that long to knock up something for you to test.

@mkovatsc
Copy link
Author

This would only work if (D)TLS and TCP are not being used.

Thought so. I my case, security is enabled via OSCORE -- will see if something is required for that.

For DTLS, it could be interesting to persist long-lasting sessions with connection identifier, I guess. For TCP, I am not sure if there is a "call home" option, where a server-initiated connection could inform about availability and the client simply re-observes.

Observe value should continue to be incremented even over a reboot

In ... add in an optional callback handler ...
Create a new function to set up the above callback handlers (need to be stored in coap_context_t).

Okay, have a function to set these callbacks globally and in coap_add/delete_observer() check if the function pointers were set and, if so, call them.

What do you mean by "need to be stored in coap_context_t"?

Create a new function ... coap_persist_observe ... coap_save_observe_value save_callback

Why pass the coap_save_observe_value here? Couldn't this callback be passed by the callback-setter from above and then update the last recorded seq no per resource, together with the frequency estimate? Or does libcoap tie Observe clock to the client, not just the resource?

@mrdeep1
Copy link
Collaborator

mrdeep1 commented Jan 26, 2023

Thought so. I my case, security is enabled via OSCORE -- will see if something is required for that.

https://www.rfc-editor.org/rfc/rfc8613#appendix-B.2 would cover that, but Appendix B.2 may get superseded.

For DTLS, it could be interesting to persist long-lasting sessions with connection identifier, I guess.

Possible I guess, but would need some work.

TCP, I am not sure if there is a "call home" option, where a server-initiated connection could inform about
availability and the client simply re-observes.

Could be done - as well as for UDP, but what the "response" trigger message is is not immediately obvious.

What do you mean by "need to be stored in coap_context_t"?

The handler locations need to be stored somewhere - in the CoAP context structure coap_context_t.

Why pass the coap_save_observe_value here?

Yes, I realized this morning that this needs to be defined at the resource or session level (so new client observe requests are treated the same way) or perhaps at the coap_context level - so would define this at the context level at same time as the other callback handlers. In addition, save_seq_freq needs to be more globally stored. save_seq_freq is not a frequency per say, but after every n updates to the observe value, save_callback() is invoked.

libcoap just implements incrementing the observe value for each unsolicited response, rather that based on a clock tick. Probably helps here as clock ticks may get reset to 0 on a reboot.

@mrdeep1
Copy link
Collaborator

mrdeep1 commented Jan 27, 2023

Apart from any OSCORE additions required, for the UDP case, I think that this should be the public API

/**
 * Callback handler definition called when a new observe has been set up.
 *
 * @param session The current session.
 * @param observe_key The pointer to the subscription.
 * @param e_proto The CoAP protocol in use for the session / endpoint.
 * @param e_listen_addr The IP/port tthat the endpoint is listening on.
 * @param s_addr_info Local / Remote IP addresses. ports etc. of session.
 * @param raw_packet L7 packet as seen on the wire (could be concatenated if
 *                   Block1 FETCH is being used.
 * @param oscore_info Has OSCORE information if OSCORE is protecting the
 *                    session or NULL if OSCORE is not in use.
 * @param user_data Application provided information from coap_observe_track().
 *
 * @return @c 1 if success else @c 0.
 */
typedef int (*coap_observe_added_t)(coap_session_t *session,
                                    coap_subscription_t *observe_key,
                                    coap_proto_t e_proto,
                                    coap_address_t *e_listen_addr,
                                    coap_addr_tuple_t *s_addr_info,
                                    coap_bin_const_t *raw_packet,
                                    coap_bin_const_t *oscore_info,
                                    void *user_data);

/**
 * Callback handler definition called when an observe is being removed.
 *
 * @param session The current session.
 * @param observe_key The pointer to the subscription.
 * @param user_data Application provided information from coap_observe_track().
 *
 * @return @c 1 if success else @c 0.
 */
typedef int (*coap_observe_deleted_t)(coap_session_t *session,
                                      coap_subscription_t *observe_key,
                                      void *user_data);

/**
 * Callback handler definition called when an observe unsolicited response is
 * being set.
 * Note: This will only get called every save_freq as defined by
 * coap_observe_track().
 *
 * @param resource_name The uri path name of the resource.
 * @param user_data Application provided information from coap_observe_track().
 * @param observe_num The current observe value just sent.
 *
 * @return @c 1 if success else @c 0.
 */
typedef int (*coap_save_observe_value_t)(coap_str_const_t *resource_name,
                                         void *user_data,
                                         uint32_t observe_num);

/**
 * Callback handler definition called when resource is removed that has been
 * observed.
 *
 * @param resource_name The uri path name of the resource.
 * @param user_data Application provided information from coap_observe_track().
 *
 * @return @c 1 if success else @c 0.
 */
typedef int (*coap_del_observe_value_t)(coap_str_const_t *resource_name,
                                        void *user_data);

/**
 * Set up callbacks to handle observe tracking so on a coap-server inadvertant
 * restart existing observe subscriptions can continue.
 *
 * @param context The current CoAP context.
 * @param observe_added Called when a new observe subscription is set up.
 * @param observe_deleted Called when a observe subscription is de-registered.
 * @param save_observe_value Called every @p save_freq so current observe value
 *                           can be tracked.
 * @param del_observe_value Called when a resource is removed.
 * @param save_freq Frequency of change of observe value for calling
 *                  @p save_observe_value
 * @param user_data App defined data (can be NULL) passed into various
 *                  callbacks.
 */
void coap_observe_track(coap_context_t *context,
                           coap_observe_added_t observe_added,
                           coap_observe_deleted_t observe_deleted,
                           coap_save_observe_value_t save_observe_value,
                           coap_del_observe_value_t del_observe_value,
                           uint32_t save_freq,
                           void *user_data);

/**
 * Set up an active subscription for an observe that was previously active
 * over a coap-server inadvertant restart.
 *
 * @param context The context that the session is to be associated with.
 * @param e_proto The CoAP protocol in use for the session / endpoint.
 * @param e_listen_addr The IP/port tthat the endpoint is listening on.
 * @param s_addr_info Local / Remote IP addresses. ports etc. of previous
 *                    session.
 * @param raw_packet L7 packet as seen on the wire (could be concatenated if
 *                   Block1 FETCH is being used.
 * @param oscore_info Has OSCORE information if OSCORE is protecting the
 *                    session or NULL if OSCORE is not in use.
 *
 * @return ptr to subscription if success else @c NULL.
 */
coap_subscription_t *coap_persist_observe(coap_context_t *context,
                                          coap_proto_t e_proto,
                                          const coap_address_t *e_listen_addr,
                                          const coap_addr_tuple_t *s_addr_info,
                                          const coap_bin_const_t *raw_packet,
                                          const coap_bin_const_t *oscore_info);

It would be the responsibility of the server application to set up all of the resources (including any dynamically allocated ones) before coap_persist_observe() is called. To dynamically set up missing resources in coap_persist_observe() is I think a step to far (which handlers are needed etc..). (The observe counter is maintained at the resource level, not the observe level).

@mrdeep1
Copy link
Collaborator

mrdeep1 commented Feb 1, 2023

Please see PR #1019 which support this persist observe functionality for UDP and OSCORE over UDP.

Please check it out and let us know if something is not working, missing or needs clarification documentation etc.

@mkovatsc
Copy link
Author

mkovatsc commented Feb 6, 2023

Dear Jon, thanks a lot for this work -- also impressive speed!
Due to illness, I have not been able to reply and only expect to be back end of this week. I will provide comments on #1019 then.

@mrdeep1
Copy link
Collaborator

mrdeep1 commented Feb 20, 2023

@mkovatsc Just checking in to see if you have had a chance to look at PR #1019?

@mrdeep1
Copy link
Collaborator

mrdeep1 commented Aug 13, 2023

@mkovatsc Any updates on this, or can it be closed now?

@mrdeep1
Copy link
Collaborator

mrdeep1 commented Mar 6, 2024

Closing this issue for now.

@mrdeep1 mrdeep1 closed this as completed Mar 6, 2024
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

No branches or pull requests

2 participants