Skip to content

Commit d6ce804

Browse files
committed
Fix Cloudflare IPs build: commit generated fallback, gate fetch behind UpdateCloudflareIPs
Port the API repo's design. The generated CloudflareNetworks.g.cs was never committed and was written after the compile glob was evaluated, so clean builds failed with CS0103. Now the fallback list is committed and the network fetch is opt-in via -p:UpdateCloudflareIPs=true, run only by the monthly workflow.
1 parent 7ae1924 commit d6ce804

4 files changed

Lines changed: 231 additions & 47 deletions

File tree

.github/workflows/update-cloudflare-proxies.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ on:
44
schedule:
55
- cron: '0 0 1 * *' # runs at 00:00 UTC on the 1st day of every month
66
workflow_dispatch:
7+
push:
8+
paths:
9+
- '.github/workflows/update-cloudflare-proxies.yml'
10+
- 'Common/CloudflareIPs.targets'
711

812
jobs:
913
update-proxies:
@@ -15,9 +19,11 @@ jobs:
1519
ref: ${{ github.ref }}
1620

1721
- uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0
22+
with:
23+
global-json-file: global.json
1824

19-
- name: Build to regenerate Cloudflare IPs
20-
run: dotnet build Common/Common.csproj
25+
- name: Regenerate Cloudflare IPs source
26+
run: dotnet build Common/Common.csproj -p:UpdateCloudflareIPs=true
2127

2228
- name: Commit and Push Changes
2329
run: |

Common/CloudflareIPs.targets

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<Project>
2+
<!--
3+
Opt-in: fetch Cloudflare IPs and regenerate Utils/CloudflareNetworks.g.cs.
4+
Enabled only by the monthly update-cloudflare-proxies workflow via -p:UpdateCloudflareIPs=true.
5+
Normal builds (local, CI, Docker) just compile the committed .g.cs and never touch the network.
6+
-->
7+
<UsingTask Condition="'$(UpdateCloudflareIPs)' == 'true'"
8+
TaskName="GenerateCloudflareIPsSource" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
9+
<ParameterGroup>
10+
<IPv4File ParameterType="System.String" Required="true" />
11+
<IPv6File ParameterType="System.String" Required="true" />
12+
<OutputFile ParameterType="System.String" Required="true" />
13+
</ParameterGroup>
14+
<Task>
15+
<Code Type="Fragment" Language="cs"><![CDATA[
16+
var v4Bytes = File.ReadAllBytes(IPv4File);
17+
var v6Bytes = File.ReadAllBytes(IPv6File);
18+
string v4Hash, v6Hash;
19+
using (var sha256 = System.Security.Cryptography.SHA256.Create())
20+
{
21+
v4Hash = System.BitConverter.ToString(sha256.ComputeHash(v4Bytes)).Replace("-", "").ToLowerInvariant();
22+
v6Hash = System.BitConverter.ToString(sha256.ComputeHash(v6Bytes)).Replace("-", "").ToLowerInvariant();
23+
}
24+
25+
// Skip regen entirely if the existing file already reflects the same upstream content.
26+
// Keeps timestamp/commit stable so the workflow only commits on real IP-list changes.
27+
if (File.Exists(OutputFile))
28+
{
29+
var existing = File.ReadAllText(OutputFile);
30+
if (existing.Contains($"// IPv4 SHA256: {v4Hash}") && existing.Contains($"// IPv6 SHA256: {v6Hash}"))
31+
{
32+
Log.LogMessage(Microsoft.Build.Framework.MessageImportance.High,
33+
"Cloudflare IP lists unchanged (SHA256 match); preserving existing CloudflareNetworks.g.cs.");
34+
}
35+
else
36+
{
37+
WriteGeneratedFile();
38+
}
39+
}
40+
else
41+
{
42+
WriteGeneratedFile();
43+
}
44+
45+
void WriteGeneratedFile()
46+
{
47+
// Every non-empty line from Cloudflare is expected to be a valid CIDR.
48+
// File.ReadAllLines handles the trailing newline; any blank/malformed line
49+
// will fail the validation below with a FormatException.
50+
var lines = File.ReadAllLines(IPv4File).Concat(File.ReadAllLines(IPv6File)).ToList();
51+
52+
var timestamp = System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ", System.Globalization.CultureInfo.InvariantCulture);
53+
54+
var sb = new System.Text.StringBuilder();
55+
sb.AppendLine("// <auto-generated />");
56+
sb.AppendLine("// DO NOT EDIT - manual changes are overwritten on the next regeneration.");
57+
sb.AppendLine("// ");
58+
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.");
59+
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.");
60+
sb.AppendLine("// ");
61+
sb.AppendLine($"// Generated: {timestamp}");
62+
sb.AppendLine($"// IPv4 SHA256: {v4Hash}");
63+
sb.AppendLine($"// IPv6 SHA256: {v6Hash}");
64+
sb.AppendLine("using System.Net;");
65+
sb.AppendLine();
66+
sb.AppendLine("namespace OpenShock.Common.Utils;");
67+
sb.AppendLine();
68+
sb.AppendLine("public static partial class TrustedProxiesFetcher");
69+
sb.AppendLine("{");
70+
sb.AppendLine(" private static readonly IPNetwork[] CloudflareNetworks =");
71+
sb.AppendLine(" [");
72+
foreach (var line in lines)
73+
{
74+
var slash = line.IndexOf('/');
75+
if (slash < 0)
76+
throw new System.FormatException($"Cloudflare IP entry '{line}' is missing the '/prefix' suffix.");
77+
78+
var addressPart = line.Substring(0, slash);
79+
var prefixPart = line.Substring(slash + 1);
80+
81+
if (!int.TryParse(prefixPart, out var prefix))
82+
throw new System.FormatException($"Cloudflare IP entry '{line}' has a non-integer prefix '{prefixPart}'.");
83+
84+
var bytes = System.Net.IPAddress.Parse(addressPart).GetAddressBytes();
85+
var inv = System.Globalization.CultureInfo.InvariantCulture;
86+
87+
string address;
88+
if (bytes.Length == 4)
89+
{
90+
// IPAddress(long) packs octet 0 in the low byte, octet 3 in the high byte.
91+
long value = (long)bytes[0]
92+
| ((long)bytes[1] << 8)
93+
| ((long)bytes[2] << 16)
94+
| ((long)bytes[3] << 24);
95+
address = $"new IPAddress(0x{value.ToString("x8", inv)}L)";
96+
}
97+
else
98+
{
99+
// IPAddress(ReadOnlySpan<byte>) — 16 bytes as hex pairs for readability.
100+
var byteList = string.Join(", ", bytes.Select(b => "0x" + b.ToString("x2", inv)));
101+
address = $"new IPAddress([{byteList}])";
102+
}
103+
104+
sb.AppendLine($" // {line}");
105+
sb.AppendLine($" new IPNetwork({address}, prefixLength: {prefix}),");
106+
sb.AppendLine();
107+
}
108+
sb.AppendLine(" ];");
109+
sb.AppendLine("}");
110+
111+
File.WriteAllText(OutputFile, sb.ToString());
112+
}
113+
]]></Code>
114+
</Task>
115+
</UsingTask>
116+
117+
<Target Name="FetchCloudflareIPs"
118+
Condition="'$(UpdateCloudflareIPs)' == 'true'"
119+
BeforeTargets="PrepareForBuild">
120+
<MakeDir Directories="$(IntermediateOutputPath)" />
121+
<DownloadFile SourceUrl="https://www.cloudflare.com/ips-v4"
122+
DestinationFolder="$(IntermediateOutputPath)"
123+
DestinationFileName="cf-v4.txt"
124+
SkipUnchangedFiles="true"
125+
Retries="3"
126+
RetryDelayMilliseconds="2000" />
127+
<DownloadFile SourceUrl="https://www.cloudflare.com/ips-v6"
128+
DestinationFolder="$(IntermediateOutputPath)"
129+
DestinationFileName="cf-v6.txt"
130+
SkipUnchangedFiles="true"
131+
Retries="3"
132+
RetryDelayMilliseconds="2000" />
133+
<GenerateCloudflareIPsSource IPv4File="$(IntermediateOutputPath)cf-v4.txt"
134+
IPv6File="$(IntermediateOutputPath)cf-v6.txt"
135+
OutputFile="$(MSBuildProjectDirectory)/Utils/CloudflareNetworks.g.cs" />
136+
</Target>
137+
</Project>

