Nginx 配合 CloudFlare 获取真实 IP 并限制访问

在最近一次网站部署中,考虑到安全因素,希望将源站完全隐藏在 CF 后,只允许 CF 访问,并尽可能的减少信息泄漏。

获取真实客户端 IP

由于经过 CloudFlare 代理后,Nginx 看到的 remote ip 其实是 CF 的 IP 地址。因此需要通过 Nginx 的 ngx_http_realip_module,还原出真实的客户端 IP。这一步在 CloudFlare 有详细的文档说明

简单来说,当发现请求 IP 为 CF 的 IP 段时,读取 CF-Connecting-IP 头,并将其中的 IP 设置为客户端 IP。CF 的 IP 段可以通过 API 获取。以下是一个脚本可以生成相关的配置:

#!/bin/bash

# 脚本:获取 Cloudflare IP 列表并生成 Nginx 配置

# 输出文件
IP_CONFIG_FILE="cf_ips.conf"

# 清空输出文件
> "$IP_CONFIG_FILE"

# Cloudflare IP 列表 API 端点
CLOUDFLARE_API_URL="https://api.cloudflare.com/client/v4/ips"

# 生成 set_real_ip_from 配置
echo "# Cloudflare IPv4 IPs" >> "$IP_CONFIG_FILE"
curl -s "$CLOUDFLARE_API_URL" | jq -r '.result.ipv4_cidrs[]' | while read -r ip; do
    echo "set_real_ip_from $ip;" >> "$IP_CONFIG_FILE"
done

echo -e "\n# Cloudflare IPv6 IPs" >> "$IP_CONFIG_FILE"
curl -s "$CLOUDFLARE_API_URL" | jq -r '.result.ipv6_cidrs[]' | while read -r ip; do
    echo "set_real_ip_from $ip;" >> "$IP_CONFIG_FILE"
done

# 添加 real_ip_header 配置
echo -e "\n# Set real IP header" >> "$IP_CONFIG_FILE"
echo "real_ip_header CF-Connecting-IP;" >> "$IP_CONFIG_FILE"

脚本运行后会生成 cf_ips.conf 文件,内含 set_real_ip_fromreal_ip_header 配置,在 httpinclude 该文件,即可自动为所有 server 自动设置真实客户端 IP。

限制仅 CloudFlare IP 访问

解决了真实客户端 IP 的问题后,接下来就是如何做到仅限 CF IP 访问。由于该 Nginx 托管了多个 server,因此并不希望直接从防火墙层面拒绝所有非 CF IP 访问。那么从 Nginx 侧,第一时间想到的就是用 ngx_http_access_module 的方式,仅放过 CF IP 段:

# Cloudflare IPv4 Allow
allow 173.245.48.0/20;
allow 103.21.244.0/22;
allow 103.22.200.0/22;
allow 103.31.4.0/22;
allow 141.101.64.0/18;
allow 108.162.192.0/18;
allow 190.93.240.0/20;
allow 188.114.96.0/20;
allow 197.234.240.0/22;
allow 198.41.128.0/17;
allow 162.158.0.0/15;
allow 104.16.0.0/13;
allow 104.24.0.0/14;
allow 172.64.0.0/13;
allow 131.0.72.0/22;

# Cloudflare IPv6 Allow
allow 2400:cb00::/32;
allow 2606:4700::/32;
allow 2803:f800::/32;
allow 2405:b500::/32;
allow 2405:8100::/32;
allow 2a06:98c0::/29;
allow 2c0f:f248::/32;

# Default deny
deny all;

但是添加了这段配置后,发现不论是否通过 CF 访问,所有请求均会被拦截,返回 403。搜索相关文章1后发现,这是因为 ngx_http_realip_module 已经先执行完毕了,此时 ngx_http_access_module 拿到的已经是被修改了的真实客户端 IP,自然无法通过检测,被拒绝访问。

为了解决该问题,我们需要使用其他手段来进行访问控制。查阅相关资料后发现,ngx_http_geo_module 是个非常好的选择。相关配置如下:

geo $realip_remote_addr $is_cf {
  default 0;
  # Cloudflare IPv4 Allow
  173.245.48.0/20 1;
  103.21.244.0/22 1;
  103.22.200.0/22 1;
  103.31.4.0/22 1;
  141.101.64.0/18 1;
  108.162.192.0/18 1;
  190.93.240.0/20 1;
  188.114.96.0/20 1;
  197.234.240.0/22 1;
  198.41.128.0/17 1;
  162.158.0.0/15 1;
  104.16.0.0/13 1;
  104.24.0.0/14 1;
  172.64.0.0/13 1;
  131.0.72.0/22 1;

  # Cloudflare IPv6 Allow
  2400:cb00::/32 1;
  2606:4700::/32 1;
  2803:f800::/32 1;
  2405:b500::/32 1;
  2405:8100::/32 1;
  2a06:98c0::/29 1;
  2c0f:f248::/32 1;
}

其中,$realip_remote_addrngx_http_realip_module 提供的变量,存储了原始的客户端地址。通过 CF 访问的情况下,这个变量存储的就是原始的 CF 地址。$is_cf 是用于储存中间结果的变量。如果原始客户端地址在 CF IP 段,则会被设置为 1,否则为 0。

