├── .github
└── workflows
│ ├── goreleaser-release.yml
│ ├── goreleaser-validate.yml
│ └── renvoate-config-validation.yml
├── .gitignore
├── .idea
├── .gitignore
├── alwaysonline.iml
├── modules.xml
└── vcs.xml
├── Dockerfile.goreleaser
├── LICENSE
├── README.md
├── dns_server.go
├── dns_server_a.go
├── dns_server_aaaa.go
├── dns_server_fallback.go
├── dns_server_soa.go
├── dns_server_txt.go
├── doc
└── assets
│ └── windows10_20h2_ncsi.png
├── docker-compose.yml
├── exceptions.go
├── go.mod
├── go.sum
├── goreleaser.yaml
├── http_log.go
├── http_server.go
├── main.go
├── port.go
├── renovate.json
└── version.go
/.github/workflows/goreleaser-release.yml:
--------------------------------------------------------------------------------
1 | name: "GoReleaser Release"
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | env:
9 | GOPATH: /tmp/go
10 |
11 | jobs:
12 | goreleaser:
13 | runs-on: ubuntu-latest
14 | permissions:
15 | contents: write
16 | packages: write
17 | steps:
18 | - uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 0
21 | - uses: actions/setup-go@v5
22 | - uses: crazy-max/ghaction-upx@v3
23 | with:
24 | install-only: true
25 | - uses: docker/login-action@v3
26 | with:
27 | username: ${{ secrets.DOCKERHUB_USERNAME }}
28 | password: ${{ secrets.DOCKERHUB_TOKEN }}
29 | - uses: goreleaser/goreleaser-action@v6
30 | with:
31 | args: release --clean
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 |
--------------------------------------------------------------------------------
/.github/workflows/goreleaser-validate.yml:
--------------------------------------------------------------------------------
1 | name: "GoReleaser Validation"
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 |
11 | env:
12 | GOPATH: /tmp/go
13 |
14 | jobs:
15 | goreleaser:
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 0
21 | - uses: actions/setup-go@v5
22 | - uses: crazy-max/ghaction-upx@v3
23 | with:
24 | install-only: true
25 | - uses: goreleaser/goreleaser-action@v6
26 | with:
27 | distribution: goreleaser
28 | args: build --snapshot --clean
29 | env:
30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31 |
--------------------------------------------------------------------------------
/.github/workflows/renvoate-config-validation.yml:
--------------------------------------------------------------------------------
1 | name: "Renovate Config Validation"
2 |
3 | on:
4 | push:
5 | paths:
6 | - renovate.json
7 | pull_request:
8 | branches:
9 | - master
10 | paths:
11 | - renovate.json
12 | jobs:
13 | renovate-config-validation:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 | - uses: actions/setup-node@v4
20 | - run: npx --yes --package renovate -- renovate-config-validator --strict
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist/
2 |
3 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,windows,macos,go,intellij
4 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,windows,macos,go,intellij
5 |
6 | ### Go ###
7 | # Binaries for programs and plugins
8 | *.exe
9 | *.exe~
10 | *.dll
11 | *.so
12 | *.dylib
13 |
14 | # Test binary, built with `go test -c`
15 | *.test
16 |
17 | # Output of the go coverage tool, specifically when used with LiteIDE
18 | *.out
19 |
20 | # Dependency directories (remove the comment below to include it)
21 | # vendor/
22 |
23 | ### Go Patch ###
24 | /vendor/
25 | /Godeps/
26 |
27 | ### Intellij ###
28 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
29 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
30 |
31 | # User-specific stuff
32 | .idea/**/workspace.xml
33 | .idea/**/tasks.xml
34 | .idea/**/usage.statistics.xml
35 | .idea/**/dictionaries
36 | .idea/**/shelf
37 |
38 | # Generated files
39 | .idea/**/contentModel.xml
40 |
41 | # Sensitive or high-churn files
42 | .idea/**/dataSources/
43 | .idea/**/dataSources.ids
44 | .idea/**/dataSources.local.xml
45 | .idea/**/sqlDataSources.xml
46 | .idea/**/dynamic.xml
47 | .idea/**/uiDesigner.xml
48 | .idea/**/dbnavigator.xml
49 |
50 | # Gradle
51 | .idea/**/gradle.xml
52 | .idea/**/libraries
53 |
54 | # Gradle and Maven with auto-import
55 | # When using Gradle or Maven with auto-import, you should exclude module files,
56 | # since they will be recreated, and may cause churn. Uncomment if using
57 | # auto-import.
58 | # .idea/artifacts
59 | # .idea/compiler.xml
60 | # .idea/jarRepositories.xml
61 | # .idea/modules.xml
62 | # .idea/*.iml
63 | # .idea/modules
64 | # *.iml
65 | # *.ipr
66 |
67 | # CMake
68 | cmake-build-*/
69 |
70 | # Mongo Explorer plugin
71 | .idea/**/mongoSettings.xml
72 |
73 | # File-based project format
74 | *.iws
75 |
76 | # IntelliJ
77 | out/
78 |
79 | # mpeltonen/sbt-idea plugin
80 | .idea_modules/
81 |
82 | # JIRA plugin
83 | atlassian-ide-plugin.xml
84 |
85 | # Cursive Clojure plugin
86 | .idea/replstate.xml
87 |
88 | # Crashlytics plugin (for Android Studio and IntelliJ)
89 | com_crashlytics_export_strings.xml
90 | crashlytics.properties
91 | crashlytics-build.properties
92 | fabric.properties
93 |
94 | # Editor-based Rest Client
95 | .idea/httpRequests
96 |
97 | # Android studio 3.1+ serialized cache file
98 | .idea/caches/build_file_checksums.ser
99 |
100 | ### Intellij Patch ###
101 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
102 |
103 | # *.iml
104 | # modules.xml
105 | # .idea/misc.xml
106 | # *.ipr
107 |
108 | # Sonarlint plugin
109 | # https://plugins.jetbrains.com/plugin/7973-sonarlint
110 | .idea/**/sonarlint/
111 |
112 | # SonarQube Plugin
113 | # https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
114 | .idea/**/sonarIssues.xml
115 |
116 | # Markdown Navigator plugin
117 | # https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
118 | .idea/**/markdown-navigator.xml
119 | .idea/**/markdown-navigator-enh.xml
120 | .idea/**/markdown-navigator/
121 |
122 | # Cache file creation bug
123 | # See https://youtrack.jetbrains.com/issue/JBR-2257
124 | .idea/$CACHE_FILE$
125 |
126 | # CodeStream plugin
127 | # https://plugins.jetbrains.com/plugin/12206-codestream
128 | .idea/codestream.xml
129 |
130 | ### macOS ###
131 | # General
132 | .DS_Store
133 | .AppleDouble
134 | .LSOverride
135 |
136 | # Icon must end with two \r
137 | Icon
138 |
139 | # Thumbnails
140 | ._*
141 |
142 | # Files that might appear in the root of a volume
143 | .DocumentRevisions-V100
144 | .fseventsd
145 | .Spotlight-V100
146 | .TemporaryItems
147 | .Trashes
148 | .VolumeIcon.icns
149 | .com.apple.timemachine.donotpresent
150 |
151 | # Directories potentially created on remote AFP share
152 | .AppleDB
153 | .AppleDesktop
154 | Network Trash Folder
155 | Temporary Items
156 | .apdisk
157 |
158 | ### VisualStudioCode ###
159 | .vscode/*
160 | !.vscode/settings.json
161 | !.vscode/tasks.json
162 | !.vscode/launch.json
163 | !.vscode/extensions.json
164 | *.code-workspace
165 |
166 | ### VisualStudioCode Patch ###
167 | # Ignore all local history of files
168 | .history
169 | .ionide
170 |
171 | ### Windows ###
172 | # Windows thumbnail cache files
173 | Thumbs.db
174 | Thumbs.db:encryptable
175 | ehthumbs.db
176 | ehthumbs_vista.db
177 |
178 | # Dump file
179 | *.stackdump
180 |
181 | # Folder config file
182 | [Dd]esktop.ini
183 |
184 | # Recycle Bin used on file shares
185 | $RECYCLE.BIN/
186 |
187 | # Windows Installer files
188 | *.cab
189 | *.msi
190 | *.msix
191 | *.msm
192 | *.msp
193 |
194 | # Windows shortcuts
195 | *.lnk
196 |
197 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,windows,macos,go,intellij
198 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Datasource local storage ignored files
5 | /../../../../../../../:\Users\jamesits\Documents\code\alwaysonline\.idea/dataSources/
6 | /dataSources.local.xml
7 | # Editor-based HTTP Client requests
8 | /httpRequests/
9 |
--------------------------------------------------------------------------------
/.idea/alwaysonline.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Dockerfile.goreleaser:
--------------------------------------------------------------------------------
1 | FROM scratch
2 | COPY alwaysonline /
3 | ENTRYPOINT ["/alwaysonline"]
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GLWTS(Good Luck With That Shit) Public License
2 | Copyright (c) Every-fucking-one, except the Author
3 |
4 | Everyone is permitted to copy, distribute, modify, merge, sell, publish,
5 | sublicense or whatever the fuck they want with this software but at their
6 | OWN RISK.
7 |
8 | Preamble
9 |
10 | The author has absolutely no fucking clue what the code in this project
11 | does. It might just fucking work or not, there is no third option.
12 |
13 |
14 | GOOD LUCK WITH THAT SHIT PUBLIC LICENSE
15 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION, AND MODIFICATION
16 |
17 | 0. You just DO WHATEVER THE FUCK YOU WANT TO as long as you NEVER LEAVE
18 | A FUCKING TRACE TO TRACK THE AUTHOR of the original product to blame for
19 | or held responsible.
20 |
21 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 | DEALINGS IN THE SOFTWARE.
25 |
26 | Good luck and Godspeed.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AlwaysOnline
2 |
3 | AlwaysOnline is a HTTP and DNS server which mocks a lot network/internet/portal detection servers.
4 |
5 | 
6 | 
7 | [](http://hub.docker.com/r/jamesits/alwaysonline)
8 |
9 | ## Usage
10 |
11 | Ports required: tcp/80, tcp/53, udp/53.
12 |
13 | Start the server:
14 |
15 | ```shell script
16 | # use docker
17 | docker run \
18 | -d --restart unless-stopped \
19 | --cap-drop ALL --cap-add NET_BIND_SERVICE \
20 | -p 80:80 -p 53:53 -p 53:53/udp \
21 | jamesits/alwaysonline:latest --ipv4 192.168.1.2 --ipv6 fd00::2
22 |
23 | # or download and run the executable
24 | chmod +x ./alwaysonline
25 | setcap cap_net_bind_service=+ep ./alwaysonline
26 | ./alwaysonline --ipv4 192.168.1.2 --ipv6 fd00::2
27 | ```
28 |
29 | (Pass the IP address end users use to connect to the server. See [Which IP Address Should I Use](#which-ip-address-should-i-use) for more information.)
30 |
31 | Hijack (delegate) the following domains (including subdomains) on your DNS server to the AlwaysOnline server IP address (and disable DNSSEC verification if possible):
32 |
33 | ```
34 | // Windows
35 | msftncsi.com
36 | msftconnecttest.com
37 | resolver1.opendns.com (if you use WindowsSpyBlocker to change NCSI config)
38 |
39 | // iOS, macOS
40 | captive.apple.com
41 |
42 | // Android
43 | clients3.google.com
44 | connectivitycheck.gstatic.com
45 | connectivitycheck.android.com
46 | connect.rom.miui.com
47 | connectivitycheck.platform.hicloud.com
48 | // www.googleapis.cn (for MCC=460 sim cards only; might impact other services; not recommended; see https://cs.android.com/android/platform/superproject/+/master:packages/modules/NetworkStack/res/values-mcc460/config.xml?q=values-mcc460)
49 |
50 | // Linux
51 | network-test.debian.org
52 | nmcheck.gnome.org
53 | www.archlinux.org (DO NOT hijack archlinux.org or other subdomains)
54 | capnet.elementary.io
55 | ```
56 |
57 | ## Development
58 |
59 | Building locally:
60 |
61 | ```shell
62 | goreleaser build --snapshot --clean --single-target
63 | ```
64 |
65 | ## FAQ / Technical Details
66 |
67 | ### Why?
68 |
69 | Microsoft Store refuses to work when NCSI reports "no Internet", even if it can load everything. Also some UWP games (e.g. ones authorized by Xbox Game Pass) refuse to work without a "Internet". But NCSI is so unreliable in my area that 99% of the time I have the disconnected globe icon on my taskbar. This is why I wrote this piece of software in the first place.
70 |
71 | By terminating portal detection servers in your LAN, you also benefit from:
72 |
73 | * Less wait when connecting to a network (mainly on Windows 10)
74 | * Less wired/wireless switching issues on Windows 10
75 | * More privacy (since your ISP and the OS vendor can no longer log these requests)
76 |
77 | ### Which IP Address Should I Use
78 |
79 | Whatever IP addresses you pass to the 2 parameters
80 |
81 | * `--ipv4`
82 | * `--ipv6`
83 |
84 | will be sent to the client in every DNS response, so that the following HTTP requests will come back to the IP addresses you specified. Generally speaking:
85 |
86 | * Use the IP addresses on the public-facing interface of your server
87 | * If the server is behind any destination NAT, reverse proxy or load balancer, use the IP address provided by the NAT/proxy/balancer service
88 | * If you don't have either IPv4 or IPv6 deployed, omit that
89 |
90 | The server will always listen on `0.0.0.0`/`[::]` regardless of the IP addresses you specified.
91 |
92 | ### Use the HTTP Server without DNS Delegation
93 |
94 | It is possible to use only the HTTP server provided by AlwaysOnline. Just make sure you fake every DNS responses right on your own DNS server.
95 |
96 | ### Security
97 |
98 | AlwaysOnline need `cap_net_bind_service=+ep` to listen on ports 53 and 80 as a non-root user. It is recommended to run AlwaysOnline in a confined, readonly environment.
99 |
100 | Exposing AlwaysOnline directly to the internet is not recommended, mainly because DNS/UDP can passively anticipate in an amplification/reflection DDoS attack. AlwaysOnline does not implement any rate limiting or abnormal client blocking mechanism.
101 |
102 | ### High Availability
103 |
104 | AlwaysOnline is stateless, so it is possible to run multiple instances to accommodate your high availability needs. Failover is realized by a 1-second DNS cache TTL and your own DNS server's failover capability.
105 |
106 | I acknowledge this is not the best way to implement HA, but it is simple and enough for casual deployments. If you use AlwaysOnline in a mission-critical environment, then external load balancer with upstream health check (e.g. Nginx or HAProxy) and/or distributed container orchestration tools (e.g. Docker Swarm Mode or Kubernetes) are strongly recommended.
107 |
108 | ### Extensibility
109 |
110 | AlwaysOnline replies to any URL segments it recognizes, regardless of which domain it comes from. If your client speaks a protocol AlwaysOnline implements but on a different domain, simply add another delegation on your DNS server and everything should work.
111 |
112 | ### About Windows 10 NCSI Service
113 |
114 | Service `NlaSvc` controls NCSI -- Network Connectivity Status Indicator, i.e. the tray icon on your taskbar showing whether you have Internet access. The service, when it goes wrong, is very annoying, as it will cause Microsoft Store to be unusable and all your UWP games unplayable even if you *actually* have Internet access.
115 |
116 | NCSI use a set of DNS and HTTP tests to detect if the device is connected to the Internet. The tests can be customized at `HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NlaSvc\Parameters\Internet`. AlwaysOnline implements the default config. (BTW, Windows 10 CMGE have the same NCSI config as every other Windows 10. )
117 |
118 | For a network to trigger the NCSI tests, you need an address, network mask and DNS server to be set. For IPv6 networks, the IP address need to be a global one (in the range `2000::/3`). Sometimes you need a default gateway, but not always.
119 |
120 | 
121 |
122 | [NCSI will cache negative results for a network](https://web.archive.org/web/20230815184404/https://learn.microsoft.com/en-us/answers/questions/400385/network-location-awareness-not-detecting-domain-ne), so if a network is detected to be non-Internet, NCSI will not test it for a long period, even if the network adapter is disabled then re-enabled.
123 |
124 | If you don't want to deploy a server or you don't have a suitable LAN environment, [NCSIOverride](https://github.com/dantmnf/NCSIOverride) can be used to fake NCSI results on a single machine.
125 |
126 | ### About Android
127 |
128 | Stock Android 7.1.1 or later will connect to both HTTP and HTTPS endpoints for internet detection. AlwaysOnline does not implement the HTTPS server due to obvious reasons.
129 |
130 | If you have privacy concerns, you can change the endpoint URLs manually with ADB. See: https://www.noisyfox.io/android-captive-portal.html
131 |
--------------------------------------------------------------------------------
/dns_server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "github.com/miekg/dns"
6 | "strings"
7 | )
8 |
9 | const DNSDefaultTTL = 1
10 |
11 | type dnsRequestHandler struct{}
12 |
13 | func newDNSReplyMsg() *dns.Msg {
14 | msg := dns.Msg{}
15 |
16 | msg.Compress = true
17 |
18 | // this is an authoritative DNS server
19 | msg.Authoritative = true
20 | msg.RecursionAvailable = false
21 |
22 | // DNSSEC disabled for now
23 | // TODO: fix DNSSEC
24 | msg.AuthenticatedData = false
25 | msg.CheckingDisabled = true
26 |
27 | return &msg
28 | }
29 |
30 | // send out the generated answer, and if the answer is not correct, send out a SERVFAIL
31 | func finishAnswer(w *dns.ResponseWriter, r *dns.Msg) {
32 | err := (*w).WriteMsg(r)
33 | if err != nil {
34 | softFailIf(err)
35 |
36 | // if answer sanity check (miekg/dns automatically does this) fails, reply with SERVFAIL
37 | msg := newDNSReplyMsg()
38 | msg.SetReply(r)
39 | msg.Rcode = dns.RcodeServerFailure
40 | err = (*w).WriteMsg(msg)
41 | softFailIf(err)
42 | }
43 |
44 | // access log to stdout
45 | fmt.Printf("[DNS] C=%d Q=%d R=%d DOMAIN=%s %s => %s\n", r.Question[0].Qclass, r.Question[0].Qtype, r.Rcode, r.Question[0].Name, (*w).RemoteAddr(), (*w).LocalAddr())
46 | }
47 |
48 | // TODO: force TCP for 1) clients which requests too fast; 2) non-existent answers
49 | // See: https://labs.apnic.net/?p=382
50 | func (this *dnsRequestHandler) ServeDNS(w dns.ResponseWriter, r *dns.Msg) {
51 | msg := newDNSReplyMsg()
52 | msg.SetReply(r)
53 |
54 | // on function return, we send out the current answer
55 | defer finishAnswer(&w, msg)
56 |
57 | // sanity check
58 | if len(r.Question) != 1 {
59 | msg.Rcode = dns.RcodeRefused
60 | return
61 | }
62 |
63 | switch r.Question[0].Qclass {
64 | case dns.ClassINET:
65 | switch r.Question[0].Qtype {
66 | case dns.TypeA:
67 | handleA(this, r, msg)
68 | return
69 |
70 | case dns.TypeAAAA:
71 | handleAAAA(this, r, msg)
72 | return
73 |
74 | case dns.TypeSOA:
75 | handleSOA(this, r, msg)
76 | return
77 |
78 | default:
79 | handleDefault(this, r, msg)
80 | return
81 | }
82 | case dns.ClassCHAOS:
83 | switch r.Question[0].Qtype {
84 | case dns.TypeTXT:
85 | if strings.EqualFold(r.Question[0].Name, "version.bind.") {
86 | // we need to reply our software version
87 | // https://serverfault.com/questions/517087/dns-how-to-find-out-which-software-a-remote-dns-server-is-running
88 | handleTXTVersionRequest(this, r, msg)
89 | } else {
90 | handleDefault(this, r, msg)
91 | }
92 | return
93 |
94 | default:
95 | handleDefault(this, r, msg)
96 | return
97 | }
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/dns_server_a.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/miekg/dns"
5 | "net"
6 | "strings"
7 | )
8 |
9 | func handleA(this *dnsRequestHandler, r, msg *dns.Msg) {
10 | switch strings.ToLower(msg.Question[0].Name) {
11 | case "dns.msftncsi.com.":
12 | msg.Answer = append(msg.Answer, &dns.A{
13 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
14 | A: net.IPv4(131, 107, 255, 255),
15 | })
16 | return
17 |
18 | case "resolver1.opendns.com.":
19 | // for https://github.com/crazy-max/WindowsSpyBlocker/blob/0e48685cf8c2b3f263f4ada9065188d6c9966cac/app/settings.json#L119
20 | msg.Answer = append(msg.Answer, &dns.A{
21 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
22 | A: net.IPv4(208, 67, 222, 222),
23 | })
24 | return
25 |
26 | default:
27 | if localResolveIp4Enabled {
28 | // for everything else, resolve to our own IP address
29 | msg.Answer = append(msg.Answer, &dns.A{
30 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
31 | A: localResolveIp4Address,
32 | })
33 | } else {
34 | // IPv4 not configured, reply empty answer
35 | msg.Answer = append(msg.Answer, &dns.A{
36 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
37 | })
38 | }
39 | return
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/dns_server_aaaa.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/miekg/dns"
5 | "net"
6 | "strings"
7 | )
8 |
9 | func handleAAAA(this *dnsRequestHandler, r, msg *dns.Msg) {
10 | switch strings.ToLower(msg.Question[0].Name) {
11 | case "dns.msftncsi.com.":
12 | msg.Answer = append(msg.Answer, &dns.AAAA{
13 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
14 | AAAA: net.ParseIP("fd3e:4f5a:5b81::1"),
15 | })
16 | return
17 |
18 | case "resolver1.opendns.com.":
19 | // for https://github.com/crazy-max/WindowsSpyBlocker/blob/0e48685cf8c2b3f263f4ada9065188d6c9966cac/app/settings.json#L119
20 | msg.Answer = append(msg.Answer, &dns.AAAA{
21 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
22 | AAAA: net.ParseIP("2620:119:35::35"),
23 | })
24 | return
25 |
26 | default:
27 | if localResolveIp6Enabled {
28 | // for everything else, resolve to our own IP address
29 | msg.Answer = append(msg.Answer, &dns.AAAA{
30 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
31 | AAAA: localResolveIp6Address,
32 | })
33 | } else {
34 | // IPv6 not configured, reply empty answer
35 | msg.Answer = append(msg.Answer, &dns.AAAA{
36 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
37 | })
38 | }
39 | return
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/dns_server_fallback.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/miekg/dns"
5 | "log"
6 | )
7 |
8 | // all unknown DNS requests are processed here
9 | func handleDefault(this *dnsRequestHandler, r, msg *dns.Msg) {
10 | if msg.RecursionDesired {
11 | // Refused
12 | msg.RecursionAvailable = false
13 | msg.Rcode = dns.RcodeRefused
14 |
15 | log.Printf("[DNS] %d %s refused: recursion requested but not available\n", msg.Question[0].Qtype, msg.Question[0].Name)
16 | } else {
17 | // NotImp
18 | msg.Rcode = dns.RcodeNotImplemented
19 |
20 | log.Printf("[DNS] %d %s refused: not implemented\n", msg.Question[0].Qtype, msg.Question[0].Name)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/dns_server_soa.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/miekg/dns"
5 | )
6 |
7 | func handleSOA(_ *dnsRequestHandler, r, msg *dns.Msg) {
8 | // When adding an upstream in Windows Server's DNS server, a SOA question to `.` (or the specific domain, if adding
9 | // as a conditional forwarder) will be generated to probe if the upstream is alive.
10 | // Reply this hardcoded answer to pass the test.
11 | msg.Answer = append(msg.Answer, &dns.SOA{
12 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
13 | Ns: "a.root-servers.net.",
14 | Mbox: "nstld.verisign-grs.com.",
15 | Serial: 114514,
16 | Refresh: 60,
17 | Retry: 10,
18 | Expire: 3600000,
19 | Minttl: DNSDefaultTTL,
20 | })
21 | }
22 |
--------------------------------------------------------------------------------
/dns_server_txt.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/miekg/dns"
5 | )
6 |
7 | // replies a TXT record containing server name and version
8 | func handleTXTVersionRequest(this *dnsRequestHandler, r, msg *dns.Msg) {
9 | msg.Answer = append(msg.Answer, &dns.TXT{
10 | Hdr: dns.RR_Header{Name: msg.Question[0].Name, Rrtype: r.Question[0].Qtype, Class: r.Question[0].Qclass, Ttl: DNSDefaultTTL},
11 | Txt: []string{"bind-⑨"},
12 | })
13 | }
14 |
--------------------------------------------------------------------------------
/doc/assets/windows10_20h2_ncsi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Jamesits/alwaysonline/f0919fec06253c59d48150e7c4d68001bc1e6475/doc/assets/windows10_20h2_ncsi.png
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | alwaysonline:
3 | build: "."
4 | image: "jamesits/alwaysonline:latest"
5 | restart: "unless-stopped"
6 | ports:
7 | - "80:80/tcp"
8 | - "53:53/tcp"
9 | - "53:53/udp"
10 | cap_add:
11 | - "NET_BIND_SERVICE"
12 | cap_drop:
13 | - "ALL"
14 | command: "--ipv4 192.168.1.2 --ipv6 fd00::2" # replace with your host's IP address
15 |
--------------------------------------------------------------------------------
/exceptions.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "log"
4 |
5 | var softErrorCount uint64
6 |
7 | // if QuitOnError is true, then panic;
8 | // else go on
9 | func softFailIf(e error) {
10 | if e != nil {
11 | softErrorCount++
12 | log.Printf("[ERROR] %s", e)
13 | }
14 | }
15 |
16 | func hardFailIf(e error) {
17 | if e != nil {
18 | panic(e)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/jamesits/alwaysonline/v2
2 |
3 | go 1.24.1
4 |
5 | require github.com/miekg/dns v1.1.66
6 |
7 | require (
8 | golang.org/x/mod v0.24.0 // indirect
9 | golang.org/x/net v0.39.0 // indirect
10 | golang.org/x/sync v0.13.0 // indirect
11 | golang.org/x/sys v0.32.0 // indirect
12 | golang.org/x/tools v0.32.0 // indirect
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
3 | github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE=
4 | github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE=
5 | golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
6 | golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
7 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
8 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
9 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
10 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
11 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
12 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
13 | golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
14 | golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
15 |
--------------------------------------------------------------------------------
/goreleaser.yaml:
--------------------------------------------------------------------------------
1 | # IDEA auto formatter is causing trouble
2 | # @formatter:off
3 | version: 2
4 |
5 | before:
6 | hooks:
7 | - "go mod verify"
8 |
9 | env:
10 | - "GO111MODULE=on"
11 | - "CGO_ENABLED=0"
12 |
13 | builds:
14 | - id: "alwaysonline"
15 | main: "."
16 | binary: "alwaysonline"
17 | mod_timestamp: "{{ .CommitTimestamp }}"
18 | goos: ["linux", "darwin", "windows"]
19 | goarch: ["amd64", "arm64"]
20 | goamd64: ["v3"]
21 | flags:
22 | - "-v"
23 | - "-trimpath"
24 | - "-buildvcs=true"
25 | asmflags:
26 | - "all=-trimpath={{ .Env.GOPATH }}"
27 | gcflags:
28 | - "all=-trimpath={{ .Env.GOPATH }}"
29 | ldflags:
30 | - "-s"
31 | - "-w"
32 | - "-X \"main.versionMajor={{ .Major }}\""
33 | - "-X \"main.versionMinor={{ .Minor }}\""
34 | - "-X \"main.versionRevision={{ .Patch }}\""
35 | - "-X \"main.versionGitCommitHash={{ .Commit }}\""
36 | - "-X \"main.versionCompileTime={{ .CommitTimestamp }}\""
37 | hooks:
38 | post:
39 | - "sh -c 'upx \"{{ .Path }}\" || true'"
40 | - "sudo setcap 'cap_net_bind_service=+ep' \"{{ .Path }}\""
41 |
42 | snapshot:
43 | version_template: "{{ incpatch .Version }}-next"
44 |
45 | archives:
46 | - id: "release"
47 | formats: ["tar.xz"]
48 | wrap_in_directory: true
49 |
50 | dockers:
51 | - dockerfile: "Dockerfile.goreleaser"
52 | goarch: amd64
53 | goamd64: v3
54 | image_templates:
55 | - jamesits/alwaysonline:{{ .Version }}-amd64
56 | build_flag_templates:
57 | - "--platform=linux/amd64"
58 | - dockerfile: "Dockerfile.goreleaser"
59 | goarch: arm64
60 | image_templates:
61 | - jamesits/alwaysonline:{{ .Version }}-arm64
62 | build_flag_templates:
63 | - "--platform=linux/arm64"
64 | docker_manifests:
65 | - name_template: "jamesits/alwaysonline:{{ .Version }}"
66 | image_templates:
67 | - jamesits/alwaysonline:{{ .Version }}-amd64
68 | - jamesits/alwaysonline:{{ .Version }}-arm64
69 |
70 | checksum:
71 | name_template: "checksums.txt"
72 | algorithm: "sha256"
73 |
74 | changelog:
75 | sort: "asc"
76 | filters:
77 | exclude:
78 | - "^doc:"
79 | - "^docs:"
80 | - "^test:"
81 | - "^cleanup:"
82 | - "^ci:"
83 | - "typo"
84 | - "readme"
85 | - "README"
86 | - "comment"
87 |
--------------------------------------------------------------------------------
/http_log.go:
--------------------------------------------------------------------------------
1 | // source: https://gist.github.com/cespare/3985516
2 | package main
3 |
4 | import (
5 | "fmt"
6 | "io"
7 | "net/http"
8 | "strings"
9 | "time"
10 | )
11 |
12 | type ApacheLogRecord struct {
13 | http.ResponseWriter
14 |
15 | ip string
16 | time time.Time
17 | method, uri, protocol string
18 | status int
19 | responseBytes int64
20 | elapsedTime time.Duration
21 | }
22 |
23 | func (r *ApacheLogRecord) Log(out io.Writer) {
24 | timeFormatted := r.time.Format("02/Jan/2006 03:04:05")
25 | fmt.Fprintf(out, "[HTTP] %s - - [%s] \"%s %s %s %d %d\" %f\n", r.ip, timeFormatted, r.method, r.uri, r.protocol, r.status, r.responseBytes, r.elapsedTime.Seconds())
26 | }
27 |
28 | func (r *ApacheLogRecord) Write(p []byte) (int, error) {
29 | written, err := r.ResponseWriter.Write(p)
30 | r.responseBytes += int64(written)
31 | return written, err
32 | }
33 |
34 | func (r *ApacheLogRecord) WriteHeader(status int) {
35 | r.status = status
36 | r.ResponseWriter.WriteHeader(status)
37 | }
38 |
39 | type ApacheLoggingHandler struct {
40 | handler http.Handler
41 | out io.Writer
42 | }
43 |
44 | func NewApacheLoggingHandler(handler http.Handler, out io.Writer) http.Handler {
45 | return &ApacheLoggingHandler{
46 | handler: handler,
47 | out: out,
48 | }
49 | }
50 |
51 | func (h *ApacheLoggingHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
52 | clientIP := r.RemoteAddr
53 | if colon := strings.LastIndex(clientIP, ":"); colon != -1 {
54 | clientIP = clientIP[:colon]
55 | }
56 |
57 | record := &ApacheLogRecord{
58 | ResponseWriter: rw,
59 | ip: clientIP,
60 | time: time.Time{},
61 | method: r.Method,
62 | uri: r.RequestURI,
63 | protocol: r.Proto,
64 | status: http.StatusOK,
65 | elapsedTime: time.Duration(0),
66 | }
67 |
68 | startTime := time.Now()
69 | h.handler.ServeHTTP(record, r)
70 | finishTime := time.Now()
71 |
72 | record.time = finishTime.UTC()
73 | record.elapsedTime = finishTime.Sub(startTime)
74 |
75 | record.Log(h.out)
76 | }
77 |
--------------------------------------------------------------------------------
/http_server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "strings"
8 | )
9 |
10 | func http_server_fallback(w http.ResponseWriter, req *http.Request) {
11 | switch strings.ToLower(req.Host) {
12 | case "captive.apple.com":
13 | hotspot_detect_html(w, req)
14 | return
15 |
16 | case "capnet.elementary.io":
17 | capnet(w, req)
18 | return
19 |
20 | case "www.archlinux.org":
21 | // mock the redirect
22 | w.Header().Add("Location", "https://archlinux.org"+req.RequestURI)
23 | w.WriteHeader(http.StatusMovedPermanently)
24 | fmt.Fprint(w, "\n
301 Moved Permanently \n\n301 Moved Permanently \nnginx \n\n\n")
25 | return
26 |
27 | case "networkcheck.kde.org":
28 | ok_upcase(w, req)
29 | return
30 |
31 | case "connectivitycheck.platform.hicloud.com":
32 | // Huawei phones generate requests like "http://connectivitycheck.platform.hicloud.com/generate_204_1ee9b362-b226-4c81-bffe-6708fa241ab8"
33 | // and the UUID is different every time
34 | generate_204(w, req)
35 | return
36 |
37 | default:
38 | // fallback to a 404 page
39 | log.Printf("[HTTP] not implemented: %s %s => \"%s%s\"\n", req.Method, req.RemoteAddr, req.Host, req.RequestURI)
40 | w.WriteHeader(http.StatusNotFound)
41 | }
42 | }
43 |
44 | // /robots.txt
45 | func robots_txt(w http.ResponseWriter, req *http.Request) {
46 | w.Header().Add("Content-Type", "text/plain; charset=utf-8")
47 | w.WriteHeader(http.StatusOK)
48 | fmt.Fprint(w, "User-agent: *\nDisallow: /\n")
49 | }
50 |
51 | // http://www.msftncsi.com/ncsi.txt
52 | func ncsi_txt(w http.ResponseWriter, req *http.Request) {
53 | w.Header().Add("Content-Type", "text/plain")
54 | w.WriteHeader(http.StatusOK)
55 | fmt.Fprint(w, "Microsoft NCSI")
56 | }
57 |
58 | // http://www.msftconnecttest.com/connecttest.txt
59 | // http://ipv6.msftconnecttest.com/connecttest.txt
60 | func connecttest(w http.ResponseWriter, req *http.Request) {
61 | w.Header().Add("Content-Type", "text/plain; charset=utf-8")
62 | w.WriteHeader(http.StatusOK)
63 | fmt.Fprint(w, "Microsoft Connect Test")
64 | }
65 |
66 | // http://www.msftconnecttest.com/redirect
67 | func redirect(w http.ResponseWriter, req *http.Request) {
68 | w.Header().Add("Location", "http://go.microsoft.com/fwlink/?LinkID=219472&clcid=0x409")
69 | w.Header().Add("Server", "Microsoft-IIS/114.514")
70 | w.Header().Add("Content-Length", "0")
71 | w.WriteHeader(http.StatusFound)
72 | w.Write([]byte{})
73 | }
74 |
75 | // http://captive.apple.com/hotspot-detect.html
76 | func hotspot_detect_html(w http.ResponseWriter, req *http.Request) {
77 | w.Header().Add("Content-Type", "text/html")
78 | w.WriteHeader(http.StatusOK)
79 | fmt.Fprint(w, "Success Success")
80 | }
81 |
82 | // http://clients3.google.com/generate_204
83 | // http://connectivitycheck.gstatic.com/generate_204
84 | // http://connectivitycheck.android.com/generate_204
85 | // http://connect.rom.miui.com/generate_204
86 | func generate_204(w http.ResponseWriter, req *http.Request) {
87 | w.Header().Add("Content-Length", "0")
88 | w.WriteHeader(http.StatusNoContent)
89 | w.Write([]byte{})
90 | }
91 |
92 | // http://network-test.debian.org/nm
93 | // http://ping.archlinux.org/nm-check.txt
94 | // http://nmcheck.gnome.org/check_network_status.txt
95 | // http://www.archlinux.org/check_network_status.txt
96 | func nm(w http.ResponseWriter, req *http.Request) {
97 | w.Header().Add("X-NetworkManager-Status", "online")
98 | w.WriteHeader(http.StatusOK)
99 | fmt.Fprint(w, "NetworkManager is online\n")
100 | }
101 |
102 | // http://networkcheck.kde.org
103 | func ok_upcase(w http.ResponseWriter, req *http.Request) {
104 | w.Header().Add("X-NetworkManager-Status", "online")
105 | w.WriteHeader(http.StatusOK)
106 | fmt.Fprint(w, "OK\n")
107 | }
108 |
109 | // http://detectportal.firefox.com/success.txt
110 | func success_txt(w http.ResponseWriter, req *http.Request) {
111 | w.WriteHeader(http.StatusOK)
112 | fmt.Fprint(w, "success\n")
113 | }
114 |
115 | // http://start.ubuntu.com/connectivity-check.html
116 | func connectivity_check_html(w http.ResponseWriter, req *http.Request) {
117 | w.WriteHeader(http.StatusOK)
118 | fmt.Fprint(w, "\n\n\nLorem Ipsum \n\n\nLorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
\n\n\n")
119 | }
120 |
121 | // http://capnet.elementary.io
122 | // warning: relative resources exist
123 | func capnet(w http.ResponseWriter, req *http.Request) {
124 | w.WriteHeader(http.StatusOK)
125 | fmt.Fprint(w, "\n\n\n\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \nYou're connected! ⋅ elementary \n \n \n \n \n \n \n \n \n\n\n\n\n \n\n
\n
You're connected! \n
Your Internet connection appears to be working. You can safely close this window and continue using your device.
\n
Why is this window appearing? \n
elementary OS automatically checks your Internet connection when you connect to a new Wi-Fi network. If it detects there is not an Internet connection (i.e. if you are connecting to a captive portal at a coffee shop or other public location), this window will appear and display the login page.
\n
Some networks can appear to be a captive portal at first, triggering this window, then actually end up connecting. In those cases, you'll see this message and can safely close the window.
\n
\n
\n\n\n\n")
126 | }
127 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "github.com/miekg/dns"
7 | "log"
8 | "net"
9 | "net/http"
10 | "os"
11 | "sync"
12 | )
13 |
14 | var mainThreadWaitGroup = &sync.WaitGroup{}
15 | var disableDNSServer bool
16 | var localDNSPortString *string
17 | var localDNSPort int
18 | var localDNSPortInvaild bool
19 | var localResolveIp4AddressString *string
20 | var localResolveIp4Address net.IP
21 | var localResolveIp4Enabled bool
22 | var localResolveIp6AddressString *string
23 | var localResolveIp6Address net.IP
24 | var localResolveIp6Enabled bool
25 | var localResolvePortString *string
26 | var localResolvePort int
27 | var localResolvePortInvaild bool
28 | var showVersionOnly *bool
29 |
30 | func main() {
31 |
32 | // arguments parsing
33 | flag.BoolVar(&disableDNSServer, "disable-dns-server", false, "disable DNS server")
34 | localDNSPortString = flag.String("dns-port", "", "listening port of the DNS server")
35 | localResolveIp4AddressString = flag.String("ipv4", "", "the IPv4 address to this server")
36 | localResolveIp6AddressString = flag.String("ipv6", "", "the IPv6 address to this server")
37 | localResolvePortString = flag.String("port", "", "listening port of server")
38 | showVersionOnly = flag.Bool("version", false, "show version and quit")
39 | flag.Parse()
40 |
41 | if *showVersionOnly {
42 | fmt.Println(getVersionFullString())
43 | return
44 | } else {
45 | log.Println(getVersionFullString())
46 | }
47 |
48 | if len(*localResolveIp4AddressString) == 0 {
49 | localResolveIp4Enabled = false
50 | localResolveIp4Address = net.ParseIP("0.0.0.0")
51 | log.Println("[CONFIG] IPv4 resolution disabled")
52 | } else {
53 | localResolveIp4Enabled = true
54 | localResolveIp4Address = net.ParseIP(*localResolveIp4AddressString)
55 | log.Printf("[CONFIG] Local server IPv4 address: %s\n", localResolveIp4Address)
56 | }
57 |
58 | if len(*localResolveIp6AddressString) == 0 {
59 | localResolveIp6Enabled = false
60 | localResolveIp6Address = net.ParseIP("::")
61 | log.Println("[CONFIG] IPv6 resolution disabled")
62 | } else {
63 | localResolveIp6Enabled = true
64 | localResolveIp6Address = net.ParseIP(*localResolveIp6AddressString)
65 | log.Printf("[CONFIG] Local server IPv6 address: %s\n", localResolveIp6Address)
66 | }
67 |
68 | if len(*localResolvePortString) == 0 {
69 | localResolvePort = 80
70 | log.Println("[CONFIG] Listen port: 80")
71 | } else {
72 | localResolvePort, localResolvePortInvaild = parsePort(*localResolvePortString)
73 | if localResolvePortInvaild || localResolvePort == 0 {
74 | localResolvePort = 80
75 | }
76 | log.Printf("[CONFIG] Listen port: %d\n", localResolvePort)
77 | }
78 |
79 | if len(*localDNSPortString) == 0 {
80 | localDNSPort = 53
81 | log.Println("[CONFIG] DNS port: 53")
82 | } else {
83 | localDNSPort, localDNSPortInvaild = parsePort(*localDNSPortString)
84 | if localDNSPortInvaild || localDNSPort == 0 {
85 | localDNSPort = 53
86 | }
87 | log.Printf("[CONFIG] DNS port: %d\n", localDNSPort)
88 | }
89 |
90 | // HTTP router setup
91 | mux := http.DefaultServeMux
92 | mux.HandleFunc("/robots.txt", robots_txt)
93 | mux.HandleFunc("/ncsi.txt", ncsi_txt)
94 | mux.HandleFunc("/redirect", redirect)
95 | mux.HandleFunc("/hotspot-detect.html", hotspot_detect_html)
96 | mux.HandleFunc("/generate_204", generate_204)
97 | mux.HandleFunc("/gen_204", generate_204)
98 | mux.HandleFunc("/nm", nm)
99 | mux.HandleFunc("/nm-check.txt", nm)
100 | mux.HandleFunc("/check_network_status.txt", nm)
101 | mux.HandleFunc("/success.txt", success_txt)
102 | mux.HandleFunc("/connecttest.txt", connecttest)
103 | mux.HandleFunc("/connectivity-check.html", connectivity_check_html)
104 | mux.HandleFunc("/", http_server_fallback) // catch all
105 |
106 | // HTTP logger setup
107 | loggingHandler := NewApacheLoggingHandler(mux, os.Stdout) // HTTP access log is sent to stdout for now
108 |
109 | // HTTP server setup
110 | if !(localResolveIp4Enabled || localResolveIp6Enabled) {
111 | openPlainHttpServer(fmt.Sprintf(":%d", localResolvePort), loggingHandler)
112 | }
113 | if localResolveIp4Enabled {
114 | openPlainHttpServer(fmt.Sprintf("%s:%d", localResolveIp4Address.String(), localResolvePort), loggingHandler)
115 | }
116 | if localResolveIp6Enabled {
117 | openPlainHttpServer(fmt.Sprintf("[%s]:%d", localResolveIp6Address.String(), localResolvePort), loggingHandler)
118 | }
119 |
120 | // HTTPS server setup
121 | // tlsHttpServer := &http.Server{
122 | // Addr: ":443",
123 | // Handler: loggingHandler,
124 | // TLSConfig: &tls.Config{
125 | // GetCertificate: ,
126 | // // ...
127 | // }
128 | // }
129 | // go tlsHttpServer.ListenAndServe()
130 |
131 | if !disableDNSServer {
132 | // DNS TCP server setup
133 | dnsTcp1 := &dns.Server{Addr: fmt.Sprintf(":%d", localDNSPort), Net: "tcp"}
134 | dnsTcp1.Handler = &dnsRequestHandler{}
135 | go dnsTcp1.ListenAndServe()
136 |
137 | // DNS UDP server setup
138 | dnsUdp1 := &dns.Server{Addr: fmt.Sprintf(":%d", localDNSPort), Net: "udp"}
139 | dnsUdp1.Handler = &dnsRequestHandler{}
140 | go dnsUdp1.ListenAndServe()
141 | } else {
142 | log.Println("[MAIN] not starting DNS server")
143 | }
144 |
145 | // done
146 | log.Println("[MAIN] Server started.")
147 |
148 | // just a normal while(1)
149 | mainThreadWaitGroup.Add(1)
150 | mainThreadWaitGroup.Wait()
151 | }
152 |
153 | func openPlainHttpServer(addr string, handler http.Handler) {
154 | plainHttpServer := &http.Server{
155 | Addr: addr,
156 | Handler: handler,
157 | }
158 | go plainHttpServer.ListenAndServe()
159 | }
160 |
--------------------------------------------------------------------------------
/port.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 | )
6 |
7 | func parsePort(service string) (port int, error bool) {
8 | prt, err := strconv.ParseUint(service, 10, 16)
9 | if err != nil {
10 | return 0, true
11 | }
12 | return int(prt), false
13 | }
14 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:recommended",
5 | "github>jamesits/.github//renovate/default",
6 | "github>jamesits/.github//renovate/group-monorepo"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/version.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "fmt"
4 |
5 | var versionMajor = "0"
6 | var versionMinor = "0"
7 | var versionRevision = "0"
8 | var versionGitCommitHash string
9 | var versionCompileTime string
10 |
11 | func getVersionNumberString() string {
12 | return fmt.Sprintf("%s.%s.%s", versionMajor, versionMinor, versionRevision)
13 | }
14 |
15 | func getVersionFullString() string {
16 | if len(versionGitCommitHash) == 0 {
17 | versionGitCommitHash = "UNKNOWN"
18 | }
19 |
20 | return fmt.Sprintf("AlwaysOnline/%s (+https://github.com/Jamesits/alwaysonline; Commit %s/%s)", getVersionNumberString(), versionGitCommitHash, versionCompileTime)
21 | }
22 |
--------------------------------------------------------------------------------