diff --git a/backend/alembic/versions/0009_rules_add_missing_columns.py b/backend/alembic/versions/0009_rules_add_missing_columns.py new file mode 100644 index 0000000..035968c --- /dev/null +++ b/backend/alembic/versions/0009_rules_add_missing_columns.py @@ -0,0 +1,35 @@ +"""add missing shorewall rule columns + +Revision ID: 0009 +Revises: 0008 +Create Date: 2026-03-01 +""" +from alembic import op +import sqlalchemy as sa + +revision = "0009" +down_revision = "0008" +branch_labels = None +depends_on = None + +_NEW_COLS = [ + ("origdest", sa.String(128)), + ("rate_limit", sa.String(64)), + ("user_group", sa.String(64)), + ("mark", sa.String(32)), + ("connlimit", sa.String(32)), + ("time", sa.String(128)), + ("headers", sa.String(128)), + ("switch_name", sa.String(32)), + ("helper", sa.String(32)), +] + + +def upgrade() -> None: + for col_name, col_type in _NEW_COLS: + op.add_column("rules", sa.Column(col_name, col_type, server_default="''", nullable=False)) + + +def downgrade() -> None: + for col_name, _ in reversed(_NEW_COLS): + op.drop_column("rules", col_name) diff --git a/backend/app/models.py b/backend/app/models.py index f921eb2..ed8fb0d 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -99,6 +99,15 @@ class Rule(Base): proto: Mapped[str] = mapped_column(String(16), default="") dport: Mapped[str] = mapped_column(String(64), default="") sport: Mapped[str] = mapped_column(String(64), default="") + origdest: Mapped[str] = mapped_column(String(128), default="") + rate_limit: Mapped[str] = mapped_column(String(64), default="") + user_group: Mapped[str] = mapped_column(String(64), default="") + mark: Mapped[str] = mapped_column(String(32), default="") + connlimit: Mapped[str] = mapped_column(String(32), default="") + time: Mapped[str] = mapped_column(String(128), default="") + headers: Mapped[str] = mapped_column(String(128), default="") + switch_name: Mapped[str] = mapped_column(String(32), default="") + helper: Mapped[str] = mapped_column(String(32), default="") comment: Mapped[str] = mapped_column(Text, default="") position: Mapped[int] = mapped_column(Integer, default=0) diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 2c3f9b8..1edea4f 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -135,6 +135,15 @@ class RuleCreate(BaseModel): proto: str = "" dport: str = "" sport: str = "" + origdest: str = "" + rate_limit: str = "" + user_group: str = "" + mark: str = "" + connlimit: str = "" + time: str = "" + headers: str = "" + switch_name: str = "" + helper: str = "" comment: str = "" position: int = 0 @@ -148,6 +157,15 @@ class RuleUpdate(BaseModel): proto: Optional[str] = None dport: Optional[str] = None sport: Optional[str] = None + origdest: Optional[str] = None + rate_limit: Optional[str] = None + user_group: Optional[str] = None + mark: Optional[str] = None + connlimit: Optional[str] = None + time: Optional[str] = None + headers: Optional[str] = None + switch_name: Optional[str] = None + helper: Optional[str] = None comment: Optional[str] = None position: Optional[int] = None @@ -163,6 +181,15 @@ class RuleOut(BaseModel): proto: str dport: str sport: str + origdest: str + rate_limit: str + user_group: str + mark: str + connlimit: str + time: str + headers: str + switch_name: str + helper: str comment: str position: int diff --git a/backend/app/shorewall_generator.py b/backend/app/shorewall_generator.py index 2fe3991..204d8cd 100644 --- a/backend/app/shorewall_generator.py +++ b/backend/app/shorewall_generator.py @@ -53,13 +53,24 @@ class ShorewallGenerator: def rules(self) -> str: lines = [ self._header("rules"), - "#ACTION".ljust(16) + "SOURCE".ljust(24) + "DEST".ljust(24) + "PROTO".ljust(10) + "DPORT".ljust(10) + "SPORT\n", + "#ACTION".ljust(16) + "SOURCE".ljust(24) + "DEST".ljust(24) + + "PROTO".ljust(10) + "DPORT".ljust(16) + "SPORT".ljust(16) + + "ORIGDEST".ljust(20) + "RATE".ljust(16) + "USER".ljust(16) + + "MARK".ljust(12) + "CONNLIMIT".ljust(14) + "TIME".ljust(20) + + "HEADERS".ljust(16) + "SWITCH".ljust(16) + "HELPER\n", "SECTION NEW\n", ] for r in sorted(self._config.rules, key=lambda x: x.position): src = (r.src_zone.name if r.src_zone else "all") + (f":{r.src_ip}" if r.src_ip else "") dst = (r.dst_zone.name if r.dst_zone else "all") + (f":{r.dst_ip}" if r.dst_ip else "") - lines.append(self._col(r.action, src, dst, r.proto or "-", r.dport or "-", r.sport or "-", width=16)) + lines.append(self._col( + r.action, src, dst, + r.proto or "-", r.dport or "-", r.sport or "-", + r.origdest or "-", r.rate_limit or "-", r.user_group or "-", + r.mark or "-", r.connlimit or "-", r.time or "-", + r.headers or "-", r.switch_name or "-", r.helper or "-", + width=16, + )) return "".join(lines) def hosts(self) -> str: diff --git a/frontend/src/routes/ConfigDetail.tsx b/frontend/src/routes/ConfigDetail.tsx index f3eca2f..7cf7610 100644 --- a/frontend/src/routes/ConfigDetail.tsx +++ b/frontend/src/routes/ConfigDetail.tsx @@ -18,7 +18,7 @@ import { zonesApi, interfacesApi, policiesApi, rulesApi, snatApi, hostsApi, para 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; 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 Host { id: number; zone_id: number; interface: string; subnet: string; options: string } interface Param { id: number; name: string; value: string } @@ -147,17 +147,40 @@ export default function ConfigDetail() { }, { key: 'proto' as const, label: 'Proto' }, { key: 'dport' as const, label: 'DPort' }, + { key: 'origdest' as const, label: 'OrigDest' }, { key: 'position' as const, label: 'Position' }, ] as Column[], fields: [ { name: 'action', label: 'Action', required: true }, - { name: 'src_zone_id', label: 'Source Zone', type: 'select' as const, options: zoneOptions }, - { name: 'dst_zone_id', label: 'Dest Zone', type: 'select' as const, options: zoneOptions }, + { name: 'src_zone_id', label: 'Source Zone', type: 'select' as const, options: [{ value: '', label: 'all' }, ...zoneOptions] }, + { name: 'dst_zone_id', label: 'Dest Zone', type: 'select' as const, options: [{ value: '', label: 'all' }, ...zoneOptions] }, { name: 'src_ip', label: 'Source IP/CIDR' }, { name: 'dst_ip', label: 'Dest IP/CIDR' }, - { name: 'proto', label: 'Protocol' }, - { name: 'dport', label: 'Dest Port' }, - { name: 'sport', label: 'Source Port' }, + { name: 'proto', label: 'Protocol', placeholder: 'e.g. tcp, udp, icmp' }, + { name: 'dport', label: 'Dest Port(s)' }, + { name: 'sport', label: 'Source Port(s)' }, + { name: 'origdest', label: 'Original Dest', placeholder: 'e.g. 192.168.1.1' }, + { name: 'rate_limit', label: 'Rate Limit', placeholder: 'e.g. 10/sec:20' }, + { name: 'user_group', label: 'User/Group', placeholder: 'e.g. joe:wheel' }, + { name: 'mark', label: 'Mark', placeholder: 'e.g. 0x100/0xff0' }, + { name: 'connlimit', label: 'ConnLimit', placeholder: 'e.g. 10:24' }, + { name: 'time', label: 'Time', placeholder: 'e.g. timestart=09:00×top=17:00' }, + { name: 'headers', label: 'Headers (IPv6)', placeholder: 'e.g. auth,esp' }, + { name: 'switch_name', label: 'Switch', placeholder: 'e.g. vpn_enabled' }, + { name: 'helper', label: 'Helper', type: 'select' as const, options: [ + { value: '', label: '(none)' }, + { value: 'amanda', label: 'amanda' }, + { value: 'ftp', label: 'ftp' }, + { value: 'irc', label: 'irc' }, + { value: 'netbios-ns', label: 'netbios-ns' }, + { value: 'pptp', label: 'pptp' }, + { value: 'Q.931', label: 'Q.931' }, + { value: 'RAS', label: 'RAS' }, + { value: 'sane', label: 'sane' }, + { value: 'sip', label: 'sip' }, + { value: 'snmp', label: 'snmp' }, + { value: 'tftp', label: 'tftp' }, + ]}, { name: 'comment', label: 'Comment' }, { name: 'position', label: 'Position', type: 'number' as const }, ] as FieldDef[],