HTTP3之QUICTLS编译冲突问题
Tags: http3, quic, quictls, openssl
Published: 2024-05-29 07:22:46

### 背景
本文主要介绍QUICTLS依赖库编译时与OpenSSL冲突问题和使用时找不到依赖库问题。

### 环境
操作系统: `Ubuntu 22.04.4 LTS`  
编译工具: `gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0`

### QUICTLS库描述
`OpenSSL`官方支持`QUIC`协议进度太慢，于是出现了基于OpenSSL库改的[QUICTLS](https://github.com/quictls/openssl)库，原因是QUIC协议不是使用原生的[TLS1.3协议](https://datatracker.ietf.org/doc/html/rfc8446)。QUICTLS是基于OpenSSL修改了TLS1.3相关内容的库，OpenSSL用户态命令还是一样。

### 问题描述
本机上原先通过`apt`安装了`OpenSSL3.x`, [编译QUICTLS](https://curl.se/docs/http3.html)后, QUICTLS默认的`openssl`命令位于`/usr/local/bin/openssl`, 官方OpenSSL位于`/usr/bin/openssl`, 环境变量`PATH`中也是按照这个目录顺序。<br>
如果键入`openssl version`，使用的是QUICTLS版本的`openssl`命令, 这时会报错:
```
openssl: error while loading shared libraries: libssl.so.81.3: cannot open shared object file: No such file or directory
```

### 问题原因
`openssl` 命令运行时找不到依赖的动态库, 使用`ldd`命令可以查看依赖共享库情况:
```bash
ldd $(which openssl)
     linux-vdso.so.1 (0x00007fff7****000)
     libssl.so.81.3 => not found
     libcrypto.so.81.3 => not found
     libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb650****00)
     /lib64/ld-linux-x86-64.so.2 (0x00007fb65****000)

```
可以看到`libssl.so.81.3`和`libcrypto.so.81.3`找不到, 官方OpenSSL的动态库是`libssl.so.3`和`libcrypto.so.3`, 位于`/usr/lib/x86_64-linux-gnu`。QUICTLS动态库名称中 **81** 是 **Q** 的ASCII码值, 以示区分:<br>
```bash
ls -l /usr/lib/x86_64-linux-gnu | grep -E 'libssl|libcrypto'
     -rw-r--r--  1 root root   9098630  2月  1 02:43 libcrypto.a
     lrwxrwxrwx  1 root root        14  2月  1 02:43 libcrypto.so -> libcrypto.so.3
     -rw-r--r--  1 root root   4451632  2月  1 02:43 libcrypto.so.3
     -rw-r--r--  1 root root    418464  2月 17  2023 libssl3.so
     -rw-r--r--  1 root root   1231268  2月  1 02:43 libssl.a
     lrwxrwxrwx  1 root root        11  2月  1 02:43 libssl.so -> libssl.so.3
     -rw-r--r--  1 root root    667864  2月  1 02:43 libssl.so.3
```

### 解决过程
解决过程也是理解Linux动态库加载流程的过程, 当程序执行时, 会通过一定的顺序寻找共享库加载。通过阅读[ld.so的manpage文档](https://man7.org/linux/man-pages/man8/ld.so.8.html) , 如果共享库没有包含slash, 按照以下的顺序寻找, 下面是原文:
```
If a shared object dependency does not contain a slash, then it
       is searched for in the following order:

       (1)  Using the directories specified in the DT_RPATH dynamic
            section attribute of the binary if present and DT_RUNPATH
            attribute does not exist.  Use of DT_RPATH is deprecated.

       (2)  Using the environment variable LD_LIBRARY_PATH, unless the
            executable is being run in secure-execution mode (see
            below), in which case this variable is ignored.

       (3)  Using the directories specified in the DT_RUNPATH dynamic
            section attribute of the binary if present.  Such
            directories are searched only to find those objects required
            by DT_NEEDED (direct dependencies) entries and do not apply
            to those objects' children, which must themselves have their
            own DT_RUNPATH entries.  This is unlike DT_RPATH, which is
            applied to searches for all children in the dependency tree.

       (4)  From the cache file /etc/ld.so.cache, which contains a
            compiled list of candidate shared objects previously found
            in the augmented library path.  If, however, the binary was
            linked with the -z nodefaultlib linker option, shared
            objects in the default paths are skipped.  Shared objects
            installed in hardware capability directories (see below) are
            preferred to other shared objects.

       (5)  In the default path /lib, and then /usr/lib.  (On some
            64-bit architectures, the default paths for 64-bit shared
            objects are /lib64, and then /usr/lib64.)  If the binary was
            linked with the -z nodefaultlib linker option, this step is
            skipped.
```
1. 设置ELF文件的DT_RPATH, 上面文档指出这个参数过时了, 但是依然很多在使用。编译时指定GCC的相关参数, 比如`-Wl,-rpath=/usr/local/lib64`, 默认情况下文档说是设置`DT_RPATH`,
```
man 1 ld
...
--enable-new-dtags
--disable-new-dtags
     This linker can create the new dynamic tags in ELF. But the older ELF systems
     may not understand them. If you specify --enable-new-dtags, the new dynamic tags
     will be created as needed and older dynamic tags will be omitted.  If you 
     specify --disable-new-dtags, no new dynamic tags will be created. By default, the 
     new dynamic tags are not created. Note that those options are only available for ELF systems.
...
```
 但是在我机器 `Ubuntu22.04, GCC11.04` 验证是默认设置的`DT_RUNPATH`, 如果要设置`DT_RPATH`, 需要显式关闭开关`-Wl,--disable-new-dtags`, 编译完成后可以使用如下命令检验: <br>
```shell
readelf -d build/client | grep -E 'RUNPATH|RPATH'
```
2. 可以设置`LD_LIBRARY_PATH`, 比如`LD_LIBRARY_PATH="/usr/local/lib64" openssl version`能正确运行
3. 设置DT_RUNPATH, 方法同1, 但是需要她的使用顺序以及她只应用与DT_NEEDED的依赖库, 他们的子依赖不会使用这个参数指定的地址, 这也是争议的地方, DT_RPATH说是过时了，而且存在安全争议，但是在检索第一位管用
4. `/etc/ld.so.cache`本地缓存，这个需要在机器上自己设置，一般在目录 `/etc/ld.so.conf.d/` 添加配置文件，然后刷新缓存: <br>
```shell
echo "/usr/local/lib64" | sudo tee /etc/ld.so.conf.d/quictls.conf # 添加配置文件
sudo ldconfig # 刷新 ld.so.cache
openssl version # 现在能正常执行
# OpenSSL 3.1.4+quic 24 Oct 2023 (Library: OpenSSL 3.1.4+quic 24 Oct 2023)
```
### 解决办法
上述已经列出了所有的解决思路和相应解决办法，一般使用最后一种，会在本机添加多个config文件，比如，我的添加了依赖库QUITLS和ngtcp2的配置文件，配置文件内容也简单，就是依赖库所在地址， 即上述的第四种方式。

### 例子
下面根据QUIC-ECHO工程依赖quictls的例子解释下GCC编译参数, 具体可以参见相关的 [Makefile](https://github.com/Shouhua/quic-echo/blob/main/Makefile) :
```shell
gcc -g -Wall -Wextra -DDEBUG -pedantic -Wl,-rpath=/usr/local/lib64  -o build/client client.c connection.c quictls.c stream.c utils.c  \
        -L/usr/local/lib64  \ # 影响后面的-lssl -lcrypto, 使她们使用quictls而不是openssl的共享库
        -lssl -lcrypto \ # 使用前面-L指定目录找到的依赖libssl.so.81.3 libcrypto.so.81.3
        -lngtcp2 -lngtcp2_crypto_quictls
```

### TIPS
ld 默认搜索的动态库路径可以通过如下途径查看：
```shell
ld --verbose | grep SEARCH_DIR | tr -s ' ;' '\n'
# OR
ldconfig -v 2>/dev/null | grep '^/'
```