Skip to content

feat(eppp): Add support for TUN interface #812

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/eppp__build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ jobs:
name: Build
strategy:
matrix:
idf_ver: ["latest", "release-v5.3"]
idf_ver: ["latest", "release-v5.5", "release-v5.4", "release-v5.3"]
test: [ { app: host, path: "examples/host" }, { app: slave, path: "examples/slave" }, { app: test_app, path: "test/test_app" }]
runs-on: ubuntu-22.04
container: espressif/idf:${{ matrix.idf_ver }}
steps:
- name: Checkout esp-protocols
uses: actions/checkout@v3
- name: Build ${{ matrix.test.app }} with IDF-${{ matrix.idf_ver }}
env:
EXPECTED_WARNING: "DeprecationWarning: 'MultiCommand' is deprecated and will be removed\nCryptographyDeprecationWarning: Parsed a serial number which wasn't positive"
shell: bash
run: |
. ${IDF_PATH}/export.sh
Expand Down
19 changes: 15 additions & 4 deletions components/eppp_link/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,26 @@ if(CONFIG_EPPP_LINK_DEVICE_ETH)
set(transport_src eppp_eth.c)
endif()

if(CONFIG_EPPP_LINK_DEVICE_SPI)
set(transport_src eppp_spi.c)
endif()

if(CONFIG_EPPP_LINK_DEVICE_UART)
set(transport_src eppp_uart.c)
endif()

if(CONFIG_EPPP_LINK_DEVICE_SDIO)
set(transport_src eppp_sdio_slave.c eppp_sdio_host.c)
set(transport_src eppp_sdio.c eppp_sdio_slave.c eppp_sdio_host.c)
endif()

if(NOT CONFIG_EPPP_LINK_USES_PPP)
set(netif_src eppp_netif_tun.c)
endif()

