works enough for now
This commit is contained in:
commit
c04a9a752a
12 changed files with 178 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Configuration files
|
||||
*.yml
|
||||
5
compiler/__init__.py
Normal file
5
compiler/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from .parser import load_services, load_globals
|
||||
from .nat import build_nat_rules
|
||||
from .firewall import build_firewall_rules
|
||||
from .dns import build_dns
|
||||
from .render import render_nat
|
||||
6
compiler/dns.py
Normal file
6
compiler/dns.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
def build_dns(services, cfg):
|
||||
return([
|
||||
{"name": s.name, "ip": s.internal_ip}
|
||||
for s in services
|
||||
if "dns" in s.exposure
|
||||
])
|
||||
9
compiler/firewall.py
Normal file
9
compiler/firewall.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
def build_firewall_rules(services, cfg):
|
||||
rules = []
|
||||
for s in services:
|
||||
if "nat" in s.exposure:
|
||||
rules.append({
|
||||
"service": s.name,
|
||||
"allow_port": s.public_port,
|
||||
})
|
||||
return(rules)
|
||||
17
compiler/models.py
Normal file
17
compiler/models.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Service:
|
||||
name: str
|
||||
internal_ip: str
|
||||
internal_port: int
|
||||
public_port: int
|
||||
protocol: str
|
||||
exposure: set[str]
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GlobalConfig:
|
||||
wan_interface: str
|
||||
public_ip: str
|
||||
lan_interface: str
|
||||
internal_cidr: str
|
||||
29
compiler/nat.py
Normal file
29
compiler/nat.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import hashlib
|
||||
|
||||
def rule_id(service_name: str, kind: str) -> int:
|
||||
h = hashlib.sha1(f"{service_name}:{kind}".encode()).hexdigest()
|
||||
return(int(h[:4], 16)) # here's hoping
|
||||
|
||||
def build_nat_rules(services, cfg):
|
||||
rules = []
|
||||
|
||||
for s in services:
|
||||
rid_wan = rule_id(s.name, "dnat_wan")
|
||||
rid_hairpin = rule_id(s.name, "dnat_hairpin")
|
||||
|
||||
rules.append({
|
||||
"id": rid_wan,
|
||||
"service": s,
|
||||
"cfg": cfg,
|
||||
"kind": "dnat_wan",
|
||||
})
|
||||
|
||||
if "nat" in s.exposure:
|
||||
rules.append({
|
||||
"id": rid_hairpin,
|
||||
"service": s,
|
||||
"cfg": cfg,
|
||||
"kind": "dnat_hairpin",
|
||||
})
|
||||
|
||||
return(rules)
|
||||
27
compiler/parser.py
Normal file
27
compiler/parser.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import yaml
|
||||
from .models import GlobalConfig, Service
|
||||
|
||||
def load_services(path: str) -> list[Service]:
|
||||
raw = yaml.safe_load(open(path))["services"]
|
||||
|
||||
services = []
|
||||
for name, s in raw.items():
|
||||
ip, port = s["endpoint"].split(":")
|
||||
services.append(Service(
|
||||
name=name,
|
||||
internal_ip=ip,
|
||||
internal_port=int(port),
|
||||
public_port=int(s["public_port"]),
|
||||
protocol=s["protocol"],
|
||||
exposure=set(s.get("exposure", [])),
|
||||
))
|
||||
return(services)
|
||||
|
||||
def load_globals(path: str) -> GlobalConfig:
|
||||
g = yaml.safe_load(open(path))["wan"]
|
||||
return GlobalConfig(
|
||||
wan_interface=g["wan_interface"],
|
||||
public_ip=g["public_ip"],
|
||||
lan_interface=g["lan_interface"],
|
||||
internal_cidr=g["internal_cidr"],
|
||||
)
|
||||
7
compiler/render.py
Normal file
7
compiler/render.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
env = Environment(loader=FileSystemLoader("templates"))
|
||||
|
||||
def render_nat(rules):
|
||||
tmpl = env.get_template("vyos_nat.j2")
|
||||
return(tmpl.render(rules=rules))
|
||||
5
config/globals.yml.example
Normal file
5
config/globals.yml.example
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
wan:
|
||||
wan_interface: eth0
|
||||
public_ip: 192.0.2.1
|
||||
lan_interface: eth1
|
||||
internal_cidr: 192.168.10.0/24
|
||||
13
config/services.yml.example
Normal file
13
config/services.yml.example
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# config/services.yml
|
||||
services:
|
||||
immich:
|
||||
endpoint: 192.168.10.20:2283
|
||||
public_port: 443
|
||||
protocol: tcp
|
||||
exposure: [nat, dns]
|
||||
|
||||
nextcloud:
|
||||
endpoint: 192.168.10.30:443
|
||||
public_port: 443
|
||||
protocol: tcp
|
||||
exposure: [nat]
|
||||
33
main.py
Normal file
33
main.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import argparse
|
||||
from compiler import (
|
||||
load_services,
|
||||
load_globals,
|
||||
build_nat_rules,
|
||||
build_firewall_rules,
|
||||
build_dns,
|
||||
render_nat,
|
||||
)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("command", choices=["plan", "render"])
|
||||
args = parser.parse_args()
|
||||
|
||||
services = load_services("config/services.yml")
|
||||
globals_config = load_globals("config/globals.yml")
|
||||
|
||||
nat_rules = build_nat_rules(services, globals_config)
|
||||
fw_rules = build_firewall_rules(services, globals_config)
|
||||
dns_entries = build_dns(services, globals_config)
|
||||
|
||||
if args.command == "plan":
|
||||
print("Services:", len(services))
|
||||
print("NAT rules:", len(nat_rules))
|
||||
print("Firewall rules:", len(fw_rules))
|
||||
print("DNS entries:", len(dns_entries))
|
||||
|
||||
elif args.command == "render":
|
||||
print(render_nat(nat_rules))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
20
templates/vyos_nat.j2
Normal file
20
templates/vyos_nat.j2
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{% for r in rules %}
|
||||
{% if r.kind == "dnat_wan" %}
|
||||
set nat destination rule {{ r.id }} description "{{ r.service.name }}"
|
||||
set nat destination rule {{ r.id }} destination port {{ r.service.public_port }}
|
||||
set nat destination rule {{ r.id }} inbound-interface name {{ r.cfg.wan_interface }}
|
||||
set nat destination rule {{ r.id }} protocol {{ r.service.protocol }}
|
||||
set nat destination rule {{ r.id }} translation address {{ r.service.internal_ip }}
|
||||
set nat destination rule {{ r.id }} translation port {{ r.service.internal_port }}
|
||||
|
||||
{% elif r.kind == "dnat_hairpin" %}
|
||||
set nat destination rule {{ r.id }} description "{{ r.service.name }} hairpin"
|
||||
set nat destination rule {{ r.id }} inbound-interface name {{ r.cfg.lan_interface }}
|
||||
set nat destination rule {{ r.id }} destination address {{ r.cfg.public_ip }}
|
||||
set nat destination rule {{ r.id }} destination port {{ r.service.public_port }}
|
||||
set nat destination rule {{ r.id }} protocol {{ r.service.protocol }}
|
||||
set nat destination rule {{ r.id }} translation address {{ r.service.internal_ip }}
|
||||
set nat destination rule {{ r.id }} translation port {{ r.service.internal_port }}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
Loading…
Add table
Add a link
Reference in a new issue