catflap 0.0.2 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'catflap'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require 'pry'
11
+ Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'catflap/version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = 'catflap'
8
+ s.version = Catflap::VERSION
9
+ s.summary = 'Manage NetFilter-based rules to grant port access on-demand ' \
10
+ 'commandline or REST API requests.'
11
+ s.description = 'A simple solution to provide on-demand service access (e.g.
12
+ port 80 on webserver), where a more robust and secure VPN solution is not
13
+ available. Essentially, it is a more user-friendly form of "port knocking".
14
+ The original proof-of-concept implementation was run for almost three years
15
+ by Demotix, to protect development and staging servers from search engine
16
+ crawlers and other unwanted traffic.'
17
+ s.authors = ['Nyk Cowham']
18
+ s.email = 'nykcowham@gmail.com'
19
+ s.homepage = 'https://github.com/nyk/catflap'
20
+ s.files = `git ls-files -z`
21
+ .split("\x0")
22
+ .reject { |f| f.match(%r{^(test|spec|tasks|features)/}) }
23
+ s.executables = ['catflap']
24
+ s.licenses = ['MIT']
25
+ s.requirements = 'NetFilters (iptables) installed and working.'
26
+ s.require_paths = ['lib']
27
+ s.add_dependency 'json', '>= 1.8.3'
28
+ s.add_development_dependency 'bundler', '~> 1.11'
29
+ s.add_development_dependency 'rake', '~> 10.0'
30
+ s.add_development_dependency 'rspec', '~> 3.0'
31
+ s.bindir = 'bin'
32
+ end
@@ -0,0 +1,30 @@
1
+ # This is an example of the main configuration file for catflap.
2
+ # By default catflap looks for this file in /usr/local/etc/catflap/config.yaml
3
+ # but you can change the location by using the --config-file <filepath> option
4
+ # of the bin/catflap command line interface.
5
+
6
+ server:
7
+ listen_addr: '0.0.0.0' # what ip address the catflap server should listen on.
8
+ port: 8778 # the TCP port that the catflap server listens to.
9
+ docroot: './ui' # you can override the ui location.
10
+ endpoint: '/catflap' # the endpoint for the REST API.
11
+ passfile: './etc/passfile.yaml' # pass phrases are stored here.
12
+ token_ttl: 15 # expire tokens after 15 seconds.
13
+ pid_path: '/var/run' # The path where the pid file should be written.
14
+
15
+ https:
16
+ port: 4773 # TCP oport that catflap https server listens to.
17
+ force: true # Force HTTP requests to redirect to HTTPS.
18
+ certificate: '' # Path to your SSL certificate file.
19
+ private_key: '' # Private key for your SSL certificate.
20
+
21
+ firewall:
22
+ plugin: 'netfilter' # options are netfilter or iptables
23
+ dports: '80,443' # lock multiple ports separating them by commas.
24
+ options: # options are specific to each firewall plugin driver.
25
+ chain: 'CATFLAP' # Namespace for the chains (e.g. CATFLAP-ALLOW, CATFLAP-DENY).
26
+ forward:
27
+ 80: 8778
28
+ 443: 4773
29
+ log_rejected: true
30
+ accept_local: true # this is set to false only when devs are testing catflap.
@@ -0,0 +1,89 @@
1
+ ### BEGIN INIT INFO
2
+ # Provides: catflap
3
+ # Default-Start: 2 3 4 5
4
+ # Default-Stop: 0 1 6
5
+ # Short-Description: Catflap daemon to provide API web service.
6
+ # Description: Provides a means to request and gain access
7
+ # to certain restricted ports like 80 and 443
8
+ ### END INIT INFO
9
+
10
+ # Using the lsb functions to perform the operations.
11
+ . /lib/lsb/init-functions
12
+ # Process name ( For display )
13
+ NAME=catflap
14
+ # Daemon name, where is the actual executable
15
+ DAEMON=/usr/local/bin/catflap
16
+
17
+ # pid file for the daemon
18
+ PIDFILE=/var/run/catflap.pid
19
+
20
+ # Include catflap defaults if available
21
+ if [ -f /etc/default/catflap ] ; then
22
+ . /etc/default/catflap
23
+ fi
24
+
25
+ # If the daemon is not there, then exit.
26
+ test -x $DAEMON || exit 5
27
+
28
+ case $1 in
29
+ start)
30
+ # Checked the PID file exists and check the actual status of process
31
+ if [ -e $PIDFILE ]; then
32
+ status_of_proc -p $PIDFILE $DAEMON "$NAME process" && status="0" || status="$?"
33
+ # If the status is SUCCESS then don't need to start again.
34
+ if [ $status = "0" ]; then
35
+ exit # Exit
36
+ fi
37
+ fi
38
+ # Start the daemon.
39
+ log_daemon_msg "Starting the process" "$NAME"
40
+ # Start the daemon with the help of start-stop-daemon
41
+ # Log the message appropriately
42
+ if start-stop-daemon --start --quiet --oknodo --background --make-pidfile --pidfile $PIDFILE --exec $DAEMON -- $DAEMON_OPTS " start" 2>&1; then
43
+ log_end_msg 0
44
+ else
45
+ log_end_msg 1
46
+ fi
47
+ ;;
48
+ stop)
49
+ # Stop the daemon.
50
+ if [ -e $PIDFILE ]; then
51
+ status_of_proc -p $PIDFILE $DAEMON "Stoppping the $NAME process" && status="0" || status="$?"
52
+ if [ "$status" = 0 ]; then
53
+ start-stop-daemon --stop --quiet --oknodo --pidfile $PIDFILE
54
+ /bin/rm -rf $PIDFILE
55
+ fi
56
+ else
57
+ log_daemon_msg "$NAME process is not running"
58
+ log_end_msg 0
59
+ fi
60
+ ;;
61
+ restart)
62
+ # Restart the daemon.
63
+ $0 stop && sleep 2 && $0 start
64
+ ;;
65
+ status)
66
+ # Check the status of the process.
67
+ if [ -e $PIDFILE ]; then
68
+ status_of_proc -p $PIDFILE $DAEMON "$NAME process" && exit 0 || exit $?
69
+ else
70
+ log_daemon_msg "$NAME Process is not running"
71
+ log_end_msg 0
72
+ fi
73
+ ;;
74
+ reload)
75
+ # Reload the process. Basically sending some signal to a daemon to reload
76
+ # it configurations.
77
+ if [ -e $PIDFILE ]; then
78
+ start-stop-daemon --stop --signal USR1 --quiet --pidfile $PIDFILE --name $NAME
79
+ log_success_msg "$NAME process reloaded successfully"
80
+ else
81
+ log_failure_msg "$PIDFILE does not exists"
82
+ fi
83
+ ;;
84
+ *)
85
+ # For invalid arguments, print the usage message.
86
+ echo "Usage: $0 {start|stop|restart|reload|status}"
87
+ exit 2
88
+ ;;
89
+ esac
@@ -0,0 +1,7 @@
1
+ # This is a sample passfile. It is recommened that you use your own
2
+ # pass phrases. A pass phrase must consist of at least two words separated by
3
+ # a space. The first word of the phrase is used as a unique key for efficient
4
+ # lookup of multiple pass phrases.
5
+ passphrases:
6
+ frisky: 'frisky kitten'
7
+ peeky: 'peeky blinders'
@@ -1,87 +1,131 @@
1
+ require 'catflap/version'
1
2
  require 'yaml'
