Class: OsCtld::ContainerControl::TransientNetwork
- Inherits:
-
Object
- Object
- OsCtld::ContainerControl::TransientNetwork
- Defined in:
- lib/osctld/container_control/transient_network.rb
Overview
A private one-shot channel inherited by exactly one transient helper. The helper can signal readiness, but cannot supply a PID or configuration.
Constant Summary collapse
- READY_TIMEOUT =
Bound startup only. User commands may legitimately run indefinitely after the private handshake has completed.
60
Instance Attribute Summary collapse
-
#runner_socket ⇒ Object
readonly
Returns the value of attribute runner_socket.
Instance Method Summary collapse
- #apply(identity, runner_identity) ⇒ Object protected
-
#authenticate_ancestry!(identity, runner_identity) ⇒ Object
protected
Retain every intermediate /proc entry and pidfd.
- #authenticate_namespaces!(identity) ⇒ Object protected
- #close ⇒ Object
-
#initialize(ct, net_config = nil) ⇒ TransientNetwork
constructor
A new instance of TransientNetwork.
- #parent_pid(identity) ⇒ Object protected
- #read_ready(runner_identity) ⇒ Object protected
- #reply(**result) ⇒ Object protected
- #same_namespace?(left, right) ⇒ Boolean protected
- #serve(runner_identity) ⇒ Object
Constructor Details
#initialize(ct, net_config = nil) ⇒ TransientNetwork
Returns a new instance of TransientNetwork.
19 20 21 22 23 |
# File 'lib/osctld/container_control/transient_network.rb', line 19 def initialize(ct, net_config = nil) @ct = ct @net_config = net_config @server_socket, @runner_socket = UNIXSocket.pair end |
Instance Attribute Details
#runner_socket ⇒ Object (readonly)
Returns the value of attribute runner_socket.
17 18 19 |
# File 'lib/osctld/container_control/transient_network.rb', line 17 def runner_socket @runner_socket end |
Instance Method Details
#apply(identity, runner_identity) ⇒ Object (protected)
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'lib/osctld/container_control/transient_network.rb', line 157 def apply(identity, runner_identity) pid = SwitchUser.fork(keep_fds: identity.files + runner_identity.files) do identity.authenticate!(cgroup_path: @ct.cgroup_path) authenticate_ancestry!(identity, runner_identity) authenticate_namespaces!(identity) sys = OsCtl::Lib::Sys.new Process.groups = [] # Enter with only the container's authority, not host CAP_NET_ADMIN. # pidfd setns also installs coupled syslog/tracing on supported kernels. sys.setns_io(identity.pidfd, OsCtl::Lib::Sys::CLONE_NEWUSER | OsCtl::Lib::Sys::CLONE_NEWNET) %i[user net].each do |name| File.open("/proc/self/ns/#{name}") do |current| unless same_namespace?(current, identity.namespace(name)) raise Errno::ESTALE, 'transient namespaces changed before attachment' end end end raise Errno::ESRCH, 'transient init exited' unless identity.alive? sys.setresgid(0, 0, 0) sys.setresuid(0, 0, 0) @net_config.setup exit!(true) rescue StandardError => e warn "transient network setup: #{e.(highlight: false)}" exit! end _, status = Process.wait2(pid) raise 'transient network child failed' unless status.success? end |
#authenticate_ancestry!(identity, runner_identity) ⇒ Object (protected)
Retain every intermediate /proc entry and pidfd. A process can reparent during exit, so recheck each relation after reaching the pinned helper.
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 |
# File 'lib/osctld/container_control/transient_network.rb', line 93 def authenticate_ancestry!(identity, runner_identity) chain = [identity] relations = [] current = identity loop do raise Errno::ESRCH, 'transient helper exited' unless runner_identity.alive? raise Errno::ESRCH, 'transient process exited' unless current.alive? parent = parent_pid(current) relations << [current, parent] break if parent == runner_identity.pid if parent <= 1 || chain.any? { |entry| entry.pid == parent } raise Errno::EXDEV, 'LXC init is not descended from the transient helper' end current = ProcessIdentity.open(parent) chain << current end relations.each do |entry, parent| unless entry.alive? && parent_pid(entry) == parent raise Errno::ESTALE, 'transient process ancestry changed' end end raise Errno::ESRCH, 'transient helper exited' unless runner_identity.alive? ensure chain&.drop(1)&.each(&:close) end |
#authenticate_namespaces!(identity) ⇒ Object (protected)
134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/osctld/container_control/transient_network.rb', line 134 def authenticate_namespaces!(identity) userns = identity.namespace(:user) File.open('/proc/self/ns/user') do |host_userns| raise Errno::EPERM, 'transient init is in the host user namespace' if same_namespace?(userns, host_userns) end owner = OsCtl::Lib::Sys.new.namespace_userns(identity.namespace(:net)) unless same_namespace?(owner, userns) raise Errno::EXDEV, 'network namespace belongs to a different user namespace' end %i[uid gid].each do |kind| expected = @ct.user.public_send(:"#{kind}_map").map(&:to_a).sort actual = identity.read_proc_file("#{kind}_map").lines.map { |line| line.split.map { |v| Integer(v) } }.sort raise Errno::EXDEV, 'transient init has a foreign ID mapping' unless actual == expected end ns_pids = identity.read_proc_file('status').lines.find { |line| line.start_with?('NSpid:') }&.split unless ns_pids && ns_pids.length > 2 && ns_pids.last == '1' raise Errno::EXDEV, 'transient process is not a container PID 1' end ensure owner&.close end |
#close ⇒ Object
55 56 57 |
# File 'lib/osctld/container_control/transient_network.rb', line 55 def close [@server_socket, @runner_socket].each { |io| io.close unless io.closed? } end |
#parent_pid(identity) ⇒ Object (protected)
124 125 126 |
# File 'lib/osctld/container_control/transient_network.rb', line 124 def parent_pid(identity) Integer(identity.read_proc_file('stat').rpartition(') ').last.split.fetch(1)) end |
#read_ready(runner_identity) ⇒ Object (protected)
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 |
# File 'lib/osctld/container_control/transient_network.rb', line 61 def read_ready(runner_identity) expected = "ready\n" received = '' deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + READY_TIMEOUT while received.bytesize < expected.bytesize raise Errno::ESRCH, 'transient helper exited' unless runner_identity.alive? remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC) raise Errno::ETIMEDOUT, 'transient helper readiness timed out' if remaining <= 0 chunk = @server_socket.read_nonblock(expected.bytesize - received.bytesize, exception: false) case chunk when :wait_readable @server_socket.wait_readable([remaining, 0.1].min) when nil raise EOFError, 'transient helper closed readiness channel' else received << chunk raise 'invalid transient network request' unless expected.start_with?(received) end end end |
#reply(**result) ⇒ Object (protected)
85 86 87 88 89 |
# File 'lib/osctld/container_control/transient_network.rb', line 85 def reply(**result) @server_socket.write("#{result.to_json}\n") rescue IOError, SystemCallError nil end |
#same_namespace?(left, right) ⇒ Boolean (protected)
128 129 130 131 132 |
# File 'lib/osctld/container_control/transient_network.rb', line 128 def same_namespace?(left, right) a = left.stat b = right.stat a.dev == b.dev && a.ino == b.ino end |
#serve(runner_identity) ⇒ Object
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
# File 'lib/osctld/container_control/transient_network.rb', line 25 def serve(runner_identity) read_ready(runner_identity) raise Errno::ESRCH, 'transient helper exited' unless runner_identity.alive? state = ContainerControl::Commands::State.run!(@ct, force: true) unless state.state == :running && state.init_pid.to_i > 1 raise Errno::ESRCH, 'transient LXC init is not running' end ProcessIdentity.open(state.init_pid, namespaces: %i[net user]) do |identity| identity.authenticate!(cgroup_path: @ct.cgroup_path) authenticate_ancestry!(identity, runner_identity) authenticate_namespaces!(identity) # Routed IPv6 gateways belong to host veths created during LXC start. # Snapshot daemon-owned configuration only after authenticated readiness. @net_config ||= NetConfig.create(@ct) apply(identity, runner_identity) end reply(status: true) true rescue StandardError => e @ct.log(:warn, "transient network setup: #{e.(highlight: false)}") if @ct.respond_to?(:log) reply(status: false, message: "transient network setup failed (#{e.class})") false ensure @server_socket.close unless @server_socket.closed? end |