header image

如何将UWP应用添加到Steam

虽然UWP现在半死不活的,但是自从有了Win32转制之后,还是有不少应用登录Microsoft Store的,再加上现在微软大力推游戏,因此将这些游戏添加进Steam就成了一个问题。

目标

说白了,将UWP应用添加到Steam就是利用PowerShell来执行一个命令

Start-Process <ProtocolName>

下面就介绍如何来找到这个<ProtocolName>

如果你的游戏安装在C盘下

其实这个方法可以用于安装在任何盘下的,只不过其他盘下我们有更简单粗暴的方法,但还是先看完这一部分,因为有的内容后面还要用到。

首先我们打开一个文件管理器窗口,在地址栏里输入%AppData%(或者打开运行窗口(Win+R)输入可以),回车,就来到了当前用户的AppData\Roaming目录下,我们先向上一层,来到AppData目录,再进入Local\Packages目录。

然后我们用文件管理器右上角的搜索框直接搜索游戏名称(注意!这一步可能要运行过游戏之后才能搜索到)比如我要找帝国时代决定版,就搜索Age(of Empires)。结果可以看到一个Age of Empires DE右键属性可以看到位置中,其在Packages下的Microsoft.MSDallas_8wekyb3d8bbwe目录里,这就是它的PackageFamilyName了。

其中,Microsoft.MSDallas就是这个Package的Name8wekyb3d8bbwePublisherId,也就是微软了),我们以管理员身份打开PowerShell(在开始按钮上点右键,然后选择Windows PowerShell (管理员)即可),然后输入命令回车执行:

2019年11月19日更新

由于帝国时代决定版包名变更,而且发现了更简单的方法,所以上面的方法作废。

首先,运行游戏。

然后,打开任务管理器(Ctrl+Alt+Delete然后点任务管理器,或者在任务栏上右键单击然后点任务管理器)。如果是简略模式,点击左下角的详细信息进入详细模式。

然后,我们可以在第一个进程选项卡中看到打开的游戏Age of Empires Definitice Edition (3),后面的(3)表示游戏有3个进程,点左边的展开箭头,可以看到三个进程,AoEDE就是主进程。我们在上面点右键,选转到详细信息,就会跳转到详细信息选项卡,并选中AoEDE.exe这个进程。但是我们点击右键、打开文件位置或者属性都是没有用的。这是我们要列表最上方的列名称上点右键、选择列,找到命令行并勾选、确定。然后我们就可以在命令行一列看到完整的路径了("C:\Program Files\WindowsApps\Microsoft.Darwin_100.1.28529.0_x64__8wekyb3d8bbwe\AoEDE.exe"),其中C:\Program Files\WindowsApps\Microsoft.Darwin__100.1.28529.0_x64_8wekyb3d8bbwe\就是我们要找的主目录。

如果不想手动输入那么长的目录,我们可以注意到,Microsoft.Darwin就是应用包的名称,我们可以通过命令来获得安装位置:

Get-AppxPackage <Name>

<Name>对于帝国时代决定版就是Microsoft.Darwin,回车运行就会得到完整的信息,包括InstallLocation。这就是游戏的安装目录了。我们执行下面的命令进入安装目录(Powershell窗口中选中内容后点击右键是复制,输入时点击右键是粘贴):

cd '<InstallLocation>'

注意<InstallLocation>两端要加上引号'",否则可能会有空格会导致命令失败。

然后我们可以用ls命令看到当前目录下的文件,这里我们需要的是appxmanifest.xml文件,我们执行下面的命令:

cat .\appxmanifest.xml

这个命令把appxmanifest.xml的内容显示到PowerShell中。我们可以找到其中的<Applications>标签,我们要的内容就在这里了,如帝国时代决定版的就是:

  <Applications>
    <Application Id="App" Executable="AoEDE.exe" EntryPoint="Windows.FullTrustApplication">
      <uap:VisualElements DisplayName="Age of Empires Definitive Edition" Square150x150Logo="SquareLogo_150x150.png" Square44x44Logo="SmallLogo_44x44.png" Description="Age of Empires: Definitive Edition" ForegroundText="light" BackgroundColor="transparent">
        <uap:SplashScreen Image="Splashscreen_1920x1080.png" />
      </uap:VisualElements>
      <Extensions>
        <uap:Extension Category="windows.protocol">
          <uap:Protocol Name="ms-xbl-68a451d4" />
        </uap:Extension>
        <uap:Extension Category="windows.protocol">
          <uap:Protocol Name="ms-xbl-multiplayer" />
        </uap:Extension>
      </Extensions>
    </Application>
  </Applications>

