ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
api_outgoing_connection.cpp
Go to the documentation of this file.
2#if defined(USE_API) && defined(USE_API_OUTGOING_CONNECTION)
3
4#include "api_connection.h"
5#include "api_server.h"
9#include "esphome/core/log.h"
10
11#include <cerrno>
12#include <cinttypes>
13#include <cstdint>
14#include <cstring>
15
16namespace esphome::api {
17
18ESPHOME_LOG_TAG(TAG, "api.outgoing");
19
20#ifndef API_OUTGOING_CONNECTION_HOST
21static constexpr uint32_t OUTGOING_TARGET_PREF_HASH = 629847102UL;
22#endif
23
24#ifndef API_OUTGOING_CONNECTION_HOST
25// Read the connection's peer address into target; false when unavailable or
26// of a family this build cannot dial
27static bool peer_to_target(APIConnection *conn, SavedOutgoingTarget &target) {
28 // Zeroed because the raw lwIP getpeername() leaves sin6_scope_id untouched
29 struct sockaddr_storage peer = {};
30 socklen_t peer_len = sizeof(peer);
31 if (conn->getpeername((struct sockaddr *) &peer, &peer_len) != 0) {
32 return false;
33 }
34 const sa_family_t family = ((struct sockaddr *) &peer)->sa_family;
35#if USE_NETWORK_IPV6
36 if (family == AF_INET6) {
37 const auto *addr6 = reinterpret_cast<const struct sockaddr_in6 *>(&peer);
38 const auto *bytes = reinterpret_cast<const uint8_t *>(&addr6->sin6_addr);
39 uint32_t prefix[3];
40 memcpy(prefix, bytes, sizeof(prefix));
41 // A dual-stack listener reports an IPv4 peer as ::ffff:a.b.c.d
42 if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == htonl(0xFFFFUL)) {
43 target.family = AF_INET;
44 memcpy(target.addr, bytes + sizeof(prefix), sizeof(struct in_addr));
45 return true;
46 }
47 // A link-local target is only reachable through the interface it came in
48 // on. Device platforms number interfaces from one; a host build can hand
49 // out an index too large to store, and a truncated one dials the wrong
50 // interface, so that target is not remembered at all.
51 if (addr6->sin6_scope_id > UINT8_MAX) {
52 return false;
53 }
54 target.family = AF_INET6;
55 memcpy(target.addr, bytes, sizeof(target.addr));
56 target.scope_id = static_cast<uint8_t>(addr6->sin6_scope_id);
57 return true;
58 }
59#endif
60 if (family != AF_INET) {
61 return false;
62 }
63 const auto *addr4 = reinterpret_cast<const struct sockaddr_in *>(&peer);
64 target.family = AF_INET;
65 memcpy(target.addr, &addr4->sin_addr, sizeof(addr4->sin_addr));
66 return true;
67}
68#endif
69
71#ifdef API_OUTGOING_CONNECTION_HOST
72 // Validation only lets through a literal both inet_pton and inet6_aton
73 // accept, so this cannot fail
74 return socket::set_sockaddr((struct sockaddr *) addr, sizeof(*addr), API_OUTGOING_CONNECTION_HOST,
75 API_OUTGOING_CONNECTION_PORT);
76#else
77#if USE_NETWORK_IPV6
78 if (this->saved_.family == AF_INET6) {
79 auto *addr6 = reinterpret_cast<struct sockaddr_in6 *>(addr);
80 memset(addr6, 0, sizeof(*addr6));
81 addr6->sin6_family = AF_INET6;
82 addr6->sin6_port = htons(API_OUTGOING_CONNECTION_PORT);
83 memcpy(&addr6->sin6_addr, this->saved_.addr, sizeof(this->saved_.addr));
84 addr6->sin6_scope_id = this->saved_.scope_id;
85 return sizeof(*addr6);
86 }
87#endif
88 if (this->saved_.family != AF_INET) {
89 return 0;
90 }
91 auto *addr4 = reinterpret_cast<struct sockaddr_in *>(addr);
92 memset(addr4, 0, sizeof(*addr4));
93 addr4->sin_family = AF_INET;
94 addr4->sin_port = htons(API_OUTGOING_CONNECTION_PORT);
95 memcpy(&addr4->sin_addr, this->saved_.addr, sizeof(addr4->sin_addr));
96 return sizeof(*addr4);
97#endif
98}
99
100#ifndef API_OUTGOING_CONNECTION_HOST
101void OutgoingConnectionManager::format_target_(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
102 struct sockaddr_storage addr;
103 socklen_t addr_len = this->target_sockaddr_(&addr);
104 if (addr_len == 0) {
105 buf[0] = '\0';
106 return;
107 }
108 // Clears buf itself if it cannot format the address
109 socket::format_sockaddr_to((struct sockaddr *) &addr, addr_len, buf);
110}
111#endif
112
114#ifndef API_OUTGOING_CONNECTION_HOST
115 this->target_pref_ = global_preferences->make_preference<SavedOutgoingTarget>(OUTGOING_TARGET_PREF_HASH, true);
116 struct sockaddr_storage addr;
117 // dump_config() prints whichever target this leaves in place
118 if (this->target_pref_.load(&this->saved_) && this->target_sockaddr_(&addr) != 0) {
119 this->host_persisted_ = true;
120 } else {
121 // Never saved, failed its size or CRC check, or holds an unknown family
122 this->saved_ = {};
123 }
124#endif
125}
126
128 if (server->has_outgoing_target_client_()) {
129 return; // on_target_client() already reset the dial state
130 }
131 if (this->dialed_conn_ != nullptr) {
132 // A live dialed session (flagged or not, e.g. a host: peer) is the
133 // target; a silent one dies on the handshake timeout
134 return;
135 }
137 switch (this->state_) {
139 // Target went away; give it the configured delay to reconnect first
140 this->schedule_wait_(now, IDLE_WAIT_MS);
141 break;
143 if (now - this->state_ts_ >= this->wait_) {
144 this->try_dial_(server, now);
145 }
146 break;
148 this->poll_connect_(server, now);
149 break;
150 }
151}
152
154 if (!network::is_connected()) {
155 // Flips within seconds of boot; recheck fast so a deep sleep wake
156 // window is not spent waiting
158 return;
159 }
160 struct sockaddr_storage addr;
161 socklen_t addr_len = this->target_sockaddr_(&addr);
162 const bool at_limit = server->at_client_limit_();
163 // No target is the steady state until a dial-back client has ever connected
164 if (addr_len == 0 || at_limit || !server->noise_ctx_.has_psk()) {
165 // Repeats for as long as the reason holds, so keep it out of debug logs
166 ESP_LOGV(TAG, "Not dialing: %s",
167 addr_len == 0 ? LOG_STR_LITERAL("no target")
168 : (at_limit ? LOG_STR_LITERAL("max connections") : LOG_STR_LITERAL("no key")));
169 // Not a dial failure; retry without escalating the backoff
171 return;
172 }
173 this->dial_socket_ = socket::socket_loop_monitored(((struct sockaddr *) &addr)->sa_family, SOCK_STREAM, IPPROTO_TCP);
174 if (!this->dial_socket_ || this->dial_socket_->setblocking(false) != 0) {
175 ESP_LOGW(TAG, "Socket %s failed: errno %d",
176 this->dial_socket_ ? LOG_STR_LITERAL("setblocking") : LOG_STR_LITERAL("create"), errno);
177 this->schedule_retry_(now);
178 return;
179 }
180#ifdef API_OUTGOING_CONNECTION_HOST
181 ESP_LOGD(TAG, "Dialing " API_OUTGOING_CONNECTION_HOST ":%u", API_OUTGOING_CONNECTION_PORT);
182#else
183 char host[socket::SOCKADDR_STR_LEN];
184 socket::format_sockaddr_to((struct sockaddr *) &addr, addr_len, host);
185 ESP_LOGD(TAG, "Dialing %s:%u", host, API_OUTGOING_CONNECTION_PORT);
186#endif
187 int err = this->dial_socket_->connect((struct sockaddr *) &addr, addr_len);
188 if (err == 0) {
189 // Immediate success (possible for localhost)
190 this->handoff_(server, now);
191 return;
192 }
193 if (errno != EINPROGRESS) {
194 ESP_LOGW(TAG, "Connect failed: %d", errno);
195 this->schedule_retry_(now);
196 return;
197 }
199 this->state_ts_ = now;
200 this->last_poll_ = now;
201}
202
204 if (now - this->state_ts_ >= CONNECT_TIMEOUT_MS) {
205 ESP_LOGW(TAG, "Connect timeout");
206 this->schedule_retry_(now);
207 return;
208 }
209 if (now - this->last_poll_ < CONNECT_POLL_INTERVAL_MS) {
210 return;
211 }
212 this->last_poll_ = now;
213 int err = 0;
214 switch (socket::poll_connect(*this->dial_socket_, err)) {
216 break;
218 this->handoff_(server, now);
219 break;
221 ESP_LOGW(TAG, "Connect failed: %d", err);
222 this->schedule_retry_(now);
223 break;
224 }
225}
226
228 this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_));
229 if (this->dialed_conn_ == nullptr) {
230 // Only preconditions (slot limit, key cleared) refuse the handoff; the
231 // peer is reachable, so do not escalate the backoff
233 return;
234 }
235 // Connected; dialed_conn_ gates further dialing until the session settles
237}
238
240 this->dial_socket_.reset(); // no-op when the socket was handed off
242 this->state_ts_ = now;
243 this->wait_ = wait;
244}
245
247 // +/-20% jitter so a fleet of devices does not retry one server in lockstep
248 const uint32_t jitter_span = this->backoff_ / 5;
249 this->schedule_wait_(now, this->backoff_ - jitter_span + (random_uint32() % (2 * jitter_span + 1)));
250 this->backoff_ = std::min(this->backoff_ * 2, BACKOFF_MAX_MS);
251}
252
254 if (conn != this->dialed_conn_) {
255 return;
256 }
257 this->dialed_conn_ = nullptr;
258 if (was_authenticated) {
259 // A working peer (e.g. a host: target that never sends the flag)
260 // disconnected normally; state is IDLE, so loop() applies the delay
261 this->backoff_ = BACKOFF_MIN_MS;
262 } else {
264 }
265}
266
268 // The target is connected; stop any dial in flight and reset the backoff.
269 // A dialed connection stays tracked unless it is this one: an inbound
270 // target must not orphan a still-open dial.
271 this->dial_socket_.reset();
272 if (conn == this->dialed_conn_) {
273 this->dialed_conn_ = nullptr;
274 }
276 this->backoff_ = BACKOFF_MIN_MS;
277#ifndef API_OUTGOING_CONNECTION_HOST
278 SavedOutgoingTarget target{};
279 if (!peer_to_target(conn, target)) {
280 ESP_LOGW(TAG, "Not remembering this target; its address cannot be dialed");
281 return;
282 }
283 if (this->host_persisted_ && memcmp(&target, &this->saved_, sizeof(target)) == 0) {
284 return; // unchanged and already on flash; avoid flash wear
285 }
286 // Use the fresh address this boot even if the flash write fails; a failed
287 // write is retried on the next flagged hello via host_persisted_
288 this->saved_ = target;
289 if (!this->persist_target_()) {
290 ESP_LOGW(TAG, "Failed to save target");
291 return;
292 }
293 char host[socket::SOCKADDR_STR_LEN];
294 this->format_target_(host);
295 ESP_LOGD(TAG, "Remembered %s as the dial target", host);
296#endif
297}
298
300 // The boot delay differs from delay: on deep sleep builds, so print the
301 // value that actually applies
302 ESP_LOGCONFIG(TAG,
303 " Outgoing connection port: %u\n"
304 " Outgoing connection boot delay: %" PRIu32 "ms",
305 API_OUTGOING_CONNECTION_PORT, BOOT_WAIT_MS);
306 // Both forms keep their text out of RAM on ESP8266: in the format string,
307 // or through LOG_STR_LITERAL
308#ifdef API_OUTGOING_CONNECTION_HOST
309 ESP_LOGCONFIG(TAG, " Outgoing connection host: " API_OUTGOING_CONNECTION_HOST);
310#else
311 char buf[socket::SOCKADDR_STR_LEN];
312 this->format_target_(buf);
313 ESP_LOGCONFIG(TAG, " Outgoing connection host: %s", buf[0] == '\0' ? LOG_STR_LITERAL("none remembered yet") : buf);
314#endif
315}
316
317} // namespace esphome::api
318#endif // USE_API && USE_API_OUTGOING_CONNECTION
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
int getpeername(struct sockaddr *addr, socklen_t *addrlen) const
Get the peer address itself, for remembering a dial-back target.
bool at_client_limit_() const
Definition api_server.h:282
bool has_outgoing_target_client_() const
Definition api_server.h:286
APIConnection * add_outgoing_client_(std::unique_ptr< socket::Socket > sock)
noise::NoiseContext noise_ctx_
Definition api_server.h:397
void poll_connect_(APIServer *server, uint32_t now)
void handoff_(APIServer *server, uint32_t now)
socklen_t target_sockaddr_(struct sockaddr_storage *addr) const
Fill addr with the target and return its length, or 0 when there is none.
void format_target_(std::span< char, socket::SOCKADDR_STR_LEN > buf) const
Format the remembered target for a log line; empty when there is none.
void on_client_removed(APIConnection *conn, bool was_authenticated)
Clears the dialed-connection gate; dying unauthenticated escalates the backoff.
void schedule_wait_(uint32_t now, uint32_t wait)
void on_target_client(APIConnection *conn)
A key-verified client declared itself a dial-back target; last one wins.
std::unique_ptr< socket::Socket > dial_socket_
void try_dial_(APIServer *server, uint32_t now)
uint16_t addr_len
uint32_t socklen_t
Definition headers.h:109
uint8_t sa_family_t
Definition headers.h:69
ESPHOME_LOG_TAG(TAG, "api.connection")
ESPHOME_ALWAYS_INLINE bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition util.h:36
ConnectPollResult poll_connect(Socket &sock, int &err_out)
Poll a connect() that returned EINPROGRESS.
Definition socket.h:223
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port)
Set a sockaddr to the specified address and port for the IP version used by socket_ip().
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span< char, SOCKADDR_STR_LEN > buf)
Format sockaddr into caller-provided buffer, returns length written (excluding null)
std::unique_ptr< Socket > socket_loop_monitored(int domain, int type, int protocol)
Create a socket and monitor it for data in the main loop.
ESPPreferences * global_preferences
uint32_t random_uint32()
Return a random 32-bit unsigned integer.
Definition helpers.cpp:12
Application App
Global storage of Application pointer - only one Application can exist.
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24