Trim the Domain Name from cached image references

This commit removes the Domain Name, if any, from the cached image
reference before computing the image fingerprint. This way the same
image, if stored in some oter mirror, is still seen as the same one.

Fixes #158
This commit is contained in:
David Cassany
2021-01-29 10:50:32 +01:00
parent 9f73a334b3
commit 18e9ce4557
59 changed files with 425 additions and 7802 deletions

View File

@@ -1,78 +0,0 @@
package oci
import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/docker/docker/pkg/idtools"
"github.com/moby/buildkit/executor"
"github.com/moby/buildkit/identity"
)
const hostsContent = `
127.0.0.1 localhost buildkitsandbox
::1 localhost ip6-localhost ip6-loopback
`
func GetHostsFile(ctx context.Context, stateDir string, extraHosts []executor.HostIP, idmap *idtools.IdentityMapping) (string, func(), error) {
if len(extraHosts) == 0 {
_, err := g.Do(ctx, stateDir, func(ctx context.Context) (interface{}, error) {
_, _, err := makeHostsFile(stateDir, nil, idmap)
return nil, err
})
if err != nil {
return "", nil, err
}
return filepath.Join(stateDir, "hosts"), func() {}, nil
}
return makeHostsFile(stateDir, extraHosts, idmap)
}
func makeHostsFile(stateDir string, extraHosts []executor.HostIP, idmap *idtools.IdentityMapping) (string, func(), error) {
p := filepath.Join(stateDir, "hosts")
if len(extraHosts) != 0 {
p += "." + identity.NewID()
}
_, err := os.Stat(p)
if err == nil {
return "", func() {}, nil
}
if !os.IsNotExist(err) {
return "", nil, err
}
b := &bytes.Buffer{}
if _, err := b.Write([]byte(hostsContent)); err != nil {
return "", nil, err
}
for _, h := range extraHosts {
if _, err := b.Write([]byte(fmt.Sprintf("%s\t%s\n", h.IP.String(), h.Host))); err != nil {
return "", nil, err
}
}
tmpPath := p + ".tmp"
if err := ioutil.WriteFile(tmpPath, b.Bytes(), 0644); err != nil {
return "", nil, err
}
if idmap != nil {
root := idmap.RootPair()
if err := os.Chown(tmpPath, root.UID, root.GID); err != nil {
return "", nil, err
}
}
if err := os.Rename(tmpPath, p); err != nil {
return "", nil, err
}
return p, func() {
os.RemoveAll(p)
}, nil
}

View File

