#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# Based on neutron/services/vpn/device_drivers/ipsec.py written in 2013
# by Nachi Ueno, NTT I3, Inc.
#
# Configuration of VPNaaS:
#
# This is an optional service thats managed by the Neutron l3-agent.
# It is not enabled by default. See [1].
# VPNaaS requires a correctly configured l3 router. See [2].
# VPNaaS uses the generic IPsec service driver. See [1].
# VPNaaS requires a Solaris specific device driver. See [3].
#
# Note[1]:
# To enable VPNaaS, add the following to:
# /etc/neutron/neutron.conf:
#
# [service_providers]
# service_provider = vpnaas
#
# This will use the generic ipsec service driver which is predefined in:
#
# Enabling VPNaaS will require the following services to be restarted:
#
# svc:/application/openstack/neutron/neutron-server:default
# svc:/application/openstack/neutron/neutron-l3-agent:default
#
# Note[2]:
#
# The [default] section of this file contains a router_id which
# is required for Solaris L3 agent and VPNaaS. Currently only a single
# router_id is supported.
# e.g.
#
# router_id = 9b17a6af-69ee-4185-a9fd-fa47502d97d6
#
# The device driver (Solaris specific) is configured in:
#
# Note[3]:
# The Solaris specific device driver is preconfigured in:
#
# [vpnagent]
# vpn_device_driver =
# neutron.services.vpn.device_drivers.solaris_ipsec.SolarisIPsecDriver
#
# Note[4] - Optional values.
#
# Some defualt values can be changed by setting values under the [solaris]
# section of /etc/neutron/vpn_agent.ini.
#
# [solaris]
# ipsec_status_check_interval = 5
# packet_logging = False
# logger_level = "message+packet"
#
# Note[5] - Logging.
#
# The default logging level is WARN. Any LOG.warn() messages will
# appear in the log file.
#
# Adding "Verbose = True" to the [default] section will cause LOG.info()
# messages to be logged in addition to LOG.warn().
#
# Adding "Debug = True" to the [default] section will cause LOG.debug()
# messages to be logged in addition to LOG.info() and LOG.warn().
#
import abc
import copy
import errno
import logging
import os
import re
import shutil
import socket
import struct
import sys
import threading
import time
import traceback
import unicodedata
import iniparse
import jinja2
import netaddr
import oslo_messaging
import six
# solaris_defaults[] and solaris_opts[] contain configurable variables
# that are used by this driver. Any of these can be changed by adding a
# variable=value entry into /etc/neutron/vpn_agent.ini under the section
# called:
#
# [solaris]
#
# If there is no value in /etc/neutron/vpn_agent.ini, the default value
# is used. Its highly recommended that the values defined in solaris_defaults[]
# are never changed. The values under solaris_opts[] may be changed if the
# defaults are not suitable.
solaris_defaults = [
'ipsec_config_template',
'ike_config_template',
'ikev2_config_template',
'ike_secret_template',
'ikev2_secret_template',
]
solaris_opts = [
]
JINJA_ENV = None
# These globals are flags used to stop the looping event
# report_status() from looking at unstable state during
# shutdown or when IKE is being restarted.
global being_shutdown
global restarting
existing_tunnels = []
def get_vpn_interfaces():
# OpenStack-created tunnel names use the convention of starting with
# "vpn" followed by a number.
vpn_tunnels = []
if p.returncode != 0:
return vpn_tunnels
if ifname not in vpn_tunnels:
return vpn_tunnels
def disable_smf_services():
"""
global restarting
try:
pass
'instance': 'default'}))
'instance': 'logger'}))
'instance': 'default'}))
'instance': 'ikev2'}))
def shutdown_vpn():
# Check to see if the VPN device_driver has been configured
# by looking in the config file. If it has we may have to give the
# driver a little more time to gracefully shutdown.
# This method is called from the smf(5) stop method. Suppress LOG
# output while shutting down.
# If we have a VPNaaS device driver, we still can't tell if its been
# configured or not. The easiest way to determine if VPNaaS is
# configured is to check if there are any IP tunnels configured.
if ifs:
def whack_ike_rules():
try:
return
connection_ids = []
if not m:
continue
connection_id = m[1]
if not connection_ids:
return
for rule in connection_ids:
rule]
try:
pass
# Deleting the IKE rules from the running IKE service will
# cause the looping status process to mark any connections
# down and update the server side. We just need to wait a
# few seconds for it to notice.
global JINJA_ENV
if not JINJA_ENV:
"""Delete tunnel interfaces using dladm(1m)/ipadm(1m).
Tunneling in Solaris is done with actual tunnel interfaces, not
tunneling as policy which is customary in other operating systems.
Solaris tunnels are configured using ipadm(1m) and is separate from
the IKE configuration. Starting the IKE and IPsec services does not
configure the tunnels. The glue between the tunnel and IPsec policy
is the tunnel name. See comment in sync() for a description
of tunnel names.
Also remove any PF bypass rules. We don't need
to delete the routes added by our evil twin add_tunnels()
because these get whacked when the tunnel is removed.
We are not really interested in any errors at this point, the
tunnels are being deleted because we are shutting down, or
we have new config.
"""
if not tunnels:
if chatty:
return
if chatty:
try:
if chatty:
try:
if chatty:
# Remove all the VPN bypass rules that were added for local subnet
# going out to remote subnet. These rules are all captured inside of
# a anchor whose name is of the form vpn_{peer_cidr}
if chatty:
if 'l3i' not in anchor:
continue
if chatty:
for l3i_anchor in l3i_anchors:
if 'vpn_' not in l3i_anchor:
continue
class BaseSolaris():
configuration files and IPsec policy configuration using
IPsec and IKE smf(5) services and plumbing of IP tunnels.
The configuration files are stored under:
The smf(5) config_file properties for the policy and ike
services are modified to point to this location.
"""
CONFIG_DIRS = [
'log',
'etc',
]
DIALECT_MAP = {
"3des": "3des",
"aes-128": "aes(128..128)",
"aes-256": "aes(256..256)",
"aes-192": "aes(192..192)",
"group2": "2",
"group5": "5",
"group14": "14",
"group15": "15",
"bi-directional": "start",
"response-only": "add",
"v2": "ikev2",
"v1": "default"
}
self.connection_status = {}
self.connection_ids = []
if not self.vpnservice:
return
for site in connections:
if not self.vpnservice:
return
"""Map neutron VPNaaS configuration to Solaris configuration
files using the templates.
"""
pass
This method may get called more than once if there is more
than one vpn-service object. The configuration data is
appended to the file in this case.
"""
"""Create config directory if it does not exist."""
"""Generate IPsec configuration files using templates.
"""
{'vpnservice': vpnservice,
pass
# This sets the status of vpn-service-list
"""Check to see if IKE is running. The output
is parsed to update status of ipsec-site-connection.
If this fails, one of the following happened:
The IKE daemon crashed.
The IKE service was disabled from the command line.
The l3_agent is being shutdown.
"""
global being_shutdown
if being_shutdown:
return False
# IKE is running. Update status of all connections
# IKE knows about.
return True
"""Update Status based on vpnservice configuration.
"""
else:
try:
else:
except RuntimeError:
("Failed to enable VPNaaS on router ID: \"%s\""),
try:
except RuntimeError:
("Failed to disable VPNaaS on router ID: \"%s\""),
"""Restart VPNaaS.
"""
"""Start VPNaaS.
"""
"""Stop VPNaaS.
"""
return {
}
"""Update the status of all of the ipsec-site-connections.
Mark all the connections this router knows about as ACTIVE or DOWN.
The update_pending_status flag is not set. This is
because we will report status back to the l3 agent directly.
We don't want the process_status_cache_check() method to change
it back again.
This should be revisited if we support more than one router.
"""
if not self.vpnservice:
return
'status': new_state,
'updated_pending_status': True
}
"""Update the status of the ipsec-site-connection
based on the output of ikeadm(1m). See get_status()
for further comments. Any connections that ended up
on the badboys list will be marked as state DOWN.
"""
global being_shutdown
global restarting
if not self.vpnservice:
return True
if restarting:
return True
if not self.get_status():
if being_shutdown:
else:
return False
return True
'status': None,
'tun_id': "",
'updated_pending_status': False
}
'status': new_status,
'updated_pending_status': True,
}
return True
"""Solaris IPsec Process manager class.
This class handles the configuration of IKE and IPsec
on Solaris. The configuration files are generated under the
standard neutron path:
This is a non default path for the Solaris utilities that read
these files. Unlike Linux, Solaris services are started by smf(5)
which passes parameters to the command(s) that make up that service.
For IPsec there are four services of interest:
The method service_setprops() sets the smf(5) properties to paths
based on the path above, so the services know where to find their
configuration.
Tunnel configuration is handled externally from IKE. The Solaris
commands dladm(1m) and ipadm(1m) use used to create the IP tunnels.
The tunnel name is the glue that binds the IPsec policy to a
particular tunnel. The tunnel name is based on the outer source and
destination IP addresses. This is because only one tunnel can exist
between any pair of addresses. The tunnel may have multiple inner
IP address pairs.
The status of the VPN connection is sampled regularly by the
get_status() method. If any state changes, this is reported back to
the l3 agent by report_status(), part of the IPsecDriver class.
get_status() calls the Solaris utility ikeadm(1m) which queries the
running IKE daemon. It determins that a VPN is ACTIVE if there is an IKE
rule in place. There may not actually be active IPsec SAs at this point
because Solaris IPsec connections are established on-demand, not part
of the start process.
Solaris commands mentioned above are called directly using pfexec(1).
"""
global being_shutdown
This function will generate the parent directory if needed.
"""
# This is going to need a lot more work if we ever support IKEv1
# and IKEv2 at the same time.
for site in connections:
'ike/ikev2.config',
"""Add tunnel interfaces using dladm(1m) and
ipadm(1m).
"""
for site in connections:
# Need an address for the tunnel end point.
# Use the first address of the remote network.
count = 0
count += 1
if (count == 2):
break
# Add the IP tunnel using dladm(1m). Its possible for more than
# one IPsec rule to use the same pair of IP addresses for the
# outer addresses of a tunnel. We can only have a single tunnel
# between any pair of addresses. So if dladm(1m) fails because
# the tunnel already exists, we can ignore this error.
try:
if m:
"%s -> %s already exists."
else:
"Inner: %s -> %s"
continue
"Added tunnel - Outer IP: %s -> %s, Inner: %s -> %s"
try:
else:
continue
try:
continue
try:
if m:
else:
continue
# Now for some Policy Based Routing (PBR) voodoo. When a Neutron
# subnet is added to a Neutron router, it adds a PBR rule that
# looks like this:
#
# anchor "l3ia18d6189_8_0/*" on l3ia18d6189_8_0 all {
# anchor "pbr" all {
# pass in inet from any to ! 192.168.80.0/24
# route-to 10.132.148.1.1@l3e88c2027b_9_0
# }
# }
#
# What this does is to pass all packets leaving interface
# "l3e2d9b3c1c_8_0:10.132.148.1" *UNLESS* the packet is destined to
# "192.168.80.0/24" which is a local address. This allows
# instances on the virtual network to reach real addresses on the
# Internet.
#
# When we create a VPN to another virtual network, we need to
# ensure packets destined for that head over the IP tunnel instead.
# To make this happen, we find the interface associated with our
# network and add a bypass rule. The rule looks like this:
#
# pass in quick on l3ia18d6189_8_0 from any to 192.168.80.0/24
#
# There will be one of these pass rules for each remote network.
# The "quick" keyword ensures it matches *BEFORE* the PBR rule.
# Find the interfaces names that start with 'l3i and have the IP
# address that matches the Inner Source IP address of an IP tunnel
# added by VPNaaS. Add a bypass rule to the sub-anchor for this
# interface
ifname = None
if p.returncode == 0:
break
if not ifname:
"VPN subnet: %s. Skipping bypass rule for '%s'" %
continue
bypass_rule = 'pass in quick from any to %s label %s' % \
"""Check to see if IKE is configured and running.
If ikeadm(1m) fails, because in.iked(1m) is not
running, the vpn-service will be switched from
ACTIVE to DOWN.
Each time a new configuration has been loaded the
IKE daemon service will be restarted. If the restart
has not yet happened, or is happening now, don't
report status as it could be stale.
If the VPN service is being stopped then always
get the status using ikeadm(1m), even if it's stale.
This information is used as a best effort to unplumb
tunnels. The VPN service is stopped when the l3 agent
smf(5) service is disabled or restarted. It's also
stopped before new configuration is loaded, then
restarted.
Assumption: Only one IKE service is running. If
we support IKEv1 and IKEv2 at the same time, this
code will need to be modified to look at rules
configured for IKEv1 and IKEv2.
By default the status of the service is checked every 60
seconds. The interval can be chanmged by setting:
ipsec_status_check_interval=<new_value>
under the [solaris] section of vpn_agent.ini .
Cache a list of ipsec-site-connections based on output of
ikeadm(1m). We are looking for the line in an IKE rule that
contains the tag "Label". The value following this will be the
connection_id.
If ikeadm fails, list of connection_ids is unchanged.
"""
try:
return False
self.connection_ids = []
if not m:
continue
connection_id = m[1]
return True
"""Restart VPNaaS.
Some optimization possible here, a restart does not
require property setting. If we are not running, we
don't need to disable stuff. See self.start(), self.stop()
"""
return
"""Start VPNaaS.
"""
if not self.vpnservice:
return
self.connection_status = {}
"""Flush IPsec SAs, this should be done with rad(1) eventually.
For disable ignore any errors.
"""
try:
"""
'instance': 'default'}))
[self.config_file])
'instance': 'default'}))
'instance': 'ikev2'}))
if self.packet_logging:
'instance': 'logger'}))
"""
'instance': 'default'}))
if self.packet_logging:
'instance': 'logger'}))
global restarting
"""IPSecVpnDriver RPC api."""
"""Get list of vpnservices.
The vpnservices including related ipsec_site_connection,
ikepolicy and ipsecpolicy on this host
"""
"""Update local status.
This method call updates status attribute of
VPNServices.
"""
"""Methods in this class are called by the l3 agent.
Check to see if VPNaaS is enabled in neutron.conf. If it's
not, just return and don't start the looping event.
for Solaris. There is a looping event which periodically
runs and, if state has changed this is reported back to the
l3 agent.
The looping method calls get_status() at a regular interval.
The interval (in seconds) is defined by:
ipsec_status_check_interval
See solaris_opts for more details
References to "process" are a little misleading. This code
configuration on a router.
For the purposes of this code:
router_id == process_id
This code is called once when the service is started. Variables
are valid until the service is disabled.
"""
try:
except:
try:
break
except:
pass
if not_configured:
return
self.process_status_cache = {}
"""VPN Service Driver will call this method
when VPN configuration objects have been modified. This
will call sync(), which will restart the device driver.
"""
"""This code is called when a new configuration is loaded, via sync().
It is also called periodically every ipsec_status_check_interval
seconds. If state has changed, report this back to the l3 agent.
Solaris currently only supports a single router.
The connection cache contains a list of the ipsec-site-connection's
that are in the neutron server configuration. This may differ from
what is actually configured.
The connection cache contains a list of the ipsec-site-connection's
that are in the neutron server configuration. This may differ from
what is actually configured.
"process" is misleading, its really refers to a router.
"""
global being_shutdown
if being_shutdown:
return
pass
"""Configure IPsec, IKE, tunnels on this router.
"""
if not vpnservice:
return
namespace = ""
elif vpnservice:
return process
""""Handling create router event.
"""
try:
except:
pass
"""Handling destroy_router event.
"""
try:
except:
return
if process_id in processes:
del processes[process_id]
'status': None,
'updated_pending_status': False,
'ipsec_site_connections': {}}
"""Get the current status from ikeadm(1m). Note that
process.status *calls* active.
There is a lot of debug here, because if this function fails
nothing gets updated.
"""
return True
return True
return True
if not found_previous_connection:
return True
continue
return False
return {
}
"""Verify the site connection cache.
This is called as a result of a sync() from the l3 agent.
The new configuration may differ from the current content
of the cache. If an ipsec-site-connection exists in the cache
but is no longer part of the new configuration (ie: it was
deleted) report this ipsec-site-connection as DOWN.
Note process_id is really the router UUID.
"""
"Checking connection cache of router ID: \"%s\"" % process_id)
stale_entries = []
if not cache[IPSEC_CONNS]:
return False
"Cache has [%s] entry for site ID: \"%s\""
"Site connection \"%s\" appears to have been deleted."
% site_conn)
'updated_pending_status': True
}
if stale_entries:
for badboy in stale_entries:
return return_value
"""Sync is called by the server side of neutron.
This will be called whenever new configuration is
delivered from the server. This forces the underlying
freshly generated configuration files.
Follow process.update() to see what happens.
The terminology "process" is very misleading. It in fact
is a reference to a router object.
The code below loops through the list of routers configured
and enables the VPNs on them. Currently Solaris only supports
a single router in Neutron. It will be a short list ...
We no longer have access to the previous configuration. The new
configuration may well be different, so we have to delete
existing configuration and tear down any tunnels. The tunnel
list existing_tunnels[] is used to cache the tunnels created.
The first time the driver is loaded, when the service is first
started, this list will be empty. The smf(5) start method checks
to see if there are any stray tunnels.
Remove all configuration files under <router_UUID>.
This directory structure is only used by the VPNaaS driver. It's
recreated every time the driver is loaded, or new configuration is
sent to the driver. Any errors generated deleting this directory
structure can be safely ignored.
The vpn-service object does not have the tunnel id, which
we need for Solaris tunnels. We will add it here.
Solaris tunnel names (see dladm(1m)) must start with an
alphabetic character and end with a decimal number and
be unique.
The glue between the IPsec policy and tunnels is the name
of the tunnel. The tunnel interface is created with dladm(1m).
If there are multiple VPNs using the same outer tunnel IP addresses
then they will share the same tunnel. To ensure that only one
tunnel is created between any pair of addresses, the tunnel name
is constructed as follows:
vpn_x_site_y
Where x is a decimal index number which is unique for each local
external IP address used as the outer source for a tunnel. Typically
this will only be a single IP address as each neutron router only
has a single external IP address. But this could change in the
future.
The decimal index y is unique per IKE peer. There may be more than
one set of inner addresses on each tunnel.
The output below shows the tunnels for a setup that has a local site
with two virtual subnets and two remote sites. One of the remote
sites has two remote subnets. The combination of two local subnets
and three remote subnets results in six ipsec-site-connections
and six tunnels on two data-link tunnels.
vpn_0_site_1 ip ok -- --
vpn_0_site_1/v4 static ok -- 192.168.90.1->192.168.40.1
vpn_0_site_1/v4a static ok -- 192.168.80.1->192.168.40.1
vpn_0_site_2 ip ok -- --
vpn_0_site_2/v4 static ok -- 192.168.90.1->192.168.100.1
vpn_0_site_2/v4a static ok -- 192.168.90.1->192.168.101.1
vpn_0_site_2/v4b static ok -- 192.168.80.1->192.168.100.1
vpn_0_site_2/v4c static ok -- 192.168.80.1->192.168.101.1
"""
try:
except:
return
global existing_tunnels
existing_tunnels = []
# Remove old configuration files.
try:
vpnservice['router_id']))
except:
pass
# Add tunnel_id's
local_ips = {}
remote_ips = {}
vpn_cnt = 0
site_cnt = 0
# If there are no vpn-services defined, we can bail out here.
# But we still need to check and delete any sub-processes that
# may have been created last time sync() was called.
if not vpnservices:
del process
return
for vpnservice in vpnservices:
if not l_id:
vpn_cnt += 1
if not r_id:
site_cnt += 1
# Next time we restart, these tunnels will be removed.
if tun_name not in existing_tunnels:
for vpnservice in vpnservices:
new_status = {
'updated_pending_status': True,
'ipsec_site_connections': {}
}
# Call start routine that sets up tunnels and
# enables smf(5) services.
return SolarisIPsecProcess(