├── .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 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
nameversion

CentOS

6

7

Ubuntu

xenial

bionic

Fedora

26

27

28

52 | 53 | ## Role Variables 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
namedescription

proxy_users

array of users whch shold have access to proxy (otherwise anybody can)

proxy_socks

enable socks proxy (true by default)

proxy_socks_port

socks proxy port (1080 be default)

proxy_socks_options

additional socks proxy options

proxy_http

enable http proxy (true by default)

proxy_http_port

http proxy port (3128 be default)

proxy_http_options

additional http proxy options

manage_firewall

If role should try to allow incoming connections to proxy on firewall

101 | 102 | ## Proxy users 103 | 104 | Proxy user is an object, which consists of 2 fields: 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
namedescription

name

username

hash

hash of the password

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 | --------------------------------------------------------------------------------