run as non root and non host network mode

This commit is contained in:
yusing 2025-01-31 06:32:09 +08:00
parent 357897a0cd
commit b3290e2665
15 changed files with 107 additions and 73 deletions

View file

@ -1,6 +1,9 @@
# set timezone to get correct log timestamp # set timezone to get correct log timestamp
TZ=ETC/UTC TZ=ETC/UTC
# Enable Prometheus Metrics
GODOXY_PROMETHEUS_ENABLED=true
# API/WebUI user password login credentials (optional) # API/WebUI user password login credentials (optional)
# These fields are not required for OIDC authentication # These fields are not required for OIDC authentication
GODOXY_API_USER=admin GODOXY_API_USER=admin
@ -35,15 +38,5 @@ GODOXY_API_JWT_TOKEN_TTL=1h
# Optional: Comma-separated list of allowed groups. # Optional: Comma-separated list of allowed groups.
# GODOXY_OIDC_ALLOWED_GROUPS=group1,group2 # GODOXY_OIDC_ALLOWED_GROUPS=group1,group2
# Proxy listening address
GODOXY_HTTP_ADDR=:80
GODOXY_HTTPS_ADDR=:443
# API listening address
GODOXY_API_ADDR=127.0.0.1:8888
# Prometheus Metrics listening address (uncomment to enable)
#GODOXY_PROMETHEUS_ADDR=:8889
# Debug mode # Debug mode
GODOXY_DEBUG=false GODOXY_DEBUG=false

View file

@ -1,10 +1,11 @@
# trunk-ignore-all(checkov/CKV_DOCKER_3)
# Stage 1: Builder # Stage 1: Builder
FROM golang:1.23.5-alpine AS builder FROM golang:1.23.5-alpine AS builder
HEALTHCHECK NONE HEALTHCHECK NONE
# package version does not matter # package version does not matter
# trunk-ignore(hadolint/DL3018) # trunk-ignore(hadolint/DL3018)
RUN apk add --no-cache tzdata make libcap-setcap RUN apk add --no-cache make
WORKDIR /src WORKDIR /src
@ -35,30 +36,36 @@ RUN --mount=type=cache,target="/go/pkg/mod" \
mv bin/godoxy /app/godoxy mv bin/godoxy /app/godoxy
# Stage 2: Final image # Stage 2: Final image
FROM scratch FROM alpine:3
LABEL maintainer="yusing@6uo.me" LABEL maintainer="yusing@6uo.me"
LABEL proxy.exclude=1 LABEL proxy.exclude=1
# copy timezone data # trunk-ignore(hadolint/DL3018)
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo RUN apk add --no-cache tzdata ca-certificates runuser libcap-setcap socat
# copy binary # copy binary
COPY --from=builder /app /app COPY --from=builder /app /app
# copy startup script
COPY scripts/docker-start.sh /app/docker-start.sh
RUN chmod +x /app/docker-start.sh
# copy example config # copy example config
COPY config.example.yml /app/config/config.yml COPY config.example.yml /app/config/config.yml
# copy certs ENV SOCKET_FORK=/app/forked.sock
COPY --from=builder /etc/ssl/certs /etc/ssl/certs ENV DOCKER_HOST=unix://${SOCKET_FORK}
ENV DOCKER_HOST=unix:///var/run/docker.sock
ENV GODOXY_DEBUG=0 ENV GODOXY_DEBUG=0
ENV PUID=1002
ENV PGID=1002
EXPOSE 80 EXPOSE 80
EXPOSE 8888 EXPOSE 8888
EXPOSE 443 EXPOSE 443
WORKDIR /app WORKDIR /app
CMD ["/app/godoxy"] ENTRYPOINT [ "/app/docker-start.sh" ]

View file

@ -45,9 +45,11 @@ get:
build: build:
mkdir -p bin mkdir -p bin
go build ${BUILD_FLAGS} -o bin/godoxy ./cmd go build ${BUILD_FLAGS} -o bin/godoxy ./cmd
if [ $(shell id -u) -eq 0 ]; \ if which setcap; then \
then setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy; \ if [ $(shell id -u) -eq 0 ]; \
else sudo setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy; \ then setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy; \
else sudo setcap CAP_NET_BIND_SERVICE=+eip bin/godoxy; \
fi \
fi fi
run: run:

