wireguardctl.psm1

#!/usr/bin/env pwsh
using namespace System.IO
using namespace System.Collections.Concurrent

using module Private\WgInStallerGenerator.psm1

#Requires -PSEdition Core
#Requires -Modules PsModuleBase, argparser, Sixel
#region Classes

# Main class
# .EXAMPLE
# [WireguardCtl]::InstallWireGuard()
class WireguardCtl : PsModuleBase {

  WireguardCtl() {
  }
  static [String] ShowBanner() {
    $modr = [WireguardCtl]::GetModuleRoot()
    $bpng = [IO.Path]::Combine($modr, 'Private', 'wgbanner.png')
    return (Sixel\ConvertTo-Sixel $bpng)
  }
  static [string] GetModuleRoot() {
    $r = (Get-Module wireguardctl -ListAvailable).ModuleBase
    $r = [string]::IsNullOrWhiteSpace($r) ? (Get-Variable -ValueOnly PSScriptRoot) : $r
    if (![IO.Directory]::Exists($r)) {
      throw [DirectoryNotFoundException]::new("ModulePath $r NOT_FOUND")
    }
    return $r
  }

  static [void] Watch() {
    [WireguardCtl]::Watch('wg0')
  }
  static [void] Watch([string]$TunnelName) {
    if ([string]::IsNullOrWhiteSpace($TunnelName)) {
      $TunnelName = 'wg0' # defaultname
    }
    $svcName = "WireguardTunnel`$$TunnelName"

    $getRx = {
      try {
        $output = & wg show $TunnelName transfer 2>$null
        if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($output)) { return $null }
        $parts = $output -split '\s+'
        if ($parts.Count -ge 2) { return [long]$parts[1] }
      } catch {
        Write-Verbose "Error executing wg show: $_"
      }
      return $null
    }

    $recv1 = $null
    while ($null -eq $recv1) {
      $svc = $null
      try {
        $svc = [System.ServiceProcess.ServiceController]::new($svcName)
        $null = $svc.Status
      } catch {
        $svc = $null
      }

      if ($null -eq $svc) {
        $confPath = [System.IO.Path]::Combine($env:ProgramFiles, 'WireGuard', 'Data', 'Configurations', "$TunnelName.conf.dpapi")
        if ([System.IO.File]::Exists($confPath)) {
          & wireguard /installtunnelservice $confPath
          Start-Sleep -Seconds 8
        } else {
          Write-Warning "Wireguard config not found at $confPath. Exiting."
          return
        }
      } elseif ($svc.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Running) {
        try {
          $svc.Start()
          $svc.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Running, [System.TimeSpan]::FromSeconds(10))
        } catch {
          Write-Warning "Failed to start service $svcName : $_"
        }
      }

      $recv1 = & $getRx
      if ($null -eq $recv1) {
        Start-Sleep -Seconds 2
      }
    }
    Start-Sleep -Seconds 200

    $recv2 = & $getRx
    if ($recv1 -eq $recv2) {
      $doRestart = $true
      while ($doRestart) {
        try {
          $svc = [System.ServiceProcess.ServiceController]::new($svcName)
          if ($svc.Status -ne [System.ServiceProcess.ServiceControllerStatus]::Stopped) {
            $svc.Stop()
            $svc.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Stopped, [System.TimeSpan]::FromSeconds(15))
          }
          $svc.Start()
          $svc.WaitForStatus([System.ServiceProcess.ServiceControllerStatus]::Running, [System.TimeSpan]::FromSeconds(15))
          $doRestart = $false
        } catch {
          $doRestart = $true
          Start-Sleep -Seconds 5
        }
      }
    }
  }
  static [void] InstallWireGuard() {
    [WireguardCtl]::InstallWireGuard("")
  }
  static [void] InstallWireGuard([string]$argsline) {
    Write-Host ([WireguardCtl]::ShowBanner())
    $argslist = if ([string]::IsNullOrWhiteSpace($argsline)) { @() } else { $argsline -split '\s+' }
    $config = [WgInStallerGenerator]::GetConfigFromArgs($argslist)
    if ($null -eq $config) {
      return
    }

    Write-Host "`nProceeding with WireGuard Installation..." -ForegroundColor Cyan

    try {
      [WgInstallerRunner]::CheckAdmin()
    } catch {
      Write-Error $_.Exception.Message
      return
    }

    if ([WgInstallerRunner]::CheckWireGuardInstalled() -ne 'Installed') {
      [WgInstallerRunner]::SilentInstallWireGuard()
    } else {
      Write-Host "WireGuard is already installed." -ForegroundColor Green
    }

    $paths = [WgInstallerPaths]::new($config.InterfaceName)
    $paths.EnsureDirectories()

    Write-Host "Generating Keys..." -ForegroundColor Yellow
    $keys = [WgInstallerRunner]::GenerateKeys($paths.WgExe)
    $privKey = $keys[0]
    $pubKey = $keys[1]

    $confContent = @"
[Interface]
PrivateKey = $privKey
Address = $($config.ClientIP)
DNS = $($config.DNS)
 
[Peer]
PublicKey = $($config.ServerPublicKey)
AllowedIPs = $($config.AllowedIPs)
Endpoint = $($config.Endpoint)
PersistentKeepalive = $($config.KeepAlive)
"@

    Write-Host "Configuring tunnel: $($config.InterfaceName)" -ForegroundColor Yellow
    [WgInstallerRunner]::WriteConfigFile($paths.WorkConf, $confContent)
    Copy-Item $paths.WorkConf $paths.FinalConf -Force

    Write-Host "Installing and starting tunnel service..." -ForegroundColor Yellow
    [WgInstallerRunner]::ImportTunnel($paths.WireGuardExe, $paths.FinalConf, $config.InterfaceName)

    Write-Host "WireGuard installed and configured successfully!" -ForegroundColor Green
    Write-Host "Client Public Key: $pubKey" -ForegroundColor Magenta
  }
}
#endregion Classes

# Types that will be available to users when they import the module.
$typestoExport = @(
  [WireguardCtl]
)
$TypeAcceleratorsClass = [PsObject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
foreach ($Type in $typestoExport) {
  if ($Type.FullName -in $TypeAcceleratorsClass::Get.Keys) {
    $Message = @(
      "Unable to register type accelerator '$($Type.FullName)'"
      'Accelerator already exists.'
    ) -join ' - '
    "TypeAcceleratorAlreadyExists $Message" | Write-Debug
  }
}
# Add type accelerators for every exportable type.
foreach ($Type in $typestoExport) {
  $TypeAcceleratorsClass::Add($Type.FullName, $Type)
}
# Remove type accelerators when the module is removed.
$MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = {
  foreach ($Type in $typestoExport) {
    $TypeAcceleratorsClass::Remove($Type.FullName)
  }
}.GetNewClosure();

$scripts = @();
$Public = Get-ChildItem "$PSScriptRoot/Public" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue
$scripts += Get-ChildItem "$PSScriptRoot/Private" -Filter "*.ps1" -Recurse -ErrorAction SilentlyContinue
$scripts += $Public

foreach ($file in $scripts) {
  try {
    if ([string]::IsNullOrWhiteSpace($file.fullname)) { continue }
    . "$($file.fullname)"
  } catch {
    Write-Warning "Failed to import function $($file.BaseName): $_"
    $host.UI.WriteErrorLine($_)
  }
}

$Param = @{
  Function = $Public.BaseName
  Cmdlet   = '*'
  Alias    = '*'
  Verbose  = $false
}
Export-ModuleMember @Param