0%

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96

typedef ll COST_T;
typedef ll CAP_T;
constexpr COST_T COST_INF = 0x3f3f3f3f3f3f3f3fll;
constexpr CAP_T CAP_INF = 0x3f3f3f3f3f3f3f3fll;


class MinCostMaxFlow {
struct Edge{
int from, to;
CAP_T flow, cap;
COST_T cost;
};

int n;
CAP_T maxFlow;
COST_T minCost;

vector<vector<int>> g;
CAP_T* a = NULL;
COST_T* d = NULL;
vector<Edge> edges;

bool* vis = NULL;
int* p = NULL;
public:
MinCostMaxFlow(int n): n(n), g(n, vector<int>()), a(new CAP_T[n]), d(new COST_T[n]), vis(new bool[n]), p(new int[n]) {

}

~MinCostMaxFlow() {
delete [] a;
delete [] d;
delete [] vis;
delete [] p;
}

void add_edge(int from, int to, CAP_T cap, COST_T cost) {
Edge temp1 = { from, to, 0, cap, cost };
Edge temp2 = { to, from, 0, 0, -cost }; //允许反向增广
edges.push_back(temp1);
edges.push_back(temp2);
int len = edges.size();
g[from].push_back(len - 2);
g[to].push_back(len - 1);
}

bool bellmanford(int s, int t) {
fill(d, d+n, COST_INF);
d[s] = 0;
memset(vis, 0, sizeof(*vis)*n);
memset(p, -1, sizeof(*p)*n);
p[s] = -1;
a[s] = CAP_INF;
queue<int> Q;
Q.push(s);
vis[s] = true;
while (!Q.empty()) {
int u = Q.front();
Q.pop();
vis[u] = false;
for (int i = 0; i < g[u].size(); i++) {
Edge& e = edges[g[u][i]];
//进行松弛,寻找最短路径也就是最小费用
if (e.cap > e.flow && d[e.to] > d[u] + e.cost) {
d[e.to] = d[u] + e.cost;
p[e.to] = g[u][i];
a[e.to] = min(a[u], e.cap - e.flow);
if (!vis[e.to]) {
Q.push(e.to);
vis[e.to] = true;
}
}
}
}
if (d[t] == COST_INF) return false;
maxFlow += a[t];
minCost += d[t] * a[t];
for (int i = t; i != s; i = edges[p[i]].from) {
edges[p[i]].flow += a[t];
edges[p[i] ^ 1].flow -= a[t];
}
return true;
}


pair<COST_T, CAP_T> solve(int s, int t) {
memset(a, 0, sizeof(*a)*n);
maxFlow = 0;
minCost = 0;

while (bellmanford(s, t))
continue;
return make_pair(minCost, maxFlow);
}
};

编译时查找动态链接库的顺序

编译时会在以下路径查找:
通过编译选项-L 指定的目录
通过链接选项–rpath=指定的目录
通过环境变量LIBRARY_PATH指定的目录(:分割)
通过环境变量LD_LIBRARY_PATH指定的目录(:分割)
/lib
/usr/lib
/etc/ld.so.cache中缓存的路径,通过/etc/ld.so.conf指定,通过ldconfig命令更新缓存。
至于查找的顺序其实不重要,因为只要找到符号通过链接就可以了,而运行时又会按照运行时查找的顺序进行查找,甚至运行时都不在相同环境,链接到的动态库也可能和编译时不是同一个。

指定编译时的动态链接库路径

通过gcc/g++的-L编译选项
通过-Wl,--rpath=传递链接选项
设置LIBRARY_PATH或LD_LIBRARY_PATH环境变量
在CMakeLists.txt中使用link_directories(${LIB_DIRS})来指定,LIB_DIRS是用一个或多个空白字符分割的多个路径
在CMakeLists.txt中使用指定rpath,方式如下

1
2
3
4
target_link_libraries(${PROJECT_NAME}
${LIB_LIST}
-Wl,-rpath,'$ORIGIN'/libs,-rpath,'$ORIGIN'/../libs
)

其中'$ORIGIN'是一个特殊的路径,实际上这里是在提前指定运行时的查找路径,'$ORIGIN'表示可执行文件所在目录,另外要注意的是如果指定多个rpath这里不能通过:分割,而是使用多次-rpath选项.
BTW: 我并不清楚为什么要使用'$ORIGIN'而不是.,因为我测试下来效果是一样的。

运行时查找动态链接库的顺序

通过rpath连接选项指定的目录
通过环境变量LD_RUN_PATH指定的目录(:分割)
通过环境变量LD_LIBRARY_PATH指定的目录(:分割)
编译时实际的链接路径(不是查找过的路径,而是找到了这个so的路径)
/lib
/usr/lib

另外可以通过ldd <可执行程序>来查看运行将使用链接库,也可以看到缺少的库。
还可以通过chrpath -l <可执行程序>查看程序被设置的rpath。

