Compare commits

...

407 Commits

Author SHA1 Message Date
yusing
aaa3c9a8d8 fix(swagger): correct type names in swagger docs
Rename icon-related types in swagger docs:
- homepage.FetchResult → iconfetch.Result
- homepage.IconMetaSearch → iconlist.IconMetaSearch
- homepage.IconSource → icons.Source
- Shorten enum varnames (IconSourceAbsolute → SourceAbsolute, etc.)
- Add x-nullable: true to rules arrays
2026-01-10 15:57:56 +08:00
yusing
bc44de3196 feat(rules): add "on: default" rule syntax for default rule
- Add OnDefault rule type that matches when no other rules match
- Add validation to prevent multiple default rules
- Fix typo: extension → extensions in route config JSON tag
2026-01-10 15:53:26 +08:00
yusing
12b784d126 feat(serialization): add validation support for custom slice types
Enhanced the ConvertSlice function to include validation for destination slices that implement the CustomValidator interface. If validation fails, errors are collected and returned, ensuring data integrity during slice conversion.
2026-01-10 15:49:58 +08:00
yusing
71f6636cc3 refactor(serialization): optimize deserialization 2026-01-10 15:43:34 +08:00
yusing
cc1fe30045 refactor(scripts/wiki): rewrite markdown links when syncing impl docs to wiki
- Convert intra-repo README links to VitePress routes for SPA navigation
- Rewrite source file references (e.g., config.go:29) to GitHub blob links
- Makefile now passes REPO_URL to update-wiki for link rewriting
- Correct agent README.md file links from full to relative paths
- skip introduction.md when syncing
2026-01-10 13:54:22 +08:00
yusing
4ec352f1f6 refactor(homepage/icon): check service health before fetching icons and add retry logic
The icon fetching logic now checks if the target service is healthy before
attempting to fetch icons. If the health monitor reports an unhealthy status,
the function returns HTTP 503 Service Unavailable instead of proceeding.

Additionally, the icon cache lookup now includes infinite retry logic with a
15-second backoff interval, improving resilience during transient service
outages. Previously, failed lookups would not be retried.

The `route` interface was extended with a `HealthMonitor()` method to support
the health check functionality.
2026-01-09 21:48:35 +08:00
yusing
df530245bd fix(homepage/icon): set icons provider on init (introduced in 74f97a6621) 2026-01-09 21:41:32 +08:00
yusing
1a022bb3f4 refactor(route): improve References method to handle FQDN alias 2026-01-09 21:38:21 +08:00
yusing
2e57ca7743 fix(agent/stream) TCP/UDP server now handle stream headers with read deadlines 2026-01-09 21:22:51 +08:00
yusing
69d04f1b76 refactor(agent): remove goutils/server dependency and use direct HTTP server setup
- Replaced `goutils/server` helper library with manual HTTP server configuration for agent socketproxy
- Removed entire `agent/pkg/server/server.go` package (43 lines) that wrapped TLS/HTTP server functionality
- Added explicit TCP listener and integrated zerolog with server's error logging
- Cleaned up 17 unused indirect agent dependencies
2026-01-09 20:27:48 +08:00
yusing
74f97a6621 refactor(homepage): reorganize icons into dedicated package structure
Split the monolithic `internal/homepage` icons functionality into a structured package hierarchy:

- `internal/homepage/icons/` - Core types (URL, Key, Meta, Provider, Source, Variant)
- `internal/homepage/icons/fetch/` - Icon fetching logic (content.go, fetch.go, route.go)
- `internal/homepage/icons/list/` - Icon listing and search (list_icons.go, list_icons_test.go)

Moved icon-related code from `internal/homepage/`:
- `icon_url.go` → `icons/url.go` (+ url_test.go)
- `content.go` → `icons/fetch/content.go`
- `route.go` → `icons/fetch/route.go`
- `list_icons.go` → `icons/list/list_icons.go` (+ list_icons_test.go)

