mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Wireguard component (#4256)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Simone Rossetto <simros85@gmail.com> Co-authored-by: Thomas Bernard <thomas0bernard@gmail.com>
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -232,7 +232,7 @@ jobs: | |||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       max-parallel: 2 |       max-parallel: 2 | ||||||
|       matrix: |       matrix: | ||||||
|         file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8] |         file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8, 10] | ||||||
|     steps: |     steps: | ||||||
|       - name: Check out code from GitHub |       - name: Check out code from GitHub | ||||||
|         uses: actions/checkout@v4 |         uses: actions/checkout@v4 | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -13,6 +13,12 @@ __pycache__/ | |||||||
| # Intellij Idea | # Intellij Idea | ||||||
| .idea | .idea | ||||||
|  |  | ||||||
|  | # Eclipse | ||||||
|  | .project | ||||||
|  | .cproject | ||||||
|  | .pydevproject | ||||||
|  | .settings/ | ||||||
|  |  | ||||||
| # Vim | # Vim | ||||||
| *.swp | *.swp | ||||||
|  |  | ||||||
|   | |||||||
| @@ -333,6 +333,7 @@ esphome/components/web_server_idf/* @dentra | |||||||
| esphome/components/whirlpool/* @glmnet | esphome/components/whirlpool/* @glmnet | ||||||
| esphome/components/whynter/* @aeonsablaze | esphome/components/whynter/* @aeonsablaze | ||||||
| esphome/components/wiegand/* @ssieb | esphome/components/wiegand/* @ssieb | ||||||
|  | esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard | ||||||
| esphome/components/wl_134/* @hobbypunk90 | esphome/components/wl_134/* @hobbypunk90 | ||||||
| esphome/components/x9c/* @EtienneMD | esphome/components/x9c/* @EtienneMD | ||||||
| esphome/components/xiaomi_lywsd03mmc/* @ahpohl | esphome/components/xiaomi_lywsd03mmc/* @ahpohl | ||||||
|   | |||||||
							
								
								
									
										113
									
								
								esphome/components/wireguard/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								esphome/components/wireguard/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | |||||||
|  | import re | ||||||
|  | import ipaddress | ||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_TIME_ID, | ||||||
|  |     CONF_ADDRESS, | ||||||
|  |     CONF_REBOOT_TIMEOUT, | ||||||
|  | ) | ||||||
|  | from esphome.components import time | ||||||
|  |  | ||||||
|  | CONF_NETMASK = "netmask" | ||||||
|  | CONF_PRIVATE_KEY = "private_key" | ||||||
|  | CONF_PEER_ENDPOINT = "peer_endpoint" | ||||||
|  | CONF_PEER_PUBLIC_KEY = "peer_public_key" | ||||||
|  | CONF_PEER_PORT = "peer_port" | ||||||
|  | CONF_PEER_PRESHARED_KEY = "peer_preshared_key" | ||||||
|  | CONF_PEER_ALLOWED_IPS = "peer_allowed_ips" | ||||||
|  | CONF_PEER_PERSISTENT_KEEPALIVE = "peer_persistent_keepalive" | ||||||
|  | CONF_REQUIRE_CONNECTION_TO_PROCEED = "require_connection_to_proceed" | ||||||
|  |  | ||||||
|  | DEPENDENCIES = ["time", "esp32"] | ||||||
|  | CODEOWNERS = ["@lhoracek", "@droscy", "@thomas0bernard"] | ||||||
|  |  | ||||||
|  | # The key validation regex has been described by Jason Donenfeld himself | ||||||
|  | # url: https://lists.zx2c4.com/pipermail/wireguard/2020-December/006222.html | ||||||
|  | _WG_KEY_REGEX = re.compile(r"^[A-Za-z0-9+/]{42}[AEIMQUYcgkosw480]=$") | ||||||
|  |  | ||||||
|  | wireguard_ns = cg.esphome_ns.namespace("wireguard") | ||||||
|  | Wireguard = wireguard_ns.class_("Wireguard", cg.Component, cg.PollingComponent) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _wireguard_key(value): | ||||||
|  |     if _WG_KEY_REGEX.match(cv.string(value)) is not None: | ||||||
|  |         return value | ||||||
|  |     raise cv.Invalid(f"Invalid WireGuard key: {value}") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _cidr_network(value): | ||||||
|  |     try: | ||||||
|  |         ipaddress.ip_network(value, strict=False) | ||||||
|  |     except ValueError as err: | ||||||
|  |         raise cv.Invalid(f"Invalid network in CIDR notation: {err}") | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(Wireguard), | ||||||
|  |         cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock), | ||||||
|  |         cv.Required(CONF_ADDRESS): cv.ipv4, | ||||||
|  |         cv.Optional(CONF_NETMASK, default="255.255.255.255"): cv.ipv4, | ||||||
|  |         cv.Required(CONF_PRIVATE_KEY): _wireguard_key, | ||||||
|  |         cv.Required(CONF_PEER_ENDPOINT): cv.string, | ||||||
|  |         cv.Required(CONF_PEER_PUBLIC_KEY): _wireguard_key, | ||||||
|  |         cv.Optional(CONF_PEER_PORT, default=51820): cv.port, | ||||||
|  |         cv.Optional(CONF_PEER_PRESHARED_KEY): _wireguard_key, | ||||||
|  |         cv.Optional(CONF_PEER_ALLOWED_IPS, default=["0.0.0.0/0"]): cv.ensure_list( | ||||||
|  |             _cidr_network | ||||||
|  |         ), | ||||||
|  |         cv.Optional(CONF_PEER_PERSISTENT_KEEPALIVE, default=0): cv.Any( | ||||||
|  |             cv.positive_time_period_seconds, | ||||||
|  |             cv.uint16_t, | ||||||
|  |         ), | ||||||
|  |         cv.Optional( | ||||||
|  |             CONF_REBOOT_TIMEOUT, default="15min" | ||||||
|  |         ): cv.positive_time_period_milliseconds, | ||||||
|  |         cv.Optional(CONF_REQUIRE_CONNECTION_TO_PROCEED, default=False): cv.boolean, | ||||||
|  |     } | ||||||
|  | ).extend(cv.polling_component_schema("10s")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |  | ||||||
|  |     cg.add(var.set_address(str(config[CONF_ADDRESS]))) | ||||||
|  |     cg.add(var.set_netmask(str(config[CONF_NETMASK]))) | ||||||
|  |     cg.add(var.set_private_key(config[CONF_PRIVATE_KEY])) | ||||||
|  |     cg.add(var.set_peer_endpoint(config[CONF_PEER_ENDPOINT])) | ||||||
|  |     cg.add(var.set_peer_public_key(config[CONF_PEER_PUBLIC_KEY])) | ||||||
|  |     cg.add(var.set_peer_port(config[CONF_PEER_PORT])) | ||||||
|  |     cg.add(var.set_keepalive(config[CONF_PEER_PERSISTENT_KEEPALIVE])) | ||||||
|  |     cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) | ||||||
|  |  | ||||||
|  |     if CONF_PEER_PRESHARED_KEY in config: | ||||||
|  |         cg.add(var.set_preshared_key(config[CONF_PEER_PRESHARED_KEY])) | ||||||
|  |  | ||||||
|  |     allowed_ips = list( | ||||||
|  |         ipaddress.collapse_addresses( | ||||||
|  |             [ | ||||||
|  |                 ipaddress.ip_network(ip, strict=False) | ||||||
|  |                 for ip in config[CONF_PEER_ALLOWED_IPS] | ||||||
|  |             ] | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     for ip in allowed_ips: | ||||||
|  |         cg.add(var.add_allowed_ip(str(ip.network_address), str(ip.netmask))) | ||||||
|  |  | ||||||
|  |     cg.add(var.set_srctime(await cg.get_variable(config[CONF_TIME_ID]))) | ||||||
|  |  | ||||||
|  |     if config[CONF_REQUIRE_CONNECTION_TO_PROCEED]: | ||||||
|  |         cg.add(var.disable_auto_proceed()) | ||||||
|  |  | ||||||
|  |     # This flag is added here because the esp_wireguard library statically | ||||||
|  |     # set the size of its allowed_ips list at compile time using this value; | ||||||
|  |     # the '+1' modifier is relative to the device's own address that will | ||||||
|  |     # be automatically added to the provided list. | ||||||
|  |     cg.add_build_flag(f"-DCONFIG_WIREGUARD_MAX_SRC_IPS={len(allowed_ips) + 1}") | ||||||
|  |     cg.add_library("droscy/esp_wireguard", "0.3.2") | ||||||
|  |  | ||||||
|  |     await cg.register_component(var, config) | ||||||
							
								
								
									
										28
									
								
								esphome/components/wireguard/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								esphome/components/wireguard/binary_sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import binary_sensor | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_STATUS, | ||||||
|  |     DEVICE_CLASS_CONNECTIVITY, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | from . import Wireguard | ||||||
|  |  | ||||||
|  | CONF_WIREGUARD_ID = "wireguard_id" | ||||||
|  |  | ||||||
|  | DEPENDENCIES = ["wireguard"] | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = { | ||||||
|  |     cv.GenerateID(CONF_WIREGUARD_ID): cv.use_id(Wireguard), | ||||||
|  |     cv.Optional(CONF_STATUS): binary_sensor.binary_sensor_schema( | ||||||
|  |         device_class=DEVICE_CLASS_CONNECTIVITY, | ||||||
|  |     ), | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     parent = await cg.get_variable(config[CONF_WIREGUARD_ID]) | ||||||
|  |  | ||||||
|  |     if status_config := config.get(CONF_STATUS): | ||||||
|  |         sens = await binary_sensor.new_binary_sensor(status_config) | ||||||
|  |         cg.add(parent.set_status_sensor(sens)) | ||||||
							
								
								
									
										30
									
								
								esphome/components/wireguard/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/wireguard/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import sensor | ||||||
|  | from esphome.const import ( | ||||||
|  |     DEVICE_CLASS_TIMESTAMP, | ||||||
|  |     ENTITY_CATEGORY_DIAGNOSTIC, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | from . import Wireguard | ||||||
|  |  | ||||||
|  | CONF_WIREGUARD_ID = "wireguard_id" | ||||||
|  | CONF_LATEST_HANDSHAKE = "latest_handshake" | ||||||
|  |  | ||||||
|  | DEPENDENCIES = ["wireguard"] | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = { | ||||||
|  |     cv.GenerateID(CONF_WIREGUARD_ID): cv.use_id(Wireguard), | ||||||
|  |     cv.Optional(CONF_LATEST_HANDSHAKE): sensor.sensor_schema( | ||||||
|  |         device_class=DEVICE_CLASS_TIMESTAMP, | ||||||
|  |         entity_category=ENTITY_CATEGORY_DIAGNOSTIC, | ||||||
|  |     ), | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     parent = await cg.get_variable(config[CONF_WIREGUARD_ID]) | ||||||
|  |  | ||||||
|  |     if latest_handshake_config := config.get(CONF_LATEST_HANDSHAKE): | ||||||
|  |         sens = await sensor.new_sensor(latest_handshake_config) | ||||||
|  |         cg.add(parent.set_handshake_sensor(sens)) | ||||||
							
								
								
									
										296
									
								
								esphome/components/wireguard/wireguard.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								esphome/components/wireguard/wireguard.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,296 @@ | |||||||
|  | #include "wireguard.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
|  | #include <ctime> | ||||||
|  | #include <functional> | ||||||
|  |  | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/time.h" | ||||||
|  | #include "esphome/components/network/util.h" | ||||||
|  |  | ||||||
|  | #include <esp_err.h> | ||||||
|  |  | ||||||
|  | #include <esp_wireguard.h> | ||||||
|  |  | ||||||
|  | // includes for resume/suspend wdt | ||||||
|  | #if defined(USE_ESP_IDF) | ||||||
|  | #include <esp_task_wdt.h> | ||||||
|  | #if ESP_IDF_VERSION_MAJOR >= 5 | ||||||
|  | #include <spi_flash_mmap.h> | ||||||
|  | #endif | ||||||
|  | #elif defined(USE_ARDUINO) | ||||||
|  | #include <esp32-hal.h> | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace wireguard { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "wireguard"; | ||||||
|  |  | ||||||
|  | static const char *const LOGMSG_PEER_STATUS = "WireGuard remote peer is %s (latest handshake %s)"; | ||||||
|  | static const char *const LOGMSG_ONLINE = "online"; | ||||||
|  | static const char *const LOGMSG_OFFLINE = "offline"; | ||||||
|  |  | ||||||
|  | void Wireguard::setup() { | ||||||
|  |   ESP_LOGD(TAG, "initializing WireGuard..."); | ||||||
|  |  | ||||||
|  |   this->wg_config_.address = this->address_.c_str(); | ||||||
|  |   this->wg_config_.private_key = this->private_key_.c_str(); | ||||||
|  |   this->wg_config_.endpoint = this->peer_endpoint_.c_str(); | ||||||
|  |   this->wg_config_.public_key = this->peer_public_key_.c_str(); | ||||||
|  |   this->wg_config_.port = this->peer_port_; | ||||||
|  |   this->wg_config_.netmask = this->netmask_.c_str(); | ||||||
|  |   this->wg_config_.persistent_keepalive = this->keepalive_; | ||||||
|  |  | ||||||
|  |   if (this->preshared_key_.length() > 0) | ||||||
|  |     this->wg_config_.preshared_key = this->preshared_key_.c_str(); | ||||||
|  |  | ||||||
|  |   this->wg_initialized_ = esp_wireguard_init(&(this->wg_config_), &(this->wg_ctx_)); | ||||||
|  |  | ||||||
|  |   if (this->wg_initialized_ == ESP_OK) { | ||||||
|  |     ESP_LOGI(TAG, "WireGuard initialized"); | ||||||
|  |     this->wg_peer_offline_time_ = millis(); | ||||||
|  |     this->srctime_->add_on_time_sync_callback(std::bind(&Wireguard::start_connection_, this)); | ||||||
|  |     this->defer(std::bind(&Wireguard::start_connection_, this));  // defer to avoid blocking setup | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGE(TAG, "cannot initialize WireGuard, error code %d", this->wg_initialized_); | ||||||
|  |     this->mark_failed(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Wireguard::loop() { | ||||||
|  |   if ((this->wg_initialized_ == ESP_OK) && (this->wg_connected_ == ESP_OK) && (!network::is_connected())) { | ||||||
|  |     ESP_LOGV(TAG, "local network connection has been lost, stopping WireGuard..."); | ||||||
|  |     this->stop_connection_(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Wireguard::update() { | ||||||
|  |   bool peer_up = this->is_peer_up(); | ||||||
|  |   time_t lhs = this->get_latest_handshake(); | ||||||
|  |   bool lhs_updated = (lhs > this->latest_saved_handshake_); | ||||||
|  |  | ||||||
|  |   ESP_LOGV(TAG, "handshake: latest=%.0f, saved=%.0f, updated=%d", (double) lhs, (double) this->latest_saved_handshake_, | ||||||
|  |            (int) lhs_updated); | ||||||
|  |  | ||||||
|  |   if (lhs_updated) { | ||||||
|  |     this->latest_saved_handshake_ = lhs; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   std::string latest_handshake = | ||||||
|  |       (this->latest_saved_handshake_ > 0) | ||||||
|  |           ? ESPTime::from_epoch_local(this->latest_saved_handshake_).strftime("%Y-%m-%d %H:%M:%S %Z") | ||||||
|  |           : "timestamp not available"; | ||||||
|  |  | ||||||
|  |   if (peer_up) { | ||||||
|  |     if (this->wg_peer_offline_time_ != 0) { | ||||||
|  |       ESP_LOGI(TAG, LOGMSG_PEER_STATUS, LOGMSG_ONLINE, latest_handshake.c_str()); | ||||||
|  |       this->wg_peer_offline_time_ = 0; | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGD(TAG, LOGMSG_PEER_STATUS, LOGMSG_ONLINE, latest_handshake.c_str()); | ||||||
|  |     } | ||||||
|  |   } else { | ||||||
|  |     if (this->wg_peer_offline_time_ == 0) { | ||||||
|  |       ESP_LOGW(TAG, LOGMSG_PEER_STATUS, LOGMSG_OFFLINE, latest_handshake.c_str()); | ||||||
|  |       this->wg_peer_offline_time_ = millis(); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGD(TAG, LOGMSG_PEER_STATUS, LOGMSG_OFFLINE, latest_handshake.c_str()); | ||||||
|  |       this->start_connection_(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // check reboot timeout every time the peer is down | ||||||
|  |     if (this->reboot_timeout_ > 0) { | ||||||
|  |       if (millis() - this->wg_peer_offline_time_ > this->reboot_timeout_) { | ||||||
|  |         ESP_LOGE(TAG, "WireGuard remote peer is unreachable, rebooting..."); | ||||||
|  |         App.reboot(); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  |   if (this->status_sensor_ != nullptr) { | ||||||
|  |     this->status_sensor_->publish_state(peer_up); | ||||||
|  |   } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  |   if (this->handshake_sensor_ != nullptr && lhs_updated) { | ||||||
|  |     this->handshake_sensor_->publish_state((double) this->latest_saved_handshake_); | ||||||
|  |   } | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Wireguard::dump_config() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "WireGuard:"); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Address: %s", this->address_.c_str()); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Netmask: %s", this->netmask_.c_str()); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Private Key: " LOG_SECRET("%s"), mask_key(this->private_key_).c_str()); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Peer Endpoint: " LOG_SECRET("%s"), this->peer_endpoint_.c_str()); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Peer Port: " LOG_SECRET("%d"), this->peer_port_); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Peer Public Key: " LOG_SECRET("%s"), this->peer_public_key_.c_str()); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Peer Pre-shared Key: " LOG_SECRET("%s"), | ||||||
|  |                 (this->preshared_key_.length() > 0 ? mask_key(this->preshared_key_).c_str() : "NOT IN USE")); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Peer Allowed IPs:"); | ||||||
|  |   for (auto &allowed_ip : this->allowed_ips_) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "    - %s/%s", std::get<0>(allowed_ip).c_str(), std::get<1>(allowed_ip).c_str()); | ||||||
|  |   } | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Peer Persistent Keepalive: %d%s", this->keepalive_, | ||||||
|  |                 (this->keepalive_ > 0 ? "s" : " (DISABLED)")); | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Reboot Timeout: %d%s", (this->reboot_timeout_ / 1000), | ||||||
|  |                 (this->reboot_timeout_ != 0 ? "s" : " (DISABLED)")); | ||||||
|  |   // be careful: if proceed_allowed_ is true, require connection is false | ||||||
|  |   ESP_LOGCONFIG(TAG, "  Require Connection to Proceed: %s", (this->proceed_allowed_ ? "NO" : "YES")); | ||||||
|  |   LOG_UPDATE_INTERVAL(this); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Wireguard::on_shutdown() { this->stop_connection_(); } | ||||||
|  |  | ||||||
|  | bool Wireguard::can_proceed() { return (this->proceed_allowed_ || this->is_peer_up()); } | ||||||
|  |  | ||||||
|  | bool Wireguard::is_peer_up() const { | ||||||
|  |   return (this->wg_initialized_ == ESP_OK) && (this->wg_connected_ == ESP_OK) && | ||||||
|  |          (esp_wireguardif_peer_is_up(&(this->wg_ctx_)) == ESP_OK); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | time_t Wireguard::get_latest_handshake() const { | ||||||
|  |   time_t result; | ||||||
|  |   if (esp_wireguard_latest_handshake(&(this->wg_ctx_), &result) != ESP_OK) { | ||||||
|  |     result = 0; | ||||||
|  |   } | ||||||
|  |   return result; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Wireguard::set_address(const std::string &address) { this->address_ = address; } | ||||||
|  | void Wireguard::set_netmask(const std::string &netmask) { this->netmask_ = netmask; } | ||||||
|  | void Wireguard::set_private_key(const std::string &key) { this->private_key_ = key; } | ||||||
|  | void Wireguard::set_peer_endpoint(const std::string &endpoint) { this->peer_endpoint_ = endpoint; } | ||||||
|  | void Wireguard::set_peer_public_key(const std::string &key) { this->peer_public_key_ = key; } | ||||||
|  | void Wireguard::set_peer_port(const uint16_t port) { this->peer_port_ = port; } | ||||||
|  | void Wireguard::set_preshared_key(const std::string &key) { this->preshared_key_ = key; } | ||||||
|  |  | ||||||
|  | void Wireguard::add_allowed_ip(const std::string &ip, const std::string &netmask) { | ||||||
|  |   this->allowed_ips_.emplace_back(ip, netmask); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Wireguard::set_keepalive(const uint16_t seconds) { this->keepalive_ = seconds; } | ||||||
|  | void Wireguard::set_reboot_timeout(const uint32_t seconds) { this->reboot_timeout_ = seconds; } | ||||||
|  | void Wireguard::set_srctime(time::RealTimeClock *srctime) { this->srctime_ = srctime; } | ||||||
|  |  | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  | void Wireguard::set_status_sensor(binary_sensor::BinarySensor *sensor) { this->status_sensor_ = sensor; } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  | void Wireguard::set_handshake_sensor(sensor::Sensor *sensor) { this->handshake_sensor_ = sensor; } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | void Wireguard::disable_auto_proceed() { this->proceed_allowed_ = false; } | ||||||
|  |  | ||||||
|  | void Wireguard::start_connection_() { | ||||||
|  |   if (this->wg_initialized_ != ESP_OK) { | ||||||
|  |     ESP_LOGE(TAG, "cannot start WireGuard, initialization in error with code %d", this->wg_initialized_); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!network::is_connected()) { | ||||||
|  |     ESP_LOGD(TAG, "WireGuard is waiting for local network connection to be available"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (!this->srctime_->now().is_valid()) { | ||||||
|  |     ESP_LOGD(TAG, "WireGuard is waiting for system time to be synchronized"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (this->wg_connected_ == ESP_OK) { | ||||||
|  |     ESP_LOGV(TAG, "WireGuard connection already started"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGD(TAG, "starting WireGuard connection..."); | ||||||
|  |  | ||||||
|  |   /* | ||||||
|  |    * The function esp_wireguard_connect() contains a DNS resolution | ||||||
|  |    * that could trigger the watchdog, so before it we suspend (or | ||||||
|  |    * increase the time, it depends on the platform) the wdt and | ||||||
|  |    * then we resume the normal timeout. | ||||||
|  |    */ | ||||||
|  |   suspend_wdt(); | ||||||
|  |   ESP_LOGV(TAG, "executing esp_wireguard_connect"); | ||||||
|  |   this->wg_connected_ = esp_wireguard_connect(&(this->wg_ctx_)); | ||||||
|  |   resume_wdt(); | ||||||
|  |  | ||||||
|  |   if (this->wg_connected_ == ESP_OK) { | ||||||
|  |     ESP_LOGI(TAG, "WireGuard connection started"); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGW(TAG, "cannot start WireGuard connection, error code %d", this->wg_connected_); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGD(TAG, "configuring WireGuard allowed IPs list..."); | ||||||
|  |   bool allowed_ips_ok = true; | ||||||
|  |   for (std::tuple<std::string, std::string> ip : this->allowed_ips_) { | ||||||
|  |     allowed_ips_ok &= | ||||||
|  |         (esp_wireguard_add_allowed_ip(&(this->wg_ctx_), std::get<0>(ip).c_str(), std::get<1>(ip).c_str()) == ESP_OK); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (allowed_ips_ok) { | ||||||
|  |     ESP_LOGD(TAG, "allowed IPs list configured correctly"); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGE(TAG, "cannot configure WireGuard allowed IPs list, aborting..."); | ||||||
|  |     this->stop_connection_(); | ||||||
|  |     this->mark_failed(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Wireguard::stop_connection_() { | ||||||
|  |   if (this->wg_initialized_ == ESP_OK && this->wg_connected_ == ESP_OK) { | ||||||
|  |     ESP_LOGD(TAG, "stopping WireGuard connection..."); | ||||||
|  |     esp_wireguard_disconnect(&(this->wg_ctx_)); | ||||||
|  |     this->wg_connected_ = ESP_FAIL; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void suspend_wdt() { | ||||||
|  | #if defined(USE_ESP_IDF) | ||||||
|  | #if ESP_IDF_VERSION_MAJOR >= 5 | ||||||
|  |   ESP_LOGV(TAG, "temporarily increasing wdt timeout to 15000 ms"); | ||||||
|  |   esp_task_wdt_config_t wdtc; | ||||||
|  |   wdtc.timeout_ms = 15000; | ||||||
|  |   wdtc.idle_core_mask = 0; | ||||||
|  |   wdtc.trigger_panic = false; | ||||||
|  |   esp_task_wdt_reconfigure(&wdtc); | ||||||
|  | #else | ||||||
|  |   ESP_LOGV(TAG, "temporarily increasing wdt timeout to 15 seconds"); | ||||||
|  |   esp_task_wdt_init(15, false); | ||||||
|  | #endif | ||||||
|  | #elif defined(USE_ARDUINO) | ||||||
|  |   ESP_LOGV(TAG, "temporarily disabling the wdt"); | ||||||
|  |   disableLoopWDT(); | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void resume_wdt() { | ||||||
|  | #if defined(USE_ESP_IDF) | ||||||
|  | #if ESP_IDF_VERSION_MAJOR >= 5 | ||||||
|  |   wdtc.timeout_ms = CONFIG_ESP_TASK_WDT_TIMEOUT_S * 1000; | ||||||
|  |   esp_task_wdt_reconfigure(&wdtc); | ||||||
|  |   ESP_LOGV(TAG, "wdt resumed with %d ms timeout", wdtc.timeout_ms); | ||||||
|  | #else | ||||||
|  |   esp_task_wdt_init(CONFIG_ESP_TASK_WDT_TIMEOUT_S, false); | ||||||
|  |   ESP_LOGV(TAG, "wdt resumed with %d seconds timeout", CONFIG_ESP_TASK_WDT_TIMEOUT_S); | ||||||
|  | #endif | ||||||
|  | #elif defined(USE_ARDUINO) | ||||||
|  |   enableLoopWDT(); | ||||||
|  |   ESP_LOGV(TAG, "wdt resumed"); | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
|  | std::string mask_key(const std::string &key) { return (key.substr(0, 5) + "[...]="); } | ||||||
|  |  | ||||||
|  | }  // namespace wireguard | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_ESP32 | ||||||
							
								
								
									
										122
									
								
								esphome/components/wireguard/wireguard.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								esphome/components/wireguard/wireguard.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,122 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
|  | #include <ctime> | ||||||
|  | #include <vector> | ||||||
|  | #include <tuple> | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/components/time/real_time_clock.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  | #include "esphome/components/binary_sensor/binary_sensor.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #include <esp_wireguard.h> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace wireguard { | ||||||
|  |  | ||||||
|  | class Wireguard : public PollingComponent { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   void loop() override; | ||||||
|  |   void update() override; | ||||||
|  |   void dump_config() override; | ||||||
|  |   void on_shutdown() override; | ||||||
|  |   bool can_proceed() override; | ||||||
|  |  | ||||||
|  |   float get_setup_priority() const override { return esphome::setup_priority::BEFORE_CONNECTION; } | ||||||
|  |  | ||||||
|  |   void set_address(const std::string &address); | ||||||
|  |   void set_netmask(const std::string &netmask); | ||||||
|  |   void set_private_key(const std::string &key); | ||||||
|  |   void set_peer_endpoint(const std::string &endpoint); | ||||||
|  |   void set_peer_public_key(const std::string &key); | ||||||
|  |   void set_peer_port(uint16_t port); | ||||||
|  |   void set_preshared_key(const std::string &key); | ||||||
|  |  | ||||||
|  |   void add_allowed_ip(const std::string &ip, const std::string &netmask); | ||||||
|  |  | ||||||
|  |   void set_keepalive(uint16_t seconds); | ||||||
|  |   void set_reboot_timeout(uint32_t seconds); | ||||||
|  |   void set_srctime(time::RealTimeClock *srctime); | ||||||
|  |  | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  |   void set_status_sensor(binary_sensor::BinarySensor *sensor); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  |   void set_handshake_sensor(sensor::Sensor *sensor); | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   /// Block the setup step until peer is connected. | ||||||
|  |   void disable_auto_proceed(); | ||||||
|  |  | ||||||
|  |   bool is_peer_up() const; | ||||||
|  |   time_t get_latest_handshake() const; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   std::string address_; | ||||||
|  |   std::string netmask_; | ||||||
|  |   std::string private_key_; | ||||||
|  |   std::string peer_endpoint_; | ||||||
|  |   std::string peer_public_key_; | ||||||
|  |   std::string preshared_key_; | ||||||
|  |  | ||||||
|  |   std::vector<std::tuple<std::string, std::string>> allowed_ips_; | ||||||
|  |  | ||||||
|  |   uint16_t peer_port_; | ||||||
|  |   uint16_t keepalive_; | ||||||
|  |   uint32_t reboot_timeout_; | ||||||
|  |  | ||||||
|  |   time::RealTimeClock *srctime_; | ||||||
|  |  | ||||||
|  | #ifdef USE_BINARY_SENSOR | ||||||
|  |   binary_sensor::BinarySensor *status_sensor_ = nullptr; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  |   sensor::Sensor *handshake_sensor_ = nullptr; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |   /// Set to false to block the setup step until peer is connected. | ||||||
|  |   bool proceed_allowed_ = true; | ||||||
|  |  | ||||||
|  |   wireguard_config_t wg_config_ = ESP_WIREGUARD_CONFIG_DEFAULT(); | ||||||
|  |   wireguard_ctx_t wg_ctx_ = ESP_WIREGUARD_CONTEXT_DEFAULT(); | ||||||
|  |  | ||||||
|  |   esp_err_t wg_initialized_ = ESP_FAIL; | ||||||
|  |   esp_err_t wg_connected_ = ESP_FAIL; | ||||||
|  |  | ||||||
|  |   /// The last time the remote peer become offline. | ||||||
|  |   uint32_t wg_peer_offline_time_ = 0; | ||||||
|  |  | ||||||
|  |   /** \brief The latest saved handshake. | ||||||
|  |    * | ||||||
|  |    * This is used to save (and log) the latest completed handshake even | ||||||
|  |    * after a full refresh of the wireguard keys (for example after a | ||||||
|  |    * stop/start connection cycle). | ||||||
|  |    */ | ||||||
|  |   time_t latest_saved_handshake_ = 0; | ||||||
|  |  | ||||||
|  |   void start_connection_(); | ||||||
|  |   void stop_connection_(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | // These are used for possibly long DNS resolution to temporarily suspend the watchdog | ||||||
|  | void suspend_wdt(); | ||||||
|  | void resume_wdt(); | ||||||
|  |  | ||||||
|  | /// Strip most part of the key only for secure printing | ||||||
|  | std::string mask_key(const std::string &key); | ||||||
|  |  | ||||||
|  | }  // namespace wireguard | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_ESP32 | ||||||
| @@ -123,6 +123,7 @@ lib_deps = | |||||||
|     DNSServer                            ; captive_portal (Arduino built-in) |     DNSServer                            ; captive_portal (Arduino built-in) | ||||||
|     esphome/ESP32-audioI2S@2.0.7         ; i2s_audio |     esphome/ESP32-audioI2S@2.0.7         ; i2s_audio | ||||||
|     crankyoldgit/IRremoteESP8266@2.7.12  ; heatpumpir |     crankyoldgit/IRremoteESP8266@2.7.12  ; heatpumpir | ||||||
|  |     droscy/esp_wireguard@0.3.2           ; wireguard | ||||||
| build_flags = | build_flags = | ||||||
|     ${common:arduino.build_flags} |     ${common:arduino.build_flags} | ||||||
|     -DUSE_ESP32 |     -DUSE_ESP32 | ||||||
| @@ -141,6 +142,7 @@ framework = espidf | |||||||
| lib_deps = | lib_deps = | ||||||
|     ${common:idf.lib_deps} |     ${common:idf.lib_deps} | ||||||
|     espressif/esp32-camera@1.0.0  ; esp32_camera |     espressif/esp32-camera@1.0.0  ; esp32_camera | ||||||
|  |     droscy/esp_wireguard@0.3.2    ; wireguard | ||||||
| build_flags = | build_flags = | ||||||
|     ${common:idf.build_flags} |     ${common:idf.build_flags} | ||||||
|     -Wno-nonnull-compare |     -Wno-nonnull-compare | ||||||
|   | |||||||
| @@ -27,3 +27,4 @@ Current test_.yaml file contents. | |||||||
| | test6.yaml | RP2040 | wifi | N/A | | test6.yaml | RP2040 | wifi | N/A | ||||||
| | test7.yaml | ESP32-C3 | wifi | N/A | | test7.yaml | ESP32-C3 | wifi | N/A | ||||||
| | test8.yaml | ESP32-S3 | wifi | None | | test8.yaml | ESP32-S3 | wifi | None | ||||||
|  | | test10.yaml | ESP32 | wifi | None | ||||||
|   | |||||||
							
								
								
									
										48
									
								
								tests/test10.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								tests/test10.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | --- | ||||||
|  | esphome: | ||||||
|  |   name: test10 | ||||||
|  |   build_path: build/test10 | ||||||
|  |  | ||||||
|  | esp32: | ||||||
|  |   board: esp32doit-devkit-v1 | ||||||
|  |   framework: | ||||||
|  |     type: arduino | ||||||
|  |  | ||||||
|  | wifi: | ||||||
|  |   ssid: "MySSID1" | ||||||
|  |   password: "password1" | ||||||
|  |   reboot_timeout: 3min | ||||||
|  |   power_save_mode: high | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   reboot_timeout: 10min | ||||||
|  |  | ||||||
|  | time: | ||||||
|  |   - platform: sntp | ||||||
|  |  | ||||||
|  | wireguard: | ||||||
|  |   id: vpn | ||||||
|  |   address: 172.16.34.100 | ||||||
|  |   netmask: 255.255.255.0 | ||||||
|  |   # NEVER use the following keys for your vpn, they are now public! | ||||||
|  |   private_key: wPBMxtNYH3mChicrbpsRpZIasIdPq3yZuthn23FbGG8= | ||||||
|  |   peer_public_key: Hs2JfikvYU03/Kv3YoAs1hrUIPPTEkpsZKSPUljE9yc= | ||||||
|  |   peer_preshared_key: 20fjM5GRnSolGPC5SRj9ljgIUyQfruv0B0bvLl3Yt60= | ||||||
|  |   peer_endpoint: wg.server.example | ||||||
|  |   peer_persistent_keepalive: 25s | ||||||
|  |   peer_allowed_ips: | ||||||
|  |     - 172.16.34.0/24 | ||||||
|  |     - 192.168.4.0/24 | ||||||
|  |  | ||||||
|  | binary_sensor: | ||||||
|  |   - platform: wireguard | ||||||
|  |     status: | ||||||
|  |       name: 'WireGuard Status' | ||||||
|  |  | ||||||
|  | sensor: | ||||||
|  |   - platform: wireguard | ||||||
|  |     latest_handshake: | ||||||
|  |       name: 'WireGuard Latest Handshake' | ||||||
		Reference in New Issue
	
	Block a user