Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions itamae/cookbooks/systemd-container/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package 'systemd-container'
package 'mmdebstrap'

directory '/var/lib/machines' do
owner 'root'
group 'root'
mode '0700'
end

directory '/etc/systemd/nspawn' do
owner 'root'
group 'root'
mode '0755'
end
1 change: 1 addition & 0 deletions itamae/cookbooks/tpm/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package 'tpm2-tools'
53 changes: 53 additions & 0 deletions itamae/hosts/wire-01.venue.rubykaigi.net/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
node.reverse_merge!(
systemd_networkd: {
migrate_netplan: false,
},
wire: {
interfaces: {
loopback: {
address4: %w[10.33.0.41],
address6: %w[2001:df0:8500:ca00::41],
},
management: {
name: 'me0',
path: '*14.0-usb-0:*:1.0',
duid: '00:00:ba:2c:33:00:41',
},
overlay: {
name: 'enp3s0',
ipv6_token: 'static:::8888:aaaa:0:1',
},
downstream: {
name: 'enp2s0',
# local_as: 65026,
# peer_as: 65030,
# link4: {
# local: '10.33.22.64',
# peer: '10.33.22.65',
# },
# link6: {
# local: '2001:df0:8500:ca22:64::a',
# peer: '2001:df0:8500:ca22:64::b',
# },
},
},
tunnels: {
wg_wire03: {
listen_port: 8703,
peer_endpoint: 'rknet-wire-03.i.open.ad.jp:8701',
peer_public_key: 'TADiMxPz1fqm5LjWH44/Q1kpt9XkAyl+umB8OEJgoGA=', # dummy
link4: {
local: '10.33.22.90',
peer: '10.33.22.91',
},
link6: {
local: '2001:df0:8500:ca22:90::a',
peer: '2001:df0:8500:ca22:90::b',
},
},
},
},
)

include_role 'wire'
include_cookbook 'bluetooth-getty'
75 changes: 75 additions & 0 deletions itamae/roles/wire/default.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
node.reverse_merge!(
systemd_networkd: {
manage_foreign_routes: false,
},
wire: {
},
bird: {
router_id: [*node.dig(:wire, :interfaces, :loopback).fetch(:address4)].first,
},
)

include_role 'base'
include_cookbook 'ruby'

include_cookbook 'cpufreq'
#include_cookbook 'nftables'

package 'wireguard-tools'


%w[
00-lo.network
].each do |fname|
template "/etc/systemd/network/#{fname}" do
owner 'root'
group 'root'
mode '0644'
notifies :restart, 'service[systemd-networkd]'
end
end

if node.dig(:wire, :interfaces, :management)
%w[
00-management.network
00-management.link
].each do |fname|
template "/etc/systemd/network/#{fname}" do
owner 'root'
group 'root'
mode '0644'
notifies :restart, 'service[systemd-networkd]'
end
end
end

file "/etc/wire.json" do
content "#{JSON.pretty_generate(node[:wire])}\n"
owner 'root'
group 'root'
mode '0644'
end

include_recipe './key.rb'
include_recipe './overlay.rb'
include_recipe './wireguard.rb'

service 'systemd-nspawn@overlay.service' do
action [:enable, :start]
end



# template '/etc/nftables/plat.conf' do
# owner 'root'
# group 'root'
# mode '0644'
# notifies :reload, 'service[nftables]'
# end
#
# template '/etc/bird/bird.conf.d/plat.conf' do
# owner 'root'
# group 'bird'
# mode '0644'
# notifies :reload, 'service[bird]'
# end
6 changes: 6 additions & 0 deletions itamae/roles/wire/key.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
include_cookbook 'tpm'

execute 'wg genkey' do
command "touch /etc/wire.key; chmod 600 /etc/wire.key; wg genkey|sudo systemd-creds encrypt --name network.wireguard.private.default --with-key 'host+tpm2' - /etc/wire.key"
not_if 'test -e /etc/wire.key'
end
70 changes: 70 additions & 0 deletions itamae/roles/wire/overlay.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
include_cookbook 'systemd-container'

execute 'mmdebstrap for overlay' do
command <<~EOF
set -xe
mmdebstrap \
--include=dbus,libpam-systemd,libnss-systemd,systemd,systemd-resolved,iproute2,iputils-ping,curl,wireguard-tools \
--dpkgopt='path-exclude=/usr/share/man/*' \
--dpkgopt='path-include=/usr/share/man/man[1-9]/*' \
--dpkgopt='path-exclude=/usr/share/locale/*' \
--dpkgopt='path-include=/usr/share/locale/locale.alias' \
--dpkgopt='path-exclude=/usr/share/doc/*' \
--dpkgopt='path-include=/usr/share/doc/*/copyright' \
--dpkgopt='path-include=/usr/share/doc/*/changelog.Debian.*' \
--variant=essential "$(lsb_release -sc)" /root/overlay.tar
importctl import-tar --class=machine /root/overlay.tar overlay
EOF
not_if 'test -e /var/lib/machines/overlay/etc/hostname'
end

