Skip to content

Commit 7475e44

Browse files
committed
feat: enhance client IP resolution
1 parent 199142e commit 7475e44

File tree

3 files changed

+65
-2
lines changed

3 files changed

+65
-2
lines changed

CLAUDE.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,6 @@ The entire backend is in `main.go`. Web assets (`web/index.html`, `web/script.js
4848

4949
**Versioning:** The version string is injected at build time via `-ldflags "-X main.Version=$(VERSION)"` and exposed via `/api/version`. The Docker image receives it via the `VERSION` build arg.
5050

51-
**Docker:** A multi-stage `Dockerfile` uses `ARG GO_VERSION` and `ARG ALPINE_VERSION` to parameterise the base images (`golang:${GO_VERSION}-alpine` / `alpine:${ALPINE_VERSION}`). The canonical values live in the top-level `env:` block of `release.yml` (`GO_VERSION`, `ALPINE_VERSION`) and are passed as build args by the workflow — one place to bump either version. Build locally: `docker build --build-arg VERSION=x.y.z -t local-clipboard .`. Run: `docker run -p 8080:8080 local-clipboard`.
51+
**Client IP resolution:** The `realIP` helper in `main.go` resolves the real client IP by checking `X-Forwarded-For` (first entry in the comma-separated list) then `X-Real-IP`, falling back to `r.RemoteAddr`. This is necessary when running behind a reverse proxy or Docker, where NAT would otherwise cause all clients to appear as the bridge gateway IP (e.g. `172.18.0.1`). The WebSocket handler uses `realIP(r)` when registering each connection.
52+
53+
**Docker:** A multi-stage `Dockerfile` uses `ARG GO_VERSION` and `ARG ALPINE_VERSION` to parameterise the base images (`golang:${GO_VERSION}-alpine` / `alpine:${ALPINE_VERSION}`). The canonical values live in the top-level `env:` block of `release.yml` (`GO_VERSION`, `ALPINE_VERSION`) and are passed as build args by the workflow — one place to bump either version. Build locally: `docker build --build-arg VERSION=x.y.z -t local-clipboard .`. Run: `docker run -p 8080:8080 local-clipboard`. When running via Docker Compose behind a reverse proxy, configure the proxy to set `X-Forwarded-For` or `X-Real-IP` so client IPs are preserved correctly.

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,49 @@ go mod vendor
111111

112112
This will download all dependencies into a `vendor/` directory for offline use.
113113

114+
## Docker
115+
116+
### Run with Docker
117+
118+
```bash
119+
docker run -p 8080:8080 ghcr.io/mokhajavi75/local-clipboard:latest
120+
```
121+
122+
### Run with Docker Compose
123+
124+
```yaml
125+
services:
126+
local-clipboard:
127+
image: ghcr.io/mokhajavi75/local-clipboard:latest
128+
restart: unless-stopped
129+
ports:
130+
- "8080:8080"
131+
```
132+
133+
### Behind a Reverse Proxy
134+
135+
When running behind a reverse proxy, configure it to forward the real client IP so sender labels show correctly instead of the Docker gateway IP.
136+
137+
**Caddy:**
138+
139+
```caddy
140+
example.com {
141+
reverse_proxy 127.0.0.1:8080 {
142+
header_up X-Real-IP {remote_host}
143+
}
144+
}
145+
```
146+
147+
> Caddy sets `X-Forwarded-For` automatically; the `header_up` line adds `X-Real-IP` as well.
148+
149+
**nginx:**
150+
151+
```nginx
152+
location / {
153+
proxy_pass http://127.0.0.1:8080;
154+
proxy_set_header X-Forwarded-For $remote_addr;
155+
proxy_set_header X-Real-IP $remote_addr;
156+
}
157+
```
158+
114159
Enjoy your local clipboard! 🎉

main.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"net"
1212
"net/http"
1313
"os"
14+
"strings"
1415
"sync"
1516
"time"
1617

@@ -307,7 +308,7 @@ func (h *Hub) handleWebSocket(w http.ResponseWriter, r *http.Request) {
307308
return
308309
}
309310

310-
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
311+
ip := realIP(r)
311312
h.register <- connInfo{conn: conn, ip: ip}
312313

313314
defer func() {
@@ -333,6 +334,21 @@ func (h *Hub) handleWebSocket(w http.ResponseWriter, r *http.Request) {
333334
}
334335
}
335336

337+
func realIP(r *http.Request) string {
338+
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
339+
// X-Forwarded-For can be a comma-separated list; the first entry is the client
340+
if i := strings.Index(xff, ","); i != -1 {
341+
return strings.TrimSpace(xff[:i])
342+
}
343+
return strings.TrimSpace(xff)
344+
}
345+
if xri := r.Header.Get("X-Real-IP"); xri != "" {
346+
return strings.TrimSpace(xri)
347+
}
348+
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
349+
return ip
350+
}
351+
336352
func getLocalIP() string {
337353
ifaces, err := net.Interfaces()
338354
if err != nil {

0 commit comments

Comments
 (0)