VPN 環境中串流和 Teams 事件的特殊考慮

注意事項

本文是一組文章的一部分,可解決遠端使用者的 Microsoft 365 優化問題。

Microsoft 365 即時活動出席者流量 (這包括 Teams 產生的即時活動的出席者,以及透過 Teams、Stream 或 Viva Engage) 使用外部編碼器產生的活動,Microsoft Teams 全體大會出席者流量和隨選 Stream 出席者流量目前已分類為服務的 URL/IP 清單中的預設優化。 這些端點會分類為 預設值 ,因為它們裝載於其他服務可能也會使用的CDN上。 客戶通常偏好 Proxy 這種類型的流量,並套用通常在這類端點上執行的任何安全性元素。

許多客戶要求提供將出席者直接從本機因特網連線連線到 Stream 或 Teams 活動所需的 URL/IP 資料,而不是透過 VPN 基礎結構路由傳送大量和延遲敏感流量。 一般而言,若沒有專用命名空間和端點的正確IP資訊,就無法這麼做,而不會為分類為 預設值的 Microsoft 365 端點提供這些資訊。

使用下列步驟,從使用強制通道 VPN 的用戶端啟用 Stream 或 Teams 事件服務的直接連線。 此解決方案旨在為客戶提供一個選項,以避免在因家工作案例而導致網路流量偏高時,透過 VPN 路由傳送事件出席者流量。 可能的話,建議您透過檢查 Proxy 存取服務。

注意事項

使用此解決方案時,可能會有服務元素無法解析為提供的IP位址,因此會周遊 VPN,但串流數據等大量流量應該會發生。 在即時事件/串流範圍之外可能有其他元素會被這個卸除攔截,但這些項目應該受到限制,因為它們必須符合 FQDN IP 相符專案,才能直接進行。

重要事項

我們建議您權衡傳送更多流量的風險,這些流量會略過 VPN,而不是即時事件的效能提升。

若要實作 Teams 事件和 Stream 的強制通道例外狀況,應套用下列步驟:

1.設定外部 DNS 解析

用戶端需要外部遞歸 DNS 解析才能使用,才能將下列主機名解析為 IP 位址。

  • *.azureedge.net
  • *.media.azure.net
  • *.bmc.cdn.office.net
  • *.ml.cdn.office.net

*.azureedge.net 用於串流事件 (在 Microsoft Stream 中設定即時串流的編碼器 - Microsoft Stream |Microsoft Docs) 。

*.media.azure.net*.bmc.cdn.office.net 用於 Teams 產生的即時活動 (快速入門活動,以及 RTMP-In 從 Teams 用戶端排程的支持活動) 。

*.media.azure.net*.bmc.cdn.office.net*.ml.cdn.office.net 用於 Teams 全體大會活動。

其中一些端點會與 Stream 或 Teams 活動以外的其他元素共用。 不建議只使用這些 FQDN 來設定 VPN 卸除,即使技術上可能在您的 VPN 解決方案中 (,例如,如果它在 FQDN 而不是 IP) 運作。

VPN 組態中不需要 FQDN,它們僅供 PAC 檔案搭配 IP 使用,以直接傳送相關的流量。

2.在需要時實作 PAC 檔案變更 ()

對於使用 PAC 檔案在 VPN 上透過 Proxy 路由傳送流量的組織而言,這通常是使用 FQDN 來達成。 不過,透過 Stream/Live Events/Town Hall,提供的主機名會包含通配符,例如 *.azureedge.net,其中也包含無法提供完整 IP 清單的其他元素。 因此,如果直接根據 DNS 通配符比對傳送要求,這些端點的流量將會遭到封鎖,因為本文稍後的 步驟 3 中沒有透過其直接路徑的路由。

若要解決此問題,我們可以提供下列IP,並將其與範例 PAC 檔案中的主機名搭配使用,如 步驟 1 中所述。 PAC 檔案會檢查 URL 是否符合用於 Stream/Live Events/Town Hall 的 URL,如果符合,則也會檢查從 DNS 查閱傳回的 IP 是否符合為服務提供的 IP。 如果 兩者相 符,則會直接路由傳送流量。 如果其中一個元素 (FQDN/IP) 不相符,則流量會傳送至 Proxy。 因此,組態可確保解析為IP和已定義命名空間範圍之外之IP的任何專案,都會如常透過VPN周遊 Proxy。