指定运行时的动态链接库路径

设置环境变量LD_RUN_PATH(:分割)
设置环境变量LD_LIBRARY_PATH(:分割)
还可以通过chrpath -r <库路径> <可执行程序>修改程序的rpath,但实际上rpath被写在可执行文件中并没有为修改它预留空间,所以只能修改成相同长度或更短的路径,所以显然这不是一个常规操作。
另外rpath只影响这个可执行程序本身而不能传递给其加载的so,如果我从程序a加载了动态库b,而动态b需要加载动态库c,即使我把b、c放在相同路径,a通过rpath的方式找到了b,而b还是找不到c,因为b的rpath里并没有这个路径。
所以总得来说最靠谱的方式还是指定LD_LIBRARY_PATH,在linux上可以通过下面的方式在启动程序同时设置环境变量

1
LD_LIBRARY_PATH=<动态库目录>:$LD_LIBRARY_PATH <可执行文件>

设置LD_LIBRARY_PATH同时保留了之前的值放在后面,这样设置的环境变量只影响可执行文件启动的程序,包括间接加载的动态链接库,

某些场景下我们需要在Docker中运行Docker,例如在Docker中部署Jenkins,在Jenkins的任务中对项目进行打包测试发布,而项目也是基于Docker的。

首先明确我们需要的是什么,Docker其实包含服务器(Docker Engine或者叫Docker Daemon)和客户端(Docker CLI),我们需要的是在容器内能够使用Docker客户端访问到Docker服务器,而Docker服务器其实不必运行在容器内,可以运行在宿主机上。

docker的客户端和服务器有一个参数决定了两者的通讯方式,默认情况下使用/var/run/docker.sock进行通讯,这个文件表示一个Unix Socket,Unix Socket是一种类似我们熟知的TCP/UDP的进程间通讯方式,我们也可以通过增加-H tcp://0.0.0.0:2376参数在启动时修改默认的通讯方式为TCP2376端口通讯。

