bifrossht 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7b03fe4cd0d9018b1ff90518b74d3019217aa67fc5c89225aaa34abb944b83f0
4
+ data.tar.gz: ff8d8cc131c39e524f1fb93638482f79e2a7f3b0db08ce73c754a3b1ab16a336
5
+ SHA512:
6
+ metadata.gz: 7445e28653f35372d83dcc0d25a039bd5d50a1471495eefc7e8b2d0187ff58f003c621528faaba087f80b2638f0231b87c2d362da17b84a40ecadb2589bcaa44
7
+ data.tar.gz: 30fc48521df93f361c5334d943d8f65c3437366d67d184428acec1bcc3c61d2f2926443ae09b31c02e2462205ce45e9ff4cc885532fd4e84f4078fd9308c7ba7
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
@@ -0,0 +1,23 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ bifrossht (0.0.1)
5
+ gli (~> 2)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ gli (2.18.2)
11
+ rake (12.3.3)
12
+ rdoc (6.1.1)
13
+
14
+ PLATFORMS
15
+ ruby
16
+
17
+ DEPENDENCIES
18
+ bifrossht!
19
+ rake (~> 12)
20
+ rdoc (~> 6)
21
+
22
+ BUNDLED WITH
23
+ 1.16.1
@@ -0,0 +1,112 @@
1
+ # bifrossht
2
+
3
+ An auto-routing ssh proxy command.
4
+
5
+ ## What is bifrossht?
6
+
7
+ `bifrossht` is a ssh `ProxyCommand` tool.
8
+
9
+ It could be used to automate the configuration of ssh hopping
10
+ in complex environments.
11
+
12
+ ## How does it work?
13
+
14
+ With the use of `HostFilter` additional lookups and rules can be
15
+ applied to the hostname before connecting:
16
+
17
+ * Additional domain lookups
18
+ * Lookup with hostname prefixes
19
+
20
+ After the hostname lookup `bifrossht` will try to detect the hop
21
+ for connecting to the target server.
22
+
23
+ First it tries to match the hop based on configured filters:
24
+
25
+ * Regex on the Hostname
26
+ * Subnet matching on the ip-address
27
+
28
+ When no explicit configuration has matched it falls back to:
29
+
30
+ * Auto-probing of hops
31
+
32
+ ## Installation
33
+
34
+ `bifrossht` is written in ruby(>= 2.0) and available from rubygems.org:
35
+
36
+ ```
37
+ gem install bifrossht
38
+ ```
39
+
40
+ ## Configuration
41
+
42
+ `bifrossht` must be configured in `~/.bifrossht.yml`:
43
+
44
+ ```yaml
45
+ ---
46
+ host_filters:
47
+ - type: SearchDomain
48
+ domains:
49
+ - cluster-xy.provider.tld
50
+ - internal.provider.tld
51
+ prefixes:
52
+ - vm00
53
+
54
+ connections:
55
+ - name: direct
56
+ type: Exec
57
+ match_addr:
58
+ - "192.168.0.0/24"
59
+ - "192.168.1.0/24"
60
+ parameters:
61
+ timeout: 1
62
+ command: nc %h %p
63
+
64
+ - name: dmz
65
+ type: Exec
66
+ match:
67
+ - "dmz.provider.tld$"
68
+ match_addr:
69
+ - "80.241.212.0/24"
70
+ parameters:
71
+ timeout: 3
72
+ command: ssh -W hop-dmz.internal.provider.tld
73
+
74
+ - name: internet
75
+ type: Exec
76
+ skip_probe: true
77
+ match:
78
+ - "your-server.de$"
79
+ - "contabo.net$"
80
+ - "compute.amazonaws.com$"
81
+ - "google.internal$"
82
+ parameters:
83
+ timeout: 5
84
+ command: proxytunnel -p gateway.internal.provider.tld:3128 -d %h:%p
85
+ ```
86
+
87
+ Then configure the `ProxyCommand` in `~/.ssh/config`:
88
+
89
+ ```
90
+ Host *
91
+ ProxyCommand bifrossht connect -p %p %h
92
+ ```
93
+
94
+ ## Troubleshooting
95
+
96
+ Run the `bifrossht` command standalone and increase log level:
97
+
98
+ ```
99
+ bifrossht -l debug connect host0815.internal.provider.tld
100
+ ```
101
+
102
+ ## Copyright
103
+
104
+ 2019 Markus Benning
105
+
106
+ ## License
107
+
108
+ This program is free software: you can redistribute it and/or modify
109
+ it under the terms of the GNU General Public License as published by
110
+ the Free Software Foundation, either version 3 of the License, or
111
+ (at your option) any later version.
112
+
@@ -0,0 +1,44 @@
1
+ require 'rake/clean'
2
+ require 'rubygems'
3
+ require 'rubygems/package_task'
4
+ require 'rdoc/task'
5
+ require 'cucumber'
6
+ require 'cucumber/rake/task'
7
+ Rake::RDocTask.new do |rd|
8
+ rd.main = "README.rdoc"
9
+ rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
10
+ rd.title = 'Your application title'
11
+ end
12
+
13
+ spec = eval(File.read('bifrossht.gemspec'))
14
+
15
+ Gem::PackageTask.new(spec) do |pkg|
16
+ end
17
+ CUKE_RESULTS = 'results.html'
18
+ CLEAN << CUKE_RESULTS
19
+ desc 'Run features'
20
+ Cucumber::Rake::Task.new(:features) do |t|
21
+ opts = "features --format html -o #{CUKE_RESULTS} --format progress -x"
22
+ opts += " --tags #{ENV['TAGS']}" if ENV['TAGS']
23
+ t.cucumber_opts = opts
24
+ t.fork = false
25
+ end
26
+
27
+ desc 'Run features tagged as work-in-progress (@wip)'
28
+ Cucumber::Rake::Task.new('features:wip') do |t|
29
+ tag_opts = ' --tags ~@pending'
30
+ tag_opts = ' --tags @wip'
31
+ t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty -x -s#{tag_opts}"
32
+ t.fork = false
33
+ end
34
+
35
+ task :cucumber => :features
36
+ task 'cucumber:wip' => 'features:wip'
37
+ task :wip => 'features:wip'
38
+ require 'rake/testtask'
39
+ Rake::TestTask.new do |t|
40
+ t.libs << "test"
41
+ t.test_files = FileList['test/*_test.rb']
42
+ end
43
+
44
+ task :default => [:test,:features]
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bifrossht/app'
4
+
5
+ exit Bifrossht::App.run(ARGV)
@@ -0,0 +1,67 @@
1
+ require 'gli'
2
+
3
+ require 'bifrossht/version.rb'
4
+ require 'bifrossht/errors.rb'
5
+ require 'bifrossht/logger.rb'
6
+ require 'bifrossht/config.rb'
7
+ require 'bifrossht/host_filter.rb'
8
+ require 'bifrossht/connection.rb'
9
+ require 'bifrossht/target.rb'
10
+
11
+ module Bifrossht
12
+ class App
13
+ extend GLI::App
14
+
15
+ program_desc 'SSH auto-routing proxy command'
16
+
17
+ version Bifrossht::VERSION
18
+
19
+ subcommand_option_handling :normal
20
+ arguments :strict
21
+
22
+ desc 'configuration file'
23
+ default_value '~/.bifrossht.yml'
24
+ arg_name 'path'
25
+ flag %i[c config]
26
+
27
+ desc 'log level'
28
+ default_value 'info'
29
+ arg_name 'log_level'
30
+ flag %i[l log_level]
31
+
32
+ desc 'connect to an target'
33
+ arg 'host'
34
+ command :connect do |c|
35
+ c.flag %i[p port], default_value: '22'
36
+ c.action do |_global_options, options, args|
37
+ target = Target.new(args[0], options[:port])
38
+
39
+ HostFilter.apply(target)
40
+ hop = Connection.find(target)
41
+ raise ApplicationError, 'no suitable hop found' unless hop
42
+
43
+ Logger.info("Connecting #{target.host} using #{hop.name}...")
44
+ hop.connect(target)
45
+ end
46
+ end
47
+
48
+ pre do |global, _command, _options, _args|
49
+ Logger.log_level(global[:l])
50
+ Config.load_config(global[:c])
51
+ HostFilter.register_filters(Config.host_filters)
52
+ Connection.register_connections(Config.connections)
53
+ true
54
+ end
55
+
56
+ post do |global, command, options, args|
57
+ # empty
58
+ end
59
+
60
+ on_error do |e|
61
+ raise e unless e.is_a? Bifrossht::Error
62
+
63
+ Logger.error(e.message)
64
+ false
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,41 @@
1
+ require 'bifrossht/config/element'
2
+ require 'bifrossht/config/host_filter'
3
+ require 'bifrossht/config/connection'
4
+
5
+ require 'yaml'
6
+
7
+ module Bifrossht
8
+ class Config
9
+ class << self
10
+ attr_reader :config
11
+
12
+ def load_config(path)
13
+ @config = YAML.load_file(File.expand_path(path)) || {}
14
+ rescue Errno::ENOENT => e
15
+ raise ParameterError, "Configuration file: #{e.message}"
16
+ end
17
+
18
+ def host_filters
19
+ @host_filters ||= build_subconfig('host_filters', Config::HostFilter)
20
+ end
21
+
22
+ def connections
23
+ @connections ||= build_subconfig('connections', Config::Connection)
24
+ end
25
+
26
+ private
27
+
28
+ def build_subconfig(key, klass)
29
+ return [] unless @config.key?(key)
30
+
31
+ params = @config[key]
32
+
33
+ unless params.is_a? Array
34
+ raise ParameterError, "#{key} must be an array"
35
+ end
36
+
37
+ params.map { |c| klass.new(c) }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,45 @@
1
+ module Bifrossht
2
+ class Config
3
+ class Connection < Config::Element
4
+ def initialize(options = {})
5
+ super
6
+
7
+ validate_presence 'type', 'name'
8
+ validate_type 'type', String
9
+ validate_type 'name', String
10
+ validate_boolean 'skip_probe'
11
+ validate_type 'parameters', Hash
12
+ validate_type 'match', Array
13
+ validate_type 'match_addr', Array
14
+ end
15
+
16
+ def type
17
+ @options['type']
18
+ end
19
+
20
+ def name
21
+ @options['name']
22
+ end
23
+
24
+ def skip_probe
25
+ @options['skip_probe'] || false
26
+ end
27
+
28
+ def parameters
29
+ @options['parameters'] || {}
30
+ end
31
+
32
+ def match
33
+ return [] if @options['match'].nil?
34
+
35
+ @options['match'].map { |re| Regexp.new re }
36
+ end
37
+
38
+ def match_addr
39
+ return [] if @options['match_addr'].nil?
40
+
41
+ @options['match_addr'].map { |ip| IPAddr.new ip }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,34 @@
1
+ module Bifrossht
2
+ class Config
3
+ class Element
4
+ def initialize(options = {})
5
+ @options = options
6
+ end
7
+
8
+ private
9
+
10
+ def validate_presence(*options)
11
+ options.each do |name|
12
+ raise ParameterError, "#{self.class.name} is missing parameter #{name}" unless @options.key?(name)
13
+ end
14
+ end
15
+
16
+ def validate_type(option, type)
17
+ return unless @options.key?(option)
18
+
19
+ raise ParameterError, "option #{option} for #{self.class} is not of type #{type}" unless @options[option].is_a? type
20
+ end
21
+
22
+ def validate_enum(option, values)
23
+ return unless @options.key?(option)
24
+
25
+ value = @options[option]
26
+ raise ParameterError, "option #{option} for #{self.class} does not allow value #{value}" unless values.include?(value)
27
+ end
28
+
29
+ def validate_boolean(option)
30
+ validate_enum(option, [true, false])
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,26 @@
1
+ module Bifrossht
2
+ class Config
3
+ class HostFilter < Config::Element
4
+ def initialize(options = {})
5
+ super
6
+
7
+ validate_presence 'type', 'domains'
8
+ validate_type 'type', String
9
+ validate_type 'domains', Array
10
+ validate_type 'prefixes', Array
11
+ end
12
+
13
+ def type
14
+ @options['type']
15
+ end
16
+
17
+ def domains
18
+ @options['domains'] || []
19
+ end
20
+
21
+ def prefixes
22
+ @options['prefixes'] || []
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,57 @@
1
+ require 'bifrossht/connection/base'
2
+ #require 'bifrossht/connection/direct'
3
+ require 'bifrossht/connection/exec'
4
+ #require 'bifrossht/connection/http_proxy'
5
+
6
+ module Bifrossht
7
+ class Connection
8
+ class << self
9
+ attr_reader :connections
10
+
11
+ def register_connections(connections = [])
12
+ connections.each { |c| register_connection(c) }
13
+ end
14
+
15
+ def register_connection(config)
16
+ @connections ||= {}
17
+
18
+ klass = build_class_name(config.type)
19
+ @connections[config.name] = klass.new(config)
20
+ end
21
+
22
+ def find(target)
23
+ c = match(target)
24
+ return c unless c.nil?
25
+
26
+ probe(target)
27
+ end
28
+
29
+ def match(target)
30
+ connections.values.select do |c|
31
+ c.match(target)
32
+ end.first
33
+ end
34
+
35
+ def probe(target)
36
+ connections.values.reject(&:skip_probe).each do |c|
37
+ Logger.debug("probing #{c.name}...")
38
+ return c if c.probe(target)
39
+ end
40
+
41
+ nil
42
+ end
43
+
44
+ def connection(hop)
45
+ @connections[hop]
46
+ end
47
+
48
+ private
49
+
50
+ def build_class_name(type)
51
+ Object.const_get("Bifrossht::Connection::#{type}")
52
+ rescue NameError => e
53
+ raise ParameterError, "Cant load connection: #{e.message}"
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,43 @@
1
+ module Bifrossht
2
+ class Connection
3
+ class Base
4
+ attr_accessor :config
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def name
11
+ config.name
12
+ end
13
+
14
+ def probe(_target)
15
+ raise 'not implemented'
16
+ end
17
+
18
+ def connect(_target)
19
+ raise 'not implemented'
20
+ end
21
+
22
+ def skip_probe
23
+ config.skip_probe
24
+ end
25
+
26
+ def match(target)
27
+ host_matches = config.match.select do |re|
28
+ re.match(target.host)
29
+ end
30
+ return true if host_matches.any?
31
+
32
+ if target.resolvable?
33
+ addr_matches = config.match_addr.select do |net|
34
+ net.include?(target.resolved_ip)
35
+ end
36
+ return true if addr_matches.any?
37
+ end
38
+
39
+ false
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,48 @@
1
+ module Bifrossht
2
+ class Connection
3
+ class Exec < Base
4
+ def probe(target)
5
+ cmd = command(target)
6
+ Logger.debug("probing command: #{cmd}")
7
+ in_r, in_w = IO.pipe
8
+ io = IO.popen(['sh', '-c', cmd, in: in_w, err: '/dev/null'])
9
+ in_w.close
10
+ ready = IO.select([io], nil, nil, timeout)
11
+ if ready
12
+ banner = io.readline
13
+ else
14
+ Logger.debug('probe timed out!')
15
+ end
16
+ Process.kill('TERM', io.pid)
17
+ in_r.close
18
+ io.close
19
+
20
+ return true if banner =~ /^SSH-/
21
+
22
+ false
23
+ rescue EOFError
24
+ false
25
+ end
26
+
27
+ def connect(target)
28
+ cmd = command(target)
29
+ Logger.debug("executing: #{cmd}")
30
+ exec cmd
31
+ end
32
+
33
+ private
34
+
35
+ def command(target)
36
+ command_pattern.gsub('%h', target.host).gsub('%p', target.port.to_s)
37
+ end
38
+
39
+ def command_pattern
40
+ config.parameters['command'] || 'ssh -W %h:%p'
41
+ end
42
+
43
+ def timeout
44
+ (config.parameters['timeout'] || 3).to_i
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,6 @@
1
+ module Bifrossht
2
+ class Error < StandardError; end
3
+
4
+ class ParameterError < Error; end
5
+ class ApplicationError < Error; end
6
+ end
@@ -0,0 +1,38 @@
1
+ require 'bifrossht/host_filter/base'
2
+ require 'bifrossht/host_filter/search_domain'
3
+
4
+ module Bifrossht
5
+ class HostFilter
6
+ class << self
7
+ attr_reader :filters
8
+
9
+ def register_filters(filters = [])
10
+ filters.each { |f| register_filter(f) }
11
+ end
12
+
13
+ def register_filter(config)
14
+ @filters ||= []
15
+
16
+ klass = build_class_name(config.type)
17
+ @filters << klass.new(config)
18
+ end
19
+
20
+ def apply(target)
21
+ filters.each do |filter|
22
+ next unless filter.match(target.host)
23
+
24
+ new_host = filter.apply(target.host)
25
+ target.rewrite(new_host)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def build_class_name(type)
32
+ Object.const_get("Bifrossht::HostFilter::#{type}")
33
+ rescue NameError => e
34
+ raise ParameterError, "Cant load host_filter: #{e.message}"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,19 @@
1
+ module Bifrossht
2
+ class HostFilter
3
+ class Base
4
+ attr_accessor :config
5
+
6
+ def initialize(config)
7
+ @config = config
8
+ end
9
+
10
+ def match(_host)
11
+ false
12
+ end
13
+
14
+ def apply(host)
15
+ host
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ require 'resolv'
2
+
3
+ module Bifrossht
4
+ class HostFilter
5
+ class SearchDomain < Base
6
+ def match(host)
7
+ host !~ /\./
8
+ end
9
+
10
+ def apply(host)
11
+ prefixes = [''] + config.prefixes
12
+
13
+ config.domains.each do |domain|
14
+ prefixes.each do |prefix|
15
+ record = "#{prefix}#{host}.#{domain}"
16
+
17
+ begin
18
+ address = Resolv.getaddress record
19
+ rescue Resolv::ResolvError => e
20
+ Logger.debug "SearchDomain: #{e.message}"
21
+ end
22
+
23
+ unless address.nil?
24
+ Logger.debug "SearchDomain: using #{record}"
25
+ return record
26
+ end
27
+ end
28
+ end
29
+
30
+ host
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,42 @@
1
+ require 'logger'
2
+ require 'forwardable'
3
+
4
+ module Bifrossht
5
+ class Logger
6
+ class << self
7
+ extend Forwardable
8
+
9
+ def_delegators :@logger, :debug, :info, :warn, :error, :fatal
10
+
11
+ def log_level(level = 'warn')
12
+ mylevel = case level
13
+ when 'debug' then ::Logger::DEBUG
14
+ when 'info' then ::Logger::INFO
15
+ when 'warn' then ::Logger::WARN
16
+ when 'error' then ::Logger::ERROR
17
+ when 'fatal' then ::Logger::FATAL
18
+ else
19
+ raise "Unknown log-level #{level}"
20
+ end
21
+
22
+ logger.level = mylevel
23
+ end
24
+
25
+ def logger
26
+ return @logger unless @logger.nil?
27
+
28
+ @logger = ::Logger.new(STDERR)
29
+ @logger.level = ::Logger::INFO
30
+ @logger.formatter = proc do |severity, _datetime, _progname, msg|
31
+ if severity == 'INFO'
32
+ "#{msg}\n"
33
+ else
34
+ "#{severity}: #{msg}\n"
35
+ end
36
+ end
37
+
38
+ @logger
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,65 @@
1
+ require 'ipaddr'
2
+ require 'resolv'
3
+
4
+ module Bifrossht
5
+ class Target
6
+ attr_reader :entries
7
+ attr_reader :port
8
+
9
+ def initialize(host, port)
10
+ @entries = [host]
11
+ @port = port.to_i
12
+ end
13
+
14
+ def rewrite(new)
15
+ return if new == host
16
+
17
+ @entries.push(new)
18
+ end
19
+
20
+ def orig_host
21
+ @entries.first
22
+ end
23
+
24
+ def host
25
+ @entries.last
26
+ end
27
+
28
+ def to_s
29
+ host
30
+ end
31
+
32
+ def ip
33
+ @ip ||= IPAddr.new host
34
+ rescue IPAddr::InvalidAddressError
35
+ nil
36
+ end
37
+
38
+ def ip?
39
+ !ip.nil?
40
+ end
41
+
42
+ def resolved_ip
43
+ @resolved_ip ||= resolve_address
44
+ end
45
+
46
+ def resolvable?
47
+ !resolved_ip.nil?
48
+ end
49
+
50
+ private
51
+
52
+ def resolve_address
53
+ return ip if ip?
54
+
55
+ record = Resolv.getaddress(host)
56
+ return nil unless record
57
+
58
+ IPAddr.new(record)
59
+ rescue Resolv::ResolvError
60
+ nil
61
+ rescue IPAddr::InvalidAddressError
62
+ nil
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,3 @@
1
+ module Bifrossht
2
+ VERSION = '0.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bifrossht
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Markus Benning
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-08-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rdoc
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: gli
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2'
55
+ description:
56
+ email: ich@markusbenning.de
57
+ executables:
58
+ - bifrossht
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - Gemfile
63
+ - Gemfile.lock
64
+ - README.md
65
+ - Rakefile
66
+ - bin/bifrossht
67
+ - lib/bifrossht/app.rb
68
+ - lib/bifrossht/config.rb
69
+ - lib/bifrossht/config/connection.rb
70
+ - lib/bifrossht/config/element.rb
71
+ - lib/bifrossht/config/host_filter.rb
72
+ - lib/bifrossht/connection.rb
73
+ - lib/bifrossht/connection/base.rb
74
+ - lib/bifrossht/connection/exec.rb
75
+ - lib/bifrossht/errors.rb
76
+ - lib/bifrossht/host_filter.rb
77
+ - lib/bifrossht/host_filter/base.rb
78
+ - lib/bifrossht/host_filter/search_domain.rb
79
+ - lib/bifrossht/logger.rb
80
+ - lib/bifrossht/target.rb
81
+ - lib/bifrossht/version.rb
82
+ homepage: https://markusbenning.de
83
+ licenses:
84
+ - GPL-3.0+
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options:
88
+ - "--title"
89
+ - bifrossht
90
+ - "--main"
91
+ - README.md
92
+ - "-ri"
93
+ require_paths:
94
+ - lib
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.7.6
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: SSH auto-routing proxy command
112
+ test_files: []