template '/etc/systemd/nspawn/overlay.nspawn' do
owner 'root'
group 'root'
mode '0644'
end

directory '/etc/systemd/system/systemd-nspawn@overlay.service.d' do
owner 'root'
group 'root'
mode '0755'
end

template '/etc/systemd/system/systemd-nspawn@overlay.service.d/dropin.conf' do
owner 'root'
group 'root'
mode '0644'
notifies :run, 'execute[systemctl daemon-reload]', :immediately
end

directory '/var/lib/machines/overlay/etc/systemd/network' do
owner 'root'
group 'root'
mode '0755'
end

template '/var/lib/machines/overlay/etc/systemd/network/00-overlay.network' do
owner 'root'
group 'root'
mode '0644'
end

execute 'chroot /var/lib/machines/overlay systemctl enable systemd-networkd.service' do
not_if 'chroot /var/lib/machines/overlay systemctl is-enabled systemd-networkd.service'
end

execute 'chroot /var/lib/machines/overlay systemctl enable systemd-resolved.service' do
not_if 'chroot /var/lib/machines/overlay systemctl is-enabled systemd-resolved.service'
end

execute 'chroot /var/lib/machines/overlay systemctl enable systemd-networkd-wait-online.service' do
not_if 'chroot /var/lib/machines/overlay systemctl is-enabled systemd-networkd-wait-online.service'
end

execute 'chroot /var/lib/machines/overlay systemctl enable network-online.target' do
not_if 'chroot /var/lib/machines/overlay systemctl is-enabled network-online.target'
end

service 'systemd-nspawn@overlay.service' do
action [:enable, :start]
end
14 changes: 14 additions & 0 deletions itamae/roles/wire/templates/etc/systemd/network/00-lo.network
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<%-
addrs4 = node.dig(:wire, :interfaces, :loopback).fetch(:address4)
addrs6 = node.dig(:wire, :interfaces, :loopback).fetch(:address6)
-%>
[Match]
Name=lo

[Network]
<%- addrs4.each do |addr| -%>
Address=<%= addr %>/32
<%- end -%>
<%- addrs6.each do |addr| -%>
Address=<%= addr %>/128
<%- end -%>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<%- iface = node.dig(:wire, :interfaces, :management) -%>
[Match]
Path=<%= iface.fetch(:path) %>

[Link]
Name=<%= iface.fetch(:name) %>
AlternativeNamesPolicy=mac
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<%- iface = node.dig(:wire, :interfaces, :management) -%>
[Match]
Name=<%= iface.fetch(:name) %>

[Network]
IPForward=no
IPv6AcceptRA=yes
DHCP=ipv4
LLDP=yes
EmitLLDP=yes
LinkLocalAddressing=yes

[DHCPv4]
SendHostname=yes
UseDNS=yes
UseMTU=yes
ClientIdentifier=duid
DUIDType=vendor
DUIDRawData=<%= iface.fetch(:duid) %>


[IPv6AcceptRA]
UseDNS=yes
UseMTU=yes
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<%- iface = @attr -%>
<%- link4 = iface.fetch(:link4) -%>
<%- link6 = iface.fetch(:link6) -%>
[Match]
Name=<%= @name %>

[Network]
IPv4Forwarding=yes
IPv6Forwarding=yes
LLDP=yes
EmitLLDP=yes

[Address]
Address=<%= link4.fetch(:local) %>/32
Peer=<%= link4.fetch(:peer) %>/32

[Address]
Address=<%= link6.fetch(:local) %>/128
Peer=<%= link6.fetch(:peer) %>/128
11 changes: 11 additions & 0 deletions itamae/roles/wire/templates/etc/systemd/nspawn/overlay.nspawn
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[Exec]
Hostname=overlay
LinkJournal=try-host
PrivateUsers=no

[Files]
ReadOnly=yes

[Network]
Private=yes
Interface=<%= node.dig(:wire, :interfaces, :overlay).fetch(:name) %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=rk-move-overlay-wg-iface
After=systemd-nspawn@overlay.service

[Service]
Type=oneshot
ExecStart=/usr/bin/rk-move-overlay-wg-iface

[Install]
WantedBy=systemd-nspawn@overlay.service
Comment on lines +1 to +10
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The rk-move-overlay-wg-iface.service does not re-run on container restart, which will break WireGuard tunnels because the interfaces are not moved to the host namespace.
Severity: HIGH

Suggested Fix

