Files
seaweedfs/weed/storage/blockvol/iscsi/auth.go
T
Ping QiuandClaude Opus 4.6 531ee764ee feat: Phase 5 CP5-3 -- CHAP auth, online resize, Prometheus metrics, 12 tests
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>
2026-03-03 01:43:42 -08:00

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)
}