package db import ( "context" "fmt" "time" ) // InstanceForwardRow is one panel-owned port forward in opnfwd. Composite // key (instance_id, port_name) — the panel allocates one forward per // manifest port per instance. type InstanceForwardRow struct { InstanceID string PortName string NatUUID string FilterUUID string TargetIP string HostPort int32 Proto string // "tcp" | "udp" | "tcp/udp" Descr string // "panel--" — matches the OPNsense rule's CreatedAt time.Time } // UpsertInstanceForward persists a single forward, replacing any prior row // for the same (instance, port_name). The replace is intentional — if the // panel re-creates a forward for an existing port (e.g. operator clicked // Reconcile and we recreated), the new uuids supersede the old. func (db *DB) UpsertInstanceForward(ctx context.Context, r InstanceForwardRow) error { _, err := db.pool.Exec(ctx, ` INSERT INTO instance_forwards (instance_id, port_name, nat_uuid, filter_uuid, target_ip, host_port, proto, descr) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (instance_id, port_name) DO UPDATE SET nat_uuid = EXCLUDED.nat_uuid, filter_uuid = EXCLUDED.filter_uuid, target_ip = EXCLUDED.target_ip, host_port = EXCLUDED.host_port, proto = EXCLUDED.proto, descr = EXCLUDED.descr `, r.InstanceID, r.PortName, r.NatUUID, r.FilterUUID, r.TargetIP, r.HostPort, r.Proto, r.Descr) if err != nil { return fmt.Errorf("upsert instance_forward: %w", err) } return nil } // ListInstanceForwards returns every forward for the given instance, used // at delete time + by the instance-modal "Forwards" pill. func (db *DB) ListInstanceForwards(ctx context.Context, instanceID string) ([]InstanceForwardRow, error) { rows, err := db.pool.Query(ctx, ` SELECT instance_id, port_name, nat_uuid, filter_uuid, target_ip, host_port, proto, descr, created_at FROM instance_forwards WHERE instance_id = $1 ORDER BY port_name `, instanceID) if err != nil { return nil, fmt.Errorf("list instance_forwards: %w", err) } defer rows.Close() var out []InstanceForwardRow for rows.Next() { var r InstanceForwardRow if err := rows.Scan(&r.InstanceID, &r.PortName, &r.NatUUID, &r.FilterUUID, &r.TargetIP, &r.HostPort, &r.Proto, &r.Descr, &r.CreatedAt); err != nil { return nil, fmt.Errorf("scan instance_forward: %w", err) } out = append(out, r) } return out, rows.Err() } // ListAllInstanceForwards is for the reconcile path — compare every // panel-tracked forward against opnfwd's view of the world. func (db *DB) ListAllInstanceForwards(ctx context.Context) ([]InstanceForwardRow, error) { rows, err := db.pool.Query(ctx, ` SELECT instance_id, port_name, nat_uuid, filter_uuid, target_ip, host_port, proto, descr, created_at FROM instance_forwards ORDER BY instance_id, port_name `) if err != nil { return nil, fmt.Errorf("list all instance_forwards: %w", err) } defer rows.Close() var out []InstanceForwardRow for rows.Next() { var r InstanceForwardRow if err := rows.Scan(&r.InstanceID, &r.PortName, &r.NatUUID, &r.FilterUUID, &r.TargetIP, &r.HostPort, &r.Proto, &r.Descr, &r.CreatedAt); err != nil { return nil, fmt.Errorf("scan instance_forward: %w", err) } out = append(out, r) } return out, rows.Err() } // DeleteInstanceForwards removes every panel-tracked forward for an // instance. Called from the delete path AFTER opnfwd's bulk-delete // returned successfully — this just drops the local mirror. func (db *DB) DeleteInstanceForwards(ctx context.Context, instanceID string) error { _, err := db.pool.Exec(ctx, `DELETE FROM instance_forwards WHERE instance_id = $1`, instanceID) if err != nil { return fmt.Errorf("delete instance_forwards: %w", err) } return nil }