@@ -1,117 +0,0 @@
package oci
import (
"context"
"path/filepath"
"strings"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
// MountOpts sets oci spec specific info for mount points
type MountOpts func([]specs.Mount) ([]specs.Mount, error)
//GetMounts returns default required for buildkit
// https://github.com/moby/buildkit/issues/429
func GetMounts(ctx context.Context, mountOpts ...MountOpts) ([]specs.Mount, error) {
mounts := []specs.Mount{
{
Destination: "/proc",
Type: "proc",
Source: "proc",
},
{
Destination: "/dev",
Type: "tmpfs",
Source: "tmpfs",
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
},
{
Destination: "/dev/pts",
Type: "devpts",
Source: "devpts",
Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"},
},
{
Destination: "/dev/shm",
Type: "tmpfs",
Source: "shm",
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
},
{
Destination: "/dev/mqueue",
Type: "mqueue",
Source: "mqueue",
Options: []string{"nosuid", "noexec", "nodev"},
},
{
Destination: "/sys",
Type: "sysfs",
Source: "sysfs",
Options: []string{"nosuid", "noexec", "nodev", "ro"},
},
}
var err error
for _, o := range mountOpts {
mounts, err = o(mounts)
if err != nil {
return nil, err
}
}
return mounts, nil
}
func withROBind(src, dest string) func(m []specs.Mount) ([]specs.Mount, error) {
return func(m []specs.Mount) ([]specs.Mount, error) {
m = append(m, specs.Mount{
Destination: dest,
Type: "bind",
Source: src,
Options: []string{"rbind", "ro"},
})
return m, nil
}
}
func hasPrefix(p, prefixDir string) bool {
prefixDir = filepath.Clean(prefixDir)
if prefixDir == "/" {
return true
}
p = filepath.Clean(p)
return p == prefixDir || strings.HasPrefix(p, prefixDir+"/")
}
func removeMountsWithPrefix(mounts []specs.Mount, prefixDir string) []specs.Mount {
var ret []specs.Mount
for _, m := range mounts {
if !hasPrefix(m.Destination, prefixDir) {
ret = append(ret, m)
}
}
return ret
}
func withProcessMode(processMode ProcessMode) func([]specs.Mount) ([]specs.Mount, error) {
return func(m []specs.Mount) ([]specs.Mount, error) {
switch processMode {
case ProcessSandbox:
// keep the default
case NoProcessSandbox:
m = removeMountsWithPrefix(m, "/proc")
procMount := specs.Mount{
Destination: "/proc",
Type: "bind",
Source: "/proc",
// NOTE: "rbind"+"ro" does not make /proc read-only recursively.
// So we keep maskedPath and readonlyPaths (although not mandatory for rootless mode)
Options: []string{"rbind"},
}
m = append([]specs.Mount{procMount}, m...)
default:
return nil, errors.Errorf("unknown process mode: %v", processMode)
}
return m, nil
}
}

View File

@@ -1,123 +0,0 @@
package oci
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"github.com/docker/docker/pkg/idtools"
"github.com/docker/libnetwork/resolvconf"
"github.com/docker/libnetwork/types"
"github.com/moby/buildkit/util/flightcontrol"
)
var g flightcontrol.Group
var notFirstRun bool
var lastNotEmpty bool
// overridden by tests
var resolvconfGet = resolvconf.Get
type DNSConfig struct {
Nameservers []string
Options []string
SearchDomains []string
}
func GetResolvConf(ctx context.Context, stateDir string, idmap *idtools.IdentityMapping, dns *DNSConfig) (string, error) {
p := filepath.Join(stateDir, "resolv.conf")
_, err := g.Do(ctx, stateDir, func(ctx context.Context) (interface{}, error) {
generate := !notFirstRun
notFirstRun = true
if !generate {
fi, err := os.Stat(p)
if err != nil {
if !os.IsNotExist(err) {
return "", err
}
generate = true
}
if !generate {
fiMain, err := os.Stat(resolvconf.Path())
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
if lastNotEmpty {
generate = true
lastNotEmpty = false
}
} else {
if fi.ModTime().Before(fiMain.ModTime()) {
generate = true
}
}
}
}
if !generate {
return "", nil
}
var dt []byte
f, err := resolvconfGet()
if err != nil {
if !os.IsNotExist(err) {
return "", err
}
} else {
dt = f.Content
}
if dns != nil {
var (
dnsNameservers = resolvconf.GetNameservers(dt, types.IP)
dnsSearchDomains = resolvconf.GetSearchDomains(dt)
dnsOptions = resolvconf.GetOptions(dt)
)
if len(dns.Nameservers) > 0 {
dnsNameservers = dns.Nameservers
}
if len(dns.SearchDomains) > 0 {
dnsSearchDomains = dns.SearchDomains
}
if len(dns.Options) > 0 {
dnsOptions = dns.Options
}
f, err = resolvconf.Build(p+".tmp", dnsNameservers, dnsSearchDomains, dnsOptions)
if err != nil {
return "", err
}
dt = f.Content
}
f, err = resolvconf.FilterResolvDNS(dt, true)
if err != nil {
return "", err
}
tmpPath := p + ".tmp"
if err := ioutil.WriteFile(tmpPath, f.Content, 0644); err != nil {
return "", err
}
if idmap != nil {
root := idmap.RootPair()
if err := os.Chown(tmpPath, root.UID, root.GID); err != nil {
return "", err
}
}
if err := os.Rename(tmpPath, p); err != nil {
return "", err
}
return "", nil
})
if err != nil {
return "", err
}
return p, nil
}

View File

