This commit is contained in:
2026-07-13 23:25:12 -03:00
parent 8a141ae86d
commit 7e0ec393a8
7 changed files with 115 additions and 110 deletions
+2 -1
View File
@@ -3,8 +3,9 @@ module shell2
go 1.25.4 go 1.25.4
require ( require (
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/msteinert/pam/v2 v2.1.0 github.com/openwall/yescrypt-go v1.0.0
github.com/xtaci/kcp-go/v5 v5.6.61 github.com/xtaci/kcp-go/v5 v5.6.61
github.com/xtaci/smux v1.5.50 github.com/xtaci/smux v1.5.50
golang.org/x/crypto v0.45.0 golang.org/x/crypto v0.45.0
+8 -6
View File
@@ -1,10 +1,12 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@@ -36,15 +38,15 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/msteinert/pam/v2 v2.1.0 h1:er5F9TKV5nGFuTt12ubtqPHEUdeBwReP7vd3wovidGY= github.com/openwall/yescrypt-go v1.0.0 h1:jsGk48zkFvtUjGVOhYPGh+CS595JmTRcKnpggK2AON4=
github.com/msteinert/pam/v2 v2.1.0/go.mod h1:KT28NNIcDFf3PcBmNI2mIGO4zZJ+9RSs/At2PB3IDVc= github.com/openwall/yescrypt-go v1.0.0/go.mod h1:e6CWtFizUEOUttaOjeVMiv1lJaJie3mfOtLJ9CCD6sA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
github.com/xtaci/kcp-go/v5 v5.6.61 h1:ajm12pGuWO+GWQNusPyPESC7Rq0yTC2rEXVYkM8ExOg= github.com/xtaci/kcp-go/v5 v5.6.61 h1:ajm12pGuWO+GWQNusPyPESC7Rq0yTC2rEXVYkM8ExOg=
+5 -6
View File
@@ -61,24 +61,23 @@ detect_pkg_manager() {
set_package_deps() { set_package_deps() {
case "$PKG_MANAGER" in case "$PKG_MANAGER" in
apt) apt)
# libpam0g-dev: PAM headers for the optional PAM auth build (cgo). PKG_DEPS=(curl wget git rsync build-essential postgresql ca-certificates unzip openssh-client openssl python3 tar gzip)
PKG_DEPS=(curl wget git rsync build-essential libpam0g-dev postgresql ca-certificates unzip openssh-client openssl python3 tar gzip)
PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables)
;; ;;
dnf|yum) dnf|yum)
PKG_DEPS=(curl wget git rsync gcc make pam-devel postgresql-server ca-certificates unzip openssh-clients openssl python3 tar gzip) PKG_DEPS=(curl wget git rsync gcc make postgresql-server ca-certificates unzip openssh-clients openssl python3 tar gzip)
PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables)
;; ;;
zypper) zypper)
PKG_DEPS=(curl wget git rsync gcc make pam-devel postgresql-server ca-certificates unzip openssh openssl python3 tar gzip) PKG_DEPS=(curl wget git rsync gcc make postgresql-server ca-certificates unzip openssh openssl python3 tar gzip)
PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables)
;; ;;
pacman) pacman)
PKG_DEPS=(curl wget git rsync base-devel pam postgresql ca-certificates unzip openssh openssl python tar gzip) PKG_DEPS=(curl wget git rsync base-devel postgresql ca-certificates unzip openssh openssl python tar gzip)
PKG_OPTIONAL_DEPS=(iptables-nft nftables) PKG_OPTIONAL_DEPS=(iptables-nft nftables)
;; ;;
apk) apk)
PKG_DEPS=(curl wget git rsync build-base linux-pam-dev postgresql ca-certificates unzip openssh-client openssl python3 tar gzip) PKG_DEPS=(curl wget git rsync build-base postgresql ca-certificates unzip openssh-client openssl python3 tar gzip)
PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables) PKG_OPTIONAL_DEPS=(postgresql-contrib iptables nftables)
;; ;;
esac esac
+95
View File
@@ -0,0 +1,95 @@
package main
import (
"crypto/subtle"
"errors"
"fmt"
"os"
"strings"
"github.com/GehirnInc/crypt"
_ "github.com/GehirnInc/crypt/apr1_crypt"
_ "github.com/GehirnInc/crypt/md5_crypt"
_ "github.com/GehirnInc/crypt/sha256_crypt"
_ "github.com/GehirnInc/crypt/sha512_crypt"
"github.com/openwall/yescrypt-go"
"golang.org/x/crypto/bcrypt"
)
// pamAuthAvailable reports that system-password ("PAM") auth is compiled in.
// The implementation is pure Go (no cgo, no libpam), so it is always available;
// on non-Linux builds /etc/shadow simply does not exist and auth fails closed.
const pamAuthAvailable = true
const shadowFile = "/etc/shadow"
var errNoSystemPassword = errors.New("account has no usable password")
// authenticatePAM verifies password against the Linux system account matching
// username. It reads the account's hash from /etc/shadow (the panel runs as
// root) and recomputes it with the same algorithm — this is the "just the auth"
// behaviour: the supplied password is checked exactly as the system would,
// with no account/session management and nothing to do with the SSH daemon.
//
// It is called "PAM" for continuity with the user-facing flag, but it does not
// link libpam; it verifies the crypt(3) hash directly. Supported hash formats:
// yescrypt ($y$), sha512-crypt ($6$), sha256-crypt ($5$), md5-crypt ($1$),
// apr1 ($apr1$) and bcrypt ($2a$/$2b$/$2y$). Returns nil on success.
func authenticatePAM(username, password string) error {
if username == "" {
return errors.New("shadow: empty username")
}
hash, err := lookupShadowHash(username)
if err != nil {
return err
}
return verifyCryptHash(hash, password)
}
// lookupShadowHash returns the password hash field for username from /etc/shadow.
func lookupShadowHash(username string) (string, error) {
data, err := os.ReadFile(shadowFile)
if err != nil {
return "", fmt.Errorf("read %s: %w", shadowFile, err)
}
for _, line := range strings.Split(string(data), "\n") {
line = strings.TrimRight(line, "\r")
if line == "" {
continue
}
fields := strings.Split(line, ":")
if len(fields) < 2 || fields[0] != username {
continue
}
hash := fields[1]
// Empty, or locked/disabled accounts (! or * in the hash field) have no
// password that any input can match — reject rather than risk a match.
if hash == "" || strings.HasPrefix(hash, "!") || strings.HasPrefix(hash, "*") {
return "", errNoSystemPassword
}
return hash, nil
}
return "", fmt.Errorf("shadow: user %q not found", username)
}
// verifyCryptHash checks password against a crypt(3)-style hash string,
// dispatching on the hash prefix. Returns nil only on an exact match.
func verifyCryptHash(hash, password string) error {
switch {
case strings.HasPrefix(hash, "$y$"):
computed, err := yescrypt.Hash([]byte(password), []byte(hash))
if err != nil {
return fmt.Errorf("yescrypt: %w", err)
}
if subtle.ConstantTimeCompare(computed, []byte(hash)) == 1 {
return nil
}
return errors.New("password mismatch")
case strings.HasPrefix(hash, "$2a$"), strings.HasPrefix(hash, "$2b$"), strings.HasPrefix(hash, "$2y$"):
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
case crypt.IsHashSupported(hash):
return crypt.NewFromHash(hash).Verify(hash, []byte(password))
default:
return fmt.Errorf("shadow: unsupported hash format")
}
}
-63
View File
@@ -1,63 +0,0 @@
//go:build linux && cgo
package main
import (
"errors"
"fmt"
"os"
"strings"
"github.com/msteinert/pam/v2"
)
// pamAuthAvailable reports that PAM authentication is compiled into this build.
const pamAuthAvailable = true
// pamServiceName returns the PAM service to use for the auth-only check. This
// is the name of a file under /etc/pam.d. Override with SSHPANEL_PAM_SERVICE;
// it defaults to "login", which exists on every mainstream distro and runs the
// standard unix auth stack (common-auth / system-auth).
func pamServiceName() string {
if s := strings.TrimSpace(os.Getenv("SSHPANEL_PAM_SERVICE")); s != "" {
return s
}
return "login"
}
// authenticatePAM verifies password against the Linux PAM stack for username.
//
// It runs ONLY the auth phase (pam_authenticate) — no account management
// (pam_acct_mgmt), no session, no credential setup, and nothing to do with the
// system SSH daemon. This is the "just the auth" behaviour: the supplied
// password is checked against the system account exactly as PAM's auth modules
// would, and nothing else. Returns nil on success, an error on failure.
//
// The panel runs as root, so pam_unix can read /etc/shadow to verify the hash
// (including yescrypt/sha512crypt) for any local account.
func authenticatePAM(username, password string) error {
if username == "" {
return errors.New("pam: empty username")
}
t, err := pam.StartFunc(pamServiceName(), username, func(s pam.Style, msg string) (string, error) {
switch s {
case pam.PromptEchoOff, pam.PromptEchoOn:
return password, nil
case pam.ErrorMsg, pam.TextInfo:
// Informational messages from modules; nothing to return.
return "", nil
default:
return "", fmt.Errorf("pam: unsupported conversation style %v", s)
}
})
if err != nil {
return fmt.Errorf("pam start: %w", err)
}
defer func() { _ = t.End() }()
// Silent keeps modules from writing to stdout/syslog noise; auth phase only.
if err := t.Authenticate(pam.Silent); err != nil {
return fmt.Errorf("pam auth: %w", err)
}
return nil
}
-16
View File
@@ -1,16 +0,0 @@
//go:build !linux || !cgo
package main
import "errors"
// pamAuthAvailable is false when the binary is built without cgo/PAM support
// (for example the Windows development build, or any CGO_ENABLED=0 build).
// In that case a user flagged for PAM auth can never authenticate.
const pamAuthAvailable = false
func pamServiceName() string { return "" }
func authenticatePAM(username, password string) error {
return errors.New("pam authentication is not supported in this build")
}
+5 -18
View File
@@ -79,29 +79,21 @@ detect_pkg_manager() {
} }
set_update_deps() { set_update_deps() {
# PAM_DEV_PKG provides <security/pam_appl.h>, needed to build the optional
# PAM auth support (cgo). Tracked separately because it is a dev header, not
# a command, so the command-based dependency check cannot detect it.
case "$PKG_MANAGER" in case "$PKG_MANAGER" in
apt) apt)
PAM_DEV_PKG="libpam0g-dev" UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip)
UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip "$PAM_DEV_PKG")
;; ;;
dnf|yum) dnf|yum)
PAM_DEV_PKG="pam-devel" UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip)
UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip "$PAM_DEV_PKG")
;; ;;
zypper) zypper)
PAM_DEV_PKG="pam-devel" UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip)
UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip "$PAM_DEV_PKG")
;; ;;
pacman) pacman)
PAM_DEV_PKG="pam" UPDATE_DEPS=(git rsync wget ca-certificates python gcc make tar gzip)
UPDATE_DEPS=(git rsync wget ca-certificates python gcc make tar gzip "$PAM_DEV_PKG")
;; ;;
apk) apk)
PAM_DEV_PKG="linux-pam-dev" UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip)
UPDATE_DEPS=(git rsync wget ca-certificates python3 gcc make tar gzip "$PAM_DEV_PKG")
;; ;;
esac esac
} }
@@ -138,11 +130,6 @@ ensure_update_dependencies() {
if ! command -v python3 >/dev/null 2>&1; then if ! command -v python3 >/dev/null 2>&1; then
missing=true missing=true
fi fi
# The PAM auth build needs <security/pam_appl.h>; a dev header install, not a
# command, so check for the header file directly.
if [ ! -e /usr/include/security/pam_appl.h ]; then
missing=true
fi
if $missing; then if $missing; then
warn "One or more updater dependencies are missing. Installing them with $PKG_MANAGER..." warn "One or more updater dependencies are missing. Installing them with $PKG_MANAGER..."
pkg_update pkg_update