Compare commits

...
292 Commits
Author SHA1 Message Date
Ara Sadoyan 56c41e2c19 Cargo.toml 2026-10-21 19:13:18 +02:00
Ara Sadoyan 6ea8b42835 Improved API for /conf endpoint 2026-10-21 19:05:21 +02:00
Ara Sadoyan af771287cb README update 2026-10-20 17:43:38 +02:00
Ara Sadoyan 0c489b649a Improved error handling, anti unwrap()-ing 2026-10-20 17:06:55 +02:00
Ara Sadoyan f895ba4554 Restructured cache. Added Redis|Valkey|Dragonfly support. 2026-10-19 19:13:24 +02:00
Ara Sadoyan a2b14b0dea Cargo version update 2026-10-16 19:06:33 +02:00
Ara Sadoyan 0ee39559ca Master, multi slave cluster 2026-10-16 17:57:33 +02:00
Ara Sadoyan cdeb120a39 ACME DNS DigitalOcean 2026-10-16 17:56:57 +02:00
Ara Sadoyan ee578854e3 Fix scheme detection on HC disable upstreams 2026-10-07 15:02:51 +02:00
Ara Sadoyan a8dc98f636 cargo version 2026-10-05 19:07:32 +02:00
Ara Sadoyan dad9539b9c Fix provider separation 2026-10-05 19:06:44 +02:00
Ara Sadoyan ab5252ee52 Fixed bug with HTTP-01 Challenge 2026-10-05 18:41:39 +02:00
Ara Sadoyan 0f7f7b723b Added Route53 support. 2026-10-05 15:40:07 +02:00
Ara Sadoyan 2b0f4d68e6 Cargo version fix 2026-10-04 12:11:36 +02:00
Ara Sadoyan 7b248f2190 Polished lookup wait for DNS-01 challenge 2026-10-04 12:08:10 +02:00
Ara Sadoyan 1ecf3a69ff Completed skeleton for DNS challenge, added Cloudflare DNS integration. 2026-10-03 13:49:07 +02:00
Ara Sadoyan 3bc398610b Intermediate commit fo DNS Challenge 2026-10-03 10:56:47 +02:00
Ara Sadoyan 91d2d414a3 Minor improvements, type changes 2026-10-01 15:19:18 +02:00
Ara Sadoyan ea09efed4d Merge branch 'main' of github.com:sadoyan/aralez 2026-09-28 11:51:23 +02:00
Ara Sadoyan 2d19415e92 merge 2026-09-28 11:51:01 +02:00
Ara Sadoyan 9aa72d20dc Merge pull request #59 from sergelogvinov/linter
fix linter warnings
2026-09-28 11:49:35 +02:00
Serge Logvinov 0814d28df6 fix linter warnings 2026-09-28 07:15:07 +00:00
Ara Sadoyan e7b4f9a327 Merge pull request #58 from sergelogvinov/build
add compat versions
2026-09-27 10:50:33 +02:00
Ara Sadoyan 1fab52456f Optimize structured logging. 2026-09-26 18:49:37 +02:00
Serge Logvinov c4732cafd2 feat: add compat verions 2026-09-26 16:04:10 +02:00
Ara Sadoyan d623a2da72 Optimize structured logging. 2026-09-26 15:58:46 +02:00
Ara Sadoyan a145c5e71b file delete 2026-09-25 18:07:03 +02:00
Ara Sadoyan 9de8c9ba97 version change 2026-09-25 18:01:15 +02:00
Ara Sadoyan 502c2ed4c2 Merge pull request #57 from sergelogvinov/build
feat: github actions
2026-09-25 17:54:45 +02:00
Ara Sadoyan a8b3f804f4 Done with logging to ElasticSearch. 2026-09-25 17:52:01 +02:00
Ara Sadoyan 1b733bc879 Intermediate ElasticSearch commit . 2026-09-25 15:26:31 +02:00
Ara Sadoyan 92ed7cebde Intermediate ElasticSearch commit . 2026-09-25 12:55:38 +02:00
Ara Sadoyan 3fe037de5a Making custom Elsticsearch logger 2. 2026-09-24 18:27:53 +02:00
Ara Sadoyan d56968c4c3 Making custom Elsticsearch logger. 2026-09-23 17:57:18 +02:00
Ara Sadoyan 71ebc6376e improving logging 2026-09-23 15:28:41 +02:00
Ara Sadoyan 54ac35a4c9 added inventory for immutable core logging files 2026-09-22 18:48:42 +02:00
Ara Sadoyan 639365c43b restructurization of logging subsystem 2026-09-22 18:25:52 +02:00
Ara Sadoyan 4cd9b459b0 Code restructurization 2026-09-21 15:31:10 +02:00
Ara Sadoyan 4ae92a264b Structural changes, move auth to separate module . 2026-09-20 18:57:45 +02:00
Serge Logvinov efcf83033f feat: github actions 2026-09-19 12:41:38 +02:00
Ara Sadoyan 5fdfa17265 Update to Pingora 0.9, improved logging, Fix cache key bug. 2026-09-18 18:33:35 +02:00
Ara Sadoyan 5efcf07dd0 Updated example main.yaml 2026-09-17 11:35:52 +02:00
Ara Sadoyan 02a329e66b Added log pattern to main.cfg 2026-09-16 19:18:18 +02:00
Ara Sadoyan 444579eefe Minor changes 2026-09-14 10:55:44 +02:00
Ara Sadoyan 0831ffb801 Merge pull request #53 from sergelogvinov/chart
feat: add aralez helm chart
2026-09-09 19:08:06 +02:00
Ara Sadoyan 5d55e2b120 configs 2026-09-09 19:01:38 +02:00
Ara Sadoyan b3c614e592 configs 2026-09-09 18:54:25 +02:00
Ara Sadoyan b22bb2fe16 Config files update, cleanup 2026-09-09 18:52:32 +02:00
Ara Sadoyan e6204a2b1d version change 2026-09-09 18:43:49 +02:00
Ara Sadoyan 6a78acd486 version change 2026-09-09 18:43:28 +02:00
Ara Sadoyan 1a2485b87a Implemented EndpointSlices #55 2026-09-09 18:42:41 +02:00
Ara Sadoyan 3329c34a09 working on #55 2026-09-09 15:39:28 +02:00
Serge 6df63f69cd feat: add aralez helm chart
Add basic Aralez deployment configuration, Helm chart linting workflow,
and Dependabot configuration.
2026-09-06 11:33:16 +02:00
Ara Sadoyan 4a4f3e6187 fixed closed #52 2026-09-04 16:24:49 +02:00
Ara Sadoyan 31f513cc2a hcl examples 2026-09-01 12:53:42 +02:00
Ara Sadoyan 0006d27d02 hcl examples 2026-09-01 12:19:09 +02:00
Ara Sadoyan d2d4100cba Finalized Consul integration,
Cached headers bug fix
2026-08-31 19:18:10 +02:00
Ara Sadoyan f575007044 Rewrite of Consul integration. 2026-08-30 18:38:31 +02:00
Ara Sadoyan bb43df0e4d Full rewrite of Kubernetes integration. 2026-08-25 19:32:42 +02:00
Ara Sadoyan 29c3d6a413 Full rewrite of Kubernetes integration. 2026-08-25 19:31:05 +02:00
Ara Sadoyan c21374f2a2 Full rewrite of Kubernetes integration. 2026-08-25 19:29:53 +02:00
Ara Sadoyan 51eec524ca Cargo 2026-08-10 15:40:50 +02:00
Ara Sadoyan 97ac839001 Removed reqwest dependency, moved all http client reqvetsts to native pingora. 2026-08-06 19:38:03 +02:00
Ara Sadoyan b63d822577 Rewrote and optimized healtchecks 2026-08-05 18:51:07 +02:00
Ara Sadoyan 600cb602e3 Readme update 2026-07-28 14:45:46 +02:00
Ara Sadoyan 29d1f1db78 Solved conflict on H2 upstream and Websocket, better gRPC ping 2026-07-24 12:28:44 +02:00
Ara Sadoyan 3ae54185ad Solved conflict on H2 upstream and Websocket, better gRPC ping 2026-07-24 12:27:12 +02:00
Ara Sadoyan 430f867792 Merge branch 'main' of github.com:sadoyan/aralez 2026-07-20 13:47:54 +02:00
Ara Sadoyan f6c092adc4 version fix 2026-07-20 13:47:05 +02:00
Ara Sadoyan 19ad8fdbf4 yaml update 2026-07-20 13:40:18 +02:00
Ara Sadoyan b55ff2d392 minor fix on memory response cache 2026-07-20 13:37:10 +02:00
Ara Sadoyan b73ccc527f Implementing in memory response cache 2026-07-20 12:25:27 +02:00
Ara Sadoyan c89fe48e44 Cargo.toml version update 2026-07-13 11:35:52 +02:00
Ara Sadoyan d21fd8cb4b config update 2026-07-13 11:32:36 +02:00
Ara Sadoyan 491442bb92 Added log file rotation and compression. Updated to Rust 1.97.0 2026-07-13 11:29:31 +02:00
Ara Sadoyan e07791e1f0 Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 13:34:47 +02:00
Ara Sadoyan 7b952b467c Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 13:34:08 +02:00
Ara Sadoyan 9a1e8fe149 Removed futures dependency, migrated to tokio::sync::mpsc .
closed #45
2026-06-30 12:27:49 +02:00
Ara Sadoyan 874ce4fddb Implementing full async access log inspired by #45 2026-06-29 21:35:10 +02:00
Ara Sadoyan 99c7a811da Header parsing broken with rsplit_once in populate_headers_and_auth and build_headers
closed #48
2026-06-27 21:34:13 +02:00
Ara Sadoyan edadda803c version change 2026-06-27 14:20:06 +02:00
Ara Sadoyan c5d0fe60a0 resolved possible divide by zero issue from #46 2026-06-27 13:38:59 +02:00
Ara Sadoyan d7fe476b24 Merge pull request #47 from sadoyan/dev
Dev
2026-06-27 12:33:32 +02:00
Ara Sadoyan aa22749393 Merge branch 'main' into dev 2026-06-27 12:32:41 +02:00
Ara Sadoyan 4b6a558b4b Merge branch 'main' into dev 2026-06-27 12:31:49 +02:00
Ara Sadoyan 6c9c7acf65 Merge pull request #46 from Taqman-probe/fix/sticky-session-404
Fix: Resolve 404/405 errors with sticky session cookie handling in /api routes
2026-06-27 12:31:29 +02:00
Taqman-probe 2845dc5923 fix 404 error on multipath and cleanup fn: clone_idmap_into 2026-06-27 13:51:12 +09:00
Ara Sadoyan f2e07a8d70 resolved #44 2026-06-26 16:32:58 +02:00
Ara Sadoyan 5815da6576 Merge pull request #43 from sadoyan/dev
Update main.yaml
2026-06-22 13:27:17 +02:00
Ara Sadoyan 1ab0916026 Merge branch 'dev' 2026-06-22 13:24:42 +02:00
Ara Sadoyan 707f725b88 Update main.yaml 2026-06-22 13:22:45 +02:00
Ara Sadoyan eb4e73ece0 Basic access/error logging. Upgrade to Pingra 8.0.1 2026-06-22 13:20:56 +02:00
Ara Sadoyan 53e7dcfd33 Pid file follow symlink problem. 2026-06-19 11:14:40 +02:00
Ara Sadoyan f3b346a28d Merge pull request #42 from sadoyan/dev
migrate from serde_yml to noyalib
2026-06-12 16:05:30 +02:00
Ara Sadoyan c011800e1e migrate from serde_yml to noyalib 2026-06-12 16:03:49 +02:00
Ara Sadoyan 265ff6b774 cleanup 2026-06-12 13:33:21 +02:00
Ara Sadoyan 1c3d9a263f example configs 2026-06-12 13:12:39 +02:00
Ara Sadoyan 162c5060c9 cleanup, minor fix 2026-06-10 11:46:30 +02:00
Ara Sadoyan 132cf45dfe cleanup, minor fix 2026-06-10 11:29:35 +02:00
Ara Sadoyan 77dcafbb4e tcp_keepalive options for kernel timer 2026-06-09 17:53:04 +02:00
Ara Sadoyan 649bd979f7 socker address #41 2026-06-07 12:07:06 +02:00
Ara Sadoyan bd315106b9 Merge pull request #41 from Taqman-probe/fix/TcpListener-bind
Fix TcpListener binding and port availability check
2026-06-07 11:57:06 +02:00
Ara Sadoyan 3ba2ed33ae cargo.toml 2026-06-07 11:38:50 +02:00
Taqman-probe c09efab9fd prevent panic when parsing invalid address without port 2026-06-07 00:47:43 +09:00
Taqman-probe 735d605f6d prevent panic when parsing invalid address without port 2026-06-06 22:32:32 +09:00
Ara Sadoyan 6773d0f502 Cargo 2026-06-04 19:12:50 +02:00
Ara Sadoyan a8cb727da5 Cargo Licence 2026-06-04 18:39:02 +02:00
Ara Sadoyan f81194aee7 Cargo Licence 2026-06-04 18:38:47 +02:00
Ara Sadoyan 0f09a2e02b Roll back to MiMalloc 2026-06-04 18:14:22 +02:00
Ara Sadoyan 27aca0a3a5 Roll back to MiMalloc 2026-06-04 17:03:19 +02:00
Ara Sadoyan 3c99ed0c44 Fixed #40 2026-06-03 16:37:56 +02:00
Ara Sadoyan 4a6e1d817b OS signals , PID file 2026-06-02 19:56:08 +02:00
Ara Sadoyan f6a2df44d0 README 2026-06-02 12:20:18 +02:00
Ara Sadoyan e8d976f5f1 A minor issue with file loader 2026-06-01 15:35:49 +02:00
Ara Sadoyan a1341e90f7 Merge branch 'Taqman-probe-fix/mismatch-file-watch' into dev 2026-06-01 14:39:44 +02:00
Ara Sadoyan ddcacc509c Merge branch 'fix/mismatch-file-watch' of github.com:Taqman-probe/aralez into Taqman-probe-fix/mismatch-file-watch 2026-06-01 14:39:06 +02:00
Ara Sadoyan 2b385cc630 gitignore 2026-06-01 14:29:50 +02:00
Ara Sadoyan 1b5223986d README, signals 2026-06-01 14:25:36 +02:00
Ara Sadoyan ac2bd8113d README update 2026-06-01 11:48:54 +02:00
Ara Sadoyan 4d9a2ecfe3 Honoring OS signals 2026-05-30 18:53:36 +02:00
Taqman-probe aa70d818b2 Fix config file change detection logic in file watcher 2026-05-30 00:34:35 +09:00
Ara Sadoyan 15d356f087 Removed Dockerfile, put content to README 2026-05-27 19:23:32 +02:00
Ara Sadoyan b17962e6d5 Merge pull request #31 from Taqman-probe/fix/rate-limit-setting-message
fix: Fix global rate limit and 4xx limit fallback in upstream config log
2026-05-27 16:28:33 +02:00
Ara Sadoyan ea779182f2 Include request path in rate limit and 4xx limit warning logs
#32
2026-05-27 16:15:55 +02:00
Ara Sadoyan 9339a142a6 Merge pull request #33 from Taqman-probe/fix/skip-tls-detection-when-hc-disabled
Skip TLS detection when healthcheck: false
2026-05-27 15:09:12 +02:00
Ara Sadoyan cfc9bd319d Merge branch 'dev' 2026-05-27 14:58:56 +02:00
Ara Sadoyan a5ced59e5c Merge branch 'main' of github.com:sadoyan/aralez 2026-05-27 14:58:33 +02:00
Ara Sadoyan c3df7bc131 gitignore 2026-05-27 14:57:34 +02:00
Ara Sadoyan 0248d73836 Merge pull request #24 from gzsombor/forwarded-for-header
X-Forwarded-For should only contain the IP address
2026-05-27 14:55:51 +02:00
Ara Sadoyan 96a5aef7d0 warning log on retry parce 2026-05-27 14:54:21 +02:00
Ara Sadoyan 5c3b72b7a3 parceyaml 2026-05-27 14:26:05 +02:00
Ara Sadoyan 3cf0fc493f Merge pull request #34 from Taqman-probe/fix/failed-hot-reload-config
Add retry mechanism for configuration parsing failures
2026-05-27 14:18:28 +02:00
Ara Sadoyan 207ee481fb update 2026-05-27 14:12:10 +02:00
Ara Sadoyan 1c2f7327aa gitignore 2026-05-27 14:03:55 +02:00
Ara Sadoyan 310a554a25 gitignore 2026-05-27 14:03:22 +02:00
Ara Sadoyan 69a5167346 Merge pull request #35 from sadoyan/dev
Dev
2026-05-27 13:56:10 +02:00
Ara Sadoyan 4734ccab2f Minor fixed #26 & #28 2026-05-27 13:40:22 +02:00
Taqman-probe 61d65f6e4e Failed hot reloading of config file 2026-05-27 18:36:00 +09:00
Taqman-probe 20ac39067d Skip TLS detection when healthcheck: false 2026-05-27 16:44:17 +09:00
Taqman-probe 7afa76de8f fix: Fix global rate limit fallback in upstream config log 2026-05-27 14:22:59 +09:00
Ara Sadoyan e29161965f Added more monitoring metrics 2026-05-26 19:34:10 +02:00
Zsombor Gegesy 9216710dda X-Forwarded-For should only contain the IP address
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-For
2026-05-23 07:11:55 +02:00
Ara Sadoyan faf840d102 Merge pull request #23 from sadoyan/dev
New features, 4xx counter
2026-05-22 16:57:07 +02:00
Ara Sadoyan d74883e16e New features, 4xx counter 2026-05-22 16:56:33 +02:00
Ara Sadoyan d95bbfcd1a Merge pull request #22 from sadoyan/dev
- Changed sticky session value from bool to u32, corresponding to `Max-Age=` of cookie
- Added counter and rate limiter for 4xx requests 
- Changed config for JWT authorization. The `data`  property is removed.
2026-05-22 16:52:27 +02:00
Ara Sadoyan d301f7225f New features, 4xx counter 2026-05-22 16:47:40 +02:00
Ara Sadoyan df02e523e4 cleanups 2026-05-21 18:34:46 +02:00
Ara Sadoyan 2f5def5c3c Changed sticky session from bool to Option<u64> 2026-05-20 21:09:23 +02:00
Ara Sadoyan 1727a2b5e7 deleted some comments 2026-05-20 17:17:04 +02:00
Ara Sadoyan 4bbedee27b JWT auth read and caches KEY from system env. 2026-05-19 15:26:05 +02:00
Ara Sadoyan 37ef118861 Dockerfile #21 2026-05-19 11:48:12 +02:00
Ara Sadoyan 00062b00da Removed authentication from API server, JWT master key as environment variable 2026-05-18 20:38:30 +02:00
Ara Sadoyan 2ce290abcf README update 2026-05-15 18:32:58 +02:00
Ara Sadoyan 2380f83d8e Added log to file option. 2026-05-15 16:00:57 +02:00
Ara Sadoyan 3965a1de93 README typo 2026-05-14 10:54:27 +02:00
Ara Sadoyan 7bc8294c22 README typo 2026-05-14 10:53:42 +02:00
Ara Sadoyan 37c2693e22 update cargo 2026-05-13 18:17:43 +02:00
Ara Sadoyan 554fa6648a Added special DEFAULT upstreams for catch up all. 2026-05-13 17:27:13 +02:00
Ara Sadoyan 20329518c1 Fix, add metrics and cleanup. 2026-05-13 16:17:11 +02:00
Ara Sadoyan 136ccc8e44 Persist config from API 2026-05-11 18:34:41 +02:00
Ara Sadoyan 1cbb19ea90 README 2026-05-09 14:53:10 +02:00
Ara Sadoyan c0e9fbc069 README site change 2026-05-09 13:54:26 +02:00
Ara Sadoyan 98318c5fb6 README 2026-05-08 19:36:38 +02:00
Ara Sadoyan fec9d5f1d6 Cleanup. Making clippy happy. 2026-05-08 16:35:20 +02:00
Ara Sadoyan 783ffb27e1 Merge pull request #20 from HrachMD/token-in-logs
Token Logging
2026-05-08 13:53:08 +02:00
Ara Sadoyan 3f6ee1799c Merge branch 'main' into token-in-logs 2026-05-08 13:52:14 +02:00
Ara Sadoyan c549750e9d README update 2026-05-08 13:07:28 +02:00
Ara Sadoyan 788f7fd4ea README update 2026-05-08 13:03:17 +02:00
hrachdev 5e29c077f3 chore: readme tweaks & fmt 2026-05-07 23:10:13 -04:00
hrachdev 22609df4ba Merge branch 'main' of github.com:HrachMD/aralez into token-in-logs 2026-05-07 23:02:58 -04:00
hrachdev c0594aed48 chore: cleanup 2026-05-07 20:02:18 -04:00
hrachdev 4f7f2d21ca fix: typo 2026-05-07 19:56:08 -04:00
hrachdev 4d04b8d7f1 fix: token logging, unneeded copy 2026-05-07 19:51:55 -04:00
Ara Sadoyan c381faabc6 README update 2026-05-07 19:57:14 +02:00
Ara Sadoyan 982feb632e minor fix update 2026-05-06 18:14:08 +02:00
Ara Sadoyan aee71c74f5 README update 2026-05-01 12:32:58 +02:00
Ara Sadoyan a0b6de9759 updated . config example 2026-04-30 18:13:37 +02:00
Ara Sadoyan a70eb53bc1 Let's Encrypt auto certificate HTTP-01 challenge #16 2026-04-30 18:04:25 +02:00
Ara Sadoyan bee307793c restructurisation grades 2026-04-27 15:28:54 +02:00
Ara Sadoyan 6e83775127 restructurisation 2026-04-27 15:22:31 +02:00
Ara Sadoyan baded40e6e Cache for JWT tokens, to minimize crypto. BRAKING: Claims key "valid" renamed to "exp" 2026-04-17 17:53:31 +02:00
Ara Sadoyan c0a419f6f7 completed implementation of #17 2026-04-15 18:23:57 +02:00
Ara Sadoyan 8aff2fa875 Standardizing implementation of #17 2026-04-14 16:11:24 +02:00
Ara Sadoyan 9b4ee26a2b Working on #17 2026-04-13 20:06:57 +02:00
Ara Sadoyan f135106a44 Changes in authentication 2026-04-08 19:05:19 +02:00
Ara Sadoyan 389c12119a code cleanup and improvements. 2026-04-08 17:00:06 +02:00
Ara Sadoyan 93a8661281 Cargo cleanup, dependency merge 2026-04-08 15:14:46 +02:00
Ara Sadoyan 0505ce2849 split upstreams.yaml file 2026-03-30 19:04:32 +02:00
Ara Sadoyan 72ed870538 split upstreams.yaml file 2026-03-27 19:24:30 +01:00
Ara Sadoyan 68140d0cf0 tye changes, optimization 2026-03-26 17:40:22 +01:00
Ara Sadoyan 7b9b206c13 optimization & cleanup 2026-03-26 16:58:53 +01:00
Ara Sadoyan 4706b281bc cleanup 2026-03-26 14:17:59 +01:00
Ara Sadoyan 1f8efc6af7 FUNDING.yml 2026-03-25 15:16:47 +01:00
Ara Sadoyan 9f595b2709 example config file update 2026-03-25 11:15:55 +01:00
Ara Sadoyan ed44516015 added redirect_to directive for upstreams 2026-03-24 16:08:14 +01:00
Ara Sadoyan 17da7862e3 upstreams ID hashing update 2026-03-18 20:06:50 +01:00
Ara Sadoyan 24d00da855 performance improvement, sticky session minor bug fix 2026-03-17 19:21:05 +01:00
Ara Sadoyan c9422759aa Minor performance improvement 2026-03-17 13:54:42 +01:00
Ara Sadoyan 94b1f77734 Type changes, auth override policy 2026-03-04 12:35:45 +01:00
Ara Sadoyan 9d986f9a28 Path level authentication 2026-03-03 19:35:16 +01:00
Ara Sadoyan 3afa2f209f pingora 0.8.0 upgrade 2026-03-03 13:54:53 +01:00
Ara Sadoyan c151fdf58b moving to boringssl 2026-02-19 18:11:54 +01:00
Ara Sadoyan 438426153f removed unwrap 2026-02-18 12:00:33 +01:00
Ara Sadoyan 9bb01fd1b0 minor improvements 2026-02-17 18:22:46 +01:00
Ara Sadoyan abb5fef1d6 minor improvements 2026-02-17 17:03:52 +01:00
Ara Sadoyan 3618687ad5 Memory allocation improvements for proxyhttp, fix issue with sticky session . 2026-02-10 19:07:43 +01:00
Ara Sadoyan a893b3c301 Memory allocation improvements for metrics collector . 2026-02-05 13:57:39 +01:00
Ara Sadoyan 3ff262c7f4 Merge pull request #13 from yerke/patch-1
Fix grammar and formatting in README.md
2026-02-04 14:41:50 +01:00
Yerkebulan Tulibergenov 062f02259f Fix grammar and formatting in README.md 2026-01-30 23:59:10 -08:00
Ara Sadoyan 1a4c9b7d55 Performance optimization in headers 2026-01-28 16:07:45 +01:00
Ara Sadoyan 6ef7f23823 Performance optimization v2 2026-01-28 13:20:31 +01:00
Ara Sadoyan 2b437c65fb Performance improvement. String removal from hot paths. 2026-01-27 16:19:51 +01:00
Ara Sadoyan 38055ae94e added new metric aralez_requests_by_upstream 2026-01-25 18:08:15 +01:00
Ara Sadoyan 703de9e909 updates on API server https://sadoyan.github.io/aralez-docs/assets/api/ 2026-01-22 16:50:51 +01:00
Ara Sadoyan 2c8b01295c Minor subfunction removal 2026-01-21 20:01:16 +01:00
Ara Sadoyan baebe1c00f Async apply of config via API 2026-01-20 19:16:27 +01:00
Ara Sadoyan 6c1d3c5ef8 Error handling on API server 2026-01-09 18:44:36 +01:00
Ara Sadoyan 2d1a827007 Removed unneeded loop 2025-12-14 12:09:11 +01:00
Ara Sadoyan a2a5250711 Performance improvements on data types . 2025-12-11 15:21:34 +01:00
Ara Sadoyan 985e923342 to https redirect bug fix 2025-12-11 13:37:40 +01:00
Ara Sadoyan 0fc79c022f perf: optimize header handling and concurrent access patterns 2025-12-10 19:09:04 +01:00
Ara Sadoyan a43bccdfb8 minor, performance improvements 2025-11-28 13:13:15 +01:00
Ara Sadoyan 5b87391fbb some more type changes, performance improvements 2025-11-27 18:47:04 +01:00
Ara Sadoyan c68a4ad83d Type changes, performance improvements 2025-11-27 18:03:34 +01:00
Ara Sadoyan 8ba8d32df1 Performance improvements, type changes 2025-11-26 12:12:41 +01:00
Ara Sadoyan 7a839065e6 update on kubernetes web client 2025-11-24 17:57:44 +01:00
Ara Sadoyan 74821654f3 Added support to send custom headers to upstream servers. 2025-11-22 23:18:06 +01:00
Ara Sadoyan 78c83b802f Merge Consul & Kubernetes discovery 2025-10-26 15:26:09 +01:00
Ara Sadoyan 012505b77e Cleaning up the code 2025-10-24 15:27:15 +02:00
Ara Sadoyan 21c4cb0901 Update README.md 2025-10-18 11:49:51 +02:00
Ara Sadoyan 86dd3d3402 README update 2025-10-18 11:48:48 +02:00
Ara Sadoyan d6b345202b README update 2025-10-17 17:03:45 +02:00
Ara Sadoyan 5209d787e4 README update 2025-10-17 16:44:57 +02:00
Ara Sadoyan 02de5f1c21 Merge remote-tracking branch 'origin/main' 2025-10-16 19:05:15 +02:00
Ara Sadoyan 9519280026 Path filter, and rate limiter for Consul 2025-10-16 19:04:46 +02:00
Ara Sadoyan e87c60cf4f unifying kubernetes and file provider configs 2025-10-15 19:13:33 +02:00
Ara Sadoyan 25693a7058 Path filtering and rate limit for kubernetes 2025-10-15 13:42:05 +02:00
Ara Sadoyan 3b0b385ec7 Create FUNDING.yml 2025-10-03 11:02:21 +02:00
Ara Sadoyan 5359c2e8e9 Create LICENSE 2025-10-02 11:14:40 +02:00
Ara Sadoyan 2b62d1e6de configs update 2025-10-02 10:56:55 +02:00
Ara Sadoyan 8a290e5084 Kubernetes path based routing 2025-10-01 20:18:36 +02:00
Ara Sadoyan 3541b20c80 intermediate minor optimization 2025-10-01 13:47:30 +02:00
Ara Sadoyan bd5fed9be0 Fix drop privileges, check root 2025-09-28 12:23:53 +02:00
Ara Sadoyan b916b152ea Changed config file parser at startup, to keep initially dead nodes in list. 2025-09-25 18:32:46 +02:00
Ara Sadoyan 5d4915d6b9 Fixed drop root privileges on ports below 1024 2025-09-19 12:46:17 +02:00
Ara Sadoyan 3ea3996e27 upgrade to pingora 0.6 2025-09-18 14:15:50 +02:00
Ara Sadoyan dd069b8532 minor fix 2025-09-17 16:51:57 +02:00
Ara Sadoyan c78245e695 disable HC for upstream. 2025-09-16 12:54:23 +02:00
Ara Sadoyan 66b1a1c399 upstreams pathconfig fix 2025-09-15 15:22:21 +02:00
Ara Sadoyan bba6dd8514 minor cleanup 2025-09-09 14:51:37 +02:00
Ara Sadoyan 79485ac69d minor cleanup 2025-09-04 18:16:09 +02:00
Ara Sadoyan 61c5625016 A coffee :-) 2025-09-02 14:57:47 +02:00
Ara Sadoyan 57bdc71acd A coffee :-) 2025-09-02 14:56:36 +02:00
Ara Sadoyan 9e09b829a6 README update 2025-09-01 17:02:57 +02:00
Ara Sadoyan d3602fa578 Added Kubernetes API support, fo ingress controller. 2025-09-01 16:32:30 +02:00
Ara Sadoyan e304482667 Optimized healthchecks and config file loading 2025-08-20 14:03:09 +02:00
Ara Sadoyan f8118f9596 TLS grades change 2025-08-05 19:08:58 +02:00
Ara Sadoyan f654312466 SSL cipher management 2025-07-29 21:25:27 +02:00
Ara Sadoyan b44f7069a0 Configurable TLS ciphers 2025-07-27 11:15:49 +02:00
Ara Sadoyan a44979ec82 Configurable TLS ciphers 2025-07-27 11:13:39 +02:00
Ara Sadoyan ece4fa20af README 2025-07-24 13:50:15 +02:00
Ara Sadoyan 2ad3a059ab Per path rate limiter 2025-07-24 13:34:15 +02:00
Ara Sadoyan 6f012cee69 Code cleanup 2025-07-22 17:40:58 +02:00
Ara Sadoyan 51c88c8f7c Some structural changes and improvements 2025-07-12 16:17:45 +02:00
Ara Sadoyan f91bc41103 benchmark image 2025-07-10 17:46:05 +02:00
Ara Sadoyan 21e1276ff5 Readme update 2025-07-09 15:22:38 +02:00
Ara Sadoyan 8463cdabbc Added configurable rate limiter 2025-07-09 15:01:20 +02:00
Ara Sadoyan d0e4b52ce6 Enable/Disable config API from config 2025-07-04 15:06:05 +02:00
Ara Sadoyan b552d24497 README 2025-07-02 19:00:05 +02:00
Ara Sadoyan 2e33d692bb Added optional minimal file server 2025-07-02 18:29:14 +02:00
Ara Sadoyan e586967830 Code cleanup, nothing special 2025-06-30 18:24:25 +02:00
Ara Sadoyan 8d4e434d6a Dynamic load of SSL certificates from disk. 2025-06-19 18:32:44 +02:00
Ara Sadoyan 60b7b3aa7a README 2025-06-16 13:42:30 +02:00
Ara Sadoyan 569db8e18d Project rename. Load multiple certificates from folder. 2025-06-16 13:32:05 +02:00
Ara Sadoyan 4126249bcd Project rename. Load multiple certificates from folder. 2025-06-16 13:29:13 +02:00
Ara Sadoyan 0779f97277 README Update 2025-06-09 18:12:25 +02:00
Ara Sadoyan b047331e6a README Update 2025-06-09 18:11:44 +02:00
Ara Sadoyan a341fa30db Add TLS to API server 2025-06-09 18:06:16 +02:00
Ara Sadoyan 9d604d62e7 METRICS.md update 2025-06-07 15:51:23 +02:00
Ara Sadoyan 4a21700552 README update 2025-06-07 11:47:29 +02:00
Ara Sadoyan f0157b6e8f README update 2025-06-07 11:38:07 +02:00
Ara Sadoyan 1370396ae8 README update 2025-06-07 10:56:31 +02:00
Ara Sadoyan 64ef4e14af README update 2025-06-07 10:11:39 +02:00
Ara Sadoyan ffc2bab79f API server changes, improvements 2025-06-06 19:30:51 +02:00
Ara Sadoyan 8e05794784 Metrics exporter for Prometheus 2025-05-28 21:24:22 +02:00
Ara Sadoyan 423c7afa90 Metrics exporter for Prometheus 2025-05-28 21:23:10 +02:00
Ara Sadoyan 78a084380a Name and config changes 2025-05-28 14:54:01 +02:00
Ara Sadoyan ada2032732 http to https redirect cleanup 2025-05-26 18:30:42 +02:00
Ara Sadoyan a89592bd07 http to https redirect cleanup 2025-05-26 16:24:15 +02:00
Ara Sadoyan 2a93bc2cd6 http to https redirect cleanup 2025-05-26 12:42:01 +02:00
Ara Sadoyan d38588a299 http to https redirect 2025-05-25 11:19:28 +02:00
Ara Sadoyan 3e93920a0d Some type changes 2025-05-21 16:49:37 +02:00
116 changed files with 12085 additions and 2775 deletions
+13
View File
@@ -0,0 +1,13 @@
[target.aarch64-unknown-linux-musl]
rustflags = [
"-C", "link-arg=-Wl,--defsym=fopen64=fopen",
"-C", "link-arg=-Wl,--defsym=fseeko64=fseeko",
"-C", "link-arg=-Wl,--defsym=ftello64=ftello"
]
[target.x86_64-unknown-linux-musl]
rustflags = [
"-C", "link-arg=-Wl,--defsym=fopen64=fopen",
"-C", "link-arg=-Wl,--defsym=fseeko64=fseeko",
"-C", "link-arg=-Wl,--defsym=ftello64=ftello"
]
+9
View File
@@ -0,0 +1,9 @@
.git/
.github/
.gitignore
.dockerignore
/charts/
#
docker-compose.yml
#
/.cargo/
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: sadoyan
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: sadoyan
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+9
View File
@@ -0,0 +1,9 @@
helm-extra-args: --timeout 300s
check-version-increment: true
debug: true
chart-dirs:
- charts
validate-maintainers: true
namespace: default
release-label: test
target-branch: main
+23
View File
@@ -0,0 +1,23 @@
---
# See https://docs.github.com/en/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
commit-message:
prefix: "chore:"
open-pull-requests-limit: 8
rebase-strategy: disabled
schedule:
interval: "monthly"
day: "monday"
time: "10:00"
timezone: "UTC"
cooldown:
default-days: 7
groups:
actions:
patterns:
- "actions/*"
- "helm/*"
+50
View File
@@ -0,0 +1,50 @@
name: Build check
on:
workflow_dispatch: {}
pull_request:
branches:
- main
paths:
- "src/**"
- "Dockerfile"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build and Test
timeout-minutes: 15
runs-on: ubuntu-24.04
if: github.event.pull_request.draft == false
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: "1.98"
components: clippy, rustfmt
- name: Install cargo-deny
uses: taiki-e/install-action@3f74d7c16a4242f1c95561e98edc25d36adb4375 # v2.87.12
with:
tool: cargo-deny
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Rust build
run: make build test
- name: Rust fmt
run: make fmt
- name: Rust linter
run: make lint
- name: Rust dependencies check
run: make deps
+28
View File
@@ -0,0 +1,28 @@
name: Helm chart check
on:
pull_request:
branches:
- main
paths:
- "charts/**"
jobs:
helm-lint:
name: Helm chart check
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install chart-testing tools
id: lint
uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0
- name: Run helm chart linter
run: ct --config .github/ct.yml lint
- name: Run helm template
run: helm lint charts/aralez
+58
View File
@@ -0,0 +1,58 @@
name: Release
on:
workflow_dispatch: {}
# push:
# tags:
# - "v*"
jobs:
goreleaser:
name: GoReleaser
runs-on: ubuntu-24.04
permissions:
contents: write
packages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Zig
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
- name: Rust toolchain
uses: dtolnay/rust-toolchain@6c977a6ca4077a0ceb28ffbe03f59d46e9ac8772 # v1
with:
toolchain: "1.98"
components: clippy, rustfmt
- name: Rust cache
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- name: Install Cosign
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Set up QEMU
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4.4.0
with:
platforms: arm64
- name: Set up docker buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Github registry login
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Run GoReleaser
timeout-minutes: 60
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
version: "v2.18.2"
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
USERNAME: ${{ github.repository_owner }}
+7
View File
@@ -5,6 +5,10 @@
*.dll
*.exe
*.sh
*.yaml
/docs
assets/
assets
/target/
*.iml
.idea/
@@ -17,4 +21,7 @@ com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
/target
/.zig-cache/
/z_shpo
/configs
Makefile
+126
View File
@@ -0,0 +1,126 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
version: 2
project_name: aralez
before:
hooks:
- cargo install --locked cargo-zigbuild
dist: target
builds:
- id: aralez
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-gnu
- aarch64-unknown-linux-gnu
env:
- RUSTFLAGS={{ if eq .Arch "amd64" }}-C target-cpu=x86-64-v3 -C target-feature=+aes,+ssse3{{else}}-C target-cpu=neoverse-n1 -C target-feature=+aes,+sha2,+neon,+crc{{ end }}
- id: aralez-musl
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-musl
- aarch64-unknown-linux-musl
env:
- RUSTFLAGS={{ if eq .Arch "amd64" }}-C target-cpu=x86-64-v3 -C target-feature=+aes,+ssse3{{else}}-C target-cpu=neoverse-n1 -C target-feature=+aes,+sha2,+neon,+crc{{ end }}
- id: aralez-compat
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-gnu
env:
- RUSTFLAGS=-C target-cpu=x86-64
- id: aralez-compat-musl
builder: rust
flags:
- --release
targets:
- x86_64-unknown-linux-musl
env:
- RUSTFLAGS=-C target-cpu=x86-64
snapshot:
version_template: edge
archives:
- id: aralez
ids:
- aralez
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-glibc"
- id: aralez-musl
ids:
- aralez-musl
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-musl"
- id: aralez-compat
ids:
- aralez-compat
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-compat-glibc"
- id: aralez-compat-musl
ids:
- aralez-compat-musl
formats: [binary]
name_template: "{{ .ProjectName }}_{{ .Arch }}-compat-musl"
dockers_v2:
- id: aralez
ids:
- aralez
flags:
- --target=release
images:
- "ghcr.io/{{ .Env.USERNAME }}/{{ .ProjectName }}"
# - "{{ .Env.USERNAME }}/{{ .ProjectName }}"
tags:
- "{{ if not .IsSnapshot }}v{{.Version}}{{ else }}edge{{ end }}"
platforms:
- linux/amd64
- linux/arm64
annotations:
org.opencontainers.image.name: "{{ .ProjectName }}"
org.opencontainers.image.licenses: "Apache-2.0"
org.opencontainers.image.source: "https://github.com/{{ .Env.USERNAME }}/{{ .ProjectName }}"
org.opencontainers.image.url: "{{ .GitURL }}"
org.opencontainers.image.created: "{{ .Date }}"
org.opencontainers.image.version: "{{ .Version }}"
org.opencontainers.image.revision: "{{ .FullCommit }}"
io.artifacthub.package.readme-url: "https://raw.githubusercontent.com/{{ .Env.USERNAME }}/{{ .ProjectName }}/main/README.md"
io.artifacthub.package.license: "Apache-2.0"
docker_signs:
- id: docker
ids:
- aralez
cmd: cosign
args:
- sign
- "--yes"
- "${artifact}"
output: true
checksum:
name_template: checksums.txt
algorithm: sha256
signs:
- artifacts: checksum
signature: "${artifact}.sigstore.json"
cmd: cosign
args:
- sign-blob
- "--yes"
- "--bundle=${signature}"
- "${artifact}"
output: true
Generated
+3742 -1191
View File
File diff suppressed because it is too large Load Diff
+62 -30
View File
@@ -1,7 +1,11 @@
[package]
name = "gazan"
version = "0.1.0"
edition = "2021"
name = "aralez"
version = "0.95.2"
edition = "2024"
license = "Apache-2.0"
description = "Reverse proxy built on top of Cloudflare's Pingora"
exclude = ["configs/*"]
repository = "https://github.com/sadoyan/aralez"
[profile.release]
opt-level = 3
@@ -11,33 +15,61 @@ panic = "abort"
strip = true
[dependencies]
tokio = { version = "1.45.0", features = ["full"] }
#pingora = { version = "0.5.0", features = ["lb", "rustls"] } # openssl, rustls, boringssl
pingora = { version = "0.5.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
serde = { version = "1.0.219", features = ["derive"] }
tokio = { version = "1.53.1", features = ["full"] }
pingora = { version = "0.9.0", features = ["lb", "openssl"] } # openssl, rustls, boringssl
pingora-core = "0.9.0"
pingora-proxy = "0.9.0"
pingora-http = "0.9.0"
pingora-limits = "0.9.0"
pingora-cache = "0.9.0"
dashmap = "7.0.0-rc2"
pingora-core = "0.5.0"
pingora-proxy = "0.5.0"
pingora-http = "0.5.0"
async-trait = "0.1.88"
env_logger = "0.11.8"
log = "0.4.27"
futures = "0.3.31"
notify = "8.0.0"
axum = { version = "0.8.4" }
reqwest = { version = "0.12.15", features = ["json", "native-tls-alpn"] }
#reqwest = { version = "0.12.15", features = ["json", "rustls-tls"] }
#reqwest = { version = "0.12.15", default-features = false, features = ["rustls-tls", "json"] }
serde_yaml = "0.9.34-deprecated"
rand = "0.9.0"
base64 = "0.22.1"
jsonwebtoken = "9.3.1"
tonic = "0.13.0"
sha2 = { version = "0.11.0-pre.5", default-features = false }
base16ct = { version = "0.2.0", features = ["alloc"] }
async-trait = "0.1.92"
log = "0.4.34"
notify = "9.0.0-rc.4"
axum = { version = "0.8.9" }
serde = { version = "1.0.229", features = ["derive", "rc"] }
#serde = { version = "1.0.229", features = ["derive"] }
noyalib = { version = "0.0.44", features = ["compat-serde-yaml"] }
rand = "0.10.2"
base64 = "0.23.1"
jsonwebtoken = { version = "11.1.0", default-features = false, features = ["use_pem", "rust_crypto"] }
sha2 = { version = "0.11.0", default-features = false }
base16ct = { version = "1.0.0", features = ["alloc"] }
urlencoding = "2.1.3"
arc-swap = "1.7.1"
#rustls = { version = "0.23.27", features = ["ring"] }
mimalloc = { version = "0.1.46", default-features = false }
arc-swap = "1.9.2"
prometheus = "0.14.0"
x509-parser = "0.18.1"
rustls-pemfile = "2.2.0"
tower-http = { version = "0.7.1", features = ["fs"] }
privdrop = "0.5.6"
serde_json = "1.0.151"
subtle = "2.6.1"
moka = { version = "0.12.16", features = ["sync"] }
ahash = "0.8.12"
instant-acme = "0.8.5"
rcgen = "0.14.10"
log4rs = { version = "1.4.0", features = ["rolling_file_appender", "compound_policy", "size_trigger", "fixed_window_roller", "gzip"] }
mimalloc = { version = "0.1.52", default-features = false }
signal-hook = "0.4.4"
sd-notify = "0.5.0"
libc = "0.2.186"
tokio-native-tls = "0.3.1"
hyper = "1.11.1"
hyper-util = "0.1.20"
http-body-util = "0.1.5"
bytes = "1.12.1"
rustls = { version = "0.23", default-features = false, features = ["ring"] }
kube = { version = "4.2.0", features = ["runtime", "client", "derive"] }
k8s-openapi = { version = "0.28.0", features = ["v1_32"] }
futures = "0.3.34"
lazy_static = "1.5.0"
anyhow = "1.0.103"
inventory = "0.3.24"
elasticsearch = "9.1.0-alpha.1"
humantime = "2.4.0"
reqwest = "0.12.28"
hickory-resolver = { version = "0.26", features = ["tokio"] }
aws-sdk-route53 = "1.127.0"
aws-config = { version = "1.12.0", features = ["behavior-version-latest"] }
redis = { version = "1.7.1", features = ["tokio-comp", "connection-manager"] }
+49
View File
@@ -0,0 +1,49 @@
# syntax = docker/dockerfile:1.22
########################################
FROM rust:1.98-trixie AS builder
ARG TARGETARCH
RUN apt-get update && \
apt-get install -y build-essential make cmake pkg-config libssl-dev musl-dev musl-tools
RUN case "${TARGETARCH}" in \
amd64) \
apt-get install -y --no-install-recommends gcc-x86-64-linux-gnu && \
rustup target add x86_64-unknown-linux-gnu; \
rustup target add x86_64-unknown-linux-musl; \
;; \
arm64) \
apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu && \
rustup target add aarch64-unknown-linux-gnu; \
rustup target add aarch64-unknown-linux-musl; \
;; \
*) echo "Unsupported architecture: ${TARGETARCH}" >&2; exit 1 ;; \
esac
RUN rustup component add rustfmt clippy && \
cargo install cargo-deny
RUN ARCH=$(case "${TARGETARCH}" in \
amd64) echo "x86_64" ;; \
arm64) echo "aarch64" ;; \
*) echo "${TARGETARCH}" ;; \
esac) && curl -LO https://ziglang.org/download/0.16.0/zig-${ARCH}-linux-0.16.0.tar.xz \
&& tar -xf zig-${ARCH}-linux-0.16.0.tar.xz -C /opt \
&& ln -s /opt/zig-${ARCH}-linux-0.16.0/zig /usr/local/bin/zig
RUN echo 'deb [trusted=yes] https://repo.goreleaser.com/apt/ /' | tee /etc/apt/sources.list.d/goreleaser.list && \
apt-get update && apt-get install -y goreleaser
RUN cargo install cargo-binstall --locked && \
cargo binstall cargo-zigbuild --no-confirm
COPY [".", "/src"]
########################################
FROM --platform=${TARGETARCH} gcr.io/distroless/cc-debian13:nonroot AS release
ARG TARGETPLATFORM
COPY ${TARGETPLATFORM}/aralez /usr/local/bin/aralez
ENTRYPOINT ["/usr/local/bin/aralez"]
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+68
View File
@@ -0,0 +1,68 @@
########################################################
USERNAME ?= aralez
TARGET ?= --all-targets
FEATURES ?= --all-features
########################################################
.PHONY: help
help:
@awk 'BEGIN {FS = ":.*?## "} /^[0-9a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
########################################################
.PHONY: all
all: build test fmt checkup ## Build and test project
.PHONY: build
build: ## Build project
cargo build --workspace $(TARGET) $(FEATURES)
.PHONY: test
test: ## Run tests
cargo test --workspace $(TARGET) $(FEATURES)
.PHONY: fmt
fmt: ## Check code formatting
cargo fmt --all -- --check
.PHONY: lint
lint: ## Run linter
cargo clippy --workspace $(TARGET) $(FEATURES) -- -D warnings
.PHONY: checkup
checkup: lint ## Run linter and check project
cargo check --workspace $(TARGET) $(FEATURES)
.PHONY: deps
deps: ## Check project dependencies for advisories
cargo deny check advisories
########################################################
.PHONY: update
update:
cargo update --verbose
.PHONY: features
features:
cargo features
########################################################
.PHONY: fix
fix:
cargo fix
.PHONY: fix-all
fix-all:
cargo fix --all
cargo clippy --workspace $(TARGET) $(FEATURES) --fix
########################################################
.PHONY: docker-build
docker-build: ## Build project in Docker
docker build --target=builder -t builder .
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v ${PWD}:/src -e USERNAME=${USERNAME} -w /src -ti builder:latest
+459 -125
View File
@@ -1,216 +1,356 @@
![Gazan](https://netangels.net/utils/gazan-white.jpg)
# Gazan - The beast-mode reverse proxy.
Gazan is a Reverse proxy, service mesh based on Cloudflare's Pingora
**What Gazan means?**
<ins>Gazan = Գազան = beast / wild animal in Armenian / Often used as a synonym to something great.</ins>.
Built on Rust, on top of **Cloudflare’s Pingora engine**, **Gazan** delivers world-class performance, security and scalability — right out of the box.
![Aralez](https://netangels.net/utils/aralez-white.jpg)
---
## 🔧 Key Features
# Aralez (Արալեզ)
- **Dynamic Config Reloads** — Upstreams can be updated live via API, no restart required
- **TLS Termination** — Built-in OpenSSL support
- **Upstreams TLS detection** — Gazan will automatically detect if upstreams uses secure connection
- **Authentication** — Supports Basic Auth, API tokens, and JWT verification
- **Load Balancing Strategies**
- Round-robin
- Failover with health checks
- Sticky sessions via cookies
- **Unified Port** — Serve HTTP and WebSocket traffic over the same connection
- **Memory Safe** — Created purely on Rust
- **High Performance** — Built with [Pingora](https://github.com/cloudflare/pingora) and tokio for async I/O
### **Reverse proxy built on top of Cloudflare's Pingora**
## 🌍 Highlights
Aralez is a high-performance Rust reverse proxy with zero-configuration automatic protocol handling, TLS, and upstream management, featuring Consul and Kubernetes integration for dynamic pod discovery and health-checked routing, acting as a
lightweight ingress-style proxy.
- ⚙️ **Upstream Providers:** Supports `file`-based static upstreams, dynamic service discovery via `Consul`.
- 🔁 **Hot Reloading:** Modify upstreams on the fly via `upstreams.yaml` — no restart needed.
- 🔮 **Automatic WebSocket Support:** Zero config — connection upgrades are handled seamlessly.
- 🔮 **Automatic GRPC Support:** Zero config, Requires `ssl` to proxy, gRPC is handled seamlessly.
- 🔮 **Upstreams Session Stickiness:** Enable/Disable Sticky sessions.
- 🔐 **TLS Termination:** Fully supports TLS for incoming and upstream traffic.
- 🛡️ **Built-in Authentication** Basic Auth, JWT, API key.
- 🧠 **Header Injection:** Global and per-route header configuration.
- 🧪 **Health Checks:** Pluggable health check methods for upstreams.
- 🛰️ **Remote Config Push:** Lightweight HTTP API to update configs from CI/CD or other systems.
---
What Aralez means ? **Aralez = Արալեզ** <ins>Named after the legendary Armenian guardian spirit, winged dog-like creature, that descend upon fallen heroes to lick their wounds and resurrect them</ins>.
Built on Rust, on top of **Cloudflare’s Pingora engine**, **Aralez** delivers world-class performance, security and scalability — right out of the box.
---
## 📁 File Structure
## Links
```
.
├── main.yaml # Main configuration loaded at startup
├── upstreams.yaml # Watched config with upstream mappings
├── etc/
│ ├── server.crt # TLS certificate (required if using TLS)
│ └── key.pem # TLS private key
```
- [**Documentation**](https://aralez.rs) : The manual you should read
- [**Downloads**](https://github.com/sadoyan/aralez/releases) : Binary downloads
- [**Issues**](https://github.com/sadoyan/aralez/issues) : Issues and requests
- [**Crates**](https://crates.io/crates/aralez) : The Rust crate registry
- [**DockerHUB**](https://hub.docker.com/r/sadoyan/aralez) : DockerHUB official repository
- [**GitHUB Packages**](https://github.com/sadoyan/aralez/pkgs/container/aralez) : GitHUB ghcr.io images
---
## 🛠 Configuration Overview
## Key Features
### 🔧 `main.yaml`
- `proxy_address_http`: `0.0.0.0:6193` (HTTP listener)
- `proxy_address_tls`: `0.0.0.0:6194` (TLS listener, optional)
- `config_address`: `0.0.0.0:3000` (HTTP API for remote config push)
- `upstreams_conf`: `etc/upstreams.yaml` (location of upstreams config)
- `log_level`: `info` (verbosity of logs)
- `hc_method`: `HEAD`, `hc_interval`: `2s` (upstream health checks)
- `user` Optional. Drop privileges to regular user. To bind to privileged ports. Requires to start as root.
- `group` Optional. Drop privileges to regular group
- Other defaults: thread count, keep-alive pool size, etc.
### 🌐 `upstreams.yaml`
- `provider`: `file` or `consul`
- File-based upstreams define:
- Hostnames and routing paths
- Backend servers (load-balanced)
- Optional request headers, specific to this upstream
- Global headers (e.g., CORS) apply to all proxied responses
- Optional authentication (Basic, API Key, JWT)
- **Dynamic Config Reloads** — Upstreams can be updated live via API, no restart required.
- **Autoload of certificates** — Automatically loads new/changed certificates from a folder, without a restart.
- **Let’s Encrypt Certificates** — Ordering and renewal of SSL/TLS certificates via the HTTP-01 challenge.
- **Upstreams TLS detection** — Aralez will automatically detect if upstreams uses secure connection.
- **Built in rate limiter** — Globar or route limit requests to upstreams.
- **Caching Proxy** — In memory cache with configurable TTL, without external dependencies.
- **Authentication** — Supports Basic Auth, API tokens, and JWT verification.
- **Basic Auth**
- **API Key** via `x-api-key` header
- **JWT Auth**, with tokens issued by Aralez itself via `/jwt` API
- **Forward Auth**, Sends requests to an authentication server.
- **Load Balancing** Round-robin, health checks, optional sticky sessions.
- **Built in file server** — Build in minimalistic file server for serving static files, should be added as upstreams for public access.
- **Upstream Providers:**
- `file` Upstreams are declared in config file.
- `consul` Upstreams are dynamically updated from Hashicorp Consul.
- `kubernetes` Upstreams are dynamically updated from kubernetes api server.
- **Auto WebSocket Support:** WS connection upgrades are handled automatically.
- **Auto gRPC Support:** gRPC detected and handled automatically.
- **Header Injection:** Global and per-route server/client headers injection.
- **Remote Config Push:** Lightweight HTTP API to update configs from CI/CD or other systems.
- **Memory Safe** — 100% Rust.
- **High Performance** — Built with [Pingora](https://github.com/cloudflare/pingora) and tokio for async I/O.
---
## 🛠 Installation
## Configuration Overview
Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/gazan/releases) repo
Make the binary executable `chmod 755 ./gazan-VERSION` and run.
### `main.yaml`
| Key | Example Value | Description |
|----------------------------------|-------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **threads** | `12` | Number of running daemon threads. Optional, defaults to 1 |
| **runuser** | `aralez` | Optional. Username for running aralez after dropping root privileges (requires launch as root) |
| **rungroup** | `aralez` | Optional. Group for running aralez after dropping root privileges (requires launch as root) |
| **daemon** | `false` | Run in background (boolean) |
| **upstream_keepalive_pool_size** | `500` | Pool size for upstream keepalive connections |
| **pid_file** | `/tmp/aralez.pid` | Path to PID file |
| **upgrade_sock** | `/tmp/aralez.sock` | Path to socket file |
| **config_address** | `0.0.0.0:3000` | HTTP API address for pushing upstreams.yaml from remote location |
| **proxy_tls_grade** | `high, medium, unsafe` | Grade of TLS ciphers. `high` matches Qualys SSL Labs A+ (defaults to `medium`) |
| **proxy_address_http** | `0.0.0.0:6193` | Aralez HTTP bind address |
| **proxy_address_tls** | `0.0.0.0:6194` | Aralez HTTPS bind address (Optional) |
| **proxy_configs** | `/etc/aralez/` | Direcotry containing configuration files, must be writeable by user `aralez` |
| **upstreams_conf** | `/etc/aralez/upstreams.yaml` | Location of the upstreams file |
| **access_log** | `access` | Configure access logging. Values: `access, error` |
| **log_level** | `info` | Log level: `info`, `warn`, `error`, `debug`, `trace`, `off` |
| **log_file** | `/tmp/aralez.log, 20, 5, compress` | Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma. Defaults to None, 100, 5, No |
| **log_pattern** | `{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}` | Optional log pattern, defaults to `{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}`. See log4rs official [documentation](https://docs.rs/log4rs/latest/log4rs/encode/pattern/struct.PatternEncoderDeserializer.html#configuration) |
| **hc_method** | `HEAD` | Healthcheck method: HEAD, GET, POST (UPPERCASE) |
| **hc_interval** | `2` | Interval for health checks in seconds |
| **file_server_folder** | `/some/local/folder` | Optional. Local folder to serve |
| **file_server_address** | `127.0.0.1:3002` | Optional. Local address for file server |
| **config_api_enabled** | `true` | Enable/disable remote config push capability |
| **cache_size_mb** | `50` | Optional. Enable internal cache with size in megabytes of value. Cache is entirely disabled when this line is commented out. |
| **cache_enabled** | `bool` | Optional. Disabled by default |
| **cache_storage** | `memory, redis://host:port` | Optional Defaults to memory works with Redis, Valkey, Dragonfly (requires redis:// prefix) |
| **cache_ttl** | `60` | Optional. Set internal cache with size in megabytes of value. Defaults to 50Mb if cache_enabled: true |
| **tcp_keepalive_idle** | `60` | Seconds of inactivity before the kernel starts sending keepalive probes to a downstream client. Defaults to 60 |
| **tcp_keepalive_interval** | `10` | Seconds between individual keepalive probes if the client does not respond. Defaults to 10 |
| **tcp_keepalive_count** | `5` | Number of unanswered probes before the kernel declares the connection dead and closes it. Defaults to 5 |
| **cluster_member** | `master, worker` | Optional. Enables cluster mode, Sets node type to master or worker. Disabled by default |
| **worker_nodes** | `http(s)://host:port,http(s)://host:port` | Required is if cluster_member: master. Comma separated list of worker nodes http(s)://host:port |
| **log_structured** | `elasticsearch` | Optional. Enables structured logging and sets backend type. Only elasticsearch et the moment. |
| **acme_dns_provider** | `cloudflare, route53, digitalocean, etc` | Optional. Enable ACME DNS-01 challenge, sets DNS provider. ONly coudflare, route53, digitalocean at the moment. |
---
## Installation
Download the prebuilt binary for your architecture from releases section of [GitHub](https://github.com/sadoyan/aralez/releases) repo Make the binary executable `chmod 755 ./aralez-VERSION` and run.
File names:
| File Name | Description |
|--------------------------|---------------------------------------------------------------|
| `gazan-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency |
| `gazan-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies |
| `gazan-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency |
| `gazan-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies |
| File Name | Description |
|---------------------------------|----------------------------------------------------------------------------|
| `aralez-x86_64-musl.gz` | Static Linux x86_64 binary, without any system dependency |
| `aralez-x86_64-glibc.gz` | Dynamic Linux x86_64 binary, with minimal system dependencies |
| `aralez-x86_64-compat-musl.gz` | Static Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `aralez-x86_64-compat-glibc.gz` | Dynamic Linux x86_64 binary, compatible with old pre Haswell CPUs |
| `aralez-aarch64-musl.gz` | Static Linux ARM64 binary, without any system dependency |
| `aralez-aarch64-glibc.gz` | Dynamic Linux ARM64 binary, with minimal system dependencies |
| `sadoyan/aralez` | Docker image on Debian 13 slim (<https://hub.docker.com/r/sadoyan/aralez>) |
## 🔌 Running the Proxy
## About binaries
```bash
./gazan -c path/to/main.yaml
**glibc** builds are in general faster, but have few, basic, Glibc dependencies:
**musl** builds are 100% portable, static compiled binaries and have zero system dependencies. In general musl builds have a little less performance.
The most intensive tests shows 107k-110k requests per second on **Glibc** binaries against 97k-100k **Musl** ones.
For running **Aralez** on very old hardware, CPUs prior Haswell, (launched before 2013) use `aralez-x86_64-compat-*.gz`
For getting the best performance on newer hardware use `aralez-x86_64-*.gz`.
**Via docker**
```shell
docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 sadoyan/aralez
docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 sadoyan/aralez:compat
docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 ghcr.io/sadoyan/aralez:latest
docker run -d -v /path/to/config:/etc/aralez:rw -p 80:80 -p 443:443 ghcr.io/sadoyan/aralez:compat
```
## 🔌 Systemd integration
**Dockerfile :**
```dockerfile
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y ca-certificates curl net-tools iputils-ping
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
COPY aralez /usr/local/bin/aralez
RUN chmod +x /usr/local/bin/aralez
RUN mkdir -p /etc/aralez/certs/upstreams
WORKDIR /etc/aralez
ENTRYPOINT ["/usr/local/bin/aralez", "-c", "/etc/aralez/main.yaml"]
```
## Running the Proxy
```bash
cat > /etc/systemd/system/gazan.service <<EOF
./aralez -c path/to/main.yaml
```
## Systemd integration
Assuming Aralez in installed in `/opt/aralez` folder
```bash
cat > /etc/systemd/system/aralez.service <<EOF
[Unit]
Description=meilisearch
Documentation=https://github.com/sadoyan/aralez
Wants=network-online.target
After=network-online.target
[Service]
Type=forking
PIDFile=/run/gazan.pid
ExecStart=/bin/gazan -d -c /etc/gazan.conf
ExecReload=kill -QUIT $MAINPID
ExecReload=/bin/gazan -u -d -c /etc/gazan.conf
WorkingDirectory = /opt/aralez/
ExecReload=/bin/kill -HUP
ExecStart=/opt/aralez/aralez -c /opt/aralez/proxyconfigs/main.yaml
KillMode=process
KillSignal=SIGINT
LimitNOFILE=infinity
LimitNPROC=infinity
Restart=on-failure
RestartSec=2
StartLimitBurst=3
StartLimitIntervalSec=10
TasksMax=infinity
[Install]
WantedBy=multi-user.target
EOF
```
```bash
systemctl enable gazan.service.
systemctl restart gazan.service.
systemctl daemon-reload
systemctl enable aralez.service.
systemctl restart aralez.service.
```
## 💡 Example
A sample `upstreams.yaml` entry:
## Example upstreams config
```yaml
provider: "file"
stickysessions: false
globals:
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
authorization:
- "jwt"
- "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
sticky_sessions: 8600
to_https: false
rate_limit: 20
x4xx_limit: 20
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
client_headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
myhost.mydomain.com:
paths:
"/":
headers:
rate_limit: 10
x4xx_limit: 10
to_https: false
server_headers:
- "X-Something-Else:Foobar"
- "X-Another-Header:Hohohohoho"
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Hopaaaaaaaaaaaar"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
"/foo":
headers:
to_https: true
authorization:
type: "jwt"
data: "266463d1-210a-4787-9a81-4aacb37a8723"
client_headers:
- "X-Another-Header:Hohohohoho"
servers:
- "127.0.0.4:8443"
- "127.0.0.5:8443"
"/.well-known/acme-challenge":
healthcheck: false
servers:
- "127.0.0.1:8001"
DEFAULT:
paths:
"/":
servers:
- "127.0.0.1:3000"
```
**This means:**
- Sticky sessions are disabled globally. This setting applies to all upstreams.
- Sticky sessions are enabled globally. This setting applies to all upstreams. If enabled the value will be set for `Max-Age=` cookie.
- HTTP to HTTPS redirect disabled globally, but can be overridden by `to_https` setting per upstream.
- All upstreams will receive custom headers : `X-Forwarded-Proto:https` and `X-Forwarded-Port:443`
- Additionally, myhost.mydomain.com with path `/` will receive custom headers : `X-Another-Header:Hohohohoho` and `X-Something-Else:Foobar`
- Requests with response 4xx to each hosted domains will be limited to 20 requests per second per virtualhost.
- Requests limits are calculated per requester ip plus requested virtualhost.
- If the requester exceeds the limit it will receive `429 Too Many Requests` error.
- Optional. Rate limiter will be disabled if the parameter is entirely removed from config.
- Requests to each hosted domains will be limited to 20 requests per second per virtualhost.
- Requests limits are calculated per requester ip plus requested virtualhost.
- If the requester exceeds the limit it will receive `429 Too Many Requests` error.
- Optional. Rate limiter will be disabled if the parameter is entirely removed from config.
- Requests to `myhost.mydomain.com/` will be limited to 20 requests per second.
- Requests with 4xx responses to `myhost.mydomain.com/` will be limited to 10 requests per second.
- Requests to `myhost.mydomain.com/` will be proxied to `127.0.0.1` and `127.0.0.2`.
- Plain HTTP to `myhost.mydomain.com/foo` will get 301 redirect to configured TLS port of Aralez.
- `myhost.mydomain.com/foo` will require authentication with JWT token, signed by `266463d1-210a-4787-9a81-4aacb37a8723`.
- Requests to `myhost.mydomain.com/foo` will be proxied to `127.0.0.4` and `127.0.0.5`.
- Requests to `myhost.mydomain.com/.well-known/acme-challenge` will be proxied to `127.0.0.1:8001`, but healthcheks are disabled.
- SSL/TLS for upstreams is detected automatically, no need to set any config parameter.
- Assuming the `127.0.0.5:8443` is SSL protected. The inner traffic will use TLS.
- Self signed certificates are silently accepted.
- Global headers (CORS for this case) will be injected to all upstreams
- Self-signed certificates are silently accepted.
- Global headers (CORS for this case) will be injected to all upstreams.
- Additional headers will be injected into the request for `myhost.mydomain.com`.
- You can choose any path, deep nested paths are supported, the best match is chosen.
- All requests to servers will require JWT token authentication (You can comment out the authorization to disable it),
- Firs parameter specifies the mechanism of authorisation `jwt`
- Second is the secret key for validating `jwt` tokens
- You can choose any path, deep nested paths are supported, the best match chosen.
- `DEFAULT` catch up everything else and proxy to `127.0.0.1:3000`
- This is a special upstream and in order to do the catch-up jub it must be **DEFAULT** all capitals
---
## 🔄 Hot Reload
## Hot Reload
- Changes to `upstreams.yaml` are applied immediately.
- No need to restart the proxy — just save the file.
- If `consul` provider is chosen, upstreams will be periodically update from Consul's API.
- Changes to `upstreams.yaml` are applied immediately on save without restart .
- If `consul` or `kubernetes` provider is chosen, upstreams will be periodically update from API.
---
## 🔐 TLS Support
## TLS Support
To enable TLS for A proxy server: Currently only OpenSSL is supported, working on Boringssl and Rustls
To enable TLS for the proxy server.
1. Set `proxy_address_tls` in `main.yaml`
2. Provide `tls_certificate` and `tls_key_file`
- Set `proxy_address_tls` in `main.yaml`
- Provide at least one `tls_certificate/tls_key_file` pair.
- First pair is required to create the TLS listener.
- This pair can be anything, even self-signed with dummy domain.
- After getting normal certificate it can be deleted
```shell
mkdir -p /etc/aralez/certificates
chown -R aralez:aralez /etc/aralez
cd /etc/aralez/certificates
openssl req -x509 -newkey rsa:4096 \
-keyout dummy.key -out dummy.crt -sha256 -days 3650 -nodes \
-subj "/C=XX/ST=StateName/L=CityName/O=CompanyName/OU=CompanySectionName/CN=CommonNameOrHostname"
```
---
## 📡 Remote Config API
## Remote Config API
You can push new `upstreams.yaml` over HTTP to `config_address` (`:3000` by default). Useful for CI/CD automation or remote config updates.
Push new `upstreams.yaml` over HTTP to `config_address` (`127.0.0.1:3000` by default).
Useful for CI/CD automation or remote config updates.
```bash
curl -XPOST --data-binary @./etc/upstreams.txt 127.0.0.1:3000/conf
```
Don't run API on address 0.0.0.0 on production servers, keep it on 127.0.0.1 and configure appropriate upstream with supported authentication and TLS termination
Example:
```yaml
config.example.com:
paths:
"/conf":
authorization:
type: "basic"
data: "user:pass"
healthcheck: false
servers:
- "127.0.0.1:3000"
```
---
## 🔐 Authentication (Optional)
## Authentication (Optional)
- Adds authentication to all requests.
- Only one method can be active at a time.
- `basic` : Standard HTTP Basic Authentication requests.
- `apikey` : Authentication via `x-api-key` header, which should match the value in config.
- `jwt`: JWT authentication implemented via `gazantoken=` url parameter. `/some/url?gazantoken=TOKEN`
- `jwt`: JWT authentication implemented via `araleztoken=` url parameter. `/some/url?araleztoken=TOKEN`
- `jwt`: JWT authentication implemented via `Authorization: Bearer <token>` header.
- To obtain JWT token, you should send **generate** request to built in api server's `/jwt` endpoint.
- `masterkey`: should match configured `masterkey` in `main.yaml` and `upstreams.yaml`.
- To obtain JWT a token, you should send **generate** request to built in api server's `/jwt` endpoint.
- `master_key`: should match configured `masterkey` in `main.yaml` and `upstreams.yaml`.
- `owner` : Just a placeholder, can be anything.
- `valid` : Time in minutes during which the generated token will be valid.
**Example JWT token generateion request**
**Example JWT token generation request**
```bash
PAYLOAD='{
"masterkey": "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774",
"master_key": "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774",
"owner": "valod",
"valid": 10
}'
@@ -230,7 +370,7 @@ curl -H "Authorization: Bearer ${TOK}" -H 'Host: myip.mydomain.com' http://127.0
With URL parameter (Very useful if you want to generate and share temporary links)
```bash
curl -H 'Host: myip.mydomain.com' "http://127.0.0.1:6193/?gazantoken=${TOK}`"
curl -H 'Host: myip.mydomain.com' "http://127.0.0.1:6193/?araleztoken=${TOK}`"
```
**Example Request with API Key**
@@ -247,13 +387,13 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
```
## 📃 License
## License
[Apache License Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
---
## 🧠 Notes
## Notes
- Uses Pingora under the hood for efficiency and flexibility.
- Designed for edge proxying, internal routing, or hybrid cloud scenarios.
@@ -261,3 +401,197 @@ curl -u username:password -H 'Host: myip.mydomain.com' http://127.0.0.1:6193/
- Transparent, fully automatic gRPC proxy.
- Sticky session support.
- HTTP2 ready.
### Summary Table: Feature Comparison
| Feature / Proxy | **Aralez** | **Nginx** | **HAProxy** | **Traefik** | **Caddy** | **Envoy** |
|--------------------|:----------:|:-----------:|:-----------:|:-----------:|:----------:|:---------:|
| **Reload** | ✅ Hot | ⚙️ Manual | ⚙️ Manual | ✅ Hot | ✅ Hot | ✅ Hot |
| **Cert load** | ✅ Hot | ❌ Reload | ❌ Reload | ✅ Yes | ✅ Yes | ⚙️ No ? |
| **Authentication** | ✅ Yes | ⚙️ Limited | ⚙️ Limited | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP2** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **TLS Grades** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ⚙️ Manual |
| **gRPC** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual | ⚙️ Manual |
| **SSL Proxy** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **HTTP/2** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **WebSocket** | ✅ Auto | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Sticky Session** | ✅ Yes | ❌ No | ⚙️ Yes | ✅ Yes | ⚙️ Limited | ✅ Manual |
| **Prometheus** | ✅ Yes | ⚙️ External | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Consul** | ✅ Yes | ❌ No | ⚙️DNS API | ✅ Yes | ❌ No | ✅ Yes |
| **Kubernetes** | ✅ Yes | ⚙️ Ingress | ⚙️ External | ✅ Yes | ⚙️ Limited | ✅ Yes |
| **Limiter** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Static Files** | ✅ Yes | ✅ Yes | ⚙️ Lua ? | ✅ Yes | ✅ Yes | ❌ No |
| **Health Checks** | ✅ Yes | ⚙️ Manual | ⚙️ Manual | ✅ Yes | ✅ Yes | ✅ Yes |
| **Built With** | Rust | C | C | Go | Go | C++ |
---
✅ **Auto** – Automatically detected and loaded
✅ **Hot** – Works immediately, no reload/restart is required
✅ **Yes** – Works immediately, no setup required
⚙️ **Manual** – Requires explicit configuration or modules
⚙️ **Reload** – Reload or restart is required
⚙️ **Limited** – Support is limited to certain features
⚙️ **External** – Requires an external module
❌ **No** – Not supported
## Simple benchmark by [Oha](https://github.com/hatoo/oha)
**These benchmarks use :**
- 3 async Rust echo servers on a local network with 1Gbit as upstreams.
- A dedicated server for running **Aralez**
- A dedicated server for running **Oha**
- The following upstreams configuration.
- 9 test URLs from simple `/` to nested up to 7 subpaths.
```yaml
myhost.mydomain.com:
paths:
"/":
to_https: false
headers:
- "X-Proxy-From:Aralez"
servers:
- "192.168.211.211:8000"
- "192.168.211.212:8000"
- "192.168.211.213:8000"
"/ping":
to_https: false
headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
servers:
- "192.168.211.211:8000"
- "192.168.211.212:8000"
```
## Results reflect synthetic performance under optimal conditions.
- CPU : Intel (R) Xeon (R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections
- Duration : 10 Minutes
- Binary : aralez-x86_64-glibc
```
Summary:
Success rate: 100.00%
Total: 600.0027 secs
Slowest: 0.2138 secs
Fastest: 0.0002 secs
Average: 0.0023 secs
Requests/sec: 129777.3838
Total data: 0 B
Size/request: 0 B
Size/sec: 0 B
Response time histogram:
0.000 [1] |
0.022 [77668026] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.043 [190362] |
0.064 [7908] |
0.086 [319] |
0.107 [4] |
0.128 [0] |
0.150 [0] |
0.171 [0] |
0.192 [0] |
0.214 [4] |
Response time distribution:
10.00% in 0.0012 secs
25.00% in 0.0016 secs
50.00% in 0.0020 secs
75.00% in 0.0026 secs
90.00% in 0.0033 secs
95.00% in 0.0040 secs
99.00% in 0.0078 secs
99.90% in 0.0278 secs
99.99% in 0.0434 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0161 secs, 0.0002 secs, 0.0316 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
Status code distribution:
[200] 77866624 responses
Error distribution:
[158] aborted due to deadline
```
![Aralez](https://netangels.net/utils/glibc10.png)
- CPU : Intel (R) Xeon (R) CPU E3-1270 v6 @ 3.80GHz
- 300 : simultaneous connections
- Duration : 10 Minutes
- Binary : aralez-x86_64-musl
```
Summary:
Success rate: 100.00%
Total: 600.0021 secs
Slowest: 0.2182 secs
Fastest: 0.0002 secs
Average: 0.0024 secs
Requests/sec: 123870.5820
Total data: 0 B
Size/request: 0 B
Size/sec: 0 B
Response time histogram:
0.000 [1] |
0.022 [74254679] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
0.044 [61400] |
0.066 [5911] |
0.087 [385] |
0.109 [0] |
0.131 [0] |
0.153 [0] |
0.175 [0] |
0.196 [0] |
0.218 [1] |
Response time distribution:
10.00% in 0.0012 secs
25.00% in 0.0016 secs
50.00% in 0.0021 secs
75.00% in 0.0028 secs
90.00% in 0.0037 secs
95.00% in 0.0045 secs
99.00% in 0.0077 secs
99.90% in 0.0214 secs
99.99% in 0.0424 secs
Details (average, fastest, slowest):
DNS+dialup: 0.0066 secs, 0.0002 secs, 0.0210 secs
DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs
Status code distribution:
[200] 74322377 responses
Error distribution:
[228] aborted due to deadline
```
![Aralez](https://netangels.net/utils/musl10.png)
## Aralez, Nginx, Traefik performance benchmark
This benchmark is done on 4 servers. With CPU Intel (R) Xeon (R) E-2174G CPU @ 3.80GHz, 64 GB RAM.
1. Sever runs Aralez, Traefik, Nginx on different ports. Tuned as much as I could .
2. 3x Upstreams servers, running Nginx. Replying with dummy json hardcoded in config file for max performance.
All servers are connected to the same switch with 1GB port in datacenter , not a home lab. The results:
![Aralez](https://raw.githubusercontent.com/sadoyan/aralez/refs/heads/main/assets/bench.png)
The results show requests per second performed by Load balancer. You can see 3 batches with 800 concurrent users.
1. Requests via http1.1 to plain text endpoint.
2. Requests to via http2 to SSL endpoint.
3. Mixed workload with plain http1.1 and htt2 SSL.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

+23
View File
@@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/
+18
View File
@@ -0,0 +1,18 @@
# yaml-language-server: $schema=https://json.schemastore.org/chart.json
apiVersion: v2
name: aralez
description: Ingress controller for Kubernetes
type: application
home: https://github.com/sadoyan/aralez/charts
sources:
- https://github.com/sadoyan/helm-charts/tree/main/charts/aralez
keywords:
- ingress-controller
- aralez
maintainers: []
#
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.0.1
#
# renovate: datasource=docker depName=ghcr.io/sadoyan/aralez
appVersion: 0.93.3
+17
View File
@@ -0,0 +1,17 @@
{{ template "chart.header" . }}
{{ template "chart.deprecationWarning" . }}
{{ template "chart.badgesSection" . }}
{{ template "chart.description" . }}
{{ template "chart.homepageLine" . }}
{{ template "chart.maintainersSection" . }}
{{ template "chart.sourcesSection" . }}
{{ template "chart.requirementsSection" . }}
{{ template "chart.valuesSection" . }}
+18
View File
@@ -0,0 +1,18 @@
ingressClass: aralez
ingressClassResource:
enabled: true
# hostNetwork: true
# hostPort:
# ports:
# http: 8080
# https: 8443
priorityClassName: system-cluster-critical
env: []
args: []
useDaemonSet: true
terminationGracePeriodSeconds: 120
View File
+104
View File
@@ -0,0 +1,104 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "aralez.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "aralez.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "aralez.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "aralez.labels" -}}
helm.sh/chart: {{ include "aralez.chart" . }}
{{ include "aralez.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- with .Values.podLabels }}
{{ toYaml . }}
{{- end }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "aralez.selectorLabels" -}}
app.kubernetes.io/name: {{ include "aralez.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Return a podAffinity/podAntiAffinity definition
*/}}
{{- define "affinities.pods" -}}
{{- if eq .Values.podAntiAffinityPreset "soft" }}
{{- include "affinities.pods.soft" . -}}
{{- else if eq .Values.podAntiAffinityPreset "hard" }}
{{- include "affinities.pods.hard" . -}}
{{- end -}}
{{- end -}}
{{/*
Return a soft podAntiAffinity definition
*/}}
{{- define "affinities.pods.soft" -}}
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchLabels: {{- (include "aralez.selectorLabels" .) | nindent 12 }}
namespaces:
- {{ .Release.Namespace | quote }}
topologyKey: kubernetes.io/hostname
weight: 1
{{- end -}}
{{/*
Return a hard podAntiAffinity definition
*/}}
{{- define "affinities.pods.hard" -}}
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels: {{- (include "aralez.selectorLabels" .) | nindent 10 }}
namespaces:
- {{ .Release.Namespace | quote }}
topologyKey: kubernetes.io/hostname
{{- end -}}
{{/*
Create the name of the service account to use
*/}}
{{- define "aralez.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "aralez.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
+21
View File
@@ -0,0 +1,21 @@
{{- $ca := genCA .Chart.Name 3650 }}
{{- $server := genSignedCert "server" (list "127.0.0.1") nil 3650 $ca }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "aralez.fullname" . }}-tls
labels:
{{- include "aralez.labels" . | nindent 4 }}
type: kubernetes.io/tls
data:
{{- $secretName := printf "%s-tls" (include "aralez.fullname" .) }}
{{- $secretData := (lookup "v1" "Secret" .Release.Namespace $secretName).data }}
{{- if and $secretData (hasKey $secretData "ca.crt") }}
ca.crt: {{ get $secretData "ca.crt" }}
tls.crt: {{ get $secretData "tls.crt" }}
tls.key: {{ get $secretData "tls.key" }}
{{- else }}
ca.crt: {{ $ca.Cert | b64enc }}
tls.crt: {{ $server.Cert | b64enc }}
tls.key: {{ $server.Key | b64enc }}
{{- end }}
+28
View File
@@ -0,0 +1,28 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
data:
aralez.yaml: |
daemon: false
upstreams_conf: /etc/aralez/upstreams.yaml
proxy_configs: /etc/aralez-proxy
proxy_address_http: "0.0.0.0:{{ .Values.hostPort.ports.http }}"
proxy_address_tls: "0.0.0.0:{{ .Values.hostPort.ports.https }}"
config_api_enabled: false
config_address: "0.0.0.0:3000"
upgrade_sock: /tmp/aralez.sock
pid_file: /tmp/aralez.pid
log_level: debug
hc_method: HEAD
hc_interval: 5
upstreams.yaml: |
provider: "kubernetes"
sticky_sessions: 172000
kubernetes: {}
{{- with .Values.upstreams }}
{{- toYaml . | nindent 4 }}
{{- end }}
+130
View File
@@ -0,0 +1,130 @@
apiVersion: apps/v1
{{- if .Values.useDaemonSet }}
kind: DaemonSet
{{- else }}
kind: Deployment
{{- end }}
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
spec:
{{- if not (or .Values.autoscaling.enabled .Values.useDaemonSet) }}
replicas: {{ .Values.replicaCount }}
{{- end }}
{{- if not .Values.useDaemonSet }}
strategy:
type: {{ .Values.updateStrategy.type }}
{{- else }}
updateStrategy:
type: {{ .Values.updateStrategy.type }}
{{- end }}
selector:
matchLabels:
{{- include "aralez.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "aralez.labels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
annotations:
{{- if .Values.upstreams }}
checksum/upstreams: {{ toJson .Values.upstreams | sha256sum }}
{{- end }}
spec:
enableServiceLinks: false
{{- if and .Values.hostNetwork .Values.useDaemonSet }}
hostNetwork: {{ .Values.hostNetwork }}
dnsPolicy: "ClusterFirstWithHostNet"
{{- end }}
{{- if .Values.priorityClassName }}
priorityClassName: {{ .Values.priorityClassName }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "aralez.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- if .Values.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: ["aralez"]
args:
- --conf=/etc/aralez/aralez.yaml
{{- range $value := .Values.args }}
- {{ $value }}
{{- end }}
env:
{{- with .Values.env }}
{{- . | toYaml | nindent 12 }}
{{- end }}
ports:
{{- if .Values.hostPort.ports.http }}
- name: http
containerPort: {{ .Values.hostPort.ports.http }}
protocol: TCP
{{- end }}
{{- if .Values.hostPort.ports.https }}
- name: https
containerPort: {{ .Values.hostPort.ports.https }}
protocol: TCP
{{- end }}
# readinessProbe:
# httpGet:
# path: /status
# port: 3000
# initialDelaySeconds: 30
# timeoutSeconds: 5
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config
mountPath: /etc/aralez
- name: tlscerts
mountPath: /etc/aralez-proxy/certificates
{{- with .Values.volumeMounts }}
{{- toYaml . | nindent 12 }}
{{- end }}
volumes:
- name: config
configMap:
name: {{ include "aralez.fullname" . }}
- name: tlscerts
secret:
secretName: {{ include "aralez.fullname" . }}-tls
defaultMode: 256
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.key
{{- with .Values.volumes }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
affinity:
{{- include "affinities.pods" . | nindent 8 }}
{{- with .Values.affinity }}
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
+24
View File
@@ -0,0 +1,24 @@
{{- if and .Values.autoscaling.enabled (not .Values.useDaemonSet) }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "aralez.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- end }}
+10
View File
@@ -0,0 +1,10 @@
{{- if .Values.ingressClassResource.enabled -}}
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
labels:
{{- include "aralez.labels" . | nindent 4 }}
name: {{ .Values.ingressClass }}
spec:
controller: {{ .Values.ingressClassResource.controllerValue }}
{{- end }}
+41
View File
@@ -0,0 +1,41 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
{{- with .Values.service.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
type: {{ .Values.service.type }}
{{- if eq .Values.service.type "NodePort" }}
externalTrafficPolicy: Local
{{- end }}
{{- with .Values.service.ipFamilies }}
ipFamilies:
{{- toYaml . | nindent 4 }}
{{- end }}
ipFamilyPolicy: {{ if eq (len .Values.service.ipFamilies) 2 }}PreferDualStack{{ else }}SingleStack{{ end }}
ports:
{{- if .Values.hostPort.ports.http }}
- port: 80
targetPort: http
protocol: TCP
name: http
{{- if .Values.hostPort.enabled }}
nodePort: {{ .Values.hostPort.ports.http }}
{{- end }}
{{- end }}
{{- if .Values.hostPort.ports.https }}
- port: 443
targetPort: https
protocol: TCP
name: https
{{- if .Values.hostPort.enabled }}
nodePort: {{ .Values.hostPort.ports.https }}
{{- end }}
{{- end }}
selector:
{{- include "aralez.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,52 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "aralez.serviceAccountName" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
automountServiceAccountToken: true
{{- end }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["get", "update", "patch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["nodes", "services", "endpoints"]
verbs: ["get", "list", "watch"]
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "aralez.fullname" . }}
labels:
{{- include "aralez.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "aralez.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "aralez.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
+164
View File
@@ -0,0 +1,164 @@
# Default values for aralez.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
replicaCount: 1
image:
repository: ghcr.io/sadoyan/aralez
pullPolicy: IfNotPresent
# Overrides the image tag whose default is the chart appVersion.
tag: ""
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
# -- Environment variables
env: []
# -- Aralez arguments
args: []
# -- Aralez upstreams file body
# refs: https://aralez.rs/docs/kubernetes/
upstreams:
rate_limit: 500000
x4xx_limit: 100000
to_https: true
server_headers:
- "X-Forwarded-Proto:https"
- "X-Forwarded-Port:443"
serviceAccount:
# Specifies whether a service account should be created
create: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""
# -- Annotations for pod.
# ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/
podAnnotations: {}
# cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
# -- Extra labels for pod.
# ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
podLabels: {}
# -- Pod Security Context.
# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
podSecurityContext:
runAsNonRoot: true
runAsUser: 9999
runAsGroup: 9999
fsGroup: 9999
fsGroupChangePolicy: "OnRootMismatch"
# -- Container Security Context.
# ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod
securityContext:
seccompProfile:
type: RuntimeDefault
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
# -- Pod host network
# Use it only for performance reason
hostNetwork: false
# -- Service node port
# refs: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport
hostPort:
enabled: false
ports:
http: 8080
https: 8443
ingressClass: aralez
ingressClassResource:
enabled: true
controllerValue: aralez.proxy/ingress-controller
# -- Bitwarden service parameters
# ref: https://kubernetes.io/docs/user-guide/services/
service:
# -- service type
type: ClusterIP
annotations: {}
# -- IP families for service
# possible values: IPv4, IPv6
# ref: https://kubernetes.io/docs/concepts/services-networking/dual-stack/
ipFamilies: [IPv4]
# -- Resource requests and limits.
# ref: https://kubernetes.io/docs/user-guide/compute-resources/
resources:
limits:
cpu: 1
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
# -- Horizontal and Vertical pod autoscaler.
# ref: https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 80
# -- Use a daemonset instead of a deployment
useDaemonSet: false
# -- The time in seconds that is allowed for a Aralez instance to gracefully shutdown
# ref: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks
terminationGracePeriodSeconds: 120
## updateStrategy for PostgreSQL StatefulSet and its slaves StatefulSets
## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
# -- Priority Class Name
# ref: https://kubernetes.io/docs/concepts/configuration/pod-priority-preemption/#priorityclass
priorityClassName: ""
# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
# secret:
# secretName: mysecret
# optional: false
# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true
# -- Node labels for pod assignment.
# ref: https://kubernetes.io/docs/user-guide/node-selection/
nodeSelector: {}
# -- Tolerations for pod assignment.
# ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
tolerations: []
# -- Pod Anti Affinity
# soft/hard
podAntiAffinityPreset: soft
# -- Affinity for pod assignment.
# ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
affinity: {}
+66
View File
@@ -0,0 +1,66 @@
job "apache" {
meta {
uuid = uuidv4()
}
datacenters = ["eu-ams-1"]
type = "service"
constraint {
attribute = node.unique.name
operator = "regexp"
value = "consul[0-9]"
}
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
progress_deadline = "10m"
auto_revert = false
canary = 0
}
migrate {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
}
group "Apache" {
network {
port "apache" { to = 80 }
}
constraint {
operator = "distinct_hosts"
value = "true"
}
count = 3
restart {
attempts = 2
interval = "30m"
delay = "15s"
mode = "fail"
}
task "health" {
service {
port = "apache"
tags = [
"aralez.service=yes",
"aralez.host=apache.example.com",
"aralez.path=/",
"aralez.redirect=http://checkip.amazonaws.com"
]
check {
type = "http"
port = "apache"
path = "/"
interval = "5s"
timeout = "2s"
}
}
driver = "docker"
config {
image = "httpd:latest"
force_pull = true
ports = ["apache"]
}
}
}
}
+70
View File
@@ -0,0 +1,70 @@
job "echo" {
meta {
uuid = uuidv4()
}
datacenters = ["eu-ams-1"]
type = "service"
constraint {
attribute = node.unique.name
operator = "regexp"
value = "consul[0-9]"
}
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
progress_deadline = "10m"
auto_revert = false
canary = 0
}
migrate {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
}
group "Echo" {
network {
port "echo" { to = 80 }
}
constraint {
operator = "distinct_hosts"
value = "true"
}
count = 3
restart {
attempts = 2
interval = "30m"
delay = "15s"
mode = "fail"
}
task "server" {
service {
port = "echo"
tags = [
"aralez.service=yes",
"aralez.host=echo.example.com",
"aralez.path=/",
"aralez.rate=20",
"aralez.4xx_rate=10",
"aralez.to_https=true",
"aralez.client_header=X-Client-Header:Some simple header",
"aralez.server_header=X-Server-Header:Some simple header",
]
check {
type = "http"
port = "echo"
path = "/"
interval = "5s"
timeout = "2s"
}
}
driver = "docker"
config {
image = "ealen/echo-server:latest"
force_pull = true
ports = ["echo"]
}
}
}
}
+72
View File
@@ -0,0 +1,72 @@
job "nginx" {
meta {
uuid = uuidv4()
}
datacenters = ["eu-ams-1"]
type = "service"
constraint {
attribute = node.unique.name
operator = "regexp"
value = "consul[0-9]"
}
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
progress_deadline = "10m"
auto_revert = false
canary = 0
}
migrate {
max_parallel = 1
health_check = "checks"
min_healthy_time = "10s"
healthy_deadline = "5m"
}
group "NginX" {
network {
port "nginx" { to = 80 }
}
constraint {
operator = "distinct_hosts"
value = "true"
}
count = 3
restart {
attempts = 2
interval = "30m"
delay = "15s"
mode = "fail"
}
task "server" {
service {
port = "nginx"
tags = [
"aralez.service=yes",
"aralez.host=nginx.example.com",
"aralez.path=/",
"aralez.rate=1",
"aralez.4xx_rate=10",
"aralez.to_https=true",
"aralez.client_header=X-Some:Some Random Header",
"aralez.client_header=X-Emos:Redaeh Modnar Emos",
"aralez.server_header=X-From:Aralez & Consul",
"aralez.server_header=X-Mrof: Lusnoc & Zelara",
]
check {
type = "http"
port = "nginx"
path = "/"
interval = "5s"
timeout = "2s"
}
}
driver = "docker"
config {
image = "nginx:latest"
force_pull = true
ports = ["nginx"]
}
}
}
}
+8
View File
@@ -0,0 +1,8 @@
# 1. Define the IngressClass so Kubernetes knows "aralez" is a valid controller
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: aralez
namespace: upstreams
spec:
controller: aralez.proxy/ingress-controller
+76
View File
@@ -0,0 +1,76 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: aralez
namespace: aralez
labels:
app.kubernetes.io/name: aralez
app.kubernetes.io/component: ingress-controller
spec:
replicas: 1
selector:
matchLabels:
app: aralez
template:
metadata:
labels:
app: aralez
spec:
serviceAccountName: aralez-sa
containers:
- name: aralez
#image: sadoyan/aralez:latest
image: nginx:latest
imagePullPolicy: IfNotPresent
env:
- name: ARALEZ_SERVICE_NAME
value: "aralez-service"
- name: ARALEZ_SERVICE_NAMESPACE
value: "aralez"
- name: ARALEZ_EXTERNAL_IP # Only for bare metal deployment, remove this in Cloud.
value: "192.168.10.10" # Only for bare metal deployment, remove this in Cloud.
ports:
- name: http
containerPort: 80
- name: https
containerPort: 443
readinessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
tcpSocket:
port: 80
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "1"
memory: "512Mi"
volumeMounts:
- name: main-config
mountPath: /etc/aralez/main.yaml
subPath: main.yaml
readOnly: true
- name: upstreams-config
mountPath: /etc/aralez/upstreams.yml
subPath: upstreams.yml
readOnly: true
- name: tls-certs
mountPath: /etc/aralez/certificates
readOnly: true
volumes:
- name: main-config
configMap:
name: aralez-main-config
- name: upstreams-config
configMap:
name: aralez-upstreams-config
- name: tls-certs
secret:
secretName: aralez-tls
+17
View File
@@ -0,0 +1,17 @@
apiVersion: v1
kind: Service
metadata:
name: aralez-service
namespace: aralez
spec:
type: NodePort
selector:
app: aralez
ports:
- name: http
port: 80
targetPort: 80
- name: https
port: 443
targetPort: 443
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Secret
metadata:
name: aralez-tls
namespace: aralez
type: kubernetes.io/tls
data:
tls1.crt: "<FIRST_BASE64_CRT_HERE>"
tls1.key: "<FIRST_BASE64_KEY_HERE>"
tls2.crt: "<SECOND_BASE64_CRT_HERE>"
tls3.key: "<BASE64_BASE64_KEY_HERE>"
+73
View File
@@ -0,0 +1,73 @@
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: api-service-v2
namespace: upstreams
annotations:
# Custom annotations parsed by Aralez
aralez.rs/rate_limit: "100"
aralez.rs/x4xx_limit: "20"
aralez.rs/client_headers: '["X-Proxy-From:Aralezzzzzzzzzzz", "X-Hopar-From:Hopaaaaaaaaaaaar"]'
aralez.rs/server_headers: '["X-Proxy-Version:Aralez", "X-Po-From:Kung Fu Panda"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: api-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: api-service-v2
port:
number: 80
---
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: wss-service-v2
namespace: upstreams
annotations:
# Custom annotations parsed by Aralez
aralez.rs/rate_limit: "50"
aralez.rs/x4xx_limit: "10"
aralez.rs/client_headers: '["X-Hors-Arev:Naturi", "X-Grand-Master:Oogway"]'
aralez.rs/server_headers: '["X-Mors-Arev:Hastat", "X-Shifu-Master:Shifu"]'
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: wss-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
---
# Define your Ingress resource targeting Aralez
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: joj-service-v2
namespace: upstreams
spec:
ingressClassName: aralez # Matches the IngressClass metadata.name above
rules:
- host: joj-service-v2.blabla.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-nginx
port:
number: 80
+16
View File
@@ -0,0 +1,16 @@
threads: 12
daemon: false
upstream_keepalive_pool_size: 500
pid_file: /tmp/aralez.pid
error_log: /tmp/aralez_err.log
upgrade_sock: /tmp/aralez.sock
config_api_enabled: false
config_address: 127.0.0.1:3000
proxy_address_http: 0.0.0.0:80
proxy_address_tls: 0.0.0.0:443
proxy_configs: /etc/aralez
proxy_tls_grade: high
upstreams_conf: /etc/aralez/upstreams.yml
log_level: info
hc_method: HEAD
hc_interval: 2
+22
View File
@@ -0,0 +1,22 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: svc-nginx
namespace: upstreams
labels:
app: svc-nginx
spec:
selector:
matchLabels:
app: svc-nginx
replicas: 3
template:
metadata:
labels:
app: svc-nginx
spec:
containers:
- name: svc-nginx
image: nginx:latest
ports:
- containerPort: 80
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: svc-nginx
namespace: upstreams
spec:
selector:
app: svc-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: api-service-v2
namespace: upstreams
spec:
selector:
app: svc-nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
+40
View File
@@ -0,0 +1,40 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: aralez-sa
namespace: aralez
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: aralez-role
rules:
# Watch Ingresses and IngressClasses across all namespaces
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingressclasses"]
verbs: ["get", "list", "watch"]
# Update Ingress status (for setting load balancer IPs)
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses/status"]
verbs: ["update", "patch"]
# Watch Services and Endpoints across all namespaces
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
# Watch EndpointSlices across all namespaces
- apiGroups: ["discovery.k8s.io"]
resources: ["endpointslices"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: aralez-ingress
subjects:
- kind: ServiceAccount
name: aralez-sa
namespace: aralez # <--- MUST match where Aralez is deployed
roleRef:
kind: ClusterRole
name: aralez-role
apiGroup: rbac.authorization.k8s.io
+16
View File
@@ -0,0 +1,16 @@
provider: "kubernetes"
to_https: false
sticky_sessions: 86000
rate_limit: 100
x4xx_limit: 100
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
kubernetes:
services:
- hostname: "svc-nginx"
upstream: "svc-nginx"
path: "/"
tokenpath: "/var/run/secrets/kubernetes.io/serviceaccount/token"
+30 -15
View File
@@ -1,19 +1,34 @@
# Main configuration file , applied on startup
threads: 12 # Nubber of daemon threads default setting
#user: pastor # Username for running gazan after dropping root privileges, requires program to start as root
#group: pastor # Group for running gazan after dropping root privileges, requires program to start as root
daemon: false # Run in background
# Main configuration file, applied on startup
threads: 12 # Number of daemon threads default setting
runuser: aralez # Username for running aralez after dropping root privileges, requires program to start as root
rungroup: aralez # Group for running aralez after dropping root privileges, requires program to start as root
upstream_keepalive_pool_size: 500 # Pool size for upstream keepalive connections
pid_file: /tmp/gazan.pid # Path to PID file
error_log: /tmp/gazan_err.log # Path to error log
upgrade_sock: /tmp/gazan.sock # Path to socket file
config_address: 0.0.0.0:3000 # HTTP API address for pushing upstreams.yaml from remote location
proxy_address_http: 0.0.0.0:6193 # Proxy HTTP bind address
proxy_address_tls: 0.0.0.0:6194 # Optional, Proxy TLS bind address
tls_certificate: etc/server.crt # Mandatory if proxy_address_tls is set
tls_key_file: etc/key.pem # Mandatory if proxy_address_tls is set
upstreams_conf: etc/upstreams.yaml # the location of upstreams file
pid_file: /tmp/aralez.pid # Path to PID file
#error_log: /tmp/aralez_err.log # Path to error log
upgrade_sock: /tmp/aralez.sock # Path to socket file
cache_enabled: true # Optional. Disabled by default
cache_storage: memory # memory | redis://host:port . Optional Defaults to memory works with Redis, Valkey, Dragonfly (requires redis:// prefix)
cache_size_mb: 50 # Optional. Set internal cache with size in megabytes of value. Defaults to 50Mb if cache_enabled: true
cache_ttl: 60 # Optional. Set Default TTL in seconds for cached items, works if cache control headers are not set via upstream. Defaults to 60 if cache_enabled: true
config_api_enabled: true # Boolean to enable/disable remote config push capability.
config_address: 127.0.0.1:3000 # HTTP API address for pushing upstreams.yaml from remote location
proxy_address_http: 0.0.0.0:80 # Proxy HTTP bind address
proxy_address_tls: 0.0.0.0:443 # Optional, Proxy TLS bind address
proxy_configs: /opt/aralez/etc # Mandatory if proxy_address_tls set, should contain a certificate and key files strictly in a format {NAME}.crt, {NAME}.key.
proxy_tls_grade: high # Grade of TLS suite for proxy (high, medium, unsafe), matching grades of Qualys SSL Labs
upstreams_conf: /opt/aralez/etc/upstreams.yaml # the location of upstreams file
file_server_folder: /opt/aralez/public # Optional, local folder to serve
file_server_address: 127.0.0.1:3002 # Optional, Local address for file server. Can set as upstream for public access.
log_level: info # info, warn, error, debug, trace, off
log_file: /tmp/aralez.log, 20, 5, compress # Optional, the location of log file, file size in megabytes, number of log files to keep, compress files .valueas are separated by comma.
log_pattern: "{d(%Y-%m-%d %H:%M:%S)} {h({l})} {t} - {m} {n}" # Optional log pattern, defaults to "{d(%Y-%m-%d %H:%M:%S)} {l} - {m}{n}"
access_log: error # all, error, (Off by default)
hc_method: HEAD # Healthcheck method (HEAD, GET, POST are supported) UPPERCASE
hc_interval: 2 #Interval for health checks in seconds
master_key: 910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774 # Mater key for working with API server and JWT Secret
tcp_keepalive_idle: 60 # Seconds of inactivity before the kernel starts sending keepalive probes to a downstream client
tcp_keepalive_interval: 10 # Seconds between individual keepalive probes if the client does not respond
tcp_keepalive_count: 5 # Number of unanswered probes before the kernel declares the connection dead and closes it
#cluster_member: master | worker Optional. Enables cluster mode, Sets node type to master or worker. Disabled by default
#worker_nodes: http://127.0.0.1:8000, http://127.0.0.1:4000 Required is if cluster_member: master. Comma separated list of worker nodes http(s)://host:port
#log_structured: elasticsearch Optional. Enables structured logging and sets backend type. Only elasticsearch et the moment.
#acme_dns_provider: cloudflare Optional. Enable ACME DNS-01 challenge, sets DNS provider. ONly coudflare, route53, digitalocean at the moment.
-19
View File
@@ -1,19 +0,0 @@
provider: "file"
globals:
headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "X-Custom-Header:Something Special"
upstreams:
myip.netangels.net:
paths:
"/":
ssl: false
headers:
- "X-Proxy-From:Gazan"
servers:
- "192.168.221.213:8000"
- "192.168.221.214:8000"
- "192.168.221.210:8000"
- "192.168.221.212:8000"
+121 -79
View File
@@ -1,95 +1,137 @@
# The file is under watch and hot reload , changes are applied immediately, no need to restart or reload
provider: "file" # consul
stickysessions: true
globals:
headers: # Global headers, appended for all upstreams and all paths.
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "X-Custom-Header:Something Special"
# authorization: # Optional, only one of auth methods below can be active at a time
# - "basic"
# - "gazan:Gazanpass1234"
# - "apikey"
# - "5a28cc4c-ce10-4ff1-824e-743c38835f5c"
# - "jwt"
# - "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
consul: # If the provider is consul. Otherwise ignored
# The file under watch and hot reload, changes are applied immediately, no need to restart or reload.
provider: "file" # "file" "consul" "kubernetes"
sticky_sessions: 172000
to_https: false
rate_limit: 500000
x4xx_limit: 100000
server_headers:
- "Y-Global-Something: Something For Servers"
client_headers:
- "X-Global-Something: Something For Clients"
#authorization:
# type: "basic"
# data: "root:toor"
# type: "jwt"
# data: "910517d9-f9a1-48de-8826-dbadacbd84af-cb6f830e-ab16-47ec-9d8f-0090de732774"
# type: "apikey"
# data: "5ecbf799-1343-4e94-a9b5-e278af5cd313-56b45249-1839-4008-a450-a60dc76d2bae"
consul:
servers:
- "http://master1:8500"
- "http://192.168.22.1:8500"
- "http://master1.digitai.local:8500"
services: # proxy: The hostname to access proxy server, real : The real service name in Consul
- proxy: "proxy-frontend-dev-frontend-srv"
real: "frontend-dev-frontend-srv"
# - proxy: "proxy-gateway-test-gateway-srv"
# real: "gateway-test-gateway-srv"
# - proxy: "proxy-backoffice-dev-backoffice-srv"
# real: "backoffice-dev-backoffice-srv"
- "http://consul1:8500"
services: # hostname: The hostname to access the proxy server, upstream : The real service name in Consul database.
- hostname: "nconsul"
upstream: "nginx-consul-NginX-health"
path: "/one"
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
rate_limit: 1
to_https: false
- hostname: "nconsul"
upstream: "nginx-consul-NginX-health"
path: "/"
token: "8e2db809-845b-45e1-8b47-2c8356a09da0-a4370955-18c2-4d6e-a8f8-ffcc0b47be81" # Consul server access token, If Consul auth is enabled
upstreams: # If provider is files. Otherwise ignored
myip.netangels.net: # Hostname, or header host to access the upstream
paths: # URL path(s) for current upstream, closest match wins
kubernetes:
servers:
- "172.16.0.11:5443" # Gets KUBERNETES_SERVICE_HOST : KUBERNETES_SERVICE_PORT_HTTPS env variables.
services:
- hostname: "api-service"
upstream: "api-service"
path: "/"
- hostname: "api-service"
upstream: "console-service"
path: "/one"
client_headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Aralez"
rate_limit: 100
to_https: false
- hostname: "api-service"
upstream: "feed-fanout-service"
path: "/two"
- hostname: "websocket-service"
upstream: "websocket-service"
path: "/"
tokenpath: "/opt/Rust/Projects/asyncweb/etc/kubetoken.txt" # Defaults to /var/run/secrets/kubernetes.io/serviceaccount/token
upstreams:
www.example.com:
paths:
"/":
headers: # Custom headers, set only for this Host and Path
- "X-Proxy-From:Gazan"
servers: # List of upstreams HOST:PORT
rate_limit: 50
x4xx_limit: 100
to_https: false
authorization:
type: "basic"
data: "root:toor"
server_headers:
- "Y-Proxy-Server-Some:Yaaaaaaaaaaaaaaa"
- "Y-Proxy-Server-From:Aralez"
- "Y-Proxy-Server-Vers:Aralez v-xxx"
client_headers:
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
- "127.0.0.3:8000"
- "127.0.0.4:8000"
"/ping":
headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
- "X-Proxy-From:Gazan"
to_https: true
client_headers:
- "X-Some-Thing:Something Else"
- "Access-Control-Allow-Origin:*"
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
- "Access-Control-Max-Age:86400"
- "Strict-Transport-Security:max-age=31536000; includeSubDomains; preload"
servers:
- "127.0.0.1:8000"
"/secret":
authorization:
type: "forward"
data: "http://127.0.0.1:8899/admin/login"
servers:
- "127.0.0.10:8000"
example.com:
paths:
"/":
redirect_to: "https://www.example.com:443"
servers:
- "127.0.0.1:80"
h2.example.com:
paths:
"/":
server_headers:
- "Y-Global-Something: Yes this is something"
client_headers:
- "Access-Control-Allow-Methods:POST, GET, OPTIONS"
rate_limit: 60
x4xx_limit: 30
authorization:
type: "jwt"
servers:
- "127.0.0.1:8000"
- "127.0.0.2:8000"
"/draw":
"/.well-known/acme-challenge":
healthcheck: false
servers:
- "192.168.1.1:8000"
polo.netangels.net:
- "127.0.0.1:8001"
"/400":
rate_limit: 4
x4xx_limit: 2
servers:
- "127.0.0.1:8899"
"/500":
healthcheck: false
servers:
- "127.0.0.1:8899"
DEFAULT:
paths:
"/":
headers:
- "X-Some-Thing:Yaaaaaaaaaaaaaaa"
healthcheck: false
servers:
- "192.168.1.1:8000"
- "192.168.1.10:8000"
- "127.0.0.1:8000"
- "127.0.0.2:8000"
- "127.0.0.3:8000"
- "127.0.0.4:8000"
glop.netangels.net:
paths:
"/":
headers:
- "X-Hopar-From:Hopaaaaaaaaaaaar"
"/.well-known/acme-challenge":
healthcheck: false
servers:
- "192.168.1.10:8000"
- "192.168.1.1:8000"
apt.netangels.net:
paths:
"/":
servers:
- "apt.netangels.net:443"
test.netangels.net:
paths:
"/":
servers:
- "myip.netangels.net:80"
127.0.0.1:
paths:
"/":
servers:
- "192.168.1.5:8080"
127.0.0.2:
paths:
"/":
servers:
- "10.0.55.171:3000"
localpost:
paths:
"/":
servers:
- "127.0.0.1:9000"
- "127.0.0.1:3000"
+16
View File
@@ -0,0 +1,16 @@
use crate::auth::types::AuthValidator;
use pingora_proxy::Session;
use subtle::ConstantTimeEq;
pub struct ApiKeyAuth<'a>(pub(crate) &'a str);
#[async_trait::async_trait]
impl AuthValidator for ApiKeyAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("x-api-key") {
if let Ok(h) = header.to_str() {
return h.as_bytes().ct_eq(self.0.as_bytes()).into();
}
}
false
}
}
+24
View File
@@ -0,0 +1,24 @@
use crate::auth::types::AuthValidator;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use pingora_proxy::Session;
use subtle::ConstantTimeEq;
pub struct BasicAuth<'a>(pub(crate) &'a str);
#[async_trait::async_trait]
impl AuthValidator for BasicAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(header) = session.get_header("authorization") {
if let Ok(h) = header.to_str() {
if let Some((_, val)) = h.split_once(' ') {
if let Ok(decoded) = STANDARD.decode(val) {
if decoded.as_slice().ct_eq(self.0.as_bytes()).into() {
return true;
}
}
}
}
}
false
}
}
+131
View File
@@ -0,0 +1,131 @@
use crate::auth::types::AuthValidator;
use crate::utils::tools::split_host_port;
use axum::http::StatusCode;
use pingora_core::connectors::http::Connector;
use pingora_core::prelude::HttpPeer;
use pingora_http::{RequestHeader, ResponseHeader};
use pingora_proxy::Session;
use std::sync::LazyLock;
static AUTH_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
pub struct ForwardAuth<'a>(pub(crate) &'a str);
#[async_trait::async_trait]
impl AuthValidator for ForwardAuth<'_> {
async fn validate(&self, session: &mut Session) -> bool {
let method = match session.req_header().method.as_str() {
"HEAD" => "HEAD",
_ => "GET",
};
let auth_url = self.0;
let (plain, tls) = if let Some(p) = auth_url.strip_prefix("http://") {
(p, false)
} else if let Some(p) = auth_url.strip_prefix("https://") {
(p, true)
} else {
return false;
};
let (addr, uri) = if let Some(pos) = plain.find('/') {
(&plain[..pos], &plain[pos..])
} else {
(plain, "/")
};
let hp = match split_host_port(addr, tls) {
Some(hp) => hp,
None => return false,
};
let peer = HttpPeer::new((hp.0, hp.1), tls, hp.0.to_string());
let (mut http_session, _) = match AUTH_CONNECTOR.get_http_session(&peer).await {
Ok(s) => s,
Err(e) => {
log::warn!("ForwardAuth: connect failed: {}", e);
return false;
}
};
let mut auth_req = match RequestHeader::build(method, uri.as_bytes(), None) {
Ok(r) => r,
Err(e) => {
log::warn!("ForwardAuth: failed to build request: {}", e);
return false;
}
};
// auth_req.headers = session.req_header().headers.clone();
auth_req.insert_header("Host", addr).ok();
auth_req.insert_header("X-Forwarded-Uri", uri).ok();
auth_req.insert_header("X-Forwarded-Method", session.req_header().method.as_str()).ok();
if let Some(auth) = session.req_header().headers.get("authorization") {
auth_req.insert_header("Authorization", auth.clone()).ok();
}
if let Some(cookie) = session.req_header().headers.get("cookie") {
auth_req.insert_header("Cookie", cookie.clone()).ok();
}
if tls {
auth_req.insert_header("X-Forwarded-Proto", "https").ok();
} else {
auth_req.insert_header("X-Forwarded-Proto", "http").ok();
}
if let Err(e) = http_session.write_request_header(Box::new(auth_req)).await {
log::warn!("ForwardAuth: write failed: {}", e);
return false;
}
let status = match http_session.read_response_header().await {
Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
log::warn!("ForwardAuth: read failed: {}", e);
return false;
}
};
let auth_headers_to_forward: Vec<(String, String)> = if let Some(resp_header) = http_session.response_header() {
resp_header
.headers
.iter()
.filter_map(|(name, value)| {
let name_str = name.as_str();
if name_str.starts_with("x-") || name_str.starts_with("remote-") || name_str.starts_with("locat") {
value.to_str().ok().map(|v| (name_str.to_string(), v.to_string()))
} else {
None
}
})
.collect()
} else {
Vec::new()
};
AUTH_CONNECTOR.release_http_session(http_session, &peer, None).await;
if (200..300).contains(&status) {
for (name, value) in auth_headers_to_forward {
session.req_header_mut().insert_header(name, value).ok();
}
true
} else if status == 302 || status == 301 {
let resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None);
match resp {
Ok(mut r) => {
for (name, value) in auth_headers_to_forward {
r.insert_header(name, value).ok();
}
let _ = r.insert_header("Content-Length", "0");
let _ = session.write_response_header(Box::new(r), true).await;
true
}
Err(_) => return false,
}
} else {
false
}
}
}
+134
View File
@@ -0,0 +1,134 @@
use crate::auth::types::AuthValidator;
use ahash::AHasher;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
use jsonwebtoken::{Algorithm, DecodingKey, Validation};
use moka::sync::Cache;
use moka::Expiry;
use pingora_proxy::Session;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::hash::{Hash, Hasher};
use std::sync::{Arc, LazyLock};
use std::time::{Duration, Instant, SystemTime};
use urlencoding::decode;
#[derive(Debug, Serialize, Deserialize)]
pub struct Claims {
pub master_key: String,
pub owner: String,
pub exp: u64,
pub random: Option<String>,
}
#[derive(Debug, Deserialize)]
struct Expired {
exp: Option<u64>,
}
static JWT_VALIDATION: LazyLock<Validation> = LazyLock::new(|| Validation::new(Algorithm::HS256));
pub static JWT_TOKEN: LazyLock<Option<Arc<str>>> = LazyLock::new(|| match env::var("JWT_KEY") {
Ok(key) if !key.is_empty() => Some(Arc::from(key.as_str())),
_ => None,
});
static JWT_CACHE: LazyLock<Cache<u64, u64>> = LazyLock::new(|| Cache::builder().max_capacity(100_000).expire_after(JwtExpiry).build());
struct JwtExpiry;
impl Expiry<u64, u64> for JwtExpiry {
fn expire_after_create(&self, _key: &u64, value: &u64, _current_time: Instant) -> Option<Duration> {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if *value > now {
Some(Duration::from_secs(value - now))
} else {
Some(Duration::ZERO)
}
}
}
pub struct JwtAuth();
#[async_trait::async_trait]
impl AuthValidator for JwtAuth {
async fn validate(&self, session: &mut Session) -> bool {
if let Some(jwtsecret) = JWT_TOKEN.clone() {
if let Some(tok) = get_query_param(session, "araleztoken") {
return check_jwt(tok.as_str(), jwtsecret.as_ref());
}
if let Some(auth_header) = session.get_header("authorization") {
if let Ok(header_str) = auth_header.to_str() {
if let Some((scheme, token)) = header_str.split_once(' ') {
if scheme.eq_ignore_ascii_case("bearer") {
return check_jwt(token, jwtsecret.as_ref());
}
}
}
}
}
false
}
}
fn get_query_param(session: &mut Session, key: &str) -> Option<String> {
let query = session.req_header().uri.query()?;
let params: HashMap<_, _> = query
.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let k = parts.next()?;
let v = parts.next().unwrap_or(""); // Some params might have no value
Some((k, v))
})
.collect();
params.get(key).and_then(|v| decode(v).ok()).map(|s| s.to_string())
}
pub fn check_jwt(token: &str, secret: &str) -> bool {
let key = hash_token(token, secret);
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if let Some(exp) = JWT_CACHE.get(&key) {
if exp < now {
return false;
}
return true;
}
match is_expired(token, now) {
Ok(true) => return false,
Ok(false) => {}
Err(_) => return false,
}
match jsonwebtoken::decode::<Claims>(token, &DecodingKey::from_secret(secret.as_ref()), &JWT_VALIDATION) {
Ok(data) => {
let now = SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
if data.claims.exp > now {
JWT_CACHE.insert(key, data.claims.exp);
true
} else {
false
}
}
Err(_) => false,
}
}
fn is_expired(token: &str, now: u64) -> Result<bool, Box<dyn std::error::Error>> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err("Invalid JWT format".into());
}
let decoded = URL_SAFE_NO_PAD.decode(parts[1])?;
let claims: Expired = serde_json::from_slice(&decoded)?;
if let Some(exp) = claims.exp {
Ok(exp < now)
} else {
Ok(true)
}
}
fn hash_token(token: &str, secret: &str) -> u64 {
let mut hasher = AHasher::default();
token.hash(&mut hasher);
secret.hash(&mut hasher);
hasher.finish()
}
+7
View File
@@ -0,0 +1,7 @@
pub mod apikey;
pub mod basic;
pub mod forward;
pub mod jwt;
pub mod types;
pub use types::*;
+23
View File
@@ -0,0 +1,23 @@
use crate::auth::apikey::ApiKeyAuth;
use crate::auth::basic::BasicAuth;
use crate::auth::forward::ForwardAuth;
use crate::auth::jwt::JwtAuth;
use crate::utils::types::InnerAuth;
use pingora_proxy::Session;
#[async_trait::async_trait]
pub trait AuthValidator {
async fn validate(&self, session: &mut Session) -> bool;
}
pub async fn authenticate(auth: &InnerAuth, session: &mut Session) -> bool {
match &*auth.auth_type {
"basic" => BasicAuth(&auth.auth_cred).validate(session).await,
"apikey" => ApiKeyAuth(&auth.auth_cred).validate(session).await,
"jwt" => JwtAuth().validate(session).await,
"forward" => ForwardAuth(&auth.auth_cred).validate(session).await,
_ => {
log::warn!("Unsupported authentication mechanism : {}", &*auth.auth_type);
false
}
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod redis;
pub mod types;
+222
View File
@@ -0,0 +1,222 @@
use async_trait::async_trait;
use bytes::Bytes;
use log::warn;
use pingora::Result;
use pingora_cache::storage::{HandleHit, HandleMiss, HitHandler, MissFinishType, MissHandler, Storage};
use pingora_cache::trace::SpanHandle;
use pingora_cache::{CacheKey, CacheMeta, PurgeOutcome, PurgeTarget, PurgeType};
use redis::aio::ConnectionManager;
use redis::{AsyncCommands, Client, RedisResult};
use std::any::Any;
use std::borrow::Cow;
use std::sync::Arc;
#[derive(Clone)]
pub struct RedisCache {
pub client: Arc<ConnectionManager>,
pub ttl: u64,
}
pub struct RedisHitHandler {
body: Option<Bytes>,
}
pub struct RedisMissHandler {
client: Arc<ConnectionManager>,
body_key: String,
body_buffer: Vec<u8>,
ttl: u64,
}
impl RedisCache {
pub async fn new(url: &str, ttl: u64) -> RedisResult<Self> {
let client = Client::open(url)?;
let manager = ConnectionManager::new(client).await?;
Ok(Self { client: Arc::new(manager), ttl })
}
fn make_key(key: &CacheKey) -> Cow<'_, str> {
match key.primary_key_str() {
Some(s) => Cow::Borrowed(s),
None => Cow::Owned(key.primary_key().iter().map(|b| format!("{:02x}", b)).collect()),
}
}
}
#[async_trait]
impl HandleHit for RedisHitHandler {
async fn read_body(&mut self) -> Result<Option<Bytes>> {
Ok(self.body.take())
}
async fn finish(self: Box<Self>, _storage: &'static (dyn Storage + Sync), _key: &CacheKey, _trace: &SpanHandle) -> Result<()> {
Ok(())
}
fn can_seek(&self) -> bool {
false
}
fn seek(&mut self, _start: usize, _end: Option<usize>) -> Result<()> {
Ok(())
}
fn should_count_access(&self) -> bool {
true
}
fn get_eviction_weight(&self) -> usize {
self.body.as_ref().map(|b| b.len()).unwrap_or(0)
}
fn entry_id(&self) -> Option<pingora_cache::eviction::CacheEntryId> {
None
}
fn as_any(&self) -> &(dyn Any + Send + Sync) {
self
}
fn as_any_mut(&mut self) -> &mut (dyn Any + Send + Sync) {
self
}
}
#[async_trait]
impl HandleMiss for RedisMissHandler {
async fn write_body(&mut self, data: Bytes, _eof: bool) -> Result<()> {
self.body_buffer.extend_from_slice(&data);
Ok(())
}
async fn finish(self: Box<Self>) -> Result<MissFinishType> {
let len = self.body_buffer.len();
let mut conn = (*self.client).clone();
if let Err(e) = conn.set_ex::<_, _, ()>(&self.body_key, &self.body_buffer, self.ttl).await {
warn!("Failed to write cache entry to Redis: {e}");
}
Ok(MissFinishType::Appended(len, None))
}
fn entry_id(&self) -> Option<pingora_cache::eviction::CacheEntryId> {
None
}
}
#[async_trait]
impl Storage for RedisCache {
async fn lookup(&'static self, key: &CacheKey, _trace: &SpanHandle) -> Result<Option<(CacheMeta, HitHandler)>> {
let base = Self::make_key(key);
// Pre-allocate exact capacities to prevent reallocations
let mut meta_key = String::with_capacity(base.len() + 5);
meta_key.push_str(&base);
meta_key.push_str(":meta");
let mut body_key = String::with_capacity(base.len() + 5);
body_key.push_str(&base);
body_key.push_str(":body");
let mut internal_key = String::with_capacity(meta_key.len() + 9);
internal_key.push_str(&meta_key);
internal_key.push_str(":internal");
let mut header_key = String::with_capacity(meta_key.len() + 7);
header_key.push_str(&meta_key);
header_key.push_str(":header");
let mut conn = (*self.client).clone();
let internal: Option<Vec<u8>> = conn.get(&internal_key).await.ok().flatten();
let header: Option<Vec<u8>> = conn.get(&header_key).await.ok().flatten();
let body: Option<Vec<u8>> = conn.get(&body_key).await.ok().flatten();
match (internal, header, body) {
(Some(internal_bytes), Some(header_bytes), Some(body_bytes)) => match CacheMeta::deserialize(&internal_bytes, &header_bytes) {
Ok(meta) => {
let handler: HitHandler = Box::new(RedisHitHandler {
body: Some(Bytes::from(body_bytes)),
});
Ok(Some((meta, handler)))
}
Err(_) => {
let _: RedisResult<()> = conn.del(&[&internal_key, &header_key, &body_key]).await;
Ok(None)
}
},
_ => Ok(None),
}
}
async fn get_miss_handler(&'static self, key: &CacheKey, meta: &CacheMeta, _trace: &SpanHandle) -> Result<MissHandler> {
let base = Self::make_key(key);
let mut meta_key = String::with_capacity(base.len() + 5);
meta_key.push_str(&base);
meta_key.push_str(":meta");
let mut body_key = String::with_capacity(base.len() + 5);
body_key.push_str(&base);
body_key.push_str(":body");
let mut internal_key = String::with_capacity(meta_key.len() + 9);
internal_key.push_str(&meta_key);
internal_key.push_str(":internal");
let mut header_key = String::with_capacity(meta_key.len() + 7);
header_key.push_str(&meta_key);
header_key.push_str(":header");
let ttl = self.ttl;
let (internal_bytes, header_bytes) = meta.serialize()?;
let mut conn = (*self.client).clone();
let _: RedisResult<()> = conn.set_ex(&internal_key, &internal_bytes, ttl).await;
let _: RedisResult<()> = conn.set_ex(&header_key, &header_bytes, ttl).await;
Ok(Box::new(RedisMissHandler {
client: self.client.clone(),
body_key,
body_buffer: Vec::with_capacity(4096),
ttl,
}))
}
async fn purge(&'static self, _target: PurgeTarget<'_>, _purge_type: PurgeType, _trace: &SpanHandle) -> Result<PurgeOutcome> {
Ok(PurgeOutcome::NotFound)
}
async fn update_meta(&'static self, key: &CacheKey, meta: &CacheMeta, _trace: &SpanHandle) -> Result<bool> {
let base = Self::make_key(key);
let mut meta_key = String::with_capacity(base.len() + 5);
meta_key.push_str(&base);
meta_key.push_str(":meta");
let mut internal_key = String::with_capacity(meta_key.len() + 9);
internal_key.push_str(&meta_key);
internal_key.push_str(":internal");
let mut header_key = String::with_capacity(meta_key.len() + 7);
header_key.push_str(&meta_key);
header_key.push_str(":header");
let ttl = self.ttl;
match meta.serialize() {
Ok((internal_bytes, header_bytes)) => {
let mut conn = (*self.client).clone();
let _: RedisResult<()> = conn.set_ex(&internal_key, &internal_bytes, ttl).await;
let _: RedisResult<()> = conn.set_ex(&header_key, &header_bytes, ttl).await;
Ok(true)
}
Err(_) => Ok(false),
}
}
fn support_streaming_partial_write(&self) -> bool {
false
}
fn as_any(&self) -> &(dyn Any + Send + Sync) {
self
}
}
+41
View File
@@ -0,0 +1,41 @@
use crate::cache::redis::RedisCache;
use crate::utils::lazylock::{CACHE_TTL, EVICTION};
use crate::utils::types::AppConfig;
use log::{info, warn};
use pingora_cache::eviction::simple_lru::Manager;
use pingora_cache::{MemCache, Storage};
use std::sync::{Arc, OnceLock};
pub static GLOBAL_STORAGE: OnceLock<Box<dyn Storage + Send + Sync>> = OnceLock::new();
pub async fn init_cache(config: Arc<AppConfig>) {
let cache_storage = config.cache_storage.clone().unwrap_or_else(|| {
info!("No cache_storage configured, defaulting to memory");
"memory".to_string()
});
let cache_type = cache_storage.splitn(2, ':').next().unwrap_or("memory");
let ttl = config.cache_ttl.unwrap_or(60);
match cache_type {
"memory" => {
CACHE_TTL.set(ttl).ok();
if let Some(size) = config.cache_size_mb {
let cache_size = size * 1024 * 1024;
drop(EVICTION.set(Manager::new(cache_size)));
}
GLOBAL_STORAGE.set(Box::new(MemCache::new())).ok();
info!("Cache initialized successfully. Storage: {}", cache_storage);
}
"redis" => match RedisCache::new(&cache_storage, ttl).await {
Ok(cache) => {
CACHE_TTL.set(ttl).ok();
GLOBAL_STORAGE.set(Box::new(cache)).ok();
info!("Cache initialized successfully. Storage: {}", cache_storage);
}
Err(e) => log::error!("Cache storage initialization failed: {}", e),
},
_ => {
warn!("Unsupported storage, disabling caching: {}", cache_storage);
}
}
}
+149
View File
@@ -0,0 +1,149 @@
use crate::cache::types::init_cache;
use crate::core::proxyhttp::LB;
use crate::ingress::{APIUpstreamProvider, ConsulProvider, Discovery, FromFileProvider, KubernetesProvider};
use crate::logging::core::{init_access_log, init_access_logging, log_builder};
use crate::tls::acme::order::refresh_order;
use crate::utils::metrics::calc_cache_metrics;
use crate::utils::parceyaml::load_configuration;
use crate::utils::tools::*;
use crate::utils::types::Configuration;
use crate::utils::*;
use async_trait::async_trait;
use dashmap::DashMap;
use log::{error, info};
use pingora_core::server::ShutdownWatch;
use pingora_core::services::background::BackgroundService;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;
#[async_trait]
impl BackgroundService for LB {
async fn start(&self, mut shutdown: ShutdownWatch) {
log_builder(&self.config, &self.config.log_file).await;
let al = &self.config.access_log.clone().unwrap_or("none".to_string());
let _ = init_access_log(al.as_str()).await;
info!("Starting background service"); // tx: Sender<Configuration>
let (tx, mut rx) = mpsc::channel::<Configuration>(1);
let tx_api = tx.clone();
let config = load_configuration(self.config.upstreams_conf.clone().as_str(), "filepath")
.await
.0
.die("Failed to load configuration");
match config.typecfg.as_str() {
"file" => {
info!("Running File discovery, requested type is: {}", config.typecfg);
tx.send(config).await.die("Running File discovery");
let file_load = FromFileProvider {
path: self.config.upstreams_conf.clone(),
};
drop(tokio::spawn(async move { file_load.start(tx).await }));
}
"kubernetes" => {
info!("Running Kubernetes discovery, requested type is: {}", config.typecfg);
let cf = Arc::from(config);
let kuber_load = KubernetesProvider { config: cf.clone() };
drop(tokio::spawn(async move { kuber_load.start(tx).await }));
}
"consul" => {
info!("Running Consul discovery, requested type is: {}", config.typecfg);
let cf = Arc::from(config);
let consul_load = ConsulProvider { config: cf.clone() };
drop(tokio::spawn(async move { consul_load.start(tx).await }));
}
_ => {
error!("Unknown discovery type: {}", config.typecfg);
}
}
let confdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/autoconfigs";
let certdir = self.config.proxy_configs.clone().unwrap_or_else(|| "/tmp".to_string()) + "/certificates";
let api_load = APIUpstreamProvider {
address: self.config.config_address.clone(),
masterkey: self.config.master_key.clone(),
config_api_enabled: self.config.config_api_enabled,
upstreams_file: self.config.upstreams_conf.clone(),
config_dir: confdir.clone(),
certs_dir: certdir.clone(),
file_server_address: self.config.file_server_address.clone(),
file_server_folder: self.config.file_server_folder.clone(),
current_upstreams: self.ump_upst.clone(),
full_upstreams: self.ump_full.clone(),
acme_dns_provider: self.config.acme_dns_provider.clone(),
};
tokio::spawn(async move { api_load.start(tx_api).await });
let uu = self.ump_upst.clone();
let ff = self.ump_full.clone();
let im = self.ump_byid.clone();
let (hc_method, hc_interval) = (self.config.hc_method.clone(), self.config.hc_interval);
tokio::spawn(async move { healthcheck::hc2(uu, ff, im, (&*hc_method.to_string(), hc_interval.to_string().parse().die("Unable to spawn HC worker"))).await });
tokio::spawn(async move { calc_cache_metrics().await });
let acme_provider = self.config.acme_dns_provider.clone();
tokio::spawn(async move { refresh_order(certdir, confdir, acme_provider).await });
init_cache(self.config.clone()).await;
init_access_logging(self.config.access_log.clone()).await;
loop {
tokio::select! {
_ = shutdown.changed() => {
break;
}
val = rx.recv() => {
let start = SystemTime::now().duration_since(UNIX_EPOCH).die(format!("ON {} {}", file!(), line!()).as_str());
if let Some(ss) = val {
clone_dashmap_into(&ss.upstreams, &self.ump_full);
clone_dashmap_into(&ss.upstreams, &self.ump_upst);
clone_idmap_into(&ss.upstreams, &self.ump_byid);
let current = self.extraparams.load_full();
let mut new = (*current).clone();
new.to_https = ss.extraparams.to_https;
new.sticky_sessions = ss.extraparams.sticky_sessions;
new.authentication = ss.extraparams.authentication.clone();
new.rate_limit = ss.extraparams.rate_limit;
new.x4xx_limit = ss.extraparams.x4xx_limit;
self.extraparams.store(Arc::new(new));
self.client_headers.clear();
self.server_headers.clear();
for entry in ss.upstreams.iter() {
let global_key = entry.key().clone();
let client_global_values = DashMap::new();
let server_global_values = DashMap::new();
let mut client_target_entry = ss.client_headers.entry(global_key.clone()).or_insert_with(DashMap::new);
client_target_entry.extend(client_global_values);
let mut server_target_entry = ss.server_headers.entry(global_key).or_insert_with(DashMap::new);
server_target_entry.extend(server_global_values);
self.server_headers.insert(server_target_entry.key().to_owned(), server_target_entry.value().to_owned());
}
for path in ss.client_headers.iter() {
let path_key = path.key().clone();
let path_headers = path.value().clone();
self.client_headers.insert(path_key.clone(), path_headers);
if let Some(global_headers) = ss.client_headers.get("GLOBAL_CLIENT_HEADERS") {
if let Some(existing_headers) = self.client_headers.get_mut(&path_key) {
merge_headers(&existing_headers, &global_headers);
}
}
}
for path in ss.server_headers.iter() {
let path_key = path.key().clone();
let path_headers = path.value().clone();
self.server_headers.insert(path_key.clone(), path_headers);
if let Some(global_headers) = ss.server_headers.get("GLOBAL_SERVER_HEADERS") {
if let Some(existing_headers) = self.server_headers.get_mut(&path_key) {
merge_headers(&existing_headers, &global_headers);
}
}
}
}
let end = SystemTime::now().duration_since(UNIX_EPOCH).die(format!("ON {} {}", file!(), line!()).as_str());
let startup = end - start;
info!("Loading upstreams took: {}ms", startup.as_millis());
}
}
}
}
}
+124
View File
@@ -0,0 +1,124 @@
use crate::core::proxyhttp::LB;
use crate::utils::types::InnerMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct GetHostsReturHeaders {
pub client_headers: Option<Vec<(String, Arc<str>)>>,
pub server_headers: Option<Vec<(String, Arc<str>)>>,
}
pub trait GetHost {
fn find_sticky_backend(&self, servers: &[Arc<InnerMap>], backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn pick_backend(&self, servers: &[Arc<InnerMap>], index: &AtomicUsize, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>>;
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders>;
}
impl GetHost for LB {
fn find_sticky_backend(&self, servers: &[Arc<InnerMap>], backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
let b = backend_id?;
let bb = self.ump_byid.get(b)?;
let target = bb.value();
servers.iter().any(|s| s.address == target.address && s.port == target.port).then(|| target.clone())
}
fn pick_backend(&self, servers: &[Arc<InnerMap>], index: &AtomicUsize, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
if servers.is_empty() {
return None;
}
if let Some(target) = self.find_sticky_backend(servers, backend_id) {
return Some(target);
}
let idx = index.fetch_add(1, Ordering::Relaxed) % servers.len();
Some(servers[idx].clone())
}
fn get_host(&self, peer: &str, path: &str, backend_id: Option<&str>) -> Option<Arc<InnerMap>> {
let host_entry = self.ump_upst.get(peer)?;
let mut end = path.len();
loop {
let slice = &path[..end];
if let Some(entry) = host_entry.get(slice) {
let (servers, index) = entry.value();
if let Some(backend) = self.pick_backend(servers, index, backend_id) {
return Some(backend);
}
}
if let Some(pos) = slice.rfind('/') {
end = pos;
} else {
break;
}
}
if let Some(entry) = host_entry.get("/") {
let (servers, index) = entry.value();
if let Some(backend) = self.pick_backend(servers, index, backend_id) {
return Some(backend);
}
}
None
}
fn get_header(&self, peer: &str, path: &str) -> Option<GetHostsReturHeaders> {
let client_entry = self.client_headers.get(peer);
let server_entry = self.server_headers.get(peer);
if client_entry.is_none() && server_entry.is_none() {
return None;
}
let mut current_path = path;
let mut clnt_match = None;
if let Some(client_entry) = client_entry {
loop {
if let Some(entry) = client_entry.get(current_path) {
if !entry.value().is_empty() {
clnt_match = Some(entry.value().clone());
break;
}
}
if current_path == "/" {
break;
}
if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break;
}
}
}
current_path = path;
let mut serv_match = None;
if let Some(server_entry) = server_entry {
loop {
if let Some(entry) = server_entry.get(current_path) {
if !entry.value().is_empty() {
serv_match = Some(entry.value().clone());
break;
}
}
if current_path == "/" {
if let Some(entry) = server_entry.get("/") {
if !entry.value().is_empty() {
serv_match = Some(entry.value().clone());
break;
}
}
break;
}
if let Some(pos) = current_path.rfind('/') {
current_path = if pos == 0 { "/" } else { &current_path[..pos] };
} else {
break;
}
}
}
let result = GetHostsReturHeaders {
client_headers: clnt_match,
server_headers: serv_match,
};
if result.client_headers.is_some() || result.server_headers.is_some() {
Some(result)
} else {
None
}
}
}
View File
+362
View File
@@ -0,0 +1,362 @@
use crate::auth::authenticate;
use crate::cache::types::GLOBAL_STORAGE;
use crate::core::gethosts::{GetHost, GetHostsReturHeaders};
use crate::logging::core::access_log;
use crate::utils::lazylock::{CACHE_LOCK, CACHE_TTL, EVICTION, LOCALHOST, RATE_LIMITER, REQUESTS_4XX, REVERSE_STORE};
use crate::utils::metrics::*;
use crate::utils::types::{AppConfig, Extraparams, Headers, InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use arc_swap::ArcSwap;
use async_trait::async_trait;
use axum::body::Bytes;
use log::error;
use pingora::ErrorSource::Upstream;
use pingora::http::{Method, RequestHeader, ResponseHeader, StatusCode};
use pingora::prelude::*;
use pingora_cache::cache_control::CacheControl;
use pingora_cache::{CacheKey, CacheMetaDefaults, RespCacheable};
use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer;
use pingora_proxy::{ProxyHttp, Session};
use sha2::{Digest, Sha256};
use std::cell::RefCell;
use std::fmt::Write;
use std::sync::Arc;
use std::time::Duration;
use tokio::time::Instant;
thread_local! {static IP_BUFFER: RefCell<String> = RefCell::new(String::with_capacity(50));}
#[derive(Clone)]
pub struct LB {
pub ump_upst: Arc<UpstreamsDashMap>,
pub ump_full: Arc<UpstreamsDashMap>,
pub ump_byid: Arc<UpstreamsIdMap>,
pub client_headers: Arc<Headers>,
pub server_headers: Arc<Headers>,
pub config: Arc<AppConfig>,
pub extraparams: Arc<ArcSwap<Extraparams>>,
pub cache_enabled: bool,
pub master_server: bool,
}
pub struct Context {
backend_id: Option<String>,
start_time: Instant,
hostname: Option<Arc<str>>,
upstream_peer: Option<Arc<InnerMap>>,
extraparams: arc_swap::Guard<Arc<Extraparams>>,
client_headers: Option<Vec<(String, Arc<str>)>>,
x4xx_limit: u32,
}
thread_local! {
static KEY_BUF: RefCell<String> = RefCell::new(String::with_capacity(256));
}
#[async_trait]
impl ProxyHttp for LB {
type CTX = Context;
fn new_ctx(&self) -> Self::CTX {
Context {
backend_id: None,
start_time: Instant::now(),
hostname: None,
upstream_peer: None,
extraparams: self.extraparams.load(),
client_headers: None,
x4xx_limit: 0,
}
}
fn cache_key_callback(&self, session: &Session, ctx: &mut Self::CTX) -> Result<CacheKey> {
let host = ctx.hostname.as_deref().unwrap_or("default");
let path = session.req_header().uri.path_and_query().map_or("/", |pq| pq.as_str());
let primary_key = KEY_BUF.with(|buf| {
let mut b = buf.borrow_mut();
b.clear();
b.push_str(host);
b.push_str(path);
b.clone()
});
Ok(CacheKey::new(primary_key, host /* host is just for debugging */))
}
fn request_cache_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<()> {
let has_auth = _ctx.upstream_peer.as_ref().map(|m| m.authorization.is_some()).unwrap_or(false) || _ctx.extraparams.authentication.is_some();
if self.cache_enabled && session.req_header().method == Method::GET && !has_auth {
if let Some(storage) = GLOBAL_STORAGE.get() {
let eviction_mgr = EVICTION.get().map(|e| e as &(dyn pingora_cache::eviction::EvictionManager + Sync));
session.cache.enable(storage.as_ref(), eviction_mgr, None, Some(&*CACHE_LOCK), None);
}
}
Ok(())
}
fn response_cache_filter(&self, _session: &Session, resp: &ResponseHeader, _ctx: &mut Self::CTX) -> Result<RespCacheable> {
let cc = CacheControl::from_resp_headers(resp);
let defaults = CacheMetaDefaults::new(
|status| {
if status == StatusCode::OK {
Some(Duration::from_secs(*CACHE_TTL.get().unwrap_or(&60)))
} else {
None
}
},
0,
0,
);
Ok(pingora_cache::filters::resp_cacheable(cc.as_ref(), resp.clone(), false, &defaults))
}
async fn request_filter(&self, session: &mut Session, _ctx: &mut Self::CTX) -> Result<bool> {
if self.master_server {
let _ = session.respond_error(400).await;
return Ok(true);
};
ACTIVE_SESSIONS.inc();
let hostname = return_header_host_from_upstream(session, &self.ump_upst);
_ctx.hostname = hostname;
let mut backend_id = None;
if _ctx.extraparams.sticky_sessions > 0 {
if let Some(cookies) = session.req_header().headers.get("cookie") {
if let Ok(cookie_str) = cookies.to_str() {
if let Some(pos) = cookie_str.find("backend_id=") {
let value = &cookie_str[pos + "backend_id=".len()..];
let end = value.find(';').unwrap_or(value.len());
backend_id = Some(&value[..end]);
}
}
}
}
match _ctx.hostname.as_ref() {
None => return Ok(false),
Some(host) => {
let optioninnermap = self.get_host(host, session.req_header().uri.path(), backend_id);
match optioninnermap {
None => return Ok(false),
Some(ref innermap) => {
if let Some(auth) = _ctx.extraparams.authentication.as_ref().or(innermap.authorization.as_ref()) {
if !authenticate(auth, session).await {
let _ = session.respond_error(401).await;
return Ok(true);
}
}
let rate = if innermap.rate_limit > 0 { innermap.rate_limit } else { _ctx.extraparams.rate_limit };
let xrate = if innermap.x4xx_limit > 0 { innermap.x4xx_limit } else { _ctx.extraparams.x4xx_limit };
if xrate > 0 || rate > 0 {
let rate_key = session.client_addr().and_then(|addr| addr.as_inet()).map(|inet| inet.ip());
if xrate > 0 {
_ctx.x4xx_limit = xrate;
if let Some(rk) = rate_key {
let count = REQUESTS_4XX.get(&rk).unwrap_or(0);
if count > xrate {
let header = ResponseHeader::build(429, None)?;
session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?;
return Ok(true);
}
}
}
if rate > 0 {
let curr_window_requests = RATE_LIMITER.observe(&rate_key, 1);
if curr_window_requests > rate {
let header = ResponseHeader::build(429, None)?;
session.set_keepalive(None);
session.write_response_header(Box::new(header), true).await?;
return Ok(true);
}
}
}
if let Some(redirect_to) = &innermap.redirect_to {
let uri = session.req_header().uri.path();
let capacity = redirect_to.len() + uri.len();
let mut s = String::with_capacity(capacity);
s.push_str(redirect_to);
s.push_str(uri);
let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
resp.insert_header("Location", s)?;
resp.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(resp), true).await?;
return Ok(true);
}
if _ctx.extraparams.to_https || innermap.to_https {
if let Some(stream) = session.stream() {
if stream.get_ssl().is_none() {
if let Some(host) = _ctx.hostname.as_ref() {
let port = self.config.proxy_port_tls.as_deref().unwrap_or("443");
let uri = session.req_header().uri.path();
let capacity = host.len() + uri.len() + 8;
let mut s = String::with_capacity(capacity);
s.push_str("https://");
s.push_str(host);
if port != "443" {
s.push(':');
s.push_str(port);
}
s.push_str(uri);
let mut resp = ResponseHeader::build(StatusCode::MOVED_PERMANENTLY, None)?;
resp.insert_header("Location", s)?;
resp.insert_header("Content-Length", "0")?;
session.write_response_header(Box::new(resp), true).await?;
return Ok(true);
}
}
}
}
}
}
_ctx.upstream_peer = optioninnermap;
}
}
Ok(false)
}
async fn upstream_peer(&self, session: &mut Session, ctx: &mut Self::CTX) -> Result<Box<HttpPeer>> {
match ctx.hostname.as_ref() {
Some(hostname) => match ctx.upstream_peer.as_ref() {
Some(innermap) => {
let mut peer = Box::new(HttpPeer::new((&*innermap.address, innermap.port), innermap.is_ssl, hostname.to_string()));
if innermap.is_http2 && !session.is_upgrade_req() {
peer.options.alpn = ALPN::H2;
}
if innermap.is_ssl {
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
}
if ctx.extraparams.sticky_sessions > 0 {
let mut s = String::with_capacity(64);
write!(
&mut s,
"{}:{}:{}:{}:{}:{}:{}:{}:{:?}",
hostname,
innermap.address,
innermap.port,
innermap.is_http2,
innermap.to_https,
innermap.x4xx_limit,
innermap.rate_limit,
innermap.healthcheck,
innermap.authorization
)
.unwrap_or(());
ctx.backend_id = Some(s);
}
Ok(peer)
}
None => {
if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await {
error!("Failed to send error response: {:?}", e);
}
Err(Box::new(Error {
etype: HTTPStatus(502),
esource: Upstream,
retry: RetryType::Decided(false),
cause: None,
context: Option::from(ImmutStr::Static("Upstream not found")),
}))
}
},
None => {
if let Err(e) = session.respond_error_with_body(502, Bytes::from("502 Bad Gateway\n")).await {
error!("Failed to send error response: {:?}", e);
}
Err(Box::new(Error {
etype: HTTPStatus(502),
esource: Upstream,
retry: RetryType::Decided(false),
cause: None,
context: None,
}))
}
}
}
async fn upstream_request_filter(&self, session: &mut Session, upstream_request: &mut RequestHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) {
IP_BUFFER.with(|buffer| {
let mut buf = buffer.borrow_mut();
buf.clear();
write!(buf, "{}", ip).unwrap_or(());
upstream_request.append_header("X-Forwarded-For", buf.as_str()).unwrap_or(false);
});
}
let hostname = ctx.hostname.as_deref().unwrap_or("localhost");
let path = session.req_header().uri.path();
let GetHostsReturHeaders { server_headers, client_headers } = match self.get_header(hostname, path) {
Some(h) => h,
None => return Ok(()),
};
if let Some(sh) = server_headers {
for (k, v) in sh {
upstream_request.insert_header(k, v.as_ref())?;
}
}
if let Some(ch) = client_headers {
ctx.client_headers = Some(ch);
}
Ok(())
}
async fn response_filter(&self, _session: &mut Session, _upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
if ctx.extraparams.sticky_sessions > 0 {
if let Some(bid) = &ctx.backend_id {
let tt = if let Some(existing) = REVERSE_STORE.get(bid) {
existing.value().clone()
} else {
let mut hasher = Sha256::new();
hasher.update(bid.as_bytes());
let hash = hasher.finalize();
let hex_hash = base16ct::lower::encode_string(&hash);
let hh = hex_hash[0..50].to_string();
REVERSE_STORE.insert(bid.clone(), hh.clone());
REVERSE_STORE.insert(hh.clone(), bid.clone());
hh
};
let mut buf = String::with_capacity(80);
buf.push_str("backend_id=");
buf.push_str(&tt);
buf.push_str("; Path=/; Max-Age=");
buf.push_str(&ctx.extraparams.sticky_sessions.to_string());
buf.push_str("; HttpOnly; SameSite=Lax");
let _ = _upstream_response.append_header("set-cookie", buf.as_str());
}
}
Ok(())
}
async fn upstream_response_filter(&self, _session: &mut Session, upstream_response: &mut ResponseHeader, ctx: &mut Self::CTX) -> Result<()> {
if let Some(client_headers) = &ctx.client_headers {
for (k, v) in client_headers.iter() {
upstream_response.append_header(k.clone(), v.as_ref())?;
}
}
Ok(())
}
async fn logging(&self, session: &mut Session, _e: Option<&pingora::Error>, ctx: &mut Self::CTX) {
let response_code = session.response_written().map_or(0, |resp| resp.status.as_u16());
let m = &MetricTypes {
method: session.req_header().method.clone(),
code: session.response_written().map(|resp| resp.status),
latency: ctx.start_time.elapsed(),
version: session.req_header().version,
upstream: ctx.hostname.take().unwrap_or_else(|| LOCALHOST.clone()),
};
calc_metrics(m);
ACTIVE_SESSIONS.dec();
if ctx.x4xx_limit > 0 && (400..=499).contains(&response_code) {
if let Some(ip) = session.client_addr().and_then(|a| a.as_inet()).map(|i| i.ip()) {
let current = REQUESTS_4XX.get(&ip).unwrap_or(0);
REQUESTS_4XX.insert(ip, current + 1);
}
}
access_log(response_code, &self.request_summary(session, ctx), session).await;
}
}
fn return_header_host_from_upstream(session: &Session, ump_upst: &UpstreamsDashMap) -> Option<Arc<str>> {
let host_str = if session.is_http2() {
session.req_header().uri.host()?
} else {
let h = session.req_header().headers.get("host")?.to_str().ok()?;
h.split_once(':').map_or(h, |(host, _)| host)
};
ump_upst.get(host_str).or_else(|| ump_upst.get("DEFAULT")).map(|entry| entry.key().clone())
}
+181
View File
@@ -0,0 +1,181 @@
use crate::core::proxyhttp::LB;
use crate::tls::grades;
use crate::tls::load;
use crate::tls::load::CertificateConfig;
use crate::utils::folder_watch;
use crate::utils::lazylock::{CACHE_TTL, EVICTION};
use crate::utils::tools::*;
use crate::utils::types::Extraparams;
use arc_swap::ArcSwap;
use dashmap::DashMap;
use log::info;
use pingora::tls::ssl::{SslAlert, SslRef};
use pingora_cache::eviction::simple_lru::Manager;
use pingora_core::listeners::TcpSocketOptions;
use pingora_core::listeners::tls::TlsSettings;
use pingora_core::prelude::{Opt, background_service};
use pingora_core::protocols::TcpKeepalive;
use pingora_core::server::Server;
use privdrop::reexports::libc::SIGQUIT;
use rustls::crypto::ring::default_provider;
use sd_notify::NotifyState;
use signal_hook::{
consts::{SIGINT, SIGTERM},
iterator::Signals,
};
use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration;
use std::{fs, thread};
pub fn run() {
default_provider().install_default().die("Failed to install RusTLS crypto provider:");
let parameters = Opt::parse_args();
let file = parameters.conf.clone().die("Failed to clone the config:");
let maincfg = crate::utils::parceyaml::parce_main_config(file.as_str());
let master_server = maincfg.cluster_member.as_deref() == Some("master");
let cache_enabled = maincfg.cache_enabled.unwrap_or(false);
// ========================================================================== //
CACHE_TTL.set(maincfg.cache_ttl.unwrap_or(60)).die("Failed to set cache TTL");
if cache_enabled {
let cache_size = maincfg.cache_size_mb.unwrap_or(50);
let _ = EVICTION.set(Manager::new(cache_size * 1024 * 1024));
}
// ========================================================================== //
let mut server = Server::new(parameters).die("Failed to start the server:");
server.bootstrap();
let uf_config = Arc::new(DashMap::new());
let ff_config = Arc::new(DashMap::new());
let im_config = Arc::new(DashMap::new());
let ch_config = Arc::new(DashMap::new());
let sh_config = Arc::new(DashMap::new());
let ec_config = Arc::new(ArcSwap::from_pointee(Extraparams {
to_https: false,
ssl_verify: false,
sticky_sessions: 0,
authentication: None,
rate_limit: 0,
x4xx_limit: 0,
}));
let cfg = Arc::new(maincfg);
let lb = LB {
ump_upst: uf_config,
ump_full: ff_config,
ump_byid: im_config,
config: cfg.clone(),
client_headers: ch_config,
server_headers: sh_config,
extraparams: ec_config,
cache_enabled,
master_server,
};
let grade = cfg.proxy_tls_grade.clone().unwrap_or("medium".to_string());
info!("TLS grade set to: [ {} ]", grade);
let bg_srvc = background_service("bgsrvc", lb.clone());
let bind_address_http = cfg.proxy_address_http.clone();
let bind_address_tls = cfg.proxy_address_tls.clone();
let mut proxy = pingora_proxy::http_proxy_service(&server.configuration, lb.clone());
check_priv(bind_address_http.as_str());
let mut tcp_options: Option<TcpSocketOptions> = None;
if let Some(idle) = cfg.tcp_keepalive_idle {
let mut to = TcpSocketOptions::default();
to.tcp_keepalive = Some(TcpKeepalive {
idle: Duration::from_secs(idle),
interval: Duration::from_secs(cfg.tcp_keepalive_interval.unwrap_or(10)),
user_timeout: Default::default(),
count: cfg.tcp_keepalive_count.unwrap_or(5usize),
});
tcp_options = Some(to);
info!(
"Applying kernel tcp_keepalive parameters: idle {}, interval {}, count {}",
idle,
cfg.tcp_keepalive_interval.unwrap_or(60),
cfg.tcp_keepalive_count.unwrap_or(5),
);
}
if let Some(bind_address_tls) = bind_address_tls {
check_priv(bind_address_tls.as_str());
let (tx, rx): (Sender<Vec<CertificateConfig>>, Receiver<Vec<CertificateConfig>>) = channel();
let certs_path = cfg.proxy_configs.clone().die("Failed certificates path") + "/certificates";
if fs::metadata(certs_path.clone()).is_err() {
fs::create_dir_all(certs_path.clone()).die("Failed to create directories");
}
thread::spawn(move || {
folder_watch(certs_path, tx).die("Failed to spawn certificates channel");
});
let certificate_configs = rx.recv().die("Failed to spawn certificates channel");
let first_set = load::Certificates::new(&certificate_configs, grade.as_str()).unwrap_or_else(|| panic!("Unable to load initial certificate info"));
let certificates = Arc::new(ArcSwap::from_pointee(first_set));
let certs_for_callback = certificates.clone();
let certs_for_watcher = certificates.clone();
let new_certs = load::Certificates::new(&certificate_configs, grade.as_str());
certs_for_watcher.store(Arc::new(new_certs.die("Failed to build the watcher")));
let mut tls_settings =
TlsSettings::intermediate(&certs_for_callback.load().default_cert_path, &certs_for_callback.load().default_key_path).die("unable to load or parse cert/key");
grades::set_tsl_grade(&mut tls_settings, grade.as_str());
tls_settings.set_servername_callback(move |ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert| certs_for_callback.load().server_name_callback(ssl_ref, ssl_alert));
tls_settings.set_alpn_select_callback(grades::prefer_h2);
proxy.add_tls_with_settings(&bind_address_tls, tcp_options.clone(), tls_settings);
let certs_for_watcher = certificates.clone();
thread::spawn(move || {
while let Ok(new_configs) = rx.recv() {
let new_certs = load::Certificates::new(&new_configs, grade.as_str());
if let Some(new_certs) = new_certs {
certs_for_watcher.store(Arc::new(new_certs));
};
}
});
}
info!("Running HTTP listener on :{}", bind_address_http);
if let Some(tc) = tcp_options {
proxy.add_tcp_with_settings(&bind_address_http, tc);
} else {
proxy.add_tcp(&bind_address_http)
}
server.add_service(proxy);
server.add_service(bg_srvc);
thread::spawn(move || server.run_forever());
if let (Some(user), Some(group)) = (cfg.rungroup.clone(), cfg.runuser.clone()) {
drop_priv(user, group, cfg.proxy_address_http.clone(), cfg.proxy_address_tls.clone());
}
let _ = sd_notify::notify(&[NotifyState::Ready]);
let pf = cfg.pid_file.clone().unwrap_or("/tmp/aralez.pid".to_string());
if let Err(e) = write_pid_file(pf.as_str()) {
panic!("Failed to write PID file: {} : {}", pf, e);
}
let mut signals = Signals::new([SIGINT, SIGTERM, SIGQUIT]).die("Failed to create signals");
if let Some(sig) = signals.forever().next() {
match sig {
SIGINT => info!("SIGINT received! Exiting..."),
SIGTERM => info!("SIGTERM received! Exiting..."),
SIGQUIT => {
thread::sleep(Duration::from_secs(300));
info!("SIGQUIT received! Exiting...")
}
_ => unreachable!(),
}
}
}
+297
View File
@@ -0,0 +1,297 @@
use crate::auth::jwt::Claims;
use crate::ingress::APIUpstreamProvider;
use crate::tls::acme::http01::{acme_create, acme_order, http01_challenge};
use crate::utils::lazylock::NODE_STATUS;
use crate::utils::metrics::{MEMORY_USAGE, OPEN_FILES, get_memory_usage, get_open_files};
use crate::utils::tools::{Die, upstreams_liveness_json, upstreams_to_json};
use crate::utils::types::{Config, Configuration, UpstreamsDashMap};
use axum::body::Body;
use axum::extract::ConnectInfo;
use axum::extract::{Query, State};
use axum::http::HeaderMap;
use axum::http::{Response, StatusCode};
use axum::response::IntoResponse;
use axum::routing::{any, get, post};
use axum::{Json, Router};
use jsonwebtoken::{EncodingKey, Header, encode};
use log::{debug, error, info, warn};
use prometheus::{Encoder, TextEncoder, gather};
use serde::Serialize;
use signal_hook::{consts::SIGQUIT, iterator::Signals};
use std::collections::HashMap;
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use tower_http::services::ServeDir;
#[derive(Serialize, Debug)]
struct OutToken {
token: String,
}
#[derive(Clone)]
pub struct AppState {
master_key: Option<String>,
pub acme_dns_provider: Option<String>,
pub cert_creds: String,
pub certs_dir: String,
upstreams_file: String,
config_sender: mpsc::Sender<Configuration>,
config_api_enabled: bool,
current_upstreams: Arc<UpstreamsDashMap>,
full_upstreams: Arc<UpstreamsDashMap>,
}
#[allow(unused_mut)]
pub async fn run_server(config: &APIUpstreamProvider, mut to_return: mpsc::Sender<Configuration>, upstreams_curr: Arc<UpstreamsDashMap>, upstreams_full: Arc<UpstreamsDashMap>) {
let credsfile = config.config_dir.clone() + "/acme_credentials.json";
let app_state = AppState {
master_key: config.masterkey.clone(),
cert_creds: credsfile,
certs_dir: config.certs_dir.clone(),
upstreams_file: config.upstreams_file.clone(),
config_sender: to_return.clone(),
config_api_enabled: config.config_api_enabled,
current_upstreams: upstreams_curr,
full_upstreams: upstreams_full,
acme_dns_provider: config.acme_dns_provider.clone(),
};
let app = Router::new()
// .route("/{*wildcard}", get(senderror))
.route("/jwt", post(jwt_gen))
.route("/acme_create", any(acme_create))
.route("/acme_order/{*domain}", any(acme_order))
.route("/.well-known/acme-challenge/{*token}", any(http01_challenge))
.route("/conf", post(conf))
.route("/workerconf", post(workerconf))
.route("/metrics", get(metrics))
.route("/status", get(status))
.with_state(app_state);
let mut static_handle: Option<tokio::task::JoinHandle<()>> = None;
if let (Some(address), Some(folder)) = (&config.file_server_address, &config.file_server_folder) {
let static_listen = port_is_available("File Server", address).await;
let static_files = ServeDir::new(folder);
let static_serve: Router = Router::new().fallback_service(static_files);
info!("Starting file server on: {}", address);
static_handle = Some(tokio::spawn(async move {
match axum::serve(static_listen, static_serve).await {
Ok(o) => info!("File server exited normally: {:?}", o),
Err(e) => error!("File server exited: {}", e),
}
}))
}
let listener = port_is_available("Config API", &config.address).await;
info!("Starting the API server on: {}", config.address);
let api_server = tokio::spawn(async move {
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
.await
.die("Can't start API server")
});
let (tx, mut rx) = mpsc::channel(1);
std::thread::spawn(move || {
let mut signals = Signals::new([SIGQUIT]).die("Can't initialize OS SIGNALS");
if let Some(sig) = signals.forever().next() {
tx.blocking_send(sig).die("Can;t send signal to main thread");
}
});
rx.recv().await;
api_server.abort();
if let Some(handle) = static_handle {
handle.abort();
}
info!("Exiting...");
}
async fn conf(
State(st): State<AppState>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
Query(params): Query<HashMap<String, String>>,
headers: HeaderMap,
content: String,
) -> impl IntoResponse {
if !st.config_api_enabled {
return (StatusCode::FORBIDDEN, "Config API is disabled !\n").into_response();
}
let api_key = headers.get("API-KEY").and_then(|v| v.to_str().ok());
let query_key = params.get("key").map(|k| k.as_str());
let authorized = match st.master_key.as_deref() {
Some(master_key) if !master_key.is_empty() => api_key == Some(master_key) || query_key == Some(master_key),
_ => false,
};
if !authorized {
warn!("Unauthorised access to /conf endpoint from {}", addr.ip());
return (StatusCode::UNAUTHORIZED, "Unauthorized: Invalid API-KEY\n").into_response();
}
let strcontent = content.as_str();
let parsed = noyalib::from_str::<Config>(strcontent);
match parsed {
Ok(_) => {
let save = params.contains_key("save");
tokio::spawn(async move {
apply_config(&content, st, save).await;
});
(StatusCode::OK, "Accepted! Applying in background\n").into_response()
}
Err(err) => {
error!("Failed to parse upstreams file: {}", err);
(StatusCode::BAD_REQUEST, format!("Failed: {}\n", err)).into_response()
}
}
}
async fn workerconf(State(st): State<AppState>, ConnectInfo(addr): ConnectInfo<SocketAddr>, headers: HeaderMap, content: String) -> impl IntoResponse {
let api_key = headers.get("API-KEY").and_then(|v| v.to_str().ok());
if api_key != st.master_key.as_deref() {
warn!("Unauthorised access to workerconf endpoint from {}", addr.ip());
return (StatusCode::UNAUTHORIZED, "Unauthorized: Invalid API-KEY\n").into_response();
}
if let Some(member) = NODE_STATUS.get() {
if let Some(mbr) = member.as_ref() {
match mbr.as_ref() {
"worker" => {
info!("Accepted new config from master server {}", addr.ip());
let strcontent = content.as_str();
let parsed = noyalib::from_str::<Config>(strcontent);
match parsed {
Ok(_) => {
tokio::spawn(async move { apply_config(content.as_str(), st, true).await });
return (StatusCode::OK, "Accepted! Applying in background\n").into_response();
}
Err(err) => {
error!("Failed to parse upstreams file: {}", err);
return (StatusCode::BAD_GATEWAY, format!("Failed: {}\n", err)).into_response();
}
}
}
_ => {
return (StatusCode::BAD_GATEWAY, "Node is not running on worker mode !\n").into_response();
}
}
}
return (StatusCode::BAD_GATEWAY, "Node is not running on worker mode !\n").into_response();
}
(StatusCode::BAD_GATEWAY, "Node is not running on worker mode !\n").into_response()
}
async fn apply_config(content: &str, st: AppState, save: bool) {
let sl = crate::utils::parceyaml::load_configuration(content, "content").await;
if let Some(serverlist) = sl.0 {
if save {
info!("Saving new upstreams to: {}", st.upstreams_file);
if let Err(err) = std::fs::write(&st.upstreams_file, content) {
error!("Error saving to: {} : {}", st.upstreams_file, err);
}
}
let _ = st.config_sender.send(serverlist).await;
}
}
async fn jwt_gen(State(state): State<AppState>, Json(payload): Json<Claims>) -> (StatusCode, Json<OutToken>) {
if let Some(master_key) = &state.master_key {
if &payload.master_key == master_key {
let now = SystemTime::now() + Duration::from_secs(payload.exp * 60);
let expire = now.duration_since(UNIX_EPOCH).unwrap_or_default().as_secs();
let claim = Claims {
master_key: String::new(),
owner: payload.owner,
exp: expire,
random: payload.random,
};
match encode(&Header::default(), &claim, &EncodingKey::from_secret(payload.master_key.as_ref())) {
Ok(t) => {
let tok = OutToken { token: t };
debug!("Generating token: {:?}", tok.token);
(StatusCode::CREATED, Json(tok))
}
Err(e) => {
let tok = OutToken { token: "ERROR".to_string() };
error!("Failed to generate token: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, Json(tok))
}
}
} else {
let tok = OutToken {
token: "Unauthorised".to_string(),
};
warn!("Unauthorised JWT generate request: {:?}", tok);
(StatusCode::FORBIDDEN, Json(tok))
}
} else {
let tok = OutToken {
token: "ERROR Getting JWT_KEY environment variable".to_string(),
};
error!("ERROR Getting JWT_KEY environment variable");
(StatusCode::INTERNAL_SERVER_ERROR, Json(tok))
}
}
async fn metrics() -> impl IntoResponse {
MEMORY_USAGE.set(get_memory_usage() as i64);
OPEN_FILES.set(get_open_files() as i64);
let metric_families = gather();
let encoder = TextEncoder::new();
let mut buffer = Vec::new();
if let Err(e) = encoder.encode(&metric_families, &mut buffer) {
return (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to encode metrics: {}", e)).into_response();
}
Response::builder()
.status(StatusCode::OK)
.header("Content-Type", encoder.format_type())
.body(Body::from(buffer))
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response())
}
#[allow(clippy::needless_return)]
async fn status(State(st): State<AppState>, Query(params): Query<HashMap<String, String>>) -> impl IntoResponse {
if params.contains_key("live") {
let r = upstreams_liveness_json(&st.full_upstreams, &st.current_upstreams);
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(format!("{}", r)))
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
}
if params.contains_key("all") {
let resp = upstreams_to_json(&st.current_upstreams);
match resp {
Ok(j) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json")
.body(Body::from(j))
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
}
Err(e) => return (StatusCode::BAD_GATEWAY, format!("Internal Server Error {}\n", e)).into_response(),
}
}
(StatusCode::INTERNAL_SERVER_ERROR, "Parameter mismatch\n").into_response()
}
pub async fn port_is_available(name: &str, address: &str) -> TcpListener {
let addr = SocketAddr::from_str(address).unwrap_or_else(|e| panic!("{}: Invalid address format: {:?}", name, e));
let t = Duration::from_secs(2);
let p = addr.port();
loop {
match TcpListener::bind(addr).await {
Ok(listener) => {
return listener;
}
Err(_) => {
warn!("{} port is not available: {} will try again in {:?}", name, p, t);
tokio::time::sleep(t).await;
}
}
}
}
// -- ⚝ by Dave -- in NeoVim ⚝ --
+93
View File
@@ -0,0 +1,93 @@
use crate::ingress::kuberconsul;
use crate::ingress::kuberconsul::{ServiceDiscovery, list_to_upstreams};
use crate::utils::httpclient;
use crate::utils::parceyaml::build_headers;
use crate::utils::tools::Die;
use crate::utils::types::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use pingora::prelude::sleep;
use rand::RngExt;
use serde::Deserialize;
use std::env;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc::Sender;
#[derive(Debug, Deserialize)]
pub struct ConsulService {
#[serde(rename = "Address")]
pub address: String,
#[serde(rename = "ServicePort")]
pub port: u16,
}
pub struct ConsulDiscovery;
#[async_trait]
impl ServiceDiscovery for ConsulDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
loop {
let upstreams = UpstreamsDashMap::new();
if let Some(consul) = config.consul.clone() {
let servers = consul.servers.unwrap_or_else(|| {
vec![format!(
"{}:{}",
env::var("CONSUL_SERVICE_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
env::var("CONSUL_SERVICE_PORT").unwrap_or_else(|_| "0".to_string())
)]
});
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let consul_data = servers.get(num).die("Reading consul data").to_string();
let ss = format!("{}/v1/catalog/service/", consul_data);
let dc = format!("{}/v1/catalog/services?filter=%22aralez.service=yes%22+in+ServiceTags", consul_data);
if let Some(infolist) = httpclient::for_consul_list(dc.as_str(), consul.token.clone()).await {
for (k, v) in infolist {
let client_header_map: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut client_header_list = Vec::new();
build_headers(&v.client_headers, config.as_ref(), &mut client_header_list);
let sever_header_map: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut server_header_list = Vec::new();
build_headers(&v.server_headers, config.as_ref(), &mut server_header_list);
if !client_header_list.is_empty() {
let path_key = v.path.clone();
client_header_map.insert(Arc::from(path_key), client_header_list);
config.client_headers.insert(Arc::from(v.host.clone()), client_header_map);
}
if !server_header_list.is_empty() {
let path_key = v.path.clone();
sever_header_map.insert(Arc::from(path_key), server_header_list);
config.server_headers.insert(Arc::from(v.host.clone()), sever_header_map);
}
let pref = format!("{}{}", ss, k);
let gsm = &GlobalServiceMapping {
upstream: k.clone(),
hostname: v.host.clone(),
path: Some(v.path),
to_https: v.to_https,
redirect_to: v.redirect,
authorization: v.auth,
rate_limit: v.rate,
x4xx_limit: v.xrate,
client_headers: v.client_headers,
server_headers: v.server_headers,
};
let list = httpclient::for_consul(pref.as_str(), consul.token.clone(), gsm).await;
list_to_upstreams(list, &upstreams, gsm);
}
}
}
if let Some(lt) = kuberconsul::clone_compare(&upstreams, &config).await {
let _ = toreturn.send(lt).await;
}
sleep(Duration::from_secs(5)).await;
}
}
}
+68
View File
@@ -0,0 +1,68 @@
use crate::core::webserver;
use crate::ingress::consul::ConsulDiscovery;
use crate::ingress::kuberconsul::ServiceDiscovery;
use crate::ingress::kubernetes::KubernetesDiscovery;
use crate::utils::types::{Configuration, UpstreamsDashMap};
use crate::utils::watch;
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::mpsc::Sender;
pub struct APIUpstreamProvider {
pub config_api_enabled: bool,
pub address: String,
pub masterkey: Option<String>,
pub acme_dns_provider: Option<String>,
pub certs_dir: String,
pub config_dir: String,
pub upstreams_file: String,
pub file_server_address: Option<String>,
pub file_server_folder: Option<String>,
pub current_upstreams: Arc<UpstreamsDashMap>,
pub full_upstreams: Arc<UpstreamsDashMap>,
}
pub struct FromFileProvider {
pub path: String,
}
pub struct ConsulProvider {
pub config: Arc<Configuration>,
}
pub struct KubernetesProvider {
pub config: Arc<Configuration>,
}
#[async_trait]
pub trait Discovery {
async fn start(&self, tx: Sender<Configuration>);
}
#[async_trait]
impl Discovery for APIUpstreamProvider {
async fn start(&self, toreturn: Sender<Configuration>) {
webserver::run_server(self, toreturn, self.current_upstreams.clone(), self.full_upstreams.clone()).await;
}
}
#[async_trait]
impl Discovery for FromFileProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(watch::file_watch(self.path.clone(), tx));
}
}
#[async_trait]
impl Discovery for ConsulProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(ConsulDiscovery.fetch_upstreams(self.config.clone(), tx));
}
}
#[async_trait]
impl Discovery for KubernetesProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(KubernetesDiscovery.fetch_upstreams(self.config.clone(), tx));
}
}
+63
View File
@@ -0,0 +1,63 @@
use crate::utils::tools::{Die, clone_dashmap_into, compare_dashmaps, print_upstreams};
use crate::utils::types::{Configuration, GlobalServiceMapping, InnerMap, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use tokio::fs::File;
use tokio::io::AsyncReadExt;
use tokio::sync::mpsc::Sender;
#[allow(clippy::type_complexity)]
pub fn list_to_upstreams(lt: Option<DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>>, upstreams: &UpstreamsDashMap, i: &GlobalServiceMapping) {
if let Some(list) = lt {
let key: Arc<str> = Arc::from(i.hostname.as_str());
match upstreams.get(&key) {
Some(upstr) => {
for (k, v) in list {
upstr.value().insert(k, v);
}
}
None => {
upstreams.insert(key, list);
}
};
}
}
pub fn match_path(conf: &GlobalServiceMapping, upstreams: &DashMap<Arc<str>, (Vec<Arc<InnerMap>>, AtomicUsize)>, values: Vec<Arc<InnerMap>>) {
let path = conf.path.as_deref().unwrap_or("/");
upstreams.insert(Arc::from(path), (values, AtomicUsize::new(0)));
}
pub async fn read_token(path: &str) -> String {
let mut file = File::open(path).await.die("Reading kubernetes token");
let mut contents = String::new();
file.read_to_string(&mut contents).await.die("Kubernetes token to string");
contents.trim().to_string()
}
#[async_trait]
pub trait ServiceDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>);
}
pub async fn clone_compare(upstreams: &UpstreamsDashMap, config: &Arc<Configuration>) -> Option<Configuration> {
if !compare_dashmaps(upstreams, &config.upstreams) {
let tosend = Configuration {
upstreams: Default::default(),
client_headers: config.client_headers.clone(),
server_headers: config.server_headers.clone(),
consul: config.consul.clone(),
kubernetes: config.kubernetes.clone(),
typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(upstreams, &config.upstreams);
clone_dashmap_into(upstreams, &tosend.upstreams);
print_upstreams(&tosend.upstreams, &tosend.extraparams);
return Some(tosend);
}
None
}
+165
View File
@@ -0,0 +1,165 @@
use crate::utils::httpclient;
use crate::utils::parceyaml::build_headers;
use crate::utils::types::{Configuration, GlobalServiceMapping, UpstreamsDashMap};
use async_trait::async_trait;
use dashmap::DashMap;
use log::error;
use rand::RngExt;
use std::env;
use std::sync::Arc;
use crate::ingress::kuberconsul;
use crate::ingress::kuberconsul::{ServiceDiscovery, clone_compare, list_to_upstreams};
use crate::ingress::kubewatcher::{AralezRouteUpdate, start_ingress_watcher_with_config};
use crate::utils::tools::Die;
use rustls::crypto::CryptoProvider;
use rustls::crypto::ring::default_provider;
use tokio::sync::mpsc::Sender;
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeEndpointSliceList {
#[serde(default)]
pub items: Vec<KubeEndpointSlice>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeEndpointSlice {
pub ports: Option<Vec<KubePort>>,
#[serde(default)]
pub endpoints: Vec<KubeEndpoint>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeEndpoint {
#[serde(default)]
pub addresses: Vec<String>,
pub conditions: Option<KubeConditions>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubeConditions {
pub ready: Option<bool>,
}
#[derive(Debug, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct KubePort {
pub port: Option<u16>,
}
pub struct KubernetesDiscovery;
#[async_trait]
impl ServiceDiscovery for KubernetesDiscovery {
async fn fetch_upstreams(&self, config: Arc<Configuration>, toreturn: Sender<Configuration>) {
if let Some(kuber) = config.kubernetes.clone() {
let servers = kuber.servers.unwrap_or_else(|| {
vec![format!(
"{}:{}",
env::var("KUBERNETES_SERVICE_HOST").unwrap_or_else(|_| "0.0.0.0".to_string()),
env::var("KUBERNETES_SERVICE_PORT_HTTPS").unwrap_or_else(|_| "0".to_string())
)]
});
let end = servers.len().saturating_sub(1);
let num = if end > 0 { rand::rng().random_range(0..end) } else { 0 };
let server = servers.get(num).die("Getting k8s api server").to_string();
let path = kuber.tokenpath.unwrap_or_else(|| "/var/run/secrets/kubernetes.io/serviceaccount/token".to_string());
let token = kuberconsul::read_token(path.as_str()).await;
if CryptoProvider::get_default().is_none() {
default_provider().install_default().die("Failed to install rustls crypto provider");
}
let (tx, mut rx) = tokio::sync::mpsc::channel::<AralezRouteUpdate>(100);
let srv = server.clone();
let tok = token.clone();
tokio::spawn(async move {
if let Err(e) = start_ingress_watcher_with_config(srv.as_str(), tok.as_str(), tx, "aralez").await {
error!("Fatal watcher error: {:?}", e);
}
});
let upstreams = UpstreamsDashMap::new();
while let Some(update) = rx.recv().await {
let host_key: Arc<str> = Arc::from(update.host.as_str());
if update.is_deleted {
upstreams.remove(&host_key);
config.client_headers.remove(&host_key);
} else {
let service = GlobalServiceMapping {
upstream: update.service_name.clone(),
hostname: update.host.clone(),
path: Some(update.path.clone()),
to_https: None,
redirect_to: None,
authorization: None,
rate_limit: update.rate_limit,
x4xx_limit: update.x4xx_limit,
client_headers: None,
server_headers: None,
};
let cheader_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let sheader_list: DashMap<Arc<str>, Vec<(String, Arc<str>)>> = DashMap::new();
let mut client_headers_list = Vec::new();
let mut server_headers_list = Vec::new();
let mut chl: Option<Vec<String>> = None;
let mut shl: Option<Vec<String>> = None;
if let Some(hdr) = service.client_headers.as_ref() {
chl.get_or_insert_with(Vec::new).extend(hdr.iter().cloned());
}
if let Some(hdr) = service.server_headers.as_ref() {
shl.get_or_insert_with(Vec::new).extend(hdr.iter().cloned());
}
if let Some(ch) = update.client_headers {
chl.get_or_insert_with(Vec::new).extend(ch);
}
if let Some(hdr) = update.server_headers.as_ref() {
shl.get_or_insert_with(Vec::new).extend(hdr.iter().cloned());
}
build_headers(&chl, config.as_ref(), &mut client_headers_list);
if !client_headers_list.is_empty() {
let path_key = service.path.as_deref().unwrap_or("/");
cheader_list.insert(Arc::from(path_key), client_headers_list);
config.client_headers.insert(host_key.clone(), cheader_list);
}
build_headers(&shl, config.as_ref(), &mut server_headers_list);
if !server_headers_list.is_empty() {
let path_key = service.path.as_deref().unwrap_or("/");
sheader_list.insert(Arc::from(path_key), server_headers_list);
config.server_headers.insert(host_key.clone(), sheader_list);
}
let url = format!(
"https://{}/apis/discovery.k8s.io/v1/namespaces/{}/endpointslices?labelSelector=kubernetes.io%2Fservice-name%3D{}",
server, update.namespace, update.service_name
);
let list = httpclient::for_kuber(&url, &token, &service).await;
if list.is_none() {
error!("Endpoint URL: {} returned empty/invalid response", url);
}
list_to_upstreams(list, &upstreams, &service);
}
if let Some(lt) = clone_compare(&upstreams, &config).await {
let _ = toreturn.send(lt).await;
}
}
}
}
}
+279
View File
@@ -0,0 +1,279 @@
use dashmap::DashMap;
use futures::StreamExt;
use hyper::Uri;
use k8s_openapi::api::core::v1::{Node, Service};
use k8s_openapi::api::networking::v1::Ingress;
use kube::api::{Patch, PatchParams};
use kube::{
api::Api,
client::Client,
config::{AuthInfo, Config},
runtime::watcher::{self, Event},
};
use log::{error, info};
use serde_json::json;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::mpsc;
pub struct AralezRouteUpdate {
pub host: String,
pub path: String,
pub namespace: String,
pub service_name: String,
pub is_deleted: bool,
pub rate_limit: Option<isize>,
pub x4xx_limit: Option<u32>,
pub client_headers: Option<Vec<String>>,
pub server_headers: Option<Vec<String>>,
}
#[derive(Debug, Clone)]
struct AralezAnnotation {
rate_limit: Option<isize>,
x4xx_limit: Option<u32>,
namespace: String,
client_headers: Option<Vec<String>>,
server_headers: Option<Vec<String>>,
}
lazy_static::lazy_static! {
static ref PATCHED_INGRESSES: Arc<DashMap<String, String>> = Arc::new(DashMap::new());
}
pub async fn start_ingress_watcher_with_config(
api_server: &str,
token: &str,
tx: mpsc::Sender<AralezRouteUpdate>,
target_class: &'static str,
) -> Result<(), Box<dyn std::error::Error>> {
let client = create_custom_k8s_client(api_server, token, true).await?;
let ingresses: Api<Ingress> = Api::all(client.clone());
let watcher_config = watcher::Config::default().any_semantic();
let mut stream = watcher::watcher(ingresses, watcher_config).boxed();
while let Some(event) = stream.next().await {
match event {
Ok(Event::Apply(ing)) => {
process_ingress_change(&client, &ing, target_class, false, &tx).await;
}
Ok(Event::Delete(ing)) => {
process_ingress_change(&client, &ing, target_class, true, &tx).await;
}
Ok(Event::InitApply(ing)) => {
process_ingress_change(&client, &ing, target_class, false, &tx).await;
}
Ok(Event::InitDone) => {
info!("Ingress watcher initial sync complete");
}
Err(err) => {
error!("Kubernetes Ingress watch error: {:?}", err);
}
_ => {}
}
}
Ok(())
}
async fn process_ingress_change(client: &Client, ing: &Ingress, target_class: &str, is_deleted: bool, tx: &mpsc::Sender<AralezRouteUpdate>) {
let spec = match &ing.spec {
Some(s) => s,
None => return,
};
let class_name = spec.ingress_class_name.as_deref().unwrap_or_default();
if class_name != target_class {
return;
}
if !is_deleted {
if let (Some(name), Some(ns)) = (ing.metadata.name.as_deref(), ing.metadata.namespace.as_deref()) {
let key = format!("{}/{}", ns, name);
let external_ip = resolve_external_ip(client).await;
let status_already_correct = ing
.status
.as_ref()
.and_then(|s| s.load_balancer.as_ref())
.and_then(|lb| lb.ingress.as_ref())
.map(|list| list.iter().any(|i| i.ip.as_deref() == Some(&external_ip)))
.unwrap_or(false);
if status_already_correct {
PATCHED_INGRESSES.insert(key, external_ip);
} else {
PATCHED_INGRESSES.insert(key.clone(), external_ip.clone());
let client_clone = client.clone();
let name_clone = name.to_string();
let ns_clone = ns.to_string();
let key_clone = key.clone();
tokio::spawn(async move {
if let Err(e) = patch_ingress_status_address(&client_clone, &ns_clone, &name_clone, &external_ip).await {
error!("Failed to patch ingress status address {}/{}: {}", ns_clone, name_clone, e);
PATCHED_INGRESSES.remove(&key_clone);
}
});
}
}
} else {
if let (Some(name), Some(ns)) = (ing.metadata.name.as_deref(), ing.metadata.namespace.as_deref()) {
PATCHED_INGRESSES.remove(&format!("{}/{}", ns, name));
}
}
if let Some(rules) = &spec.rules {
for rule in rules {
let host = rule.host.clone().unwrap_or_else(|| "*".to_string());
if let Some(http) = &rule.http {
for path_entry in &http.paths {
let path = path_entry.path.clone().unwrap_or_else(|| "/".to_string());
if let Some(service) = &path_entry.backend.service {
let service_name = service.name.clone();
let annot = parse_ingress_config(ing);
let update = AralezRouteUpdate {
host: host.clone(),
path,
service_name,
is_deleted,
rate_limit: annot.rate_limit,
x4xx_limit: annot.x4xx_limit,
client_headers: annot.client_headers,
server_headers: annot.server_headers,
namespace: annot.namespace,
};
if let Err(e) = tx.send(update).await {
error!("Failed to send route update to channel: {:?}", e);
}
}
}
}
}
}
}
pub async fn create_custom_k8s_client(api_server_url: &str, bearer_token: &str, insecure_tls: bool) -> Result<Client, Box<dyn std::error::Error>> {
let full_url = if !api_server_url.starts_with("http://") && !api_server_url.starts_with("https://") {
format!("https://{}", api_server_url)
} else {
api_server_url.to_string()
};
let uri: Uri = full_url.parse()?;
let mut config = Config::new(uri);
config.auth_info = AuthInfo {
token: Some(bearer_token.trim().to_string().into()),
..Default::default()
};
config.connect_timeout = Some(Duration::from_secs(10));
config.read_timeout = None;
if insecure_tls {
config.accept_invalid_certs = true;
}
let client = Client::try_from(config)?;
Ok(client)
}
fn parse_ingress_config(ing: &Ingress) -> AralezAnnotation {
let namespace = ing.metadata.namespace.clone().unwrap_or_else(|| "default".to_string());
let annotations = ing.metadata.annotations.as_ref();
let client_headers = annotations.and_then(|a| a.get("aralez.rs/client_headers")).and_then(|ch| serde_json::from_str(ch).ok());
let server_headers = annotations.and_then(|a| a.get("aralez.rs/server_headers")).and_then(|sh| serde_json::from_str(sh).ok());
AralezAnnotation {
rate_limit: annotations.and_then(|a| a.get("aralez.rs/rate_limit")).and_then(|v| v.parse().ok()),
x4xx_limit: annotations.and_then(|a| a.get("aralez.rs/x4xx_limit")).and_then(|v| v.parse().ok()),
client_headers,
server_headers,
namespace,
}
}
pub async fn patch_ingress_status_address(client: &Client, namespace: &str, name: &str, external_ip: &str) -> Result<(), kube::Error> {
let ingresses: Api<Ingress> = Api::namespaced(client.clone(), namespace);
let patch = json!({
"status": {
"loadBalancer": {
"ingress": [
{
"ip": external_ip
}
]
}
}
});
let params = PatchParams::default();
match ingresses.patch_status(name, &params, &Patch::Strategic(patch)).await {
Ok(_) => {
info!("Successfully patched Ingress {}/{} ADDRESS to {}", namespace, name, external_ip);
Ok(())
}
Err(e) => {
error!("Failed to patch status for Ingress {}/{}: {:?}", namespace, name, e);
Err(e)
}
}
}
pub async fn discover_node_ip(client: &Client) -> Option<String> {
let nodes: Api<Node> = Api::all(client.clone());
if let Ok(node_list) = nodes.list(&Default::default()).await {
for node in node_list {
if let Some(status) = node.status {
if let Some(addresses) = status.addresses {
for addr in &addresses {
if addr.type_ == "ExternalIP" {
return Some(addr.address.clone());
}
}
for addr in &addresses {
if addr.type_ == "InternalIP" {
return Some(addr.address.clone());
}
}
}
}
}
}
None
}
pub async fn discover_controller_ip(client: &Client, namespace: &str, service_name: &str) -> Option<String> {
let services: Api<Service> = Api::namespaced(client.clone(), namespace);
if let Ok(svc) = services.get(service_name).await {
if let Some(status) = svc.status {
if let Some(lb) = status.load_balancer {
if let Some(ingress_list) = lb.ingress {
for ing in ingress_list {
if let Some(ip) = ing.ip {
return Some(ip);
}
if let Some(hostname) = ing.hostname {
return Some(hostname);
}
}
}
}
}
}
None
}
pub async fn resolve_external_ip(client: &Client) -> String {
if let Ok(ip) = std::env::var("ARALEZ_EXTERNAL_IP") {
let trimmed = ip.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
let svc_name = std::env::var("ARALEZ_SERVICE_NAME").unwrap_or_else(|_| "aralez-service".to_string());
let svc_ns = std::env::var("ARALEZ_SERVICE_NAMESPACE").unwrap_or_else(|_| "aralez".to_string());
if let Some(ip) = discover_controller_ip(client, &svc_ns, &svc_name).await {
return ip;
}
if let Some(ip) = discover_node_ip(client).await {
return ip;
}
"127.0.0.1".to_string()
}
+7
View File
@@ -0,0 +1,7 @@
pub mod consul;
pub mod discovery;
pub mod kuberconsul;
pub mod kubernetes;
pub mod kubewatcher;
pub use discovery::*;
+7
View File
@@ -0,0 +1,7 @@
pub mod auth;
pub mod cache;
pub mod core;
pub mod ingress;
pub mod logging;
pub mod tls;
pub mod utils;
+337
View File
@@ -0,0 +1,337 @@
use crate::logging::types::start_logging_backend;
use crate::utils::metrics::LOGGING_ERRORS;
use crate::utils::tools::Die;
use crate::utils::types::AppConfig;
use anyhow::Result;
use log::{Level, LevelFilter, Record, error, info, log};
use log4rs::append::Append;
use log4rs::append::rolling_file::RollingFileAppender;
use log4rs::append::rolling_file::policy::compound::CompoundPolicy;
use log4rs::append::rolling_file::policy::compound::roll::fixed_window::FixedWindowRoller;
use log4rs::append::rolling_file::policy::compound::trigger::size::SizeTrigger;
use log4rs::config::Logger;
use log4rs::{
append::console::ConsoleAppender,
config::{Appender, Config as Log4rsConfig, Root},
encode::pattern::PatternEncoder,
};
use pingora_cache::CachePhase;
use pingora_http::Version;
use pingora_proxy::Session;
use serde::Serialize;
use std::cell::RefCell;
use std::fmt::Write;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::OnceLock;
use tokio::sync::mpsc;
#[derive(Debug)]
pub struct LogMessage {
pub response_code: u16,
pub summary: String,
pub client_ip: IpAddr,
pub version: Version,
pub user_agent: String,
pub cache_status: CachePhase,
}
#[derive(Debug, Serialize, Clone)]
pub struct StructuredSystemLog {
pub target: &'static str,
pub level: Level,
pub message: String,
}
thread_local! {
static LOG_BUF: RefCell<String> = RefCell::new(String::with_capacity(512));
}
static ACCESS_LOG_SENDER: OnceLock<mpsc::Sender<LogMessage>> = OnceLock::new();
pub(crate) static SYSTEM_LOG_SENDER: OnceLock<mpsc::Sender<StructuredSystemLog>> = OnceLock::new();
static ACCESS_LOG: OnceLock<LogLevel> = OnceLock::new();
const LOG_BUFFER: usize = 16384;
static IS_STRUCTURED: OnceLock<bool> = OnceLock::new();
static LOG_BACKEND: OnceLock<&'static str> = OnceLock::new();
pub fn set_backend(key: String) -> Result<(), &'static str> {
let static_key: &'static str = Box::leak(key.into_boxed_str());
LOG_BACKEND.set(static_key).map_err(|_| "LOG_BACKEND was already initialized!")
}
pub fn get_backend() -> &'static str {
LOG_BACKEND.get().copied().unwrap_or("system")
}
#[derive(Debug)]
pub struct StructuredChannelAppender {
sender: mpsc::Sender<StructuredSystemLog>,
}
impl StructuredChannelAppender {
pub fn new(sender: mpsc::Sender<StructuredSystemLog>) -> Self {
Self { sender }
}
}
impl Append for StructuredChannelAppender {
fn append(&self, record: &Record) -> Result<()> {
let message = LOG_BUF.with(|buf| {
let mut b = buf.borrow_mut();
b.clear();
let _ = write!(&mut *b, "{}", record.args());
b.clone()
});
let target: &'static str = unsafe { std::mem::transmute(record.target()) };
let msg = StructuredSystemLog {
target,
level: record.level(),
message,
};
let _ = self.sender.try_send(msg);
Ok(())
}
fn flush(&self) {}
}
pub async fn log_builder(conf: &AppConfig, location: &Option<String>) {
let log_level = match conf.log_level.as_str() {
"info" => LevelFilter::Info,
"error" => LevelFilter::Error,
"warn" => LevelFilter::Warn,
"debug" => LevelFilter::Debug,
"trace" => LevelFilter::Trace,
"off" => LevelFilter::Off,
_ => {
println!("Error reading log level, defaulting to: INFO");
LevelFilter::Info
}
};
let mut pat: String = "{d(%Y-%m-%d %H:%M:%S)} {l} - {m} {n}".to_string();
if let Some(ptrn) = conf.log_pattern.clone() {
pat = ptrn;
}
let pattern = pat.as_str();
if let Some(backend) = conf.log_structured.as_deref() {
if let Some(backend_name) = backend.split_whitespace().next() {
let _ = set_backend(backend_name.to_string());
init_structured_log().await;
}
let syslog_appender: Option<Box<dyn Append>> = SYSTEM_LOG_SENDER
.get()
.cloned()
.map(|sender| Box::new(StructuredChannelAppender::new(sender)) as Box<dyn Append>);
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let mut config_builder = Log4rsConfig::builder().appender(Appender::builder().build("stdout", Box::new(stdout)));
if let Some(appender) = syslog_appender {
config_builder = config_builder
.appender(Appender::builder().build("syslog_appender", appender))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_proxy", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_core", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_pool", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("pingora_cache", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("hyper_util", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("aralez", log_level))
.logger(Logger::builder().appender("syslog_appender").additive(false).build("h2", log_level));
} else {
config_builder = config_builder
.logger(Logger::builder().build("pingora_core", LevelFilter::Off))
.logger(Logger::builder().build("pingora_pool", LevelFilter::Off))
.logger(Logger::builder().build("pingora_cache", LevelFilter::Off))
.logger(Logger::builder().build("hyper_util", LevelFilter::Off))
.logger(Logger::builder().build("aralez", LevelFilter::Off))
.logger(Logger::builder().build("h2", LevelFilter::Off));
}
let config = config_builder.build(Root::builder().appender("stdout").build(log_level)).die("Logging config builder");
if let Err(e) = log4rs::init_config(config) {
eprintln!("Warning: log4rs logger already initialized: {:?}", e);
}
info!("Enabling structured logging with backend : {}", backend);
let _ = IS_STRUCTURED.set(true);
return;
}
if let Some(location) = location {
let parts: Vec<&str> = location.split(',').map(|s| s.trim()).collect();
let path = match parts.first().filter(|s| !s.is_empty()) {
Some(p) => p,
None => {
error!("Invalid log location string provided; falling back to stdout");
return;
}
};
let compress = parts.get(3).unwrap_or(&"No");
let size_mb: u64 = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(100);
let keep: u32 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(5);
let pattern_str = match compress {
&"compress" => format!("{}.{{}}.gz", path),
_ => format!("{}.{{}}", path),
};
let roller = FixedWindowRoller::builder().build(pattern_str.as_str(), keep).die("Logging roller builder");
let trigger = SizeTrigger::new(size_mb * 1024 * 1024);
let policy = CompoundPolicy::new(Box::new(trigger), Box::new(roller));
let file = match RollingFileAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build(path, Box::new(policy)) {
Ok(f) => f,
Err(e) => {
error!("Failed to create log file at {}: {}. Falling back to stdout.", path, e);
return;
}
};
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.logger(Logger::builder().build("pingora_proxy", LevelFilter::Off))
.build(Root::builder().appender("file").build(log_level))
.die("Parsing Lo4rsConfig");
if let Err(e) = log4rs::init_config(config) {
eprintln!("Warning: log4rs logger already initialized: {:?}", e);
}
info!("Logging to: {}, Max file size: {}mb, Files to keep: {}, compression: {} ", path, size_mb, keep, compress);
} else {
let stdout = ConsoleAppender::builder().encoder(Box::new(PatternEncoder::new(pattern))).build();
let config = Log4rsConfig::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.logger(Logger::builder().build("pingora_proxy", LevelFilter::Off))
.build(Root::builder().appender("stdout").build(log_level))
.die("Parsing Lo4rsConfig");
if let Err(e) = log4rs::init_config(config) {
eprintln!("Warning: log4rs logger already initialized: {:?}", e);
}
info!("No files are configured, logging to stdout");
}
}
pub async fn init_access_log(level_str: &str) {
let level = LogLevel::parse(level_str);
let _ = ACCESS_LOG.set(level);
}
#[derive(Debug)]
pub enum LogLevel {
Access,
Error,
None,
}
impl LogLevel {
pub fn parse(s: &str) -> Self {
match s {
"all" => LogLevel::Access,
"error" => LogLevel::Error,
_ => LogLevel::None,
}
}
}
#[derive(Debug)]
pub enum MatchStatus {
Ok2xx,
Er4xx,
Er5xx,
}
impl MatchStatus {
#[inline]
pub fn from_code(code: u16) -> Self {
match code {
100..=399 => Self::Ok2xx,
400..=499 => Self::Er4xx,
_ => Self::Er5xx,
}
}
}
pub async fn access_log(response_code: u16, summary: &str, session: &Session) {
let level = ACCESS_LOG.get().unwrap_or(&LogLevel::None);
let status = MatchStatus::from_code(response_code);
let should_log = match level {
LogLevel::Access => true,
LogLevel::None => false,
LogLevel::Error => matches!(status, MatchStatus::Er5xx),
};
if !should_log {
return;
}
let ip = session
.client_addr()
.and_then(|addr| addr.as_inet())
.map(|addr| addr.ip())
.unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST));
let user_agent = session.req_header().headers.get("user-agent").and_then(|v| v.to_str().ok()).unwrap_or("-");
let msg = LogMessage {
response_code,
summary: summary.to_owned(),
client_ip: ip,
version: session.req_header().version,
user_agent: user_agent.to_owned(),
cache_status: session.cache.phase(),
};
if IS_STRUCTURED.get().is_some() {
write_access_log(&msg).await;
return;
}
if let Some(sender) = ACCESS_LOG_SENDER.get() {
if sender.try_send(msg).is_err() {
LOGGING_ERRORS.inc();
}
}
}
pub async fn init_access_logging(enabled: Option<String>) {
if enabled.is_some() {
LOGGING_ERRORS.set(0);
info!("Enabling {:?} log, with buffer of {} messages", ACCESS_LOG.get().unwrap_or(&LogLevel::None), LOG_BUFFER);
let (ltx, lrx) = mpsc::channel(LOG_BUFFER);
let _ = ACCESS_LOG_SENDER.set(ltx);
tokio::spawn(async move { access_log_receiver(lrx).await });
}
}
pub async fn init_structured_log() {
let (tx, rx) = mpsc::channel::<StructuredSystemLog>(LOG_BUFFER);
let _ = SYSTEM_LOG_SENDER.set(tx);
let backend = get_backend();
start_logging_backend(backend, rx);
}
pub async fn access_log_receiver(mut receiver: mpsc::Receiver<LogMessage>) {
while let Some(msg) = receiver.recv().await {
write_access_log(&msg).await;
}
}
async fn write_access_log(msg: &LogMessage) {
let level = match MatchStatus::from_code(msg.response_code) {
MatchStatus::Ok2xx => Level::Info,
MatchStatus::Er4xx => Level::Warn,
MatchStatus::Er5xx => Level::Error,
};
log!(
level,
"{}, {}, {}, client: {}, version: {:?}, useragent: {}",
msg.response_code,
msg.cache_status.as_str(),
msg.summary,
msg.client_ip,
msg.version,
msg.user_agent,
);
}
+187
View File
@@ -0,0 +1,187 @@
use crate::logging::core::StructuredSystemLog;
use crate::logging::types::{LogBackendPlugin, WriteLog};
use crate::utils::tools::get_hostname;
use async_trait::async_trait;
use bytes::{BufMut, BytesMut};
use elasticsearch::auth::Credentials;
use elasticsearch::http::transport::Transport;
use elasticsearch::{BulkParts, Elasticsearch};
use humantime::format_rfc3339;
use serde_json::json;
use std::env;
use std::sync::LazyLock;
use std::time::{Duration, SystemTime};
use tokio::sync::mpsc;
use tokio::sync::OnceCell;
static CLIENT: OnceCell<Elasticsearch> = OnceCell::const_new();
#[derive(Debug)]
struct ElasticHost {
url: &'static str,
}
struct ElasticSearchConfig {
pub hosts: &'static [ElasticHost],
pub user: &'static str,
pub password: &'static str,
pub hostname: &'static str,
pub index_name: &'static str,
pub flush_duration: u64,
pub batch_len: usize,
pub buffer_size: usize,
}
static ELASTIC: LazyLock<ElasticSearchConfig> = LazyLock::new(ElasticSearchConfig::load_from_env);
impl ElasticSearchConfig {
pub fn load_from_env() -> Self {
let raw_hosts = env::var("LOG_ELASTIC_HOSTS").unwrap_or_else(|_| "http://127.0.0.1:9200".to_string());
let mut hosts_struct = Vec::new();
for entry in raw_hosts.split(',') {
let entry = entry.trim();
if entry.is_empty() {
continue;
}
let (scheme, rest) = if let Some(stripped) = entry.strip_prefix("https://") {
("https", stripped)
} else if let Some(stripped) = entry.strip_prefix("http://") {
("http", stripped)
} else {
("http", entry)
};
let (address, port) = match rest.split_once(':') {
Some((host_part, port_part)) => {
let port = port_part.parse::<u16>().unwrap_or(9200);
(host_part, port)
}
None => (rest, 9200),
};
let full_url = format!("{}://{}:{}", scheme, address, port);
hosts_struct.push(ElasticHost {
url: Box::leak(full_url.into_boxed_str()),
});
}
let user = env::var("LOG_ELASTIC_USER").unwrap_or_else(|_| "elastic".to_string());
let password = env::var("LOG_ELASTIC_PASSWORD").unwrap_or_else(|_| "elastic".to_string());
let index_name = env::var("LOG_ELASTIC_INDEX").unwrap_or_else(|_| "aralez-logs".to_string());
let s_flush_duration = env::var("LOG_ELASTIC_FLUSH_DURATION").unwrap_or_else(|_| "2".to_string());
let flush_duration = s_flush_duration.parse::<u64>().unwrap_or(2);
let s_batch_len = env::var("LOG_ELASTIC_BATCH_LEN").unwrap_or_else(|_| "200".to_string());
let batch_len = s_batch_len.parse::<usize>().unwrap_or(200);
let buffer_size: usize = 100 * 512;
Self {
hosts: Box::leak(hosts_struct.into_boxed_slice()),
user: Box::leak(user.into_boxed_str()),
password: Box::leak(password.into_boxed_str()),
flush_duration,
batch_len,
buffer_size,
hostname: Box::leak(get_hostname().into_boxed_str()),
index_name: Box::leak(index_name.into_boxed_str()),
}
}
}
async fn make_es_pool() -> Elasticsearch {
let valid_urls: Vec<&str> = ELASTIC.hosts.iter().map(|target| target.url).collect();
if valid_urls.is_empty() {
log::error!("No valid Elasticsearch host URLs found in configuration");
return Elasticsearch::default();
}
let transport = match Transport::sniffing_node_list(valid_urls, Duration::from_secs(180)) {
Ok(t) => t,
Err(e) => {
log::error!("Failed to create sniffing transport: {}", e);
return Elasticsearch::default();
}
};
let credentials = Credentials::Basic(ELASTIC.user.to_string(), ELASTIC.password.to_string());
transport.set_auth(credentials);
let client = Elasticsearch::new(transport);
if let Ok(res) = client.ping().send().await {
if res.status_code().is_success() {
log::info!("Elasticsearch sniffing pool initialized successfully");
return client;
}
}
log::warn!("Elasticsearch pool initialized, but cluster ping returned non-200");
client
}
struct ElasticSearch;
#[async_trait]
impl WriteLog for ElasticSearch {
async fn run(&self, mut rx: mpsc::Receiver<StructuredSystemLog>) {
let mut batch = Vec::with_capacity(ELASTIC.batch_len);
let mut timer = tokio::time::interval(Duration::from_secs(ELASTIC.flush_duration));
let mut buffer = BytesMut::with_capacity(ELASTIC.buffer_size);
let index_header = format!("{{\"index\":{{\"_index\":\"{}\"}}}}\n", ELASTIC.index_name).into_bytes();
loop {
tokio::select! {
Some(msg) = rx.recv() => {
batch.push(msg);
if batch.len() >= ELASTIC.batch_len {
flush_to_es(&mut batch, &mut buffer, &index_header, ELASTIC.hostname).await;
}
}
_ = timer.tick() => {
if !batch.is_empty() {
flush_to_es(&mut batch, &mut buffer, &index_header, ELASTIC.hostname).await;
}
}
}
}
}
}
async fn flush_to_es(batch: &mut Vec<StructuredSystemLog>, buffer: &mut BytesMut, index_header: &[u8], hostname: &str) {
buffer.clear();
for msg in batch.drain(..) {
buffer.extend_from_slice(index_header);
let logstash_payload = json!({
"@timestamp": format_rfc3339(SystemTime::now()).to_string(),
"@version": "1",
"log": {
"level": msg.level,
"logger": msg.target,
},
"message": msg.message,
"host": {
"hostname": hostname,
}
});
if serde_json::to_writer((buffer).writer(), &logstash_payload).is_ok() {
buffer.extend_from_slice(b"\n");
}
}
if buffer.is_empty() {
return;
}
let payload = buffer.split().freeze();
let client = CLIENT.get_or_init(make_es_pool).await;
let _ = client.bulk(BulkParts::None).body(vec![payload]).send().await;
}
inventory::submit! {
LogBackendPlugin {
name: "elasticsearch",
factory: || Box::new(ElasticSearch),
}
}
+22
View File
@@ -0,0 +1,22 @@
use crate::logging::core::StructuredSystemLog;
use crate::logging::types::{LogBackendPlugin, WriteLog};
use async_trait::async_trait;
use tokio::sync::mpsc;
pub struct Example;
#[async_trait]
impl WriteLog for Example {
async fn run(&self, mut rx: mpsc::Receiver<StructuredSystemLog>) {
while let Some(msg) = rx.recv().await {
println!("Sending log Example : {:?}", msg);
}
}
}
inventory::submit! {
LogBackendPlugin {
name: "example",
factory: || Box::new(Example),
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod core;
pub mod elasticsearch;
pub mod example;
pub mod types;
+36
View File
@@ -0,0 +1,36 @@
use crate::logging::core::StructuredSystemLog;
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::LazyLock;
use tokio::sync::mpsc;
#[async_trait]
pub trait WriteLog: Send + Sync {
async fn run(&self, rx: mpsc::Receiver<StructuredSystemLog>);
}
pub struct LogBackendPlugin {
pub name: &'static str,
pub factory: fn() -> Box<dyn WriteLog>,
}
inventory::collect!(LogBackendPlugin);
static BACKENDS: LazyLock<HashMap<&'static str, Box<dyn WriteLog>>> = LazyLock::new(|| {
let mut map = HashMap::new();
for plugin in inventory::iter::<LogBackendPlugin> {
map.insert(plugin.name, (plugin.factory)());
}
map
});
pub fn start_logging_backend(backend: &str, rx: mpsc::Receiver<StructuredSystemLog>) {
if let Some(logger) = BACKENDS.get(backend) {
let logger_ref = logger.as_ref();
tokio::spawn(async move {
logger_ref.run(rx).await;
});
} else {
log::warn!("Unsupported logging mechanism: {}", backend);
}
}
+8 -3
View File
@@ -1,9 +1,14 @@
mod utils;
mod web;
use aralez::core;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
// pub static A: CountingAllocator = CountingAllocator;
fn main() {
web::start::run();
if std::env::args().any(|a| a == "--version" || a == "-v") {
println!("aralez {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
core::start::run();
}
+58
View File
@@ -0,0 +1,58 @@
use instant_acme::{Account, AccountCredentials, LetsEncrypt, NewAccount};
use log::info;
use std::fs;
use std::path::Path;
use std::sync::OnceLock;
static ACCOUNT: OnceLock<Account> = OnceLock::new();
pub async fn get_account(file: &str) -> Result<&'static Account, Box<dyn std::error::Error>> {
if let Some(account) = ACCOUNT.get() {
return Ok(account);
}
if let Some(credentials) = load_credentials(file) {
let acc_builder = Account::builder()?;
let account = acc_builder.from_credentials(credentials).await?;
let _ = ACCOUNT.set(account);
info!("Loaded existing ACME account");
} else {
info!("No existing credentials found, creating new account");
create_account(file).await?;
}
ACCOUNT.get().ok_or("Failed to initialize account".into())
}
async fn create_account(file: &str) -> Result<(), Box<dyn std::error::Error>> {
let new_account = NewAccount {
contact: &[],
terms_of_service_agreed: true,
only_return_existing: false,
};
let acc_builder = Account::builder()?;
let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Production.url().to_string(), None).await?;
// let (account, credentials) = acc_builder.create(&new_account, LetsEncrypt::Staging.url().to_string(), None).await?;
info!("Account created: {:?}", account.id());
save_credentials(&credentials, file)?;
let _ = ACCOUNT.set(account);
Ok(())
}
pub async fn load_or_create(file: &str) -> Result<String, Box<dyn std::error::Error>> {
let account = get_account(file).await?;
Ok(account.id().to_string() + "\n")
}
fn save_credentials(credentials: &AccountCredentials, file: &str) -> Result<(), Box<dyn std::error::Error>> {
let json = serde_json::to_string_pretty(credentials)?;
fs::write(file, json)?;
info!("ACME credentials saved to {}", file);
Ok(())
}
fn load_credentials(file: &str) -> Option<AccountCredentials> {
if !Path::new(file).exists() {
return None;
}
let json = fs::read_to_string(file).ok()?;
serde_json::from_str(&json).ok()
}
+75
View File
@@ -0,0 +1,75 @@
use crate::tls::acme::lookup::lookup_wait;
use crate::tls::acme::types::{DnsBackendPlugin, DnsProvider};
use crate::utils::tools::Die;
use log::info;
use reqwest::Client;
use serde_json::json;
pub struct CloudflareProvider {
pub client: Client,
pub api_token: String,
pub zone_id: String,
}
impl CloudflareProvider {
pub fn new() -> Self {
let api_token = std::env::var("CLOUDFLARE_API_TOKEN").die("Environment variable CLOUDFLARE_API_TOKEN is required");
let zone_id = std::env::var("CLOUDFLARE_ZONE_ID").die("Environment variable CLOUDFLARE_ZONE_ID is required");
Self {
client: Client::new(),
api_token,
zone_id,
}
}
}
#[async_trait::async_trait]
impl DnsProvider for CloudflareProvider {
async fn create_txt_record(&self, _domain: &str, name: &str, value: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records", self.zone_id);
let response = self
.client
.post(&url)
.bearer_auth(&self.api_token)
.json(&json!({
"type": "TXT",
"name": name,
"content": value,
"ttl": 120,
}))
.send()
.await?;
let res_json: serde_json::Value = response.json().await?;
if !res_json["success"].as_bool().unwrap_or(false) {
return Err(format!("Cloudflare API error: {:?}", res_json["errors"]).into());
}
let record_id = res_json["result"]["id"].as_str().ok_or("missing id")?.to_string();
info!("Created TXT record name: {}, id: {}", name, record_id);
lookup_wait(name, Some("1.1.1.1"), 60, value).await;
Ok(record_id)
}
async fn delete_txt_record(&self, record_id: &str, record_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://api.cloudflare.com/client/v4/zones/{}/dns_records/{}", self.zone_id, record_id);
let response = self.client.delete(&url).bearer_auth(&self.api_token).send().await?;
let res_json: serde_json::Value = response.json().await?;
if !res_json["success"].as_bool().unwrap_or(false) {
return Err(format!("Cloudflare API error deleting record: {:?}", res_json["errors"]).into());
}
info!("Deleted TXT record name: {}, id: {}", record_name, record_id);
Ok(())
}
}
inventory::submit! {
DnsBackendPlugin {
name: "cloudflare",
factory: || Box::new(CloudflareProvider::new()),
}
}
+71
View File
@@ -0,0 +1,71 @@
use crate::tls::acme::lookup::lookup_wait;
use crate::tls::acme::types::{DnsBackendPlugin, DnsProvider};
use crate::utils::tools::Die;
use async_trait::async_trait;
use log::info;
use serde_json::json;
use std::env;
pub struct DigitalOceanProvider {
token: String,
client: reqwest::Client,
}
impl DigitalOceanProvider {
pub fn new() -> Self {
let token = env::var("DIGITALOCEAN_TOKEN").die("Environment variable DIGITALOCEAN_TOKEN is required");
Self {
token,
client: reqwest::Client::new(),
}
}
}
#[async_trait]
impl DnsProvider for DigitalOceanProvider {
async fn create_txt_record(&self, domain: &str, name: &str, value: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
let relative_name = name.strip_suffix(&format!(".{}", domain)).unwrap_or(name);
let url = format!("https://api.digitalocean.com/v2/domains/{}/records", domain);
let res = self
.client
.post(&url)
.bearer_auth(&self.token)
.json(&json!({
"type": "TXT",
"name": relative_name,
"data": value,
"ttl": 30
}))
.send()
.await?;
if !res.status().is_success() {
let err_text = res.text().await?;
return Err(format!("DigitalOcean API error: {}", err_text).into());
}
let json: serde_json::Value = res.json().await?;
let record_id = json["domain_record"]["id"].as_i64().ok_or("Missing record ID in DigitalOcean response")?.to_string();
info!("Created TXT record name: {}, id: {}", name, record_id);
lookup_wait(name, Some("1.1.1.1"), 60, value).await;
Ok(record_id)
}
async fn delete_txt_record(&self, record_id: &str, domain: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let url = format!("https://api.digitalocean.com/v2/domains/{}/records/{}", domain, record_id);
let res = self.client.delete(&url).bearer_auth(&self.token).send().await?;
if !res.status().is_success() {
let err_text = res.text().await?;
return Err(format!("Failed to delete DigitalOcean record: {}", err_text).into());
}
Ok(())
}
}
inventory::submit! {
DnsBackendPlugin {
name: "digitalocean",
factory: || Box::new(DigitalOceanProvider::new()),
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod cloudflare;
pub mod digitalocean;
pub mod route53;
+130
View File
@@ -0,0 +1,130 @@
// ------------------------ //
// This file is vibecoded !
// ------------------------ //
use crate::tls::acme::lookup::lookup_wait;
use crate::tls::acme::types::{DnsBackendPlugin, DnsProvider};
use crate::utils::tools::Die;
use aws_sdk_route53::Client;
use aws_sdk_route53::types::{Change, ChangeAction, ChangeBatch, ResourceRecord, ResourceRecordSet, RrType};
use log::info;
use tokio::runtime::Handle;
pub struct Route53Provider {
pub client: Client,
pub hosted_zone_id: String,
}
impl Route53Provider {
pub fn new() -> Self {
let hosted_zone_id = std::env::var("AWS_HOSTED_ZONE_ID").die("Environment variable AWS_HOSTED_ZONE_ID is required");
let config = if let Ok(handle) = Handle::try_current() {
// If we're inside a tokio runtime, we can block on it directly
handle.block_on(aws_config::load_from_env())
} else {
// If we're completely outside tokio, spin up a temporary current-thread runtime
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.die("Failed to create current-thread runtime")
.block_on(aws_config::load_from_env())
};
let client = Client::new(&config);
Self { client, hosted_zone_id }
}
pub async fn get_authoritative_ns_ip(&self) -> Option<String> {
// Query Route53 for the zone's details (includes assigned Name Servers)
let response = self.client.get_hosted_zone().id(&self.hosted_zone_id).send().await.ok()?;
// Pick the first name server assigned to this zone (e.g., "ns-1289.awsdns-29.org")
let ns_domain = response.delegation_set()?.name_servers().first()?;
// Resolve that nameserver's domain name to an IP address
let ips = tokio::net::lookup_host(format!("{}:53", ns_domain)).await.ok()?;
let ip = ips.into_iter().next()?.ip().to_string();
info!("Targeting Route53 authoritative NS {} ({}) for polling", ns_domain, ip);
Some(ip)
}
}
#[async_trait::async_trait]
impl DnsProvider for Route53Provider {
async fn create_txt_record(&self, _domain: &str, name: &str, value: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
// AWS Route53 requires TXT strings to be wrapped in double quotes
let formatted_value = format!("\"{}\"", value);
let change = Change::builder()
.action(ChangeAction::Upsert)
.resource_record_set(
ResourceRecordSet::builder()
.name(name)
.r#type(RrType::Txt)
.ttl(60)
.resource_records(ResourceRecord::builder().value(formatted_value).build()?)
.build()?,
)
.build()?;
let change_batch = ChangeBatch::builder().changes(change).build()?;
let response = self
.client
.change_resource_record_sets()
.hosted_zone_id(&self.hosted_zone_id)
.change_batch(change_batch)
.send()
.await?;
let record_id = response.change_info().ok_or("Missing ChangeInfo in Route53 response")?.id().to_string();
info!("Created Route53 TXT record name: {}, change_id: {}", name, record_id);
let target_dns_ip = self.get_authoritative_ns_ip().await;
lookup_wait(name, target_dns_ip.as_deref(), 60, value).await;
Ok(record_id)
}
async fn delete_txt_record(&self, _record_id: &str, record_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
// Since Route53 requires record value context to delete, we look up or pass existing TXT record values.
// For ACME cleanup, issuing a DELETE with wildcards/upsert strategy or direct deletion:
let list_res = self
.client
.list_resource_record_sets()
.hosted_zone_id(&self.hosted_zone_id)
.start_record_name(record_name)
.start_record_type(RrType::Txt)
.max_items(1)
.send()
.await?;
if let Some(record_set) = list_res.resource_record_sets().first() {
if record_set.name().trim_end_matches('.') == record_name.trim_end_matches('.') {
let change = Change::builder().action(ChangeAction::Delete).resource_record_set(record_set.clone()).build()?;
let change_batch = ChangeBatch::builder().changes(change).build()?;
self.client
.change_resource_record_sets()
.hosted_zone_id(&self.hosted_zone_id)
.change_batch(change_batch)
.send()
.await?;
info!("Deleted Route53 TXT record name: {}", record_name);
return Ok(());
}
}
info!("Route53 TXT record name: {} already deleted or not found", record_name);
Ok(())
}
}
inventory::submit! {
DnsBackendPlugin {
name: "route53",
factory: || Box::new(Route53Provider::new()),
}
}
+50
View File
@@ -0,0 +1,50 @@
use crate::core::webserver;
use crate::tls::acme::{account, order};
use crate::utils::lazylock::CHALLENGES;
use axum::body::Body;
use axum::extract::State;
use axum::http::{Response, StatusCode};
use axum::response::IntoResponse;
#[allow(clippy::needless_return)]
pub async fn acme_create(State(state): State<webserver::AppState>) -> impl IntoResponse {
match account::load_or_create(state.cert_creds.as_str()).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
}
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create account {}\n", e)).into_response();
}
};
}
#[allow(clippy::needless_return)]
pub async fn acme_order(State(state): State<webserver::AppState>, axum::extract::Path(domain): axum::extract::Path<String>) -> impl IntoResponse {
let domain_clean = domain.trim_matches('/');
match order::order(domain_clean, state.cert_creds.as_str(), state.certs_dir, state.acme_dns_provider).await {
Ok(txt) => {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(txt))
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
}
Err(e) => {
return (StatusCode::INTERNAL_SERVER_ERROR, format!("Internal Server Error: {}\n", e)).into_response();
}
};
}
pub async fn http01_challenge(axum::extract::Path(token): axum::extract::Path<String>) -> impl IntoResponse {
if let Ok(challenges) = CHALLENGES.read() {
if let Some(key_authorization) = challenges.get(&token) {
return Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "text/plain")
.body(Body::from(key_authorization.clone()))
.unwrap_or_else(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error\n").into_response());
}
}
(StatusCode::NOT_FOUND, "Not found\n").into_response()
}
+46
View File
@@ -0,0 +1,46 @@
use crate::utils::tools::Die;
use hickory_resolver::TokioResolver;
use hickory_resolver::config::{NameServerConfig, ResolverConfig, ResolverOpts};
use hickory_resolver::net::runtime::TokioRuntimeProvider;
use hickory_resolver::proto::rr::RData;
use log::info;
use std::net::IpAddr;
use std::str::FromStr;
use std::time::Duration;
pub async fn lookup_wait(record_name: &str, dns_server_ip: Option<&str>, max_wait_secs: u64, expected_value: &str) -> bool {
let target_ip = dns_server_ip.and_then(|ip| IpAddr::from_str(ip).ok()).unwrap_or_else(|| IpAddr::from([1, 1, 1, 1]));
let name_server = NameServerConfig::udp(target_ip);
let config = ResolverConfig::from_name_servers(vec![name_server]);
let mut opts = ResolverOpts::default();
opts.cache_size = 0;
let resolver = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
.with_options(opts)
.build()
.die("failed to build DNS resolver");
let start = std::time::Instant::now();
let poll_interval = Duration::from_secs(3);
while start.elapsed().as_secs() < max_wait_secs {
if let Ok(txt_lookup) = resolver.txt_lookup(record_name).await {
for record in txt_lookup.answers() {
if let RData::TXT(txt) = &record.data {
for data in &txt.txt_data {
if let Ok(txt_str) = std::str::from_utf8(data) {
if txt_str == expected_value {
tokio::time::sleep(Duration::from_secs(5)).await;
info!("DNS record propagated successfully for {} : {}", record_name, txt_str);
return true;
}
}
}
}
}
} else {
info!("Waiting for TXT record {}", record_name);
}
tokio::time::sleep(poll_interval).await;
}
false
}
+9
View File
@@ -0,0 +1,9 @@
pub mod account;
pub mod dns;
pub mod http01;
pub mod lookup;
pub mod order;
pub mod types;
pub use account::*;
pub use order::*;
+120
View File
@@ -0,0 +1,120 @@
use crate::tls::acme::account::get_account;
use crate::tls::acme::types::get_provider;
use crate::utils::lazylock::CHALLENGES;
use crate::utils::lazylock::DOMAINS;
use crate::utils::tools::Die;
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use instant_acme::{ChallengeType, Identifier, NewOrder, RetryPolicy};
use log::{error, info};
use pingora::prelude::sleep;
use rcgen::{CertificateParams, DistinguishedName, KeyPair};
use sha2::{Digest, Sha256};
use std::fs;
use std::time::Duration;
use x509_parser::prelude::*;
pub async fn refresh_order(certs_dir: String, autoconf_dir: String, provider: Option<String>) {
let credsfile = autoconf_dir + "/acme_credentials.json";
loop {
for item in DOMAINS.iter() {
let _what = order(item.key(), credsfile.as_str(), certs_dir.clone(), provider.clone()).await;
}
sleep(Duration::from_secs(12 * 3600)).await;
}
}
pub async fn order(domain: &str, credsfile: &str, certs_dir: String, provider: Option<String>) -> Result<String, Box<dyn std::error::Error>> {
let crt = format!("{}/{}.crt", certs_dir, domain);
let key = format!("{}/{}.key", certs_dir, domain);
if DOMAINS.get(domain).is_none() {
DOMAINS.insert(domain.to_string(), true);
let newlist: Vec<String> = DOMAINS.iter().map(|item| item.key().to_string()).collect();
if let Ok(json_content) = serde_json::to_string_pretty(&newlist) {
let autocfg_file = credsfile.replace("/acme_credentials.json", "/domains.json");
if let Err(err) = fs::write(&autocfg_file, json_content) {
error!("Error Updating domains for certificates: {} : {}", domain, err);
return Err(Box::from(err));
}
}
}
if let Ok(expiry) = cert_expiry(crt.as_str()) {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)?.as_secs();
if expiry > now + 30 * 24 * 3600 {
return Ok("Fresh certificate exists. Not renewing ! \n".to_string());
}
};
let account = get_account(credsfile).await?;
let mut order = account.new_order(&NewOrder::new(&[Identifier::Dns(domain.to_string())])).await?;
let mut authorizations = order.authorizations();
while let Some(auth) = authorizations.next().await {
let mut auth = auth?;
let domain_name = auth.identifier().to_string();
let (challenge_type, is_dns) = match &provider {
Some(_) => (ChallengeType::Dns01, true),
None => (ChallengeType::Http01, false),
};
let mut challenge_handle = auth.challenge(challenge_type).ok_or("Requested challenge type not offered by ACME server")?;
let key_auth = challenge_handle.key_authorization();
let key_auth_str = key_auth.as_str().to_string();
if is_dns {
let prov = provider.as_ref().die("Failed to get DNS provider");
let dns_provider = get_provider(prov.clone());
let dns_value = calculate_dns_value(&key_auth_str);
let clean_domain = domain_name.trim_start_matches("*.").to_string();
let record_name = format!("_acme-challenge.{}", clean_domain);
match dns_provider.create_txt_record(&clean_domain, &record_name, &dns_value).await {
Ok(record_id) => {
let ready_res = challenge_handle.set_ready().await;
let _ = dns_provider.delete_txt_record(&record_id, &record_name).await;
ready_res?;
}
Err(e) => {
eprintln!("Failed to create DNS record: {}", e);
return Err(e);
}
}
} else {
let token = key_auth_str.split('.').next().ok_or("Invalid key authorization")?.to_string();
CHALLENGES.write().die("Failed to write challenge").insert(token, key_auth_str);
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
challenge_handle.set_ready().await?;
}
}
let status = order.poll_ready(&RetryPolicy::default()).await?;
info!("ACME poll_ready status: {:?}", status);
let mut params = CertificateParams::new(vec![domain.to_owned()])?;
params.distinguished_name = DistinguishedName::new();
let private_key = KeyPair::generate()?;
let signing_request = params.serialize_request(&private_key)?;
let csr_der = signing_request.der();
order.finalize_csr(csr_der).await?;
let cert_chain_pem = order.poll_certificate(&RetryPolicy::default()).await?;
CHALLENGES.write().die("Challenges lock poisoned").clear();
let private_key_pem = private_key.serialize_pem();
fs::write(crt, cert_chain_pem)?;
fs::write(key, private_key_pem)?;
Ok("Certificate is successfully generated \n".to_string())
}
fn cert_expiry(path: &str) -> Result<u64, Box<dyn std::error::Error>> {
let pem = fs::read(path)?;
let (_, pem) = parse_x509_pem(&pem)?;
let (_, cert) = parse_x509_certificate(&pem.contents)?;
let expiry = cert.validity().not_after.timestamp() as u64;
Ok(expiry)
}
fn calculate_dns_value(key_auth: &str) -> String {
let hash = Sha256::digest(key_auth.as_bytes());
URL_SAFE_NO_PAD.encode(hash)
}
+32
View File
@@ -0,0 +1,32 @@
use async_trait::async_trait;
use std::collections::HashMap;
use std::sync::LazyLock;
pub struct DnsBackendPlugin {
pub name: &'static str,
pub factory: fn() -> Box<dyn DnsProvider>,
}
inventory::collect!(DnsBackendPlugin);
static FACTORIES: LazyLock<HashMap<&'static str, fn() -> Box<dyn DnsProvider>>> = LazyLock::new(|| {
let mut map = HashMap::new();
for plugin in inventory::iter::<DnsBackendPlugin> {
map.insert(plugin.name, plugin.factory);
}
map
});
#[async_trait]
pub trait DnsProvider: Send + Sync {
async fn create_txt_record(&self, domain: &str, name: &str, value: &str) -> Result<String, Box<dyn std::error::Error + Send + Sync>>;
async fn delete_txt_record(&self, record_id: &str, record_name: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>>;
}
pub fn get_provider(provider: String) -> Box<dyn DnsProvider> {
FACTORIES
.get(provider.as_str())
.map(|factory| factory())
.unwrap_or_else(|| panic!("Unknown DNS provider '{}'. Available providers: {:?}", provider, FACTORIES.keys().collect::<Vec<_>>()))
}
+75
View File
@@ -0,0 +1,75 @@
use log::{info, warn};
use pingora::tls::ssl::{select_next_proto, AlpnError, SslRef, SslVersion};
use pingora_core::listeners::tls::TlsSettings;
#[derive(Debug)]
pub struct CipherSuite {
pub high: &'static str,
pub medium: &'static str,
pub legacy: &'static str,
}
const CIPHERS: CipherSuite = CipherSuite {
high: "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305",
medium: "ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:AES128-GCM-SHA256",
legacy: "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH",
};
#[derive(Debug)]
pub enum TlsGrade {
High,
Medium,
Legacy,
}
impl TlsGrade {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"high" => Some(TlsGrade::High),
"medium" => Some(TlsGrade::Medium),
"unsafe" => Some(TlsGrade::Legacy),
_ => None,
}
}
}
pub fn prefer_h2<'a>(_ssl: &mut SslRef, alpn_in: &'a [u8]) -> Result<&'a [u8], AlpnError> {
match select_next_proto("\x02h2\x08http/1.1".as_bytes(), alpn_in) {
Some(p) => Ok(p),
_ => Err(AlpnError::NOACK),
}
}
pub fn set_tsl_grade(tls_settings: &mut TlsSettings, grade: &str) {
let config_grade = TlsGrade::parse(grade);
match config_grade {
Some(TlsGrade::High) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1_2));
// let _ = tls_settings.set_max_proto_version(Some(SslVersion::TLS1_3));
let _ = tls_settings.set_cipher_list(CIPHERS.high);
// let _ = tls_settings.set_ciphersuites(CIPHERS.high);
let _ = tls_settings.set_cipher_list(CIPHERS.high);
info!("TLS grade: {:?}, => High", tls_settings.options());
}
Some(TlsGrade::Medium) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
info!("TLS grade: {:?}, => Medium", tls_settings.options());
}
Some(TlsGrade::Legacy) => {
let _ = tls_settings.set_min_proto_version(Some(SslVersion::SSL3));
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
// let _ = tls_settings.set_ciphersuites(CIPHERS.legacy);
let _ = tls_settings.set_cipher_list(CIPHERS.legacy);
warn!("TLS grade: {:?}, => UNSAFE", tls_settings.options());
}
None => {
// Defaults to Medium
let _ = tls_settings.set_min_proto_version(Some(SslVersion::TLS1));
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
// let _ = tls_settings.set_ciphersuites(CIPHERS.medium);
let _ = tls_settings.set_cipher_list(CIPHERS.medium);
warn!("TLS grade is not detected defaulting top Medium");
}
}
}
+187
View File
@@ -0,0 +1,187 @@
use crate::tls::grades;
use crate::utils::tools::Die;
use dashmap::DashMap;
use log::error;
use pingora::tls::ssl::{NameType, SniError, SslAlert, SslContext, SslFiletype, SslMethod, SslRef};
use rustls_pemfile::{Item, read_one};
use serde::Deserialize;
use std::collections::HashSet;
use std::fs::File;
use std::io::BufReader;
use x509_parser::extensions::GeneralName;
use x509_parser::nom::Err as NomErr;
use x509_parser::prelude::*;
#[derive(Clone, Deserialize, Debug)]
pub struct CertificateConfig {
pub cert_path: String,
pub key_path: String,
}
#[derive(Debug)]
pub struct CertificateInfo {
pub common_names: Vec<String>,
pub alt_names: Vec<String>,
pub ssl_context: SslContext,
#[allow(dead_code)]
pub cert_path: String, // Only used for logging
#[allow(dead_code)]
pub key_path: String, // Only used for logging
}
#[derive(Debug)]
pub struct Certificates {
configs: Vec<CertificateInfo>,
name_map: DashMap<String, SslContext>,
pub default_cert_path: String,
pub default_key_path: String,
}
impl Certificates {
pub fn new(configs: &Vec<CertificateConfig>, _grade: &str) -> Option<Self> {
let default_cert = configs.first().die("At least one TLS certificate required");
let mut cert_infos = Vec::new();
let name_map: DashMap<String, SslContext> = DashMap::new();
for config in configs {
let cert_info = load_cert_info(&config.cert_path, &config.key_path, _grade);
match cert_info {
Some(cert) => {
for name in &cert.common_names {
name_map.insert(name.clone(), cert.ssl_context.clone());
}
for name in &cert.alt_names {
name_map.insert(name.clone(), cert.ssl_context.clone());
}
cert_infos.push(cert)
}
None => {
error!("Unable to load certificate info | public: {}, private: {}", config.cert_path, config.key_path);
return None;
}
}
}
Some(Self {
name_map,
configs: cert_infos,
default_cert_path: default_cert.cert_path.clone(),
default_key_path: default_cert.key_path.clone(),
})
}
fn find_ssl_context(&self, server_name: &str) -> Option<SslContext> {
if let Some(ctx) = self.name_map.get(server_name) {
return Some(ctx.clone());
}
for config in &self.configs {
for name in &config.common_names {
if name.starts_with("*.") && server_name.ends_with(&name[1..]) {
return Some(config.ssl_context.clone());
}
}
for name in &config.alt_names {
if name.starts_with("*.") && server_name.ends_with(&name[1..]) {
return Some(config.ssl_context.clone());
}
}
}
None
}
pub fn server_name_callback(&self, ssl_ref: &mut SslRef, ssl_alert: &mut SslAlert) -> Result<(), SniError> {
let server_name = ssl_ref.servername(NameType::HOST_NAME);
log::debug!("TLS connect: server_name = {:?}, ssl_ref = {:?}, ssl_alert = {:?}", server_name, ssl_ref, ssl_alert);
// let start_time = Instant::now();
if let Some(name) = server_name {
match self.find_ssl_context(name) {
Some(ctx) => {
ssl_ref.set_ssl_context(&ctx).map_err(|_| SniError::ALERT_FATAL)?;
}
None => {
log::debug!("No matching server name found");
}
}
}
// println!("Context ==> {:?} <==", start_time.elapsed());
Ok(())
}
}
pub fn load_cert_info(cert_path: &str, key_path: &str, _grade: &str) -> Option<CertificateInfo> {
let mut common_names = HashSet::new();
let mut alt_names = HashSet::new();
let file = File::open(cert_path);
match file {
Err(e) => {
log::error!("Failed to open certificate file: {:?}", e);
return None;
}
Ok(file) => {
let mut reader = BufReader::new(file);
match read_one(&mut reader) {
Err(e) => {
log::error!("Failed to decode PEM from certificate file: {:?}", e);
return None;
}
Ok(leaf) => match leaf {
Some(Item::X509Certificate(cert)) => match X509Certificate::from_der(&cert) {
Err(NomErr::Error(e)) | Err(NomErr::Failure(e)) => {
log::error!("Failed to parse certificate: {:?}", e);
return None;
}
Err(_) => {
log::error!("Unknown error while parsing certificate");
return None;
}
Ok((_, x509)) => {
let subject = x509.subject();
for attr in subject.iter_common_name() {
if let Ok(cn) = attr.as_str() {
common_names.insert(cn.to_string());
}
}
if let Ok(Some(san)) = x509.subject_alternative_name() {
for name in san.value.general_names.iter() {
if let GeneralName::DNSName(dns) = name {
let dns_string = dns.to_string();
if !common_names.contains(&dns_string) {
alt_names.insert(dns_string);
}
}
}
}
}
},
_ => {
log::error!("Failed to read certificate");
return None;
}
},
}
}
}
if let Ok(ssl_context) = create_ssl_context(cert_path, key_path) {
Some(CertificateInfo {
cert_path: cert_path.to_string(),
key_path: key_path.to_string(),
common_names: common_names.into_iter().collect(),
alt_names: alt_names.into_iter().collect(),
ssl_context,
})
} else {
log::error!("Failed to create SSL context from cert paths");
None
}
}
fn create_ssl_context(cert_path: &str, key_path: &str) -> Result<SslContext, Box<dyn std::error::Error>> {
let mut ctx = SslContext::builder(SslMethod::tls())?;
ctx.set_certificate_chain_file(cert_path)?;
ctx.set_private_key_file(key_path, SslFiletype::PEM)?;
ctx.set_alpn_select_callback(grades::prefer_h2);
let built = ctx.build();
Ok(built)
}
+4
View File
@@ -0,0 +1,4 @@
pub mod acme;
pub mod grades;
pub mod load;
pub use load::*;
-9
View File
@@ -1,9 +0,0 @@
pub mod auth;
pub mod consul;
pub mod discovery;
mod filewatch;
pub mod healthcheck;
pub mod jwt;
pub mod parceyaml;
pub mod structs;
pub mod tools;
-104
View File
@@ -1,104 +0,0 @@
use crate::utils::jwt::check_jwt;
use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use pingora_proxy::Session;
use std::collections::HashMap;
use urlencoding::decode;
trait AuthValidator {
fn validate(&self, session: &Session) -> bool;
}
struct BasicAuth<'a>(&'a str);
struct ApiKeyAuth<'a>(&'a str);
struct JwtAuth<'a>(&'a str);
impl AuthValidator for BasicAuth<'_> {
fn validate(&self, session: &Session) -> bool {
if let Some(header) = session.get_header("authorization") {
if let Some((_, val)) = header.to_str().ok().unwrap().split_once(' ') {
let decoded = STANDARD.decode(val).ok().unwrap();
let decoded_str = String::from_utf8(decoded).ok().unwrap();
return decoded_str == self.0;
}
}
false
}
}
impl AuthValidator for ApiKeyAuth<'_> {
fn validate(&self, session: &Session) -> bool {
if let Some(header) = session.get_header("x-api-key") {
return header.to_str().ok().unwrap() == self.0;
}
false
}
}
impl AuthValidator for JwtAuth<'_> {
fn validate(&self, session: &Session) -> bool {
let jwtsecret = self.0;
if let Some(tok) = get_query_param(session, "gazantoken") {
return check_jwt(tok.as_str(), jwtsecret);
}
// if let Some(header) = session.get_header("authorization") {
// let h = header.to_str().ok().unwrap().split(" ").collect::<Vec<_>>();
// match h.len() {
// n => {
// return check_jwt(h[n - 1], jwtsecret);
// }
// }
// }
if let Some(auth_header) = session.get_header("authorization") {
if let Ok(header_str) = auth_header.to_str() {
if let Some((scheme, token)) = header_str.split_once(' ') {
if scheme.eq_ignore_ascii_case("bearer") {
return check_jwt(token, jwtsecret);
}
}
}
}
false
}
}
fn validate(auth: &dyn AuthValidator, session: &Session) -> bool {
auth.validate(session)
}
pub fn authenticate(c: &[String], session: &Session) -> bool {
match c[0].as_str() {
"basic" => {
let auth = BasicAuth(c[1].as_str().into());
validate(&auth, session)
}
"apikey" => {
let auth = ApiKeyAuth(c[1].as_str().into());
validate(&auth, session)
}
"jwt" => {
let auth = JwtAuth(c[1].as_str().into());
validate(&auth, session)
}
_ => {
println!("Unsupported authentication mechanism : {}", c[0]);
false
}
}
}
pub fn get_query_param(session: &Session, key: &str) -> Option<String> {
let query = session.req_header().uri.query()?;
let params: HashMap<_, _> = query
.split('&')
.filter_map(|pair| {
let mut parts = pair.splitn(2, '=');
let k = parts.next()?;
let v = parts.next().unwrap_or(""); // Some params might have no value
Some((k, v))
})
.collect();
params.get(key).map(|v| decode(v).ok()).flatten().map(|s| s.to_string())
}
-141
View File
@@ -1,141 +0,0 @@
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::{Configuration, ServiceMapping, UpstreamsDashMap};
use crate::utils::tools::{clone_dashmap_into, compare_dashmaps};
use dashmap::DashMap;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use log::{info, warn};
use pingora::prelude::sleep;
use rand::Rng;
use reqwest::header::{HeaderMap, HeaderValue};
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::atomic::AtomicUsize;
use std::time::Duration;
#[derive(Debug, Deserialize)]
struct Service {
#[serde(rename = "ServiceTaggedAddresses")]
tagged_addresses: HashMap<String, TaggedAddress>,
}
#[derive(Debug, Deserialize)]
struct TaggedAddress {
#[serde(rename = "Address")]
address: String,
#[serde(rename = "Port")]
port: u16,
}
pub async fn start(fp: String, mut toreturn: Sender<Configuration>) {
let config = load_configuration(fp.as_str(), "filepath");
let headers = DashMap::new();
match config {
Some(config) => {
if config.typecfg.to_string() != "consul" {
info!("Not running Consul discovery, requested type is: {}", config.typecfg);
return;
}
info!("Consul Discovery is enabled : {}", config.typecfg);
let consul = config.consul.clone();
let prev_upstreams = UpstreamsDashMap::new();
match consul {
Some(consul) => {
let servers = consul.servers.unwrap();
info!("Consul Servers => {:?}", servers);
let end = servers.len();
loop {
let num = rand::rng().random_range(1..end);
headers.clear();
for (k, v) in config.headers.clone() {
headers.insert(k.to_string(), v);
}
let consul_data = servers.get(num).unwrap().to_string();
let upstreams = consul_request(consul_data, consul.services.clone(), consul.token.clone());
match upstreams.await {
Some(upstreams) => {
if !compare_dashmaps(&upstreams, &prev_upstreams) {
let mut tosend: Configuration = Configuration {
upstreams: Default::default(),
headers: Default::default(),
consul: None,
typecfg: "".to_string(),
extraparams: config.extraparams.clone(),
};
clone_dashmap_into(&upstreams, &prev_upstreams);
clone_dashmap_into(&upstreams, &tosend.upstreams);
tosend.headers = headers.clone();
tosend.extraparams.authentication = config.extraparams.authentication.clone();
tosend.typecfg = config.typecfg.clone();
tosend.consul = config.consul.clone();
toreturn.send(tosend).await.unwrap();
}
}
None => {}
}
sleep(Duration::from_secs(5)).await;
}
}
None => {}
}
}
None => {}
}
}
async fn consul_request(url: String, whitelist: Option<Vec<ServiceMapping>>, token: Option<String>) -> Option<UpstreamsDashMap> {
let upstreams = UpstreamsDashMap::new();
let ss = url.clone() + "/v1/catalog/service/";
match whitelist {
Some(whitelist) => {
for k in whitelist.iter() {
let pref: String = ss.clone() + &k.real;
let list = get_by_http(pref.clone(), token.clone()).await;
match list {
Some(list) => {
upstreams.insert(k.proxy.clone(), list);
}
None => {
warn!("Whitelist not found for {}", k.proxy);
}
}
}
}
None => {}
}
Some(upstreams)
}
async fn get_by_http(url: String, token: Option<String>) -> Option<DashMap<String, (Vec<(String, u16, bool, bool)>, AtomicUsize)>> {
let client = reqwest::Client::new();
let mut headers = HeaderMap::new();
if let Some(token) = token {
headers.insert("X-Consul-Token", HeaderValue::from_str(&token).unwrap());
}
let to = Duration::from_secs(1);
let u = client.get(url).timeout(to).send();
let mut values = Vec::new();
let upstreams: DashMap<String, (Vec<(String, u16, bool, bool)>, AtomicUsize)> = DashMap::new();
match u.await {
Ok(r) => {
let jason = r.json::<Vec<Service>>().await;
match jason {
Ok(whitelist) => {
for service in whitelist {
let addr = service.tagged_addresses.get("lan_ipv4").unwrap().address.clone();
let prt = service.tagged_addresses.get("lan_ipv4").unwrap().port.clone();
let to_add = (addr, prt, false, false);
values.push(to_add);
}
}
Err(_) => return None,
}
}
Err(_) => return None,
}
upstreams.insert("/".to_string(), (values, AtomicUsize::new(0)));
Some(upstreams)
}
+298
View File
@@ -0,0 +1,298 @@
use crate::utils::hcclient::{HttpClientRequest, httpclient};
use crate::utils::lazylock::{GLOBAL_PARAMS, MASTER_KEY, WORKERS};
use crate::utils::tools::Die;
use crate::utils::types::{Consul, Extraparams, Headers, Kubernetes, UpstreamsDashMap};
use bytes::Bytes;
use log::{error, info, warn};
use noyalib::to_string;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::ops::Deref;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UpstreamConfigDto {
pub paths: HashMap<String, PathConfigDto>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PathConfigDto {
pub is_http2: bool,
pub healthcheck: bool,
pub is_ssl: bool,
pub to_https: bool,
pub rate_limit: isize,
pub x4xx_limit: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization: Option<HashMap<String, String>>,
pub servers: Vec<String>,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub client_headers: Option<Vec<String>>,
// #[serde(skip_serializing_if = "Option::is_none")]
// pub server_headers: Option<Vec<String>>,
}
#[derive(Clone, Debug)]
pub struct SendConfigDto {
pub upstreams: HashMap<String, UpstreamConfigDto>,
pub client_headers: Headers,
pub server_headers: Headers,
pub consul: Option<Consul>,
pub kubernetes: Option<Kubernetes>,
pub typecfg: String,
pub extraparams: Extraparams,
}
pub fn serialize_config(data: &UpstreamsDashMap) -> String {
let config = GLOBAL_PARAMS.read().die("Failed to read config");
let mut snapshot: HashMap<String, UpstreamConfigDto> = HashMap::new();
for outer_entry in data.iter() {
let domain = outer_entry.key().to_string();
let mut paths: HashMap<String, PathConfigDto> = HashMap::new();
for inner_entry in outer_entry.value().iter() {
let path = inner_entry.key().to_string();
let (vec_inner, _atomic_val) = inner_entry.value();
if let Some(first) = vec_inner.first() {
let servers = vec_inner.iter().map(|item| format!("{}:{}", item.address, item.port)).collect();
let mut auth = None;
if let Some(ath) = first.authorization.clone() {
let mut a = HashMap::new();
a.insert("type".to_string(), ath.auth_type.clone().to_string());
a.insert("data".to_string(), ath.auth_cred.clone().to_string());
auth = Some(a);
}
paths.insert(
path,
PathConfigDto {
is_http2: first.is_http2,
healthcheck: first.healthcheck,
is_ssl: first.is_ssl,
to_https: first.to_https,
rate_limit: first.rate_limit,
x4xx_limit: first.x4xx_limit,
redirect_to: first.redirect_to.clone().as_ref().map(|s| s.to_string()),
authorization: auth,
servers,
},
);
}
}
snapshot.insert(domain, UpstreamConfigDto { paths });
}
let payload = SendConfigDto {
upstreams: snapshot,
client_headers: config.client_headers.clone(),
server_headers: config.server_headers.clone(),
consul: config.consul.clone(),
kubernetes: config.kubernetes.clone(),
typecfg: config.typecfg.clone(),
extraparams: config.extraparams.clone(),
};
to_yaml(&payload)
}
#[derive(Serialize, Debug)]
struct YamlAuth {
#[serde(rename = "type")]
auth_type: String,
data: String,
}
#[derive(Serialize)]
struct YamlPath {
servers: Vec<String>,
#[serde(skip_serializing_if = "is_false")]
is_http2: bool,
#[serde(skip_serializing_if = "is_false")]
to_https: bool,
#[serde(skip_serializing_if = "is_false")]
is_ssl: bool,
#[serde(skip_serializing_if = "is_true")] // healthcheck true is default
healthcheck: bool,
#[serde(skip_serializing_if = "is_zero_isize")]
rate_limit: isize,
#[serde(skip_serializing_if = "is_zero_u32")]
x4xx_limit: u32,
#[serde(skip_serializing_if = "Option::is_none")]
redirect_to: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
authorization: Option<YamlAuth>,
#[serde(skip_serializing_if = "Option::is_none")]
client_headers: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
server_headers: Option<Vec<String>>,
}
#[derive(Serialize)]
struct YamlUpstream {
paths: HashMap<String, YamlPath>,
}
#[derive(Serialize)]
struct YamlConfig {
provider: String,
sticky_sessions: u64,
to_https: bool,
rate_limit: isize,
x4xx_limit: u32,
#[serde(skip_serializing_if = "Vec::is_empty")]
server_headers: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
client_headers: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
authorization: Option<YamlAuth>,
upstreams: HashMap<String, YamlUpstream>,
}
fn is_false(v: &bool) -> bool {
!v
}
fn is_true(v: &bool) -> bool {
*v
}
fn is_zero_isize(v: &isize) -> bool {
*v == 0
}
fn is_zero_u32(v: &u32) -> bool {
*v == 0
}
pub fn to_yaml(config: &SendConfigDto) -> String {
let server_headers: Vec<String> = config
.server_headers
.get("GLOBAL_SERVER_HEADERS")
.map(|paths| {
paths
.value()
.iter()
.flat_map(|inner| inner.value().iter().map(|(k, v)| format!("{}:{}", k, v)).collect::<Vec<_>>())
.collect()
})
.unwrap_or_default();
let client_headers: Vec<String> = config
.client_headers
.get("GLOBAL_CLIENT_HEADERS")
.map(|paths| {
paths
.value()
.iter()
.flat_map(|inner| inner.value().iter().map(|(k, v)| format!("{}:{}", k, v)).collect::<Vec<_>>())
.collect()
})
.unwrap_or_default();
let authorization = config.extraparams.authentication.as_ref().map(|auth| YamlAuth {
auth_type: auth.auth_type.to_string(),
data: auth.auth_cred.to_string(),
});
let upstreams: HashMap<String, YamlUpstream> = config
.upstreams
.iter()
.map(|(domain, upstream)| {
let paths = upstream
.paths
.iter()
.map(|(path, pcfg)| {
let ch: Option<Vec<String>> = {
if let Some(domain_ref) = config.client_headers.get(domain.as_str()) {
if let Some(path_ref) = domain_ref.get(path.as_str()) {
let collected: Vec<String> = path_ref.value().iter().map(|(k, v)| format!("{}:{}", k, v)).collect();
if !collected.is_empty() { Some(collected) } else { None }
} else {
None
}
} else {
None
}
};
let sh: Option<Vec<String>> = {
if let Some(domain_ref) = config.server_headers.get(domain.as_str()) {
if let Some(path_ref) = domain_ref.get(path.as_str()) {
let collected: Vec<String> = path_ref.value().iter().map(|(k, v)| format!("{}:{}", k, v)).collect();
if !collected.is_empty() { Some(collected) } else { None }
} else {
None
}
} else {
None
}
};
let auth = pcfg.authorization.as_ref().map(|a| YamlAuth {
auth_type: a.get("type").cloned().unwrap_or_default(),
data: a.get("data").cloned().unwrap_or_default(),
});
(
path.clone(),
YamlPath {
servers: pcfg.servers.clone(),
is_http2: pcfg.is_http2,
is_ssl: pcfg.is_ssl,
healthcheck: pcfg.healthcheck,
rate_limit: pcfg.rate_limit,
x4xx_limit: pcfg.x4xx_limit,
to_https: pcfg.to_https,
redirect_to: pcfg.redirect_to.clone(),
authorization: auth,
client_headers: ch,
server_headers: sh,
},
)
})
.collect();
(domain.clone(), YamlUpstream { paths })
})
.collect();
let yaml_config = YamlConfig {
provider: config.typecfg.clone(),
sticky_sessions: config.extraparams.sticky_sessions,
to_https: config.extraparams.to_https,
rate_limit: config.extraparams.rate_limit,
x4xx_limit: config.extraparams.x4xx_limit,
server_headers,
client_headers,
authorization,
upstreams,
};
to_string(&yaml_config).unwrap_or_default()
}
pub async fn send_to_worker(data: String) {
let masterkey = MASTER_KEY.get().die("Master key not set");
if let Some(workers) = WORKERS.get() {
for worker in workers {
let prefix = if worker.tls { "https://" } else { "http://" };
let worker_url = format!("{}{}:{}", prefix, worker.address, worker.port);
let status = httpclient(HttpClientRequest {
method: "POST",
tls: worker.tls,
host: worker.address.as_ref(),
path: "/workerconf",
address: worker.address.as_ref(),
port: worker.port,
url: worker_url.as_str(),
payload: Bytes::from(data.clone()),
headers: Some(vec![("API-KEY", masterkey.clone().die("Master key must be set in the cluster mode").deref())]),
})
.await;
if (200..500).contains(&status.2) {
info!("Successfully updated Worker {}, status {}", worker_url, status.2);
} else {
error!("Cannot connect to worker node, status {}", status.2);
}
}
} else {
warn!("Workers list has not been initialized yet.");
}
}
-44
View File
@@ -1,44 +0,0 @@
use crate::utils::consul;
use crate::utils::filewatch;
use crate::utils::structs::Configuration;
use crate::web::webserver;
use async_trait::async_trait;
use futures::channel::mpsc::Sender;
pub struct FromFileProvider {
pub path: String,
}
pub struct APIUpstreamProvider {
pub address: String,
pub masterkey: String,
}
pub struct ConsulProvider {
pub path: String,
}
#[async_trait]
pub trait Discovery {
async fn start(&self, tx: Sender<Configuration>);
}
#[async_trait]
impl Discovery for APIUpstreamProvider {
async fn start(&self, toreturn: Sender<Configuration>) {
webserver::run_server(self.address.clone(), self.masterkey.clone(), toreturn).await;
}
}
#[async_trait]
impl Discovery for FromFileProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(filewatch::start(self.path.clone(), tx.clone()));
}
}
#[async_trait]
impl Discovery for ConsulProvider {
async fn start(&self, tx: Sender<Configuration>) {
tokio::spawn(consul::start(self.path.clone(), tx.clone()));
}
}
-71
View File
@@ -1,71 +0,0 @@
use crate::utils::parceyaml::load_configuration;
use crate::utils::structs::Configuration;
use futures::channel::mpsc::Sender;
use futures::SinkExt;
use log::{error, info, warn};
use notify::event::ModifyKind;
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use pingora::prelude::sleep;
use std::path::Path;
use std::time::{Duration, Instant};
use tokio::task;
pub async fn start(fp: String, mut toreturn: Sender<Configuration>) {
sleep(Duration::from_millis(50)).await; // For having nice logs :-)
let file_path = fp.as_str();
let parent_dir = Path::new(file_path).parent().unwrap();
let (local_tx, mut local_rx) = tokio::sync::mpsc::channel::<notify::Result<Event>>(1);
let snd = load_configuration(file_path, "filepath");
match snd {
Some(snd) => {
if snd.typecfg != "file" {
warn!("Disabling file watcher, requested discovery type is: {}", snd.typecfg);
return;
}
info!("Watching for changes in {:?}", parent_dir);
toreturn.send(snd).await.unwrap();
}
None => {}
}
let _watcher_handle = task::spawn_blocking({
let parent_dir = parent_dir.to_path_buf(); // Move directory path into the closure
move || {
let mut watcher = RecommendedWatcher::new(
move |res| {
let _ = local_tx.blocking_send(res);
},
Config::default(),
)
.unwrap();
watcher.watch(&parent_dir, RecursiveMode::Recursive).unwrap();
let (_rtx, mut rrx) = tokio::sync::mpsc::channel::<bool>(1);
let _ = rrx.blocking_recv();
}
});
let mut start = Instant::now();
while let Some(event) = local_rx.recv().await {
match event {
Ok(e) => match e.kind {
EventKind::Modify(ModifyKind::Data(_)) | EventKind::Create(..) | EventKind::Remove(..) => {
if e.paths[0].to_str().unwrap().ends_with("yaml") {
if start.elapsed() > Duration::from_secs(2) {
start = Instant::now();
// info!("Config File changed :=> {:?}", e);
let snd = load_configuration(file_path, "filepath");
match snd {
Some(snd) => {
toreturn.send(snd).await.unwrap();
}
None => {}
}
}
}
}
_ => (),
},
Err(e) => error!("Watch error: {:?}", e),
}
}
}
+32
View File
@@ -0,0 +1,32 @@
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
pub struct CountingAllocator;
pub static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
pub static DEALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
pub static ALLOC_BYTES: AtomicUsize = AtomicUsize::new(0);
#[allow(dead_code)]
unsafe impl GlobalAlloc for CountingAllocator {
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
ALLOC_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
System.alloc(layout)
}
#[allow(unsafe_op_in_unsafe_fn)]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
DEALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
System.dealloc(ptr, layout)
}
}
// Uncomment following lines and comment allocator in main.rs
// #[global_allocator]
// pub static A: CountingAllocator = CountingAllocator;
// #[allow(dead_code)]
// fn for_example() {
// let before = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
// let after = crate::utils::fordebug::ALLOC_COUNT.load(Ordering::Relaxed);
// println!("Allocations : {}", after - before);
// }
+30
View File
@@ -0,0 +1,30 @@
use bytes::Bytes;
use http_body_util::Full;
use hyper::Request;
use hyper_util::client::legacy::Client;
use hyper_util::client::legacy::connect::HttpConnector;
use hyper_util::rt::TokioExecutor;
use log::error;
pub async fn ping_grpc(addr: &str) -> bool {
let connector = HttpConnector::new();
let client = Client::builder(TokioExecutor::new()).http2_only(true).build(connector);
let uri = format!("{}{}", addr, "/aralez.Probe/Ping");
let body = Full::new(Bytes::from(vec![0, 0, 0, 0, 0]));
match Request::post(uri.clone()).header("content-type", "application/grpc").header("te", "trailers").body(body) {
Ok(request) => match client.request(request).await {
Ok(resp) => resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.map(|v| v.starts_with("application/grpc"))
.unwrap_or(false),
Err(_) => false,
},
Err(e) => {
error!("Failed to create request: {}", e);
false
}
}
}
+119
View File
@@ -0,0 +1,119 @@
use crate::utils::grpc::ping_grpc;
use bytes::Bytes;
use pingora_core::connectors::http::Connector;
use pingora_core::listeners::ALPN;
use pingora_core::prelude::HttpPeer;
use pingora_http::RequestHeader;
use std::sync::LazyLock;
use std::time::Duration;
pub static HC_CONNECTOR: LazyLock<Connector> = LazyLock::new(|| Connector::new(None));
pub struct HttpClientRequest<'a> {
pub method: &'a str,
pub tls: bool,
pub host: &'a str,
pub path: &'a str,
pub address: &'a str,
pub port: u16,
pub url: &'a str,
pub payload: Bytes,
pub headers: Option<Vec<(&'a str, &'a str)>>, // ← add this
}
pub async fn httpclient(req: HttpClientRequest<'_>) -> (bool, bool, u16) {
let HttpClientRequest {
method,
tls,
host,
path,
address,
port,
url,
payload,
headers,
} = req;
let method = match method {
"HEAD" => "HEAD",
"GET" => "GET",
"POST" => "POST",
_ => "GET",
};
let mut peer = HttpPeer::new((address, port), tls, host.to_string());
if tls {
peer.options.verify_cert = false;
peer.options.verify_hostname = false;
}
let mut is_h2 = false;
peer.options.total_connection_timeout = Option::from(Duration::from_secs(5));
peer.options.alpn = ALPN::H2H1;
let (mut http_session, _) = match HC_CONNECTOR.get_http_session(&peer).await {
Ok(s) => s,
Err(e) => {
if let Some(msg) = e.context {
log::warn!("{}, type: {}", msg.as_str(), e.etype.as_str());
} else {
log::warn!("Fail to connect to addr: {}, tls {}, host {}", address, tls, host);
}
return (false, false, 500);
}
};
match http_session.as_http2() {
Some(_) => {
peer.options.alpn = ALPN::H2;
is_h2 = true;
}
None => {
if ping_grpc(url).await {
return (true, true, 204);
}
}
}
let mut req = match RequestHeader::build(method, path.as_bytes(), None) {
Ok(r) => r,
Err(e) => {
log::warn!("Failed to build request: {}", e);
return (false, false, 500);
}
};
req.insert_header("Host", host).ok();
let body_bytes = if method == "POST" {
req.insert_header("Content-Type", "application/json").ok();
req.insert_header("Content-Length", payload.len().to_string()).ok();
if let Some(headers) = headers {
for (k, v) in headers {
req.insert_header(k.to_owned(), v).ok();
}
}
payload
} else {
Bytes::new()
};
if let Err(e) = http_session.write_request_header(Box::new(req)).await {
log::warn!("Write header failed: {}", e);
return (false, false, 500);
}
if let Err(e) = http_session.write_request_body(body_bytes, true).await {
log::warn!("Write body failed: {}", e);
return (false, false, 500);
}
let status = match http_session.read_response_header().await {
Ok(_) => http_session.response_header().map(|r| r.status.as_u16()).unwrap_or(500),
Err(e) => {
if !is_h2 && ping_grpc(url).await {
return (true, true, 204);
}
log::warn!("Health Check read failed ({}) : {} - {}", if is_h2 { "H2" } else { "H1" }, host, e);
return (false, false, 500);
}
};
HC_CONNECTOR.release_http_session(http_session, &peer, None).await;
((200..500).contains(&status), is_h2, status)
}
+106 -129
View File
@@ -1,155 +1,132 @@
use crate::utils::structs::{UpstreamsDashMap, UpstreamsIdMap};
use crate::utils::deser::{send_to_worker, serialize_config};
use crate::utils::hcclient::{HttpClientRequest, httpclient};
use crate::utils::lazylock::{IM_A_MASTER, NODE_STATUS, REVERSE_STORE};
use crate::utils::tools::*;
use crate::utils::types::{InnerMap, UpstreamsDashMap, UpstreamsIdMap};
use bytes::Bytes;
use dashmap::DashMap;
use log::{error, info, warn};
use reqwest::{Client, Version};
use std::sync::atomic::AtomicUsize;
use log::info;
use std::sync::Arc;
use std::sync::atomic::AtomicUsize;
use std::time::Duration;
use tokio::time::interval;
use tonic::transport::Endpoint;
use tokio::net::TcpStream;
use tokio::time::{interval, timeout};
use tokio_native_tls::native_tls::TlsConnector;
const EMPTY_PAYLOAD: Bytes = Bytes::from_static(b"");
pub async fn hc2(upslist: Arc<UpstreamsDashMap>, fullist: Arc<UpstreamsDashMap>, idlist: Arc<UpstreamsIdMap>, params: (&str, u64)) {
let mut period = interval(Duration::from_secs(params.1));
let mut first_run = 0;
if let Some(member) = NODE_STATUS.get() {
if let Some(mbr) = member.as_ref() {
match mbr.as_ref() {
"worker" => {
info!("Disabling health-checks. Running in cluster mode as {}", mbr);
return;
}
"master" => {
info!("Disabling client/server routing. Running in cluster mode as {}", mbr);
}
_ => {}
}
}
}
loop {
tokio::select! {
_ = period.tick() => {
let totest : UpstreamsDashMap = DashMap::new();
let fclone : UpstreamsDashMap = clone_dashmap(&fullist);
for val in fclone.iter() {
let host = val.key();
let inner = DashMap::new();
let mut _scheme: (String, u16, bool, bool) = ("".to_string(), 0, false, false);
for path_entry in val.value().iter() {
// let inner = DashMap::new();
let path = path_entry.key();
let mut innervec= Vec::new();
for k in path_entry.value().0 .iter().enumerate() {
let (ip, port, _ssl, _version) = k.1;
let mut _link = String::new();
let tls = detect_tls(ip, port).await;
let mut is_h2 = false;
// if tls.1 == Some(Version::HTTP_11) {
// println!(" V1: ==> {:?}", tls.1)
// }else if tls.1 == Some(Version::HTTP_2) {
// is_h2 = true;
// println!(" V2: ==> {:?}", tls.1)
// }
if tls.1 == Some(Version::HTTP_2) {
is_h2 = true;
// println!(" V2: ==> {} ==> {:?}", tls.0, tls.1)
}
match tls.0 {
true => _link = format!("https://{}:{}{}", ip, port, path),
false => _link = format!("http://{}:{}{}", ip, port, path),
}
// if _pref == "https://" {
// _scheme = (ip.to_string(), *port, true);
// }else {
// _scheme = (ip.to_string(), *port, false);
// }
_scheme = (ip.to_string(), *port, tls.0, is_h2);
// let link = format!("{}{}:{}{}", _pref, ip, port, path);
let resp = http_request(_link.as_str(), params.0, "").await;
match resp.0 {
true => {
if resp.1 {
_scheme = (ip.to_string(), *port, tls.0, true);
}
innervec.push(_scheme.clone());
}
false => {
warn!("Dead Upstream : {}", _link);
}
}
}
inner.insert(path.clone().to_owned(), (innervec, AtomicUsize::new(0)));
}
totest.insert(host.clone(), inner);
}
if first_run == 1 {
info!("Performing initial hatchecks and upstreams ssl detection");
clone_idmap_into(&totest, &idlist);
info!("Gazan is up and ready to serve requests");
}
first_run+=1;
if ! compare_dashmaps(&totest, &upslist){
let totest = initiate_upstreams(&fullist, params.0).await;
if !compare_dashmaps(&totest, &upslist) {
clone_dashmap_into(&totest, &upslist);
clone_idmap_into(&totest, &idlist);
if let Some(true) = IM_A_MASTER.get(){
let binary_output = serialize_config(&totest);
let _ = send_to_worker(binary_output).await;
}
REVERSE_STORE.clear();
}
}
}
}
}
pub(crate) async fn initiate_upstreams(fullist: &UpstreamsDashMap, method: &str) -> UpstreamsDashMap {
let totest: UpstreamsDashMap = DashMap::new();
let fclone = clone_dashmap(fullist);
for val in fclone.iter() {
let host = val.key();
let inner = DashMap::new();
#[allow(dead_code)]
async fn http_request(url: &str, method: &str, payload: &str) -> (bool, bool) {
let client = Client::builder().danger_accept_invalid_certs(true).build().unwrap();
let timeout = Duration::from_secs(1);
if !["POST", "GET", "HEAD"].contains(&method) {
error!("Method {} not supported. Only GET|POST|HEAD are supported ", method);
return (false, false);
}
async fn send_request(client: &Client, method: &str, url: &str, payload: &str, timeout: Duration) -> Option<reqwest::Response> {
match method {
"POST" => client.post(url).body(payload.to_owned()).timeout(timeout).send().await.ok(),
"GET" => client.get(url).timeout(timeout).send().await.ok(),
"HEAD" => client.head(url).timeout(timeout).send().await.ok(),
_ => None,
}
}
for path_entry in val.value().iter() {
let path = path_entry.key();
let mut innervec = Vec::new();
match send_request(&client, method, url, payload, timeout).await {
Some(response) => {
let status = response.status().as_u16();
((99..499).contains(&status), false)
}
None => {
// let fallback_url = url.replace("https", "http");
// ping_grpc(&fallback_url).await
(ping_grpc(&url).await, true)
}
}
}
for upstream in path_entry.value().0.iter() {
let tls = if upstream.healthcheck {
detect_tls(upstream.address.as_ref(), &upstream.port, host).await
} else {
upstream.is_ssl
};
let mut scheme = InnerMap {
address: upstream.address.clone(),
port: upstream.port,
is_ssl: tls,
is_http2: upstream.is_http2,
to_https: upstream.to_https,
rate_limit: upstream.rate_limit,
x4xx_limit: upstream.x4xx_limit,
healthcheck: upstream.healthcheck,
redirect_to: upstream.redirect_to.clone(),
authorization: upstream.authorization.clone(),
};
pub async fn ping_grpc(addr: &str) -> bool {
let endpoint_result = Endpoint::from_shared(addr.to_owned());
if let Ok(endpoint) = endpoint_result {
let endpoint = endpoint.timeout(Duration::from_secs(2));
match tokio::time::timeout(Duration::from_secs(3), endpoint.connect()).await {
Ok(Ok(_channel)) => {
// println!("{:?} ==> {:?} ==> {}", endpoint, _channel, addr);
true
if scheme.healthcheck {
let link = if tls {
format!("https://{}:{}{}", upstream.address, upstream.port, path)
} else {
format!("http://{}:{}{}", upstream.address, upstream.port, path)
};
let resp = httpclient(HttpClientRequest {
method,
tls,
host,
path,
address: upstream.address.as_ref(),
port: upstream.port,
url: link.as_str(),
payload: EMPTY_PAYLOAD,
headers: None,
})
.await;
if resp.0 {
if resp.1 {
scheme.is_http2 = resp.1;
}
innervec.push(Arc::from(scheme));
}
} else {
innervec.push(Arc::from(scheme));
}
}
_ => false,
inner.insert(path.clone(), (innervec, AtomicUsize::new(0)));
}
} else {
false
totest.insert(host.clone(), inner);
}
totest
}
async fn detect_tls(ip: &str, port: &u16) -> (bool, Option<Version>) {
let url = format!("https://{}:{}", ip, port);
// let url = format!("{}:{}", ip, port);
let client = Client::builder().timeout(Duration::from_secs(2)).danger_accept_invalid_certs(true).build().unwrap();
match client.get(&url).send().await {
Ok(response) => (true, Some(response.version())),
Err(e) => {
if e.is_builder() || e.is_connect() || e.to_string().contains("tls") {
(false, None)
} else {
(false, None)
}
}
}
async fn detect_tls(ip: &str, port: &u16, sni: &str) -> bool {
let addr = format!("{ip}:{port}");
let timeout_duration = Duration::from_secs(2); // THINK !!!
let stream = match timeout(timeout_duration, TcpStream::connect(&addr)).await {
Ok(Ok(s)) => s,
Ok(Err(_)) => return false,
Err(_) => return false,
};
let connector = tokio_native_tls::TlsConnector::from(
TlsConnector::builder()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.build()
.die("Failed to create TLS connector"),
);
connector.connect(sni, stream).await.is_ok()
}

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