XF Surge Guard

XF Surge Guard 1.0.4

No permission to download

zeeb0t

Well-known member
Licensed customer
zeeb0t submitted a new resource:

XF Surge Guard - Protects XenForo from repeated guest traffic bursts before they waste server resources.

View attachment 338667

XF Surge Guard helps protect XenForo from repeated unauthenticated guest traffic bursts that waste PHP, MySQL, and XenForo resources.

It is designed for a very specific problem:

A guest or bot repeatedly requests dynamic XenForo URLs that produce errors, missing routes, or other wasteful responses. XenForo keeps booting, routing, querying, and rendering error pages for requests that have already proven to be...​

Read more about this resource...
 
zeeb0t updated XF Surge Guard with a new update entry:

Release: v1.0.3

XF Surge Guard 1.0.3​


This release rolls up the fixes and hardening work from the original 1.0.0 package into a single recommended update.

Fixed​


  • Fixed automatic src/config.php bootstrap insertion so the guard hook is inserted after the opening <?php line instead of being appended to the end of the file.
  • Improved bootstrap write/remove safety during install, upgrade, and uninstall.
  • Removed persistent src/config.php backup files so...

Read the rest of this update entry...
 
still trying to figure out what is actually happening but wanted to mention that in dark admin mode, table headers are in weird colors.

1779899108631.webp

thanks for all the hard work on this and the cloudflare rules post!
 
so yeah. one way to see what this addon is doing is opening the link recorded by it (or just make a fake url on your board) and open it in incognito (twice if it is not already registered).

it basically just loads a not found message instead of xenforo not found page.

1779899600383.webp
 
so yeah. one way to see what this addon is doing is opening the link recorded by it (or just make a fake url on your board) and open it in incognito (twice if it is not already registered).

it basically just loads a not found message instead of xenforo not found page.

View attachment 338704
Correct. The URL would typically load up all of XenForo to say the same thing, in an expensive way. The add-on identifies the errornous URLs being hit by guest traffic and bypasses all of XenForo to show a no frills 404, as you see. Net result is saved resource in doing so. Kinda boring, really, but it can make a huge difference if your forum gets scanned a lot by scrapers or security scanners and so on - my forum gets hit so far at times, it otherwise runs out of database connections and stops serving real users. Not anymore..
 
Hi @zeeb0t
Just wondering if it's possible to show the IP each bot visitor, and if it's possible to route them to an HTML page/file for the '404' error?

Thanks!
 
Hi @zeeb0t
Just wondering if it's possible to show the IP each bot visitor, and if it's possible to route them to an HTML page/file for the '404' error?

Thanks!

Good question.

Showing the IP against recent hits / guard events is possible, and I can see that being useful as an optional ACP detail. At the moment the add-on is intentionally not trying to be a full bot analytics tool, but IP visibility for troubleshooting makes sense.

For the response itself, yes, a future option for a small static custom 404 HTML body could work. The important part is that it stays lightweight and does not route back through XenForo, a template, a page node, or another dynamic 404 handler, otherwise it defeats the purpose of Surge Guard.

So I can see future options like:
  • minimal default 404 response
  • optional static custom 404 HTML body
  • optional IP display in recent hits / guard events
For proper themed 404 pages, redirects, missing URL tracking, etc, something like Siropu’s Custom 404 Page add-on is a good fit.

The two add-ons should still be able to work alongside each other: custom error pages for normal users/guests where XenForo is handling the request, and lightweight/static responses from Surge Guard where repeated unauthenticated junk traffic is being stopped early before it keeps wasting XenForo/PHP/MySQL resources.

So they solve different problems.

still trying to figure out what is actually happening but wanted to mention that in dark admin mode, table headers are in weird colors.

View attachment 338703

thanks for all the hard work on this and the cloudflare rules post!

Thanks for spotting the dark admin mode issue. I believe that is just an ACP styling bug and I’ll clean up the table header colours in the next point release.
 
@zeeb0t, нi, thanks for the addon, but please change the admin panel template to a normal one, eliminating inline styles and phrases. Example :
HTML:
<xf:title>XF Surge Guard</xf:title>

<xf:css src="zee_surge_guard_dashboard.less" />

