Tencent Cloud lighthouse Firewall tool

上次去北京出差,为了便捷地访问家里内网中的一些服务,就在腾讯云服务器上部署了一个 frps 服务,在本地内网的 Openwrt 路由器上安装 frpc 客户端,将内网中的一台 Windows 服务器穿透到腾讯云服务器上。然后通过 Windows RDP 远程连接到这台 Windows 机器上,来使用内网的一些服务。之前也尝试过 WireGuard,但是使用了一段时间体验下来感觉还是通过内网穿透的方式比较稳定和流畅。于是最终的方式还是选用 frp 内网穿透的方案。

本着方便省事儿的原则就放心大胆地开放了云服务器的安全组规则。不幸的是,由于这样的疏忽,某一天我的 Windows 虚拟机被弱口令(admin2020)给爆破了。巨大的损失就是挂载到 Windows 虚拟机上的 NAS 存储被勒索病毒进行了加密 😂。不过好在最最重要的数据全部存到了 OneDrive 上,NAS 上损失的都是一些下载的电影、书籍以及一些 ISO、虚拟机模版之类的文件。一下子损失了 8TB 的数据很心疼,毕竟是自己辛辛苦苦搜集的资源,但是仔细想一下,这些资源都不是自己的,基本上都是从下载下来的,还可以通过同样的方式找回来。或许是之前看过《断舍离》的缘故,也看的开了,难受了一小会儿之后就好过来了。毕竟这个世界上,没有无法不能失去的东西,也没有非得必须要得到的东西。

有了此次教训,就开始考虑对手上的云主机资源进行安全加固,将一些与内网互通的云主机安全组/防火墙的通用去除了允许所有,只添加本地公网 IP 的允许规则。但是对于家庭宽带用户来讲,公网 IP 并不是一个固定的 IP,而是会一直不断变化,总不能每次变化之后再登录到云主机控制台手动添加一下吧。于是就想着有没有自动化的方式来自动添加和更新安全组/防火墙规则呢?

Terraform

第一个想到的方案便是 terraform,主流的云厂商都有对应的 provider 支持,腾讯云应该也是能够支持的。不过看了官方的 terraform-provider-tencentcloud repo 文档之后,并没有找到给 lighthouse 主机配置防火墙规则的支持,遂放弃。

cfwctl

既然 terraform 不支持,那就自己造轮子写一个吧,就叫它 Cloud Firewall Control Tool,简称 cfwctl。腾讯云官方的 API 文档还可以,还能在线生成代码,用起来也十分方便。考虑到会将该工具运行到运行到 arm64 的路由器上,因此跨平台运行 cfwctl 使用 golang 来实现无疑是个不错的选择,正好也能来练练手。

执行的操作其实很简单,先是通过某种方式获取本地机器的公网 IP,然后将该 IP 添加到对应实例的防火墙规则当中,并在规则描述中添加标识符来标记。目前自己只需要添加规则,先凑活着用一段时间,看下效果如何。目前只支持腾讯云的 lighthouse 实例,后续有机会再增加几个别的云厂商支持。

获取公网 IP

首先要获取到我们本地网路的公网 IP,由于公网 IP 可能一直是变化的,所以我们每次更新防火墙规则之前都需要获取最新的公网 IP。以下是具体实现的代码:

package main

import (
	"fmt"
	"io/ioutil"
	"net/http"
	"regexp"
)

// 定义 IPv4 的正则表达式,目的是从获取公网 IP 的  API 返回结果中过滤出 IPv4 地址
const ipv4_regex = `(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}`

// 定义几个几个提供获取公网 IPv4 地址的 API URL
var urlList = []string{
	"https://ip.me"
	"http://ip.sb",
	"http://ip.cip.cc",
	"http://myip.ipip.net",
}

func GetPublicIP() string {
	for _, url := range urlList {
    // 创建一个 http client
		client := &http.Client{}
    // 设置 client 的请求方法为 GET 以及请求的 URL
		request, err := http.NewRequest("GET", url, nil)
		if err != nil {
			continue
		}
    // 设置 Client 的 User-Agent 为 curl,不然一些 API 会返回 html 的结果
		request.Header.Set("User-Agent", "curl/7.54.0")
		resp, err := client.Do(request)
		if resp.StatusCode != 200 && err != nil {
			continue
		}
		defer resp.Body.Close()
		body, err := ioutil.ReadAll(resp.Body)
    // 从 API 返回结果中用正则匹配出 IPv4 地址
		reg := regexp.MustCompile(ipv4_regex)
		ipList := reg.FindAllString(string(body), -1)
    // 如果匹配结果中有 IPv4 地址,则返回第一个元素即可
		if len(ipList) > 0 {
			fmt.Printf("my public ip is %s\n", ipList[0])
			return ipList[0]
		}
	}
	return ""
}

