BackIcon为公司搭建VPN

2023年11月13日

Openai logomark

Hamster1963

目的

公司原先购买 Astrill VPN 的付费账号给同事使用,优点在于登录即用,无需进行太多的配置,但是在墙越来越高的情况下,难免出现无法连接与间歇断线的问题。不仅运营和客服的同事表示严重影响了工作效率,同时每次出问题的时候我们也只能通过尝试更换连接不同的服务器来解决。

Untitled

在 Astrill 高昂的费用下,如果有一种节点可以由我们自己去监控调整,同时费用较低的方案就大好不过了。因此我直接将家用的方案稍加改进搬到了公司来,详细配置可以查看这篇文章:

家庭负载均衡节点部署 | Hamster

在新的方案下,全部的外网流量(如亚马逊,谷歌)都会经过中央的代理服务器后进行二次分流再转发至真正的服务器上,后面的流量传输与目前主流的科学上网方式几乎是一样的。

新的整体流量图可以参考如下:

Untitled

与旧 VPN 最大不同的在于节点的选择与流量的出口集中在了内网的服务器中,不仅可以统一根据员工进行流量管理也巧妙地绕开了 VPN 与机场对用户数的限制。

采用软件

在服务器上,全部服务部署在群晖 NAS 的容器环境中,加上动态的 DDNS 一共需要部署三个服务:

  1. DDNS-GO 负责域名的动态解析
  2. x-ui 负责用户流量管理与转发至 v2raya
  3. v2raya 负责最终的节点选择与流量转发

在员工电脑具体应用的选择上,选择了 Clash for Windows 作为客户端,通过获取配置文件以连接到内网服务器。

核心构建

最核心需要解决的是配置下放到员工的问题,在使用旧 VPN 时,员工输入邮箱密码登录 VPN 后进行使用,而新版的有什么更好的解决办法直接下放配置到 clash 呢?

在经过一番调研后决定使用钉钉的宜搭搭建一个一键配置页面,员工通过打开页面获取配置,获取成功后点击一键配置直接进行 clash 的配置。

Untitled

一键配置

点击一键配置的时候会发生什么?主要会经历下面几个流程

  1. 向后端申请用户配置
  2. 后端在 xui 中创建用户配置与端口
  3. 后端将 xui 生成的连接信息与 clash 基础配置文件结合
  4. 将配置文件连接变成 clash 配置短链返回至客户端
  5. 自动调用 clash 下载并启用配置

接下来将会一步步介绍代码实现。

首先是创建配置接口的请求与返回结构体:

// CreateMemberProxyConfigReq 创建配置信息 Req请求
type CreateMemberProxyConfigReq struct {
	g.Meta    `method:"post" tags:"网络代理" summary:"创建配置信息" dc:"创建配置信息"`
	StaffName string `json:"staff_name"   v:"required #请输入 staff_name"    ` // 员工姓名
	StaffId   string `json:"staff_id"      v:"required #请输入 staff_id"   `   // 员工id
}

// CreateMemberProxyConfigRes 创建配置信息 Res返回
type CreateMemberProxyConfigRes struct {
	Id             uint        `json:"id"               ` // 网络代理成员表主键id
	StaffName      string      `json:"staff_name"       ` // 员工姓名
	StaffId        string      `json:"staff_id"         ` // 员工id
	ShortConfigUrl string      `json:"short_config_url" ` // 短链接
	ClashConfigUrl string      `json:"clash_config_url" ` // 代理软件配置URL
	OriConfigUrl   string      `json:"ori_config_url"   ` // 原始配置URL
	Port           int         `json:"port"             ` // 网络端口
	CreateTime     *gtime.Time `json:"create_time"      ` // 创建时间
	UpdateTime     *gtime.Time `json:"update_time"      ` // 更新时间
}

在后端接口中,首先进行的是 xui 面板用户的创建,需要传入端口,协议,密码等信息。通过抓取 xui 接口获取数据结构。

