diff --git a/appliances/VRouter/WireGuard/main.rb b/appliances/VRouter/WireGuard/main.rb index 7774dab6..2621e6bd 100644 --- a/appliances/VRouter/WireGuard/main.rb +++ b/appliances/VRouter/WireGuard/main.rb @@ -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' @@ -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) @@ -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 diff --git a/appliances/VRouter/WireGuard/main2.rb b/appliances/VRouter/WireGuard/main2.rb deleted file mode 100644 index 3dbd2532..00000000 --- a/appliances/VRouter/WireGuard/main2.rb +++ /dev/null @@ -1,253 +0,0 @@ -# frozen_string_literal: true - -require 'erb' -require 'ipaddr' -require 'yaml' -require 'base64' -require_relative '../vrouter.rb' - -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 update(opts) - conf = "ONEAPP_VNF_WG_PEER#{@peer}='#{Base64.strict_encode64(to_s_client(opts))}'" - - bash "onegate vm update #{VM_ID} --data #{conf}" - rescue StandardError => e - msg :error, e.message - 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' - - # 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 - - # The NIC to connect to the private subnet (MANDATORY) - ONEAPP_VNF_WG_INTERFACE_IN = env :ONEAPP_VNF_WG_INTERFACE_IN, nil - - # Listen port number, defaults to 51820 - ONEAPP_VNF_WG_LISTEN_PORT = env :ONEAPP_VNF_WG_LISTEN_PORT, 51820 - - # WG device name, defaults to wg0 - ONEAPP_VNF_WG_DEVICE = env :ONEAPP_VNF_WG_DEVICE, 'wg0' - - # 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, '' - - def parse_env - iout = ONEAPP_VNF_WG_INTERFACE_OUT - iin = ONEAPP_VNF_WG_INTERFACE_IN - - mgmt = detect_mgmt_nics - - raise "Forbidden public (out) interface: #{iout}" if mgmt.include?(iout) - - @addrs_in ||= nics_to_addrs([iin]) - @addr_in ||= @addrs_in[iin] - - @nets_in ||= nics_to_subnets([iin]) - @net_in ||= @nets_in[iin] - - if @net_in.nil? || @net_in[0].empty? || @addr_in.nil? || @addr_in[0].empty? - raise "Wrong configuration for private (in) interface: #{iin}" - end - - { - ONEAPP_VNF_WG_DEVICE => { - 'listen_port' => ONEAPP_VNF_WG_LISTEN_PORT, - 'iface_out' => iout, - 'server_addr' => @addr_in[0], - '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' - - puts bash 'apk --no-cache add cdrkit ruby wireguard-tools-wg-quick' - puts bash 'gem install --no-document json-schema' - - file "#{initdir}/one-wg", <<~SERVICE, mode: 'u=rwx,g=rx,o=' - #!/sbin/openrc-run - - source /run/one-context/one_env - - command="/usr/bin/ruby" - command_args="-r /etc/one-appliance/lib/helpers.rb -r #{__FILE__}" - - depend() { - after net firewall keepalived - } - - start() { - $command $command_args -e Service::WireGuard.execute 1>>/var/log/one-appliance/one-wg.log 2>&1 - } - - stop() { - $command $command_args -e Service::WireGuard.cleanup 1>>/var/log/one-appliance/one-wg.log 2>&1 - } - SERVICE - - toggle [:update] - end - - # Configure WG service - def configure(basedir: '/etc/wireguard') - msg :info, 'WireGuard::configure' - - 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 - - parse_env.each do |dev, opts| - 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(<<~CONF, trim_mode: '-').result(binding), mode: 'u=rw,g=r,o=', overwrite: true - [Interface] - Address = <%= opts['server_addr'] %> - ListenPort = <%= opts['listen_port'] %> - PrivateKey = <%= opts['private_key'] %> - <% peers.each do |p| %> - <%= p.to_s_server %> - <% end %> - CONF - - peers.each do |p| - p.update opts - end - end - end - - def bootstrap - msg :info, 'WireGuard::bootstrap' - end - - # -------------------------------------------------------------------------- - # WG helper functions - # -------------------------------------------------------------------------- - def execute - msg :info, 'WireGuard::execute' - - parse_env[:cfg]&.each do |dev, _| - bash <<~BASH - wg-quick up '#{dev}' - echo 1 > '/proc/sys/net/ipv4/conf/#{dev}/forwarding' - BASH - end - end - - def cleanup - msg :info, 'WireGuard::cleanup' - - parse_env[:cfg]&.each do |dev, _| - bash "wg-quick down '#{dev}'" - end - end - - def toggle(operations) - operations.each do |op| - msg :info, "WireGuard::toggle([:#{op}])" - case op - when :disable - puts bash 'rc-update del one-wg default ||:' - when :update - puts bash 'rc-update -u' - else - puts bash "rc-service one-wg #{op.to_s}" - end - end - end - -end -end