c81df55dcb
Sibling shares source for the local cross-drive tier. Reuses the tier2Mirror seam, selectTier2TargetFrom (narrow source-drive seam extracted from selectTier2Target), tier2ReconcileRoots (pure extraction), tier2SafeRemove, the marker-LAST discipline and the recordTier2* helpers. Per-app paths are untouched. - shares_payload.go: deterministic _shares-manifest.json + best-effort passdb capture - tier2_shares.go: per-source-drive legs -> cross-drive target, payload, marker LAST - infra.SambaContainerName/SambaPassdbVolume/Mount: single source of truth for the container identity (renderer, stacks execs, backup execs, monitor all read it) - RESERVED-NAME finding: ValidateSMBShareName did NOT exclude a leading underscore, so "_shares" was an accepted share name. Now refused; RunAllTier2 additionally skips a "_shares" stack loudly as defense in depth. - fix: shareSourceDrive returned a slash-normalised path, which made the target selector's source-drive equality check miss (a group could target its own drive)
131 lines
5.4 KiB
Go
131 lines
5.4 KiB
Go
package infra
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Samba (LAN network-sharing) renderers — R-7 slice 1. PURE like the rest of this package: share
|
|
// list in, file contents out; no docker, no filesystem. The orchestration (availability filtering,
|
|
// write, compose up, smbpasswd) lives in internal/stacks/samba.go (ReconcileSamba). The transport +
|
|
// daemon set are the R-6 spike verdict (SPIKE-lan-discovery-2026-07-18): host network, smbd + nmbd +
|
|
// wsdd, `bind interfaces only` on lo+eth0.
|
|
|
|
// SambaShareRender is one exported folder as the renderer needs it (already availability-filtered by
|
|
// the caller — a dead-mount share is simply absent from the slice, never rendered).
|
|
type SambaShareRender struct {
|
|
Name string
|
|
Path string // absolute host path
|
|
ReadOnly bool
|
|
}
|
|
|
|
// SambaData is the full input for both samba renderers.
|
|
type SambaData struct {
|
|
ServerName string // NetBIOS name (validated by the settings layer)
|
|
Shares []SambaShareRender
|
|
UID int // household uid/gid the container runs shares as (1000)
|
|
}
|
|
|
|
// SambaHouseholdUser is the single household SMB account name (matches the entrypoint's unix user).
|
|
const SambaHouseholdUser = "felhom"
|
|
|
|
// SambaContainerName is the fixed container name the compose render pins. It is THE single source of
|
|
// truth for it: the renderer below interpolates this constant, stacks.sambaContainer aliases it for
|
|
// the exec paths, monitor's effective-protected set watches it (R-7b liveness), and the backup
|
|
// package execs it for the passdb capture/restore. Anything that needs "which container is samba"
|
|
// reads this — never a second string literal.
|
|
const SambaContainerName = "felhom-samba"
|
|
|
|
// SambaPassdbVolume / SambaPassdbMount name the docker volume that holds the household SMB credential
|
|
// (the passdb tdb set) and where the container mounts it. R-7b's payload capture/restore targets this
|
|
// mount point, so it is pinned here beside the render that creates it rather than duplicated there.
|
|
const (
|
|
SambaPassdbVolume = "samba-passdb"
|
|
SambaPassdbMount = "/var/lib/samba"
|
|
)
|
|
|
|
// RenderSambaConfig renders smb.conf: the hardened global block (bind interfaces only = lo eth0,
|
|
// SMB2+ floor, NetBIOS on for flat-name resolution) plus one [section] per share. Deterministic:
|
|
// shares are emitted in the given order (the caller preserves registry order). force user/group pin
|
|
// every written file to the household uid so apps and both backup tiers see consistent ownership.
|
|
func RenderSambaConfig(d SambaData) string {
|
|
var b strings.Builder
|
|
b.WriteString("# Samba (LAN network-sharing) — managed by felhom-controller (R-7).\n")
|
|
b.WriteString("# WARNING: auto-generated. Manual edits are overwritten on the next share change.\n")
|
|
b.WriteString("[global]\n")
|
|
b.WriteString(" workgroup = WORKGROUP\n")
|
|
b.WriteString(" server string = Felhom hálózati megosztás\n")
|
|
fmt.Fprintf(&b, " netbios name = %s\n", d.ServerName)
|
|
b.WriteString(" security = user\n")
|
|
b.WriteString(" map to guest = never\n")
|
|
b.WriteString(" server min protocol = SMB2\n")
|
|
b.WriteString(" disable netbios = no\n")
|
|
b.WriteString(" bind interfaces only = yes\n")
|
|
b.WriteString(" interfaces = lo eth0\n")
|
|
b.WriteString(" smb ports = 445\n")
|
|
b.WriteString(" load printers = no\n")
|
|
b.WriteString(" printing = bsd\n")
|
|
b.WriteString(" printcap name = /dev/null\n")
|
|
b.WriteString(" disable spoolss = yes\n")
|
|
|
|
for _, sh := range d.Shares {
|
|
ro := "no"
|
|
if sh.ReadOnly {
|
|
ro = "yes"
|
|
}
|
|
fmt.Fprintf(&b, "\n[%s]\n", sh.Name)
|
|
fmt.Fprintf(&b, " path = %s\n", sh.Path)
|
|
fmt.Fprintf(&b, " read only = %s\n", ro)
|
|
fmt.Fprintf(&b, " valid users = %s\n", SambaHouseholdUser)
|
|
fmt.Fprintf(&b, " force user = %s\n", SambaHouseholdUser)
|
|
fmt.Fprintf(&b, " force group = %s\n", SambaHouseholdUser)
|
|
b.WriteString(" create mask = 0644\n")
|
|
b.WriteString(" directory mask = 0755\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// RenderSambaCompose renders the samba stack's docker-compose.yml: host network (the spike mandate —
|
|
// the default bridge is deaf to LAN multicast), the pinned image, smb.conf bind-mounted read-only,
|
|
// the passdb named volume, and one bind per share (:ro for read-only shares — defense in depth beside
|
|
// the smb.conf-level `read only`). Deterministic: share binds are sorted so the output is stable.
|
|
func RenderSambaCompose(d SambaData) string {
|
|
shares := make([]SambaShareRender, len(d.Shares))
|
|
copy(shares, d.Shares)
|
|
sort.Slice(shares, func(i, j int) bool { return shares[i].Path < shares[j].Path })
|
|
|
|
var binds strings.Builder
|
|
for _, sh := range shares {
|
|
if sh.ReadOnly {
|
|
fmt.Fprintf(&binds, " - %s:%s:ro\n", sh.Path, sh.Path)
|
|
} else {
|
|
fmt.Fprintf(&binds, " - %s:%s\n", sh.Path, sh.Path)
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf(`# Samba (LAN network-sharing) — managed by felhom-controller (R-7 slice 1).
|
|
# WARNING: auto-generated. Manual edits are overwritten on the next share change.
|
|
# Host network is REQUIRED (SPIKE-lan-discovery-2026-07-18): the default docker bridge cannot
|
|
# receive the LAN multicast that WSD/mDNS discovery needs.
|
|
|
|
services:
|
|
%[1]s:
|
|
image: %[2]s
|
|
container_name: %[1]s
|
|
restart: unless-stopped
|
|
network_mode: host
|
|
environment:
|
|
- FELHOM_SERVER_NAME=%[3]s
|
|
- FELHOM_IFACE=eth0
|
|
- FELHOM_UID=%[4]d
|
|
- FELHOM_GID=%[4]d
|
|
volumes:
|
|
- ./smb.conf:/etc/samba/smb.conf:ro
|
|
- %[5]s:%[6]s
|
|
%[7]s
|
|
volumes:
|
|
%[5]s:
|
|
`, SambaContainerName, SambaImage, d.ServerName, d.UID, SambaPassdbVolume, SambaPassdbMount, binds.String())
|
|
}
|