Module: Shells::PfSenseCommon

Included in:
PfSenseSerialSession, PfSenseSshSession
Defined in:
lib/shells/pf_sense_common.rb

Overview

Common functionality for interacting with a pfSense device.

Defined Under Namespace

Classes: MenuNavigationFailure, PublicKeyInvalid, PublicKeyNotFound, RestartNow, UserNotFound

Constant Summary collapse

BASE_SHELL =

The base shell used when possible.

'/bin/sh'
PF_SHELL =

The pfSense shell itself.

'/usr/local/sbin/pfSsh.php'
PF_PROMPT =

The prompt in the pfSense shell.

'pfSense shell:'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#pf_sense_hostObject

Gets the hostname of the pfSense device.



63
64
65
# File 'lib/shells/pf_sense_common.rb', line 63

def pf_sense_host
  @pf_sense_host
end

#pf_sense_userObject

Gets the user currently logged into the pfSense device.



59
60
61
# File 'lib/shells/pf_sense_common.rb', line 59

def pf_sense_user
  @pf_sense_user
end

#pf_sense_versionObject

Gets the version of the pfSense firmware.



55
56
57
# File 'lib/shells/pf_sense_common.rb', line 55

def pf_sense_version
  @pf_sense_version
end

Class Method Details

.included(base) ⇒ Object

:nodoc:



70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/shells/pf_sense_common.rb', line 70

def self.included(base)  #:nodoc:

  # Trap the RestartNow exception.
  # When encountered, change the :quit option to '/sbin/reboot'.
  # This requires rewriting the @options instance variable since the hash is frozen
  # after initial validation.
  base.on_exception do |shell, ex|
    if ex.is_a?(Shells::PfSenseCommon::RestartNow)
      shell.send(:change_quit, '/sbin/reboot')
      true
    else
      false
    end
  end

end

Instance Method Details

#apply_filter_configObject

Apply the firewall configuration.

You need to apply the firewall configuration after you make changes to aliases, NAT rules, or filter rules.



201
202
203
204
205
206
207
# File 'lib/shells/pf_sense_common.rb', line 201

def apply_filter_config
  pf_exec(
      'require_once("shaper.inc");',
      'require_once("filter.inc");',
      'filter_configure_sync();'
  )
end

#apply_user_config(user_id) ⇒ Object

Applies the user configuration for the specified user.



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/shells/pf_sense_common.rb', line 211

def apply_user_config(user_id)
  user_id = user_id.to_i
  pf_exec(
      'require_once("auth.inc");',
      "$user_entry = $config[\"system\"][\"user\"][#{user_id}];",
      '$user_groups = array();',
      'foreach ($config["system"]["group"] as $gidx => $group) {',
      '  if (is_array($group["member"])) {',
      "    if (in_array(#{user_id}, $group[\"member\"])) { $user_groups[] = $group[\"name\"]; }",
      '  }',
      '}',
      # Intentionally run set_groups before and after to ensure group membership gets fully applied.
      'local_user_set_groups($user_entry, $user_groups);',
      'local_user_set($user_entry);',
      'local_user_set_groups($user_entry, $user_groups);'
  )
end

#config_parsed?Boolean

Determines if the configuration has been parsed during this session.

Returns:

  • (Boolean)


165
166
167
# File 'lib/shells/pf_sense_common.rb', line 165

def config_parsed?
  instance_variable_defined?(:@config_parsed) && instance_variable_get(:@config_parsed)
end

#enable_cert_auth(public_key = '~/.ssh/id_rsa.pub') ⇒ Object

Enabled public key authentication for the current pfSense user.

Once this has been done you should be able to connect without using a password.



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/shells/pf_sense_common.rb', line 233

def enable_cert_auth(public_key = '~/.ssh/id_rsa.pub')
  cert_regex = /^ssh-[rd]sa (?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)? \S*$/m

  # get our cert unless the user provided a full cert for us.
  unless public_key =~ cert_regex
    public_key = File.expand_path(public_key)
    if File.exist?(public_key)
      public_key = File.read(public_key).to_s.strip
    else
      raise Shells::PfSenseCommon::PublicKeyNotFound
    end
    raise Shells::PfSenseCommon::PublicKeyInvalid unless public_key =~ cert_regex
  end

  cfg = get_config_section 'system'
  user_id = nil
  user_name = options[:user].downcase
  cfg['user'].each_with_index do |user,index|
    if user['name'].downcase == user_name
      user_id = index

      authkeys = Base64.decode64(user['authorizedkeys'].to_s).gsub("\r\n", "\n").strip
      unless authkeys == '' || authkeys =~ cert_regex
        warn "Existing authorized keys for user #{options[:user]} are invalid and are being reset."
        authkeys = ''
      end

      if authkeys == ''
        user['authorizedkeys'] = Base64.strict_encode64(public_key)
      else
        authkeys = authkeys.split("\n")
        unless authkeys.include?(public_key)
          authkeys << public_key unless authkeys.include?(public_key)
          user['authorizedkeys'] = Base64.strict_encode64(authkeys.join("\n"))
        end
      end

      break
    end
  end


  raise Shells::PfSenseCommon::UserNotFound unless user_id

  set_config_section 'system', cfg, "Enable certificate authentication for #{options[:user]}."

  apply_user_config user_id
