feat: rename masq to snat throughout, update generator to Shorewall 5 snat format

This commit is contained in:
2026-03-01 01:30:19 +01:00
parent 1b543ed44a
commit 686ce911bb
11 changed files with 115 additions and 94 deletions

View File

@@ -92,7 +92,7 @@ def generate_config(
selectinload(models.Config.policies).selectinload(models.Policy.dst_zone),
selectinload(models.Config.rules).selectinload(models.Rule.src_zone),
selectinload(models.Config.rules).selectinload(models.Rule.dst_zone),
selectinload(models.Config.masq_entries),
selectinload(models.Config.snat_entries),
)
.filter(models.Config.id == config_id, models.Config.owner_id == current_user.id)
.first()

View File

@@ -1,64 +0,0 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app import models, schemas
from app.auth import get_current_user
from app.database import get_db
router = APIRouter()
def _owner_config(config_id: int, db: Session, user: models.User) -> models.Config:
config = db.query(models.Config).filter(
models.Config.id == config_id, models.Config.owner_id == user.id
).first()
if not config:
raise HTTPException(status_code=404, detail="Config not found")
return config
@router.get("/{config_id}/masq", response_model=list[schemas.MasqOut])
def list_masq(config_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
return db.query(models.Masq).filter(models.Masq.config_id == config_id).all()
@router.post("/{config_id}/masq", response_model=schemas.MasqOut, status_code=201)
def create_masq(config_id: int, body: schemas.MasqCreate, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
masq = models.Masq(**body.model_dump(), config_id=config_id)
db.add(masq)
db.commit()
db.refresh(masq)
return masq
@router.get("/{config_id}/masq/{masq_id}", response_model=schemas.MasqOut)
def get_masq(config_id: int, masq_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
masq = db.query(models.Masq).filter(models.Masq.id == masq_id, models.Masq.config_id == config_id).first()
if not masq:
raise HTTPException(status_code=404, detail="Masq entry not found")
return masq
@router.put("/{config_id}/masq/{masq_id}", response_model=schemas.MasqOut)
def update_masq(config_id: int, masq_id: int, body: schemas.MasqUpdate, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
masq = db.query(models.Masq).filter(models.Masq.id == masq_id, models.Masq.config_id == config_id).first()
if not masq:
raise HTTPException(status_code=404, detail="Masq entry not found")
for field, value in body.model_dump(exclude_none=True).items():
setattr(masq, field, value)
db.commit()
db.refresh(masq)
return masq
@router.delete("/{config_id}/masq/{masq_id}", status_code=204)
def delete_masq(config_id: int, masq_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
masq = db.query(models.Masq).filter(models.Masq.id == masq_id, models.Masq.config_id == config_id).first()
if not masq:
raise HTTPException(status_code=404, detail="Masq entry not found")
db.delete(masq)
db.commit()

64
backend/app/api/snat.py Normal file
View File

@@ -0,0 +1,64 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from app import models, schemas
from app.auth import get_current_user
from app.database import get_db
router = APIRouter()
def _owner_config(config_id: int, db: Session, user: models.User) -> models.Config:
config = db.query(models.Config).filter(
models.Config.id == config_id, models.Config.owner_id == user.id
).first()
if not config:
raise HTTPException(status_code=404, detail="Config not found")
return config
@router.get("/{config_id}/snat", response_model=list[schemas.SnatOut])
def list_snat(config_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
return db.query(models.Snat).filter(models.Snat.config_id == config_id).all()
@router.post("/{config_id}/snat", response_model=schemas.SnatOut, status_code=201)
def create_snat(config_id: int, body: schemas.SnatCreate, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
snat = models.Snat(**body.model_dump(), config_id=config_id)
db.add(snat)
db.commit()
db.refresh(snat)
return snat
@router.get("/{config_id}/snat/{snat_id}", response_model=schemas.SnatOut)
def get_snat(config_id: int, snat_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
snat = db.query(models.Snat).filter(models.Snat.id == snat_id, models.Snat.config_id == config_id).first()
if not snat:
raise HTTPException(status_code=404, detail="SNAT entry not found")
return snat
@router.put("/{config_id}/snat/{snat_id}", response_model=schemas.SnatOut)
def update_snat(config_id: int, snat_id: int, body: schemas.SnatUpdate, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
snat = db.query(models.Snat).filter(models.Snat.id == snat_id, models.Snat.config_id == config_id).first()
if not snat:
raise HTTPException(status_code=404, detail="SNAT entry not found")
for field, value in body.model_dump(exclude_none=True).items():
setattr(snat, field, value)
db.commit()
db.refresh(snat)
return snat
@router.delete("/{config_id}/snat/{snat_id}", status_code=204)
def delete_snat(config_id: int, snat_id: int, db: Session = Depends(get_db), user: models.User = Depends(get_current_user)):
_owner_config(config_id, db, user)
snat = db.query(models.Snat).filter(models.Snat.id == snat_id, models.Snat.config_id == config_id).first()
if not snat:
raise HTTPException(status_code=404, detail="SNAT entry not found")
db.delete(snat)
db.commit()

View File

@@ -1,7 +1,7 @@
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.sessions import SessionMiddleware
from app.api import auth, configs, zones, interfaces, policies, rules, masq
from app.api import auth, configs, zones, interfaces, policies, rules, snat
from app.database import settings
app = FastAPI(title="Shorefront", version="0.1.0")
@@ -21,7 +21,7 @@ app.include_router(zones.router, prefix="/configs", tags=["zones"])
app.include_router(interfaces.router, prefix="/configs", tags=["interfaces"])
app.include_router(policies.router, prefix="/configs", tags=["policies"])
app.include_router(rules.router, prefix="/configs", tags=["rules"])
app.include_router(masq.router, prefix="/configs", tags=["masq"])
app.include_router(snat.router, prefix="/configs", tags=["snat"])
@app.get("/health")

View File

@@ -34,7 +34,7 @@ class Config(Base):
interfaces: Mapped[list["Interface"]] = relationship("Interface", back_populates="config", cascade="all, delete-orphan")
policies: Mapped[list["Policy"]] = relationship("Policy", back_populates="config", cascade="all, delete-orphan", order_by="Policy.position")
rules: Mapped[list["Rule"]] = relationship("Rule", back_populates="config", cascade="all, delete-orphan", order_by="Rule.position")
masq_entries: Mapped[list["Masq"]] = relationship("Masq", back_populates="config", cascade="all, delete-orphan")
snat_entries: Mapped[list["Snat"]] = relationship("Snat", back_populates="config", cascade="all, delete-orphan")
class Zone(Base):
@@ -102,8 +102,8 @@ class Rule(Base):
dst_zone: Mapped["Zone | None"] = relationship("Zone", foreign_keys=[dst_zone_id])
class Masq(Base):
__tablename__ = "masq"
class Snat(Base):
__tablename__ = "snat"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
config_id: Mapped[int] = mapped_column(Integer, ForeignKey("configs.id"), nullable=False)
@@ -112,4 +112,4 @@ class Masq(Base):
to_address: Mapped[str] = mapped_column(String(64), default="")
comment: Mapped[str] = mapped_column(Text, default="")
config: Mapped["Config"] = relationship("Config", back_populates="masq_entries")
config: Mapped["Config"] = relationship("Config", back_populates="snat_entries")

View File

@@ -160,22 +160,22 @@ class RuleOut(BaseModel):
model_config = {"from_attributes": True}
# --- Masq ---
class MasqCreate(BaseModel):
# --- Snat ---
class SnatCreate(BaseModel):
source_network: str
out_interface: str
to_address: str = ""
comment: str = ""
class MasqUpdate(BaseModel):
class SnatUpdate(BaseModel):
source_network: Optional[str] = None
out_interface: Optional[str] = None
to_address: Optional[str] = None
comment: Optional[str] = None
class MasqOut(BaseModel):
class SnatOut(BaseModel):
id: int
config_id: int
source_network: str
@@ -192,4 +192,4 @@ class GenerateOut(BaseModel):
interfaces: str
policy: str
rules: str
masq: str
snat: str

View File

@@ -48,10 +48,11 @@ class ShorewallGenerator:
lines.append(self._col(r.action, src, dst, r.proto or "-", r.dport or "-", r.sport or "-", width=16))
return "".join(lines)
def masq(self) -> str:
lines = [self._header("masq"), "#INTERFACE".ljust(24) + "SOURCE".ljust(24) + "ADDRESS\n"]
for m in self._config.masq_entries:
lines.append(self._col(m.out_interface, m.source_network, m.to_address or "-", width=24))
def snat(self) -> str:
lines = [self._header("snat"), "#ACTION".ljust(24) + "SOURCE".ljust(24) + "DEST\n"]
for m in self._config.snat_entries:
action = f"SNAT:{m.to_address}" if m.to_address else "MASQUERADE"
lines.append(self._col(action, m.source_network, m.out_interface, width=24))
return "".join(lines)
def as_json(self) -> dict:
@@ -60,7 +61,7 @@ class ShorewallGenerator:
"interfaces": self.interfaces(),
"policy": self.policy(),
"rules": self.rules(),
"masq": self.masq(),
"snat": self.snat(),
}
def as_zip(self) -> bytes:
@@ -70,5 +71,5 @@ class ShorewallGenerator:
zf.writestr("interfaces", self.interfaces())
zf.writestr("policy", self.policy())
zf.writestr("rules", self.rules())
zf.writestr("masq", self.masq())
zf.writestr("snat", self.snat())
return buf.getvalue()