1
-H, --host=[unix:///var/run/docker.sock]: tcp://[host]:[port][path]

方法一

我们可以通过在创建容器时指定-v /var/run/docker.sock:/var/run/docker.sock把宿主机的Socket映射到容器内,容器内的Docket客户端就相当于直接连到了宿主机的Docker Engine。

这样容器内和容器外使用的是同一个engine可以相互操作对方创建的镜像有一定风险。

方法二

那么还有一个方法就是使用别人制作的特殊系统镜像,镜像名称通常包含dind字样,表示支持Docker in Docker,例如gitlab/dind,这种镜像启动时需要--privileged参数,启动后容器内部预装了docker环境,容器内外的engine是独立不互通的,这种方法的问题是系统的版本是固定的,取决于dind镜像提供的什么版本。

方法三

替换docker运行时为sysbox

1
docker run --runtime=sysbox-runc --name sysbox-dind -d docker:dind

需要安装sysbox,参考sysbox/install-package.md at master · nestybox/sysbox (github.com)

因为安装前需要停止并删除所有容器,我并没有尝试过。

配置主服务器

vim /etc/mysql/mysql.conf.d/mysqld.cnf
找到server-id附近的代码,修改为

1
2
3
4
5
6
7
server-id               = 1
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
binlog_do_db = test # 需要写binlog的数据库
binlog_ignore_db = mysql # 不需要写binlog的数据库
# binlog_do_db和binlog_ignore_db可以仅指定一项,也可以都不指定,都不指定则全部写binlog

重启数据库
service mysql restart

防止出现打开文件数限制

ulimit -n 65535

备份数据库

xtrabackup --backup --host=127.0.0.1 --user=root --password=zrjm6E13RdhTPBmB --target-dir=/data/dbbackup/20221009
如果不存在xtrabackup命令
apt install percona-xtrabackup

复制到远程

rsync -avpP -e ssh /data/dbbackup/20221009 root@从库:/data/dbbackup/
或通过共享目录在从服务器挂载备份目录

配置从服务器

vim /etc/mysql/mysql.conf.d/mysqld.cnf
找到server-id附近的代码,修改为

1
2
3
4
5
6
7
8
9
10
server-id               = 2
log_bin = /var/log/mysql/mysql-bin.log
expire_logs_days = 10
max_binlog_size = 100M
#binlog_do_db = include_database_name
#binlog_ignore_db = include_database_name
replicate-do-db = test #需要同步的数据库
replicate-ignore-db = mysql #不同步系统数据库
super_read_only = 1
read_only = 1

准备恢复

这步并没有连接数据库,只是对备份文件做一些设置,在主或从上执行都可以,安全一点应该再从库上执行,因为从库和主库的数据库版本未必完全一致。
xtrabackup --prepare --user=root --password=zrjm6E13RdhTPBmB --target-dir=/data/dbbackup/20221009

连上从数据库停止SLAVE

mysql -h127.0.0.1 -P3306 -uroot -p
STOP SLAVE;

停止从服务器数据库

service mysql stop

清空/var/lib/mysql目录,顺便备份

mv /var/lib/mysql /var/lib/mysql_backup
如果不需要备份直接删除也可以

还原数据库

xtrabackup --copy-back --target-dir=/data/dbbackup/20221009

修正文件权限

chown -R mysql:mysql /var/lib/mysql

查看并记下备份到的LOG_FILE和LOG_POS,后面设置用

1
2
cd /data/dbbackup/20221009
cat xtrabackup_binlog_info

显示类似
mysql-bin.000891 408016029

启动从服务器数据库

service mysql start

连上从数据库重设SLAVE

mysql -h127.0.0.1 -P3306 -uroot -p

修改其中的MASTER_LOG_FILE & MASTER_LOG_POS为前面查看得到的

1
2
3
4
5
6
7
8
CHANGE MASTER TO
MASTER_HOST='192.168.1.140',
MASTER_USER='repl',
MASTER_PASSWORD='4Bd9x25ERcacAqwX',
MASTER_LOG_FILE='mysql-bin.000891',
MASTER_LOG_POS=408016029;

START SLAVE;

查看本机作为从库的状态
SHOW SLAVE STATUS\G
关注下面3个字段

1
2
3
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Seconds_Behind_Master: 0

Slave_IO_Running & Slave_SQL_Running 应该都是Yes,Seconds_Behind_Master可能为一个正数表示从库落后主库的秒数,开启一段时间后应该会变成0。

PS:
1)mysql-bin文件在主服务器上是不断增加的,会保留一段时间旧的,一段时间后自动删除,在还原并配置从服务器时,要保证主服务器上的MASTER_LOG_FILE还存在才能成功,后面可以有新的文件会自动同步过来,所以在备份还原期间不用停止或锁住主数据库。
2)备份前、复制到远程前、还原前,都应该确保有足够的磁盘空间。
使用df - h来查看磁盘剩余空间,使用du -sh来查看当前文件夹占用空间。
3)如果还原时遇到 Error: copy_file() failed. 可以用md5sum校验复制的文件是否损坏,如果两边md5sum不一致或读取错误无法计算md5sum则说明损坏了。这时需要重新copy损坏的文件并重新执行恢复命令。
4)root在localhost的密码和在%的密码可能不同,连接mysql时分别尝试不指定-h或指定-h127.0.0.1来连接。
5)主从同步出错修复时跳过修改配置文件的步骤,确认备份点的MASTER_LOG_FILE是否存在,若存在也可跳过备份步骤,直接STOP、还原、START、STOP SLAVE; CHANGE MASTER……; START SLAVE;
6) 如果Seconds_Behind_Master不断增大,检查主库和从库的数据库版本,最好主次版本都完全一致,如果不一致,从库应该高于主库,而不能让主库高于从库。

之前讲过通过ngrok实现内网穿透,frp是和ngrok类似的工具,最近发现ngrok的一些不足,frp可以解决,主要有以下几点:

  1. ngrok不支持UDP穿透,这也是我更换成frp的最大原因。
  2. 映射TCP端口时不能指定服务器端口,只能接受随机分配的端口。
  3. ngrok在转发流量特别大的时候速度跟不上且不稳定,我用ngrok转发seafile网盘的流量,然后传1G以上的文件,会出现传不完就卡住失败甚至ngrok客户端出错崩溃的情况。
  4. ngrok是在编译客户端和服务器时预先成好证书,过程略烦琐,如果我有多个ngrok服务器,那么就要有多个不同的客户端可执行文件而不能通过相同的可执行文件不同的配置,这似乎不太合理。

搭建frp服务器

frp服务器可以通过docker搭建或者直接下载可执行文件
https://hub.docker.com/r/snowdreamtech/frps
https://github.com/fatedier/frp/releases
我是通过docker搭建的

1
docker run --restart=always --network host -d -v /etc/frp/frps.ini:/etc/frp/frps.ini --name frps snowdreamtech/frps

可以看到需要一个配置文件frps.ini

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[common]
# 监听端口
bind_port = 7000
# 面板端口
dashboard_port = 7500
# 登录面板账号设置
dashboard_user = admin
dashboard_pwd = password
# 设置http及https协议下代理端口
vhost_http_port = 7080
vhost_https_port = 7443
# 子域名配置
subdomain_host = frp.kyo86.com
# 身份验证
token = 随意填一些字符

子域名需要配合指定*.frp.kyo86.com泛域名解析使用

客户端使用

