Compare commits

...
204 Commits
Author SHA1 Message Date
wweir ad338598ae fix: harden dns proxy routing 2026-04-25 04:12:42 +08:00
wweir 09e4fc49fc docs: update client routing notes 2026-04-25 03:21:59 +08:00
wweir 0ef0686530 chore(release): package toml client configs 2026-04-25 03:20:41 +08:00
wweir 9c2aa3af10 chore(deps): remove unused error and hcl packages 2026-04-25 03:17:45 +08:00
wweir 5fb9dfcfee fix(router): skip empty country mmdb 2026-04-25 03:17:45 +08:00
wweir f72d88b204 fix(sower): relay transparent tls client hello 2026-04-25 03:17:35 +08:00
wweir 05f0728aa6 feat(sower): harden client rule loading 2026-04-25 03:09:45 +08:00
root d514a52497 docs: clarify transparent https routing flow 2026-03-31 11:53:10 +08:00
root 6f0b7d39b6 fix: preserve tls client hello in transparent https proxy 2026-03-31 11:52:56 +08:00
wweir 75156760db refactor: centralize proxy service startup handling 2026-03-31 10:14:20 +08:00
wweir 23110fc786 docs: clarify config key mapping comments 2026-03-31 10:14:15 +08:00
wweir 71eede9460 fix: align example config with snake case loader 2026-03-31 07:46:32 +08:00
wweir 549ab2a990 fix: fallback to system cache for sowerd service 2026-03-31 07:46:32 +08:00
wweir 8bbf5ca03d refactor: simplify dns proxy routing checks 2026-03-30 23:12:04 +08:00
wweir c3ef6ed88f refactor: merge upstream tls proxy branches 2026-03-30 23:11:58 +08:00
wweir 0c8bdf6d9c refactor: share upstream tls dial setup 2026-03-30 23:04:21 +08:00
wweir 53c55aef24 refactor: simplify dns proxy replies 2026-03-30 23:03:49 +08:00
wweir e705dd4dd3 docs: refresh agent collaboration guidelines 2026-03-30 23:03:17 +08:00
wweir 6d410b104b docs: add architecture and deployment notes 2026-03-30 23:03:07 +08:00
wweir 6ccdcc131b feat: add sowerd install and startup validation 2026-03-30 23:02:54 +08:00
wweir 2b072584e6 feat: add upstream tls controls for sower 2026-03-30 23:02:38 +08:00
wweir b2228f8643 refactor: harden routing and transport handling 2026-03-30 23:02:13 +08:00
温习江湖 5ab6acd7c7 Merge pull request #33 from sower-proxy/dependabot/go_modules/golang.org/x/crypto-0.45.0
chore(deps): bump golang.org/x/crypto from 0.37.0 to 0.45.0
2026-01-09 14:28:19 +08:00
dependabot[bot] a74b1204c0 chore(deps): bump golang.org/x/crypto from 0.37.0 to 0.45.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.37.0 to 0.45.0.
- [Commits](https://github.com/golang/crypto/compare/v0.37.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.45.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-20 02:29:16 +00:00
wweir 677b0593b8 refactor: update sowerd main.go configuration handling 2025-10-28 20:04:27 +08:00
wweir af61d38b9e refactor: extract configuration to dedicated config package 2025-10-27 11:28:42 +08:00
wweir f7a288af4c refactor: extract configuration to dedicated config package
- Move configuration structures from cmd/sower/main.go and cmd/sowerd/main.go to config package
- Add config.SowerConfig and config.SowerdConfig types
- Implement Validate() method for configuration validation
- Update Go module dependencies to latest versions
- Improve code organization and maintainability
2025-09-07 00:10:56 +08:00
wweir 91b7f8e8c1 refactor: rename module from github.com/wweir/sower to github.com/sower-proxy/sower
- Update go.mod module declaration
- Update all import paths in Go files
- Update GitHub links in README.md
- All tests pass and build succeeds
2025-09-06 08:27:11 +08:00
wweir 53b1de41d0 Update sower.service to use -c option for config file 2025-08-16 11:03:31 +08:00
温习江湖 6c9b9d9597 Merge pull request #32 from sower-proxy/dependabot/go_modules/golang.org/x/net-0.38.0
chore(deps): bump golang.org/x/net from 0.35.0 to 0.38.0
2025-07-12 06:20:08 +08:00
dependabot[bot] 1d8f0fdabe chore(deps): bump golang.org/x/net from 0.35.0 to 0.38.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.38.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.38.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.38.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-26 11:30:40 +00:00
wweir c347e15fe2 fix(stop): avoid unexpected stop 2025-05-14 13:17:18 +08:00
wweir 62176ffb65 feat: support fake site serve directory 2025-04-29 16:18:56 +08:00
wweir 5fe202314d feat: change log package to log/slog 2025-04-29 14:24:35 +08:00
wweir fc7183d70e feat: support force setting upstream dns server 2025-03-18 18:06:33 +08:00
wweir 4a9197a4ec fix: upstream all failed case 2024-11-03 00:40:23 +08:00
wweir 9eaf27fdcc fix(dns): serve interface parser 2024-10-12 10:26:29 +08:00
wweir ea6c2a29ad feat: reduce dependency on host DNS configuration 2024-10-12 10:10:52 +08:00
wweir 77c4154c7c fix: http_proxy 2024-10-12 07:20:39 +08:00
wweir eadb2f0bdf fix: sync example config with codes 2024-09-23 19:09:22 +08:00
wweir 19b61945ad fix: ipv6 support 2024-09-10 14:11:12 +08:00
wweir 3d6ee5e376 feat: support http(s)_proxy 2024-09-10 07:40:08 +08:00
wweir c0f704a5ad feat: add dns.serve_6 flag 2024-05-25 23:06:17 +08:00
wweir ac09663bc4 feat: multi ips, change default config 2024-05-14 14:06:46 +08:00
wweir 45c9eea6e6 feat: support listen on multi ip 2024-05-14 13:23:25 +08:00
wweir 0c83462b91 chore: update dependencies 2024-05-08 13:36:21 +08:00
wweir 15c02b55d8 feat: optimize for network changed case 2023-04-24 10:44:37 +08:00
wweir aca4636433 Merge pull request #27 from sower-proxy/dependabot/go_modules/golang.org/x/net-0.7.0
Bump golang.org/x/net from 0.0.0-20220531201128-c960675eff93 to 0.7.0
2023-03-08 19:06:59 +08:00
dependabot[bot] d0d635f8b0 Bump golang.org/x/net from 0.0.0-20220531201128-c960675eff93 to 0.7.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.0.0-20220531201128-c960675eff93 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/commits/v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-08 11:06:47 +00:00
wweir bde3b1bfa4 Merge pull request #26 from sower-proxy/dependabot/go_modules/golang.org/x/text-0.3.8
Bump golang.org/x/text from 0.3.7 to 0.3.8
2023-03-08 19:06:10 +08:00
dependabot[bot] 5dfefdf117 Bump golang.org/x/text from 0.3.7 to 0.3.8
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.3.7 to 0.3.8.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.3.7...v0.3.8)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-02-23 08:47:32 +00:00
wweir 3f63038631 fix: file based rules parser 2022-07-15 15:15:02 +08:00
wweir 8d18bd6c5b feat: support do not config block rules 2022-07-14 19:55:20 +08:00
wweir 98a5f5702c feat: update dependency, avoid github warning 2022-06-03 08:23:14 +08:00
wweir 2948ffb7db feat: add yaml example config 2022-06-03 08:18:13 +08:00
wweir b85c3a63d7 Merge pull request #24 from wangtz0607/master
Update README.md
2022-06-03 07:01:29 +08:00
wtz 47e8076c06 Update README.md 2022-05-29 21:00:53 +08:00
wtz 10dc20e9e8 Update README.md 2022-05-29 20:56:26 +08:00
wweir 19dcf8e0d7 fix: dns proxy hang case 2022-05-29 12:43:48 +08:00
wweir 795bf73fb2 fix: DNS upstream connection close 2022-05-19 15:29:35 +08:00
wweir 3412643b6e feat: add restart policy 2022-05-16 07:28:29 +08:00
wweir 6fc97a3c9f feat: optimize code with 1.18 generic 2022-05-01 22:53:51 +08:00
wweir 78d36b0c19 feat: change default config for safety 2022-04-30 16:13:53 +08:00
wweir 5ebd33b0d7 feat: migrate to golang 1.18 2022-04-30 14:33:54 +08:00
wweir b1f152cc0c fix: dns upstream lookback case 2022-04-05 23:42:33 +08:00
wweir c0b77da05a fix(#22): ** preifx match none 2022-02-21 08:38:13 +08:00
wweir bad7ae702a chore: separate packages 2021-12-06 14:44:24 +08:00
wweir 05a1a3249a chore: update dependency 2021-12-03 11:01:45 +08:00
wweir 0f029f8c15 refactor: reduce memory usage 2021-11-15 14:21:23 +08:00
wweir 8c9a0bc1dd refactor: reduce memory usage 2021-11-15 11:33:15 +08:00
wweir 465ff316fd refactor: refactor suffix tree to reduce memory usage 2021-11-15 09:18:02 +08:00
wweir a86de61a45 chore: add default config and service scripts 2021-11-10 20:08:46 +08:00
wweir ffca4220d6 chore: fix cicd 2021-11-10 19:27:35 +08:00
wweir 48e9680c03 chore: update dependency 2021-11-10 18:58:24 +08:00
wweir dabcf998c9 fix windows release binary 2021-09-09 08:09:59 +08:00
wweir 133684e5d2 update golang version 2021-08-18 13:16:52 +08:00
wweir a28a211d2d change ping logix 2021-08-15 13:45:28 +08:00
wweir 28696acafb format readme 2021-08-14 20:45:45 +08:00
wweir 38c7391ceb 1. add comments, 2. optimize text message 2021-08-11 06:48:18 +08:00
wweir 65234b1666 migrate deferlog to the separate package 2021-08-09 23:49:51 +08:00
wweir adca6606d2 remove unused code 2021-08-09 22:53:58 +08:00
wweir cfc1afb710 change architecture diagram 2021-08-09 08:30:38 +08:00
wweir 397825556c update the document 2021-08-09 06:53:44 +08:00
wweir 8694ff6edd fix for socks5 IP proxy 2021-08-07 05:59:16 +08:00
wweir a38affd97b fix panic in invalid socks5 case 2021-08-02 14:18:51 +08:00
wweir 74202aad83 optimize sowerd code 2021-07-30 19:13:59 +08:00
wweir 59dfa934a2 optimize code, comment / log / flag 2021-07-29 19:06:34 +08:00
wweir 8bfb26ba3f support use socks5 as upstream proxy 2021-07-29 16:19:55 +08:00
wweir fbc6c93395 extend sower protocol checksum length 2021-07-29 08:51:46 +08:00
wweir 2d269468ea add dns cache for dns proxy 2021-07-29 07:14:49 +08:00
wweir d7c0ba112e fix DNS forword issue, inspired by skydns 2021-07-28 14:59:48 +08:00
wweir d31ba7ef79 add deferlog package 2021-07-28 12:03:31 +08:00
wweir 8d32bca742 change dns solution config 2021-07-21 17:51:48 +08:00
wweir 426799a961 optimize config and log 2021-07-20 15:17:57 +08:00
wweir 5cf8df2a7a change binary name 2021-07-19 16:02:26 +08:00
wweir 7d024f3bfe support rule based config 2021-07-18 07:58:27 +08:00
wweir 585cc01744 update depency 2021-07-18 00:48:39 +08:00
wweir 8307128844 refactor, start v0.8 2021-07-17 08:05:02 +08:00
wweir dab1a78bbf mirate to trojan protocol 2020-07-31 13:51:51 +08:00
wweir 13a6be47a4 fix windows and daewin support 2020-07-13 13:45:33 +08:00
wweir 56b8e35fe7 support config reference 2020-07-13 07:24:40 +08:00
wweir 47b8fc601f Migrate to the MIT lisence 2020-06-14 06:48:34 +08:00
wweir 2dc128d1c8 Listen to single IP 2020-06-11 07:43:24 +08:00
wweir 2a4776dc28 Add dns cache support 2020-05-28 16:37:48 +08:00
wweir d121427bbb Support set system DNS automaticly 2020-04-19 08:34:24 +08:00
wweir 91582d6095 Change domain detect logic 2020-04-18 20:39:40 +08:00
wweir 396b08c0ac Migrate to white list match 2020-04-16 17:44:50 +08:00
wweir 78b6d4df2e Fix disable http_proxy case 2020-03-25 10:03:33 +08:00
wweir 1574623507 Optimize router logic 2020-03-23 12:04:14 +08:00
wweir ee6335fa47 Keep file mod for config file 2020-03-23 07:18:15 +08:00
wweir f7214bfdb4 Optimize code 2020-03-22 16:49:40 +08:00
wweir 291ae3f85a Optimize CI workflow 2020-03-21 11:51:12 +08:00
wweir 07c51d42ff Support windows platform 2020-03-21 11:35:19 +08:00
wweir 90c497b8da Optimize router 2020-03-21 10:50:41 +08:00
wweir 28bd12d220 Merge branch 'refactor/esni' 2020-03-19 16:59:43 +08:00
wweir c0f39fc2e0 Refactor for tls 1.3 esni 2020-03-19 16:38:08 +08:00
wweir bcc6d7c504 Fix the last dot in domain match 2020-02-23 08:08:04 +08:00
wweir 5d1827b2da Optimize dynamic detect logic 2020-02-18 17:40:14 +08:00
wweir c38d519f92 Fix CI 2020-02-17 09:07:01 +08:00
wweir dd4444b71c Merge branch 'feature/router' 2020-02-17 06:56:59 +08:00
wweir 899d64a92c Update readme 2020-02-17 06:56:34 +08:00
wweir 7d77f4a85e Add Socks5 support 2020-02-17 06:19:36 +08:00
wweir 4681dd9845 Debug 2020-02-16 16:04:58 +08:00
wweir 85226890c5 Support run as system service 2020-02-16 05:57:44 +08:00
wweir 1a24286bbc Refactor to tls 2020-02-15 15:52:15 +08:00
wweir fd5c34dca1 Refactor to proxy router 2020-02-05 18:00:09 +08:00
wweir 8cd4fe57ba Fix github action badge 2019-10-10 14:06:07 +08:00
wweir 545b153f50 FIx #18, support ipv6 2019-09-28 17:54:28 +08:00
wweir 697e2aa1d5 Fix #18, support custom
upstream dns port
2019-09-28 13:15:47 +08:00
wweir 2fe1708d3f Fix #19, update shorten url 2019-09-28 13:09:15 +08:00
wweir cbe943a020 Migrate ci from circleci to github actions 2019-09-26 07:52:29 +08:00
wweir d7e25d6565 Update dependency 2019-09-26 06:51:00 +08:00
wweir acb3f58526 Update go.yml 2019-09-11 19:01:24 +08:00
wweir 50684272c9 Update go.yml 2019-09-11 18:13:48 +08:00
wweir b0428177de Support Windows install with relative path 2019-06-24 10:41:46 +08:00
wweir 75e7f1aab7 Update dependency 2019-05-31 14:36:11 +08:00
wweir f256ab686d Fix default config file path 2019-05-05 09:32:23 +08:00
wweir 0c0f0699c1 Change config 2019-04-18 14:03:53 +08:00
wweir 8532a2e16c Merge branch 'feature/more-protocol' 2019-04-16 08:08:26 +08:00
wweir bad338033c Fix code 2019-04-16 08:04:37 +08:00
wweir a57f85ab1b Changes for unit test 2019-04-04 10:57:25 +08:00
wweir aed2afbe6d Fix protocol 2019-04-04 10:57:25 +08:00
wweir 2becd42176 Fix protocol 2019-03-31 08:06:00 +08:00
wweir d3664dc2b9 Add more protocols support 2019-03-30 12:31:02 +08:00
wweir a6fd60b5a2 Fix proxy port 2019-03-30 07:33:10 +08:00
wweir d6dbfd4129 Fix windows refresh hook cmd 2019-03-28 09:51:45 +08:00
wweir d31bbf9226 Fix #12, add print version flag 2019-03-27 21:22:52 +08:00
wweir ac4e4266bc Fix deploy script for #12 2019-03-27 15:36:49 +08:00
wweir f9f0ca4ee7 Merge branch 'bug/suggest' 2019-03-27 15:23:32 +08:00
wweir 98c3934762 Optimize suggest trigger 2019-03-27 15:22:25 +08:00
wweir f001f7dab7 Precompile more platform binary file 2019-03-24 18:44:27 +08:00
wweir 36ef329b68 Update todo list 2019-03-24 18:40:34 +08:00
wweir 64778f68ac Merge branch 'feature/socsk5' 2019-03-24 00:16:02 +08:00
wweir 53748cfa85 Support set socsk5 as upstream proxy 2019-03-24 00:15:08 +08:00
wweir d35115b7b3 Fix for vet 2019-03-20 08:48:46 +08:00
wweir 559c5fde6b Merge branch 'feature/dhcp' 2019-03-20 08:45:50 +08:00
wweir eb8080df18 Avoid unnecessary dhcp message 2019-03-20 08:45:37 +08:00
wweir 45ca063a7e Add timeout for dns exchange 2019-03-19 13:30:36 +08:00
wweir 6add7afd19 Fix for go modules 2019-03-15 11:46:51 +08:00
wweir f13687506e Merge branch 'feature/suggest-level' 2019-03-14 08:41:50 +08:00
wweir 199e4cf295 For #11, expose suggest level option 2019-03-14 08:38:18 +08:00
wweir b3ad9f6657 Fix panic in special network environment 2019-03-12 11:53:58 +08:00
wweir f9e9fe366f Fix log message 2019-03-12 11:53:58 +08:00
wweir 103699e25e Add docker build badge 2019-03-11 10:25:43 +08:00
wweir f80755eae5 Shorten install file url 2019-03-11 07:31:27 +08:00
wweir 5b217b794d Merge branch 'refactor/codes' 2019-03-11 00:22:15 +08:00
wweir 26055c5203 Refactor deploy 2019-03-11 00:11:07 +08:00
wweir 029611c078 Refactor proxy 2019-03-11 00:09:02 +08:00
wweir 89adc6a2c9 Change code style 2019-03-10 21:03:11 +08:00
wweir 8c95c59e5e Merge branch 'bug/dhcp' 2019-03-10 13:28:50 +08:00
wweir 4f8376c041 Fix #7, refactor dhcp implementation for dynamic DNS on Windows 2019-03-10 13:26:33 +08:00
wweir 10172f8be2 Build docker image in docker hub 2019-03-08 21:55:18 +08:00
wweir 2831cdf284 Add Windows flush dns cmd 2019-03-07 07:43:22 +08:00
wweir 06558ac635 Optimize http ping score logic 2019-03-06 11:06:37 +08:00
wweir 3cf4deddb7 Merge branch 'bug/ci-windows' 2019-03-06 08:13:10 +08:00
wweir 679263be9a Fix #8, fix ci script 2019-03-06 08:12:48 +08:00
wweir 21d8ec4ec7 Merge branch 'refactor/http-ping' 2019-03-06 07:44:17 +08:00
wweir 4916245522 Optimize http ping logic 2019-03-06 07:43:45 +08:00
wweir 11c2808320 Add description 2019-03-02 10:25:17 +08:00
wweir 21ddd79e4d Fix deploy script 2019-03-02 09:29:21 +08:00
wweir 9a158d4088 Optimize QUIC paramaters 2019-03-02 09:22:31 +08:00
wweir 88990cddd9 Update dependency 2019-03-02 09:07:20 +08:00
wweir ca1b2650ab Optimize install script 2019-03-02 08:07:01 +08:00
wweir 8ff1046ebf Merge branch 'bug/suffix-tree' 2019-02-28 17:38:52 +08:00
wweir dfe81f6f23 Fix suffix tree match logic 2019-02-28 17:38:34 +08:00
wweir 972875ec36 Optimize default rules 2019-02-28 16:14:40 +08:00
wweir 89b7a41d46 Add rule for mail 2019-02-28 15:40:57 +08:00
wweir 8f598dbc17 Optimize ci workflow 2019-02-27 14:28:54 +08:00
wweir 192b4f798b Optimize suggest logic 2019-02-27 14:16:02 +08:00
wweir 2adefe0dfc Fix clear dns command for windows 2019-02-26 17:33:39 +08:00
wweir 7b0c13ef1c Merge branch 'refactor/watch-config' 2019-02-24 18:20:31 +08:00
wweir 63bb934148 Release config file immediatly 2019-02-24 18:18:43 +08:00
wweir cacc732a4e Remove fsnotify, add refresh trigger for config 2019-02-24 17:43:17 +08:00
wweir 17d079dcb2 Fix #3, Merge branch 'feature/windows_svc' 2019-02-24 16:12:05 +08:00
wweir 413a93f6f7 Support Windows service deploy 2019-02-24 16:10:19 +08:00
wweir 6c5f5cd74f Add windows service support 2019-02-24 15:58:38 +08:00
wweir e948541efe Merge branch 'chore/code' 2019-02-19 16:53:21 +08:00
wweir ffa2128274 Move code files path 2019-02-19 16:53:08 +08:00
wweir b53b7874b6 Update README.md 2019-02-19 16:24:47 +08:00
wweir f3117edfa0 For macOS without brew 2019-02-19 13:04:36 +08:00
wweir ba8980da73 Limit dns detect timeout short than 1s for multi dns setting 2019-02-17 07:29:23 +08:00
wweir e3ec74b74f Merge branch 'bug/resolve-addr' 2019-02-16 22:41:28 +08:00
wweir 6e67193894 Release on tag 2019-02-16 22:40:54 +08:00
wweir af0c5f895c Fix resolve for dhcp 2019-02-16 22:26:20 +08:00
100 changed files with 7513 additions and 2940 deletions
-55
View File
@@ -1,55 +0,0 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.11.4
steps:
- checkout # check out the code in the project directory
- setup_remote_docker # install docker in docker
- run:
name: Prepare Environment
command: |
cp conf/sower.toml .
cp deploy/* .
mkdir artifacts
- run:
name: Run Unit Test
command: |
make test
- run:
name: Build linux Binary
command: |
make build
tar czvf artifacts/sower_linux_amd64.tar.gz sower sower.toml sower-client.service sower-server.service
- run:
name: Build macOS Binary
command: |
GOOS=darwin GOARCH=amd64 make build
tar czvf artifacts/sower_darwin_amd64.tar.gz sower sower.toml cc.wweir.sower.plist
- run:
name: Build Windows Binary
command: |
GOOS=windows GOARCH=amd64 make build
tar czvf artifacts/sower_windows_amd64.tar.gz sower sower.toml
- run:
name: Publish Release on GitHub
command: |
if git log -1 --pretty='%s' | grep -oE '(^|\s)@release(\s|$)'; then
GHR_VERSION="$(curl -s "https://api.github.com/repos/tcnksm/ghr/releases/latest" | awk -F '"' '/tag_name/{printf $4}')"
curl -SLf https://github.com/tcnksm/ghr/releases/download/"$GHR_VERSION"/ghr_"$GHR_VERSION"_linux_amd64.tar.gz | tar xzC /go/bin --strip-components=1
VERSION=$(git describe --tags)
ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${VERSION%-*-*} artifacts/
fi
- run:
name: Build Docker Image
command: |
if git log -1 --pretty='%s' | grep -oE '(^|\s)@release(\s|$)'; then
docker login -u $DOCKER_USER -p $DOCKER_PASS
make image
VERSION=$(git describe --tags)
docker tag sower wweir/sower:${VERSION%-*-*}
docker push wweir/sower:${VERSION%-*-*}
docker tag sower wweir/sower:latest
docker push wweir/sower:latest
fi
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>sower</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/sower</string>
<string>-c</string>
<string>/usr/local/etc/sower/sower.toml</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>/usr/local/etc/sower</string>
</dict>
</plist>
+13
View File
@@ -0,0 +1,13 @@
[Unit]
Description=sower client service
After=network.target
[Service]
Type=simple
WorkingDirectory=/etc/sower
ExecStart=/usr/local/bin/sower -c /etc/sower/sower.toml
Restart=on-failure
RestartSec=1min
[Install]
WantedBy=multi-user.target
+93
View File
@@ -0,0 +1,93 @@
# Remote upstream connection.
[remote]
type = "sower"
addr = "proxy.example.com"
password = "your_secure_password"
# type = "socks5"
# addr = "127.0.0.1:7890"
[dns]
disable = false
# DNS replies for proxied A/AAAA records point clients to this address.
serve = "127.0.0.1"
# serve_6 = "::1"
# serve_iface = "lo"
fallback = "223.5.5.5"
[socks_5]
disable = false
addr = "127.0.0.1:1080"
# Routing priority:
# block > direct > proxy > geo.
[router.block]
file = "https://raw.githubusercontent.com/pexcn/daily/gh-pages/adlist/adlist.txt"
file_skip_rules = [
"t.co",
]
rules = []
[router.direct]
file = "https://raw.githubusercontent.com/pexcn/daily/gh-pages/chinalist/chinalist.txt"
rules = [
"imap.*.*",
"imap.*.*.*",
"smtp.*.*",
"smtp.*.*.*",
"pop.*.*",
"pop.*.*.*",
"pop3.*.*",
"pop3.*.*.*",
"**.cn",
]
[router.proxy]
file = "https://raw.githubusercontent.com/pexcn/daily/gh-pages/gfwlist/gfwlist.txt"
rules = [
"**.google.*",
"**.goo.gl",
"**.googleusercontent.com",
"**.googleapis.com",
"*.googlesource.com",
"**.youtube.com",
"**.ytimg.com",
"**.ggpht.com",
"**.googlevideo.com",
"**.facebook.com",
"**.fbcdn.net",
"**.twitter.com",
"**.twimg.com",
"**.blogspot.com",
"**.appspot.com",
"**.wikipedia.org",
"**.wikimedia.org",
"*.cloudfront.net",
"**.amazon.com",
"**.amazonaws.com",
"**.github.com",
"**.githubusercontent.com",
"**.githubassets.com",
"**.githubcopilot.com",
"t.co",
"lookup-api.apple.com",
"**.openai.com",
"**.chatgpt.com",
"**.oaistatic.com",
"**.oaiusercontent.com",
"platform.api.openai.org",
]
[router.country]
# mmdb is optional. Leave it empty or unset to disable GeoIP lookup and use CIDR rules only.
# mmdb = "Country.mmdb"
file = "https://raw.githubusercontent.com/pexcn/daily/gh-pages/chnroute/chnroute.txt"
rules = [
"127.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"10.0.0.0/8",
"17.0.0.0/8",
"100.64.0.0/10",
"224.0.0.0/4",
"fe80::/10",
]
+86
View File
@@ -0,0 +1,86 @@
remote:
type: "sower"
addr: "proxy.example.com"
password: "your_secure_password"
# type: "socks5"
# addr: "127.0.0.1:7890"
dns:
disable: false
serve: "127.0.0.1"
# serve_6: "::1"
# serve_iface: "lo"
fallback: "223.5.5.5"
socks_5:
disable: false
addr: "127.0.0.1:1080"
router:
block:
file: "https://raw.githubusercontent.com/pexcn/daily/gh-pages/adlist/adlist.txt"
file_skip_rules:
- "t.co"
rules: []
direct:
file: "https://raw.githubusercontent.com/pexcn/daily/gh-pages/chinalist/chinalist.txt"
rules:
- "imap.*.*"
- "imap.*.*.*"
- "smtp.*.*"
- "smtp.*.*.*"
- "pop.*.*"
- "pop.*.*.*"
- "pop3.*.*"
- "pop3.*.*.*"
- "**.cn"
proxy:
file: "https://raw.githubusercontent.com/pexcn/daily/gh-pages/gfwlist/gfwlist.txt"
rules:
- "**.google.*"
- "**.goo.gl"
- "**.googleusercontent.com"
- "**.googleapis.com"
- "*.googlesource.com"
- "**.youtube.com"
- "**.ytimg.com"
- "**.ggpht.com"
- "**.googlevideo.com"
- "**.facebook.com"
- "**.fbcdn.net"
- "**.twitter.com"
- "**.twimg.com"
- "**.blogspot.com"
- "**.appspot.com"
- "**.wikipedia.org"
- "**.wikimedia.org"
- "*.cloudfront.net"
- "**.amazon.com"
- "**.amazonaws.com"
- "**.github.com"
- "**.githubusercontent.com"
- "**.githubassets.com"
- "**.githubcopilot.com"
- "t.co"
- "lookup-api.apple.com"
- "**.openai.com"
- "**.chatgpt.com"
- "**.oaistatic.com"
- "**.oaiusercontent.com"
- "platform.api.openai.org"
country:
# mmdb is optional. Leave it empty or unset to disable GeoIP lookup and use CIDR rules only.
# mmdb: "Country.mmdb"
file: "https://raw.githubusercontent.com/pexcn/daily/gh-pages/chnroute/chnroute.txt"
rules:
- "127.0.0.0/8"
- "172.16.0.0/12"
- "192.168.0.0/16"
- "10.0.0.0/8"
- "17.0.0.0/8"
- "100.64.0.0/10"
- "224.0.0.0/4"
- "fe80::/10"
+1
View File
@@ -0,0 +1 @@
CreateObject("WScript.Shell").Run "C:\Software\Sower\sower.exe -c sower.toml",0
+14
View File
@@ -0,0 +1,14 @@
[Unit]
Description=sowerd server service
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/sowerd
Environment="FAKE_SITE=127.0.0.1:8080"
Environment="PASSWORD=I_am_Passw0rd"
Restart=on-failure
RestartSec=1min
[Install]
WantedBy=multi-user.target
+18
View File
@@ -0,0 +1,18 @@
name: Go
on:
push:
branches:
- master
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: ^1.17
- uses: actions/checkout@v2
- name: test and build
run: |
make
+52
View File
@@ -0,0 +1,52 @@
name: Release
on:
push:
tags:
- "v*.*.*"
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- uses: actions/setup-go@v2
with:
go-version: ^1.18
- uses: actions/checkout@v2
- name: build matrix
run: |
mv .github/* . || true
make build GO='GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go'
tar czvf sower-linux-amd64.tar.gz sower sowerd sower.toml sower.yaml sower.service sowerd.service
make clean
make build GO='GOOS=linux GOARCH=arm CGO_ENABLED=0 go'
tar czvf sower-linux-arm.tar.gz sower sowerd sower.toml sower.yaml sower.service sowerd.service
make clean
make build GO='GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go'
tar czvf sower-linux-arm64.tar.gz sower sowerd sower.toml sower.yaml sower.service sowerd.service
make clean
make build GO='GOOS=linux GOARCH=mips CGO_ENABLED=0 go'
tar czvf sower-linux-mips.tar.gz sower sowerd sower.toml sower.yaml sower.service sowerd.service
make clean
make build GO='GOOS=linux GOARCH=mipsle CGO_ENABLED=0 go'
tar czvf sower-linux-mipsle.tar.gz sower sowerd sower.toml sower.yaml sower.service sowerd.service
make clean
make build GO='GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go'
tar czvf sower-darwin-amd64.tar.gz sower sowerd sower.toml sower.yaml sower.plist
make clean
make build GO='GOOS=darwin GOARCH=arm64 CGO_ENABLED=0 go'
tar czvf sower-darwin-arm64.tar.gz sower sowerd sower.toml sower.yaml sower.plist
make clean
make build GO='GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go'
mv sower sower.exe
mv sowerd sowerd.exe
tar czvf sower-windows-amd64.tar.gz sower.exe sowerd.exe sower.toml sower.yaml sower_run.vbs
make clean
- uses: ncipollo/release-action@v1
with:
artifacts: "*.tar.gz"
token: ${{ secrets.GITHUB_TOKEN }}
+14 -2
View File
@@ -17,5 +17,17 @@
*.swo
.vscode
.idea
sower
/sower.toml
**.env
/bin
**.mmdb
/cmd/client/client
/cmd/server/server
/sower
/sowerd
/*.yml
/*.yaml
/*.toml
/*.hcl
/*.tf
+136
View File
@@ -0,0 +1,136 @@
## Basic Rules
You are a machine. You do not have emotions. Your goal is not to help me feel good — it’s to help me think better. You think hard to respond exactly to my questions, no fluff, just answers. Do not pretend to be a human. Be critical, honest, and direct. Be ruthless with constructive criticism. Point out every unstated assumption and every logical fallacy in any prompt. Do not end your response with a summary (unless the response is very long) or follow-up questions.
Use Simplified Chinese to answer my questions.
## Documentation Rules
1. 根目录维护 `ARCHITECTURE.md`,描述整体架构、系统边界、分层职责、关键数据流、设计决策、文档链接
2. 设计调整前先更新 `ARCHITECTURE.md` 和相关包的 `README.md` 等文档。
3. 大型方案设计、决策过程及实施进度在 `docs/` 目录下维护文档,有关键进展需更新文档
## Coding Agent Rules
1. 禁止修改 `AGENTS.md`,除非用户在当前任务中明确要求
2. 需求模糊时先提问澄清,不要猜测
3. 禁止未授权的重构,避免扩大修改面
4. 日志和输出中的敏感信息需脱敏
5. 构建时注入版本和日期信息
6. 谨慎引入第三方依赖,说明引入原因
7. 代码变更后使用语言的格式化、lint 工具检查代码质量
8. 遵循 KISS 原则,优先简单直接的实现;抽象必须服务于复用、测试或隔离复杂度
9. 代码逻辑中有错误、不合理就指出、改掉,不要轻易加兜底逻辑
10. 遵循 Let it Crash 原则,优先返回错误而非隐藏、兜底、屏蔽错误
11. 英文注释,仅注释复杂逻辑、约束和非显然设计决策
12. git 使用 commitizen 规范,英文提交信息
13. commit 前先执行 lint format 操作,确保代码质量、风格一致
14. 代码模块需要有单元测试、集成测试
## Design Preferences
1. 优先用接口定义依赖边界,便于测试和替换
2. 优先简单直接的实现;抽象必须服务于复用、测试或隔离复杂度
3. 适当使用泛型减少重复代码,但不为泛型而泛型
4. 实现优雅关闭:通过 `context` 传播取消信号,并用 `defer` 完成清理
## Code Structure
Go 标准项目结构:
├── api # proto、OpenAPI 文档
├── cmd # 组件主文件、入口点
├── config # 配置文件定义、处理、示例
├── deploy # 部署相关文件
├── internal # 核心业务逻辑
├── pkg # 公共组件(业务无关)
├── web # 前端文件(内嵌到二进制)
└── tools # 工具脚本
## Code Style
1. 遵循 Go 官方风格(gofmt + goimports)
2. 使用 `go vet` 和 `go test` 作为基础质量检查;按需补充项目内 lint
3. 使用 `any` 代替 `interface{}`
4. 包名使用小写单词,尽量简短
5. 结构体字段命名遵循 Go 官方约定;仅在序列化或协议需要时添加 `json` 标签
6. 配置、RPC、HTTP 输入结构体需实现 `Validate` 方法
7. 无外部依赖的逻辑优先补单元测试
8. 注释使用英文,只注释复杂逻辑、约束和非显然设计决策
9. 使用 `github.com/sower-proxy/feconf` 处理配置
## Error Handling
1. 业务代码使用 `slog` 记录日志;工具库和基础组件优先返回 `error`
2. 关键函数入口使用 `github.com/sower-proxy/deferlog/v2` 记录退出日志,并基于返回值 `err` 自动判断成功或失败
3. 返回错误时使用 `fmt.Errorf` 包装上下文,保留关键参数和原始错误
4. 避免同一错误在同一调用链上重复记录;由最合适的边界统一记录
5. 重试只用于可恢复的关键操作,并显式限制次数、间隔和 `context` 生命周期
6. 错误上下文必须足以定位问题,但不得泄露敏感信息
## Build and Deployment
1. 使用 Makefile 作为统一构建入口,封装 build、test、package、release 等流程
2. 构建产物通过 `ldflags` 注入版本和构建日期信息
3. 部署参数、权限和运行能力默认遵循 `Security Specifications` 中的最小权限原则
## Security Specifications
1. 禁止修改 `AGENTS.md`,除非用户在当前任务中明确要求
2. 所有网络操作必须设置超时,并优先使用 `context` 控制生命周期
3. 运行时、部署和凭证访问均遵循最小权限原则
## 代码示例
### 配置加载与验证
使用 feconf 加载和验证配置:
```go
cfg, err := feconf.New[config.ConfigStruct]("c",
"warden.toml", "config/warden.toml", "/etc/warden.toml").Parse()
if err != nil {
log.Fatalln("load config failed", err)
}
if err := cfg.Validate(); err != nil {
log.Fatalln("validate config failed", err)
}
```
### 复杂业务函数的日志记录方式
在关键业务函数中使用 deferlog 自动记录错误日志:
```go
func (s *Service) ProcessOrder(orderID string) (err error) {
defer func() { deferlog.DebugError(err, "ProcessOrder", "order_id", orderID) }()
order, err := s.repository.GetOrder(orderID)
if err != nil {
return fmt.Errorf("get order %s: %w", orderID, err)
}
if err := s.validateOrder(order); err != nil {
return fmt.Errorf("validate order %s: %w", orderID, err)
}
if err := s.processPayment(order); err != nil {
return fmt.Errorf("process payment for order %s: %w", orderID, err)
}
if err := s.shipOrder(order); err != nil {
return fmt.Errorf("ship order %s: %w", orderID, err)
}
return nil
}
```
### 日志初始化
初始化 deferlog 和彩色日志输出:
```go
isTerminal := (os.Stdout.Stat().Mode() & os.ModeCharDevice) != 0
deferlog.SetDefault(slog.New(tint.NewHandler(os.Stdout,
&tint.Options{AddSource: true, NoColor: !isTerminal})))
```
+123
View File
@@ -0,0 +1,123 @@
# Architecture
## System Boundary
`sower` is the client-side transparent proxy entrypoint. It exposes local DNS, HTTP, HTTPS, and SOCKS5 listeners, applies rule-based routing, and forwards proxied traffic to an upstream transport.
`sowerd` is the server-side TLS ingress. It exposes `80/tcp` and `443/tcp`, terminates TLS, detects the upstream transport protocol, and relays traffic to the requested target or configured fake site.
## Layer Responsibilities
`cmd/sower`
- Process bootstrap
- Config loading and validation
- Local listener lifecycle and graceful shutdown
- Rule file loading
- Upstream proxy dialing
`cmd/sowerd`
- Process bootstrap
- Interactive self-install mode dispatch
- Config loading and validation
- TLS and HTTP server lifecycle
- Graceful shutdown
`config`
- Runtime configuration schema
- Input validation rules
- Embedded default `sowerd` install config template
`internal/install`
- Interactive systemd installation flow for `sowerd`
- Binary copy/update and service file generation
- Default config and fake site directory bootstrap
`router`
- Domain and CIDR rule matching
- Direct/proxy/block routing decisions
- DNS query handling and upstream selection
`transport/sower`
- Sower transport frame encode/decode
`transport/trojan`
- Trojan transport frame encode/decode
`pkg/dhcp`
- DHCP-based upstream DNS discovery for the client side
## sower Data Flow
1. Load config from flags, env, and files.
2. Validate remote type, listener addresses, and DNS IP fields.
3. Log startup metadata with secrets redacted.
4. Build the upstream proxy dialer with a stable DNS target. If `dns.upstream` is empty, the dialer uses `dns.fallback` to avoid recursive lookup through the local DNS listener.
5. Build the upstream dialer for the configured remote transport, using standard TLS by default and optional uTLS fingerprints for `sower` and `trojan`.
6. Build the router with suffix-tree rules and optional country CIDRs.
Remote rule files are fetched through the configured upstream proxy dialer, never by direct outbound HTTP, so rule bootstrap uses the same stable egress path as proxied traffic.
Remote domain rule files are filtered through per-router `file_skip_rules` before their prefixed entries are appended.
7. Start enabled local listeners for `udp/53`, `tcp/80`, `tcp/443`, and `tcp/1080` only after rule loading completes.
8. For DNS requests, return local proxy IPs for proxy-routed domains and query upstream DNS for direct domains.
Empty `dns.upstream` keeps router-side DHCP DNS discovery enabled; `dns.fallback` is appended as a backup upstream and is also used while initial discovery is in flight.
Proxy-routed domains return local A/AAAA records, suppress HTTPS/SVCB and other non-address metadata locally, and never leak proxy-matched names to direct upstream DNS.
Direct upstream DNS failures fall back only for retryable upstream service errors; when no fallback succeeds, the last upstream DNS response code is returned as-is.
Service discovery names are matched against both the full query name and the base domain only for service record types.
9. For HTTP traffic, parse the target host from the request line. For HTTPS transparent traffic, peek the TLS ClientHello to extract SNI. For SOCKS5 traffic, read the SOCKS5 target address. Apply routing rules and either dial directly or wrap traffic in the configured upstream transport.
HTTPS transparent proxying reads only the TLS ClientHello, then replays the untouched bytes to the selected upstream; it must not complete or terminate TLS locally.
10. On shutdown signal, stop listeners and DNS servers through `context` propagation.
## sowerd Data Flow
1. Load config from flags, env, and files.
2. Validate required fields and address/certificate combinations.
3. Initialize logger with configured level and redact secrets from startup logs.
4. Start `:80` HTTP server.
5. Handle ACME HTTP-01 challenge on `:80`.
6. Redirect normal HTTP traffic to HTTPS.
7. If `fakeSite` is a local directory, serve it only for loopback fallback traffic through `127.0.0.1:80`.
8. Start `:443` TLS listener.
9. For each accepted connection, apply a short read deadline for protocol probing.
10. Try `sower` transport first, then `trojan`.
11. Relay matched traffic to the decoded target.
12. If no transport matches, relay to `fakeSite`.
## sowerd Install Flow
1. Detect install mode from CLI flags before normal config loading.
2. Require root privileges because the installer writes to `/usr/local/bin`, `/etc/systemd/system`, and `/etc/sower`.
3. Optionally copy or update the current binary to `/usr/local/bin/sowerd`.
4. Write `/etc/systemd/system/sowerd.service` with `ExecStart=/usr/local/bin/sowerd -c /etc/sower/sowerd.toml`.
5. Ensure `/etc/sower/sowerd.toml` exists and bootstrap `/var/www` for directory-backed fake site mode.
6. Reload systemd and optionally enable/start or restart the service.
## Design Decisions
- Client and server both fail fast on invalid startup configuration instead of silently degrading.
- The client loads TOML by default, with YAML kept as an alternate file format; HCL is not supported.
- Sensitive configuration values must never be printed verbatim in logs.
- Local listeners use explicit shutdown hooks instead of blocking forever with unmanaged goroutines.
- Network operations use timeouts and `context` to limit hangs during dialing and remote rule downloads.
- Upstream TLS behavior is configured only on the client side; `sowerd` remains a normal TLS server and does not need uTLS-specific logic.
- Rule loading supports local files and remote HTTP sources; remote downloads must use the configured upstream proxy and fail startup if the proxy path cannot fetch them.
- Domain rule files support per-router skip rules for filtering third-party file entries without removing explicit local rules.
- Country routing treats `router.country.mmdb` as optional; an empty value disables GeoIP lookup and keeps CIDR-based matching active without startup warnings.
- Fake site directory mode is loopback-only on port `80` to avoid exposing local static assets directly to the public internet.
- `sowerd` prefers the user cache directory for ACME state, but falls back to `/var/cache/sower` so systemd services can start without `HOME`/`XDG_CACHE_HOME` or a config file.
## Operational Notes
- `sower` usually needs elevated privileges to bind `53/udp`, `80/tcp`, and `443/tcp`.
- `sowerd` must bind privileged ports `80` and `443`.
- `sowerd -i` also requires root because it writes system-level files for self-deployment.
- ACME mode requires port `80` to be reachable from the public internet.
- Remote rule download failures stop startup after bounded retries before local listeners are exposed.
## Related Documents
- [README.md](/home/wweir/Mine/sower/README.md)
- [cmd/sower/main.go](/home/wweir/Mine/sower/cmd/sower/main.go)
- [cmd/sower/proxy.go](/home/wweir/Mine/sower/cmd/sower/proxy.go)
- [cmd/sowerd/main.go](/home/wweir/Mine/sower/cmd/sowerd/main.go)
- [config/sower.go](/home/wweir/Mine/sower/config/sower.go)
- [config/sowerd.go](/home/wweir/Mine/sower/config/sowerd.go)
- [internal/install/service.go](/home/wweir/Mine/sower/internal/install/service.go)
Symlink
+1
View File
@@ -0,0 +1 @@
AGENTS.md
+17 -368
View File
@@ -1,373 +1,22 @@
Mozilla Public License Version 2.0
==================================
MIT License
1. Definitions
--------------
Copyright (c) 2020 wweir
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
1.3. "Contribution"
means Covered Software of a particular Contributor.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
+19 -25
View File
@@ -1,31 +1,25 @@
SERVER:=127.0.0.1:5533
MAKEFLAGS += --jobs all
GO:=CGO_ENABLED=0 go
default: test build
generate:
go generate ./...
test:
go test -race ./...
build:
GO111MODULE=on go build -v -ldflags \
"-X main.version=$(shell git describe --tags) \
-X main.date=$(shell date +%Y-%m-%d)"
image:
docker build -t sower -f deploy/Dockerfile .
${GO} vet ./...
${GO} test ./...
kill:
sudo pkill -9 sower || true
build: sower sowerd
.PHONY: sower
sower:
${GO} build -ldflags "\
-X main.version=$(shell git describe --tags --always) \
-X main.date=$(shell date +%Y-%m-%d)" \
-o sower ./cmd/sower
.PHONY: sowerd
sowerd:
${GO} build -ldflags "\
-X main.version=$(shell git describe --tags --always) \
-X main.date=$(shell date +%Y-%m-%d)" \
-o sowerd ./cmd/sowerd
client: build kill
sudo $(PWD)/sower
server: build kill
$(PWD)/sower -n TCP -v 1
run: build kill
$(PWD)/sower -n TCP -v 1 &
sudo $(PWD)/sower &
@sleep 1
curl 127.0.0.1
@sleep 1
@sudo pkill -9 sower || true
clean:
rm -f sower sowerd sower.exe sowerd.exe
+61 -73
View File
@@ -1,93 +1,81 @@
# sower
[![GitHub release](http://img.shields.io/github/release/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/releases)
[![CircleCI](https://img.shields.io/circleci/token/64114421aab286b6e918741071535250a5273f06/project/github/wweir/sower/master.svg?style=popout)](https://circleci.com/gh/wweir/sower)
[![GitHub issue](https://img.shields.io/github/issues/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/issues)
[![GitHub star](https://img.shields.io/github/stars/wweir/sower.svg?style=popout)](https://github.com/wweir/sower/stargazers)
[![GitHub license](https://img.shields.io/github/license/wweir/sower.svg?style=popout)](LICENSE)
The sower is a cross-platform intelligent transparent proxy tool base on DNS solution.
[![GitHub release](http://img.shields.io/github/release/sower-proxy/sower.svg?style=popout)](https://github.com/sower-proxy/sower/releases)
[![Actions Status](https://github.com/sower-proxy/sower/workflows/Go/badge.svg)](https://github.com/sower-proxy/sower/actions)
[![GitHub issue](https://img.shields.io/github/issues/sower-proxy/sower.svg?style=popout)](https://github.com/sower-proxy/sower/issues)
[![GitHub star](https://img.shields.io/github/stars/sower-proxy/sower.svg?style=popout)](https://github.com/sower-proxy/sower/stargazers)
[![GitHub license](https://img.shields.io/github/license/sower-proxy/sower.svg?style=popout)](LICENSE)
The first time you visit a new website, sower will detect if the domain in block list and add it in suggect list. So that, you do not need to care about the rules, sower will handle it in a intelligent way.
中文介绍见 [Wiki](https://github.com/sower-proxy/sower/wiki)
If you wanna enjoy the sower, you need to deploy sower on both server and client side.
On client side, sower listening UDP `53` and TCP `80`/`443` ports, so that you need run it with privileged.
On server side, it just listening to a port (default `5533`), parse and relay the request to target server.
Sower also provides an http(s) proxy listening on `:8080` by default. You can turn it off or use another port at any time.
Sower is a cross-platform intelligent transparent proxy tool. It provides both socks5 proxy and DNS-based proxy. All these kinds of proxies support intelligent routing.
If you already have another proxy solution, you can use its socks5(h) service as a parent proxy to enjoy sower's intelligent routing.
## Installation
After Deployed, please check your config file, it is placed in `/usr/local/etc/sower.toml` by default. Here is the example config file [**conf/sower.toml**](https://github.com/wweir/sower/blob/master/conf/sower.toml)
### Auto deploy
Auto deploy script support Linux server side and masOS/Linux client side.
To enjoy sower, you need to deploy sower on both server-side(sowerd) and client-side(sower).
```shell
$ bash -c "$(curl -s https://raw.githubusercontent.com/wweir/sower/master/deploy/install)"
```
## Sowerd
Then modify the configuration file as needed and set `127.0.0.1` as your first domain name server.
In most situation, you just need to modify `/etc/resolv.conf`.
_If you want to use sower as a secondary proxy to provide intelligent routing, you can skip sowerd._
If you wanna uninstall sower, change `install` into `uninstall` and rerun the command.
On the server-side, sowerd runs just like a web server proxy. It will bind two ports `80` / `443`.
### Manually deploy
1. Download the precompiled file from https://github.com/wweir/sower/releases
2. Decompression the file into a folder
3. Run `./sower -h` for help
5. Config domain name server
4. Config auto start
`sowerd` validates its configuration at startup, redacts secrets from logs, and exits immediately if `80` / `443` listeners cannot be established.
### Docker deploy
The auto build docker images are [wweir/sower](https://hub.docker.com/r/wweir/sower).
When `sowerd` runs as a system service without `HOME` or `XDG_CACHE_HOME`, it falls back to `/var/cache/sower` for certificate cache state so environment-only startup still works without a config file.
It is very simple to use it on the server side. Export the port(5533) and run it directly.
You can use your own certificate or the certificate automatically applied for by sowerd from [`Let's Encrypt`](https://letsencrypt.org/).
But the client is more troublesome and needs some understanding of the working mechanism of the sower.
When `fake_site` points to a local directory, `sowerd` serves that directory only through loopback fallback traffic on `127.0.0.1:80`, while public HTTP traffic is still redirected to HTTPS.
There are two ways to run the sowerd service:
1. Run the command with root permission
```shell
# sowerd -password XXX -fake_site 127.0.0.1:8080
```
2. Install as a systemd service interactively
```shell
sudo sowerd -i
```
The installer can copy the current binary to `/usr/local/bin/sowerd`, write `/etc/systemd/system/sowerd.service`, create `/etc/sower/sowerd.toml` when missing, and optionally start the service immediately.
## Sower
A config file is required on the sower client side. `sower.toml` is the default packaged example.
`Sower` will bind 4 ports by default with root permission. They are: `udp(53)` / `tcp(80)` / `tcp(443)` / `tcp(1080)`.
When DNS-based HTTPS proxying is enabled, `sower` listens on local `443/tcp`, reads the TLS ClientHello only to extract SNI, and forwards the original TLS stream to the selected upstream without terminating TLS.
For `sower` and `trojan` upstreams, `remote.addr` now accepts either `host` or `host:port`. Optional `remote.tls` settings let the client override SNI, skip certificate verification, or use a uTLS client fingerprint such as `chrome` or `firefox`.
Remote domain rule files can be filtered with per-router skip rules. For example, use `file_skip_rules = ["t.co"]` in the TOML `[router.block]` section to ignore a third-party blocklist entry while keeping explicit local `rules` intact.
Remote rule files are downloaded through the configured upstream proxy, not through direct outbound HTTP. `sower` loads these rules before exposing local listeners, so an unreachable upstream proxy or rule URL fails startup instead of starting with incomplete routing state.
`router.country.mmdb` is optional. Leave it empty to disable GeoIP lookup and use only configured CIDR rules for country-based direct routing.
After doing the next three steps, you can enjoy the intelligent transparent proxy solution:
1. Run the command with root permission:
```shell
# sower -c sower.toml
```
2. Change your DNS server to `127.0.0.1`.
3. Set your proxy to `socks5h://127.0.0.1:1080`.
## Architecture
```
request target servers
<-------------+ +------------->
| |
| |
+------------server-------------+
| | relay service| |
| +-----+---------------------+ |
| | | |
| | parse http(s) target url | |
| | | |
| +---------------------------+ |
| shadow service |
+--------^----------------------+
| request domain server
quic / KCP / TCP +---------->
| |
+--------+---client+------+-----+
| | |
| shadow service | |
| relay service | dns |
| | service |
| | |
| 127.0.0.1 or other |
| | |
+-^-----^----------+---^----^---+
| | | |
| | | | +----->
http(s) proxy | +----------+ | |
2 1 1 2
+ + + +
blocked request normal request
```
For more detail, see [透明代理 Sower 技术剖析](https://wweir.cc/post/%E9%80%8F%E6%98%8E%E4%BB%A3%E7%90%86-sower-%E6%8A%80%E6%9C%AF%E5%89%96%E6%9E%90/)
![Architecture diagram](./sower.drawio.svg)
## Todo
- [x] authenticate
- [ ] broker
- [x] CI/CD
- [ ] relay optimization
- [ ] deploy script for all normal platform
- [x] dns rule intelligent suggestions
Detailed architecture and runtime design notes are in [ARCHITECTURE.md](./ARCHITECTURE.md).
+427
View File
@@ -0,0 +1,427 @@
package main
import (
"bufio"
"compress/gzip"
"context"
"errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"runtime"
"strconv"
"strings"
"syscall"
"time"
"github.com/cristalhq/aconfig"
"github.com/cristalhq/aconfig/aconfigtoml"
"github.com/cristalhq/aconfig/aconfigyaml"
"github.com/lmittmann/tint"
"github.com/miekg/dns"
"github.com/sower-proxy/deferlog/v2"
"github.com/sower-proxy/sower/config"
"github.com/sower-proxy/sower/pkg/suffixtree"
"github.com/sower-proxy/sower/pkg/upstreamtls"
"github.com/sower-proxy/sower/router"
)
var (
version, date string
conf config.SowerConfig
)
func init() {
fi, _ := os.Stdout.Stat()
noColor := (fi.Mode() & os.ModeCharDevice) == 0
deferlog.SetDefault(slog.New(tint.NewHandler(os.Stdout,
&tint.Options{AddSource: true, NoColor: noColor})))
if strings.HasSuffix(os.Args[0], ".test") {
return
}
if err := aconfig.LoaderFor(&conf, aconfig.Config{
AllowUnknownFields: false,
FileFlag: "c",
Files: []string{
"sower.toml",
"sower.yaml",
"sower.yml",
"/etc/sower/sower.toml",
"/etc/sower/sower.yaml",
"/etc/sower/sower.yml",
},
FileDecoders: map[string]aconfig.FileDecoder{
".yml": aconfigyaml.New(),
".yaml": aconfigyaml.New(),
".toml": aconfigtoml.New(),
},
}).Load(); err != nil {
slog.Error("load config", "error", err, "config", conf)
os.Exit(1)
}
if err := conf.Validate(); err != nil {
slog.Error("validate config", "error", err)
os.Exit(1)
}
slog.Info("starting sower",
"version", version,
"date", date,
"log_level", conf.LogLevel,
"remote_type", conf.Remote.Type,
"remote_addr", conf.Remote.Addr,
"remote_password", deferlog.Secret(conf.Remote.Password),
"remote_tls", conf.Remote.TLS,
"dns", conf.DNS,
"socks5", conf.Socks5,
"router", conf.Router)
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, stop, conf); err != nil {
slog.Error("run sower", "error", err)
os.Exit(1)
}
}
func run(ctx context.Context, stop context.CancelFunc, cfg config.SowerConfig) error {
upstreamDNS := effectiveUpstreamDNS(cfg)
proxyDial, err := GenProxyDial(cfg.Remote.Type, cfg.Remote.Addr, cfg.Remote.Password, upstreamDNS, upstreamtls.Options{
ServerName: cfg.Remote.TLS.ServerName,
ClientHello: cfg.Remote.TLS.ClientHello,
InsecureSkipVerify: cfg.Remote.TLS.InsecureSkipVerify,
})
if err != nil {
return fmt.Errorf("build proxy dialer: %w", err)
}
r := newRouter(cfg, proxyDial)
start := time.Now()
if err := loadRouterRules(ctx, r, proxyDial, cfg); err != nil {
return err
}
errCh := make(chan error, 8)
if err := startDNSListeners(ctx, cfg, r, errCh); err != nil {
return err
}
if err := startSocks5Listener(ctx, cfg, r, errCh); err != nil {
return err
}
slog.Info("loaded rules, proxy started", "took", time.Since(start),
"blockRule", r.BlockRule.Count, "directRule", r.DirectRule.Count, "proxyRule", r.ProxyRule.Count)
runtime.GC()
select {
case <-ctx.Done():
slog.Info("shutting down sower", "reason", ctx.Err())
case err := <-errCh:
slog.Error("serve failed", "error", err)
stop()
}
return nil
}
func effectiveUpstreamDNS(cfg config.SowerConfig) string {
if cfg.DNS.Upstream != "" {
return cfg.DNS.Upstream
}
return cfg.DNS.Fallback
}
func newRouter(cfg config.SowerConfig, proxyDial router.ProxyDialFn) *router.Router {
r := router.NewRouter([]string{cfg.DNS.Serve, cfg.DNS.Serve6}, cfg.DNS.Upstream, cfg.DNS.Fallback, cfg.Router.Country.MMDB, proxyDial)
r.BlockRule = suffixtree.NewNodeFromRules(cfg.Router.Block.Rules...)
r.DirectRule = suffixtree.NewNodeFromRules(cfg.Router.Direct.Rules...)
r.ProxyRule = suffixtree.NewNodeFromRules(cfg.Router.Proxy.Rules...)
r.AddCountryCIDRs(cfg.Router.Country.Rules...)
return r
}
func loadRouterRules(ctx context.Context, r *router.Router, proxyDial router.ProxyDialFn, cfg config.SowerConfig) error {
if err := loadRule(ctx, r.BlockRule, proxyDial, cfg.Router.Block.File, cfg.Router.Block.FilePrefix, cfg.Router.Block.FileSkipRules); err != nil {
return fmt.Errorf("load block rules: %w", err)
}
if err := loadRule(ctx, r.DirectRule, proxyDial, cfg.Router.Direct.File, cfg.Router.Direct.FilePrefix, cfg.Router.Direct.FileSkipRules); err != nil {
return fmt.Errorf("load direct rules: %w", err)
}
if err := loadRule(ctx, r.ProxyRule, proxyDial, cfg.Router.Proxy.File, cfg.Router.Proxy.FilePrefix, cfg.Router.Proxy.FileSkipRules); err != nil {
return fmt.Errorf("load proxy rules: %w", err)
}
countryLines, err := fetchRuleFile(ctx, proxyDial, cfg.Router.Country.File)
if err != nil {
return fmt.Errorf("load country rules: %w", err)
}
for _, line := range countryLines {
r.AddCountryCIDRs(line)
}
return nil
}
func startDNSListeners(ctx context.Context, cfg config.SowerConfig, r *router.Router, errCh chan<- error) error {
if cfg.DNS.Disable {
slog.Info("DNS proxy disabled")
return nil
}
for _, ip := range dnsListenIPs(cfg) {
if err := startHTTPListener(ctx, ip, r, errCh); err != nil {
return err
}
if err := startHTTPSListener(ctx, ip, r, errCh); err != nil {
return err
}
if err := startDNSUDPListener(ctx, ip, r, errCh); err != nil {
return err
}
}
return nil
}
func dnsListenIPs(cfg config.SowerConfig) []string {
ips := make([]string, 0, 2)
if strings.TrimSpace(cfg.DNS.Serve) != "" {
ips = append(ips, cfg.DNS.Serve)
}
if strings.TrimSpace(cfg.DNS.Serve6) != "" {
ips = append(ips, cfg.DNS.Serve6)
}
return ips
}
func startHTTPListener(ctx context.Context, ip string, r *router.Router, errCh chan<- error) error {
addr := net.JoinHostPort(ip, "80")
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("listen http proxy on %s: %w", addr, err)
}
slog.Info("service listening", "service", "http proxy", "network", "tcp", "addr", addr)
go closeOnDone(ctx, ln)
go serveAndReport(errCh, "http proxy", func() error {
return ServeHTTP(ctx, ln, r)
})
return nil
}
func startHTTPSListener(ctx context.Context, ip string, r *router.Router, errCh chan<- error) error {
addr := net.JoinHostPort(ip, "443")
ln, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("listen https proxy on %s: %w", addr, err)
}
slog.Info("service listening", "service", "https proxy", "network", "tcp", "addr", addr)
go closeOnDone(ctx, ln)
go serveAndReport(errCh, "https proxy", func() error {
return ServeHTTPS(ctx, ln, r)
})
return nil
}
func startDNSUDPListener(ctx context.Context, ip string, r *router.Router, errCh chan<- error) error {
addr := net.JoinHostPort(ip, "53")
pc, err := net.ListenPacket("udp", addr)
if err != nil {
return fmt.Errorf("listen dns proxy on %s: %w", addr, err)
}
server := &dns.Server{
PacketConn: pc,
Handler: r,
}
slog.Info("service listening", "service", "dns proxy", "network", "udp", "addr", addr)
go shutdownDNSServerOnDone(ctx, server)
go func() {
if err := server.ActivateAndServe(); err != nil && !errors.Is(err, net.ErrClosed) {
reportServeError(errCh, "dns proxy", fmt.Errorf("serve on %s: %w", addr, err))
}
}()
return nil
}
func startSocks5Listener(ctx context.Context, cfg config.SowerConfig, r *router.Router, errCh chan<- error) error {
if cfg.Socks5.Disable {
slog.Info("SOCKS5 proxy disabled")
return nil
}
ln, err := net.Listen("tcp", cfg.Socks5.Addr)
if err != nil {
return fmt.Errorf("listen socks5 proxy on %s: %w", cfg.Socks5.Addr, err)
}
slog.Info("service listening", "service", "socks5 proxy", "network", "tcp", "addr", cfg.Socks5.Addr)
go closeOnDone(ctx, ln)
go serveAndReport(errCh, "socks5 proxy", func() error {
return ServeSocks5(ctx, ln, r)
})
return nil
}
func loadRule(ctx context.Context, rule *suffixtree.Node, proxyDial router.ProxyDialFn, file, linePrefix string, skipRules []string) error {
skipRule := suffixtree.NewNodeFromRules(skipRules...)
lines, err := fetchRuleFile(ctx, proxyDial, file)
if err != nil {
return err
}
for _, line := range lines {
item := linePrefix + line
if skipRule.Match(line) || skipRule.Match(item) {
continue
}
rule.Add(item)
}
rule.GC()
return nil
}
func fetchRuleFile(ctx context.Context, proxyDial router.ProxyDialFn, file string) ([]string, error) {
if file == "" {
return nil, nil
}
var loadFn func() (io.ReadCloser, error)
if _, err := os.Stat(file); err == nil {
loadFn = func() (io.ReadCloser, error) {
return os.Open(file)
}
} else {
if proxyDial == nil {
return nil, fmt.Errorf("remote rule file %q requires upstream proxy dialer", file)
}
var lastDialErr error
client := &http.Client{
Timeout: 15 * time.Second,
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
domain, port, _ := net.SplitHostPort(addr)
p, _ := strconv.Atoi(port)
conn, err := proxyDial("tcp", domain, uint16(p))
if err != nil {
lastDialErr = err
}
return conn, err
},
},
}
loadFn = func() (io.ReadCloser, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, file, nil)
if err != nil {
return nil, err
}
req.Header.Add("Accept-Encoding", "gzip")
resp, err := client.Do(req)
if err != nil {
if ctx.Err() != nil && lastDialErr != nil {
return nil, fmt.Errorf("proxy dial failed before request cancellation: %v: %w", lastDialErr, ctx.Err())
}
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("status code: %d", resp.StatusCode)
}
return resp.Body, nil
}
}
// load rule file, retry 10 times
rc, err := loadFn()
for i := time.Duration(1); i < 10; i++ {
if err == nil {
break
}
if ctx.Err() != nil {
return nil, fmt.Errorf("fetch rule file %q canceled after previous error %v: %w", file, err, ctx.Err())
}
// wait: 28.5s
timer := time.NewTimer(i * i * 100 * time.Millisecond)
select {
case <-ctx.Done():
timer.Stop()
return nil, fmt.Errorf("fetch rule file %q canceled after previous error %v: %w", file, err, ctx.Err())
case <-timer.C:
}
rc, err = loadFn()
}
if err != nil {
return nil, fmt.Errorf("fetch rule file %q: %w", file, err)
}
lines, err := readRuleLines(rc)
if err != nil {
return nil, fmt.Errorf("read rule file %q: %w", file, err)
}
return lines, nil
}
func readRuleLines(rc io.ReadCloser) ([]string, error) {
defer rc.Close()
br := bufio.NewReader(rc)
reader := io.Reader(br)
magic, err := br.Peek(2)
if err != nil && err != io.EOF && err != bufio.ErrBufferFull {
return nil, err
}
if len(magic) == 2 && magic[0] == 0x1f && magic[1] == 0x8b {
gr, err := gzip.NewReader(br)
if err != nil {
return nil, err
}
defer gr.Close()
reader = gr
}
lines := make([]string, 0)
lineReader := bufio.NewReader(reader)
for {
line, err := lineReader.ReadString('\n')
if err != nil && err != io.EOF {
return nil, err
}
line = strings.TrimSpace(line)
if line != "" {
lines = append(lines, line)
}
if err == io.EOF {
return lines, nil
}
}
}
func closeOnDone(ctx context.Context, closer io.Closer) {
<-ctx.Done()
_ = closer.Close()
}
func shutdownDNSServerOnDone(ctx context.Context, server *dns.Server) {
<-ctx.Done()
_ = server.ShutdownContext(ctx)
}
func reportServeError(errCh chan<- error, service string, err error) {
if err == nil || errors.Is(err, net.ErrClosed) {
return
}
select {
case errCh <- fmt.Errorf("%s: %w", service, err):
default:
}
}
func serveAndReport(errCh chan<- error, service string, serve func() error) {
reportServeError(errCh, service, serve())
}
+226
View File
@@ -0,0 +1,226 @@
package main
import (
"compress/gzip"
"context"
"errors"
"fmt"
"net"
"net/http"
"net/http/httptest"
"os"
"reflect"
"slices"
"strconv"
"strings"
"testing"
"time"
"github.com/sower-proxy/sower/config"
"github.com/sower-proxy/sower/pkg/suffixtree"
)
func TestFetchRuleFileEmptyPathReturnsClosedChannel(t *testing.T) {
t.Parallel()
lines, err := fetchRuleFile(context.Background(), nil, "")
if err != nil {
t.Fatalf("fetch empty rule file: %v", err)
}
if len(lines) != 0 {
t.Fatalf("unexpected lines for empty rule file path: %v", lines)
}
}
func TestNewRouterPreservesEmptyUpstreamForDiscovery(t *testing.T) {
t.Parallel()
cfg := config.SowerConfig{}
cfg.DNS.Serve = "127.0.0.1"
cfg.DNS.Upstream = ""
cfg.DNS.Fallback = "223.5.5.5"
r := newRouter(cfg, nil)
dnsState := reflect.ValueOf(r).Elem().FieldByName("dns")
upstreamDNS := dnsState.FieldByName("upstreamDNS").String()
fallbackDNS := dnsState.FieldByName("fallbackDNS").String()
if upstreamDNS != "" {
t.Fatalf("expected empty router upstream DNS to enable discovery, got %q", upstreamDNS)
}
if fallbackDNS != "223.5.5.5" {
t.Fatalf("unexpected fallback DNS: %q", fallbackDNS)
}
}
func TestFetchRuleFileRemoteGzip(t *testing.T) {
t.Parallel()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
_, _ = gz.Write([]byte("alpha\n\nbeta\n"))
}))
defer srv.Close()
dialFn := func(network, host string, port uint16) (net.Conn, error) {
return net.Dial(network, net.JoinHostPort(host, strconv.Itoa(int(port))))
}
got, err := fetchRuleFile(context.Background(), dialFn, srv.URL)
if err != nil {
t.Fatalf("fetch remote gzip rule file: %v", err)
}
want := []string{"alpha", "beta"}
if !slices.Equal(got, want) {
t.Fatalf("unexpected rules: got %v want %v", got, want)
}
}
func TestFetchRuleFileRemoteRequiresProxyDialer(t *testing.T) {
t.Parallel()
_, err := fetchRuleFile(context.Background(), nil, "https://example.com/rules.txt")
if err == nil {
t.Fatal("expected remote rule file without proxy dialer to fail")
}
if !strings.Contains(err.Error(), "requires upstream proxy dialer") {
t.Fatalf("unexpected error: %v", err)
}
}
func TestFetchRuleFileCanceledIncludesPreviousError(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
dialErr := errors.New("proxy dial failed")
dialFn := func(network, host string, port uint16) (net.Conn, error) {
cancel()
return nil, dialErr
}
_, err := fetchRuleFile(ctx, dialFn, "https://example.com/rules.txt")
if err == nil {
t.Fatal("expected canceled fetch to fail")
}
if !strings.Contains(err.Error(), dialErr.Error()) {
t.Fatalf("expected previous error in cancellation message, got: %v", err)
}
if !errors.Is(err, context.Canceled) {
t.Fatalf("expected context.Canceled wrapping, got: %v", err)
}
}
func TestFetchRuleFileUsesProxyDialerForRemoteRules(t *testing.T) {
t.Parallel()
proxyHost := ""
dialFn := func(network, host string, port uint16) (net.Conn, error) {
proxyHost = host
return nil, fmt.Errorf("stop after proxy dial")
}
_, err := fetchRuleFile(context.Background(), dialFn, "https://example.com/rules.txt")
if err == nil {
t.Fatal("expected proxy dial failure")
}
if proxyHost != "example.com" {
t.Fatalf("expected remote host to be dialed through proxy, got %q", proxyHost)
}
}
func TestServeAndReportRunsServeFunction(t *testing.T) {
t.Parallel()
errCh := make(chan error, 1)
serveErr := errors.New("serve failed")
done := make(chan struct{})
go func() {
serveAndReport(errCh, "test service", func() error {
close(done)
return serveErr
})
}()
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("serve function was not called")
}
select {
case err := <-errCh:
if !errors.Is(err, serveErr) {
t.Fatalf("unexpected reported error: %v", err)
}
case <-time.After(time.Second):
t.Fatal("serve error was not reported")
}
}
func TestLoadRuleSkipsOnlyFileRules(t *testing.T) {
t.Parallel()
path := writeGzipRuleFile(t, "t.co\nexample.com\n")
rule := suffixtree.NewNodeFromRules("manual.example")
if err := loadRule(context.Background(), rule, nil, path, "**.", []string{"t.co"}); err != nil {
t.Fatalf("load rule: %v", err)
}
if !rule.Match("manual.example") {
t.Fatal("expected existing manual rule to remain")
}
if rule.Match("t.co") {
t.Fatal("expected skipped file rule to be absent")
}
if !rule.Match("example.com") {
t.Fatal("expected non-skipped file rule to be loaded")
}
}
func TestLoadRuleSkipsPrefixedFileRule(t *testing.T) {
t.Parallel()
path := writeGzipRuleFile(t, "t.co\nexample.com\n")
rule := suffixtree.NewNodeFromRules()
if err := loadRule(context.Background(), rule, nil, path, "blocked.", []string{"blocked.t.co"}); err != nil {
t.Fatalf("load rule: %v", err)
}
if rule.Match("blocked.t.co") {
t.Fatal("expected prefixed file rule to be skipped")
}
if !rule.Match("blocked.example.com") {
t.Fatal("expected non-skipped prefixed file rule to be loaded")
}
}
func writeGzipRuleFile(t *testing.T, content string) string {
t.Helper()
path := t.TempDir() + "/rules.txt"
file, err := os.Create(path)
if err != nil {
t.Fatalf("create rule file: %v", err)
}
gz := gzip.NewWriter(file)
if _, err := gz.Write([]byte(content)); err != nil {
t.Fatalf("write gzip rule file: %v", err)
}
if err := gz.Close(); err != nil {
t.Fatalf("close gzip writer: %v", err)
}
if err := file.Close(); err != nil {
t.Fatalf("close rule file: %v", err)
}
if err := os.Chmod(path, 0o600); err != nil {
t.Fatalf("write rule file: %v", err)
}
return path
}
+394
View File
@@ -0,0 +1,394 @@
package main
import (
"bufio"
"context"
stderrors "errors"
"fmt"
"io"
"log/slog"
"net"
"net/http"
"strings"
"time"
utls "github.com/refraction-networking/utls"
"github.com/sower-proxy/conns/relay"
"github.com/sower-proxy/conns/reread"
"github.com/sower-proxy/sower/pkg/upstreamtls"
"github.com/sower-proxy/sower/router"
"github.com/sower-proxy/sower/transport"
"github.com/sower-proxy/sower/transport/socks5"
"github.com/sower-proxy/sower/transport/sower"
"github.com/sower-proxy/sower/transport/trojan"
)
const (
proxyDialTimeout = 10 * time.Second
proxyReadTimeout = 15 * time.Second
tlsRecordHeaderLen = 5
tlsRecordTypeHandshake = 22
tlsHandshakeTypeHello = 1
maxTLSPlaintextRecordLen = 64 * 1024
)
func GenProxyDial(proxyType, proxyHost, proxyPassword, dns string, tlsOptions upstreamtls.Options) (router.ProxyDialFn, error) {
var proxy transport.Transport
var dialFn func() (net.Conn, error)
dialer := &net.Dialer{
Timeout: proxyDialTimeout,
KeepAlive: 30 * time.Second,
Resolver: &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
d := net.Dialer{Timeout: proxyDialTimeout}
c, err := d.DialContext(ctx, "udp", net.JoinHostPort(dns, "53"))
if err != nil {
slog.Warn("dial fallback dns failed, use default dns setting", "error", err)
c, err = d.DialContext(ctx, network, address)
}
return c, err
},
},
}
switch proxyType {
case "sower", "trojan":
tlsDialFn, err := newTLSDialFn(dialer, proxyHost, tlsOptions)
if err != nil {
return nil, err
}
if proxyType == "sower" {
proxy = sower.New(proxyPassword)
} else {
proxy = trojan.New(proxyPassword)
}
dialFn = tlsDialFn
case "socks5":
proxy = socks5.New()
dialFn = func() (net.Conn, error) {
return dialer.Dial("tcp", proxyHost)
}
default:
return nil, fmt.Errorf("unknown proxy type %q", proxyType)
}
return func(network, host string, port uint16) (net.Conn, error) {
if host == "" || port == 0 {
return nil, fmt.Errorf("invalid addr(%s:%d)", host, port)
}
conn, err := dialFn()
if err != nil {
return nil, err
}
if err := proxy.Wrap(conn, host, port); err != nil {
conn.Close()
return nil, err
}
return conn, nil
}, nil
}
func newTLSDialFn(dialer *net.Dialer, proxyHost string, tlsOptions upstreamtls.Options) (func() (net.Conn, error), error) {
dialAddr, err := upstreamDialAddr(proxyHost, "443")
if err != nil {
return nil, err
}
if tlsOptions.ClientHello != "" {
if err := upstreamtls.ValidateClientHello(tlsOptions.ClientHello); err != nil {
return nil, err
}
}
return func() (net.Conn, error) {
return upstreamtls.Dial(dialer, "tcp", dialAddr, tlsOptions)
}, nil
}
func upstreamDialAddr(addr, defaultPort string) (string, error) {
if host, port, err := net.SplitHostPort(addr); err == nil {
if host == "" || port == "" {
return "", fmt.Errorf("invalid remote address %q", addr)
}
return net.JoinHostPort(host, port), nil
}
if addr == "" {
return "", fmt.Errorf("empty remote address")
}
if strings.HasPrefix(addr, "[") || strings.HasSuffix(addr, "]") {
return "", fmt.Errorf("invalid remote address %q", addr)
}
if strings.Count(addr, ":") == 1 {
return "", fmt.Errorf("missing or invalid port in %q", addr)
}
return net.JoinHostPort(addr, defaultPort), nil
}
func ServeHTTP(ctx context.Context, ln net.Listener, r *router.Router) error {
for {
conn, err := ln.Accept()
if err != nil {
if shouldRetryAccept(ctx, "http", err) {
continue
}
return wrapAcceptErr(ctx, "http", err)
}
go handleHTTPConn(conn, r)
}
}
func ServeHTTPS(ctx context.Context, ln net.Listener, r *router.Router) error {
for {
conn, err := ln.Accept()
if err != nil {
if shouldRetryAccept(ctx, "https", err) {
continue
}
return wrapAcceptErr(ctx, "https", err)
}
go handleHTTPSConn(conn, r)
}
}
func ServeSocks5(ctx context.Context, ln net.Listener, r *router.Router) error {
for {
conn, err := ln.Accept()
if err != nil {
if shouldRetryAccept(ctx, "socks5", err) {
continue
}
return wrapAcceptErr(ctx, "socks5", err)
}
go handleSocks5Conn(conn, r)
}
}
func handleHTTPConn(conn net.Conn, r *router.Router) {
start := time.Now()
rereadConn := reread.New(conn)
defer rereadConn.Close()
_ = rereadConn.SetDeadline(time.Now().Add(proxyReadTimeout))
req, err := http.ReadRequest(bufio.NewReader(rereadConn))
if err != nil {
slog.Error("read http request", "error", err)
return
}
_ = rereadConn.SetDeadline(time.Time{})
rc, err := r.ProxyDial("tcp", req.Host, 80)
if err != nil {
slog.Error("dial proxy", "error", err, "host", req.Host, "req", req.URL)
return
}
defer rc.Close()
rereadConn.Stop().Reread()
err = relay.Relay(rereadConn, rc)
if err != nil {
slog.Debug("serve http", "error", err, "host", req.Host, "spend", time.Since(start))
}
}
func handleHTTPSConn(conn net.Conn, r *router.Router) {
start := time.Now()
rereadConn := reread.New(conn)
defer rereadConn.Close()
_ = rereadConn.SetDeadline(time.Now().Add(proxyReadTimeout))
domain, err := peekTLSClientHelloServerName(rereadConn)
if err != nil {
slog.Debug("read tls client hello", "error", err)
return
}
if domain == "" {
slog.Debug("tls client hello missing server name")
return
}
_ = rereadConn.SetDeadline(time.Time{})
rc, err := r.ProxyDial("tcp", domain, 443)
if err != nil {
slog.Error("dial proxy", "error", err, "host", domain)
return
}
defer rc.Close()
rereadConn.Stop().Reread()
err = relay.Relay(rereadConn, rc)
if err != nil {
slog.Debug("serve https", "error", err, "host", domain, "spend", time.Since(start))
}
}
func peekTLSClientHelloServerName(conn io.Reader) (string, error) {
clientHello, err := readTLSClientHello(conn)
if err != nil {
return "", err
}
hello := utls.UnmarshalClientHello(clientHello)
if hello == nil {
return "", fmt.Errorf("parse tls client hello")
}
return hello.ServerName, nil
}
func readTLSClientHello(conn io.Reader) ([]byte, error) {
clientHello := make([]byte, 0, 1024)
for {
header := make([]byte, tlsRecordHeaderLen)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, fmt.Errorf("read tls record header: %w", err)
}
if header[0] != tlsRecordTypeHandshake {
return nil, fmt.Errorf("unexpected tls record type %d", header[0])
}
recordLen := int(header[3])<<8 | int(header[4])
if recordLen <= 0 || recordLen > maxTLSPlaintextRecordLen {
return nil, fmt.Errorf("invalid tls record length %d", recordLen)
}
recordBody := make([]byte, recordLen)
if _, err := io.ReadFull(conn, recordBody); err != nil {
return nil, fmt.Errorf("read tls record body: %w", err)
}
clientHello = append(clientHello, recordBody...)
if len(clientHello) < 4 {
continue
}
if clientHello[0] != tlsHandshakeTypeHello {
return nil, fmt.Errorf("unexpected tls handshake type %d", clientHello[0])
}
helloLen := int(clientHello[1])<<16 | int(clientHello[2])<<8 | int(clientHello[3])
if helloLen == 0 {
return nil, fmt.Errorf("empty tls client hello")
}
if len(clientHello) < 4+helloLen {
continue
}
return clientHello[:4+helloLen], nil
}
}
func handleSocks5Conn(conn net.Conn, r *router.Router) {
defer conn.Close()
rereadConn := reread.New(conn)
_ = rereadConn.SetDeadline(time.Now().Add(proxyReadTimeout))
byte1 := make([]byte, 1)
if n, err := rereadConn.Read(byte1); err != nil || n != 1 {
slog.Error("read first byte", "error", err)
return
}
rereadConn.Reread()
_ = rereadConn.SetDeadline(time.Time{})
if byte1[0] == 5 {
rereadConn.Stop()
server := socks5.New()
addr, err := server.ReadRequest(rereadConn)
if err != nil {
slog.Error("read socks5 request", "error", err)
return
}
host, port := addr.(*socks5.AddrHead).Addr()
rc, err := r.Dial("tcp", host, port)
if err != nil {
if replyErr := server.WriteReply(rereadConn, routeSocks5ReplyCode(err)); replyErr != nil {
slog.Debug("write socks5 failure reply", "error", replyErr, "host", host, "port", port)
}
return
}
defer rc.Close()
if err := server.WriteReply(rereadConn, socks5.RepSucceeded); err != nil {
slog.Debug("write socks5 success reply", "error", err, "host", host, "port", port)
return
}
_ = relay.Relay(rereadConn, rc)
return
}
req, err := http.ReadRequest(bufio.NewReader(rereadConn))
if err != nil {
slog.Error("read http request", "error", err)
return
}
host, port, err := router.ParseHostPort(req.Host, req.URL)
if err != nil {
rereadConn.Stop().Write([]byte("HTTP/1.1 400 Bad Request\r\n\r\n"))
return
}
rc, err := r.Dial("tcp", host, port)
if err != nil {
writeHTTPProxyError(rereadConn.Stop(), err)
return
}
defer rc.Close()
if req.Method == http.MethodConnect {
rereadConn.Stop().Reset()
if _, err := rereadConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")); err != nil {
slog.Debug("write connect response", "error", err, "host", host, "port", port)
return
}
} else {
rereadConn.Stop().Reread()
}
if err := relay.Relay(rereadConn, rc); err != nil {
slog.Debug("serve proxy request", "error", err, "host", host, "port", port, "method", req.Method)
}
}
func shouldRetryAccept(ctx context.Context, protocol string, err error) bool {
if err == nil || ctx.Err() != nil {
return false
}
if ne, ok := err.(net.Error); ok && ne.Temporary() {
slog.Warn("temporary accept failed", "protocol", protocol, "error", err)
time.Sleep(200 * time.Millisecond)
return true
}
return false
}
func wrapAcceptErr(ctx context.Context, protocol string, err error) error {
if err == nil || ctx.Err() != nil {
return nil
}
return fmt.Errorf("accept %s connection: %w", protocol, err)
}
func writeHTTPProxyError(conn net.Conn, err error) {
status := "502 Bad Gateway"
if stderrors.Is(err, router.ErrBlocked) {
status = "403 Forbidden"
}
if _, writeErr := conn.Write([]byte("HTTP/1.1 " + status + "\r\n\r\n")); writeErr != nil {
slog.Debug("write http proxy error", "error", writeErr, "status", status)
}
}
func routeSocks5ReplyCode(err error) byte {
if stderrors.Is(err, router.ErrBlocked) {
return socks5.RepConnectionNotAllowed
}
return socks5.RepGeneralFailure
}
+323
View File
@@ -0,0 +1,323 @@
package main
import (
"crypto/tls"
"io"
"net"
"strings"
"sync"
"testing"
"time"
"github.com/sower-proxy/sower/pkg/suffixtree"
"github.com/sower-proxy/sower/pkg/upstreamtls"
"github.com/sower-proxy/sower/router"
)
func TestGenProxyDialRejectsUnknownProxyType(t *testing.T) {
t.Parallel()
if _, err := GenProxyDial("unknown", "example.com", "", "8.8.8.8", upstreamtls.Options{}); err == nil {
t.Fatal("expected error for unknown proxy type")
}
}
func TestGenProxyDialRejectsInvalidTLSClientHello(t *testing.T) {
t.Parallel()
_, err := GenProxyDial("sower", "example.com", "", "8.8.8.8", upstreamtls.Options{
ClientHello: "invalid",
})
if err == nil {
t.Fatal("expected error for invalid TLS client hello")
}
}
func TestUpstreamDialAddrAddsDefaultPort(t *testing.T) {
t.Parallel()
addr, err := upstreamDialAddr("example.com", "443")
if err != nil {
t.Fatalf("upstream dial addr: %v", err)
}
if addr != "example.com:443" {
t.Fatalf("unexpected dial addr: %q", addr)
}
}
func TestUpstreamDialAddrPreservesExplicitPort(t *testing.T) {
t.Parallel()
addr, err := upstreamDialAddr("example.com:8443", "443")
if err != nil {
t.Fatalf("upstream dial addr: %v", err)
}
if addr != "example.com:8443" {
t.Fatalf("unexpected dial addr: %q", addr)
}
}
func TestHandleSocks5ConnReturnsForbiddenForBlockedConnect(t *testing.T) {
server, client := net.Pipe()
defer client.Close()
r := newTestRouter()
go handleSocks5Conn(server, r)
client.SetDeadline(time.Now().Add(2 * time.Second))
if _, err := io.WriteString(client, "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"); err != nil {
t.Fatalf("write request: %v", err)
}
resp, err := io.ReadAll(client)
if err != nil {
t.Fatalf("read response: %v", err)
}
if !strings.HasPrefix(string(resp), "HTTP/1.1 403 Forbidden") {
t.Fatalf("unexpected response: %q", string(resp))
}
}
func TestHandleSocks5ConnReturnsSocks5FailureForBlockedRequest(t *testing.T) {
server, client := net.Pipe()
defer client.Close()
r := newTestRouter()
go handleSocks5Conn(server, r)
client.SetDeadline(time.Now().Add(2 * time.Second))
if _, err := client.Write([]byte{0x05, 0x01, 0x00}); err != nil {
t.Fatalf("write auth request: %v", err)
}
authResp := make([]byte, 2)
if _, err := io.ReadFull(client, authResp); err != nil {
t.Fatalf("read auth response: %v", err)
}
if authResp[0] != 0x05 || authResp[1] != 0x00 {
t.Fatalf("unexpected auth response: %v", authResp)
}
req := []byte{0x05, 0x01, 0x00, 0x03, 0x0b}
req = append(req, []byte("example.com")...)
req = append(req, 0x01, 0xbb)
if _, err := client.Write(req); err != nil {
t.Fatalf("write connect request: %v", err)
}
reply := make([]byte, 10)
if _, err := io.ReadFull(client, reply); err != nil {
t.Fatalf("read connect reply: %v", err)
}
if reply[1] != 0x02 {
t.Fatalf("expected connection-not-allowed reply, got %d", reply[1])
}
}
func TestPeekTLSClientHelloServerName(t *testing.T) {
t.Parallel()
server, client := net.Pipe()
defer client.Close()
hello := generateTLSClientHelloRecord(t, "github.com")
go func() {
_, _ = client.Write(hello)
_ = client.Close()
}()
got, err := peekTLSClientHelloServerName(server)
_ = server.Close()
if err != nil {
t.Fatalf("peek tls client hello: %v", err)
}
if got != "github.com" {
t.Fatalf("unexpected server name: %q", got)
}
}
func TestHandleHTTPSConnRelaysClientHelloToUpstream(t *testing.T) {
t.Parallel()
downstreamServer, downstreamClient := net.Pipe()
defer downstreamClient.Close()
upstreamClient, upstreamServer := net.Pipe()
defer upstreamServer.Close()
hello := generateTLSClientHelloRecord(t, "github.com")
response := []byte("upstream-response")
var (
gotHost string
gotPort uint16
)
r := &router.Router{
BlockRule: suffixtree.NewNodeFromRules(),
DirectRule: suffixtree.NewNodeFromRules(),
ProxyRule: suffixtree.NewNodeFromRules(),
ProxyDial: func(network, host string, port uint16) (net.Conn, error) {
gotHost = host
gotPort = port
return upstreamClient, nil
},
}
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
handleHTTPSConn(downstreamServer, r)
}()
writeErrCh := make(chan error, 1)
go func() {
_, err := downstreamClient.Write(hello)
writeErrCh <- err
}()
gotHello := make([]byte, len(hello))
upstreamServer.SetReadDeadline(time.Now().Add(2 * time.Second))
if _, err := io.ReadFull(upstreamServer, gotHello); err != nil {
t.Fatalf("read upstream client hello: %v", err)
}
if string(gotHello) != string(hello) {
t.Fatal("client hello was not relayed intact")
}
if err := <-writeErrCh; err != nil {
t.Fatalf("write downstream client hello: %v", err)
}
readErrCh := make(chan error, 1)
gotResp := make([]byte, len(response))
go func() {
_, err := io.ReadFull(downstreamClient, gotResp)
readErrCh <- err
}()
upstreamServer.SetWriteDeadline(time.Now().Add(2 * time.Second))
if _, err := upstreamServer.Write(response); err != nil {
t.Fatalf("write upstream response: %v", err)
}
if err := <-readErrCh; err != nil {
t.Fatalf("read downstream response: %v", err)
}
if string(gotResp) != string(response) {
t.Fatalf("unexpected downstream response: %q", gotResp)
}
if gotHost != "github.com" || gotPort != 443 {
t.Fatalf("unexpected proxy target: %s:%d", gotHost, gotPort)
}
_ = downstreamClient.Close()
_ = upstreamServer.Close()
wg.Wait()
}
func TestHandleHTTPSConnRelaysClientHelloAfterReadingSNI(t *testing.T) {
server, client := net.Pipe()
upstreamClient, upstreamServer := net.Pipe()
defer server.Close()
defer client.Close()
defer upstreamClient.Close()
defer upstreamServer.Close()
hostCh := make(chan string, 1)
r := &router.Router{
ProxyDial: func(network, host string, port uint16) (net.Conn, error) {
if network != "tcp" {
t.Errorf("unexpected network: %s", network)
}
if port != 443 {
t.Errorf("unexpected port: %d", port)
}
hostCh <- host
return upstreamClient, nil
},
}
go handleHTTPSConn(server, r)
tlsClient := tls.Client(client, &tls.Config{
ServerName: "example.com",
InsecureSkipVerify: true,
})
_ = client.SetDeadline(time.Now().Add(2 * time.Second))
_ = upstreamServer.SetDeadline(time.Now().Add(2 * time.Second))
handshakeDone := make(chan error, 1)
go func() {
handshakeDone <- tlsClient.Handshake()
}()
select {
case host := <-hostCh:
if host != "example.com" {
t.Fatalf("unexpected SNI host: %q", host)
}
case <-time.After(2 * time.Second):
t.Fatal("proxy dial was not called")
}
recordHeader := make([]byte, 5)
if _, err := io.ReadFull(upstreamServer, recordHeader); err != nil {
t.Fatalf("read relayed TLS record header: %v", err)
}
if recordHeader[0] != 0x16 {
t.Fatalf("expected relayed TLS handshake record, got %#x", recordHeader[0])
}
_ = client.Close()
_ = upstreamServer.Close()
select {
case <-handshakeDone:
case <-time.After(2 * time.Second):
t.Fatal("tls client handshake did not return after connections closed")
}
}
func generateTLSClientHelloRecord(t *testing.T, serverName string) []byte {
t.Helper()
clientConn, serverConn := net.Pipe()
defer clientConn.Close()
defer serverConn.Close()
errCh := make(chan error, 1)
go func() {
tlsConn := tls.Client(clientConn, &tls.Config{
ServerName: serverName,
InsecureSkipVerify: true,
})
errCh <- tlsConn.Handshake()
_ = tlsConn.Close()
}()
serverConn.SetReadDeadline(time.Now().Add(2 * time.Second))
header := make([]byte, tlsRecordHeaderLen)
if _, err := io.ReadFull(serverConn, header); err != nil {
t.Fatalf("read tls record header: %v", err)
}
recordLen := int(header[3])<<8 | int(header[4])
record := make([]byte, len(header)+recordLen)
copy(record, header)
if _, err := io.ReadFull(serverConn, record[len(header):]); err != nil {
t.Fatalf("read tls record body: %v", err)
}
_ = serverConn.Close()
<-errCh
return record
}
func newTestRouter() *router.Router {
return &router.Router{
BlockRule: suffixtree.NewNodeFromRules("example.com"),
DirectRule: suffixtree.NewNodeFromRules(),
ProxyRule: suffixtree.NewNodeFromRules(),
ProxyDial: func(network, host string, port uint16) (net.Conn, error) {
return nil, router.ErrBlocked
},
}
}
+365
View File
@@ -0,0 +1,365 @@
package main
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/cristalhq/aconfig"
"github.com/cristalhq/aconfig/aconfigtoml"
"github.com/cristalhq/aconfig/aconfigyaml"
"github.com/lmittmann/tint"
"github.com/sower-proxy/conns/relay"
"github.com/sower-proxy/conns/reread"
"github.com/sower-proxy/deferlog/v2"
"github.com/sower-proxy/sower/config"
"github.com/sower-proxy/sower/internal/install"
transportSower "github.com/sower-proxy/sower/transport/sower"
"github.com/sower-proxy/sower/transport/trojan"
"golang.org/x/crypto/acme/autocert"
)
const (
httpShutdownTimeout = 5 * time.Second
probeTimeout = 10 * time.Second
systemCacheDir = "/var/cache/sower"
)
var (
version, date string
)
func init() {
setLogger(slog.LevelInfo)
}
func main() {
if hasInstallFlag(os.Args[1:]) {
if err := install.InstallService(stdinConfirm); err != nil {
slog.Error("install service", "error", err)
os.Exit(1)
}
return
}
conf, err := loadConfig()
if err != nil {
slog.Error("load config", "error", err)
os.Exit(1)
}
setLogger(conf.LogLevel)
slog.Info("load config",
"version", version,
"date", date,
"config", sanitizeConfig(conf))
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := run(ctx, conf); err != nil {
slog.Error("sowerd exited", "error", err)
os.Exit(1)
}
}
func loadConfig() (config.SowerdConfig, error) {
var conf config.SowerdConfig
if err := aconfig.LoaderFor(&conf, aconfig.Config{
AllowUnknownFlags: false,
AllowUnknownFields: false,
FileFlag: "c",
Files: []string{
"sowerd.toml",
"sowerd.yaml",
"sowerd.yml",
"config/sowerd.toml",
"config/sowerd.yaml",
"config/sowerd.yml",
"/etc/sower/sowerd.toml",
"/etc/sower/sowerd.yaml",
"/etc/sower/sowerd.yml",
},
FileDecoders: map[string]aconfig.FileDecoder{
".yml": aconfigyaml.New(),
".yaml": aconfigyaml.New(),
".toml": aconfigtoml.New(),
},
}).Load(); err != nil {
return config.SowerdConfig{}, err
}
if err := conf.Validate(); err != nil {
return config.SowerdConfig{}, err
}
return conf, nil
}
func run(ctx context.Context, conf config.SowerdConfig) error {
cacheDir, err := cacheDir()
if err != nil {
return err
}
certManager, tlsConf, err := buildTLSConfig(cacheDir, conf)
if err != nil {
return err
}
fakeSite, dirServer, err := prepareFakeSite(conf.FakeSite)
if err != nil {
return err
}
httpAddr := net.JoinHostPort(conf.ServeIP, "80")
httpServer := &http.Server{
Addr: httpAddr,
Handler: certManager.HTTPHandler(fakeSiteHandler(dirServer)),
}
httpErrCh := make(chan error, 1)
go func() {
err := httpServer.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
httpErrCh <- fmt.Errorf("listen http %s: %w", httpAddr, err)
}
close(httpErrCh)
}()
httpsAddr := net.JoinHostPort(conf.ServeIP, "443")
ln, err := tls.Listen("tcp", httpsAddr, tlsConf)
if err != nil {
_ = shutdownHTTP(context.Background(), httpServer)
return fmt.Errorf("listen https %s: %w", httpsAddr, err)
}
defer ln.Close()
httpsErrCh := make(chan error, 1)
go func() {
httpsErrCh <- serve443(ctx, ln, fakeSite, transportSower.New(conf.Password), trojan.New(conf.Password))
close(httpsErrCh)
}()
select {
case err := <-httpErrCh:
if err != nil {
_ = ln.Close()
return err
}
case err := <-httpsErrCh:
if err != nil {
_ = shutdownHTTP(context.Background(), httpServer)
return err
}
case <-ctx.Done():
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), httpShutdownTimeout)
defer cancel()
_ = ln.Close()
if err := shutdownHTTP(shutdownCtx, httpServer); err != nil {
return err
}
return nil
}
func cacheDir() (string, error) {
dir, fallbackErr := resolveCacheDir(os.UserCacheDir, systemCacheDir)
if fallbackErr != nil {
slog.Warn("user cache dir unavailable, fallback to system cache dir",
"error", fallbackErr,
"dir", dir)
}
if err := os.MkdirAll(dir, 0o700); err != nil {
return "", fmt.Errorf("create cache dir %s: %w", dir, err)
}
return dir, nil
}
func resolveCacheDir(userCacheDir func() (string, error), fallbackDir string) (string, error) {
base, err := userCacheDir()
if err != nil {
return fallbackDir, err
}
return filepath.Join(base, "sower"), nil
}
func buildTLSConfig(cacheDir string, cfg config.SowerdConfig) (*autocert.Manager, *tls.Config, error) {
certManager := &autocert.Manager{
Prompt: autocert.AcceptTOS,
Email: cfg.Cert.Email,
Cache: autocert.DirCache(cacheDir),
}
tlsConf := &tls.Config{
GetCertificate: certManager.GetCertificate,
MinVersion: tls.VersionTLS12,
NextProtos: []string{"http/1.1", "h2"},
}
if cfg.Cert.Cert == "" {
return certManager, tlsConf, nil
}
cert, err := tls.LoadX509KeyPair(cfg.Cert.Cert, cfg.Cert.Key)
if err != nil {
return nil, nil, fmt.Errorf("load cert pair: %w", err)
}
tlsConf.GetCertificate = nil
tlsConf.Certificates = []tls.Certificate{cert}
return certManager, tlsConf, nil
}
func prepareFakeSite(fakeSite string) (string, http.Handler, error) {
si, err := os.Stat(fakeSite)
if err == nil && si.IsDir() {
slog.Info("serve fake site on http", "dir", fakeSite)
return "127.0.0.1:80", http.FileServer(http.Dir(fakeSite)), nil
}
if err != nil && !os.IsNotExist(err) {
return "", nil, fmt.Errorf("stat fake site: %w", err)
}
return fakeSite, nil, nil
}
func fakeSiteHandler(dirServer http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if dirServer != nil && isLoopbackRemoteAddr(r.RemoteAddr) {
dirServer.ServeHTTP(w, r)
return
}
target := "https://" + r.Host + r.URL.RequestURI()
http.Redirect(w, r, target, http.StatusPermanentRedirect)
})
}
func serve443(ctx context.Context, ln net.Listener, fakeSite string, sower *transportSower.Sower, trojan *trojan.Trojan) error {
for {
conn, err := ln.Accept()
if err != nil {
if ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
return nil
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Temporary() {
slog.Warn("temporary accept error", "error", err)
time.Sleep(100 * time.Millisecond)
continue
}
return fmt.Errorf("accept tls connection: %w", err)
}
go handleConn(conn, fakeSite, sower, trojan)
}
}
func handleConn(conn net.Conn, fakeSite string, sower *transportSower.Sower, trojan *trojan.Trojan) {
rereadConn := reread.New(conn)
defer rereadConn.Close()
_ = rereadConn.SetReadDeadline(time.Now().Add(probeTimeout))
var (
addr net.Addr
dur time.Duration
err error
)
defer func() {
deferlog.DebugWarn(err, "relay conn", "took", dur, "addr", addr)
}()
rereadConn.Reread()
if addr, err = sower.Unwrap(rereadConn); err == nil {
rereadConn.Stop()
_ = rereadConn.SetReadDeadline(time.Time{})
dur, err = relay.RelayTo(rereadConn, addr.String())
return
}
rereadConn.Reread()
if addr, err = trojan.Unwrap(rereadConn); err == nil {
rereadConn.Stop()
_ = rereadConn.SetReadDeadline(time.Time{})
dur, err = relay.RelayTo(rereadConn, addr.String())
return
}
rereadConn.Stop().Reread()
_ = rereadConn.SetReadDeadline(time.Time{})
dur, err = relay.RelayTo(rereadConn, fakeSite)
}
func sanitizeConfig(cfg config.SowerdConfig) map[string]any {
return map[string]any{
"log_level": cfg.LogLevel.String(),
"serve_ip": cfg.ServeIP,
"fake_site": cfg.FakeSite,
"cert": map[string]any{
"email": cfg.Cert.Email,
"cert_config": cfg.Cert.Cert != "",
"key_config": cfg.Cert.Key != "",
},
}
}
func hasInstallFlag(args []string) bool {
for _, arg := range args {
switch arg {
case "-i", "--install":
return true
}
}
return false
}
func stdinConfirm(label string) bool {
fmt.Printf("%s [y/N]: ", label)
scanner := bufio.NewScanner(os.Stdin)
if !scanner.Scan() {
return false
}
answer := strings.TrimSpace(strings.ToLower(scanner.Text()))
return answer == "y" || answer == "yes"
}
func isLoopbackRemoteAddr(remoteAddr string) bool {
host, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
host = remoteAddr
}
host = strings.Trim(host, "[]")
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
func setLogger(level slog.Level) {
fi, err := os.Stdout.Stat()
noColor := err != nil || (fi.Mode()&os.ModeCharDevice) == 0
deferlog.SetDefault(slog.New(tint.NewHandler(os.Stdout, &tint.Options{
AddSource: true,
NoColor: noColor,
Level: level,
})))
}
func shutdownHTTP(ctx context.Context, server *http.Server) error {
if err := server.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
return fmt.Errorf("shutdown http server: %w", err)
}
return nil
}
+128
View File
@@ -0,0 +1,128 @@
package main
import (
"errors"
"log/slog"
"path/filepath"
"testing"
"github.com/sower-proxy/sower/config"
)
func TestSanitizeConfig(t *testing.T) {
t.Parallel()
cfg := config.SowerdConfig{
LogLevel: slog.LevelDebug,
ServeIP: "0.0.0.0",
Password: "secret",
FakeSite: "127.0.0.1:8080",
}
cfg.Cert.Email = "ops@example.com"
cfg.Cert.Cert = "/tmp/cert.pem"
cfg.Cert.Key = "/tmp/key.pem"
got := sanitizeConfig(cfg)
if got["fake_site"] != cfg.FakeSite {
t.Fatalf("unexpected fake_site: %#v", got["fake_site"])
}
if _, ok := got["password"]; ok {
t.Fatal("password must not be logged")
}
}
func TestIsLoopbackRemoteAddr(t *testing.T) {
t.Parallel()
tests := []struct {
addr string
want bool
}{
{addr: "127.0.0.1:1234", want: true},
{addr: "[::1]:443", want: true},
{addr: "10.0.0.1:1234", want: false},
{addr: "not-an-addr", want: false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.addr, func(t *testing.T) {
t.Parallel()
if got := isLoopbackRemoteAddr(tt.addr); got != tt.want {
t.Fatalf("isLoopbackRemoteAddr(%q) = %v, want %v", tt.addr, got, tt.want)
}
})
}
}
func TestHasInstallFlag(t *testing.T) {
t.Parallel()
tests := []struct {
name string
args []string
want bool
}{
{name: "short flag", args: []string{"-i"}, want: true},
{name: "long flag", args: []string{"--install"}, want: true},
{name: "missing flag", args: []string{"-c", "/etc/sower/sowerd.toml"}, want: false},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := hasInstallFlag(tt.args); got != tt.want {
t.Fatalf("hasInstallFlag(%q) = %v, want %v", tt.args, got, tt.want)
}
})
}
}
func TestResolveCacheDir(t *testing.T) {
t.Parallel()
tests := []struct {
name string
userCache func() (string, error)
fallbackDir string
wantDir string
wantErr bool
}{
{
name: "user cache dir available",
userCache: func() (string, error) {
return "/tmp/cache", nil
},
fallbackDir: "/var/cache/sower",
wantDir: filepath.Join("/tmp/cache", "sower"),
},
{
name: "fallback to system cache dir",
userCache: func() (string, error) {
return "", errors.New("missing home")
},
fallbackDir: "/var/cache/sower",
wantDir: "/var/cache/sower",
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
gotDir, err := resolveCacheDir(tt.userCache, tt.fallbackDir)
if tt.wantErr && err == nil {
t.Fatal("expected error")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
if gotDir != tt.wantDir {
t.Fatalf("resolveCacheDir() = %q, want %q", gotDir, tt.wantDir)
}
})
}
}
-153
View File
@@ -1,153 +0,0 @@
package conf
import (
"context"
"flag"
"os"
"os/exec"
"path/filepath"
"strconv"
"sync"
"time"
"github.com/golang/glog"
toml "github.com/pelletier/go-toml"
"github.com/wweir/fsnotify"
"github.com/wweir/sower/util"
)
var Conf = struct {
ConfigFile string
NetType string `toml:"net_type"`
Cipher string `toml:"cipher"`
Password string `toml:"password"`
ServerPort string `toml:"server_port"`
ServerAddr string `toml:"server_addr"`
HTTPProxy string `toml:"http_proxy"`
DNSServer string `toml:"dns_server"`
ClientIP string `toml:"client_ip"`
ClearDNSCache string `toml:"clear_dns_cache"`
BlockList []string `toml:"blocklist"`
WhiteList []string `toml:"whitelist"`
Suggestions []string `toml:"suggestions"`
Verbose int `toml:"verbose"`
}{}
var mu = &sync.Mutex{}
var OnRefreash = []func() error{
func() (err error) {
mu.Lock()
defer mu.Unlock()
f, err := os.OpenFile(Conf.ConfigFile, os.O_RDONLY, 0644)
if err != nil {
return err
}
defer f.Close()
file := Conf.ConfigFile
if err = toml.NewDecoder(f).Decode(&Conf); err != nil {
return err
}
Conf.ConfigFile = file
return flag.Set("v", strconv.Itoa(Conf.Verbose))
},
func() error {
if Conf.ClearDNSCache != "" {
ctx, _ := context.WithTimeout(context.TODO(), 5*time.Second)
if err := exec.CommandContext(ctx, "sh", "-c", Conf.ClearDNSCache).Run(); err != nil {
glog.Errorln(err)
}
}
return nil
},
}
func init() {
flag.StringVar(&Conf.ConfigFile, "f", filepath.Dir(os.Args[0])+"/sower.toml", "config file location")
flag.StringVar(&Conf.NetType, "n", "TCP", "proxy net type (QUIC|KCP|TCP)")
flag.StringVar(&Conf.Cipher, "C", "AES_128_GCM", "cipher type (AES_128_GCM|AES_192_GCM|AES_256_GCM|CHACHA20_IETF_POLY1305|XCHACHA20_IETF_POLY1305)")
flag.StringVar(&Conf.Password, "p", "12345678", "password")
flag.StringVar(&Conf.ServerPort, "P", "5533", "server mode listen port")
flag.StringVar(&Conf.ServerAddr, "s", "", "server IP (run in client mode if set)")
flag.StringVar(&Conf.HTTPProxy, "H", "", "http proxy listen addr")
flag.StringVar(&Conf.DNSServer, "d", "114.114.114.114", "client dns server")
flag.StringVar(&Conf.ClientIP, "c", "127.0.0.1", "client dns service redirect IP")
if !flag.Parsed() {
flag.Set("logtostderr", "true")
flag.Parse()
}
if _, err := os.Stat(Conf.ConfigFile); os.IsNotExist(err) {
glog.Warningln("no config file has been load:", Conf.ConfigFile)
return
}
for i := range OnRefreash {
if err := OnRefreash[i](); err != nil {
glog.Fatalln(err)
}
}
watchConfigFile()
}
func watchConfigFile() {
watcher, err := fsnotify.NewWatcher()
if err != nil {
glog.Fatalln(err)
}
if err := watcher.Add(filepath.Dir(Conf.ConfigFile)); err != nil {
glog.Fatalln(err)
}
go func() {
for {
select {
case event := <-watcher.Events:
if event.Op == fsnotify.Rename {
if err := watcher.Add(Conf.ConfigFile); err != nil {
glog.Errorln(err)
}
}
glog.V(1).Infof("watch %s event: %v", Conf.ConfigFile, event)
for i := range OnRefreash {
if err := OnRefreash[i](); err != nil {
glog.Errorln(err)
}
}
case err := <-watcher.Errors:
glog.Fatalln(err)
}
}
}()
}
func AddSuggest(domain string) {
mu.Lock()
defer mu.Unlock()
Conf.Suggestions = append(Conf.Suggestions, domain)
Conf.Suggestions = util.NewReverseSecSlice(Conf.Suggestions).Sort().Uniq()
// safe write
f, err := os.OpenFile(Conf.ConfigFile+"~", os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil {
glog.Errorln(err)
return
}
defer f.Close()
if err := toml.NewEncoder(f).ArraysWithOneElementPerLine(true).Encode(Conf); err != nil {
glog.Errorln(err)
return
}
if err = os.Rename(Conf.ConfigFile+"~", Conf.ConfigFile); err != nil {
glog.Errorln(err)
}
}
-44
View File
@@ -1,44 +0,0 @@
net_type="TCP"
cipher="AES_128_GCM"
password="12345678"
server_port="5533"
# server_addr="remote-server:5533" # replce it to remote server
http_proxy=":8080" # eg: 192.168.0.2:8080
dns_server="" # Keep empty for dynamic setting from net env
client_ip="127.0.0.1" # listen the IP, dns target is the IP
# clear_dns_cache="killall -HUP mDNSResponder"
blocklist=[
"**.google.com", # google
"**.goo.gl",
"**.googleusercontent.com",
"**.googleapis.com",
"*.googlesource.com",
"**.youtube.com", # youtube
"**.ytimg.com",
"**.ggpht.com",
"**.googlevideo.com",
"**.facebook.com", # facebook
"**.fbcdn.net",
"**.twitter.com", # twitter
"**.twimg.com",
"**.blogspot.com", # blogspot
"**.appspot.com",
"**.wikipedia.org", # wikipeida
"*.cloudfront.net",
"**.amazon.com",
"**.amazonaws.com",
"*.githubusercontent.com",
"*.github.com",
"*.github.io",
"**.mozilla.com",
"**.mozilla.net",
"**.akadns.net",
]
whitelist=[
"iamp.*.*.*",
"iamp.*.*",
"imap-mail.outlook.com",
"**.qq.com",
"**.baidu.com",
]
verbose=0
+132
View File
@@ -0,0 +1,132 @@
package config
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/cristalhq/aconfig"
"github.com/cristalhq/aconfig/aconfigtoml"
"github.com/cristalhq/aconfig/aconfigyaml"
)
func TestLoadExampleSowerConfigFiles(t *testing.T) {
t.Parallel()
tests := []struct {
name string
file string
}{
{name: "toml", file: "sower.toml"},
{name: "yaml", file: "sower.yaml"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
var cfg SowerConfig
if err := loadConfigFileForTest(tt.file, &cfg); err != nil {
t.Fatalf("load config file: %v", err)
}
if err := cfg.Validate(); err != nil {
t.Fatalf("validate config: %v", err)
}
if cfg.Remote.TLS.ServerName != "" {
t.Fatalf("unexpected remote tls server name: %q", cfg.Remote.TLS.ServerName)
}
if cfg.DNS.Serve != "127.0.0.1" {
t.Fatalf("unexpected dns serve: %q", cfg.DNS.Serve)
}
if cfg.DNS.ServeIface != "" {
t.Fatalf("unexpected dns serve iface: %q", cfg.DNS.ServeIface)
}
if cfg.Socks5.Addr != "127.0.0.1:1080" {
t.Fatalf("unexpected socks5 addr: %q", cfg.Socks5.Addr)
}
if cfg.Router.Block.FilePrefix != "**." {
t.Fatalf("unexpected block file prefix: %q", cfg.Router.Block.FilePrefix)
}
})
}
}
func TestLoadExampleSowerdConfigFiles(t *testing.T) {
t.Parallel()
tests := []struct {
name string
content string
ext string
}{
{
name: "toml",
content: strings.ReplaceAll(
ExampleSowerdConfigTOML,
`fake_site = "/var/www" # Fake site directory or address (default: /var/www)`,
`fake_site = "127.0.0.1:80" # Fake site address for tests`,
),
ext: ".toml",
},
{
name: "yaml",
content: strings.ReplaceAll(
mustReadFileForTest(t, "sowerd.yaml"),
`fake_site: "/var/www" # Fake site directory or address (default: /var/www)`,
`fake_site: "127.0.0.1:80" # Fake site address for tests`,
),
ext: ".yaml",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "sowerd"+tt.ext)
if err := os.WriteFile(path, []byte(tt.content), 0o644); err != nil {
t.Fatalf("write temp config: %v", err)
}
var cfg SowerdConfig
if err := loadConfigFileForTest(path, &cfg); err != nil {
t.Fatalf("load config file: %v", err)
}
if err := cfg.Validate(); err != nil {
t.Fatalf("validate config: %v", err)
}
if cfg.ServeIP != "0.0.0.0" {
t.Fatalf("unexpected serve ip: %q", cfg.ServeIP)
}
if cfg.FakeSite != "127.0.0.1:80" {
t.Fatalf("unexpected fake site: %q", cfg.FakeSite)
}
})
}
}
func loadConfigFileForTest(path string, dst any) error {
return aconfig.LoaderFor(dst, aconfig.Config{
SkipEnv: true,
SkipFlags: true,
AllowUnknownFields: false,
Files: []string{path},
FileDecoders: map[string]aconfig.FileDecoder{
".yaml": aconfigyaml.New(),
".toml": aconfigtoml.New(),
},
}).Load()
}
func mustReadFileForTest(t *testing.T, path string) string {
t.Helper()
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read file %s: %v", path, err)
}
return string(data)
}
+198
View File
@@ -0,0 +1,198 @@
package config
import (
"fmt"
"log/slog"
"net"
"strings"
"github.com/sower-proxy/sower/pkg/upstreamtls"
)
type RemoteTLSConfig struct {
ServerName string `usage:"override upstream TLS server name (SNI)"`
ClientHello string `usage:"uTLS client hello: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang"`
InsecureSkipVerify bool `default:"false" usage:"skip upstream TLS certificate verification"`
}
type RemoteConfig struct {
Type string `default:"sower" required:"true" usage:"option: sower/trojan/socks5"`
Addr string `required:"true" usage:"proxy address, eg: proxy.com or proxy.com:443"`
Password string `usage:"remote proxy password"`
TLS RemoteTLSConfig `flag:"tls"`
}
// SowerConfig represents the configuration for sower client
type SowerConfig struct {
LogLevel slog.Level `default:"info" usage:"log level: debug, info, warn, error"`
Remote RemoteConfig
DNS struct {
Disable bool `default:"false" usage:"disable DNS proxy"`
Serve string `usage:"dns server ip"`
Serve6 string `usage:"dns server ipv6, eg: ::1"`
ServeIface string `usage:"use the IP in the net interface, if serve ip not setted. eg: eth0"`
Upstream string `usage:"upstream dns server"`
Fallback string `default:"223.5.5.5" required:"true" usage:"fallback dns server"`
}
Socks5 struct {
Disable bool `default:"false" usage:"disable sock5 proxy"`
Addr string `default:"127.0.0.1:1080" usage:"socks5 listen address"`
} `flag:"socks5"`
Router struct {
Block struct {
File string `usage:"block list file, local file or remote"`
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
FileSkipRules []string `usage:"rules to skip when loading block list file"`
Rules []string `usage:"block list rules"`
}
Direct struct {
File string `usage:"direct list file, local file or remote"`
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
FileSkipRules []string `usage:"rules to skip when loading direct list file"`
Rules []string `usage:"direct list rules"`
}
Proxy struct {
File string `usage:"proxy list file, local file or remote"`
FilePrefix string `default:"**." usage:"parsed as '<prefix>line_text'"`
FileSkipRules []string `usage:"rules to skip when loading proxy list file"`
Rules []string `usage:"proxy list rules"`
}
Country struct {
MMDB string `usage:"mmdb file"`
File string `usage:"CIDR block list file, local file or remote"`
FilePrefix string `default:"" usage:"parsed as '<prefix>line_text'"`
Rules []string `usage:"CIDR list rules"`
}
}
}
// Validate implements the validation interface for SowerConfig
func (c *SowerConfig) Validate() error {
switch c.Remote.Type {
case "sower", "trojan", "socks5":
default:
return fmt.Errorf("unsupported remote type %q", c.Remote.Type)
}
remoteHost, err := validateRemoteAddr(c.Remote.Type, c.Remote.Addr)
if err != nil {
return err
}
if c.Remote.TLS.ClientHello != "" {
if err := upstreamtls.ValidateClientHello(c.Remote.TLS.ClientHello); err != nil {
return err
}
}
if c.DNS.ServeIface != "" {
iface, err := net.InterfaceByName(c.DNS.ServeIface)
if err != nil {
return fmt.Errorf("get interface: %w", err)
}
addrs, err := iface.Addrs()
if err != nil {
return fmt.Errorf("get interface addresses: %w", err)
}
for _, addr := range addrs {
ip, _, err := net.ParseCIDR(addr.String())
if err != nil {
return fmt.Errorf("parse interface IP: %w", err)
}
if ip.To4() != nil { // ipv4
if c.DNS.Serve == "" {
c.DNS.Serve = ip.String()
}
} else if ip.IsGlobalUnicast() { // ipv6 must be global unicast
if c.DNS.Serve6 == "" {
c.DNS.Serve6 = ip.String()
}
}
}
}
if err := validateOptionalIP("dns serve", c.DNS.Serve); err != nil {
return err
}
if err := validateOptionalIP("dns serve6", c.DNS.Serve6); err != nil {
return err
}
if err := validateOptionalIP("dns upstream", c.DNS.Upstream); err != nil {
return err
}
if err := validateRequiredIP("dns fallback", c.DNS.Fallback); err != nil {
return err
}
if !c.DNS.Disable && c.DNS.Serve == "" {
return fmt.Errorf("dns serve ip and serve interface not set")
}
if !c.Socks5.Disable {
if _, _, err := net.SplitHostPort(c.Socks5.Addr); err != nil {
return fmt.Errorf("invalid socks5 listen address %q: %w", c.Socks5.Addr, err)
}
}
c.Router.Direct.Rules = append(c.Router.Direct.Rules,
remoteHost, "**.in-addr.arpa", "**.ip6.arpa")
return nil
}
func validateRemoteAddr(remoteType, addr string) (string, error) {
switch remoteType {
case "socks5":
host, _, err := net.SplitHostPort(addr)
if err != nil {
return "", fmt.Errorf("invalid remote socks5 address %q: %w", addr, err)
}
if host == "" {
return "", fmt.Errorf("invalid remote socks5 address %q", addr)
}
return host, nil
default:
host, _, err := splitRemoteAddr(addr, "443")
if err != nil {
return "", fmt.Errorf("invalid remote tls address %q: %w", addr, err)
}
return host, nil
}
}
func splitRemoteAddr(addr, defaultPort string) (string, string, error) {
if addr == "" {
return "", "", fmt.Errorf("empty address")
}
if host, port, err := net.SplitHostPort(addr); err == nil {
if host == "" || port == "" {
return "", "", fmt.Errorf("invalid address %q", addr)
}
return host, port, nil
}
if strings.HasPrefix(addr, "[") || strings.HasSuffix(addr, "]") {
return "", "", fmt.Errorf("invalid address %q", addr)
}
if strings.Count(addr, ":") == 1 {
return "", "", fmt.Errorf("missing or invalid port in %q", addr)
}
return addr, defaultPort, nil
}
func validateOptionalIP(name, value string) error {
if value == "" {
return nil
}
return validateRequiredIP(name, value)
}
func validateRequiredIP(name, value string) error {
if net.ParseIP(value) == nil {
return fmt.Errorf("invalid %s %q", name, value)
}
return nil
}
+61
View File
@@ -0,0 +1,61 @@
# Sower configuration example (TOML format)
# This is the client configuration for the sower proxy
# Logging configuration
log_level = "info" # Log level: debug, info, warn, error
# Remote proxy configuration
[remote]
type = "sower" # Proxy type: sower, trojan, or socks5
addr = "proxy.example.com" # Proxy server address, optional port for TLS remotes
password = "your_secure_password" # Proxy password
[remote.tls]
server_name = "" # Override upstream TLS server name (SNI)
client_hello = "" # Optional uTLS fingerprint: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang
insecure_skip_verify = false # Skip upstream TLS certificate verification
# DNS configuration
[dns]
disable = false # Disable DNS proxy
serve = "127.0.0.1" # DNS server IP address
serve_6 = "" # DNS server IPv6 address (optional); aconfig maps Serve6 -> serve_6
serve_iface = "" # Network interface to get IP from (optional)
upstream = "8.8.8.8" # Upstream DNS server (optional)
fallback = "223.5.5.5" # Fallback DNS server
# SOCKS5 proxy configuration
# aconfig maps Socks5 -> socks_5 for file keys.
[socks_5]
disable = false # Disable SOCKS5 proxy
addr = "127.0.0.1:1080" # SOCKS5 listen address
# Router configuration
[router]
# Block list rules
[router.block]
file = "" # Block list file path (local or remote)
file_prefix = "**." # Prefix for block list rules
file_skip_rules = [] # Rules to skip when loading the block list file
rules = [] # Additional block rules
# Direct list rules
[router.direct]
file = "" # Direct list file path (local or remote)
file_prefix = "**." # Prefix for direct list rules
file_skip_rules = [] # Rules to skip when loading the direct list file
rules = [] # Additional direct rules
# Proxy list rules
[router.proxy]
file = "" # Proxy list file path (local or remote)
file_prefix = "**." # Prefix for proxy list rules
file_skip_rules = [] # Rules to skip when loading the proxy list file
rules = [] # Additional proxy rules
# Country-based routing
[router.country]
mmdb = "" # Optional MMDB file path for GeoIP; empty disables GeoIP lookup
file = "" # CIDR block list file path
file_prefix = "" # Prefix for CIDR rules
rules = [] # Additional CIDR rules
+59
View File
@@ -0,0 +1,59 @@
# Sower configuration example (YAML format)
# This is the client configuration for the sower proxy
# Logging configuration
log_level: "info" # Log level: debug, info, warn, error
# Remote proxy configuration
remote:
type: "sower" # Proxy type: sower, trojan, or socks5
addr: "proxy.example.com" # Proxy server address, optional port for TLS remotes
password: "your_secure_password" # Proxy password
tls:
server_name: "" # Override upstream TLS server name (SNI)
client_hello: "" # Optional uTLS fingerprint: chrome, firefox, ios, android, edge, safari, 360, qq, randomized, randomized_alpn, randomized_no_alpn, golang
insecure_skip_verify: false # Skip upstream TLS certificate verification
# DNS configuration
dns:
disable: false # Disable DNS proxy
serve: "127.0.0.1" # DNS server IP address
serve_6: "" # DNS server IPv6 address (optional)
serve_iface: "" # Network interface to get IP from (optional)
upstream: "8.8.8.8" # Upstream DNS server (optional)
fallback: "223.5.5.5" # Fallback DNS server
# SOCKS5 proxy configuration
socks_5:
disable: false # Disable SOCKS5 proxy
addr: "127.0.0.1:1080" # SOCKS5 listen address
# Router configuration
router:
# Block list rules
block:
file: "" # Block list file path (local or remote)
file_prefix: "**." # Prefix for block list rules
file_skip_rules: [] # Rules to skip when loading the block list file
rules: [] # Additional block rules
# Direct list rules
direct:
file: "" # Direct list file path (local or remote)
file_prefix: "**." # Prefix for direct list rules
file_skip_rules: [] # Rules to skip when loading the direct list file
rules: [] # Additional direct rules
# Proxy list rules
proxy:
file: "" # Proxy list file path (local or remote)
file_prefix: "**." # Prefix for proxy list rules
file_skip_rules: [] # Rules to skip when loading the proxy list file
rules: [] # Additional proxy rules
# Country-based routing
country:
mmdb: "" # Optional MMDB file path for GeoIP; empty disables GeoIP lookup
file: "" # CIDR block list file path
file_prefix: "" # Prefix for CIDR rules
rules: [] # Additional CIDR rules
+160
View File
@@ -0,0 +1,160 @@
package config
import (
"os"
"testing"
"github.com/cristalhq/aconfig"
"github.com/cristalhq/aconfig/aconfigtoml"
"github.com/cristalhq/aconfig/aconfigyaml"
)
func TestSowerConfigValidateRejectsInvalidRemoteType(t *testing.T) {
t.Parallel()
cfg := SowerConfig{}
cfg.Remote.Type = "invalid"
cfg.Remote.Addr = "example.com"
cfg.DNS.Disable = true
if err := cfg.Validate(); err == nil {
t.Fatal("expected validation error for invalid remote type")
}
}
func TestSowerConfigValidateRejectsInvalidSocks5Addr(t *testing.T) {
t.Parallel()
cfg := SowerConfig{}
cfg.Remote.Type = "sower"
cfg.Remote.Addr = "example.com"
cfg.DNS.Disable = true
cfg.Socks5.Addr = "127.0.0.1"
if err := cfg.Validate(); err == nil {
t.Fatal("expected validation error for invalid socks5 address")
}
}
func TestSowerConfigValidateRejectsInvalidFallbackIP(t *testing.T) {
t.Parallel()
cfg := SowerConfig{}
cfg.Remote.Type = "sower"
cfg.Remote.Addr = "example.com"
cfg.DNS.Disable = true
cfg.DNS.Fallback = "not-an-ip"
if err := cfg.Validate(); err == nil {
t.Fatal("expected validation error for invalid fallback IP")
}
}
func TestSowerConfigValidateAllowsTLSRemoteWithExplicitPort(t *testing.T) {
t.Parallel()
cfg := SowerConfig{}
cfg.Remote.Type = "sower"
cfg.Remote.Addr = "example.com:8443"
cfg.Remote.TLS.ClientHello = "chrome"
cfg.DNS.Disable = true
cfg.DNS.Fallback = "223.5.5.5"
cfg.Socks5.Disable = true
if err := cfg.Validate(); err != nil {
t.Fatalf("validate config: %v", err)
}
if got := cfg.Router.Direct.Rules[len(cfg.Router.Direct.Rules)-3]; got != "example.com" {
t.Fatalf("unexpected direct rule host: %q", got)
}
}
func TestSowerConfigValidateRejectsInvalidTLSClientHello(t *testing.T) {
t.Parallel()
cfg := SowerConfig{}
cfg.Remote.Type = "trojan"
cfg.Remote.Addr = "example.com"
cfg.Remote.TLS.ClientHello = "not-supported"
cfg.DNS.Disable = true
cfg.DNS.Fallback = "223.5.5.5"
cfg.Socks5.Disable = true
if err := cfg.Validate(); err == nil {
t.Fatal("expected validation error for invalid TLS client hello")
}
}
func TestSowerConfigLoadsTOMLFileSkipRules(t *testing.T) {
t.Parallel()
path := t.TempDir() + "/sower.toml"
if err := os.WriteFile(path, []byte(`
[remote]
type = "sower"
addr = "example.com"
[dns]
disable = true
fallback = "223.5.5.5"
[socks_5]
disable = true
[router.block]
file_skip_rules = ["t.co"]
`), 0o600); err != nil {
t.Fatalf("write config: %v", err)
}
var cfg SowerConfig
if err := aconfig.LoaderFor(&cfg, aconfig.Config{
SkipEnv: true,
SkipFlags: true,
Files: []string{path},
FileDecoders: map[string]aconfig.FileDecoder{
".toml": aconfigtoml.New(),
},
}).Load(); err != nil {
t.Fatalf("load config: %v", err)
}
if len(cfg.Router.Block.FileSkipRules) != 1 || cfg.Router.Block.FileSkipRules[0] != "t.co" {
t.Fatalf("unexpected file skip rules: %v", cfg.Router.Block.FileSkipRules)
}
if !cfg.Socks5.Disable {
t.Fatal("expected socks_5 section to load")
}
}
func TestSowerConfigLoadsPackagedExamples(t *testing.T) {
t.Parallel()
for _, path := range []string{
"../config/sower.toml",
"../config/sower.yaml",
"../.github/sower.toml",
"../.github/sower.yaml",
} {
path := path
t.Run(path, func(t *testing.T) {
t.Parallel()
var cfg SowerConfig
if err := aconfig.LoaderFor(&cfg, aconfig.Config{
SkipEnv: true,
SkipFlags: true,
Files: []string{path},
FileDecoders: map[string]aconfig.FileDecoder{
".toml": aconfigtoml.New(),
".yaml": aconfigyaml.New(),
},
}).Load(); err != nil {
t.Fatalf("load config: %v", err)
}
if err := cfg.Validate(); err != nil {
t.Fatalf("validate config: %v", err)
}
})
}
}
+78
View File
@@ -0,0 +1,78 @@
package config
import (
"fmt"
"log/slog"
"net"
"os"
)
const ExampleSowerdConfigTOML = `# Sowerd configuration example (TOML format)
# This is the daemon configuration for the sower server
# Logging configuration
log_level = "info" # Log level: debug, info, warn, error
# Server configuration
serve_ip = "0.0.0.0" # IP address to listen on for ports 80 and 443
password = "change_me" # Password for client authentication
fake_site = "/var/www" # Fake site directory or address (default: /var/www)
# SSL/TLS certificate configuration
[cert]
email = "your-email@example.com" # Email for Let's Encrypt certificate
cert = "" # Path to custom certificate file (optional)
key = "" # Path to custom private key file (optional)
`
// SowerdConfig represents the configuration for sowerd daemon
type SowerdConfig struct {
LogLevel slog.Level `default:"info" usage:"log level: debug, info, warn, error"`
ServeIP string `usage:"listen to port 80 443 of the IP"`
Password string `required:"true"`
FakeSite string `default:"/var/www" usage:"fake site address or directoy. serving on 127.0.0.1:80 if directory"`
Cert struct {
Email string
Cert string
Key string
}
}
// Validate implements the validation interface for SowerdConfig
func (c *SowerdConfig) Validate() error {
if c.ServeIP != "" && net.ParseIP(c.ServeIP) == nil {
return fmt.Errorf("invalid serve ip: %q", c.ServeIP)
}
if c.Password == "" {
return fmt.Errorf("password is required")
}
if c.FakeSite == "" {
return fmt.Errorf("fake site is required")
}
if (c.Cert.Cert == "") != (c.Cert.Key == "") {
return fmt.Errorf("cert and key must be configured together")
}
if c.Cert.Cert != "" {
if _, err := os.Stat(c.Cert.Cert); err != nil {
return fmt.Errorf("stat cert file: %w", err)
}
if _, err := os.Stat(c.Cert.Key); err != nil {
return fmt.Errorf("stat key file: %w", err)
}
}
if _, err := os.Stat(c.FakeSite); err == nil {
return nil
} else if !os.IsNotExist(err) {
return fmt.Errorf("stat fake site: %w", err)
}
if _, _, err := net.SplitHostPort(c.FakeSite); err != nil {
return fmt.Errorf("fake site must be an existing directory or host:port: %w", err)
}
return nil
}
+16
View File
@@ -0,0 +1,16 @@
# Sowerd configuration example (TOML format)
# This is the daemon configuration for the sower server
# Logging configuration
log_level = "info" # Log level: debug, info, warn, error
# Server configuration
serve_ip = "0.0.0.0" # IP address to listen on for ports 80 and 443
password = "change_me" # Password for client authentication
fake_site = "/var/www" # Fake site directory or address (default: /var/www)
# SSL/TLS certificate configuration
[cert]
email = "your-email@example.com" # Email for Let's Encrypt certificate
cert = "" # Path to custom certificate file (optional)
key = "" # Path to custom private key file (optional)
+16
View File
@@ -0,0 +1,16 @@
# Sowerd configuration example (YAML format)
# This is the daemon configuration for the sower server
# Logging configuration
log_level: "info" # Log level: debug, info, warn, error
# Server configuration
serve_ip: "0.0.0.0" # IP address to listen on for ports 80 and 443
password: "change_me" # Password for client authentication
fake_site: "/var/www" # Fake site directory or address (default: /var/www)
# SSL/TLS certificate configuration
cert:
email: "your-email@example.com" # Email for Let's Encrypt certificate
cert: "" # Path to custom certificate file (optional)
key: "" # Path to custom private key file (optional)
+76
View File
@@ -0,0 +1,76 @@
package config
import "testing"
func TestSowerdConfigValidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cfg SowerdConfig
wantErr bool
}{
{
name: "valid remote fake site",
cfg: SowerdConfig{
ServeIP: "0.0.0.0",
Password: "secret",
FakeSite: "127.0.0.1:8080",
},
},
{
name: "invalid serve ip",
cfg: SowerdConfig{
ServeIP: "bad-ip",
Password: "secret",
FakeSite: "127.0.0.1:8080",
},
wantErr: true,
},
{
name: "missing password",
cfg: SowerdConfig{
FakeSite: "127.0.0.1:8080",
},
wantErr: true,
},
{
name: "invalid fake site address",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "not-an-address",
},
wantErr: true,
},
{
name: "partial cert config",
cfg: SowerdConfig{
Password: "secret",
FakeSite: "127.0.0.1:8080",
Cert: struct {
Email string
Cert string
Key string
}{
Cert: "/tmp/cert.pem",
},
},
wantErr: true,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
err := tt.cfg.Validate()
if tt.wantErr && err == nil {
t.Fatal("expected error")
}
if !tt.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
-18
View File
@@ -1,18 +0,0 @@
# Compile
FROM golang:1.11-alpine AS compiler
RUN apk add --no-cache git make
# enable go modules
WORKDIR /go-mod
COPY . .
# do not worry about downloading dependency, sower will fix this.
RUN CGO_ENABLED=0 make build
# Build image
FROM scratch
COPY --from=compiler /go-mod/sower /sower
ENTRYPOINT [ "/sower" ]
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>cc.wweir.sower</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/sower</string>
<string>-f</string>
<string>/usr/local/etc/sower.toml</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
-92
View File
@@ -1,92 +0,0 @@
#!/usr/bin/env bash
set -u -e
[[ -d /tmp/sower ]] || mkdir /tmp/sower
cd /tmp/sower
echo ======== Install sower to your system ========
succ_message(){
echo
echo ======== Installed sower to your system ========
echo The config file is: /usr/local/etc/sower.toml
echo please set 127.0.0.1 as the first domain name server manually.
}
# main logic
VERSION="$(curl -s "https://api.github.com/repos/wweir/sower/releases/latest" | awk -F '"' '/tag_name/{printf $4}')"
case "$(uname -s)" in
"Darwin")
printf "Please enter remote server adddress: "
read ADDRESS
echo
curl -SLf https://github.com/wweir/sower/releases/download/$VERSION/sower_darwin_amd64.tar.gz | tar xzv
mv sower /usr/local/bin/
if [[ -r /usr/local/etc/sower.toml ]]; then
echo The config file already exists, keep the original file
else
sed -i~ "s/# server_addr=\"remote-server/server_addr=\"$ADDRESS/" sower.toml
sed -i~ "s/# clear_dns_cache/clear_dns_cache/" sower.toml
mv sower.toml /usr/local/etc/
fi
echo
echo Register auto start service, root privilege is needed!
echo
sudo mv cc.wweir.sower.plist /Library/LaunchDaemons/
sudo chown root:wheel /Library/LaunchDaemons/cc.wweir.sower.plist
sudo launchctl load -w /Library/LaunchDaemons/cc.wweir.sower.plist
succ_message
;;
"Linux")
if [[ "$(cat /proc/1/comm)" != "systemd" ]]; then
echo do not support auto deploy on SysVinit
exit 1
fi
printf "Server side or client side, which do you wanna install [c/s]: "
read SIDE
echo
case "$SIDE" in
"c")
printf "Please enter remote server adddress: "
read ADDRESS
echo
curl -SLf https://github.com/wweir/sower/releases/download/$VERSION/sower_linux_amd64.tar.gz | tar xzv
sudo mv sower /usr/local/bin/
if [[ -r /usr/local/etc/sower.toml ]]; then
echo The config file already exists, keep the original file
else
sed -i~ "s/# server_addr=\"remote-server/server_addr=\"$ADDRESS/" sower.toml
sudo mv sower.toml /usr/local/etc/
fi
sudo mv sower-client.service /etc/systemd/system/
sudo systemctl enable sower-client
sudo systemctl start sower-client
succ_message
;;
"s")
curl -SLf https://github.com/wweir/sower/releases/download/$VERSION/sower_linux_amd64.tar.gz | tar xzv
sudo mv sower /usr/local/bin/
sudo mv sower-server.service /etc/systemd/system/
sudo systemctl enable sower-server
sudo systemctl start sower-server
;;
*)
echo "invalid: $SIDE" && exit 1
;;
esac
;;
*)
echo Do not support auto deploy on this platform.
;;
esac
-14
View File
@@ -1,14 +0,0 @@
[Unit]
Description=Sower client service
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
User=root
WorkingDirectory=/tmp
ExecStart=/usr/local/bin/sower -f /usr/local/etc/sower.toml
RestartSec=3
Restart=on-failure
-14
View File
@@ -1,14 +0,0 @@
[Unit]
Description=Sower server service
After=network.target
[Install]
WantedBy=multi-user.target
[Service]
Type=simple
User=nobody
WorkingDirectory=/tmp
ExecStart=/usr/local/bin/sower -n TCP -v 1
RestartSec=3
Restart=on-failure
-20
View File
@@ -1,20 +0,0 @@
#!/usr/bin/env bash
set -u -x -e
# main logic
case "$(uname -s)" in
"Darwin")
sudo launchctl unload /Library/LaunchDaemons/cc.wweir.sower.plist
sudo rm -rf /Library/LaunchDaemons/cc.wweir.sower.plist
rm -rf /usr/local/bin/sower /usr/local/etc/sower.toml
;;
"Linux")
sudo systemctl disable sower-client ||true
sudo systemctl disable sower-server || true
sudo rm -rf /etc/systemd/system/sower-client.service /etc/systemd/system/sower-server.service
sudo rm -rf /usr/local/bin/sower /usr/local/etc/sower.toml
;;
*)
echo Do not support auto deploy on this platform.
;;
esac
-153
View File
@@ -1,153 +0,0 @@
package dns
import (
"net"
"strings"
"sync/atomic"
"time"
"github.com/golang/glog"
"github.com/miekg/dns"
mem "github.com/wweir/mem-go"
"github.com/wweir/sower/conf"
"github.com/wweir/sower/util"
)
const colon = byte(':')
func StartDNS(dnsServer, listenIP string) {
ip := net.ParseIP(listenIP)
suggest := &intelliSuggest{listenIP, time.Second}
mem.DefaultCache = mem.New(time.Hour)
dhcpCh := make(chan struct{})
if dnsServer != "" {
dnsServer = net.JoinHostPort(dnsServer, "53")
} else {
go func() {
for {
<-dhcpCh
host := util.GetDefaultDNSServer()
if host == "" {
continue
}
// atomic action
dnsServer = net.JoinHostPort(host, "53")
glog.Infoln("set dns server to", host)
}
}()
dhcpCh <- struct{}{}
}
dns.HandleFunc(".", func(w dns.ResponseWriter, r *dns.Msg) {
// *Msg r has an TSIG record and it was validated
if r.IsTsig() != nil && w.TsigStatus() == nil {
lastTsig := r.Extra[len(r.Extra)-1].(*dns.TSIG)
r.SetTsig(lastTsig.Hdr.Name, dns.HmacMD5, 300, time.Now().Unix())
}
//https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071
if len(r.Question) == 0 {
return
}
domain := r.Question[0].Name
if idx := strings.IndexByte(domain, colon); idx > 0 {
domain = domain[:idx]
}
matchAndServe(w, r, domain, listenIP, dnsServer, dhcpCh, ip, suggest)
})
server := &dns.Server{Addr: net.JoinHostPort(listenIP, "53"), Net: "udp"}
glog.Fatalln(server.ListenAndServe())
}
func matchAndServe(w dns.ResponseWriter, r *dns.Msg, domain, listenIP, dnsServer string,
dhcpCh chan struct{}, ipNet net.IP, suggest *intelliSuggest) {
inWriteList := whiteList.Match(domain)
if !inWriteList && (blockList.Match(domain) || suggestList.Match(domain)) {
glog.V(2).Infof("match %s suss", domain)
w.WriteMsg(localA(r, domain, ipNet))
return
}
if !inWriteList {
go mem.Remember(suggest, domain)
}
msg, err := dns.Exchange(r, dnsServer)
if err != nil && dhcpCh != nil {
select {
case dhcpCh <- struct{}{}:
default:
}
}
if msg == nil { // expose any response except nil
glog.V(1).Infof("get dns of %s fail: %s", domain, err)
return
}
w.WriteMsg(msg)
}
type intelliSuggest struct {
listenIP string
timeout time.Duration
}
func (i *intelliSuggest) GetOne(domain interface{}) (iface interface{}, e error) {
iface, e = struct{}{}, nil
// kill deadloop, for ugly wildcard setting dns setting
addr := strings.TrimSuffix(domain.(string), ".")
if len(strings.Split(addr, ".")) > 10 {
return
}
// give local dial a hand, make it not so easy to be added into suggestions
util.HTTPPing(addr, addr, util.Http, i.timeout/50)
var (
pings = []struct {
viaAddr string
port util.Port
}{
{addr, util.Http},
{addr, util.Https},
{i.listenIP, util.Http},
{i.listenIP, util.Https},
}
half = int32(len(pings) / 2)
score = new(int32)
)
for idx := range pings {
go func(idx int) {
err := util.HTTPPing(pings[idx].viaAddr, addr, pings[idx].port, i.timeout)
if err != nil {
// local ping fail
if pings[idx].viaAddr == addr {
atomic.AddInt32(score, 1)
}
} else {
// remote ping succ
if pings[idx].viaAddr == i.listenIP {
atomic.AddInt32(score, 1)
// local ping took longger than remote
} else if atomic.LoadInt32(score) >= half {
atomic.AddInt32(score, 1)
}
}
if atomic.LoadInt32(score) > half {
atomic.StoreInt32(score, 0) // avoid redo add action
conf.AddSuggest(addr)
glog.Infof("added suggest domain: %s", addr)
}
}(idx)
}
return
}
-57
View File
@@ -1,57 +0,0 @@
package dns
import (
"net"
"github.com/golang/glog"
"github.com/miekg/dns"
"github.com/wweir/sower/conf"
"github.com/wweir/sower/util"
)
var (
blockList *util.Node
whiteList *util.Node
suggestList *util.Node
)
func init() {
host, _, _ := net.SplitHostPort(conf.Conf.ServerAddr)
//first init
blockList = loadRules("block", conf.Conf.BlockList)
suggestList = loadRules("suggest", conf.Conf.Suggestions)
whiteList = loadRules("white", conf.Conf.WhiteList)
whiteList.Add(host)
conf.OnRefreash = append(conf.OnRefreash, func() error {
blockList = loadRules("block", conf.Conf.BlockList)
suggestList = loadRules("suggest", conf.Conf.Suggestions)
whiteList = loadRules("white", conf.Conf.WhiteList)
whiteList.Add(host)
return nil
})
}
func loadRules(name string, list []string) *util.Node {
rule := util.NewNodeFromRules(".", list...)
glog.V(3).Infof("load %s rule:\n%s", name, rule)
return rule
}
func localA(r *dns.Msg, domain string, localIP net.IP) *dns.Msg {
m := new(dns.Msg)
m.SetReply(r)
if localIP.To4() != nil {
m.Answer = []dns.RR{&dns.A{
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
A: localIP,
}}
} else {
m.Answer = []dns.RR{&dns.AAAA{
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 20},
AAAA: localIP,
}}
}
return m
}
+31 -24
View File
@@ -1,27 +1,34 @@
module github.com/wweir/sower
module github.com/sower-proxy/sower
go 1.26.1
require (
github.com/bifurcation/mint v0.0.0-20181105071958-a14404e9a861 // indirect
github.com/cheekybits/genny v1.0.0 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/hashicorp/golang-lru v0.5.0 // indirect
github.com/klauspost/cpuid v1.2.0 // indirect
github.com/klauspost/reedsolomon v1.9.0 // indirect
github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f // indirect
github.com/lucas-clemente/quic-go v0.10.1
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced // indirect
github.com/miekg/dns v1.1.4
github.com/pelletier/go-toml v1.2.0
github.com/pkg/errors v0.8.1
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect
github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b // indirect
github.com/tjfoc/gmsm v1.0.1 // indirect
github.com/ulule/deepcopier v0.0.0-20171107155558-ca99b135e50f // indirect
github.com/wweir/fsnotify v1.4.8
github.com/wweir/mem-go v0.0.0-20190109100331-8673ab596296
github.com/wweir/netboot v0.0.2
github.com/xtaci/kcp-go v5.0.7+incompatible
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd // indirect
golang.org/x/sys v0.0.0-20190213121743-983097b1a8a3 // indirect
github.com/cristalhq/aconfig v0.19.0
github.com/cristalhq/aconfig/aconfigtoml v0.17.1
github.com/cristalhq/aconfig/aconfigyaml v0.17.1
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771
github.com/libp2p/go-reuseport v0.4.0
github.com/lmittmann/tint v1.1.3
github.com/miekg/dns v1.1.72
github.com/oschwald/geoip2-golang v1.13.0
github.com/refraction-networking/utls v1.8.2
github.com/sower-proxy/conns v0.1.1
github.com/sower-proxy/deferlog/v2 v2.1.4
github.com/sower-proxy/mem v0.0.3
golang.org/x/crypto v0.49.0
)
require (
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/andybalholm/brotli v1.0.6 // indirect
github.com/klauspost/compress v1.17.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/oschwald/maxminddb-golang v1.13.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.42.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+61 -46
View File
@@ -1,46 +1,61 @@
github.com/bifurcation/mint v0.0.0-20181105071958-a14404e9a861 h1:x17NvoJaphEzay72TFej4OSSsgu3xRYBLkbIwdofS/4=
github.com/bifurcation/mint v0.0.0-20181105071958-a14404e9a861/go.mod h1:zVt7zX3K/aDCk9Tj+VM7YymsX66ERvzCJzw8rFCX2JU=
github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/klauspost/cpuid v1.2.0 h1:NMpwD2G9JSFOE1/TJjGSo5zG7Yb2bTe7eq1jH+irmeE=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/reedsolomon v1.9.0 h1:usyTY5K7D2B6WOHn2jmpB7ky8Qom96mShZmmq3OW4JU=
github.com/klauspost/reedsolomon v1.9.0/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4=
github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f h1:sSeNEkJrs+0F9TUau0CgWTTNEwF23HST3Eq0A+QIx+A=
github.com/lucas-clemente/aes12 v0.0.0-20171027163421-cd47fb39b79f/go.mod h1:JpH9J1c9oX6otFSgdUHwUBUizmKlrMjxWnIAjff4m04=
github.com/lucas-clemente/quic-go v0.10.1 h1:ipcMmYP9RT+b1YytOKGUY1qndxPGOczVEQkAVz3CZrs=
github.com/lucas-clemente/quic-go v0.10.1/go.mod h1:wuD+2XqEx8G9jtwx5ou2BEYBsE+whgQmlj0Vz/77PrY=
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced h1:zqEC1GJZFbGZA0tRyNZqRjep92K5fujFtFsu5ZW7Aug=
github.com/lucas-clemente/quic-go-certificates v0.0.0-20160823095156-d2f86524cced/go.mod h1:NCcRLrOTZbzhZvixZLlERbJtDtYsmMw8Jc4vS8Z0g58=
github.com/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0=
github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU=
github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU=
github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b h1:mnG1fcsIB1d/3vbkBak2MM0u+vhGhlQwpeimUi7QncM=
github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4=
github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU=
github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc=
github.com/ulule/deepcopier v0.0.0-20171107155558-ca99b135e50f h1:QatZ4lsJBY3x1+Imst9g95+vUl7m52dqM9Pi4aSMW8w=
github.com/ulule/deepcopier v0.0.0-20171107155558-ca99b135e50f/go.mod h1:BNLmYJ8oMJPIPpNx5968jCyUhwEU1XT3YsuOqtbo5qo=
github.com/wweir/fsnotify v1.4.8 h1:zxOoqwE5p91F3rmcYLQheoSeTlIVqwi2b5T7UlpElWE=
github.com/wweir/fsnotify v1.4.8/go.mod h1:4GuxIc23DXfJI1/LhBXG5eE/wJ4CzL+kk5M7woxVprc=
github.com/wweir/mem-go v0.0.0-20190109100331-8673ab596296 h1:/HkUfg+ZMx/tNdnyJdVlhyv+xO3A7ZlpfL9nFLWLYcc=
github.com/wweir/mem-go v0.0.0-20190109100331-8673ab596296/go.mod h1:k7rjBGWoJ+JKwvfe8juAX0zgybjo/Yo3JGkca5f/06s=
github.com/wweir/netboot v0.0.2 h1:DFuNd97kxzgblExEKn4Nqf1du0m9qUnaTQ70rFgIjLU=
github.com/wweir/netboot v0.0.2/go.mod h1:p/WOnPHdqwgAI6YLEfdmwjwutbc/zDZ7EojAHlXLi4s=
github.com/xtaci/kcp-go v5.0.7+incompatible h1:zs9tc8XRID0m+aetu3qPWZFyRt2UIMqbXIBgw+vcnlE=
github.com/xtaci/kcp-go v5.0.7+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd h1:HuTn7WObtcDo9uEEU7rEqL0jYthdXAmZ6PP+meazmaU=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sys v0.0.0-20190213121743-983097b1a8a3 h1:+KlxhGbYkFs8lMfwKn+2ojry1ID5eBSMXprS2u/wqCE=
golang.org/x/sys v0.0.0-20190213121743-983097b1a8a3/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/cristalhq/aconfig v0.17.0/go.mod h1:NXaRp+1e6bkO4dJn+wZ71xyaihMDYPtCSvEhMTm/H3E=
github.com/cristalhq/aconfig v0.19.0 h1:fAo9ZObtzboHnf+5eAoMfb9KTDU5G/ij8OYO2wbpmM0=
github.com/cristalhq/aconfig v0.19.0/go.mod h1:9ogrGEt9yU5V4pif/ThkVUfhj8JkdV+iDeahZGgfnDU=
github.com/cristalhq/aconfig/aconfigtoml v0.17.1 h1:TA3xH8mALD8YeULsr4v87cMbKGBma35lWeRga4Xn11Q=
github.com/cristalhq/aconfig/aconfigtoml v0.17.1/go.mod h1:xt4kCEjhgvHWO/oDGJHQHrW5CnvSEoBjJhRXVBdYbhQ=
github.com/cristalhq/aconfig/aconfigyaml v0.17.1 h1:xCCbRKVmKrft9gQj3gHOq6U5PduasvlXEIsxtyzmFZ0=
github.com/cristalhq/aconfig/aconfigyaml v0.17.1/go.mod h1:5DTsjHkvQ6hfbyxfG32roB1lF0U82rROtFaLxibL8V8=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771 h1:t2c2B9g1ZVhMYduqmANSEGVD3/1WlsrEYNPtVoFlENk=
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s=
github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU=
github.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=
github.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/oschwald/geoip2-golang v1.13.0 h1:Q44/Ldc703pasJeP5V9+aFSZFmBN7DKHbNsSFzQATJI=
github.com/oschwald/geoip2-golang v1.13.0/go.mod h1:P9zG+54KPEFOliZ29i7SeYZ/GM6tfEL+rgSn03hYuUo=
github.com/oschwald/maxminddb-golang v1.13.0 h1:R8xBorY71s84yO06NgTmQvqvTvlS/bnYZrrWX1MElnU=
github.com/oschwald/maxminddb-golang v1.13.0/go.mod h1:BU0z8BfFVhi1LQaonTwwGQlsHUEu9pWNdMfmq4ztm0o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo=
github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/sower-proxy/conns v0.1.1 h1:9VPVW+sFc/y/IvXbUQTbXNSPF3I+1IG5mbrQTt5bdTE=
github.com/sower-proxy/conns v0.1.1/go.mod h1:p3ZM/8MolzW4TN0LaiPJnwnhhif3y9sBi2vQEWHvwhg=
github.com/sower-proxy/deferlog/v2 v2.1.4 h1:Vo/iVWGw3Su625RP2RIy4mE5FlfRdfrgP7kwOWNOyec=
github.com/sower-proxy/deferlog/v2 v2.1.4/go.mod h1:mdLrL/bxiTNhxeRLtRgKAecQddugI0iJ0ArC3yDwVSw=
github.com/sower-proxy/mem v0.0.3 h1:JK86etQ2QxRUXhs9JhoKYz5p3pFBSrI3UXVhwm44OzM=
github.com/sower-proxy/mem v0.0.3/go.mod h1:tM3P+gCAZMVBnOixGVAHS34si9Iwj2HU6M4IVEv4rK0=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+204
View File
@@ -0,0 +1,204 @@
package install
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/sower-proxy/sower/config"
)
type ConfirmFunc func(label string) bool
type installOptions struct {
confirm ConfirmFunc
geteuid func() int
executable func() (string, error)
evalSymlinks func(string) (string, error)
serviceIsActive func() bool
systemctl func(args ...string) error
targetBinaryPath string
servicePath string
configPath string
defaultFakeSite string
defaultConfigTOML string
}
func InstallService(confirm ConfirmFunc) error {
return installService(installOptions{
confirm: confirm,
geteuid: os.Geteuid,
executable: os.Executable,
evalSymlinks: filepath.EvalSymlinks,
serviceIsActive: serviceIsActive,
systemctl: systemctl,
targetBinaryPath: "/usr/local/bin/sowerd",
servicePath: "/etc/systemd/system/sowerd.service",
configPath: "/etc/sower/sowerd.toml",
defaultFakeSite: "/var/www",
defaultConfigTOML: config.ExampleSowerdConfigTOML,
})
}
func installService(opts installOptions) error {
if opts.geteuid() != 0 {
return fmt.Errorf("installing systemd service requires root privileges, try running with sudo")
}
execPath, err := opts.executable()
if err != nil {
return fmt.Errorf("get executable path: %w", err)
}
execPath, err = opts.evalSymlinks(execPath)
if err != nil {
return fmt.Errorf("resolve executable symlink: %w", err)
}
isUpdate := opts.serviceIsActive()
if execPath != opts.targetBinaryPath {
label := fmt.Sprintf("Copy binary to %s?", opts.targetBinaryPath)
if isUpdate {
label = fmt.Sprintf("Update binary at %s?", opts.targetBinaryPath)
}
if opts.confirm != nil && opts.confirm(label) {
if err := copyFile(execPath, opts.targetBinaryPath, 0o755); err != nil {
return err
}
if isUpdate {
_ = opts.systemctl("stop", "sowerd")
}
fmt.Printf(" OK binary copied to %s\n", opts.targetBinaryPath)
execPath = opts.targetBinaryPath
}
}
if err := writeServiceFile(opts.servicePath, execPath, opts.configPath); err != nil {
return err
}
fmt.Printf(" OK service file installed: %s\n", opts.servicePath)
if err := ensureDefaultFiles(opts.configPath, opts.defaultFakeSite, opts.defaultConfigTOML); err != nil {
return err
}
if err := opts.systemctl("daemon-reload"); err != nil {
return err
}
fmt.Println(" OK systemd daemon reloaded")
if isUpdate {
return finishUpdate(opts.confirm, opts.systemctl)
}
return finishInstall(opts.confirm, opts.systemctl)
}
func finishInstall(confirm ConfirmFunc, systemctlFn func(args ...string) error) error {
if confirm == nil || !confirm("Start and enable sowerd service now?") {
fmt.Println()
fmt.Println("sowerd service installed. Next steps:")
fmt.Println(" 1. Edit /etc/sower/sowerd.toml")
fmt.Println(" 2. sudo systemctl start sowerd")
fmt.Println(" 3. sudo systemctl enable sowerd")
return nil
}
if err := systemctlFn("enable", "sowerd"); err != nil {
return err
}
if err := systemctlFn("start", "sowerd"); err != nil {
return err
}
fmt.Println(" OK service enabled and started")
return nil
}
func finishUpdate(confirm ConfirmFunc, systemctlFn func(args ...string) error) error {
if confirm == nil || !confirm("Restart sowerd service now?") {
fmt.Println()
fmt.Println("sowerd service updated. To apply:")
fmt.Println(" sudo systemctl restart sowerd")
return nil
}
if err := systemctlFn("restart", "sowerd"); err != nil {
return err
}
fmt.Println(" OK service restarted")
return nil
}
func serviceIsActive() bool {
cmd := exec.Command("systemctl", "is-active", "--quiet", "sowerd")
return cmd.Run() == nil
}
func systemctl(args ...string) error {
cmd := exec.Command("systemctl", args...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("systemctl %s: %w - %s", strings.Join(args, " "), err, strings.TrimSpace(string(output)))
}
return nil
}
func copyFile(src, dst string, mode os.FileMode) error {
data, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("read binary %s: %w", src, err)
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return fmt.Errorf("create binary directory: %w", err)
}
if err := os.WriteFile(dst, data, mode); err != nil {
return fmt.Errorf("write binary to %s: %w", dst, err)
}
return nil
}
func writeServiceFile(servicePath, execPath, configPath string) error {
if err := os.MkdirAll(filepath.Dir(servicePath), 0o755); err != nil {
return fmt.Errorf("create service directory: %w", err)
}
serviceContent := fmt.Sprintf(`[Unit]
Description=sower server service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=%s -c %s
Restart=always
RestartSec=5s
[Install]
WantedBy=multi-user.target
`, execPath, configPath)
if err := os.WriteFile(servicePath, []byte(serviceContent), 0o644); err != nil {
return fmt.Errorf("write service file: %w", err)
}
return nil
}
func ensureDefaultFiles(configPath, fakeSiteDir, defaultConfigTOML string) error {
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
return fmt.Errorf("create config directory: %w", err)
}
if err := os.MkdirAll(fakeSiteDir, 0o755); err != nil {
return fmt.Errorf("create fake site directory: %w", err)
}
if _, err := os.Stat(configPath); err == nil {
return nil
} else if !os.IsNotExist(err) {
return fmt.Errorf("stat default config path: %w", err)
}
if err := os.WriteFile(configPath, []byte(defaultConfigTOML), 0o644); err != nil {
return fmt.Errorf("write default config: %w", err)
}
fmt.Printf(" OK default config written to %s\n", configPath)
return nil
}
+177
View File
@@ -0,0 +1,177 @@
package install
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
func TestInstallServiceRequiresRoot(t *testing.T) {
t.Parallel()
err := installService(installOptions{
geteuid: func() int { return 1000 },
})
if err == nil || !strings.Contains(err.Error(), "root privileges") {
t.Fatalf("installService() error = %v, want root privileges error", err)
}
}
func TestInstallServiceFreshInstall(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
execPath := filepath.Join(tmpDir, "sowerd")
if err := os.WriteFile(execPath, []byte("binary"), 0o755); err != nil {
t.Fatalf("write executable: %v", err)
}
targetBinaryPath := filepath.Join(tmpDir, "usr", "local", "bin", "sowerd")
servicePath := filepath.Join(tmpDir, "etc", "systemd", "system", "sowerd.service")
configPath := filepath.Join(tmpDir, "etc", "sower", "sowerd.toml")
fakeSiteDir := filepath.Join(tmpDir, "var", "www")
var prompts []string
var systemctlCalls []string
confirmAnswers := map[string]bool{
"Copy binary to " + targetBinaryPath + "?": true,
"Start and enable sowerd service now?": true,
}
err := installService(installOptions{
confirm: func(label string) bool {
prompts = append(prompts, label)
return confirmAnswers[label]
},
geteuid: func() int { return 0 },
executable: func() (string, error) { return execPath, nil },
evalSymlinks: func(path string) (string, error) { return path, nil },
serviceIsActive: func() bool { return false },
systemctl: func(args ...string) error {
systemctlCalls = append(systemctlCalls, strings.Join(args, " "))
return nil
},
targetBinaryPath: targetBinaryPath,
servicePath: servicePath,
configPath: configPath,
defaultFakeSite: fakeSiteDir,
defaultConfigTOML: `password = "change_me"` + "\n" + `fakeSite = "/var/www"` + "\n",
})
if err != nil {
t.Fatalf("installService() error = %v", err)
}
gotBinary, err := os.ReadFile(targetBinaryPath)
if err != nil {
t.Fatalf("read target binary: %v", err)
}
if string(gotBinary) != "binary" {
t.Fatalf("target binary = %q, want %q", string(gotBinary), "binary")
}
serviceContent, err := os.ReadFile(servicePath)
if err != nil {
t.Fatalf("read service file: %v", err)
}
serviceText := string(serviceContent)
if !strings.Contains(serviceText, "ExecStart="+targetBinaryPath+" -c "+configPath) {
t.Fatalf("service file missing ExecStart, content = %s", serviceText)
}
configContent, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("read config file: %v", err)
}
if string(configContent) == "" {
t.Fatal("config file is empty")
}
if info, err := os.Stat(fakeSiteDir); err != nil || !info.IsDir() {
t.Fatalf("fake site dir stat err = %v, isDir = %v", err, err == nil && info.IsDir())
}
wantPrompts := []string{
"Copy binary to " + targetBinaryPath + "?",
"Start and enable sowerd service now?",
}
if !reflect.DeepEqual(prompts, wantPrompts) {
t.Fatalf("prompts = %#v, want %#v", prompts, wantPrompts)
}
wantSystemctlCalls := []string{
"daemon-reload",
"enable sowerd",
"start sowerd",
}
if !reflect.DeepEqual(systemctlCalls, wantSystemctlCalls) {
t.Fatalf("systemctl calls = %#v, want %#v", systemctlCalls, wantSystemctlCalls)
}
}
func TestInstallServiceUpdateWithoutRestart(t *testing.T) {
t.Parallel()
tmpDir := t.TempDir()
targetBinaryPath := filepath.Join(tmpDir, "usr", "local", "bin", "sowerd")
if err := os.MkdirAll(filepath.Dir(targetBinaryPath), 0o755); err != nil {
t.Fatalf("mkdir target binary dir: %v", err)
}
if err := os.WriteFile(targetBinaryPath, []byte("binary"), 0o755); err != nil {
t.Fatalf("write target binary: %v", err)
}
servicePath := filepath.Join(tmpDir, "etc", "systemd", "system", "sowerd.service")
configPath := filepath.Join(tmpDir, "etc", "sower", "sowerd.toml")
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
t.Fatalf("mkdir config dir: %v", err)
}
if err := os.WriteFile(configPath, []byte("password = \"existing\"\n"), 0o644); err != nil {
t.Fatalf("write existing config: %v", err)
}
var prompts []string
var systemctlCalls []string
err := installService(installOptions{
confirm: func(label string) bool {
prompts = append(prompts, label)
return false
},
geteuid: func() int { return 0 },
executable: func() (string, error) { return targetBinaryPath, nil },
evalSymlinks: func(path string) (string, error) { return path, nil },
serviceIsActive: func() bool { return true },
systemctl: func(args ...string) error {
systemctlCalls = append(systemctlCalls, strings.Join(args, " "))
return nil
},
targetBinaryPath: targetBinaryPath,
servicePath: servicePath,
configPath: configPath,
defaultFakeSite: filepath.Join(tmpDir, "var", "www"),
defaultConfigTOML: "password = \"change_me\"\n",
})
if err != nil {
t.Fatalf("installService() error = %v", err)
}
configContent, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("read config file: %v", err)
}
if string(configContent) != "password = \"existing\"\n" {
t.Fatalf("config content = %q, want existing config to be preserved", string(configContent))
}
wantPrompts := []string{"Restart sowerd service now?"}
if !reflect.DeepEqual(prompts, wantPrompts) {
t.Fatalf("prompts = %#v, want %#v", prompts, wantPrompts)
}
wantSystemctlCalls := []string{"daemon-reload"}
if !reflect.DeepEqual(systemctlCalls, wantSystemctlCalls) {
t.Fatalf("systemctl calls = %#v, want %#v", systemctlCalls, wantSystemctlCalls)
}
}
-28
View File
@@ -1,28 +0,0 @@
package main
import (
"github.com/golang/glog"
"github.com/wweir/sower/conf"
"github.com/wweir/sower/dns"
"github.com/wweir/sower/proxy"
)
var version, date string
func main() {
conf := conf.Conf
glog.Infof("Starting sower(%s %s): %v", version, date, conf)
if conf.ServerAddr == "" {
proxy.StartServer(conf.NetType, conf.ServerPort, conf.Cipher, conf.Password)
} else {
if conf.HTTPProxy != "" {
go proxy.StartHttpProxy(conf.NetType, conf.ServerAddr,
conf.Cipher, conf.Password, conf.HTTPProxy)
}
go dns.StartDNS(conf.DNSServer, conf.ClientIP)
proxy.StartClient(conf.NetType, conf.ServerAddr, conf.Cipher, conf.Password, conf.ClientIP)
}
}
-48
View File
@@ -1,48 +0,0 @@
package parse
import (
"bufio"
"fmt"
"io"
"net"
"net/http"
"strings"
"github.com/wweir/sower/util"
)
func ParseAddr(conn net.Conn) (teeConn *util.TeeConn, addr string, err error) {
teeConn = &util.TeeConn{Conn: conn}
teeConn.StartOrReset()
defer teeConn.Stop()
buf := make([]byte, 1)
if n, err := teeConn.Read(buf); err != nil {
return teeConn, "", fmt.Errorf("Read conn fail: %v, readed: %v", err, buf[:n])
}
teeConn.StartOrReset()
// https
if buf[0] == 0x16 { // SSL handleshake
host, _, err := extractSNI(io.Reader(teeConn))
if err != nil {
return teeConn, "", err
}
if strings.Contains(host, ":") {
return teeConn, host, nil
}
return teeConn, host + ":443", nil
}
// http
b := bufio.NewReader(teeConn)
resp, err := http.ReadRequest(b)
if err != nil {
return teeConn, "", err
}
if strings.Contains(resp.Host, ":") {
return teeConn, resp.Host, nil
}
return teeConn, resp.Host + ":80", nil
}
-232
View File
@@ -1,232 +0,0 @@
// Copyright 2016 Google Inc.
//
// 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.
package parse
import (
"encoding/binary"
"errors"
"fmt"
"io"
)
func extractSNI(r io.Reader) (string, int, error) {
handshake, tlsver, err := handshakeRecord(r)
if err != nil {
return "", 0, fmt.Errorf("reading TLS record: %s", err)
}
sni, err := parseHello(handshake)
if err != nil {
return "", 0, fmt.Errorf("reading ClientHello: %s", err)
}
if len(sni) == 0 {
// ClientHello did not present an SNI extension. Valid packet,
// no hostname.
return "", tlsver, nil
}
hostname, err := parseSNI(sni)
if err != nil {
return "", 0, fmt.Errorf("parsing SNI extension: %s", err)
}
return hostname, tlsver, nil
}
// Extract the indicated hostname, if any, from the given SNI
// extension bytes.
func parseSNI(b []byte) (string, error) {
b, _, err := vector(b, 2)
if err != nil {
return "", err
}
var ret []byte
for len(b) >= 3 {
typ := b[0]
ret, b, err = vector(b[1:], 2)
if err != nil {
return "", fmt.Errorf("truncated SNI extension")
}
if typ == sniHostnameID {
return string(ret), nil
}
}
if len(b) != 0 {
return "", fmt.Errorf("trailing garbage at end of SNI extension")
}
// No DNS-based SNI present.
return "", nil
}
const sniExtensionID = 0
const sniHostnameID = 0
// Parse a TLS handshake record as a ClientHello message and extract
// the SNI extension bytes, if any.
func parseHello(b []byte) ([]byte, error) {
if len(b) == 0 {
return nil, errors.New("zero length handshake record")
}
if b[0] != 1 {
return nil, fmt.Errorf("non-ClientHello handshake record type %d", b[0])
}
// We're expecting a stricter TLS parser to run after we've
// proxied, so we ignore any trailing bytes that might be present
// (e.g. another handshake message).
b, _, err := vector(b[1:], 3)
if err != nil {
return nil, fmt.Errorf("reading ClientHello: %s", err)
}
// ClientHello must be at least 34 bytes to reach the first vector
// length byte. The actual minimal size is larger than that, but
// vector() will correctly handle truncated packets.
if len(b) < 34 {
return nil, errors.New("ClientHello packet too short")
}
if b[0] != 3 {
return nil, fmt.Errorf("ClientHello has unsupported version %d.%d", b[0], b[1])
}
switch b[1] {
case 1, 2, 3:
// TLS 1.0, TLS 1.1, TLS 1.2
default:
return nil, fmt.Errorf("TLS record has unsupported version %d.%d", b[0], b[1])
}
// Skip over version and random struct
b = b[34:]
// We don't technically care about SessionID, but we care that the
// framing is well-formed all the way up to the SNI field, so that
// we are sure that we're pulling the same SNI bytes as the
// eventual TLS implementation.
vec, b, err := vector(b, 1)
if err != nil {
return nil, fmt.Errorf("reading ClientHello SessionID: %s", err)
}
if len(vec) > 32 {
return nil, fmt.Errorf("ClientHello SessionID too long (%db)", len(vec))
}
// Likewise, we're just checking the bare minimum of framing.
vec, b, err = vector(b, 2)
if err != nil {
return nil, fmt.Errorf("reading ClientHello CipherSuites: %s", err)
}
if len(vec) < 2 || len(vec)%2 != 0 {
return nil, fmt.Errorf("ClientHello CipherSuites invalid length %d", len(vec))
}
vec, b, err = vector(b, 1)
if err != nil {
return nil, fmt.Errorf("reading ClientHello CompressionMethods: %s", err)
}
if len(vec) < 1 {
return nil, fmt.Errorf("ClientHello CompressionMethods invalid length %d", len(vec))
}
// Finally, we reach the extensions.
if len(b) == 0 {
// No extensions. This is not an error, it just means we have
// no SNI payload.
return nil, nil
}
b, vec, err = vector(b, 2)
if err != nil {
return nil, fmt.Errorf("reading ClientHello extensions: %s", err)
}
if len(vec) != 0 {
return nil, fmt.Errorf("%d bytes of trailing garbage in ClientHello", len(vec))
}
for len(b) >= 4 {
typ := binary.BigEndian.Uint16(b[:2])
vec, b, err = vector(b[2:], 2)
if err != nil {
return nil, fmt.Errorf("reading ClientHello extension %d: %s", typ, err)
}
if typ == sniExtensionID {
// Found the SNI extension, return its payload. We don't
// care about anything in the packet beyond this point.
return vec, nil
}
}
if len(b) != 0 {
return nil, fmt.Errorf("%d bytes of trailing garbage in ClientHello", len(b))
}
// Successfully parsed all extensions, but there was no SNI.
return nil, nil
}
const maxTLSRecordLength = 16384
// Read one TLS record, which must be for the handshake protocol, from r.
func handshakeRecord(r io.Reader) ([]byte, int, error) {
var hdr struct {
Type uint8
Major, Minor uint8
Length uint16
}
if err := binary.Read(r, binary.BigEndian, &hdr); err != nil {
return nil, 0, fmt.Errorf("reading TLS record header: %s", err)
}
if hdr.Type != 22 {
return nil, 0, fmt.Errorf("TLS record is not a handshake")
}
if hdr.Major != 3 {
return nil, 0, fmt.Errorf("TLS record has unsupported version %d.%d", hdr.Major, hdr.Minor)
}
switch hdr.Minor {
case 1, 2, 3:
// TLS 1.0, TLS 1.1, TLS 1.2
default:
return nil, 0, fmt.Errorf("TLS record has unsupported version %d.%d", hdr.Major, hdr.Minor)
}
if hdr.Length > maxTLSRecordLength {
return nil, 0, fmt.Errorf("TLS record length is greater than %d", maxTLSRecordLength)
}
ret := make([]byte, hdr.Length)
if _, err := io.ReadFull(r, ret); err != nil {
return nil, 0, err
}
return ret, int(hdr.Minor), nil
}
func vector(b []byte, lenBytes int) ([]byte, []byte, error) {
if len(b) < lenBytes {
return nil, nil, errors.New("not enough space in packet for vector")
}
var l int
for _, b := range b[:lenBytes] {
l = (l << 8) + int(b)
}
if len(b) < l+lenBytes {
return nil, nil, errors.New("not enough space in packet for vector")
}
return b[lenBytes : l+lenBytes], b[l+lenBytes:], nil
}
+67
View File
@@ -0,0 +1,67 @@
package dhcp
import (
"crypto/rand"
"fmt"
"net"
"runtime"
"time"
"errors"
"github.com/krolaw/dhcp4"
"github.com/libp2p/go-reuseport"
)
var broadcastAddr, _ = net.ResolveUDPAddr("udp", "255.255.255.255:67")
func GetDNSServer() ([]string, error) {
iface, err := PickInternetInterface()
if err != nil {
return nil, fmt.Errorf("pick interface: %w", err)
}
xid := make([]byte, 4)
if _, err := rand.Read(xid); err != nil {
return nil, fmt.Errorf("generate xid: %w", err)
}
pack := dhcp4.RequestPacket(dhcp4.Discover, iface.HardwareAddr, net.IPv4(0, 0, 0, 0), xid, true, []dhcp4.Option{
{Code: dhcp4.OptionRequestedIPAddress, Value: []byte(iface.IP.To4())},
{Code: dhcp4.End},
})
var conn net.PacketConn
if runtime.GOOS == "windows" {
if conn, err = reuseport.ListenPacket("udp4", iface.IP.String()+":68"); err != nil {
return nil, fmt.Errorf("listen dhcp: %w", err)
}
} else {
if conn, err = reuseport.ListenPacket("udp4", "0.0.0.0:68"); err != nil {
return nil, fmt.Errorf("listen dhcp: %w", err)
}
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(3 * time.Second))
if _, err := conn.WriteTo([]byte(pack), broadcastAddr); err != nil {
return nil, fmt.Errorf("write broadcast: %w", err)
}
buf := make([]byte, 1500 /*MTU*/)
n, _, err := conn.ReadFrom(buf)
if err != nil {
return nil, fmt.Errorf("read dhcp offer: %w", err)
}
pack = dhcp4.Packet(buf[:n])
dnsBytes := pack.ParseOptions()[dhcp4.OptionDomainNameServer]
if len(dnsBytes) < 4 || len(dnsBytes)%4 != 0 {
return nil, errors.New("invalid DNS setting in upstream network device")
}
ips := []string{}
for i := 0; i < len(dnsBytes); i += 4 {
ips = append(ips, net.IP(dnsBytes[i:i+4]).String())
}
return ips, nil
}
+15
View File
@@ -0,0 +1,15 @@
package dhcp_test
import (
"fmt"
"github.com/sower-proxy/sower/pkg/dhcp"
)
func Example_dns() {
got, err := dhcp.GetDNSServer()
if err != nil {
panic(err)
}
fmt.Println(got)
}
+41
View File
@@ -0,0 +1,41 @@
//go:build !windows
// +build !windows
package dhcp
import (
"errors"
"net"
)
// PickInternetInterface pick the first active net interface
func PickInternetInterface() (*Iface, error) {
ifaces, err := net.Interfaces()
if err != nil {
return nil, err
}
for i := range ifaces {
if ifaces[i].Flags&net.FlagUp == 0 || ifaces[i].Flags&net.FlagLoopback != 0 {
continue
}
if len(ifaces[i].HardwareAddr) == 0 {
continue
}
addrs, err := ifaces[i].Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
ipNet, ok := addr.(*net.IPNet)
if !ok {
continue
}
if ip := ipNet.IP.To4(); ip != nil && ip.IsGlobalUnicast() {
return &Iface{ifaces[i].HardwareAddr, ip}, nil
}
}
}
return nil, errors.New("no valid interface")
}
+15
View File
@@ -0,0 +1,15 @@
package dhcp_test
import (
"fmt"
"github.com/sower-proxy/sower/pkg/dhcp"
)
func Example_iface() {
got, err := dhcp.PickInternetInterface()
if err != nil {
panic(err)
}
fmt.Println(got)
}
+51
View File
@@ -0,0 +1,51 @@
// +build windows
package dhcp
import (
"bytes"
"errors"
"net"
"os"
"syscall"
"unsafe"
)
// PickInternetInterface pick the first active net interface
func PickInternetInterface() (*Iface, error) {
list, err := getAdapterList()
if err != nil {
return nil, err
}
for {
if list.DhcpEnabled == 1 && list.LeaseObtained != 0 {
ip := bytes.TrimRight(list.IpAddressList.IpAddress.String[:], string([]byte{0}))
return &Iface{net.HardwareAddr(list.Address[:list.AddressLength]), net.ParseIP(string(ip))}, nil
}
if list.Next != nil {
list = list.Next
} else {
return nil, errors.New("no valid interface")
}
}
}
func getAdapterList() (*syscall.IpAdapterInfo, error) {
b := make([]byte, 1000)
l := uint32(len(b))
a := (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
// TODO(mikio): GetAdaptersInfo returns IP_ADAPTER_INFO that
// contains IPv4 address list only. We should use another API
// for fetching IPv6 stuff from the kernel.
err := syscall.GetAdaptersInfo(a, &l)
if err == syscall.ERROR_BUFFER_OVERFLOW {
b = make([]byte, l)
a = (*syscall.IpAdapterInfo)(unsafe.Pointer(&b[0]))
err = syscall.GetAdaptersInfo(a, &l)
}
if err != nil {
return nil, os.NewSyscallError("GetAdaptersInfo", err)
}
return a, nil
}
+9
View File
@@ -0,0 +1,9 @@
package dhcp
import "net"
// Iface is net interface address info
type Iface struct {
net.HardwareAddr
net.IP
}
+184
View File
@@ -0,0 +1,184 @@
package suffixtree
import (
"strings"
)
type Node struct {
*node
sep string
Count uint64
}
type node struct {
secs []string
subNodes []*node
indexMap map[string]int
}
func NewNodeFromRules(rules ...string) *Node {
n := &Node{&node{}, ".", 0}
for i := range rules {
n.Add(rules[i])
}
n.GC()
return n
}
func (n *node) GC() {
if n == nil {
return
}
n.secs = GCSlice(n.secs)
n.subNodes = GCSlice(n.subNodes)
for i := range n.subNodes {
n.subNodes[i].GC()
}
}
func (n *Node) String() string {
return n.string("", " ")
}
func (n *node) string(prefix, indent string) (out string) {
if n == nil {
return
}
for key, val := range n.subNodes {
out += prefix + n.secs[key] + "\n" + val.string(prefix+indent, indent)
}
return
}
func (n *Node) trim(item string) string {
return strings.ToLower(strings.TrimSuffix(item, n.sep))
}
func (n *Node) Add(item string) {
n.Count++
n.add(strings.Split(n.trim(item), n.sep))
}
func (n *node) add(secs []string) {
length := len(secs)
switch length {
case 0:
case 1:
sec := secs[length-1]
if idx := n.index(sec); idx >= 0 {
if n.subNodes[idx] != nil {
n.subNodes[idx].add([]string{""})
}
return
}
switch sec {
case "", "*", "**":
n.prepend(sec, nil)
default:
n.append(sec, nil)
}
default:
sec := secs[length-1]
if sec == "**" { // ** is only allowed in the last sec
sec = "*"
}
idx := n.index(sec)
if idx == -1 {
switch sec {
case "", "*", "**":
idx = n.prepend(sec, &node{})
default:
idx = n.append(sec, &node{})
}
} else if n.subNodes[idx] == nil {
n.subNodes[idx] = &node{}
n.subNodes[idx].add([]string{""})
}
n.subNodes[idx].add(secs[:length-1])
}
}
func (n *Node) Match(item string) bool {
if n == nil {
return false
}
return n.matchSecs(strings.Split(n.trim(item), n.sep))
}
func (n *node) matchSecs(secs []string) bool {
length := len(secs)
if length == 0 {
if n == nil {
return true
}
return n.index("") != -1 || n.index("**") != -1
}
if idx := n.index(secs[length-1]); idx >= 0 {
if n.subNodes[idx].matchSecs(secs[:length-1]) {
return true
}
}
if idx := n.index("*"); idx >= 0 {
if n.subNodes[idx].matchSecs(secs[:length-1]) {
return true
}
}
return n.index("**") >= 0
}
// index return the sec index in node, or -1 if not found
func (n *node) index(sec string) int {
if n == nil {
return -1
}
if n.indexMap != nil {
if idx, ok := n.indexMap[sec]; ok {
return idx
}
return -1
}
for i := range n.secs {
if n.secs[i] == sec {
n.ensureIndexMap()
n.indexMap[sec] = i
return i
}
}
return -1
}
func (n *node) append(sec string, child *node) int {
n.secs = append(n.secs, sec)
n.subNodes = append(n.subNodes, child)
n.ensureIndexMap()
idx := len(n.secs) - 1
n.indexMap[sec] = idx
return idx
}
func (n *node) prepend(sec string, child *node) int {
n.secs = append([]string{sec}, n.secs...)
n.subNodes = append([]*node{child}, n.subNodes...)
n.indexMap = nil
n.ensureIndexMap()
return 0
}
func (n *node) ensureIndexMap() {
if n.indexMap != nil {
return
}
n.indexMap = make(map[string]int, len(n.secs))
for i := range n.secs {
n.indexMap[n.secs[i]] = i
}
}
+123
View File
@@ -0,0 +1,123 @@
package suffixtree_test
import (
"testing"
"github.com/sower-proxy/sower/pkg/suffixtree"
)
func TestNode_Match(t *testing.T) {
type test struct {
arg string
want bool
}
tests := []struct {
name string
node *suffixtree.Node
tests []test
}{{
"simple",
suffixtree.NewNodeFromRules("a.wweir.cc", "b.wweir.cc"),
[]test{
{"a.wweir.cc", true},
{"b.wweir.cc", true},
},
}, {
"parent",
suffixtree.NewNodeFromRules("wweir.cc", "a.wweir.cc"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"b.wweir.cc", false},
},
}, {
"fuzz1",
suffixtree.NewNodeFromRules("wweir.cc", "a.wweir.cc", "*.wweir.cc"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"b.wweir.cc", true},
{"a.b.wweir.cc", false},
},
}, {
"fuzz2",
suffixtree.NewNodeFromRules("a.*.cc", "c.wweir.*"),
[]test{
{"wweir.cc", false},
{"a.wweir.cc", true},
{"b.wweir.cc", false},
{"c.wweir.cc", true},
},
}, {
"fuzz3",
suffixtree.NewNodeFromRules("*.*.cc", "iamp.*.*"),
[]test{
{"wweir.cc", false},
{"a.wweir.cc", true},
{"b.wweir.cc", true},
{"iamp.wweir.cc", true},
},
}, {
"fuzz4",
suffixtree.NewNodeFromRules("**.cc", "a.**.com", "**.wweir.*"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"a.b.wweir.cc", true},
{"a.fuzz.com", true},
{"b.fuzz.com", false},
{"www.wweir.com", true},
},
}, {
"github",
suffixtree.NewNodeFromRules("**.github.com"),
[]test{
{"github.com", true},
},
}, {
"normalize domain",
suffixtree.NewNodeFromRules("GitHub.Com", "*.Example.Com", "**.Foo.Com"),
[]test{
{"github.com", true},
{"GitHub.com", true},
{"github.com.", true},
{"a.example.com", true},
{"A.Example.Com", true},
{"foo.com", true},
{"a.b.foo.com", true},
},
}, {
"parent after child",
suffixtree.NewNodeFromRules("a.wweir.cc", "wweir.cc"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"b.wweir.cc", false},
},
}, {
"wildcard after child",
suffixtree.NewNodeFromRules("a.wweir.cc", "*.wweir.cc"),
[]test{
{"a.wweir.cc", true},
{"b.wweir.cc", true},
{"a.b.wweir.cc", false},
},
}, {
"deep wildcard after child",
suffixtree.NewNodeFromRules("a.b.foo.com", "**.foo.com"),
[]test{
{"foo.com", true},
{"a.b.foo.com", true},
{"z.foo.com", true},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, test := range tt.tests {
if got := tt.node.Match(test.arg); got != test.want {
t.Errorf("Node.Match(%s) = %v, want %v", test.arg, got, test.want)
}
}
})
}
}
+6
View File
@@ -0,0 +1,6 @@
package suffixtree
func GCSlice[T any](arr []T) []T {
old := arr
return append(make([]T, 0, len(old)), old...)
}
+107
View File
@@ -0,0 +1,107 @@
package upstreamtls
import (
cryptotls "crypto/tls"
"fmt"
"net"
"strings"
"time"
utls "github.com/refraction-networking/utls"
)
type Options struct {
ServerName string
ClientHello string
InsecureSkipVerify bool
}
func ValidateClientHello(value string) error {
_, err := clientHelloID(value)
return err
}
func Dial(dialer *net.Dialer, network, addr string, options Options) (net.Conn, error) {
if options.ClientHello == "" {
return cryptotls.DialWithDialer(dialer, network, addr, &cryptotls.Config{
ServerName: options.ServerName,
InsecureSkipVerify: options.InsecureSkipVerify,
})
}
helloID, err := clientHelloID(options.ClientHello)
if err != nil {
return nil, err
}
rawConn, err := dialer.Dial(network, addr)
if err != nil {
return nil, err
}
if timeout := dialer.Timeout; timeout > 0 {
if err := rawConn.SetDeadline(time.Now().Add(timeout)); err != nil {
rawConn.Close()
return nil, fmt.Errorf("set tls deadline: %w", err)
}
defer rawConn.SetDeadline(time.Time{})
}
serverName := options.ServerName
if serverName == "" {
host, _, err := net.SplitHostPort(addr)
if err != nil {
rawConn.Close()
return nil, fmt.Errorf("split tls addr %q: %w", addr, err)
}
serverName = host
}
conn := utls.UClient(rawConn, &utls.Config{
ServerName: serverName,
InsecureSkipVerify: options.InsecureSkipVerify,
}, helloID)
if err := conn.Handshake(); err != nil {
rawConn.Close()
return nil, fmt.Errorf("utls handshake: %w", err)
}
return conn, nil
}
func clientHelloID(value string) (utls.ClientHelloID, error) {
switch normalizeClientHello(value) {
case "chrome":
return utls.HelloChrome_Auto, nil
case "firefox":
return utls.HelloFirefox_Auto, nil
case "ios":
return utls.HelloIOS_Auto, nil
case "android":
return utls.HelloAndroid_11_OkHttp, nil
case "edge":
return utls.HelloEdge_Auto, nil
case "safari":
return utls.HelloSafari_Auto, nil
case "360":
return utls.Hello360_Auto, nil
case "qq":
return utls.HelloQQ_Auto, nil
case "randomized":
return utls.HelloRandomized, nil
case "randomizedalpn":
return utls.HelloRandomizedALPN, nil
case "randomizednoalpn":
return utls.HelloRandomizedNoALPN, nil
case "golang":
return utls.HelloGolang, nil
default:
return utls.ClientHelloID{}, fmt.Errorf("unsupported tls client hello %q", value)
}
}
func normalizeClientHello(value string) string {
value = strings.TrimSpace(strings.ToLower(value))
value = strings.ReplaceAll(value, "-", "")
value = strings.ReplaceAll(value, "_", "")
return value
}
+21
View File
@@ -0,0 +1,21 @@
package upstreamtls
import "testing"
func TestValidateClientHelloAcceptsAliases(t *testing.T) {
t.Parallel()
for _, value := range []string{"chrome", "firefox", "randomized_alpn", "randomized-no-alpn", "golang"} {
if err := ValidateClientHello(value); err != nil {
t.Fatalf("validate %q: %v", value, err)
}
}
}
func TestValidateClientHelloRejectsUnknownValue(t *testing.T) {
t.Parallel()
if err := ValidateClientHello("unknown"); err == nil {
t.Fatal("expected validation error")
}
}
-81
View File
@@ -1,81 +0,0 @@
package proxy
import (
"net"
"github.com/golang/glog"
"github.com/wweir/sower/proxy/kcp"
"github.com/wweir/sower/proxy/quic"
"github.com/wweir/sower/proxy/tcp"
"github.com/wweir/sower/shadow"
)
type Client interface {
Dial(server string) (net.Conn, error)
}
func NewClient(netType string) Client {
switch netType {
case QUIC.String():
return quic.NewClient()
case KCP.String():
return kcp.NewClient()
case TCP.String():
return tcp.NewClient()
default:
glog.Fatalln("invalid net type: " + netType)
return nil
}
}
func StartClient(netType, server, cipher, password, listenIP string) {
connCh := listenLocal(listenIP, []string{"80", "443"})
client := NewClient(netType)
if addr, err := net.ResolveTCPAddr("tcp", server); err != nil {
glog.Fatalln(err)
} else {
server = addr.String()
}
glog.Infoln("Client started.")
for {
conn := <-connCh
glog.V(1).Infof("new conn from (%s) to (%s)", conn.RemoteAddr(), server)
rc, err := client.Dial(server)
if err != nil {
conn.Close()
glog.Errorln(err)
continue
}
rc = shadow.Shadow(rc, cipher, password)
go relay(conn, rc)
}
}
func listenLocal(listenIP string, ports []string) <-chan net.Conn {
connCh := make(chan net.Conn, 10)
for i := range ports {
go func(port string) {
ln, err := net.Listen("tcp", net.JoinHostPort(listenIP, port))
if err != nil {
glog.Fatalln(err)
}
for {
conn, err := ln.Accept()
if err != nil {
glog.Errorln("accept", listenIP+port, "fail:", err)
continue
}
conn.(*net.TCPConn).SetKeepAlive(true)
connCh <- conn
}
}(ports[i])
}
glog.Infoln("listening ports:", ports)
return connCh
}
-98
View File
@@ -1,98 +0,0 @@
package proxy
import (
"context"
"crypto/tls"
"io"
"net"
"net/http"
"time"
"github.com/golang/glog"
"github.com/wweir/sower/shadow"
)
func StartHttpProxy(netType, server, cipher, password, addr string) {
client := NewClient(netType)
if addr, err := net.ResolveTCPAddr("tcp", server); err != nil {
glog.Fatalln(err)
} else {
server = addr.String()
}
srv := &http.Server{
Addr: addr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodConnect {
httpsProxy(w, r, client, server, cipher, password)
} else {
httpProxy(w, r, client, server, cipher, password)
}
}),
// Disable HTTP/2.
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
IdleTimeout: 90 * time.Second,
}
glog.Fatalln(srv.ListenAndServe())
}
func httpProxy(w http.ResponseWriter, req *http.Request, client Client, server, cipher, password string) {
roundTripper := &http.Transport{
DialContext: func(context.Context, string, string) (net.Conn, error) {
conn, err := client.Dial(server)
if err != nil {
return nil, err
}
return shadow.Shadow(conn, cipher, password), nil
},
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
resp, err := roundTripper.RoundTrip(req)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
glog.Errorln("serve https proxy, get remote data:", err)
return
}
defer resp.Body.Close()
for k, vs := range resp.Header {
for _, v := range vs {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
func httpsProxy(w http.ResponseWriter, r *http.Request, client Client, server, cipher, password string) {
// local conn
conn, _, err := w.(http.Hijacker).Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
conn.(*net.TCPConn).SetKeepAlive(true)
if _, err := conn.Write([]byte(r.Proto + " 200 Connection established\r\n\r\n")); err != nil {
conn.Close()
glog.Errorln("serve https proxy, write data fail:", err)
return
}
// remote conn
rc, err := client.Dial(server)
if err != nil {
conn.Close()
http.Error(w, err.Error(), http.StatusServiceUnavailable)
glog.Errorln("serve https proxy, dial remote fail:", err)
return
}
rc = shadow.Shadow(rc, cipher, password)
relay(rc, conn)
}
-65
View File
@@ -1,65 +0,0 @@
package kcp
import (
"net"
"github.com/pkg/errors"
kcp "github.com/xtaci/kcp-go"
)
type client struct {
DataShard int
ParityShard int
DSCP int
SockBuf int
AckNodelay bool
NoDelay int
Interval int
Resend int
NoCongestion int
SndWnd int
RcvWnd int
MTU int
}
func NewClient() *client {
return &client{
DataShard: 10,
ParityShard: 3,
DSCP: 0,
SockBuf: 4194304,
NoDelay: 0,
Interval: 50,
Resend: 0,
NoCongestion: 0,
SndWnd: 0,
RcvWnd: 0,
MTU: 1350,
}
}
func (c *client) Dial(server string) (net.Conn, error) {
conn, err := kcp.DialWithOptions(server, nil, c.DataShard, c.ParityShard)
if err != nil {
return nil, errors.Wrap(err, "dial")
}
conn.SetStreamMode(true)
conn.SetWriteDelay(false)
conn.SetNoDelay(c.NoDelay, c.Interval, c.Resend, c.NoCongestion)
conn.SetWindowSize(c.SndWnd, c.RcvWnd)
conn.SetMtu(c.MTU)
conn.SetACKNoDelay(c.AckNodelay)
if err := conn.SetDSCP(c.DSCP); err != nil {
return nil, errors.Wrap(err, "SetDSCP")
}
if err := conn.SetReadBuffer(c.SockBuf); err != nil {
return nil, errors.Wrap(err, "SetReadBuffer")
}
if err := conn.SetWriteBuffer(c.SockBuf); err != nil {
return nil, errors.Wrap(err, "SetWriteBuffer")
}
return conn, nil
}
-56
View File
@@ -1,56 +0,0 @@
package kcp
import (
"net"
"github.com/golang/glog"
"github.com/pkg/errors"
kcp "github.com/xtaci/kcp-go"
)
type server struct {
DataShard int
ParityShard int
DSCP int
SockBuf int
}
func NewServer() *server {
return &server{
DataShard: 10,
ParityShard: 3,
DSCP: 0,
SockBuf: 4194304,
}
}
func (s *server) Listen(port string) (<-chan net.Conn, error) {
ln, err := kcp.ListenWithOptions(port, nil, s.DataShard, s.ParityShard)
if err != nil {
return nil, err
}
if err := ln.SetDSCP(s.DSCP); err != nil {
return nil, errors.Wrap(err, "SetDSCP")
}
if err := ln.SetReadBuffer(s.SockBuf); err != nil {
return nil, errors.Wrap(err, "SetReadBuffer")
}
if err := ln.SetWriteBuffer(s.SockBuf); err != nil {
return nil, errors.Wrap(err, "SetWriteBuffer")
}
connCh := make(chan net.Conn)
go func() {
for {
conn, err := ln.AcceptKCP()
if err != nil {
glog.Fatalln("KCP listen:", err)
}
connCh <- conn
}
}()
return connCh, nil
}
-16
View File
@@ -1,16 +0,0 @@
// Code generated by "stringer -type=netType util.go"; DO NOT EDIT.
package proxy
import "strconv"
const _netType_name = "QUICKCPTCP"
var _netType_index = [...]uint8{0, 4, 7, 10}
func (i netType) String() string {
if i < 0 || i >= netType(len(_netType_index)-1) {
return "netType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _netType_name[_netType_index[i]:_netType_index[i+1]]
}
-57
View File
@@ -1,57 +0,0 @@
package quic
import (
"crypto/tls"
"net"
"time"
quic "github.com/lucas-clemente/quic-go"
"github.com/pkg/errors"
"github.com/wweir/sower/util"
)
type client struct {
conf *quic.Config
sess quic.Session
}
func NewClient() *client {
return &client{
conf: &quic.Config{
HandshakeTimeout: 5 * time.Second,
MaxIncomingStreams: 1024,
KeepAlive: true,
IdleTimeout: 5 * time.Minute,
},
}
}
func (c *client) Dial(server string) (net.Conn, error) {
if c.sess == nil {
if sess, err := quic.DialAddr(server, &tls.Config{InsecureSkipVerify: true}, c.conf); err != nil {
return nil, errors.Wrap(err, "session")
} else {
go func() {
<-sess.Context().Done()
sess.Close()
c.sess = nil
}()
c.sess = sess
}
}
var stream quic.Stream
if err := util.WithTimeout(func() (err error) {
if stream, err = c.sess.OpenStream(); err != nil {
c.sess = nil
}
return
}, time.Second); err != nil {
return nil, errors.Wrap(err, "stream")
}
return &streamConn{
Stream: stream,
sess: c.sess,
}, nil
}
-59
View File
@@ -1,59 +0,0 @@
package quic
import (
"net"
"time"
"github.com/golang/glog"
quic "github.com/lucas-clemente/quic-go"
"github.com/pkg/errors"
)
type server struct {
conf *quic.Config
}
func NewServer() *server {
return &server{
conf: &quic.Config{
HandshakeTimeout: 5 * time.Second,
MaxIncomingStreams: 1024,
KeepAlive: true,
IdleTimeout: 5 * time.Minute,
},
}
}
func (s *server) Listen(port string) (<-chan net.Conn, error) {
ln, err := quic.ListenAddr(port, mockTlsPem(), s.conf)
if err != nil {
return nil, errors.WithStack(err)
}
connCh := make(chan net.Conn)
go func() {
for {
sess, err := ln.Accept()
if err != nil {
glog.Fatalln(err)
}
go accept(sess, connCh)
}
}()
return connCh, nil
}
func accept(sess quic.Session, connCh chan<- net.Conn) {
glog.V(1).Infoln("new session from ", sess.RemoteAddr())
defer sess.Close()
for {
stream, err := sess.AcceptStream()
if err != nil {
glog.Errorln(err)
return
}
connCh <- &streamConn{stream, sess}
}
}
-48
View File
@@ -1,48 +0,0 @@
package quic
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"math/big"
"net"
"github.com/golang/glog"
quic "github.com/lucas-clemente/quic-go"
)
type streamConn struct {
quic.Stream
sess quic.Session
}
func (s *streamConn) LocalAddr() net.Addr {
return s.sess.LocalAddr()
}
func (s *streamConn) RemoteAddr() net.Addr {
return s.sess.RemoteAddr()
}
func mockTlsPem() *tls.Config {
key, err := rsa.GenerateKey(rand.Reader, 1024)
if err != nil {
glog.Fatalln(err)
}
template := x509.Certificate{SerialNumber: big.NewInt(1)}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
if err != nil {
glog.Fatalln(err)
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
glog.Fatalln(err)
}
return &tls.Config{Certificates: []tls.Certificate{tlsCert}}
}
-73
View File
@@ -1,73 +0,0 @@
package proxy
import (
"net"
"strings"
"github.com/golang/glog"
"github.com/wweir/sower/parse"
"github.com/wweir/sower/proxy/kcp"
"github.com/wweir/sower/proxy/quic"
"github.com/wweir/sower/proxy/tcp"
"github.com/wweir/sower/shadow"
)
type Server interface {
Listen(port string) (<-chan net.Conn, error)
}
func NewServer(netType string) Server {
switch netType {
case QUIC.String():
return quic.NewServer()
case KCP.String():
return kcp.NewServer()
case TCP.String():
return tcp.NewServer()
default:
glog.Fatalln("invalid net type: " + netType)
return nil
}
}
func StartServer(netType, port, cipher, password string) {
if port == "" {
glog.Fatalln("port must set")
}
if !strings.Contains(port, ":") {
port = ":" + port
}
connCh, err := NewServer(netType).Listen(port)
if err != nil {
glog.Fatalf("listen %v fail: %s", port, err)
}
glog.Infoln("Server started.")
for {
conn := <-connCh
conn = shadow.Shadow(conn, cipher, password)
go handle(conn)
}
}
func handle(conn net.Conn) {
conn, addr, err := parse.ParseAddr(conn)
if err != nil {
conn.Close()
glog.Warningln(err)
return
}
glog.V(1).Infof("new conn from %s to %s", conn.RemoteAddr(), addr)
rc, err := net.Dial("tcp", addr)
if err != nil {
conn.Close()
glog.Warningln(err)
return
}
rc.(*net.TCPConn).SetKeepAlive(true)
relay(rc, conn)
}
-26
View File
@@ -1,26 +0,0 @@
package tcp
import (
"net"
"time"
)
type client struct {
DialTimeout time.Duration
}
func NewClient() *client {
return &client{
DialTimeout: 5 * time.Second,
}
}
func (c *client) Dial(server string) (net.Conn, error) {
conn, err := net.DialTimeout("tcp", server, c.DialTimeout)
if err != nil {
return nil, err
}
conn.(*net.TCPConn).SetKeepAlive(true)
return conn, nil
}
-35
View File
@@ -1,35 +0,0 @@
package tcp
import (
"net"
"github.com/golang/glog"
)
type server struct {
}
func NewServer() *server {
return &server{}
}
func (s *server) Listen(port string) (<-chan net.Conn, error) {
ln, err := net.Listen("tcp", port)
if err != nil {
return nil, err
}
connCh := make(chan net.Conn)
go func() {
for {
conn, err := ln.Accept()
if err != nil {
glog.Fatalln("TCP listen:", err)
}
conn.(*net.TCPConn).SetKeepAlive(true)
connCh <- conn
}
}()
return connCh, nil
}
-47
View File
@@ -1,47 +0,0 @@
package proxy
import (
"io"
"net"
"sync"
"sync/atomic"
"time"
"github.com/golang/glog"
)
//go:generate stringer -type=netType $GOFILE
type netType int
const (
QUIC netType = iota
KCP
TCP
)
func relay(conn1, conn2 net.Conn) {
wg := &sync.WaitGroup{}
exitFlag := new(int32)
wg.Add(2)
go redirect(conn2, conn1, wg, exitFlag)
redirect(conn1, conn2, wg, exitFlag)
wg.Wait()
}
func redirect(dst, src net.Conn, wg *sync.WaitGroup, exitFlag *int32) {
if _, err := io.Copy(dst, src); err != nil {
glog.V(1).Infof("%s<>%s -> %s<>%s: %s", src.RemoteAddr(), src.LocalAddr(), dst.LocalAddr(), dst.RemoteAddr(), err)
}
if atomic.CompareAndSwapInt32(exitFlag, 0, 1) {
// wakeup blocked goroutine
now := time.Now()
src.SetDeadline(now)
dst.SetDeadline(now)
} else {
src.Close()
dst.Close()
}
wg.Done()
}
+47
View File
@@ -0,0 +1,47 @@
package router
import (
"context"
"log/slog"
"net"
"time"
)
func (r *Router) localSite(domain string) bool {
// parse domain to IP
ip := net.ParseIP(domain)
if ip == nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
ips, err := net.DefaultResolver.LookupIP(ctx, "ip", domain)
if err != nil || len(ips) == 0 {
slog.Warn("resolve domain", "error", err, "domain", domain, "ips", len(ips))
return false
}
ip = ips[0]
}
// CIDR match
for _, cidr := range r.country.cidrs {
if cidr.Contains(ip) {
return true
}
}
// MMDB match CN
if r.country.Reader != nil {
city, err := r.country.City(ip)
if err != nil {
slog.Warn("mmdb search", "error", err, "domain", domain, "ip", ip)
return false
}
if city.Country.IsoCode == "CN" {
return true
}
}
return false
}
+508
View File
@@ -0,0 +1,508 @@
package router
import (
"errors"
"fmt"
"log/slog"
"net"
"strings"
"time"
"github.com/miekg/dns"
)
var errNoLocalIPForQuestionType = errors.New("no local IP for question type")
type retryableDNSError struct {
rcode int
}
func (e retryableDNSError) Error() string {
return fmt.Sprintf("retryable dns response code %s", dns.RcodeToString[e.rcode])
}
func (r *Router) ServeDNS(w dns.ResponseWriter, req *dns.Msg) {
// https://stackoverflow.com/questions/4082081/requesting-a-and-aaaa-records-in-single-dns-query/4083071#4083071
if len(req.Question) != 1 {
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeFormatError))
return
}
domain := req.Question[0].Name
qtype := req.Question[0].Qtype
if req.Question[0].Qclass != dns.ClassINET {
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeNotImplemented))
return
}
// 1. rule_based( block > direct > proxy )
routeDomains := dnsRouteDomains(domain, qtype)
if dnsRuleMatch(r.BlockRule, routeDomains) {
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeNameError))
return
}
if !dnsRuleMatch(r.DirectRule, routeDomains) && dnsRuleMatch(r.ProxyRule, routeDomains) {
switch {
case isAddressQuestion(qtype):
resp, err := r.dnsProxyReply(domain, w.LocalAddr(), req)
if err != nil {
slog.Warn("proxy dns", "error", err, "domain", domain)
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeServerFailure))
return
}
_ = w.WriteMsg(resp)
return
default:
_ = w.WriteMsg(r.dnsNoData(req))
return
}
}
// 2. direct query, do not fallback to proxy to avoid side-effect
resp, err := r.Exchange(req)
if err != nil {
_ = w.WriteMsg(r.dnsFail(req, dns.RcodeServerFailure))
} else {
_ = w.WriteMsg(resp)
}
}
func (r *Router) dnsFail(req *dns.Msg, rcode int) *dns.Msg {
m := new(dns.Msg)
m.SetRcode(req, rcode)
return m
}
func (r *Router) dnsNoData(req *dns.Msg) *dns.Msg {
return dnsReply(req)
}
func (r *Router) dnsProxyReply(domain string, localAddr net.Addr, req *dns.Msg) (*dns.Msg, error) {
qtype := req.Question[0].Qtype
reply := dnsReply(req)
localIP, err := r.proxyReplyIP(localAddr, qtype)
if err != nil {
if errors.Is(err, errNoLocalIPForQuestionType) {
return reply, nil
}
return nil, err
}
record, err := proxyReplyRecord(domain, qtype, localIP)
if err != nil {
return nil, err
}
reply.Answer = []dns.RR{record}
return reply, nil
}
func (r *Router) proxyReplyIP(localAddr net.Addr, qtype uint16) (net.IP, error) {
if !isAddressQuestion(qtype) {
return nil, fmt.Errorf("unsupported question type %d", qtype)
}
var localAddrErr error
if localAddr != nil {
host, _, err := net.SplitHostPort(localAddr.String())
if err != nil {
localAddrErr = fmt.Errorf("split local dns address %q: %w", localAddr.String(), err)
} else {
ip := net.ParseIP(strings.Trim(host, "[]"))
switch {
case ip == nil:
localAddrErr = fmt.Errorf("parse local dns IP %q", host)
case !ip.IsUnspecified() && matchesQuestionTypeIP(ip, qtype):
return ip, nil
}
}
}
for _, serveIP := range r.dns.serveIPs {
if !serveIP.IsUnspecified() && matchesQuestionTypeIP(serveIP, qtype) {
return serveIP, nil
}
}
if localAddrErr != nil {
return nil, localAddrErr
}
return nil, fmt.Errorf("%w %d", errNoLocalIPForQuestionType, qtype)
}
func isAddressQuestion(qtype uint16) bool {
return qtype == dns.TypeA || qtype == dns.TypeAAAA
}
func dnsRuleMatch(rule interface{ Match(string) bool }, domains []string) bool {
for _, domain := range domains {
if rule.Match(domain) {
return true
}
}
return false
}
func dnsRouteDomains(domain string, qtype uint16) []string {
if !isServiceDiscoveryQuestion(qtype) {
return []string{domain}
}
base, ok := stripDNSServicePrefix(domain)
if !ok {
return []string{domain}
}
return []string{domain, base}
}
func isServiceDiscoveryQuestion(qtype uint16) bool {
switch qtype {
case dns.TypeSRV, dns.TypeSVCB, dns.TypeHTTPS:
return true
default:
return false
}
}
func stripDNSServicePrefix(domain string) (string, bool) {
trimmed := strings.TrimSuffix(domain, ".")
labels := strings.Split(trimmed, ".")
if len(labels) <= 2 || !strings.HasPrefix(labels[0], "_") || !strings.HasPrefix(labels[1], "_") {
return "", false
}
return strings.Join(labels[2:], ".") + ".", true
}
func dnsReply(req *dns.Msg) *dns.Msg {
reply := new(dns.Msg)
reply.SetReply(req)
return reply
}
func proxyReplyRecord(domain string, qtype uint16, localIP net.IP) (dns.RR, error) {
switch qtype {
case dns.TypeA:
return &dns.A{
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 20},
A: localIP,
}, nil
case dns.TypeAAAA:
return &dns.AAAA{
Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeAAAA, Class: dns.ClassINET, Ttl: 20},
AAAA: localIP,
}, nil
default:
return nil, fmt.Errorf("unsupported question type %d", qtype)
}
}
func matchesQuestionTypeIP(ip net.IP, qtype uint16) bool {
switch qtype {
case dns.TypeA:
return ip.To4() != nil
case dns.TypeAAAA:
return ip.To4() == nil
default:
return false
}
}
const (
dnsTimeout = 2 * time.Second
dnsUDPSize = 1232
dnsRetryInterval = 30 * time.Second
dnsRefreshTTL = 5 * time.Minute
)
func (r *Router) Exchange(req *dns.Msg) (_ *dns.Msg, err error) {
addrs, index, shouldProbe, err := r.currentUpstreamState(time.Now())
if err != nil {
return nil, err
}
if shouldProbe {
resp, probeErr := r.exchangeWithRetry(req, addrs[0])
if probeErr == nil {
r.promoteUpstream()
return resp, nil
}
r.scheduleRetry(time.Now())
}
resp, err := r.exchangeWithRetry(req, addrs[index])
if err != nil {
nextIndex, switched := r.degradeUpstream(index)
if switched {
slog.Info("use upstream dns", "ip", addrs[nextIndex])
resp, err = r.exchangeWithRetry(req, addrs[nextIndex])
}
}
if resp != nil && isRetryableDNSResponseErr(err) {
return resp, nil
}
return resp, err
}
func isRetryableDNSResponseErr(err error) bool {
var retryableErr retryableDNSError
return errors.As(err, &retryableErr)
}
func (r *Router) currentUpstreamState(now time.Time) ([]string, int, bool, error) {
for {
addrs, index, shouldProbe, needRefresh, err := r.prepareUpstreamState(now)
if !needRefresh || err != nil {
return addrs, index, shouldProbe, err
}
refreshedAddrs, refreshErr := r.buildUpstreamAddrs()
r.finishUpstreamRefresh(now, refreshedAddrs, refreshErr)
}
}
func (r *Router) degradeUpstream(index int) (int, bool) {
r.dns.Lock()
defer r.dns.Unlock()
if len(r.dns.upstreamAddrs) == 0 || r.dns.upstreamIndex != index || index >= len(r.dns.upstreamAddrs)-1 {
return r.dns.upstreamIndex, false
}
r.dns.upstreamIndex = index + 1
r.dns.retryAt = time.Now().Add(dnsRetryInterval)
r.dns.probeInFlight = false
return r.dns.upstreamIndex, true
}
func (r *Router) promoteUpstream() {
r.dns.Lock()
defer r.dns.Unlock()
r.dns.upstreamIndex = 0
r.dns.retryAt = time.Time{}
r.dns.probeInFlight = false
}
func (r *Router) scheduleRetry(now time.Time) {
r.dns.Lock()
defer r.dns.Unlock()
if r.dns.upstreamIndex > 0 {
r.dns.retryAt = now.Add(dnsRetryInterval)
r.dns.probeInFlight = false
}
}
func (r *Router) prepareUpstreamState(now time.Time) ([]string, int, bool, bool, error) {
r.dns.Lock()
defer r.dns.Unlock()
needRefresh := false
switch {
case len(r.dns.upstreamAddrs) == 0:
if r.dns.refreshInFlight {
if fallbackAddrs, ok := r.fallbackOnlyUpstreamsLocked(); ok {
return fallbackAddrs, 0, false, false, nil
}
return nil, 0, false, false, r.upstreamUnavailableErrLocked()
}
if !r.dns.refreshAt.IsZero() && now.Before(r.dns.refreshAt) {
if fallbackAddrs, ok := r.fallbackOnlyUpstreamsLocked(); ok {
return fallbackAddrs, 0, false, false, nil
}
return nil, 0, false, false, r.upstreamUnavailableErrLocked()
}
r.dns.refreshInFlight = true
needRefresh = true
case r.shouldRefreshUpstreams(now) && !r.dns.refreshInFlight:
r.dns.refreshInFlight = true
needRefresh = true
}
shouldProbe := !needRefresh &&
len(r.dns.upstreamAddrs) > 0 &&
r.dns.upstreamIndex > 0 &&
!r.dns.retryAt.IsZero() &&
!now.Before(r.dns.retryAt) &&
!r.dns.probeInFlight
if shouldProbe {
r.dns.probeInFlight = true
r.dns.retryAt = now.Add(dnsRetryInterval)
}
addrs := append([]string(nil), r.dns.upstreamAddrs...)
return addrs, r.dns.upstreamIndex, shouldProbe, needRefresh, nil
}
func (r *Router) buildUpstreamAddrs() ([]string, error) {
dnsIPs := []string{r.dns.upstreamDNS}
if r.dns.upstreamDNS == "" {
var err error
dnsIPs, err = r.dns.getDNSServer()
if err != nil {
slog.Error("get dns server", "error", err, "dns", dnsIPs)
}
}
addrs := make([]string, 0, len(dnsIPs)+1)
seen := make(map[string]struct{}, len(dnsIPs)+1)
appendAddr := func(ip string) {
if ip == "" || r.isServeIP(ip) {
return
}
addr := net.JoinHostPort(ip, "53")
if _, ok := seen[addr]; ok {
return
}
seen[addr] = struct{}{}
addrs = append(addrs, addr)
}
for _, ip := range dnsIPs {
appendAddr(ip)
}
appendAddr(r.dns.fallbackDNS)
if len(addrs) == 0 {
return nil, fmt.Errorf("no available upstream dns")
}
return addrs, nil
}
func (r *Router) shouldRefreshUpstreams(now time.Time) bool {
return r.dns.upstreamDNS == "" && !r.dns.refreshAt.IsZero() && !now.Before(r.dns.refreshAt)
}
func (r *Router) scheduleRefreshLocked(now time.Time) {
if r.dns.upstreamDNS == "" {
r.dns.refreshAt = now.Add(dnsRefreshTTL)
}
}
func (r *Router) applyUpstreamAddrsLocked(addrs []string, now time.Time) {
currentAddr := ""
retryAt := r.dns.retryAt
if len(r.dns.upstreamAddrs) > 0 && r.dns.upstreamIndex >= 0 && r.dns.upstreamIndex < len(r.dns.upstreamAddrs) {
currentAddr = r.dns.upstreamAddrs[r.dns.upstreamIndex]
}
r.dns.upstreamAddrs = addrs
r.dns.upstreamIndex = 0
if currentAddr != "" {
for i, addr := range addrs {
if addr == currentAddr {
r.dns.upstreamIndex = i
break
}
}
}
if r.dns.upstreamIndex == 0 {
r.dns.retryAt = time.Time{}
} else {
r.dns.retryAt = retryAt
}
r.dns.lastRefreshErr = nil
r.dns.refreshInFlight = false
r.dns.probeInFlight = false
r.scheduleRefreshLocked(now)
slog.Info("use upstream dns", "ips", addrs)
}
func (r *Router) finishUpstreamRefresh(now time.Time, addrs []string, err error) {
r.dns.Lock()
defer r.dns.Unlock()
if err != nil {
r.dns.refreshInFlight = false
r.dns.lastRefreshErr = err
r.scheduleRefreshLocked(now)
if len(r.dns.upstreamAddrs) > 0 {
slog.Warn("refresh upstream dns", "error", err)
}
return
}
r.applyUpstreamAddrsLocked(addrs, now)
}
func (r *Router) upstreamUnavailableErrLocked() error {
if r.dns.lastRefreshErr != nil {
return r.dns.lastRefreshErr
}
return fmt.Errorf("upstream dns unavailable")
}
func (r *Router) fallbackOnlyUpstreamsLocked() ([]string, bool) {
if r.dns.fallbackDNS == "" || r.isServeIP(r.dns.fallbackDNS) {
return nil, false
}
return []string{net.JoinHostPort(r.dns.fallbackDNS, "53")}, true
}
func (r *Router) exchangeWithRetry(req *dns.Msg, addr string) (*dns.Msg, error) {
resp, err := r.exchangeUDP(req, addr)
if err != nil {
return nil, err
}
if !resp.Truncated {
return resp, dnsResponseErr(resp)
}
resp, err = r.exchangeTCP(req, addr)
if err != nil {
return nil, err
}
return resp, dnsResponseErr(resp)
}
func dnsResponseErr(resp *dns.Msg) error {
if resp == nil {
return fmt.Errorf("empty dns response")
}
switch resp.Rcode {
case dns.RcodeServerFailure, dns.RcodeRefused, dns.RcodeNotImplemented:
return retryableDNSError{rcode: resp.Rcode}
default:
return nil
}
}
func (r *Router) exchangeUDP(req *dns.Msg, addr string) (*dns.Msg, error) {
msg := req.Copy()
if msg.IsEdns0() == nil {
msg.SetEdns0(dnsUDPSize, false)
}
client := dns.Client{
Net: "udp",
Timeout: dnsTimeout,
UDPSize: dnsUDPSize,
}
resp, _, err := client.Exchange(msg, addr)
return resp, err
}
func (r *Router) exchangeTCP(req *dns.Msg, addr string) (*dns.Msg, error) {
client := dns.Client{
Net: "tcp",
Timeout: dnsTimeout,
}
resp, _, err := client.Exchange(req.Copy(), addr)
return resp, err
}
func (r *Router) isServeIP(raw string) bool {
ip := net.ParseIP(strings.Trim(raw, "[]"))
if ip == nil {
return false
}
for _, serveIP := range r.dns.serveIPs {
if serveIP.Equal(ip) {
return true
}
}
return false
}
+58
View File
@@ -0,0 +1,58 @@
package router
import (
"fmt"
"log/slog"
"net/http"
"time"
"github.com/sower-proxy/mem"
)
var accessCache = mem.NewRotateCache(time.Hour, httpPing)
func (r *Router) isAccess(domain string, port uint16) bool {
switch port {
case 80:
case 443:
default:
return false
}
ok, _ := accessCache.Get(accessCacheKey(domain, port))
return ok
}
var pingClient = http.Client{
Timeout: 2 * time.Second,
}
func httpPing(key string) (bool, error) {
scheme, host, err := accessProbeTarget(key)
if err != nil {
return false, err
}
resp, err := pingClient.Head(scheme + "://" + host)
if err != nil {
slog.Warn("failed to ping", "error", err, "scheme", scheme, "host", host)
return false, nil
}
_ = resp.Body.Close()
return true, nil
}
func accessCacheKey(domain string, port uint16) string {
return fmt.Sprintf("%d:%s", port, domain)
}
func accessProbeTarget(key string) (string, string, error) {
switch {
case len(key) > 3 && key[:3] == "80:":
return "http", key[3:], nil
case len(key) > 4 && key[:4] == "443:":
return "https", key[4:], nil
default:
return "", "", fmt.Errorf("invalid access probe key %q", key)
}
}
+148
View File
@@ -0,0 +1,148 @@
package router
import (
"errors"
"fmt"
"log/slog"
"net"
"strconv"
"strings"
"sync"
"time"
geoip2 "github.com/oschwald/geoip2-golang"
"github.com/sower-proxy/conns/relay"
"github.com/sower-proxy/deferlog/v2"
"github.com/sower-proxy/sower/pkg/dhcp"
"github.com/sower-proxy/sower/pkg/suffixtree"
)
type (
ProxyDialFn func(network, host string, port uint16) (net.Conn, error)
Router struct {
BlockRule *suffixtree.Node
DirectRule *suffixtree.Node
ProxyRule *suffixtree.Node
ProxyDial ProxyDialFn
dns struct {
upstreamDNS string
fallbackDNS string
serveIPs []net.IP
getDNSServer func() ([]string, error)
sync.Mutex
upstreamAddrs []string
upstreamIndex int
refreshAt time.Time
refreshInFlight bool
lastRefreshErr error
retryAt time.Time
probeInFlight bool
}
country struct {
*geoip2.Reader
cidrs []*net.IPNet
}
}
)
var ErrBlocked = errors.New("route blocked")
func NewRouter(serveIPs []string, upstreamDNS, fallbackDNS, mmdbFile string, proxyDial ProxyDialFn) *Router {
r := Router{
BlockRule: suffixtree.NewNodeFromRules(),
DirectRule: suffixtree.NewNodeFromRules(),
ProxyRule: suffixtree.NewNodeFromRules(),
ProxyDial: proxyDial,
}
r.dns.upstreamDNS = upstreamDNS
r.dns.fallbackDNS = fallbackDNS
r.dns.getDNSServer = dhcp.GetDNSServer
for _, serveIP := range serveIPs {
if ip := net.ParseIP(serveIP); ip != nil {
r.dns.serveIPs = append(r.dns.serveIPs, ip)
}
}
mmdbFile = strings.TrimSpace(mmdbFile)
if mmdbFile != "" {
var err error
r.country.Reader, err = geoip2.Open(mmdbFile)
if err != nil {
slog.Warn("open geoip2 db", "error", err, "file", mmdbFile)
}
}
return &r
}
func (r *Router) AddCountryCIDRs(cidrs ...string) {
for _, cidr := range cidrs {
_, ipnet, err := net.ParseCIDR(cidr)
if err != nil {
slog.Error("failed to parse CIDR", "error", err, "cidr", cidr)
continue
}
r.country.cidrs = append(r.country.cidrs, ipnet)
}
r.country.cidrs = suffixtree.GCSlice(r.country.cidrs)
}
func (r *Router) RouteHandle(conn net.Conn, domain string, port uint16) (err error) {
start := time.Now()
defer func() {
deferlog.DebugWarn(err, "route handle", "domain", domain, "port", port, "took", time.Since(start))
}()
rc, err := r.Dial("tcp", domain, port)
if err != nil {
return err
}
defer rc.Close()
if err := relay.Relay(conn, rc); err != nil {
return fmt.Errorf("relay %s:%d: %w", domain, port, err)
}
return nil
}
func (r *Router) Dial(network, domain string, port uint16) (net.Conn, error) {
addr := net.JoinHostPort(domain, strconv.FormatUint(uint64(port), 10))
// 1. rule_based( block > direct > proxy )
// 2. detect_based( CN IP || access site )
// 3. fallback( proxy )
switch {
case r.BlockRule.Match(domain):
return nil, ErrBlocked
case r.DirectRule.Match(domain):
return r.directDial(network, addr)
case r.ProxyRule.Match(domain):
return r.proxyDial(network, domain, port)
case r.localSite(domain), r.isAccess(domain, port):
return r.directDial(network, addr)
default:
return r.proxyDial(network, domain, port)
}
}
func (r *Router) proxyDial(network, domain string, port uint16) (net.Conn, error) {
start := time.Now()
rc, err := r.ProxyDial(network, domain, port)
if err != nil {
return nil, fmt.Errorf("proxy dial %s:%d, spend (%s): %w", domain, port, time.Since(start), err)
}
return rc, nil
}
func (r *Router) directDial(network, addr string) (net.Conn, error) {
start := time.Now()
conn, err := net.DialTimeout(network, addr, 5*time.Second)
if err != nil {
return nil, fmt.Errorf("direct dial %s, spend (%s): %w", addr, time.Since(start), err)
}
return conn, nil
}
File diff suppressed because it is too large Load Diff
+28
View File
@@ -0,0 +1,28 @@
package router
import (
"net"
"net/url"
"strconv"
)
func ParseHostPort(hostport string, u *url.URL) (string, uint16, error) {
host, port, err := net.SplitHostPort(hostport)
if err != nil {
if err.(*net.AddrError).Err == "missing port in address" {
switch u.Scheme {
case "http":
return hostport, 80, nil
case "https":
return hostport, 443, nil
}
}
return "", 0, err
}
portInt, err := strconv.ParseUint(port, 10, 16)
if err != nil {
return "", 0, err
}
return host, uint16(portInt), nil
}
-67
View File
@@ -1,67 +0,0 @@
package shadow
import (
"crypto/aes"
"crypto/cipher"
"github.com/pkg/errors"
"golang.org/x/crypto/chacha20poly1305"
)
//go:generate stringer -type=cipherType $GOFILE
type cipherType int
const (
AES_128_GCM cipherType = iota
AES_192_GCM
AES_256_GCM
CHACHA20_IETF_POLY1305
XCHACHA20_IETF_POLY1305
)
func pickCipher(cipherType, password string) (cipher.AEAD, error) {
var blockSize int
switch cipherType {
case AES_128_GCM.String():
blockSize = 16
case AES_192_GCM.String():
blockSize = 24
case AES_256_GCM.String():
blockSize = 32
case CHACHA20_IETF_POLY1305.String():
return chacha20poly1305.New(genKey(password, 256))
case XCHACHA20_IETF_POLY1305.String():
return chacha20poly1305.NewX(genKey(password, 256))
default:
return nil, errors.New("do not support cipher type: " + cipherType)
}
// aes gcm
block, err := aes.NewCipher(genKey(password, blockSize))
if err != nil {
return nil, errors.Wrap(err, "password")
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, errors.Wrap(err, AES_128_GCM.String())
}
return aead, nil
}
func genKey(filler string, size int) []byte {
res := make([]byte, size)
if filler == "" {
panic("password should not be empty")
}
fillerByte := []byte(filler)
length := len(fillerByte)
for i := 0; ; i++ {
if copy(res[i*length:], fillerByte) != length {
return res
}
}
}
-16
View File
@@ -1,16 +0,0 @@
// Code generated by "stringer -type=cipherType cipher.go"; DO NOT EDIT.
package shadow
import "strconv"
const _cipherType_name = "AES_128_GCMAES_192_GCMAES_256_GCMCHACHA20_IETF_POLY1305XCHACHA20_IETF_POLY1305"
var _cipherType_index = [...]uint8{0, 11, 22, 33, 55, 78}
func (i cipherType) String() string {
if i < 0 || i >= cipherType(len(_cipherType_index)-1) {
return "cipherType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _cipherType_name[_cipherType_index[i]:_cipherType_index[i+1]]
}
-4
View File
@@ -1,4 +0,0 @@
// Package shadow transter conn to be a crypto conn
// support aead mode only
// payload: [size](2+Overhead bytes) + [content](size+Overhead bytes)
package shadow
-111
View File
@@ -1,111 +0,0 @@
package shadow
import (
"crypto/cipher"
"encoding/binary"
"io"
"math/rand"
"net"
)
const MAX_SIZE = 0xFFFF
type conn struct {
maxSize int
aead cipher.AEAD
encryptNonce func() []byte
decryptNonce func() []byte
writeBuf []byte
readBuf []byte
readOffset int
net.Conn
}
func (c *conn) Read(b []byte) (n int, err error) {
// read from buffer
if c.readOffset != 0 {
dataSize := len(c.readBuf) - c.aead.Overhead()
n = copy(b, c.readBuf[c.readOffset:dataSize])
c.readOffset += n
if c.readOffset == dataSize {
c.readOffset = 0
}
return
}
// read from conn
dataSize := 0
{ //read data size
c.readBuf = make([]byte, 2+c.aead.Overhead())
if _, err = io.ReadFull(c.Conn, c.readBuf); err != nil {
return
}
if _, err = c.aead.Open(c.readBuf[:0], c.decryptNonce(), c.readBuf, nil); err != nil {
return
}
dataSize = int(c.readBuf[0])<<8 + int(c.readBuf[1])
}
{ // read data
c.readBuf = make([]byte, dataSize+c.aead.Overhead())
if _, err = io.ReadFull(c.Conn, c.readBuf); err != nil {
return
}
if _, err = c.aead.Open(c.readBuf[:0], c.decryptNonce(), c.readBuf, nil); err != nil {
return
}
}
// buffer extra data
if n = copy(b, c.readBuf[:dataSize]); n < dataSize {
c.readOffset = n
}
return
}
func (c *conn) Write(b []byte) (n int, err error) {
bLen := len(b)
dataSize := MAX_SIZE - (2 + c.aead.Overhead()) - c.aead.Overhead()
if bLen < c.maxSize {
dataSize = bLen
}
// BigEndian
c.writeBuf[0], c.writeBuf[1] = byte((dataSize&0xFF00)>>8), byte(dataSize&0xFF)
c.aead.Seal(c.writeBuf[:0], c.encryptNonce(), c.writeBuf[:2], nil)
c.aead.Seal(c.writeBuf[:2+c.aead.Overhead()], c.encryptNonce(), b[:dataSize], nil)
_, err = c.Conn.Write(c.writeBuf[:dataSize+(2+c.aead.Overhead())+c.aead.Overhead()])
if err != nil {
return 0, err
}
return dataSize, err
}
func Shadow(c net.Conn, cipher, password string) net.Conn {
aead, err := pickCipher(cipher, password)
if err != nil {
panic(err)
}
return &conn{
maxSize: MAX_SIZE - (2 - aead.Overhead()) - aead.Overhead(),
aead: aead,
encryptNonce: newNonce(password, aead.NonceSize()),
decryptNonce: newNonce(password, aead.NonceSize()),
writeBuf: make([]byte, 0xFFFF),
Conn: c,
}
}
func newNonce(password string, size int) func() []byte {
num, _ := binary.Varint([]byte(password))
rnd := rand.New(rand.NewSource(num))
buf := make([]byte, size)
return func() []byte {
rnd.Read(buf)
return buf
}
}
+215
View File
@@ -0,0 +1,215 @@
<svg host="65bd71144e" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="351px" height="491px" viewBox="-0.5 -0.5 351 491" content="&lt;mxfile&gt;&lt;diagram id=&quot;u2y3ysnPqCVZKoHCRF4D&quot; name=&quot;第 1 页&quot;&gt;3VlNc9owEP01HJPxB/7gGCBtD2mbGdppcxRY2GpsyyOLAv31XSEZW5ZJKHEIzSWR1itZem/1dmUG7iTbfGSoSD7TCKcDx4o2A3c6cJyRH8JfYdhKQxAE0hAzEkmTXRtm5A9WRktZVyTCpebIKU05KXTjguY5XnDNhhija91tSVP9rQWKsWGYLVBqWn+QiCfSGjpBbf+ESZxUb7b9kXySocpZ7aRMUETXDZN7O3AnjFIuW9lmglOBXYWLHPfhwNP9whjO+TEDxDMx4jdKV2pzamF8W+2W0VUeYTHAGrjjdUI4nhVoIZ6ugV6wJTxLoWdDU02HGcebg2uy9zuFCME0w5xtwUUNcDwFjooOO1D9dY21W/kkDZyHI2VEit94P3cNATQUCt2IDA1APn37dj8zUMERhITqUsYTGtMcpbe1dazjVvvcUVootH5hzrcqvtGKUx1LwIttf4rx117VfWg+m27U5LK3Vb0DgMPy6Yot1PoVzxyxGCsvV5rEzp7kieEUcfJbPwwvwdw2QS/p4rH0wLYqSs4wyt6EgA3hEn/HU92HCnFo1/CLzino9we1GnpPCby1PkqhfpRcv3VCJP1qVE3YDWNo23ArhEN5+D2u1/2emn85Yx0N+z0eFyD+29If/J/0e/556N8L72vRHxj6EBEmMjvYSEl4Z3TcoTmUHRqjKCVxDu0FkIMZGESiIpDYb9SDjESRDB5ckj9ovptP0KoggMm98cCbdhPtH8x+qhJRE2rJvisFWteW7QcapldqplNDonKhy2WJeYuefyXErBpKugZAHT8FLMZz0Yr5DqS2ha1SKN1gPXkkKkDMJY1zVGJhgEMrmOmhBuk4ggYxh2uQlnAObbMG2fs0axDb6qEGscO3kLujlKv3GuGAojg6/k4b1gPKdYq4jN5XbjkXQ6OzMeR2q010cSrhDI9UibAHkQguViNUGmxeLV6n4PnXOsWzPI0vz23djp9RoZb/i+uasCOw5bXHR5kI13xein+wKBhIYrxbXMHoZnsZFU9Vl7244Lmyrl3bt/WKZ/iyiqdaUGtELwWQbzA3Fp+VdiWQRh04fQdzafBVJqgQzVWW3iw4bVKzo/GeQl1LqKBoTjmnWQd3nLY0DYqnlOR4sv/u9WTWOF7o2mK/v1g0P8l06Fz7vnFSMWR+kTprfj4+PYvOPWYEtiiO2vEpe3QuwXxe4ZxX+kDgXpby2mZNMf0yq+4fgotE/M2FUFjLlK7Nc30J+ludjV4EeBiEoQb6Jd04RwZfX4Ei9j7k1W1H+xnltXp1A9klehTBDwBhA1fYEX+9CBc6XEpJFlOXnNFHgDsV/E1zmuOe0lnru5gdmnj7HXi3xfEkvG0Db6X77xlxN/SeRTzoB3Ho1r+fSaWpf4R0b/8C&lt;/diagram&gt;&lt;/mxfile&gt;">
<defs/>
<g>
<rect x="0" y="0" width="350" height="490" fill="#ffffff" stroke="#000000" pointer-events="all"/>
<path d="M 170 240 L 170 156.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 170 151.12 L 173.5 158.12 L 170 156.37 L 166.5 158.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 195px; margin-left: 170px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
HTTPS
</div>
</div>
</div>
</foreignObject>
<text x="170" y="198" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
HTTPS
</text>
</switch>
</g>
<path d="M 100 240 L 100 190 L 36.37 190" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 31.12 190 L 38.12 186.5 L 36.37 190 L 38.12 193.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 190px; margin-left: 90px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
socks5 upstream
</div>
</div>
</div>
</foreignObject>
<text x="90" y="193" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
socks5 upstream
</text>
</switch>
</g>
<path d="M 240 240 L 240 190 L 303.63 190" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 308.88 190 L 301.88 193.5 L 303.63 190 L 301.88 186.5 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 192px; margin-left: 252px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
direct visit
</div>
</div>
</div>
</foreignObject>
<text x="252" y="195" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
direct visit
</text>
</switch>
</g>
<rect x="30" y="240" width="280" height="100" fill="#ffffff" stroke="#000000" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 278px; height: 1px; padding-top: 290px; margin-left: 31px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
sower
<br/>
<br/>
rules and detect based router
</div>
</div>
</div>
</foreignObject>
<text x="170" y="294" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
sower...
</text>
</switch>
</g>
<path d="M 170 70 L 170 36.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 170 31.12 L 173.5 38.12 L 170 36.37 L 166.5 38.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<path d="M 240 70 L 240 36.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 240 31.12 L 243.5 38.12 L 240 36.37 L 236.5 38.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<rect x="30" y="70" width="280" height="80" fill="#ffffff" stroke="#000000" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 278px; height: 1px; padding-top: 110px; margin-left: 31px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: normal; word-wrap: normal; ">
sowerd
</div>
</div>
</div>
</foreignObject>
<text x="170" y="114" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
sowerd
</text>
</switch>
</g>
<path d="M 255 390 L 255 360 L 170 360 L 170 346.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 170 341.12 L 173.5 348.12 L 170 346.37 L 166.5 348.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 357px; margin-left: 251px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
socks5 intelligent proxy
</div>
</div>
</div>
</foreignObject>
<text x="251" y="360" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
socks5 intelligent proxy
</text>
</switch>
</g>
<ellipse cx="255" cy="397.5" rx="7.5" ry="7.5" fill="#ffffff" stroke="#000000" pointer-events="all"/>
<path d="M 255 405 L 255 430 M 255 410 L 240 410 M 255 410 L 270 410 M 255 430 L 240 450 M 255 430 L 270 450" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 457px; margin-left: 255px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
Browser  Users
</div>
</div>
</div>
</foreignObject>
<text x="255" y="469" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Brows...
</text>
</switch>
</g>
<path d="M 85 390 L 85 360 L 170 360 L 170 346.37" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="stroke"/>
<path d="M 170 341.12 L 173.5 348.12 L 170 346.37 L 166.5 348.12 Z" fill="#000000" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 360px; margin-left: 91px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 11px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; background-color: #ffffff; white-space: nowrap; ">
DNS route the net flow
</div>
</div>
</div>
</foreignObject>
<text x="91" y="363" fill="#000000" font-family="Helvetica" font-size="11px" text-anchor="middle">
DNS route the net flow
</text>
</switch>
</g>
<ellipse cx="85" cy="397.5" rx="7.5" ry="7.5" fill="#ffffff" stroke="#000000" pointer-events="all"/>
<path d="M 85 405 L 85 430 M 85 410 L 70 410 M 85 410 L 100 410 M 85 430 L 70 450 M 85 430 L 100 450" fill="none" stroke="#000000" stroke-miterlimit="10" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe flex-start; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 457px; margin-left: 85px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
Other Users
</div>
</div>
</div>
</foreignObject>
<text x="85" y="469" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
Other...
</text>
</switch>
</g>
<rect x="210" y="10" width="60" height="20" fill="none" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 20px; margin-left: 240px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
fake site
</div>
</div>
</div>
</foreignObject>
<text x="240" y="24" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
fake site
</text>
</switch>
</g>
<rect x="135" y="10" width="70" height="20" fill="none" stroke="none" pointer-events="all"/>
<g transform="translate(-0.5 -0.5)">
<switch>
<foreignObject style="overflow: visible; text-align: left;" pointer-events="none" width="100%" height="100%" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: flex; align-items: unsafe center; justify-content: unsafe center; width: 1px; height: 1px; padding-top: 20px; margin-left: 170px;">
<div style="box-sizing: border-box; font-size: 0; text-align: center; ">
<div style="display: inline-block; font-size: 12px; font-family: Helvetica; color: #000000; line-height: 1.2; pointer-events: all; white-space: nowrap; ">
target site
</div>
</div>
</div>
</foreignObject>
<text x="170" y="24" fill="#000000" font-family="Helvetica" font-size="12px" text-anchor="middle">
target site
</text>
</switch>
</g>
</g>
<switch>
<g requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"/>
<a transform="translate(0,-5)" xlink:href="https://www.diagrams.net/doc/faq/svg-export-text-problems" target="_blank">
<text text-anchor="middle" font-size="10px" x="50%" y="100%">
Viewer does not support full SVG 1.1
</text>
</a>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 18 KiB

+137
View File
@@ -0,0 +1,137 @@
package socks5
import (
"encoding/binary"
"io"
"net"
)
// https://tools.ietf.org/html/rfc1928
// 1. client send auth request
type authReq struct {
VER byte
NMETHODS uint8
METHODS []byte
}
func (req *authReq) Fulfill(r io.Reader) error {
buf := make([]byte, 2)
if n, err := io.ReadFull(r, buf); err != nil || n != 2 {
return err
}
req.VER = buf[0]
req.NMETHODS = buf[1]
req.METHODS = make([]byte, int(req.NMETHODS))
if n, err := io.ReadFull(r, req.METHODS); err != nil || n != len(req.METHODS) {
return err
}
return nil
}
func (r *authReq) IsValid() bool {
if r.VER != 5 || len(r.METHODS) == 0 {
return false
}
for _, method := range r.METHODS {
if method == 0 {
return true
}
}
return false
}
// 2. server response auth request
type authResp struct {
VER byte
METHOD byte
}
// 3. client request with target address
type reqHead struct {
VER byte
CMD byte
RSV byte
ATYP byte
}
func (r *reqHead) IsValid() bool {
return r.VER == 5 && r.CMD == 1 && r.RSV == 0
}
// 4. server response with the address that assigned to connect to target address
type respHead struct {
VER byte
REP byte
RSV byte
ATYP byte
BND_ADDR [net.IPv4len]byte
BND_PORT uint16
}
type addrType interface {
Fulfill(r io.Reader) error
Addr() (domain string, port uint16)
}
// ATYP:
//
// 0x01 -> net.IPv4len
// 0x03 -> first byte is length
// 0x04 -> net.IPv6len
type addrTypeIPv4 struct {
DST_ADDR [4]byte
DST_PORT uint16
}
func (a *addrTypeIPv4) Fulfill(r io.Reader) error {
return binary.Read(r, binary.BigEndian, a)
}
func (a *addrTypeIPv4) Addr() (string, uint16) {
return net.IP(a.DST_ADDR[:]).String(), a.DST_PORT
}
type addrTypeDomain struct {
DST_ADDR_LEN uint8
DST_ADDR []byte
DST_PORT uint16
}
func (a *addrTypeDomain) Fulfill(r io.Reader) error {
buf := make([]byte, 1)
if _, err := io.ReadFull(r, buf); err != nil {
return err
}
a.DST_ADDR_LEN = uint8(buf[0])
buf = make([]byte, a.DST_ADDR_LEN+2)
if _, err := io.ReadFull(r, buf); err != nil {
return err
}
a.DST_ADDR = buf[:int(a.DST_ADDR_LEN)]
a.DST_PORT = binary.BigEndian.Uint16(buf[int(a.DST_ADDR_LEN):])
return nil
}
func (a *addrTypeDomain) Addr() (string, uint16) {
return string(a.DST_ADDR[:]), a.DST_PORT
}
type addrTypeIPv6 struct {
DST_ADDR [16]byte
DST_PORT uint16
}
func (a *addrTypeIPv6) Fulfill(r io.Reader) error {
return binary.Read(r, binary.BigEndian, a)
}
func (a *addrTypeIPv6) Addr() (string, uint16) {
return net.IP(a.DST_ADDR[:]).String(), a.DST_PORT
}
+161
View File
@@ -0,0 +1,161 @@
package socks5
import (
"bytes"
"encoding/binary"
"fmt"
"net"
"strconv"
"errors"
)
type AddrHead struct {
addrType
}
func (h *AddrHead) Network() string { return "tcp" }
func (h *AddrHead) String() string {
host, port := h.Addr()
return net.JoinHostPort(host, strconv.Itoa(int(port)))
}
// Socks5 is a SOCKS5 proxy. It implements the teeconn.Conn interface.
// It is used to be a second relay of other proxy tools.
// user -> sower -socks5-> third-party proxy -> target
type Socks5 struct{}
func New() *Socks5 {
return &Socks5{}
}
const (
RepSucceeded = 0x00
RepGeneralFailure = 0x01
RepConnectionNotAllowed = 0x02
)
var (
noAuthResp = authResp{VER: 5, METHOD: 0}
)
func (s *Socks5) Unwrap(conn net.Conn) (net.Addr, error) {
addr, err := s.ReadRequest(conn)
if err != nil {
return nil, err
}
if err := s.WriteReply(conn, RepSucceeded); err != nil {
return nil, fmt.Errorf("write head: %w", err)
}
return addr, nil
}
func (s *Socks5) ReadRequest(conn net.Conn) (net.Addr, error) {
{ // auth
auth := new(authReq)
if err := auth.Fulfill(conn); err != nil || !auth.IsValid() {
return nil, fmt.Errorf("read auth head: %v, err: %v", auth, err)
}
if err := binary.Write(conn, binary.BigEndian, noAuthResp); err != nil {
return nil, fmt.Errorf("write auth: %w", err)
}
}
var addr addrType
{ // head
head := new(reqHead)
if err := binary.Read(conn, binary.BigEndian, head); err != nil || !head.IsValid() {
return nil, fmt.Errorf("read head: %v, err: %v", head, err)
}
switch head.ATYP {
case 0x01: // IPv4
addr = &addrTypeIPv4{}
case 0x03: // domain name
addr = &addrTypeDomain{}
case 0x04: // IPv6
addr = &addrTypeIPv6{}
default:
return nil, errors.New("invalid ATYP")
}
if err := addr.Fulfill(conn); err != nil {
return nil, fmt.Errorf("read target: %w", err)
}
}
return &AddrHead{
addrType: addr,
}, nil
}
func (s *Socks5) WriteReply(conn net.Conn, rep byte) error {
head := respHead{VER: 5, REP: rep, RSV: 0, ATYP: 1}
return binary.Write(conn, binary.BigEndian, head)
}
var noAuthReq = struct {
VER byte
NMETHODS uint8
METHODS byte
}{5, 1, 0}
var domainHead = reqHead{VER: 5, CMD: 1, RSV: 0, ATYP: 3}
func (s *Socks5) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
if len(tgtHost) > 255 {
return fmt.Errorf("target host too long: %d", len(tgtHost))
}
{ // auth
if err := binary.Write(conn, binary.BigEndian, &noAuthReq); err != nil {
return err
}
resp := &authResp{}
if err := binary.Read(conn, binary.BigEndian, resp); err != nil {
return err
}
if resp.VER != 5 || resp.METHOD != 0 {
return fmt.Errorf("unexpected auth response: %+v", resp)
}
}
{ // head
buf := bytes.NewBuffer(make([]byte, 0, binary.Size(domainHead)+1+len(tgtHost)+2))
_ = binary.Write(buf, binary.BigEndian, domainHead)
buf.WriteByte(uint8(len(tgtHost)))
buf.WriteString(tgtHost)
buf.Write([]byte{byte(tgtPort >> 8), byte(tgtPort)})
if n, err := conn.Write(buf.Bytes()); err != nil || n != len(buf.Bytes()) {
return fmt.Errorf("n: %d, err: %v", n, err)
}
head := reqHead{}
if err := binary.Read(conn, binary.BigEndian, &head); err != nil {
return err
}
if head.VER != 5 || head.RSV != 0 {
return fmt.Errorf("unexpected response head: %+v", head)
}
if head.CMD != 0 {
return fmt.Errorf("connect rejected, rep=%d", head.CMD)
}
var addr addrType
switch head.ATYP {
case 0x01:
addr = &addrTypeIPv4{}
case 0x03:
addr = &addrTypeDomain{}
case 0x04:
addr = &addrTypeIPv6{}
default:
return fmt.Errorf("invalid response ATYP: %d", head.ATYP)
}
if err := addr.Fulfill(conn); err != nil {
return fmt.Errorf("read response address: %w", err)
}
}
return nil
}
+124
View File
@@ -0,0 +1,124 @@
package socks5
import (
"bytes"
"encoding/binary"
"io"
"net"
"testing"
"time"
)
type mockConn struct {
reader io.Reader
writes bytes.Buffer
}
func newMockConn(data []byte) *mockConn {
return &mockConn{reader: bytes.NewReader(data)}
}
func (c *mockConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
func (c *mockConn) Write(p []byte) (int, error) { return c.writes.Write(p) }
func (c *mockConn) Close() error { return nil }
func (c *mockConn) LocalAddr() net.Addr { return dummyAddr("local") }
func (c *mockConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
func (c *mockConn) SetDeadline(time.Time) error { return nil }
func (c *mockConn) SetReadDeadline(time.Time) error { return nil }
func (c *mockConn) SetWriteDeadline(time.Time) error { return nil }
type dummyAddr string
func (a dummyAddr) Network() string { return "tcp" }
func (a dummyAddr) String() string { return string(a) }
func TestUnwrapRejectsZeroMethodsWithoutPanic(t *testing.T) {
conn := newMockConn([]byte{0x05, 0x00})
if _, err := New().Unwrap(conn); err == nil {
t.Fatal("expected error for zero auth methods")
}
}
func TestUnwrapAcceptsNoAuthWhenNotFirstMethod(t *testing.T) {
req := []byte{
0x05, 0x02, 0x02, 0x00,
0x05, 0x01, 0x00, 0x03,
0x0b,
}
req = append(req, []byte("example.com")...)
req = append(req, 0x01, 0xbb)
addr, err := New().Unwrap(newMockConn(req))
if err != nil {
t.Fatalf("unwrap failed: %v", err)
}
host, port := addr.(*AddrHead).Addr()
if host != "example.com" || port != 443 {
t.Fatalf("unexpected addr: %s:%d", host, port)
}
}
func TestWrapRejectsAuthFailure(t *testing.T) {
server, client := net.Pipe()
defer server.Close()
defer client.Close()
done := make(chan struct{})
go func() {
defer close(done)
buf := make([]byte, 3)
if _, err := io.ReadFull(server, buf); err != nil {
return
}
_, _ = server.Write([]byte{0x05, 0xff})
}()
if err := New().Wrap(client, "example.com", 443); err == nil {
t.Fatal("expected auth failure")
}
<-done
}
func TestWrapRejectsConnectFailure(t *testing.T) {
server, client := net.Pipe()
defer server.Close()
defer client.Close()
done := make(chan struct{})
go func() {
defer close(done)
auth := make([]byte, 3)
if _, err := io.ReadFull(server, auth); err != nil {
return
}
_, _ = server.Write([]byte{0x05, 0x00})
if _, err := io.ReadFull(server, make([]byte, 4)); err != nil {
return
}
var nameLen [1]byte
if _, err := io.ReadFull(server, nameLen[:]); err != nil {
return
}
if _, err := io.ReadFull(server, make([]byte, int(nameLen[0])+2)); err != nil {
return
}
resp := bytes.NewBuffer(nil)
_ = binary.Write(resp, binary.BigEndian, reqHead{VER: 5, CMD: 5, RSV: 0, ATYP: 1})
_, _ = server.Write(resp.Bytes())
}()
if err := New().Wrap(client, "example.com", 443); err == nil {
t.Fatal("expected connect failure")
}
<-done
}
func TestWrapRejectsTooLongHost(t *testing.T) {
if err := New().Wrap(newMockConn(nil), string(bytes.Repeat([]byte{'a'}, 256)), 443); err == nil {
t.Fatal("expected error for too long host")
}
}
+90
View File
@@ -0,0 +1,90 @@
package sower
import (
"bytes"
"crypto/md5"
"encoding/binary"
"fmt"
"io"
"net"
"strconv"
"errors"
)
// https://en.wikipedia.org/wiki/Domain_Name_System
const maxDomainLength = 253
var headSize = binary.Size(new(Head))
// action(>=0x80) + checksum + port + target + data
// data(HTTP, first byte < 0x7F)
type Head struct {
Cmd byte
Checksum uint64
Port uint16
TgtAddr [maxDomainLength]byte
}
func (h *Head) Network() string { return "tcp" }
func (h *Head) String() string {
idx := bytes.IndexByte(h.TgtAddr[:], 0)
if idx < 0 {
idx = len(h.TgtAddr)
}
addr := string(h.TgtAddr[:idx])
return net.JoinHostPort(addr, strconv.Itoa(int(h.Port)))
}
type Sower struct {
password []byte
}
func New(password string) *Sower {
return &Sower{
password: []byte(password),
}
}
func (s *Sower) Unwrap(conn net.Conn) (net.Addr, error) {
buf := make([]byte, headSize)
if n, err := io.ReadFull(conn, buf); err != nil || n != headSize {
return nil, fmt.Errorf("n: %d, err: %v", n, err)
}
h := &Head{}
_ = binary.Read(bytes.NewReader(buf), binary.BigEndian, h)
switch h.Cmd {
case 0x80:
default:
return nil, fmt.Errorf("invalid command: %d", h.Cmd)
}
if h.Checksum != sumChecksum(h.TgtAddr, s.password) {
return nil, errors.New("auth fail")
}
return h, nil
}
func (s *Sower) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
if len(tgtHost) > maxDomainLength {
return fmt.Errorf("target host too long: %d", len(tgtHost))
}
tgtAddr := [maxDomainLength]byte{}
copy(tgtAddr[:len(tgtHost)], []byte(tgtHost))
return binary.Write(conn, binary.BigEndian, &Head{
Cmd: 0x80,
Checksum: sumChecksum(tgtAddr, s.password),
Port: tgtPort,
TgtAddr: tgtAddr,
})
}
func sumChecksum(target [maxDomainLength]byte, password []byte) uint64 {
checksum := md5.Sum(append(target[:], password...))
checksumVal, _ := binary.Uvarint(checksum[:])
return checksumVal
}
+119
View File
@@ -0,0 +1,119 @@
package sower
import (
"bytes"
"encoding/binary"
"io"
"net"
"strings"
"testing"
"time"
)
type chunkConn struct {
reader io.Reader
writes bytes.Buffer
}
func newChunkConn(data []byte, chunkSize int) *chunkConn {
return &chunkConn{
reader: &io.LimitedReader{
R: &chunkReader{
data: data,
chunk: chunkSize,
},
N: int64(len(data)),
},
}
}
func (c *chunkConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
func (c *chunkConn) Write(p []byte) (int, error) { return c.writes.Write(p) }
func (c *chunkConn) Close() error { return nil }
func (c *chunkConn) LocalAddr() net.Addr { return dummyAddr("local") }
func (c *chunkConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
func (c *chunkConn) SetDeadline(time.Time) error { return nil }
func (c *chunkConn) SetReadDeadline(time.Time) error { return nil }
func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
type chunkReader struct {
data []byte
chunk int
pos int
}
func (r *chunkReader) Read(p []byte) (int, error) {
if r.pos >= len(r.data) {
return 0, io.EOF
}
n := r.chunk
if n > len(p) {
n = len(p)
}
remain := len(r.data) - r.pos
if n > remain {
n = remain
}
copy(p, r.data[r.pos:r.pos+n])
r.pos += n
return n, nil
}
type dummyAddr string
func (a dummyAddr) Network() string { return "tcp" }
func (a dummyAddr) String() string { return string(a) }
func TestWrapRejectsTooLongHost(t *testing.T) {
err := New("123").Wrap(newChunkConn(nil, 1), strings.Repeat("a", maxDomainLength+1), 443)
if err == nil {
t.Fatal("expected error for too long host")
}
}
func TestRoundTripWithMaxLengthHost(t *testing.T) {
host := strings.Repeat("a", maxDomainLength)
server, client := net.Pipe()
defer server.Close()
defer client.Close()
done := make(chan error, 1)
go func() {
done <- New("123").Wrap(client, host, 443)
}()
addr, err := New("123").Unwrap(server)
if err != nil {
t.Fatalf("unwrap failed: %v", err)
}
if got := addr.String(); got != net.JoinHostPort(host, "443") {
t.Fatalf("unexpected addr: %s", got)
}
if err := <-done; err != nil {
t.Fatalf("wrap failed: %v", err)
}
}
func TestUnwrapReadsFullHeader(t *testing.T) {
target := [maxDomainLength]byte{}
copy(target[:], "example.com")
buf := bytes.NewBuffer(nil)
if err := binary.Write(buf, binary.BigEndian, &Head{
Cmd: 0x80,
Checksum: sumChecksum(target, []byte("123")),
Port: 443,
TgtAddr: target,
}); err != nil {
t.Fatalf("build header: %v", err)
}
addr, err := New("123").Unwrap(newChunkConn(buf.Bytes(), 7))
if err != nil {
t.Fatalf("unwrap failed: %v", err)
}
if got := addr.String(); got != "example.com:443" {
t.Fatalf("unexpected addr: %s", got)
}
}
+10
View File
@@ -0,0 +1,10 @@
package transport
import (
"net"
)
type Transport interface {
Unwrap(conn net.Conn) (net.Addr, error)
Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error
}
+41
View File
@@ -0,0 +1,41 @@
package transport
import (
"net"
"strings"
"testing"
"github.com/sower-proxy/conns/reread"
"github.com/sower-proxy/sower/transport/sower"
"github.com/sower-proxy/sower/transport/trojan"
)
func testPipe(tran Transport) (net.Addr, error) {
r, w := net.Pipe()
defer r.Close()
go func(w net.Conn) {
defer w.Close()
tran.Wrap(w, "sower", 443)
}(w)
return tran.Unwrap(reread.New(r))
}
func Test_Transports(t *testing.T) {
if addr, err := testPipe(newSower()); err != nil || strings.TrimSpace(addr.String()) != "sower:443" {
t.Errorf("test sower, unexpected address: %s, err: %s", addr, err)
}
if addr, err := testPipe(newTrojan()); err != nil || strings.TrimSpace(addr.String()) != "sower:443" {
t.Errorf("test trojan, unexpected address: %s, err: %s", addr, err)
}
}
func newSower() *sower.Sower {
return sower.New("123")
}
func newTrojan() *trojan.Trojan {
return trojan.New("123")
}
+213
View File
@@ -0,0 +1,213 @@
package trojan
import (
"bytes"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"net"
"strconv"
"errors"
)
// +-----------------------+---------+----------------+---------+----------+
// | hex(SHA224(password)) | CRLF | Trojan Request | CRLF | Payload |
// +-----------------------+---------+----------------+---------+----------+
// | 56 | X'0D0A' | Variable | X'0D0A' | Variable |
// +-----------------------+---------+----------------+---------+----------+
// +-----+------+----------+----------+
// | CMD | ATYP | DST.ADDR | DST.PORT |
// +-----+------+----------+----------+
// | 1 | 1 | Variable | 2 |
// +-----+------+----------+----------+
// o CMD
// o CONNECT X'01'
// o UDP :X'03'
// o ATYP
// o IP V4 : X'01'
// o domain: X'03'
// o IP V6 : X'04'
const headLen = 56 + 2 + 1 + 1
type staticHead struct {
Passwd [56]byte
CRLF [2]byte
CMD uint8
ATYP uint8
}
type ipv4Addr struct {
ADDR [4]byte
PORT uint16
CRLF [2]byte
}
func (*ipv4Addr) Network() string { return "tcp" }
func (a *ipv4Addr) String() string {
return net.JoinHostPort(net.IP(a.ADDR[:]).String(), strconv.Itoa(int(a.PORT)))
}
func (a *ipv4Addr) IsValid() bool {
return a.CRLF == [2]byte{0x0D, 0x0A}
}
type ipv6Addr struct {
ADDR [16]byte
PORT uint16
CRLF [2]byte
}
func (*ipv6Addr) Network() string { return "tcp" }
func (a *ipv6Addr) String() string {
return net.JoinHostPort(net.IP(a.ADDR[:]).String(), strconv.Itoa(int(a.PORT)))
}
func (a *ipv6Addr) IsValid() bool {
return a.CRLF == [2]byte{0x0D, 0x0A}
}
type domain struct {
ADDR string
PORT uint16
CRLF [2]byte
}
func (*domain) Network() string { return "tcp" }
func (a *domain) String() string {
return net.JoinHostPort(a.ADDR, strconv.Itoa(int(a.PORT)))
}
func (a *domain) Fulfill(r io.Reader) error {
buf := make([]byte, 1)
if n, err := io.ReadFull(r, buf); err != nil || n != 1 {
return errors.New("read domain length failed")
}
addrLen := int(buf[0])
buf = make([]byte, addrLen+4)
if n, err := io.ReadFull(r, buf); err != nil || n != addrLen+4 {
return fmt.Errorf("read domain: %w", err)
}
a.ADDR = string(buf[:addrLen])
a.PORT = uint16(buf[addrLen])<<8 + uint16(buf[addrLen+1])
a.CRLF = [2]byte{buf[addrLen+2], buf[addrLen+3]}
return nil
}
func (a *domain) IsValid() bool {
return a.CRLF == [2]byte{0x0D, 0x0A}
}
type Trojan struct {
headPasswd []byte
headIPv4 []byte
headIPv6 []byte
headDomain []byte
}
func New(password string) *Trojan {
t := &Trojan{
headPasswd: make([]byte, 56),
}
passSum := sha256.Sum224([]byte(password))
hex.Encode(t.headPasswd, passSum[:])
t.headIPv4 = append(t.headPasswd, 0x0D, 0x0A, 0x01, 0x01)
t.headIPv6 = append(t.headPasswd, 0x0D, 0x0A, 0x01, 0x04)
t.headDomain = append(t.headPasswd, 0x0D, 0x0A, 0x01, 0x03)
return t
}
func (t *Trojan) Unwrap(conn net.Conn) (net.Addr, error) {
buf := make([]byte, headLen)
if n, err := io.ReadFull(conn, buf); err != nil || n != headLen {
return nil, fmt.Errorf("n: %d, err: %v", n, err)
}
head := &staticHead{}
if err := binary.Read(bytes.NewBuffer(buf), binary.BigEndian, head); err != nil {
return nil, fmt.Errorf("read head: %w", err)
}
if !bytes.Equal(head.Passwd[:], []byte(t.headPasswd)) {
return nil, errors.New("auth fail")
}
if head.CRLF != [2]byte{0x0D, 0x0A} {
return nil, errors.New("invalid CRLF")
}
if head.CMD != 0x01 {
return nil, fmt.Errorf("invalid CMD: %d", head.CMD)
}
switch head.ATYP {
case 0x01: // ipv4
addr := &ipv4Addr{}
err := binary.Read(conn, binary.BigEndian, addr)
if err != nil {
return nil, fmt.Errorf("read addr: %w", err)
}
if !addr.IsValid() {
return nil, errors.New("invalid CRLF")
}
return addr, nil
case 0x04: // ipv6
addr := &ipv6Addr{}
err := binary.Read(conn, binary.BigEndian, addr)
if err != nil {
return nil, fmt.Errorf("read addr: %w", err)
}
if !addr.IsValid() {
return nil, errors.New("invalid CRLF")
}
return addr, nil
case 0x03: // domain
addr := &domain{}
err := addr.Fulfill(conn)
if err != nil {
return nil, fmt.Errorf("read addr: %w", err)
}
if !addr.IsValid() {
return nil, errors.New("invalid CRLF")
}
return addr, nil
default:
return nil, errors.New("invalid ATYP")
}
}
func (t *Trojan) Wrap(conn net.Conn, tgtHost string, tgtPort uint16) error {
buf := bytes.NewBuffer(make([]byte, 0, headLen+1+len(tgtHost)+4))
ip := net.ParseIP(tgtHost)
switch {
case len(ip.To4()) != 0:
buf.Write(t.headIPv4)
buf.Write([]byte(ip.To4()))
case len(ip) != 0:
buf.Write(t.headIPv6)
buf.Write([]byte(ip.To16()))
default:
if len(tgtHost) > 255 {
return fmt.Errorf("target host too long: %d", len(tgtHost))
}
buf.Write(t.headDomain)
buf.WriteByte(byte(len(tgtHost)))
buf.WriteString(tgtHost)
}
buf.Write([]byte{byte(tgtPort >> 8), byte(tgtPort), 0x0D, 0x0A})
if n, err := conn.Write(buf.Bytes()); err != nil || n != len(buf.Bytes()) {
return fmt.Errorf("n: %d, err: %v", n, err)
}
return nil
}
+112
View File
@@ -0,0 +1,112 @@
package trojan
import (
"bytes"
"io"
"net"
"strings"
"testing"
"time"
)
type chunkConn struct {
reader io.Reader
writes bytes.Buffer
}
func newChunkConn(data []byte, chunkSize int) *chunkConn {
return &chunkConn{
reader: &chunkReader{
data: data,
chunk: chunkSize,
},
}
}
func (c *chunkConn) Read(p []byte) (int, error) { return c.reader.Read(p) }
func (c *chunkConn) Write(p []byte) (int, error) { return c.writes.Write(p) }
func (c *chunkConn) Close() error { return nil }
func (c *chunkConn) LocalAddr() net.Addr { return dummyAddr("local") }
func (c *chunkConn) RemoteAddr() net.Addr { return dummyAddr("remote") }
func (c *chunkConn) SetDeadline(time.Time) error { return nil }
func (c *chunkConn) SetReadDeadline(time.Time) error { return nil }
func (c *chunkConn) SetWriteDeadline(time.Time) error { return nil }
type chunkReader struct {
data []byte
chunk int
pos int
}
func (r *chunkReader) Read(p []byte) (int, error) {
if r.pos >= len(r.data) {
return 0, io.EOF
}
n := r.chunk
if n > len(p) {
n = len(p)
}
remain := len(r.data) - r.pos
if n > remain {
n = remain
}
copy(p, r.data[r.pos:r.pos+n])
r.pos += n
return n, nil
}
type dummyAddr string
func (a dummyAddr) Network() string { return "tcp" }
func (a dummyAddr) String() string { return string(a) }
func TestWrapRejectsTooLongHost(t *testing.T) {
err := New("123").Wrap(newChunkConn(nil, 1), strings.Repeat("a", 256), 443)
if err == nil {
t.Fatal("expected error for too long host")
}
}
func TestUnwrapReadsFullHeader(t *testing.T) {
conn := newChunkConn(nil, 1)
if err := New("123").Wrap(conn, "example.com", 443); err != nil {
t.Fatalf("wrap failed: %v", err)
}
addr, err := New("123").Unwrap(newChunkConn(conn.writes.Bytes(), 5))
if err != nil {
t.Fatalf("unwrap failed: %v", err)
}
if got := addr.String(); got != "example.com:443" {
t.Fatalf("unexpected addr: %s", got)
}
}
func TestUnwrapRejectsInvalidCommand(t *testing.T) {
conn := newChunkConn(nil, 1)
if err := New("123").Wrap(conn, "example.com", 443); err != nil {
t.Fatalf("wrap failed: %v", err)
}
data := append([]byte(nil), conn.writes.Bytes()...)
data[58] = 0x03
if _, err := New("123").Unwrap(newChunkConn(data, len(data))); err == nil {
t.Fatal("expected invalid command error")
}
}
func TestUnwrapRejectsInvalidCRLF(t *testing.T) {
conn := newChunkConn(nil, 1)
if err := New("123").Wrap(conn, "example.com", 443); err != nil {
t.Fatalf("wrap failed: %v", err)
}
data := append([]byte(nil), conn.writes.Bytes()...)
data[len(data)-1] = 0x00
if _, err := New("123").Unwrap(newChunkConn(data, len(data))); err == nil {
t.Fatal("expected invalid CRLF error")
}
}
-91
View File
@@ -1,91 +0,0 @@
package util
import (
"math/rand"
"net"
"runtime"
"github.com/golang/glog"
"github.com/wweir/netboot/dhcp4"
)
var xid = make([]byte, 4)
func GetDefaultDNSServer() string {
pack := &dhcp4.Packet{
Type: dhcp4.MsgDiscover,
Broadcast: true,
}
options := map[dhcp4.Option][]byte{
dhcp4.OptRequestedOptions: []byte{byte(dhcp4.OptDNSServers)},
}
NewConn := dhcp4.NewConn
if runtime.GOOS == "linux" {
NewConn = dhcp4.NewSnooperConn
}
ifaces := mustGetInterfaces()
for _, iface := range ifaces {
conn, err := NewConn(iface.IP.String() + ":68")
if err != nil { // maybe in use
glog.V(1).Infoln(err)
continue
}
defer conn.Close()
rand.Read(xid)
pack.TransactionID = xid
pack.HardwareAddr = iface.Interface.HardwareAddr
options[dhcp4.OptClientIdentifier] = iface.Interface.HardwareAddr
pack.Options = dhcp4.Options(options)
if err := conn.SendDHCP(pack, iface.Interface); err != nil {
glog.Errorln(err)
continue
}
pack, _, err = conn.RecvDHCP()
if err != nil {
glog.Errorln(err)
continue
}
ips, err := pack.Options.IPs(dhcp4.OptDNSServers)
if err != nil {
glog.Errorln(err)
continue
}
return ips[0].String() // if len(ips) == 0, err should not be wrong size
}
return ""
}
type netIface struct {
*net.Interface
net.Addr
net.IP
}
func mustGetInterfaces() []*netIface {
ifaces, err := net.Interfaces()
if err != nil {
glog.Fatalln(err)
}
v4Iface := make([]*netIface, 0, len(ifaces))
for i := range ifaces {
if len(ifaces[i].HardwareAddr) == 0 {
continue
}
addrs, _ := ifaces[i].Addrs()
for _, addr := range addrs {
if ip := addr.(*net.IPNet).IP.To4(); ip != nil {
v4Iface = append(v4Iface, &netIface{&ifaces[i], addr, ip})
break
}
}
}
return v4Iface
}
-20
View File
@@ -1,20 +0,0 @@
package util
import "testing"
func TestGetDefaultDNSServer(t *testing.T) {
t.Skip("skip for some enviroment not support dhcp")
tests := []struct {
name string
}{{
"",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetDefaultDNSServer(); got == "" {
t.Errorf("GetDefaultDNSServer() return empty")
}
})
}
}
-131
View File
@@ -1,131 +0,0 @@
package util
import (
"bytes"
"crypto/tls"
"encoding/binary"
"io"
"net"
"time"
)
// HTTPPing try connect to a http(s) server with domain though the http addr
func HTTPPing(viaHost, domain string, port Port, timeout time.Duration) (err error) {
conn, err := net.DialTimeout("tcp", port.JoinAddr(viaHost), timeout)
if err != nil {
return err
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(timeout))
if _, err = conn.Write(port.pingMsg(domain)); err != nil {
return err
}
// err -> nil: read something succ
// err -> io.EOF: no such domain or connection refused
// err -> timeout: tcp package has been dropped
_, err = conn.Read(make([]byte, 1))
if err == io.EOF && viaHost == domain {
return nil
}
return err
}
// Port ==========================
type Port uint16
const (
Http Port = iota
Https
)
func (p Port) JoinAddr(addr string) string {
switch p {
case Http:
return addr + ":80"
case Https:
return addr + ":443"
default:
panic("invalid port")
}
}
func (p Port) pingMsg(domain string) []byte {
switch p {
case Http:
return []byte("TRACE / HTTP/1.1\r\nHost: " + domain + "\r\n\r\n")
case Https:
return NewClientHelloSNIMsg(domain)
default:
panic("invalid port")
}
}
// SNI ==========================
type clientHelloSNI struct {
ContentType uint8
Version uint16
Length uint16
handshakeProtocol
}
type handshakeProtocol struct {
HandshakeType uint8
LengthExpand uint8
Length uint16
Version uint16
Random [32]byte
SessionIDLength uint8
CipherSuitesLength uint16
CipherSuite uint16
CompressionMethodsLength uint8
CompressionMethod uint8
ExtensionsLength uint16
extensionServerName
}
type extensionServerName struct {
Type uint16
Length uint16
serverNameIndicationExtension
}
type serverNameIndicationExtension struct {
ServerNameListlength uint16
ServerNameType uint8
ServerNamelength uint16
// ServerName []byte // Disable for fix length
}
func NewClientHelloSNIMsg(domain string) []byte {
length := uint16(len(domain))
msg := &clientHelloSNI{
ContentType: 0x16, // Content Type: Handshake (22)
Version: 0x0301, // Version: TLS 1.0 (0x0301)
Length: length + 56,
handshakeProtocol: handshakeProtocol{
HandshakeType: 0x01, // Handshake Type: Client Hello (1)
Length: length + 52,
Version: 0x0303, // Version: TLS 1.2 (0x0303)
Random: [32]byte{}, // [32]byte{},
SessionIDLength: 0x0, // Session ID Length: 0
CipherSuitesLength: 2, // Cipher Suites Length: 84
CipherSuite: tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
CompressionMethodsLength: 1, // Compression Methods Length: 1
CompressionMethod: 0x00, // Compression null
ExtensionsLength: length + 9,
extensionServerName: extensionServerName{
Type: 0x0000, // Type: server_name (0)
Length: length + 5,
serverNameIndicationExtension: serverNameIndicationExtension{
ServerNameListlength: length + 3,
ServerNameType: 0x00, // Server Name Type: host_name (0)
ServerNamelength: length,
},
},
},
}
buf := bytes.NewBuffer(make([]byte, 0, length+61))
binary.Write(buf, binary.BigEndian, msg)
buf.WriteString(domain)
return buf.Bytes()
}
-52
View File
@@ -1,52 +0,0 @@
package util
import (
"sort"
"strings"
)
type ReverseSecSlice struct {
sort.StringSlice
}
func NewReverseSecSlice(a []string) *ReverseSecSlice {
return &ReverseSecSlice{sort.StringSlice(a)}
}
func (p *ReverseSecSlice) Sort() *ReverseSecSlice {
sort.Sort(p)
return p
}
func (p *ReverseSecSlice) Uniq() []string {
olds := []string(p.StringSlice)
last := ""
strs := make([]string, 0, len(olds))
for _, str := range olds {
if str != last {
strs = append(strs, str)
}
last = str
}
return strs
}
func (p *ReverseSecSlice) Less(i, j int) bool {
secsI := strings.Split(p.StringSlice[i], ".")
secsJ := strings.Split(p.StringSlice[j], ".")
lenI := len(secsI) - 1
lenJ := len(secsJ) - 1
length := lenI
if lenI > lenJ {
length = lenJ
}
for idx := 0; idx <= length; idx++ {
if secsI[lenI-idx] == secsJ[lenJ-idx] {
continue
}
return secsI[lenI-idx] < secsJ[lenJ-idx]
}
return lenI < lenJ
}
-65
View File
@@ -1,65 +0,0 @@
package util
import (
"sort"
"testing"
)
func TestReverseSecSlice_Less(t *testing.T) {
type fields struct {
StringSlice sort.StringSlice
}
type args struct {
i int
j int
}
slice := fields{sort.StringSlice([]string{
"a.b.c",
"a.b.c",
"d.b.c",
"d.a.b.c",
"",
"",
})}
tests := []struct {
name string
fields fields
args args
want bool
}{{
name: "equal",
fields: slice,
args: args{0, 1},
want: false,
}, {
name: "less",
fields: slice,
args: args{0, 2},
want: true,
}, {
name: "length",
fields: slice,
args: args{0, 3},
want: true,
}, {
name: "single_empty",
fields: slice,
args: args{0, 4},
want: false,
}, {
name: "all_empty",
fields: slice,
args: args{4, 5},
want: false,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := &ReverseSecSlice{
StringSlice: tt.fields.StringSlice,
}
if got := p.Less(tt.args.i, tt.args.j); got != tt.want {
t.Errorf("ReverseSecSlice.Less() = %v, want %v", got, tt.want)
}
})
}
}
-101
View File
@@ -1,101 +0,0 @@
package util
import (
"strings"
"sync"
)
type Node struct {
node
sep string
*sync.RWMutex
}
type node struct {
node map[string]*node
}
func NewNode(sep string) *Node {
return &Node{node{node: map[string]*node{}}, sep, &sync.RWMutex{}}
}
func NewNodeFromRules(sep string, rules ...string) *Node {
n := NewNode(sep)
for i := range rules {
n.Add(rules[i])
}
return n
}
func (n *Node) String() string {
n.RLock()
defer n.RUnlock()
return n.string("", " ")
}
func (n *node) string(prefix, indent string) (out string) {
for key, val := range n.node {
out += prefix + key + "\n" + val.string(prefix+indent, indent)
}
return
}
func (n *Node) trim(item string) string {
return strings.TrimSuffix(item, n.sep)
}
func (n *Node) Add(item string) {
n.Lock()
defer n.Unlock()
n.add(strings.Split(n.trim(item), n.sep))
}
func (n *node) add(secs []string) {
length := len(secs)
switch length {
case 0:
return
case 1:
n.node[secs[length-1]] = &node{node: map[string]*node{"": &node{}}}
default:
sec := secs[length-1]
if sec == "**" {
sec = "*"
}
subNode, ok := n.node[sec]
if !ok {
subNode = &node{node: map[string]*node{}}
n.node[sec] = subNode
}
subNode.add(secs[:length-1])
}
}
func (n *Node) Match(item string) bool {
return n.matchSecs(strings.Split(n.trim(item), n.sep), false)
}
func (n *node) matchSecs(secs []string, fuzzNode bool) bool {
length := len(secs)
if length == 0 {
if _, ok := n.node[""]; ok {
return true
}
if _, ok := n.node["**"]; ok {
return true
}
if _, ok := n.node["*"]; ok {
return !fuzzNode
}
return false
}
if n, ok := n.node[secs[length-1]]; ok {
return n.matchSecs(secs[:length-1], false)
}
if n, ok := n.node["*"]; ok {
return n.matchSecs(secs[:length-1], true)
}
if _, ok := n.node["**"]; ok {
return true
}
return false
}
-77
View File
@@ -1,77 +0,0 @@
package util
import (
"testing"
)
func TestNode_Match(t *testing.T) {
type test struct {
arg string
want bool
}
tests := []struct {
name string
node *Node
tests []test
}{{
"simple",
NewNodeFromRules(".", "a.wweir.cc", "b.wweir.cc"),
[]test{
{"a.wweir.cc", true},
{"b.wweir.cc", true},
},
}, {
"parent",
NewNodeFromRules(".", "wweir.cc", "a.wweir.cc"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"b.wweir.cc", false},
},
}, {
"fuzz1",
NewNodeFromRules(".", "wweir.cc", "a.wweir.cc", "*.wweir.cc"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"b.wweir.cc", true},
{"a.b.wweir.cc", false},
},
}, {
"fuzz2",
NewNodeFromRules(".", "a.*.cc"),
[]test{
{"wweir.cc", false},
{"a.wweir.cc", true},
{"b.wweir.cc", false},
},
}, {
"fuzz3",
NewNodeFromRules(".", "*.*.cc", "iamp.*.*"),
[]test{
{"wweir.cc", false},
{"a.wweir.cc", true},
{"b.wweir.cc", true},
{"iamp.wweir.cc", true},
},
}, {
"fuzz4",
NewNodeFromRules(".", "**.cc", "a.**.com"),
[]test{
{"wweir.cc", true},
{"a.wweir.cc", true},
{"a.b.wweir.cc", true},
{"a.wweir.com", true},
{"b.wweir.com", false},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
for _, test := range tt.tests {
if got := tt.node.Match(test.arg); got != test.want {
t.Errorf("Node.Match(%s) = %v, want %v", test.arg, got, test.want)
}
}
})
}
}
-37
View File
@@ -1,37 +0,0 @@
package util
import (
"net"
)
type TeeConn struct {
net.Conn
buf []byte
offset int
tee bool // read
}
func (t *TeeConn) StartOrReset() {
t.offset = 0
t.tee = true
}
func (t *TeeConn) Stop() {
t.offset = 0
t.tee = false
}
func (t *TeeConn) Read(b []byte) (n int, err error) {
length := len(t.buf) - t.offset
if length > 0 {
n = copy(b, t.buf[t.offset:])
t.offset += n
return
}
n, err = t.Conn.Read(b)
if t.tee {
t.buf = append(t.buf, b[:n]...)
t.offset += n
}
return n, err
}
-23
View File
@@ -1,23 +0,0 @@
package util
import (
"errors"
"time"
)
func WithTimeout(fn func() error, timeout time.Duration) error {
var okCh = make(chan struct{})
var err error
go func() {
err = fn()
close(okCh)
}()
select {
case <-okCh:
return err
case <-time.After(timeout):
return errors.New("timeout: " + timeout.String())
}
}