diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 9261d60d5f..2c5b8ec94f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -62,7 +62,7 @@ repos:
     hooks:
       - id: commit message scopes
         name: "commit message must be scoped with: mdns, modem, websocket, asio, mqtt_cxx, console, common"
-        entry: '\A(?!(feat|fix|ci|bump|test|docs)\((mdns|modem|common|console|websocket|asio|mqtt_cxx|examples)\)\:)'
+        entry: '\A(?!(feat|fix|ci|bump|test|docs)\((mdns|modem|common|console|websocket|asio|mqtt_cxx|examples|eppp_link)\)\:)'
         language: pygrep
         args: [--multiline]
         stages: [commit-msg]
diff --git a/components/eppp_link/.cz.yaml b/components/eppp_link/.cz.yaml
new file mode 100644
index 0000000000..abdb707180
--- /dev/null
+++ b/components/eppp_link/.cz.yaml
@@ -0,0 +1,8 @@
+---
+commitizen:
+  bump_message: 'bump(eppp_link): $current_version -> $new_version'
+  pre_bump_hooks: python ../../ci/changelog.py eppp_link
+  tag_format: epp_link-v$version
+  version: 0.0.1
+  version_files:
+  - idf_component.yml
diff --git a/components/eppp_link/CMakeLists.txt b/components/eppp_link/CMakeLists.txt
new file mode 100644
index 0000000000..e7e9c1432e
--- /dev/null
+++ b/components/eppp_link/CMakeLists.txt
@@ -0,0 +1,3 @@
+idf_component_register(SRCS "eppp_link.c"
+                    INCLUDE_DIRS "include"
+                    PRIV_REQUIRES esp_netif esp_driver_spi esp_driver_gpio esp_timer driver)
diff --git a/components/eppp_link/Kconfig b/components/eppp_link/Kconfig
new file mode 100644
index 0000000000..4327cba334
--- /dev/null
+++ b/components/eppp_link/Kconfig
@@ -0,0 +1,35 @@
+menu "eppp_link"
+
+    choice EPPP_LINK_DEVICE
+        prompt "Choose PPP device"
+        default EPPP_LINK_DEVICE_UART
+        help
+            Select which peripheral to use for PPP link
+
+        config EPPP_LINK_DEVICE_UART
+            bool "UART"
+            help
+                Use UART.
+
+        config EPPP_LINK_DEVICE_SPI
+            bool "SPI"
+            help
+                Use SPI.
+    endchoice
+
+    config EPPP_LINK_CONN_MAX_RETRY
+        int "Maximum retry"
+        default 6
+        help
+            Set the Maximum retry to infinitely avoid reconnecting
+            This is used only with the simplified API (eppp_connect()
+            and eppp_listen())
+
+    config EPPP_LINK_PACKET_QUEUE_SIZE
+        int "Packet queue size"
+        default 64
+        help
+            Size of the Tx packet queue.
+            You can decrease the number for slower bit rates.
+
+endmenu
diff --git a/components/eppp_link/LICENSE b/components/eppp_link/LICENSE
new file mode 100644
index 0000000000..d645695673
--- /dev/null
+++ b/components/eppp_link/LICENSE
@@ -0,0 +1,202 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/components/eppp_link/README.md b/components/eppp_link/README.md
new file mode 100644
index 0000000000..51ba0ae6d5
--- /dev/null
+++ b/components/eppp_link/README.md
@@ -0,0 +1,44 @@
+# ESP PPP Link component (eppp_link)
+
+The component provides a general purpose connectivity engine between two micro-controllers, one acting as PPP server (slave), the other one as PPP client (host). Typical application is a WiFi connectivity provider for chips that do not have WiFi:
+
+```
+             SLAVE micro                                  HOST micro
+    \|/  +----------------+                           +----------------+
+     |   |                |          serial line      |                |
+     +---+ WiFi NAT PPPoS |======== UART / SPI =======| PPPoS client   |
+         |        (server)|                           |                |
+         +----------------+                           +----------------+
+```
+
+## API
+
+### Client
+
+* `eppp_connect()` -- Simplified API. Provides the initialization, starts the task and blocks until we're connected
+
+### Server
+
+* `eppp_listen()` -- Simplified API. Provides the initialization, starts the task and blocks until the client connects
+
+### Manual actions
+
+* `eppp_init()` -- Initializes one endpoint (client/server).
+* `eppp_deinit()` -- Destroys the endpoint
+* `eppp_netif_start()` -- Starts the network, could be called after startup or whenever a connection is lost
+* `eppp_netif_stop()` --  Stops the network
+* `eppp_perform()` -- Perform one iteration of the PPP task (need to be called regularly in task-less configuration)
+
+## Throughput
+
+Tested with WiFi-NAPT example, no IRAM optimizations
+
+### UART @ 3Mbauds
+
+* TCP - 2Mbits
+* UDP - 2Mbits
+
+### SPI @ 20MHz
+
+* TCP - 6Mbits
+* UDP - 10Mbits
diff --git a/components/eppp_link/eppp_link.c b/components/eppp_link/eppp_link.c
new file mode 100644
index 0000000000..46e2d03b1a
--- /dev/null
+++ b/components/eppp_link/eppp_link.c
@@ -0,0 +1,853 @@
+/*
+ * SPDX-FileCopyrightText: 2019-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+#include <string.h>
+#include <stdint.h>
+#include "sdkconfig.h"
+#include "esp_log.h"
+#include "esp_netif.h"
+#include "esp_check.h"
+#include "esp_event.h"
+#include "esp_netif_ppp.h"
+#include "eppp_link.h"
+
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+#include "driver/spi_master.h"
+#include "driver/spi_slave.h"
+#include "driver/gpio.h"
+#include "esp_timer.h"
+#elif CONFIG_EPPP_LINK_DEVICE_UART
+#include "driver/uart.h"
+#endif
+
+static const int GOT_IPV4 = BIT0;
+static const int CONNECTION_FAILED = BIT1;
+#define CONNECT_BITS (GOT_IPV4|CONNECTION_FAILED)
+
+static EventGroupHandle_t s_event_group = NULL;
+static const char *TAG = "eppp_link";
+static int s_retry_num = 0;
+static int s_eppp_netif_count = 0; // used as a suffix for the netif key
+static eppp_channel_fn_t s_rx = NULL;
+
+struct eppp_handle {
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+    QueueHandle_t out_queue;
+    QueueHandle_t ready_semaphore;
+    spi_device_handle_t spi_device;
+    spi_host_device_t spi_host;
+    int gpio_intr;
+#elif CONFIG_EPPP_LINK_DEVICE_UART
+    QueueHandle_t uart_event_queue;
+    uart_port_t uart_port;
+#endif
+    esp_netif_t *netif;
+    enum eppp_type role;
+    bool stop;
+    bool exited;
+    bool netif_stop;
+};
+
+struct packet {
+    size_t len;
+    uint8_t *data;
+    int channel;
+};
+
+static esp_err_t transmit_channel(void *netif, void *buffer, size_t len)
+{
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    struct packet buf = { .len = len };
+    buf.channel = 1;
+    buf.data = malloc(len);
+    if (buf.data == NULL) {
+        ESP_LOGE(TAG, "Failed to allocate packet");
+        return ESP_FAIL;
+    }
+    memcpy(buf.data, buffer, len);
+    BaseType_t ret = xQueueSend(h->out_queue, &buf, pdMS_TO_TICKS(10));
+    if (ret != pdTRUE) {
+        ESP_LOGE(TAG, "Failed to queue packet to slave!");
+        return ESP_FAIL;
+    }
+    return ESP_OK;
+}
+
+static esp_err_t transmit(void *h, void *buffer, size_t len)
+{
+    struct eppp_handle *handle = h;
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+#define MAX_PAYLOAD 1600
+    struct packet buf = { };
+
+    uint8_t *current_buffer = buffer;
+    size_t remaining = len;
+    do {
+        size_t batch = remaining > MAX_PAYLOAD ? MAX_PAYLOAD : remaining;
+        buf.data = malloc(batch);
+        if (buf.data == NULL) {
+            ESP_LOGE(TAG, "Failed to allocate packet");
+            return ESP_FAIL;
+        }
+        buf.len = batch;
+        remaining -= batch;
+        memcpy(buf.data, current_buffer, batch);
+        current_buffer += batch;
+        BaseType_t ret = xQueueSend(handle->out_queue, &buf, pdMS_TO_TICKS(10));
+        if (ret != pdTRUE) {
+            ESP_LOGE(TAG, "Failed to queue packet to slave!");
+            return ESP_FAIL;
+        }
+    } while (remaining > 0);
+#elif CONFIG_EPPP_LINK_DEVICE_UART
+    ESP_LOG_BUFFER_HEXDUMP("ppp_uart_send", buffer, len, ESP_LOG_VERBOSE);
+    uart_write_bytes(handle->uart_port, buffer, len);
+#endif
+    return ESP_OK;
+}
+
+static void netif_deinit(esp_netif_t *netif)
+{
+    if (netif == NULL) {
+        return;
+    }
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    if (h == NULL) {
+        return;
+    }
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+    vQueueDelete(h->out_queue);
+    if (h->role == EPPP_CLIENT) {
+        vSemaphoreDelete(h->ready_semaphore);
+    }
+#endif
+    free(h);
+    esp_netif_destroy(netif);
+    if (s_eppp_netif_count > 0) {
+        s_eppp_netif_count--;
+    }
+}
+
+static esp_netif_t *netif_init(enum eppp_type role)
+{
+    if (s_eppp_netif_count > 9) {
+        ESP_LOGE(TAG, "Cannot create more than 10 instances");
+        return NULL;
+    }
+
+    // Create the object first
+    struct eppp_handle *h = calloc(1, sizeof(struct eppp_handle));
+    if (!h) {
+        ESP_LOGE(TAG, "Failed to allocate eppp_handle");
+        return NULL;
+    }
+    h->role = role;
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+    h->out_queue = xQueueCreate(CONFIG_EPPP_LINK_PACKET_QUEUE_SIZE, sizeof(struct packet));
+    if (!h->out_queue) {
+        ESP_LOGE(TAG, "Failed to create the packet queue");
+        free(h);
+        return NULL;
+    }
+    if (role == EPPP_CLIENT) {
+        h->ready_semaphore = xSemaphoreCreateBinary();
+        if (!h->ready_semaphore) {
+            ESP_LOGE(TAG, "Failed to create the packet queue");
+            vQueueDelete(h->out_queue);
+            free(h);
+            return NULL;
+        }
+    }
+#endif
+
+    esp_netif_driver_ifconfig_t driver_cfg = {
+        .handle = h,
+        .transmit = transmit,
+    };
+    const esp_netif_driver_ifconfig_t *ppp_driver_cfg = &driver_cfg;
+
+    esp_netif_inherent_config_t base_netif_cfg = ESP_NETIF_INHERENT_DEFAULT_PPP();
+    char if_key[] = "EPPP0"; // netif key needs to be unique
+    if_key[sizeof(if_key) - 2 /* 2 = two chars before the terminator */ ] += s_eppp_netif_count++;
+    base_netif_cfg.if_key = if_key;
+    if (role == EPPP_CLIENT) {
+        base_netif_cfg.if_desc = "pppos_client";
+    } else {
+        base_netif_cfg.if_desc = "pppos_server";
+    }
+    esp_netif_config_t netif_ppp_config = { .base = &base_netif_cfg,
+                                            .driver = ppp_driver_cfg,
+                                            .stack = ESP_NETIF_NETSTACK_DEFAULT_PPP
+                                          };
+
+    esp_netif_t *netif = esp_netif_new(&netif_ppp_config);
+    if (!netif) {
+        ESP_LOGE(TAG, "Failed to create esp_netif");
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+        vQueueDelete(h->out_queue);
+#endif
+        free(h);
+        return NULL;
+    }
+    return netif;
+
+}
+
+esp_err_t eppp_netif_stop(esp_netif_t *netif, TickType_t stop_timeout)
+{
+    esp_netif_action_disconnected(netif, 0, 0, 0);
+    esp_netif_action_stop(netif, 0, 0, 0);
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    for (int wait = 0; wait < 100; wait++) {
+        vTaskDelay(stop_timeout / 100);
+        if (h->netif_stop) {
+            break;
+        }
+    }
+    if (!h->netif_stop) {
+        return ESP_FAIL;
+    }
+
+    return ESP_OK;
+}
+
+esp_err_t eppp_netif_start(esp_netif_t *netif)
+{
+    esp_netif_action_start(netif, 0, 0, 0);
+    esp_netif_action_connected(netif, 0, 0, 0);
+    return ESP_OK;
+}
+
+static int get_netif_num(esp_netif_t *netif)
+{
+    if (netif == NULL) {
+        return -1;
+    }
+    const char *ifkey = esp_netif_get_ifkey(netif);
+    if (strstr(ifkey, "EPPP") == NULL) {
+        return -1; // not our netif
+    }
+    int netif_cnt = ifkey[4] - '0';
+    if (netif_cnt < 0 || netif_cnt > 9) {
+        ESP_LOGE(TAG, "Unexpected netif key %s", ifkey);
+        return -1;
+    }
+    return  netif_cnt;
+}
+
+static void on_ppp_event(void *arg, esp_event_base_t base, int32_t event_id, void *data)
+{
+    esp_netif_t **netif = data;
+    if (base == NETIF_PPP_STATUS && event_id == NETIF_PPP_ERRORUSER) {
+        ESP_LOGI(TAG, "Disconnected %d", get_netif_num(*netif));
+        struct eppp_handle *h = esp_netif_get_io_driver(*netif);
+        h->netif_stop = true;
+    }
+}
+
+static void on_ip_event(void *arg, esp_event_base_t base, int32_t event_id, void *data)
+{
+    ip_event_got_ip_t *event = (ip_event_got_ip_t *)data;
+    esp_netif_t *netif = event->esp_netif;
+    if (event_id == IP_EVENT_STA_GOT_IP) {
+        ESP_LOGI(TAG, "Got IPv4 event: Interface \"%s(%s)\" address: " IPSTR, esp_netif_get_desc(netif),
+                 esp_netif_get_ifkey(netif), IP2STR(&event->ip_info.ip));
+    }
+    int netif_cnt = get_netif_num(netif);
+    if (netif_cnt < 0) {
+        return;
+    }
+    if (event_id == IP_EVENT_PPP_GOT_IP) {
+        ESP_LOGI(TAG, "Got IPv4 event: Interface \"%s(%s)\" address: " IPSTR, esp_netif_get_desc(netif),
+                 esp_netif_get_ifkey(netif), IP2STR(&event->ip_info.ip));
+        xEventGroupSetBits(s_event_group, GOT_IPV4 << (netif_cnt * 2));
+    } else if (event_id == IP_EVENT_PPP_LOST_IP) {
+        ESP_LOGI(TAG, "Disconnected");
+        s_retry_num++;
+        if (s_retry_num > CONFIG_EPPP_LINK_CONN_MAX_RETRY) {
+            ESP_LOGE(TAG, "PPP Connection failed %d times, stop reconnecting.", s_retry_num);
+            xEventGroupSetBits(s_event_group, CONNECTION_FAILED << (netif_cnt * 2));
+        } else {
+            ESP_LOGI(TAG, "PPP Connection failed %d times, try to reconnect.", s_retry_num);
+            eppp_netif_start(netif);
+        }
+    }
+}
+
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+
+#define TRANSFER_SIZE (MAX_PAYLOAD + 4)
+#define SHORT_PAYLOAD (48)
+#define CONTROL_SIZE (SHORT_PAYLOAD + 4)
+
+#define CONTROL_MASTER 0xA5
+#define CONTROL_MASTER_WITH_DATA 0xA6
+#define CONTROL_SLAVE 0x5A
+#define CONTROL_SLAVE_WITH_DATA 0x5B
+#define DATA_MASTER 0xAF
+#define DATA_SLAVE 0xFA
+
+#define MAX(a,b) (((a)>(b))?(a):(b))
+
+struct header {
+    union {
+        uint16_t size;
+        struct {
+            uint8_t short_size;
+            uint8_t long_size;
+        } __attribute__((packed));
+    };
+    uint8_t magic;
+    uint8_t channel;
+    uint8_t checksum;
+} __attribute__((packed));
+
+static void IRAM_ATTR gpio_isr_handler(void *arg)
+{
+    static uint32_t s_last_time;
+    uint32_t now = esp_timer_get_time();
+    uint32_t diff = now - s_last_time;
+    if (diff < 5) { // debounce
+        return;
+    }
+    s_last_time = now;
+
+    BaseType_t yield = false;
+    struct eppp_handle *h = arg;
+    xSemaphoreGiveFromISR(h->ready_semaphore, &yield);
+    if (yield) {
+        portYIELD_FROM_ISR();
+    }
+}
+
+static esp_err_t deinit_master(esp_netif_t *netif)
+{
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    ESP_RETURN_ON_ERROR(spi_bus_remove_device(h->spi_device), TAG, "Failed to remove SPI bus");
+    ESP_RETURN_ON_ERROR(spi_bus_free(h->spi_host), TAG, "Failed to free SPI bus");
+    return ESP_OK;
+}
+
+static esp_err_t init_master(struct eppp_config_spi_s *config, esp_netif_t *netif)
+{
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    h->spi_host = config->host;
+    h->gpio_intr = config->intr;
+    spi_bus_config_t bus_cfg = {};
+    bus_cfg.mosi_io_num = config->mosi;
+    bus_cfg.miso_io_num = config->miso;
+    bus_cfg.sclk_io_num = config->sclk;
+    bus_cfg.quadwp_io_num = -1;
+    bus_cfg.quadhd_io_num = -1;
+    bus_cfg.max_transfer_sz = 2000;
+    bus_cfg.flags = 0;
+    bus_cfg.intr_flags = 0;
+
+    // TODO: Init and deinit SPI bus separately (per Kconfig?)
+    if (spi_bus_initialize(config->host, &bus_cfg, SPI_DMA_CH_AUTO) != ESP_OK) {
+        return ESP_FAIL;
+    }
+
+    spi_device_interface_config_t dev_cfg = {};
+    dev_cfg.clock_speed_hz = config->freq;
+    dev_cfg.mode = 0;
+    dev_cfg.spics_io_num = config->cs;
+    dev_cfg.cs_ena_pretrans = 0;
+    dev_cfg.cs_ena_posttrans = 0;
+    dev_cfg.duty_cycle_pos = 128;
+    dev_cfg.input_delay_ns = 6;
+    dev_cfg.pre_cb = NULL;
+    dev_cfg.post_cb = NULL;
+    dev_cfg.cs_ena_posttrans = 3;
+    dev_cfg.queue_size = 3;
+
+    if (spi_bus_add_device(config->host, &dev_cfg, &h->spi_device) != ESP_OK) {
+        return ESP_FAIL;
+    }
+
+    //GPIO config for the handshake line.
+    gpio_config_t io_conf = {
+        .intr_type = GPIO_INTR_POSEDGE,
+        .mode = GPIO_MODE_INPUT,
+        .pull_up_en = 1,
+        .pin_bit_mask = BIT64(config->intr),
+    };
+
+    gpio_config(&io_conf);
+    gpio_install_isr_service(0);
+    gpio_set_intr_type(config->intr, GPIO_INTR_POSEDGE);
+    gpio_isr_handler_add(config->intr, gpio_isr_handler, esp_netif_get_io_driver(netif));
+    return ESP_OK;
+}
+
+static void post_setup(spi_slave_transaction_t *trans)
+{
+    gpio_set_level((int)trans->user, 1);
+}
+
+static void post_trans(spi_slave_transaction_t *trans)
+{
+    gpio_set_level((int)trans->user, 0);
+}
+
+static esp_err_t deinit_slave(esp_netif_t *netif)
+{
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    ESP_RETURN_ON_ERROR(spi_slave_free(h->spi_host), TAG, "Failed to free SPI slave host");
+    ESP_RETURN_ON_ERROR(spi_bus_remove_device(h->spi_device), TAG, "Failed to remove SPI device");
+    ESP_RETURN_ON_ERROR(spi_bus_free(h->spi_host), TAG, "Failed to free SPI bus");
+    return ESP_OK;
+}
+
+static esp_err_t init_slave(struct eppp_config_spi_s *config, esp_netif_t *netif)
+{
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    h->spi_host = config->host;
+    h->gpio_intr = config->intr;
+    spi_bus_config_t bus_cfg = {};
+    bus_cfg.mosi_io_num = config->mosi;
+    bus_cfg.miso_io_num = config->miso;
+    bus_cfg.sclk_io_num = config->sclk;
+    bus_cfg.quadwp_io_num = -1;
+    bus_cfg.quadhd_io_num = -1;
+    bus_cfg.flags = 0;
+    bus_cfg.intr_flags = 0;
+
+    //Configuration for the SPI slave interface
+    spi_slave_interface_config_t slvcfg = {
+        .mode = 0,
+        .spics_io_num = config->cs,
+        .queue_size = 3,
+        .flags = 0,
+        .post_setup_cb = post_setup,
+        .post_trans_cb = post_trans,
+    };
+
+    //Configuration for the handshake line
+    gpio_config_t io_conf = {
+        .intr_type = GPIO_INTR_DISABLE,
+        .mode = GPIO_MODE_OUTPUT,
+        .pin_bit_mask = BIT64(config->intr),
+    };
+
+    gpio_config(&io_conf);
+    gpio_set_pull_mode(config->mosi, GPIO_PULLUP_ONLY);
+    gpio_set_pull_mode(config->sclk, GPIO_PULLUP_ONLY);
+    gpio_set_pull_mode(config->cs, GPIO_PULLUP_ONLY);
+
+    //Initialize SPI slave interface
+    if (spi_slave_initialize(config->host, &bus_cfg, &slvcfg, SPI_DMA_CH_AUTO) != ESP_OK) {
+        return ESP_FAIL;
+    }
+    return ESP_OK;
+}
+
+union transaction {
+    spi_transaction_t master;
+    spi_slave_transaction_t slave;
+};
+
+typedef void (*set_transaction_t)(union transaction *t, size_t len, const void *tx_buffer, void *rx_buffer, int gpio_intr);
+typedef esp_err_t (*perform_transaction_t)(union transaction *t, struct eppp_handle *h);
+
+static void set_transaction_master(union transaction *t, size_t len, const void *tx_buffer, void *rx_buffer, int gpio_intr)
+{
+    t->master.length = len * 8;
+    t->master.tx_buffer = tx_buffer;
+    t->master.rx_buffer = rx_buffer;
+}
+
+static void set_transaction_slave(union transaction *t, size_t len, const void *tx_buffer, void *rx_buffer, int gpio_intr)
+{
+    t->slave.user = (void *)gpio_intr;
+    t->slave.length = len * 8;
+    t->slave.tx_buffer = tx_buffer;
+    t->slave.rx_buffer = rx_buffer;
+}
+
+static esp_err_t perform_transaction_master(union transaction *t, struct eppp_handle *h)
+{
+    xSemaphoreTake(h->ready_semaphore, portMAX_DELAY); // Wait until slave is ready
+    return spi_device_transmit(h->spi_device, &t->master);
+}
+
+static esp_err_t perform_transaction_slave(union transaction *t, struct eppp_handle *h)
+{
+    return spi_slave_transmit(h->spi_host, &t->slave, portMAX_DELAY);
+}
+
+esp_err_t eppp_add_channel(int nr, eppp_channel_fn_t *tx, const eppp_channel_fn_t rx)
+{
+
+    *tx = transmit_channel;
+    s_rx = rx;
+    return ESP_OK;
+}
+
+esp_err_t eppp_perform(esp_netif_t *netif)
+{
+    static WORD_ALIGNED_ATTR uint8_t out_buf[TRANSFER_SIZE] = {};
+    static WORD_ALIGNED_ATTR uint8_t in_buf[TRANSFER_SIZE] = {};
+
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    union transaction t;
+
+    // Three types of frames (control, control+data, data), two roles (master, slave)
+    const uint8_t FRAME_OUT_CTRL = h->role == EPPP_CLIENT ? CONTROL_MASTER : CONTROL_SLAVE;
+    const uint8_t FRAME_OUT_CTRL_EX = h->role == EPPP_CLIENT ? CONTROL_MASTER_WITH_DATA : CONTROL_SLAVE_WITH_DATA;
+    const uint8_t FRAME_OUT_DATA = h->role == EPPP_CLIENT ? DATA_MASTER : DATA_SLAVE;
+    const uint8_t FRAME_IN_CTRL = h->role == EPPP_SERVER ? CONTROL_MASTER : CONTROL_SLAVE;
+    const uint8_t FRAME_IN_CTRL_EX = h->role == EPPP_SERVER ? CONTROL_MASTER_WITH_DATA : CONTROL_SLAVE_WITH_DATA;
+    const uint8_t FRAME_IN_DATA = h->role == EPPP_SERVER ? DATA_MASTER : DATA_SLAVE;
+    // Two actions (prepare and perform transaction) for these two roles (master, slave)
+    const set_transaction_t set_transaction = h->role == EPPP_CLIENT ? set_transaction_master : set_transaction_slave;
+    const perform_transaction_t perform_transaction = h->role == EPPP_CLIENT ? perform_transaction_master : perform_transaction_slave;
+
+    if (h->stop) {
+        return ESP_ERR_TIMEOUT;
+    }
+
+    struct packet buf = { .len = 0 };
+    struct header *head = (void *)out_buf;
+    bool need_data_frame = false;
+    size_t out_long_payload = 0;
+    head->magic = FRAME_OUT_CTRL_EX;
+    head->size = 0;
+    head->checksum = 0;
+    head->channel = 0;
+    BaseType_t tx_queue_stat = xQueueReceive(h->out_queue, &buf, 0);
+    if (tx_queue_stat == pdTRUE && buf.data) {
+        head->channel = buf.channel;
+        if (buf.len > SHORT_PAYLOAD) {
+            head->magic = FRAME_OUT_CTRL;
+            head->size = buf.len;
+            out_long_payload = buf.len;
+            need_data_frame = true;
+        } else {
+            head->magic = FRAME_OUT_CTRL_EX;
+            head->long_size = 0;
+            head->short_size = buf.len;
+            memcpy(out_buf + sizeof(struct header), buf.data, buf.len);
+            free(buf.data);
+        }
+    }
+    memset(&t, 0, sizeof(t));
+    set_transaction(&t, CONTROL_SIZE, out_buf, in_buf, h->gpio_intr);
+    for (int i = 0; i < sizeof(struct header) - 1; ++i) {
+        head->checksum += out_buf[i];
+    }
+    esp_err_t ret = perform_transaction(&t, h);
+    if (ret != ESP_OK) {
+        ESP_LOGE(TAG, "spi_device_transmit failed");
+        return ESP_FAIL;
+    }
+    head = (void *)in_buf;
+    uint8_t checksum = 0;
+    for (int i = 0; i < sizeof(struct header) - 1; ++i) {
+        checksum += in_buf[i];
+    }
+    if (checksum != head->checksum) {
+        ESP_LOGE(TAG, "Wrong checksum");
+        return ESP_FAIL;
+    }
+    if (head->magic != FRAME_IN_CTRL && head->magic != FRAME_IN_CTRL_EX) {
+        ESP_LOGE(TAG, "Wrong magic");
+        return ESP_FAIL;
+    }
+    if (head->magic == FRAME_IN_CTRL_EX && head->short_size > 0) {
+        if (head->channel == 0) {
+            esp_netif_receive(netif, in_buf + sizeof(struct header), head->short_size, NULL);
+        } else {
+//            ESP_LOGE(TAG, "Got channel %d size %d", head->channel, head->short_size);
+            if (s_rx != NULL) {
+                s_rx(netif, in_buf + sizeof(struct header), head->short_size);
+            }
+        }
+    }
+    size_t in_long_payload = 0;
+    if (head->magic == FRAME_IN_CTRL) {
+        need_data_frame = true;
+        in_long_payload = head->size;
+    }
+    if (!need_data_frame) {
+        return ESP_OK;
+    }
+
+    // now, we need data frame
+    head = (void *)out_buf;
+    head->magic = FRAME_OUT_DATA;
+    head->size = out_long_payload;
+    head->checksum = 0;
+    head->channel = buf.channel;
+    for (int i = 0; i < sizeof(struct header) - 1; ++i) {
+        head->checksum += out_buf[i];
+    }
+    if (head->size > 0) {
+        memcpy(out_buf + sizeof(struct header), buf.data, buf.len);
+//            ESP_LOG_BUFFER_HEXDUMP(TAG, out_buf + sizeof(struct header), head->size, ESP_LOG_INFO);
+        free(buf.data);
+    }
+
+    memset(&t, 0, sizeof(t));
+    set_transaction(&t, MAX(in_long_payload, out_long_payload) + sizeof(struct header), out_buf, in_buf, h->gpio_intr);
+
+    ret = perform_transaction(&t, h);
+    if (ret != ESP_OK) {
+        ESP_LOGE(TAG, "spi_device_transmit failed");
+        return ESP_FAIL;
+    }
+    head = (void *)in_buf;
+    checksum = 0;
+    for (int i = 0; i < sizeof(struct header) - 1; ++i) {
+        checksum += in_buf[i];
+    }
+    if (checksum != head->checksum) {
+        ESP_LOGE(TAG, "Wrong checksum");
+        return ESP_FAIL;
+    }
+    if (head->magic != FRAME_IN_DATA) {
+        ESP_LOGE(TAG, "Wrong magic");
+        return ESP_FAIL;
+    }
+
+    if (head->size > 0) {
+        ESP_LOG_BUFFER_HEXDUMP(TAG, in_buf + sizeof(struct header), head->size, ESP_LOG_VERBOSE);
+        if (head->channel == 0) {
+            esp_netif_receive(netif, in_buf + sizeof(struct header), head->size, NULL);
+        } else {
+//            ESP_LOGE(TAG, "Got channel %d size %d", head->channel, head->size);
+            if (s_rx != NULL) {
+                s_rx(netif, in_buf + sizeof(struct header), head->size);
+            }
+        }
+    }
+    return ESP_OK;
+}
+
+static void ppp_task(void *args)
+{
+    esp_netif_t *netif = args;
+    while (eppp_perform(netif) != ESP_ERR_TIMEOUT) {}
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    h->exited = true;
+    vTaskDelete(NULL);
+}
+
+#elif CONFIG_EPPP_LINK_DEVICE_UART
+#define BUF_SIZE (1024)
+
+static esp_err_t init_uart(struct eppp_handle *h, eppp_config_t *config)
+{
+    h->uart_port = config->uart.port;
+    uart_config_t uart_config = {};
+    uart_config.baud_rate = config->uart.baud;
+    uart_config.data_bits = UART_DATA_8_BITS;
+    uart_config.parity    = UART_PARITY_DISABLE;
+    uart_config.stop_bits = UART_STOP_BITS_1;
+    uart_config.flow_ctrl = UART_HW_FLOWCTRL_DISABLE;
+    uart_config.source_clk = UART_SCLK_DEFAULT;
+
+    ESP_RETURN_ON_ERROR(uart_driver_install(h->uart_port, config->uart.rx_buffer_size, 0, config->uart.queue_size, &h->uart_event_queue, 0), TAG, "Failed to install UART");
+    ESP_RETURN_ON_ERROR(uart_param_config(h->uart_port, &uart_config), TAG, "Failed to set params");
+    ESP_RETURN_ON_ERROR(uart_set_pin(h->uart_port, config->uart.tx_io, config->uart.rx_io, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE), TAG, "Failed to set UART pins");
+    ESP_RETURN_ON_ERROR(uart_set_rx_timeout(h->uart_port, 1), TAG, "Failed to set UART Rx timeout");
+    return ESP_OK;
+}
+
+static void deinit_uart(struct eppp_handle *h)
+{
+    uart_driver_delete(h->uart_port);
+}
+
+esp_err_t eppp_perform(esp_netif_t *netif)
+{
+    static uint8_t buffer[BUF_SIZE] = {};
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    uart_event_t event = {};
+    if (h->stop) {
+        return ESP_ERR_TIMEOUT;
+    }
+
+    if (xQueueReceive(h->uart_event_queue, &event, pdMS_TO_TICKS(100)) != pdTRUE) {
+        return ESP_OK;
+    }
+    if (event.type == UART_DATA) {
+        size_t len;
+        uart_get_buffered_data_len(h->uart_port, &len);
+        if (len) {
+            len = uart_read_bytes(h->uart_port, buffer, BUF_SIZE, 0);
+            ESP_LOG_BUFFER_HEXDUMP("ppp_uart_recv", buffer, len, ESP_LOG_VERBOSE);
+            esp_netif_receive(netif, buffer, len, NULL);
+        }
+    } else {
+        ESP_LOGW(TAG, "Received UART event: %d", event.type);
+    }
+    return ESP_OK;
+}
+
+static void ppp_task(void *args)
+{
+    esp_netif_t *netif = args;
+    while (eppp_perform(netif) == ESP_OK) {}
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    h->exited = true;
+    vTaskDelete(NULL);
+}
+
+#endif // CONFIG_EPPP_LINK_DEVICE_SPI / UART
+
+static bool have_some_eppp_netif(esp_netif_t *netif, void *ctx)
+{
+    return get_netif_num(netif) > 0;
+}
+
+static void remove_handlers(void)
+{
+    esp_netif_t *netif = esp_netif_find_if(have_some_eppp_netif, NULL);
+    if (netif == NULL) {
+        // if EPPP netif in the system, we cleanup the statics
+        vEventGroupDelete(s_event_group);
+        s_event_group = NULL;
+        esp_event_handler_unregister(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event);
+        esp_event_handler_unregister(NETIF_PPP_STATUS, ESP_EVENT_ANY_ID, on_ppp_event);
+    }
+}
+
+void eppp_deinit(esp_netif_t *netif)
+{
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    if (h->role == EPPP_CLIENT) {
+        deinit_master(netif);
+    } else {
+        deinit_slave(netif);
+    }
+#elif CONFIG_EPPP_LINK_DEVICE_UART
+    deinit_uart(esp_netif_get_io_driver(netif));
+#endif
+    netif_deinit(netif);
+}
+
+esp_netif_t *eppp_init(enum eppp_type role, eppp_config_t *config)
+{
+    esp_netif_t *netif = netif_init(role);
+    if (!netif) {
+        ESP_LOGE(TAG, "Failed to initialize PPP netif");
+        remove_handlers();
+        return NULL;
+    }
+    esp_netif_ppp_config_t netif_params;
+    ESP_ERROR_CHECK(esp_netif_ppp_get_params(netif, &netif_params));
+    netif_params.ppp_our_ip4_addr.addr = config->ppp.our_ip4_addr;
+    netif_params.ppp_their_ip4_addr.addr = config->ppp.their_ip4_addr;
+    netif_params.ppp_error_event_enabled = true;
+    ESP_ERROR_CHECK(esp_netif_ppp_set_params(netif, &netif_params));
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+    if (role == EPPP_CLIENT) {
+        init_master(&config->spi, netif);
+
+        // as a client, try to actively connect (not waiting for server's interrupt)
+        struct eppp_handle *h = esp_netif_get_io_driver(netif);
+        xSemaphoreGive(h->ready_semaphore);
+    } else {
+        init_slave(&config->spi, netif);
+
+    }
+#elif CONFIG_EPPP_LINK_DEVICE_UART
+    init_uart(esp_netif_get_io_driver(netif), config);
+#endif
+    return netif;
+}
+
+esp_netif_t *eppp_open(enum eppp_type role, eppp_config_t *config, TickType_t connect_timeout)
+{
+#if CONFIG_EPPP_LINK_DEVICE_UART
+    if (config->transport != EPPP_TRANSPORT_UART) {
+        ESP_LOGE(TAG, "Invalid transport: UART device must be enabled in Kconfig");
+        return NULL;
+    }
+#endif
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+    if (config->transport != EPPP_TRANSPORT_SPI) {
+        ESP_LOGE(TAG, "Invalid transport: SPI device must be enabled in Kconfig");
+        return NULL;
+    }
+#endif
+
+    if (config->task.run_task == false) {
+        ESP_LOGE(TAG, "task.run_task == false is invalid in this API. Please use eppp_init()");
+        return NULL;
+    }
+    if (s_event_group == NULL) {
+        s_event_group = xEventGroupCreate();
+        if (esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, on_ip_event, NULL) != ESP_OK) {
+            ESP_LOGE(TAG, "Failed to register IP event handler");
+            remove_handlers();
+            return NULL;
+        }
+        if (esp_event_handler_register(NETIF_PPP_STATUS, ESP_EVENT_ANY_ID, on_ppp_event, NULL) !=  ESP_OK) {
+            ESP_LOGE(TAG, "Failed to register PPP status handler");
+            remove_handlers();
+            return NULL;
+        }
+    }
+    esp_netif_t *netif = eppp_init(role, config);
+    if (!netif) {
+        remove_handlers();
+        return NULL;
+    }
+
+    eppp_netif_start(netif);
+
+    if (xTaskCreate(ppp_task, "ppp connect", config->task.stack_size, netif, config->task.priority, NULL) != pdTRUE) {
+        ESP_LOGE(TAG, "Failed to create a ppp connection task");
+        eppp_deinit(netif);
+        return NULL;
+    }
+    int netif_cnt = get_netif_num(netif);
+    if (netif_cnt < 0) {
+        eppp_close(netif);
+        return NULL;
+    }
+    ESP_LOGI(TAG, "Waiting for IP address %d", netif_cnt);
+    EventBits_t bits = xEventGroupWaitBits(s_event_group, CONNECT_BITS << (netif_cnt * 2), pdFALSE, pdFALSE, connect_timeout);
+    if (bits & (CONNECTION_FAILED << (netif_cnt * 2))) {
+        ESP_LOGE(TAG, "Connection failed!");
+        eppp_close(netif);
+        return NULL;
+    }
+    ESP_LOGI(TAG, "Connected! %d", netif_cnt);
+    return netif;
+}
+
+esp_netif_t *eppp_connect(eppp_config_t *config)
+{
+    return eppp_open(EPPP_CLIENT, config, portMAX_DELAY);
+}
+
+esp_netif_t *eppp_listen(eppp_config_t *config)
+{
+    return eppp_open(EPPP_SERVER, config, portMAX_DELAY);
+}
+
+void eppp_close(esp_netif_t *netif)
+{
+    struct eppp_handle *h = esp_netif_get_io_driver(netif);
+    if (eppp_netif_stop(netif, pdMS_TO_TICKS(60000)) != ESP_OK) {
+        ESP_LOGE(TAG, "Network didn't exit cleanly");
+    }
+    h->stop = true;
+    for (int wait = 0; wait < 100; wait++) {
+        vTaskDelay(pdMS_TO_TICKS(10));
+        if (h->exited) {
+            break;
+        }
+    }
+    if (!h->exited) {
+        ESP_LOGE(TAG, "Cannot stop ppp_task");
+    }
+    eppp_deinit(netif);
+    remove_handlers();
+}
diff --git a/components/eppp_link/eppp_link_types.h b/components/eppp_link/eppp_link_types.h
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/components/eppp_link/examples/host/CMakeLists.txt b/components/eppp_link/examples/host/CMakeLists.txt
new file mode 100644
index 0000000000..3405abc526
--- /dev/null
+++ b/components/eppp_link/examples/host/CMakeLists.txt
@@ -0,0 +1,8 @@
+# The following four lines of boilerplate have to be in your project's CMakeLists
+# in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.16)
+set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/iperf)
+
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+project(pppos_host)
diff --git a/components/eppp_link/examples/host/README.md b/components/eppp_link/examples/host/README.md
new file mode 100644
index 0000000000..a6592627c6
--- /dev/null
+++ b/components/eppp_link/examples/host/README.md
@@ -0,0 +1,9 @@
+
+# Client side demo of ESP-PPP-Link
+
+This is a basic demo of using esp-mqtt library, but connects to the internet using a PPPoS client. To run this example, you would need a PPP server that provides connectivity to the MQTT broker used in this example (by default a public broker accessible on the internet).
+
+If configured, this example could also run a ping session and an iperf console.
+
+
+The PPP server could be a Linux computer with `pppd` service or an ESP32 acting like a connection gateway with PPPoS server (see the "slave" project).
diff --git a/components/eppp_link/examples/host/main/CMakeLists.txt b/components/eppp_link/examples/host/main/CMakeLists.txt
new file mode 100644
index 0000000000..0198ddd323
--- /dev/null
+++ b/components/eppp_link/examples/host/main/CMakeLists.txt
@@ -0,0 +1,2 @@
+idf_component_register(SRCS app_main.c register_iperf.c
+                    INCLUDE_DIRS ".")
diff --git a/components/eppp_link/examples/host/main/Kconfig.projbuild b/components/eppp_link/examples/host/main/Kconfig.projbuild
new file mode 100644
index 0000000000..02881e10a3
--- /dev/null
+++ b/components/eppp_link/examples/host/main/Kconfig.projbuild
@@ -0,0 +1,43 @@
+menu "Example Configuration"
+
+    config EXAMPLE_GLOBAL_DNS
+        hex "Set global DNS server"
+        range 0 0xFFFFFFFF
+        default 0x08080808
+        help
+            Global DNS server address.
+
+    config EXAMPLE_MQTT
+        bool "Run mqtt example"
+        default y
+        help
+            Run MQTT client after startup.
+
+    config EXAMPLE_BROKER_URL
+        string "Broker URL"
+        depends on EXAMPLE_MQTT
+        default "mqtt://mqtt.eclipseprojects.io"
+        help
+            URL of the broker to connect to.
+
+    config EXAMPLE_ICMP_PING
+        bool "Run ping example"
+        default y
+        help
+            Ping configured address after startup.
+
+    config EXAMPLE_PING_ADDR
+        hex "Ping IPv4 address"
+        depends on EXAMPLE_ICMP_PING
+        range 0 0xFFFFFFFF
+        default 0x08080808
+        help
+            Address to send ping requests.
+
+    config EXAMPLE_IPERF
+        bool "Run iperf"
+        default y
+        help
+            Init and run iperf console.
+
+endmenu
diff --git a/components/eppp_link/examples/host/main/app_main.c b/components/eppp_link/examples/host/main/app_main.c
new file mode 100644
index 0000000000..2f147c9f5a
--- /dev/null
+++ b/components/eppp_link/examples/host/main/app_main.c
@@ -0,0 +1,212 @@
+/*
+ * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+#include "esp_system.h"
+#include "nvs_flash.h"
+#include "esp_event.h"
+#include "esp_netif.h"
+#include "eppp_link.h"
+#include "lwip/sockets.h"
+#include "esp_log.h"
+#include "mqtt_client.h"
+#include "ping/ping_sock.h"
+#include "esp_console.h"
+
+void register_iperf(void);
+
+static const char *TAG = "eppp_host_example";
+
+#if CONFIG_EXAMPLE_MQTT
+static void mqtt_event_handler(void *args, esp_event_base_t base, int32_t event_id, void *event_data)
+{
+    ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32 "", base, event_id);
+    esp_mqtt_event_handle_t event = event_data;
+    esp_mqtt_client_handle_t client = event->client;
+    int msg_id;
+    switch ((esp_mqtt_event_id_t)event_id) {
+    case MQTT_EVENT_CONNECTED:
+        ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
+        msg_id = esp_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0);
+        ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
+
+        msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0);
+        ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
+
+        msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1);
+        ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
+
+        msg_id = esp_mqtt_client_unsubscribe(client, "/topic/qos1");
+        ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
+        break;
+    case MQTT_EVENT_DISCONNECTED:
+        ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
+        break;
+
+    case MQTT_EVENT_SUBSCRIBED:
+        ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
+        msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0);
+        ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
+        break;
+    case MQTT_EVENT_UNSUBSCRIBED:
+        ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
+        break;
+    case MQTT_EVENT_PUBLISHED:
+        ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
+        break;
+    case MQTT_EVENT_DATA:
+        ESP_LOGI(TAG, "MQTT_EVENT_DATA");
+        printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
+        printf("DATA=%.*s\r\n", event->data_len, event->data);
+        break;
+    case MQTT_EVENT_ERROR:
+        ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
+        if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
+            ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno));
+        }
+        break;
+    default:
+        ESP_LOGI(TAG, "Other event id:%d", event->event_id);
+        break;
+    }
+}
+
+static void mqtt_app_start(void)
+{
+    esp_mqtt_client_config_t mqtt_cfg = {
+        .broker.address.uri = CONFIG_EXAMPLE_BROKER_URL,
+    };
+
+    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
+    /* The last argument may be used to pass data to the event handler, in this example mqtt_event_handler */
+    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
+    esp_mqtt_client_start(client);
+}
+#endif // MQTT
+
+#if CONFIG_EXAMPLE_ICMP_PING
+static void test_on_ping_success(esp_ping_handle_t hdl, void *args)
+{
+    uint8_t ttl;
+    uint16_t seqno;
+    uint32_t elapsed_time, recv_len;
+    ip_addr_t target_addr;
+    esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &recv_len, sizeof(recv_len));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &elapsed_time, sizeof(elapsed_time));
+    printf("%" PRId32 "bytes from %s icmp_seq=%d ttl=%d time=%" PRId32 " ms\n",
+           recv_len, inet_ntoa(target_addr.u_addr.ip4), seqno, ttl, elapsed_time);
+}
+
+static void test_on_ping_timeout(esp_ping_handle_t hdl, void *args)
+{
+    uint16_t seqno;
+    ip_addr_t target_addr;
+    esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
+    printf("From %s icmp_seq=%d timeout\n", inet_ntoa(target_addr.u_addr.ip4), seqno);
+}
+
+static void test_on_ping_end(esp_ping_handle_t hdl, void *args)
+{
+    uint32_t transmitted;
+    uint32_t received;
+    uint32_t total_time_ms;
+    esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &received, sizeof(received));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &total_time_ms, sizeof(total_time_ms));
+    printf("%" PRId32 " packets transmitted, %" PRId32 " received, time %" PRId32 "ms\n", transmitted, received, total_time_ms);
+
+}
+#endif // PING
+
+void app_main(void)
+{
+    ESP_LOGI(TAG, "[APP] Startup..");
+    ESP_LOGI(TAG, "[APP] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size());
+    ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
+
+    ESP_ERROR_CHECK(nvs_flash_init());
+    ESP_ERROR_CHECK(esp_netif_init());
+    ESP_ERROR_CHECK(esp_event_loop_create_default());
+
+    /* Sets up the default EPPP-connection
+     */
+    eppp_config_t config = EPPP_DEFAULT_CLIENT_CONFIG();
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+    config.transport = EPPP_TRANSPORT_SPI;
+#else
+    config.transport = EPPP_TRANSPORT_UART;
+    config.uart.tx_io = 10;
+    config.uart.rx_io = 11;
+    config.uart.baud = 2000000;
+#endif
+    esp_netif_t *eppp_netif = eppp_connect(&config);
+    if (eppp_netif == NULL) {
+        ESP_LOGE(TAG, "Failed to connect");
+        return ;
+    }
+    // Setup global DNS
+    esp_netif_dns_info_t dns;
+    dns.ip.u_addr.ip4.addr = esp_netif_htonl(CONFIG_EXAMPLE_GLOBAL_DNS);
+    dns.ip.type = ESP_IPADDR_TYPE_V4;
+    ESP_ERROR_CHECK(esp_netif_set_dns_info(eppp_netif, ESP_NETIF_DNS_MAIN, &dns));
+
+#if CONFIG_EXAMPLE_IPERF
+    esp_console_repl_t *repl = NULL;
+    esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
+    esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
+    repl_config.prompt = "iperf>";
+    // init console REPL environment
+    ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl));
+
+    register_iperf();
+
+    printf("\n =======================================================\n");
+    printf(" |       Steps to Test PPP Client Bandwidth            |\n");
+    printf(" |                                                     |\n");
+    printf(" |  1. Enter 'help', check all supported commands      |\n");
+    printf(" |  2. Start PPP server on host system                 |\n");
+    printf(" |     - pppd /dev/ttyUSB1 115200 192.168.11.1:192.168.11.2 modem local noauth debug nocrtscts nodetach +ipv6\n");
+    printf(" |  3. Wait ESP32 to get IP from PPP server            |\n");
+    printf(" |  4. Enter 'pppd info' (optional)                    |\n");
+    printf(" |  5. Server: 'iperf -u -s -i 3'                      |\n");
+    printf(" |  6. Client: 'iperf -u -c SERVER_IP -t 60 -i 3'      |\n");
+    printf(" |                                                     |\n");
+    printf(" =======================================================\n\n");
+
+    // start console REPL
+    ESP_ERROR_CHECK(esp_console_start_repl(repl));
+#endif
+
+#if CONFIG_EXAMPLE_ICMP_PING
+    ip_addr_t target_addr = { .type = IPADDR_TYPE_V4, .u_addr.ip4.addr = esp_netif_htonl(CONFIG_EXAMPLE_PING_ADDR) };
+
+    esp_ping_config_t ping_config = ESP_PING_DEFAULT_CONFIG();
+    ping_config.timeout_ms = 2000;
+    ping_config.interval_ms = 20,
+    ping_config.target_addr = target_addr;
+    ping_config.count = 100; // ping in infinite mode
+    /* set callback functions */
+    esp_ping_callbacks_t cbs;
+    cbs.on_ping_success = test_on_ping_success;
+    cbs.on_ping_timeout = test_on_ping_timeout;
+    cbs.on_ping_end = test_on_ping_end;
+    esp_ping_handle_t ping;
+    esp_ping_new_session(&ping_config, &cbs, &ping);
+    /* start ping */
+    esp_ping_start(ping);
+#endif // PING
+
+#if CONFIG_EXAMPLE_MQTT
+    mqtt_app_start();
+#endif
+}
diff --git a/components/eppp_link/examples/host/main/idf_component.yml b/components/eppp_link/examples/host/main/idf_component.yml
new file mode 100644
index 0000000000..7ecb517e8a
--- /dev/null
+++ b/components/eppp_link/examples/host/main/idf_component.yml
@@ -0,0 +1,4 @@
+dependencies:
+  espressif/eppp_link:
+    version: "*"
+    override_path: "../../.."
diff --git a/components/eppp_link/examples/host/main/register_iperf.c b/components/eppp_link/examples/host/main/register_iperf.c
new file mode 100644
index 0000000000..d2169c3763
--- /dev/null
+++ b/components/eppp_link/examples/host/main/register_iperf.c
@@ -0,0 +1,183 @@
+/*
+ * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <inttypes.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/event_groups.h"
+#include "sys/socket.h" // for INADDR_ANY
+#include "esp_netif.h"
+#include "esp_log.h"
+#include "esp_system.h"
+#include "esp_event.h"
+#include "esp_log.h"
+#include "esp_netif.h"
+#include "esp_netif_ppp.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/event_groups.h"
+
+#include "esp_console.h"
+#include "esp_event.h"
+#include "esp_bit_defs.h"
+#include "argtable3/argtable3.h"
+#include "iperf.h"
+#include "sdkconfig.h"
+
+/* "iperf" command */
+
+static struct {
+    struct arg_str *ip;
+    struct arg_lit *server;
+    struct arg_lit *udp;
+    struct arg_lit *version;
+    struct arg_int *port;
+    struct arg_int *length;
+    struct arg_int *interval;
+    struct arg_int *time;
+    struct arg_int *bw_limit;
+    struct arg_lit *abort;
+    struct arg_end *end;
+} iperf_args;
+
+static int ppp_cmd_iperf(int argc, char **argv)
+{
+    int nerrors = arg_parse(argc, argv, (void **)&iperf_args);
+    iperf_cfg_t cfg;
+
+    if (nerrors != 0) {
+        arg_print_errors(stderr, iperf_args.end, argv[0]);
+        return 0;
+    }
+
+    memset(&cfg, 0, sizeof(cfg));
+
+    // ethernet iperf only support IPV4 address
+    cfg.type = IPERF_IP_TYPE_IPV4;
+
+    /* iperf -a */
+    if (iperf_args.abort->count != 0) {
+        iperf_stop();
+        return 0;
+    }
+
+    if (((iperf_args.ip->count == 0) && (iperf_args.server->count == 0)) ||
+            ((iperf_args.ip->count != 0) && (iperf_args.server->count != 0))) {
+        ESP_LOGE(__func__, "Wrong mode! ESP32 should run in client or server mode");
+        return 0;
+    }
+
+    /* iperf -s */
+    if (iperf_args.ip->count == 0) {
+        cfg.flag |= IPERF_FLAG_SERVER;
+    }
+    /* iperf -c SERVER_ADDRESS */
+    else {
+        cfg.destination_ip4 = esp_ip4addr_aton(iperf_args.ip->sval[0]);
+        cfg.flag |= IPERF_FLAG_CLIENT;
+    }
+
+    if (iperf_args.length->count == 0) {
+        cfg.len_send_buf = 0;
+    } else {
+        cfg.len_send_buf = iperf_args.length->ival[0];
+    }
+
+    cfg.source_ip4 = INADDR_ANY;
+
+    /* iperf -u */
+    if (iperf_args.udp->count == 0) {
+        cfg.flag |= IPERF_FLAG_TCP;
+    } else {
+        cfg.flag |= IPERF_FLAG_UDP;
+    }
+
+    /* iperf -p */
+    if (iperf_args.port->count == 0) {
+        cfg.sport = IPERF_DEFAULT_PORT;
+        cfg.dport = IPERF_DEFAULT_PORT;
+    } else {
+        if (cfg.flag & IPERF_FLAG_SERVER) {
+            cfg.sport = iperf_args.port->ival[0];
+            cfg.dport = IPERF_DEFAULT_PORT;
+        } else {
+            cfg.sport = IPERF_DEFAULT_PORT;
+            cfg.dport = iperf_args.port->ival[0];
+        }
+    }
+
+    /* iperf -i */
+    if (iperf_args.interval->count == 0) {
+        cfg.interval = IPERF_DEFAULT_INTERVAL;
+    } else {
+        cfg.interval = iperf_args.interval->ival[0];
+        if (cfg.interval <= 0) {
+            cfg.interval = IPERF_DEFAULT_INTERVAL;
+        }
+    }
+
+    /* iperf -t */
+    if (iperf_args.time->count == 0) {
+        cfg.time = IPERF_DEFAULT_TIME;
+    } else {
+        cfg.time = iperf_args.time->ival[0];
+        if (cfg.time <= cfg.interval) {
+            cfg.time = cfg.interval;
+        }
+    }
+
+    /* iperf -b */
+    if (iperf_args.bw_limit->count == 0) {
+        cfg.bw_lim = IPERF_DEFAULT_NO_BW_LIMIT;
+    } else {
+        cfg.bw_lim = iperf_args.bw_limit->ival[0];
+        if (cfg.bw_lim <= 0) {
+            cfg.bw_lim = IPERF_DEFAULT_NO_BW_LIMIT;
+        }
+    }
+
+    printf("mode=%s-%s sip=" IPSTR ":%" PRIu16 ", dip=%" PRIu32 ".%" PRIu32 ".%" PRIu32 ".%" PRIu32 ":%" PRIu16 ", interval=%" PRIu32 ", time=%" PRIu32 "\r\n",
+           cfg.flag & IPERF_FLAG_TCP ? "tcp" : "udp",
+           cfg.flag & IPERF_FLAG_SERVER ? "server" : "client",
+           (uint16_t) cfg.source_ip4 & 0xFF,
+           (uint16_t) (cfg.source_ip4 >> 8) & 0xFF,
+           (uint16_t) (cfg.source_ip4 >> 16) & 0xFF,
+           (uint16_t) (cfg.source_ip4 >> 24) & 0xFF,
+           cfg.sport,
+           cfg.destination_ip4 & 0xFF, (cfg.destination_ip4 >> 8) & 0xFF,
+           (cfg.destination_ip4 >> 16) & 0xFF, (cfg.destination_ip4 >> 24) & 0xFF, cfg.dport,
+           cfg.interval, cfg.time);
+
+    iperf_start(&cfg);
+    return 0;
+}
+
+void register_iperf(void)
+{
+
+    iperf_args.ip = arg_str0("c", "client", "<ip>",
+                             "run in client mode, connecting to <host>");
+    iperf_args.server = arg_lit0("s", "server", "run in server mode");
+    iperf_args.udp = arg_lit0("u", "udp", "use UDP rather than TCP");
+    iperf_args.version = arg_lit0("V", "ipv6_domain", "use IPV6 address rather than IPV4");
+    iperf_args.port = arg_int0("p", "port", "<port>",
+                               "server port to listen on/connect to");
+    iperf_args.length = arg_int0("l", "len", "<length>", "set read/write buffer size");
+    iperf_args.interval = arg_int0("i", "interval", "<interval>",
+                                   "seconds between periodic bandwidth reports");
+    iperf_args.time = arg_int0("t", "time", "<time>", "time in seconds to transmit for (default 10 secs)");
+    iperf_args.bw_limit = arg_int0("b", "bandwidth", "<bandwidth>", "bandwidth to send at in Mbits/sec");
+    iperf_args.abort = arg_lit0("a", "abort", "abort running iperf");
+    iperf_args.end = arg_end(1);
+    const esp_console_cmd_t iperf_cmd = {
+        .command = "iperf",
+        .help = "iperf command",
+        .hint = NULL,
+        .func = &ppp_cmd_iperf,
+        .argtable = &iperf_args
+    };
+    ESP_ERROR_CHECK(esp_console_cmd_register(&iperf_cmd));
+}
diff --git a/components/eppp_link/examples/host/sdkconfig.defaults b/components/eppp_link/examples/host/sdkconfig.defaults
new file mode 100644
index 0000000000..2857f7ac8e
--- /dev/null
+++ b/components/eppp_link/examples/host/sdkconfig.defaults
@@ -0,0 +1,8 @@
+# This file was generated using idf.py save-defconfig. It can be edited manually.
+# Espressif IoT Development Framework (ESP-IDF) 5.3.0 Project Minimal Configuration
+#
+CONFIG_UART_ISR_IN_IRAM=y
+CONFIG_LWIP_PPP_SUPPORT=y
+CONFIG_LWIP_PPP_VJ_HEADER_COMPRESSION=n
+CONFIG_LWIP_PPP_DEBUG_ON=y
+CONFIG_EPPP_LINK_DEVICE_SPI=y
diff --git a/components/eppp_link/examples/rpc/client/CMakeLists.txt b/components/eppp_link/examples/rpc/client/CMakeLists.txt
new file mode 100644
index 0000000000..3405abc526
--- /dev/null
+++ b/components/eppp_link/examples/rpc/client/CMakeLists.txt
@@ -0,0 +1,8 @@
+# The following four lines of boilerplate have to be in your project's CMakeLists
+# in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.16)
+set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/examples/common_components/iperf)
+
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+project(pppos_host)
diff --git a/components/eppp_link/examples/rpc/client/README.md b/components/eppp_link/examples/rpc/client/README.md
new file mode 100644
index 0000000000..a6592627c6
--- /dev/null
+++ b/components/eppp_link/examples/rpc/client/README.md
@@ -0,0 +1,9 @@
+
+# Client side demo of ESP-PPP-Link
+
+This is a basic demo of using esp-mqtt library, but connects to the internet using a PPPoS client. To run this example, you would need a PPP server that provides connectivity to the MQTT broker used in this example (by default a public broker accessible on the internet).
+
+If configured, this example could also run a ping session and an iperf console.
+
+
+The PPP server could be a Linux computer with `pppd` service or an ESP32 acting like a connection gateway with PPPoS server (see the "slave" project).
diff --git a/components/eppp_link/examples/rpc/client/main/CMakeLists.txt b/components/eppp_link/examples/rpc/client/main/CMakeLists.txt
new file mode 100644
index 0000000000..e6169a604e
--- /dev/null
+++ b/components/eppp_link/examples/rpc/client/main/CMakeLists.txt
@@ -0,0 +1,3 @@
+idf_component_register(SRCS app_main.c register_iperf.c client.cpp
+                    PRIV_INCLUDE_DIRS ../../common
+                    INCLUDE_DIRS ".")
diff --git a/components/eppp_link/examples/rpc/client/main/Kconfig.projbuild b/components/eppp_link/examples/rpc/client/main/Kconfig.projbuild
new file mode 100644
index 0000000000..503bfa61a5
--- /dev/null
+++ b/components/eppp_link/examples/rpc/client/main/Kconfig.projbuild
@@ -0,0 +1,21 @@
+menu "Example Configuration"
+
+    config ESP_WIFI_SSID
+        string "WiFi SSID"
+        default "myssid"
+        help
+            SSID (network name) for the example to connect to.
+
+    config ESP_WIFI_PASSWORD
+        string "WiFi Password"
+        default "mypassword"
+        help
+            WiFi password (WPA or WPA2) for the example to use.
+
+    config EXAMPLE_IPERF
+        bool "Run iperf"
+        default y
+        help
+            Init and run iperf console.
+
+endmenu
diff --git a/components/eppp_link/examples/rpc/client/main/app_main.c b/components/eppp_link/examples/rpc/client/main/app_main.c
new file mode 100644
index 0000000000..4c85c6abe3
--- /dev/null
+++ b/components/eppp_link/examples/rpc/client/main/app_main.c
@@ -0,0 +1,305 @@
+/*
+ * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+#include "esp_system.h"
+#include "nvs_flash.h"
+#include "esp_event.h"
+#include "esp_netif.h"
+#include "eppp_link.h"
+#include "lwip/sockets.h"
+#include "esp_log.h"
+#include "mqtt_client.h"
+#include "ping/ping_sock.h"
+#include "esp_console.h"
+#include "esp_wifi_remote.h"
+
+void register_iperf(void);
+esp_err_t client_init(void);
+
+static const char *TAG = "eppp_host_example";
+
+#if CONFIG_EXAMPLE_MQTT
+static void mqtt_event_handler(void *args, esp_event_base_t base, int32_t event_id, void *event_data)
+{
+    ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32 "", base, event_id);
+    esp_mqtt_event_handle_t event = event_data;
+    esp_mqtt_client_handle_t client = event->client;
+    int msg_id;
+    switch ((esp_mqtt_event_id_t)event_id) {
+    case MQTT_EVENT_CONNECTED:
+        ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED");
+        msg_id = esp_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0);
+        ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
+
+        msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0);
+        ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
+
+        msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1);
+        ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id);
+
+        msg_id = esp_mqtt_client_unsubscribe(client, "/topic/qos1");
+        ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id);
+        break;
+    case MQTT_EVENT_DISCONNECTED:
+        ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED");
+        break;
+
+    case MQTT_EVENT_SUBSCRIBED:
+        ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id);
+        msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0);
+        ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id);
+        break;
+    case MQTT_EVENT_UNSUBSCRIBED:
+        ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id);
+        break;
+    case MQTT_EVENT_PUBLISHED:
+        ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id);
+        break;
+    case MQTT_EVENT_DATA:
+        ESP_LOGI(TAG, "MQTT_EVENT_DATA");
+        printf("TOPIC=%.*s\r\n", event->topic_len, event->topic);
+        printf("DATA=%.*s\r\n", event->data_len, event->data);
+        break;
+    case MQTT_EVENT_ERROR:
+        ESP_LOGI(TAG, "MQTT_EVENT_ERROR");
+        if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) {
+            ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno));
+        }
+        break;
+    default:
+        ESP_LOGI(TAG, "Other event id:%d", event->event_id);
+        break;
+    }
+}
+
+static void mqtt_app_start(void)
+{
+    esp_mqtt_client_config_t mqtt_cfg = {
+        .broker.address.uri = CONFIG_EXAMPLE_BROKER_URL,
+    };
+
+    esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg);
+    /* The last argument may be used to pass data to the event handler, in this example mqtt_event_handler */
+    esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL);
+    esp_mqtt_client_start(client);
+}
+#endif // MQTT
+
+#if CONFIG_EXAMPLE_ICMP_PING
+static void test_on_ping_success(esp_ping_handle_t hdl, void *args)
+{
+    uint8_t ttl;
+    uint16_t seqno;
+    uint32_t elapsed_time, recv_len;
+    ip_addr_t target_addr;
+    esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &recv_len, sizeof(recv_len));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &elapsed_time, sizeof(elapsed_time));
+    printf("%" PRId32 "bytes from %s icmp_seq=%d ttl=%d time=%" PRId32 " ms\n",
+           recv_len, inet_ntoa(target_addr.u_addr.ip4), seqno, ttl, elapsed_time);
+}
+
+static void test_on_ping_timeout(esp_ping_handle_t hdl, void *args)
+{
+    uint16_t seqno;
+    ip_addr_t target_addr;
+    esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
+    printf("From %s icmp_seq=%d timeout\n", inet_ntoa(target_addr.u_addr.ip4), seqno);
+}
+
+static void test_on_ping_end(esp_ping_handle_t hdl, void *args)
+{
+    uint32_t transmitted;
+    uint32_t received;
+    uint32_t total_time_ms;
+    esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &received, sizeof(received));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &total_time_ms, sizeof(total_time_ms));
+    printf("%" PRId32 " packets transmitted, %" PRId32 " received, time %" PRId32 "ms\n", transmitted, received, total_time_ms);
+
+}
+#endif // PING
+
+/*
+ * local netifs (wifi and ppp)
+ */
+static esp_netif_t *s_wifi_netif;
+static esp_netif_t *s_ppp_netif;
+static eppp_channel_fn_t s_tx;
+
+static esp_err_t remote_wifi_receive(void *h, void *buffer, size_t len)
+{
+    if (s_wifi_netif) {
+        return esp_netif_receive(s_wifi_netif, buffer, len, NULL);
+    }
+    return ESP_OK;
+}
+
+esp_err_t remote_wifi_transmit_wrap(void *h, void *buffer, size_t len, void *netstack_buffer)
+{
+    if (s_tx) {
+        return s_tx(s_ppp_netif, buffer, len);
+    }
+    return ESP_OK;
+}
+
+static esp_err_t remote_wifi_transmit(void *h, void *buffer, size_t len)
+{
+    if (s_tx) {
+        return s_tx(s_ppp_netif, buffer, len);
+    }
+    return ESP_OK;
+}
+
+// this is needed as the ESP_NETIF_NETSTACK_DEFAULT_WIFI_STA config frees the eb on pbuf-free
+static void wifi_free(void *h, void *buffer)
+{
+}
+
+static void remote_wifi_netif(void)
+{
+    esp_netif_driver_ifconfig_t driver_cfg = {
+        .handle = (void *)1,
+        .transmit = remote_wifi_transmit,
+        .transmit_wrap = remote_wifi_transmit_wrap,
+        .driver_free_rx_buffer = wifi_free
+
+    };
+    const esp_netif_driver_ifconfig_t *wifi_driver_cfg = &driver_cfg;
+    esp_netif_config_t netif_config = {
+        .base = ESP_NETIF_BASE_DEFAULT_WIFI_STA,
+        .driver = wifi_driver_cfg,
+        .stack = ESP_NETIF_NETSTACK_DEFAULT_WIFI_STA
+    };
+    s_wifi_netif = esp_netif_new(&netif_config);
+}
+
+static void wifi_init(void *ctx)
+{
+    client_init();
+
+    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
+    ESP_ERROR_CHECK(esp_wifi_remote_init(&cfg));
+
+    wifi_config_t wifi_config = {
+        .sta = {
+            .ssid = CONFIG_ESP_WIFI_SSID,
+            .password = CONFIG_ESP_WIFI_PASSWORD,
+        },
+    };
+
+    esp_err_t err = esp_wifi_remote_set_mode(WIFI_MODE_STA);
+    ESP_LOGI(TAG, "esp_wifi_remote_set_mode() returned = %x", err);
+    ESP_ERROR_CHECK(esp_wifi_remote_set_config(WIFI_IF_STA, &wifi_config) );
+    ESP_ERROR_CHECK(esp_wifi_remote_start() );
+    vTaskDelay(pdMS_TO_TICKS(1000));
+    uint8_t mac[6];
+    ESP_ERROR_CHECK(esp_wifi_remote_get_mac(WIFI_IF_STA, mac) );
+
+    esp_netif_set_mac(s_wifi_netif, mac);
+    vTaskDelay(pdMS_TO_TICKS(1000));    // we're not supporting WIFI_EVENT yet, just play with delays for now
+
+    esp_netif_action_start(s_wifi_netif, 0, 0, 0);
+    ESP_ERROR_CHECK(esp_wifi_remote_connect() );
+
+    esp_netif_action_connected(s_wifi_netif, 0, 0, 0);
+    vTaskDelete(NULL);
+}
+
+void app_main(void)
+{
+    ESP_LOGI(TAG, "[APP] Startup..");
+    ESP_LOGI(TAG, "[APP] Free memory: %" PRIu32 " bytes", esp_get_free_heap_size());
+    ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
+
+    ESP_ERROR_CHECK(nvs_flash_init());
+    ESP_ERROR_CHECK(esp_netif_init());
+    ESP_ERROR_CHECK(esp_event_loop_create_default());
+
+    /* Sets up the default EPPP-connection
+     */
+    eppp_config_t config = EPPP_DEFAULT_CLIENT_CONFIG();
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+    config.transport = EPPP_TRANSPORT_SPI;
+    config.task.priority = 14;
+    config.spi.freq = 40000000;
+#else
+    config.transport = EPPP_TRANSPORT_UART;
+    config.uart.tx_io = 10;
+    config.uart.rx_io = 11;
+    config.uart.baud = 2000000;
+#endif
+    s_ppp_netif = eppp_connect(&config);
+    if (s_ppp_netif == NULL) {
+        ESP_LOGE(TAG, "Failed to connect");
+        return ;
+    }
+    eppp_add_channel(1, &s_tx, remote_wifi_receive);
+    remote_wifi_netif();
+    if (s_wifi_netif == NULL) {
+        ESP_LOGE(TAG, "Failed to create wifi netif");
+        return ;
+    }
+
+    xTaskCreate(&wifi_init, "initwifi", 8192, NULL, 18, NULL);
+
+#if CONFIG_EXAMPLE_IPERF
+    esp_console_repl_t *repl = NULL;
+    esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT();
+    esp_console_dev_uart_config_t uart_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT();
+    repl_config.prompt = "iperf>";
+    // init console REPL environment
+    ESP_ERROR_CHECK(esp_console_new_repl_uart(&uart_config, &repl_config, &repl));
+
+    register_iperf();
+
+    printf("\n =======================================================\n");
+    printf(" |       Steps to Test PPP Client Bandwidth            |\n");
+    printf(" |                                                     |\n");
+    printf(" |  1. Enter 'help', check all supported commands      |\n");
+    printf(" |  2. Start PPP server on host system                 |\n");
+    printf(" |     - pppd /dev/ttyUSB1 115200 192.168.11.1:192.168.11.2 modem local noauth debug nocrtscts nodetach +ipv6\n");
+    printf(" |  3. Wait ESP32 to get IP from PPP server            |\n");
+    printf(" |  4. Enter 'pppd info' (optional)                    |\n");
+    printf(" |  5. Server: 'iperf -u -s -i 3'                      |\n");
+    printf(" |  6. Client: 'iperf -u -c SERVER_IP -t 60 -i 3'      |\n");
+    printf(" |                                                     |\n");
+    printf(" =======================================================\n\n");
+
+    // start console REPL
+    ESP_ERROR_CHECK(esp_console_start_repl(repl));
+#endif
+
+#if CONFIG_EXAMPLE_ICMP_PING
+    ip_addr_t target_addr = { .type = IPADDR_TYPE_V4, .u_addr.ip4.addr = esp_netif_htonl(CONFIG_EXAMPLE_PING_ADDR) };
+
+    esp_ping_config_t ping_config = ESP_PING_DEFAULT_CONFIG();
+    ping_config.timeout_ms = 2000;
+    ping_config.interval_ms = 20,
+    ping_config.target_addr = target_addr;
+    ping_config.count = 100; // ping in infinite mode
+    /* set callback functions */
+    esp_ping_callbacks_t cbs;
+    cbs.on_ping_success = test_on_ping_success;
+    cbs.on_ping_timeout = test_on_ping_timeout;
+    cbs.on_ping_end = test_on_ping_end;
+    esp_ping_handle_t ping;
+    esp_ping_new_session(&ping_config, &cbs, &ping);
+    /* start ping */
+    esp_ping_start(ping);
+#endif // PING
+
+#if CONFIG_EXAMPLE_MQTT
+    mqtt_app_start();
+#endif
+}
diff --git a/components/eppp_link/examples/rpc/client/main/client.cpp b/components/eppp_link/examples/rpc/client/main/client.cpp
new file mode 100644
index 0000000000..c06242a928
--- /dev/null
+++ b/components/eppp_link/examples/rpc/client/main/client.cpp
@@ -0,0 +1,192 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+#include "esp_log.h"
+#include "esp_tls.h"
+#include "esp_wifi.h"
+#include <sys/socket.h>
+#include <netdb.h>
+#include <errno.h>
+#include <memory>
+#include "rpc.hpp"
+#include "esp_wifi_remote.h"
+
+#define PORT 3333
+
+
+static const char *TAG = "client";
+esp_tls_t *tls;
+
+
+
+extern "C" esp_err_t client_init(void)
+{
+    esp_err_t ret = ESP_OK;
+//    char buf[512];
+//    int len;
+    const char host[] = "192.168.11.1";
+    int port = 3333;
+    esp_tls_cfg_t cfg = {};
+    cfg.skip_common_name = true;
+
+    tls = esp_tls_init();
+    if (!tls) {
+        ESP_LOGE(TAG, "Failed to allocate esp_tls handle!");
+        ret = ESP_FAIL;
+        goto exit;
+    }
+    if (esp_tls_conn_new_sync(host, strlen(host), port, &cfg, tls) <= 0) {
+        ESP_LOGE(TAG, "Failed to open a new connection");
+        ret = ESP_FAIL;
+        goto exit;
+    }
+
+//
+//
+//    len = esp_tls_conn_write(tls,"AHOJ",4);
+//    if (len <= 0) {
+//        ESP_LOGE(TAG, "Failed to write data to the connection");
+//        goto cleanup;
+//    }
+//    memset(buf, 0x00, sizeof(buf));
+//    len = esp_tls_conn_read(tls, (char *)buf, len);
+//    if (len <= 0) {
+//        ESP_LOGE(TAG, "Failed to read data from the connection");
+//        goto cleanup;
+//    }
+//    ESP_LOGI(TAG, "Data from the connection (size=%d)", len);
+//    ESP_LOG_BUFFER_HEXDUMP(TAG, buf, len, ESP_LOG_INFO);
+
+//cleanup:
+//    esp_tls_conn_destroy(tls);
+exit:
+    return ret;
+}
+
+extern "C" int client_deinit(void)
+{
+    return esp_tls_conn_destroy(tls);
+}
+
+
+//template <typename T> uint8_t *marshall(uint32_t id, T *t, size_t& len)
+//{
+//    len = sizeof(RpcData<T>);
+//    auto *data = new RpcData<T>();
+//    data->id = id;
+//    data->size = sizeof(RpcData<T>);
+//    memcpy(&data->value, t, sizeof(T));
+//    return data->get();
+//}
+
+
+extern "C" esp_err_t esp_wifi_remote_set_mode(wifi_mode_t mode)
+{
+//    auto data = std::make_unique<RpcData<wifi_mode_t>>(SET_MODE);
+//    size_t size;
+//    auto buf = data->marshall(&mode, size);
+////    auto buf = marshall(SET_MODE, &mode, size);
+////    ESP_LOGE(TAG, "size=%d", (int)data->size_);
+//    ESP_LOG_BUFFER_HEXDUMP(TAG, buf, size, ESP_LOG_INFO);
+//    int len = esp_tls_conn_write(tls, buf, size);
+//    if (len <= 0) {
+//        ESP_LOGE(TAG, "Failed to write data to the connection");
+//        return ESP_FAIL;
+//    }
+    RpcEngine rpc(tls);
+
+    if (rpc.send(SET_MODE, &mode) != ESP_OK) {
+        return ESP_FAIL;
+    }
+
+//    RpcHeader header{};
+
+    auto header = rpc.get_header();
+    auto resp = rpc.get_payload<esp_err_t>(SET_MODE, header);
+
+//    int len = esp_tls_conn_read(tls, (char *)&header, sizeof(header));
+//    if (len <= 0) {
+//        ESP_LOGE(TAG, "Failed to read data from the connection");
+//        return ESP_FAIL;
+//    }
+
+//    auto resp = std::make_unique<RpcData<esp_err_t>>(SET_MODE);
+//    if (resp->head.size != header.size || resp->head.id != header.id) {
+//        ESP_LOGE(TAG, "Data size mismatch problem! %d expected, %d given", (int)resp->head.size, (int)header.size);
+//        ESP_LOGE(TAG, "API ID mismatch problem! %d expected, %d given", (int)resp->head.id, (int)header.id);
+//        return ESP_FAIL;
+//    }
+//    int len = esp_tls_conn_read(tls, (char *)resp->value(), resp->head.size);
+//    if (len <= 0) {
+//        ESP_LOGE(TAG, "Failed to read data from the connection");
+//        return ESP_FAIL;
+//    }
+//    ESP_LOG_BUFFER_HEXDUMP(TAG, resp->value(), data->head.size, ESP_LOG_INFO);
+//    ESP_LOGE(TAG, "value_ is returned %x", *(int*)(resp->value()));
+
+    return resp;
+}
+
+
+extern "C" esp_err_t esp_wifi_remote_set_config(wifi_interface_t interface, wifi_config_t *conf)
+{
+    RpcEngine rpc(tls);
+    esp_wifi_remote_config params = { .interface = interface, .conf = {} };
+    memcpy(&params.conf, conf, sizeof(wifi_config_t));
+    if (rpc.send(SET_CONFIG, &params) != ESP_OK) {
+        return ESP_FAIL;
+    }
+    auto header = rpc.get_header();
+    return rpc.get_payload<esp_err_t>(SET_CONFIG, header);
+}
+
+extern "C" esp_err_t esp_wifi_remote_init(wifi_init_config_t *config)
+{
+    RpcEngine rpc(tls);
+
+    if (rpc.send(INIT, config) != ESP_OK) {
+        return ESP_FAIL;
+    }
+    auto header = rpc.get_header();
+    return rpc.get_payload<esp_err_t>(INIT, header);
+}
+
+extern "C" esp_err_t esp_wifi_remote_start(void)
+{
+    RpcEngine rpc(tls);
+
+    if (rpc.send(START) != ESP_OK) {
+        return ESP_FAIL;
+    }
+    auto header = rpc.get_header();
+    return rpc.get_payload<esp_err_t>(START, header);
+}
+
+extern "C" esp_err_t esp_wifi_remote_connect(void)
+{
+    RpcEngine rpc(tls);
+
+    if (rpc.send(CONNECT) != ESP_OK) {
+        return ESP_FAIL;
+    }
+    auto header = rpc.get_header();
+    return rpc.get_payload<esp_err_t>(CONNECT, header);
+}
+
+extern "C" esp_err_t esp_wifi_remote_get_mac(wifi_interface_t ifx, uint8_t mac[6])
+{
+    RpcEngine rpc(tls);
+
+    if (rpc.send(GET_MAC, &ifx) != ESP_OK) {
+        return ESP_FAIL;
+    }
+    auto header = rpc.get_header();
+    auto ret = rpc.get_payload<esp_wifi_remote_mac_t>(GET_MAC, header);
+    ESP_LOG_BUFFER_HEXDUMP("MAC", ret.mac, 6, ESP_LOG_INFO);
+
+    memcpy(mac, ret.mac, 6);
+    return ret.err;
+
+}
diff --git a/components/eppp_link/examples/rpc/client/main/idf_component.yml b/components/eppp_link/examples/rpc/client/main/idf_component.yml
new file mode 100644
index 0000000000..42c0a17d95
--- /dev/null
+++ b/components/eppp_link/examples/rpc/client/main/idf_component.yml
@@ -0,0 +1,4 @@
+dependencies:
+  espressif/eppp_link:
+    version: "*"
+    override_path: "../../../.."
diff --git a/components/eppp_link/examples/rpc/client/main/register_iperf.c b/components/eppp_link/examples/rpc/client/main/register_iperf.c
new file mode 100644
index 0000000000..63fded10c5
--- /dev/null
+++ b/components/eppp_link/examples/rpc/client/main/register_iperf.c
@@ -0,0 +1,179 @@
+/*
+ * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <inttypes.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/event_groups.h"
+#include "sys/socket.h" // for INADDR_ANY
+#include "esp_netif.h"
+#include "esp_log.h"
+#include "esp_system.h"
+#include "esp_event.h"
+#include "esp_log.h"
+#include "esp_netif.h"
+#include "esp_netif_ppp.h"
+#include "freertos/FreeRTOS.h"
+#include "freertos/event_groups.h"
+
+#include "esp_console.h"
+#include "esp_event.h"
+#include "esp_bit_defs.h"
+#include "argtable3/argtable3.h"
+#include "iperf.h"
+#include "sdkconfig.h"
+
+/* "iperf" command */
+
+static struct {
+    struct arg_str *ip;
+    struct arg_lit *server;
+    struct arg_lit *udp;
+    struct arg_lit *version;
+    struct arg_int *port;
+    struct arg_int *length;
+    struct arg_int *interval;
+    struct arg_int *time;
+    struct arg_int *bw_limit;
+    struct arg_lit *abort;
+    struct arg_end *end;
+} iperf_args;
+
+static int ppp_cmd_iperf(int argc, char **argv)
+{
+    int nerrors = arg_parse(argc, argv, (void **)&iperf_args);
+    // ethernet iperf only support IPV4 address
+    iperf_cfg_t cfg = {.type = IPERF_IP_TYPE_IPV4};
+
+    if (nerrors != 0) {
+        arg_print_errors(stderr, iperf_args.end, argv[0]);
+        return 0;
+    }
+
+    /* iperf -a */
+    if (iperf_args.abort->count != 0) {
+        iperf_stop();
+        return 0;
+    }
+
+    if (((iperf_args.ip->count == 0) && (iperf_args.server->count == 0)) ||
+            ((iperf_args.ip->count != 0) && (iperf_args.server->count != 0))) {
+        ESP_LOGE(__func__, "Wrong mode! ESP32 should run in client or server mode");
+        return 0;
+    }
+
+    /* iperf -s */
+    if (iperf_args.ip->count == 0) {
+        cfg.flag |= IPERF_FLAG_SERVER;
+    }
+    /* iperf -c SERVER_ADDRESS */
+    else {
+        cfg.destination_ip4 = esp_ip4addr_aton(iperf_args.ip->sval[0]);
+        cfg.flag |= IPERF_FLAG_CLIENT;
+    }
+
+    if (iperf_args.length->count == 0) {
+        cfg.len_send_buf = 0;
+    } else {
+        cfg.len_send_buf = iperf_args.length->ival[0];
+    }
+
+    cfg.source_ip4 = INADDR_ANY;
+
+    /* iperf -u */
+    if (iperf_args.udp->count == 0) {
+        cfg.flag |= IPERF_FLAG_TCP;
+    } else {
+        cfg.flag |= IPERF_FLAG_UDP;
+    }
+
+    /* iperf -p */
+    if (iperf_args.port->count == 0) {
+        cfg.sport = IPERF_DEFAULT_PORT;
+        cfg.dport = IPERF_DEFAULT_PORT;
+    } else {
+        if (cfg.flag & IPERF_FLAG_SERVER) {
+            cfg.sport = iperf_args.port->ival[0];
+            cfg.dport = IPERF_DEFAULT_PORT;
+        } else {
+            cfg.sport = IPERF_DEFAULT_PORT;
+            cfg.dport = iperf_args.port->ival[0];
+        }
+    }
+
+    /* iperf -i */
+    if (iperf_args.interval->count == 0) {
+        cfg.interval = IPERF_DEFAULT_INTERVAL;
+    } else {
+        cfg.interval = iperf_args.interval->ival[0];
+        if (cfg.interval <= 0) {
+            cfg.interval = IPERF_DEFAULT_INTERVAL;
+        }
+    }
+
+    /* iperf -t */
+    if (iperf_args.time->count == 0) {
+        cfg.time = IPERF_DEFAULT_TIME;
+    } else {
+        cfg.time = iperf_args.time->ival[0];
+        if (cfg.time <= cfg.interval) {
+            cfg.time = cfg.interval;
+        }
+    }
+
+    /* iperf -b */
+    if (iperf_args.bw_limit->count == 0) {
+        cfg.bw_lim = IPERF_DEFAULT_NO_BW_LIMIT;
+    } else {
+        cfg.bw_lim = iperf_args.bw_limit->ival[0];
+        if (cfg.bw_lim <= 0) {
+            cfg.bw_lim = IPERF_DEFAULT_NO_BW_LIMIT;
+        }
+    }
+
+    printf("mode=%s-%s sip=" IPSTR ":%" PRIu16 ", dip=%" PRIu32 ".%" PRIu32 ".%" PRIu32 ".%" PRIu32 ":%" PRIu16 ", interval=%" PRIu32 ", time=%" PRIu32 "\r\n",
+           cfg.flag & IPERF_FLAG_TCP ? "tcp" : "udp",
+           cfg.flag & IPERF_FLAG_SERVER ? "server" : "client",
+           (uint16_t) cfg.source_ip4 & 0xFF,
+           (uint16_t) (cfg.source_ip4 >> 8) & 0xFF,
+           (uint16_t) (cfg.source_ip4 >> 16) & 0xFF,
+           (uint16_t) (cfg.source_ip4 >> 24) & 0xFF,
+           cfg.sport,
+           cfg.destination_ip4 & 0xFF, (cfg.destination_ip4 >> 8) & 0xFF,
+           (cfg.destination_ip4 >> 16) & 0xFF, (cfg.destination_ip4 >> 24) & 0xFF, cfg.dport,
+           cfg.interval, cfg.time);
+
+    iperf_start(&cfg);
+    return 0;
+}
+
+void register_iperf(void)
+{
+
+    iperf_args.ip = arg_str0("c", "client", "<ip>",
+                             "run in client mode, connecting to <host>");
+    iperf_args.server = arg_lit0("s", "server", "run in server mode");
+    iperf_args.udp = arg_lit0("u", "udp", "use UDP rather than TCP");
+    iperf_args.version = arg_lit0("V", "ipv6_domain", "use IPV6 address rather than IPV4");
+    iperf_args.port = arg_int0("p", "port", "<port>",
+                               "server port to listen on/connect to");
+    iperf_args.length = arg_int0("l", "len", "<length>", "set read/write buffer size");
+    iperf_args.interval = arg_int0("i", "interval", "<interval>",
+                                   "seconds between periodic bandwidth reports");
+    iperf_args.time = arg_int0("t", "time", "<time>", "time in seconds to transmit for (default 10 secs)");
+    iperf_args.bw_limit = arg_int0("b", "bandwidth", "<bandwidth>", "bandwidth to send at in Mbits/sec");
+    iperf_args.abort = arg_lit0("a", "abort", "abort running iperf");
+    iperf_args.end = arg_end(1);
+    const esp_console_cmd_t iperf_cmd = {
+        .command = "iperf",
+        .help = "iperf command",
+        .hint = NULL,
+        .func = &ppp_cmd_iperf,
+        .argtable = &iperf_args
+    };
+    ESP_ERROR_CHECK(esp_console_cmd_register(&iperf_cmd));
+}
diff --git a/components/eppp_link/examples/rpc/client/sdkconfig.defaults b/components/eppp_link/examples/rpc/client/sdkconfig.defaults
new file mode 100644
index 0000000000..b4b1b3a8fc
--- /dev/null
+++ b/components/eppp_link/examples/rpc/client/sdkconfig.defaults
@@ -0,0 +1,12 @@
+# This file was generated using idf.py save-defconfig. It can be edited manually.
+# Espressif IoT Development Framework (ESP-IDF) 5.3.0 Project Minimal Configuration
+#
+CONFIG_IDF_TARGET="esp32s3"
+CONFIG_ESP_TLS_INSECURE=y
+CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY=y
+CONFIG_ESP_SYSTEM_PANIC_PRINT_HALT=y
+CONFIG_LWIP_PPP_SUPPORT=y
+CONFIG_LWIP_PPP_SERVER_SUPPORT=y
+CONFIG_LWIP_PPP_VJ_HEADER_COMPRESSION=n
+CONFIG_EPPP_LINK_DEVICE_SPI=y
+CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=4096
diff --git a/components/eppp_link/examples/rpc/common/esp_wifi_remote.h b/components/eppp_link/examples/rpc/common/esp_wifi_remote.h
new file mode 100644
index 0000000000..2c59d65aff
--- /dev/null
+++ b/components/eppp_link/examples/rpc/common/esp_wifi_remote.h
@@ -0,0 +1,39 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+#pragma once
+
+#include "esp_wifi.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct esp_wifi_remote_config {
+    wifi_interface_t interface;
+    wifi_config_t conf;
+};
+
+struct esp_wifi_remote_mac_t {
+    esp_err_t err;
+    uint8_t mac[6];
+};
+
+
+esp_err_t esp_wifi_remote_set_config(wifi_interface_t interface, wifi_config_t *conf);
+
+esp_err_t esp_wifi_remote_set_mode(wifi_mode_t mode);
+
+esp_err_t esp_wifi_remote_init(wifi_init_config_t *config);
+
+esp_err_t esp_wifi_remote_start(void);
+
+esp_err_t esp_wifi_remote_connect(void);
+
+esp_err_t esp_wifi_remote_get_mac(wifi_interface_t ifx, uint8_t mac[6]);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/components/eppp_link/examples/rpc/common/rpc.hpp b/components/eppp_link/examples/rpc/common/rpc.hpp
new file mode 100644
index 0000000000..1f5fbf02e6
--- /dev/null
+++ b/components/eppp_link/examples/rpc/common/rpc.hpp
@@ -0,0 +1,102 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+#pragma once
+
+typedef enum api_id {
+    INIT,
+    SET_MODE,
+    SET_CONFIG,
+    START,
+    CONNECT,
+    GET_MAC
+} api_id_t;
+
+struct RpcHeader {
+    uint32_t id;
+    uint32_t size;
+} __attribute((__packed__));
+
+template <typename T> struct RpcData {
+    RpcHeader head;
+    T value_;
+    explicit RpcData(api_id_t id):  head{id, sizeof(T)} {}
+
+
+    uint8_t *value()
+    {
+        return (uint8_t *)&value_;
+    }
+
+    uint8_t *marshall(T *t, size_t &size)
+    {
+        size = head.size + sizeof(RpcHeader);
+        memcpy(&value_, t, sizeof(T));
+        return (uint8_t *)this;
+    }
+
+
+} __attribute((__packed__));
+
+class RpcEngine {
+public:
+    explicit RpcEngine(esp_tls_t *tls): tls_(tls) {}
+
+    template <typename T> esp_err_t send(api_id_t id, T *t)
+    {
+        RpcData<T> req(id);
+        size_t size;
+        auto buf = req.marshall(t, size);
+        ESP_LOGD("rpc", "Sending API id:%d", (int)id);
+        ESP_LOG_BUFFER_HEXDUMP("rpc", buf, size, ESP_LOG_VERBOSE);
+        int len = esp_tls_conn_write(tls_, buf, size);
+        if (len <= 0) {
+            ESP_LOGE("rpc", "Failed to write data to the connection");
+            return ESP_FAIL;
+        }
+        return ESP_OK;
+    }
+
+    esp_err_t send(api_id_t id) // specialization for (void)
+    {
+        RpcHeader head = { .id = id, .size = 0 };
+        int len = esp_tls_conn_write(tls_, &head, sizeof(head));
+        if (len <= 0) {
+            ESP_LOGE("rpc", "Failed to write data to the connection");
+            return ESP_FAIL;
+        }
+        return ESP_OK;
+    }
+
+    RpcHeader get_header()
+    {
+        RpcHeader header{};
+        int len = esp_tls_conn_read(tls_, (char *)&header, sizeof(header));
+        if (len <= 0) {
+            ESP_LOGE("prc", "Failed to read data from the connection");
+            return {};
+        }
+        return header;
+    }
+
+    template <typename T> T get_payload(api_id_t id, RpcHeader &head)
+    {
+        RpcData<T> resp(id);
+        if (head.id != id || head.size != resp.head.size) {
+            ESP_LOGE("prc", "Failed to read data from the connection");
+            return {};
+        }
+        int len = esp_tls_conn_read(tls_, (char *)resp.value(), resp.head.size);
+        if (len <= 0) {
+            ESP_LOGE("rps", "Failed to read data from the connection");
+            return {};
+        }
+        return resp.value_;
+    }
+
+private:
+    esp_tls_t *tls_;
+
+};
diff --git a/components/eppp_link/examples/rpc/server/CMakeLists.txt b/components/eppp_link/examples/rpc/server/CMakeLists.txt
new file mode 100644
index 0000000000..144b9e1a21
--- /dev/null
+++ b/components/eppp_link/examples/rpc/server/CMakeLists.txt
@@ -0,0 +1,6 @@
+# The following five lines of boilerplate have to be in your project's
+# CMakeLists in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.16)
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+project(pppos_slave)
diff --git a/components/eppp_link/examples/rpc/server/README.md b/components/eppp_link/examples/rpc/server/README.md
new file mode 100644
index 0000000000..a8ff85f9f4
--- /dev/null
+++ b/components/eppp_link/examples/rpc/server/README.md
@@ -0,0 +1,7 @@
+
+# Wi-Fi station to PPPoS server
+
+This example demonstrate using NAPT to bring connectivity from WiFi station to PPPoS server.
+
+This example expect a PPPoS client to connect to the server and use the connectivity.
+The client could be a Linux computer with `pppd` service or another microcontroller with PPP client (or another ESP32 with not WiFi interface)
diff --git a/components/eppp_link/examples/rpc/server/main/CMakeLists.txt b/components/eppp_link/examples/rpc/server/main/CMakeLists.txt
new file mode 100644
index 0000000000..d22419860c
--- /dev/null
+++ b/components/eppp_link/examples/rpc/server/main/CMakeLists.txt
@@ -0,0 +1,3 @@
+idf_component_register(SRCS "station_example_main.c" "server.cpp"
+                    PRIV_INCLUDE_DIRS ../../common
+                    INCLUDE_DIRS ".")
diff --git a/components/eppp_link/examples/rpc/server/main/Kconfig.projbuild b/components/eppp_link/examples/rpc/server/main/Kconfig.projbuild
new file mode 100644
index 0000000000..ae4d7fb9a8
--- /dev/null
+++ b/components/eppp_link/examples/rpc/server/main/Kconfig.projbuild
@@ -0,0 +1,21 @@
+menu "Example Configuration"
+
+    config ESP_WIFI_SSID
+        string "WiFi SSID"
+        default "myssid"
+        help
+            SSID (network name) for the example to connect to.
+
+    config ESP_WIFI_PASSWORD
+        string "WiFi Password"
+        default "mypassword"
+        help
+            WiFi password (WPA or WPA2) for the example to use.
+
+    config ESP_MAXIMUM_RETRY
+        int "Maximum retry"
+        default 5
+        help
+            Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent.
+
+endmenu
diff --git a/components/eppp_link/examples/rpc/server/main/idf_component.yml b/components/eppp_link/examples/rpc/server/main/idf_component.yml
new file mode 100644
index 0000000000..42c0a17d95
--- /dev/null
+++ b/components/eppp_link/examples/rpc/server/main/idf_component.yml
@@ -0,0 +1,4 @@
+dependencies:
+  espressif/eppp_link:
+    version: "*"
+    override_path: "../../../.."
diff --git a/components/eppp_link/examples/rpc/server/main/server.cpp b/components/eppp_link/examples/rpc/server/main/server.cpp
new file mode 100644
index 0000000000..a5c34d84f8
--- /dev/null
+++ b/components/eppp_link/examples/rpc/server/main/server.cpp
@@ -0,0 +1,213 @@
+/*
+ * SPDX-FileCopyrightText: 2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+//
+// Created by david on 1/10/24.
+//
+#include "esp_log.h"
+#include "esp_tls.h"
+#include <sys/socket.h>
+#include <netdb.h>
+#include <errno.h>
+#include <memory>
+#include <esp_private/wifi.h>
+#include "esp_wifi.h"
+#include "rpc.hpp"
+#include "esp_wifi_remote.h"
+
+#define PORT 3333
+static const char *TAG = "server";
+static esp_tls_t *tls;
+
+const unsigned char servercert[] = "-----BEGIN CERTIFICATE-----\n"
+                                   "MIIDKzCCAhOgAwIBAgIUBxM3WJf2bP12kAfqhmhhjZWv0ukwDQYJKoZIhvcNAQEL\n"
+                                   "BQAwJTEjMCEGA1UEAwwaRVNQMzIgSFRUUFMgc2VydmVyIGV4YW1wbGUwHhcNMTgx\n"
+                                   "MDE3MTEzMjU3WhcNMjgxMDE0MTEzMjU3WjAlMSMwIQYDVQQDDBpFU1AzMiBIVFRQ\n"
+                                   "UyBzZXJ2ZXIgZXhhbXBsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n"
+                                   "ALBint6nP77RCQcmKgwPtTsGK0uClxg+LwKJ3WXuye3oqnnjqJCwMEneXzGdG09T\n"
+                                   "sA0SyNPwrEgebLCH80an3gWU4pHDdqGHfJQa2jBL290e/5L5MB+6PTs2NKcojK/k\n"
+                                   "qcZkn58MWXhDW1NpAnJtjVniK2Ksvr/YIYSbyD+JiEs0MGxEx+kOl9d7hRHJaIzd\n"
+                                   "GF/vO2pl295v1qXekAlkgNMtYIVAjUy9CMpqaQBCQRL+BmPSJRkXBsYk8GPnieS4\n"
+                                   "sUsp53DsNvCCtWDT6fd9D1v+BB6nDk/FCPKhtjYOwOAZlX4wWNSZpRNr5dfrxKsb\n"
+                                   "jAn4PCuR2akdF4G8WLUeDWECAwEAAaNTMFEwHQYDVR0OBBYEFMnmdJKOEepXrHI/\n"
+                                   "ivM6mVqJgAX8MB8GA1UdIwQYMBaAFMnmdJKOEepXrHI/ivM6mVqJgAX8MA8GA1Ud\n"
+                                   "EwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBADiXIGEkSsN0SLSfCF1VNWO3\n"
+                                   "emBurfOcDq4EGEaxRKAU0814VEmU87btIDx80+z5Dbf+GGHCPrY7odIkxGNn0DJY\n"
+                                   "W1WcF+DOcbiWoUN6DTkAML0SMnp8aGj9ffx3x+qoggT+vGdWVVA4pgwqZT7Ybntx\n"
+                                   "bkzcNFW0sqmCv4IN1t4w6L0A87ZwsNwVpre/j6uyBw7s8YoJHDLRFT6g7qgn0tcN\n"
+                                   "ZufhNISvgWCVJQy/SZjNBHSpnIdCUSJAeTY2mkM4sGxY0Widk8LnjydxZUSxC3Nl\n"
+                                   "hb6pnMh3jRq4h0+5CZielA4/a+TdrNPv/qok67ot/XJdY3qHCCd8O2b14OVq9jo=\n"
+                                   "-----END CERTIFICATE-----";
+const unsigned char prvtkey[] = "-----BEGIN PRIVATE KEY-----\n"
+                                "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCwYp7epz++0QkH\n"
+                                "JioMD7U7BitLgpcYPi8Cid1l7snt6Kp546iQsDBJ3l8xnRtPU7ANEsjT8KxIHmyw\n"
+                                "h/NGp94FlOKRw3ahh3yUGtowS9vdHv+S+TAfuj07NjSnKIyv5KnGZJ+fDFl4Q1tT\n"
+                                "aQJybY1Z4itirL6/2CGEm8g/iYhLNDBsRMfpDpfXe4URyWiM3Rhf7ztqZdveb9al\n"
+                                "3pAJZIDTLWCFQI1MvQjKamkAQkES/gZj0iUZFwbGJPBj54nkuLFLKedw7DbwgrVg\n"
+                                "0+n3fQ9b/gQepw5PxQjyobY2DsDgGZV+MFjUmaUTa+XX68SrG4wJ+DwrkdmpHReB\n"
+                                "vFi1Hg1hAgMBAAECggEAaTCnZkl/7qBjLexIryC/CBBJyaJ70W1kQ7NMYfniWwui\n"
+                                "f0aRxJgOdD81rjTvkINsPp+xPRQO6oOadjzdjImYEuQTqrJTEUnntbu924eh+2D9\n"
+                                "Mf2CAanj0mglRnscS9mmljZ0KzoGMX6Z/EhnuS40WiJTlWlH6MlQU/FDnwC6U34y\n"
+                                "JKy6/jGryfsx+kGU/NRvKSru6JYJWt5v7sOrymHWD62IT59h3blOiP8GMtYKeQlX\n"
+                                "49om9Mo1VTIFASY3lrxmexbY+6FG8YO+tfIe0tTAiGrkb9Pz6tYbaj9FjEWOv4Vc\n"
+                                "+3VMBUVdGJjgqvE8fx+/+mHo4Rg69BUPfPSrpEg7sQKBgQDlL85G04VZgrNZgOx6\n"
+                                "pTlCCl/NkfNb1OYa0BELqWINoWaWQHnm6lX8YjrUjwRpBF5s7mFhguFjUjp/NW6D\n"
+                                "0EEg5BmO0ePJ3dLKSeOA7gMo7y7kAcD/YGToqAaGljkBI+IAWK5Su5yldrECTQKG\n"
+                                "YnMKyQ1MWUfCYEwHtPvFvE5aPwKBgQDFBWXekpxHIvt/B41Cl/TftAzE7/f58JjV\n"
+                                "MFo/JCh9TDcH6N5TMTRS1/iQrv5M6kJSSrHnq8pqDXOwfHLwxetpk9tr937VRzoL\n"
+                                "CuG1Ar7c1AO6ujNnAEmUVC2DppL/ck5mRPWK/kgLwZSaNcZf8sydRgphsW1ogJin\n"
+                                "7g0nGbFwXwKBgQCPoZY07Pr1TeP4g8OwWTu5F6dSvdU2CAbtZthH5q98u1n/cAj1\n"
+                                "noak1Srpa3foGMTUn9CHu+5kwHPIpUPNeAZZBpq91uxa5pnkDMp3UrLIRJ2uZyr8\n"
+                                "4PxcknEEh8DR5hsM/IbDcrCJQglM19ZtQeW3LKkY4BsIxjDf45ymH407IQKBgE/g\n"
+                                "Ul6cPfOxQRlNLH4VMVgInSyyxWx1mODFy7DRrgCuh5kTVh+QUVBM8x9lcwAn8V9/\n"
+                                "nQT55wR8E603pznqY/jX0xvAqZE6YVPcw4kpZcwNwL1RhEl8GliikBlRzUL3SsW3\n"
+                                "q30AfqEViHPE3XpE66PPo6Hb1ymJCVr77iUuC3wtAoGBAIBrOGunv1qZMfqmwAY2\n"
+                                "lxlzRgxgSiaev0lTNxDzZkmU/u3dgdTwJ5DDANqPwJc6b8SGYTp9rQ0mbgVHnhIB\n"
+                                "jcJQBQkTfq6Z0H6OoTVi7dPs3ibQJFrtkoyvYAbyk36quBmNRjVh6rc8468bhXYr\n"
+                                "v/t+MeGJP/0Zw8v/X2CFll96\n"
+                                "-----END PRIVATE KEY-----";
+
+
+extern "C" esp_err_t rpc_example_wifi_recv(void *buffer, uint16_t len, void *eb);
+
+static esp_err_t perform()
+{
+    RpcEngine rpc(tls);
+
+    auto header = rpc.get_header();
+
+    switch (header.id) {
+    case SET_MODE: {
+        auto req = rpc.get_payload<wifi_mode_t>(SET_MODE, header);
+        auto ret = esp_wifi_set_mode(req);
+        if (rpc.send(SET_MODE, &ret) != ESP_OK) {
+            return ESP_FAIL;
+        }
+        break;
+    }
+    case INIT: {
+        auto req = rpc.get_payload<wifi_init_config_t>(INIT, header);
+        req.osi_funcs = &g_wifi_osi_funcs;
+        req.wpa_crypto_funcs = g_wifi_default_wpa_crypto_funcs;
+        auto ret = esp_wifi_init(&req);
+        if (rpc.send(INIT, &ret) != ESP_OK) {
+            return ESP_FAIL;
+        }
+        break;
+    }
+    case SET_CONFIG: {
+        auto req = rpc.get_payload<esp_wifi_remote_config>(SET_CONFIG, header);
+        auto ret = esp_wifi_set_config(req.interface, &req.conf);
+        if (rpc.send(SET_CONFIG, &ret) != ESP_OK) {
+            return ESP_FAIL;
+        }
+        break;
+    }
+    case START: {
+        if (header.size != 0) {
+            return ESP_FAIL;
+        }
+
+        // setup wifi callbacks now
+        esp_wifi_internal_reg_rxcb(WIFI_IF_STA, rpc_example_wifi_recv);
+        esp_wifi_internal_reg_netstack_buf_cb(esp_netif_netstack_buf_ref, esp_netif_netstack_buf_free);
+        //
+        auto ret = esp_wifi_start();
+        if (rpc.send(START, &ret) != ESP_OK) {
+            return ESP_FAIL;
+        }
+        break;
+    }
+    case CONNECT: {
+        if (header.size != 0) {
+            return ESP_FAIL;
+        }
+
+        auto ret = esp_wifi_connect();
+        if (rpc.send(CONNECT, &ret) != ESP_OK) {
+            return ESP_FAIL;
+        }
+        break;
+    }
+    case GET_MAC: {
+        auto req = rpc.get_payload<wifi_interface_t>(GET_MAC, header);
+        esp_wifi_remote_mac_t resp = {};
+        resp.err = esp_wifi_get_mac(req, resp.mac);
+        if (rpc.send(GET_MAC, &resp) != ESP_OK) {
+            return ESP_FAIL;
+        }
+        break;
+    }
+    }
+    return ESP_OK;
+}
+
+static void server(void *ctx)
+{
+    struct sockaddr_in dest_addr = {};
+    int ret;
+    int opt = 1;
+    dest_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+    dest_addr.sin_family = AF_INET;
+    dest_addr.sin_port = htons(PORT);
+    int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
+    if (listen_sock < 0) {
+        printf("Unable to create socket: errno %d", errno);
+        return;
+    }
+    setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
+    ret = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
+    if (ret != 0) {
+        printf("Socket unable to bind: errno %d", errno);
+        return;
+    }
+
+    ret = listen(listen_sock, 1);
+    if (ret != 0) {
+        printf("Error occurred during listen: errno %d", errno);
+        return;
+    }
+    struct sockaddr_storage source_addr;
+    socklen_t addr_len = sizeof(source_addr);
+    int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len);
+    if (sock < 0) {
+        printf("Unable to accept connection: errno %d", errno);
+        return;
+    }
+    printf("Socket accepted ip address: %s\n", inet_ntoa(((struct sockaddr_in *)&source_addr)->sin_addr));
+
+
+    esp_tls_cfg_server_t cfg = {};
+    cfg.servercert_buf = servercert;
+    cfg.servercert_bytes = sizeof(servercert);
+    cfg.serverkey_buf = prvtkey;
+    cfg.serverkey_bytes = sizeof(prvtkey);
+
+
+
+    tls = esp_tls_init();
+    if (!tls) {
+        goto exit;
+    }
+    ESP_LOGI(TAG, "performing session handshake");
+    ret = esp_tls_server_session_create(&cfg, sock, tls);
+    if (ret != 0) {
+        ESP_LOGE(TAG, "esp_tls_create_server_session failed");
+        goto exit;
+    }
+    ESP_LOGI(TAG, "Secure socket open");
+    while (perform() == ESP_OK) {}
+
+    esp_tls_server_session_delete(tls);
+exit:
+    vTaskDelete(nullptr);
+
+}
+
+extern "C" esp_err_t server_init(void)
+{
+    xTaskCreate(&server, "server", 8192, NULL, 5, NULL);
+    return ESP_OK;
+}
diff --git a/components/eppp_link/examples/rpc/server/main/station_example_main.c b/components/eppp_link/examples/rpc/server/main/station_example_main.c
new file mode 100644
index 0000000000..dbcd1bacfa
--- /dev/null
+++ b/components/eppp_link/examples/rpc/server/main/station_example_main.c
@@ -0,0 +1,64 @@
+/*
+ * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+
+#include <string.h>
+#include <esp_private/wifi.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/event_groups.h"
+#include "esp_system.h"
+#include "esp_wifi.h"
+#include "esp_event.h"
+#include "esp_log.h"
+#include "nvs_flash.h"
+#include "eppp_link.h"
+#include "esp_wifi_remote.h"
+
+static const char *TAG = "sta2pppos";
+
+esp_err_t server_init(void);
+
+static eppp_channel_fn_t s_tx;
+static esp_netif_t *s_ppp_netif;
+
+static esp_err_t netif_recv(void *h, void *buffer, size_t len)
+{
+    return esp_wifi_internal_tx(WIFI_IF_STA, buffer, len);
+}
+
+esp_err_t rpc_example_wifi_recv(void *buffer, uint16_t len, void *eb)
+{
+    if (s_tx) {
+        esp_err_t ret = s_tx(s_ppp_netif, buffer, len);
+        esp_wifi_internal_free_rx_buffer(eb);
+        return ret;
+    }
+    return ESP_OK;
+}
+
+void app_main(void)
+{
+    //Initialize NVS
+    esp_err_t ret = nvs_flash_init();
+    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
+        ESP_ERROR_CHECK(nvs_flash_erase());
+        ret = nvs_flash_init();
+    }
+    ESP_ERROR_CHECK(ret);
+
+    ESP_ERROR_CHECK(esp_netif_init());
+    ESP_ERROR_CHECK(esp_event_loop_create_default());
+    eppp_config_t config = EPPP_DEFAULT_SERVER_CONFIG();
+    config.transport = EPPP_TRANSPORT_SPI;
+    s_ppp_netif = eppp_listen(&config);
+    if (s_ppp_netif == NULL) {
+        ESP_LOGE(TAG, "Failed to setup connection");
+        return ;
+    }
+
+    eppp_add_channel(1, &s_tx, netif_recv);
+
+    server_init();
+}
diff --git a/components/eppp_link/examples/rpc/server/sdkconfig.defaults b/components/eppp_link/examples/rpc/server/sdkconfig.defaults
new file mode 100644
index 0000000000..2eb96884e3
--- /dev/null
+++ b/components/eppp_link/examples/rpc/server/sdkconfig.defaults
@@ -0,0 +1,5 @@
+CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=4096
+CONFIG_LWIP_PPP_SUPPORT=y
+CONFIG_LWIP_PPP_SERVER_SUPPORT=y
+CONFIG_LWIP_PPP_VJ_HEADER_COMPRESSION=n
+CONFIG_EPPP_LINK_DEVICE_SPI=y
diff --git a/components/eppp_link/examples/slave/CMakeLists.txt b/components/eppp_link/examples/slave/CMakeLists.txt
new file mode 100644
index 0000000000..144b9e1a21
--- /dev/null
+++ b/components/eppp_link/examples/slave/CMakeLists.txt
@@ -0,0 +1,6 @@
+# The following five lines of boilerplate have to be in your project's
+# CMakeLists in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.16)
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+project(pppos_slave)
diff --git a/components/eppp_link/examples/slave/README.md b/components/eppp_link/examples/slave/README.md
new file mode 100644
index 0000000000..a8ff85f9f4
--- /dev/null
+++ b/components/eppp_link/examples/slave/README.md
@@ -0,0 +1,7 @@
+
+# Wi-Fi station to PPPoS server
+
+This example demonstrate using NAPT to bring connectivity from WiFi station to PPPoS server.
+
+This example expect a PPPoS client to connect to the server and use the connectivity.
+The client could be a Linux computer with `pppd` service or another microcontroller with PPP client (or another ESP32 with not WiFi interface)
diff --git a/components/eppp_link/examples/slave/main/CMakeLists.txt b/components/eppp_link/examples/slave/main/CMakeLists.txt
new file mode 100644
index 0000000000..2ba044442a
--- /dev/null
+++ b/components/eppp_link/examples/slave/main/CMakeLists.txt
@@ -0,0 +1,2 @@
+idf_component_register(SRCS "station_example_main.c"
+                    INCLUDE_DIRS ".")
diff --git a/components/eppp_link/examples/slave/main/Kconfig.projbuild b/components/eppp_link/examples/slave/main/Kconfig.projbuild
new file mode 100644
index 0000000000..ae4d7fb9a8
--- /dev/null
+++ b/components/eppp_link/examples/slave/main/Kconfig.projbuild
@@ -0,0 +1,21 @@
+menu "Example Configuration"
+
+    config ESP_WIFI_SSID
+        string "WiFi SSID"
+        default "myssid"
+        help
+            SSID (network name) for the example to connect to.
+
+    config ESP_WIFI_PASSWORD
+        string "WiFi Password"
+        default "mypassword"
+        help
+            WiFi password (WPA or WPA2) for the example to use.
+
+    config ESP_MAXIMUM_RETRY
+        int "Maximum retry"
+        default 5
+        help
+            Set the Maximum retry to avoid station reconnecting to the AP unlimited when the AP is really inexistent.
+
+endmenu
diff --git a/components/eppp_link/examples/slave/main/idf_component.yml b/components/eppp_link/examples/slave/main/idf_component.yml
new file mode 100644
index 0000000000..7ecb517e8a
--- /dev/null
+++ b/components/eppp_link/examples/slave/main/idf_component.yml
@@ -0,0 +1,4 @@
+dependencies:
+  espressif/eppp_link:
+    version: "*"
+    override_path: "../../.."
diff --git a/components/eppp_link/examples/slave/main/station_example_main.c b/components/eppp_link/examples/slave/main/station_example_main.c
new file mode 100644
index 0000000000..3b1445472f
--- /dev/null
+++ b/components/eppp_link/examples/slave/main/station_example_main.c
@@ -0,0 +1,137 @@
+/*
+ * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+
+#include <string.h>
+#include "freertos/FreeRTOS.h"
+#include "freertos/event_groups.h"
+#include "esp_system.h"
+#include "esp_wifi.h"
+#include "esp_event.h"
+#include "esp_log.h"
+#include "nvs_flash.h"
+#include "eppp_link.h"
+
+/* FreeRTOS event group to signal when we are connected*/
+static EventGroupHandle_t s_wifi_event_group;
+
+/* The event group allows multiple bits for each event, but we only care about two events:
+ * - we are connected to the AP with an IP
+ * - we failed to connect after the maximum amount of retries */
+#define WIFI_CONNECTED_BIT BIT0
+#define WIFI_FAIL_BIT      BIT1
+
+static const char *TAG = "sta2pppos";
+
+static int s_retry_num = 0;
+
+static void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
+{
+    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
+        esp_wifi_connect();
+    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
+        if (s_retry_num < CONFIG_ESP_MAXIMUM_RETRY) {
+            esp_wifi_connect();
+            s_retry_num++;
+            ESP_LOGI(TAG, "retry to connect to the AP");
+        } else {
+            xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
+        }
+        ESP_LOGI(TAG, "connect to the AP fail");
+    } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
+        ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
+        ESP_LOGI(TAG, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
+        s_retry_num = 0;
+        xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
+    }
+}
+
+void wifi_init_sta(void)
+{
+    s_wifi_event_group = xEventGroupCreate();
+
+    ESP_ERROR_CHECK(esp_netif_init());
+
+    ESP_ERROR_CHECK(esp_event_loop_create_default());
+    esp_netif_create_default_wifi_sta();
+
+    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
+    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
+
+    esp_event_handler_instance_t instance_any_id;
+    esp_event_handler_instance_t instance_got_ip;
+    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT,
+                    ESP_EVENT_ANY_ID,
+                    &event_handler,
+                    NULL,
+                    &instance_any_id));
+    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT,
+                    IP_EVENT_STA_GOT_IP,
+                    &event_handler,
+                    NULL,
+                    &instance_got_ip));
+
+    wifi_config_t wifi_config = {
+        .sta = {
+            .ssid = CONFIG_ESP_WIFI_SSID,
+            .password = CONFIG_ESP_WIFI_PASSWORD,
+        },
+    };
+    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA) );
+    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config) );
+    ESP_ERROR_CHECK(esp_wifi_start() );
+
+    ESP_LOGI(TAG, "wifi_init_sta finished.");
+
+    /* Waiting until either the connection is established (WIFI_CONNECTED_BIT) or connection failed for the maximum
+     * number of re-tries (WIFI_FAIL_BIT). The bits are set by event_handler() (see above) */
+    EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
+                                           WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
+                                           pdFALSE,
+                                           pdFALSE,
+                                           portMAX_DELAY);
+
+    /* xEventGroupWaitBits() returns the bits before the call returned, hence we can test which event actually
+     * happened. */
+    if (bits & WIFI_CONNECTED_BIT) {
+        ESP_LOGI(TAG, "connected to ap SSID:%s password:%s",
+                 CONFIG_ESP_WIFI_SSID, CONFIG_ESP_WIFI_PASSWORD);
+    } else if (bits & WIFI_FAIL_BIT) {
+        ESP_LOGI(TAG, "Failed to connect to SSID:%s, password:%s",
+                 CONFIG_ESP_WIFI_SSID, CONFIG_ESP_WIFI_PASSWORD);
+    } else {
+        ESP_LOGE(TAG, "UNEXPECTED EVENT");
+    }
+}
+
+void app_main(void)
+{
+    //Initialize NVS
+    esp_err_t ret = nvs_flash_init();
+    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
+        ESP_ERROR_CHECK(nvs_flash_erase());
+        ret = nvs_flash_init();
+    }
+    ESP_ERROR_CHECK(ret);
+
+    ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
+    wifi_init_sta();
+
+    eppp_config_t config = EPPP_DEFAULT_SERVER_CONFIG();
+#if CONFIG_EPPP_LINK_DEVICE_SPI
+    config.transport = EPPP_TRANSPORT_SPI;
+#else
+    config.transport = EPPP_TRANSPORT_UART;
+    config.uart.tx_io = 11;
+    config.uart.rx_io = 10;
+    config.uart.baud = 2000000;
+#endif
+    esp_netif_t *eppp_netif = eppp_listen(&config);
+    if (eppp_netif == NULL) {
+        ESP_LOGE(TAG, "Failed to setup connection");
+        return ;
+    }
+    ESP_ERROR_CHECK(esp_netif_napt_enable(eppp_netif));
+}
diff --git a/components/eppp_link/examples/slave/sdkconfig.defaults b/components/eppp_link/examples/slave/sdkconfig.defaults
new file mode 100644
index 0000000000..ec1a1a43b0
--- /dev/null
+++ b/components/eppp_link/examples/slave/sdkconfig.defaults
@@ -0,0 +1,8 @@
+CONFIG_UART_ISR_IN_IRAM=y
+CONFIG_LWIP_IP_FORWARD=y
+CONFIG_LWIP_IPV4_NAPT=y
+CONFIG_LWIP_TCPIP_TASK_STACK_SIZE=4096
+CONFIG_LWIP_PPP_SUPPORT=y
+CONFIG_LWIP_PPP_SERVER_SUPPORT=y
+CONFIG_LWIP_PPP_VJ_HEADER_COMPRESSION=n
+CONFIG_EPPP_LINK_DEVICE_SPI=y
diff --git a/components/eppp_link/idf_component.yml b/components/eppp_link/idf_component.yml
new file mode 100644
index 0000000000..fbab49cb81
--- /dev/null
+++ b/components/eppp_link/idf_component.yml
@@ -0,0 +1,6 @@
+version: 0.0.9
+url: https://github.com/espressif/esp-protocols/tree/master/components/eppp_link
+description: The component provides a general purpose PPP connectivity, typically used as WiFi-PPP router
+dependencies:
+  idf:
+    version: '>=5.2'
diff --git a/components/eppp_link/include/eppp_link.h b/components/eppp_link/include/eppp_link.h
new file mode 100644
index 0000000000..ad448ea7d7
--- /dev/null
+++ b/components/eppp_link/include/eppp_link.h
@@ -0,0 +1,109 @@
+/*
+ * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#define EPPP_DEFAULT_SERVER_IP() ESP_IP4TOADDR(192, 168, 11, 1)
+#define EPPP_DEFAULT_CLIENT_IP() ESP_IP4TOADDR(192, 168, 11, 2)
+
+#define EPPP_DEFAULT_CONFIG(our_ip, their_ip) { \
+    .transport = EPPP_TRANSPORT_UART,           \
+    .spi = {                                    \
+            .host = 1,                          \
+            .mosi = 11,                         \
+            .miso = 13,                         \
+            .sclk = 12,                         \
+            .cs = 10,                           \
+            .intr = 2,                          \
+            .freq = 20*1000*1000,               \
+    },                                          \
+    .uart = {   \
+            .port = 1,          \
+            .baud = 921600,     \
+            .tx_io = 25,        \
+            .rx_io = 26,        \
+            .queue_size = 16,   \
+            .rx_buffer_size = 1024, \
+    },  \
+    . task = {                  \
+            .run_task = true,   \
+            .stack_size = 4096, \
+            .priority = 18,     \
+    },  \
+    . ppp = {   \
+            .our_ip4_addr = our_ip,     \
+            .their_ip4_addr = their_ip, \
+    }   \
+}
+
+#define EPPP_DEFAULT_SERVER_CONFIG() EPPP_DEFAULT_CONFIG(EPPP_DEFAULT_SERVER_IP(), EPPP_DEFAULT_CLIENT_IP())
+#define EPPP_DEFAULT_CLIENT_CONFIG() EPPP_DEFAULT_CONFIG(EPPP_DEFAULT_CLIENT_IP(), EPPP_DEFAULT_SERVER_IP())
+
+typedef enum eppp_type {
+    EPPP_SERVER,
+    EPPP_CLIENT,
+} eppp_type_t;
+
+typedef enum eppp_transport {
+    EPPP_TRANSPORT_UART,
+    EPPP_TRANSPORT_SPI,
+} eppp_transport_t;
+
+
+typedef struct eppp_config_t {
+    eppp_transport_t transport;
+
+    struct eppp_config_spi_s {
+        int host;
+        int mosi;
+        int miso;
+        int sclk;
+        int cs;
+        int intr;
+        int freq;
+    } spi;
+
+    struct eppp_config_uart_s {
+        int port;
+        int baud;
+        int tx_io;
+        int rx_io;
+        int queue_size;
+        int rx_buffer_size;
+    } uart;
+
+    struct eppp_config_task_s {
+        bool run_task;
+        int stack_size;
+        int priority;
+    } task;
+
+    struct eppp_config_pppos_s {
+        uint32_t our_ip4_addr;
+        uint32_t their_ip4_addr;
+    } ppp;
+
+} eppp_config_t;
+
+esp_netif_t *eppp_connect(eppp_config_t *config);
+
+esp_netif_t *eppp_listen(eppp_config_t *config);
+
+void eppp_close(esp_netif_t *netif);
+
+esp_netif_t *eppp_init(enum eppp_type role, eppp_config_t *config);
+
+void eppp_deinit(esp_netif_t *netif);
+
+esp_netif_t *eppp_open(enum eppp_type role, eppp_config_t *config, TickType_t connect_timeout);
+
+esp_err_t eppp_netif_stop(esp_netif_t *netif, TickType_t stop_timeout);
+
+esp_err_t eppp_netif_start(esp_netif_t *netif);
+
+esp_err_t eppp_perform(esp_netif_t *netif);
+
+typedef esp_err_t (*eppp_channel_fn_t)(void *h, void *buffer, size_t len);
+
+esp_err_t eppp_add_channel(int nr, eppp_channel_fn_t *tx, const eppp_channel_fn_t rx);
diff --git a/components/eppp_link/test/test_app/CMakeLists.txt b/components/eppp_link/test/test_app/CMakeLists.txt
new file mode 100644
index 0000000000..cc4589c454
--- /dev/null
+++ b/components/eppp_link/test/test_app/CMakeLists.txt
@@ -0,0 +1,7 @@
+# The following four lines of boilerplate have to be in your project's CMakeLists
+# in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.16)
+set(EXTRA_COMPONENT_DIRS $ENV{IDF_PATH}/tools/unit-test-app/components)
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+project(test_app)
diff --git a/components/eppp_link/test/test_app/README.md b/components/eppp_link/test/test_app/README.md
new file mode 100644
index 0000000000..cb8add3aa4
--- /dev/null
+++ b/components/eppp_link/test/test_app/README.md
@@ -0,0 +1,73 @@
+
+# Test application running both server and client on the same device
+
+Need to connect client's Tx to server's Rx and vice versa:
+GPIO25 - GPIO4
+GPIO26 - GPIO5
+
+We wait for the connection and then we start pinging the client's address on server's netif.
+
+## Example of output:
+
+```
+I (393) eppp_test_app: [APP] Startup..
+I (393) eppp_test_app: [APP] Free memory: 296332 bytes
+I (393) eppp_test_app: [APP] IDF version: v5.3-dev-1154-gf14d9e7431-dirty
+I (423) uart: ESP_INTR_FLAG_IRAM flag not set while CONFIG_UART_ISR_IN_IRAM is enabled, flag updated
+I (423) uart: queue free spaces: 16
+I (433) eppp_link: Waiting for IP address
+I (433) uart: ESP_INTR_FLAG_IRAM flag not set while CONFIG_UART_ISR_IN_IRAM is enabled, flag updated
+I (443) uart: queue free spaces: 16
+I (443) eppp_link: Waiting for IP address
+I (6473) esp-netif_lwip-ppp: Connected
+I (6513) eppp_link: Got IPv4 event: Interface "pppos_client" address: 192.168.11.2
+I (6523) esp-netif_lwip-ppp: Connected
+I (6513) eppp_link: Connected!
+I (6523) eppp_link: Got IPv4 event: Interface "pppos_server" address: 192.168.11.1
+I (6553) main_task: Returned from app_main()
+64bytes from 192.168.11.2 icmp_seq=1 ttl=255 time=18 ms
+64bytes from 192.168.11.2 icmp_seq=2 ttl=255 time=19 ms
+64bytes from 192.168.11.2 icmp_seq=3 ttl=255 time=19 ms
+64bytes from 192.168.11.2 icmp_seq=4 ttl=255 time=20 ms
+64bytes from 192.168.11.2 icmp_seq=5 ttl=255 time=19 ms
+64bytes from 192.168.11.2 icmp_seq=6 ttl=255 time=19 ms
+64bytes from 192.168.11.2 icmp_seq=7 ttl=255 time=19 ms
+From 192.168.11.2 icmp_seq=8 timeout        // <-- Disconnected Tx-Rx wires
+From 192.168.11.2 icmp_seq=9 timeout
+```
+## Test cases
+
+This test app exercises these methods of setting up server-client connection:
+* simple blocking API (eppp_listen() <--> eppp_connect()): Uses network events internally and waits for connection
+* simplified non-blocking API (eppp_open(EPPP_SERVER, ...) <--> eppp_open(EPPP_SERVER, ...) ): Uses events internally, optionally waits for connecting
+* manual API (eppp_init(), eppp_netif_start(), eppp_perform()): User to manually drive Rx task
+    - Note that the ping test for this test case takes longer, since we call perform for both server and client from one task, for example:
+
+```
+TEST(eppp_test, open_close_taskless)I (28562) uart: ESP_INTR_FLAG_IRAM flag not set while CONFIG_UART_ISR_IN_IRAM is enabled, flag updated
+I (28572) uart: ESP_INTR_FLAG_IRAM flag not set while CONFIG_UART_ISR_IN_IRAM is enabled, flag updated
+Note: esp_netif_init() has been called. Until next reset, TCP/IP task will periodicially allocate memory and consume CPU time.
+I (28602) uart: ESP_INTR_FLAG_IRAM flag not set while CONFIG_UART_ISR_IN_IRAM is enabled, flag updated
+I (28612) uart: queue free spaces: 16
+I (28612) uart: ESP_INTR_FLAG_IRAM flag not set while CONFIG_UART_ISR_IN_IRAM is enabled, flag updated
+I (28622) uart: queue free spaces: 16
+I (28642) esp-netif_lwip-ppp: Connected
+I (28642) esp-netif_lwip-ppp: Connected
+I (28642) test: Got IPv4 event: Interface "pppos_server(EPPP0)" address: 192.168.11.1
+I (28642) esp-netif_lwip-ppp: Connected
+I (28652) test: Got IPv4 event: Interface "pppos_client(EPPP1)" address: 192.168.11.2
+I (28662) esp-netif_lwip-ppp: Connected
+64bytes from 192.168.11.2 icmp_seq=1 ttl=255 time=93 ms
+64bytes from 192.168.11.2 icmp_seq=2 ttl=255 time=98 ms
+64bytes from 192.168.11.2 icmp_seq=3 ttl=255 time=99 ms
+64bytes from 192.168.11.2 icmp_seq=4 ttl=255 time=99 ms
+64bytes from 192.168.11.2 icmp_seq=5 ttl=255 time=99 ms
+5 packets transmitted, 5 received, time 488ms
+I (29162) esp-netif_lwip-ppp: User interrupt
+I (29162) test: Disconnected interface "pppos_client(EPPP1)"
+I (29172) esp-netif_lwip-ppp: User interrupt
+I (29172) test: Disconnected interface "pppos_server(EPPP0)"
+MALLOC_CAP_8BIT usage: Free memory delta: 0 Leak threshold: -64
+MALLOC_CAP_32BIT usage: Free memory delta: 0 Leak threshold: -64
+ PASS
+```
diff --git a/components/eppp_link/test/test_app/main/CMakeLists.txt b/components/eppp_link/test/test_app/main/CMakeLists.txt
new file mode 100644
index 0000000000..c75a706cc2
--- /dev/null
+++ b/components/eppp_link/test/test_app/main/CMakeLists.txt
@@ -0,0 +1,4 @@
+idf_component_register(SRCS app_main.c
+                    INCLUDE_DIRS "."
+                    REQUIRES test_utils
+                    PRIV_REQUIRES unity nvs_flash esp_netif driver esp_event)
diff --git a/components/eppp_link/test/test_app/main/app_main.c b/components/eppp_link/test/test_app/main/app_main.c
new file mode 100644
index 0000000000..e4edfa487b
--- /dev/null
+++ b/components/eppp_link/test/test_app/main/app_main.c
@@ -0,0 +1,344 @@
+/*
+ * SPDX-FileCopyrightText: 2023-2024 Espressif Systems (Shanghai) CO LTD
+ *
+ * SPDX-License-Identifier: Unlicense OR CC0-1.0
+ */
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stddef.h>
+#include "esp_system.h"
+#include "esp_event.h"
+#include "esp_netif.h"
+#include "esp_netif_ppp.h"
+#include "eppp_link.h"
+#include "lwip/sockets.h"
+#include "esp_log.h"
+#include "ping/ping_sock.h"
+#include "driver/uart.h"
+#include "test_utils.h"
+#include "unity.h"
+#include "test_utils.h"
+#include "unity_fixture.h"
+#include "memory_checks.h"
+#include "lwip/sys.h"
+
+#define CLIENT_INFO_CONNECTED   BIT0
+#define CLIENT_INFO_DISCONNECT  BIT1
+#define CLIENT_INFO_CLOSED      BIT2
+#define PING_SUCCEEDED          BIT3
+#define PING_FAILED             BIT4
+#define STOP_WORKER_TASK        BIT5
+#define WORKER_TASK_STOPPED     BIT6
+
+TEST_GROUP(eppp_test);
+TEST_SETUP(eppp_test)
+{
+    // Perform some open/close operations to disregard lazy init one-time allocations
+    // LWIP: core protection mutex
+    sys_arch_protect();
+    sys_arch_unprotect(0);
+    // UART: install and delete both drivers to disregard potential leak in allocated interrupt slot
+    TEST_ESP_OK(uart_driver_install(UART_NUM_1, 256, 0, 0, NULL, 0));
+    TEST_ESP_OK(uart_driver_delete(UART_NUM_1));
+    TEST_ESP_OK(uart_driver_install(UART_NUM_2, 256, 0, 0, NULL, 0));
+    TEST_ESP_OK(uart_driver_delete(UART_NUM_2));
+    // PING: used for timestamps
+    struct timeval time;
+    gettimeofday(&time, NULL);
+
+    test_utils_record_free_mem();
+    TEST_ESP_OK(test_utils_set_leak_level(0, ESP_LEAK_TYPE_CRITICAL, ESP_COMP_LEAK_GENERAL));
+}
+
+TEST_TEAR_DOWN(eppp_test)
+{
+    test_utils_finish_and_evaluate_leaks(32, 64);
+}
+
+static void test_on_ping_end(esp_ping_handle_t hdl, void *args)
+{
+    EventGroupHandle_t event = args;
+    uint32_t transmitted;
+    uint32_t received;
+    uint32_t total_time_ms;
+    esp_ping_get_profile(hdl, ESP_PING_PROF_REQUEST, &transmitted, sizeof(transmitted));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_REPLY, &received, sizeof(received));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_DURATION, &total_time_ms, sizeof(total_time_ms));
+    printf("%" PRId32 " packets transmitted, %" PRId32 " received, time %" PRId32 "ms\n", transmitted, received, total_time_ms);
+    if (transmitted == received) {
+        xEventGroupSetBits(event, PING_SUCCEEDED);
+    } else {
+        xEventGroupSetBits(event, PING_FAILED);
+    }
+}
+
+static void test_on_ping_success(esp_ping_handle_t hdl, void *args)
+{
+    uint8_t ttl;
+    uint16_t seqno;
+    uint32_t elapsed_time, recv_len;
+    ip_addr_t target_addr;
+    esp_ping_get_profile(hdl, ESP_PING_PROF_SEQNO, &seqno, sizeof(seqno));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_TTL, &ttl, sizeof(ttl));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_IPADDR, &target_addr, sizeof(target_addr));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_SIZE, &recv_len, sizeof(recv_len));
+    esp_ping_get_profile(hdl, ESP_PING_PROF_TIMEGAP, &elapsed_time, sizeof(elapsed_time));
+    printf("%" PRId32 "bytes from %s icmp_seq=%d ttl=%d time=%" PRId32 " ms\n",
+           recv_len, inet_ntoa(target_addr.u_addr.ip4), seqno, ttl, elapsed_time);
+}
+
+struct client_info {
+    esp_netif_t *netif;
+    EventGroupHandle_t event;
+};
+
+static void open_client_task(void *ctx)
+{
+    struct client_info *info = ctx;
+    eppp_config_t config = EPPP_DEFAULT_CLIENT_CONFIG();
+    config.uart.port = UART_NUM_2;
+    config.uart.tx_io = 4;
+    config.uart.rx_io = 5;
+
+    info->netif = eppp_connect(&config);
+    xEventGroupSetBits(info->event, CLIENT_INFO_CONNECTED);
+
+    // wait for disconnection trigger
+    EventBits_t bits = xEventGroupWaitBits(info->event, CLIENT_INFO_DISCONNECT, pdFALSE, pdFALSE, pdMS_TO_TICKS(50000));
+    TEST_ASSERT_EQUAL(bits & CLIENT_INFO_DISCONNECT, CLIENT_INFO_DISCONNECT);
+    eppp_close(info->netif);
+    xEventGroupSetBits(info->event, CLIENT_INFO_CLOSED);
+    vTaskDelete(NULL);
+}
+
+TEST(eppp_test, init_deinit)
+{
+    // Init and deinit server size
+    eppp_config_t config = EPPP_DEFAULT_CONFIG(0, 0);
+    esp_netif_t *netif = eppp_init(EPPP_SERVER, &config);
+    TEST_ASSERT_NOT_NULL(netif);
+    eppp_deinit(netif);
+    netif = NULL;
+    // Init and deinit client size
+    netif = eppp_init(EPPP_CLIENT, &config);
+    TEST_ASSERT_NOT_NULL(netif);
+    eppp_deinit(netif);
+}
+
+static EventBits_t ping_test(uint32_t addr, esp_netif_t *netif, EventGroupHandle_t event)
+{
+    ip_addr_t target_addr = { .type = IPADDR_TYPE_V4, .u_addr.ip4.addr = addr };
+    esp_ping_config_t ping_config = ESP_PING_DEFAULT_CONFIG();
+    ping_config.interval_ms = 100;
+    ping_config.target_addr = target_addr;
+    ping_config.interface = esp_netif_get_netif_impl_index(netif);
+    esp_ping_callbacks_t cbs = { .cb_args = event, .on_ping_end = test_on_ping_end, .on_ping_success = test_on_ping_success };
+    esp_ping_handle_t ping;
+    esp_ping_new_session(&ping_config, &cbs, &ping);
+    esp_ping_start(ping);
+    // Wait for the client thread closure and delete locally created objects
+    EventBits_t bits = xEventGroupWaitBits(event, PING_SUCCEEDED | PING_FAILED, pdFALSE, pdFALSE, pdMS_TO_TICKS(50000));
+    TEST_ASSERT_EQUAL(bits & (PING_SUCCEEDED | PING_FAILED), PING_SUCCEEDED);
+    esp_ping_stop(ping);
+    esp_ping_delete_session(ping);
+    return bits;
+}
+
+TEST(eppp_test, open_close)
+{
+    test_case_uses_tcpip();
+
+    eppp_config_t config = EPPP_DEFAULT_SERVER_CONFIG();
+    struct client_info client = { .netif = NULL, .event =  xEventGroupCreate()};
+
+    TEST_ESP_OK(esp_event_loop_create_default());
+
+    TEST_ASSERT_NOT_NULL(client.event);
+
+    // Need to connect the client in a separate thread, as the simplified API blocks until connection
+    xTaskCreate(open_client_task, "client_task", 4096, &client, 5, NULL);
+
+    // Now start the server
+    esp_netif_t *eppp_server = eppp_listen(&config);
+
+    // Wait for the client to connect
+    EventBits_t bits = xEventGroupWaitBits(client.event, CLIENT_INFO_CONNECTED, pdFALSE, pdFALSE, pdMS_TO_TICKS(50000));
+    TEST_ASSERT_EQUAL(bits & CLIENT_INFO_CONNECTED, CLIENT_INFO_CONNECTED);
+
+    // Check that both server and client are valid netif pointers
+    TEST_ASSERT_NOT_NULL(eppp_server);
+    TEST_ASSERT_NOT_NULL(client.netif);
+
+    // Now that we're connected, let's try to ping clients address
+    bits = ping_test(config.ppp.their_ip4_addr, eppp_server, client.event);
+    TEST_ASSERT_EQUAL(bits & (PING_SUCCEEDED | PING_FAILED), PING_SUCCEEDED);
+
+    // Trigger client disconnection and close the server
+    xEventGroupSetBits(client.event, CLIENT_INFO_DISCONNECT);
+    eppp_close(eppp_server);
+
+    // Wait for the client thread closure and delete locally created objects
+    bits = xEventGroupWaitBits(client.event, CLIENT_INFO_CLOSED, pdFALSE, pdFALSE, pdMS_TO_TICKS(50000));
+    TEST_ASSERT_EQUAL(bits & CLIENT_INFO_CLOSED, CLIENT_INFO_CLOSED);
+
+    TEST_ESP_OK(esp_event_loop_delete_default());
+    vEventGroupDelete(client.event);
+
+    // wait for the lwip sockets to close cleanly
+    vTaskDelay(pdMS_TO_TICKS(1000));
+}
+
+static void on_event(void *arg, esp_event_base_t base, int32_t event_id, void *data)
+{
+    EventGroupHandle_t event = arg;
+    if (base == IP_EVENT && event_id == IP_EVENT_PPP_GOT_IP) {
+        ip_event_got_ip_t *e = (ip_event_got_ip_t *)data;
+        esp_netif_t *netif = e->esp_netif;
+        ESP_LOGI("test", "Got IPv4 event: Interface \"%s(%s)\" address: " IPSTR, esp_netif_get_desc(netif),
+                 esp_netif_get_ifkey(netif), IP2STR(&e->ip_info.ip));
+        if (strcmp("pppos_server", esp_netif_get_desc(netif)) == 0) {
+            xEventGroupSetBits(event, 1 << EPPP_SERVER);
+        } else if (strcmp("pppos_client", esp_netif_get_desc(netif)) == 0) {
+            xEventGroupSetBits(event, 1 << EPPP_CLIENT);
+        }
+    } else if (base == NETIF_PPP_STATUS && event_id == NETIF_PPP_ERRORUSER) {
+        esp_netif_t **netif = data;
+        ESP_LOGI("test", "Disconnected interface \"%s(%s)\"", esp_netif_get_desc(*netif), esp_netif_get_ifkey(*netif));
+        if (strcmp("pppos_server", esp_netif_get_desc(*netif)) == 0) {
+            xEventGroupSetBits(event, 1 << EPPP_SERVER);
+        } else if (strcmp("pppos_client", esp_netif_get_desc(*netif)) == 0) {
+            xEventGroupSetBits(event, 1 << EPPP_CLIENT);
+        }
+    }
+}
+
+TEST(eppp_test, open_close_nonblocking)
+{
+    test_case_uses_tcpip();
+    EventGroupHandle_t event = xEventGroupCreate();
+
+    eppp_config_t server_config = EPPP_DEFAULT_SERVER_CONFIG();
+    TEST_ESP_OK(esp_event_loop_create_default());
+
+    // Open the server size
+    TEST_ESP_OK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, on_event, event));
+    esp_netif_t *eppp_server = eppp_open(EPPP_SERVER, &server_config, 0);
+    TEST_ASSERT_NOT_NULL(eppp_server);
+    // Open the client size
+    eppp_config_t client_config = EPPP_DEFAULT_SERVER_CONFIG();
+    client_config.uart.port = UART_NUM_2;
+    client_config.uart.tx_io = 4;
+    client_config.uart.rx_io = 5;
+    esp_netif_t *eppp_client = eppp_open(EPPP_CLIENT, &client_config, 0);
+    TEST_ASSERT_NOT_NULL(eppp_client);
+    const EventBits_t wait_bits = (1 << EPPP_SERVER) | (1 << EPPP_CLIENT);
+    EventBits_t bits = xEventGroupWaitBits(event, wait_bits, pdTRUE, pdTRUE, pdMS_TO_TICKS(50000));
+    TEST_ASSERT_EQUAL(bits & wait_bits, wait_bits);
+
+    // Now that we're connected, let's try to ping clients address
+    bits = ping_test(server_config.ppp.their_ip4_addr, eppp_server, event);
+    TEST_ASSERT_EQUAL(bits & (PING_SUCCEEDED | PING_FAILED), PING_SUCCEEDED);
+
+    // stop network for both client and server
+    eppp_netif_stop(eppp_client, 0); // ignore result, since we're not waiting for clean close
+    eppp_close(eppp_server);
+    eppp_close(eppp_client);         // finish client close
+    TEST_ESP_OK(esp_event_loop_delete_default());
+    vEventGroupDelete(event);
+
+    // wait for the lwip sockets to close cleanly
+    vTaskDelay(pdMS_TO_TICKS(1000));
+}
+
+
+struct worker {
+    esp_netif_t *eppp_server;
+    esp_netif_t *eppp_client;
+    EventGroupHandle_t event;
+};
+
+static void worker_task(void *ctx)
+{
+    struct worker *info = ctx;
+    while (1) {
+        eppp_perform(info->eppp_server);
+        eppp_perform(info->eppp_client);
+        if (xEventGroupGetBits(info->event) & STOP_WORKER_TASK) {
+            break;
+        }
+    }
+    xEventGroupSetBits(info->event, WORKER_TASK_STOPPED);
+    vTaskDelete(NULL);
+}
+
+TEST(eppp_test, open_close_taskless)
+{
+    test_case_uses_tcpip();
+    struct worker info = { .event = xEventGroupCreate() };
+
+    TEST_ESP_OK(esp_event_loop_create_default());
+    TEST_ESP_OK(esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, on_event, info.event));
+    TEST_ESP_OK(esp_event_handler_register(NETIF_PPP_STATUS, ESP_EVENT_ANY_ID, on_event, info.event));
+
+    // Create server
+    eppp_config_t server_config = EPPP_DEFAULT_SERVER_CONFIG();
+    info.eppp_server = eppp_init(EPPP_SERVER, &server_config);
+    TEST_ASSERT_NOT_NULL(info.eppp_server);
+    // Create client
+    eppp_config_t client_config = EPPP_DEFAULT_CLIENT_CONFIG();
+    client_config.uart.port = UART_NUM_2;
+    client_config.uart.tx_io = 4;
+    client_config.uart.rx_io = 5;
+    info.eppp_client = eppp_init(EPPP_CLIENT, &client_config);
+    TEST_ASSERT_NOT_NULL(info.eppp_client);
+    // Start workers
+    xTaskCreate(worker_task, "worker", 4096, &info, 5, NULL);
+    // Start network
+    TEST_ESP_OK(eppp_netif_start(info.eppp_server));
+    TEST_ESP_OK(eppp_netif_start(info.eppp_client));
+
+    const EventBits_t wait_bits = (1 << EPPP_SERVER) | (1 << EPPP_CLIENT);
+    EventBits_t bits = xEventGroupWaitBits(info.event, wait_bits, pdTRUE, pdTRUE, pdMS_TO_TICKS(50000));
+    TEST_ASSERT_EQUAL(bits & wait_bits, wait_bits);
+    xEventGroupClearBits(info.event, wait_bits);
+
+    // Now that we're connected, let's try to ping clients address
+    bits = ping_test(server_config.ppp.their_ip4_addr, info.eppp_server, info.event);
+    TEST_ASSERT_EQUAL(bits & (PING_SUCCEEDED | PING_FAILED), PING_SUCCEEDED);
+
+    // stop network for both client and server, we won't wait for completion so expecting ESP_FAIL
+    TEST_ASSERT_EQUAL(eppp_netif_stop(info.eppp_client, 0), ESP_FAIL);
+    TEST_ASSERT_EQUAL(eppp_netif_stop(info.eppp_server, 0), ESP_FAIL);
+    // and wait for completion
+    bits = xEventGroupWaitBits(info.event, wait_bits, pdTRUE, pdTRUE, pdMS_TO_TICKS(50000));
+    TEST_ASSERT_EQUAL(bits & wait_bits, wait_bits);
+
+    // now stop the worker
+    xEventGroupSetBits(info.event, STOP_WORKER_TASK);
+    bits = xEventGroupWaitBits(info.event, WORKER_TASK_STOPPED, pdTRUE, pdTRUE, pdMS_TO_TICKS(50000));
+    TEST_ASSERT_EQUAL(bits & WORKER_TASK_STOPPED, WORKER_TASK_STOPPED);
+
+    // and destroy objects
+    eppp_deinit(info.eppp_server);
+    eppp_deinit(info.eppp_client);
+    TEST_ESP_OK(esp_event_loop_delete_default());
+    vEventGroupDelete(info.event);
+
+    // wait for the lwip sockets to close cleanly
+    vTaskDelay(pdMS_TO_TICKS(1000));
+}
+
+
+TEST_GROUP_RUNNER(eppp_test)
+{
+    RUN_TEST_CASE(eppp_test, init_deinit)
+    RUN_TEST_CASE(eppp_test, open_close)
+    RUN_TEST_CASE(eppp_test, open_close_nonblocking)
+    RUN_TEST_CASE(eppp_test, open_close_taskless)
+}
+
+void app_main(void)
+{
+    UNITY_MAIN(eppp_test);
+}
diff --git a/components/eppp_link/test/test_app/main/idf_component.yml b/components/eppp_link/test/test_app/main/idf_component.yml
new file mode 100644
index 0000000000..7ecb517e8a
--- /dev/null
+++ b/components/eppp_link/test/test_app/main/idf_component.yml
@@ -0,0 +1,4 @@
+dependencies:
+  espressif/eppp_link:
+    version: "*"
+    override_path: "../../.."
diff --git a/components/eppp_link/test/test_app/sdkconfig.defaults b/components/eppp_link/test/test_app/sdkconfig.defaults
new file mode 100644
index 0000000000..0442a3bc8a
--- /dev/null
+++ b/components/eppp_link/test/test_app/sdkconfig.defaults
@@ -0,0 +1,12 @@
+# This file was generated using idf.py save-defconfig. It can be edited manually.
+# Espressif IoT Development Framework (ESP-IDF) 5.3.0 Project Minimal Configuration
+#
+CONFIG_UART_ISR_IN_IRAM=y
+CONFIG_ESP_NETIF_IP_LOST_TIMER_INTERVAL=0
+CONFIG_FREERTOS_UNICORE=y
+CONFIG_HEAP_TRACING_STANDALONE=y
+CONFIG_HEAP_TRACING_STACK_DEPTH=6
+CONFIG_LWIP_PPP_SUPPORT=y
+CONFIG_LWIP_PPP_VJ_HEADER_COMPRESSION=n
+CONFIG_LWIP_PPP_DEBUG_ON=y
+CONFIG_UNITY_ENABLE_FIXTURE=y