end

#exec_prompt(&block) ⇒ Object

:nodoc:



137
138
139
140
141
142
143
144
145
146
# File 'lib/shells/pf_sense_common.rb', line 137

def exec_prompt(&block) #:nodoc:
  debug 'Initializing pfSense shell...'
  exec '/usr/local/sbin/pfSsh.php', command_timeout: 5
  begin
    block.call
  ensure
    debug 'Quitting pfSense shell...'
    send_data 'exit' + line_ending
  end
end

#exec_shell(&block) ⇒ Object

:nodoc:



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/shells/pf_sense_common.rb', line 98

def exec_shell(&block) #:nodoc:
  super do
    # We want to drop to the shell before executing the block.
    # So we'll navigate the menu to get the option for the shell.
    # For this first navigation we allow a delay only if we are not connected to a serial device.
    # Serial connections are always on, so they don't need to initialize first.
    menu_option = get_menu_option 'Shell', !(Shells::SerialSession > self.class)
    raise MenuNavigationFailure unless menu_option

    # For 2.3 and 2.4 this is a valid match.
    # If future versions change the default prompt, we need to change our process.
    # [VERSION][USER@HOSTNAME]/root:  where /root is the current dir.
    shell_regex = /\[(?<VER>[^\]]*)\]\[(?<USERHOST>[^\]]*)\](?<CD>\/.*):\s*$/

    # Now we execute the menu option and wait for the shell_regex to match.
    temporary_prompt(shell_regex) do
      exec menu_option.to_s, command_timeout: 5

      # Once we have a match we should be able to repeat it and store the information from the shell.
      data = prompt_match.match(combined_output)
      self.pf_sense_version = data['VER']
      self.pf_sense_user, _, self.pf_sense_host = data['USERHOST'].partition('@')
    end

    block.call

    # Wait for the shell_regex to match again.
    temporary_prompt(shell_regex) { wait_for_prompt nil, 4, false }

    # Exit the shell to return to the menu.
    send_data 'exit' + line_ending

    # After the block we want to know what the Logout option is and we change the quit command to match.
    menu_option = get_menu_option 'Logout'
    raise MenuNavigationFailure unless menu_option
    change_quit menu_option.to_s
  end
end

#get_config_section(section_name) ⇒ Object

Gets a configuration section from the pfSense device.



171
172
173
174
# File 'lib/shells/pf_sense_common.rb', line 171

def get_config_section(section_name)
  parse_config unless config_parsed?
  JSON.parse pf_exec("echo json_encode($config[#{section_name.to_s.inspect}]);").strip
end

#line_endingObject

:nodoc:



66
67
68
# File 'lib/shells/pf_sense_common.rb', line 66

def line_ending #:nodoc:
  "\n"
end

#parse_configObject

Reloads the pfSense configuration on the device.



158
159
160
161
# File 'lib/shells/pf_sense_common.rb', line 158

def parse_config
  pf_exec 'parse_config(true);'
  @config_parsed = true
end

#pf_exec(*commands) ⇒ Object

Executes a series of commands on the pfSense shell.



150
151
152
153
154
# File 'lib/shells/pf_sense_common.rb', line 150

def pf_exec(*commands)
  ret = ''
  commands.each { |cmd| ret += exec(cmd) }
  ret + exec('exec')
end

#quitObject

Exits the shell session immediately.



292
293
294
295
# File 'lib/shells/pf_sense_common.rb', line 292

def quit
  raise Shells::SessionCompleted if session_complete?
  raise Shells::ShellBase::QuitNow
end

#rebootObject

Exits the shell session immediately and requests a reboot of the pfSense device.



285
286
287
288
# File 'lib/shells/pf_sense_common.rb', line 285

def reboot
  raise Shells::SessionCompleted if session_complete?
  raise Shells::PfSenseCommon::RestartNow
end

#set_config_section(section_name, values, message = '') ⇒ Object

Sets a configuration section to the pfSense device.

Returns the number of changes made to the configuration.



180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
# File 'lib/shells/pf_sense_common.rb', line 180

def set_config_section(section_name, values, message = '')
  current_values = get_config_section(section_name)
  changes = generate_config_changes("$config[#{section_name.to_s.inspect}]", current_values, values)
  if changes&.any?
    if message.to_s.strip == ''
      message = "Updating #{section_name} section."
    end
    changes << "write_config(#{message.inspect});"

    pf_exec(*changes)

    (changes.size - 1)
  else
    0
  end
end

#validate_optionsObject

:nodoc:



87
88
89
90
91
92
93
94
95
96
# File 'lib/shells/pf_sense_common.rb', line 87

def validate_options  #:nodoc:
  super
  options[:shell] = :shell
  options[:prompt] = 'pfSense shell:'
  options[:quit] = 'exit'
  options[:retrieve_exit_code] = false
  options[:on_non_zero_exit_code] = :ignore
  options[:override_set_prompt] = ->(sh) { true }
  options[:override_get_exit_code] = ->(sh) { 0 }
end