2
- require 'ipaddr'
3
+ require 'digest'
3
4
 
5
+ ##
6
+ # Catflap controller class
7
+ #
8
+ # This class controls the initialization of the configuration file, setting
9
+ # options, initializing the firewall plugin driver and loading pass phrases.
10
+ #
11
+ # @author Nyk Cowham <nykcowham@gmail.com>
4
12
  class Catflap
13
+ # @!attribute noop
14
+ # @return [Boolean] true if no operation is to be performed.
15
+ # @!attribute verbose
16
+ # @return [Boolean] true if command output should be written to stdout.
17
+ # @!attribute [r] fwplugin
18
+ # @return [String] the name of the firewall driver plugin file and class.
19
+ # @!attribute [r] bind_addr
20
+ # @return [String] the IP address the Catflap server should listen to.
21
+ # @!attribute [r] port
22
+ # @return [Integer] the port number the Catflap server should listen to.
23
+ # @!attribute [r] docroot
24
+ # @return [String] file path location that HTML/CSS/JS files are located.
25
+ # @!attribute [r] endpoint
26
+ # @return [String] the name of the REST service endpoint (e.g. /catflap).
27
+ # @!attribute [r] https
28
+ # @return [Hash<Symbol, String>] the HTTPS server configuration options.
29
+ # @!attribute [r] daemonize
30
+ # @return [Boolean] true if the web server should run in the background.
31
+ # @!attribute [r] dports
32
+ # @return [String] comma-separated list of ports to guard. (e.g. '80,443').
33
+ # @!attribute [r] passphrases
34
+ # @return [Hash<String, String>] associative array of pass phrases. The
35
+ # key is the first word of the pass phrase where words are separated by
36
+ # a space or non-alphanumeric character (except for the underscore).
37
+ # @!attribute [r] passfile
38
+ # @return [String] file path of the YAML file containg pass phrases.
39
+ # @!attribute [r] token_ttl
40
+ # @return [Integer] the time-to-live in seconds before tokens expire.
41
+ # @!attribute [r] pid_path
42
+ # @return [String] path/directory where pid file should be written.
43
+ # @!attribute [r] redirect_url
44
+ # @return [String] URL browser should be redirect to after authentication.
45
+ # @!attribute [r] firewall
46
+ # @return [FirewallPlugin]
47
+ # a firewall object that is implemented by the firewall driver plugin.
5
48
 
