Skip to content

Commit

Permalink
F #68: Manage multiple instances and reboots
Browse files Browse the repository at this point in the history
Configuration (server-side) is stored in virtual router VMs using:

- ONEAPP_VNF_WG_SERVER wg0.conf file base64 encoded
- ONEAPP_VNF_WG_SERVER_TIMESTAMP when file was generated

Virtual router will reuse this configuration if present in any of the
virtual router VMs.

State is managed through onegate, thus required to run the WG service
  • Loading branch information
rsmontero committed May 6, 2024
1 parent 08626ed commit 0a1d6bd
Show file tree
Hide file tree
Showing 2 changed files with 244 additions and 310 deletions.
301 changes: 244 additions & 57 deletions appliances/VRouter/WireGuard/main.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,159 @@
require 'erb'
require 'ipaddr'
require 'yaml'
require 'base64'
require_relative '../vrouter.rb'

begin
require 'json-schema'
rescue LoadError
# NOTE: This handles the install stage.
end

module Service
module WireGuard
extend self

# This class represents a WG peer and includes function to render and publish
# its configuration to the virtual router VM template
class Peer
@@peers = 0

def initialize(subnet, ip)
@subnet = IPAddr.new(subnet)

raise "Peer IP #{ip} not in peer subnet #{subnet}" unless @subnet.include? ip

@ip = IPAddr.new(ip)

@peer = @@peers
@@peers = @@peers + 1

shared_k = bash('wg genpsk', chomp: true)
private_k = bash('wg genkey', chomp: true)
public_k = bash("wg pubkey <<< '#{private_k}'", chomp: true)

@wgpeer = {
'address' => "#{@ip.to_s}/#{@subnet.prefix}",
'preshared_key' => shared_k,
'private_key' => private_k,
'public_key' => public_k,
'allowed_ips' => %w[0.0.0.0/0]
}
end

def to_s_client(opts)
<<~PEER
[Interface]
Address = #{@wgpeer['address']}
PrivateKey = #{@wgpeer['private_key']}
[Peer]
Endpoint = #{opts['server_addr']}:#{opts['listen_port']}
PublicKey = #{@wgpeer['public_key']}
PresharedKey = #{@wgpeer['preshared_key']}
AllowedIPs = #{@wgpeer['allowed_ips'].join(%[,])}
PEER
end

def to_s_server
<<~PEER
[Peer]
PresharedKey = #{@wgpeer['preshared_key']}
PublicKey = #{@wgpeer['public_key']}
AllowedIPs = #{@wgpeer['address'].split(%[/])[0]}/32
PEER
end

def to_template(opts)
peer_conf64 = Base64.strict_encode64(to_s_client(opts))

"ONEAPP_VNF_WG_PEER#{@peer}=#{peer_conf64}"
end
end


DEPENDS_ON = %w[Service::Failover]

# --------------------------------------------------------------------------
# WireGuard Configuration parameters.
# --------------------------------------------------------------------------
#
# ONEAPP_VNF_WG_ENABLED = "YES"
# ONEAPP_VNF_WG_INTERFACE_OUT = "eth0"
# ONEAPP_VNF_WG_INTERFACE_IN = "eth1"
# ONEAPP_VNF_WG_LISTEN_PORT = "51820"
# ONEAPP_VNF_WG_DEVICE = "wg0"
# ONEAPP_VNG_WG_PEERS = "10.0.0.1 10.0.0.2 10.0.0.3 10.0.0.4"
# --------------------------------------------------------------------------
# The VM ID of the Virtual Router
VM_ID = env :VM_ID, nil

# Enables the service
ONEAPP_VNF_WG_ENABLED = env :ONEAPP_VNF_WG_ENABLED, 'NO'

ONEAPP_VNF_WG_INTERFACES_OUT = env :ONEAPP_VNF_WG_INTERFACES_OUT, nil # nil -> none, empty -> all
# The NIC to connect clients, its IP will be the service endpoint (MANDATORY)
ONEAPP_VNF_WG_INTERFACE_OUT = env :ONEAPP_VNF_WG_INTERFACE_OUT, nil

ONEAPP_VNF_WG_CFG_LOCATION = env :ONEAPP_VNF_WG_CFG_LOCATION, '/dev/sr0:/onewg.yml'
# The NIC to connect to the private subnet (MANDATORY)
ONEAPP_VNF_WG_INTERFACE_IN = env :ONEAPP_VNF_WG_INTERFACE_IN, nil