<xf:pageaction>
    <div class="xf_surge_guard_pageaction">
        <xf:button href="{{ link('options/groups/zeeSurgeGuard') }}" icon="settings">{{ phrase('options') }}</xf:button>

        <xf:form action="{{ link('surge-guard/publish') }}" ajax="true" class="xf_surge_guard_inline_form">
            <xf:button type="submit" icon="refresh">{{ phrase('xf_surge_guard_re_evaluate_now') }}</xf:button>
        </xf:form>
    </div>
</xf:pageaction>

<div class="block">
    <div class="block-container">
        <h2 class="block-header">{{ phrase('status') }}</h2>

        <div class="block-body">
            <div class="block-row">
                <table class="xf_surge_guard_status_table">
                    <tbody>
                        <tr class="xf_surge_guard_cell_left">
                            <th>{{ phrase('xf_surge_guard_mode') }}</th>
                            <td><b>{$mode}</b></td>

                            <th>{{ phrase('xf_surge_guard_state_file') }}</th>
                            <td>
                                <xf:if is="$state.exists">
                                    <xf:if is="$state.valid">{{ phrase('valid') }}<xf:else />{{ phrase('xf_surge_guard_invalid') }}</xf:if>
                                <xf:else />
                                    {{ phrase('missing') }}
                                </xf:if>
                            </td>

                            <th>{{ phrase('xf_surge_guard_effective_guard') }}</th>
                            <td>
                                <xf:if is="$health.guard_effective">Yes<xf:else />{{ phrase('no') }}</xf:if>
                            </td>
                        </tr>

                        <tr class="xf_surge_guard_cell_left">
                            <th>{{ phrase('xf_surge_guard_exported_rules') }}</th>
                            <td>{$state.rule_count}</td>

                            <th>{{ phrase('xf_surge_guard_generated') }}</th>
                            <td>
                                <xf:if is="$state.generated_at">
                                    {{ date_time($state.generated_at) }}
                                <xf:else />
                                    -
                                </xf:if>
                            </td>

                            <th>{{ phrase('xf_surge_guard_state_expires') }}</th>
                            <td>
                                <xf:if is="$state.expires_at">
                                    {{ date_time($state.expires_at) }}
                                <xf:else />
                                    -
                                </xf:if>
                            </td>
                        </tr>

                        <tr class="xf_surge_guard_cell_left">
                            <th>{{ phrase('xf_surge_guard_bootstrap') }}</th>
                            <td><b>{$health.bootstrap.state}</b></td>

                            <th>{{ phrase('xf_surge_guard_config_writable') }}</th>
                            <td><xf:if is="$health.bootstrap.config_writable">{{ phrase('yes') }}<xf:else />{{ phrase('no') }}</xf:if></td>

                            <th>{{ phrase('xf_surge_guard_bootstrap_file') }}</th>
                            <td><xf:if is="$health.bootstrap.bootstrap_exists">{{ phrase('xf_surge_guard_exists') }}<xf:else />{{ phrase('missing') }}</xf:if></td>
                        </tr>

                        <tr class="xf_surge_guard_cell_left">
                            <th>{{ phrase('xf_surge_guard_bootstrap_note') }}</th>
                            <td colspan="5">{$health.bootstrap.message}</td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>

