Compare commits

..

20 Commits

Author SHA1 Message Date
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
yusing
b12999210f feat(docker): add tmpfs caching for Next.js in compose files 2025-09-14 21:24:01 +08:00
yusing
8b8969f033 fix(auth): change userpass to redirect to login and update documentation 2025-09-14 21:11:20 +08:00
yusing
025ebab1ce refactor(api): remove unused ErrorCode type 2025-09-14 20:50:07 +08:00
yusing
ea7bd0d19a fix(docker): update dev docker compose 2025-09-14 18:39:40 +08:00
yusing
f889f5c08d fix(oidc): simplify LoginHandler to always redirect to IdP 2025-09-14 14:33:28 +08:00
yusing
932c20f32d chore(docker): update .gitignore to exclude all .env files and modify dev.compose.yml to include env_file for development 2025-09-14 13:47:02 +08:00
yusing
2a08c55e39 feat(auth): add GET endpoint for logout and update documentation 2025-09-14 13:07:24 +08:00
yusing
93e1d17090 fix(auth): revert userpass PostAuthCallback to respond http 200 2025-09-14 11:19:37 +08:00
yusing
d72d403e2c docs(README): update README files to include new Star History section and replace outdated screenshots
- Added "Star History" section with a chart link.
- Replaced outdated screenshots with new "Routes" and "Servers" images.
- Removed references to deleted screenshots for better clarity.
2025-09-14 01:30:37 +08:00
yusing
b5d70a0592 docs(README): remove WebUI announcement from README 2025-09-14 01:15:36 +08:00
39 changed files with 244 additions and 159 deletions

1
.gitignore vendored
View File

@@ -29,6 +29,7 @@ todo.md
.aider*
mtrace.json
.env
*.env
.cursorrules
.cursor/
.windsurfrules

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