Updated all consumers to use the new package structure:
- `cmd/main.go`
- `internal/api/v1/favicon.go`
- `internal/api/v1/icons.go`
- `internal/idlewatcher/handle_http.go`
- `internal/route/route.go`
2026-01-09 12:06:54 +08:00
yusing
dc1b70d2d7 chore(deps): update dependencies 2026-01-09 10:57:24 +08:00
Yuzerion
6fac5d2d3e feat(agent): agent stream tunneling with TLS and dTLS (UDP) (#188)
* **New Features**
  * Multiplexed TLS port: HTTP API and a custom stream protocol can share one port via ALPN.
  * Agent-side TCP and DTLS/UDP stream tunneling with health-check support and runtime capability detection.
  * Agents now advertise per-agent stream support (TCP/UDP).

* **Documentation**
  * Added comprehensive stream protocol documentation.

* **Tests**
  * Extended integration and concurrency tests covering multiplexing, TCP/UDP streams, and health checks.

* **Chores**
  * Compose/template updated to expose both TCP and UDP ports.
2026-01-09 10:52:35 +08:00
yusing
4275cdae38 docs: enhance package README documentation 2026-01-09 10:27:55 +08:00
yusing
45c821fa98 docs: simplify agent/pkg/certs README 2026-01-09 10:13:32 +08:00
yusing
d4b7ae808f fix(route): allow hostname for stream routes; introduced in 3643add8a3 2026-01-09 10:09:59 +08:00
yusing
7687dca456 fix(middleware/redirect): use net.JoinHostPort for setting HTTPS host 2026-01-09 02:29:50 +08:00
yusing
45d6e3bab7 fix(stream): properly handle remote stream scheme IPv4/6 2026-01-09 01:49:22 +08:00
yusing
41eb8c2ffa fix(monitor): remove unnecssary return type 2026-01-09 00:42:07 +08:00
yusing
2e3ebefc4e fix(health/http): potential panic when error is tlsErr 2026-01-09 00:41:25 +08:00
yusing
5aa7dc09e5 fix(health): remove unnecessary containerId parameter 2026-01-09 00:40:08 +08:00
yusing
c7d4703622 docs: update README for autocert package to reflect changes in renewal scheduling and primary consumers 2026-01-09 00:06:27 +08:00
yusing
7e99f3465f docs: update goutils docs 2026-01-08 23:48:24 +08:00
yusing
e9d7edef12 docs: add per package README for implementation details (AI generated with human review) 2026-01-08 23:39:19 +08:00
yusing
13441286d1 docs(idlewatcher): update README to include loading page and SSE endpoint details
- Added information about the loading page (HTML + JS + CSS) and the SSE endpoint for wake events.
- Clarified the health monitor implementation and readiness tracking in the architecture overview.
- Correct state machine syntax.
2026-01-08 20:31:44 +08:00
yusing
86f35878fb feat(docs): add health check and monitor packages README; mermaid styling fix 2026-01-08 18:18:17 +08:00
yusing
7556a06716 feat(scriptsi): add script to sync implementation docs with wiki
- Introduced a new `update-wiki` script to automate the synchronization of implementation documentation from the repository to the wiki.
- Added necessary configuration files including `package.json`, `tsconfig.json`, and `.gitignore` for the new script.
- Updated the Makefile to include a target for running the `update-wiki` script.
2026-01-08 18:17:06 +08:00
yusing
7385761bdf fix(health): correct context handling, move NewMonitor, and improve docker health check errors
- Correct BaseContext nil check in Context() method
- Move NewMonitor from monitor.go to new.go
- Export ErrDockerHealthCheckFailedTooManyTimes and add ErrDockerHealthCheckNotAvailable
- Return ErrDockerHealthCheckNotAvailable when container has no health check configured
- Only log first docker health check failure and skip logging for ErrDockerHealthCheckNotAvailable
- Use mon.Context() instead of mon.task.Context() to avoid nil panic
2026-01-08 18:15:02 +08:00
yusing
581503e160 refactor: move internal/watcher/health to internal/health 2026-01-08 15:08:02 +08:00
yusing
243e7e9e95 refactor(health): restructure health check implementations into dedicated check package
- Move health check implementations from monitor/ to new check/ package
- Add h2c, tcp4/6, udp4/6 scheme support to agent health check API
- Add timeout URL parameter to agent health check endpoint
- Remove unused agent dependencies (dnsproviders, lego, various cloud SDKs)
- Use net.JoinHostPort instead of fmt.Sprintf for port joining
2026-01-08 14:54:33 +08:00
yusing
8b5cb947c8 refactor(agent): extract agent pool and HTTP utilities to dedicated package
Moved non-agent-specific logic from agent/pkg/agent/ to internal/agentpool/:
- pool.go: Agent pool management (Get, Add, Remove, List, Iter, etc.)
- http_requests.go: HTTP utilities (health checks, forwarding, websockets, reverse proxy)
- agent.go: Agent struct with HTTP client management

This separates general-purpose pool management from agent-specific configuration,
improving code organization and making the agent package focused on agent config only.
2026-01-08 12:02:21 +08:00
yusing
9ea9e62ee8 refactor: remove NoCopy struct; move RefCounter struct to goutils and update usage; remove internal/utils entirely 2026-01-07 17:17:12 +08:00
yusing
1ebba20216 fix(docker): add TLS check; correct dial handling and reconnection for custom docker provider; modernize pointer arithemetic with unsafe.Add 2026-01-07 15:28:53 +08:00
yusing
7bfb57ea30 fix(stream): nil panic for excluded routes 2026-01-07 15:24:28 +08:00
yusing
25ceb512b4 feat(route): add bind address support for TCP/UDP routes
- Introduced a new `Bind` field in the route configuration to specify the address to listen on for TCP and UDP routes.
- Defaulted the bind address to "0.0.0.0" if not provided.
- Enhanced validation to ensure the bind address is a valid IP.
- Updated stream initialization to use the correct network type (tcp4/tcp6 or udp4/udp6) based on the bind address.
- Refactored stream creation functions to accept the network type as a parameter.
2026-01-07 15:05:55 +08:00
yusing
9205af3a4f feat(api/cert): enhance certificate info retrieval
- Introduced a new method `GetCertInfos` to fetch details of all available certificates.
- Updated the `Info` handler to return an array of `CertInfo` instead of a single certificate.
- Improved error handling for cases with no available certificates.
- Refactored related error messages for clarity.
2026-01-07 10:54:33 +08:00
yusing
08f4d9e95f chore: update goutils 2026-01-07 10:24:08 +08:00
yusing
a44b9e352c refactor(docker): simplify flow of isLocal check 2026-01-06 16:38:49 +08:00
yusing
424398442b refactor: replace gperr.Builder with gperr.Group for concurrent error handling
- Updated various files to utilize gperr.Group for cleaner concurrency error handling.
- Removed sync.WaitGroup usage, simplifying the code structure.
- Ensured consistent error reporting across different components.
2026-01-06 16:29:35 +08:00
yusing
724617a2b3 chore(go.mod): update goquery comment and add description for x/sync package 2026-01-05 20:58:56 +08:00
yusing
61c8ac04e8 feat(autocert): add back inwx provider 2026-01-05 20:55:04 +08:00
yusing
cc27942c4d chore(deps): update dependencies 2026-01-05 20:47:56 +08:00
yusing
1c2515cb29 chore(docs): add README.md across multiple packages 2026-01-04 22:01:48 +08:00
yusing
45720db754 fix(Makefile): correct test command 2026-01-04 21:57:25 +08:00
yusing
1b9cfa6540 fix(autocert): forceRenewalDoneCh was never closed 2026-01-04 20:40:38 +08:00
yusing
f1d906ac11 fix(test): update test expectations 2026-01-04 20:31:11 +08:00
yusing
2835fd5fb0 fix(autocert): ensure extra certificate registration and renewal scheduling
Extra providers were not being properly initialized during NewProvider(),
causing certificate registration and renewal scheduling to be skipped.

- Add ConfigExtra type with idx field for provider indexing
- Add MergeExtraConfig() for inheriting main provider settings
- Add setupExtraProviders() for recursive extra provider initialization
- Refactor NewProvider to return error and call setupExtraProviders()
- Add provider-scoped logger with "main" or "extra[N]" name
- Add batch operations: ObtainCertIfNotExistsAll(), ObtainCertAll()
- Add ForceExpiryAll() with completion tracking via WaitRenewalDone()
- Add RenewMode (force/ifNeeded) for controlling renewal behavior
- Add PrintCertExpiriesAll() for logging all provider certificate expiries

Summary of staged changes:
- config.go: Added ConfigExtra type, MergeExtraConfig(), recursive validation with path uniqueness checking
- provider.go: Added provider indexing, scoped logger, batch cert operations, force renewal with completion tracking, RenewMode control
- setup.go: New file with setupExtraProviders() for proper extra provider initialization
- setup_test.go: New tests for extra provider setup
- multi_cert_test.go: New tests for multi-certificate functionality
- renew.go: Updated to use new provider API with error handling
- state.go: Updated to handle NewProvider error return
2026-01-04 20:30:58 +08:00
yusing
11d0c61b9c refactor(state): replace Entrypoint method with ShortLinkMatcher interface
- Cleaned up agent go.mod by removing unused indirect dependencies.
2026-01-04 12:43:05 +08:00
Yuzerion
c00854a124 feat(autocert): add multi-certificate support (#185)
Multi-certificate, SNI matching with exact map and suffix tree

Add support for multiple TLS certificates with SNI-based selection. The
root provider maintains a single centralized SNI matcher that uses an
exact match map for O(1) lookups, falling back to a suffix tree for
wildcard matching.

Key features:
- Add `Extra []Config` field to autocert.Config for additional certificates
- Each extra entry must specify unique `cert_path` and `key_path`
- Extra certs inherit main config (except `email` and `extra` fields)
- Extra certs participate in ACME obtain/renew cycles independently
- SNI selection precedence: exact match > wildcard match, main > extra
- Single centralized SNI matcher on root provider rebuilt after cert changes

The SNI matcher structure:
- Exact match map: O(1) lookup for exact domain matches
- Suffix tree: Efficient wildcard matching (e.g., *.example.com)

Implementation details:
- Provider.GetCert() now uses SNI from ClientHelloInfo for selection
- Main cert is returned as fallback when no SNI match is found
- Extra providers are created as child providers with merged configs
- SNI matcher is rebuilt after Setup() and after ObtainCert() completes
2026-01-04 00:37:26 +08:00
yusing
117dbb62f4 refactor(docker): accept unix and ssh scheme for providers 2026-01-03 20:06:31 +08:00
yusing
2c28bc116c fix(h2c_test_server): correct listening on message 2026-01-03 12:58:14 +08:00
yusing
1d90bec9ed refactor(benchmark): restart bench server after each run 2026-01-03 12:54:18 +08:00
yusing
b2df749cd1 refactor(io,reverseproxy): suppress "client disconnected" error; optimize CopyClose method 2026-01-03 12:41:11 +08:00
yusing
1916f73e78 refactor(route): modernize code with unsafe.Add 2026-01-03 12:40:55 +08:00
yusing
99ab9beb4a refactor(http/transport): increase MaxIdleConnsPerHost to 1000 2026-01-03 12:40:28 +08:00
yusing
5de064aa47 refactor(benchmark): replace whoami service with bench server
- Updated dev.compose.yml to define a new bench service that serves 4096 bytes of random data.
- Modified configurations for Traefik, Caddy, and Nginx to route traffic to the new bench service.
- Added Dockerfile and Go application for the bench server, including necessary Go modules.
- Updated benchmark script to target the new bench service endpoint.
2026-01-03 12:40:10 +08:00
yusing
880e11c414 refactor(http/reverseproxy): performance improvement
- Replaced req.Clone with req.WithContext and url/header/trailer cloning.
- Added conditional handling for "Expect" headers to manage 1xx responses with appropriate tracing.
2026-01-03 02:30:15 +08:00
yusing
0dfce823bf refactor(http): performance improvement
- Introduced a sync.Pool for ResponseRecorder to optimize memory usage.
- Updated ServeHTTP method to utilize the new GetResponseRecorder and PutResponseRecorder functions.
- Adjusted NewResponseRecorder to leverage the pooling mechanism.
2026-01-03 02:20:01 +08:00
yusing
c2583fc756 refactor(benchmark): update whoami service configuration to use FQDN alias 2026-01-03 02:10:00 +08:00
yusing
cf6246d58a refactor(benchmark): remove unused Docker socket configuration from benchmark service 2026-01-03 02:04:49 +08:00
yusing
fb040afe90 refactor(benchmark): benchmark script functionality and fairness 2026-01-03 00:57:50 +08:00
yusing
dc8abe943d feat(benchmark): enhance dev.compose.yml with benchmark services and scripts
- Added benchmark services (whoami, godoxy, traefik, caddy, nginx) to dev.compose.yml.
- Introduced a new benchmark.sh script for load testing using wrk and h2load.
- Updated Makefile to include a benchmark target for easy execution of the new script.
2026-01-03 00:28:59 +08:00
yusing
587b83cf14 fix(idlewatcher): pass context to ProxmoxProvider 2026-01-02 22:17:40 +08:00
yusing
a4658caf02 refactor(config): correct logic in InitFromFile 2026-01-02 21:56:34 +08:00
yusing
ef9ee0e169 feat(websocket): update goutils - deduplicate data to avoid unnecessary traffic 2026-01-02 18:04:08 +08:00
yusing
7eadec9752 chore: remove unused utils/deep_equal.go 2026-01-02 18:03:13 +08:00
yusing
dd35a4159f refactor(api/health): simplify health info type
- Updated health-related functions to return simplified health information.
- Introduced HealthStatusString type for correct swagger and schema generation.
- Refactored HealthJSON structure to utilize the new HealthStatusString type.
2026-01-02 18:02:49 +08:00
yusing
f28667e23e refactor: add context handling in various functions
- Modified functions to accept context.Context as a parameter for better context management.
- Updated Init methods in Proxmox and Config to use the provided context.
- Adjusted UpdatePorts and NewProxmoxProvider to utilize the context for operations.
2026-01-02 17:41:36 +08:00
yusing
8009da9e4d chore: go mod tidy 2026-01-02 15:49:03 +08:00
yusing
590743f1ef feat(entrypoint): implement short link #177
- Added ShortLinkMatcher to handle short link routing.
- Integrated short link handling in Entrypoint.
- Introduced tests for short link matching and dispatching.
- Configured default domain suffix for subdomain aliases.
2026-01-02 15:42:15 +08:00
yusing
1f4c30a48e fix(docker): update scheme validation to include 'tcp' in DockerProviderConfigDetailed 2026-01-02 10:55:42 +08:00
yusing
bae7387a5d feat(dev): add jotty and postgres-test services to dev.compose.yml 2026-01-02 01:20:05 +08:00
yusing
67fc48383d refactor(monitor): include detail in service down notification log 2026-01-02 01:17:47 +08:00
yusing
1406881071 feat(http/h2c): h2c test server with a Dockerfile
- Implemented a basic HTTP/2 server that responds with "ok" to requests.
- Updated dev.compose.yml to include a service for it
2026-01-02 01:17:28 +08:00
yusing
7976befda4 feat(http): enable HTTP/2 support in server configuration
- Added NextProtos to TLSConfig to prefer HTTP/2 and fallback to HTTP/1.1.
- Configured the server to handle HTTP/2 connections, with error logging for configuration failures.
2026-01-02 01:11:07 +08:00
yusing
8139311074 feat(healthcheck/http): implement h2c health check support and refactor request handling
- Added support for health checks using the h2c scheme.
- Refactored common header setting into a dedicated function.
- Updated CheckHealth method to differentiate between HTTP and h2c checks.
2026-01-02 00:46:48 +08:00
yusing
2690bf548d chore: update swagger add h2c scheme type 2026-01-01 18:56:11 +08:00
yusing
d3358ebd89 feat(http/reverseproxy): h2c support with scheme: h2c 2026-01-01 18:54:49 +08:00
yusing
fd74bfedf0 fix(agent): improve url handling to not break urls with encoded characters 2026-01-01 18:25:27 +08:00
yusing
a47170da39 feat(metrics): add IsExcluded field to RouteUptimeAggregate for enhanced status tracking
- updated swagger
2026-01-01 13:20:17 +08:00
yusing
89a4ca767d fix(homepage): improve alphabetical sorting by normalizing item names (#181)
- Updated the sorting function to use Title case for item names to ensure consistent alphabetical ordering.
2026-01-01 12:31:36 +08:00
yusing
3dbbde164b fix(route): enhance host parsing with port suffix support
- Added logic to strip the trailing :port from the host when searching for routes.
- Updated findRouteByDomains function to ensure consistent host formatting.
- Added related tests
2025-12-30 22:46:38 +08:00
yusing
e75eede332 chore(goutils): update subproject commit reference to 51a75d68 2025-12-30 22:01:01 +08:00
yusing
e4658a8f09 fix(route): update health monitor initialization to use implementation instance 2025-12-30 21:59:43 +08:00
yusing
e25ccdbd24 chore: upgrade dependencies 2025-12-30 21:56:54 +08:00
yusing
5087800fd7 fix(tests/metrics): correct syntax error 2025-12-30 21:53:10 +08:00
yusing
d7f33b7390 chore(.gitignore): add dev-data directory to ignore list 2025-12-30 21:52:04 +08:00
yusing
1978329314 feat(route): add CommandRoute for routing requests to specified routes
- Introduced CommandRoute to handle routing requests to other defined routes.
- Added validation to ensure a single argument is provided for the route.
- Implemented command handler to serve the specified route or return a 404 error if not found.
2025-12-30 21:49:47 +08:00
yusing
dba8441e8a refactor(routes): add excluded routes to health check and route list
- Updated route iteration to include all routes, including excluded ones.
- Renamed existing functions for clarity.
- Adjusted health info retrieval to reflect changes in route iteration.
- Improved route management by adding health monitoring capabilities for excluded routes.
2025-12-30 12:39:58 +08:00
yusing
44fc678496 refactor(docker): simplify docker host parsing 2025-12-29 10:38:43 +08:00
yusing
0b410311da fix(oidc): add trailing slash to OIDCAuthBasePath to work with paths like /authorize 2025-12-29 10:22:38 +08:00
yusing
dc39f0cb6e chore(swagger): add installation instruction for swaggo in Makefile 2025-12-23 17:18:59 +08:00
yusing
e232b9d122 chore(swagger): update swagger regarding new docker config structure 2025-12-23 17:18:13 +08:00
yusing
41f8d3cfc0 refactor(docker): update TLS config validation to require both CertFile and KeyFile exists or both empty 2025-12-23 12:25:26 +08:00
Yuzerion
5ab0392cd3 feat: docker over tls (#178) 2025-12-23 12:01:11 +08:00
yusing
09702266a9 feat(debug): implement debug server for development environment
- Added `listenDebugServer` function to handle debug requests.
- Introduced table based debug page with different functionalities.
- Updated Makefile to use `scc` for code analysis instead of `cloc`.
2025-12-22 16:57:47 +08:00
yusing
14f3ed95ea feat(auth): modernize block page styling 2025-12-22 15:48:55 +08:00
yusing
eb3aa21e37 fix(healthcheck): fix fileserver health check by removing zero port check 2025-12-22 12:04:09 +08:00
yusing
a6e86ea420 fix(auth): correct logic in AuthOrProceed when auth is disabled 2025-12-22 12:00:14 +08:00
yusing
dd96e09a7a refactor(docker): streamline label loading in loadDeleteIdlewatcherLabels function 2025-12-22 11:54:37 +08:00
yusing
4d08efbd4f chore(deps): upgrade dependencies 2025-12-22 11:54:13 +08:00
yusing
f67480d085 feat(oidc): make rate limit customizable; per oidc instance rate limit
- add env variables OIDC_RATE_LIMIT and OIDC_RATE_LIMIT_PERIOD
- default rate limit changed to 10 rps from 1 rps
- rate limit is no longer applied globally
2025-12-22 10:43:41 +08:00
yusing
736985b79d fix(auth): enforce HTML acceptance in OIDC login handler 2025-12-22 10:35:43 +08:00
yusing
1fb1ee0279 refactor(auth): enhance error handling in OIDC login and callback handlers with user-friendly pages 2025-12-22 10:35:07 +08:00
yusing
4b2a6023bb refactor(auth): update WriteBlockPage function to include action text and URL 2025-12-22 10:27:48 +08:00
yusing
5852053ef9 fix(config): remove duplicated reload error 2025-12-21 11:23:42 +08:00
yusing
c687795cd8 refactor(docker): remove unnecessary http client in NewClient method 2025-12-21 11:23:21 +08:00
yusing
93af695e95 refactor(list_icons): interning app category names to save memory 2025-12-20 20:43:21 +08:00
yusing
58325e60b4 refactor(docker): remove deprecated client.WithAPIVersionNegotiation() 2025-12-20 19:51:43 +08:00
yusing
b134b92704 feat(fileserver): implement spa support; add spa and index fields to config 2025-12-20 19:24:39 +08:00
yusing
376ac61279 fix(healthcheck): nil panic on health check 2025-12-20 11:07:42 +08:00
yusing
dca701e044 fix(healthcheck): nil panic on agents 2025-12-20 10:03:43 +08:00
yusing
4bb3af3671 feat(workflow): add cherry-pick workflow for tagging into compat branch 2025-12-18 23:24:48 +08:00
yusing
95efc127cf fix(idlewatcher): incorrect "dependency has positive idle timeout" error 2025-12-18 23:22:42 +08:00
yusing
6e55c4624b refactor(http): consolidate User-Agent header in health monitor 2025-12-18 00:25:47 +08:00
yusing
e9374364dd feat(reverse_proxy): add scheme mismatch handling for retry logic in reverse proxy 2025-12-18 00:24:46 +08:00
yusing
216679eb8d fix(docker): nil panic reading container names 2025-12-17 23:17:11 +08:00
yusing
505a3d3972 refactor(http): enhance health check error logic by treating all 5xx as unhealthy 2025-12-17 17:43:59 +08:00
yusing
27512b4d04 chore: upgrade dependencies 2025-12-17 17:43:53 +08:00
yusing
88d7255c7a fix(idlewatcher): directly serve the request on ready instead of redirecting 2025-12-17 11:46:52 +08:00
yusing
ea67095967 refactor(docker): streamline idlewatcher label handling
- Introduced a map for idlewatcher labels to simplify the loading of configuration values.
- Simplify logic to check for the presence of an idle timeout and handle dependencies.
2025-12-17 10:33:37 +08:00
yusing
86a46d191d feat(idlewatcher): add option to disable loading page 2025-12-17 10:33:33 +08:00
yusing
b7250b29e0 fix(idlewatcher): improve container readiness handling in wakeFromHTTP
- Updated the wakeFromHTTP method to send a 100 Continue response to prevent client wait-header timeout.
- Implemented logic for non-HTML requests to wait for the container to become ready, returning an error message if it times out, or redirecting if successful.
- Adjusted the waitForReady method to return true upon receiving a ready notification.
2025-12-17 10:30:06 +08:00
yusing
e44ecc0ccc fix(access_log): fix slice out-of-bound panic on log rotation 2025-12-16 17:20:59 +08:00
yusing
6f9f995100 fix(config): nil panic introduced in ff934a4bb2911f5fa3c23d8fe6fea252d881fdc3; remove duplicated log 2025-12-16 15:04:21 +08:00
yusing
496aec6bb6 refactor: simplify and optimize deserialization 2025-12-16 14:48:33 +08:00
yusing
4afed02fc2 refactor(pool): simplify and optimize SizedPool; remove sync pool 2025-12-16 14:08:38 +08:00
yusing
f7eb4b132a refactor(config): remove unused ActiveConfig 2025-12-16 11:57:09 +08:00
yusing
ff934a4bb2 fix(config): fix default values not applied 2025-12-16 11:55:47 +08:00
yusing
db0cbc6577 refactor(config): remove unnecessary indirection 2025-12-16 11:22:17 +08:00
yusing
de3f92246f feat(rules): add protocol matching functionality
- Introduced a new checker for HTTP protocols (http, https, h3) in the routing rules.
- Added corresponding test cases to validate protocol matching behavior in requests.
2025-12-16 10:22:00 +08:00
yusing
c143593284 fix(icons): add handling for dark icons for walkxcode 2025-12-15 15:42:59 +08:00
yusing
31bf889d4a refactor(favicon): enhance FindIcon function to support icon variants
- Updated FindIcon to accept an additional variant parameter for improved icon fetching.
- Adjusted FavIcon and GetFavIconFromAlias functions to utilize the new variant handling logic.
2025-12-15 14:54:05 +08:00
yusing
baa7e72ad6 refactor(icon): improve handling in WithVariant 2025-12-15 14:42:31 +08:00
yusing
f43e07fe60 fix(favicon): enhance variant handling in GetFavIconFromAlias function
- Added fallback logic to handle cases where the requested icon variant is unavailable.
- If variant not provided, do not call WithVariant.
2025-12-15 14:37:33 +08:00
yusing
d319ee99ad fix(favicon): correct icon cache key in FindIcon method 2025-12-15 14:31:16 +08:00
yusing
ab58559afc refactor(icon): add variant handling for absolute/relative icons in WithVariant method 2025-12-15 14:30:31 +08:00
yusing
a6bdbb5603 chore: update api swagger 2025-12-15 12:28:14 +08:00
yusing
a0c589c546 feat(favicon): add variant support for favicons
- Introduced a new Variant field in GetFavIconRequest to specify icon variants (light/dark).
- Updated GetFavIconFromAlias function to handle the variant when fetching favicons.
- Added WithVariant method in IconURL to manage icon variants effectively.
2025-12-15 12:28:03 +08:00
yusing
76b8252755 fix(socket-proxy): update golang version. fix Dockerfile 2025-12-10 17:56:04 +08:00
yusing
d547872a41 fix(ci): correct socket-proxy github workflow 2025-12-10 17:39:39 +08:00
yusing
8d4618cedf chore(deps): go mod tidy 2025-12-10 17:37:46 +08:00
yusing
2ba758939b chore(deps): upgrade dependencies 2025-12-10 17:27:00 +08:00
yusing
fdd37b777a fix(http): 'runtime error: comparing uncomparable type httputils.UnwrittenBody' 2025-12-10 17:23:13 +08:00
yusing
bc19a54976 chore(deps): upgrade dependencies in submodules 2025-12-08 14:17:14 +08:00
yusing
12d999809f fix(http): correct Unwrap method and enhance error handling in Hijack method
- Updated the Hijack method in LazyResponseModifier and ResponseModifier to return a wrapped error for unsupported hijacking.
- Added a nil check in LazyResponseModifier's Unwrap method to ensure safe access to the underlying ResponseWriter.
2025-12-08 14:06:58 +08:00
yusing
6771293336 fix(middleware): enhance response modification handling in ServeHTTP
- Replaced ResponseModifier with new LazyResponseModifier.
- Added logic to skip modification for non-HTML content.
2025-12-08 13:45:53 +08:00
yusing
d240c9dfee fix(io): limit buffer size to 16KB to avoid high memory usage and improve context propagation 2025-12-08 10:46:00 +08:00
yusing
c7eda38933 refactor(route): simplify context handling in RouteContext
- Removed unnecessary requestInternal struct and directly accessed the context field of http.Request.
- Simplified the initialization of ctxFieldOffset.
2025-12-05 18:26:34 +08:00
yusing
09caa888ad refactor(config): update config structures to use strutils.Redacted for sensitive fields
- Modified Config structs in various packages to replace string fields with strutils.Redacted to prevent logging sensitive information.
- Updated serialization methods to accommodate new data types.
- Adjusted API token handling in Proxmox configuration.
2025-12-05 18:26:16 +08:00
yusing
e41a487371 chore: remove go.work 2025-12-05 17:51:22 +08:00
yusing
7c08a8da2e Revert "ci: Add workflow to automatically merge main into compat on tag push"
This reverts commit 9930f3fa2e.
2025-12-05 16:29:45 +08:00
yusing
82df824490 chore: go mod tidy 2025-12-05 16:20:29 +08:00
yusing
2f341001c1 chore(Makefile): add socket-proxy to docker build test and update build command syntax 2025-12-05 16:10:14 +08:00
yusing
25ee8041da refactor(http,rules): move SharedData and ResponseModifier to httputils
- implemented dependency injection for rule auth handler
2025-12-05 16:06:36 +08:00
yusing
8687a57b6c fix(Dockerfile): exclude goutils in mod caching stage 2025-12-05 01:29:30 +08:00
yusing
3f4ed31e46 fix(middleware): skip modify response for websocket and event-stream requests in ServeHTTP 2025-12-05 01:18:27 +08:00
yusing
9930f3fa2e ci: Add workflow to automatically merge main into compat on tag push 2025-12-05 01:11:50 +08:00
yusing
2157545e17 fix(route): nil panic when used as an idlewatcher dependency 2025-12-05 01:10:48 +08:00
yusing
f721395ff0 refactor(healthcheck): agent health check 2025-12-05 00:45:24 +08:00
yusing
0dc7c59af1 refactor(deps): upgrade go to 1.25.5; isolate dependencies for reverseproxy, websocket and server modules 2025-12-05 00:36:16 +08:00
yusing
e3fe126a5c chore(example): introduce health check configuration defaults in example config 2025-12-04 18:08:26 +08:00
yusing
aa2575696d fix(http): handle 0 content length properly in some cases 2025-12-04 17:33:01 +08:00
yusing
c1f9c2c957 fix(middleware): skip modification for HEAD requests in ModifyHTML middleware 2025-12-04 17:27:26 +08:00
yusing
c098fef615 fix(http): enhance Content-Length handling in ResponseModifier
- Introduced origContentLength and bodyModified fields to track original content length and body modification status.
- Updated ContentLength and ContentLengthStr methods to return accurate content length based on body modification state.
- Adjusted Write and FlushRelease methods to ensure proper handling of Content-Length header.
- Modified middleware to use the new ContentLengthStr method.
2025-12-04 17:26:15 +08:00
yusing
9cdc985fb0 fix(tests): correct test expectations for middleware bypass and rules 2025-12-04 16:18:14 +08:00
yusing
2034738422 refactor(labels): refine wildcard expansion logic and tests
- Added multiple test cases for the ExpandWildcard function to cover various scenarios including basic wildcards, no wildcards, empty labels, and YAML configurations.
- Improved handling of nested maps and invalid YAML inputs.
- Ensured that explicit labels and reference aliases are correctly processed and expanded.
2025-12-04 16:16:43 +08:00
yusing
55a42b81de refactor(healthcheck): streamline health check configuration and defaults
- Moved health check constants from common package alongside type definition.
- Updated health check configuration to use struct directly instead of pointers.
- Introduced global default health check config
2025-12-04 15:19:10 +08:00
yusing
48627753d6 refactor(routes): simplify route exclusion check and health check defaults 2025-12-04 15:12:35 +08:00
yusing
09b514393d chore(deps): upgrade go version and dependencies 2025-12-04 12:18:12 +08:00
yusing
3b2ae5dbd6 refactor: move some utility functions to goutils and update references 2025-12-04 12:17:33 +08:00
yusing
fac3d67a51 chore; update goutils 2025-11-23 11:46:04 +08:00
yusing
cb642d7b32 fix(middleware): correct body mutation behavior in ServeHTTP
Refactor ServeHTTP to properly handle response body mutations by:
- Using ResponseModifier to capture response before modification
- Reading body content and allowing middleware to modify it
- Writing modified body back if changed during modification
- Ensuring proper order: RequestModifier before, ResponseModifier after next()

Previously, httputils.NewModifyResponseWriter did not correctly handle
body mutations. The new implementation captures the full response,
allows modification via modifyResponse, and properly writes back any
changes to the body.

Add BodyReader() and SetBody() methods to ResponseModifier to support
reading and replacing response body content.
2025-11-17 16:32:58 +08:00
yusing
9285977495 chore(deps): upgrade dependencies 2025-11-17 15:15:06 +08:00
yusing
e00cd8a35b fix(http): add Body field to response ModifyResponseWriter to avoid nil dereference panic 2025-11-17 15:12:13 +08:00
yusing
8ac459c038 fix(rules): ignore unsupported flush errors in ResponseModifier 2025-11-17 10:59:20 +08:00
yusing
1bcaf0dab5 fix(docker): revert to API version negotiation for Docker client 2025-11-15 10:52:03 +08:00
yusing
a291a49a0e fix(swagger): update netip.Addr definition type to string 2025-11-14 22:28:29 +08:00
yusing
28fdf3d2f4 fix(types/route): update HealthCheck JSON tag to be nullable for load-balancer routes 2025-11-14 22:16:30 +08:00
yusing
84b17baf46 refactor(rules): correct json format; remove MarshalJSON from []Rules 2025-11-14 20:53:35 +08:00
yusing
06ddb178f8 fix(proxmox): handle case when no nodes are available in AvailableNodeNames function 2025-11-14 10:59:58 +08:00
yusing
61fa7d2665 chore(debug): add debug logging for bypass rules and remove for route validation 2025-11-14 10:58:55 +08:00
yusing
615521ee1c chore: update submodule goutils 2025-11-14 10:33:11 +08:00
yusing
bbe308e821 ci: add Jenkinsfile for CI pipeline and configure SonarQube analysis 2025-11-13 23:39:23 +08:00
yusing
c156173757 refactor(docker): migrate from github.com/docker/docker to github.com/moby/moby 2025-11-13 23:03:27 +08:00
yusing
b1aae1cacf fix(rule): allow manifest.json in webui rule to make PWA work properly 2025-11-13 20:51:20 +08:00
yusing
f46552b477 fix(log): fix scrambled config log output 2025-11-13 20:49:38 +08:00
yusing
efe1350ffd chore: upgrade dependencies 2025-11-13 15:30:15 +08:00
yusing
219eedf3c5 fix(oidc): correct behavior when working with bypass rules
- Introduced a new handler for unknown paths in the OIDCProvider to prevent fallback to the default login page.
- Forced OIDC middleware to treat unknown path as logic path to redirect to login property when bypass rules is declared.
- Refactored OIDC path constants.
- Updated checkBypass middleware to enforce path prefixes for bypass rules, ensuring proper request handling.
2025-11-13 15:13:20 +08:00
yusing
f6dcc8f118 fix(idlewatcher): correct variable declaration for event data parsing in loading.js 2025-11-07 17:08:58 +08:00
yusing
4d6541c851 chore(deps): update Go version in Dockerfile and go mod tidy 2025-11-07 16:35:35 +08:00
yusing
c9db350cbc fix(route): add workaround for WebUI rule preset loading in validateRules function
- Introduced a temporary fix for loading the "webui.yml" rule preset when the container image is "godoxy-frontend".
- Added error handling for cases where the rule preset is not found.
- Marked the change with a FIXME comment to investigate the underlying issue in the future.
2025-11-07 16:16:58 +08:00
yusing
56374d595a fix(idlewatcher): correct target URL validation logic in NewWatcher function 2025-11-07 15:55:56 +08:00
yusing
d81521f293 refactor: improve HTTPS detection logic by using case-insensitive comparison for X-Forwarded-Proto header 2025-11-07 15:49:51 +08:00
yusing
e9ac3cd1a9 refactor: fix incorrect logic introduced in previous commits and improve error handling 2025-11-07 15:48:38 +08:00
yusing
d33ff2192a refactor(loadbalancer): implement sticky sessions and improve algorithm separation
- Refactor load balancer interface to separate server selection (ChooseServer) from request handling
- Add cookie-based sticky session support with configurable max-age and secure cookie handling
- Integrate idlewatcher requests with automatic sticky session assignment
- Improve algorithm implementations:
  * Replace fnv with xxhash3 for better performance in IP hash and server keys
  * Add proper bounds checking and error handling in all algorithms
  * Separate concerns between server selection and request processing
- Add Sticky and StickyMaxAge fields to LoadBalancerConfig
- Create dedicated sticky.go for session management utilities
2025-11-07 15:24:57 +08:00
yusing
910ef639a4 feat(idlewatcher): implement real-time SSE-based loading page with enhanced UX
This major overhaul of the idlewatcher system introduces a modern, real-time loading experience with Server-Sent Events (SSE) streaming and improved error handling.

- **Real-time Event Streaming**: New SSE endpoint (`/$godoxy/wake-events`) provides live updates during container wake process
- **Enhanced Loading Page**: Modern console-style interface with timestamped events and color-coded status messages
- **Improved Static Asset Management**: Dedicated paths for CSS, JS, and favicon to avoid conflicting with upstream assets
- **Event History Buffer**: Stores wake events for reconnecting clients and debugging

- Refactored HTTP request handling with cleaner static asset routing
- Added `WakeEvent` system with structured event types (starting, waking_dep, dep_ready, container_woke, waiting_ready, ready, error)
- Implemented thread-safe event broadcasting using xsync.Map for concurrent SSE connections
- Enhanced error handling with detailed logging and user-friendly error messages
- Simplified loading page template system with better asset path management
- Fixed race conditions in dependency waking and state management

- Removed `common.go` functions (canceled, waitStarted) - moved inline for better context
- Updated Waker interface to accept context parameter in Wake() method
- New static asset paths use `/$godoxy/` prefix to avoid conflicts

- Console-style output with Fira Code font for better readability
- Color-coded event types (yellow for starting, blue for dependencies, green for success, red for errors)
- Automatic page refresh when container becomes ready
- Improved visual design with better glassmorphism effects and responsive layout
- Real-time progress feedback during dependency wake and container startup

This change transforms the static loading page into a dynamic, informative experience that keeps users informed during the wake process while maintaining backward compatibility with existing routing behavior.
2025-11-07 14:58:33 +08:00
yusing
3cbd70f73a fix(health_check): correct agent routes health check logic 2025-11-07 10:21:15 +08:00
yusing
83d70d3bb2 fix(health_monitor): fix UpdateURL behavior in Docker and AgentProxied health monitor 2025-11-07 00:53:34 +08:00
yusing
bbb1b8497f fix(health_monitor): treat some errors as unhealth instead of actual error in RawHealthMonitor 2025-11-06 23:37:34 +08:00
yusing
d57d76dc65 feat(loading_page): move loading page css to style.css and serve as static asset 2025-11-06 20:32:40 +08:00
yusing
ef893974ea feat(route): implement PreferOver method for deterministic route replacement 2025-11-06 20:19:49 +08:00
yusing
b90f2409ab refactor(docker): set alias initially to have better debuggability 2025-11-06 20:15:03 +08:00
yusing
36e9b0d416 chore: upgrade go to 1.25.4 and dependencies 2025-11-06 20:13:01 +08:00
yusing
306cb7a20e fix(access_logger): fix stdout and path not working at the same time 2025-11-01 12:07:22 +08:00
yusing
e3915210aa fix(time): data race in DefaultTimeNow 2025-11-01 02:18:24 +08:00
yusing
e8fb202ea9 fix(docker): fix wildcard not working correctly with #N ref aliases 2025-11-01 02:10:09 +08:00
yusing
082b2f5da2 refactor(websocket): use only the first error and fix race condition 2025-11-01 01:28:12 +08:00
yusing
e670acb4b8 fix(access_logger): nil panic when stdout only, improve concurrency safety 2025-11-01 01:17:55 +08:00
yusing
77e486f4fe refactor(route): ensure validation and start only starts once, and lock error before finishing 2025-10-31 18:10:09 +08:00
yusing
3ccaba3163 fix(validation): prioritize pointer method for custom validation in serialization 2025-10-31 18:06:41 +08:00
yusing
705923960c feat(fileserver): add rules support for fileservers 2025-10-31 17:32:37 +08:00
yusing
ca737c8979 fix(modify-html): re-enable modifying HTML with chunked encoding 2025-10-31 17:30:23 +08:00
yusing
b6b5d4dbd7 fix(auth): handle nil defaultAuth to prevent nil panic before auth intializes 2025-10-31 17:15:03 +08:00
yusing
b2919fbaf6 feat(rules): supress some errors in rule execution 2025-10-31 17:13:09 +08:00
yusing
722c40d103 chore(examples): update example configurations with comments for certificate paths and lite variant 2025-10-30 11:45:06 +08:00
yusing
860d9c71b6 fix(pool,io): overlap memory on buffer splitting; hook in HookReadCloser should run after Close 2025-10-29 22:48:28 +08:00
yusing
e354d901c4 fix(monitor): safer approach to avoid nil panic in edge cases 2025-10-29 00:19:17 +08:00
yusing
921a8fb935 fix(monitor): handle missing container state in Docker health check 2025-10-28 23:49:59 +08:00
yusing
975354cdc1 chore(compose): comment out user for lite variant in example configuration 2025-10-28 23:15:54 +08:00
yusing
7d38bfd2d2 build: drop old image name support 2025-10-28 22:05:35 +08:00
yusing
5506cafa26 fix(rules): pages not loading correct for lite webui variant 2025-10-28 22:00:54 +08:00
yusing
9fd5bff81a fix(oidc): fix Webui OIDC loop 2025-10-28 21:54:46 +08:00
yusing
38041ca5b8 fix(pool): handle buffer capacity check in GetSized
- Added a check to ensure the buffer's capacity is sufficient before reusing it.
- Included a FIXME comment to address an unexpected condition in buffer allocation.
2025-10-28 21:48:27 +08:00
yusing
61be88c1d3 chore: upgrade dependencies 2025-10-28 21:40:23 +08:00
yusing
cb4dcb962e fix(http): nil panic in goutils http/intercept.go 2025-10-28 21:37:19 +08:00
yusing
1797a222cd fix(middlewares): correctly bypass middlewares with response rules 2025-10-28 20:44:46 +08:00
yusing
098fb7e62d fix(compose): update rootless compose example 2025-10-28 17:03:20 +08:00
yusing
d4dfec8293 refactor(http): proper ResponseWriter and headers handling across files 2025-10-28 14:43:10 +08:00
yusing
f29b69ff3b refactor(rules): remove Flush method and replace with http.NewResponseController in ResponseModifier 2025-10-27 17:46:23 +08:00
yusing
5e00e1c437 fix(middleware): correct and simplify HTML modification / buffer management logic, correct Accept-Encoding header 2025-10-27 15:08:29 +08:00
yusing
39c8cc2820 fix(auth): nil panic by handling in TryRefreshToken 2025-10-27 14:25:05 +08:00
yusing
56232dbd0e fix(monitor): nil panic in DockerHealthMonitor 2025-10-27 12:46:22 +08:00
yusing
baf774f927 fix(middleware): properly release buffer on error and not to reuse content for bytes.Buffer 2025-10-26 23:16:38 +08:00
yusing
a3c82209c6 refactor(api): disable caching completely 2025-10-26 21:33:58 +08:00
yusing
386d946bd2 feat(rules): support variables for error comand 2025-10-26 20:25:46 +08:00
yusing
ee9bf31d30 chore(compose): add comments for lite variant uid/gid configuration in example 2025-10-26 19:46:59 +08:00
yusing
2c87eebee3 chore(compose): remove host network_mode from example 2025-10-26 19:27:56 +08:00
yusing
5be784d567 chore(env): remove frontend port configuration from example files 2025-10-26 19:26:53 +08:00
yusing
a999c51bf8 fix(metrics): json marshaling 2025-10-26 16:57:16 +08:00
yusing
7ca722b256 fix(metrics): correct network data aggregation logic in system_info.go 2025-10-26 16:46:34 +08:00
yusing
51295be463 fix(json): ensure valid json 2025-10-26 16:38:08 +08:00
yusing
51fc5f017a feat(api): add sonic build tag in Makefile to let gin use sonic for json handling 2025-10-26 16:36:41 +08:00
yusing
e4996733fc fix(types): add placeholder field in VirtualMemoryStat for swagger 2025-10-26 16:04:28 +08:00
yusing
f76d86dfa2 feat(api): rules playground API
- updated swagger
2025-10-26 15:56:18 +08:00
yusing
8778f4ea73 fix(json): unmarshal error introduced in previous commit 2025-10-26 01:29:39 +08:00
yusing
6f75bb7593 refactor(api): replace apitypes module and fix swagger generation 2025-10-26 01:05:18 +08:00
yusing
964ba1eac1 chore: update dev environment configuration and base images
- Changed API_SECRET to API_JWT_SECRET in dev.compose.yml
- Updated base image from alpine to debian in dev.Dockerfile
- Upgraded golang version from 1.25.2 to 1.25.3 in Dockerfile
2025-10-25 23:31:53 +08:00
yusing
6e7b571946 feat(rules): add regex for image and font file paths in webui presets 2025-10-25 23:31:22 +08:00
yusing
fc7a81faf5 chore: upgrade dependencies 2025-10-25 23:27:35 +08:00
yusing
488ad160e7 fix(rules): ensure postform and form initialized, fix tests 2025-10-25 23:07:18 +08:00
yusing
1ec2872f3d feat(rules): replace go templates with custom variable expansion
- Replace template syntax ({{ .Request.Method }}) with $-prefixed variables ($req_method)
- Implement custom variable parser with static ($req_method, $status_code) and dynamic ($header(), $arg(), $form()) variables
- Replace templateOrStr interface with templateString struct and ExpandVars methods
- Add parser improvements for reliable quote handling
- Add new error types: ErrUnterminatedParenthesis, ErrUnexpectedVar, ErrExpectOneOrTwoArgs
- Update all tests and help text to use new variable syntax
- Add comprehensive unit and benchmark tests for variable expansion
2025-10-25 22:43:47 +08:00
yusing
9c3346dd9d fix(agent): correct usage of task in StartAgentServer and updated test expectations 2025-10-22 00:02:13 +08:00
yusing
203faa8e7e chore: update goutils 2025-10-22 00:01:27 +08:00
yusing
fbc853fa6a fix(script): incorrectly set DOCKER_SOCKET as rootless 2025-10-20 20:48:38 +08:00
yusing
3fefbdfded chore: update goutils 2025-10-20 20:46:31 +08:00
yusing
48be6def12 feat(autocert): add hostinger autocert provider
- upgraded dependencies and submodules
2025-10-19 10:48:00 +08:00
yusing
94d6b7a168 fix(pool): missing storeFullCap when allocating new buffer 2025-10-18 19:59:14 +08:00
yusing
1ca4b4939e perf(healthcheck): stop docker client from hogging resources in health checks 2025-10-18 19:35:32 +08:00
yusing
f8716d990e perf(pool): split bytes pool into tiered sized and unsized pools
- Remove BytesPoolWithMemory; split into UnsizedBytesPool and 11-tier SizedBytesPool
- Track buffer capacities with xsync Map to prevent capacity leaks
- Improve buffer reuse: split large buffers and put remainders back in pool
- Optimize small buffers to use unsized pool
- Expand test coverage and benchmarks for various allocation sizes
2025-10-18 17:38:01 +08:00
yusing
5a91db8d10 perf: use fasthttp for health checks; upgrade go to 1.25.3 2025-10-17 22:50:13 +08:00
yusing
3e73be60a1 fix(gotify): error if token not present 2025-10-16 10:25:38 +08:00
yusing
af9363209b fix(systeminfo): correct system info JSON format 2025-10-16 10:09:51 +08:00
yusing
ccc35b2a00 refactor: remove functional.Set wrapper 2025-10-16 10:08:25 +08:00
yusing
44536139c1 refactor: refine byte pools usage and fix memory leak in rules 2025-10-15 23:53:26 +08:00
yusing
2b4c39a79e perf(mem): reduced memory usage in metrics and task by string interning and deduplicating fields 2025-10-15 23:51:47 +08:00
yusing
ddf78aacba perf(logging): optimize multi-line message formatting
- Refactors the fmtMessage function to use strings.Builder
  - Simplifies multi-writer creation with a helper function
  - Updates the new console writer initialization pattern
  - Moves InitLogger function to the top
  - Fixed NewLoggerWithFixedLevel
2025-10-15 21:18:25 +08:00
yusing
f5a006ce81 refactor(task): fix onFinish not being called and simplify by replacing semaphore with channel 2025-10-15 15:07:19 +08:00
yusing
290af4e311 perf(mem): replace Scheme and ExcludedReason string with uint8 type to reduce mem usage 2025-10-15 14:35:44 +08:00
yusing
feafdf05f2 fix(validation): correct CustomValidator and strutils.Parser handling, add tests 2025-10-15 14:20:47 +08:00
yusing
b09bfd6c1e fix(serialization): use replace os.LookupEnv with env.LookupEnv 2025-10-15 00:12:17 +08:00
yusing
e13b18621d fix(favicon): add status code in error message 2025-10-15 00:11:38 +08:00
Yuzerion
53f3397b7a feat(rules): add post-request rules system with response manipulation (#160)
* Add comprehensive post-request rules support for response phase
* Enable response body, status, and header manipulation via set commands
* Refactor command handlers to support both request and response phases
* Implement response modifier system for post-request template execution
* Support response-based rule matching with status and header checks
* Add comprehensive benchmarks for matcher performance
* Refactor authentication and proxying commands for unified error handling
* Support negated conditions with !
* Enhance error handling, error formatting and validation
* Routes: add `rule_file` field with rule preset support
* Environment variable substitution: now supports variables without `GODOXY_` prefix

* new conditions:
  * `on resp_header <key> [<value>]`
  * `on status <status>`
* new commands:
  * `require_auth`
  * `set resp_header <key> <template>`
  * `set resp_body <template>`
  * `set status <code>`
  * `log <level> <path> <template>`
  * `notify <level> <provider> <title_template> <body_template>`
2025-10-14 23:53:06 +08:00
yusing
19968834d2 fix(autocert): added back timewebcloud provider 2025-10-13 07:12:38 +08:00
yusing
d41c6f8d77 fix(cookie): net/http: invalid Cookie.Domain .0.0.1:3000 2025-10-13 07:09:17 +08:00
yusing
dcc5ab8952 fix(entrypoint): 404 everything with match_domains 2025-10-13 06:45:29 +08:00
yusing
cc8858332d fix(agent): remove leftover pointer in tls.Config 2025-10-12 22:23:14 +08:00
yusing
82f02ea2bf fix(rules): nil panic when only having default rule 2025-10-12 22:21:01 +08:00
yusing
046ff8a020 chore: update config example about new not_found rule 2025-10-12 22:02:52 +08:00
yusing
dc9ae32e8f feat(entrypoint): add not found rule to customize 404 behavior 2025-10-12 21:04:49 +08:00
yusing
5640d5d454 fix(serialization): correctly handle json tag 2025-10-12 20:59:12 +08:00
yusing
c66de99fcb perf: further optimize http and body buffer handling 2025-10-12 20:57:51 +08:00
yusing
eef994082c fix(panic): nil panic in IterRoutes 2025-10-12 16:51:52 +08:00
yusing
8c670ab92e chore: update README.md and config.example.yml for new changes 2025-10-12 14:25:55 +08:00
yusing
d11ddb7c91 fix(ci): checkout submodules 2025-10-12 14:23:00 +08:00
yusing
78aea4b4d2 fix: gopsutil 2025-10-12 13:04:28 +08:00
yusing
80dd142861 refactor(rules): rename Static and Returning commands into Terminating and NonTerminating commands 2025-10-12 09:38:06 +08:00
yusing
92aa61e732 refactor(log): simplify access logger and disable stdout buffering
- Remove MultiWriter complexity and use single writer interface
  - Disable buffering for stdout logging to ensure immediate output
  - Replace slice-based closer/rotate support with type assertions
  - Simplify rotation result handling by passing result pointer
  - Update buffer size constants and improve memory management
  - Remove redundant stdout_logger.go and multi_writer.go files
  - Fix test cases to match new rotation API signature
2025-10-11 19:14:59 +08:00
yusing
848f26aa86 test(list-icon): fix tests regarding previous changes 2025-10-11 19:05:49 +08:00
yusing
81e500fcfc fix(log): stdout logging format 2025-10-11 18:30:38 +08:00
yusing
f417e0fa25 fix(notif): remove test logging 2025-10-11 18:13:19 +08:00
yusing
cb5a8e7b9d fix(acl): correct acl log handling and add country to summary 2025-10-11 17:03:08 +08:00
yusing
16cad11e89 fix(notif): format not being applied correctly 2025-10-11 16:54:52 +08:00
yusing
2bfbdbf519 refactor(acl): adjust summary format and add total count 2025-10-11 16:13:52 +08:00
yusing
d5e9a7b3b6 fix(notif): respect Method and empty content-type 2025-10-11 16:02:18 +08:00
yusing
7ea415078f fix(config): fix error and logging handling 2025-10-11 14:00:04 +08:00
yusing
e67704695b fix(acl): complete tcp and udp wrapper interface 2025-10-11 13:47:13 +08:00
yusing
804c7eec60 fix: env parsing 2025-10-11 13:37:18 +08:00
yusing
ea8be56bf8 fix(server): race condition on server stop 2025-10-11 13:26:10 +08:00
yusing
20c77edce5 chore: go mod tidy 2025-10-11 13:20:19 +08:00
yusing
4f2f0f58e2 fix(autocert): nil dereference 2025-10-11 13:12:45 +08:00
yusing
ac8ad149b8 fix(icons): list icon logic 2025-10-11 13:07:50 +08:00
yusing
14ec80c883 fix: Dockerfile mod caching 2025-10-11 13:01:52 +08:00
yusing
5de5f854ce fix: dockerfile 2025-10-11 12:56:55 +08:00
yusing
3d8994b42e chore: enhance example config 2025-10-11 12:46:54 +08:00
yusing
66043e4a26 refactor!: simplify lego generate script; add and drop some dns provider 2025-10-11 12:44:16 +08:00
yusing
d1e403e16f fix(docker): correct image in rootless docker compose 2025-10-11 11:27:14 +08:00
yusing
e72e20af69 fix(script): add missing domain input in setup.sh 2025-10-11 11:16:39 +08:00
yusing
ad6201c27a feat(dev): add parca in dev docker for profiling 2025-10-10 23:24:39 +08:00
yusing
c4c9e9300c chore: simplify dev docker and update Makefile accordingly 2025-10-10 23:24:14 +08:00
yusing
b23c3f1c3b refactor(icons): replace mutex-based cache with atomic synk.Value
- Remove sync.RWMutex and Cache struct in favor of atomic Value
  - Implement background goroutine for periodic icon updates
  - Add backward compatibility for old cache format
  - Improve concurrent access to icon cache
  - Simplify ListAvailableIcons()
2025-10-10 23:21:30 +08:00
yusing
38c0419483 feat(config): add temporary logging for failed reloads
- Add tmpLogBuf and tmpLog fields to capture config loading logs
  - Flush temporary logs only when reload succeeds
  - Extract NewLogger function for creating custom loggers
  - Update State interface to include FlushTmpLog method
2025-10-10 22:20:12 +08:00
yusing
357ce38b18 fix(idlewatcher): correctly restart on config reload 2025-10-10 21:57:36 +08:00
yusing
ef34c3ffdd fix(server): should wait for server to stop 2025-10-10 21:47:03 +08:00
yusing
2e411373a2 fix(config): failed reload should not start providers in new state 2025-10-10 21:46:02 +08:00
yusing
3dedd66ad1 test(rules): add tests for glob and regex, remove old path glob test 2025-10-10 21:39:21 +08:00
yusing
98f047d88a fix(rules): correct dollar sign handling 2025-10-10 21:37:54 +08:00
yusing
973a58e982 fix(import): remove unused import in rules/validate.go 2025-10-10 20:49:12 +08:00
yusing
4b55d1c607 fix: missing bracket in main.go 2025-10-10 20:46:47 +08:00
yusing
63eff4707c fix: submodules url 2025-10-10 20:40:33 +08:00
yusing
55a74c36b0 refactor(acl): optimize slice allocation in logNotifyLoop 2025-10-10 20:21:53 +08:00
yusing
fbabb7b7fb refactor(acl): default not to notify allowed and skip when total is 0 2025-10-10 20:20:09 +08:00
yusing
7a1841e9a5 fix(acl): add json tag for notify 2025-10-10 15:26:08 +08:00
yusing
d82bfd0ebd feat(acl): add periodic notification system for access summaries
- Add Notify configuration with To field and interval
  - Track allowed/blocked IP counts per address
  - Send periodic summary notifications with access statistics
  - Optimize logging with channel-based processing for concurrent safety
2025-10-10 15:24:48 +08:00
yusing
1f41c035ea feat(notification): add To field to LogMessage 2025-10-10 14:47:20 +08:00
yusing
c2c9f42fb3 feat(rules): glob and regex support, env var substitution
- optimized `remote` rule for ip matching
- updated descriptions
2025-10-10 14:43:48 +08:00
yusing
60cfff3435 refactor(idlewatcher): streamline loading screen favicon handling 2025-10-10 12:55:06 +08:00
yusing
c93a460043 refactor(config): add omitempty on some fields 2025-10-10 10:34:49 +08:00
yusing
9bf7a0beef revert(config): added back pointer for agent and notification config for correct unmarshaling 2025-10-10 10:07:49 +08:00
yusing
c89c737ecd fix(pool): variable shadowing 2025-10-10 10:01:42 +08:00
yusing
382fc61a9c chore: update submodules 2025-10-10 09:57:08 +08:00
yusing
b2de33e835 fix: Makefile 2025-10-10 09:55:21 +08:00
yusing
86644054e6 refactor: use goutils/env in socket-proxy 2025-10-10 09:54:16 +08:00
yusing
c2dcabe144 refactor(rules): remove 'caller' parameter in BuildHandler 2025-10-10 09:53:44 +08:00
yusing
c59ddc1df6 refactor: add ShouldExclude() bool to Route interface 2025-10-10 09:53:08 +08:00
yusing
f4db874fd6 refactor(proxmox): rename checkIPPrivate to privateIPOrNil 2025-10-10 09:52:38 +08:00
yusing
f334f5c13c feat(config): pretty print active config and routes by provider on load / reload 2025-10-10 09:51:32 +08:00
yusing
5acc4c3894 fix(homepage): logic error in ListAvailableIcons, ensure the cache is ready in GetHomepageMeta 2025-10-10 09:27:23 +08:00
yusing
a8aa82f687 chore: upgrade dependencies 2025-10-10 09:11:29 +08:00
yusing
0f3a1ac6e6 refactor: improved task lifecycle management 2025-10-10 09:07:47 +08:00
yusing
9fceda6729 fix: data race in strings.Title 2025-10-09 23:08:30 +08:00
yusing
becb49e864 fix(uptime): set to 0 instead of returning error on overflow check 2025-10-09 22:53:38 +08:00
yusing
3aed41e078 refactor: move version.go to goutils 2025-10-09 01:14:43 +08:00
yusing
8047067b2b refactor(utils): move utils/atomic to goutils 2025-10-09 01:07:47 +08:00
yusing
c3fa7c66a7 feat(entrypoint): added CatchAll and NotFound rules and handler 2025-10-09 01:03:16 +08:00
yusing
cab68807ee refactor(config): restructured with better concurrency and error handling, reduced cross referencing 2025-10-09 01:02:24 +08:00
yusing
d08be872a0 refactor(errors): simplify gperr.Builder usage 2025-10-09 00:28:22 +08:00
yusing
bb5f0cdf09 chore(go): upgrade to go1.25.2 2025-10-08 23:38:33 +08:00
yusing
a150f1a628 refactor(config): reduce references to config.GetInstance() 2025-10-07 21:49:00 +08:00
yusing
584db2efce refactor(docker): use atomic.Int instead of plain integer 2025-10-07 21:30:12 +08:00
yusing
c27bc0e129 refactor(docker): simplify docker client initialization in api 2025-10-07 21:26:52 +08:00
yusing
b46b464e65 refactor: add goutils as submodule, remove go.mod from internal/utils 2025-10-07 20:43:41 +08:00
yusing
52ec309f6b chore: go mod tidy 2025-10-05 20:41:37 +08:00
yusing
6051f75145 refactor(favicon): improve cache and error handling 2025-10-05 20:37:27 +08:00
yusing
f4f104d206 refactor: add go-oidc as submodule 2025-10-05 12:38:40 +08:00
yusing
448a2fbd6f chore: update gopsutil 2025-10-05 12:21:05 +08:00
yusing
74224c8e87 refactor(metrics): optimize and simplify system info; add gopsutil as submodule 2025-10-05 12:05:58 +08:00
yusing
ae57edfcb0 refactor(routes): remove unnecessary indirection 2025-10-03 23:28:03 +08:00
yusing
fc23e262d7 chore: update dependencies 2025-10-03 23:26:27 +08:00
yusing
11a3935e0c refactor(serialization): streamline custom validation logic in ValidateWithCustomValidator function 2025-10-03 23:20:14 +08:00
yusing
42e7adbf86 refactor(serialization): small optimization 2025-10-03 23:19:13 +08:00
yusing
1e0c7a15d8 refactor(metrics): optimize memory allocation in period entries
- Replace heap allocation with stack-allocated array in Entries.Get() method.
- Also refactor uptime module to use value types instead of pointer types.
2025-10-03 23:19:12 +08:00
yusing
ba8edb160f refactor(metrics): replace hardcoded time with contants, merge three tickers into one 2025-10-03 23:19:12 +08:00
Yuzerion
4852efcf9c feat: faster serialization (#157)
* refactor: improve deserialization performance

* refactor(serialization): simplify string conversion logic in Convert function

* fix(serialization): default value lookup

* refactor: add comment about concurrency in RegisterDefaultValueFactory

---------

Co-authored-by: yusing <yusing@6uo.me>
2025-10-02 20:30:31 +08:00
yusing
ef40793301 fix: Dockerfile 2025-10-01 19:56:38 +08:00
yusing
80862bcd2e chore(go.mod): mod tidy and exclude problematic v0.4.2 of goutils 2025-09-29 17:58:44 +08:00
yusing
45b16abd68 refactor(health): improve health status JSON unmarshalling 2025-09-29 17:52:03 +08:00
yusing
f411e17d80 feat(json): improve JSON performance with bytedance/sonic 2025-09-29 17:43:34 +08:00
yusing
024100aa8c fix(agent): failed to parse agent proxy config: unexpected end of JSON input 2025-09-28 20:30:02 +08:00
yusing
9d508c5950 feat(health): add random delay to health monitor to prevent thundering herd problem 2025-09-28 02:35:44 +08:00
yusing
2ff5e5c0b6 chore(deps): upgrade dependencies 2025-09-27 14:19:30 +08:00
yusing
2a05c6a630 refactor: move websocket package and some http utils to seperate repo 2025-09-27 14:16:42 +08:00
yusing
6776f20332 refactor: move task, error and testing utils to separte repo; apply gofumpt 2025-09-27 13:41:50 +08:00
yusing
5043ef778f refactor: remove gphttp.ServerError method 2025-09-27 12:47:51 +08:00
yusing
22bcf1201b refactor: move some io, http and string utils to separate repo 2025-09-27 12:46:41 +08:00
yusing
acecd827d6 refactor(synk): consolidate pool statistics tracking and replace GC tracking with dropped tracking 2025-09-27 11:35:38 +08:00
yusing
b2713a4b83 refactor(health): optimize health checking 2025-09-27 11:32:18 +08:00
yusing
e2aeef3a86 refactor(synk): replace runtime weak pointer functions with weak package and simplify buffer handling 2025-09-27 11:24:50 +08:00
yusing
9545482a44 refactor(pprof): remove memory profiling settings and enhance GC logging 2025-09-27 11:24:10 +08:00
yusing
d406b940d9 style: fix some golangci-lint warnings 2025-09-26 23:45:59 +08:00
yusing
dc1175ad69 refactor(http): remove and replace error helpers with standard http.Error 2025-09-26 23:39:00 +08:00
yusing
1409a4e8b9 chore(lint): update linting tool versions in trunk.yaml and enable fieldalignment check 2025-09-26 23:32:48 +08:00
yusing
8ec9752656 refactor(env): move env parsing to separate repo (cont. f7149453d6) 2025-09-26 21:41:57 +08:00
yusing
a932688ca3 chore(deps): upgrade gopsutils 2025-09-26 21:40:28 +08:00
yusing
55c1c918ba refactor: remove / throttle some debug logging 2025-09-26 21:00:35 +08:00
yusing
14e243d245 chore(deps): upgrade dependencies 2025-09-26 20:47:21 +08:00
yusing
f7149453d6 refactor(env): move env parsing to separate repo 2025-09-26 20:41:10 +08:00
yusing
00d137d05c refactor: remove obsolete args.go 2025-09-22 17:18:53 +08:00
yusing
f9affba9fc refactor(modules): replace github.com/yusing/go-proxy with github.com/yusing/godoxy 2025-09-22 16:44:59 +08:00
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
590 changed files with 37547 additions and 21079 deletions

View File

@@ -63,9 +63,6 @@ GODOXY_METRICS_DISABLE_DISK=false
GODOXY_METRICS_DISABLE_NETWORK=false
GODOXY_METRICS_DISABLE_SENSORS=false
# Frontend listening port
GODOXY_FRONTEND_PORT=3000
# Frontend aliases (subdomains / FQDNs, e.g. godoxy, godoxy.domain.com)
GODOXY_FRONTEND_ALIASES=godoxy

View File

@@ -25,6 +25,8 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
with:
submodules: 'recursive'
- uses: actions/setup-go@v5
with:
go-version-file: go.mod

View File

@@ -10,7 +10,6 @@ jobs:
uses: ./.github/workflows/docker-image.yml
with:
image_name: ${{ github.repository_owner }}/godoxy
old_image_name: ${{ github.repository_owner }}/go-proxy
tag: latest
target: main
build-prod-agent:

View File

@@ -6,13 +6,12 @@ on:
- main
paths:
- "socket-proxy/**"
- "socket-proxy.Dockerfile"
- ".github/workflows/docker-image-socket-proxy.yml"
tags-ignore:
- '**'
- "**"
workflow_dispatch:
permissions:
contents: read
jobs:
build:
uses: ./.github/workflows/docker-image.yml

View File

@@ -9,9 +9,6 @@ on:
image_name:
required: true
type: string
old_image_name:
required: false
type: string
target:
required: true
type: string
@@ -156,17 +153,6 @@ jobs:
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY }}/${{ inputs.image_name }}@sha256:%s ' *)
- name: Old image name
if: inputs.old_image_name != ''
run: |
docker buildx imagetools create -t ${{ env.REGISTRY }}/${{ inputs.old_image_name }}:${{ steps.meta.outputs.version }}\
${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.image_name }}:${{ steps.meta.outputs.version }}
- name: Inspect image (old)
if: inputs.old_image_name != ''
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ inputs.old_image_name }}:${{ steps.meta.outputs.version }}

View File

@@ -0,0 +1,39 @@
name: Cherry-pick into Compat
on:
push:
tags:
- v*
paths:
- ".github/workflows/merge-main-into-compat.yml"
jobs:
cherry-pick:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure git user
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Cherry-pick commits from last tag
run: |
git fetch origin compat
git checkout compat
CURRENT_TAG=${{ github.ref_name }}
PREV_TAG=$(git describe --tags --abbrev=0 $CURRENT_TAG^ 2>/dev/null || echo "")
if [ -z "$PREV_TAG" ]; then
echo "No previous tag found. Cherry-picking all commits up to $CURRENT_TAG"
git rev-list --reverse --no-merges $CURRENT_TAG | xargs -r git cherry-pick
else
echo "Cherry-picking commits from $PREV_TAG to $CURRENT_TAG"
git rev-list --reverse --no-merges $PREV_TAG..$CURRENT_TAG | xargs -r git cherry-pick
fi
- name: Push compat
run: |
git push origin compat

1
.gitignore vendored
View File

@@ -40,3 +40,4 @@ tsconfig.tsbuildinfo
!agent.compose.yml
!agent/pkg/**
dev-data/

9
.gitmodules vendored Normal file
View File

@@ -0,0 +1,9 @@
[submodule "internal/gopsutil"]
path = internal/gopsutil
url = https://github.com/godoxy-app/gopsutil.git
[submodule "internal/go-oidc"]
path = internal/go-oidc
url = https://github.com/godoxy-app/go-oidc.git
[submodule "goutils"]
path = goutils
url = https://github.com/yusing/goutils.git

View File

@@ -70,7 +70,6 @@ linters:
govet:
disable:
- shadow
- fieldalignment
enable-all: true
misspell:
locale: US
@@ -108,8 +107,7 @@ linters:
- all
- -SA1019
dot-import-whitelist:
- github.com/yusing/go-proxy/internal/utils/testing
- github.com/yusing/go-proxy/internal/api/v1/utils
- github.com/yusing/godoxy/internal/utils/testing
tagalign:
align: false
sort: true

View File

@@ -21,9 +21,9 @@ lint:
- markdownlint
- yamllint
enabled:
- checkov@3.2.467
- golangci-lint2@2.4.0
- hadolint@2.12.1-beta
- checkov@3.2.471
- golangci-lint2@2.5.0
- hadolint@2.14.0
- actionlint@1.7.7
- git-diff-check
- gofmt@1.20.4
@@ -32,7 +32,7 @@ lint:
- prettier@3.6.2
- shellcheck@0.11.0
- shfmt@3.6.0
- trufflehog@3.90.5
- trufflehog@3.90.8
actions:
disabled:
- trunk-announce

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

@@ -1,5 +1,5 @@
# Stage 1: deps
FROM golang:1.25.1-alpine AS deps
FROM golang:1.25.5-alpine AS deps
HEALTHCHECK NONE
# package version does not matter
@@ -7,13 +7,20 @@ HEALTHCHECK NONE
RUN apk add --no-cache tzdata make libcap-setcap
ENV GOPATH=/root/go
ENV GOCACHE=/root/.cache/go-build
WORKDIR /src
COPY goutils/go.mod goutils/go.sum ./goutils/
COPY internal/go-oidc/go.mod internal/go-oidc/go.sum ./internal/go-oidc/
COPY internal/gopsutil/go.mod internal/gopsutil/go.sum ./internal/gopsutil/
COPY go.mod go.sum ./
# remove godoxy stuff from go.mod first
RUN sed -i '/^module github\.com\/yusing\/go-proxy/!{/github\.com\/yusing\/go-proxy/d}' go.mod && \
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
sed -i '/^module github\.com\/yusing\/godoxy/!{/github\.com\/yusing\/godoxy/d}' go.mod && \
sed -i '/^module github\.com\/yusing\/goutils/!{/github\.com\/yusing\/goutils/d}' go.mod && \
go mod download -x
# Stage 2: builder
@@ -28,6 +35,7 @@ COPY internal ./internal
COPY pkg ./pkg
COPY agent ./agent
COPY socket-proxy ./socket-proxy
COPY goutils ./goutils
ARG VERSION
ENV VERSION=${VERSION}
@@ -35,9 +43,6 @@ ENV VERSION=${VERSION}
ARG MAKE_ARGS
ENV MAKE_ARGS=${MAKE_ARGS}
ENV GOCACHE=/root/.cache/go-build
ENV GOPATH=/root/go
RUN --mount=type=cache,target=/root/.cache/go-build \
--mount=type=cache,target=/root/go/pkg/mod \
make ${MAKE_ARGS} docker=1 build

11
Jenkinsfile vendored Normal file
View File

@@ -0,0 +1,11 @@
node {
stage('SCM') {
checkout scm
}
stage('SonarQube Analysis') {
def scannerHome = tool 'SonarScanner';
withSonarQubeEnv() {
sh "${scannerHome}/bin/sonar-scanner"
}
}
}

View File

@@ -3,10 +3,13 @@ export VERSION ?= $(shell git describe --tags --abbrev=0)
export BUILD_DATE ?= $(shell date -u +'%Y%m%d-%H%M')
export GOOS = linux
WEBUI_DIR ?= ../godoxy-frontend
DOCS_DIR ?= ../godoxy-wiki
REPO_URL ?= https://github.com/yusing/godoxy
LDFLAGS = -X github.com/yusing/go-proxy/pkg.version=${VERSION} -checklinkname=0
WEBUI_DIR ?= ../godoxy-webui
DOCS_DIR ?= ${WEBUI_DIR}/wiki
GO_TAGS = sonic
LDFLAGS = -X github.com/yusing/goutils/version.version=${VERSION} -checklinkname=0
ifeq ($(agent), 1)
NAME = godoxy-agent
@@ -26,26 +29,28 @@ ifeq ($(trace), 1)
endif
ifeq ($(race), 1)
debug = 1
BUILD_FLAGS += -race
endif
ifeq ($(debug), 1)
CGO_ENABLED = 1
GODOXY_DEBUG = 1
BUILD_FLAGS += -gcflags=all='-N -l' -tags debug -asan
else ifeq ($(pprof), 1)
GO_TAGS += debug
BUILD_FLAGS += -race
else ifeq ($(debug), 1)
CGO_ENABLED = 1
GODOXY_DEBUG = 1
GO_TAGS += debug
# FIXME: BUILD_FLAGS += -asan -gcflags=all='-N -l'
else ifeq ($(pprof), 1)
CGO_ENABLED = 0
GORACE = log_path=logs/pprof strip_path_prefix=$(shell pwd)/ halt_on_error=1
BUILD_FLAGS += -tags pprof
GO_TAGS += pprof
VERSION := ${VERSION}-pprof
else
CGO_ENABLED = 0
LDFLAGS += -s -w
BUILD_FLAGS += -pgo=auto -tags production
GO_TAGS += production
BUILD_FLAGS += -pgo=auto
endif
BUILD_FLAGS += -ldflags='$(LDFLAGS)'
BUILD_FLAGS += -tags '$(GO_TAGS)' -ldflags='$(LDFLAGS)'
BIN_PATH := $(shell pwd)/bin/${NAME}
export NAME
@@ -72,11 +77,12 @@ endif
.PHONY: debug
test:
GODOXY_TEST=1 go test ./internal/...
CGO_ENABLED=1 go test -v -race ${BUILD_FLAGS} ./internal/...
docker-build-test:
docker build -t godoxy .
docker build --build-arg=MAKE_ARGS=agent=1 -t godoxy-agent .
docker build --build-arg=MAKE_ARGS=socket-proxy=1 -t godoxy-socket-proxy .
go_ver := $(shell go version | cut -d' ' -f3 | cut -d'o' -f2)
files := $(shell find . -name go.mod -type f -or -name Dockerfile -type f)
@@ -107,20 +113,29 @@ mod-tidy:
build:
mkdir -p $(shell dirname ${BIN_PATH})
cd ${PWD} && go build ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
go build -C ${PWD} ${BUILD_FLAGS} -o ${BIN_PATH} ./cmd
${POST_BUILD}
run:
cd ${PWD} && [ -f .env ] && godotenv -f .env go run ${BUILD_FLAGS} ./cmd
dev:
docker compose -f dev.compose.yml up -t 0 -d
docker compose -f dev.compose.yml $(args)
dev-build: build
docker compose -f dev.compose.yml up -t 0 -d --build
docker compose -f dev.compose.yml up -t 0 -d app --force-recreate
dev-logs:
docker compose -f dev.compose.yml logs -f app
benchmark:
@if [ -z "$(TARGET)" ]; then \
docker compose -f dev.compose.yml up -d --force-recreate godoxy traefik caddy nginx; \
else \
docker compose -f dev.compose.yml up -d --force-recreate $(TARGET); \
fi
sleep 1
@./scripts/benchmark.sh
dev-run: build
cd dev-data && ${BIN_PATH}
mtrace:
${BIN_PATH} debug-ls-mtrace > mtrace.json
@@ -138,21 +153,28 @@ ci-test:
act -n --artifact-server-path /tmp/artifacts -s GITHUB_TOKEN="$$(gh auth token)"
cloc:
cloc --include-lang=Go --not-match-f '_test.go$$' .
scc -w -i go --not-match '_test.go$$'
push-github:
git push origin $(shell git rev-parse --abbrev-ref HEAD)
gen-swagger:
swag init --parseDependency --parseInternal -g handler.go -d internal/api -o internal/api/v1/docs
# go install github.com/swaggo/swag/cmd/swag@latest
swag init --parseDependency --parseInternal --parseFuncBody -g handler.go -d internal/api -o internal/api/v1/docs
python3 scripts/fix-swagger-json.py
# we don't need this
rm internal/api/v1/docs/docs.go
gen-swagger-markdown: gen-swagger
# brew tap go-swagger/go-swagger && brew install go-swagger
swagger generate markdown -f internal/api/v1/docs/swagger.yaml --skip-validation --output ${DOCS_DIR}/src/API.md
gen-api-types: gen-swagger
# --disable-throw-on-error
pnpx swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
bunx --bun swagger-typescript-api generate --sort-types --generate-union-enums --axios --add-readonly --route-types \
--responses -o ${WEBUI_DIR}/lib -n api.ts -p internal/api/v1/docs/swagger.json
bunx --bun prettier --config ${WEBUI_DIR}/.prettierrc --write ${WEBUI_DIR}/lib/api.ts
.PHONY: update-wiki
update-wiki:
DOCS_DIR=${DOCS_DIR} REPO_URL=${REPO_URL} bun --bun scripts/update-wiki/main.ts

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)
@@ -58,6 +59,7 @@ Have questions? Ask [ChatGPT](https://chatgpt.com/g/g-6825390374b481919ad482f2e4
- Country **(Maxmind account required)**
- Timezone **(Maxmind account required)**
- **Access logging**
- Periodic notification of access summaries for number of allowed and blocked connections
- **Advanced Automation**
- Automatic SSL certificate management with Let's Encrypt ([using DNS-01 Challenge](https://docs.godoxy.dev/DNS-01-Providers))
- Auto-configuration for Docker containers
@@ -128,6 +130,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)
- [截圖](#截圖)
- [閒置休眠](#閒置休眠)
- [監控](#監控)
@@ -57,6 +58,7 @@
- 國家 **(需要 Maxmind 帳戶)**
- 時區 **(需要 Maxmind 帳戶)**
- **存取日誌記錄**
- 定時發送摘要 (允許和拒絕的連線次數)
- **自動化**
- 使用 Let's Encrypt 自動管理 SSL 憑證 ([使用 DNS-01 驗證](https://docs.godoxy.dev/DNS-01-Providers))
- Docker 容器自動配置
@@ -144,6 +146,20 @@
└── .env
```
## 更新 / 卸載系統代理 (System Agent)
更新:
```bash
sudo /bin/bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- update
```
卸載:
```bash
sudo /bin/bash -c "$(curl -fsSL https://github.com/yusing/godoxy/raw/refs/heads/main/scripts/install-agent.sh)" -- uninstall
```
## 截圖
### 閒置休眠

52
agent/cmd/README.md Normal file
View File

@@ -0,0 +1,52 @@
# agent/cmd
The main entry point for the GoDoxy Agent, a secure monitoring and proxy agent that runs alongside Docker containers.
## Overview
This package contains the `main.go` entry point for the GoDoxy Agent. The agent is a TLS-enabled server that provides:
- Secure Docker socket proxying with client certificate authentication
- HTTP proxy capabilities for container traffic
- System metrics collection and monitoring
- Health check endpoints
## Architecture
```mermaid
graph TD
A[main] --> B[Logger Init]
A --> C[Load CA Certificate]
A --> D[Load Server Certificate]
A --> E[Log Version Info]
A --> F[Start Agent Server]
A --> G[Start Socket Proxy]
A --> H[Start System Info Poller]
A --> I[Wait Exit]
F --> F1[TLS with mTLS]
F --> F2[Agent Handler]
G --> G1[Docker Socket Proxy]
```
## Main Function Flow
1. **Logger Setup**: Configures zerolog with console output
1. **Certificate Loading**: Loads CA and server certificates for TLS/mTLS
1. **Version Logging**: Logs agent version and configuration
1. **Agent Server**: Starts the main HTTPS server with agent handlers
1. **Socket Proxy**: Starts Docker socket proxy if configured
1. **System Monitoring**: Starts system info polling
1. **Graceful Shutdown**: Waits for exit signal (3 second timeout)
## Configuration
See `agent/pkg/env/README.md` for configuration options.
## Dependencies
- `agent/pkg/agent` - Core agent types and constants
- `agent/pkg/env` - Environment configuration
- `agent/pkg/server` - Server implementation
- `socketproxy/pkg` - Docker socket proxy
- `internal/metrics/systeminfo` - System metrics

View File

@@ -1,22 +1,32 @@
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"net"
"net/http"
"os"
stdlog "log"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/agent/pkg/server"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
httpServer "github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils/strutils"
"github.com/yusing/go-proxy/pkg"
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/agent/stream"
"github.com/yusing/godoxy/agent/pkg/env"
"github.com/yusing/godoxy/agent/pkg/handler"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
"github.com/yusing/goutils/version"
)
// TODO: support IPv6
func main() {
writer := zerolog.ConsoleWriter{
Out: os.Stderr,
@@ -27,24 +37,24 @@ func main() {
ca := &agent.PEMPair{}
err := ca.Load(env.AgentCACert)
if err != nil {
gperr.LogFatal("init CA error", err)
log.Fatal().Err(err).Msg("init CA error")
}
caCert, err := ca.ToTLSCert()
if err != nil {
gperr.LogFatal("init CA error", err)
log.Fatal().Err(err).Msg("init CA error")
}
srv := &agent.PEMPair{}
srv.Load(env.AgentSSLCert)
if err != nil {
gperr.LogFatal("init SSL error", err)
log.Fatal().Err(err).Msg("init SSL error")
}
srvCert, err := srv.ToTLSCert()
if err != nil {
gperr.LogFatal("init SSL error", err)
log.Fatal().Err(err).Msg("init SSL error")
}
log.Info().Msgf("GoDoxy Agent version %s", pkg.GetVersion())
log.Info().Msgf("GoDoxy Agent version %s", version.Get())
log.Info().Msgf("Agent name: %s", env.AgentName)
log.Info().Msgf("Agent port: %d", env.AgentPort)
log.Info().Msgf("Agent runtime: %s", env.Runtime)
@@ -53,27 +63,102 @@ func main() {
Tips:
1. To change the agent name, you can set the AGENT_NAME environment variable.
2. To change the agent port, you can set the AGENT_PORT environment variable.
`)
`)
t := task.RootTask("agent", false)
opts := server.Options{
CACert: caCert,
ServerCert: srvCert,
Port: env.AgentPort,
// One TCP listener on AGENT_PORT, then multiplex by TLS ALPN:
// - Stream ALPN: route to TCP stream tunnel handler (via http.Server.TLSNextProto)
// - Otherwise: route to HTTPS API handler
tcpListener, err := net.ListenTCP("tcp", &net.TCPAddr{Port: env.AgentPort})
if err != nil {
gperr.LogFatal("failed to listen on port", err)
}
server.StartAgentServer(t, opts)
caCertPool := x509.NewCertPool()
caCertPool.AddCert(caCert.Leaf)
muxTLSConfig := &tls.Config{
Certificates: []tls.Certificate{*srvCert},
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS12,
// Keep HTTP limited to HTTP/1.1 (matching current agent server behavior)
// and add the stream tunnel ALPN for multiplexing.
NextProtos: []string{"http/1.1", stream.StreamALPN},
}
if env.AgentSkipClientCertCheck {
muxTLSConfig.ClientAuth = tls.NoClientCert
}
// TLS listener feeds the HTTP server. ALPN stream connections are intercepted
// using http.Server.TLSNextProto.
tlsLn := tls.NewListener(tcpListener, muxTLSConfig)
streamSrv := stream.NewTCPServerHandler(t.Context())
httpSrv := &http.Server{
Handler: handler.NewAgentHandler(),
BaseContext: func(net.Listener) context.Context {
return t.Context()
},
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
// When a client negotiates StreamALPN, net/http will call this hook instead
// of treating the connection as HTTP.
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
// ServeConn blocks until the tunnel finishes.
streamSrv.ServeConn(conn)
},
},
}
{
subtask := t.Subtask("agent-http", true)
t.OnCancel("stop_http", func() {
_ = streamSrv.Close()
_ = httpSrv.Close()
_ = tlsLn.Close()
})
go func() {
err := httpSrv.Serve(tlsLn)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Error().Err(err).Msg("agent HTTP server stopped with error")
}
subtask.Finish(err)
}()
log.Info().Int("port", env.AgentPort).Msg("HTTPS API server started (ALPN mux enabled)")
}
log.Info().Int("port", env.AgentPort).Msg("TCP stream handler started (via TLSNextProto)")
{
udpServer := stream.NewUDPServer(t.Context(), "udp", &net.UDPAddr{Port: env.AgentPort}, caCert.Leaf, srvCert)
subtask := t.Subtask("agent-stream-udp", true)
t.OnCancel("stop_stream_udp", func() {
_ = udpServer.Close()
})
go func() {
err := udpServer.Start()
subtask.Finish(err)
}()
log.Info().Int("port", env.AgentPort).Msg("UDP stream server started")
}
if socketproxy.ListenAddr != "" {
runtime := strutils.Title(string(env.Runtime))
log.Info().Msgf("%s socket listening on: %s", runtime, socketproxy.ListenAddr)
opts := httpServer.Options{
Name: runtime,
HTTPAddr: socketproxy.ListenAddr,
Handler: socketproxy.NewHandler(),
l, err := net.Listen("tcp", socketproxy.ListenAddr)
if err != nil {
gperr.LogFatal("failed to listen on port", err)
}
httpServer.StartServer(t, opts)
errLog := log.Logger.With().Str("level", "error").Str("component", "socketproxy").Logger()
srv := http.Server{
Handler: socketproxy.NewHandler(),
BaseContext: func(net.Listener) context.Context {
return t.Context()
},
ErrorLog: stdlog.New(&errLog, "", 0),
}
srv.Serve(l)
}
systeminfo.Poller.Start()

View File

@@ -1,113 +1,104 @@
module github.com/yusing/go-proxy/agent
module github.com/yusing/godoxy/agent
go 1.25.1
go 1.25.5
replace github.com/yusing/go-proxy => ..
replace github.com/yusing/go-proxy/socketproxy => ../socket-proxy
replace github.com/yusing/go-proxy/internal/utils => ../internal/utils
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d
replace (
github.com/shirou/gopsutil/v4 => ../internal/gopsutil
github.com/yusing/godoxy => ../
github.com/yusing/godoxy/socketproxy => ../socket-proxy
github.com/yusing/goutils => ../goutils
github.com/yusing/goutils/http/reverseproxy => ../goutils/http/reverseproxy
github.com/yusing/goutils/http/websocket => ../goutils/http/websocket
github.com/yusing/goutils/server => ../goutils/server
)
exclude github.com/containerd/nerdctl/mod/tigron v0.0.0
require (
github.com/gin-gonic/gin v1.10.1
github.com/bytedance/sonic v1.14.2
github.com/gin-gonic/gin v1.11.0
github.com/gorilla/websocket v1.5.3
github.com/puzpuzpuz/xsync/v4 v4.1.0
github.com/pion/dtls/v3 v3.0.10
github.com/pion/transport/v3 v3.1.1
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
github.com/yusing/go-proxy v0.17.6
github.com/yusing/go-proxy/internal/utils v0.0.0
github.com/yusing/go-proxy/socketproxy v0.0.0-00010101000000-000000000000
github.com/yusing/godoxy v0.23.1
github.com/yusing/godoxy/socketproxy v0.0.0-00010101000000-000000000000
github.com/yusing/goutils v0.7.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/PuerkitoBio/goquery v1.10.3 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/cli v28.4.0+incompatible // indirect
github.com/docker/docker v28.4.0+incompatible // indirect
github.com/docker/cli v29.1.3+incompatible // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/gotify/server/v2 v2.7.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/moby/api v1.52.0 // indirect
github.com/moby/moby/client v0.2.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
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/pkg/errors v0.9.1 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.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
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-zerolog/v2 v2.7.3 // indirect
github.com/shirou/gopsutil/v4 v4.25.8 // indirect
github.com/puzpuzpuz/xsync/v4 v4.2.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.58.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.12 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
github.com/vincent-petithory/dataurl v1.0.0 // indirect
github.com/yusing/ds v0.1.0 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect
github.com/yusing/ds v0.3.1 // indirect
github.com/yusing/gointernals v0.1.16 // indirect
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260109021609-78fda75d1e58 // indirect
github.com/yusing/goutils/http/websocket v0.0.0-20260109021609-78fda75d1e58 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.21.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.13.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/arch v0.23.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.32.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gotest.tools/v3 v3.5.2 // indirect
)

View File

@@ -1,52 +1,64 @@
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw=
github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY=
github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc=
github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=
github.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v28.4.0+incompatible h1:RBcf3Kjw2pMtwui5V0DIMdyeab8glEw5QY0UUU4C9kY=
github.com/docker/cli v28.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/docker v28.4.0+incompatible h1:KVC7bz5zJY/4AZe/78BIvCnPsLaC9T/zh72xnlrTTOk=
github.com/docker/docker v28.4.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=
github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=
github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c=
github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-acme/lego/v4 v4.31.0 h1:gd4oUYdfs83PR1/SflkNdit9xY1iul2I4EystnU8NXM=
github.com/go-acme/lego/v4 v4.31.0/go.mod h1:m6zcfX/zcbMYDa8s6AnCMnoORWNP8Epnei+6NBCTUGs=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -61,16 +73,17 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d h1:bNqtnmyhGDxpBSaFYIo7ferYRIc/QzlaGfIhh/JmMPk=
github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d/go.mod h1:7iQ/w4jyGYJCZ56dZLNztwM4atNxj5C2HNTBxhLvV8A=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -80,12 +93,14 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gotify/server/v2 v2.7.2 h1:YRgYl/kB7Uh4OINd7gK60N3QBTY4YEeKTrBLhd67LC4=
github.com/gotify/server/v2 v2.7.2/go.mod h1:KJH+8yhkAxArygPwaRfp9otHjt6tE0YSzdrsgPX/5EE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/gotify/server/v2 v2.8.0 h1:E3UDDn/3rFZi1sjZfbuhXNnxJP3ACZhdcw/iySegPRA=
github.com/gotify/server/v2 v2.8.0/go.mod h1:6ci5adxcE2hf1v+2oowKiQmixOxXV8vU+CRLKP6sqZA=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -96,8 +111,12 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/luthermonson/go-proxmox v0.3.2 h1:/zUg6FCl9cAABx0xU3OIgtDtClY0gVXxOCsrceDNylc=
github.com/luthermonson/go-proxmox v0.3.2/go.mod h1:oyFgg2WwTEIF0rP6ppjiixOHa5ebK1p8OaRiFhvICBQ=
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -105,21 +124,19 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
github.com/moby/moby/api v1.52.0 h1:00BtlJY4MXkkt84WhUZPRqt5TvPbgig2FZvTbe3igYg=
github.com/moby/moby/api v1.52.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc=
github.com/moby/moby/client v0.2.1 h1:1Grh1552mvv6i+sYOdY+xKKVTvzJegcVMhuXocyDz/k=
github.com/moby/moby/client v0.2.1/go.mod h1:O+/tw5d4a1Ha/ZA/tPxIZJapJRUS6LNZ1wiVRxYHyUE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -128,30 +145,39 @@ 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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/puzpuzpuz/xsync/v4 v4.1.0 h1:x9eHRl4QhZFIPJ17yl4KKW9xLyVWbb3/Yq4SXpjF71U=
github.com/puzpuzpuz/xsync/v4 v4.1.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
github.com/puzpuzpuz/xsync/v4 v4.2.0 h1:dlxm77dZj2c3rxq0/XNvvUKISAmovoXF4a4qM6Wvkr0=
github.com/puzpuzpuz/xsync/v4 v4.2.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.58.0 h1:ggY2pvZaVdB9EyojxL1p+5mptkuHyX5MOSv4dgWF4Ug=
github.com/quic-go/quic-go v0.58.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
github.com/samber/slog-zerolog/v2 v2.7.3 h1:/MkPDl/tJhijN2GvB1MWwBn2FU8RiL3rQ8gpXkQm2EY=
github.com/samber/slog-zerolog/v2 v2.7.3/go.mod h1:oWU7WHof4Xp8VguiNO02r1a4VzkgoOyOZhY5CuRke60=
github.com/samber/slog-zerolog/v2 v2.9.0 h1:6LkOabJmZdNLaUWkTC3IVVA+dq7b/V0FM6lz6/7+THI=
github.com/samber/slog-zerolog/v2 v2.9.0/go.mod h1:gnQW9VnCfM34v2pRMUIGMsZOVbYLqY/v0Wxu6atSVGc=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
@@ -159,148 +185,83 @@ github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusing/ds v0.1.0 h1:aiZs7jPMN3MEChUsddMYjpZFHhhAmkxrwRyIUnGy5AU=
github.com/yusing/ds v0.1.0/go.mod h1:KC785+mtt+Bau0LLR+slExDaUjeiqLT1k9Or6Rpryh4=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yusing/ds v0.3.1 h1:mCqTgTQD8RhiBpcysvii5kZ7ZBmqcknVsFubNALGLbY=
github.com/yusing/ds v0.3.1/go.mod h1:XhKV4l7cZwBbbl7lRzNC9zX27zvCM0frIwiuD40ULRk=
github.com/yusing/gointernals v0.1.16 h1:GrhZZdxzA+jojLEqankctJrOuAYDb7kY1C93S1pVR34=
github.com/yusing/gointernals v0.1.16/go.mod h1:B/0FVXt4WPmgzVy3ynzkqKi+BSGaJVmwCJBRXYapo34=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.opentelemetry.io/auto/sdk v1.2.0 h1:YpRtUFjvhSymycLS2T81lT6IGhcUP+LUPtv0iv1N8bM=
go.opentelemetry.io/auto/sdk v1.2.0/go.mod h1:1deq2zL7rwjwC8mR7XgY2N+tlIl6pjmEUoLDENMEzwk=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 h1:bDMKF3RUSxshZ5OjOTi8rsHGaPKsAt76FaqgvIUySLc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0/go.mod h1:dDT67G/IkA46Mr2l9Uj7HsQVwsjASyV9SjGofsiUZDA=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 h1:ywCL7vA2n3vVHyf+bx1ZV/knaTPRI8GIeKY0MEhEeOc=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 h1:APHvLLYBhtZvsbnpkfknDZ7NyH4z5+ub/I0u8L3Oz6g=
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1/go.mod h1:xUjFWUnWDpZ/C0Gu0qloASKFb6f8/QXiiXhSPFsD668=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -309,3 +270,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=

108
agent/pkg/agent/README.md Normal file
View File

@@ -0,0 +1,108 @@
# Agent Package
The `agent` package provides the client-side implementation for interacting with GoDoxy agents. It handles agent configuration, secure communication via TLS, and provides utilities for agent deployment and management.
## Architecture Overview
```mermaid
graph TD
subgraph GoDoxy Server
AP[Agent Pool] --> AC[AgentConfig]
end
subgraph Agent Communication
AC -->|HTTPS| AI[Agent Info API]
AC -->|TLS| ST[Stream Tunneling]
end
subgraph Deployment
G[Generator] --> DC[Docker Compose]
G --> IS[Install Script]
end
subgraph Security
NA[NewAgent] --> Certs[Certificates]
end
```
## File Structure
| File | Purpose |
| ---------------------------------------- | --------------------------------------------------------- |
| [`config.go`](config.go) | Core configuration, initialization, and API client logic. |
| [`new_agent.go`](new_agent.go) | Agent creation and certificate generation logic. |
| [`docker_compose.go`](docker_compose.go) | Generator for agent Docker Compose configurations. |
| [`bare_metal.go`](bare_metal.go) | Generator for bare metal installation scripts. |
| [`env.go`](env.go) | Environment configuration types and constants. |
| `common/` | Shared constants and utilities for agents. |
## Core Types
### [`AgentConfig`](config.go:29)
The primary struct used by the GoDoxy server to manage a connection to an agent. It stores the agent's address, metadata, and TLS configuration.
### [`AgentInfo`](config.go:45)
Contains basic metadata about the agent, including its version, name, and container runtime (Docker or Podman).
### [`PEMPair`](new_agent.go:53)
A utility struct for handling PEM-encoded certificate and key pairs, supporting encryption, decryption, and conversion to `tls.Certificate`.
## Agent Creation and Certificate Management
### Certificate Generation
The [`NewAgent`](new_agent.go:147) function creates a complete certificate infrastructure for an agent:
- **CA Certificate**: Self-signed root certificate with 1000-year validity.
- **Server Certificate**: For the agent's HTTPS server, signed by the CA.
- **Client Certificate**: For the GoDoxy server to authenticate with the agent.
All certificates use ECDSA with P-256 curve and SHA-256 signatures.
### Certificate Security
- Certificates are encrypted using AES-GCM with a provided encryption key.
- The [`PEMPair`](new_agent.go:53) struct provides methods for encryption, decryption, and conversion to `tls.Certificate`.
- Base64 encoding is used for certificate storage and transmission.
## Key Features
### 1. Secure Communication
All communication between the GoDoxy server and agents is secured using mutual TLS (mTLS). The [`AgentConfig`](config.go:29) handles the loading of CA and client certificates to establish secure connections.
### 2. Agent Discovery and Initialization
The [`Init`](config.go:231) and [`InitWithCerts`](config.go:110) methods allow the server to:
- Fetch agent metadata (version, name, runtime).
- Verify compatibility between server and agent versions.
- Test support for TCP and UDP stream tunneling.
### 3. Deployment Generators
The package provides interfaces and implementations for generating deployment artifacts:
- **Docker Compose**: Generates a `docker-compose.yml` for running the agent as a container via [`AgentComposeConfig.Generate()`](docker_compose.go:21).
- **Bare Metal**: Generates a shell script to install and run the agent as a systemd service via [`AgentEnvConfig.Generate()`](bare_metal.go:27).
### 4. Fake Docker Host
The package supports a "fake" Docker host scheme (`agent://<addr>`) to identify containers managed by an agent, allowing the GoDoxy server to route requests appropriately. See [`IsDockerHostAgent`](config.go:90) and [`GetAgentAddrFromDockerHost`](config.go:94).
## Usage Example
```go
cfg := &agent.AgentConfig{}
cfg.Parse("192.168.1.100:8081")
ctx := context.Background()
if err := cfg.Init(ctx); err != nil {
log.Fatal(err)
}
fmt.Printf("Connected to agent: %s (Version: %s)\n", cfg.Name, cfg.Version)
```

View File

@@ -1,67 +0,0 @@
package agent
import (
"iter"
"github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/go-proxy/internal/common"
)
var agentPool = xsync.NewMap[string, *AgentConfig](xsync.WithPresize(10))
func init() {
if common.IsTest {
agentPool.Store("test-agent", &AgentConfig{
Addr: "test-agent",
})
}
}
func GetAgent(agentAddrOrDockerHost string) (*AgentConfig, bool) {
if !IsDockerHostAgent(agentAddrOrDockerHost) {
return getAgentByAddr(agentAddrOrDockerHost)
}
return getAgentByAddr(GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
}
func GetAgentByName(name string) (*AgentConfig, bool) {
for _, agent := range agentPool.Range {
if agent.Name == name {
return agent, true
}
}
return nil, false
}
func AddAgent(agent *AgentConfig) {
agentPool.Store(agent.Addr, agent)
}
func RemoveAgent(agent *AgentConfig) {
agentPool.Delete(agent.Addr)
}
func RemoveAllAgents() {
agentPool.Clear()
}
func ListAgents() []*AgentConfig {
agents := make([]*AgentConfig, 0, agentPool.Size())
for _, agent := range agentPool.Range {
agents = append(agents, agent)
}
return agents
}
func IterAgents() iter.Seq2[string, *AgentConfig] {
return agentPool.Range
}
func NumAgents() int {
return agentPool.Size()
}
func getAgentByAddr(addr string) (agent *AgentConfig, ok bool) {
agent, ok = agentPool.Load(addr)
return
}

View File

@@ -0,0 +1,3 @@
package common
const CertsDNSName = "godoxy.agent"

View File

@@ -4,8 +4,11 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/url"
@@ -15,31 +18,51 @@ import (
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/agent/pkg/certs"
"github.com/yusing/go-proxy/pkg"
"github.com/yusing/godoxy/agent/pkg/agent/common"
agentstream "github.com/yusing/godoxy/agent/pkg/agent/stream"
"github.com/yusing/godoxy/agent/pkg/certs"
gperr "github.com/yusing/goutils/errs"
httputils "github.com/yusing/goutils/http"
"github.com/yusing/goutils/version"
)
type AgentConfig struct {
Addr string `json:"addr"`
Name string `json:"name"`
Version string `json:"version"`
Runtime ContainerRuntime `json:"runtime"`
AgentInfo
httpClient *http.Client
tlsConfig *tls.Config
l zerolog.Logger
Addr string `json:"addr"`
IsTCPStreamSupported bool `json:"supports_tcp_stream"`
IsUDPStreamSupported bool `json:"supports_udp_stream"`
// for stream
caCert *x509.Certificate
clientCert *tls.Certificate
tlsConfig tls.Config
l zerolog.Logger
} // @name Agent
type AgentInfo struct {
Version version.Version `json:"version" swaggertype:"string"`
Name string `json:"name"`
Runtime ContainerRuntime `json:"runtime"`
}
// Deprecated. Replaced by EndpointInfo
const (
EndpointVersion = "/version"
EndpointName = "/name"
EndpointRuntime = "/runtime"
EndpointVersion = "/version"
EndpointName = "/name"
EndpointRuntime = "/runtime"
)
const (
EndpointInfo = "/info"
EndpointProxyHTTP = "/proxy/http"
EndpointHealth = "/health"
EndpointLogs = "/logs"
EndpointSystemInfo = "/system_info"
AgentHost = CertsDNSName
AgentHost = common.CertsDNSName
APIEndpointBase = "/godoxy/agent"
APIBaseURL = "https://" + AgentHost + APIEndpointBase
@@ -81,13 +104,15 @@ func (cfg *AgentConfig) Parse(addr string) error {
return nil
}
var serverVersion = pkg.GetVersion()
var serverVersion = version.Get()
func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte) error {
// InitWithCerts initializes the agent config with the given CA, certificate, and key.
func (cfg *AgentConfig) InitWithCerts(ctx context.Context, ca, crt, key []byte) error {
clientCert, err := tls.X509KeyPair(crt, key)
if err != nil {
return err
}
cfg.clientCert = &clientCert
// create tls config
caCertPool := x509.NewCertPool()
@@ -95,71 +120,115 @@ func (cfg *AgentConfig) StartWithCerts(ctx context.Context, ca, crt, key []byte)
if !ok {
return errors.New("invalid ca certificate")
}
cfg.tlsConfig = &tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
ServerName: CertsDNSName,
// Keep the CA leaf for stream client dialing.
if block, _ := pem.Decode(ca); block == nil || block.Type != "CERTIFICATE" {
return errors.New("invalid ca certificate")
} else if cert, err := x509.ParseCertificate(block.Bytes); err != nil {
return err
} else {
cfg.caCert = cert
}
// create transport and http client
cfg.httpClient = cfg.NewHTTPClient()
cfg.tlsConfig = tls.Config{
Certificates: []tls.Certificate{clientCert},
RootCAs: caCertPool,
ServerName: common.CertsDNSName,
MinVersion: tls.VersionTLS12,
}
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// get agent name
name, _, err := cfg.Fetch(ctx, EndpointName)
status, err := cfg.fetchJSON(ctx, EndpointInfo, &cfg.AgentInfo)
if err != nil {
return err
}
cfg.Name = string(name)
var streamUnsupportedErrs gperr.Builder
if status == http.StatusOK {
// test stream server connection
const fakeAddress = "localhost:8080" // it won't be used, just for testing
// test TCP stream support
err := agentstream.TCPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
if err != nil {
streamUnsupportedErrs.Addf("failed to connect to stream server via TCP: %w", err)
} else {
cfg.IsTCPStreamSupported = true
}
// test UDP stream support
err = agentstream.UDPHealthCheck(cfg.Addr, cfg.caCert, cfg.clientCert)
if err != nil {
streamUnsupportedErrs.Addf("failed to connect to stream server via UDP: %w", err)
} else {
cfg.IsUDPStreamSupported = true
}
} else {
// old agent does not support EndpointInfo
// fallback with old logic
cfg.IsTCPStreamSupported = false
cfg.IsUDPStreamSupported = false
streamUnsupportedErrs.Adds("agent version is too old, does not support stream tunneling")
// get agent name
name, _, err := cfg.fetchString(ctx, EndpointName)
if err != nil {
return err
}
cfg.Name = name
// check agent version
agentVersion, _, err := cfg.fetchString(ctx, EndpointVersion)
if err != nil {
return err
}
cfg.Version = version.Parse(agentVersion)
// check agent runtime
runtime, status, err := cfg.fetchString(ctx, EndpointRuntime)
if err != nil {
return err
}
switch status {
case http.StatusOK:
switch runtime {
case "docker":
cfg.Runtime = ContainerRuntimeDocker
// case "nerdctl":
// cfg.Runtime = ContainerRuntimeNerdctl
case "podman":
cfg.Runtime = ContainerRuntimePodman
default:
return fmt.Errorf("invalid agent runtime: %s", runtime)
}
case http.StatusNotFound:
// backward compatibility, old agent does not have runtime endpoint
cfg.Runtime = ContainerRuntimeDocker
default:
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtime)
}
}
cfg.l = log.With().Str("agent", cfg.Name).Logger()
// check agent version
agentVersionBytes, _, err := cfg.Fetch(ctx, EndpointVersion)
if err != nil {
return err
if err := streamUnsupportedErrs.Error(); err != nil {
gperr.LogWarn("agent has limited/no stream tunneling support, TCP and UDP routes via agent will not work", err, &cfg.l)
}
// check agent runtime
runtimeBytes, status, err := cfg.Fetch(ctx, EndpointRuntime)
if err != nil {
return err
}
switch status {
case http.StatusOK:
switch string(runtimeBytes) {
case "docker":
cfg.Runtime = ContainerRuntimeDocker
// case "nerdctl":
// cfg.Runtime = ContainerRuntimeNerdctl
case "podman":
cfg.Runtime = ContainerRuntimePodman
default:
return fmt.Errorf("invalid agent runtime: %s", runtimeBytes)
}
case http.StatusNotFound:
// backward compatibility, old agent does not have runtime endpoint
cfg.Runtime = ContainerRuntimeDocker
default:
return fmt.Errorf("failed to get agent runtime: HTTP %d %s", status, runtimeBytes)
}
cfg.Version = string(agentVersionBytes)
agentVersion := pkg.ParseVersion(cfg.Version)
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)
return nil
}
func (cfg *AgentConfig) Start(ctx context.Context) error {
// Init initializes the agent config with the given context.
func (cfg *AgentConfig) Init(ctx context.Context) error {
filepath, ok := certs.AgentCertsFilepath(cfg.Addr)
if !ok {
return fmt.Errorf("invalid agent host: %s", cfg.Addr)
@@ -175,13 +244,39 @@ func (cfg *AgentConfig) Start(ctx context.Context) error {
return fmt.Errorf("failed to extract agent certs: %w", err)
}
return cfg.StartWithCerts(ctx, ca, crt, key)
return cfg.InitWithCerts(ctx, ca, crt, key)
}
func (cfg *AgentConfig) NewHTTPClient() *http.Client {
return &http.Client{
Transport: cfg.Transport(),
// NewTCPClient creates a new TCP client for the agent.
//
// It returns an error if
// - the agent is not initialized
// - the agent does not support TCP stream tunneling
// - the agent stream server address is not initialized
func (cfg *AgentConfig) NewTCPClient(targetAddress string) (net.Conn, error) {
if cfg.caCert == nil || cfg.clientCert == nil {
return nil, errors.New("agent is not initialized")
}
if !cfg.IsTCPStreamSupported {
return nil, errors.New("agent does not support TCP stream tunneling")
}
return agentstream.NewTCPClient(cfg.Addr, targetAddress, cfg.caCert, cfg.clientCert)
}
// NewUDPClient creates a new UDP client for the agent.
//
// It returns an error if
// - the agent is not initialized
// - the agent does not support UDP stream tunneling
// - the agent stream server address is not initialized
func (cfg *AgentConfig) NewUDPClient(targetAddress string) (net.Conn, error) {
if cfg.caCert == nil || cfg.clientCert == nil {
return nil, errors.New("agent is not initialized")
}
if !cfg.IsUDPStreamSupported {
return nil, errors.New("agent does not support UDP stream tunneling")
}
return agentstream.NewUDPClient(cfg.Addr, targetAddress, cfg.caCert, cfg.clientCert)
}
func (cfg *AgentConfig) Transport() *http.Transport {
@@ -195,10 +290,14 @@ func (cfg *AgentConfig) Transport() *http.Transport {
}
return cfg.DialContext(ctx)
},
TLSClientConfig: cfg.tlsConfig,
TLSClientConfig: &cfg.tlsConfig,
}
}
func (cfg *AgentConfig) TLSConfig() *tls.Config {
return &cfg.tlsConfig
}
var dialer = &net.Dialer{Timeout: 5 * time.Second}
func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
@@ -208,3 +307,58 @@ func (cfg *AgentConfig) DialContext(ctx context.Context) (net.Conn, error) {
func (cfg *AgentConfig) String() string {
return cfg.Name + "@" + cfg.Addr
}
func (cfg *AgentConfig) do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
if err != nil {
return nil, err
}
client := http.Client{
Transport: cfg.Transport(),
}
return client.Do(req)
}
func (cfg *AgentConfig) fetchString(ctx context.Context, endpoint string) (string, int, error) {
resp, err := cfg.do(ctx, "GET", endpoint, nil)
if err != nil {
return "", 0, err
}
defer resp.Body.Close()
data, release, err := httputils.ReadAllBody(resp)
if err != nil {
return "", 0, err
}
ret := string(data)
release(data)
return ret, resp.StatusCode, nil
}
// fetchJSON fetches a JSON response from the agent and unmarshals it into the provided struct
//
// It will return the status code of the response, and error if any.
// If the status code is not http.StatusOK, out will be unchanged but error will still be nil.
func (cfg *AgentConfig) fetchJSON(ctx context.Context, endpoint string, out any) (int, error) {
resp, err := cfg.do(ctx, "GET", endpoint, nil)
if err != nil {
return 0, err
}
defer resp.Body.Close()
data, release, err := httputils.ReadAllBody(resp)
if err != nil {
return 0, err
}
defer release(data)
if resp.StatusCode != http.StatusOK {
return resp.StatusCode, nil
}
err = json.Unmarshal(data, out)
if err != nil {
return 0, err
}
return resp.StatusCode, nil
}

View File

@@ -1,72 +0,0 @@
package agent
import (
"context"
"io"
"net/http"
"github.com/gorilla/websocket"
"github.com/yusing/go-proxy/internal/net/gphttp/reverseproxy"
nettypes "github.com/yusing/go-proxy/internal/net/types"
)
func (cfg *AgentConfig) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, APIBaseURL+endpoint, body)
if err != nil {
return nil, err
}
return cfg.httpClient.Do(req)
}
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
req.RequestURI = ""
resp, err := cfg.httpClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
func (cfg *AgentConfig) Fetch(ctx context.Context, endpoint string) ([]byte, int, error) {
resp, err := cfg.Do(ctx, "GET", endpoint, nil)
if err != nil {
return nil, 0, err
}
defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body)
return data, resp.StatusCode, nil
}
func (cfg *AgentConfig) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
transport := cfg.Transport()
dialer := websocket.Dialer{
NetDialContext: transport.DialContext,
NetDialTLSContext: transport.DialTLSContext,
}
return dialer.DialContext(ctx, APIBaseURL+endpoint, http.Header{
"Host": {AgentHost},
})
}
// ReverseProxy reverse proxies the request to the agent
//
// 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 {
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
}

View File

@@ -3,6 +3,8 @@ package agent
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
@@ -10,18 +12,13 @@ import (
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"math/big"
"strings"
"time"
"crypto/ecdsa"
"crypto/elliptic"
"fmt"
)
const (
CertsDNSName = "godoxy.agent"
"github.com/yusing/godoxy/agent/pkg/agent/common"
)
func toPEMPair(certDER []byte, key *ecdsa.PrivateKey) *PEMPair {
@@ -157,7 +154,7 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
SerialNumber: caSerialNumber,
Subject: pkix.Name{
Organization: []string{"GoDoxy"},
CommonName: CertsDNSName,
CommonName: common.CertsDNSName,
},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1000, 0, 0), // 1000 years
@@ -197,9 +194,9 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
Subject: pkix.Name{
Organization: caTemplate.Subject.Organization,
OrganizationalUnit: []string{"Server"},
CommonName: CertsDNSName,
CommonName: common.CertsDNSName,
},
DNSNames: []string{CertsDNSName},
DNSNames: []string{common.CertsDNSName},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1000, 0, 0), // Add validity period
KeyUsage: x509.KeyUsageDigitalSignature,
@@ -229,9 +226,9 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
Subject: pkix.Name{
Organization: caTemplate.Subject.Organization,
OrganizationalUnit: []string{"Client"},
CommonName: CertsDNSName,
CommonName: common.CertsDNSName,
},
DNSNames: []string{CertsDNSName},
DNSNames: []string{common.CertsDNSName},
NotBefore: time.Now(),
NotAfter: time.Now().AddDate(1000, 0, 0),
KeyUsage: x509.KeyUsageDigitalSignature,
@@ -244,5 +241,5 @@ func NewAgent() (ca, srv, client *PEMPair, err error) {
}
client = toPEMPair(clientCertDER, clientKey)
return
return ca, srv, client, err
}

View File

@@ -10,6 +10,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/agent/pkg/agent/common"
)
func TestNewAgent(t *testing.T) {
@@ -72,7 +73,7 @@ func TestServerClient(t *testing.T) {
clientTLSConfig := &tls.Config{
Certificates: []tls.Certificate{*clientTLS},
RootCAs: caPool,
ServerName: CertsDNSName,
ServerName: common.CertsDNSName,
}
server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

@@ -0,0 +1,197 @@
# Stream proxy protocol
This package implements a small header-based handshake that allows an authenticated client to request forwarding to a `(host, port)` destination. It supports both TCP-over-TLS and UDP-over-DTLS transports.
## Overview
```mermaid
graph TD
subgraph Client
TC[TCPClient] -->|TLS| TSS[TCPServer]
UC[UDPClient] -->|DTLS| USS[UDPServer]
end
subgraph Stream Protocol
H[StreamRequestHeader]
end
TSS -->|Redirect| DST1[Destination TCP]
USS -->|Forward UDP| DST2[Destination UDP]
```
## Header
The on-wire header is a fixed-size binary blob:
- `Version` (8 bytes)
- `HostLength` (1 byte)
- `Host` (255 bytes, NUL padded)
- `PortLength` (1 byte)
- `Port` (5 bytes, NUL padded)
- `Flag` (1 byte, protocol flags)
- `Checksum` (4 bytes, big-endian CRC32)
Total: `headerSize = 8 + 1 + 255 + 1 + 5 + 1 + 4 = 275` bytes.
Checksum is `crc32.ChecksumIEEE(header[0:headerSize-4])`.
### Flags
The `Flag` field is a bitmask of protocol flags defined by `FlagType`:
| Flag | Value | Purpose |
| ---------------------- | ----- | ---------------------------------------------------------------------- |
| `FlagCloseImmediately` | `1` | Health check probe - server closes immediately after validating header |
See [`FlagType`](header.go:26) and [`FlagCloseImmediately`](header.go:28).
See [`StreamRequestHeader`](header.go:30).
## File Structure
| File | Purpose |
| ----------------------------------- | ------------------------------------------------------------ |
| [`header.go`](header.go) | Stream request header structure and validation. |
| [`tcp_client.go`](tcp_client.go:12) | TCP client implementation with TLS transport. |
| [`tcp_server.go`](tcp_server.go:13) | TCP server implementation for handling stream requests. |
| [`udp_client.go`](udp_client.go:13) | UDP client implementation with DTLS transport. |
| [`udp_server.go`](udp_server.go:17) | UDP server implementation for handling DTLS stream requests. |
| [`common.go`](common.go:11) | Connection manager and shared constants. |
## Constants
| Constant | Value | Purpose |
| ---------------------- | ------------------------- | ------------------------------------------------------- |
| `StreamALPN` | `"godoxy-agent-stream/1"` | TLS ALPN protocol for stream multiplexing. |
| `headerSize` | `275` bytes | Total size of the stream request header. |
| `dialTimeout` | `10s` | Timeout for establishing destination connections. |
| `readDeadline` | `10s` | Read timeout for UDP destination sockets. |
| `FlagCloseImmediately` | `1` | Flag for health check probe - server closes immediately |
See [`common.go`](common.go:11).
## Public API
### Types
#### `StreamRequestHeader`
Represents the on-wire protocol header used to negotiate a stream tunnel.
```go
type StreamRequestHeader struct {
Version [8]byte // Fixed to "0.1.0" with NUL padding
HostLength byte // Actual host name length (0-255)
Host [255]byte // NUL-padded host name
PortLength byte // Actual port string length (0-5)
Port [5]byte // NUL-padded port string
Flag FlagType // Protocol flags (e.g., FlagCloseImmediately)
Checksum [4]byte // CRC32 checksum of header without checksum
}
```
**Methods:**
- `NewStreamRequestHeader(host, port string) (*StreamRequestHeader, error)` - Creates a header for the given host and port. Returns error if host exceeds 255 bytes or port exceeds 5 bytes.
- `NewStreamHealthCheckHeader() *StreamRequestHeader` - Creates a header with `FlagCloseImmediately` set for health check probes.
- `Validate() bool` - Validates the version and checksum.
- `GetHostPort() (string, string)` - Extracts the host and port from the header.
- `ShouldCloseImmediately() bool` - Returns true if `FlagCloseImmediately` is set.
### TCP Functions
- [`NewTCPClient()`](tcp_client.go:26) - Creates a TLS client connection and sends the stream header.
- [`NewTCPServerHandler()`](tcp_server.go:24) - Creates a handler for ALPN-multiplexed connections (no listener).
- [`NewTCPServerFromListener()`](tcp_server.go:36) - Wraps an existing TLS listener.
- [`NewTCPServer()`](tcp_server.go:45) - Creates a fully-configured TCP server with TLS listener.
### UDP Functions
- [`NewUDPClient()`](udp_client.go:27) - Creates a DTLS client connection and sends the stream header.
- [`NewUDPServer()`](udp_server.go:26) - Creates a DTLS server listening on the given UDP address.
## Health Check Probes
The protocol supports health check probes using the `FlagCloseImmediately` flag. When a client sends a header with this flag set, the server validates the header and immediately closes the connection without establishing a destination tunnel.
This is useful for:
- Connectivity testing between agent and server
- Verifying TLS/DTLS handshake and mTLS authentication
- Monitoring stream protocol availability
**Usage:**
```go
header := stream.NewStreamHealthCheckHeader()
// Send header over TLS/DTLS connection
// Server will validate and close immediately
```
Both TCP and UDP servers silently handle health check probes without logging errors.
See [`NewStreamHealthCheckHeader()`](header.go:66) and [`FlagCloseImmediately`](header.go:28).
## TCP behavior
1. Client establishes a TLS connection to the stream server.
2. Client sends exactly one header as a handshake.
3. After the handshake, both sides proxy raw TCP bytes between client and destination.
Server reads the header using `io.ReadFull` to avoid dropping bytes.
See [`NewTCPClient()`](tcp_client.go:26) and [`(*TCPServer).redirect()`](tcp_server.go:116).
## UDP-over-DTLS behavior
1. Client establishes a DTLS connection to the stream server.
2. Client sends exactly one header as a handshake.
3. After the handshake, both sides proxy raw UDP datagrams:
- client -> destination: DTLS payload is written to destination `UDPConn`
- destination -> client: destination payload is written back to the DTLS connection
Responses do **not** include a header.
The UDP server uses a bidirectional forwarding model:
- One goroutine forwards from client to destination
- Another goroutine forwards from destination to client
The destination reader uses `readDeadline` to periodically wake up and check for context cancellation. Timeouts do not terminate the session.
See [`NewUDPClient()`](udp_client.go:27) and [`(*UDPServer).handleDTLSConnection()`](udp_server.go:89).
## Connection Management
Both `TCPServer` and `UDPServer` create a dedicated destination connection per incoming stream session and close it when the session ends (no destination connection reuse).
## Error Handling
| Error | Description |
| --------------------- | ----------------------------------------------- |
| `ErrInvalidHeader` | Header validation failed (version or checksum). |
| `ErrCloseImmediately` | Health check probe - server closed immediately. |
Errors from connection creation are propagated to the caller.
See [`header.go`](header.go:23).
## Integration
This package is used by the agent to provide stream tunneling capabilities. See the parent [`agent`](../README.md) package for integration details with the GoDoxy server.
### Certificate Requirements
Both TCP and UDP servers require:
- CA certificate for client verification
- Server certificate for TLS/DTLS termination
Both clients require:
- CA certificate for server verification
- Client certificate for mTLS authentication
### ALPN Protocol
The `StreamALPN` constant (`"godoxy-agent-stream/1"`) is used to multiplex stream tunnel traffic and HTTPS API traffic on the same port. Connections negotiating this ALPN are routed to the stream handler.

View File

@@ -0,0 +1,24 @@
package stream
import (
"time"
"github.com/pion/dtls/v3"
"github.com/yusing/goutils/synk"
)
const (
dialTimeout = 10 * time.Second
readDeadline = 10 * time.Second
)
// StreamALPN is the TLS ALPN protocol id used to multiplex the TCP stream tunnel
// and the HTTPS API on the same TCP port.
//
// When a client negotiates this ALPN, the agent will route the connection to the
// stream tunnel handler instead of the HTTP handler.
const StreamALPN = "godoxy-agent-stream/1"
var dTLSCipherSuites = []dtls.CipherSuiteID{dtls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256}
var sizedPool = synk.GetSizedBytesPool()

View File

@@ -0,0 +1,117 @@
package stream
import (
"encoding/binary"
"errors"
"fmt"
"hash/crc32"
"reflect"
"unsafe"
)
const (
versionSize = 8
hostSize = 255
portSize = 5
flagSize = 1
checksumSize = 4 // crc32 checksum
headerSize = versionSize + 1 + hostSize + 1 + portSize + flagSize + checksumSize
)
var version = [versionSize]byte{'0', '.', '1', '.', '0', 0, 0, 0}
var ErrInvalidHeader = errors.New("invalid header")
var ErrCloseImmediately = errors.New("close immediately")
type FlagType uint8
const FlagCloseImmediately FlagType = 1 << iota
type StreamRequestHeader struct {
Version [versionSize]byte
HostLength byte
Host [hostSize]byte
PortLength byte
Port [portSize]byte
Flag FlagType
Checksum [checksumSize]byte
}
func init() {
if headerSize != reflect.TypeFor[StreamRequestHeader]().Size() {
panic("headerSize does not match the size of StreamRequestHeader")
}
}
func NewStreamRequestHeader(host, port string) (*StreamRequestHeader, error) {
if len(host) > hostSize {
return nil, fmt.Errorf("host is too long: max %d characters, got %d", hostSize, len(host))
}
if len(port) > portSize {
return nil, fmt.Errorf("port is too long: max %d characters, got %d", portSize, len(port))
}
header := &StreamRequestHeader{}
copy(header.Version[:], version[:])
header.HostLength = byte(len(host))
copy(header.Host[:], host)
header.PortLength = byte(len(port))
copy(header.Port[:], port)
header.updateChecksum()
return header, nil
}
func NewStreamHealthCheckHeader() *StreamRequestHeader {
header := &StreamRequestHeader{}
copy(header.Version[:], version[:])
header.Flag |= FlagCloseImmediately
header.updateChecksum()
return header
}
// ToHeader converts header byte array to a copy of itself as a StreamRequestHeader.
func ToHeader(buf *[headerSize]byte) StreamRequestHeader {
return *(*StreamRequestHeader)(unsafe.Pointer(buf))
}
func (h *StreamRequestHeader) GetHostPort() (string, string) {
return string(h.Host[:h.HostLength]), string(h.Port[:h.PortLength])
}
func (h *StreamRequestHeader) Validate() bool {
if h.Version != version {
return false
}
if h.HostLength > hostSize {
return false
}
if h.PortLength > portSize {
return false
}
return h.validateChecksum()
}
func (h *StreamRequestHeader) ShouldCloseImmediately() bool {
return h.Flag&FlagCloseImmediately != 0
}
func (h *StreamRequestHeader) updateChecksum() {
checksum := crc32.ChecksumIEEE(h.BytesWithoutChecksum())
binary.BigEndian.PutUint32(h.Checksum[:], checksum)
}
func (h *StreamRequestHeader) validateChecksum() bool {
checksum := crc32.ChecksumIEEE(h.BytesWithoutChecksum())
return checksum == binary.BigEndian.Uint32(h.Checksum[:])
}
func (h *StreamRequestHeader) BytesWithoutChecksum() []byte {
return (*[headerSize - checksumSize]byte)(unsafe.Pointer(h))[:]
}
func (h *StreamRequestHeader) Bytes() []byte {
return (*[headerSize]byte)(unsafe.Pointer(h))[:]
}

View File

@@ -0,0 +1,26 @@
package stream
import (
"testing"
)
func TestStreamRequestHeader_RoundTripAndChecksum(t *testing.T) {
h, err := NewStreamRequestHeader("example.com", "443")
if err != nil {
t.Fatalf("NewStreamRequestHeader: %v", err)
}
if !h.Validate() {
t.Fatalf("expected header to validate")
}
var buf [headerSize]byte
copy(buf[:], h.Bytes())
h2 := ToHeader(&buf)
if !h2.Validate() {
t.Fatalf("expected round-tripped header to validate")
}
host, port := h2.GetHostPort()
if host != "example.com" || port != "443" {
t.Fatalf("unexpected host/port: %q:%q", host, port)
}
}

View File

@@ -0,0 +1,122 @@
package stream
import (
"crypto/tls"
"crypto/x509"
"net"
"time"
"github.com/yusing/godoxy/agent/pkg/agent/common"
)
type TCPClient struct {
conn net.Conn
}
// NewTCPClient creates a new TCP client for the agent.
//
// It will establish a TLS connection and send a stream request header to the server.
//
// It returns an error if
// - the target address is invalid
// - the stream request header is invalid
// - the TLS configuration is invalid
// - the TLS connection fails
// - the stream request header is not sent
func NewTCPClient(serverAddr, targetAddress string, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
host, port, err := net.SplitHostPort(targetAddress)
if err != nil {
return nil, err
}
header, err := NewStreamRequestHeader(host, port)
if err != nil {
return nil, err
}
return newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
}
func TCPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
header := NewStreamHealthCheckHeader()
conn, err := newTCPClientWIthHeader(serverAddr, header, caCert, clientCert)
if err != nil {
return err
}
conn.Close()
return nil
}
func newTCPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
// Setup TLS configuration
caCertPool := x509.NewCertPool()
caCertPool.AddCert(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{*clientCert},
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
NextProtos: []string{StreamALPN},
ServerName: common.CertsDNSName,
}
// Establish TLS connection
conn, err := tls.DialWithDialer(&net.Dialer{Timeout: dialTimeout}, "tcp", serverAddr, tlsConfig)
if err != nil {
return nil, err
}
// Send the stream header once as a handshake.
if _, err := conn.Write(header.Bytes()); err != nil {
_ = conn.Close()
return nil, err
}
return &TCPClient{
conn: conn,
}, nil
}
func (c *TCPClient) Read(p []byte) (n int, err error) {
return c.conn.Read(p)
}
func (c *TCPClient) Write(p []byte) (n int, err error) {
return c.conn.Write(p)
}
func (c *TCPClient) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *TCPClient) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func (c *TCPClient) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *TCPClient) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t)
}
func (c *TCPClient) SetWriteDeadline(t time.Time) error {
return c.conn.SetWriteDeadline(t)
}
func (c *TCPClient) Close() error {
return c.conn.Close()
}
// ConnectionState exposes the underlying TLS connection state when the client is
// backed by *tls.Conn.
//
// This is primarily used by tests and diagnostics.
func (c *TCPClient) ConnectionState() tls.ConnectionState {
if tc, ok := c.conn.(*tls.Conn); ok {
return tc.ConnectionState()
}
return tls.ConnectionState{}
}

View File

@@ -0,0 +1,179 @@
package stream
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"io"
"net"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
ioutils "github.com/yusing/goutils/io"
)
type TCPServer struct {
ctx context.Context
listener net.Listener
}
// NewTCPServerHandler creates a TCP stream server that can serve already-accepted
// connections (e.g. handed off by an ALPN multiplexer).
//
// This variant does not require a listener. Use TCPServer.ServeConn to handle
// each incoming stream connection.
func NewTCPServerHandler(ctx context.Context) *TCPServer {
s := &TCPServer{ctx: ctx}
return s
}
// NewTCPServerFromListener creates a TCP stream server from an already-prepared
// listener.
//
// The listener is expected to yield connections that are already secured (e.g.
// a TLS/mTLS listener, or pre-handshaked *tls.Conn). This is used when the agent
// multiplexes HTTPS and stream-tunnel traffic on the same port.
func NewTCPServerFromListener(ctx context.Context, listener net.Listener) *TCPServer {
s := &TCPServer{
ctx: ctx,
listener: listener,
}
return s
}
func NewTCPServer(ctx context.Context, listener *net.TCPListener, caCert *x509.Certificate, serverCert *tls.Certificate) *TCPServer {
caCertPool := x509.NewCertPool()
caCertPool.AddCert(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{*serverCert},
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS12,
NextProtos: []string{StreamALPN},
}
tcpListener := tls.NewListener(listener, tlsConfig)
return NewTCPServerFromListener(ctx, tcpListener)
}
func (s *TCPServer) Start() error {
if s.listener == nil {
return net.ErrClosed
}
context.AfterFunc(s.ctx, func() {
_ = s.listener.Close()
})
for {
conn, err := s.listener.Accept()
if err != nil {
if errors.Is(err, net.ErrClosed) && s.ctx.Err() != nil {
return s.ctx.Err()
}
return err
}
go s.handle(conn)
}
}
// ServeConn serves a single stream connection.
//
// The provided connection is expected to be already secured (TLS/mTLS) and to
// speak the stream protocol (i.e. the client will send the stream header first).
//
// This method blocks until the stream finishes.
func (s *TCPServer) ServeConn(conn net.Conn) {
s.handle(conn)
}
func (s *TCPServer) Addr() net.Addr {
if s.listener == nil {
return nil
}
return s.listener.Addr()
}
func (s *TCPServer) Close() error {
if s.listener == nil {
return nil
}
return s.listener.Close()
}
func (s *TCPServer) logger(clientConn net.Conn) *zerolog.Logger {
ev := log.With().Str("protocol", "tcp").
Str("remote", clientConn.RemoteAddr().String())
if s.listener != nil {
ev = ev.Str("addr", s.listener.Addr().String())
}
l := ev.Logger()
return &l
}
func (s *TCPServer) loggerWithDst(dstConn net.Conn, clientConn net.Conn) *zerolog.Logger {
ev := log.With().Str("protocol", "tcp").
Str("remote", clientConn.RemoteAddr().String()).
Str("dst", dstConn.RemoteAddr().String())
if s.listener != nil {
ev = ev.Str("addr", s.listener.Addr().String())
}
l := ev.Logger()
return &l
}
func (s *TCPServer) handle(conn net.Conn) {
defer conn.Close()
dst, err := s.redirect(conn)
if err != nil {
// Health check probe: close connection
if errors.Is(err, ErrCloseImmediately) {
s.logger(conn).Info().Msg("Health check received")
return
}
s.logger(conn).Err(err).Msg("failed to redirect connection")
return
}
defer dst.Close()
pipe := ioutils.NewBidirectionalPipe(s.ctx, conn, dst)
err = pipe.Start()
if err != nil {
s.loggerWithDst(dst, conn).Err(err).Msg("failed to start bidirectional pipe")
return
}
}
func (s *TCPServer) redirect(conn net.Conn) (net.Conn, error) {
// Read the stream header once as a handshake.
var headerBuf [headerSize]byte
_ = conn.SetReadDeadline(time.Now().Add(dialTimeout))
if _, err := io.ReadFull(conn, headerBuf[:]); err != nil {
return nil, err
}
_ = conn.SetReadDeadline(time.Time{})
header := ToHeader(&headerBuf)
if !header.Validate() {
return nil, ErrInvalidHeader
}
// Health check: close immediately if FlagCloseImmediately is set
if header.ShouldCloseImmediately() {
return nil, ErrCloseImmediately
}
// get destination connection
host, port := header.GetHostPort()
return s.createDestConnection(host, port)
}
func (s *TCPServer) createDestConnection(host, port string) (net.Conn, error) {
addr := net.JoinHostPort(host, port)
conn, err := net.DialTimeout("tcp", addr, dialTimeout)
if err != nil {
return nil, err
}
return conn, nil
}

View File

@@ -0,0 +1,26 @@
package stream_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/agent/pkg/agent/stream"
)
func TestTCPHealthCheck(t *testing.T) {
certs := genTestCerts(t)
srv := startTCPServer(t, certs)
err := stream.TCPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
require.NoError(t, err, "health check")
}
func TestUDPHealthCheck(t *testing.T) {
certs := genTestCerts(t)
srv := startUDPServer(t, certs)
err := stream.UDPHealthCheck(srv.Addr.String(), certs.CaCert, certs.ClientCert)
require.NoError(t, err, "health check")
}

View File

@@ -0,0 +1,94 @@
package stream_test
import (
"bufio"
"context"
"crypto/tls"
"crypto/x509"
"io"
"net"
"net/http"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/agent/pkg/agent/common"
"github.com/yusing/godoxy/agent/pkg/agent/stream"
)
func TestTLSALPNMux_HTTPAndStreamShareOnePort(t *testing.T) {
certs := genTestCerts(t)
baseLn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
require.NoError(t, err, "listen tcp")
defer baseLn.Close()
baseAddr := baseLn.Addr().String()
caCertPool := x509.NewCertPool()
caCertPool.AddCert(certs.CaCert)
serverTLS := &tls.Config{
Certificates: []tls.Certificate{*certs.SrvCert},
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
MinVersion: tls.VersionTLS12,
NextProtos: []string{"http/1.1", stream.StreamALPN},
}
ctx, cancel := context.WithCancel(t.Context())
defer cancel()
streamSrv := stream.NewTCPServerHandler(ctx)
defer func() { _ = streamSrv.Close() }()
tlsLn := tls.NewListener(baseLn, serverTLS)
defer func() { _ = tlsLn.Close() }()
// HTTP server
httpSrv := &http.Server{Handler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("ok"))
}),
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){
stream.StreamALPN: func(_ *http.Server, conn *tls.Conn, _ http.Handler) {
streamSrv.ServeConn(conn)
},
},
}
go func() { _ = httpSrv.Serve(tlsLn) }()
defer func() { _ = httpSrv.Close() }()
// Stream destination
dstAddr, closeDst := startTCPEcho(t)
defer closeDst()
// HTTP client over the same port
clientTLS := &tls.Config{
Certificates: []tls.Certificate{*certs.ClientCert},
RootCAs: caCertPool,
MinVersion: tls.VersionTLS12,
NextProtos: []string{"http/1.1"},
ServerName: common.CertsDNSName,
}
hc, err := tls.Dial("tcp", baseAddr, clientTLS)
require.NoError(t, err, "dial https")
defer hc.Close()
_ = hc.SetDeadline(time.Now().Add(2 * time.Second))
_, err = hc.Write([]byte("GET / HTTP/1.1\r\nHost: godoxy-agent\r\n\r\n"))
require.NoError(t, err, "write http request")
r := bufio.NewReader(hc)
statusLine, err := r.ReadString('\n')
require.NoError(t, err, "read status line")
require.Contains(t, statusLine, "200", "expected 200")
// Stream client over the same port
client := NewTCPClient(t, baseAddr, dstAddr, certs)
defer client.Close()
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
msg := []byte("ping over mux")
_, err = client.Write(msg)
require.NoError(t, err, "write stream payload")
buf := make([]byte, len(msg))
_, err = io.ReadFull(client, buf)
require.NoError(t, err, "read stream payload")
require.Equal(t, msg, buf)
}

View File

@@ -0,0 +1,201 @@
package stream_test
import (
"crypto/tls"
"fmt"
"io"
"sync"
"testing"
"time"
"github.com/pion/dtls/v3"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/agent/stream"
)
func TestTCPServer_FullFlow(t *testing.T) {
certs := genTestCerts(t)
dstAddr, closeDst := startTCPEcho(t)
defer closeDst()
srv := startTCPServer(t, certs)
client := NewTCPClient(t, srv.Addr.String(), dstAddr, certs)
defer client.Close()
// Ensure ALPN is negotiated as expected (required for multiplexing).
withState, ok := client.(interface{ ConnectionState() tls.ConnectionState })
require.True(t, ok, "tcp client should expose TLS connection state")
require.Equal(t, stream.StreamALPN, withState.ConnectionState().NegotiatedProtocol)
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
msg := []byte("ping over tcp")
_, err := client.Write(msg)
require.NoError(t, err, "write to client")
buf := make([]byte, len(msg))
_, err = io.ReadFull(client, buf)
require.NoError(t, err, "read from client")
require.Equal(t, string(msg), string(buf), "unexpected echo")
}
func TestTCPServer_ConcurrentConnections(t *testing.T) {
certs := genTestCerts(t)
dstAddr, closeDst := startTCPEcho(t)
defer closeDst()
srv := startTCPServer(t, certs)
const nClients = 25
errs := make(chan error, nClients)
var wg sync.WaitGroup
wg.Add(nClients)
for i := range nClients {
go func() {
defer wg.Done()
client := NewTCPClient(t, srv.Addr.String(), dstAddr, certs)
defer client.Close()
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
msg := fmt.Appendf(nil, "ping over tcp %d", i)
if _, err := client.Write(msg); err != nil {
errs <- fmt.Errorf("write to client: %w", err)
return
}
buf := make([]byte, len(msg))
if _, err := io.ReadFull(client, buf); err != nil {
errs <- fmt.Errorf("read from client: %w", err)
return
}
if string(msg) != string(buf) {
errs <- fmt.Errorf("unexpected echo: got=%q want=%q", string(buf), string(msg))
return
}
}()
}
wg.Wait()
close(errs)
for err := range errs {
require.NoError(t, err)
}
}
func TestUDPServer_RejectInvalidClient(t *testing.T) {
certs := genTestCerts(t)
// Generate a self-signed client cert that is NOT signed by the CA
_, _, invalidClientPEM, err := agent.NewAgent()
require.NoError(t, err, "generate invalid client certs")
invalidClientCert, err := invalidClientPEM.ToTLSCert()
require.NoError(t, err, "parse invalid client cert")
dstAddr, closeDst := startUDPEcho(t)
defer closeDst()
srv := startUDPServer(t, certs)
// Try to connect with a client cert from a different CA
_, err = stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, invalidClientCert)
require.Error(t, err, "expected error when connecting with client cert from different CA")
var handshakeErr *dtls.HandshakeError
require.ErrorAs(t, err, &handshakeErr, "expected handshake error")
}
func TestUDPServer_RejectClientWithoutCert(t *testing.T) {
certs := genTestCerts(t)
dstAddr, closeDst := startUDPEcho(t)
defer closeDst()
srv := startUDPServer(t, certs)
time.Sleep(time.Second)
// Try to connect without any client certificate
// Create a TLS cert without a private key to simulate no client cert
emptyCert := &tls.Certificate{}
_, err := stream.NewUDPClient(srv.Addr.String(), dstAddr, certs.CaCert, emptyCert)
require.Error(t, err, "expected error when connecting without client cert")
require.ErrorContains(t, err, "no certificate provided", "expected no cert error")
}
func TestUDPServer_FullFlow(t *testing.T) {
certs := genTestCerts(t)
dstAddr, closeDst := startUDPEcho(t)
defer closeDst()
srv := startUDPServer(t, certs)
client := NewUDPClient(t, srv.Addr.String(), dstAddr, certs)
defer client.Close()
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
msg := []byte("ping over udp")
_, err := client.Write(msg)
require.NoError(t, err, "write to client")
buf := make([]byte, 2048)
n, err := client.Read(buf)
require.NoError(t, err, "read from client")
require.Equal(t, string(msg), string(buf[:n]), "unexpected echo")
}
func TestUDPServer_ConcurrentConnections(t *testing.T) {
certs := genTestCerts(t)
dstAddr, closeDst := startUDPEcho(t)
defer closeDst()
srv := startUDPServer(t, certs)
const nClients = 25
errs := make(chan error, nClients)
var wg sync.WaitGroup
wg.Add(nClients)
for i := range nClients {
go func() {
defer wg.Done()
client := NewUDPClient(t, srv.Addr.String(), dstAddr, certs)
defer client.Close()
_ = client.SetDeadline(time.Now().Add(5 * time.Second))
msg := fmt.Appendf(nil, "ping over udp %d", i)
if _, err := client.Write(msg); err != nil {
errs <- fmt.Errorf("write to client: %w", err)
return
}
buf := make([]byte, 2048)
n, err := client.Read(buf)
if err != nil {
errs <- fmt.Errorf("read from client: %w", err)
return
}
if string(msg) != string(buf[:n]) {
errs <- fmt.Errorf("unexpected echo: got=%q want=%q", string(buf[:n]), string(msg))
return
}
}()
}
wg.Wait()
close(errs)
for err := range errs {
require.NoError(t, err)
}
}

View File

@@ -0,0 +1,177 @@
package stream_test
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"io"
"net"
"testing"
"time"
"github.com/pion/transport/v3/udp"
"github.com/stretchr/testify/require"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/agent/stream"
)
// CertBundle holds all certificates needed for testing.
type CertBundle struct {
CaCert *x509.Certificate
SrvCert *tls.Certificate
ClientCert *tls.Certificate
}
// genTestCerts generates certificates for testing and returns them as a CertBundle.
func genTestCerts(t *testing.T) CertBundle {
t.Helper()
caPEM, srvPEM, clientPEM, err := agent.NewAgent()
require.NoError(t, err, "generate agent certs")
caCert, err := caPEM.ToTLSCert()
require.NoError(t, err, "parse CA cert")
srvCert, err := srvPEM.ToTLSCert()
require.NoError(t, err, "parse server cert")
clientCert, err := clientPEM.ToTLSCert()
require.NoError(t, err, "parse client cert")
return CertBundle{
CaCert: caCert.Leaf,
SrvCert: srvCert,
ClientCert: clientCert,
}
}
// startTCPEcho starts a TCP echo server and returns its address and close function.
func startTCPEcho(t *testing.T) (addr string, closeFn func()) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err, "listen tcp")
done := make(chan struct{})
go func() {
defer close(done)
for {
c, err := ln.Accept()
if err != nil {
return
}
go func(conn net.Conn) {
defer conn.Close()
_, _ = io.Copy(conn, conn)
}(c)
}
}()
return ln.Addr().String(), func() {
_ = ln.Close()
<-done
}
}
// startUDPEcho starts a UDP echo server and returns its address and close function.
func startUDPEcho(t *testing.T) (addr string, closeFn func()) {
t.Helper()
pc, err := net.ListenPacket("udp", "127.0.0.1:0")
require.NoError(t, err, "listen udp")
uc := pc.(*net.UDPConn)
done := make(chan struct{})
go func() {
defer close(done)
buf := make([]byte, 65535)
for {
n, raddr, err := uc.ReadFromUDP(buf)
if err != nil {
return
}
_, _ = uc.WriteToUDP(buf[:n], raddr)
}
}()
return uc.LocalAddr().String(), func() {
_ = uc.Close()
<-done
}
}
// TestServer wraps a server with its startup goroutine for cleanup.
type TestServer struct {
Server interface{ Close() error }
Addr net.Addr
}
// startTCPServer starts a TCP server and returns a TestServer for cleanup.
func startTCPServer(t *testing.T, certs CertBundle) TestServer {
t.Helper()
tcpLn, err := net.ListenTCP("tcp", &net.TCPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0})
require.NoError(t, err, "listen tcp")
ctx, cancel := context.WithCancel(t.Context())
srv := stream.NewTCPServer(ctx, tcpLn, certs.CaCert, certs.SrvCert)
errCh := make(chan error, 1)
go func() { errCh <- srv.Start() }()
t.Cleanup(func() {
cancel()
_ = srv.Close()
err := <-errCh
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) {
t.Logf("tcp server exit: %v", err)
}
})
return TestServer{
Server: srv,
Addr: srv.Addr(),
}
}
// startUDPServer starts a UDP server and returns a TestServer for cleanup.
func startUDPServer(t *testing.T, certs CertBundle) TestServer {
t.Helper()
ctx, cancel := context.WithCancel(t.Context())
srv := stream.NewUDPServer(ctx, "udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0}, certs.CaCert, certs.SrvCert)
errCh := make(chan error, 1)
go func() { errCh <- srv.Start() }()
time.Sleep(100 * time.Millisecond)
t.Cleanup(func() {
cancel()
_ = srv.Close()
err := <-errCh
if err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, net.ErrClosed) && !errors.Is(err, udp.ErrClosedListener) {
t.Logf("udp server exit: %v", err)
}
})
return TestServer{
Server: srv,
Addr: srv.Addr(),
}
}
// NewTCPClient creates a TCP client connected to the server with test certificates.
func NewTCPClient(t *testing.T, serverAddr, targetAddress string, certs CertBundle) net.Conn {
t.Helper()
client, err := stream.NewTCPClient(serverAddr, targetAddress, certs.CaCert, certs.ClientCert)
require.NoError(t, err, "create tcp client")
return client
}
// NewUDPClient creates a UDP client connected to the server with test certificates.
func NewUDPClient(t *testing.T, serverAddr, targetAddress string, certs CertBundle) net.Conn {
t.Helper()
client, err := stream.NewUDPClient(serverAddr, targetAddress, certs.CaCert, certs.ClientCert)
require.NoError(t, err, "create udp client")
return client
}

View File

@@ -0,0 +1,118 @@
package stream
import (
"crypto/tls"
"crypto/x509"
"net"
"time"
"github.com/pion/dtls/v3"
"github.com/yusing/godoxy/agent/pkg/agent/common"
)
type UDPClient struct {
conn net.Conn
}
// NewUDPClient creates a new UDP client for the agent.
//
// It will establish a DTLS connection and send a stream request header to the server.
//
// It returns an error if
// - the target address is invalid
// - the stream request header is invalid
// - the DTLS configuration is invalid
// - the DTLS connection fails
// - the stream request header is not sent
func NewUDPClient(serverAddr, targetAddress string, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
host, port, err := net.SplitHostPort(targetAddress)
if err != nil {
return nil, err
}
header, err := NewStreamRequestHeader(host, port)
if err != nil {
return nil, err
}
return newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
}
func newUDPClientWIthHeader(serverAddr string, header *StreamRequestHeader, caCert *x509.Certificate, clientCert *tls.Certificate) (net.Conn, error) {
// Setup DTLS configuration
caCertPool := x509.NewCertPool()
caCertPool.AddCert(caCert)
dtlsConfig := &dtls.Config{
Certificates: []tls.Certificate{*clientCert},
RootCAs: caCertPool,
InsecureSkipVerify: false,
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
ServerName: common.CertsDNSName,
CipherSuites: dTLSCipherSuites,
}
raddr, err := net.ResolveUDPAddr("udp", serverAddr)
if err != nil {
return nil, err
}
// Establish DTLS connection
conn, err := dtls.Dial("udp", raddr, dtlsConfig)
if err != nil {
return nil, err
}
// Send the stream header once as a handshake.
if _, err := conn.Write(header.Bytes()); err != nil {
_ = conn.Close()
return nil, err
}
return &UDPClient{
conn: conn,
}, nil
}
func UDPHealthCheck(serverAddr string, caCert *x509.Certificate, clientCert *tls.Certificate) error {
header := NewStreamHealthCheckHeader()
conn, err := newUDPClientWIthHeader(serverAddr, header, caCert, clientCert)
if err != nil {
return err
}
conn.Close()
return nil
}
func (c *UDPClient) Read(p []byte) (n int, err error) {
return c.conn.Read(p)
}
func (c *UDPClient) Write(p []byte) (n int, err error) {
return c.conn.Write(p)
}
func (c *UDPClient) LocalAddr() net.Addr {
return c.conn.LocalAddr()
}
func (c *UDPClient) RemoteAddr() net.Addr {
return c.conn.RemoteAddr()
}
func (c *UDPClient) SetDeadline(t time.Time) error {
return c.conn.SetDeadline(t)
}
func (c *UDPClient) SetReadDeadline(t time.Time) error {
return c.conn.SetReadDeadline(t)
}
func (c *UDPClient) SetWriteDeadline(t time.Time) error {
return c.conn.SetWriteDeadline(t)
}
func (c *UDPClient) Close() error {
return c.conn.Close()
}

View File

@@ -0,0 +1,208 @@
package stream
import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"io"
"net"
"time"
"github.com/pion/dtls/v3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
type UDPServer struct {
ctx context.Context
network string
laddr *net.UDPAddr
listener net.Listener
dtlsConfig *dtls.Config
}
func NewUDPServer(ctx context.Context, network string, laddr *net.UDPAddr, caCert *x509.Certificate, serverCert *tls.Certificate) *UDPServer {
caCertPool := x509.NewCertPool()
caCertPool.AddCert(caCert)
dtlsConfig := &dtls.Config{
Certificates: []tls.Certificate{*serverCert},
ClientCAs: caCertPool,
ClientAuth: dtls.RequireAndVerifyClientCert,
ExtendedMasterSecret: dtls.RequireExtendedMasterSecret,
CipherSuites: dTLSCipherSuites,
}
s := &UDPServer{
ctx: ctx,
network: network,
laddr: laddr,
dtlsConfig: dtlsConfig,
}
return s
}
func (s *UDPServer) Start() error {
listener, err := dtls.Listen(s.network, s.laddr, s.dtlsConfig)
if err != nil {
return err
}
s.listener = listener
context.AfterFunc(s.ctx, func() {
_ = s.listener.Close()
})
for {
conn, err := s.listener.Accept()
if err != nil {
// Expected error when context cancelled
if errors.Is(err, net.ErrClosed) && s.ctx.Err() != nil {
return s.ctx.Err()
}
return err
}
go s.handleDTLSConnection(conn)
}
}
func (s *UDPServer) Addr() net.Addr {
if s.listener != nil {
return s.listener.Addr()
}
return s.laddr
}
func (s *UDPServer) Close() error {
if s.listener != nil {
return s.listener.Close()
}
return nil
}
func (s *UDPServer) logger(clientConn net.Conn) *zerolog.Logger {
l := log.With().Str("protocol", "udp").
Str("addr", s.Addr().String()).
Str("remote", clientConn.RemoteAddr().String()).Logger()
return &l
}
func (s *UDPServer) loggerWithDst(clientConn net.Conn, dstConn *net.UDPConn) *zerolog.Logger {
l := log.With().Str("protocol", "udp").
Str("addr", s.Addr().String()).
Str("remote", clientConn.RemoteAddr().String()).
Str("dst", dstConn.RemoteAddr().String()).Logger()
return &l
}
func (s *UDPServer) handleDTLSConnection(clientConn net.Conn) {
defer clientConn.Close()
// Read the stream header once as a handshake.
var headerBuf [headerSize]byte
_ = clientConn.SetReadDeadline(time.Now().Add(dialTimeout))
if _, err := io.ReadFull(clientConn, headerBuf[:]); err != nil {
s.logger(clientConn).Err(err).Msg("failed to read stream header")
return
}
_ = clientConn.SetReadDeadline(time.Time{})
header := ToHeader(&headerBuf)
if !header.Validate() {
s.logger(clientConn).Error().Bytes("header", headerBuf[:]).Msg("invalid stream header received")
return
}
// Health check probe: close connection
if header.ShouldCloseImmediately() {
s.logger(clientConn).Info().Msg("Health check received")
return
}
host, port := header.GetHostPort()
dstConn, err := s.createDestConnection(host, port)
if err != nil {
s.logger(clientConn).Err(err).Msg("failed to get or create destination connection")
return
}
defer dstConn.Close()
go s.forwardFromDestination(dstConn, clientConn)
buf := sizedPool.GetSized(65535)
defer sizedPool.Put(buf)
for {
select {
case <-s.ctx.Done():
return
default:
n, err := clientConn.Read(buf)
// Per net.Conn contract, Read may return (n > 0, err == io.EOF).
// Always forward any bytes we got before acting on the error.
if n > 0 {
if _, werr := dstConn.Write(buf[:n]); werr != nil {
s.logger(clientConn).Err(werr).Msgf("failed to write %d bytes to destination", n)
return
}
}
if err != nil {
// Expected shutdown paths.
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
return
}
s.logger(clientConn).Err(err).Msg("failed to read from client")
return
}
}
}
}
func (s *UDPServer) createDestConnection(host, port string) (*net.UDPConn, error) {
addr := net.JoinHostPort(host, port)
udpAddr, err := net.ResolveUDPAddr("udp", addr)
if err != nil {
return nil, err
}
dstConn, err := net.DialUDP("udp", nil, udpAddr)
if err != nil {
return nil, err
}
return dstConn, nil
}
func (s *UDPServer) forwardFromDestination(dstConn *net.UDPConn, clientConn net.Conn) {
buffer := sizedPool.GetSized(65535)
defer sizedPool.Put(buffer)
for {
select {
case <-s.ctx.Done():
return
default:
_ = dstConn.SetReadDeadline(time.Now().Add(readDeadline))
n, err := dstConn.Read(buffer)
if err != nil {
// The destination socket can be closed when the client disconnects (e.g. during
// the stream support probe in AgentConfig.StartWithCerts). Treat that as a
// normal exit and avoid noisy logs.
if errors.Is(err, net.ErrClosed) {
return
}
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
s.loggerWithDst(clientConn, dstConn).Err(err).Msg("failed to read from destination")
return
}
if _, err := clientConn.Write(buffer[:n]); err != nil {
s.loggerWithDst(clientConn, dstConn).Err(err).Msgf("failed to write %d bytes to client", n)
return
}
}
}
}

View File

@@ -1,44 +0,0 @@
services:
agent:
image: "{{.Image}}"
container_name: godoxy-agent
restart: always
network_mode: host # do not change this
environment:
AGENT_NAME: "{{.Name}}"
AGENT_PORT: "{{.Port}}"
AGENT_CA_CERT: "{{.CACert}}"
AGENT_SSL_CERT: "{{.SSLCert}}"
# use agent as a docker socket proxy: [host]:port
# set LISTEN_ADDR to enable (e.g. 127.0.0.1:2375)
LISTEN_ADDR:
POST: false
ALLOW_RESTARTS: false
ALLOW_START: false
ALLOW_STOP: false
AUTH: false
BUILD: false
COMMIT: false
CONFIGS: false
CONTAINERS: false
DISTRIBUTION: false
EVENTS: true
EXEC: false
GRPC: false
IMAGES: false
INFO: false
NETWORKS: false
NODES: false
PING: true
PLUGINS: false
SECRETS: false
SERVICES: false
SESSION: false
SWARM: false
SYSTEM: false
TASKS: false
VERSION: true
VOLUMES: false
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./data:/app/data

View File

@@ -5,7 +5,8 @@ services:
restart: always
{{ if eq .ContainerRuntime "podman" -}}
ports:
- "{{.Port}}:{{.Port}}"
- "{{.Port}}:{{.Port}}/tcp"
- "{{.Port}}:{{.Port}}/udp"
{{ else -}}
network_mode: host # do not change this
{{ end -}}

View File

@@ -0,0 +1,122 @@
# agent/pkg/agentproxy
Package for configuring HTTP proxy connections through the GoDoxy Agent using HTTP headers.
## Overview
This package provides types and functions for parsing and setting agent proxy configuration via HTTP headers. It supports both a modern base64-encoded JSON format and a legacy header-based format for backward compatibility.
## Architecture
```mermaid
graph LR
A[HTTP Request] --> B[ConfigFromHeaders]
B --> C{Modern Format?}
C -->|Yes| D[Parse X-Proxy-Config Base64 JSON]
C -->|No| E[Parse Legacy Headers]
D --> F[Config]
E --> F
F --> G[SetAgentProxyConfigHeaders]
G --> H[Modern Headers]
G --> I[Legacy Headers]
```
## Public Types
### Config
```go
type Config struct {
Scheme string // Proxy scheme (http or https)
Host string // Proxy host (hostname or hostname:port)
HTTPConfig // Extended HTTP configuration
}
```
The `HTTPConfig` embedded type (from `internal/route/types`) includes:
- `NoTLSVerify` - Skip TLS certificate verification
- `ResponseHeaderTimeout` - Timeout for response headers
- `DisableCompression` - Disable gzip compression
## Public Functions
### ConfigFromHeaders
```go
func ConfigFromHeaders(h http.Header) (Config, error)
```
Parses proxy configuration from HTTP request headers. Tries modern format first, falls back to legacy format if not present.
### proxyConfigFromHeaders
```go
func proxyConfigFromHeaders(h http.Header) (Config, error)
```
Parses the modern base64-encoded JSON format from `X-Proxy-Config` header.
### proxyConfigFromHeadersLegacy
```go
func proxyConfigFromHeadersLegacy(h http.Header) Config
```
Parses the legacy header format:
- `X-Proxy-Host` - Proxy host
- `X-Proxy-Https` - Whether to use HTTPS
- `X-Proxy-Skip-Tls-Verify` - Skip TLS verification
- `X-Proxy-Response-Header-Timeout` - Response timeout in seconds
### SetAgentProxyConfigHeaders
```go
func (cfg *Config) SetAgentProxyConfigHeaders(h http.Header)
```
Sets headers for modern format with base64-encoded JSON config.
### SetAgentProxyConfigHeadersLegacy
```go
func (cfg *Config) SetAgentProxyConfigHeadersLegacy(h http.Header)
```
Sets headers for legacy format with individual header fields.
## Header Constants
Modern headers:
- `HeaderXProxyScheme` - Proxy scheme
- `HeaderXProxyHost` - Proxy host
- `HeaderXProxyConfig` - Base64-encoded JSON config
Legacy headers (deprecated):
- `HeaderXProxyHTTPS`
- `HeaderXProxySkipTLSVerify`
- `HeaderXProxyResponseHeaderTimeout`
## Usage Example
```go
// Reading configuration from incoming request headers
func handleRequest(w http.ResponseWriter, r *http.Request) {
cfg, err := agentproxy.ConfigFromHeaders(r.Header)
if err != nil {
http.Error(w, "Invalid proxy config", http.StatusBadRequest)
return
}
// Use cfg.Scheme and cfg.Host to proxy the request
// ...
}
```
## Integration
This package is used by `agent/pkg/handler/proxy_http.go` to configure reverse proxy connections based on request headers.

View File

@@ -0,0 +1,73 @@
package agentproxy
import (
"encoding/base64"
"net/http"
"strconv"
"time"
"github.com/bytedance/sonic"
route "github.com/yusing/godoxy/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 cfg.Host == "" || err != nil {
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 cfg
}
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 = sonic.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, _ := sonic.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))
}

102
agent/pkg/certs/README.md Normal file
View File

@@ -0,0 +1,102 @@
# agent/pkg/certs
Certificate management package for creating and extracting certificate archives.
## Overview
This package provides utilities for packaging SSL certificates into ZIP archives and extracting them. It is used by the GoDoxy Agent to distribute certificates to clients in a convenient format.
## Architecture
```mermaid
graph LR
A[Raw Certs] --> B[ZipCert]
B --> C[ZIP Archive]
C --> D[ca.pem]
C --> E[cert.pem]
C --> F[key.pem]
G[ZIP Archive] --> H[ExtractCert]
H --> I[ca, crt, key]
```
## Public Functions
### ZipCert
```go
func ZipCert(ca, crt, key []byte) ([]byte, error)
```
Creates a ZIP archive containing three PEM files:
- `ca.pem` - CA certificate
- `cert.pem` - Server/client certificate
- `key.pem` - Private key
**Parameters:**
- `ca` - CA certificate in PEM format
- `crt` - Certificate in PEM format
- `key` - Private key in PEM format
**Returns:**
- ZIP archive bytes
- Error if packing fails
### ExtractCert
```go
func ExtractCert(data []byte) (ca, crt, key []byte, err error)
```
Extracts certificates from a ZIP archive created by `ZipCert`.
**Parameters:**
- `data` - ZIP archive bytes
**Returns:**
- `ca` - CA certificate bytes
- `crt` - Certificate bytes
- `key` - Private key bytes
- Error if extraction fails
### AgentCertsFilepath
```go
func AgentCertsFilepath(host string) (filepathOut string, ok bool)
```
Generates the file path for storing agent certificates.
**Parameters:**
- `host` - Agent hostname
**Returns:**
- Full file path within `certs/` directory
- `false` if host is invalid (contains path separators or special characters)
### isValidAgentHost
```go
func isValidAgentHost(host string) bool
```
Validates that a host string is safe for use in file paths.
## Constants
```go
const AgentCertsBasePath = "certs"
```
Base directory for storing certificate archives.
## File Format
The ZIP archive uses `zip.Store` compression (no compression) for fast creation and extraction. Each file is stored with its standard name (`ca.pem`, `cert.pem`, `key.pem`).

View File

@@ -6,7 +6,7 @@ import (
"io"
"path/filepath"
"github.com/yusing/go-proxy/internal/utils/strutils"
strutils "github.com/yusing/goutils/strings"
)
const AgentCertsBasePath = "certs"

View File

@@ -4,7 +4,7 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/yusing/go-proxy/agent/pkg/certs"
"github.com/yusing/godoxy/agent/pkg/certs"
)
func TestZipCert(t *testing.T) {

52
agent/pkg/env/README.md vendored Normal file
View File

@@ -0,0 +1,52 @@
# agent/pkg/env
Environment configuration package for the GoDoxy Agent.
## Overview
This package manages environment variable parsing and provides a centralized location for all agent configuration options. It is automatically initialized on import.
## Variables
| Variable | Type | Default | Description |
| -------------------------- | ---------------- | ---------------------- | --------------------------------------- |
| `DockerSocket` | string | `/var/run/docker.sock` | Path to Docker socket |
| `AgentName` | string | System hostname | Agent identifier |
| `AgentPort` | int | `8890` | Agent server port |
| `AgentSkipClientCertCheck` | bool | `false` | Skip mTLS certificate verification |
| `AgentCACert` | string | (empty) | Base64 Encoded CA certificate + key |
| `AgentSSLCert` | string | (empty) | Base64 Encoded server certificate + key |
| `Runtime` | ContainerRuntime | `docker` | Container runtime (docker or podman) |
## ContainerRuntime Type
```go
type ContainerRuntime string
const (
ContainerRuntimeDocker ContainerRuntime = "docker"
ContainerRuntimePodman ContainerRuntime = "podman"
)
```
## Public Functions
### DefaultAgentName
```go
func DefaultAgentName() string
```
Returns the system hostname as the default agent name. Falls back to `"agent"` if hostname cannot be determined.
### Load
```go
func Load()
```
Reloads all environment variables from the environment. Called automatically on package init, but can be called again to refresh configuration.
## Validation
The `Load()` function validates that `Runtime` is either `docker` or `podman`. An invalid runtime causes a fatal error.

18
agent/pkg/env/env.go vendored
View File

@@ -3,8 +3,8 @@ package env
import (
"os"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/goutils/env"
"github.com/rs/zerolog/log"
)
@@ -32,14 +32,14 @@ func init() {
}
func Load() {
DockerSocket = common.GetEnvString("DOCKER_SOCKET", "/var/run/docker.sock")
AgentName = common.GetEnvString("AGENT_NAME", DefaultAgentName())
AgentPort = common.GetEnvInt("AGENT_PORT", 8890)
AgentSkipClientCertCheck = common.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
DockerSocket = env.GetEnvString("DOCKER_SOCKET", "/var/run/docker.sock")
AgentName = env.GetEnvString("AGENT_NAME", DefaultAgentName())
AgentPort = env.GetEnvInt("AGENT_PORT", 8890)
AgentSkipClientCertCheck = env.GetEnvBool("AGENT_SKIP_CLIENT_CERT_CHECK", false)
AgentCACert = common.GetEnvString("AGENT_CA_CERT", "")
AgentSSLCert = common.GetEnvString("AGENT_SSL_CERT", "")
Runtime = agent.ContainerRuntime(common.GetEnvString("RUNTIME", "docker"))
AgentCACert = env.GetEnvString("AGENT_CA_CERT", "")
AgentSSLCert = env.GetEnvString("AGENT_SSL_CERT", "")
Runtime = agent.ContainerRuntime(env.GetEnvString("RUNTIME", "docker"))
switch Runtime {
case agent.ContainerRuntimeDocker, agent.ContainerRuntimePodman: //, agent.ContainerRuntimeNerdctl:

127
agent/pkg/handler/README.md Normal file
View File

@@ -0,0 +1,127 @@
# agent/pkg/handler
HTTP request handler package for the GoDoxy Agent.
## Overview
This package provides the HTTP handler for the GoDoxy Agent server, including endpoints for:
- Version information
- Agent name and runtime
- Health checks
- System metrics (via SSE)
- HTTP proxy routing
- Docker socket proxying
## Architecture
```mermaid
graph TD
A[HTTP Request] --> B[NewAgentHandler]
B --> C{ServeMux Router}
C --> D[GET /version]
C --> E[GET /name]
C --> F[GET /runtime]
C --> G[GET /health]
C --> H[GET /system-info]
C --> I[GET /proxy/http/#123;path...#125;]
C --> J[ /#42; Docker Socket]
H --> K[Gin Router]
K --> L[WebSocket Upgrade]
L --> M[SystemInfo Poller]
```
## Public Types
### ServeMux
```go
type ServeMux struct{ *http.ServeMux }
```
Wrapper around `http.ServeMux` with agent-specific endpoint helpers.
**Methods:**
- `HandleEndpoint(method, endpoint string, handler http.HandlerFunc)` - Registers handler with API base path
- `HandleFunc(endpoint string, handler http.HandlerFunc)` - Registers GET handler with API base path
## Public Functions
### NewAgentHandler
```go
func NewAgentHandler() http.Handler
```
Creates and configures the HTTP handler for the agent server. Sets up:
- Gin-based metrics handler with WebSocket support for SSE
- All standard agent endpoints
- HTTP proxy endpoint
- Docker socket proxy fallback
## Endpoints
| Endpoint | Method | Description |
| ----------------------- | -------- | ------------------------------------ |
| `/version` | GET | Returns agent version |
| `/name` | GET | Returns agent name |
| `/runtime` | GET | Returns container runtime |
| `/health` | GET | Health check with scheme query param |
| `/system-info` | GET | System metrics via SSE or WebSocket |
| `/proxy/http/{path...}` | GET/POST | HTTP proxy with config from headers |
| `/*` | \* | Docker socket proxy |
## Sub-packages
### proxy_http.go
Handles HTTP proxy requests by reading configuration from request headers and proxying to the configured upstream.
**Key Function:**
- `ProxyHTTP(w, r)` - Proxies HTTP requests based on `X-Proxy-*` headers
### check_health.go
Handles health check requests for various schemes.
**Key Function:**
- `CheckHealth(w, r)` - Performs health checks with configurable scheme
**Supported Schemes:**
- `http`, `https` - HTTP health check
- `h2c` - HTTP/2 cleartext health check
- `tcp`, `udp`, `tcp4`, `udp4`, `tcp6`, `udp6` - TCP/UDP health check
- `fileserver` - File existence check
## Usage Example
```go
package main
import (
"net/http"
"github.com/yusing/godoxy/agent/pkg/handler"
)
func main() {
mux := http.NewServeMux()
mux.Handle("/", handler.NewAgentHandler())
http.ListenAndServe(":8890", mux)
}
```
## WebSocket Support
The handler includes a permissive WebSocket upgrader for internal use (no origin check). This enables real-time system metrics streaming via Server-Sent Events (SSE).
## Docker Socket Integration
All unmatched requests fall through to the Docker socket handler, allowing the agent to proxy Docker API calls when configured.

View File

@@ -1,19 +1,18 @@
package handler
import (
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/go-proxy/internal/watcher/health/monitor"
"github.com/bytedance/sonic"
healthcheck "github.com/yusing/godoxy/internal/health/check"
"github.com/yusing/godoxy/internal/types"
)
var defaultHealthConfig = types.DefaultHealthConfig()
func CheckHealth(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
scheme := query.Get("scheme")
@@ -21,9 +20,12 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
http.Error(w, "missing scheme", http.StatusBadRequest)
return
}
timeout := parseMsOrDefault(query.Get("timeout"))
var result *types.HealthCheckResult
var err error
var (
result types.HealthCheckResult
err error
)
switch scheme {
case "fileserver":
path := query.Get("path")
@@ -31,24 +33,21 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
http.Error(w, "missing path", http.StatusBadRequest)
return
}
_, err := os.Stat(path)
result = &types.HealthCheckResult{Healthy: err == nil}
if err != nil {
result.Detail = err.Error()
}
case "http", "https": // path is optional
result, err = healthcheck.FileServer(path)
case "http", "https", "h2c": // path is optional
host := query.Get("host")
path := query.Get("path")
if host == "" {
http.Error(w, "missing host", http.StatusBadRequest)
return
}
result, err = monitor.NewHTTPHealthMonitor(&url.URL{
Scheme: scheme,
Host: host,
Path: path,
}, defaultHealthConfig).CheckHealth()
case "tcp", "udp":
url := url.URL{Scheme: scheme, Host: host}
if scheme == "h2c" {
result, err = healthcheck.H2C(r.Context(), &url, http.MethodHead, path, timeout)
} else {
result, err = healthcheck.HTTP(&url, http.MethodHead, path, timeout)
}
case "tcp", "udp", "tcp4", "udp4", "tcp6", "udp6":
host := query.Get("host")
if host == "" {
http.Error(w, "missing host", http.StatusBadRequest)
@@ -61,12 +60,10 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
return
}
if port != "" {
host = fmt.Sprintf("%s:%s", host, port)
host = net.JoinHostPort(host, port)
}
result, err = monitor.NewRawHealthMonitor(&url.URL{
Scheme: scheme,
Host: host,
}, defaultHealthConfig).CheckHealth()
url := url.URL{Scheme: scheme, Host: host}
result, err = healthcheck.Stream(r.Context(), &url, timeout)
}
if err != nil {
@@ -76,5 +73,18 @@ func CheckHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(result)
sonic.ConfigDefault.NewEncoder(w).Encode(result)
}
func parseMsOrDefault(msStr string) time.Duration {
if msStr == "" {
return types.HealthCheckTimeoutDefault
}
timeoutMs, _ := strconv.ParseInt(msStr, 10, 64)
if timeoutMs == 0 {
return types.HealthCheckTimeoutDefault
}
return time.Duration(timeoutMs) * time.Millisecond
}

View File

@@ -10,9 +10,9 @@ import (
"testing"
"github.com/stretchr/testify/require"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/handler"
"github.com/yusing/go-proxy/internal/types"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/handler"
"github.com/yusing/godoxy/internal/types"
)
func TestCheckHealthHTTP(t *testing.T) {

View File

@@ -1,16 +1,16 @@
package handler
import (
"fmt"
"net/http"
"github.com/bytedance/sonic"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/pkg"
socketproxy "github.com/yusing/go-proxy/socketproxy/pkg"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/env"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
socketproxy "github.com/yusing/godoxy/socketproxy/pkg"
"github.com/yusing/goutils/version"
)
type ServeMux struct{ *http.ServeMux }
@@ -44,14 +44,14 @@ func NewAgentHandler() http.Handler {
}
mux.HandleFunc(agent.EndpointProxyHTTP+"/{path...}", ProxyHTTP)
mux.HandleEndpoint("GET", agent.EndpointVersion, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, pkg.GetVersion())
})
mux.HandleEndpoint("GET", agent.EndpointName, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, env.AgentName)
})
mux.HandleEndpoint("GET", agent.EndpointRuntime, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, env.Runtime)
mux.HandleFunc(agent.EndpointInfo, func(w http.ResponseWriter, r *http.Request) {
agentInfo := agent.AgentInfo{
Version: version.Get(),
Name: env.AgentName,
Runtime: env.Runtime,
}
w.Header().Set("Content-Type", "application/json")
sonic.ConfigDefault.NewEncoder(w).Encode(agentInfo)
})
mux.HandleEndpoint("GET", agent.EndpointHealth, CheckHealth)
mux.HandleEndpoint("GET", agent.EndpointSystemInfo, metricsHandler.ServeHTTP)

View File

@@ -1,14 +1,14 @@
package handler
import (
"crypto/tls"
"fmt"
"net/http"
"net/http/httputil"
"strconv"
"strings"
"time"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/agentproxy"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/agentproxy"
)
func NewTransport() *http.Transport {
@@ -24,42 +24,47 @@ 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 = ""
r.URL.Host = ""
r.URL.Path = r.URL.Path[agent.HTTPProxyURLPrefixLen:] // strip the {API_BASE}/proxy/http prefix
r.RequestURI = r.URL.String()
// Strip the {API_BASE}/proxy/http prefix while preserving URL escaping.
//
// NOTE: `r.URL.Path` is decoded. If we rewrite it without keeping `RawPath`
// in sync, Go may re-escape the path (e.g. turning "%5B" into "%255B"),
// which breaks urls with percent-encoded characters, like Next.js static chunk URLs.
prefix := agent.APIEndpointBase + agent.EndpointProxyHTTP
r.URL.Path = strings.TrimPrefix(r.URL.Path, prefix)
if r.URL.RawPath != "" {
if after, ok := strings.CutPrefix(r.URL.RawPath, prefix); ok {
r.URL.RawPath = after
} else {
// RawPath is no longer a valid encoding for Path; force Go to re-derive it.
r.URL.RawPath = ""
}
}
r.RequestURI = ""
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

@@ -1,43 +0,0 @@
package server
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/agent/pkg/env"
"github.com/yusing/go-proxy/agent/pkg/handler"
"github.com/yusing/go-proxy/internal/net/gphttp/server"
"github.com/yusing/go-proxy/internal/task"
)
type Options struct {
CACert, ServerCert *tls.Certificate
Port int
}
func StartAgentServer(parent task.Parent, opt Options) {
caCertPool := x509.NewCertPool()
caCertPool.AddCert(opt.CACert.Leaf)
// Configure TLS
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{*opt.ServerCert},
ClientCAs: caCertPool,
ClientAuth: tls.RequireAndVerifyClientCert,
}
if env.AgentSkipClientCertCheck {
tlsConfig.ClientAuth = tls.NoClientCert
}
agentServer := &http.Server{
Addr: fmt.Sprintf(":%d", opt.Port),
Handler: handler.NewAgentHandler(),
TLSConfig: tlsConfig,
}
server.Start(parent, agentServer, nil, &log.Logger)
}

73
cmd/README.md Normal file
View File

@@ -0,0 +1,73 @@
# cmd
Main entry point package for GoDoxy, a lightweight reverse proxy with WebUI for Docker containers.
## Overview
This package contains the `main.go` entry point that initializes and starts the GoDoxy server. It coordinates the initialization of all core components including configuration loading, API server, authentication, and monitoring services.
## Architecture
```mermaid
graph TD
A[main] --> B[Init Profiling]
A --> C[Init Logger]
A --> D[Parallel Init]
D --> D1[DNS Providers]
D --> D2[Icon Cache]
D --> D3[System Info Poller]
D --> D4[Middleware Compose Files]
A --> E[JWT Secret Setup]
A --> F[Create Directories]
A --> G[Load Config]
A --> H[Start Proxy Servers]
A --> I[Init Auth]
A --> J[Start API Server]
A --> K[Debug Server]
A --> L[Uptime Poller]
A --> M[Watch Changes]
A --> N[Wait Exit]
```
## Main Function Flow
The `main()` function performs the following initialization steps:
1. **Profiling Setup**: Initializes pprof endpoints for performance monitoring
1. **Logger Initialization**: Configures zerolog with memory logging
1. **Parallel Initialization**: Starts DNS providers, icon cache, system info poller, and middleware
1. **JWT Secret**: Ensures API JWT secret is set (generates random if not provided)
1. **Directory Preparation**: Creates required directories for logs, certificates, etc.
1. **Configuration Loading**: Loads YAML configuration and reports any errors
1. **Proxy Servers**: Starts HTTP/HTTPS proxy servers based on configuration
1. **Authentication**: Initializes authentication system with access control
1. **API Server**: Starts the REST API server with all configured routes
1. **Debug Server**: Starts the debug page server (development mode)
1. **Monitoring**: Starts uptime and system info polling
1. **Change Watcher**: Starts watching for Docker container and configuration changes
1. **Graceful Shutdown**: Waits for exit signal with configured timeout
## Configuration
The main configuration is loaded from `config/config.yml`. Required directories include:
- `logs/` - Log files
- `config/` - Configuration directory
- `certs/` - SSL certificates
- `proxy/` - Proxy-related files
## Environment Variables
- `API_JWT_SECRET` - Secret key for JWT authentication (optional, auto-generated if not set)
## Dependencies
- `internal/api` - REST API handlers
- `internal/auth` - Authentication and ACL
- `internal/config` - Configuration management
- `internal/dnsproviders` - DNS provider integration
- `internal/homepage` - WebUI dashboard
- `internal/logging` - Logging infrastructure
- `internal/metrics` - System metrics collection
- `internal/route` - HTTP routing and middleware
- `github.com/yusing/goutils/task` - Task lifecycle management

View File

@@ -0,0 +1,18 @@
FROM golang:1.25.5-alpine AS builder
HEALTHCHECK NONE
WORKDIR /src
COPY go.mod go.sum ./
COPY main.go ./
RUN go build -o bench_server main.go
FROM scratch
COPY --from=builder /src/bench_server /app/run
USER 1001:1001
CMD ["/app/run"]

3
cmd/bench_server/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module github.com/yusing/godoxy/cmd/bench_server
go 1.25.5

0
cmd/bench_server/go.sum Normal file
View File

34
cmd/bench_server/main.go Normal file
View File

@@ -0,0 +1,34 @@
package main
import (
"log"
"net/http"
"math/rand/v2"
)
var printables = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
var random = make([]byte, 4096)
func init() {
for i := range random {
random[i] = printables[rand.IntN(len(printables))]
}
}
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write(random)
})
server := &http.Server{
Addr: ":80",
Handler: handler,
}
log.Println("Bench server listening on :80")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("ListenAndServe: %v", err)
}
}

257
cmd/debug_page.go Normal file
View File

@@ -0,0 +1,257 @@
//go:build !production
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
"github.com/yusing/godoxy/internal/api"
apiV1 "github.com/yusing/godoxy/internal/api/v1"
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
certApi "github.com/yusing/godoxy/internal/api/v1/cert"
dockerApi "github.com/yusing/godoxy/internal/api/v1/docker"
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/idlewatcher"
idlewatcherTypes "github.com/yusing/godoxy/internal/idlewatcher/types"
)
type debugMux struct {
endpoints []debugEndpoint
mux http.ServeMux
}
type debugEndpoint struct {
name string
method string
path string
}
func newDebugMux() *debugMux {
return &debugMux{
endpoints: make([]debugEndpoint, 0),
mux: *http.NewServeMux(),
}
}
func (mux *debugMux) registerEndpoint(name, method, path string) {
mux.endpoints = append(mux.endpoints, debugEndpoint{name: name, method: method, path: path})
}
func (mux *debugMux) HandleFunc(name, method, path string, handler http.HandlerFunc) {
mux.registerEndpoint(name, method, path)
mux.mux.HandleFunc(method+" "+path, handler)
}
func (mux *debugMux) Finalize() {
mux.mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, `
<!DOCTYPE html>
<html>
<head>
<style>
body {
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji;
font-size: 16px;
line-height: 1.5;
color: #f8f9fa;
background-color: #121212;
margin: 0;
padding: 0;
}
table {
border-collapse: collapse;
width: 100%;
margin-top: 20px;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #333;
}
th {
background-color: #1e1e1e;
font-weight: 600;
color: #f8f9fa;
}
td {
color: #e9ecef;
}
.link {
color: #007bff;
text-decoration: none;
}
.link:hover {
text-decoration: underline;
}
.method {
color: #6c757d;
font-family: monospace;
}
.path {
color: #6c757d;
font-family: monospace;
}
</style>
</head>
<body>
<table>
<thead>
<tr>
<th>Name</th>
<th>Method</th>
<th>Path</th>
</tr>
</thead>
<tbody>`)
for _, endpoint := range mux.endpoints {
fmt.Fprintf(w, "<tr><td><a class='link' href=%q>%s</a></td><td class='method'>%s</td><td class='path'>%s</td></tr>", endpoint.path, endpoint.name, endpoint.method, endpoint.path)
}
fmt.Fprintln(w, `
</tbody>
</table>
</body>
</html>`)
})
}
func listenDebugServer() {
mux := newDebugMux()
mux.mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/svg+xml")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><text x="50" y="50" text-anchor="middle" dominant-baseline="middle">🐙</text></svg>`))
})
mux.HandleFunc("Auth block page", "GET", "/auth/block", AuthBlockPageHandler)
mux.HandleFunc("Idlewatcher loading page", "GET", idlewatcherTypes.PathPrefix, idlewatcher.DebugHandler)
apiHandler := newApiHandler(mux)
mux.mux.HandleFunc("/api/v1/", apiHandler.ServeHTTP)
mux.Finalize()
go http.ListenAndServe(":7777", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Expires", "0")
mux.mux.ServeHTTP(w, r)
}))
}
func newApiHandler(debugMux *debugMux) *gin.Engine {
r := gin.New()
r.Use(api.ErrorHandler())
r.Use(api.ErrorLoggingMiddleware())
r.Use(api.NoCache())
registerGinRoute := func(router gin.IRouter, method, name string, path string, handler gin.HandlerFunc) {
if group, ok := router.(*gin.RouterGroup); ok {
debugMux.registerEndpoint(name, method, group.BasePath()+path)
} else {
debugMux.registerEndpoint(name, method, path)
}
router.Handle(method, path, handler)
}
registerGinRoute(r, "GET", "App version", "/api/v1/version", apiV1.Version)
v1 := r.Group("/api/v1")
if auth.IsEnabled() {
v1Auth := v1.Group("/auth")
{
registerGinRoute(v1Auth, "HEAD", "Auth check", "/check", authApi.Check)
registerGinRoute(v1Auth, "POST", "Auth login", "/login", authApi.Login)
registerGinRoute(v1Auth, "GET", "Auth callback", "/callback", authApi.Callback)
registerGinRoute(v1Auth, "POST", "Auth callback", "/callback", authApi.Callback)
registerGinRoute(v1Auth, "POST", "Auth logout", "/logout", authApi.Logout)
registerGinRoute(v1Auth, "GET", "Auth logout", "/logout", authApi.Logout)
}
}
{
// enable cache for favicon
registerGinRoute(v1, "GET", "Route favicon", "/favicon", apiV1.FavIcon)
registerGinRoute(v1, "GET", "Route health", "/health", apiV1.Health)
registerGinRoute(v1, "GET", "List icons", "/icons", apiV1.Icons)
registerGinRoute(v1, "POST", "Config reload", "/reload", apiV1.Reload)
registerGinRoute(v1, "GET", "Route stats", "/stats", apiV1.Stats)
route := v1.Group("/route")
{
registerGinRoute(route, "GET", "List routes", "/list", routeApi.Routes)
registerGinRoute(route, "GET", "Get route", "/:which", routeApi.Route)
registerGinRoute(route, "GET", "List providers", "/providers", routeApi.Providers)
registerGinRoute(route, "GET", "List routes by provider", "/by_provider", routeApi.ByProvider)
registerGinRoute(route, "POST", "Playground", "/playground", routeApi.Playground)
}
file := v1.Group("/file")
{
registerGinRoute(file, "GET", "List files", "/list", fileApi.List)
registerGinRoute(file, "GET", "Get file", "/content", fileApi.Get)
registerGinRoute(file, "PUT", "Set file", "/content", fileApi.Set)
registerGinRoute(file, "POST", "Set file", "/content", fileApi.Set)
registerGinRoute(file, "POST", "Validate file", "/validate", fileApi.Validate)
}
homepage := v1.Group("/homepage")
{
registerGinRoute(homepage, "GET", "List categories", "/categories", homepageApi.Categories)
registerGinRoute(homepage, "GET", "List items", "/items", homepageApi.Items)
registerGinRoute(homepage, "POST", "Set item", "/set/item", homepageApi.SetItem)
registerGinRoute(homepage, "POST", "Set items batch", "/set/items_batch", homepageApi.SetItemsBatch)
registerGinRoute(homepage, "POST", "Set item visible", "/set/item_visible", homepageApi.SetItemVisible)
registerGinRoute(homepage, "POST", "Set item favorite", "/set/item_favorite", homepageApi.SetItemFavorite)
registerGinRoute(homepage, "POST", "Set item sort order", "/set/item_sort_order", homepageApi.SetItemSortOrder)
registerGinRoute(homepage, "POST", "Set item all sort order", "/set/item_all_sort_order", homepageApi.SetItemAllSortOrder)
registerGinRoute(homepage, "POST", "Set item fav sort order", "/set/item_fav_sort_order", homepageApi.SetItemFavSortOrder)
registerGinRoute(homepage, "POST", "Set category order", "/set/category_order", homepageApi.SetCategoryOrder)
registerGinRoute(homepage, "POST", "Item click", "/item_click", homepageApi.ItemClick)
}
cert := v1.Group("/cert")
{
registerGinRoute(cert, "GET", "Get cert info", "/info", certApi.Info)
registerGinRoute(cert, "GET", "Renew cert", "/renew", certApi.Renew)
}
agent := v1.Group("/agent")
{
registerGinRoute(agent, "GET", "List agents", "/list", agentApi.List)
registerGinRoute(agent, "POST", "Create agent", "/create", agentApi.Create)
registerGinRoute(agent, "POST", "Verify agent", "/verify", agentApi.Verify)
}
metrics := v1.Group("/metrics")
{
registerGinRoute(metrics, "GET", "Get system info", "/system_info", metricsApi.SystemInfo)
registerGinRoute(metrics, "GET", "Get all system info", "/all_system_info", metricsApi.AllSystemInfo)
registerGinRoute(metrics, "GET", "Get uptime", "/uptime", metricsApi.Uptime)
}
docker := v1.Group("/docker")
{
registerGinRoute(docker, "GET", "Get container", "/container/:id", dockerApi.GetContainer)
registerGinRoute(docker, "GET", "List containers", "/containers", dockerApi.Containers)
registerGinRoute(docker, "GET", "Get docker info", "/info", dockerApi.Info)
registerGinRoute(docker, "GET", "Get docker logs", "/logs/:id", dockerApi.Logs)
registerGinRoute(docker, "POST", "Start docker container", "/start", dockerApi.Start)
registerGinRoute(docker, "POST", "Stop docker container", "/stop", dockerApi.Stop)
registerGinRoute(docker, "POST", "Restart docker container", "/restart", dockerApi.Restart)
}
}
return r
}
func AuthBlockPageHandler(w http.ResponseWriter, r *http.Request) {
auth.WriteBlockPage(w, http.StatusForbidden, "Forbidden", "Login", "/login")
}

7
cmd/debug_page_prod.go Normal file
View File

@@ -0,0 +1,7 @@
//go:build production
package main
func listenDebugServer() {
// no-op
}

View File

@@ -0,0 +1,18 @@
FROM golang:1.25.5-alpine AS builder
HEALTHCHECK NONE
WORKDIR /src
COPY go.mod go.sum ./
COPY main.go ./
RUN go build -o h2c_test_server main.go
FROM scratch
COPY --from=builder /src/h2c_test_server /app/run
USER 1001:1001
CMD ["/app/run"]

View File

@@ -0,0 +1,7 @@
module github.com/yusing/godoxy/cmd/h2c_test_server
go 1.25.5
require golang.org/x/net v0.48.0
require golang.org/x/text v0.32.0 // indirect

View File

@@ -0,0 +1,4 @@
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=

View File

@@ -0,0 +1,26 @@
package main
import (
"log"
"net/http"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
)
func main() {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
server := &http.Server{
Addr: ":80",
Handler: h2c.NewHandler(handler, &http2.Server{}),
}
log.Println("H2C server listening on :80")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("ListenAndServe: %v", err)
}
}

View File

@@ -5,19 +5,22 @@ import (
"sync"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/auth"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/config"
"github.com/yusing/go-proxy/internal/dnsproviders"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/homepage"
"github.com/yusing/go-proxy/internal/logging"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/metrics/systeminfo"
"github.com/yusing/go-proxy/internal/metrics/uptime"
"github.com/yusing/go-proxy/internal/net/gphttp/middleware"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/pkg"
"github.com/yusing/godoxy/internal/api"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/config"
"github.com/yusing/godoxy/internal/dnsproviders"
iconlist "github.com/yusing/godoxy/internal/homepage/icons/list"
"github.com/yusing/godoxy/internal/logging"
"github.com/yusing/godoxy/internal/logging/memlogger"
"github.com/yusing/godoxy/internal/metrics/systeminfo"
"github.com/yusing/godoxy/internal/metrics/uptime"
"github.com/yusing/godoxy/internal/net/gphttp/middleware"
"github.com/yusing/godoxy/internal/route/rules"
gperr "github.com/yusing/goutils/errs"
"github.com/yusing/goutils/server"
"github.com/yusing/goutils/task"
"github.com/yusing/goutils/version"
)
func parallel(fns ...func()) {
@@ -32,11 +35,11 @@ func main() {
initProfiling()
logging.InitLogger(os.Stderr, memlogger.GetMemLogger())
log.Info().Msgf("GoDoxy version %s", pkg.GetVersion())
log.Info().Msgf("GoDoxy version %s", version.Get())
log.Trace().Msg("trace enabled")
parallel(
dnsproviders.InitProviders,
homepage.InitIconListCache,
iconlist.InitCache,
systeminfo.Poller.Start,
middleware.LoadComposeFiles,
)
@@ -50,26 +53,31 @@ func main() {
prepareDirectory(dir)
}
cfg, err := config.Load()
err := config.Load()
if err != nil {
gperr.LogWarn("errors in config", err)
}
cfg.Start(&config.StartServersOptions{
Proxy: true,
})
config.StartProxyServers()
if err := auth.Initialize(); err != nil {
log.Fatal().Err(err).Msg("failed to initialize authentication")
}
rules.InitAuthHandler(auth.AuthOrProceed)
// API Handler needs to start after auth is initialized.
cfg.StartServers(&config.StartServersOptions{
API: true,
server.StartServer(task.RootTask("api_server", false), server.Options{
Name: "api",
HTTPAddr: common.APIHTTPAddr,
Handler: api.NewHandler(),
})
listenDebugServer()
uptime.Poller.Start()
config.WatchChanges()
task.WaitExit(cfg.Value().TimeoutShutdown)
task.WaitExit(config.Value().TimeoutShutdown)
}
func prepareDirectory(dir string) {

View File

@@ -10,16 +10,10 @@ import (
"time"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/utils/strutils"
strutils "github.com/yusing/goutils/strings"
)
const mb = 1024 * 1024
func initProfiling() {
debug.SetGCPercent(-1)
debug.SetMemoryLimit(50 * mb)
debug.SetMaxStack(4 * mb)
go func() {
log.Info().Msgf("pprof server started at http://localhost:7777/debug/pprof/")
log.Error().Err(http.ListenAndServe(":7777", nil)).Msg("pprof server failed")
@@ -27,9 +21,14 @@ func initProfiling() {
go func() {
ticker := time.NewTicker(time.Second * 10)
defer ticker.Stop()
var m runtime.MemStats
var gcStats debug.GCStats
for range ticker.C {
var m runtime.MemStats
runtime.ReadMemStats(&m)
debug.ReadGCStats(&gcStats)
log.Info().Msgf("-----------------------------------------------------")
log.Info().Msgf("Timestamp: %s", time.Now().Format(time.RFC3339))
log.Info().Msgf(" Go Heap - In Use (Alloc/HeapAlloc): %s", strutils.FormatByteSize(m.Alloc))
@@ -37,8 +36,12 @@ func initProfiling() {
log.Info().Msgf(" Go Stacks - In Use (StackInuse): %s", strutils.FormatByteSize(m.StackInuse))
log.Info().Msgf(" Go Runtime - Other Sys (MSpanInuse, MCacheInuse, BuckHashSys, GCSys, OtherSys): %s", strutils.FormatByteSize(m.MSpanInuse+m.MCacheInuse+m.BuckHashSys+m.GCSys+m.OtherSys))
log.Info().Msgf(" Go Runtime - Total from OS (Sys): %s", strutils.FormatByteSize(m.Sys))
log.Info().Msgf(" Go Runtime - Freed from OS (HeapReleased): %s", strutils.FormatByteSize(m.HeapReleased))
log.Info().Msgf(" Number of Goroutines: %d", runtime.NumGoroutine())
log.Info().Msgf(" Number of GCs: %d", m.NumGC)
log.Info().Msgf(" Number of completed GC cycles: %d", m.NumGC)
log.Info().Msgf(" Number of GCs: %d", gcStats.NumGC)
log.Info().Msgf(" Total GC time: %s", gcStats.PauseTotal)
log.Info().Msgf(" Last GC time: %s", gcStats.LastGC.Format(time.DateTime))
log.Info().Msg("-----------------------------------------------------")
}
}()

View File

@@ -22,26 +22,28 @@ services:
- ${SOCKET_PROXY_LISTEN_ADDR:-127.0.0.1:2375}:2375
frontend:
image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}
# lite variant
# image: ghcr.io/yusing/godoxy-frontend:${TAG:-latest}-lite
container_name: godoxy-frontend
restart: unless-stopped
network_mode: host # do not change this
env_file: .env
# comment out `user` for lite variant
user: ${GODOXY_UID:-1000}:${GODOXY_GID:-1000}
read_only: true
tmpfs:
- /app/.next/cache # next image caching
# for lite variant, do not change uid/gid
# - /var/cache/nginx:uid=101,gid=101
# - /run:uid=101,gid=101
security_opt:
- no-new-privileges:true
cap_drop:
- all
depends_on:
- app
environment:
HOSTNAME: 127.0.0.1
PORT: ${GODOXY_FRONTEND_PORT:-3000}
labels:
proxy.aliases: ${GODOXY_FRONTEND_ALIASES:-godoxy}
proxy.#1.port: ${GODOXY_FRONTEND_PORT:-3000}
# proxy.#1.middlewares.cidr_whitelist: |
# status: 403
# message: IP not allowed
@@ -74,10 +76,9 @@ services:
- ./error_pages:/app/error_pages:ro
- ./data:/app/data
# To use autocert, certs will be stored in "./certs".
# You can also use a docker volume to store it
# This path stores certs obtained from autocert and agent TLS client certs
- ./certs:/app/certs
# remove "./certs:/app/certs" and uncomment below to use existing certificate
# mount existing certificate
# - /path/to/certs/cert.crt:/app/certs/cert.crt
# - /path/to/certs/priv.key:/app/certs/priv.key

View File

@@ -4,6 +4,8 @@
# autocert:
# provider: local
# cert_path: /path/to/cert.crt # default: /app/certs/cert.crt
# key_path: /path/to/priv.key # default: /app/certs/priv.key
# 2. cloudflare
# autocert:
@@ -17,6 +19,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)
@@ -31,12 +37,21 @@
# - country:US
# - timezone:Asia/Shanghai
# log: # warning: logging ACL can be slow based on the number of incoming connections and configured rules
# buffer_size: 65536 # (default: 64KB)
# path: /app/logs/acl.log # (default: none)
# stdout: false # (default: false)
# keep: last 10 # (default: none)
# keep: 30 days # (default: 30 days)
# log_allowed: false # (default: false)
# notify:
# interval: 1m # (default: 1m)
# to: [gotify, discord] # names under providers.notification
# include_allowed: false # (default: false)
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 routes will 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,20 +72,27 @@ 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
access_log:
format: combined
path: /app/logs/entrypoint.log
stdout: false # (default: false)
keep: 30 days # (default: 30 days)
# customize behavior for non-existent routes, e.g. pass over to another proxy
#
# rules:
# not_found:
# - name: default
# do: proxy http://other-proxy:8080
defaults:
healthcheck:
interval: 5s
timeout: 15s
retries: 3
providers:
# include files are standalone yaml files under `config/` directory
@@ -95,9 +117,13 @@ providers:
# remote-1: tcp://10.0.2.1:2375
# remote-2: ssh://root:1234@10.0.2.2
# notification providers (notify when service health changes)
# notification providers
#
# notification:
# - name: ntfy
# provider: ntfy
# url: https://ntfy.domain.tld
# topic: godoxy
# - name: gotify
# provider: gotify
# url: https://gotify.domain.tld
@@ -106,6 +132,11 @@ providers:
# provider: webhook
# url: https://discord.com/api/webhooks/...
# template: discord # this means use payload template from internal/notif/templates/discord.json
# - name: pushover
# provider: webhook
# url: https://api.pushover.net/1/messages.json
# mime_type: application/x-www-form-urlencoded
# payload: '{"token": "your-app-token", "user": "your-user-key", "title": $title, "message": $message}'
# Proxmox providers (for idlesleep support for proxmox LXCs)
#
@@ -115,8 +146,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,33 +1,7 @@
# Stage 1: deps
FROM golang:1.25.0-alpine AS deps
HEALTHCHECK NONE
FROM debian:bookworm-slim
# package version does not matter
# trunk-ignore(hadolint/DL3018)
RUN apk add --no-cache tzdata make libcap-setcap
# Stage 3: Final image
FROM alpine:3.22
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}
ENV DOCKER_HOST=unix:///var/run/docker.sock
# copy binary
COPY bin/${TARGET} /app/run
RUN apt-get update && apt-get install -y ca-certificates
WORKDIR /app
RUN chown -R 1000:1000 /app
CMD ["/app/run"]
CMD ["/app/run"]

View File

@@ -1,15 +1,19 @@
x-benchmark: &benchmark
restart: no
labels:
proxy.exclude: true
proxy.#1.healthcheck.disable: true
services:
app:
image: godoxy-dev
build:
context: .
dockerfile: dev.Dockerfile
args:
- TARGET=godoxy
container_name: godoxy-proxy-dev
restart: unless-stopped
env_file: dev.env
environment:
DOCKER_HOST: unix:///var/run/docker.sock
TZ: Asia/Hong_Kong
API_ADDR: 127.0.0.1:8999
API_USER: dev
@@ -17,13 +21,14 @@ services:
API_SKIP_ORIGIN_CHECK: true
API_JWT_TTL: 24h
DEBUG: true
API_SECRET: 1234567891234567
API_JWT_SECRET: 1234567891234567
labels:
proxy.exclude: true
proxy.#1.healthcheck.disable: true
ipc: host
network_mode: host
volumes:
- ./bin/godoxy:/app/run:ro
- /var/run/docker.sock:/var/run/docker.sock
- ./dev-data/config:/app/config
- ./dev-data/certs:/app/certs
@@ -31,6 +36,19 @@ services:
- ./dev-data/data:/app/data
- ./dev-data/logs:/app/logs
- ~/certs/myCA.pem:/etc/ssl/certs/ca.crt:ro
parca:
image: ghcr.io/parca-dev/parca:v0.24.2
container_name: godoxy-parca
restart: unless-stopped
command: [/parca, --config-path, /parca.yaml]
network_mode: host
# ports:
# - 7070:7070
configs:
- source: parca
target: /parca.yaml
labels:
proxy.#1.port: "7070"
tinyauth:
image: ghcr.io/steveiliop56/tinyauth:v3
container_name: tinyauth
@@ -41,3 +59,199 @@ services:
- USERS=user:$$2a$$10$$UdLYoJ5lgPsC0RKqYH/jMua7zIn0g9kPqWmhYayJYLaZQ/FTmH2/u # user:password
labels:
proxy.tinyauth.port: "3000"
jotty: # issue #182
image: ghcr.io/fccview/jotty:latest
container_name: jotty
user: "1000:1000"
tmpfs:
- /app/data:rw,uid=1000,gid=1000
- /app/config:rw,uid=1000,gid=1000
- /app/.next/cache:rw,uid=1000,gid=1000
restart: unless-stopped
environment:
- NODE_ENV=production
labels:
proxy.aliases: "jotty.my.app"
postgres-test:
image: postgres:18-alpine
container_name: postgres-test
restart: unless-stopped
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
h2c_test_server:
build:
context: cmd/h2c_test_server
dockerfile: Dockerfile
container_name: h2c_test
restart: unless-stopped
labels:
proxy.#1.scheme: h2c
proxy.#1.port: 80
bench: # returns 4096 bytes of random data
<<: *benchmark
build:
context: cmd/bench_server
dockerfile: Dockerfile
container_name: bench
godoxy:
<<: *benchmark
build: .
container_name: godoxy-benchmark
ports:
- 8080:80
configs:
- source: godoxy_config
target: /app/config/config.yml
- source: godoxy_provider
target: /app/config/providers.yml
traefik:
<<: *benchmark
image: traefik:latest
container_name: traefik
command:
- --api.insecure=true
- --entrypoints.web.address=:8081
- --providers.file.directory=/etc/traefik/dynamic
- --providers.file.watch=true
- --log.level=ERROR
ports:
- 8081:8081
configs:
- source: traefik_config
target: /etc/traefik/dynamic/routes.yml
caddy:
<<: *benchmark
image: caddy:latest
container_name: caddy
ports:
- 8082:80
configs:
- source: caddy_config
target: /etc/caddy/Caddyfile
tmpfs:
- /data
- /config
nginx:
<<: *benchmark
image: nginx:latest
container_name: nginx
command: nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
ports:
- 8083:80
configs:
- source: nginx_config
target: /etc/nginx/nginx.conf
configs:
godoxy_config:
content: |
providers:
include:
- providers.yml
godoxy_provider:
content: |
bench.domain.com:
host: bench
traefik_config:
content: |
http:
routers:
bench:
rule: "Host(`bench.domain.com`)"
entryPoints:
- web
service: bench
services:
bench:
loadBalancer:
servers:
- url: "http://bench:80"
caddy_config:
content: |
{
admin off
auto_https off
default_bind 0.0.0.0
servers {
protocols h1 h2c
}
}
http://bench.domain.com {
reverse_proxy bench:80
}
nginx_config:
content: |
worker_processes auto;
worker_rlimit_nofile 65535;
error_log /dev/null;
pid /var/run/nginx.pid;
events {
worker_connections 10240;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 10000;
upstream backend {
server bench:80;
keepalive 128;
}
server {
listen 80 default_server;
server_name _;
http2 on;
return 404;
}
server {
listen 80;
server_name bench.domain.com;
http2 on;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $$host;
proxy_set_header X-Real-IP $$remote_addr;
proxy_set_header X-Forwarded-For $$proxy_add_x_forwarded_for;
proxy_buffering off;
}
}
}
parca:
content: |
object_storage:
bucket:
type: "FILESYSTEM"
config:
directory: "./data"
scrape_configs:
- job_name: "parca"
scrape_interval: "1s"
static_configs:
- targets: [ 'localhost:7777' ]

278
go.mod
View File

@@ -1,254 +1,190 @@
module github.com/yusing/go-proxy
module github.com/yusing/godoxy
go 1.25.1
go 1.25.5
replace github.com/yusing/go-proxy/agent => ./agent
replace github.com/yusing/go-proxy/internal/dnsproviders => ./internal/dnsproviders
replace github.com/yusing/go-proxy/internal/utils => ./internal/utils
replace github.com/coreos/go-oidc/v3 => github.com/godoxy-app/go-oidc/v3 v3.0.0-20250816044348-0630187cb14b
replace github.com/shirou/gopsutil/v4 => github.com/godoxy-app/gopsutil/v4 v4.0.0-20250816043325-ee003f88b84d
replace (
github.com/coreos/go-oidc/v3 => ./internal/go-oidc
github.com/shirou/gopsutil/v4 => ./internal/gopsutil
github.com/yusing/godoxy/agent => ./agent
github.com/yusing/godoxy/internal/dnsproviders => ./internal/dnsproviders
github.com/yusing/goutils => ./goutils
github.com/yusing/goutils/http/reverseproxy => ./goutils/http/reverseproxy
github.com/yusing/goutils/http/websocket => ./goutils/http/websocket
github.com/yusing/goutils/server => ./goutils/server
)
require (
github.com/PuerkitoBio/goquery v1.10.3 // parsing HTML for extract fav icon
github.com/coreos/go-oidc/v3 v3.15.0 // oidc authentication
github.com/docker/docker v28.4.0+incompatible // docker daemon
github.com/PuerkitoBio/goquery v1.11.0 // parsing HTML for extract fav icon; modify_html middleware
github.com/coreos/go-oidc/v3 v3.17.0 // oidc authentication
github.com/fsnotify/fsnotify v1.9.0 // file watcher
github.com/go-acme/lego/v4 v4.26.0 // acme client
github.com/go-playground/validator/v10 v10.27.0 // validator
github.com/gin-gonic/gin v1.11.0 // api server
github.com/go-acme/lego/v4 v4.31.0 // acme client
github.com/go-playground/validator/v10 v10.30.1 // validator
github.com/gobwas/glob v0.2.3 // glob matcher for route rules
github.com/gorilla/websocket v1.5.3 // websocket for API and agent
github.com/gotify/server/v2 v2.7.2 // reference the Message struct for json response
github.com/gotify/server/v2 v2.8.0 // reference the Message struct for json response
github.com/lithammer/fuzzysearch v1.1.8 // fuzzy search for searching icons and filtering metrics
github.com/puzpuzpuz/xsync/v4 v4.1.0 // lock free map for concurrent operations
github.com/pires/go-proxyproto v0.8.1 // proxy protocol support
github.com/puzpuzpuz/xsync/v4 v4.2.0 // lock free map for concurrent operations
github.com/rs/zerolog v1.34.0 // logging
github.com/shirou/gopsutil/v4 v4.25.8 // system info metrics
github.com/vincent-petithory/dataurl v1.0.0 // data url for fav icon
golang.org/x/crypto v0.42.0 // encrypting password with bcrypt
golang.org/x/net v0.44.0 // HTTP header utilities
golang.org/x/oauth2 v0.31.0 // oauth2 authentication
golang.org/x/sync v0.17.0
golang.org/x/time v0.13.0 // time utilities
golang.org/x/crypto v0.46.0 // encrypting password with bcrypt
golang.org/x/net v0.48.0 // HTTP header utilities
golang.org/x/oauth2 v0.34.0 // oauth2 authentication
golang.org/x/sync v0.19.0 // errgroup and singleflight for concurrent operations
golang.org/x/time v0.14.0 // time utilities
)
require (
github.com/docker/cli v28.4.0+incompatible
github.com/goccy/go-yaml v1.18.0 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/luthermonson/go-proxmox v0.2.3
github.com/oschwald/maxminddb-golang v1.13.1
github.com/quic-go/quic-go v0.54.0
github.com/samber/slog-zerolog/v2 v2.7.3
github.com/spf13/afero v1.15.0
github.com/stretchr/testify v1.11.1
github.com/yusing/go-proxy/agent v0.0.0-20250913143824-493c0afdface
github.com/yusing/go-proxy/internal/dnsproviders v0.0.0-20250913143824-493c0afdface
github.com/yusing/go-proxy/internal/utils v0.0.0
github.com/bytedance/gopkg v0.1.3 // xxhash64 for fast hash
github.com/bytedance/sonic v1.14.2 // fast json parsing
github.com/docker/cli v29.1.3+incompatible // needs docker/cli/cli/connhelper connection helper for docker client
github.com/goccy/go-yaml v1.19.2 // yaml parsing for different config files
github.com/golang-jwt/jwt/v5 v5.3.0 // jwt authentication
github.com/luthermonson/go-proxmox v0.3.2 // proxmox API client
github.com/moby/moby/api v1.52.0 // docker API
github.com/moby/moby/client v0.2.1 // docker client
github.com/oschwald/maxminddb-golang v1.13.1 // maxminddb for geoip database
github.com/quic-go/quic-go v0.58.0 // http3 support
github.com/shirou/gopsutil/v4 v4.25.12 // system information
github.com/spf13/afero v1.15.0 // afero for file system operations
github.com/stretchr/testify v1.11.1 // testing framework
github.com/valyala/fasthttp v1.69.0 // fast http for health check
github.com/yusing/ds v0.3.1 // data structures and algorithms
github.com/yusing/godoxy/agent v0.0.0-20260109022755-4275cdae3854
github.com/yusing/godoxy/internal/dnsproviders v0.0.0-20260109022755-4275cdae3854
github.com/yusing/gointernals v0.1.16
github.com/yusing/goutils v0.7.0
github.com/yusing/goutils/http/reverseproxy v0.0.0-20260109021609-78fda75d1e58
github.com/yusing/goutils/http/websocket v0.0.0-20260109021609-78fda75d1e58
github.com/yusing/goutils/server v0.0.0-20260109021609-78fda75d1e58
)
require (
cloud.google.com/go/auth v0.16.5 // indirect
cloud.google.com/go/auth v0.18.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.8.0 // indirect
github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.5.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/aws/aws-sdk-go-v2 v1.39.0 // indirect
github.com/aws/aws-sdk-go-v2/config v1.31.8 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.12 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.7 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.7 // indirect
github.com/aws/aws-sdk-go-v2/service/lightsail v1.48.4 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.58.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.29.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.4 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.38.4 // indirect
github.com/aws/smithy-go v1.23.0 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/diskfs/go-diskfs v1.7.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/djherbis/times v1.6.0 // indirect
github.com/docker/go-connections v0.6.0
github.com/docker/go-units v0.5.0 // indirect
github.com/ebitengine/purego v0.8.4 // indirect
github.com/exoscale/egoscale/v3 v3.1.26 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/ebitengine/purego v0.9.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
github.com/go-errors/errors v1.5.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-resty/resty/v2 v2.16.5 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.15.0 // indirect
github.com/gophercloud/gophercloud v1.14.1 // indirect
github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect
github.com/googleapis/gax-go/v2 v2.16.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
github.com/infobloxopen/infoblox-go-client/v2 v2.10.0 // indirect
github.com/jinzhu/copier v0.4.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
github.com/labbsr0x/goh v1.0.1 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/linode/linodego v1.57.0 // indirect
github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
github.com/liquidweb/liquidweb-go v1.6.4 // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/magefile/mage v1.15.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
github.com/miekg/dns v1.1.69 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/nrdcg/auroradns v1.1.0 // indirect
github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
github.com/nrdcg/desec v0.11.0 // indirect
github.com/nrdcg/freemyip v0.3.0 // indirect
github.com/nrdcg/goacmedns v0.2.0 // indirect
github.com/nrdcg/goinwx v0.11.0 // indirect
github.com/nrdcg/mailinabox v0.2.0 // indirect
github.com/nrdcg/nodion v0.1.0 // indirect
github.com/nrdcg/porkbun v0.4.0 // indirect
github.com/nzdjb/go-metaname v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/ovh/go-ovh v1.9.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/peterhellberg/link v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // 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
github.com/pquerna/otp v1.5.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/regfish/regfish-dnsapi-go v0.1.1 // indirect
github.com/sacloud/api-client-go v0.3.3 // indirect
github.com/sacloud/go-http v0.1.9 // indirect
github.com/sacloud/iaas-api-go v1.17.1 // indirect
github.com/sacloud/packages-go v0.0.11 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/samber/lo v1.51.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/samber/lo v1.52.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.34 // indirect
github.com/selectel/domains-go v1.1.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
github.com/samber/slog-zerolog/v2 v2.9.0 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.36 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
github.com/softlayer/softlayer-go v1.2.1 // indirect
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
github.com/sony/gobreaker v1.0.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/transip/gotransip/v6 v6.26.0 // indirect
github.com/ultradns/ultradns-go-sdk v1.8.1-20250722213956-faef419 // indirect
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
github.com/volcengine/volc-sdk-golang v1.0.219 // indirect
github.com/vultr/govultr/v3 v3.23.0 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/atomic v1.11.0
go.uber.org/mock v0.6.0 // indirect
go.uber.org/ratelimit v0.3.1 // indirect
golang.org/x/mod v0.28.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
google.golang.org/api v0.249.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.9 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/tools v0.40.0 // indirect
google.golang.org/api v0.259.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect
google.golang.org/grpc v1.78.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ns1/ns1-go.v2 v2.15.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
require (
github.com/gin-gonic/gin v1.10.1
github.com/yusing/ds v0.1.0
)
require (
github.com/akamai/AkamaiOPEN-edgegrid-golang/v11 v11.1.0 // indirect
github.com/aziontech/azionapi-go-sdk v0.142.0 // indirect
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.14.1 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/boombuler/barcode v1.1.0 // indirect
github.com/bytedance/sonic/loader v0.4.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/dnsimple/dnsimple-go/v4 v4.0.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect
github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/google/go-querystring v1.2.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/namedotcom/go/v4 v4.0.2 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.100.0 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.100.0 // indirect
github.com/onsi/ginkgo/v2 v2.25.1 // indirect
github.com/onsi/gomega v1.38.1 // indirect
github.com/kolo/xmlrpc v0.0.0-20220921171641-a4b6fa1dd06b // indirect
github.com/linode/linodego v1.64.0 // indirect
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
github.com/nrdcg/goinwx v0.12.0 // indirect
github.com/nrdcg/oci-go-sdk/common/v1065 v1065.105.2 // indirect
github.com/nrdcg/oci-go-sdk/dns/v1065 v1065.105.2 // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/selectel/go-selvpcclient/v4 v4.1.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/pion/dtls/v3 v3.0.10 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/pquerna/otp v1.5.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.37.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.21.0 // indirect
google.golang.org/genproto v0.0.0-20250908214217-97024824d090 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250826171959-ef028d996bc1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/ulikunitz/xz v0.5.15 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vultr/govultr/v3 v3.26.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.23.0 // indirect
)

2417
go.sum

File diff suppressed because it is too large Load Diff

1
goutils Submodule

Submodule goutils added at 326c1f1eb3

282
internal/acl/README.md Normal file
View File

@@ -0,0 +1,282 @@
# ACL (Access Control List)
Access control at the TCP connection level with IP/CIDR, timezone, and country-based filtering.
## Overview
The ACL package provides network-level access control by wrapping TCP listeners and validating incoming connections against configurable allow/deny rules. It integrates with MaxMind GeoIP for geographic-based filtering and supports access logging with notification batching.
### Primary consumers
- `internal/entrypoint` - Wraps the main TCP listener for connection filtering
- Operators - Configure rules via YAML configuration
### Non-goals
- HTTP request-level filtering (handled by middleware)
- Authentication or authorization (see `internal/auth`)
- VPN or tunnel integration
### Stability
Stable internal package. The public API is the `Config` struct and its methods.
## Public API
### Exported types
```go
type Config struct {
Default string // "allow" or "deny" (default: "allow")
AllowLocal *bool // Allow private/loopback IPs (default: true)
Allow Matchers // Allow rules
Deny Matchers // Deny rules
Log *accesslog.ACLLoggerConfig // Access logging configuration
Notify struct {
To []string // Notification providers
Interval time.Duration // Notification frequency (default: 1m)
IncludeAllowed *bool // Include allowed in notifications (default: false)
}
}
```
```go
type Matcher struct {
match MatcherFunc
}
```
```go
type Matchers []Matcher
```
### Exported functions and methods
```go
func (c *Config) Validate() gperr.Error
```
Validates configuration and sets defaults. Must be called before `Start`.
```go
func (c *Config) Start(parent task.Parent) gperr.Error
```
Initializes the ACL, starts the logger and notification goroutines.
```go
func (c *Config) IPAllowed(ip net.IP) bool
```
Returns true if the IP is allowed based on configured rules. Performs caching and GeoIP lookup if needed.
```go
func (c *Config) WrapTCP(lis net.Listener) net.Listener
```
Wraps a `net.Listener` to filter connections by IP.
```go
func (matcher *Matcher) Parse(s string) error
```
Parses a matcher string in the format `{type}:{value}`. Supported types: `ip`, `cidr`, `tz`, `country`.
## Architecture
### Core components
```mermaid
graph TD
A[TCP Listener] --> B[TCPListener Wrapper]
B --> C{IP Allowed?}
C -->|Yes| D[Accept Connection]
C -->|No| E[Close Connection]
F[Config] --> G[Validate]
G --> H[Start]
H --> I[Matcher Evaluation]
I --> C
J[MaxMind] -.-> K[IP Lookup]
K -.-> I
L[Access Logger] -.-> M[Log & Notify]
M -.-> B
```
### Connection filtering flow
```mermaid
sequenceDiagram
participant Client
participant TCPListener
participant Config
participant MaxMind
participant Logger
Client->>TCPListener: Connection Request
TCPListener->>Config: IPAllowed(clientIP)
alt Loopback IP
Config-->>TCPListener: true
else Private IP (allow_local)
Config-->>TCPListener: true
else Cached Result
Config-->>TCPListener: Cached Result
else Evaluate Allow Rules
Config->>Config: Check Allow list
alt Matches
Config->>Config: Cache true
Config-->>TCPListener: Allowed
else Evaluate Deny Rules
Config->>Config: Check Deny list
alt Matches
Config->>Config: Cache false
Config-->>TCPListener: Denied
else Default Action
Config->>MaxMind: Lookup GeoIP
MaxMind-->>Config: IPInfo
Config->>Config: Apply default rule
Config->>Config: Cache result
Config-->>TCPListener: Result
end
end
end
alt Logging enabled
Config->>Logger: Log access attempt
end
```
### Matcher types
| Type | Format | Example |
| -------- | ----------------- | --------------------- |
| IP | `ip:address` | `ip:192.168.1.1` |
| CIDR | `cidr:network` | `cidr:192.168.0.0/16` |
| TimeZone | `tz:timezone` | `tz:Asia/Shanghai` |
| Country | `country:ISOCode` | `country:GB` |
## Configuration Surface
### Config sources
Configuration is loaded from `config/config.yml` under the `acl` key.
### Schema
```yaml
acl:
default: "allow" # "allow" or "deny"
allow_local: true # Allow private/loopback IPs
log:
log_allowed: false # Log allowed connections
notify:
to: ["gotify"] # Notification providers
interval: "1m" # Notification interval
include_allowed: false # Include allowed in notifications
```
### Hot-reloading
Configuration requires restart. The ACL does not support dynamic rule updates.
## Dependency and Integration Map
### Internal dependencies
- `internal/maxmind` - IP geolocation lookup
- `internal/logging/accesslog` - Access logging
- `internal/notif` - Notifications
- `internal/task/task.go` - Lifetime management
### Integration points
```go
// Entrypoint uses ACL to wrap the TCP listener
aclListener := config.ACL.WrapTCP(listener)
http.Server.Serve(aclListener, entrypoint)
```
## Observability
### Logs
- `ACL started` - Configuration summary on start
- `log_notify_loop` - Access attempts (allowed/denied)
Log levels: `Info` for startup, `Debug` for client closure.
### Metrics
No metrics are currently exposed.
## Security Considerations
- Loopback and private IPs are always allowed unless explicitly denied
- Cache TTL is 1 minute to limit memory usage
- Notification channel has a buffer of 100 to prevent blocking
- Failed connections are immediately closed without response
## Failure Modes and Recovery
| Failure | Behavior | Recovery |
| --------------------------------- | ------------------------------------- | --------------------------------------------- |
| Invalid matcher syntax | Validation fails on startup | Fix configuration syntax |
| MaxMind database unavailable | GeoIP lookups return unknown location | Default action applies; cache hit still works |
| Notification provider unavailable | Notification dropped | Error logged, continues operation |
| Cache full | No eviction, uses Go map | No action needed |
## Usage Examples
### Basic configuration
```go
aclConfig := &acl.Config{
Default: "allow",
AllowLocal: ptr(true),
Allow: acl.Matchers{
{match: matchIP(net.ParseIP("192.168.1.0/24"))},
},
Deny: acl.Matchers{
{match: matchISOCode("CN")},
},
}
if err := aclConfig.Validate(); err != nil {
log.Fatal(err)
}
if err := aclConfig.Start(parent); err != nil {
log.Fatal(err)
}
```
### Wrapping a TCP listener
```go
listener, err := net.Listen("tcp", ":443")
if err != nil {
log.Fatal(err)
}
// Wrap with ACL
aclListener := aclConfig.WrapTCP(listener)
// Use with HTTP server
server := &http.Server{}
server.Serve(aclListener)
```
### Creating custom matchers
```go
matcher := &acl.Matcher{}
err := matcher.Parse("country:US")
if err != nil {
log.Fatal(err)
}
// Use the matcher
allowed := matcher.match(ipInfo)
```

View File

@@ -1,17 +1,22 @@
package acl
import (
"fmt"
"math"
"net"
"sync/atomic"
"time"
"github.com/puzpuzpuz/xsync/v4"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging/accesslog"
"github.com/yusing/go-proxy/internal/maxmind"
"github.com/yusing/go-proxy/internal/task"
"github.com/yusing/go-proxy/internal/utils"
"github.com/yusing/godoxy/internal/common"
"github.com/yusing/godoxy/internal/logging/accesslog"
"github.com/yusing/godoxy/internal/maxmind"
"github.com/yusing/godoxy/internal/notif"
gperr "github.com/yusing/goutils/errs"
strutils "github.com/yusing/goutils/strings"
"github.com/yusing/goutils/task"
)
type Config struct {
@@ -21,16 +26,42 @@ type Config struct {
Deny Matchers `json:"deny"`
Log *accesslog.ACLLoggerConfig `json:"log"`
Notify struct {
To []string `json:"to"` // list of notification providers
Interval time.Duration `json:"interval"` // interval between notifications
IncludeAllowed *bool `json:"include_allowed"` // default: false
} `json:"notify"`
config
valErr gperr.Error
}
const defaultNotifyInterval = 1 * time.Minute
type config struct {
defaultAllow bool
allowLocal bool
ipCache *xsync.Map[string, *checkCache]
logAllowed bool
logger *accesslog.AccessLogger
// will be nil if Notify.To is empty
// these are per IP, reset every Notify.Interval
allowedCount map[string]uint32
blockedCount map[string]uint32
// these are total, never reset
totalAllowedCount uint64
totalBlockedCount uint64
logAllowed bool
// will be nil if Log is nil
logger accesslog.AccessLogger
// will never tick if Notify.To is empty
notifyTicker *time.Ticker
notifyAllowed bool
// will be nil if both Log and Notify.To are empty
logNotifyCh chan ipLog
}
type checkCache struct {
@@ -39,10 +70,18 @@ type checkCache struct {
created time.Time
}
type ipLog struct {
info *maxmind.IPInfo
allowed bool
}
// could be nil
var ActiveConfig atomic.Pointer[Config]
const cacheTTL = 1 * time.Minute
func (c *checkCache) Expired() bool {
return c.created.Add(cacheTTL).Before(utils.TimeNow())
return c.created.Add(cacheTTL).Before(time.Now())
}
// TODO: add stats
@@ -69,6 +108,10 @@ func (c *Config) Validate() gperr.Error {
c.allowLocal = true
}
if c.Notify.Interval < 0 {
c.Notify.Interval = defaultNotifyInterval
}
if c.Log != nil {
c.logAllowed = c.Log.LogAllowed
}
@@ -79,6 +122,12 @@ func (c *Config) Validate() gperr.Error {
}
c.ipCache = xsync.NewMap[string, *checkCache]()
if c.Notify.IncludeAllowed != nil {
c.notifyAllowed = *c.Notify.IncludeAllowed
} else {
c.notifyAllowed = false
}
return nil
}
@@ -86,7 +135,7 @@ func (c *Config) Valid() bool {
return c != nil && c.valErr == nil
}
func (c *Config) Start(parent *task.Task) gperr.Error {
func (c *Config) Start(parent task.Parent) gperr.Error {
if c.Log != nil {
logger, err := accesslog.NewAccessLogger(parent, c.Log)
if err != nil {
@@ -97,6 +146,23 @@ func (c *Config) Start(parent *task.Task) gperr.Error {
if c.valErr != nil {
return c.valErr
}
if c.needLogOrNotify() {
c.logNotifyCh = make(chan ipLog, 100)
}
if c.needNotify() {
c.allowedCount = make(map[string]uint32)
c.blockedCount = make(map[string]uint32)
c.notifyTicker = time.NewTicker(c.Notify.Interval)
} else {
c.notifyTicker = time.NewTicker(time.Duration(math.MaxInt64)) // never tick
}
if c.needLogOrNotify() {
go c.logNotifyLoop(parent)
}
log.Info().
Str("default", c.Default).
Bool("allow_local", c.allowLocal).
@@ -113,16 +179,93 @@ func (c *Config) cacheRecord(info *maxmind.IPInfo, allow bool) {
c.ipCache.Store(info.Str, &checkCache{
IPInfo: info,
allow: allow,
created: utils.TimeNow(),
created: time.Now(),
})
}
func (c *config) log(info *maxmind.IPInfo, allowed bool) {
if c.logger == nil {
return
func (c *Config) needLogOrNotify() bool {
return c.needLog() || c.needNotify()
}
func (c *Config) needLog() bool {
return c.logger != nil
}
func (c *Config) needNotify() bool {
return len(c.Notify.To) > 0
}
func (c *Config) getCachedCity(ip string) string {
record, ok := c.ipCache.Load(ip)
if ok {
if record.City != nil {
if record.City.Country.IsoCode != "" {
return record.City.Country.IsoCode
}
return record.City.Location.TimeZone
}
}
if !allowed || c.logAllowed {
c.logger.LogACL(info, !allowed)
return "unknown location"
}
func (c *Config) logNotifyLoop(parent task.Parent) {
defer c.notifyTicker.Stop()
for {
select {
case <-parent.Context().Done():
return
case log := <-c.logNotifyCh:
if c.logger != nil {
if !log.allowed || c.logAllowed {
c.logger.LogACL(log.info, !log.allowed)
}
}
if c.needNotify() {
if log.allowed {
if c.notifyAllowed {
c.allowedCount[log.info.Str]++
c.totalAllowedCount++
}
} else {
c.blockedCount[log.info.Str]++
c.totalBlockedCount++
}
}
case <-c.notifyTicker.C: // will never tick when notify is disabled
total := len(c.allowedCount) + len(c.blockedCount)
if total == 0 {
continue
}
total++
fieldsBody := make(notif.ListBody, total)
i := 0
fieldsBody[i] = fmt.Sprintf("Total: allowed %d, blocked %d", c.totalAllowedCount, c.totalBlockedCount)
i++
for ip, count := range c.allowedCount {
fieldsBody[i] = fmt.Sprintf("%s (%s): allowed %d times", ip, c.getCachedCity(ip), count)
i++
}
for ip, count := range c.blockedCount {
fieldsBody[i] = fmt.Sprintf("%s (%s): blocked %d times", ip, c.getCachedCity(ip), count)
i++
}
notif.Notify(&notif.LogMessage{
Level: zerolog.InfoLevel,
Title: "ACL Summary for last " + strutils.FormatDuration(c.Notify.Interval),
Body: fieldsBody,
To: c.Notify.To,
})
clear(c.allowedCount)
clear(c.blockedCount)
}
}
}
// log and notify if needed
func (c *Config) logAndNotify(info *maxmind.IPInfo, allowed bool) {
if c.logNotifyCh != nil {
c.logNotifyCh <- ipLog{info: info, allowed: allowed}
}
}
@@ -137,30 +280,30 @@ func (c *Config) IPAllowed(ip net.IP) bool {
}
if c.allowLocal && ip.IsPrivate() {
c.log(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
c.logAndNotify(&maxmind.IPInfo{IP: ip, Str: ip.String()}, true)
return true
}
ipStr := ip.String()
record, ok := c.ipCache.Load(ipStr)
if ok && !record.Expired() {
c.log(record.IPInfo, record.allow)
c.logAndNotify(record.IPInfo, record.allow)
return record.allow
}
ipAndStr := &maxmind.IPInfo{IP: ip, Str: ipStr}
if c.Allow.Match(ipAndStr) {
c.log(ipAndStr, true)
c.logAndNotify(ipAndStr, true)
c.cacheRecord(ipAndStr, true)
return true
}
if c.Deny.Match(ipAndStr) {
c.log(ipAndStr, false)
c.logAndNotify(ipAndStr, false)
c.cacheRecord(ipAndStr, false)
return false
}
c.log(ipAndStr, c.defaultAllow)
c.logAndNotify(ipAndStr, c.defaultAllow)
c.cacheRecord(ipAndStr, c.defaultAllow)
return c.defaultAllow
}

View File

@@ -4,8 +4,8 @@ import (
"net"
"strings"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/maxmind"
"github.com/yusing/godoxy/internal/maxmind"
gperr "github.com/yusing/goutils/errs"
)
type MatcherFunc func(*maxmind.IPInfo) bool

View File

@@ -5,8 +5,8 @@ import (
"reflect"
"testing"
maxmind "github.com/yusing/go-proxy/internal/maxmind/types"
"github.com/yusing/go-proxy/internal/serialization"
maxmind "github.com/yusing/godoxy/internal/maxmind/types"
"github.com/yusing/godoxy/internal/serialization"
)
func TestMatchers(t *testing.T) {

View File

@@ -1,6 +1,7 @@
package acl
import (
"errors"
"io"
"net"
"time"
@@ -54,6 +55,21 @@ func (s *TCPListener) Accept() (net.Conn, error) {
return c, nil
}
type tcpListener interface {
SetDeadline(t time.Time) error
}
var _ tcpListener = (*net.TCPListener)(nil)
func (s *TCPListener) SetDeadline(t time.Time) error {
switch lis := s.lis.(type) {
case tcpListener:
return lis.SetDeadline(t)
default:
return errors.New("not a TCPListener")
}
}
func (s *TCPListener) Close() error {
return s.lis.Close()
}

View File

@@ -1,6 +1,7 @@
package acl
import (
"errors"
"net"
"time"
)
@@ -74,6 +75,31 @@ func (s *UDPListener) SetWriteDeadline(t time.Time) error {
return s.lis.SetWriteDeadline(t)
}
type udpListener interface {
SetReadBuffer(bytes int) error
SetWriteBuffer(bytes int) error
}
var _ udpListener = (*net.UDPConn)(nil)
func (s *UDPListener) SetReadBuffer(bytes int) error {
switch lis := s.lis.(type) {
case udpListener:
return lis.SetReadBuffer(bytes)
default:
return errors.New("not a UDPConn")
}
}
func (s *UDPListener) SetWriteBuffer(bytes int) error {
switch lis := s.lis.(type) {
case udpListener:
return lis.SetWriteBuffer(bytes)
default:
return errors.New("not a UDPConn")
}
}
func (s *UDPListener) Close() error {
return s.lis.Close()
}

View File

@@ -0,0 +1,281 @@
# Agent Pool
Thread-safe pool for managing remote Docker agent connections.
## Overview
The agentpool package provides a centralized pool for storing and retrieving remote agent configurations. It enables GoDoxy to connect to Docker hosts via agent connections instead of direct socket access, enabling secure remote container management.
### Primary consumers
- `internal/route/provider` - Creates agent-based route providers
- `internal/docker` - Manages agent-based Docker client connections
- Configuration loading during startup
### Non-goals
- Agent lifecycle management (handled by `agent/pkg/agent`)
- Agent health monitoring
- Agent authentication/authorization
### Stability
Stable internal package. The pool uses `xsync.Map` for lock-free concurrent access.
## Public API
### Exported types
```go
type Agent struct {
*agent.AgentConfig
httpClient *http.Client
fasthttpHcClient *fasthttp.Client
}
```
### Exported functions
```go
func Add(cfg *agent.AgentConfig) (added bool)
```
Adds an agent to the pool. Returns `true` if added, `false` if already exists. Uses `LoadOrCompute` to prevent duplicates.
```go
func Has(cfg *agent.AgentConfig) bool
```
Checks if an agent exists in the pool.
```go
func Remove(cfg *agent.AgentConfig)
```
Removes an agent from the pool.
```go
func RemoveAll()
```
Removes all agents from the pool. Called during configuration reload.
```go
func Get(agentAddrOrDockerHost string) (*Agent, bool)
```
Retrieves an agent by address or Docker host URL. Automatically detects if the input is an agent address or Docker host URL and resolves accordingly.
```go
func GetAgent(name string) (*Agent, bool)
```
Retrieves an agent by name. O(n) iteration over pool contents.
```go
func List() []*Agent
```
Returns all agents as a slice. Creates a new copy for thread safety.
```go
func Iter() iter.Seq2[string, *Agent]
```
Returns an iterator over all agents. Uses `xsync.Map.Range`.
```go
func Num() int
```
Returns the number of agents in the pool.
```go
func (agent *Agent) HTTPClient() *http.Client
```
Returns an HTTP client configured for the agent.
## Architecture
### Core components
```mermaid
graph TD
A[Agent Config] --> B[Add to Pool]
B --> C[xsync.Map Storage]
C --> D{Get Request}
D -->|By Address| E[Load from map]
D -->|By Docker Host| F[Resolve agent addr]
D -->|By Name| G[Iterate & match]
H[Docker Client] --> I[Get Agent]
I --> C
I --> J[HTTP Client]
J --> K[Agent Connection]
L[Route Provider] --> M[List Agents]
M --> C
```
### Thread safety model
The pool uses `xsync.Map[string, *Agent]` for concurrent-safe operations:
- `Add`: `LoadOrCompute` prevents race conditions and duplicates
- `Get`: Lock-free read operations
- `Iter`: Consistent snapshot iteration via `Range`
- `Remove`: Thread-safe deletion
### Test mode
When running tests (binary ends with `.test`), a test agent is automatically added:
```go
func init() {
if strings.HasSuffix(os.Args[0], ".test") {
agentPool.Store("test-agent", &Agent{
AgentConfig: &agent.AgentConfig{
Addr: "test-agent",
},
})
}
}
```
## Configuration Surface
No direct configuration. Agents are added via configuration loading from `config/config.yml`:
```yaml
providers:
agents:
- addr: agent.example.com:443
name: remote-agent
tls:
ca_file: /path/to/ca.pem
cert_file: /path/to/cert.pem
key_file: /path/to/key.pem
```
## Dependency and Integration Map
### Internal dependencies
- `agent/pkg/agent` - Agent configuration and connection settings
- `xsync/v4` - Concurrent map implementation
### External dependencies
- `valyala/fasthttp` - Fast HTTP client for agent communication
### Integration points
```go
// Docker package uses agent pool for remote connections
if agent.IsDockerHostAgent(host) {
a, ok := agentpool.Get(host)
if !ok {
panic(fmt.Errorf("agent %q not found", host))
}
opt := []client.Opt{
client.WithHost(agent.DockerHost),
client.WithHTTPClient(a.HTTPClient()),
}
}
```
## Observability
### Logs
No specific logging in the agentpool package. Client creation/destruction is logged in the docker package.
### Metrics
No metrics are currently exposed.
## Security Considerations
- TLS configuration is loaded from agent configuration
- Connection credentials are not stored in the pool after agent creation
- HTTP clients are created per-request to ensure credential freshness
## Failure Modes and Recovery
| Failure | Behavior | Recovery |
| -------------------- | -------------------- | ---------------------------- |
| Agent not found | Returns `nil, false` | Add agent to pool before use |
| Duplicate add | Returns `false` | Existing agent is preserved |
| Test mode activation | Test agent added | Only during test binaries |
## Performance Characteristics
- O(1) lookup by address
- O(n) iteration for name-based lookup
- Pre-sized to 10 entries via `xsync.WithPresize(10)`
- No locks required for read operations
- HTTP clients are created per-call to ensure fresh connections
## Usage Examples
### Adding an agent
```go
agentConfig := &agent.AgentConfig{
Addr: "agent.example.com:443",
Name: "my-agent",
}
added := agentpool.Add(agentConfig)
if !added {
log.Println("Agent already exists")
}
```
### Retrieving an agent
```go
// By address
agent, ok := agentpool.Get("agent.example.com:443")
if !ok {
log.Fatal("Agent not found")
}
// By Docker host URL
agent, ok := agentpool.Get("http://docker-host:2375")
if !ok {
log.Fatal("Agent not found")
}
// By name
agent, ok := agentpool.GetAgent("my-agent")
if !ok {
log.Fatal("Agent not found")
}
```
### Iterating over all agents
```go
for addr, agent := range agentpool.Iter() {
log.Printf("Agent: %s at %s", agent.Name, addr)
}
```
### Using with Docker client
```go
// When creating a Docker client with an agent host
if agent.IsDockerHostAgent(host) {
a, ok := agentpool.Get(host)
if !ok {
panic(fmt.Errorf("agent %q not found", host))
}
opt := []client.Opt{
client.WithHost(agent.DockerHost),
client.WithHTTPClient(a.HTTPClient()),
}
dockerClient, err := client.New(opt...)
}
```

View File

@@ -0,0 +1,54 @@
package agentpool
import (
"net"
"net/http"
"time"
"github.com/valyala/fasthttp"
"github.com/yusing/godoxy/agent/pkg/agent"
)
type Agent struct {
*agent.AgentConfig
httpClient *http.Client
fasthttpHcClient *fasthttp.Client
}
func newAgent(cfg *agent.AgentConfig) *Agent {
transport := cfg.Transport()
transport.MaxIdleConns = 100
transport.MaxIdleConnsPerHost = 100
transport.ReadBufferSize = 16384
transport.WriteBufferSize = 16384
return &Agent{
AgentConfig: cfg,
httpClient: &http.Client{
Transport: transport,
},
fasthttpHcClient: &fasthttp.Client{
DialTimeout: func(addr string, timeout time.Duration) (net.Conn, error) {
if addr != agent.AgentHost+":443" {
return nil, &net.AddrError{Err: "invalid address", Addr: addr}
}
return net.DialTimeout("tcp", cfg.Addr, timeout)
},
TLSConfig: cfg.TLSConfig(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 3 * time.Second,
DisableHeaderNamesNormalizing: true,
DisablePathNormalizing: true,
NoDefaultUserAgentHeader: true,
ReadBufferSize: 1024,
WriteBufferSize: 1024,
},
}
}
func (agent *Agent) HTTPClient() *http.Client {
return &http.Client{
Transport: agent.Transport(),
}
}

View File

@@ -0,0 +1,96 @@
package agentpool
import (
"context"
"fmt"
"io"
"net/http"
"time"
"github.com/bytedance/sonic"
"github.com/gorilla/websocket"
"github.com/valyala/fasthttp"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/goutils/http/reverseproxy"
)
func (cfg *Agent) Do(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, agent.APIBaseURL+endpoint, body)
if err != nil {
return nil, err
}
return cfg.httpClient.Do(req)
}
func (cfg *Agent) Forward(req *http.Request, endpoint string) (*http.Response, error) {
req.URL.Host = agent.AgentHost
req.URL.Scheme = "https"
req.URL.Path = agent.APIEndpointBase + endpoint
req.RequestURI = ""
resp, err := cfg.httpClient.Do(req)
if err != nil {
return nil, err
}
return resp, nil
}
type HealthCheckResponse struct {
Healthy bool `json:"healthy"`
Detail string `json:"detail"`
Latency time.Duration `json:"latency"`
}
func (cfg *Agent) DoHealthCheck(timeout time.Duration, query string) (ret HealthCheckResponse, err error) {
req := fasthttp.AcquireRequest()
defer fasthttp.ReleaseRequest(req)
resp := fasthttp.AcquireResponse()
defer fasthttp.ReleaseResponse(resp)
req.SetRequestURI(agent.APIBaseURL + agent.EndpointHealth + "?" + query)
req.Header.SetMethod(fasthttp.MethodGet)
req.Header.Set("Accept-Encoding", "identity")
req.SetConnectionClose()
start := time.Now()
err = cfg.fasthttpHcClient.DoTimeout(req, resp, timeout)
ret.Latency = time.Since(start)
if err != nil {
return ret, err
}
if status := resp.StatusCode(); status != http.StatusOK {
ret.Detail = fmt.Sprintf("HTTP %d %s", status, resp.Body())
return ret, nil
} else {
err = sonic.Unmarshal(resp.Body(), &ret)
if err != nil {
return ret, err
}
}
return ret, nil
}
func (cfg *Agent) Websocket(ctx context.Context, endpoint string) (*websocket.Conn, *http.Response, error) {
transport := cfg.Transport()
dialer := websocket.Dialer{
NetDialContext: transport.DialContext,
NetDialTLSContext: transport.DialTLSContext,
}
return dialer.DialContext(ctx, agent.APIBaseURL+endpoint, http.Header{
"Host": {agent.AgentHost},
})
}
// ReverseProxy reverse proxies the request to the agent
//
// 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 *Agent) ReverseProxy(w http.ResponseWriter, req *http.Request, endpoint string) {
rp := reverseproxy.NewReverseProxy("agent", agent.AgentURL, cfg.Transport())
req.URL.Host = agent.AgentHost
req.URL.Scheme = "https"
req.URL.Path = endpoint
req.RequestURI = ""
rp.ServeHTTP(w, req)
}

View File

@@ -0,0 +1,79 @@
package agentpool
import (
"iter"
"os"
"strings"
"github.com/puzpuzpuz/xsync/v4"
"github.com/yusing/godoxy/agent/pkg/agent"
)
var agentPool = xsync.NewMap[string, *Agent](xsync.WithPresize(10))
func init() {
if strings.HasSuffix(os.Args[0], ".test") {
agentPool.Store("test-agent", &Agent{
AgentConfig: &agent.AgentConfig{
Addr: "test-agent",
},
})
}
}
func Get(agentAddrOrDockerHost string) (*Agent, bool) {
if !agent.IsDockerHostAgent(agentAddrOrDockerHost) {
return getAgentByAddr(agentAddrOrDockerHost)
}
return getAgentByAddr(agent.GetAgentAddrFromDockerHost(agentAddrOrDockerHost))
}
func GetAgent(name string) (*Agent, bool) {
for _, agent := range agentPool.Range {
if agent.Name == name {
return agent, true
}
}
return nil, false
}
func Add(cfg *agent.AgentConfig) (added bool) {
_, loaded := agentPool.LoadOrCompute(cfg.Addr, func() (*Agent, bool) {
return newAgent(cfg), false
})
return !loaded
}
func Has(cfg *agent.AgentConfig) bool {
_, ok := agentPool.Load(cfg.Addr)
return ok
}
func Remove(cfg *agent.AgentConfig) {
agentPool.Delete(cfg.Addr)
}
func RemoveAll() {
agentPool.Clear()
}
func List() []*Agent {
agents := make([]*Agent, 0, agentPool.Size())
for _, agent := range agentPool.Range {
agents = append(agents, agent)
}
return agents
}
func Iter() iter.Seq2[string, *Agent] {
return agentPool.Range
}
func Num() int {
return agentPool.Size()
}
func getAgentByAddr(addr string) (agent *Agent, ok bool) {
agent, ok = agentPool.Load(addr)
return agent, ok
}

View File

@@ -2,25 +2,25 @@ package api
import (
"net/http"
"strconv"
"time"
"reflect"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/codec/json"
"github.com/gorilla/websocket"
"github.com/rs/zerolog/log"
apitypes "github.com/yusing/go-proxy/internal/api/types"
apiV1 "github.com/yusing/go-proxy/internal/api/v1"
agentApi "github.com/yusing/go-proxy/internal/api/v1/agent"
authApi "github.com/yusing/go-proxy/internal/api/v1/auth"
certApi "github.com/yusing/go-proxy/internal/api/v1/cert"
dockerApi "github.com/yusing/go-proxy/internal/api/v1/docker"
fileApi "github.com/yusing/go-proxy/internal/api/v1/file"
homepageApi "github.com/yusing/go-proxy/internal/api/v1/homepage"
metricsApi "github.com/yusing/go-proxy/internal/api/v1/metrics"
routeApi "github.com/yusing/go-proxy/internal/api/v1/route"
"github.com/yusing/go-proxy/internal/auth"
"github.com/yusing/go-proxy/internal/common"
"github.com/yusing/go-proxy/internal/gperr"
apiV1 "github.com/yusing/godoxy/internal/api/v1"
agentApi "github.com/yusing/godoxy/internal/api/v1/agent"
authApi "github.com/yusing/godoxy/internal/api/v1/auth"
certApi "github.com/yusing/godoxy/internal/api/v1/cert"
dockerApi "github.com/yusing/godoxy/internal/api/v1/docker"
fileApi "github.com/yusing/godoxy/internal/api/v1/file"
homepageApi "github.com/yusing/godoxy/internal/api/v1/homepage"
metricsApi "github.com/yusing/godoxy/internal/api/v1/metrics"
routeApi "github.com/yusing/godoxy/internal/api/v1/route"
"github.com/yusing/godoxy/internal/auth"
"github.com/yusing/godoxy/internal/common"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
)
// @title GoDoxy API
@@ -45,6 +45,9 @@ func NewHandler() *gin.Engine {
r := gin.New()
r.Use(ErrorHandler())
r.Use(ErrorLoggingMiddleware())
r.Use(NoCache())
log.Debug().Msg("gin codec json.API: " + reflect.TypeOf(json.API).Name())
r.GET("/api/v1/version", apiV1.Version)
@@ -69,7 +72,7 @@ func NewHandler() *gin.Engine {
}
{
// enable cache for favicon
v1.GET("/favicon", apiV1.FavIcon).Use(Cache(time.Hour * 24))
v1.GET("/favicon", apiV1.FavIcon)
v1.GET("/health", apiV1.Health)
v1.GET("/icons", apiV1.Icons)
v1.POST("/reload", apiV1.Reload)
@@ -81,6 +84,7 @@ func NewHandler() *gin.Engine {
route.GET("/:which", routeApi.Route)
route.GET("/providers", routeApi.Providers)
route.GET("/by_provider", routeApi.ByProvider)
route.POST("/playground", routeApi.Playground)
}
file := v1.Group("/file")
@@ -139,15 +143,13 @@ func NewHandler() *gin.Engine {
}
}
// disable cache by default
r.Use(NoCache())
return r
}
func NoCache() gin.HandlerFunc {
return func(c *gin.Context) {
// skip cache if Cache-Control header is set or if caching is explicitly enabled
if !c.GetBool("cache_enabled") && c.Writer.Header().Get("Cache-Control") == "" {
// skip cache if Cache-Control header is set
if c.Writer.Header().Get("Cache-Control") == "" {
c.Header("Cache-Control", "no-cache, no-store, must-revalidate")
c.Header("Pragma", "no-cache")
c.Header("Expires", "0")
@@ -156,20 +158,6 @@ func NoCache() gin.HandlerFunc {
}
}
func Cache(duration time.Duration) gin.HandlerFunc {
return func(c *gin.Context) {
// Signal to NoCache middleware that caching is intended
c.Set("cache_enabled", true)
// skip cache if Cache-Control header is set
if c.Writer.Header().Get("Cache-Control") == "" {
c.Header("Cache-Control", "public, max-age="+strconv.FormatFloat(duration.Seconds(), 'f', 0, 64)+", immutable")
c.Header("Pragma", "public")
c.Header("Expires", time.Now().Add(duration).Format(time.RFC1123))
}
c.Next()
}
}
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := auth.GetDefaultAuth().CheckToken(c.Request)

View File

@@ -1,55 +0,0 @@
package apitypes
import (
"errors"
"github.com/yusing/go-proxy/internal/gperr"
)
type ErrorResponse struct {
Message string `json:"message"`
Error string `json:"error,omitempty" extensions:"x-nullable"`
} // @name ErrorResponse
type serverError struct {
Message string
Err error
}
// Error returns a generic error response
func Error(message string, err ...error) ErrorResponse {
if len(err) > 0 {
var gpErr gperr.Error
if errors.As(err[0], &gpErr) {
return ErrorResponse{
Message: message,
Error: string(gpErr.Plain()),
}
}
return ErrorResponse{
Message: message,
Error: err[0].Error(),
}
}
return ErrorResponse{
Message: message,
}
}
func InternalServerError(err error, message string) error {
return serverError{
Message: message,
Err: err,
}
}
func (e serverError) Error() string {
if e.Err != nil {
return e.Message + ": " + e.Err.Error()
}
return e.Message
}
func (e serverError) Unwrap() error {
return e.Err
}

View File

@@ -1,29 +0,0 @@
package apitypes
type QueryOptions struct {
Limit int `binding:"required,min=1,max=20" form:"limit"`
Offset int `binding:"omitempty,min=0" form:"offset"`
OrderBy QueryOrder `binding:"omitempty,oneof=created_at updated_at" form:"order_by"`
Order QueryOrderDirection `binding:"omitempty,oneof=asc desc" form:"order"`
}
type QueryOrder string
const (
QueryOrderCreatedAt QueryOrder = "created_at"
QueryOrderUpdatedAt QueryOrder = "updated_at"
)
type QueryOrderDirection string
const (
QueryOrderDirectionAsc QueryOrderDirection = "asc"
QueryOrderDirectionDesc QueryOrderDirection = "desc"
)
type QueryResponse struct {
Total int64 `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
HasMore bool `json:"has_more"`
}

View File

@@ -1,18 +0,0 @@
package apitypes
type SuccessResponse struct {
Message string `json:"message"`
Details map[string]any `json:"details,omitempty" extensions:"x-nullable"`
} // @name SuccessResponse
func Success(message string, extra ...map[string]any) SuccessResponse {
if len(extra) > 0 {
return SuccessResponse{
Message: message,
Details: extra[0],
}
}
return SuccessResponse{
Message: message,
}
}

197
internal/api/v1/README.md Normal file
View File

@@ -0,0 +1,197 @@
# API v1 Package
Implements the v1 REST API handlers for GoDoxy, exposing endpoints for managing routes, Docker containers, certificates, metrics, and system configuration.
## Overview
The `internal/api/v1` package implements the HTTP handlers that power GoDoxy's REST API. It uses the Gin web framework and provides endpoints for route management, container operations, certificate handling, system metrics, and configuration.
### Primary Consumers
- **WebUI**: The homepage dashboard and admin interface consume these endpoints
### Non-goals
- Authentication and authorization logic (delegated to `internal/auth`)
- Route proxying and request handling (handled by `internal/route`)
- Docker container lifecycle management (delegated to `internal/docker`)
- Certificate issuance and storage (handled by `internal/autocert`)
### Stability
This package is stable. Public API endpoints follow semantic versioning for request/response contracts. Internal implementation may change between minor versions.
## Public API
### Exported Types
Types are defined in `goutils/apitypes`:
| Type | Purpose |
| -------------------------- | -------------------------------- |
| `apitypes.ErrorResponse` | Standard error response format |
| `apitypes.SuccessResponse` | Standard success response format |
### Handler Subpackages
| Package | Purpose |
| ---------- | ---------------------------------------------- |
| `route` | Route listing, details, and playground testing |
| `docker` | Docker container management and monitoring |
| `cert` | Certificate information and renewal |
| `metrics` | System metrics and uptime information |
| `homepage` | Homepage items and category management |
| `file` | Configuration file read/write operations |
| `auth` | Authentication and session management |
| `agent` | Remote agent creation and management |
## Architecture
### Handler Organization
Package structure mirrors the API endpoint paths (e.g., `auth/login.go` handles `/auth/login`).
### Request Flow
```mermaid
sequenceDiagram
participant Client
participant GinRouter
participant Handler
participant Service
participant Response
Client->>GinRouter: HTTP Request
GinRouter->>Handler: Route to handler
Handler->>Service: Call service layer
Service-->>Handler: Data or error
Handler->>Response: Format JSON response
Response-->>Client: JSON or redirect
```
## Configuration Surface
API listening address is configured with `GODOXY_API_ADDR` environment variable.
## Dependency and Integration Map
### Internal Dependencies
| Package | Purpose |
| ----------------------- | --------------------------- |
| `internal/route/routes` | Route storage and iteration |
| `internal/docker` | Docker client management |
| `internal/config` | Configuration access |
| `internal/metrics` | System metrics collection |
| `internal/homepage` | Homepage item generation |
| `internal/agentpool` | Remote agent management |
| `internal/auth` | Authentication services |
### External Dependencies
| Package | Purpose |
| ------------------------------ | --------------------------- |
| `github.com/gin-gonic/gin` | HTTP routing and middleware |
| `github.com/gorilla/websocket` | WebSocket support |
| `github.com/moby/moby/client` | Docker API client |
## Observability
### Logs
Handlers log at `INFO` level for requests and `ERROR` level for failures. Logs include:
- Request path and method
- Response status code
- Error details (when applicable)
### Metrics
No dedicated metrics exposed by handlers. Request metrics collected by middleware.
## Security Considerations
- All endpoints (except `/api/v1/version`) require authentication
- Input validation using Gin binding tags
- Path traversal prevention in file operations
- WebSocket connections use same auth middleware as HTTP
## Failure Modes and Recovery
| Failure | Behavior |
| ----------------------------------- | ------------------------------------------ |
| Docker host unreachable | Returns partial results with errors logged |
| Certificate provider not configured | Returns 404 |
| Invalid request body | Returns 400 with error details |
| Authentication failure | Returns 302 redirect to login |
| Agent not found | Returns 404 |
## Usage Examples
### Listing All Routes via WebSocket
```go
import (
"github.com/gorilla/websocket"
)
func watchRoutes(provider string) error {
url := "ws://localhost:8888/api/v1/route/list"
if provider != "" {
url += "?provider=" + provider
}
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return err
}
defer conn.Close()
for {
_, message, err := conn.ReadMessage()
if err != nil {
return err
}
// message contains JSON array of routes
processRoutes(message)
}
}
```
### Getting Container Status
```go
import (
"encoding/json"
"net/http"
)
type Container struct {
Server string `json:"server"`
Name string `json:"name"`
ID string `json:"id"`
Image string `json:"image"`
}
func listContainers() ([]Container, error) {
resp, err := http.Get("http://localhost:8888/api/v1/docker/containers")
if err != nil {
return nil, err
}
defer resp.Body.Close()
var containers []Container
if err := json.NewDecoder(resp.Body).Decode(&containers); err != nil {
return nil, err
}
return containers, nil
}
```
### Health Check
```bash
curl http://localhost:8888/health
```
)

View File

@@ -7,7 +7,7 @@ import (
"time"
"github.com/rs/zerolog/log"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/agent"
)
type PEMPairResponse struct {

View File

@@ -8,8 +8,9 @@ import (
_ "embed"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/agent/pkg/agent"
apitypes "github.com/yusing/go-proxy/internal/api/types"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/internal/agentpool"
apitypes "github.com/yusing/goutils/apitypes"
)
type NewAgentRequest struct {
@@ -50,7 +51,7 @@ func Create(c *gin.Context) {
}
hostport := net.JoinHostPort(request.Host, strconv.Itoa(request.Port))
if _, ok := agent.GetAgent(hostport); ok {
if _, ok := agentpool.Get(hostport); ok {
c.JSON(http.StatusConflict, apitypes.Error("agent already exists"))
return
}

View File

@@ -5,9 +5,11 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/internal/net/gphttp/httpheaders"
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
"github.com/yusing/godoxy/internal/agentpool"
"github.com/yusing/goutils/http/httpheaders"
"github.com/yusing/goutils/http/websocket"
_ "github.com/yusing/goutils/apitypes"
)
// @x-id "list"
@@ -17,16 +19,15 @@ import (
// @Tags agent,websocket
// @Accept json
// @Produce json
// @Success 200 {array} Agent
// @Success 200 {array} agent.AgentConfig
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /agent/list [get]
func List(c *gin.Context) {
if httpheaders.IsWebsocket(c.Request.Header) {
websocket.PeriodicWrite(c, 10*time.Second, func() (any, error) {
return agent.ListAgents(), nil
return agentpool.List(), nil
})
} else {
c.JSON(http.StatusOK, agent.ListAgents())
c.JSON(http.StatusOK, agentpool.List())
}
}

View File

@@ -6,10 +6,13 @@ import (
"os"
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/agent/pkg/agent"
"github.com/yusing/go-proxy/agent/pkg/certs"
. "github.com/yusing/go-proxy/internal/api/types"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/godoxy/agent/pkg/agent"
"github.com/yusing/godoxy/agent/pkg/certs"
"github.com/yusing/godoxy/internal/agentpool"
config "github.com/yusing/godoxy/internal/config/types"
"github.com/yusing/godoxy/internal/route/provider"
apitypes "github.com/yusing/goutils/apitypes"
gperr "github.com/yusing/goutils/errs"
)
type VerifyNewAgentRequest struct {
@@ -35,44 +38,88 @@ type VerifyNewAgentRequest struct {
func Verify(c *gin.Context) {
var request VerifyNewAgentRequest
if err := c.ShouldBindJSON(&request); err != nil {
c.JSON(http.StatusBadRequest, Error("invalid request", err))
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
filename, ok := certs.AgentCertsFilepath(request.Host)
if !ok {
c.JSON(http.StatusBadRequest, Error("invalid host", nil))
c.JSON(http.StatusBadRequest, apitypes.Error("invalid host", nil))
return
}
ca, err := fromEncryptedPEMPairResponse(request.CA)
if err != nil {
c.JSON(http.StatusBadRequest, Error("invalid CA", err))
c.JSON(http.StatusBadRequest, apitypes.Error("invalid CA", err))
return
}
client, err := fromEncryptedPEMPairResponse(request.Client)
if err != nil {
c.JSON(http.StatusBadRequest, Error("invalid client", err))
c.JSON(http.StatusBadRequest, apitypes.Error("invalid client", err))
return
}
nRoutesAdded, err := config.GetInstance().VerifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
nRoutesAdded, err := verifyNewAgent(request.Host, ca, client, request.ContainerRuntime)
if err != nil {
c.JSON(http.StatusBadRequest, Error("invalid request", err))
c.JSON(http.StatusBadRequest, apitypes.Error("invalid request", err))
return
}
zip, err := certs.ZipCert(ca.Cert, client.Cert, client.Key)
if err != nil {
c.Error(InternalServerError(err, "failed to zip certs"))
c.Error(apitypes.InternalServerError(err, "failed to zip certs"))
return
}
if err := os.WriteFile(filename, zip, 0o600); err != nil {
c.Error(InternalServerError(err, "failed to write certs"))
c.Error(apitypes.InternalServerError(err, "failed to write certs"))
return
}
c.JSON(http.StatusOK, Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
c.JSON(http.StatusOK, apitypes.Success(fmt.Sprintf("Added %d routes", nRoutesAdded)))
}
var errAgentAlreadyExists = gperr.New("agent already exists")
func verifyNewAgent(host string, ca agent.PEMPair, client agent.PEMPair, containerRuntime agent.ContainerRuntime) (int, gperr.Error) {
var agentCfg agent.AgentConfig
agentCfg.Addr = host
agentCfg.Runtime = containerRuntime
// check if agent host exists in the config
cfgState := config.ActiveState.Load()
for _, a := range cfgState.Value().Providers.Agents {
if a.Addr == host {
return 0, errAgentAlreadyExists
}
}
// check if agent host exists in the agent pool
if agentpool.Has(&agentCfg) {
return 0, errAgentAlreadyExists
}
err := agentCfg.InitWithCerts(cfgState.Context(), ca.Cert, client.Cert, client.Key)
if err != nil {
return 0, gperr.Wrap(err, "failed to initialize agent config")
}
provider := provider.NewAgentProvider(&agentCfg)
if _, loaded := cfgState.LoadOrStoreProvider(provider.String(), provider); loaded {
return 0, gperr.Errorf("provider %s already exists", provider.String())
}
// agent must be added before loading routes
added := agentpool.Add(&agentCfg)
if !added {
return 0, errAgentAlreadyExists
}
err = provider.LoadRoutes()
if err != nil {
cfgState.DeleteProvider(provider.String())
agentpool.Remove(&agentCfg)
return 0, gperr.Wrap(err, "failed to load routes")
}
return provider.NumRoutes(), nil
}

View File

@@ -3,7 +3,7 @@ package auth
import (
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/internal/auth"
"github.com/yusing/godoxy/internal/auth"
)
// @x-id "callback"

View File

@@ -2,7 +2,7 @@ package auth
import (
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/internal/auth"
"github.com/yusing/godoxy/internal/auth"
)
// @x-id "check"

View File

@@ -2,7 +2,7 @@ package auth
import (
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/internal/auth"
"github.com/yusing/godoxy/internal/auth"
)
// @x-id "login"

View File

@@ -2,7 +2,7 @@ package auth
import (
"github.com/gin-gonic/gin"
"github.com/yusing/go-proxy/internal/auth"
"github.com/yusing/godoxy/internal/auth"
)
// @x-id "logout"

View File

@@ -1,53 +1,41 @@
package certapi
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
apitypes "github.com/yusing/go-proxy/internal/api/types"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/godoxy/internal/autocert"
apitypes "github.com/yusing/goutils/apitypes"
)
type CertInfo struct {
Subject string `json:"subject"`
Issuer string `json:"issuer"`
NotBefore int64 `json:"not_before"`
NotAfter int64 `json:"not_after"`
DNSNames []string `json:"dns_names"`
EmailAddresses []string `json:"email_addresses"`
} // @name CertInfo
// @x-id "info"
// @BasePath /api/v1
// @Summary Get cert info
// @Description Get cert info
// @Tags cert
// @Produce json
// @Success 200 {object} CertInfo
// @Failure 403 {object} apitypes.ErrorResponse
// @Failure 404 {object} apitypes.ErrorResponse
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /cert/info [get]
// @Success 200 {array} autocert.CertInfo
// @Failure 403 {object} apitypes.ErrorResponse "Unauthorized"
// @Failure 404 {object} apitypes.ErrorResponse "No certificates found or autocert is not enabled"
// @Failure 500 {object} apitypes.ErrorResponse "Internal server error"
// @Router /cert/info [get]
func Info(c *gin.Context) {
autocert := config.GetInstance().AutoCertProvider()
if autocert == nil {
provider := autocert.ActiveProvider.Load()
if provider == nil {
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
return
}
cert, err := autocert.GetCert(nil)
certInfos, err := provider.GetCertInfos()
if err != nil {
if errors.Is(err, autocert.ErrNoCertificates) {
c.JSON(http.StatusNotFound, apitypes.Error("no certificate found"))
return
}
c.Error(apitypes.InternalServerError(err, "failed to get cert info"))
return
}
certInfo := CertInfo{
Subject: cert.Leaf.Subject.CommonName,
Issuer: cert.Leaf.Issuer.CommonName,
NotBefore: cert.Leaf.NotBefore.Unix(),
NotAfter: cert.Leaf.NotAfter.Unix(),
DNSNames: cert.Leaf.DNSNames,
EmailAddresses: cert.Leaf.EmailAddresses,
}
c.JSON(http.StatusOK, certInfo)
c.JSON(http.StatusOK, certInfos)
}

View File

@@ -6,11 +6,10 @@ import (
"github.com/gin-gonic/gin"
"github.com/rs/zerolog/log"
apitypes "github.com/yusing/go-proxy/internal/api/types"
config "github.com/yusing/go-proxy/internal/config/types"
"github.com/yusing/go-proxy/internal/gperr"
"github.com/yusing/go-proxy/internal/logging/memlogger"
"github.com/yusing/go-proxy/internal/net/gphttp/websocket"
"github.com/yusing/godoxy/internal/autocert"
"github.com/yusing/godoxy/internal/logging/memlogger"
apitypes "github.com/yusing/goutils/apitypes"
"github.com/yusing/goutils/http/websocket"
)
// @x-id "renew"
@@ -24,7 +23,7 @@ import (
// @Failure 500 {object} apitypes.ErrorResponse
// @Router /cert/renew [get]
func Renew(c *gin.Context) {
autocert := config.GetInstance().AutoCertProvider()
autocert := autocert.ActiveProvider.Load()
if autocert == nil {
c.JSON(http.StatusNotFound, apitypes.Error("autocert is not enabled"))
return
@@ -40,33 +39,33 @@ func Renew(c *gin.Context) {
logs, cancel := memlogger.Events()
defer cancel()
done := make(chan struct{})
go func() {
defer close(done)
// Stream logs until WebSocket connection closes (renewal runs in background)
for {
select {
case <-manager.Context().Done():
return
case l := <-logs:
if err != nil {
return
}
err = autocert.ObtainCert()
if err != nil {
gperr.LogError("failed to obtain cert", err)
_ = manager.WriteData(websocket.TextMessage, []byte(err.Error()), 10*time.Second)
} else {
log.Info().Msg("cert obtained successfully")
err = manager.WriteData(websocket.TextMessage, l, 10*time.Second)
if err != nil {
return
}
}
}
}()
for {
select {
case l := <-logs:
if err != nil {
return
}
err = manager.WriteData(websocket.TextMessage, l, 10*time.Second)
if err != nil {
return
}
case <-done:
return
}
// renewal happens in background
ok := autocert.ForceExpiryAll()
if !ok {
log.Error().Msg("cert renewal already in progress")
time.Sleep(1 * time.Second) // wait for the log above to be sent
return
}
log.Info().Msg("cert force renewal requested")
autocert.WaitRenewalDone(manager.Context())
}

Some files were not shown because too many files have changed in this diff Show More