Compare commits

...

18 Commits

Author SHA1 Message Date
yusing
6b3bf84148 fix(stream): nil panic when logging error 2025-09-22 10:27:09 +08:00
yusing
62a667758d docs(README): add section for updating and uninstalling system agent 2025-09-21 13:19:27 +08:00
yusing
ddd27156fc refactor(Dockerfile): simplify development Dockerfile 2025-09-21 13:00:43 +08:00
yusing
af8e2d56b2 fix(agent): respect response header timeout and compression settings 2025-09-21 11:58:31 +08:00
yusing
74a215b894 feat(agentproxy): simplify configuration handling and related header management 2025-09-21 11:52:42 +08:00
yusing
ccdc0046fd refactor(agent): update version handling in AgentConfig to use pkg.Version type 2025-09-21 11:51:17 +08:00
yusing
2f7fdc4c51 feat(version): add comparison methods 2025-09-21 11:47:50 +08:00
yusing
de1f4da126 feat(ReverseProxy): add SSL/TLS configuration options and build TLS config method 2025-09-21 10:47:37 +08:00
yusing
a48ccb4423 refactor(server): improve proxy protocol handling 2025-09-19 11:59:34 +08:00
yusing
193fd9a249 docs(config): update config.example.yml with access control and proxy protocol comments 2025-09-19 10:47:35 +08:00
yusing
0bc4c4af77 fix(vscode): update schema URLs in settings.example.json 2025-09-19 10:41:27 +08:00
yusing
5fa1417add fix(server): set default logger in server start options if not provided 2025-09-19 10:31:00 +08:00
yusing
b763c92645 refactor(stream): update TCP and UDP stream listeners to support proxy protocol and ACL wrapping 2025-09-19 10:23:47 +08:00
yusing
09b14a47e9 refactor(config): add SupportProxyProtocol to Entrypoint config 2025-09-18 17:36:19 +08:00
yusing
83a69322fa refactor(server): enhance server start options and support for proxy protocol 2025-09-18 17:34:02 +08:00
yusing
3aba5a1911 refactor(agent): simplify ReverseProxy method by directly modifying request URL 2025-09-17 14:07:06 +08:00
yusing
ca805edfe0 fix(agent): incorrect uri in reverse proxy 2025-09-17 14:03:35 +08:00
yusing
7205bf47de feat(autocert): add DNS resolver options to Config and update provider initialization 2025-09-16 15:43:49 +08:00
26 changed files with 473 additions and 146 deletions

View File

@@ -1,10 +1,10 @@
{
"yaml.schemas": {
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/config.schema.json": [
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/config.schema.json": [
"config.example.yml",
"config.yml"
],
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/src/types/godoxy/routes.schema.json": [
"https://github.com/yusing/godoxy-webui/raw/refs/heads/main/types/godoxy/routes.schema.json": [
"providers.example.yml"
]
}

View File

