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" // SambaHostInterface is the guest's LAN interface, and the single source of truth for it: smb.conf's // `interfaces =` line, the container's FELHOM_IFACE env, and the controller's LAN-address read // (stacks.SambaLANAddress, v0.151.0) must all name the SAME nic or the service and the address the // page prints drift apart. const SambaHostInterface = "eth0" // 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") fmt.Fprintf(&b, " interfaces = lo %s\n", SambaHostInterface) 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=%[8]s - 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(), SambaHostInterface) }