Files
seaweedfs/weed/storage/blockvol/iscsi/auth_test.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

184 lines
5.6 KiB
Go

package iscsi
import (
"crypto/md5"
"encoding/hex"
"strconv"
"strings"
"testing"
)
// TestCHAP_LoginSuccess verifies that a correct CHAP username/password
// completes login through the full SecurityNeg -> LoginOp -> FFP flow.
func TestCHAP_LoginSuccess(t *testing.T) {
config := DefaultTargetConfig()
config.TargetName = "iqn.2024.com.seaweedfs:vol1"
config.CHAPConfig = CHAPConfig{
Enabled: true,
Username: "testuser",
Secret: "s3cret",
}
ln := NewLoginNegotiator(config)
resolver := newResolver(config.TargetName)
// PDU 1: Initiator sends SecurityNeg with AuthMethod=CHAP
p1 := NewParams()
p1.Set("InitiatorName", "iqn.2024.com.test:initiator1")
p1.Set("TargetName", config.TargetName)
p1.Set("AuthMethod", "CHAP")
req1 := makeLoginReq(StageSecurityNeg, StageLoginOp, true, p1)
resp1 := ln.HandleLoginPDU(req1, resolver)
if resp1.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("expected success, got class=%d detail=%d",
resp1.LoginStatusClass(), resp1.LoginStatusDetail())
}
if resp1.LoginTransit() {
t.Fatal("expected T=0 (no transit) in challenge response")
}
// Parse challenge params from response
rp1, err := ParseParams(resp1.DataSegment)
if err != nil {
t.Fatalf("parse resp1 params: %v", err)
}
chapA, _ := rp1.Get("CHAP_A")
if chapA != "5" {
t.Fatalf("expected CHAP_A=5, got %q", chapA)
}
chapIStr, _ := rp1.Get("CHAP_I")
chapCStr, _ := rp1.Get("CHAP_C")
if chapIStr == "" || chapCStr == "" {
t.Fatalf("missing CHAP_I or CHAP_C in response")
}
// Compute CHAP response
chapID, _ := strconv.Atoi(chapIStr)
challenge, _ := hex.DecodeString(strings.TrimPrefix(chapCStr, "0x"))
h := md5.New()
h.Write([]byte{byte(chapID)})
h.Write([]byte("s3cret"))
h.Write(challenge)
chapR := "0x" + hex.EncodeToString(h.Sum(nil))
// PDU 2: Initiator sends CHAP_N + CHAP_R
p2 := NewParams()
p2.Set("CHAP_N", "testuser")
p2.Set("CHAP_R", chapR)
req2 := makeLoginReq(StageSecurityNeg, StageLoginOp, true, p2)
resp2 := ln.HandleLoginPDU(req2, resolver)
if resp2.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("expected success after CHAP, got class=%d detail=%d",
resp2.LoginStatusClass(), resp2.LoginStatusDetail())
}
if !resp2.LoginTransit() {
t.Fatal("expected T=1 (transit) after successful CHAP")
}
// Verify AuthMethod=CHAP echoed in the second response.
rp2, err := ParseParams(resp2.DataSegment)
if err != nil {
t.Fatalf("parse resp2 params: %v", err)
}
if am, ok := rp2.Get("AuthMethod"); !ok || am != "CHAP" {
t.Fatalf("expected AuthMethod=CHAP in second response, got %q (ok=%v)", am, ok)
}
// PDU 3: LoginOp -> FullFeature
p3 := NewParams()
p3.Set("MaxRecvDataSegmentLength", "65536")
req3 := makeLoginReq(StageLoginOp, StageFullFeature, true, p3)
resp3 := ln.HandleLoginPDU(req3, resolver)
if resp3.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("expected success at FFP, got class=%d detail=%d",
resp3.LoginStatusClass(), resp3.LoginStatusDetail())
}
if !ln.Done() {
t.Fatal("expected login Done after FFP transition")
}
}
// TestCHAP_LoginWrongPassword verifies that an incorrect CHAP response
// is rejected with AuthFailure.
func TestCHAP_LoginWrongPassword(t *testing.T) {
config := DefaultTargetConfig()
config.TargetName = "iqn.2024.com.seaweedfs:vol1"
config.CHAPConfig = CHAPConfig{
Enabled: true,
Username: "testuser",
Secret: "s3cret",
}
ln := NewLoginNegotiator(config)
resolver := newResolver(config.TargetName)
// PDU 1: SecurityNeg with AuthMethod=CHAP
p1 := NewParams()
p1.Set("InitiatorName", "iqn.2024.com.test:initiator1")
p1.Set("TargetName", config.TargetName)
p1.Set("AuthMethod", "CHAP")
req1 := makeLoginReq(StageSecurityNeg, StageLoginOp, true, p1)
resp1 := ln.HandleLoginPDU(req1, resolver)
if resp1.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("expected success for challenge, got class=%d detail=%d",
resp1.LoginStatusClass(), resp1.LoginStatusDetail())
}
// PDU 2: Wrong password
p2 := NewParams()
p2.Set("CHAP_N", "testuser")
p2.Set("CHAP_R", "0xdeadbeefdeadbeefdeadbeefdeadbeef") // wrong
req2 := makeLoginReq(StageSecurityNeg, StageLoginOp, true, p2)
resp2 := ln.HandleLoginPDU(req2, resolver)
if resp2.LoginStatusClass() != LoginStatusInitiatorErr ||
resp2.LoginStatusDetail() != LoginDetailAuthFailure {
t.Fatalf("expected AuthFailure, got class=%d detail=%d",
resp2.LoginStatusClass(), resp2.LoginStatusDetail())
}
}
// TestCHAP_DisabledAllowsLogin verifies that with CHAP disabled,
// AuthMethod=None works as before.
func TestCHAP_DisabledAllowsLogin(t *testing.T) {
config := DefaultTargetConfig()
config.TargetName = "iqn.2024.com.seaweedfs:vol1"
// CHAPConfig.Enabled defaults to false
ln := NewLoginNegotiator(config)
resolver := newResolver(config.TargetName)
// Single SecurityNeg PDU with transit to LoginOp
p := NewParams()
p.Set("InitiatorName", "iqn.2024.com.test:initiator1")
p.Set("TargetName", config.TargetName)
p.Set("AuthMethod", "None")
req := makeLoginReq(StageSecurityNeg, StageLoginOp, true, p)
resp := ln.HandleLoginPDU(req, resolver)
if resp.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("expected success, got class=%d detail=%d",
resp.LoginStatusClass(), resp.LoginStatusDetail())
}
if !resp.LoginTransit() {
t.Fatal("expected transit with CHAP disabled")
}
// LoginOp -> FFP
p2 := NewParams()
p2.Set("MaxRecvDataSegmentLength", "65536")
req2 := makeLoginReq(StageLoginOp, StageFullFeature, true, p2)
resp2 := ln.HandleLoginPDU(req2, resolver)
if resp2.LoginStatusClass() != LoginStatusSuccess {
t.Fatalf("expected success at FFP, got class=%d detail=%d",
resp2.LoginStatusClass(), resp2.LoginStatusDetail())
}
if !ln.Done() {
t.Fatal("expected login Done")
}
}