客户端也可以通过docker或直接下载可执行文件
https://hub.docker.com/r/snowdreamtech/frpc
https://github.com/fatedier/frp/releases
有两种使用方式,一种是通过frpc.ini配置文件进行配置,一种是通过命令参数进行配置。

配置文件样例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[common]
server_addr = 服务器IP
server_port = 7000
# 身份验证
token = 与服务端frps.ini中保存一致

[jenkins]
type = http
local_ip = 127.0.0.1
local_port = 8080
use_encryption = false
use_compression = false
subdomain = jenkins

[joplin]
type = tcp
local_ip = 127.0.0.1
local_port = 22300
route_port = 22300
use_encryption = false
use_compression = false

命令行的方式样例

1
2
3
frpc http -n 代理名称 -s frp服务器地址:frp服务器端口 -l 本地端口 --sd 子域名 -t TOKEN
frpc tcp -n 代理名称 -s frp服务器地址:frp服务器端口 -l 本地端口 -r 远程端口 -t TOKEN
frpc udp -n 代理名称 -s frp服务器地址:frp服务器端口 -l 本地端口 -r 远程端口 -t TOKEN

容器启动命令

1
docker run -p 8080:8080 -p 50000:50000 -h jenkins --name=jenkins --restart=on-failure -v /etc/localtime:/etc/localtime -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts-jdk11

我通常会把/root和/mnt也映射进容器,并且通过root用户启动容器,用起来方便但安全性差,需视情况选择。

1
2
3
4
5
6
docker run -u root --privileged -p 8080:8080 -p 50000:50000 -h jenkins --name=jenkins --restart=on-failure\
-v /etc/localtime:/etc/localtime\
-v jenkins_home:/var/jenkins_home\
-v /root:/root\
-v /mnt:/mnt\
jenkins/jenkins:lts-jdk11

修改Jenkins时区

系统管理(Manage Jenkins)-脚本命令行(Script Console)

执行

1
System.setProperty('org.apache.commons.jelly.tags.fmt.timeZone','Asia/Shanghai')

以root进入容器

1
docker exec -uroot -it jenkins bash

修改jenkins启动脚本

1
vim /usr/local/bin/jenkins.sh

找到java启动war的语句,增加启动参数

1
-Xms256M -Xmx256M -Xmn96M -Xss1M -Djava.awt.headless=true -Dorg.apache.commons.jelly.tags.fmt.timeZone=Asia/Shanghai

参照修改Linux时区

重启容器,在宿主机执行

1
docker restart jenkins

允许匿名通过Token构建项目

默认不允许匿名触发构建,也不建议匿名触发,在外部触发可以通过

1
curl -o /dev/null -s -w %{http_code} http://<用户名>:<密码或API_TOKEN>@<JENKINS_URL>/job/<PROJECT>/build?token=<PROJ_TOKEN>

或者

1
curl -w %{http_code} -u <用户名>:<API_TOKEN> http://<JENKINS_URL>/job/<PROJECT>/build?token=<PROJ_TOKEN>

其中API_TOKEN是属于用户的,在用户管理界面可以配置。PROJ_TOKEN是属于项目的,在项目配置界面构建选项可以配置。

如需匿名触发构建

系统管理-全局安全配置-授权策略-登录用户可以做任何事

勾选“匿名用户具有可读权限”

这样设置之后就可以通过

1
curl -o /dev/null -s -w %{http_code} <JENKINS_URL>/job/<PROJECT>/build?token=<TOKEN>

从外部来触发构建,token在项目里设置。

项目权限配置策略

依赖插件 Matrix Authorization Strategy

需求:

  1. 系统管理员可以看到和控制所有的项目
  2. 普通用户可以创建项目和完全控制自己创建的项目,以及被人别人授权控制的项目

配置方法

系统管理-全局安全配置
系统管理员会自动勾上Administer去权限,多个系统管理就手动Add user然后勾上Administer
对Authenticated Users 只勾上 全部Read 和任务Create
配置项目时勾上启动项目安全,如果是对所有人开放管理和使用的项目,就把Authenticated Users后面的勾都勾上
如果是仅自己使用,就把自己用户名后面的所有勾都勾上

BTW: 不方便的一点就是目前添加用户需要手动输入用户名,而不能选择,希望后面的版本会改进。

rsync可以用在两个目录间同步文件,无论是相同主机或不同主机间。

rsync命令格式

1
rsync -av --delete --exclude-from=exclude.lst --stop-after=240 源路径/ 目标路径/

其中-av是多个选项的缩写,表示递归的进行、不改变文件属性、不改变文件owner等,–delete表示删除目标路径下载源路径不存在的文件。

–exclude-from可以让我们编写一个列表排除不想同步的文件

1
--exclude-from=exclude.lst

exclude.lst样例

1
2
3
4
test
*_test
test.*
*_test.*

–stop-after指定同步超时时间(分钟)

1
--stop-after=240