@@ -1,13 +0,0 @@
package oci
// ProcMode configures PID namespaces
type ProcessMode int
const (
// ProcessSandbox unshares pidns and mount procfs.
ProcessSandbox ProcessMode = iota
// NoProcessSandbox uses host pidns and bind-mount procfs.
// Note that NoProcessSandbox allows build containers to kill (and potentially ptrace) an arbitrary process in the BuildKit host namespace.
// NoProcessSandbox should be enabled only when the BuildKit is running in a container as an unprivileged user.
NoProcessSandbox
)

View File

@@ -1,254 +0,0 @@
// +build !windows
package oci
import (
"context"
"path"
"sync"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/contrib/seccomp"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/oci"
"github.com/containerd/continuity/fs"
"github.com/docker/docker/pkg/idtools"
"github.com/mitchellh/hashstructure"
"github.com/moby/buildkit/executor"
"github.com/moby/buildkit/snapshot"
"github.com/moby/buildkit/solver/pb"
"github.com/moby/buildkit/util/entitlements/security"
"github.com/moby/buildkit/util/network"
"github.com/moby/buildkit/util/system"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
)
// Ideally we don't have to import whole containerd just for the default spec
// GenerateSpec generates spec using containerd functionality.
// opts are ignored for s.Process, s.Hostname, and s.Mounts .
func GenerateSpec(ctx context.Context, meta executor.Meta, mounts []executor.Mount, id, resolvConf, hostsFile string, namespace network.Namespace, processMode ProcessMode, idmap *idtools.IdentityMapping, opts ...oci.SpecOpts) (*specs.Spec, func(), error) {
c := &containers.Container{
ID: id,
}
_, ok := namespaces.Namespace(ctx)
if !ok {
ctx = namespaces.WithNamespace(ctx, "buildkit")
}
if meta.SecurityMode == pb.SecurityMode_INSECURE {
opts = append(opts, security.WithInsecureSpec())
} else if system.SeccompSupported() && meta.SecurityMode == pb.SecurityMode_SANDBOX {
opts = append(opts, seccomp.WithDefaultProfile())
}
switch processMode {
case NoProcessSandbox:
// Mount for /proc is replaced in GetMounts()
opts = append(opts,
oci.WithHostNamespace(specs.PIDNamespace))
// TODO(AkihiroSuda): Configure seccomp to disable ptrace (and prctl?) explicitly
}
// Note that containerd.GenerateSpec is namespaced so as to make
// specs.Linux.CgroupsPath namespaced
s, err := oci.GenerateSpec(ctx, nil, c, opts...)
if err != nil {
return nil, nil, err
}
// set the networking information on the spec
namespace.Set(s)
s.Process.Args = meta.Args
s.Process.Env = meta.Env
s.Process.Cwd = meta.Cwd
s.Process.Rlimits = nil // reset open files limit
s.Process.NoNewPrivileges = false // reset nonewprivileges
s.Hostname = "buildkitsandbox"
s.Mounts, err = GetMounts(ctx,
withProcessMode(processMode),
withROBind(resolvConf, "/etc/resolv.conf"),
withROBind(hostsFile, "/etc/hosts"),
)
if err != nil {
return nil, nil, err
}
s.Mounts = append(s.Mounts, specs.Mount{
Destination: "/sys/fs/cgroup",
Type: "cgroup",
Source: "cgroup",
Options: []string{"ro", "nosuid", "noexec", "nodev"},
})
if processMode == NoProcessSandbox {
var maskedPaths []string
for _, s := range s.Linux.MaskedPaths {
if !hasPrefix(s, "/proc") {
maskedPaths = append(maskedPaths, s)
}
}
s.Linux.MaskedPaths = maskedPaths
var readonlyPaths []string
for _, s := range s.Linux.ReadonlyPaths {
if !hasPrefix(s, "/proc") {
readonlyPaths = append(readonlyPaths, s)
}
}
s.Linux.ReadonlyPaths = readonlyPaths
}
if meta.SecurityMode == pb.SecurityMode_INSECURE {
if err = oci.WithWriteableCgroupfs(ctx, nil, c, s); err != nil {
return nil, nil, err
}
if err = oci.WithWriteableSysfs(ctx, nil, c, s); err != nil {
return nil, nil, err
}
}
if idmap != nil {
s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{
Type: specs.UserNamespace,
})
s.Linux.UIDMappings = specMapping(idmap.UIDs())
s.Linux.GIDMappings = specMapping(idmap.GIDs())
}
sm := &submounts{}
var releasers []func() error
releaseAll := func() {
sm.cleanup()
for _, f := range releasers {
f()
}
}
for _, m := range mounts {
if m.Src == nil {
return nil, nil, errors.Errorf("mount %s has no source", m.Dest)
}
mountable, err := m.Src.Mount(ctx, m.Readonly)
if err != nil {
releaseAll()
return nil, nil, errors.Wrapf(err, "failed to mount %s", m.Dest)
}
mounts, release, err := mountable.Mount()
if err != nil {
releaseAll()
return nil, nil, errors.WithStack(err)
}
releasers = append(releasers, release)
for _, mount := range mounts {
mount, err = sm.subMount(mount, m.Selector)
if err != nil {
releaseAll()
return nil, nil, err
}
s.Mounts = append(s.Mounts, specs.Mount{
Destination: m.Dest,
Type: mount.Type,
Source: mount.Source,
Options: mount.Options,
})
}
}
return s, releaseAll, nil
}
type mountRef struct {
mount mount.Mount
unmount func() error
}
type submounts struct {
m map[uint64]mountRef
}
func (s *submounts) subMount(m mount.Mount, subPath string) (mount.Mount, error) {
if path.Join("/", subPath) == "/" {
return m, nil
}
if s.m == nil {
s.m = map[uint64]mountRef{}
}
h, err := hashstructure.Hash(m, nil)
if err != nil {
return mount.Mount{}, nil
}
if mr, ok := s.m[h]; ok {
sm, err := sub(mr.mount, subPath)
if err != nil {
return mount.Mount{}, nil
}
return sm, nil
}
lm := snapshot.LocalMounterWithMounts([]mount.Mount{m})
mp, err := lm.Mount()
if err != nil {
return mount.Mount{}, err
}
opts := []string{"rbind"}
for _, opt := range m.Options {
if opt == "ro" {
opts = append(opts, opt)
}
}
s.m[h] = mountRef{
mount: mount.Mount{
Source: mp,
Type: "bind",
Options: opts,
},
unmount: lm.Unmount,
}
sm, err := sub(s.m[h].mount, subPath)
if err != nil {
return mount.Mount{}, err
}
return sm, nil
}
func (s *submounts) cleanup() {
var wg sync.WaitGroup
wg.Add(len(s.m))
for _, m := range s.m {
func(m mountRef) {
go func() {
m.unmount()
wg.Done()
}()
}(m)
}
wg.Wait()
}
func sub(m mount.Mount, subPath string) (mount.Mount, error) {
src, err := fs.RootPath(m.Source, subPath)
if err != nil {
return mount.Mount{}, err
}
m.Source = src
return m, nil
}
func specMapping(s []idtools.IDMap) []specs.LinuxIDMapping {
var ids []specs.LinuxIDMapping
for _, item := range s {
ids = append(ids, specs.LinuxIDMapping{
HostID: uint32(item.HostID),
ContainerID: uint32(item.ContainerID),
Size: uint32(item.Size),
})
}
return ids
}