6
- attr_accessor :config, :chain, :port, :dports, :print, :noop, :log_rejected
49
+ attr_accessor :verbose, :noop, :daemonize
7
50
 
8
- def initialize config_file
9
- config_file = config_file || '/usr/local/etc/catflap.yaml'
10
- @config = {}
11
- @config = YAML.load_file config_file if File.readable? config_file
12
- @port = @config['server']['port'] || 4777
13
- @chain = @config['rules']['chain'] || 'catflap-accept'
14
- @dports = @config['rules']['dports'] || '80,443'
15
- @print = false
16
- @noop = false
17
- @log_rejected = true
18
- end
51
+ attr_reader :fwplugin, :listen_addr, :port, :docroot, :endpoint, :dports,
52
+ :passfile, :passphrases, :token_ttl, :pid_path, :redirect_url,
53
+ :firewall, :https
19
54
 
20
- def install_rules!
21
- output = "iptables -N #{@chain}\n" # Create a new user-defined chain as a container for our catflap netfilter rules
22
- output << "iptables -A #{@chain} -s 127.0.0.1 -p tcp -m multiport --dports #{@dports} -j ACCEPT\n" # Accept packets to localhost
23
- output << "iptables -A INPUT -p tcp -m multiport --dports #{@dports} -j #{@chain}\n" # Jump from INPUT chain to the catflap chain
24
- output << "iptables -A INPUT -p tcp -m multiport --dports #{@dports} -j LOG\n" if @log_rejected # Log any rejected packets to /var/log/messages
25
- output << "iptables -A INPUT -p tcp -m multiport --dports #{@dports} -j DROP\n" # Drop any other packets to the ports on the INPUT chain
26
- execute! output
55
+ # @param [String, nil] file_path file path of the YAML configuration file.
56
+ # @param [Boolean] noop set to true to suppress destructive operations.
57
+ # @param [Boolean] verbose set to true to print operations to stdout stream.
58
+ # @return [Catflap]
59
+ def initialize(file_path = nil, noop = false, verbose = false)
60
+ @noop = noop
61
+ @verbose = verbose
62
+ initialize_config(file_path)
63
+ initialize_firewall_plugin
64
+ load_passphrases
27
65
  end
28
66
 
29
- def uninstall_rules!
30
- output = "iptables -D INPUT -p tcp -m multiport --dports #{@dports} -j #{@chain}\n" # Remove user-defined chain from INPUT chain
31
- output << "iptables -F #{@chain}\n" # Flush the catflap user-defined chain
32
- output << "iptables -X #{@chain}\n" # Remove the catflap chain
33
- output << "iptables -D INPUT -p tcp -m multiport --dports #{@dports} -j LOG\n" # Remove the logging rule
34
- output << "iptables -D INPUT -p tcp -m multiport --dports #{@dports} -j DROP\n" # Remove the packet dropping rule
35
- execute! output
36
- end
67
+ # Initialize the configuration options from the YAML configuration file.
68
+ # @param [String, nil] file_path file path of the YAML configuration file.
69
+ # @return void
70
+ def initialize_config(file_path = nil)
71
+ @config = read_config_file(file_path)
72
+ alias_server_attributes
37
73
 