View file

@ -4,14 +4,12 @@ services:
image: ghcr.io/yusing/go-proxy-frontend:latest image: ghcr.io/yusing/go-proxy-frontend:latest
container_name: godoxy-frontend container_name: godoxy-frontend
restart: unless-stopped restart: unless-stopped
network_mode: host
env_file: .env env_file: .env
depends_on: depends_on:
- app - app
# modify below to fit your needs # modify below to fit your needs
labels: labels:
proxy.aliases: gp proxy.aliases: godoxy
proxy.#1.port: 3000
# proxy.#1.middlewares.cidr_whitelist.status: 403 # proxy.#1.middlewares.cidr_whitelist.status: 403
# proxy.#1.middlewares.cidr_whitelist.message: IP not allowed # proxy.#1.middlewares.cidr_whitelist.message: IP not allowed
# proxy.#1.middlewares.cidr_whitelist.allow: | # proxy.#1.middlewares.cidr_whitelist.allow: |
@ -23,8 +21,13 @@ services:
image: ghcr.io/yusing/go-proxy:latest image: ghcr.io/yusing/go-proxy:latest
container_name: godoxy container_name: godoxy
restart: always restart: always
network_mode: host
env_file: .env env_file: .env
ports:
- 80:80 # http
- 443:443 # https
- 8081:8081 # prometheus
extra_hosts:
- host.docker.internal:host-gateway
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- ./config:/app/config - ./config:/app/config

View file