def parse_env
@interfaces_out ||= parse_interfaces ONEAPP_VNF_WG_INTERFACES_OUT
@mgmt ||= detect_mgmt_nics
@interfaces ||= @interfaces_out.keys - @mgmt
# Listen port number, defaults to 51820
ONEAPP_VNF_WG_LISTEN_PORT = env :ONEAPP_VNF_WG_LISTEN_PORT, 51820

iso_path, cfg_path = ONEAPP_VNF_WG_CFG_LOCATION.split(%[:])
# WG device name, defaults to wg0
ONEAPP_VNF_WG_DEVICE = env :ONEAPP_VNF_WG_DEVICE, 'wg0'

schema = YAML.load bash("#{File.dirname(__FILE__)}/onewg schema show", chomp: true)
# Peers by IP address, each address MUST no be assigned to any VM (i.e. put
# on hold or exclude from VNET AR's) (MANDATORY)
# For example 5 PEERS:
# ONEAPP_VNG_WG_PEERS = "10.0.0.1 10.0.0.2 10.0.0.3 10.0.0.4 10.0.0.5"
ONEAPP_VNF_WG_PEERS = env :ONEAPP_VNF_WG_PEERS, ''

document = YAML.load bash("isoinfo -i #{iso_path} -R -x #{cfg_path}", chomp: true)
#Base folder to store WG configuration
ETC_DIR = '/etc/wireguard'

if JSON::Validator.validate(schema, document)
{ cfg: document }
else
msg :error, 'YAML config looks invalid!'
{ cfg: nil }
end
def wg_environment
iout = ONEAPP_VNF_WG_INTERFACE_OUT
iin = ONEAPP_VNF_WG_INTERFACE_IN

mgmt = detect_mgmt_nics

raise "Forbidden ONEAPP_VNF_WG_INTERFACE_OUT interface: #{iout}" if mgmt.include?(iout)

#-----------------------------------------------------------------------
# Get IP address information for INTERFACE_IN
#-----------------------------------------------------------------------
eps = detect_endpoints

raise "Cannot find address information for #{iin}" if eps[iin].nil?

rc = iin.match /eth(\d+)/i

raise "Wrong format for ONEAPP_VNF_WG_INTERFACE_IN: #{iin}" if rc.nil?

addr_in = eps[iin]["ETH#{rc[1]}_EP0"]

raise "Cannot get IP address for #{iin}" if addr_in.nil? || addr_in.empty?

server_addr, server_prefix = addr_in.split('/')

nets_in = nics_to_subnets([iin])
net_in = nets_in[iin]

raise "Cannot get net addres for #{iin}" if nets_in[iin].nil? || net_in[0].empty?

#-----------------------------------------------------------------------
# Return configuration for the WG device
#-----------------------------------------------------------------------
{
'listen_port' => ONEAPP_VNF_WG_LISTEN_PORT,
'iface_out' => iout,
'server_addr' => server_addr,
'private_key' => bash('wg genkey', chomp: true),
'peer_subnet' => net_in[0],
'peers' => ONEAPP_VNF_WG_PEERS.split(' ').map {|p| p.chomp }
}
end

# --------------------------------------------------------------------------
# SERIVCE INTERFACE: install, configure and bootstrap methods
# --------------------------------------------------------------------------
# Installs WireGuard service. Log set to /var/log/one-appliance/one-wg.log
def install(initdir: '/etc/init.d')
msg :info, 'WireGuard::install'

Expand Down Expand Up @@ -72,63 +186,120 @@ def install(initdir: '/etc/init.d')
toggle [:update]
end

def configure(basedir: '/etc/wireguard')
# Configure WG service, just return and postpone to execute
def configure(basedir: ETC_DIR)
msg :info, 'WireGuard::configure'

# NOTE: We always disable it at re-contexting / reboot in case an user enables it manually.
unless ONEAPP_VNF_WG_ENABLED
# NOTE: We always disable it at re-contexting / reboot in case an user enables it manually.
toggle [:stop, :disable]
return
end
end

parse_env[:cfg]&.each do |dev, opts|
unless @interfaces.include?(opts['interface_out'])
msg :error, "Forbidden outgoing interface: #{opts['interface_out']}"
next
end
def bootstrap
msg :info, 'WireGuard::bootstrap'
end

def bootstrap
msg :info, 'WireGuard::bootstrap'
end

subnet = IPAddr.new(opts['peer_subnet'])
# --------------------------------------------------------------------------
# WG helper functions
# --------------------------------------------------------------------------
def execute
msg :info, 'WireGuard::execute'

opts = wg_environment

ids = onegate_vmids
conf64 = ''
tstamp = 0