大量数据首次同步时会耗时很长,而后续增量同步则耗时较少,我们可能不想影响服务器的其他工作选择在指定时段同步,那么加上这个参数可以防止首次同步持续很久,这样将通过连续多天把首次完整同步做完。

在低版本上这个参数叫--time-limit但是我测试下来指定这个参数超时后并不会停下来,不知道是不是BUG。

通常情况我只选择性使用上面这些参数,更多的参数可以通过rsync --help查看

一般来说源路径和目标路径都是目录,最好在结尾加上 / ,以避免在目标路径下新建出源路径文件夹的歧义。

如果路径是本地的,没什么特别的。如果路径是远程的,可以使用

1
[user@]HOST[:PORT]:/path/

也可以使用

1
rsync://[USER@]HOST[:PORT]/MODULE/path/

前者通过ssh服务(默认22端口)

后者通过rsync服务(默认873端口)

如果是少量文件或临时的同步,使用ssh就够了,如果是大量的定期的同步,应该使用rsync,因为rsync需要在一端开启rsync服务,另一端使用rsync相当于客户端连接服务器,而有理由相信rsync服务器会做更多的事来减少不必要的网络流量。

rsync服务器配置文件样例

/etc/rsyncd.conf

1
2
3
4
5
6
7
8
9
10
11
12
use chroot = false
max connections = 0
syslog facility = deamon
log file = /var/log/rsyncd.log
pid file = /var/run/rsyncd.pid

[database]
path = /data/data_manager/database
comment = database
strict modes = false
read only = 1
list = no

rsync服务器配置文件详解

全局参数

在文件中 [module] 之外的所有配置行都是全局参数。当然也可以在全局参数部分定义模块参数,这时该参数的值就是所有模块的默认值。

参数 说明 默认值
address 在独立运行时,用于指定的服务器运行的 IP 地址。由 xinetd 运行时将忽略此参数,使用命令行上的 –address 选项替代。 本地所有IP
port 指定 rsync 守护进程监听的端口号。 由 xinetd 运行时将忽略此参数,使用命令行上的–port 选项替代。 873
motd file 指定一个消息文件,当客户连接服务器时该文件的内容显示给客户。 无
pid file rsync 的守护进程将其 PID 写入指定的文件。 无
log file 指定 rsync 守护进程的日志文件,而不将日志发送给 syslog。 无
syslog facility 指定 rsync 发送日志消息给 syslog 时的消息级别。 daemon
socket options 指定自定义 TCP 选项。 无

模块参数

模块参数主要用于定义 rsync 服务器哪个目录要被同步。模块声明的格式必须为 [module] 形式,这个名字就是在 rsync 客户端看到的名字,类似于 Samba 服务器提供的共享名。而服务器真正同步的数据是通过 path 来指定的。可以根据自己的需要,来指定多个模块,模块中可以定义以下参数:

a. 基本模块参数

参数 说明 默认值
path 指定当前模块在 rsync 服务器上的同步路径,该参数是必须指定的。 无
comment 给模块指定一个描述,该描述连同模块名在客户连接得到模块列表时显示给客户。 无

b. 模块控制参数

参数 说明 默认值
use chroot 若为 true,则 rsync 在传输文件之前首先 chroot 到 path 参数所指定的目录下。这样做的原因是实现额外的安全防护,但是缺点是需要 root 权限,并且不能备份指向 path 外部的符号连接所指向的目录文件。 true
uid 指定该模块以指定的 UID 传输文件。 nobody
gid 指定该模块以指定的 GID 传输文件。 nobody
max connections 指定该模块的最大并发连接数量以保护服务器,超过限制的连接请求将被告知随后再试。 0(没有限制)
lock file 指定支持 max connections 参数的锁文件。 /var/run/rsyncd.lock
list 指定当客户请求列出可以使用的模块列表时,该模块是否应该被列出。如果设置该选项为 false,可以创建隐藏的模块。 true
read only 指定是否允许客户上传文件。若为 true 则不允许上传;若为 false 并且服务器目录也具有读写权限则允许上传。 true
write only 指定是否允许客户下载文件。若为 true 则不允许下载;若为 false 并且服务器目录也具有读权限则允许下载。 false
ignore errors 指定在 rsync 服务器上运行 delete 操作时是否忽略 I/O 错误。一般来说 rsync 在出现 I/O 错误时将将跳过 –delete 操作,以防止因为暂时的资源不足或其它 I/O 错误导致的严重问题。 true
ignore nonreadable 指定 rysnc 服务器完全忽略那些用户没有访问权限的文件。这对于在需要备份的目录中有些不应该被备份者获得的文件时是有意义的。 false
timeout 该选项可以覆盖客户指定的 IP 超时时间。从而确保 rsync 服务器不会永远等待一个崩溃的客户端。对于匿名 rsync 服务器来说,理想的数字是 600(单位为秒)。 0 (未限制)
dont compress 用来指定那些在传输之前不进行压缩处理的文件。该选项可以定义一些不允许客户对该模块使用的命令选项列表。必须使用选项全名,而不能是简称。当发生拒绝某个选项的情况时,服务器将报告错误信息然后退出。例如,要防止使用压缩,应该是:”dont compress = *”。 *.gz *.tgz *.zip *.z *.rpm *.deb *.iso *.bz2 *.tbz

