Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 10 additions & 14 deletions .github/workflows/update-cloudflare-proxies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ on:
schedule:
- cron: '0 0 1 * *' # runs at 00:00 UTC on the 1st day of every month
workflow_dispatch:

env:
IP_MERGED_FILE: Common/cloudflare-ips.txt
push:
paths:
- '.github/workflows/update-cloudflare-proxies.yml'
- 'Common/CloudflareIPs.targets'

jobs:
update-proxies:
Expand All @@ -17,23 +18,18 @@ jobs:
with:
ref: ${{ github.ref }}

- name: Fetch Cloudflare IPs and Update Files
env:
IPV4_URL: https://www.cloudflare.com/ips-v4
IPV6_URL: https://www.cloudflare.com/ips-v6
run: |
set -euo pipefail
- uses: actions/setup-dotnet@v5
with:
global-json-file: global.json

echo "Fetching Cloudflare IP lists and merging"
curl -s $IPV4_URL > $IP_MERGED_FILE
echo "" >> $IP_MERGED_FILE
curl -s $IPV6_URL >> $IP_MERGED_FILE
- name: Regenerate Cloudflare IPs source
run: dotnet build Common/Common.csproj -p:UpdateCloudflareIPs=true

- name: Commit and Push Changes
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add ${{ env.IP_MERGED_FILE }}
git add Common/Utils/CloudflareNetworks.g.cs

if git diff --cached --quiet; then
echo "No changes detected."
Expand Down
137 changes: 137 additions & 0 deletions Common/CloudflareIPs.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<Project>
<!--
Opt-in: fetch Cloudflare IPs and regenerate Utils/CloudflareNetworks.g.cs.
Enabled only by the monthly update-cloudflare-proxies workflow via -p:UpdateCloudflareIPs=true.
Normal builds (local, CI, Docker) just compile the committed .g.cs and never touch the network.
-->
<UsingTask Condition="'$(UpdateCloudflareIPs)' == 'true'"
TaskName="GenerateCloudflareIPsSource" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
<ParameterGroup>
<IPv4File ParameterType="System.String" Required="true" />
<IPv6File ParameterType="System.String" Required="true" />
<OutputFile ParameterType="System.String" Required="true" />
</ParameterGroup>
<Task>
<Code Type="Fragment" Language="cs"><![CDATA[
var v4Bytes = File.ReadAllBytes(IPv4File);
var v6Bytes = File.ReadAllBytes(IPv6File);
string v4Hash, v6Hash;
using (var sha256 = System.Security.Cryptography.SHA256.Create())
{
v4Hash = System.BitConverter.ToString(sha256.ComputeHash(v4Bytes)).Replace("-", "").ToLowerInvariant();
v6Hash = System.BitConverter.ToString(sha256.ComputeHash(v6Bytes)).Replace("-", "").ToLowerInvariant();
}

// Skip regen entirely if the existing file already reflects the same upstream content.
// Keeps timestamp/commit stable so the workflow only commits on real IP-list changes.
if (File.Exists(OutputFile))
{
var existing = File.ReadAllText(OutputFile);
if (existing.Contains($"// IPv4 SHA256: {v4Hash}") && existing.Contains($"// IPv6 SHA256: {v6Hash}"))
{
Log.LogMessage(Microsoft.Build.Framework.MessageImportance.High,
"Cloudflare IP lists unchanged (SHA256 match); preserving existing CloudflareNetworks.g.cs.");
}
else
{
WriteGeneratedFile();
}
}
else
{
WriteGeneratedFile();
}

void WriteGeneratedFile()
{
// Every non-empty line from Cloudflare is expected to be a valid CIDR.
// File.ReadAllLines handles the trailing newline; any blank/malformed line
// will fail the validation below with a FormatException.
var lines = File.ReadAllLines(IPv4File).Concat(File.ReadAllLines(IPv6File)).ToList();

var timestamp = System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);

var sb = new System.Text.StringBuilder();
sb.AppendLine("// <auto-generated />");
sb.AppendLine("// DO NOT EDIT - manual changes are overwritten on the next regeneration.");
sb.AppendLine("// ");
sb.AppendLine("// Cloudflare public proxy IP ranges (https://www.cloudflare.com/ips), baked in as a startup fallback for TrustedProxiesFetcher when the live fetch at application start fails or times out.");
sb.AppendLine("// Regenerated by the FetchCloudflareIPs target in Common.csproj, gated on -p:UpdateCloudflareIPs=true. The Update Cloudflare Proxies GitHub workflow is the only caller that passes that flag.");
sb.AppendLine("// ");
sb.AppendLine($"// Generated: {timestamp}");
sb.AppendLine($"// IPv4 SHA256: {v4Hash}");
sb.AppendLine($"// IPv6 SHA256: {v6Hash}");
sb.AppendLine("using System.Net;");
sb.AppendLine();
sb.AppendLine("namespace OpenShock.Common.Utils;");
sb.AppendLine();
sb.AppendLine("public static partial class TrustedProxiesFetcher");
sb.AppendLine("{");
sb.AppendLine(" private static readonly IPNetwork[] CloudflareNetworks =");
sb.AppendLine(" [");
foreach (var line in lines)
{
var slash = line.IndexOf('/');
if (slash < 0)
throw new System.FormatException($"Cloudflare IP entry '{line}' is missing the '/prefix' suffix.");

var addressPart = line.Substring(0, slash);
var prefixPart = line.Substring(slash + 1);

if (!int.TryParse(prefixPart, out var prefix))
throw new System.FormatException($"Cloudflare IP entry '{line}' has a non-integer prefix '{prefixPart}'.");

var bytes = System.Net.IPAddress.Parse(addressPart).GetAddressBytes();
var inv = System.Globalization.CultureInfo.InvariantCulture;

string address;
if (bytes.Length == 4)
{
// IPAddress(long) packs octet 0 in the low byte, octet 3 in the high byte.
long value = (long)bytes[0]
| ((long)bytes[1] << 8)
| ((long)bytes[2] << 16)
| ((long)bytes[3] << 24);
address = $"new IPAddress(0x{value.ToString("x8", inv)}L)";
}
else
{
// IPAddress(ReadOnlySpan<byte>) — 16 bytes as hex pairs for readability.
var byteList = string.Join(", ", bytes.Select(b => "0x" + b.ToString("x2", inv)));
address = $"new IPAddress([{byteList}])";
}

sb.AppendLine($" // {line}");
sb.AppendLine($" new IPNetwork({address}, prefixLength: {prefix}),");
sb.AppendLine();
}
sb.AppendLine(" ];");
sb.AppendLine("}");

File.WriteAllText(OutputFile, sb.ToString());
}
]]></Code>
</Task>
</UsingTask>

