├── .gitignore ├── .ruby-gemset ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── lib └── net │ ├── openvpn.rb │ └── openvpn │ ├── client_config.rb │ ├── errors.rb │ ├── generators │ └── keys │ │ ├── authority.rb │ │ ├── base.rb │ │ ├── client.rb │ │ ├── directory.rb │ │ ├── properties.rb │ │ └── server.rb │ ├── host.rb │ ├── parser │ └── server_config.rb │ ├── server.rb │ └── version.rb ├── net-openvpn.gemspec └── spec ├── lib └── net │ └── openvpn │ ├── generators │ └── keys │ │ ├── client_spec.rb │ │ ├── directory_spec.rb │ │ └── properties_spec.rb │ └── host_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg 2 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | net-openvpn 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.1.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in net-openvpn.gemspec 4 | gemspec -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | net-openvpn (0.8.7) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | diff-lcs (1.2.5) 10 | fakefs (0.5.0) 11 | rake (10.1.1) 12 | rspec (2.14.1) 13 | rspec-core (~> 2.14.0) 14 | rspec-expectations (~> 2.14.0) 15 | rspec-mocks (~> 2.14.0) 16 | rspec-core (2.14.7) 17 | rspec-expectations (2.14.5) 18 | diff-lcs (>= 1.1.3, < 2.0) 19 | rspec-mocks (2.14.5) 20 | serialport (1.3.0) 21 | 22 | PLATFORMS 23 | ruby 24 | 25 | DEPENDENCIES 26 | bundler (~> 1.5) 27 | fakefs 28 | net-openvpn! 29 | rake 30 | rspec 31 | serialport 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net-Openvpn 2 | 3 | Net-Openvpn is a gem for configuring a local OpenVPN installation. 4 | 5 | ## Requirements 6 | 7 | You will need these packages. 8 | 9 | * openvpn 10 | * easy-rsa 11 | 12 | You can install these on Debian based systems by running this command: 13 | 14 | ```sh 15 | apt-get install openvpn easy-rsa 16 | ``` 17 | 18 | ### Easy RSA 19 | 20 | Easy RSA is only needed for key generation, so if you are not doing any of that then you don't need to worry. 21 | 22 | Sometimes `easy-rsa` is packaged with `openvpn` so if you can't find the `easy-rsa` package anywhere have a 23 | look in `/usr/share/doc/openvpn/examples` for the `easy-rsa` folder. 24 | 25 | ```sh 26 | sudo cp /usr/share/doc/openvpn/examples/easy-rsa/2.0 /usr/local/easy-rsa 27 | ``` 28 | 29 | You could also clone the `release/2.x` branch from the `easy-rsa` repo at `https://github.com/OpenVPN/easy-rsa.git` then copy the `easy-rsa/2.0` folder to wherever you want. 30 | 31 | ```sh 32 | git clone https://github.com/OpenVPN/easy-rsa.git -b release/2.x 33 | sudo cp easy-rsa/easy-rsa/2.0 /usr/local/easy-rsa 34 | ``` 35 | 36 | Then you can just globally override the property for `:easy_rsa` to specify the location of the scripts folder (see below). 37 | 38 | ## Usage 39 | 40 | ### Server configuration 41 | 42 | Modifying the config for a server (config file will be called `auckland-office.conf`): 43 | 44 | ```ruby 45 | server = Net::Openvpn.server("auckland-office") 46 | server.set :port, 1194 47 | server.save 48 | ``` 49 | 50 | ### Host Configuration (read: client-config-directive) 51 | 52 | **Technically this is a client**, and I should have named it `Client` instead of `Host`, but I don't want to break existing apps using this gem. So I aliased `Net::Openvpn::Client` to `Net::Openvpn::Host` so you can use the former. However, objects returned by initialization will still be of the type `Net::Openvpn::Host`. 53 | 54 | This is how you set the IP address of a VPN host with the hostname `optimus`: 55 | 56 | ```ruby 57 | host = Net::Openvpn.host("optimus") 58 | host.ip = 10.8.0.24 59 | host.network = 10.8.0.0 60 | host.save 61 | ``` 62 | 63 | You can also use a ActiveModel kind of initialization to allow you to create a host in one fell swoop: 64 | 65 | ```ruby 66 | Net::Openvpn::Host.new("optimus", ip: "10.8.0.10", network: "10.8.0.0").save 67 | ``` 68 | 69 | This would create a file at `/etc/openvpn/ccd/optimus` containing the following: 70 | 71 | ``` 72 | ifconfig-push 10.8.0.24 10.8.0.0 73 | ``` 74 | 75 | So that any host connecting to the VPN with a hostname of `optimus` get assigned `10.8.0.24`. 76 | 77 | There are also some other handy methods on the host object: 78 | 79 | ```ruby 80 | host.file # where is the file kept? 81 | host.remove # get rid of the host (delete the file) 82 | host.exist? # does the file exist? 83 | host.new? # has it been saved yet? 84 | host.ip # what is the ip of this host 85 | host.network # what is the network of this host 86 | ``` 87 | 88 | ## Generating certificates and keys 89 | 90 | **WARNING: This functionality is a little bit experimental at the moment, I am sure there are bugs present which would be found with more specs.** 91 | 92 | The goal is to build these generators into the Host (read: Client) and Server classes above so you can do something like `server.generate_keys!`. 93 | 94 | ### Default key properties 95 | 96 | You will probably need to set key properties when generating keys. There are some 97 | defaults already set and they can be seen by calling `Properties#default` (but are listed here for brevity): 98 | 99 | ```ruby 100 | > Net::Openvpn::Generators::Keys::Properties.default 101 | { 102 | :easy_rsa => "/usr/share/easy-rsa", 103 | :openssl => "openssl", 104 | :pkcs11tool => "pkcs11-tool", 105 | :grep => "grep", 106 | :key_dir => "/etc/openvpn/keys", 107 | :key_dir_owner => "root", 108 | :key_dir_group => "root", 109 | :key_dir_permission => "700", 110 | :pkcs11_module_path => "changeme", 111 | :pkcs11_pin => 1234, 112 | :key_size => 1024, 113 | :ca_expire => 3650, 114 | :key_expire => 3650, 115 | :key_country => "US", 116 | :key_province => "CA", 117 | :key_city => "SanFrancisco", 118 | :key_org => "Fort-Funston", 119 | :key_email => "me@myhost.mydomain", 120 | :key_cn => "changeme", 121 | :key_name => "changeme", 122 | :key_ou => "changeme", 123 | :key_config => "/usr/share/easy-rsa/openssl-1.0.0.cnf", 124 | :key_index => "/etc/openvpn/keys/index.txt" 125 | } 126 | ``` 127 | 128 | ### Overriding key properties globally 129 | 130 | Key properties can be overidden by creating the file: `/etc/openvpn/props.yml` 131 | 132 | In this way you can override the default `openssl.cnf` file, the location of your 133 | `easy-rsa` folder, key size or any properties listed above! 134 | 135 | ```yml 136 | --- 137 | :easy_rsa: /usr/share/doc/openvpn/examples/easy-rsa/2.0 138 | :key_config: /path/to/openssl.cnf 139 | :key_dir: /path/to/generated/keys 140 | ``` 141 | 142 | Properties set in the YAML file will override the default ones. You can see which properties are specified in the YAML file by checking the `Properties#yaml`. 143 | 144 | ```ruby 145 | > Net::Openvpn::Generators::Keys::Properties.default 146 | { 147 | :easy_rsa => "/usr/share/doc/openvpn/examples/easy-rsa/2.0", 148 | :key_config => "/path/to/openssl.cnf", 149 | :key_dir => "/path/to/generated/keys" 150 | } 151 | ``` 152 | 153 | But really you should use `Net::Openvpn#props` to get properties because that will merge the defaults with the properties from the YAML file, the latter overriding keys in the former. 154 | 155 | ### Overriding key properties at generation time 156 | 157 | You can also provide key properties when you do the actual generation of the keys as 158 | described below. These properties will override properties set in the YAML file. 159 | 160 | These properties can be supplied directly to the `Authority`, `Client`, `Server` and `Directory` classes in the `Generators::Keys` module as arguments to the `new` method: 161 | 162 | ```ruby 163 | Net::Openvpn::Generators::Keys::Directory.new key_dir_permission: 0770 164 | 165 | Net::Openvpn::Generators::Keys::Authority.new key_size: 8192 # this will take hours lol 166 | 167 | Net::Openvpn::Generators::Keys::Client.new( 168 | "fred", 169 | key_country: "Switzerland", 170 | key_province: "Romandy", 171 | key_city: "Geneva", 172 | key_email: "fred@example.com" 173 | ) 174 | 175 | Net::Openvpn::Generators::Keys::Server.new( 176 | "norvpn01", 177 | key_country: "Norway", 178 | key_province: "Ostlandet", 179 | key_city: "Oslo", 180 | key_email: "admin@example.com" 181 | ) 182 | ``` 183 | 184 | ### Key Directory 185 | 186 | To start with the first thing you will need to do is setup the key directory. This can be done with the following line: 187 | 188 | ```ruby 189 | # keys = Net::Openvpn.generator(:directory).new 190 | key_dir = Net::Openvpn::Generators::Keys::Directory.new 191 | key_dir.generate 192 | key_dir.exist? # check that it worked 193 | ``` 194 | 195 | This should generate the following files/folders: 196 | 197 | * /etc/openvpn/keys 198 | * /etc/openvpn/keys/index.txt 199 | * /etc/openvpn/keys/serial 200 | 201 | ### Certificate Authority 202 | 203 | You will also need to generate the certificate authority and DH key like so. 204 | 205 | ```ruby 206 | # keys = Net::Openvpn.generator(:authority).new 207 | ca = Net::Openvpn::Generators::Keys::Authority.new 208 | ca.generate 209 | ca.exist? 210 | ``` 211 | 212 | This should generate the following files/folders: 213 | 214 | * /etc/openvpn/keys/ca.crt 215 | * /etc/openvpn/keys/ca.key 216 | * /etc/openvpn/keys/dh1024.pem 217 | 218 | ### Servers 219 | 220 | ```ruby 221 | # keys = Net::Openvpn.generator(:server).new("swzvpn04") 222 | keys = Net::Openvpn::Generators::Keys::Server.new("swzvpn04") 223 | keys.generate 224 | keys.exist? # returns true if the key files exist 225 | keys.valid? # returns true if the keys are valid in the index 226 | ``` 227 | 228 | This should generate the following files/folders: 229 | 230 | * /etc/openvpn/keys/swzvpn04.key 231 | * /etc/openvpn/keys/swzvpn04.crt 232 | 233 | ### Clients 234 | 235 | ```ruby 236 | # keys = Net::Openvpn.generator(:client).new("fred") 237 | keys = Net::Openvpn::Generators::Keys::Client.new("fred") 238 | keys.generate 239 | keys.exist? # returns true if the key files exist 240 | keys.valid? # returns true if the keys are valid in the index 241 | ``` 242 | 243 | This should generate the following files/folders: 244 | 245 | * /etc/openvpn/keys/fred.key 246 | * /etc/openvpn/keys/fred.crt 247 | 248 | Revoke the keys like so (UNTESTED!): 249 | 250 | ```ruby 251 | keys = Net::Openvpn::Generators::Keys::Client.new("fred") 252 | keys.revoke! 253 | keys.valid? # returns false 254 | ``` 255 | 256 | ## Rails Permissions 257 | 258 | If you are running rails and you want to give the rails user access, you could do it like this: 259 | 260 | ```sh 261 | groupadd openvpn 262 | chown root.openvpn /etc/openvpn -R 263 | chmod ug+rwx /etc/openvpn -R 264 | chmod o-rwx /etc/openvpn -R 265 | cd /etc/openvpn 266 | chmod g-rwx easy-rsa *.key *.crt *.pem 267 | usermod -aG openvpn rails-app-user 268 | ``` 269 | 270 | Then override the following properties in your `/etc/openvpn/props.yml` file: 271 | 272 | ```yaml 273 | --- 274 | :key_dir_group: "openvpn" 275 | :key_dir_permission: 0700 276 | ``` 277 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | require "rspec/core/rake_task" 4 | 5 | RSpec::Core::RakeTask.new 6 | 7 | task :default => :spec 8 | task :test => :spec 9 | 10 | task :console do 11 | begin 12 | # use Pry if it exists 13 | require 'pry' 14 | require 'net/openvpn' 15 | Pry.start 16 | rescue LoadError 17 | require 'irb' 18 | require 'irb/completion' 19 | require 'net/openvpn' 20 | ARGV.clear 21 | IRB.start 22 | end 23 | end 24 | 25 | task :c => :console 26 | -------------------------------------------------------------------------------- /lib/net/openvpn.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'fileutils' 3 | 4 | require 'net/openvpn/server' 5 | require 'net/openvpn/host' 6 | require 'net/openvpn/errors' 7 | require 'net/openvpn/client_config' 8 | require 'net/openvpn/parser/server_config' 9 | 10 | require 'net/openvpn/generators/keys/base' 11 | require 'net/openvpn/generators/keys/directory' 12 | require 'net/openvpn/generators/keys/client' 13 | require 'net/openvpn/generators/keys/server' 14 | require 'net/openvpn/generators/keys/properties' 15 | require 'net/openvpn/generators/keys/authority' 16 | 17 | module Net 18 | module Openvpn 19 | class << self 20 | 21 | def basepath(path="") 22 | path = "/#{path}" unless path.empty? 23 | "/etc/openvpn#{path}" 24 | end 25 | 26 | def ccdpath(path="") 27 | path = "/#{path}" unless path.empty? 28 | basepath "ccd#{path}" 29 | end 30 | 31 | def host(hostname) 32 | Net::Openvpn::Host.new(hostname) 33 | end 34 | 35 | def server(name) 36 | Net::Openvpn::Server.new(name) 37 | end 38 | 39 | def generator(type) 40 | case type 41 | when :client 42 | Net::Openvpn::Generators::Keys::Client 43 | when :server 44 | Net::Openvpn::Generators::Keys::Server 45 | when :directory 46 | Net::Openvpn::Generators::Keys::Directory 47 | when :authority 48 | Net::Openvpn::Generators::Keys::Authority 49 | end 50 | end 51 | 52 | # Returns the default key properties merged with 53 | # the properties stored in /etc/openvpn/props.yml 54 | def props 55 | props = Openvpn::Generators::Keys::Properties 56 | 57 | props.default.merge props.yaml 58 | end 59 | 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/net/openvpn/client_config.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | 3 | module Net 4 | module Openvpn 5 | class ClientConfig 6 | 7 | def initialize(hostname) 8 | @hostname = hostname 9 | load if exists? 10 | end 11 | 12 | def load 13 | ccd = File.read(path) 14 | matches = ccd.match /ifconfig-push ([0-9\.]+) ([0-9\.]+)/ 15 | @ip = matches[1] 16 | @network = matches[2] 17 | end 18 | 19 | def path 20 | Net::Openvpn.ccdpath @hostname 21 | end 22 | 23 | def exists? 24 | File.exists? path 25 | end 26 | 27 | def ip=(ip) 28 | @ip = ip 29 | end 30 | 31 | def network=(network) 32 | @network = network 33 | end 34 | 35 | def validate! 36 | raise ArgumentError, "No IP set!" if @ip.nil? or @ip.empty? 37 | raise ArgumentError, "No network set!" if @network.nil? or @network.empty? 38 | end 39 | 40 | def remove 41 | return true if !File.exist? path 42 | FileUtils.rm path 43 | end 44 | 45 | def save 46 | validate! 47 | 48 | File.open(path, "w") do |f| 49 | f.puts "ifconfig-push #{@ip} #{@network}" 50 | end 51 | end 52 | 53 | end 54 | end 55 | end -------------------------------------------------------------------------------- /lib/net/openvpn/errors.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | module Errors 4 | class KeyGeneration < StandardError; end 5 | class CertificateRevocation < StandardError; end 6 | end 7 | end 8 | end -------------------------------------------------------------------------------- /lib/net/openvpn/generators/keys/authority.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | module Generators 4 | module Keys 5 | class Authority < Base 6 | 7 | def initialize(**props) 8 | super(nil, props) 9 | end 10 | 11 | def generate 12 | @key_dir.exist? or raise Errors::KeyGeneration, "Key directory has not been generated yet" 13 | !exist? or raise Errors::KeyGeneration, "Authority already exists!" 14 | 15 | FileUtils.cd(@props[:easy_rsa]) do 16 | system "#{cli_prop_vars} ./pkitool --initca" 17 | system "#{cli_prop_vars} ./build-dh" 18 | end 19 | 20 | true 21 | end 22 | 23 | def filepaths 24 | [ 25 | "#{@props[:key_dir]}/ca.key", 26 | "#{@props[:key_dir]}/ca.crt", 27 | "#{@props[:key_dir]}/dh#{@props[:key_size]}.pem" 28 | ] 29 | end 30 | 31 | def self.exist? 32 | Authority.new.exist? 33 | end 34 | 35 | end 36 | end 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /lib/net/openvpn/generators/keys/base.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | module Generators 4 | module Keys 5 | class Base 6 | attr_reader :props 7 | 8 | def initialize(name, props) 9 | @name = name 10 | @props = Openvpn.props.merge props 11 | @props[:key_cn] = @name 12 | @key_dir = Directory.new(@props) 13 | 14 | Properties.validate! @props 15 | end 16 | 17 | def generate 18 | raise NotImplementedError 19 | end 20 | 21 | # Returns true if all the generated keys exist or false if not 22 | def exist? 23 | filepaths.each do |file| 24 | return false if !File.exist? file 25 | end 26 | true 27 | end 28 | 29 | # Returns true if the generated keys are valid by checking 30 | # the key index and then checking the pemfile against the crt 31 | # file. 32 | def valid? 33 | return false unless @key_dir.exist? 34 | 35 | # read the index file 36 | m = File.read(Openvpn.props[:key_index]).match /^V.*CN=#{@name}.*$/ 37 | 38 | return false if m.nil? 39 | 40 | # get the pem number and build the paths 41 | pem = m[0].split("\t")[3] 42 | pem_path = "#{Openvpn.props[:key_dir]}/#{pem}.pem" 43 | crt_path = "#{Openvpn.props[:key_dir]}/#{@name}.crt" 44 | 45 | # Check the pem against the current cert for the name 46 | File.read(pem_path) == File.read(crt_path) 47 | end 48 | 49 | # Revokes the keys 50 | # 51 | # Returns true if the keys were revoked or false if the keys do 52 | # not exist or are not valid 53 | # 54 | # raises `Net::Openvpn::Errors::CertificateRevocation` if the key 55 | # failed to be revoked 56 | # 57 | def revoke! 58 | return false unless exist? and valid? 59 | 60 | FileUtils.cd(Openvpn.props[:easy_rsa]) do 61 | output = %x[#{cli_prop_vars} ./revoke-full #{@name}] 62 | raise Errors::CertificateRevocation, "Revoke command failed" if !output.include? "error 23" # error 23 means key was revoked 63 | end 64 | 65 | !valid? or raise Errors::CertificateRevocation, "Certificates were still valid after being revoked" 66 | 67 | true 68 | end 69 | 70 | private 71 | 72 | # Generates the variable string of key properties 73 | # to preceed easy-rsa script calls 74 | # 75 | # An example with just two properties: 76 | # 77 | # EASY_RSA="/usr/share/easy-rsa" KEY_CN="fred" build-key ... 78 | # 79 | def cli_prop_vars 80 | Properties.to_cli_vars(@props) 81 | end 82 | 83 | def filepaths 84 | raise NotImplementedError 85 | end 86 | 87 | end 88 | end 89 | end 90 | end 91 | end -------------------------------------------------------------------------------- /lib/net/openvpn/generators/keys/client.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | module Generators 4 | module Keys 5 | class Client < Base 6 | 7 | def initialize(name, **props) 8 | super(name, props) 9 | end 10 | 11 | # Generates the certificates for a VPN client 12 | # 13 | # Raises `Net::Openvpn::Errors::KeyGeneration` if there were problems 14 | # 15 | # Returns true if the generation was successful 16 | # 17 | def generate 18 | @key_dir.exist? or raise Errors::KeyGeneration, "Key directory has not been generated yet" 19 | Authority.exist? or raise Errors::KeyGeneration, "Certificate Authority has not been created" 20 | 21 | revoke! if valid? 22 | 23 | FileUtils.cd(@props[:easy_rsa]) do 24 | system "#{cli_prop_vars} ./pkitool #{@name}" 25 | end 26 | 27 | exist? or raise Errors::KeyGeneration, "Keys do not exist" 28 | valid? or raise Errors::KeyGeneration, "keys are not valid" 29 | 30 | true 31 | end 32 | 33 | # Returns an array containing the paths to the generated keys 34 | def filepaths 35 | [ key, certificate ] 36 | end 37 | 38 | def certificate 39 | "#{@props[:key_dir]}/#{@name}.crt" 40 | end 41 | 42 | def key 43 | "#{@props[:key_dir]}/#{@name}.key" 44 | end 45 | 46 | end 47 | end 48 | end 49 | end 50 | end -------------------------------------------------------------------------------- /lib/net/openvpn/generators/keys/directory.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | module Generators 4 | module Keys 5 | class Directory 6 | 7 | def initialize(**props) 8 | @props = Openvpn.props.merge props 9 | end 10 | 11 | def exist? 12 | File.directory?(@props[:key_dir]) and 13 | File.exist?(@props[:key_index]) and 14 | File.exist?("#{@props[:key_dir]}/serial") 15 | end 16 | 17 | # Sets up the directory where keys are to be generated. 18 | # Also creates the serial and index.txt used by the pkitool 19 | # that comes with easy-rsa 20 | def generate 21 | 22 | FileUtils.mkdir_p @props[:key_dir] unless 23 | File.directory? @props[:key_dir] 24 | 25 | FileUtils.cd(@props[:key_dir]) do 26 | FileUtils.touch @props[:key_index] 27 | File.open("serial", "w") {|f| f.write "01" } 28 | end 29 | 30 | FileUtils.chown_R( 31 | @props[:key_dir_owner], 32 | @props[:key_dir_group], 33 | @props[:key_dir] 34 | ) 35 | 36 | FileUtils.chmod_R( 37 | @props[:key_dir_permission], 38 | @props[:key_dir] 39 | ) 40 | 41 | end 42 | 43 | end 44 | end 45 | end 46 | end 47 | end -------------------------------------------------------------------------------- /lib/net/openvpn/generators/keys/properties.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | module Generators 4 | module Keys 5 | module Properties 6 | class << self 7 | 8 | # Returns the properties loaded from a YAML file 9 | # located in /etc/openvpn/props.yml 10 | def yaml 11 | return {} unless File.exist? Openvpn.basepath "props.yml" 12 | YAML.load(File.read(Openvpn.basepath "props.yml")) 13 | end 14 | 15 | # Returns the default set of properties as per the easy-rsa 16 | # 'vars' script 17 | def default 18 | props = { 19 | easy_rsa: "/usr/share/easy-rsa", 20 | openssl: "openssl", 21 | pkcs11tool: "pkcs11-tool", 22 | grep: "grep", 23 | key_dir: "#{Openvpn.basepath}/keys", 24 | key_dir_owner: "root", 25 | key_dir_group: "root", 26 | key_dir_permission: 0700, 27 | pkcs11_module_path: "dummy", 28 | pkcs11_pin: "dummy", 29 | key_size: 1024, 30 | ca_expire: 3650, 31 | key_expire: 3650, 32 | key_country: "US", 33 | key_province: "CA", 34 | key_city: "SanFrancisco", 35 | key_org: "Fort-Funston", 36 | key_email: "me@myhost.mydomain", 37 | key_cn: "changeme", 38 | key_name: "changeme", 39 | key_ou: "changeme", 40 | pkcs11_module_path: "changeme", 41 | pkcs11_pin: 1234 42 | } 43 | 44 | props[:key_config] = "#{props[:easy_rsa]}/openssl-1.0.0.cnf" 45 | props[:key_index] = "#{props[:key_dir]}/index.txt" 46 | 47 | props 48 | end 49 | 50 | alias_method :defaults, :default # POLS 51 | 52 | # Ensures that all the required properties are available to 53 | # stop the easy-rsa scripts having a cry 54 | def validate!(props) 55 | 56 | end 57 | 58 | # Creates a list of variables to preceed a bash command 59 | def to_cli_vars(props) 60 | string = "" 61 | props.each do |key, value| 62 | prop = key.to_s.upcase 63 | string+= "#{prop}=\"#{value}\" " 64 | end 65 | "export #{string}; " 66 | end 67 | 68 | end 69 | end 70 | end 71 | end 72 | end 73 | end -------------------------------------------------------------------------------- /lib/net/openvpn/generators/keys/server.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | module Generators 4 | module Keys 5 | class Server < Base 6 | 7 | def initialize(name, **props) 8 | super(name, props) 9 | end 10 | 11 | def generate 12 | @key_dir.exist? or raise Errors::KeyGeneration, "Key directory has not been generated yet" 13 | Authority.exist? or raise Errors::KeyGeneration, "Certificate Authority has not been created" 14 | 15 | revoke! if valid? 16 | 17 | FileUtils.cd(@props[:easy_rsa]) do 18 | system "#{cli_prop_vars} ./pkitool --server #{@name}" 19 | end 20 | 21 | exist? or raise Openvpn::Errors::KeyGeneration, "Keys do not exist" 22 | valid? or raise Openvpn::Errors::KeyGeneration, "keys are not valid" 23 | 24 | end 25 | 26 | # Returns an array containing the paths to the generated keys 27 | def filepaths 28 | [ key, certificate ] 29 | end 30 | 31 | def certificate 32 | "#{@props[:key_dir]}/#{@name}.crt" 33 | end 34 | 35 | def key 36 | "#{@props[:key_dir]}/#{@name}.key" 37 | end 38 | 39 | end 40 | end 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /lib/net/openvpn/host.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | class Host 4 | 5 | attr_accessor :ip, :network 6 | attr_reader :hostname 7 | alias_method :name, :hostname 8 | 9 | def initialize(hostname, **params) 10 | @hostname = hostname 11 | @config = Net::Openvpn::ClientConfig.new(@hostname) 12 | 13 | params.each do |key, value| 14 | self.send("#{key}=".to_sym, value) 15 | end 16 | end 17 | 18 | def generate_key 19 | 20 | end 21 | 22 | def generate_ovpn 23 | 24 | end 25 | 26 | def file 27 | @config.path 28 | end 29 | 30 | def path 31 | @config.path 32 | end 33 | 34 | def save 35 | @config.ip = ip 36 | @config.network = network 37 | @config.save 38 | end 39 | 40 | def remove 41 | @config.remove 42 | end 43 | 44 | def new? 45 | !@config.exists? 46 | end 47 | 48 | def exist? 49 | @config.exists? 50 | end 51 | 52 | end 53 | end 54 | end 55 | 56 | Net::Openvpn::Client = Net::Openvpn::Host -------------------------------------------------------------------------------- /lib/net/openvpn/parser/server_config.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | module Parser 4 | module ServerConfig 5 | class << self 6 | 7 | def parse(text) 8 | config = {} 9 | 10 | text.each_line do |line| 11 | next if line =~ /^$/ 12 | parts = line.split(" ") 13 | key = parts.first 14 | value = parts[1..parts.size].join(" ") 15 | config[key.to_sym] = value 16 | end 17 | 18 | config 19 | end 20 | 21 | def generate(config) 22 | text = "" 23 | config.each do |key, value| 24 | text.concat "#{key} #{value}\n" 25 | end 26 | text 27 | end 28 | 29 | end 30 | end 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /lib/net/openvpn/server.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | class Server 4 | 5 | def initialize(name) 6 | @name = name 7 | load if exists? 8 | end 9 | 10 | def load 11 | @config = Net::Openvpn::Parser::ServerConfig.parse(File.read(path)) 12 | end 13 | 14 | def get(key) 15 | @config[key] 16 | end 17 | 18 | def set(key, value) 19 | @config[key] = value 20 | end 21 | 22 | def path 23 | Net::Openvpn.basepath "#{@name}.conf" 24 | end 25 | 26 | def exists? 27 | File.exists? path 28 | end 29 | 30 | def save 31 | text = Net::Openvpn::Parser::ServerConfig.generate(@config) 32 | File.open(path, "w") do |f| 33 | f.puts text 34 | end 35 | end 36 | 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /lib/net/openvpn/version.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | module Openvpn 3 | VERSION = "0.8.7" 4 | end 5 | end -------------------------------------------------------------------------------- /net-openvpn.gemspec: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'net/openvpn/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "net-openvpn" 8 | spec.version = Net::Openvpn::VERSION 9 | spec.authors = ["Robert McLeod"] 10 | spec.email = ["robert@penguinpower.co.nz"] 11 | spec.description = %q{Net-Openvpn is an openvpn library for configuring a local OpenVPN service} 12 | spec.summary = %q{Local OpenVPN configurator} 13 | spec.homepage = "https://github.com/penguinpowernz/net-openvpn" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.require_paths = ["lib"] 18 | 19 | spec.add_development_dependency "bundler", "~> 1.5" 20 | spec.add_development_dependency "rake" 21 | spec.add_development_dependency "rspec" 22 | spec.add_development_dependency "fakefs" 23 | spec.add_development_dependency "serialport" 24 | 25 | end 26 | 27 | -------------------------------------------------------------------------------- /spec/lib/net/openvpn/generators/keys/client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Net::Openvpn::Generators::Keys::Client do 4 | subject(:client) { Net::Openvpn::Generators::Keys::Client.new(name, props) } 5 | let(:directory) { Net::Openvpn::Generators::Keys::Directory.new } 6 | let(:authority) { Net::Openvpn::Generators::Keys::Authority } 7 | let(:name) { "test" } 8 | let(:props) { {} } 9 | 10 | before(:each) { setup_filesystem(:tmp) } 11 | after(:each) { destroy_filesystem(:tmp) } 12 | 13 | it "should set the CN to the name" do 14 | expect(client.props[:key_cn]).to eq name 15 | end 16 | 17 | context "when a client has not been generated" do 18 | it "should not exist" do 19 | expect(client).to_not exist 20 | end 21 | 22 | it "should not be valid" do 23 | expect(client).to_not be_valid 24 | end 25 | end 26 | 27 | context "when the key directory has not been generated" do 28 | 29 | it "should raise an error" do 30 | expect { client.generate }.to raise_error( 31 | Net::Openvpn::Errors::KeyGeneration, 32 | "Key directory has not been generated yet" 33 | ) 34 | end 35 | 36 | end 37 | 38 | context "when the key directory has been generated" do 39 | before(:each) { directory.generate } 40 | 41 | context "and the authority has not been generated" do 42 | before(:each) { expect(authority).to_not exist } 43 | 44 | it "should raise an error" do 45 | expect { client.generate }.to raise_error( 46 | Net::Openvpn::Errors::KeyGeneration, 47 | "Certificate Authority has not been created" 48 | ) 49 | end 50 | end 51 | 52 | context "and the authority has been generated" do 53 | before(:each) do 54 | authority.new.generate 55 | expect(authority).to exist 56 | end 57 | 58 | context "and the client has not been generated" do 59 | it "should not exist" do 60 | expect(client).to_not exist 61 | end 62 | 63 | it "should not be valid" do 64 | expect(client).to_not be_valid 65 | end 66 | end 67 | 68 | context "and the client has been generated" do 69 | before(:each) { client.generate } 70 | 71 | it "should exist" do 72 | expect(client).to exist 73 | end 74 | 75 | it "should be valid" do 76 | expect(client).to be_valid 77 | end 78 | 79 | it "should allow revocation" do 80 | expect(client.revoke!).to be_true 81 | expect(client).to_not be_valid 82 | end 83 | end 84 | end 85 | 86 | end 87 | end -------------------------------------------------------------------------------- /spec/lib/net/openvpn/generators/keys/directory_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Net::Openvpn::Generators::Keys::Directory, fakefs: true do 4 | subject(:dir) { Net::Openvpn::Generators::Keys::Directory.new(props) } 5 | let(:props) { {} } 6 | 7 | before(:each) { setup_filesystem(:fake) } 8 | 9 | it "should exist after generation" do 10 | dir.generate 11 | expect(dir).to exist 12 | end 13 | 14 | it "should create the key dir folder" do 15 | dir.generate 16 | expect(File.exist? "/etc/openvpn/keys").to be_true 17 | end 18 | 19 | context "when the key dir property is changed" do 20 | let(:props) { { key_dir: "/etc/openvpn/my_keys", key_index: "/etc/openvpn/my_keys/index.txt" } } 21 | it "should create the key dir folder" do 22 | dir.generate 23 | expect(File.exist? "/etc/openvpn/my_keys").to be_true 24 | end 25 | end 26 | 27 | end -------------------------------------------------------------------------------- /spec/lib/net/openvpn/generators/keys/properties_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Net::Openvpn::Generators::Keys::Properties, fakefs: true do 4 | subject(:properties) { Net::Openvpn::Generators::Keys::Properties } 5 | subject(:defaults) { properties.defaults } 6 | subject(:yaml) { properties.yaml } 7 | 8 | context "when there is no props.yml file" do 9 | it "should have key size of 1024 from defaults" do 10 | expect(defaults[:key_size]).to eq 1024 11 | end 12 | 13 | it "should have empty hash from yaml" do 14 | expect(yaml).to be_empty 15 | end 16 | end 17 | 18 | context "when there is a props.yml file" do 19 | before(:each) { setup_filesystem(:fake) } 20 | 21 | it "should have key size of 384 from yaml" do 22 | expect(yaml[:key_size]).to eq 384 23 | end 24 | 25 | it "should have key size of 1024 from defaults" do 26 | expect(defaults[:key_size]).to eq 1024 27 | end 28 | end 29 | end -------------------------------------------------------------------------------- /spec/lib/net/openvpn/host_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'fileutils' 3 | 4 | describe Net::Openvpn::Host, fakefs: true do 5 | let(:hostname) { "test" } 6 | let(:network) { "10.8.0.0" } 7 | let(:ip) { "10.8.0.10" } 8 | subject(:host) { Net::Openvpn::Host.new(hostname, ip: ip, network: network) } 9 | 10 | before(:each) do 11 | FileUtils.mkdir_p("/etc/openvpn/ccd") 12 | end 13 | 14 | it "should create a host client configuration" do 15 | expect(host).to_not exist 16 | host.save 17 | expect(host).to exist 18 | end 19 | 20 | it "should remove a host client configuration" do 21 | host.save 22 | expect(host).to exist 23 | host.remove 24 | expect(host).to_not exist 25 | end 26 | 27 | it "should have the correct info in the CCD file" do 28 | host.save 29 | expect(File.read host.file).to include ip 30 | expect(File.read host.file).to include network 31 | end 32 | 33 | it "should be new when it hasn't been saved" do 34 | expect(host).to be_new 35 | end 36 | 37 | it "should not be new when it has been saved" do 38 | host.save 39 | expect(host).to_not be_new 40 | end 41 | 42 | it "should assign the ip" do 43 | ip = "10.10.0.10" 44 | host.ip = ip 45 | expect(host.ip).to eq ip 46 | end 47 | 48 | it "should assign the network" do 49 | network = "10.10.0.0" 50 | host.network = network 51 | expect(host.network).to eq network 52 | end 53 | 54 | it "should save the ip to the file" do 55 | host.save 56 | ip = "10.10.0.10" 57 | host.ip = ip 58 | host.save 59 | expect(File.read host.file).to include ip 60 | end 61 | 62 | it "should save the network to the file" do 63 | host.save 64 | network = "10.10.0.0" 65 | host.network = network 66 | host.save 67 | expect(File.read host.file).to include network 68 | end 69 | 70 | it "should return the name of the host" do 71 | expect(host.name).to eq hostname 72 | end 73 | 74 | it "should return the ip of the host" do 75 | expect(host.ip).to eq ip 76 | end 77 | 78 | it "should return the network of the host" do 79 | expect(host.network).to eq network 80 | end 81 | 82 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'net/openvpn' 2 | require 'fakefs/spec_helpers' 3 | require 'yaml' 4 | 5 | RSpec.configure do |config| 6 | config.include FakeFS::SpecHelpers, fakefs: true 7 | end 8 | 9 | def setup_overrides(path) 10 | 11 | overrides = { 12 | key_dir_owner: ENV["USER"], 13 | key_dir_group: ENV["USER"], 14 | key_permission: 0770, 15 | key_size: 384 # tiny key to speed up the tests 16 | } 17 | 18 | File.open("#{path}/props.yml", "w") { |f| f.write overrides.to_yaml } 19 | 20 | end 21 | 22 | def stub_basepath 23 | Net::Openvpn.stub(:basepath) do |path| 24 | path = "/#{path}" unless path.nil? 25 | "/tmp/openvpn#{path}" 26 | end 27 | end 28 | 29 | def setup_filesystem(type) 30 | case type 31 | when :fake 32 | FakeFS::FileSystem.clone "/usr/share/easy-rsa" 33 | FileUtils.mkdir_p "/etc/openvpn" 34 | setup_overrides "/etc/openvpn" 35 | when :tmp 36 | FileUtils.mkdir_p "/tmp/openvpn" 37 | setup_overrides "/tmp/openvpn" 38 | stub_basepath 39 | end 40 | end 41 | 42 | def destroy_filesystem(type) 43 | FileUtils.rm_rf "/tmp/openvpn" 44 | end --------------------------------------------------------------------------------