mirror of
https://github.com/esphome/esphome.git
synced 2026-10-18 20:22:49 +00:00
[socket] Share the allow-list reject log and config lines through Ipv4Allow (#20983)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)));
|
||||
|
||||
Reference in New Issue
Block a user