From 02c8f71957d6e68f8052f17c1dd27cfe0745f563 Mon Sep 17 00:00:00 2001 From: "Adrian A. Baumann" Date: Sun, 1 Mar 2026 11:27:36 +0100 Subject: [PATCH] feat: complete snat with all shorewall columns (proto, port, ipsec, mark, user, switch, origdest, probability) --- .../versions/0010_snat_add_missing_columns.py | 34 +++++++++++++++++++ backend/app/models.py | 8 +++++ backend/app/schemas.py | 24 +++++++++++++ backend/app/shorewall_generator.py | 16 +++++++-- frontend/src/routes/ConfigDetail.tsx | 15 ++++++-- helm/shorefront/values.yaml | 2 +- 6 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 backend/alembic/versions/0010_snat_add_missing_columns.py diff --git a/backend/alembic/versions/0010_snat_add_missing_columns.py b/backend/alembic/versions/0010_snat_add_missing_columns.py new file mode 100644 index 0000000..db3a3f3 --- /dev/null +++ b/backend/alembic/versions/0010_snat_add_missing_columns.py @@ -0,0 +1,34 @@ +"""add missing shorewall snat columns + +Revision ID: 0010 +Revises: 0009 +Create Date: 2026-03-01 +""" +from alembic import op +import sqlalchemy as sa + +revision = "0010" +down_revision = "0009" +branch_labels = None +depends_on = None + +_NEW_COLS = [ + ("proto", sa.String(16)), + ("port", sa.String(64)), + ("ipsec", sa.String(128)), + ("mark", sa.String(32)), + ("user_group", sa.String(64)), + ("switch_name", sa.String(32)), + ("origdest", sa.String(128)), + ("probability", sa.String(16)), +] + + +def upgrade() -> None: + for col_name, col_type in _NEW_COLS: + op.add_column("snat", sa.Column(col_name, col_type, server_default="''", nullable=False)) + + +def downgrade() -> None: + for col_name, _ in reversed(_NEW_COLS): + op.drop_column("snat", col_name) diff --git a/backend/app/models.py b/backend/app/models.py index ed8fb0d..540546e 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -124,6 +124,14 @@ class Snat(Base): source_network: Mapped[str] = mapped_column(String(64), nullable=False) out_interface: Mapped[str] = mapped_column(String(32), nullable=False) to_address: Mapped[str] = mapped_column(String(64), default="") + proto: Mapped[str] = mapped_column(String(16), default="") + port: Mapped[str] = mapped_column(String(64), default="") + ipsec: Mapped[str] = mapped_column(String(128), default="") + mark: Mapped[str] = mapped_column(String(32), default="") + user_group: Mapped[str] = mapped_column(String(64), default="") + switch_name: Mapped[str] = mapped_column(String(32), default="") + origdest: Mapped[str] = mapped_column(String(128), default="") + probability: Mapped[str] = mapped_column(String(16), default="") comment: Mapped[str] = mapped_column(Text, default="") config: Mapped["Config"] = relationship("Config", back_populates="snat_entries") diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 1edea4f..b876b76 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -201,6 +201,14 @@ class SnatCreate(BaseModel): source_network: str out_interface: str to_address: str = "" + proto: str = "" + port: str = "" + ipsec: str = "" + mark: str = "" + user_group: str = "" + switch_name: str = "" + origdest: str = "" + probability: str = "" comment: str = "" @@ -208,6 +216,14 @@ class SnatUpdate(BaseModel): source_network: Optional[str] = None out_interface: Optional[str] = None to_address: Optional[str] = None + proto: Optional[str] = None + port: Optional[str] = None + ipsec: Optional[str] = None + mark: Optional[str] = None + user_group: Optional[str] = None + switch_name: Optional[str] = None + origdest: Optional[str] = None + probability: Optional[str] = None comment: Optional[str] = None @@ -217,6 +233,14 @@ class SnatOut(BaseModel): source_network: str out_interface: str to_address: str + proto: str + port: str + ipsec: str + mark: str + user_group: str + switch_name: str + origdest: str + probability: str comment: str model_config = {"from_attributes": True} diff --git a/backend/app/shorewall_generator.py b/backend/app/shorewall_generator.py index 204d8cd..01868c3 100644 --- a/backend/app/shorewall_generator.py +++ b/backend/app/shorewall_generator.py @@ -87,10 +87,22 @@ class ShorewallGenerator: return "".join(lines) def snat(self) -> str: - lines = [self._header("snat"), "#ACTION".ljust(24) + "SOURCE".ljust(24) + "DEST\n"] + lines = [ + self._header("snat"), + "#ACTION".ljust(24) + "SOURCE".ljust(24) + "DEST".ljust(20) + + "PROTO".ljust(10) + "PORT".ljust(16) + "IPSEC".ljust(16) + + "MARK".ljust(12) + "USER/GROUP".ljust(16) + "SWITCH".ljust(16) + + "ORIGDEST".ljust(20) + "PROBABILITY\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)) + lines.append(self._col( + action, m.source_network, m.out_interface, + m.proto or "-", m.port or "-", m.ipsec or "-", + m.mark or "-", m.user_group or "-", m.switch_name or "-", + m.origdest or "-", m.probability or "-", + width=16, + )) return "".join(lines) def as_json(self) -> dict: diff --git a/frontend/src/routes/ConfigDetail.tsx b/frontend/src/routes/ConfigDetail.tsx index 7cf7610..9dc3d8c 100644 --- a/frontend/src/routes/ConfigDetail.tsx +++ b/frontend/src/routes/ConfigDetail.tsx @@ -19,7 +19,7 @@ interface Zone { id: number; name: string; type: string; options: string } interface Iface { id: number; name: string; zone_id: number; options: string } interface Policy { id: number; src_zone_id: number; dst_zone_id: number; policy: string; log_level: string; comment: string; position: number } interface Rule { id: number; action: string; src_zone_id: number | null; dst_zone_id: number | null; src_ip: string; dst_ip: string; proto: string; dport: string; sport: string; origdest: string; rate_limit: string; user_group: string; mark: string; connlimit: string; time: string; headers: string; switch_name: string; helper: string; comment: string; position: number } -interface Snat { id: number; source_network: string; out_interface: string; to_address: string; comment: string } +interface Snat { id: number; source_network: string; out_interface: string; to_address: string; proto: string; port: string; ipsec: string; mark: string; user_group: string; switch_name: string; origdest: string; probability: string; comment: string } interface Host { id: number; zone_id: number; interface: string; subnet: string; options: string } interface Param { id: number; name: string; value: string } @@ -194,12 +194,21 @@ export default function ConfigDetail() { { key: 'out_interface' as const, label: 'Out Interface' }, { key: 'source_network' as const, label: 'Source Network' }, { key: 'to_address' as const, label: 'To Address' }, - { key: 'comment' as const, label: 'Comment' }, + { key: 'proto' as const, label: 'Proto' }, + { key: 'probability' as const, label: 'Probability' }, ] as Column[], fields: [ { name: 'out_interface', label: 'Out Interface', required: true }, { name: 'source_network', label: 'Source Network', required: true }, - { name: 'to_address', label: 'To Address' }, + { name: 'to_address', label: 'To Address (blank = MASQUERADE)', placeholder: 'e.g. 1.2.3.4' }, + { name: 'proto', label: 'Protocol', placeholder: 'e.g. tcp, udp' }, + { name: 'port', label: 'Port', placeholder: 'e.g. 80, 1024:65535' }, + { name: 'ipsec', label: 'IPsec', placeholder: 'e.g. mode=tunnel' }, + { name: 'mark', label: 'Mark', placeholder: 'e.g. 0x100/0xff0' }, + { name: 'user_group', label: 'User/Group', placeholder: 'e.g. joe:wheel' }, + { name: 'switch_name', label: 'Switch', placeholder: 'e.g. vpn_enabled' }, + { name: 'origdest', label: 'Orig Dest', placeholder: 'e.g. 1.2.3.4' }, + { name: 'probability', label: 'Probability', placeholder: 'e.g. 0.25' }, { name: 'comment', label: 'Comment' }, ] as FieldDef[], }, diff --git a/helm/shorefront/values.yaml b/helm/shorefront/values.yaml index f35e3ec..7cc8fac 100644 --- a/helm/shorefront/values.yaml +++ b/helm/shorefront/values.yaml @@ -42,4 +42,4 @@ keycloak: redirectUri: https://shorefront.baumann.gr/api/auth/oidc/callback containers: - version: "0.008" + version: "0.009"