登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go Unix socket 怎样读取对端凭据

来源:17golang原创

时间:2026-10-14 23:53:55 302浏览 收藏

我在给本机服务加 Unix socket 授权时,最容易混淆的是两件事:socket 文件的权限决定“谁能尝试连接”,连接建立后的对端凭据决定“这条连接究竟来自哪个进程和用户”。Go 的 net.UnixConn 没有直接返回 pid、uid、gid 的高层方法,但可以通过 SyscallConn 暴露受控的文件描述符,再读取平台提供的 socket credential。

官方文档:https://pkg.go.dev/net、https://pkg.go.dev/golang.org/x/sys/unix

要点速览
  • 文件模式和对端凭据是两层控制,不能用 chmod 代替身份判断。
  • Linux 常用 SO_PEERCRED,通过 x/sys/unix.GetsockoptUcred 得到 pid、uid、gid。
  • 读取 fd 时必须留在 RawConn.Control 回调内,不能把 fd 保存到回调外继续使用。
  • 平台相关常量和结构体应放在 build-tag 文件中,业务授权只依赖自己的统一结果类型。

先分清 socket 权限与连接身份

Unix socket 路径通常会受到目录权限、socket 文件 mode 和服务进程权限的影响。这些检查发生在连接尝试阶段,适合挡住大多数无权访问者;但如果服务还需要按用户、组或进程做细粒度授权,就要在连接建立后读取对端凭据。

这两个层次不能互相替代。例如把 socket 文件设成只有某个组可写,并不等于服务端可以从业务消息里安全地相信一个“uid”字段;客户端自己发送的字段只是普通数据。真正的身份判断应来自内核为该 socket 连接提供的凭据,并且每条新连接都重新读取。

Go UnixConn 通过 SyscallConn 和 RawConn.Control 访问 fd,再读取对端 pid uid gid 的结构说明图
图1:Go 从 UnixConn 进入文件描述符并读取对端凭据的结构说明图,不是运行截图。

从 UnixConn 进入受控文件描述符

服务端一般从 net.Listener.Accept 得到一个 net.Conn,Unix socket 场景再断言为 *net.UnixConn。官方接口提供的 SyscallConn 返回 syscall.RawConn,而 Control 会在底层 fd 仍然有效时调用回调。这个生命周期约束很重要:不要把回调参数里的 fd 存到全局变量,也不要在回调返回后再调用系统接口。

func readPeerCredential(conn *net.UnixConn) (*unix.Ucred, error) {
    raw, err := conn.SyscallConn()
    if err != nil {
        return nil, fmt.Errorf("获取 Unix socket fd: %w", err)
    }

    var cred *unix.Ucred
    var optionErr error
    // Control 回调期间 fd 才由 RawConn 保证有效,系统调用必须放在这里。
    if err := raw.Control(func(fd uintptr) {
        // SO_PEERCRED 是 Linux 的连接对端凭据选项,返回 pid、uid、gid。
        cred, optionErr = unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
    }); err != nil {
        return nil, fmt.Errorf("访问 Unix socket fd: %w", err)
    }
    if optionErr != nil {
        return nil, fmt.Errorf("读取 SO_PEERCRED: %w", optionErr)
    }
    if cred == nil {
        return nil, errors.New("内核没有返回对端凭据")
    }
    return cred, nil
}

这段实现依赖 golang.org/x/sys/unix,因此应放在 Linux 文件中,例如 peercred_linux.go。Ucred.Pid 是进程号,Uid 和 Gid 是内核返回的用户与组标识;它们不是客户端在消息体里自报的字符串。

把授权动作放在读取成功之后

读取到凭据后,先转换成业务层自己的小结构,再交给授权函数。不要让业务代码到处依赖 unix.Ucred,这样未来替换 BSD/macOS 实现时,权限规则不需要重写。

type PeerIdentity struct {
    PID int32
    UID uint32
    GID uint32
}