38
- def purge_rules!
39
- output = "iptables -F #{@chain}"
40
- execute! output
74
+ @https ||= {}
41
75
  end
42
76
 
43
- def list_rules
44
- system "iptables -S #{@chain}"
77
+ # Alias the server configuration attributes to instance variables.
78
+ def alias_server_attributes
79
+ @config['server'].each do |key, value|
80
+ instance_variable_set('@' + key, value)
81
+ end
45
82
  end
46
83
 
47
- def check_address ip
48
- check_user_input ip
49
- return system "iptables -C #{@chain} -s #{ip} -p tcp -m multiport --dports #{@dports} -j ACCEPT\n"
50
- end
84
+ # Find YAML configuration file, load and read it.
85
+ # @param [String, nil] file_path if one was passed on command line.
86
+ # @return [Hash] the parsed YAML file as a nested hash.
87
+ def read_config_file(file_path)
88
+ # Look for config file in order of precedence.
89
+ unless file_path
90
+ ['~/.catflap', '/usr/local/etc/catflap', '/etc/catflap'].each do |path|
91
+ file = File.expand_path(path + '/config.yaml')
92
+ if File.readable? file
93
+ file_path = file
94
+ break
95
+ end
96
+ end
97
+ end
51
98
 
52
- def add_address! ip
53
- check_user_input ip
54
- output = "iptables -I #{@chain} 1 -s #{ip} -p tcp -m multiport --dports #{@dports} -j ACCEPT\n"
55
- execute! output
99
+ YAML.load_file(file_path || './etc/config.yaml')
56
100
  end
57
101
 
58
- def delete_address! ip
59
- check_user_input ip
60
- output = "iptables -D #{@chain} -s #{ip} -p tcp -m multiport --dports #{@dports} -j ACCEPT\n"
61
- execute! output
62
- end
102
+ # Initialize the firewall plugin driver.
103
+ # @return [FirewallPlugin] an object of a class inheriting from FirewallPlugin
104
+ def initialize_firewall_plugin
105
+ plugin = @config['firewall']['plugin']
106
+ driver = plugin.capitalize + 'Driver'
107
+ require_relative "catflap/plugins/firewall/#{plugin}.rb"
108
+ @firewall =
109
+ Object.const_get(driver).new(@config, @noop, @verbose)
110
+ end
63
111
 
64
- def add_addresses_from_file! filepath
65
- if File.readable? filepath
66
- output = ""
67
- File.open(filepath, "r").each_line do |ip|
68
- check_user_input ip
69
- output << "iptables -I #{@chain} 1 -s #{ip.chomp} -p tcp -m multiport --dports #{@dports} -j ACCEPT\n"
70
- end
71
- execute! output
112
+ # Load the pass phrase YAML file.
113
+ # @raise [IOError] if the file is missing or not readable.
114
+ # @return void
115
+ def load_passphrases
116
+ if @passfile && File.readable?(@passfile)
117
+ phrases = YAML.load_file(@passfile)
118
+ @passphrases = phrases['passphrases']
72
119
  else
73
- puts "The file #{filepath} is not readable!"
74
- exit 1
120
+ raise IOError, "Cannot read the passfile: #{@passfile}!"
75
121
  end
76
122
  end
77
123
 
78
- def execute! output
79
- if @print then puts output end
80
- system output unless @noop
81
- end
82
-
83
- def check_user_input suspect
84
- return IPAddr.new(suspect)
124
+ # Generates a SHA256 encrypted token based on a passphrase and a timestamp
125
+ # @param [String] pass the passphrase stored in passfile.
126
+ # @param [String] timestamp to salt the digest.
127
+ # @return [String] a token in the form of a SHA256 digest of a string.
128
+ def generate_token(pass, salt)
129
+ Digest::SHA256.hexdigest(pass + salt)
85
130
  end
86
-
87
131
  end
