diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml index fc7fc178c..b05ba5177 100644 --- a/.github/workflows/Build.yml +++ b/.github/workflows/Build.yml @@ -53,11 +53,11 @@ jobs: - name: Update OS and Compilers run: | - sudo apt install -y cmake ninja-build + sudo apt install -y cmake ninja-build libpam0g-dev - name: Build proxy run: | - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_BUILD_WERROR=OFF -G Ninja + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_BUILD_WERROR=OFF -DENABLE_USE_PAM_AUTH=ON -G Ninja cmake --build build --config Release - name: Archive artifacts @@ -72,7 +72,7 @@ jobs: image: alpine:3.20.1 steps: - name: Install packages - run: apk add bash git nasm yasm pkgconfig build-base clang cmake ninja linux-headers + run: apk add bash git nasm yasm pkgconfig build-base clang cmake ninja linux-headers linux-pam-dev - uses: actions/checkout@v4 @@ -94,7 +94,7 @@ jobs: - name: Update OS and Compilers run: | - sudo apt install -y cmake ninja-build + sudo apt install -y cmake ninja-build libpam0g-dev - name: Build mimalloc run: | @@ -108,7 +108,7 @@ jobs: - name: Build proxy run: | - cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_BUILD_WERROR=OFF -DENABLE_MIMALLOC_STATIC=ON -G Ninja + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_BUILD_WERROR=OFF -DENABLE_MIMALLOC_STATIC=ON -DENABLE_USE_PAM_AUTH=ON -G Ninja cmake --build build --config Release - name: Archive artifacts @@ -124,7 +124,7 @@ jobs: - name: Update OS and Compilers run: | - sudo apt install -y cmake ninja-build + sudo apt install -y cmake ninja-build libpam0g-dev - name: Build proxy run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index 24c73c26f..13f2e8217 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,7 +59,9 @@ option(ENABLE_REUSEPORT "Build with REUSEPORT support" OFF) option(ENABLE_SYSTEM_ZLIB "Build with system zlib support" OFF) option(ENABLE_SYSTEM_OPENSSL "Build with system openssl support" OFF) + option(ENABLE_USE_SYSTEMD_LOG "Build with systemd log support" OFF) +option(ENABLE_USE_PAM_AUTH "Build with PAM authentication support" OFF) option(ENABLE_USE_OPENSSL "Build with openssl support" ON) option(ENABLE_USE_BORINGSSL "Build with boringssl support" OFF) @@ -136,6 +138,15 @@ endif() ################################################################################ +if (UNIX AND NOT APPLE) + if (ENABLE_USE_PAM_AUTH) + add_definitions(-DUSE_PAM_AUTH) + link_libraries(pam) + endif() +endif() + +################################################################################ + if (WIN32) set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") diff --git a/Dockerfile b/Dockerfile index 0b7274b31..b1e1bdac0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,8 @@ RUN echo "https://dl-cdn.alpinelinux.org/alpine/edge/main" > /etc/apk/repositori RUN apk add -u alpine-keys --allow-untrusted RUN apk update RUN apk upgrade -RUN apk add --no-cache bash git nasm yasm pkgconfig build-base clang cmake ninja linux-headers +RUN apk add --no-cache bash git nasm yasm pkgconfig build-base clang cmake ninja linux-headers linux-pam-dev linux-pam +RUN ln -s /lib/libpam.* /usr/lib/ ADD . /proxy diff --git a/Dockerfile.ubuntu b/Dockerfile.ubuntu index be5eaf481..30e5d2f0f 100644 --- a/Dockerfile.ubuntu +++ b/Dockerfile.ubuntu @@ -3,10 +3,10 @@ FROM ubuntu:22.04 AS builder RUN apt-get update && apt-get install --fix-missing -y ca-certificates RUN sed -i "s@http://.*archive.ubuntu.com@https://mirrors.pku.edu.cn@g" /etc/apt/sources.list && sed -i "s@http://.*security.ubuntu.com@https://mirrors.pku.edu.cn@g" /etc/apt/sources.list RUN apt-get update && apt-get upgrade -y -RUN apt-get install -y cmake gcc g++ ninja-build +RUN apt-get install -y cmake gcc g++ ninja-build linux-pam-dev ADD . /proxy -RUN cd /proxy && mkdir -p build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_SNMALLOC_STATIC=ON -G Ninja && ninja -RUN cd /proxy && mkdir -p wolfssl && cd wolfssl && cmake .. -DENABLE_USE_OPENSSL=OFF -DENABLE_USE_WOLFSSL=ON -DENABLE_SNMALLOC_STATIC=ON -DCMAKE_BUILD_TYPE=Release -G Ninja && ninja +RUN cd /proxy && mkdir -p build && cd build && cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_SNMALLOC_STATIC=ON -DENABLE_USE_PAM_AUTH=ON -G Ninja && ninja +RUN cd /proxy && mkdir -p wolfssl && cd wolfssl && cmake .. -DENABLE_USE_OPENSSL=OFF -DENABLE_USE_WOLFSSL=ON -DENABLE_SNMALLOC_STATIC=ON -DENABLE_USE_PAM_AUTH=ON -DCMAKE_BUILD_TYPE=Release -G Ninja && ninja FROM ubuntu:22.04 RUN apt-get update && apt-get install -y ca-certificates diff --git a/README.md b/README.md index c569521c8..db84b5bc7 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ docker build . -t proxy:v1 | transparent | 启用透明代理,默认禁用,此选项仅 linux 平台有效 | | so_mark | 用于发起向 `proxy_pass` 连接时设置 so_mark 以方便实现代理流量的策略路由,仅在 transparent 启动时有效 | | local_ip | 用于向上游服务或目标服务发起连接时使用指定的本地网口 `ip` 地址,通常用于机器上有多个 `ip` 时使用特定 `ip` 向外发起连接 | +| pam_auth | 用于指定使用 `PAM` 认证模块进行认证,参数值为 `PAM` 服务名称,`PAM` 能和使用 `auth_users` 参数同时用于认证,优先使用 `auth_users` 参数进行认证,需要在编译时添加 `-DENABLE_USE_PAM_AUTH=ON` 选项以启用 `PAM` 模块认证功能 | | auth_users | 认证信息列表,客户端必须满足其中一对用户/密码才能握手通过,默认用户密码是 `jack:1111`(默认需要认证是为了避免不小心被当成别人免费的跳板),若需要设置为无需要认证代理模式,必须置 `auth_users` 参数为 "" | | proxy_pass | 当前服务作为中间级联服务时,`proxy_pass` 指定上游代理服务地址,格式为 `url` 格式,如果有认证信息并必须包含认证信息,如: `https://jack:1111@example.com:1080/` | | proxy_pass_ssl | 向 `proxy_pass` 指定的上游代理服务连接时,是否通过 `ssl` 安全传输,注意必须在上游代理服务启用 `ssl` 相关证书域名密钥等信息. | @@ -180,6 +181,16 @@ Host example 然后当使用 `ssh` 连接主机 `example` 时将会按上述配置文件中参数创建 `proxy_server` 代理隧道,通过 `proxy_pass` 指定的代理服务器连接目标 `HostName` 所指的服务器。 +## 使用 PAM 模块认证介绍 + +`proxy_server` 支持使用 `PAM` 模块进行认证(仅支持 `pam` 的 `linux` 平台,编译 `proxy_server` 时需要在 `cmake` 中添加 `-DENABLE_USE_PAM_AUTH=ON` 选项以启用 `PAM` 模块认证功能),具体使用方法如下: + +1. 配置 `PAM` 服务,如将 `doc` 目录下 `pam.example` 下的文件 `proxy-service` 复制到 `/etc/pam.d/` 目录中,文件名即为 `PAM` 服务名称。 +2. 在 `proxy_server` 中指定 `PAM` 服务名称,如 `--pam_auth proxy-service`。 +3. 使用 `linux` 命令添加用户到 `PAM` 服务中,如 `useradd jack`,其中 `jack` 为用户名,使用 `passwd jack` 设置密码如 `1111`。 +4. 测试认证是否生效,如 `curl -x http://jack:1111@localhost:1080/ https://google.com`,如果返回 `200 OK` 则说明认证生效。 + +`PAM` 模块认证可以使用 `linux` 命令添加或管理用户,可以极大的方便 `proxy_server` 的用户管理,而不必依赖复杂的数据库系统,当然如果你是开发人员,也可以开发一个支持数据库认证的 `PAM` 的 `so` 模块(可参考 `doc` 下 `pam.example` 的 `pam_sqlite.c` 如何实现 `PAM` 认证模块),或者使用 `PAM` 模块认证来实现 `LDAP` 认证等等各种方式。 ## 静态文件 http 服务器(可配置为云音乐播放器) diff --git a/doc/pam.example/pam_sqlite.c b/doc/pam.example/pam_sqlite.c new file mode 100644 index 000000000..e5c2bd977 --- /dev/null +++ b/doc/pam.example/pam_sqlite.c @@ -0,0 +1,106 @@ +// +// 使用下面命令编译并安装到 pam 模块目录 +// gcc -fPIC -fstack-protector -c pam_sqlite.c +// gcc -shared -o pam_sqlite.so pam_sqlite.o -lpam -lsqlite3 +// cp pam_sqlite.so /usr/lib/security/ +// +// pam_sqlite.so 模块用到的数据库的表结构如下: +// +// 表名:users +// 字段:username(用户名,主键)、password(密码,非空) +// +// CREATE TABLE users ( +// username TEXT PRIMARY KEY, +// password TEXT NOT NULL +// ); +// + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#define DB_PATH "/tmp/users.db" + +// 核心函数:处理身份验证 +PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { + const char *user; + const char *password; + int retval; + + const char* db_path = DB_PATH; + if (argc >= 1) { + db_path = argv[0]; + } + + pam_syslog(pamh, LOG_INFO, "db path: %s", db_path); + + // 1. 获取认证的用户名 + retval = pam_get_user(pamh, &user, "Username: "); + if (retval != PAM_SUCCESS || user == NULL) { + pam_syslog(pamh, LOG_ERR, "pam_get_user failed!"); + return PAM_AUTH_ERR; + } + + // 2. 获取认证的密码 + retval = pam_get_authtok(pamh, PAM_AUTHTOK, &password, NULL); + if (retval != PAM_SUCCESS) { + pam_syslog(pamh, LOG_ERR, "pam_get_authtok failed!"); + return PAM_AUTH_ERR; + } + + // 3. 查询 SQLite 数据库 + sqlite3 *db; + sqlite3_stmt *res; + + if (sqlite3_open(db_path, &db) != SQLITE_OK) { + pam_syslog(pamh, LOG_ERR, "sqlite open failed: %s", sqlite3_errmsg(db)); + return PAM_SERVICE_ERR; + } + + const char *sql = "SELECT password FROM users WHERE username = ?;"; + if (sqlite3_prepare_v2(db, sql, -1, &res, 0) != SQLITE_OK) { + pam_syslog(pamh, LOG_ERR, "sqlite prepare failed: %s", sqlite3_errmsg(db)); + sqlite3_close(db); + return PAM_SERVICE_ERR; + } + + sqlite3_bind_text(res, 1, user, -1, SQLITE_STATIC); + + int step = sqlite3_step(res); + int authenticated = 0; + + if (step == SQLITE_ROW) { + const char *db_password = (const char *)sqlite3_column_text(res, 0); + // 注意:生产环境应使用密码哈希(如 bcrypt)进行比对 + if (strcmp(password, db_password) == 0) { + pam_syslog(pamh, LOG_NOTICE, "auth ok: %s", user); + authenticated = 1; + } else { + pam_syslog(pamh, LOG_NOTICE, "bad password: %s", user); + } + } + + sqlite3_finalize(res); + sqlite3_close(db); + + if (authenticated == 0) { + pam_syslog(pamh, LOG_NOTICE, "authenticated failed: %s", user); + } + + return authenticated ? PAM_SUCCESS : PAM_AUTH_ERR; +} + +PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { + return PAM_SUCCESS; +} + +PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { + return PAM_SUCCESS; +} diff --git a/doc/pam.example/proxy-service b/doc/pam.example/proxy-service new file mode 100644 index 000000000..83771203d --- /dev/null +++ b/doc/pam.example/proxy-service @@ -0,0 +1,12 @@ +# 这是一个 PAM 示例,将此配置文件 proxy-service 复制到 /etc/pam.d/ 目录中,然后 +# 就可以使用 proxy_server 的 --pam_auth 参数指定为 "proxy-service" 即可. + +# 以下为使用 pam_sqlite.c 编译的 pam_sqlite.so 通过 sqlite 数据库中的用户密码认证配置 +#auth required pam_sqlite.so /var/users.db +#account required pam_permit.so + +# 以下为使用 linux 用户密码认证的配置 +auth required pam_unix.so +account required pam_unix.so +session required pam_unix.so + diff --git a/proxy/include/proxy/proxy_server.hpp b/proxy/include/proxy/proxy_server.hpp index 84cb7539f..cbc0fdbac 100644 --- a/proxy/include/proxy/proxy_server.hpp +++ b/proxy/include/proxy/proxy_server.hpp @@ -104,7 +104,12 @@ # include #endif // USE_BOOST_FILESYSTEM +#ifdef USE_PAM_AUTH +# include +# include +#endif // USE_PAM_AUTH +#include #include #include #include @@ -419,6 +424,12 @@ R"x*x*x( // - 为空表示不启用该功能。 std::string stdio_target_; + // PAM 认证服务配置名称。 + // + // - 为空表示不启用 PAM 认证。 + // - 否则,将使用指定的 PAM 服务配置进行认证。 + std::string pam_auth_; + // 授权(认证)用户列表。 // // auth_users 的含义: @@ -871,21 +882,133 @@ R"x*x*x( } } +#ifdef USE_PAM_AUTH + static int pam_conv_func(int num_msg, const struct pam_message **msg, + struct pam_response **resp, void *appdata_ptr) + { + if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) + return PAM_CONV_ERR; + + *resp = (struct pam_response *)std::calloc(num_msg, sizeof(struct pam_response)); + if (*resp == nullptr) + return PAM_BUF_ERR; + + const char *password = (const char *)appdata_ptr; + + for (int i = 0; i < num_msg; i++) + { + if (msg[i]->msg_style == PAM_PROMPT_ECHO_OFF) + { + (*resp)[i].resp = strdup(password); + (*resp)[i].resp_retcode = 0; + } + else + { + (*resp)[i].resp = nullptr; + } + } + + return PAM_SUCCESS; + } + + bool pam_authenticate_user(const char *service, const char *username, const char *password) noexcept + { + pam_handle_t *pamh = nullptr; + struct pam_conv conv = { + .conv = pam_conv_func, + .appdata_ptr = (void *)password // 传入密码 + }; + + int retval = pam_start(service, username, &conv, &pamh); + if (retval != PAM_SUCCESS) + { + log_conn_warning() << ", pam_start failed: " << pam_strerror(pamh, retval); + return false; + } + + retval = pam_authenticate(pamh, 0); // 核心认证 + if (retval != PAM_SUCCESS) { + log_conn_warning() << ", authentication failed: " << pam_strerror(pamh, retval); + pam_end(pamh, retval); + return false; + } + + retval = pam_acct_mgmt(pamh, 0); // 检查账户(如锁定、过期) + if (retval != PAM_SUCCESS) { + log_conn_warning() << ", account management failed: " << pam_strerror(pamh, retval); + pam_end(pamh, retval); + return false; + } + + pam_end(pamh, PAM_SUCCESS); + + log_conn_debug() << ", pam_authenticate_user success"; + + return true; + } + + template + auto async_pam_auth(const std::string& username, const std::string& passwd, + const std::string& service_name, CompletionToken&& token) noexcept + { + return net::async_initiate + ([this, username, passwd, service_name](auto&& handler) mutable + { + std::thread([this, username = std::move(username), passwd = std::move(passwd), + service_name = std::move(service_name), handler = std::move(handler)]() mutable + { + auto slot = net::get_associated_cancellation_slot(handler); + auto executor = net::get_associated_executor(handler); + std::atomic_bool cancelled = false; + + if (slot.is_connected()) + { + slot.assign([&cancelled](net::cancellation_type_t) mutable + { + cancelled = true; + }); + } + + boost::system::error_code ec; + bool result = pam_authenticate_user( + service_name.c_str(), + username.c_str(), + passwd.c_str()); + + net::post(executor, + [ec = std::move(ec), result, handler = std::move(handler)]() mutable + { + handler(ec, result); + }); + } + ).detach(); + }, token); + } +#endif // USE_PAM_AUTH + // 检查是否需要认证. inline bool auth_required() const noexcept { - return !m_option.auth_users_.empty(); + if (!m_option.auth_users_.empty()) + return true; + + if (!m_option.pam_auth_.empty()) + return true; + + return false; } // 检查用户密码等相关配置信息, 包括更新绑定的本地网络地址和限速等信息. - inline bool check_userpasswd( + inline net::awaitable check_userpasswd( const std::string& username, const std::string& passwd, bool skip_passwd = false) noexcept { // 若不需要认证, 直接返回 true. if (!auth_required()) - return true; + co_return true; + // 检查用户名和密码是否匹配. for (const auto& [user, pwd, addr, proxy_pass] : m_option.auth_users_) { if (username == user) @@ -899,11 +1022,26 @@ R"x*x*x( if (proxy_pass) m_proxy_pass = proxy_pass; - return true; + co_return true; } } - return false; + // 如果启用了 PAM 认证, 则尝试使用 PAM 认证. + if (!skip_passwd && !m_option.pam_auth_.empty()) + { +#ifdef USE_PAM_AUTH + boost::system::error_code ec; + auto result = co_await async_pam_auth(username, passwd, + m_option.pam_auth_, net_awaitable[ec]); + if (result) + { + user_rate_limit_config(username); + co_return true; + } +#endif + } + + co_return false; } public: @@ -2462,7 +2600,7 @@ R"x*x*x( << (socks4a ? hostname : dst_endpoint.address().to_string()); // 用户认证逻辑, 如果用户认证列表为空, 则表示不需要用户认证. - bool verify_passed = check_userpasswd(userid, "", true); + bool verify_passed = co_await check_userpasswd(userid, "", true); if (auth_required()) { @@ -2581,23 +2719,23 @@ R"x*x*x( co_return; } - inline int http_authorization(std::string_view pa) noexcept + inline net::awaitable http_authorization(std::string_view pa) noexcept { if (!auth_required()) - return PROXY_AUTH_SUCCESS; + co_return PROXY_AUTH_SUCCESS; if (pa.empty()) - return PROXY_AUTH_NONE; + co_return PROXY_AUTH_NONE; auto pos = pa.find(' '); if (pos == std::string::npos) - return PROXY_AUTH_ILLEGAL; + co_return PROXY_AUTH_ILLEGAL; auto type = pa.substr(0, pos); auto auth = pa.substr(pos + 1); if (type != "Basic") - return PROXY_AUTH_ILLEGAL; + co_return PROXY_AUTH_ILLEGAL; std::string userinfo( beast::detail::base64::decoded_size(auth.size()), 0); @@ -2612,11 +2750,11 @@ R"x*x*x( std::string uname = userinfo.substr(0, pos); std::string passwd = userinfo.substr(pos + 1); - bool verify_passed = check_userpasswd(uname, passwd); + bool verify_passed = co_await check_userpasswd(uname, passwd); if (!verify_passed) - return PROXY_AUTH_FAILED; + co_return PROXY_AUTH_FAILED; - return PROXY_AUTH_SUCCESS; + co_return PROXY_AUTH_SUCCESS; } inline net::awaitable http_proxy_get() noexcept @@ -2675,7 +2813,7 @@ R"x*x*x( // http 代理认证, 如果请求的 rarget 不是 http url 或认证 // 失败, 则按正常 web 请求处理. - auto auth_result = http_authorization(pa); + auto auth_result = co_await http_authorization(pa); if (auth_result != PROXY_AUTH_SUCCESS || !get_url_proxy) { // 如果 doc 目录为空, 则不允许访问目录 @@ -2863,11 +3001,11 @@ R"x*x*x( : ", proxy_authorization: " + pa); // http 代理认证. - auto auth = http_authorization(pa); + auto auth = co_await http_authorization(pa); if (auth != PROXY_AUTH_SUCCESS) { log_conn_warning() - << ", proxy err: " + << ", proxy auth error: " << pauth_error_message(auth); auto date_string = server_date_string(); @@ -3034,7 +3172,7 @@ R"x*x*x( passwd.push_back(read(p)); // SOCKS5验证用户和密码, 用户认证逻辑. - bool verify_passed = check_userpasswd(uname, passwd); + bool verify_passed = co_await check_userpasswd(uname, passwd); if (auth_required()) { diff --git a/server/proxy_server/main.cpp b/server/proxy_server/main.cpp index e3a9af0ff..6a95aa014 100644 --- a/server/proxy_server/main.cpp +++ b/server/proxy_server/main.cpp @@ -72,6 +72,10 @@ std::string ssl_ciphers; std::string ssl_dhparam; std::string proxy_ssl_name; +#ifdef USE_PAM_AUTH +std::string pam_auth; +#endif // USE_PAM_AUTH + bool transparent = false; bool autoindex = false; bool htpasswd = false; @@ -227,6 +231,9 @@ start_proxy_server(net::io_context& ioc, server_ptr& server) } } +#ifdef USE_PAM_AUTH + opt.pam_auth_ = pam_auth; +#endif opt.proxy_pass_use_ssl_ = proxy_pass_ssl; opt.ssl_cert_path_ = ssl_cert_dir; @@ -418,6 +425,9 @@ int main(int argc, char** argv) ("tcp_timeout", po::value(&tcp_timeout)->default_value(-1), "Set TCP timeout for TCP connections.") ("rate_limit", po::value(&rate_limit)->default_value(-1), "Set TCP rate limit for connection.") +#ifdef USE_PAM_AUTH + ("pam_auth", po::value(&pam_auth)->value_name("pam service"), "Enable PAM authentication, specify PAM service name.") +#endif ("auth_users", po::value>(&auth_users)->multitoken()->default_value(std::vector{"jack:1111"}), "List of authorized users(default user: jack:1111) (e.g: user1:passwd1 user2:passwd2).") ("users_rate_limit", po::value>(&users_rate_limit)->multitoken(), "List of users rate limit (e.g: user1:1000000 user2:800000).")