@ -2,19 +2,17 @@ package query
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
v1 "github.com/yusing/go-proxy/internal/api/v1" v1 "github.com/yusing/go-proxy/internal/api/v1"
U "github.com/yusing/go-proxy/internal/api/v1/utils" U "github.com/yusing/go-proxy/internal/api/v1/utils"
"github.com/yusing/go-proxy/internal/common"
E "github.com/yusing/go-proxy/internal/error" E "github.com/yusing/go-proxy/internal/error"
"github.com/yusing/go-proxy/internal/net/http/middleware" "github.com/yusing/go-proxy/internal/net/http/middleware"
) )
func ReloadServer() E.Error { func ReloadServer() E.Error {
resp, err := U.Post(common.APIHTTPURL+"/v1/reload", "", nil) resp, err := U.FetchAPI(http.MethodPost, "/v1/reload", nil)
if err != nil { if err != nil {
return E.From(err) return E.From(err)
} }
@ -32,7 +30,7 @@ func ReloadServer() E.Error {
} }
func List[T any](what string) (_ T, outErr E.Error) { func List[T any](what string) (_ T, outErr E.Error) {
resp, err := U.Get(fmt.Sprintf("%s/v1/list/%s", common.APIHTTPURL, what)) resp, err := U.FetchAPI(http.MethodGet, "/v1/list/"+what, nil)
if err != nil { if err != nil {
outErr = E.From(err) outErr = E.From(err)
return return

View file

@ -1,7 +1,9 @@
package utils package utils
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"io"
"net" "net"
"net/http" "net/http"
@ -26,3 +28,15 @@ var (
Post = httpClient.Post Post = httpClient.Post
Head = httpClient.Head Head = httpClient.Head
) )
func FetchAPI(method, endpoint string, body []byte) (*http.Response, error) {
var bodyReader io.Reader
if body != nil {
bodyReader = bytes.NewReader(body)
}
req, err := http.NewRequest(method, "http://localhost"+common.APIHTTPAddr+endpoint, bodyReader)
if err != nil {
return nil, err
}
return httpClient.Do(req)
}

View file

@ -10,6 +10,13 @@ const (
KeepAlive = 60 * time.Second KeepAlive = 60 * time.Second
) )
const (
ProxyHTTPAddr = ":80"
ProxyHTTPSAddr = ":443"
APIHTTPAddr = ":8080"
MetricsHTTPAddr = ":8081"
)
// file, folder structure // file, folder structure
const ( const (

View file

@ -23,26 +23,7 @@ var (
EnableLogStreaming = GetEnvBool("LOG_STREAMING", true) EnableLogStreaming = GetEnvBool("LOG_STREAMING", true)
DebugMemLogger = GetEnvBool("DEBUG_MEM_LOGGER", false) && EnableLogStreaming DebugMemLogger = GetEnvBool("DEBUG_MEM_LOGGER", false) && EnableLogStreaming
ProxyHTTPAddr, PrometheusEnabled = GetEnvBool("PROMETHEUS_ENABLED", true)
ProxyHTTPHost,
ProxyHTTPPort,
ProxyHTTPURL = GetAddrEnv("HTTP_ADDR", ":80", "http")
ProxyHTTPSAddr,
ProxyHTTPSHost,
ProxyHTTPSPort,
ProxyHTTPSURL = GetAddrEnv("HTTPS_ADDR", ":443", "https")
APIHTTPAddr,
APIHTTPHost,
APIHTTPPort,
APIHTTPURL = GetAddrEnv("API_ADDR", "127.0.0.1:8888", "http")
MetricsHTTPAddr,
MetricsHTTPHost,
MetricsHTTPPort,
MetricsHTTPURL = GetAddrEnv("PROMETHEUS_ADDR", "", "http")
PrometheusEnabled = MetricsHTTPURL != ""
APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", "")) APIJWTSecret = decodeJWTKey(GetEnvString("API_JWT_SECRET", ""))
APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour) APIJWTTokenTTL = GetDurationEnv("API_JWT_TOKEN_TTL", time.Hour)

View file

@ -124,30 +124,30 @@ func (c *Container) setPublicIP() {
return return
} }
if strings.HasPrefix(c.DockerHost, "unix://") { if strings.HasPrefix(c.DockerHost, "unix://") {
c.PublicIP = "127.0.0.1" c.PublicIP = "localhost"
return return
} }
url, err := url.Parse(c.DockerHost) url, err := url.Parse(c.DockerHost)
if err != nil { if err != nil {
logging.Err(err).Msgf("invalid docker host %q, falling back to 127.0.0.1", c.DockerHost) logging.Err(err).Msgf("invalid docker host %q, falling back to localhost", c.DockerHost)
c.PublicIP = "127.0.0.1" c.PublicIP = "localhost"
return return
} }
c.PublicIP = url.Hostname() c.PublicIP = url.Hostname()
} }
func (c *Container) setPrivateIP(helper containerHelper) { func (c *Container) setPrivateIP(helper containerHelper) {
if !strings.HasPrefix(c.DockerHost, "unix://") { // if !strings.HasPrefix(c.DockerHost, "unix://") {
return // return
} // }
if helper.NetworkSettings == nil { // if helper.NetworkSettings == nil {
return // return
} // }
for _, v := range helper.NetworkSettings.Networks { // for _, v := range helper.NetworkSettings.Networks {
if v.IPAddress == "" { // if v.IPAddress == "" {
continue // continue
} // }
c.PrivateIP = v.IPAddress // c.PrivateIP = v.IPAddress
return // return
} // }
} }

View file

@ -4,14 +4,13 @@ import (
"testing" "testing"
"github.com/yusing/go-proxy/internal/net/http/loadbalancer/types" "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
loadbalance "github.com/yusing/go-proxy/internal/net/http/loadbalancer/types"
. "github.com/yusing/go-proxy/internal/utils/testing" . "github.com/yusing/go-proxy/internal/utils/testing"
) )
func TestRebalance(t *testing.T) { func TestRebalance(t *testing.T) {
t.Parallel() t.Parallel()
t.Run("zero", func(t *testing.T) { t.Run("zero", func(t *testing.T) {
lb := New(new(loadbalance.Config)) lb := New(new(types.Config))
for range 10 { for range 10 {
lb.AddServer(types.TestNewServer(0)) lb.AddServer(types.TestNewServer(0))
} }
@ -19,7 +18,7 @@ func TestRebalance(t *testing.T) {
ExpectEqual(t, lb.sumWeight, maxWeight) ExpectEqual(t, lb.sumWeight, maxWeight)
}) })
t.Run("less", func(t *testing.T) { t.Run("less", func(t *testing.T) {
lb := New(new(loadbalance.Config)) lb := New(new(types.Config))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1)) lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2)) lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .3)) lb.AddServer(types.TestNewServer(float64(maxWeight) * .3))
@ -30,7 +29,7 @@ func TestRebalance(t *testing.T) {
ExpectEqual(t, lb.sumWeight, maxWeight) ExpectEqual(t, lb.sumWeight, maxWeight)
}) })
t.Run("more", func(t *testing.T) { t.Run("more", func(t *testing.T) {
lb := New(new(loadbalance.Config)) lb := New(new(types.Config))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .1)) lb.AddServer(types.TestNewServer(float64(maxWeight) * .1))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .2)) lb.AddServer(types.TestNewServer(float64(maxWeight) * .2))
lb.AddServer(types.TestNewServer(float64(maxWeight) * .3)) lb.AddServer(types.TestNewServer(float64(maxWeight) * .3))

