Class: OsCtld::ContainerControl::TransientNetwork

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

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.full_message(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)

Returns:

  • (Boolean)


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.full_message(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