664 lines
18 KiB
Ruby
664 lines
18 KiB
Ruby
##
|
|
# The Vici module implements a native ruby client side library for the
|
|
# strongSwan VICI protocol. The Connection class provides a high-level
|
|
# interface to issue requests or listen for events.
|
|
#
|
|
# Copyright (C) 2019 Tobias Brunner
|
|
# HSR Hochschule fuer Technik Rapperswil
|
|
#
|
|
# Copyright (C) 2014 Martin Willi
|
|
# Copyright (C) 2014 revosec AG
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
# THE SOFTWARE.
|
|
|
|
module Vici
|
|
##
|
|
# Vici specific exception all others inherit from
|
|
class Error < StandardError
|
|
end
|
|
|
|
##
|
|
# Error while parsing a vici message from the daemon
|
|
class ParseError < Error
|
|
end
|
|
|
|
##
|
|
# Error while encoding a vici message from ruby data structures
|
|
class EncodeError < Error
|
|
end
|
|
|
|
##
|
|
# Error while exchanging messages over the vici Transport layer
|
|
class TransportError < Error
|
|
end
|
|
|
|
##
|
|
# Generic vici command execution error
|
|
class CommandError < Error
|
|
end
|
|
|
|
##
|
|
# Error if an issued vici command is unknown by the daemon
|
|
class CommandUnknownError < CommandError
|
|
end
|
|
|
|
##
|
|
# Error if a command failed to execute in the daemon
|
|
class CommandExecError < CommandError
|
|
end
|
|
|
|
##
|
|
# Generic vici event handling error
|
|
class EventError < Error
|
|
end
|
|
|
|
##
|
|
# Tried to register to / unregister from an unknown vici event
|
|
class EventUnknownError < EventError
|
|
end
|
|
|
|
##
|
|
# Exception to raise from an event listening closure to stop listening
|
|
class StopEventListening < Exception
|
|
end
|
|
|
|
##
|
|
# The Message class provides the low level encoding and decoding of vici
|
|
# protocol messages. Directly using this class is usually not required.
|
|
class Message
|
|
SECTION_START = 1
|
|
SECTION_END = 2
|
|
KEY_VALUE = 3
|
|
LIST_START = 4
|
|
LIST_ITEM = 5
|
|
LIST_END = 6
|
|
|
|
def initialize(data = "")
|
|
if data.nil?
|
|
@root = {}
|
|
elsif data.is_a?(Hash)
|
|
@root = data
|
|
else
|
|
@encoded = data
|
|
end
|
|
end
|
|
|
|
##
|
|
# Get the raw byte encoding of an on-the-wire message
|
|
def encoding
|
|
@encoded = encode(@root) if @encoded.nil?
|
|
@encoded
|
|
end
|
|
|
|
##
|
|
# Get the root element of the parsed ruby data structures
|
|
def root
|
|
@root = parse(@encoded) if @root.nil?
|
|
@root
|
|
end
|
|
|
|
private
|
|
|
|
def encode_name(name)
|
|
[name.length].pack("c") << name
|
|
end
|
|
|
|
def encode_value(value)
|
|
value = value.to_s if value.class != String
|
|
[value.length].pack("n") << value
|
|
end
|
|
|
|
def encode_kv(encoding, key, value)
|
|
encoding << KEY_VALUE << encode_name(key) << encode_value(value)
|
|
end
|
|
|
|
def encode_section(encoding, key, value)
|
|
encoding << SECTION_START << encode_name(key)
|
|
encoding << encode(value) << SECTION_END
|
|
end
|
|
|
|
def encode_list(encoding, key, value)
|
|
encoding << LIST_START << encode_name(key)
|
|
value.each do |item|
|
|
encoding << LIST_ITEM << encode_value(item)
|
|
end
|
|
encoding << LIST_END
|
|
end
|
|
|
|
def encode(node)
|
|
encoding = ""
|
|
node.each do |key, value|
|
|
encoding = if value.is_a?(Hash)
|
|
encode_section(encoding, key, value)
|
|
elsif value.is_a?(Array)
|
|
encode_list(encoding, key, value)
|
|
else
|
|
encode_kv(encoding, key, value)
|
|
end
|
|
end
|
|
encoding
|
|
end
|
|
|
|
def parse_name(encoding)
|
|
len = encoding.unpack("c")[0]
|
|
name = encoding[1, len]
|
|
[encoding[(1 + len)..-1], name]
|
|
end
|
|
|
|
def parse_value(encoding)
|
|
len = encoding.unpack("n")[0]
|
|
value = encoding[2, len]
|
|
[encoding[(2 + len)..-1], value]
|
|
end
|
|
|
|
def parse(encoding)
|
|
stack = [{}]
|
|
list = nil
|
|
until encoding.empty?
|
|
type = encoding.unpack("c")[0]
|
|
encoding = encoding[1..-1]
|
|
case type
|
|
when SECTION_START
|
|
encoding, name = parse_name(encoding)
|
|
stack.push(stack[-1][name] = {})
|
|
when SECTION_END
|
|
raise ParseError, "unexpected section end" if stack.length == 1
|
|
stack.pop
|
|
when KEY_VALUE
|
|
encoding, name = parse_name(encoding)
|
|
encoding, value = parse_value(encoding)
|
|
stack[-1][name] = value
|
|
when LIST_START
|
|
encoding, name = parse_name(encoding)
|
|
stack[-1][name] = []
|
|
list = name
|
|
when LIST_ITEM
|
|
raise ParseError, "unexpected list item" if list.nil?
|
|
encoding, value = parse_value(encoding)
|
|
stack[-1][list].push(value)
|
|
when LIST_END
|
|
raise ParseError, "unexpected list end" if list.nil?
|
|
list = nil
|
|
else
|
|
raise ParseError, "invalid type: #{type}"
|
|
end
|
|
end
|
|
raise ParseError, "unexpected message end" if stack.length > 1
|
|
stack[0]
|
|
end
|
|
end
|
|
|
|
##
|
|
# The Transport class implements to low level segmentation of packets
|
|
# to the underlying transport stream. Directly using this class is usually
|
|
# not required.
|
|
class Transport
|
|
CMD_REQUEST = 0
|
|
CMD_RESPONSE = 1
|
|
CMD_UNKNOWN = 2
|
|
EVENT_REGISTER = 3
|
|
EVENT_UNREGISTER = 4
|
|
EVENT_CONFIRM = 5
|
|
EVENT_UNKNOWN = 6
|
|
EVENT = 7
|
|
|
|
##
|
|
# Create a transport layer using a provided socket for communication.
|
|
def initialize(socket)
|
|
@socket = socket
|
|
@events = {}
|
|
end
|
|
|
|
##
|
|
# Receive data from socket, until len bytes read
|
|
def recv_all(len)
|
|
encoding = ""
|
|
while encoding.length < len
|
|
data = @socket.recv(len - encoding.length)
|
|
raise TransportError, "connection closed" if data.empty?
|
|
encoding << data
|
|
end
|
|
encoding
|
|
end
|
|
|
|
##
|
|
# Send data to socket, until all bytes sent
|
|
def send_all(encoding)
|
|
len = 0
|
|
len += @socket.send(encoding[len..-1], 0) while len < encoding.length
|
|
end
|
|
|
|
##
|
|
# Write a packet prefixed by its length over the transport socket. Type
|
|
# specifies the message, the optional label and message get appended.
|
|
def write(type, label, message)
|
|
encoding = ""
|
|
encoding << label.length << label if label
|
|
encoding << message.encoding if message
|
|
send_all([encoding.length + 1, type].pack("Nc") + encoding)
|
|
end
|
|
|
|
##
|
|
# Read a packet from the transport socket. Returns the packet type, and
|
|
# if available in the packet a label and the contained message.
|
|
def read
|
|
len = recv_all(4).unpack("N")[0]
|
|
encoding = recv_all(len)
|
|
type = encoding.unpack("c")[0]
|
|
len = 1
|
|
case type
|
|
when CMD_REQUEST, EVENT_REGISTER, EVENT_UNREGISTER, EVENT
|
|
label = encoding[2, encoding[1].unpack("c")[0]]
|
|
len += label.length + 1
|
|
when CMD_RESPONSE, CMD_UNKNOWN, EVENT_CONFIRM, EVENT_UNKNOWN
|
|
label = nil
|
|
else
|
|
raise TransportError, "invalid message: #{type}"
|
|
end
|
|
message = if encoding.length == len
|
|
Message.new
|
|
else
|
|
Message.new(encoding[len..-1])
|
|
end
|
|
[type, label, message]
|
|
end
|
|
|
|
def dispatch_event(name, message)
|
|
@events[name].each do |handler|
|
|
handler.call(name, message)
|
|
end
|
|
end
|
|
|
|
def read_and_dispatch_event
|
|
type, label, message = read
|
|
raise TransportError, "unexpected message: #{type}" if type != EVENT
|
|
|
|
dispatch_event(label, message)
|
|
end
|
|
|
|
def read_and_dispatch_events
|
|
loop do
|
|
type, label, message = read
|
|
return type, label, message if type != EVENT
|
|
|
|
dispatch_event(label, message)
|
|
end
|
|
end
|
|
|
|
##
|
|
# Send a command with a given name, and optionally a message. Returns
|
|
# the reply message on success.
|
|
def request(name, message = nil)
|
|
write(CMD_REQUEST, name, message)
|
|
type, _label, message = read_and_dispatch_events
|
|
case type
|
|
when CMD_RESPONSE
|
|
return message
|
|
when CMD_UNKNOWN
|
|
raise CommandUnknownError, name
|
|
else
|
|
raise CommandError, "invalid response for #{name}"
|
|
end
|
|
end
|
|
|
|
##
|
|
# Register a handler method for the given event name
|
|
def register(name, handler)
|
|
write(EVENT_REGISTER, name, nil)
|
|
type, _label, _message = read_and_dispatch_events
|
|
case type
|
|
when EVENT_CONFIRM
|
|
if @events.key?(name)
|
|
@events[name] += [handler]
|
|
else
|
|
@events[name] = [handler]
|
|
end
|
|
when EVENT_UNKNOWN
|
|
raise EventUnknownError, name
|
|
else
|
|
raise EventError, "invalid response for #{name} register"
|
|
end
|
|
end
|
|
|
|
##
|
|
# Unregister a handler method for the given event name
|
|
def unregister(name, handler)
|
|
write(EVENT_UNREGISTER, name, nil)
|
|
type, _label, _message = read_and_dispatch_events
|
|
case type
|
|
when EVENT_CONFIRM
|
|
@events[name] -= [handler]
|
|
when EVENT_UNKNOWN
|
|
raise EventUnknownError, name
|
|
else
|
|
raise EventError, "invalid response for #{name} unregister"
|
|
end
|
|
end
|
|
end
|
|
|
|
##
|
|
# The Connection class provides the high-level interface to monitor, configure
|
|
# and control the IKE daemon. It takes a connected stream-oriented Socket for
|
|
# the communication with the IKE daemon.
|
|
#
|
|
# This class takes and returns ruby objects for the exchanged message data.
|
|
# * Sections get encoded as Hash, containing other sections as Hash, or
|
|
# * Key/Values, where the values are Strings as Hash values
|
|
# * Lists get encoded as Arrays with String values
|
|
# Non-String values that are not a Hash nor an Array get converted with .to_s
|
|
# during encoding.
|
|
class Connection
|
|
##
|
|
# Create a connection, optionally using the given socket
|
|
def initialize(socket = nil)
|
|
socket = UNIXSocket.new("/var/run/charon.vici") if socket.nil?
|
|
@transp = Transport.new(socket)
|
|
end
|
|
|
|
##
|
|
# Get daemon version information
|
|
def version
|
|
call("version")
|
|
end
|
|
|
|
##
|
|
# Get daemon statistics and information.
|
|
def stats
|
|
call("stats")
|
|
end
|
|
|
|
##
|
|
# Reload strongswan.conf settings.
|
|
def reload_settings
|
|
call("reload-settings")
|
|
end
|
|
|
|
##
|
|
# Initiate a connection. The provided closure is invoked for each log line.
|
|
def initiate(options, &block)
|
|
call_with_event("initiate", Message.new(options), "control-log", &block)
|
|
end
|
|
|
|
##
|
|
# Terminate a connection. The provided closure is invoked for each log line.
|
|
def terminate(options, &block)
|
|
call_with_event("terminate", Message.new(options), "control-log", &block)
|
|
end
|
|
|
|
##
|
|
# Initiate the rekeying of an SA.
|
|
def rekey(options)
|
|
call("rekey", Message.new(options))
|
|
end
|
|
|
|
##
|
|
# Redirect an IKE_SA.
|
|
def redirect(options)
|
|
call("redirect", Message.new(options))
|
|
end
|
|
|
|
##
|
|
# Install a shunt/route policy.
|
|
def install(policy)
|
|
call("install", Message.new(policy))
|
|
end
|
|
|
|
##
|
|
# Uninstall a shunt/route policy.
|
|
def uninstall(policy)
|
|
call("uninstall", Message.new(policy))
|
|
end
|
|
|
|
##
|
|
# List matching active SAs. The provided closure is invoked for each
|
|
# matching SA.
|
|
def list_sas(match = nil, &block)
|
|
call_with_event("list-sas", Message.new(match), "list-sa", &block)
|
|
end
|
|
|
|
##
|
|
# List matching installed policies. The provided closure is invoked
|
|
# for each matching policy.
|
|
def list_policies(match, &block)
|
|
call_with_event("list-policies", Message.new(match), "list-policy",
|
|
&block)
|
|
end
|
|
|
|
##
|
|
# List matching loaded connections. The provided closure is invoked
|
|
# for each matching connection.
|
|
def list_conns(match = nil, &block)
|
|
call_with_event("list-conns", Message.new(match), "list-conn", &block)
|
|
end
|
|
|
|
##
|
|
# Get the names of connections managed by vici.
|
|
def get_conns
|
|
call("get-conns")
|
|
end
|
|
|
|
##
|
|
# List matching loaded certificates. The provided closure is invoked
|
|
# for each matching certificate definition.
|
|
def list_certs(match = nil, &block)
|
|
call_with_event("list-certs", Message.new(match), "list-cert", &block)
|
|
end
|
|
|
|
##
|
|
# List matching loaded certification authorities. The provided closure is
|
|
# invoked for each matching certification authority definition.
|
|
def list_authorities(match = nil, &block)
|
|
call_with_event("list-authorities", Message.new(match), "list-authority",
|
|
&block)
|
|
end
|
|
|
|
##
|
|
# Get the names of certification authorities managed by vici.
|
|
def get_authorities
|
|
call("get-authorities")
|
|
end
|
|
|
|
##
|
|
# Load a connection into the daemon.
|
|
def load_conn(conn)
|
|
call("load-conn", Message.new(conn))
|
|
end
|
|
|
|
##
|
|
# Unload a connection from the daemon.
|
|
def unload_conn(conn)
|
|
call("unload-conn", Message.new(conn))
|
|
end
|
|
|
|
##
|
|
# Load a certificate into the daemon.
|
|
def load_cert(cert)
|
|
call("load-cert", Message.new(cert))
|
|
end
|
|
|
|
##
|
|
# Load a private key into the daemon.
|
|
def load_key(key)
|
|
call("load-key", Message.new(key))
|
|
end
|
|
|
|
##
|
|
# Unload a private key from the daemon.
|
|
def unload_key(key)
|
|
call("unload-key", Message.new(key))
|
|
end
|
|
|
|
##
|
|
# Get the identifiers of private keys loaded via vici.
|
|
def get_keys
|
|
call("get-keys")
|
|
end
|
|
|
|
##
|
|
# Load a private key located on a token into the daemon.
|
|
def load_token(token)
|
|
call("load-token", Message.new(token))
|
|
end
|
|
|
|
##
|
|
# Load a shared key into the daemon.
|
|
def load_shared(shared)
|
|
call("load-shared", Message.new(shared))
|
|
end
|
|
|
|
##
|
|
# Unload a shared key from the daemon.
|
|
def unload_shared(shared)
|
|
call("unload-shared", Message.new(shared))
|
|
end
|
|
|
|
##
|
|
# Get the unique identifiers of shared keys loaded via vici.
|
|
def get_shared
|
|
call("get-shared")
|
|
end
|
|
|
|
##
|
|
# Flush credential cache.
|
|
def flush_certs(match = nil)
|
|
call("flush-certs", Message.new(match))
|
|
end
|
|
|
|
##
|
|
# Clear all loaded credentials.
|
|
def clear_creds
|
|
call("clear-creds")
|
|
end
|
|
|
|
##
|
|
# Load a certification authority into the daemon.
|
|
def load_authority(authority)
|
|
call("load-authority", Message.new(authority))
|
|
end
|
|
|
|
##
|
|
# Unload a certification authority from the daemon.
|
|
def unload_authority(authority)
|
|
call("unload-authority", Message.new(authority))
|
|
end
|
|
|
|
##
|
|
# Load a virtual IP / attribute pool into the daemon.
|
|
def load_pool(pool)
|
|
call("load-pool", Message.new(pool))
|
|
end
|
|
|
|
##
|
|
# Unload a virtual IP / attribute pool from the daemon.
|
|
def unload_pool(pool)
|
|
call("unload-pool", Message.new(pool))
|
|
end
|
|
|
|
##
|
|
# Get the currently loaded pools.
|
|
def get_pools(options)
|
|
call("get-pools", Message.new(options))
|
|
end
|
|
|
|
##
|
|
# Get currently loaded algorithms and their implementation.
|
|
def get_algorithms
|
|
call("get-algorithms")
|
|
end
|
|
|
|
##
|
|
# Get global or connection-specific counters for IKE events.
|
|
def get_counters(options = nil)
|
|
call("get-counters", Message.new(options))
|
|
end
|
|
|
|
##
|
|
# Reset global or connection-specific IKE event counters.
|
|
def reset_counters(options = nil)
|
|
call("reset-counters", Message.new(options))
|
|
end
|
|
|
|
##
|
|
# Listen for a set of event messages. This call is blocking, and invokes
|
|
# the passed closure for each event received. The closure receives the
|
|
# event name and the event message as argument. To stop listening, the
|
|
# closure may raise a StopEventListening exception, the only caught
|
|
# exception.
|
|
def listen_events(events, &block)
|
|
self.class.instance_eval do
|
|
define_method(:listen_event) do |label, message|
|
|
block.call(label, message.root)
|
|
end
|
|
end
|
|
events.each do |event|
|
|
@transp.register(event, method(:listen_event))
|
|
end
|
|
begin
|
|
loop do
|
|
@transp.read_and_dispatch_event
|
|
end
|
|
rescue StopEventListening
|
|
ensure
|
|
events.each do |event|
|
|
@transp.unregister(event, method(:listen_event))
|
|
end
|
|
end
|
|
end
|
|
|
|
##
|
|
# Issue a command request. Checks if the reply of a command indicates
|
|
# "success", otherwise raises a CommandExecError exception.
|
|
def call(command, request = nil)
|
|
check_success(@transp.request(command, request))
|
|
end
|
|
|
|
##
|
|
# Issue a command request, but register for a specific event while the
|
|
# command is active. VICI uses this mechanism to stream potentially large
|
|
# data objects continuously. The provided closure is invoked for all
|
|
# event messages.
|
|
def call_with_event(command, request, event, &block)
|
|
self.class.instance_eval do
|
|
define_method(:call_event) do |_label, message|
|
|
block.call(message.root)
|
|
end
|
|
end
|
|
@transp.register(event, method(:call_event))
|
|
begin
|
|
reply = @transp.request(command, request)
|
|
ensure
|
|
@transp.unregister(event, method(:call_event))
|
|
end
|
|
check_success(reply)
|
|
end
|
|
|
|
##
|
|
# Check if the reply of a command indicates "success", otherwise raise a
|
|
# CommandExecError exception
|
|
def check_success(reply)
|
|
root = reply.root
|
|
if root.key?("success") && root["success"] != "yes"
|
|
raise CommandExecError, root["errmsg"]
|
|
end
|
|
|
|
root
|
|
end
|
|
end
|
|
end
|