<Extensions>标签下,我们可以看到<uap:Protocol Name="ms-xbl-68a451d4" />ms-xbl-68a451d4就是我们要找的ProtocolName了。

另类方法

上面我们得到Package的Name之后(或者Get-AppxPackage <Name>得到更详细的PackageFullName),可以到注册表中的HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Local Settings\Software\Microsoft\Windows\CurrentVersion\AppModel\PackageRepository\Packages\<PackageFullName>\windows.protocol下看到我们要找的ProtocolName

或者在HKEY_CURRENT_USER\SOFTWARE\Classes\Extensions\ContractId\Windows.Protocol\<PackageFullName>\ActivatableClassId\<SomeString>\CustomProperties,选中之后右边的Name数据就是ProtocolName。其中<PackageFullName>就是PackageFullName<SomeString>是一串字符串,不用在意含义。

如果你的游戏安装在其他盘上

上面的方法依然有效,但我们有更简单粗暴的方法:

非常简单,其他盘上会有一个WindowsApps的目录,游戏就装在这里面了。我们还是以管理员身份打开PowerShell(在开始按钮上点右键,然后选择Windows PowerShell (管理员)即可),然后输入命令回车执行:

cd 'D:\WindowsApps'

上面的命令是进入目录,其中D是对应的盘符,根据实际情况修改。然后我们用下面的命令来查看子目录:

ls

如果你只安装了一个游戏,那么就很好办了,直接进入目录查看其中的appxmanifest.xml就可以了(方法见上面)。

如果你安装了多个游戏,一个一个打开appxmanifest.xml看吧,文件中的<DisplayName>标签中可以看到这个Package的名称。如果实在太多,那还是按照上面的在C盘中方法来吧。

添加到Steam

首先打开Steam,然后点主界面左下角的添加游戏,然后选择添加非Stema游戏,这时会打开一个添加游戏对话框,随便选一个程序即可。

然后找到我们刚才添加的程序,右键属性,上面的名称改成游戏名称,目标修改成"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"起始位置留空。

然后点设置启动选项,填写-WindowStyle "Minimized" -NoExit -Command "Start-Process <ProtocolName>:",其中<ProtocolName>替换成我们之前找到的内容,注意后面有一个英文冒号:WindowStyle参数指定PowerShell窗口的运行方式,这里设置为最小化MinimizedNoExit参数指定运行命令后不退出,退出了Steam就不会显示游戏中状态了,游戏过后我们需要手动关闭窗口。Command参数就是指定要执行的命令了。比如帝国时代决定版就是-WindowStyle "Minimized" -NoExit -Command "Start-Process ms-xbl-multiplayer:"。设置好之后确定

然后关闭属性窗口,就可以运行游戏了。

最后

没图标总觉得不好看,可以自己去找图标,然后修改了就行。

Steam跨区指南

由于厂商的销售政策或其他一些众所周知的原因,Steam上的游戏在国区不一定买得到,因此需要通过一些手段购买,下面就来说一说。

首先

你需要准备一个美国的代理。虽然也可以跨到其他区,但是不推荐,因为如果不小心(或者说故意)跨到了低价区,等待你的很可能是游戏被收回,甚至账号被封禁。美区由于是全球区,还是高价区,G胖也不会跟钱过不去,所以相对来说比较安全。

Sub大法

严格来说,这个并不算跨区,但有的商品确实可以通过这种手段购买,因此也说一说。

Steam上的商品链接其实有三种,一种就是最基本的游戏或DLC(或其他程序、视频等,以下简称为App),比如https://store.steampowered.com/app/393080/Call_of_Duty_Modern_Warfare_Remastered/393080就是这个App的AppID,可以说是Steam上最基本的SKU(库存单位)了。后面的Call_of_Duty_Modern_Warfare_Remastered是App名称,其实可以不要,也就是说通过https://store.steampowered.com/app/393080照样可以访问。

接着是Bundle,这是捆绑包,也就是将多个App打包销售。比如https://store.steampowered.com/bundle/9193/Sid_Meiers_Civilization_VI_Gold_Edition/,可以看到形式和上面的App链接差不多,自然中间的9193就是BundleID了。

