/* xdrv_80_wireguard_clientc.ino - creates a VPN connection to a Wireguard site Copyright (C) 2024 Stephan Hadinger This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifdef USE_WIREGUARD #define XDRV_80 80 #include "esp_wireguard.h" #include "IniFile.h" #include "LList.h" /*********************************************************************************************\ * Commands \*********************************************************************************************/ static const char WIREGUARD_CONF_FNAME[] = "/wireguard.conf"; static const char WIREGUARD_NETMASK[] = "0.0.0.0"; const char kWireGuardCommands[] PROGMEM = "WG|" // Prefix "Connect|Stop"; void (* const WireGuardCommand[])(void) PROGMEM = { &CmndWGConnect, &CmndWGStop }; typedef struct { ip_addr_t addr; ip_addr_t mask; } allowed_ips_t; struct Wireguard_t { bool configured = false; bool auto_connect = false; bool started = false; bool peer_up = false; int8_t peer_status = -1; // known state: -1 unknown, 0 DOWN, 1 UP uint32_t connected_since_utc = 0; String endpoint; LList allowed_ips; // used by lib wireguard_config_t config = {}; wireguard_ctx_t ctx = {0}; } Wireguard; /*********************************************************************************************\ * WireGuard internal lower level functions \*********************************************************************************************/ // WireguardLoadConfig // // Load configuration from INI file // returns 'true' if succesful bool WireguardLoadConfig(const char *filename) { if (filename == NULL) { return false; } if (!ffsp) { AddLog(LOG_LEVEL_INFO, PSTR("WG : WireGuard initialization failed, no file system")); return false; } File file = ffsp->open(filename, "r"); if (!file) { AddLog(LOG_LEVEL_DEBUG, PSTR("WG : file '%s' not present, skipping"), filename); return false; } IniFile ini(file); bool valconf = true; wireguard_config_t& config = Wireguard.config; valconf = true; valconf = valconf && ini.getValueBase64("Interface", "PrivateKey", config.private_key2, sizeof(config.private_key2)); valconf = valconf && ini.getCIDR("Interface", "Address", &config.address2, &config.subnet); valconf = valconf && ini.getValueBase64("Peer", "PublicKey", config.public_key2, sizeof(config.public_key2)); valconf = valconf && ini.getValueBase64("Peer", "PresharedKey", config.preshared_key2, sizeof(config.preshared_key2)); valconf = valconf && ini.getDomainPort("Peer", "Endpoint", Wireguard.endpoint, Wireguard.config.port, 51820 /*default port*/); // read optional NetMask ipaddr_aton(WIREGUARD_NETMASK, &config.netmask2); ini.getIPAddress("Tasmota", "Netmask", &Wireguard.config.netmask2); // read optional PersistentKeepalive ini.getValueUInt16("Peer", "PersistentKeepalive", config.persistent_keepalive); // read optional AutoConnect ini.getValueBool("Tasmota", "AutoConnect", Wireguard.auto_connect); // add allowedIPs String allowed_ips_str; ini.getValueString("Peer", "AllowedIPs", allowed_ips_str); if (valconf) { // read optional AllowedIPs allowed_ips_t allowip; while (allowed_ips_str.length() > 0) { int32_t comma = allowed_ips_str.indexOf(","); String cidr = (comma > 0) ? allowed_ips_str.substring(0, comma) : allowed_ips_str; cidr.trim(); // AddLog(LOG_LEVEL_DEBUG, ">>>: allowed_ips_str '%s' comma %i cidr '%s'", allowed_ips_str.c_str(), comma, cidr.c_str()); if (IniFile::parseCIDR(cidr, &allowip.addr, &allowip.mask)) { Wireguard.allowed_ips.addHead(allowip); } else { AddLog(LOG_LEVEL_INFO, PSTR("WG : Failed to parse allowed_ips '%s', skipping"), cidr.c_str()); } if (comma > 0) { allowed_ips_str = allowed_ips_str.substring(comma + 1); } else { break; } } } file.close(); if (!valconf) { AddLog(LOG_LEVEL_INFO, PSTR("WG : WireGuard initialization failed, invalid configuration")); return false; } // now parse values Wireguard.config.endpoint = Wireguard.endpoint.c_str(); AddLog(LOG_LEVEL_DEBUG, PSTR("WG : reading '%s' address:%s/%s netmask:%s endpoint:%s:%i allowed_ips_str:'%s' PersistentKeepalive:%i"), filename, IPAddress(&config.address2).toString().c_str(), IPAddress(&config.subnet).toString().c_str(), IPAddress(&config.netmask2).toString().c_str(), Wireguard.config.endpoint, Wireguard.config.port, allowed_ips_str.c_str(), config.persistent_keepalive); return true; } // WireguardConnect // // Connect to peer bool WireguardConnect(void) { if (!Wireguard.configured || Wireguard.started) { return false; } esp_err_t err = esp_wireguard_connect(&Wireguard.ctx); if (err == ESP_OK) { Wireguard.started = true; if (!Wireguard.allowed_ips.isEmpty()) { for (const allowed_ips_t & allowedip : Wireguard.allowed_ips) { err = esp_wireguard_add_allowed_ip(&Wireguard.ctx, allowedip.addr, allowedip.mask); if (err != ESP_OK) { AddLog(LOG_LEVEL_INFO, PSTR("WG : Failed to add allowed_ips, no space left")); break; } AddLog(LOG_LEVEL_DEBUG, PSTR("WG : Added allowed_ips %s/%s"), IPAddress(&allowedip.addr).toString().c_str(), IPAddress(&allowedip.mask).toString().c_str()); } } else { // allowed_ips is empty, so we add 0.0.0.0/0.0.0.0 ip_addr_t ip_zero = IPADDR4_INIT_BYTES(0, 0, 0, 0); err = esp_wireguard_add_allowed_ip(&Wireguard.ctx, ip_zero, ip_zero); AddLog(LOG_LEVEL_DEBUG, PSTR("WG : Added default allowed_ips 0.0.0.0/0.0.0.0")); } return true; } return false; } // WireguardStop // // Stop the current Wireguard connection // Do nothing if there is no connection void WireguardStop(void) { if (!Wireguard.configured || !Wireguard.started) { return; } // stop wireguard esp_wireguard_disconnect(&Wireguard.ctx); Wireguard.started = false; AddLog(LOG_LEVEL_INFO, PSTR("WG : Wireguard peer DOWN")); } /*********************************************************************************************\ * WireGuard commands \*********************************************************************************************/ // Initialize Wireguard client void WireguardInit(void) { if (WireguardLoadConfig(WIREGUARD_CONF_FNAME)) { esp_wireguard_init(&Wireguard.config, &Wireguard.ctx); Wireguard.configured = true; } else { Wireguard.configured = false; } } // WireGuard Connect void CmndWGConnect(void) { if (!Wireguard.configured) { ResponseCmndChar(PSTR("Not configured")); return; } if (Wireguard.started) { ResponseCmndChar(PSTR("Already started")); return; } if (WireguardConnect()) { ResponseCmndChar(PSTR("Success")); } else { ResponseCmndChar(PSTR("Failed")); } } // WireGuard Stop void CmndWGStop(void) { if (!Wireguard.configured) { ResponseCmndChar(PSTR("Not configured")); return; } if (!Wireguard.started) { ResponseCmndChar(PSTR("Not started")); return; } // stop wireguard Wireguard.auto_connect = false; // prevent auto-reconnect when we asked for a manual stop WireguardStop(); ResponseCmndChar(PSTR("Success")); } // Loop every second void WireguardLoop(void) { if (Wireguard.started) { esp_err_t err = esp_wireguard_peer_is_up(&Wireguard.ctx); if (err == ESP_OK) { if (Wireguard.peer_status != 1) { Wireguard.peer_status = 1; if (Rtc.utc_time >= START_VALID_TIME) { // record the connection time only if we have a valid time Wireguard.connected_since_utc = Rtc.utc_time; } AddLog(LOG_LEVEL_INFO, PSTR("WG : Wireguard peer UP")); } // second chance, if connection happened with no time, and now time is valid if (Wireguard.connected_since_utc == 0 && Rtc.utc_time >= START_VALID_TIME) { Wireguard.connected_since_utc = Rtc.utc_time; } } else { if (Wireguard.peer_status != 0) { Wireguard.peer_status = 0; AddLog(LOG_LEVEL_INFO, PSTR("WG : Wireguard peer DOWN")); } } } } void WireguardNetworkUpDown(bool up) { if (up) { if (!Wireguard.started && Wireguard.auto_connect) { WireguardConnect(); } } else { // Network is down if (Wireguard.started) { WireguardStop(); } } } /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv80(uint32_t function) { bool result = false; if (TasmotaGlobal.no_autoexec) { return result; } // do nothing in case of bootloop switch (function) { case FUNC_INIT: WireguardInit(); break; case FUNC_COMMAND: result = DecodeCommand(kWireGuardCommands, WireGuardCommand); break; case FUNC_EVERY_SECOND: WireguardLoop(); break; #ifdef USE_WEBSERVER case FUNC_WEB_STATUS_RIGHT: if (Wireguard.started && Wireguard.peer_status == 1) { // number of seconds since connection, or -1 if no valid time int32_t seconds = Wireguard.connected_since_utc ? Rtc.utc_time - Wireguard.connected_since_utc : -1; WSContentStatusSticker(PSTR("VPN")); } break; #endif // USE_WEBSERVER case FUNC_NETWORK_UP: WireguardNetworkUpDown(true); break; case FUNC_NETWORK_DOWN: WireguardNetworkUpDown(false); break; } return result; } #endif // USE_WIREGUARD