View File

@@ -1,99 +0,0 @@
package oci
import (
"context"
"errors"
"os"
"strconv"
"strings"
"github.com/containerd/containerd/containers"
containerdoci "github.com/containerd/containerd/oci"
"github.com/containerd/continuity/fs"
"github.com/opencontainers/runc/libcontainer/user"
"github.com/opencontainers/runtime-spec/specs-go"
)
func GetUser(ctx context.Context, root, username string) (uint32, uint32, []uint32, error) {
// fast path from uid/gid
if uid, gid, err := ParseUIDGID(username); err == nil {
return uid, gid, nil, nil
}
passwdFile, err := openUserFile(root, "/etc/passwd")
if err == nil {
defer passwdFile.Close()
}
groupFile, err := openUserFile(root, "/etc/group")
if err == nil {
defer groupFile.Close()
}
execUser, err := user.GetExecUser(username, nil, passwdFile, groupFile)
if err != nil {
return 0, 0, nil, err
}
var sgids []uint32
for _, g := range execUser.Sgids {
sgids = append(sgids, uint32(g))
}
return uint32(execUser.Uid), uint32(execUser.Gid), sgids, nil
}
// ParseUIDGID takes the fast path to parse UID and GID if and only if they are both provided
func ParseUIDGID(str string) (uid uint32, gid uint32, err error) {
if str == "" {
return 0, 0, nil
}
parts := strings.SplitN(str, ":", 2)
if len(parts) == 1 {
return 0, 0, errors.New("groups ID is not provided")
}
if uid, err = parseUID(parts[0]); err != nil {
return 0, 0, err
}
if gid, err = parseUID(parts[1]); err != nil {
return 0, 0, err
}
return
}
func openUserFile(root, p string) (*os.File, error) {
p, err := fs.RootPath(root, p)
if err != nil {
return nil, err
}
return os.Open(p)
}
func parseUID(str string) (uint32, error) {
if str == "root" {
return 0, nil
}
uid, err := strconv.ParseUint(str, 10, 32)
if err != nil {
return 0, err
}
return uint32(uid), nil
}
// WithUIDGID allows the UID and GID for the Process to be set
// FIXME: This is a temporeray fix for the missing supplementary GIDs from containerd
// once the PR in containerd is merged we should remove this function.
func WithUIDGID(uid, gid uint32, sgids []uint32) containerdoci.SpecOpts {
return func(_ context.Context, _ containerdoci.Client, _ *containers.Container, s *containerdoci.Spec) error {
setProcess(s)
s.Process.User.UID = uid
s.Process.User.GID = gid
s.Process.User.AdditionalGids = sgids
return nil
}
}
// setProcess sets Process to empty if unset
// FIXME: Same on this one. Need to be removed after containerd fix merged
func setProcess(s *containerdoci.Spec) {
if s.Process == nil {
s.Process = &specs.Process{}
}
}