c. 模块文件筛选参数

参数 说明 默认值
exclude 指定多个由空格隔开的多个文件或目录(相对路径),并将其添加到 exclude 列表中。这等同于在客户端命令中使用 –exclude 来指定模式。 空
exclude from 指定一个包含 exclude 规则定义的文件名,服务器从该文件中读取 exclude 列表定义。 空
include 指定多个由空格隔开的多个文件或目录(相对路径),并将其添加到 include 列表中。这等同于在客户端命令中使用 –include 来指定模式 。 空
include from 指定一个包含 include 规则定义的文件名,服务器从该文件中读取 include 列表定义。 空
  • 一个模块只能指定一个exclude 参数、一个include 参数。
  • 结合 include 和 exclude 可以定义复杂的exclude/include 规则 。
  • 这几个参数分别与相应的rsync 客户命令选项等价,唯一不同的是它们作用在服务器端。
  • 关于如何书写规则文件的内容请参考http://www.howtocn.org/rsync:use_rsync。

d. 模块用户认证参数

参数 说明 默认值
auth users 指定由空格或逗号分隔的用户名列表,只有这些用户才允许连接该模块。这里的用户和系统用户没有任何关系。用户名和口令以明文方式存放在 secrets file 参数指定的文件中。 (匿名方式)
secrets file 指定一个 rsync 认证口令文件。只有在 auth users 被定义时,该文件才起作用。 空
strict modes 指定是否监测口令文件的权限。若为 true 则口令文件只能被 rsync 服务器运行身份的用户访问,其他任何用户不可以访问该文件。 true
  • rsync 认证口令文件的权限一定是 600,否则客户端将不能连接服务器。
  • rsync 认证口令文件中每一行指定一个 用户名:口令 对,格式为:

username:passwd

  • 一般来说口令最好不要超过8个字符。若您只配置匿名访问的 rsync 服务器,则无需设置上述参数。

e. 模块访问控制参数

参数 说明 默认值
hosts allow 用一个主机列表指定哪些主机客户允许连接该模块。不匹配主机列表的主机将被拒绝。 *
hosts deny 用一个主机列表指定哪些主机客户不允许连接该模块。 空

客户主机列表定义可以是以下形式:

  • 单个IP地址。例如:192.168.0.1
  • 整个网段。例如:192.168.0.0/24,192.168.0.0/255.255.255.0
  • 可解析的单个主机名。例如:centos,centos.bsmart.cn
  • 域内的所有主机。例如:*.bsmart.cn
  • “*”则表示所有。
  • 多个列表项要用空格间隔。

f. 模块日志参数

参数 说明 默认值
transfer logging 使 rsync 服务器将传输操作记录到传输日志文件。 false
log format 指定传输日志文件的字段。 ”%o %h [%a] %m (%u) %f %l”

设置了”log file”参数时,在日志每行的开始会添加”%t [%p]“。

可以使用的日志格式定义符如下所示:

  • %a - 远程IP地址

  • %h - 远程主机名

  • %l - 文件长度字符数

  • %p - 该次 rsync 会话的 PID

  • %o - 操作类型:”send” 或 “recv”

  • %f - 文件名

  • %P - 模块路径

  • %m - 模块名

  • %t - 当前时间

  • %u - 认证的用户名(匿名时是 null)

  • %b - 实际传输的字节数

  • %c - 当发送文件时,记录该文件的校验码

Ubuntu上默认使用的是netplan管理网络(至少18.04~22.04都是,其他更早的版本我没用过)

在18.04上查看 /etc/network/interfaces文件可以看到

1
2
3
4
# ifupdown has been replaced by netplan(5) on this system.  See
# /etc/netplan for current configuration.
# To re-enable ifupdown on this system, you can run:
# sudo apt install ifupdown

在22.04上索性这个文件都没了。

在没有网络的情况下无法 sudo apt install ifupdown,只能去配置netplan,/etc/netplan是一个目录,下面会有一个默认的 *.yaml 文件。

我遇到过 50-cloud-init.yaml 以及 00-installer-config.yaml,总就是通过它配置网络。

可以先用ifconfig -a查看一下当前的网络状态和网卡名称。如果ifconfig命令也没有,可以用ip ad

假设我们有两块网卡,一个叫enp2s0是有线网卡,一个叫wlp3s0是无线网卡。

分别看一下配置方案:

有线网卡固定IP配置

先启用网卡

1
ifconfig enp2s0 up

编辑/etc/netplan/*.yaml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
network:
renderer: networkd
ethernets:
enp2s0:
dhcp4: no
addresses:
- 192.168.1.10/24
gateway4: 192.168.1.1
nameservers:
addresses:
- 114.114.114.114
- 8.8.8.8
search: []
version: 2

应用生效

1
netplan apply

有线网卡动态IP配置

先启用网卡

1
ifconfig enp2s0 up

编辑/etc/netplan/*.yaml配置

1
2
3
4
5
6
network:
renderer: networkd
ethernets:
enp2s0:
dhcp4: yes
version: 2

应用生效

1
netplan apply

无线网卡固定IP配置

配置无线网卡要先配好有限然后安装两个东西

1
apt update && apt install -y wpasupplicant network-manager

然后关闭有线网卡,开启无线网卡

1
2
ifconfig enp2s0 down
ifconfig wlp3s0 up

编辑/etc/netplan/*.yaml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
network:
renderer: NetworkManager
ethernets:
wlp3s0:
dhcp4: no
addresses:
- 192.168.1.10/24
gateway4: 192.168.1.1
nameservers:
addresses:
- 114.114.114.114
- 8.8.8.8
search: []
access-points:
"WIFI名称":
password: "WIFI密码"
version: 2

应用生效

1
2
sudo netplan generate
sudo netplan apply

无线网卡动态IP配置

类似无线网卡固定IP的步骤,配置改为

1
2
3
4
5
6
7
8
9
network:
renderer: NetworkManager
ethernets:
wlp3s0:
dhcp4: yes
access-points:
"WIFI名称":
password: "WIFI密码"
version: 2

常用网络命令

临时指定IP(重启失效)

1
ifconfig <网口号> <IP地址> netmask <子网掩码>

Example:

1
ifconfig eth0 192.168.1.141 netmask 255.255.255.0

如果没有ifconfig命令,可以用ip命令

1
ip addr add 192.168.1.141/24 dev eth0

配置默认网关

1
route add default gw 192.168.1.1

查看路由表

1
netstat -rn

或

1
route -n

或

1
ip route

Destination 0.0.0.0的 GateWay就是默认网关

添加一条路由

1
ip route add 192.168.10.0/24 via 192.168.1.99

手动动态获取IP地址

比如我们把网络接口插上网线UP起来,然后什么都不配置,直接dhclient,如果路由器上开启了DHCP,那么应该会分到一个IP,有时候明明配置好了,但是重启不会自动获取IP地址,手动执行一次dhclient就可以获取到,不清楚原因

1
dhclient

查看开放端口和对应的进程ID

1
netstat -anp

在发现一个端口被占用,想找出对应的进程

1
netstat -anp|grep <port>

测试一个端口是否开放

1
telnet <ip> <port>

或

1
nc -v <ip> <port>

BTW:如果一些网络命令不能执行,安装这几个包试试

1
apt update && apt install -y iproute2 iputils-ping net-tools

之前在公司内网搭建了PPTPD服务器解决疫情期间远程办公从家里连接公司内网服务器的问题,但一直不太稳定,平时随便用用还行,偶尔断开可以重连,一直想换个方式搭建,之前尝试过其他搭建方式没有成功,后来恢复线下办公后需求不强了就没有继续尝试,今天发现在Ubuntu系统下基于Docker搭建相对简单,于是记录下来

使用Docker Hub上的kylemanna/openvpn镜像

kylemanna/openvpn - Docker Image | Docker Hub

拉取镜像

1
docker pull kylemanna/openvpn

设置卷名环境变量

取一个名称用来创建用于存放openvpn数据的docker volume,建议用ovpn-data-前缀,后面的example换成自己取的

1
OVPN_DATA="ovpn-data-example"

初始化数据

VPN.SERVERNAME.COM换成自己的IP地址或域名

1
2
3
docker volume create --name $OVPN_DATA
docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_genconfig -u udp://VPN.SERVERNAME.COM
docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn ovpn_initpki

启动OpenVPN服务器

1
docker run --name=openvpn -v $OVPN_DATA:/etc/openvpn -d -p 1194:1194/udp --cap-add=NET_ADMIN kylemanna/openvpn

生成不带密码的客户端证书

CLIENTNAME换成自定义的客户端标识,可以取形如USERNAME@SERVERNAME方便管理

1
docker run -v $OVPN_DATA:/etc/openvpn --rm -it kylemanna/openvpn easyrsa build-client-full CLIENTNAME nopass

导出包含证书的客户端配置文件

CLIENTNAME同样换成自定义的客户端标识,导出为当前目录的CLIENTNAME.ovpn文件

1
docker run -v $OVPN_DATA:/etc/openvpn --rm kylemanna/openvpn ovpn_getclient CLIENTNAME > CLIENTNAME.ovpn

如果服务器端口做过NAT映射,可以打开CLIENTNAME.ovpn修改其中的端口号

作废客户端证书

先查看openvpn数据卷的所在位置

1
docker volume inspect ovpn-data-example

删除文件

1
rm ***/_data/pki/private/CLIENTNAME.key

