feat(security): harden gateway boundaries and usage policies

Consolidate subscription usage policy enforcement, privacy-safe persistence, and gateway security hardening into one reviewable change.

Includes bounded HTTP and execution envelopes, header and protocol guards, DNS and relay validation, authentication and secret projection hardening, secure backup/install paths, and regression coverage.
This commit is contained in:
elky
2026-09-04 03:45:52 +08:00
parent ddcbeb3ae9
commit 579f2c7cc1
1019 changed files with 190437 additions and 26080 deletions
+409 -28
View File
@@ -1,9 +1,11 @@
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Net.Http
$Repo = if ($env:AETHER_TUNNEL_RELEASE_REPO) { $env:AETHER_TUNNEL_RELEASE_REPO } else { 'fawney19/Aether' }
$ReleaseTag = $env:AETHER_TUNNEL_RELEASE_TAG
$InstallDir = $env:AETHER_TUNNEL_INSTALL_DIR
$ConfigPath = $env:AETHER_TUNNEL_CONFIG
$TunnelReleaseTagPattern = '^tunnel-v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$'
function Say([string]$Message) { Write-Host "[Aether Tunnel] $Message" }
function Fail([string]$Message) { throw "[Aether Tunnel] $Message" }
@@ -15,15 +17,126 @@ function Prompt-IfEmpty([string]$Name, [string]$Value, [string]$Prompt) {
return $Read
}
function Assert-SafeReleaseRepo([string]$Value) {
if ([string]::IsNullOrWhiteSpace($Value) -or
$Value.Length -gt 200 -or
$Value -cnotmatch '^[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*$') {
Fail 'Release repository must be a safe GitHub OWNER/REPO identifier'
}
}
function Assert-SafeTunnelReleaseTag([string]$Value) {
if ([string]::IsNullOrWhiteSpace($Value) -or
$Value.Length -gt 128 -or
$Value -cnotmatch $TunnelReleaseTagPattern) {
Fail 'Release tag must use tunnel-v followed by a valid semantic version'
}
}
function Assert-TrustedGithubUri([Uri]$Uri) {
if (-not $Uri.IsAbsoluteUri -or
$Uri.Scheme -cne 'https' -or
-not [string]::IsNullOrEmpty($Uri.UserInfo) -or
-not [string]::IsNullOrEmpty($Uri.Fragment) -or
$Uri.Port -ne 443) {
Fail "GitHub downloads must use credential-free HTTPS on port 443: $Uri"
}
$HostName = $Uri.IdnHost.ToLowerInvariant()
$TrustedHost = $HostName -in @('api.github.com', 'github.com', 'objects.githubusercontent.com', 'release-assets.githubusercontent.com') -or
$HostName.EndsWith('.objects.githubusercontent.com', [StringComparison]::Ordinal) -or
$HostName.EndsWith('.release-assets.githubusercontent.com', [StringComparison]::Ordinal)
if (-not $TrustedHost) { Fail "GitHub download redirected to an untrusted host: $HostName" }
}
function Get-TrustedGithubBytes([string]$UriText) {
$CurrentUri = [Uri]::new($UriText, [UriKind]::Absolute)
Assert-TrustedGithubUri $CurrentUri
$Handler = [System.Net.Http.HttpClientHandler]::new()
$Handler.AllowAutoRedirect = $false
$Client = [System.Net.Http.HttpClient]::new($Handler, $true)
$Client.DefaultRequestHeaders.UserAgent.ParseAdd('aether-tunnel-installer')
try {
for ($RedirectCount = 0; $RedirectCount -le 10; $RedirectCount++) {
$Response = $null
try {
$Response = $Client.GetAsync(
$CurrentUri,
[System.Net.Http.HttpCompletionOption]::ResponseHeadersRead
).GetAwaiter().GetResult()
$StatusCode = [int]$Response.StatusCode
if ($StatusCode -in @(301, 302, 303, 307, 308)) {
if ($RedirectCount -eq 10) { Fail 'GitHub download redirected too many times' }
$Location = $Response.Headers.Location
if (-not $Location) { Fail 'GitHub redirect is missing the Location header' }
$CurrentUri = if ($Location.IsAbsoluteUri) {
$Location
} else {
[Uri]::new($CurrentUri, $Location)
}
Assert-TrustedGithubUri $CurrentUri
continue
}
if (-not $Response.IsSuccessStatusCode) {
Fail "GitHub download returned HTTP $StatusCode for $CurrentUri"
}
$Bytes = $Response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult()
return ,$Bytes
} finally {
if ($Response) { $Response.Dispose() }
}
}
Fail 'GitHub download redirected too many times'
} finally {
$Client.Dispose()
}
}
function Read-TrustedGithubJson([string]$Uri) {
$Bytes = Get-TrustedGithubBytes $Uri
$Text = [Text.UTF8Encoding]::new($false, $true).GetString($Bytes)
return ($Text | ConvertFrom-Json)
}
function Save-TrustedGithubFile([string]$Uri, [string]$Path) {
$Bytes = Get-TrustedGithubBytes $Uri
$Stream = [IO.File]::Open($Path, [IO.FileMode]::CreateNew, [IO.FileAccess]::Write, [IO.FileShare]::None)
try {
$Stream.Write($Bytes, 0, $Bytes.Length)
$Stream.Flush($true)
} finally {
$Stream.Dispose()
}
}
function Assert-SafeNodeName([string]$Value) {
if ([string]::IsNullOrWhiteSpace($Value) -or
$Value.Length -gt 255 -or
$Value -ne $Value.Trim() -or
$Value -match '[\x00-\x1F\x7F]') {
Fail 'Node name must be 1 to 255 characters without surrounding whitespace or control characters'
}
}
function ConvertTo-TomlQuotedString([string]$Value) {
return ($Value | ConvertTo-Json -Compress)
}
function Resolve-LatestTunnelTag {
if (-not [string]::IsNullOrWhiteSpace($ReleaseTag)) { return $ReleaseTag }
Assert-SafeReleaseRepo $Repo
if (-not [string]::IsNullOrWhiteSpace($ReleaseTag)) {
Assert-SafeTunnelReleaseTag $ReleaseTag
$RequestedUri = "https://api.github.com/repos/$Repo/releases/tags/$ReleaseTag"
$RequestedRelease = Read-TrustedGithubJson $RequestedUri
if ($RequestedRelease.draft -or ([string]$RequestedRelease.tag_name -cne $ReleaseTag)) {
Fail 'GitHub returned a draft or mismatched tunnel release'
}
return $ReleaseTag
}
$Uri = "https://api.github.com/repos/$Repo/releases?per_page=100"
$Releases = Invoke-RestMethod -Uri $Uri -Headers @{ 'User-Agent' = 'aether-tunnel-installer' }
$TunnelReleases = @($Releases | Where-Object { -not $_.draft -and $_.tag_name -like 'tunnel-v*' } | Sort-Object published_at -Descending)
$Releases = Read-TrustedGithubJson $Uri
$TunnelReleases = @($Releases | Where-Object {
-not $_.draft -and -not $_.prerelease -and ([string]$_.tag_name -cmatch $TunnelReleaseTagPattern)
} | Sort-Object published_at -Descending)
if ($TunnelReleases.Count -eq 0) { Fail "No tunnel-v* release found in $Repo" }
return $TunnelReleases[0].tag_name
}
@@ -34,6 +147,159 @@ function Test-IsAdministrator {
return $Principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Protect-SensitiveConfigFile([string]$Path) {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
Fail "Sensitive config file does not exist: $Path"
}
Assert-NotReparsePoint $Path 'Sensitive config file'
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$CurrentSid = $Identity.User
if (-not $CurrentSid) { Fail 'Unable to resolve the current Windows account SID' }
$Acl = [System.Security.AccessControl.FileSecurity]::new()
$Acl.SetAccessRuleProtection($true, $false)
$Acl.SetOwner($CurrentSid)
$AllowedSids = @(
$CurrentSid.Value,
'S-1-5-18',
'S-1-5-32-544'
) | Select-Object -Unique
foreach ($SidValue in $AllowedSids) {
$Sid = [Security.Principal.SecurityIdentifier]::new($SidValue)
$Rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
$Sid,
[System.Security.AccessControl.FileSystemRights]::FullControl,
[System.Security.AccessControl.AccessControlType]::Allow
)
$Acl.AddAccessRule($Rule) | Out-Null
}
Set-Acl -LiteralPath $Path -AclObject $Acl
}
function Protect-SensitiveConfigDirectory([string]$Path) {
if (-not (Test-Path -LiteralPath $Path -PathType Container)) {
Fail "Sensitive config directory does not exist: $Path"
}
Assert-NotReparsePoint $Path 'Sensitive config directory'
$Identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$CurrentSid = $Identity.User
if (-not $CurrentSid) { Fail 'Unable to resolve the current Windows account SID' }
$Acl = [System.Security.AccessControl.DirectorySecurity]::new()
$Acl.SetAccessRuleProtection($true, $false)
$Acl.SetOwner($CurrentSid)
$AllowedSids = @($CurrentSid.Value, 'S-1-5-18', 'S-1-5-32-544') | Select-Object -Unique
foreach ($SidValue in $AllowedSids) {
$Sid = [Security.Principal.SecurityIdentifier]::new($SidValue)
$Rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
$Sid,
[System.Security.AccessControl.FileSystemRights]::FullControl,
[System.Security.AccessControl.InheritanceFlags]'ContainerInherit, ObjectInherit',
[System.Security.AccessControl.PropagationFlags]::None,
[System.Security.AccessControl.AccessControlType]::Allow
)
$Acl.AddAccessRule($Rule) | Out-Null
}
Set-Acl -LiteralPath $Path -AclObject $Acl
}
function Protect-SensitiveConfigArtifacts([string]$Path) {
if (Test-Path -LiteralPath $Path -PathType Leaf) {
Protect-SensitiveConfigFile $Path
}
$Directory = Split-Path -Parent $Path
$Leaf = Split-Path -Leaf $Path
if (-not (Test-Path -LiteralPath $Directory -PathType Container)) { return }
foreach ($Backup in Get-ChildItem -LiteralPath $Directory -File) {
if ($Backup.Name.StartsWith("$Leaf.bak.", [StringComparison]::OrdinalIgnoreCase)) {
Protect-SensitiveConfigFile $Backup.FullName
}
}
}
function Assert-NotReparsePoint([string]$Path, [string]$Description) {
if (-not (Test-Path -LiteralPath $Path)) { return }
$Item = Get-Item -LiteralPath $Path -Force
if (($Item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
Fail "$Description must not be a reparse point or symbolic link: $Path"
}
}
function Assert-NoReparsePointAncestors([string]$Path, [string]$Description) {
$Current = [IO.Path]::GetFullPath($Path)
while (-not [string]::IsNullOrEmpty($Current)) {
if (Test-Path -LiteralPath $Current) {
Assert-NotReparsePoint $Current "$Description ancestor"
}
$Parent = Split-Path -Parent $Current
if ([string]::IsNullOrEmpty($Parent) -or $Parent -eq $Current) { break }
$Current = $Parent
}
}
function Assert-NotHardLink([string]$Path, [string]$Description) {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return }
$Item = Get-Item -LiteralPath $Path -Force
$LinkTypeProperty = $Item.PSObject.Properties['LinkType']
if (-not $LinkTypeProperty) {
Fail "Unable to verify hard-link safety for $Description: $Path"
}
if ([string]$Item.LinkType -eq 'HardLink') {
Fail "$Description must not be a hard link: $Path"
}
}
function Initialize-SecureConfigPath([string]$Path) {
$Directory = Split-Path -Parent $Path
Assert-NoReparsePointAncestors $Directory 'Config directory'
[IO.Directory]::CreateDirectory($Directory) | Out-Null
Assert-NotReparsePoint $Directory 'Config directory'
Protect-SensitiveConfigDirectory $Directory
Assert-NotReparsePoint $Path 'Config file'
if (Test-Path -LiteralPath $Path) {
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
Fail "Config path is not a regular file: $Path"
}
Assert-NotHardLink $Path 'Config file'
Protect-SensitiveConfigFile $Path
}
}
function Write-SensitiveUtf8File([string]$Path, [string]$Content) {
$CreateStream = [IO.File]::Open(
$Path,
[IO.FileMode]::CreateNew,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
$CreateStream.Dispose()
Protect-SensitiveConfigFile $Path
$Stream = [IO.File]::Open(
$Path,
[IO.FileMode]::Open,
[IO.FileAccess]::Write,
[IO.FileShare]::None
)
try {
$Encoding = [Text.UTF8Encoding]::new($false)
$Writer = [IO.StreamWriter]::new($Stream, $Encoding)
try {
$Writer.Write($Content)
$Writer.Flush()
} finally {
$Writer.Dispose()
}
} finally {
$Stream.Dispose()
}
}
function Initialize-Paths {
if ([string]::IsNullOrWhiteSpace($script:InstallDir)) {
if (Test-IsAdministrator) {
@@ -49,9 +315,83 @@ function Initialize-Paths {
$script:ConfigPath = Join-Path $env:APPDATA 'AetherTunnel\aether-tunnel.toml'
}
}
$script:InstallDir = [IO.Path]::GetFullPath($script:InstallDir)
$script:ConfigPath = [IO.Path]::GetFullPath($script:ConfigPath)
}
function Install-VerifiedTunnelBinary([string]$SourceBinary) {
Assert-NoReparsePointAncestors $script:InstallDir 'Install directory'
[IO.Directory]::CreateDirectory($script:InstallDir) | Out-Null
Assert-NotReparsePoint $script:InstallDir 'Install directory'
$TargetBinary = Join-Path $script:InstallDir 'aether-tunnel.exe'
Assert-NotReparsePoint $TargetBinary 'Install target'
if ((Test-Path -LiteralPath $TargetBinary) -and
-not (Test-Path -LiteralPath $TargetBinary -PathType Leaf)) {
Fail "Install target is not a regular file: $TargetBinary"
}
Assert-NotHardLink $TargetBinary 'Install target'
$TempBinary = Join-Path $script:InstallDir ('.aether-tunnel.tmp.' + [Guid]::NewGuid().ToString('N'))
try {
[IO.File]::Copy($SourceBinary, $TempBinary, $false)
Assert-NotReparsePoint $TargetBinary 'Install target'
Assert-NotHardLink $TargetBinary 'Install target'
if (Test-Path -LiteralPath $TargetBinary -PathType Leaf) {
[IO.File]::Replace($TempBinary, $TargetBinary, $null, $true)
} elseif (Test-Path -LiteralPath $TargetBinary) {
Fail "Install target changed to a non-file during installation: $TargetBinary"
} else {
[IO.File]::Move($TempBinary, $TargetBinary)
}
} finally {
if (Test-Path -LiteralPath $TempBinary) {
Remove-Item -LiteralPath $TempBinary -Force
}
}
}
function Expand-VerifiedTunnelArchive([string]$Archive, [string]$Destination) {
Add-Type -AssemblyName System.IO.Compression.FileSystem
$Zip = [System.IO.Compression.ZipFile]::OpenRead($Archive)
try {
$Entries = @($Zip.Entries)
if ($Entries.Count -ne 1) { Fail 'Release archive must contain exactly one file' }
$Entry = $Entries[0]
if (($Entry.FullName -ne 'aether-tunnel.exe') -or ($Entry.Name -ne 'aether-tunnel.exe')) {
Fail 'Release archive must contain only aether-tunnel.exe at its root'
}
$UnixType = (($Entry.ExternalAttributes -shr 16) -band 0xF000)
if (($UnixType -ne 0) -and ($UnixType -ne 0x8000)) {
Fail 'aether-tunnel.exe in release archive is not a regular file'
}
if ($Entry.Length -le 0) { Fail 'aether-tunnel.exe in release archive is empty' }
$InputStream = $Entry.Open()
try {
$OutputStream = [System.IO.File]::Open(
$Destination,
[System.IO.FileMode]::CreateNew,
[System.IO.FileAccess]::Write,
[System.IO.FileShare]::None
)
try {
$InputStream.CopyTo($OutputStream)
} finally {
$OutputStream.Dispose()
}
} finally {
$InputStream.Dispose()
}
} finally {
$Zip.Dispose()
}
}
function Install-AetherTunnelBinary([string]$Tag, [string]$TempDir) {
Assert-SafeReleaseRepo $Repo
Assert-SafeTunnelReleaseTag $Tag
if (-not [Environment]::Is64BitOperatingSystem) { Fail 'Windows release currently supports amd64 only' }
$Asset = 'aether-tunnel-windows-amd64.zip'
$Base = "https://github.com/$Repo/releases/download/$Tag"
@@ -59,30 +399,36 @@ function Install-AetherTunnelBinary([string]$Tag, [string]$TempDir) {
$Sums = Join-Path $TempDir 'SHA256SUMS.txt'
Say "Downloading $Tag / $Asset"
Invoke-WebRequest -Uri "$Base/$Asset" -OutFile $Archive
try { Invoke-WebRequest -Uri "$Base/SHA256SUMS.txt" -OutFile $Sums } catch { $Sums = $null }
Save-TrustedGithubFile "$Base/$Asset" $Archive
Save-TrustedGithubFile "$Base/SHA256SUMS.txt" $Sums
if (-not (Test-Path -LiteralPath $Sums -PathType Leaf)) { Fail 'SHA256SUMS.txt download is missing' }
if ($Sums -and (Test-Path $Sums)) {
$ExpectedLine = Get-Content $Sums | Where-Object { $_ -match "\s$([regex]::Escape($Asset))$" } | Select-Object -First 1
if ($ExpectedLine) {
$Expected = ($ExpectedLine -split '\s+')[0]
$Actual = (Get-FileHash -Algorithm SHA256 $Archive).Hash.ToLowerInvariant()
if ($Actual -ne $Expected.ToLowerInvariant()) { Fail "SHA256 verification failed for $Asset" }
}
$EscapedAsset = [regex]::Escape($Asset)
$ChecksumTargetPattern = '(?:^|\s)\*?' + $EscapedAsset + '(?:\s|$)'
$ExpectedLines = @(Get-Content -LiteralPath $Sums | Where-Object { $_ -match $ChecksumTargetPattern })
if ($ExpectedLines.Count -ne 1) {
Fail "SHA256SUMS.txt must contain exactly one entry for $Asset"
}
$ChecksumPattern = '^\s*(?<hash>\S+)\s+\*?' + $EscapedAsset + '\s*$'
$ExpectedMatch = [regex]::Match([string]$ExpectedLines[0], $ChecksumPattern)
if (-not $ExpectedMatch.Success) { Fail "SHA256SUMS.txt has an invalid entry for $Asset" }
$Expected = $ExpectedMatch.Groups['hash'].Value
if ($Expected -notmatch '^[0-9A-Fa-f]{64}$') { Fail "SHA256SUMS.txt has an invalid hash for $Asset" }
$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $Archive).Hash.ToLowerInvariant()
if ($Actual -ne $Expected.ToLowerInvariant()) { Fail "SHA256 verification failed for $Asset" }
$ExtractDir = Join-Path $TempDir 'extract'
Expand-Archive -Path $Archive -DestinationPath $ExtractDir -Force
New-Item -ItemType Directory -Force -Path $ExtractDir | Out-Null
$Binary = Join-Path $ExtractDir 'aether-tunnel.exe'
if (-not (Test-Path $Binary)) { Fail 'aether-tunnel.exe not found in release asset' }
New-Item -ItemType Directory -Force -Path $script:InstallDir | Out-Null
Copy-Item $Binary (Join-Path $script:InstallDir 'aether-tunnel.exe') -Force
Expand-VerifiedTunnelArchive $Archive $Binary
Install-VerifiedTunnelBinary $Binary
Say "Installed binary: $(Join-Path $script:InstallDir 'aether-tunnel.exe')"
}
function Test-LegacySingleServerConfig([string]$Path) {
if (-not (Test-Path $Path)) { return $false }
foreach ($Line in Get-Content $Path) {
if (-not (Test-Path -LiteralPath $Path)) { return $false }
foreach ($Line in Get-Content -LiteralPath $Path) {
if ($Line -match '^\s*\[') { return $false }
if ($Line -match '^\s*(aether_url|management_token)\s*=') { return $true }
}
@@ -90,10 +436,10 @@ function Test-LegacySingleServerConfig([string]$Path) {
}
function Test-ServerExists([string]$Path, [string]$QuotedUrl, [string]$QuotedName) {
if (-not (Test-Path $Path)) { return $false }
if (-not (Test-Path -LiteralPath $Path)) { return $false }
$FoundUrl = $false
$FoundName = $false
foreach ($Line in Get-Content $Path) {
foreach ($Line in Get-Content -LiteralPath $Path) {
if ($Line -match '^\s*\[\[servers\]\]\s*$') {
if ($FoundUrl -and $FoundName) { return $true }
$FoundUrl = $false
@@ -106,8 +452,8 @@ function Test-ServerExists([string]$Path, [string]$QuotedUrl, [string]$QuotedNam
}
function Add-ServerConfig([string]$AetherUrl, [string]$ManagementToken, [string]$NodeName, [string]$TunnelSecurity, [string]$TunnelEncryptionKey) {
$ConfigDir = Split-Path -Parent $script:ConfigPath
New-Item -ItemType Directory -Force -Path $ConfigDir | Out-Null
Assert-SafeNodeName $NodeName
Initialize-SecureConfigPath $script:ConfigPath
if (Test-LegacySingleServerConfig $script:ConfigPath) {
Fail "Existing config uses removed top-level aether_url/management_token. Run aether-tunnel setup to migrate to [[servers]] first: $script:ConfigPath"
@@ -123,11 +469,18 @@ function Add-ServerConfig([string]$AetherUrl, [string]$ManagementToken, [string]
return
}
if (Test-Path $script:ConfigPath) {
Copy-Item $script:ConfigPath "$script:ConfigPath.bak.$(Get-Date -Format yyyyMMddHHmmss)" -Force
$ConfigExists = Test-Path -LiteralPath $script:ConfigPath -PathType Leaf
$ExistingContent = if ($ConfigExists) {
[IO.File]::ReadAllText($script:ConfigPath)
} else {
''
}
if ($ConfigExists) {
$BackupPath = "$script:ConfigPath.bak.$(Get-Date -Format yyyyMMddHHmmss).$([Guid]::NewGuid().ToString('N'))"
Write-SensitiveUtf8File $BackupPath $ExistingContent
}
$Prefix = if ((Test-Path $script:ConfigPath) -and ((Get-Item $script:ConfigPath).Length -gt 0)) { "`n" } else { '' }
$Prefix = if ($ExistingContent.Length -gt 0) { "`n" } else { '' }
$Block = @(
"$Prefix# Added by Aether Tunnel one-click installer. Existing config is preserved.",
'[[servers]]',
@@ -142,15 +495,35 @@ function Add-ServerConfig([string]$AetherUrl, [string]$ManagementToken, [string]
if ($TunnelEncryptionKey) {
$Block += "`ntunnel_encryption_key = $QuotedTunnelEncryptionKey"
}
Add-Content -Path $script:ConfigPath -Value ($Block + "`n") -Encoding UTF8
$ConfigDir = Split-Path -Parent $script:ConfigPath
$TempPath = Join-Path $ConfigDir ("." + (Split-Path -Leaf $script:ConfigPath) + ".tmp." + [Guid]::NewGuid().ToString('N'))
try {
Write-SensitiveUtf8File $TempPath ($ExistingContent + $Block + "`n")
Assert-NotReparsePoint $script:ConfigPath 'Config file'
if ($ConfigExists) {
if (-not (Test-Path -LiteralPath $script:ConfigPath -PathType Leaf)) {
Fail "Config file changed while it was being updated: $script:ConfigPath"
}
[IO.File]::Replace($TempPath, $script:ConfigPath, $null, $true)
} else {
[IO.File]::Move($TempPath, $script:ConfigPath)
}
} finally {
if (Test-Path -LiteralPath $TempPath) {
Remove-Item -LiteralPath $TempPath -Force
}
}
Protect-SensitiveConfigArtifacts $script:ConfigPath
Say "Appended [[servers]] to: $script:ConfigPath"
}
function Main {
Assert-SafeReleaseRepo $Repo
Initialize-Paths
$AetherUrl = Prompt-IfEmpty 'AETHER_TUNNEL_AETHER_URL' $env:AETHER_TUNNEL_AETHER_URL 'Aether URL'
$ManagementToken = Prompt-IfEmpty 'AETHER_TUNNEL_MANAGEMENT_TOKEN' $env:AETHER_TUNNEL_MANAGEMENT_TOKEN 'Management token (ae_xxx)'
$NodeName = Prompt-IfEmpty 'AETHER_TUNNEL_NODE_NAME' $env:AETHER_TUNNEL_NODE_NAME 'Node name'
Assert-SafeNodeName $NodeName
$TunnelSecurity = if ($env:AETHER_TUNNEL_SECURITY) { $env:AETHER_TUNNEL_SECURITY } else { '' }
$TunnelEncryptionKey = if ($env:AETHER_TUNNEL_ENCRYPTION_KEY) { $env:AETHER_TUNNEL_ENCRYPTION_KEY } else { '' }
if ($TunnelSecurity -and ($TunnelSecurity -notin @('off', 'non_tls_required'))) {
@@ -161,13 +534,21 @@ function Main {
}
$TempDir = Join-Path ([IO.Path]::GetTempPath()) ("aether-tunnel-" + [Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Force -Path $TempDir | Out-Null
Assert-NoReparsePointAncestors (Split-Path -Parent $TempDir) 'Temporary directory'
if (Test-Path -LiteralPath $TempDir) { Fail "Secure temporary path already exists: $TempDir" }
[IO.Directory]::CreateDirectory($TempDir) | Out-Null
Assert-NotReparsePoint $TempDir 'Temporary directory'
Protect-SensitiveConfigDirectory $TempDir
try {
$Tag = Resolve-LatestTunnelTag
Assert-SafeTunnelReleaseTag $Tag
Install-AetherTunnelBinary $Tag $TempDir
Add-ServerConfig $AetherUrl $ManagementToken $NodeName $TunnelSecurity $TunnelEncryptionKey
} finally {
Remove-Item -Recurse -Force $TempDir -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $TempDir) {
Assert-NotReparsePoint $TempDir 'Temporary directory'
Remove-Item -Recurse -Force -LiteralPath $TempDir -ErrorAction SilentlyContinue
}
}
Say 'Complete. Start or configure the node with:'