Build out the PrivateNetwork function for services

This change adds the ability to effectively use the PrivateNetwork
functionality systemd provides for services. Now, if enabled, services
can be created in a network namespace which isolates it from the reset
of the host. Additional options have been added allowing access into the
network namespace over ephemeral devices as needed.

Highlights:
* Isolated private networking for services will sandbox using a stand
  alone namespace which has no access to anything via the network.
* Access into a private namespace can be provided over a single network
  interface which can be IP'd via local DHCP + NAT or using an upstream
  DHCP server.
* Tests have been added to exercise the new functionality.

All of the funcality has been documented in the defaults of this role.

Change-Id: I6751765131f32393a1605eb2100bec46199d980a
Signed-off-by: Kevin Carter <kevin@cloudnull.com>
This commit is contained in:
Kevin Carter 2019-02-04 01:07:55 -06:00 committed by Kevin Carter (cloudnull)
parent 6907b0c9f2
commit 6285b6c638
8 changed files with 307 additions and 4 deletions

View File

@ -37,9 +37,36 @@ systemd_TasksAccounting: true
# Sandboxing options
systemd_PrivateTmp: false
systemd_PrivateDevices: false
systemd_PrivateNetwork: false
systemd_PrivateUsers: false
# Systemd provides for the ability to start a given service in a network
# namespace. When `systemd_PrivateNetwork` is `true` a service will be
# started within a namepsace created using the name of the service unit.
systemd_PrivateNetwork: false
# When `systemd_PrivateNetwork` is enabled, it may be desirable to add a
# specific link into the service namespace using the MACVLAN interface.
# The option `systemd_PrivateNetworkIsolated`, when set to `false`, will
# create a MACVLAN interface which binds to the host interface defined
# by the option `systemd_PrivateNetworkInterface`; uses the gateway
# interface by default. The MODE used by the MACVLAN interface can be
# changed using the option `systemd_PrivateNetworkMode`.
systemd_PrivateNetworkIsolated: true
systemd_PrivateNetworkInterface: "{{ ansible_default_ipv4['interface'] }}"
systemd_PrivateNetworkMode: bridge
# When `systemd_PrivateNetworkIsolated` is disabled, an interface is
# created on the host and within the service namespace. If this interface
# needs an IP address DHCP can be enabled which will, by default, send
# DHCP requests through the interface defined by the option
# `systemd_PrivateNetworkInterface`.
systemd_PrivateNetworkDHCP: false
# DHCP can be localized to only the physical host using option
# `systemd_PrivateNetworkLocalDHCP`. Setting this option to `true`, will
# create a networkd configuration for DHCPServer using the MACVLAN interface
# defined by `systemd_PrivateNetworkInterface`. The gateway set within the
# service namespace will be set using `systemd_PrivateNetworkLocalDHCPGateway`.
systemd_PrivateNetworkLocalDHCP: false
systemd_PrivateNetworkLocalDHCPGateway: "10.0.5.1/24"
# Start service after a given target. This is here because we want to define common
# after targets used on most services. This can be overridden or agumented using
# the "systemd_services" dictionary option "after_targets".

View File

@ -26,3 +26,9 @@
- 'item is changed'
tags:
- systemd-service
- name: systemd networkd restart
systemd:
name: "systemd-networkd"
state: restarted
enabled: true

View File

@ -28,6 +28,34 @@
tags:
- always
- name: Ensure networkd is available
block:
- name: Check for networkd
command: "which networkctl"
failed_when: false
changed_when: false
register: networkd_installed
- name: Notify user
debug:
msg: >-
Local DHCP has been disabled because networkd was not installed or
is not part of the $PATH.
run_once: true
when:
- networkd_installed.rc != 0
- name: Disable local DHCP
set_fact:
systemd_PrivateNetworkLocalDHCP: false
when:
- networkd_installed.rc != 0
when:
- systemd_PrivateNetwork | bool
- systemd_PrivateNetworkLocalDHCP | bool
tags:
- systemd-service
- name: Create TEMP run dir
file:
path: "/var/run/{{ item.service_name | replace(' ', '_') }}"
@ -64,6 +92,45 @@
tags:
- systemd-service
- name: Create netns service entry
template:
src: "systemd-netns@.service.j2"
dest: "/etc/systemd/system/systemd-netns@.service"
mode: "0644"
owner: "root"
group: "root"
when:
- systemd_PrivateNetwork | bool
tags:
- systemd-service
- name: Create netns-access service entry
template:
src: "systemd-netns-access@.service.j2"
dest: "/etc/systemd/system/systemd-netns-access@.service"
mode: "0644"
owner: "root"
group: "root"
when:
- systemd_PrivateNetwork | bool
tags:
- systemd-service
- name: Create netns dhcp server
template:
src: "systemd-dhcp.network.j2"
dest: "/etc/systemd/network/systemd-mv-{{ systemd_PrivateNetworkInterface }}.network"
mode: "0644"
owner: "root"
group: "root"
when:
- systemd_PrivateNetwork | bool
- systemd_PrivateNetworkLocalDHCP | bool
notify:
- systemd networkd restart
tags:
- systemd-service
- name: Create tmpfiles.d entry
template:
src: "systemd-tmpfiles.j2"