收集 CDN 端點的目前清單

Teams 活動會使用多個 CDN 提供者串流至客戶,以提供最佳的涵蓋範圍、品質和復原能力。 目前使用來自 Microsoft 和 Verizon 的 Azure CDN。 經過一段時間后,這可能會因為區域可用性之類的情況而變更。 本文是讓您隨時掌握IP範圍的來源。

針對來自 Microsoft 的 Azure CDN,您可以從 從官方 Microsoft 下載中心下載 Azure IP 範圍和服務標籤 – 公用雲 端下載清單 - 您必須特別尋找 JSON 中的 AzureFrontdoor.Frontend 服務標籤; addressPrefixes 會顯示 IPv4/IPv6 子網。 經過一段時間后,IP 可能會變更,但服務標籤清單一律會在它們開始使用之前更新。

針對來自 Verizon 的 Azure CDN (Edgecast) 您可以使用 Edge 節點找到 詳盡的清單 - 列出 (選取 [ 試用 ) - 您必須特別尋找 [Premium_Verizon ] 區段。 請注意,此 API 會顯示源 (和 Anycast) 的所有 Edgecast IP。 目前沒有可讓 API 區分原點和 Anycast 的機制。

若要在 PAC 檔案中實作此功能,您可以使用下列範例來傳送 Microsoft 365 優化流量直接 (這是建議的最佳做法,) 透過 FQDN,以及透過 FQDN 和傳回的 IP 位址組合直接傳送重要的串流/即時事件流量。 您必須將佔位元元名稱 Contoso 編輯為您特定租用戶的名稱,其中 contoso 來自 contoso.onmicrosoft.com

範例 PAC 檔案

以下是如何產生 PAC 檔案的範例:

  1. 將下列腳本儲存至本機硬碟 Get-TLEPacFile.ps1。

  2. 移至 Verizon URL ,並下載產生的 JSON (將它複製貼到檔案中,例如 cdnedgenodes.json)

  3. 將檔案放入與腳本相同的資料夾中。

  4. 在 PowerShell 視窗中,執行下列命令。 如果您想要 SPO URL,請變更其他專案的租用戶名稱。 這是類型 2,因此 [優化 ] 和 [ 允許 (類型 1 只會優化) 。

    .\Get-TLEPacFile.ps1 -Instance Worldwide -Type 2 -TenantName <contoso> -CdnEdgeNodesFilePath .\cdnedgenodes.json -FilePath TLE.pac
    
  5. TLE.pac 檔案將包含 IPv4/IPv6) (的所有命名空間和 IP。

Get-TLEPacFile.ps1
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

<#PSScriptInfo

.VERSION 1.0.5

.AUTHOR Microsoft Corporation

.GUID 7f692977-e76c-4582-97d5-9989850a2529

.COMPANYNAME Microsoft

.COPYRIGHT
Copyright (c) Microsoft Corporation. All rights reserved.
Licensed under the MIT License.

.TAGS PAC Microsoft Microsoft365 365

.LICENSEURI

.PROJECTURI http://aka.ms/ipurlws

.ICONURI

.EXTERNALMODULEDEPENDENCIES

.REQUIREDSCRIPTS

.EXTERNALSCRIPTDEPENDENCIES

.RELEASENOTES

#>

<#

.SYNOPSIS

Create a PAC file for Microsoft 365 prioritized connectivity

.DESCRIPTION

This script will access updated information to create a PAC file to prioritize Microsoft 365 Urls for
better access to the service. This script will allow you to create different types of files depending
on how traffic needs to be prioritized.

.PARAMETER Instance

The service instance inside Microsoft 365.

.PARAMETER ClientRequestId

The client request id to connect to the web service to query up to date Urls.

.PARAMETER DirectProxySettings

The direct proxy settings for priority traffic.

.PARAMETER DefaultProxySettings

The default proxy settings for non priority traffic.

.PARAMETER Type

The type of prioritization to give. Valid values are 1 and 2, which are 2 different modes of operation.
Type 1 will send Optimize traffic to the direct route. Type 2 will send Optimize and Allow traffic to
the direct route.

.PARAMETER Lowercase

Flag this to include lowercase transformation into the PAC file for the host name matching.

.PARAMETER TenantName

The tenant name to replace wildcard Urls in the webservice.

.PARAMETER ServiceAreas

The service areas to filter endpoints by in the webservice.

.PARAMETER FilePath

The file to print the content to.

.EXAMPLE

Get-TLEPacFile.ps1 -ClientRequestId b10c5ed1-bad1-445f-b386-b919946339a7 -DefaultProxySettings "PROXY 4.4.4.4:70" -FilePath type1.pac

.EXAMPLE

Get-TLEPacFile.ps1 -ClientRequestId b10c5ed1-bad1-445f-b386-b919946339a7 -Instance China -Type 2 -DefaultProxySettings "PROXY 4.4.4.4:70" -FilePath type2.pac

.EXAMPLE

Get-TLEPacFile.ps1 -ClientRequestId b10c5ed1-bad1-445f-b386-b919946339a7 -Instance WorldWide -Lowercase -TenantName tenantName -ServiceAreas Sharepoint

#>

#Requires -Version 2

[CmdletBinding(SupportsShouldProcess=$True)]
Param (
    [Parameter(Mandatory = $false)]
    [ValidateSet('Worldwide', 'Germany', 'China', 'USGovDoD', 'USGovGCCHigh')]
    [String] $Instance = "Worldwide",

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [guid] $ClientRequestId = [Guid]::NewGuid().Guid,

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [String] $DirectProxySettings = 'DIRECT',

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [String] $DefaultProxySettings = 'PROXY 10.10.10.10:8080',

    [Parameter(Mandatory = $false)]
    [ValidateRange(1, 2)]
    [int] $Type = 1,

    [Parameter(Mandatory = $false)]
    [switch] $Lowercase = $false,

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string] $TenantName,

    [Parameter(Mandatory = $false)]
    [ValidateSet('Exchange', 'SharePoint', 'Common', 'Skype')]
    [string[]] $ServiceAreas,

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string] $FilePath,

    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string] $CdnEdgeNodesFilePath
)