Common/Common.csproj

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -11,49 +11,5 @@
1111
<PackageReference Include="Serilog" />
1212
</ItemGroup>
1313

14-
<!-- Fetch Cloudflare IPs at build time and generate C# source -->
15-
<UsingTask TaskName="GenerateCloudflareIPsSource" TaskFactory="RoslynCodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll">
16-
<ParameterGroup>
17-
<IPv4File ParameterType="System.String" Required="true" />
18-
<IPv6File ParameterType="System.String" Required="true" />
19-
<OutputFile ParameterType="System.String" Required="true" />
20-
</ParameterGroup>
21-
<Task>
22-
<Code Type="Fragment" Language="cs"><![CDATA[
23-
var lines = new List<string>();
24-
lines.AddRange(File.ReadAllLines(IPv4File).Where(l => !string.IsNullOrWhiteSpace(l)));
25-
lines.AddRange(File.ReadAllLines(IPv6File).Where(l => !string.IsNullOrWhiteSpace(l)));
26-
27-
var sb = new System.Text.StringBuilder();
28-
sb.AppendLine("// <auto-generated/>");
29-
sb.AppendLine("using System.Net;");
30-
sb.AppendLine();
31-
sb.AppendLine("namespace OpenShock.Common.Utils;");
32-
sb.AppendLine();
33-
sb.AppendLine("public static partial class TrustedProxiesFetcher");
34-
sb.AppendLine("{");
35-
sb.AppendLine(" private static readonly IPNetwork[] CloudflareNetworks =");
36-
sb.AppendLine(" [");
37-
foreach (var line in lines)
38-
sb.AppendLine($" IPNetwork.Parse(\"{line.Trim()}\"),");
39-
sb.AppendLine(" ];");
40-
sb.AppendLine("}");
41-
42-
File.WriteAllText(OutputFile, sb.ToString());
43-
]]></Code>
44-
</Task>
45-
</UsingTask>
46-
47-
<Target Name="FetchCloudflareIPs" BeforeTargets="PrepareForBuild">
48-
<MakeDir Directories="$(IntermediateOutputPath)" />
49-
<DownloadFile SourceUrl="https://www.cloudflare.com/ips-v4"
50-
DestinationFolder="$(IntermediateOutputPath)"
51-
DestinationFileName="cf-v4.txt" />
52-
<DownloadFile SourceUrl="https://www.cloudflare.com/ips-v6"
53-
DestinationFolder="$(IntermediateOutputPath)"
54-
DestinationFileName="cf-v6.txt" />
55-
<GenerateCloudflareIPsSource IPv4File="$(IntermediateOutputPath)cf-v4.txt"
56-
IPv6File="$(IntermediateOutputPath)cf-v6.txt"
57-
OutputFile="$(MSBuildProjectDirectory)/Utils/CloudflareNetworks.g.cs" />
58-
</Target>
14+
<Import Project="CloudflareIPs.targets" />
5915
</Project>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// <auto-generated />
2+
// DO NOT EDIT - manual changes are overwritten on the next regeneration.
3+
//
4+
// 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.
5+
// 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.
6+
//
7+
// Generated: 2026-04-24T17:08:34Z
8+
// IPv4 SHA256: f02c6d83bc01ab0ae8577160e036d700c7455359bce054df884e5d7d9e4e9e7b
9+
// IPv6 SHA256: 9e9d39e3e83bad00c4decafd53c63fa62029f3d95db68de937d2be28234ca0a9
10+
using System.Net;
11+
12+
namespace OpenShock.Common.Utils;
13+
14+
public static partial class TrustedProxiesFetcher
15+
{
16+
private static readonly IPNetwork[] CloudflareNetworks =
17+
[
18+
// 173.245.48.0/20
19+
new IPNetwork(new IPAddress(0x0030f5adL), prefixLength: 20),
20+
21+
// 103.21.244.0/22
22+
new IPNetwork(new IPAddress(0x00f41567L), prefixLength: 22),
23+
24+
// 103.22.200.0/22
25+
new IPNetwork(new IPAddress(0x00c81667L), prefixLength: 22),
26+
27+
// 103.31.4.0/22
28+
new IPNetwork(new IPAddress(0x00041f67L), prefixLength: 22),
29+
30+
// 141.101.64.0/18
31+
new IPNetwork(new IPAddress(0x0040658dL), prefixLength: 18),
32+
33+
// 108.162.192.0/18
34+
new IPNetwork(new IPAddress(0x00c0a26cL), prefixLength: 18),
35+
36+
// 190.93.240.0/20
37+
new IPNetwork(new IPAddress(0x00f05dbeL), prefixLength: 20),
38+
39+
// 188.114.96.0/20
40+
new IPNetwork(new IPAddress(0x006072bcL), prefixLength: 20),
41+
42+
// 197.234.240.0/22
43+
new IPNetwork(new IPAddress(0x00f0eac5L), prefixLength: 22),
44+
45+
// 198.41.128.0/17
46+
new IPNetwork(new IPAddress(0x008029c6L), prefixLength: 17),
47+
48+
// 162.158.0.0/15
49+
new IPNetwork(new IPAddress(0x00009ea2L), prefixLength: 15),
50+
51+
// 104.16.0.0/13
52+
new IPNetwork(new IPAddress(0x00001068L), prefixLength: 13),
53+
54+
// 104.24.0.0/14
55+
new IPNetwork(new IPAddress(0x00001868L), prefixLength: 14),
56+
57+
// 172.64.0.0/13
58+
new IPNetwork(new IPAddress(0x000040acL), prefixLength: 13),
59+
60+
// 131.0.72.0/22
61+
new IPNetwork(new IPAddress(0x00480083L), prefixLength: 22),
62+
63+
// 2400:cb00::/32
64+
new IPNetwork(new IPAddress([0x24, 0x00, 0xcb, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),
65+
66+
// 2606:4700::/32
67+
new IPNetwork(new IPAddress([0x26, 0x06, 0x47, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),
68+
69+
// 2803:f800::/32
70+
new IPNetwork(new IPAddress([0x28, 0x03, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),
71+
72+
// 2405:b500::/32
73+
new IPNetwork(new IPAddress([0x24, 0x05, 0xb5, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),
74+
75+
// 2405:8100::/32
76+
new IPNetwork(new IPAddress([0x24, 0x05, 0x81, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),
77+
78+
// 2a06:98c0::/29
79+
new IPNetwork(new IPAddress([0x2a, 0x06, 0x98, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 29),
80+
81+
// 2c0f:f248::/32
82+
new IPNetwork(new IPAddress([0x2c, 0x0f, 0xf2, 0x48, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), prefixLength: 32),
83+
84+
];
85+
}

0 commit comments

Comments
 (0)