<Target Name="FetchCloudflareIPs"
Condition="'$(UpdateCloudflareIPs)' == 'true'"
BeforeTargets="PrepareForBuild">
<MakeDir Directories="$(IntermediateOutputPath)" />
<DownloadFile SourceUrl="https://www.cloudflare.com/ips-v4"
DestinationFolder="$(IntermediateOutputPath)"
DestinationFileName="cf-v4.txt"
SkipUnchangedFiles="true"
Retries="3"
RetryDelayMilliseconds="2000" />
<DownloadFile SourceUrl="https://www.cloudflare.com/ips-v6"
DestinationFolder="$(IntermediateOutputPath)"
DestinationFileName="cf-v6.txt"
SkipUnchangedFiles="true"
Retries="3"
RetryDelayMilliseconds="2000" />
<GenerateCloudflareIPsSource IPv4File="$(IntermediateOutputPath)cf-v4.txt"
IPv6File="$(IntermediateOutputPath)cf-v6.txt"
OutputFile="$(MSBuildProjectDirectory)/Utils/CloudflareNetworks.g.cs" />
</Target>
</Project>
7 changes: 1 addition & 6 deletions Common/Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,7 @@
<PackageReference Include="Z.EntityFramework.Plus.EFCore" />
</ItemGroup>

<!-- Files to copy -->
<ItemGroup>
<EmbeddedResource Include="cloudflare-ips.txt">
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
<Import Project="CloudflareIPs.targets" />

<!-- Capture git commit only if we're in a git repo; pipe output back to MSBuild -->
<Target Name="SetSourceRevisionId" BeforeTargets="InitializeSourceControlInformation">
Expand Down
85 changes: 85 additions & 0 deletions Common/Utils/CloudflareNetworks.g.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// <auto-generated />
// DO NOT EDIT - manual changes are overwritten on the next regeneration.
//
// Cloudflare public proxy IP ranges (https://www.cloudflare.com/ips), baked in as a startup fallback for TrustedProxiesFetcher when the live fetch at application start fails or times out.
// Regenerated by the FetchCloudflareIPs target in Common.csproj, gated on -p:UpdateCloudflareIPs=true. The Update Cloudflare Proxies GitHub workflow is the only caller that passes that flag.
//
// Generated: 2026-04-24T17:08:34Z
// IPv4 SHA256: f02c6d83bc01ab0ae8577160e036d700c7455359bce054df884e5d7d9e4e9e7b
// IPv6 SHA256: 9e9d39e3e83bad00c4decafd53c63fa62029f3d95db68de937d2be28234ca0a9
using System.Net;