<div class="block">
    <div class="block-container">
        <h2 class="block-header">{{ phrase('xf_surge_guard_currently_managed_rules') }}</h2>

        <xf:if is="$managedRules">
            <div class="block-body">
                <div class="block-row">
                    {{ phrase('xf_surge_guard_managedRules') }}
                </div>
            </div>

            <table class="xf_surge_guard_data_table">
                <thead>
                    <tr class="xf_surge_guard_cell_left">
                        <th>{{ phrase('xf_surge_guard_path') }}</th>
                        <th>{{ phrase('type') }}</th>
                        <th>{{ phrase('xf_surge_guard_requests') }}</th>
                        <th>{{ phrase('xf_surge_guard_failures') }}</th>
                        <th>{{ phrase('xf_surge_guard_clients') }}</th>
                        <th>{{ phrase('xf_surge_guard_score') }}</th>
                        <th>{{ phrase('expires') }}</th>
                        <th>{{ phrase('action') }}</th>
                    </tr>
                </thead>
                <tbody>
                    <xf:foreach loop="$managedRules" value="$rule">
                        <tr class="xf_surge_guard_cell_left">
                            <td>
                                <code>{$rule.path_sample}</code>
                                <xf:if is="$rule.query_shape_sample">
                                    <div class="u-muted"><code>?{$rule.query_shape_sample}</code></div>
                                </xf:if>
                                <div class="u-muted">{$rule.reason}</div>
                            </td>
                            <td>{$rule.match_type}</td>
                            <td>{$rule.request_count}</td>
                            <td>{$rule.failure_count}</td>
                            <td>{$rule.unique_client_count}</td>
                            <td>{$rule.score}</td>
                            <td>{{ date_time($rule.guarded_until) }}</td>
                            <td>
                                <xf:form action="{{ link('surge-guard/release') }}" ajax="true" class="xf_surge_guard_inline_form">
                                    <input type="hidden" name="pattern_id" value="{$rule.pattern_id}" />
                                    <xf:button type="submit" class="button--link">{{ phrase('xf_surge_guard_release') }}</xf:button>
                                </xf:form>
                            </td>
                        </tr>
                    </xf:foreach>
                </tbody>
            </table>
        <xf:else />
            <div class="block-body">
                <div class="block-row">{{ phrase('xf_surge_guard_managedNoRules') }}</div>
            </div>
        </xf:if>
    </div>
</div>

<div class="block">
    <div class="block-container">
        <h2 class="block-header">{{ phrase('xf_surge_guard_last_hits') }}</h2>

        <xf:if is="$recentHits">
            <table class="xf_surge_guard_data_table">
                <thead>
                    <tr class="xf_surge_guard_cell_left">
                        <th>{{ phrase('date_time') }}</th>
                        <th>{{ phrase('status') }}</th>
                        <th>{{ phrase('xf_surge_guard_result') }}</th>
                        <th>{{ phrase('method') }}</th>
                        <th>{{ phrase('xf_surge_guard_path') }}</th>
                        <th>{{ phrase('xf_surge_guard_client_key') }}</th>
                    </tr>
                </thead>
                <tbody>
                    <xf:foreach loop="$recentHits" value="$hit">
                        <tr class="xf_surge_guard_cell_left">
                            <td>{{ date_time($hit.time) }}</td>
                            <td>{$hit.status_code}</td>
                            <td>{$hit.result}</td>
                            <td>{$hit.method}</td>
                            <td>
                                <code>{$hit.path}</code>
                                <xf:if is="$hit.query_shape">
                                    <div class="u-muted"><code>?{$hit.query_shape}</code></div>
                                </xf:if>
                            </td>
                            <td><code>{$hit.client_key}</code></td>
                        </tr>
                    </xf:foreach>
                </tbody>
            </table>
        <xf:else />
            <div class="block-body">
                <div class="block-row">{{ phrase('xf_surge_guard_no_hits') }}</div>
            </div>
        </xf:if>

        <div class="block-footer">
            {{ phrase('xf_surge_guard_description_hits_log') }}
        </div>
    </div>
</div>

<div class="block">
    <div class="block-container">
        <h2 class="block-header">{{ phrase('xf_surge_guard_recent_guard_events') }}</h2>

        <xf:if is="$recentEvents">
            <table class="xf_surge_guard_data_table">
                <thead>
                    <tr class="xf_surge_guard_cell_left">
                        <th>{{ phrase('date_time') }}</th>
                        <th>{{ phrase('type') }}</th>
                        <th>{{ phrase('xf_surge_guard_pattern') }}</th>
                        <th>{{ phrase('message') }}</th>
                    </tr>
                </thead>
                <tbody>
                    <xf:foreach loop="$recentEvents" value="$event">
                        <tr class="xf_surge_guard_cell_left">
                            <td>{{ date_time($event.event_date) }}</td>
                            <td>{$event.event_type}</td>
                            <td>{$event.pattern_id}</td>
                            <td>{$event.message}</td>
                        </tr>
                    </xf:foreach>
                </tbody>
            </table>
        <xf:else />
            <div class="block-body">
                <div class="block-row">{{ phrase('xf_surge_guard_empty_logs') }}</div>
            </div>
        </xf:if>
    </div>
