mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-24 17:04:30 +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>
184 lines
5.6 KiB
Go
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")
|
|
}
|
|
}
|