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