You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SSH流程中PAM模块内Goroutine无法启动问题求助

解决Go编写的PAM模块在SSH环境中Goroutine无法运行的问题

我用Go开发了一个SSH授权的PAM模块,用pamtester测试时一切正常,但在SSH流程中,模块内的Goroutine完全无法启动。核心需求是通过HTTP请求完成授权,但标准库http.Client会自动创建Goroutine,导致请求卡住。以下是相关代码和环境信息:

代码示例

package main

/*
#include <security/pam_modules.h>
typedef const char cchar_t;
*/
import "C"
import (
    "log"
)

func main() {}

//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
    log.Printf("golib.so: before start goroutine")
    go func() {
        // SSH环境下这段代码永远不会执行
        log.Printf("golib.so: something out of the goroutine")
    }()
    log.Printf("golib.so: after start goroutine")

    return C.PAM_SUCCESS
}

//export pam_sm_setcred
func pam_sm_setcred(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
    return C.PAM_IGNORE
}

环境信息

  • OS: Ubuntu 22.04.4 LTS
  • SSH: OpenSSH_8.9p1 Ubuntu-3ubuntu0.10, OpenSSL 3.0.2 15 Mar 2022
  • Go: go1.22.5 linux/amd64
  • pamtester: 0.1.2

解决方案

方案一:让Goroutine在SSH的PAM环境中正常运行

问题根源在于SSH调用PAM模块时,运行环境可能限制了线程创建,或者当前线程在Go函数返回后脱离了Go runtime的控制,导致Goroutine无法被调度执行。可以通过以下方式解决:

  1. 同步等待Goroutine完成
    如果逻辑需要等待Goroutine执行完毕(比如HTTP请求的回调),可以使用sync.WaitGroup阻塞当前线程,直到Goroutine完成:
import "sync"

//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
    log.Printf("golib.so: before start goroutine")
    
    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        log.Printf("golib.so: something out of the goroutine")
        // 这里执行HTTP请求或其他逻辑
    }()
    
    wg.Wait() // 等待Goroutine完成后再返回
    log.Printf("golib.so: after goroutine finished")

    return C.PAM_SUCCESS
}
  1. 强制Go调度器执行Goroutine
    如果不需要等待Goroutine完成,但需要确保它能被调度,可以在启动Goroutine后调用runtime.Gosched()让出当前线程的执行权,同时短暂等待调度器切换:
import "runtime"
import "time"

//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
    log.Printf("golib.so: before start goroutine")
    
    go func() {
        log.Printf("golib.so: something out of the goroutine")
    }()
    
    runtime.Gosched() // 让出当前线程,让调度器处理其他Goroutine
    time.Sleep(50 * time.Millisecond) // 短暂等待确保Goroutine有机会执行
    
    log.Printf("golib.so: after start goroutine")
    return C.PAM_SUCCESS
}
  1. 绑定线程到Go runtime
    在PAM函数开头调用runtime.LockOSThread(),将当前C线程绑定到Go的调度线程,确保Go runtime不会在函数返回后销毁该线程,从而让后续Goroutine有机会执行:
import "runtime"

//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
    runtime.LockOSThread()
    defer runtime.UnlockOSThread()
    
    log.Printf("golib.so: before start goroutine")
    go func() {
        log.Printf("golib.so: something out of the goroutine")
    }()
    
    log.Printf("golib.so: after start goroutine")
    return C.PAM_SUCCESS
}

方案二:使用无Goroutine的HTTP请求

如果不想依赖Go的Goroutine,可以手动实现基于TCP的HTTP请求,完全在当前线程同步执行,避免任何Goroutine创建:

import (
    "fmt"
    "io"
    "net"
    "net/url"
    "strings"
)

// 手动发送HTTP GET请求,无Goroutine依赖
func syncHTTPGet(rawURL string) ([]byte, error) {
    // 解析URL
    u, err := url.Parse(rawURL)
    if err != nil {
        return nil, err
    }

    // 处理主机和端口
    host := u.Host
    if !strings.Contains(host, ":") {
        host += ":80" // 默认HTTP端口
    }

    // 建立TCP连接
    conn, err := net.Dial("tcp", host)
    if err != nil {
        return nil, err
    }
    defer conn.Close()

    // 构造HTTP请求
    request := fmt.Sprintf(
        "GET %s HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n",
        u.Path, u.Host,
    )

    // 发送请求
    _, err = conn.Write([]byte(request))
    if err != nil {
        return nil, err
    }

    // 读取响应
    var response []byte
    buf := make([]byte, 4096)
    for {
        n, err := conn.Read(buf)
        if err != nil {
            if err == io.EOF {
                break
            }
            return nil, err
        }
        response = append(response, buf[:n]...)
    }

    return response, nil
}

// 在PAM函数中调用
//export pam_sm_authenticate
func pam_sm_authenticate(pamh *C.pam_handle_t, flags C.int, argc C.int, argv **C.cchar_t) C.int {
    resp, err := syncHTTPGet("http://your-auth-server.com/check")
    if err != nil {
        log.Printf("HTTP request failed: %v", err)
        return C.PAM_AUTH_ERR
    }

    log.Printf("Auth response: %s", resp)
    return C.PAM_SUCCESS
}

为什么pamtester正常而SSH不行?

pamtester运行在普通进程环境中,允许Go runtime创建新的操作系统线程来调度Goroutine;而SSH调用PAM模块时,可能处于受限的线程上下文(比如单线程环境、进程资源限制),导致Go无法创建新线程,Goroutine被永久挂起无法执行。

内容的提问来源于stack exchange,提问作者GreNodge

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 09:50:53