// NewProxy xui节点数据结构
type NewProxy struct {
	Up             int            `json:"up"`
	Down           int            `json:"down"`
	Total          int            `json:"total"`
	Remark         string         `json:"remark"`
	Enable         bool           `json:"enable"`
	ExpiryTime     int64          `json:"expiryTime"`
	Listen         string         `json:"listen"`
	Port           int            `json:"port"`
	Protocol       string         `json:"protocol"`
	Settings       ProxySettings  `json:"settings"`
	StreamSettings StreamSettings `json:"streamSettings"`
	Sniffing       Sniffing       `json:"sniffing"`
}

在创建用户完成后,需要反向将其中的信息转化为 Vmess 协议的链接。大体的结构如下:

type ConfigVmess struct {
	V    string `json:"v"`
	Ps   string `json:"ps"`
	Add  string `json:"add"`
	Port int    `json:"port"`
	Id   string `json:"id"`
	Aid  int    `json:"aid"`
	Net  string `json:"net"`
	Type string `json:"type"`
	Host string `json:"host"`
	Path string `json:"path"`
	Tls  string `json:"tls"`
}

通过生成 Vmess 链接方法,将生成的 Vmess与配置文件结合起来生成完整长连接:

// generateVmess 生成vmess链接
func generateVmess(proxy *g_consts.NewProxy) (vmessUrl, fullConfigUrl string) {
	newConfig := &g_consts.ConfigVmess{
		V:    "2",
		Ps:   proxy.Remark,
		Add:  "*.buycoffee.top",
		Port: proxy.Port,
		Id:   proxy.Settings.Clients[0].Id,
		Aid:  proxy.Settings.Clients[0].AlterId,
		Net:  proxy.StreamSettings.Network,
		Type: proxy.StreamSettings.TcpSettings.Header.Type,
		Host: "",
		Path: "",
		Tls:  "none",
	}
	// 构建Vmess链接
	vmessUrl = "vmess://" + gbase64.EncodeString(gjson.New(newConfig).MustToJsonString())
	// 转换成UrlEncode
	vmessUrl = gurl.Encode(vmessUrl)
	// 转换成配置文件URL
	baseUrl := "https://api.tsutsu.one/sub?target=clash&url="
	fullConfigUrl = baseUrl + vmessUrl + "&insert=false&config=https%3A%2F%2Fcdn.jsdelivr.net%2Fgh%2Flhl77%2Fsub-ini%40main%2Ftsutsu-mini-gfw.ini"
	return vmessUrl, fullConfigUrl
}

最后则是将长链接通过搭建在 cloudflare 上的短链服务变成短链接,加上 clash://的前缀便完成了整套生成配置的流程。

// 生成短链
	shortResp, err := g.Client().ContentJson().Post(ctx, g_consts.ShortUrlBaseUrl, g.Map{
		"url": config.ConfigUrl,
	})
	defer func(shortResp *gclient.Response) {
		err := shortResp.Close()
		if err != nil {
			glog.Warning(ctx, "关闭短链请求", err)
		}
	}(shortResp)

基于 Cloudflare worker 的短链服务可以参考下面这个仓库:

https://github.com/xyTom/Url-Shorten-Worker

最终我们会生成一个类似于这样的链接:

clash://install-config?url=https://url../TDHDJH

在宜搭中点击一键配置时则会打开这个链接,而链接则会自动调用 clash 并下载启用配置文件。

通过这种方式,员工的学习成本相较于之前降低了不少,直接点击一个配置按钮即可完成科学上网的需求。

仪表盘查看

通过这种方式,将入口与出口统一在内网服务器,中央管理后,也可以方便地查看各项数据。

Untitled

后记

其实从一开始这只是一个简单的尝试,从 2 个同事开始使用,反馈一直不错,一直到现在有 20 多位同事采用这种新的科学上网方式,在几次的迭代后系统也趋于稳定,不仅降低了维护成本也提升了同事对外网访问的满意程度。所以我的加薪在哪里😭…..(完