@@ -0,0 +1,102 @@
1
+ require 'catflap'
2
+ require 'catflap/http'
3
+
4
+ ##
5
+ # Command controller class.
6
+ #
7
+ # This class separates the implementation details of the command line
8
+ # interface from the actual interface. This allows for creating custom
9
+ # command tools by directly implementing this class.
10
+ # @author Nyk Cowham <nykcowham@gmail.com>
11
+ class CfCommand
12
+ include CfWebserver
13
+
14
+ # Initialize a new CatflapCli object
15
+ # @param [Hash<String, String>] options an associative array of options
16
+ # read from the configuration file built by Catflap::initialize_config().
17
+ # @return CatflapCli
18
+ # @see
19
+ # Catflap - options are generated from file: Catflap::initialize_config()
20
+ def initialize(options)
21
+ @options = options
22
+ @cf = Catflap.new(
23
+ options[:config_file], options[:noop], options[:verbose]
24
+ )
25
+ @cf.daemonize = @options[:daemonize]
26
+ end
27
+
28
+ # A handler function to dispatch commands received from the front-end to the
29
+ # firewall driver class or the Catflap web service.
30
+ # @param [String] command the command that is to be executed.
31
+ # @param [String] arg and argument for the command, (e.g. an IP address).
32
+ # @return void
33
+ # @raise ArgumentError when a required command argument is missing.
34
+ # @raise NameError when the command is not recognized.
35
+ def dispatch_commands(command, arg)
36
+ # handle commands and options.
37
+ case command
38
+ when 'version'
39
+ "Catflap version #{Catflap::VERSION}"
40
+ when 'start'
41
+ server_start(@cf, @options[:https])
42
+ when 'stop'
43
+ server_stop(@cf, @options[:https])
44
+ when 'status'
45
+ server_status(@cf, @options[:https])
46
+ when 'restart'
47
+ begin
48
+ server_stop(@cf, @options[:https])
49
+ server_start(@cf, @options[:https])
50
+ end
51
+ when 'reload'
52
+ @cf.load_passphrases
53
+ when 'purge'
54
+ @cf.firewall.purge_rules
55
+ when 'install'
56
+ @cf.firewall.install_rules
57
+ when 'uninstall'
58
+ @cf.firewall.uninstall_rules
59
+ when 'list'
60
+ puts @cf.firewall.list_rules
61
+ when 'grant'
62
+ raise ArgumentError, 'You must provide a valid IP address' if arg.nil?
63
+ @cf.firewall.add_address(arg) unless @cf.firewall.check_address(arg)
64
+ when 'revoke'
65
+ raise ArgumentError, 'You must provide a valid IP address' if arg.nil?
66
+ @cf.firewall.delete_address(arg) if @cf.firewall.check_address(arg)
67
+ when 'check'
68
+ raise ArgumentError, 'You must provide a valid IP address' unless arg
69
+ return @cf.firewall.check_address(arg)
70
+ when 'bulkload'
71
+ raise ArgumentError, 'You must provide a file path' unless arg
72
+ add_addresses_from_file(arg)
73
+ when nil # catflap --version can be run with no command, so that's ok.
74
+ else
75
+ raise NameError, "there is no command '#{command}'"
76
+ end
77
+ end
78
+ # rubocop:enable Metrics/CyclomaticComplexity,Metrics/MethodLength
79
+ # rubocop:enable Metrics/PerceivedComplexity,Metrics/AbcSize
80
+
81
+ # Handler function to bulkload IP's to the firewall.
82
+ #
83
+ # Checking that the file path points to readable file ensures that we can
84
+ # safely accept the user-submitted parameter without any additional data
85
+ # sanitization.
86
+ # @param [String] filepath path to the bulkload file of IP addresses to add.
87
+ # @return void
88
+ # @raise IOError if the file cannot be found or is not readable.
89
+ #
90
+ # @note Every IP address in the file is validated to ensure that it resolves
91
+ # to a valid IP address.
92
+ # @see Firewall Firewall::assert_valid_ipaddr(ip)
93
+ def add_addresses_from_file(filepath)
94
+ if File.readable? filepath
95
+ File.open(filepath, 'r').each_line do |ip|
96
+ @cf.firewall.add_address(ip.chomp)
97
+ end
98
+ else
99
+ raise IOError, "The file #{filepath} is not readable!"
100
+ end
101
+ end
102
+ end