Skip to content

Commit

Permalink
Combine server and client to one binary
Browse files Browse the repository at this point in the history
Most of dependency size is coming from wireguard, so adding server to client binary adds <5% overhead.
  • Loading branch information
ntnj committed Jul 15, 2023
1 parent 1d7f03b commit 3213668
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 2 additions & 10 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
ENTRYPOINT ["/bin/tunwg"]
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,18 @@ 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=<ip-of-server> TUNWG_PORT=<wireguard-port> tunwgs
go install github.com/ntnj/tunwg/tunwg@latest
TUNWG_RUN_SERVER=true TUNWG_API=example.com TUNWG_IP=<ip-of-server> TUNWG_PORT=<wireguard-port> tunwg
```

With docker:
```docker-compose.yml
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
```
Expand All @@ -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

Expand Down
15 changes: 15 additions & 0 deletions tunwg/tunwg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
Expand Down
68 changes: 32 additions & 36 deletions tunwgs/tunwgs.go → tunwg/tunwgs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 3213668

Please sign in to comment.