然后就是Sub了,Sub其实是用来区分不同的区域和销售版本的,一个App可以对应多个Sub。理论上你访问相应的区域时,购买App时就是购买相应的Sub。有的Sub可以用链接直接访问购买,比如https://store.steampowered.com/sub/306734/(对应的App是无法直接访问的https://store.steampowered.com/app/943690/),自然,最重要的就是如何获得这个SubID

这里我们就要利用一个大杀器https://steamdb.info/,这个网站是自动抓取Steam上的数据然后整理成数据库的。我们可以进入网站后在搜索框里通过内容的名称(默认就是搜索Sub)或AppID来搜索,或者直接通过类似Steam的链接格式访问(比如https://steamdb.info/app/943690/,就是上面的App对应的SteamDB页面了,然后我们可以在左侧找到PricesInformation等,点击其中的Packages,就可以看到这个App对应的Sub了(或者直接点,https://steamdb.info/app/943690/subs/)。

SUBID一列就不用解释了,链接点了之后可以进入Sub的SteamDB页面,NAME一列的右边如果有个地球图标的话,表示这是在Steam商店里销售的Sub。如果有多个在售的Sub,就要看清楚名称是什么版本的。点左侧SUBID的链接进去后看Sub的Information里有没有锁区、限区等,如果没问题,点页面上Store(或者自己构造Steam的Sub链接)来访问购买了。

对于锁区、限区的App或Sub,我们只能通过下面的方法来访问了。

切换区域法

有的App,或者搜索无法搜到,或者锁区限区。就算通过其他方法获得了链接,甚至挂上了代理,也还是显示“您所在的国家/地区不允许看到此内容。”。比如https://store.steampowered.com/app/947650/。可以发现有的访问之后链接中多了个agecheck,原来这些游戏有不可描述的内容,而国区是直接屏蔽的这样的年龄验证的。锁区限区则是有的直接不给访问,有的访问之后无法购买。

这个时候,我们先挂上代理,然后随便加一件东西到购物车里。然后进入购物车,这时可以发现,购物车商品列表的右上角多了个国家/地区的切换。

点击之后,选项有之前购买过的地区(也就是当前的钱包区域)、现在访问的地区(也就是代理的地区)和其他。如果挂的是美国代理,这里就可以切换成美国了。

然后我们再去上面的链接,就会发现可以访问了,我们可以将它加入购物车。

等等!还没完!我们再进入购物车,再把区域切换回中国,可以发现,这个商品仍然在购物车里,而且还有国区价格。我们这时只要愉快地购买就行了。

当然,不是所有内容都可以切换回国区购买,有的内容切换回国区就从购物车里消失了。这说明是发行商实行了限区政策(前面的是因为有年龄限制,但发行商并没有设置限区)。那我们先试试下面的方法,如果还不行,我们只能从美国区以高价购买了。

如果切换到了美国区购买,购买之后钱包区域会切换为美国,这个时候就不能接收中国区礼物了。我们需要用上面的方法再在中国区购买之后,才能把钱包区域切换回中国。

总结

基本上,这两种方法结合,已经能解决大多数情况了。跨美国区也相对安全(虽然价格贵一些)。虽然上述方法也适用于其他低价区,但是不建议跨低价区,收回或者封禁那就得不偿失了。对于俄区低价区,其实可以去yuplay看看,有的游戏购买不锁区,但是激活和运行锁区的,可以通过特殊手段激活和运行(或者激活后等三个月解锁运行)。以后有机会再说吧。

内网穿透之frp

  • 2018-06-23更新:
    由于新版(0.18.0开始)和之前的版本不兼容,因此针对新版做了一些补充。补充的内容基于0.20.0版本。

  • 2019-05-09更新:
    更改了一些文件的保存位置。

之前讲过ngrok,但是那个需要自己编译,也已经很久很没有更新了(作者开发了ngrok 2,但是变成私有软件了)。所以这次我们换一个大概是中国人编写的frp,作者提供了详细的中文文档,上手十分简单。

frp的GitHub页面,我们可以在Releases页面下载到编译好的版本。提供各种架构的版本:darwin是macOS,linuxwindows就不用多说了,386就是32位平台,amd64就是64位平台,其他还有armmips等多种平台。如果没有还可以自己编译,但不在本文涉及范围。