func authorize(conn *net.UnixConn) error {
    cred, err := readPeerCredential(conn)
    if err != nil {
        // 身份读取失败时拒绝连接,避免把“未知”当成可信身份。
        return fmt.Errorf("无法确认对端身份: %w", err)
    }

    identity := PeerIdentity{PID: cred.Pid, UID: cred.Uid, GID: cred.Gid}
    // 示例规则只允许指定 uid;实际项目还应结合组、部署命名空间和审计策略。
    if identity.UID != 1001 {
        return fmt.Errorf("uid %d 不在允许列表", identity.UID)
    }
    return nil
}

实际服务中,授权规则通常不应只写死一个 uid。可以把允许的 uid/gid 放进配置,把读取到的 pid 写入审计日志,并把“读取失败”“身份不允许”“业务认证失败”分成不同错误类型,方便排障。

平台差异要隔离,而不是复制 Linux 常量

SO_PEERCRED 和 Ucred 是 Linux 路径。其他 Unix 系统可能使用不同的 socket option、返回结构或系统调用;BSD/macOS 常见的是 getpeereid 一类的接口。不要为了让一份文件跨平台编译,给不存在的常量加类型转换;更稳妥的做法是让每个平台各自实现同一个 Go 函数签名。

Linux 使用 SO_PEERCRED 和 Ucred,BSD macOS 使用不同接口,再汇聚到统一 peer credentials 适配层的对比说明图
图2:不同 Unix 平台读取对端凭据的适配边界说明图,不是平台截图。
// peercred_linux.go
//go:build linux

func peerIdentity(conn *net.UnixConn) (PeerIdentity, error) {
    // Linux 实现调用 SO_PEERCRED,其他平台文件提供同名函数。
    cred, err := readPeerCredential(conn)
    if err != nil {
        return PeerIdentity{}, err
    }
    return PeerIdentity{PID: cred.Pid, UID: cred.Uid, GID: cred.Gid}, nil
}

在 BSD 或 macOS 文件里,使用该平台实际支持的接口,把结果填入同一个 PeerIdentity。对于不支持 Unix socket credential 的构建目标,应明确返回“不支持”,而不是返回全零身份;全零值可能被误认为是一个真实用户。

几个容易踩到的边界

第一,pid 不是永久身份。进程退出后,操作系统可能复用同一个 pid;如果授权决定需要长期绑定进程,应该结合连接生命周期、uid/gid 和更高层的认证材料,而不是只保存 pid。

第二,容器和用户命名空间会改变“数字 uid 的含义”。服务看到的是当前内核命名空间下的标识,不能直接把宿主机配置中的数字与容器里的 uid 机械对应。部署时要把 namespace、镜像用户和 socket 所在目录权限一起说明。

第三,Unix socket 的路径名、抽象命名空间和平台实现并不完全相同。Linux 以 @ 开头的地址属于抽象命名空间,而不是普通文件路径;这会影响你对文件 mode 的预期,但不会改变“连接凭据需要从底层 socket 获取”的分层思路。

常见问题

只检查 socket 文件权限够不够?

如果需求只是限制谁能连接,文件和目录权限可能已经足够;只要还要在服务内部按 uid、gid 或进程做授权,就应该读取内核提供的对端凭据。

为什么不从请求内容中读取 uid?

请求内容由客户端控制,最多是业务字段,不能替代内核身份。它可以作为业务用户信息,但不应单独作为本地进程授权依据。

Go 标准库能直接返回对端 uid 吗?

net.UnixConn 提供的是受控原始连接入口,具体 credential option 属于平台能力。Linux 示例通常配合 golang.org/x/sys/unix 完成读取,并把这段代码隔离在平台文件中。

我的落地顺序是:先用目录和 socket mode 缩小连接入口,再在每次 Accept 后读取 peer credential,最后把平台代码收口到适配层。这样即使部署目标从 Linux 换到 BSD/macOS,业务授权仍只面对统一的身份结构和明确的“不支持/读取失败”结果。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>