mirror of https://github.com/arendst/Tasmota.git
457 lines
16 KiB
C++
457 lines
16 KiB
C++
/*
|
|
xdrv_38_ping.ino - support for ICMP Ping
|
|
|
|
Copyright (C) 2021 Theo Arends and Stephan Hadinger
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#ifdef USE_PING
|
|
|
|
#define XDRV_38 38
|
|
|
|
#include "lwip/icmp.h"
|
|
#include "lwip/inet_chksum.h"
|
|
#include "lwip/raw.h"
|
|
#include "lwip/timeouts.h"
|
|
|
|
const char kPingCommands[] PROGMEM = "|" // no prefix
|
|
D_CMND_PING
|
|
;
|
|
|
|
void (* const PingCommand[])(void) PROGMEM = {
|
|
&CmndPing,
|
|
};
|
|
|
|
extern "C" {
|
|
|
|
extern uint32 system_relative_time(uint32 time);
|
|
extern void ets_bzero(void *s, size_t n);
|
|
|
|
const uint16_t Ping_ID = 0xAFAF; // PING packet ID
|
|
const size_t Ping_data_size = 32; // default packet size
|
|
const uint32_t Ping_timeout_ms = 1000; // default time-out of 1 second, which is enough for LAN/WIFI
|
|
const uint32_t Ping_coarse = 1000; // interval between sending packets, 1 packet every second
|
|
|
|
typedef struct Ping_t {
|
|
ip_addr_t ip; // target IPv4 address
|
|
Ping_t *next; // next object in linked list
|
|
uint16_t seq_num; // next sequence number
|
|
uint16_t seqno; // reject a packet already received
|
|
uint8_t success_count; // sucessful responses received
|
|
uint8_t timeout_count; // time-outs (no responses)
|
|
uint8_t to_send_count; // number of packets remaining to send
|
|
uint32_t ping_time_sent; // timestamp when the packet was sent
|
|
uint32_t min_time; // minimum time in ms for a successful response
|
|
uint32_t max_time; // maximum time in ms for a successful response
|
|
uint32_t sum_time; // cumulated time in ms for all successful responses (used to compute the average)
|
|
bool done; // indicates the ping campaign is finished
|
|
bool fast; // fast mode, i.e. stop pings when first successful response
|
|
String hostname; // original hostname before convertion to IP address
|
|
} Ping_t;
|
|
|
|
// globals
|
|
Ping_t *ping_head = nullptr; // head of the Linked List for ping objects
|
|
struct raw_pcb *t_ping_pcb = nullptr; // registered with first ping, deregistered after last ping, the same pcb is used for all packets
|
|
#ifdef USE_IPV6
|
|
struct raw_pcb *t_ping6_pcb = nullptr; // IPv6 version, registered with first ping, deregistered after last ping, the same pcb is used for all packets
|
|
#endif // USE_IPV6
|
|
|
|
// ================================================================================
|
|
// Find the Ping object indexed by IP address
|
|
// ================================================================================
|
|
//
|
|
// find the ping structure corresponding to the specified IP, or nullptr if not found
|
|
//
|
|
Ping_t ICACHE_FLASH_ATTR * t_ping_find(const ip_addr_t *ip) {
|
|
Ping_t *ping = ping_head;
|
|
while (ping != nullptr) {
|
|
if (ip_addr_cmp(&ping->ip, ip)) {
|
|
return ping;
|
|
}
|
|
ping = ping->next;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
// ================================================================================
|
|
// Timer called a packet response is in time-out
|
|
// ================================================================================
|
|
//
|
|
// called after the ICMP timeout occured
|
|
// we never received the packet, increase the timeout count
|
|
//
|
|
void ICACHE_FLASH_ATTR t_ping_timeout(void* arg) {
|
|
Ping_t *ping = (Ping_t*) arg;
|
|
ping->timeout_count++;
|
|
}
|
|
|
|
// ================================================================================
|
|
// Send ICMP packet
|
|
// ================================================================================
|
|
// Prepare a echo ICMP request
|
|
//
|
|
void t_ping_prepare_echo(struct icmp_echo_hdr *iecho, uint16_t len, Ping_t *ping) {
|
|
size_t data_len = len - sizeof(struct icmp_echo_hdr);
|
|
|
|
ICMPH_TYPE_SET(iecho, ICMP_ECHO);
|
|
ICMPH_CODE_SET(iecho, 0);
|
|
iecho->chksum = 0;
|
|
iecho->id = Ping_ID;
|
|
ping->seq_num++;
|
|
if (ping->seq_num == 0x7fff) { ping->seq_num = 0; }
|
|
|
|
iecho->seqno = htons(ping->seq_num); // TODO
|
|
|
|
/* fill the additional data buffer with some data */
|
|
for (uint32_t i = 0; i < data_len; i++) {
|
|
((char*)iecho)[sizeof(struct icmp_echo_hdr) + i] = (char)i;
|
|
}
|
|
|
|
iecho->chksum = inet_chksum(iecho, len);
|
|
}
|
|
|
|
#ifdef USE_IPV6
|
|
// Prepare a echo ICMP6 request
|
|
//
|
|
void t_ping_prepare_echo6(struct icmp6_echo_hdr *iecho6, uint16_t len, Ping_t *ping) {
|
|
// TODO
|
|
size_t data_len = len - sizeof(struct icmp6_echo_hdr);
|
|
|
|
iecho6->type = ICMP6_TYPE_EREQ;
|
|
iecho6->code = 0;
|
|
iecho6->chksum = 0;
|
|
iecho6->id = Ping_ID;
|
|
ping->seq_num++;
|
|
if (ping->seq_num == 0x7fff) { ping->seq_num = 0; }
|
|
|
|
iecho6->seqno = htons(ping->seq_num);
|
|
|
|
/* fill the additional data buffer with some data */
|
|
for (uint32_t i = 0; i < data_len; i++) {
|
|
((char*)iecho6)[sizeof(struct icmp6_echo_hdr) + i] = (char)i;
|
|
}
|
|
// checksum is calculated by lwip
|
|
}
|
|
#endif // USE_IPV6
|
|
|
|
//
|
|
// send the ICMP packet
|
|
//
|
|
void ICACHE_FLASH_ATTR t_ping_send(struct raw_pcb *raw, Ping_t *ping) {
|
|
struct pbuf *p;
|
|
uint16_t ping_size = sizeof(struct icmp_echo_hdr) + Ping_data_size;
|
|
|
|
ping->ping_time_sent = millis();
|
|
p = pbuf_alloc(PBUF_IP, ping_size, PBUF_RAM);
|
|
if (!p) { return; }
|
|
if ((p->len == p->tot_len) && (p->next == nullptr)) {
|
|
|
|
#ifdef USE_IPV6
|
|
// different format for IPv4 and IPv6 packets
|
|
if (IP_IS_V6_VAL(ping->ip)) {
|
|
// IPv6
|
|
struct icmp6_echo_hdr *iecho6;
|
|
iecho6 = (struct icmp6_echo_hdr *) p->payload;
|
|
t_ping_prepare_echo6(iecho6, ping_size, ping);
|
|
// set parameters for checksum handling
|
|
t_ping6_pcb->chksum_reqd = 1;
|
|
t_ping6_pcb->chksum_offset = offsetof(icmp6_echo_hdr, chksum);
|
|
|
|
// AddLog(LOG_LEVEL_DEBUG, "PNG: sending ICMP6(%i-%i)=%*_H", p->len, ping_size, p->len, p->payload);
|
|
raw_sendto(t_ping6_pcb, p, &ping->ip);
|
|
// AddLog(LOG_LEVEL_DEBUG, "PNG: sending ICMP6(%i-%i)=%*_H", p->len, ping_size, p->len, p->payload);
|
|
} else
|
|
#endif // USE_IPV6
|
|
{
|
|
// IPv4
|
|
struct icmp_echo_hdr *iecho;
|
|
iecho = (struct icmp_echo_hdr *) p->payload;
|
|
t_ping_prepare_echo(iecho, ping_size, ping);
|
|
raw_sendto(t_ping_pcb, p, &ping->ip);
|
|
// AddLog(LOG_LEVEL_DEBUG, "PNG: sending ICMP4(%i-%i)=%*_H", p->len, ping_size, p->len, p->payload);
|
|
}
|
|
}
|
|
pbuf_free(p);
|
|
}
|
|
|
|
// ================================================================================
|
|
// Timer called when it's time to send next packet, of when finished
|
|
// ================================================================================
|
|
// this timer is called every x seconds to send a new packet, whatever happened to the previous packet
|
|
static void ICACHE_FLASH_ATTR t_ping_coarse_tmr(void *arg) {
|
|
Ping_t *ping = (Ping_t*) arg;
|
|
if (ping->to_send_count > 0) {
|
|
ping->to_send_count--;
|
|
// have we sent all packets?
|
|
t_ping_send(t_ping_pcb, ping); // ICMP can also send ICMP6
|
|
|
|
sys_timeout(Ping_timeout_ms, t_ping_timeout, ping);
|
|
sys_timeout(Ping_coarse, t_ping_coarse_tmr, ping);
|
|
} else {
|
|
sys_untimeout(t_ping_coarse_tmr, ping);
|
|
ping->done = true;
|
|
}
|
|
}
|
|
|
|
// ================================================================================
|
|
// Callback: a packet response was received
|
|
// ================================================================================
|
|
//
|
|
// Reveived packet
|
|
//
|
|
static uint8_t ICACHE_FLASH_ATTR t_ping_recv(void *arg, struct raw_pcb *pcb, struct pbuf *p, const ip_addr_t *addr) {
|
|
// AddLog(LOG_LEVEL_DEBUG, "PNG: from %s pub(%i)=%*_H", IPAddress(*addr).toString().c_str(), p->len, p->len, p->payload);
|
|
Ping_t *ping = t_ping_find(addr);
|
|
|
|
if (nullptr == ping) { // unknown source address
|
|
return 0; // don't eat the packet and ignore it
|
|
}
|
|
|
|
size_t pbuf_header_len = PBUF_TRANSPORT_HLEN;
|
|
bool ipv6 = false;
|
|
#ifdef USE_IPV6
|
|
if (pcb == t_ping6_pcb) {
|
|
pbuf_header_len = PBUF_IP_HLEN;
|
|
ipv6 = true;
|
|
}
|
|
#endif // USE_IPV6
|
|
if (pbuf_header(p, -pbuf_header_len)==0) {
|
|
// AddLog(LOG_LEVEL_DEBUG, "PNG: received(%i)=%*_H", p->len, p->len, p->payload);
|
|
struct icmp_echo_hdr *iecho;
|
|
iecho = (struct icmp_echo_hdr *)p->payload;
|
|
|
|
uint8_t icmp_resp_type = ICMP_ER;
|
|
#ifdef USE_IPV6
|
|
icmp_resp_type = (ipv6 ? ICMP6_TYPE_EREP : ICMP_ER);
|
|
#endif // USE_IPV6
|
|
if ((iecho->id == Ping_ID) && (iecho->seqno == htons(ping->seq_num)) && iecho->type == icmp_resp_type) {
|
|
|
|
if (iecho->seqno != ping->seqno){ // debounce already received packet
|
|
/* do some ping result processing */
|
|
sys_untimeout(t_ping_timeout, ping); // remove time-out handler
|
|
uint32_t delay = millis() - ping->ping_time_sent;
|
|
|
|
ping->sum_time += delay;
|
|
if (delay < ping->min_time) { ping->min_time = delay; }
|
|
if (delay > ping->max_time) { ping->max_time = delay; }
|
|
|
|
ping->success_count++;
|
|
ping->seqno = iecho->seqno;
|
|
if (ping->fast) { // if fast mode, abort further pings when first successful response is received
|
|
sys_untimeout(t_ping_coarse_tmr, ping);
|
|
ping->done = true;
|
|
ping->to_send_count = 0;
|
|
}
|
|
}
|
|
|
|
pbuf_free(p);
|
|
return 1; /* eat the packet */
|
|
}
|
|
}
|
|
|
|
return 0; /* don't eat the packet */
|
|
}
|
|
|
|
// ================================================================================
|
|
// Internal structure PCB management
|
|
// ================================================================================
|
|
// we are going to send a packet, make sure pcb is initialized
|
|
void t_ping_register_pcb(void) {
|
|
if (nullptr == t_ping_pcb) {
|
|
t_ping_pcb = raw_new(IP_PROTO_ICMP);
|
|
|
|
raw_recv(t_ping_pcb, t_ping_recv, nullptr); // we cannot register data structure here as we can only register one
|
|
raw_bind(t_ping_pcb, IP4_ADDR_ANY);
|
|
}
|
|
#ifdef USE_IPV6
|
|
if (nullptr == t_ping6_pcb) {
|
|
t_ping6_pcb = raw_new(IP6_NEXTH_ICMP6);
|
|
|
|
raw_recv(t_ping6_pcb, t_ping_recv, nullptr); // we cannot register data structure here as we can only register one
|
|
raw_bind(t_ping6_pcb, IP6_ADDR_ANY);
|
|
}
|
|
#endif // USE_IPV6
|
|
}
|
|
|
|
// we have finsihed a ping series, deallocated if no more ongoing
|
|
void t_ping_deregister_pcb(void) {
|
|
if (nullptr == ping_head) { // deregister only if no ping is flying
|
|
raw_remove(t_ping_pcb);
|
|
t_ping_pcb = nullptr;
|
|
#ifdef USE_IPV6
|
|
raw_remove(t_ping6_pcb);
|
|
t_ping6_pcb = nullptr;
|
|
#endif // USE_IPV6
|
|
}
|
|
}
|
|
|
|
// ================================================================================
|
|
// Start pings
|
|
// ================================================================================
|
|
// returns:
|
|
// 0: OK
|
|
// -1: ping already ongoing for this address
|
|
// -2: unable to resolve address
|
|
int32_t t_ping_start(const char *hostname, uint32_t count) {
|
|
IPAddress ipfull;
|
|
bool host_resolved = WifiHostByName(hostname, ipfull);
|
|
|
|
ip_addr_t ip;
|
|
ip_addr_set_any_val(false, ip);
|
|
#ifdef USE_IPV6
|
|
ipfull.to_ip_addr_t(&ip);
|
|
#else
|
|
ip_addr_set_ip4_u32_val(ip, (uint32_t)ipfull);
|
|
#endif
|
|
|
|
// check if pings are already ongoing for this IP
|
|
if (host_resolved && t_ping_find(&ip)) {
|
|
return -1;
|
|
}
|
|
|
|
Ping_t *ping = new Ping_t();
|
|
if (0 == count) {
|
|
count = 4;
|
|
ping->fast = true;
|
|
}
|
|
ping->min_time = UINT32_MAX;
|
|
ping->ip = ip;
|
|
ping->to_send_count = count - 1;
|
|
ping->hostname = hostname;
|
|
|
|
// add to Linked List from head
|
|
ping->next = ping_head;
|
|
ping_head = ping; // insert at head
|
|
|
|
if (!host_resolved) { // If invalid address, set as completed
|
|
ping->done = true;
|
|
return -2;
|
|
}
|
|
|
|
// send
|
|
t_ping_register_pcb();
|
|
t_ping_send(t_ping_pcb, ping);
|
|
|
|
// set timers for time-out and cadence
|
|
sys_timeout(Ping_timeout_ms, t_ping_timeout, ping);
|
|
sys_timeout(Ping_coarse, t_ping_coarse_tmr, ping);
|
|
|
|
return 0;
|
|
}
|
|
|
|
}
|
|
|
|
// Check if any ping requests is completed, and publish the results
|
|
void PingResponsePoll(void) {
|
|
Ping_t *ping = ping_head;
|
|
Ping_t **prev_link = &ping_head; // previous link pointer (used to remove en entry)
|
|
|
|
while (ping != nullptr) {
|
|
if (ping->done) {
|
|
uint32_t success = ping->success_count;
|
|
bool resolved = !ip_addr_isany_val(ping->ip);
|
|
|
|
if (!resolved) {
|
|
Response_P(PSTR("{\"" D_JSON_PING "\":{\"%s\":{"
|
|
"\"Reachable\":false"
|
|
",\"IP\":\"\""
|
|
",\"Success\":false"
|
|
"}}}"),
|
|
ping->hostname.c_str()
|
|
);
|
|
} else {
|
|
Response_P(PSTR("{\"" D_JSON_PING "\":{\"%s\":{"
|
|
"\"Reachable\":%s"
|
|
",\"IP\":\"%s\""
|
|
",\"Success\":%d"
|
|
",\"Timeout\":%d"
|
|
",\"MinTime\":%d"
|
|
",\"MaxTime\":%d"
|
|
",\"AvgTime\":%d"
|
|
"}}}"),
|
|
ping->hostname.c_str(),
|
|
success ? PSTR("true") : PSTR("false"),
|
|
#ifdef USE_IPV6
|
|
IPAddress(&ping->ip).toString(true).c_str(),
|
|
#else
|
|
IPAddress(ip_addr_get_ip4_u32(&ping->ip)).toString().c_str(),
|
|
#endif
|
|
// ip & 0xFF, (ip >> 8) & 0xFF, (ip >> 16) & 0xFF, ip >> 24,
|
|
success,
|
|
ping->timeout_count,
|
|
success ? ping->min_time : 0,
|
|
ping->max_time,
|
|
success ? ping->sum_time / success : 0
|
|
);
|
|
}
|
|
MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_TELE, PSTR(D_JSON_PING));
|
|
|
|
// remove from linked list
|
|
*prev_link = ping->next;
|
|
// don't increment prev_link
|
|
Ping_t *ping_to_delete = ping;
|
|
ping = ping->next; // move to next before deleting the object
|
|
delete ping_to_delete; // free memory allocated
|
|
} else {
|
|
prev_link = &ping->next;
|
|
ping = ping->next;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*********************************************************************************************\
|
|
* Ping Command
|
|
\*********************************************************************************************/
|
|
|
|
void CmndPing(void) {
|
|
uint32_t count = XdrvMailbox.index;
|
|
|
|
RemoveSpace(XdrvMailbox.data);
|
|
if (count > 10) { count = 8; } // max 8 seconds
|
|
|
|
int32_t res = t_ping_start(XdrvMailbox.data, count);
|
|
if (0 == res) {
|
|
ResponseCmndDone();
|
|
} else if (-1 == res) {
|
|
ResponseCmndChar_P(PSTR("Ping already ongoing for this IP"));
|
|
} else {
|
|
ResponseCmndChar_P(PSTR("Unable to resolve IP address"));
|
|
}
|
|
}
|
|
|
|
|
|
/*********************************************************************************************\
|
|
* Interface
|
|
\*********************************************************************************************/
|
|
|
|
bool Xdrv38(uint32_t function)
|
|
{
|
|
bool result = false;
|
|
|
|
switch (function) {
|
|
case FUNC_EVERY_250_MSECOND:
|
|
PingResponsePoll(); // TODO
|
|
break;
|
|
case FUNC_COMMAND:
|
|
result = DecodeCommand(kPingCommands, PingCommand);
|
|
break;
|
|
case FUNC_ACTIVE:
|
|
result = true;
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#endif // USE_PING
|