ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
ota_esphome.h
Go to the documentation of this file.
1#pragma once
2
4#ifdef USE_OTA
7#ifdef USE_OTA_ENCRYPTION
9#endif
10#ifdef USE_OTA_DEFLATE
11#include "ota_esphome_inflate.h"
12#endif
14#include "esphome/core/log.h"
17
18// UDP data phase: raw lwIP needs a main loop lock (or a single thread); the host polls a socket instead
19#ifdef USE_ESP32
20#include <sdkconfig.h>
21#endif
22#if defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || \
23 (defined(USE_ESP32) && defined(CONFIG_LWIP_TCPIP_CORE_LOCKING))
24#define ESPHOME_OTA_UDP
25#define ESPHOME_OTA_UDP_LWIP
26#include "lwip/ip_addr.h"
27#include "lwip/pbuf.h"
28#include "lwip/udp.h"
29#if defined(USE_LIBRETINY) && !LWIP_TCPIP_CORE_LOCKING
30#undef ESPHOME_OTA_UDP
31#undef ESPHOME_OTA_UDP_LWIP
32#endif
33#elif defined(USE_HOST)
34#define ESPHOME_OTA_UDP
35#endif
36
37namespace esphome {
38
41 public:
42 enum class OTAState : uint8_t {
43 IDLE,
44 MAGIC_READ, // Reading magic bytes
45 MAGIC_ACK, // Sending OK and version after magic bytes
46 FEATURE_READ, // Reading feature flags from client
47 FEATURE_ACK, // Sending feature acknowledgment
48#ifdef USE_OTA_PASSWORD
49 AUTH_SEND, // Sending authentication request
50 AUTH_READ, // Reading authentication data
51#endif // USE_OTA_PASSWORD
52#ifdef USE_OTA_ENCRYPTION
53 NOISE_HANDSHAKE, // Exchanging Noise handshake frames
54#endif
55 DATA, // BLOCKING! Processing OTA data (update, etc.)
56 };
57#ifdef USE_OTA_PASSWORD
58 void set_auth_password(const std::string &password) { password_ = password; }
59#else
60 // Stub so lambdas referencing set_auth_password() produce a clear error instead of
61 // a cryptic "no member" diagnostic. Only fires if the stub is actually instantiated.
62 template<bool B = false> void set_auth_password(const std::string &) {
63 static_assert(B, "set_auth_password() requires the OTA auth path to be compiled. "
64 "Add 'password: \"\"' (empty string) to your 'ota: - platform: esphome' "
65 "config to enable runtime password rotation.");
66 }
67#endif // USE_OTA_PASSWORD
68
69#ifdef USE_OTA_ENCRYPTION
71 void set_noise_psk(const uint8_t *psk) { this->noise_ctx_.set_psk(psk); }
72#endif
73
75 void set_port(uint16_t port) { this->port_ = port; }
76
77 void setup() override;
78 void dump_config() override;
79 float get_setup_priority() const override;
80 void loop() override;
81
82 uint16_t get_port() const { return this->port_; }
83
84 protected:
85 void handle_handshake_();
86 void handle_data_();
87
88#ifdef ESPHOME_OTA_UDP
89 // UDP transport for the data phase; wire format in ota_esphome_udp.cpp
90 static constexpr size_t UDP_OUT_SIZE = 64; // our unacked responses: at most three encrypted ones
91 // Messages the device keeps unread: like TCP's 4 segment window, so no more memory than TCP holds
92 static constexpr uint8_t UDP_WINDOW = 4;
93#ifdef ESPHOME_OTA_UDP_LWIP
94 using UdpSlot = struct pbuf *;
95#else
96 using UdpSlot = struct OtaUdpDatagram *;
97#endif
98 // Lives on handle_data_'s stack; the receive callback shares it under the lwIP lock
99 struct UdpLink {
100 ~UdpLink();
101 void release(); // closes the socket and frees what is queued
102#ifdef ESPHOME_OTA_UDP_LWIP
103 struct udp_pcb *pcb{nullptr};
105#else
106 std::unique_ptr<socket::Socket> sock;
107 struct sockaddr_storage peer; // the TCP peer; its port is replaced by peer_port
109#endif
110 uint16_t peer_port{0}; // learned from the client's datagrams
111 // The session token, our answer to the client: top bit clear, so never an error code; all zero declines
112 uint8_t reply[4]{};
113 UdpSlot queue[UDP_WINDOW]{}; // slot seq % UDP_WINDOW for seq in [consumed, consumed + UDP_WINDOW)
114 uint16_t received{0}; // every seq before it is here
115 uint16_t consumed{0}; // the message being read
116 uint16_t read_offset{0}; // payload bytes of it already read
117 uint16_t out_base{0}; // stream offset of out[0]
118 uint8_t out_len{0};
119 bool committed{false}; // the client heard us over UDP
121 };
122 // Fills link.reply on success; leaves it all zero (declined) otherwise
123 void udp_open_(UdpLink &link);
124 // Waits for the client to commit to UDP or fall back to TCP; false on failure
125 bool udp_start_();
126 // Like a blocking socket read with SO_RCVTIMEO: up to len bytes, or -1 with EWOULDBLOCK
127 ssize_t udp_read_(uint8_t *buf, size_t len);
128 bool udp_send_(const uint8_t *data, size_t len);
129 // In a UDP session, keeps answering until the client has our responses (an error code), briefly
130 void udp_linger_();
131 // A datagram from the client's IP; caller holds the lwIP lock (or runs in the receive callback)
132 static void udp_on_datagram(UdpLink &link, UdpSlot p, uint16_t port);
133 // Caller holds the lwIP lock (or runs in the receive callback)
134 static void udp_send_ack(UdpLink &link, uint16_t prompted_by);
135#ifdef ESPHOME_OTA_UDP_LWIP
136 static void udp_recv_cb(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, uint16_t port);
137 void udp_poll_() {}
138#else
139 void udp_poll_(); // hands what the socket received to udp_on_datagram
140#endif
141#endif
142 // Data phase only (udp_ shares storage with the handshake's connect time): TCP or, once committed, the UDP stream
143 inline ssize_t transport_read_(uint8_t *buf, size_t len) {
144#ifdef ESPHOME_OTA_UDP
145 if (this->udp_ != nullptr)
146 return this->udp_read_(buf, len);
147#endif
148 return this->client_->read(buf, len);
149 }
150#ifdef USE_OTA_PASSWORD
151 static constexpr size_t SHA256_HEX_SIZE = 64; // SHA256 hash as hex string (32 bytes * 2)
152 bool handle_auth_send_();
153 bool handle_auth_read_();
154 bool select_auth_type_();
155 void cleanup_auth_();
156 void log_auth_warning_(const LogString *msg);
157#endif // USE_OTA_PASSWORD
158 bool readall_(uint8_t *buf, size_t len);
159 bool writeall_(const uint8_t *buf, size_t len);
160
161#ifdef USE_OTA_ENCRYPTION
162 // Heap-allocated only while an encrypted OTA session is active.
166 NoiseCipherState *send_cipher{nullptr};
167 NoiseCipherState *recv_cipher{nullptr};
168 uint16_t frame_len{0}; // total frame size once the header is parsed, 0 until then
169 uint16_t frame_pos{0}; // bytes read or written so far
170 bool writing{false}; // a produced handshake frame is still being flushed
171 uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE];
172 };
173 // The api server's live context when it exists, otherwise our own (a build
174 // time key, or the saved key loaded in safe mode)
175 const noise::NoiseContext &noise_context_() const;
176 // True once the feature ack offers noise and the client asked for it
177 bool noise_offered_() const;
179 bool noise_start_session_(uint8_t server_feature_flags);
182 size_t noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len);
184 void noise_send_reject_(const LogString *reason);
185 ssize_t noise_decrypt_(uint8_t *buf, size_t len);
186 ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext);
187 bool noise_readall_(uint8_t *buf, size_t len);
188 ssize_t noise_read_data_(uint8_t *buf, size_t capacity);
189 static constexpr size_t NOISE_MAX_RESPONSE = 4; // the UDP token
190 bool noise_write_(const uint8_t *data, size_t len);
191#endif // USE_OTA_ENCRYPTION
192
193 // Data-phase I/O dispatch: through the noise transport when a session is
194 // active, straight to the socket otherwise.
195 // len is at most NOISE_MAX_RESPONSE
196 inline bool data_write_(const uint8_t *data, size_t len) {
197#ifdef USE_OTA_ENCRYPTION
198 if (this->noise_ != nullptr)
199 return this->noise_write_(data, len);
200#endif
201 return this->writeall_(data, len);
202 }
203 inline bool data_write_byte_(uint8_t byte) { return this->data_write_(&byte, 1); }
204 // When encrypted, buf must have room for len + noise::MAC_SIZE bytes.
205 inline bool data_readall_(uint8_t *buf, size_t len) {
206#ifdef USE_OTA_ENCRYPTION
207 if (this->noise_ != nullptr)
208 return this->noise_readall_(buf, len);
209#endif
210 return this->readall_(buf, len);
211 }
212
213 // Upload accounting shared by the data loop and the inflate read callback
215 size_t ota_size{0}; // bytes the client sends
216 size_t total{0}; // bytes received so far
217#if USE_OTA_VERSION == 2
218 size_t acknowledged{0};
219#endif
222 };
223 // Up to OTA_BUFFER_SIZE bytes into buf; returns bytes read, -1 on failure (logged)
224 ssize_t receive_data_(uint8_t *buf, DataTransfer &xfer);
225 // Raw lwIP cannot service the radio during a sector write, so the ack waits
226 // for the write there; a socket task lets the next block arrive meanwhile
227#ifdef USE_SOCKET_IMPL_LWIP_TCP
228 static constexpr bool ACK_AFTER_WRITE = true;
229#else
230 static constexpr bool ACK_AFTER_WRITE = false;
231#endif
232 void send_chunk_acks_(DataTransfer &xfer);
233 inline void ack_received_(DataTransfer &xfer) {
234 if (!ACK_AFTER_WRITE)
235 this->send_chunk_acks_(xfer);
236 }
237 inline void ack_written_(DataTransfer &xfer) {
238 if (ACK_AFTER_WRITE)
239 this->send_chunk_acks_(xfer);
240 }
241 inline bool read_size_(uint8_t *buf, size_t &size, const LogString *desc);
242 // Writes to the backend and logs a failure
243 ota::OTAResponseTypes write_flash_(uint8_t *data, size_t len);
244
245 bool try_read_(size_t to_read, const LogString *desc);
246 bool try_write_(size_t to_write, const LogString *desc);
247
248 inline bool would_block_(int error_code) const { return error_code == EAGAIN || error_code == EWOULDBLOCK; }
249 bool handle_read_error_(ssize_t read, const LogString *desc);
250 bool handle_write_error_(ssize_t written, const LogString *desc);
251 inline void transition_ota_state_(OTAState next_state) {
252 this->ota_state_ = next_state;
253 this->handshake_buf_pos_ = 0; // Reset buffer position for next state
254 }
255
256 void server_failed_(const LogString *msg);
257 void log_socket_error_(const LogString *msg);
258 void log_read_error_(const LogString *what);
260 void log_start_(const LogString *phase);
261 void log_remote_closed_(const LogString *during);
262 void cleanup_connection_();
264 uint8_t error_byte = static_cast<uint8_t>(error);
265 this->client_->write(&error_byte, 1); // Best effort, non-blocking
266 this->cleanup_connection_();
267 }
269
270#ifdef USE_OTA_PASSWORD
271 std::string password_;
273#endif // USE_OTA_PASSWORD
274#ifdef USE_OTA_ENCRYPTION
276#ifdef USE_OTA_ENCRYPTION_PROVISIONED
277 // Backs noise_ctx_ in safe mode, where no api server holds the saved key
279#endif
281#endif // USE_OTA_ENCRYPTION
282
284 std::unique_ptr<socket::Socket> client_;
286
287#ifdef ESPHOME_OTA_UDP_LWIP
288 union {
289 uint32_t client_connect_time_{0}; // handshake states
290 UdpLink *udp_; // data phase (never both), points into handle_data_'s stack
291 };
292 static_assert(sizeof(UdpLink *) == sizeof(uint32_t), "clearing client_connect_time_ must clear udp_");
293#else
295#ifdef ESPHOME_OTA_UDP
296 UdpLink *udp_{nullptr}; // data phase, points into handle_data_'s stack
297#endif
298#endif
299#if defined(ESPHOME_OTA_UDP) && defined(USE_OTA_ENCRYPTION)
300 static_assert(sizeof(UdpLink::reply) <= NOISE_MAX_RESPONSE, "the token goes out through noise_write_");
301#endif
302 static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
303 // Buffer size for OTA data transfer. The upload client derives its maximum
304 // encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this
305 // minus the 16-byte MAC); both must change together.
306 static constexpr size_t OTA_BUFFER_SIZE = 1040;
307#ifdef USE_OTA_ENCRYPTION
308 // espota2.NOISE_MAX_PLAINTEXT; shrinking the buffer would reject every
309 // frame a current CLI sends
310 static constexpr size_t NOISE_CLIENT_MAX_PLAINTEXT = 1024;
311 static_assert(OTA_BUFFER_SIZE >= NOISE_CLIENT_MAX_PLAINTEXT + noise::MAC_SIZE,
312 "OTA_BUFFER_SIZE must fit a full encrypted data frame");
313#endif
314#ifdef USE_OTA_DEFLATE
315 // At least 1 << espota2.DEFLATE_WINDOW_BITS; also the inflate output buffer
316 static constexpr size_t OTA_INFLATE_WINDOW_SIZE = 4096;
317 // Heap-allocated only while a deflate upload is negotiated; the decoder
318 // state is the base so the read callback can recover the session
320 // The session outlives the upload it serves, but these three are borrowed
321 // from inflate_data_'s caller and dangle once that call returns; only that
322 // call, and the flush and read callback it drives, may read them
325 uint8_t *in; // caller's buffer for the compressed input
327 size_t written; // inflated bytes in flash
328 size_t flushed; // bytes of the current window already in flash
329 ota::OTAResponseTypes error; // first failure inside the read callback
331 };
332#ifndef CLANG_TIDY // static analysis sets every define at once
333 static_assert(!ota::OTABackend::supports_compression(),
334 "USE_OTA_DEFLATE is for backends that cannot store a gzip image");
335#endif
336 // Writes the decoded bytes not yet in flash without moving dest
338 ota::OTAResponseTypes inflate_data_(uint8_t *in, size_t image_size, DataTransfer &xfer);
340#endif
341
342 static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
343 // Derived from the feature byte; storing it would pad the trailing bytes
344 bool extended_proto_() const;
345#ifdef USE_OTA_PARTITIONS
348#endif
349 uint16_t port_;
353 uint8_t ota_features_{0};
354 bool remote_closed_{false}; // the peer hung up cleanly during a blocking read
355#ifdef USE_OTA_PASSWORD
356 uint8_t auth_buf_pos_{0};
357 uint8_t auth_type_{0}; // Store auth type to know which hasher to use
358#endif // USE_OTA_PASSWORD
359};
360
361} // namespace esphome
362#endif
ESPHomeOTAComponent provides a simple way to integrate Over-the-Air updates into your app using Ardui...
Definition ota_esphome.h:40
static constexpr size_t OTA_BUFFER_SIZE
bool handle_noise_handshake_()
Drive the non-blocking handshake from loop(); returns true once the transport ciphers are ready.
ssize_t receive_data_(uint8_t *buf, DataTransfer &xfer)
bool would_block_(int error_code) const
bool noise_write_(const uint8_t *data, size_t len)
Blocking write of a short response (at most NOISE_MAX_RESPONSE bytes) as one encrypted frame.
uint16_t get_port() const
Definition ota_esphome.h:82
static constexpr size_t SHA256_HEX_SIZE
static constexpr uint8_t MAGIC_BYTES[5]
bool udp_send_(const uint8_t *data, size_t len)
ota::OTAResponseTypes write_flash_(uint8_t *data, size_t len)
bool writeall_(const uint8_t *buf, size_t len)
bool try_read_(size_t to_read, const LogString *desc)
static constexpr bool ACK_AFTER_WRITE
static constexpr size_t OTA_INFLATE_WINDOW_SIZE
bool data_readall_(uint8_t *buf, 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)
noise::NoiseContext noise_ctx_
ssize_t transport_read_(uint8_t *buf, size_t len)
void ack_written_(DataTransfer &xfer)
ssize_t noise_decrypt_(uint8_t *buf, size_t len)
Decrypt a ciphertext in place; returns the plaintext size or -1.
bool noise_readall_(uint8_t *buf, size_t len)
Blocking read of one frame whose plaintext must be exactly len bytes (control units are one unit per ...
bool noise_start_session_(uint8_t server_feature_flags)
Start the responder handshake, on the session reserved at offer time.
void set_noise_psk(const uint8_t *psk)
psk points at 32 bytes that live in flash for the life of the program
Definition ota_esphome.h:71
void set_auth_password(const std::string &password)
Definition ota_esphome.h:58
ota::OTABackendPtr backend_
bool try_write_(size_t to_write, const LogString *desc)
bool noise_try_read_frame_()
Non-blocking read of one handshake frame into the session buffer.
static constexpr size_t NOISE_CLIENT_MAX_PLAINTEXT
RAMUniquePtr< NoiseSession > noise_
bool handle_write_error_(ssize_t written, const LogString *desc)
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext)
Blocking read of one frame whose ciphertext size must be within the given bounds, decrypted in place;...
void send_chunk_acks_(DataTransfer &xfer)
bool data_write_byte_(uint8_t byte)
size_t noise_frame_payload_len_(const uint8_t *header, size_t min_len, size_t max_len)
Payload length from a frame header, or 0 (logged) when the indicator or the length is out of range.
void log_auth_warning_(const LogString *msg)
ssize_t udp_read_(uint8_t *buf, size_t len)
RAMUniquePtr< InflateSession > inflate_
float get_setup_priority() const override
void send_error_and_cleanup_(ota::OTAResponseTypes error)
bool handle_read_error_(ssize_t read, const LogString *desc)
ssize_t noise_read_data_(uint8_t *buf, size_t capacity)
Blocking read of one data-phase frame, decrypted in place; returns the plaintext size,...
static constexpr uint8_t UDP_WINDOW
Definition ota_esphome.h:92
void log_read_error_(const LogString *what)
bool readall_(uint8_t *buf, size_t len)
bool data_write_(const uint8_t *data, size_t len)
void set_port(uint16_t port)
Manually set the port OTA should listen on.
Definition ota_esphome.h:75
void set_auth_password(const std::string &)
Definition ota_esphome.h:62
RAMUniquePtr< uint8_t[]> auth_buf_
void noise_send_reject_(const LogString *reason)
Best-effort explicit reject frame so the client can log a readable reason.
RAMUniquePtr< noise::psk_t > saved_psk_
void ack_received_(DataTransfer &xfer)
uint8_t handshake_buf_[HANDSHAKE_BUF_SIZE]
static constexpr size_t HANDSHAKE_BUF_SIZE
ota::OTAResponseTypes inflate_data_(uint8_t *in, size_t image_size, DataTransfer &xfer)
const noise::NoiseContext & noise_context_() const
void server_failed_(const LogString *msg)
void transition_ota_state_(OTAState next_state)
bool noise_try_write_frame_()
Non-blocking write of the pending session-buffer frame.
bool read_size_(uint8_t *buf, size_t &size, const LogString *desc)
static void udp_on_datagram(UdpLink &link, UdpSlot p, uint16_t port)
socket::ListenSocket * server_
void log_remote_closed_(const LogString *during)
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)
static constexpr size_t NOISE_MAX_RESPONSE
void log_start_(const LogString *phase)
ota::OTAResponseTypes inflate_flush_(InflateSession &session)
void log_socket_error_(const LogString *msg)
void set_psk(const uint8_t *psk)
psk points at 32 bytes that outlive the context (PROGMEM or caller owned RAM); nullptr means no key.
Definition noise.h:32
Sans-IO responder side of a Noise_NNpsk0_25519_ChaChaPoly_SHA256 handshake.
uint32_t socklen_t
Definition headers.h:109
__int64 ssize_t
Definition httplib.h:178
decltype(make_ota_backend()) OTABackendPtr
std::unique_ptr< T, RAMDeleter< T > > RAMUniquePtr
unique_ptr over RAMAllocator storage
Definition helpers.h:2287
const void size_t len
Definition hal.h:68
uint16_t size
Definition helpers.cpp:34
int written
Definition helpers.h:1262
uint16_t uint16_t & capacity
Definition helpers.cpp:34
static void uint32_t
uint8_t window[OTA_INFLATE_WINDOW_SIZE]
uint8_t frame_buf[noise::FRAME_HEADER_SIZE+1+noise::MAX_HANDSHAKE_SIZE]
noise::NoiseResponderHandshake handshake