commit 02b1acc0405e2ddad88e3583ef85d3f4e12f9841
Author: Philippe PITTOLI
Date: Sat Jul 27 15:23:01 2019 +0200
networkd: v0.1
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