image::logo/horizontal.png[Ubuntu 加固] = Ubuntu 加固。 Systemd 版本。
:icons: font
快速提高 Ubuntu 服务器安全性的方法。
使用新安装和配置的系统作为参考或黄金镜像。将该镜像用作基线安装介质,并确保任何未来的安装都使用配置管理工具(如 https://www.ansible.com/[Ansible] 或 https://puppet.com/[Puppet])符合基准和策略。
在 Ubuntu 20.04 Focal Fossa
和 Ubuntu 22.04 Jammy Jellyfish
上测试通过。
如果您只对安全focused的systemd配置感兴趣,可以查看link:systemd.adoc[单独的文档]。
如果您想测试主机设置,可以在link:README.adoc#tests[这里]找到说明。
注意:阅读代码,不要在未经非生产环境测试的情况下运行此脚本。该代码不具有幂等性,请在生产环境中使用 https://github.com/konstruktoid/ansible-role-hardening[Ansible 角色]代替。
注意:在 https://github.com/konstruktoid/hardening/actions/workflows/slsa.yml[slsa 工作流程]下有一个 https://slsa.dev/[SLSA] 工件,用于文件校验和验证。
== Packer 模板和 Ansible playbook
在 link:packer/[Packer 目录]中提供了 https://www.packer.io/[Packer] 模板。
在 https://github.com/konstruktoid/ansible-role-hardening[konstruktoid/ansible-role-hardening] 仓库中提供了 Ansible playbook。
== 使用方法
. 开始服务器安装。
. 选择语言和键盘布局。
. 选择"Ubuntu Server (minimized)"。
. 配置网络连接。
. 对系统进行分区,请参阅下面的建议。
. 不要安装 OpenSSH 服务器、"Featured Server Snaps"或任何其他软件包。
. 完成安装并重启。
. 登录。
. 如果需要,使用 grub-mkpasswd-pbkdf2
设置 Grub2 密码。有关更多信息,请参阅 https://help.ubuntu.com/community/Grub2/Passwords[https://help.ubuntu.com/community/Grub2/Passwords]。
. 安装必要的软件包:sudo apt-get -y install git net-tools procps --no-install-recommends
。
. 下载脚本:git clone https://github.com/konstruktoid/hardening.git
。
. 在 ubuntu.cfg
文件中更改配置选项。确保更新 CHANGEME 变量,否则脚本将失败。
. 运行脚本:sudo bash ubuntu.sh
。
. 重启。
=== 推荐的分区和选项
[source,shell]
/boot (rw) /home (rw,nosuid,nodev) /var/log (rw,nosuid,nodev,noexec) /var/log/audit (rw,nosuid,nodev,noexec) /var/tmp (rw,nosuid,nodev,noexec)
注意,脚本会自动添加 /tmp
。
== 配置选项
[source,shell]
FW_ADMIN='127.0.0.1' // <1> SSH_GRPS='sudo' // <2> SSH_PORT='22' // <3> SYSCTL_CONF='./misc/sysctl.conf' // <4> AUDITD_MODE='1' // <5> AUDITD_RULES='./misc/audit-base.rules ./misc/audit-aggressive.rules ./misc/audit-docker.rules' // <6> LOGROTATE_CONF='./misc/logrotate.conf' // <7> NTPSERVERPOOL='0.ubuntu.pool.ntp.org 1.ubuntu.pool.ntp.org 2.ubuntu.pool.ntp.org 3.ubuntu.pool.ntp.org pool.ntp.org' // <8> TIMEDATECTL='' // <9> VERBOSE='N' // <10> AUTOFILL='N' // <11> ADMINEMAIL="root@localhost" // <12> KEEP_SNAPD='Y' // <13> CHANGEME='' // <14>
配置文件 // <15>
ADDUSER='/etc/adduser.conf' AUDITDCONF='/etc/audit/auditd.conf' AUDITRULES='/etc/audit/rules.d/hardening.rules' COMMONPASSWD='/etc/pam.d/common-password' COMMONACCOUNT='/etc/pam.d/common-account' COMMONAUTH='/etc/pam.d/common-auth' COREDUMPCONF='/etc/systemd/coredump.conf' DEFAULTGRUB='/etc/default/grub.d' DISABLEFS='/etc/modprobe.d/disablefs.conf' DISABLEMOD='/etc/modprobe.d/disablemod.conf' DISABLENET='/etc/modprobe.d/disablenet.conf' FAILLOCKCONF='/etc/security/faillock.conf' JOURNALDCONF='/etc/systemd/journald.conf' LIMITSCONF='/etc/security/limits.conf' LOGINDCONF='/etc/systemd/logind.conf' LOGINDEFS='/etc/login.defs' LOGROTATE='/etc/logrotate.conf' PAMLOGIN='/etc/pam.d/login' PSADCONF='/etc/psad/psad.conf' PSADDL='/etc/psad/auto_dl' RESOLVEDCONF='/etc/systemd/resolved.conf' RKHUNTERCONF='/etc/default/rkhunter' RSYSLOGCONF='/etc/rsyslog.conf' SECURITYACCESS='/etc/security/access.conf' SSHFILE='/etc/ssh/ssh_config' SSHDFILE='/etc/ssh/sshd_config' SYSCTL='/etc/sysctl.conf' SYSTEMCONF='/etc/systemd/system.conf' TIMESYNCD='/etc/systemd/timesyncd.conf' UFWDEFAULT='/etc/default/ufw' USERADD='/etc/default/useradd' USERCONF='/etc/systemd/user.conf'
<1> 允许通过 SSH 连接的 IP 地址,用空格分隔。
<2> 用户必须属于哪个组才能通过 SSH 访问,用空格分隔。
<3> 配置 SSH 端口。
<4> 更严格的 sysctl 设置。
<5> Auditd 失败模式。0=silent 1=printk 2=panic。
<6> Auditd 规则。
<7> Logrotate 设置。
<8> NTP 服务器池。
<9> 添加特定时区或留空使用系统默认设置。
<10> 是否显示所有细节。
<11> 让脚本猜测 FW_ADMIN
和 SSH_GRPS
设置。
<12> 添加有效的电子邮件地址,以便 PSAD 可以发送通知。
<13> 如果为 'Y'
,则将保留 snapd
包以防止被移除。
<14> 添加一些内容以验证您确实浏览了代码。
<15> 默认配置文件位置。
== 函数
=== 按执行顺序排列的函数列表
注意,所有函数在代码中都有 f_
前缀。
==== pre
设置 apt
标志并执行基本权限检查。
pre
函数位于 link:scripts/pre[./scripts/pre]。
==== kernel
如果 hashsize
存在且可写,则将 https://github.com/jeffmurphy/NetPass/blob/master/doc/netfilter_conntrack_perf.txt#L175[/sys/module/nf_conntrack/parameters/hashsize] 设置为 1048576。
如果 lockdown
存在且可写,则将 https://man7.org/linux/man-pages/man7/kernel_lockdown.7.html[/sys/kernel/security/lockdown] 设置为 confidentiality
。
kernel
函数位于 link:scripts/kernel[./scripts/kernel]。
==== firewall
如果已安装,则配置 https://help.ubuntu.com/community/UFW[UFW]。
允许来自 $FW_ADMIN
中的地址到 $SSH_PORT
的连接。
设置日志记录和 IPT_SYSCTL=/etc/sysctl.conf
。
firewall
函数位于 link:scripts/ufw[./scripts/ufw]。
==== disablenet
禁用 dccp
、sctp
、rds
和 tipc
内核模块。
disablenet
函数位于 link:scripts/disablenet[./scripts/disablenet]。
==== disablefs
禁用 cramfs
、freevxfs
、jffs2
、ksmbd
、hfs
、hfsplus
、udf
内核模块。
disablefs
函数位于 link:scripts/disablefs[./scripts/disablefs]。
==== disablemod
禁用 bluetooth
、bnep
、btusb
、cpia2
、firewire-core
、floppy
、n_hdlc
、net-pf-31
、pcspkr
、soundcore
、thunderbolt
、usb-midi
、usb-storage
、uvcvideo
和 v4l2_common
内核模块。
请注意,禁用 usb-storage
模块将禁用所有 USB 存储设备的使用。如果需要使用这些设备,应相应配置 USBGuard
。
disablemod
函数位于 link:scripts/disablemod[./scripts/disablemod]。
==== systemdconf
在 $SYSTEMCONF
和 $USERCONF
中设置 CrashShell=no
、DefaultLimitCORE=0
、DefaultLimitNOFILE=1024
、DefaultLimitNPROC=1024
和 DumpCore=no
。
systemdconf
函数位于 link:scripts/systemdconf[./scripts/systemdconf]。
==== resolvedconf
在 $RESOLVEDCONF
中设置 DNS=$dnslist
、DNSOverTLS=opportunistic
、DNSSEC=allow-downgrade
和 FallbackDNS=1.0.0.1
,其中 $dnslist
是一个包含 /etc/resolv.conf
中存在的名称服务器的数组。
resolvedconf
函数位于 link:scripts/resolvedconf[./scripts/resolvedconf]。
==== logindconf
在 $LOGINDCONF
中设置 IdleAction=lock
、IdleActionSec=15min
、KillExcludeUsers=root
、KillUserProcesses=1
和 RemoveIPC=yes
。
logindconf
函数位于 link:scripts/logindconf[./scripts/logindconf]。
==== journalctl
将 link:misc/logrotate.conf[./misc/logrotate.conf] 复制到 $LOGROTATE
。
在 $JOURNALDCONF
中设置 Compress=yes
、ForwardToSyslog=yes
和 Storage=persistent
。
如果 RSYSLOGCONF
可写,则在其中设置 $FileCreateMode 0600/
。
journalctl
函数位于 link:scripts/journalctl[./scripts/journalctl]。
==== timesyncd
在 $TIMESYNCD
中设置 NTP=${SERVERARRAY}
、FallbackNTP=${FALLBACKARRAY}
和 RootDistanceMaxSec=1
,其中数组包含最多四个延迟小于 50ms 的时间服务器。
timesyncd
函数位于 link:scripts/timesyncd[./scripts/timesyncd]。
==== fstab
如果 /etc/fstab
中存在 /boot
和 /home
分区,则将它们配置为 defaults,nosuid,nodev
。
如果 /etc/fstab
中存在 /var/log
、/var/log/audit
和 /var/tmp
分区,则将它们配置为 defaults,nosuid,nodev,noexec
。
如果 /etc/fstab
中不存在以下分区,则添加:
/run/shm tmpfs rw,noexec,nosuid,nodev
/dev/shm tmpfs rw,noexec,nosuid,nodev
/proc proc rw,nosuid,nodev,noexec,relatime,hidepid=2
从 /etc/fstab
中移除所有软盘驱动器。
将 ./config/tmp.mount[./config/tmp.mount] 复制到 /etc/systemd/system/tmp.mount
,从 /etc/fstab
中移除 /tmp
,并启用 tmpfs /tmp
挂载。
/proc
的 hidepid
选项在 https://www.kernel.org/doc/html/latest/filesystems/proc.html#mount-options[https://www.kernel.org/doc/html/latest/filesystems/proc.html#mount-options] 中有描述。
fstab
函数位于 link:scripts/fstab[./scripts/fstab]。
==== prelink
将二进制文件和库恢复到预链接前的原始内容,并卸载 prelink
。
prelink
函数位于 link:scripts/prelink[./scripts/prelink]。
==== aptget_configure
设置以下 apt
选项:
Acquire::http::AllowRedirect "false";
APT::Get::AllowUnauthenticated "false";
APT::Periodic::AutocleanInterval "7";
APT::Install-Recommends "false";
APT::Get::AutomaticRemove "true";
APT::Install-Suggests "false";
Acquire::AllowDowngradeToInsecureRepositories "false";
Acquire::AllowInsecureRepositories "false";
APT::Sandbox::Seccomp "1";
aptget_configure
函数位于 link:scripts/aptget[./scripts/aptget]。
==== aptget
升级已安装的软件包。
aptget
函数位于 link:scripts/aptget[./scripts/aptget]。
==== hosts
在 /etc/hosts.allow
中设置 sshd : ALL : ALLOW
和 ALL: LOCAL, 127.0.0.1
,在 /etc/hosts.deny
中设置 ALL: ALL
。
有关主机访问控制文件的格式,请参见 https://manpages.ubuntu.com/manpages/jammy/man5/hosts_access.5.html[https://manpages.ubuntu.com/manpages/jammy/man5/hosts_access.5.html]。
hosts
函数位于 link:scripts/hosts[./scripts/hosts]。
==== issue
在 /etc/issue
、/etc/issue.net
和 /etc/motd
中写入仅限授权使用的通知。
移除 /etc/update-motd.d/
中每个文件的可执行标志。
issue
函数位于 link:scripts/issue[./scripts/issue]。
==== sudo
使用 https://manpages.ubuntu.com/manpages/jammy/man8/pam_wheel.8.html[pam_wheel] 将 su
访问权限限制为 sudo
组成员。
设置以下 https://manpages.ubuntu.com/manpages/jammy/man5/sudoers.5.html[sudo 选项]:
!pwfeedback
!visiblepw
logfile=/var/log/sudo.log
passwd_timeout=1
timestamp_timeout=5
use_pty
sudo
函数位于 link:scripts/sudo[./scripts/sudo]。
==== logindefs
在 https://manpages.ubuntu.com/manpages/jammy/man5/login.defs.5.html[$LOGINDEFS] 中写入以下内容:
LOG_OK_LOGINS yes
UMASK 077
PASS_MIN_DAYS 1
PASS_MAX_DAYS 60
DEFAULT_HOME no
ENCRYPT_METHOD SHA512
USERGROUPS_ENAB no
SHA_CRYPT_MIN_ROUNDS 10000
SHA_CRYPT_MAX_ROUNDS 65536
logindefs
函数位于 link:scripts/logindefs[./scripts/logindefs]。
==== sysctl
将 link:misc/sysctl.conf[./misc/sysctl.conf] 复制到 $SYSCTL
。
有关设置选项的说明,请参见 https://www.kernel.org/doc/html/latest/admin-guide/sysctl/[https://www.kernel.org/doc/html/latest/admin-guide/sysctl/]。
sysctl
函数位于 link:scripts/sysctl[./scripts/sysctl]。
==== limitsconf
在 https://manpages.ubuntu.com/manpages/jammy/en/man5/limits.conf.5.html[$LIMITSCONF] 中设置以下内容:
hard maxlogins 10
hard core 0
soft nproc 512
hard nproc 1024
limitsconf
函数位于 link:scripts/limits[./scripts/limits]。
==== adduser
在 $ADDUSER
中设置 DIR_MODE=0750
、DSHELL=/bin/false
和 USERGROUPS=yes
。
在 $USERADD
中设置 INACTIVE=30
和 SHELL=/bin/false
。
adduser
函数位于 link:scripts/adduser[./scripts/adduser]。
==== rootaccess
将 +:root:127.0.0.1/'
写入 $SECURITYACCESS
,并将 console
写入 /etc/securetty
。
屏蔽 debug-shell。
rootaccess
函数位于 ./scripts/rootaccess。
==== package_install
安装 acct
、aide-common
、cracklib-runtime
、debsums
、gnupg2
、haveged
、libpam-pwquality
、libpam-tmpdir
、needrestart
、openssh-server
、postfix
、psad
、rkhunter
、sysstat
、systemd-coredump
、tcpd
、update-notifier-common
和 vlock
。
package_install
函数位于 ./scripts/packages。
==== psad
安装和配置 PSAD
psad
函数位于 ./scripts/psad。
==== coredump
将 Storage=none
和 ProcessSizeMax=0
写入 $COREDUMPCONF
。
coredump
函数位于 ./scripts/coredump。
==== usbguard
安装和配置 USBGuard。
usbguard
函数位于 ./scripts/usbguard。
==== postfix
安装 postfix
并使用 postconf 设置 disable_vrfy_command=yes
、inet_interfaces=loopback-only
、smtpd_banner="\$myhostname"
和 smtpd_client_restrictions=permit_mynetworks,reject
。
postfix
函数位于 ./scripts/postfix。
==== apport
禁用 apport、ubuntu-report 和 popularity-contest。
apport
函数位于 ./scripts/apport。
==== motdnews
禁用 apt_news
和 motd-news。
motdnews
函数位于 ./scripts/motdnews。
==== rkhunter
在 $RKHUNTERCONF
中设置 CRON_DAILY_RUN="yes"
和 APT_AUTOGEN="yes"
。
rkhunter
函数位于 ./scripts/rkhunter。
==== sshconfig
在 $SSHFILE
中设置 HashKnownHosts yes
、Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr
和 MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256
。
sshconfig
函数位于 ./scripts/sshdconfig。
==== sshdconfig
配置 OpenSSH
守护进程。配置更改将放置在 Include
选项定义的目录中(如果存在),否则将修改 $SSHDFILE。
默认情况下,/etc/ssh/sshd_config.d/hardening.conf
将包含以下内容:
[source,shell]
AcceptEnv LANG LC_* AllowAgentForwarding no AllowGroups sudo AllowTcpForwarding no Banner /etc/issue.net Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes256-ctr ClientAliveCountMax 3 ClientAliveInterval 200 Compression no GSSAPIAuthentication no HostbasedAuthentication no IgnoreUserKnownHosts yes KbdInteractiveAuthentication no KerberosAuthentication no KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp521,ecdh-sha2-nistp384,ecdh-sha2-nistp256,diffie-hellman-group-exchange-sha256 LogLevel VERBOSE LoginGraceTime 20 Macs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,hmac-sha2-512,hmac-sha2-256 MaxAuthTries 3 MaxSessions 3 MaxStartups 10:30:60 PasswordAuthentication no PermitEmptyPasswords no PermitRootLogin no PermitUserEnvironment no Port 22 PrintLastLog yes PrintMotd no RekeyLimit 512M 1h StrictModes yes TCPKeepAlive no UseDNS no UsePAM yes X11Forwarding no
sshdconfig
函数位于 ./scripts/sshdconfig。
==== password
将 ./config/pwquality.conf 复制到 /etc/security/pwquality.conf
,
从 PAM $COMMONAUTH
中删除 nullok
。
根据安装情况配置 faillock 或 pam_tally2。
将密码列表添加到 cracklib。
password
函数位于 ./scripts/password。
==== cron
禁用 atd 并只允许 root 使用 at 或 cron。
cron
函数位于 ./scripts/cron。
==== ctrlaltdel
屏蔽 ctrl-alt-del.target。
ctrlaltdel
函数位于 ./scripts/ctraltdel。
==== auditd
配置 auditd。
有关使用的规则,请参见 ./misc/audit-base.rules、./misc/audit-aggressive.rules 和 ./misc/audit-docker.rules。
auditd
函数位于 ./scripts/auditd。
==== aide
从 AIDE 中排除 /var/lib/lxcfs/cgroup
和 /var/lib/docker
。
aide
函数位于 ./scripts/aide。
==== rhosts
删除任何现有的 hosts.equiv
或 .rhosts
文件。
rhosts
函数位于 ./scripts/rhosts。
==== users
删除 games
、gnats
、irc
、list
、news
、sync
和 uucp
用户。
users
函数位于 ./scripts/users。
==== lockroot
锁定 root 账户
lockroot
函数位于 link:scripts/lockroot[./scripts/lockroot]。
==== package_remove
移除 apport*
、autofs
、avahi*
、beep
、git
、pastebinit
、popularity-contest
、rsh*
、rsync
、talk*
、telnet*
、tftp*
、whoopsie
、xinetd
、yp-tools
、ypbind
等软件包。
package_remove
函数位于 link:scripts/packages[./scripts/packages]。
==== suid
确保 link:misc/suid.list[./misc/suid.list] 中的可执行文件没有设置 suid 位。
suid
函数位于 link:scripts/suid[./scripts/suid]。
==== restrictcompilers
将所有已安装的编译器的模式更改为 0750
。
restrictcompilers
函数位于 link:scripts/compilers[./scripts/compilers]。
==== umask
将默认 https://manpages.ubuntu.com/manpages/jammy/man2/umask.2.html[umask] 设置为 077
。
umask
函数位于 link:scripts/umask[./scripts/umask]。
==== path
将 ./config/initpath.sh[./config/initpath.sh] 复制到 /etc/profile.d/initpath.sh
,并为 root
用户设置 PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin
,为其他用户设置 PATH=/usr/local/bin:/usr/sbin:/usr/bin:/bin:/snap/bin
。
path
函数位于 link:scripts/path[./scripts/path]。
==== aa_enforce
强制执行可用的 https://manpages.ubuntu.com/manpages/jammy/en/man7/apparmor.7.html[apparmor] 配置文件。
aa_enforce
函数位于 link:scripts/apparmor[./scripts/apparmor]。
==== aide_post
创建一个新的 AIDE 数据库。
aide_post
函数位于 link:scripts/aide[./scripts/aide]。
==== aide_timer
将 systemd AIDE 检查服务和计时器复制到 /etc/systemd/system/。
aide_timer
函数位于 link:scripts/aide[./scripts/aide]。
==== aptget_noexec
添加 DPkg::Pre-Invoke
和 DPkg::Post-Invoke
以确保软件包更新在 noexec
/tmp
分区上不会失败。
aptget_noexec
函数位于 link:scripts/aptget[./scripts/aptget]。
==== aptget_clean
运行 https://manpages.ubuntu.com/manpages/jammy/en/man8/apt-get.8.html[apt-get] clean
和 autoremove
。
aptget_clean
函数位于 link:scripts/aptget[./scripts/aptget]。
==== systemddelta
如果在详细模式下运行,则执行 https://manpages.ubuntu.com/manpages/jammy/man1/systemd-delta.1.html[systemd-delta]。
systemddelta
函数位于 link:scripts/systemddelta[./scripts/systemddelta]。
==== post
确保安装 https://manpages.ubuntu.com/manpages/jammy/man1/fwupdmgr.1.html[fwupdmgr] 和 https://packages.ubuntu.com/jammy/secureboot-db[secureboot-db],并更新 GRUB。
post
函数位于 link:scripts/post[./scripts/post]。
==== checkreboot
检查是否需要重启。
checkreboot
函数位于 link:scripts/reboot[./scripts/reboot]。
== 测试 在 link:tests/[tests 目录] 中有大约 760 个 https://github.com/bats-core/bats-core[Bats 测试],用于测试上述大部分设置。
[source,shell]
sudo apt-get -y install bats git clone https://github.com/konstruktoid/hardening.git cd hardening/tests/ sudo bats .
=== 使用 Vagrant 进行测试自动化
运行 bash ./runTests.sh
将使用 https://www.vagrantup.com/[Vagrant] 在所有支持的 Ubuntu 版本上运行上述所有测试、https://github.com/CISOfy/Lynis[Lynis] 和 https://www.open-scap.org/[OpenSCAP],并使用 https://www.cisecurity.org/benchmark/ubuntu_linux[CIS Ubuntu 基准]。
该脚本将生成一个名为 TESTRESULTS.adoc
的文件和 HTML 格式的 CIS 报告。
=== 测试主机
运行位于 link:tests/[tests 目录] 中的 bash ./runHostTests.sh
将生成一个 TESTRESULTS-<HOSTNAME>.adoc
报告。
运行位于 link:tests/[tests 目录] 中的 bash ./runHostTestsCsv.sh
将生成一个 TESTRESULTS-<HOSTNAME>.csv
报告。
== 推荐阅读 https://public.cyber.mil/stigs/downloads/?_dl_facet_stigs=operating-systems%2Cunix-linux[Canonical Ubuntu 20.04 LTS STIG - Ver 1, Rel 3] + https://www.cisecurity.org/benchmark/distribution_independent_linux/[CIS Distribution Independent Linux Benchmark] + https://www.cisecurity.org/benchmark/ubuntu_linux/[CIS Ubuntu Linux Benchmark] + https://www.ncsc.gov.uk/collection/end-user-device-security/platform-specific-guidance/ubuntu-18-04-lts[EUD Security Guidance: Ubuntu 18.04 LTS] + https://wiki.ubuntu.com/Security/Features + https://help.ubuntu.com/community/StricterDefaults +
== 贡献 你想贡献吗?太好了!我们随时欢迎贡献,无论大小。如果你发现了什么奇怪的地方,请随时 https://github.com/konstruktoid/hardening/issues/[提交新问题],通过 https://github.com/konstruktoid/hardening/pulls[创建拉取请求] 来改进代码,或者通过 https://github.com/sponsors/konstruktoid[赞助这个项目]。
Logo 由 https://github.com/reallinfo[reallinfo] 提供。