变更内核版本

proxmox8.2默认的内核为proxmox-kernel-6.8.4-2-pve,但后续的步骤不支持该版本,需进行降级

安装新内核

1
apt install proxmox-kernel-6.5.13-5-pve

固定启动内核版本

1
proxmox-boot-tool kernel pin 6.5.13-5-pve

重启系统

内核验证

重启后是用uname -a查看结果是否为Linux pve 6.5.13-5-pve #1 SMP PREEMPT_DYNAMIC PMX 6.5.13-5 (2024-04-05T11:03Z) x86_64 GNU/Linux,是的话才代表内核切换成功

编译i915-sriov-dkms(仅适用于12代处理器,我用的n100)

编译前基础组件安装

1
2
3
4
apt install proxmox-header-6.5.13-5-pve
apt install git
apt install build-*
apt install dkms

下载

1
2
cd /usr/src
git clone https://github.com/strongtz/i915-sriov-dkms.git i915-sriov-dkms-6.5

修改编译配置

1
2
cd /usr/src/i915-sriov-dkms-6.5
git reset --hard d2b3b6e

关于为什么要切换git库的版本号,否则编译不会成功。具体可以见issue165pr164

修改dkms.conf文件,将PACKAGE_NAME和PACKAGE_VERSION改为如下值

1
2
PACKAGE_NAME="i915-sriov-dkms"
PACKAGE_VERSION="6.5"

编译

1
dkms install --force -m i915-sriov-dkms -v 6.5

结束后运行dkms status,确认是否有以下结果

1
i915-sriov-dkms/6.5, 6.5.13-5-pve, x86_64: installed

有则说明编译成功

配置

修改grub

修改/etc/default/grub, 给GRUB_CMD_LINE_LINUX添加intel_iommu=on i915.enable_guc=3 i915.max_vfs=7

使用update-grub刷新grub

修改modules

修改/etc/modules, 添加以下四行

1
2
3
4
vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd

使用update-initramfs -u刷新

设置虚拟核显数

安装开机设置工具

1
apt install sysfsutils

编辑/etc/sysfs.conf,加入

1
devices/pci0000:00/0000:00:02.0/sriov_numvfs = 1

其中1代表虚拟出一个核显,主要是n100太弱了,且我只需要虚拟出一个给windows硬解码,左边的key是核显对应的位置。得通过lspci结合pve web ui才能确定,此处只代表n100的

设置完成后重启

验证

通过web ui可以看到有如下图, 则代表成功

成功后通过web ui给指定的主机加pci设备直通即可,记得先别勾上主GPU,启动后先去装上核显驱动,关机,然后将主gpu勾选上,重新开机,开机后打开任务管理器看到GPU,就说明GPU直通了

注意: 设置主GPU可能会导致pve web控制台黑屏,若只需要硬件加速,不需要图像输出,可以考虑关闭主GPU的选项,原因见wiki,如果出现了43错误大概率也是因为驱动导致的,可以具体看看wiki

注意: 检查机型是否为qe35, 这有利于提高性能

查看硬盘id

pve宿主机查看硬盘id

1
ls -l /dev/disk/by-id/

挂载硬盘

1
2
#qm set vmid -deviceno /dev/disk/by-id/disk-id
qm set 101 -sata0 /dev/disk/by-id/ata-ST1000LM024_HN-M101MBB_S30YJ9AF306914

vmid: 虚拟机编号
deviceno: 设备编号
disk-id: 上一步查到的磁盘id

硬盘休眠

停止监控

修改/etc/lvm/lvm.conf文件,过滤不需要监控的盘,操作见PVE论坛

1
2
# 添加 "r|/dev/sda|" 到 global_filter
global_filter=["r|/dev/zd.*|","r|/dev/rbd.*|","r|/dev/sda*|"]
1
2
# 重启
pvestatd restart

验证

通过以上方式允许硬盘休眠后,可以让硬盘立即休眠来测试下是否还会被唤醒

1
2
3
4
# 立即休眠
hdparm -y /dev/sda
# 验证
hdparm -C /dev/sda

验证输出中会有drive state is: standby

休眠

验证通过后即可配置硬盘休眠策略