@@ -33,6 +33,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [How does GoDoxy work](#how-does-godoxy-work)
- [Update / Uninstall system agent](#update--uninstall-system-agent)
- [Screenshots](#screenshots)
- [idlesleeper](#idlesleeper)
- [Metrics and Logs](#metrics-and-logs)
@@ -128,6 +129,20 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
>
> For example, with the label `proxy.aliases: qbt` you can access your app via `qbt.domain.com`.
## Update / Uninstall system agent
Update:
```bash
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- update
```
Uninstall:
```bash
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- uninstall
```
## Screenshots
### idlesleeper

View File

@@ -34,6 +34,7 @@
- [安裝](#安裝)
- [手動安裝](#手動安裝)
- [資料夾結構](#資料夾結構)
- [更新 / 卸載系統代理 (System Agent)](#更新--卸載系統代理-system-agent)
- [截圖](#截圖)
- [閒置休眠](#閒置休眠)
- [監控](#監控)
@@ -144,6 +145,20 @@
└── .env
```
## 更新 / 卸載系統代理 (System Agent)
更新:
```bash
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- update
```
卸載:
```bash
bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- uninstall
```
## 截圖
### 閒置休眠

View File

@@ -71,6 +71,7 @@ require (
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect

View File

@@ -128,6 +128,8 @@ github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

View File

@@ -22,7 +22,7 @@ import (
type AgentConfig struct {
Addr string `json:"addr"`
Name string `json:"name"`
Version string `json:"version"`
Version pkg.Version `json:"version"`
Runtime ContainerRuntime `json:"runtime"`
httpClient *http.Client
@@ -148,11 +148,10 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtimeBytes)
}
cfg.Version = string(agentVersionBytes)
agentVersion := pkg.ParseVersion(cfg.Version)
cfg.Version = pkg.ParseVersion(string(agentVersionBytes))
if serverVersion.IsNewerMajorThan(agentVersion) {
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, agentVersion)
if serverVersion.IsNewerThanMajor(cfg.Version) {
log.Warn().Msgf("agent %s major version mismatch: server: %s, agent: %s", cfg.Name, serverVersion, cfg.Version)
}
log.Info().Msgf("agent %q initialized", cfg.Name)

View File

@@ -19,7 +19,6 @@ func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io
}
func (cfg *AgentConfig) Forward(req *http.Request, endpoint string) (*http.Response, error) {
req = req.WithContext(req.Context())
req.URL.Host = AgentHost
req.URL.Scheme = "https"
req.URL.Path = APIEndpointBase + endpoint
@@ -56,17 +55,11 @@ func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websoc
//
// It will create a new request with the same context, method, and body, but with the agent host and scheme, and the endpoint
// If the request has a query, it will be added to the proxy request's URL
func (cfg *AgentConfig) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) error {
func (cfg *AgentConfig) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
rp := reverseproxy.NewReverseProxy("agent", nettypes.NewURL(AgentURL), cfg.Transport())
uri := APIEndpointBase + endpoint
if req.URL.RawQuery != "" {
uri += "?" + req.URL.RawQuery
}
r, err := http.NewRequestWithContext(req.Context(), req.Method, uri, req.Body)
if err != nil {
return err
}
r.Header = req.Header
rp.ServeHTTP(w, r)
return nil
req.URL.Host = AgentHost
req.URL.Scheme = "https"
req.URL.Path = endpoint
req.RequestURI = ""
rp.ServeHTTP(w, req)
}

View File

@@ -0,0 +1,76 @@
package agentproxy
import (
"encoding/base64"
"encoding/json"
"net/http"
"strconv"
"time"
route "github.com/yusing/go-proxy/internal/route/types"
)
type Config struct {
Scheme string `json:"scheme,omitempty"`
Host string `json:"host,omitempty"` // host or host:port
route.HTTPConfig
}
func ConfigFromHeaders(h http.Header) (Config, error) {
cfg, err := proxyConfigFromHeaders(h)
if err != nil {
return cfg, err
}
if cfg.Host == "" {
cfg = proxyConfigFromHeadersLegacy(h)
}
return cfg, nil
}
func proxyConfigFromHeadersLegacy(h http.Header) (cfg Config) {
cfg.Host = h.Get(HeaderXProxyHost)
isHTTPS, _ := strconv.ParseBool(h.Get(HeaderXProxyHTTPS))
cfg.NoTLSVerify, _ = strconv.ParseBool(h.Get(HeaderXProxySkipTLSVerify))
responseHeaderTimeout, err := strconv.Atoi(h.Get(HeaderXProxyResponseHeaderTimeout))
if err != nil {
responseHeaderTimeout = 0
}
cfg.ResponseHeaderTimeout = time.Duration(responseHeaderTimeout) * time.Second
cfg.Scheme = "http"
if isHTTPS {
cfg.Scheme = "https"
}
return
}
func proxyConfigFromHeaders(h http.Header) (cfg Config, err error) {
cfg.Scheme = h.Get(HeaderXProxyScheme)
cfg.Host = h.Get(HeaderXProxyHost)
cfgBase64 := h.Get(HeaderXProxyConfig)
cfgJSON, err := base64.StdEncoding.DecodeString(cfgBase64)
if err != nil {
return cfg, err
}
err = json.Unmarshal(cfgJSON, &cfg)
return cfg, err
}
func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header) {
h.Set(HeaderXProxyHost, cfg.Host)
h.Set(HeaderXProxyHTTPS, strconv.FormatBool(cfg.Scheme == "https"))
h.Set(HeaderXProxySkipTLSVerify, strconv.FormatBool(cfg.NoTLSVerify))
h.Set(HeaderXProxyResponseHeaderTimeout, strconv.Itoa(int(cfg.ResponseHeaderTimeout.Round(time.Second).Seconds())))
}
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header) {
h.Set(HeaderXProxyHost, cfg.Host)
h.Set(HeaderXProxyScheme, string(cfg.Scheme))
cfgJSON, _ := json.Marshal(cfg.HTTPConfig)
cfgBase64 := base64.StdEncoding.EncodeToString(cfgJSON)
h.Set(HeaderXProxyConfig, cfgBase64)
}

View File

@@ -1,27 +1,14 @@
package agentproxy
import (
"net/http"
"strconv"
const (
HeaderXProxyScheme = "X-Proxy-Scheme"
HeaderXProxyHost = "X-Proxy-Host"
HeaderXProxyConfig = "X-Proxy-Config"
)
// deprecated
const (
HeaderXProxyHost = "X-Proxy-Host"
HeaderXProxyHTTPS = "X-Proxy-Https"
HeaderXProxySkipTLSVerify = "X-Proxy-Skip-Tls-Verify"
HeaderXProxyResponseHeaderTimeout = "X-Proxy-Response-Header-Timeout"
)
type AgentProxyHeaders struct {
Host string
IsHTTPS bool
SkipTLSVerify bool
ResponseHeaderTimeout int
}
func SetAgentProxyHeaders(r *http.Request, headers *AgentProxyHeaders) {
r.Header.Set(HeaderXProxyHost, headers.Host)
r.Header.Set(HeaderXProxyHTTPS, strconv.FormatBool(headers.IsHTTPS))
r.Header.Set(HeaderXProxySkipTLSVerify, strconv.FormatBool(headers.SkipTLSVerify))
r.Header.Set(HeaderXProxyResponseHeaderTimeout, strconv.Itoa(headers.ResponseHeaderTimeout))
}

View File

@@ -1,10 +1,9 @@
package handler
import (
"crypto/tls"
"fmt"
"net/http"
"net/http/httputil"
"strconv"
"time"
"github.com/yusing/go-proxy/agent/pkg/agent"
@@ -24,31 +23,24 @@ func NewTransport() *http.Transport {
}
func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
host := r.Header.Get(agentproxy.HeaderXProxyHost)
isHTTPS, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxyHTTPS))
skipTLSVerify, _ := strconv.ParseBool(r.Header.Get(agentproxy.HeaderXProxySkipTLSVerify))
responseHeaderTimeout, err := strconv.Atoi(r.Header.Get(agentproxy.HeaderXProxyResponseHeaderTimeout))
cfg, err := agentproxy.ConfigFromHeaders(r.Header)
if err != nil {
responseHeaderTimeout = 0
}
if host == "" {
http.Error(w, "missing required headers", http.StatusBadRequest)
http.Error(w, fmt.Sprintf("failed to parse agent proxy config: %s", err.Error()), http.StatusBadRequest)
return
}
scheme := "http"
if isHTTPS {
scheme = "https"
}
transport := NewTransport()
if skipTLSVerify {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
if cfg.ResponseHeaderTimeout > 0 {
transport.ResponseHeaderTimeout = cfg.ResponseHeaderTimeout
}
if cfg.DisableCompression {
transport.DisableCompression = true
}
if responseHeaderTimeout > 0 {
transport.ResponseHeaderTimeout = time.Duration(responseHeaderTimeout) * time.Second
transport.TLSClientConfig, err = cfg.BuildTLSConfig(r.URL)
if err != nil {
http.Error(w, fmt.Sprintf("failed to build TLS client config: %s", err.Error()), http.StatusInternalServerError)
return
}
r.URL.Scheme = ""
@@ -58,8 +50,8 @@ func ProxyHTTP(w http.ResponseWriter, r *http.Request) {
rp := &httputil.ReverseProxy{
Director: func(r *http.Request) {
r.URL.Scheme = scheme
r.URL.Host = host
r.URL.Scheme = cfg.Scheme
r.URL.Host = cfg.Host
},
Transport: transport,
}

View File

@@ -39,5 +39,5 @@ func StartAgentServer(parent task.Parent, opt Options) {
TLSConfig: tlsConfig,
}
server.Start(parent, agentServer, nil, &log.Logger)
server.Start(parent, agentServer, server.WithLogger(&log.Logger))
}

View File

@@ -17,6 +17,10 @@
# 3. other providers, see https://docs.godoxy.dev/DNS-01-Providers
# Access Control
# When enabled, it will be applied globally at connection level,
# all incoming connections (web, tcp and udp) will be checked against the ACL rules.
# acl:
# default: allow # or deny (default: allow)
# allow_local: true # or false (default: true)
@@ -37,6 +41,11 @@
# keep: last 10 # (default: none)
entrypoint:
# Proxy Protocol: https://www.haproxy.com/blog/use-the-proxy-protocol-to-preserve-a-clients-ip-address
# When set to true, web entrypoint and all tcp routeswill be wrapped with Proxy Protocol listener in order to preserve the client's IP address.
# Note that HTTP/3 with proxy protocol is not supported yet.
support_proxy_protocol: false
# Below define an example of middleware config
# 1. set security headers
# 2. block non local IP connections
@@ -57,14 +66,6 @@ entrypoint:
X-Frame-Options: SAMEORIGIN
Referrer-Policy: same-origin
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
# - use: CIDRWhitelist
# allow:
# - "127.0.0.1"
# - "10.0.0.0/8"
# - "172.16.0.0/12"
# - "192.168.0.0/16"
# status: 403
# message: "Forbidden"
# - use: RedirectHTTP
# below enables access log
@@ -115,8 +116,8 @@ providers:
# secret: aaaa-bbbb-cccc-dddd
# no_tls_verify: true
# Check https://docs.godoxy.dev/Certificates-and-domain-matching
# for explaination of `match_domains`
# Match domains
# See https://docs.godoxy.dev/Certificates-and-domain-matching
#
# match_domains:
# - my.site

View File

@@ -1,23 +1,17 @@
# Stage 1: deps
FROM golang:1.25.0-alpine AS deps
FROM alpine:3.22 AS deps
HEALTHCHECK NONE
# package version does not matter
# trunk-ignore(hadolint/DL3018)
RUN apk add --no-cache tzdata make libcap-setcap
RUN apk add --no-cache tzdata
# Stage 3: Final image
FROM alpine:3.22
# Stage 2: Final image
FROM deps
LABEL maintainer="yusing@6uo.me"
LABEL proxy.exclude=1
# copy timezone data
COPY --from=deps /usr/share/zoneinfo /usr/share/zoneinfo
# copy certs
COPY --from=deps /etc/ssl/certs /etc/ssl/certs
ARG TARGET
ENV TARGET=${TARGET}

1
go.mod
View File

@@ -213,6 +213,7 @@ require (
require (
github.com/gin-gonic/gin v1.10.1
github.com/pires/go-proxyproto v0.8.1
github.com/yusing/ds v0.1.0
)

2
go.sum
View File

@@ -1424,6 +1424,8 @@ github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=

View File

@@ -46,6 +46,7 @@ func SystemInfo(c *gin.Context) {
systeminfo.Poller.ServeHTTP(c)
return
}
c.Request.URL.RawQuery = query.Encode()
agent, ok := agentPkg.GetAgent(agentAddr)
if !ok {
@@ -69,10 +70,6 @@ func SystemInfo(c *gin.Context) {
c.Status(resp.StatusCode)
io.Copy(c.Writer, resp.Body)
} else {
err := agent.ReverseProxy(c.Writer, c.Request, agentPkg.EndpointSystemInfo+"?"+query.Encode())
if err != nil {
c.Error(apitypes.InternalServerError(err, "failed to reverse proxy"))
return
}
agent.ReverseProxy(c.Writer, c.Request, agentPkg.EndpointSystemInfo)
}
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/go-acme/lego/v4/certcrypto"
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/lego"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/common"
@@ -27,6 +28,8 @@ type Config struct {
Provider string `json:"provider,omitempty"`
Options map[string]any `json:"options,omitempty"`
Resolvers []string `json:"resolvers,omitempty"`
// Custom ACME CA
CADirURL string `json:"ca_dir_url,omitempty"`
CACerts []string `json:"ca_certs,omitempty"`
@@ -111,6 +114,12 @@ func (cfg *Config) Validate() gperr.Error {
return b.Error()
}
func (cfg *Config) dns01Options() []dns01.ChallengeOption {
return []dns01.ChallengeOption{
dns01.CondOption(len(cfg.Resolvers) > 0, dns01.AddRecursiveNameservers(cfg.Resolvers)),
}
}
func (cfg *Config) GetLegoConfig() (*User, *lego.Config, gperr.Error) {
if err := cfg.Validate(); err != nil {
return nil, nil, err

View File

@@ -286,7 +286,7 @@ func (p *Provider) initClient() error {
return err
}
err = legoClient.Challenge.SetDNS01Provider(p.cfg.challengeProvider)
err = legoClient.Challenge.SetDNS01Provider(p.cfg.challengeProvider, p.cfg.dns01Options()...)
if err != nil {
return err
}

View File

@@ -204,12 +204,13 @@ func (cfg *Config) StartServers(opts ...*StartServersOptions) {
opt := opts[0]
if opt.Proxy {
server.StartServer(cfg.task, server.Options{
Name: "proxy",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr,
Handler: cfg.entrypoint,
ACL: cfg.value.ACL,
Name: "proxy",
CertProvider: cfg.AutoCertProvider(),
HTTPAddr: common.ProxyHTTPAddr,
HTTPSAddr: common.ProxyHTTPSAddr,
Handler: cfg.entrypoint,
ACL: cfg.value.ACL,
SupportProxyProtocol: cfg.value.Entrypoint.SupportProxyProtocol,
})
}
if opt.API {

View File

@@ -37,8 +37,9 @@ type (
MaxMind *maxmind.Config `json:"maxmind" yaml:"maxmind,omitempty"`
}
Entrypoint struct {
Middlewares []map[string]any `json:"middlewares"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
SupportProxyProtocol bool `json:"support_proxy_protocol"`
Middlewares []map[string]any `json:"middlewares"`
AccessLog *accesslog.RequestLoggerConfig `json:"access_log" validate:"omitempty"`
}
HomepageConfig struct {
UseDefaultCategories bool `json:"use_default_categories"`

View File

@@ -9,6 +9,8 @@ import (
"net/http"
"time"
"github.com/pires/go-proxyproto"
h2proxy "github.com/pires/go-proxyproto/helper/http2"
"github.com/quic-go/quic-go/http3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
@@ -28,6 +30,7 @@ type Server struct {
https *http.Server
startTime time.Time
acl *acl.Config
proxyProto bool
l zerolog.Logger
}
@@ -39,6 +42,8 @@ type Options struct {
CertProvider CertProvider
Handler http.Handler
ACL *acl.Config
SupportProxyProtocol bool
}
type httpServer interface {
@@ -86,6 +91,7 @@ func NewServer(opt Options) (s *Server) {
https: httpsSer,
l: logger,
acl: opt.ACL,
proxyProto: opt.SupportProxyProtocol,
}
}
@@ -99,30 +105,86 @@ func (s *Server) Start(parent task.Parent) {
subtask := parent.Subtask("server."+s.Name, false)
if s.https != nil && common.HTTP3Enabled {
s.https.TLSConfig.NextProtos = []string{http3.NextProtoH3, "h2", "http/1.1"}
h3 := &http3.Server{
Addr: s.https.Addr,
Handler: s.https.Handler,
TLSConfig: http3.ConfigureTLSConfig(s.https.TLSConfig),
if s.proxyProto {
// TODO: support proxy protocol for HTTP/3
s.l.Warn().Msg("HTTP/3 is enabled, but proxy protocol is yet not supported for HTTP/3")
} else {
s.https.TLSConfig.NextProtos = []string{http3.NextProtoH3, "h2", "http/1.1"}
h3 := &http3.Server{
Addr: s.https.Addr,
Handler: s.https.Handler,
TLSConfig: http3.ConfigureTLSConfig(s.https.TLSConfig),
}
Start(subtask, h3, WithProxyProtocolSupport(s.proxyProto), WithACL(s.acl), WithLogger(&s.l))
if s.http != nil {
s.http.Handler = advertiseHTTP3(s.http.Handler, h3)
}
// s.https is not nil (checked above)
s.https.Handler = advertiseHTTP3(s.https.Handler, h3)
}
Start(subtask, h3, s.acl, &s.l)
if s.http != nil {
s.http.Handler = advertiseHTTP3(s.http.Handler, h3)
}
// s.https is not nil (checked above)
s.https.Handler = advertiseHTTP3(s.https.Handler, h3)
}
Start(subtask, s.http, s.acl, &s.l)
Start(subtask, s.https, s.acl, &s.l)
Start(subtask, s.http, WithProxyProtocolSupport(s.proxyProto), WithACL(s.acl), WithLogger(&s.l))
Start(subtask, s.https, WithProxyProtocolSupport(s.proxyProto), WithACL(s.acl), WithLogger(&s.l))
}
func Start[Server httpServer](parent task.Parent, srv Server, acl *acl.Config, logger *zerolog.Logger) (port int) {
type ServerStartOptions struct {
tcpWrappers []func(l net.Listener) net.Listener
udpWrappers []func(l net.PacketConn) net.PacketConn
logger *zerolog.Logger
proxyProto bool
}
type ServerStartOption func(opts *ServerStartOptions)
func WithTCPWrappers(wrappers ...func(l net.Listener) net.Listener) ServerStartOption {
return func(opts *ServerStartOptions) {
opts.tcpWrappers = wrappers
}
}
func WithUDPWrappers(wrappers ...func(l net.PacketConn) net.PacketConn) ServerStartOption {
return func(opts *ServerStartOptions) {
opts.udpWrappers = wrappers
}
}
func WithLogger(logger *zerolog.Logger) ServerStartOption {
return func(opts *ServerStartOptions) {
opts.logger = logger
}
}
func WithACL(acl *acl.Config) ServerStartOption {
return func(opts *ServerStartOptions) {
if acl == nil {
return
}
opts.tcpWrappers = append(opts.tcpWrappers, acl.WrapTCP)
opts.udpWrappers = append(opts.udpWrappers, acl.WrapUDP)
}
}
func WithProxyProtocolSupport(value bool) ServerStartOption {
return func(opts *ServerStartOptions) {
opts.proxyProto = value
}
}
func Start[Server httpServer](parent task.Parent, srv Server, optFns ...ServerStartOption) (port int) {
if srv == nil {
return
}
setDebugLogger(srv, logger)
var opts ServerStartOptions
for _, optFn := range optFns {
optFn(&opts)
}
if opts.logger == nil {
opts.logger = &log.Logger
}
setDebugLogger(srv, opts.logger)
proto := proto(srv)
task := parent.Subtask(proto, true)
@@ -137,40 +199,47 @@ func Start[Server httpServer](parent task.Parent, srv Server, acl *acl.Config, l
}
l, err := lc.Listen(task.Context(), "tcp", srv.Addr)
if err != nil {
HandleError(logger, err, "failed to listen on port")
HandleError(opts.logger, err, "failed to listen on port")
return
}
port = l.Addr().(*net.TCPAddr).Port
if opts.proxyProto {
l = &proxyproto.Listener{Listener: l}
}
if srv.TLSConfig != nil {
l = tls.NewListener(l, srv.TLSConfig)
}
if acl != nil {
l = acl.WrapTCP(l)
for _, wrapper := range opts.tcpWrappers {
l = wrapper(l)
}
if opts.proxyProto {
serveFunc = getServeFunc(l, h2proxy.NewServer(srv, nil).Serve)
} else {
serveFunc = getServeFunc(l, srv.Serve)
}
serveFunc = getServeFunc(l, srv.Serve)
task.OnCancel("stop", func() {
stop(srv, l, logger)
stop(srv, l, opts.logger)
})
case *http3.Server:
l, err := lc.ListenPacket(task.Context(), "udp", srv.Addr)
if err != nil {
HandleError(logger, err, "failed to listen on port")
HandleError(opts.logger, err, "failed to listen on port")
return
}
port = l.LocalAddr().(*net.UDPAddr).Port
if acl != nil {
l = acl.WrapUDP(l)
for _, wrapper := range opts.udpWrappers {
l = wrapper(l)
}
serveFunc = getServeFunc(l, srv.Serve)
task.OnCancel("stop", func() {
stop(srv, l, logger)
stop(srv, l, opts.logger)
})
}
logStarted(srv, logger)
logStarted(srv, opts.logger)
go func() {
err := convertError(serveFunc())
if err != nil {
HandleError(logger, err, "failed to serve "+proto+" server")
HandleError(opts.logger, err, "failed to serve "+proto+" server")
}
task.Finish(err)
}()

View File

@@ -1,7 +1,6 @@
package route
import (
"crypto/tls"
"net/http"
"sync"
@@ -20,6 +19,7 @@ import (
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
"github.com/yusing/go-proxy/pkg"
)
type ReveseProxyRoute struct {
@@ -44,10 +44,12 @@ func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, gperr.Error) {
trans = a.Transport()
proxyURL = nettypes.NewURL(agent.HTTPProxyURL)
} else {
trans = gphttp.NewTransport()
if httpConfig.NoTLSVerify {
trans.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec
tlsConfig, err := httpConfig.BuildTLSConfig(&base.ProxyURL.URL)
if err != nil {
return nil, err
}
trans = gphttp.NewTransportWithTLSConfig(tlsConfig)
if httpConfig.ResponseHeaderTimeout > 0 {
trans.ResponseHeaderTimeout = httpConfig.ResponseHeaderTimeout
}
@@ -67,15 +69,19 @@ func NewReverseProxyRoute(base *Route) (*ReveseProxyRoute, gperr.Error) {
}
if a != nil {
headers := &agentproxy.AgentProxyHeaders{
Host: base.ProxyURL.Host,
IsHTTPS: base.ProxyURL.Scheme == "https",
SkipTLSVerify: httpConfig.NoTLSVerify,
ResponseHeaderTimeout: int(httpConfig.ResponseHeaderTimeout.Seconds()),
cfg := agentproxy.Config{
Scheme: base.ProxyURL.Scheme,
Host: base.ProxyURL.Host,
HTTPConfig: httpConfig,
}
setHeaderFunc := cfg.SetAgentProxyConfigHeaders
if !a.Version.IsOlderThan(pkg.Ver(0, 18, 6)) {
setHeaderFunc = cfg.SetAgentProxyConfigHeadersLegacy
}
ori := rp.HandlerFunc
rp.HandlerFunc = func(w http.ResponseWriter, r *http.Request) {
agentproxy.SetAgentProxyHeaders(r, headers)
setHeaderFunc(r.Header)
ori(w, r)
}
}

View File

@@ -4,14 +4,16 @@ import (
"context"
"net"
"github.com/pires/go-proxyproto"
"github.com/rs/zerolog"
config "github.com/yusing/go-proxy/internal/config/types"
nettypes "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/utils"
"go.uber.org/atomic"
)
type TCPTCPStream struct {
listener *net.TCPListener
listener net.Listener
laddr *net.TCPAddr
dst *net.TCPAddr
@@ -34,12 +36,20 @@ func NewTCPTCPStream(listenAddr, dstAddr string) (nettypes.Stream, error) {
}
func (s *TCPTCPStream) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) {
listener, err := net.ListenTCP("tcp", s.laddr)
var err error
s.listener, err = net.ListenTCP("tcp", s.laddr)
if err != nil {
logErr(s, err, "failed to listen")
return
}
s.listener = listener
if proxyProto := config.GetInstance().Value().Entrypoint.SupportProxyProtocol; proxyProto {
s.listener = &proxyproto.Listener{Listener: s.listener}
}
if acl := config.GetInstance().Value().ACL; acl != nil {
s.listener = acl.WrapTCP(s.listener)
}
s.preDial = preDial
s.onRead = onRead
go s.listen(ctx)
@@ -60,7 +70,14 @@ func (s *TCPTCPStream) LocalAddr() net.Addr {
}
func (s *TCPTCPStream) MarshalZerologObject(e *zerolog.Event) {
e.Str("protocol", "tcp-tcp").Str("listen", s.listener.Addr().String()).Str("dst", s.dst.String())
e.Str("protocol", "tcp-tcp")
if s.listener != nil {
e.Str("listen", s.listener.Addr().String())
}
if s.dst != nil {
e.Str("dst", s.dst.String())
}
}
func (s *TCPTCPStream) listen(ctx context.Context) {

View File

@@ -3,12 +3,14 @@ package stream
import (
"bytes"
"context"
"fmt"
"maps"
"net"
"sync"
"time"
"github.com/rs/zerolog"
config "github.com/yusing/go-proxy/internal/config/types"
nettypes "github.com/yusing/go-proxy/internal/net/types"
"github.com/yusing/go-proxy/internal/utils/synk"
"go.uber.org/atomic"
@@ -16,7 +18,7 @@ import (
type UDPUDPStream struct {
name string
listener *net.UDPConn
listener net.PacketConn
laddr *net.UDPAddr
dst *net.UDPAddr
@@ -34,7 +36,7 @@ type UDPUDPStream struct {
type udpUDPConn struct {
srcAddr *net.UDPAddr
dstConn *net.UDPConn
listener *net.UDPConn
listener net.PacketConn
lastUsed atomic.Time
closed atomic.Bool
mu sync.Mutex
@@ -66,12 +68,15 @@ func NewUDPUDPStream(listenAddr, dstAddr string) (nettypes.Stream, error) {
}
func (s *UDPUDPStream) ListenAndServe(ctx context.Context, preDial, onRead nettypes.HookFunc) {
listener, err := net.ListenUDP("udp", s.laddr)
var err error
s.listener, err = net.ListenUDP("udp", s.laddr)
if err != nil {
logErr(s, err, "failed to listen")
return
}
s.listener = listener
if acl := config.GetInstance().Value().ACL; acl != nil {
s.listener = acl.WrapUDP(s.listener)
}
s.preDial = preDial
s.onRead = onRead
go s.listen(ctx)
@@ -108,7 +113,13 @@ func (s *UDPUDPStream) LocalAddr() net.Addr {
}
func (s *UDPUDPStream) MarshalZerologObject(e *zerolog.Event) {
e.Str("protocol", "udp-udp").Str("name", s.name).Str("dst", s.dst.String())
e.Str("protocol", "udp-udp")
if s.name != "" {
e.Str("name", s.name)
}
if s.dst != nil {
e.Str("dst", s.dst.String())
}
}
func (s *UDPUDPStream) listen(ctx context.Context) {
@@ -120,7 +131,7 @@ func (s *UDPUDPStream) listen(ctx context.Context) {
case <-ctx.Done():
return
default:
n, srcAddr, err := s.listener.ReadFromUDP(buf)
n, srcAddr, err := s.listener.ReadFrom(buf)
if err != nil {
if s.closed.Load() {
return
@@ -129,6 +140,12 @@ func (s *UDPUDPStream) listen(ctx context.Context) {
continue
}
srcAddrUDP, ok := srcAddr.(*net.UDPAddr)
if !ok {
logErr(s, fmt.Errorf("unexpected source address type: %T", srcAddr), "unexpected source address type")
continue
}
logDebugf(s, "read %d bytes from %s", n, srcAddr)
if s.onRead != nil {
@@ -139,7 +156,7 @@ func (s *UDPUDPStream) listen(ctx context.Context) {
}
// Get or create connection, passing the initial data
go s.getOrCreateConnection(ctx, srcAddr, bytes.Clone(buf[:n]))
go s.getOrCreateConnection(ctx, srcAddrUDP, bytes.Clone(buf[:n]))
}
}
}
@@ -233,7 +250,7 @@ func (conn *udpUDPConn) handleResponses(ctx context.Context) {
_ = conn.dstConn.SetReadDeadline(time.Time{})
// Forward response back to client using the listener
_, err = conn.listener.WriteToUDP(buf[:n], conn.srcAddr)
_, err = conn.listener.WriteTo(buf[:n], conn.srcAddr)
if err != nil {
if !conn.closed.Load() {
logErrf(conn, err, "failed to write %d bytes to client", n)

View File

@@ -1,11 +1,118 @@
package route
import (
"crypto/tls"
"crypto/x509"
"net/url"
"os"
"strings"
"time"
"github.com/yusing/go-proxy/internal/gperr"
)
type HTTPConfig struct {
NoTLSVerify bool `json:"no_tls_verify,omitempty"`
ResponseHeaderTimeout time.Duration `json:"response_header_timeout,omitempty" swaggertype:"primitive,integer"`
DisableCompression bool `json:"disable_compression,omitempty"`
// SSL/TLS proxy options (nginx-like)
SSLServerName *string `json:"ssl_server_name,omitempty"` // SNI server name
SSLTrustedCertificate string `json:"ssl_trusted_certificate,omitempty"` // Path to trusted CA certificates
SSLCertificate string `json:"ssl_certificate,omitempty"` // Path to client certificate
SSLCertificateKey string `json:"ssl_certificate_key,omitempty"` // Path to client certificate key
SSLProtocols []string `json:"ssl_protocols,omitempty"` // Allowed TLS protocols
}
// BuildTLSConfig creates a TLS configuration based on the HTTP config options.
func (cfg *HTTPConfig) BuildTLSConfig(targetURL *url.URL) (*tls.Config, gperr.Error) {
tlsConfig := &tls.Config{}
// Handle InsecureSkipVerify (legacy NoTLSVerify option)
if cfg.NoTLSVerify {
tlsConfig.InsecureSkipVerify = true
}
// Handle ssl_server_name (SNI)
if cfg.SSLServerName != nil {
switch *cfg.SSLServerName {
case "off":
// Disable SNI by setting empty string
tlsConfig.ServerName = ""
case "on", "":
// Use hostname from target URL for SNI
tlsConfig.ServerName = targetURL.Hostname()
default:
tlsConfig.ServerName = *cfg.SSLServerName
}
} else {
// Default behavior - use hostname for SNI
tlsConfig.ServerName = targetURL.Hostname()
}
// Handle ssl_trusted_certificate
if cfg.SSLTrustedCertificate != "" {
caCertData, err := os.ReadFile(cfg.SSLTrustedCertificate)
if err != nil {
return nil, gperr.New("failed to read trusted certificate file").
Subject(cfg.SSLTrustedCertificate).
With(err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCertData) {
return nil, gperr.New("failed to parse trusted certificates").
Subject(cfg.SSLTrustedCertificate)
}
tlsConfig.RootCAs = caCertPool
}
// Handle ssl_certificate and ssl_certificate_key (client certificates)
if cfg.SSLCertificate != "" {
if cfg.SSLCertificateKey == "" {
return nil, gperr.New("ssl_certificate_key is required when ssl_certificate is specified")
}
clientCert, err := tls.LoadX509KeyPair(cfg.SSLCertificate, cfg.SSLCertificateKey)
if err != nil {
return nil, gperr.New("failed to load client certificate").
Subject(cfg.SSLCertificate).
With(err)
}
tlsConfig.Certificates = []tls.Certificate{clientCert}
}
// Handle ssl_protocols (TLS versions)
if len(cfg.SSLProtocols) > 0 {
var minVersion, maxVersion uint16
for _, protocol := range cfg.SSLProtocols {
var version uint16
switch strings.ToLower(protocol) {
case "tlsv1.0":
version = tls.VersionTLS10
case "tlsv1.1":
version = tls.VersionTLS11
case "tlsv1.2":
version = tls.VersionTLS12
case "tlsv1.3":
version = tls.VersionTLS13
default:
return nil, gperr.New("unsupported TLS protocol").
Subject(protocol)
}
if minVersion == 0 || version < minVersion {
minVersion = version
}
if maxVersion == 0 || version > maxVersion {
maxVersion = version
}
}
tlsConfig.MinVersion = minVersion
tlsConfig.MaxVersion = maxVersion
}
return tlsConfig, nil
}

View File

@@ -43,8 +43,8 @@ func init() {
type Version struct{ Generation, Major, Minor int }
func Ver(major, minor, patch int) Version {
return Version{major, minor, patch}
func Ver(gen, major, minor int) Version {
return Version{gen, major, minor}
}
func (v Version) String() string {
@@ -55,13 +55,38 @@ func (v Version) MarshalText() ([]byte, error) {
return []byte(v.String()), nil
}
func (v Version) IsNewerMajorThan(other Version) bool {
func (v Version) IsNewerThan(other Version) bool {
if v.Generation != other.Generation {
return v.Generation > other.Generation
}
if v.Major != other.Major {
return v.Major > other.Major
}
return v.Minor > other.Minor
}
func (v Version) IsNewerThanMajor(other Version) bool {
if v.Generation != other.Generation {
return v.Generation > other.Generation
}
return v.Major > other.Major
}
func (v Version) IsOlderThan(other Version) bool {
return !v.IsNewerThan(other)
}
func (v Version) IsOlderThanMajor(other Version) bool {
if v.Generation != other.Generation {
return v.Generation < other.Generation
}
return v.Major < other.Major
}
func (v Version) IsOlderMajorThan(other Version) bool {
return !v.IsNewerThanMajor(other)
}
func (v Version) IsEqual(other Version) bool {
return v.Generation == other.Generation && v.Major == other.Major && v.Minor == other.Minor
}