diff --git a/.github/workflows/update-cloudflare-proxies.yml b/.github/workflows/update-cloudflare-proxies.yml index 9f9bb8e6..6ed0e573 100644 --- a/.github/workflows/update-cloudflare-proxies.yml +++ b/.github/workflows/update-cloudflare-proxies.yml @@ -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: @@ -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." diff --git a/Common/CloudflareIPs.targets b/Common/CloudflareIPs.targets new file mode 100644 index 00000000..afd006a6 --- /dev/null +++ b/Common/CloudflareIPs.targets @@ -0,0 +1,137 @@ + + + + + + + + + + "); + 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) — 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()); + } + ]]> + + + + + + + + + + diff --git a/Common/Common.csproj b/Common/Common.csproj index 8c6c3174..a1eb241f 100644 --- a/Common/Common.csproj +++ b/Common/Common.csproj @@ -35,12 +35,7 @@ - - - - Never - - + diff --git a/Common/Utils/CloudflareNetworks.g.cs b/Common/Utils/CloudflareNetworks.g.cs new file mode 100644 index 00000000..5682c2a0 --- /dev/null +++ b/Common/Utils/CloudflareNetworks.g.cs @@ -0,0 +1,85 @@ +// +// 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), + + ]; +} diff --git a/Common/Utils/TrustedProxiesFetcher.cs b/Common/Utils/TrustedProxiesFetcher.cs index 1a3142e9..bc1bc087 100644 --- a/Common/Utils/TrustedProxiesFetcher.cs +++ b/Common/Utils/TrustedProxiesFetcher.cs @@ -2,7 +2,7 @@ namespace OpenShock.Common.Utils; -public static class TrustedProxiesFetcher +public static partial class TrustedProxiesFetcher { private static readonly HttpClient Client = new(); @@ -77,14 +77,7 @@ public static async Task 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]; } diff --git a/Common/cloudflare-ips.txt b/Common/cloudflare-ips.txt deleted file mode 100644 index fd160bd4..00000000 --- a/Common/cloudflare-ips.txt +++ /dev/null @@ -1,22 +0,0 @@ -173.245.48.0/20 -103.21.244.0/22 -103.22.200.0/22 -103.31.4.0/22 -141.101.64.0/18 -108.162.192.0/18 -190.93.240.0/20 -188.114.96.0/20 -197.234.240.0/22 -198.41.128.0/17 -162.158.0.0/15 -104.16.0.0/13 -104.24.0.0/14 -172.64.0.0/13 -131.0.72.0/22 -2400:cb00::/32 -2606:4700::/32 -2803:f800::/32 -2405:b500::/32 -2405:8100::/32 -2a06:98c0::/29 -2c0f:f248::/32 \ No newline at end of file diff --git a/global.json b/global.json index 2e206e82..7cceb992 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.0", + "version": "10.0.100", "rollForward": "latestMinor", "allowPrerelease": false },