View File

@@ -1,163 +0,0 @@
package security
import (
"context"
"fmt"
"os"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/oci"
"github.com/opencontainers/runc/libcontainer/system"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
)
// WithInsecureSpec sets spec with All capability.
func WithInsecureSpec() oci.SpecOpts {
return func(_ context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error {
addCaps := []string{
"CAP_FSETID",
"CAP_KILL",
"CAP_FOWNER",
"CAP_MKNOD",
"CAP_CHOWN",
"CAP_DAC_OVERRIDE",
"CAP_NET_RAW",
"CAP_SETGID",
"CAP_SETUID",
"CAP_SETPCAP",
"CAP_SETFCAP",
"CAP_NET_BIND_SERVICE",
"CAP_SYS_CHROOT",
"CAP_AUDIT_WRITE",
"CAP_MAC_ADMIN",
"CAP_MAC_OVERRIDE",
"CAP_DAC_READ_SEARCH",
"CAP_SYS_PTRACE",
"CAP_SYS_MODULE",
"CAP_SYSLOG",
"CAP_SYS_RAWIO",
"CAP_SYS_ADMIN",
"CAP_LINUX_IMMUTABLE",
"CAP_SYS_BOOT",
"CAP_SYS_NICE",
"CAP_SYS_PACCT",
"CAP_SYS_TTY_CONFIG",
"CAP_SYS_TIME",
"CAP_WAKE_ALARM",
"CAP_AUDIT_READ",
"CAP_AUDIT_CONTROL",
"CAP_SYS_RESOURCE",
"CAP_BLOCK_SUSPEND",
"CAP_IPC_LOCK",
"CAP_IPC_OWNER",
"CAP_LEASE",
"CAP_NET_ADMIN",
"CAP_NET_BROADCAST",
}
for _, cap := range addCaps {
s.Process.Capabilities.Bounding = append(s.Process.Capabilities.Bounding, cap)
s.Process.Capabilities.Ambient = append(s.Process.Capabilities.Ambient, cap)
s.Process.Capabilities.Effective = append(s.Process.Capabilities.Effective, cap)
s.Process.Capabilities.Inheritable = append(s.Process.Capabilities.Inheritable, cap)
s.Process.Capabilities.Permitted = append(s.Process.Capabilities.Permitted, cap)
}
s.Linux.ReadonlyPaths = []string{}
s.Linux.MaskedPaths = []string{}
s.Process.ApparmorProfile = ""
s.Linux.Resources.Devices = []specs.LinuxDeviceCgroup{
{
Allow: true,
Type: "c",
Access: "rwm",
},
{
Allow: true,
Type: "b",
Access: "rwm",
},
}
if !system.RunningInUserNS() {
// Devices automatically mounted on insecure mode
s.Linux.Devices = append(s.Linux.Devices, []specs.LinuxDevice{
// Writes to this come out as printk's, reads export the buffered printk records. (dmesg)
{
Path: "/dev/kmsg",
Type: "c",
Major: 1,
Minor: 11,
},
// Cuse (character device in user-space)
{
Path: "/dev/cuse",
Type: "c",
Major: 10,
Minor: 203,
},
// Fuse (virtual filesystem in user-space)
{
Path: "/dev/fuse",
Type: "c",
Major: 10,
Minor: 229,
},
// Kernel-based virtual machine (hardware virtualization extensions)
{
Path: "/dev/kvm",
Type: "c",
Major: 10,
Minor: 232,
},
// TAP/TUN network device
{
Path: "/dev/net/tun",
Type: "c",
Major: 10,
Minor: 200,
},
// Loopback control device
{
Path: "/dev/loop-control",
Type: "c",
Major: 10,
Minor: 237,
},
}...)
loopID, err := getFreeLoopID()
if err != nil {
logrus.Debugf("failed to get next free loop device: %v", err)
}
for i := 0; i <= loopID+7; i++ {
s.Linux.Devices = append(s.Linux.Devices, specs.LinuxDevice{
Path: fmt.Sprintf("/dev/loop%d", i),
Type: "b",
Major: 7,
Minor: int64(i),
})
}
}
return nil
}
}
func getFreeLoopID() (int, error) {
fd, err := os.OpenFile("/dev/loop-control", os.O_RDWR, 0644)
if err != nil {
return 0, err
}
defer fd.Close()
const _LOOP_CTL_GET_FREE = 0x4C82
r1, _, uerr := unix.Syscall(unix.SYS_IOCTL, fd.Fd(), _LOOP_CTL_GET_FREE, 0)
if uerr == 0 {
return int(r1), nil
}
return 0, errors.Errorf("error getting free loop device: %v", uerr)
}

View File

@@ -1,28 +0,0 @@
package network
import (
"github.com/containerd/containerd/oci"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func NewHostProvider() Provider {
return &host{}
}
type host struct {
}
func (h *host) New() (Namespace, error) {
return &hostNS{}, nil
}
type hostNS struct {
}
func (h *hostNS) Set(s *specs.Spec) {
oci.WithHostNamespace(specs.NetworkNamespace)(nil, nil, nil, s)
}
func (h *hostNS) Close() error {
return nil
}

View File

@@ -1,19 +0,0 @@
package network
import (
"io"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
// Provider interface for Network
type Provider interface {
New() (Namespace, error)
}
// Namespace of network for workers
type Namespace interface {
io.Closer
// Set the namespace on the spec
Set(*specs.Spec)
}

View File

@@ -1,26 +0,0 @@
package network
import (
specs "github.com/opencontainers/runtime-spec/specs-go"
)
func NewNoneProvider() Provider {
return &none{}
}
type none struct {
}
func (h *none) New() (Namespace, error) {
return &noneNS{}, nil
}
type noneNS struct {
}
func (h *noneNS) Set(s *specs.Spec) {
}
func (h *noneNS) Close() error {
return nil
}