namespace OpenShock.Common.Utils;

public static partial class TrustedProxiesFetcher
{
private static readonly IPNetwork[] CloudflareNetworks =
[
// 173.245.48.0/20
new IPNetwork(new IPAddress(0x0030f5adL), prefixLength: 20),

// 103.21.244.0/22
new IPNetwork(new IPAddress(0x00f41567L), prefixLength: 22),

// 103.22.200.0/22
new IPNetwork(new IPAddress(0x00c81667L), prefixLength: 22),

// 103.31.4.0/22
new IPNetwork(new IPAddress(0x00041f67L), prefixLength: 22),

// 141.101.64.0/18
new IPNetwork(new IPAddress(0x0040658dL), prefixLength: 18),

// 108.162.192.0/18
new IPNetwork(new IPAddress(0x00c0a26cL), prefixLength: 18),

// 190.93.240.0/20
new IPNetwork(new IPAddress(0x00f05dbeL), prefixLength: 20),

// 188.114.96.0/20
new IPNetwork(new IPAddress(0x006072bcL), prefixLength: 20),

// 197.234.240.0/22
new IPNetwork(new IPAddress(0x00f0eac5L), prefixLength: 22),

// 198.41.128.0/17
new IPNetwork(new IPAddress(0x008029c6L), prefixLength: 17),

// 162.158.0.0/15
new IPNetwork(new IPAddress(0x00009ea2L), prefixLength: 15),

// 104.16.0.0/13
new IPNetwork(new IPAddress(0x00001068L), prefixLength: 13),

// 104.24.0.0/14
new IPNetwork(new IPAddress(0x00001868L), prefixLength: 14),

// 172.64.0.0/13
new IPNetwork(new IPAddress(0x000040acL), prefixLength: 13),

// 131.0.72.0/22
new IPNetwork(new IPAddress(0x00480083L), prefixLength: 22),

// 2400:cb00::/32
new IPNetwork(new IPAddress([0x24, 0x00, 0xcb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),

// 2606:4700::/32
new IPNetwork(new IPAddress([0x26, 0x06, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),

// 2803:f800::/32
new IPNetwork(new IPAddress([0x28, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),

// 2405:b500::/32
new IPNetwork(new IPAddress([0x24, 0x05, 0xb5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),

// 2405:8100::/32
new IPNetwork(new IPAddress([0x24, 0x05, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),

// 2a06:98c0::/29
new IPNetwork(new IPAddress([0x2a, 0x06, 0x98, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 29),

// 2c0f:f248::/32
new IPNetwork(new IPAddress([0x2c, 0x0f, 0xf2, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),

];
}
11 changes: 2 additions & 9 deletions Common/Utils/TrustedProxiesFetcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace OpenShock.Common.Utils;

public static class TrustedProxiesFetcher
public static partial class TrustedProxiesFetcher
{
private static readonly HttpClient Client = new();

Expand Down Expand Up @@ -77,14 +77,7 @@ public static async Task<IPNetwork[]> GetTrustedNetworksAsync(bool fetch = true)
cfProxies = await FetchCloudflareIPs();
}

if (cfProxies is null)
{
var assembly = typeof(TrustedProxiesFetcher).Assembly;
var resourceName = assembly.GetName().Name + ".cloudflare-ips.txt";
await using var stream = assembly.GetManifestResourceStream(resourceName) ?? throw new NullReferenceException("Could not open embedded cloudflare-ips.txt file");
using var reader = new StreamReader(stream);
cfProxies = ParseNetworks(await reader.ReadToEndAsync());
}
cfProxies ??= CloudflareNetworks;

return [.. PrivateNetworksParsed, .. cfProxies];
}
Expand Down
22 changes: 0 additions & 22 deletions Common/cloudflare-ips.txt

This file was deleted.

2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.0",
"version": "10.0.100",
"rollForward": "latestMinor",
"allowPrerelease": false
},
Expand Down
Loading