From 2cddbdff814014be74945c7d31510ab90b57e09a Mon Sep 17 00:00:00 2001 From: Mor David Date: Wed, 3 Dec 2025 06:00:21 +0200 Subject: [PATCH] Add files via upload --- FlareTunnel.go | 2311 ++++++++++++++++++++++++++++++++++++++ README.md | 208 ++++ blacklist-aggressive.txt | 177 +++ blacklist-minimal.txt | 52 + blacklist.txt | 254 +++++ build.bat | 64 ++ build.sh | 64 ++ example_usage.py | 154 +++ go.mod | 7 + go.sum | 4 + logo.png | Bin 0 -> 58551 bytes 11 files changed, 3295 insertions(+) create mode 100644 FlareTunnel.go create mode 100644 README.md create mode 100644 blacklist-aggressive.txt create mode 100644 blacklist-minimal.txt create mode 100644 blacklist.txt create mode 100644 build.bat create mode 100644 build.sh create mode 100644 example_usage.py create mode 100644 go.mod create mode 100644 go.sum create mode 100644 logo.png diff --git a/FlareTunnel.go b/FlareTunnel.go new file mode 100644 index 0000000..7771319 --- /dev/null +++ b/FlareTunnel.go @@ -0,0 +1,2311 @@ +package main + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/json" + "encoding/pem" + "fmt" + "io" + "math/big" + "net" + "net/http" + "net/url" + "os" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/olekukonko/tablewriter" +) + +// ==================================================================== +// CONSTANTS & TYPES +// ==================================================================== + +const ( + Version = "1.0.0" + CloudflareBaseURL = "https://api.cloudflare.com/client/v4" + WorkerScript = `/** + * FlareTunnel - Cloudflare Worker URL Redirection Script + */ +addEventListener('fetch', event => { + event.respondWith(handleRequest(event.request)) +}) + +async function handleRequest(request) { + try { + const url = new URL(request.url) + const targetUrl = getTargetUrl(url, request.headers) + + if (!targetUrl) { + return createErrorResponse('No target URL specified', { + usage: { + query_param: '?url=https://example.com', + header: 'X-Target-URL: https://example.com', + path: '/https://example.com' + } + }, 400) + } + + let targetURL + try { + targetURL = new URL(targetUrl) + } catch (e) { + return createErrorResponse('Invalid target URL', { provided: targetUrl }, 400) + } + + // Build target URL with filtered query parameters + const targetParams = new URLSearchParams() + for (const [key, value] of url.searchParams) { + if (!['url', '_cb', '_t'].includes(key)) { + targetParams.append(key, value) + } + } + if (targetParams.toString()) { + targetURL.search = targetParams.toString() + } + + // Create proxied request + const proxyRequest = createProxyRequest(request, targetURL) + const response = await fetch(proxyRequest) + + // Process and return response + return createProxyResponse(response, request.method) + + } catch (error) { + return createErrorResponse('Proxy request failed', { + message: error.message, + timestamp: new Date().toISOString() + }, 500) + } +} + +function getTargetUrl(url, headers) { + // Priority: query param > header > path + let targetUrl = url.searchParams.get('url') + + if (!targetUrl) { + targetUrl = headers.get('X-Target-URL') + } + + if (!targetUrl && url.pathname !== '/') { + const pathUrl = url.pathname.slice(1) + if (pathUrl.startsWith('http')) { + targetUrl = pathUrl + } + } + + return targetUrl +} + +function createProxyRequest(request, targetURL) { + const proxyHeaders = new Headers() + const allowedHeaders = [ + 'accept', 'accept-language', 'accept-encoding', 'authorization', + 'cache-control', 'content-type', 'origin', 'referer', 'user-agent' + ] + + // Copy allowed headers + for (const [key, value] of request.headers) { + if (allowedHeaders.includes(key.toLowerCase())) { + proxyHeaders.set(key, value) + } + } + + proxyHeaders.set('Host', targetURL.hostname) + + // Set X-Forwarded-For header + const customXForwardedFor = request.headers.get('X-My-X-Forwarded-For') + if (customXForwardedFor) { + proxyHeaders.set('X-Forwarded-For', customXForwardedFor) + } else { + proxyHeaders.set('X-Forwarded-For', generateRandomIP()) + } + + return new Request(targetURL.toString(), { + method: request.method, + headers: proxyHeaders, + body: ['GET', 'HEAD'].includes(request.method) ? null : request.body + }) +} + +function createProxyResponse(response, requestMethod) { + const responseHeaders = new Headers() + + // Copy ALL response headers (let browser handle encoding) + for (const [key, value] of response.headers) { + responseHeaders.set(key, value) + } + + // Add/Override CORS headers + responseHeaders.set('Access-Control-Allow-Origin', '*') + responseHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS, PATCH, HEAD') + responseHeaders.set('Access-Control-Allow-Headers', '*') + + if (requestMethod === 'OPTIONS') { + return new Response(null, { status: 204, headers: responseHeaders }) + } + + return new Response(response.body, { + status: response.status, + statusText: response.statusText, + headers: responseHeaders + }) +} + +function createErrorResponse(error, details, status) { + return new Response(JSON.stringify({ error, ...details }), { + status, + headers: { 'Content-Type': 'application/json' } + }) +} + +function generateRandomIP() { + return [1, 2, 3, 4].map(() => Math.floor(Math.random() * 255) + 1).join('.') +}` +) + +// Account represents a Cloudflare account configuration +type Account struct { + Name string `json:"name"` + APIToken string `json:"api_token"` + AccountID string `json:"account_id"` + ZoneID string `json:"zone_id,omitempty"` +} + +// Config represents the FlareTunnel configuration +type Config struct { + Accounts []Account `json:"accounts"` +} + +// Worker represents a Cloudflare Worker deployment +type Worker struct { + Name string `json:"name"` + URL string `json:"url"` + CreatedAt string `json:"created_at"` + ID string `json:"id"` + AccountID string `json:"account_id"` + ConfigAccountName string `json:"config_account_name,omitempty"` +} + +// Analytics represents worker analytics data +type Analytics struct { + Success bool `json:"success"` + TotalRequests int `json:"total_requests"` + PerWorker map[string]int `json:"per_worker"` + Limit int `json:"limit"` + Error string `json:"error,omitempty"` +} + +// ==================================================================== +// CLOUDFLARE API CLIENT +// ==================================================================== + +type CloudflareClient struct { + APIToken string + AccountID string + BaseURL string + Headers map[string]string + subdomain string +} + +func NewCloudflareClient(apiToken, accountID string) *CloudflareClient { + return &CloudflareClient{ + APIToken: apiToken, + AccountID: accountID, + BaseURL: CloudflareBaseURL, + Headers: map[string]string{ + "Authorization": "Bearer " + apiToken, + "Content-Type": "application/json", + }, + } +} + +func (c *CloudflareClient) GetSubdomain() (string, error) { + if c.subdomain != "" { + return c.subdomain, nil + } + + url := fmt.Sprintf("%s/accounts/%s/workers/subdomain", c.BaseURL, c.AccountID) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return "", err + } + + for k, v := range c.Headers { + req.Header.Set(k, v) + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + c.subdomain = strings.ToLower(c.AccountID) + return c.subdomain, nil + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + var result struct { + Result struct { + Subdomain string `json:"subdomain"` + } `json:"result"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err == nil { + if result.Result.Subdomain != "" { + c.subdomain = result.Result.Subdomain + return c.subdomain, nil + } + } + } + + c.subdomain = strings.ToLower(c.AccountID) + return c.subdomain, nil +} + +func (c *CloudflareClient) CreateWorker(name string) (*Worker, error) { + if name == "" { + name = generateWorkerName() + } + + url := fmt.Sprintf("%s/accounts/%s/workers/scripts/%s", c.BaseURL, c.AccountID, name) + + var b bytes.Buffer + writer := multipartWriter(&b) + + metadata := map[string]string{ + "body_part": "script", + "main_module": "worker.js", + } + metadataJSON, _ := json.Marshal(metadata) + + writer.WriteField("metadata", string(metadataJSON)) + writer.WriteField("script", WorkerScript) + contentType := writer.FormDataContentType() + writer.Close() + + req, err := http.NewRequest("PUT", url, &b) + if err != nil { + return nil, err + } + + req.Header.Set("Authorization", "Bearer "+c.APIToken) + req.Header.Set("Content-Type", contentType) + + client := &http.Client{Timeout: 60 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("failed to create worker: %s", string(body)) + } + + // Enable subdomain + subdomain, _ := c.GetSubdomain() + subdomainURL := fmt.Sprintf("%s/accounts/%s/workers/scripts/%s/subdomain", c.BaseURL, c.AccountID, name) + subdomainBody := bytes.NewBufferString(`{"enabled":true}`) + subdomainReq, _ := http.NewRequest("POST", subdomainURL, subdomainBody) + for k, v := range c.Headers { + subdomainReq.Header.Set(k, v) + } + client.Do(subdomainReq) + + workerURL := fmt.Sprintf("https://%s.%s.workers.dev", name, subdomain) + + return &Worker{ + Name: name, + URL: workerURL, + CreatedAt: time.Now().Format("2006-01-02 15:04:05"), + ID: name, + AccountID: c.AccountID, + }, nil +} + +func (c *CloudflareClient) ListWorkers() ([]*Worker, error) { + url := fmt.Sprintf("%s/accounts/%s/workers/scripts", c.BaseURL, c.AccountID) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + + for k, v := range c.Headers { + req.Header.Set(k, v) + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result struct { + Result []struct { + ID string `json:"id"` + CreatedOn string `json:"created_on"` + } `json:"result"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + + subdomain, _ := c.GetSubdomain() + workers := []*Worker{} + + for _, script := range result.Result { + if strings.HasPrefix(script.ID, "flaretunnel-") { + workers = append(workers, &Worker{ + Name: script.ID, + URL: fmt.Sprintf("https://%s.%s.workers.dev", script.ID, subdomain), + CreatedAt: script.CreatedOn, + AccountID: c.AccountID, + }) + } + } + + return workers, nil +} + +func (c *CloudflareClient) DeleteWorker(name string) error { + url := fmt.Sprintf("%s/accounts/%s/workers/scripts/%s", c.BaseURL, c.AccountID, name) + req, err := http.NewRequest("DELETE", url, nil) + if err != nil { + return err + } + + for k, v := range c.Headers { + req.Header.Set(k, v) + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 && resp.StatusCode != 404 { + return fmt.Errorf("failed to delete worker: status %d", resp.StatusCode) + } + + return nil +} + +func (c *CloudflareClient) GetAnalytics() (*Analytics, error) { + graphqlURL := "https://api.cloudflare.com/client/v4/graphql" + + now := time.Now() + dateStart := now.Add(-24 * time.Hour).Format("2006-01-02T15:04:05Z") + dateEnd := now.Format("2006-01-02T15:04:05Z") + + query := ` + query WorkersAnalytics($accountTag: string!, $datetimeStart: string!, $datetimeEnd: string!) { + viewer { + accounts(filter: {accountTag: $accountTag}) { + workersInvocationsAdaptive( + limit: 10000 + filter: { + datetime_geq: $datetimeStart + datetime_leq: $datetimeEnd + } + orderBy: [sum_requests_DESC] + ) { + dimensions { + scriptName + } + sum { + requests + errors + subrequests + } + } + } + } + } + ` + + variables := map[string]interface{}{ + "accountTag": c.AccountID, + "datetimeStart": dateStart, + "datetimeEnd": dateEnd, + } + + payload := map[string]interface{}{ + "query": query, + "variables": variables, + } + + payloadBytes, _ := json.Marshal(payload) + req, err := http.NewRequest("POST", graphqlURL, bytes.NewBuffer(payloadBytes)) + if err != nil { + return nil, err + } + + for k, v := range c.Headers { + req.Header.Set(k, v) + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return &Analytics{Success: false, Limit: 100000, PerWorker: make(map[string]int)}, nil + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + return &Analytics{Success: false, Limit: 100000, PerWorker: make(map[string]int)}, nil + } + + var result struct { + Data struct { + Viewer struct { + Accounts []struct { + WorkersInvocationsAdaptive []struct { + Dimensions struct { + ScriptName string `json:"scriptName"` + } `json:"dimensions"` + Sum struct { + Requests int `json:"requests"` + } `json:"sum"` + } `json:"workersInvocationsAdaptive"` + } `json:"accounts"` + } `json:"viewer"` + } `json:"data"` + } + + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return &Analytics{Success: false, Limit: 100000, PerWorker: make(map[string]int)}, nil + } + + analytics := &Analytics{ + Success: true, + Limit: 100000, + PerWorker: make(map[string]int), + } + + if len(result.Data.Viewer.Accounts) > 0 { + for _, inv := range result.Data.Viewer.Accounts[0].WorkersInvocationsAdaptive { + scriptName := inv.Dimensions.ScriptName + requests := inv.Sum.Requests + analytics.PerWorker[scriptName] = requests + analytics.TotalRequests += requests + } + } + + return analytics, nil +} + +// ==================================================================== +// SSL CERTIFICATE GENERATION +// ==================================================================== + +func generateCACert(certPath, keyPath string) error { + if _, err := os.Stat(certPath); err == nil { + if _, err := os.Stat(keyPath); err == nil { + return nil + } + } + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return err + } + + notBefore := time.Now() + notAfter := notBefore.Add(3650 * 24 * time.Hour) + + serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Country: []string{"US"}, + Province: []string{"CA"}, + Locality: []string{"Local"}, + Organization: []string{"FlareTunnel"}, + CommonName: "FlareTunnel CA", + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + if err != nil { + return err + } + + certOut, err := os.Create(certPath) + if err != nil { + return err + } + pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + certOut.Close() + + keyOut, err := os.Create(keyPath) + if err != nil { + return err + } + pem.Encode(keyOut, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + keyOut.Close() + + fmt.Printf("āœ“ Generated CA certificate: %s\n", certPath) + return nil +} + +func generateHostCert(hostname, caCertPath, caKeyPath string) (*tls.Certificate, error) { + caCertPEM, err := os.ReadFile(caCertPath) + if err != nil { + return nil, err + } + + block, _ := pem.Decode(caCertPEM) + caCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + + caKeyPEM, err := os.ReadFile(caKeyPath) + if err != nil { + return nil, err + } + + keyBlock, _ := pem.Decode(caKeyPEM) + caKey, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes) + if err != nil { + return nil, err + } + + priv, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, err + } + + notBefore := time.Now() + notAfter := notBefore.Add(365 * 24 * time.Hour) + + serialNumber, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Country: []string{"US"}, + Province: []string{"CA"}, + Locality: []string{"Local"}, + Organization: []string{"FlareTunnel"}, + CommonName: hostname, + }, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + DNSNames: []string{hostname, "*." + hostname}, + } + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, &priv.PublicKey, caKey) + if err != nil { + return nil, err + } + + certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}) + + cert, err := tls.X509KeyPair(certPEM, keyPEM) + if err != nil { + return nil, err + } + + return &cert, nil +} + +// ==================================================================== +// FLARETUNNEL MANAGER +// ==================================================================== + +type FlareTunnel struct { + Config *Config + Clients map[string]*CloudflareClient + EndpointsFile string + ConfigFile string + workers []*Worker + workersMutex sync.RWMutex +} + +func NewFlareTunnel(configFile string) (*FlareTunnel, error) { + if configFile == "" { + configFile = "flaretunnel.json" + } + + config, err := loadConfig(configFile) + if err != nil { + return nil, err + } + + ft := &FlareTunnel{ + Config: config, + Clients: make(map[string]*CloudflareClient), + EndpointsFile: "flaretunnel_endpoints.json", + ConfigFile: configFile, + } + + for _, account := range config.Accounts { + ft.Clients[account.Name] = NewCloudflareClient(account.APIToken, account.AccountID) + } + + return ft, nil +} + +func loadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + // Remove BOM if present (UTF-8-sig compatibility) + if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF { + data = data[3:] + } + + var config Config + if err := json.Unmarshal(data, &config); err != nil { + return nil, err + } + + return &config, nil +} + +func (ft *FlareTunnel) SaveEndpoints(workers []*Worker) error { + data, err := json.MarshalIndent(workers, "", " ") + if err != nil { + return err + } + + return os.WriteFile(ft.EndpointsFile, data, 0644) +} + +func (ft *FlareTunnel) LoadEndpoints() ([]*Worker, error) { + data, err := os.ReadFile(ft.EndpointsFile) + if err != nil { + return nil, err + } + + // Remove BOM if present + if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF { + data = data[3:] + } + + var workers []*Worker + if err := json.Unmarshal(data, &workers); err != nil { + return nil, err + } + + return workers, nil +} + +func (ft *FlareTunnel) SyncEndpoints() ([]*Worker, error) { + allWorkers := []*Worker{} + + for accountName, client := range ft.Clients { + workers, err := client.ListWorkers() + if err != nil { + continue + } + + for _, w := range workers { + w.ConfigAccountName = accountName + allWorkers = append(allWorkers, w) + } + } + + if len(allWorkers) > 0 { + ft.SaveEndpoints(allWorkers) + } + + return allWorkers, nil +} + +func (ft *FlareTunnel) CreateWorkers(count int, accountName string, distribute bool) error { + fmt.Printf("\nCreating %d FlareTunnel endpoint(s)...\n", count) + + created := []*Worker{} + + if accountName != "" { + // Single account + client, ok := ft.Clients[accountName] + if !ok { + return fmt.Errorf("account '%s' not found", accountName) + } + + fmt.Printf(" Using account: %s\n", accountName) + + for i := 0; i < count; i++ { + worker, err := client.CreateWorker("") + if err != nil { + fmt.Printf(" [%d/%d] Failed: %v\n", i+1, count, err) + continue + } + worker.ConfigAccountName = accountName + created = append(created, worker) + fmt.Printf(" [%d/%d] %s -> %s\n", i+1, count, worker.Name, worker.URL) + } + } else if distribute && len(ft.Clients) > 1 { + // Distribute across accounts + fmt.Printf(" Distribution mode: Checking quotas across %d account(s)...\n", len(ft.Clients)) + + accountQuotas := make(map[string]int) + for name, client := range ft.Clients { + analytics, _ := client.GetAnalytics() + remaining := 100000 - analytics.TotalRequests + accountQuotas[name] = remaining + fmt.Printf(" %s: %d requests remaining\n", name, remaining) + } + + totalQuota := 0 + for _, quota := range accountQuotas { + totalQuota += quota + } + + if totalQuota == 0 { + totalQuota = len(ft.Clients) + for name := range accountQuotas { + accountQuotas[name] = 1 + } + } + + workersPerAccount := make(map[string]int) + for name, quota := range accountQuotas { + proportion := float64(quota) / float64(totalQuota) + workersPerAccount[name] = max(1, int(float64(count)*proportion)) + } + + // Adjust to exact count + for sum := sumMap(workersPerAccount); sum < count; sum = sumMap(workersPerAccount) { + maxAccount := "" + maxQuota := 0 + for name, quota := range accountQuotas { + if quota > maxQuota { + maxQuota = quota + maxAccount = name + } + } + workersPerAccount[maxAccount]++ + } + + fmt.Printf("\n Distribution plan:\n") + for name, wc := range workersPerAccount { + fmt.Printf(" %s: %d worker(s)\n", name, wc) + } + fmt.Println() + + createdCount := 0 + for name, wc := range workersPerAccount { + client := ft.Clients[name] + for i := 0; i < wc; i++ { + worker, err := client.CreateWorker("") + if err != nil { + fmt.Printf(" [%d/%d] [%s] Failed: %v\n", createdCount+1, count, name, err) + continue + } + worker.ConfigAccountName = name + created = append(created, worker) + createdCount++ + fmt.Printf(" [%d/%d] [%s] %s -> %s\n", createdCount, count, name, worker.Name, worker.URL) + } + } + } else { + // Default to first account + var firstClient *CloudflareClient + var firstName string + for name, client := range ft.Clients { + firstClient = client + firstName = name + break + } + + fmt.Printf(" Using account: %s\n", firstName) + + for i := 0; i < count; i++ { + worker, err := firstClient.CreateWorker("") + if err != nil { + fmt.Printf(" [%d/%d] Failed: %v\n", i+1, count, err) + continue + } + worker.ConfigAccountName = firstName + created = append(created, worker) + fmt.Printf(" [%d/%d] %s -> %s\n", i+1, count, worker.Name, worker.URL) + } + } + + ft.SyncEndpoints() + fmt.Printf("\nCreated: %d, Failed: %d\n", len(created), count-len(created)) + + return nil +} + +func (ft *FlareTunnel) ListWorkers(verbose, checkStatus bool) error { + workers, err := ft.SyncEndpoints() + if err != nil || len(workers) == 0 { + fmt.Println("āŒ No FlareTunnel endpoints found") + fmt.Println("šŸ’” Create some with: go run FlareTunnel.go create --count 5") + return nil + } + + // Verbose mode: check status as well + if verbose { + checkStatus = true // Force status check in verbose mode! + } + + // Get analytics for all accounts + allAnalytics := make(map[string]*Analytics) + for accountName, client := range ft.Clients { + analytics, _ := client.GetAnalytics() + allAnalytics[accountName] = analytics + } + + if checkStatus { + fmt.Printf("\nšŸ” Checking status of %d worker(s)...\n", len(workers)) + fmt.Println("This may take a few seconds...\n") + } + + fmt.Println() + table := tablewriter.NewWriter(os.Stdout) + + // Build header based on flags + header := []string{"#", "Account", "Name"} + if verbose { + header = append(header, "Created", "Age") + } + header = append(header, "URL", "Requests") + if checkStatus { + header = append(header, "Status (ms)") + } else { + header = append(header, "Status") + } + table.SetHeader(header) + table.SetBorder(true) + + for idx, worker := range workers { + row := []string{strconv.Itoa(idx), worker.ConfigAccountName, worker.Name} + + // Add verbose info (Created, Age) + if verbose { + createdStr := "Unknown" + ageStr := "Unknown" + + // Extract timestamp from worker name: flaretunnel-1764721536-kmoiok + re := regexp.MustCompile(`flaretunnel-(\d+)-`) + if matches := re.FindStringSubmatch(worker.Name); len(matches) > 1 { + if timestamp, err := strconv.ParseInt(matches[1], 10, 64); err == nil { + createdTime := time.Unix(timestamp, 0) + createdStr = createdTime.Format("2006-01-02 15:04:05") + + // Calculate age + age := time.Since(createdTime) + if age.Hours() < 1 { + ageStr = fmt.Sprintf("%dm ago", int(age.Minutes())) + } else if age.Hours() < 24 { + ageStr = fmt.Sprintf("%dh ago", int(age.Hours())) + } else { + ageStr = fmt.Sprintf("%dd ago", int(age.Hours()/24)) + } + } + } + + row = append(row, createdStr, ageStr) + } + + // Add URL + row = append(row, worker.URL) + + // Add requests count + requests := "0" + if analytics, ok := allAnalytics[worker.ConfigAccountName]; ok && analytics.Success { + if req, ok := analytics.PerWorker[worker.Name]; ok { + requests = strconv.Itoa(req) + } + } + row = append(row, requests) + + // Add status + if checkStatus { + // Test actual worker + testURL := worker.URL + "?url=" + url.QueryEscape("https://httpbin.org/status/200") + start := time.Now() + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(testURL) + elapsed := time.Since(start) + + if err != nil { + row = append(row, "āŒ Failed") + } else { + resp.Body.Close() + if resp.StatusCode == 200 { + row = append(row, fmt.Sprintf("āœ… %dms", elapsed.Milliseconds())) + } else { + row = append(row, fmt.Sprintf("āš ļø %d", resp.StatusCode)) + } + } + } else { + row = append(row, "āœ… Active") + } + + table.Append(row) + } + + table.Render() + + fmt.Println("\nšŸ’” Use worker index with: go run FlareTunnel.go tunnel --workers 0,1,2 --verbose") + fmt.Println() + + return nil +} + +func (ft *FlareTunnel) CleanupWorkers(accountName string) error { + if accountName != "" { + client, ok := ft.Clients[accountName] + if !ok { + return fmt.Errorf("account '%s' not found", accountName) + } + + fmt.Printf("\nšŸ—‘ļø Cleaning up account: %s\n", accountName) + workers, _ := client.ListWorkers() + + for _, worker := range workers { + if err := client.DeleteWorker(worker.Name); err != nil { + fmt.Printf(" āœ— Failed to delete: %s\n", worker.Name) + } else { + fmt.Printf(" āœ“ Deleted: %s\n", worker.Name) + } + } + } else { + fmt.Printf("\nšŸ—‘ļø Cleaning up ALL accounts (%d total)\n", len(ft.Clients)) + + for name, client := range ft.Clients { + fmt.Printf(" Account: %s\n", name) + workers, _ := client.ListWorkers() + + for _, worker := range workers { + if err := client.DeleteWorker(worker.Name); err != nil { + fmt.Printf(" āœ— Failed to delete: %s\n", worker.Name) + } else { + fmt.Printf(" āœ“ Deleted: %s\n", worker.Name) + } + } + } + + os.Remove(ft.EndpointsFile) + } + + return nil +} + +func (ft *FlareTunnel) TestWorkers(targetURL, method string) error { + workers, err := ft.LoadEndpoints() + if err != nil || len(workers) == 0 { + fmt.Println("āŒ No workers available") + return fmt.Errorf("no workers found") + } + + fmt.Printf("\nTesting %d FlareTunnel endpoint(s) with %s\n", len(workers), targetURL) + + successCount := 0 + uniqueIPs := make(map[string]bool) + + for _, worker := range workers { + fmt.Printf("\nTesting endpoint: %s\n", worker.Name) + + testURL := worker.URL + "?url=" + url.QueryEscape(targetURL) + + client := &http.Client{Timeout: 30 * time.Second} + req, err := http.NewRequest(method, testURL, nil) + if err != nil { + fmt.Printf(" āœ— Request failed: %v\n", err) + continue + } + + resp, err := client.Do(req) + if err != nil { + fmt.Printf(" āœ— Request failed: %v\n", err) + continue + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + successCount++ + fmt.Printf(" āœ“ Request successful! Status: %d\n", resp.StatusCode) + + body, _ := io.ReadAll(resp.Body) + bodyStr := strings.TrimSpace(string(body)) + + // Try to extract IP from common formats + if strings.Contains(targetURL, "ifconfig.me") { + fmt.Printf(" Origin IP: %s\n", bodyStr) + uniqueIPs[bodyStr] = true + } else if strings.Contains(targetURL, "httpbin.org/ip") { + var data map[string]interface{} + if json.Unmarshal(body, &data) == nil { + if origin, ok := data["origin"].(string); ok { + fmt.Printf(" Origin IP: %s\n", origin) + uniqueIPs[origin] = true + } + } + } else { + fmt.Printf(" Response Length: %d bytes\n", len(body)) + } + } else { + fmt.Printf(" āœ— Request failed! Status: %d\n", resp.StatusCode) + } + } + + fmt.Printf("\nTest Results:\n") + fmt.Printf(" Working endpoints: %d/%d\n", successCount, len(workers)) + if len(uniqueIPs) > 0 { + fmt.Printf(" Unique IP addresses: %d\n", len(uniqueIPs)) + for ip := range uniqueIPs { + fmt.Printf(" - %s\n", ip) + } + } + + return nil +} + +func (ft *FlareTunnel) ExportConfig(outputFile string) error { + if _, err := os.Stat("flaretunnel.json"); os.IsNotExist(err) { + fmt.Println("āŒ No configuration file found (flaretunnel.json)") + return err + } + + configData, err := os.ReadFile("flaretunnel.json") + if err != nil { + fmt.Printf("āŒ Failed to read configuration: %v\n", err) + return err + } + + exportData := map[string]interface{}{ + "exported_at": time.Now().Format(time.RFC3339), + "config": json.RawMessage(configData), + } + + data, err := json.MarshalIndent(exportData, "", " ") + if err != nil { + return err + } + + if err := os.WriteFile(outputFile, data, 0644); err != nil { + fmt.Printf("āŒ Failed to write export file: %v\n", err) + return err + } + + var config Config + json.Unmarshal(configData, &config) + + fmt.Printf("āœ… Exported configuration to %s\n", outputFile) + fmt.Println("\nšŸ“Š Summary:") + fmt.Printf(" Accounts: %d\n", len(config.Accounts)) + for _, acc := range config.Accounts { + fmt.Printf(" • %s: %s...\n", acc.Name, acc.AccountID[:min(8, len(acc.AccountID))]) + } + fmt.Println("\nāš ļø WARNING: This file contains API tokens! Keep it secure! šŸ”’") + + return nil +} + +func (ft *FlareTunnel) ImportConfig(inputFile string, merge bool) error { + if _, err := os.Stat(inputFile); os.IsNotExist(err) { + fmt.Printf("āŒ Import file not found: %s\n", inputFile) + return err + } + + importData, err := os.ReadFile(inputFile) + if err != nil { + fmt.Printf("āŒ Failed to read import file: %v\n", err) + return err + } + + // Remove BOM if present + if len(importData) >= 3 && importData[0] == 0xEF && importData[1] == 0xBB && importData[2] == 0xBF { + importData = importData[3:] + } + + var importWrapper map[string]interface{} + var newConfig Config + + if err := json.Unmarshal(importData, &importWrapper); err != nil { + fmt.Printf("āŒ Failed to parse import file: %v\n", err) + return err + } + + // Check if it's an export format or direct config + if configData, ok := importWrapper["config"]; ok { + configBytes, _ := json.Marshal(configData) + json.Unmarshal(configBytes, &newConfig) + } else { + json.Unmarshal(importData, &newConfig) + } + + if len(newConfig.Accounts) == 0 { + fmt.Println("āŒ No accounts found in import file") + return fmt.Errorf("no accounts in import") + } + + fmt.Printf("šŸ“„ Import file: %s\n", inputFile) + fmt.Printf(" Accounts: %d\n\n", len(newConfig.Accounts)) + + for idx, acc := range newConfig.Accounts { + fmt.Printf(" %d. %s: %s...\n", idx+1, acc.Name, acc.AccountID[:min(8, len(acc.AccountID))]) + } + + var finalConfig Config + + if merge && fileExists("flaretunnel.json") { + existingData, _ := os.ReadFile("flaretunnel.json") + json.Unmarshal(existingData, &finalConfig) + + existingNames := make(map[string]bool) + for _, acc := range finalConfig.Accounts { + existingNames[acc.Name] = true + } + + for _, acc := range newConfig.Accounts { + if !existingNames[acc.Name] { + finalConfig.Accounts = append(finalConfig.Accounts, acc) + fmt.Printf("āž• Added account: %s\n", acc.Name) + } else { + fmt.Printf("āš ļø Skipped duplicate: %s\n", acc.Name) + } + } + } else { + finalConfig = newConfig + } + + reader := bufio.NewReader(os.Stdin) + fmt.Print("\nConfirm import? (y/N): ") + confirm, _ := reader.ReadString('\n') + + if strings.ToLower(strings.TrimSpace(confirm)) != "y" { + fmt.Println("Import cancelled.") + return nil + } + + data, _ := json.MarshalIndent(finalConfig, "", " ") + if err := os.WriteFile("flaretunnel.json", data, 0644); err != nil { + fmt.Printf("āŒ Failed to save configuration: %v\n", err) + return err + } + + fmt.Println("\nāœ… Configuration imported!") + fmt.Printf("šŸ“Š Total accounts: %d\n", len(finalConfig.Accounts)) + + return nil +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func fileExists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +// ==================================================================== +// PROXY SERVER +// ==================================================================== + +type ProxyServer struct { + Host string + Port int + Workers []*Worker + CurrentWorkerIndex int + RotationMode string + Verbose bool + AllowIPAccess bool + CACertPath string + CAKeyPath string + BlacklistPatterns []string + InlineBlockPatterns []string + BlacklistStats map[string]int + UpstreamProxy string + UpstreamVerifySSL bool + CacheCerts bool + NoSSLIntercept bool + mutex sync.Mutex + certCache map[string]*tls.Certificate + certMutex sync.RWMutex +} + +func NewProxyServer(host string, port int) *ProxyServer { + return &ProxyServer{ + Host: host, + Port: port, + RotationMode: "round-robin", + BlacklistStats: make(map[string]int), + certCache: make(map[string]*tls.Certificate), + } +} + +func (ps *ProxyServer) LoadWorkers(endpointsFile string, workerIndices []int) error { + data, err := os.ReadFile(endpointsFile) + if err != nil { + return err + } + + var allWorkers []*Worker + if err := json.Unmarshal(data, &allWorkers); err != nil { + return err + } + + if len(workerIndices) > 0 { + selected := []*Worker{} + for _, idx := range workerIndices { + if idx >= 0 && idx < len(allWorkers) { + selected = append(selected, allWorkers[idx]) + } + } + ps.Workers = selected + } else { + ps.Workers = allWorkers + } + + return nil +} + +func (ps *ProxyServer) LoadBlacklist(blacklistFile string) error { + patterns := []string{} + + // Load from file + if blacklistFile != "" { + if _, err := os.Stat(blacklistFile); err == nil { + file, err := os.Open(blacklistFile) + if err == nil { + defer file.Close() + scanner := bufio.NewScanner(file) + + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if line != "" && !strings.HasPrefix(line, "#") { + patterns = append(patterns, line) + } + } + + if len(patterns) > 0 { + fmt.Printf("āœ… Loaded %d patterns from %s\n", len(patterns), blacklistFile) + } + } + } + } + + // Add inline patterns + if len(ps.InlineBlockPatterns) > 0 { + patterns = append(patterns, ps.InlineBlockPatterns...) + fmt.Printf("āœ… Added %d inline patterns\n", len(ps.InlineBlockPatterns)) + } + + ps.BlacklistPatterns = patterns + + return nil +} + +func (ps *ProxyServer) GetWorkerURL() string { + if len(ps.Workers) == 0 { + return "" + } + + ps.mutex.Lock() + defer ps.mutex.Unlock() + + if ps.RotationMode == "random" { + return ps.Workers[time.Now().UnixNano()%int64(len(ps.Workers))].URL + } else if ps.RotationMode == "round-robin" { + worker := ps.Workers[ps.CurrentWorkerIndex] + ps.CurrentWorkerIndex = (ps.CurrentWorkerIndex + 1) % len(ps.Workers) + return worker.URL + } + + return ps.Workers[0].URL +} + +func (ps *ProxyServer) IsBlacklisted(targetURL string) bool { + if len(ps.BlacklistPatterns) == 0 { + return false + } + + urlLower := strings.ToLower(targetURL) + parsedURL, _ := url.Parse(targetURL) + hostname := "" + path := "" + + if parsedURL != nil { + hostname = strings.ToLower(parsedURL.Hostname()) + path = strings.ToLower(parsedURL.Path) + } + + for _, pattern := range ps.BlacklistPatterns { + patternLower := strings.ToLower(pattern) + if strings.Contains(hostname, patternLower) || + strings.Contains(path, patternLower) || + strings.Contains(urlLower, patternLower) || + strings.HasSuffix(urlLower, patternLower) { + + ps.mutex.Lock() + ps.BlacklistStats[pattern]++ + ps.mutex.Unlock() + + return true + } + } + + return false +} + +func (ps *ProxyServer) HandleHTTP(w http.ResponseWriter, r *http.Request) { + targetURL := r.URL.String() + if !strings.HasPrefix(targetURL, "http://") && !strings.HasPrefix(targetURL, "https://") { + targetURL = "http://" + r.Host + r.URL.String() + } + + if ps.IsBlacklisted(targetURL) { + if ps.Verbose { + fmt.Printf("🚫 BLOCKED (blacklist): %s\n", targetURL) + } + http.Error(w, "Blacklisted", http.StatusForbidden) + return + } + + parsedURL, _ := url.Parse(targetURL) + if !ps.AllowIPAccess && isIPAddress(parsedURL.Hostname()) { + if ps.Verbose { + fmt.Printf("šŸ›”ļø BLOCKED (IP): %s\n", parsedURL.Hostname()) + } + http.Error(w, "Direct IP access blocked", http.StatusForbidden) + return + } + + workerURL := ps.GetWorkerURL() + if workerURL == "" { + http.Error(w, "No workers available", http.StatusServiceUnavailable) + return + } + + proxyURL := workerURL + "?url=" + url.QueryEscape(targetURL) + + if ps.Verbose { + fmt.Printf("\nšŸ“¤ [%s] %s\n", r.Method, targetURL) + fmt.Printf(" ↓ via Worker: %s\n", workerURL) + } + + // Create proxy request + proxyReq, err := http.NewRequest(r.Method, proxyURL, r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // Copy headers + for k, v := range r.Header { + if !strings.EqualFold(k, "Host") && !strings.EqualFold(k, "Connection") { + proxyReq.Header[k] = v + } + } + + // Send request with optional upstream proxy + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: !ps.UpstreamVerifySSL}, + } + + if ps.UpstreamProxy != "" { + proxyURL, err := url.Parse(ps.UpstreamProxy) + if err == nil { + transport.Proxy = http.ProxyURL(proxyURL) + } + } + + client := &http.Client{ + Timeout: 30 * time.Second, + Transport: transport, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + resp, err := client.Do(proxyReq) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + defer resp.Body.Close() + + // Copy response headers + for k, v := range resp.Header { + w.Header()[k] = v + } + + w.WriteHeader(resp.StatusCode) + io.Copy(w, resp.Body) + + if ps.Verbose { + status := "āœ…" + if resp.StatusCode >= 400 { + status = "āš ļø" + } + fmt.Printf(" ↑ %s %d\n", status, resp.StatusCode) + } +} + +func (ps *ProxyServer) HandleCONNECT(w http.ResponseWriter, r *http.Request) { + if ps.CACertPath == "" { + http.Error(w, "HTTPS not supported", http.StatusNotImplemented) + return + } + + host := r.Host + hostname := strings.Split(host, ":")[0] + + if ps.Verbose { + fmt.Printf("\nšŸ”’ [CONNECT] %s\n", host) + } + + // Get or generate certificate + ps.certMutex.RLock() + cert, exists := ps.certCache[hostname] + ps.certMutex.RUnlock() + + if !exists { + newCert, err := generateHostCert(hostname, ps.CACertPath, ps.CAKeyPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + cert = newCert + + ps.certMutex.Lock() + ps.certCache[hostname] = cert + ps.certMutex.Unlock() + } + + // Send 200 Connection Established + w.WriteHeader(http.StatusOK) + + // Hijack connection + hijacker, ok := w.(http.Hijacker) + if !ok { + http.Error(w, "Hijacking not supported", http.StatusInternalServerError) + return + } + + clientConn, _, err := hijacker.Hijack() + if err != nil { + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return + } + defer clientConn.Close() + + // Wrap with TLS + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{*cert}, + } + + tlsConn := tls.Server(clientConn, tlsConfig) + defer tlsConn.Close() + + if err := tlsConn.Handshake(); err != nil { + if ps.Verbose { + fmt.Printf("āœ— SSL handshake failed: %v\n", err) + } + return + } + + // Read HTTPS request + reader := bufio.NewReader(tlsConn) + req, err := http.ReadRequest(reader) + if err != nil { + return + } + + targetURL := "https://" + hostname + req.URL.String() + + if ps.IsBlacklisted(targetURL) { + if ps.Verbose { + fmt.Printf(" 🚫 BLOCKED (blacklist): %s\n", targetURL) + } + return + } + + workerURL := ps.GetWorkerURL() + if workerURL == "" { + return + } + + proxyURL := workerURL + "?url=" + url.QueryEscape(targetURL) + + if ps.Verbose { + fmt.Printf(" šŸ“¤ [%s] %s\n", req.Method, targetURL) + fmt.Printf(" ↓ via Worker: %s\n", workerURL) + } + + // Create proxy request + proxyReq, err := http.NewRequest(req.Method, proxyURL, req.Body) + if err != nil { + return + } + + for k, v := range req.Header { + if !strings.EqualFold(k, "Host") && !strings.EqualFold(k, "Connection") { + proxyReq.Header[k] = v + } + } + + // Setup transport with optional upstream proxy + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: !ps.UpstreamVerifySSL}, + } + + if ps.UpstreamProxy != "" { + upstreamURL, err := url.Parse(ps.UpstreamProxy) + if err == nil { + transport.Proxy = http.ProxyURL(upstreamURL) + } + } + + client := &http.Client{ + Timeout: 30 * time.Second, + Transport: transport, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + } + + resp, err := client.Do(proxyReq) + if err != nil { + return + } + defer resp.Body.Close() + + // Write response + tlsConn.Write([]byte(fmt.Sprintf("HTTP/1.1 %d %s\r\n", resp.StatusCode, resp.Status))) + + for k, v := range resp.Header { + for _, vv := range v { + tlsConn.Write([]byte(fmt.Sprintf("%s: %s\r\n", k, vv))) + } + } + + tlsConn.Write([]byte("\r\n")) + io.Copy(tlsConn, resp.Body) + + if ps.Verbose { + status := "āœ…" + if resp.StatusCode >= 400 { + status = "āš ļø" + } + fmt.Printf(" ↑ %s %d\n", status, resp.StatusCode) + } +} + +func (ps *ProxyServer) Start(blacklistFile string) error { + // Setup SSL + ps.CACertPath = "flaretunnel_ca.crt" + ps.CAKeyPath = "flaretunnel_ca.key" + + if !ps.NoSSLIntercept { + if err := generateCACert(ps.CACertPath, ps.CAKeyPath); err != nil { + fmt.Printf("āš ļø SSL setup failed: %v\n", err) + ps.CACertPath = "" + } + } else { + ps.CACertPath = "" + } + + // Load blacklist + ps.LoadBlacklist(blacklistFile) + + fmt.Println("\n" + strings.Repeat("=", 80)) + fmt.Println("šŸš€ FlareTunnel Tunnel Server Started") + fmt.Println(strings.Repeat("=", 80)) + fmt.Printf("šŸ“” Listening: %s:%d\n", ps.Host, ps.Port) + fmt.Printf("āš™ļø Workers: %d\n", len(ps.Workers)) + fmt.Printf("šŸ”„ Rotation: %s\n", ps.RotationMode) + + if ps.NoSSLIntercept { + fmt.Printf("šŸ”’ SSL/HTTPS: āœ— Disabled by --no-ssl-intercept\n") + } else if ps.CACertPath != "" { + fmt.Printf("šŸ”’ SSL/HTTPS: āœ“ Enabled (HTTPS CONNECT supported)\n") + } else { + fmt.Printf("šŸ”’ SSL/HTTPS: āœ— Disabled\n") + } + + if ps.AllowIPAccess { + fmt.Printf("šŸ›”ļø IP Blocking: āŒ Disabled (--unsafe)\n") + } else { + fmt.Printf("šŸ›”ļø IP Blocking: āœ… Enabled (saves Worker requests)\n") + } + + blacklistMsg := fmt.Sprintf("🚫 Blacklist: %d pattern(s)", len(ps.BlacklistPatterns)) + if blacklistFile == "blacklist-minimal.txt" && len(ps.BlacklistPatterns) > 0 { + blacklistMsg += " āœ… (default: blacklist-minimal.txt)" + } + fmt.Println(blacklistMsg) + + if ps.UpstreamProxy != "" { + fmt.Printf("šŸ”— Upstream: %s\n", ps.UpstreamProxy) + } else { + fmt.Printf("šŸ”— Upstream: āŒ None (direct to Workers)\n") + } + + if ps.Verbose { + fmt.Printf("šŸ“ Verbose: āœ… Enabled\n") + } else { + fmt.Printf("šŸ“ Verbose: āŒ Disabled\n") + } + + fmt.Println("\nšŸ“‹ Selected Workers:") + for idx, worker := range ps.Workers { + workerName := worker.Name + if len(workerName) > 50 { + workerName = workerName[:47] + "..." + } + fmt.Printf(" [%d] %s\n", idx, workerName) + } + + if ps.UpstreamProxy != "" { + fmt.Println("\nšŸ”€ Request Flow:") + fmt.Printf(" Client → FlareTunnel:%d → %s → Cloudflare Workers → Target\n", ps.Port, ps.UpstreamProxy) + } + + fmt.Println("\nāš™ļø Proxy Configuration:") + fmt.Printf(" HTTP Proxy: %s:%d\n", ps.Host, ps.Port) + fmt.Printf(" HTTPS Proxy: %s:%d\n", ps.Host, ps.Port) + + if ps.CACertPath != "" { + fmt.Println("\nšŸ” For HTTPS without warnings, install CA certificate:") + fmt.Printf(" šŸ“„ File: %s\n", ps.CACertPath) + fmt.Printf(" šŸ’” Or use verify=False in code\n") + } + + fmt.Println("\nšŸ’” Tips:") + if len(ps.BlacklistPatterns) > 0 { + fmt.Println(" • Blacklist active - saving Worker requests! šŸ’°") + } else { + fmt.Println(" • No blacklist - use --blacklist blacklist-minimal.txt to save requests") + } + if !ps.AllowIPAccess { + fmt.Println(" • IP blocking active - Cloudflare doesn't support IPs anyway") + } + fmt.Println(" • Press Ctrl+C to stop") + fmt.Println(strings.Repeat("=", 80)) + fmt.Println() + + // Start server + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + ps.HandleCONNECT(w, r) + } else { + ps.HandleHTTP(w, r) + } + }) + + server := &http.Server{ + Addr: fmt.Sprintf("%s:%d", ps.Host, ps.Port), + Handler: handler, + } + + return server.ListenAndServe() +} + +// ==================================================================== +// UTILITY FUNCTIONS +// ==================================================================== + +func generateWorkerName() string { + timestamp := time.Now().Unix() + suffix := randomString(6) + return fmt.Sprintf("flaretunnel-%d-%s", timestamp, suffix) +} + +func randomString(n int) string { + const letters = "abcdefghijklmnopqrstuvwxyz" + b := make([]byte, n) + for i := range b { + b[i] = letters[time.Now().UnixNano()%int64(len(letters))] + time.Sleep(1 * time.Nanosecond) + } + return string(b) +} + +func isIPAddress(hostname string) bool { + host := strings.Split(hostname, ":")[0] + ip := net.ParseIP(host) + return ip != nil +} + +func testWorker(workerURL string) bool { + testURL := workerURL + "?url=" + url.QueryEscape("https://httpbin.org/status/200") + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Get(testURL) + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == 200 +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func sumMap(m map[string]int) int { + sum := 0 + for _, v := range m { + sum += v + } + return sum +} + +// Simple multipart writer +type simpleMultipartWriter struct { + buf *bytes.Buffer + boundary string +} + +func multipartWriter(buf *bytes.Buffer) *simpleMultipartWriter { + return &simpleMultipartWriter{ + buf: buf, + boundary: "----FlareTunnelBoundary" + randomString(16), + } +} + +func (w *simpleMultipartWriter) WriteField(field, value string) { + w.buf.WriteString("--" + w.boundary + "\r\n") + w.buf.WriteString(fmt.Sprintf("Content-Disposition: form-data; name=\"%s\"\r\n\r\n", field)) + w.buf.WriteString(value + "\r\n") +} + +func (w *simpleMultipartWriter) Close() { + w.buf.WriteString("--" + w.boundary + "--\r\n") +} + +func (w *simpleMultipartWriter) FormDataContentType() string { + return "multipart/form-data; boundary=" + w.boundary +} + +// ==================================================================== +// CLI COMMANDS +// ==================================================================== + +func setupConfig() error { + fmt.Println(strings.Repeat("=", 70)) + fmt.Println("šŸ”§ FlareTunnel Multi-Account Configuration") + fmt.Println(strings.Repeat("=", 70)) + fmt.Println() + fmt.Println("Getting Cloudflare Credentials:") + fmt.Println("1. Sign up at https://cloudflare.com") + fmt.Println("2. Go to https://dash.cloudflare.com/profile/api-tokens") + fmt.Println("3. Click Create Token - use 'Edit Cloudflare Workers' template") + fmt.Println("4. Set account and zone resources to all") + fmt.Println("5. Copy the token and Account ID") + fmt.Println() + fmt.Println("šŸ’” Tip: You can add multiple accounts for more quota!") + fmt.Println(" Each account = 100,000 requests/day") + fmt.Println() + + accounts := []Account{} + reader := bufio.NewReader(os.Stdin) + + for accountNum := 1; ; accountNum++ { + fmt.Printf("\nšŸ“‹ Account #%d\n", accountNum) + fmt.Println(strings.Repeat("-", 40)) + + fmt.Printf("Account name (e.g., 'main', 'backup') [account-%d]: ", accountNum) + accountName, _ := reader.ReadString('\n') + accountName = strings.TrimSpace(accountName) + if accountName == "" { + accountName = fmt.Sprintf("account-%d", accountNum) + } + + fmt.Print("API token: ") + apiToken, _ := reader.ReadString('\n') + apiToken = strings.TrimSpace(apiToken) + if apiToken == "" { + if accountNum == 1 { + fmt.Println("āŒ API token is required") + return fmt.Errorf("no API token provided") + } + break + } + + fmt.Print("Account ID: ") + accountID, _ := reader.ReadString('\n') + accountID = strings.TrimSpace(accountID) + if accountID == "" { + if accountNum == 1 { + fmt.Println("āŒ Account ID is required") + return fmt.Errorf("no account ID provided") + } + break + } + + accounts = append(accounts, Account{ + Name: accountName, + APIToken: apiToken, + AccountID: accountID, + }) + + fmt.Printf("āœ… Account '%s' added!\n", accountName) + + if accountNum >= 1 { + fmt.Print("\nAdd another account? (y/N): ") + addMore, _ := reader.ReadString('\n') + if strings.ToLower(strings.TrimSpace(addMore)) != "y" { + break + } + } + } + + if len(accounts) == 0 { + fmt.Println("āŒ No accounts configured") + return fmt.Errorf("no accounts configured") + } + + config := Config{Accounts: accounts} + data, _ := json.MarshalIndent(config, "", " ") + + if err := os.WriteFile("flaretunnel.json", data, 0644); err != nil { + fmt.Printf("āŒ Error saving configuration: %v\n", err) + return err + } + + fmt.Println() + fmt.Println(strings.Repeat("=", 70)) + fmt.Println("āœ… Configuration saved!") + fmt.Println(strings.Repeat("=", 70)) + fmt.Println("šŸ“„ Config file: flaretunnel.json") + fmt.Printf("šŸ“Š Accounts configured: %d\n", len(accounts)) + fmt.Printf("šŸš€ Total daily quota: %d requests\n", len(accounts)*100000) + fmt.Println() + + for _, acc := range accounts { + fmt.Printf(" • %s: %s...\n", acc.Name, acc.AccountID[:8]) + } + + fmt.Println() + fmt.Println("šŸŽ‰ FlareTunnel is now configured!") + + return nil +} + +func parseWorkerIndices(indicesStr string) []int { + indices := []int{} + parts := strings.Split(indicesStr, ",") + + for _, part := range parts { + part = strings.TrimSpace(part) + if strings.Contains(part, "-") { + rangeParts := strings.Split(part, "-") + if len(rangeParts) == 2 { + start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0])) + end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1])) + if err1 == nil && err2 == nil { + for i := start; i <= end; i++ { + indices = append(indices, i) + } + } + } + } else { + if idx, err := strconv.Atoi(part); err == nil { + indices = append(indices, idx) + } + } + } + + return indices +} + +// ==================================================================== +// MAIN +// ==================================================================== + +func main() { + if len(os.Args) < 2 { + printHelp() + return + } + + command := os.Args[1] + + switch command { + case "config": + setupConfig() + + case "create": + count := 1 + accountName := "" + distribute := false + + for i := 2; i < len(os.Args); i++ { + switch os.Args[i] { + case "--count": + if i+1 < len(os.Args) { + count, _ = strconv.Atoi(os.Args[i+1]) + i++ + } + case "--account": + if i+1 < len(os.Args) { + accountName = os.Args[i+1] + i++ + } + case "--distribute": + distribute = true + } + } + + ft, err := NewFlareTunnel("flaretunnel.json") + if err != nil { + fmt.Printf("āŒ Configuration error: %v\n", err) + fmt.Println("Run: go run FlareTunnel.go config") + return + } + + ft.CreateWorkers(count, accountName, distribute) + + case "list": + verbose := false + checkStatus := false + + for i := 2; i < len(os.Args); i++ { + switch os.Args[i] { + case "--verbose", "-v": + verbose = true + case "--status": + checkStatus = true + } + } + + // If both flags provided, verbose already includes status check + if verbose && checkStatus { + fmt.Println("āš ļø Note: --verbose already includes live status check, --status flag is redundant\n") + // checkStatus is already covered by verbose, no need to set it + } + + ft, err := NewFlareTunnel("flaretunnel.json") + if err != nil { + fmt.Printf("āŒ Configuration error: %v\n", err) + return + } + + ft.ListWorkers(verbose, checkStatus) + + case "test": + targetURL := "https://ifconfig.me/ip" + method := "GET" + + for i := 2; i < len(os.Args); i++ { + switch os.Args[i] { + case "--url": + if i+1 < len(os.Args) { + targetURL = os.Args[i+1] + i++ + } + case "--method": + if i+1 < len(os.Args) { + method = os.Args[i+1] + i++ + } + } + } + + ft, err := NewFlareTunnel("flaretunnel.json") + if err != nil { + fmt.Printf("āŒ Configuration error: %v\n", err) + return + } + + ft.TestWorkers(targetURL, method) + + case "export": + outputFile := "flaretunnel_config_backup.json" + + for i := 2; i < len(os.Args); i++ { + switch os.Args[i] { + case "--output": + if i+1 < len(os.Args) { + outputFile = os.Args[i+1] + i++ + } + } + } + + ft, _ := NewFlareTunnel("flaretunnel.json") + ft.ExportConfig(outputFile) + + case "import": + inputFile := "" + merge := false + + for i := 2; i < len(os.Args); i++ { + switch os.Args[i] { + case "--input": + if i+1 < len(os.Args) { + inputFile = os.Args[i+1] + i++ + } + case "--merge": + merge = true + } + } + + if inputFile == "" { + fmt.Println("āŒ Error: --input required for import command") + fmt.Println("Usage: go run FlareTunnel.go import --input config_backup.json [--merge]") + return + } + + ft := &FlareTunnel{} + ft.ImportConfig(inputFile, merge) + + case "cleanup": + accountName := "" + + for i := 2; i < len(os.Args); i++ { + switch os.Args[i] { + case "--account": + if i+1 < len(os.Args) { + accountName = os.Args[i+1] + i++ + } + } + } + + ft, err := NewFlareTunnel("flaretunnel.json") + if err != nil { + fmt.Printf("āŒ Configuration error: %v\n", err) + return + } + + reader := bufio.NewReader(os.Stdin) + if accountName != "" { + fmt.Printf("Delete ALL workers from account '%s'? (y/N): ", accountName) + } else { + fmt.Print("Delete ALL FlareTunnel endpoints from ALL accounts? (y/N): ") + } + + confirm, _ := reader.ReadString('\n') + if strings.ToLower(strings.TrimSpace(confirm)) == "y" { + ft.CleanupWorkers(accountName) + } else { + fmt.Println("Cleanup cancelled.") + } + + case "tunnel": + host := "127.0.0.1" + port := 8080 + workersStr := "" + mode := "round-robin" + verbose := false + unsafe := false + blacklist := "blacklist-minimal.txt" + upstreamProxy := "" + upstreamVerifySSL := false + cacheCerts := false + noSSLIntercept := false + blockPatterns := []string{} + + for i := 2; i < len(os.Args); i++ { + switch os.Args[i] { + case "--host": + if i+1 < len(os.Args) { + host = os.Args[i+1] + i++ + } + case "--port": + if i+1 < len(os.Args) { + port, _ = strconv.Atoi(os.Args[i+1]) + i++ + } + case "--workers": + if i+1 < len(os.Args) { + workersStr = os.Args[i+1] + i++ + } + case "--mode": + if i+1 < len(os.Args) { + mode = os.Args[i+1] + i++ + } + case "--verbose", "-v": + verbose = true + case "--unsafe": + unsafe = true + case "--blacklist": + if i+1 < len(os.Args) { + blacklist = os.Args[i+1] + i++ + } + case "--upstream-proxy": + if i+1 < len(os.Args) { + upstreamProxy = os.Args[i+1] + i++ + } + case "--upstream-verify-ssl": + upstreamVerifySSL = true + case "--cache-certs": + cacheCerts = true + case "--no-ssl-intercept": + noSSLIntercept = true + case "--block": + if i+1 < len(os.Args) { + blockPatterns = append(blockPatterns, os.Args[i+1]) + i++ + } + } + } + + ps := NewProxyServer(host, port) + ps.RotationMode = mode + ps.Verbose = verbose + ps.AllowIPAccess = unsafe + ps.UpstreamProxy = upstreamProxy + ps.UpstreamVerifySSL = upstreamVerifySSL + ps.CacheCerts = cacheCerts + ps.NoSSLIntercept = noSSLIntercept + + // Add inline block patterns + if len(blockPatterns) > 0 { + ps.InlineBlockPatterns = blockPatterns + } + + var workerIndices []int + if workersStr != "" { + workerIndices = parseWorkerIndices(workersStr) + } + + if err := ps.LoadWorkers("flaretunnel_endpoints.json", workerIndices); err != nil { + fmt.Printf("āŒ Failed to load workers: %v\n", err) + fmt.Println("Run: go run FlareTunnel.go create --count 5") + return + } + + if len(ps.Workers) == 0 { + fmt.Println("āŒ No workers available") + return + } + + ps.Start(blacklist) + + default: + printHelp() + } +} + +func printHelp() { + help := ` +FlareTunnel - Cloudflare Workers Proxy System (Go Version) + +Usage: go run FlareTunnel.go [options] + +Commands: + config Configure Cloudflare API credentials (interactive) + create Create new Cloudflare Workers + list List all Workers with analytics + test Test Workers with a target URL + export Export configuration to backup file + import Import configuration from backup file + cleanup Delete Workers (prompts for confirmation) + tunnel Start local proxy server + +Examples: + # Configuration + go run FlareTunnel.go config + + # Create Workers + go run FlareTunnel.go create --count 5 + go run FlareTunnel.go create --count 10 --distribute + go run FlareTunnel.go create --count 3 --account main + + # List Workers + go run FlareTunnel.go list # Basic list + go run FlareTunnel.go list --verbose # Detailed + live status check + go run FlareTunnel.go list --status # Only live response times + + # Test Workers + go run FlareTunnel.go test + go run FlareTunnel.go test --url https://httpbin.org/ip + go run FlareTunnel.go test --url https://example.com --method POST + + # Export/Import Config + go run FlareTunnel.go export --output my_backup.json + go run FlareTunnel.go import --input my_backup.json + go run FlareTunnel.go import --input config.json --merge + + # Start Tunnel + go run FlareTunnel.go tunnel --verbose + go run FlareTunnel.go tunnel --workers 0,1,2 --mode random + go run FlareTunnel.go tunnel --port 9090 --blacklist blacklist.txt + go run FlareTunnel.go tunnel --upstream-proxy http://127.0.0.1:8888 + + # Cleanup + go run FlareTunnel.go cleanup + go run FlareTunnel.go cleanup --account main + +Options: + Create: + --count N Number of workers to create (default: 1) + --account NAME Create on specific account + --distribute Auto-distribute across accounts based on quota + + List: + --verbose, -v Show detailed info + live status check (created, age, response times) + --status Check only live worker status (response times, no verbose info) + + Test: + --url URL Target URL to test (default: https://ifconfig.me/ip) + --method METHOD HTTP method (default: GET) + + Export: + --output FILE Output file (default: flaretunnel_config_backup.json) + + Import: + --input FILE Input config backup file (REQUIRED) + --merge Merge with existing config (skip duplicates) + + Cleanup: + --account NAME Delete from specific account only + + Tunnel: + --host HOST Bind host (default: 127.0.0.1) + --port PORT Bind port (default: 8080) + --workers INDICES Worker indices (e.g., '0,1,2' or '0-2') + --mode MODE Rotation mode: random, round-robin (default: round-robin) + --verbose, -v Enable verbose logging + --unsafe Allow IP access (not recommended) + --upstream-proxy URL Upstream proxy (e.g., http://127.0.0.1:8080) + --upstream-verify-ssl Verify SSL for upstream proxy + --cache-certs Cache SSL certs to disk + --no-ssl-intercept Disable SSL interception + --blacklist FILE Blacklist file (default: blacklist-minimal.txt) + --block PATTERN Block pattern (can be used multiple times) +` + fmt.Println(help) +} + diff --git a/README.md b/README.md new file mode 100644 index 0000000..2149893 --- /dev/null +++ b/README.md @@ -0,0 +1,208 @@ +# šŸš€ FlareTunnel + +
+ +FlareTunnel + +**A unified proxy system that routes traffic through Cloudflare Workers for IP rotation and anonymity** + +``` +Client → FlareTunnel (local) → Cloudflare Workers → Target Website +``` + +![FlareTunnel Banner](https://img.shields.io/badge/FlareTunnel-v1.0.0-red?style=for-the-badge&logo=security&logoColor=white) +![Go](https://img.shields.io/badge/Go-1.19+-00ADD8?style=for-the-badge&logo=go&logoColor=white) +![License](https://img.shields.io/badge/License-Non--Commercial-orange?style=for-the-badge) +
+ +**FlareTunnel** is a powerful, unified proxy system that leverages **Cloudflare Workers** to create a robust, rotating proxy network. It allows you to route your traffic through Cloudflare's global edge network, providing high anonymity, speed, and reliability. + +## ✨ Features + +* **🌐 Unlimited Rotating Proxies**: Automatically deploy and manage multiple Cloudflare Workers as proxy endpoints. +* **šŸ”„ Smart Load Balancing**: Distributes traffic across your workers using Random or Round-Robin strategies. +* **⚔ High Performance**: Uses Cloudflare's global edge network for low latency. +* **šŸ” SSL/HTTPS Support**: Full support for HTTPS traffic with optional SSL interception for deep inspection. +* **šŸ‘„ Multi-Account Support**: seamless management of multiple Cloudflare accounts to maximize request quotas (100k requests/day per account). +* **šŸ›”ļø Ad & Tracker Blocking**: Built-in blacklist system to block unwanted traffic and save worker quotas. +* **šŸ“Š Analytics**: Real-time usage statistics and quota tracking per account. + +## šŸ—ļø Architecture + +```mermaid +graph LR + Client["Client (Browser/App)"] -->|HTTP/HTTPS| LocalProxy["Local Proxy :8080"] + subgraph FlareTunnel System + LocalProxy -->|Load Balancing| Rotator{Worker Rotator} + Rotator -->|Request A| W1[Worker 1] + Rotator -->|Request B| W2[Worker 2] + Rotator -->|Request C| W3[Worker 3] + end + W1 -->|Fetch| Target[Target Website] + W2 -->|Fetch| Target + W3 -->|Fetch| Target + style LocalProxy fill:#7289da,stroke:#333,stroke-width:2px,color:white + style W1 fill:#f38020,stroke:#333,stroke-width:2px,color:white + style W2 fill:#f38020,stroke:#333,stroke-width:2px,color:white + style W3 fill:#f38020,stroke:#333,stroke-width:2px,color:white + style Client fill:#fff,stroke:#333,stroke-width:2px + style Target fill:#fff,stroke:#333,stroke-width:2px +``` + +## šŸ“¦ Installation + +### Build from Source +```bash +git clone https://github.com/MorDavid/FlareTunnel.git +cd FlareTunnel +go build -o FlareTunnel FlareTunnel.go +``` + +## šŸš€ Usage + +### 1. Configuration +First, set up your Cloudflare credentials. You'll need your Account ID and an API Token (with "Edit Cloudflare Workers" permission). + +```bash +./FlareTunnel config +``` + +### 2. Create Proxies +Deploy new workers to your Cloudflare account. + +```bash +# Create 5 new proxy workers +./FlareTunnel create --count 5 +``` + +### 3. Start the Tunnel +Start the local proxy server. By default, it runs on `localhost:8080`. + +```bash +./FlareTunnel tunnel +``` + +Now configure your browser or application to use the proxy: +* **Host**: `127.0.0.1` +* **Port**: `8080` + +## šŸ› ļø Commands Reference + +| Command | Description | +|---------|-------------| +| `config` | Configure Cloudflare API credentials (supports multiple accounts) | +| `create` | Deploy new Worker proxies | +| `list` | List all active proxies and show usage stats | +| `tunnel` | Start the local proxy server | +| `test` | Test connectivity of your proxies | +| `cleanup` | Delete all workers from your account | + +## šŸ“– Basic Usage + +### Browser Configuration +``` +HTTP Proxy: 127.0.0.1:8080 +HTTPS Proxy: 127.0.0.1:8080 +``` + +### Python +```python +import requests +import urllib3 +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +proxies = { + 'http': 'http://127.0.0.1:8080', + 'https': 'http://127.0.0.1:8080' +} + +r = requests.get("https://httpbin.org/ip", + proxies=proxies, + verify=False) + +print(r.json()['origin']) # Cloudflare Worker IP +``` + +### Quick Test +```bash +./FlareTunnel test +``` + +--- + +## šŸŽÆ Common Commands + +```bash +# Worker Management +./FlareTunnel list # List all workers +./FlareTunnel list --verbose # Detailed view (created, age, live status) +./FlareTunnel list --status # Check worker response times +./FlareTunnel test # Test workers +./FlareTunnel cleanup # Delete workers from ALL accounts +./FlareTunnel cleanup --account main # Delete workers from 'main' only + +# Multi-Account Worker Creation +./FlareTunnel create --count 10 --distribute # Auto-distribute based on quota +./FlareTunnel create --count 5 --account main # Create on specific account + +# Configuration Backup & Restore +./FlareTunnel export # Export config (accounts + credentials) +./FlareTunnel import --input config.json # Import config (replace) +./FlareTunnel import --input config.json --merge # Merge with existing + +# Tunnel (Proxy Server) +./FlareTunnel tunnel --verbose # Basic +./FlareTunnel tunnel --workers 0-2 # Specific workers +./FlareTunnel tunnel --mode random # Random rotation + +# With Blacklist (Recommended!) +./FlareTunnel tunnel --verbose # Default: blacklist-minimal.txt +./FlareTunnel tunnel --blacklist blacklist.txt --verbose + +# With Burp Suite +./FlareTunnel tunnel --port 9090 --upstream-proxy http://127.0.0.1:8080 --verbose +``` + +--- + +## šŸ’” Blacklist System + +### blacklist-minimal.txt (Default) ⚔ +``` +āœ… Analytics (google-analytics, mixpanel) +āœ… Images (.jpg, .png, .gif, etc.) +āœ… Fonts (.woff, .ttf, etc.) +āœ… Source maps (.map) + +Saves: ~30-40% Worker requests +Website: Works perfectly in browser +``` + +### blacklist.txt (Full) šŸ”„ +``` +āœ… Everything in minimal +āœ… Advertising +āœ… Social tracking +āœ… CSS/JS files +āœ… CDN libraries + +Saves: ~60-70% Worker requests +Website: May look broken (missing assets) +``` + +### blacklist-aggressive.txt (Maximum) šŸ’Ŗ +``` +āœ… Everything in full +āœ… Almost everything except HTML/API + +Saves: ~80-90% Worker requests +Website: Will break in browser (automation tools only) +``` + +--- + +## āš ļø Disclaimer + +This tool is for educational and research purposes only. Please respect Cloudflare's Terms of Service. The authors are not responsible for any misuse of this tool. + +**Made with ā¤ļø for the security and automation community** diff --git a/blacklist-aggressive.txt b/blacklist-aggressive.txt new file mode 100644 index 0000000..616677d --- /dev/null +++ b/blacklist-aggressive.txt @@ -0,0 +1,177 @@ +# FlareTunnel Blacklist - Aggressive Version šŸ”„šŸ’Ŗ +# Blocks almost everything except HTML/API requests +# Useful for automation tools (scrapers, bruteforce, etc.) +# Website will look broken in browsers - but you'll save hundreds of thousands of Worker requests! + +# ======================================== +# šŸ“Š Analytics & Tracking +# ======================================== +google-analytics.com +www.google-analytics.com +ssl.google-analytics.com +analytics.google.com +stats.g.doubleclick.net +doubleclick.net +googletagmanager.com +googletagservices.com +facebook.com/tr +connect.facebook.net +graph.facebook.com +px.ads.linkedin.com +analytics.linkedin.com +analytics.tiktok.com +business.tiktok.com +ads.tiktok.com +log.byteoversea.com +hotjar.com +static.hotjar.com +script.hotjar.com +api.mixpanel.com +decide.mixpanel.com +segment.com +cdn.segment.com +api.segment.io +omtrdc.net +adobedc.net +demdex.net +track.hubspot.com +api.hubspot.com +fullstory.com +script.crazyegg.com +js-agent.newrelic.com +bam.nr-data.net +datadoghq-browser-agent.com +browser-intake-datadoghq.com +sentry.io +browser.sentry-cdn.com +criteo.com +taboola.com +outbrain.com +mixpanel.com +amplitude.com +heap.io +logrocket.com +newrelic.com +quantcast.com +clarity.ms +datadoghq.com +pendo.io +mouseflow.com +crazyegg.com +optimizely.com +kissmetrics.com +chartbeat.com + +# ======================================== +# šŸ“¢ Advertising +# ======================================== +googlesyndication.com +googleadservices.com +adnxs.com +criteo.com +outbrain.com +taboola.com +media.net +pubmatic.com +rubiconproject.com +adroll.com +adsystem.com + +# ======================================== +# šŸ–¼ļø ALL Images +# ======================================== +.jpg +.jpeg +.png +.gif +.webp +.ico +.svg +.bmp +.tiff +.avif + +# ======================================== +# šŸŽØ ALL CSS & Fonts +# ======================================== +.css +.woff +.woff2 +.ttf +.eot +.otf +fonts.googleapis.com +fonts.gstatic.com + +# ======================================== +# šŸ“¦ ALL JavaScript +# ======================================== +.js +.map +.min.js +.bundle.js +ajax.googleapis.com +cdnjs.cloudflare.com +cdn.jsdelivr.net +unpkg.com + +# ======================================== +# šŸŽ¬ ALL Media +# ======================================== +.mp4 +.webm +.ogg +.mp3 +.wav +.m4a + +# ======================================== +# šŸ“„ Documents +# ======================================== +.pdf +.zip +.rar +.doc +.docx +.xls +.xlsx + +# ======================================== +# šŸ‘„ Social Media +# ======================================== +facebook.com/plugins +twitter.com/widgets +platform.twitter.com +linkedin.com/widgets +instagram.com/embed +pinterest.com +tiktok.com/embed + +# ======================================== +# šŸ”Œ ALL Widgets +# ======================================== +disqus.com +livechatinc.com +tawk.to +intercom.io +drift.com +zendesk.com +/chat +/livechat +/widget + +# ======================================== +# šŸŽÆ Tracking Paths +# ======================================== +/pixel +/track +/analytics +/beacon +/log +/collect +/event +/_ga +/_gat +/gtag +/impression + diff --git a/blacklist-minimal.txt b/blacklist-minimal.txt new file mode 100644 index 0000000..196505b --- /dev/null +++ b/blacklist-minimal.txt @@ -0,0 +1,52 @@ +# ======================================== +# šŸ“Š Analytics (Saves the most!) +# ======================================== +google-analytics.com +www.google-analytics.com +ssl.google-analytics.com +analytics.google.com +stats.g.doubleclick.net +doubleclick.net +googletagmanager.com +googletagservices.com +facebook.com/tr +connect.facebook.net +graph.facebook.com +px.ads.linkedin.com +analytics.linkedin.com +analytics.tiktok.com +business.tiktok.com +ads.tiktok.com +log.byteoversea.com +hotjar.com +static.hotjar.com +script.hotjar.com +api.mixpanel.com +decide.mixpanel.com +segment.com +cdn.segment.com +api.segment.io +omtrdc.net +adobedc.net +demdex.net +track.hubspot.com +api.hubspot.com +fullstory.com +script.crazyegg.com +js-agent.newrelic.com +bam.nr-data.net +datadoghq-browser-agent.com +browser-intake-datadoghq.com +sentry.io +browser.sentry-cdn.com +criteo.com +taboola.com +outbrain.com + +# ======================================== +# šŸŽØ Fonts +# ======================================== +fonts.googleapis.com +fonts.gstatic.com +use.typekit.net +use.fontawesome.com diff --git a/blacklist.txt b/blacklist.txt new file mode 100644 index 0000000..d4b9333 --- /dev/null +++ b/blacklist.txt @@ -0,0 +1,254 @@ +# FlareTunnel Blacklist - Full Version šŸ”„ +# Save Worker Requests! šŸ’° +# Each line = one pattern. Lines with # = comments +# Patterns checked against: hostname, path, extension, full URL + +# ======================================== +# šŸ“Š Analytics & Tracking (Saves a lot!) +# ======================================== +google-analytics.com +googletagmanager.com +analytics.google.com +doubleclick.net +stats.g.doubleclick.net +facebook.com/tr +connect.facebook.net +hotjar.com +static.hotjar.com +mixpanel.com +cdn.mxpnl.com +segment.com +cdn.segment.com +amplitude.com +heap.io +fullstory.com +logrocket.com +logrocket.io +newrelic.com +js-agent.newrelic.com +quantcast.com +sentry.io +sentry-cdn.com +clarity.ms +datadoghq.com +pendo.io +mouseflow.com +crazyegg.com +optimizely.com +kissmetrics.com +chartbeat.com +parsely.com +omniconvert.com + +# ======================================== +# šŸ“¢ Advertising & Marketing +# ======================================== +googlesyndication.com +googleadservices.com +adservice.google.com +adnxs.com +ads.yahoo.com +advertising.com +adsystem.com +adroll.com +criteo.com +criteo.net +outbrain.com +taboola.com +media.net +yieldmo.com +pubmatic.com +rubiconproject.com +openx.net +indexww.com +contextweb.com +turn.com +adform.net +smartadserver.com +serving-sys.com +adsafeprotected.com +doubleverify.com +moatads.com + +# ======================================== +# šŸ‘„ Social Media Tracking +# ======================================== +facebook.com/plugins +twitter.com/widgets +platform.twitter.com +linkedin.com/widgets +platform.linkedin.com +instagram.com/embed +pinterest.com/widgets +snapchat.com/widgets +tiktok.com/embed +reddit.com/static + +# ======================================== +# šŸ–¼ļø Image Formats (Saves a lot!) +# ======================================== +.jpg +.jpeg +.png +.gif +.webp +.ico +.svg +.bmp +.tiff +.avif +.jfif + +# ======================================== +# šŸŽØ Fonts & CSS +# ======================================== +.woff +.woff2 +.ttf +.eot +.otf +.css +fonts.googleapis.com +fonts.gstatic.com +use.typekit.net +cloud.typography.com + +# ======================================== +# šŸ“¦ JavaScript & Maps +# ======================================== +.js +.map +.min.js +.bundle.js + +# ======================================== +# šŸŽ¬ Media Files (Video/Audio) +# ======================================== +.mp4 +.webm +.ogg +.mp3 +.wav +.m4a +.flac +.aac + +# ======================================== +# šŸ“„ Documents & Archives +# ======================================== +.pdf +.zip +.rar +.7z +.tar +.gz +.doc +.docx +.xls +.xlsx +.ppt +.pptx + +# ======================================== +# šŸŽÆ Common Tracking Paths +# ======================================== +/pixel +/pixel.gif +/track +/tracking +/analytics +/beacon +/log +/collect +/event +/events +/telemetry +/metrics +/stats +/ping +/heartbeat +/_ga +/_gat +/_gid +/gtag +/fbevents +/tr.gif +/impression +/view +/click + +# ======================================== +# 🌐 CDN & Static Assets +# ======================================== +cdnjs.cloudflare.com/ajax/libs +cdn.jsdelivr.net +unpkg.com +stackpath.bootstrapcdn.com +maxcdn.bootstrapcdn.com +ajax.googleapis.com/ajax/libs + +# ======================================== +# šŸ”Œ Third-Party Widgets +# ======================================== +disqus.com +zopim.com +livechatinc.com +tawk.to +intercom.io +drift.com +olark.com +zendesk.com/embeddable +helpscout.com +uservoice.com + +# ======================================== +# šŸŽ® Gaming/App Analytics +# ======================================== +unity3d.com/stats +unityads.unity3d.com +api.gameanalytics.com +firebase.google.com + +# ======================================== +# šŸ›”ļø Security/Bot Detection (Careful!) +# ======================================== +# Note: Blocking these may break some websites +# recaptcha.net +# google.com/recaptcha +# hcaptcha.com +# cloudflare.com/cdn-cgi + +# ======================================== +# šŸŽÆ Specific Trackers +# ======================================== +bat.bing.com +t.co +bit.ly +ow.ly +goo.gl +tinyurl.com + +# ======================================== +# šŸ“± Mobile Analytics +# ======================================== +app-measurement.com +firebaselogging.googleapis.com +crashlytics.com +fabric.io + +# ======================================== +# šŸ’¬ Chat Widgets +# ======================================== +/chat.js +/livechat +/widget +/embed.js + +# ======================================== +# āš™ļø Add your own patterns here: +# ======================================== +# Examples: +# example-tracker.com +# .mp4 +# /my-custom-tracking-path + diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..52edd31 --- /dev/null +++ b/build.bat @@ -0,0 +1,64 @@ +@echo off +REM Build script for FlareTunnel Go version (Windows) + +echo šŸ”Ø Building FlareTunnel... +echo. + +echo šŸ“¦ Downloading dependencies... +go mod download +if errorlevel 1 ( + echo āŒ Failed to download dependencies + pause + exit /b 1 +) + +echo. +echo šŸ—ļø Building for Windows... +go build -ldflags="-s -w" -o flaretunnel.exe flaretunnel.go +if errorlevel 1 ( + echo āŒ Build failed + pause + exit /b 1 +) + +echo. +echo āœ… Build complete: flaretunnel.exe +echo. +echo šŸš€ Quick start: +echo flaretunnel.exe config # Configure accounts +echo flaretunnel.exe create --count 5 # Create workers +echo flaretunnel.exe list --verbose # List workers +echo flaretunnel.exe tunnel --verbose # Start proxy +echo. + +set /p REPLY="Build for other platforms? (y/N) " +if /i "%REPLY%"=="y" ( + echo. + echo šŸŒ Building for multiple platforms... + + echo Building for Linux (amd64)... + set GOOS=linux + set GOARCH=amd64 + go build -ldflags="-s -w" -o flaretunnel-linux-amd64 flaretunnel.go + + echo Building for macOS (amd64)... + set GOOS=darwin + set GOARCH=amd64 + go build -ldflags="-s -w" -o flaretunnel-macos-amd64 flaretunnel.go + + echo Building for macOS (arm64)... + set GOOS=darwin + set GOARCH=arm64 + go build -ldflags="-s -w" -o flaretunnel-macos-arm64 flaretunnel.go + + echo. + echo āœ… Cross-compilation complete! + echo. + echo šŸ“¦ Built binaries: + dir /b flaretunnel-* + echo. +) + +echo ✨ Done! +pause + diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..8b88caa --- /dev/null +++ b/build.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Build script for FlareTunnel Go version + +set -e + +echo "šŸ”Ø Building FlareTunnel..." + +# Get dependencies +echo "šŸ“¦ Downloading dependencies..." +go mod download + +# Build for current platform +echo "šŸ—ļø Building for current platform..." +go build -ldflags="-s -w" -o flaretunnel flaretunnel.go + +echo "āœ… Build complete: ./flaretunnel" +echo "" +echo "šŸ“Š Binary size:" +ls -lh flaretunnel | awk '{print $5}' +echo "" +echo "šŸš€ Quick start:" +echo " ./flaretunnel config # Configure accounts" +echo " ./flaretunnel create --count 5 # Create workers" +echo " ./flaretunnel list --verbose # List workers" +echo " ./flaretunnel tunnel --verbose # Start proxy" +echo "" + +# Optional: Build for other platforms +read -p "Build for other platforms? (y/N) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]] +then + echo "šŸŒ Building for multiple platforms..." + + # Windows + echo " Building for Windows (amd64)..." + GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o flaretunnel-windows-amd64.exe flaretunnel.go + + # Linux + echo " Building for Linux (amd64)..." + GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o flaretunnel-linux-amd64 flaretunnel.go + + # macOS Intel + echo " Building for macOS (amd64)..." + GOOS=darwin GOARCH=amd64 go build -ldflags="-s -w" -o flaretunnel-macos-amd64 flaretunnel.go + + # macOS Apple Silicon + echo " Building for macOS (arm64)..." + GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o flaretunnel-macos-arm64 flaretunnel.go + + # Linux ARM (Raspberry Pi, etc.) + echo " Building for Linux (arm64)..." + GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o flaretunnel-linux-arm64 flaretunnel.go + + echo "" + echo "āœ… Cross-compilation complete!" + echo "" + echo "šŸ“¦ Built binaries:" + ls -lh flaretunnel-* | awk '{print " " $9 " - " $5}' + echo "" +fi + +echo "✨ Done!" + diff --git a/example_usage.py b/example_usage.py new file mode 100644 index 0000000..05f89d3 --- /dev/null +++ b/example_usage.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +""" +FlareTunnel - Quick Usage Example +Make sure proxy is running: python FlareTunnel.py tunnel --verbose +""" + +import requests +import urllib3 +import time + +# Disable SSL warnings +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + +# FlareTunnel proxy configuration +proxies = { + 'http': 'http://127.0.0.1:8080', + 'https': 'http://127.0.0.1:8080' +} + +print("=" * 80) +print("FlareTunnel - Usage Example") +print("=" * 80) +print() +print("Make sure proxy is running: python FlareTunnel.py tunnel --verbose") +print() +print("=" * 80) +print() + +# Test 1: Simple GET +print("šŸ“” Test 1: GET Request") +print("-" * 40) +try: + response = requests.get( + "https://httpbin.org/get?test=123", + proxies=proxies, + verify=False, + timeout=10 + ) + print(f"āœ“ Status: {response.status_code}") + data = response.json() + print(f"āœ“ Origin IP: {data.get('origin', 'N/A')}") + print(f"āœ“ Args: {data.get('args', {})}") + print() +except Exception as e: + print(f"āœ— Error: {e}") + print() + +# Test 2: POST with JSON +print("šŸ“¤ Test 2: POST Request with JSON") +print("-" * 40) +try: + response = requests.post( + "https://httpbin.org/post", + json={ + "username": "testuser", + "action": "login", + "timestamp": time.time() + }, + proxies=proxies, + verify=False, + timeout=10 + ) + print(f"āœ“ Status: {response.status_code}") + data = response.json() + print(f"āœ“ Received JSON: {data.get('json', {})}") + print() +except Exception as e: + print(f"āœ— Error: {e}") + print() + +# Test 3: Custom Headers +print("šŸ“‹ Test 3: Custom Headers") +print("-" * 40) +try: + response = requests.get( + "https://httpbin.org/headers", + headers={ + "X-Custom-Header": "FlareTunnel-Test", + "User-Agent": "FlareTunnel/2.0" + }, + proxies=proxies, + verify=False, + timeout=10 + ) + print(f"āœ“ Status: {response.status_code}") + data = response.json() + headers_received = data.get('headers', {}) + print(f"āœ“ Custom Header: {headers_received.get('X-Custom-Header', 'Not received')}") + print(f"āœ“ User-Agent: {headers_received.get('User-Agent', 'Not received')}") + print() +except Exception as e: + print(f"āœ— Error: {e}") + print() + +# Test 4: IP Rotation Check +print("šŸ”„ Test 4: IP Rotation (5 requests)") +print("-" * 40) +ips = set() +for i in range(5): + try: + response = requests.get( + "https://httpbin.org/ip", + proxies=proxies, + verify=False, + timeout=10 + ) + if response.status_code == 200: + ip = response.json().get('origin', 'N/A') + ips.add(ip) + print(f" Request {i+1}: {ip}") + time.sleep(0.5) + except Exception as e: + print(f" Request {i+1}: Error - {e}") + +print(f"\nāœ“ Unique IPs: {len(ips)}") +for ip in sorted(ips): + print(f" - {ip}") +print() + +# Test 5: Real Website +print("🌐 Test 5: Real Website (example.com)") +print("-" * 40) +try: + response = requests.get( + "https://example.com", + proxies=proxies, + verify=False, + timeout=10 + ) + print(f"āœ“ Status: {response.status_code}") + print(f"āœ“ Size: {len(response.content):,} bytes") + # Show first 100 chars + content_preview = response.text[:100].replace('\n', ' ') + print(f"āœ“ Preview: {content_preview}...") + print() +except Exception as e: + print(f"āœ— Error: {e}") + print() + +# Summary +print("=" * 80) +print("āœ… All tests completed!") +print("=" * 80) +print() +print("šŸ’” Key Points:") +print(" • All requests went through Cloudflare Workers") +print(" • Use rotation modes to change IPs (--mode random)") +print(" • Use blacklist to save Worker requests (default: blacklist-minimal.txt)") +print(" • IP addresses are blocked by default (Cloudflare doesn't support them)") +print() +print("šŸ“š Learn More:") +print(" • README.md - Full documentation") +print(" • python FlareTunnel.py --help - All commands") +print() diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f0e2e78 --- /dev/null +++ b/go.mod @@ -0,0 +1,7 @@ +module flaretunnel + +go 1.21 + +require github.com/olekukonko/tablewriter v0.0.5 + +require github.com/mattn/go-runewidth v0.0.9 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..56c7ba4 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..1d8fd03cefe2bd8cf4032f70b77da734b79c901f GIT binary patch literal 58551 zcmb4qb8seK&~9woPByk}+fFvNZQI7iw(ZTv)+Twwjs3>P&G-B6-?!>c)y$kTHFNq@ zpE}+BJlzwmtSE&DhX)4&0)i+bEv^az0($@5u7ZL3zHT+mVSnF1T~(z-LDwhf1HU`4 zPSQHAARzGQ|8s(Zb~#zMA?Z>LV>J3LmC;Ld)SM zA{;|@5)@Mj!yS{rN=sf+vahXjJO�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 R%Add{`&CJ@O57;$e*n;7WZVD% literal 0 HcmV?d00001