From 6546c549eb2056509b5ef4839e1555e04937d02e Mon Sep 17 00:00:00 2001 From: Ping Qiu Date: Sat, 28 Feb 2026 10:04:41 -0800 Subject: [PATCH] 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 --- .../blockvol/iscsi/cmd/iscsi-target/main.go | 6 ++++- weed/storage/blockvol/iscsi/login.go | 27 ++++++++++++++++--- weed/storage/blockvol/iscsi/target.go | 26 +++++++++++++----- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/weed/storage/blockvol/iscsi/cmd/iscsi-target/main.go b/weed/storage/blockvol/iscsi/cmd/iscsi-target/main.go index 6d023ec06..13d74ebc8 100644 --- a/weed/storage/blockvol/iscsi/cmd/iscsi-target/main.go +++ b/weed/storage/blockvol/iscsi/cmd/iscsi-target/main.go @@ -22,9 +22,10 @@ import ( func main() { volPath := flag.String("vol", "", "path to BlockVol file") 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") 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() if *volPath == "" { @@ -73,6 +74,9 @@ func main() { config.TargetName = *iqn config.TargetAlias = "SeaweedFS BlockVol" ts := iscsi.NewTargetServer(*addr, config, logger) + if *portal != "" { + ts.SetPortalAddr(*portal) + } ts.AddVolume(*iqn, adapter) // Graceful shutdown on signal diff --git a/weed/storage/blockvol/iscsi/login.go b/weed/storage/blockvol/iscsi/login.go index 4d42d20ab..8d6d2d125 100644 --- a/weed/storage/blockvol/iscsi/login.go +++ b/weed/storage/blockvol/iscsi/login.go @@ -216,6 +216,27 @@ func (ln *LoginNegotiator) HandleLoginPDU(req *PDU, resolver TargetResolver) *PD setLoginReject(resp, LoginStatusInitiatorErr, LoginDetailMissingParam) 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 { setLoginReject(resp, LoginStatusInitiatorErr, LoginDetailInitiatorError) return resp @@ -319,9 +340,9 @@ func (ln *LoginNegotiator) negotiateParams(req *Params, resp *Params) { case "DataDigest": resp.Set(key, "None") 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: // Unknown keys: respond with NotUnderstood resp.Set(key, "NotUnderstood") diff --git a/weed/storage/blockvol/iscsi/target.go b/weed/storage/blockvol/iscsi/target.go index 8746a22ce..d20bef19d 100644 --- a/weed/storage/blockvol/iscsi/target.go +++ b/weed/storage/blockvol/iscsi/target.go @@ -17,11 +17,12 @@ var ( // TargetServer manages the iSCSI target: TCP listener, volume registry, // and active sessions. 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 activeMu sync.Mutex @@ -72,15 +73,28 @@ func (ts *TargetServer) HasTarget(name string) bool { 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. func (ts *TargetServer) ListTargets() []DiscoveryTarget { ts.mu.RLock() defer ts.mu.RUnlock() + addr := ts.portalAddr + if addr == "" { + addr = ts.ListenAddr() + } targets := make([]DiscoveryTarget, 0, len(ts.volumes)) for iqn := range ts.volumes { targets = append(targets, DiscoveryTarget{ Name: iqn, - Address: ts.ListenAddr(), + Address: addr, }) } return targets