添加 Firewall 规则

package main

import (
	"fmt"
	"os"

	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
	"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
	lighthouse "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lighthouse/v20200324"
)

// 定义 API 请求的 URL
const endpoint = "lighthouse.tencentcloudapi.com"

// 定义请求 API 的 Client 结构体
type Client struct {
	SecretId  string
	SecretKey string
	InstaceId string
	Region    string
	Endpoint  string
}

// 通过该方法从环境变量中读取 Ck Sk 等信息,并返回一个 client 对象
func NewClient() Client {
	client := Client{
		SecretId:  os.Getenv("TENCENTCLOUD_SECRET_ID"),
		SecretKey: os.Getenv("TENCENTCLOUD_SECRET_KEY"),
		InstaceId: os.Getenv("TENCENTCLOUD_INSTANCE_ID"),
		Region:    os.Getenv("TENCENTCLOUD_REGION"),
		Endpoint:  endpoint,
	}
	if client.SecretId == "" || client.SecretKey == "" || client.InstaceId == "" || client.Region == "" {
		panic("Please set TENCENTCLOUD_SECRET_ID, TENCENTCLOUD_SECRET_KEY, TENCENTCLOUD_INSTANCE_ID, TENCENTCLOUD_REGION")
	}
	return client
}

// 定义添加防火墙规则的方法
func (c Client) AddRules(firewallRules []*lighthouse.FirewallRule) {
	credential := common.NewCredential(c.SecretId, c.SecretKey)
	cpf := profile.NewClientProfile()
	cpf.HttpProfile.Endpoint = endpoint
	client, _ := lighthouse.NewClient(credential, c.Region, cpf)

	request := lighthouse.NewCreateFirewallRulesRequest()
	request.InstanceId = common.StringPtr(c.InstaceId)
	request.FirewallRules = firewallRules

	response, err := client.CreateFirewallRules(request)
	if _, ok := err.(*errors.TencentCloudSDKError); ok {
		fmt.Printf("An API error has returned: %s", err)
		return
	}
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s", response.ToJsonString())
}

获取防火墙规则

func (c Client) GetRules() string {
	credential := common.NewCredential(c.SecretId, c.SecretKey)
	cpf := profile.NewClientProfile()
	cpf.HttpProfile.Endpoint = endpoint
	client, _ := lighthouse.NewClient(credential, c.Region, cpf)

	request := lighthouse.NewDescribeFirewallRulesRequest()

	request.InstanceId = common.StringPtr(c.InstaceId)

	response, err := client.DescribeFirewallRules(request)
	if _, ok := err.(*errors.TencentCloudSDKError); ok {
		fmt.Printf("An API error has returned: %s", err)
		return ""
	}
	if err != nil {
		panic(err)
	}
	return response.ToJsonString()
}

使用

  • 在本地的 ~/.bashrc 或者 ~/.zshrc 文件中设置一些 AKSK 信息、实例 ID、region 信息等参数;
export TENCENTCLOUD_SECRET_ID="AKiiiplQntjJbcMp1"
export TENCENTCLOUD_SECRET_KEY="SKkkkiiwwlwjmmG5"
export TENCENTCLOUD_INSTANCE_ID="lhins-qjxazjaa"
export TENCENTCLOUD_REGION="ap-beijing"
  • 设置好环境变量之后,再编译运行看下能否成功
$ go build -o cfwctl
$ chmod +x cfwctl
$ cfwctl add

my public ip is 193.191.231.82
{"Response":{"RequestId":"30e71243-1793-112e-9e41-b310ec599b90"}}%
  • 如果是 arm64 的 OpenWrt 环境,在本地开发机上进行跨平台编译,然后将编译好的 cfwctl 二进制文件 scp 到路由器上,再添加 cron job 定时任务即可,这样就能自动定时更新防火墙规则来。
$ CGO_ENABLED=0  GOOS=linux  GOARCH=arm64 go build -o cfwctl