有了这个变量之后,我们就可以使用 ngx_http_rewrite_module 来进行判断,并拒绝访问。在需要的地方增加如下代码:

if ($is_cf = 0) {
    return 444;
}

即可拒绝掉所有非 CF 访问的情况。

使用 OpenResty

如果在使用 OpenResty,那么有更简单的方式2可以做这个事情。借助 access_by_lua_block,我们可以直接在 lua 里访问相关变量进行判断,并拒绝非 CF 的访问:

access_by_lua_block {
    if ngx.var.remote_addr == ngx.var.realip_remote_addr then
            return ngx.exit(ngx.HTTP_FORBIDDEN)
    end
}

原理其实也很简单,如果 ngx.var.remote_addrngx.var.realip_remote_addr 是相同的,则证明 ngx_http_realip_module 没有起作用,那么说明原始访问 IP 一定不在 CF IP 段内。

注意事项

  1. 有条件的情况下(即 Nginx 所有 server 都托管在 CF),应使用防火墙直接拒绝所有非 CF IP 的 TCP 链接
  2. 由于 if 指令只能使用在 serverlocation 使用,因此该方法在多个 server 内,需要重复配置
  3. 由于 ssl_reject_handshake 无法在 if 内使用,因此如果攻击者猜对了 server_name,那么证书信息依然会泄漏,存在被探测到源站的可能

参考文档

  1. https://serverfault.com/questions/601339/how-do-i-deny-all-requests-not-from-cloudflare/826428#826428 ↩︎
  2. https://stackoverflow.com/questions/39176931/nginx-allowdeny-realip-remote-addr/39183303#39183303 ↩︎

QNAP 新版 License Center 校验流程逆向

最近新添置了两个支持 ONVIF 协议的摄像头,想使用 QNAP 的 QVR 进行录像。但是打开 QVR 发现,只能添加两个通道,后续新的通道需要按年付费解锁。依稀记得原先是有 8 个免费通道可用的,于是打开官方文档,发现:

系统软件授权
QTSQVR Pro自带 8 通道
QTSQVR Elite自带 2 通道
QuTS heroQVR Elite自带 2 通道

原来官方提供的 8 通道 QVR Pro 只支持 QTS 系统,不支持基于 ZFS 的 QuTS hero 系统。既然官方不支持,那只能自己动手了。

TL; DR

  1. GPT 太好用了,大大提升逆向效率
  2. 新版校验用的 Public Key 经过简单的凯撒密码加密后打包在 libqlicense.so
  3. LIF 文件的 floating 相关字段需要干掉,否则会联机校验

激活流程

在研究的过程中,发现已有前人详细分析了激活流程的各个步骤,分析了各个文件的存储、编码、加密、验证格式,并编写了相关脚本和代码。具体文章在这里,此处不多做赘述:

https://jxcn.org/2022/03/qnap-license

尝试动手

参考文章,尝试在 NAS 上进行操作。结果发现,找不到文章中提及的 qlicense_public_key.pem 文件,全盘搜索后发现依然找不到该文件。查看文章评论发现,似乎官方在 23 年左右修改了一次校验逻辑,去掉了该文件。所有 23 年之后发布的版本,都无法使用替换 key 文件的方式进行激活。

重新分析

虽然官方修改了相关逻辑,但是有前人经验参考,还是很快发现了关键所在。

先用 IDA 打开 qlicense_tool ,找到离线激活的处理函数,发现对输入进行简单处理后直接调用了 qcloud_license_offline_activate 函数,位于 libqlicense.so 动态链接库中。这个函数在几层调用之后,最终调用了 qcloud_license_internal_verify 函数,对 LIF 文件的签名进行了计算。

由于静态分析不太容易理清逻辑,于是上 GDB 准备直接调试。找了下发现 MyQNAP Repo 里有编译好的 GDB,但是安装却失败了。研究了下发现是作者把 X86 和 X41 的链接搞反了。X86 下载到的包是 X41 的,导致安装失败。于是手动下载了正确的包之后安装,一切顺利。

通过 gdb 调试发现,qcloud_license_internal_verify 函数的第一个入参就是校验用的 Public Key,打出来长这样:

-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEDJcPoMIFyYDlhTUzNSeT/+3ZKcByILoI
Fw8mLv07Hpy2I5qgAGQu66vF3VUjFKgpDDFKVER9jwjjmXUOpoCXX4ynvFUpEM25
ULJE86Z6WjcLqyG03Mv6d3GPYNl/cJYt
-----END PUBLIC KEY-----

通过 hook fopen 函数,想找到这个 key 存储在哪个文件,但是发现并没有结果。于是想到,这段 key 可能被打进了 so 文件本身,于是通过 Strings 搜索,但是依然没有找到相关字符串。

一筹莫展之际,在调用链路上乱点,突然注意到这样一段奇怪的代码(IDA 还原得出):

    while ( 1 )
    {
      result[v3] = (v1[v3] + 95) % 128;
      if ( ++v3 == 359 )
        break;
      result = (_BYTE *)*a1;
    }