View file

@ -22,7 +22,7 @@ func (redirectHTTP) before(w http.ResponseWriter, r *http.Request) (proceed bool
if i := strings.Index(host, ":"); i != -1 { if i := strings.Index(host, ":"); i != -1 {
host = host[:i] // strip port number if present host = host[:i] // strip port number if present
} }
r.URL.Host = host + ":" + common.ProxyHTTPSPort r.URL.Host = host + common.ProxyHTTPSAddr
logging.Debug().Str("url", r.URL.String()).Msg("redirect to https") logging.Debug().Str("url", r.URL.String()).Msg("redirect to https")
http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect) http.Redirect(w, r, r.URL.String(), http.StatusTemporaryRedirect)
return true return true

View file

@ -15,7 +15,7 @@ func TestRedirectToHTTPs(t *testing.T) {
}) })
ExpectNoError(t, err) ExpectNoError(t, err)
ExpectEqual(t, result.ResponseStatus, http.StatusTemporaryRedirect) ExpectEqual(t, result.ResponseStatus, http.StatusTemporaryRedirect)
ExpectEqual(t, result.ResponseHeaders.Get("Location"), "https://example.com:"+common.ProxyHTTPSPort) ExpectEqual(t, result.ResponseHeaders.Get("Location"), "https://example.com"+common.ProxyHTTPSAddr)
} }
func TestNoRedirect(t *testing.T) { func TestNoRedirect(t *testing.T) {

View file

@ -41,7 +41,7 @@ func TestHTTPConfigDeserialize(t *testing.T) {
if err != nil { if err != nil {
ExpectNoError(t, err) ExpectNoError(t, err)
} }
ExpectDeepEqual(t, cfg.HTTPConfig, &tt.expected) ExpectDeepEqual(t, cfg.HTTPConfig, tt.expected)
}) })
} }
} }

View file

@ -116,6 +116,11 @@ func (e *RawEntry) Finalize() {
} }
} }
switch e.Host {
case "localhost", "127.0.0.1", "[::1]":
e.Host = "host.docker.internal"
}
if e.Scheme == "" && isDocker { if e.Scheme == "" && isDocker {
switch { switch {
case e.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp": case e.Host == cont.PublicIP && cont.PublicPortMapping[pp].Type == "udp":

25
scripts/docker-start.sh Executable file
View file

@ -0,0 +1,25 @@
#!/bin/sh
echo "Running as PUID: ${PUID}, PGID: ${PGID}"
echo "Creating user"
addgroup -S -g "${PGID}" godoxyg
adduser -S -D -H -s /bin/false -u "${PUID}" -g "${PGID}" godoxy
echo "Setting up permissions"
chown -R godoxy:godoxyg /app
setcap CAP_NET_BIND_SERVICE=+eip /app/godoxy
# fork docker socket if exists
if test -e /var/run/docker.sock; then
echo "Proxying docker socket"
socat -v "UNIX-LISTEN:${SOCKET_FORK}",fork UNIX-CONNECT:/var/run/docker.sock >/dev/null 2>&1 &
# wait for socket to be ready
while [ ! -S "${SOCKET_FORK}" ]; do
sleep 0.1
done
chmod 660 "${SOCKET_FORK}"
chown godoxy:godoxyg "${SOCKET_FORK}"
fi
echo "Done"
runuser -u godoxy -g godoxyg -- /app/godoxy