├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── README.md
├── create_user.py
├── pfweb
├── __init__.py
├── app.py
├── config.py
├── constants.py
├── static
│ ├── css
│ │ ├── bootstrap.min.css
│ │ ├── datatables.min.css
│ │ ├── jquery-ui.min.css
│ │ └── main.css
│ ├── fonts
│ │ ├── glyphicons-halflings-regular.eot
│ │ ├── glyphicons-halflings-regular.svg
│ │ ├── glyphicons-halflings-regular.ttf
│ │ ├── glyphicons-halflings-regular.woff
│ │ └── glyphicons-halflings-regular.woff2
│ └── js
│ │ ├── bootstrap.min.js
│ │ ├── datatables.min.js
│ │ ├── jquery-3.1.1.min.js
│ │ ├── jquery-ui.min.js
│ │ └── main.js
└── templates
│ ├── base.html
│ ├── dash.html
│ ├── edit.html
│ ├── edit_rule.html
│ ├── edit_table.html
│ ├── error.html
│ ├── login.html
│ ├── pfinfo.html
│ ├── rules.html
│ ├── states.html
│ └── tables.html
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | env/
12 | build/
13 | develop-eggs/
14 | dist/
15 | downloads/
16 | eggs/
17 | .eggs/
18 | lib/
19 | lib64/
20 | parts/
21 | sdist/
22 | var/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 |
27 | # PyInstaller
28 | # Usually these files are written by a python script from a template
29 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
30 | *.manifest
31 | *.spec
32 |
33 | # Installer logs
34 | pip-log.txt
35 | pip-delete-this-directory.txt
36 |
37 | # Unit test / coverage reports
38 | htmlcov/
39 | .tox/
40 | .coverage
41 | .coverage.*
42 | .cache
43 | nosetests.xml
44 | coverage.xml
45 | *,cover
46 | .hypothesis/
47 |
48 | # Translations
49 | *.mo
50 | *.pot
51 |
52 | # Django stuff:
53 | *.log
54 | local_settings.py
55 |
56 | # Flask stuff:
57 | instance/
58 | .webassets-cache
59 |
60 | # Scrapy stuff:
61 | .scrapy
62 |
63 | # Sphinx documentation
64 | docs/_build/
65 |
66 | # PyBuilder
67 | target/
68 |
69 | # IPython Notebook
70 | .ipynb_checkpoints
71 |
72 | # pyenv
73 | .python-version
74 |
75 | # celery beat schedule file
76 | celerybeat-schedule
77 |
78 | # dotenv
79 | .env
80 |
81 | # virtualenv
82 | venv/
83 | ENV/
84 |
85 | # Spyder project settings
86 | .spyderproject
87 |
88 | # Rope project settings
89 | .ropeproject
90 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [0.1.0dev4] - 2016-12-10
4 | [Full Changelog](https://github.com/nahun/pfweb/compare/v0.1.0dev3...v0.1.0dev4)
5 |
6 | ### Added
7 | - Session lifetime
8 | - Direction icons in rules list
9 | - Table name validation
10 | - States details popover for each rule
11 | - Table details popover in rules list
12 | - Allow IPv6 NAT and RDR rules
13 | - Interface selection for source and destination
14 |
15 | ### Fixed
16 | - Fix default values in form fields
17 | - Error 400 page receives username
18 | - Fix reading interface media on dashboard
19 | - Fix default answer in create_user script
20 | - Fix IPv6 prefix length of 32 not showing
21 |
22 | ### Changed
23 | - Formatting on dashboard for numbers
24 | - Rename pfweb.py to app.py
25 | - Show deprecated IPv6 addresses in dashboard
26 | - Combine source and destination fields in states
27 | - Only allow NAT when direction is set to out
28 |
29 | ## [0.1.0dev3] - 2016-11-29
30 | [Full Changelog](https://github.com/nahun/pfweb/compare/0.1.0.dev2...v0.1.0dev3)
31 |
32 | ### Added
33 | - Support for rdr-to and nat-to rules
34 | - Initial support for global PF options in config file. State-policy is first.
35 |
36 | ### Fixed
37 | - Fix save order button being enabled even when no row was selected
38 | - Fix glyphicon font files not being installed with setuptools
39 | - Some formatting and sorting when listing states
40 | - Fix a previous fix of bootstrap css as now CSS and JS use their own directories
41 |
42 | ### Changed
43 | - Disable and enable the port fields. Easier to remember to change the port type.
44 |
45 | ## [0.1.0dev2] - 2016-11-25
46 | [Full Changelog](https://github.com/nahun/pfweb/compare/b6f7396...0.1.0.dev2)
47 |
48 | ### Added
49 | - Ability to change the order of rules
50 | - PF information on dashboard
51 | - pf.conf is now saved after each change making them persistent
52 | - Status menu items. Lists out entire PF info and the PF state table.
53 |
54 | ### Fixed
55 | - Fix setting ICMP type to ANY
56 |
57 | ### Changed
58 | - Autofocus on username field
59 | - Move to all local JS and CSS files instead of CDNs
60 |
61 | ## [0.1.0dev1] - 2016-11-21
62 | - Initial Release
63 |
64 | [0.1.0dev4]: https://github.com/nahun/pfweb/tree/v0.1.0dev4
65 | [0.1.0dev3]: https://github.com/nahun/pfweb/tree/v0.1.0dev3
66 | [0.1.0dev2]: https://github.com/nahun/pfweb/tree/0.1.0.dev2
67 | [0.1.0dev1]: https://github.com/nahun/pfweb/commit/b6f7396
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 2-Clause License
2 |
3 | Copyright (c) 2016, Nathan Wheeler
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include *.md
2 | recursive-include pfweb *.html *.js *.css *.eot *.svg *.ttf *.woff *.woff2
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pfweb
2 |
3 | pfweb is a python web application to manage the OpenBSD Packet Filter (PF). It
4 | uses *py-pf* to interface with PF and Flask for the web framework. The look
5 | and feel is based on pfSense and a lot of the ideas are ripped off from them.
6 |
7 | ## Warning!
8 | There are a lot of people that would say running a web interface for PF is a
9 | bad idea. There are many reasons why, but here are a couple good ones:
10 |
11 | - **Security**: Running pfweb requires the user running the application to have
12 | write access to */dev/pf* to make changes. This gives access to the kernel.
13 | - **Features**: When using a web application to manage PF instead of
14 | just modifying *pf.conf*, you lose massive amounts of powerful features and
15 | flexibility.
16 |
17 | So why would use you use pfweb? Maybe a home network or an already secured lab.
18 | I don't judge though, use it how you want. I've had fun making it as well.
19 |
20 | ### Development
21 |
22 | As of Nov 2016 pfweb is under initial development. Use at your own risk.
23 |
24 | ## Dependencies
25 |
26 | - [OpenBSD 6.0+](http://www.openbsd.org/): Only tested on OpenBSD 6.0 amd64
27 | - [py-pf](http://www.kernel-panic.it/software/py-pf/): Python module for
28 | managing OpenBSD's Packet Filter
29 | - [Flask](http://flask.pocoo.org/): A microframework for Python based on
30 | Werkzeug and Jinja 2
31 | - [Flask-Login](https://flask-login.readthedocs.io/): User session management
32 | for Flask
33 |
34 | ## Installation
35 |
36 | Installation under a virtualenv will work fine.
37 |
38 | pfweb utilizes the well written
39 | [py-pf module](http://www.kernel-panic.it/software/py-pf/). The version in
40 | PyPi is not up to date so you'll need to clone from the
41 | [py-pf github repo](https://github.com/dotpy/py-pf) and install.
42 |
43 | ```sh
44 | $ git clone https://github.com/dotpy/py-pf.git
45 | $ cd py-pf
46 | $ python setup.py install
47 | ```
48 |
49 | pfweb is under heavy development right now, so it is probably best to clone
50 | from github. First install Flask and Flask-Login then install pfweb.
51 |
52 | ```sh
53 | $ pip install Flask flask-login
54 | $ git clone https://github.com/nahun/pfweb.git
55 | $ cd pfweb
56 | $ python setup.py install
57 | ```
58 |
59 | Or if the version on PyPi is actually current you can use pip:
60 |
61 | ```sh
62 | $ pip install pfweb
63 | ```
64 |
65 | ## Setup
66 |
67 | After installation, you'll have to decide how you want to run it. There are
68 | many options for python web applications such as FastCGI, mod_wsgi, uWSGI,
69 | etc... You can refer to
70 | [Flask's documentation](http://flask.pocoo.org/docs/0.11/deploying/#deployment)
71 | for more detail, but this guide will concentrate on FastCGI, flup, and
72 | OpenBSD's httpd
73 |
74 | ### Install flup
75 |
76 | You'll need to install [flup](https://pypi.python.org/pypi/flup/1.0.2), the
77 | FastCGI server:
78 |
79 | ```sh
80 | $ pip install flup
81 | ```
82 |
83 | ### Create the FastCGI Server
84 |
85 | Then you need to create a FastCGI server file such as *pfweb.fcgi*:
86 |
87 | ```python
88 | from flup.server.fcgi import WSGIServer
89 | import pfweb
90 |
91 | if __name__ == '__main__':
92 | WSGIServer(pfweb.app, bindAddress='/var/www/run/pfweb.sock').run()
93 | ```
94 |
95 | Make sure the socket file path is in httpd's chroot of /var/www otherwise
96 | httpd won't be able to read it.
97 |
98 | ### Setup httpd
99 |
100 | Setup `/etc/httpd.conf` to use the fastcgi socket and listen on your IP. Edit
101 | the certificate paths with your own.
102 |
103 | ```
104 | domain="example.com"
105 |
106 | server $domain {
107 | listen on 1.2.3.4 port 80
108 | block return 301 "https://$SERVER_NAME$REQUEST_URI"
109 | }
110 |
111 | server $domain {
112 | listen on 1.2.3.4 tls port 443
113 | fastcgi socket "/run/pfweb.sock"
114 |
115 | tls {
116 | certificate "/etc/ssl/example.com.crt"
117 | key "/etc/ssl/private/example.com.key"
118 | }
119 | }
120 | ```
121 |
122 | Remember, httpd runs in a chroot under /var/www so set your fastcgi socket
123 | accordingly.
124 |
125 | ### PF Permissions
126 |
127 | You'll need to give a user access to /dev/pf and /etc/pf.conf so we don't run
128 | anything as root. Create or use whichever group you want, we'll use *pfweb*.
129 | Also make sure the user running your webserver and FastCGI server is a member
130 | of that group.
131 |
132 | ```
133 | # chown root:pfweb /dev/pf /etc/pf.conf
134 | # chmod g+rw /dev/pf /etc/pf.conf
135 | ```
136 |
137 | ### Create a Config File
138 |
139 | Now lets create the config file and username and password used to login to
140 | pfweb. The *pfweb.ini* file can exist at:
141 |
142 | - ~/.pfweb.ini
143 | - /etc/pfweb.ini
144 | - /usr/local/etc/pfweb.ini
145 |
146 | pfweb will choose from that order. There are two required parameters that you
147 | must set manually, the Flask *secret_key* used in sessions and a *salt* to hash
148 | the password we'll be setting for authentication. Create the pfweb.ini with
149 | random strings used for these two parameters.
150 |
151 | ```ini
152 | [main]
153 | secret_key = longrandomstring
154 | salt = anotherrandomstring
155 | ```
156 |
157 | ### Create a Username and Password
158 |
159 | There are a few ways we can accomplish creating the username and password:
160 |
161 | #### 1. Use `create_user.py`
162 |
163 | The script is distributed in the package so you will need to find it in your
164 | installation or just [download](create_user.py?raw=true) it from the
165 | repo.
166 |
167 | ```sh
168 | $ python create_user.py
169 | Enter username: admin
170 | Enter Password:
171 | Confirm Password:
172 | User tester created successfully
173 | ```
174 |
175 | #### 2. pfweb Config() manually
176 |
177 | You can import the pfweb Config object and create the credentials manually:
178 |
179 | ```python
180 | >>> from pfweb.config import Config
181 | >>> c = Config()
182 | >>> c.create_user('your_username', 'your_password')
183 | ```
184 |
185 | #### 3. Manually hash your password
186 |
187 | Using python you can hash your password and enter it in manually to the config
188 | file. This will hash the password the same way the Config.create_user() method
189 | does.
190 |
191 | ```python
192 | >>> import hashlib, binascii
193 | >>> salt='your_salt'
194 | >>> password='your_password'
195 | >>> dk = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 100000)
196 | >>> binascii.hexlify(dk)
197 | '26383a0418be31cb6906418b367e19eb404cb8296e8f03521244b21cc079b82c'
198 | ```
199 |
200 | Copy that hash and paste it into your config file with your username:
201 |
202 | ```ini
203 | [main]
204 | secret_key = longrandomstring
205 | salt = anotherrandomstring
206 | username = admin
207 | password = 26383a0418be31cb6906418b367e19eb404cb8296e8f03521244b21cc079b82c
208 | ```
209 |
210 | ### Run the servers
211 |
212 | Run the FastCGI server and (re)start httpd. The httpd_flags="" command is
213 | obviously optional if you already have it running. Make sure to run the
214 | FastCGI server as the correct user that has access to PF.
215 | ```sh
216 | $ python pfweb.fcgi
217 | ```
218 |
219 | ```
220 | # echo httpd_flags="" >> /etc/rc.conf.local
221 | # rcctl restart httpd
222 | ```
223 |
224 | You should now be able be able to reach pfweb in your browser
225 |
226 | ## Considerations
227 |
228 | ### FastCGI Process Managers
229 |
230 | Just as the
231 | [Flask docs](http://flask.pocoo.org/docs/0.11/deploying/fastcgi/#running-fastcgi-processes)
232 | say, you may want to use a process manager for your FastCGI server.
233 | [Supervisor](http://supervisord.org/configuration.html#fcgi-program-x-section-settings)
234 | works and OpenBSD has a package. Install with `pkg_add supervisor` then create
235 | a config file at `/etc/supervisord.d/pfweb.ini`
236 |
237 | ```ini
238 | [fcgi-program:pfweb]
239 | socket=unix:///var/www/run/pfweb.sock
240 | command=/path/to/python /path/to/pfweb.fcgi
241 | socket_owner=www
242 | user=www
243 | process_name=%(program_name)s_%(process_num)02d
244 | numprocs=5
245 | autostart=true
246 | autorestart=true
247 | ```
248 |
249 | Edit `/etc/supervisord.conf` and uncomment the two lines at the end:
250 |
251 | ```ini
252 | [include]
253 | files = supervisord.d/*.ini
254 | ```
255 |
256 | Restart supervisord and you should be good to go.
257 |
258 | ## Screenshots
259 |
260 | [Imgur Album](http://imgur.com/a/JsqCF)
261 |
--------------------------------------------------------------------------------
/create_user.py:
--------------------------------------------------------------------------------
1 | #!/usr/local/bin/python2.7
2 | from pfweb.config import Config
3 |
4 | from getpass import getpass
5 |
6 | config = Config()
7 |
8 | username = ""
9 | while True:
10 | prompt = 'Enter username: '
11 | if username != "":
12 | prompt = 'Enter username [{}]: '.format(username)
13 |
14 | username_input = raw_input(prompt)
15 | if username_input != "":
16 | username = username_input
17 |
18 | if username == "":
19 | print "Username cannot be blank"
20 | continue
21 |
22 | password1 = getpass('Enter Password: ')
23 | password2 = getpass('Confirm Password: ')
24 |
25 | if password1 == password2:
26 | break
27 | else:
28 | print "Passwords do not match"
29 |
30 | config.create_user(username, password1)
31 |
32 | print "User " + username + " created successfully"
--------------------------------------------------------------------------------
/pfweb/__init__.py:
--------------------------------------------------------------------------------
1 | from pfweb.app import app
2 | __all__ = ['app', 'config', 'constants']
3 |
--------------------------------------------------------------------------------
/pfweb/app.py:
--------------------------------------------------------------------------------
1 | import pf
2 | import socket
3 | import json
4 | import platform, subprocess, time, shutil
5 | from datetime import timedelta
6 |
7 | from flask import Flask, session, render_template, redirect, url_for, request, jsonify
8 | import flask_login
9 |
10 | from pfweb.config import Config
11 | from pfweb.constants import *
12 |
13 | # Get config settings
14 | settings = Config()
15 | settings.get_settings()
16 |
17 | # Setup Flask
18 | app = Flask(__name__)
19 | app.secret_key = settings.secret_key
20 | # Set session lifetime
21 | app.permanent_session_lifetime = timedelta(minutes=240)
22 |
23 | # Setup Login Manager extension
24 | login_manager = flask_login.LoginManager()
25 | login_manager.session_protection = "strong"
26 | login_manager.init_app(app)
27 |
28 | # Load packet filter to be used in views
29 | packetfilter = pf.PacketFilter()
30 |
31 | class BadRequestError(Exception):
32 | """HTTP 400"""
33 |
34 | class User(flask_login.UserMixin):
35 | """Flask Login User Class"""
36 |
37 | @login_manager.user_loader
38 | def user_loader(username):
39 | """Flask Login User Loader"""
40 |
41 | if username != settings.username:
42 | return None
43 |
44 | user = User()
45 | user.id = username
46 | return user
47 |
48 | @login_manager.unauthorized_handler
49 | def unauthorized_handler():
50 | """Redirect to the login page when not authenticated"""
51 | return redirect(url_for('login'))
52 |
53 | @app.before_request
54 | def before_request():
55 | """Operations performed on every request"""
56 |
57 | # Reset session timer
58 | session.modified = True
59 |
60 | @app.route("/login", methods=['GET', 'POST'])
61 | def login():
62 | """Show login page and authenticate user"""
63 |
64 | if not settings.username or not settings.password:
65 | return "Config Error: username or password is not set"
66 |
67 | # Initialize alert message
68 | message = None
69 |
70 | # Process form fields
71 | if request.method == 'POST':
72 | # Process user login
73 | if request.form.get('login.submitted'):
74 | username = request.form.get('username')
75 | if username == settings.username and settings.hash_password(request.form.get('password')) == settings.password:
76 | user = User()
77 | user.id = username
78 | flask_login.login_user(user)
79 | return redirect(url_for('dash'))
80 | else:
81 | message = "Bad username or password"
82 |
83 | if settings.username == None or settings.password == None:
84 | # Show error about no initial user. Should never actually happen
85 | message = "Initial user not yet created"
86 | return render_template('login.html', no_login=True, message=message)
87 |
88 | return render_template('login.html', message=message)
89 |
90 | @app.route('/logout')
91 | def logout():
92 | """Logout user and redirect to home"""
93 |
94 | flask_login.logout_user()
95 | return redirect(url_for('dash'))
96 |
97 | @app.route("/")
98 | @flask_login.login_required
99 | def dash():
100 | """Show home dashboard"""
101 |
102 | # Get uptime
103 | current_time = int(time.time())
104 | uptime_seconds = int(subprocess.check_output(["/sbin/sysctl", "-n", "kern.boottime"]))
105 | uptime_delta = timedelta(seconds=(current_time - uptime_seconds))
106 |
107 | # Place info in a dict
108 | sys_info = {
109 | 'hostname': socket.getfqdn(),
110 | 'os': "{} {} ({})".format(platform.system(), platform.release(), platform.machine()),
111 | 'uptime': str(uptime_delta),
112 | 'current_time': time.strftime("%a, %b %d %Y %H:%M:%S %Z", time.localtime())
113 | }
114 |
115 | # Interfaces to skip for stats
116 | skip_ifaces = ['all', 'carp', 'egress', 'enc', 'enc0', 'lo', 'lo0', 'pflog', 'pflog0']
117 | # Type of stats to use
118 | stats = ['Rules', 'States', 'Packets In', 'Packets Out', 'Bytes In', 'Bytes Out']
119 | # Initialize the structures to hold the data
120 | if_stats = dict()
121 | if_info = list()
122 |
123 | # Start the table string for stats
124 | ifstats_output = "";
125 |
126 | # Go through each interface
127 | for iface in packetfilter.get_ifaces():
128 | if iface.name not in skip_ifaces:
129 | # Add up all the packet and bytes
130 | packets_in = iface.packets["in"][pf.PF_PASS][0] + iface.packets["in"][pf.PF_DROP][0] + iface.packets["in"][pf.PF_PASS][1] + iface.packets["in"][pf.PF_DROP][1]
131 | packets_out = iface.packets["out"][pf.PF_PASS][0] + iface.packets["out"][pf.PF_DROP][0] + iface.packets["out"][pf.PF_PASS][1] + iface.packets["out"][pf.PF_DROP][1]
132 | bytes_in = iface.bytes["in"][pf.PF_PASS][0] + iface.bytes["in"][pf.PF_DROP][0] + iface.bytes["in"][pf.PF_PASS][1] + iface.bytes["in"][pf.PF_DROP][1]
133 | bytes_out = iface.bytes["out"][pf.PF_PASS][0] + iface.bytes["out"][pf.PF_DROP][0] + iface.bytes["out"][pf.PF_PASS][1] + iface.bytes["out"][pf.PF_DROP][1]
134 |
135 | # Store each into a dict
136 | if_stats[iface.name] = {
137 | 'name': iface.name,
138 | 'Rules': iface.rules,
139 | 'States': iface.states,
140 | 'Packets In': sizeof_fmt(packets_in, num_type='int'),
141 | 'Packets Out': sizeof_fmt(packets_out, num_type='int'),
142 | 'Bytes In': sizeof_fmt(bytes_in),
143 | 'Bytes Out': sizeof_fmt(bytes_out)
144 | }
145 |
146 | # Add interface header to table
147 | ifstats_output += " {} ".format(iface.name)
148 |
149 | # Gather interface info from ifconfig and split into array of lines
150 | ifconfig = subprocess.check_output(["/sbin/ifconfig", str(iface.name)]).splitlines()
151 | # Initialize dict for interface info. Sets used on IPs so only shows uniques
152 | if_info_dict = {
153 | 'ipv4': set(),
154 | 'ipv6': set(),
155 | 'media': None,
156 | 'status': None,
157 | 'name': iface.name
158 | }
159 | # Run through each line of ifconfig output
160 | for line in ifconfig:
161 | # Remove first tab and split line into fields of words
162 | line = line.replace('\t', '', 1).split(" ")
163 | if line[0] == 'inet':
164 | # IPv4 addresses
165 | if_info_dict['ipv4'].add(line[1])
166 | elif line[0] == 'inet6' and not line[1].startswith('fe80'):
167 | # IPv6 addresses. Skip local and deprecated IPs
168 | if_info_dict['ipv6'].add((line[1], True if 'deprecated' in line else False))
169 | elif line[0] == 'media:':
170 | # Get speed and duplex
171 | if line[2] == 'autoselect':
172 | if_info_dict['media'] = "{} {}".format(line[3].strip("("), line[4].strip(")").split(",")[0])
173 | else:
174 | if_info_dict['media'] = "{} {}".format(line[2], line[3].split(",")[0])
175 | elif line[0] == 'status:':
176 | # Show whether interface is up or down
177 | if line[1] == 'active':
178 | if_info_dict['status'] = True
179 | else:
180 | if_info_dict['status'] = False
181 |
182 | # Add info to overall list
183 | if_info.append(if_info_dict)
184 |
185 | # Continue to body in stats table
186 | ifstats_output += "
")[0],f=t.each;p.style.cssText="background-color:rgba(1,1,1,.5)",d.rgba=p.style.backgroundColor.indexOf("rgba")>-1,f(c,function(t,e){e.cache="_"+t,e.props.alpha={idx:3,type:"percent",def:1}}),h.fn=t.extend(h.prototype,{parse:function(n,a,r,l){if(n===e)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(a),a=e);var u=this,d=t.type(n),p=this._rgba=[];return a!==e&&(n=[n,a,r,l],d="array"),"string"===d?this.parse(s(n)||o._default):"array"===d?(f(c.rgba.props,function(t,e){p[e.idx]=i(n[e.idx],e)}),this):"object"===d?(n instanceof h?f(c,function(t,e){n[e.cache]&&(u[e.cache]=n[e.cache].slice())}):f(c,function(e,s){var o=s.cache;f(s.props,function(t,e){if(!u[o]&&s.to){if("alpha"===t||null==n[t])return;u[o]=s.to(u._rgba)}u[o][e.idx]=i(n[t],e,!0)}),u[o]&&0>t.inArray(null,u[o].slice(0,3))&&(u[o][3]=1,s.from&&(u._rgba=s.from(u[o])))}),this):e},is:function(t){var i=h(t),s=!0,n=this;return f(c,function(t,o){var a,r=i[o.cache];return r&&(a=n[o.cache]||o.to&&o.to(n._rgba)||[],f(o.props,function(t,i){return null!=r[i.idx]?s=r[i.idx]===a[i.idx]:e})),s}),s},_space:function(){var t=[],e=this;return f(c,function(i,s){e[s.cache]&&t.push(i)}),t.pop()},transition:function(t,e){var s=h(t),n=s._space(),o=c[n],a=0===this.alpha()?h("transparent"):this,r=a[o.cache]||o.to(a._rgba),l=r.slice();return s=s[o.cache],f(o.props,function(t,n){var o=n.idx,a=r[o],h=s[o],c=u[n.type]||{};null!==h&&(null===a?l[o]=h:(c.mod&&(h-a>c.mod/2?a+=c.mod:a-h>c.mod/2&&(a-=c.mod)),l[o]=i((h-a)*e+a,n)))}),this[n](l)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),s=i.pop(),n=h(e)._rgba;return h(t.map(i,function(t,e){return(1-s)*n[e]+s*t}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(t,e){return null==t?e>2?1:0:t});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(t,e){return null==t&&(t=e>2?1:0),e&&3>e&&(t=Math.round(100*t)+"%"),t});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),s=i.pop();return e&&i.push(~~(255*s)),"#"+t.map(i,function(t){return t=(t||0).toString(16),1===t.length?"0"+t:t}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),h.fn.parse.prototype=h.fn,c.hsla.to=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e,i,s=t[0]/255,n=t[1]/255,o=t[2]/255,a=t[3],r=Math.max(s,n,o),l=Math.min(s,n,o),h=r-l,c=r+l,u=.5*c;return e=l===r?0:s===r?60*(n-o)/h+360:n===r?60*(o-s)/h+120:60*(s-n)/h+240,i=0===h?0:.5>=u?h/c:h/(2-c),[Math.round(e)%360,i,u,null==a?1:a]},c.hsla.from=function(t){if(null==t[0]||null==t[1]||null==t[2])return[null,null,null,t[3]];var e=t[0]/360,i=t[1],s=t[2],o=t[3],a=.5>=s?s*(1+i):s+i-s*i,r=2*s-a;return[Math.round(255*n(r,a,e+1/3)),Math.round(255*n(r,a,e)),Math.round(255*n(r,a,e-1/3)),o]},f(c,function(s,n){var o=n.props,a=n.cache,l=n.to,c=n.from;h.fn[s]=function(s){if(l&&!this[a]&&(this[a]=l(this._rgba)),s===e)return this[a].slice();var n,r=t.type(s),u="array"===r||"object"===r?s:arguments,d=this[a].slice();return f(o,function(t,e){var s=u["object"===r?t:e.idx];null==s&&(s=d[e.idx]),d[e.idx]=i(s,e)}),c?(n=h(c(d)),n[a]=d,n):h(d)},f(o,function(e,i){h.fn[e]||(h.fn[e]=function(n){var o,a=t.type(n),l="alpha"===e?this._hsla?"hsla":"rgba":s,h=this[l](),c=h[i.idx];return"undefined"===a?c:("function"===a&&(n=n.call(this,c),a=t.type(n)),null==n&&i.empty?this:("string"===a&&(o=r.exec(n),o&&(n=c+parseFloat(o[2])*("+"===o[1]?1:-1))),h[i.idx]=n,this[l](h)))})})}),h.hook=function(e){var i=e.split(" ");f(i,function(e,i){t.cssHooks[i]={set:function(e,n){var o,a,r="";if("transparent"!==n&&("string"!==t.type(n)||(o=s(n)))){if(n=h(o||n),!d.rgba&&1!==n._rgba[3]){for(a="backgroundColor"===i?e.parentNode:e;(""===r||"transparent"===r)&&a&&a.style;)try{r=t.css(a,"backgroundColor"),a=a.parentNode}catch(l){}n=n.blend(r&&"transparent"!==r?r:"_default")}n=n.toRgbaString()}try{e.style[i]=n}catch(l){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=h(e.elem,i),e.end=h(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}})},h.hook(a),t.cssHooks.borderColor={expand:function(t){var e={};return f(["Top","Right","Bottom","Left"],function(i,s){e["border"+s+"Color"]=t}),e}},o=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(n),function(){function e(e){var i,s,n=e.ownerDocument.defaultView?e.ownerDocument.defaultView.getComputedStyle(e,null):e.currentStyle,o={};if(n&&n.length&&n[0]&&n[n[0]])for(s=n.length;s--;)i=n[s],"string"==typeof n[i]&&(o[t.camelCase(i)]=n[i]);else for(i in n)"string"==typeof n[i]&&(o[i]=n[i]);return o}function i(e,i){var s,n,a={};for(s in i)n=i[s],e[s]!==n&&(o[s]||(t.fx.step[s]||!isNaN(parseFloat(n)))&&(a[s]=n));return a}var s=["add","remove","toggle"],o={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};t.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(e,i){t.fx.step[i]=function(t){("none"!==t.end&&!t.setAttr||1===t.pos&&!t.setAttr)&&(n.style(t.elem,i,t.end),t.setAttr=!0)}}),t.fn.addBack||(t.fn.addBack=function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}),t.effects.animateClass=function(n,o,a,r){var l=t.speed(o,a,r);return this.queue(function(){var o,a=t(this),r=a.attr("class")||"",h=l.children?a.find("*").addBack():a;h=h.map(function(){var i=t(this);return{el:i,start:e(this)}}),o=function(){t.each(s,function(t,e){n[e]&&a[e+"Class"](n[e])})},o(),h=h.map(function(){return this.end=e(this.el[0]),this.diff=i(this.start,this.end),this}),a.attr("class",r),h=h.map(function(){var e=this,i=t.Deferred(),s=t.extend({},l,{queue:!1,complete:function(){i.resolve(e)}});return this.el.animate(this.diff,s),i.promise()}),t.when.apply(t,h.get()).done(function(){o(),t.each(arguments,function(){var e=this.el;t.each(this.diff,function(t){e.css(t,"")})}),l.complete.call(a[0])})})},t.fn.extend({addClass:function(e){return function(i,s,n,o){return s?t.effects.animateClass.call(this,{add:i},s,n,o):e.apply(this,arguments)}}(t.fn.addClass),removeClass:function(e){return function(i,s,n,o){return arguments.length>1?t.effects.animateClass.call(this,{remove:i},s,n,o):e.apply(this,arguments)}}(t.fn.removeClass),toggleClass:function(e){return function(i,s,n,o,a){return"boolean"==typeof s||void 0===s?n?t.effects.animateClass.call(this,s?{add:i}:{remove:i},n,o,a):e.apply(this,arguments):t.effects.animateClass.call(this,{toggle:i},s,n,o)}}(t.fn.toggleClass),switchClass:function(e,i,s,n,o){return t.effects.animateClass.call(this,{add:i,remove:e},s,n,o)}})}(),function(){function n(e,i,s,n){return t.isPlainObject(e)&&(i=e,e=e.effect),e={effect:e},null==i&&(i={}),t.isFunction(i)&&(n=i,s=null,i={}),("number"==typeof i||t.fx.speeds[i])&&(n=s,s=i,i={}),t.isFunction(s)&&(n=s,s=null),i&&t.extend(e,i),s=s||i.duration,e.duration=t.fx.off?0:"number"==typeof s?s:s in t.fx.speeds?t.fx.speeds[s]:t.fx.speeds._default,e.complete=n||i.complete,e}function o(e){return!e||"number"==typeof e||t.fx.speeds[e]?!0:"string"!=typeof e||t.effects.effect[e]?t.isFunction(e)?!0:"object"!=typeof e||e.effect?!1:!0:!0}function a(t,e){var i=e.outerWidth(),s=e.outerHeight(),n=/^rect\((-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto),?\s*(-?\d*\.?\d*px|-?\d+%|auto)\)$/,o=n.exec(t)||["",0,i,s,0];return{top:parseFloat(o[1])||0,right:"auto"===o[2]?i:parseFloat(o[2]),bottom:"auto"===o[3]?s:parseFloat(o[3]),left:parseFloat(o[4])||0}}t.expr&&t.expr.filters&&t.expr.filters.animated&&(t.expr.filters.animated=function(e){return function(i){return!!t(i).data(s)||e(i)}}(t.expr.filters.animated)),t.uiBackCompat!==!1&&t.extend(t.effects,{save:function(t,i){for(var s=0,n=i.length;n>s;s++)null!==i[s]&&t.data(e+i[s],t[0].style[i[s]])},restore:function(t,i){for(var s,n=0,o=i.length;o>n;n++)null!==i[n]&&(s=t.data(e+i[n]),t.css(i[n],s))},setMode:function(t,e){return"toggle"===e&&(e=t.is(":hidden")?"show":"hide"),e},createWrapper:function(e){if(e.parent().is(".ui-effects-wrapper"))return e.parent();var i={width:e.outerWidth(!0),height:e.outerHeight(!0),"float":e.css("float")},s=t("
").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),n={width:e.width(),height:e.height()},o=document.activeElement;try{o.id}catch(a){o=document.body}return e.wrap(s),(e[0]===o||t.contains(e[0],o))&&t(o).trigger("focus"),s=e.parent(),"static"===e.css("position")?(s.css({position:"relative"}),e.css({position:"relative"})):(t.extend(i,{position:e.css("position"),zIndex:e.css("z-index")}),t.each(["top","left","bottom","right"],function(t,s){i[s]=e.css(s),isNaN(parseInt(i[s],10))&&(i[s]="auto")}),e.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),e.css(n),s.css(i).show()},removeWrapper:function(e){var i=document.activeElement;return e.parent().is(".ui-effects-wrapper")&&(e.parent().replaceWith(e),(e[0]===i||t.contains(e[0],i))&&t(i).trigger("focus")),e}}),t.extend(t.effects,{version:"1.12.1",define:function(e,i,s){return s||(s=i,i="effect"),t.effects.effect[e]=s,t.effects.effect[e].mode=i,s},scaledDimensions:function(t,e,i){if(0===e)return{height:0,width:0,outerHeight:0,outerWidth:0};var s="horizontal"!==i?(e||100)/100:1,n="vertical"!==i?(e||100)/100:1;return{height:t.height()*n,width:t.width()*s,outerHeight:t.outerHeight()*n,outerWidth:t.outerWidth()*s}},clipToBox:function(t){return{width:t.clip.right-t.clip.left,height:t.clip.bottom-t.clip.top,left:t.clip.left,top:t.clip.top}},unshift:function(t,e,i){var s=t.queue();e>1&&s.splice.apply(s,[1,0].concat(s.splice(e,i))),t.dequeue()},saveStyle:function(t){t.data(i,t[0].style.cssText)},restoreStyle:function(t){t[0].style.cssText=t.data(i)||"",t.removeData(i)},mode:function(t,e){var i=t.is(":hidden");return"toggle"===e&&(e=i?"show":"hide"),(i?"hide"===e:"show"===e)&&(e="none"),e},getBaseline:function(t,e){var i,s;switch(t[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=t[0]/e.height}switch(t[1]){case"left":s=0;break;case"center":s=.5;break;case"right":s=1;break;default:s=t[1]/e.width}return{x:s,y:i}},createPlaceholder:function(i){var s,n=i.css("position"),o=i.position();return i.css({marginTop:i.css("marginTop"),marginBottom:i.css("marginBottom"),marginLeft:i.css("marginLeft"),marginRight:i.css("marginRight")}).outerWidth(i.outerWidth()).outerHeight(i.outerHeight()),/^(static|relative)/.test(n)&&(n="absolute",s=t("<"+i[0].nodeName+">").insertAfter(i).css({display:/^(inline|ruby)/.test(i.css("display"))?"inline-block":"block",visibility:"hidden",marginTop:i.css("marginTop"),marginBottom:i.css("marginBottom"),marginLeft:i.css("marginLeft"),marginRight:i.css("marginRight"),"float":i.css("float")}).outerWidth(i.outerWidth()).outerHeight(i.outerHeight()).addClass("ui-effects-placeholder"),i.data(e+"placeholder",s)),i.css({position:n,left:o.left,top:o.top}),s},removePlaceholder:function(t){var i=e+"placeholder",s=t.data(i);s&&(s.remove(),t.removeData(i))},cleanUp:function(e){t.effects.restoreStyle(e),t.effects.removePlaceholder(e)},setTransition:function(e,i,s,n){return n=n||{},t.each(i,function(t,i){var o=e.cssUnit(i);o[0]>0&&(n[i]=o[0]*s+o[1])}),n}}),t.fn.extend({effect:function(){function e(e){function n(){l.removeData(s),t.effects.cleanUp(l),"hide"===i.mode&&l.hide(),r()}function r(){t.isFunction(h)&&h.call(l[0]),t.isFunction(e)&&e()}var l=t(this);i.mode=u.shift(),t.uiBackCompat===!1||a?"none"===i.mode?(l[c](),r()):o.call(l[0],i,n):(l.is(":hidden")?"hide"===c:"show"===c)?(l[c](),r()):o.call(l[0],i,r)}var i=n.apply(this,arguments),o=t.effects.effect[i.effect],a=o.mode,r=i.queue,l=r||"fx",h=i.complete,c=i.mode,u=[],d=function(e){var i=t(this),n=t.effects.mode(i,c)||a;i.data(s,!0),u.push(n),a&&("show"===n||n===a&&"hide"===n)&&i.show(),a&&"none"===n||t.effects.saveStyle(i),t.isFunction(e)&&e()};return t.fx.off||!o?c?this[c](i.duration,h):this.each(function(){h&&h.call(this)}):r===!1?this.each(d).each(e):this.queue(l,d).queue(l,e)},show:function(t){return function(e){if(o(e))return t.apply(this,arguments);var i=n.apply(this,arguments);return i.mode="show",this.effect.call(this,i)}}(t.fn.show),hide:function(t){return function(e){if(o(e))return t.apply(this,arguments);var i=n.apply(this,arguments);return i.mode="hide",this.effect.call(this,i)}}(t.fn.hide),toggle:function(t){return function(e){if(o(e)||"boolean"==typeof e)return t.apply(this,arguments);var i=n.apply(this,arguments);return i.mode="toggle",this.effect.call(this,i)}}(t.fn.toggle),cssUnit:function(e){var i=this.css(e),s=[];return t.each(["em","px","%","pt"],function(t,e){i.indexOf(e)>0&&(s=[parseFloat(i),e])}),s},cssClip:function(t){return t?this.css("clip","rect("+t.top+"px "+t.right+"px "+t.bottom+"px "+t.left+"px)"):a(this.css("clip"),this)},transfer:function(e,i){var s=t(this),n=t(e.to),o="fixed"===n.css("position"),a=t("body"),r=o?a.scrollTop():0,l=o?a.scrollLeft():0,h=n.offset(),c={top:h.top-r,left:h.left-l,height:n.innerHeight(),width:n.innerWidth()},u=s.offset(),d=t("").appendTo("body").addClass(e.className).css({top:u.top-r,left:u.left-l,height:s.innerHeight(),width:s.innerWidth(),position:o?"fixed":"absolute"}).animate(c,e.duration,e.easing,function(){d.remove(),t.isFunction(i)&&i()})}}),t.fx.step.clip=function(e){e.clipInit||(e.start=t(e.elem).cssClip(),"string"==typeof e.end&&(e.end=a(e.end,e.elem)),e.clipInit=!0),t(e.elem).cssClip({top:e.pos*(e.end.top-e.start.top)+e.start.top,right:e.pos*(e.end.right-e.start.right)+e.start.right,bottom:e.pos*(e.end.bottom-e.start.bottom)+e.start.bottom,left:e.pos*(e.end.left-e.start.left)+e.start.left})}}(),function(){var e={};t.each(["Quad","Cubic","Quart","Quint","Expo"],function(t,i){e[i]=function(e){return Math.pow(e,t+2)}}),t.extend(e,{Sine:function(t){return 1-Math.cos(t*Math.PI/2)},Circ:function(t){return 1-Math.sqrt(1-t*t)},Elastic:function(t){return 0===t||1===t?t:-Math.pow(2,8*(t-1))*Math.sin((80*(t-1)-7.5)*Math.PI/15)},Back:function(t){return t*t*(3*t-2)},Bounce:function(t){for(var e,i=4;((e=Math.pow(2,--i))-1)/11>t;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*e-2)/22-t,2)}}),t.each(e,function(e,i){t.easing["easeIn"+e]=i,t.easing["easeOut"+e]=function(t){return 1-i(1-t)},t.easing["easeInOut"+e]=function(t){return.5>t?i(2*t)/2:1-i(-2*t+2)/2}})}(),t.effects,t.effects.define("highlight","show",function(e,i){var s=t(this),n={backgroundColor:s.css("backgroundColor")};"hide"===e.mode&&(n.opacity=0),t.effects.saveStyle(s),s.css({backgroundImage:"none",backgroundColor:e.color||"#ffff99"}).animate(n,{queue:!1,duration:e.duration,easing:e.easing,complete:i})})}); -------------------------------------------------------------------------------- /pfweb/static/js/main.js: -------------------------------------------------------------------------------- 1 | function save_rules_order() { 2 | // Store new order in array with old location as value 3 | var order = []; 4 | $('#rulestable > tbody > tr').each(function(index, row) { 5 | order.push(parseInt(row.id.slice(9))); 6 | }); 7 | 8 | // Create a form with a JSON array and submit 9 | form = $(''); 10 | order_form = $(''); 11 | form.append(order_form).submit(); 12 | } 13 | 14 | function remove_rule(rule) { 15 | resp = confirm("Are you sure you wish to delete this rule?"); 16 | 17 | if(resp == false) { 18 | return 19 | } 20 | 21 | location.href = "/firewall/rules/remove/" + rule 22 | } 23 | 24 | function remove_table(table) { 25 | $("#table_" + table).prop("checked", true); 26 | 27 | $("#delete_tables_submit").trigger("click"); 28 | 29 | $("#table_" + table).prop("checked", false); 30 | } 31 | 32 | function add_address() { 33 | // Get next ID to use 34 | field_id = $('#address_fields').children().last().attr('id').split("-")[1]; 35 | new_id = (parseInt(field_id) + 1); 36 | // Create new dom for addr 37 | field = $('Hostname | 14 |{{ sys_info.hostname }} | 15 |
---|---|
Operating System | 18 |{{ sys_info.os }} | 19 |
Uptime | 22 |{{ sys_info.uptime }} | 23 |
Current Time | 26 |{{ sys_info.current_time }} | 27 |
40 | | Total | 41 |Rate | 42 |
---|---|---|
Status | 47 |{% if pf_info.enabled %}Enabled{% else %}Disabled{% endif %} for {{ pf_info.since }} | 48 |49 | |
State Table Entries | 52 |{{ pf_info.states }} | 53 |54 | |
State Table Searches | 57 |{{ pf_info.searches.total }} | 58 |{{ pf_info.searches.rate }}/s | 59 |
State Table Inserts | 62 |{{ pf_info.inserts.total }} | 63 |{{ pf_info.inserts.rate }}/s | 64 |
State Table Removals | 67 |{{ pf_info.removals.total }} | 68 |{{ pf_info.removals.rate }}/s | 69 |
Matches | 72 |{{ pf_info.match.total }} | 73 |{{ pf_info.match.rate }}/s | 74 |
{{ iface.name }} | 90 |{% if iface.status %}{% else %}{% endif %} | 91 |{{ iface.media }} | 92 |
93 | {% for ip in iface.ipv4 %}{{ ip }} {% endfor %} 94 | {% for ip in iface.ipv6|sort(attribute=1) %}{% if ip[1] %}{{ ip[0] }}{% else %}{{ ip[0] }}{% endif%} {% endfor %} 95 | |
96 |
---|
Your request could not be understood by the server.
7 | {% if msg %}Error Message: {{ msg }}
{% endif %} 8 |9 | {{ status.info }} 10 | 11 | LIMITS: 12 | {% for limit, val in status.limits|dictsort %}{{ "{:<15} hard limit {:>7}".format(limit, val) }}18 |
{% endfor %} 13 | TIMEOUTS: 14 | {% for timeout, val in status.timeouts|dictsort %}{{ "{:<16} {:>5}s".format(timeout, val) }}
{% endfor %} 15 | INTERFACES: 16 | {% for iface in status.ifaces %}{{ iface|string }}{% endfor %} 17 |
IF | 12 |Proto | 13 |Source (Original SRC) Destination (Original DST) | 14 |State | 15 |Packets | 16 |Bytes | 17 |Expires | 18 |
---|---|---|---|---|---|---|
{{ state.ifname }} | 24 |{{ state.proto }} | 25 |{{ state.src }} {{ state.dst }} | 26 |{{ state.state }} | 27 |{{ state.packets[1]|safe }} | 28 |{{ state.bytes[1]|safe }} | 29 |{{ state.expires[1] }} | 30 |