ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ota_esphome_udp.cpp
Go to the documentation of this file.
1#include "ota_esphome.h"
2#if defined(USE_OTA) && defined(ESPHOME_OTA_UDP)
3
5#include "esphome/core/hal.h"
7#include "esphome/core/log.h"
8
9#ifdef ESPHOME_OTA_UDP_LWIP
10#include "lwip/inet.h"
11#ifdef USE_LIBRETINY
12#include "lwip/tcpip.h"
13#endif
14#endif
15
16#include <algorithm>
17#include <cstring>
18#include <memory>
19
20/*
21 * UDP data phase: the same byte stream as TCP, cut into numbered datagrams.
22 * client -> device [type | COMMITTED][token u32][seq u16][device bytes received u16][payload]
23 * device -> client [ACK][token u32][received u16][seq that prompted it u16][stream offset u16][window u8][our bytes]
24 * The lwIP callback acks each datagram on arrival, so flash writes never look like loss, and keeps messages in its
25 * UDP_WINDOW slots until the OTA loop reads them. "window": free slots in the high nibble, and in the low nibble bit k
26 * set when received + k is already here (like TCP SACK).
27 */
28
29namespace esphome {
30
31ESPHOME_LOG_TAG(TAG, "esphome.ota");
32
33// PING (0x02) and PROBE (0x03) only prompt an ACK
34static constexpr uint8_t UDP_MSG_DATA = 0x01;
35static constexpr uint8_t UDP_FLAG_COMMITTED = 0x80;
36static constexpr uint8_t UDP_MSG_ACK = 0x40;
37static constexpr size_t UDP_HEADER_SIZE = 9;
38static constexpr size_t UDP_ACK_HEADER_SIZE = 12;
39// The client's 60 s wait for the token over a lossy TCP link, plus its 15 s probe
40static constexpr uint32_t UDP_COMMIT_TIMEOUT = 80000;
41// Like the TCP socket's SO_RCVTIMEO in handle_data_, so readall_ treats both alike
42static constexpr uint32_t UDP_READ_WAIT_MS = 2000;
43// How often the commit wait looks at the UDP link between reads of the TCP fallback byte
44static constexpr uint32_t UDP_COMMIT_POLL_MS = 100;
45static constexpr uint32_t UDP_LINGER_MS = 3000;
46
47#ifdef ESPHOME_OTA_UDP_LWIP
48#ifdef USE_LIBRETINY
49// LibreTiny's LwIPLock is a no-op; take the core lock directly (the header drops UDP without one)
50struct UdpLock {
51 UdpLock() { LOCK_TCPIP_CORE(); }
52 ~UdpLock() { UNLOCK_TCPIP_CORE(); }
53};
54#else
55using UdpLock = LwIPLock;
56#endif
57static uint16_t slot_len(struct pbuf *p) { return p->tot_len; }
58static uint16_t slot_copy(struct pbuf *p, void *dst, uint16_t len, uint16_t offset) {
59 return pbuf_copy_partial(p, dst, len, offset);
60}
61static void slot_free(struct pbuf *p) { pbuf_free(p); }
62#else
63// The host has no lwIP thread: the OTA loop drains the socket itself. Empty bodies, not = default, so clang-tidy
64// does not flag the guards as unused
65struct UdpLock {
66 UdpLock() {}
67 ~UdpLock() {}
68};
69struct OtaUdpDatagram {
70 uint16_t len;
71 uint8_t data[1500];
72};
73static uint16_t slot_len(OtaUdpDatagram *p) { return p->len; }
74static uint16_t slot_copy(OtaUdpDatagram *p, void *dst, uint16_t len, uint16_t offset) {
75 const uint16_t n = std::min<uint16_t>(len, p->len - offset);
76 memcpy(dst, p->data + offset, n);
77 return n;
78}
79static void slot_free(OtaUdpDatagram *p) { delete p; }
80#endif
81
82static void put_u16(uint8_t *at, uint16_t value) {
83 at[0] = value >> 8;
84 at[1] = value & 0xFF;
85}
86
88
90 UdpLock lock;
91#ifdef ESPHOME_OTA_UDP_LWIP
92 if (this->pcb != nullptr)
93 udp_remove(this->pcb);
94 this->pcb = nullptr;
95#else
96 this->sock = nullptr;
97#endif
98 for (auto &p : this->queue) {
99 if (p != nullptr)
100 slot_free(p);
101 p = nullptr;
102 }
103}
104
105void ESPHomeOTAComponent::udp_send_ack(UdpLink &link, uint16_t prompted_by) {
106 const uint16_t len = UDP_ACK_HEADER_SIZE + link.out_len;
107#ifdef ESPHOME_OTA_UDP_LWIP
108 struct pbuf *p = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
109 if (p == nullptr)
110 return; // Like a lost ACK: the client asks again
111 auto *ack = static_cast<uint8_t *>(p->payload);
112#else
113 uint8_t ack[UDP_ACK_HEADER_SIZE + UDP_OUT_SIZE];
114#endif
115 ack[0] = UDP_MSG_ACK;
116 memcpy(ack + 1, link.reply, 4);
117 put_u16(ack + 5, link.received);
118 put_u16(ack + 7, prompted_by);
119 put_u16(ack + 9, link.out_base);
120 uint8_t window = (UDP_WINDOW - static_cast<uint16_t>(link.received - link.consumed)) << 4;
121 for (uint8_t k = 1; k < UDP_WINDOW; k++) {
122 if (link.queue[(link.received + k) % UDP_WINDOW] != nullptr)
123 window |= 1 << k;
124 }
125 ack[11] = window;
126 memcpy(ack + UDP_ACK_HEADER_SIZE, link.out, link.out_len);
127 // Best effort: a lost ACK is repeated for the client's next datagram
128#ifdef ESPHOME_OTA_UDP_LWIP
129 udp_sendto(link.pcb, p, &link.peer_ip, link.peer_port);
130 pbuf_free(p);
131#else
132 struct sockaddr_storage to = link.peer;
133 if (to.ss_family == AF_INET6) {
134 reinterpret_cast<sockaddr_in6 *>(&to)->sin6_port = htons(link.peer_port);
135 } else {
136 reinterpret_cast<sockaddr_in *>(&to)->sin_port = htons(link.peer_port);
137 }
138 link.sock->sendto(ack, len, 0, reinterpret_cast<const struct sockaddr *>(&to), link.peer_len);
139#endif
140}
141
143 uint8_t hdr[UDP_HEADER_SIZE];
144 if (slot_copy(p, hdr, UDP_HEADER_SIZE, 0) != UDP_HEADER_SIZE || memcmp(hdr + 1, link.reply, 4) != 0) {
145 slot_free(p);
146 return;
147 }
148 // The token proves the sender; answer the port it arrived from, which NAT may have rewritten
149 link.peer_port = port;
150 link.committed |= (hdr[0] & UDP_FLAG_COMMITTED) != 0;
151 // Drop the responses the client already has
152 const uint16_t acked = encode_uint16(hdr[7], hdr[8]) - link.out_base;
153 if (acked != 0 && acked <= link.out_len) {
154 link.out_len -= acked;
155 memmove(link.out, link.out + acked, link.out_len);
156 link.out_base += acked;
157 }
158 const uint16_t seq = encode_uint16(hdr[5], hdr[6]);
159 UdpSlot &slot = link.queue[seq % UDP_WINDOW];
160 if ((hdr[0] & ~UDP_FLAG_COMMITTED) == UDP_MSG_DATA && slot_len(p) > UDP_HEADER_SIZE &&
161 static_cast<uint16_t>(seq - link.consumed) < UDP_WINDOW && slot == nullptr) {
162 slot = p;
163 while (static_cast<uint16_t>(link.received - link.consumed) < UDP_WINDOW &&
164 link.queue[link.received % UDP_WINDOW] != nullptr)
165 link.received++;
166 } else {
167 slot_free(p);
168 }
169 udp_send_ack(link, seq);
170}
171
172#ifdef ESPHOME_OTA_UDP_LWIP
173// lwIP context: the tcpip thread (ESP32, LibreTiny), SYS (ESP8266) or an IRQ (RP2040)
174void ESPHomeOTAComponent::udp_recv_cb(void *arg, struct udp_pcb * /*pcb*/, struct pbuf *p, const ip_addr_t *addr,
175 uint16_t port) {
176 auto &link = *static_cast<UdpLink *>(arg);
177 if (!ip_addr_cmp(addr, &link.peer_ip)) {
178 pbuf_free(p);
179 return;
180 }
181 udp_on_datagram(link, p, port);
182}
183#else
184static bool same_ip(const struct sockaddr_storage &a, const struct sockaddr_storage &b) {
185 uint32_t ipv4_a, ipv4_b;
186 const bool v4_a = socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&a), &ipv4_a);
187 const bool v4_b = socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&b), &ipv4_b);
188 if (v4_a || v4_b)
189 return v4_a && v4_b && ipv4_a == ipv4_b;
190 return a.ss_family == AF_INET6 && b.ss_family == AF_INET6 &&
191 memcmp(&reinterpret_cast<const sockaddr_in6 *>(&a)->sin6_addr,
192 &reinterpret_cast<const sockaddr_in6 *>(&b)->sin6_addr, sizeof(in6_addr)) == 0;
193}
194
196 UdpLink &link = *this->udp_;
197 for (;;) {
198 uint8_t buf[sizeof(OtaUdpDatagram::data)];
199 struct sockaddr_storage from;
200 socklen_t from_len = sizeof(from);
201 const ssize_t len = link.sock->recvfrom(buf, sizeof(buf), reinterpret_cast<struct sockaddr *>(&from), &from_len);
202 if (len < 0)
203 return;
204 if (!same_ip(from, link.peer))
205 continue;
206 auto *datagram = new OtaUdpDatagram; // NOLINT(cppcoreguidelines-owning-memory)
207 datagram->len = len;
208 memcpy(datagram->data, buf, len);
209 const uint16_t port = from.ss_family == AF_INET6 ? ntohs(reinterpret_cast<sockaddr_in6 *>(&from)->sin6_port)
210 : ntohs(reinterpret_cast<sockaddr_in *>(&from)->sin_port);
211 udp_on_datagram(link, datagram, port);
212 }
213}
214#endif
215
217 struct sockaddr_storage peer;
218 socklen_t peer_len = sizeof(peer);
219 if (this->client_->getpeername(reinterpret_cast<struct sockaddr *>(&peer), &peer_len) != 0)
220 return;
221 uint8_t token[4];
222 if (!random_bytes(token, sizeof(token)))
223 return;
224#ifdef ESPHOME_OTA_UDP_LWIP
225 uint32_t ipv4;
226 if (socket::sockaddr_to_ipv4(reinterpret_cast<const struct sockaddr *>(&peer), &ipv4)) {
227 ip_addr_set_ip4_u32(&link.peer_ip, ipv4);
228 } else {
229#if USE_NETWORK_IPV6 && LWIP_IPV6
230 inet6_addr_to_ip6addr(ip_2_ip6(&link.peer_ip), &reinterpret_cast<const sockaddr_in6 *>(&peer)->sin6_addr);
231 IP_SET_TYPE_VAL(link.peer_ip, IPADDR_TYPE_V6);
232#else
233 return;
234#endif
235 }
236 UdpLock lock;
237#if LWIP_IPV6
238 link.pcb = udp_new_ip_type(IPADDR_TYPE_ANY);
239#else
240 link.pcb = udp_new();
241#endif
242 // On failure the reply stays all zero and ~UdpLink removes the pcb
243 if (link.pcb == nullptr || udp_bind(link.pcb, IP_ANY_TYPE, this->port_) != ERR_OK)
244 return;
245 udp_recv(link.pcb, udp_recv_cb, &link);
246#else
247 link.peer = peer;
248 link.peer_len = peer_len;
249 // Same domain as the listener, so the peer address from getpeername fits
250 link.sock = socket::socket_ip(SOCK_DGRAM, IPPROTO_UDP);
251 struct sockaddr_storage local;
252 const socklen_t local_len =
253 socket::set_sockaddr_any(reinterpret_cast<struct sockaddr *>(&local), sizeof(local), this->port_);
254 if (link.sock == nullptr || local_len == 0 ||
255 link.sock->bind(reinterpret_cast<struct sockaddr *>(&local), local_len) != 0 ||
256 link.sock->setblocking(false) != 0)
257 return;
258#endif
259 // Below the error codes (0x80 and up) the client checks the first byte against, and never all zero
260 token[0] = (token[0] & 0x7F) | 0x01;
261 memcpy(link.reply, token, sizeof(token));
262}
263
265 // TCP now only carries a fallback byte; short read timeouts let the UDP link be checked in between
266 struct timeval tv {
267 0, UDP_COMMIT_POLL_MS * 1000
268 };
269 this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
270 const uint32_t start = millis();
271 while (millis() - start < UDP_COMMIT_TIMEOUT) {
272 this->udp_poll_();
273 bool committed;
274 {
275 UdpLock lock;
276 committed = this->udp_->committed;
277 }
278 if (committed) {
279 // The client heard us over UDP, so TCP is done. Outside the lock: closing a socket waits on the tcpip thread
280 this->client_ = nullptr;
281 return true;
282 }
283 // A raw byte outside any Noise frame: the client heard nothing over UDP. Forging it only forces the ordinary
284 // TCP path, which dropping our datagrams forces anyway
285 uint8_t fallback;
286 const ssize_t read = this->client_->read(&fallback, 1);
287 if (read == 1) {
288 this->udp_->release();
289 this->udp_ = nullptr;
290 tv = {UDP_READ_WAIT_MS / 1000, 0};
291 this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
292 return true;
293 }
294 if (read == 0) {
295 this->remote_closed_ = true;
296 return false;
297 }
298 if (!this->would_block_(errno))
299 return false;
300 App.feed_wdt();
301 }
302 return false;
303}
304
306 UdpLink &link = *this->udp_;
307 const uint32_t start = millis();
308 size_t got = 0;
309 for (;;) {
310 this->udp_poll_();
311 UdpSlot p;
312 {
313 UdpLock lock;
314 p = link.consumed != link.received ? link.queue[link.consumed % UDP_WINDOW] : nullptr;
315 }
316 if (p == nullptr) {
317 if (got != 0)
318 return got;
319 if (millis() - start >= UDP_READ_WAIT_MS)
320 break;
322 continue;
323 }
324 // The callback never touches the slot being read, so the copy runs without the lock
325 const uint16_t offset = UDP_HEADER_SIZE + link.read_offset;
326 const uint16_t n = slot_copy(p, buf + got, std::min<size_t>(len - got, slot_len(p) - offset), offset);
327 got += n;
328 link.read_offset += n;
329 if (UDP_HEADER_SIZE + link.read_offset >= slot_len(p)) {
330 UdpLock lock;
331 slot_free(p);
332 link.queue[link.consumed % UDP_WINDOW] = nullptr;
333 // A full window just opened: tell the client, which is holding back
334 const bool was_full = static_cast<uint16_t>(link.received - link.consumed) == UDP_WINDOW;
335 link.consumed++;
336 link.read_offset = 0;
337 if (was_full)
338 udp_send_ack(link, link.received - 1);
339 }
340 if (got == len)
341 return got;
342 }
343 errno = EWOULDBLOCK;
344 return -1;
345}
346
347bool ESPHomeOTAComponent::udp_send_(const uint8_t *data, size_t len) {
348 UdpLink &link = *this->udp_;
349 UdpLock lock;
350 if (link.out_len + len > UDP_OUT_SIZE)
351 return false;
352 memcpy(link.out + link.out_len, data, len);
353 link.out_len += len;
354 // Prompted by nothing new, so the client takes no round trip sample from it
355 udp_send_ack(link, link.received - 1);
356 return true;
357}
358
360 if (this->udp_ == nullptr)
361 return;
362 const uint32_t start = millis();
363 while (millis() - start < UDP_LINGER_MS) {
364 this->udp_poll_();
365 {
366 UdpLock lock;
367 if (this->udp_->out_len == 0)
368 return;
369 }
371 }
372}
373
374} // namespace esphome
375#endif // USE_OTA && ESPHOME_OTA_UDP
void feed_wdt()
Feed the task watchdog.
bool would_block_(int error_code) const
bool udp_send_(const uint8_t *data, size_t len)
static void udp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, uint16_t port)
ssize_t udp_read_(uint8_t *buf, size_t len)
static constexpr uint8_t UDP_WINDOW
Definition ota_esphome.h:92
static void udp_on_datagram(UdpLink &link, UdpSlot p, uint16_t port)
static constexpr size_t UDP_OUT_SIZE
Definition ota_esphome.h:90
std::unique_ptr< socket::Socket > client_
static void udp_send_ack(UdpLink &link, uint16_t prompted_by)
Helper class to lock the lwIP TCPIP core when making lwIP API calls from non-TCPIP threads.
Definition helpers.h:2107
uint32_t socklen_t
Definition headers.h:109
__int64 ssize_t
Definition httplib.h:178
@ IPADDR_TYPE_V6
Definition ip_address.h:32
ESPHOME_LOG_TAG(TAG, CRASH_LOG_TAG)
std::unique_ptr< Socket > socket_ip(int type, int protocol)
Create a socket in the newest available IP domain (IPv6 or IPv4) of the given type and protocol.
bool sockaddr_to_ipv4(const struct sockaddr *addr, uint32_t *out)
Network order IPv4 of an AF_INET or v4 mapped AF_INET6 peer; false for anything else.
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port)
Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
bool random_bytes(uint8_t *data, size_t len)
Generate len random bytes using the platform's secure RNG (hardware RNG or OS CSPRNG).
Definition helpers.cpp:20
const void size_t len
Definition hal.h:68
LwIPLock UdpLock
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:980
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
struct in6_addr sin6_addr
Definition headers.h:89
sa_family_t ss_family
Definition headers.h:104
uint16_t seq
uint8_t ack
SemaphoreHandle_t lock