##################################################################################################################
### Global constants
##################################################################################################################

$baseServiceUrl = "https://endpoints.office.com/endpoints/$Instance/?ClientRequestId={$ClientRequestId}"
$directProxyVarName = "direct"
$defaultProxyVarName = "proxyServer"
$bl = "`r`n"

##################################################################################################################
### Functions to create PAC files
##################################################################################################################

function Get-PacClauses
{
    param(
        [Parameter(Mandatory = $false)]
        [string[]] $Urls,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $ReturnVarName
    )

    if (!$Urls)
    {
        return ""
    }

    $clauses =  (($Urls | ForEach-Object { "shExpMatch(host, `"$_`")" }) -Join "$bl        || ")

@"
    if($clauses)
    {
        return $ReturnVarName;
    }
"@
}

function Get-PacString
{
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [array[]] $MapVarUrls
    )

@"
// This PAC file will provide proxy config to Microsoft 365 services
//  using data from the public web service for all endpoints
function FindProxyForURL(url, host)
{
    var $directProxyVarName = "$DirectProxySettings";
    var $defaultProxyVarName = "$DefaultProxySettings";

$( if ($Lowercase) { "    host = host.toLowerCase();" })

$( ($MapVarUrls | ForEach-Object { Get-PACClauses -ReturnVarName $_.Item1 -Urls $_.Item2 }) -Join "$bl$bl" )

$( if (!$ServiceAreas -or $ServiceAreas.Contains('Skype')) { Get-TLEPacConfiguration })

    return $defaultProxyVarName;
}
"@ -replace "($bl){3,}","$bl$bl" # Collapse more than one blank line in the PAC file so it looks better.
}

##################################################################################################################
### Functions to get and filter endpoints
##################################################################################################################

function Get-TLEPacConfiguration {
    param ()
    $PreBlock = @"
    // Don't Proxy Teams Live Events traffic

    if(shExpMatch(host, "*.azureedge.net")
    || shExpMatch(host, "*.bmc.cdn.office.net")
    || shExpMatch(host, "*.ml.cdn.office.net")
    || shExpMatch(host, "*.media.azure.net"))
    {
        var resolved_ip = dnsResolveEx(host);

"@
    $TLESb = New-Object 'System.Text.StringBuilder'
    $TLESb.Append($PreBlock) | Out-Null

    if (![string]::IsNullOrEmpty($CdnEdgeNodesFilePath) -and (Test-Path -Path $CdnEdgeNodesFilePath)) {
        $CdnData = Get-Content -Path $CdnEdgeNodesFilePath -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json | Select-Object -ExpandProperty value | 
            Where-Object { $_.name -eq 'Premium_Verizon'} | Select-Object -First 1 -ExpandProperty properties | 
            Select-Object -ExpandProperty ipAddressGroups
        $CdnData | Select-Object -ExpandProperty ipv4Addresses | ForEach-Object {
            if ($TLESb.Length -eq $PreBlock.Length) {
                $TLESb.Append("        if(") | Out-Null
            }
            else {
                $TLESb.AppendLine() | Out-Null
                $TLESb.Append("        || ") | Out-Null
            }
            $TLESb.Append("isInNetEx(resolved_ip, `"$($_.BaseIpAddress)/$($_.prefixLength)`")") | Out-Null
        }
        $CdnData | Select-Object -ExpandProperty ipv6Addresses | ForEach-Object {
            if ($TLESb.Length -eq $PreBlock.Length) {
                $TLESb.Append("        if(") | Out-Null
            }
            else {
                $TLESb.AppendLine() | Out-Null
                $TLESb.Append("        || ") | Out-Null
            }
            $TLESb.Append("isInNetEx(resolved_ip, `"$($_.BaseIpAddress)/$($_.prefixLength)`")") | Out-Null
        }
    }
    $AzureIPsUrl = Invoke-WebRequest -Uri "https://www.microsoft.com/en-us/download/confirmation.aspx?id=56519" -UseBasicParsing -ErrorAction SilentlyContinue  | 
            Select-Object -ExpandProperty Links | Select-Object -ExpandProperty href | 
            Where-Object { $_.EndsWith('.json') -and $_ -match 'ServiceTags' } | Select-Object -First 1
    if ($AzureIPsUrl) {
        Invoke-RestMethod -Uri $AzureIPsUrl -ErrorAction SilentlyContinue | Select-Object -ExpandProperty values | 
            Where-Object { $_.name -eq 'AzureFrontDoor.Frontend' } | Select-Object -First 1 -ExpandProperty properties |
            Select-Object -ExpandProperty addressPrefixes | ForEach-Object {
                if ($TLESb.Length -eq $PreBlock.Length) {
                    $TLESb.Append("        if(") | Out-Null
                }
                else {
                    $TLESb.AppendLine() | Out-Null
                    $TLESb.Append("        || ") | Out-Null
                }
                $TLESb.Append("isInNetEx(resolved_ip, `"$_`")") | Out-Null
            }
    }
    if ($TLESb.Length -gt $PreBlock.Length) {
        $TLESb.AppendLine(")") | Out-Null
        $TLESb.AppendLine("        {") | Out-Null
        $TLESb.AppendLine("            return $directProxyVarName;") | Out-Null
        $TLESb.AppendLine("        }") | Out-Null
    }
    else {
        $TLESb.AppendLine("        // no addresses found for service via script") | Out-Null
    }
    $TLESb.AppendLine("    }") | Out-Null
    return $TLESb.ToString()
}

