SMB shares and NFS exports may now point to any existing directory (USB disks under /mnt or /media, /srv, home folders...) instead of only ZFS dataset mountpoints. System trees (/etc, /usr, /var, /boot, /proc, /root, ...) are refused, also after resolving symlinks. The path suggestions list mounted data filesystems from /proc/self/mounts in addition to ZFS mountpoints. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+37
-19
@@ -53,21 +53,39 @@ func (s *Server) setApplyErr(e string) {
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// mountpoints lists dataset mountpoints — the only places shares may point to.
|
||||
func mountpoints(st sched.Status) []string {
|
||||
// pathSuggestions lists ZFS dataset mountpoints plus other mounted data
|
||||
// filesystems (USB disks, /mnt, /srv…) for the path input. Any non-system
|
||||
// folder may be shared; these are only hints.
|
||||
func (s *Server) pathSuggestions(st sched.Status) []string {
|
||||
seen := map[string]bool{}
|
||||
var out []string
|
||||
for _, d := range st.Datasets {
|
||||
if strings.HasPrefix(d.Mountpoint, "/") {
|
||||
out = append(out, d.Mountpoint)
|
||||
add := func(p string) {
|
||||
if strings.HasPrefix(p, "/") && !seen[p] && shares.ValidateNotSystem(p) == nil {
|
||||
seen[p] = true
|
||||
out = append(out, p)
|
||||
}
|
||||
}
|
||||
for _, d := range st.Datasets {
|
||||
add(d.Mountpoint)
|
||||
}
|
||||
for _, p := range shares.ParseMounts(s.readMounts()) {
|
||||
add(p)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
func procMounts() string {
|
||||
b, err := os.ReadFile("/proc/self/mounts")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func (s *Server) sharesView(st sched.Status, form smbForm, nform nfsForm) sharesVM {
|
||||
c := s.shares.Get()
|
||||
vm := sharesVM{C: c, Svc: st.Services, Mountpoints: mountpoints(st), Owner: s.shm.Owner, Host: st.Host,
|
||||
vm := sharesVM{C: c, Svc: st.Services, Mountpoints: s.pathSuggestions(st), Owner: s.shm.Owner, Host: st.Host,
|
||||
Form: form, NForm: nform, ApplyErr: s.applyErr(), Squashes: []string{"root", "all", "none"}}
|
||||
if vm.Form.Name == "" && vm.Form.Orig == "" {
|
||||
vm.Form.New, vm.Form.Guest, vm.Form.Browseable, vm.Form.HideSys = true, true, true, true
|
||||
@@ -147,17 +165,22 @@ func (s *Server) sharesDone(w http.ResponseWriter, r *http.Request, msg, anchor
|
||||
http.Redirect(w, r, "/shares?msg="+url.QueryEscape(msg)+anchor, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// pathOK checks that a share path exists and lies inside a ZFS dataset.
|
||||
func (s *Server) pathOK(st sched.Status, p string) error {
|
||||
// pathOK checks that a share path is an existing, non-system directory, also
|
||||
// after resolving symlinks (so /tank/link -> /etc is refused).
|
||||
func (s *Server) pathOK(p string) error {
|
||||
if err := shares.ValidatePath(p); err != nil {
|
||||
return err
|
||||
}
|
||||
if !shares.UnderRoots(p, mountpoints(st)) {
|
||||
return errPathOutside
|
||||
}
|
||||
if !s.dirExists(p) {
|
||||
return errPathMissing
|
||||
}
|
||||
rp, err := s.realPath(p)
|
||||
if err != nil {
|
||||
return errPathMissing
|
||||
}
|
||||
if rp != p && shares.ValidateNotSystem(rp) != nil {
|
||||
return strErr("path resolves to " + rp + ", a system directory")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -170,10 +193,7 @@ type strErr string
|
||||
|
||||
func (e strErr) Error() string { return string(e) }
|
||||
|
||||
const (
|
||||
errPathOutside = strErr("path must be inside a ZFS dataset (see the list of mountpoints)")
|
||||
errPathMissing = strErr("path does not exist or is not a directory")
|
||||
)
|
||||
const errPathMissing = strErr("path does not exist or is not a directory")
|
||||
|
||||
func (s *Server) smbSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.ParseForm(); err != nil {
|
||||
@@ -210,7 +230,6 @@ func (s *Server) smbSave(w http.ResponseWriter, r *http.Request) {
|
||||
if len(f.Path) > 1 {
|
||||
f.Path = strings.TrimRight(f.Path, "/")
|
||||
}
|
||||
st := s.sch.Status()
|
||||
sh := shares.SMBShare{Name: f.Name, Path: f.Path, Comment: f.Comment, ReadOnly: f.ReadOnly, Guest: f.Guest,
|
||||
Users: f.Users, Browseable: f.Browseable, HideSysFiles: f.HideSys}
|
||||
if sh.Guest {
|
||||
@@ -220,7 +239,7 @@ func (s *Server) smbSave(w http.ResponseWriter, r *http.Request) {
|
||||
s.sharesFail(w, r, "Not saved: "+err.Error(), f, nfsForm{})
|
||||
return
|
||||
}
|
||||
if err := s.pathOK(st, sh.Path); err != nil {
|
||||
if err := s.pathOK(sh.Path); err != nil {
|
||||
s.sharesFail(w, r, "Not saved: "+err.Error(), f, nfsForm{})
|
||||
return
|
||||
}
|
||||
@@ -276,13 +295,12 @@ func (s *Server) nfsSave(w http.ResponseWriter, r *http.Request) {
|
||||
if len(f.Path) > 1 {
|
||||
f.Path = strings.TrimRight(f.Path, "/")
|
||||
}
|
||||
st := s.sch.Status()
|
||||
e := shares.NFSExport{Path: f.Path, Clients: shares.SplitList(f.Clients), ReadOnly: f.ReadOnly, Squash: f.Squash, Async: f.Async}
|
||||
if err := e.Validate(); err != nil {
|
||||
s.sharesFail(w, r, "Not saved: "+err.Error(), smbForm{}, f)
|
||||
return
|
||||
}
|
||||
if err := s.pathOK(st, e.Path); err != nil {
|
||||
if err := s.pathOK(e.Path); err != nil {
|
||||
s.sharesFail(w, r, "Not saved: "+err.Error(), smbForm{}, f)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user