View File

@ -0,0 +1,20 @@
[Match]
Name=mv-{{ systemd_PrivateNetworkInterface }}
[Network]
DHCPServer=true
Address={{ systemd_PrivateNetworkLocalDHCPGateway }}
{% if (systemd_version | int) >= 230 %}
IPMasquerade=true
IPForward=true
{% endif %}
[DHCPServer]
PoolOffset=50
PoolSize=200
DefaultLeaseTimeSec=300s
{% if (systemd_version | int) >= 230 %}
EmitDNS=true
EmitNTP=true
EmitTimezone=true
{% endif %}

View File

@ -0,0 +1,50 @@
[Unit]
Description=Named network namespace %I
Documentation=https://github.com/openstack/ansible-role-systemd_service
After=syslog.target
After=network.target
After=systemd-netns@%i.service
After=%i.service
{% if not (systemd_PrivateNetworkIsolated | bool) %}
BoundBy=systemd-netns@%i.service
{% endif %}
[Service]
Type=oneshot
RemainAfterExit=true
# Start process
ExecStart=/usr/bin/env ip netns exec %I ip link set lo up
ExecStart=-/usr/bin/env ip link add mv-{{ systemd_PrivateNetworkInterface }} link {{ systemd_PrivateNetworkInterface }} type macvlan mode {{ systemd_PrivateNetworkMode }}
ExecStart=-/usr/bin/env ip link set mv-{{ systemd_PrivateNetworkInterface }} up
ExecStart=/usr/bin/env sysctl -w net.ipv4.ip_forward=1
{% if (systemd_PrivateNetworkLocalDHCP | bool) %}
{% if (systemd_version | int) <= 230 %}
ExecStart=-/usr/bin/env iptables -t nat -D POSTROUTING -s {{ systemd_PrivateNetworkLocalDHCPGateway}} -o {{ systemd_PrivateNetworkInterface }} -j MASQUERADE
ExecStart=/usr/bin/env iptables -t nat -A POSTROUTING -s {{ systemd_PrivateNetworkLocalDHCPGateway}} -o {{ systemd_PrivateNetworkInterface }} -j MASQUERADE
{% endif %}
ExecStartPre=-/usr/bin/env ip address add {{ systemd_PrivateNetworkLocalDHCPGateway }} dev mv-{{ systemd_PrivateNetworkInterface }}
{% endif %}
{% if not (systemd_PrivateNetworkIsolated | bool) %}
ExecStart=-/usr/bin/env ip link add mv-pivot link {{ systemd_PrivateNetworkInterface }} type macvlan mode {{ systemd_PrivateNetworkMode }}
ExecStart=/usr/bin/env ip link set mv-pivot netns %I name {{ systemd_PrivateNetworkInterface }}
ExecStart=/usr/bin/env ip netns exec %I ip link set dev {{ systemd_PrivateNetworkInterface }} up
{% if (systemd_PrivateNetworkDHCP | bool) %}
ExecStart=/usr/bin/env ip netns exec %I dhclient {{ systemd_PrivateNetworkInterface }} -v
{% endif %}
# Stop process
{% if (systemd_PrivateNetworkLocalDHCP | bool) %}
{% if (systemd_version | int) <= 230 %}
ExecStop=/usr/bin/env iptables -t nat -D POSTROUTING -s {{ systemd_PrivateNetworkLocalDHCPGateway}} -o {{ systemd_PrivateNetworkInterface }} -j MASQUERADE
{% endif %}
{% endif %}
{% if (systemd_PrivateNetworkLocalDHCP | bool) %}
{% if (systemd_version | int) <= 230 %}
ExecStop=/usr/bin/env iptables -t nat -D POSTROUTING -s {{ systemd_PrivateNetworkLocalDHCPGateway}} -o {{ systemd_PrivateNetworkInterface }} -j MASQUERADE
{% endif %}
{% endif %}
{% endif %}
[Install]
WantedBy=multi-user.target
WantedBy=network-online.target

View File

