如何在 WSL2 中愉快地使用主机代理

如何在 WSL2 中愉快地使用主机代理

1. 设置环境变量 http_proxy 和 https_proxy

首先,WSL2本质上属于虚拟机,要使用主机的代理,前提是打开代理软件的“允许局域网连接”开关。

对于某C开头的代理软件,在主界面启用“Allow LAN”就可以了。

然后就是获取外部主机的ip地址,要注意的是这个ip地址不是一成不变的,每次Windows启动,这个ip地址都会变化。

有两种获取方法:

  • 读取/etc/resolv.conf
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateResolvConf = false
nameserver 192.168.160.1

其中的192.168.160.1就是外部主机的ip了。

  • 从“控制面板 -> 网络和 Internet -> 网络连接”中获取

找到“vEthernet (WSL)”这个网卡,查看详细信息,就可以从中获取IPV4地址了。
由此引申出另一种方法:Windows命令行下执行ipconfig命令,从返回的结果中获取。

/images/e32_net.png

我们当然是希望在使用WSL2时自动把环境变量http_proxyhttps_proxy自动配置好。

假设主机代理软件的端口号为12333,编辑~/.bashrc文件,在结尾追加:

# Host Proxy
export hostip=$(cat /etc/resolv.conf | grep '^nameserver' | head -n1 | cut -d' ' -f2)
export http_proxy="http://${hostip}:12333"
export https_proxy="http://${hostip}:12333"
# 如果你希望git也全局走代理的话, 再加上这两行:
git config --global http.proxy http://${hostip}:12333
git config --global https.proxy http://${hostip}:12333

在这里我们把主机ip也定义为了一个环境变量,方便在其他地方使用。

2. 让 ssh 走代理

众所周知,GitHub推荐用户通过ssh url来克隆和拉取远程仓库,这样做更安全更简单(不再需要每次push时输入用户名和密码了)。

但是通过ssh克隆仓库时速度不是很稳定,有时速度很快有时一直是龟速。有没有办法让ssh也走代理呢?

这个可以有。从 larryhou/connect-proxy 这个仓库中下载connect.c文件。

/images/e32_github_connect.png

我们只需要这一个文件,所以不用克隆整个仓库。

然后,用WSL2的gcc编译这个文件,这个就不需要赘述了吧,很简单,不用加任何多余的参数。

gcc ./connect.c -o connect

然后,把编译得到的connect文件放到一个方便调用的位置(我习惯放在~/.local/bin/),并且chomd +x

最后,编辑~/.ssh/config文件(没有的话新建一个):

Host *
  ProxyCommand connect -H ${hostip}:12333 %h %p

这里我们就使用了刚才在~/.bashrc定义的环境变量hostip

现在再使用git通过ssh url拉取仓库,你会发现可以走代理了。

3. 防火墙问题

以上这些都配置完成之后,你可能会遇到使用代理时完全没有网。尝试ping一下主机ip,发现ping不通。

这其实是Windows防火墙搞的鬼,最简单粗暴也是最不推荐的解决方法:关闭Windows防火墙。

更合理的做法是给Windows防火墙添加一个放行规则。

通过“控制面板 -> Windows Defender 防火墙 -> 高级设置”,打开“高级安全 Windows Defender 防火墙”面板。

然后,添加一个入站规则:

  • 规则类型选择“自定义”
  • 规则应用于“所有程序”
  • 协议类型选择“任何”
  • 规则应用于“任何本地 IP 地址”
  • 在“应用于哪些远程 IP 地址”这里,填上WSL2虚拟网卡vEthernet (WSL)的ip地址(同样也是前文中提到的主机ip地址)。可以填具体的ip地址,也可以填地址段(比如192.168.160.0/24即可覆盖到192.168.160.0192.168.160.255中的所有ip地址)
  • 操作选择“允许连接”
  • 何时应用规则,把“域、专用、公用”全部选上
  • 随便起个名字,描述写不写无所谓

这些都搞好之后,再在WSL2中ping主机ip,应该就可以ping通了,同时代理也没问题了。如果还是ping不通,就需要检查一下刚才定义的规则是不是哪里没有填写正确。

看到这里,你可能会说:每次Windows启动,这个WSL2虚拟网卡的ip地址都会变,那岂不是我每次开电脑都要再设置一遍防火墙规则?

这个同样可以简化操作。首先,我们可以通过powershell的New-NetFirewallRuleGet-NetFirewallRuleSet-NetFirewallRule命令来新建、获取、更新防火墙规则。那么,我们就可以编写一个脚本,并且让这个脚本每次开机都执行一次,实现自动更新防火墙规则。

贴上我自己用python编写的脚本:

#!/usr/bin/env python3
# encoding: utf-8

import subprocess
import re
import sys

