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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<p align="center">
<img src="https://img.shields.io/badge/macOS-13%2B-blue" alt="macOS 13+">
<img src="https://img.shields.io/badge/Swift-5.9-orange" alt="Swift 5.9">
<a href="https://github.com/GeiserX/vpn-macos-bypass/releases"><img src="https://img.shields.io/badge/version-1.6.10-green" alt="Version"></a>
<a href="https://github.com/GeiserX/vpn-macos-bypass/releases"><img src="https://img.shields.io/badge/version-1.6.11-green" alt="Version"></a>
</p>

## Why?
Expand Down
121 changes: 107 additions & 14 deletions Sources/RouteManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -286,13 +286,26 @@ final class RouteManager: ObservableObject {
let id: UUID
var domain: String
var enabled: Bool
var includeSubdomains: Bool
var resolvedIP: String?
var lastResolved: Date?

init(domain: String, enabled: Bool = true) {
init(domain: String, enabled: Bool = true, includeSubdomains: Bool = true) {
self.id = UUID()
self.domain = domain
self.enabled = enabled
self.includeSubdomains = includeSubdomains
}

// Custom decoder for backward compatibility with configs missing includeSubdomains
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
domain = try container.decode(String.self, forKey: .domain)
enabled = try container.decode(Bool.self, forKey: .enabled)
includeSubdomains = try container.decodeIfPresent(Bool.self, forKey: .includeSubdomains) ?? false
resolvedIP = try container.decodeIfPresent(String.self, forKey: .resolvedIP)
lastResolved = try container.decodeIfPresent(Date.self, forKey: .lastResolved)
}
}

Expand Down Expand Up @@ -935,9 +948,13 @@ final class RouteManager: ObservableObject {
// Collect all domains to resolve (for parallel resolution)
var allDomains: [(domain: String, source: String)] = []

// Add custom domains
// Add custom domains (and www. subdomains if includeSubdomains is enabled)
for domain in config.domains where domain.enabled {
allDomains.append((domain.domain, domain.domain))
// Also add www. subdomain if includeSubdomains is enabled
if domain.includeSubdomains && !domain.domain.hasPrefix("www.") {
allDomains.append(("www.\(domain.domain)", domain.domain))
}
}

// Add service domains
Expand Down Expand Up @@ -1145,7 +1162,7 @@ final class RouteManager: ObservableObject {
}
}

