2 Commits
Author SHA1 Message Date
a.gritsenkoandClaude Opus 5.5 b965d2c9db SMB: optional legacy mode (SMB1 + NTLMv1) for old clients
build / build (push) Successful in 1m1s
Supermicro BMC virtual media ("CD-ROM image" over a Windows share) only
offers the SMB1 dialect "NT LM 0.12", which Samba rejects with
server min protocol = SMB2. Add an opt-in "Allow legacy clients" SMB
setting that renders server min protocol = NT1 and
ntlm auth = ntlmv1-permitted. Off by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:59:53 +03:00
a.gritsenkoandClaude Opus 5.5 6362f1b9f7 SMB: map to guest = Never when no share allows guests
build / build (push) Successful in 1m2s
With "Bad User" an unknown account (the Windows login) silently became a
guest session; Windows 10/11 refuse guest sessions and report "network
path not found" instead of asking for a password, so password-only shares
were unreachable from Explorer. Map to guest only while a guest share
exists, and document the net use workaround on the shares page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:50:12 +03:00
7 changed files with 66 additions and 4 deletions
+1
View File
@@ -28,6 +28,7 @@ type SMBConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Workgroup string `json:"workgroup"` Workgroup string `json:"workgroup"`
ServerString string `json:"server_string"` ServerString string `json:"server_string"`
Legacy bool `json:"legacy"` // allow SMB1 + NTLMv1 (IPMI/BMC virtual media, old devices)
Shares []SMBShare `json:"shares"` Shares []SMBShare `json:"shares"`
} }
+24 -2
View File
@@ -15,20 +15,42 @@ func yesno(b bool) string {
return "no" return "no"
} }
func anyGuest(c Config) bool {
for _, s := range c.SMB.Shares {
if s.Guest {
return true
}
}
return false
}
// RenderSMB produces the complete smb.conf. owner is the Unix account every // RenderSMB produces the complete smb.conf. owner is the Unix account every
// SMB session acts as (force user), so files end up with the same owner the // SMB session acts as (force user), so files end up with the same owner the
// containers use. // containers use.
func RenderSMB(c Config, owner string) string { func RenderSMB(c Config, owner string) string {
var b strings.Builder var b strings.Builder
fmt.Fprintf(&b, "%s\n\n[global]\n", Header) fmt.Fprintf(&b, "%s\n\n[global]\n", Header)
// Bad User turns unknown accounts (e.g. the Windows login) into guests.
// Without guest shares that only yields a guest session Windows refuses
// with a misleading "network path not found" instead of a password prompt.
mapGuest := "Never"
if anyGuest(c) {
mapGuest = "Bad User"
}
minProto := "server min protocol = SMB2"
if c.SMB.Legacy {
// old embedded clients (e.g. Supermicro BMC virtual media) only offer
// SMB1 "NT LM 0.12" and authenticate with NTLMv1
minProto = "server min protocol = NT1\n ntlm auth = ntlmv1-permitted"
}
g := []string{ g := []string{
"workgroup = " + c.SMB.Workgroup, "workgroup = " + c.SMB.Workgroup,
"server string = " + c.SMB.ServerString, "server string = " + c.SMB.ServerString,
"server role = standalone server", "server role = standalone server",
"security = user", "security = user",
"map to guest = Bad User", "map to guest = " + mapGuest,
"guest account = nobody", "guest account = nobody",
"server min protocol = SMB2", minProto,
"unix charset = UTF-8", "unix charset = UTF-8",
"logging = file", "logging = file",
"log file = /var/log/samba/log.%m", "log file = /var/log/samba/log.%m",
+13
View File
@@ -40,6 +40,19 @@ func TestRenderSMB(t *testing.T) {
if strings.Count(out, "\n veto files = ") != 1 { if strings.Count(out, "\n veto files = ") != 1 {
t.Errorf("veto files only where requested") t.Errorf("veto files only where requested")
} }
// without guest shares unknown users are rejected, so Windows asks for a password
c := sample()
c.SMB.Shares = c.SMB.Shares[1:]
if out := RenderSMB(c, "ansible"); !strings.Contains(out, "map to guest = Never") || strings.Contains(out, "Bad User") {
t.Errorf("user-only config must not map to guest:\n%s", out)
}
if out := RenderSMB(c, "ansible"); strings.Contains(out, "NT1") || strings.Contains(out, "ntlmv1") {
t.Errorf("SMB1 enabled without the legacy flag")
}
c.SMB.Legacy = true
if out := RenderSMB(c, "ansible"); !strings.Contains(out, "\n server min protocol = NT1\n ntlm auth = ntlmv1-permitted\n") || strings.Contains(out, "SMB2") {
t.Errorf("legacy mode:\n%s", out)
}
} }
func TestRenderExports(t *testing.T) { func TestRenderExports(t *testing.T) {
+1
View File
@@ -204,6 +204,7 @@ func (s *Server) smbSettings(w http.ResponseWriter, r *http.Request) {
c.SMB.Enabled = r.FormValue("enabled") != "" c.SMB.Enabled = r.FormValue("enabled") != ""
c.SMB.Workgroup = strings.ToUpper(strings.TrimSpace(r.FormValue("workgroup"))) c.SMB.Workgroup = strings.ToUpper(strings.TrimSpace(r.FormValue("workgroup")))
c.SMB.ServerString = strings.TrimSpace(r.FormValue("server_string")) c.SMB.ServerString = strings.TrimSpace(r.FormValue("server_string"))
c.SMB.Legacy = r.FormValue("legacy") != ""
if err := shares.ValidateComment(c.SMB.ServerString); err != nil { if err := shares.ValidateComment(c.SMB.ServerString); err != nil {
s.sharesFail(w, r, "Not saved: server description: "+err.Error(), smbForm{}, nfsForm{}) s.sharesFail(w, r, "Not saved: server description: "+err.Error(), smbForm{}, nfsForm{})
return return
+21
View File
@@ -226,3 +226,24 @@ func TestSharesNFSFlow(t *testing.T) {
t.Errorf("delete: %d %v", rec.Code, f.calls) t.Errorf("delete: %d %v", rec.Code, f.calls)
} }
} }
func TestSharesSMBLegacySetting(t *testing.T) {
srv, _, _ := newTestServer(t)
f := &shareRunner{users: "alice:1001:\n"}
withShareRunner(srv, f)
postForm(srv, "/shares/smb/save", url.Values{"name": {"iso"}, "path": {"/tank/data"}, "users": {"alice"}, "browseable": {"1"}})
rec := postForm(srv, "/shares/smb/settings", url.Values{"enabled": {"1"}, "workgroup": {"workgroup"}, "server_string": {"NAS"}, "legacy": {"1"}})
if rec.Code != http.StatusSeeOther || !srv.shares.Get().SMB.Legacy {
t.Fatalf("legacy not saved: %d %s", rec.Code, rec.Body.String())
}
if conf := f.written(shares.DefaultSMBConf); !strings.Contains(conf, "server min protocol = NT1") {
t.Errorf("smb.conf without SMB1:\n%s", conf)
}
if !strings.Contains(get(srv, "/shares").Body.String(), `name="legacy" value="1" checked`) {
t.Errorf("legacy checkbox not shown as checked")
}
postForm(srv, "/shares/smb/settings", url.Values{"enabled": {"1"}, "workgroup": {"WORKGROUP"}, "server_string": {"NAS"}})
if srv.shares.Get().SMB.Legacy || !strings.Contains(f.written(shares.DefaultSMBConf), "server min protocol = SMB2") {
t.Errorf("legacy not cleared")
}
}
+3 -1
View File
@@ -18,6 +18,7 @@
<label class="check"><input type="checkbox" name="enabled" value="1" {{if .C.SMB.Enabled}}checked{{end}}> SMB server enabled</label> <label class="check"><input type="checkbox" name="enabled" value="1" {{if .C.SMB.Enabled}}checked{{end}}> SMB server enabled</label>
<label>Workgroup <input name="workgroup" value="{{.C.SMB.Workgroup}}"></label> <label>Workgroup <input name="workgroup" value="{{.C.SMB.Workgroup}}"></label>
<label>Description <input name="server_string" value="{{.C.SMB.ServerString}}"></label> <label>Description <input name="server_string" value="{{.C.SMB.ServerString}}"></label>
<label class="check"><input type="checkbox" name="legacy" value="1" {{if .C.SMB.Legacy}}checked{{end}}> Allow legacy clients (SMB1 + NTLMv1) — IPMI/BMC virtual media, old devices; less secure</label>
<div class="field"><span class="field-name">&nbsp;</span><span><button type="submit" class="secondary">Save SMB settings</button></span></div> <div class="field"><span class="field-name">&nbsp;</span><span><button type="submit" class="secondary">Save SMB settings</button></span></div>
</form> </form>
@@ -80,7 +81,8 @@
<div class="field"><span class="field-name">&nbsp;</span><span><button type="submit" class="secondary">Add user / set password</button></span></div> <div class="field"><span class="field-name">&nbsp;</span><span><button type="submit" class="secondary">Add user / set password</button></span></div>
</form> </form>
<p class="muted small">Windows: <span class="mono">\\{{.Host}}\share</span> in Explorer, or map a network drive. macOS: Finder → Go → Connect to Server → <span class="mono">smb://{{.Host}}/share</span>. <p class="muted small">Windows: <span class="mono">\\{{.Host}}\share</span> in Explorer, or map a network drive. macOS: Finder → Go → Connect to Server → <span class="mono">smb://{{.Host}}/share</span>.
Note: Windows 10/11 Pro/Enterprise refuse <i>guest</i> shares by default (error 0x80070035 «network path not found») — use an SMB user with a password, or allow guest access on the PC (gpedit: Lanman Workstation → Enable insecure guest logons).</p> Note: Windows 10/11 Pro/Enterprise refuse <i>guest</i> shares by default (error 0x80070035 «network path not found») — use an SMB user with a password, or allow guest access on the PC (gpedit: Lanman Workstation → Enable insecure guest logons).
If a password share still gives 0x80070035, connect explicitly: <span class="mono">net use \\{{.Host}}\share /user:&lt;name&gt; *</span> (Windows keeps one login per server — run <span class="mono">net use * /delete</span> first).</p>
</section> </section>
<section class="card" id="nfs"> <section class="card" id="nfs">
+3 -1
View File
@@ -36,7 +36,9 @@ also after resolving symlinks. Definitions are stored in
/etc/samba/smb.conf (whole file, header "Managed by nasmon") and /etc/samba/smb.conf (whole file, header "Managed by nasmon") and
/etc/exports.d/nasmon.exports, validates with testparm, then reloads smbd / /etc/exports.d/nasmon.exports, validates with testparm, then reloads smbd /
exportfs -ra. A rejected config is rolled back to the .bak copy. Daemons are exportfs -ra. A rejected config is rolled back to the .bak copy. Daemons are
enabled while at least one share exists and disabled otherwise. enabled while at least one share exists and disabled otherwise. "Allow legacy
clients" in the SMB settings turns on SMB1 + NTLMv1 (server min protocol = NT1)
for IPMI/BMC virtual media and other old devices; it is off by default.
Alerts: the × on an open alert acknowledges it (hidden and not mailed until the Alerts: the × on an open alert acknowledges it (hidden and not mailed until the
problem changes or clears); the × on a resolved alert removes it from the log. problem changes or clears); the × on a resolved alert removes it from the log.