├── .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 | 
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