VPN 环境中的流和 Teams 事件的特殊注意事项

注意

本文属于解决远程用户 Microsoft 365 优化问题的系列文章。

Microsoft 365 实时事件与会者流量 (这包括 Teams 制作的实时活动的与会者,以及通过 Teams、Stream 或 Viva Engage) 使用外部编码器生成的与会者流量,Microsoft Teams 市政厅与会者流量和按需流与会者流量目前在服务的 URL/IP 列表中归类为默认优化。 这些终结点被归类为 默认 终结点,因为它们托管在 CDN 上,其他服务可能也使用。 客户通常倾向于代理此类流量,并应用通常在此类终结点上执行的任何安全元素。

许多客户要求提供直接从本地 Internet 连接将与会者连接到 Stream 或 Teams 活动的 URL/IP 数据,而不是通过 VPN 基础结构路由对高容量和延迟敏感的流量。 通常,如果没有专用命名空间和终结点的准确 IP 信息,这是不可能的,而分类为 默认的 Microsoft 365 终结点不提供这些信息。

使用以下步骤通过强制隧道 VPN 从客户端启用流或 Teams 事件服务的直接连接。 此解决方案旨在为客户提供一个选项,避免通过 VPN 路由事件与会者流量,同时由于在家办公方案而导致网络流量较高。 如果可能,我们建议通过检查代理访问服务。

注意

使用此解决方案时,可能会有一些服务元素无法解析为提供的 IP 地址,因此会遍历 VPN,但大量高容量流量(如流式处理数据)应该如此。 在实时事件/流范围之外,可能会有其他元素被此卸载捕获,但这些元素应受到限制,因为它们必须同时满足 FQDN IP 匹配,然后才能直接执行。

重要

建议权衡发送更多绕过 VPN 的流量的风险,以提升实时事件的性能。

若要为 Teams 事件和流实现强制隧道异常,应应用以下步骤:

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 生成的实时事件 (快速入门事件,以及从 Teams 客户端) 计划的 RTMP-In 支持事件。

*.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 文件更改)

对于在 VPN 上利用 PAC 文件通过代理路由流量的组织,通常使用 FQDN 来实现这一点。 但是,对于 Stream/Live Events/Town hall,提供的主机名包含通配符(如 *.azureedge.net),该通配符还包含无法提供完整 IP 列表的其他元素。 因此,如果仅根据 DNS 通配符匹配直接发送请求,则发往这些终结点的流量将被阻止,因为本文后面的 步骤 3 中没有通过直接路径进行路由。

为了解决此问题,我们可以提供以下 IP,并将其与示例 PAC 文件中的主机名结合使用,如 步骤 1 中所述。 PAC 文件检查 URL 是否与用于流/实时事件/市政厅的 URL 匹配,如果匹配,则还会检查从 DNS 查找返回的 IP 是否与为服务提供的 IP 匹配。 如果 两者都 匹配,则流量将直接路由。 如果 FQDN/IP) (任一元素不匹配,则将流量发送到代理。 因此,配置可确保解析为 IP 和已定义命名空间范围之外的 IP 的任何内容都像平常一样通过 VPN 遍历代理。

收集 CDN 终结点的当前列表

Teams 事件使用多个 CDN 提供程序流式传输到客户,以提供最佳覆盖范围、质量和复原能力。 目前,使用来自 Microsoft 和 Verizon 的 Azure CDN。 随着时间的推移,这种情况可能会因区域可用性等情况而更改。 本文旨在使你能够及时了解 IP 范围的最新动态。

对于来自 Microsoft 的 Azure CDN,可以从 Microsoft 官方下载中心下载 Azure IP 范围和服务标记 – 公有云 来下载列表,需要专门查找 JSON 中的服务标记 AzureFrontdoor.Frontend ; addressPrefixes 将显示 IPv4/IPv6 子网。 随着时间的推移,IP 可能会更改,但服务标记列表在投入使用之前始终会更新。

对于 Verizon (Edgecast) 的 Azure CDN,可以使用 Edge Nodes 找到详尽列表 - 列表 (选择“) 试用 ”,需要专门查找 “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 上配置路由以启用直接出口

最后一步是为将 当前 CDN 终结点列表收集 到 VPN 配置中所述的 Teams 事件 IP 添加直接路由,以确保流量不会通过强制隧道发送到 VPN。 有关如何为 Microsoft 365 优化终结点执行此操作的详细信息,请参阅为 Microsoft 365 实现 VPN 拆分隧道实现 VPN 拆分隧道部分。 对于本文档中列出的流或 Teams 事件 IP,此过程完全相同。

请注意,仅 IP (不) 收集 当前 CDN 终结点列表 的 FQDN 用于 VPN 配置。

常见问题

这是否会将我的所有流量直接发送到服务?

否,这将为 Teams 事件或流式传输视频直接发送对延迟敏感的流式处理流量,如果任何其他流量未解析为已发布的 IP,它们将继续使用 VPN 隧道。

是否需要使用 IPv6 地址?

否,仅在需要时连接才能为 IPv4。

为什么这些 IP 未在 Microsoft 365 URL/IP 服务中发布?

Microsoft 对服务中信息的格式和类型进行严格控制,以确保客户能够可靠地使用该信息来实现基于终结点类别的安全和最佳路由。

“默认终结点”类别没有提供 IP 信息的原因有很多, (默认终结点可能不受 Microsoft 控制,可能更改过于频繁,或者可能位于与其他元素共享的块中,) 。 因此,默认终结点设计为通过 FQDN 发送到检查代理,就像普通 Web 流量一样。

在这种情况下,上述终结点是可由非 Microsoft 控制的元素使用的 CDN,而不是实时事件或流,因此直接发送流量也意味着解析为这些 IP 的任何其他内容也将直接从客户端发送。 由于当前全球危机的独特性,并且为了满足客户的短期需求,Microsoft 提供了上述信息供客户根据需要使用。

Microsoft 正在努力重新配置 Teams 事件终结点,以允许它们在未来包含在“允许/优化”终结点类别中。

是否只需允许访问这些 IP?

否,访问 URL/IP 服务中的所有必需标记终结点对于服务运行至关重要。 此外,任何标记为 Stream (ID 41-45) 的可选终结点是必需的。

此建议将涵盖哪些方案?

  1. Teams 应用中生成的实时事件
  2. 查看流托管内容
  3. 外部设备 (编码器) 生成的事件
  4. Teams 大会堂

此建议是否涵盖演示者流量?

它不是;上述建议纯粹是针对那些使用该服务的人。 从 Teams 内部演示将看到演示者的流量流向 URL/IP 服务行 11 中列出的“优化标记的 UDP 终结点”,以及针对 Microsoft 365实现 VPN 拆分隧道的“实现 VPN 拆分隧道”部分中概述的详细 VPN 卸载建议。

此配置是否面临直接发送的市政厅、实时事件 & 流以外的流量的风险?

是的,由于共享 FQDN 用于服务的某些元素,这是不可避免的。 此流量通常通过公司代理发送,该代理可以应用检查。 在 VPN 拆分隧道方案中,同时使用 FQDN 和 IP 会将此风险范围缩小到最低,但它仍然存在。 客户可以从卸载配置中删除 *.azureedge.net 域,并将此风险降低到最低水平,但这将删除流支持的实时事件 (Teams 计划、流编码器事件、Teams 中生成的Viva Engage事件、Viva Engage计划的流编码器事件以及流) 中流计划事件或按需查看的卸载。 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 全局网络