From 32136686611f62ef673ebf9e59d9f5b14e1e50ef Mon Sep 17 00:00:00 2001 From: Nitin Jain Date: Sat, 15 Jul 2023 19:46:36 +0000 Subject: [PATCH] Combine server and client to one binary Most of dependency size is coming from wireguard, so adding server to client binary adds <5% overhead. --- .github/workflows/release.yml | 2 +- Dockerfile | 12 ++----- README.md | 12 +++---- tunwg/tunwg.go | 15 ++++++++ {tunwgs => tunwg}/tunwgs.go | 68 +++++++++++++++++------------------ 5 files changed, 56 insertions(+), 53 deletions(-) rename {tunwgs => tunwg}/tunwgs.go (83%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 724c74b..d64f570 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: with: go-version-file: go.mod - run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o bin/tunwg ./tunwg - - run: CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-w -s" -o bin/tunwgq-darwin ./tunwg + - run: CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags="-w -s" -o bin/tunwg-darwin ./tunwg - run: CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-w -s" -o bin/tunwg.exe ./tunwg - run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-w -s" -o bin/tunwg-arm64 ./tunwg - run: CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags="-w -s" -o bin/tunwg-darwin-arm64 ./tunwg diff --git a/Dockerfile b/Dockerfile index 5d44a0e..db1d978 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,23 +7,15 @@ COPY go.sum ./ RUN go mod download COPY . ./ -RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags="-w -s" -o bin/ ./tunwgs ./tunwg - -FROM alpine as upx - -RUN apk add --no-cache upx - -COPY --from=build /app/bin/ /app/bin/ -RUN upx --best /app/bin/* +RUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags="-w -s" -o bin/ ./tunwg FROM alpine RUN apk add -U --no-cache ca-certificates COPY --from=build /app/bin/ /bin/ -# COPY --from=upx /app/bin/ /bin/ VOLUME /data ENV TUNWG_PATH=/data -CMD ["/bin/tunwg"] \ No newline at end of file +ENTRYPOINT ["/bin/tunwg"] \ No newline at end of file diff --git a/README.md b/README.md index bde8906..d00fca3 100644 --- a/README.md +++ b/README.md @@ -106,8 +106,8 @@ Since anyone can run a server on `l.tunwg.com` domain, be careful when using coo The instance at `l.tunwg.com` runs on a VPS with very limited resources and may be bandwidth limited. For critical use cases, you can self-host your own tunwg server. ```bash -go install github.com/ntnj/tunwg/tunwgs@latest -TUNWG_API=example.com TUNWG_IP= TUNWG_PORT= tunwgs +go install github.com/ntnj/tunwg/tunwg@latest +TUNWG_RUN_SERVER=true TUNWG_API=example.com TUNWG_IP= TUNWG_PORT= tunwg ``` With docker: @@ -115,9 +115,9 @@ With docker: tunwgs: image: ghcr.io/ntnj/tunwg network_mode: host # or ports, 80,443,443/udp - command: tunwgs environment: - TUNWG_PORT: 443 + TUNWG_RUN_SERVER: true + TUNWG_PORT: 443 # udp port that is used for wireguard connections. TUNWG_IP: "a.b.c.d" # ip of server TUNWG_API: example.com # all subdomains should resolve to server ``` @@ -142,11 +142,11 @@ The generated domain name is an encoding of the internal wireguard IP address an ### Develop locally -Run server: `TUNWG_TEST_LOCALHOST=true TUNWG_PORT=443 TUNWG_IP=127.0.0.1 go run ./tunwgs` +Run server: `TUNWG_TEST_LOCALHOST=true TUNWG_RUN_SERVER=true TUNWG_KEY=tunwgs TUNWG_PORT=443 TUNWG_IP=127.0.0.1 go run ./tunwg` Run client: `TUNWG_TEST_LOCALHOST=true go run ./tunwg --forward=http://localhost:8000` -Test: `curl -k -Li --resolve abcd.l.tunwg.com:443:127.0.0.1 https://abcd.l.tunwg.com` +Test: `curl -k -Li --connect-to ::127.0.0.1: https://abcd.l.tunwg.com` ## Possible Future Improvements diff --git a/tunwg/tunwg.go b/tunwg/tunwg.go index df76ef8..7e0c253 100644 --- a/tunwg/tunwg.go +++ b/tunwg/tunwg.go @@ -26,6 +26,21 @@ var limitFlag = flag.String("limit", "", "username password in htpasswd format. var portFlag = flag.Uint("p", 0, "port to forward") func main() { + if os.Getenv("TUNWG_RUN_SERVER") == "true" { + tunwgServer() + return + } + if len(os.Args) > 1 && (os.Args[1] == "tunwgs" || os.Args[1] == "/bin/tunwgs") { + // Reproduce the older docker environment + os.Args = append(os.Args[:1], os.Args[2:]...) + if os.Getenv("TUNWG_KEY") == "" { + os.Setenv("TUNWG_KEY", "tunwgs") + } + tunwgServer() + return + } else if len(os.Args) > 1 && os.Args[1] == "tunwg" { + os.Args = append(os.Args[:1], os.Args[2:]...) + } flag.Parse() if (*forwardFlag == "") == (*portFlag == 0) { log.Fatalf("Specify one of port to forward (-p) or urls to forward (--forward)") diff --git a/tunwgs/tunwgs.go b/tunwg/tunwgs.go similarity index 83% rename from tunwgs/tunwgs.go rename to tunwg/tunwgs.go index b2255ac..2254c22 100644 --- a/tunwgs/tunwgs.go +++ b/tunwg/tunwgs.go @@ -25,7 +25,7 @@ import ( "inet.af/tcpproxy" ) -func main() { +func tunwgServer() { flag.Parse() if internal.GetListenPort() <= 0 { log.Fatalf("TUNWG_PORT needs to be set") @@ -35,46 +35,22 @@ func main() { if err := internal.Initialize(); err != nil { log.Fatalf("failed to initialize: %v", err) } - l, err := internal.ListenTCPWg(&net.TCPAddr{Port: 443}) - if err != nil { - log.Fatalf("failed to listen: %v", err) - } + l443 := &tcpproxy.TargetListener{Address: "https"} go func() { - if err := http.Serve(tls.NewListener(l, internal.GetTLSConfig()), apiMux()); err != nil { + if err := http.Serve(tls.NewListener(l443, internal.GetTLSConfig()), apiMux()); err != nil { log.Fatalf("failed to serve api: %v", err) } }() + l80 := &tcpproxy.TargetListener{Address: "http"} go func() { - if err := http.ListenAndServe(":80", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") { - http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently) - return - } - rp := &httputil.ReverseProxy{ - Rewrite: func(pr *httputil.ProxyRequest) { - ipport, err := getIPForDomain(pr.In.Host) - if err != nil { - log.Printf("unable to find host: %v", pr.In.Host) - w.WriteHeader(http.StatusNotFound) - return - } - newPort := netip.AddrPortFrom(ipport.Addr(), 80) - pr.Out.URL.Scheme = "http" - pr.Out.URL.Host = fmt.Sprintf("%v", newPort.String()) - }, - Transport: &http.Transport{ - DialContext: internal.DialWg, - }, - } - rp.ServeHTTP(w, r) - })); err != nil { + if err := http.Serve(l80, sslRedirect()); err != nil { log.Fatalf("failed to serve redirect handler: %v", err) } }() go globalPersist.loadFromDisk() go globalPersist.backgroundWriter(time.Minute) go internal.BackgroundLogger(10 * time.Second) - log.Fatalf("failed to run: %v", runSniProxy()) + log.Fatalf("failed to run: %v", runSniProxy(l80, l443)) } func allowUserKey(key wgtypes.Key, endpoint string) error { @@ -88,6 +64,29 @@ func allowUserKey(key wgtypes.Key, endpoint string) error { return internal.WgSetIpc(ipc) } +func sslRedirect() *http.ServeMux { + mux := http.NewServeMux() + mux.Handle("/.well-known/acme-challenge/", &httputil.ReverseProxy{ + Rewrite: func(pr *httputil.ProxyRequest) { + ipport, err := getIPForDomain(pr.In.Host) + if err != nil { + log.Printf("unable to find host: %v", pr.In.Host) + return + } + newPort := netip.AddrPortFrom(ipport.Addr(), 80) + pr.Out.URL.Scheme = "http" + pr.Out.URL.Host = fmt.Sprintf("%v", newPort.String()) + }, + Transport: &http.Transport{ + DialContext: internal.DialWg, + }, + }) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusMovedPermanently) + }) + return mux +} + func apiMux() *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request) { @@ -160,13 +159,10 @@ func apiMux() *http.ServeMux { return mux } -func runSniProxy() error { +func runSniProxy(l80, l443 *tcpproxy.TargetListener) error { var proxy tcpproxy.Proxy - proxy.AddSNIRoute(":443", internal.ApiDomain(), &tcpproxy.DialProxy{ - Addr: fmt.Sprintf("[%v]:443", internal.GetLocalWgIp()), - DialContext: internal.DialWg, - DialTimeout: time.Second, - }) + proxy.AddRoute(":80", l80) + proxy.AddSNIRoute(":443", internal.ApiDomain(), l443) proxy.AddSNIRouteFunc(":443", func(ctx context.Context, sniName string) (tcpproxy.Target, bool) { log.Printf("received request for: %v", sniName) addr, err := getIPForDomain(sniName)