1
2
3
4
# 修改APM_level为127
hdparm -B 127 /dev/sda
# 设置硬盘休眠时间为 5 分钟
hdparm -S 60 /dev/sda

若要持久化按以下步骤操作
编辑/etc/hdparm.conf

1
2
3
4
/dev/disk/by-id/ata-ST500LT012-9WS142_W0V62FGE {
apm = 127
spindown_time = 60
}
1
/usr/lib/pm-utils/power.d/95hdparm-apm resume

注意点

若使用了pve_source更改了首页概要信息,且概要信息中也有硬盘,以上配置会失效。经测试哪怕不选统计通电时间,通电时间也会显示,硬盘休眠后会被唤醒。
但pve_source源码未开放,目前我也无法单独把nvme的监控打开,把sata的监控关上,只能放弃硬盘的监控了

经过一段时间测试,上方的配置仅适合冷备盘,若为热备盘、下载盘(开启做种)、影音盘,apm需改为128,spindown_time改为240,否则频繁的休眠启动会降低机械硬盘的寿命

缘由

在PVE中安装了RockyLinux,已经可以通过ssh进行连接了。但有时候不在内网环境内,我又不喜欢将内网的Linux SSH端口直接暴露在外网,通过PVE远程控制是我比较喜欢的做法,至于PVE WEB暴露在公网上的安全性问题,我是用2FA解决。

但PVE WEB操作linux很不方便的一点是不支持复制粘贴,因为默认的控制台使用的是noVPC。为了支持复制粘贴需要支持xterm.js

设置

教程参见Serial_TerminalEnable Serial Console for Rocky Linux / CentOS / RHEL in Proxmox

为虚拟机设置串口

在宿主机上输入

1
qm set vmid -serial0 socket

或者在web界面配置

配置串口(一般网上教程会跳过的地方)

复制ttyS0服务

1
cp /usr/lib/systemd/system/serial-getty@.service /etc/systemd/system/serial-getty@ttyS0.service

修改serial-getty@ttyS0.serviceExecStart改为如下

1
-/sbin/agetty --keep-baud 115200,38400,9600 %I $TERM

将串口设置自启动

1
2
3
4
5
ln -s /etc/systemd/system/serial-getty@ttyS0.service /etc/systemd/system/getty.target.wants/

systemctl daemon-reload
systemctl start serial-getty@ttyS0.service
systemctl enable serial-getty@ttyS0.service

更新Grub

修改/etc/default/grub中的GRUB_CMDLINE_LINUX

1
GRUB_CMDLINE_LINUX="quiet console=tty0 console=ttyS0,115200" 

若原本GRUB_CMDLINE_LINUX已经有内容,则将其添加最后即可

刷新grub

1
grub2-mkconfig -o "$(readlink -e /etc/grub2.conf)"

最后重启即可

注意点

一开始我按着中文资料做了好几回都不成功,是用dmesg | grep ttyS进行搜索,ttyS0也是存在的,xterm控制台能打开但一直卡在starting serial terminal on interface serial0不动,按网上说的按Enter也没反应。当时想肯定是ttyS0有问题,只是linux基础比较薄弱,最后找到设置中的第二篇教程才设置成功

题外话

当然,也可以使用异地组网来解决我最开始的问题。但异地组网方案像zerotier,为了保证稳定性最好自建planet。自建planet异地组网安全性比内网穿透暴露pve web更高(仅对你本人而言),只是如果你经常去网络管理比较完善的单位,被运维识别出来就比较尴尬了。

最近弄了台小主机做软路由,底层使用pve。pve出厂自带了https,但要求使用8006端口访问,但这会被浏览器标记为不安全,连记住密码都不能使用,为了解决这个问题需要进行两步操作

端口转发与持久化

1
iptables -t nat -A PREROUTING -p tcp --dport 443 -j REDIRECT --to-ports 8006

这样就能通过443端口直接访问pve web ui了,但iptables在重启后会丢失,需要进行持久化

1
apt-get install iptables-persistent

安装过程中,会询问是否保存iptables,选择Yes即可

后续需要恢复iptables,可运行

1
netfilter-persistent reload

添加了iptables规则需要保存,可运行

1
netfilter-persistent save

对应的文件持久化在/etc/iptables文件夹下

