diff --git a/install/config/config.yml b/install/config/config.yml index b86f7890..b255760f 100644 --- a/install/config/config.yml +++ b/install/config/config.yml @@ -4,12 +4,7 @@ gerbil: start_port: 51820 base_endpoint: "{{.DashboardDomain}}" -{{if .HybridMode}} -managed: - id: "{{.HybridId}}" - secret: "{{.HybridSecret}}" - -{{else}} + app: dashboard_url: "https://{{.DashboardDomain}}" log_level: "info" @@ -28,6 +23,7 @@ server: methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] allowed_headers: ["X-CSRF-Token", "Content-Type"] credentials: false + {{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}} {{if .EnableEmail}} email: smtp_host: "{{.EmailSMTPHost}}" @@ -40,5 +36,4 @@ flags: require_email_verification: {{.EnableEmail}} disable_signup_without_invite: true disable_user_create_org: false - allow_raw_resources: true -{{end}} \ No newline at end of file + allow_raw_resources: true \ No newline at end of file diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index c06e55ca..6ec3989c 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -33,7 +33,7 @@ services: ports: - 51820:51820/udp - 21820:21820/udp - - 443:{{if .HybridMode}}8443{{else}}443{{end}} + - 443:443 - 80:80 {{end}} traefik: diff --git a/install/config/traefik/traefik_config.yml b/install/config/traefik/traefik_config.yml index 8bb5aa6c..a9693ce6 100644 --- a/install/config/traefik/traefik_config.yml +++ b/install/config/traefik/traefik_config.yml @@ -3,17 +3,12 @@ api: dashboard: true providers: -{{if not .HybridMode}} http: endpoint: "http://pangolin:3001/api/v1/traefik-config" pollInterval: "5s" file: filename: "/etc/traefik/dynamic_config.yml" -{{else}} - file: - directory: "/var/dynamic" - watch: true -{{end}} + experimental: plugins: badger: @@ -27,7 +22,7 @@ log: maxBackups: 3 maxAge: 3 compress: true -{{if not .HybridMode}} + certificatesResolvers: letsencrypt: acme: @@ -36,22 +31,18 @@ certificatesResolvers: email: "{{.LetsEncryptEmail}}" storage: "/letsencrypt/acme.json" caServer: "https://acme-v02.api.letsencrypt.org/directory" -{{end}} + entryPoints: web: address: ":80" websecure: address: ":443" -{{if .HybridMode}} proxyProtocol: - trustedIPs: - - 0.0.0.0/0 - - ::1/128{{end}} transport: respondingTimeouts: readTimeout: "30m" -{{if not .HybridMode}} http: + http: tls: - certResolver: "letsencrypt"{{end}} + certResolver: "letsencrypt" serversTransport: insecureSkipVerify: true diff --git a/install/main.go b/install/main.go index f16dc214..8c857ec3 100644 --- a/install/main.go +++ b/install/main.go @@ -2,7 +2,6 @@ package main import ( "bufio" - "bytes" "embed" "fmt" "io" @@ -48,10 +47,8 @@ type Config struct { InstallGerbil bool TraefikBouncerKey string DoCrowdsecInstall bool + EnableGeoblocking bool Secret string - HybridMode bool - HybridId string - HybridSecret string } type SupportedContainer string @@ -98,24 +95,6 @@ func main() { fmt.Println("\n=== Generating Configuration Files ===") - // If the secret and id are not generated then generate them - if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") { - // fmt.Println("Requesting hybrid credentials from cloud...") - credentials, err := requestHybridCredentials() - if err != nil { - fmt.Printf("Error requesting hybrid credentials: %v\n", err) - fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.") - os.Exit(1) - } - config.HybridId = credentials.RemoteExitNodeId - config.HybridSecret = credentials.Secret - fmt.Printf("Your managed credentials have been obtained successfully.\n") - fmt.Printf(" ID: %s\n", config.HybridId) - fmt.Printf(" Secret: %s\n", config.HybridSecret) - fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.") - readBool(reader, "Have you adopted your node?", true) - } - if err := createConfigFiles(config); err != nil { fmt.Printf("Error creating config files: %v\n", err) os.Exit(1) @@ -125,6 +104,15 @@ func main() { fmt.Println("\nConfiguration files created successfully!") + // Download MaxMind database if requested + if config.EnableGeoblocking { + fmt.Println("\n=== Downloading MaxMind Database ===") + if err := downloadMaxMindDatabase(); err != nil { + fmt.Printf("Error downloading MaxMind database: %v\n", err) + fmt.Println("You can download it manually later if needed.") + } + } + fmt.Println("\n=== Starting installation ===") if readBool(reader, "Would you like to install and start the containers?", true) { @@ -174,7 +162,7 @@ func main() { fmt.Println("Looks like you already installed Pangolin!") } - if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() { + if !checkIsCrowdsecInstalledInCompose() { fmt.Println("\n=== CrowdSec Install ===") // check if crowdsec is installed if readBool(reader, "Would you like to install CrowdSec?", false) { @@ -230,7 +218,7 @@ func main() { } } - if !config.HybridMode && !alreadyInstalled { + if !alreadyInstalled { // Setup Token Section fmt.Println("\n=== Setup Token ===") @@ -251,9 +239,7 @@ func main() { fmt.Println("\nInstallation complete!") - if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() { - fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) - } + fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } func podmanOrDocker(reader *bufio.Reader) SupportedContainer { @@ -328,66 +314,38 @@ func collectUserInput(reader *bufio.Reader) Config { // Basic configuration fmt.Println("\n=== Basic Configuration ===") - for { - response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "") - if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") { - config.HybridMode = true - break - } else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") { - config.HybridMode = false - break - } - fmt.Println("Please answer 'yes' or 'no'") + + config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") + + // Set default dashboard domain after base domain is collected + defaultDashboardDomain := "" + if config.BaseDomain != "" { + defaultDashboardDomain = "pangolin." + config.BaseDomain + } + config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) + config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") + config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) + + // Email configuration + fmt.Println("\n=== Email Configuration ===") + config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) + + if config.EnableEmail { + config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") + config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) + config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") + config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? + config.EmailNoReply = readString(reader, "Enter no-reply email address", "") } - if config.HybridMode { - alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false) - - if alreadyHaveCreds { - config.HybridId = readString(reader, "Enter your ID", "") - config.HybridSecret = readString(reader, "Enter your secret", "") - } - - // Try to get public IP as default - publicIP := getPublicIP() - if publicIP != "" { - fmt.Printf("Detected public IP: %s\n", publicIP) - } - config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP) - config.InstallGerbil = true - } else { - config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") - - // Set default dashboard domain after base domain is collected - defaultDashboardDomain := "" - if config.BaseDomain != "" { - defaultDashboardDomain = "pangolin." + config.BaseDomain - } - config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) - config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") - config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) - - // Email configuration - fmt.Println("\n=== Email Configuration ===") - config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) - - if config.EnableEmail { - config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") - config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) - config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") - config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? - config.EmailNoReply = readString(reader, "Enter no-reply email address", "") - } - - // Validate required fields - if config.BaseDomain == "" { - fmt.Println("Error: Domain name is required") - os.Exit(1) - } - if config.LetsEncryptEmail == "" { - fmt.Println("Error: Let's Encrypt email is required") - os.Exit(1) - } + // Validate required fields + if config.BaseDomain == "" { + fmt.Println("Error: Domain name is required") + os.Exit(1) + } + if config.LetsEncryptEmail == "" { + fmt.Println("Error: Let's Encrypt email is required") + os.Exit(1) } // Advanced configuration @@ -395,6 +353,7 @@ func collectUserInput(reader *bufio.Reader) Config { fmt.Println("\n=== Advanced Configuration ===") config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) + config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", false) if config.DashboardDomain == "" { fmt.Println("Error: Dashboard Domain name is required") @@ -429,11 +388,6 @@ func createConfigFiles(config Config) error { return nil } - // the hybrid does not need the dynamic config - if config.HybridMode && strings.Contains(path, "dynamic_config.yml") { - return nil - } - // skip .DS_Store if strings.Contains(path, ".DS_Store") { return nil @@ -663,18 +617,30 @@ func checkPortsAvailable(port int) error { return nil } -func checkIsPangolinInstalledWithHybrid() bool { - // Check if config/config.yml exists and contains hybrid section - if _, err := os.Stat("config/config.yml"); err != nil { - return false +func downloadMaxMindDatabase() error { + fmt.Println("Downloading MaxMind GeoLite2 Country database...") + + // Download the GeoLite2 Country database + if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz", + "https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil { + return fmt.Errorf("failed to download GeoLite2 database: %v", err) } - - // Read config file to check for hybrid section - content, err := os.ReadFile("config/config.yml") - if err != nil { - return false + + // Extract the database + if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil { + return fmt.Errorf("failed to extract GeoLite2 database: %v", err) } - - // Check for hybrid section - return bytes.Contains(content, []byte("managed:")) + + // Find the .mmdb file and move it to the config directory + if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil { + return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err) + } + + // Clean up the downloaded files + if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil { + fmt.Printf("Warning: failed to clean up temporary files: %v\n", err) + } + + fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!") + return nil } diff --git a/install/quickStart.go b/install/quickStart.go deleted file mode 100644 index ece8e8ff..00000000 --- a/install/quickStart.go +++ /dev/null @@ -1,110 +0,0 @@ -package main - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "time" -) - -const ( - FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e" - // CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" - CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start" -) - -// HybridCredentials represents the response from the cloud API -type HybridCredentials struct { - RemoteExitNodeId string `json:"remoteExitNodeId"` - Secret string `json:"secret"` -} - -// APIResponse represents the full response structure from the cloud API -type APIResponse struct { - Data HybridCredentials `json:"data"` -} - -// RequestPayload represents the request body structure -type RequestPayload struct { - Token string `json:"token"` -} - -func generateValidationToken() string { - timestamp := time.Now().UnixMilli() - data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp) - obfuscated := make([]byte, len(data)) - for i, char := range []byte(data) { - obfuscated[i] = char + 5 - } - return base64.StdEncoding.EncodeToString(obfuscated) -} - -// requestHybridCredentials makes an HTTP POST request to the cloud API -// to get hybrid credentials (ID and secret) -func requestHybridCredentials() (*HybridCredentials, error) { - // Generate validation token - token := generateValidationToken() - - // Create request payload - payload := RequestPayload{ - Token: token, - } - - // Marshal payload to JSON - jsonData, err := json.Marshal(payload) - if err != nil { - return nil, fmt.Errorf("failed to marshal request payload: %v", err) - } - - // Create HTTP request - req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("failed to create HTTP request: %v", err) - } - - // Set headers - req.Header.Set("Content-Type", "application/json") - req.Header.Set("X-CSRF-Token", "x-csrf-protection") - - // Create HTTP client with timeout - client := &http.Client{ - Timeout: 30 * time.Second, - } - - // Make the request - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to make HTTP request: %v", err) - } - defer resp.Body.Close() - - // Check response status - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode) - } - - // Read response body for debugging - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %v", err) - } - - // Print the raw JSON response for debugging - // fmt.Printf("Raw JSON response: %s\n", string(body)) - - // Parse response - var apiResponse APIResponse - if err := json.Unmarshal(body, &apiResponse); err != nil { - return nil, fmt.Errorf("failed to decode API response: %v", err) - } - - // Validate response data - if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" { - return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret") - } - - return &apiResponse.Data, nil -}