Skip to content

Commit

Permalink
tested openvpn_client module (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed Mar 12, 2024
1 parent b052bab commit 7f3765c
Show file tree
Hide file tree
Showing 12 changed files with 514 additions and 115 deletions.
179 changes: 91 additions & 88 deletions README.md

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions docs/source/modules/openvpn.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,18 @@ ansibleguy.opnsense.openvpn_client
:header: "Parameter", "Type", "Required", "Default", "Aliases", "Comment"
:widths: 15 10 10 10 10 45

"description","string","true","\-","name, desc","The name used to match this config to existing entries"
"name","string","true","\-","description, desc","The name used to match this config to existing entries"
"remote","string","true","\-","peer, server","Remote host name or IP address with optional port"
"protocol","string","false","udp","proto","One of: 'udp', 'udp4', 'udp6', 'tcp', 'tcp4', 'tcp6'. Use protocol for communicating with remote host."
"port","integer","false","\-","\-","Port number to use. Specifies a bind address, or nobind when client does not have a specific bind address."
"port","integer","false","\-","local_port, bind_port","Port number to use. Specifies a bind address, or nobind when client does not have a specific bind address."
"address","string","false","\-","bind_address, bind, ip","Optional IP address for bind. If specified, OpenVPN will bind to this address only. If unspecified, OpenVPN will bind to all interfaces."
"mode","string","false","tun","type","One of: 'tun', 'tap'. Choose the type of tunnel, OSI Layer 3 [tun] is the most common option to route IPv4 or IPv6 traffic, [tap] offers Ethernet 802.3 (OSI Layer 2) connectivity between hosts and is usually combined with a bridge."
"log_level","integer","false","\-","verbosity, verb","From 0 to 11. Output verbosity level. 0 = no output, 1-4 = normal, 5 = log packets, 6-11 debug"
"keepalive_interval","integer","false","\-","kai","Ping interval in seconds. 0 to disable keep alive"
"keepalive_timeout","integer","false","\-","kat","Causes OpenVPN to restart after n seconds pass without reception of a ping or other packet from remote."
"carp_depend_on","string","false","\-","vip, vip_depend, carp, carp_depend","The CARP VHID to depend on. When this virtual address is not in master state, then the instance will be shutdown."
"certificate","string","true","\-","cert","Certificate to use for this service."
"ca","string","false","\-","certificate_authority, authority","Select a certificate authority when it differs from the attached certificate."
"certificate","string","true if no ca","\-","cert","Certificate to use for this service."
"ca","string","false","true if no certificate","certificate_authority, authority","Select a certificate authority when it differs from the attached certificate."
"tls_key","string","false","\-","tls_static_key","Add an additional layer of HMAC authentication on top of the TLS control channel to mitigate DoS attacks and attacks on the TLS stack. The prefixed mode determines if this measurement is only used for authentication (--tls-auth) or includes encryption (--tls-crypt)."
"authentication","string","false","\-","auth, auth_algo","One of: 'BLAKE2b512', 'BLAKE2s256', 'whirlpool', 'none', 'MD4', 'MD5', 'MD5-SHA1', 'RIPEMD160', 'SHA1', 'SHA224', 'SHA256', 'SHA3-224', 'SHA3-256', 'SHA3-384', 'SHA3-512', 'SHA384', 'SHA512', 'SHA512-224', 'SHA512-256', 'SHAKE128', 'SHAKE256'. Authenticate data channel packets and (if enabled) tls-auth control channel packets with HMAC using message digest algorithm alg."
"username","string","false","\-","user","(optional) Username to send to the server for authentication when required."
Expand Down
6 changes: 6 additions & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ action_groups:
- ansibleguy.opnsense.ids_ruleset
- ansibleguy.opnsense.ids_ruleset_properties
- ansibleguy.opnsense.ids_user_rule
openvpn:
- ansibleguy.opnsense.openvpn_client
- ansibleguy.opnsense.openvpn_server
- ansibleguy.opnsense.openvpn_static_key
- ansibleguy.opnsense.openvpn_overwrite
all:
- metadata:
extend_group:
Expand All @@ -123,6 +128,7 @@ action_groups:
- ansibleguy.opnsense.nat
- ansibleguy.opnsense.system
- ansibleguy.opnsense.ids
- ansibleguy.opnsense.openvpn

plugin_routing:
modules:
Expand Down
58 changes: 45 additions & 13 deletions plugins/module_utils/main/openvpn_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,39 @@
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.api import \
Session
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.helper.main import \
validate_int_fields
validate_int_fields, is_unset, get_key_by_value_from_selection
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.cls import BaseModule


class Client(BaseModule):
FIELD_ID = 'description'
FIELD_ID = 'name'
CMDS = {
'add': 'addItem',
'del': 'delItem',
'set': 'setItem',
'search': 'get',
'toggle': 'toggleItem',
'add': 'add',
'del': 'del',
'set': 'set',
'search': 'search',
'detail': 'get',
'toggle': 'toggle',
}
API_KEY_PATH = 'openvpn.Instances.Instance'
API_KEY_PATH = 'instance'
API_MOD = 'openvpn'
API_CONT = 'instances'
API_CONT_REL = 'service'
API_CMD_REL = 'reconfigure'
FIELDS_CHANGE = []
FIELDS_ALL = ['enabled', FIELD_ID]
FIELDS_CHANGE = [
'remote', 'protocol', 'port', 'address', 'mode', 'log_level', 'keepalive_interval', 'keepalive_timeout',
'carp_depend_on', 'certificate', 'ca', 'tls_key', 'authentication', 'username', 'password', 'renegotiate_time',
'network_local', 'network_remote', 'options', 'mtu', 'fragment_size', 'mss_fix',
]
FIELDS_ALL = ['role', 'enabled', 'vpnid', FIELD_ID]
FIELDS_ALL.extend(FIELDS_CHANGE)
FIELDS_TRANSLATE = {
'name': 'description',
'protocol': 'proto',
'address': 'local',
'mode': 'dev_type',
'log_level': 'verb',
'certificate': 'cert',
'ca': 'ca',
'authentication': 'auth',
'renegotiate_time': 'reneg-sec',
'network_local': 'push_route',
Expand All @@ -46,25 +51,52 @@ class Client(BaseModule):
'list': ['network_local', 'network_remote', 'options'],
'select': [
'certificate', 'ca', 'tls_key', 'authentication', 'carp_depend_on', 'log_level',
'mode', 'protocol',
'mode', 'protocol', 'remote', 'role',
],
'select_opt_list_idx': ['log_level'],
'int': ['fragment_size', 'mtu'],
}
INT_VALIDATIONS = {
'mtu': {'min': 60, 'max': 65535},
'fragment_size': {'min': 0, 'max': 65528},
}
EXIST_ATTR = 'instance'
FIELDS_DIFF_EXCLUDE = ['vpnid']

def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
BaseModule.__init__(self=self, m=module, r=result, s=session)
self.instance = {}

def check(self) -> None:
self.p['role'] = 'client'
self.p['log_level'] = f"o{self.p['log_level']}"

if self.p['state'] == 'present':
validate_int_fields(module=self.m, data=self.p, field_minmax=self.INT_VALIDATIONS)

if is_unset(self.p['remote']):
self.m.fail_json(
"You need to provide a 'remote' target to create an openvpn-client!"
)

if is_unset(self.p['certificate']) and is_unset(self.p['ca']):
self.m.fail_json(
"You need to either provide a 'certificate' or 'ca' to create an openvpn-client!"
)


self._base_check()

if not is_unset(self.p['ca']):
self.p['ca'] = get_key_by_value_from_selection(
selection=self.b.raw['ca'],
value=self.p['ca'],
)

if not is_unset(self.p['certificate']):
self.p['certificate'] = get_key_by_value_from_selection(
selection=self.b.raw[self.FIELDS_TRANSLATE['certificate']],
value=self.p['certificate'],
)

if self.p['state'] == 'present':
self.r['diff']['after'] = self.b.build_diff(data=self.p)
5 changes: 5 additions & 0 deletions plugins/modules/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
'webproxy_acl', 'webproxy_icap', 'webproxy_auth', 'ipsec_connection', 'ipsec_pool',
'ipsec_child', 'ipsec_vti', 'ipsec_auth_local', 'ipsec_auth_remote', 'frr_general', 'unbound_general',
'unbound_acl', 'ids_general', 'ids_policy', 'ids_rule', 'ids_ruleset', 'ids_user_rule', 'ids_policy_rule',
'openvpn_instance',
]