以上功能应该也可以通过iptables-save与iptables-restore实现,但我懒得指定路径了,直接用iptables-persistent实现

如果pve是直接被映射到公网上的,可以考虑把8006端口禁用掉

1
2
#禁止访问8006端口
iptables -A INPUT -i ! lo -p tcp --dport 8006 -j DROP

受信任的HTTPS

虽然443端口能够访问了,但因为自带的证书是自签名的,没有CA仍不受信任。

如果只是在内网中使用,一个简单的解决办法是去扒拉pve中的root ca文件,到自己的电脑中安装,这样就能解决问题。

但如果一开始设置的ip及自定义域名跟后续的不一样,那出场自带的ssl证书就废了。此时既可以通过root ca重新签发,也可以使用mkcert生成,反正一个道理,生成的ssl证书上传到pve,需要信任的客户端安装root ca即可。

懒得手工生成,且具备公网域名的用户,可以考虑使用acme。

首先配置acme通用信息,如图所示

数据中心-ACME

先注册默认的acme用户,然后选择自己的dns提供商,我这边是腾讯云

需要注意的是,这边对不同的dns域名提供商所看到的web表单是不一样的,比如阿里云就可以让你直接填对应的key和地址,但腾讯云这边还不行。
API数据字段指的是API凭据,以Key=Value的形式填入就好,Value部分不需要用双引号包起来,那样反而会导致验证失败
至于填入的Key可以参考ACME。如果这里面没有,可以直接到/usr/share/proxmox-acme/dnsapi下看脚本,本质上就是将Key-Value注入到脚本里,脚本来实现不同提供商的验签,最终实现添加dns txt验证记录的目的。至于key去对应的供应商那申请就行

然后到pve节点中,选择凭据,添加

选择刚刚配置的插件填入域名即可

配置完成后点击立即预定凭证, 有效期三个月,每三个月点一下

带有子路径的Spring应用上下文设置

在开发中,程序员往往启动应用能跑就好了,但到了线上环境,应用不一定能够分配到一个域名,从而需要在 URL 上设置前缀

1
www.example.com/spring-app

对于该情况网络上通常使用 Nginx 进行反向代理,一个示范配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# nginx
server {
listen 80;
listen 443;
server_name www.example.com;

location /spring-app/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;

proxy_pass http://ip:port/;
}
}

问题

该配置基本能够满足常见应用的需求,但却仍有缺憾,虽然大多数 api 已经被成功代理,但少数依赖于 ContextPath 对象的方法将发生意料外的行为

因为此时应用的server.servlet.context-path按默认配置为/,按对外部来讲却是/spring-app/,这将导致以下几种常见现象

  1. api 间有重定向行为,但重定向到了以/为基准的路径上,但因为 nginx 反向代理,导致地址错误
  2. 静态资源加载依赖于 ContextPath,但此时 ContextPath 为/导致资源未正常加载

解决方法

设置server.servlet.context-path

针对于 ContextPath 的问题,最简单的便是直接变更应用的 server.servlet.context-path, 配置如下

1
2
3
4
# application.yaml
server:
servlet:
context-path: /spring-app/

这样以上两个问题就能够被解决,但该方案也有自己的问题:

  1. 一个应用只能指定一个 ContextPath,若存在多个不同子路径的反向代理,该方法就无法正常运转
  2. 应用已经在内网被其他服务引用了,但又需要代理到外网的子路径给其他外部服务使用,且内网应用无法修改引用的地址(比如维护人员已经全部离场)

设置server.forward-headers-strategy

1
2
3
# application.yaml
server:
forward-headers-strategy: framework
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# nginx
server {
listen 80;
listen 443;
server_name www.example.com;

location /spring-app/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /spring-app/;
proxy_set_header Host $http_host;

proxy_pass http://ip:port/;
}
}

通过设置在 spring boot 的配置文件中将server.forward-headers-strategy设置为framework,并且在 nginx 设置X-Forwarded-Prefix为与 location 一致,每个请求执行时应用的 ContextPath 将与X-Forwarded-Prefix一致

MinIO 单机部署

安装

安装请参照官方地址

部署

安装完成后,使用以下命令进行启动