function Get-Regex
{
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $Fqdn
    )

    return "^" + $Fqdn.Replace(".", "\.").Replace("*", ".*").Replace("?", ".?") + "$"
}

function Match-RegexList
{
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $ToMatch,

        [Parameter(Mandatory = $false)]
        [string[]] $MatchList
    )

    if (!$MatchList)
    {
        return $false
    }
    foreach ($regex in $MatchList)
    {
        if ($regex -ne $ToMatch -and $ToMatch -match (Get-Regex $regex))
        {
            return $true
        }
    }
    return $false
}

function Get-Endpoints
{
    $url = $baseServiceUrl
    if ($TenantName)
    {
        $url += "&TenantName=$TenantName"
    }
    if ($ServiceAreas)
    {
        $url += "&ServiceAreas=" + ($ServiceAreas -Join ",")
    }
    return Invoke-RestMethod -Uri $url
}

function Get-Urls
{
    param(
        [Parameter(Mandatory = $false)]
        [psobject[]] $Endpoints
    )

    if ($Endpoints)
    {
        return $Endpoints | Where-Object { $_.urls } | ForEach-Object { $_.urls } | Sort-Object -Unique
    }
    return @()
}

function Get-UrlVarTuple
{
    param(
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string] $VarName,

        [Parameter(Mandatory = $false)]
        [string[]] $Urls
    )
    return New-Object 'Tuple[string,string[]]'($VarName, $Urls)
}