编辑

1
vim ***/_data/pki/index.txt

删除CLIENTNAME所在的行

取消全局代理

默认配置会将全部对外流量通过VPN,如果想要指定仅访问公司局域网时走VPN,其他流量走默认路由不变,可以通过在.ovpn配置文件中追加

1
2
3
4
5
route-nopull
route-metric 150
route remote_host 255.255.255.255 net_gateway
route 172.19.0.0 255.255.0.0 net_gateway
route 192.168.1.0 255.255.0.0 vpn_gateway

在.ovpn配置文件的结尾找到

1
# redirect-gateway def1

注释掉这一行

其中route-nopull表示不接受服务器对于修改路由表的推送,而可以按照我们之后指定的规则去修改。

我所在的内网网段是172.19.0.0/16,所以明确指定路由,走net_gateway,也就是原有网关。

192.168.1.0/24是需要走VPN的,所以也明确指定了路由,走vpn_gateway。

redirect-gateway def1作用是将默认网关重定向到 VPN 服务器上,所以要注释掉。

修改服务器配置

<openvpn容器卷路径>/_data/openvpn.conf

1
#push "block-outside-dns"

<openvpn容器卷路径>/_data/ovpn_env.sh

1
declare -x OVPN_DISABLE_PUSH_BLOCK_DNS=1

重启服务器,重连客户端

客户端

Windows 7

Windows 10

macOS v2

macOS v3

Ubuntu

安装openvpn

1
apt -y install openvpn

客户端和服务器会同时安装,我们不用服务器可以把它关掉

1
service openvpn stop

禁止openvpn服务器开启自启动

1
systemctl disable openvpn

用客户端配置文件在后台创建VPN连接

1
openvpn --daemon --config CLIENTNAME.ovpn

BTW:根据测试,同一份配置文件似乎不能在多个主机重复使用,如果使用会造成两边连接频繁断开。

最长公共前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 预处理字符串s,得到所有s任意后缀即s[i:]和s[j:]的所有最长公共前缀
class LCP {
vector<vector<int>> dp;
public:
LCP(const string& s) {
dp.resize(s.size()+1, vector<int>(s.size()+1));
for(int j=s.size()-1;j>=0;j--) {
for(int i=j;i>=0;i--) {
dp[i][j]=(s[i]==s[j]?dp[i+1][j+1]+1:0);
}
}
}

int operator()(int i, int j) const {
return dp[min(i,j)][max(i,j)];
}
};

这个算法本身本身就是动态规划,并不难,而有时这个问题会嵌套于另一个问题里,使整体变得略复杂,而如果能想到LCP问题可以用O(n^2)预处理,则复杂问题迎刃而解。

6195. 对字母串可执行的最大删除数 - 力扣(LeetCode)

一个朴素的解法是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int deleteString(string s) {
int n = s.size();
// 用dp[i] 表示删除s[i:]的最大操作次数
// 删除一个长度为j的前缀之后得到的是一个子问题:s[i+j:]的最大操作次数
// 能删除s[i:i+j]的前提是s[i:i+j]==s[i+j:i+j+j]
vector<int> dp(n, 1);
for(int i=n-2;i>=0;i--) {
for(int j=1;j<=(n-i)/2;j++) {
if(memcmp(&s[i], &s[i+j], j) == 0) { // s[i:i+j]==s[i+j:i+j+j]
dp[i] = max(dp[i], 1+dp[i+j]);
}
}
}
return dp[0];
}
};

如果将memcmp(&s[i], &s[i+j], j)视为O(n)的复杂度,那么算法整体是O(n^3)复杂度。

但如果我们预处理得到s所有后缀的最长公共前缀的长度,就可以判断长度是否>=j来代替判断子串相等

从而把O(n3)的复杂度降到O(n2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
int deleteString(string s) {
int n = s.size();
// 用dp[i] 表示删除s[i:]的最大操作次数
// 删除一个长度为j的前缀之后得到的是一个子问题:s[i+j:]的最大操作次数
// 能删除s[i:i+j]的前提是s[i:i+j]==s[i+j:i+j+j]
LCP lcp(s);
vector<int> dp(n, 1);
for(int i=n-2;i>=0;i--) {
for(int j=1;j<=(n-i)/2;j++) {
if(lcp(i, i+j)>= j) { // s[i:i+j]==s[i+j:i+j+j]
dp[i] = max(dp[i], 1+dp[i+j]);
}
}
}
return dp[0];
}
};