// Custom domains
// Custom domains (and www. subdomains if includeSubdomains is enabled)
for domain in config.domains where domain.enabled {
if let cachedIPs = dnsDiskCache[domain.domain] {
for ip in cachedIPs where !seenDestinations.contains(ip) {
Expand All @@ -1156,6 +1173,19 @@ final class RouteManager: ObservableObject {
dnsCache[domain.domain] = firstIP
}
}
// Also add www. subdomain if includeSubdomains is enabled
if domain.includeSubdomains && !domain.domain.hasPrefix("www.") {
let wwwDomain = "www.\(domain.domain)"
if let cachedIPs = dnsDiskCache[wwwDomain] {
for ip in cachedIPs where !seenDestinations.contains(ip) {
seenDestinations.insert(ip)
routesToAdd.append((destination: ip, gateway: gateway, isNetwork: false, source: domain.domain))
}
if let firstIP = cachedIPs.first {
dnsCache[wwwDomain] = firstIP
}
}
}
}

log(.info, "Applying \(routesToAdd.count) routes from cache...")
Expand Down Expand Up @@ -1199,6 +1229,10 @@ final class RouteManager: ObservableObject {
}
for domain in config.domains where domain.enabled {
domainsToResolve.append((domain.domain, domain.domain))
// Also add www. subdomain if includeSubdomains is enabled
if domain.includeSubdomains && !domain.domain.hasPrefix("www.") {
domainsToResolve.append(("www.\(domain.domain)", domain.domain))
}
}

// Resolve DNS in parallel
Expand Down Expand Up @@ -1354,6 +1388,10 @@ final class RouteManager: ObservableObject {

for domain in config.domains where domain.enabled {
domainsToResolve.append((domain.domain, domain.domain))
// Also add www. subdomain if includeSubdomains is enabled
if domain.includeSubdomains && !domain.domain.hasPrefix("www.") {
domainsToResolve.append(("www.\(domain.domain)", domain.domain))
}
}

for service in config.services where service.enabled {
Expand Down Expand Up @@ -1480,27 +1518,38 @@ final class RouteManager: ObservableObject {
}
}

func addDomain(_ domain: String) {
func addDomain(_ domain: String, includeSubdomains: Bool = true) {
let cleaned = cleanDomain(domain)
guard !cleaned.isEmpty else { return }
guard !config.domains.contains(where: { $0.domain == cleaned }) else {
log(.warning, "Domain \(cleaned) already exists")
return
}

config.domains.append(DomainEntry(domain: cleaned))
config.domains.append(DomainEntry(domain: cleaned, includeSubdomains: includeSubdomains))
saveConfig()
log(.success, "Added domain: \(cleaned)")
let subdomainNote = includeSubdomains ? " (+www)" : ""
log(.success, "Added domain: \(cleaned)\(subdomainNote)")

// Apply route immediately if VPN connected
if isVPNConnected, let gateway = localGateway {
isApplyingRoutes = true
Task {
// Apply routes for main domain
if let routes = await applyRoutesForDomain(cleaned, gateway: gateway) {
await MainActor.run {
activeRoutes.append(contentsOf: routes)
}
}
// Also apply routes for www. subdomain if includeSubdomains is enabled
if includeSubdomains && !cleaned.hasPrefix("www.") {
let wwwDomain = "www.\(cleaned)"
if let wwwRoutes = await applyRoutesForDomain(wwwDomain, gateway: gateway, source: cleaned) {
await MainActor.run {
activeRoutes.append(contentsOf: wwwRoutes)
}
}
}
await MainActor.run {
isApplyingRoutes = false
}
Expand Down Expand Up @@ -1544,6 +1593,15 @@ final class RouteManager: ObservableObject {
activeRoutes.append(contentsOf: routes)
}
}
// Also add www. subdomain routes if includeSubdomains is enabled
if domain.includeSubdomains && !domain.domain.hasPrefix("www.") {
let wwwDomain = "www.\(domain.domain)"
if let wwwRoutes = await applyRoutesForDomain(wwwDomain, gateway: gateway, source: domain.domain) {
await MainActor.run {
activeRoutes.append(contentsOf: wwwRoutes)
}
}
}
} else {
// Domain was just disabled - remove its routes
await removeRoutesForSource(domain.domain)
Expand Down Expand Up @@ -1580,6 +1638,15 @@ final class RouteManager: ObservableObject {
activeRoutes.append(contentsOf: routes)
}
}
// Also add www. subdomain routes if includeSubdomains is enabled
if domain.includeSubdomains && !domain.domain.hasPrefix("www.") {
let wwwDomain = "www.\(domain.domain)"
if let wwwRoutes = await applyRoutesForDomain(wwwDomain, gateway: gateway, source: domain.domain) {
await MainActor.run {
activeRoutes.append(contentsOf: wwwRoutes)
}
}
}
} else {
await removeRoutesForSource(domain.domain)
}
Expand Down Expand Up @@ -2218,6 +2285,15 @@ final class RouteManager: ObservableObject {
// Fallback to disk cache when DNS failed at startup
entries.append((domain.domain, firstIP))
}
// Also add www. subdomain if includeSubdomains is enabled
if domain.includeSubdomains && !domain.domain.hasPrefix("www.") {
let wwwDomain = "www.\(domain.domain)"
if let cachedIP = dnsCache[wwwDomain] {
entries.append((wwwDomain, cachedIP))
} else if let diskCachedIPs = dnsDiskCache[wwwDomain], let firstIP = diskCachedIPs.first {
entries.append((wwwDomain, firstIP))
}
}
}

for service in config.services where service.enabled {
Expand Down Expand Up @@ -2315,18 +2391,35 @@ final class RouteManager: ObservableObject {

private func cleanDomain(_ input: String) -> String {
var domain = input.trimmingCharacters(in: .whitespacesAndNewlines)
// Remove protocol
if let url = URL(string: domain), let host = url.host {
domain = host
} else {
domain = domain
.replacingOccurrences(of: "https://", with: "")
.replacingOccurrences(of: "http://", with: "")

// Remove any protocol scheme (https://, http://, ssh://, ftp://, etc.)
// Pattern: starts with letter, followed by letters/digits/+/./-, then ://
if let range = domain.range(of: "^[a-zA-Z][a-zA-Z0-9+.-]*://", options: .regularExpression) {
domain = String(domain[range.upperBound...])
}

// Remove userinfo (user:pass@) if present
if let atIndex = domain.firstIndex(of: "@") {
domain = String(domain[domain.index(after: atIndex)...])
}
// Remove path

// Remove path and query string
if let slashIndex = domain.firstIndex(of: "/") {
domain = String(domain[..<slashIndex])
}
if let queryIndex = domain.firstIndex(of: "?") {
domain = String(domain[..<queryIndex])
}

// Remove port number
if let colonIndex = domain.lastIndex(of: ":") {
// Make sure it's actually a port (after hostname, not in IPv6)
let afterColon = String(domain[domain.index(after: colonIndex)...])
if Int(afterColon) != nil {
domain = String(domain[..<colonIndex])
}
}

return domain.lowercased()
}

Expand Down
Loading