idf_component_register(SRCS eppp_link.c ${transport_src}
idf_component_register(SRCS eppp_link.c ${transport_src} ${netif_src}
INCLUDE_DIRS "include"
PRIV_REQUIRES esp_netif esp_timer esp_eth ${driver_deps})

if(CONFIG_EPPP_LINK_DEVICE_ETH)
idf_component_get_property(ethernet_init espressif__ethernet_init COMPONENT_LIB)
target_link_libraries(${COMPONENT_LIB} PRIVATE ${ethernet_init})
idf_component_optional_requires(PRIVATE ethernet_init espressif__ethernet_init)
endif()
12 changes: 9 additions & 3 deletions components/eppp_link/Kconfig
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
menu "eppp_link"

config EPPP_LINK_USES_LWIP
bool
default "y"
config EPPP_LINK_USES_PPP
bool "Use PPP network interface"
default "n"
select LWIP_PPP_SUPPORT
select LWIP_PPP_SERVER_SUPPORT
help
Enable this option to use PPP network interface.
This is useful when pairing with another PPP device,
e.g. pppd service on Linux.
By default EPPP_LINK uses plain TUN interface,
relying on transports to split on packet boundaries.

choice EPPP_LINK_DEVICE
prompt "Choose PPP device"
Expand Down
31 changes: 26 additions & 5 deletions components/eppp_link/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,45 @@
# ESP PPP Link component (eppp_link)

The component provides a general purpose connectivity engine between two microcontrollers, one acting as PPP server (slave), the other one as PPP client (host).
This component could be used for extending network using physical serial connection. Applications could vary from providing PRC engine for multiprocessor solutions to serial connection to POSIX machine. This uses a standard PPP protocol to negotiate IP addresses and networking, so standard PPP toolset could be used, e.g. a `pppd` service on linux. Typical application is a WiFi connectivity provider for chips that do not have WiFi
The component provides a general purpose connectivity engine between two microcontrollers, one acting as PPP server, the other one as PPP client.
This component could be used for extending network using physical serial connection. Applications could vary from providing PRC engine for multiprocessor solutions to serial connection to POSIX machine. This uses a standard PPP protocol (if enabled) to negotiate IP addresses and networking, so standard PPP toolset could be used, e.g. a `pppd` service on linux. Typical application is a WiFi connectivity provider for chips that do not have WiFi.
Uses simplified TUN network interface by default to enable faster data transfer on non-UART transports.

## Typical application

Using this component we can construct a WiFi connectivity gateway on PPP channel. The below diagram depicts an application where
PPP server is running on a WiFi capable chip with NAPT module translating packets between WiFi and PPPoS interface.
We usually call this node a SLAVE microcontroller. The "HOST" microcontroller runs PPP client and connects only to the serial line,
brings in the WiFi connectivity from the "SLAVE" microcontroller.
We usually call this node a communication coprocessor, or a "SLAVE" microcontroller.
The main microcontroller (sometimes also called the "HOST") runs PPP client and connects only to the serial line,
brings in the WiFi connectivity from the communication coprocessor.

```
SLAVE micro HOST micro
Communication comprocessor Main microcontroller
\|/ +----------------+ +----------------+
| | | (serial) line | |
+---+ WiFi NAT PPPoS |=== UART / SPI / SDIO / ETH ===| PPPoS client |
| (server)| | |
+----------------+ +----------------+
```

## Configuration

### Choose the transport layer

Use `idf.py menuconfig` to select the transport layer:

* `CONFIG_EPPP_LINK_UART` -- Use UART transport layer
* `CONFIG_EPPP_LINK_SPI` -- Use SPI transport layer
* `CONFIG_EPPP_LINK_SDIO` -- Use SDIO transport layer
* `CONFIG_EPPP_LINK_ETHERNET` -- Use Ethernet transport
- Note: Ethernet creates it's own task, so calling `eppp_perform()` would not work
- Note: Add dependency to ethernet_init component to use other Ethernet drivers
- Note: You can override functions `eppp_transport_ethernet_deinit()` and `eppp_transport_ethernet_init()` to use your own Ethernet driver

### Choose the network interface

Use PPP netif for UART; Keep the default (TUN) for others


## API

### Client
Expand Down
144 changes: 118 additions & 26 deletions components/eppp_link/eppp_eth.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,18 @@
#include "esp_check.h"
#include "esp_event.h"
#include "esp_mac.h"
#include "esp_idf_version.h"
#include "eppp_link.h"
#include "eppp_transport.h"
#include "esp_eth_driver.h"
#include "ethernet_init.h"
#include "esp_eth_spec.h"
#include "eppp_transport_eth.h"
// Use Ethernet Init component if available
// (otherwise use just simple init/deinit with generic MAC/PHY)
#if __has_include("ethernet_init.h")
#define USE_ETHERNET_INIT_COMPONENT
#include "ethernet_init.h"
#endif

typedef struct header {
uint8_t dst[ETH_ADDR_LEN];
Expand All @@ -26,10 +33,57 @@ typedef struct header {
static const char *TAG = "eppp_ethernet";
static bool s_is_connected = false;
static esp_eth_handle_t *s_eth_handles = NULL;
static uint8_t s_out_buffer[ETH_MAX_PACKET_SIZE];
static uint8_t s_their_mac[ETH_ADDR_LEN];
static uint8_t s_our_mac[ETH_ADDR_LEN];

#ifndef USE_ETHERNET_INIT_COMPONENT
static esp_eth_handle_t s_handles[1] = { NULL };
static esp_eth_mac_t *s_mac = NULL;
static esp_eth_phy_t *s_phy = NULL;

static void simple_deinit(esp_eth_handle_t *handle_array[])
{
if (s_handles[0] != NULL) {
esp_eth_driver_uninstall(s_handles[0]);
s_handles[0] = NULL;
}
if (s_mac != NULL) {
s_mac->del(s_mac);
s_mac = NULL;
}
if (s_phy != NULL) {
s_phy->del(s_phy);
s_phy = NULL;
}
}

static esp_err_t simple_init(struct eppp_config_ethernet_s *config, esp_eth_handle_t *handle_array[])
{
esp_err_t ret = ESP_OK;
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
eth_esp32_emac_config_t esp32_emac_config = ETH_ESP32_EMAC_DEFAULT_CONFIG();
esp32_emac_config.smi_gpio.mdc_num = config->mdc_io;
esp32_emac_config.smi_gpio.mdio_num = config->mdio_io;
s_mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
phy_config.phy_addr = config->phy_addr;
phy_config.reset_gpio_num = config->rst_io;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
s_phy = esp_eth_phy_new_generic(&phy_config);
#else
s_phy = esp_eth_phy_new_ip101(&phy_config);
#endif
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(s_mac, s_phy);
ESP_GOTO_ON_ERROR(esp_eth_driver_install(&eth_config, &s_handles[0]), err, TAG, "Ethernet driver install failed");
*handle_array = s_handles;
return ESP_OK;
err:
simple_deinit(handle_array);
return ret;
}

#endif // USE_ETHERNET_INIT_COMPONENT

static void event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
Expand Down Expand Up @@ -65,55 +119,93 @@ static esp_err_t receive(esp_eth_handle_t h, uint8_t *buffer, uint32_t len, void
return ESP_FAIL;
}

__attribute__((weak)) esp_err_t eppp_transport_ethernet_init(esp_eth_handle_t *handle_array[])
__attribute__((weak)) esp_err_t eppp_transport_ethernet_init(struct eppp_config_ethernet_s *config, esp_eth_handle_t *handle_array[])
{
#ifdef USE_ETHERNET_INIT_COMPONENT
uint8_t eth_port_cnt = 0;
ESP_RETURN_ON_ERROR(ethernet_init_all(handle_array, &eth_port_cnt), TAG, "Failed to init common eth drivers");
ESP_RETURN_ON_FALSE(eth_port_cnt == 1, ESP_ERR_INVALID_ARG, TAG, "multiple Ethernet devices detected, please init only one");
return ESP_OK;
#else
return simple_init(config, handle_array);
#endif
}

__attribute__((weak)) void eppp_transport_ethernet_deinit(esp_eth_handle_t *handle_array)
__attribute__((weak)) void eppp_transport_ethernet_deinit(esp_eth_handle_t *handle_array[])
{
#ifdef USE_ETHERNET_INIT_COMPONENT
ethernet_deinit_all(s_eth_handles);
#else
simple_deinit(handle_array);
#endif
}

esp_err_t eppp_transport_tx(void *h, void *buffer, size_t len)
{
static uint8_t out_buffer[ETH_MAX_PACKET_SIZE];
if (!s_is_connected) {
return ESP_FAIL;
}
// setup Ethernet header
header_t *head = (header_t *)out_buffer;
memcpy(head->dst, s_their_mac, ETH_ADDR_LEN);
memcpy(head->src, s_our_mac, ETH_ADDR_LEN);
head->len = len;
// support only payloads with len <= ETH_MAX_PAYLOAD_LEN
if (len > ETH_MAX_PAYLOAD_LEN) {
return ESP_FAIL;
}
memcpy(out_buffer + ETH_HEADER_LEN, buffer, len);
return esp_eth_transmit(s_eth_handles[0], out_buffer, len + ETH_HEADER_LEN);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Transmit could be optimized by using esp_eth_transmit_ctrl_vargs function. If you use it, you can skip the whole memcpy(out_buffer + ETH_HEADER_LEN, buffer, len); operation. The Ethernet frame is then assembled only during copying to DMA.

Note that you can use esp_eth_transmit_vargs wrapper to ensure backward compatibility with older versions of IDF.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Thanks, will use it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Checked the implementation of esp_eth_transmit_ctrl_vargs(), but it seems like it expect the whole Ethernet frames including headers, so you cannot transmit header and payload in two buffers, correct?

esp_eth_transmit_ctrl_vargs(eth_handle, &ts, 2, test_pkt, transmit_size));
// here `test_pkt[] = { eth-header, payload }`

I would still need to reassembly the frame here, since I got only TUN payload (starting from IP header) from the network (unlike standard network handlers, where we get Ethernet frames).
Could make some improvements, like "prepend" the headers to a pbuf, but that would again bring lwip deps here.

}

esp_err_t eppp_transport_init(eppp_config_t *config, esp_netif_t *esp_netif)
static esp_err_t start_driver(esp_netif_t *esp_netif)
{
ESP_RETURN_ON_ERROR(eppp_transport_ethernet_init(&s_eth_handles), TAG, "Failed to initialize Ethernet driver");
ESP_RETURN_ON_ERROR(esp_eth_update_input_path(s_eth_handles[0], receive, esp_netif), TAG, "Failed to set Ethernet Rx callback");
sscanf(CONFIG_EPPP_LINK_ETHERNET_OUR_ADDRESS, "%2" PRIu8 ":%2" PRIu8 ":%2" PRIi8 ":%2" PRIu8 ":%2" PRIu8 ":%2" PRIu8,
sscanf(CONFIG_EPPP_LINK_ETHERNET_OUR_ADDRESS, "%2" PRIx8 ":%2" PRIx8 ":%2" PRIx8 ":%2" PRIx8 ":%2" PRIx8 ":%2" PRIx8,
&s_our_mac[0], &s_our_mac[1], &s_our_mac[2], &s_our_mac[3], &s_our_mac[4], &s_our_mac[5]);

sscanf(CONFIG_EPPP_LINK_ETHERNET_THEIR_ADDRESS, "%2" PRIu8 ":%2" PRIu8 ":%2" PRIi8 ":%2" PRIu8 ":%2" PRIu8 ":%2" PRIu8,
sscanf(CONFIG_EPPP_LINK_ETHERNET_THEIR_ADDRESS, "%2" PRIx8 ":%2" PRIx8 ":%2" PRIx8 ":%2" PRIx8 ":%2" PRIx8 ":%2" PRIx8,
&s_their_mac[0], &s_their_mac[1], &s_their_mac[2], &s_their_mac[3], &s_their_mac[4], &s_their_mac[5]);
esp_eth_ioctl(s_eth_handles[0], ETH_CMD_S_MAC_ADDR, s_our_mac);
ESP_RETURN_ON_ERROR(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, event_handler, NULL), TAG, "Failed to register Ethernet handlers");
ESP_RETURN_ON_ERROR(esp_eth_start(s_eth_handles[0]), TAG, "Failed to start Ethernet driver");
return ESP_OK;
}

void eppp_transport_deinit(void)
static esp_err_t post_attach(esp_netif_t *esp_netif, void *args)
{
esp_eth_stop(s_eth_handles[0]);
eppp_transport_ethernet_deinit(s_eth_handles);
eppp_transport_handle_t h = (eppp_transport_handle_t)args;
ESP_RETURN_ON_FALSE(h, ESP_ERR_INVALID_ARG, TAG, "Transport handle cannot be null");
h->base.netif = esp_netif;

esp_netif_driver_ifconfig_t driver_ifconfig = {
.handle = h,
.transmit = eppp_transport_tx,
};

ESP_RETURN_ON_ERROR(esp_netif_set_driver_config(esp_netif, &driver_ifconfig), TAG, "Failed to set driver config");
ESP_LOGI(TAG, "EPPP Ethernet transport attached to EPPP netif %s", esp_netif_get_desc(esp_netif));
ESP_RETURN_ON_ERROR(start_driver(esp_netif), TAG, "Failed to start EPPP ethernet driver");
ESP_LOGI(TAG, "EPPP Ethernet driver started");
return ESP_OK;
}

esp_err_t eppp_transport_tx(void *h, void *buffer, size_t len)
eppp_transport_handle_t eppp_eth_init(struct eppp_config_ethernet_s *config)
{
if (!s_is_connected) {
return ESP_FAIL;
}
// setup Ethernet header
header_t *head = (header_t *)s_out_buffer;
memcpy(head->dst, s_their_mac, ETH_ADDR_LEN);
memcpy(head->src, s_our_mac, ETH_ADDR_LEN);
head->len = len;
// support only payloads with len <= ETH_MAX_PAYLOAD_LEN
if (len > ETH_MAX_PAYLOAD_LEN) {
return ESP_FAIL;
}
memcpy(s_out_buffer + ETH_HEADER_LEN, buffer, len);
return esp_eth_transmit(s_eth_handles[0], s_out_buffer, len + ETH_HEADER_LEN);
__attribute__((unused)) esp_err_t ret = ESP_OK;
ESP_RETURN_ON_FALSE(config, NULL, TAG, "Config cannot be null");
eppp_transport_handle_t h = calloc(1, sizeof(struct eppp_handle));
ESP_RETURN_ON_FALSE(h, NULL, TAG, "Failed to allocate eppp_handle");
ESP_GOTO_ON_ERROR(eppp_transport_ethernet_init(config, &s_eth_handles), err, TAG, "Failed to init Ethernet transport");
h->base.post_attach = post_attach;
return h;
err:
return NULL;
}

void eppp_eth_deinit(eppp_transport_handle_t h)
{
esp_eth_stop(s_eth_handles[0]);
eppp_transport_ethernet_deinit(&s_eth_handles);
}
Loading