多看两眼之后意识到,这就是凯撒密码。(虽然凯撒密码现在看来其实更应该算是编码而非加密,但是考虑到确实有相关定义,因此后续均表述为加密和解密。)也就是说,之所以在 Strings 内找不到相关字符串,是因为写入的是通过凯撒密码加密后的字符串。于是把这段丢给 GPT,让它写个加密和解密的代码:

# -*- coding: utf-8 -*-

def decode(input_string):
    result = []
    for char in input_string:
        # 将每个字符的 ASCII 值加 95,然后对 128 取模
        encoded_char = (ord(char) + 95) % 128
        result.append(chr(encoded_char))
    return ''.join(result)

def encode(encoded_string):
    result = []
    for char in encoded_string:
        # 逆向解码:恢复原始字符
        decoded_char = (ord(char) - 95) % 128
        result.append(chr(decoded_char))
    return ''.join(result)

original_text = """-----BEGIN PUBLIC KEY-----
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEDJcPoMIFyYDlhTUzNSeT/+3ZKcByILoI
Fw8mLv07Hpy2I5qgAGQu66vF3VUjFKgpDDFKVER9jwjjmXUOpoCXX4ynvFUpEM25
ULJE86Z6WjcLqyG03Mv6d3GPYNl/cJYt
-----END PUBLIC KEY-----"""
encoded_text = encode(original_text)
decoded_text = decode(encoded_text)

print("Original:", original_text)
print("Encoded:", encoded_text)
print("Decoded:", decoded_text)

跑了一下,得到实际存储的字符串:

NNNNNcfhjoAqvcmjdAlfzNNNNN+niz\u0018fbzil\u0010{j\u001b\u000bQdbrzglUffbdjez\bbfek\u0004q\u0010njg\u001aze\r\tuv\u001bot\u0006uPLT{l\u0004c\u001ajm\u0010j+g\u0018Y\u000em\u0017QXi\u0011\u001aSjV\u0012\bbhr\u0016WW\u0017gTwv\u000bgl\b\u0011eeglwfsZ\u000b\u0018\u000b\u000b\u000eyvp\u0011\u0010dyyU\u001a\u000f\u0017gv\u0011fnSV+vmkfYW{Wx\u000b\u0004m\u0012\u001ahQTn\u0017W\u0005Thqzo\rP\u0004kz\u0015+NNNNNfoeAqvcmjdAlfzNNNNN

由于开头和结尾的 ----- 会被加密为 NNNNN 因此直接在 Strings 中搜索,顺利定位到相关资源,位于 .rodata 段 38B40 的位置,长度 212。

接下来操作就简单了,生成一对新的 key 之后,直接替换指定位置即可。注意,由于长度是写死在代码里的,因此二者必须等长,否则可能会出现无法预料的错误。

再丢给 GPT 写个十六进制替换的脚本:

# -*- coding: utf-8 -*-

import binascii

def hex_to_bytes(hex_str):
    """将16进制字符串转换为字节数据。"""
    return binascii.unhexlify(hex_str)

def patch_file(file_path, search_hex, replace_hex):
    # 将16进制模式转换为字节数据
    search_bytes = hex_to_bytes(search_hex)
    replace_bytes = hex_to_bytes(replace_hex)
    
    # 检查替换块的长度是否匹配
    if len(search_bytes) != len(replace_bytes):
        raise ValueError("Length mismatch! 搜索模式和替换内容长度必须相同!")

    # 读取文件内容
    with open(file_path, "rb") as f:
        data = f.read()

    # 查找并替换内容
    patched_data = data.replace(search_bytes, replace_bytes)

    # 如果没有找到匹配的内容
    if data == patched_data:
        print("No Match. 没有找到匹配的内容。")
    else:
        # 将修改后的内容写回文件
        with open(file_path, "wb") as f:
            f.write(patched_data)
        print("Patch success. 文件已成功更新。")

# 使用示例
file_path = "libqlicense.so"           # 需要打补丁的文件路径
old_hex = "4e4e4e4e4e6366686a6f417176636d6a64416c667a4e4e4e4e4e2b6e697a1866627a696c107b6a1b0b516462727a676c55666662646a657a086266656b0471106e6a671a7a650d0975761b6f740675504c547b6c04631a6a6d106a2b6718590e6d17515869111a536a56120862687216575717675477760b676c08116565676c7766735a0b180b0b0e7976701110647979551a0f17677611666e53562b766d6b6659577b57780b046d121a6851546e1757055468717a6f0d50046b7a152b4e4e4e4e4e666f65417176636d6a64416c667a4e4e4e4e4e"              # 要搜索的16进制字符串
new_hex = "4e4e4e4e4e6366686a6f417176636d6a64416c667a4e4e4e4e4exxx4e4e4e4e4e666f65417176636d6a64416c667a4e4e4e4e4e"             # 替换为的16进制字符串

# 执行替换操作
patch_file(file_path, old_hex, new_hex)

这里使用 Hex Encoding 的原因是加密后的字符串存在一些非 ASCII 字符。

替换后,用原作者的相关脚本和工具,生成了一个 LIF,尝试使用 qlicense_tool 激活。此时发现,激活死活失败,但是调试发现 verify 实际已经过了。研究了好久之后,发现是 Console Management 工具导致的。由于 qlicense_tool 在激活过程中会运行被激活服务的脚本检测 License 正确性,而 Console Management 拦截了相关命令,导致返回包不是合法的 json,导致失败。在系统设置内关闭 Console Management 后,激活成功。

检测报错

本以为到此激活就成功了。结果打开许可证中心之后,发现许可证无效了。查阅相关日志,发现许可证中心发送了一个 POST 请求到 https://license.myqnapcloud.io/v1.1/license/device/installed ,然后报错许可不属于该设备,然后将许可改为失效。

阅读原作者相关文章,结合代码,发现是因为原作者是为了 QuTS Cloud 设计的代码,由于 Cloud 版没有离线激活功能,所以会定期刷新 Token,这里一刷新就发现许可有问题了。于是修改代码,将 LIF 内的 floating 相关字段全部干掉。包括:

FloatingUUID
FloatingToken
LicenseCheckPeriod

重新生成 LIF 文件,再次激活,已经不会向 myqnapcloud 验证激活信息了。

无痛解决 TeslaMate 访问 OpenStreetMap API 的问题

TeslaMate 是一款特斯拉汽车专用的增强工具,使用了破解的官方 App 的 API,将车机记录的大量信息进行了导出和展示,极大的增强了特斯拉车主的使用体验。

但是由于一些原因,中国特斯拉的汽车的数据都是存放在中国大陆,且不允许境外 IP 访问。因此,中国车主想要使用 TeslaMate,则必须在境内部署相关的服务。

不过,境内部署虽然可以解决访问 Tesla API 的问题,却也带来了新的问题,就是调用境外 API 的时候会出现错误,导致部分功能无法使用。

具体来说,TeslaMate 使用了 OpenStreetMap 提供的 API,用来将经纬度坐标转换为地名。然而,OpenStreetMap 的域名在境内是无法访问的,因此会导致解析坐标点的时候出现失败,导致在 Grafana 的 Drives(行程) 面板不显示行程的起止地址。

为了解决这个问题,我首先想到了设置 http_proxy 等环境变量的方式。但是搜索之后发现,似乎 TeslaMate 并不会读区这几个环境变量,因此无法使用这种方式强制走代理访问相关 API。而且,由于 Tesla 本身的 API 不能走代理访问,必须使用中国 IP 访问,因此如果要使用这种方式的话,还需要配置 Proxy 的分流策略,非常麻烦。

综合考虑各种方案,我最终选择了 sniproxy 的方式。sniproxy 是允许在不解密流量本身的情况下,进行 SSL/TLS 流量转发的方案。使用 sniproxy 时,只需要将目标域名的 IP 强制指向 sniproxy 的 IP 地址(如修改 /etc/hosts),既可以完成代理。

方案一:直接在本地运行 sniproxy

  • 优点:不占用母鸡的 443 端口,不影响现有服务
  • 缺点:配置较为繁琐,需要两个额外的容器互相配合

考虑到搭建 sniproxy 需要占用一个 IP 的 443 端口,因此我决定在 docker compose 内增加 sniproxy 的相关容器,来做到不占用母鸡的 443 端口的目的。由于 sniproxy 是运行在 TeslaMate 同一台机器上的,因此还需要一个额外的代理来将流量转发出去。这一步有很多方案,使用 warp+、ss、v2 等均可实现,我选择使用 v2 的方案。

最终修改后的 docker-compose.yaml 新增的内容如下:

  sniproxy:
    image: sniproxy
    restart: always
    command: ./sniproxy
    depends_on:
      - xray
    networks:
      default:
        aliases:
          - nominatim.openstreetmap.org

  xray:
    image: teddysun/xray:latest
    restart: always
    volumes:
      - ./xray:/etc/xray

其中,sniproxy 镜像是对 https://github.com/XIU2/SNIProxy 项目的简单包装,将配置文件写在了镜像内。配置内容如下:

# 监听端口(注意需要引号)
listen_addr: ":443"

# 可选:启用 Socks5 前置代理
enable_socks5: true
# 可选:配置 Socks5 代理地址
socks_addr: xray:40000

# 可选:允许所有域名(会忽略下面的 rules 列表)
#allow_all_hosts: true

# 可选:仅允许指定域名
rules:
  - openstreetmap.org

这里的 socks_addr 为下方 xray 容器内的 socks 代理监听地址。

这段配置的重点在于,sniproxy 容器配置了 network alias。这个选项的作用是,告诉 docker compose,将指定的域名解析到当前容器。因此,加上了这段配置后,docker 容器内解析 nominatim.openstreetmap.org 这个域名,会自动得到 sniproxy 容器的 docker 网络的 IP,达到将 OpenStreetMap 的 API 请求自动导向 sniproxy,进而导向 xray 代理的目的。

方案二:使用单独的服务器部署 sniproxy

  • 优点:配置简单,无需额外容器
  • 缺点:需要额外的服务器部署 sniproxy,需要占用一个 443 端口

当然,如果有多余的服务器,可以提供一个 443 端口的话,就不用这么麻烦了。直接在服务器上搭建好 sniproxy,然后在 docker-compose.yaml 内的 teslamate 容器配置里增加下面这段就可以了:

extra_hosts:
  nominatim.openstreetmap.org: sniproxy_server_ip

这段配置等同于在 teslamate 的 docker 容器的 /etc/hosts 文件内,写入了域名与 IP 的映射关系,因此所有的 OpenStreetMap 相关的 API 请求,都会被导向 sniproxy 的 IP 地址。

P.S. 其实使用 nginx 的 stream 模块,配合 stream_ssl stream_ssl_preread 两个模块,可以做到不影响现有的网站服务的前提下,与 sniproxy 兼容。但是此方法需要对现有的 nginx 配置进行较大的改动,而我本人的 nginx 配置有十几个文件,实在是不想改,因此就放弃了这种方式。

罗技 Logi MX 系列键盘布局切换方法

本方法适用于所有 MX 系列键盘,非 MX 系列没有尝试,不保证可用。

罗技的 MX 系列键盘通常有两个布局模式,最主要的区别就在于底部的 start 按键和 alt 按键。

通过按键的标记可以看到,start 键(即 Windows 徽标键)在 mac 布局下是 opt 键,而 alt 键在 mac 布局下是 cmd 键。

但是这里的按键布局,翻遍了说明都没有找到如何切换。通过搜索发现,跟键盘第一次连接的设备有关。如果键盘第一次连接 mac,则使用左侧的 mac 布局。如果连接的是 Windows,则使用右侧的 Windows 布局。

这导致对双系统用户极度不友好。切换系统时,会出现按键标记与实际功能不符的情况。于是经过多方搜索,找到了手动切换布局的方法:

切换到 Windows 布局:按住 FN + P 键 3 秒(Hold FN+P for 3 seconds)

切换到 mac 布局:按住 FN + O 键 3 秒(Hold FN+O for 3 seconds)

GoLand 切换代码跳转时的默认系统

由于 go 原生支持了交叉编译,且允许自由的通过编译参数来做到多系统分别编译不同的代码,因此很多项目都实用这种方式来屏蔽跨系统的 API 差异。但是如果我们在非目标系统进行开发,如在 Windows 或 macOS 开发 Linux 程序,就会出现代码跳转的时候无法跳转到正确的文件。

这是因为 GoLand 默认使用我们开发环境的信息来搜索和跳转,因此如果在 Windows 开发,就会跳转到对应的 Windows 的实现。这时候,我们可以通过修改配置,来修改这一跳转行为。具体需要修改的配置为:

Preferences -> Go -> Build Tags & Vendoring
Build Tags & Vendoring

或者使用双击 Shift 输入 Build Tags 来快速定位到这个配置。

在这个配置里,将 OS 和 Arch 修改为目标系统即可。

设置完成后立刻生效,此时再跳转有多系统差异的函数,即可直接跳转到与设置对应的实现。

安全的调试容器内的进程

通常来说,使用 gdb 等工具调试测试或生产环境的进程,是非常好的查找 Bug 的方式。但是当我们步入容器时代后,使用 gdb 工具却会遇到一些麻烦。

当我们进入容器内,尝试使用 gdb attach 的方式开始调试已运行的进程时,会得到这样的报错:

Attaching to process 123
ptrace: Operation not permitted.

通过简单的搜索我们可以发现,这是因为容器内为了安全,默认没有给 SYS_PTRACE 权限的原因。于是,很多教程告诉我们,用这个命令启动容器解决这个问题:

docker run xxx --cap-add=SYS_PTRACE

然而,在生产环境授予容器 SYS_PTRACE 权限是很危险的,有可能造成容器逃逸。因此,如果需要启用该权限,建议慎重考虑。

那么,有没有办法避开这个权限呢?由于容器本身其实就是主机上的一个进程,所以我们自然会想到,能否直接从主机找到对应的进程,然后在主机上执行 gdb attach 呢?

事实上我们可以这样操作,但是由于容器内的二进制文件在主机上并没有,所以 gdb attach 之后会报 No such file,并不能正常工作。就算我们把二进制拷贝出来,放在对应的位置上,gdb 还是会有 warning:

warning: Target and debugger are in different PID namespaces; thread lists and other data are likely unreliable.  Connect to gdbserver inside the container.

这个 warning 就是告诉我们,正在运行的 gdb 进程的 PID namespace 与想要调试的进程的不同,这会导致我们的线程列表等数据出现偏差,可能会影响调试。

考虑到容器的实现原理就是在不同的 namespace 中运行程序,因此如果我们能以容器进程的 namespace 启动一个调试器,那就能解决这个问题了。经过一番搜索,发现 Linux 提供了一个叫 nsenter 的工具来做这件事。我们只需要这样操作:

nsenter -m -u -i -n -p --target ${PID} bash

就可以启动一个跟容器内进程共用同一个 namespace 的 bash,并且该 bash 并没有 SYS_PTRACE 权限的限制,可以非常方便的使用 gdb 了。

腾讯云云函数 SCF 与轻量服务器互通指南

去年末腾讯云轻量服务器推出了内网互联功能,但是好像有很多小伙伴不太清楚用法。因此写本文给大家一些使用思路。

本文基于官方文档 https://cloud.tencent.com/document/product/1207/56847

注意事项

  1. 轻量内网互联功能、云联网及 VPC 等功能本身不收费,但若本地互联带宽超过 5Gbps 或需要跨地域互联,则需要收取相关带宽费用。因此本文仅做同地域互联展示
  2. 由于轻量的内网 IP 无法自定义,所以建议先购买好需要的轻量服务,再规划内网其他服务(如 SCF、MySQL 等)的网段,以避免内网 IP 冲突
  3. 若轻量云 VPC 的 CIDR 与云函数 VPC 的 CIDR 有重合,可能带来一些奇怪的访问不通的问题,建议在网段规划时直接避开重合段
  4. 云联网只是打通网络,实际端口访问还是受到防火墙限制。若出现访问不通,请检查轻量服务器的防火墙设置:https://cloud.tencent.com/document/product/1207/44577
  5. 已有云函数可以通过编辑函数配置加入 VPC,无需新建
  6. 该方法打通网络后,不仅可以做到云函数访问轻量云资源,同样反向访问也是可以的。如允许轻量云服务器访问 VPC 内的其他 CVM 或 MySQL、Kafka、Redis 等资源,但是要注意对应资源的安全组设置,需要放通轻量云服务器的相关网段

操作流程

轻量云内网互联

在对应区域购买轻量云服务,本文以北京区为例

购买完成后进入 内网互联 页面,可以看到对应区域的关联选项:

内网互通

点击关联云联网,弹出关联选择页面:

关联云联网

选择需要的云联网。若还未创建云联网,则点击新建

新建云联网

新建云联网时,由于我们只用本地带宽,因此服务质量直接选择白金即可。若有跨地域互联需求,则按照预算选择对应的服务质量。

下方的关联 VPC 实例可以先不填。直接点击右边的小叉删除即可。

新建完成后,回到关联云联网页面,选择对应的云联网并确认

关联申请成功

看到这里说明关联申请已成功提交。接下来需要进入云联网控制台同意关联申请。点击页面中的跳转链接即可快速跳转到对应的云联网控制台

云联网控制台

这里要注意,由于轻量服务器是处在腾讯云内部独立账号下,因此这里的所属账号可能与自己的账号不同,这是正常的。如果这里出现了多条记录,我们找对应时间为我们申请关联的时间,备注为 Lighthouse VPC 的申请记录,并点击同意。

接受申请后,状态变为绿色的已连接。

此时回到轻量云的控制台,刷新后会发现已关联成功

内网互联成功

到这里,轻量云和云联网的关联已全部完成。此处的内网 CIDR 需要记下,在后续的 VPC 创建中要用。

云函数 SCF 内网互联

接下来我们处理 SCF 与内网资源的互通。

首先我们需要一个 VPC 给 SCF 使用。如果已有规划好的 VPC,则可跳过新建 VPC 步骤。

打开 VPC 控制台,切换到对应的区域。这里使用北京区域。

新建 VPC

新建 VPC 时,有两个 CIDR 需要配置。一个是整个大 VPC 的 CIDR 范围,一个是单个子网的 CIDR 范围。还记得之前轻量的 CIDR 么?此处的大 VPC CIDR 可以包含轻量的 CIDR,但是子网的 CIDR 范围最好不要跟轻量的 CIDR 有重叠,否则需要对路由表进行细致的调整,还可能出现 IP 冲突,比较麻烦。

由于我们之前拿到的轻量的 CIDR 是 10.0.24.0/22,因此这里我们给子网分配 10.0.0.0/24,避免冲突。

新建 VPC 成功

接下来我们需要将这个 VPC 关联到刚才的云联网中。

回到云联网的控制台,在关联实例页面,选择新增实例

新增关联

依次选择私有网络-对应地域-对应 VPC,并确认。

关联完成

确认后即可看到我们刚刚新建的 VPC 和轻量的 VPC 均为已连接状态。

为了确保没有 CIDR 冲突,我们需要确认一下路由表状态。点击上方的路由表页面进行确认

云联网路由表

看到两条路由表均为有效且启用的状态,说明互联已完成。

若此处出现路由冲突,请参考官方文档解决。

云函数使用 VPC 网络

现在一切准备就绪,我们可以使用 SCF 访问轻量服务器了。

为了测试方便,我在轻量服务器上启动了一个 nginx 服务,并开启了对应的防火墙。这里的操作与互联无关,因此不再赘述。

现在来到云函数的控制台,在对应地域新建云函数。这里选择北京,使用 python 语言。

函数代码使用 python 写了一个简单的获取网页内容并输出:

# -*- coding: utf8 -*-
import json
import urllib.request
def main_handler(event, context):
    resp = urllib.request.urlopen("http://10.0.24.x") # 这里是轻量服务器的内网 IP
    print(resp.read())

在填写完函数内容后,展开下方的高级设置:

高级设置

我们需要关注的是这里的私有网络,其他设置按需调整即可,与互联无关。

我们勾选启用私有网络:

启用私有网络

待加载完成后,选择我们之前创建好并关联了云联网的私有网络,然后点击页面最下方的完成即可。

待函数部署成功后,查看函数配置,看到对应的 VPC 配置,即为配置成功。

函数配置

接下来测试一下函数是否可以跑通,进入函数代码页面,点击编辑器下方的测试:

函数测试

测试完成后,会在下方输出测试结果:

测试结果

可以看到,云函数成功通过内网拉到了位于轻量云服务器上的 nginx 服务的数据。

Linux 下查看指定进程环境变量信息

Linux 会为每个进程生成一个目录,保存了进程相关的大量信息。具体位置在:

/proc/${pid}

一般有如下文件:

total 0
-rw-r--r-- 1 root root 0 Dec 31 00:00 autogroup
-r-------- 1 root root 0 Dec 31 00:00 auxv
-r--r--r-- 1 root root 0 Dec 31 00:00 cgroup
--w------- 1 root root 0 Dec 31 00:00 clear_refs
-r--r--r-- 1 root root 0 Dec 31 00:00 cmdline
-rw-r--r-- 1 root root 0 Dec 31 00:00 comm
-rw-r--r-- 1 root root 0 Dec 31 00:00 coredump_filter
-r--r--r-- 1 root root 0 Dec 31 00:00 cpuset
lrwxrwxrwx 1 root root 0 Dec 31 00:00 cwd -> /root
-r-------- 1 root root 0 Dec 31 00:00 environ
lrwxrwxrwx 1 root root 0 Dec 31 00:00 exe -> /usr/bin/bash
dr-x------ 2 root root 0 Dec 31 00:00 fd
dr-x------ 2 root root 0 Dec 31 00:00 fdinfo
-r--r--r-- 1 root root 0 Dec 31 00:00 hostinfo
-r-------- 1 root root 0 Dec 31 00:00 io
-r--r--r-- 1 root root 0 Dec 31 00:00 latency
-r--r--r-- 1 root root 0 Dec 31 00:00 limits
-rw-r--r-- 1 root root 0 Dec 31 00:00 loginuid
-r--r--r-- 1 root root 0 Dec 31 00:00 maps
-rw------- 1 root root 0 Dec 31 00:00 mem
-r--r--r-- 1 root root 0 Dec 31 00:00 mountinfo
-r--r--r-- 1 root root 0 Dec 31 00:00 mounts
-r-------- 1 root root 0 Dec 31 00:00 mountstats
dr-xr-xr-x 7 root root 0 Dec 31 00:00 net
dr-x--x--x 2 root root 0 Dec 31 00:00 ns
-r--r--r-- 1 root root 0 Dec 31 00:00 numa_maps
-rw-r--r-- 1 root root 0 Dec 31 00:00 oom_adj
-r--r--r-- 1 root root 0 Dec 31 00:00 oom_score
-rw-r--r-- 1 root root 0 Dec 31 00:00 oom_score_adj
-r--r--r-- 1 root root 0 Dec 31 00:00 pagemap
-r--r--r-- 1 root root 0 Dec 31 00:00 personality
lrwxrwxrwx 1 root root 0 Dec 31 00:00 root -> /
-rw-r--r-- 1 root root 0 Dec 31 00:00 sched
-r--r--r-- 1 root root 0 Dec 31 00:00 sessionid
-r--r--r-- 1 root root 0 Dec 31 00:00 smaps
-r--r--r-- 1 root root 0 Dec 31 00:00 stack
-r--r--r-- 1 root root 0 Dec 31 00:00 stat
-r--r--r-- 1 root root 0 Dec 31 00:00 statm
-r--r--r-- 1 root root 0 Dec 31 00:00 status
-r--r--r-- 1 root root 0 Dec 31 00:00 syscall
dr-xr-xr-x 3 root root 0 Dec 31 00:00 task
-r--r--r-- 1 root root 0 Dec 31 00:00 wchan

其中的 exe 指向进程的可执行文件,cwd 指向进程的工作目录,environ 就是进程所看到的环境变量的信息了。

直接 cat 这个文件,可以看到所有的变量信息。但是这里看到的变量都是挤在一起的,没有换行。

使用 vim 打开这个文件,可以看到实际上文件中是有符号分隔的,在 vim 里展示为 ^@ 。经过搜索,该符号代表的其实是 \0,也就是字符串结尾的意思。于是使用 tr 命令稍作处理,即可得到可读性强的换行分隔的环境变量信息:

tr '\0' '\n'< /proc/${pid}/environ

AWS Lightsail 修改 DB 参数

AWS 推出的 Lightsail Database 是目前比较实惠的托管 DB 方案。不过相对来说,Lightsail 的控制面板功能较少,很多参数无法修改。经过搜索发现,其实我们有很多参数可以调,只是需要通过命令行的方式来调整。

准备工作

安装 aws-cli 工具

https://aws.amazon.com/cli/

https://lightsail.aws.amazon.com/ls/docs/en_us/articles/lightsail-how-to-set-up-and-configure-aws-cli

Linux:

sudo apt-get install awscli

MacOS:

brew install awscli

Python(通用):

pip install awscli

设置 Access Key

https://lightsail.aws.amazon.com/ls/docs/en_us/articles/lightsail-how-to-set-up-access-keys-to-use-sdk-api-cli

先在 AWS 控制台新建用户或 Key:

https://console.aws.amazon.com/iam/home#/users

然后执行:

aws configure

按照提示依次输入:

AWS Access Key ID 控制台中创建的 Key
AWS Secret Access Key 控制台中创建的 Key 对于的 Secret
Default region name 可用区,https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html
Default output format 输出格式,建议 json

获取现有设置

aws lightsail get-relational-database-parameters --relational-database-name DatabaseName > current_params.json

注意 DatabaseName 替换为创建 DB 时设置的名称。就是 Lightsail 控制面板里显示的那个。

执行成功后打开 json 文件,可以看到所有变量。

注意每个变量有几个属性:

Allowed values 允许的变量范围
Apply method 变量的生效时间。immediate 表示立即生效,pending-reboot 表示重启后生效
Apply type 底层引擎支持的生效方式。dynamic 动态,可以立即生效,static 静态,必须重启后才能生效
Data type 数据类型
Description 变量描述
Is modifiable 能否修改
Parameter name 变量名

这里我们用最大连接数举例:

{
    "allowedValues": "1-100000",
    "applyMethod": "pending-reboot",
    "applyType": "dynamic",
    "dataType": "integer",
    "description": "The number of simultaneous client connections allowed.",
    "isModifiable": true,
    "parameterName": "max_connections",
    "parameterValue": "{DBInstanceClassMemory/12582880}"
}

可以看到,最大连接数是一个动态变量,整型,可修改。默认是实例内存大小/12582880,也就是 1G 内存约 80 个链接。实际比这个数值少,没有具体深究。

修改设置

找到了对于的参数,就可以修改了。修改参数使用的指令是:

aws lightsail update-relational-database-parameters --relational-database-name DatabaseName --parameters "parameterName=ParameterName,parameterValue=NewParameterValue,applyMethod=ApplyMethod"

DatabaseName 是实例名,ParameterName 替换为要修改的变量,NewParameterValue 替换为变量的值,ApplyMethod 替换为想要的生效方式。

比如,我们修改最大连接数到 1000,重启后生效,对应的命令为:

aws lightsail update-relational-database-parameters --relational-database-name DatabaseName --parameters "parameterName=max_connections,parameterValue=1000,applyMethod=pending-reboot"

成功后会收到这样的响应:

{
    "operations": [
        {
            "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
            "resourceName": "DatabaseName",
            "resourceType": "RelationalDatabase",
            "createdAt": 1570000000.000,
            "location": {
                "availabilityZone": "ap-northeast-1a",
                "regionName": "ap-northeast-1"
            },
            "isTerminal": true,
            "operationDetails": "",
            "operationType": "UpdateRelationalDatabaseParameters",
            "status": "Succeeded",
            "statusChangedAt": 1570000000.000
        }
    ]
}

看到 status Succeeded 就是设置成功啦!

官方文档:

https://lightsail.aws.amazon.com/ls/docs/en_us/articles/amazon-lightsail-updating-database-parameters

 

远程桌面工具对比

由于之前很好用的 TeamView 最近开始大力检测商业用途,导致很多个人用户几乎完全无法使用,因此急需找一个替代品来使用。

通过小众软件的文章推荐,大致看到了两个不错的选择:AnyDesk 和 Remote Utilities。至于向日葵之类需要注册、登录、安装的国内软件不在考虑范围内。

于是开始试用 AnyDesk 和 Remote Utilities,下面是一个简单的对比。

  1. 从配置上来说,两款软件的配置项都很相似。相对来说 AnyDesk 的配置界面更亲民,组织更合理。但是配置项相对较少;
  2. 从画质上来说,AnyDesk 更胜一筹。相对来说颜色失真更少,帧率更高。不过这里没有使用工具测量,纯人眼感受;
  3. 从系统资源占用来看,二者相差不大。在同一台机器同网络同分辨率的环境下,基本 CPU 和 内存占用都差不多;
  4. 从网络流量来看,AnyDesk 远超 Remote Utilities。还是相同的测试,屏幕持续播放相同的视频,AnyDesk 的流量峰值基本在 1M 以内,偶尔会窜到 10M,但是会迅速回落。而 Remote Utilities 则基本稳定在 10M 以上,最高可以达到 50M。相比较而言,Remote Utilities 的流量优化就大大弱于 AnyDesk 了;
  5. 从部署上来看,Remote Utilities 支持自定义 ID 服务器。当工作在这种模式下的时候,内网机器不需要访问外网就可以互相连接,是一个很不错的点。但是呢,由于 Remote Utilities 对网络流量优化较差,这台 ID 服务器需要有极高的带宽和极佳的网络质量来支撑机器间的互联。因此,个人用户几乎无法使用这个功能,因为服务器的费用可能比直接购买 TeamView 还高;
  6. 从客户端上看,AnyDesk 的移动客户端相对来说做的更好。整体界面、操控手感都远胜 Remote Utilities。感觉 Remote Utilities 的移动客户端就是个残废。。。至少到目前为止,那几个触屏手势我都没掌握要怎么触发;

综上,如果是对隐私要求非常严格的用户或者存在纯内网环境,那么 Remote Utilities 是个不错的选择。除此之外,还是强烈建议使用 AnyDesk 吧!