@@ -21,8 +21,6 @@ A lightweight, simple, and performant reverse proxy with WebUI.
Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e48936a1-godoxy-assistant)! (Thanks to [@ismesid](https://github.com/arevindh))
**New WebUI and is now available in nightly tag [(Demo)](https://nightly.demo.godoxy.dev), feedbacks are welcomed!**
</div>
## Table of content
@@ -41,6 +39,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
- [Manual Setup](#manual-setup)
- [Folder structrue](#folder-structrue)
- [Build it yourself](#build-it-yourself)
- [Star History](#star-history)
## Running demo
@@ -140,22 +139,12 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
<div align="center">
<table>
<tr>
<td align="center"><img src="screenshots/uptime.png" alt="Uptime Monitor" width="250"/></td>
<td align="center"><img src="screenshots/docker-logs.jpg" alt="Docker Logs" width="250"/></td>
<td align="center"><img src="screenshots/docker.jpg" alt="Server Overview" width="250"/></td>
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
</tr>
<tr>
<td align="center"><b>Uptime Monitor</b></td>
<td align="center"><b>Docker Logs</b></td>
<td align="center"><b>Server Overview</b></td>
</tr>
<tr>
<td align="center"><img src="screenshots/system-monitor.jpg" alt="System Monitor" width="250"/></td>
<td align="center"><img src="screenshots/system-info-graphs.jpg" alt="Graphs" width="250"/></td>
</tr>
<tr>
<td align="center"><b>System Monitor</b></td>
<td align="center"><b>Graphs</b></td>
<td align="center"><b>Routes</b></td>
<td align="center"><b>Servers</b></td>
</tr>
</table>
</div>
@@ -207,4 +196,8 @@ Configure Wildcard DNS Record(s) to point to machine running `GoDoxy`, e.g.
5. build binary with `make build`
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=yusing/godoxy&type=Date)](https://www.star-history.com/#yusing/godoxy&Date)
[🔼Back to top](#table-of-content)

View File

@@ -38,6 +38,7 @@
- [閒置休眠](#閒置休眠)
- [監控](#監控)
- [自行編譯](#自行編譯)
- [Star History](#star-history)
## 運行示例
@@ -84,8 +85,6 @@
- **高效能**
-**[Go](https://go.dev)** 語言編寫
[🔼 回到頂部](#目錄)
## 前置需求
設置 DNS 記錄指向運行 `GoDoxy` 的機器,例如:
@@ -110,8 +109,6 @@
3. 現在可以在 WebUI `https://godoxy.yourdomain.com` 進行額外配置
[🔼 回到頂部](#目錄)
### 手動安裝
1. 建立 `config` 目錄,然後將 `config.example.yml` 下載到 `config/config.yml`
@@ -153,29 +150,17 @@
![閒置休眠](screenshots/idlesleeper.webp)
[🔼 回到頂部](#目錄)
### 監控
<div align="center">
<table>
<tr>
<td align="center"><img src="screenshots/uptime.png" alt="Uptime Monitor" width="250"/></td>
<td align="center"><img src="screenshots/docker-logs.jpg" alt="Docker Logs" width="250"/></td>
<td align="center"><img src="screenshots/docker.jpg" alt="Server Overview" width="250"/></td>
<td align="center"><img src="screenshots/routes.jpg" alt="Routes" width="350"/></td>
<td align="center"><img src="screenshots/servers.jpg" alt="Servers" width="350"/></td>
</tr>
<tr>
<td align="center"><b>運行時間監控</b></td>
<td align="center"><b>Docker 日誌</b></td>
<td align="center"><b>伺服器概覽</b></td>
</tr>
<tr>
<td align="center"><img src="screenshots/system-monitor.jpg" alt="System Monitor" width="250"/></td>
<td align="center"><img src="screenshots/system-info-graphs.jpg" alt="Graphs" width="250"/></td>
</tr>
<tr>
<td align="center"><b>系統監控</b></td>
<td align="center"><b>圖表</b></td>
<td align="center"><b>路由</b></td>
<td align="center"><b>伺服器</b></td>
</tr>
</table>
</div>
@@ -192,4 +177,8 @@
5. 使用 `make build` 編譯二進制檔案
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=yusing/godoxy&type=Date)](https://www.star-history.com/#yusing/godoxy&Date)
[🔼 回到頂部](#目錄)

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

@@ -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

@@ -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

@@ -28,6 +28,8 @@ services:
env_file: .env
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
read_only: true
tmpfs:
- /app/.next/cache # next image caching
security_opt:
- no-new-privileges:true
cap_drop:

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

@@ -8,13 +8,13 @@ services:
- TARGET=godoxy
container_name: godoxy-proxy-dev
restart: unless-stopped
env_file: dev.env
environment:
TZ: Asia/Hong_Kong
API_ADDR: :8999
API_ADDR: 127.0.0.1:8999
API_USER: dev
API_PASSWORD: 1234
API_SKIP_ORIGIN_CHECK: true
API_JWT_SECURE: false
API_JWT_TTL: 24h
DEBUG: true
API_SECRET: 1234567891234567
@@ -30,8 +30,7 @@ services:
- ./dev-data/error_pages:/app/error_pages:ro
- ./dev-data/data:/app/data
- ./dev-data/logs:/app/logs
depends_on:
- tinyauth
- ~/certs/myCA.pem:/etc/ssl/certs/ca.crt:ro
tinyauth:
image: ghcr.io/steveiliop56/tinyauth:v3
container_name: tinyauth

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

@@ -56,6 +56,7 @@ func NewHandler() *gin.Engine {
v1Auth.GET("/callback", authApi.Callback)
v1Auth.POST("/callback", authApi.Callback)
v1Auth.POST("/logout", authApi.Logout)
v1Auth.GET("/logout", authApi.Logout)
}
}

View File

@@ -1,17 +0,0 @@
package apitypes
type ErrorCode int
const (
ErrorCodeUnauthorized ErrorCode = iota + 1
ErrorCodeNotFound
ErrorCodeInternalServerError
)
func (e ErrorCode) String() string {
return []string{
"Unauthorized",
"Not Found",
"Internal Server Error",
}[e]
}

View File

@@ -5,14 +5,14 @@ import (
"github.com/yusing/go-proxy/internal/auth"
)
// @x-id "check"
// @x-id "check"
// @Base /api/v1
// @Summary Check authentication status
// @Description Checks if the user is authenticated by validating their token
// @Tags auth
// @Produce plain
// @Success 200 {string} string "OK"
// @Failure 403 {string} string "Forbidden: use X-Redirect-To header to redirect to login page"
// @Failure 302 {string} string "Redirects to login page or IdP"
// @Router /auth/check [head]
func Check(c *gin.Context) {
auth.AuthCheckHandler(c.Writer, c.Request)

View File

@@ -12,7 +12,6 @@ import (
// @Tags auth
// @Produce plain
// @Success 302 {string} string "Redirects to login page or IdP"
// @Failure 403 {string} string "Forbidden(webui): follow X-Redirect-To header"
// @Failure 429 {string} string "Too Many Requests"
// @Router /auth/login [post]
func Login(c *gin.Context) {

View File

@@ -13,6 +13,7 @@ import (
// @Produce plain
// @Success 302 {string} string "Redirects to home page"
// @Router /auth/logout [post]
// @Router /auth/logout [get]
func Logout(c *gin.Context) {
auth.GetDefaultAuth().LogoutHandler(c.Writer, c.Request)
}

View File

@@ -239,8 +239,8 @@
"type": "string"
}
},
"403": {
"description": "Forbidden: use X-Redirect-To header to redirect to login page",
"302": {
"description": "Redirects to login page or IdP",
"schema": {
"type": "string"
}
@@ -267,12 +267,6 @@
"type": "string"
}
},
"403": {
"description": "Forbidden(webui): follow X-Redirect-To header",
"schema": {
"type": "string"
}
},
"429": {
"description": "Too Many Requests",
"schema": {
@@ -285,6 +279,26 @@
}
},
"/auth/logout": {
"get": {
"description": "Logs out the user by invalidating the token",
"produces": [
"text/plain"
],
"tags": [
"auth"
],
"summary": "Logout",
"responses": {
"302": {
"description": "Redirects to home page",
"schema": {
"type": "string"
}
}
},
"x-id": "logout",
"operationId": "logout"
},
"post": {
"description": "Logs out the user by invalidating the token",
"produces": [

View File

@@ -1581,8 +1581,8 @@ paths:
description: OK
schema:
type: string
"403":
description: 'Forbidden: use X-Redirect-To header to redirect to login page'
"302":
description: Redirects to login page or IdP
schema:
type: string
summary: Check authentication status
@@ -1600,10 +1600,6 @@ paths:
description: Redirects to login page or IdP
schema:
type: string
"403":
description: 'Forbidden(webui): follow X-Redirect-To header'
schema:
type: string
"429":
description: Too Many Requests
schema:
@@ -1613,6 +1609,19 @@ paths:
- auth
x-id: login
/auth/logout:
get:
description: Logs out the user by invalidating the token
produces:
- text/plain
responses:
"302":
description: Redirects to home page
schema:
type: string
summary: Logout
tags:
- auth
x-id: logout
post:
description: Logs out the user by invalidating the token
produces:

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

@@ -247,12 +247,7 @@ func (auth *OIDCProvider) LoginHandler(w http.ResponseWriter, r *http.Request) {
SetTokenCookie(w, r, auth.getAppScopedCookieName(CookieOauthState), state, 300*time.Second)
// redirect user to Idp
url := auth.oauthConfig.AuthCodeURL(state, optRedirectPostAuth(r))
if IsFrontend(r) {
w.Header().Set("X-Redirect-To", url)
w.WriteHeader(http.StatusForbidden)
} else {
http.Redirect(w, r, url, http.StatusFound)
}
http.Redirect(w, r, url, http.StatusFound)
}
func parseClaims(idToken *oidc.IDToken) (*IDTokenClaims, error) {

View File

@@ -125,12 +125,11 @@ func (auth *UserPassAuth) PostAuthCallbackHandler(w http.ResponseWriter, r *http
return
}
SetTokenCookie(w, r, auth.TokenCookieName(), token, auth.tokenTTL)
http.Redirect(w, r, "/", http.StatusFound)
w.WriteHeader(http.StatusOK)
}
func (auth *UserPassAuth) LoginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Redirect-To", "/login")
w.WriteHeader(http.StatusForbidden)
http.Redirect(w, r, "/login", http.StatusFound)
}
func (auth *UserPassAuth) LogoutHandler(w http.ResponseWriter, r *http.Request) {

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

@@ -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)

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)
@@ -120,7 +125,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 +134,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 +150,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 +244,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

@@ -26,6 +26,8 @@ services:
restart: unless-stopped
env_file: .env
read_only: true
tmpfs:
- /app/.next/cache # next image caching
security_opt:
- no-new-privileges:true
cap_drop:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

BIN
screenshots/routes.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

BIN
screenshots/servers.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

After

Width:  |  Height:  |  Size: 476 KiB