@ -0,0 +1,30 @@
[Unit]
Description=Named network namespace %I
Documentation=https://github.com/openstack/ansible-role-systemd_service
After=syslog.target
After=network.target
{% if not (systemd_PrivateNetworkIsolated | bool) %}
BindsTo=systemd-netns-access@%i.service
{% endif %}
JoinsNamespaceOf=systemd-netns@%i.service
[Service]
Type=oneshot
RemainAfterExit=true
PrivateNetwork=true
# Start process
ExecStartPre=-/usr/bin/env ip netns delete %I
ExecStart=/usr/bin/env ip netns add %I
ExecStart=/usr/bin/env ip netns exec %I ip link set lo up
ExecStart=/usr/bin/env umount /var/run/netns/%I
ExecStart=/usr/bin/env mount --bind /proc/self/ns/net /var/run/netns/%I
# Stop process
ExecStop=/usr/bin/env ip netns delete %I
[Install]
WantedBy=multi-user.target
WantedBy=network-online.target

View File

@ -2,10 +2,22 @@
[Unit]
Description={{ item.service_name }} service
{% if (systemd_PrivateNetwork | bool) %}
BindsTo=systemd-netns@{{ item.service_name | replace(' ', '_') }}.service
JoinsNamespaceOf=systemd-netns@{{ item.service_name | replace(' ', '_') }}.service
{% if (item.after_targets is defined) %}
{% set _ = item.after_targets.append('systemd-netns@' + item.service_name | replace(' ', '_') + '.service') %}
{% else %}
{% set _ = systemd_after_targets.append('systemd-netns@' + item.service_name | replace(' ', '_') + '.service') %}
{% endif %}
{% endif %}
{% set after_targets = item.after_targets | default(systemd_after_targets) %}
{% for target in after_targets %}
After={{ target }}
{% endfor %}
{% for item in systemd_unit_docs %}
Documentation={{ item }}
{% endfor %}

View File

@ -14,6 +14,100 @@
# See the License for the specific language governing permissions and
# limitations under the License.
- name: Playbook for role testing
hosts: localhost
connection: local
user: root
become: true
roles:
- role: "systemd_service"
systemd_services:
- service_name: "test isolated service0"
execstarts: "/usr/bin/env python -m SimpleHTTPServer 8001"
enabled: yes
systemd_PrivateNetwork: yes
post_tasks:
- name: Check Services
command: systemctl status "test_isolated_service0"
changed_when: false
tags:
- skip_ansible_lint
- name: Check Services
shell: ip netns exec test_isolated_service0 ss -ntlp | grep python
changed_when: false
tags:
- skip_ansible_lint
- name: Check isolated services
command: ip netns exec test_isolated_service0 ip -o link
changed_when: false
register: isolated_service0
tags:
- skip_ansible_lint
- name: Check negative service testing
fail:
msg: >-
Two links not found within the namespace: {{ isolated_service1.stdout_lines }}
when:
- (isolated_service0.stdout_lines | length) != 1
- name: Playbook for role testing
hosts: localhost
connection: local
user: root
become: true
roles:
- role: "systemd_service"
systemd_services:
- service_name: "test isolated service1"
execstarts: "/usr/bin/env python -m SimpleHTTPServer 8001"
enabled: yes
systemd_PrivateNetwork: yes
systemd_PrivateNetworkIsolated: no
systemd_PrivateNetworkDHCP: yes
systemd_PrivateNetworkLocalDHCP: yes
when:
- (ansible_os_family | lower) != "redhat"
post_tasks:
- name: Check Services
command: systemctl status "test_isolated_service1"
changed_when: false
when:
- (ansible_os_family | lower) != "redhat"
tags:
- skip_ansible_lint
- name: Check Services
shell: ip netns exec test_isolated_service1 ss -ntlp | grep python
changed_when: false
when:
- (ansible_os_family | lower) != "redhat"
tags:
- skip_ansible_lint
- name: Check isolated linked services
command: ip netns exec test_isolated_service1 ip -o link
changed_when: false
register: isolated_service1
when:
- (ansible_os_family | lower) != "redhat"
tags:
- skip_ansible_lint
- name: Check negative service testing
fail:
msg: >-
Two links not found within the namespace: {{ isolated_service1.stdout_lines }}
when:
- (ansible_os_family | lower) != "redhat"
- (isolated_service1.stdout_lines | length) != 2
- name: Playbook for role testing
hosts: localhost
connection: local
@ -84,7 +178,6 @@
- name: Run the systemd service role
include_role:
name: systemd_service
private: true
vars:
systemd_services:
- service_name: "test simple service1"
@ -101,7 +194,6 @@
- name: Run the systemd service role
include_role:
name: systemd_service
private: true
vars:
systemd_services:
- service_name: "test simple service2"
@ -119,7 +211,6 @@
- name: Run the systemd service role
include_role:
name: systemd_service
private: true
vars:
systemd_services:
- service_name: "test simple service3"