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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- This is an empty migration.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- AlterTable
ALTER TABLE "domains" ADD COLUMN "realIpCloudflare" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "realIpCustomCidrs" TEXT[] DEFAULT ARRAY[]::TEXT[],
ADD COLUMN "realIpEnabled" BOOLEAN NOT NULL DEFAULT false;
5 changes: 5 additions & 0 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ model Domain {
sslExpiry DateTime?
modsecEnabled Boolean @default(true)

// Real IP Configuration (for Cloudflare and other proxies)
realIpEnabled Boolean @default(false)
realIpCloudflare Boolean @default(false) // Use Cloudflare IP ranges
realIpCustomCidrs String[] @default([]) // Custom CIDR ranges for set_real_ip_from

// Relations
upstreams Upstream[]
loadBalancer LoadBalancerConfig?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ server {
}
proxy_ssl_server_name on;
proxy_ssl_name ${domain.name};
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
`
: ''
}
Expand Down Expand Up @@ -415,7 +415,7 @@ server {
}
proxy_ssl_server_name on;
proxy_ssl_name ${domain.name};
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
`
: ''
}
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/domains/domains/domains.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,15 @@ export class DomainsController {
return;
}

const { name, upstreams, loadBalancer, modsecEnabled } = req.body;
const { name, upstreams, loadBalancer, modsecEnabled, realIpConfig } = req.body;