下载解包之后,会得到几个文件,frps就是服务器端程序,frpc就是客户端程序,后缀ini的就是对应的配置文件,full的就是完整的配置文件。

服务器端配置

基本配置

frpsfrps.ini上传到服务器,让后放到某个目录下(我放到了/opt/frps/目录下)。然后修改frps.ini文件(你也可以先修改了再上传)。

基本上只要一条设置就够了(每条选项一行,=之前是选项名,之后是值,[]内是选项块的名字):

[common]
bind_port = 7000

bind_port就是绑定的端口,设置一个没有被占用的就行了。值为端口号的整数。
如果服务器有多个IP地址,我们还可以设置bind_addr来绑定特定的IP地址。值为点分十进制的IPv4地址(如1.2.3.4)。

2018-06-23更新:新版增加了IPv6支持,因此值还可以设置为IPv6地址(必须用方括号[],如[2400::1:23:456:7890])。

PS:由于Go语言的特点,0.0.0.0[::]是一样的效果,会同时监听IPv4和IPv6地址,但在用netstat里只能看到IPv6被监听,实际上是通过了IPv4-Mapped IPv6 Address同时兼听了IPv4地址的。

如果要开启kcp,我们还可以设置kcp_bind_port选项。值为端口号。
为了增加安全性,我们可以加上密码privilege_token选项。值为字符串。

2018-06-23更新:从0.17.0版开始,privilege_token不再支持,请改为token

其他更多选项及相关说明请参见frps_full.ini文件。

然后我们只要运行如下命令就可以了:

/opt/frps/frps -c /opt/frps/frps.ini

开机自动运行

当然,我们要让它开机自动运行,如果系统使用的是systemd来进行初始化,我们只需要编写一个service文件(保存为/lib/systemd/system/frps.service)就行了:

2019-05-09更新:
请将文件保存到/etc/systemd/system/frps.service

[Unit]
Description=frp server
After=network.target

[Service]
Type=simple
ExecStart=/opt/frps/frps -c /opt/frps/frps.ini
Restart=on-failure

[Install]
WantedBy=multi-user.target

然后照例启用和启动服务就行(逐条执行):

sudo systemctl enable frps
sudo systemctl start frps
sudo systemctl status frps

上面最后一条命令是查看服务状态的,如果是active就是成功运行了。

客户端配置

服务器端配置很简单,客户端配置就要复杂一点了。客户端里的配置文件一样是上面介绍的格式,下面我们以[]包含的不同块来分开介绍。注意,所有选项都在一个配置文件里。

如果是运行在NAS上的,同样上传到NAS之后使用(我上传到了/opt/frpc/目录)。

[common]

如同名字所说,这里都是一些公共配置,基本上如下:

[common]
server_addr = 1.2.3.4
server_port = 7000
admin_addr = 127.0.0.1
admin_port = 7400

server_addr就是我们的服务器IP地址,你也可以用域名,但是不推荐。
server_port就是我们在上面bind_port设置的端口号。
admin_addradmin_port是为了能够热加载配置而设置的,监听本机(127.0.0.1)的7400端口。
如果启用了密码,设置privilege_token和上面服务器设置的一样就行了。

2018-06-23更新:从0.17.0版开始,privilege_token不再支持,请改为token

如果上面设置了kcp_bind_port,我们还可以用protocol = kcp来启用kcp协议。
其他更多选项及相关说明请参见frps_full.ini文件。

公共设置就这些了,基本上这样就能用了。下面我们来设置具体的端口映射。

最基本的映射

基本的映射只要设置一个唯一的块名,然后下面做对应的设置就行了。比如我们设置SSH访问:

[ssh]
type = tcp
local_ip = 127.0.0.1
local_port = 22
remote_port = 23333

[ssh]就是一个名称,我们自己设置就行了,用[]包上即可,下面的选项都是对应在这个块下的。一个ini可以包含多个设置。
type就是类型,大多数服务都是tcp的。除此之外还支持udphttp服务(当然,http也是tcp的一种,有这个只是为了方便虚拟主机的设置,详见GitHub上的说明),还有一个stcp我们下面会提到。
local_ip就是我们想转发到外网的本地主机的IP,127.0.0.1代表的就是本机。
local_port就是服务的端口了,22是ssh的默认端口。
remote_port就是我们想在服务器上打开的端口,比如我这里设置23333
如果需要加密,我们可以在每个映射下设置use_encryption = true
如果需要压缩,我们可以设置use_compression = true
其他更多选项及相关说明请参见frps_full.ini文件。