Expand Down Expand Up @@ -348,6 +349,10 @@ def run_module():
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.ids_policy_rule import \
Rule as Target_Obj

elif target == 'openvpn_instance':
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.openvpn_client import \
Client as Target_Obj

except AttributeError:
module_dependency_error()

Expand Down
16 changes: 8 additions & 8 deletions plugins/modules/openvpn_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
def run_module():
module_args = dict(
# general
description=dict(
type='str', required=True, default='', aliases=['desc', 'name'],
name=dict(
type='str', required=True, aliases=['desc', 'description'],
description='The name used to match this config to existing entries'
),
remote=dict(
type='str', required=True, aliases=['peer', 'server'],
type='str', required=False, aliases=['peer', 'server'],
description='Remote host name or IP address with optional port'
),
protocol=dict(
Expand All @@ -40,7 +40,7 @@ def run_module():
description='Use protocol for communicating with remote host.'
),
port=dict(
type='str', required=False, default='',
type='str', required=False, default='', aliases=['local_port', 'bind_port'],
description='Port number to use.'
'Specifies a bind address, or nobind when client does not have a specific bind address.'
),
Expand Down Expand Up @@ -78,7 +78,7 @@ def run_module():
),
# trust
certificate=dict(
type='str', required=True, default='', aliases=['cert'],
type='str', required=False, aliases=['cert'],
description='Certificate to use for this service.'
),
ca=dict(
Expand All @@ -95,7 +95,7 @@ def run_module():
authentication=dict(
type='str', required=False, default='', aliases=['auth', 'auth_algo'],
choices=[
'BLAKE2b512', 'BLAKE2s256', 'whirlpool', 'none',
'', 'BLAKE2b512', 'BLAKE2s256', 'whirlpool', 'none',
'MD4', 'MD5', 'MD5-SHA1', 'RIPEMD160', 'SHA1', 'SHA224', 'SHA256', 'SHA3-224', 'SHA3-256',
'SHA3-384', 'SHA3-512', 'SHA384', 'SHA512', 'SHA512-224', 'SHA512-256', 'SHAKE128', 'SHAKE256',
],
Expand All @@ -120,12 +120,12 @@ def run_module():
),
# routing
network_local=dict(
type='list', elements='str', required=False, default=[], aliases=['local', 'net_local', 'push_route'],
type='list', elements='str', required=False, default=[], aliases=['net_local', 'push_route'],
description='These are the networks accessible on this host, these are pushed via route{-ipv6} '
'clauses in OpenVPN to the client.'
),
network_remote=dict(
type='list', elements='str', required=False, default=[], aliases=['remote', 'net_remote', 'route'],
type='list', elements='str', required=False, default=[], aliases=['net_remote', 'route'],
description='Remote networks for the server, add route to routing table after connection is established'
),
# misc
Expand Down
130 changes: 128 additions & 2 deletions plugins/modules/openvpn_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,139 @@

def run_module():
module_args = dict(
description=dict(type='str', required=False, default='', aliases=['desc']),
port=dict(type='int', required=False, default=1194),
# general
name=dict(
type='str', required=True, aliases=['desc', 'description'],
description='The name used to match this config to existing entries'
),
remote=dict(
type='str', required=False, aliases=['peer', 'server'],
description='Remote host name or IP address with optional port'
),
protocol=dict(
type='str', required=False, default='udp', aliases=['proto'],
choices=['udp', 'udp4', 'udp6', 'tcp', 'tcp4', 'tcp6'],
description='Use protocol for communicating with remote host.'
),
port=dict(
type='str', required=False, default='', aliases=['local_port', 'bind_port'],
description='Port number to use.'
'Specifies a bind address, or nobind when client does not have a specific bind address.'
),
address=dict(
type='str', required=False, default='', aliases=['bind_address', 'ip', 'bind'],
description='Optional IP address for bind.'
'If specified, OpenVPN will bind to this address only.'
'If unspecified, OpenVPN will bind to all interfaces.'
),
mode=dict(
type='str', required=False, default='tun', aliases=['type'], choices=['tun', 'tap'],
description='Choose the type of tunnel, OSI Layer 3 [tun] is the most common option '
'to route IPv4 or IPv6 traffic, [tap] offers Ethernet 802.3 (OSI Layer 2) connectivity '
'between hosts and is usually combined with a bridge.'
),
log_level=dict(
type='int', required=False, default=3, aliases=['verbosity', 'verb'],
choices=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
description='Output verbosity level. 0 = no output, 1-4 = normal, 5 = log packets, 6-11 debug'
),
keepalive_interval=dict(
type='str', required=False, default='', aliases=['kai'],
description='Ping interval in seconds. 0 to disable keep alive'
),
keepalive_timeout=dict(
type='str', required=False, default='', aliases=['kat'],
description='Causes OpenVPN to restart after n seconds pass without reception of a '
'ping or other packet from remote.'
),
carp_depend_on=dict(
aliases=['vip', 'vip_depend', 'carp', 'carp_depend'],
type='str', required=False, default='',
description='The carp VHID to depend on, when this virtual address is not in '
'master state, the interface cost will be set to the demoted cost'
),
# trust
certificate=dict(
type='str', required=False, aliases=['cert'],
description='Certificate to use for this service.'
),
ca=dict(
type='str', required=False, default='', aliases=['certificate_authority', 'authority'],
description='Select a certificate authority when it differs from the attached certificate.'
),
tls_key=dict(
type='str', required=False, default='', aliases=['tls_static_key'],
description='Add an additional layer of HMAC authentication on top of the TLS control channel to '
'mitigate DoS attacks and attacks on the TLS stack. The prefixed mode determines if '
'this measurement is only used for authentication (--tls-auth) or includes encryption '
'(--tls-crypt).'
),
authentication=dict(
type='str', required=False, default='', aliases=['auth', 'auth_algo'],
choices=[
'', 'BLAKE2b512', 'BLAKE2s256', 'whirlpool', 'none',
'MD4', 'MD5', 'MD5-SHA1', 'RIPEMD160', 'SHA1', 'SHA224', 'SHA256', 'SHA3-224', 'SHA3-256',
'SHA3-384', 'SHA3-512', 'SHA384', 'SHA512', 'SHA512-224', 'SHA512-256', 'SHAKE128', 'SHAKE256',
],
description='Authenticate data channel packets and (if enabled) tls-auth control channel packets with '
'HMAC using message digest algorithm alg.'
),
# auth
username=dict(
type='str', required=False, default='', aliases=['user'],
description='(optional) Username to send to the server for authentication when required.'
),
password=dict(
type='str', required=False, default='', aliases=['pwd'], no_log=True,
description='Password belonging to the user specified above'
),
renegotiate_time=dict(
type='str', required=False, default='', aliases=['reneg_time', 'reneg'],
description='Renegotiate data channel key after n seconds (default=3600). When using a one time '
'password, be advised that your connection will automatically drop because your '
'password is not valid anymore. Set to 0 to disable, remember to change your '
'client as well.'
),
# routing
network_local=dict(
type='list', elements='str', required=False, default=[], aliases=['net_local', 'push_route'],
description='These are the networks accessible on this host, these are pushed via route{-ipv6} '
'clauses in OpenVPN to the client.'
),
network_remote=dict(
type='list', elements='str', required=False, default=[], aliases=['net_remote', 'route'],
description='Remote networks for the server, add route to routing table after connection is established'
),
# misc
options=dict(
type='list', elements='str', required=False, default=[], aliases=['opts'],
description='Various less frequently used yes/no options which can be set for this instance.',
choices=[
'client-to-client', 'duplicate-cn', 'passtos', 'persist-remote-ip', 'route-nopull', 'route-noexec',
'remote-random',
],
),
mtu=dict(
type='str', required=False, default='', aliases=['tun_mtu'],
description='Take the TUN device MTU to be tun-mtu and derive the link MTU from it.'
),
fragment_size=dict(
type='str', required=False, default='', aliases=['frag_size'],
description='Enable internal datagram fragmentation so that no UDP datagrams are sent which are larger '
'than the specified byte size.'
),
mss_fix=dict(
type='bool', required=False, default=False, aliases=['mss'],
description='Announce to TCP sessions running over the tunnel that they should limit their send packet '
'sizes such that after OpenVPN has encapsulated them, the resulting UDP packet size that '
'OpenVPN sends to its peer will not exceed the recommended size.'
),
**RELOAD_MOD_ARG,
**STATE_MOD_ARG,
**OPN_MOD_ARGS,
)


result = dict(
changed=False,
diff={
Expand Down
5 changes: 5 additions & 0 deletions plugins/modules/reload.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def run_module():
'webproxy',
'bind',
'ids',
'openvpn',
],
description='What part of the running config should be reloaded'
),
Expand Down Expand Up @@ -141,6 +142,10 @@ def run_module():
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.ids_general import \
General as Target_Obj

elif target == 'openvpn':
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.openvpn_client import \
Client as Target_Obj

except MODULE_EXCEPTIONS:
module_dependency_error()

Expand Down
4 changes: 4 additions & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ run_test 'ids_rule' 1
run_test 'ids_user_rule' 1
run_test 'ids_policy' 1
run_test 'ids_policy_rule' 1
run_test 'openvpn_client' 1
# run_test 'openvpn_overwrite' 1
# run_test 'openvpn_static_key' 1
# run_test 'openvpn_server' 1
run_test 'system' 1
run_test 'package' 1

Expand Down
Loading

0 comments on commit 7f3765c

Please sign in to comment.