commit 2cddbdff814014be74945c7d31510ab90b57e09a Author: Mor David Date: Wed Dec 3 06:00:21 2025 +0200 Add files via upload 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 0000000..1d8fd03 Binary files /dev/null and b/logo.png differ