Remove managed add maxmind

This commit is contained in:
Owen
2025-10-14 14:15:33 -07:00
parent 42facf8e12
commit 1fbf2bfb8d
5 changed files with 77 additions and 235 deletions

View File

@@ -4,12 +4,7 @@
gerbil: gerbil:
start_port: 51820 start_port: 51820
base_endpoint: "{{.DashboardDomain}}" base_endpoint: "{{.DashboardDomain}}"
{{if .HybridMode}}
managed:
id: "{{.HybridId}}"
secret: "{{.HybridSecret}}"
{{else}}
app: app:
dashboard_url: "https://{{.DashboardDomain}}" dashboard_url: "https://{{.DashboardDomain}}"
log_level: "info" log_level: "info"
@@ -28,6 +23,7 @@ server:
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"] methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"] allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false credentials: false
{{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
{{if .EnableEmail}} {{if .EnableEmail}}
email: email:
smtp_host: "{{.EmailSMTPHost}}" smtp_host: "{{.EmailSMTPHost}}"
@@ -41,4 +37,3 @@ flags:
disable_signup_without_invite: true disable_signup_without_invite: true
disable_user_create_org: false disable_user_create_org: false
allow_raw_resources: true allow_raw_resources: true
{{end}}

View File

@@ -33,7 +33,7 @@ services:
ports: ports:
- 51820:51820/udp - 51820:51820/udp
- 21820:21820/udp - 21820:21820/udp
- 443:{{if .HybridMode}}8443{{else}}443{{end}} - 443:443
- 80:80 - 80:80
{{end}} {{end}}
traefik: traefik:

View File

@@ -3,17 +3,12 @@ api:
dashboard: true dashboard: true
providers: providers:
{{if not .HybridMode}}
http: http:
endpoint: "http://pangolin:3001/api/v1/traefik-config" endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s" pollInterval: "5s"
file: file:
filename: "/etc/traefik/dynamic_config.yml" filename: "/etc/traefik/dynamic_config.yml"
{{else}}
file:
directory: "/var/dynamic"
watch: true
{{end}}
experimental: experimental:
plugins: plugins:
badger: badger:
@@ -27,7 +22,7 @@ log:
maxBackups: 3 maxBackups: 3
maxAge: 3 maxAge: 3
compress: true compress: true
{{if not .HybridMode}}
certificatesResolvers: certificatesResolvers:
letsencrypt: letsencrypt:
acme: acme:
@@ -36,22 +31,18 @@ certificatesResolvers:
email: "{{.LetsEncryptEmail}}" email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json" storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory" caServer: "https://acme-v02.api.letsencrypt.org/directory"
{{end}}
entryPoints: entryPoints:
web: web:
address: ":80" address: ":80"
websecure: websecure:
address: ":443" address: ":443"
{{if .HybridMode}} proxyProtocol:
trustedIPs:
- 0.0.0.0/0
- ::1/128{{end}}
transport: transport:
respondingTimeouts: respondingTimeouts:
readTimeout: "30m" readTimeout: "30m"
{{if not .HybridMode}} http: http:
tls: tls:
certResolver: "letsencrypt"{{end}} certResolver: "letsencrypt"
serversTransport: serversTransport:
insecureSkipVerify: true insecureSkipVerify: true

View File

@@ -2,7 +2,6 @@ package main
import ( import (
"bufio" "bufio"
"bytes"
"embed" "embed"
"fmt" "fmt"
"io" "io"
@@ -48,10 +47,8 @@ type Config struct {
InstallGerbil bool InstallGerbil bool
TraefikBouncerKey string TraefikBouncerKey string
DoCrowdsecInstall bool DoCrowdsecInstall bool
EnableGeoblocking bool
Secret string Secret string
HybridMode bool
HybridId string
HybridSecret string
} }
type SupportedContainer string type SupportedContainer string
@@ -98,24 +95,6 @@ func main() {
fmt.Println("\n=== Generating Configuration Files ===") 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 { if err := createConfigFiles(config); err != nil {
fmt.Printf("Error creating config files: %v\n", err) fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1) os.Exit(1)
@@ -125,6 +104,15 @@ func main() {
fmt.Println("\nConfiguration files created successfully!") 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 ===") fmt.Println("\n=== Starting installation ===")
if readBool(reader, "Would you like to install and start the containers?", true) { 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!") fmt.Println("Looks like you already installed Pangolin!")
} }
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() { if !checkIsCrowdsecInstalledInCompose() {
fmt.Println("\n=== CrowdSec Install ===") fmt.Println("\n=== CrowdSec Install ===")
// check if crowdsec is installed // check if crowdsec is installed
if readBool(reader, "Would you like to install CrowdSec?", false) { 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 // Setup Token Section
fmt.Println("\n=== Setup Token ===") fmt.Println("\n=== Setup Token ===")
@@ -251,9 +239,7 @@ func main() {
fmt.Println("\nInstallation complete!") 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 { func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
@@ -328,66 +314,38 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration // Basic configuration
fmt.Println("\n=== 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)", "") config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
config.HybridMode = true // Set default dashboard domain after base domain is collected
break defaultDashboardDomain := ""
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") { if config.BaseDomain != "" {
config.HybridMode = false defaultDashboardDomain = "pangolin." + config.BaseDomain
break }
} config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
fmt.Println("Please answer 'yes' or 'no'") 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 { // Validate required fields
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false) if config.BaseDomain == "" {
fmt.Println("Error: Domain name is required")
if alreadyHaveCreds { os.Exit(1)
config.HybridId = readString(reader, "Enter your ID", "") }
config.HybridSecret = readString(reader, "Enter your secret", "") if config.LetsEncryptEmail == "" {
} fmt.Println("Error: Let's Encrypt email is required")
os.Exit(1)
// 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)
}
} }
// Advanced configuration // Advanced configuration
@@ -395,6 +353,7 @@ func collectUserInput(reader *bufio.Reader) Config {
fmt.Println("\n=== Advanced Configuration ===") fmt.Println("\n=== Advanced Configuration ===")
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) 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 == "" { if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required") fmt.Println("Error: Dashboard Domain name is required")
@@ -429,11 +388,6 @@ func createConfigFiles(config Config) error {
return nil return nil
} }
// the hybrid does not need the dynamic config
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
return nil
}
// skip .DS_Store // skip .DS_Store
if strings.Contains(path, ".DS_Store") { if strings.Contains(path, ".DS_Store") {
return nil return nil
@@ -663,18 +617,30 @@ func checkPortsAvailable(port int) error {
return nil return nil
} }
func checkIsPangolinInstalledWithHybrid() bool { func downloadMaxMindDatabase() error {
// Check if config/config.yml exists and contains hybrid section fmt.Println("Downloading MaxMind GeoLite2 Country database...")
if _, err := os.Stat("config/config.yml"); err != nil {
return false // 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 // Extract the database
content, err := os.ReadFile("config/config.yml") if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
if err != nil { return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
return false
} }
// Check for hybrid section // Find the .mmdb file and move it to the config directory
return bytes.Contains(content, []byte("managed:")) 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
} }

View File

@@ -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
}