├── .gitignore
├── .yamllint
├── LICENSE
├── README.adoc
├── README.md
├── defaults
└── main.yml
├── handlers
└── main.yml
├── library
└── iptables_raw.py
├── meta
└── main.yml
├── molecule
├── default
│ ├── Dockerfile.j2
│ ├── molecule.yml
│ ├── playbook.yml
│ ├── tests
│ │ └── test_basic.yml
│ └── verify.yml
├── firewalld
│ ├── molecule.yml
│ ├── playbook.yml
│ ├── prepare.yml
│ ├── tests
│ │ └── test_default.yml
│ └── verify.yml
├── iptables
│ ├── Dockerfile.j2
│ ├── molecule.yml
│ ├── playbook.yml
│ ├── tests
│ │ └── test_default.yml
│ └── verify.yml
├── ufw
│ ├── INSTALL.rst
│ ├── molecule.yml
│ ├── playbook.yml
│ ├── prepare.yml
│ ├── tests
│ │ └── test_service.yml
│ └── verify.yml
├── unusual
│ ├── INSTALL.rst
│ ├── molecule.yml
│ ├── playbook.yml
│ ├── prepare.yml
│ ├── tests
│ │ └── test_default.yml
│ └── verify.yml
└── users
│ ├── Dockerfile.j2
│ ├── molecule.yml
│ ├── playbook.yml
│ └── tests
│ └── test_default.py
├── tasks
├── CentOS.yml
├── Debian.yml
├── Fedora.yml
├── Ubuntu.yml
├── firewall.yml
├── firewalld.yml
├── iptables.yml
├── main.yml
└── ufw.yml
├── templates
└── 3proxy.cfg.j2
└── vars
└── main.yml
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/vim,python,ansible,sublimetext,visualstudiocode
3 |
4 | ### Ansible ###
5 | *.retry
6 |
7 | ### Python ###
8 | # Byte-compiled / optimized / DLL files
9 | __pycache__/
10 | *.py[cod]
11 | *$py.class
12 |
13 | # C extensions
14 | *.so
15 |
16 | # Distribution / packaging
17 | .Python
18 | build/
19 | develop-eggs/
20 | dist/
21 | downloads/
22 | eggs/
23 | .eggs/
24 | lib/
25 | lib64/
26 | parts/
27 | sdist/
28 | var/
29 | wheels/
30 | *.egg-info/
31 | .installed.cfg
32 | *.egg
33 | MANIFEST
34 |
35 | # PyInstaller
36 | # Usually these files are written by a python script from a template
37 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
38 | *.manifest
39 | *.spec
40 |
41 | # Installer logs
42 | pip-log.txt
43 | pip-delete-this-directory.txt
44 |
45 | # Unit test / coverage reports
46 | htmlcov/
47 | .tox/
48 | .coverage
49 | .coverage.*
50 | .cache
51 | nosetests.xml
52 | coverage.xml
53 | *.cover
54 | .hypothesis/
55 | .pytest_cache/
56 |
57 | # Translations
58 | *.mo
59 | *.pot
60 |
61 | # Django stuff:
62 | *.log
63 | local_settings.py
64 | db.sqlite3
65 |
66 | # Flask stuff:
67 | instance/
68 | .webassets-cache
69 |
70 | # Scrapy stuff:
71 | .scrapy
72 |
73 | # Sphinx documentation
74 | docs/_build/
75 |
76 | # PyBuilder
77 | target/
78 |
79 | # Jupyter Notebook
80 | .ipynb_checkpoints
81 |
82 | # pyenv
83 | .python-version
84 |
85 | # celery beat schedule file
86 | celerybeat-schedule
87 |
88 | # SageMath parsed files
89 | *.sage.py
90 |
91 | # Environments
92 | .env
93 | .venv
94 | env/
95 | venv/
96 | ENV/
97 | env.bak/
98 | venv.bak/
99 |
100 | # Spyder project settings
101 | .spyderproject
102 | .spyproject
103 |
104 | # Rope project settings
105 | .ropeproject
106 |
107 | # mkdocs documentation
108 | /site
109 |
110 | # mypy
111 | .mypy_cache/
112 |
113 | ### Python Patch ###
114 | .venv/
115 |
116 | ### Python.VirtualEnv Stack ###
117 | # Virtualenv
118 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/
119 | [Bb]in
120 | [Ii]nclude
121 | [Ll]ib
122 | [Ll]ib64
123 | [Ll]ocal
124 | [Ss]cripts
125 | pyvenv.cfg
126 | pip-selfcheck.json
127 |
128 | ### SublimeText ###
129 | # Cache files for Sublime Text
130 | *.tmlanguage.cache
131 | *.tmPreferences.cache
132 | *.stTheme.cache
133 |
134 | # Workspace files are user-specific
135 | *.sublime-workspace
136 |
137 | # Project files should be checked into the repository, unless a significant
138 | # proportion of contributors will probably not be using Sublime Text
139 | # *.sublime-project
140 |
141 | # SFTP configuration file
142 | sftp-config.json
143 |
144 | # Package control specific files
145 | Package Control.last-run
146 | Package Control.ca-list
147 | Package Control.ca-bundle
148 | Package Control.system-ca-bundle
149 | Package Control.cache/
150 | Package Control.ca-certs/
151 | Package Control.merged-ca-bundle
152 | Package Control.user-ca-bundle
153 | oscrypto-ca-bundle.crt
154 | bh_unicode_properties.cache
155 |
156 | # Sublime-github package stores a github token in this file
157 | # https://packagecontrol.io/packages/sublime-github
158 | GitHub.sublime-settings
159 |
160 | ### Vim ###
161 | # Swap
162 | [._]*.s[a-v][a-z]
163 | [._]*.sw[a-p]
164 | [._]s[a-rt-v][a-z]
165 | [._]ss[a-gi-z]
166 | [._]sw[a-p]
167 |
168 | # Session
169 | Session.vim
170 |
171 | # Temporary
172 | .netrwhist
173 | *~
174 | # Auto-generated tag files
175 | tags
176 | # Persistent undo
177 | [._]*.un~
178 |
179 | ### VisualStudioCode ###
180 | .vscode/*
181 | !.vscode/settings.json
182 | !.vscode/tasks.json
183 | !.vscode/launch.json
184 | !.vscode/extensions.json
185 |
186 |
187 | # End of https://www.gitignore.io/api/vim,python,ansible,sublimetext,visualstudiocode
188 |
189 |
--------------------------------------------------------------------------------
/.yamllint:
--------------------------------------------------------------------------------
1 | extends: default
2 |
3 | rules:
4 | braces:
5 | max-spaces-inside: 1
6 | level: error
7 | brackets:
8 | max-spaces-inside: 1
9 | level: error
10 | line-length: disable
11 | # NOTE(retr0h): Templates no longer fail this lint rule.
12 | # Uncomment if running old Molecule templates.
13 | # truthy: disable
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 asm0dey
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.adoc:
--------------------------------------------------------------------------------
1 | 3proxy
2 | ------
3 |
4 | :source-highlighter: highlightjs
5 |
6 | In our hard time everyone needs a bit more security. This role helps you
7 | install fast and powerful 3proxy proxy server
8 |
9 | *NB*: If some of your servers use iptables (without ufw/firewalld) — you should put module https://github.com/Nordeus/ansible_iptables_raw[iptables_raw] into library folder next to your playbook
10 |
11 | Supported OSes
12 | ~~~~~~~~~~~~~~
13 |
14 | [cols=",",options="header",]
15 | |============================
16 | |name |version
17 | .2+|CentOS |6
18 | |7
19 | .2+|Ubuntu |xenial
20 | |bionic
21 | .3+|Fedora |26
22 | |27
23 | |28
24 | |============================
25 |
26 | Role Variables
27 | ~~~~~~~~~~~~~~
28 |
29 | [cols=",",options="header",]
30 | |=======================================================================
31 | |name |description
32 | |proxy_users |array of users whch shold have access to proxy (otherwise anybody can)
33 | |proxy_socks |enable socks proxy (true by default)
34 | |proxy_socks_port |socks proxy port (1080 be default)
35 | |proxy_socks_options |additional socks proxy options
36 | |proxy_http |enable http proxy (true by default)
37 | |proxy_http_port |http proxy port (3128 be default)
38 | |proxy_http_options |additional http proxy options
39 | |manage_firewall |If role should try to allow incoming connections to proxy on firewall
40 | |=======================================================================
41 |
42 | Proxy users
43 | ~~~~~~~~~~~
44 |
45 | Proxy user is an object, which consists of 2 fields:
46 |
47 | [cols=",",options="header",]
48 | |==========================
49 | |name |description
50 | |name |username
51 | |hash |hash of the password
52 | |==========================
53 |
54 | Hash can be obtained from command
55 | `openssl passwd -1 'yourcomplexpasswordHere'`
56 |
57 | Example Playbook
58 | ~~~~~~~~~~~~~~~~
59 |
60 | [source,yaml]
61 | ----
62 | - hosts: all
63 | roles:
64 | - role: 3proxy
65 | proxy_users:
66 | - { name: "asm0dey", hash: "$1$pL3Ho94u$2.wCxrLfacj82UMPJSy/6/" }
67 | - { name: "asm0dey2", hash: "$1$pL3Ho94u$2.wCxrLfacj82UMPJSy/6/" }
68 | ----
69 |
70 | Development
71 | ~~~~~~~~~~~
72 |
73 | You need to have vagrant, docker, ansible and molecule installed to be able to run tests. Of course you can just implemet what you need without tests, but having tests is always better
74 |
75 | License
76 | ~~~~~~~
77 |
78 | MIT
79 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 3proxy
2 |
3 | In our hard time everyone needs a bit more security. This role helps you
4 | install fast and powerful 3proxy proxy server
5 |
6 | **NB**: If some of your servers use iptables (without ufw/firewalld) — you should put role [iptables\_raw](https://github.com/Nordeus/ansible_iptables_raw) into library folder next to your playbook
7 |
8 | ## Supported OSes
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
20 |
21 |
22 |
23 | CentOS |
24 | 6 |
25 |
26 |
27 | 7 |
28 | |
29 |
30 |
31 | Ubuntu |
32 | xenial |
33 |
34 |
35 | bionic |
36 | |
37 |
38 |
39 | Fedora |
40 | 26 |
41 |
42 |
43 | 27 |
44 | |
45 |
46 |
47 | 28 |
48 | |
49 |
50 |
51 |
52 |
53 | ## Role Variables
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
65 |
66 |
67 |
68 | proxy_users |
69 | array of users whch shold have access to proxy (otherwise anybody can) |
70 |
71 |
72 | proxy_socks |
73 | enable socks proxy (true by default) |
74 |
75 |
76 | proxy_socks_port |
77 | socks proxy port (1080 be default) |
78 |
79 |
80 | proxy_socks_options |
81 | additional socks proxy options |
82 |
83 |
84 | proxy_http |
85 | enable http proxy (true by default) |
86 |
87 |
88 | proxy_http_port |
89 | http proxy port (3128 be default) |
90 |
91 |
92 | proxy_http_options |
93 | additional http proxy options |
94 |
95 |
96 | manage_firewall |
97 | If role should try to allow incoming connections to proxy on firewall |
98 |
99 |
100 |
101 |
102 | ## Proxy users
103 |
104 | Proxy user is an object, which consists of 2 fields:
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
116 |
117 |
118 |
119 | name |
120 | username |
121 |
122 |
123 | hash |
124 | hash of the password |
125 |
126 |
127 |
128 |
129 | Hash can be obtained from command
130 | `openssl passwd -1 'yourcomplexpasswordHere'`
131 |
132 | ## Example Playbook
133 |
134 | ```yaml
135 | - hosts: all
136 | roles:
137 | - role: 3proxy
138 | proxy_users:
139 | - { name: "asm0dey", hash: "$1$pL3Ho94u$2.wCxrLfacj82UMPJSy/6/" }
140 | - { name: "asm0dey2", hash: "$1$pL3Ho94u$2.wCxrLfacj82UMPJSy/6/" }
141 | ```
142 |
143 | ## Development
144 |
145 | You need to have vagrant, docker, ansible and molecule installed to be able to run tests. Of course you can just implemet what you need without tests, but having tests is always better
146 |
147 | ## License
148 |
149 | MIT
150 |
--------------------------------------------------------------------------------
/defaults/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | proxy_users: []
3 | proxy_socks: true
4 | proxy_socks_port: 1080
5 | proxy_socks_options: ""
6 | proxy_http: true
7 | proxy_http_port: 3128
8 | proxy_http_options: ""
9 | manage_firewall: true
10 |
--------------------------------------------------------------------------------
/handlers/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Reloads systemd daemon
3 | systemd:
4 | daemon_reload: true
5 | when: ansible_service_mgr == 'systemd'
6 | listen: "reload service"
7 |
8 | - name: Ensures 3proxy service config is up to date
9 | service:
10 | name: 3proxy
11 | state: restarted
12 | listen: "reload service"
13 |
14 | - name: restart firewalld
15 | service:
16 | name: firewalld
17 | state: restarted
18 |
19 | - name: restart ufw
20 | service:
21 | name: ufw
22 | state: restarted
23 |
--------------------------------------------------------------------------------
/library/iptables_raw.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Make coding more python3-ish
5 | from __future__ import (absolute_import, division, print_function)
6 | __metaclass__ = type
7 |
8 | """
9 | (c) 2016, Strahinja Kustudic
10 | (c) 2016, Damir Markovic
11 |
12 | This file is part of Ansible
13 |
14 | Ansible is free software: you can redistribute it and/or modify
15 | it under the terms of the GNU General Public License as published by
16 | the Free Software Foundation, either version 3 of the License, or
17 | (at your option) any later version.
18 |
19 | Ansible is distributed in the hope that it will be useful,
20 | but WITHOUT ANY WARRANTY; without even the implied warranty of
21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 | GNU General Public License for more details.
23 |
24 | You should have received a copy of the GNU General Public License
25 | along with Ansible. If not, see .
26 | """
27 |
28 | ANSIBLE_METADATA = {
29 | 'metadata_version': '1.1',
30 | 'status': ['preview'],
31 | 'supported_by': 'community'
32 | }
33 |
34 | DOCUMENTATION = '''
35 | ---
36 | module: iptables_raw
37 | short_description: Manage iptables rules
38 | version_added: "2.5"
39 | description:
40 | - Add/remove iptables rules while keeping state.
41 | options:
42 | backup:
43 | description:
44 | - Create a backup of the iptables state file before overwriting it.
45 | required: false
46 | choices: ["yes", "no"]
47 | default: "no"
48 | ipversion:
49 | description:
50 | - Target the IP version this rule is for.
51 | required: false
52 | default: "4"
53 | choices: ["4", "6"]
54 | keep_unmanaged:
55 | description:
56 | - If set to C(yes) keeps active iptables (unmanaged) rules for the target
57 | C(table) and gives them C(weight=90). This means these rules will be
58 | ordered after most of the rules, since default priority is 40, so they
59 | shouldn't be able to block any allow rules. If set to C(no) deletes all
60 | rules which are not set by this module.
61 | - "WARNING: Be very careful when running C(keep_unmanaged=no) for the
62 | first time, since if you don't specify correct rules, you can block
63 | yourself out of the managed host."
64 | required: false
65 | choices: ["yes", "no"]
66 | default: "yes"
67 | name:
68 | description:
69 | - Name that will be used as an identifier for these rules. It can contain
70 | alphanumeric characters, underscore, hyphen, dot, or a space; has to be
71 | UNIQUE for a specified C(table). You can also pass C(name=*) with
72 | C(state=absent) to flush all rules in the selected table, or even all
73 | tables with C(table=*).
74 | required: true
75 | rules:
76 | description:
77 | - The rules that we want to add. Accepts multiline values.
78 | - "Note: You can only use C(-A)/C(--append), C(-N)/C(--new-chain), and
79 | C(-P)/C(--policy) to specify rules."
80 | required: false
81 | state:
82 | description:
83 | - The state this rules fragment should be in.
84 | choices: ["present", "absent"]
85 | required: false
86 | default: present
87 | table:
88 | description:
89 | - The table this rule applies to. You can specify C(table=*) only with
90 | with C(name=*) and C(state=absent) to flush all rules in all tables.
91 | choices: ["filter", "nat", "mangle", "raw", "security", "*"]
92 | required: false
93 | default: filter
94 | weight:
95 | description:
96 | - Determines the order of the rules. Lower C(weight) means higher
97 | priority. Supported range is C(0 - 99)
98 | choices: ["0 - 99"]
99 | required: false
100 | default: 40
101 | notes:
102 | - Requires C(iptables) package. Debian-based distributions additionally
103 | require C(iptables-persistent).
104 | - "Depending on the distribution, iptables rules are saved in different
105 | locations, so that they can be loaded on boot. Red Hat distributions (RHEL,
106 | CentOS, etc): C(/etc/sysconfig/iptables) and C(/etc/sysconfig/ip6tables);
107 | Debian distributions (Debian, Ubuntu, etc): C(/etc/iptables/rules.v4) and
108 | C(/etc/iptables/rules.v6); other distributions: C(/etc/sysconfig/iptables)
109 | and C(/etc/sysconfig/ip6tables)."
110 | - This module saves state in C(/etc/ansible-iptables) directory, so don't
111 | modify this directory!
112 | author:
113 | - "Strahinja Kustudic (@kustodian)"
114 | - "Damir Markovic (@damirda)"
115 | '''
116 |
117 | EXAMPLES = '''
118 | # Allow all IPv4 traffic coming in on port 80 (http)
119 | - iptables_raw:
120 | name: allow_tcp_80
121 | rules: '-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT'
122 |
123 | # Set default rules with weight 10 and disregard all unmanaged rules
124 | - iptables_raw:
125 | name: default_rules
126 | weight: 10
127 | keep_unmanaged: no
128 | rules: |
129 | -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
130 | -A INPUT -i lo -j ACCEPT
131 | -A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j ACCEPT
132 | -P INPUT DROP
133 | -P FORWARD DROP
134 | -P OUTPUT ACCEPT
135 |
136 | # Allow all IPv6 traffic coming in on port 443 (https) with weight 50
137 | - iptables_raw:
138 | ipversion: 6
139 | weight: 50
140 | name: allow_tcp_443
141 | rules: '-A INPUT -p tcp -m tcp --dport 443 -j ACCEPT'
142 |
143 | # Remove the above rule
144 | - iptables_raw:
145 | state: absent
146 | ipversion: 6
147 | name: allow_tcp_443
148 |
149 | # Define rules with a custom chain
150 | - iptables_raw:
151 | name: custom1_rules
152 | rules: |
153 | -N CUSTOM1
154 | -A CUSTOM1 -s 192.168.0.0/24 -j ACCEPT
155 |
156 | # Reset all IPv4 iptables rules in all tables and allow all traffic
157 | - iptables_raw:
158 | name: '*'
159 | table: '*'
160 | state: absent
161 | '''
162 |
163 | RETURN = '''
164 | state:
165 | description: state of the rules
166 | returned: success
167 | type: string
168 | sample: present
169 | name:
170 | description: name of the rules
171 | returned: success
172 | type: string
173 | sample: open_tcp_80
174 | weight:
175 | description: weight of the rules
176 | returned: success
177 | type: int
178 | sample: 40
179 | ipversion:
180 | description: IP version of iptables used
181 | returned: success
182 | type: int
183 | sample: 6
184 | rules:
185 | description: passed rules
186 | returned: success
187 | type: string
188 | sample: "-A INPUT -p tcp -m tcp --dport 80 -j ACCEPT"
189 | table:
190 | description: iptables table used
191 | returned: success
192 | type: string
193 | sample: filter
194 | backup:
195 | description: if the iptables file should backed up
196 | returned: success
197 | type: boolean
198 | sample: False
199 | keep_unmanaged:
200 | description: if it should keep unmanaged rules
201 | returned: success
202 | type: boolean
203 | sample: True
204 | '''
205 |
206 | from ansible.module_utils.basic import AnsibleModule
207 | from ansible.module_utils.basic import json
208 |
209 | import time
210 | import fcntl
211 | import re
212 | import shlex
213 | import os
214 | import tempfile
215 |
216 | try:
217 | from collections import defaultdict
218 | except ImportError:
219 | # This is a workaround for Python 2.4 which doesn't have defaultdict.
220 | class defaultdict(dict):
221 | def __init__(self, default_factory, *args, **kwargs):
222 | super(defaultdict, self).__init__(*args, **kwargs)
223 | self.default_factory = default_factory
224 |
225 | def __getitem__(self, key):
226 | try:
227 | return super(defaultdict, self).__getitem__(key)
228 | except KeyError:
229 | return self.__missing__(key)
230 |
231 | def __missing__(self, key):
232 | try:
233 | self[key] = self.default_factory()
234 | except TypeError:
235 | raise KeyError("Missing key %s" % (key, ))
236 | else:
237 | return self[key]
238 |
239 |
240 | # Genereates a diff dictionary from an old and new table dump.
241 | def generate_diff(dump_old, dump_new):
242 | diff = dict()
243 | if dump_old != dump_new:
244 | diff['before'] = dump_old
245 | diff['after'] = dump_new
246 | return diff
247 |
248 |
249 | def compare_dictionaries(dict1, dict2):
250 | if dict1 is None or dict2 is None:
251 | return False
252 | if not (isinstance(dict1, dict) and isinstance(dict2, dict)):
253 | return False
254 | shared_keys = set(dict2.keys()) & set(dict2.keys())
255 | if not (len(shared_keys) == len(dict1.keys()) and len(shared_keys) == len(dict2.keys())):
256 | return False
257 | dicts_are_equal = True
258 | for key in dict1.keys():
259 | if isinstance(dict1[key], dict):
260 | dicts_are_equal = dicts_are_equal and compare_dictionaries(dict1[key], dict2[key])
261 | else:
262 | dicts_are_equal = dicts_are_equal and (dict1[key] == dict2[key])
263 | if not dicts_are_equal:
264 | break
265 | return dicts_are_equal
266 |
267 |
268 | class Iptables:
269 |
270 | # Default chains for each table
271 | DEFAULT_CHAINS = {
272 | 'filter': ['INPUT', 'FORWARD', 'OUTPUT'],
273 | 'raw': ['PREROUTING', 'OUTPUT'],
274 | 'nat': ['PREROUTING', 'INPUT', 'OUTPUT', 'POSTROUTING'],
275 | 'mangle': ['PREROUTING', 'INPUT', 'FORWARD', 'OUTPUT', 'POSTROUTING'],
276 | 'security': ['INPUT', 'FORWARD', 'OUTPUT']
277 | }
278 |
279 | # List of tables
280 | TABLES = list(DEFAULT_CHAINS.copy().keys())
281 |
282 | # Directory which will store the state file.
283 | STATE_DIR = '/etc/ansible-iptables'
284 |
285 | # Key used for unmanaged rules
286 | UNMANAGED_RULES_KEY_NAME = '$unmanaged_rules$'
287 |
288 | # Only allow alphanumeric characters, underscore, hyphen, dots, or a space for
289 | # now. We don't want to have problems while parsing comments using regular
290 | # expressions.
291 | RULE_NAME_ALLOWED_CHARS = 'a-zA-Z0-9_ .-'
292 |
293 | module = None
294 |
295 | def __init__(self, module, ipversion):
296 | # Create directory for json files.
297 | if not os.path.exists(self.STATE_DIR):
298 | os.makedirs(self.STATE_DIR)
299 | if Iptables.module is None:
300 | Iptables.module = module
301 | self.state_save_path = self._get_state_save_path(ipversion)
302 | self.system_save_path = self._get_system_save_path(ipversion)
303 | self.state_dict = self._read_state_file()
304 | self.bins = self._get_bins(ipversion)
305 | self.iptables_names_file = self._get_iptables_names_file(ipversion)
306 | # Check if we have a required iptables version.
307 | self._check_compatibility()
308 | # Save active iptables rules for all tables, so that we don't
309 | # need to fetch them every time using 'iptables-save' command.
310 | self._active_rules = {}
311 | self._refresh_active_rules(table='*')
312 |
313 | def __eq__(self, other):
314 | return (isinstance(other, self.__class__) and compare_dictionaries(other.state_dict, self.state_dict))
315 |
316 | def __ne__(self, other):
317 | return not self.__eq__(other)
318 |
319 | def _get_bins(self, ipversion):
320 | if ipversion == '4':
321 | return {'iptables': Iptables.module.get_bin_path('iptables'),
322 | 'iptables-save': Iptables.module.get_bin_path('iptables-save'),
323 | 'iptables-restore': Iptables.module.get_bin_path('iptables-restore')}
324 | else:
325 | return {'iptables': Iptables.module.get_bin_path('ip6tables'),
326 | 'iptables-save': Iptables.module.get_bin_path('ip6tables-save'),
327 | 'iptables-restore': Iptables.module.get_bin_path('ip6tables-restore')}
328 |
329 | def _get_iptables_names_file(self, ipversion):
330 | if ipversion == '4':
331 | return '/proc/net/ip_tables_names'
332 | else:
333 | return '/proc/net/ip6_tables_names'
334 |
335 | # Return a list of active iptables tables
336 | def _get_list_of_active_tables(self):
337 | if os.path.isfile(self.iptables_names_file):
338 | table_names = open(self.iptables_names_file, 'r').read()
339 | return table_names.splitlines()
340 | else:
341 | return []
342 |
343 | # If /etc/debian_version exist, this means this is a debian based OS (Ubuntu, Mint, etc...)
344 | def _is_debian(self):
345 | return os.path.isfile('/etc/debian_version')
346 |
347 | # If /etc/arch-release exist, this means this is an ArchLinux OS
348 | def _is_arch_linux(self):
349 | return os.path.isfile('/etc/arch-release')
350 |
351 | # If /etc/gentoo-release exist, this means this is Gentoo
352 | def _is_gentoo(self):
353 | return os.path.isfile('/etc/gentoo-release')
354 |
355 | # Get the iptables system save path.
356 | # Supports RHEL/CentOS '/etc/sysconfig/' location.
357 | # Supports Debian/Ubuntu/Mint, '/etc/iptables/' location.
358 | # Supports Gentoo, '/var/lib/iptables/' location.
359 | def _get_system_save_path(self, ipversion):
360 | # distro detection, path setting should be added
361 | if self._is_debian():
362 | # Check if iptables-persistent packages is installed
363 | if not os.path.isdir('/etc/iptables'):
364 | Iptables.module.fail_json(msg="This module requires 'iptables-persistent' package!")
365 | if ipversion == '4':
366 | return '/etc/iptables/rules.v4'
367 | else:
368 | return '/etc/iptables/rules.v6'
369 | elif self._is_arch_linux():
370 | if ipversion == '4':
371 | return '/etc/iptables/iptables.rules'
372 | else:
373 | return '/etc/iptables/ip6tables.rules'
374 | elif self._is_gentoo():
375 | if ipversion == '4':
376 | return '/var/lib/iptables/rules-save'
377 | else:
378 | return '/var/lib/ip6tables/rules-save'
379 | else:
380 | if ipversion == '4':
381 | return '/etc/sysconfig/iptables'
382 | else:
383 | return '/etc/sysconfig/ip6tables'
384 |
385 | # Return path to json state file.
386 | def _get_state_save_path(self, ipversion):
387 | if ipversion == '4':
388 | return self.STATE_DIR + '/iptables.json'
389 | else:
390 | return self.STATE_DIR + '/ip6tables.json'
391 |
392 | # Checks if iptables is installed and if we have a correct version.
393 | def _check_compatibility(self):
394 | from distutils.version import StrictVersion
395 | cmd = [self.bins['iptables'], '--version']
396 | rc, stdout, stderr = Iptables.module.run_command(cmd, check_rc=False)
397 | if rc == 0:
398 | result = re.search(r'^ip6tables\s+v(\d+\.\d+)\.\d+$', stdout)
399 | if result:
400 | version = result.group(1)
401 | # CentOS 5 ip6tables (v1.3.x) doesn't support comments,
402 | # which means it cannot be used with this module.
403 | if StrictVersion(version) < StrictVersion('1.4'):
404 | Iptables.module.fail_json(msg="This module isn't compatible with ip6tables versions older than 1.4.x")
405 | else:
406 | Iptables.module.fail_json(msg="Could not fetch iptables version! Is iptables installed?")
407 |
408 | # Read rules from the json state file and return a dict.
409 | def _read_state_file(self):
410 | json_str = '{}'
411 | if os.path.isfile(self.state_save_path):
412 | try:
413 | json_str = open(self.state_save_path, 'r').read()
414 | except:
415 | Iptables.module.fail_json(msg="Could not read the state file '%s'!" % self.state_save_path)
416 | try:
417 | read_dict = defaultdict(lambda: dict(dump='', rules_dict={}), json.loads(json_str))
418 | except:
419 | Iptables.module.fail_json(msg="Could not parse the state file '%s'! Please manually delete it to continue." % self.state_save_path)
420 | return read_dict
421 |
422 | # Checks if a table exists in the state_dict.
423 | def _has_table(self, tbl):
424 | return tbl in self.state_dict
425 |
426 | # Deletes table from the state_dict.
427 | def _delete_table(self, tbl):
428 | if self._has_table(tbl):
429 | del self.state_dict[tbl]
430 |
431 | # Acquires lock or exits after wait_for_seconds if it cannot be acquired.
432 | def acquire_lock_or_exit(self, wait_for_seconds=10):
433 | lock_file = self.STATE_DIR + '/.iptables.lock'
434 | i = 0
435 | f = open(lock_file, 'w+')
436 | while i < wait_for_seconds:
437 | try:
438 | fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
439 | return
440 | except IOError:
441 | i += 1
442 | time.sleep(1)
443 | Iptables.module.fail_json(msg="Could not acquire lock to continue execution! "
444 | "Probably another instance of this module is running.")
445 |
446 | # Check if a table has anything to flush (to check all tables pass table='*').
447 | def table_needs_flush(self, table):
448 | needs_flush = False
449 | if table == '*':
450 | for tbl in Iptables.TABLES:
451 | # If the table exists or if it needs to be flushed that means will make changes.
452 | if self._has_table(tbl) or self._single_table_needs_flush(tbl):
453 | needs_flush = True
454 | break
455 | # Only flush the specified table
456 | else:
457 | if self._has_table(table) or self._single_table_needs_flush(table):
458 | needs_flush = True
459 | return needs_flush
460 |
461 | # Check if a passed table needs to be flushed.
462 | def _single_table_needs_flush(self, table):
463 | needs_flush = False
464 | active_rules = self._get_active_rules(table)
465 | if active_rules:
466 | policies = self._filter_default_chain_policies(active_rules, table)
467 | chains = self._filter_custom_chains(active_rules, table)
468 | rules = self._filter_rules(active_rules, table)
469 | # Go over default policies and check if they are all ACCEPT.
470 | for line in policies.splitlines():
471 | if not re.search(r'\bACCEPT\b', line):
472 | needs_flush = True
473 | break
474 | # If there is at least one rule or custom chain, that means we need flush.
475 | if len(chains) > 0 or len(rules) > 0:
476 | needs_flush = True
477 | return needs_flush
478 |
479 | # Returns a copy of the rules dict of a passed table.
480 | def _get_table_rules_dict(self, table):
481 | return self.state_dict[table]['rules_dict'].copy()
482 |
483 | # Returns saved table dump.
484 | def get_saved_table_dump(self, table):
485 | return self.state_dict[table]['dump']
486 |
487 | # Sets saved table dump.
488 | def _set_saved_table_dump(self, table, dump):
489 | self.state_dict[table]['dump'] = dump
490 |
491 | # Updates saved table dump from the active rules.
492 | def refresh_saved_table_dump(self, table):
493 | active_rules = self._get_active_rules(table)
494 | self._set_saved_table_dump(table, active_rules)
495 |
496 | # Sets active rules of the passed table.
497 | def _set_active_rules(self, table, rules):
498 | self._active_rules[table] = rules
499 |
500 | # Return active rules of the passed table.
501 | def _get_active_rules(self, table, clean=True):
502 | active_rules = ''
503 | if table == '*':
504 | all_rules = []
505 | for tbl in Iptables.TABLES:
506 | if tbl in self._active_rules:
507 | all_rules.append(self._active_rules[tbl])
508 | active_rules = '\n'.join(all_rules)
509 | else:
510 | active_rules = self._active_rules[table]
511 | if clean:
512 | return self._clean_save_dump(active_rules)
513 | else:
514 | return active_rules
515 |
516 | # Refresh active rules of a table ('*' for all tables).
517 | def _refresh_active_rules(self, table):
518 | if table == '*':
519 | for tbl in Iptables.TABLES:
520 | self._set_active_rules(tbl, self._get_system_active_rules(tbl))
521 | else:
522 | self._set_active_rules(table, self._get_system_active_rules(table))
523 |
524 | # Get iptables-save dump of active rules of one or all tables (pass '*') and return it as a string.
525 | def _get_system_active_rules(self, table):
526 | active_tables = self._get_list_of_active_tables()
527 | if table == '*':
528 | cmd = [self.bins['iptables-save']]
529 | # If there are no active tables, that means there are no rules
530 | if not active_tables:
531 | return ""
532 | else:
533 | cmd = [self.bins['iptables-save'], '-t', table]
534 | # If the table is not active, that means it has no rules
535 | if table not in active_tables:
536 | return ""
537 | rc, stdout, stderr = Iptables.module.run_command(cmd, check_rc=True)
538 | return stdout
539 |
540 | # Splits a rule into tokens
541 | def _split_rule_into_tokens(self, rule):
542 | try:
543 | return shlex.split(rule, comments=True)
544 | except:
545 | msg = "Could not parse the iptables rule:\n%s" % rule
546 | Iptables.module.fail_json(msg=msg)
547 |
548 | # Removes comment lines and empty lines from rules.
549 | @staticmethod
550 | def clean_up_rules(rules):
551 | cleaned_rules = []
552 | for line in rules.splitlines():
553 | # Remove lines with comments and empty lines.
554 | if not (Iptables.is_comment(line) or Iptables.is_empty_line(line)):
555 | cleaned_rules.append(line)
556 | return '\n'.join(cleaned_rules)
557 |
558 | # Checks if the line is a custom chain in specific iptables table.
559 | @staticmethod
560 | def is_custom_chain(line, table):
561 | default_chains = Iptables.DEFAULT_CHAINS[table]
562 | if re.match(r'\s*(:|(-N|--new-chain)\s+)[^\s]+', line) \
563 | and not re.match(r'\s*(:|(-N|--new-chain)\s+)\b(' + '|'.join(default_chains) + r')\b', line):
564 | return True
565 | else:
566 | return False
567 |
568 | # Checks if the line is a default chain of an iptables table.
569 | @staticmethod
570 | def is_default_chain(line, table):
571 | default_chains = Iptables.DEFAULT_CHAINS[table]
572 | if re.match(r'\s*(:|(-P|--policy)\s+)\b(' + '|'.join(default_chains) + r')\b\s+(ACCEPT|DROP)', line):
573 | return True
574 | else:
575 | return False
576 |
577 | # Checks if a line is an iptables rule.
578 | @staticmethod
579 | def is_rule(line):
580 | # We should only allow adding rules with '-A/--append', since others don't make any sense.
581 | if re.match(r'\s*(-A|--append)\s+[^\s]+', line):
582 | return True
583 | else:
584 | return False
585 |
586 | # Checks if a line starts with '#'.
587 | @staticmethod
588 | def is_comment(line):
589 | if re.match(r'\s*#', line):
590 | return True
591 | else:
592 | return False
593 |
594 | # Checks if a line is empty.
595 | @staticmethod
596 | def is_empty_line(line):
597 | if re.match(r'^$', line.strip()):
598 | return True
599 | else:
600 | return False
601 |
602 | # Return name of custom chain from the rule.
603 | def _get_custom_chain_name(self, line, table):
604 | if Iptables.is_custom_chain(line, table):
605 | return re.match(r'\s*(:|(-N|--new-chain)\s+)([^\s]+)', line).group(3)
606 | else:
607 | return ''
608 |
609 | # Return name of default chain from the rule.
610 | def _get_default_chain_name(self, line, table):
611 | if Iptables.is_default_chain(line, table):
612 | return re.match(r'\s*(:|(-N|--new-chain)\s+)([^\s]+)', line).group(3)
613 | else:
614 | return ''
615 |
616 | # Return target of the default chain from the rule.
617 | def _get_default_chain_target(self, line, table):
618 | if Iptables.is_default_chain(line, table):
619 | return re.match(r'\s*(:|(-N|--new-chain)\s+)([^\s]+)\s+([A-Z]+)', line).group(4)
620 | else:
621 | return ''
622 |
623 | # Removes duplicate custom chains from the table rules.
624 | def _remove_duplicate_custom_chains(self, rules, table):
625 | all_rules = []
626 | custom_chain_names = []
627 | for line in rules.splitlines():
628 | # Extract custom chains.
629 | if Iptables.is_custom_chain(line, table):
630 | chain_name = self._get_custom_chain_name(line, table)
631 | if chain_name not in custom_chain_names:
632 | custom_chain_names.append(chain_name)
633 | all_rules.append(line)
634 | else:
635 | all_rules.append(line)
636 | return '\n'.join(all_rules)
637 |
638 | # Returns current iptables-save dump cleaned from comments and packet/byte counters.
639 | def _clean_save_dump(self, simple_rules):
640 | cleaned_dump = []
641 | for line in simple_rules.splitlines():
642 | # Ignore comments.
643 | if Iptables.is_comment(line):
644 | continue
645 | # Reset counters for chains (begin with ':'), for easier comparing later on.
646 | if re.match(r'\s*:', line):
647 | cleaned_dump.append(re.sub(r'\[([0-9]+):([0-9]+)\]', '[0:0]', line))
648 | else:
649 | cleaned_dump.append(line)
650 | cleaned_dump.append('\n')
651 | return '\n'.join(cleaned_dump)
652 |
653 | # Returns lines with default chain policies.
654 | def _filter_default_chain_policies(self, rules, table):
655 | chains = []
656 | for line in rules.splitlines():
657 | if Iptables.is_default_chain(line, table):
658 | chains.append(line)
659 | return '\n'.join(chains)
660 |
661 | # Returns lines with iptables rules from an iptables-save table dump
662 | # (removes chain policies, custom chains, comments and everything else). By
663 | # default returns all rules, if 'only_unmanged=True' returns rules which
664 | # are not managed by Ansible.
665 | def _filter_rules(self, rules, table, only_unmanaged=False):
666 | filtered_rules = []
667 | for line in rules.splitlines():
668 | if Iptables.is_rule(line):
669 | if only_unmanaged:
670 | tokens = self._split_rule_into_tokens(line)
671 | # We need to check if a rule has a comment which starts with 'ansible[name]'
672 | if '--comment' in tokens:
673 | comment_index = tokens.index('--comment') + 1
674 | if comment_index < len(tokens):
675 | # Fetch the comment
676 | comment = tokens[comment_index]
677 | # Skip the rule if the comment starts with 'ansible[name]'
678 | if not re.match(r'ansible\[[' + Iptables.RULE_NAME_ALLOWED_CHARS + r']+\]', comment):
679 | filtered_rules.append(line)
680 | else:
681 | # Fail if there is no comment after the --comment parameter
682 | msg = "Iptables rule is missing a comment after the '--comment' parameter:\n%s" % line
683 | Iptables.module.fail_json(msg=msg)
684 | # If it doesn't have comment, this means it is not managed by Ansible and we should append it.
685 | else:
686 | filtered_rules.append(line)
687 | else:
688 | filtered_rules.append(line)
689 | return '\n'.join(filtered_rules)
690 |
691 | # Same as _filter_rules(), but returns custom chains
692 | def _filter_custom_chains(self, rules, table, only_unmanaged=False):
693 | filtered_chains = []
694 | # Get list of managed custom chains, which is needed to detect unmanaged custom chains
695 | managed_custom_chains_list = self._get_custom_chains_list(table)
696 | for line in rules.splitlines():
697 | if Iptables.is_custom_chain(line, table):
698 | if only_unmanaged:
699 | # The chain is not managed by this module if it's not in the list of managed custom chains.
700 | chain_name = self._get_custom_chain_name(line, table)
701 | if chain_name not in managed_custom_chains_list:
702 | filtered_chains.append(line)
703 | else:
704 | filtered_chains.append(line)
705 | return '\n'.join(filtered_chains)
706 |
707 | # Returns list of custom chains of a table.
708 | def _get_custom_chains_list(self, table):
709 | custom_chains_list = []
710 | for key, value in self._get_table_rules_dict(table).items():
711 | # Ignore UNMANAGED_RULES_KEY_NAME key, since we only want managed custom chains.
712 | if key != Iptables.UNMANAGED_RULES_KEY_NAME:
713 | for line in value['rules'].splitlines():
714 | if Iptables.is_custom_chain(line, table):
715 | chain_name = self._get_custom_chain_name(line, table)
716 | if chain_name not in custom_chains_list:
717 | custom_chains_list.append(chain_name)
718 | return custom_chains_list
719 |
720 | # Prepends 'ansible[name]: ' to iptables rule '--comment' argument,
721 | # or adds 'ansible[name]' as a comment if there is no comment.
722 | def _prepend_ansible_comment(self, rules, name):
723 | commented_lines = []
724 | for line in rules.splitlines():
725 | # Extract rules only since we cannot add comments to custom chains.
726 | if Iptables.is_rule(line):
727 | tokens = self._split_rule_into_tokens(line)
728 | if '--comment' in tokens:
729 | # If there is a comment parameter, we need to prepand 'ansible[name]: '.
730 | comment_index = tokens.index('--comment') + 1
731 | if comment_index < len(tokens):
732 | # We need to remove double quotes from comments, since there
733 | # is an incompatiblity with older iptables versions
734 | comment_text = tokens[comment_index].replace('"', '')
735 | tokens[comment_index] = 'ansible[' + name + ']: ' + comment_text
736 | else:
737 | # Fail if there is no comment after the --comment parameter
738 | msg = "Iptables rule is missing a comment after the '--comment' parameter:\n%s" % line
739 | Iptables.module.fail_json(msg=msg)
740 | else:
741 | # If comment doesn't exist, we add a comment 'ansible[name]'
742 | tokens += ['-m', 'comment', '--comment', 'ansible[' + name + ']']
743 | # Escape and quote tokens in case they have spaces
744 | tokens = [self._escape_and_quote_string(x) for x in tokens]
745 | commented_lines.append(" ".join(tokens))
746 | # Otherwise it's a chain, and we should just return it.
747 | else:
748 | commented_lines.append(line)
749 | return '\n'.join(commented_lines)
750 |
751 | # Double quote a string if it contains a space and escape double quotes.
752 | def _escape_and_quote_string(self, s):
753 | escaped = s.replace('"', r'\"')
754 | if re.search(r'\s', escaped):
755 | return '"' + escaped + '"'
756 | else:
757 | return escaped
758 |
759 | # Add table rule to the state_dict.
760 | def add_table_rule(self, table, name, weight, rules, prepend_ansible_comment=True):
761 | self._fail_on_bad_rules(rules, table)
762 | if prepend_ansible_comment:
763 | self.state_dict[table]['rules_dict'][name] = {'weight': weight, 'rules': self._prepend_ansible_comment(rules, name)}
764 | else:
765 | self.state_dict[table]['rules_dict'][name] = {'weight': weight, 'rules': rules}
766 |
767 | # Remove table rule from the state_dict.
768 | def remove_table_rule(self, table, name):
769 | if name in self.state_dict[table]['rules_dict']:
770 | del self.state_dict[table]['rules_dict'][name]
771 |
772 | # TODO: Add sorting of rules so that diffs in check_mode look nicer and easier to follow.
773 | # Sorting would be done from top to bottom like this:
774 | # * default chain policies
775 | # * custom chains
776 | # * rules
777 | #
778 | # Converts rules from a state_dict to an iptables-save readable format.
779 | def get_table_rules(self, table):
780 | generated_rules = ''
781 | # We first add a header e.g. '*filter'.
782 | generated_rules += '*' + table + '\n'
783 | rules_list = []
784 | custom_chains_list = []
785 | default_chain_policies = []
786 | dict_rules = self._get_table_rules_dict(table)
787 | # Return list of rule names sorted by ('weight', 'rules') tuple.
788 | for rule_name in sorted(dict_rules, key=lambda x: (dict_rules[x]['weight'], dict_rules[x]['rules'])):
789 | rules = dict_rules[rule_name]['rules']
790 | # Fail if some of the rules are bad
791 | self._fail_on_bad_rules(rules, table)
792 | rules_list.append(self._filter_rules(rules, table))
793 | custom_chains_list.append(self._filter_custom_chains(rules, table))
794 | default_chain_policies.append(self._filter_default_chain_policies(rules, table))
795 | # Clean up empty strings from these two lists.
796 | rules_list = list(filter(None, rules_list))
797 | custom_chains_list = list(filter(None, custom_chains_list))
798 | default_chain_policies = list(filter(None, default_chain_policies))
799 | if default_chain_policies:
800 | # Since iptables-restore applies the last chain policy it reads, we
801 | # have to reverse the order of chain policies so that those with
802 | # the lowest weight (higher priority) are read last.
803 | generated_rules += '\n'.join(reversed(default_chain_policies)) + '\n'
804 | if custom_chains_list:
805 | # We remove duplicate custom chains so that iptables-restore
806 | # doesn't fail because of that.
807 | generated_rules += self._remove_duplicate_custom_chains('\n'.join(sorted(custom_chains_list)), table) + '\n'
808 | if rules_list:
809 | generated_rules += '\n'.join(rules_list) + '\n'
810 | generated_rules += 'COMMIT\n'
811 | return generated_rules
812 |
813 | # Sets unmanaged rules for the passed table in the state_dict.
814 | def _set_unmanaged_rules(self, table, rules):
815 | self.add_table_rule(table, Iptables.UNMANAGED_RULES_KEY_NAME, 90, rules, prepend_ansible_comment=False)
816 |
817 | # Clears unmanaged rules of a table.
818 | def clear_unmanaged_rules(self, table):
819 | self._set_unmanaged_rules(table, '')
820 |
821 | # Updates unmanaged rules of a table from the active rules.
822 | def refresh_unmanaged_rules(self, table):
823 | # Get active iptables rules and clean them up.
824 | active_rules = self._get_active_rules(table)
825 | unmanaged_chains_and_rules = []
826 | unmanaged_chains_and_rules.append(self._filter_custom_chains(active_rules, table, only_unmanaged=True))
827 | unmanaged_chains_and_rules.append(self._filter_rules(active_rules, table, only_unmanaged=True))
828 | # Clean items which are empty strings
829 | unmanaged_chains_and_rules = list(filter(None, unmanaged_chains_and_rules))
830 | self._set_unmanaged_rules(table, '\n'.join(unmanaged_chains_and_rules))
831 |
832 | # Check if there are bad lines in the specified rules.
833 | def _fail_on_bad_rules(self, rules, table):
834 | for line in rules.splitlines():
835 | tokens = self._split_rule_into_tokens(line)
836 | if '-t' in tokens or '--table' in tokens:
837 | msg = ("Iptables rules cannot contain '-t/--table' parameter. "
838 | "You should use the 'table' parameter of the module to set rules "
839 | "for a specific table.")
840 | Iptables.module.fail_json(msg=msg)
841 | # Fail if the parameter --comment doesn't have a comment after
842 | if '--comment' in tokens and len(tokens) <= tokens.index('--comment') + 1:
843 | msg = "Iptables rule is missing a comment after the '--comment' parameter:\n%s" % line
844 | Iptables.module.fail_json(msg=msg)
845 | if not (Iptables.is_rule(line) or
846 | Iptables.is_custom_chain(line, table) or
847 | Iptables.is_default_chain(line, table) or
848 | Iptables.is_comment(line)):
849 | msg = ("Bad iptables rule '%s'! You can only use -A/--append, -N/--new-chain "
850 | "and -P/--policy to specify rules." % line)
851 | Iptables.module.fail_json(msg=msg)
852 |
853 | # Write rules to dest path.
854 | def _write_rules_to_file(self, rules, dest):
855 | tmp_path = self._write_to_temp_file(rules)
856 | Iptables.module.atomic_move(tmp_path, dest)
857 |
858 | # Write text to a temp file and return path to that file.
859 | def _write_to_temp_file(self, text):
860 | fd, path = tempfile.mkstemp()
861 | Iptables.module.add_cleanup_file(path) # add file for cleanup later
862 | tmp = os.fdopen(fd, 'w')
863 | tmp.write(text)
864 | tmp.close()
865 | return path
866 |
867 | #
868 | # Public and private methods which make changes on the system
869 | # are named 'system_*' and '_system_*', respectively.
870 | #
871 |
872 | # Flush all rules in a passed table.
873 | def _system_flush_single_table_rules(self, table):
874 | # Set all default chain policies to ACCEPT.
875 | for chain in Iptables.DEFAULT_CHAINS[table]:
876 | cmd = [self.bins['iptables'], '-t', table, '-P', chain, 'ACCEPT']
877 | Iptables.module.run_command(cmd, check_rc=True)
878 | # Then flush all rules.
879 | cmd = [self.bins['iptables'], '-t', table, '-F']
880 | Iptables.module.run_command(cmd, check_rc=True)
881 | # And delete custom chains.
882 | cmd = [self.bins['iptables'], '-t', table, '-X']
883 | Iptables.module.run_command(cmd, check_rc=True)
884 | # Update active rules in the object.
885 | self._refresh_active_rules(table)
886 |
887 | # Save active iptables rules to the system path.
888 | def _system_save_active(self, backup=False):
889 | # Backup if needed
890 | if backup:
891 | Iptables.module.backup_local(self.system_save_path)
892 | # Get iptables-save dump of all tables
893 | all_active_rules = self._get_active_rules(table='*', clean=False)
894 | # Move iptables-save dump of all tables to the iptables_save_path
895 | self._write_rules_to_file(all_active_rules, self.system_save_path)
896 |
897 | # Apply table dict rules to the system.
898 | def system_apply_table_rules(self, table, test=False):
899 | dump_path = self._write_to_temp_file(self.get_table_rules(table))
900 | if test:
901 | cmd = [self.bins['iptables-restore'], '-t', dump_path]
902 | else:
903 | cmd = [self.bins['iptables-restore'], dump_path]
904 | rc, stdout, stderr = Iptables.module.run_command(cmd, check_rc=False)
905 | if rc != 0:
906 | if test:
907 | dump_contents_file = open(dump_path, 'r')
908 | dump_contents = dump_contents_file.read()
909 | dump_contents_file.close()
910 | msg = "There is a problem with the iptables rules:" \
911 | + '\n\nError message:\n' \
912 | + stderr \
913 | + '\nGenerated rules:\n#######\n' \
914 | + dump_contents + '#####'
915 | else:
916 | msg = "Could not load iptables rules:\n\n" + stderr
917 | Iptables.module.fail_json(msg=msg)
918 | self._refresh_active_rules(table)
919 |
920 | # Flush one or all tables (to flush all tables pass table='*').
921 | def system_flush_table_rules(self, table):
922 | if table == '*':
923 | for tbl in Iptables.TABLES:
924 | self._delete_table(tbl)
925 | if self._single_table_needs_flush(tbl):
926 | self._system_flush_single_table_rules(tbl)
927 | # Only flush the specified table.
928 | else:
929 | self._delete_table(table)
930 | if self._single_table_needs_flush(table):
931 | self._system_flush_single_table_rules(table)
932 |
933 | # Saves state file and system iptables rules.
934 | def system_save(self, backup=False):
935 | self._system_save_active(backup=backup)
936 | rules = json.dumps(self.state_dict, sort_keys=True, indent=4, separators=(',', ': '))
937 | self._write_rules_to_file(rules, self.state_save_path)
938 |
939 |
940 | def main():
941 |
942 | module = AnsibleModule(
943 | argument_spec=dict(
944 | ipversion=dict(required=False, choices=["4", "6"], type='str', default="4"),
945 | state=dict(required=False, choices=['present', 'absent'], default='present', type='str'),
946 | weight=dict(required=False, type='int', default=40),
947 | name=dict(required=True, type='str'),
948 | table=dict(required=False, choices=Iptables.TABLES + ['*'], default="filter", type='str'),
949 | rules=dict(required=False, type='str', default=""),
950 | backup=dict(required=False, type='bool', default=False),
951 | keep_unmanaged=dict(required=False, type='bool', default=True),
952 | ),
953 | supports_check_mode=True,
954 | )
955 |
956 | check_mode = module.check_mode
957 | changed = False
958 | ipversion = module.params['ipversion']
959 | state = module.params['state']
960 | weight = module.params['weight']
961 | name = module.params['name']
962 | table = module.params['table']
963 | rules = module.params['rules']
964 | backup = module.params['backup']
965 | keep_unmanaged = module.params['keep_unmanaged']
966 |
967 | kw = dict(state=state, name=name, rules=rules, weight=weight, ipversion=ipversion,
968 | table=table, backup=backup, keep_unmanaged=keep_unmanaged)
969 |
970 | iptables = Iptables(module, ipversion)
971 |
972 | # Acquire lock so that only one instance of this object can exist.
973 | # Fail if the lock cannot be acquired within 10 seconds.
974 | iptables.acquire_lock_or_exit(wait_for_seconds=10)
975 |
976 | # Clean up rules of comments and empty lines.
977 | rules = Iptables.clean_up_rules(rules)
978 |
979 | # Check additional parameter requirements
980 | if state == 'present' and name == '*':
981 | module.fail_json(msg="Parameter 'name' can only be '*' if 'state=absent'")
982 | if state == 'present' and table == '*':
983 | module.fail_json(msg="Parameter 'table' can only be '*' if 'name=*' and 'state=absent'")
984 | if state == 'present' and not name:
985 | module.fail_json(msg="Parameter 'name' cannot be empty")
986 | if state == 'present' and not re.match('^[' + Iptables.RULE_NAME_ALLOWED_CHARS + ']+$', name):
987 | module.fail_json(msg="Parameter 'name' not valid! It can only contain alphanumeric characters, "
988 | "underscore, hyphen, or a space, got: '%s'" % name)
989 | if weight < 0 or weight > 99:
990 | module.fail_json(msg="Parameter 'weight' can be 0-99, got: %d" % weight)
991 | if state == 'present' and rules == '':
992 | module.fail_json(msg="Parameter 'rules' cannot be empty when 'state=present'")
993 |
994 | # Flush rules of one or all tables
995 | if state == 'absent' and name == '*':
996 | # Check if table(s) need to be flushed
997 | if iptables.table_needs_flush(table):
998 | changed = True
999 | if not check_mode:
1000 | # Flush table(s)
1001 | iptables.system_flush_table_rules(table)
1002 | # Save state and system iptables rules
1003 | iptables.system_save(backup=backup)
1004 | # Exit since there is nothing else to do
1005 | kw['changed'] = changed
1006 | module.exit_json(**kw)
1007 |
1008 | # Initialize new iptables object which will store new rules
1009 | iptables_new = Iptables(module, ipversion)
1010 |
1011 | if state == 'present':
1012 | iptables_new.add_table_rule(table, name, weight, rules)
1013 | else:
1014 | iptables_new.remove_table_rule(table, name)
1015 |
1016 | if keep_unmanaged:
1017 | iptables_new.refresh_unmanaged_rules(table)
1018 | else:
1019 | iptables_new.clear_unmanaged_rules(table)
1020 |
1021 | # Refresh saved table dump with active iptables rules
1022 | iptables_new.refresh_saved_table_dump(table)
1023 |
1024 | # Check if there are changes in iptables, and if yes load new rules
1025 | if iptables != iptables_new:
1026 |
1027 | changed = True
1028 |
1029 | # Test generated rules
1030 | iptables_new.system_apply_table_rules(table, test=True)
1031 |
1032 | if check_mode:
1033 | # Create a predicted diff for check_mode.
1034 | # Diff will be created from rules generated from the state dictionary.
1035 | if hasattr(module, '_diff') and module._diff:
1036 | # Update unmanaged rules in the old object so the generated diff
1037 | # from the rules dictionaries is more accurate.
1038 | iptables.refresh_unmanaged_rules(table)
1039 | # Generate table rules from rules dictionaries.
1040 | table_rules_old = iptables.get_table_rules(table)
1041 | table_rules_new = iptables_new.get_table_rules(table)
1042 | # If rules generated from dicts are not equal, we generate a diff from them.
1043 | if table_rules_old != table_rules_new:
1044 | kw['diff'] = generate_diff(table_rules_old, table_rules_new)
1045 | else:
1046 | # TODO: Update this comment to be better.
1047 | kw['diff'] = {'prepared': "System rules were not changed (e.g. rule "
1048 | "weight changed, redundant rule, etc)"}
1049 | else:
1050 | # We need to fetch active table dump before we apply new rules
1051 | # since we will need them to generate a diff.
1052 | table_active_rules = iptables_new.get_saved_table_dump(table)
1053 |
1054 | # Apply generated rules.
1055 | iptables_new.system_apply_table_rules(table)
1056 |
1057 | # Refresh saved table dump with active iptables rules.
1058 | iptables_new.refresh_saved_table_dump(table)
1059 |
1060 | # Save state and system iptables rules.
1061 | iptables_new.system_save(backup=backup)
1062 |
1063 | # Generate a diff.
1064 | if hasattr(module, '_diff') and module._diff:
1065 | table_active_rules_new = iptables_new.get_saved_table_dump(table)
1066 | if table_active_rules != table_active_rules_new:
1067 | kw['diff'] = generate_diff(table_active_rules, table_active_rules_new)
1068 | else:
1069 | # TODO: Update this comment to be better.
1070 | kw['diff'] = {'prepared': "System rules were not changed (e.g. rule "
1071 | "weight changed, redundant rule, etc)"}
1072 |
1073 | kw['changed'] = changed
1074 | module.exit_json(**kw)
1075 |
1076 |
1077 | if __name__ == '__main__':
1078 | main()
1079 |
--------------------------------------------------------------------------------
/meta/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | galaxy_info:
3 | author: asm0dey
4 | description: Package to install 3proxy
5 | license: MIT
6 |
7 | min_ansible_version: 1.2
8 |
9 | platforms:
10 | - name: Ubuntu
11 | versions:
12 | - bionic
13 | - xenial
14 | - name: EL
15 | versions:
16 | - 6
17 | - 7
18 | - name: Fedora
19 | versions:
20 | - 26
21 | - 27
22 | - 28
23 | - name: Debian
24 | versions:
25 | - stretch
26 | - jessie
27 |
28 | galaxy_tags:
29 | - networking
30 | - proxy
31 | - 3proxy
32 |
--------------------------------------------------------------------------------
/molecule/default/Dockerfile.j2:
--------------------------------------------------------------------------------
1 | # Molecule managed
2 |
3 | {% if item.registry is defined %}
4 | FROM {{ item.registry.url }}/{{ item.image }}
5 | {% else %}
6 | FROM {{ item.image }}
7 | {% endif %}
8 |
9 | RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get upgrade -y && apt-get install -y python sudo bash ca-certificates iproute2 && apt-get clean; \
10 | elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python2-dnf bash iproute && dnf clean all; \
11 | elif [ $(command -v yum) ]; then yum makecache fast && yum update -y && yum install -y python sudo yum-plugin-ovl bash iproute && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
12 | elif [ $(command -v zypper) ]; then zypper refresh && zypper update -y && zypper install -y python sudo bash python-xml && zypper clean -a; \
13 | elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
14 | elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi
15 |
--------------------------------------------------------------------------------
/molecule/default/molecule.yml:
--------------------------------------------------------------------------------
1 | ---
2 | dependency:
3 | name: galaxy
4 | driver:
5 | name: docker
6 | lint:
7 | name: yamllint
8 | platforms:
9 | - name: ubuntu
10 | image: solita/ubuntu-systemd:latest
11 | command: /sbin/init
12 | capabilities:
13 | - SYS_ADMIN
14 | volumes:
15 | - /sys/fs/cgroup:/sys/fs/cgroup:ro
16 | - name: ubuntu-bio
17 | image: solita/ubuntu-systemd:bionic
18 | command: /sbin/init
19 | capabilities:
20 | - SYS_ADMIN
21 | volumes:
22 | - /sys/fs/cgroup:/sys/fs/cgroup:ro
23 | - name: centos7
24 | image: solita/centos-systemd
25 | command: /sbin/init
26 | capabilities:
27 | - SYS_ADMIN
28 | volumes:
29 | - /sys/fs/cgroup:/sys/fs/cgroup:ro
30 | - name: fedora26
31 | image: fedora:26
32 | command: /sbin/init
33 | capabilities:
34 | - SYS_ADMIN
35 | volumes:
36 | - /sys/fs/cgroup:/sys/fs/cgroup:ro
37 | - name: fedora27
38 | image: fedora:27
39 | command: /sbin/init
40 | capabilities:
41 | - SYS_ADMIN
42 | volumes:
43 | - /sys/fs/cgroup:/sys/fs/cgroup:ro
44 | - name: fedora28
45 | image: fedora:28
46 | command: /sbin/init
47 | capabilities:
48 | - SYS_ADMIN
49 | volumes:
50 | - /sys/fs/cgroup:/sys/fs/cgroup:ro
51 | provisioner:
52 | name: ansible
53 | lint:
54 | name: ansible-lint
55 | scenario:
56 | name: default
57 | verifier:
58 | name: goss
59 | lint:
60 | name: yamllint
61 | enabled: false
62 |
--------------------------------------------------------------------------------
/molecule/default/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Converge
3 | hosts: all
4 | roles:
5 | - role: 3proxy
6 |
--------------------------------------------------------------------------------
/molecule/default/tests/test_basic.yml:
--------------------------------------------------------------------------------
1 | ---
2 | port:
3 | tcp:1080:
4 | listening: true
5 | tcp:3128:
6 | listening: true
7 | service:
8 | 3proxy:
9 | enabled: true
10 | running: true
11 | file:
12 | {{if .Env.OS | regexMatch "([Dd]ebian|[Uu]buntu)"}}
13 | /etc/3proxy/3proxy.cfg:
14 | {{else}}
15 | /etc/3proxy.cfg:
16 | {{end}}
17 | exists: true
18 | contains:
19 | - "!users"
20 | - "!auth strong"
21 |
--------------------------------------------------------------------------------
/molecule/default/verify.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # This is an example playbook to execute goss tests.
3 | # Tests need distributed to the appropriate ansible host/groups
4 | # prior to execution by `goss validate`.
5 |
6 | - name: Verify
7 | hosts: all
8 | become: true
9 | vars:
10 | goss_version: v0.3.6
11 | goss_arch: amd64
12 | goss_dst: /usr/local/bin/goss
13 | goss_url: "https://github.com/aelsabbahy/goss/releases/download/{{ goss_version }}/goss-linux-{{ goss_arch }}"
14 | goss_test_directory: /tmp
15 | goss_format: documentation
16 | tasks:
17 | - name: Download and install Goss
18 | get_url:
19 | url: "{{ goss_url }}"
20 | dest: "{{ goss_dst }}"
21 | mode: 0755
22 | register: download_goss
23 | until: download_goss is succeeded
24 | retries: 3
25 |
26 | - name: Copy Goss tests to remote
27 | copy:
28 | src: "{{ item }}"
29 | dest: "{{ goss_test_directory }}/{{ item | basename }}"
30 | with_fileglob:
31 | - "{{ lookup('env', 'MOLECULE_VERIFIER_TEST_DIRECTORY') }}/test_*.yml"
32 |
33 | - name: Register test files
34 | shell: "ls {{ goss_test_directory }}/test_*.yml"
35 | register: test_files
36 |
37 | - name: Execute Goss tests
38 | command: "{{ goss_dst }} -g {{ item }} validate --format {{ goss_format }}"
39 | register: test_results
40 | with_items: "{{ test_files.stdout_lines }}"
41 | environment:
42 | OS: "{{ ansible_distribution }}"
43 |
44 | - name: Display details about the Goss results
45 | debug:
46 | msg: "{{ item.stdout_lines }}"
47 | with_items: "{{ test_results.results }}"
48 |
49 | - name: Fail when tests fail
50 | fail:
51 | msg: "Goss failed to validate"
52 | when: item.rc != 0
53 | with_items: "{{ test_results.results }}"
54 |
--------------------------------------------------------------------------------
/molecule/firewalld/molecule.yml:
--------------------------------------------------------------------------------
1 | ---
2 | dependency:
3 | name: galaxy
4 | driver:
5 | name: vagrant
6 | provider:
7 | name: virtualbox
8 | lint:
9 | name: yamllint
10 | platforms:
11 | - name: instance
12 | box: centos/7
13 | provider_raw_config_args:
14 | - "customize ['modifyvm', :id, '--uartmode1', 'disconnected']"
15 | provisioner:
16 | name: ansible
17 | lint:
18 | name: ansible-lint
19 | scenario:
20 | name: firewalld
21 | verifier:
22 | name: goss
23 | lint:
24 | name: yamllint
25 |
--------------------------------------------------------------------------------
/molecule/firewalld/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Converge
3 | hosts: all
4 | become: true
5 | roles:
6 | - role: 3proxy
7 |
--------------------------------------------------------------------------------
/molecule/firewalld/prepare.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Prepare
3 | hosts: all
4 | gather_facts: false
5 | tasks:
6 | - name: Install python for Ansible
7 | raw: which python || (dnf -y update && dnf install -y python)
8 | become: true
9 | changed_when: false
10 |
--------------------------------------------------------------------------------
/molecule/firewalld/tests/test_default.yml:
--------------------------------------------------------------------------------
1 | ---
2 | port:
3 | tcp:1080:
4 | listening: true
5 | tcp:3128:
6 | listening: true
7 | command:
8 | firewall-cmd --query-port 1080/tcp:
9 | exit-status: 0
10 | stdout: ["yes"]
11 | stderr: []
12 | timeout: 10000
13 | firewall-cmd --query-port 3128/tcp:
14 | exit-status: 0
15 | stdout: ["yes"]
16 | stderr: []
17 | timeout: 10000
18 |
--------------------------------------------------------------------------------
/molecule/firewalld/verify.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # This is an example playbook to execute goss tests.
3 | # Tests need distributed to the appropriate ansible host/groups
4 | # prior to execution by `goss validate`.
5 |
6 | - name: Verify
7 | hosts: all
8 | become: true
9 | vars:
10 | goss_version: v0.3.6
11 | goss_arch: amd64
12 | goss_dst: /usr/local/bin/goss
13 | goss_url: "https://github.com/aelsabbahy/goss/releases/download/{{ goss_version }}/goss-linux-{{ goss_arch }}"
14 | goss_test_directory: /tmp
15 | goss_format: json_oneline
16 | tasks:
17 | - name: Download and install Goss
18 | get_url:
19 | url: "{{ goss_url }}"
20 | dest: "{{ goss_dst }}"
21 | mode: 0755
22 | register: download_goss
23 | until: download_goss is succeeded
24 | retries: 3
25 |
26 | - name: Copy Goss tests to remote
27 | copy:
28 | src: "{{ item }}"
29 | dest: "{{ goss_test_directory }}/{{ item | basename }}"
30 | with_fileglob:
31 | - "{{ lookup('env', 'MOLECULE_VERIFIER_TEST_DIRECTORY') }}/test_*.yml"
32 |
33 | - name: Register test files
34 | shell: "ls {{ goss_test_directory }}/test_*.yml"
35 | register: test_files
36 |
37 | - name: Execute Goss tests
38 | command: "{{ goss_dst }} -g {{ item }} validate --format {{ goss_format }}"
39 | register: test_results
40 | with_items: "{{ test_files.stdout_lines }}"
41 |
42 | - name: Display details about the Goss results
43 | debug:
44 | msg: "{{ item.stdout_lines }}"
45 | with_items: "{{ test_results.results }}"
46 |
47 | - name: Fail when tests fail
48 | fail:
49 | msg: "Goss failed to validate"
50 | when: item.rc != 0
51 | with_items: "{{ test_results.results }}"
52 |
--------------------------------------------------------------------------------
/molecule/iptables/Dockerfile.j2:
--------------------------------------------------------------------------------
1 | # Molecule managed
2 |
3 | FROM solita/ubuntu-systemd:bionic
4 |
5 | RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get upgrade -y && apt-get install -y python sudo bash ca-certificates iproute2 iptables && apt-get clean; \
6 | elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python2-dnf bash iproute && dnf clean all; \
7 | elif [ $(command -v yum) ]; then yum makecache fast && yum update -y && yum install -y python sudo yum-plugin-ovl bash iproute && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
8 | elif [ $(command -v zypper) ]; then zypper refresh && zypper update -y && zypper install -y python sudo bash python-xml && zypper clean -a; \
9 | elif [ $(command -v apk) ]; then apk update && apk add --no-cache iptables python sudo bash ca-certificates iproute2; \
10 | elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi
11 |
--------------------------------------------------------------------------------
/molecule/iptables/molecule.yml:
--------------------------------------------------------------------------------
1 | ---
2 | dependency:
3 | name: galaxy
4 | driver:
5 | name: vagrant
6 | provider:
7 | name: virtualbox
8 | lint:
9 | name: yamllint
10 | platforms:
11 | - name: instance-1
12 | config_options:
13 | synced_folder: true
14 | box: centos/6
15 | memory: 1024
16 | cpus: 1
17 | provision: true
18 | provisioner:
19 | name: ansible
20 | lint:
21 | name: ansible-lint
22 | scenario:
23 | name: iptables
24 | verifier:
25 | name: goss
26 | lint:
27 | name: yamllint
28 |
--------------------------------------------------------------------------------
/molecule/iptables/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Converge
3 | hosts: all
4 | become: true
5 | roles:
6 | - role: 3proxy
7 |
--------------------------------------------------------------------------------
/molecule/iptables/tests/test_default.yml:
--------------------------------------------------------------------------------
1 | ---
2 | command:
3 | iptables -S:
4 | exit-status: 0
5 | stdout:
6 | - -A INPUT -p tcp -m tcp --dport 1080 -m conntrack --ctstate NEW,ESTABLISHED -m comment --comment "ansible[Allow INPUT for port 1080]" -j ACCEPT
7 | - -A INPUT -p tcp -m tcp --dport 3128 -m conntrack --ctstate NEW,ESTABLISHED -m comment --comment "ansible[Allow INPUT for port 3128]" -j ACCEPT
8 | - -A OUTPUT -p tcp -m tcp --dport 1080 -m conntrack --ctstate ESTABLISHED -m comment --comment "ansible[Allow OUTPUT for port 1080]" -j ACCEPT
9 | - -A OUTPUT -p tcp -m tcp --dport 3128 -m conntrack --ctstate ESTABLISHED -m comment --comment "ansible[Allow OUTPUT for port 3128]" -j ACCEPT
10 | stderr: []
11 | timeout: 10000
12 |
--------------------------------------------------------------------------------
/molecule/iptables/verify.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # This is an example playbook to execute goss tests.
3 | # Tests need distributed to the appropriate ansible host/groups
4 | # prior to execution by `goss validate`.
5 |
6 | - name: Verify
7 | hosts: all
8 | become: true
9 | vars:
10 | goss_version: v0.3.2
11 | goss_arch: amd64
12 | goss_dst: /usr/local/bin/goss
13 | goss_url: "http://github.com/aelsabbahy/goss/releases/download/{{ goss_version }}/goss-linux-{{ goss_arch }}"
14 | goss_test_directory: /tmp
15 | goss_format: documentation
16 | tasks:
17 | - name: Check if goss is downloaded
18 | command: "which {{ goss_dst }}"
19 | register: goss_exists
20 | check_mode: false
21 | changed_when: false # Never report as changed
22 | ignore_errors: true
23 |
24 | - name: Download and install Goss
25 | command: "wget {{ goss_url }} -O {{ goss_dst }}"
26 | register: download_goss
27 | until: download_goss is succeeded
28 | retries: 3
29 | when: goss_exists.rc != 0
30 |
31 | - name: Chmod
32 | file:
33 | path: "{{ goss_dst }}"
34 | state: file
35 | mode: 0755
36 |
37 | - name: Copy Goss tests to remote
38 | copy:
39 | src: "{{ item }}"
40 | dest: "{{ goss_test_directory }}/{{ item | basename }}"
41 | with_fileglob:
42 | - "{{ lookup('env', 'MOLECULE_VERIFIER_TEST_DIRECTORY') }}/test_*.yml"
43 |
44 | - name: Register test files
45 | shell: "ls {{ goss_test_directory }}/test_*.yml"
46 | register: test_files
47 |
48 | - name: Execute Goss tests
49 | command: "{{ goss_dst }} -g {{ item }} validate --format {{ goss_format }}"
50 | register: test_results
51 | with_items: "{{ test_files.stdout_lines }}"
52 |
53 | - name: Display details about the Goss results
54 | debug:
55 | msg: "{{ item.stdout_lines }}"
56 | with_items: "{{ test_results.results }}"
57 |
58 | - name: Fail when tests fail
59 | fail:
60 | msg: "Goss failed to validate"
61 | when: item.rc != 0
62 | with_items: "{{ test_results.results }}"
63 |
--------------------------------------------------------------------------------
/molecule/ufw/INSTALL.rst:
--------------------------------------------------------------------------------
1 | *******
2 | Vagrant driver installation guide
3 | *******
4 |
5 | Requirements
6 | ============
7 |
8 | * Vagrant
9 | * Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop
10 | * python-vagrant
11 |
12 | Install
13 | =======
14 |
15 | .. code-block:: bash
16 |
17 | $ sudo pip install python-vagrant
18 |
--------------------------------------------------------------------------------
/molecule/ufw/molecule.yml:
--------------------------------------------------------------------------------
1 | ---
2 | dependency:
3 | name: galaxy
4 | driver:
5 | name: vagrant
6 | provider:
7 | name: virtualbox
8 | lint:
9 | name: yamllint
10 | platforms:
11 | - name: xenial
12 | box: ubuntu/xenial64
13 | provider_raw_config_args:
14 | - "customize ['modifyvm', :id, '--uartmode1', 'disconnected']"
15 | - name: bionic
16 | box: ubuntu/bionic64
17 | provider_raw_config_args:
18 | - "customize ['modifyvm', :id, '--uartmode1', 'disconnected']"
19 | provisioner:
20 | name: ansible
21 | lint:
22 | name: ansible-lint
23 | scenario:
24 | name: ufw
25 | verifier:
26 | name: goss
27 | lint:
28 | name: yamllint
29 |
--------------------------------------------------------------------------------
/molecule/ufw/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Converge
3 | become: true
4 | hosts: all
5 | roles:
6 | - role: 3proxy
7 |
--------------------------------------------------------------------------------
/molecule/ufw/prepare.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Prepare
3 | hosts: all
4 | gather_facts: false
5 | tasks:
6 | - name: Install python for Ansible
7 | raw: test -e /usr/bin/python || (apt -y update && apt install -y python-minimal)
8 | become: true
9 | changed_when: false
10 |
--------------------------------------------------------------------------------
/molecule/ufw/tests/test_service.yml:
--------------------------------------------------------------------------------
1 | ---
2 | package:
3 | ufw:
4 | installed: true
5 | port:
6 | tcp:1080:
7 | listening: true
8 | tcp:3128:
9 | listening: true
10 | service:
11 | ufw:
12 | enabled: true
13 | running: true
14 | command:
15 | 'grep ''### tuple ###'' /etc/ufw/*.rules':
16 | exit-status: 0
17 | stdout:
18 | - allow tcp 1080
19 | - allow tcp 3128
20 | stderr: []
21 | timeout: 10000
22 |
--------------------------------------------------------------------------------
/molecule/ufw/verify.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # This is an example playbook to execute goss tests.
3 | # Tests need distributed to the appropriate ansible host/groups
4 | # prior to execution by `goss validate`.
5 |
6 | - name: Verify
7 | hosts: all
8 | become: true
9 | vars:
10 | goss_version: v0.3.6
11 | goss_arch: amd64
12 | goss_dst: /usr/local/bin/goss
13 | goss_url: "https://github.com/aelsabbahy/goss/releases/download/{{ goss_version }}/goss-linux-{{ goss_arch }}"
14 | goss_test_directory: /tmp
15 | goss_format: json_oneline
16 | tasks:
17 | - name: Download and install Goss
18 | get_url:
19 | url: "{{ goss_url }}"
20 | dest: "{{ goss_dst }}"
21 | mode: 0755
22 | register: download_goss
23 | until: download_goss is succeeded
24 | retries: 3
25 |
26 | - name: Copy Goss tests to remote
27 | copy:
28 | src: "{{ item }}"
29 | dest: "{{ goss_test_directory }}/{{ item | basename }}"
30 | with_fileglob:
31 | - "{{ lookup('env', 'MOLECULE_VERIFIER_TEST_DIRECTORY') }}/test_*.yml"
32 |
33 | - name: Register test files
34 | shell: "ls {{ goss_test_directory }}/test_*.yml"
35 | register: test_files
36 |
37 | - name: Execute Goss tests
38 | command: "{{ goss_dst }} -g {{ item }} validate --format {{ goss_format }}"
39 | register: test_results
40 | with_items: "{{ test_files.stdout_lines }}"
41 |
42 | - name: Display details about the Goss results
43 | debug:
44 | msg: "{{ item.stdout_lines }}"
45 | with_items: "{{ test_results.results }}"
46 |
47 | - name: Fail when tests fail
48 | fail:
49 | msg: "Goss failed to validate"
50 | when: item.rc != 0
51 | with_items: "{{ test_results.results }}"
52 |
--------------------------------------------------------------------------------
/molecule/unusual/INSTALL.rst:
--------------------------------------------------------------------------------
1 | *******
2 | Vagrant driver installation guide
3 | *******
4 |
5 | Requirements
6 | ============
7 |
8 | * Vagrant
9 | * Virtualbox, Parallels, VMware Fusion, VMware Workstation or VMware Desktop
10 | * python-vagrant
11 |
12 | Install
13 | =======
14 |
15 | .. code-block:: bash
16 |
17 | $ sudo pip install python-vagrant
18 |
--------------------------------------------------------------------------------
/molecule/unusual/molecule.yml:
--------------------------------------------------------------------------------
1 | ---
2 | dependency:
3 | name: galaxy
4 | driver:
5 | name: vagrant
6 | provider:
7 | name: virtualbox
8 | lint:
9 | name: yamllint
10 | platforms:
11 | - name: debian9
12 | box: debian/stretch64
13 | - name: debian8
14 | box: debian/jessie64
15 | - name: centos6
16 | box: bento/centos-6.10
17 | box_version: 201807.12.0
18 | provisioner:
19 | name: ansible
20 | lint:
21 | name: ansible-lint
22 | scenario:
23 | name: unusual
24 | verifier:
25 | name: goss
26 | lint:
27 | name: yamllint
28 | enabled: false
29 |
--------------------------------------------------------------------------------
/molecule/unusual/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Converge
3 | hosts: all
4 | become: true
5 | roles:
6 | - role: 3proxy
7 |
--------------------------------------------------------------------------------
/molecule/unusual/prepare.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Prepare
3 | hosts: all
4 | gather_facts: false
5 | tasks:
6 | - name: Install python for Ansible
7 | raw: (command -v apt && (test -e /usr/bin/python || (apt -y update && apt install -y python-minimal))) || (command -v yum && (command -v python || (yum -y update && yum install -y python)))
8 | become: true
9 | changed_when: false
10 |
--------------------------------------------------------------------------------
/molecule/unusual/tests/test_default.yml:
--------------------------------------------------------------------------------
1 | ---
2 | port:
3 | tcp:1080:
4 | listening: true
5 | tcp:3128:
6 | listening: true
7 | service:
8 | 3proxy:
9 | enabled: true
10 | running: true
11 | file:
12 | {{if .Env.OS | regexMatch "([Dd]ebian|[Uu]buntu)"}}
13 | /etc/3proxy/3proxy.cfg:
14 | {{else}}
15 | /etc/3proxy.cfg:
16 | {{end}}
17 | exists: true
18 | contains:
19 | - "!users"
20 | - "!auth strong"
21 |
--------------------------------------------------------------------------------
/molecule/unusual/verify.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # This is an example playbook to execute goss tests.
3 | # Tests need distributed to the appropriate ansible host/groups
4 | # prior to execution by `goss validate`.
5 |
6 | - name: Verify
7 | hosts: all
8 | become: true
9 | vars:
10 | goss_version: v0.3.2
11 | goss_arch: amd64
12 | goss_dst: /usr/local/bin/goss
13 | goss_sha256sum: 2f6727375db2ea0f81bee36e2c5be78ab5ab8d5981f632f761b25e4003e190ec
14 | goss_url: "https://github.com/aelsabbahy/goss/releases/download/{{ goss_version }}/goss-linux-{{ goss_arch }}"
15 | goss_test_directory: /tmp
16 | goss_format: documentation
17 | tasks:
18 | - name: Check if goss is downloaded
19 | command: "which {{ goss_dst }}"
20 | register: goss_exists
21 | check_mode: false
22 | changed_when: false # Never report as changed
23 | ignore_errors: true
24 |
25 | - name: Download and install Goss
26 | command: "wget {{ goss_url }} -O {{ goss_dst }}"
27 | register: download_goss
28 | until: download_goss is succeeded
29 | retries: 3
30 | when: goss_exists.rc != 0
31 |
32 | - name: Chmod
33 | file:
34 | path: "{{ goss_dst }}"
35 | state: file
36 | mode: 0755
37 |
38 | - name: Copy Goss tests to remote
39 | copy:
40 | src: "{{ item }}"
41 | dest: "{{ goss_test_directory }}/{{ item | basename }}"
42 | with_fileglob:
43 | - "{{ lookup('env', 'MOLECULE_VERIFIER_TEST_DIRECTORY') }}/test_*.yml"
44 |
45 | - name: Register test files
46 | shell: "ls {{ goss_test_directory }}/test_*.yml"
47 | register: test_files
48 |
49 | - name: Execute Goss tests
50 | command: "{{ goss_dst }} -g {{ item }} validate --format {{ goss_format }}"
51 | register: test_results
52 | with_items: "{{ test_files.stdout_lines }}"
53 | environment:
54 | OS: "{{ ansible_distribution }}"
55 |
56 | - name: Display details about the Goss results
57 | debug:
58 | msg: "{{ item.stdout_lines }}"
59 | with_items: "{{ test_results.results }}"
60 |
61 | - name: Fail when tests fail
62 | fail:
63 | msg: "Goss failed to validate"
64 | when: item.rc != 0
65 | with_items: "{{ test_results.results }}"
66 |
--------------------------------------------------------------------------------
/molecule/users/Dockerfile.j2:
--------------------------------------------------------------------------------
1 | # Molecule managed
2 |
3 | {% if item.registry is defined %}
4 | FROM {{ item.registry.url }}/{{ item.image }}
5 | {% else %}
6 | FROM {{ item.image }}
7 | {% endif %}
8 |
9 | RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get upgrade -y && apt-get install -y python sudo bash ca-certificates iproute2 && apt-get clean; \
10 | elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python2-dnf bash iproute && dnf clean all; \
11 | elif [ $(command -v yum) ]; then yum makecache fast && yum update -y && yum install -y python sudo yum-plugin-ovl bash iproute && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
12 | elif [ $(command -v zypper) ]; then zypper refresh && zypper update -y && zypper install -y python sudo bash python-xml && zypper clean -a; \
13 | elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
14 | elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi
15 |
--------------------------------------------------------------------------------
/molecule/users/molecule.yml:
--------------------------------------------------------------------------------
1 | ---
2 | dependency:
3 | name: galaxy
4 | driver:
5 | name: docker
6 | lint:
7 | name: yamllint
8 | platforms:
9 | - name: centos7
10 | image: solita/centos-systemd
11 | command: /sbin/init
12 | capabilities:
13 | - SYS_ADMIN
14 | volumes:
15 | - /sys/fs/cgroup:/sys/fs/cgroup:ro
16 | provisioner:
17 | name: ansible
18 | lint:
19 | name: ansible-lint
20 | scenario:
21 | name: users
22 | verifier:
23 | name: testinfra
24 | lint:
25 | name: flake8
26 |
--------------------------------------------------------------------------------
/molecule/users/playbook.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Converge
3 | hosts: all
4 | become: true
5 | roles:
6 | - role: 3proxy
7 | proxy_users:
8 | - { name: "asm0dey", hash: "$1$pL3Ho94u$2.wCxrLfacj82UMPJSy/6/" }
9 | - { name: "asm0dey2", hash: "$1$pL3Ho94u$2.wCxrLfacj82UMPJSy/6/" }
10 |
--------------------------------------------------------------------------------
/molecule/users/tests/test_default.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import testinfra.utils.ansible_runner
4 |
5 | testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
6 | os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('all')
7 |
8 |
9 | def test_service_runs(host):
10 | proxy = host.service("3proxy")
11 | assert proxy.is_running
12 | assert proxy.is_enabled
13 |
14 |
15 | def test_ports_open(host):
16 | assert host.socket("tcp://0.0.0.0:3128").is_listening
17 | assert host.socket("tcp://0.0.0.0:1080").is_listening
18 |
19 |
20 | def test_users_lines(host):
21 | cfg_path = ""
22 | if host.system_info.distribution == "ubuntu":
23 | cfg_path = "/etc/3proxy/3proxy.cfg"
24 | else:
25 | cfg_path = "/etc/3proxy.cfg"
26 | cfg = host.file(cfg_path)
27 | assert cfg.contains('users "asm0dey:CR:$1$pL3Ho94u$2.wCxrLfacj82UMPJSy/' +
28 | '6/" "asm0dey2:CR:$1$pL3Ho94u$2.wCxrLfacj82UMPJSy/6/"')
29 | assert cfg.contains("auth strong")
30 |
--------------------------------------------------------------------------------
/tasks/CentOS.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Installs libselinux-python
3 | package:
4 | name: libselinux-python
5 |
6 | - name: Ensures EPEL repo is installed
7 | yum_repository:
8 | name: epel
9 | description: EPEL YUM repo
10 | baseurl: https://download.fedoraproject.org/pub/epel/$releasever/$basearch/
11 |
12 | - name: Ensures 3proxy is installed
13 | yum:
14 | name: 3proxy
15 | state: present
16 | enablerepo: epel
17 | disable_gpg_check: true
18 |
19 | - name: Ensures 3proxy log dir exists
20 | file:
21 | name: /var/log/3proxy
22 | state: directory
23 |
24 | - name: Configures 3proxy
25 | template:
26 | src: 3proxy.cfg.j2
27 | dest: /etc/3proxy.cfg
28 | notify: "reload service"
29 |
--------------------------------------------------------------------------------
/tasks/Debian.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Ensures gpg is installed
3 | package:
4 | name: gnupg
5 |
6 | - name: Ensures iptables-persistent is installed
7 | package:
8 | name: iptables-persistent
9 | when: manage_firewall
10 |
11 | - name: Ensures 3proxy repository key is installed
12 | apt_key:
13 | keyserver: keyserver.ubuntu.com
14 | id: D8BAAED4A75968C1
15 |
16 | - name: Ensures 3proxy repository is added
17 | apt_repository:
18 | repo: deb http://ppa.launchpad.net/artyom.h31/3proxy/ubuntu xenial main
19 | state: present
20 | update_cache: true
21 | mode: 0644
22 | filename: artyom_h31-ubuntu-3proxy-xenial.list
23 |
24 | - name: Ensures 3proxy is installed
25 | package:
26 | name: 3proxy
27 |
28 | - name: Ensures 3proxy log dir exists
29 | file:
30 | name: /var/log/3proxy
31 | state: directory
32 | owner: nobody
33 | group: nogroup
34 |
35 | - name: Configures 3proxy
36 | template:
37 | src: 3proxy.cfg.j2
38 | dest: /etc/3proxy/3proxy.cfg
39 | owner: nobody
40 | group: nogroup
41 | notify: "reload service"
42 |
43 | - name: get info about 3proxy service
44 | systemd:
45 | name: 3proxy.service
46 | enabled: true
47 | when: ansible_service_mgr == 'systemd'
48 | register: proxy_info
49 |
50 | - name: Set log directory writable for 3proxy if needed
51 | ini_file:
52 | path: "{{ proxy_info.status.FragmentPath }}"
53 | section: Service
54 | option: Type
55 | value: forking
56 | when: ansible_service_mgr == 'systemd'
57 | notify: reload service
58 |
--------------------------------------------------------------------------------
/tasks/Fedora.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Installs libselinux-python
3 | package:
4 | name: libselinux-python
5 |
6 | - name: Ensures 3proxy is installed
7 | dnf:
8 | name: 3proxy
9 | state: present
10 |
11 | - name: Ensures 3proxy log dir exists
12 | file:
13 | name: /var/log/3proxy
14 | state: directory
15 |
16 | - name: Configures 3proxy
17 | template:
18 | src: 3proxy.cfg.j2
19 | dest: /etc/3proxy.cfg
20 | notify: "reload service"
21 |
--------------------------------------------------------------------------------
/tasks/Ubuntu.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Ensures 3proxy PPA is added
3 | apt_repository:
4 | repo: ppa:artyom.h31/3proxy
5 |
6 | - name: Ensures 3proxy is installed
7 | apt:
8 | name: 3proxy
9 | update_cache: true
10 |
11 | - name: Ensures 3proxy log dir exists
12 | file:
13 | name: /var/log/3proxy
14 | state: directory
15 | owner: nobody
16 | group: nogroup
17 |
18 | - name: Configures 3proxy
19 | template:
20 | src: 3proxy.cfg.j2
21 | dest: /etc/3proxy/3proxy.cfg
22 | owner: nobody
23 | group: nogroup
24 | notify: "reload service"
25 |
--------------------------------------------------------------------------------
/tasks/firewall.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # Everything here is borrowed from kyl191/ansible-role-openvpn and adapted for 3proxy
3 | - name: Check for firewalld
4 | command: which firewall-cmd
5 | register: firewalld
6 | check_mode: false
7 | changed_when: false # Never report as changed
8 | ignore_errors: true
9 |
10 | - name: Check for ufw
11 | command: which ufw
12 | register: ufw
13 | check_mode: false
14 | changed_when: false # Never report as changed
15 | ignore_errors: true
16 |
17 | - name: Check for iptables
18 | command: which iptables
19 | register: iptables
20 | check_mode: false
21 | changed_when: false # Never report as changed
22 | ignore_errors: true
23 |
24 | - name: Fail on both firewalld & ufw
25 | fail:
26 | msg: "Both FirewallD and UFW are detected, firewall situation is unknown"
27 | when: firewalld.rc == 0 and ufw.rc == 0
28 |
29 | - name: Add port rules (iptables)
30 | include: iptables.yml
31 | when: firewalld.rc != 0 and ufw.rc != 0 and iptables.rc == 0
32 |
33 | - name: Add port rules (firewalld)
34 | include: firewalld.yml
35 | when: firewalld.rc == 0 and ufw.rc != 0
36 |
37 | - name: Add port rules (ufw)
38 | include: ufw.yml
39 | when: firewalld.rc != 0 and ufw.rc == 0
40 |
--------------------------------------------------------------------------------
/tasks/firewalld.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Ensures firewalld socks port is open
3 | firewalld:
4 | state: enabled
5 | zone: public
6 | port: "{{ item.item }}/tcp"
7 | permanent: true
8 | immediate: false
9 | when: item.if
10 | with_items:
11 | - { item: "{{ proxy_socks_port }}", if: "{{ proxy_socks }}" }
12 | - { item: "{{ proxy_http_port }}", if: "{{ proxy_http }}" }
13 | notify: restart firewalld
14 |
--------------------------------------------------------------------------------
/tasks/iptables.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: Enable iptables rules
3 | iptables_raw:
4 | rules: "-A {{ item[1].chain }} -p tcp --dport {{ item[0].value }} -m conntrack --ctstate {{ item[1].ctstate }} -j ACCEPT"
5 | name: "Allow {{ item[1].chain }} for port {{ item[0].value }}"
6 | with_nested:
7 | - [ { value: "{{ proxy_socks_port }}", if: "{{ proxy_socks }}" }, { value: "{{ proxy_http_port }}", if: "{{ proxy_http }}" } ]
8 | - [ { ctstate: "NEW,ESTABLISHED", chain: "INPUT", }, { ctstate: "ESTABLISHED", chain: "OUTPUT" } ]
9 | when: item[0].if
10 |
--------------------------------------------------------------------------------
/tasks/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: gather os specific variables
3 | include: "{{ item }}"
4 | with_first_found:
5 | - "{{ ansible_distribution }}-{{ ansible_distribution_major_version}}.yml"
6 | - "{{ ansible_distribution }}.yml"
7 | tags: vars
8 |
9 | - name: get info about 3proxy service
10 | systemd:
11 | name: 3proxy.service
12 | enabled: true
13 | when: ansible_service_mgr == 'systemd'
14 | register: proxy_info
15 |
16 | - name: Set log directory writable for 3proxy if needed
17 | ini_file:
18 | path: "{{ proxy_info.status.FragmentPath }}"
19 | section: Service
20 | option: ReadWritePaths
21 | value: /var/log/3proxy
22 | when: ansible_service_mgr == 'systemd'
23 | notify: reload service
24 |
25 | - name: Ensures 3proxy service is started and enabled
26 | service:
27 | name: 3proxy
28 | enabled: true
29 | state: started
30 | ignore_errors: true
31 |
32 | - name: Detect firewall type
33 | include_tasks: firewall.yml
34 | when: manage_firewall
35 |
--------------------------------------------------------------------------------
/tasks/ufw.yml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: ufw - enable forwarding
3 | lineinfile:
4 | dest: /etc/default/ufw
5 | regexp: "^DEFAULT_FORWARD_POLICY="
6 | line: DEFAULT_FORWARD_POLICY="ACCEPT"
7 |
8 | - name: ufw - Allow incoming VPN connection
9 | ufw:
10 | direction: in
11 | proto: tcp
12 | to_port: "{{ item.item }}"
13 | rule: allow
14 | when: item.if
15 | with_items:
16 | - { item: "{{ proxy_socks_port }}", if: "{{ proxy_socks }}" }
17 | - { item: "{{ proxy_http_port }}", if: "{{ proxy_http }}" }
18 | notify: restart ufw
19 |
--------------------------------------------------------------------------------
/templates/3proxy.cfg.j2:
--------------------------------------------------------------------------------
1 | {% if ansible_distribution != 'Ubuntu' %}daemon{% endif %}
2 |
3 | nserver 8.8.8.8
4 | nserver 8.8.4.4
5 | nscache 65536
6 | log /var/log/3proxy/access.log D
7 | rotate 30
8 | archiver gz /bin/gzip %F
9 |
10 | {% if proxy_users | length > 0 %}
11 | auth strong
12 | {% endif %}
13 |
14 | external {{ ansible_default_ipv4["address"] }}
15 |
16 | {% if proxy_users | length > 0 %}
17 | users {% for user in proxy_users %}"{{ user.name }}:CR:{{ user.hash }}" {% endfor %}
18 | {% endif %}
19 |
20 | {% if proxy_socks %}
21 | socks -p{{ proxy_socks_port }} {{ proxy_socks_options }}
22 | {% endif %}
23 |
24 | {% if proxy_http %}
25 | proxy -p{{ proxy_http_port }} {{ proxy_http_options }}
26 | {% endif %}
--------------------------------------------------------------------------------
/vars/main.yml:
--------------------------------------------------------------------------------
1 | ---
2 | # vars file for 3proxy
3 |
--------------------------------------------------------------------------------