From 02b1acc0405e2ddad88e3583ef85d3f4e12f9841 Mon Sep 17 00:00:00 2001 From: Philippe PITTOLI Date: Sat, 27 Jul 2019 15:23:01 +0200 Subject: [PATCH] networkd: v0.1 --- README.md | 103 +++++++++++++++++++++++++++ shard.yml | 34 +++++++++ src/colors.cr | 5 ++ src/main.cr | 34 +++++++++ src/networkd.cr | 146 +++++++++++++++++++++++++++++++++++++++ src/networkdcliparser.cr | 38 ++++++++++ src/pongc.cr | 35 ++++++++++ src/pongd.cr | 15 ++++ src/rules.cr | 85 +++++++++++++++++++++++ src/tcp.cr | 51 ++++++++++++++ src/tcpd.cr | 129 ++++++++++++++++++++++++++++++++++ 11 files changed, 675 insertions(+) create mode 100644 README.md create mode 100644 shard.yml create mode 100644 src/colors.cr create mode 100644 src/main.cr create mode 100644 src/networkd.cr create mode 100644 src/networkdcliparser.cr create mode 100644 src/pongc.cr create mode 100644 src/pongd.cr create mode 100644 src/rules.cr create mode 100644 src/tcp.cr create mode 100644 src/tcpd.cr diff --git a/README.md b/README.md new file mode 100644 index 0000000..9215177 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ + +Networkd is a program to handle networking for all other software. + +# WARNING + +Security is TBD. Currently, only TCPd is implemented, which means no communication security. + +# Networkd functionalities + +## firewall + +`Networkd` has to filter the connections to local services. + +```Warning +WIP. +``` + +## authentication + +`Networkd` has to authenticate clients asking for a service. + +```Warning +WIP. +``` + +## redirection + +Central networking management allows for functionalities such as redirections. +For example, a local client asking for the authentication can be authenticated with a distant authentication service. + +## encapsulation + +```Warning +TBD. WIP. +``` + + +# Configuration + +Configuration is yet to be defined. + +* redirection +* firewall +* authentication + +# Usage + +This program can be used as follow: + +```sh +# with some static rules +networkd --allow in authd tls:example.com --deny in * * --allow out pong tls:pong.example.com:9000 +networkd --redirect authd nextversion-authd +``` + +## usage examples + +`networkd` is requested each time a client is launched when the right environment variable is used. +For example, we want to connect to a distant `authd` service: + + IPC_NETWORKD="authd tls://user@passwd:example.com:9000/authd" + + +```Warning +Currently, the networkd only works with tcp and unix routes. +``` + + IPC_NETWORKD="pongd tcp://example.com:9000/pongd" + +# Changelog + +* v0.1: (current) networkd (redirections), tcpd + + * `networkd` understands URIs (`tcp://example.com/service` or `unix:///service`) + * `tcp` scheme is understood: `networkd` contacts the `tcpd` service + * `unix` scheme is understood: `networkd` performs a redirection + + +# Roadmap + + +* v0.2: webipcd, documentation +* v0.3: firewall + redirections +* v0.4: static configuration: default routes, authentication +* v0.5: tlsd built-in, pre-shared keys +* v0.6: udpd +* v1.0: TBD + + +# Networkd explanations + +1. client contacts `networkd` +1. `networkd` understand the request from the client then contacts the local service responsible for the communication protocol required +1. once the distant connection is established (between the two `tlsd` services for example) `networkd` provides a file descriptor to the client +1. finally, the client can perform requests to the distant service transparently + + during the connection: + + client <-> networkd <-> tlsd <=> tlsd <-> networkd <-> service + + then: + + client <-> tlsd <=> tlsd <-> server diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..ea48d1e --- /dev/null +++ b/shard.yml @@ -0,0 +1,34 @@ +name: networkd +version: 0.1.0 + +authors: + - karchnu + +description: | + Short description of networkd + +dependencies: + ipc: + git: https://git.karchnu.fr/JunkOS/ipc.cr + branch: idontknowwhatimdoinglol + +targets: + pongc: + main: src/pongc.cr + pongd: + main: src/pongd.cr + networkd: + main: src/main.cr + tcpd: + main: src/tcpd.cr + tcp: + main: src/tcp.cr + + # webipc: + # main: src/webipcd.cr + # websocketclient: + # main: src/websocket-client.cr + # websocketserver: + # main: src/websocket-server.cr + +license: ISC diff --git a/src/colors.cr b/src/colors.cr new file mode 100644 index 0000000..4c4d49c --- /dev/null +++ b/src/colors.cr @@ -0,0 +1,5 @@ +CRED = "\033[31m" +CBLUE = "\033[36m" +CGREEN = "\033[32m" +CRESET = "\033[00m" +CORANGE = "\033[33m" diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..10e4ade --- /dev/null +++ b/src/main.cr @@ -0,0 +1,34 @@ +require "./networkd" + +networkd = NetworkD.new "network" + +# --deny +# --allow +# --redirect +# --redirect + +networkd.parse_cli ARGV +puts networkd.to_s + +networkd.loop do |event| + puts "there is an event!" + case event + when IPC::Event::Connection + puts "\033[32mConnection:\033[00m fd #{event.connection.fd}" + when IPC::Event::Disconnection + puts "\033[32mDisconnection:\033[00m fd #{event.connection.fd}" + when IPC::Event::ExtraSocket + puts "\033[32mExtrasocket:\033[00m fd #{event.connection.fd}" + when IPC::Event::Switch + puts "\033[31mSwitch\033[00m" + when IPC::Exception + puts "\033[31mException\033[00m" + when IPC::Event::Message + puts "\033[32mthere is a message\033[00m" + puts event.message.to_s + + networkd.service_lookup event.message, event.connection + end +end + +# pp! rules.authorized? Rule::Direction::In, "authd", "tls:192.168.0.42" diff --git a/src/networkd.cr b/src/networkd.cr new file mode 100644 index 0000000..15f709f --- /dev/null +++ b/src/networkd.cr @@ -0,0 +1,146 @@ + +require "./rules" +require "./networkdcliparser" +require "ipc" +require "uri" + +class IPC::NetworkD < IPC::Service + + # The client asks to networkd to open a connection to a service + # the service name is used unless there is a redirection that is provided to the client through + # a IPC_NETWORK environment variable + # This environment variable is sent from the client to networkd, that's what is parsed here + + # parse_lookup_payload extract the right service URI to use from the IPC_NETWORK content + # sent by the client in the form: + # `requested-service-name;service1 uri;service2 uri;...` etc. + 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 + + # from each item (separated by a semicolon), get the service name and the new uri to use + # 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| + x = /([^ ]+) ([^ ]+)/.match(item) + unless x.nil? + service, newuri = x.captures() + if service.nil? + next + elsif newuri.nil? + next + end + + puts "service: #{service} redirection uri: #{newuri}" + uri = URI.parse newuri + services_redirections[service] = uri + end + end + + services_redirections.each do |k, v| + puts "\033[36mpossible redirection (from env. var.):\033[00m service: #{k} uri: #{v}" + if k == requested_service_name + requested_service = v + end + end + + requested_service + end + + # XXX: WIP + def service_lookup (message : IPC::Message, origin : IPC::Connection) + payload = String.new message.payload + + requested_service = IPC::NetworkD.parse_lookup_payload payload + + # TODO: connect to the service then provide the file descriptor to the client + begin + scheme = requested_service.scheme + if scheme.nil? + raise "no SCHEME in redirection" + end + + service_name = scheme + if scheme == "unix" + # scheme == unix => simple redirection + + # the URI is "unix:///service" so the path is "/service" + # first, remove its slash prefix + service_name = requested_service.path.lchop + end + + puts "service name: #{service_name}" + service = IPC::Connection.new service_name + + # TODO: for remote services, we have to connect to communication service + # these communication services need an URI to work on + # The protocol: + # networkd sends an URI to the communication service, which responds with a "OK" message + if scheme != "unix" + service.send 1.to_u8, "#{requested_service.to_s}\n" + response = service.read + payload = String.new response.payload + if payload.chomp != "OK" + raise "service #{service_name} response was #{payload.chomp}" + end + end + + # Then we provide the file descriptor to the client + r = LibIPC.ipc_provide_fd(origin.fd, service.fd) + if r != 0 + m = String.new LibIPC.ipc_errors_get (r) + raise Exception.new "cannot send the file descriptor of the requested service: #{m}" + end + + # finally, the service should be closed in networkd + service.close + rescue e + puts "\033[31mException during the connection to the requested service #{requested_service}: #{e}\033[00m" + # when a problem occurs, close the client connection + + begin + # LibIPC.ipc_connections_print pointerof(@connections) + remove_fd origin.fd + + origin.close + + rescue ex + puts "\033[31mException during a client removal: #{ex}\033[00m" + end + end + end + + def wait_event(server : IPC::Connection?, &block) : Tuple(LibIPC::EventType, IPC::Message, IPC::Connection) + event = LibIPC::Event.new + + # TODO: networkd should be able to transfer messages??? + r = LibIPC.ipc_wait_event self.pointer, @service_info.pointer, pointerof(event) + if r != 0 + m = String.new LibIPC.ipc_errors_get (r) + yield IPC::Exception.new "error waiting for a new event: #{m}" + end + + connection = IPC::Connection.new event.origin.unsafe_as(Pointer(LibIPC::Connection)).value + message = event.message.unsafe_as(Pointer(LibIPC::Message)) + + return event.type, IPC::Message.new(message), connection + end +end + +class NetworkD < IPC::NetworkD + @rules = RuleSet.new + @redirections = RedirectionSet.new + + def parse_cli (argv : Array(String)) + NetworkDCLIParser.parse_rules argv, @rules, @redirections + end + + def to_s + @rules.to_s + "\n" + @redirections.to_s + end +end diff --git a/src/networkdcliparser.cr b/src/networkdcliparser.cr new file mode 100644 index 0000000..95c7c9e --- /dev/null +++ b/src/networkdcliparser.cr @@ -0,0 +1,38 @@ + +class NetworkDCLIParser + def self.pack_args (argv : Array(String)) + last_flag = nil : String? + + argv.chunks do |x| + if x[0..1] == "--" + last_flag = x + end + + last_flag + end + end + + def self.parse_rules (argv : Array(String), rules : RuleSet, redirections : RedirectionSet) + args = NetworkDCLIParser.pack_args argv + + args.each do |flag, parameters| + # puts "flag: #{flag}, params: #{parameters.join(' ')}" + if flag == "--allow" || flag == "--deny" + parameters[3] # will crash if non-existant + + rules << Rule.from_args parameters + elsif flag == "--redirect" + if parameters.size == 3 + redirections << Redirection.new parameters[1], parameters[2] + elsif parameters.size == 5 + raise "--redirect with 4 parameters not implemented, yet" + else + raise "--redirect [ ]" + end + else + raise "oh no" + end + end + end +end + diff --git a/src/pongc.cr b/src/pongc.cr new file mode 100644 index 0000000..06813f3 --- /dev/null +++ b/src/pongc.cr @@ -0,0 +1,35 @@ +require "ipc" + +client = IPC::Client.new("pong") + +# client.send(LibIPC::MessageType::Data, 42, "salut ça va ?") +client.send(42.to_u8, "salut ça va ?") + +# # client.send(LibIPC::MessageType::Data, 42, "salut ça va ?") +# m = client.read +# +# puts "message received: #{m.to_s}" +# +# sleep 1 +# +# # client.send(LibIPC::MessageType::Data, 42, "salut ça va ?") +# client.send(42.to_u8, "autre truc") + +# # client.send(LibIPC::MessageType::Data, 42, "salut ça va ?") +# m = client.read +# +# puts "message received: #{m.to_s}" +# +# sleep 1 +# +# client.close + +client.loop do |event| + case event + when IPC::Event::Message + puts "\033[32mthere is a message\033[00m" + puts event.message.to_s + client.close + exit + end +end diff --git a/src/pongd.cr b/src/pongd.cr new file mode 100644 index 0000000..8b7f078 --- /dev/null +++ b/src/pongd.cr @@ -0,0 +1,15 @@ +require "ipc" +require "./colors" + +IPC::Service.new ("pong") do |event| + case event + when IPC::Event::Connection + puts "#{CBLUE}IPC::Event::Connection#{CRESET}, client: #{event.connection.fd}" + when IPC::Event::Disconnection + puts "#{CBLUE}IPC::Event::Disconnection#{CRESET}, client: #{event.connection.fd}" + when IPC::Event::Message + puts "#{CGREEN}IPC::Event::Message#{CRESET}" + puts event.message.to_s + event.connection.send event.message + end +end diff --git a/src/rules.cr b/src/rules.cr new file mode 100644 index 0000000..ee447b5 --- /dev/null +++ b/src/rules.cr @@ -0,0 +1,85 @@ + +class Redirection + property origin : String + property destination : String + + property originurl : String | Nil + property destinationurl : String | Nil + + def initialize (@origin, @destination) + end + + def initialize (@origin, @destination, @originurl, @destinationurl) + end + + def to_s + "Redirection #{origin} #{destination}" + # if @originurl + # "#{origin} #{destination}" + # else + # "#{origin} #{destination} #{originurl} #{destinationurl}" + # end + end +end + +class RedirectionSet < Array(Redirection) + def to_s + map(&.to_s).join("\n") + end +end + +class Rule + enum Type + Allow + Deny + end + + enum Direction + In + Out + end + + getter service : String + getter url : Regex + getter type : Type + getter direction : Direction + + def initialize (@type, @direction, @service, @url) + end + + def matches_service?(service : String) + @service == "*" || @service == service + end + + def matches_uri?(uri) + !@url.match(uri).nil? + end + + def self.from_args(args : Array(String)) + Rule.new( + Type.parse(args[0][2..]), + Rule::Direction.parse(args[1]), + args[2], + Regex.new args[3] + ) + end + + def to_s + "#{type} #{direction} #{service} #{url}" + end +end + +class RuleSet < Array(Rule) + def authorized?(direction : Rule::Direction, service : String, uri : String) : Bool + self.select(&.direction.==(direction)) + .select(&.matches_service?(service)) + .select(&.matches_uri?(uri)) + .[0]?.try &.type.allow? || false + end + + def to_s + map(&.to_s).join("\n") + end +end + + diff --git a/src/tcp.cr b/src/tcp.cr new file mode 100644 index 0000000..bd0f8ca --- /dev/null +++ b/src/tcp.cr @@ -0,0 +1,51 @@ +require "ipc" +require "option_parser" + + +service_name = "tcp" +port_to_listen = 1234 +hostname = "localhost" +requested_service_name = "pong" + +OptionParser.parse! do |parser| + parser.on "-p port", "--port port", "Port to listen on." do |port| + port_to_listen = port.to_u16 + end + + parser.on "-h hostname", "--host-name hostname", "Hostname." do |name| + hostname = name + end + + parser.on "-s service-name", "--service-name service-name", "Service name." do |name| + service_name = name + end + + parser.on "-r requested-service-name", "--requested-service-name requested-service-name", "Requested service name." do |name| + requested_service_name = name + end + + parser.on "-h", "--help", "Show this help" do + puts parser + exit 0 + end +end + + +service = IPC::Client.new service_name + +# 1. send service name +service.send 1.to_u8, "tcp://#{hostname}:#{port_to_listen}/#{requested_service_name}" + +# 2. receive "OK" +message = service.read +puts "message read: #{String.new message.payload}" + +# 3. sending a message to the pong service +puts "sending 'coucou' to the pong service" +service.send 2.to_u8, "coucou" + +# 4. receiving a response +message = service.read +puts "message read: #{String.new message.payload}" + +service.close diff --git a/src/tcpd.cr b/src/tcpd.cr new file mode 100644 index 0000000..3bdf392 --- /dev/null +++ b/src/tcpd.cr @@ -0,0 +1,129 @@ +require "option_parser" +require "ipc" +require "socket" +require "./colors" + +class WrappedTCPFileDescriptor < TCPSocket + # do not close the connection when garbage collected!! + def finalize + # puts "WrappedTCPFileDescriptor garbage collection!!" + # super + end +end + +service_name = "tcp" +port_to_listen = 1234 + +OptionParser.parse! do |parser| + parser.on "-p port", "--port port", "Port to listen on." do |port| + port_to_listen = port.to_u16 + end + + parser.on "-s service-name", "--service-name service-name", "Service name." do |name| + service_name = name + end + + parser.on "-h", "--help", "Show this help" do + puts parser + exit 0 + end +end + + +fdlist_client = [] of TCPSocket +fdlist = [] of TCPSocket + +server = TCPServer.new("localhost", port_to_listen) +service = IPC::SwitchingService.new service_name +service << server.fd + +service.loop do |event| + # TODO: remove closed tcp connections + fdlist_client.select do |x| ! x.closed? end + case event + when IPC::Event::Connection + puts "#{CBLUE}IPC::Event::Connection#{CRESET}" + when IPC::Event::Disconnection + puts "#{CBLUE}IPC::Event::Disconnection#{CRESET}" + when IPC::Event::ExtraSocket + puts "#{CBLUE}IPC::Event::ExtraSocket#{CRESET}" + + if server.fd == event.connection.fd + client = server.accept + fdlist << client + service << client.fd + puts "#{CBLUE}new client: #{client.fd}#{CRESET}" + next + end + + # since it's an external communication + # we have to read the message here, it's not handled by default in libipc + client = WrappedTCPFileDescriptor.new(fd: event.connection.fd, family: Socket::Family::INET) + message = client.gets + if message.nil? + # disconnection + puts "#{CBLUE}disconnection of client #{event.connection.fd}#{CRESET}" + service.remove_fd event.connection.fd + fdlist.select do |x| x.fd != event.connection.fd end + client.close + else + message.chomp + puts "#{CORANGE}message from client #{client.fd} (#{message.size} bytes): #{message}#{CRESET}" + begin + requested_service = message + newservice = IPC::Connection.new requested_service + service << newservice.fd + service.switch.add event.connection.fd, newservice.fd + client << "OK\n" + rescue e + puts "#{CRED}Exception during connection to the service: #{e}#{CRESET}" + end + # client << message + end + when IPC::Event::Switch + puts "\033[36mIPC::Event::Switch#{CRESET}: from fd #{event.connection.fd}" + + # IPC::Event::Message has to be the last entry + # because ExtraSocket and Switch inherit from Message class + when IPC::Event::Message + puts "#{CBLUE}IPC::Event::Message#{CRESET}: #{event.connection.fd}" + puts "\033[33mconnection to the service: #{String.new event.message.payload}\033[00m" + + begin + # should be in the format: service-name IP port + payload = String.new event.message.payload + uri = URI.parse payload.chomp + host = uri.host + port = uri.port + host ||= "localhost" + port ||= 9000 + + requested_service = uri.path.lchop + + newservice = TCPSocket.new(host, port) + puts "sending the requested service to the remote tcpd: #{requested_service}" + newservice << "#{requested_service}\n" + + puts "waiting for a response from the remote tcpd" + response = newservice.gets + if response.nil? + raise "#{CRED}No response from the tcpd server#{CRESET}" + end + response.chomp + puts "#{CGREEN}response from the remote tcpd: #{CRESET}#{response}" + + # TODO: when to remove this? This has to happen, memory leak otherwise + # XXX: hint, check after a select for all dead connections + fdlist_client << newservice + + # newservice = IPC::Connection.new requested_service + service << newservice.fd + service.switch.add event.connection.fd, newservice.fd + # service.switch.print + event.connection.send 1.to_u8, "OK" + rescue e + puts "\033[31mException: #{e}" + event.connection.close + end + end +end