1
2
3
4
export MINIO_ROOT_USER='MINIO_ROOT_USER'
export MINIO_ROOT_PASSWORD='MINIO_ROOT_PASSWORD'
export MINIO_BROWSER_REDIRECT_URL=公网域名
nohup minio server --console-address :9001 --config-dir /etc/minio /data/minio > /root/components/minio/minio.log 2>&1 &

MINIO_ROOT_USER 是最高管理员权限账号,若不填默认为 minioadmin
MINIO_ROOT_PASSWORD 是最高管理员权限密码,若不填默认为 minioadmin
MINIO_BROWSER_REDIRECT_URL 是浏览器重定向域名,若希望通过反向代理将服务暴露到公网,通过域名访问,请设置对应域名

配置及安全

访问域名或 ip:port,进入 minio 的控制台,进行 Bucket 的创建,创建完成后即可通过超级管理员的账号进行 minio 的访问

以上已经完成了 minio 开发所需的基本要求,但考虑未来的 MinIO 复用的可能性,建议 MinIO 管理员账号密码不能外泄。而应该到 Policy 菜单新建一种访问策略

图中配置如下所示:

1
2
3
4
5
6
7
8
9
10
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": ["arn:aws:s3:::template-*/*"]
}
]
}

配置描述了即将被创建的策略拥有以 template 开头的所有 bucket 的所有操作权限。要是不嫌麻烦可以针对每个 bucket 写一个项填充到 Resource 的值里面

Policy 编写完成后,可以新建用户,将 Policy 分配给他,然后在用户下新建 Service Accounts,后者比用户更贴近开发常见的 accessKey 与 accessSecret 的概念。若有需要还可以建立用户组。

最后检查 Service Accounts,确保他拥有刚刚创建的 Policy。此时一个具备应用所需的最低访问权限的开发密钥对就配置好了。

反向代理

服务部署后若有需要代理到域名上时,可以参考官方

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
server {
listen 80;
server_name example.com;

# 允许报文头出现特殊字符
ignore_invalid_headers off;
# 允许超大文件
client_max_body_size 0;
# 禁用代理缓冲区
proxy_buffering off;

location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;

proxy_connect_timeout 300;
# 启用HTTP1.1,使用Keeplive
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;

proxy_pass http://localhost:9000;
}
}

实际业务开发中存在将所有业务放到一个域名下的情况,如确保同源等,此时官方也给出了配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Proxy requests to the bucket "photos" to MinIO server running on port 9000
location /photos/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;

proxy_connect_timeout 300;
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;

proxy_pass http://localhost:9000;
}

# Proxy any other request to the application server running on port 9001
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;

proxy_connect_timeout 300;
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;

proxy_pass http://localhost:9001;
}

但该配置存在一点问题,若用户需要访问 photos 这一 bucket 的相关信息,因为代理过程中多了/,nginx 会将请求指向代理的其他应用,所以推荐以下写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
location /bucket {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;

proxy_connect_timeout 300;
# Default is HTTP/1, keepalive is only enabled in HTTP/1.1
proxy_http_version 1.1;
proxy_set_header Connection "";
chunked_transfer_encoding off;

proxy_pass http://localhost:9000;
}

location /minio-console/ {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $http_host;

proxy_pass http://localhost:9001/;
}

以上写法可以确保 bucket 本身的信息查询是可用的。配置中的第二个 location 则是将 MinIO 的控制台代理出来,但目前 subpath 支持并不好,会被重定向到/。但能正常使用,只是不能刷新,一旦刷新将会被指向其他的 location 中(console v0.19.3已修复该问题)

问题描述

当前经过 encodeURI,多数 MVC 框架已经能够正常工作而无需担心特殊字符集的问题。

而如果直接在路径上携带入[]|等字符,MVC 框架会根据自己的情况返回错误的 http 状态码

而一般的 http client 也会自行将 uri 编码,在服务端与客户端都已经做好对应的操作的前提下,仍有可能因为特殊字符导致不可访问的情况。

当使用 nginx 进行转发的时候,如果转发服务带上 request_uri,哪怕是/,如

1
http://example.com/

那么 nginx 解析请求会进行 decode,此时特殊字符将直接被传递到目标服务,引发错误

If the proxy_pass directive is specified with a URI, then when a request is passed to the server, the part of a normalized request URI matching the location is replaced by a URI specified in the directive:
如果使用 URI 指定了 proxy_pass 指令,那么当请求传递到服务器时,与该位置匹配的规范化请求 URI 的一部分将被该指令中指定的 URI 代替:

1
2
3
4
5
6
7
set $modified_uri $request_uri;

if ($modified_uri ~ "^/([\w]{2})(/.*)") {
set $modified_uri $1;
}

proxy_pass http://example$modified_uri;

目前在 nginx 的新版本已经不用再担心该问题,至少在 1.20.1 以上版本,Nginx 并不会再出现因 proxy_pass 指令带上 URI 导致特殊字符传递导致报错的问题

解决方法

nginx 反向代理不使用/

1
http://example.com

nginx 对 uri 会直接转发,只需客户端本身已经做了 encode,便能够避免问题。

If proxy_pass is specified without a URI, the request URI is passed to the server in the same form as sent by a client when the original request is processed, or the full normalized request URI is passed when processing the changed URI:
如果指定 proxy_pass 时不带 URI,则将请求 URI 以与客户端在处理原始请求时发送的格式相同的形式传递给服务器,或者在处理更改的 URI 时传递完整的规范化请求 URI:

rewrite


1
2
3
4
5
6
location /foo {
rewrite ^ $request_uri; # get original URI
rewrite ^/foo(/.*) /bar$1 break; # drop /foo, put /bar, 此处如果转发目标无需前缀,可直接填$1
return 400; # if the second rewrite won't match
proxy_pass http://localhost:8080$uri;
}

目标服务自身的变更(Spring Boot)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class MyTomcatWebServerCustomizer
implements WebServerFactoryCustomizer<TomcatServletWebServerFactory> {

@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.addConnectorCustomizers(new TomcatConnectorCustomizer() {
@Override
public void customize(Connector connector) {
connector.setAttribute("relaxedQueryChars", "yourvaluehere");
}
});
}
}

Oracle 密码过期解决方案

背景

本地使用 docker 来启动 oracle,但遭遇密码过期的情况,因账号已过期无法使用 gui 访问

解决方案

首先 docker 使用 exec 进入容器内部

其次运行,进入 oracle 命令行工具

1
2
export ORACLE_SID=YourDB
sqlplus /nolog

最后运行

1
connect sys(用户名) as sysdba

将提醒你重置密码

后续处理

1
2
3
select profile from DBA_USERS where username = '<username>';
alter profile <profile_name> limit password_life_time UNLIMITED;
select resource_name,limit from dba_profiles where profile='<profile_name>';

Java 代码混淆备忘

ProGuard

在各类 Java 混淆器里面 ProGuard 是被介绍最多的,详细的过程不在此介绍。

但在实际的使用中,因为 spring boot 携带有大量的反射,类名获取以及繁琐的规则配置,要对 spring boot 的 bean 进行混淆是个艰难的工作。

最终这一方案被废弃

obfuscator

obfuscator提供了一个简单的 gui 工具,使用方便,最后我选择了它作为代码的混淆工具。

但在使用过程中仍有几个注意事项

  1. LineNumberRemover中的Add Local Names不可勾选,这可能导致 spring boot 的依赖注入失败,失败原因为发现两个同名的 bean
  2. LineNumberRemover中的Rename Local variables不可勾选,这会重命名参数变量,若 controller 中没有通过@PathVariable 和@RequestParam 指定读取路径变量与查询参数的名称,可能导致错误
  3. HideMembers中的Enabled需要取消勾选,若勾上,可能导致依赖注入失败
  4. 该工具仅限于 jar 包使用,且在前后端分离后端仅提供 api 的项目中使用更易成功
  5. 混淆过的 jar 包已经被压缩过了,需要解压后通过 jar cfM0 tccw.jar 目录 重新打包才能正常运行

在以上配置中,可以很容易的进行业务代码混淆,生成大量不可读的变量与数字替换

在 windows 上,Docker 的部分端口无法使用,有时候会提示无权限,有时候提示是无法操作

  1. windows 快速启动导致的
    关闭快速启动

  2. hype-v 保留了端口,无法使用
    弃用保留端口

  3. 开机端口分配,类似于上一条
    netsh int ip set dynamicport tcp start=49152 num=16384

0%