# 防火墙规则的名字
# 如果你想要改名字的话, 首先不要与其他防火墙规则名字冲突
# 其次, 你可能需要手动删除用旧名字命名的防火墙规则
NETFIREWALL_RULE_NAME = "FUCK WSL2"

# WSL2虚拟网卡的名字
# 默认名为"vEthernet (WSL)".
# 如果你把WSL2虚拟网卡的名字改了, 那这里也需要改
WSL_VETHERNET_NAME = "vEthernet (WSL)"

assert sys.platform == "win32", \
    "你为什么要在 Windows 之外的系统运行这个脚本呢?"
assert subprocess.getstatusoutput("where wsl.exe 2>NUL")[0] == 0, \
    "看起来你好像没有安装 WSL..."
assert subprocess.getstatusoutput("net session 1>&2>NUL")[0] == 0, \
    "请使用管理员权限运行此脚本!"

def get_wsl_vethernet_ip():
    """
    从Windows的'ipconfig'命令返回的结果中获取WSL2虚拟网卡的ip地址
    (首选方法)
    """
    rc_, text = subprocess.getstatusoutput("ipconfig")
    assert rc_ == 0, '无法执行 "ipconfig"!'
    flag = False
    for line in text.splitlines():
        if WSL_VETHERNET_NAME in line:
            flag = True
            continue
        if line.strip() and line[0] != " ":
            flag = False
            continue
        if flag and "IPv4" in line:
            return line.strip().split(":")[1].strip()
    # 使用备选方法
    return get_wsl_vethernet_ip_backup_plan()

def get_wsl_vethernet_ip_backup_plan():
    """
    通过在wsl中执行'cat /etc/resolv.conf'命令来获取WSL2虚拟网卡的ip地址
    (备选方法, 因为需要启动wsl)
    """
    rc_, text = subprocess.getstatusoutput('wsl "cat" "/etc/resolv.conf"')
    assert rc_ == 0, '无法执行 "wsl cat /etc/resolv.conf"!'
    for line in text.splitlines():
        if "nameserver" in line:
            return line.strip().split(" ", 1)[1]
    raise Exception('无法获取 WSL2 虚拟网卡的 ip 地址!')

def is_ipv4_address(ip_):
    """ 正则校验是否为标准的ipv4地址 """
    return bool(
        re.search(r'^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$', ip_)
    )

def set_wsl_firewall_rule(ip_):
    """ 正式开始设置防火墙规则 """
    assert is_ipv4_address(ip_), '"%s" 不是一个标准的 ipv4 地址!' % ip_
    ip_subnet = re.sub(r'\.\d+$', ".0/24", ip_)
    # 先检查以NETFIREWALL_RULE_NAME命名的防火墙规则是否存在, 有则更新, 没有则新建
    if subprocess.getstatusoutput('powershell "Get-NetFirewallRule -DisplayName \\"%s\\"' % NETFIREWALL_RULE_NAME)[0] == 0:
        firewall_rule_func = "Set-NetFirewallRule"
    else:
        firewall_rule_func = "New-NetFirewallRule"
    return subprocess.getstatusoutput(
        'powershell '
        '"%s -DisplayName \\"%s\\" -RemoteAddress \\"%s\\" -Enabled True"'
        % (firewall_rule_func, NETFIREWALL_RULE_NAME, ip_subnet)
    )[0]

if __name__ == "__main__":
    wsl_ip = get_wsl_vethernet_ip()
    print("- WSL2 虚拟网卡 ipv4 地址: %s" % wsl_ip)
    print("- 正在更新防火墙规则...")
    rc = set_wsl_firewall_rule(wsl_ip)
    if rc == 0:
        print("- 完成!")
    sys.exit(rc)

最后,编写一个计划任务,让Windows每次启动时执行一次此脚本。

在“控制面板 -> 所有控制面板项 -> 管理工具 -> 任务计划程序”中,创建一个计划任务:

  • 在“常规”标签:
    • 名字随便起一个,描述写不写无所谓
    • 安全选项中,选择“只在用户登录时运行”,并且一定要勾选上“使用最高权限运行”
    • 配置选择“Windows 10”
  • 在“触发器”标签,点击“新建”:
    • “开始任务”选择“登录时”
    • “设置”选择“特定的用户”,然后保持默认,不用点“更改用户”(默认就是当前已登录的用户)
    • “高级设置”保持默认,无需更改
  • 在“操作”标签,点击“新建”:
    • “操作”选择“启动程序”
    • “程序或脚本”填上pythonw.exe的完整路径(用pythonw.exe可以避免出现命令提示符窗口)
    • “添加参数”填上我们刚才编写的python脚本文件的完整路径
/images/e32_jhrw_1.png
/images/e32_jhrw_2.png
/images/e32_jhrw_3.png

保存之后,重启电脑。

可以再次打开“任务计划程序”面板,查看“上次运行结果”来判断计划任务有没有成功执行。

至此,你应该不会再为WSL2的代理问题而发愁了。