function Get-MapVarUrls
{
    Write-Verbose "Retrieving all endpoints for instance $Instance from web service."
    $Endpoints = Get-Endpoints

    if ($Type -eq 1)
    {
        $directUrls = Get-Urls ($Endpoints | Where-Object { $_.category -eq "Optimize" })
        $nonDirectPriorityUrls = Get-Urls ($Endpoints | Where-Object { $_.category -ne "Optimize" }) | Where-Object { Match-RegexList $_ $directUrls }
        return @(
            Get-UrlVarTuple -VarName $defaultProxyVarName -Urls $nonDirectPriorityUrls
            Get-UrlVarTuple -VarName $directProxyVarName -Urls $directUrls
        )
    }
    elseif ($Type -eq 2)
    {
        $directUrls = Get-Urls ($Endpoints | Where-Object { $_.category -in @("Optimize", "Allow")})
        $nonDirectPriorityUrls = Get-Urls ($Endpoints | Where-Object { $_.category -notin @("Optimize", "Allow") }) | Where-Object { Match-RegexList $_ $directUrls }
        return @(
            Get-UrlVarTuple -VarName $defaultProxyVarName -Urls $nonDirectPriorityUrls
            Get-UrlVarTuple -VarName $directProxyVarName -Urls $directUrls
        )
    }
}

##################################################################################################################
### Main script
##################################################################################################################

$content = Get-PacString (Get-MapVarUrls)

if ($FilePath)
{
    $content | Out-File -FilePath $FilePath -Encoding ascii
}
else
{
    $content
}

腳本會根據 AzureFrontDoor.Frontend下載 URL 和金鑰自動剖析 Azure 清單,因此不需要手動取得該清單。

同樣地,我們不建議只使用 FQDN 來執行 VPN 卸除; 同時利用 函式中的 FQDN 和 IP 位址,有助於將此卸除的使用範圍限定為一組有限的端點,包括即時事件/串流。 函式的結構化方式會導致 FQDN 的 DNS 查閱與用戶端直接列出的 FQDN 相符,也就是剩餘命名空間的 DNS 解析保持不變。

如果您想要限制卸除與 Teams 事件和 Stream 無關之端點的風險,您可以從設定中移除 *.azureedge.net 網域,因為這是所有 Azure CDN 客戶使用的共用網域。 缺點是使用 Stream 所提供之外部編碼器的任何事件都不會優化,但 Teams 內產生/組織的事件將會是。

3.在 VPN 上設定路由以啟用直接輸出

最後一個步驟是新增 Teams 事件 IP 的直接路由,如將 CDN 端點的目前清單收集 到 VPN 組態中所述,以確保流量不會透過強制通道傳送至 VPN。 如需如何針對 Microsoft 365 優化端點執行這項操作的詳細資訊,請參閱為 Microsoft 365 實作 VPN 分割通道的實作 VPN 分割通道一節。 此程式與本檔中列出的 Stream 或 Teams 事件 IP 完全相同。