To ensure the service's lifecycle is tied to the container's, add RemainAfterExit=yes to the [Service] section and PartOf=systemd-nspawn@overlay.service to the [Install] section of the service file. This will cause the service to be stopped and restarted along with the container.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location:
itamae/roles/wire/templates/etc/systemd/system/rk-move-overlay-wg-iface.service#L1-L10

Potential issue: The systemd service `rk-move-overlay-wg-iface.service` is configured as
`Type=oneshot` but lacks the `RemainAfterExit=yes` directive and a proper lifecycle
dependency like `PartOf=`. As a result, the service runs only once when the
`systemd-nspawn@overlay.service` container initially starts. If the container restarts
for any reason (e.g., a crash or update), this service will not be re-triggered. This
causes newly created WireGuard interfaces to remain within the container's network
namespace instead of being moved to the host, rendering the WireGuard tunnels
non-functional until a manual intervention or a full host reboot occurs.

Did we get this right? 👍 / 👎 to inform future reviews.

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[Service]
ExecStart=
ExecStart=/usr/bin/systemd-nspawn --quiet --keep-unit --boot --link-journal=try-guest --network-veth -U --settings=override --machine=%i --load-credential=network.wireguard.private.default:%d/network.wireguard.private.default
LoadCredentialEncrypted=network.wireguard.private.default:/etc/wire.key
43 changes: 43 additions & 0 deletions itamae/roles/wire/templates/usr/bin/rk-move-overlay-wg-iface
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env ruby
require 'fileutils'
require 'json'

config = JSON.parse(File.read('/etc/wire.json'))
tunnel_ifaces = config.fetch('tunnels', {}).keys.sort

puts "Waiting for overlay container to be online"
10.times do |i|
if system(*%w(systemd-run --machine overlay --wait --quiet --pipe --collect id))
break
else
sleep 2
end
end
Comment on lines +9 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The retry loop in rk-move-overlay-wg-iface fails silently if the container never comes online, leading to a less informative error message from a subsequent command.
Severity: MEDIUM

Suggested Fix

After the 10.times loop, add a check to verify that the container is actually online. If it is not, raise an explicit exception with a clear error message, such as 'Container failed to come online after 10 attempts'.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: itamae/roles/wire/templates/usr/bin/rk-move-overlay-wg-iface#L9-L15

Potential issue: The script `rk-move-overlay-wg-iface` contains a retry loop that
attempts to connect to the 'overlay' container 10 times. If all attempts fail, the loop
exits silently without raising an error. A subsequent command on line 17 then fails, but
its error message (`Failed to connect to machine 'overlay'`) is less specific than an
explicit error about the container failing to become available after all retries. This
silent failure of the loop degrades diagnostic information, making it harder to debug
why the container failed to start.

Did we get this right? 👍 / 👎 to inform future reviews.

puts "Waiting for overlay network to be online"
system(*%w(systemd-run --machine overlay --wait --quiet --pipe --collect /lib/systemd/systemd-networkd-wait-online --ipv6 -o routable --dns), exception: true)

# Reconfigure tunnels to populate Endpoint= where DNS name is specified
tunnel_ifaces.each do |iface|
puts "Reconfiguring #{iface} in overlay"
system(*%w(systemd-run --machine overlay --wait --quiet --pipe --collect networkctl reconfigure), iface)
end

leader = IO.popen(%w(machinectl show -p Leader --value overlay), 'r', &:read)
puts "Leader PID: #{leader.inspect}"
leader_i = leader.to_i
raise "Failed to get leader PID" if leader_i <= 0

FileUtils.mkdir_p '/var/run/netns'
FileUtils.rm_f '/var/run/netns/overlay' rescue nil
FileUtils.ln_s "/proc/#{leader_i}/ns/net", "/var/run/netns/overlay"

system(*%w(ip netns exec overlay ip link), exception: true)

tunnel_ifaces.each do |iface|
if File.exist?(File.join("/", "sys", "class", "net", iface))
puts "Interface #{iface} already exists"
system(*%w(ip link delete ), iface, exception: true)
end
puts "Acquiring #{iface}"
system(*%w(ip netns exec overlay ip link set), iface, *%w(netns 1), exception: true)
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[Match]
Name=<%= node.dig(:wire, :interfaces, :overlay).fetch(:name) %>

[Link]
RequiredForOnline=routable

[Network]
DHCP=ipv6
IPv6AcceptRA=yes
IPv4Forwarding=off
IPv6Forwarding=off

<%= node.fetch(:wire).fetch(:tunnels, {}).each_key.map { |name| "Tunnel=#{name}" }.join("\n") %>

[IPv6AcceptRA]
Token=<%= node.dig(:wire, :interfaces, :overlay).fetch(:ipv6_token) %>

[DHCPv6]
UseDNS=yes
Loading