[socket] Share the allow-list reject log and config lines through Ipv4Allow (#20983)

This commit is contained in:
J. Nick Koston
2026-10-17 06:51:29 -10:00
committed by GitHub
parent d16d8c111e
commit 635df9c6ad
6 changed files with 87 additions and 22 deletions
+1
View File
@@ -274,6 +274,7 @@ FILTER_SOURCE_FILES = filter_source_files_from_defines(
"lwip_raw_udp_impl.cpp": "USE_SOCKET_IMPL_LWIP_TCP",
"bsd_sockets_impl.cpp": "USE_SOCKET_IMPL_BSD_SOCKETS",
"lwip_sockets_impl.cpp": "USE_SOCKET_IMPL_LWIP_SOCKETS",
"ipv4_allow.cpp": "USE_SOCKET_IPV4_ALLOW",
"ipv4_resolve.cpp": "USE_SOCKET_IPV4_RESOLVE",
"tcp_client_link.cpp": "USE_SOCKET_TCP_CLIENT_LINK",
"tcp_listener.cpp": "USE_SOCKET_TCP_LISTENER",
+36
View File
@@ -0,0 +1,36 @@
#include "ipv4_allow.h"
#ifdef USE_SOCKET_IPV4_ALLOW
#include "esphome/core/log.h"
#include <span>
namespace esphome::socket {
bool Ipv4Allow::admit(const struct sockaddr *peer, socklen_t len, const char *tag, uint32_t now) {
if (this->allows(peer)) {
return true;
}
if (this->last_reject_log_ms_ == 0 || now - this->last_reject_log_ms_ >= REJECT_LOG_INTERVAL_MS) {
this->last_reject_log_ms_ = now;
char text[SOCKADDR_STR_LEN];
format_sockaddr_to(peer, len, std::span<char, SOCKADDR_STR_LEN>(text));
ESP_LOGW(tag, "Rejected %s", text);
}
return false;
}
void Ipv4Allow::dump_config(const char *tag) const {
for (size_t i = 0; i < this->count_; i++) {
const Ipv4AllowEntry e = this->entry(i);
// Network order is dotted order, and the contiguous mask's popcount is the prefix.
const auto *b = reinterpret_cast<const uint8_t *>(&e.addr);
ESP_LOGCONFIG(tag, " Allowed IP: %u.%u.%u.%u/%u", b[0], b[1], b[2], b[3],
static_cast<unsigned>(__builtin_popcount(e.mask)));
}
}
} // namespace esphome::socket
#endif
+8 -1
View File
@@ -47,6 +47,10 @@ class Ipv4Allow {
return false;
}
/// allows(peer), logging a refused peer at most once every REJECT_LOG_INTERVAL_MS.
bool admit(const struct sockaddr *peer, socklen_t len, const char *tag, uint32_t now);
/// One config line per allowed network.
void dump_config(const char *tag) const;
size_t size() const { return this->count_; }
/// A copy of entry i, read from flash.
Ipv4AllowEntry entry(size_t i) const {
@@ -55,9 +59,12 @@ class Ipv4Allow {
return e;
}
private:
protected:
static constexpr uint32_t REJECT_LOG_INTERVAL_MS = 5000;
const Ipv4AllowEntry *entries_{nullptr};
size_t count_{0};
uint32_t last_reject_log_ms_{0};
};
} // namespace esphome::socket
+4 -18
View File
@@ -12,9 +12,6 @@ namespace esphome::socket {
// One client at a time; a second connection waits in the stack until the first drops.
static constexpr int LISTEN_BACKLOG = 1;
#ifdef USE_SOCKET_IPV4_ALLOW
static constexpr uint32_t REJECT_LOG_INTERVAL_MS = 5000;
#endif
void TcpListener::try_listen_(TcpClientLink &link) {
this->listen_ = socket_ip_loop_monitored(SOCK_STREAM, IPPROTO_TCP);
@@ -57,31 +54,20 @@ void TcpListener::accept_(TcpClientLink &link) {
return;
}
const auto *sa = reinterpret_cast<const struct sockaddr *>(&peer);
char text[SOCKADDR_STR_LEN];
format_sockaddr_to(sa, peer_len, std::span<char, SOCKADDR_STR_LEN>(text));
#ifdef USE_SOCKET_IPV4_ALLOW
if (!this->allow_.allows(sa)) {
uint32_t now = App.get_loop_component_start_time();
if (this->last_reject_log_ms_ == 0 || now - this->last_reject_log_ms_ >= REJECT_LOG_INTERVAL_MS) {
this->last_reject_log_ms_ = now;
ESP_LOGW(this->tag_, "Rejected %s", text);
}
if (!this->allow_.admit(sa, peer_len, this->tag_, App.get_loop_component_start_time())) {
return;
}
#endif
char text[SOCKADDR_STR_LEN];
format_sockaddr_to(sa, peer_len, std::span<char, SOCKADDR_STR_LEN>(text));
link.adopt(std::move(client));
ESP_LOGI(this->tag_, "Client connected from %s", text);
}
void TcpListener::dump_config() const {
#ifdef USE_SOCKET_IPV4_ALLOW
for (size_t i = 0; i < this->allow_.size(); i++) {
Ipv4AllowEntry e = this->allow_.entry(i);
// Network order is dotted order, and the contiguous mask's popcount is the prefix.
const auto *b = reinterpret_cast<const uint8_t *>(&e.addr);
ESP_LOGCONFIG(this->tag_, " Allowed IP: %u.%u.%u.%u/%u", b[0], b[1], b[2], b[3],
static_cast<unsigned>(__builtin_popcount(e.mask)));
}
this->allow_.dump_config(this->tag_);
#endif
}
-1
View File
@@ -50,7 +50,6 @@ class TcpListener {
std::unique_ptr<ListenSocket> listen_;
const char *tag_{nullptr};
#ifdef USE_SOCKET_IPV4_ALLOW
uint32_t last_reject_log_ms_{0};
Ipv4Allow allow_;
#endif
};
@@ -9,8 +9,8 @@
namespace esphome::socket::testing {
// The size_t count packs into the pointer's padding; no RAM over a uint8_t.
static_assert(sizeof(Ipv4Allow) == 2 * sizeof(void *), "unexpected padding in Ipv4Allow");
// Entries pointer, count and the reject-log timestamp; nothing else may creep in.
static_assert(sizeof(Ipv4Allow) <= 3 * sizeof(void *), "unexpected padding in Ipv4Allow");
// 192.168.175.20/32 and 192.168.175.0/24, network order, host bits cleared,
// mirroring what add_ipv4_allow emits.
@@ -26,6 +26,42 @@ static bool allows_peer(const Ipv4Allow &list, const char *ip) {
return list.allows(reinterpret_cast<const struct sockaddr *>(&peer));
}
// Exposes the reject-log timestamp, the only trace the throttle leaves besides the log line.
struct ThrottledAllow : public Ipv4Allow {
using Ipv4Allow::last_reject_log_ms_;
};
// admit() with the same production parser.
static bool admits_peer(Ipv4Allow &list, const char *ip, uint32_t now) {
struct sockaddr_storage peer {};
socklen_t len = set_sockaddr(reinterpret_cast<struct sockaddr *>(&peer), sizeof(peer), ip, 0);
EXPECT_NE(len, 0);
return list.admit(reinterpret_cast<const struct sockaddr *>(&peer), len, "test", now);
}
TEST(Ipv4Allow, AdmitLogsARejectAtMostOncePerInterval) {
ThrottledAllow list;
list.set(ENTRIES, std::size(ENTRIES));
EXPECT_TRUE(admits_peer(list, "192.168.175.66", 1000));
EXPECT_TRUE(admits_peer(list, "::ffff:192.168.175.20", 1000));
EXPECT_EQ(list.last_reject_log_ms_, 0u);
EXPECT_FALSE(admits_peer(list, "10.0.0.1", 1000));
EXPECT_EQ(list.last_reject_log_ms_, 1000u);
// Inside the interval the peer is still refused, but the log line is skipped.
EXPECT_FALSE(admits_peer(list, "10.0.0.2", 1001));
EXPECT_EQ(list.last_reject_log_ms_, 1000u);
EXPECT_FALSE(admits_peer(list, "10.0.0.3", 5999));
EXPECT_EQ(list.last_reject_log_ms_, 1000u);
// Exactly one interval later it logs again.
EXPECT_FALSE(admits_peer(list, "10.0.0.4", 6000));
EXPECT_EQ(list.last_reject_log_ms_, 6000u);
// An admitted peer leaves the throttle alone.
EXPECT_TRUE(admits_peer(list, "192.168.175.1", 6001));
EXPECT_EQ(list.last_reject_log_ms_, 6000u);
list.dump_config("test");
}
TEST(Ipv4Allow, EmptyAllowsEveryPeer) {
Ipv4Allow list;
EXPECT_TRUE(list.allows(htonl(0xC0A8AF01)));