請注意,只有收集 目前CDN端點清單 (不是 FQDN) 的IP應該用於 VPN 組態。

常見問題集

這會將我所有的流量直接傳送至服務嗎?

否,這會傳送 Teams 事件或串流視訊直接傳輸的延遲敏感串流流量,如果任何其他流量未解析為發佈的 IP,則會繼續使用 VPN 通道。

我需要使用 IPv6 位址嗎?

否,連線能力只能在必要時為IPv4。

為什麼這些IP未在 Microsoft 365 URL/IP 服務中發佈?

Microsoft 對於服務中的資訊格式和類型有嚴格的控制,以確保客戶可以可靠地使用資訊,根據端點類別實作安全且最佳的路由。

默認端點類別沒有提供IP資訊,原因有很多 (預設端點可能不在 Microsoft 的控制範圍之外、變更頻率過高,或可能位於與其他元素共用) 區塊中。 基於這個理由,預設端點的設計目的是要透過 FQDN 傳送至檢查中的 Proxy,例如一般 Web 流量。

在此情況下,上述端點是CDN,可能由即時事件或串流以外的非 Microsoft 控制元素使用,因此傳送流量直接傳輸也表示也會直接從用戶端傳送解析為這些IP的任何專案。 由於目前全球危機的獨特本質,以及為了符合客戶的短期需求,Microsoft 已提供上述資訊,讓客戶能夠視方式使用。

Microsoft 正致力於重新設定 Teams 事件端點,以允許它們在未來包含在 [允許/優化端點] 類別中。

我是否只需要允許存取這些IP?

否,存取 URL/IP 服務中所有必要標示的端點對於服務運作而言是不可或缺的。 此外,任何標示為 Stream (識別碼 41-45) 的選擇性端點都是必要的。

此建議將涵蓋哪些案例?

  1. Teams 應用程式內產生的即時活動
  2. 檢視串流裝載的內容
  3. 產生事件) 外部裝置 (編碼器
  4. Teams 全體大會

此建議是否涵蓋演示者流量?

但不行;上述建議僅適用於取用服務的人員。 從 Teams 內進行簡報時,演示者的流量會流向 URL/IP 服務第 11 列中列出的優化標示 UDP 端點,其中包含 Microsoft 365 實作 VPN 分割通道實作 VPN 分割通道一節中所述的詳細 VPN 卸除建議。

此設定是否有直接傳送全體大會、即時活動 & Stream 以外的流量風險?

是,由於用於服務某些元素的共用 FQDN,這是無法避免的。 此流量通常會透過可套用檢查的公司 Proxy 傳送。 在 VPN 分割通道案例中,同時使用 FQDN 和 IP 會將此風險的範圍降到最低,但仍存在。 客戶可以從卸除設定中移除 *.azureedge.net 網域,並將此風險降到最低,但這會移除 Stream 支援的即時活動卸除, (Teams 排程、串流編碼器事件、Teams 中產生的 Viva Engage 活動、Viva Engage 排程的 Stream 編碼器事件,以及 Stream 排程事件或從 Stream) 隨選檢視。 Teams (中排程和產生的活動,包括全體大會) 不會受到影響。

概觀:Microsoft 365 的 VPN 分割通道

實作 Microsoft 365 的 VPN 分割通道

Microsoft 365 的常見 VPN 分割通道案例

保護 VPN 分割通道的 Teams 媒體流量

適用於中國使用者的 Microsoft 365 效能優化

Microsoft 365 網路連線能力準則

評定 Microsoft 365 網路連線

Microsoft 365 網路和效能微調

在今日獨特的遠端工作情境中,安全專業人員與 IT 達到現代安全控制的另一種方法 (Microsoft 安全小組部落格) (英文)

Microsoft 強化 VPN 效能:使用 Windows 10 VPN 設定檔以允許自動連線功能

在 VPN 上執行:Microsoft 如何使遠端員工保持聯繫 (英文)

Microsoft 全球網路