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 | ||||
|       max-parallel: 2 | ||||
|       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: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v4 | ||||
|   | ||||
							
								
								
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -13,6 +13,12 @@ __pycache__/ | ||||
| # Intellij Idea | ||||
| .idea | ||||
|  | ||||
| # Eclipse | ||||
| .project | ||||
| .cproject | ||||
| .pydevproject | ||||
| .settings/ | ||||
|  | ||||
| # Vim | ||||
| *.swp | ||||
|  | ||||
|   | ||||
| @@ -333,6 +333,7 @@ esphome/components/web_server_idf/* @dentra | ||||
| esphome/components/whirlpool/* @glmnet | ||||
| esphome/components/whynter/* @aeonsablaze | ||||
| esphome/components/wiegand/* @ssieb | ||||
| esphome/components/wireguard/* @droscy @lhoracek @thomas0bernard | ||||
| esphome/components/wl_134/* @hobbypunk90 | ||||
| esphome/components/x9c/* @EtienneMD | ||||
| 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) | ||||
|     esphome/ESP32-audioI2S@2.0.7         ; i2s_audio | ||||
|     crankyoldgit/IRremoteESP8266@2.7.12  ; heatpumpir | ||||
|     droscy/esp_wireguard@0.3.2           ; wireguard | ||||
| build_flags = | ||||
|     ${common:arduino.build_flags} | ||||
|     -DUSE_ESP32 | ||||
| @@ -141,6 +142,7 @@ framework = espidf | ||||
| lib_deps = | ||||
|     ${common:idf.lib_deps} | ||||
|     espressif/esp32-camera@1.0.0  ; esp32_camera | ||||
|     droscy/esp_wireguard@0.3.2    ; wireguard | ||||
| build_flags = | ||||
|     ${common:idf.build_flags} | ||||
|     -Wno-nonnull-compare | ||||
|   | ||||
| @@ -27,3 +27,4 @@ Current test_.yaml file contents. | ||||
| | test6.yaml | RP2040 | wifi | N/A | ||||
| | test7.yaml | ESP32-C3 | wifi | N/A | ||||
| | 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