这样,当我们启动客户端之后,就可以通过访问服务器的23333端口来访问实际运行在本地主机的22端口上的SSH服务了。下面是一个Transmission远程访问的配置示例:

[transmission]
type = tcp
local_ip = 127.0.0.1
local_port = 9091
remote_port = 9091

当然,有时我们觉得这样直接把服务暴露给公网太不安全,那怎么办呢,上面提到过的stcp就是一个解决方案。

安全的映射

安全的映射我们需要两个客户端,一个运行在提供服务的主机上(简称A),一个运行在访问服务的主机上(简称B),通过服务器中转访问,服务器并不直接开放端口。

在提供服务的主机(A)上设置

我们还是以ssh服务为例:

[secret_ssh]
type = stcp
local_ip = 127.0.0.1
local_port = 22
sk = abcdef

前三项配置和前面的基本一样,就是type设置成了stcp。但最后我们不再设置remote_port,而是设置了一个sk,这是安全访问的密码,只有密码一致才能访问。

运行客户端之后,我们就可以通过另一个客户端来访问了。

在访问服务的主机(B)上设置

我们在另一台主机(B)上同样设置frpc.ini[common]部分是一样的,但是我们设置的并不是为了提供服务,而是访问服务,所以有所不同

[common]
server_addr = 1.2.3.4
server_port = 7000

[secret_ssh_vistor]
type = stcp
role = vistor
server_name = secret_ssh
sk = abcdef
bind_addr = 127.0.0.1
bind_port = 23333

type一样是stcp
多了个role = vistor表示我们是访客而不是提供服务的(吐个槽,作者这里错成vistor了,怎么想都是visitor才对嘛)。
2018-06-23更新:作者已经修正为了,请使用visitor作为正确的值。
然后server_name要和我们上面在主机A上设置的[]内的名字一样。
sk也要和上面一样。
bind_addr表示绑定在本机(B)上的IP地址,你可以绑定0.0.0.0或者具体的IP地址来再向局域网内提供访问。
bing_port就是绑定本机的端口了。

运行客户端之后,我们就可以访问本机(B)的23333端口,来访问主机A上的22端口的SSH服务了。

点对点映射

2018-06-23新增此节

新版增加了点对点映射,设置方法和安全映射差不多,仅type的值设置为xtcp

点对点映射时,连接不通过服务器,而是直接在两个主机(A和B)之间建立,但并不能支持所有网络环境,如果不能正常工作,还是需要使用安全映射。

自动运行客户端

我们可以先运行一下看看有没有什么错误。

/opt/frpc/frpc -c /opt/frpc/frpc.ini

因为我是运行在NAS上的,所以还是要自动运行。设置起来和服务器基本是一摸一样的,只不过把frps改成了frpc

如果系统使用的是systemd来进行初始化,我们只需要编写一个service文件(保存为/lib/systemd/system/frpc.service)就行了:

2019-05-09更新:
请将文件保存到/etc/systemd/system/frpc.service

[Unit]
Description=frp client
After=network.target

[Service]
Type=simple
ExecStart=/opt/frpc/frpc -c /opt/frpc/frpc.ini
ExecReload=/opt/frpc/frpc -c /opt/frpc/frpc.ini --reload
Restart=on-failure

[Install]
WantedBy=multi-user.target

多了一个ExecReload选项是为了可以热加载配置,因为我们主要都是修改客户端配置,这样可以方便修改之后立即生效。

然后照例启用和启动服务就行(逐条执行):

sudo systemctl enable frpc
sudo systemctl start frpc
sudo systemctl status frpc

上面最后一条命令是查看服务状态的,如果是active就是成功运行了。如果我们需要热加载配置,只需要用命令sudo systemctl reload frpc就可以了。

补充

如果是运行在Windows上的,网上有很多资料如何自动运行,就不细说了。如果只是临时使用(比如是作为上面提到的安全映射里的主机B),那么只需要在命令行执行代码就行了(打开命令行的方式:在文件管理器的frpc.exe所在文件夹里按住Shift点右键,然后选择在此处打开命令行提示符/Powershell窗口)。

frpc -c frpc.ini

2018-06-23更新:新版可以直接用命令行参数启动,不需要建立ini文件。如:

frpc tcp -s "127.0.0.1:7000" -n ssh -l 22 -r 6000