const domain = await domainsService.createDomain(
{
name,
upstreams,
loadBalancer,
modsecEnabled,
realIpConfig,
},
req.user!.userId,
req.user!.username,
Expand Down Expand Up @@ -151,7 +152,7 @@ export class DomainsController {
}

const { id } = req.params;
const { name, status, modsecEnabled, upstreams, loadBalancer } = req.body;
const { name, status, modsecEnabled, upstreams, loadBalancer, realIpConfig } = req.body;

const domain = await domainsService.updateDomain(
id,
Expand All @@ -161,6 +162,7 @@ export class DomainsController {
modsecEnabled,
upstreams,
loadBalancer,
realIpConfig,
},
req.user!.userId,
req.user!.username,
Expand Down
15 changes: 15 additions & 0 deletions apps/api/src/domains/domains/domains.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,9 @@ export class DomainsRepository {
name: input.name,
status: 'inactive' as const,
modsecEnabled: input.modsecEnabled !== undefined ? input.modsecEnabled : true,
realIpEnabled: input.realIpConfig?.realIpEnabled || false,
realIpCloudflare: input.realIpConfig?.realIpCloudflare || false,
realIpCustomCidrs: input.realIpConfig?.realIpCustomCidrs || [],
upstreams: {
create: input.upstreams.map((u: CreateUpstreamData) => ({
host: u.host,
Expand Down Expand Up @@ -211,6 +214,18 @@ export class DomainsRepository {
input.modsecEnabled !== undefined
? input.modsecEnabled
: currentDomain.modsecEnabled,
realIpEnabled:
input.realIpConfig?.realIpEnabled !== undefined
? input.realIpConfig.realIpEnabled
: currentDomain.realIpEnabled,
realIpCloudflare:
input.realIpConfig?.realIpCloudflare !== undefined
? input.realIpConfig.realIpCloudflare
: currentDomain.realIpCloudflare,
realIpCustomCidrs:
input.realIpConfig?.realIpCustomCidrs !== undefined
? input.realIpConfig.realIpCustomCidrs
: currentDomain.realIpCustomCidrs,
},
});

Expand Down
9 changes: 9 additions & 0 deletions apps/api/src/domains/domains/domains.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,20 @@ export interface LoadBalancerConfigData {
healthCheckPath?: string;
}

// Real IP configuration data
export interface RealIpConfigData {
realIpEnabled?: boolean;
realIpCloudflare?: boolean;
realIpCustomCidrs?: string[];
}

// Domain creation input
export interface CreateDomainInput {
name: string;
upstreams: CreateUpstreamData[];
loadBalancer?: LoadBalancerConfigData;
modsecEnabled?: boolean;
realIpConfig?: RealIpConfigData;
}

// Domain update input
Expand All @@ -47,6 +55,7 @@ export interface UpdateDomainInput {
modsecEnabled?: boolean;
upstreams?: CreateUpstreamData[];
loadBalancer?: LoadBalancerConfigData;
realIpConfig?: RealIpConfigData;
}

// Domain query filters
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/domains/domains/dto/create-domain.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CreateUpstreamData, LoadBalancerConfigData } from '../domains.types';
import { CreateUpstreamData, LoadBalancerConfigData, RealIpConfigData } from '../domains.types';

/**
* DTO for creating a new domain
Expand All @@ -8,4 +8,5 @@ export interface CreateDomainDto {
upstreams: CreateUpstreamData[];
loadBalancer?: LoadBalancerConfigData;
modsecEnabled?: boolean;
realIpConfig?: RealIpConfigData;
}
3 changes: 2 additions & 1 deletion apps/api/src/domains/domains/dto/update-domain.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CreateUpstreamData, LoadBalancerConfigData } from '../domains.types';
import { CreateUpstreamData, LoadBalancerConfigData, RealIpConfigData } from '../domains.types';

/**
* DTO for updating a domain
Expand All @@ -9,4 +9,5 @@ export interface UpdateDomainDto {
modsecEnabled?: boolean;
upstreams?: CreateUpstreamData[];
loadBalancer?: LoadBalancerConfigData;
realIpConfig?: RealIpConfigData;
}
129 changes: 129 additions & 0 deletions apps/api/src/domains/domains/services/cloudflare-ips.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import logger from '../../../utils/logger';

/**
* Service for fetching Cloudflare IP ranges
*/
export class CloudflareIpsService {
private static readonly IPV4_URL = 'https://www.cloudflare.com/ips-v4';
private static readonly IPV6_URL = 'https://www.cloudflare.com/ips-v6';

// Fallback IPs if fetch fails
private static readonly FALLBACK_IPV4 = [
'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',
];

private static readonly FALLBACK_IPV6 = [
'2400:cb00::/32',
'2606:4700::/32',
'2803:f800::/32',
'2405:b500::/32',
'2405:8100::/32',
'2a06:98c0::/29',
'2c0f:f248::/32',
];

// Cache for IPs (valid for 24 hours)
private static cachedIPs: string[] | null = null;
private static cacheTimestamp: number = 0;
private static readonly CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours

/**
* Get Cloudflare IPs (IPv4 + IPv6)
* Tries to fetch from Cloudflare, falls back to hardcoded list
*/
async getCloudflareIPs(): Promise<string[]> {
// Check cache first
if (CloudflareIpsService.cachedIPs &&
Date.now() - CloudflareIpsService.cacheTimestamp < CloudflareIpsService.CACHE_TTL) {
logger.info('Using cached Cloudflare IPs');
return CloudflareIpsService.cachedIPs;
}

// Try to fetch fresh IPs
try {
const [ipv4List, ipv6List] = await Promise.all([
this.fetchIPs(CloudflareIpsService.IPV4_URL),
this.fetchIPs(CloudflareIpsService.IPV6_URL),
]);

const allIPs = [...ipv4List, ...ipv6List];

if (allIPs.length > 0) {
// Update cache
CloudflareIpsService.cachedIPs = allIPs;
CloudflareIpsService.cacheTimestamp = Date.now();
logger.info(`Fetched ${allIPs.length} Cloudflare IPs successfully`);
return allIPs;
}

throw new Error('No IPs fetched');
} catch (error) {
logger.warn('Failed to fetch Cloudflare IPs, using fallback list', error);
return this.getFallbackIPs();
}
}

/**
* Fetch IPs from a URL
*/
private async fetchIPs(url: string): Promise<string[]> {
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'User-Agent': 'nginx-love/1.0',
},
signal: AbortSignal.timeout(5000), // 5 second timeout
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

const text = await response.text();
return text
.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
} catch (error) {
logger.error(`Failed to fetch from ${url}:`, error);
return [];
}
}

/**
* Get fallback IPs (hardcoded list)
*/
private getFallbackIPs(): string[] {
return [
...CloudflareIpsService.FALLBACK_IPV4,
...CloudflareIpsService.FALLBACK_IPV6,
];
}

/**
* Clear cache (for testing or manual refresh)
*/
clearCache(): void {
CloudflareIpsService.cachedIPs = null;
CloudflareIpsService.cacheTimestamp = 0;
logger.info('Cloudflare IPs cache cleared');
}
}

// Export singleton instance
export const cloudflareIpsService = new CloudflareIpsService();
1 change: 1 addition & 0 deletions apps/api/src/domains/domains/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './nginx-config.service';
export * from './nginx-reload.service';
export * from './upstream-health.service';
export * from './cloudflare-ips.service';
Loading
Loading