require "option_parser" require "ipc" require "uri" require "baguette-crystal-base" require "./lib_modifications" # All standard library modifications. require "./ipcd/*" Baguette::Context.verbosity = 4 class Baguette::Configuration class IPC < Base property rules_directory : String = "/etc/baguette/ipcd/rules" # default property redirections_directory : String = "/etc/baguette/ipcd/redirections" # default property verbosity : Int32 = 2 property ipc_timer : Int32 = 30_000 # 30 seconds property print_ipc_timer : Bool = false property print_ipc_connection : Bool = false property print_ipc_disconnection : Bool = false property print_ipc_extra_socket : Bool = false property print_ipc_message_received : Bool = false property print_ipc_message_sent : Bool = false property print_ipc_switch : Bool = false def initialize end end end module IPCd class ServiceUnavailable < ::Exception end class NoScheme < ::Exception end class NoFileDescriptor < ::Exception end class ServiceError < ::Exception end class SharingFileDescriptor < ::Exception end class Service < IPC::Server @rules = RuleSet.new @redirections = RedirectionSet.new def parse_cli (argv : Array(String)) CLIParser.parse_rules argv, @rules, @redirections end def parse_rule_files(directory : String) unless File.directory? directory raise "not a valid directory: #{directory}" end files = Dir.new(directory).children.map {|f| directory + "/" + f } Array(Rule).from_yaml_files(files).each do |r| @rules << r end rescue e Baguette::Log.error "reading directory #{directory}: #{e}" end def parse_redirection_files(directory : String) unless File.directory? directory raise "not a valid directory: #{directory}" end files = Dir.new(directory).children.map {|f| directory + "/" + f } Array(Redirection).from_yaml_files(files).each do |r| @redirections << r end rescue e Baguette::Log.error "reading directory #{directory}: #{e}" end def to_s(io : IO) io << "ipcd\n" @rules.to_s(io) @redirections.to_s(io) end # Client => ipcd => Service. # ipcd creates a network connection to the remote host, # then provides a file descriptor to the client. # Besides ipcd configuration, the client shares its IPC_NETWORK # environment variable to ipcd to understand how to contact the service. # IPC_NETWORK environment variable contains pairs of URIs (redirections). # Format: "requested-service new-URI" # Example: "pong tcp://example.com:8000/pong" # # The URI indicates the scheme, the domain (or directly the IP address) and the port # to contact the remote service. The path indicates the remote service to use. # # Several redirections can be put into the IPC_NETWORK variable, using a semi-colon. # Example: "pong tcp://example.com/pong;auth tls://example.com:9000/auth" # ipcd prioritises IPC_NETWORK over its own configuration for redirections. # This environment variable is sent from the client to ipcd, that's what is parsed here. # parse_lookup_payload extracts the right service URI to use from the message sent by # a client, containing the service to contact and the IPC_NETWORK content. # Format: "requested-service;IPC_NETWORK content" # Example: "pong;pong tcp://example.com:3000/pong-8" def self.parse_lookup_payload (payload : String) : URI items = payload.split (";") requested_service_name = items.delete_at(0).chomp() requested_service = URI.parse "unix:///#{requested_service_name}" services_redirections = {} of String => URI # Format: "service-name URI;service-name URI" # URI can be: # * distant service (with protocol to use): https://some.example.com/pong # * local service: "local:newpong" or simply "newpong" items.each do |item| case item when /(?[^ ]+) +(?[^ ]+)/ origin, destination_uri = $~["origin"], $~["destination_uri"] uri = URI.parse destination_uri if uri.scheme.nil? uri = URI.parse "unix:///#{destination_uri}" end services_redirections[origin] = uri else Baguette::Log.error "cannot understand configuration #{item}" end end services_redirections.each do |k, v| if k == requested_service_name Baguette::Log.info "prefered redirection (from IPC_NETWORK): #{k} => #{v}" requested_service = v end end requested_service end # XXX: WIP def service_lookup (message : IPC::Message, client_fd : Int32) requested_service = IPCd::Service.parse_lookup_payload String.new(message.payload) # Now, we want to get the service used for transport. # Network services used to carry libipc messages are named based on the scheme. # Examples: tcp, udp, ws, etc. network_service_name = requested_service.scheme if network_service_name.nil? raise NoScheme.new end # In case of "unix", then it's a simple redirection to do, no network required. if requested_service.scheme == "unix" # The URI is "unix:///service" so the path is "/service". # Service to contact is simply the path minus the slash. network_service_name = requested_service.path.lchop end Baguette::Log.debug "connecting to #{network_service_name}" service = begin IPC::Client.new network_service_name rescue e # Better raise message. raise ServiceUnavailable.new network_service_name end # Will probably never happen in practice. if service.fd.nil? raise NoFileDescriptor.new network_service_name end service_fd = service.fd.not_nil! # A network service is necessary for remote communications, and it requires an URI. # TODO: protocol between ipcd and network services. # The (draft) protocol: # - ipcd sends an URI to the network service # - network service responds with a "OK" message if requested_service.scheme != "unix" service.send_now service_fd, 1.to_u8, requested_service.to_s response = service.read payload = String.new response.payload if payload.chomp != "OK" raise ServiceError.new "service #{network_service_name} response was #{payload.chomp}" end end Baguette::Log.debug "providing the fd from #{network_service_name} to the client #{client_fd}" # Inform the client that we successfully contacted the service. send_now client_fd, 3.to_u8, "OK" # Provide the file descriptor to the client. r = LibIPC.ipc_provide_fd(client_fd, service_fd) if r.error_code != 0 m = String.new r.error_message.to_slice raise SharingFileDescriptor.new "cannot send the file descriptor of the requested service: #{m}" end Baguette::Log.debug "everything went well, closing the service fd" # Finally, the service should be closed in ipcd. service.close end end end def main simulation, no_configuration, configuration_file = Baguette::Configuration.option_parser config = if no_configuration Baguette::Configuration::IPC.new elsif c = Baguette::Configuration::IPC.get configuration_file c else Baguette::Configuration::IPC.new end ipcd = IPCd::Service.new "network" # --deny # --allow # --redirect # --redirect ipcd.parse_cli ARGV ipcd.parse_rule_files config.rules_directory ipcd.parse_redirection_files config.redirections_directory Baguette::Log.debug ipcd.to_s ipcd.base_timer = config.ipc_timer ipcd.timer = config.ipc_timer if simulation pp! config ipcd.close exit 0 end ipcd.loop do |event| case event when IPC::Event::Connection Baguette::Log.debug "Connection: fd #{event.fd}" if config.print_ipc_connection when IPC::Event::Disconnection Baguette::Log.debug "Disconnection: fd #{event.fd}" if config.print_ipc_disconnection when IPC::Event::ExtraSocket Baguette::Log.debug "Extrasocket: fd #{event.fd}" if config.print_ipc_extra_socket when IPC::Event::Switch Baguette::Log.debug "Switch" if config.print_ipc_switch when IPC::Event::MessageReceived Baguette::Log.debug "Message received: #{event.fd}" if config.print_ipc_message_received Baguette::Log.debug event.message.to_s was_ok = begin ipcd.service_lookup event.message, event.fd true rescue e : IPCd::ServiceUnavailable Baguette::Log.error "cannot contact #{e}" false rescue e : IPCd::NoScheme Baguette::Log.error "#{e}" false rescue e : IPCd::NoFileDescriptor Baguette::Log.error "#{e}" false rescue e : IPCd::ServiceError Baguette::Log.error "#{e}" false rescue e : IPCd::SharingFileDescriptor Baguette::Log.error "#{e}" false rescue e Baguette::Log.error "#{e}" false end unless was_ok # Inform the client that we successfully contacted the service. ipcd.send_now event.fd, 3.to_u8, "NOT OK" end # # Job done: disconnection. # ipcd.remove_fd event.fd when IPC::Event::MessageSent Baguette::Log.debug "Message sent: #{event.fd}" if config.print_ipc_message_sent when IPC::Event::Timer Baguette::Log.debug "Timer" if config.print_ipc_timer when IPC::Exception Baguette::Log.debug "IPC::Exception: #{event}" end end # pp! rules.authorized? Rule::Direction::In, "authd", "tls:192.168.0.42" end main