具体参数含义和用法请使用frpc -h命令查看。

最后

感谢frp作者fatedier的工作,大家如果有问题咨询作者或者想捐款给作者,可以在GitHub上的说明的最后找到相关的联系方式。

Ngxin HTTPS代理Transmission Web UI

在上一篇文章中,我设置了用ngrok转发端口来实现对内网NAS的访问,但是如果转发用的是国内的VPS,会由于域名没有备案而被关停。解决方法一个是搞个备案过的域名。现在阿里云等云服务都有便宜的域名,所以这不是大问题。还有个方法就是绕开HTTP访问,比如我这里用的HTTPS。

方法简述

就是在NAS上建立一个Nginx服务,然后启用HTTPS(这部分就不详细介绍了,网上文章很多,我配置的是8443端口以免常用端口被检测),然后通过代理将访问路径/transmission/的请求转发到NAS的9091端口上。

初步方案

一开始就直接做了个转发,在Nginx的网站配置如下:

    location /transmission {
        proxy_pass: http://127.0.0.1:9091;
    }

但是这样却出现了问题,由于缺少Transmission的一个特有的请求头会造成409错误,而且不知为啥有静态文件无法获取。

改进方案

有这种需求的肯定不止我一个,所以马上就放狗搜了一下,还真找到了一篇文章,把上面的配置改成如下:

    location ^~ /transmission {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header X-NginX-Proxy true;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_pass_header X-Transmission-Session-Id;
        add_header   Front-End-Https   on;

        location /transmission/rpc {
            proxy_pass http://127.0.0.1:9091;
        }

        location /transmission/web/ {
            proxy_pass http://127.0.0.1:9091;
        }

        location /transmission/upload {
            proxy_pass http://127.0.0.1:9091;
        }

        location /transmission/web/style/ {
            alias /usr/share/transmission/web/style/;
        }

        location /transmission/web/javascript/ {
            alias /usr/share/transmission/web/javascript/;
        }

        location /transmission/web/images/ {
            alias /usr/share/transmission/web/images/;
        }

        location /transmission/ {
            return 301 https://$server_name/transmission/web;
        }
}

