Browse Source

fix: iSCSI login and discovery bugs found in WSL2 smoke test

- Skip InitiatorAlias in negotiation (was returning NotUnderstood)
- Capture TargetName in StageLoginOp direct-jump path (iscsiadm skips
  security stage, sends CSG=LoginOp directly -- nil SCSIHandler crash)
- Add portalAddr to TargetServer for discovery responses (listener on
  [::] is not routable from WSL2 clients)
- Add -portal flag to iscsi-target binary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feature/sw-block
Ping Qiu 1 week ago
parent
commit
6546c549eb
  1. 6
      weed/storage/blockvol/iscsi/cmd/iscsi-target/main.go
  2. 27
      weed/storage/blockvol/iscsi/login.go
  3. 26
      weed/storage/blockvol/iscsi/target.go

6
weed/storage/blockvol/iscsi/cmd/iscsi-target/main.go

@ -22,9 +22,10 @@ import (
func main() { func main() {
volPath := flag.String("vol", "", "path to BlockVol file") volPath := flag.String("vol", "", "path to BlockVol file")
addr := flag.String("addr", ":3260", "listen address") addr := flag.String("addr", ":3260", "listen address")
portal := flag.String("portal", "", "advertised address for discovery (e.g. 10.0.0.1:3260,1)")
iqn := flag.String("iqn", "iqn.2024.com.seaweedfs:vol1", "target IQN") iqn := flag.String("iqn", "iqn.2024.com.seaweedfs:vol1", "target IQN")
create := flag.Bool("create", false, "create a new volume file") create := flag.Bool("create", false, "create a new volume file")
size := flag.String("size", "1G", "volume size (e.g., 1G, 100M) used with -create")
size := flag.String("size", "1G", "volume size (e.g., 1G, 100M) -- used with -create")
flag.Parse() flag.Parse()
if *volPath == "" { if *volPath == "" {
@ -73,6 +74,9 @@ func main() {
config.TargetName = *iqn config.TargetName = *iqn
config.TargetAlias = "SeaweedFS BlockVol" config.TargetAlias = "SeaweedFS BlockVol"
ts := iscsi.NewTargetServer(*addr, config, logger) ts := iscsi.NewTargetServer(*addr, config, logger)
if *portal != "" {
ts.SetPortalAddr(*portal)
}
ts.AddVolume(*iqn, adapter) ts.AddVolume(*iqn, adapter)
// Graceful shutdown on signal // Graceful shutdown on signal

27
weed/storage/blockvol/iscsi/login.go

@ -216,6 +216,27 @@ func (ln *LoginNegotiator) HandleLoginPDU(req *PDU, resolver TargetResolver) *PD
setLoginReject(resp, LoginStatusInitiatorErr, LoginDetailMissingParam) setLoginReject(resp, LoginStatusInitiatorErr, LoginDetailMissingParam)
return resp return resp
} }
// Session type
if st, ok := params.Get("SessionType"); ok {
ln.SessionType = st
}
if ln.SessionType == "" {
ln.SessionType = "Normal"
}
// Target name (required for Normal sessions)
if tn, ok := params.Get("TargetName"); ok {
if ln.SessionType == "Normal" {
if resolver == nil || !resolver.HasTarget(tn) {
setLoginReject(resp, LoginStatusInitiatorErr, LoginDetailNotFound)
return resp
}
}
ln.TargetName = tn
ln.targetOK = true
} else if ln.SessionType == "Normal" && !ln.targetOK {
setLoginReject(resp, LoginStatusInitiatorErr, LoginDetailMissingParam)
return resp
}
} else { } else {
setLoginReject(resp, LoginStatusInitiatorErr, LoginDetailInitiatorError) setLoginReject(resp, LoginStatusInitiatorErr, LoginDetailInitiatorError)
return resp return resp
@ -319,9 +340,9 @@ func (ln *LoginNegotiator) negotiateParams(req *Params, resp *Params) {
case "DataDigest": case "DataDigest":
resp.Set(key, "None") resp.Set(key, "None")
case "TargetName", "InitiatorName", "SessionType", "AuthMethod": case "TargetName", "InitiatorName", "SessionType", "AuthMethod":
// Already handled or declarative skip
case "TargetAlias":
// Informational from initiator — skip
// Already handled or declarative -- skip
case "TargetAlias", "InitiatorAlias":
// Informational -- skip
default: default:
// Unknown keys: respond with NotUnderstood // Unknown keys: respond with NotUnderstood
resp.Set(key, "NotUnderstood") resp.Set(key, "NotUnderstood")

26
weed/storage/blockvol/iscsi/target.go

@ -17,11 +17,12 @@ var (
// TargetServer manages the iSCSI target: TCP listener, volume registry, // TargetServer manages the iSCSI target: TCP listener, volume registry,
// and active sessions. // and active sessions.
type TargetServer struct { type TargetServer struct {
mu sync.RWMutex
listener net.Listener
config TargetConfig
volumes map[string]BlockDevice // target IQN -> device
addr string
mu sync.RWMutex
listener net.Listener
config TargetConfig
volumes map[string]BlockDevice // target IQN -> device
addr string
portalAddr string // advertised address for discovery (if empty, uses listener addr)
// Active session tracking for graceful shutdown // Active session tracking for graceful shutdown
activeMu sync.Mutex activeMu sync.Mutex
@ -72,15 +73,28 @@ func (ts *TargetServer) HasTarget(name string) bool {
return ok return ok
} }
// SetPortalAddr sets the address advertised in discovery responses.
// Use this when the listen address is 0.0.0.0 or [::] and clients
// need a routable IP. Format: "ip:port,portal-group" (e.g. "10.0.0.1:3260,1").
func (ts *TargetServer) SetPortalAddr(addr string) {
ts.mu.Lock()
defer ts.mu.Unlock()
ts.portalAddr = addr
}
// ListTargets implements TargetLister. // ListTargets implements TargetLister.
func (ts *TargetServer) ListTargets() []DiscoveryTarget { func (ts *TargetServer) ListTargets() []DiscoveryTarget {
ts.mu.RLock() ts.mu.RLock()
defer ts.mu.RUnlock() defer ts.mu.RUnlock()
addr := ts.portalAddr
if addr == "" {
addr = ts.ListenAddr()
}
targets := make([]DiscoveryTarget, 0, len(ts.volumes)) targets := make([]DiscoveryTarget, 0, len(ts.volumes))
for iqn := range ts.volumes { for iqn := range ts.volumes {
targets = append(targets, DiscoveryTarget{ targets = append(targets, DiscoveryTarget{
Name: iqn, Name: iqn,
Address: ts.ListenAddr(),
Address: addr,
}) })
} }
return targets return targets

Loading…
Cancel
Save