├── .gitignore ├── Dockerfile ├── blocklist.go ├── config.toml ├── go.mod ├── docker-compose.yml ├── config.go ├── menu.html ├── .github └── workflows │ └── go.yml ├── main.go ├── go.sum ├── dns.go ├── util.go ├── README.md ├── cert.go ├── http.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | cert/ 2 | freenews 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine 2 | 3 | WORKDIR /app 4 | 5 | ADD ./freenews . 6 | ADD ./config.toml . 7 | 8 | # DNS Ports 9 | EXPOSE 53/tcp 10 | EXPOSE 53/udp 11 | EXPOSE 853/tcp 12 | EXPOSE 853/udp 13 | 14 | # HTTP Ports 15 | EXPOSE 80/tcp 16 | EXPOSE 443/tcp 17 | 18 | RUN chmod +x /app/freenews 19 | 20 | ENTRYPOINT ["/app/freenews"] 21 | -------------------------------------------------------------------------------- /blocklist.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | var blockList []string 10 | 11 | func loadBlockList() { 12 | b, err := os.ReadFile(*blockListPath) 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | lines := strings.Split(string(b), "\n") 17 | for _, line := range lines { 18 | if strings.HasPrefix(line, "#") { 19 | continue 20 | } 21 | blockList = append(blockList, line) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | # The landing/info page where you can download the MITM CA to your device 2 | info_host = "free.news" 3 | # Use Cloudflare DNS 4 | upstream_dns = "1.1.1.1:53" 5 | 6 | [[host]] 7 | name = "example.com" 8 | # Defaults: 9 | # from_google_cache = false 10 | # This option will ignore all other header based options 11 | # social_referer = true 12 | # googlebot_ua = true 13 | # googlebot_ip = true 14 | # disable_cookie = true 15 | # disable_js = true 16 | # inject_html = "" 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fipso/freenews 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/BurntSushi/toml v1.3.2 7 | github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89 8 | github.com/dsnet/compress v0.0.1 9 | github.com/miekg/dns v1.1.58 10 | ) 11 | 12 | require ( 13 | golang.org/x/mod v0.16.0 // indirect 14 | golang.org/x/net v0.22.0 // indirect 15 | golang.org/x/sys v0.18.0 // indirect 16 | golang.org/x/text v0.14.0 // indirect 17 | golang.org/x/tools v0.19.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | server: 5 | image: "ghcr.io/fipso/freenews:main" 6 | ports: 7 | - "53:53" 8 | - "80:80" 9 | - "443:443" 10 | - "853:853" 11 | volumes: 12 | - ./config.toml:/app/config.toml 13 | - ./cert/:/app/cert/ 14 | # Uncomment if you want to use DNS over TLS 15 | # this is highly recommended, but requires owning a domain 16 | # and getting a SSL certificate from for ex: lets encrypt 17 | #command: "-dotDomain YOUR_DOMAIN.com" 18 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/BurntSushi/toml" 7 | ) 8 | 9 | type Config struct { 10 | InfoHost string `toml:"info_host"` 11 | UpstreamDNS string `toml:"upstream_dns"` 12 | Hosts []HostOptions `toml:"host"` 13 | } 14 | 15 | type HostOptions struct { 16 | Name string `toml:"name"` 17 | FromGoogleCache *bool `toml:"from_google_cache,omitempty"` 18 | SocialReferer *bool `toml:"social_referer,omitempty"` 19 | GooglebotUA *bool `toml:"googlebot_ua,omitempty"` 20 | GooglebotIP *bool `toml:"googlebot_ip,omitempty"` 21 | DisableCookies *bool `toml:"disable_cookies,omitempty"` 22 | DisableJS *bool `toml:"disable_js,omitempty"` 23 | InjectHTML *string `toml:"inject_html,omitempty"` 24 | } 25 | 26 | func parseConfigFile() { 27 | _, err := toml.DecodeFile("config.toml", &config) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /menu.html: -------------------------------------------------------------------------------- 1 | ⚠️ Served by FreeNews R.Proxy | Share 🌍 2 | 3 | 41 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ "main", "dev" ] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | # Build staticly 11 | CGO_ENABLED: 0 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.18 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Upload a Build Artifact 28 | uses: actions/upload-artifact@v3.1.0 29 | with: 30 | name: freenews 31 | path: ./freenews 32 | 33 | docker: 34 | needs: build 35 | runs-on: ubuntu-latest 36 | permissions: 37 | contents: read 38 | packages: write 39 | 40 | steps: 41 | - uses: actions/checkout@v3 42 | 43 | - uses: actions/download-artifact@v2 44 | with: 45 | name: freenews 46 | 47 | - name: Log in to the Container registry 48 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 49 | with: 50 | registry: ${{ env.REGISTRY }} 51 | username: ${{ github.actor }} 52 | password: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | - name: Extract metadata (tags, labels) for Docker 55 | id: meta 56 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 57 | with: 58 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 59 | 60 | - name: Build and push Docker image 61 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 62 | with: 63 | context: . 64 | push: true 65 | tags: ${{ steps.meta.outputs.tags }} 66 | labels: ${{ steps.meta.outputs.labels }} 67 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "flag" 7 | "log" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | ca *x509.Certificate 14 | transport *http.Transport 15 | tlsHttpServerConfig *tls.Config 16 | tlsDoTServerConfig *tls.Config 17 | caString string 18 | publicIP *string 19 | mitmAAAA = flag.String("mitmAAAA", "", "IPv6 address to use for MITM") 20 | dnsPort *int 21 | dnsTlsPort *int 22 | httpPort *int 23 | httpsPort *int 24 | dotDomain *string 25 | blockListPath *string 26 | config Config 27 | ) 28 | 29 | func main() { 30 | //Parse flags 31 | publicIP = flag.String("publicIP", getPublicIP(), "Public interface ip address") 32 | dnsPort = flag.Int("dnsPort", 53, "Port for normal UDP DNS") 33 | dnsTlsPort = flag.Int("dnsTlsPort", 853, "Port for DNS over TLS aka. DoT") 34 | httpPort = flag.Int("httpPort", 80, "Port for HTTP Reverse Proxy") 35 | httpsPort = flag.Int("httpsPort", 443, "Port for HTTPS Reverse Proxy") 36 | dotDomain = flag.String("dotDomain", "", "Domain for DNS over TLS") 37 | blockListPath = flag.String("blockList", "", "Path to a DNS block list file") 38 | flag.Parse() 39 | 40 | //Parse config file 41 | //TODO make flags overridable 42 | parseConfigFile() 43 | 44 | serverIps := make([]string, 0, 2) 45 | if publicIP != nil { 46 | serverIps = append(serverIps, *publicIP) 47 | } 48 | if mitmAAAA != nil { 49 | serverIps = append(serverIps, *mitmAAAA) 50 | } 51 | log.Printf("[*] Welcome. Public DNS Server IPs: %v", strings.Join(serverIps, ", ")) 52 | setupCerts() 53 | if len(ca.Signature) != 0 { 54 | log.Printf("[*] CA Signature: %x...", ca.Signature[:16]) 55 | } else { 56 | log.Printf("[*] Generated New CA:\n%s ", caString) 57 | } 58 | 59 | if *blockListPath != "" { 60 | log.Printf("[*] Using block list: %s", *blockListPath) 61 | loadBlockList() 62 | } 63 | 64 | go serveDNS() 65 | if *dotDomain != "" { 66 | setupDoTCerts() 67 | go serveDNSoverTLS() 68 | } 69 | 70 | serveHTTP() 71 | } 72 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= 2 | github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89 h1:2pkAuIM8OF1fy4ToFpMnI4oE+VeUNRbGrpSLKshK0oQ= 4 | github.com/bobesa/go-domain-util v0.0.0-20190911083921-4033b5f7dd89/go.mod h1:/09nEjna1UMoasyyQDhOrIn8hi2v2kiJglPWed1idck= 5 | github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= 6 | github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= 7 | github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= 8 | github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= 9 | github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 10 | github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4= 11 | github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY= 12 | github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= 13 | golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= 14 | golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 15 | golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 16 | golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= 17 | golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 18 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 19 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 20 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 21 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 22 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 23 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 24 | golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw= 25 | golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc= 26 | -------------------------------------------------------------------------------- /dns.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "strconv" 7 | 8 | "github.com/miekg/dns" 9 | ) 10 | 11 | func answerQuery(m *dns.Msg) { 12 | for _, q := range m.Question { 13 | switch q.Qtype { 14 | case dns.TypeA: 15 | answerQuestionType(m, q, "A", *publicIP, "127.0.0.1") 16 | case dns.TypeAAAA: 17 | // If this isn't set, and a query doesn't have an A type question that we answer here, 18 | // the query will be forwarded upstream which might not be the result we intended. 19 | if mitmAAAA != nil { 20 | answerQuestionType(m, q, "AAAA", *mitmAAAA, "::1") 21 | } 22 | } 23 | } 24 | } 25 | 26 | func answerQuestionType(m *dns.Msg, q dns.Question, _type, mitmHost, blockHost string) { 27 | host := q.Name[:len(q.Name)-1] 28 | 29 | // Check if question is for the info host or on unpaywall list 30 | options := getHostOptions(host) 31 | if compareBase(host, config.InfoHost) || options != nil { 32 | record := fmt.Sprintf("%s %s %s", q.Name, _type, mitmHost) 33 | rr, err := dns.NewRR(record) 34 | if err != nil { 35 | log.Println("[ERR]", err) 36 | return 37 | } 38 | m.Answer = append(m.Answer, rr) 39 | return 40 | } 41 | 42 | // Check if host is on blocklist 43 | for _, blocked := range blockList { 44 | if host == blocked { 45 | record := fmt.Sprintf("%s %s %s", q.Name, _type, blockHost) 46 | rr, err := dns.NewRR(record) 47 | if err != nil { 48 | log.Println("[ERR]", err) 49 | continue 50 | } 51 | m.Answer = append(m.Answer, rr) 52 | break 53 | } 54 | } 55 | } 56 | 57 | func handleDnsRequest(w dns.ResponseWriter, req *dns.Msg) { 58 | m := new(dns.Msg) 59 | m.SetReply(req) 60 | m.Compress = false 61 | 62 | switch req.Opcode { 63 | case dns.OpcodeQuery: 64 | answerQuery(m) 65 | } 66 | 67 | // If we dont know what to do forward request to upstream dns 68 | if len(m.Answer) == 0 { 69 | c := &dns.Client{Net: "udp"} 70 | res, _, err := c.Exchange(req, config.UpstreamDNS) 71 | if err != nil { 72 | dns.HandleFailed(w, req) 73 | log.Println("[ERR]", err) 74 | return 75 | } 76 | w.WriteMsg(res) 77 | return 78 | } 79 | 80 | log.Printf("[DNS] Response: %s", m.Answer) 81 | w.WriteMsg(m) 82 | } 83 | 84 | func serveDNS() { 85 | // attach request handler func 86 | dns.HandleFunc(".", handleDnsRequest) 87 | 88 | // start server 89 | go func() { 90 | log.Printf("[DNS] Listening on 0.0.0.0:%d(udp only)", *dnsPort) 91 | server := &dns.Server{Addr: ":" + strconv.Itoa(*dnsPort), Net: "udp"} 92 | err := server.ListenAndServe() 93 | defer server.Shutdown() 94 | if err != nil { 95 | log.Fatalf("[ERR] Failed to start DNS server: %s\n ", err.Error()) 96 | } 97 | }() 98 | } 99 | 100 | func serveDNSoverTLS() error { 101 | log.Printf("[DNS-TLS] Listening on %s:%d(tcp/tls)", *dotDomain, *dnsTlsPort) 102 | server := &dns.Server{ 103 | Addr: ":" + strconv.Itoa(*dnsTlsPort), 104 | Net: "tcp-tls", 105 | TLSConfig: tlsDoTServerConfig, 106 | } 107 | err := server.ListenAndServe() 108 | defer server.Shutdown() 109 | if err != nil { 110 | return err 111 | } 112 | return nil 113 | } 114 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/flate" 6 | "compress/gzip" 7 | "encoding/json" 8 | "io" 9 | "net/http" 10 | "strconv" 11 | 12 | "github.com/bobesa/go-domain-util/domainutil" 13 | "github.com/dsnet/compress/brotli" 14 | ) 15 | 16 | // https://stackoverflow.com/questions/41670155/get-public-ip-in-golang 17 | type IP struct { 18 | Query string 19 | } 20 | 21 | func getPublicIP() string { 22 | req, err := http.Get("http://ip-api.com/json/") 23 | if err != nil { 24 | return err.Error() 25 | } 26 | defer req.Body.Close() 27 | 28 | body, err := io.ReadAll(req.Body) 29 | if err != nil { 30 | return err.Error() 31 | } 32 | 33 | var ip IP 34 | json.Unmarshal(body, &ip) 35 | 36 | return ip.Query 37 | } 38 | 39 | func compareBase(name1, name2 string) bool { 40 | return domainutil.Domain(name1) == domainutil.Domain(name2) 41 | } 42 | 43 | func getHostOptions(host string) *HostOptions { 44 | for _, entry := range config.Hosts { 45 | //Only compare domain + tld. Ignore subdomains 46 | if compareBase(entry.Name, host) { 47 | //log.Println("match", entry) 48 | return &entry 49 | } 50 | } 51 | return nil 52 | } 53 | 54 | //Stolen from: https://github.com/drk1wi/Modlishka/blob/00a2385a0952c48202ed0e314b0be016e0613ba7/core/proxy.go#L375 55 | 56 | func decompress(httpResponse *http.Response) (buffer []byte, err error) { 57 | body := httpResponse.Body 58 | compression := httpResponse.Header.Get("Content-Encoding") 59 | 60 | var reader io.ReadCloser 61 | 62 | switch compression { 63 | case "x-gzip": 64 | fallthrough 65 | case "gzip": 66 | // A format using the Lempel-Ziv coding (LZ77), with a 32-bit CRC. 67 | 68 | reader, err = gzip.NewReader(body) 69 | if err != io.EOF { 70 | buffer, _ = io.ReadAll(reader) 71 | defer reader.Close() 72 | } else { 73 | // Unset error 74 | err = nil 75 | } 76 | 77 | case "deflate": 78 | // Using the zlib structure (defined in RFC 1950) with the deflate compression algorithm (defined in RFC 1951). 79 | 80 | reader = flate.NewReader(body) 81 | buffer, _ = io.ReadAll(reader) 82 | defer reader.Close() 83 | 84 | case "br": 85 | // A format using the Brotli algorithm. 86 | 87 | c := brotli.ReaderConfig{} 88 | reader, err = brotli.NewReader(body, &c) 89 | buffer, _ = io.ReadAll(reader) 90 | defer reader.Close() 91 | 92 | case "compress": 93 | // Unhandled: Fallback to default 94 | 95 | fallthrough 96 | 97 | default: 98 | reader = body 99 | buffer, err = io.ReadAll(reader) 100 | if err != nil { 101 | return nil, err 102 | } 103 | defer reader.Close() 104 | } 105 | 106 | return 107 | } 108 | 109 | // GZIP content 110 | func gzipBuffer(input []byte) []byte { 111 | var b bytes.Buffer 112 | gz := gzip.NewWriter(&b) 113 | if _, err := gz.Write(input); err != nil { 114 | panic(err) 115 | } 116 | if err := gz.Flush(); err != nil { 117 | panic(err) 118 | } 119 | if err := gz.Close(); err != nil { 120 | panic(err) 121 | } 122 | return b.Bytes() 123 | } 124 | 125 | // Deflate content 126 | func deflateBuffer(input []byte) []byte { 127 | var b bytes.Buffer 128 | zz, err := flate.NewWriter(&b, 0) 129 | 130 | if err != nil { 131 | panic(err) 132 | } 133 | if _, err = zz.Write(input); err != nil { 134 | panic(err) 135 | } 136 | if err := zz.Flush(); err != nil { 137 | panic(err) 138 | } 139 | if err := zz.Close(); err != nil { 140 | panic(err) 141 | } 142 | return b.Bytes() 143 | } 144 | 145 | func compress(httpResponse *http.Response, buffer []byte) { 146 | compression := httpResponse.Header.Get("Content-Encoding") 147 | switch compression { 148 | case "x-gzip": 149 | fallthrough 150 | case "gzip": 151 | buffer = gzipBuffer(buffer) 152 | 153 | case "deflate": 154 | buffer = deflateBuffer(buffer) 155 | 156 | case "br": 157 | // Brotli writer is not available just compress with something else 158 | httpResponse.Header.Set("Content-Encoding", "deflate") 159 | buffer = deflateBuffer(buffer) 160 | 161 | default: 162 | // Whatif? 163 | } 164 | 165 | body := io.NopCloser(bytes.NewReader(buffer)) 166 | httpResponse.Body = body 167 | httpResponse.ContentLength = int64(len(buffer)) 168 | httpResponse.Header.Set("Content-Length", strconv.Itoa(len(buffer))) 169 | 170 | httpResponse.Body.Close() 171 | } 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FreeNews 🔨💵🧱 2 | 3 | ![build status](https://github.com/fipso/freenews/actions/workflows/go.yml/badge.svg?branch=main) 4 | 5 | Reverse Proxy & DNS based solution to bypass paywalls written in go 6 | 7 | ### Features 8 | 9 | - Pull from Google Cache Bypass (shoutout to 12ft.io) 10 | - HTTP Header based bypasses 11 | - AdsBot-Google User Agent 12 | - X-Forwarded-For Google Datacenter IP 13 | - Twitter t.co Referer 14 | - Drop Cookie & Set-Cookie 15 | - HTTP Body patches 16 | - Disable JS. Removes