ids.each do |vmid|
t, c = onegate_conf(vmid)

peers = opts['peers'].to_h.each_with_object({}) do |(k, v), acc|
next if v['public_key'].nil? && v['private_key'].nil?
conf64 = c if (tstamp == 0 || t > tstamp) && c && !c.empty?
end

v['public_key'] ||= bash("wg pubkey <<< #{v['private_key']}", chomp: true)
if !conf64.empty?
# ------------------------------------------------------------------
# Reuse existing configuration file in virtual router
# ------------------------------------------------------------------
msg :info, '[WireGuard::execute] Using existing configuration'

acc[k] = v
file "#{ETC_DIR}/#{ONEAPP_VNF_WG_DEVICE}.conf",
Base64.strict_decode64(conf64),
mode: 'u=rw,g=r,o=',
overwrite: true
else
msg :info, '[WireGuard::execute] Generating a new configuration'

# ------------------------------------------------------------------
# Generate a new configuration
# ------------------------------------------------------------------
peers = []

opts['peers'].each do |ip|
p = Peer.new opts['peer_subnet'], ip
peers << p
rescue StandardError => e
msg :error, e.message
next
end

file "#{basedir}/#{dev}.conf", ERB.new(<<~PEER, trim_mode: '-').result(binding), mode: 'u=rw,g=r,o=', overwrite: true
conf = ERB.new(<<~CONF, trim_mode: '-').result(binding)
[Interface]
Address = <%= subnet.succ.to_s %>/<%= subnet.prefix %>
ListenPort = <%= opts['server_port'] %>
Address = <%= opts['server_addr'] %>
ListenPort = <%= opts['listen_port'] %>
PrivateKey = <%= opts['private_key'] %>
<%- peers.each do |k, v| -%>
[Peer]
PresharedKey = <%= v['preshared_key'] %>
PublicKey = <%= v['public_key'] %>
AllowedIPs = <%= v['address'].split(%[/])[0] %>/32
<%- end -%>
PEER
end
end
<% peers.each do |p| %>
<%= p.to_s_server %>
<% end %>
CONF

file "#{ETC_DIR}/#{ONEAPP_VNF_WG_DEVICE}.conf",
conf,
mode: 'u=rw,g=r,o=',
overwrite: true

# ------------------------------------------------------------------
# Save configuration to virtual router VMs
# ------------------------------------------------------------------
info = []

peers.each do |p|
info << p.to_template(opts)
end

def execute
msg :info, 'WireGuard::execute'
info << "ONEAPP_VNF_WG_SERVER=#{Base64.strict_encode64(conf)}"
info << "ONEAPP_VNF_WG_SERVER_TIMESTAMP=#{Time.now.to_i}"

parse_env[:cfg]&.each do |dev, _|
bash <<~BASH
wg-quick up '#{dev}'
echo 1 > '/proc/sys/net/ipv4/conf/#{dev}/forwarding'
BASH
data = info.join("\n")

ids.each do |vmid|
msg :info, "[WireGuard::execute] Updating VM #{vmid}"

bash "onegate vm update #{vmid} --data \"#{data}\""
rescue StandardError => e
msg :error, e.message
next
end
end

msg :info, "[WireGuard::execute] bringing up #{ONEAPP_VNF_WG_DEVICE}"

bash <<~BASH
wg-quick up '#{ONEAPP_VNF_WG_DEVICE}'
echo 1 > '/proc/sys/net/ipv4/conf/#{ONEAPP_VNF_WG_DEVICE}/forwarding'
BASH
end

def cleanup
msg :info, 'WireGuard::cleanup'

parse_env[:cfg]&.each do |dev, _|
bash "wg-quick down '#{dev}'"
end
bash "wg-quick down '#{ONEAPP_VNF_WG_DEVICE}'"
end

def toggle(operations)
Expand All @@ -145,8 +316,24 @@ def toggle(operations)
end
end

def bootstrap
msg :info, 'WireGuard::bootstrap'
# Get the vm ids of the virtual router. Used to get/set WG configuration
def onegate_vmids
vr = onegate_vrouter_show

vr['VROUTER']['VMS']['ID']
rescue
[VM_ID]
end

# Get configuration from the VM template
def onegate_conf(vm_id)
vm = onegate_vm_show(vm_id)
utmp = vm['VM']['USER_TEMPLATE']

[utmp['ONEAPP_VNF_WG_SERVER_TIMESTAMP'], utmp['ONEAPP_VNF_WG_SERVER']]
rescue
[0, '']
end

end
end
Loading

0 comments on commit 0a1d6bd

Please sign in to comment.