这样就行了,今后只要访问VPS的/transmission/(如http://example.com:8443/transmission)就和直接访问Transmission的9091端口一样了。

更进一步

但是这样还是能直接访问8443端口,能不能让这个端口不能直接访问呢,答案是可以。

我们需要用到cookie,cookie是什么就不多解释了。我们只要在Nginx的网站配置中最初的location之前添加如下内容就可以了。

    if ($http_cookie !~ "key=abcdef") {
        reutrn 444;
    }

上面配置的意思是,访问网站时的请求要包含一个名称是key、值是abcdef的cookie(这两个参数可以自己设定),否则就抛弃请求。444是一个特殊的错误码,Nginx遇到这个错误码会直接抛弃请求(就好像没有服务似的),而不会真正地返回错误。

这样,我们只要用一些特殊手段(Firefox或者Chrome的开发者工具或者一些附加组件/扩展程序)给网站添加一个名称是key、值是abcdef的cookie,就能访问了,而没有这个cookie时访问就好像服务不存在一样,彻底断绝检测到未备案网站的可能性。

内网穿透之ngrok [2017-10-13 UPDATED]

2017年10月13日更新:转发其他IP的端口

鉴于IPv4地址资源的枯竭,而电信公司又不部署IPv6,越来越多的宽带用户难以获得公网IP,导致NAS、私有云等应用难以实现,所以我们需要一种有效的能穿透NAT的工具。

省事的方法

目前有很多提供NAT穿透服务的网站,比如老牌的花生壳,以及NAT123等等。但缺点显而易见——免费的服务限制太多(限端口、限带宽),收费的又太贵。

还有一些基于这次提到ngrok提供服务的,虽然收费便宜点,但还是不能随心所欲。

ngrok介绍

ngrok是国外一个开发者基于GO语言写的服务,并且通过ngrok.com提供服务,不过1.0版本的软件是开源的(虽然停止了更新)。上面说到的基于ngrok和我们这次要架设的服务,都是基于1.0版本的。

准备工作

首先你要有自己的服务器(运行Linux的VPS),用来转发请求,然后你需要穿透NAT的设备要能够运行第三方的程序。最后你需要一台Linux的电脑(当然你在服务器上执行操作也可以,Windows 10的WSL也是不错的选择)。下面的代码会说明是在服务器本地机器还是需要穿透NAT的设备上执行。

所有的Linux操作都是基于Ubuntu或Debian,其他系统的操作大同小异,如果不会请留言。

你需要给你的服务器绑定一个域名,对于大多数人这不是问题了。如果你要用到子域名的话,再把子域名的泛解析绑定。

比如绑定了example.com的域名,如果要用子域名的话就要添加*的泛解析,DNS服务商都会有说明的,请详细阅读。

获取源代码

我们首先要在本地机器上部署编译环境:

sudo apt-get install build-essential golang mercurial git

然后获取源代码:

git clone https://github.com/inconshreveable/ngrok.git

这样会把源代码克隆岛ngrok目录下,我们切换一下目录:

cd ngrok

创建证书

因为ngrok的隧道是通过TLS传输数据的,我们可以用默认的证书,但这样谁都可以连接你的服务器了,所以我们要自己创建证书。

以下代码在本机一条一条地执行。第一条中的example.com记得改成你自己的域名。

NGROK_DOMAIN="example.com"
openssl genrsa -out base.key 2048
openssl req -new -x509 -nodes -key base.key -days 10000 -subj "/CN=$NGROK_DOMAIN" -out base.pem
openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=$NGROK_DOMAIN" -out server.csr
openssl x509 -req -in server.csr -CA base.pem -CAkey base.key -CAcreateserial -days 10000 -out server.crt

我们需要的是server.keyserver.crtbase.pem三个文件。我们要放到对应的目录里:

cp server.key assets/server/tls/snakeoil.key
cp server.crt assets/server/tls/snakeoil.crt
cp base.pem assets/client/tls/ngrokroot.crt

修改代码

因为ngrok会默认监听IPv6端口,考虑到国内的网络环境,我们需要修改一下代码,否则将无法使用。主要修改两个地方,一个是src/ngrok/server/tunnel.go的109行net.ListenTCP后面的"tcp"改成"tcp4",另一处是src/ngrok/conn/conn.go的57行的net.Listen后的"tcp"同样改成"tcp4"

编译

编译很简单,我们首先通过GOOSGOARCH两个变量指定编译的目标系统和目标架构,目标系统有darwin(即OS X)、freebsdlinux,目标架构有386(32位系统)、amd64(64位系统)和arm(ARM嵌入式系统)。然后再通过serverclientrelease-serverrelease-client来指定编译的是客户端还是服务器端。有没有release的区别是,包含release的编译结果会把assets目录下的内容包括进去,从而可以独立执行。如果你今后还要更换证书,建议编译不包含release的版本。

以下代码都在本机执行。

编译服务器:

GOOS=linux GOARCH=amd64 make release-server

得到的服务器文件是bin/ngrokd

编译客户端,因为我的NAS是ARM架构的所以如下,各位请按自己的需求编译:

GOOS=linux GOARCH=arm make release-client

得到的客户端文件是bin/linux_arm/ngrok

不同系统或架构编译得到的文件位置可能不同,但都在bin目录下,并且文件名服务器是ngrokd客户端是ngrok,下面请根据实际情况操作。

配置服务器

首先把文件复制到服务器上。请选择你熟悉的手段。我是直接scp上传:

scp bin/ngrokd example.com:~

然后就要到服务器上操作了,通过SSH连接到服务器上。

首先把文件放到/opt/ngrokd目录下(这步可根据你的习惯来):

sudo mkdir /opt/ngrokd
sudo cp ngrokd /opt/ngrokd/

然后我们就要建立服务了。我使用的是Ubuntu 16.04 LTS,所以用systemd很简单,把如下内容保存到/lib/systemd/system/ngrokd.service这个文件(自己创建):

[Unit]
Description=ngrok server
After=network.target

[Service]
Type=simple
ExecStart=/opt/ngrokd/ngrokd -domain example.com -httpAddr "" -httpsAddr "" -tunnelAddr ":4443" -log "/var/log/ngrokd.log"
Restart=on-failure

[Install]
WantedBy=multi-user.target

其中ExecStart这一项后面就是我们ngrokd执行命令了。参数-domain表示服务器域名,请改成你自己的域名-httpAddr表示默认监听的HTTP端口,-httpsAddr表示默认监听的HTTPS端口,因为我用不到所以都设置成空字符串""来关闭监听,如果需要打开的话记得格式是:12345(冒号+端口号)这样的;-tunnelAddr表示服务器监听客户端连接的隧道端口号,格式和前面一样;-log表示日志文件位置;还有个-log-level用来控制日志记录的事件级别,选项有DEBUGINFOWARNINGERROR

如果编译的是不带release的版本,还可以通过-tlsCrt-tlsKey选项来指定证书文件的位置。

设置好之后,我们就要启用服务了,很简单:

sudo systemctl enable ngrokd

其中enable是命令表示启用服务(开机会自动启动),其他命令还有disable(禁用服务)、start(启动服务)、restart(重启服务)、stop(停止服务)、status(查看服务状态)等。所以我们就启动服务:

sudo systemctl start ngrokd

然后通过status查看服务,如果是active (runing)那么就OK了。

配置设备

先把从本机把客户端复制到我们的设备(这里我的是NAS)上。请选择你熟悉的手段。我还是一样使用scp

scp bin/linux_arm/ngrok 192.168.11.50:~

接下来我们就要到设备上操作了,同样通过SSH连接到设备上。

首先还是一样的把程序复制到/opt/ngrok目录下(这步同样可根据你的习惯来):

sudo mkdir /opt/ngrok
sudo cp ngrok /opt/ngrok/

然后还是一样的建立服务,因为我的NAS运行的是Debian,还是用systemd,操作和服务器上几乎一样。把如下内容保存到/lib/systemd/system/ngrok.service这个文件:

[Unit]
Description=ngrok client
After=network.target

[Service]
Type=simple
ExecStart=/opt/ngrok/ngrok -config "/opt/ngrok/ngrok.yml" -log "/var/log/ngrok.log" start transmission ssh
Restart=on-failure

[Install]
WantedBy=multi-user.target

同样还是看ExecStart后面的命令,这里加载了一个配置文件/opt/ngrok/ngrok.yml(之后我们会建立),-log记录日志和服务器端是类似的,最后用start transmission ssh启动了配置文件中的transmissonssh两个隧道。

那么配置文件是什么样呢?我们把如下内容保存到/opt/ngrok/ngrok.yml中:

server_addr: example.com:4443
trust_host_root_certs: false
tunnels:
  transmission:
    remote_port: 9091
    proto:
      tcp: 9091
  ssh:
    remote_port: 23333
    proto:
      tcp: 22

server_addr就是我们在服务器配置的域名和端口。

tunnels下就是隧道设置。

每一级前面多两个空格缩进的表示是上一级的子项。比如transmissionssh就是tunnels的子项,remote_portproto又是他们的子项,以此类推。

transmissionssh就是隧道的名称,每个对应一个隧道。上面的启动命令中start后面的就是这个。

remote_port表示远程对应的端口。

proto表示协议,支持的有tcphttphttps。因为我没有开启服务器上的http和https,所以我都是用的tcp(TCP也可以支持HTTP或HTTPS,之所以有独立的http和https选项是为了简化设置,直接对应服务器上的相应设置)。

tcp就是协议,后面跟着的是端口号。

所以上面的设置文件就表示,将服务器的9091端口的连接转发到设备的9091端口上,服务器的23333端口的连接转发到设备的22端口上。这样我们就可以通过访问服务器的相应端口来访问设备上的服务了。

2017年10月13日更新

还可以转发其他IP的端口,方法就是在proto下的tcp(或httphttps)后的端口号写成IP地址:端口号的格式(中间是英文冒号)。

如:

      tcp: 192.168.11.1:80

最后别忘了启用服务:

sudo systemctl enable ngrok
sudo systemctl start ngrok

最后

这样,我们就将设备上的9091端口和22端口的对应到了服务器的9091和23333端口上了。比如我要访问设备的SSH,就可以这样:

ssh -p23333 example.com

这样有个缺点,就是只能建立到运行ngrok的设备的隧道,如果你的设备不能运行ngrok,那就有点麻烦了。 2017年10月13日更新:其实是可以转发其他IP的端口的,详见上面的配置设备一节。

如果不能运行ngrok的设备提供的是HTTP或者HTTPS访问,还有一种变通的方法是在运行ngrok的设备(比如树莓派)上通过nginx的建立代理,代理到不能运行ngrok的设备上。

虽然有局限,但我们已经成功地穿透内网了。只希望IPv6快点到来,让我们不必再处理这些麻烦事。