mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-24 08:54:28 +00:00
CHAP authentication (RFC 7143 S12.1): - auth.go: CHAPAuthenticator with MD5 challenge-response, ValidateCHAPConfig - login.go: multi-PDU SecurityNeg flow (challenge → verify → transit) - main.go: -chap-user/-chap-secret CLI flags with validation Online volume expand: - blockvol.go: Expand() with flusher pause, snapMu TOCTOU guard, alignment check - Rejects shrink (ErrShrinkNotSupported) and resize with active snapshots Prometheus metrics: - metrics.go: metricsAdapter wrapping BlockDevice, 15 metrics (counters, histograms, gauge funcs for WAL/dirty-map/epoch/role/snapshots) - Dedicated prometheus.NewRegistry() per server instance Admin HTTP endpoints: - POST /snapshot (create/delete/restore/list) - POST /resize (online expand) - GET /metrics (Prometheus text format) - VolumeSize added to /status response Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
110 lines
3.1 KiB
Go
110 lines
3.1 KiB
Go
// auth.go implements CHAP authentication for iSCSI (RFC 7143 S12.1).
|
|
// Only unidirectional (target authenticates initiator) with MD5 (algorithm 5).
|
|
package iscsi
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
var ErrCHAPSecretEmpty = errors.New("iscsi: CHAP enabled but secret is empty")
|
|
|
|
// CHAPConfig holds CHAP authentication settings.
|
|
type CHAPConfig struct {
|
|
Enabled bool
|
|
Username string // expected initiator username (empty = accept any)
|
|
Secret string // shared secret
|
|
}
|
|
|
|
type chapState int
|
|
|
|
const (
|
|
chapIdle chapState = iota
|
|
chapChallengeSent
|
|
chapDone
|
|
)
|
|
|
|
// CHAPAuthenticator drives the target side of a CHAP exchange.
|
|
type CHAPAuthenticator struct {
|
|
config CHAPConfig
|
|
state chapState
|
|
id uint8 // challenge identifier (0-255)
|
|
challenge []byte // 16 random bytes
|
|
}
|
|
|
|
// ValidateCHAPConfig checks that a CHAPConfig is well-formed.
|
|
// Must be called at startup before passing the config to NewLoginNegotiator.
|
|
func ValidateCHAPConfig(c CHAPConfig) error {
|
|
if c.Enabled && c.Secret == "" {
|
|
return ErrCHAPSecretEmpty
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NewCHAPAuthenticator creates a CHAP authenticator for one login session.
|
|
// The config must have been validated with ValidateCHAPConfig at startup.
|
|
func NewCHAPAuthenticator(config CHAPConfig) *CHAPAuthenticator {
|
|
return &CHAPAuthenticator{config: config, state: chapIdle}
|
|
}
|
|
|
|
// IsEnabled returns whether CHAP authentication is required.
|
|
func (a *CHAPAuthenticator) IsEnabled() bool {
|
|
return a.config.Enabled
|
|
}
|
|
|
|
// GenerateChallenge produces the CHAP_A, CHAP_I, CHAP_C parameters for the
|
|
// first security negotiation response. Must be called exactly once.
|
|
func (a *CHAPAuthenticator) GenerateChallenge() (map[string]string, error) {
|
|
// Generate random id byte.
|
|
var idBuf [1]byte
|
|
if _, err := rand.Read(idBuf[:]); err != nil {
|
|
return nil, fmt.Errorf("chap: generate id: %w", err)
|
|
}
|
|
a.id = idBuf[0]
|
|
|
|
// Generate 16-byte random challenge.
|
|
a.challenge = make([]byte, 16)
|
|
if _, err := rand.Read(a.challenge); err != nil {
|
|
return nil, fmt.Errorf("chap: generate challenge: %w", err)
|
|
}
|
|
|
|
a.state = chapChallengeSent
|
|
|
|
return map[string]string{
|
|
"CHAP_A": "5", // MD5
|
|
"CHAP_I": fmt.Sprintf("%d", a.id), // decimal
|
|
"CHAP_C": "0x" + hex.EncodeToString(a.challenge), // hex with 0x prefix
|
|
}, nil
|
|
}
|
|
|
|
// Verify checks the initiator's CHAP_N (username) and CHAP_R (response).
|
|
// Returns true if authentication succeeds.
|
|
func (a *CHAPAuthenticator) Verify(chapN, chapR string) bool {
|
|
if a.state != chapChallengeSent {
|
|
return false
|
|
}
|
|
a.state = chapDone
|
|
|
|
// Check username if configured.
|
|
if a.config.Username != "" && chapN != a.config.Username {
|
|
return false
|
|
}
|
|
|
|
// Compute expected response: MD5(id_byte || secret_bytes || challenge_bytes).
|
|
h := md5.New()
|
|
h.Write([]byte{a.id})
|
|
h.Write([]byte(a.config.Secret))
|
|
h.Write(a.challenge)
|
|
expected := hex.EncodeToString(h.Sum(nil))
|
|
|
|
// Normalize initiator response: strip "0x" prefix if present.
|
|
got := strings.TrimPrefix(chapR, "0x")
|
|
got = strings.TrimPrefix(got, "0X")
|
|
|
|
return strings.EqualFold(expected, got)
|
|
}
|