</div>
and add a separate template for the styles. Example :
CSS:
.xf_surge_guard_pageaction {
    display: flex;
    gap: 6px;
    align-items: center;
}
.xf_surge_guard_inline_form {
    display: inline-block;
    margin: 0;
}
.xf_surge_guard_status_table {
    width: 100%;
    border-collapse: collapse;
}
.xf_surge_guard_status_table th,
.xf_surge_guard_status_table td {
    padding: 8px;
}
.xf_surge_guard_status_table th {
    text-align: left;
}
.xf_surge_guard_status_table th:nth-child(1),
.xf_surge_guard_status_table th:nth-child(3),
.xf_surge_guard_status_table th:nth-child(5) {
    width: 16%;
}
.xf_surge_guard_status_table td:nth-child(2),
.xf_surge_guard_status_table td:nth-child(4) {
    width: 17%;
}
.xf_surge_guard_status_table td:nth-child(6) {
    width: 18%;
}
.xf_surge_guard_data_table {
    width: 100%;
    border-collapse: collapse;
}
.xf_surge_guard_data_table th,
.xf_surge_guard_data_table td {
    padding: 8px;
    border-bottom: 1px solid #eee;
}
.xf_surge_guard_data_table thead tr {
    background: #f5f5f5;
}
.xf_surge_guard_data_table thead th {
    border-bottom: 1px solid #ddd;
}
.xf_surge_guard_cell_left {
    text-align: left;
}
And of course new phrases:
XML:
  <phrase title="xf_surge_guard_bootstrap" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Bootstrap]]></phrase>
  <phrase title="xf_surge_guard_bootstrap_file" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Bootstrap file]]></phrase>
  <phrase title="xf_surge_guard_bootstrap_note" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Bootstrap note]]></phrase>
  <phrase title="xf_surge_guard_client_key" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Client key]]></phrase>
  <phrase title="xf_surge_guard_clients" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Clients]]></phrase>
  <phrase title="xf_surge_guard_config_writable" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Config writable]]></phrase>
  <phrase title="xf_surge_guard_currently_managed_rules" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Currently managed rules]]></phrase>
  <phrase title="xf_surge_guard_description_hits_log" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[This is a bounded local diagnostic log, not a full traffic log. Early-blocked requests are intentionally not logged per-hit.]]></phrase>
  <phrase title="xf_surge_guard_effective_guard" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Effective guard]]></phrase>
  <phrase title="xf_surge_guard_empty_logs" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[No recent events.]]></phrase>
  <phrase title="xf_surge_guard_exists" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Exists]]></phrase>
  <phrase title="xf_surge_guard_exported_rules" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Exported rules]]></phrase>
  <phrase title="xf_surge_guard_failures" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Failures]]></phrase>
  <phrase title="xf_surge_guard_generated" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Generated]]></phrase>
  <phrase title="xf_surge_guard_invalid" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Invalid]]></phrase>
  <phrase title="xf_surge_guard_last_hits" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Last 5 observed hits]]></phrase>
  <phrase title="xf_surge_guard_managedNoRules" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[No rules are currently managed.]]></phrase>
  <phrase title="xf_surge_guard_managedRules" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[These are active patterns currently exported for early bootstrap protection.]]></phrase>
  <phrase title="xf_surge_guard_mode" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Mode]]></phrase>
  <phrase title="xf_surge_guard_no_hits" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[No recent observed hits yet.]]></phrase>
  <phrase title="xf_surge_guard_path" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Path]]></phrase>
  <phrase title="xf_surge_guard_pattern" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Pattern]]></phrase>
  <phrase title="xf_surge_guard_re_evaluate_now" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Re-evaluate now]]></phrase>
  <phrase title="xf_surge_guard_recent_guard_events" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Recent guard events]]></phrase>
  <phrase title="xf_surge_guard_release" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Release]]></phrase>
  <phrase title="xf_surge_guard_requests" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Requests]]></phrase>
  <phrase title="xf_surge_guard_result" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Result]]></phrase>
  <phrase title="xf_surge_guard_score" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[Score]]></phrase>
  <phrase title="xf_surge_guard_state_expires" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[State expires]]></phrase>
  <phrase title="xf_surge_guard_state_file" addon_id="Zee/SurgeGuard" version_id="1000470" version_string="1.0.4"><![CDATA[State file]]></phrase>
And class ConfigBootstrap replace the hard phrases with \XF::phrase('xf_surge_guard_something')
 
Last edited:
Back
Top Bottom