Skip to content

Commit

Permalink
Merge pull request #21 from alegarsan11/develop
Browse files Browse the repository at this point in the history
Versión 2.0.0
  • Loading branch information
alegarsan11 authored May 15, 2024
2 parents 35221b3 + a05036e commit 9e5059b
Show file tree
Hide file tree
Showing 42 changed files with 1,502 additions and 499 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ __pycache__/
*.py[cod]
*$py.class
*nftables_info.png

report.html
report
assets
# C extensions
*.so
# Distribution / packaging
Expand Down
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,13 @@ To run the project:
- Grant permissions:
`sudo chmod +x run.sh`
- Execute the file:
`./run.sh`
`sudo ./run.sh`
- to kill all process
`sudo killall python`

- Execute the test files:
`python -m pytest` (On the nftables-frontend folder)
- Execute coverage:
`python -m pytest --cov`
- Generate report of coverage
`python -m pytest --cov --cov-report=html`
6 changes: 6 additions & 0 deletions nftables-frontend/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[report]
omit =
tests/*
*/migrations/*
api.py

46 changes: 24 additions & 22 deletions nftables-frontend/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ def flush_chain_request(name, family, table):

def create_rule_request(rule_id, chain_name, chain_table, family, statement, statement_term, statement_type):
expr = []
rule = service.get_rule(rule_id)
saddr = None
daddr = None
saddr_object = None
Expand Down Expand Up @@ -185,23 +184,23 @@ def create_rule_request(rule_id, chain_name, chain_table, family, statement, sta
if saddr:
if family == "inet":
if ":" in saddr:
expr.append({"match":{"op":"==","left":{"payload":{"field":"saddr", "protocol":"ip6"}}, "right": saddr}})
expr.append({"match":{"op":"==","left":{"payload":{"protocol":"ip6", "field":"saddr"}}, "right": saddr}})
elif "." in saddr:
expr.append({"match":{"op":"==","left":{"payload":{"field":"saddr", "protocol":"ip"}}, "right": saddr}})
expr.append({"match":{"op":"==","left":{"payload":{"protocol":"ip","field":"saddr" }}, "right": saddr}})
elif family == "bridge":
expr.append({"match":{"op":"==","left":{"payload":{"field":"saddr", "protocol":"ether"}}, "right": daddr}})
expr.append({"match":{"op":"==","left":{"payload":{"protocol":"ether", "field":"saddr"}}, "right": daddr}})
if daddr:
if family == "inet":
if ":" in daddr:
expr.append({"match":{"op":"==","left":{"payload":{"field":"daddr", "protocol":"ip6"}}, "right": daddr}})
expr.append({"match":{"op":"==","left":{"payload":{"protocol":"ip6" ,"field":"daddr"}}, "right": daddr}})
elif "." in daddr:
expr.append({"match":{"op":"==","left":{"payload":{"field":"daddr", "protocol":"ip"}}, "right": daddr}})
expr.append({"match":{"op":"==","left":{"payload":{ "protocol":"ip", "field":"daddr"}}, "right": daddr}})
elif family == "bridge":
expr.append({"match":{"op":"==","left":{"payload":{"field":"daddr", "protocol":"ether"}}, "right": daddr}})
expr.append({"match":{"op":"==","left":{"payload":{"protocol":"ether", "field":"daddr" }}, "right": daddr}})
if sport:
expr.append({"match":{"op":"==","left":{"payload":{"field":"sport", "protocol":"tcp"}}, "right": sport}})
expr.append({"match":{"op":"==","left":{"payload":{"protocol":"tcp", "field":"sport"}}, "right": sport}})
if dport:
expr.append({"match":{"op":"==","left":{"payload":{"field":"dport", "protocol":"tcp"}}, "right": dport}})
expr.append({"match":{"op":"==","left":{"payload":{"protocol":"tcp","field":"dport"}}, "right": dport}})
if input_interface:
expr.append({"input_interface": input_interface})
if output_interface:
Expand All @@ -210,28 +209,28 @@ def create_rule_request(rule_id, chain_name, chain_table, family, statement, sta
expr.append({"counter": None})
if saddr_object:
if service.check_set_or_map == "ipv4_addr":
expr.append({"match":{"op":"in","left":{"payload":{"field":"saddr", "protocol": "ip"}}, "right": "@"+saddr_object}})
expr.append({"match":{"op":"in","left":{"payload":{"protocol": "ip","field":"saddr" }}, "right": "@"+saddr_object}})
if service.check_set_or_map == "ipv6_addr":
expr.append({"match":{"op":"in","left":{"payload":{"field":"saddr", "protocol": "ip6"}}, "right": "@"+saddr_object}})
expr.append({"match":{"op":"in","left":{"payload":{ "protocol": "ip6", "field":"saddr"}}, "right": "@"+saddr_object}})
if service.check_set_or_map == "ether_addr":
expr.append({"match":{"op":"in","left":{"payload":{"field":"saddr", "protocol": "ether"}}, "right": "@"+saddr_object}})
expr.append({"match":{"op":"in","left":{"payload":{ "protocol": "ether", "field":"saddr"}}, "right": "@"+saddr_object}})
if daddr_object:
if service.check_set_or_map == "ipv4_addr":
expr.append({"match":{"op":"in","left":{"payload":{"field":"daddr", "protocol": "ip"}}, "right": "@"+daddr_object}})
expr.append({"match":{"op":"in","left":{"payload":{"protocol": "ip","field":"daddr"}}, "right": "@"+daddr_object}})
if service.check_set_or_map == "ipv6_addr":
expr.append({"match":{"op":"in","left":{"payload":{"field":"daddr", "protocol": "ip6"}}, "right": "@"+daddr_object}})
expr.append({"match":{"op":"in","left":{"payload":{"protocol": "ip6", "field":"daddr" }}, "right": "@"+daddr_object}})
if service.check_set_or_map == "ether_addr":
expr.append({"match":{"op":"in","left":{"payload":{"field":"daddr", "protocol": "ether"}}, "right": "@"+daddr_object}})
expr.append({"match":{"op":"in","left":{"payload":{"protocol": "ether", "field":"daddr" }}, "right": "@"+daddr_object}})
if sport_object:
expr.append({"match":{"op":"in","left":{"payload":{"field":"sport", "protocol":"tcp"}}, "right": "@"+sport_object}})
expr.append({"match":{"op":"in","left":{"payload":{ "protocol":"tcp", "field":"sport"}}, "right": "@"+sport_object}})
if dport_object:
expr.append({"match":{"op":"in","left":{"payload":{"field":"dport", "protocol":"tcp"}}, "right": "@"+sport_object}})
expr.append({"match":{"op":"in","left":{"payload":{ "protocol":"tcp", "field":"dport"}}, "right": "@"+sport_object}})
if accept:
expr.append({"accept": None})
if drop:
expr.append({"drop": None})
if reject:
expr.append({"reject": None})
expr.append({"reject": {'type': 'icmp', 'expr': 'port-unreachable'}})
if return_:
expr.append({"return": None})
if jump:
Expand All @@ -241,7 +240,7 @@ def create_rule_request(rule_id, chain_name, chain_table, family, statement, sta
if queue:
expr.append({"queue": {"num": queue}})
if log:
expr.append({"log": {"prefix": "Rule" + str(rule.id)+ " " + str(rule.table().name), "level": "info"}})
expr.append({"log": {"prefix": "Rule" + str(rule_id)+ " " , "level": "info"}})
if limit:
expr.append({"limit": {"rate": limit, "burst": 50, "per": limit_per}})
if masquerade:
Expand Down Expand Up @@ -282,11 +281,14 @@ def create_rule_request(rule_id, chain_name, chain_table, family, statement, sta
}]
}
}
response = requests.post('http://localhost:8000/rules/create_rule', json=json_data)
if expr != []:
response = requests.post('http://localhost:8000/rules/create_rule', json=json_data)
else:
return [], "Error creating rule."
if response.json()["status"] == "success":
return "Success"
return expr, "Success"
else:
return "Error creating rule."
return [], response.json()

def delete_rule_request(rule_id):
rule= service.get_rule(rule_id)
Expand Down
64 changes: 33 additions & 31 deletions nftables-frontend/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,39 @@
import os
from service import create_default_user, login_manager


app = Flask(__name__)
app.register_blueprint(visualization_bp)
app.register_blueprint(creation_bp)
dir_path = os.path.dirname(os.path.realpath(__file__))
app.config['SECRET_KEY'] = 'hfds732klejds90ahg'
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{dir_path}/instance/nftables.db'
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
app.config['SESSION_COOKIE_SECURE'] = True
login_manager.init_app(app)
db.init_app(app)

with app.app_context():
db.create_all()
create_default_user()

migrate = Migrate(app, db)
Bootstrap(app)

@app.errorhandler(404)
def page_not_found(e):
return render_template('error.html', message='Page not found'), 404


@app.errorhandler(500)
def internal_error(e):
return render_template('error.html', message="Internal server error"), 500

@app.route('/favicon.ico')
def favicon():
return app.send_static_file('favicon.ico')
def create_app():
app = Flask(__name__)
app.register_blueprint(visualization_bp)
app.register_blueprint(creation_bp)
dir_path = os.path.dirname(os.path.realpath(__file__))
app.config['SECRET_KEY'] = 'hfds732klejds90ahg'
app.config['SQLALCHEMY_DATABASE_URI'] = f'sqlite:///{dir_path}/instance/nftables.db'
app.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
app.config['SESSION_COOKIE_SECURE'] = True
login_manager.init_app(app)
db.init_app(app)

with app.app_context():
db.create_all()
create_default_user()

migrate = Migrate(app, db)
Bootstrap(app)

@app.errorhandler(404)
def page_not_found(e):
return render_template('error.html', message='Page not found'), 404

@app.errorhandler(500)
def internal_error(e):
return render_template('error.html', message="Internal server error"), 500

@app.route('/favicon.ico')
def favicon():
return app.send_static_file('favicon.ico')

return app

if __name__ == '__main__':
app = create_app()
app.run(debug=False)
41 changes: 28 additions & 13 deletions nftables-frontend/forms/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,12 @@ def validate_password(self, password):
class UpdateUserForm(FlaskForm):
username = StringField('Username', validators=[DataRequired()])
role = StringField('Role', validators=[DataRequired()])
is_active = SelectField('Active', choices=[('True', 'True'), ('False', 'False')], validators=[DataRequired()])
submit = SubmitField('Update User')

def validate_role(self, role):
if role.data not in ['administrator', 'user', 'guest']:
raise ValidationError('Role must be one of: administrator, user, guest.')

def validate_is_active(self, is_active):
if is_active.data not in ['True', 'False']:
raise ValidationError('Active must be one of: True, False.')

class TableForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
Expand All @@ -78,15 +74,14 @@ class ChainForm(FlaskForm):
('accept', 'accept'),
('drop', 'drop'),
('reject', 'reject') ], validators=[DataRequired()])
description = StringField('Description')
submit = SubmitField('Create Chain')

def validate_name(self, name):
if " " in name.data or "-" in name.data or "/" in name.data or "." in name.data or "," in name.data or ";" in name.data or ":" in name.data or "@" in name.data or "#" in name.data or "$" in name.data or "%" in name.data or "^" in name.data or "&" in name.data or "*" in name.data or "(" in name.data or ")" in name.data or "+" in name.data or "=" in name.data or "[" in name.data or "]" in name.data or "{" in name.data or "}" in name.data or "|" in name.data or "<" in name.data or ">" in name.data or "?" in name.data or "!" in name.data or "'" in name.data or '"' in name.data or "\\" in name.data or "`" in name.data or "~" in name.data:
raise ValidationError('Chain name invalid. (Must not contain special characters or spaces.)')

def validate_table(self, table):
table = Table.query.filter_by(name=table.data).first()
table = Table.query.filter_by(id=table.data).first()
if not table:
raise ValidationError('Table does not exist.')

Expand Down Expand Up @@ -141,19 +136,19 @@ def validate_dst_ip(self, dst_ip):
raise ValidationError('Destination IP must be a valid IP address with a network mask.')

def validate_src_ip_objects(self, src_ip_objects):
if src_ip_objects.data and self.src_ip.data:
if src_ip_objects.data != "--Selects--" and self.src_ip.data:
raise ValidationError('Source IP and Source IP Sets or Maps cannot be used together.')

def validate_dst_ip_objects(self, dst_ip_objects):
if dst_ip_objects.data and self.dst_ip.data:
if dst_ip_objects.data != "--Selects--" and self.dst_ip.data:
raise ValidationError('Destination IP and Destination IP Sets or Maps cannot be used together.')

def validate_src_port_objects(self, src_port_objects):
if src_port_objects.data and self.src_port.data:
if src_port_objects.data != "--Selects--" and self.src_port.data:
raise ValidationError('Source Port and Source Port Sets or Maps cannot be used together.')

def validate_dst_port_objects(self, dst_port_objects):
if dst_port_objects.data and self.dst_port.data:
if dst_port_objects.data != "--Selects--" and self.dst_port.data:
raise ValidationError('Destination Port and Destination Port Sets or Maps cannot be used together.')

def validate_src_port(self, src_port):
Expand Down Expand Up @@ -241,11 +236,10 @@ def validate_redirect(self, redirect):
elif not isinstance(int(redirect.data), int) or not 0 <= int(redirect.data) <= 65535:
raise ValidationError('Redirect must be a port number between 0 and 65535.')
except ValueError:
raise ValidationError('Redirect must be a valid IP address')
raise ValidationError('Condition on dst or src port must be especified to create redirect and must be a valid port number between 0 and 65535.')

class RuleForm(FlaskForm):
chain = StringField('Chain', validators=[DataRequired()])
family = StringField('Family', validators=[DataRequired()])
handle = StringField('Handle', validators=[Optional()])
statements = FormField(NotTerminalStatementForm)
statements_term = FormField(TerminalStatementForm)
Expand Down Expand Up @@ -323,4 +317,25 @@ class AddElementMap(FlaskForm):

class DeleteElementMap(FlaskForm):
key = StringField('Key', validators=[DataRequired()])


class AddListForm(FlaskForm):
name = StringField('Name', validators=[DataRequired()])
family = StringField('Family', validators=[DataRequired()])
element = StringField('Element', validators=[DataRequired()])
table = StringField('Table Name', validators=[DataRequired()])
type = SelectField('Type', choices=[('ipv4_addr', 'ipv4_addr')], validators=[DataRequired()])
description = StringField('Description', validators=[Optional()])

def validate_family(self, family):
if family.data not in ['ip', 'inet', 'arp', 'bridge', 'netdev']:
raise ValidationError('Family must be one of: ip, inet, arp, bridge, netdev.')
def validate_name(self, name):
if " " in name.data or "-" in name.data or "/" in name.data or "." in name.data or "," in name.data or ";" in name.data or ":" in name.data or "@" in name.data or "#" in name.data or "$" in name.data or "%" in name.data or "^" in name.data or "&" in name.data or "*" in name.data or "(" in name.data or ")" in name.data or "+" in name.data or "=" in name.data or "[" in name.data or "]" in name.data or "{" in name.data or "}" in name.data or "|" in name.data or "<" in name.data or ">" in name.data or "?" in name.data or "!" in name.data or "'" in name.data or '"' in name.data or "\\" in name.data or "`" in name.data or "~" in name.data:
raise ValidationError('Set name invalid. (Must not contain special characters or spaces.)')
def validate_type(self, type):
if type.data not in ['ipv4_addr']:
raise ValidationError('Type must be one of: ipv4_addr.')
def validate_table(self, table):
table = Table.query.filter_by(id=table.data).first()
if not table:
raise ValidationError('Table does not exist.')
23 changes: 13 additions & 10 deletions nftables-frontend/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class Table(db.Model):
family = db.Column(db.String(80), nullable=False)
description = db.Column(db.String(120), nullable=True)
chains = db.relationship('Chain', backref='table', lazy=True, cascade="all, delete-orphan")
username = db.Column(db.String(80), db.ForeignKey('user.username'), nullable=True)

def save(self):
db.session.add(self)
Expand All @@ -37,11 +38,9 @@ class Chain(db.Model):
__tablename__ = 'chain'
id= db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), nullable=False)
table_id = db.Column(db.Integer, db.ForeignKey('table.name'), nullable=False)
family = db.Column(db.String(120), nullable=True)
table_id = db.Column(db.Integer, db.ForeignKey('table.id'), nullable=False)
policy = db.Column(db.String(120), nullable=True)
rules = db.relationship('Rule', backref='chain', lazy=True, cascade="all, delete-orphan")
description = db.Column(db.String(120), nullable=True)

def get_table(self):
return Table.query.filter_by(name=self.table_id, family=self.family).first()
Expand All @@ -67,7 +66,6 @@ def __repr__(self):
class Rule(db.Model):
id = db.Column(db.Integer, primary_key=True)
chain_id = db.Column(db.Integer, db.ForeignKey('chain.id'), nullable=False)
family = db.Column(db.String(120), nullable=False)
expr = db.Column(db.String(120), nullable=False)
handle = db.Column(db.String(120), nullable=True)
description = db.Column(db.String(120), nullable=True)
Expand All @@ -80,12 +78,17 @@ def statements(self):
return Statement.query.filter_by(rule_id=self.id).all()

def table(self):
chain = Chain.query.filter_by(id=self.chain_id, family=self.family).first()
base_chain = BaseChain.query.filter_by(id=self.chain_id, family=self.family).first()
chain = Chain.query.filter_by(id=self.chain_id).first()
base_chain = BaseChain.query.filter_by(id=self.chain_id).first()
if base_chain:
return base_chain.table
else:
return chain.table

def to_string(self):
statements = ', '.join([str(statement) for statement in self.statements()])
table = self.table()
return f"Rule ID: {self.id}, Chain ID: {self.chain_id}, Expression: {self.expr}, Handle: {self.handle}, Description: {self.description}, Statements: {statements}, Table: {table}"

class Statement(db.Model):
id = db.Column(db.Integer, primary_key=True)
Expand Down Expand Up @@ -134,24 +137,24 @@ def __repr__(self):

def is_empty(self):
return not any([self.limit, self.log, self.counter, self.masquerade, self.snat, self.dnat, self.redirect])



class Set(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), nullable=False)
family = db.Column(db.String(120), nullable=False)
type = db.Column(db.String(120), nullable=False)
elements = db.Column(db.String(120), nullable=True)
description = db.Column(db.String(120), nullable=True)
table_id = db.Column(db.Integer, db.ForeignKey('table.name'), nullable=False)
table_id = db.Column(db.Integer, db.ForeignKey('table.id'), nullable=False)

def __repr__(self):
return '<Set %r>' % self.name

class Map(db.Model):
id = db.Column(db.Integer, primary_key=True)
table_id = db.Column(db.Integer, db.ForeignKey('table.name'), nullable=False)
table_id = db.Column(db.Integer, db.ForeignKey('table.id'), nullable=False)
name = db.Column(db.String(120), nullable=False)
family = db.Column(db.String(120), nullable=False)
description = db.Column(db.String(120), nullable=True)
type = db.Column(db.String(120), nullable=True)
map = db.Column(db.String(120), nullable=True)
Expand Down
Loading

0 comments on commit 9e5059b

Please sign in to comment.