@@ -40,6 +40,8 @@ import (
40
40
// threadNsPath is the /proc path to the current netns handle for the current thread
41
41
const threadNsPath = "/proc/thread-self/ns/net"
42
42
43
+ var errNoFreeName = errors .New ("failed to find free netns path name" )
44
+
43
45
// GetNSRunDir returns the dir of where to create the netNS. When running
44
46
// rootless, it needs to be at a location writable by user.
45
47
func GetNSRunDir () (string , error ) {
@@ -60,14 +62,26 @@ func NewNSAtPath(nsPath string) (ns.NetNS, error) {
60
62
// NewNS creates a new persistent (bind-mounted) network namespace and returns
61
63
// an object representing that namespace, without switching to it.
62
64
func NewNS () (ns.NetNS , error ) {
65
+ nsRunDir , err := GetNSRunDir ()
66
+ if err != nil {
67
+ return nil , err
68
+ }
69
+
70
+ // Create the directory for mounting network namespaces
71
+ // This needs to be a shared mountpoint in case it is mounted in to
72
+ // other namespaces (containers)
73
+ err = makeNetnsDir (nsRunDir )
74
+ if err != nil {
75
+ return nil , err
76
+ }
77
+
63
78
for i := 0 ; i < 10000 ; i ++ {
64
- b := make ([]byte , 16 )
65
- _ , err := rand .Reader .Read (b )
79
+ nsName , err := getRandomNetnsName ()
66
80
if err != nil {
67
- return nil , fmt . Errorf ( "failed to generate random netns name: %v" , err )
81
+ return nil , err
68
82
}
69
- nsName := fmt . Sprintf ( "netns-%x-%x-%x-%x-%x" , b [ 0 : 4 ], b [ 4 : 6 ], b [ 6 : 8 ], b [ 8 : 10 ], b [ 10 :] )
70
- ns , err := NewNSWithName ( nsName )
83
+ nsPath := path . Join ( nsRunDir , nsName )
84
+ ns , err := newNSPath ( nsPath )
71
85
if err == nil {
72
86
return ns , nil
73
87
}
@@ -77,62 +91,128 @@ func NewNS() (ns.NetNS, error) {
77
91
}
78
92
return nil , err
79
93
}
80
- return nil , errors . New ( "failed to find free netns path name" )
94
+ return nil , errNoFreeName
81
95
}
82
96
83
- // NewNSWithName creates a new persistent (bind-mounted) network namespace and returns
84
- // an object representing that namespace, without switching to it.
85
- func NewNSWithName (name string ) (ns.NetNS , error ) {
97
+ // NewNSFrom creates a persistent (bind-mounted) network namespace from the
98
+ // given netns path, i.e. /proc/<pid>/ns/net, and returns the new full path to
99
+ // the bind mounted file in the netns run dir.
100
+ func NewNSFrom (fromNetns string ) (string , error ) {
86
101
nsRunDir , err := GetNSRunDir ()
87
102
if err != nil {
88
- return nil , err
103
+ return "" , err
89
104
}
90
105
91
- // Create the directory for mounting network namespaces
92
- // This needs to be a shared mountpoint in case it is mounted in to
93
- // other namespaces (containers)
94
- err = os .MkdirAll (nsRunDir , 0o755 )
106
+ err = makeNetnsDir (nsRunDir )
95
107
if err != nil {
96
- return nil , err
108
+ return "" , err
97
109
}
98
110
99
- // Remount the namespace directory shared. This will fail if it is not
100
- // already a mountpoint, so bind-mount it on to itself to "upgrade" it
101
- // to a mountpoint.
102
- err = unix .Mount ("" , nsRunDir , "none" , unix .MS_SHARED | unix .MS_REC , "" )
103
- if err != nil {
104
- if err != unix .EINVAL {
105
- return nil , fmt .Errorf ("mount --make-rshared %s failed: %q" , nsRunDir , err )
111
+ for i := 0 ; i < 10000 ; i ++ {
112
+ nsName , err := getRandomNetnsName ()
113
+ if err != nil {
114
+ return "" , err
106
115
}
116
+ nsPath := filepath .Join (nsRunDir , nsName )
107
117
108
- // Recursively remount /run/netns on itself. The recursive flag is
109
- // so that any existing netns bindmounts are carried over.
110
- err = unix .Mount (nsRunDir , nsRunDir , "none" , unix .MS_BIND | unix .MS_REC , "" )
118
+ // create an empty file to use as at the mount point
119
+ err = createNetnsFile (nsPath )
111
120
if err != nil {
112
- return nil , fmt .Errorf ("mount --rbind %s %s failed: %q" , nsRunDir , nsRunDir , err )
121
+ // retry when the name already exists
122
+ if errors .Is (err , os .ErrExist ) {
123
+ continue
124
+ }
125
+ return "" , err
113
126
}
114
127
115
- // Now we can make it shared
116
- err = unix .Mount ("" , nsRunDir , "none" , unix .MS_SHARED | unix .MS_REC , "" )
128
+ err = unix .Mount (fromNetns , nsPath , "none" , unix .MS_BIND | unix .MS_SHARED | unix .MS_REC , "" )
117
129
if err != nil {
118
- return nil , fmt .Errorf ("mount --make-rshared %s failed: %q" , nsRunDir , err )
130
+ // Do not leak the ns on errors
131
+ _ = os .RemoveAll (nsPath )
132
+ return "" , fmt .Errorf ("failed to bind mount ns at %s: %v" , nsPath , err )
119
133
}
134
+ return nsPath , nil
120
135
}
121
136
122
- nsPath := path .Join (nsRunDir , name )
123
- return newNSPath (nsPath )
137
+ return "" , errNoFreeName
124
138
}
125
139
126
- func newNSPath ( nsPath string ) (ns. NetNS , error ) {
127
- // create an empty file at the mount point
128
- mountPointFd , err := os . OpenFile ( nsPath , os . O_RDWR | os . O_CREATE | os . O_EXCL , 0o600 )
140
+ func getRandomNetnsName ( ) (string , error ) {
141
+ b := make ([] byte , 16 )
142
+ _ , err := rand . Reader . Read ( b )
129
143
if err != nil {
130
- return nil , err
144
+ return "" , fmt . Errorf ( "failed to generate random netns name: %v" , err )
131
145
}
132
- if err := mountPointFd .Close (); err != nil {
133
- return nil , err
146
+ return fmt .Sprintf ("netns-%x-%x-%x-%x-%x" , b [0 :4 ], b [4 :6 ], b [6 :8 ], b [8 :10 ], b [10 :]), nil
147
+ }
148
+
149
+ func makeNetnsDir (nsRunDir string ) error {
150
+ err := os .MkdirAll (nsRunDir , 0o755 )
151
+ if err != nil {
152
+ return err
153
+ }
154
+ // Important, the bind mount setup is racy if two process try to set it up in parallel.
155
+ // This can have very bad consequences because we end up with two duplicated mounts
156
+ // for the netns file that then might have a different parent mounts.
157
+ // Also because as root netns dir is also created by ip netns we should not race against them.
158
+ // Use a lock on the netns dir like they do, compare the iproute2 ip netns add code.
159
+ // https://github.com/iproute2/iproute2/blob/8b9d9ea42759c91d950356ca43930a975d0c352b/ip/ipnetns.c#L806-L815
160
+
161
+ dirFD , err := unix .Open (nsRunDir , unix .O_RDONLY | unix .O_DIRECTORY | unix .O_CLOEXEC , 0 )
162
+ if err != nil {
163
+ return & os.PathError {Op : "open" , Path : nsRunDir , Err : err }
164
+ }
165
+ // closing the fd will also unlock so we do not have to call flock(fd,LOCK_UN)
166
+ defer unix .Close (dirFD )
167
+
168
+ err = unix .Flock (dirFD , unix .LOCK_EX )
169
+ if err != nil {
170
+ return fmt .Errorf ("failed to lock %s dir: %w" , nsRunDir , err )
171
+ }
172
+
173
+ // Remount the namespace directory shared. This will fail with EINVAL
174
+ // if it is not already a mountpoint, so bind-mount it on to itself
175
+ // to "upgrade" it to a mountpoint.
176
+ err = unix .Mount ("" , nsRunDir , "none" , unix .MS_SHARED | unix .MS_REC , "" )
177
+ if err == nil {
178
+ return nil
179
+ }
180
+ if err != unix .EINVAL {
181
+ return fmt .Errorf ("mount --make-rshared %s failed: %q" , nsRunDir , err )
182
+ }
183
+
184
+ // Recursively remount /run/netns on itself. The recursive flag is
185
+ // so that any existing netns bindmounts are carried over.
186
+ err = unix .Mount (nsRunDir , nsRunDir , "none" , unix .MS_BIND | unix .MS_REC , "" )
187
+ if err != nil {
188
+ return fmt .Errorf ("mount --rbind %s %s failed: %q" , nsRunDir , nsRunDir , err )
134
189
}
135
190
191
+ // Now we can make it shared
192
+ err = unix .Mount ("" , nsRunDir , "none" , unix .MS_SHARED | unix .MS_REC , "" )
193
+ if err != nil {
194
+ return fmt .Errorf ("mount --make-rshared %s failed: %q" , nsRunDir , err )
195
+ }
196
+
197
+ return nil
198
+ }
199
+
200
+ // createNetnsFile created the file with O_EXCL to ensure there are no conflicts with others
201
+ // Callers should check for ErrExist and loop over it to find a free file.
202
+ func createNetnsFile (path string ) error {
203
+ mountPointFd , err := os .OpenFile (path , os .O_RDWR | os .O_CREATE | os .O_EXCL , 0o600 )
204
+ if err != nil {
205
+ return err
206
+ }
207
+ return mountPointFd .Close ()
208
+ }
209
+
210
+ func newNSPath (nsPath string ) (ns.NetNS , error ) {
211
+ // create an empty file to use as at the mount point
212
+ err := createNetnsFile (nsPath )
213
+ if err != nil {
214
+ return nil , err
215
+ }
136
216
// Ensure the mount point is cleaned up on errors; if the namespace
137
217
// was successfully mounted this will have no effect because the file
138
218
// is in-use
0 commit comments